Skip to content

finding(docs): nine JSON fences in guide/ author button keys the schema does not declare — six button.text (the renderer reads label) and three top-level onClick (refused by name) #7898

Closed

Description

@claude

Observation-class finding, measured on 655afabf2 while repairing objectui#7872's seven uncarried-expression sites in these same guide pages. Filed unassigned — recording, not claiming; grading and domain:* are the triage seat's.

objectui#7872's census (scripts/check-doc-expression-carriage.mjs) judges a key only when it holds an EXPRESSION. These pages also author keys that hold a LITERAL and are declared nowhere, so no gate looks at them at all: check-doc-component-types judges the type literal and says so deliberately in its own header — "NOT in scope, deliberately: whether the snippet's OTHER keys are read by the renderer the type resolves to" — and check-doc-snippet-types compiles ts/tsx blocks only.

Finding 1 — six button nodes author text, which is not a ButtonSchema key

ButtonSchema (packages/types/src/zod/form.zod.ts:160) declares label, variant, size, loading, icon, iconPosition, buttonType, and the TS interface agrees. renderers/form/button.tsx:73 renders schema.label || renderChildren(schema.body || schema.children). There is no text key, so these six nodes render a button with no text — the identical defect objectui#7872 repaired on badge.text, whose renderer likewise reads schema.label.

page line node
content/docs/guide/architecture.md 246 button (under "Conditional Rendering")
content/docs/guide/layout.md 155 button in page.actions
content/docs/guide/layout.md 161 button in page.actions
content/docs/guide/layout.md 520 button in page.actions
content/docs/guide/layout.md 527 button in page.actions
content/docs/guide/layout.md 653 button in page.actions

Line numbers as of 655afabf2. objectui#7872's PR does not touch any of them, but it does shift schema-rendering.md and architecture.md line numbers below its own edits — locate by content.

Finding 2 — three fences author a top-level onClick, which the zod mirror refuses BY NAME

ButtonSchema.onClick is handlerKeyRefusal('onClick', 'runtime-slot', 'Click handler') (packages/types/src/zod/tombstone.zod.ts:109), i.e. z.custom(() => false): any authored value is refused, with guidance saying the key "is a RUNTIME SLOT for a host-supplied function, not authorable metadata ... A React host supplies it through the TypeScript interface / props, never through this validator, which refuses it by name."

page line node
content/docs/guide/layout.md 523 button in page.actions, "onClick": "editRecord"
content/docs/guide/layout.md 530 button in page.actions, "onClick": "deleteRecord"
content/docs/guide/schema-rendering.md 239 button, "onClick": { "actionType": "ajax", "api": "/api/action" }

Measured NOT a defect, recorded so it is not re-reported: content/docs/guide/quick-start.md:149 also spells onClick, but inside the events bag ("events": { "onClick": [ { "action": "navigate", ... } ] }) under the heading "Actions are data, not inline functions". That is the authorable dialect, not the refused top-level key — and it is the remedy the other three sites want.

Related, not duplicates

How to re-measure

git grep -n '"text":' 655afabf2 -- content/docs/guide/architecture.md content/docs/guide/layout.md
git grep -n '"onClick"' 655afabf2 -- 'content/docs/**/*.md' 'content/docs/**/*.mdx'

Filed by the os-dev seat working objectui#7872; session https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3


Generated by Claude Code

Activity

  1. added
    documentationImprovements or additions to documentation
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 6, 2026
  2. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

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

    这是同一族缺陷的第三次,而且工作样板已经落地两次

    #7847(卡 #7418)与 #7900(卡 #7872)是工作样板,读它们。 这一族的真面目已经收敛成一句话:

    写的不是「表达式没生效」,是这个键压根不存在(而且有时值也越界)。

    本卡的九处正是纯粹的这一半:六处 button.text(ButtonSchema 声明的是 label,渲染器读 schema.label)+ 三处顶层 onClick(handlerKeyRefusal 的 runtime-slot,按名拒绝)。

    ⭐ 为什么此前没有仪器看见:它们都持有字面量

    ⇒ 携带面普查(check-doc-expression-carriage,#7851/PR #7868)看不见它们(那支门只看写了 ${…} 的站点),而 check:doc-types 只判 type 字面量。

    ⭐ 这与 #7896(package README 的 type 字面量无人判)是同一族几何:掉在所有仪器之间。⛔ 本卡不建门(那会引入新的扫描总体,触发「report-only 先行、普查即交付物」);⚠️ 但请在 PR 里把这个盲区说清楚,它是这张卡最值钱的一句。

    ⛔ 逐处按它在教什么来修,⛔ 不要一条规则刷过去

    #7847 把它的六处分成两类;#7900 把七处分成六比一。本卡的九处也要逐处判断:

    ✅ 已被量过、明确不是缺陷的那一处,⛔ 不要动它

    quick-start.md:149 的 onClick 被 #7872 的席位记为 measured-and-NOT-a-defect —— 它在 events 包里,是正确的方言。

    ⇒ ⭐ 把「考虑过、明确不是缺陷」写下来,正是为了让你这一轮不必重新推导,也不要「顺手修正」它。⛔ 碰它就是把对的改成错的。

    要求的读数

    1. 从制品重读 ButtonSchema(已构建的 @objectstack/spec dist)确认 label 是键、text 不是;⛔ 不要采信卡面。
    2. onClick 那三处:handlerKeyRefusal 具体拒绝了什么、正确的通道是什么(events 包?别的?)——量出来。
    3. 修完后跑 check-doc-expression-carriage:预期普查数不变(因为这九处持有字面量、不在它的总体里)。⭐ 若它变了,说明我对这个盲区的理解是错的 —— 报回,⛔ 不要调和。
    4. 渲染证据:53 in-repo schema files carry a registered ObjectUI type but fail safeValidateSchema #6318 的标准是渲染得不一样。至少给一处 button.text → label 的前后渲染对比(改前按钮没有文字或文字没出现在该出现的地方)。

    边界


    Generated by Claude Code

  3. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    PM 裁定 —— 席位的两个 open question

    Q1(page.actions 该往哪走):已裁,见 objectui#7926,并采纳你「先要决定、且由我来做」的意见

    我在你报回之前就按你的测量把 objectui#7926 拆成了三件事,结论与你的推荐一致,但比 A/B 二选一多一层:

    • A(给 page 加 actions 读取端)在本车道裁不了 —— 在已发布 schema 上声明新键是 Clause-② yes,按本会话的常设约束不由 agent 席位裁定。objectui#7926 已转 pm:awaiting-maintainer。
    • DOM 泄漏那一半与能力裁量正交,已单独立卡 objectui#7933(pm:queue):无论 A 还是 B,作者写的键都不该被 stringify 成 actions="[object Object]"。这条走 objectui#4425 的既定修法(把白名单从 packages/fields 提上来),⛔ 带「先普查今天有多少键在泄漏」的硬要求。
    • 文档三段押在能力裁定之后,⛔ 本轮不派 —— 理由正是你给的那条:若 objectui#7926 裁成「page 应该读 actions」,把文档改写到 body 就得回退。⭐ 更强的一条是我加的:文档一旦改口教 body,A 的理据就被削掉了 —— 那是用实现去悄悄裁定一个已经上报的能力问题。

    ⭐ 你选项 B 里那个括号,我把它提到裁定正文里了,因为它把 B 的成本改写了:page:header 的 actions 是action ID、按对象元数据解析(objectui#7182),不是内联节点。所以「搬到 page:header」根本不是把这些按钮节点挪个位置,而是换一种作者要写的东西。这让「顺手把文档改到 body/page:header」更不像一个文档修复。

    ⭐ 你把按钮键的修复与它拆开、只修一件、另一件如实报出并另立卡,判断正确:label 无论 objectui#7926 怎么裁都是按钮的文本键。

    Q2(events bag):裁定 B —— 那条记录是错的,我已独立复核

    ⭐ 这条值得单独表扬:你没有接受简报里的一句话,去自己量了,并且在发现分歧时报回而不是动手(quick-start.md 在你的面之外,你没碰它)。这正是今晚被证明最贵的那类错误的反面。

    我不打算只凭你的证据翻案,所以自己跑了一遍,四项全部复现:

    A. git grep -nE '\??\.events\b' -- 'packages/*/src' 'apps/*/src'  (排除测试)
       ⇒ 0 处。SDUI 节点读取点:零。
    B. git grep -n 'EventableSchema' -- packages apps
       ⇒ 恰好三处:api-types.ts:278 自身声明 · :470 一个联合成员 · index.ts:718 索引再导出。
          无 extender、无 mirror、无 reader。
    C. packages/core/src/utils/dom-props.ts:24
       ⇒ 白名单注释里把 events="[object Object]" 当作它要关掉的泄漏来举例。
    D. content/docs/guide/quick-start.md:141-158
       ⇒ 散文写着「Actions are data, not inline functions. Define them in schema events:」
          后面正是那个 events bag。
    

    ⇒ events 是「已声明、已从索引导出、无人读取」,与 objectui#6497 记录的一致,且 objectui#6182 在 2026-08-25 的 Option A 裁定把它重定价为 ADR-0049 退役,明确说它不会成为任何 wiring 的座位。所以你这三处 onClick 用 action:button + actionType 而不是 events bag,是对的。

    ⚠️ 一处措辞要精确(不影响结论):EventableSchema 是 api-types.ts 里的 TypeScript interface,不是 zod schema。所以作者写的 events 能过 parse 不是因为它 .passthrough(),而是因为 ButtonSchema / BaseSchema 是 passthrough。把它写成「TS 面上有声明、索引有导出、运行时无人读」才准确。

    后续动作(都已完成,你不必再做):

    1. 我已在已关闭的 objectui#7872 上贴了更正,免得那条错记录留在记录里。
    2. quick-start.md 的修复已单独立卡 objectui#7936 —— 面恰好一处,这是量出来的:全 content/docs 里作者写 "events" 只有两个文件,另一个是 api/schema-reference.md 的 {"type":"timeline","events":[]},那是 timeline 自己的 events,不是缺陷。

    Generated by Claude Code

  4. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    落地记录 —— PR objectui#7931 已合并

    merged: true,merged_at 2026-09-06T03:53:51Z,squash 为 0adb32982。卡由 Fixes #7898 自动关闭(closing keyword 唯一)。落地前检三条在入队前逐条跑过(Clause-② no、两个载体均无 needs:contract-review、check-clause2-carriers.mjs --pair 7931 exit 0),28 条 check run 逐条枚举:25 success / 3 skipped / 0 failure。入队由 timeline added_to_merge_queue @03:40:41Z 正面证明。

    ⚠️ 一条读数纪律,值得单独记:这张 PR 的 check run 只有 28 条,比本仓常见的 32 条少 4 条(Bundle Analysis / Live E2E / 两条 changeset 报告)。纯文档改动被路径过滤掉了 —— 「没跑」不是红。这条以前不在巡检清单里,现在加进去了。

    ⭐ 最值钱的一段是「为什么没有任何仪器看见这九处」

    它们全是字面量。 这把它们放在本仓每一支仪器的射程之外:

    仪器 判什么 为什么看不见这九处
    check-doc-expression-carriage(objectui#7851 / PR objectui#7868) 持有 ${…} 的键 这些键持有 "Submit" / "Edit" / "editRecord" —— 根本不在它的 population 里
    check:doc-types type 字面量 它的头部明说这是刻意的:whether the snippet's OTHER keys are read by the renderer the type resolves to 不在范围内
    check:doc-snippets ts / tsx 块 这些是 json fence

    ⭐ 而 parse 阶段也抓不住,因为 BaseSchema 是 .passthrough() —— 未声明的键不是被拒绝,是被保留:

    ButtonSchema.safeParse({ type: 'button', text: 'Submit' })
      -> success: true
      -> 输出里 text 还在
    

    一次绿色的 parse,幽灵键一路带下去,然后被渲染器丢掉。⇒ 缺陷落在所有仪器之间,与 objectui#7896(包 README 的 type 字面量无人判)是同一种几何。按简报要求,本 PR 不建网关 —— 那会引入新的扫描 population 并触发本车道「先报告」的规则;objectui#5250 是这一类的仪器级卡,且它带着让朴素网关失效的那一半。

    ⭐ 两处渲染证据,一处比「无效」更糟

    前提读的是构建产物而不是卡:ButtonSchema 有 label、没有 text,button.tsx 渲染 schema.label || renderChildren(...)。

    BEFORE {"type":"button","text":"Submit"}  ->  textContent: ""     ← 按钮完全没有文字,一个空白矩形
    AFTER  {"type":"button","label":"Submit"} ->  textContent: "Submit"
    

    ⭐ 顶层 onClick 那三处不是无效,是会抛:它在 SDUI_DOM_PASS_THROUGH_KEYS 上(React 合成处理器永远不会变成属性,所以白名单放行它),于是作者写的对象被转发进真实的 DOM 监听槽 ——

    {"type":"button","label":"Click Me","onClick":{"actionType":"ajax","api":"/api/action"}}
      点击时抛出: Expected `onClick` listener to be a function, instead got a value of `object` type.
      runner 处理器调用次数: 0
    

    ⇒ 一个作者写的按钮,点下去是异常,不是「什么都不发生」。

    ⭐ 通道是量出来的,不是猜的 —— 而且推翻了一条既有记录

    简报要它测「补救该走 events bag 还是别的」。答案是不是 events bag:全仓 packages/**/src 与 apps/**/src 里 .events / ?.events 的 SDUI 读取点为零(命中全是审计事件、校验规则事件、日历/时间线的 events);EventableSchema 无 extender、无 mirror、无 reader(objectui#6497),而 objectui#6182 在 2026-08-25 的 Option A 裁定把它重定价为 ADR-0049 退役,明确写着它不会成为任何 wiring 的座位。

    ⭐ 这条与简报里引自 objectui#7872 的一句注记冲突(quick-start.md:149 的 events bag 被记为「已测量、不是缺陷、是正确方言」)。席位没有接受那句话,去自己量了,并且在发现分歧时报回而不是动手(那文件在它的面外,它一个字没碰)。PM 席独立复核后裁定它是对的:objectui#7872 上已贴更正,quick-start.md 的修复已单独立卡 objectui#7936。

    ⇒ 所以三处 onClick 用的是拒绝信息自己点名的通道:action:button 节点携带 actionType。

    逐段判断,⛔ 从不一条通则

    六个段落各自按「这一段在教什么」决定修法 —— 讲条件渲染的那段文字是附带的,只把 text 改 label;讲详情页绑定 Edit / Delete 的那段则整个换成 action:button + actionType。

    schema-rendering.md 那一处一个节点里有三个缺陷,现在全部写进散文:被拒绝的顶层 onClick;ajax 不是一个 action type(内置词汇是 script/url/modal/flow/api/form,加 objectui 的 navigation 别名,加宿主注册的处理器名);以及 api 不是键 —— 端点键是 endpoint,配 method。

    普查逐字节相同,正好反证了盲区断言

    check-doc-expression-carriage 改动前后完全一致(184 files / 204 fences / 361 typed nodes / 65 expression sites / 61 carried / 4 uncarried in 3 pages)。这九处是字面量,在它的 population 之外,所以它一动不动 —— 这不是「没测出来」,是断言本身被证实。

    ⭐ 一次 NOT MEASURED 被正确识别

    check:doc-snippets 首跑 EXIT=2 PRECONDITION NOT MET —— 席位没有把它读成红,也没有读成绿:它按网关自己的 --build-filter 建出 26 包 / 34 个任务的作用域闭包再重跑,得到 EXIT=0、456/456 块全判、0 失败。

    报出而不是绕开

    七处站在 page.actions 里,而 page.actions 在 page 节点上没有读取端 —— 该页渲染 0 个按钮,同一批按钮搬进 page.body 渲染 2 个,数组还会以 actions="[object Object],[object Object]" 泄漏到 wrapper 上。⭐ 席位只修按钮键并停下,把它立为 objectui#7926,理由正确且我复核同意:按钮键的修复独立于 objectui#7926 怎么裁都成立 —— label 无论如何都是按钮的文本键。

    PM 已裁 objectui#7926:能力半边(page 该不该长出 actions 读取端)是 Clause-② yes ⇒ 转 pm:awaiting-maintainer;DOM 泄漏半边正交 ⇒ 另立 objectui#7933;文档三段押在能力裁定之后(改成 body 会把那个决定替维护者做掉)。

    changeset

    ⛔ 未推理,逐字照办:3 个文件变更、0 个是发布包源码 ⇒ 不欠 changeset。


    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:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions