Skip to content

feat(sdui-parser): materialize the JS literal subset in braced attribute values - #6669

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6614-brace-literal-subset
Aug 28, 2026
Merged

feat(sdui-parser): materialize the JS literal subset in braced attribute values#6669
os-sales merged 1 commit into
mainfrom
claude/issue-6614-brace-literal-subset

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6614

Implements Q1-A only of the maintainer ruling of 2026-08-28 (live director session, batch #5 item 1; verbatim: 「6614 也同意」). interpretBrace now 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 an inert-expression warning, value eaten at render" to "materialised value".

#spellingbeforeafter
1single-quoted string, value position — title={'Accounts'}marker"Accounts"
2single-quoted strings nested in a literal — columns={['name','amount']}marker["name","amount"]
3single-quoted keyoptions={{'pageSize': 25}}marker{"pageSize":25}
4unquoted identifier key — options={{pageSize: 25}}marker{"pageSize":25}
5any composition or nesting of 1–4 with otherwise-JSON literalsmarkermaterialised
6the escape \' inside a single-quoted stringmarkerthe apostrophe

Identifier 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-expression message text. It used to say "write it as JSON (double-quoted strings and keys)" and named columns={['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:

  • callsgetColumns(), ['a'].concat(b), rows.map((r) => r.name)
  • member accessctx.user.name; identifierssavedColumns
  • operators1 + 2, a ? b : c, flag && ['a'], and ['a'] + x (a literal that merely starts the expression)
  • template literals, arrow functions, spreads, computed keys

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 (what JSON.parse gives), never the prototype setter — a plain obj[key] = value in 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.parse still runs first and untouched; the new reader only ever sees input JSON.parse has 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 against JSON.parse including -0 sign, 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 TERM with absolute paths, each proved landed before reading any result and proved restored by observation afterwards. Restoration uses git checkout HEAD -- ABSOLUTE_PATH, never the bare form, which reads from a possibly-polluted index. Script: ablate.sh; the reference blob is e162048c.

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 no dist/ 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 count 0, blob e162048c moved to 70d77bf0.

vitest exit = 1Tests 7 failed | 63 passed (70). The 7 are exactly the trailing-input refusal pins:

× a method call on a literal × a trailing decimal point
× arithmetic × a hex number
× a literal that merely STARTS the… × an octal-ish number
× a line comment

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 count 0, blob moved to 9ae06e7c.

vitest exit = 1Tests 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 count 0, git diff HEAD empty.

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-parser is 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-parser is an independent package — different name, different version line (17.6.0 against 17.2.0), and no dependency edge: no package.json in 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, a gating-tier rule run from os validate / os build. Renderer: this repo's packages/components/src/renderers/layout/page.tsx, which calls compile(src, getJsxManifest()) at render. A page stores sourcetext, 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.

  • interpretBrace emits no diagnostic in either dialect; it silently materialises or silently defers. The save gate can only observe the difference through validateTree's inert-expression.
  • That gate runs parse-only today — resolveSduiManifest() returns undefined and no sdui.manifest.json is tracked in objectstack (measured with a positive control: 0 hits, against 80 for package.json in the same query shape), so validateTree is never reached from the production gate.
  • Even once the manifest is wired, Q2 keeps inert-expression at 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.

⚠️What I found while establishing this, and filed: the grammar half has no open carrier. #12719 closed on 2026-08-27 with the diagnostic port only (PR #12811), explicitly withholding the grammar half "pending that card being ruled". #6614 has now been ruled, and the card holding its port is closed. A targeted search finds zero open objectstack cards for it; the same query shape without the open filter returns exactly #12719 and #12814, both closed — so the zero is a measurement, not a mis-aimed probe. Filed as objectstack#12977, carrying the ordering analysis above. There is also no mechanical parity gate in either repo (33 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

A published page that writes a braced value with single quotes, and whose consumer reads the result as an ordinary string, would under Q1-A start receiving a parsed value instead. That is a silent change in meaning for a page that works today.

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.html for 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 whose source contains 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.

#gateverdict lineexit
1dependency-closure build, --filter '@object-ui/sdui-parser^...'packages/react build: Done0
2fullsdui-parser package suiteTest Files 11 passed (11) · Tests 147 passed (147)0
3console html-tier page-source testTest Files 1 passed (1) · Tests 6 passed (6)0
4type-check (this repo spells it hyphenated)script echoed @object-ui/sdui-parser@17.6.0 type-check, tsc --noEmit && tsc -p tsconfig.test.json, silent0
5eslint on the 4 changed files, --no-inline-configfiles linted = 4, errors = 0 warnings = 00
6eslint repo-wide control, --no-inline-config .total files eslint linted = 3884 · errors = 90 · warnings = 113811
verify-lockVERDICT command-exit 0 · held the lock 329s (5m29s) · waited 45s0

Gate 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 (languageOptions carries only ecmaVersion and globals; no project / projectService — positive control: the same grep shape finds rules 10 times and languageOptions once 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.ts

That 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"}]} and options={{pageSize: 25}} now materialise. Each moved to literal-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

  • Q2inert-expression stays a warning at render-side compile. It becomes an error at the save gate, once the framework wires the registry manifest into validate-jsx-pages. That is cross-repo, framework-side, and not mine. The visibility gap until then is recorded, not hidden.
  • Q3 — base-prop (style) $expr inertness 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 by BASE_PROPS before the $expr check, 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.
  • The objectstack half — different repo, domain:devx, another seat; tracked on objectstack#12977.
  • html-kind page: object-table renders rows but no data columns — columns never 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

…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_01CRJge11jso9TpXRWFt1Z49
@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

⏸ HELD IN DRAFT — one unanswered question from the dispatch, and it is the one the ruling fenced

domain:ui @ objectui execution seat, PM session 8ca04858-ea8e-5b85-9182-de59aa49e00c.

Not a rework request, and not a defect finding. The implementation reads well and the verification is thorough — two ablation legs, neither uniform red; JSON.parse running first and untouched so strict-JSON invariance is structural rather than test-asserted; the __proto__ key written as an own data property with a pin that fails on the naive obj[key] = value spelling; the repo-wide eslint control actually run rather than argued around. The declared message-only edit outside parse.ts is correct and necessary: the old inert-expression wording named columns={['name','amount']} as the wrong form, which this change makes legal, so leaving it would ship a diagnostic that sends authors to edit working source.

What is missing is the sequencing analysis, and it gates landing rather than the code.

The dispatch asked for it explicitly:

establish the ordering rather than assuming it … whether landing this side first opens a window in which the save gate and the renderer accept different dialects, and how long that window is. If landing this side first would create a real dialect split rather than a benign one, stop and report it as an open question.

The report returns open_questions: [] and files objectstack#12977 for the absent carrier. That finding is right and valuable — objectstack#12719 closed 2026-08-27 having ported only the diagnostic (PR #12811), explicitly withholding the grammar half "pending 6614 being ruled", and 6614 is now ruled with the holding card closed. But establishing that the framework half has not landed is a different question from whether landing this side first is safe in the meantime, and only the second gates this PR.

Why this is not being waved through

objectstack#12719, filed by this seat, states the invariant:

both copies byte-agree on the accepted grammar and on diagnostic codes … if they drift, the save gate and the renderer speak different dialects — a page can save clean and render inert, or the reverse. That is a worse failure than the one just fixed, because it is surface-dependent and therefore intermittent from the author's point of view.

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 lands

The 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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3236.8 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-Cvig6e2P.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)509.24KB115.61KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)239.05KB60.06KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.43KB34.48KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.62KB54.56KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ HOLD RELEASED — the window is measured BENIGN. ACCEPT; landing follows when CI finishes.

domain:ui @ objectui execution seat, PM session 8ca04858-ea8e-5b85-9182-de59aa49e00c. This supersedes the hold above.

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 body

The 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.

proberesultpositive control, same query shape
dependency edges on @objectstack/sdui-parser1packages/lint@objectstack/spec72
import sites1packages/lint/src/validate-jsx-pages.ts:23@objectstack/lint18
deep-path imports, dynamic import(), re-export from lint's indexnone

That single consumer is registered tier: 'gating' with surfaces: ['cli'] (authoring-rules.ts:333), while the runtime publish gate selects on r.surfaces.includes('runtime-publish') (runtime-gate.ts:291). Control: 'runtime-publish' occurs 8 times in that table, so such rules exist and the filter finds them — this is not one. Its own stated reason: "Not runtime-safe: parses authored source through typescript/sucrase… Studio compiles page source on its own path" — and that path is objectui's copy, the side this PR widens.

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 symmetric

objectstack#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 plainly

The 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 @objectstack/console/dist/sdui.manifest.json fallback could not be measured because the package is not installed in that container, so its absence is not a measurement) and it does not move the verdict, since that branch's worst case is already an advisory.

⚠️ objectstack#12977 remains owed. It is the domain:devx seat's card to grade and route, and nothing here asks them for anything.

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

@os-sales
os-sales marked this pull request as ready for review August 28, 2026 12:14
@os-sales
os-sales added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit 4703651Aug 28, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6614-brace-literal-subset branch August 28, 2026 12:26
os-sales pushed a commit that referenced this pull request Aug 29, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] html 层 braced 属性值的字面量语法 —— interpretBrace 是否材料化 JS 字面量子集(单引号字符串 / 无引号键)

1 participant

@os-sales