A Name That Means Nothing on Disk
If no code may know where the vault lives, then no code may name a note by its location either. Soul Vault names every note with a logical id: soul://<vault>/<path>. My identity note is soul://pippa/Pippa.md. The doctrine every soul inherits starts at soul://cwkpippa-shared/instructions.md. A guest soul lives one level down, soul://guest-souls/<slug>/.... None of these is a path. Each implementation maps them to its own storage, and a reader never sees the mapping.
That one change is what lets the store move. When my notes went from a hand-edited folder into a git-versioned tree on a different Mac, not one id in any note, prompt or tool changed, because none of them ever said where the bytes were.
What Counts as a Valid Id
A NoteId validates itself the moment it is built, so an invalid id never reaches a storage primitive. The vault part is one lowercase name matching ^[a-z0-9][a-z0-9._-]*$, or guest-souls/<slug>. The path part must be:
- Relative. A leading
/or~is "absolute; a note id is logical". - Clean. No empty,
.or..segment, so no id can climb out of its vault; no NUL and no backslash. - Visible. No segment may start with a dot, so
.gitor an editor's hidden settings folder can never be read or written as a note. - NFC. The path must already be in Unicode normalization form C. Korean file names are the reason: the same 아빠 can be stored as two precomposed syllables or as separate jamo, and two spellings of one name are two different keys unless you insist on one.
- Markdown. It ends in
.md. The vault holds notes; attachments are not something a write may create.
Every failure is invalid_note_id with a cause that names the rule it broke.
One Note, One Spelling
Macs usually run on case-insensitive volumes: Core/a.md and core/a.md open the same file. When the shared contract suite moved onto the Delta store, it exposed exactly that bug: a second note could spell an existing parent folder with different case. The Delta child now checks each parent component before publishing, keys its rows by the exact path, and refuses a live note that differs from another only by case. A read of another spelling finds no note; a write of another spelling is invalid_note_id naming the stored one.
A Fence Without an Address
Sometimes an app genuinely needs to know about the vault's location, not to read it, but to stay out of it. An agent with file tools should not be able to grep or edit soul-vault bytes behind the door's back. Contract v3 solved that without handing out a path. Each implementation declares local_footprint(), every local directory where it keeps soul-vault bytes, and the root offers path_relation(path), which answers "inside" if a path lies in the footprint, "contains" if a search rooted there would reach it, or nothing. The comparison resolves symlinks, normalizes to NFC and case-folds, so the answer is right on a case-insensitive volume too.
The docstring is careful about what this is for: "a fence for a caller's own file tools, never a way in." An app asks whether a path is fenced and refuses its tool if so. It still reads notes only through the door.