Skip to content

Commit d3fd297

Browse files
feat(scripts): judge JSON doc fences through the strict authoring face (objectui#5250 slice B) (#12127)
Part of #5250 Clause-②: no Slice B of the objectui#5250 programme: the JSON-fence gate the maintainer ruling (comment 5534418546, 2026-09-04, decision batch #25, option 2) orders "on top of" the strict authoring face, plus the docs repairs it cannot land without. Slice A (objectui#11069, objectui#12054, objectui#12050) moved `objectui validate` and `objectui check` onto `StrictAnyComponentSchema`. This PR runs that same face over every json/jsonc node document in `content/docs`. The rendering face's `.passthrough()` is not touched, and nothing under `packages/` changes. This card stays open: slice C (the non-docs class (i) documents) is a separate PR. ## What changed 1. **New gate `scripts/check-doc-json-fences.mjs`** (`pnpm check:doc-json`), a sibling script rather than a branch inside the ts/tsx gate, because its only precondition is a built `@object-ui/types`, not the 34-package closure the ts/tsx gate needs. - Verdict: `StrictAnyComponentSchema.safeParse(document).success`, read from the BUILT `@object-ui/types` `./zod` entry. That is the module `@object-ui/cli` imports for `validateAuthoredDocument`, whose whole body is the same call. So the docs gate and `objectui validate` cannot disagree about a document. A test runs the CLI's own door over the corpus and asserts identical verdicts. - Population: the measurement script's node-document predicate (json/jsonc fence, parses, plain object, string `type`, `type` names an arm of `AnyComponentSchema`), with two corrections, both toward reading more. A `jsonc` fence is read with `jsonc-parser` and the options `objectui check` uses, because the measurement's `JSON.parse` dropped every commented jsonc fence. A fence inside a blockquote is read, with the ts/tsx gate's quote rule. - Not judged, but counted per bucket and printed: not JSON, not an object, no string `type`, a `type` no node schema declares (with the types). `--list` prints every fence with its bucket. - Fragments: a deliberate fragment carries a `doc-json: fragment` marker (the MDX-comment form in `.mdx`, the HTML-comment form in `.md`) on the line above its fence. The fragment design follows objectui#5138: an explicit marker, never a silent skip. Every run re-judges the declaration: the fence must be a node document the strict face REFUSES. A marker on an accepted fence, or on a non-document, is stale. A dangling marker, a short reason, or a verification claim (the ts/tsx gate's `VERIFICATION_CLAIM`) is a finding. The namespace is `doc-json:`, not `doc-snippet:`, because the ts/tsx gate reports its own marker above any non-ts fence as stale. - Controls, before any verdict: RESOLUTION (the face resolves inside `packages/types/dist/`), REFUSES (one undeclared key: strict refuses it, tolerant accepts it), ACCEPTS (the declared-only twin is accepted), and ANTI-IDLE (at least one judged document, and the anchor page `content/docs/blocks/forms.mdx` contributes one). - Exit codes: the sibling gates' three: 0 clean, 1 findings, 2 could not run. - Scan surface: `content/docs` only, which is the population the measurement priced. The header and every summary say that pages outside it are not read. 2. **Wiring:** the last step of `.github/workflows/doc-snippet-types.yml`, after the `--build-filter` build. That closure already carries `@object-ui/types...`, so no build step is added. One `package.json` script line. 3. **Docs repairs:** 14 pages under `content/docs`, re-derived on the dispatch base. Each refused fence is repaired or declared, as tabled below. 4. **Edits outside the claim's file surface,** each forced by an existing pin or by a statement this change makes false (deviations, declared): - `content/docs/guide/ci-cd-pipeline.md`: the command-parity sweep in `scripts/__tests__/ci-cd-pipeline-doc.test.ts` requires the workflow's new command to be named on the page. Its table row also said "two gates, one job", which is now three. - `scripts/check-doc-component-types.mjs`: eight `DOC_TYPE_EXEMPTIONS` entries pointed at `type` literals that only the removed snippets spelled. That gate reports such entries as stale, so they are deleted with their sites, each with a one-line note. - `scripts/check-doc-snippet-types.mjs`: four more exports (`quoteDepthOf`, `stripQuotePrefix`, `isBlankAtDepth` and `MIN_REASON_LENGTH`), so that both gates read quoted fences and fragment reasons by one rule. Also its header item "schema-key validity ... left unruled on purpose", which this PR makes false for json fences, now points at the sibling. ## Population on the dispatch base 29b9949 - **Measurement predicate** (JSON.parse, no blockquotes): 231 json/jsonc fences, 185 node documents, 26 refused. Of those, 13 are strict-only and 13 are red on both faces; 8 are refused naming no key. These are the same numbers the measurement report recorded at `47b1f0b`. - **This gate's predicate:** 233 fences, 191 node documents, 29 refused. Two more come from quoted fences in `api/schema-reference.md`, both accepted. Four more come from commented jsonc fences: `fields/location.mdx` is accepted, and three are refused (listed below). - **After the repairs** (head of this PR): 227 fences, 185 judged, 179 accepted, 6 declared fragments (each confirmed refused), 0 refused. Not judged: 32 with no string `type` and 10 with a `type` no node schema declares. Instrument: the gate itself, plus a throwaway copy of the measurement predicate for the base reading. ## H3: classification of the red-on-both documents Cited by page and section heading, never by line, per AGENTS.md section 5 rule #11. | page, section | type | classification | disposition | |:--|:--|:--|:--| | `api/schema-reference.md`, DetailSchema | `detail` | teaching error: field `type: "email"` is outside the detail field vocabulary | repaired: `text` | | `components/basic/icon.mdx`, "`name` is identity, not the glyph" | `icon` | deliberate refused example: the page states it is refused | declared | | `components/index.md`, Example: Button | `button` | teaching error: `size: "md"` is not a declared size | repaired: `default` | | `guide/dashboard-filters.md`, Step 3 (`optionsFrom`) | `select` (collision) | not a node: a `globalFilters[]` member whose `type` is the filter's input type | declared | | `guide/expressions.md`, Index in Loops | `list` | teaching error: no item template exists, and `items` must be an array | section rewritten (see below) | | `guide/expressions.md`, Dependent Fields | `form` | teaching error: a form takes `fields`, and select options are `{label, value}` objects | repaired | | `guide/schema-playground.md`, Form | `form` | teaching error: string options, field `defaultValue`, and form `title` | repaired | | `guide/schema-playground.md`, Grid | `grid` | teaching error: a data table taught under the layout `grid` type | rewritten as `data-table` | | `guide/schema-playground.md`, Composing Schemas | `page` | teaching error: grid `gap: "md"` | repaired: `4` | | `utilities/runner.mdx`, 1. Plugin Development | `page` | deliberate: the child is the reader's own plugin type | declared | | `utilities/runner.mdx`, 3. Integration Testing | `div` | deliberate: a type-only skeleton | declared | | `utilities/vscode-extension.mdx`, Snippets, `objectui-form` | `form` | teaching error: a snippet the extension never shipped, and `onSubmit` is a runtime slot | section rewritten from the shipped snippets | | `utilities/vscode-extension.mdx`, Snippets, `objectui-grid` | `data-table` | teaching error: a snippet the extension never shipped | section rewritten | | `guide/expressions.md`, Scoped Data (jsonc, newly read) | `list` | teaching error: as Index in Loops | section rewritten | | `utilities/runner.mdx`, Add Custom Schemas (jsonc, newly read) | `page` | deliberate: the reader's `my-component` | declared | None of the 15 is exempted silently. Five are declared with a written reason, and the gate confirms each declared fence is refused. The sixth declaration is the Validation fence in the next table. ## Strict-only pairs: per-pair disposition and runtime evidence **Instrument:** a throwaway dom-project test, never committed. It renders the node through the real `SchemaRenderer` and the real registry (`packages/components/src/renderers`), once with the key and once without it, and compares the normalised `document.body` HTML. **Controls:** each control changes the output, so the instrument can see a read key: `button.label`, `empty.title`, `input.inputType`, and `form.defaultValues`. | page, section | key | disposition | runtime reading | |:--|:--|:--|:--| | `blocks/authentication.mdx` Wire Up Submission; `blocks/ecommerce.mdx` Add to Cart Action; `blocks/forms.mdx` Add Submit Action | `button.action` | replaced: an `action:button` node with `properties.{label, actionType: "api", target, method}` (AGENTS.md section 5 rule #4; the same form as `guide/schema-rendering.md`'s api example; `executeAPI` reads `target` and `method`) | identical HTML | | `blocks/dashboard.mdx` Add Actions | `button.action` (navigate) | replaced: `action:button`, `actionType: "url"`, `target` (`executeUrl` reads `target`) | identical HTML | | `blocks/marketing.mdx` Add Analytics | `button.action` (analytics) | subsection removed: no `analytics` executor exists (the built-ins are script, url, modal, flow, api, form and navigation) | identical HTML | | `blocks/dashboard.mdx` Add Real Data; `blocks/ecommerce.mdx` Add Product Data | `card.dataSource` | replaced: a data-bound block (`element:number` and `object-grid` with the per-element `dataSource` binding, verbatim from `guide/data-source.md` and `utilities/data-objectstack.mdx`). `SchemaRenderer` strips `dataSource` from the props it spreads | identical HTML | | `guide/expressions.md` Empty State; `guide/schema-rendering.md` Empty States | `empty.message` | replaced: `title`, which the empty renderer reads (control: `title` changes the heading) | differs only by a stray `message` DOM attribute from the props spread | | `guide/expressions.md` Dynamic Validation (now Validation) | `input.validations` | replaced: `inputType: "email"` (control: it changes the input's `type`) | identical HTML | | `guide/schema-playground.md` Card | `card.icon` | removed | differs only by a stray `icon` DOM attribute | | `utilities/runner.mdx` Dashboard Example | `card.value`, `card.change` (x3) | replaced: `children` (a text node) and `description` | differs only by stray `value` / `change` DOM attributes | | `utilities/vscode-extension.mdx` Snippets, `objectui-app` | `app.pages` | section rewritten from `packages/vscode-extension/snippets/objectui.json` (the real `oui-*` prefixes) | identical HTML | | `utilities/vscode-extension.mdx` Validation (jsonc, newly read) | `button.invalidProp` | rewritten: the extension reports no unknown property (its JSON schema is `additionalProperties: true`, and `SchemaValidator` checks only `inputType`, `variant` and labels). The fence now shows the `variant` warning it really emits, and is declared as a deliberately refused node | not applicable | **Red-on-both repairs, probed the same way:** - `list.itemTemplate`: differs only by an `itemtemplate` DOM attribute. - `form.title`: differs only by the HTML `title` attribute on the `form` element, so the page now says a form draws no heading. - `grid` `title` / `rows` / `actions` / `pagination`: identical. - `data-table` `dataSource` and `columns[].key` / `title`: identical. - `form.onSubmit` (object): identical. - `form` `fields[].defaultValue` on a select field: identical. It was replaced by the form's `defaultValues`, which the control shows the form reads. ## Proofs - **Reverse leg.** With the 14 repaired pages reverted to `29b99490` (`git checkout 29b9949 -- PAGES`), the gate exits 1 with 29 findings. Restored with `git checkout HEAD -- PAGES`; `git diff HEAD` is empty afterwards. - **Ablation 1.** Prediction, written before the run: exit 2, REFUSES. `judge` was pointed at `AnyComponentSchema` through objectstack's `scripts/ablation-replace.mjs` (anchor 1 -> 0, blob changed). Result: exit 2, "REFUSES: the strict face accepted a document carrying an undeclared key". Restored: blob == HEAD, `git diff HEAD` empty. - **Ablation 2.** Prediction: exit 2, anti-idle. `JSON_FENCE_LANGUAGES` was emptied of `json`/`jsonc`. Result: exit 2, 0 judged documents, anchor-unread. Restored: blob == HEAD. The run also showed that the summary line still printed "anchor ... read", which the last commit corrects. ## Gates (HEAD `3c0d3123`) | command | exit | |:--|:--| | `node scripts/check-doc-json-fences.mjs` | 0 | | locked `pnpm type-check:scripts` plus `vitest run` over the gate's tests and every test that reads an edited page or the workflow (40 files) | 0, "Test Files 40 passed (40)", "Tests 1926 passed (1926)" | | `pnpm check:doc-types` | 0 | | `pnpm check:doc-fences` | 0 | | `pnpm docs:check-links` | 0, "Links are valid across 17 scan roots" | | `pnpm check:new-line-citations` | 0, "0 new citation(s)" (one fixture address declared) | | `pnpm check:control-bytes` | 0 | | `node scripts/check-doc-expression-carriage.mjs` | 0 (report-only) | | `pnpm check:test-path-roots` | 0 | | `pnpm lint:root` | 0, 0 errors, 34 warnings, none in a touched file | | `node scripts/check-changeset-presence.mjs` | 0, "no changeset is owed", so none is added | | `node scripts/check-governed-queue-guard.mjs --test` over the 21 paths | 0, NOT GOVERNED | | `pnpm check:doc-snippets` / `pnpm check:doc-examples` | NOT MEASURED: they need the 34-package build. No ts/tsx fence changed; the ts/tsx gate's edit is an export list and one header item, and its own test file is in the green run above. Declared to CI | ## Acceptance notes - **Class (ii) finding, not repaired here.** `form` `fields[].defaultValue` is read at runtime on a TEXT field: the probe renders `value="zzz"` with it and nothing without it. It is inert on a select field, which is the case this PR repaired. The strict face refuses it, so `objectui validate` tells an author to remove a key that works on text fields. Reported in the card report; no declaration is made here. - **Scan surface.** Pages outside `content/docs` are not read. At this base the strict face refuses four json node documents there: two in the root `README.md`, one in `packages/plugin-dashboard/README.md`, and one in a dated audit under `docs/audits/`. Widening the surface needs those repaired or ledgered first. Reported, not done here. - **Earlier leads.** `guide/dashboard-filters.md` was listed as a class (i) carrier in the earlier reports. It is not a node at all: its `type: "select"` is a filter input type that collides with a node type. The session that produced this change is `https://claude.ai/code/session_01DwLS3LzXyNmyTunMGbpc8W`. --- _Generated by [Claude Code](https://claude.ai/code/session_01DwLS3LzXyNmyTunMGbpc8W)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 802f0bc commit d3fd297

22 files changed

Lines changed: 1218 additions & 219 deletions

‎.github/workflows/doc-snippet-types.yml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,15 @@ jobs:
185185
# narrowing what the gate scans.
186186
- name: Compile JSDoc `@example` blocks against the built types
187187
run: node scripts/check-doc-example-types.mjs
188+
189+
# objectui#5250 (the maintainer ruling's JSON-fence slice). Every `json` /
190+
# `jsonc` node document in `content/docs` is judged through the STRICT
191+
# authoring face — `StrictAnyComponentSchema`, read from the
192+
# `@object-ui/types` the build step above produced (the snippet gate's
193+
# closure carries it) — the face `objectui validate` judges with. A fence
194+
# that is deliberately not a complete node document is declared on the
195+
# line above it, and the declaration is re-judged on every run. Last of
196+
# the three, for the reason the step above gives: a newly wired blocking
197+
# gate must not mask an established one.
198+
- name: Judge JSON doc fences through the strict authoring face
199+
run: node scripts/check-doc-json-fences.mjs

‎content/docs/api/schema-reference.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ A single-record detail view with grouped fields, actions, and tabs.
710710
"title": "Customer Info",
711711
"fields": [
712712
{ "name": "customer", "label": "Customer", "type": "text" },
713-
{ "name": "email", "label": "Email", "type": "email" },
713+
{ "name": "email", "label": "Email", "type": "text" },
714714
{ "name": "created", "label": "Created", "type": "date", "format": "MMM d, yyyy" }
715715
]
716716
},

