Skip to content

test(scripts): split check-readme-exports' repo-state cases per package, not per tree - #7794

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-7460-readme-exports-half-built
Sep 5, 2026
Merged

test(scripts): split check-readme-exports' repo-state cases per package, not per tree#7794
os-sam merged 1 commit into
mainfrom
claude/issue-7460-readme-exports-half-built

Conversation

@os-sam

@os-sam os-sam commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7460

Which repair path, and the measurement that chose it

Both — the three-way split and the mandatory diagnostic — but the split is not a third if. It is the split the gate already uses.

scripts/check-readme-exports.mjs's own header rule (THE SHRINK-ONLY RULE IS SUSPENDED WHERE NOTHING WAS COMPARED) is stated per declaration: a declaration is judgeable iff its own package's export surface is on disk. The test invented a whole-tree boolean (census.packagesUnbuilt === 0) on top of that, which is why a mix had nowhere to go. Replacing the boolean with the per-package partition means built / unbuilt / any mix fall out of one rule, with no state enumerated and therefore no fourth state to be surprised by. Per the triage note on the card, this introduces no second build-state semantics — it removes the one that was invented.

The measurement that chose it, on the real half-built tree (packagesUnbuilt = 2):

ledger OFF: 3 stale-omission  +  3 unjudgeable  |  unjudgeable-type: 0
            stale-omission at: plugin-gantt::GanttTask, plugin-kanban::KanbanColumn, plugin-kanban::KanbanCard
            unjudgeable    at: plugin-ai/README.md (3 self-imports)
ledger ON : 3 unjudgeable, excerptsNotJudged = 0, vacuous = []

So the correct answer for a partly built tree is not a loosening: all three ledgered packages are built here, so all three must still come back stale-omission with the ledger off, and excerptsNotJudged must be 0, not 3. The old line hard-coded Object.keys(PARTIAL_EXCERPTS).length, which is only the CI (never-built) answer. Fully built, the new form reduces to exactly the old built leg; completely unbuilt, to exactly the old unbuilt leg.

A second break in the same leg, measured while writing this

The old unbuilt leg also asserted findings.length + vacuous.length > 0 — "on an unbuilt tree the gate must FAIL". That is not sound of every mix. Measured on a real tree with only plugin-tree unbuilt (build the doc-gate closure, then plugin-ai):

census.packagesUnbuilt = 1 | unbuilt: ["packages/plugin-tree"]
findings.length = 0 | vacuous.length = 0
=> old line evaluates: 0 > 0 ? false      ...on a tree the gate itself PASSES

plugin-tree is unbuilt carrying zero README self-imports and no documented type, so there is genuinely nothing to report and the gate exits 0 correctly. That line is now conditioned on there being unjudgeable work, and the "nothing is silently skipped" guarantee it stood for is stated per package instead (every unbuilt package's README self-imports must all be reported unjudgeable), which is strictly stronger.

The diagnostic (mandatory half)

Every state-dependent assertion now ends with a line naming census.packagesUnbuilt, the packages behind it, and the fact that the state is not the cause:

build state: census.packagesUnbuilt = 2 of 40 packages (35 read, 3 declare no type entry);
unbuilt: packages/plugin-ai, packages/plugin-tree. Every assertion in this case is written
per PACKAGE and holds on a built, an unbuilt and a partly built tree alike, so reaching this
message means a real disagreement — NOT that you built the wrong subset of the tree.

A third case, judges the packages the card named, was also silently early-returning on any non-fully-built tree — on the half-built tree it stopped asserting anything about the 34 packages that were built. It now excuses unbuilt packages by name and checks the excuse against the census.

Three build states, real readings

Command for every row: pnpm exec vitest run scripts/__tests__/check-readme-exports.test.ts.
"before" = origin/main's version of the file on the same tree; "after" = this branch.

build state how it was made census before after
completely unbuilt fresh worktree, pnpm install, no build packagesUnbuilt=36 of 40, packagesRead=1, findings 427, vacuous 4 GREEN 1 file / 87 tests GREEN 1 file / 87 tests
half built pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2 (the card's named real path; 34 of 40 get a dist/) packagesUnbuilt=2plugin-ai, plugin-tree RED 2 failed / 85 passed (87) GREEN 87 / 87
fully built pnpm exec turbo run build --concurrency=2 packagesUnbuilt=0, packagesRead=37 GREEN 87 / 87 GREEN 87 / 87
(bonus) only plugin-tree unbuilt the row above plus turbo run build --filter=@object-ui/plugin-ai... packagesUnbuilt=1 RED 2 failed / 85 passed (87) GREEN 87 / 87

The half-built reds reproduced on a real, unmutated tree, verbatim:

 FAIL  scripts/__tests__/check-readme-exports.test.ts >
       the PARTIAL_EXCERPTS ledger, as it stands in this repository >
       hides ONLY omissions, and the tree says so in BOTH build states
AssertionError: expected false to be true // Object.is equality
 ❯ scripts/__tests__/check-readme-exports.test.ts:898:74
    898|       expect(off.findings.some((f) => f.verdict === 'unjudgeable-type'…

 FAIL  scripts/__tests__/check-readme-exports.test.ts >
       repo state — assertions that hold whether or not the tree is built >
       finds no fabricated or wrong-path import when built, and refuses to pass when not
AssertionError: expected +0 to be 3 // Object.is equality
 ❯ scripts/__tests__/check-readme-exports.test.ts:950:47
    950|       expect(result.census.excerptsNotJudged).toBe(Object.keys(PARTIAL…

Neither names packagesUnbuilt; both case titles promise the opposite.

Known unrelated red on the fully-built row

scripts/__tests__/check-sdui-registration-pins.test.ts is RED on the fully-built tree — measured here as 1 failed | 10 passed (11), on derives the keys from the arrays, and the ruled controls are among them. That is #6893, in flight on claude/issue-6893-sdui-pins-build-state. Not touched here; this branch changes exactly one file.

Lit control

Two legs, both on the half-built tree, both restored under trap RESTORE_FN EXIT INT TERM with git checkout HEAD -- ABSOLUTE_PATH.

Leg 1 — disable the new per-package split (put the old hard-coded Object.keys(PARTIAL_EXCERPTS).length back). Mutation confirmed on disk before reading: injected marker count 1, deleted anchor count 0, blob b55bc69d…f121b056…. Result: RED, and it names itself:

AssertionError: the ledger's suspension count is not the number of entries whose own package
is unbuilt (none). build state: census.packagesUnbuilt = 2 of 40 packages (35 read, 3 declare
no type entry); unbuilt: packages/plugin-ai, packages/plugin-tree. Every assertion in this
case is written per PACKAGE and holds on a built, an unbuilt and a partly built tree alike,
so reaching this message means a real disagreement — NOT that you built the wrong subset of
the tree.: expected +0 to be 3 // Object.is equality

That is the card's expected +0 to be 3, with the state now spelled out ahead of it.

Leg 2 — same broken split, but the diagnostic blanked (note: ''), to show which text is load-bearing. Mutation confirmed on disk: both markers count 1, anchor count 0, blob df35fc58…. Result: RED, and the string packagesUnbuilt occurs 0 times in the entire failure log — back to today's "nothing to search for".

Restoration, proven for both legs (not by the trap firing, by comparing bytes):

HEAD blob = b55bc69da02a144b7ae055c400bd96300dc1bbfe
on disk   = b55bc69da02a144b7ae055c400bd96300dc1bbfe   MATCH
git diff HEAD        -> empty
git status --porcelain -> empty

The two before rows in the table above were produced the same way (git checkout origin/main -- ABSOLUTE_PATH, swap confirmed by blob hash 3501467424f9… ≠ HEAD, restored and re-proved identically).

Gates

  • node scripts/check-changeset-presence.mjsEXIT 0, verdict line verbatim:

    ✅ No source or published contract of a released package changed in this range, so no changeset is owed.

    (preceded by Compared the working tree with abdcd189c (merge-base with origin/main): 1 file(s) changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 0 changeset(s) added.) No changeset added.

  • pnpm type-check:scripts (tsc -p tsconfig.scripts.json) — EXIT 0. This is the project that actually compiles scripts/__tests__/*.ts; turbo run type-check structurally cannot reach it.

  • pnpm check:control-bytes✅ check-control-bytes: OK (scanned 6352 tracked text file(s); skipped 85 binary).

  • pnpm check:shell-escape-residue✅ check-shell-escape-residue: OK (…)

  • pnpm check:vi-mock-specifiers, pnpm check:vi-mock-inherit, pnpm check:doc-example-readers, pnpm type-check:coverage — all EXIT 0.

  • ESLint, narrowed to the one changed file (eslint scripts/__tests__/check-readme-exports.test.ts --no-inline-config --format json): 1 file linted, 0 errors, 0 warnings. The repo-wide pnpm lint sweep was NOT run locally and is left to CI.

All readings above are on 7bc4f79f7, the branch head.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3


Generated by Claude Code

…ge, not per tree

Three cases in `scripts/__tests__/check-readme-exports.test.ts` split the tree
with the whole-tree boolean `census.packagesUnbuilt === 0`: one leg for "built",
one for "unbuilt". A partly built tree is a third state neither leg describes,
and it is what another gate's own printed command produces —
`turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter)`
covers only the 26 packages that gate's documents import, leaving `plugin-ai`
and `plugin-tree` unbuilt. In that state the unbuilt leg judged a mostly-built
tree and two cases failed with `expected false to be true` and
`expected +0 to be 3`, naming neither the counter nor the state.

The split is now per PACKAGE, which is the split the gate itself already uses
("THE SHRINK-ONLY RULE IS SUSPENDED WHERE NOTHING WAS COMPARED"): a declaration
is judgeable iff its own package's export surface is on disk. Built, unbuilt and
every mix fall out of one rule instead of a leg each. Measured while writing it,
the old leg had a second break from the same cause: `plugin-tree` is unbuilt
with zero README self-imports, so on a tree with only that one unbuilt the
whole-tree "the gate must FAIL" line reddened a correct tree. That line is now
conditioned on there being unjudgeable work to report.

Every state-dependent assertion also fails with a message naming
`census.packagesUnbuilt`, the packages behind it, and the fact that the build
state is not the cause — the half of the fix that buys back the round the
filer spent suspecting their own diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
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.test.ts reds on a HALF-built tree — and another gate’s own printed build command is what produces one

1 participant