Skip to content

fix(check-published-readme-exports): refuse the third no-population state — imports read, nothing resolved - #10410

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9911-readme-exports-empty-population
Aug 20, 2026
Merged

fix(check-published-readme-exports): refuse the third no-population state — imports read, nothing resolved#10410
os-zhuang merged 1 commit into
mainfrom
claude/issue-9911-readme-exports-empty-population

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #9911

check:published-readme-exports is emphatic about #4690"zero is a broken scan, not a clean repo" — and hard-refuses two no-population states with exit 1. A third state on the same axis was not refused: documents exist, import statements exist, but no import resolves to a workspace member. targets stays empty, so bound stays empty in every document, both halves check nothing, and the run exits 0.

Reproduced first, on the gate as it stood

A fixture publishing one document and three non-workspace imports, run against a byte-identical copy of origin/main's gate (sha256 5933313337c5…, verified against git show origin/main:):

EXIT=0
✓ check:published-readme-exports — 1 published document(s) across 1 workspace package(s); 3 import statement(s), 0 workspace type entr(ies).
  Import half: 0 documented symbol(s) checked …
  Call-site half: 0 documented `X.y(…)` call(s) checked, on 0 import-bound name(s) and 0 name(s) built from one.

Three zeros, 0 workspace type entr(ies), and a pass in CI. #9767 made it legible; this PR makes it a verdict.

The shape of the fix

The population axis is now one staged function, populationRefusal:

published documents  →  import statements  →  workspace type entries

What distinguishes stage 3 from the two already there. Each stage is consulted only because the previous one was non-zero, so the three are mutually exclusive by construction rather than by careful wording — stage 3 cannot shadow or overlap the two above it. What separates them for a reader is the remedy each implies: stage 2 sends you to the extractor, stage 3 to workspace resolution. So stage 3 prints the two counts that are not zero, which is precisely what stage 2 cannot say. A stage the caller has not measured is omitted, never passed as 0publishedDocs knows only the document count and must not speak for the other two (pinned).

Pass 1's census is lifted out of run() as reachedTargets, so the self-test constructs the state with the same code the run uses. A hand-written targets: 0 would pin the refusal's arithmetic while proving nothing about the scan that produces the zero — the exact vacuity this card is about.

Why the refusal stops at targets — measured, not reasoned

#10365 (#9870) landed on this file two hours before this card and left two more quantities a no-population run touches. Neither is in the refusal, and the reason is a measurement on a fixture with a wholly legitimate population of one:

quantity on a real population of one verdict
derivedReceivers 0 — the fence imports a name and calls a static on it without constructing anything ⛔ refusing here reddens a healthy tree
unreadCalls / unreadReceivers NOT read: 2 — and 3 on the empty fixture ⛔ it is the complement; a no-population run drives it UP, not to zero

So the handover's "two more quantities a no-population run would zero" holds for derivedReceivers only; the NOT read pair moves the other way. A zero-test on it would be backwards, and a ceiling-test on it is a coverage ratchet — a different card, wanting a population that exists. targets.size is the whole condition because it is the one quantity that is zero iff both halves are structurally vacuous.

Evidence

It fires on the real state and nothing else — same three fixtures, gate before vs after:

fixture docs / imports / targets before after
nothing resolves 1 / 3 / 0 exit 0 ✗ exit 1, stage-3 refusal
no imports at all 1 / 0 / — exit 1 exit 1, output identical
population of ONE 1 / 3 / 1 exit 0 exit 0, output identical

The healthy tree is untouched. Both gate versions run against this repo with a full build, output diffed:

✓ check:published-readme-exports — 60 published document(s) across 77 workspace package(s); 212 import statement(s), 49 workspace type entr(ies).
  Import half: 313 documented symbol(s) checked …
  Call-site half: 78 documented `X.y(…)` call(s) checked, on 233 import-bound name(s) and 46 name(s) built from one.
IDENTICAL — the healthy tree is untouched

49 workspace type entr(ies) against a threshold of 0 — the refusal has the whole tree as headroom.

Ablations — a green self-test proves nothing until it goes red. Three mutations, each confirmed on disk by anchored grep -c on both the injected and the removed text (never a bare git diff --stat), each restored to a sha256-identical file with the self-test re-run green in between:

  1. neuter the refusal (if (false && targets === 0)) → 1 failure, the stage-3 assertion.
  2. stop the import counter (a dead extractor) → 7 failures, led by expected {"imports":3,"targets":0} / actual {"imports":0,"targets":0}. ⭐ This is the one that matters: it proves the reject side is asserted positively. Without the imports: 3 count, an extractImports that had stopped matching entirely would still produce targets: 0 and sail through as a correctly-firing refusal.
  3. widen it to targets <= 1 → caught by populationRefusal — a population of ONE is never refused, the false-red direction.

