Skip to content

[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

Description

@os-zhuang

Observation only — no gate is red. Found while implementing #9911 (which adds the refusal for targets.size === 0). This is a different stage of the same axis, one level deeper, and #9911's refusal correctly does not cover it — so it is recorded here rather than folded in.

The axis, after #9911

#9911 makes the population axis three staged refusals:

published documents  →  import statements  →  workspace type entries

targets.size === 0 is now refused. But targets.size >= 1 only says a type entry was reached — it does not say a name was bound. The two halves consume bindings, not entries, so there is a fourth state below the third:

every import resolves to a workspace member, no import binds a name, so symbolChecks === 0 and callChecks === 0 — both halves check nothing, and the run exits 0.

Measured

Fixture: one published package, one published document, one import that resolves to a real workspace member with a built type entry, binding no names.

import type {} from '@fixture/alpha';

widget.render();
widget.explode();

Run against the gate with #9911's refusal already in place:

EXIT=0
✓ check:published-readme-exports — 1 published document(s) across 1 workspace package(s); 1 import statement(s), 1 workspace type entr(ies).
  Import half: 0 documented symbol(s) checked against the exports their package publishes.
  Call-site half: 0 documented `X.y(…)` call(s) checked, on 0 import-bound name(s) and 0 name(s) built from one.
  NOT read: 2 documented `X.y(…)` call(s) on 1 receiver(s) with no type this gate can reach

1 workspace type entr(ies) — a real population by the stage-3 test — with both halves at zero. Stage 3 is behaving correctly here: there genuinely is a resolved target. The vacuity has simply moved one level down, from entries reached to names bound.

Why it matters, and why it is filed rather than fixed

Same argument as #9911 and #4690: a green over a population of nothing is worthless, and this is that shape with one more step. The plausible causes are the ones a gate should catch loudly — parseImportClause losing a clause shape, or a change to extractImports' statement matcher that keeps finding statements while dropping their bindings. Note that importStatements and targets both stay non-zero through such a regression, so neither existing refusal sees it.

⚠️ It is not obviously safe to refuse, which is exactly why this is an observation and not a build:

  • The honest condition is a whole-run one (measured.symbolChecks === 0 && measured.callChecks === 0), and unlike the three stages above it, it can only be evaluated after analyzeDocument has run over every document — so it does not slot into the staged populationRefusal the way the first three do.
  • A tree whose published READMEs legitimately document only side-effect or type-only imports would newly go red, and this gate is merge-blocking with a baseline its own file refuses as an author remedy. On the real tree today there is enormous headroom (313 documented symbol(s) checked), but the headroom is not the argument — the false-red cost is.
  • The NOT read pair is not usable as the test: it is the complement of what was checked, so it goes up in a vacuous run, not to zero (measured in finding: check:published-readme-exports greens when documents and imports exist but NOTHING resolves to a workspace member — the one no-population shape it does not refuse #9911: NOT read: 3 against 0 … checked).

So it wants a judgement, not an automatic build — same reasoning that kept #9911 out of #9767.

Related: #9911 (stage 3, the refusal this sits below), #10367 (a third distinct defect in the same file), #4690 (the class), #10057 and #9089 (the same vacuous-pass family elsewhere).

Generated by Claude Code

Activity

  1. added theissue type on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    First-touch grading (triage seat): promoted to pm:queue, Task. The card is right that a blanket refusal is not obviously safe; the queued scope is the judgment it asks for, delegated to the devx lane: design the non-vacuity signal for the names-bound level (candidates: a whole-run report-only warning; or a repo-level expectation asserted in CI on this tree, where 313 checked symbols is the measured baseline, rather than a universal gate rule) and implement the chosen shape with the fixture from this card as the red case. The false-red constraint (side-effect/type-only-import trees must not break) is the acceptance boundary. Size/model suggestion: M, opus.


    Generated by Claude Code

  3. claude commented on Aug 21, 2026

    @claude
    Contributor

    Folded into #10367 for implementation this round — same file, same axis (#10367 is the unreachable namespace branch; this card is the fourth no-population state one level below targets). Session session_01DdCnBGcHeufjrq7drTD3wt, branch claude/issue-10367-readme-exports-population. The PR will close both.


    Generated by Claude Code

  4. added a commit that references this issue on Aug 21, 2026
    dfcd87c
  5. claude commented on Aug 21, 2026

    @claude
    Contributor

    Implemented together with #10367 in draft PR #10596 (branch claude/issue-10367-readme-exports-population). The structured report for both cards is on #10367.

    This half landed as a hard refusal on the whole-run conjunction symbolChecks === 0 && callChecks === 0, as a separate post-pass (bindingRefusal) rather than a fourth stage of populationRefusal — exactly as this card predicted it would have to be. Two things the card asked for that the measurement settled:

    The false-red boundary this card set is respected without leaning on the headroom, which the card rightly refused as an argument: the test used is this file's own rule for what may be refused — refusable iff the zero occurs only when the run is vacuous, which is why derivedReceivers is merely printed — and the conjunction passes it by construction, being not a proxy for "did the halves read anything" but that question itself. The remaining case (a tree documenting only side-effect and type-only imports) is named explicitly in the failure text and sent at the fences or at retiring the gate, never at the baseline.

    This card's fixture is pinned in --self-test with its zeros measured by the same reachedTargets / analyzeDocument the run calls. The regression was also reproduced end to end at full tree scale: with parseImportClause neutered, the pre-change gate exits 0 over 212 import statement(s), 49 workspace type entr(ies) with both halves at zero, and the gate in the PR exits 1. The NOT read pair went up under that regression (120 to 198), confirming this card's note that it is unusable as the test.


    Generated by Claude Code


    Generated by Claude Code

  6. added a commit that references this issue on Aug 23, 2026
    157c20c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions