Skip to content

test(formal): give feature witness histories their raw ITF states and gate the corpus at load - #177

Merged
lan17 merged 1 commit into
claude/nifty-jones-4ad6acfrom
claude/feature-witness-histories
Sep 15, 2026
Merged

lan17 merged 1 commit into
claude/nifty-jones-4ad6acfrom
claude/feature-witness-histories

Conversation

@lan17

@lan17 lan17 commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Stacked on #176, which removed the triple parse from the effects witness gate. The feature profiles had the same shape: test/formal-features.test.ts parsed every history at import for its replay, checkWitnesses(name, paths) re-read and re-parsed each file in loadCorpus, and the ten classifiers keyed on explicit inputs (runtimeWitnesses, recoveryShadowWitnesses, the five public-prefix families, recoveryReadWitnesses, runtimeBoundaryWitnesses and publicPrefixWitnesses itself) read every file a third time through readTrace and re-decoded private state through privateStates. On the generated corpus the policy, layers and shadow gates each took 2.5 to 3.5 s inside vitest locally, all of it re-reading.

Histories

formal/replay/witnesses/trace.mjs replaces privateStates with witnessStates(raw, context), which returns the raw ITF states of a history together with the private predictions decoded from them once. loadCorpus spreads that beside the parsed steps, so a feature history is { path, steps, states, predictions }: states are the raw ITF states for every profile that has classifiers keyed on explicit inputs, as #176 already made them for effects, and predictions are what the five corpus classifiers that read decoded private state (policy, recovery, independent, layers, shadow) now destructure; layers.mjs already called them predictions. Every path-taking classifier takes histories instead of paths and evaluateCorpus hands the corpus to all of them; runtimeBoundaryTraceWitnesses takes a { states } history rather than a raw ITF document. The smoke-fixture skips (states[0]?.s?.sources / phase === undefined) and every credit, checkpoint and recorder call are unchanged. checkWitnesses(profile, paths, registry) is unchanged for formal/witnesses.mjs and every port that runs the CLI.

The eight classifier unit tests and the provenance test hand histories to the classifiers directly and no longer write temp files. Type declarations gained RawHistory and PrivateHistory in trace.d.mts, and loadCorpus is typed per profile ("effects", "local-clock", any other name a feature profile).

The gate runs at load

test/formal-features.test.ts loads each profile's histories once with loadCorpus(name, paths), runs checkCorpus over that corpus at load, and keeps only { path, steps } for the replays plus the gate's missing list. The first version of this change kept the full histories at module level for the whole file; the review pass measured that at about 2.16 GB of live heap for the twelve profiles against about 216 MB on main, because every it closure pinned the raw ITF states, and showed the file dying at import under a 2 GB V8 heap where main survives. With the projection the file passes under NODE_OPTIONS=--max-old-space-size=2048 (peak RSS 1.64 GB, 4837 tests). test/formal-effects.test.ts gets the same shape. Since the gate it bodies now assert a precomputed list, the 30 s timeouts #172 added to them are gone; the evaluator's cost sits in module import, which has no per-test timeout.

Measurements

Evaluator cost in isolation on the generated corpus, locally: checkWitnesses(paths) on the #176 tree against checkCorpus(loadCorpus(paths)) now, where the second column excludes the load that the test module shares with its replays.

profile histories checkWitnesses(paths) before checkCorpus(corpus) after
policy 541 2269 ms 41 ms
layers 527 3080 ms 70 ms
shadow 1031 2618 ms 204 ms
independent 519 1423 ms 40 ms
recovery 542 1039 ms 48 ms
shadow-layers 278 994 ms 36 ms
recovery-read 282 734 ms 75 ms
source-budgets 263 661 ms 27 ms
scope 266 615 ms 24 ms
runtime-boundaries 281 537 ms 22 ms
admission 134 295 ms 11 ms
local-failure 132 123 ms 9 ms

Labels, per-label provenance and history sequences from the two paths are identical for every profile. The three-file gate run (test/formal-effects.test.ts test/formal-local-clock.test.ts test/formal-features.test.ts against the full corpus) went from 32.6 s to 24.1 s, with peak RSS 2.29 GB.

Verification

