Skip to content

@memhtml/html

@memhtml/html is the memory file format: parse, serialize, hash, fence-language detection, the surgical head editors, the template that writes a new file, and the closed vocabulary all of them read. The format is semantic HTML5 over that vocabulary and this package is its only implementation, so a memory file stays a document a browser renders with no server, and the index stays derivable from the tree alone. Every element in the vocabulary earns its place by carrying indexer semantics, which is why the parse output is named for what the indexer stores rather than for the markup it came from: gist is the one <mark>, eventAt is the first <time>, facets are the <dl> pairs. The package sits on parse5, effect, a pinned highlight.js, and @memhtml/contracts for the closed edge, type and error vocabularies.

vocabulary.ts is the bottom of the stack and holds no logic, because the vocabulary IS the policy: there is no sanitizer library here and no allow/deny engine, only data. Both metadata planes use the same hyphenated prefix (META_PREFIX, LINK_REL_PREFIX) because HTML5 metadata names are a flat token space and a rel token cannot hold a colon at all. REQUIRED_META is the four facts no pure function can invent, a type, a status, and the two write stamps; memhtml-confidence, memhtml-importance and memhtml-author are deliberately absent because the files table documents a default for each, so a hand-authored file missing them is completed rather than refused. META_ORDER is the emit order and a diff-stability contract: a new scalar goes at the end of the scalar block and a new repeatable at the end of the repeatable block, so two agents stamping different keys leave each other’s lines in place. REPEATABLE_META names the three keys that may appear more than once, one element per value so correcting one tag is a one-line diff, and isMemoryMetaName and isRepeatableMeta are the predicates over both lists. The element tables split by role: DOCUMENT_ELEMENTS is the skeleton that carries no indexer semantics, ARTICLE_ELEMENTS is one entry per row of the format’s element table (with tr added, because a <table> cannot hold a cell without a row), PERSON_ELEMENTS is the <address> a person file adds, FIGURE_SCOPED_ELEMENTS is <div> and <span> permitted only inside a <figure> where a pasted sample carries its own containers, and KNOWN_ELEMENTS is their union. The rest are serialization and scoping facts the writer and the hash need: VOID_ELEMENTS, GIST_EXCLUDED_ELEMENTS (a command line is body text and searchable, and it is not the claim), FORBIDDEN_ELEMENTS and FORBIDDEN_ATTRIBUTES.

tree.ts is parse5 navigation narrowed to what the format needs, pure, total, and ignorant of the vocabulary. Source locations are always on, because the head editors splice by byte offset and a second parse to obtain them would let two views of the same bytes disagree. parseArticleFragment parses article inner HTML in <article> context on purpose: fragment parsing without a context element runs in <template> content, where table-scoped elements are foster-parented out and their text would vanish from the digest, so parsing in the element the markup came from is what makes contentHash agree on a doc and on the whole file. The walks are iterative rather than recursive so a pathologically deep hand-authored file cannot overflow the stack. markup.ts is the writer, and it exists because parse5’s own serializer is unusable here for two reasons. The costlier one is <pre>: the parser swallows one newline after the start tag and parse5 does not re-emit it, so a file loses one newline on every write, and <pre> whitespace is inside the hash scope verbatim, so that drift would move a memory’s dedup key on a pass that edited nothing. The second is attribute order, which parse5 preserves, leaving two files differing only in attribute order as different bytes forever; sorting by name canonicalizes them. escapeText writes U+00A0 as &nbsp; because an invisible character in a claim is a trap, and escapeAttribute turns line breaks into character references so an emitted <meta> or <link> is always one source line, which is what the line-splicing editors depend on.

hash.ts owns the one value in the system that must be invariant under head edits. The content hash is sha256 over the whitespace-normalized text content of <article>, except inside <pre> where whitespace passes through byte for byte, so meta and <link> edits are outside the scope by construction and confidence decay, access bookkeeping and the sleep phases’ own stamping never look like content changes. Without that invariance a corpus-wide decay pass would present the whole corpus as new content and dedup would collapse. Only ASCII whitespace collapses; U+00A0 is content, because a non-breaking space inside a claim is a typographic decision worth preserving. A block element contributes a collapsible space at each edge, so the digest is a function of the article’s words and not of its indentation, and the outer trim applies only to a leading or trailing collapsible segment, since trimming the whole result would let <pre> a</pre> and <pre>a</pre> hash identically. canonicalText takes CanonicalTextOptions, whose excludeCode omits <pre> and <code> per the gist rule, canonicalArticleText is the same fold with code kept, and contentHash accepts a parsed doc, an article node, a HashableArticle, or raw HTML, locating <article> first so head content never reaches the digest and hashing a string with no article as article-inner-HTML rather than failing, because refusing malformed input is parseMemory’s job and not the digest’s. HASH_ALGORITHM is the prefix that makes a stored digest self-describing and isContentHash recognizes the sha256:<64 hex> form.

constraints.ts is the six format constraints as pure predicates over a parsed document. Constraints 1 through 5 are violations and the file is not a memory; constraint 6 is a warning, because an element outside the vocabulary still indexes and the format has to degrade gracefully on a file a human hand-wrote in a hurry. Nothing here throws and nothing repairs: a checker that silently fixed a violation would make memhtml doctor report a clean corpus that the next hand-edit breaks again. isValidDatetime admits exactly two forms, an ISO date and a canonical UTC instant, and is narrower than HTML’s own datetime grammar because files.event_at, files.due_at and files.valid_until are compared and ordered as raw strings, so every admitted value has to sort lexicographically as it sorts chronologically; a space separator, a non-UTC offset, or optional seconds each break that, and range is checked too so 2026-13-45 is refused rather than stored as an unsortable date. isRootRelativeHref requires the document-reference form, repo-root-relative with a leading slash and no scheme, host or .. segment, since a relative href breaks on the first git mv of the source file and a protocol-relative one silently leaves the repo. The <mark> rules are the format’s center: exactly one, non-empty, leading the article’s first <p> or <li>, and never inside an <aside> or <details>, so a recall line cannot quote a caveat or something the author chose to hide. Emptiness is tested with the real gist rule (canonicalText with excludeCode, then trimmed) rather than a convenient approximation, because parse.ts derives gist from exactly that text, so <mark><code>drain --vip</code></mark> indexes with no gist and is refused. checkDocument collects rather than short-circuits, so one parse tells an author everything wrong with the file, and returns a CheckResult of violations and warnings in constraint order then document order, with warnings deduplicated by element name so forty stray <div>s produce one actionable line. VIOLATION_SEPARATOR is how that list is joined into the single string InvalidMemory.reason carries.

parse.ts implements the extraction table one element at a time, and the interesting part is what it refuses versus what it drops. A malformed optional meta is dropped and left for memhtml doctor: memhtml-confidence="high" means the file stated no confidence and the files default applies, and a blank value is malformed rather than zero, since Number("") is 0 and a blank confidence must not index as 0.00. The datetime metas and the task metas are refusals instead, because a drop is only safe where the files table owns a default and an instant has none: dropping an unsortable memhtml-valid-until WIDENS the window to always-valid, so the file would answer an as-of query for instants it said the fact was already dead, and a task with no memhtml-task-status has no lifecycle position at all, so it would be invisible to the surface that exists to show it. parseMemory fails with InvalidMemory and no partial doc, and checkMemory is the total variant that returns the structured violation and warning lists without building one. document.ts holds the shapes, as Effect schemas paired with the interfaces they decode to, and they live here rather than in @memhtml/contracts because they are format types: the extraction fields exist only because the HTML has those elements. MemoryMetas states each field’s coordinate space on the field, and an absent value means the file did not state it, with no default substituted, so a hand-authored omission stays distinguishable from a deliberate assertion. ArticleExtractions is what the indexer reads out of <article>, with html a serialization fixed point, bodyText the FTS and embedding input including asides, details bodies, captions and <pre>, gist the one span nothing derives, eventAt the world time the recency arm coalesces with the write stamp, and the document-order lists (facets, citations, definedTerms, summaryTexts, asideTexts, captions, codeLangs, abbreviations) each feeding their own table or entity promotion. MemoryLink carries an unprefixed rel from the closed edge vocabulary with the href as the file spells it, Facet pairs a <dt> with each of its <dd>s and carries a numericValue only when a <data value> parses as a finite number, deliberately unitless because the unit lives in the human phrasing, and Citation is a <cite> or a <q> with its optional source URI.

