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,_sealand 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,historyand 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.