Skip to content

feat(guards): enforce the apparatus, retire the baselines, triage the debt (#327) - #334

Merged
m2ux merged 6 commits into
mainfrom
fix/327-measurement-apparatus
Jul 28, 2026
Merged

feat(guards): enforce the apparatus, retire the baselines, triage the debt (#327)#334
m2ux merged 6 commits into
mainfrom
fix/327-measurement-apparatus

Conversation

@m2ux

@m2ux m2ux commented Jul 28, 2026

Copy link
Copy Markdown
Owner

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@85e89edd after #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, that check:technique-template also validates artifact bodies, is preserved on that guard's registry entry), and the workflows submodule 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.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 your change fixed.

Nothing is stored, so nothing drifts, no N fixed noise, 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.yml runs typecheck, test:ci, and check:all on every pull request.

Two constraints shaped it: "test": "vitest" is watch mode, so a test:ci script 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 .engineering nests a private repo CI cannot clone. The corpus is therefore a second actions/checkout of the pinned gitlink (git rev-parse HEAD:workflows) — no submodule machinery, no URL rewriting, no credential handling, and .engineering stays 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:

Verdict Count Guard behaviour
harmless 70 correct by design — suppressed
fix-later 125 real debt — suppressed, counted in the summary; tracked as #336
live-bug 0 reported; the guard stays red until fixed
untriaged 0 reported
stale 0 reported

Rationale 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-untriaged prints what still needs one, --emit-all prunes 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-vuln computed security_remote_configured and origin_untracked and discarded both, so a stealth run's isolation rested on a human reading git remote -v.

check:review-mode follows the same shape with three accepted instances in ACCEPTED_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 when is a bare expression over bag names (has_debt_markers == true), so an output whose only consumer was a when gate read as dead. Same shape as the validate target case 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 how concurrency: 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 validate action's target expression 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_RE matched *(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_verified and commits_signed, and substrate-node-security-audit requiring user_request without 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.sh checks out the submodules a worktree records and makes node_modules resolvable. Idempotent, and the resolvability test is require.resolve("tsx/cli") rather than the presence of a node_modules directory — vitest leaves a bare node_modules/.vite behind that would otherwise read as provisioned.

R5 — delete the baselines

All four are gone. audience and identifier-qualification are hard zero. review-mode-gating keeps three reasoned acceptances in code. binding-fidelity's 1282-line baseline is replaced by the triage file.

S1 — one registry, one sweep

scripts/guards.ts is the single enumeration of what verifies this repo, and check:all walks it: 17 guards, one table, ~1.2s. Adding an entry enforces the guard in check:all, in check:delta, and in CI.

  [PASS] binding-fidelity            0.9s  OK — no live or untriaged binding defects (210 triaged as accepted debt)
  [PASS] identifier-qualification    0.4s  OK — every technique I/O id is a qualified noun phrase
  ...
17 guard(s) in 1.2s — 17 pass, 0 fail, 0 unmeasured

The three scripts package.json never invoked are registered as 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 — it used to print total unresolved: N and exit 0.

S2 — fail loud instead of passing vacuously

One shared protocol (scripts/guard-protocol.ts) with three exit codes: 0 clean, 1 findings, 2 could not measure. check:all never folds exit 2 into a pass.

requireWorkflowsRoot rejects a missing, non-directory, or workflow-less root and names the knob that selected it; assertScanned fails a guard that inspected nothing. check-prism-lens-reachability and analyze-io-protocol-refs stopped hardcoding the path, check-stealth-isolation dropped its private copy of the precedence rules, and the 14 pinned corpus paths across 13 test files route through tests/corpus-root.ts.

check-site-links and check-svg-layout are correctly not corpus-scoped — they read site/ — 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.json records 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:stamp updates it alongside a submodule bump.

S4 — bench:token scope

--workflow (default work-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

Before Now
Full guard sweep 14 hand-chained invocations, 14 output formats npm run check:all1.2s
"Did I cause this?" stash → run → unstash → run → diff by hand npm run check:delta, base half cached
Test suite ~43s wall

Suite time is dominated by three corpus walks: workflow-e2e 40s, snapshot 37s, definition-lint 37s. 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 — clean
  • npm run check:all — 17/17, 0 unmeasured
  • npm run check:delta — no new findings against origin/main
  • Full vitest suite — 729 passed, 14 skipped. One local-only failure (session-crypto expects EACCES, gets ENOENT under 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

m2ux and others added 5 commits July 28, 2026 10:36
… 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>
…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>
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.

Measurement cost: retire the baseline strategy, and reframe the lean-mode lever (#324 D2)

1 participant