serialize.ts is the inverse: parseMemory(serializeMemory(doc)) yields the doc again, which is the property that lets sleep read, adjust and write a file back without touching content it did not mean to touch. The output is deterministic, one <meta> per line in META_ORDER with no head indentation, so stamping memhtml-updated produces a one-line git diff a human actually reviews, and article.html is re-parsed and re-serialized rather than interpolated so a hand-assembled doc still gets the canonical fixed-point markup. metaPairs is the ordered [name, content] expansion, repeatable keys one pair per value, confidence rendered to two decimals and every other number as an integer. editors.ts is the alternative to that round trip for a bookkeeping pass: byte-level splices that change exactly one line and provably do not touch the article. The sleep phases stamp memhtml-updated, decay memhtml-confidence, bump memhtml-reprieves and set memhtml-needs-revision on files they otherwise leave alone, and re-serializing the whole document for that would make the pass look like a content change in git diff and risk moving the dedup key. Splicing by source offset keeps the hash invariant by construction, so contentHash(setMeta(html, name, value)) equals contentHash(html) for every name. setMeta replaces the first meta of a name or inserts a line in META_ORDER position and refuses an unknown name by returning the input unchanged, addMeta appends another value of a repeatable name and is a no-op on a value already present so a re-run of a phase cannot grow the head, addLink is idempotent on the (rel, href) pair because the conflict phase promotes the same corroborated edge on each run and a duplicated <link> would become a duplicated edges row, removeMeta and removeLink cut whole lines (omitting href drops every link of that rel), and readMeta and readLinks are the deliberately tolerant read halves: the document is parsed and nothing is validated, so a caller holding raw bytes can ask what a file points at without the file having to be a well-formed memory.

fences.ts owns the fenced-code grammar and only the grammar: what opens a fence, what closes it, and what language its info string names. There is one copy because the template renders a block and the write doors split prose into paragraphs, and two answers to “is this line a fence?” would let the splitter keep a block intact that the template then fails to recognize. Backtick fences only, per CommonMark: three or more open, a run at least as long closes, and the info string may hold no backtick; tilde fences are deliberately absent because agents write backticks. LANG_TOKEN is the language-token grammar, wide enough for ts, c++, c# and objective-c and closed to whitespace and markup characters, so a data-lang value is always safe in view-source and safe to promote to a lang: entity. fenceOpeningOf and closesFence are the line-level predicates, and fencedBlockOf returns a FencedBlock only when the whole paragraph is the fence: prose sharing a paragraph with a fence is not a fence, and an unterminated fence is undefined, because rendering either as code would commit markup the author may not have finished while escaping it as text keeps the file saying what the author typed.

detect.ts is a port of a measured detector, not a fresh design, because an unlabeled fence carries no language and wrong metadata costs more than none. A private evaluation corpus swept two candidates, flourite and highlight.js highlightAuto, over 332 real fences and file slices and picked the operating point where measured precision first reaches a 95% floor: highlight.js won at threshold 0.28685957116771854 with precision 95.18% at 25.0% coverage, where flourite reached 100% precision at only 3.0% coverage, abstaining exactly where this one stamps. DEPLOY_THRESHOLD is 0.30, a conservative rounding up of that point, safe because confidence is monotone in evidence: re-measured on the same corpus it stamped 81 snippets, 77 correctly, for 95.06% precision at 24.4% coverage, and both numbers are recorded because the measured one is the evidence and the deployed one is the decision. detect returns a Detection with the threshold not yet applied, scoring 1 - exp(-(top - runnerUp) / lines), the per-line evidence margin between the winning grammar and its closest real competitor, where a runner-up normalizing to the same canonical token is not disagreement and so charges no penalty, and where hljs runs its full grammar set deliberately: restricting it to the twelve-name vocabulary measured 0.9% coverage against 25%, because with the true language’s grammar absent a runner-up wins unopposed and wins confidently. detectLang applies both independent gates, in-vocabulary and at or above the threshold, since a confident detection of a language outside CANONICAL_LANGS is still nothing, and the corpus has hljs naming smali at 0.86 and autohotkey at 0.55 for snippets that are really bash and TypeScript. CANONICAL_LANGS is one lowercase token per language because data-lang promotes to a lang: entity by exact string match, and normalizeLang folds the aliases seen in detector output, info strings and file extensions (highlight.js names its HTML grammar “xml”, ships no TOML grammar, and SQL dialects collapse because a dialect distinction is noise at entity granularity). Three operational decisions come with the port. highlight.js is pinned exactly, no caret, because relevance scores are grammar-dependent and a bump silently moves every confidence and therefore which fences get stamped; measured against 11.12.0, one snippet fell from 0.5406 to 0.1993 and stopped stamping toml, so a bump is a deliberate decision that re-runs the eval and re-derives the threshold. The library loads lazily on the first detection, because eagerly imported its 192 grammars cost roughly 100ms and 12MB in every process that touches the format layer, including the read path, which detects nothing. Fences longer than 4096 characters abstain without running hljs at all, since highlightAuto is super-linear in input size (5KB is about 450ms and 100KB about 122s of blocking CPU on the pinned build) and the MCP server is one single-threaded process, so an uncapped detector would let one large unlabeled fence wedge every other request. Detection is also write-time only: the stamp goes into the file, which is the system of record, and rebuild reads data-lang back through parse.ts and never re-detects, so a dropped and rebuilt index stays a pure function of the tree instead of a function of the installed hljs version. Author strings are untouched throughout, since this vocabulary gates detector output alone.

template.ts is what the write path calls instead of the serializer, because an agent supplies a title, a claim and prose, and something has to turn that into the <mark>-led first paragraph the format requires: an agent hand-authoring that placement would violate constraint 1 regularly, and a template that places it always places it correctly. NewMemoryInput mirrors the write tool’s parameters, with claim required because a memory with no claim has nothing to disclose at Tier 1, body one string per paragraph, and articleHtml the escape hatch for a correction carrying its own <dl> or <figure>, where the caller then owns constraint 1. articleHtmlFor joins the first body paragraph onto the claim’s own paragraph as its tail, drops empty paragraphs, and renders a body paragraph that is a fenced block as <figure><pre><code data-lang> rather than a <p>, never joined onto the claim, so the claim still leads the article and code stays out of the sentence; the language rides on data-lang because class is a constraint-3 violation and lang is a human-language attribute, and the info string wins outright, so detectLang runs only on an unlabeled fence and no detector opinion overrides what a human wrote. DEFAULT_TASK_STATUS is todo, defaulted rather than required of the caller because parseMemory refuses a task with no memhtml-task-status and it is stamped only for a task, since a non-task carrying one is a file git holds and the indexer skips. newMemoryDoc builds the doc with memhtml-content-hash already stamped from the article the template just built, so the file reaching disk is self-consistent and the indexer’s own recomputation agrees with it on the first read, and renderTemplate is the one function the write path calls for bytes.

The package publishes one import path, @memhtml/html, which re-exports the eleven modules below, so the reference on this page is the whole exported surface. tree.ts is internal: it reaches consumers only as the parse5 node types the other modules accept and return.

