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.