Skip to content

Contracts

1. What the package is

@memhtml/contracts is the innermost workspace package: it imports only effect, and every other package imports it. It holds the closed vocabularies, the path algebra, the edge model, and the typed failures — the words the rest of the system agrees on. Closed vocabularies lists the vocabularies’ values; this page lists every exported symbol, 79 across 6 modules, as the source documents it.

Module Exports What it holds
edges.ts 22 The four non-mixing edge classes. The class is what keeps a person or task edge out of PageRank, MMR, and the retention bridge count. Every memory-graph query filters edge_class = 'memory', and the SQL CHECK constraint refuses a rel that belongs to another class.
errors.ts 8 A driver or filesystem rejection, reduced to the operation that failed. The payload deliberately excludes SQL text, parameters, and row contents so a storage error can be returned to an agent without leaking corpus content; the driver’s own message goes to Effect.logError at the adapter edge instead.
guidance.ts 3 The recall discipline: how an agent uses a memory store well, stated once.
paths.ts 17 Path algebra. Every function here is pure and total, and a path is always the repo-root-relative git-tree form: no leading slash, forward slashes only.
slug.ts 9 Slug rules. A slug is the filename stem and the path is the id. There is no uuid anywhere in the system, so the slug carries the whole burden of being stable, readable, and filesystem-safe on every platform git runs on.
types.ts 20 The memory type vocabulary, closed. Ten values, restated by the files.memory_type CHECK constraint in SQL.

2. By module

Each table row is one exported name: what kind of thing it is (a name declared as both a schema value and its type is one row), its declaration’s own first line, and the first paragraph of its doc comment. A symbol whose comment runs longer is quoted in full beneath the table.

2.1. edges.ts

The four non-mixing edge classes. The class is what keeps a person or task edge out of PageRank, MMR, and the retention bridge count. Every memory-graph query filters edge_class = 'memory', and the SQL CHECK constraint refuses a rel that belongs to another class.

Symbol Kind Declaration Summary
EDGE_CLASSES constant const EDGE_CLASSES = ["memory", "person", "provenance", "task"] as const The four non-mixing edge classes. The class is what keeps a person or task edge out of PageRank, MMR, and the retention bridge count. Every memory-graph query filters edge_class = 'memory', and the SQL CHECK constraint refuses a rel that belongs to another class.
EdgeClass schema, type const EdgeClass = Schema.Literals(EDGE_CLASSES) undocumented
MEMORY_RELS constant const MEMORY_RELS = [ … The nine memory rels. supersedes and contradicts are penalty-bearing: they gate the retention contested_status signal, so sleep promotes a corroborated one into both files rather than leaving it in the rebuildable index.
MemoryRel schema, type const MemoryRel = Schema.Literals(MEMORY_RELS) undocumented
PERSON_RELS constant const PERSON_RELS = ["about_person", "authored_by"] as const The two person rels, pointing at resources/people/*.
PersonRel schema, type const PersonRel = Schema.Literals(PERSON_RELS) undocumented
PROVENANCE_RELS constant const PROVENANCE_RELS = ["from_session"] as const The one provenance rel, linking a memory to the session that produced it.
ProvenanceRel schema, type const ProvenanceRel = Schema.Literals(PROVENANCE_RELS) undocumented
TASK_RELS constant const TASK_RELS = ["blocks", "subtask_of"] as const The two task rels, both between two task files.
TaskRel schema, type const TaskRel = Schema.Literals(TASK_RELS) undocumented
ALL_RELS constant const ALL_RELS = [...MEMORY_RELS, ...PERSON_RELS, ...PROVENANCE_RELS, ...TASK_RELS] as const Every rel across all four classes. The edges.rel column’s full vocabulary.
EdgeRel schema, type const EdgeRel = Schema.Literals(ALL_RELS) undocumented
relClassFor function const relClassFor = (rel: EdgeRel): EdgeClass => … The class a rel belongs to. Total over ALL_RELS and injective per class: a rel name appears in exactly one class, which is what lets the class be derived rather than carried alongside the rel and risk disagreeing with it.
relsForClass function const relsForClass = (edgeClass: EdgeClass): ReadonlyArray<EdgeRel> => … The rels of one class. The inverse of relClassFor, as a set.
isEdgeRel function const isEdgeRel = (rel: string): rel is EdgeRel => … True when rel is in the closed vocabulary. Narrows an untrusted string.
EDGE_PROVENANCES constant const EDGE_PROVENANCES = ["authored", "sleep", "import"] as const Where an edge came from. derived edges are only ever sleep-provenanced.
EdgeProvenance schema, type const EdgeProvenance = Schema.Literals(EDGE_PROVENANCES) undocumented
REL_TOKEN_PREFIX constant const REL_TOKEN_PREFIX = "memhtml-" A <link rel> token, which is the rel prefixed for the HTML plane. rel tokens cannot hold a colon, so the prefix is hyphenated and the rel’s own underscores become hyphens: laterally_related ⇒ memhtml-laterally-related.
relTokenFor function const relTokenFor = (rel: EdgeRel): string => … The HTML <link rel> token for a rel.
relForToken function `const relForToken = (token: string): EdgeRel undefined => …`
Edge schema, type const Edge = Schema.Struct( … One edge. derived separates a sleep-mined suspicion from an authored assertion: the retention contested_status signal counts only derived: false contradictions, so an uncorroborated machine guess can never evict a memory.
isWellFormedEdge function const isWellFormedEdge = (edge: Edge): boolean => … True when an edge’s declared class matches its rel and it is not a self-loop, the two conditions the edges table’s CHECK constraints enforce, stated once here so a caller can refuse a bad edge before the driver does.

TASK_RELS

The two task rels, both between two task files.

Their own class for the same reason the person rels have one: task topology is working state, and a blocks edge entering PageRank would let an agent’s to-do list reweight the retention of its knowledge. @memhtml/store’s linkMemories refuses a task rel unless BOTH endpoints are tasks, and the edges CHECK refuses the rel under any other class.

Edge

One edge. derived separates a sleep-mined suspicion from an authored assertion: the retention contested_status signal counts only derived: false contradictions, so an uncorroborated machine guess can never evict a memory.

strength is unitless in [0, 1]; an authored edge is 1.0 and a mined one carries its cosine. srcPath/dstPath are repo-root-relative with no leading slash.

2.2. errors.ts

A driver or filesystem rejection, reduced to the operation that failed. The payload deliberately excludes SQL text, parameters, and row contents so a storage error can be returned to an agent without leaking corpus content; the driver’s own message goes to Effect.logError at the adapter edge instead.

Symbol Kind Declaration Summary
StorageFailure class class StorageFailure extends Schema.TaggedError<StorageFailure>()("StorageFailure", … A driver or filesystem rejection, reduced to the operation that failed. The payload deliberately excludes SQL text, parameters, and row contents so a storage error can be returned to an agent without leaking corpus content; the driver’s own message goes to Effect.logError at the adapter edge instead.
WriteConflict class class WriteConflict extends Schema.TaggedError<WriteConflict>()("WriteConflict", … Two writers touched the same file. ourSha is the blob sha this process wrote from, theirSha the blob sha now in the tree. Recovery belongs to the caller: re-read the current content and reapply.
ModelUnavailable class class ModelUnavailable extends Schema.TaggedError<ModelUnavailable>()("ModelUnavailable", … Bedrock refused the call: throttling, an unavailable model, or a denied region.
InvalidMemory class class InvalidMemory extends Schema.TaggedError<InvalidMemory>()("InvalidMemory", … A memory that violates the file format or the type/placement vocabulary.
PathNotFound class class PathNotFound extends Schema.TaggedError<PathNotFound>()("PathNotFound", … A repo-root-relative path with no file behind it.
DuplicateContent class class DuplicateContent extends Schema.TaggedError<DuplicateContent>()("DuplicateContent", … The content hash already belongs to an active file. existingPath is what the caller wanted to create, so a deduped write is answerable without a second query.
DirtyTree class class DirtyTree extends Schema.TaggedError<DirtyTree>()("DirtyTree", … An operation that requires a clean tree found uncommitted changes.
LlmContractViolation class class LlmContractViolation extends Schema.TaggedError<LlmContractViolation>()( … The model broke its structured-output contract: an undecodable tool payload, a max_tokens stop, or a refusal. The item is reported with no result, and a violation does not become a value.

2.3. guidance.ts

The recall discipline: how an agent uses a memory store well, stated once.

The CLI’s GUIDE teaches the mechanics of the binary (the envelope, the three write doors, when to batch, the authoring rule). The MCP tool descriptions teach each tool’s arguments. Neither says WHEN to recall, how to treat a hit, when to write, or how to cite, and an agent that has only the mechanics answers from a gap it could have searched. This module is that missing layer, and it is in @memhtml/contracts so that every surface renders the same words: the manifest’s recall-discipline guide topic, the fenced block memhtml integrations install writes into a host’s instruction file, the skill it installs, and the pointer the retrieval tools carry.

The text names both doors where they differ (memory_search on MCP, memhtml search on the CLI) so the same paragraph is correct whichever surface the reader is on. Written in the second person to an agent mid-task: action first, every claim true of this build.

Symbol Kind Declaration Summary
RECALL_DISCIPLINE constant const RECALL_DISCIPLINE: ReadonlyArray<string> = [ … One paragraph per rule, in the order an agent meets them: recall, read, cite, reinforce, write.
RECALL_DISCIPLINE_TEXT constant const RECALL_DISCIPLINE_TEXT = RECALL_DISCIPLINE.join("\n\n") The discipline as one string, paragraphs joined by a blank line: the form a guide topic renders.
RECALL_DISCIPLINE_SHORT constant const RECALL_DISCIPLINE_SHORT … Two sentences for a surface that cannot carry the paragraphs, appended to the retrieval tools’ descriptions so an MCP-only agent still meets the two rules that most often go wrong.

2.4. paths.ts

Path algebra. Every function here is pure and total, and a path is always the repo-root-relative git-tree form: no leading slash, forward slashes only.

Symbol Kind Declaration Summary
ARCS_DIR constant const ARCS_DIR = "areas/arcs" Behavioral arcs. Under areas/ because PARA is fixed at four buckets.
PEOPLE_DIR constant const PEOPLE_DIR = "resources/people" The person plane, folded into resources/ rather than given its own bucket.
INBOX_DIR constant const INBOX_DIR = "areas/inbox" Where a memory lands when no rule claims it. memhtml doctor reports inbox depth as a health signal, so an unplaceable memory is visible rather than lost.
TASKS_SUBDIR constant const TASKS_SUBDIR = "tasks" The directory segment every task file sits under, appended to its workspace’s project directory or to the inbox.
ARCHIVE_BUCKET constant const ARCHIVE_BUCKET = "archive" The bucket eviction moves into, partitioned by year.
MEMORY_EXTENSION constant const MEMORY_EXTENSION = ".html" The file extension every memory carries.
normalizePath function const normalizePath = (path: string): string => … Reduce a caller-supplied path to the canonical git-tree form: leading slashes dropped (callers may pass the <link href> document-reference form), repeated slashes collapsed, trailing slash dropped.
paraBucketOf function `const paraBucketOf = (path: string): ParaBucket undefined => …`
memoryPathViolation function `const memoryPathViolation = (path: string): string undefined => …`
isValidMemoryPath function const isValidMemoryPath = (path: string): boolean => memoryPathViolation(path) === undefined True when a path is a usable memory path: rooted in a PARA bucket, ending in .html, carrying no . or .. segment.
PlacementInput interface interface PlacementInput … What placementFor decides from. path is the caller’s explicit override.
placementFor function const placementFor = (input: PlacementInput): string => … The directory a memory belongs in, following design §2.1’s six rules in order, with the task rule sitting between the arc rule and the person rule. Total: it always returns a directory rooted in a PARA bucket, so the write path never guesses twice and never fails.
memoryPathFor function const memoryPathFor = ( … The full path for a new memory. An explicit valid path is authoritative and returned verbatim in canonical form; otherwise the directory comes from placementFor and the filename from filenameFor, which date-prefixes an episodic entry.
archivePathFor function const archivePathFor = (path: string, year: number): string => … The archive path a memory moves to on eviction: archive/<YYYY>/<original-path>, with the original path mirrored exactly beneath the year.
originalPathFor function `const originalPathFor = (archivePath: string): string undefined => …`
isArchivePath function const isArchivePath = (path: string): boolean => originalPathFor(path) !== undefined True when a path sits under the archive bucket with a year partition.
archiveYearOf function `const archiveYearOf = (path: string): number undefined => …`

TASKS_SUBDIR

The directory segment every task file sits under, appended to its workspace’s project directory or to the inbox.

A subdirectory rather than a fifth bucket: PARA is fixed at four, and a task belongs to whatever the memory beside it belongs to. Keeping tasks in one named segment is what makes ls projects/<slug>/tasks the list operation, which is the design’s CRUDL-without-retrieval contract. The segment name is therefore part of the contract.

normalizePath

Reduce a caller-supplied path to the canonical git-tree form: leading slashes dropped (callers may pass the <link href> document-reference form), repeated slashes collapsed, trailing slash dropped.

The trailing slash is removed by endsWith/slice rather than by /\/+$/, which is not a style choice: an unanchored-left \/+$ is quadratic on a long run of slashes followed by a non-slash, measured 2026-08-18 at 4 ms / 57 ms / 769 ms / 3049 ms for n = 2k / 8k / 32k / 64k. The collapse above happens to defuse it — after it, no run of two slashes survives, so the pattern can only ever match one character — but that made the cost of this function depend on the ORDER of three chained calls, with nothing stating it and nothing checking it. Reordering them, or dropping the collapse, would reintroduce a multi-second stall on 64 KB of input across this function’s callers, all of which sit on the write path that accepts agent-supplied paths. A single-character slice cannot be reordered into a hazard. packages/contracts/tests/paths.test.ts asserts the cost curve at adversarial sizes.

memoryPathViolation

Why a path is not a usable memory path, or undefined when it is one.

The RULE and its explanation in one function, because a refusal that restated the rule in its own words would be a second copy of it, free to name a clause this function does not check. A caller that refuses an unusable path quotes this string; isValidMemoryPath is the same question asked as a boolean.

Each clause names the input it saw rather than the rule in the abstract, so a caller holding the message can act without re-reading the format doc. The traversal clause is the security one: it is what keeps a caller-supplied path from escaping the memory repo.

isValidMemoryPath

True when a path is a usable memory path: rooted in a PARA bucket, ending in .html, carrying no . or .. segment.

memoryPathViolation asked as a boolean, so the predicate and the refusal’s reason cannot disagree about which paths are usable.

PlacementInput

What placementFor decides from. path is the caller’s explicit override.

Every optional field also admits undefined explicitly. Under exactOptionalPropertyTypes a bare path?: string would refuse placementFor({ path: params.path, … }) for a string | undefined param, forcing every tool adapter to strip absent fields by hand. The contract therefore accepts the shape the MCP and CLI layers actually hold.

placementFor

The directory a memory belongs in, following design §2.1’s six rules in order, with the task rule sitting between the arc rule and the person rule. Total: it always returns a directory rooted in a PARA bucket, so the write path never guesses twice and never fails.

Returns the directory, not the full path, because the filename needs a title this input does not carry. memoryPathFor composes the two. An explicit path contributes its directory; when that path is unusable it is ignored rather than propagated, so the return stays a valid bucket and this function stays total.

Totality is why the refusal cannot live here. A caller that wants an unusable path refused rather than re-derived asks the store for it (strictPath on a WriteInput), which gates on memoryPathViolation before any of this runs and quotes its reason. Refusing here instead would make placement fallible for every caller, including the sleep phases that place a synthesized arc and have no caller path to be wrong about.

archivePathFor

The archive path a memory moves to on eviction: archive/<YYYY>/<original-path>, with the original path mirrored exactly beneath the year.

year is a calendar year (a label, not an offset). Mirroring the whole original path is what makes the mapping injective and invertible, so git log --follow reads through the move and diff -M reports it as R100 rather than a delete plus an add.

2.5. slug.ts

Slug rules. A slug is the filename stem and the path is the id. There is no uuid anywhere in the system, so the slug carries the whole burden of being stable, readable, and filesystem-safe on every platform git runs on.

Symbol Kind Declaration Summary
SLUG_MAX_LENGTH constant const SLUG_MAX_LENGTH = 80 Maximum slug length in characters, before any collision suffix.
SLUG_FALLBACK constant const SLUG_FALLBACK = "untitled" The stem used when a title reduces to nothing sluggable, such as an all-punctuation or non-Latin title. A placeholder beats an empty filename, and memhtml doctor can find these by name.
slugify function const slugify = (title: string): string => … Kebab-case a title into [a-z0-9-], at most SLUG_MAX_LENGTH characters.
isSlug function const isSlug = (value: string): boolean => … True when a string is already a valid slug, the fixed point of slugify.
withCollisionOrdinal function const withCollisionOrdinal = (slug: string, ordinal: number): string => … Append a collision suffix. ordinal is a 1-based ordinal for display in the filename; ordinal 1 is the unsuffixed slug, 2 becomes -2, and so on, matching the -2/-3 convention. The suffix is added inside the length budget, so a maximum-length slug is shortened rather than overflowed.
EPISODIC_PREFIX_LENGTH constant const EPISODIC_PREFIX_LENGTH = 9 The YYYYMMDD- prefix an episodic filename carries. Time is part of an episodic entry’s identity and it never receives a correction in place, so the date belongs in the name; every other type is timeless and correctable, so it gets a bare slug.
datePrefix function const datePrefix = (at: Date): string => … Format an instant as the YYYYMMDD stamp of an episodic filename, in UTC.
filenameFor function const filenameFor = (input: … The filename for a memory: 20260802-slug.html for episodic, slug.html otherwise. The date prefix sits outside the slug’s length budget, because it is identity, not title.
hasDatePrefix function const hasDatePrefix = (filename: string): boolean => /^\d{8}-/.test(filename) True when a filename carries the YYYYMMDD- episodic prefix.

slugify

Kebab-case a title into [a-z0-9-], at most SLUG_MAX_LENGTH characters.

Diacritics are folded to their base letters (déployé ⇒ deploye) rather than dropped, so a title stays recognizable. Runs of separators collapse to one hyphen and the result carries no leading or trailing hyphen, which makes the function idempotent: a slug fed back in comes out unchanged.

Truncation cuts at SLUG_MAX_LENGTH and then trims any hyphen the cut exposed, so a truncated slug is still a valid slug rather than one ending mid-separator.

withCollisionOrdinal

Append a collision suffix. ordinal is a 1-based ordinal for display in the filename; ordinal 1 is the unsuffixed slug, 2 becomes -2, and so on, matching the -2/-3 convention. The suffix is added inside the length budget, so a maximum-length slug is shortened rather than overflowed.

The result never equals the input, at any slug length. That is what makes the store’s collision loop (packages/store/src/store.ts, pathFor) terminate rather than re-propose the name that collided. It is not free near the length cap: for a slug whose own tail IS the suffix, cutting to make room and appending the suffix can rebuild the slug — and because the cut also trims any hyphen it exposes, the rebuild can recur at MORE than one cut width. The stem is therefore re-cut from its own post-trim length until appending the suffix no longer reproduces the input; each re-cut strictly shortens the stem, so the loop terminates and the result stays inside the budget.

2.6. types.ts

The memory type vocabulary, closed. Ten values, restated by the files.memory_type CHECK constraint in SQL.

arc is in the vocabulary but absent from WRITABLE_MEMORY_TYPES: an arc is synthesized by the sleep cycle from many memories, so an agent naming one directly would be asserting a conclusion the corpus has not yet earned.

task is ONE axis with the other nine rather than a parallel kind column, because three overlapping type vocabularies is what made the predecessor memory system’s classification unanswerable. A task is a memory type whose retrieval, dedup, and curation treatment a filter states, not a second axis. Tasks are default-excluded from search and skipped by sleep. See @memhtml/index‘s assembleScope and the sleep phases’ excludeTypes.

Symbol Kind Declaration Summary
MEMORY_TYPES constant const MEMORY_TYPES = [ … The memory type vocabulary, closed. Ten values, restated by the files.memory_type CHECK constraint in SQL.
MemoryType schema, type const MemoryType = Schema.Literals(MEMORY_TYPES) undocumented
WRITABLE_MEMORY_TYPES constant const WRITABLE_MEMORY_TYPES = MEMORY_TYPES.filter( … The nine types memory_write exposes. arc is system-written only, so the tool parameter enum is narrower than the storage vocabulary by exactly that one value.
WritableMemoryType schema, type const WritableMemoryType = Schema.Literals([ … undocumented
isWritableMemoryType function const isWritableMemoryType = (type: MemoryType): type is WritableMemoryType => type !== "arc" True for the nine types an agent may write. Narrows, so a caller can branch on it.
PARA_BUCKETS constant const PARA_BUCKETS = ["projects", "areas", "resources", "archive"] as const PARA’s four buckets, closed and ordered. archive is a bucket rather than a status because eviction is a git mv. The path itself records the state, so git log --follow reads through it and diff -M reports the move as R100.
ParaBucket schema, type const ParaBucket = Schema.Literals(PARA_BUCKETS) undocumented
MemoryStatus schema, type const MemoryStatus = Schema.Literals(["active", "archived"]) The status a memory file carries in memhtml-status.
TASK_STATUSES constant const TASK_STATUSES = ["todo", "doing", "blocked", "done"] as const A task’s own status, carried in memhtml-task-status, a SEPARATE axis from MemoryStatus, which stays active | archived for every type including task.
TaskStatus schema, type const TaskStatus = Schema.Literals(TASK_STATUSES) undocumented
isTaskStatus function const isTaskStatus = (value: string): value is TaskStatus => … True when a string is in the closed task-status vocabulary. Narrows an untrusted value.
Importance constant const Importance = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 10 })) Importance, 1-10 inclusive, 1-based ordinal on a display scale, never an arithmetic input on its own. The retention scorer divides it by 10 to reach [0, 1] before it meets any other signal.
Confidence constant const Confidence = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 })) Confidence, unitless in [0, 1]. 1.0 is an unqualified assertion.
MemoryPath constant const MemoryPath = Schema.String.check(Schema.isMinLength(1)) A repo-root-relative path to a memory file, e.g. areas/oncall/rollback-order.html. No leading slash: this is the git-tree form, the files.path primary key, and the id of a memory. <link href> values carry the same path with a leading /. That is a document-reference form, converted at the HTML boundary, never stored here.
ENTITY_SEPARATOR constant const ENTITY_SEPARATOR = ":" A type:name entity reference, e.g. service:checkout-api, person:sanju. The prefix before the first colon is the entity type; everything after is the name, which may itself contain colons.
parseEntity function const parseEntity = ( … Split an entity reference into its type and name at the first separator. undefined when the reference has no separator, or when the type or the name on either side of it is empty.
normalizeEntityName function const normalizeEntityName = (name: string): string => … Lowercase, NFC-normalize, collapse internal whitespace, trim. What it means for two entity names to be the SAME name.
normalizeEntityRef function const normalizeEntityRef = (entity: string): string => … A whole reference in that form: both halves normalized, rejoined, padding around the separator gone.
PERSON_ENTITY_PREFIX constant const PERSON_ENTITY_PREFIX = 'person${ENTITY_SEPARATOR}' The person: entity prefix, which routes a semantic memory to resources/people/.
isPersonEntity function const isPersonEntity = (entity: string): boolean => … True when an entity reference names a person: the person: prefix plus a name that survives trim().

MEMORY_TYPES

The memory type vocabulary, closed. Ten values, restated by the files.memory_type CHECK constraint in SQL.

arc is in the vocabulary but absent from WRITABLE_MEMORY_TYPES: an arc is synthesized by the sleep cycle from many memories, so an agent naming one directly would be asserting a conclusion the corpus has not yet earned.

task is ONE axis with the other nine rather than a parallel kind column, because three overlapping type vocabularies is what made the predecessor memory system’s classification unanswerable. A task is a memory type whose retrieval, dedup, and curation treatment a filter states, not a second axis. Tasks are default-excluded from search and skipped by sleep. See @memhtml/index‘s assembleScope and the sleep phases’ excludeTypes.

TASK_STATUSES

A task’s own status, carried in memhtml-task-status, a SEPARATE axis from MemoryStatus, which stays active | archived for every type including task.

Two axes rather than four memhtml-status values because active/archived is what every archive, correction, and publish path switches on, and a fifth value there would silently change the meaning of each of them. Finishing a task stamps done AND archives the file through the same archiveMemory machinery, so done is not a resting state on its own and “what did I finish” is answered by the archive tree plus git log.

normalizeEntityName

Lowercase, NFC-normalize, collapse internal whitespace, trim. What it means for two entity names to be the SAME name.

It lives in contracts rather than beside a caller because it is a vocabulary rule, and a second copy of a vocabulary rule fails silently: entity-resolution decides which memhtml-entity metas to rewrite by comparing a name against this form, so a divergent copy would have the phase canonicalize files toward a spelling nothing else recognizes, reporting merges no query can reach.

file_entities stores names AS AUTHORED, not in this form, and that is deliberate — the phase finds its work by reading those rows back, so a projection that pre-normalized would hide every unnormalized meta from the one pass whose job is to fix it. The two SQL doors fold with lower() on both sides instead: a narrower fold (SQLite’s lower() is ASCII-only) that cannot disagree with itself across the JS/SQL seam. Full canonicalization is durable in the TREE, applied by the phase.

normalizeEntityRef

A whole reference in that form: both halves normalized, rejoined, padding around the separator gone.

For callers comparing references in TypeScript — the write path deciding whether a candidate entity is one the corpus already names. The SQL doors do NOT use this; see the seam note above.

Total over unparseable input. A string with no separator normalizes as a whole and is returned without one, so the caller still decides what an untyped reference means rather than receiving a silently invented type.

isPersonEntity

True when an entity reference names a person: the person: prefix plus a name that survives trim().

The trim is what makes this predicate agree with the rest of the person plane. placementFor routes on it, and the sleep phase that mints the person file and its memhtml-about-person links keys on entity_name.trim() !== "". A whitespace-only name accepted here would land a memory in resources/people/ that no phase will ever give a person file to link at — and slugify maps that name to untitled, which names nobody.

3. Provenance

A loader generates this page from packages/contracts/src while the site builds, so no file in the repository holds it: the modules are the ones index.ts re-exports, each symbol is an exported declaration read with the TypeScript compiler, and every description is the doc comment above it. Change the registry and this page changes with it.