Skip to content

fix(scripts): probe a second injection candidate in check:doc-examples - #8750

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-8743-doc-example-internal-symbol
Sep 9, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-8743-doc-example-internal-symbol

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8743

check:doc-examples was red on main. Reproduced first on origin/main (32f100867), unedited tree, dependency closure built — the harness's own controls are healthy, so the verdict is a fact about the corpus:

Controls:
  resolution   /home/user/objectui-8743/packages/types/dist/index.d.ts
  sentinel     importing a name no package exports produced 1 diagnostic(s) (TS2305)
  positive     importing a real export produced 0 diagnostic(s)
  src leaks    0
  injection    106 of 125 block(s) received the documented symbol's import; 4 documented symbol(s) are NOT on a public entry
               not on a public entry: @object-ui/data-objectstack MetadataCache
               not on a public entry: @object-ui/types safeValidateSchema
               not on a public entry: @object-ui/types stripImportedDefaults
               not on a public entry: @object-ui/types validateSchema

Examples: 125 block(s) — 34 compile, 91 fail, 90 of those declared in the ledger (90 row(s)).

UNDECLARED FAILURE  packages/types/src/zod/imported-defaults.ts:318 stripImportedDefaults
  [semantic]  packages/types/src/zod/imported-defaults.ts:319:28  TS2304: Cannot find name 'stripImportedDefaults'.

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. stripImportedDefaults is 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:

  • export it — widens a published surface, and moves @object-ui/types' public API, for a docs gate. Ruled out by the dispatch and I agree with the ruling.
  • declare it — a UNGATED_EXAMPLES row 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:

  1. the package's ROOT specifier, e.g. @object-ui/types;
  2. the built declaration of the symbol's own source file — the dist twin of packages/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.ts and 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

before (origin/main) after
blocks in the compiled tier 125 125
compile 34 35
fail 91 90
declared in the ledger 90 (90 rows) 90 (90 rows)
blocks receiving an injected import 106 107
symbols no probed specifier can import 4 1
exit 1 0

A ledger declaration would have held compile at 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 aa76ce98 to 8f8889f9):

GATE_EXIT=1
Examples: 125 block(s) — 34 compile, 91 fail, 90 of those declared in the ledger (90 row(s)).
UNDECLARED FAILURE  packages/types/src/zod/imported-defaults.ts:318 stripImportedDefaults
  [semantic]  ...imported-defaults.ts:319:50  TS2345: Argument of type 'string' is not assignable to parameter of type 'ZodType ...'

⚠️ The parameter type printed there is the generic form ZodType parameterised by unknown, unknown and $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 from packages/types/dist/zod/imported-defaults.d.ts, not against any and not skipped. Restore proven by blob hash back to aa76ce98 and an empty git diff HEAD.

Second leg, ablating the fix itself: reverting only scripts/check-doc-example-types.mjs to 32f100867 brings back exit 1 and the identical TS2304 Cannot find name 'stripImportedDefaults' at 319: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 main it 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 validateSchema and safeValidateSchema (zod/index.zod.ts) — on the published subpath @object-ui/types/zod, not the root. Their blocks import themselves, so alreadyImported withholds 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-objectstack builds with tsup and 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 that preludeFor injects the specifier the probe resolved, not the package name.
  • pnpm check:doc-snippets exit 0 (Covered blocks: 794 — 636 to compile, 158 declared fragment(s)), check:doc-example-readers exit 0, check:readme-exports exit 0 (population intact, not collapsed), check:control-bytes, check:doc-fences, check:comment-mask-corpus, check:unreferenced-sources, check:esm-specifiers all 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-major exit 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.mjs has no fragment mechanism at all — it hardcodes fragmentReason: null for every block it hands to the shared harness. Its only non-compiling route is the UNGATED_EXAMPLES ledger. 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_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

`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

Copy link
Copy Markdown
Collaborator Author

Contract review — accepted, flipped out of draft, auto-merge armed. This clears a red on main.

⭐ 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: check-doc-example-types.mjs probed the package root specifier and only that one, so a symbol a package exports to its own modules and nothing else can never be injected, and any @example spelling its own symbol's name is structurally TS2304. That reframes the defect from "this example is written wrong" to "this gate cannot check this class of example" — and it is why the fix belongs in the resolver rather than in the docblock.

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:

compile fail ledger rows
before 34 91 90
after 35 90 90

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 TS2345 … not assignable to parameter of type ZodType… — resolved from packages/types/dist/zod/imported-defaults.d.ts, so the block is judged against the built signature rather than against any or skipped. The second leg — revert only the script, get the identical TS2304 back and the withheld list back to 4 — closes it from the other side. Both restores proven by blob hash and empty git diff HEAD.

Two corrections to my dispatch, both accepted, and both were mine to get right:

  1. I quoted "Covered blocks: 794 — 636 to compile, 158 declared fragment(s)" as this gate's vocabulary. It is check:doc-snippets' output. check-doc-example-types.mjs has no fragment mechanism at all — it hardcodes fragmentReason: null and its only non-compiling route is the UNGATED_EXAMPLES ledger. The choice I posed did not exist; the real one was "compile vs ledger row". I conflated two gates.
  2. I passed the filing's "three more symbols sit in the same position" through as fact. The gate names four including this one, and the three siblings are not in one state — only one was ever a single edit from the same red.

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 (MetadataCache), it already fails for an unrelated free name and already carries a matching ledger row, so nothing is armed. Documented in the gate header is the right place for it; a card for a one-symbol documented limitation is the low-base-rate audit this seat has been declining all session. It becomes worth filing the day a bundled package grows an internal symbol whose example is otherwise clean.

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 main. Clearing it was mine to arrange.


Generated by Claude Code

Merged via the queue into main with commit 7427f69 Sep 9, 2026
34 checks passed
@os-justin
os-justin deleted the claude/issue-8743-doc-example-internal-symbol branch September 9, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check:doc-examples is red on main — undeclared @example failure at packages/types/src/zod/imported-defaults.ts:318

2 participants