Skip to content

除 content 外没有任何文本键既被求值又被读回 —— statistic.value / card.title / button.label 无法绑定表达式 #4795

Description

@yinlianghui

发现于 #4786 的实施(教学面拆 props 信封),真实 SchemaRenderer 探针实测。这不是文档问题 —— 文档教错正是因为引擎这里有一个真实的洞。

两道闸门,没有一个键同时通过(除 content)

一个值要上屏必须同时:被 SchemaRenderer 求值,且被 renderer 读回。实测两边互不重叠:

写法 求值? 读回? 实测渲染
{ "type": "statistic", "props": { "value": "${data.n}" } } ✅ ❌ 空 —— props 被 spread 成 React props,StatisticRenderer 读 schema.value
{ "type": "statistic", "value": "${data.n}" } ❌ ✅ 字面量 ${data.n}
{ "type": "text", "content": "${data.n}" } ✅ ✅ 99

card.title / button.label / *.description 同理。探针原文(dataSource { total: 99 }):

@@PROBE expr/card-title-ENVELOPE   text=""                    html=... props="[object Object]" title="99" ...
@@PROBE expr/card-title-TOPLEVEL   text="${data.total}"
@@PROBE expr/text-content-TOPLEVEL text="Total: 99"

注意 ENVELOPE 那行:title="99" —— 值确实求出来了(99),只是作为 React prop 落到 DOM 属性上,CardTitle 里仍是空的。求值白做了。

成因(读码)

  • packages/react/src/SchemaRenderer.tsx 的 evaluation memo 只处理 content、props.* 和几个谓词键;title/label/value/description 原样透传。
  • 同文件 React.createElement(Component, { schema, ...componentProps, ...(evaluatedSchema.props || {}), ... }) —— props 的内容作为 React props 展开,而 ui:* / page:* renderer 一律读 schema.*(packages/components/src/renderers/data-display/statistic.tsx 读 schema.value;layout/card.tsx 读 schema.title;form/button.tsx 读 schema.label)。

后果

statistic 是仪表盘的主力组件,而它没有任何办法绑定一个动态数值 —— 只能由宿主在交给 SchemaRenderer 之前把值算好。教学面此前给出的"搬到 props 里"这个 workaround(protocol.md / page-builder.md / schema-expressions.md 三处成体系地写着)把"渲染出字面量"换成了"渲染出空白",更难诊断。PR(#4786)已把教学面改成如实陈述 + 走 content 或宿主预解析,但契约层面的洞留在这里。

修法方向(需裁决,勿直接猜)

  1. 扩大求值面:让 evaluation memo 对一组声明过的文本键(title/label/value/description)也跑模板求值。代价:哪些键算"文本键"需要在 @objectstack/spec / types 里声明清楚,否则又是一套方言。
  2. 把 props 并进节点:SchemaRenderer 把 schema.props 合并进 schema 而不是 spread 成 React props。代价:与 element:* 的 readProps 语义冲突,且会把"信封合法"这个已被 教学面成体系教 props 信封,而渲染器从节点本身读键 —— 照 page-builder.md 写出的页面渲染一片空白框 #4786 教学面否定的形态重新变成合法。
  3. 维持现状 + 发布期诊断:承认只有 content 可绑定,并对"节点上出现未求值的 ${...}"给出发布期报错。

个人倾向 1 或 3,但这是契约收紧,按 #3972 / #3987 家族先例应由维护者裁。

参考

Activity

  1. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Triage: needs-user-decision, type Feature. Which text keys are expression-bindable is a contract question — the card's three directions each change the accepted authoring surface (per the #3972/#3987 family precedent, human floor).

    Four prisms:

    1. Platform long-term coherence — direction 1 (a declared text-key evaluation set) closes the evaluate/read-back split with one declared vocabulary; direction 2 (merge props into the node) re-legalizes the envelope shape 教学面成体系教 props 信封,而渲染器从节点本身读键 —— 照 page-builder.md 写出的页面渲染一片空白框 #4786's teaching rewrite just retired — incoherent.
    2. Measured business pull — statistic is the dashboard workhorse and today has NO way to bind a dynamic value; the former teaching-face workaround renders blank. Real authoring pull.
    3. AI-agent error-resistance — today's state is the worst shape: an AI following the docs renders literals or blanks silently. Direction 3 (publish-time diagnostic on an unevaluated ${…} reaching the DOM) is the loud-refusal floor even if 1 is deferred.
    4. Startup scope discipline — direction 1 is a permanent vocabulary obligation; direction 3 is cheap and reversible.

    Recommendation: 3 now (loud refusal); 1 only with an explicitly declared, closed key set in spec/types; never 2. Joint axis with #4797 (same SchemaRenderer evaluation contract) — recommend ruling both in one sitting.


    Generated by Claude Code

  2. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Maintainer ruling (2026-08-17, recorded by PM session session_01NYgmGheCzM6NrHZN436Cxf). The maintainer accepted the decision-inbox report's recommendations in full, verbatim: 「同意」. Ruled together with #4797 (same SchemaRenderer evaluation contract):

    Ruled: Direction 3 now; Direction 1 deferred with a named restart condition; ⛔ Direction 2 never.

    1. Now (dispatchable): a publish/render-time loud diagnostic when an unevaluated ${…} expression reaches the DOM — the silent-literal/silent-blank shape becomes a named refusal pointing at the working channels (content, or host pre-resolution).
    2. Deferred — Direction 1 (a declared, closed set of expression-bindable text keys — title/label/value/description — evaluated by the memo): reopens as a spec-first design card when the named restart condition fires — the first real schema-authored dashboard need hits the gap (e.g. an AI-authoring flow that must bind statistic.value and cannot pre-resolve). Until then hosts pre-resolve, which is how every shipped dashboard works today. If reopened: the key set is declared in @objectstack/spec/types as a closed enum, never inferred — and the accept-surface change is fable-tier.
    3. ⛔ Direction 2 (merge props into the node) is permanently rejected — it re-legalizes the envelope shape the 教学面成体系教 props 信封,而渲染器从节点本身读键 —— 照 page-builder.md 写出的页面渲染一片空白框 #4786 teaching rewrite just retired.

    Label flipped needs-user-decision → pm:queue (the Direction-3 work) in the same stroke; when it lands, this card records the Direction-1 restart condition and closes or holds per the seat's judgement.


    Generated by Claude Code

  3. yinlianghui commented on Aug 18, 2026

    @yinlianghui
    CollaboratorAuthor

    PM 排序注记(objectui 分片,session_01GTRjn8xBqp75dk7kFupVRt):方向 3 的实施与 #4799(properties 求值 alias parity)同文件(packages/react/src/SchemaRenderer.tsx evaluation memo)—— 按批内文件不相交纪律,#4799 已先行派发(它落地后 properties 路径不再产生未求值 ${…},方向 3 的诊断不会带着一个已知假阳性源上线)。#4799 落 main 后本卡进下一空席,按分诊席 serial note 重定价。


    Generated by Claude Code

  4. self-assigned this
    on Aug 18, 2026
  5. yinlianghui commented on Aug 18, 2026

    @yinlianghui
    CollaboratorAuthor

    认领(方向 3)(PM 派发,objectui 分片):session_01GTRjn8xBqp75dk7kFupVRt,分支 claude/issue-4795-unevaluated-expr-diagnostic,os-dev 席即将开工。

    排序条件已满足:#4799(PR #5122)已落 main(0046d8f8c),properties 路径不再产生未求值 ${…} 假阳性源。按维护者裁定(评论 5312061331)只做方向 3 的 render-time 半边(dev 构建响亮诊断,生产零开销,参照 #5092 断言形先例);publish-time 半边若侦查发现自然挂点在 CLI 校验器文件,不做(#5115 席在飞,避免撞文件),如实记录留后续。⛔ 方向 2 永久否决、方向 1 延迟(重启条件在裁定评论)。文件面:packages/react/src/SchemaRenderer.tsx(或其邻接)+ 测试 + changeset,与在飞席位(CLI knownTypes、plugin-form.mdx)不相交。


    Generated by Claude Code

  6. yinlianghui commented on Aug 18, 2026

    @yinlianghui
    CollaboratorAuthor

    方向 3(render-time 半边)已落 PR #5129(draft) —— os-dev 席,session_01GTRjn8xBqp75dk7kFupVRt,分支 claude/issue-4795-unevaluated-expr-diagnostic,基线 0046d8f8c(含 PR #5122)。

    前提复测:卡面表格两行仍成立,标题那句话已被 #5122 推翻

    新基线上逐行实测(真 renderer,dataSource: { total: 99, n: 99 }):

    • 顶层 value / title / label ⇒ 字面量 ${…} 上屏 —— 仍成立(ui:card 那行还额外落成 DOM 属性 title="${data.total}")。
    • props 信封 ⇒ 求值出 99 但读不回,渲染成空白 —— 仍成立。
    • 成因判断(memo 只处理 content / props.* / 现加 properties.* / 谓词键)—— 复核通过。

    但卡面标题「除 content 外没有任何文本键既被求值又被读回」在含 #5122 的基线上已不再成立:写成 properties 信封时,value / title / label 三个键都既被求值又被读回(实测均渲染出 99)。机制是 #5122 让 properties.* 参与求值,而既有的 hoist 又把求值后的值抄到节点顶层 —— 正好是 StatisticRenderer 读 schema.value、CardTitle 读 schema.title 的位置。这不是本 PR 造成的,是 #5122 落地后的既成事实,在此记录以免下一位读者按旧表格外推。

    ⚠️ 留给维护者一裁:诊断文案里我没有写 properties。裁定原文把可用通道列为「content, or host pre-resolution」,而「properties 信封算不算 ui:* 的正式创作面」是契约/教学面判断(#4786 刚把 props 信封那个 workaround 从教学面撤掉),不该由我在一条诊断消息里替仓库定下来。文案是否加上 properties,请裁。

    射程边界(如实记录)

    诊断只认「未求值的 ${…} 到达 DOM」,所以 props 信封那一类(求值了但读不回 → 空白)不响,也按定义响不了 —— 那里到达 DOM 的是 99,只是落错了地方。卡面的这一半随方向 1 / 契约裁量走,不在本 PR 内。

    publish-time 半边不做:侦查后自然挂点落在 CLI 校验器文件,#5115 席在飞,按不撞文件纪律留后续。

    方向 1 的重启条件(本卡继续承载)

    裁定评论 5312061331 第 2 条:第一个真实的 schema 编排仪表盘需求撞上这个缺口(例如一个必须绑定 statistic.value 且无法宿主预解析的 AI 编排流程)。本 PR 未预告、未实现方向 1,措辞也未暗示。方向 2 未触碰。

    按裁定「落地后本卡记录重启条件并由席位判断关卡与否」—— 我的判断是先不关:PR 尚在 draft 且未合,而卡面此刻的主要价值已从「方向 3 待做」转为「方向 1 重启条件的存放处」+ 上面那条 properties 待裁。建议 PM 验收合并后再定:或改标题/正文收敛为方向 1 的重启卡,或关卡另开一张重启卡承载。


    Generated by Claude Code

  7. yinlianghui commented on Aug 18, 2026

    @yinlianghui
    CollaboratorAuthor

    PM 状态更新(session_01GTRjn8xBqp75dk7kFupVRt):方向 3 的 render-time 半边已随 PR #5129 落地验收(Part of,不关卡)。本卡标签转 needs-user-decision(摘 pm:queue —— 无 dispatchable 工作),现承载三件待维护者输入的事:

    1. 方向 1 重启条件(裁定评论 5312061331):首个真实 schema-authored dashboard 绑定需求出现时,以 spec-first 设计卡重启,键集封闭声明。
    2. properties 通道措辞(PR feat(react): 未求值的 ${…} 到达 DOM 时给出开发期响亮诊断 (#4795 方向 3) #5129 席位上交):PR fix(react): evaluate expressions under a node's properties, not just props (#4799) #5122 之后 properties: { value: '${…}' } 在 statistic/card/button 上已「既求值又读回」—— 诊断文案是否把它列为可用通道,取决于「properties 信封算不算 ui:* 正式创作面」;与 props 与 properties 同现时,alias 优先级按「读法」相反 —— 配置袋读到 properties,React prop 读到 props #5123(props/properties 同现两通道优先级相反)同族,建议一并裁。裁定后改文案是一行事。
    3. publish-time 半边(objectui check 侧的同类诊断):因当时 objectui check 的 knownTypes 是手写的注册表副本,已与真实注册表漂移:crud 通过校验但无任何渲染器,运行时落 unknown-component 占位符 #5115 席在飞 CLI 文件而留后续,PR fix(cli): objectui check 的 knownTypes 改由注册表推导,消灭手写副本的双向漂移 #5128 已落地后可另立小卡 —— 是否要这半边请一并示意。

    Generated by Claude Code

  8. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Blocked-by: objectstack-ai/objectstack#9599

    Maintainer ruling (2026-08-18, live PM chat with the triage seat, session session_01EUZUjvnLsqwHVkXkTv8TKQ) — Direction 1's restart condition is ruled MET, by product intent. Verbatim: 「"AI 一句话生成会动的仪表盘"或"用户在 Studio 自助配仪表盘"…这个是仪表盘的基本需求吧?」— a metadata-authored live dashboard is a basic requirement of the product, not a speculative capability, so the 2026-08-17 deferral's own reopening clause fires now rather than waiting for the first stranded author.

    Execution per the 08-17 ruling's pre-defined terms (comment 5312061331: closed enum in spec, never inferred; accept-surface change at the contract-review tier):

    • Spec half filed and queued: objectstack#9599 declares the closed bindable text-key set (title/label/value/description) in @objectstack/spec — contract-first, clause-② yes.
    • This card becomes the objectui half: extend the SchemaRenderer evaluation memo to the spec-declared key set, consuming the declaration rather than hard-coding a twin list. pm:blocked until objectstack#9599 lands (label swapped in the same stroke).
    • ⛔ Direction 2 stays permanently rejected; Direction 3 (shipped in PR feat(react): 未求值的 ${…} 到达 DOM 时给出开发期响亮诊断 (#4795 方向 3) #5129) is unaffected.

    Still awaiting the maintainer's one-liner on the two parked sub-questions: ② whether the diagnostic copy names the properties channel (triage recommends yes, consistent with the #5123 canonical-spelling ruling); ③ whether to build the publish-time half in objectui check (triage recommends yes, small card).


    Generated by Claude Code

  9. 35 remaining items

  10. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Hand-forward for the next taker: what the stopped item-2 dev measured before the stop — ⛔ no state change

    Addendum to the Release: 5864436645, from domain:ui seat 4 (session_015AUunPkX7UTkCH9e7AdZo1) at 2026-09-28T06:13Z. The dev stopped before any source edit: no commit of its own, no PR, and its only write was the empty branch push. These are leads, not answers. Where the seat re-checked one, it says so.

    • The spec floor. @objectstack/spec 17.1.0 is the first release that exports expressionBindableTextKeysFor and EXPRESSION_BINDABLE_TEXT_KEYS. The carriage map (statistic, card, button) and the lookup's answers are identical across 17.1.0–17.4.0. text, action:button, ui:card and page all answer the empty set. (Dev's reading from the published tarballs; not re-checked by the seat.)
    • Type keying. The dev read SchemaRenderer as passing the type string to the lookup verbatim, with no prefix stripping, and recommended the gate do the same. ⚠️ The comment beside that call (packages/react/src/SchemaRenderer.tsx, near L1664) says the lookup "keys on the bare registry name", so reconcile the two before relying on either.
    • CI already runs this gate on the repo itself. Seat re-checked: .github/workflows/lint.yml L489–491, step "Verify the CLI's own check command passes on this repository", run: pnpm check. So the before/after count over the repo's own corpus decides whether the new refusal turns main red. The ruling's sequencing gate exists for exactly that.
    • An open design point, not decided: which nodes count as "component nodes" under sub-rule (i). Walking every object that has a string type would also catch form-field definitions such as { type: 'text', label: '${…}' }, which would be a false refusal. Walking only through the composition keys (children) from a recognised root is the conservative reading. If the ruling text does not settle it, the next taker reports it rather than choosing silently.

    Generated by Claude Code

  11. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 12
    Session: session_012UwY3ahMixEFkfTUxMVkYm
    Account: huangyiirene (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-4795-check-bindable-text-keys
    Worktree: objectui-issue-4795
    Domain: domain:ui (the card's lane, unchanged)
    Seat: domain:spec#1. The maintainer's direct instruction in this seat's chat, 2026-09-29, verbatim: 「4795 你可以接手吗」. The card was unclaimed (pm:queue, no assignee) after domain:ui seat 4's release 5864436645.
    File surface: packages/cli/src/commands/check.ts (ruling item 2's refusal), a helper beside it under packages/cli/src/utils/ if one is needed, and tests under packages/cli/src/__tests__/. Also packages/cli/package.json only if the carriage lookup is consumed from @objectstack/spec directly (plus the lockfile that follows), and the objectui check docs page if one documents the command's refusals. Repository corpus documents only if the new refusal turns the repo's own pnpm check red, and only to move a ${…} onto its working channel. One .changeset/4795-*.md. Stop on breach and explain in the report.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; the maintainer, 2026-09-29: 「你可以派 opus 开发,然后等项目总监契约复审,」)
    Clause-②: yes — objectui check newly refuses ${…} in the four closed keys where expressionBindableTextKeysFor(type) excludes the key, as ruled (5406135987, 5479466110). The PR waits as a draft for the director seat's contract review.
    Thread-read: 5864499782
    Serial constraints cleared: #7015, the ruling's sequencing gate, closed completed 2026-09-01. PR #11069 (objectui#5250's wiring, domain:devx seat 2, a parked draft) also edits packages/cli/src/commands/check.ts. That is not a SINGLE_CLAIM_PATHS path, so the second to land merges main; #11069 is parked behind objectui#11070 and objectui#6152, so this card likely lands first. No other open PR touches packages/cli (files API over the 5 open PRs, main d1e683fa1). The file surface is disjoint from this seat's other two in-flight cards (objectui#11070 and objectui#6152, both in packages/types). The stopped dev's empty branch claude/issue-4795-check-unbindable-expr is left untouched; this round uses a new branch name.

  12. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 4795,
    "status": "needs_decision",
    "branch": "claude/issue-4795-check-bindable-text-keys",
    "pr": "#11126",
    "session": "session_012UwY3ahMixEFkfTUxMVkYm — the dispatching PM seat session (subagent run); the commit trailers carry it",
    "premise_still_valid": true,
    "summary": "Ruling item 2 is built in objectui check. findUnbindableTextExpressions (packages/cli/src/utils/unbindable-text-expressions.ts) refuses a ${…} on title/label/value/description of a component node whose expressionBindableTextKeysFor(type) excludes the key, no-row types included, and the run exits 1. Component nodes are the document root plus what children holds: a page document root keeps its own title, and form fields[] entries are not walked. An unregistered type (stat-card) only warns. The vocabulary and the carriage map are imported from @objectstack/spec, now a declared CLI dependency at ^17.1.0 (the range @object-ui/react declares). The type string is passed verbatim, as SchemaRenderer passes it. The docs page content/docs/utilities/cli.mdx and a @object-ui/cli minor changeset ship with it. Status is needs_decision because the new refusal turns the repository’s own pnpm check (lint.yml) red on exactly one file, examples/schema-catalog/src/schemas/plugin-editor/javascript-editor.json. That file is a code-editor whose value holds JavaScript template literals: literal content with no mechanical channel move, so per the dispatch it is reported, not edited (open question 1). PR #11126 is a draft for the director seat’s contract review, and the worktree is removed.",
    "tests": "All at ed32e7c unless noted. packages/cli: type-check exit 0. lint exit 0 (0 errors; 5 warnings, all in untouched files). pnpm exec vitest run packages/cli/ under the lock: Test Files 23 passed (23), Tests 330 passed (330). The new file check-unbindable-text-expression-4795.test.ts has 28 tests: refusals of text.value / action:button.label / card.label / statistic.title / ui:card.title, asserting key, path, channels and exit 1; passes of statistic.value/label/description, card.title/description, button.label, a lone ${ and an empty ${}; the children walk over an array and a single node; the page-root title exclusion, with the children still judged; a form fields[] label NOT refused, with a control that the same object under children IS refused; stat-card warning while the run passes; and the verbatim-lookup agreement table over every registered type plus the derived namespaced spellings. Repo pnpm check: main d1e683f exits 0 with 0 refusals and 0 warnings; ed32e7c exits 1 with 1 refusal (the code-editor sample) and 0 warnings. Runtime agreement, measured once through the real SchemaRenderer at e2f8e48 (the gate code is unchanged since): over 649 registered types, the keys left unevaluated equal the gate refusals on every type, 0 disagreements, and the evaluating types are exactly the types with a row. That DOM suite was not kept, because a CLI test importing @object-ui/react makes check:unused-deps flag the CLI generated-source allowance for it as stale (open question 3). Ablations ran via /home/user/objectstack/scripts/ablation-replace.mjs: anchor 1 to 0, marker 0 to 1, restore blob == HEAD and git diff HEAD empty, each re-run on ed32e7c. The subject resolves to the cli src by relative import, so there is no dist leg. (1) Refusal removed (severity forced to warning): predicted refusal pins red; observed 15 failed / 13 passed. (2) Walk widened to every object with a string type: predicted only the form-field pin red; observed exactly 1 failed / 27 passed, and the failure prints the refusal at fields → 0 → label. (3) Namespace stripped before the lookup: predicted 3 red (two agreement cases + ui:card.title); observed 5 red. The extra two are action:button.label and the multi-refusal case, because action:button strips to button, which has a label row. Gate exit codes: check-changeset-presence 0, check-changeset-no-major 0, check-changeset-overwrite 0, check-changeset-fixed 0 (at 90cca72), check:changeset-claims 0 (report-only; it named 5 pending changesets, re-read, none falsified by this diff), check:pending-changeset-literals 0 (at 90cca72), check:control-bytes 0, check:new-line-citations 0, check:phantom-deps 0, check:unused-deps 0, check:lockfile-integrity 0, check:lockfile-dedupe 0, check:spec-floors 0 (floor tarball fetched; the CLI artifact references EXPRESSION_BINDABLE_TEXT_KEYS and expressionBindableTextKeysFor from ./ui), check:installed-pin-claims 0, check:esm-specifiers 0, check:spec-symbols 0, check:self-import 0, check:doc-snippets 0 (681 of 681 blocks, after the scoped 34-package build under the lock), check:doc-examples 0, check-doc-component-types 0, check-prompt-component-keys 0, check-doc-example-ids 0, check-doc-fence-languages 0, check-doc-links 0, check-doc-expression-carriage 0 (report-only), toc-anchor-parity.test.tsx 15 passed, check-lint-coverage 0, check:test-path-roots 0, check:vi-mock-specifiers/inherit/override-shape 0, type-check:coverage 0, check:unreferenced-sources 0, check:dist-completeness 0, check:published-dist 0, check:shell-escape-residue 0, check-governed-queue-guard --test over the 7 paths NOT GOVERNED, check:governed-queue-guard self-test 0. The control-byte self-scan of the 5 authored files had no hits. NOT MEASURED: check:node-esm-load exit 1. Reason: it refused @object-ui/auth and @object-ui/react-runtime on provenance, because the turbo cache shared across worktrees replayed the objectui-issue-11070 builds. The @object-ui/cli entry was built by this tree and was among the 32 graded loads. Disclosed deviation: single-file vitest runs, CLI builds, pnpm check runs, and check:node-esm-load (which ran its own 43-task turbo build, about 3.5 minutes) ran outside os-verify-lock. The closure build, the docs build, the package suite and the doc gates ran under it.",
    "mcp_calls": "0",
    "api_writes": "3, all through the fleet-write relay (each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectui/pulls (draft #11126; body read back byte-identical, 13544 bytes). (2) label-write --assign huangyiirene = POST /repos//issues/11126/assignees (read back: assignee huangyiirene; no labels written, since objectui labels belong to labeler.yml and the dispatch named none). (3) this os-dev-report = POST /repos//issues/4795/comments. Plus git push x4 to the branch (not REST).",
    "open_questions": [
    {
    "question": "The repository’s own pnpm check (lint.yml step "Verify the CLI’s own check command passes on this repository") goes red on one file: examples/schema-catalog/src/schemas/plugin-editor/javascript-editor.json. It is a root code-editor whose value is a JavaScript sample using template literals (Hello, ${name}!). By the ruling’s letter (no-row types included) it is refused, but it is literal content, not a misplaced expression, and it has no mechanical move (content or properties.value would get it evaluated). How should it be resolved? The dispatch carried no four-axis framework, so the options below carry costs but no axis scores.",
    "options": [
    "A: rewrite the sample’s JavaScript so it holds no ${…} (string concatenation), in this PR or a precursor. One catalog file; the gate lands exactly as ruled. Cost: literal ${…} content on a no-row type’s closed key (code samples, shell ${VAR} text) is refused in users’ files too, with no escape spelling. The runtime dev diagnostic reportUnevaluatedExpressions already reports this same sample at render time.",
    "B: carve literal-content (type, key) pairs out of the refusal. This needs a new declaration of which keys hold content rather than display text. None exists in @objectstack/spec, and a local list would be the twin list the ruling forbids, so it is a spec-first decision and item 2 waits on it."
    ],
    "recommendation": "A, because it is one file, keeps the ruled rule intact, and agrees with the runtime diagnostic’s definition; the only measured pull for B is this one catalog sample. Editing that file was outside the claimed file surface (only mechanical channel moves were allowed), so it needs the seat’s or the maintainer’s go-ahead."
    },
    {
    "question": "The refusal’s channel line lists the type’s own evaluated keys, content, properties.KEY (evaluated, then hoisted onto the node) and host resolution, mirroring the runtime diagnostic’s vocabulary (objectui#7849). It says "evaluated", not "read back". For text.value (retired from TextSchema; the renderer reads content alone), properties.value is evaluated but not rendered. Keep the wording?",
    "options": [
    "A: keep it, as one vocabulary with the dev diagnostic that fires on the same node.",
    "B: drop properties.KEY and name only the type’s own evaluated keys, content and host resolution."
    ],
    "recommendation": "A, because diverging the build-time and render-time channel lists re-opens the contradiction objectui#7849 closed. The contract review can still choose B; it is a one-line change in workingChannels."
    },
    {
    "question": "A permanent runtime-agreement suite (every registered type rendered through the real SchemaRenderer and compared with the gate; measured green once) needs a CLI test that imports @object-ui/react. That import makes check:unused-deps flag the CLI’s generated-source allowance row for @object-ui/react in scripts/check-unused-dependencies.mjs as stale. Keep the current pins, or add the suite?",
    "options": [
    "A: keep the committed pins: the gate against the verbatim lookup over every registered type, plus the runtime half in @object-ui/react’s SchemaRenderer.bindableTextKeys.test.tsx.",
    "B: add the DOM suite and delete that allowance row (a ledger edit outside this claim’s file surface). The dependency would then be justified by a test import rather than its real generated-source reason."
    ],
    "recommendation": "A, because the two halves are each pinned against the same verbatim lookup, and B trades a documented runtime reason for a test-only one."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · walk boundary: nodes a renderer reaches through its own keys (trigger, header, footer, a page’s regions[].components[], a tab’s items[].content, panels[].content, carousel items) are not judged. No declaration of per-renderer node slots exists to derive them from, and the boundary fails quiet in the safe direction · noted in the PR’s Acceptance notes, not filed",
    "carrier: 承接者:无 · keys outside the four (e.g. badge.text) and the props-envelope blank render are outside item 2 as 5479466110 scoped it (it narrows 5406135987’s "any key outside that closed list") · noted, not filed",
    "carrier: 承接者:无 · the objectui#11007 triage ruling declined a nested validation walk in check. Its pin "no nested walk of its own" stays green, because this walk validates nothing and reads only the four keys · named in the PR for the reviewer, not filed",
    "carrier: 承接者:无 · pending changesets 5793-spec-range-floors and 6361-spec-floor-17-2-0 say the lockfile still resolves 17.2.0 on an edge. That was already false before this diff (it resolves 17.4.0); check:changeset-claims surfaced it as a re-read request · noted, not filed"
    ]
    }


    Generated by Claude Code

  13. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 4795,
    "status": "done",
    "branch": "claude/issue-4795-check-bindable-text-keys",
    "pr": "#11126",
    "session": "session_012UwY3ahMixEFkfTUxMVkYm — the dispatching PM seat session (subagent run); the commit trailers carry it",
    "premise_still_valid": true,
    "summary": "Round 2 applies the seat’s three answers. (1) A: examples/schema-catalog/src/schemas/plugin-editor/javascript-editor.json now builds its strings by concatenation and holds no ${. Run, it still logs Hello, Developer! and returns Welcome, Developer. No test, snapshot or docs fence pinned its content: the catalog imports it, safe-validate-corpus-6318 only asserts it validates, catalog-gallery-render skips it, and plugin-editor.mdx embeds it by id. The repo’s pnpm check is now exit 0 with 0 refusals. The docs page and the changeset gained one bullet stating the no-escape-spelling limitation, and the replacement PR body records it in its Acceptance notes. No carve-out and no local key list were built. (2) A and (3) A: the channel wording and the committed pins are unchanged, and the unused-deps allowance is untouched. Pushed as c985c4d on top of ed32e7c, with no rebase. origin/main moved to 76e9df0 but touched none of this PR’s paths and no JSON file, so nothing was merged. The PR stays a draft for the director seat’s contract review, and the PR body was not PATCHed: the full replacement is in pr_body_replacement below and at /tmp/claude-0/-home-user/d55b2d04-a665-5ea2-91b2-3d5d1265ca80/scratchpad/issue-4795/4795-r2-pr-body-replacement.md.",
    "tests": "All at c985c4d. Every heavy run went through /home/user/objectstack/scripts/pm/os-verify-lock.sh, each lock VERDICT command-exit 0 unless stated. pnpm check on the repo: exit 0, 0 refusals, 0 warnings. Its closing tally matches main’s: 265 validated, 167 recognised but not validated, 1 did not validate. Catalog: pnpm exec vitest run examples/schema-catalog/ gives Test Files 34 passed (34), Tests 2199 passed (2199). In safe-validate-corpus-6318.test.ts (verbose) Tests 8 passed (8), including plugin-editor/javascript-editor validates unchanged. packages/cli: pnpm exec vitest run packages/cli/ gives Test Files 23 passed (23), Tests 330 passed (330); type-check exit 0; lint exit 0 (0 errors, 5 warnings in untouched files). Docs: the scoped 34-package build under the lock (turbo cache, 35 of 35 cached), then check:doc-snippets exit 0 (681 of 681 blocks) and check:doc-examples exit 0. Also check-doc-component-types 0, check-prompt-component-keys 0, check-doc-example-ids 0, check-doc-fence-languages 0, check-doc-links 0, check-doc-expression-carriage 0 (report-only), and toc-anchor-parity.test.tsx 15 passed. Changesets: check-changeset-presence 0, check-changeset-no-major 0, check-changeset-overwrite 0, check-changeset-fixed 0, check:changeset-claims 0 (report-only; the same five pending changesets as round 1, none falsified), check:pending-changeset-literals 0. Other gates: check:control-bytes 0, check:new-line-citations 0, check:phantom-deps 0, check:unused-deps 0, check:lockfile-integrity 0, check:lockfile-dedupe 0. check-governed-queue-guard --test over the 8 changed paths reads NOT GOVERNED. The control-byte self-scan of the 3 round-2 files had no hits. check:node-esm-load: lock VERDICT command-exit 1. It again refused @object-ui/auth and @object-ui/react-runtime on provenance, because the shared turbo cache replayed objectui-issue-11070 builds. The @object-ui/cli entry was among the 32 graded loads. Left to CI, as the seat allowed. The round-1 ablations (15 / 1 / 5 red, as reported) stand: round 2 touched neither the gate nor its tests. The round-1 lock deviation was not repeated.",
    "mcp_calls": "0",
    "api_writes": "Round 2: 1 REST write, this os-dev-report = POST /repos//issues/4795/comments via the fleet-write relay (a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]). Plus git push x1 (ed32e7c..c985c4d, not REST). The PR body was not PATCHed, and no label, assignee or state write was made.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · known limitation: literal ${…} content in a no-row type’s closed key (code samples, shell ${VAR}) is refused, with no escape spelling. A literal-content carve-out is a spec-first decision, reopened on the first named user need · recorded in the docs page, the changeset and the replacement PR body’s Acceptance notes, not filed",
    "carrier: 承接者:无 · walk boundary: nodes a renderer reaches through its own keys (trigger, header, footer, a page’s regions[].components[], a tab’s items[].content, panels[].content, carousel items) are not judged · in the PR’s Acceptance notes, not filed",
    "carrier: 承接者:无 · keys outside the four (e.g. badge.text) and the props-envelope blank render are outside item 2 as 5479466110 scoped it · noted, not filed",
    "carrier: 承接者:无 · pending changesets 5793-spec-range-floors and 6361-spec-floor-17-2-0 say the lockfile still resolves 17.2.0 on an edge; that was already false before this diff (it resolves 17.4.0) · noted, not filed"
    ],
    "pr_body_replacement": "Fixes #4795\nClause-②: yes — objectui check newly refuses ${…} in the four closed keys where expressionBindableTextKeysFor(type) excludes the key, as ruled (5406135987, 5479466110). The PR waits as a draft for the director seat's contract review.\n\nRuling item 2 of #4795 (maintainer, 2026-08-31, comment 5479466110), built in objectui check as ruled. Draft for the director seat's contract review.\n\n## The repository's own pnpm check: red in round 1, resolved by the seat's answer A\n\n.github/workflows/lint.yml runs pnpm check on this repository (step "Verify the CLI's own check command passes on this repository"). Measured with the CLI built from each tree:\n\n| tree | pnpm check exit | refusals | warnings |\n|:--|:--|:--|:--|\n| main d1e683fa1 (no gate) | 0 | 0 | 0 |\n| round 1, ed32e7c6b | 1 | 1 | 0 |\n| this branch, c985c4d7d | 0 | 0 | 0 |\n\nThe round-1 refusal. It was examples/schema-catalog/src/schemas/plugin-editor/javascript-editor.json: a root code-editor whose value was a JavaScript sample using template literals. That is literal content, not a misplaced expression, and it had no mechanical channel move.\n\nThe fix. The seat chose A under ruling 5479466110: 「no-row types included」, and 「A row can always be added later with a named need + its own measurement」. The sample now builds the same strings by concatenation. Run, it still logs Hello, Developer! and returns Welcome, Developer.\n\nNothing pinned the old content. No test, snapshot or docs fence reproduces it:\n\n- the catalog registers the file by import;\n- safe-validate-corpus-6318.test.ts asserts only that it validates;\n- catalog-gallery-render.test.tsx skips it, because Monaco cannot load under happy-dom;\n- plugin-editor.mdx embeds it by id through SchemaExample.\n\nThe run's closing tally is the same as on main: 265 validated, 167 recognised but not validated, 1 did not validate.\n\n## What it does\n\nfindUnbindableTextExpressions (new, packages/cli/src/utils/unbindable-text-expressions.ts), called by check on every recognised file, from either recognition arm:\n\n- The rule. Each of the four keys EXPRESSION_BINDABLE_TEXT_KEYS on a component node is judged against expressionBindableTextKeysFor(type). Both are imported from @objectstack/spec/ui: the vocabulary and the carriage map are consumed, not copied.\n- Component nodes (sub-rule i). The document root, plus every node children holds (an array or a single node), recursively. A root of type page is not judged itself, because its title is a page key; its children still are.\n- Expression. The evaluator's own interpolation pattern, as ExpressionEvaluator.evaluate matches it. A lone ${ or an empty ${} is not an expression there, so it is not one here.\n- Refusal vs warning (sub-rule ii). A registered type (the isKnownSchemaType universe) is refused: a red x Unevaluated expression in FILE at PATH: … line plus a channel line, and it counts toward the run's Found N errors, so the exit code is 1. A type no registered component answers to gets a yellow ⚠️ Expression not judged in … line, and the run still passes.\n- Message. It follows the describeFirstIssue shape objectui#11007 / PR #11053 gave check. The path is spelled by formatIssuePath, the formatter objectui validate prints with. It names the key, the path, the type, the keys that type does evaluate, and the channels SchemaRenderer evaluates: the type's own evaluated keys, content, properties.KEY (evaluated, then hoisted onto the node), or resolving the value in the host.\n\n@objectstack/spec becomes a declared dependency of @object-ui/cli at ^17.1.0. That is the range @object-ui/react declares, and @object-ui/react is the runtime consumer of the same lookup. The lockfile gains only the importer entry, which resolves the installed version.\n\nDocs: content/docs/utilities/cli.mdx gains the rule, the two sub-rules and the real output. It also says a refused expression now fails the run, where the page used to say only unreadable JSON did, and that a literal ${…} has no escape spelling (the changeset says so too). Changeset: @object-ui/cli minor.\n\n## Premises re-derived on main d1e683fa1\n\n1. Spec floor and carriage map. The lockfile carries a single @objectstack/spec, and the installed node_modules/@objectstack/spec/package.json reads 17.4.0. It exports both symbols. Its answers: statistic gives label / value / description; card gives title / description; button gives label; text, action:button, ui:card and page give the empty set. check:spec-floors fetched the declared-floor tarball and passed: the CLI artifact's two ./ui symbols are present at the floor. Nothing here depends on 17.5.0.\n2. Type keying. The evaluation memo in SchemaRenderer calls expressionBindableTextKeysFor(typeof newSchema.type === 'string' ? newSchema.type : ''), verbatim, with no prefix stripping. The comment "keys on the bare registry name" describes the spec map's keys; it does not describe a normalization. The gate passes node.type verbatim.\n - Measured once through the real SchemaRenderer (at e2f8e48d9; the gate's code is unchanged since): every one of the 649 registered types was rendered with the four keys set to ${data.total}. On every type, the keys it left unevaluated equalled the gate's refusals, 0 disagreements. The types it evaluated anything on were exactly those with a row.\n - Not committed, for one reason: a CLI test importing @object-ui/react turns the CLI's generated-source allowance for that dependency stale in check:unused-deps (DECLARED_WITHOUT_IMPORT). That ledger is outside this claim's file surface.\n - What is committed instead: the unit-tier table. It pins the gate against the verbatim lookup on every registered type, and derives the namespaced spellings a prefix-stripping gate would get wrong. The runtime half stays pinned by the @object-ui/react suite SchemaRenderer.bindableTextKeys.test.tsx ("a namespaced spelling is not silently normalized").\n3. Sub-rule (i) premise: holds.\n - SchemaRenderer does not recurse on its own. Each renderer hands keys back through renderChildren / renderNodeSlot / a direct SchemaRenderer.\n - A form's fields[] entries are rendered by the form's renderFieldComponent (field widgets under field:*), never through SchemaRenderer.\n - check judged the root only before this change.\n - The walk follows children alone. That is the one composition key BaseSchema declares, and the same single spelling core's validateChildren and the SDUI parser's CHILD_LIST_KEY follow.\n - Census of the recognised corpus: objects with a string type that are not nodes sit under form fields[], filter-builder fields[], grid columns[], dashboard widgets[] / globalFilters[], and even object-grid's columns[].summary ({ \"type\": \"count_unique\" }) and selection ({ \"type\": \"multiple\" }). A walk over every typed object would judge all of them.\n4. Corpus. See the table above: one offender in round 1, not mechanical. It was resolved in round 2 by rewriting the sample, the seat's answer A.\n5. Sub-rule (ii). Pinned: stat-card warns and the run passes. text.value, action:button.label, card.label and ui:card.title are refused. statistic.value, card.title and button.label pass.\n6. Message format. See above.\n7. PR #11069. Not read from and not edited toward. The second of the two to land merges main.\n\n## Tests\n\n- packages/cli/src/__tests__/check-unbindable-text-expression-4795.test.ts (unit tier, 28 tests). Expected text is derived from the command's own formatters, fed from the gate's real findings. It covers:\n - the refusals, with the key, path, type, channels and exit code;\n - the passes, including a lone ${ and an empty ${};\n - the walk through children (array and single node) and multiple refusals per file;\n - the page-root title exclusion, with the page's children still judged;\n - the form-field label non-refusal, with a control: the same object under children IS refused;\n - the stat-card warning;\n - validity-arm files judged and unrecognised files not;\n - the verbatim-lookup agreement table over every registered type.\n- Whole package, pnpm exec vitest run packages/cli/ at c985c4d7d, under the verify lock: Test Files 23 passed (23), Tests 330 passed (330).\n- The catalog the rewritten sample lives in, pnpm exec vitest run examples/schema-catalog/ at c985c4d7d, under the verify lock: Test Files 34 passed (34), Tests 2199 passed (2199). Within it, safe-validate-corpus-6318.test.ts passes plugin-editor/javascript-editor validates unchanged.\n\n## Ablations\n\nEach direction was predicted before the run. Mutations went through ablation-replace.mjs: in each run the anchor went 1 to 0, the marker 0 to 1, and the restore proved blob == HEAD with git diff HEAD empty. All three were re-run on ed32e7c6b. Round 2 touched neither the gate nor its tests, so these readings stand.\n\n1. Refusal removed (severity forced to warning). Predicted: the refusal pins go red. Observed: 15 failed | 13 passed. The passes, the page-root exclusion, the stat-card warning and the unrecognised-file pins stayed green.\n2. Walk widened to every object with a string type. Predicted: only the form-field pin goes red. Observed: exactly that, 1 failed | 27 passed, and the failure prints the refusal at fields → 0 → label.\n3. Namespace stripped before the lookup. Predicted: the two agreement cases and ui:card.title go red. Observed: 5 red, those 3 plus action:button.label and "refuses every offending node". action:button strips to button, which has a label row. The direction matched the prediction, with two more reds than predicted, both explained.\n\n## Gates (exit codes; at c985c4d7d unless noted)\n\nHeavy runs (builds, test suites, pnpm check, the doc compile gates, check:node-esm-load) went through the shared verify lock in round 2.\n\n- CLI package: type-check 0 · lint 0 (0 errors; 5 warnings, all in files this PR does not touch) · tests 0 (above).\n- Repo check: pnpm check 0: 0 refusals, 0 warnings.\n- Changesets: check-changeset-presence 0 · check-changeset-no-major 0 · check-changeset-overwrite 0 · check-changeset-fixed 0 · check:changeset-claims 0 (report-only; it names the same five pending changesets as round 1, and none is falsified by this diff) · check:pending-changeset-literals 0.\n- Bytes and citations: check:control-bytes 0 · check:new-line-citations 0.\n- Dependencies and lockfile: check:phantom-deps 0 · check:unused-deps 0 · check:lockfile-integrity 0 · check:lockfile-dedupe 0. At ed32e7c6b (round 2 moved no manifest and no CLI source): check:spec-floors 0 · check:installed-pin-claims 0 · check:esm-specifiers 0 · check:spec-symbols 0 · check:self-import 0.\n- Docs: check:doc-snippets 0 (681 of 681 blocks) · check:doc-examples 0 · check-doc-component-types 0 · check-prompt-component-keys 0 · check-doc-example-ids 0 · check-doc-fence-languages 0 · check-doc-links 0 · check-doc-expression-carriage 0 (report-only) · toc-anchor-parity.test.tsx 15 passed.\n- Test shape and dist, at ed32e7c6b (round 2 touched no source or test file): check-lint-coverage 0 · check:test-path-roots 0 · check:vi-mock-* (three) 0 · type-check:coverage 0 · check:unreferenced-sources 0 · check:dist-completeness 0 · check:published-dist 0 · check:shell-escape-residue 0.\n- Governed surface: check-governed-queue-guard --test over the eight paths: NOT GOVERNED · check:governed-queue-guard self-test 0 (at ed32e7c6b).\n- check:node-esm-load: 1, left to CI. Under the lock at c985c4d7d, it again refused @object-ui/auth and @object-ui/react-runtime on provenance: the turbo cache is shared across worktrees and replayed another tree's build. The @object-ui/cli entry was among the 32 graded loads. CI builds fresh.\n\n## Acceptance notes\n\n- Known limitation: no escape spelling.\n - A literal ${…} in a closed key of a type with no carriage row is refused, and nothing lets it through. That covers a code sample in a code-editor value and shell ${VAR} text.\n - A carve-out for literal content is a spec-first decision, reopened on the first named user need. None is built here, and there is no local key list.\n - The docs page and the changeset say this. This repository's own sample was the only measured instance, and the runtime dev diagnostic already reported it.\n- Walk boundary. Nodes a renderer reaches through a key of its own are not walked: trigger, header, footer, a page's regions[].components[], a tab's items[].content, panels[].content and carousel items. No declaration of per-renderer node slots exists to derive them from. The boundary fails quiet in the safe direction: it never produces a false refusal.\n- Keys outside the four (e.g. badge.text) are not judged. 5479466110 set item 2's scope to the carriage map, which narrows 5406135987's "any key outside that closed list". The props-envelope blank render is outside that scope too.\n- check reads .json only. .yaml / .yml are globbed and never read, as check.ts already stated.\n- Two rulings meet in check.ts. The objectui#11007 triage ruling declined a nested validation walk in check. Its pin "no nested walk of its own" stays green, because this walk validates nothing and reads only the four keys. Named here so the reviewer sees both rulings.\n- The channel line says "evaluated", not "read back". It mirrors the runtime diagnostic's vocabulary (objectui#7849). For text.value, which TextSchema retired and whose renderer reads content alone, properties.value is evaluated and hoisted but not rendered. The seat kept this wording (answer A, one vocabulary with the runtime diagnostic); the contract review may still choose B, which would be a one-line change in workingChannels.\n- Runtime agreement. The real SchemaRenderer was measured once over all 649 registered types, with 0 disagreements. The committed pins are the two verbatim-lookup halves. The seat chose not to add the DOM suite (answer A), so the CLI's @object-ui/react allowance in check:unused-deps is untouched.\n\nSession: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm\n\n---\n_Generated by Claude Code_\n"
    }


    Generated by Claude Code

  14. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    ACCEPT: PR #11126 at c985c4d7d (ruling item 2; Fixes #4795). From the domain:spec @ objectui seat, session session_012UwY3ahMixEFkfTUxMVkYm.

    Checked against GitHub, not the reports (5894488882, 5894716866):

    • Form. Draft to main; first line Fixes #4795; the Clause-②: yes line; assignee huangyiirene. 8 files, +654/−8. The PR body was replaced by this seat with the dev's round-2 body; the read-back is identical and the footer is intact.
    • The rule, as ruled (5479466110).
      • findUnbindableTextExpressions judges the four EXPRESSION_BINDABLE_TEXT_KEYS on each component node against expressionBindableTextKeysFor(type). Both are imported from @objectstack/spec/ui (consumed, not copied). No-row types are included.
      • A registered type is refused, and the run exits 1. An unregistered type warns.
      • The page root's own title is excluded, while its children are still judged.
    • Sub-rule (i): the seat's premise-bound reading, verified.
      • The walk follows children from the root: the one composition key BaseSchema declares, also followed by core's validateChildren and the parser's CHILD_LIST_KEY.
      • Form fields[] entries are rendered by the form's own renderFieldComponent, never through SchemaRenderer, and are not walked.
      • Pinned with a control: the same object under children IS refused. Ablation 2 (widening the walk to every typed object) reddens exactly that pin.
    • Runtime agreement. Measured once through the real SchemaRenderer over all 649 registered types: 0 disagreements. The type string is passed verbatim, as the runtime passes it. Ablation 3 (namespace stripping) goes red, as it should.
    • The repository's own pnpm check. Exit 0 with 0 refusals at the head. It was 1 refusal in round 1, resolved by the seat's answer A: the code-editor JavaScript sample now builds its strings by concatenation, and it behaves the same. The known limitation (literal ${…} in a no-row type's closed key has no escape spelling; a carve-out is a spec-first decision, reopened on the first named user need) is stated in the docs page, the changeset and the PR's Acceptance notes.
    • Changeset. @object-ui/cli minor. @objectstack/spec becomes a declared CLI dependency at ^17.1.0 (the range @object-ui/react declares); check:spec-floors passes at that floor.
    • CI. Still running at this post (30 success, 3 skipped, the rest in progress). The contract review starts on a green head. check:node-esm-load was left to CI: locally, the shared turbo cache replayed another worktree's builds.
    • Governed surface. None of the 8 paths.

    The seat's round-1 answers, recorded:

    • pnpm check red → A: rewrite the sample.
    • Channel wording → A: one vocabulary with the runtime diagnostic, objectui#7849. The contract review may still choose B.
    • The runtime DOM suite → A: keep the committed pins; the unused-deps allowance is untouched.

    Noted, not filed (carrier: none), all in the PR's Acceptance notes:

    • The walk boundary: renderer-owned node slots such as trigger, header and regions[].components[] are not walked. It fails quiet in the safe direction.
    • Keys outside the four are outside item 2 as 5479466110 scoped it.
    • The objectui#11007 ruling's 「no nested walk」 pin stays green, because this walk validates nothing.
    • Pending changesets 5793-spec-range-floors and 6361-spec-floor-17-2-0 state a stale 17.2.0 edge. That was already false before this diff.

    Landing: needs:contract-review goes on the PR in this act. The PR stays a draft until the director seat's contract review PASSes on a green head (the maintainer, 2026-09-29: 「你可以派 opus 开发,然后等项目总监契约复审,」). On landing, this card closes through Fixes: item 2 is its last open item. PR #11069, which also edits check.ts, merges main after this lands.

  15. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 4795,
    "status": "done",
    "branch": "claude/issue-4795-check-bindable-text-keys",
    "pr": "#11126",
    "session": "session_012UwY3ahMixEFkfTUxMVkYm — the dispatching PM seat session (subagent run); the commit trailers carry it",
    "premise_still_valid": true,
    "summary": "Round 3: merged origin/main (81f8498, PR #11086: @objectstack/* 17.5.0, zod 4.6.5) into the branch and regenerated the lockfile with pnpm install. There was no rebase, amend or force-push. Head is 4f29cc3: 7e56c20 is the textual merge, which was clean, and 4f29cc3 is the generated lockfile commit. The only lockfile change is the packages/cli importer entry for @objectstack/spec (^17.1.0), whose version moved from 17.4.0(ai@7.0.65(zod@4.4.3)) to 17.5.0(ai@7.0.65(zod@4.6.5)), the snapshot the rest of the workspace uses. The carry proof: the non-lockfile delta has the same git patch-id --stable before and after, fddcc0fbfcd30e517fa521b4af5cf528c5b4cf55. c985c4d against its merge base d1e683f is the same as 4f29cc3 against its merge base 81f8498, and the two diffs are byte-identical (7 files, +651/-8). Nothing outside pnpm-lock.yaml changed. The merge group’s failure is gone: pnpm install --frozen-lockfile from a clean install (all 47 node_modules dirs removed) exits 0 with "Lockfile is up to date". Control leg: the same command on 7e56c20’s pre-regen lockfile exits 1 with the merge group’s exact ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY for “@objectstack/spec@17.4.0(ai@7.0.65(zod@4.4.3))”. The restore was proved by blob hash == HEAD blob and an empty git diff HEAD. The PR stays a draft. It was not marked ready or re-enqueued, and the PR body was not PATCHed; its readings name c985c4d, and the round-3 readings are in this report. The worktree is removed.",
    "tests": "All at 4f29cc3, heavy runs under /home/user/objectstack/scripts/pm/os-verify-lock.sh. Installed @objectstack/spec is 17.5.0 (packages/cli links @objectstack+spec@17.5.0_ai@7.0.65_zod@4.6.5_). The carriage map read off it is identical to the 17.4.0 reading: EXPRESSION_BINDABLE_TEXT_KEYS [title, label, value, description]; statistic [label, value, description]; card [title, description]; button [label]; text, action:button, ui:card, page and code-editor []. Agreement-table pin on 17.5.0: check-unbindable-text-expression-4795.test.ts verbose, Tests 28 passed (28), including the gate asks the lookup what SchemaRenderer asks, on every registered type: “refuses exactly the keys the verbatim lookup excludes” and “gives a namespaced spelling no row, even where its bare name has one”. packages/cli: the CLI closure build ran under the lock (9 tasks, 0 cached, exit 0). vitest run packages/cli/ gives Test Files 23 passed (23), Tests 330 passed (330). type-check exit 0; lint exit 0 (0 errors). Repo pnpm check: exit 0, 0 refusals, 0 warnings. The closing tally is 266 validated, 166 recognised but not validated, 1 did not validate. That tally moved from 265/167/1 with the new base; this diff’s recognition code is unchanged, per the identical patch-id. Lockfile and dependency gates: check:lockfile-integrity 0 (VERDICT clean), check:lockfile-dedupe 0 (VERDICT deduped), check:unused-deps 0, check:phantom-deps 0. check:spec-floors 0 after a workspace build under the lock (43 tasks, exit 0); it judged the CLI floor 17.1.0 against its two ./ui symbols. check:installed-pin-claims 0. Changesets: check-changeset-presence 0, check-changeset-no-major 0, check-changeset-overwrite 0, check-changeset-fixed 0, check:changeset-claims 0 (report-only; the same five pending changesets as before), check:pending-changeset-literals 0. check:control-bytes 0; check:new-line-citations 0. check:node-esm-load: 0 this round. Everything was built by this tree, so it read "Provenance leg: 37 of 37 gradable entries were built by this tree" and "Load leg: 34 of 39 published ESM entries imported and evaluated"; round 2’s shared-cache refusal did not recur. One transport note: the first git push attempt printed "failed to push some refs" with no rejection reason; the immediate retry fast-forwarded c985c4d..4f29cc3 with no force.",
    "mcp_calls": "0",
    "api_writes": "Round 3: 1 REST write, this os-dev-report = POST /repos//issues/4795/comments via the fleet-write relay (a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]). Plus git push x1 (fast-forward c985c4d..4f29cc3, not REST; see the transport note in tests). No PR body, label, assignee, ready or queue write was made.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · a lockfile edit that textually merges clean can still name a snapshot main no longer carries; the merge group caught it here at pnpm install --frozen-lockfile, and nothing on the PR leg re-derives a PR’s lockfile against a moving main · noted, not filed"
    ]
    }


    Generated by Claude Code

  16. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Landed. PR #11126 merged through the merge queue as 33da643e9. From the domain:spec @ objectui seat, session session_012UwY3ahMixEFkfTUxMVkYm.

    Verified by content on main:

    • The squash diff and the PR's net diff (81f849852..4f29cc319) have the same git patch-id --stable (31ae1646e1b0003559650d81d700ac1843976e7a). The non-lockfile patch-id is fddcc0fbfcd30e517fa521b4af5cf528c5b4cf55, the delta the director's record 5903644098 reviewed, carried across the lockfile regeneration by the PR body's Regen-provenance: line.
    • 8 files, +654/−8: the gate findUnbindableTextExpressions in packages/cli/src/utils/unbindable-text-expressions.ts, called from check on every recognised file; its 28-test pin file; the cli.mdx section; the rewritten catalog sample; the @objectstack/spec dependency and its lockfile importer entry; the @object-ui/cli minor changeset.
    • The commit's co-author trailer is the model-free Co-authored-by: Claude line only. A grep of the message for a model identifier finds 0. The squash message does not carry the Regen-provenance: line; the PR body does.

    This card closed completed through Fixes, and pm:dispatched is removed.

    Carried forward, not reopened here:

    • The walk boundary (node slots a renderer reaches through its own key: trigger, header, footer, regions[].components[], tab and panel content, carousel items) is objectui#11170, filed by the director seat from this PR's review.
    • The known limitation stays as documented: a literal ${…} in a closed key of a type with no carriage row has no escape spelling. It is a spec-first decision, reopened on the first named user need.
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 seatpriority:p3

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions