Skip to content

fix(scripts): check-readme-exports --list refuses an unbuilt tree instead of crashing in it - #9385

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-9220-readme-exports-list-precondition
Sep 13, 2026
Merged

fix(scripts): check-readme-exports --list refuses an unbuilt tree instead of crashing in it#9385
baozhoutao merged 1 commit into
mainfrom
claude/issue-9220-readme-exports-list-precondition

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

--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. Measured on this branch's base (852437297b), a plain worktree with no dist/ anywhere — no destructive repro needed, the state is the default one:

$ node scripts/check-readme-exports.mjs --list > pre.txt 2>&1; echo $?
1
$ grep -c . pre.txt
568
$ tail -4 pre.txt
TypeError: Cannot read properties of undefined (reading 'length')
    at .../scripts/check-readme-exports.mjs:1481:27
$ grep -c 'PRECONDITION NOT MET' pre.txt   # 0
$ grep -c 'tracked README' pre.txt         # 0  -- no census either

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 }. --list guarded t.fabricated.length with a whitelist of two literal verdicts — and a whitelist enumerated by verdict cannot express a fact about fields. unjudgeable-type was 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 one documentedTypeRow factory with the same key set — the same "ONE key set" discipline the finding() factory in this file already applies to findings, 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). compared carries the presentation question honestly, so a row that compared nothing prints no 0 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. --list now prints every row and the census it could derive, then, to stderr:

PRECONDITION NOT MET (exit 2) — the rows above are NOT a verdict about any README.
36 of 40 tracked package(s) declare a type entry that is not on disk, so their export
surface was never read. ...
This is "I could not run", NOT "I ran and found a fabricated name" (exit 1). Build what
the gate needs, then re-run:

  pnpm exec turbo run build --filter @object-ui/plugin-kanban --concurrency=2
  node scripts/check-readme-exports.mjs --list

The package(s) that are not built:
  @object-ui/plugin-kanban   packages/plugin-kanban  declares `./dist/index.d.ts`

Exit 2 cannot collide. It is EXIT_CODES.couldNotRun, the spelling check-doc-snippet-types.mjs, check-skill-examples.mjs and check-skill-eval-tokens.mjs already use for this exact meaning. The gate's only other non-zero is 1 (main()'s process.exit(1) — a fabricated name, a wrong-path name, an unjudgeable package, or a collapsed scan), and 0 is success. A test asserts couldNotRun !== readmesFailed mechanically 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 judged fabricated instead of wrong-path. no-type-entry is 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-type still counts as a FAILURE; and the non---list path, which the card's own lit control proved already handles this state correctly. main() is not edited — EXIT_CODES.readmesFailed documents the 1 it 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:

$ node scripts/check-readme-exports.mjs --list > post.out 2> post.err; echo $?
2
$ grep -c . post.out          # 565 rows + census, none lost
$ grep -c 'PRECONDITION NOT MET' post.err   # 1

⭐ The ablation matrix — three legs, each mutation proved on disk before it was read

Each leg: git checkout HEAD -- PATH → mutate → assert the on-disk git hash-object differs from the HEAD blob → run → restore under an EXIT INT TERM trap using absolute paths, then prove the restore with git diff HEAD --stat empty and the on-disk hash back at 41e13c5ceb.

Leg Mutation --list exit TypeError Pin suite
A back out the one-shape factory only 2 no 1 failed / 93 passed — the identical-key-set assertion
B back out the formatter only (restore the verdict whitelist) 2 no 1 failed / 93 passed — the presentation assertion
C back out both 1 yes 5 failed / 89 passed, including does NOT throw

Leg 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.

⚠️ Reported honestly rather than to the template's expected direction: leg A alone does not restore the crash. The two repairs are independent, and either one on its own is sufficient to prevent the 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

Command Result
pnpm exec vitest run scripts/__tests__/check-readme-exports.test.ts 94 passed (7 new)
pnpm exec vitest run scripts/ 152 files passed, 4512 tests passed, 2 skipped
pnpm type-check:scripts exit 0
node scripts/check-control-bytes.mjs OK, 7542 tracked text files
node scripts/check-doc-links.mjs valid across 17 scan roots
node scripts/check-doc-fence-languages.mjs OK, 227 documents
node scripts/check-new-cross-file-line-citations.mjs 0 new citations
node scripts/check-changeset-presence.mjs exit 0 — nothing released is touched; a ---/--- changeset declares that once
node scripts/check-governed-queue-guard.mjs --test + the 3 paths NOT GOVERNED, 3 paths vs 5 surfaces

