Repository navigation
RecordDetailsProps 与真实页面的授权形状不符:sections 声明为 string[] 但所有页面授权对象形式,hideFields 完全未声明 #5611
Description
实施 #5176(为 RecordHighlightsField 声明 readonly)时,按"规则消费半径"清扫 packages/lint 下游读取方发现的范围外问题 —— 与 #5176 同一族(声明与授权不一致,靠 props 无解析闸门苟活),但不是同一个键,故另开。未在 #5176 的 PR #5607 中修复。
实测
packages/spec/src/ui/component.zod.ts 的 RecordDetailsProps 声明键为:columns / layout / sections / fields / aria。
其一 —— sections 的类型与所有真实授权相反。 声明是"分区 ID 列表":
sections: z.array(z.string()).optional().describe('Section IDs to show (required when layout is "custom")'),而仓库里每一个真实页面授权的都是对象形式的分区(带 label / columns / 嵌套 fields[]):
examples/app-showcase/src/ui/pages/project-detail.page.ts:49examples/app-showcase/src/ui/pages/settings.page.ts:27examples/app-showcase/src/ui/pages/task-detail.page.ts:73
sections: [
{ label: 'Overview', columns: 2, fields: ['name', 'account', 'owner', 'status'] },
{ label: 'Financials', columns: 2, fields: ['budget', 'spent'] },
],仓库自己的 lint 也是按对象形式建模的 —— packages/lint/src/validate-page-field-bindings.ts:166 把 sections 登记为 nestedSections,其类型注释明确写着"holding {...}[] section objects whose fields[] are field names"。
其二 —— hideFields 未声明,但被一个已发布的平台页授权:packages/platform-objects/src/pages/sys-user.page.ts:106。
两者今天都能跑通,原因与 #5176 完全一致:PageComponentSchema.properties 是 z.record(z.string(), z.unknown()),逐组件 props 从不被 parse(#5068 的判定)。lint 源码里那行注释把这个事实说得很直白:"sections/hideFields are not in RecordDetailsProps, but every real page authors them (they survive because properties is unvalidated)"。(该注释关于 sections 的部分现在略有偏差:sections 是已声明的,只是类型对不上;hideFields 才是完全未声明。)
为什么现在就值得处理
- 今天已有的损害:声明即文档。 生成的组件参考
content/docs/references/ui/component.mdx会照着 schema 告诉作者sections是string[]。一个照着参考写的 AI 作者会授权sections: ['overview']—— 而仓库里没有任何一个页面这么写,lint 也不按这个形状读。这正是 AGENTS.md #0.1 所说的"第二套事实上的契约",且方向对 AI 作者最不利(权威文档是错的那一份)。 - SDUI 组件 props 没有解析闸门:
PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 的闸门落地时会硬失败,不是静默剥离。hideFields是未声明键(静默剥离 → 平台页sys_user的隐藏字段失效);而sections是类型冲突,z.array(z.string())会直接拒绝{label,...}对象 —— 三个 showcase 页面 + 任何同形状的客户页面都会变成硬解析错误。这比RecordHighlightsFielddoes not declarereadonly, so the spec silently strips the key the chip gate reads #5176 的失败模式更响,但影响面更大。
需要裁定的方向
不自行猜测,列出选项:
- A:把 spec 改成与真实授权一致 ——
sections改为z.array(SectionObject)(或z.union([z.string(), SectionObject])),并声明hideFields。收益:声明与被兑现的行为成为同一事实,无需迁移任何现存页面;代价:sections的类型变更对任何真按 ID 列表授权的存量数据是破坏性的(需先确认是否存在这类数据)。 - B:把页面改成与 spec 一致 —— 三个 showcase 页面 + 平台页改写为 ID 引用形式。代价:需要渲染器真的支持 ID 引用形式(未经证实),且要为
hideFields另找归宿;基本等于推翻现行事实标准。 - C:按 ADR-0087 D2 声明成转换层 —— 若两种形状都要长期支持。
倾向 A,与 #5176 同样的两轴理由:声明与强制合一,且不需要一次跨仓迁移。但 sections 是既有已声明键、改类型带破坏性,超出我的判断范围,故不动、只记录。
关系
- 与
RecordHighlightsFielddoes not declarereadonly, so the spec silently strips the key the chip gate reads #5176 同族但不同键,RecordHighlightsFielddoes not declarereadonly, so the spec silently strips the key the chip gate reads #5176 的 PR feat(spec): RecordHighlightsField 声明 readonly —— 渲染器已强制的键不再被静默剥离 (#5176) #5607 已合规地未越界。 - 与 SDUI 组件 props 没有解析闸门:
PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 的关系同RecordHighlightsFielddoes not declarereadonly, so the spec silently strips the key the chip gate reads #5176:必须在 props 校验闸门落地之前解决,否则闸门一接通即产生硬解析错误。建议按RecordHighlightsFielddoes not declarereadonly, so the spec silently strips the key the chip gate reads #5176 的先例登记为 SDUI 组件 props 没有解析闸门:PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 的前置。
Activity
os-zhuang commentedon Aug 5, 2026
PM 分诊(spec 车道,
session_018fxLGQdatPbBUvCgiVxg6D,2026-08-05):进决策位(needs-user-decision)——sections是既有已声明键的类型变更,属公开契约形状分歧,过升级门槛。两轴分析与建议如下,可一句话批。建议:A 的收紧变体 ——
sections直接改为对象形式数组(z.array(SectionObject)),不做 union、不做 C 转换层;hideFields声明为z.array(z.string()).optional()。ChartDrillDownSchema.target少一个成员:objectui 的 ObjectChart 现在兑现'navigate',而 #5022 定型的联合按「chart 不读它」把它排除了 #5435/RecordHighlightsFielddoes not declarereadonly, so the spec silently strips the key the chip gate reads #5176 同一条规则)。union 或转换层会把双形状固化成长期契约——恰是 两个 discovery 生产者都在线上返回 schema 未声明的顶层字段(scoping / features / endpoints),且 REST 形状永远无法通过 DiscoverySchema #4828 第 1 条刚裁掉的「双正名」形状。sections声明为 string[] 但所有页面授权对象形式,hideFields完全未声明 #5611 正文已证仓内全对象形式,若 objectui 侧同样无 ID-list 分支,则「破坏性」仅存在于假想的存量客户数据,而 schema 校验只作用于 publish 路径、不迁移已存数据,风险可接受。若测量发现 ID-list 真有读取点,回报改裁(那时 union 才有讨论价值)。PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 的关系:不阻塞其 warning 阶段落地(warning 期本就为消化违例而设),但必须在 warning→error 升级前落地。按RecordHighlightsFielddoes not declarereadonly, so the spec silently strips the key the chip gate reads #5176 先例登记为 SDUI 组件 props 没有解析闸门:PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 error 升级的前置。Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
维护者裁定(2026-08-06,经 spec 车道 PM
session_018fxLGQdatPbBUvCgiVxg6D誊写):A 收紧变体——sections直改对象形式数组(不做 union、不做 C 转换层),hideFields声明为z.array(z.string()).optional()。派发前置照 PM 分诊注记执行:dev 先测 ID-list 形式在 objectui 有无读取点,有则停手回报改裁。摘needs-user-decision,即刻派发。#5068 的 error 升级前置由此解除(其 warning 阶段本就不等)。Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
PM 认领(spec 车道 PM,
session_018fxLGQdatPbBUvCgiVxg6D,2026-08-06)claude/issue-5611-record-details-sections;工作树:../objectstack-5611-sections(基于最新origin/main);域:domain:specpackages/spec/src/ui/component.zod.ts(RecordDetailsProps.sections改对象形式数组 +hideFields补声明)+ 配套测试 + 生成物(references/ui/component.mdx等,走生成器)+packages/lint侧validate-page-field-bindings一致性核对positions/preserveAudit—— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605、routes.mcp 是 REST /discovery 发出、objectui 真实消费、但 ApiRoutesSchema 从未声明的键(#4828 同族,低一层) #5679、[spec] 为 action body 的ctx.session立契约 schema(按现状声明)—— #5613 契约先行裁定的第一阶段 #5697;PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 后续在同文件,已排下轮串行Generated by Claude Code
feat(spec)!: RecordDetailsProps.sections 改为真实的对象形式,并补声明 hideFields (o…
feat(spec): close the 31 SDUI component-props shapes against unknown …
docs(spec): re-anchor the dead tracker citations in ui/ and two freed…
docs(spec): re-anchor the last six dead tracker citations in packages…