Repository navigation
列身份双读家族:field ?? name 两种优先序并存于 15+ 处——ingestion 归一 + 单键消费 + 禁新增闸门(objectstack#4115) #3104
Description
Activity
- added 3 commits that reference this issue
on Jul 31, 2026 收官:三期全部合并,家族归零
期 PR 合并 PR1 盘点 + 复现 + 归一器 #3119 6ae818ePR2 消费者收敛 #3122 95b7214PR3 闸门 + 响亮化 #3124 已合 验收三条全部成立:复现测试绿(渲染身份 === 请求身份)、ratchet 家族归零、「关系列裸 id」缺陷类有回归钉住。
对本 issue 正文的三处更正
正文的盘点有几处与实况不符。写在这里而不是让它留在正文里——按 objectstack#4115 的说法,一个错误的清单就是给下一个 session 埋的前提。
一、缺陷机制比正文描述的更严重:不是「不同文件用不同优先序」,是各自和自己打架
正文的框架是「name 优先的点位」对「field 优先的点位」分散在不同文件。逐点位读完源码后,真正的机制是同一个文件里,请求路径和渲染路径读同一个
schema.columns却解析出两个字段:路径 读法 请求( $expand/$select)f?.field,只读 field(单键读,扫描器看不见,因为它不是 alternation)渲染(FLS 闸门 / hiddenFields / fieldOrder / 导出 ×2 / hide-fields) f.name || f.fieldName || f.field,name 优先ListView和ObjectGrid各自内部就是这个形状。后果有两个,正文只写了第一个:{ field: 'account', name: 'account_name' }→ 按account取数,而字段级权限检查、导出、hide-fields 全按account_name。{ name: 'account' }→ 渲染路径把这列画出来,请求路径解析成undefined直接滤掉。列在那里,后面什么都没有。
第二种在独立渲染的
object-grid节点上尤其活跃(页面直接 author 一个 grid,上面没有list-view,不经过 ingestion fold)。PR2 的测试把它照了出来:AssertionError: expected [ 'id', undefined ] to include 'account'—— 一个字面的
undefined空洞进了$select,服务端不返回该字段,整列全空。二、家族真实规模是 22,不是 15+,也不是 PR1 记的 24
正文举证的两处不是同一身份的双读,而是两层概念(正是 #3090 的判别法):
resolveActionParams.ts(3 处) —— 同一文件里两种优先序都对。field ?? name选的是「从 row 上读哪个键」(行数据按对象字段做键);name ?? field命名的是「动作负载里的 param」,缺省取所绑字段名。dashboard-filters.ts——DashboardFilterDef两个键都显式声明:name是过滤器句柄,field是所指字段。f.name || f.field是「没起名的过滤器,句柄缺省取所绑字段」,是派生不是双读。
PR1 我自己也误分流了两处,PR2 读清楚它们喂给谁之后改判:
ViewPreview.tsx——toFormFieldEntry把 ViewItem 的表单 section 适配成object-form的形状,是 表单字段簇:spec↔runtime 是双层词汇——枢纽补缺、覆盖闸门、边界响亮化(objectstack#4115) #3090 的两层 join。SchemaForm.tsx——summariseComposite把任意 composite/数组元数据渲染成弹层摘要并猜一个展示键,条目是 validations/actions/JSON schema 声明的任何东西。
所以「两种优先序对半分 ⇒ 污染已经发生」这个推论,有一部分其实是两层概念。真正的污染在别处,而且更糟。
三、
accessorKey与key都留在原位,是有意的accessorKey是 TanStack Table 的列键(TableColumn.accessorKey),命名的是表格库列模型里的槽位;key是通用条目键。两者都不是 ObjectStack 元数据身份,跨这条边界归一只会把合并固化下来。RelatedList保持accessorKey || columnIdentity(col),ObjectTree/record-related-list保持columnIdentity() || key。ratchet 有一条 pin 守住accessorKey的优先级——它一旦失守,导入的 TanStack 列会整列变空。
未知项的结论
fieldName全仓没有写入方。grep 'fieldName:'的每一处命中都是函数形参或无关接口成员(AIFieldSuggestion.fieldName),没有任何代码往列对象上写它。它只活在读者里(防御性写法)。归一器仍然接纳(宿主存量元数据可能带),但这个键没有生产者。
两处「查完之后决定不做」
lint 规则不加。 正文要求评估
no-restricted-syntax的误报率后再决定——评估做了:家族归零后扫描器剩下的 12 处命中,12 处全是合法的,误报率 100%、真阳性 0。因为区别在于这两个键在那一层里意味着什么,不在于表达式怎么写,语法规则看不见。叠上去等于在 12 处正确代码上写 inline disable,而那恰好训练下一个作者伸手拿 disable。ratchet 每个点位带verdict和why,新命中必须被分流而不是被消音。评估过程写进了 ratchet 的 header。台账条目本来就合规。
ListColumnSchema已是按引用 re-export,且spec-subschema-parity.test.ts:154已有toBe(SpecListColumnSchema)的 identity pin——正是 objectstack#4115 强调的那条(忠实的副本能通过所有值比较,引用同一性是唯一能区分 re-export 和 fork 的检查)。无需改动。
一处明确没做,及原因
fold 的「镜像 → 删除」没做。 我在 PR1 描述里把它列成了 PR3 的活,但本 issue 的 PR3 清单里没有这一项,而且风险是实的:
columns条目会越过包边界进入宿主应用和第三方 renderer,把name从它们脚下抽掉是我们没有清单的破坏性变更。PR2 收敛的是仓内消费者,这个前提只覆盖仓内。真要删需要单开一件:major/minor + 迁移说明 + 宿主侧排查。当前状态是自洽的——缺陷已闭合(家族归零 + ratchet 守住),矛盾元数据会在 dev 下自己喊出来。
遗留观察(未开件)
正文提到「同一个 pattern 在相邻表面语义相反,是 AI 复制粘贴必然踩雷的结构性原因」。#3090 和本件各自加固了自己那一侧,但相邻性本身没被处理——没有任何东西告诉作者「你现在在列这一层,不是表单那一层」。
另外,动作参数身份是同一形状在隔壁一层:4 处 param-name 读点(
resolveActionParams×2、ActionPreview、resolveBulkActions)目前全是 name 优先、一致,那 1 处field ?? name是另一层概念。今天没有缺陷,但没有共享 reader,一致性靠手工维持——结构上等同于本件漂移之前的状态。属于预防,不是止血。
Generated by Claude Code
- added a commit that references this issue
on Aug 2, 2026 - added a commit that references this issue
on Aug 3, 2026 - added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 24, 2026 - added a commit that references this issue
on Sep 24, 2026
背景:同一列,两条管线解析出两个身份
列/字段身份的双读
field ?? name在仓内已扩散成家族病:15+ 处、4 个键(field/name/fieldName/accessorKey·key),且两种优先序几乎对半分:plugin-list/ListView.tsx×8(name || fieldName || field)、plugin-grid/ObjectGrid.tsx:510、core/utils/dashboard-filters.ts:146core/utils/expand-fields.ts:107(field ?? name ?? fieldName)、plugin-detail/RelatedList.tsx:646,654(accessorKey || field || name)、plugin-detail/renderers/record-related-list.tsx:25(field || name || key)、app-shell/utils/resolveActionParams.ts:161;显示层另有 ViewPreview:58、SchemaForm:1410spec 里列的规范键是
field(ListColumnSchema),name是 objectui 遗留 —— 与表单正好相反(#3090:表单 runtime 键是name)。同一个 pattern 在相邻表面语义相反,是 AI 复制粘贴必然踩雷的结构性原因;两种优先序并存说明污染已经发生过。内建缺陷场景(待 PR1 落成红测试):列对象同时携带两键时,渲染路径(ListView,name 优先)与 $expand 请求路径(expand-fields,field 优先)解析出不同身份 → 展开了 A 字段、渲染的是 B 字段 → 关系列显示裸 id —— 已知高频缺陷类。排序键与渲染列不符、导出缺列、related-list 错列同根源。
方向(预设,PR1 开工时验证)
归一方向 = 列的规范键
field;归一点 = 元数据 ingestion chokepoint(先例:#2598normalizeSchemaReferenceKeys双键 stamp;app-shell 已有view-item-normalize.ts可挂);消费者逐步收敛为单键读;新增双读被闸门拒绝。不做的:在消费端继续铺field ?? name(reference↔reference_to 的教训)。分期
PR1 盘点 + 复现 + 归一器(零行为变化)
field+name的列,证明 ListView 渲染身份 ≠ expand-fields 请求身份(裸 id 场景落地)normalizeColumnIdentity(或挂view-item-normalize):ingestion 处 stamp 规范键 —— 过渡期 stamp 双键(field权威、name镜像),行为零变化;mutation-testfieldName第三键的写入方与存量;accessorKey是 TanStack Table 内部键 —— 归一时区分「元数据身份」与「表格库适配键」,后者不属于本战役PR2 消费者收敛
PR3 闸门 + 响亮化
no-restricted-syntax匹配.field ?? .name模式的误报率后决定是否叠 lint)验收
业务价值:列表/详情是本产品第一表面,列身份解析不一致是「关系列裸 id / 排序不生效 / 导出缺列」工单的持续工厂 —— 修的是缺陷类,不是单个缺陷。playbook 全部来自 #3090 已验证的三板斧(chokepoint → 单键消费 → 闸门+响亮化)。
关联:objectstack#4115、#3090(playbook)、#2598(chokepoint 先例)、#3103(热身件,先行)。