From ec67429b2a984b27307b4d23345c9c094785e0d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 07:50:13 +0000 Subject: [PATCH 1/2] docs(skills): tag the 16 comment-carrying JSON fences `jsonc` Sixteen fenced blocks under `skills/objectui/**` are tagged `json` while their bodies carry `//` comments -- syntax JSON forbids and JSONC allows -- so a reader or a gate that trusts the tag and parses the block fails. `check-skill-examples.mjs` already implements both dialects (`JSON_FENCE_LANGUAGES`, `parseJsonFence`); its header records that the corpus had zero `jsonc` fences, so the tag was the only thing missing. Tag-only, by line number, after asserting each opener is exactly the six bytes of the `json` info string: 11 in guides/schema-expressions.md, 4 in rules/protocol.md, 1 in rules/styling.md. +1 byte per fence, +16 bytes total, zero line-count change, and the whole diff is 16 opener lines. Seven of the sixteen now parse under the repository's own JSONC dialect; the other nine stay unparseable for a reason a tag cannot fix (seven list several top-level documents in one fence, two carry `...` elisions), which is recorded on objectui#7462 rather than repaired here. Ref: objectui#7462 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- skills/objectui/guides/schema-expressions.md | 22 ++++++++++---------- skills/objectui/rules/protocol.md | 8 +++---- skills/objectui/rules/styling.md | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/skills/objectui/guides/schema-expressions.md b/skills/objectui/guides/schema-expressions.md index caecb6660b..3773413fc1 100644 --- a/skills/objectui/guides/schema-expressions.md +++ b/skills/objectui/guides/schema-expressions.md @@ -29,7 +29,7 @@ full boundary tables -- the evaluated fields, the raw ones, `visible` over -- are in [`rules/protocol.md`](../rules/protocol.md), which is the anchor for this rule. The four cases that decide most schemas: -```json +```jsonc // Evaluated, then dropped -- renders an empty card { "type": "card", "props": { "title": "${data.customer.name}" } } @@ -167,7 +167,7 @@ name, so calls are case-insensitive. Each condition field has two forms — a shorthand and an `On` suffix: -```json +```jsonc { "hidden": true } // static boolean { "hidden": "${data.role !== 'admin'}" } // template expression { "hiddenOn": "data.role !== 'admin'" } // raw expression (no ${} needed) @@ -325,7 +325,7 @@ what reaches the screen. `list` is the component authors reach for first, and it is **data-as-nodes**: the array it renders *is* the node list. It never reads `children`. -```json +```jsonc // ❌ Renders two EMPTY
  • . `children` is not a template — `list` never reads it, // and `${item.name}` would render literally even if it did. { @@ -352,7 +352,7 @@ descriptor: section exists to close: binding `list` to ordinary records produces one empty `
  • ` per record — the right number of bullets, no text in any of them. -```json +```jsonc // ✅ Authored items — `title` and `ordered` are read off the node { "type": "list", @@ -362,7 +362,7 @@ section exists to close: binding `list` to ordinary records produces one empty } ``` -```json +```jsonc // ✅ Bound data, already node-shaped: dataSource = { rows: [{ "content": "Ada" }, { "content": "Linus" }] } { "type": "list", "bind": "rows" } ``` @@ -371,7 +371,7 @@ Only `body` entries go back through `SchemaRenderer`, so they are the one place inside a list where expressions are evaluated at all — against the host scope, never against a current element: -```json +```jsonc // ✅ `${data.*}` works inside `body`; there is still no `${item.*}` { "type": "list", @@ -388,7 +388,7 @@ never against a current element: accessors (`accessorKey`). Cell values are plain property lookups — never expressions — and `table` does not read `bind`. -```json +```jsonc // ✅ `table`: inline rows + column accessors, no per-row scope { "type": "table", @@ -444,7 +444,7 @@ Expressions are compiled once per unique `(expression, variableNames)` pair and **Cause:** The field isn't expression-evaluated. Use `content`. -```json +```jsonc // ❌ Won't evaluate — `value` is read but never templated { "type": "text", "value": "${data.total}" } @@ -460,7 +460,7 @@ Expressions are compiled once per unique `(expression, variableNames)` pair and **Cause 1:** `visible` is also set and takes priority. **Cause 2:** Expression returns a non-boolean truthy value — use explicit comparison. -```json +```jsonc // ❌ Truthy but not boolean { "hidden": "${data.count}" } @@ -476,7 +476,7 @@ is **not** blocked — measured through the schema path, it returns a real `Date`. Prefer a formula function anyway: a `Date` object stringifies into the DOM as a locale-dependent blob. -```json +```jsonc // ✅ Works, but renders "Thu Jan 01 1970 …" { "type": "text", "content": "${new Date(data.timestamp)}" } @@ -489,7 +489,7 @@ the DOM as a locale-dependent blob. ### Object literal in expression -```json +```jsonc // ❌ Object literals not supported { "type": "text", "style": "${{ color: 'red' }}" } diff --git a/skills/objectui/rules/protocol.md b/skills/objectui/rules/protocol.md index 5e6a201e97..5ea19e06fe 100644 --- a/skills/objectui/rules/protocol.md +++ b/skills/objectui/rules/protocol.md @@ -154,7 +154,7 @@ key under `props` never reaches a `ui:*` / `page:*` renderer at all. **❌ FORBIDDEN:** Adding custom properties not defined in `@objectstack/spec`. **Example violation:** -```json +```jsonc { "type": "data-table", "fields": [...], // ❌ spec uses "columns" @@ -163,7 +163,7 @@ key under `props` never reaches a `ui:*` / `page:*` renderer at all. ``` **✅ CORRECT:** -```json +```jsonc { "type": "data-table", "columns": [...] // ✅ declared by DataTableSchema, read off the node @@ -174,7 +174,7 @@ key under `props` never reaches a `ui:*` / `page:*` renderer at all. When the entire string is a single `${expression}`, the result preserves its type: -```json +```jsonc "${data.count}" // → returns number 42, not string "42" "${data.isActive}" // → returns boolean true, not string "true" "Count: ${data.count}" // → returns string "Count: 42" (mixed template) @@ -184,7 +184,7 @@ When the entire string is a single `${expression}`, the result preserves its typ The `bind` field is NOT expression-evaluated. It's a path string resolved by `useDataScope()`, and only a component that calls that hook reads it: -```json +```jsonc { "type": "list", "bind": "customerNames" // Resolved as dataSource.customerNames diff --git a/skills/objectui/rules/styling.md b/skills/objectui/rules/styling.md index fe511a6f52..06654f354a 100644 --- a/skills/objectui/rules/styling.md +++ b/skills/objectui/rules/styling.md @@ -167,7 +167,7 @@ const buttonVariants = cva( **Every component must accept `className` on its schema node** to allow JSON-level style overrides. Like every other key, it is read off the node itself — not out of a `props` envelope, which the renderers never read: -```json +```jsonc { "type": "card", "className": "bg-red-500", // ✅ User can override styles From dc4811c534d322e37c49d7ef4e712ce7d14eba07 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 04:00:14 +0000 Subject: [PATCH 2/2] test(components): read jsonc guide fences too, so the retag does not hide the bind example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guide extractor in `skill-guide-data-table-binding.test.tsx` matched a ```json opener literally, so this branch's retag of `rules/protocol.md`'s four comment-carrying fences made the `"bind": "customerNames"` list example invisible to every assertion in the file. Two of them then read that absence as data: - the counter-probe "still teaches `bind` in a JSON block" — "AssertionError: expected false to be true"; - "its `list` example renders one entry per bound item" — "AssertionError: the guide must carry a parseable list example bound with `bind`: expected undefined to be truthy". The pull-request-level run never saw it: that job trims the suite for a docs-only diff, and the full run is the merge_group one — which is why this branch was ejected from the queue twice on the same failure. `parseBlock` already strips `//` comments before `JSON.parse`, so jsonc bodies parse today; only the extractor was behind. Widening it to accept both tags is the whole fix. No retag is reverted and the `bind` block does not go back to `json`: those fences carry `//` comments, which is the falsehood this branch exists to remove. Counts re-derived at ec67429 with a faithful re-implementation of `jsonBlocks` + `parseBlock` — fences / data-table / list / bind=customerNames / offenders, narrow then widened: guides/schema-expressions.md 8/1/1/1/0 -> 19/1/5/1/0 rules/protocol.md 6/0/0/0/0 -> 10/0/1/1/0 guides/data-integration.md 4/1/1/1/0 -> 4/1/1/1/0 The data-table count is unchanged at 1 / 0 / 1, so `expect(extra).toEqual([])` and the `[taught]` node selection are untouched; the offenders list stays empty; the two failing assertions get their block back. The newly visible protocol.md node is the same shape the other two guides already carry, character for character: {"type":"list","bind":"customerNames"}. Reverse verification, one mutation, proven on disk before any verdict was read: restoring the narrow json-only extractor put the file back to blob 421bca4 (the parent's blob for that path) with a widened-regex count of 0 and a narrow-regex count of 1, and made exactly those two assertions red and nothing else — "Tests 2 failed | 14 passed (16)", shared-lock verdict "command-exit 1". The restore ran under an EXIT/INT/TERM trap with an absolute path and is proven by hash: back to 76fdb28. With the fix in place the same whole-file command gives "Test Files 1 passed (1)" / "Tests 16 passed (16)", lock verdict "command-exit 0". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- .changeset/jsonc-fence-extractor-test-only.md | 7 +++++++ .../__tests__/skill-guide-data-table-binding.test.tsx | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 .changeset/jsonc-fence-extractor-test-only.md diff --git a/.changeset/jsonc-fence-extractor-test-only.md b/.changeset/jsonc-fence-extractor-test-only.md new file mode 100644 index 0000000000..03c7c1f195 --- /dev/null +++ b/.changeset/jsonc-fence-extractor-test-only.md @@ -0,0 +1,7 @@ +--- +--- + +Test-only change in `@object-ui/components`: the skill-guide fence extractor in +`skill-guide-data-table-binding.test.tsx` now reads `jsonc` fences as well as +`json`, so retagging a comment-carrying guide example no longer hides it from +every assertion in that file. No published behaviour changes. diff --git a/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx b/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx index 421bca4d63..76fdb2814e 100644 --- a/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx +++ b/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx @@ -91,10 +91,16 @@ function readGuide(rel: string): string { return fs.readFileSync(path.join(repoRoot, rel), 'utf8'); } -/** Every fenced ```json block body in a markdown file, in document order. */ +/** + * Every fenced ```json OR ```jsonc block body in a markdown file, in document + * order. Both tags are read on purpose: the guides tag their comment-carrying + * examples ```jsonc, and `parseBlock` below already strips those `//` comments + * before parsing. Reading only ```json would let a retag silently hide an + * example from every assertion here — which is a pass, not a failure. + */ function jsonBlocks(md: string): string[] { const out: string[] = []; - const fence = /```json\n([\s\S]*?)```/g; + const fence = /```jsonc?\n([\s\S]*?)```/g; let m: RegExpExecArray | null; while ((m = fence.exec(md)) !== null) out.push(m[1]); return out;