‎content/docs/blocks/authentication.mdx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ behaviour. Common customisations:
5454

5555
### Wire Up Submission
5656

57+
A control that runs something is its own node type, `action:button`, and its
58+
props go in the `properties` bag; `actionType: "api"` sends the request to
59+
`target`:
60+
5761
```json
5862
{
59-
"type": "button",
60-
"label": "Sign In",
61-
"action": {
62-
"type": "submit",
63-
"endpoint": "/api/auth/login",
63+
"type": "action:button",
64+
"properties": {
65+
"label": "Sign In",
66+
"actionType": "api",
67+
"target": "/api/auth/login",
6468
"method": "POST"
6569
}
6670
}

‎content/docs/blocks/dashboard.mdx‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ Customize dashboard blocks for your needs:
3333

3434
### Add Real Data
3535

36+
A `card` fetches nothing. A live figure comes from a data-bound block: an
37+
`element:number` takes its object from the per-element `dataSource` binding and
38+
its aggregate from its `properties` bag (see the
39+
[Data Source guide](/docs/guide/data-source)):
40+
3641
```json
3742
{
38-
"type": "card",
39-
"dataSource": {
40-
"api": "/api/stats/revenue",
41-
"method": "GET"
42-
}
43+
"type": "element:number",
44+
"dataSource": { "object": "order" },
45+
"properties": { "aggregate": "count" }
4346
}
4447
```
4548

@@ -55,13 +58,16 @@ Customize dashboard blocks for your needs:
5558

5659
### Add Actions
5760

61+
A control that runs something is its own node type, `action:button`, and its
62+
props go in the `properties` bag; `actionType: "url"` navigates to `target`:
63+
5864
```json
5965
{
60-
"type": "button",
61-
"label": "View Details",
62-
"action": {
63-
"type": "navigate",
64-
"path": "/dashboard/details"
66+
"type": "action:button",
67+
"properties": {
68+
"label": "View Details",
69+
"actionType": "url",
70+
"target": "/dashboard/details"
6571
}
6672
}
6773
```

‎content/docs/blocks/ecommerce.mdx‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,30 @@ Customize e-commerce blocks for your store:
3939

4040
### Add Product Data
4141

42+
A `card` fetches nothing. Records come from a data-bound block that names its
43+
object in the per-element `dataSource` binding (see the
44+
[Data Source guide](/docs/guide/data-source)):
45+
4246
```json
4347
{
44-
"type": "card",
45-
"dataSource": {
46-
"api": "/api/products",
47-
"method": "GET"
48-
}
48+
"type": "object-grid",
49+
"dataSource": { "object": "product", "limit": 20 }
4950
}
5051
```
5152

5253
### Add to Cart Action
5354

55+
A control that runs something is its own node type, `action:button`, and its
56+
props go in the `properties` bag; `actionType: "api"` sends the request to
57+
`target`:
58+
5459
```json
5560
{
56-
"type": "button",
57-
"label": "Add to Cart",
58-
"action": {
59-
"type": "submit",
60-
"endpoint": "/api/cart",
61+
"type": "action:button",
62+
"properties": {
63+
"label": "Add to Cart",
64+
"actionType": "api",
65+
"target": "/api/cart",
6166
"method": "POST"
6267
}
6368
}

‎content/docs/blocks/forms.mdx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,17 @@ or in a TypeScript-authored schema pass the real thing —
9393

9494
### Add Submit Action
9595

96+
A control that runs something is its own node type, `action:button`, and its
97+
props go in the `properties` bag; `actionType: "api"` sends the request to
98+
`target`:
99+
96100
```json
97101
{
98-
"type": "button",
99-
"label": "Submit",
100-
"action": {
101-
"type": "submit",
102-
"endpoint": "/api/contact",
102+
"type": "action:button",
103+
"properties": {
104+
"label": "Submit",
105+
"actionType": "api",
106+
"target": "/api/contact",
103107
"method": "POST"
104108
}
105109
}

‎content/docs/blocks/marketing.mdx‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,6 @@ Customize marketing blocks for your campaigns:
4646
}
4747
```
4848

49-
### Add Analytics
50-
51-
```json
52-
{
53-
"type": "button",
54-
"label": "Get Started",
55-
"action": {
56-
"type": "analytics",
57-
"event": "cta_clicked"
58-
}
59-
}
60-
```
61-
6249
### Customize Content
6350

6451
```json

‎content/docs/components/basic/icon.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The glyph is named by `icon`:
3434
`name` is the SDUI identity key every node carries, alongside `id`. It is **not**
3535
read as a glyph name on `ui:icon`, and writing one there renders no icon:
3636

37+
{/* doc-json: fragment — a deliberately refused node: it shows the retired name-as-glyph shape the paragraph below describes */}
3738
```json
3839
{ "type": "icon", "id": "save_icon", "name": "save_icon" }
3940
```

‎content/docs/components/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ All ObjectUI components follow the same schema-based pattern:
128128
"type": "button",
129129
"label": "Click Me",
130130
"variant": "default",
131-
"size": "md",
131+
"size": "default",
132132
"className": "mt-4"
133133
}
134134
```

‎content/docs/guide/ci-cd-pipeline.md‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ one has its own section below.
3434
| `skill-examples.yml` | Skill Example Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a MARKED fenced example in a `skills/` or `.claude/skills/` guide no longer compiles against the packages' built types, no longer parses as JSON, uses a bare `any`, or carries a marker that opts nothing in |
3535
| `skill-eval-tokens.yml` | Skill Eval Token Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when an eval assertion's `must_contain` token is not taught as a whole token anywhere in its own `skills/` bundle |
3636
| `doc-component-types.yml` | Doc Component Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `content/docs/**.mdx` snippet teaches a `type` nothing registers |
37-
| `doc-snippet-types.yml` | Doc Snippet Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a covered documentation snippet no longer compiles against the packages' built types, **or** when a covered JSDoc `@example` block does (two gates, one job — see below) |
37+
| `doc-snippet-types.yml` | Doc Snippet Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a covered documentation snippet no longer compiles against the packages' built types, **or** when a covered JSDoc `@example` block does, **or** when a `json` / `jsonc` node document in `content/docs` is refused by the strict authoring face (three gates, one job — see below) |
3838
| `doc-fence-languages.yml` | Doc Fence Language Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a TypeScript block sits under a fence the snippet gate does not read |
3939
| `doc-example-ids.yml` | Doc Example Id Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `content/docs/**` page references a schema-catalog example id the registry does not carry, or when the gate's own population collapses |
4040
| `pre-install-import-graph.yml` | Pre-Install Import Graph Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a gate a workflow runs *before* `pnpm install` reaches a package anywhere in its import graph |
@@ -1159,7 +1159,7 @@ names: `pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --b
11591159

11601160
### JSDoc `@example` Types — the second gate in this job (`doc-snippet-types.yml`)
11611161

1162-
The same job's **last** step runs `scripts/check-doc-example-types.mjs`
1162+
The same job's second blocking step runs `scripts/check-doc-example-types.mjs`
11631163
(`pnpm check:doc-examples`). It asks the snippet gate's question of a different corpus: every fenced
11641164
`ts` / `tsx` block inside a JSDoc `@example` on an **exported** declaration under
11651165
`packages/NAME/src/**` must compile `--strict` against the packages' built `dist/*.d.ts`, or be
@@ -1174,6 +1174,7 @@ copying it out of an IDE hover got TS2322, and nothing in this repository had ev
11741174
| `pnpm check:doc-snippets` (`scripts/check-doc-snippet-types.mjs`) | fenced `ts` / `tsx` blocks in the **documents** the `UNGATED_DOCS` ledger does not exempt | **Yes** |
11751175
| `pnpm check:doc-snippets --emit-census` | code this repository's generators emit from **template literals** | No — report-only |
11761176
| `pnpm check:doc-examples` (`scripts/check-doc-example-types.mjs`) | fenced `ts` / `tsx` blocks in JSDoc **`@example`** tags on exported declarations under `packages/NAME/src/**` | **Yes** |
1177+
| `pnpm check:doc-json` (`scripts/check-doc-json-fences.mjs`) | fenced `json` / `jsonc` **node documents** in `content/docs`, judged through the strict authoring face | **Yes** |
11771178

11781179
**Why it shares this job rather than getting a workflow.** It is a *sibling* of the gate above, not a
11791180
fork: it imports that script's compiler host, its built-`.d.ts` resolution and every one of its
@@ -1211,6 +1212,43 @@ example, or declare it. Run it locally exactly as this job does: build first
12111212
(`pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter)`), then
12121213
`pnpm check:doc-examples`; the gate prints that same recipe when it exits `2`.
12131214

1215+
### JSON Node Documents — the third gate in this job (`doc-snippet-types.yml`)
1216+
1217+
The job's **last** step runs `scripts/check-doc-json-fences.mjs` (`pnpm check:doc-json`). The two
1218+
gates above read `ts` / `tsx` only, so a `json` fence was checked by nothing:
1219+
[#5229](https://github.com/objectstack-ai/objectui/issues/5229) taught a `validation` bag on an
1220+
`input` node in `content/docs/blocks/forms.mdx` while every shipped tool was green. Every fenced
1221+
`json` / `jsonc` block under `content/docs` that parses into an object whose `type` names a node
1222+
schema is judged through the **strict authoring face** — `StrictAnyComponentSchema`, read from the
1223+
`@object-ui/types` the build step above produced, the face `objectui validate` and `objectui check`
1224+
judge an authored document with ([#5250](https://github.com/objectstack-ai/objectui/issues/5250)).
1225+
So a key no schema declares fails here exactly as it fails `objectui validate`; the tolerant face
1226+
keeps it, because `BaseSchemaCore` is `.passthrough()` for the renderer.
1227+
1228+
**What it does not judge, it counts.** A fence that is not JSON, not an object, carries no string
1229+
`type`, or names a `type` no node schema declares is not judged; the summary prints each bucket, and
1230+
`--list` prints every fence with its bucket. Pages outside `content/docs` are not read, and the
1231+
summary says so.
1232+
1233+
**Fragments are declared, and the declaration is re-judged.** A node document that is deliberately
1234+
not complete or not valid — a refused example that shows the error, an excerpt that elides required
1235+
keys, an object whose `type` only collides with a node type — carries
1236+
`{/* doc-json: fragment - why */}` (the HTML-comment form in `.md`) on the line above its fence. The
1237+
declared fence must be one the strict face refuses: a declaration on a fence the face accepts, or on
1238+
one that is not a node document at all, is reported as stale, so the declarations can only shrink.
1239+
1240+
**It proves itself before it judges the docs.** The face must resolve inside `packages/types/dist/`;
1241+
a document carrying an undeclared key must be refused by the strict face and accepted by the
1242+
tolerant one; a declared-only document must be accepted; and the walk must judge at least one node
1243+
document, including one from its anchor page. Its exit codes are the sibling gates' three: `0`
1244+
clean, `1` the gate ran and found errors, `2` it **could not run** — `@object-ui/types` unbuilt, or
1245+
a control or the enumeration failed — which is neither green nor red.
1246+
1247+
**If it fails:** each refusal names the page, the line of its fence, and each reason — an undeclared
1248+
key by name and path, anything else by the face's own message. Remove the key, or replace it with
1249+
the declared key its renderer reads; declare a deliberate fragment. Run it locally with
1250+
`pnpm --filter @object-ui/types build`, then `pnpm check:doc-json`.
1251+
12141252
## Fence Languages (`doc-fence-languages.yml`)
12151253

12161254
**Triggers:** Push and PR to `main`/`develop`, merge-queue builds, plus manual dispatch — **no path

0 commit comments

Comments
 (0)