11/**
2- * `inert-expression` — the html tier's silent-vanish hole for braced non-JSON
3- * values, ported into this copy in lockstep with objectui PR #6613.
2+ * `inert-expression` — the html tier's silent-vanish hole for braced values
3+ * this tier cannot materialize. Ported into this copy in lockstep with
4+ * objectui's `packages/sdui-parser` (objectui#6613, message reworded by
5+ * objectui#6614).
46 *
5- * `interpretBrace` materializes strict-JSON values only; anything else — the
6- * single-quoted array every JSX author writes, unquoted object keys, any JS
7- * expression — becomes the deferred `{ $expr }` marker, and NOTHING downstream
8- * evaluates that marker (this tier parses, never executes — ADR-0080; no
9- * renderer consumes `$expr`). So `columns={['name','amount']}` used to compile
10- * with ZERO diagnostics into a value every renderer's defensive non-array read
11- * degrades to "no columns declared": rows render, the author's whole data
12- * binding is eaten, and no surface ever says why. That is ADR-0078's prohibited
13- * parsed-but-silently-inert state, reported from production as objectui#6598.
7+ * `interpretBrace` materializes strict JSON plus the JS LITERAL SUBSET
8+ * (objectui#6614 Q1-A, ruled 2026-08-28); a GENUINE EXPRESSION still becomes
9+ * the deferred `{ $expr }` marker, and NOTHING downstream evaluates that marker
10+ * (this tier parses, never executes — ADR-0080; no renderer consumes `$expr`).
11+ * Such a value reaches the renderer as an opaque object, every defensive
12+ * non-array read degrades it to "not declared", and the author's binding is
13+ * eaten in silence. That is ADR-0078's prohibited parsed-but-silently-inert
14+ * state, reported from production as objectui#6598.
1415 *
1516 * WHY THIS FILE EXISTS HERE AND NOT ONLY THERE. There are two copies of this
1617 * parser — objectui's `packages/sdui-parser` and this repo's hoisted
17- * `@objectstack/sdui-parser` — and the invariant is that both copies agree on
18- * the accepted grammar AND on diagnostic codes. If they drift, the save gate
19- * and the renderer speak different dialects: a page can save clean and render
20- * inert, or the reverse — surface-dependent, and therefore intermittent from
21- * the author's point of view. These pins are the objectstack half of that
22- * lockstep; the emitted diagnostic is byte-equal to objectui's .
18+ * `@objectstack/sdui-parser` — and the invariant (#12719) is that both copies
19+ * agree on the accepted grammar AND on diagnostic codes. If they drift, the
20+ * save gate and the renderer speak different dialects: a page can save clean
21+ * and render inert, or the reverse — surface-dependent, and therefore
22+ * intermittent from the author's point of view. These pins are the objectstack
23+ * half of that lockstep .
2324 *
24- * Severity is pinned as WARNING deliberately (the objectui#5709 precedent for
25- * inert authored keys), and `ok` is pinned true alongside it: this port reports
26- * an ALREADY-inert state, so it must leave the accept/reject set exactly where
27- * it stood. Escalating to error, widening the accepted literal grammar (single
28- * quotes / unquoted keys — objectui#6614), and base-prop (`style`) coverage are
29- * open contract decisions; a change to any of those should move these pins
30- * consciously, not by accident.
25+ * ⭐ WHAT MOVED IN #6614 Q1-A, AND WHY IT IS NOT AN ACCIDENT. This file
26+ * originally pinned `columns={['name','amount']}`, `columns={[{field:"name"}]}`
27+ * and `options={{pageSize: 25}}` as WARNING cases, and said in so many words
28+ * that widening the literal grammar "should move these pins consciously, not by
29+ * accident". Q1-A widened it, so those three spellings now MATERIALIZE and are
30+ * correct — the whole point of the ruling. They moved to
31+ * `literal-subset-6614.test.ts`, which pins their values; each was replaced
32+ * here by a genuine expression, so this file still pins the same FACT (an inert
33+ * braced value is never silent) on the same side of the new boundary.
34+ *
35+ * Severity stays WARNING deliberately (the objectui#5709 precedent for inert
36+ * authored keys), and `ok` is pinned true alongside it: this diagnostic reports
37+ * an ALREADY-inert state, so it leaves the accept/reject set exactly where it
38+ * stood. ⛔ Escalation to error is objectui#6614 **Q2**, which lands at the SAVE
39+ * GATE once the framework wires the registry manifest into `validate-jsx-pages`
40+ * (#12719 records that gap) — not here, and not at render.
3141 */
3242import { describe , expect , it } from 'vitest' ;
3343import { compile } from '../index.js' ;
@@ -47,9 +57,12 @@ const manifest: Manifest = {
4757 } ,
4858} ;
4959
50- describe ( 'inert-expression: braced non-JSON on a declared input warns instead of vanishing' , ( ) => {
51- it ( 'single-quoted array — the JSX habit — draws the warning and stays in the tree as $expr' , ( ) => {
52- const r = compile ( `<list-view objectName="account" columns={['name','amount']} />` , manifest ) ;
60+ describe ( 'inert-expression: a braced EXPRESSION on a declared input warns instead of vanishing' , ( ) => {
61+ it ( 'a method call — the shape #6598 could not materialize — warns and stays as $expr' , ( ) => {
62+ const r = compile (
63+ `<list-view objectName="account" columns={rows.map((r) => r.name)} />` ,
64+ manifest ,
65+ ) ;
5366 expect ( r . diagnostics ) . toEqual ( [
5467 expect . objectContaining ( {
5568 severity : 'warning' ,
@@ -60,33 +73,37 @@ describe('inert-expression: braced non-JSON on a declared input warns instead of
6073 ] ) ;
6174 // The marker itself is unchanged — the tree still carries the deferred
6275 // value; only the silence is gone.
63- expect ( r . tree ?. columns ) . toEqual ( { $expr : "['name','amount']" } ) ;
76+ expect ( r . tree ?. columns ) . toEqual ( { $expr : 'rows.map((r) => r.name)' } ) ;
77+ // Warning, not error: the page still compiles (the objectui#5709 posture).
78+ expect ( r . ok ) . toBe ( true ) ;
6479 } ) ;
6580
66- it ( 'the message carries the FIX , not merely the complaint ' , ( ) => {
81+ it ( 'the message names the CURRENT accepted grammar , not a now-legal spelling ' , ( ) => {
6782 // An arrival pin, not a departure pin: "stopped being silent" is satisfied
68- // by any diagnostic at all. What this port owes the author is the remedy —
69- // name JSON, and show the corrected spelling next to the broken one. A
70- // message rewrite that drops the remedy turns this red.
83+ // by any diagnostic at all. What this port owes the author is advice that
84+ // is still TRUE after objectui#6614 Q1-A — the pre-#6614 wording told the
85+ // author to "write it as JSON (double-quoted strings and keys)" and named
86+ // `columns={['name','amount']}` as the wrong form, which would now send
87+ // them to edit working source.
7188 const [ d ] = compile (
72- `<list-view objectName="account" columns={[' name','amount'] } />` ,
89+ `<list-view objectName="account" columns={rows.map((r) => r. name) } />` ,
7390 manifest ,
7491 ) . diagnostics ;
75- expect ( d . message ) . toMatch ( / J S O N / ) ;
76- expect ( d . message ) . toContain ( 'double-quoted strings and keys ' ) ;
77- expect ( d . message ) . toContain ( ' columns={[" name"," amount"]}' ) ;
78- expect ( d . message ) . toContain ( ` columns={[' name','amount']}` ) ;
92+ expect ( d . message ) . not . toMatch ( / d o u b l e - q u o t e d / ) ;
93+ expect ( d . message ) . toContain ( 'LITERALS only ' ) ;
94+ expect ( d . message ) . toContain ( ` columns={[' name',' amount']} works` ) ;
95+ expect ( d . message ) . toContain ( ' columns={rows.map((r) => r. name)} cannot' ) ;
7996 } ) ;
8097
81- it ( 'unquoted object keys draw the same warning' , ( ) => {
82- const r = compile ( `<list-view objectName="account" columns={[{field:"name"}] } />` , manifest ) ;
98+ it ( 'a bare identifier draws the same warning' , ( ) => {
99+ const r = compile ( `<list-view objectName="account" columns={savedColumns } />` , manifest ) ;
83100 expect ( r . diagnostics ) . toEqual ( [
84101 expect . objectContaining ( { severity : 'warning' , code : 'inert-expression' } ) ,
85102 ] ) ;
86103 } ) ;
87104
88105 it ( 'an $expr on an object-typed input is covered too' , ( ) => {
89- const r = compile ( `<list-view objectName="account" options={{pageSize: 25 }} />` , manifest ) ;
106+ const r = compile ( `<list-view objectName="account" options={{...defaults }} />` , manifest ) ;
90107 expect ( r . diagnostics ) . toEqual ( [
91108 expect . objectContaining ( { severity : 'warning' , code : 'inert-expression' , tag : 'list-view' } ) ,
92109 ] ) ;
@@ -108,14 +125,14 @@ describe('inert-expression: braced non-JSON on a declared input warns instead of
108125 } ) ;
109126
110127 it ( 'the accept/reject set does not move — every inert spelling still compiles' , ( ) => {
111- // The load-bearing property of this port: it reports an ALREADY-inert
128+ // The load-bearing property: this diagnostic reports an ALREADY-inert
112129 // state, so `ok` (no error-severity diagnostic — the save gate's pass/fail)
113130 // is exactly what it was before the diagnostic existed. Escalating the
114131 // severity to error is objectui#6614's Q2 and would land here first.
115132 for ( const source of [
116- `<list-view objectName="account" columns={[' name','amount'] } />` ,
117- `<list-view objectName="account" columns={[{field:"name"}] } />` ,
118- `<list-view objectName="account" options={{pageSize: 25 }} />` ,
133+ `<list-view objectName="account" columns={rows.map((r) => r. name) } />` ,
134+ `<list-view objectName="account" columns={savedColumns } />` ,
135+ `<list-view objectName="account" options={{...defaults }} />` ,
119136 ] ) {
120137 const r = compile ( source , manifest ) ;
121138 expect ( r . ok ) . toBe ( true ) ;
@@ -124,7 +141,7 @@ describe('inert-expression: braced non-JSON on a declared input warns instead of
124141 } ) ;
125142
126143 it ( 'an $expr on an UNKNOWN prop keeps drawing unknown-prop, not a double report' , ( ) => {
127- const r = compile ( `<list-view objectName="account" aggregate={{field:' amount'} } />` , manifest ) ;
144+ const r = compile ( `<list-view objectName="account" aggregate={someTotal( amount) } />` , manifest ) ;
128145 expect ( r . diagnostics ) . toEqual ( [
129146 expect . objectContaining ( { severity : 'warning' , code : 'unknown-prop' } ) ,
130147 ] ) ;
0 commit comments