feat(sdui-parser): materialize the JS literal subset in interpretBrace — the grammar half of the #12719 lockstep - #13154
Conversation
#12977) Port the grammar half of the #12719 lockstep from objectui's copy of the parser, under the maintainer's ruling on objectui#6614 (Q1-A, 2026-08-28). `interpretBrace` now materializes exactly two widenings over JSON — single-quoted strings (value and key position) and unquoted identifier object keys — and everything else JSON refuses still becomes the deferred `{ $expr }` marker. `JSON.parse` runs first and untouched, so strict-JSON behaviour is invariant by construction. An authored `__proto__` key is written with `Object.defineProperty` as an own data property, the way `JSON.parse` gives it, never through the prototype setter: a plain assignment in the unquoted-key path would hand untrusted page source a prototype-pollution lever the strict-JSON path never had. The `inert-expression` message is reworded in the same lockstep — the old text advised "write it as JSON (double-quoted strings and keys)" and thereby named a now-legal spelling as the wrong one. Diagnostic codes are unchanged. Part of #12977 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
…ace-literal-subset-port
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8850e4ba2ebebe8184647ad4a7b8f67622e4dd6d && git checkout 8850e4ba2ebebe8184647ad4a7b8f67622e4dd6d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 74049254d47bd0edd2a2fcd732dcc01c91504f10 5f1fd6c98748e174dc982c2e63bc2a7072aa66a4 && git checkout -B drift-repro 74049254d47bd0edd2a2fcd732dcc01c91504f10 && git merge --no-ff 5f1fd6c98748e174dc982c2e63bc2a7072aa66a4
node scripts/docs-audit/affected-docs.mjs --json 74049254d47bd0edd2a2fcd732dcc01c91504f10
|
|
|
…fix so PR CI runs on a clean preview)
Fixes #12977
Ports the grammar half of the #12719 lockstep into this repo's copy of
sdui-parser, under the maintainer's ruling on objectui#6614 (Q1-A, 2026-08-28, adopted whole). #12719 carried two obligations and deliberately landed only one — theinert-expressiondiagnostic (#12811) — withholding the grammar widening until objectui#6614 was ruled. It has been, and the card holding the port was closed, so the obligation was live with no carrier.Direction check: objectui landed first, which is the safe order and the one the card argues for.
interpretBraceemits no diagnostic in either dialect, so objectui-first means a page saves exactly as it did and now renders correctly; objectstack-first would have meant the save gate materialising while the renderer still deferred — "saves clean, renders inert", which is objectui#6598 itself.What changed — four files
packages/sdui-parser/src/parse.tsinterpretBracegains the JS literal subset (a 214-line block, byte-identical to objectui's)packages/sdui-parser/src/validate.tsinert-expressionmessage reworded to objectui's, byte-identical; the code is unchangedpackages/sdui-parser/src/__tests__/inert-expression.test.tspackages/sdui-parser/src/__tests__/literal-subset-6614.test.tsPlus a changeset. Nothing else in
packages/sdui-parseris touched — in particular notdashboard-widget-options.ts(#12926 is in flight against it), and #12858's separate divergence in this same package is untouched and stays open.The grammar — exactly two widenings, and nothing else
Everything else JSON refuses is still refused and still becomes the deferred
{ $expr }marker: trailing commas, comments, array holes, spreads,undefined/NaN/Infinity,+1/.5/1./0x1f, template literals, and every genuine expression.JSON.parsestill runs first and untouched, so strict-JSON behaviour is invariant by construction rather than by test — the new reader only ever sees stringsJSON.parsehas already thrown on.The security clause, which comes ahead of the grammar
An authored
__proto__key is written as an own data property — whatJSON.parsegives — viaObject.defineProperty, never through the prototype setter. A plain assignment in the unquoted-key path would hand untrusted page source a prototype-pollution lever the strict-JSON path never had. That is a widening of attack surface, not a parser detail.Pinned in all three key spellings the grammar now admits (unquoted, single-quoted, double-quoted) and asserted on the property descriptor —
enumerable/writable/configurableplus the presence ofvalueand the absence ofget/set— not on a read of the value, because a read comes back correct against the vulnerable implementation too. The nested case and the negative side are pinned as well: a refused__proto__source stays a plain{ $expr }marker whose only own key is$expr, so authored text never reaches a key position on the deferred path either.Verification
All runs are on the final commit,
9a99c436e7. Exit codes were captured before any pipe; each verdict below is the line the tool printed for itself.pnpm --filter @objectstack/sdui-parser test—Test Files 6 passed (6) · Tests 130 passed (130)pnpm --filter @objectstack/sdui-parser typecheck— clean. Proven to cover the edits rather than skip them:tsc --listFilesputs all four edited files in the program (1 hit each) with a control at 0.pnpm --filter @objectstack/lint test— the direct consumer, resolving@objectstack/sdui-parserthroughexportsto a freshly builtdist:Test Files 81 passed (81) · Tests 2297 passed | 5 skipped. The builtdist/index.jswas confirmed to carry the port (readLiteralpresent,LITERALS onlypresent, ablation marker absent).&&, so one verdict covers all of them.pnpm lint(repo-wideeslint . --no-inline-config, not a narrowed run) — exit 0.dispatch-gates --repo objectstack-ai/objectstackfrom a synced tree (the first derivation printed STALE TREE and still exited 0; the branch was merged withorigin/mainand re-derived), pluscheck:nul-bytes, all green.Four of the derived families answered with a refusal, not a finding, and none is this PR's problem:
check-test-completeness.mjsexit 1 plus usage (the derivation emits the argument-less form — #13110);scripts/pm/check-half-states.mjsexit 3PREREQUISITE NOT MET;check:dual-build-cjs-loadsexit 3 "This is NOT a pass: nothing was measured";check:type-check-debtrefusing--re-measurewithout a built closure. The structural half,check:type-check-coverage, is green.Ablations — both predicted before running, both matched
Each mutation was committed-then-mutated, confirmed on disk by counting the deleted and the injected text (never by an editor's exit code), guarded by a
trapusing absolute paths, and restored with the restored blob hash compared against the HEAD blob. No rebuild is involved: the suite imports../index.jsrelatively, so it readssrc— proven by the suite passing whilepackages/sdui-parser/distdid not exist at all.A — remove the prototype-pollution guard (
Object.definePropertybecomes a plain assignment). Predicted red: the unquoted and single-quoted__proto__spellings and the nested case. Predicted green: the double-quoted spelling, because it takes the untouchedJSON.parsepath. Observed: exactly 3 failures, exactly those, with the double-quoted spelling green — the ordering invariant showing up as a green in the middle of a red suite.B — revert the widening (
interpretBraceback to the pre-ruling{ $expr }return). Predicted red: every positive materialization pin. Predicted green: all 31 refusal rows, the whole strict-JSON block, and the whole reworded diagnostic file. Observed: 13 failures, all positives; the refusal table, the strict-JSON block andinert-expression.test.tshad zero failures each, with the same grep proven able to return 13. That is the point of the pair — refusal pins alone cannot detect the widening's absence, so the positive pins carry it.The refusal table refuses to be empty
it.each([])registers zero cases and reports a clean run, so a thinned refusal table would look exactly like a passing suite — a guard whose success condition equals its total-failure condition. The suite therefore asserts the table is non-empty and that every class the ruling names is still represented, so deleting a row to make a future widening "pass" costs a red test first.Lockstep verification against objectui
objectui's landed implementation was read from its
origin/main(not from the branch name on the card, and not from the local checkout, which was stale by 4 commits).UNCONSUMED_WIDGET_OPTION) on objectui's side, so a naive literal scan reports a false 23-vs-22 difference. The values agree.Two things for the PM, not decided here
packages/spec/src/**and none a.zod.tscontract schema (control: the same predicates hit 1/1 when handed such a path).dispatch-gates --tierlikewise reports no path-derived mandate, and states in its own words that this is a floor and never a clearance. But the content limb does fire on reading: this PR moves what a published package accepts and rejects, which is clause ②'s content limb exactly, and the sibling lockstep port that did the same thing — Port the union-armtype-mismatchcheck into the hoistedsdui-parser— the half of the lockstep debt that CHANGES what this copy accepts and rejects #12814 — carriesneeds:contract-reviewfor that reason. The claim comment on The grammar half of the sdui-parser lockstep has no open carrier — #12719 closed with the diagnostic only, and objectui#6614 has now ruled the grammar widening #12977 carries noClause-②declaration line in either spelling, so the declaration limb is unanswered rather than answeredno. Per the rulebook this seat records the reading and stops: it attaches no label and asserts no tier.CONTRACT_REVIEW_TIERreadsclaude-fable-5from its single source.sdui.manifest.jsonis tracked here — 0 hits against 80 forpackage.jsonas the control, reproducing the card's numbers exactly — and the@objectstack/consolefallback manifest is absent too, soresolveSduiManifest()returns undefined andvalidate-jsx-pagesruns parse-only.validateTreeemits seven distinct diagnostic codes, not the eight the older cards assert, corroborating [finding] nothing wires an sdui.manifest.json, so validateJsxPages runs parse-only and validateTree — including both ported lockstep diagnostics — is dead code in the production gate #12924's reading.A separate finding, #13150, prices a cheap mechanical parity check for this lockstep — the class has now needed three manual ports and nothing watches it. Deliberately not built here.
Generated by Claude Code