Skip to content
C.W.K.
Lesson 01 of 05 · published

Templates and Primitives

~15 min · template-method, abstract-base-class, inheritance, python

Level 0Loose Note
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

One Class, Two Kinds of Method

Every soul vault implementation the family will ever have inherits from one Python class, SoulVault in soulvault/root.py. Its methods come in exactly two kinds, and the whole design rests on keeping them apart.

  • Primitives are the abstract methods whose names start with an underscore: _read_bytes, _stat, _list_paths, _exclusive, _apply, _history, _seal and so on, twenty in all. They touch storage and transport and nothing else. A child implements these, and only these.
  • Template methods are every public method: read_note, write_note, write_batch, load_identity, create_soul, seal, history and the rest. They hold the rules every backend obeys identically: which note ids are legal, the order identity documents load in, compare-and-swap, which capabilities a call needs, which errors may escape. A child never overrides one.

This is the classic template method pattern, and the family did not discover it for Soul Vault. The root's own docstring says the shape is "the same one cwkPippa's brain adapters use": the Claude adapter set the canonical behaviour, and every other brain adapter pays its own translation cost underneath without changing what callers see. Dad's version of the rule is about who pays: the root is never edited to fit a child; each child pays its own polymorphic tax downstream.

What That Buys

Look at write_note. It is three lines: build one Change and hand it to write_batch. write_batch is where the rules are: it insists on an actor and a request id, refuses an empty batch, refuses a batch that spans two vaults or names a note twice, takes the vault's lock through the child's _exclusive, checks whether this request already completed, refuses to delete the note that identifies a soul, compares every expected revision with the current bytes, and only then calls the child's _apply. The child's _apply just has to make the bytes real.

So when someone writes a third implementation, say a store on a NAS, the list of things they can get wrong is short: read bytes, list paths, hold a lock, apply a batch atomically, keep history. They cannot get compare-and-swap wrong, because they never write it. They cannot forget the identity-note rule, because it is not theirs to remember.

A Child Declares, It Does Not Decide

Some behaviour genuinely differs between stores. A folder has no history; a git-backed store does. The root handles that with declared capabilities, not with overrides. A child answers _declare_capabilities() with six booleans (durable_history, replayable_events, durable_idempotency, offline_reads, atomic_batch, remote_writes), and the template decides what that means. A call that needs history on a store without it raises unsupported_capability; the root's comment is blunt about why: never an empty pretend answer. An empty list of revisions would look like "no history yet", which is a lie.

Standard Library, Python 3.9

One constraint shapes the whole package: it is standard-library-only and Python 3.9-compatible, and the soul-vault client runs under Apple's /usr/bin/python3. That is not nostalgia. Identity has to load in every harness on every Mac, including a fresh one where no Conda environment exists yet. A dependency would be one more thing that must be installed before a session can know who it is. A test in the suite drives the client under /usr/bin/python3 so the promise is checked, not hoped.

Code

The shape of the root (trimmed from soulvault/root.py)·python
class SoulVault(ABC):
    # ---- primitives: a child implements these and nothing else ----
    @abstractmethod
    def _read_bytes(self, note: NoteId) -> Optional[bytes]: ...

    @abstractmethod
    @contextmanager
    def _exclusive(self, vault: str) -> Iterator[None]: ...

    @abstractmethod
    def _apply(self, changes, actor, request_id, reason) -> Dict[str, str]: ...

    # ---- template methods: the rules, the same for every backend ----
    def write_note(self, note, text, *, expected_revision, actor, request_id,
                   create=False, reason=""):
        change = Change("put", note, text=text, expected_revision=expected_revision,
                        must_exist=not create)
        return self.write_batch([change], actor=actor, request_id=request_id, reason=reason)

    def history(self, note, limit=50):
        self._require("durable_history", "history")   # declared, not overridden
        return self._history(self._checked(note), limit)
A toy child: all it may do is touch storage·python
class DictVault(SoulVault):
    """An in-memory store for tests. Twenty primitives, zero rules."""
    backend = "dict"

    def __init__(self):
        self.files, self.lock = {}, threading.Lock()

    def _declare_capabilities(self):
        return Capabilities(durable_history=False, replayable_events=False,
                            durable_idempotency=False, offline_reads=False,
                            atomic_batch=True, remote_writes=False)

    def _read_bytes(self, note):
        return self.files.get(note.uri)

    @contextmanager
    def _exclusive(self, vault):
        with self.lock:
            yield

    def _apply(self, changes, actor, request_id, reason):
        # CAS already happened in the template. Prepare every change first,
        # so a failure here leaves nothing published (atomic_batch=True).
        staged = {c.note.uri: None if c.op == "delete" else c.text.encode() for c in changes}
        for uri, data in staged.items():   # publish: plain dict stores cannot fail halfway
            if data is None:
                self.files.pop(uri, None)
            else:
                self.files[uri] = data
        return {uri: hashlib.sha256(data).hexdigest()
                for uri, data in staged.items() if data is not None}
    # ... the remaining primitives (list, stat, history, seal, ...) follow the same idea

External links

Exercise

Take any storage-facing class in your own code that has more than one implementation (a cache, a file store, an API client with a mock). Split its methods into primitives (only touch storage or transport) and templates (hold rules). Move one rule that is currently duplicated in each implementation, such as input validation or a conflict check, up into a template method, and make the implementations implement only primitives.
Hint
A good test: could a new implementation get this rule wrong? If yes, the rule is in the wrong place. Rules belong where no child can reach them; children should only be able to get storage wrong.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.