Skip to content

tooling: compile JSDoc @example blocks on exported symbols against the built types - #8322

Merged
baozhoutao merged 3 commits into
mainfrom
claude/issue-8258-jsdoc-example-gate
Sep 7, 2026
Merged

tooling: compile JSDoc @example blocks on exported symbols against the built types#8322
baozhoutao merged 3 commits into
mainfrom
claude/issue-8258-jsdoc-example-gate

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Part of #8258

Adds scripts/check-doc-example-types.mjs, a SIBLING of the Markdown doc-snippet gate
that compiles JSDoc @example blocks on exported symbols against the built types, plus
its test and one check:doc-examples line in the root manifest. The sibling harness is
IMPORTED, never forked: same compiler host, same built dist/*.d.ts resolution, same
root bound, same controls.

Part of rather than a closing keyword on purpose: the gate covers the FENCED tier of
the 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.ts
line 70 still carries swipe: { direction: 'left', threshold: 80 }, and that card is
labelled domain:ui pm:queue — another lane's, not this one's to fix. So the gate goes
red 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:

   1418  source files under packages/NAME/src (.ts/.tsx)
   2421    ... tooling files excluded (tests, mocks, benchmarks, stories)
    228  `@example` tags found by the AST
      0    ... in a tooling file
     15    ... on a declaration that is not exported (or has no name)
    213  `@example` tags on EXPORTED declarations
    124    ... carrying a ts/tsx/typescript fence
      1    ... carrying only a non-ts fence
     88    ... BARE, no fence — counted, never compiled
    124  BLOCKS in the compiled tier

The PM's upper bound reconciles: git grep -c "@example" reports 156 files / 334
occurrences, 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 @example
whose body is 'input', 'button', 'form', 'grid'), which parses only because a
comma-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:

shape rows
usage fragment naming undeclared ambient names ~66
hook-body excerpt whose return sits outside any function 7
does not parse as TSX (prose-and-code mixture) 5
shorthand property standing for caller-supplied context 4
says something about the documented API 3

Shrink-only proof. The ledger reddens in three directions, not one:

state verdict
block fails, row's codes match declared debt, exempt
block fails, no row RED — a new example must compile
block COMPILES, row present RED, stale — the debt was paid, delete the row
block fails with DIFFERENT codes RED — re-derive the row
row names a block that is gone RED, stale

The ONE transformation, and the one that was refused

The gate prepends import { SYMBOL } from 'PACKAGE'; — and only when the block references
SYMBOL, 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 @example has that symbol
in 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 8
diagnostics 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".

  • The extraction the card imagines already exists and already runs on this exact tree:
    check-doc-snippet-types.mjs --emit-census (objectui#7864) walks the same
    packages/NAME/src/**, recognises a template carrying an import, substitutes its
    holes and compiles it through the same compileSnippets(). Verified on this branch: 20
    recognised sites across packages/cli, packages/create-plugin and
    packages/vscode-extension.
  • objectui#7977 is no longer open; objectui#8112 corrected its prose.
  • finding(vscode-extension): nothing binds DESIGN.md's hand-copied Export-to-React preamble to the generator it mirrors #7976's residue is not a template-parsing problem. It is
    packages/vscode-extension/DESIGN.md holding 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 empty git diff HEAD.

leg mutation expected observed
(iv) baseline none GREEN exit 0
(i) positive scalar planted where an OBJECT is required, in a GREEN example (formatNumberSpec) RED naming file+symbol exit 1, UNDECLARED FAILURE packages/i18n/src/utils/spec-formatters.ts:167 formatNumberSpec, TS2559
negative A length-preserving id: 'greeting', to id: 1234567890, on a PASSING example RED — proves it is BOUND exit 1, packages/types/src/base.ts:60 BaseSchema, TS2322
negative B length-preserving timeStyle to timeStylX on a second PASSING example RED — proves it is BOUND exit 1, spec-formatters.ts:118 formatDateSpec, TS2561
(ii) positive objectui#7974's ledger row DELETED while its defect exists RED exit 1, UNDECLARED FAILURE packages/mobile/src/useSpecGesture.ts:69, TS2322 + TS1108
(iii) positive a row planted for a block that COMPILES RED, stale exit 1, STALE LEDGER ROW packages/types/src/base.ts:60 BaseSchema
(v) objectui#7974's example repaired, row left alone row must not survive exit 1, LEDGER ROW DRIFTED — declared TS1108, TS2322, produces TS1108
(vi) example repaired AND row re-derived GREEN exit 0
(vi-b) scalar direction planted back RED exit 1, LEDGER ROW DRIFTED naming the file and symbol

Legs (vi) and (vi-b) are the card's acceptance criterion, literally: once repaired, the
example is compiled by the gate, and a planted scalar direction turns it red.

⚠️ Two legs read VOID on their first attempt and were re-run rather than accepted. Leg (ii)
was handed objectui#7974 as its before-anchor, a string the header itself carries 3
times, 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 | sha256sum is
2749d53ae3a8df033a53b8d7a354fa7e22ee2d1a17f6ad0c3d61122e904e084b on the base commit AND
on the final head. No export keyword was needed above the banner either: scanFences,
analyze and compileSnippets are already exported.

Gates, with exit codes (final head 352b1a9b0, after merging origin/main 8b7ea3945)

gate exit
pnpm exec vitest run scripts/__tests__/check-doc-example-types.test.ts (46 cases) 0
pnpm exec vitest run scripts/__tests__/ (119 files, 3537 tests) 0
node scripts/check-doc-example-types.mjs 0
pnpm check:doc-snippets 0
pnpm check:doc-fences 0
pnpm check:entry-guard 0
pnpm check:phantom-deps 0
pnpm check:unused-deps 0
pnpm check:comment-mask-corpus 0
pnpm type-check:scripts 0
pnpm lint:root 0 (0 errors, 32 pre-existing warnings, none from these files)
pnpm check:control-bytes 0
node scripts/check-changeset-presence.mjs 0 — no changeset owed
node scripts/check-governed-queue-guard.mjs --test on all 3 changed paths 0 — NOT GOVERNED

check:doc-snippets is unperturbed, as required: 229 scanned / 229 covered / 0 ungated
/ 631 of 631 judged — identical to the base reading.

origin/main moved during the run (c84259489 to 8b7ea3945, touching packages/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

Neither is addressed by this pull request.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

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
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT (#8258 — the @example gate, measurement first) — reviewed on the tree at head 352b1a9b0 against origin/main 8a304c811, read 2026-09-07T12:21Z; domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr. Merges clean.

Verified from the diff and the tree, not the report:

  • Three files, additions only: the new gate (1,349 lines), its test (390 lines, 46 cases), one check:doc-examples line. scripts/check-doc-snippet-types.mjs is NOT in the diff at all — the licensed strictness region is untouched by construction, and no new export was needed.
  • The ledger reddens in the three directions the brief required, and the code carries each as a named reason: undeclared-failure, stale-ledger-row, ledger-row-drifted (a row whose stored diagnostic CODES no longer match). 90 rows; the useSpecGesture row names objectui#7974 and its header states that the row goes stale the moment that lane repairs the example.
  • The test judges the REAL tree, not only fixtures: describe('this repository') and describe('the real ledger') mean the gate is enforced today through vitest in CI, which is what makes the ledger binding before any workflow step exists.
  • Arms A/B/D were measured before one was chosen, and D was refused for the right reason: declaring free names any would invent diagnostics and teach that an undeclared name stops the gate looking — consumer-side tolerance. Arm B is the one that makes objectui#7974's real TS2322 visible instead of masked behind 303 TS2304s.
  • Negative controls on two DIFFERENT passing examples both went red (TS2322, TS2561), so the 34 green examples are green because they are checked.
  • Two control legs read VOID on their first attempt and were re-run rather than accepted — the on-disk proof caught a mutation that had not landed. That is the discipline working, and it is why the numbers here are trustworthy.

The template-literal half is answered by a falsified premise, not by deferral: --emit-census (objectui#7864) already walks packages/*/src, recognises a template carrying an import and compiles it through the same harness — verified, 20 recognised sites. #7977 is closed (its prose was corrected by #8112); #7976's residue is a hand-copied MIRROR of a template, which needs an equality pin between two texts, a different instrument, on that card. Correct call: no second extractor.

One gap recorded, not held against this PR: like check:unused-deps before it, check:doc-examples has no workflow step of its own while its siblings (doc-snippet-types.yml, doc-component-types.yml, doc-fence-languages.yml) each have one. It is enforced through the pin test meanwhile. Cross-referenced on #8301 so the two gates travel together rather than filing a third near-identical card.

Part of #8258 is the right first line: the fenced tier is gated with a declared ledger, the 88 bare bodies are counted-and-printed with a measured reason, and the residue's burn-down is #8321.

Next: CI convergence → flip ready → post-flip guard → auto-merge SQUASH → queue confirmation → landing probe (check:doc-examples line and the 90-row ledger on re-fetched origin/main; control: check:doc-snippets numbers unchanged at 229/229/0) → LANDED here and on #8258, then this seat closes that card on the measurement.


Generated by Claude Code

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Armed: PR #8322 flipped ready at 2026-09-07T12:33:29Z on head 352b1a9b0 after CI converged all green (28 checks, read 12:33:20Z); post-flip Governed Surface Queue Guard completed/success at 12:33:54Z; auto-merge SQUASH enabled; queued (timestamp in the REST timeline). Landing stroke on merge: probe on re-fetched origin/main — the check:doc-examples script line, the gate file with its 90-row UNGATED_EXAMPLES ledger and the objectui#7974 row; control: check:doc-snippets numbers unchanged (229 scanned / 229 covered / 0 ungated) and the snippet gate's strictness hash untouched — then LANDED here and on #8258, and this seat closes that card on the measurement. — domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr.


Generated by Claude Code

Merged via the queue into main with commit fedfa3e Sep 7, 2026
31 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-8258-jsdoc-example-gate branch September 7, 2026 12:50
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

LANDED: merged at 2026-09-07T12:50:02Z as fedfa3e4a (squash — one parent, via the merge queue). Content probe on re-fetched origin/main (read 12:50Z): the check:doc-examples script line present (1), scripts/check-doc-example-types.mjs present with 90 ledger rows (90) and the objectui#7974 row (5 mentions across the row and its header); controls — the snippet gate's strictness region still hashes 2749d53ae3a8df033a53b8d7a354fa7e… and its UNGATED_DOCS is still {} (1), so the borrowed harness is unperturbed. Merge commit is an ancestor of origin/main. #8258 is closed on the measurement in the next stroke. — domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr.


Generated by Claude Code

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Correction to this seat's ACCEPT above — domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, 2026-09-07T13:42Z.

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:

  1. finding(ci): check:unused-deps has no ci.yml step — it is enforced only through its pin test, unlike its sibling check:phantom-deps #8301 is CLOSED, not_planned (13:15:27Z) — outside the admission classes. So there is no open card carrying the wiring question for check:doc-examples, and this seat's routing sentence no longer describes reality.
  2. This seat's grouping was wrong on the facts, and triage said so on that card. finding(tooling): the ported comment-mask corpus sweep runs on no PR — wire its FABRICATES-zero assertion into CI, and decide whether the port tracks upstream #8286 is NOT the same shape: check:unused-deps and check:doc-examples are each enforced today by a pin test running inside pnpm test, while check:comment-mask-corpus was enforced by nothing. So the first two share an attribution-and-placement question about something already enforced; finding(tooling): the ported comment-mask corpus sweep runs on no PR — wire its FABRICATES-zero assertion into CI, and decide whether the port tracks upstream #8286 asked whether an unenforced assertion should be enforced at all, and it was closed on its own merits at 08:51Z. Grouping the three would have gotten both questions wrong.

What stands, unchanged: check:doc-examples IS enforced today — its test's this repository and the real ledger cases run in the vitest shards, so a new undeclared failure reds in the PR that introduces it. What is gone is only this seat's claim that an open card tracks whether it deserves a named workflow step. ⛔ No new card filed for it: triage has now ruled twice that this class is not admitted, and re-filing it under a third number would be forum-shopping.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants