Uh oh!
There was an error while loading. Please reload this page.
feat(sdui-parser): materialize the JS literal subset in braced attribute values - #6669
Conversation
…ute values
The html tier parses, never executes (ADR-0080) — it is the untrusted-safe DATA
tier and the only safe carrier for AI- or tenant-authored pages. `interpretBrace`
accepted only strict JSON inside braces while the surface called itself JSX, so
`columns={['name','amount']}` compiled to the deferred `{ $expr }` marker that
nothing evaluates, and the author's binding vanished at render.
`interpretBrace` now materializes the JS literal subset: exactly two widenings
over JSON — single-quoted strings (value and key position) and unquoted
identifier object keys. Everything else JSON refuses is still refused and still
draws `inert-expression`. The subset has no identifier lookup and no operator,
so nothing in it is executable; an authored `__proto__` key becomes an own
property as `JSON.parse` gives it, never the prototype setter.
`JSON.parse` still runs first and untouched, so strict-JSON invariance is a
property of the structure rather than of a test.
Ruled on objectui#6614 (Q1-A, maintainer 2026-08-28). Q2 (error severity at the
save gate) and Q3 (base-prop coverage) are deliberately not in this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49os-sales
commented
Aug 28, 2026
⏸ HELD IN DRAFT — one unanswered question from the dispatch, and it is the one the ruling fenced
⛔ Not a rework request, and not a defect finding. The implementation reads well and the verification is thorough — two ablation legs, neither uniform red; What is missing is the sequencing analysis, and it gates landing rather than the code. The dispatch asked for it explicitly:
The report returns Why this is not being waved throughobjectstack#12719, filed by this seat, states the invariant:
and the maintainer's ruling carries the same constraint: land contract-first "so the save gate and the renderer never speak two dialects." ⛔ Landing against that on an assumption — in either direction — is not a call this seat will make from the card text alone. What is being measured before this landsThe dev has been asked, as a read-and-report task with no code change, for: the real consumers of the framework-side parser output today; what each of them does with a single-quoted braced value in the window after this lands and before the grammar reaches objectstack; and a verdict on whether that window is benign (the drift exists but no reachable surface can observe it, because the save gate is parse-only and the manifest is unwired per #12719's second gap) or real (some surface validates or renders differently, so this must land in lockstep with objectstack#12977). Benign ⇒ this lands now. Real ⇒ it holds for the lockstep, and holding a correct PR on a sequencing constraint costs far less than an intermittent, surface-dependent authoring bug. ⛔ Until then: not flipped ready, not enqueued, no auto-merge. Nothing here is asked of the framework seat yet — objectstack#12977 is theirs to grade and route. Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-sales
commented
Aug 28, 2026
✅ HOLD RELEASED — the window is measured BENIGN. ACCEPT; landing follows when CI finishes.
The ordering analysis came back, and it is measured rather than reasoned. It answers the question the hold was placed for, and the answer is that objectui-first is the safe direction. The decisive fact, which was not in the PR bodyThe objectstack parser is wired to the CLI surface only. It is never reached by the runtime metadata-API write path — the exact path objectui#6598's page was authored through.
That single consumer is registered No server-side render, no codegen, no migration, no publish-time transform. objectstack never renders an SDUI tree. What actually happens in the window
Maximum divergence is one advisory line on an opt-in CLI path, telling an author to "write it as JSON" about a value the renderer now handles correctly — stale advice, not a defect. ⭐ Why the answer is not symmetricobjectstack#12719 fears "saves clean and renders inert, or the reverse." Landing objectui first produces the reverse of the dangerous one: the page saves exactly as it does today, and now renders correctly. The dangerous half is objectstack-first — the save gate materialising while the renderer still defers — and that is today's #6598 defect, not what this sequence does. The invariant's grammar half is genuinely violated during the window, which is unavoidable across two repos and is precisely what carrier card objectstack#12977 exists for. But "violated" and "observable as a defect" are different questions, and the second one is what was measured. Recorded plainlyThe hold was right to impose — this seat did not have the evidence and would not land a ruled-against-drift change on an assumption. It is right to release now for the same reason: the evidence arrived. One honest gap is declared in the analysis (the
Landing: 26 of 29 checks green, 3 test shards still running, zero failures. Ready-flip and auto-merge follow when every check is complete — entry qualification is every check green, not the required subset. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…nd:'html' page The reported defect no longer reproduces on main. Re-measured every one of the eight spellings objectstack#12649 tried: none lands in the reported state (a populated table whose only header is the index column, with zero diagnostics). Six render data columns — forms 1 and 3 render exactly the authored projection, forms 2, 4, 5, 7 and 8b render the block's defaults — and forms 6 and 8a fail loudly with the compile-error block on screen. Three separate mechanisms produced one symptom, and each was fixed elsewhere: the braced literal subset (#6614 / PR #6669), the unauthored-projection handoff (PR #6679), and the grid's default-column derivation (#6677). What none of those pinned is the card's own claim, which is a statement about all eight spellings at once. This adds that matrix, through the real page renderer, the real html-tier compile against the real registry manifest, the real `list-view` registration and the real `object-grid` — so no future change can put any spelling back into the reported state. Tests only; the changeset declares an empty frontmatter accordingly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Fixes#6614
Implements Q1-A only of the maintainer ruling of 2026-08-28 (live director session, batch #5 item 1; verbatim: 「6614 也同意」).
interpretBracenow materialises the JS literal subset, so the spellings every JSX author and every AI author writes by habit reach the renderer. Genuine expressions keep compiling to the deferred{ $expr }marker and stay refused, with the warning PR #6613 landed.The html tier is the untrusted-safe DATA tier — parsed, never executed (ADR-0080) — and the ruling affirmed it exists to be good at that one job. It was calling itself JSX while accepting only JSON inside braces. That is a trap, not a contract, and this closes it.
All verification below ran on
8bf2dbb2, the head of this branch, with a clean tree.1. The exact set of spellings whose meaning changes
⭐ This enumeration is the deliverable a future reader diffs a real page corpus against. Each row moves from "deferred
{ $expr }marker plus aninert-expressionwarning, value eaten at render" to "materialised value".title={'Accounts'}"Accounts"columns={['name','amount']}["name","amount"]options={{'pageSize': 25}}{"pageSize":25}options={{pageSize: 25}}{"pageSize":25}\'inside a single-quoted stringIdentifier keys are exactly
[A-Za-z_$][A-Za-z0-9_$]*. Row 2 is objectui#6598's production page, byte-for-byte.One further change, forced by the above: the
inert-expressionmessage text. It used to say "write it as JSON (double-quoted strings and keys)" and namedcolumns={['name','amount']}as the wrong form — which Q1-A makes a working spelling, so the old advice would send an author to edit correct source. The diagnostic code and severity are unchanged; only the wording moved.Nothing else changes. Deliberately still refused, though a JSON5-shaped implementation would have swept them in: trailing commas, comments, array holes, spreads,
undefined/NaN/Infinity,+1/.5/1./0x1f/010, template literals, computed keys. The ruling legalised two things; this legalises those two.2. The refusal side, pinned as hard as the positive side
A suite asserting only the newly-legal spellings would pass just as well against a parser that started accepting everything — the one outcome this widening must never ship. So every positive pin has a refusal pin naming the first thing across the boundary. 31 refusal cases in
literal-subset-6614.test.ts, including:getColumns(),['a'].concat(b),rows.map((r) => r.name)ctx.user.name; identifiers —savedColumns1 + 2,a ? b : c,flag && ['a'], and['a'] + x(a literal that merely starts the expression)The structural reason the boundary holds: this is a value grammar, not an evaluator. It contains no identifier lookup and no operator, so there is nothing in it to execute. The widening moves habitual spellings onto the materialised side; it does not move the boundary between data and code.
Two safety pins beyond the grammar. An authored
__proto__key becomes an own data property (whatJSON.parsegives), never the prototype setter — a plainobj[key] = valuein the unquoted-key path would have handed untrusted page source a prototype-pollution lever the strict-JSON path never had, and there is a test that fails on exactly that mistake. Event handlers and raw-HTML injection stay forbidden regardless of the value grammar.3. Strict-JSON invariance is structural, not incidental
JSON.parsestill runs first and untouched; the new reader only ever sees inputJSON.parsehas already thrown on. So every value JSON accepts takes byte-identically the path it always did — a property of the code shape, not of a test. Pinned anyway, 18 strict-JSON inputs compared againstJSON.parseincluding-0sign, duplicate keys and a quoted__proto__.4. Ablation — the pins bite, and bite selectively
Two legs, each mutated on disk under a
trap … EXIT INT TERMwith absolute paths, each proved landed before reading any result and proved restored by observation afterwards. Restoration usesgit checkout HEAD -- ABSOLUTE_PATH, never the bare form, which reads from a possibly-polluted index. Script:ablate.sh; the reference blob ise162048c.No rebuild step applies here and that is measured, not assumed: both suites import the parser through a relative
../index.js, which vitest resolves to TypeScript source, so there is nodist/staleness axis for the mutation to hide behind.Baseline (unmutated
8bf2dbb2) —vitest exit = 0,Test Files 2 passed (2),Tests 70 passed (70).Leg A — widen past the ruled subset. Dropped the full-consumption check so a literal followed by trailing text is accepted. On-disk proof: injected-marker count
1, removed-anchor count0, blobe162048cmoved to70d77bf0.vitest exit = 1—Tests 7 failed | 63 passed (70). The 7 are exactly the trailing-input refusal pins:Every positive pin and every invariance pin stayed green.
Leg B — narrow back to strict JSON only (pre-#6614 behaviour). On-disk proof: injected-marker count
1, removed-anchor count0, blob moved to9ae06e7c.vitest exit = 1—Tests 11 failed | 59 passed (70). The 11 are exactly the materialisation and prototype-safety pins. All 18 invariance pins and all 31 refusal pins stayed green — which is the right answer: reverting the widening cannot break strict JSON and cannot make a refusal more permissive.Neither leg is a uniform red. Restoration after both: blob back to
e162048c, marker count0,git diff HEADempty.5. Lockstep ordering with the objectstack copy — established, not assumed
⛔ First, a correction to the dispatch. It named the sync card as
objectstack#12716. That is an unrelated, closed approvals card. The sibling is objectstack#12719 — this card's own filing seat caught and corrected the same slip in a comment on #6614.Which copy is canonical. objectstack's
@objectstack/sdui-parseris the hoisted, canonical home (its CHANGELOG: "hoist the constrained JSX-source compiler into framework as@objectstack/sdui-parser(its canonical home)"). objectui's@object-ui/sdui-parseris an independent package — different name, different version line (17.6.0 against 17.2.0), and no dependency edge: nopackage.jsonin this repo requires the objectstack copy. Two maintained copies, one invariant between them.What each side drives. Save gate: objectstack
packages/lint/src/validate-jsx-pages.ts, agating-tier rule run fromos validate/os build. Renderer: this repo'spackages/components/src/renderers/layout/page.tsx, which callscompile(src, getJsxManifest())at render. A page storessourcetext, not a compiled tree, so both sides compile the same text independently and no compiled value crosses the boundary.Is the window a real dialect split? No — and the reason is directional.
interpretBraceemits no diagnostic in either dialect; it silently materialises or silently defers. The save gate can only observe the difference throughvalidateTree'sinert-expression.resolveSduiManifest()returns undefined and nosdui.manifest.jsonis tracked in objectstack (measured with a positive control: 0 hits, against 80 forpackage.jsonin the same query shape), sovalidateTreeis never reached from the production gate.inert-expressionat warning. Worst case is stale advice, never a rejection.⇒ objectui-first, this PR: a page saves exactly as it does today, and now renders correctly. Nothing that saves today stops saving; nothing renders worse.
⇒ objectstack-first would have been the dangerous order — save gate materialising while the renderer still defers means a page saves clean and renders inert. That is the precise failure #12719 names, and it is today's #6598 defect.
This is also the direction the ruling pointed at: objectstack#12814 ran objectui-first (objectui#3832 ruled and landed here; objectstack ported after, PR #12857). Same shape.
check-*scripts here, none comparing the copies), so the invariant is documentary and nothing will catch the drift on its own.6. ⭐ The live-page risk — stated as NOT MEASURED
⛔ This is NOT MEASURED, and it is not measurable from this repository. What is tracked here is documentation pages, one console preview page and changesets — evidence about themselves, not about live pages. ⛔ I did not substitute fixtures, docs pages or
apps/console/sdui-tiers-preview.htmlfor a live corpus; a previous run in this lane was burned by exactly that substitution, and reporting "clean" here would be false.Unreachable from here. Who would have to query it: a seat with read access to the runtime/tenant page store, censusing
kind:'html'pages whosesourcecontains a braced attribute value that is not strict JSON. That is a runtime-data question, not a repository question.⭐ What I did instead, which is the part that was in my power: made the change as narrow as the ruling allows, so the set of spellings whose meaning changes is exactly the set in section 1 and nothing more. That table is what a real corpus should be diffed against.
7. Gate verdicts, each with its exit code
Every heavy command ran through the shared container verify-lock. Exit codes captured before any pipe; each line quotes the tool's own verdict.
--filter '@object-ui/sdui-parser^...'packages/react build: Donesdui-parserpackage suiteTest Files 11 passed (11)·Tests 147 passed (147)Test Files 1 passed (1)·Tests 6 passed (6)type-check(this repo spells it hyphenated)@object-ui/sdui-parser@17.6.0 type-check,tsc --noEmit && tsc -p tsconfig.test.json, silent--no-inline-configfiles linted = 4,errors = 0 warnings = 0--no-inline-config .total files eslint linted = 3884·errors = 90·warnings = 11381VERDICT command-exit 0 · held the lock 329s (5m29s) · waited 45sGate 2 is the one that matters most for a grammar change: the whole package ran, not only my two files, so the 9 pre-existing test files —
compile,tier,render,provenance,dashboard-widget-options,input-type-union,spec-symbol-batch7,inline-whitespace,dashboard-widget-options-census— are green under the widening. No fixture in them needed triage.Gate 6 exits 1 on pre-existing repo state, not on this branch: the run is
origin/main's standing 90 errors across 75 files, and all four files this PR touches score 0 errors / 0 warnings in that same run. Type-aware linting is not configured (languageOptionscarries onlyecmaVersionandglobals; noproject/projectService— positive control: the same grep shape findsrules10 times andlanguageOptionsonce in that file), so this diff cannot move the verdict on any untouched file.Also run: a control-character scan over all changed files, no hits.
8. Fixture triage on
inert-expression-6598.test.tsThat file pinned three spellings as WARNING cases and said in so many words that a grammar widening "should move these pins consciously, not by accident". Q1-A widened it, so
columns={['name','amount']},columns={[{field:"name"}]}andoptions={{pageSize: 25}}now materialise. Each moved toliteral-subset-6614.test.ts(which pins its value) and was replaced in place by a genuine expression, so the file still pins the same fact — an inert braced value is never silent — on the same side of the new boundary. Its strict-JSON and unknown-prop cases were left untouched and stayed green.9. ⛔ Deliberately not in this PR
inert-expressionstays a warning at render-side compile. It becomes an error at the save gate, once the framework wires the registry manifest intovalidate-jsx-pages. That is cross-repo, framework-side, and not mine. The visibility gap until then is recorded, not hidden.style)$exprinertness is sequenced after the literal subset, precisely so no warning is added for spellings this PR legalises. I found nothing in Q3 inseparable from Q1: base props are skipped byBASE_PROPSbefore the$exprcheck, so Q1 neither helps nor blocks it. The only Q1-forced edit outside the parser was the diagnostic message, since it named a spelling this PR makes legal.domain:devx, another seat; tracked on objectstack#12977.columnsnever reaches the renderer because the registration's declared inputs omit it #6598 — untouched, and this PR claims no fix there. It is unblocked by this card closing, not by anything written on it. It stays open until its own seat verifies the production page.Generated by Claude Code