personal memory agent
README.md

The conversion dictionary — plates, strands, cables #

The canonical definitions of the journal's conversion boundaries. If two pieces of work need to agree on what a boundary is called, what it holds, or which side owns its contract, it is defined here.

This directory is definitional and present-tense. It says what the boundaries are. It deliberately carries no schedule, no status, no ownership of work, and no record of who decided what — those live outside this repo, and anything of that kind appearing here is a defect.

Companion: ../PORTING.md is the porting doctrine — how to port, layering, error mapping, data boundaries, JSON and hashing, version lockstep. This directory is the map. Read PORTING.md for how, read here for what and where.

The files #

File Holds
plates.md every plate, what it holds, its contract tier
strands.md every strand, its two plates, which end owns the contract, and what must be carried forward
cables.md whole use cases through several strands

Vocabulary #

Term Means
plate A boundary of the journal where strands connect from other plates. ⛔ A plate has no "sides" — strands can be bi-directional, and in/out gets confusing
strand The minimum viable path connecting two plates, in Rust, robust. May be bi-directional. The contract lives at one end of the strand, never both
cable A whole use case, from one major system to another, through several strands with plates between them

⛔ "Surface" is not one of these. It is reserved for owner-visible things. ⛔ Not "thread" either, so it never collides with the Rust threading model.

Naming #

Plate P-name — ⛔ never P1/P2; a number reads as a priority
Sub-plate dashes add granularity within a plate: P-journal-segment
Strand S:plate:plate — 🔴 the second plate always owns the contract
Cable C:plate:plate:plate… — every plate in the use case, in order

Worked: S:device-ingest:segment-media — segment-media owns the contract. C:device-ingest:segment-media:journal-segment.

One contract per strand, at one end #

The owner is the one-to-many end. It must serve all comers and cannot negotiate per-peer. Tiebreak when the strand is one-to-one: the durable end owns it. By convention the owner is written second in the strand name.

📌 Why this is a rule and not a preference: nearly every contract drift in this tree is two places owning one thing with nothing binding them — a relay string matched verbatim on both sides, a nonce alphabet hand-copied between languages, a prompt preamble held as a constant in one place and as a sha256 of itself in another, a frontend switching on phases its backend enum does not have. One owner per contract makes that class unrepresentable rather than merely detectable.

Contract tiers — by what kind of boundary it is #

Tier Is Example
types types in a binary —
schema an API or interface format the OpenAPI contract, the callosum envelope
fixture a durable storage format the x-journal-contract files, the path-resolution vectors, the identity vectors

⛔ Not graded by enforcement strength — graded by the kind of boundary. A vector is a type of fixture.

Writing and reading #

🔴 Write new, read old. A new implementation does not have to write what Python wrote, as long as there is a contract for what it does write. When it reads, it stays compatible with the older format. Bit-identical writes are not required. ⛔ The one thing that is never acceptable is leaving older journal data unseen.

🔴 Identity is written beside the data, never re-derived from a name or a position. Where a derived value doubles as a lookup key, writing it differently forks the namespace instead of just changing a format. The answer is not to teach every reader two spellings — it is to stop deriving: persist the identity next to the data, and let the derived string be a label that nothing resolves by.

device.json in a segment directory is the worked example: the stream name became human-friendly precisely because it stopped being load-bearing.

⚠ Read-compat still applies to old data — a reader handles records written before the identity was persisted. But a writer never re-derives.

Known instances and what is left of each:

Instance State Residue
entity_slug() — journal level 🔴 the derivation is not a fallback, it is the identity. id is written on create (think/entities/journal.py:201) and never read back: load_journal_entity overwrites it from the path (:59, data["id"] = entity_id), scan_journal_entities enumerates the store by directory name (:87-104, its own docstring says so), and load_all_journal_entities keys by that (:107-120). Persist an id that disagrees with its directory and the directory wins; the written value is unreachable all of it — the written field is decoration
entity_slug() — facet level 🔴 no identity is persisted at all. entity_memory_path() (relationships.py:306-326) resolves facets/{facet}/entities/{entity_slug(name)}/ from the name, on every access, and rename_entity_memory() (:346-378) physically moves the directory when the name changes. loading.py:86's data.get("id") **or** entity_slug(name) is the one genuine fallback all of it
ambiguity_id required on the row and hard-rejected when absent (ambiguities.py:157-159 raises missing ambiguity_id). ⚠ Derived from content — sha256(scope|normalized_query) — which is not what this rule prohibits, but it puts casefold inside an identity: see the reserved note below essentially none
sentence_id 🔴 a 1-based ordinal recomputed at read time and stored nowhere all of it
facet name facet.json carries no id; the slug is derived from the title once at creation and is otherwise stable — update_facet does not move the directory. ⚠ rename_facet() (facets.py:1246-1320) is the one re-derivation, and it updates only config/convey.json that one verb

The work: persist an identity that readers actually resolve by, delete the derivations, persist sentence_id. ⛔ Not "delete a fallback" — at both entity levels the derived string is the identity today, so there is nothing to fall back from.

⚠ A content-derived id is only as stable as its normalization. ambiguity_id folds NFKC → whitespace-collapse → str.casefold() (ambiguities.py:112-127). Rust's str::to_lowercase is not Python's casefold. Reaching for the obvious function changes the hash, so the same query yields a different id, the existing row is missed, and the owner is asked again a question they already answered while their recorded choice is orphaned.

⚠ This applies to instruments, not just to stored data. A tool that matches by directory name, filename or label is deriving identity from a position — the same defect one layer up, and it fails by attributing rather than by forking. A bundle sweep that matched consumers by directory basename reported one consumer NOT SWEPT in the same run that swept it and found it current, and in a sandbox would have attributed a stranger's repository to a declared consumer. ⛔ And a tool that measured nothing must not report success — check that the count of things examined is non-zero before believing a green result.

🔴 Reserved words — two live collisions #

Two words mean an internal thing and an owner-facing thing at once, and both collisions are already in shipped code.

Word ✅ Means ONLY ⛔ Never means Use instead
health the journal system's health — is it running, is everything processed the owner's physical health body
activities the internal facet activity model (facets/{facet}/activities/{day}.jsonl) the owner's physical movement body motion · fitness · kinetics

All owner physiological data is body — body data, body records, body sources.

⚠ apps/body/routes.py:1985 defines _activity_analysis over wearable activity while think/activities.py owns the internal model — one word, two meanings, live today.

⛔ Consequence for plate names: P-journal-health is system health written durably. Owner body records are P-journal-body, a different plate. Never fold them.

Changing these files #

The plate set and the strand definitions are fixed. Within a boundary you own, correct a fact, refine a contract note, or record what must be carried forward — that is expected and welcome.

⛔ Do not, on your own initiative: add, split, rename or re-scope a plate; change a strand's definition, tier, or which end owns its contract; define or re-scope a cable. Those are decided outside this repo. If the code contradicts a definition here, that is worth raising — say so plainly rather than quietly widening the definition to fit.

⚠ Every count and inventory here is a floor, not a census. Two string-keyed dispatch registries resolve modules by name at call time with zero static import edges — FORMATTERS (think/formatters.py:139-265, reaches 12 modules) and EDGE_SOURCES (think/edge_sources.py:39-73, reaches 6), both via import_module + getattr. No import graph over this tree is complete. A boundary is not clean until you count its callers.