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;
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