Module What it contributes
constraints.ts checkDocument and its CheckResult, VIOLATION_SEPARATOR, and the two value grammars isValidDatetime and isRootRelativeHref.
detect.ts CANONICAL_LANGS and CanonicalLang, normalizeLang, DEPLOY_THRESHOLD, detect with its Detection, and detectLang.
document.ts MemoryDoc, MemoryMetas, ArticleExtractions, MemoryLink, Facet and Citation, each a schema paired with the interface it decodes to.
editors.ts The write half (setMeta, addMeta, removeMeta, addLink, removeLink) and the tolerant read half (readMeta, readLinks).
fences.ts LANG_TOKEN, fenceOpeningOf, closesFence, and fencedBlockOf with its FencedBlock.
hash.ts HASH_ALGORITHM, canonicalText with CanonicalTextOptions, canonicalArticleText, contentHash with HashableArticle, and isContentHash.
markup.ts escapeText, escapeAttribute, writeChildren and writeOuter.
parse.ts parseMemory (the doc or InvalidMemory) and checkMemory (the total violation and warning lists).
serialize.ts serializeMemory and metaPairs.
template.ts NewMemoryInput, articleHtmlFor, newMemoryDoc, renderTemplate and DEFAULT_TASK_STATUS.
tree.ts Nothing on the package surface: the parse5 navigation and the two parsers the modules above share.
vocabulary.ts The naming conventions (META_PREFIX, LINK_REL_PREFIX), the metadata lists (REQUIRED_META, META_ORDER, MemoryMetaName, isMemoryMetaName, REPEATABLE_META, RepeatableMeta, isRepeatableMeta), the element tables (DOCUMENT_ELEMENTS, ARTICLE_ELEMENTS, PERSON_ELEMENTS, FIGURE_SCOPED_ELEMENTS, KNOWN_ELEMENTS), and the serialization and scoping facts (VOID_ELEMENTS, GIST_EXCLUDED_ELEMENTS, FORBIDDEN_ELEMENTS, FORBIDDEN_ATTRIBUTES).

The parsed shape ArticleExtractions decodes to, named so a signature can refer to it.

  • ArticleExtractionsShape
readonly abbreviations: readonly string[];

<abbr title> expansions in document order. FTS-visible.

ArticleExtractionsShape.abbreviations

readonly asideTexts: readonly string[];

<aside> texts in document order. In bodyText and therefore searchable, and left out of a recall index line. An aside is a scope caveat, so quoting it as the memory would present the exception as the rule.

ArticleExtractionsShape.asideTexts

readonly bodyText: string = Schema.String;

All article text, whitespace-collapsed. The FTS body and the embedding input. Includes <aside>, <details> bodies, <figcaption>, and <pre>, so everything is searchable.

ArticleExtractionsShape.bodyText

readonly captions: readonly string[];

<figcaption> texts in document order. FTS-visible; the <pre> body is not gist-visible.

ArticleExtractionsShape.captions

readonly citations: readonly object[];

<cite> and <q> in document order, one row each in file_citations.

ArticleExtractionsShape.citations

readonly codeLangs: readonly string[];

data-lang values of <code> elements, lowercased, in document order. Each promotes to a lang:<value> entity the way a <dfn> promotes to concept:. A memory carrying a TypeScript snippet is findable by --entity lang:ts without the author restating the language in a memhtml-entity meta the fence’s info string already carried.

ArticleExtractionsShape.codeLangs

readonly definedTerms: readonly string[];

<dfn> terms, in document order. Each promotes to a concept:<term> entity, so a semantic memory that defines a term is findable by the term without the author also writing a memhtml-entity meta.

ArticleExtractionsShape.definedTerms

readonly eventAt?: string;

The FIRST <time datetime> value, as authored (ISO date or ISO datetime, so 2026-07-28 and 2026-07-28T14:03:11Z both occur). This is when the remembered fact HAPPENED, so it is world time and not write time. The recency arm ranks by coalesce(event_at, updated_at), so an episodic memory backdates correctly. Absent when the article names no time.

ArticleExtractionsShape.eventAt

readonly facets: readonly object[];

<dt>/<dd> pairs in document order, one row each in file_facets.

ArticleExtractionsShape.facets

readonly gist: string = Schema.String;

The ONE <mark> span’s text, the claim and only the claim. It is the span the author chose, the Tier-1 disclosure line, and the span a correction targets. Nothing derives it, so it is not a summary and not the first sentence.

ArticleExtractionsShape.gist

readonly html: string = Schema.String;

The article’s inner HTML, trimmed. A serialization fixed point: re-parsing and re-serializing it yields the same bytes, which is what makes the round-trip property hold.

ArticleExtractionsShape.html

readonly summaryTexts: readonly string[];

<summary> texts in document order. Always disclosed in recall, as Tier 2 of the fold. The matching <details> body is Tier 3 and reaches an agent only through memory_read.

ArticleExtractionsShape.summaryTexts


Which element’s text holds a block tag, and the tag as the text reads.

readonly element: "mark" | "p";
readonly tag: string;

Where a block tag sits in a text: the position it starts at, and the tag as written.

readonly offset: number;
readonly tag: string;

What canonicalText may skip.

readonly excludeCode?: boolean;

Omit <pre> and <code> subtrees, per the gist rule. A command line is body text and it is searchable, and it is not part of the claim.


The outcome of checking a document. Both lists are in constraint order, then document order.

readonly violations: readonly string[];
readonly warnings: readonly string[];

The parsed shape Citation decodes to, named so a signature can refer to it.

  • CitationShape
readonly href?: string;

CitationShape.href

readonly text: string = Schema.String;

CitationShape.text


What the detector concluded, threshold not yet applied. Exported for tests and for the sweep.

readonly confidence: number;

Monotone evidence score in [0, 1). NOT a calibrated probability, and meaningful only against DEPLOY_THRESHOLD.

readonly lang:
| "html"
| "javascript"
| "typescript"
| "python"
| "bash"
| "json"
| "sql"
| "yaml"
| "toml"
| "css"
| "go"
| "rust"
| undefined;

Canonical language, or undefined when hljs abstained or named a language outside the vocabulary.


The parsed shape Facet decodes to, named so a signature can refer to it.

  • FacetShape
readonly name: string = Schema.String;

FacetShape.name

readonly numericValue?: number;

FacetShape.numericValue

readonly value: string = Schema.String;

FacetShape.value


One recognized fenced block: its code verbatim, and the info string’s language when it names one.

readonly code: string;

The lines between the fences, joined verbatim, with indentation and blank lines preserved.

readonly lang?: string;

The first info-string token, lowercased, when it matches LANG_TOKEN.


What contentHash accepts. A caller that already holds a parsed article passes the node; the store and indexer, which hold bytes, pass a string or the article HTML.

readonly article: object;
readonly html: string;

The parsed shape MemoryDoc decodes to, named so a signature can refer to it.

  • MemoryDocShape
readonly aliases: readonly string[];

memhtml-alias values as authored, in document order. The other names this file’s subject is recorded under — a person file’s declaration that laura and l.desabelin are the same person as its own person: entity.

Meant as EVIDENCE for entity resolution, which can merge an alias-backed pair whatever the name similarity says. That is why the field is a bare string list rather than a parsed type:name: the declaring file’s own memhtml-entity already carries the type, and an alias that restated it could disagree with it.

MemoryDocShape.aliases

readonly article: object = ArticleExtractions;
readonly abbreviations: readonly string[];

<abbr title> expansions in document order. FTS-visible.

readonly asideTexts: readonly string[];

<aside> texts in document order. In bodyText and therefore searchable, and left out of a recall index line. An aside is a scope caveat, so quoting it as the memory would present the exception as the rule.

