From 0a6cba7b910503ff89686c68e910097931874bed Mon Sep 17 00:00:00 2001 From: os-support-ai Date: Thu, 20 Aug 2026 02:17:56 +0000 Subject: [PATCH] fix(docs/gate): check-doc-component-types collects `.md` under content/docs, not `.mdx` only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collector walked `.mdx` only, so 40 `.md` guides under the same tree were neither judged nor declared ungated. Widened to the `DOC_EXTENSIONS = ['.mdx', '.md']` spelling `check-doc-snippet-types.mjs` already uses — two collectors walking one tree two different ways is the defect one level up. Measured on f2e11ae6f: 143 -> 183 pages, 564 -> 887 `type` literals, and 67 first-collection findings across 15 `.md` pages. Ten of those were real key errors and are fixed here: building-crud-app.md ObjectGrid/ObjectForm/ObjectDetail -> object-grid / object-form / detail-view (PascalCase is a component NAME; the registry key is lower-kebab, and the registry lookup is case-sensitive) expressions.md empty-state -> empty (EmptySchema declares it) schema-rendering.md empty-state -> empty schema-playground.md grid-layout -> grid (fence and prose) api/schema-reference.md the Email field's `link` -> `email` The remaining 57 sites are 31 (file, value) DOC_TYPE_EXEMPTIONS entries, each with a reason naming the vocabulary it really belongs to: ActionSchema discriminants, ComponentInput.type in `inputs[]`, dashboard widget kinds, object metadata field types, package.json's own `"type": "module"`, and walkthrough placeholders the pages register themselves. One entry is debt rather than vocabulary: `crud` is on the render path with four declaration faces and no renderer. Filed as #5373 and the entry says to delete it when that lands. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE --- .changeset/doc-component-types-md-surface.md | 7 + .github/workflows/doc-component-types.yml | 13 +- content/docs/api/schema-reference.md | 4 +- content/docs/guide/building-crud-app.md | 10 +- content/docs/guide/expressions.md | 2 +- content/docs/guide/schema-playground.md | 4 +- content/docs/guide/schema-rendering.md | 2 +- .../check-doc-component-types.test.ts | 86 +++++++- scripts/check-doc-component-types.mjs | 186 +++++++++++++++++- 9 files changed, 286 insertions(+), 28 deletions(-) create mode 100644 .changeset/doc-component-types-md-surface.md diff --git a/.changeset/doc-component-types-md-surface.md b/.changeset/doc-component-types-md-surface.md new file mode 100644 index 0000000000..07abfde41f --- /dev/null +++ b/.changeset/doc-component-types-md-surface.md @@ -0,0 +1,7 @@ +--- +--- + +`scripts/check-doc-component-types.mjs` now collects `.md` pages under `content/docs` +alongside `.mdx`, converging on the `DOC_EXTENSIONS` spelling +`scripts/check-doc-snippet-types.mjs` already uses. No published package changes: the +diff is the gate script, its test, its workflow comments, and six documentation pages. diff --git a/.github/workflows/doc-component-types.yml b/.github/workflows/doc-component-types.yml index 67f037387c..fd357f17be 100644 --- a/.github/workflows/doc-component-types.yml +++ b/.github/workflows/doc-component-types.yml @@ -5,8 +5,9 @@ name: Doc Component Types # precisely the shape both of those workflows skip. `ci.yml`'s `type-check` job # decides whether to run its expensive steps with a `git diff` that excludes # `content/**`, `'**/*.md'`, `docs/**` and `apps/site/**` — so a PR that edits -# only `content/docs/**.mdx` reports the context and runs none of the gates -# inside it. A gate against a wrong `type` in a teaching snippet, wired there, +# only `content/docs/**` reports the context and runs none of the gates inside +# it. Note the `'**/*.md'` term especially: since objectui#5342 this gate reads +# the `.md` guides too, which is exactly the extension that diff excludes. A gate against a wrong `type` in a teaching snippet, wired there, # would be blind to every change that can introduce one. # # This is the fifth instance of the shape in this repo and the reasoning is @@ -23,9 +24,9 @@ name: Doc Component Types # gate, one home. # # It needs no install and no build. The script reads the checkout with `node:fs` -# only: 143 mdx files for the snippets, and the `packages/` + `apps/` sources for -# the registered-key universe it compares them against. A few seconds. Keep it -# that way if you add checks to it — the moment this needs `pnpm install` it +# only: 183 pages for the snippets (143 `.mdx` + 40 `.md`, objectui#5342), and +# the `packages/` + `apps/` sources for the registered-key universe it compares +# them against. A few seconds. Keep it that way if you add checks to it — the moment this needs `pnpm install` it # stops being cheap enough to run unfiltered, and the filter is the hole. on: @@ -65,7 +66,7 @@ jobs: with: node-version: '22.x' - # A `type` string in a `content/docs/**.mdx` code block is not rendered, + # A `type` string in a `content/docs/**` code block is not rendered, # not parsed and not compared against anything, so a snippet can name a # component that does not exist and every check in the repo stays green — # while a reader who copies it gets the renderer's red "Unknown component diff --git a/content/docs/api/schema-reference.md b/content/docs/api/schema-reference.md index f9aa192bcc..38cd3a77bf 100644 --- a/content/docs/api/schema-reference.md +++ b/content/docs/api/schema-reference.md @@ -680,7 +680,7 @@ A single-record detail view with grouped fields, actions, and tabs. "title": "Customer Info", "fields": [ { "name": "customer", "label": "Customer", "type": "text" }, - { "name": "email", "label": "Email", "type": "link" }, + { "name": "email", "label": "Email", "type": "email" }, { "name": "created", "label": "Created", "type": "date", "format": "MMM d, yyyy" } ] }, @@ -1109,7 +1109,7 @@ An enhanced detail view for a single record with sections, tabs, related records "fields": [ { "name": "firstName", "label": "First Name", "type": "text" }, { "name": "lastName", "label": "Last Name", "type": "text" }, - { "name": "email", "label": "Email", "type": "link" }, + { "name": "email", "label": "Email", "type": "email" }, { "name": "avatar", "label": "Photo", "type": "image" } ] } diff --git a/content/docs/guide/building-crud-app.md b/content/docs/guide/building-crud-app.md index 3c7e6951ba..7da10239e0 100644 --- a/content/docs/guide/building-crud-app.md +++ b/content/docs/guide/building-crud-app.md @@ -209,7 +209,7 @@ function App() {
{ setEditId(row.id); setShowForm(true); }} /> @@ -246,7 +246,7 @@ Add a "New Task" button and handle row clicks to open the edit form: {showForm && ( void }) { + // objectui#5342. The collector used to walk `.mdx` only, so 40 `.md` guides + // under the SAME tree were neither judged nor declared. This asserts the + // walk, not the verdict: revert `DOC_EXTENSIONS` to `['.mdx']` and the + // `from-md` site disappears while every other test in this file stays green. + const { sites, counters } = withTree((write) => { + write('content/docs/a.mdx', ['```json', '{ "type": "from-mdx" }', '```'].join('\n')); + write('content/docs/guide/b.md', ['```json', '{ "type": "from-md" }', '```'].join('\n')); + // Not a page: the `meta.json` sidecars fumadocs keeps beside the prose. + write('content/docs/meta.json', '{ "pages": ["a"] }'); + }, (dir) => scanDocs(dir)); + expect(sites.map((s) => s.value).sort()).toEqual(['from-md', 'from-mdx']); + expect(counters.files).toBe(2); + }); + + it('judges a `.md` page by the same rule, so an unregistered type there is a finding', () => { + const { findings } = withTree((write) => { + write('packages/demo/src/index.tsx', "ComponentRegistry.register('div', C, { namespace: 'ui' });\n"); + write('content/docs/guide/b.md', ['```json', '{ "type": "not-a-component" }', '```'].join('\n')); + }, (dir) => analyze(dir, BARE)); + const f = findings as Finding[]; + expect(f.map((x) => x.reason)).toContain('unregistered-doc-type'); + expect(f.find((x) => x.reason === 'unregistered-doc-type')?.site).toBe('content/docs/guide/b.md:2'); + }); + it('reports an unterminated fence rather than guessing where code stops', () => { const { findings } = withTree((write) => { write('content/docs/x.mdx', ['```json', '{ "type": "div" }'].join('\n')); @@ -367,6 +392,18 @@ describe('the scan cannot collapse quietly', () => { expect(counters.registered).toBeGreaterThan(400); }); + it('really walks the `.md` half of this tree — the objectui#5342 widening, pinned', () => { + // A repo-level assertion because the fixture above proves only the + // mechanism. `content/docs` holds 143 `.mdx` and 40 `.md`; a revert to + // `.mdx`-only drops ~323 `type` literals out of the scan and this repository + // stays green while judging none of them. + const { sites, counters } = scanDocs(repoRoot); + const mdFiles = new Set(sites.filter((s: { file: string }) => s.file.endsWith('.md')).map((s: { file: string }) => s.file)); + expect(mdFiles.size, 'no `.md` page carries a scanned `type` literal — the collector narrowed').toBeGreaterThan(15); + expect(mdFiles.has('content/docs/api/schema-reference.md')).toBe(true); + expect(counters.files).toBeGreaterThan(170); + }); + it('this repository is green', () => { const findings = analyze(repoRoot).findings as Finding[]; expect(findings.map((f) => `${f.reason} :: ${f.site} :: ${f.value ?? ''}`)).toEqual([]); @@ -408,6 +445,49 @@ describe('the three snippets this gate found on its first run stay fixed', () => // ── 6. the wiring ──────────────────────────────────────────────────────────── +describe('objectui#5342 — the key errors the widened collector found stay fixed', () => { + // Named rather than left to the repo-wide green assertion, for the same reason + // the block above names its three: these are the live specimens the extension + // widening produced, and a revert would otherwise read as an unrelated + // regression somewhere in a 183-file scan. Each one rendered the OBJUI-001 + // "Unknown component type" panel for a reader who copied it. + const read = (rel: string) => fs.readFileSync(path.join(repoRoot, rel), 'utf8'); + + it('the CRUD guide spells the registered keys, not the PascalCase component names', () => { + const body = read('content/docs/guide/building-crud-app.md'); + for (const wrong of ['ObjectGrid', 'ObjectForm', 'ObjectDetail']) { + expect(body, `${wrong} is a component NAME; the registry key is lower-kebab`).not.toContain( + `type: '${wrong}'`, + ); + } + expect(body).toContain("type: 'object-grid'"); + expect(body).toContain("type: 'object-form'"); + expect(body).toContain("type: 'detail-view'"); + }); + + it('the two empty-state snippets spell `empty`, the key EmptySchema declares', () => { + // packages/types/src/feedback.ts declares `type: 'empty'` and + // packages/components/src/renderers/feedback/empty.tsx registers it. + for (const rel of ['content/docs/guide/expressions.md', 'content/docs/guide/schema-rendering.md']) { + expect(read(rel), `${rel} still teaches empty-state`).not.toContain('"type": "empty-state"'); + expect(read(rel)).toContain('"type": "empty"'); + } + }); + + it('the playground teaches `grid`, in the fenced snippet AND in the prose beside it', () => { + const body = read('content/docs/guide/schema-playground.md'); + expect(body, 'nothing registers grid-layout').not.toContain('grid-layout'); + expect(body).toContain('"type": "grid"'); + }); + + it('the schema reference gives its Email field a field type that exists', () => { + // `link` is not in fieldWidgetMap; `email` and `url` are. + const body = read('content/docs/api/schema-reference.md'); + expect(body).not.toContain('"label": "Email", "type": "link"'); + expect(body).toContain('"label": "Email", "type": "email"'); + }); +}); + describe('wiring — the gate is reachable and a docs-only PR starts it', () => { const workflowDir = path.join(repoRoot, '.github/workflows'); const workflowPath = path.join(workflowDir, 'doc-component-types.yml'); @@ -444,7 +524,7 @@ describe('wiring — the gate is reachable and a docs-only PR starts it', () => it('runs it in NO path-filtered workflow — the change that breaks it is docs-only', () => { // The whole reason this is its own workflow. `ci.yml`'s type-check job // excludes `content/**` from the diff that decides whether its gates run, so - // a PR editing only `content/docs/**.mdx` would start this gate nowhere. + // a PR editing only `content/docs/**` would start this gate nowhere. expect(workflowFiles.length, 'the workflow directory scan returned implausibly few files').toBeGreaterThan(5); for (const file of workflowFiles) { const yaml = yamlOf(file); diff --git a/scripts/check-doc-component-types.mjs b/scripts/check-doc-component-types.mjs index 2a88fdf458..e8ea8e4a6c 100644 --- a/scripts/check-doc-component-types.mjs +++ b/scripts/check-doc-component-types.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node /** - * Every `type` string literal in a `content/docs/**.mdx` code block must name a - * component the repository actually registers — or be declared, per file, as - * belonging to some other vocabulary. + * Every `type` string literal in a `content/docs/**` code block — `.mdx` and + * `.md` alike — must name a component the repository actually registers, or be + * declared, per file, as belonging to some other vocabulary. * * Run: node scripts/check-doc-component-types.mjs (also `pnpm check:doc-types`) * Exit: 0 = every teaching snippet names a registered type (or a declared @@ -142,9 +142,29 @@ const scriptDir = dirname(fileURLToPath(import.meta.url)); // ── Configuration ──────────────────────────────────────────────────────────── -/** Where the teaching prose lives. */ +/** Where the teaching prose lives. This gate walks `content/docs` and nothing + * else: not `skills/**`, not the package READMEs (`check-doc-snippet-types.mjs` + * covers those for its own question), not `docs/**`. */ const DOCS_ROOT = 'content/docs'; +/** Page extensions collected under `DOCS_ROOT`. BOTH are collected, and that is + * the whole content of the scan surface: `content/docs` is authored in a mix of + * `.mdx` and `.md` — the same guide tree, the same renderer, the same reader — + * and an extension is not a coverage decision. Deliberately kept identical to + * `check-doc-snippet-types.mjs`'s `DOC_EXTENSIONS`: two collectors walking one + * tree two different ways is a defect one level up from either gate, and it is + * how a page ends up covered by the question it passes and invisible to the one + * it fails. + * + * Collecting only `.mdx` is what objectui#5342 measured, and unlike its sibling + * objectui#5174 this file was never lying about it — the sentence above used to + * say `.mdx` and the ledger was keyed by `.mdx` paths throughout. It was a + * stated coverage decision, not a broken promise, and the decision is now the + * other way: 40 `.md` pages sat outside the ledger, so they were neither + * covered nor declared ungated. Anything else under the tree (the `meta.json` + * sidecars) holds no prose and is not a page. */ +const DOC_EXTENSIONS = ['.mdx', '.md']; + /** Where registrations live. Every workspace source root that can register. */ const SOURCE_ROOTS = ['packages', 'apps', 'examples']; @@ -211,12 +231,36 @@ const OPEN_REGISTRATION_SITES = { * Per-file declarations that a `type` value in that page belongs to a * vocabulary other than the SDUI component registry. * - * Keyed `` -> `` -> reason. The reason must + * Keyed `` -> `` -> reason. The reason must * name the vocabulary and, where one exists, where it is declared — an * exemption that only says "not a component" teaches the next reader nothing * and cannot be re-checked. */ const DOC_TYPE_EXEMPTIONS = { + 'content/docs/api/schema-reference.md': { + action: + 'ActionSchema discriminant under a CRUD schema\'s ACTION LISTS, never a rendered child — ' + + '`toolbar.actions[]`, `rowActions[]`, `batchActions[]` and a detail page\'s `actions[]` are ' + + 'each typed `ActionSchema[]` (packages/types/src/crud.ts:175, 379, 480, 484, 561, 612), and ' + + 'that interface declares `type: \'action\'` at crud.ts:89. Same vocabulary as the ' + + '`core/enhanced-actions.mdx` entry below.', + crud: + 'CRUDSchema discriminant — packages/types/src/crud.ts:418 declares it, zod/crud.zod.ts:158 ' + + 'validates it, core/src/validation/schema-validator.ts:135 has a branch for it and ' + + 'builder/schema-builder.ts:170 constructs it. FOUR declaration faces and NO registered ' + + 'renderer, and unlike its siblings in this table it IS on the render path (CRUDComponentSchema ' + + 'is in the node union at types/src/index.ts:852), so a node spelling it paints OBJUI-001. ' + + 'Ledgered rather than re-spelled because there is no registered spelling to move to: register ' + + 'a renderer / retire CRUDSchema under ADR-0049 / demote it off the node union are three ' + + 'different edits to this page, and picking one is a contract decision objectui#5115 left open ' + + 'after PR objectui#5128 closed only its CLI half. Filed as objectui#5373. DELETE this entry ' + + 'when that lands — the gate reports a stale exemption, so it cannot be forgotten.', + string: + 'PageNodeSchema variable declaration\'s data type inside `variables[]`, next to `name` / ' + + '`defaultValue` — `PageVariable` (packages/types/src/layout.ts:566, re-exported from ' + + '@objectstack/spec\'s `PageVariableSchema`). Same vocabulary as blocks/block-schema.mdx\'s ' + + '`string`.', + }, 'content/docs/blocks/authentication.mdx': { submit: 'ActionSchema discriminant under a button\'s `action` key, not a node type. ' + @@ -264,6 +308,13 @@ const DOC_TYPE_EXEMPTIONS = { 'First member of a TypeScript union of view-action ids (`\'share\' | \'settings\' | ' + '\'duplicate\' | \'delete\'`) in a Schema API declaration, not a node type.', }, + 'content/docs/components/index.md': { + 'component-name': + 'Metasyntactic placeholder in the page\'s "Usage Pattern" template — the block shows the SHAPE ' + + 'every component schema has (`type` / `className` / component-specific props) and the value ' + + 'stands for whichever key the reader picked from the catalog below it. Nothing registers the ' + + 'literal string, by design.', + }, 'content/docs/core/app-schema.mdx': { item: 'AppSchema menu entry kind — a navigation item, sibling of `group`. Not a rendered node.', group: 'AppSchema menu entry kind — a navigation group holding `children` items.', @@ -299,11 +350,129 @@ const DOC_TYPE_EXEMPTIONS = { array: 'JSON Schema property type inside a field\'s `schema.properties`, not a node type.', string: 'JSON Schema property type inside a field\'s `schema.properties`, not a node type.', }, + 'content/docs/guide/architecture.md': { + 'my-grid': + 'Deliberate placeholder in the "register your component, then address it by key" contrast — ' + + 'the snippet\'s own line above spells `ComponentRegistry.register(\'my-grid\', MyGrid)`, so it ' + + 'is unregistered in this repository by design.', + string: + '`ComponentInput.type` in a `register(...)` call\'s `inputs[]` — a DESIGNER input\'s coarse ' + + 'control kind (packages/types/src/base.ts:386), sibling of `number` / `boolean` / `enum`. ' + + 'Not a node type.', + }, + 'content/docs/guide/component-registry.md': { + custom: + 'Placeholder discriminant in a "type your custom component" `interface CustomSchema extends ' + + 'BaseSchema` declaration — the page is teaching the reader to declare their own schema ' + + 'interface, so the literal is theirs to register.', + 'my-component': + 'Deliberate placeholder in the "register a custom component" walkthrough — the page registers ' + + 'this key itself (`ComponentRegistry.register(\'my-component\', MyComponent, …)`) and then ' + + 'shows the JSON that addresses it.', + string: + '`ComponentInput.type` in a `register(...)` call\'s `inputs[]` — a designer input\'s coarse ' + + 'control kind (packages/types/src/base.ts:386), not a node type.', + }, + 'content/docs/guide/console-architecture.md': { + delete: + '`ActionDef.type` passed to `useActionRunner().execute(...)` — a RunnableActionType, the ' + + 'action vocabulary declared at packages/core/src/actions/ActionRunner.ts:112. An action being ' + + 'run, not a node being rendered.', + }, + 'content/docs/guide/dashboard-filters.md': { + bar: 'Dashboard widget kind under `widgets[]`, alongside `line` — same vocabulary as the ' + + 'plugins/plugin-dashboard.mdx entry below. Not a node type.', + line: 'Dashboard widget kind under `widgets[]`, alongside `bar` — same vocabulary as the ' + + 'plugins/plugin-dashboard.mdx entry below. Not a node type.', + }, 'content/docs/guide/objectos-integration.mdx': { 'my-custom-widget': 'Deliberate placeholder in the "register a lazy custom widget" walkthrough — the reader ' + 'supplies this key.', }, + 'content/docs/guide/plugin-development.md': { + array: + '`ComponentInput.type` in this walkthrough\'s own `register(...)` `inputs[]` — a designer ' + + 'input\'s coarse control kind (packages/types/src/base.ts:386), not a node type.', + board: + 'The walkthrough\'s OWN plugin key. This page builds `@object-ui/plugin-board` end to end, so ' + + 'every `board` here — the `BoardSchema` interface, the `register(\'board\', BoardRenderer, …)` ' + + 'call, the test fixture and the final JSON — is the key the READER registers by following the ' + + 'page. Unregistered in this repository by design.', + enum: '`ComponentInput.type` in the walkthrough\'s `inputs[]` — the coarse kind that carries an ' + + '`enum` list of allowed values, not a node type.', + string: + '`ComponentInput.type` in the walkthrough\'s `inputs[]` (packages/types/src/base.ts:386), not ' + + 'a node type.', + }, + 'content/docs/guide/plugins.md': { + module: + 'The `package.json` manifest\'s OWN `"type": "module"` field — Node\'s ESM switch, in a block ' + + 'showing the plugin package\'s manifest. Not a UI schema at all. objectui#5127 is the same ' + + 'collision measured on `objectui check`, which read every JSON file\'s root `type` as a ' + + 'component key and reported `module` as unknown in any Node project.', + 'my-feature': + 'Deliberate placeholder for the reader\'s own plugin schema — the page\'s `MyFeatureSchema ' + + 'extends BaseSchema` declaration in the "author your plugin\'s types" step.', + }, + 'content/docs/guide/record-edit-modes.md': { + picklist: + 'ObjectStack object-metadata FIELD type inside a `fields` record, alongside `text` and ' + + '`lookup` — the picker/lookup family spelling packages/core/src/utils/record-title.ts:101 ' + + 'names explicitly. A field\'s data type, not a node type.', + }, + 'content/docs/guide/schema-overview.md': { + action: + 'ActionSchema discriminant in a `const action: ActionSchema = { … }` declaration — this page ' + + 'tours each schema family by declaring one of each, so the literal is the document\'s own ' + + 'discriminant. Same vocabulary as core/enhanced-actions.mdx.', + block: + 'BlockSchema discriminant in a `const block: BlockSchema = { … }` declaration — ' + + 'packages/types/src/blocks.ts, validated by zod/blocks.zod.ts. A block definition is not a ' + + 'rendered node.', + group: 'AppSchema menu entry kind — a navigation group holding `children` items, same ' + + 'vocabulary as core/app-schema.mdx.', + item: 'AppSchema menu entry kind — a navigation item, sibling of `group`. Same vocabulary as ' + + 'core/app-schema.mdx. Not a rendered node.', + string: + 'BlockVariable.type in the BlockSchema tour\'s `variables[]` — a variable declaration\'s data ' + + 'type, next to `name` / `defaultValue`.', + theme: + 'ThemeSchema discriminant in a `const theme: ThemeComponentSchema = { … }` declaration — ' + + 'packages/types/src/theme.ts declares the theme document\'s own `type`, validated by ' + + 'zod/theme.zod.ts. Same vocabulary as core/theme-schema.mdx.', + }, + 'content/docs/guide/schema-playground.md': { + reset: + 'ActionSchema discriminant under a form\'s `actions[]`, alongside `submit` — an action ' + + 'definition in a list, not a rendered child. Same vocabulary as blocks/authentication.mdx.', + submit: + 'ActionSchema discriminant under a form\'s `actions[]`, alongside `reset` — an action ' + + 'definition in a list, not a rendered child. Same vocabulary as blocks/authentication.mdx.', + }, + 'content/docs/guide/schema-rendering.md': { + 'admin-panel': + 'Stand-in for one of the READER\'s own registered components in the "move logic to ' + + 'expressions" pattern block, whose subject is `visibleOn` — the two nodes exist to be shown ' + + 'and hidden, and nothing about the pattern depends on which components they are. Weaker than ' + + 'the `my-component` placeholder above it, which the same page registers in its own snippet: ' + + 'these two are never registered on the page, so the name alone does not announce that they ' + + 'are the reader\'s. Recorded here as the disclosed cost of leaving the block\'s subject alone.', + 'my-component': + 'Deliberate placeholder — the snippet\'s own line above spells ' + + '`ComponentRegistry.register(\'my-component\', MyComponent)`, then shows the schema that ' + + 'addresses it.', + 'user-panel': + 'Stand-in for one of the READER\'s own registered components in the `visibleOn` pattern block, ' + + 'the `${!user.isAdmin}` half of the pair — see the `admin-panel` entry above for the full ' + + 'reason and its known weakness.', + }, + 'content/docs/plugins/index.md': { + 'plugin-component-name': + 'Metasyntactic placeholder in the page\'s "Usage Pattern" template — the block shows the shape ' + + 'every plugin node has and the value stands for whichever plugin key the reader picked from ' + + 'the table above it. Nothing registers the literal string, by design.', + }, 'content/docs/plugins/plugin-dashboard.mdx': { bar: 'Dashboard widget kind under `widgets[]`, alongside `line`. Not a node type.', line: 'Dashboard widget kind under `widgets[]`, alongside `bar`. Not a node type.', @@ -694,7 +863,7 @@ export function deriveRegistryKeys(root, options = {}) { */ export function scanDocs(root) { const docsDir = join(root, DOCS_ROOT); - const files = walkFiles(docsDir, (f) => f.endsWith('.mdx')).sort(); + const files = walkFiles(docsDir, (f) => DOC_EXTENSIONS.some((ext) => f.endsWith(ext))).sort(); const sites = []; const counters = { files: files.length, codeBlocks: 0, typeSites: 0 }; @@ -837,7 +1006,7 @@ const HINTS = { 'stale-indirect-registration': 'An INDIRECT_REGISTRATIONS entry no longer resolves to keys, so the universe lost them silently.', 'unterminated-code-fence': - 'An mdx file has an unclosed ``` fence. The scan cannot separate code from prose past that point.', + 'A doc file has an unclosed ``` fence. The scan cannot separate code from prose past that point.', }; const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); @@ -875,7 +1044,8 @@ if (invokedDirectly) { } console.log( - `Scanned ${counters.files} mdx file(s), ${counters.codeBlocks} code block(s), ` + + `Scanned ${counters.files} doc file(s) (${DOC_EXTENSIONS.join(' + ')}), ` + + `${counters.codeBlocks} code block(s), ` + `${counters.typeSites} \`type\` literal(s) against ${counters.registryKeys} registered key(s) ` + `derived from ${counters.sourceFiles} source file(s) (${counters.resolved} resolved call site(s), ` + `${counters.indirect} indirect, ${counters.open} open): ` +