Repository navigation
fix(pm): governed-merges self-test refuses as NOT MEASURED when its generator toolchain is absent - #15283
Merged
os-steve merged 3 commits intoSep 4, 2026
Conversation
…s generator toolchain is absent
In a freshly created worktree, before `pnpm install`, `pnpm
check:pm-governed-merges` exited 1 — a finding's code — and reported
self-test battery "#11705 end to end, against the REAL generator"
registered 5 case(s), below its pinned floor of 7 — cases that used to
run no longer do.
Nothing had stopped running. The battery's live case execs the real
generator (`pnpm --filter @objectstack/spec exec tsx …`), which in an
uninstalled checkout exits 254 with `Command "tsx" not found`; that arrives
at `sinkGeneratorVerdict` wearing the same `outputs: null` shape as every
other generator failure, the battery takes its one-assertion branch, and the
floor renders a NOT-MEASURED condition as a measured red — prescribing a
hunt through the generator for a deleted block that was never deleted.
Three sibling gates met the same worktree in the same minute and answered
correctly with exit 3 PREREQUISITE NOT MET. This gate now does too:
- `generatorToolchainPrerequisite()` asks whether the row's recompute can
run at all, by looking for the same binary `pnpm exec` would. It is
consulted at the top of `selfTest()`, before a single case runs, so the
shared frame's load-bearing clause is literally true when it prints. It is
strictly a narrowing of one exit code into another — no branch in it can
turn a red into a green, and the audit paths never consult it.
- The refusal reuses `scripts/import-prerequisite.mjs` — the exit code, the
frame and the pipe advisory are the repo's, not a sixth spelling.
- The reason string is deliberately NOT classified: an uninstalled checkout
and a generator reporting real drift both arrive as a non-zero `--check`
exit, so reading that text would fold a finding into a refusal.
- `batteryFloorFailures()` is lifted to module scope and takes its ledger as
an argument, so the direction that matters most — a genuinely deleted case
still reds, and still names WHICH battery — is asserted for the first
time. Its behaviour, order and every message it prints are unchanged.
Both directions are pinned (15 new cases, 245 -> 260 assertions): an
uninstalled checkout refuses with exit 3 end to end, on a copy of this
file's own module graph, naming the prerequisite and the remedy and no
longer printing the shrunken-battery words; a ledger one case short of any
floor still reds and still names that battery.
No floor is lowered, no case is deleted or skipped, and the post-merge
audit's verdicts are untouched.
Fixes #15272
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
The shared advisory names the importer by PATH, which is right for the gates it was written for but incomplete here: this file has modes, and `node scripts/pm/check-governed-merges.mjs` without `--self-test` runs the network SWEEP rather than the cases that just refused. The helper has no seam for an invocation, so the correct entry point is stated in this gate's own detail lines, where the claim about what went unmeasured already lives. Pinned end to end: 245 -> 261 assertions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
… of itself Found by ablating the probe: the fixture child reaches the prerequisite battery only when the probe did NOT refuse, and there its own copied tree is a perfect seed for the next copy. Measured, an ablated probe produced one self-test per second, each waiting on the next — 67 processes before it was cut off by hand. A hang is the worst reading a gate can return: neither green nor red, and nothing in CI distinguishes it from slow. The child is now marked in its argv, a marked run refuses to spawn a grandchild, and the cases it would have measured FAIL with that reason instead — the same number of cases, in both worlds, and a loud red where there was an unbounded wait. 261 -> 262 assertions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
This was referenced Sep 4, 2026
os-steve
marked this pull request as ready for review
September 4, 2026 08:58
os-steve
enabled auto-merge
September 4, 2026 08:58
os-steve
deleted the
claude/issue-15272-governed-merges-prerequisite-exit
branch
September 4, 2026 09:28
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.
Fixes #15272
In a freshly created worktree, before
pnpm install,pnpm check:pm-governed-mergesexited 1 — a finding's code — and reported:Nothing had stopped running. Three sibling gates met the same worktree in the same minute and answered correctly with exit 3
PREREQUISITE NOT MET. This one reported a NOT MEASURED condition as a measured red, on a governance gate, and prescribed a hunt for code nobody deleted.The mechanism, measured
The card deliberately did not measure which dependency that battery loads. It is the generator toolchain, and the chain is:
runSinkGenerator, which spawnspnpm --filter @objectstack/spec exec tsx scripts/build-skill-references.ts --check.ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "tsx" not found(measured directly, both through the function and through pnpm).runSinkGeneratormaps every failure — absent toolchain, spawn error, unreadable manifest, real drift — to the sameoutputs: nullshape with a reason string. That is correct and deliberate for the AUDIT, where fail-closed is the ruled answer.So the registration is silent rather than throwing because
spawnSyncreturns a status and the code turns it into a reason — there is no exception anywhere on the path.The repair
generatorToolchainPrerequisite(root, entry)asks whether the row's recompute can run at all, by looking for the same binarypnpm execwould resolve. Two remedies kept apart: nothing installed at all, and an install that does not carry the runner.selfTest(), before a single case runs — not beside the battery — so the shared frame's load-bearing clause ("this gate exited before running a single check") is literally true when it prints.--test, the sweep) never consult it.--checkexit, so reading that text would fold a real finding into a refusal — the expensive direction.Reuse, not a sixth spelling
The exit-3 path is a shared helper:
scripts/import-prerequisite.mjsexportsEXIT_PREREQUISITE_NOT_MET,INSTALL_FIXandreportPrerequisiteNotMet, and every sibling that answers this class routes through it. This gate now does too — the code, the frame, the "Nothing was measured … It is NOT a finding" clause and the pipe advisory are all the repo's. The helper is not edited.One thing it cannot express, reported rather than worked around: its advisory names the importer by path, which is right for the single-mode gates it was written for. This file has modes, and
node scripts/pm/check-governed-merges.mjswithout--self-testruns the network sweep. The helper takes no invocation argument, so the correct entry point (pnpm check:pm-governed-merges) is stated in this gate's own detail lines, where the claim about what went unmeasured already lives.Both directions pinned
batteryFloorFailures()is lifted to module scope and takes its ledger as an argument. It had no pin of its own — until it took its inputs there was no way to hand it a ledger — so the direction that matters most was asserted nowhere. Its behaviour, its order and every message it prints are unchanged; ledger fixtures are built from this roster, so each message quotes the real pinned number.New battery, 17 cases (245 → 262 assertions):
node_modules, and prescribes the installnode_modules: exit 3, naming the class, the prerequisite and the remedy, stating in words that nothing was measured, and no longer printing the shrunken-battery diagnosisDID NOT RUN; an assertion under no declared battery and a shrunk roster are both named; a full ledger produces nothingA defect the ablation found, and closed
Ablating the probe turned the end-to-end fixture into an unbounded chain of self-tests — the child reaches that battery only when the probe did not refuse, and there its own copied tree is a perfect seed for the next copy. Measured: one process per second, 67 of them before it was cut off by hand. A hang is the worst reading a gate can return, neither green nor red and indistinguishable from slow. The child is now marked in its argv, a marked run refuses to spawn a grandchild, and the cases it would have measured fail with that reason instead — the same number of cases in both worlds, and a loud red where there was an unbounded wait.
No floor lowered, no case removed
SELF_TEST_BATTERIESfloors are untouched; the roster's own size floor moves 22 → 23 with the battery added to it. No case is deleted or skipped, no other script is touched, and the post-merge audit's verdicts are unchanged —--teston this file still answers NOT governed at exit 0.Verification (union re-run at
d510d4ec, after the final commit)Repro, exit codes captured by redirect, never through a pipe:
origin/mainscripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(identical to the dispatched list; no family added).scripts/also run: 6 green.check:published-readme-exportsreports 1, entirely on unbuiltdist/entries in packages this diff does not touch (its own--self-testpasses; filed separately as a finding of the same class as this card).pnpm lintgreen throughscripts/pm/os-verify-lock.sh(VERDICT command-exit 0, held 75s, waited 0s) — the full scan, not a narrowing.bash .claude/hooks/guard-governed-enqueue.selftest.sh: 49 passed, 0 failed (the hook imports this file's--test).registered 5 case(s), below its pinned floor of 7again, exit 1 — the card's exact shape."the governed predicate: the 2026-08-18 unified list, exactly" registered 7 case(s), below its pinned floor of 8. The floor still bites.git hash-objectequal to the HEAD blob with an emptygit diff HEADand an emptygit status --porcelain.Tooling-only (
scripts/pm/**), publishes nothing ⇒skip-changeset.Generated by Claude Code