Skip to content

feat(tooling): read plugin key tables in the doc component-type gate, and judge the namespace half - #6205

Merged
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5106-doc-type-scan-surface
Aug 25, 2026
Merged

feat(tooling): read plugin key tables in the doc component-type gate, and judge the namespace half#6205
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5106-doc-type-scan-surface

Conversation

@yinlianghui-tw

Copy link
Copy Markdown
Collaborator

Part of #5106

Route A as triage adopted it. Two of the order's three parts had already landed — see "Premise" below — so this PR is part 1, plus one defect found inside the mechanism it extends.

What changed

scripts/check-doc-component-types.mjs read fenced code only. The #5002 family (PRs #5071 / #5078 / #5079 / #5085 / #5089 / #5093 / #5100 / #5104) replaced the plugin pages' fictional *Components registration loops with a markdown key table. The new form is the right one, and it landed entirely outside the scan surface — while the code blocks it replaced had been inside it. The fact "which keys does this plugin claim" moved from a checked place to an unchecked one.

It also never judged a namespace at all. It compared bare keys against a universe that merely happens to contain namespaced ones, so view:dashboard documented as plugin-dashboard:dashboard produced no signal from any static check.

Both halves of every key-table row are now judged.

The anchor is the table HEADER, not the row shape — this is the whole design

The obvious row heuristic ("first cell is a backticked token containing a colon, second cell is a backticked token") was built and measured over this tree before being rejected. It matches 33 rows, of which only 22 are keys. The other 11:

SiteRowWhat it really is
guide/console-architecture.md:104`/apps/:appName/:objectName` | `ObjectView`React route pattern
utilities/runner.mdx:99`http://localhost:5173/` | `LocalBundleLoader`URL
guide/metadata-diagnostics.md:43`GET /api/v1/meta/items/:type/:name`HTTP route
guide/designing-app-navigation.md:21`{ "type": "object", … }`JSON literal

Every one would be a false RED on correct documentation — the expensive direction for a gate whose entire value is being trusted about docs. So the anchor is | Namespaced key | Bare-name fallback | … |, which is a declaration by the page that the rows beneath it are registry keys. It discriminates perfectly where a row shape cannot, and costs authors nothing they were not already writing. Prose backticks are not dragged into the scan.

Deliberately not checked: when the fallback cell reads none — `skipFallback: true` , the gate does not assert the bare name is absent. It cannot — the universe is a deliberate union across every package, so view:grid skipping its own fallback says nothing about whether another package registers a bare grid, and one does. Asserting the negative would red plugins/plugin-grid.mdx:185, which is correct.

DOC_TYPE_EXEMPTIONS deliberately does not apply to table rows: a row under a header saying "Namespaced key" has already declared its vocabulary, so an exemption there would be a lie rather than a fact.

Controls — the card's own discriminating experiment, run as a before/after pair

Each run against HEAD's gate and this branch's gate on the identical mutated tree, so the pair isolates the change rather than the tree. Every mutation proven on disk by anchored counts; every restore proven by an empty git diff HEAD.

ControlPredictedHEAD's gateThis branch
Fake key in the markdown table (plugin-dashboard:phantom-widget)green → redexit 0, "Every documented component type is registered."exit 1, 2 findings (namespaced + bare) at :107
Same fake key in a fenced block (:155)red → redexit 1unregistered-doc-typeexit 1, identical finding — no surface traded
Namespace half: namespace: 'view''dash' in plugin-dashboard/src/index.tsx:83green → redexit 0, fully greenexit 1plugin-dashboard.mdx:99 [unregistered-key-table-key] namespaced 'view:dashboard'
Exemption non-vacuity: unregistered "type": "crud" into schema-reference.md (a .md file that has exemptions)redexit 1unregistered-doc-type — exemptions are keyed (file, value) and do not swallow it

The namespace control is the one worth reading twice: only one finding is produced, because the bare dashboard still matches. That is exactly why a gate judging only bare keys can never notice a namespace move, and why the namespaced cell is the half that closes it.

One honest note: the fenced control's first attempt was a no-op — the perl -i anchor matched zero lines, exited 0, and the gate ran green, which would have read as "the fenced surface still works" while testing nothing. Caught by the anchored count not moving (phantom stayed 0). Re-run against a verified anchor at :155; the table above is the real reading.

Non-vacuity, and a dead floor found inside it

