Repository navigation
test(formal): give feature witness histories their raw ITF states and gate the corpus at load - #177
Merged
lan17 merged 1 commit intoSep 15, 2026
Conversation
… 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
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #176, which removed the triple parse from the effects witness gate. The feature profiles had the same shape:
test/formal-features.test.tsparsed every history at import for its replay,checkWitnesses(name, paths)re-read and re-parsed each file inloadCorpus, and the ten classifiers keyed on explicit inputs (runtimeWitnesses,recoveryShadowWitnesses, the five public-prefix families,recoveryReadWitnesses,runtimeBoundaryWitnessesandpublicPrefixWitnessesitself) read every file a third time throughreadTraceand re-decoded private state throughprivateStates. 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.mjsreplacesprivateStateswithwitnessStates(raw, context), which returns the raw ITF states of a history together with the private predictions decoded from them once.loadCorpusspreads that beside the parsed steps, so a feature history is{ path, steps, states, predictions }:statesare the raw ITF states for every profile that has classifiers keyed on explicit inputs, as #176 already made them for effects, andpredictionsare what the five corpus classifiers that read decoded private state (policy, recovery, independent, layers, shadow) now destructure;layers.mjsalready called them predictions. Every path-taking classifier takes histories instead of paths andevaluateCorpushands the corpus to all of them;runtimeBoundaryTraceWitnessestakes 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 forformal/witnesses.mjsand 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
RawHistoryandPrivateHistoryintrace.d.mts, andloadCorpusis typed per profile ("effects","local-clock", any other name a feature profile).The gate runs at load
test/formal-features.test.tsloads each profile's histories once withloadCorpus(name, paths), runscheckCorpusover that corpus at load, and keeps only{ path, steps }for the replays plus the gate'smissinglist. 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 everyitclosure 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 underNODE_OPTIONS=--max-old-space-size=2048(peak RSS 1.64 GB, 4837 tests).test/formal-effects.test.tsgets the same shape. Since the gateitbodies 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 againstcheckCorpus(loadCorpus(paths))now, where the second column excludes the load that the test module shares with its replays.checkWitnesses(paths)beforecheckCorpus(corpus)afterLabels, 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.tsagainst 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 auditpasses; noformal/*.mdprose 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 theinputs[].sha256entries for the edited files underformal/replay/witnesses/, which change by design whenever a replay source changes.witness-report.jsonis byte-identical. Go'sTestGeneratedWitnessEvidenceandTestWitnessEvidenceBindsSharedReplaySourcespass 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
historyParsercomment that claimed every history keeps raw states when local-clock histories do not. Two style claims (thepredictions: statesdestructuring 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