readonly bodyText: string = Schema.String;

All article text, whitespace-collapsed. The FTS body and the embedding input. Includes <aside>, <details> bodies, <figcaption>, and <pre>, so everything is searchable.

readonly captions: readonly string[];

<figcaption> texts in document order. FTS-visible; the <pre> body is not gist-visible.

readonly citations: readonly object[];

<cite> and <q> in document order, one row each in file_citations.

readonly codeLangs: readonly string[];

data-lang values of <code> elements, lowercased, in document order. Each promotes to a lang:<value> entity the way a <dfn> promotes to concept:. A memory carrying a TypeScript snippet is findable by --entity lang:ts without the author restating the language in a memhtml-entity meta the fence’s info string already carried.

readonly definedTerms: readonly string[];

<dfn> terms, in document order. Each promotes to a concept:<term> entity, so a semantic memory that defines a term is findable by the term without the author also writing a memhtml-entity meta.

readonly eventAt?: string;

The FIRST <time datetime> value, as authored (ISO date or ISO datetime, so 2026-07-28 and 2026-07-28T14:03:11Z both occur). This is when the remembered fact HAPPENED, so it is world time and not write time. The recency arm ranks by coalesce(event_at, updated_at), so an episodic memory backdates correctly. Absent when the article names no time.

readonly facets: readonly object[];

<dt>/<dd> pairs in document order, one row each in file_facets.

readonly gist: string = Schema.String;

The ONE <mark> span’s text, the claim and only the claim. It is the span the author chose, the Tier-1 disclosure line, and the span a correction targets. Nothing derives it, so it is not a summary and not the first sentence.

readonly html: string = Schema.String;

The article’s inner HTML, trimmed. A serialization fixed point: re-parsing and re-serializing it yields the same bytes, which is what makes the round-trip property hold.

readonly summaryTexts: readonly string[];

<summary> texts in document order. Always disclosed in recall, as Tier 2 of the fold. The matching <details> body is Tier 3 and reaches an agent only through memory_read.

MemoryDocShape.article

readonly entities: readonly string[];

memhtml-entity values as authored, e.g. service:checkout-api, in document order.

MemoryDocShape.entities

readonly links: readonly object[];

MemoryDocShape.links

readonly metas: object = MemoryMetas;
readonly archivedAt?: string;

When eviction moved it under archive/<YYYY>/. Present iff status is archived.

readonly author?: string;

Who wrote it, as agent:<model> or human:<name>.

readonly confidence?: number;

Unitless in [0, 1]. 1.0 is an unqualified assertion.

readonly contentHash?: string;

The sha256:<hex> the file claims for its own article. Advisory: the parser reports it verbatim and leaves it alone, so a stale value is visible to memhtml doctor rather than silently corrected into agreement.

readonly createdAt: string = Schema.String;
readonly dueAt?: string;

When a task is due. An ISO date or datetime, ordered as a string exactly as event_at is, so due_at < now is a lexicographic comparison rather than a parse per row.

A wall-clock DEADLINE. validUntil says when a remembered fact stops being true, and this says when work is late, so neither a write time nor a validity bound belongs here.

readonly importance?: number;

1-10 inclusive, a display ordinal. The retention scorer divides by 10 before using it.

readonly memoryType:
| "episodic"
| "semantic"
| "procedural"
| "agent_insight"
| "user_preference"
| "error_pattern"
| "verdict"
| "precedent"
| "arc"
| "task" = MemoryType;
readonly needsRevision?: boolean;

A curation pass flagged the claim as stale or contested and wants an agent to revisit it.

readonly promptId?: string;

The prompt within that session. Joins trace_prompts.prompt_id.

readonly reprieves?: number;

How many times a retention pass spared this memory from eviction. A count, monotonically non-decreasing, >= 0.

readonly sessionId?: string;

The Claude Code session that produced it. Joins traces.session_id.

readonly status: "active" | "archived" = MemoryStatus;
readonly supersededBy?: string;

The path that replaced it, repo-root-relative with a leading / (the document-reference form). The inverse of a memhtml-supersedes link on the newer file.

readonly taskStatus?: "todo" | "doing" | "blocked" | "done";

A task’s own lifecycle position, present iff memoryType is task. The parser reports a violation either way round. A separate axis from MemoryMetas.status, which stays active/archived for a task as for anything else.

readonly turnUuid?: string;

The turn within that session. Joins trace_prompts.turn_uuid.

readonly updatedAt: string = Schema.String;
readonly validFrom?: string;

Bitemporal validity of the fact, not of the row. Absent means always-valid.

readonly validUntil?: string;

MemoryDocShape.metas

readonly tags: readonly string[];

memhtml-tag values as authored, in document order. Open vocabulary.

MemoryDocShape.tags

readonly title: string = Schema.String;

The <title> text. The human name of the memory and the slug’s source.

MemoryDocShape.title

readonly warnings: readonly string[];

Vocabulary warnings, such as an element outside the closed vocabulary or a <div> outside a <figure>. Format constraint 6: the file still parses and still indexes, so a hand-authored file degrades gracefully instead of being refused.

MemoryDocShape.warnings


The parsed shape MemoryLink decodes to, named so a signature can refer to it.

  • MemoryLinkShape
readonly href: string = Schema.String;

MemoryLinkShape.href

readonly rel:
| "supersedes"
| "contradicts"
| "caused_by"
| "leads_to"
| "part_of"
| "relates_to"
| "example_of"
| "supports"
| "laterally_related"
| "about_person"
| "authored_by"
| "from_session"
| "blocks"
| "subtask_of" = EdgeRel;

MemoryLinkShape.rel


The parsed shape MemoryMetas decodes to, named so a signature can refer to it.

  • MemoryMetasShape
readonly archivedAt?: string;

When eviction moved it under archive/<YYYY>/. Present iff status is archived.

MemoryMetasShape.archivedAt

readonly author?: string;

Who wrote it, as agent:<model> or human:<name>.

MemoryMetasShape.author

readonly confidence?: number;

Unitless in [0, 1]. 1.0 is an unqualified assertion.

MemoryMetasShape.confidence

readonly contentHash?: string;

The sha256:<hex> the file claims for its own article. Advisory: the parser reports it verbatim and leaves it alone, so a stale value is visible to memhtml doctor rather than silently corrected into agreement.

MemoryMetasShape.contentHash

readonly createdAt: string = Schema.String;

MemoryMetasShape.createdAt

readonly dueAt?: string;

When a task is due. An ISO date or datetime, ordered as a string exactly as event_at is, so due_at < now is a lexicographic comparison rather than a parse per row.

A wall-clock DEADLINE. validUntil says when a remembered fact stops being true, and this says when work is late, so neither a write time nor a validity bound belongs here.

MemoryMetasShape.dueAt

readonly importance?: number;

1-10 inclusive, a display ordinal. The retention scorer divides by 10 before using it.

MemoryMetasShape.importance

readonly memoryType:
| "episodic"
| "semantic"
| "procedural"
| "agent_insight"
| "user_preference"
| "error_pattern"
| "verdict"
| "precedent"
| "arc"
| "task" = MemoryType;

MemoryMetasShape.memoryType

readonly needsRevision?: boolean;

A curation pass flagged the claim as stale or contested and wants an agent to revisit it.

MemoryMetasShape.needsRevision

readonly promptId?: string;

The prompt within that session. Joins trace_prompts.prompt_id.

MemoryMetasShape.promptId

readonly reprieves?: number;

How many times a retention pass spared this memory from eviction. A count, monotonically non-decreasing, >= 0.

MemoryMetasShape.reprieves

readonly sessionId?: string;

The Claude Code session that produced it. Joins traces.session_id.

MemoryMetasShape.sessionId

readonly status: "active" | "archived" = MemoryStatus;

MemoryMetasShape.status

readonly supersededBy?: string;

The path that replaced it, repo-root-relative with a leading / (the document-reference form). The inverse of a memhtml-supersedes link on the newer file.

MemoryMetasShape.supersededBy

readonly taskStatus?: "todo" | "doing" | "blocked" | "done";

A task’s own lifecycle position, present iff memoryType is task. The parser reports a violation either way round. A separate axis from MemoryMetas.status, which stays active/archived for a task as for anything else.

MemoryMetasShape.taskStatus

readonly turnUuid?: string;

The turn within that session. Joins trace_prompts.turn_uuid.

MemoryMetasShape.turnUuid

readonly updatedAt: string = Schema.String;

MemoryMetasShape.updatedAt

readonly validFrom?: string;

Bitemporal validity of the fact, not of the row. Absent means always-valid.

MemoryMetasShape.validFrom

readonly validUntil?: string;

MemoryMetasShape.validUntil


What memory_write supplies. Mirrors the tool’s parameters, with the format’s own additions.

readonly aliases?: readonly string[];

Other names this file’s subject is recorded under, as memhtml-alias metas. A person file’s identity declaration, meant as merge evidence for entity resolution.

readonly articleHtml?: string;

Pre-authored article markup, used verbatim in place of claim/body. The escape hatch for a correction that carries a <dl> or a <figure>. It must already contain its own <mark>, so the caller owns constraint 1 when it uses this.

readonly at: string;

ISO-8601 UTC instant of write time. Stamps both memhtml-created and memhtml-updated.

readonly author?: string;
readonly body?: readonly string[];

Prose after the claim, one paragraph per element. The claim’s own paragraph is the first one, so body[0] becomes the claim paragraph’s tail rather than a second <p>.

readonly claim: string;

The claim: exactly the text that becomes the one <mark> span and therefore files.gist. Required, because a memory with no claim has nothing to disclose at Tier 1.

readonly confidence?: number;
readonly dueAt?: string;

When a task is due. An ISO date or datetime, string-ordered.

readonly entities?: readonly string[];

type:name entity references, e.g. service:checkout-api.

readonly importance?: number;
readonly links?: readonly object[];
readonly memoryType:
| "episodic"
| "semantic"
| "procedural"
| "agent_insight"
| "user_preference"
| "error_pattern"
| "verdict"
| "precedent"
| "arc"
| "task";
readonly promptId?: string;
readonly sessionId?: string;
readonly tags?: readonly string[];
readonly taskStatus?: "todo" | "doing" | "blocked" | "done";

A task’s lifecycle position. Absent on a task defaults to DEFAULT_TASK_STATUS; absent on any other type stays absent, because only a task carries one.

readonly title: string;

The <title>, and the slug’s source.

readonly turnUuid?: string;
readonly validFrom?: string;

Bitemporal validity of the fact. Absent means always-valid.

readonly validUntil?: string;
type CanonicalLang = typeof CANONICAL_LANGS[number];

type MemoryMetaName = typeof META_ORDER[number];

type ProseParagraph =
| {
claim?: string;
index?: number;
kind: "p";
text?: string;
}
| {
block: FencedBlock;
index: number;
kind: "figure";
};

One paragraph of the article the prose path renders, before escaping: a <p> holding text, or a <figure> holding a fenced block. index is the body paragraph it came from.

{
claim?: string;
index?: number;
kind: "p";
text?: string;
}
readonly claim?: string;

The claim, on the first paragraph only: the text of its <mark>.

readonly index?: number;
readonly kind: "p";
readonly text?: string;

The body paragraph’s text, absent on a claim that stands alone.


{
block: FencedBlock;
index: number;
kind: "figure";
}

type RepeatableMeta = typeof REPEATABLE_META[number];
const ARTICLE_ELEMENTS: readonly ["article", "mark", "time", "dl", "dt", "dd", "data", "cite", "q", "dfn", "figure", "figcaption", "details", "summary", "aside", "section", "abbr", "pre", "code", "kbd", "samp", "var", "table", "caption", "thead", "tbody", "tr", "th", "td", "p", "ul", "ol", "li", "a", "strong", "em"];

The body vocabulary, one entry per row of the format’s element table.

tr is here although the table lists only caption/thead/tbody/th/td: a <table> cannot hold a cell without a row, so refusing tr would make every real table warn. Nothing else is added by inference. An element the format does not name is a warning, which is the graceful-degradation rule for hand-authored files.


const ArticleExtractions: Struct<{
abbreviations: $Array<String>;
asideTexts: $Array<String>;
bodyText: String;
captions: $Array<String>;
citations: $Array<Struct<{
href: optional<String>;
text: String;
}>>;
codeLangs: $Array<String>;
definedTerms: $Array<String>;
eventAt: optional<String>;
facets: $Array<Struct<{
name: String;
numericValue: optional<Number>;
value: String;
}>>;
gist: String;
html: String;
summaryTexts: $Array<String>;
}>;

What the indexer reads out of <article>. Field names are the indexer’s own, so the indexing pipeline consumes this struct without a translation layer.


const CANONICAL_LANGS: readonly ["typescript", "javascript", "python", "bash", "json", "sql", "yaml", "html", "toml", "css", "go", "rust"];

The languages a DETECTION may name. One canonical lowercase token each, because data-lang promotes to a lang: entity by exact string match, so two spellings of TypeScript would be two entities. Anything outside this list is “do not stamp”.


const Citation: Struct<{
href: optional<String>;
text: String;
}>;

One <cite> or <q>. href is the <q cite> URI when present, an absolute or root-relative source URI, not necessarily a memory path.


const DEFAULT_TASK_STATUS: TaskStatus = "todo";

The status a new task starts in.

Defaulted rather than required of the caller because parseMemory REFUSES a task with no memhtml-task-status. A template that omitted it would render a file the format rejects, and every memhtml task add would have to restate the obvious opening state.


const DEPLOY_THRESHOLD: 0.3 = 0.3;

The confidence at or above which a detection is stamped: 0.30.

See the module header for provenance. This is the measured 0.28685957116771854 rounded in the safe direction, not a guess and not a round number chosen for looking tidy.


const DOCUMENT_ELEMENTS: readonly ["html", "head", "body", "title", "meta", "link"];

The document skeleton. These carry no indexer semantics of their own; they exist so the file is a valid HTML5 document a browser renders with no server.


const Facet: Struct<{
name: String;
numericValue: optional<Number>;
value: String;
}>;

One <dt>/<dd> pair. name is the <dt> text, value the <dd> text.

numericValue is present only when the <dd> holds a <data value> whose attribute parses as a finite number. It is unitless here on purpose: the unit lives in the human phrasing (<data value="120">about two minutes</data> is seconds because the prose says so), so a consumer cannot read a unit off the number alone.


const FIGURE_SCOPED_ELEMENTS: readonly ["div", "span"];

<div> and <span> are permitted only inside a <figure>, where a pasted code sample legitimately carries its own markup. Outside a figure they are the generic-container habit the closed vocabulary exists to refuse.


const FORBIDDEN_ATTRIBUTES: ReadonlySet<string>;

Attributes forbidden anywhere: presentation is the stylesheet’s job, not the memory’s.


const FORBIDDEN_ELEMENTS: ReadonlySet<string>;

Elements forbidden anywhere: a memory file is data, and data does not execute.

Beyond the script/style pair, the list bans every element that embeds a browsing context or foreign resource (iframe, frame, frameset, object, embed, applet), rewrites the document’s own URL space (base), or submits data to a target (form). These are violations rather than constraint-6 warnings because the corpus is published as a static site: a file carrying one would execute, embed, or navigate in every reader’s browser under the corpus’s own origin. link/meta need attribute-level rules instead (the scheme check and the http-equiv refusal in constraints.ts), since both are required skeleton elements.


const GIST_EXCLUDED_ELEMENTS: ReadonlySet<string>;

Elements whose text the gist must not absorb. A command line is body text and it is searchable. The claim is prose in the <mark> span.


const HASH_ALGORITHM: "sha256" = "sha256";

The digest’s algorithm prefix. A hash is self-describing so a stored value can be re-verified.


const KNOWN_ELEMENTS: ReadonlySet<string>;

Every element name the vocabulary knows, at any position.


const LANG_TOKEN: RegExp;

The language token grammar. Covers the identifiers real info strings carry, such as ts, c++, c#, objective-c, and python3, while refusing whitespace and markup characters, so a data-lang value is always safe in view-source and safe to promote to a lang: entity.


const LINK_REL_PREFIX: "memhtml-" = "memhtml-";

const MemoryDoc: Struct<{
aliases: $Array<String>;
article: Struct<{
abbreviations: $Array<String>;
asideTexts: $Array<String>;
bodyText: String;
captions: $Array<String>;
citations: $Array<Struct<{
href: optional<String>;
text: String;
}>>;
codeLangs: $Array<String>;
definedTerms: $Array<String>;
eventAt: optional<String>;
facets: $Array<Struct<{
name: String;
numericValue: optional<Number>;
value: String;
}>>;
gist: String;
html: String;
summaryTexts: $Array<String>;
}>;
entities: $Array<String>;
links: $Array<Struct<{
href: String;
rel: Literals<readonly ["supersedes", "contradicts", "caused_by", "leads_to", "part_of", "relates_to", "example_of", "supports", "laterally_related", "about_person", "authored_by", "from_session", "blocks", "subtask_of"]>;
}>>;
metas: Struct<{
archivedAt: optional<String>;
author: optional<String>;
confidence: optional<Number>;
contentHash: optional<String>;
createdAt: String;
dueAt: optional<String>;
importance: optional<Int>;
memoryType: Literals<readonly ["episodic", "semantic", "procedural", "agent_insight", "user_preference", "error_pattern", "verdict", "precedent", "arc", "task"]>;
needsRevision: optional<Boolean>;
promptId: optional<String>;
reprieves: optional<Number>;
sessionId: optional<String>;
status: Literals<readonly ["active", "archived"]>;
supersededBy: optional<String>;
taskStatus: optional<Literals<readonly ["todo", "doing", "blocked", "done"]>>;
turnUuid: optional<String>;
updatedAt: String;
validFrom: optional<String>;
validUntil: optional<String>;
}>;
tags: $Array<String>;
title: String;
warnings: $Array<String>;
}>;

A parsed memory file. Frozen: a consumer that wants a changed doc builds a new one, so a shared doc cannot be mutated out from under the hash a caller already computed.


const MemoryLink: Struct<{
href: String;
rel: Literals<readonly ["supersedes", "contradicts", "caused_by", "leads_to", "part_of", "relates_to", "example_of", "supports", "laterally_related", "about_person", "authored_by", "from_session", "blocks", "subtask_of"]>;
}>;

One <link rel="memhtml-*">. rel is the unprefixed rel from the closed edge vocabulary; the memhtml--prefixed hyphenated token is the wire form and is re-derived on serialize.

href is the document-reference form, repo-root-relative with a leading slash, as it appears in the file. The git-tree form the edges table stores drops that slash; @memhtml/contracts’s normalizePath is the conversion, applied at the store boundary.


const MemoryMetas: Struct<{
archivedAt: optional<String>;
author: optional<String>;
confidence: optional<Number>;
contentHash: optional<String>;
createdAt: String;
dueAt: optional<String>;
importance: optional<Int>;
memoryType: Literals<readonly ["episodic", "semantic", "procedural", "agent_insight", "user_preference", "error_pattern", "verdict", "precedent", "arc", "task"]>;
needsRevision: optional<Boolean>;
promptId: optional<String>;
reprieves: optional<Number>;
sessionId: optional<String>;
status: Literals<readonly ["active", "archived"]>;
supersededBy: optional<String>;
taskStatus: optional<Literals<readonly ["todo", "doing", "blocked", "done"]>>;
turnUuid: optional<String>;
updatedAt: String;
validFrom: optional<String>;
validUntil: optional<String>;
}>;

The head’s typed metadata. Absent means the file did not state it, and no default is substituted. The files table owns the defaults, and a parser that invented confidence: 1.0 would make a hand-authored omission indistinguishable from a deliberate assertion.

Timestamps are ISO-8601 UTC instants of write time. createdAt is when the memory was first written and updatedAt when it last changed. When the remembered fact happened is article.eventAt instead.


const META_ORDER: readonly ["memhtml-type", "memhtml-status", "memhtml-created", "memhtml-updated", "memhtml-confidence", "memhtml-importance", "memhtml-content-hash", "memhtml-author", "memhtml-session", "memhtml-prompt", "memhtml-turn", "memhtml-valid-from", "memhtml-valid-until", "memhtml-reprieves", "memhtml-archived", "memhtml-superseded-by", "memhtml-needs-revision", "memhtml-task-status", "memhtml-due", "memhtml-entity", "memhtml-tag", "memhtml-alias"];

Every metadata name in the closed vocabulary, in the order the serializer emits them. A stable order is what makes a meta-only edit a one-line git diff, so two writers stamping different keys leave each other’s lines in place.


const META_PREFIX: "memhtml-" = "memhtml-";

The memory file format’s naming conventions. HTML5 metadata names are a flat token space where colons are reserved-ish, and rel tokens cannot hold a colon at all, so both planes use the same hyphenated prefix.


const PERSON_ELEMENTS: readonly ["address"];

<address> is the contact surface a person file adds. Permitted everywhere rather than only under resources/people/, because this module sees HTML and no path, and a warning keyed on a directory would belong to memhtml doctor, not to the parser.


const REPEATABLE_META: readonly ["memhtml-entity", "memhtml-tag", "memhtml-alias"];

Meta keys that may appear more than once. Each value is its own <meta> element rather than a comma-joined string, so correcting one tag is a one-line diff.

memhtml-alias is the third: a person file declares the other names the same person is recorded under, so laura de-sabelin can state that laura is them. Entity resolution can read it as EVIDENCE and merge an alias-backed pair whatever the character distance says, which no string similarity can supply — laura against laura de-sabelin scores 0.476, below even the review band. One meta per alias, for the same one-line-diff reason.


const REQUIRED_META: readonly ["memhtml-type", "memhtml-status", "memhtml-created", "memhtml-updated"];

The metas a file must carry. Each of the five is a fact no pure function can invent: a type cannot be guessed from prose, a status cannot be inferred, and a timestamp cannot be synthesized without a clock. memhtml-confidence, memhtml-importance, and memhtml-author are deliberately absent, because the files table documents a default for each (1.0, 5, agent), so a hand-authored file missing them is completed rather than refused.


const VIOLATION_SEPARATOR: "; " = "; ";

How violations are joined into InvalidMemory.reason, which carries a single string.


const VOID_ELEMENTS: ReadonlySet<string>;

Elements with no end tag, per the HTML serialization algorithm. The serializer emits these as a start tag alone; emitting </meta> would make the file invalid HTML5.

function addLink(
html,
rel,
href
): string;

Append a <link rel="memhtml-…"> edge. Idempotent on the (rel, href) pair, in any spelling of the rel the reader accepts, because a caller can add the same edge on every run, as a session’s link op can, and a duplicated <link> would become a duplicated edges row.

string

| "supersedes" | "contradicts" | "caused_by" | "leads_to" | "part_of" | "relates_to" | "example_of" | "supports" | "laterally_related" | "about_person" | "authored_by" | "from_session" | "blocks" | "subtask_of"

string

string


function addMeta(
html,
name,
value
): string;

Append another <meta> of a repeatable name, a new memhtml-entity or memhtml-tag, after the last one already present. Adding a value that is already there is a no-op, so the operation is idempotent and a re-run of a bookkeeping pass cannot grow the head.

string

string

string

string


function articleHtmlFor(input): string;

The article markup for an input: articleHtml verbatim when it is supplied, and otherwise the paragraphs proseParagraphs plans for the claim and body.

NewMemoryInput

string


function blockTagInArticle(html): BlockTagInArticle | undefined;

The first <p>, or <mark> outside any <p>, in a memory file’s article whose text holds a block tag outside code, or undefined when none does.

A <p> is read whole, its <mark> included, because that is the paragraph the prose path rendered: the claim, a space, and the first body paragraph. A <mark> outside a <p> (the first <li> of authored markup) is read on its own. The text is what a reader sees, so &lt;p&gt; in the file reads as <p> here.

string

BlockTagInArticle | undefined


function blockTagInRun(text): BlockTagInRun | undefined;

The first block tag in a paragraph’s text outside a backtick span, or undefined when there is none. The text is read as the HTML parser will hold it in a <p>: line breaks in all three spellings, NULs dropped. A backtick span opens and closes on one line, so a tag on a line of its own is prose even when the lines around it carry backticks.

offset is the tag’s position in text as given, and tag is the tag sliced from the line as the parser reads it (backticks kept, NULs dropped), cut to TAG_SHOWN characters.

string

BlockTagInRun | undefined


function canonicalArticleText(root): string;

The digest’s input. canonicalText with <pre>/<code> kept, so everything is hashed.

Node

string


function canonicalText(root, options?): string;

The exact string the digest is taken over: article text with runs of ASCII whitespace collapsed to one space, <pre> descendants passed through byte for byte.

Block-element edges contribute a collapsible space, so the hash is a function of the article’s words and not of its indentation: <li>one</li><li>two</li> and the same list pretty-printed across lines yield one digest. Without that edge space the flat form would canonicalize to onetwo, so reformatting a file would move its dedup key while changing nothing a reader can see.

The outer trim is applied only to a leading or trailing collapsible segment. Trimming the whole result would let <pre> a</pre> and <pre>a</pre> hash identically, and the leading whitespace of a code sample is exactly the kind of difference a <pre> exists to keep.

Node

CanonicalTextOptions = {}

string


function checkDocument(document): CheckResult;

Check a parsed document against all six constraints. Violations are collected rather than short-circuited, so one parse tells an author everything wrong with the file.

Document

CheckResult


function checkMemory(html): object;

Check a file without building a doc: the structured { violations, warnings } memhtml doctor reports. Total, so a completely malformed string yields violations instead of throwing.

string

object

readonly violations: readonly string[];
readonly warnings: readonly string[];

function closesFence(line, opening): boolean;

True when a line closes a fence opened by opening: same-or-longer backtick run, nothing else.

string

string

boolean


function contentHash(input): string;

The content hash of an article, from a parsed document, an article node, or the article’s inner HTML. Passing whole-file HTML works too: the <article> element is located first, so head content does not reach the digest.

string | Node | HashableArticle

string


function detect(code): Detection;

hljs’s own evidence, squeezed into the sweepable scalar the eval calibrated.

confidence = 1 - exp(-(top - runnerUp) / lines), the per-line evidence MARGIN between the best language and its closest REAL competitor. Two refinements carry their weight, and a grid search over the alternatives (relative margin, top score alone, saturating absolute margin) put all of them at ≤9% coverage against this shape’s 25%:

  • Canonical-aware runner-up: when the runner-up normalizes to the SAME token as the winner (pgsql vs n1ql, xml vs xhtml), the margin is the FULL top score. A dialect duel is not disagreement about what to stamp, and charging it as one would abstain on the cases the detector is most right about.
  • Per-line normalization: absolute relevance grows with snippet length, so a raw margin conflates “confident” with “long”. Margin per line does not.

hljs runs its FULL grammar set deliberately. Restricting highlightAuto to the 12-name vocabulary measured 0.9% coverage against 25% at the precision floor: with the true language’s grammar absent, a runner-up wins unopposed and wins CONFIDENTLY, so the false positives land exactly where the threshold cannot see them.

string

Detection


function detectLang(code):
| "html"
| "javascript"
| "typescript"
| "python"
| "bash"
| "json"
| "sql"
| "yaml"
| "toml"
| "css"
| "go"
| "rust"
| undefined;

The canonical language to stamp on an unlabeled fence, or undefined to stamp nothing.

Both gates are independent and both must pass: the detection must be IN the vocabulary, and its confidence must reach DEPLOY_THRESHOLD. A confident detection of a language outside the vocabulary is still undefined. Measured on the corpus, hljs names smali at confidence 0.86 and autohotkey at 0.55 for snippets that are really bash and TypeScript. Confidence says only “this grammar won by a wide margin”, never “this grammar is one we stamp”.

Pure and synchronous: hljs is synchronous, so the write path calls this inline with no Effect.

string

| "html" | "javascript" | "typescript" | "python" | "bash" | "json" | "sql" | "yaml" | "toml" | "css" | "go" | "rust" | undefined


function escapeAttribute(value): string;

Escape an attribute value. Double quotes are the fixed quote style, so < and > need no escape. Line breaks become character references so an emitted <meta> or <link> is always one source line: the head editors splice by line boundaries, so a literal newline inside an attribute would split one head entry across two of their lines. \r is referenced too, because the HTML preprocessor folds a literal one into \n on re-parse.

string

string


function escapeText(text): string;

Escape a text run. U+00A0 becomes &nbsp; because a literal no-break space is invisible in an editor and reads as an ordinary space in review. An invisible character in a memory’s claim is a trap, so the file names it.

string

string


function fencedBlockOf(paragraph): FencedBlock | undefined;

Parse a paragraph as a fenced block, or undefined when it is not one.

The whole paragraph must be the fence: an opening line, the code, a closing line. Prose sharing a paragraph with a fence is not a fence. The paragraph splitter keeps a block intact as its own paragraph, so a mixed paragraph means the author did not blank-line-separate their fence, and escaping it as text is the graceful reading of that.

An unterminated fence (opener, no closer) is also undefined. Rendering it as code would commit markup the author may not have finished. Rendering it as escaped text keeps the backticks visible, so the file says what the author typed.

string

FencedBlock | undefined


function fenceOpeningOf(line): string | undefined;

The backtick run of a fence-opening line, or undefined when the line opens no fence. The run’s length is what a closing line must meet or exceed.

string

string | undefined


function isContentHash(value): boolean;

True when a value is a well-formed sha256:<64 hex> digest.

string

boolean


function isMemoryMetaName(name): name is "memhtml-entity" | "memhtml-tag" | "memhtml-alias" | "memhtml-type" | "memhtml-status" | "memhtml-created" | "memhtml-updated" | "memhtml-confidence" | "memhtml-importance" | "memhtml-content-hash" | "memhtml-author" | "memhtml-session" | "memhtml-prompt" | "memhtml-turn" | "memhtml-valid-from" | "memhtml-valid-until" | "memhtml-reprieves" | "memhtml-archived" | "memhtml-superseded-by" | "memhtml-needs-revision" | "memhtml-task-status" | "memhtml-due";

True when a memhtml--prefixed metadata name is in the closed vocabulary.

string

name is “memhtml-entity” | “memhtml-tag” | “memhtml-alias” | “memhtml-type” | “memhtml-status” | “memhtml-created” | “memhtml-updated” | “memhtml-confidence” | “memhtml-importance” | “memhtml-content-hash” | “memhtml-author” | “memhtml-session” | “memhtml-prompt” | “memhtml-turn” | “memhtml-valid-from” | “memhtml-valid-until” | “memhtml-reprieves” | “memhtml-archived” | “memhtml-superseded-by” | “memhtml-needs-revision” | “memhtml-task-status” | “memhtml-due”


function isRepeatableMeta(name): name is "memhtml-entity" | "memhtml-tag" | "memhtml-alias";

True when a metadata name may legitimately appear more than once in one head.

string

name is “memhtml-entity” | “memhtml-tag” | “memhtml-alias”


function isRootRelativeHref(href): boolean;

True when a <link> href is the document-reference form the format requires: repo-root-relative with a leading slash, no scheme, no host, no .. segment.

A relative href would break on the first git mv of the source file and a protocol-relative //host/x would silently leave the repo, so both are refused rather than normalized.

string

boolean


function isValidDatetime(value): boolean;

True when a datetime value is one this format accepts: a calendar date, or a date with a UTC time. Narrower than HTML’s own datetime grammar (which admits durations, weeks, bare times, offsets, and variable precision) because files.event_at and files.due_at are compared and ordered as strings. A value that does not sort lexicographically alongside the others would corrupt the recency arm and the overdue query alike.

Range is checked too, so 2026-13-45 is refused rather than stored as an unsortable date.

string

boolean


function metaPairs(doc): readonly (readonly [
| "memhtml-entity"
| "memhtml-tag"
| "memhtml-alias"
| "memhtml-type"
| "memhtml-status"
| "memhtml-created"
| "memhtml-updated"
| "memhtml-confidence"
| "memhtml-importance"
| "memhtml-content-hash"
| "memhtml-author"
| "memhtml-session"
| "memhtml-prompt"
| "memhtml-turn"
| "memhtml-valid-from"
| "memhtml-valid-until"
| "memhtml-reprieves"
| "memhtml-archived"
| "memhtml-superseded-by"
| "memhtml-needs-revision"
| "memhtml-task-status"
| "memhtml-due", string])[];

The metas of a doc as [name, content] pairs in META_ORDER, repeatable keys expanded to one pair per value. A meta whose value is absent contributes nothing, so the file states what it knows and the files defaults cover the rest.

MemoryDoc

readonly (readonly [ | "memhtml-entity" | "memhtml-tag" | "memhtml-alias" | "memhtml-type" | "memhtml-status" | "memhtml-created" | "memhtml-updated" | "memhtml-confidence" | "memhtml-importance" | "memhtml-content-hash" | "memhtml-author" | "memhtml-session" | "memhtml-prompt" | "memhtml-turn" | "memhtml-valid-from" | "memhtml-valid-until" | "memhtml-reprieves" | "memhtml-archived" | "memhtml-superseded-by" | "memhtml-needs-revision" | "memhtml-task-status" | "memhtml-due", string])[]


function newMemoryDoc(input): MemoryDoc;

A fresh MemoryDoc for an input, with memhtml-content-hash already stamped from the article the template just built, so the file that reaches disk is self-consistent and the indexer’s own recomputation agrees with it on the first read.

NewMemoryInput

MemoryDoc


function normalizeLang(raw):
| "html"
| "javascript"
| "typescript"
| "python"
| "bash"
| "json"
| "sql"
| "yaml"
| "toml"
| "css"
| "go"
| "rust"
| undefined;

Canonical token for a raw language name, or undefined when it is outside the vocabulary.

string

| "html" | "javascript" | "typescript" | "python" | "bash" | "json" | "sql" | "yaml" | "toml" | "css" | "go" | "rust" | undefined


function parseMemory(html): Effect<MemoryDoc, InvalidMemory>;

Parse a memory file. Fails with InvalidMemory whose reason is every violation joined by VIOLATION_SEPARATOR. The error type carries one string, so the list is joined rather than smuggled through a field the frozen contract does not have. A caller that wants the structured list calls checkMemory.

string

Effect<MemoryDoc, InvalidMemory>


function proseParagraphs(claim, body): readonly ProseParagraph[];

The paragraphs the prose path renders for a claim and body, in order: the claim’s <p>, with the first body paragraph joined onto it as its tail, then each remaining paragraph. Empty paragraphs are dropped rather than emitted, so a trailing blank in the tool payload does not become an empty element.

A body paragraph that is a fenced code block, whole, becomes a <figure><pre><code> instead of a <p>, the one way the prose path can author real code markup. A fence is not joined onto the claim’s paragraph. When the first body paragraph is a fence, the claim stands alone in its <p> and the figure follows, so the claim still leads the article (constraint 1) and code stays out of the sentence.

Exported because the doors judge a body by what it renders to: the text of each <p> here, read by blockTagInRun, is the text blockTagInArticle reads from the file.

string

readonly string[]

readonly ProseParagraph[]


function proseParagraphText(paragraph): string;

The text a rendered <p> holds once read: its claim, a space, and its body paragraph.

ProseParagraph & object

string


function readLinks(html): readonly MemoryLink[];

Every <link rel="memhtml-*"> edge in the head, in document order. The read half of addLink/removeLink, tolerant the way readMeta is: the document is parsed but nothing is validated — no MemoryDoc, no constraint check — so a caller holding raw bytes can ask what a file points at without the file having to be a well-formed memory. An unknown rel token is dropped exactly as parseMemory’s own reader drops it.

string

readonly MemoryLink[]


function readMeta(html, name): string | undefined;

The value of a head meta, or undefined. The read half of setMeta, no parse needed.

string

string

string | undefined


function removeLink(
html,
rel,
href?
): string;

Drop a <link rel="memhtml-…"> edge, in every spelling of the rel the reader accepts. Omitting href drops every link of that rel; naming one drops just that pair, which is what the integrity phase does when it replaces a dangling href with the target’s new path.

string

| "supersedes" | "contradicts" | "caused_by" | "leads_to" | "part_of" | "relates_to" | "example_of" | "supports" | "laterally_related" | "about_person" | "authored_by" | "from_session" | "blocks" | "subtask_of"

string

string


function removeMeta(
html,
name,
value?
): string;

Drop a <meta> of a name, one whole line each. Omitting value drops every meta of that name; naming one drops just the metas carrying it, which is how a session’s unlabel op takes one memhtml-entity off a file and leaves its other entities alone. A name or value that is absent is a no-op. Like removeLink, the cut is outside the article, so the content hash is unchanged.

string

string

string

string


function renderTemplate(input): string;

A fresh memory file as bytes. The one function the write path calls. The extraction fields on the intermediate doc are empty because serialization reads only article.html, and the real extractions come back from parseMemory on the next read.

NewMemoryInput

string


function serializeMemory(doc): string;

Serialize a doc to a whole file. Inverse of parseMemory on the fields the format carries: parseMemory(serializeMemory(doc)) yields doc again, which is the property that lets a caller read, adjust, and write a file back without touching content it did not mean to touch.

article.html is re-parsed and re-serialized rather than interpolated, so a caller that hand-assembled a doc still gets the canonical, fixed-point markup the round-trip needs.

MemoryDoc

string


function setMeta(
html,
name,
value
): string;

Set a head meta to one value, replacing the first <meta> of that name in place or inserting a line in META_ORDER position. Exactly one line changes, and the article stays outside the edited range, so contentHash(setMeta(html, name, value)) === contentHash(html) holds for every name, repeatable ones included.

On a repeatable key this sets the FIRST value. addMeta is the append. An unknown name is refused by returning the input unchanged rather than writing a meta memhtml doctor would immediately flag.

string

string

string

string


function writeChildren(parent): string;

The inner markup of a parent node: its children, the node itself excluded.

ParentNode

string


function writeOuter(node): string;

A node and its subtree, the node included.

Node

string