diff --git a/.changeset/10088-json-fences-parse.md b/.changeset/10088-json-fences-parse.md new file mode 100644 index 0000000000..a9380625b6 --- /dev/null +++ b/.changeset/10088-json-fences-parse.md @@ -0,0 +1,4 @@ +--- +--- + +Docs and test-only change; no published behaviour changes. Every `json` fence under `content/docs` now survives `JSON.parse`, and a fence that annotates JSON with comments is tagged `jsonc`. The one source file touched is a test in `@object-ui/types`: `page-actions-refusal-7926.test.ts` pinned four unparseable `json` fences in `guide/layout.md` as its blind spot, and that count is zero now. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 476bbddd4c..9b42cd0411 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -427,6 +427,8 @@ The repository root also has a `docs/` directory, and it is **not** part of the **A code block inside one of those records is a SPECIMEN, not an example to copy.** An ADR states what was decided on a date and a dated audit states what was measured on one, so a snippet inside either is part of the record — edited until it compiles, it makes the record say something it never said. Fence such a block `plaintext` (an unhighlighted spelling `scripts/check-doc-fence-languages.mjs` already lists, and one `scripts/check-doc-snippet-types.mjs` does not compile), never `ts` / `tsx` / `typescript`. Code a reader may copy belongs in `content/docs/**` or `skills/objectui/**`, where a gate compiles it and a wrong line can be fixed without falsifying a record. Maintainer ruling 2026-09-08 (objectui#8363); the four records that predate it are not edited — they are named, with their measured counts, in that gate's `UNGATED_DOCS` ledger. +**A `json` fence is what a reader copies into a metadata file, so its body must pass a strict `JSON.parse`.** A block that annotates JSON with `//` or `/* … */` comments or trailing commas is fenced `jsonc` instead, and it must still be one JSON document once those are removed. The tag is how a reader tells a deliberate annotation from a mistake, and a `json` fence that does not parse is always the mistake. The other non-parsing shapes are admitted by no JSON dialect, so rewrite them rather than retag them. A `${…}` expression goes on one line inside its string, because a raw newline in a string is invalid under both tags. A bad/good comparison becomes one fence per document, with its label on the line above. An elision (`...`, `[...]`, a `"..."` member) is removed, which leaves an empty list or the bare node. A counter-example still parses: what is wrong with it belongs in its content, not its syntax. This is the contract `parseJsonFence` in `scripts/check-skill-examples.mjs` enforces on the skills tree's marked fences. ⚠️ Nothing enforces it for `content/docs` today. `scripts/check-doc-expression-carriage.mjs` reads these fences but normalises comments, raw newlines and elisions away by design, and whether a gate should block a `json` fence that does not parse is the open decision objectui#10943. + ```bash # Start the documentation site dev server pnpm site:dev diff --git a/content/docs/guide/deployment.md b/content/docs/guide/deployment.md index a169552382..8dfd9b81d3 100644 --- a/content/docs/guide/deployment.md +++ b/content/docs/guide/deployment.md @@ -274,7 +274,7 @@ HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD wget -qO- http://localhost:80/health.json || exit 1 ``` -```json +```jsonc // railway.json (excerpt) { "deploy": { diff --git a/content/docs/guide/expressions.md b/content/docs/guide/expressions.md index 8bc5b2ecbe..10f1eba08f 100644 --- a/content/docs/guide/expressions.md +++ b/content/docs/guide/expressions.md @@ -220,7 +220,7 @@ The scope the evaluator builds is what you published, plus two names the rendere Some components provide scoped data: -```json +```jsonc { "type": "list", "items": "${users}", @@ -337,12 +337,7 @@ Available: All standard `Math` functions ```json { "type": "text", - "content": "${ - status === 'active' ? 'Active' : - status === 'pending' ? 'Pending review' : - status === 'error' ? 'Failed' : - 'Unknown' - }" + "content": "${status === 'active' ? 'Active' : status === 'pending' ? 'Pending review' : status === 'error' ? 'Failed' : 'Unknown'}" } ``` @@ -351,11 +346,7 @@ Available: All standard `Math` functions ```json { "type": "alert", - "visibleOn": "${ - (user.role === 'admin' || user.role === 'moderator') && - user.isActive && - !user.isSuspended - }" + "visibleOn": "${(user.role === 'admin' || user.role === 'moderator') && user.isActive && !user.isSuspended}" } ``` @@ -364,12 +355,7 @@ Available: All standard `Math` functions ```json { "type": "text", - "content": "${ - users - .filter(u => u.isActive) - .map(u => u.name) - .join(', ') - }" + "content": "${users.filter(u => u.isActive).map(u => u.name).join(', ')}" } ``` @@ -380,11 +366,7 @@ Available: All standard `Math` functions ```json { "type": "text", - "content": "${ - new Date().getHours() < 12 ? 'Good morning' : - new Date().getHours() < 18 ? 'Good afternoon' : - 'Good evening' - }, ${user.firstName}!" + "content": "${new Date().getHours() < 12 ? 'Good morning' : new Date().getHours() < 18 ? 'Good afternoon' : 'Good evening'}, ${user.firstName}!" } ``` @@ -526,16 +508,17 @@ evaluated on every component type: Expressions are re-evaluated when data changes. Avoid expensive operations: +❌ Bad: Complex computation in expression + ```json -// ❌ Bad: Complex computation in expression { "type": "text", "content": "${users.map(u => expensiveOperation(u)).join(', ')}" } - -// ✅ Good: Pre-compute, and publish the result ``` +✅ Good: Pre-compute, and publish the result + ```tsx @@ -670,13 +653,17 @@ language's own. A membership test is written with the array method: ### 1. Keep Expressions Simple +❌ Bad: Too complex + ```json -// ❌ Bad: Too complex { "content": "${users.filter(u => u.age > 18).map(u => ({...u, isAdult: true})).reduce((acc, u) => acc + u.score, 0)}" } +``` -// ✅ Good: Pre-compute complex logic +✅ Good: Pre-compute complex logic + +```json { "content": "${adultUsersScore}" } @@ -684,13 +671,17 @@ language's own. A membership test is written with the array method: ### 2. Use Meaningful Variable Names +❌ Bad + ```json -// ❌ Bad { "visibleOn": "${x && y || z}" } +``` -// ✅ Good +✅ Good + +```json { "visibleOn": "${isAdmin && isActive || isSuperUser}" } @@ -698,13 +689,17 @@ language's own. A membership test is written with the array method: ### 3. Handle Null/Undefined +❌ Bad: Might throw error + ```json -// ❌ Bad: Might throw error { "content": "${user.address.city}" } +``` -// ✅ Good: Safe access +✅ Good: Safe access + +```json { "content": "${user.address?.city || 'N/A'}" } diff --git a/content/docs/guide/layout.md b/content/docs/guide/layout.md index c56fd060f9..570a150066 100644 --- a/content/docs/guide/layout.md +++ b/content/docs/guide/layout.md @@ -238,10 +238,10 @@ Control page content width: { "type": "page", "title": "Settings", - "maxWidth": "lg", // Centered content with max width + "maxWidth": "lg", "children": { "type": "form", - "fields": [...] + "fields": [] } } ``` @@ -515,17 +515,17 @@ Omit `sidebar` and the content fills the width under the top bar. { "label": "General", "value": "general", - "content": { "type": "form", "fields": [...] } + "content": { "type": "form", "fields": [] } }, { "label": "Security", "value": "security", - "content": { "type": "form", "fields": [...] } + "content": { "type": "form", "fields": [] } }, { "label": "Notifications", "value": "notifications", - "content": { "type": "form", "fields": [...] } + "content": { "type": "form", "fields": [] } } ] } @@ -660,11 +660,11 @@ Control page content padding: ```json { "type": "page", - "padding": false, // Remove default padding + "padding": false, "children": { "type": "container", - "className": "p-8", // Custom padding - "children": [...] + "className": "p-8", + "children": [] } } ``` @@ -743,10 +743,10 @@ Use constrained width for forms and reading content: ```json { "type": "page", - "maxWidth": "lg", // Better for forms + "maxWidth": "lg", "children": { "type": "form", - "fields": [...] + "fields": [] } } ``` diff --git a/content/docs/guide/schema-rendering.md b/content/docs/guide/schema-rendering.md index fd652b4a3f..65da7d502a 100644 --- a/content/docs/guide/schema-rendering.md +++ b/content/docs/guide/schema-rendering.md @@ -277,11 +277,7 @@ it on `label`: `text` is not a `BadgeSchema` key. "title": "Welcome!", "children": { "type": "text", - "content": "${ - user.isNew ? 'Start with the quick tour.' : - user.tasks.length === 0 ? 'You are all caught up.' : - 'You have tasks waiting.' - }" + "content": "${user.isNew ? 'Start with the quick tour.' : user.tasks.length === 0 ? 'You are all caught up.' : 'You have tasks waiting.'}" } } ``` diff --git a/content/docs/guide/troubleshooting.md b/content/docs/guide/troubleshooting.md index 676010ff12..cbee49ce3b 100644 --- a/content/docs/guide/troubleshooting.md +++ b/content/docs/guide/troubleshooting.md @@ -366,14 +366,21 @@ two different fields: the row fetch asks the server for one field while the grid renders another, so the cell has nothing behind it and the relation is never expanded. -```jsonc -// ✗ two identities on one column +✗ two identities on one column + +```json { "field": "account", "name": "account_name" } +``` + +✗ legacy-only: the renderer shows it, the request used to drop it -// ✗ legacy-only: the renderer shows it, the request used to drop it +```json { "name": "account" } +``` + +✓ canonical -// ✓ canonical +```json { "field": "account" } ``` diff --git a/content/docs/plugins/index.md b/content/docs/plugins/index.md index bc39d0f1e5..fc3754833f 100644 --- a/content/docs/plugins/index.md +++ b/content/docs/plugins/index.md @@ -352,7 +352,7 @@ Now all plugin components are available in your schemas! All ObjectUI plugins follow the same usage pattern: -```json +```jsonc { "type": "plugin-component-name", "className": "tailwind-classes" diff --git a/content/docs/utilities/runner.mdx b/content/docs/utilities/runner.mdx index 36ee17ef50..07844b9349 100644 --- a/content/docs/utilities/runner.mdx +++ b/content/docs/utilities/runner.mdx @@ -211,8 +211,10 @@ strict pnpm workspace.) then re-run `pnpm install` from the repo root: ```json -"dependencies": { - "@object-ui/plugin-yourplugin": "workspace:*" +{ + "dependencies": { + "@object-ui/plugin-yourplugin": "workspace:*" + } } ``` @@ -243,7 +245,7 @@ import '@object-ui/plugin-yourplugin' 5. **Use in schemas:** -```json +```jsonc { "type": "your-component" // Your component's keys go here, on the node itself — renderers read @@ -293,9 +295,9 @@ Test how multiple plugins work together: { "type": "div", "children": [ - { "type": "object-kanban", "..." }, - { "type": "bar-chart", "..." }, - { "type": "data-table", "..." } + { "type": "object-kanban" }, + { "type": "bar-chart" }, + { "type": "data-table" } ] } ``` @@ -493,7 +495,7 @@ ComponentRegistry.register('my-component', MyComponent) Add a page document to `src/app-data/pages/`. The file name is the route: this one is served at `/my-page`. -```json +```jsonc // src/app-data/pages/my-page.json { "type": "page", diff --git a/content/docs/utilities/vscode-extension.mdx b/content/docs/utilities/vscode-extension.mdx index 926001e66e..e43c3ca049 100644 --- a/content/docs/utilities/vscode-extension.mdx +++ b/content/docs/utilities/vscode-extension.mdx @@ -184,7 +184,7 @@ Hover over properties to see: Real-time validation with error messages: -```json +```jsonc { "type": "button", "invalidProp": true // ❌ Error: Unknown property diff --git a/packages/types/src/__tests__/page-actions-refusal-7926.test.ts b/packages/types/src/__tests__/page-actions-refusal-7926.test.ts index b8d4eb96c3..746a54685a 100644 --- a/packages/types/src/__tests__/page-actions-refusal-7926.test.ts +++ b/packages/types/src/__tests__/page-actions-refusal-7926.test.ts @@ -180,7 +180,7 @@ describe('objectui#7926 — the guide no longer authors `actions` on a `page` no try { doc = JSON.parse(m[1]); } catch { - doc = null; // elided fragment (`[...]`), counted as blind below + doc = null; // a fence that does not parse, counted as blind below } out.push({ line, doc, body: m[1] }); } @@ -221,17 +221,15 @@ describe('objectui#7926 — the guide no longer authors `actions` on a `page` no // requirement, carried into this card by its dispatch). The COUNT is pinned, // not the line numbers — a line list would redden on every unrelated edit // above it, and a permanently red pin is one nobody reads. + // + // The count was four — the page's `[...]` elisions — until objectui#10088 made + // every `json` fence on the page parse. It is zero now, so every `page` node + // the guide authors in a `json` fence is judged above, and a new fence that + // does not parse is red here rather than a fifth blind entry. ⚠️ This reader + // visits `json` only: a `jsonc` fence on this page would sit OUTSIDE its + // population, not inside this count. const blind = fences.filter((f) => f.doc === null); - expect(blind).toHaveLength(4); - // …and each is blind for the DECLARED reason, an author's `[...]` elision — - // never because a real document stopped parsing. That is the half that makes - // the count above mean something. - for (const f of blind) { - expect({ line: f.line, elided: f.body.includes('...') }).toEqual({ - line: f.line, - elided: true, - }); - } + expect(blind.map((f) => `${GUIDE_PATH}:${f.line}`)).toEqual([]); }); it('every `page` node the guide teaches declares its content under `children`', () => {