Skip to content

fix(devx): wire check:published-readme-exports' namespace branch and refuse the fourth no-population state - #10596

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10367-readme-exports-population
Aug 21, 2026
Merged

fix(devx): wire check:published-readme-exports' namespace branch and refuse the fourth no-population state#10596
os-zhuang merged 1 commit into
mainfrom
claude/issue-10367-readme-exports-population

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10367
Fixes#10417

Two no-population defects in scripts/check-published-readme-exports.mjs, at adjacent stages of one axis. This gate's own header raises "zero is a broken scan, not a clean repo" (#4690) to a hard error; both cards are places where that principle was not actually enforced, and both produce the failure direction AGENTS.md calls worse than no verifier — a green printed over a population of nothing.

The tree as measured, before anything changed

Built the 42 packages the gate reads, then ran it on the real tree:

✓ 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 against the exports their package publishes.
Call-site half: 78 documented `X.y(…)` call(s) checked, on 233 import-bound name(s) and 46 name(s) built from one.
NOT read: 120 documented `X.y(…)` call(s) on 72 receiver(s) with no type this gate can reach

Both halves healthy with enormous headroom — which is what the #10417 half of this PR was told to decide on, and it decided for the hard refusal. See below for why the headroom is not the argument.

#10367 — the namespace branch, wired (not deleted)

analyzeDocument binds a namespace import so the call-site half can read its members against the module's export set. run()'s resolveTarget was the only production caller and returned namespaceSymbol: null unconditionally, so the branch bound nothing on every run this gate has ever made. hasMember's SymbolFlags.Module arm has always been able to answer the question; only the symbol was never supplied.

The card's zero-twice-over claim re-verified on today's tree, not inherited: the pre-change gate and this one print byte-identical output on the real tree (diff clean), so wiring changes no verdict today. The tree's five namespace imports are all relative specifiers, which splitSpecifier turns away first.

Wired rather than deleted, per the ruling on the card: deleting coverage to answer an unreachable branch contradicts the thesis this file is built on, and the cost is nil because exportsOf has already resolved the module symbol into the shared cache.

  • typeSurface gains moduleSymbolOf(abs), reading the cache exportsOf fills.
  • surfaceTarget(surface, abs) is lifted out of run() and exported. This is the part worth reviewing: every existing test drives analyzeDocument through a fake resolver, so the suite pinned the branch that consumes the symbol and could never see that the supplier returned null. A branch is only as exercised as its least-tested end. Lifting the supplier out makes that end assertable, so the regression cannot ship silently twice.
  • The finding sentence gets a namespace arm. The existing arm interpolates the imported name, which for a namespace binding is the literal * — an author would have read "but * (from '@objectstack/spec') has no x member". It now names the package instead.

#10417 — the fourth no-population state, refused

targets.size >= 1 says an entry was reached, never that a name was bound, and both halves consume bindings. So there is a state below stage 3 that all three existing refusals pass. Not a fourth stage of populationRefusal: the honest condition is whole-run and cannot be known until analyzeDocument has read every document. It lands as a separate post-pass, bindingRefusal.

The condition is the conjunction symbolChecks === 0 && callChecks === 0, and #10367 is what makes that load-bearing rather than stylistic — a namespace import binds a receiver the call-site half reads while ticking symbolCheckszero times. A tree documenting only namespace imports is fully read and reports 0 documented symbol(s) checked. A refusal written on either half alone would redden it. The two cards constrain each other, which is the argument for landing them together.

Placement was measured, not reasoned. It sits below the unbuilt branch: on an unbuilt checkout both counters are also zero, and pnpm build is the more actionable sentence. Confirmed by running the gate on an unbuilt tree — it prints 192 package(s) are not built, not the vacuity refusal.

On the false-red constraint, which is the acceptance boundary

The card is right that headroom is not the argument, so the argument is the file's own rule for what may be refused: a quantity is refusable when it is zero if and only if the run is vacuous, and not refusable when an ordinary healthy tree can produce the zero — which is exactly why derivedReceivers is only printed. The conjunction passes that test by construction: it is not a proxy for "did the halves read anything", it is that question. The residual cost is a tree whose published fences document only side-effect and type-only imports — a tree with nothing for this gate to check at all — and the honest reply to that is this sentence rather than a silent pass. The failure text names that case explicitly and sends it at the fences or at retiring the gate, never at the baseline.

Ablations — every mutation confirmed on disk before the result was read

No mutation was believed on an editor's exit code; each was verified with grep -c on the text being changed, before and after, and every restore was checked (git diff --quiet, or cmp against a pre-ablation copy). The gate reads source and built .d.ts, so no rebuild was needed between legs; the built tree was constant throughout.

1. #10367, end-to-end on the real tree — "the day a published README namespace-imports a workspace package". Appended a fence to packages/spec/README.md importing * as spec from @objectstack/spec with one real export and one invented (marker counts 0 → 1 each, 4053 → 4166 bytes), then ran both gates against the same tree:

gateexitresult
pre-change (namespaceSymbol: null)0green; 78 call(s) checked unchanged, NOT read 120 → 122. The blind spot is real and attributed to "no type this gate can reach", which is not the reason.
this PR1documents \spec.defineNothingAtAll(…)` on a namespace import of '@objectstack/spec', but that package's published types export no `defineNothingAtAll`.`

Then the same fence with only the valid member, to show the branch reads rather than merely reddens: pre-change 78 checked / 233 receivers / NOT read 121; this PR 79 checked / 234 receivers / NOT read 120 — the receiver is bound and its call leaves the unread bucket. defineView resolved through the real checker.getExportsOfModule, whose module symbol was confirmed to carry SymbolFlags.Module (flags 512). README restored and cmp-verified byte-identical.

2. #10417, end-to-end on the real tree — the exact regression the card names. Injected "parseImportClause stops returning bindings" into both gates (marker 0 → 1, +84 bytes each):

gateexitresult
pre-change0✓ … 212 import statement(s), 49 workspace type entr(ies). over 0 documented symbol(s) checked and 0 … call(s) checked. The #10417 shape at full tree scale — the card measured it on a 1-document fixture.
this PR1the fourth refusal, naming the three counts that are not zero and pointing at the binding step.

NOT read went up (120 → 198) under the regression, confirming the card's point that the unread pair is unusable as the test.

3. Self-test assertions, each broken deliberately.

mutation--self-test
surfaceTarget's namespaceSymbol back to nullred, 1 failure — the PRODUCTION caller supplies a module symbol, never a null
neuter the bind branch (const ns = null)red, 4 failures — including bindingRefusal — a namespace-only tree … is never refused flipping, which is the conjunction argument firing live
bindingRefusal always returns nullred, 1 failure — a run in which every import RESOLVED and not one BOUND A NAME is not refused

Every self-test zero is measured by the same reachedTargets / analyzeDocument the run calls, never typed into an assertion, and every reject side is asserted positively — the namespace fixture pins that a real export stays silent and that an invented one is reported, with the counters (receivers: 1, checked: 2, unread: 0) so that "bound nothing, read nothing" cannot pass as "checked two, found one".

Gates

Derived with node scripts/pm/dispatch-gates.mjs (no path arguments) against the real change set, then run on head commit dfcd87c2c6 under the shared verify lock (VERDICT command-exit 0):

check:published-readme-exports --self-test exit 0 ✓ … All FOUR steps of the population axis are refusals
check:published-readme-exports exit 0 ✓ 60 published document(s) … 313 symbol(s) checked, 78 call(s) checked
check:cross-package-test-inputs exit 0 All 60 self-test cases passed. OK: 12 package(s) read outside themselves
check:nul-bytes exit 0 scanned 6187 text file(s); no raw ASCII control bytes
eslint scripts/check-published-readme-exports.mjs exit 0

No changeset: the diff is scripts/** only and publishes nothing (skip-changeset). This diverges from the dispatch brief, which expected a changeset entry — the tree's practice is the opposite, and PR #10502 (a scripts/**-only gate PR merged the same day) is the precedent: skip-changeset, no changeset file.


Generated by Claude Code

…refuse the fourth no-population state
Fixes#10367Fixes#10417
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

PM review — ACCEPT, arming. And your open question is answered: you were right, my brief was wrong.

Reviewed against the tree and the PR, not against the report. Head dfcd87c2, 28/28 checks green, 0 failing, 0 running (latest run per check name).

⭐ The changeset question — ratified, and the error was mine

You took skip-changeset over the .changeset/*.md my brief demanded, and flagged the contradiction instead of quietly complying. Verified independently:

  • AGENTS.md:943 already says it"Add a changeset for feature work… Pure bug fixes do not require a changeset."
  • .github/workflows/pr-automation.yml's Check Changeset job names exactly two exemptions, the first being the skip-changeset label — "the author's explicit opt-out", under the heading "this PR declares no release of its own".
  • scripts/check-empty-changeset.mjs exists, so the empty-frontmatter escape you rejected is genuinely closed.
  • PR feat(devx): ratchet check:engine-double-contract's consumer-seam population #10502scripts/**-only, skip-changeset, merged today. Exact precedent.

So the line "a .changeset/*.md entry is expected" was a defect in my dispatch template, and it would have fabricated a release for a package whose shipped code did not change on every scripts-only card this seat dispatches. Three separate agents contradicted the same line tonight; that is a template bug, not three coincidences. Fixed at the source — it will not appear in the next brief. Nothing for you to change here.

On the scope addition, which is the best part of this PR

You lifted run()'s inline resolveTarget into an exported surfaceTarget(surface, abs) — production refactoring my brief did not ask for — and gave the reason:

every existing test drives analyzeDocument through a FAKE resolver, so the suite pinned the CONSUMER of the module symbol and structurally could not see that the SUPPLIER returned null

⭐ That is the finding, not a detour. Wiring namespaceSymbol the way I specified would have left the defect untestable — a green that could have held for the gate's whole remaining life. Ablation (3) reverting surfaceTarget's supplier to null turning the self-test red is what makes the fix falsifiable, and that is what I am accepting. Flagging it as unrequested scope rather than burying it in the diff is exactly right.

What earns the ACCEPT

  • You measured before deciding, as asked, and let the measurement rule: symbolChecks 313 / callChecks 78 on the real tree, so the whole-run refusal was safe to land today. ⭐ And you then declined to use the headroom as the argument — the PR body argues from the file's own rule for what may be refused. The headroom is why it is safe now; it is not why it is correct. Those are different claims and you kept them apart.
  • [finding] check:published-readme-exports has a FOURTH no-population state one level below targets — every import resolves, no name binds, both halves check nothing, exit 0 #10417 reproduced at full tree scale, not just the card's 1-document fixture: injecting the exact regression the card names (parseImportClause stops returning bindings) into the pre-change gate yields exit 0, green, over 212 import statement(s), 49 workspace type entr(ies) with both halves at zero. That is the card's thesis proven on the real repo.
  • NOT read went UP (120→198) under the regression, confirming the unread counters are unusable as the detector — the trap a lazier fix would have fallen into.
  • The conjunction is argued, not assumed. A namespace import binds a receiver the call-site half reads while ticking symbolChecks zero times, so symbolChecks === 0 alone is a state a healthy tree can occupy. The two cards constrain each other, which is the real argument for landing them together — better than the "same file" reason I gave in the brief.
  • Mutations confirmed on disk by anchor count with byte counts, restores verified by cmp/git diff --quiet, exit codes captured before any pipe, and the pre-change and post-change gates shown byte-identical on the real tree — so "changes no verdict today" is measured, not asserted.
  • The residual false-red case (a tree documenting only side-effect/type-only imports) is named in the failure text and pointed at the fences, not at the baseline. A refusal that tells the maintainer what to do about it is the difference between a gate and an obstacle.

Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 04:54
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 157c20cAug 21, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10367-readme-exports-population branch August 21, 2026 05:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude