fix(spec): six dist-reading gates answer an unmet build prerequisite with exit 3, not a finding's 1 - #19463
Conversation
…with exit 3 The five gates that adopted the #7122 dist precondition, plus the bundle reader next door, all spelled their refusal `process.exit(1)` -- the code a real finding uses. `scripts/import-prerequisite.mjs:250` declares `EXIT_PREREQUISITE_NOT_MET = 3` for exactly these two words and argues the case, so the repo's vocabulary was being contradicted from the one place a caller looks. The bite is not cosmetic: `scripts/pm/dispatch-gates.mjs --ran` derives its NOT-MEASURED class from the recorded exit code and counts every other non-kill code as a family that RAN, so a gate that refused before reading a single declaration was reconciled as coverage the round did not have. `lib/dist-freshness.ts` -- already the one home for this precondition -- gains the constant and the fleet-shaped refusal text; the six call sites use them. What a real finding returns is untouched: every `fail()`, `refuse()`, self-test and ratchet verdict still exits 1. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…o-end legs `expectRefusal` in the adoption test pinned `status` to 1, so the roster moves with the gate in the same PR. It now pins `EXIT_PREREQUISITE_NOT_MET` and the two sentences that make the number legible. `dist-freshness.test.ts` gains the spec-side half of the one-vocabulary pin: the constant is 3, it is distinct from a finding's 1, it agrees with the root frame read from `scripts/import-prerequisite.mjs` itself (inside the `scripts/**` radius `packages/spec` already declares), and the refusal text carries the fleet phrase plus the "Nothing was measured" paragraph. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…ence `scripts/import-prerequisite.d.mts` is partial by design and declares only the two loaders, so tsc sees a namespace without the exit-code constants and refused the direct cast (TS2352) under `check:scripts-typecheck`. The cast now goes through `unknown`, and the two `typeof` assertions are what stop a vanished export from reading as `undefined` on both sides of a comparison. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: Read first-hand from a worktree pinned to that sha (base ① Derived judgmentsAccept/reject behaviour. The diff moves exactly seven refusal sites in six gates from exit 1 to Text. Population. Constant. Hand-carried, as claimed. Ablation. Reproduced on disk without running it: restoring
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19227
Refs #19450
Clause-②: no
Six
packages/specgates refused an unmet build prerequisite withprocess.exit(1)— thecode a real finding uses — while
scripts/import-prerequisite.mjs:250declaresEXIT_PREREQUISITE_NOT_MET = 3for exactly those two words and argues the case in its owndocblock. This lands the six on that vocabulary. ⛔ Nothing about what a real finding returns
moves: every
fail(),refuse(), self-test, ratchet and diagnostics verdict still exits 1.Why it is not cosmetic — reproduced in this round
scripts/pm/dispatch-gates.mjs --randerives its NOT-MEASURED class from the recorded exitcode (
dispatch-gates.mjs:13932) and counts every other non-kill code as a family that RAN.Driven twice over the same derivation of this very branch, same tool, same tree, only the six
recorded codes differing:
exit 1(before this PR)65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN— and the tool calls it "a DERIVED zero — all 65 recorded an exit code and none of them is 3"exit 3(after this PR)65 derived, 59 run, 6 NOT-MEASURED, 0 UNRUN, each of the six named with "recorded exit 3 … PREREQUISITE NOT MET"So the card's sharpest claim — filed as UNREPRODUCED by both the filing seat and the PM — holds:
a gate that refused before reading a single declaration was reconciled as coverage the round did
not have. That is a reconciliation instrument reporting coverage it does not have, not a code
mismatch.
The true population — six gates, seven sites
The card verified four files with
grep -c 'process.exit(1)'and could not locatecheck-api-surface/check-api-surface-declarations"under that spelling". Measured hereagainst
origin/main@8e368dc, the population is the set of guarded refusals reached throughlib/dist-freshness.ts:build-api-surface.tsdistcheck:api-surface— spelledbuild-*because one file servesgen:andcheck:check-browser-reachable-entries.tsNOT MEASUREDand then exited 1check-dual-source-exports.tsdist, both modesexit(1)x6check-entry-nameability.tsdistexit(1)x0 — the instrument was blind: this gatereturns its code toprocess.exit(main())check-exported-any.tsdistexit(1)x5check-skill-examples.tsdistand a self-package with no declarations (wasfail())exit(1)x6; the refusal text the card could not find is atcheck-skill-examples.ts:3219check-api-surface-declarationsdoes not exist as a separate gate under any spelling. The nearestcandidate,
build-declaration-map.ts(check:declaration-map), reads nodistand has noprerequisite refusal;
build-export-origins.tssays in its own header that it readssrc/, neverthe built dist. Recorded as: not present.
#19450 is the same defect measured independently on
check-skill-examples.ts(the refusal fireswhen
packages/client-react/distholds no declarations; the built closure returns a true exit 0).Its site is covered here. ⛔
Refs, notFixes— the dedupe/close call is triage's.Dispositioned, not changed — with readings
check-entry-nameability.ts's dead-canary, foreign-diagnostic and zero-probe refusals keepreturn 1. The gate says so itself: "Both are findings; neither is a pass."check-skill-examples.ts'srefuse()keeps exit 1 — a surface that never reached tsc is thegate's own invariant breaking, not an unbuilt workspace.
--self-testfailure, battery floor and verdict handshake keeps exit 1: a broken detectoris a finding about the instrument.
check-dual-source-exports.ts's missing tracked baseline keeps exit 1 — a checked-in filethat is absent is a repo-state finding, not a build-environment prerequisite.
check-generated.ts:751refuses areadsDistgenerator on a stale dist, but folds it intofailedand exitsfailed ? 1 : 0across every sub-gate. There is no per-gate exit to movewithout changing what the aggregate returns for a real failure. Left alone, recorded here.
What the change is
lib/dist-freshness.tsis already this package's one home for the precondition ("they live in ONEhome, this file, so the next bundle-reading gate finds them together"), so it gains
EXIT_PREREQUISITE_NOT_MET,EXIT_FINDINGSandprerequisiteNotMetText(); the seven call sitesuse them. The verdict's own message — cause, damage, two-line build remedy — is passed through
byte for byte; the wrapper adds the fleet phrase
GATE: PREREQUISITE NOT MET — …thatcheck-dts-closure,check-dual-build-cjs-loads,check-i18n-bundles,check-i18n-coverageandcheck-closing-target-claimall already print, plus the "Nothing was measured" paragraph thatnames the code. A number with no sentence beside it leaves the reader where they were.
Route (the brief offered both). The shared
scripts/import-prerequisite.mjsframe is notadopted: it is a dependency LOADER (
requireDependency/requireDefaultExport) answering "canthis gate import the package it names?", which is a different prerequisite reached a different
way. The constant is hand-carried with the citation, because
scripts/import-prerequisite.d.mtsis partial by design — its own header records that the exit-code constants are deliberately left
out — so importing the name is TS2305 until that root-tree mirror is extended, and extending it is
outside this card's surface. Drift is closed by a pin on each side instead: that module's
--self-testasserts=== 3there, anddist-freshness.test.tsasserts the same here andcompares against the value read from that module itself.
Verification
origin/main(8f6d831) is merged; it touched none ofpackages/spec/scripts/,scripts/import-prerequisite.mjsorscripts/pm/dispatch-gates.mjs. Everything below is measuredafter that merge, on
c28fe19, with the closure rebuilt on the merged sources (turbo run buildover spec + client + client-react, 34 tasks, exit 0) — each exit captured before any pipe:Six green is the positive control: the change refuses nothing CI is legitimately in.
Ablation. The refusal in
check-exported-any.tswas put back toconsole.error(freshness.message); process.exit(1);throughscripts/ablation-replace.mjs(anchor hit x1, blob8e3498ca72bf -> 391020853d3a— the mutation is proved on disk, not by an editor's exit code).dist-freshness-adoption.test.tsthen read 1 failed | 9 passed, the one failure beingcheck:exported-any … AssertionError: expected 1 to be 3. Nine cases stayed green, so the pin istargeted rather than blanket. Restored through the same tool: blob back to
8e3498ca72bf, equal toHEAD,
git diff HEADempty. Nodistpreflight applies — these gates run from source undertsxand the sandbox copies
packages/spec/scripts, which is precisely why the mutation reached the run.Tests and gates, all exit 0:
check:scripts-typecheck·@objectstack/spec typecheck·check:cross-package-test-inputs·check:nul-bytes·check:test-source-alias·check:issue-citations·check:pm-dispatch-gates.Changeset:
skip-changeset, measured.packages/spec'sfiles[]isdist json-schema liveness prompts llms.txt README.md src/**/*.zod.ts CHANGELOG.md api-surface spec-changes.json—scripts/is not on it. After the build,prerequisiteNotMetText,EXIT_PREREQUISITE_NOT_METandinspectDistFreshnesseach grep to 0 files under those paths,while the positive control (
QueryAST55,ObjectSchema59) fires. Nothing here publishes.Governed surfaces: none, and none falsified
The landing-path question, re-measured rather than inherited. The instrument reaches governed prose
(control: 8
exithits inAGENTS.md). Acrossdocs/adr/**,docs/NORTH-STAR.md,.claude/**,skills/**,AGENTS.mdandCLAUDE.md, no file pins a number to these gates' prerequisitestate. The gate NAMES appear (
AGENTS.md:731,736,743,.claude/skills/spec-property-retirement/SKILL.md:285,321-322) and carry no exit code..claude/skills/pm-dispatch/references/lanes/— the surface #14008 had to repair — holds exactlyone
exitmention today,director.md:66, about a different tool.AGENTS.md:736("a failurethere is a real finding to fix") reads MORE true once a prerequisite stops being spelled as a
finding. ⇒ no governed hunk is pulled in, and this stays an ordinary queue landing.
Acceptance notes
Found while sweeping for the true population; ⛔ not fixed here, to be filed rather than ridden in:
build-docs.ts:103-119is a seventh site of the same class. It refuses a stale or missingjson-schematree — a different artefact reached through a different predicate(
schemaTreeIsStale, notdistIsStale), outside the "six gates" this card rules on — and it,too, answers exit 1. Same remedy shape ("Generate it first"), same reader confusion.
throwbackstops reach the shell as exit 1 with a node stack trace for thesame event:
build-api-surface.ts:145,check-exported-any.ts:143,check-dual-source-exports.ts:125("Is the package built?") andcheck-entry-nameability.ts:311("Build first"). These are exceptions, not refusals with a chosen code, so converting them is a
behaviour change rather than a constant swap — which is why they are named here instead.
AGENTS.md:735-737callscheck:skill-examples,check:exported-anyandcheck:dual-source-exports"pure source audits", yet all three read the builtdistand refuseon it. Pre-existing prose imprecision, untouched by this diff and carried by no gate. Successor:
the next PR that edits that paragraph for another reason.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Generated by Claude Code