Skip to content

fix(scripts): read the receivers a README builds, and print the blind spot that remains (#9870) - #10365

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9870-readme-exports-unresolvable-receivers
Aug 20, 2026
Merged

fix(scripts): read the receivers a README builds, and print the blind spot that remains (#9870)#10365
os-zhuang merged 1 commit into
mainfrom
claude/issue-9870-readme-exports-unresolvable-receivers

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9870

check:published-readme-exports could type a receiver only when the fence had
imported it. Its call-site half was therefore checking 8 call sites while
262 went unread — and the green line's 60 published document(s) read as
coverage of documents, when what it covered was the import-bound receivers
inside them.

The measurement, before any sweeping

Swept with the gate's ownpublishedDocs(), so the population is the gate's.

The card's numbers reproduce, and they have moved. At f9d7acf8 (the tree
PR #9868 measured, 11 minutes after this card was filed) the census lands on
235 call sites / 60 documents exactly, with driver-sql 27/6,
metadata 10/2 and driver-mongodb 10/2 matching the card's table row for row.
The definition that reproduces it: bare-identifier member calls inside code
fences whose receiver is neither import-bound from a workspace package nor
console/this. Two deltas worth recording:

  • On current main the count is 225, not 235. PR docs(driver-sql): rewrite the published README to the shipped driver surface (#9867) #9906 repaired
    driver-sql's README on 2026-08-19 — so the "1 proven fabricated" instance
    is no longer in the tree
    ; it was fixed, not found again. The honest statement
    of repo state today is 225 unmeasured, 0 proven-live.
  • runtime/README.md counts 47/18 against the card's 41/15 (mine is a
    superset), and affected documents 39 against 38. The headline reproduces; two
    rows differ by a definitional hair.

The two escape routes the card named are not the same size. Classifying all
262 by whether any widening could reach the receiver's type:

receiver provenancesitesreachable?
const x = new <imported class>(…)103yes — instance type
const x = [await] <imported fn>(…)6yes — awaited return type
free variable, never declared41no
global (console, Object, Date)36no
other initialiser shape20no
function parameter17no
member call on an unknown receiver13no
call-expression receiver (kernel.getService('mcp').registerTool(…))13no — 0 with an import-bound base
non-workspace import (z from zod), other13no

So the call-expression rule is deliberately not written. Every one of
those 13 is rooted in a free variable or a non-workspace import
(z.string(…).url(, res.status(…).json(). A rule for the shape would ship
scanning an empty population — the #4690 failure this gate's own header raises
to a hard error. It becomes writable the day a fence chains off an import-bound
base, against a population that exists.

What the widening surfaces — the number this card was gated on

1 finding in 70 newly-read call sites. Zero false positives.

packages/plugins/plugin-hono-server/README.md
line 31: documents `kernel.start(…)` on a value the fence builds with
`new ObjectKernel(…)`, but what ObjectKernel constructs has no `start` member.

Adjudicated by hand and real: ObjectKernel ships bootstrap() and
shutdown() and no start. It reads plausibly because the IKernel
interface does declare start() — but the concrete class the fence constructs
does not implement that name, and eight sibling READMEs spell the same step
await kernel.bootstrap(). Fixed here, with a changeset; the gate refuses its
own baseline as an author remedy, so a finding gets repaired or it does not ship.

No sweep was truncated: the widening's entire output is one site, and it is in
this PR.

beforeafter
call sites checked878
receivers typed233 import-bound233 + 46 built from one
call sites unread190120
findings01 (true positive)

⚠️ A vacuous first cut, caught by instrumentation and now pinned

The first version of this widening ran green while measuring nothing. The
derived binding never carried hasMemberVia, so a ?? (() => true) fallback
fired for all 70 sites it had just claimed to widen onto — the green line said
78 … call(s) checked and 70 of those checks were the fallback returning "not
knowable". Louder than before and measuring less: #4690 rebuilt inside the fix.
A findings-only assertion cannot see it, because the correct answer for the
vacuous case is also "no findings".

Two changes, so it cannot recur: an unanswerable receiver is now not readable
(it stays out of callChecks and lands in the NOT read count, where a
widening that widened nothing announces itself), and the self-test asserts the
counters for both an answering and a non-answering target.

Instrumented re-run of the fixed version, all 78 sites: 77 DECIDED:found,
1 DECIDED:missing, 0 bail-outs
— no site reaches a verdict through an
any / index-signature / overload escape hatch.

Why zero false positives

Every ambiguity in extractLocalBindingsrejects rather than guesses,
because the two failure directions are not symmetric: a wrong green leaves a
site unread (where all 262 already sit — no loss), while a wrong red accuses a
correct README on a merge-blocking gate whose baseline its own file refuses to
mute. Rejected: names declared twice from different sources, names with any
other initialiser shape, and names also introduced as a parameter, for/catch
binding, destructuring pattern or function/class declaration.

The green line now states its actual domain

This is the card's own preferred option (1), measured by the gate on every run
so it cannot drift:

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
(free variables, parameters, globals, non-workspace imports). Visibility, not a verdict — see #9870.

Visibility only — no new merge-blocking context, no exit code changes.

Verification

All at 4ce4cc392, the final commit; exit codes captured before any pipe.

Gate set derived with node scripts/pm/dispatch-gates.mjs (no args — it takes
its own change set from the merge base), all 13 green:
check:published-readme-exports (+ --self-test), check:changeset-gate-self-tests,
check:cross-package-test-inputs, check:objectui-changeset, check:slot-lookup,
check:test-source-alias, check:type-source-resolution, check:nul-bytes,
check-adr-0087-registration, check-changeset-no-major, check-empty-changeset,
docs-audit/check-affected-docs. Plus eslint --no-inline-config on the changed
script.

Four ablations, each confirmed on disk before running and restored
byte-identically
(git diff --quiet HEAD verified on every restore leg):

ablationmutated legrestore leg
bootstrap()start() in the real READMEgate exit 1, naming the siteexit 0
extractLocalBindings binds nothingself-test exit 1, 5 pins fireexit 0
reinstate the () => true fallbackself-test exit 1, only the vacuity pin firesexit 0
drop the NOT read: clauseself-test exit 1, 2 pins fireexit 0

The zero-hit has its positive control: ablation 1 is the same command that
reports "no violations", shown firing on the real tree.

Scope

Clause ②: the path limb is packages/spec/src/** and this is not that. On
the content limb my reading is not triggered: no product contract's
accept/reject behaviour changes and no public surface widens. What widens is a
CI recognizer's own accept/reject; the only product-visible change is a
one-word README repair.

#9911 is untouched, as instructed — the gate still exits 0 on
targets.size === 0. Noted for whoever takes it: this PR changes what the
population is
, the quantity that refusal is about. Specifically, targets
(workspace type entries reached by imports) is unaffected — this widening adds
no import statements and no new type entries, so #9911's targets.size === 0
condition is neither easier nor harder to trigger. What did change is
measured.callChecks, which now has a non-vacuous floor: derivedReceivers and
the NOT read pair give #9911 two more quantities that a no-population run
would zero, if its author wants them.


Generated by Claude Code

… spot that remains (#9870)
`check:published-readme-exports` typed a receiver only when the fence had
imported it, so its call-site half was checking 8 call sites while 262 across
39 of 60 published documents went unread — and the green line's `60 published
document(s)` read as coverage of documents rather than of the import-bound
receivers inside them.
Measured with the gate's own `publishedDocs()`, the two escape routes the card
named are not the same size: 109 of the 251 never-import-bound receivers are
BUILT from a name the fence did import (`const kernel = new ObjectKernel()`,
`const stack = await bootStack(app)`), while call-expression receivers have a
population of zero with an import-bound base — so the first is implemented and
the second deliberately is not, rather than shipping a rule that scans nothing.
Call-site half: 8 → 78 checks. One finding in the 70 newly-read sites, and it
is real: plugin-hono-server's README booted a kernel with `await kernel.start()`
where `ObjectKernel` ships `bootstrap()`. Zero false positives — every ambiguity
in `extractLocalBindings` rejects rather than guesses, because a wrong green
leaves a site unread while a wrong red accuses a correct README on a
merge-blocking gate whose baseline refuses to mute it.
The green line now also states what it could NOT read (120 calls on 72
receivers), marked as visibility rather than a verdict.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-hono-server/README.md) — pages documenting those are invisible to this run

Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9a1ed7a09ea3bf491c06929e73224bde92282340packageMentionDocs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Aug 20, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 20, 2026 16:58
@os-zhuang
os-zhuang enabled auto-merge August 20, 2026 16:58
@os-zhuang
os-zhuang added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit b03a880Aug 20, 2026
37 of 39 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9870-readme-exports-unresolvable-receivers branch August 20, 2026 17:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/ltooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check:published-readme-exports cannot read 235 call sites across 38 of 60 published docs — 1 proven fabricated, 234 unmeasured

1 participant

@os-zhuang