Found while running the derived gate family for #13486 (PR linked below). Filed rather than folded in: it is a different file, a different gate, and a different defect class from that card.
Measured at df33dd2111
pnpm check:type-check-debt, run in a worktree where the workspace type closure has not been built, refuses to measure and signals that refusal by throwing — process exit 1:
Error: --re-measure cannot run: 48 workspace dependenc(ies) of the ledgered packages have no built
type entry point on disk -- @objectstack/account, @objectstack/client, ...
Build the closure first, exactly as lint.yml does before this step:
pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'
at measureLedgers (scripts/check-type-check-coverage.mjs:3231:11)
The refusal itself is right, and its reasoning is stated at length in the message. What is wrong is only the exit-code class.
The convention it breaks
This repo already has a documented, distinct code for "nothing was measured". scripts/check-test-completeness.mjs states it in its own failure text:
(Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe ...)
and check:dual-build-cjs-loads answers the identical condition — a gate that reads built output, run against a tree with no dist/ — with exit 3:
PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/.
Run `pnpm build` first. ⛔ This is NOT a pass: nothing was measured.
So three gates in one lint family meet the same "prerequisite not satisfied" condition and two of them answer 3 while check:type-check-debt answers 1.
Why the class matters here specifically
Exit 1 from this gate has a strong, specific meaning: a package's recorded debt went up. The prescribed response to that is to fix at source — the gate's own text makes raising a TEST_DEBT entry a maintainer-only act. A reader who takes the unmeasurable for that red is pointed straight at the ledger, which is the one place they must not go on this evidence.
Anything that reads exit codes rather than prose cannot tell the two apart: a CI step, a wrapper script, or an agent doing a gate reconciliation. The prose is unambiguous, but the prose is not what an exit-code reader reads — which is the whole reason the 3-vs-1 split was introduced elsewhere.
Not a live CI failure
lint.yml builds the closure before this step, so CI does not reach the branch. The exposure is local runs and derived-family gate sweeps, where a reader has to recognise the message to classify the result correctly. That is a real cost — it is exactly the recognition the exit-3 convention exists to make unnecessary — but it is not a red on main.
Suggested shape, not a decision
Return 3 on the not-built branch and keep 1 for a genuine ledger drift, the way the two sibling gates already do. Worth checking in the same change whether any caller currently keys on the raw throw. Also worth deciding, rather than assuming, whether the same treatment is owed to check:type-check-coverage, which shares the module.
Found while running the derived gate family for #13486 (PR linked below). Filed rather than folded in: it is a different file, a different gate, and a different defect class from that card.
Measured at
df33dd2111pnpm check:type-check-debt, run in a worktree where the workspace type closure has not been built, refuses to measure and signals that refusal by throwing — process exit 1:The refusal itself is right, and its reasoning is stated at length in the message. What is wrong is only the exit-code class.
The convention it breaks
This repo already has a documented, distinct code for "nothing was measured".
scripts/check-test-completeness.mjsstates it in its own failure text:and
check:dual-build-cjs-loadsanswers the identical condition — a gate that reads built output, run against a tree with nodist/— with exit 3:So three gates in one lint family meet the same "prerequisite not satisfied" condition and two of them answer 3 while
check:type-check-debtanswers 1.Why the class matters here specifically
Exit 1 from this gate has a strong, specific meaning: a package's recorded debt went up. The prescribed response to that is to fix at source — the gate's own text makes raising a
TEST_DEBTentry a maintainer-only act. A reader who takes the unmeasurable for that red is pointed straight at the ledger, which is the one place they must not go on this evidence.Anything that reads exit codes rather than prose cannot tell the two apart: a CI step, a wrapper script, or an agent doing a gate reconciliation. The prose is unambiguous, but the prose is not what an exit-code reader reads — which is the whole reason the 3-vs-1 split was introduced elsewhere.
Not a live CI failure
lint.ymlbuilds the closure before this step, so CI does not reach the branch. The exposure is local runs and derived-family gate sweeps, where a reader has to recognise the message to classify the result correctly. That is a real cost — it is exactly the recognition the exit-3 convention exists to make unnecessary — but it is not a red onmain.Suggested shape, not a decision
Return 3 on the not-built branch and keep 1 for a genuine ledger drift, the way the two sibling gates already do. Worth checking in the same change whether any caller currently keys on the raw throw. Also worth deciding, rather than assuming, whether the same treatment is owed to
check:type-check-coverage, which shares the module.