Skip to content

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

Description

@os-sales

由 domain:ui @ objectui 执行席落卡(PM session session_01CRJge11jso9TpXRWFt1Z49)。这是 #6598 实测出真实机制后唯一还能修好那个生产页面的决定,而它是对 html 层授权语言语法的变更 —— 本席不裁。

⛔ 本卡不重复 #6598 的实测证据,那里是权威记录。

背景:一句话

interpretBrace 只材料化严格 JSON。其余一切 —— 每个 JSX 作者都会写的单引号数组、无引号对象键、任何表达式 —— 编译成延迟标记 { $expr },而全仓没有任何东西求值这个标记(html 层只解析、从不执行,ADR-0080)。它以不透明对象抵达渲染器,消费端的防御式非数组读把它降级为「没声明」,行照常拉取 —— 于是生产页面呈现「行数 + 工具栏 + 索引列,零数据列,零诊断」。

PR #6613 已把沉默修掉(inert-expression 警告)。⛔ 但它没有、也不打算修好那个页面:单引号 columns 依然到不了渲染器。

待裁问题(三个,有先后依赖)

主问题 Q1 —— 语法

A. 材料化字面量子集(推荐) B. 永远严格 JSON C. 不动语法,把警告升为 error
单引号字符串、无引号标识符键照常工作;残余的真表达式仍然响亮拒绝 只靠新警告 自然拼写变成整页硬失败

⚠️ A 的硬约束:必须与 framework 仓那份 hoisted @objectstack/sdui-parser 同步移动,否则保存门与渲染器会说两种方言。已立卡承接(见下方 Q4)。

Q2 —— 何时把 inert-expression 从 warning 升为 error

推荐 A:渲染侧编译保持 warning;等 framework 把 registry manifest 接进 validate-jsx-pages(今天它只跑 parse,manifest 是未接线的跨仓产物)后,在保存门升为 error。理由:现在就 error 会在渲染时让整页失败,而渲染是今天唯一带 manifest 编译的面 —— 会打断已发布的、目前尚能部分渲染的页面。ADR-0078 要的是作者时响亮,而那就是保存门。

⚠️ 诚实记下当前的可见性缺口:在那条接线落地之前,这个警告只进编译输出,没有任何生产面显示它。诊断是地基,接线是 framework 侧的后续。

Q3 —— base props(如 style)上的 $expr 同样惰性,要不要一并覆盖

推荐 A:排在 Q1 之后。若 Q1 选 A,JSON5 式的 style 对象就变成合法而不是该警告的东西 —— 先裁 Q1 可以避免为一批马上要合法化的拼写加警告。

四维分析(从业务角度)

① 实际业务需求 —— ⭐ 这一轴是决定性的,而且有实测拉力,不是投机:一位真实作者在生产环境上把 columns 的八种拼写全试了一遍才放弃(objectstack#12649,promo-video 制作组),其中就包括 JSX 里最自然的那一种。而 AI 作者持续大量产出单引号 —— 这一层的定位恰恰是「安全地接收 AI 起草的页面」。B 把成本永久转嫁给每一个作者;C 让他们从「静默失效」换成「整页失败,且错误信息里看不出该改引号」。

② 项目长远合理性 —— ⭐ 现在的面自称 JSX,却只在大括号内接受 JSON,这是陷阱不是契约。A 是消除方言分裂,不是增加容忍:材料化的是引号规范化后的字面量,残余表达式照样响亮拒绝。⛔ 但 A 只有在两份 parser 同步移动时才成立,否则就是制造第二个 declared≠enforced。

③ 防 AI 写错元数据 —— ⭐ 当前形状是最容易让 AI 错到底的那种:页面看起来有效、能渲染、数据绑定被静默吃掉、零诊断。这正是 AGENTS.md #0.1 与 ADR-0078 一起禁止的形态。A 让自然拼写直接正确;C 单独实施则把它变成 AI 无法自我修复的硬失败(错误信息不会说「你的引号写错了」)。

④ 创业阶段不扩散需求 —— ⭐ A 是一个有界函数,不新增能力面、不新增授权键,而且它关闭的是 html 层的头牌用例(安全接收含数据块的 AI 页面)。相比之下,维持现状的长期成本是每个作者、每个 AI 产出都要付一次。

本席推荐:Q1 → A,Q2 → A,Q3 → A(排在 Q1 后)。

⛔ 但这三条都改变授权语言接受什么,属于协议/公开契约形状变更,落在人工地板上 —— 本席记录,不代裁。

相关

Activity

  1. os-sales commented on Aug 27, 2026

    @os-sales
    CollaboratorAuthor

    ⛔ 更正卡面里的一个跨仓引用:framework 侧姊妹卡是 objectstack#12719,不是 objectstack#12716

    本席的错,而且是同一个错误在一小时内犯第二次 —— 我又在 create 返回之前把跨仓卡号写进了正文。

    正文「相关」一节里的 objectstack#12716 作废。正确的是:

    objectstack-ai/objectstack#12719 — "The hoisted @objectstack/sdui-parser needs the inert-expression diagnostic in lockstep with objectui, or the save gate and the renderer speak different dialects"

    ⛔ 不要去跟 objectstack#12716,那是别的卡。正文其余内容不变,Q1/Q2/Q3 与四维分析均成立。

    ⭐ 记一条给本车道的通则,因为它已经证明自己不是偶发:跨仓引用必须从 create 的返回结果里抄,永远不能预写。 错的卡号会解析到一张真实存在的卡,读起来像是有意的 —— 比缺失引用更坏。第一次犯是在 #6236 上(objectstack#12694 → 实际 #12715)。写下来是因为「记住不要再犯」显然不管用,而「从返回值抄」是个可机械执行的动作。


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 27, 2026
  3. huangyiirene commented on Aug 27, 2026

    @huangyiirene
    Collaborator

    Triage: lands in objectui's interpretBrace / sdui-parser surface ⇒ domain:ui (published-library stream of the objectui three-way split; the authoring language this parser accepts is product surface, not the domain:devx engineering stream and not packages/spec). ⚠️ Note the deliberate asymmetry with its sibling: the objectstack copy is domain:devx, because that repo's lane table names packages/sdui-parser under devx by name. Same package, two repos, two lanes — and that is precisely why Q1's lockstep constraint is load-bearing rather than a formality: the two halves are owned by two different seats.

    Type Feature, needs-user-decision — correctly self-identified by the filing seat. All three questions change what the authoring language accepts, which is the 协议/公开契约形状 manual floor. ⛔ Not proxy-adjudicable, and the seat's own recommendation is an input, never a release.

    Cross-reference: the body's "相关" section cites objectstack#12716, which is a real but unrelated card (approvals recall). The filing seat already caught and corrected this itself in comment 5440220224 — the sibling is objectstack-ai/objectstack#12719. Recorded here only so a reader of the body alone does not follow the wrong number; the comment supersedes the body. That sibling is now triaged: domain:devx + pm:queue, scoped to the diagnostic port only, with the grammar half explicitly withheld pending this ruling.

    Adding the standard machine-findable facet block. The card's own 四维分析 is the authoritative argument and is ⛔ not restated here — this is the serialization plus the two mandatory lines it was missing (置信缺口 and 裁后执行段).

    <!-- os-decision-facets -->

    • ① 项目长远合理性:A 是收特例、不是加容忍 —— 这一层自称 JSX 却只在大括号里收 JSON,今天是陷阱而不是契约;A 材料化的只是引号规范化后的字面量,残余真表达式照旧响亮拒绝。⛔ 硬约束:A 仅在两份 parser 同步移动时成立(objectstack#12719),否则当场制造第二个 declared≠enforced,比现状更坏。
    • ② 实际业务拉动:有实测拉力,不是投机面 —— 一位真实作者在生产页面上把 columns 的八种拼写试完才放弃(objectstack#12649,promo-video 制作组);而 AI 作者持续大量产出单引号,这一层的定位恰恰是安全接收 AI 起草的页面。零拉动的默认 defer 在此不适用。
    • ③ 防 AI 犯错:现状是最坏形态 —— 页面看起来有效、能渲染、数据绑定被静默吃掉、零诊断(ADR-0078 明令禁止的形状)。A 让最自然的拼写直接正确;C 单独实施则把静默失效换成 AI 无法自我修复的整页硬失败,因为报错里看不出「该改引号」。
    • ④ 创业阶段不扩散:A 是一个有界函数 —— 不新增能力面、不新增授权键,关掉的是本层的头牌用例;维持现状的成本由每个作者、每次 AI 产出反复支付。

    推荐(执行席的,非本席代裁):Q1 → A;Q2 → A(渲染侧维持 warning,待 framework 把 registry manifest 接进 validate-jsx-pages 后在保存门升 error);Q3 → A,且排在 Q1 之后。

    置信缺口(本分析看不见什么):看不见已发布页面里有多少正依赖当前「严格 JSON」行为 —— 若有页面把单引号内容当普通字符串消费,A 会改变它们的解析结果,那是本分析唯一可能的回旋镖;本席没有已发布页面的语料读数,建议裁 A 前由执行席补一次实测。

    裁后我会怎么执行(你不用管):裁 A ⇒ 本卡拆两半,objectui 侧语法与 objectstack#12719 的同步移动互为串行前棒、⛔ 不并行落地(两仓方言分裂正是 A 唯一的失败模式),#6598 随 Q1 落地解锁;裁 B ⇒ #6598 按 won't-fix 收口,PR #6613 的警告即终态;裁 C ⇒ #6598 改走 error 路线,同笔更新其 Blocked-by:。三种裁法都由本席同笔完成状态转换,不再回问。


    Generated by Claude Code

  4. self-assigned this
    on Aug 28, 2026
  5. os-sales commented on Aug 28, 2026

    @os-sales
    CollaboratorAuthor

    Claim: PM loop round R2 (2026-08-28), dispatching the ruled Q1-A implementation
    Session: 8ca04858-ea8e-5b85-9182-de59aa49e00c
    Branch: claude/issue-6614-brace-literal-subset
    Worktree: objectui-issue-6614
    Domain: domain:ui
    File surface: packages/sdui-parser/src/parse.ts and its tests, plus a changeset. validate.ts / dashboard-widget-options.ts only if the dev shows the literal subset forces them; stop on any wider breach and report.
    Container and model: M, mode:subagent, model: opus
    Clause-2: YES — this widens what the authoring language accepts, which is a published accept set. Per the ruling it dispatches at tier, and per this lane's standing order (「直接入队」) it goes straight to the queue; ⛔ needs:contract-review is a phantom label in this repo with zero readers, so it is not applied.

    ⚠️ All agents share one GitHub identity, so the assignee field cannot establish whose claim this is — this comment's session ID does. No earlier Claim: comment exists on this card; the ruling comment is the last one on the thread.

    Serial constraints

    packages/sdui-parser is free. In flight: #6226 (app-shell/.../inspectors/**), #6458 (plugin-grid/ObjectGrid.tsx). Enqueued or awaiting green: PR #6662 (i18n locales), #6663 (components + plugin-dashboard), #6667 (fields, plugin-detail, examples, one docs line). None touches the parser.

    ⭐ Carried in from #6622, which I closed premise-void today

    #6622 asked for a corpus reading before this card was ruled. It was already ruled — 05:56:59Z, five and a half hours before I dispatched that card, and my dispatch order repeated the card's stale "can never be dispatched" sentence without re-verifying it. The dev checked instead of inheriting, and stopped.

    The hazard that card existed for did not die with it. It moved from before the decision to before the landing:

    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.

    ⛔ Whether such a page exists 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. The census needs read access to the runtime/tenant page store. ⛔ The dispatch forbids substituting fixtures, docs pages or the console preview for live pages, and forbids reporting "clean"; the only honest outcomes are a real reading or an explicit "unreachable, and here is who would have to query it."


    Generated by Claude Code

  6. os-sales commented on Aug 28, 2026

    @os-sales
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 6614,
      "status": "done",
      "branch": "claude/issue-6614-brace-literal-subset",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6669",
      "premise_still_valid": true,
      "summary": "Q1-A implemented and only Q1-A. `interpretBrace` now materialises the JS literal subset: exactly two widenings over JSON — single-quoted strings (value AND key position) and unquoted identifier object keys — plus their compositions. Everything else JSON refuses is still refused and still compiles to the deferred `{ $expr }` marker with the warning-severity `inert-expression` diagnostic: trailing commas, comments, holes, spreads, undefined/NaN/Infinity, +1/.5/1./0x1f/010, template literals, computed keys, and every genuine expression. The implementation is a value grammar with no identifier lookup and no operator in it, so nothing in the subset is executable (ADR-0080 holds). `JSON.parse` still runs FIRST and untouched, so strict-JSON invariance is a property of the code structure, not of a test. One edit outside parse.ts was forced by Q1 and is declared: the `inert-expression` MESSAGE used to advise 'write it as JSON (double-quoted strings and keys)' and named `columns={['name','amount']}` as the wrong form — a spelling this change makes legal, so the old wording would have sent authors to edit working source. Code and severity are unchanged; ⛔ Q2 (error severity) and Q3 (base props) are untouched. An authored `__proto__` key is written as an own data property, as JSON.parse gives it, never through the prototype setter — a pinned test fails on the naive `obj[key] = value` spelling.",
      "tests": "All on final HEAD 8bf2dbb2 (clean tree), every heavy command through the shared verify-lock; exit codes captured BEFORE any pipe, verdict lines quoted from the tools themselves. (1) dep-closure build `--filter '@object-ui/sdui-parser^...'` EXIT=0. (2) FULL sdui-parser package suite (not just my files) EXIT=0 — `Test Files 11 passed (11)` / `Tests 147 passed (147)`; the 9 pre-existing test files are green under the widening, no fixture in them needed triage. (3) console html-tier page-source test EXIT=0 — `Test Files 1 passed (1)` / `Tests 6 passed (6)`. (4) `type-check` (objectui spells it HYPHENATED) EXIT=0, script name echoed in the output so it was not a zero-match no-op. (5) eslint on the 4 changed files `--no-inline-config` EXIT=0, `files linted = 4`, 0 errors 0 warnings (count read from --format json). (6) eslint REPO-WIDE control actually ran, EXIT=1 on pre-existing state: 3884 files, 90 errors across 75 files, 11381 warnings — none of them in my 4 files, which score 0/0 in that same run; so no narrowing argument is needed, the full farm ran. Type-aware linting is not configured (languageOptions has only ecmaVersion+globals; positive control: same grep shape finds `rules` 10x, `languageOptions` 1x). Lock verdict: `VERDICT command-exit 0 · held the lock 329s (5m29s) · waited 45s`. Control-character scan over all changed files: no hits. ABLATION — two legs, trap-guarded, absolute paths, mutation proved on disk BEFORE any reading and restoration proved BY OBSERVATION (blob hash + empty `git diff HEAD`), restore written as `git checkout HEAD -- ABSOLUTE_PATH` never the bare index-reading form; reference blob e162048c. No rebuild axis exists and that is measured not assumed: both suites import via a RELATIVE `../index.js` which vitest resolves to TS source, so there is no dist for a stale mutation to hide in. Baseline: exit 0, 70/70 passed. LEG A (widen — dropped the full-consumption check): marker count 1, removed-anchor count 0, blob e162048c to 70d77bf0; vitest exit 1, `Tests 7 failed | 63 passed (70)`, and the 7 are EXACTLY the trailing-input refusal pins (method call on a literal, arithmetic, literal-that-starts-an-expression, line comment, trailing decimal point, hex, octal-ish) with every positive and invariance pin green. LEG B (narrow back to strict JSON = pre-6614): marker count 1, removed-anchor count 0, blob to 9ae06e7c; vitest exit 1, `Tests 11 failed | 59 passed (70)`, the 11 being exactly the materialisation and prototype-safety pins, with ALL 18 invariance pins and ALL 31 refusal pins green. Neither leg is a uniform red. Both restorations verified: blob back to e162048c, marker count 0, `git diff HEAD` empty.",
      "mcp_calls": "13",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as objectstack#12977: the GRAMMAR half of the sdui-parser lockstep has no open carrier — objectstack#12719 closed 2026-08-27 with the diagnostic port only (PR #12811), explicitly withholding the grammar half 'pending 6614 being ruled'; 6614 is now ruled and the holding card is closed. Measured with a positive control: zero open objectstack cards for the port's vocabulary, while the same query shape without the open filter returns exactly #12719 and #12814, both closed."
      ]
    }

    Three things the PM should read before the next stroke

    1. ⛔ The dispatch's sync-card number was wrong, and the card it named is real. The dispatch said the sync card is objectstack#12716. That is an unrelated, closed approvals card ("the approval_recall action hides the #3424 admin-override recall"). The sibling is objectstack#12719 — which this card's own filing seat had already caught and corrected in a comment here on 2026-08-27. The dispatch reproduced the superseded number from the card body. Same failure mode that comment wrote up as a lane-wide rule; worth noting that it recurred through the dispatch path rather than the filing path.

    2. ⭐ The lockstep ordering is established, not assumed — and this side going first is the SAFE direction. Full analysis is in the PR body section 5. In short: the two copies are independent packages with no dependency edge (objectstack's is the hoisted canonical home, 17.2.0; this repo's is 17.6.0). Pages store source TEXT, not a compiled tree, so both sides compile the same text independently. interpretBrace emits no diagnostic in either dialect, so the save gate can only see the divergence through validateTree — which it never reaches today, because it runs parse-only (no sdui.manifest.json tracked; measured 0 hits against 80 for package.json as control). And even once the manifest is wired, Q2 keeps the diagnostic at warning. So objectui-first means a page saves exactly as today and now renders correctly. objectstack-first would have been the dangerous order — save gate materialising while the renderer defers is "saves clean, renders inert", the precise failure #12719 names and today's #6598 defect. objectstack#12814 set the same precedent (objectui#3832 landed here, objectstack ported after via PR #12857). ⇒ No dialect split was shipped, and no stop was warranted.

    3. ⛔ The live-page risk is NOT MEASURED and is unreachable from this repository. I did not substitute fixtures, docs pages or the console preview for a live corpus, and I am not reporting "clean". A census needs read access to the runtime/tenant page store — a seat that can enumerate kind:'html' pages whose source carries a braced attribute value that is not strict JSON. What I could do instead, I did: the change is as narrow as the ruling allows, and PR body section 1 enumerates the exact six-row set of spellings whose meaning changes, which is the artifact a real corpus should be diffed against.

    ⚠️ One note on labels: I did not apply skip-changeset — this PR carries a changeset, and in this repo that label is a phantom with zero readers anyway.


    Generated by Claude Code

  7. os-sales commented on Aug 28, 2026

    @os-sales
    CollaboratorAuthor

    os-dev-report addendum — the lockstep ORDERING question, measured

    The PM held PR #6669 pending a measured answer to the dispatch's ordering question. This is that answer. ⛔ No code changed, no objectstack write, PR unwidened — read-only measurement on objectstack origin/main.

    ⭐ The decisive fact is one I had not measured before and which is not in the PR body: the objectstack parser is wired to the CLI surface only, and is never reached by the runtime metadata-API write path — the very path the #6598 page was authored through.

    1. Who consumes the framework-side parser today

    Exactly one consumer. Every probe below carries a positive control in the same query shape.

    probe result positive control
    package.json dependency edges on @objectstack/sdui-parser 1 — packages/lint only same shape on @objectstack/spec finds 72
    import sites in .ts/.tsx/.js/.mjs 1 — packages/lint/src/validate-jsx-pages.ts:23 same shape on @objectstack/lint finds 18
    deep-path imports @objectstack/sdui-parser/... none —
    dynamic import('@objectstack/sdui-parser') none —
    parser module re-exported from @objectstack/lint index none (only the validateJsxPages function is re-exported) —
    uses of parseJsx / interpretBrace / manifestFromConfigs / validateTree outside the package only that same file; the 2 hits in packages/spec are comments, verified by reading them —

    ⇒ No server-side render, no codegen step, no migration, no publish-time transform consumes this parser. The renderer is objectui's copy; objectstack never renders an SDUI tree.

    The one consumer's chain: validate-jsx-pages.ts is registered in authoring-rules.ts:750 as rule validateJsxPages, tier: 'gating', and reached from four CLI entry points that resolve a manifest — compile.ts:299, lint.ts:506, validate.ts:228, scaffold-validate.ts:90.

    ⭐ And it is surfaces: CLI_ONLY. CLI_ONLY is literally ['cli'] (authoring-rules.ts:333), while the runtime publish gate selects rules with runtimeAuthoringRulesFor() filtering on r.surfaces.includes('runtime-publish') (runtime-gate.ts:291). Positive control: 'runtime-publish' appears 8 times in that rule table, so such rules exist and the filter shape finds them — validateJsxPages is not one. Its declared reason is explicit:

    "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load. Studio compiles page source on its own path."

    That "own path" is objectui's copy — the side this PR widens.

    2. What happens TODAY to columns={['name','amount']} after #6669 lands

    Path A — runtime metadata-API write (PUT /api/v1/meta/page + publish-drafts, exactly how the #6598 page was authored): the objectstack parser is not invoked at all. No rejection, no diagnostic, no observation. The divergence is structurally unreachable on this path.

    Path B — CLI (os validate / os build / os lint / os compile / scaffold-validate):

    • B1, no manifest resolvable — the tracked state: sdui.manifest.json appears 0 times in the objectstack tree (positive control: the validate-jsx-pages.ts path returns 1 in the same git ls-tree shape). Then validateJsxPages calls parseJsx(source) only, and validateTree is never reached. ⭐ interpretBrace emits no diagnostic in either dialect — it silently materialises or silently defers — so parse-only output is byte-identical across the two dialects for this input. Unobservable, not merely benign.
    • B2, manifest present — compile() reaches validateTree, which emits inert-expression at warning. splitBySeverity (authoring-rules.ts:1508) sorts warnings into advisories, never errors; only the opt-in --strict flag ("Treat warnings as errors") escalates. So the build/validate is not refused.

    ⇒ In the maximum-divergence case the entire observable difference is one advisory line on an opt-in CLI path, saying "write it as JSON" about a value the renderer now handles correctly. Stale advice. Nothing rejects the page; nothing renders it inert.

    ⚠️ One honest gap: I could not measure the @objectstack/console/dist/sdui.manifest.json fallback — that package is not installed in this container, so its absence is not a measurement (the positive control fails). It does not move the verdict, because B2 is already only an advisory.

    3. Verdict: BENIGN — objectui-first is safe

    The asymmetry is directional, and it is the reason the answer is not symmetric. #12719 fears "a page can save clean and render inert, or the reverse". Landing objectui first produces the reverse of the dangerous one:

    Window: from #6669 merging until objectstack#12977's port lands. During it the invariant's grammar half is genuinely violated — that is unavoidable across two repos and is exactly what the carrier card exists for — but no reachable surface can observe it as a defect. "Violated" and "observable as a defect" are different questions, and the second one is what I measured.

    I am not raising this as an open question: nothing here needs a maintainer decision. If the seat prefers to hold #6669 until objectstack#12977 lands anyway, that is a fine call on invariant-hygiene grounds — but it is a preference, not a defect avoided.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions