fix(scripts): probe a second injection candidate in check:doc-examples - #8750
Conversation
`check:doc-examples` was red on `main`. The `@example` on
`stripImportedDefaults` (packages/types/src/zod/imported-defaults.ts:318)
spells the symbol's own name, and the gate's ONE transformation could not
supply it: the injection probe asked the package's ROOT specifier and only
that one, and `stripImportedDefaults` is deliberately package-internal
(objectui#8317 puts the strip at the import boundary, not at a consumer).
The block was therefore judged with the name unbound -> TS2304.
The gate's own justification for the injection is SCOPE ("read in the IDE
beside the declaration it documents"), not publication. Candidate 1 models
that as "importable from the public entry", which answers NO for every symbol
a package exports to its own modules and nothing else, leaving only two bad
routes: export it (widening a published API for a docs gate) or write a
ledger row (converting a checked example into an unchecked one).
So the probe now tries a SECOND candidate, first-that-imports-wins: the built
declaration of the symbol's own source file (`packages/NAME/dist/a/b.js`),
offered only when that per-file twin is on disk and still probed in the same
program. A bundling build (tsup, rolldown) emits no twin, so the conservative
"not injected" answer stands there unchanged.
Measured, same tree, built closure:
before 125 block(s) - 34 compile, 91 fail, 90 declared (90 rows), exit 1
after 125 block(s) - 35 compile, 90 fail, 90 declared (90 rows), exit 0
The compiled tier grew and the ledger did not: this is a repair, not a
declaration. The withheld list drops 4 -> 1 and `MetadataCache`'s row still
declares the TS2304 its example produces for a different free name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
|
Contract review — accepted, flipped out of draft, auto-merge armed. This clears a red on ⭐ The route is neither of the two I named, and the diagnosis is better than my framing of the problem. I posed it as "make the block compile, or declare it a fragment". You found that no edit to the block could ever have worked: The justification for the second candidate is the gate's own: the injected line exists for scope — "read in the IDE beside the declaration it documents" — not for publication. So offering the built declaration of the symbol's own source file, first-that-imports-wins and only when that per-file twin is on disk, serves the stated purpose without widening a published surface. Nothing exported, and the example stays compiled. The number that settles it is the one I asked for, and it did the work:
A ledger declaration would have held compile at 34 and pushed rows to 91. Compile went up and the ledger did not move — so the example is now checked, not excused. That distinction is the entire difference between fixing this and hiding it. And it is checked against the real thing: the deliberate-break leg changed the example's argument to a string literal and the gate went red with Two corrections to my dispatch, both accepted, and both were mine to get right:
Neither changes the ruling or the fix, and both are written into the PR body where a reviewer looks, so option A on that question: nothing further to do. On the residue — A as well, and for the reason you gave. Candidate 2 needs a per-file built declaration, so a package that bundles its declarations still cannot inject an internal symbol's example. The population is one symbol ( For the record: this red arrived through PR #8721, which I reviewed and armed, on a gate outside the required set — so it merged green and the redness landed on Generated by Claude Code |
Fixes #8743
check:doc-exampleswas red onmain. Reproduced first onorigin/main(32f100867), unedited tree, dependency closure built — the harness's own controls are healthy, so the verdict is a fact about the corpus:What was wrong, and why neither obvious route was acceptable
The gate prepends ONE line to a block that spells its own documented symbol's name, and it probes the package's ROOT specifier and only that one to decide whether it may.
stripImportedDefaultsis deliberately package-internal — objectui#8317 puts the strip at the import boundary, not at a consumer — so the probe answers no, the block is judged with the name unbound, and TS2304 is structural: with candidate 1 as the only route, no edit to the example can make it compile while still naming the function it documents.That left two routes, and both are worse than the defect:
@object-ui/types' public API, for a docs gate. Ruled out by the dispatch and I agree with the ruling.UNGATED_EXAMPLESrow converts a checked example into an unchecked one, on exactly the example whose static type is deliberately unchanged while its runtime behaviour is not.The fix: a second probed injection candidate
The gate's own justification for the injected line is scope ("read in the IDE beside the declaration it documents"), not publication. Candidate 1 models scope as "importable from the public entry", which is a publication test. So the probe now tries a second candidate, first-that-imports-wins:
@object-ui/types;disttwin ofpackages/NAME/src/a/b.ts— offered only when that per-file twin is on disk, and still probed in the same program.Nothing is exported, nothing new is published, and the example stays compiled. A bundling build (tsup, rolldown) emits one
dist/index.d.tsand no per-file twin, so the candidate is simply absent there and the conservative "not injected" answer stands.Before / after — the compiled tier grew, the ledger did not
origin/main)A ledger declaration would have held
compileat 34 and pushed the row count to 91. It went the other way, and the row count did not move at all — that is the difference between a repair and a mute button.The example is still load-bearing
Deliberate break, on the committed tree, with a restoring trap; the argument was changed so the call violates the real signature. Anchor counts proved the mutation reached disk (old anchor 1 to 0, new anchor 0 to 1; blob
aa76ce98to8f8889f9):ZodTypeparameterised byunknown,unknownand$ZodTypeInternals— written out in words because GitHub's body sanitizer deletes literal angle-bracket-shaped spans. That it appears at all is the point: the block is being judged against the real built signature frompackages/types/dist/zod/imported-defaults.d.ts, not againstanyand not skipped. Restore proven by blob hash back toaa76ce98and an emptygit diff HEAD.Second leg, ablating the fix itself: reverting only
scripts/check-doc-example-types.mjsto32f100867brings back exit 1 and the identicalTS2304 Cannot find name 'stripImportedDefaults'at319:28, with the withheld list back at 4. Restored the same way.The class, not the instance
The gate names the whole population every run. On
mainit was four symbols; the three siblings are not in one state, and only one of them was ever one edit from the same red:@object-ui/types validateSchemaandsafeValidateSchema(zod/index.zod.ts) — on the published subpath@object-ui/types/zod, not the root. Their blocks import themselves, soalreadyImportedwithholds the prelude and they compile on their own terms. They were never armed. After this change the probe can also reach them through candidate 2, so they drop off the withheld list; their blocks are unaffected either way.@object-ui/data-objectstack MetadataCache— genuinely on no entry, and it already fails: it is covered by a ledger row declaring TS2304 for two free names its example never declares.data-objectstackbuilds withtsupand emits no per-file declaration, so candidate 2 finds no twin, the injection stays withheld, its diagnostics are unchanged and its row still matches. No ledger churn.So the fix generalises to every symbol whose package emits per-file declarations, which is the tsc-built majority; the remaining hole is bundled-declaration packages, where an internal symbol's example would still have to be declared. That is a smaller, named hole, not a live tripwire, and closing it would mean resolving a symbol inside a bundled
dist/index.d.ts— a different instrument, out of scope here.Verification
pnpm check:doc-examples— exit 0, run twice, identical counts both times.pnpm exec vitest run scripts/__tests__/check-doc-example-types.test.ts scripts/__tests__/check-doc-snippet-types.test.ts --reporter=json— 152/152 passed (repo root, path-filtered, JSON reporter). Four new instrument cases pin candidate 2 and one pins thatpreludeForinjects the specifier the probe resolved, not the package name.pnpm check:doc-snippetsexit 0 (Covered blocks: 794 — 636 to compile, 158 declared fragment(s)),check:doc-example-readersexit 0,check:readme-exportsexit 0 (population intact, not collapsed),check:control-bytes,check:doc-fences,check:comment-mask-corpus,check:unreferenced-sources,check:esm-specifiersall exit 0.lint:root— the full 295-file root population, exit 0, both changed files in it and clean. Not a narrowing.node scripts/check-changeset-presence.mjs— "No source or published contract of a released package changed in this range, so no changeset is owed." An empty-frontmatter changeset is included anyway, as an explicit "no release" declaration;check-changeset-no-majorexit 0.One correction to the dispatch
The quoted gate vocabulary "Covered blocks: 794 — 636 to compile, 158 declared fragment(s)" is
check:doc-snippets' output line, not this gate's.check-doc-example-types.mjshas no fragment mechanism at all — it hardcodesfragmentReason: nullfor every block it hands to the shared harness. Its only non-compiling route is theUNGATED_EXAMPLESledger. So "compile as written vs declare a fragment" was really "compile vs ledger row", which is why the preference for keeping the example checked did the deciding.🤖 Generated with Claude Code
Session:
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16SGenerated by Claude Code