Census now in the verdict line:

Scanned 184 doc file(s) (.mdx + .md), 1057 code block(s), 895 `type` literal(s) against 659
registered key(s) derived from 136 source file(s) (235 resolved call site(s), 133 indirect,
2 open): 760 registered, 135 exempted; 4 key table(s), 24 row(s), 45 table key(s) judged
(namespaced + bare), 45 registered.

Re-derived counts (the order asked; the card's 143/40 is from 2026-08-18): 144 .mdx + 40 .md = 184. All 24 rows across 4 tables pass today — the card's "现存的每张键表实测都是对的" still holds.

Zero key tables fails: renaming the header in all four files (4 → 0 occurrences, proven) gives
The scan collapsed: keyTables = 0, below the floor of 2. — not a green pass.

While adding those floors I found the existing one was dead. FLOORS.docFiles named a counter that never existedscanDocs publishes files — so it evaluated undefined < 100, which is false. The one floor whose job is catching the walk finding nothing had been inert for its whole life. Fixed by spelling, and the class closed: a floor naming no counter now fails loudly, naming the real counter list. Proven by re-introducing the exact original spelling:

FLOORS names `docFiles`, which is not a counter this scan publishes (callSites, codeBlocks,
exempted, files, indirect, keyTableKeys, ...). A floor over a missing counter compares
`undefined` and can never fail, so it guards nothing. Fix the spelling or drop the entry.

This is a bounded in-place fix, not scope creep: same defect class (this gate's own collapse detection, the mechanism this PR extends), mechanical, and evidence-pinned.

Premise: two of the three ordered parts had already landed

Verified against origin/main before writing code, per the order's own instruction to re-derive.

Since the intended corpus no longer exists, the controls above serve as this PR's validation corpus — which is what the order specified as the discriminating experiment anyway.

Verification

Union re-run on the final commit 93124e4d5:

  • node scripts/check-doc-component-types.mjsexit 0, census above
  • Root vitest (pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378), gate + every consumer that imports it — check-doc-component-types, known-schema-types-derivation-5115, check-pre-install-import-graph, scripts-type-check, catalog-gallery-renderall passed. A tooling diff is not automatically tooling-only: consumers import deriveRegistryKeys, which is untouched; scanDocs/analyze changed additively. 49 tests in the gate's own file (+12 new), 652 across the consumer set.
  • check-control-bytes → OK (5122 files) · type-check:scripts → exit 0 · lint:coverage, type-check:coverage, check-changeset-presence, check-pre-install-import-graph, check-doc-fences → exit 0

Exit codes captured by redirect before any pipe; each verdict quoted from the gate's own line.

Declared narrowings (CI runs the full farm regardless):

  • eslint narrowed to the two changed source files: both resolve under eslint's own config (2 files linted per --format json), 0 errors / 0 warnings, and eslint.config.js enables no type-aware linting (no projectService, no parserOptions.project), so this diff cannot move any untouched file's verdict.
  • check:doc-snippetsdeclined to run — "the packages it resolves against are not built" — an unbuilt worktree, not a finding. This diff contains zero content/docs bytes.

Out of scope, reported not fixed

The widened surface found no new ghosts: all 45 table keys are registered. Per the order, no other ghost was fixed.

Generated by Claude Code


Generated by Claude Code

… and judge the namespace half
`scripts/check-doc-component-types.mjs` read fenced code only. The objectui#5002
family (eight PRs) replaced the plugin pages' fictional registration loops with a
markdown key table — the right form, landing entirely outside the scan surface,
while the code blocks it replaced had been inside it. A fake key in a table was
green; the same key in a fence was red.
It also never judged a namespace. It compared bare keys against a universe that
merely happens to contain namespaced ones, so flipping a registration's
`namespace` left every doc teaching the old one green.
Both halves of every key-table row are now judged. The anchor is the table
HEADER, not the row shape: the row heuristic was measured over this tree and
matched 33 rows of which only 22 were keys — the rest are route patterns, URLs,
HTTP routes and JSON literals, every one a false red on correct docs.
Also fixes a dead floor: `FLOORS.docFiles` named a counter that never existed
(`scanDocs` publishes `files`), so it compared `undefined` and the check meant to
catch the walk finding nothing was inert. The class is closed too — a floor
naming no counter now fails loudly.
Part of #5106
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
Sign up for freeto 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

@yinlianghui-tw@claude