The three-file gate run against the full generated corpus: 5534 tests passed, also under the 2 GB heap cap for the features file. The witness evaluator unit tests and the whole unit suite pass (3099 tests). make audit passes; no formal/*.md prose changed, so the ledgers are untouched.

node formal/witnesses.mjs evaluate --profile all: all 14 evidence files are identical to the pre-#176 snapshot except the inputs[].sha256 entries for the edited files under formal/replay/witnesses/, which change by design whenever a replay source changes. witness-report.json is byte-identical. Go's TestGeneratedWitnessEvidence and TestWitnessEvidenceBindsSharedReplaySources pass against the regenerated evidence.

Review

An adversarial review pass (three lenses, two skeptics per finding) confirmed two items, both applied: the module-level retention described above, and a historyParser comment that claimed every history keeps raw states when local-clock histories do not. Two style claims (the predictions: states destructuring alias in three classifiers, and the repeated one-line history literal in the unit tests) were refuted as pre-existing or not defects.

🤖 Generated with Claude Code

… gate the corpus at load

The feature-profile gates in test/formal-features.test.ts had the same
redundancy #176 removed for effects: the test parsed every history at
import, checkWitnesses re-read and re-parsed each file in loadCorpus, and
ten classifiers keyed on explicit inputs (runtimeWitnesses,
recoveryShadowWitnesses, the public-prefix families, recoveryReadWitnesses,
runtimeBoundaryWitnesses) read every file again through readTrace and
re-decoded private state through privateStates.

trace.mjs replaces privateStates with witnessStates(raw, context), which
returns the raw ITF states and the private predictions decoded from them
once. A feature history is now { path, steps, states, predictions }: states
are the raw ITF states, as they already were for effects, and the five
corpus classifiers that read decoded private state destructure predictions.
Every path-taking classifier takes histories instead of paths,
evaluateCorpus hands the corpus to all of them, and
runtimeBoundaryTraceWitnesses takes a { states } history. checkWitnesses is
unchanged for formal/witnesses.mjs. The classifier unit tests hand
histories directly instead of writing temp files.

The effects and features test modules load each history once, run
checkCorpus over the corpus at load and keep only { path, steps } for the
replays plus the gate's missing list. Keeping the full histories at module
level pinned about 2.16 GB of raw states for the twelve feature profiles
(about 216 MB on main) and died at import under a 2 GB V8 heap; with the
projection the features file passes under --max-old-space-size=2048. The
gate `it` bodies now assert a precomputed list, so the 30 s timeouts from
#172 are gone.

Evaluator cost in isolation, locally: policy 2269 ms -> 41 ms, layers 3080
-> 70, shadow 2618 -> 204, independent 1423 -> 40, recovery 1039 -> 48; the
three-file gate run 32.6 s -> 24.1 s. Labels, provenance and history
sequences are identical, and `node formal/witnesses.mjs evaluate` evidence
differs from the pre-#176 snapshot only in the inputs[].sha256 of the edited
replay files.
@lan17
lan17 merged commit bb66071 into claude/nifty-jones-4ad6ac Sep 15, 2026
7 checks passed
@lan17
lan17 deleted the claude/feature-witness-histories branch September 15, 2026 22:05
lan17 added a commit that referenced this pull request Sep 16, 2026
…176)

The witness completion gates parsed their corpus three times. Each gate
test module parses every history at import for its replay;
`checkWitnesses(profile, paths)` re-read and re-parsed every file in
`loadCorpus`; and the classifiers keyed on explicit inputs
(`effectsAuthorityWitnesses`, `runtimeWitnesses`,
`recoveryShadowWitnesses`, the five public-prefix families,
`recoveryReadWitnesses`, `runtimeBoundaryWitnesses`) read every file a
third time through `readTrace` and re-decoded private state through
`privateStates`. On the 554-history effects corpus the redundant passes
pushed the gate past vitest's default 5 s timeout on hosted runners
(main run 34833641404, then #172's run); #172 gave that gate 30 s of
headroom. On the generated corpus the policy, layers and shadow feature
gates each took 2.5 to 3.5 s inside vitest locally, all of it
re-reading. This PR removes the cost for every profile. It is two
commits: the effects and local-clock gates first, then the feature
profiles (#177, merged into this branch).

### Evaluator

`formal/replay/witnesses/index.mjs` gains `checkCorpus(profile, corpus,
registry)`, the completion gate over histories the caller has already
loaded. `checkWitnesses(profile, paths, registry)` is now
`checkCorpus(profile, loadCorpus(profile, paths), registry)`, so
`formal/witnesses.mjs` and every port that runs the CLI are unchanged.
`loadCorpus` resolves a profile's strict parser once and applies it per
file. `evaluateCorpus` lost its `paths` parameter, which could only
disagree with the corpus, and hands the corpus to every classifier;
`evaluateWitnesses`, which had no caller since #164, is deleted.

`formal/replay/witnesses/trace.mjs` replaces `privateStates` with
`witnessStates(raw, context)`, which returns the raw ITF states of a
history together with the private predictions decoded from them once.
Effects histories are `{ path, steps, states }` and feature histories `{
path, steps, states, predictions }`: `states` are the raw ITF states
that the input-keyed classifiers read and validate themselves, and
`predictions` are what the five corpus classifiers that read decoded
private state (policy, recovery, independent, layers, shadow) now
destructure; `layers.mjs` already called them predictions. Every
path-taking classifier takes histories instead of paths, and
`runtimeBoundaryTraceWitnesses` takes a `{ states }` history rather than
a raw ITF document. The smoke-fixture skips (`states[0]?.s?.sources` /
`phase === undefined`) and every credit, checkpoint and recorder call
are unchanged.

The classifier unit tests hand histories to the classifiers directly and
no longer write temp files. Type declarations gained `RawHistory` and
`PrivateHistory` in `trace.d.mts`, and `loadCorpus` is typed per profile
(`"effects"`, `"local-clock"`, any other name a feature profile).

### The gate runs at load

`test/formal-effects.test.ts` and `test/formal-features.test.ts` load
each profile's histories once with `loadCorpus`, run `checkCorpus` over
that corpus at load, and keep only `{ path, steps }` for the replays
plus the gate's `missing` list; `test/formal-local-clock.test.ts` passes
the traces it already parsed. An earlier version kept the full histories
at module level for the whole file; the review pass measured that at
about 2.16 GB of live heap for the twelve feature profiles against about
216 MB on main, because every `it` closure pinned the raw ITF states,
and showed the file dying at import under a 2 GB V8 heap where main
survives. With the projection the features file passes under
`NODE_OPTIONS=--max-old-space-size=2048` (peak RSS 1.64 GB). Since the
gate `it` bodies now assert a precomputed list, the 30 s timeouts #172
added to them are gone; the evaluator's cost sits in module import,
which has no per-test timeout.

### Measurements

Evaluator cost in isolation on the generated corpus, locally:
`checkWitnesses(paths)` on main against `checkCorpus(loadCorpus(paths))`
now, where the second column excludes the load the test module shares
with its replays.

| profile | histories | `checkWitnesses(paths)` before |
`checkCorpus(corpus)` after |
| --- | --- | --- | --- |
| effects | 554 | 1052 ms | 264 ms |
| policy | 541 | 2269 ms | 41 ms |
| layers | 527 | 3080 ms | 70 ms |
| shadow | 1031 | 2618 ms | 204 ms |
| independent | 519 | 1423 ms | 40 ms |
| recovery | 542 | 1039 ms | 48 ms |
| shadow-layers | 278 | 994 ms | 36 ms |
| recovery-read | 282 | 734 ms | 75 ms |
| source-budgets | 263 | 661 ms | 27 ms |
| scope | 266 | 615 ms | 24 ms |
| runtime-boundaries | 281 | 537 ms | 22 ms |
| admission | 134 | 295 ms | 11 ms |
| local-failure | 132 | 123 ms | 9 ms |

Labels, per-label provenance and history sequences from the two paths
are identical for every profile. The three-file gate run
(`test/formal-effects.test.ts test/formal-local-clock.test.ts
test/formal-features.test.ts` against the full corpus) went from 38.7 s
on main to 32.6 s after the first commit and 24.1 s after the second,
with peak RSS 2.29 GB.

### Verification

The three-file gate run against the full generated corpus: 5534 tests
passed, also under the 2 GB heap cap for the features file. The witness
evaluator unit tests and the whole unit suite pass (3099 tests). `make
audit` passes after refreshing the ledger for the one-sentence
`PORTING.md` update (its hash in `formal/source-audit.json`, then that
file's hash at both pins in `formal/go-parity.json`; the paragraph keeps
its line count because the ledger also pins heading line numbers).

`node formal/witnesses.mjs evaluate --profile all` before and after: all
14 evidence files are identical except the `inputs[].sha256` entries for
the edited files under `formal/replay/witnesses/`, which change by
design whenever a replay source changes. `witness-report.json` is
byte-identical. Go's `TestGeneratedWitnessEvidence` and
`TestWitnessEvidenceBindsSharedReplaySources` pass against the
regenerated evidence.

### Review

Each commit had an adversarial review pass (three lenses, two skeptics
per finding). The first confirmed three low-severity items, all applied:
`evaluateWitnesses` was dead since #164 and is deleted rather than
re-plumbed; the effects test used an inline copy of `readTrace` where
`loadCorpus` already did the same map, so the single-history builder
became module-private, which also keeps the eager unknown-profile error;
and that builder's comment was trimmed. The second confirmed two items,
both applied: the module-level retention described above, and a
`historyParser` comment that claimed every history keeps raw states when
local-clock histories do not. Six other claims (error ordering in
`loadCorpus`, the dropped array guard in the authority classifier, two
readings of the `PORTING.md` sentence, the `predictions: states`
destructuring alias, the repeated one-line history literal in the unit
tests) were refuted as unreachable, pre-existing or not behavior
changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant