Skip to content

The fidelity contract

A projection binds a source — a codebase, a filesystem, another mem — to a destination mem and populates it. The fidelity contract is the promise the engine makes about that mem afterward: it will tell you, deterministically and without inventing numbers, how faithfully the mem still reflects the sources its binding declares.

memstead projection verify <binding> computes that measurement. It mutates no entity in the destination — it reads the store, the anchors, and the source, records durable findings, and renders a report. It is not a pure read, though: see Verify writes below. Repair is a separate operation (sync); verify only measures.

Fidelity is reported as three things, always relative to a declared binding — a mem is never judged against an absolute ideal, only against what its own binding says it should contain:

  • Coverage — how much of the source in scope is accounted for in the mem. The denominator is the per-medium enumeration of the source set, S(D); the report states that provenance rather than quoting a bare percentage.
  • Accuracy — of the content that is anchored back to a source, how much still matches that source (anchor resolution).
  • Freshness — how the mem’s recorded baselines compare to the current state of the source it was built from.

The report never blends these into a single score. Each is stated on its own, with the honest denominator behind it.

Coverage is grain-weighted, never laundered

Section titled “Coverage is grain-weighted, never laundered”

Anchors come at different grains. A file anchor covers one file; a tree anchor covers a directory that may fan out over hundreds of files. Folding a one-entity, two-hundred-file tree anchor into a flat “coverage %” would make a sparsely-anchored mem look exhaustively covered.

So the report keeps tree-anchor fan-out on its own axis — a tree anchor shows as one anchor fanning out over N files, never merged into the file-level percentage. A reader sees direct file/span coverage and tree fan-out as two separate facts, and can judge for themselves how much confidence the tree anchor earns.

Every anchored entity carries a provenance class that says how it came to reference its source:

  • anchored — a hash-bearing anchor to a specific source artifact; its content can be checked against the source.
  • derived — computed or synthesised from one or more input artifacts (derived_from lists them). Hash-bearing like anchored, so it takes part in drift adjudication; an entity-grain derived row written without a hash is pinned at write to its target’s prepared hash (hash_source: pinned), so the next verify reads it against the target as it was when the claim was made.
  • authored — a human or agent wrote it directly; the source informed the author but does not own the bytes.
  • informed-by — the artifact shaped the entity without being reproduced in it.

authored content is excluded from the coverage and accuracy denominators and reported as its own bucket. Measuring authored prose against a source it was never meant to reproduce would manufacture false drift; the contract refuses to do that, and says so where the excluded bucket appears.

Quoted spans: the claim is the words, not the page

Section titled “Quoted spans: the claim is the words, not the page”

A hash-bearing anchor on the url, span or file grain can carry the verbatim words the entity quotes, as span beside content. The write checks that the span occurs in the supplied content and refuses (INVALID_ANCHOR, naming the span) when it does not; the stored row then carries the span, a hash over it (span_hash) and the document’s prepared hash. A span written without content is accepted with the span unverified until an observation with text adjudicates it.

From then on the row is adjudicated on the span’s presence, not on the document hash: it resolves while the words are in the observed text, whatever else on the page changed (the new document hash is kept as the row’s last observation), and reads span_absent once they are gone. It never reads drifted: the row’s claim is the span, and the page around it moving is not the claim moving. Two entities quoting one document from two different extractions resolve from one observation as long as each span is present, which is what a whole-document hash could not give them. The span is part of the row’s identity beside artifact, grain and class, so several spans on one document are several rows, each with its own state, and anchors_unset can name a span to remove one.

The comparison runs in the canonical span form and is exact beyond it. The form removes exactly the noise two extractions of one document disagree on: Unicode NFC composition; every run of whitespace (spaces, tabs, line breaks) to one space; soft hyphens removed; a hyphen directly followed by a line break removed, so a word split at a line end matches its unsplit form; typographic quotes, guillemets and dashes to their ASCII forms; the ligatures ff fi fl ffi ffl resolved to their letter pairs. Nothing else: a changed digit, a comma against a point, a changed unit, a changed case, a reordered word or a dropped negation reads absent, and no similarity threshold exists anywhere on the path. Case folding is deliberately absent (capitalisation carries meaning in the sources’ languages), and so is NFKC (it would equate m² with m2).

An absent observation reads recheck for a span row as for any other: a failed retrieval is not the source saying the words are gone. A hash observation carries no text to look in and leaves the row as it was.

The anchor states, then: resolves, drifted, recheck, orphaned (the artifact is gone) and span_absent (the artifact stands, the quoted words do not). Span rows are stored in anchors sidecar version 3; an engine that reads only versions 1 and 2 refuses such a sidecar typed rather than loading the rows with their span dropped.

Url anchors: observed by you, aged by the engine

Section titled “Url anchors: observed by you, aged by the engine”

The engine never fetches. A url anchor therefore has no live observation of its own: it is written with the retrieved text as content (the engine records the hash), and it is re-adjudicated only when someone supplies a fresh observation, memstead verify-anchors --mem <m> --observations <file> with rows of {artifact, hash | content | absent: true, observed_at?}. A supplied row runs through the same resolution a file anchor does; a row with no observation stays unobserved, never scored. Each observation is recorded on the anchor row (last_observed), and from then on every surface that shows the row’s state also shows how old that state is, unobserved for N days, because a state observed months ago is not today’s state.

Set hash_stability: stable on an immutable document, a dated PDF, an archived page, a versioned standard, so that a changed hash reads as drifted. Leave the default unstable for a living page, where a change is only a recheck. The choice sits with the author because only the author knows which kind of document the URL names.

A mem can carry several bindings, and the anchors in it are not all the same binding’s business. A report answers for exactly one population: the anchors the binding under verification is responsible for. Membership is decided by what the anchor records about its own origin where it records one, and by the binding’s declared scope where it does not, so an anchor pointing at an artifact this binding’s scope does not cover is not counted against it.

Excluded anchors are named, never dropped. The report states how many were excluded because another binding wrote them and how many because they sit outside this binding’s scope, and lists the artifacts. Exclusion is a reporting decision: nothing is deleted, rewritten, or marked invalid, and an excluded anchor cannot raise a finding against a binding that does not answer for it.

The coverage axis handles its own exclusions the other way round, and the difference is deliberate. An artifact you rule out with memstead projection exclude is dropped from coverage.uncovered and counted beside the figure as coverage.excluded, because that array answers a different question: what is still owed. A reader counts it and a gate reads it, so an entry left inside it stays owed however it is annotated, whereas an anchor excluded from a population is not owed by anyone and naming it is the whole point. Nothing is lost either way: each dropped artifact keeps its reasoning under “Excluded on purpose” in the rendered report and in disposed_excluded_rationales in the JSON. The rule, in one line: a population exclusion is named in place, a coverage exclusion is moved out and counted.

Coverage has an entity side too. A destination entity that holds no anchor at all is backed by nothing in any source, so no verify can speak to it; the report names those under coverage.unanchored_entities, a reading and never a verdict, since under curated semantics an entity can legitimately be authored from no single artifact. The ones that carry no anchor on purpose (a withdrawn claim kept as the record of what was claimed, an “about this graph” entity) are declared with memstead projection exclude <binding> --entity-exclusions '{"<id>": "<why>"}': they leave that list, are counted as coverage.excluded_entities, and keep their reason under “Entities excluded on purpose” in the rendered report and excluded_entity_rationales in the JSON. The same rule as the artifact side: moved out, counted, and named with the reasoning.

Between the two sides sits the claim no verify watches: an entity whose prose names an in-scope artifact by path and carries no anchor on it. Every anchor the entity holds may resolve while a sentence about a file it never anchored stands falsified. The verify pass records one unanchored-mention finding per entity and artifact (prose only; a path inside a fenced code block is not a claim), naming the section, and the coverage block carries the count beside uncovered. Path matching follows the binding’s source join, so the source-relative and the workspace-relative spelling of one file are one artifact. It is a finding, never a refusal: the write that names the file still lands. The remedy is the anchor (memstead_update with anchors, so the next verify watches the claim) or, where the artifact is mined and deliberately warrants no entity, memstead projection exclude with a rationale, after which mentions of it raise nothing. The sync brief lists the open ones beside the drifted and uncovered findings, and memstead health carries each as an UNANCHORED_MENTION warning, so the exposure is work with an owner rather than an invisible number.

The report also states what its denominator counted. One artifact legitimately carries several anchors at different grains or classes, so the row count and the distinct-artifact count are printed side by side rather than the rows being merged into one figure a reader would misread as artifacts.

Anchors written before anchors recorded their producing binding carry no such record. Those are counted, not discarded: filtering them out strictly would empty the axis for every mem written before the field existed, and an empty report reads as success. The report says how many anchors it counted on that basis, so a population established by provenance is distinguishable from one resting on the fallback.

