From f62a9238378ad1d3c10285cd5987e3505d96427c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 13:50:38 +0000 Subject: [PATCH] fix(sdui-parser): braced non-JSON values draw an inert-expression warning instead of vanishing silently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A braced attribute value that fails JSON materialization compiles to the parser's 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 reads degraded it to 'not declared', and the author's binding vanished with zero diagnostics anywhere (ADR-0078's prohibited parsed-but-silently-inert state). validateTree now names it at compile time, warning severity, with the fix in the message. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49 --- .changeset/6598-inert-expression-warning.md | 26 +++++ .../__tests__/inert-expression-6598.test.ts | 100 ++++++++++++++++++ packages/sdui-parser/src/validate.ts | 26 ++++- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 .changeset/6598-inert-expression-warning.md create mode 100644 packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts 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); }