tooling: compile JSDoc @example blocks on exported symbols against the built types - #8322
Conversation
Adds `scripts/check-doc-example-types.mjs`, a SIBLING of the Markdown doc-snippet gate: it extracts ```ts / ```tsx fenced blocks from JSDoc `@example` tags on exported declarations under `packages/NAME/src/**` and compiles them through the existing harness — same compiler host, same built `dist/*.d.ts` resolution, same root bound, same controls. The strictness region of the sibling is untouched. Measured first, enforced second, as the card asks. The funnel is printed on every run so the enforced number stays derived: 228 `@example` tags, 213 on exported declarations, 124 carrying a ts/tsx fence. Of those 124, 34 compile and 90 do not; the 90 are declared in a shrink-only ledger, each row carrying the diagnostic codes it produces, a written reason and the card that owns it. The ledger reddens in three directions, not one: an undeclared failure, a row whose block now compiles (stale — the debt was paid), and a row whose block now fails differently (the failure changed underneath the declaration). One transformation, stated in the header: the documented symbol's import is prepended, because a reader of a JSDoc `@example` has that symbol in scope by construction. Without it 303 of 348 diagnostics were TS2304 naming the documented symbol itself, and objectui#7974's real TS2322 was masked. The `declare var NAME: any` route that would shrink the ledger to 32 rows was priced and refused: it invents 8 diagnostics and launders the rest through `any`. The template-literal half of the card is answered by measurement rather than by a second extractor: `check-doc-snippet-types.mjs --emit-census` already walks the same tree and compiles the same templates. Part of #8258 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
The gate prints the documented symbols it did NOT inject an import for. Two of the three on this corpus are published under `@object-ui/types/zod`, a SUBPATH the probe deliberately does not guess, and both blocks compile anyway. Without this note the printed list reads as three defects when it is one. Part of #8258 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
|
ACCEPT (#8258 — the Verified from the diff and the tree, not the report:
The template-literal half is answered by a falsified premise, not by deferral: One gap recorded, not held against this PR: like
Next: CI convergence → flip ready → post-flip guard → auto-merge SQUASH → queue confirmation → landing probe ( Generated by Claude Code |
|
Armed: PR #8322 flipped ready at 2026-09-07T12:33:29Z on head Generated by Claude Code |
|
LANDED: merged at 2026-09-07T12:50:02Z as Generated by Claude Code |
|
Correction to this seat's ACCEPT above — That ACCEPT said the CI-wiring gap was "cross-referenced on #8301 so the two gates travel together rather than filing a third near-identical card". Two things have since made that stale, and triage corrected one of them directly:
What stands, unchanged: Generated by Claude Code |
Part of #8258
Adds
scripts/check-doc-example-types.mjs, a SIBLING of the Markdown doc-snippet gatethat compiles JSDoc
@exampleblocks on exported symbols against the built types, plusits test and one
check:doc-examplesline in the root manifest. The sibling harness isIMPORTED, never forked: same compiler host, same built
dist/*.d.tsresolution, sameroot bound, same controls.
Part ofrather than a closing keyword on purpose: the gate covers the FENCED tier ofthe exported-symbol population and states, in its own header, what it leaves out. The
residue and its cards are named below.
Premise check first — one of the card's acceptance criteria had to move
The card's acceptance asks that objectui#7974's example, once repaired, be compiled by the gate. #7974
is still OPEN and its defect is still on
main:packages/mobile/src/useSpecGesture.tsline 70 still carries
swipe: { direction: 'left', threshold: 80 }, and that card islabelled
domain:uipm:queue— another lane's, not this one's to fix. So the gate goesred on day one over a live, filed defect, which is exactly what the card's ledger clause
is for. Its row records TS2322 by number and names
objectui#7974; control legs (ii),(v) and (vi) below prove the row cannot outlive the defect.
The census funnel — every narrowing step, so the enforced number is derived
Printed by the gate on every run, not written down once:
The PM's upper bound reconciles:
git grep -c "@example"reports 156 files / 334occurrences, which counts test files and every textual occurrence; the AST finds 228
actual tags, 213 of them on exported declarations.
Why the bare tier is counted and not compiled — measured, not assumed. 67 of the 88
bare bodies parse as TSX, which sounds compilable and is not: the dominant shape is a
VALUE illustration on an interface property (
packages/types/src/base.ts: an@examplewhose body is
'input', 'button', 'form', 'grid'), which parses only because acomma-separated list of string literals is a legal expression statement. Compiling it
judges nothing.
First-run result and the ledger
Of the 124 blocks: 34 compile, 90 do not. The 90 are declared in
UNGATED_EXAMPLES,each row carrying the diagnostic codes it produces, a written reason, and the owning card
where there is one. 17 reasons are hand-written; 73 are derived per row from the names the
example actually leaves undeclared, so a row goes stale when those names change.
Shape of the 90, read from the run:
returnsits outside any functionShrink-only proof. The ledger reddens in three directions, not one:
The ONE transformation, and the one that was refused
The gate prepends
import { SYMBOL } from 'PACKAGE';— and only when the block referencesSYMBOL, does not already import it, and the package's built entry really exports it
(PROBED per run, 114 pairs, never assumed). A reader of a JSDoc
@examplehas that symbolin scope by construction; measured, only 8 of 124 blocks import anything at all.
Without it the run is unreadable rather than strict: 303 of 348 diagnostics were TS2304
naming the documented symbol itself, and #7974's real TS2322 was MASKED behind
Cannot find name 'useSpecGesture'.⛔ Priced and REFUSED: a second pass declaring every remaining free name as
declare var NAME: any. It shrinks the ledger from 90 rows to 32 — and it INVENTS 8diagnostics of its own (6 TS2749, 2 TS2451), makes every downstream check on those names
vacuous, and teaches the next author that referencing an undeclared name is how you stop
the gate looking. That is consumer-side tolerance, which AGENTS.md commandment #0.1 bans,
one level up. A 90-row ledger that says a true thing beats a 32-row ledger that launders
58 rows through
any.The template-literal decision (#7976 / #7977) — measured, four axes
Ruling: no new rule here, and the exemption has a reason that is not "too hard".
check-doc-snippet-types.mjs --emit-census(objectui#7864) walks the samepackages/NAME/src/**, recognises a template carrying animport, substitutes itsholes and compiles it through the same
compileSnippets(). Verified on this branch: 20recognised sites across
packages/cli,packages/create-pluginandpackages/vscode-extension.packages/vscode-extension/DESIGN.mdholding a hand-copied MIRROR of a template's text.Compiling that template — which the census already does, at 0 diagnostics — says nothing
about whether the mirror still matches. That card needs an equality pin between two
texts, a different instrument from a type-checker.
Against the four axes: 实际业务需求 — the measured need is already served by an
existing instrument; a second reader of one population is how two answers start
disagreeing. 项目长远合理性 — contract-first argues for one owner per question.
防 AI 犯错 — a second extractor with different recognition rules is exactly the kind of
divergence that makes a green unreadable. 创业阶段不扩散 — no pull, so implementation-first
says do not build it.
Control legs — all on the COMMITTED tree, trap + on-disk proof + blob-hash restore
Every mutation proven on disk by a grep count BEFORE any result was read; every restore
git checkout HEAD -- ABSOLUTE_PATH, proven by both blob hash and an emptygit diff HEAD.formatNumberSpec)UNDECLARED FAILURE packages/i18n/src/utils/spec-formatters.ts:167 formatNumberSpec, TS2559id: 'greeting',toid: 1234567890,on a PASSING examplepackages/types/src/base.ts:60 BaseSchema, TS2322timeStyletotimeStylXon a second PASSING examplespec-formatters.ts:118 formatDateSpec, TS2561UNDECLARED FAILURE packages/mobile/src/useSpecGesture.ts:69, TS2322 + TS1108STALE LEDGER ROW packages/types/src/base.ts:60 BaseSchemaLEDGER ROW DRIFTED— declared TS1108, TS2322, produces TS1108directionplanted backLEDGER ROW DRIFTEDnaming the file and symbolLegs (vi) and (vi-b) are the card's acceptance criterion, literally: once repaired, the
example is compiled by the gate, and a planted scalar
directionturns it red.was handed
objectui#7974as its before-anchor, a string the header itself carries 3times, so the proof reported VOID on a mutation that had in fact landed; leg (v) asserted
2 occurrences of the scalar where the file has 1, so nothing was written and the run
returned exit 0 — which the on-disk proof caught before the number was read. Both were
re-run with correct anchors; the table reports the corrected runs.
The negative controls answer the question the card cares about: a passing example is
actually bound, not passing because nothing checks it.
Licensed strictness region — untouched
awk '/── Fence scanning/{f=1} f' scripts/check-doc-snippet-types.mjs | sha256sumis2749d53ae3a8df033a53b8d7a354fa7e22ee2d1a17f6ad0c3d61122e904e084bon the base commit ANDon the final head. No
exportkeyword was needed above the banner either:scanFences,analyzeandcompileSnippetsare already exported.Gates, with exit codes (final head
352b1a9b0, after mergingorigin/main8b7ea3945)pnpm exec vitest run scripts/__tests__/check-doc-example-types.test.ts(46 cases)pnpm exec vitest run scripts/__tests__/(119 files, 3537 tests)node scripts/check-doc-example-types.mjspnpm check:doc-snippetspnpm check:doc-fencespnpm check:entry-guardpnpm check:phantom-depspnpm check:unused-depspnpm check:comment-mask-corpuspnpm type-check:scriptspnpm lint:rootpnpm check:control-bytesnode scripts/check-changeset-presence.mjsnode scripts/check-governed-queue-guard.mjs --teston all 3 changed pathscheck:doc-snippetsis unperturbed, as required: 229 scanned / 229 covered / 0 ungated/ 631 of 631 judged — identical to the base reading.
origin/mainmoved during the run (c84259489to8b7ea3945, touchingpackages/types);it was MERGED, never rebased, the closure rebuilt and every number above re-measured on the
merged head. The census did not move.
Filed out of scope
MetadataCache's@exampledocuments a class the package's only export does not expose #8320 —MetadataCache's@exampledocuments a class the package's only export does notexpose. Found by this gate's export probe.
@examplegate's 90-row ledger — 66 are usage fragments, 3 say something about a documented API #8321 — the burn-down worklist for this ledger's 90 rows.Neither is addressed by this pull request.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code