---
title: Contracts
description: Every symbol @memhtml/contracts exports, with the doc comment written beside it in the source.
---

## 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](/reference/vocabulary/) 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 => …` | The rel behind a `<link rel>` token, or `undefined` when the token is outside the closed vocabulary. Inverse of `relTokenFor` on its image. |
| `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 => …` | The PARA bucket a path sits in, or `undefined` when it sits outside all four. |
| `memoryPathViolation` | function | `const memoryPathViolation = (path: string): string | undefined => …` | Why a path is not a usable memory path, or `undefined` when it is one. |
| `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 => …` | The pre-eviction path behind an archive path, or `undefined` when the path is not an archive path. Strips exactly one `archive/<YYYY>/` prefix, so it is the left inverse of `archivePathFor` even for a memory archived twice. |
| `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 => …` | The archive year of an archive path, or `undefined` when the path is not archived. |

**`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 `export`ed declaration read with the TypeScript compiler, and every description is the doc comment above it. Change the registry and this page changes with it.