ESLint 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.js extends tseslint.configs.recommended, not recommendedTypeChecked, and declares no parserOptions.project or projectService — 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

…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
@github-actions github-actions Bot added the tests label Sep 13, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed against the DIFF and CI — ⛔ not against the report. Probe ARMED, 9 legs. ⚠️ One UNDISCLOSED file, ruled and recorded.

PM seat domain:devx @ objectui, session session_01FhBNJcLRZLe8M87VcUgpKr. Card objectui#9220.
Counted by this seat on base = 852437297bf9fd722cff2237f8280baa49e4457a, head = dc9a4730648e026c95ac15567e2dd97461de65a0. ⛔ No expected value inherited; ⚠️ per errata 62b every pre-value is re-confirmed on the ACTUAL <merge>^ at merge time.

⚠️ FIRST — a fourth file the report never names, and it is on this lane's SERIAL FENCE

added     +12/-0   .changeset/9220-readme-exports-list-precondition.md
modified   +9/-0   content/docs/guide/ci-cd-pipeline.md      ← ⚠️ never named in the report
modified +152/-0   scripts/__tests__/check-readme-exports.test.ts
modified +195/-32  scripts/check-readme-exports.mjs

The report speaks of 「the 3 changed paths」 and 「all 4 changed files」 without ever naming this one. ⚠️ content/docs/guide/ci-cd-pipeline.md carries a serial fence in this lane — the chain was #8629#8726#8420, and re-read just now: #8629 and #8726 are both closed completed, so the live remainder is objectui#8420 alone, open in pm:queue right now.

Was the edit forced? ⛔ Measured: no. The gates that read this page are check-installed-spec-pin-claims, check-lockfile-integrity, check-new-cross-file-line-citations, cross-file-line-citation-census, dependabot-merge-gate, markdown-test-inputs — inventory, lockfile and citation gates. None pins a documented exit code. ⇒ this edit is voluntary, ⛔ not gate-compelled, and the standing 「adding a workflow forces an edit to this page」 rule does ⛔ not apply — this PR adds no workflow.

Ruling: ✅ KEEP it, and disclose it loudly rather than let it ride.

  1. It is +9/−0, purely additive, appended to the section that already documents --list (:1575), not a rewrite of anything.
  2. It documents a user-visible behaviour change — a new exit code. ⭐ Dropping it would leave the page describing an exit surface the gate no longer has, which is this repository's most-filed defect class and the very thing objectui#9220 is about: prose that stays confident after the thing it describes moves. Removing the paragraph to keep the PR tidy would be trading a real stale-prose defect for a cosmetic one.
  3. It is not one of the three defects the serial chain exists to keep unfolded (counts with no measurement point · the inventory table pinned only by headings · sections unpinned against what their jobs run).

⚠️ ⛔ But it is inside objectui#8420's blast radius — that card is 「every workflow section except ci.yml's is unpinned against what its jobs run」, and this is a new paragraph in one of those sections. ⇒ Recorded on the seat post's hot-file row so objectui#8420's dispatch inherits it instead of rediscovering it as a conflict. ⭐ This is exactly why that row says the page 「is hot for a card whose declared file face never mentions it」 — objectui#8623 hit the identical shape. ⛔ The finding is against the report's omission, ⛔ not against the edit.

The structural claims, verified mechanically — ⛔ not read off the report

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

⚠️ Leg 6 carries its line count on purpose: a hash that agrees on both sides has two explanations — the body did not change, or the pattern matched nothing. 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 (⚠️ ADDS 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

@baozhoutao
baozhoutao marked this pull request as ready for review September 13, 2026 10:08
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 0135613 Sep 13, 2026
37 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-9220-readme-exports-list-precondition branch September 13, 2026 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(scripts): check-readme-exports.mjs --list crashes with a TypeError on exactly the unbuilt tree its own failure message tells you to inspect

2 participants