Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/6598-inert-expression-warning.md
Original file line numberDiff line numberDiff line change
@@ -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.
100 changes: 100 additions & 0 deletions packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts
Original file line numberDiff line numberDiff line change
@@ -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(`<list-view objectName="account" columns={['name','amount']} />`, 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(`<list-view objectName="account" columns={[{field:"name"}]} />`, 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(`<list-view objectName="account" options={{pageSize: 25}} />`, 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 [
`<list-view objectName="account" columns={["name","amount"]} />`,
`<list-view objectName="account" columns={[{"field":"name","label":"Full Name"}]} />`,
`<list-view objectName="account" options={{"pageSize":25}} />`,
`<list-view objectName="account" />`,
]) {
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(`<list-view objectName="account" aggregate={{field:'amount'}} />`, manifest);
expect(r.diagnostics).toEqual([
expect.objectContaining({ severity: 'warning', code: 'unknown-prop' }),
]);
});
});
26 changes: 25 additions & 1 deletion packages/sdui-parser/src/validate.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(sdui-parser): braced non-JSON values draw an inert-expression warning instead of vanishing silently by claude[bot] · Pull Request #6613 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/6598-inert-expression-warning.md
Original file line numberDiff line numberDiff line change
@@ -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.
100 changes: 100 additions & 0 deletions packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts
Original file line numberDiff line numberDiff line change
@@ -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(`<list-view objectName="account" columns={['name','amount']} />`, 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(`<list-view objectName="account" columns={[{field:"name"}]} />`, 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(`<list-view objectName="account" options={{pageSize: 25}} />`, 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 [
`<list-view objectName="account" columns={["name","amount"]} />`,
`<list-view objectName="account" columns={[{"field":"name","label":"Full Name"}]} />`,
`<list-view objectName="account" options={{"pageSize":25}} />`,
`<list-view objectName="account" />`,
]) {
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(`<list-view objectName="account" aggregate={{field:'amount'}} />`, manifest);
expect(r.diagnostics).toEqual([
expect.objectContaining({ severity: 'warning', code: 'unknown-prop' }),
]);
});
});
26 changes: 25 additions & 1 deletion packages/sdui-parser/src/validate.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(sdui-parser): braced non-JSON values draw an inert-expression warning instead of vanishing silently by claude[bot] · Pull Request #6613 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/6598-inert-expression-warning.md
Original file line numberDiff line numberDiff line change
@@ -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.
100 changes: 100 additions & 0 deletions packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts
Original file line numberDiff line numberDiff line change
@@ -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(`<list-view objectName="account" columns={['name','amount']} />`, 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(`<list-view objectName="account" columns={[{field:"name"}]} />`, 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(`<list-view objectName="account" options={{pageSize: 25}} />`, 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 [
`<list-view objectName="account" columns={["name","amount"]} />`,
`<list-view objectName="account" columns={[{"field":"name","label":"Full Name"}]} />`,
`<list-view objectName="account" options={{"pageSize":25}} />`,
`<list-view objectName="account" />`,
]) {
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(`<list-view objectName="account" aggregate={{field:'amount'}} />`, manifest);
expect(r.diagnostics).toEqual([
expect.objectContaining({ severity: 'warning', code: 'unknown-prop' }),
]);
});
});
26 changes: 25 additions & 1 deletion packages/sdui-parser/src/validate.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(sdui-parser): braced non-JSON values draw an inert-expression warning instead of vanishing silently by claude[bot] · Pull Request #6613 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/6598-inert-expression-warning.md
Original file line numberDiff line numberDiff line change
@@ -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.
100 changes: 100 additions & 0 deletions packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts
Original file line numberDiff line numberDiff line change
@@ -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(`<list-view objectName="account" columns={['name','amount']} />`, 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(`<list-view objectName="account" columns={[{field:"name"}]} />`, 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(`<list-view objectName="account" options={{pageSize: 25}} />`, 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 [
`<list-view objectName="account" columns={["name","amount"]} />`,
`<list-view objectName="account" columns={[{"field":"name","label":"Full Name"}]} />`,
`<list-view objectName="account" options={{"pageSize":25}} />`,
`<list-view objectName="account" />`,
]) {
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(`<list-view objectName="account" aggregate={{field:'amount'}} />`, manifest);
expect(r.diagnostics).toEqual([
expect.objectContaining({ severity: 'warning', code: 'unknown-prop' }),
]);
});
});
26 changes: 25 additions & 1 deletion packages/sdui-parser/src/validate.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(sdui-parser): braced non-JSON values draw an inert-expression warning instead of vanishing silently by claude[bot] · Pull Request #6613 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/6598-inert-expression-warning.md
Original file line numberDiff line numberDiff line change
@@ -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.
100 changes: 100 additions & 0 deletions packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts
Original file line numberDiff line numberDiff line change
@@ -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(`<list-view objectName="account" columns={['name','amount']} />`, 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(`<list-view objectName="account" columns={[{field:"name"}]} />`, 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(`<list-view objectName="account" options={{pageSize: 25}} />`, 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 [
`<list-view objectName="account" columns={["name","amount"]} />`,
`<list-view objectName="account" columns={[{"field":"name","label":"Full Name"}]} />`,
`<list-view objectName="account" options={{"pageSize":25}} />`,
`<list-view objectName="account" />`,
]) {
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(`<list-view objectName="account" aggregate={{field:'amount'}} />`, manifest);
expect(r.diagnostics).toEqual([
expect.objectContaining({ severity: 'warning', code: 'unknown-prop' }),
]);
});
});
26 changes: 25 additions & 1 deletion packages/sdui-parser/src/validate.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(sdui-parser): braced non-JSON values draw an inert-expression warning instead of vanishing silently by claude[bot] · Pull Request #6613 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/6598-inert-expression-warning.md
Original file line numberDiff line numberDiff line change
@@ -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.
100 changes: 100 additions & 0 deletions packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts
Original file line numberDiff line numberDiff line change
@@ -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(`<list-view objectName="account" columns={['name','amount']} />`, 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(`<list-view objectName="account" columns={[{field:"name"}]} />`, 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(`<list-view objectName="account" options={{pageSize: 25}} />`, 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 [
`<list-view objectName="account" columns={["name","amount"]} />`,
`<list-view objectName="account" columns={[{"field":"name","label":"Full Name"}]} />`,
`<list-view objectName="account" options={{"pageSize":25}} />`,
`<list-view objectName="account" />`,
]) {
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(`<list-view objectName="account" aggregate={{field:'amount'}} />`, manifest);
expect(r.diagnostics).toEqual([
expect.objectContaining({ severity: 'warning', code: 'unknown-prop' }),
]);
});
});
26 changes: 25 additions & 1 deletion packages/sdui-parser/src/validate.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(sdui-parser): braced non-JSON values draw an inert-expression warning instead of vanishing silently by claude[bot] · Pull Request #6613 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/6598-inert-expression-warning.md
Original file line numberDiff line numberDiff line change
@@ -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.
100 changes: 100 additions & 0 deletions packages/sdui-parser/src/__tests__/inert-expression-6598.test.ts
Original file line numberDiff line numberDiff line change
@@ -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(`<list-view objectName="account" columns={['name','amount']} />`, 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(`<list-view objectName="account" columns={[{field:"name"}]} />`, 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(`<list-view objectName="account" options={{pageSize: 25}} />`, 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 [
`<list-view objectName="account" columns={["name","amount"]} />`,
`<list-view objectName="account" columns={[{"field":"name","label":"Full Name"}]} />`,
`<list-view objectName="account" options={{"pageSize":25}} />`,
`<list-view objectName="account" />`,
]) {
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(`<list-view objectName="account" aggregate={{field:'amount'}} />`, manifest);
expect(r.diagnostics).toEqual([
expect.objectContaining({ severity: 'warning', code: 'unknown-prop' }),
]);
});
});
26 changes: 25 additions & 1 deletion packages/sdui-parser/src/validate.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
Loading