feat(guards): enforce the apparatus, retire the baselines, triage the debt (#327) - #334
Merged
Conversation
… debt (#327) The repo grew 17 guard scripts in five weeks. CI ran four checks, 15 guards ran only when someone remembered, and four guards wrote what they found into stored baselines that nothing ever revisited — so the apparatus found bugs and then lost them. Two of #324's most expensive defects had been sitting in a baseline file while they shipped. R1 — merge-base deltas instead of stored baselines. scripts/check-delta.ts resolves the merge-base with the integration branch, materialises it in a throwaway worktree with the workflows submodule pinned to the commit THAT tree recorded, runs the guard registry against both trees, and reports only the difference (including what the change fixed). Nothing is stored, so nothing drifts, and the verdict is always scoped to the change. Base results cache under .guard-cache/ keyed by (base commit, base corpus commit), so the doubled runtime is paid once per rebase. It also replaces the stash / re-run / unstash / re-run dance that happened three times during the #324 session. R2 — .github/workflows/verify.yml runs typecheck, the vitest suite (test:ci, because "test" is watch mode), and check:all on every pull request. Both submodules are branches of this repo behind an SSH URL the default token cannot use, and .engineering nests a private repo CI cannot clone, so the corpus is a second checkout of the pinned gitlink rather than a submodule init. R5 — the audience, identifier-qualification, and review-mode-gating baselines are deleted. audience and identifier-qualification are hard zero. review-mode-gating keeps three accepted instances in ACCEPTED_HEADLESS_AUTO_ADVANCE, each with the reason it is safe under the workflow's declared headless-review design; the list is hand-written, has no regenerate flag, and a stale entry fails the guard. binding-fidelity's 1282-line baseline is deleted too. R3 — every one of the 210 remaining binding-fidelity findings now carries a verdict and a named rationale in scripts/binding-fidelity-triage.json: 70 harmless, 140 fix-later, 0 live, 0 untriaged. harmless and fix-later suppress; live-bug REPORTS, so a known live defect keeps the guard red. An untriaged finding is reported; a stale entry is reported. Getting to an honest 210 from the reported 229 meant fixing the guard's reach, not the count. binding-fidelity now walks activities/ recursively (meta/activities/patterns/ — five borrowable pattern activities — had never been measured at all), skips fenced code blocks when scanning reads (an artifact template's {impact} is a field of the rendered report, not a bag read), and counts a validate action's target expression as consumption (an output whose only consumer is a validate gate was reported as dead — the opposite of dead). The shared OPTIONAL_INPUT_RE also missed "(optional, default X)", so inputs that declared both read as required-and-unsupplied on the guard AND on the server's provenance annotation. R4 — scripts/provision-worktree.sh checks out the submodules a worktree records and makes node_modules resolvable, so guards and tests measure the worktree being edited instead of the main copy. Idempotent; npm run worktree:provision. S1 — scripts/guards.ts is the one enumeration of what verifies this repo, and check:all walks it: 17 guards, one table, ~1.3s. Adding an entry enforces the guard in check:all, check:delta, and CI. The three scripts package.json never invoked are registered (check:refs, check:activities, check:workflow-yaml); validate-workflow-yaml gained a corpus-wide default instead of demanding a positional path, and check-all-refs now exits 1 on an unresolved reference rather than printing the count and passing. S2 — corpus resolution is mandatory and loud. requireWorkflowsRoot rejects a missing, non-directory, or workflow-less root and names the knob that selected it; assertScanned fails a guard that inspected nothing; guard-protocol exits 2 for "could not measure", which check:all never folds into a pass. check-prism-lens-reachability and analyze-io-protocol-refs no longer hardcode the path, check-stealth-isolation drops its private copy of the precedence rules, and the 14 pinned corpus paths across 13 test files route through tests/corpus-root.ts. S3 — tests/e2e/__snapshots__/corpus-sha.json records the corpus commit the walk snapshots were generated against, and a mismatch fails with both SHAs named. Six e2e tests were red for exactly this reason during #324 and diagnosing it produced nothing. npm run baseline:stamp. S4 — bench:token takes --workflow (default work-package) and records it, and a comparison across two different workflows is reported but can never pass --gate. It previously walked one workflow structurally and reported a confident delta for a change it could not see. S5 — measured against a working check:all: the full guard sweep is 1.3s wall (17 guards, concurrent) where it used to be 14 hand-chained invocations with 14 output formats. The vitest suite is ~43s wall; workflow-e2e (40s), snapshot (37s) and definition-lint (37s) dominate, all three corpus walks. Refs #327 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… not the corpus (#327) Running the delta on the change that introduces it exposed three ways the comparison could mislead, all from comparing across two guard versions rather than two corpora: - A guard that speaks the finding protocol in this tree but not in the base fell through to the coarse path, where every line of the head's JSON read as a new finding. It now compares verdicts only and reports [PROTOCOL]. - A base tree that cannot run a guard as invoked (this change taught two validators to accept --root, so the base treats it as a positional path) declared the whole delta incomplete. Only the HEAD failing to measure is a hole; a base that cannot measure means there is no before-state to subtract, reported as [NO-BASE]. - On the coarse path, new output lines were counted as findings. Without a finding protocol an output-format tweak is indistinguishable from a finding, so changed lines are now shown (capped) but only a clean-to-red verdict change is counted. Verified end to end: a deliberately bogus step-binding input key in the corpus is reported as exactly one introduced finding, and reverting it returns the delta to clean. Refs #327 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lean-looking table (#327) A filter combination can select nothing (--corpus-only --only site-links). The sweep printed an empty table and exited 0, which reads exactly like a clean run — the failure mode the whole change is about. It now exits 2 and says nothing was measured. Also hoist the head finding-key set out of the delta's filter callback. Refs #327 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… unconsumed-verdict class (#327) Burning down the `verification-result-unconsumed` triage class found one more place the guard could not see. A step's `when` is a bare expression over bag names (`has_debt_markers == true`), not a `{token}` and not a structured `variable:` key, so an output whose only consumer was a `when` gate read as dead — the same blind spot as a `validate` action's `target`, which this branch already fixed. Both now route through one expression scan. That cleared two findings with no corpus change: ponytail's `has_debt_markers` and meta's `resume_intent_requested`. The remaining eight are fixed in the corpus (see the companion corpus commit): three verdicts gained a validate action, two gates now read the declared verdict instead of a parallel probe, and three outputs the checkpoint actually owns were removed from their technique. `--emit-all` joins `--emit-untriaged` so the triage file can be pruned when a finding stops occurring, rather than accumulating stale entries. The stale-entry check earned its keep here: it named all nine dropped entries the moment their findings went away. Triage after the burn-down: 201 findings — 70 harmless, 131 fix-later, 0 live, 0 untriaged, 0 stale. The `verification-result-unconsumed` class is empty. `run_status` moved to `terminal-product-unconsumed` (a final-activity manifest status, not a verification) and `source_coverage_complete` to a new `verdict-component-of-composite` rationale (the op folds it into `validation_passed`, which the transitions consume). One walk snapshot re-baselined: the review-mode walk no longer presents `ticket-completeness`. Refs #327 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rebasing onto main picked up the merged artifact-declaration corpus change (#330/#331), which independently closed six binding-fidelity findings: prism's `subsystem_outputs`, work-packages' `key_findings` and `planning_recommendation`, and substrate's `crate_map` (×2) and `reconnaissance_data`. The stale-entry check named all six on the first run after the rebase. That is the property the retired baseline did not have: a baselined entry whose finding had been fixed sat there accumulating — binding-fidelity's carried 27 of them, overstating the recorded debt by ~12%. 195 findings now: 70 harmless, 125 fix-later, 0 live, 0 untriaged, 0 stale. Refs #327 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
m2ux
force-pushed
the
fix/327-measurement-apparatus
branch
from
July 28, 2026 09:40
4fc9777 to
d99f8e7
Compare
…er (#327) The first CI run of the new Verify workflow failed — not on an assertion, but on `tests/e2e/definition-lint.test.ts`'s `beforeAll` timing out at 120s. 736 tests passed and nothing was wrong with the corpus or the code; a GitHub runner is roughly 4x slower than a local machine, and a hook that measures ~30s locally does not fit in 120s there. That budget was already a moving literal: 60s -> 120s as the corpus grew, with a comment saying so. Raising it to a third number would have been the same fix a third time, so the per-walk allowance now lives in tests/e2e/budgets.ts and the hooks derive from it. The next corpus growth moves one constant instead of a hunt for which literal to raise. - definition-lint's six-walk hook: walkHookTimeout(6) - robot-execution's one-walk hook: walkHookTimeout(1) (was a flat 60s) - vitest testTimeout 30s -> 60s: a single walk sits near 30s on a runner, so the per-test budget had no headroom either The walks stay sequential. They are independent, but two concurrent walks of one workflow could land on the same planning slug, and that is not an isolation property this change can prove. No assertion, policy, or baseline is touched. Refs #327 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Jul 28, 2026
This was referenced Jul 28, 2026
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.
Closes #327.
Corpus halves: #333 (merged) and #335 (open). This PR's submodule pointer pins #335's tip, so #335 merges first.
Rebased onto
main@85e89eddafter #332 landed. Two conflicts, both resolved:docs/development.md(my rewrite of the Validating Workflows section replaced the per-guard command list #332 had just amended — the fact it added, thatcheck:technique-templatealso validates artifact bodies, is preserved on that guard's registry entry), and theworkflowssubmodule pointer. Rebasing also surfaced six findings that #331's corpus work had independently fixed; the stale-entry check named all six on the first run, and they are pruned in a dedicated commit.The repo grew 17 guard scripts in five weeks. CI ran four checks, 15 guards ran only when someone remembered, and four guards wrote what they found into stored baselines that nothing revisited — so the apparatus found bugs and then lost them. Both of #324's headline defects had been sitting in
binding-fidelity-baseline.json, detected and silenced, while they shipped and burned a three-hour live run.R1 — merge-base deltas instead of stored baselines
scripts/check-delta.tsresolves the merge-base with the integration branch, materialises it in a throwaway worktree with theworkflowssubmodule pinned to the commit that tree recorded, runs the guard registry against both trees, and reports only the difference — including what your change fixed.Nothing is stored, so nothing drifts, no
N fixednoise, no baseline-update PRs, and the verdict is always exact and scoped to the change. Base results cache under.guard-cache/keyed by (base commit, base corpus commit), so the doubled runtime is paid once per rebase rather than once per run. It also removes the stash / re-run / unstash / re-run dance that happened three separate times during the #324 session.Running it on the change that introduces it surfaced three ways a cross-guard-version comparison can mislead, all now handled: a guard on the finding protocol in HEAD but not in base compares verdicts rather than reading every JSON line as a finding (
[PROTOCOL]); a base that cannot run a guard as invoked means there is no before-state, not a hole in the delta ([NO-BASE]); and on the coarse path new output lines are shown but not counted, because without a finding protocol a format tweak is indistinguishable from a finding.Proven end to end: a deliberately bogus step-binding input key in the corpus reports as exactly one introduced finding; reverting it returns the delta to clean.
R2 — CI enforcement
.github/workflows/verify.ymlrunstypecheck,test:ci, andcheck:allon every pull request.Two constraints shaped it:
"test": "vitest"is watch mode, so atest:ciscript was added rather than relying on vitest's CI detection; and both submodules are branches of this repo behind an SSH URL the default token cannot use, while.engineeringnests a private repo CI cannot clone. The corpus is therefore a secondactions/checkoutof the pinned gitlink (git rev-parse HEAD:workflows) — no submodule machinery, no URL rewriting, no credential handling, and.engineeringstays out. It verifies the corpus commit the tree under review actually adopts, not the branch tip.R3 — triage the suppressed findings
Every remaining finding carries a verdict and a named rationale in
scripts/binding-fidelity-triage.json:harmlessfix-laterlive-bugRationale classes:
shared-op-return-contract,shared-op-caller-argument,pattern-library-seed,artifact-template-placeholder,documentation-notation,external-tool-syntax,verdict-component-of-composite(harmless);undeclared-seed,terminal-product-unconsumed(fix-later). A finding absent from the file is untriaged and reported; an entry matching nothing is stale and reported. There is no--update-baseline— a verdict is a human judgement, which is exactly what the retired baselines let a regenerate flag skip.--emit-untriagedprints what still needs one,--emit-allprunes what no longer occurs.One class burned down
verification-result-unconsumed— an operation returns a verdict and no gate, condition, or validate target reads it — held 10 findings and is now empty. Eight are fixed in the corpus (#335): three verdicts gained a validate action, two gates now read the declared verdict instead of a parallel probe, and three outputs the checkpoint actually owns were removed from their technique. One was a guard blind spot (below) and one was misclassified by the predicate-name heuristic.Two of the three enforced verdicts were security seams:
remediate-vulncomputedsecurity_remote_configuredandorigin_untrackedand discarded both, so a stealth run's isolation rested on a human readinggit remote -v.check:review-modefollows the same shape with three accepted instances inACCEPTED_HEADLESS_AUTO_ADVANCE, each with the reason it is safe under the workflow's declared headless-review design.229 → 195 was mostly guard reach, not paydown
Five blind spots were making the count dishonest:
A step's
whenis a bare expression over bag names (has_debt_markers == true), so an output whose only consumer was awhengate read as dead. Same shape as thevalidatetargetcase below; both now route through one expression scan. The reverse direction — resolving those names, so a gate naming an unproducible value is a finding — is Guard reach: a gate expression is counted as consumption but never resolved #341.The activity walk was non-recursive, so
meta/activities/patterns/had never been measured at all — five borrowable pattern activities, invisible in both directions. That is howconcurrency: concurrency(a binding to an unproducible bag name) survived.Reads were scanned inside fenced code blocks, so an artifact template's
{impact}— a field of the rendered report — read as an unresolved bag read. Inline code spans are deliberately still scanned: house style backticks real designators.A
validateaction'stargetexpression didn't count as consumption, so an output whose only consumer is a validate gate read as dead. The opposite of dead.The shared
OPTIONAL_INPUT_REmatched*(optional)*but not*(optional, default X)*, so inputs declaring both read as required-and-unsupplied — on the guard and on the server's provenance annotation, which shares the constant.Three live defects found and fixed in #333: headless review runs auto-creating GitHub/Jira issues, the stealth push path discarding
push_remote_verifiedandcommits_signed, andsubstrate-node-security-auditrequiringuser_requestwithout declaring it.The stale-entry check has now earned its keep twice: it named the nine entries the verdict burn-down closed, and — after rebasing onto
main— the six that #331's corpus work had independently fixed. A baseline would have kept all fifteen; binding-fidelity's carried 27 such entries, overstating the recorded debt by ~12%.R4 — provision worktrees so they can measure
scripts/provision-worktree.shchecks out the submodules a worktree records and makesnode_modulesresolvable. Idempotent, and the resolvability test isrequire.resolve("tsx/cli")rather than the presence of anode_modulesdirectory — vitest leaves a barenode_modules/.vitebehind that would otherwise read as provisioned.R5 — delete the baselines
All four are gone.
audienceandidentifier-qualificationare hard zero.review-mode-gatingkeeps three reasoned acceptances in code.binding-fidelity's 1282-line baseline is replaced by the triage file.S1 — one registry, one sweep
scripts/guards.tsis the single enumeration of what verifies this repo, andcheck:allwalks it: 17 guards, one table, ~1.2s. Adding an entry enforces the guard incheck:all, incheck:delta, and in CI.The three scripts
package.jsonnever invoked are registered ascheck:refs,check:activities,check:workflow-yaml.validate-workflow-yamlgained a corpus-wide default instead of demanding a positional path, andcheck-all-refsnow exits 1 on an unresolved reference — it used to printtotal unresolved: Nand exit 0.S2 — fail loud instead of passing vacuously
One shared protocol (
scripts/guard-protocol.ts) with three exit codes:0clean,1findings,2could not measure.check:allnever folds exit 2 into a pass.requireWorkflowsRootrejects a missing, non-directory, or workflow-less root and names the knob that selected it;assertScannedfails a guard that inspected nothing.check-prism-lens-reachabilityandanalyze-io-protocol-refsstopped hardcoding the path,check-stealth-isolationdropped its private copy of the precedence rules, and the 14 pinned corpus paths across 13 test files route throughtests/corpus-root.ts.check-site-linksandcheck-svg-layoutare correctly not corpus-scoped — they readsite/— so the issue's S2 surface count of 5 is really 3 plus those two.S3 — corpus-SHA-stamped baselines
tests/e2e/__snapshots__/corpus-sha.jsonrecords the corpus commit the walk snapshots were generated against, and a mismatch fails naming both SHAs with the remedy. Six e2e tests were red for exactly this reason during #324, and diagnosing it produced nothing.npm run baseline:stampupdates it alongside a submodule bump.S4 — bench:token scope
--workflow(defaultwork-package), recorded in the output, and a comparison across two different workflows is reported but can never pass--gate— the same discipline already applied to context mode. It previously walked one workflow structurally and reported a confident delta for a change it could not see.S5 — cost, re-measured
npm run check:all— 1.2snpm run check:delta, base half cachedSuite time is dominated by three corpus walks:
workflow-e2e40s,snapshot37s,definition-lint37s. Untouched by this change; it is genuine measurement, not overhead.Lean mode — the #324 D2 decision
The issue's hypothesis holds: most of the demand for a cheaper run was accidental cost, and it is gone. A full sweep is one 1.2s command; measurement no longer needs two manual runs.
Recommendation: build the challenge-pass cap if it is still wanted — it is cheap and low-risk. Keep the measurement cap deferred. Making thorough runs cheap beat making cheap runs less trustworthy, and a mode that reduces measurement is the wrong lever on a repo that just spent an issue proving its measurements were being discarded. The declared-mode shape itself is sound and would follow review mode's precedent.
Verification
npm run typecheck— cleannpm run check:all— 17/17, 0 unmeasurednpm run check:delta— no new findings againstorigin/mainsession-cryptoexpectsEACCES, getsENOENTunder bubblewrap); passes outside the sandbox and is unrelated to this change.Not in scope, per the issue: reverting or reducing the guards. They are the only reason any of this was findable.
🤖 Generated with Claude Code