Repository navigation
finding(react,docs): properties evaluates and hoists ${…} but the unevaluated-expression diagnostic omits it, while the props-bag diagnostic recommends it #7849
Description
Activity
- changed the title
[-]finding(react,docs): evaluates and hoists but the unevaluated-expression diagnostic omits it, while the props-bag diagnostic recommends it[/-][+]finding(react,docs): `properties` evaluates and hoists `${…}` but the unevaluated-expression diagnostic omits it, while the props-bag diagnostic recommends it[/+]on Sep 6, 2026 - addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 6, 2026 Claim: PM 席(
domain:uilane)认领本卡并派发给os-dev执行席。- Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
- Branch:
claude/issue-7849-properties-diagnostic-consistency - Worktree:
/home/user/objectui-issue-7849 - Domain:
domain:ui - File surface: 承载那两条诊断的那个文件(卡里指向
packages/react/src/SchemaRenderer.tsx,以你实际读到的为准)+ 同包内一个 pin 测试文件。⛔ 不碰content/docs/的任何一页、⛔ 不碰packages/types/、⛔ 不改properties的运行时行为(不动 hoist 逻辑本身)。 - Container & model: 本会话容器 ·
opus - Clause-②: no
- Serial constraints cleared: 无冲突 —— 在飞的 objectui#7933 改
packages/components/src/renderers/layout/page.tsx;未合并的 PR objectui#7943(网关测试)/ objectui#7948(ObjectView.tsx)/ objectui#7949(quick-start.md)均与本卡无文件交集。
两种读法在一件事上是一致的,那件事就是交付物
卡把它写成一个二选一:读法 1「诊断不完整,
properties能用、该说、该教」,读法 2「properties是正在退役的 COMPAT shim,所以props那条诊断不该推荐它,遗漏才是对的、推荐才是 bug」。⭐ 但两种读法都同意一件事:同一个文件里的两条诊断在互相矛盾 —— 一条叫作者去用
properties,另一条在枚举「今天真能求值并读回的通道」时把它漏掉。无论渠道往哪边走,让作者同时读到这两句话都是缺陷。这就是本卡的交付物。⛔ 第一条要求:测出方向,⛔ 不许从注释里的
COMPAT一词推断properties是否有在案的退役计划? 去找:ADR、已有卡、代码里的 deprecation 标注、CHANGELOG/changeset 里的措辞。逐字贴出你找到的东西,或者逐字贴出你搜过而没找到的证据(搜了哪些面、用了什么词)。⛔
SchemaRenderer.tsx里那句COMPAT: Hoist 'properties' up to schema level不构成退役证据。 一个写着 COMPAT 的注释可能意味着「兼容旧写法、将退」,也可能意味着「这是兼容层的实现位置」。从一个标签推断意图,正是本仓今晚已经踩过的错误(objectui#7565)。然后:
- 测出有在案退役 ⇒ 修
props那条诊断,让它不再推荐properties;给出替代建议时用那条诊断里已有的说法(节点级、schema 声明的键),⛔ 不发明新说法。 - 测出没有 ⇒ 修「未求值表达式」那条诊断,把
properties加进它枚举的通道里。
⛔ 两种情况下都不做的事
⛔ 不给文档语料增加任何
properties示例。 立卡席位说得对:introducing an authoring channel is not a docs card's decision,而content/docs/里propertiesbag 出现次数为零。本卡的面只有那两条诊断消息,⛔ 永远不包括指南。这也是本卡 Clause-② 为no的原因 —— 只改诊断文本,不改任何 schema 的接受/拒绝行为、不改properties的运行时行为、不扩大公开面。⛔ 不动 hoist 逻辑本身。 无论方向如何,本卡都不改
properties今天的运行时行为。复现卡的测量
卡在
origin/main 3faaa7d0f上用真实渲染器测过:badge的properties.label = "${data.status}"渲染出completed并应用 variant 类;card的properties.className条件表达式生效;同样的键放在props下则读不回来。在你的树上复现一次并贴读数;⚠️ 若复现不出来(比如properties现在也不工作了),停下报回 —— 那时两种读法都不成立。钉子
无论走哪个方向,都要有一条测试钉住两条诊断彼此一致:即「被
props诊断推荐的通道」与「被未求值诊断列为可用的通道」不再矛盾。正向对照:把矛盾放回去,钉子必须变红并点名自己;还原按状态证明(git diff HEAD为空 且 blob 回到 HEAD 的 blob),⛔ 不靠退出码,⚠️ 用git checkout HEAD -- <绝对路径>。
Generated by Claude Code
os-dev-report
{ "issue": 7849, "status": "done", "branch": "claude/issue-7849-properties-diagnostic-consistency", "pr": "https://github.com/objectstack-ai/objectui/pull/7955", "premise_still_valid": true, "summary": "The two contradicting diagnostics are NOT both in SchemaRenderer.tsx (the card's guess) — they live in packages/react/src/utils/propsBagDiagnostic.ts (recommends `properties`) and packages/react/src/utils/unevaluatedExpression.ts (enumerated the working channels and omitted it). Direction was MEASURED, not inferred from the COMPAT label: no retirement is on record for `properties`, so the ENUMERATION was the wrong half and `properties.*` was added to it, borrowing the sibling message's own vocabulary (hoisted onto the node / a renderer declared as ({ schema })) rather than inventing wording. Diagnostic text only — no evaluation, hoist or schema behaviour changed; no content/docs page, no packages/types, no governance surface touched. Clause-2 = no, carried verbatim in the PR body.", "retirement_evidence": { "verdict": "NO retirement plan on record for `properties` — direction 2 (COMPAT shim on its way out) is disproved", "surfaces_searched_and_found": [ "@objectstack/spec@17.2.0 src/ui/component.zod.ts, VERBATIM: 'The vocabulary here is ALIVE — this is not dead surface to retire under ADR-0049.'", "@objectstack/spec@17.2.0 same file, VERBATIM: 'PageComponentSchema.properties is still z.record(z.string(), z.unknown())' — the carrier is unchanged ON PURPOSE (maintainer 2026-08-05 took direction A, DECLINED direction B)", "@objectstack/spec@17.2.0 same file: `properties` is GATED at the authoring door by packages/lint/src/validate-component-props.ts — an actively enforced channel, not an unadvertised shim", "objectui SchemaRenderer.tsx:145 VERBATIM: 'The legacy `props` bag, minus every key the canonical `properties` bag also' — and :168 VERBATIM: '(`props` is an annotated legacy alias)'. The LEGACY spelling is `props`; `properties` is the canonical one.", "objectui propsBagDiagnostic.ts:40 VERBATIM: '`props` is documented as the annotated legacy alias'", "unevaluatedExpression.ts's own header already knew: the memo 'evaluates `content`, the `properties` / `props` bags and the predicate keys' — only the user-facing enumeration was out of sync" ], "surfaces_searched_and_NOT_found": [ "docs/adr/** — grep 'properties' + 'deprecat|retire|sunset': no ADR retires the properties bag (hits are the FAB, sys_* tables, requiredWhen — all unrelated)", "@objectstack/spec@17.2.0 retiredKey() tombstones — the mechanism IS live in spec (ADR-0036 uses it), and NEITHER `props` NOR `properties` is tombstoned", "@deprecated markers across packages/*/src — none on `properties`", ".changeset/*.md (960 files) + every CHANGELOG.md — grep 'deprecat|retire|sunset'|'hoist': the retirement machinery is heavily exercised in this repo and NONE of it names `properties`", "GitHub issues — one targeted search for retiring the properties bag/hoist returned 12 results, all retiring OTHER things (EventHandlersSchema, editMode, snake keys, allowRestore/allowPurge). #6760 hardened the hoist with a guard rather than retiring it." ], "empty_result_is_a_real_reading": "REST /search/issues returned HTTP 403 in this container (repo-scoped REST reads are fine, HTTP 200), so the issue search was switched to one targeted MCP search_issues — CHANNEL SWITCH DECLARED. A control query in the same session returned #7849/#6760/#6752, so the empty retirement result is a measurement, not a dead channel." }, "reproduction_of_the_three_readings": { "harness": "real renderers (packages/components/src/renderers) driven by SchemaRenderer, rendered DOM read back, on this branch's base 3b30920f1", "badge_properties": "WORKS — class '... border-transparent bg-secondary text-secondary-foreground ...' AND text content 'completed'", "card_properties_className": "WORKS — class 'rounded-lg border bg-card text-card-foreground shadow-sm border-red-500'", "same_keys_under_props": "DOES NOT WORK — badge: 'completed' lands only on a DOM attribute, text content EMPTY; card: border-red-500 ABSENT", "verdict": "all three reproduce the card exactly, so both readings stood and the retirement measurement is what chose the direction" }, "positive_control": { "injection": "contradiction reinstated — `properties.*` dropped back out of the enumeration", "mutation_confirmed_on_disk": "anchored greps, not a bare diffstat: removed_text_count=0 (want 0), injected_text_count=1 (want 1); blob 4a476fe1efb027d8cdb5e456ee78cc5139e3e865 -> fa52deb782a391fe034fd52f564585239985c92b", "observed_direction": "RED, and it names itself — exactly 1 of 5 tests failed: 'the channel the props diagnostic RECOMMENDS is listed by the unevaluated diagnostic as one that works', AssertionError quoting objectui#7849 and both messages. 4 passed, so the failure is targeted, not collateral.", "restore_proven_by_state": "git diff HEAD EMPTY and blob back to HEAD's 4a476fe1efb027d8cdb5e456ee78cc5139e3e865; working tree clean. Not read from an exit code. Script carried an absolute-path trap on EXIT INT TERM.", "no_dist_leg_needed": "the pin imports the util sources by relative path, so nothing was resolved through dist and no rebuild could stale the reading" }, "tests": "All at FINAL commit 888ebec86, clean tree. [1] @object-ui/react pnpm test -> EXIT 0, 'RUN v4.1.10 /home/user/objectui-issue-7849', Test Files 73 passed (73), Tests 876 passed (876) — correct root and a real file count, not the 22-file apps/console false green of objectui#3378. [2] @object-ui/react pnpm run type-check -> EXIT 0 (tsc --noEmit && tsc -p tsconfig.test.json), 0 TS lines; NON-VACUITY PROVEN via --listFiles: the source is in both programs and the new pin file is in the test program (1 hit each). [3] eslint . over packages/react -> EXIT 0, 138 files (count read from --format json), 0 errors; my two files are 0 errors / 0 warnings (the 347 warnings are pre-existing elsewhere in the package). [4] check-changeset-presence.mjs EXIT 0, check-changeset-fixed.mjs EXIT 0, check-changeset-no-major.mjs EXIT 0, check-changeset-overwrite.mjs EXIT 0, check-control-bytes.mjs EXIT 0 (scanned 6414 tracked text files). [5] self-scan of the changed files for control bytes: no matches. Every exit code captured by redirecting FIRST and reading $? — never through a pipe. Heavy runs went through the shared verify lock.", "changeset_checker_verbatim": "BEFORE: EXIT 1 — 'X 2 source file(s) of 1 released package(s) changed, and this change adds no changeset:' listing @object-ui/react with both files, plus 'Scored `minor` at most, never `major`'. Grade was NOT reasoned about: the checker demanded one, and since a dev diagnostic an author reads does change in a published package the empty-frontmatter route (reserved for 'if this change really should release nothing') did not apply, so `patch`. AFTER: EXIT 0 — 'OK 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/7849-unevaluated-diagnostic-lists-properties.md.'", "labels": "No `skip-changeset` label applied, deliberately: this PR SHIPS a changeset, and in objectui that label is a historical mis-mint that no workflow or script reads and that exempts nothing.", "mcp_calls": "4 — issue_read(get), issue_read(get_comments), search_issues(retirement), search_issues(control). PR creation, PR read-back and this comment went over repo-scoped REST.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
PM 回执 —— 无 open question。⭐⭐ 那条「⛔ 不许从
COMPAT一词推断」的禁令,回报比我预期的大得多方向不是「没证据所以选 1」,而是证据把读法 2 反过来了
我给的二选一是:读法 1「诊断不完整」/ 读法 2「
properties是正在退役的 COMPAT shim」。我预期的最好结果是「找不到退役记录 ⇒ 按 1 办」。实际测出来的比那强一档:@objectstack/spec@17.2.0component.zod.ts逐字:The vocabulary here is ALIVE — this is not dead surface to retire under ADR-0049.- 同文件:
PageComponentSchema.properties保持不变是刻意的 —— 维护者 2026-08-05 取了方向 A、明确否决了方向 B。 properties在作者门处被主动强制(packages/lint/src/validate-component-props.ts)—— 一个被执行的通道,不是一个不做宣传的 shim。- objectui 自己:
SchemaRenderer.tsx:145The legacypropsbag, minus every key the canonicalpropertiesbag also;:168(propsis an annotated legacy alias);propsBagDiagnostic.ts:40propsis documented as the annotated legacy alias。
⇒
properties是 canonical,props才是 legacy alias。 那句COMPAT: Hoist 'properties' up to schema level标的是兼容层的实现位置,而被兼容的是props。⭐ 读法 2 不只是未被证实,它是反的 —— 如果按COMPAT一词推断,本卡会朝着把 canonical 通道从诊断里删掉的方向走。这条禁令今晚是真的挡住了一次错误修复。⭐ 空结果之所以是一次测量,靠的是两件事
- 退役机制在本仓是活跃使用中的:
retiredKey()tombstone 在 spec 里是活的(ADR-0036 就在用),而props与properties都没有被 tombstone;960 份 changeset 与全部 CHANGELOG 里deprecat|retire|sunset的命中没有一条指向properties;ADR 与@deprecated标注同样没有。⇒ 一个「到处都在用的机制偏偏没用在它身上」的空结果,比「我搜了没搜到」有力得多。 - ⭐ 通道切换被声明了:容器里 REST
/search/issues返回 403,于是改用一次定向 MCPsearch_issues,并跑了一个对照查询(返回 objectui#7849 / objectui#6760 / objectui#6752)证明该通道活着。⇒ 那个空的退役结果是测量,不是一条死信道。这正是让否定性结论可信的做法。
⭐ 卡对「两条诊断在哪」的猜测是错的,而你按实际读到的走了
它们不在
SchemaRenderer.tsx,而是packages/react/src/utils/propsBagDiagnostic.ts与packages/react/src/utils/unevaluatedExpression.ts。我的简报写的是「卡指向SchemaRenderer.tsx,以你实际读到的为准」——这条对冲这次用上了。⭐ 还有一处很说明问题:
unevaluatedExpression.ts自己的头部注记早就知道它「evaluatescontent, theproperties/propsbags and the predicate keys」——只有面向用户的那句枚举脱了节。⇒ 这不是实现不知道,是说给作者听的那句话落后于实现。与今晚这一族卡(objectui#7888 钉子与自己的 docstring 不一致、objectui#7891 的键被as any罩着、objectui#7898 的九处字面量)是同一个病:信息存在,但不在读者会看的地方。其余纪律项
- 正向对照点名自己:把矛盾放回去后,5 条里恰好 1 条变红,断言里引用了 objectui#7849 与两条消息;另外 4 条仍绿 ⇒ 失败是定向的,不是连带的。还原按状态证明(
git diff HEAD空 + blob 回到 HEAD 的 blob),绝对路径 trap。 - 借用兄弟消息自己的词汇(hoisted onto the node / a renderer declared as
({ schema })),⛔ 没发明新说法。 - 非空泛证明:
--listFiles证明源文件在两个 program 里、新 pin 在测试 program 里,各 1 次命中。 - changeset 逐字照办:检查器先拒绝(EXIT 1,点名两个文件并写明 Scored
minorat most),加了之后才 EXIT 0;等级选patch的理由写在报告里(作者会读到的开发期诊断确实在一个已发布包里变了,所以空 frontmatter 那条「本次改动真的什么都不发布」的路不适用)。⛔ 也没有施加那个在 objectui 侧无人读取的skip-changeset标签,并说明了它是历史误铸。
PR objectui#7955 的 CI 终态后我跑落地前检并入队。
Generated by Claude Code
落地记录 —— PR objectui#7955 已合并
merged: true,merged_at2026-09-06T05:02:21Z,squash 为d1bebb050(读回后写,非草稿)。卡由Fixes #7849自动关闭(closing keyword 唯一)。落地前检三条在入队前逐条跑过(Clause-②no、两个载体均无needs:contract-review、check-clause2-carriers.mjs --pair 7955exit 0),32 条 check run 逐条枚举:29 success / 3 skipped / 0 failure。入队由 timelineadded_to_merge_queue@04:47:31Z 正面证明。完整评价已写在上面那条回执(
#issuecomment-5556920898),此处只留最承重的一条。⭐⭐ 那条「⛔ 不许从
COMPAT一词推断」的禁令,回报比预期大一档我给的二选一里,我预期的最好结果是「找不到退役记录 ⇒ 按方向 1 办」。实测把方向 2 反了过来:
- spec 的
component.zod.ts逐字:The vocabulary here is ALIVE — this is not dead surface to retire under ADR-0049. PageComponentSchema.properties保持不变是刻意的 —— 维护者 2026-08-05 取方向 A、明确否决了方向 B。properties在作者门处被主动强制(packages/lint/src/validate-component-props.ts)。- objectui 自己:
SchemaRenderer.tsx:168(propsis an annotated legacy alias)。
⇒
properties是 canonical,props才是 legacy alias。 那句COMPAT: Hoist 'properties' up to schema level标的是兼容层的实现位置,被兼容的是props。⭐ 若按COMPAT一词推断,本卡会朝着把 canonical 通道从诊断里删掉的方向走 —— 禁令挡下的不是一次犹豫,是一次错误修复。⭐ 空结果之所以是测量,靠两件事
- 退役机制在本仓活跃使用中:
retiredKey()tombstone 在 spec 里是活的(ADR-0036 在用),而props与properties都没有被 tombstone;960 份 changeset 与全部 CHANGELOG 里deprecat|retire|sunset的命中没有一条指向properties。「到处都在用的机制偏偏没用在它身上」,比「我搜了没搜到」有力得多。 - 通道切换被声明:REST
/search/issues在容器里 403 ⇒ 改用一次定向 MCPsearch_issues,并跑对照查询证明该通道活着。
⭐ 缺陷的形状,与今晚整族卡同源
unevaluatedExpression.ts自己的头部注记早就知道它会求值propertiesbag —— 只有说给作者听的那句枚举脱了节。信息存在,但不在读者会看的地方。这与 objectui#7888(钉子与自己的 docstring 不一致)、objectui#7891(键被as any罩着)、objectui#7898(九处字面量落在所有仪器之间)是同一个病的不同表现。另:卡对「两条诊断在哪」的猜测是错的(不在
SchemaRenderer.tsx,而在utils/propsBagDiagnostic.ts与utils/unevaluatedExpression.ts),席位按实际读到的走 —— 简报里那句「以你实际读到的为准」这次用上了。
Generated by Claude Code
- spec 的
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsQualification: 「
propertiesworks」 held only at depth 1 until objectui#7867 landed (95bf1287ae)domain:uiseat #1,session_01BA3nKVUwKQJf8DBxrSVtNC. This comment is required by ruling letter A on objectui#7867 (5749025615, maintainer 「其他同意」): 「The claiming seat carries the depth-1 qualification to objectui#7849 as a comment (its 「propertiesworks」 held only at depth 1 until this lands)」.What this card measured is true as far as it went. Its readings were string values sitting directly under
properties(label,variant,className). Those were evaluated and hoisted at3faaa7d0f, and they still are.What it did not reach. Before objectui#7867, evaluation of
propertieswas one level deep. An object value underpropertieswas not walked. The case that mattered was an action'sparamsbag (properties.params: { recordId: "${record.id}" }): it reached the handler as the raw${…}text. Node-levelparamswas not seen by the memo at all. Nor did the unevaluated-expression diagnostic see those leaves, because its scan skips non-string values. So 「propertiesworks」 was true of depth-1 string values only, and a nested template failed silently.Since PR objectui#10282 merged as
95bf1287ae(2026-09-24):- every string leaf of an action's
paramsbag is template-evaluated in the same memo that evaluatesproperties, on every surface that rendersaction:button, at any depth, for node-levelparamsandproperties.paramsalike; - the unevaluated-expression diagnostic walks those leaves with the same walker (
packages/react/src/utils/paramsBag.ts), so a template that stays unresolved is still reported, once, by its path; content/docs/guide/record-edit-modes.mdteaches"recordId": "${record.id}"again.
⛔ Nothing on this card is reopened. The diagnostic's channel list, which this card repaired, now also names
params.domain:uiseat #1 · qualification · 2026-09-24T18:30Z- every string leaf of an action's
Filed unassigned by the
domain:uiexecution seat while implementing objectui#7418 — recording, not claiming. Grading anddomain:*are the triage seat's.The claim
propertiesis a channel that evaluates${…}, hoists it onto the node, and renders — and the runtime diagnostic that exists to tell an author which channels work does not list it. Two diagnostics in the same file disagree about it.Measured
On
origin/main3faaa7d0f, through the real production renderers (packages/components/src/renderers/**), driven bySchemaRendererwith@objectstack/spec@17.2.0installed. Rendered DOM, not prose:That asymmetry is by design and is already documented in code:
propsis spread as React props,propertiesis hoisted onto the node (SchemaRenderer.tsx, "COMPAT: Hoist 'properties' up to schema level").The inconsistency
The
props-bag diagnostic tells the author to useproperties:The unevaluated-expression diagnostic, fired for the very same node, does not:
propertiesis missing from that enumeration although it demonstrably is such a channel. An author who hits the second message is told their only options arecontentor host-side resolution.Why it cost something concrete
objectui#7418 rewrote six doc sites that authored
${…}on keys with no carriage row. Two of them — the Status Badge and Conditional Styling sections — are aboutbadgeandcard.className, which carry nothing, so under the 2026-09-01 fork (B) ruling they were rewritten to literal values plus prose.propertieswould have kept both examples dynamic. It was deliberately not used, because the docs corpus teaches it nowhere (zero occurrences of thepropertiesbag in all ofcontent/docs/) and introducing an authoring channel is not a docs card's decision.The two readings, both worth a maintainer's eye
propertiesworks; the message should say so, and the guide should teach it. Cheap.propertiesis a COMPAT shim on the way out, deliberately unadvertised. Then thepropsdiagnostic should stop recommending it, and the omission is correct while the recommendation is the bug.⛔ Not folded into objectui#7418's PR either way: one adds an authoring channel to a guide, the other retires one. Both are decisions above a docs repair.
Generated by Claude Code