Uh oh!
There was an error while loading. Please reload this page.
Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 - #13982
Conversation
… not a finding's 1 Every refusal in this gate reached node's uncaught handler, which exits 1 -- the code the gate reserves for "a package's recorded debt went UP", whose prescribed remedy ends at a ledger only a maintainer may raise. So an unmeasurable run pointed the reader at the one place that evidence must never send them, and anything reading exit codes rather than prose could not tell the two apart at all. Routes all eleven refusal sites through a new refusePrerequisite(), which prints the raising site's message VERBATIM under a PREREQUISITE NOT MET frame and exits 3 -- the code check-test-completeness.mjs states in its own failure text and check-dual-build-cjs-loads.mjs already answers the identical "reads built output, tree has no dist/" condition with. Exit 1 now means only a finding: a structural problem, or a ledger entry that drifted upward. A tsconfig.json checked into the tree that does not parse stays a finding. The self-test pins the class over the refusing FUNCTION BODIES rather than over a constant, because the regression shape is one careless `throw new Error(` added by an author not thinking about exit codes -- invisible to every consumer, all of which treat any non-zero as failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
ACCEPT — ⛔ My Zone 2 C count was wrong, and it was wrong in the direction that mattersI handed the dev the card's single line reference — I re-counted independently at the head sha rather than accepting the number — 11 call sites, at ⇒ Scoped to my count, ten of eleven refusals would still exit 1 — and two of those ten fire on the default invocation, so the card's own defect would have survived on the path most callers take. ⭐ Falsified in the safe direction, with counts. That is the result I ask for. ⭐⭐ The negative control — the best thing in this PR, and nobody asked for itThe self-test pins that the four refusing functions contain no bare So the dev made it a measurement: ⇒ This is the lane's instrument rule — an absence is not a reading until the instrument is known to be able to report presence — applied to the dev's own pin, unprompted. ⛔ Not decoration: without it, a later refactor that removed every bare throw in the file would leave the pin green and blind. ⭐ |
Uh oh!
There was an error while loading. Please reload this page.
…ne stroke with its advisory `scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame that 45 root and package gates import. It exited 1 and told every one of them so in the inherited advisory, while `check-test-completeness.mjs`, `check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR #13982) and `pm/check-half-states.mjs` answer the same two words with 3 — a class `half-state-patrol.yml` already reads by number. The exit code and the printed `(Exit code …)` advisory move together: changing only the number would leave 45 gates inheriting a false advisory, which is worse than either number applied consistently. Nothing mechanical changes today, and the change is not sold as if it did: every consumer of these gates treats any non-zero as failure — the `&&` chains in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing finer. The refusal itself, its wording, and a real verdict's exit 1 are all untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
…ne stroke with its advisory (objectstack-ai#14009) `scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame that 45 root and package gates import. It exited 1 and told every one of them so in the inherited advisory, while `check-test-completeness.mjs`, `check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR objectstack-ai#13982) and `pm/check-half-states.mjs` answer the same two words with 3 — a class `half-state-patrol.yml` already reads by number. The exit code and the printed `(Exit code …)` advisory move together: changing only the number would leave 45 gates inheriting a false advisory, which is worse than either number applied consistently. Nothing mechanical changes today, and the change is not sold as if it did: every consumer of these gates treats any non-zero as failure — the `&&` chains in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing finer. The refusal itself, its wording, and a real verdict's exit 1 are all untouched. Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC Co-authored-by: Claude <noreply@anthropic.com>
Fixes#13885
check:type-check-debt, run where the workspace type closure is not built, refuses to measure — correctly — and signalled that refusal by throwing. An uncaught throw reaches node's default handler, which exits 1: the code this gate reserves for "a package's recorded debt went UP", whose prescribed remedy ends at aDEBT/TEST_DEBTledger only a maintainer may raise. So the one result that is not evidence about the ledger pointed the reader straight at it, and anything reading exit codes rather than prose could not tell the two apart at all.The refusal, its message, its reasoning and when it fires are all unchanged. Only the class changed.
Re-derived line reference
The card cites
measureLedgers ... check-type-check-coverage.mjs:3231. Re-derived onorigin/mainataee1fd9ec, that is exact — reproduced verbatim from a real run:After this change the same refusal is at
scripts/check-type-check-coverage.mjs:3345.It is not ONE branch — it is eleven (Zone 2 C, re-derived)
The PM's count was one. Measured, this file refuses to measure in eleven places, and every one of them exited 1. They now all route through a single
refusePrerequisite()and exit 3.turbobinarypnpm installtscbinarypnpm installgit check-ignorecould not be spawnedgit check-ignoreexited unexpectedlyEvery one of those eleven messages is embedded byte-identically in the new output. Measured on the card's own branch: the refusal text before the change and after it are both 2424 bytes and compare equal.
The boundary, and what deliberately did NOT move.
readTsconfig(line 1499) still throws for atsconfig.jsonchecked into the tree that does not parse. That is a fact about the tree, which is what a finding is — the same linecheck-test-completeness.mjsdraws when it keeps a malformed command line at 1 while a missing input gets 3. Exit 1 from this gate now means only: a structural problem, a ledger entry that drifted upward, or a stale heap-ceiling pin.Triage question 2 — does
check:type-check-coverageshare the treatment?Answered rather than assumed, because the two invocations are one file. Yes, for the two refusals it actually shares (rows 1637 and 1644 above, in the observation half both invocations run) and no further: the structural half reads only
package.jsonandtsconfig.jsonfiles that are checked in, so it has no prerequisite branch of its own to reclassify. Its non-zero stays a claim about the tree.Zone 2 A — the consumer enumeration, and what each does with 1 vs 3
Searched: every file under
.github/workflows/, every file underscripts/, andpackage.json. No consumer anywhere special-cases exit 1, and none would now read 3 as success.lint.yml, jobType Check · debt ledger, step "Re-measure the type-check DEBT / TEST_DEBT ledger"run: pnpm check:type-check-debt— nocontinue-on-error, noif:, no wrapperlint.yml, jobType Check · source gates, step "Check every package is type-check covered or ledgered"run: pnpm check:type-check-coveragepackage.jsonscripts (both)node ... --self-test && node ...; the&&chain yields the last command's status and pnpm propagates itscripts/pm/os-verify-lock.shVERDICT command-exit N, exits Nsh -c "exit 3"yieldsVERDICT command-exit 3)scripts/pm/dispatch-gates.mjsspawnSynctargets aregitand itselfMeasured propagation through the pnpm layer, on an unbuilt worktree:
Direction of error: nothing regressed toward green. GitHub Actions fails a
run:step on any non-zero, so both CI consumers are byte-identical in behaviour. No.github/workflows/**change is needed and none is made.Which precedent, and why
scripts/check-test-completeness.mjs. It is the more argued of the two: it carries a full "Exit codes — and why a REFUSAL has its own one" header, it names its codes as constants so its self-test pins the value each path returns rather than a comment about it, and — the part that decided where to stop here — it states a boundary out loud: exit 3 is for an input the world failed to supply; a malformed command line, which is the caller's own typo, stays 1.scripts/check-dual-build-cjs-loads.mjsagrees on 3 for the identical condition and contributed the "⛔ This is NOT a pass: nothing was measured" sentence shape. It also carries a third code (2, for "the run read almost nothing"); this gate has no analogue and did not adopt one.A third data point, reported rather than changed.
scripts/import-prerequisite.mjs— the shared frame that 20-plus root gates use for "can this gate load the package it imports?" — printsPREREQUISITE NOT METand exits 1, deliberately, with its reasoning stated at the function ("a second failure code would be a new contract nobody asked for"). That is a documented decision, not an oversight, and it is out of this card's scope; it is filed separately so the tension is triaged rather than silently deepened by this PR.Both directions, demonstrated
Prerequisite branch, on an unbuilt worktree, before and after:
A real finding still exits 1, shown twice — once in each half of the gate, so the claim is not carried by one code path:
That mutation was made against the committed tree and restored with
git checkout HEAD -- packages/spec/package.json; the restore is proven by an emptygit diff HEADand bygit hash-objectmatching theHEADblob (0db5f14e...), not by the command's exit code.And a full, real measurement still passes end to end — the closure built, the whole ledger re-measured:
⛔ Not one ledger entry was touched. The re-measure above reads the ledger exactly as
mainrecords it.How the class is pinned
18 new cases in
--self-test, which bothpackage.jsonscripts run on every invocation. The load-bearing one is not a pin over a constant:It reads the four refusing functions' real bodies, because the regression shape is not a wrong number typed anywhere — it is one careless
throw new Error(...)added by an author who never thought about exit codes, and it announces itself with a green CI (every consumer above treats any non-zero as failure, so 1-instead-of-3 is invisible to all of them).readTsconfigis the negative control: it still throws on purpose, so the pin has something to see and cannot pass by looking at an empty set.The pin was ablated rather than assumed. Reverting the card's own branch to
throw new Error(and rebuilding nothing (this is a source-run script):The mutation was confirmed on disk by a
str.replacewhose single-hit count was asserted before writing and by the blob hash changing (e2a91d7dto22a12247); the restore is proven bygit hash-objectreturning theHEADblob again, not by a trap firing.The other 17 cases pin the constants (3 is the refusal code, 1 is a finding, they are distinct from each other and from 0) and six clauses of the refusal text — including that it carries the raising site's message verbatim and that it turns the reader away from the ledger.
One behavioural repair the exit path required
measureTestDebtcreated a temp directory outside the repository and removed it in afinally. Afinallydoes not run onprocess.exit, so a refusal raised from insidetscErrorCountwould have leaked one. The directory is now also registered onexitand de-registered in thefinally, so both paths clean up. Verified after the full 29-entry re-measure above: zeroobjectstack-debt-remeasure-*directories left behind.Changeset
None, deliberately —
skip-changesetinstead. This PR changes one file underscripts/, publishes nothing from any package, and alters no runtime behaviour any consumer of a published package can observe. Precedent onmainis uniform: script-only landings carry no changeset (for example2aef62eba fix(scripts): let check-role-word skip machine-generated REGIONSand010518bb1 docs(pm): express the self-test's size as a floor— one file each, zero changesets). TheCheck Changesetjob has no path exemption, so the label is the mechanism, and it is applied on this PR rather than left for the gate to go red first.Gates: derived vs run
Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackat the final commit92d273fe8— 16 families (14 by path, 2 by the "adds or edits a GATE SCRIPT" change kind).comm -23of derived against run is empty. Two were run beyond the derived set on purpose:check:ratchet-remedy-authority, which builds its scan surface withreaddirSyncand so cannot be path-derived, andcheck:nul-bytes.Every exit code above was captured before any pipe, into a file, never as
$?after atail.check-test-completenessanswering 3 is the convention this card is about, working: the derived family hands out the bare invocation, which has no log to grade, so the gate refuses. Its own header documents that exact interaction. It is NOT MEASURED, and it is unrelated to this diff.Repo-wide ESLint was run in full rather than narrowed —
eslint . --no-inline-config, 5597 files, 0 errors, 0 warnings, 99s under the shared verification lock.All readings above are from the tree at
92d273fe8.Generated by Claude Code
Generated by Claude Code