diff --git a/.changeset/6598-inert-expression-warning.md b/.changeset/6598-inert-expression-warning.md
new file mode 100644
index 0000000000..84ebbeb9f2
--- /dev/null
+++ b/.changeset/6598-inert-expression-warning.md
@@ -0,0 +1,26 @@
+---
+'@object-ui/sdui-parser': minor
+---
+
+html tier: a braced attribute value that is not strict JSON now draws a `inert-expression` warning instead of vanishing silently (objectui#6598)
+
+`interpretBrace` materializes strict-JSON values only; anything else — the
+single-quoted array every JSX author writes (`columns={['name','amount']}`),
+unquoted object keys, any JS expression — compiles to the deferred `{ $expr }`
+marker, and nothing downstream evaluates that marker: the html tier parses,
+never executes (ADR-0080), and no renderer consumes `$expr`. The value reached
+the renderer as an opaque object, defensive non-array/non-object reads degraded
+it to "not declared", and the author's binding vanished with zero diagnostics
+anywhere — a production page's `list-view` rendered its row count and toolbar
+with no data columns, through eight `columns` spellings (objectui#6598, moved
+from objectstack#12649). That is ADR-0078's prohibited parsed-but-silently-inert
+state.
+
+`validateTree` now emits a warning-severity `inert-expression` diagnostic when a
+declared input's value is the `$expr` marker, with the fix in the message: write
+the value as JSON (double-quoted strings and keys). Warning, not error, per the
+objectui#5709 posture for inert authored keys — pages keep compiling and
+rendering exactly as before; the silence is what changed. Escalating the
+severity, widening the accepted literal grammar (e.g. materializing
+single-quoted strings), and covering base props like `style` are contract
+decisions deliberately left on objectui#6598.
diff --git a/packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts b/packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts
new file mode 100644
index 0000000000..abc8cebdd4
--- /dev/null
+++ b/packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts
@@ -0,0 +1,100 @@
+/**
+ * objectui#6598 — the html tier's silent-vanish hole for braced non-JSON values.
+ *
+ * `interpretBrace` materializes strict-JSON values only; anything else becomes
+ * the deferred `{ $expr }` marker, and NOTHING downstream evaluates that marker
+ * (this tier parses, never executes — ADR-0080; a repo-wide grep finds zero
+ * `$expr` consumers outside this package). So `columns={['name','amount']}` —
+ * the universal JSX spelling, single quotes — used to compile with ZERO
+ * diagnostics into a value every renderer's defensive non-array read degrades
+ * to "no columns declared": rows render, the author's whole data binding is
+ * eaten, and no surface ever says why. That is ADR-0078's prohibited
+ * parsed-but-silently-inert state, reported from production as objectui#6598
+ * (moved from objectstack#12649).
+ *
+ * These cases pin the correction: a `$expr` value on a DECLARED input now draws
+ * the warning-severity `inert-expression` diagnostic, message carrying the fix.
+ * Severity is pinned as WARNING deliberately (the objectui#5709 precedent for
+ * inert authored keys): escalating to error, widening the accepted literal
+ * grammar (single quotes / unquoted keys), and base-prop (`style`) coverage are
+ * open contract decisions on the issue — a change to any of those should move
+ * these pins consciously, not by accident.
+ *
+ * The fixture manifest mirrors the LIVE `list-view` registration's relevant
+ * inputs (packages/plugin-list/src/index.tsx) but is deliberately synthetic —
+ * tier.test.ts's `object-table` fixture standing in for the live registration
+ * is how the issue got mis-anchored in the first place. The live-path
+ * (registry → SchemaRenderer → grid handoff) evidence lives in the issue
+ * report, not here: this file pins the compile half.
+ */
+import { describe, expect, it } from 'vitest';
+import { compile } from '../index.js';
+import type { Manifest } from '../types.js';
+
+const manifest: Manifest = {
+ components: {
+ 'list-view': {
+ type: 'list-view',
+ namespace: 'plugin-list',
+ inputs: [
+ { name: 'objectName', type: 'string', required: true },
+ { name: 'columns', type: 'array' },
+ { name: 'options', type: 'object' },
+ ],
+ },
+ },
+};
+
+describe('inert-expression: braced non-JSON on a declared input warns instead of vanishing', () => {
+ it("single-quoted array — the JSX habit — draws the warning and stays in the tree as $expr", () => {
+ const r = compile(``, manifest);
+ expect(r.diagnostics).toEqual([
+ expect.objectContaining({
+ severity: 'warning',
+ code: 'inert-expression',
+ tag: 'list-view',
+ message: expect.stringContaining('"columns"'),
+ }),
+ ]);
+ // The marker itself is unchanged — the tree still carries the deferred
+ // value; only the silence is gone. The message names the fix.
+ expect(r.tree?.columns).toEqual({ $expr: "['name','amount']" });
+ expect(r.diagnostics[0].message).toMatch(/JSON/);
+ // Warning, not error: the page still compiles (the objectui#5709 posture).
+ expect(r.ok).toBe(true);
+ });
+
+ it('unquoted object keys draw the same warning', () => {
+ const r = compile(``, manifest);
+ expect(r.diagnostics).toEqual([
+ expect.objectContaining({ severity: 'warning', code: 'inert-expression' }),
+ ]);
+ });
+
+ it('an $expr on an object-typed input is covered too', () => {
+ const r = compile(``, manifest);
+ expect(r.diagnostics).toEqual([
+ expect.objectContaining({ severity: 'warning', code: 'inert-expression', tag: 'list-view' }),
+ ]);
+ });
+
+ it('strict-JSON spellings stay diagnostic-free — the warning cannot fire on a working page', () => {
+ for (const source of [
+ ``,
+ ``,
+ ``,
+ ``,
+ ]) {
+ const r = compile(source, manifest);
+ expect(r.diagnostics).toEqual([]);
+ expect(r.ok).toBe(true);
+ }
+ });
+
+ it('an $expr on an UNKNOWN prop keeps drawing unknown-prop, not a double report', () => {
+ const r = compile(``, manifest);
+ expect(r.diagnostics).toEqual([
+ expect.objectContaining({ severity: 'warning', code: 'unknown-prop' }),
+ ]);
+ });
+});
diff --git a/packages/sdui-parser/src/validate.ts b/packages/sdui-parser/src/validate.ts
index 1b6b0d3748..58f6c87606 100644
--- a/packages/sdui-parser/src/validate.ts
+++ b/packages/sdui-parser/src/validate.ts
@@ -82,7 +82,31 @@ export function validateTree(tree: SchemaElement | null, manifest: Manifest): Ma
if (input.binding) {
bindings.push({ tag: node.type, input: key, kind: input.binding, value });
}
- if (!isExpr(value)) {
+ if (isExpr(value)) {
+ // A braced value that failed JSON materialization compiled to the
+ // parser's deferred `{ $expr }` marker — and NOTHING downstream
+ // evaluates that marker: this tier parses, never executes
+ // (ADR-0080), and no renderer consumes `$expr`. The value therefore
+ // reaches the renderer as an opaque object, every defensive
+ // non-array/non-object read degrades it to "not declared", and the
+ // author's binding silently vanishes (objectui#6598: eight `columns`
+ // spellings on a data block, all eaten without a single diagnostic —
+ // rows rendered, zero data columns). ADR-0078 prohibits exactly this
+ // parsed-but-silently-inert state, so name it at compile time, with
+ // the fix in the message. Warning, not error, per the objectui#5709
+ // precedent for inert authored keys — escalation to error (and any
+ // widening of the accepted literal grammar, e.g. single-quoted
+ // strings) is a contract decision tracked on objectui#6598.
+ diagnostics.push({
+ severity: 'warning',
+ code: 'inert-expression',
+ message:
+ `<${node.type}> prop "${key}" is a braced expression this tier never evaluates — ` +
+ `the value will be silently ignored at render. Write it as JSON ` +
+ `(double-quoted strings and keys), e.g. columns={["name","amount"]} not columns={['name','amount']}`,
+ tag: node.type,
+ });
+ } else {
const typeDiag = checkType(node.type, input, value);
if (typeDiag) diagnostics.push(typeDiag);
}