Skip to content

feat(scripts): gate documented SchemaExample ids against the catalog registry - #9009

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-8623-doc-example-id-gate
Sep 10, 2026
Merged

baozhoutao merged 4 commits into
mainfrom
claude/issue-8623-doc-example-id-gate

Conversation

@claude

@claude claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes objectui#8623

What this adds

check:doc-example-ids — a static check that every SchemaExample reference in
content/docs/** names an id the schema catalog registry actually carries, plus its
test and its own workflow.

SchemaExample resolves its id prop through getExample, and that lookup throws
on an unknown id rather than degrading. Per the triage ruling that behaviour is
unchanged here — no fallback in getExample, and no registry entry added so a
reference resolves. What was missing is that nothing asked the question before render,
so a mistyped or stale id passed every check in the repository and crashed the page it
was on.

The population — re-measured on this branch point, not inherited

origin/main had moved from the sha named in the dispatch (e8b7b078 -> 9b83f479), so
the reading was retaken:

reading count
pages walked under content/docs/** (.mdx + .md) 184
SchemaExample references found 416 (415 distinct)
ids in the catalog registry 432
references that resolve 414
references exempted as illustrative 2
real references that do not resolve 0

Control: the intersection is non-empty (414 of 416), so the misses are a reading and
not a broken matcher. The registry itself is read twice from the generated index — the
REGISTRY object's own keys, which is what getExample indexes, and the schema import
specifiers above them — and the two must agree exactly or the gate reports could not
run
.

Both exemptions are the syntax placeholders in the schema-catalog authoring guide, exactly
as the dispatch measured: the angle-bracket template in the snippet teaching the tag, and
the ellipsis in the prose sentence describing it. There is not one broken real id in the
docs today.

The exemption is a shape rule, not a list of two literals

A value carrying angle-bracket metasyntax, or an elision (Unicode or ASCII), cannot be a
filename-derived id — so it is illustrative. That rule is argued in the script's header
and it is never silent: every run prints each exemption with its page, its value and the
marker that matched. Two things it deliberately does not do:

  • It does not exempt a value that is merely wrong. Bad case, wrong segment count, a
    typo — all still judged. A pinned test fixtures a well-formed typo and requires it red.
  • It does not trust its own premise. assertMarkersAreImpossible re-derives from the live
    registry on every run that no real id carries a marker, and exits could not run if that
    ever stops holding, rather than letting the exemption swallow a real reference.

Green on landing proves nothing — so here is the gate going red

Firing control, on the real tree. One real id in a layout docs page was mutated to a
typo, the gate was run, and the page was restored. Restoration is proved by hash rather
than by an exit code — pre-mutation blob 36867f58, post-mutation f216a5ca,
post-restore 36867f58, and git diff HEAD empty:

=== GATE EXIT ON MUTATED TREE = 1
Doc example ids — walked 184 page(s) under content/docs, found 416 SchemaExample reference(s) (415 distinct), against 432 catalog id(s).

Exempted 2 illustrative reference(s) — metasyntax, never an id:
  content/docs/guide/schema-catalog.mdx (line 15)  id="…"  — elision
  content/docs/guide/schema-catalog.mdx (line 44)  id="[angle-bracket template]"  — angle-bracket metavariable

⛔ 1 reference(s) name an id the catalog registry does not carry:
  content/docs/components/layout/card.mdx (line 8)  id="components-layout-card/basic-crad"

Each one is a throw at page render in the published docs, not a blank tile. Fix the id, or add the schema and regenerate the catalog index. ⛔ Do not make getExample fall back — a silently wrong example teaches the wrong thing.

(The two exempted values are shown with the angle-bracket one written out in words; the
run prints the literal.)

Firing control, in the test. scripts/__tests__/check-doc-example-ids.test.ts spawns
the real script as a process over throwaway trees — not a unit test of a helper —
and asserts exit 1 for an unknown id, exit 1 for a tag with no id at all, and exit 0
once the same fixture's id is corrected, so the red was the id and not the fixture.

Non-empty-population controls. A walk that reads zero pages, a matcher that finds zero
references, and a registry that reads as zero ids each exit 2 (could not run), because
all three otherwise produce the same clean verdict as a clean tree. Each has its own pin.

Verdict on the restored tree, at final HEAD 61dd16b56:

Doc example ids — walked 184 page(s) under content/docs, found 416 SchemaExample reference(s) (415 distinct), against 432 catalog id(s).

Exempted 2 illustrative reference(s) — metasyntax, never an id:
  content/docs/guide/schema-catalog.mdx (line 15)  id="…"  — elision
  content/docs/guide/schema-catalog.mdx (line 44)  id="[angle-bracket template]"  — angle-bracket metavariable

✅ 414 real reference(s) all resolve in the catalog registry.

Why a fourth check-doc-* script and not a leg on one of the three

Each neighbour's header was read before this one was named, and none can be extended
without blurring a question it declares it answers alone:

  • the doc-example types checker compiles fenced ts/tsx inside JSDoc @example comments
    on exported declarations under packages/*/src/**, against the built .d.ts — a
    different population, a different instrument, a different universe;
  • the doc-example shared-reader checker asks whether a doc comment hand-spells a ladder
    a published shared reader owns, and its header rules content/docs/** out explicitly;
  • the doc component-types checker is closest in shape — it walks these same pages and
    derives its universe from source every run, which is why this script borrows both habits
    — but its subject is a type string literal inside a fenced code block judged against the
    component registry, and its header states it answers one question only. An example id is a
    JSX attribute in the page's prose judged against the example catalog; folding two
    vocabularies into one gate makes its verdict line ambiguous for no shared work.

The script name and the check:doc-example-ids alias collide with none of the three, and a
pin asserts all four still point at their own files.

Wiring: its own workflow, not a lint.yml leg

The dispatch asked for a workflow leg. It got its own workflow, for the reason the
doc-component-types workflow header records for the neighbouring gate over these same pages:
the change that introduces this defect is docs-only, and that is precisely the shape
ci.yml and lint.yml short-circuit — ci.yml's type-check job decides with a git diff
that excludes content/**. Wired into lint.yml, this gate would be blind to every change
that can introduce the defect. It needs no install and no build, so it can afford to run
unfiltered. This also leaves the lint.yml seam with objectui#8554 untouched entirely; the
only shared file is the package.json scripts block, one added line.

Two files beyond the declared face, both forced by existing pins

Running the whole scripts/__tests__ suite turned two pins red on the new workflow, and
both are wiring the repository already requires:

  • the CI/CD pipeline guide needs a section naming the workflow file plus an inventory row —
    a workflow nobody documented is a check contributors get blocked by without knowing it
    exists;
  • the Dependabot merge gate must classify every check a pull request produces exactly once.
    The new check subscribes pull_request with no trigger-level path filter, so it goes in
    the required set beside the three neighbouring doc gates.

⚠️ The docs page edited is the CI guide, not a page this gate reads for ids, and it was
not edited to make any check pass — it was edited because an existing pin requires it. No
content/docs page's SchemaExample id was touched.

Verification

Run under the shared verification lock where it holds cores; gate scripts read the checkout
only.

what result
pnpm exec vitest run scripts/__tests__ 138 files passed, 2 skipped; 4026 tests passed (26 of them this gate's)
the new gate on the real tree exit 0, output above
the new gate on a mutated real tree exit 1, output above
tsc -p tsconfig.scripts.json exit 0 — and --listFiles confirms both new files are in that program, so it is a measurement rather than an exclusion
eslint . repo-wide, --format json 4743 files linted; both new files 0 errors / 0 warnings. The 79 files with pre-existing errors are untouched by this branch, and the config enables no type-aware linting, so this diff cannot move any untouched file's verdict
changeset presence / no-major / fixed-group exit 0 each; the presence gate's own verdict line: no released package's source or published contract changed, and a changeset is declared anyway
control bytes, action-ref convention, doc links, doc component types, doc fence languages, shell-escape residue, new cross-file line citations exit 0 each
governed-surface guard --test over this diff NOT GOVERNED — ordinary merge-queue route

check-doc-snippet-types is not owed: the docs edit adds zero fenced blocks (git diff
^+ fence count is 0), so no snippet population moved. Everything else is CI's.

Boundaries honoured

  • getExample's throwing behaviour is untouched; no fallback, no registry entry added to
    make a reference resolve.
  • The population stops at content/docs/**. examples/, the READMEs, the smoke test, the
    script test, the extractor CLI, the Python regenerator and the site's own components are
    all out, and the script's header says why.
  • No test skipped, disabled or quarantined. content/docs/releases/ untouched.
  • scripts/check-eager-closure-budget.mjs untouched (objectui#8554's file).

⚠️ One dispatch premise my own measurement falsified

Premise 2 is false as stated. It said the reference shape outside content/docs "still
lives in exactly the seven files the card listed". Measured tree-wide on this branch point it
lives in nine: the seven, plus the site's SchemaExample component and its
SchemaCatalogIndex sibling. The card's original grep was scoped to scripts/ examples/ packages/, which never contained apps/, so the seven-file reading was correct for that
scope and the dispatch generalised it one step too far. Control: the same command shape
over the same population returns 179 files for a term known to be everywhere, so the reader
was firing. This changes nothing about the ruling — both extra files are the machinery that
consumes ids rather than pages that reference them, and both are outside the population
triage scoped — but the dispatch's count is wrong and is recorded as wrong here.

Premises 1, 3 and 4 all held. The dispatch's population table (415 distinct / 432 registry /
413 resolving / 2 missing, both placeholders in the authoring guide) reproduced exactly on
the newer tip.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

…atalog registry

`SchemaExample` resolves its `id` prop through `getExample(id)`, which throws on
an unknown id rather than degrading. That is the right runtime behaviour — a
silently wrong or empty example teaches the wrong thing — but it means a
mistyped or stale id is a throw at page render in the published docs, and
nothing asked the question before then.

Adds `check:doc-example-ids`: it walks every `.mdx`/`.md` page under
`content/docs/**`, reads each `<SchemaExample>` tag's id, and resolves it
against the ids the generated schema-catalog index carries. Illustrative
references are recognised by SHAPE — a value carrying angle-bracket metasyntax
or an elision cannot be a filename-derived id — rather than by a list of the two
literals the tree happens to contain today, and every exemption is printed on
every run. The rule's premise is re-derived from the live registry each run: if
a real id ever carries a marker, the gate stops instead of exempting it.

The population is `content/docs/**` only. The same reference shape appears in
the examples READMEs, the generated index, the catalog smoke test, a script
test, the extractor CLI, the Python regenerator and the site's own components;
all of those are illustrative prose or the machinery itself, and a gate that
failed on a README's teaching id would be its own defect.

Gets its own workflow rather than a leg on `ci.yml`/`lint.yml` for the reason
`doc-component-types.yml` records: the change that introduces this defect is
docs-only, which is exactly the shape those two workflows skip.

The population is clean today, so the test carries a firing control — the real
script, spawned over a throwaway tree with a deliberately unknown id, asserted
to exit 1 — plus controls that an empty walk, an empty reference set and a
half-read registry each report "could not run" rather than a clean zero.

`getExample` is unchanged, and no fallback entry was added to the registry.

Co-authored-by: Claude <noreply@anthropic.com>
Two existing pins require every workflow to be both documented and bucketed,
and both went red on the new one:

  - `ci-cd-pipeline-doc.test.ts` requires a section naming the workflow file,
    plus a row in the inventory table — a workflow nobody documented is a check
    contributors get blocked by without knowing it exists.
  - `dependabot-merge-gate.test.ts` requires every check a pull request
    produces to be classified exactly once. `Doc Example Id Check` subscribes
    `pull_request` with no trigger-level path filter, so every pull request
    produces it, which puts it in the required set beside the three
    neighbouring doc gates.

The docs edit is to the CI guide, not to a page this gate reads for ids.

Co-authored-by: Claude <noreply@anthropic.com>
claude Bot and others added 2 commits September 10, 2026 17:51
`check:entry-guard` refused the hand-typed guard, and it is right to: node
leaves `process.argv[1]` as the caller typed it, so a script reached through a
symlink compares two different paths, answers false and does nothing — exit 0,
no output, which a CI wrapper reading only the status cannot tell from a pass.

Routes it through the one shared predicate instead. The baseline is untouched:
`KNOWN_HAND_TYPED_GUARDS` is shrink-only and adding a line to it is not a
supported way to make this pass.

Behaviour is unchanged when the script is invoked normally, and now correct
through a symlink: both spellings print the same funnel and exit 0.

Co-authored-by: Claude <noreply@anthropic.com>
@baozhoutao
baozhoutao marked this pull request as ready for review September 10, 2026 18:17
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 37a3169 Sep 10, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-8623-doc-example-id-gate branch September 10, 2026 18:52
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