Gates (derived with node scripts/pm/dispatch-gates.mjs, re-derived on the final commit 057a3ac7; verdict lines quoted from each gate's own output, exit codes captured before any pipe):

  • pnpm check:published-readme-exportsEXIT=0, self-test + run both green (inside os-verify-lock.sh, reads built dist/*.d.ts).
  • pnpm check:published-readme-linksEXIT=0. It imports publishedDocs, whose refusal this PR rewires, so it is run deliberately: 152 outbound link(s) across 60 published markdown file(s).
  • pnpm check:cross-package-test-inputsEXIT=0, All 60 self-test cases passed. (pure source scanner, run outside the lock — declared.)
  • pnpm check:nul-bytesEXIT=0, scanned 6117 text file(s) … no raw ASCII control bytes, plus an independent grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' on the changed file.
  • pnpm buildEXIT=0 (inside the lock, held 369s).

Clause ② — not triggered. The path limb is packages/spec/src/**; this is root scripts/. On content: this changes a gate's verdict, not a product contract or the public authorable surface.

Changeset: skip-changeset. Judged by publish surface, not habit — the diff is one file under root scripts/, which publishes nothing, and it repairs no packages/* README (the real tree has 0 findings, so the new refusal surfaces nothing to fix).

Out of scope

#10367 — the namespace-import branch that can never fire (namespaceSymbol hardcoded null). Deliberately untouched. This PR makes it neither easier nor harder to fire: reachedTargets moved the census that decides which packages are reached, while namespaceSymbol: null is set later in resolveTarget, on the resolution path this change does not enter. The new refusal cannot mask it either — a tree reaching zero targets now exits 1 before resolveTarget is ever built.


Generated by Claude Code


Generated by Claude Code

…tate — imports read, nothing resolved

`check:published-readme-exports` hard-refuses two no-population states with
exit 1 ("the scan read nothing", "no imports at all"), because a green over an
unread tree is worthless (#4690). A third state on the same axis was not
refused: documents exist, import statements exist, but no import resolves to a
workspace member — `targets` stays empty, so `bound` stays empty in every
document, both halves check nothing, and the run exits 0.

Measured before the fix, on a fixture publishing one document and three
non-workspace imports: exit 0, under a green line reading `3 import
statement(s), 0 workspace type entr(ies)` with three zeros below it.

The population axis is now one staged function, `populationRefusal`: documents
-> import statements -> workspace type entries. Each stage is consulted only
because the previous one was non-zero, which makes the three refusals mutually
exclusive by construction rather than by careful wording, and a stage the
caller has not measured is omitted rather than passed as 0 (so `publishedDocs`,
which knows only the document count, cannot speak for the other two). Stage 3
prints the two counts that are NOT zero — exactly what stage 2 cannot say —
because the two have different remedies: stage 2 points at the extractor,
stage 3 at workspace resolution.

Pass 1's census is lifted out of `run()` as `reachedTargets`, so the self-test
builds the no-population state with the same code the run uses. A hand-written
`targets: 0` would pin the refusal's arithmetic while proving nothing about the
scan that produces the zero — the exact vacuity this refusal exists to catch.

#9870 left two more quantities a run can zero; neither is in the refusal, and
that was measured rather than reasoned. `derivedReceivers` is 0 on a wholly
legitimate population of one (a fence that imports a name and calls a static on
it without constructing anything), so refusing there would redden a real tree.
`unreadCalls`/`unreadReceivers` are the complement of what was checked, so a
no-population run drives them UP, not to zero — the empty fixture reports
`NOT read: 3` against `0 ... checked`. `targets.size` is the whole condition
because it is the one quantity that is zero iff both halves are vacuous.

Verified: the healthy tree's output is byte-identical to the pre-change gate's
(60 documents, 212 imports, 49 workspace type entries); the existing stage-2
refusal is unchanged; a population of one stays green. Three ablations each
drove the new self-test red — neutering the refusal, stopping the import
counter, and widening the refusal to `targets <= 1` — with byte-identical
restores between.

Fixes #9911

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants