发现于 #4871 的账本实测(PR 分支 claude/issue-4871-schemaform-labelling,基线 167ec42e7)。未认领,交 PM triage。
机制
#4871 给 WIDGETS 注册表加了 labelling 声明层,MetadataField 据此发 for 或 IDREF。但 FieldControl 里有 6 条不经过注册表 的渲染路径 —— 它们没有注册键,因而没有声明面,MetadataField 一律按默认的 control 发 for,而这 6 条路径全都不消费 id。
同一次 probe(真 SchemaForm 渲染,可编辑 + 只读两态,列口径与 #4871 账本一致)测得:
editable read-only
(spec) composite for=DANGLING hostIdEl=NONE for=DANGLING hostIdEl=NONE
(spec) repeater for=DANGLING hostIdEl=NONE for=DANGLING hostIdEl=NONE
(spec) record for=DANGLING hostIdEl=NONE for=DANGLING hostIdEl=NONE
(fallback) nested-object for=DANGLING hostIdEl=NONE for=DANGLING hostIdEl=NONE
(fallback) array-of-obj for=DANGLING hostIdEl=NONE for=DANGLING hostIdEl=NONE
(fallback) raw-json for=DANGLING hostIdEl=NONE for=DANGLING hostIdEl=NONE
对照:同一次 probe 里的内建标量分支(string / number / enum-select / array-of-primitive / boolean switch)全部 for=RESOLVES-LABELABLE,不在本单范围。
hostIdEl=NONE 即文档里没有任何元素持有 mdf-<字段名> —— 与 #4871 / #4857 / #4788 同一失效类:可见 label 的关联「解析到无」,工具链读作已闭合的关联,比干脆没有关联更糟。
#4871 的合裁(2026-08-17)明确把门定在注册表 上:「registered ⇒ declared」。这 6 条路径没有注册键,声明层无处挂;更要紧的是决定权的位置 :
所以按 Prime Directive #10 单独立卡,不夹带进 #4871 的 PR。
建议
两种落地形态,成本差别明显,建议由维护者裁:
上提解析 :把 FieldControl 里的 union 解析 + 结构化分支判定抽成一个纯函数,MetadataField 先调它拿到「这个字段最终渲染 control 还是 group」,再据此发通道。与 [app-shell] metadata-admin 的 SchemaForm 无条件给每个 widget 发 Label htmlFor={id},但 17 个注册 widget 里只有 5 个消费 id —— 分组类控件面的 for 悬空且组无可访问名 #4871 的 resolveRegisteredWidget 同形,契约一致;改动集中在 SchemaForm.tsx。
宿主包裹 :对这 6 条路径套 [fields] 非 group-labelled widget 的只读替换显示丢掉 host 下发的整份 plumbing —— label 的 for 悬空、description 零消费者(email / url / phone 实测) #4788 的宿主容器(id + aria-labelledby + role="group"),label 去掉 for。不需要上提解析(6 条路径都是「非可 label 面」这一类),但要先确认第 3 点里那三条兜底不会退化到某个可 label 的标量控件。
边界与去重
发现于 #4871 的账本实测(PR 分支
claude/issue-4871-schemaform-labelling,基线167ec42e7)。未认领,交 PM triage。机制
#4871 给
WIDGETS注册表加了labelling声明层,MetadataField据此发for或 IDREF。但FieldControl里有 6 条不经过注册表的渲染路径 —— 它们没有注册键,因而没有声明面,MetadataField一律按默认的control发for,而这 6 条路径全都不消费id。同一次 probe(真
SchemaForm渲染,可编辑 + 只读两态,列口径与 #4871 账本一致)测得:对照:同一次 probe 里的内建标量分支(string / number / enum-select / array-of-primitive / boolean switch)全部
for=RESOLVES-LABELABLE,不在本单范围。hostIdEl=NONE即文档里没有任何元素持有mdf-<字段名>—— 与 #4871 / #4857 / #4788 同一失效类:可见 label 的关联「解析到无」,工具链读作已闭合的关联,比干脆没有关联更糟。为什么 #4871 没有顺手做
#4871 的合裁(2026-08-17)明确把门定在注册表上:「registered ⇒ declared」。这 6 条路径没有注册键,声明层无处挂;更要紧的是决定权的位置:
composite/repeater/record由fieldSpec.type决定,MetadataField写 label 前就知道 —— 这三条其实可以直接判定;type:'object'且带properties⇒ 递归 SchemaForm /type:'array'且 items 是 object ⇒ RepeaterField / 其余 ⇒ RawJsonEditor)是在FieldControl内部、按 union 解析后的effectiveschema 选的 —— 也就是 label 已经写完之后。这正是color-picker那个「宿主写 label 时还不知道渲染什么」的形状,[app-shell] metadata-admin 的 SchemaForm 无条件给每个 widget 发Label htmlFor={id},但 17 个注册 widget 里只有 5 个消费 id —— 分组类控件面的for悬空且组无可访问名 #4871 是靠拆注册项解决的,这里对应的解法是把这段解析上提到 label 之前,面比 [app-shell] metadata-admin 的 SchemaForm 无条件给每个 widget 发Label htmlFor={id},但 17 个注册 widget 里只有 5 个消费 id —— 分组类控件面的for悬空且组无可访问名 #4871 裁定的范围大。所以按 Prime Directive #10 单独立卡,不夹带进 #4871 的 PR。
建议
两种落地形态,成本差别明显,建议由维护者裁:
FieldControl里的 union 解析 + 结构化分支判定抽成一个纯函数,MetadataField先调它拿到「这个字段最终渲染 control 还是 group」,再据此发通道。与 [app-shell] metadata-admin 的 SchemaForm 无条件给每个 widget 发Label htmlFor={id},但 17 个注册 widget 里只有 5 个消费 id —— 分组类控件面的for悬空且组无可访问名 #4871 的resolveRegisteredWidget同形,契约一致;改动集中在SchemaForm.tsx。for悬空、description 零消费者(email / url / phone 实测) #4788 的宿主容器(id +aria-labelledby+role="group"),label 去掉for。不需要上提解析(6 条路径都是「非可 label 面」这一类),但要先确认第 3 点里那三条兜底不会退化到某个可 label 的标量控件。边界与去重
Label htmlFor={id},但 17 个注册 widget 里只有 5 个消费 id —— 分组类控件面的for悬空且组无可访问名 #4871:那单是WIDGETS注册表的 19 个注册项 + 声明层 + color 拆双注册,已实施。本单是同一宿主里注册表之外的路径。SchemaForm composite repeater record raw JSON dangling label for metadata-admin),无同题单。