fix(scripts): check-readme-exports --list refuses an unbuilt tree instead of crashing in it - #9385
Conversation
…ee instead of crashing in it
`--list` is this gate's own documented diagnostic, and the only state a
developer reaches for it in is the state where the gate just failed. On an
unbuilt tree it died there: the row formatter read `t.fabricated.length`
behind a guard that whitelisted two literal verdicts, `unjudgeable-type` was
not one of them, and the run threw `TypeError: Cannot read properties of
undefined (reading 'length')` on the first declaration it could not judge --
no census, no row past that one, and exit 1, the same code the gate uses to
report a genuinely fabricated name.
Two repairs, in that order:
1. One row shape. Four call sites push into `documentedTypes` and three
pushed a bare `{ ...site, verdict }`. A field-access guard enumerated by
VERDICT cannot express a fact about FIELDS, so adding a third string to
that whitelist would fix today's instance and leave the construct intact
for the next verdict anyone adds. Every row now comes out of
`documentedTypeRow` with the same key set, and the verdict check retreats
to what it should govern -- whether to PRINT this census detail, not
whether these fields can be READ. `compared` carries that presentation
question honestly: a row that compared nothing prints no "0 of 0".
2. A precondition, on the convention this repo already declared in
`check-doc-snippet-types.mjs`. `--list` prints every row and the census it
could derive, then `PRECONDITION NOT MET (exit 2)` naming the unbuilt
packages and a build command scoped to them. Exit 2 is dedicated so the
status discriminates again: 1 still means a verdict was read.
The gate's judgement is untouched -- `unjudgeable-type` is still a FAILURE,
and the non-`--list` path, which already handled this state correctly, is not
edited.
Fixes #9220
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Reviewed against the DIFF and CI — ⛔ not against the report. Probe ARMED, 9 legs.
|
| leg | kind | pre | post |
|---|---|---|---|
1 · bare push { ...site, verdict: 'unjudgeable-type' } |
must-LEAVE | 1 | 0 |
2 · one-shape factory documentedTypeRow( |
must-ARRIVE | 0 | 5 |
3 · verdict WHITELIST t.verdict === 'local-declaration' in the --list formatter |
must-LEAVE | 1 | 0 |
4 · PRECONDITION NOT MET |
must-ARRIVE | 0 | 1 |
5 · process.exit( call sites |
must-GROW-BY-ONE | 2 | 3 |
6 · ⭐ main() body hash |
must-STAY-UNCHANGED | ffd6626716d72c7a (145 lines) |
identical (145 lines) |
7 · ⭐ findings.push({ ...site, verdict: 'unjudgeable-type' |
must-STAY — the FAILURE fence | 1 | 1 |
8 · control check-readme-exports, known direction, HITS |
control | 10 | 11 |
9 · nonsense control check-raedme-exports |
control | 0 | 0 |
e3b0c44298fc1c14 is the sha256 of the empty string and would mean the latter. 145 lines on both sides rules it out. (This seat published exactly that vacuous leg on PR objectui#9384 an hour ago; it is not repeating it silently.)
⭐ Legs 5 + 6 + 7 together are the three fences triage set, each read from the tree rather than from the report: the new exit code is reachable only from the --list path (main() byte-identical), the gate's judgement is untouched, and unjudgeable-type still lands in findings and still drives the ❌ block ⇒ still a FAILURE.
⭐ Two places the dev beat the instructions, and both are worth keeping
1. The repro is sharper than the card's. The card prescribes a destructive mv packages/plugin-kanban/dist /tmp/parked. The dev never ran it: a fresh worktree has no dist/ at all, so the unbuilt state is the default state and the TypeError reproduces with no mutation of any tree. ⇒ ⛔ nothing was moved in any checkout, and the repro is now something anyone can run without a restore step to forget.
2. ⭐⭐ The ablation was reported as MEASURED, against the template's presumption. Triage's acceptance reads 「撤掉同形化那一步,该 pin 必须变红」. It is met — but the dev states plainly that it goes red on the identical-key-set assertion and ⛔ not on a throw, because the two repairs are independent and either alone prevents the crash; only backing out both (their leg L5) reproduces the original TypeError, and that is the positive control with the known direction.
⇒ ⭐ That is the correct instinct and the harder one: an acceptance item is a claim about the world, and when the world turns out to be shaped differently, you report the shape — you do not bend the measurement to the sentence. The honest version is more informative than the presumed one, because it establishes the two repairs are independently sufficient — which the template's expected result would have hidden.
Remaining
CI on dc9a473064: 27 success · 3 skipped · 5 in flight · ⛔ 0 red. Still draft.
Next: green → draft→ready (Governed Surface Queue Guard, 6/6 this shift ⇒ the pre-flip green is stale) → re-wait every check → enqueue → confirm added_to_merge_queue → ⭐ re-count all nine legs on the ACTUAL merge parent.
⛔ No acceptance item unmet, no fence crossed, nothing narrowed. The one finding is the undisclosed fourth file — kept, ruled, and written down where the next card will find it.
Generated by Claude Code
--listis this gate's own documented diagnostic, and the only state a developer reaches for it in is the state where the gate just failed. On an unbuilt tree it died there. Measured on this branch's base (852437297b), a plain worktree with nodist/anywhere — no destructive repro needed, the state is the default one:Exit 1 — the same code the gate uses to report a genuinely fabricated name.
What changed, in the order the repair had to happen
1. One row shape, because the guard was enumerated by the wrong thing
Four call sites push into
documentedTypes; three pushed a bare{ ...site, verdict }.--listguardedt.fabricated.lengthwith a whitelist of two literal verdicts — and a whitelist enumerated by verdict cannot express a fact about fields.unjudgeable-typewas the third bare shape and was not in it.⛔ Adding
'unjudgeable-type'to that ternary was explicitly declined by triage and is not what this PR does: it repairs today's instance and leaves the construct standing for the next verdict anyone adds. Every row now comes out of onedocumentedTypeRowfactory with the same key set — the same "ONE key set" discipline thefinding()factory in this file already applies tofindings, one screen down. The verdict check retreats to what it should govern: whether to print this census detail (presentation), not whether these fields can be read (safety).comparedcarries the presentation question honestly, so a row that compared nothing prints no0 of 0— which would state a comparison that never ran.2. A precondition, on the convention this repo already declared
Following
scripts/check-doc-snippet-types.mjs, which prints its findings and then refuses.--listnow prints every row and the census it could derive, then, to stderr:Exit 2 cannot collide. It is
EXIT_CODES.couldNotRun, the spellingcheck-doc-snippet-types.mjs,check-skill-examples.mjsandcheck-skill-eval-tokens.mjsalready use for this exact meaning. The gate's only other non-zero is1(main()'sprocess.exit(1)— a fabricated name, a wrong-path name, an unjudgeable package, or a collapsed scan), and0is success. A test assertscouldNotRun !== readmesFailedmechanically rather than leaving the claim to prose.Every unbuilt package is named, not only those carrying a README: an unbuilt package contributes no names to
nameOwners, so a README import of a name it really does export is judgedfabricatedinstead ofwrong-path.no-type-entryis deliberately not a precondition — no build fixes it, and it is a verdict about the manifest.⛔ What was NOT touched
The gate's judgement logic;
unjudgeable-typestill counts as a FAILURE; and the non---listpath, which the card's own lit control proved already handles this state correctly.main()is not edited —EXIT_CODES.readmesFaileddocuments the1it spells literally and does not reach it, said in the docblock rather than left to read as live (AGENTS.md #9).Measurement
Post-state, same command, same tree, at
dc9a473064:⭐ The ablation matrix — three legs, each mutation proved on disk before it was read
Each leg:
git checkout HEAD -- PATH→ mutate → assert the on-diskgit hash-objectdiffers from the HEAD blob → run → restore under anEXIT INT TERMtrap using absolute paths, then prove the restore withgit diff HEAD --statempty and the on-disk hash back at41e13c5ceb.--listexitLeg C reproduces the reported defect exactly — exit 1,
TypeError, no census. That is the positive control, and it is what makes the pin's "does not throw" assertion carry information rather than be green by construction.TypeError— the formatter no longer reads a field the bare row lacks. The triage acceptance item ("back out the 同形化 step and that pin MUST go red") is met — leg A turns the suite red — but it turns it red on the shape assertion, not on a throw, and saying otherwise would misdescribe the mechanism.Gates run locally, before the push
pnpm exec vitest run scripts/__tests__/check-readme-exports.test.tspnpm exec vitest run scripts/pnpm type-check:scriptsnode scripts/check-control-bytes.mjsnode scripts/check-doc-links.mjsnode scripts/check-doc-fence-languages.mjsnode scripts/check-new-cross-file-line-citations.mjsnode scripts/check-changeset-presence.mjs---/---changeset declares that oncenode scripts/check-governed-queue-guard.mjs --test+ the 3 pathsESLint narrowing, declared: the two touched source files were linted directly and report 0 errors / 0 warnings (
--format json, 2 files). The repo-wide run at this head reads 4920 files, 95 errors, 13052 warnings, all pre-existing and none in a file this PR touches (checked by filtering the JSON for the changed paths). The narrowing is sound because type-aware linting is not enabled —eslint.config.jsextendstseslint.configs.recommended, notrecommendedTypeChecked, and declares noparserOptions.projectorprojectService— so this diff cannot move the verdict on any file it does not contain.Fixes #9220
🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code