An anchor ties an entity to a source artifact, and the measurement above checks the artifact end: does the source still say what was recorded. The other end is the entity itself, and it can vanish. The engine’s own delete and rename paths take an entity’s anchors with it, so this only happens when something writes the mem from outside: a sibling process, a branch reset, a file removed by hand, an archive installed over the top.

A sidecar row naming an entity the mem no longer holds is reported as dangling, on the binding report and on the standalone anchor surface alike. It is not one of the four states, which describe the artifact end: a vanished entity says nothing about the source, and the repair is not the same one. An orphaned anchor asks whether its entity should be re-anchored or pruned; a dangling row asks why the entity went missing. It counts toward no figure, so it can no longer resolve at a hundred percent for an entity that does not exist, and it does not make an artifact count as covered.

Nothing repairs it. Deleting the row would tidy the sidecar and erase the only remaining evidence that something wrote the mem behind the engine’s back.

The check reads an absence from the loaded graph, which is only evidence when the graph holds everything the mem has. Where it does not (a mem whose lazy load has not run, or one with a file that failed to parse) no row is called dangling and the report says why instead. A mem that is not mounted or is quarantined gets no report at all: both surfaces refuse it before measuring. A clean anchor axis over an entity end nobody examined would be exactly the false assurance this contract exists to prevent.

A figure never travels without its population

Section titled “A figure never travels without its population”

Anchor resolution is reported as a percentage, and a percentage is read as health. That is the number a CI gate branches on and the number a person scans for. It is also the number that can be perfectly correct and still mean almost nothing: correct over whatever happened to be in the sidecar, over rows nobody managed to look at, over spans nobody checked.

So every surface that reports the figure reports the population it was computed over in the same breath: how many rows, over how many distinct artifacts, what was excluded and why, and how much could not be adjudicated. Figure and statement render as one unit, so a compact or budget-trimmed rendering cannot carry the number and drop the caveat.

Rows that could not be adjudicated make the verdict inconclusive, not clean. An anchor the pass could not observe, a span never checked against its artifact, an entity end nobody reconciled: each is an absence of measurement, and an axis resting on one has not earned a clean verdict. Exclusions do not, and the difference is the point. An out-of-scope or other-binding anchor is a complete, correct answer about a row this binding does not answer for. Treating a known exclusion as an unknown would destroy exactly the distinction this contract is built on.

The same distinction runs through the standalone anchor surface, which is where a reader arrives without a binding in hand. An artifact that is gone is a measured failure; an anchor that could not be observed is the absence of a measurement. They used to share one bucket.

Not every check costs the same, so verification is layered — cheap deterministic measurement first, expensive judgment last and only under a budget:

  1. Deterministic measurement — coverage, anchor resolution, and freshness are computed on every verify with no judgment call. This is the tier-1 fidelity report.
  2. Hash adjudication — for hash-bearing anchors over a stable medium, the prepared-content hash is compared to the recorded one. A mismatch is a drifted finding. Still deterministic, still no model call.
  3. Sampled deeper adjudication — mismatches a hash cannot settle are adjudicated under a per-run cap, sampled on a rotation so no artifact is starved, with a level-triggered full walk that guarantees eventual coverage of an enumerable source. Whatever the cap defers is queued as the adjudication backlog, and its depth is reported — deferred work is visible, never silently dropped.

Check records travel with a sealed archive

Section titled “Check records travel with a sealed archive”

The judgment tier leaves a trace the engine records rather than infers: a check record per entity and kind (verification, conformance, a foreign x-<name> kind), with the verdict, the method, the checker’s declared identity handle and the entity’s hash at check time. Those records are workspace state, and a sealed .mem archive carries the latest one per entity and kind in an optional member, .memstead/checks.json, so an installed or served mem answers “who checked this, and is that check still fresh” from the archive alone. Freshness on an archive mount is the workspace derivation applied to the sealed entity: a record sealed over a later edit reads check_stale, never fresh. An archive without the member reads never_checked and says so; a workspace ledger beside the mount is never consulted for it. Identities travel exactly as declared, and the method note passes the provenance member’s redaction; nothing is anonymised and nothing is mapped to a person. Each sealed record also carries the independence reading the export derived for it from the source mem’s provenance (the archive has none of its own), so the mount answers “was that check independent of the executors” the way the source engine did at export time.

The report is engine-rendered and deterministic — no model call, so two runs over the same state produce byte-identical output. It opens with a rollup verdict and the top concrete actions (“3 entities describe deleted code — run sync”), with the underlying numbers available as drill-down. An operator reads the verdict; an agent reads the actions; both come from one computation.

Where a medium cannot support a measurement, the report says so as a degradation rather than faking a green result. A medium with no change signal renders freshness as “unknowable,” and a green freshness verdict is structurally unreachable for it — the contract would rather admit a blind spot than paper over one.

Where there is no mem to measure, there is no report. A binding whose destination mem this workspace does not serve (one that does not exist yet, which projection init allows, or one quarantined at boot) is refused before verify observes or records anything: UNKNOWN_MEM (exit 3) or MEM_QUARANTINED (exit 5), with the remedy in the message. A report over it would count every in-scope artifact as uncovered and every anchor as absent, and the onboarding verdict (“this mem predates its binding”) would be false: a mem that is not there predates nothing. The verify and sync briefs refuse the same binding the same way, and memstead status lists it as an action rather than as onboarding.

Three caps bound what the contract can currently claim — positioning decisions and known gaps, stated plainly rather than left silent or dressed up as imminent features. If you are gating a pull request on verify (the CI guide), these are the edges of what the gate can see:

  • Web-medium sync and enumeration. A web medium can be named and read, but the engine does not enumerate or maintain it. Because its capability row advertises no enumeration and no retrievable base, a sync or enumeration-dependent operation cannot be declared against a web medium: projection init scaffolds the binding build-only and says which operations it dropped and why; projection enable sync refuses outright; and a record that carries one anyway is refused at validation. Asking a web binding to sync therefore names the capability gap itself, never a remedy no medium can honour.

  • Preparation is a registry, and it ships four preparations. A source’s preparation names a preparation the engine registers: entity-load-bearing for graph sources (an entity anchor hashes its type’s load-bearing sections, so a notes-only edit does not drift a dependent), dated-entries for path-shaped sources (a file of dated entries is delivered as units <path>#<stamp> in stamp order, identical on every pass, and a unit anchor drifts only when its own entry changes), code-map for code sources (a file anchor hashes the file’s interface digest and a tree anchor the code map of the scoped files under it, so an implementation edit does not drift an anchor and a signature change does, within the digest’s recorded limits; the digest is heuristic, by language family), and quoted-phrase for citations (an anchor <path-or-url-or-entity>#<phrase> resolves while the text still says those words and reads orphaned once they are gone, whatever else changed). A tree anchor on a source without a code map is recorded but never hashed: it resolves recheck, not drift. Non-text media conversion (PDF, DOCX, audio) is a non-goal: an agent with a capable read tool extracts, and the prepared-content hash already falls back to a raw-byte digest for a binary artifact, so drift over a PDF is detected without any preparation. An identifier the registry does not know is refused at declaration on every edit path; a hand-edited record carrying one is skipped at run time with the registered set named, never run over content the engine cannot prepare. The registered set is listed in the binding reference.

  • The mtime #synced baseline does not survive a fresh checkout. A binding using mtime change-detection compares modification times against a recorded baseline, and a CI clone gives every file a new mtime, so the baseline is meaningless there. This bounds the changed-source slice — what sync acts on. verify adjudicates anchors by content hash, so a bumped-mtime checkout still verifies clean; the cap is real for the loop, milder for the gate. Use git change-detection for anything you intend to gate.

The first two are positioning decisions — deliberate boundaries, not roadmap promises. The last is a known defect with a fix planned: the mtime baseline’s non-portability is a portability bug, not a choice. The distinction matters to anyone deciding what to build a gate on — a boundary will still be there next year; a defect should not be. When the defect is fixed, its refusal becomes a measurement and the paragraph retires.

Graph-medium verify was on this list until it started measuring: a graph-source binding now enumerates the source mem’s in-scope entities as a real denominator and resolves its entity anchors against the live graph, so a stale-pinned anchor over a changed source entity reports drifted like any other.

None of the three is omitted: the honest shape of a contract is to name its own edges, and to say which of them it intends to keep. The list shrinks when a cap is fixed rather than when it is quietly dropped — graph-medium verify left it that way.

Verify is not a read-only command, and no part of this contract should be read as saying it is. A completed run records its findings store and backfills observed content hashes onto hash-less anchors in the mem’s anchors sidecar (on a mem-repo-backed mem, a backfill that records anything is a commit). The #verified baseline, the one write into the mem’s own config, moves only when you pass --advance: a bare projection verify leaves that config byte-identical, so a gate that verifies in order to read does not make the binding look freshly verified. The measurement pass itself takes a shared engine borrow and is structurally incapable of mutating an entity; what it writes is measurement bookkeeping, and a failed or aborted run never advances the baseline. On CI’s ephemeral checkout this is harmless. In a working tree you need pristine, it is not.