Development record
Amnesia is a repository property, proven by an eval
Summary
spc-65 delivers itd-187: a repository eval that assembles one reading definition twice over an unchanged repository state and asserts the two assembled inputs are byte-identical, the manifest excluded from the comparison. Amnesia becomes a property of what the assembler passes, checkable by anyone with the repository, rather than an instruction to an agent that only a case run could evidence. The decision log entry of 2026-08-28, ruling (3), is explicit: amnesia is proven by a repository eval, never by a case run, which is why the closing run of the cycle carries only purpose durability and convergence.
The eval also pins the two determinism preconditions itd-187 places on the
assembler (itd-183,
spec spc-61):
hash-only manifests with no timestamps, and a lexicographic walk order. It lands
in the existing smoke harness package (evals/)
behind the smoke build tag, so make smoke and CI's smoke job run it with
no change to Makefile or
.github/workflows/ci.yml. Passing in
CI is a cycle exit criterion.
spc-61 is being written concurrently. Where it is still a stub, this spec designs against itd-183's own text and the assembler row of the cycle build plan, and inherits the four interface demands spc-64 places on it rather than adding a fifth.
Scope
In: the double-assembly comparison and the identity relation it asserts; the exclusion of the manifest from that comparison and the separate, weaker assertions made about the manifest itself; the independent lexicographic order oracle; the order-adversarial fixture that gives the oracle something to catch; the comparator meta-test that proves the comparison can fail.
Out: everything under Out of scope. In particular this eval asserts nothing about what the assembler included, which is spc-64's property.
Approach
The identity relation
Two assemblies of the same definition over the same repository state at the same commit must produce byte-identical assembled input. The comparison is a byte comparison of the whole artefact, not a semantic one: a semantic comparison would tolerate exactly the reordering and re-serialisation this eval exists to refuse.
The manifest sits outside the comparison, because it legitimately carries a run identifier that differs between runs. It is not, however, unasserted. Two weaker properties are checked on each run's manifest: no key or scalar value in it is timestamp-shaped, and its item paths appear in lexicographic order. That is the whole of itd-187's "determinism preconditions it enforces on the assembler", made concrete.
Assembling twice, from two paths
The eval materialises the shared fixture corpus
(evals/testdata/cold-reading/baseline/, owned by spc-64), initialises it as a
git repository, and makes one commit. It then copies the whole tree, .git
included, to a second temporary directory, so the second assembly runs at a
different absolute path over an identical commit.
Assembling from two different paths is a deliberate strengthening of itd-187's criterion, decided here: a run-to-run comparison in one directory cannot see an absolute path or a temporary directory name embedded in the output, and that leak is both a determinism failure and a breach of the repository's rule that no absolute local path enters an artefact. Comparing across paths catches a path the two runs DIFFER in, on the first run.
It cannot catch one they SHARE. Both trees are created under one temporary parent, so a parent embedded in both outputs leaves the comparison agreeing and reporting nothing — the same blindness one level up. The artefacts are therefore also held to a path detector, on two mechanisms: every ancestor of the fixture's repository root and HOME up to the process temporary directory, which is where this harness's own directories stop and the machine's begin, and a shape match for any other absolute path, which reaches the machine's own directories that no list of known paths could enumerate. The comparison and the detector reach different halves of one rule.
The order oracle
Byte-identity alone is satisfied by any order the assembler picks consistently, including a filesystem-dependent one that changes on another machine. So the eval carries its own oracle: it collects the item paths from the assembled input, sorts a copy with the eval's own lexicographic sort, and asserts the two agree. The oracle is the eval's sort, never the assembler's, which is what makes this a check rather than a restatement.
evals/testdata/cold-reading/order/ gives the oracle something to catch: a set
of records whose names sort differently under byte order, case-insensitive
order, and numeric-suffix order, and whose creation order is deliberately not
their sorted order. The eval owns this directory; spc-64 shares the corpus it
sits in.
Decisions this spec makes that the record did not
- The timestamp scan is confined to the manifest. Projected record bodies legitimately quote dates in prose, so a timestamp scan over the assembled input would fire on the record's own text. The manifest carries paths, field names, and hashes only, so a timestamp-shaped token there is unambiguously a defect.
- Identity is asserted across two paths, per the reasoning above, rather than twice in one directory.
- The comparison is over the assembled-input artefact as a whole, taken as a file from the assembler's dry-run output directory rather than from stdout, so that interleaved diagnostics can never be mistaken for a difference.
- The eval binds to the verb, not the package, matching spc-64: it invokes
the built binary through the harness's existing
TestMainbuild andrunhelper inevals/smoke_test.go. Here the reason is not independence from the include table but that a repository property must be checkable by running the shipped binary. - One definition, not four. itd-187's claim is about the assembler's determinism, which is position-independent, so the eval asserts identity at one position and, for cheapness rather than principle, repeats the order oracle at each of the others.
Wiring
Go test files in package evals with //go:build smoke || coldreading, so the
same files run in the smoke harness and in the cold-reading lane. make smoke
runs go test -tags smoke ./evals/...; make evals-cold-reading runs
go test -tags coldreading ./evals/..., and the cold-reading-evals CI job runs
that target. The target and the job were landed by spc-64, and selection is by
build tag rather than by test name, so this spec's tests joined both lanes with
no Makefile and no workflow edit of their own. The same
caveat spc-64 records applies: on a pull request confined to docs/,
.abcd/development/, .abcd/work/, and the root prose files, the diff
classifier stands the smoke job down, and the merge-queue entry, which runs
the full set on the would-be merge result, is what gates the merge.
Ruled by the maintainer, 2026-08-30 — the evals get their own always-run CI
job. The smoke job stands down on a pull request confined to docs/,
.abcd/development/, .abcd/work/ and the root prose files, and those are
precisely the paths these evals read: a record-only change is the diff most able
to introduce warm content into material the assembler includes, so standing the
eval down there is anti-correlated with the risk. A stood-down job still reports
its check context green, which is a green for work that did not happen.
The remedy follows the reasoning ci.yml already documents for the ubuntu unit
lane, which never stands down because its tests read the live tree under the
allowlist. So: a small CI job carrying no inert condition, running these two
evals alone behind their own make target, while the rest of the smoke harness
keeps standing down. The workflow edit lands with this spec's build, reviewed
alongside the evals it serves; the merge-queue entry continues to run everything
regardless.
Acceptance criteria mapping
The criteria were split on 2026-08-31, before this spec was built, because the second criterion as written named a precondition no eval may establish for itself. The numbering below is the positional authority ac-1..ac-4.
| itd-187 criterion | How spc-65 satisfies it | Pinned by |
|---|---|---|
| ac-1 — two assemblies from two distinct paths over one commit are byte-identical, manifest excluded | Two assemblies over one commit from two temporary directories, compared as whole artefacts taken from the dry-run output directory | TestAssembledInputIsByteIdenticalAcrossRuns |
| ac-2 — item paths agree with the eval's own lexicographic sort | The order oracle is the eval's sort, never the assembler's, run against the order-adversarial fixture, so a consistent-but-not-lexicographic order fails where byte identity alone would accept it | TestWalkOrderIsLexicographic, TestFixtureOrderIsAdversarial |
| ac-3 — a timestamp-shaped key or scalar in the manifest fails | The timestamp scan is confined to the manifest, which carries paths, field names and hashes only, so a timestamp-shaped token there is unambiguously a defect | TestManifestCarriesNoTimestamp |
| ac-4 — the comparator reports a difference naming the differing item | Two synthetic pairs, one differing in item order and one in a single scalar; the anti-vacuity guard without which a comparator that compared nothing would pass everything else here | TestComparatorReportsADifference |
The criterion these three replace read "given a nondeterminism introduced into the assembler, the eval fails", and its Given is unestablishable by any shipped artefact, since an eval must not patch the code under test. itd-187 discloses the remainder as a recorded hand-run: the walk sort removed by a one-line local patch, the test watched red, the patch reverted before the branch is pushed, and the run recorded in the pull-request body.
Tests
Every test below is watched red before the change and green after.
TestAssembledInputIsByteIdenticalAcrossRunsis watched red against an assembler whose candidate slice is rebuilt from a map, so the order varies between runs. That is the mutation that proves the comparison can fail against real nondeterminism. Removing the walk sort does not: it yields a different order rather than an unstable one, so both assemblies still agree and the byte comparison stays green. The run is recorded in the pull request body and the patch is reverted before the branch is pushed.TestWalkOrderIsLexicographicis watched red against the removed walk sort, which is the mutation that suits it, and stays the standing guard afterwards: it fails on any consistent-but-not-lexicographic order, which the byte comparison alone would accept.TestManifestCarriesNoTimestampis watched red against a manifest with a generation time stamped into it.TestComparatorReportsADifferencefeeds the comparator two synthetic artefacts differing only in item order, and a second pair differing only in one scalar, and demands a reported difference naming the differing item. It is the anti-vacuity guard: without it, a comparator that silently compared nothing would pass every other test here.TestFixtureOrderIsAdversarialasserts theorder/fixture's creation order is not its sorted order, so the order oracle cannot pass by coincidence.
Out of scope
- The assembler, its include table, its projection, and its manifest format: spc-61.
- What the assembler included or excluded, and the sentinel corpus that tests it: spc-64.
- The reading definitions (spc-62) and the output contract (spc-63).
- Determinism of anything downstream of the assembler: a reading's own output is not required to be reproducible, and nothing here suggests it is.
- Cross-commit stability. The claim is identity at one commit; a changed repository state is expected to change the assembled input, and that is what makes the manifest's commit reference meaningful.
Frontmatter
| id | spc-65 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-65-amnesia-is-a-repository-property-proven.md
open on GitHub ↗ · commit history ↗
References1
- itd-187Amnesia is a repository property, proven by an eval — the same state assembled twice is byte-identical, so no case run is spent evidencing itimplements
mentions9
- dcn-26083000000000252026-08-30 — The two cold-reading evals get their own always-run CI job rather than riding the `smoke` job that stands down on record-only pull requests. The stand-down is anti-correlated with the risk these evals exist to catch: they read the live record tree, and `docs/`, `.abcd/development/`, `.abcd/work/` and the root prose files are both the classifier's inert allowlist and the material the assembler includes, so the diff most able to introduce warm content is exactly the diff that skips the check. A stood-down job still reports its context green, so the current shape manufactures a green for work that did not happen, which the loud-staging principle refuses. The remedy follows the reasoning `ci.yml` already documents for the ubuntu unit lane, which never stands down precisely because its tests read the live tree: a small job with no `inert` condition running these two evals alone, the rest of the smoke harness unchanged, the workflow edit landing with itd-186/187 so it is reviewed beside the evals it serves. Ruled before the build rather than after, because spc-64 and spc-65 both promised no workflow edit would be needed and that promise is what changes.
- iss-2608311331229368spc-65 and itd-187 both prescribe watching TestAssembledInputIsByteIdenticalAcrossRuns red by removing the assembler's walk sort. Run on a copy of the tree, that mutation does not make it red: removing the sort yields a different but still deterministic order, so two assemblies of one commit stay byte-identical and only the order oracle fires. The mutations that do make the identity assertion red are a genuinely nondeterministic walk (candidates rebuilt from a map) and an absolute repository path embedded in the bundle. The record's prescribed hand-run for ac-1 names a mutation that cannot falsify ac-1.
- iss-2608311331273317internal/core/reading/manifest.go documents that two assemblies of one repository state at one commit produce manifests differing in run_id and in nothing else. Nothing asserts it. spc-65 scopes the manifest to two weaker properties (no timestamp-shaped key or scalar, item paths in lexicographic order) and excludes it from the byte comparison, so a nondeterminism confined to the manifest — a changed content hash, an item recorded at a different field — is invisible to the amnesia eval. The cheap closure is a manifest comparison modulo run_id in the same eval.
- iss-2608311502474022The amnesia eval's timestamp scan rests on a premise that is already false for paths. spc-65 confines the scan to the manifest on the ground that the manifest carries paths, field names and hashes only, so a timestamp-shaped token there is unambiguously a defect. But the packed-digit rule is an unanchored run of eight or more digits and item paths are scanned as plain scalars, so a manifest naming a record path that carries a long numeric id is reported as a packed run of digits, which is how a moment travels. Fed a path holding a real nineteen-digit capture id, the scan fires. It is latent today because no admitted record family carries such a path, but the justification the scoping decision rests on is wrong now rather than at some future point, and the scan will produce a false finding the first time such a family is admitted.
- itd-183The cold reading sees exactly what the assembler passes — positive inclusion, field projection, and a per-run manifest
- spc-61The cold-reading input assembler: positive inclusion at field granularity, a pathless bundle, and a hashed per-run manifest
- spc-62Four cold-reading definitions: one verbatim blindness core, four objects, four licences no definition may borrow
- spc-63The cold-reading output contract: a strict per-position schema, a supply-regime gate, and a staged ingest that leaves evidence or nothing
- spc-64The read-block eval falsifies the firewall