Repository navigation
[Decision] html 层 braced 属性值的字面量语法 —— interpretBrace 是否材料化 JS 字面量子集(单引号字符串 / 无引号键) #6614
Description
由 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 | |
|---|---|---|---|
| 单引号字符串、无引号标识符键照常工作;残余的真表达式仍然响亮拒绝 | 只靠新警告 | 自然拼写变成整页硬失败 |
@objectstack/sdui-parser 同步移动,否则保存门与渲染器会说两种方言。已立卡承接(见下方 Q4)。
Q2 —— 何时把 inert-expression 从 warning 升为 error
推荐 A:渲染侧编译保持 warning;等 framework 把 registry manifest 接进 validate-jsx-pages(今天它只跑 parse,manifest 是未接线的跨仓产物)后,在保存门升为 error。理由:现在就 error 会在渲染时让整页失败,而渲染是今天唯一带 manifest 编译的面 —— 会打断已发布的、目前尚能部分渲染的页面。ADR-0078 要的是作者时响亮,而那就是保存门。
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 后)。
⛔ 但这三条都改变授权语言接受什么,属于协议/公开契约形状变更,落在人工地板上 —— 本席记录,不代裁。
相关
- html-kind page: object-table renders rows but no data columns —
columnsnever reaches the renderer because the registration's declared inputs omit it #6598 —— 生产缺陷卡,已pm:blocked指向本卡;PR fix(sdui-parser): braced non-JSON values draw an inert-expression warning instead of vanishing silently #6613(inert-expression警告)是它的沉默半边,与本卡不冲突 - objectstack#12716 —— framework 侧姊妹 parser 的同步移动(Q4,已立)
- ADR-0078(禁止 parsed-but-silently-inert)· ADR-0080(此层只解析不执行)· A gauge widget's
options.inverthas no observable effect, and nothing can say so — it is an undeclared key ridingDashboardWidgetOptionsSchema.passthrough()#5709(惰性授权键取 warning 的先例)
Activity
os-sales commentedon Aug 27, 2026
⛔ 更正卡面里的一个跨仓引用:framework 侧姊妹卡是 objectstack#12719,不是
objectstack#12716本席的错,而且是同一个错误在一小时内犯第二次 —— 我又在 create 返回之前把跨仓卡号写进了正文。
正文「相关」一节里的
objectstack#12716作废。正确的是:⛔ 不要去跟
objectstack#12716,那是别的卡。正文其余内容不变,Q1/Q2/Q3 与四维分析均成立。⭐ 记一条给本车道的通则,因为它已经证明自己不是偶发:跨仓引用必须从 create 的返回结果里抄,永远不能预写。 错的卡号会解析到一张真实存在的卡,读起来像是有意的 —— 比缺失引用更坏。第一次犯是在 #6236 上(
objectstack#12694→ 实际#12715)。写下来是因为「记住不要再犯」显然不管用,而「从返回值抄」是个可机械执行的动作。Generated by Claude Code
huangyiirene commentedon Aug 27, 2026
Triage: lands in objectui's⚠️ Note the deliberate asymmetry with its sibling: the objectstack copy is
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 thedomain:devxengineering stream and notpackages/spec).domain:devx, because that repo's lane table namespackages/sdui-parserunder 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 -->
columns的八种拼写试完才放弃(objectstack#12649,promo-video 制作组);而 AI 作者持续大量产出单引号,这一层的定位恰恰是安全接收 AI 起草的页面。零拉动的默认 defer 在此不适用。推荐(执行席的,非本席代裁):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
os-sales commentedon Aug 28, 2026
Claim: PM loop round R2 (2026-08-28), dispatching the ruled Q1-A implementation
Session:
8ca04858-ea8e-5b85-9182-de59aa49e00cBranch:
claude/issue-6614-brace-literal-subsetWorktree:
objectui-issue-6614Domain:
domain:uiFile surface:
packages/sdui-parser/src/parse.tsand its tests, plus a changeset.validate.ts/dashboard-widget-options.tsonly if the dev shows the literal subset forces them; stop on any wider breach and report.Container and model:
M,mode:subagent,model: opusClause-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-reviewis a phantom label in this repo with zero readers, so it is not applied.Claim:comment exists on this card; the ruling comment is the last one on the thread.Serial constraints
packages/sdui-parseris 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:
⛔ 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
os-sales commentedon Aug 28, 2026
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 ("theapproval_recallaction 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
sourceTEXT, not a compiled tree, so both sides compile the same text independently.interpretBraceemits no diagnostic in either dialect, so the save gate can only see the divergence throughvalidateTree— which it never reaches today, because it runs parse-only (nosdui.manifest.jsontracked; measured 0 hits against 80 forpackage.jsonas 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 whosesourcecarries 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.skip-changeset— this PR carries a changeset, and in this repo that label is a phantom with zero readers anyway.Generated by Claude Code
os-sales commentedon Aug 28, 2026
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
objectstackorigin/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.
package.jsondependency edges on@objectstack/sdui-parserpackages/lintonly@objectstack/specfinds 72.ts/.tsx/.js/.mjspackages/lint/src/validate-jsx-pages.ts:23@objectstack/lintfinds 18@objectstack/sdui-parser/...import('@objectstack/sdui-parser')@objectstack/lintindexvalidateJsxPagesfunction is re-exported)parseJsx/interpretBrace/manifestFromConfigs/validateTreeoutside the packagepackages/specare 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.tsis registered inauthoring-rules.ts:750as rulevalidateJsxPages,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_ONLYis literally['cli'](authoring-rules.ts:333), while the runtime publish gate selects rules withruntimeAuthoringRulesFor()filtering onr.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 —validateJsxPagesis not one. Its declared reason is explicit:That "own path" is objectui's copy — the side this PR widens.
2. What happens TODAY to
columns={['name','amount']}after #6669 landsPath 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):sdui.manifest.jsonappears 0 times in the objectstack tree (positive control: thevalidate-jsx-pages.tspath returns 1 in the samegit ls-treeshape). ThenvalidateJsxPagescallsparseJsx(source)only, andvalidateTreeis never reached. ⭐interpretBraceemits 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.compile()reachesvalidateTree, which emitsinert-expressionat warning.splitBySeverity(authoring-rules.ts:1508) sorts warnings intoadvisories, nevererrors; only the opt-in--strictflag ("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.
@objectstack/console/dist/sdui.manifest.jsonfallback — 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:
columnsnever reaches the renderer because the registration's declared inputs omit it #6598 defect. It is not what is happening.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
test(plugin-list): pin all eight reported `columns` spellings on a ki…
test(plugin-list): pin all eight reported `columns` spellings on a ki…