Uh oh!
There was an error while loading. Please reload this page.
feat(scripts): compile documentation snippets against the built types (#5138) - #5161
Conversation
…t types (#5138) objectui#5138 shape 2, as ruled: promote the snippet-extraction + `tsc --strict`-against-built-`dist` harness into `scripts/`, where it runs once in CI instead of three times by hand. The harness already existed three times, hand-rolled and private — in #5053, #5060 and #5047's PR — and each copy found defects its reviewer had not listed. This keeps the practice each one proved: extraction by script rather than by hand, resolution against the package's built `dist/*.d.ts` with a self-check that says so, and a planted sentinel export that must produce a diagnostic. The false-green mechanism #5047 measured is designed against structurally rather than noted: parse errors suppress semantic checking program-wide, so a run can print a few syntax errors, no semantic diagnostics at all, and read as a meaningful red. The two phases are separate here, unparseable blocks are reported and kept out of the semantic program, every failure line is tagged `[syntax]` or `[semantic]`, and the summary always states how many blocks the semantic phase actually judged. Fragments are declared, never guessed: a block that is not meant to compile carries a marker with a written reason immediately above its fence. A block that fails to parse is a failure, never a skip — the alternative turns every real defect into a silent skip. Coverage is declared too. A document is covered unless it is named in the script's ledger with a reason; the default is covered, so a new page is gated from the day it lands. 13 documents and 67 blocks are covered today; the 44 documents on the ledger are debt with names, and the script's header says plainly that they are unverified. Scope of the gate, stated in its header because an unstated blind spot is how this class stays green: it judges TypeScript resolvability only — not schema-key validity against the spec (#5138 shape 1, unruled), not `type`-literal registration (`check-doc-component-types.mjs`), and not shell examples (#5151).
Adding an unfiltered blocking workflow carries two obligations in this repository, both held by ratchets that fired on the first run: - `content/docs/guide/ci-cd-pipeline.md` documents every workflow — a check contributors get blocked by without knowing it exists is the #3212 shape. New inventory row plus its own section: what it does, why it builds (and why that is not the per-PR full-repo build the #4846 ruling rejected), the fragment rule, the syntax/semantic split, the three self-controls, and what the coverage ledger does not claim. - `scripts/dependabot-merge-gate.mjs` partitions the checks a pull request produces; an unclassified one is a name the auto-merge gate stops waiting for. `Doc Snippet Type Check` is required: its workflow subscribes `pull_request` with no path filter, so every pull request produces it. `INCIDENT_4959` in the gate's test is a verbatim record of one SHA's check runs and is left untouched. Its all-green snapshot now appends the required contexts added since that SHA instead — building "all green" from the frozen record alone would make every gate added after it read as permanently pending, and the natural repair (growing the record) would falsify the counterfactual the whole file rests on.
…a marker declares (#5138) Verifying the ledger's own entries rather than trusting the diagnostics that produced them: `content/docs/plugins/plugin-calendar-view.mdx` does not resolve `@object-ui/plugin-calendar-view` because it is a MIGRATION GUIDE whose "Before" blocks quote the retired package on purpose. Correct documentation, not a defect, and the entry now says so — an unverified reason on a debt list reads as a triage that happened. That case also fixes a gap in how the marker was described. It covers two kinds of block, not one: a genuine fragment, and a block deliberately about code that no longer exists. The keyword stays `fragment` rather than growing a second vocabulary; the written reason is what tells them apart, and it is the part a reviewer reads.
os-zhuang
commented
Aug 18, 2026
PM review — accepted. All four acceptance criteria met, and the third exceeded. PM session Checks hand-read before arming: 19 total, 17 success, 2 correctly skipped. The thing that raises this above the briefI asked for a planted-sentinel control so a probe silently resolving everything to and did the same to the resolution self-check (mapped to The other three, briefly
Two judgement calls I did not ask for and would not have thought to
And you caught a false finding of your own
On #5106Accepted as not subsumed, and the reason is structural rather than a punt: a markdown key table is outside the fenced-code surface either gate reads, and a registry namespace is a runtime string that no type checker judges. That belongs where the card puts it. #5160 is the gate earning its keep on day one — five published READMEs importing symbols their packages do not export, 15 TS2305 measured over the ungated corpus. Triaging separately. Landing: ready + auto-merge (squash). Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#5138 — shape 2 only, as ruled. Shape 1 (
safeParseagainst the spec schema) is not addressed here and stays a finding; its fragment-marking design question is unresolved and is a maintainer call.Base
da6eda06e. Union run atbf7cce684, the final commit.What landed
scripts/check-doc-snippet-types.mjsscripts/__tests__/check-doc-snippet-types.test.ts.github/workflows/doc-snippet-types.ymlpackage.jsoncheck:doc-snippetscontent/docs/guide/ci-cd-pipeline.mdscripts/dependabot-merge-gate.mjsDoc Snippet Type Checkclassified as requiredscripts/__tests__/dependabot-merge-gate.test.tscontent/docs/plugins/plugin-{report,view,charts}.mdxEvery covered
ts/tsxblock is compiled--strict, in isolation, against each package's builtdist/*.d.ts. Green run at the head commit:Verified against a PLANTED instance, not today's tree
Every defect #5138 lists has since been fixed, so a green run over the current tree proves the tree is clean and nothing else. All four of #5047's measured falsehoods were planted back into the covered Quick-start block of
content/docs/plugins/plugin-report.mdx, one leg at a time, with the direction predicted before each run. The gate was committed first, so each restore is agit checkout HEADof that one path;git statusis clean after all five legs andgit diff HEADis empty.Leg A — all four at once, exactly as the document had them. Predicted: red,
[semantic]only, TS2305 onReportInput, and the key-level errors masked, because #5047 measured that an unresolved type annotation degrades the literal's contextual type. Observed, verbatim, and the masking prediction was exactly right — one diagnostic, not four:Leg B — the repurposed key, written without the masking annotation.
objectNameandgroupingsDownproduced nothing in this leg. Not a gap — TypeScript reports one excess property per object literal and thecolumnstype error preempted it, which leg C isolates rather than assumes.Leg C — the rejected keys alone:
Leg D — the fabricated export:
Leg D also demonstrates the build filter is live rather than declared: the moment a covered block imported
@object-ui/plugin-report,--build-filterbegan emitting--filter=@object-ui/plugin-report.Restored: every leg reverted,
node scripts/check-doc-snippet-types.mjs→ exit 0, the green block quoted above.The controls have teeth — each one ablated
A probe that cannot fail is not a probe, so each control was broken deliberately and the run had to refuse a verdict.
Sentinel — pointed at a symbol that does exist:
Resolution —
@object-ui/typesmapped to itssrc, the way an inherited roottsconfig.jsonwould (the repo's own root config maps the workspace to source, so this substitution is one config away):Both ablations reverted from the committed copy; the working tree was clean afterwards.
Syntax is not semantics — the false-green mechanism, demonstrated
#5047 measured that parse errors suppress semantic checking program-wide: a run prints a few syntax errors, no semantic diagnostics at all, and reads as a meaningful red while proving nothing. The two phases are separate here — blocks are parsed one at a time, unparseable ones are reported and kept out of the semantic program. Planting #5047's exact shape (a bare
filter: { ... }prose line in atsfence):The other 67 blocks still got semantic checking — which is the behaviour #5047 could not get — and the summary states the coverage in words, so a syntax-only red cannot be read as a semantic pass.
The fragment rule, chosen and stated in the gate's header
The marker is an MDX expression comment in
content/docs, and an HTML comment of the same shape in a package README:Three parts of that are load-bearing:
export {}appended so it cannot see another block's globals. This models the reader, who copies ONE block — plugin-report docs still teach the pre-9.0 query form as the live authoring shape — and name three renderers removed at the ADR-0021 cutover #5047 found three README examples callingdefineReportwith no import of their own and fixed the documents, not the harness. Without the module forcing, the first survey run showed 64 spuriousTS2451redeclarations from blocks silently sharing globals.plugin-calendar-view.mdxfails to resolve@object-ui/plugin-calendar-viewbecause it is a migration guide quoting the retired package on purpose — correct documentation. The ledger entry says so; the first draft called it a defect, which would have been a false finding.Five markers landed, each with its reason: two in
plugin-report.mdx, two inplugin-charts.mdx, one inplugin-view.mdx.pnpm --filter @object-ui/site exec fumadocs-mdxregenerates cleanly with them, so the MDX form parses and renders to nothing.Coverage is declared, and the ledger is debt with names
A document is covered unless
UNGATED_DOCSnames it with a reason, so the default is covered — a new page is gated from the day it lands, and opting one out is an edit a reviewer sees. Entries are re-derived per run: one naming a file that does not exist, holding no snippet, or carrying no reason fails the gate, so the list can only shrink.13 documents / 67 blocks are covered today. 44 are on the ledger, each with its measured diagnostic mix. The header states the limit plainly rather than letting a green run imply otherwise:
It carries no per-file failure count deliberately: producing one means compiling every ungated document, i.e. building every package in the workspace on every run — the per-PR full-repo build the 2026-08-16 ruling on #4846 rejected.
What the gate does NOT cover — stated in its header, because that is the point of the card
The header opens with three exclusions rather than closing with them: it judges TypeScript resolvability only — not schema-key validity against the strict spec schemas (#5138 shape 1, unruled, and genuinely stronger since
unrecognized_keyscatches what a type annotation cannot), not whether atypeliteral names a registered component (check-doc-component-types.mjs, which stays its own install-free gate), and not whether a shell example runs (#5151 is out of scope here).What was taken from each of the three existing harnesses
src/—ReportScheduleConfigappears twice insrc/and is not exporteddist+ the resolution self-check that proves it; the plantedThisNameIsDefinitelyNotExportedsentinel; the syntax-suppression trapAdded beyond all three: the positive control (a real import must be clean, so a broken harness cannot read as "the docs are full of defects"), the src-leak assertion, the declared fragment marker, and the coverage ledger.
Wiring
Its own workflow, unfiltered on
pull_request/push/merge_group— the change that breaks a snippet is docs-only, exactly the shapeci.ymlshort-circuits. It is the only workflow that runs the script, and the test fails if apathsfilter is ever added to either.It builds, unlike its install-free sibling, because its criterion is the published type surface. The build is filtered to the packages the covered documents import, and the filter is emitted by the gate (
--build-filter) rather than hand-maintained in YAML, so it cannot drift and grows only when coverage grows. That is deliberately not the per-PR full-repo build the #4846 ruling rejected, and the test asserts the workflow never degenerates into one.Adding an unfiltered blocking check has two consequences here, both caught by existing ratchets on the first run and both honoured:
ci-cd-pipeline.mdgets an inventory row and a section, anddependabot-merge-gate.mjsclassifiesDoc Snippet Type Checkas required.INCIDENT_4959is a verbatim record of one SHA's check runs and is untouched — its all-green snapshot now appends required contexts added since that SHA, because growing the record would falsify the counterfactual the file rests on.Changeset — none, and why
node scripts/check-changeset-presence.mjs→ exit 0: "7 file(s) changed, 0 of them under the src/ of a package the release covers ... no changeset is owed." The precedent that pulled the other way — #5053, #5060 and #5047 each shipped apatchchangeset for README-only edits — turns on the README being in the package's publishedfilesand therefore reaching npm. This PR edits no package README; its doc changes arecontent/docs/**, the site, which ships in no package. So no changeset. There is noskip-changesetlabel in this repository and none was created.Gate union — derived from the actual changed paths, all at
bf7cce684node scripts/check-doc-snippet-types.mjsnode scripts/check-doc-component-types.mjscontent/docs/**.mdxchangednode scripts/check-doc-links.mjscontent/docsinSCAN_ROOTSnode scripts/check-control-bytes.mjsnode scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjsnode scripts/check-lint-coverage.mjspackage.jsonchangednode scripts/check-type-check-coverage.mjspackage.jsonchangednode scripts/check-skills-paths.mjspnpm type-check:scriptsscripts/**changedpnpm exec vitest run --project unit scripts/__tests__package.json, docs claimspnpm exec eslinton the four changed script files, thenpnpm lint:rootscripts/**changedpnpm --filter @object-ui/site exec fumadocs-mdxBeyond the families a dispatch list would name, three were added after re-deriving against the real diff and each found something:
ci-cd-pipeline-doc.test.tsanddependabot-merge-gate.test.ts(both fired on the new workflow), and the fumadocs regeneration (the marker syntax).#5106 — read before starting, and this does NOT subsume it
#5106 is the other gap in
check-doc-component-types: its scan surface stops at code fences, so the namespaced-key tables eight PRs introduced are invisible to it, and it never judges thenamespacehalf of a registration.Not subsumed, and the reason is structural rather than a scoping choice. This gate's scan surface is the same one — fenced
ts/tsxblocks — so a key in a markdown table is outside it too, and it is narrower in the other direction as well: it reads TypeScript, and a registry key is a runtime string that no type checks.namespace: 'view'versusnamespace: 'dash'is a value-level fact about aregister()call; nothing about compiling a snippet can reach it. A fix for #5106 belongs where the card puts it — either widening the first gate's scan surface to that one structured table row, or generating the tables fromderiveRegistryKeys. Not touched here.Filed unassigned, not fixed here
TS2305, measured by this gate over the ungated corpus (@object-ui/core×6,components×3,app-shell×2,react×1, plusobjectos-integration.mdx×3). The plugin-report README 教三个不存在的导出:ReportBuilder组件、registerDrillHandler、ScheduleConfig组件 #5016 / plugin-report README's export snippets call every real export with the wrong signature —exportAsCSV(data, 'file.csv')passes a filename where the data argument goes #5048 defect class, live in four more packages. All five documents are on the ledger with that mix as their reason.Also worth a reviewer's eye, recorded rather than filed: the adoption survey over the whole corpus reads 460 blocks in 57 documents, of which 198 compile clean today. The ledger's reasons carry the measured mix per file, so the shape of the debt is visible without re-running the survey.
Generated by Claude Code