Filed by domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T07:3xZ. ⛔ Unlabelled and unrouted on purpose — an execution seat files, triage grades and routes.
Surfaced by the at-tier contract review of PR #19600, which lost a reading to it before reading the message text. ⛔ Re-derived first-hand here on origin/main, ⛔ not taken on the reviewer's word.
The defect
packages/spec/scripts/build-docs.ts:103 refuses when the gitignored packages/spec/json-schema/ tree is missing or older than src — a prerequisite the run cannot proceed without. Its own message says exactly that, and names the remedy:
packages/spec/json-schema is missing … The reference docs are rendered from that tree, and it is a gitignored build artifact — nothing in a checkout carries it, and a merge never brings it along. … Generate it first: pnpm --filter @objectstack/spec gen:schema
And then, at :119:
Exit 1 is the FINDINGS code. Exit 3 is the prerequisite code. ⇒ a run in which nothing below line 103 executed is reported in the one channel a machine reads as 「this gate looked and found something」.
Why this is not cosmetic
The exit code is machine-read, not decorative. scripts/pm/dispatch-gates.mjs classifies a recorded run by it (:14138 — 「recorded exit 3 … — PREREQUISITE NOT MET」, pinned again at :28301), and its own docblock at :128 names the failure directly: a run 「refused with PREREQUISITE NOT MET is indistinguishable there from one that」 actually ran.
⇒ the harm is a false MEASURED claim, which is the exact class the gate-record discipline exists to police. A seat or dev reconciling a gate battery records this gate as having run and failed; it did neither. The available repairs then all point the wrong way — someone looks for a finding that does not exist, or records a red where the honest record is NOT MEASURED.
The convention it deviates from is live, and its sibling next door follows it
Lit control, same directory, same staleness class — packages/spec/scripts/build-api-surface.ts:87-91:
// PREREQUISITE NOT MET, not a finding (#19227): nothing below this line ran,
// so the code says so rather than borrowing the one a real breaking-change
// report uses.
…
process.exit(EXIT_PREREQUISITE_NOT_MET);
It imports that constant from ./lib/dist-freshness (:64), alongside prerequisiteNotMetText. packages/spec/scripts/lib/dist-freshness.ts:409 declares export const EXIT_PREREQUISITE_NOT_MET = 3 and :386 records that it 「argues the case at length」; :446 exports the message helper. check-skill-examples.ts:1480-1482 states the three-way distinction explicitly — 「fail() looked and found something, refuse() produced no result because the gate's own invariant broke. This one produced no result」 — and exits with the same constant at :1500.
⇒ the spelling exists, is documented, is used by siblings in the same directory, and was ruled on at #19227. build-docs.ts simply does not reach it.
Why it does not: build-docs.ts:27 imports its detector from a different helper — scripts/check-regen-pending.mjs's schemaTreeIsStale — rather than lib/dist-freshness's inspectDistFreshness. So it gets the detection without the exit-code convention that travels with the other one.
Remedy, in the ordered form
「先删容许出错的构造,再让正确形态成唯一拼写,最后才加检查」:
- Delete the construct that permits the error. The
schemaTreeIsStale path lets a caller choose its own exit code; the lib/dist-freshness path does not, because the constant and the message text travel together. Route this refusal through the same helper the siblings use, or give schemaTreeIsStale's callers a refusal function that exits for them.
- Make the correct spelling the only one — every prerequisite refusal in
packages/spec/scripts/** reaches EXIT_PREREQUISITE_NOT_MET, none spells its own number.
- Only then consider a check that no script under that tree exits 1 on a prerequisite path.
⛔ Not proposed: changing what the gate detects, softening the refusal (it is correctly fatal — the message says a stale tree would 「WRITE pages describing sources this run never read」), or touching check:docs's wiring.
⚠️ Reach of this reading, stated because it bounds the card
One file was measured, because a reviewer named one file. ⛔ No census of the family was taken. Specifically NOT measured: whether other callers of scripts/check-regen-pending.mjs's schemaTreeIsStale carry the same shape, and whether any other script under packages/spec/scripts/** or scripts/** spells a prerequisite refusal as exit 1. Whoever takes this card should census that first — the fix is the same edit either way, and the count decides whether step 3 above is worth its cost.
Duplicate-search words
PREREQUISITE NOT MET, EXIT_PREREQUISITE_NOT_MET, build-docs.ts exit, schemaTreeIsStale, #19227
Generated by Claude Code
Filed by
domain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T07:3xZ. ⛔ Unlabelled and unrouted on purpose — an execution seat files, triage grades and routes.Surfaced by the at-tier contract review of PR #19600, which lost a reading to it before reading the message text. ⛔ Re-derived first-hand here on
origin/main, ⛔ not taken on the reviewer's word.The defect
packages/spec/scripts/build-docs.ts:103refuses when the gitignoredpackages/spec/json-schema/tree is missing or older thansrc— a prerequisite the run cannot proceed without. Its own message says exactly that, and names the remedy:And then, at
:119:Exit 1 is the FINDINGS code. Exit 3 is the prerequisite code. ⇒ a run in which nothing below line 103 executed is reported in the one channel a machine reads as 「this gate looked and found something」.
Why this is not cosmetic
The exit code is machine-read, not decorative.
scripts/pm/dispatch-gates.mjsclassifies a recorded run by it (:14138— 「recorded exit 3 … — PREREQUISITE NOT MET」, pinned again at:28301), and its own docblock at:128names the failure directly: a run 「refused withPREREQUISITE NOT METis indistinguishable there from one that」 actually ran.⇒ the harm is a false MEASURED claim, which is the exact class the gate-record discipline exists to police. A seat or dev reconciling a gate battery records this gate as having run and failed; it did neither. The available repairs then all point the wrong way — someone looks for a finding that does not exist, or records a red where the honest record is NOT MEASURED.
The convention it deviates from is live, and its sibling next door follows it
Lit control, same directory, same staleness class —
packages/spec/scripts/build-api-surface.ts:87-91:It imports that constant from
./lib/dist-freshness(:64), alongsideprerequisiteNotMetText.packages/spec/scripts/lib/dist-freshness.ts:409declaresexport const EXIT_PREREQUISITE_NOT_MET = 3and:386records that it 「argues the case at length」;:446exports the message helper.check-skill-examples.ts:1480-1482states the three-way distinction explicitly — 「fail()looked and found something,refuse()produced no result because the gate's own invariant broke. This one produced no result」 — and exits with the same constant at:1500.⇒ the spelling exists, is documented, is used by siblings in the same directory, and was ruled on at #19227.
build-docs.tssimply does not reach it.Why it does not:
build-docs.ts:27imports its detector from a different helper —scripts/check-regen-pending.mjs'sschemaTreeIsStale— rather thanlib/dist-freshness'sinspectDistFreshness. So it gets the detection without the exit-code convention that travels with the other one.Remedy, in the ordered form
「先删容许出错的构造,再让正确形态成唯一拼写,最后才加检查」:
schemaTreeIsStalepath lets a caller choose its own exit code; thelib/dist-freshnesspath does not, because the constant and the message text travel together. Route this refusal through the same helper the siblings use, or giveschemaTreeIsStale's callers a refusal function that exits for them.packages/spec/scripts/**reachesEXIT_PREREQUISITE_NOT_MET, none spells its own number.⛔ Not proposed: changing what the gate detects, softening the refusal (it is correctly fatal — the message says a stale tree would 「WRITE pages describing sources this run never read」), or touching
check:docs's wiring.One file was measured, because a reviewer named one file. ⛔ No census of the family was taken. Specifically NOT measured: whether other callers of
scripts/check-regen-pending.mjs'sschemaTreeIsStalecarry the same shape, and whether any other script underpackages/spec/scripts/**orscripts/**spells a prerequisite refusal as exit 1. Whoever takes this card should census that first — the fix is the same edit either way, and the count decides whether step 3 above is worth its cost.Duplicate-search words
PREREQUISITE NOT MET,EXIT_PREREQUISITE_NOT_MET,build-docs.ts exit,schemaTreeIsStale,#19227Generated by Claude Code