fix(list,grid,detail,tree,core): 消费者收敛到单键 — 列身份家族归零 (#3104 PR2) - #3122
Merged
Conversation
…3104 PR2) PR1 (#3119) put a canonicalizing fold at ListView's ingestion boundary. This converges the 22 read sites themselves onto `columnIdentity()`, so a surface that is NOT downstream of that fold resolves the same identity anyway. That distinction is the user-visible part. A standalone `object-grid` node — authored directly on a page, with no `list-view` above it — never passed through `normalizeListViewSchema`. Its `getSelectFields` read `c.field` alone while the `ensureId` probe one line above read `f?.name || f?.field`, so a legacy `{name:'account'}` column reached `$select` as a literal `undefined` hole: the server never returned the field and every cell in that column came back empty. Same for ObjectTree, RelatedList and the record:details / record:related_list renderers. Converged: ListView ×9 + its 2 request builders -> columnIdentity() RelatedList ×8 -> accessorKey || columnIdentity() ObjectGrid (probe + projection) -> columnIdentity() ObjectTree -> columnIdentity() || key buildExpandFields -> columnIdentity() record-details / record-related-list -> columnIdentity() (|| key) `accessorKey` keeps its precedence in RelatedList — it is TanStack Table's column key, not ObjectStack metadata identity, and only the `field || name` tail was converged. `key` stays a tail fallback in ObjectTree and record-related-list for the same reason: it is a generic entry key. Two incidental fixes TypeScript surfaced once the resolver stopped returning `any`: ListView's filter-field options and its hide-fields popover both built entries keyed `undefined` for a column with no resolvable identity. Those entries could never match a column; they are now dropped. Inventory re-triage: PR1 recorded 24 family members. Two were mis-classified and are reclassified rather than converged — reading what they actually feed shows they are not column reads at all. ViewPreview adapts a ViewItem FORM section to what object-form selects by (#3090's two-layer join); SchemaForm renders an arbitrary metadata ARRAY into a popover summary and guesses at a display key. So the family was 22, and it is now 0. The ratchet asserts that, asserts each converged surface actually routes through the shared reader (a surface that dropped identity resolution instead of converging it goes red), and pins accessorKey's precedence in RelatedList. Refs: objectstack#4115, #3090 (playbook), #3119 (PR1) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2pdPmf2yZSd4wFDs1NHY5
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-zhuang
marked this pull request as ready for review
July 31, 2026 12:02
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…3124) Closes the battle opened in #3104. PR1 (#3119) put the canonicalizing fold at ingestion; PR2 (#3122) converged all 22 read sites onto columnIdentity(). This is the audible half. A column carrying two identity keys that DISAGREE now logs a one-time dev-mode warning naming which key won and what to change. The fold making the two halves agree is what stops the bug, but silently rewriting `name` to match `field` also hides that the producer is emitting a contradiction. The renderer recovering is not the same as the metadata being right, so the recovery says so. Deliberately narrow: - Only contradictions. `{name:'stage'}` is legacy, not conflicting — stamped without noise. - Warn once per (identity, conflicting spelling). Columns are re-normalized on every render, and a warning that floods the console is one that gets muted. Keyed by the pair rather than the identity alone, so a column carrying two different stale spellings reports both. - Silent under NODE_ENV=production, and the fold still runs there. No lint rule, and that is a measured decision. #3104 asked for no-restricted-syntax on `.field ?? .name` to be evaluated on its false-positive rate first. With the family at zero, all 12 remaining scanner hits are legitimate — a syntactic rule cannot tell a two-layer join from a dual read, because the distinction is what the keys MEAN in that layer, not how the expression is spelled. Adopting it would mean 12 inline disables on correct code, which trains the next author to reach for the disable. The ratchet carries a verdict and a why per site instead. The evaluation is written into its header. Ledger item resolved with no change needed: #3104 flagged ListColumn for disposition under objectstack#4115 (spec-named symbols must be imports, not declarations). ListColumnSchema is already a by-reference re-export of @objectstack/spec/ui, and spec-subschema-parity.test.ts already pins it by reference identity — the only check that distinguishes a re-export from a faithful fork. Already compliant. Verified: M5 (drop the warn call) turns 4 of the new tests red. vitest core+list+grid+detail+tree+view+types -> 188 files / 2663 tests green vitest app-shell -> 250 files / 2089 tests green turbo type-check, eslint -> green Refs: objectstack#4115, #3090 (playbook), #3119 (PR1), #3122 (PR2) Claude-Session: https://claude.ai/code/session_01C2pdPmf2yZSd4wFDs1NHY5 Co-authored-by: Claude <noreply@anthropic.com>
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#3104 PR2(消费者收敛)。接 #3119(PR1,已合)。台账:objectstack#4115。
PR1 的 chokepoint 盖不到的地方,才是这一件的用户价值
PR1 把归一挂在
normalizeListViewSchema—— ListView 的组件边界。但独立渲染的object-grid节点(页面直接 author 一个 grid,上面没有list-view)从不经过那道 fold。ObjectTree、RelatedList、record:details/record:related_list同理。ObjectGrid 的自相矛盾在这些表面上就是活的:
于是
{ name: 'account' }这样一列进到$select里是一个字面的undefined空洞 —— 服务端根本不返回该字段,那一列每个格子都是空的。测试里的失败信息把它照了出来:收敛
ListView×9 + 它的 2 个请求构造器name || fieldName || fieldvsf?.fieldcolumnIdentity()RelatedList×8accessorKey || field || nameaccessorKey || columnIdentity()ObjectGrid(探针 + 投影)columnIdentity()ObjectTreename || fieldName || field || keycolumnIdentity() || keybuildExpandFieldsfield ?? name ?? fieldNamecolumnIdentity()record-details/record-related-listfield || name (|| key)columnIdentity() (|| key)accessorKey保住优先级 —— 它是 TanStack Table 的列键,不是 ObjectStack 元数据身份,只有field || name那截被收敛(而且保留||而非??,空串的 fall-through 语义原样保留)。key在ObjectTree/record-related-list里同样留作尾部兜底:它是通用条目键。两个 TypeScript 顺手挖出来的既有缺陷
resolver 不再返回
any之后,tsc立刻指出 ListView 有两处会构造出value: undefined/name: undefined的条目(筛选字段下拉 + hide-fields 弹层)—— 对「没有可解析身份」的列。这种条目永远匹配不上任何一列,现在直接丢弃。不是我引入的,是原来f.name || f.fieldName || f.field的any把它藏住了。盘点再分流:24 → 22 → 0
PR1 记了 24 个家族成员。读清楚它们实际喂给谁之后,有两处是我分流错了,这里改判而不是收敛 —— 它们根本不是列读取:
ViewPreview.tsx——toFormFieldEntry把 ViewItem 的表单 section 适配成object-form选字段用的形状(field→name)。这是 表单字段簇:spec↔runtime 是双层词汇——枢纽补缺、覆盖闸门、边界响亮化(objectstack#4115) #3090 的两层 join,不是列。用columnIdentity收敛它是范畴错误。SchemaForm.tsx——summariseComposite把任意 composite/数组元数据值渲染成弹层摘要,猜一个展示键。数组条目是 validations / actions / JSON schema 声明的任何东西,没有列词汇表可收敛。所以家族实际是 22,现在是 0。ratchet 从 34 降到 12(全部非家族residue)。
ratchet 现在钉三件事
RelatedList里accessorKey仍在columnIdentity之前(source-level pin,注释里明写它是 source-level 而不是行为断言)—— 这条一旦破,导入的 TanStack 列会全部解析不出来、整列变空。mutation-test
c.fieldcolumnIdentity.test.tsxexpected [ 'id', undefined ] to include 'account'(PR1 的 M1/M2/M3 仍在,ratchet 与复现测试未变。)
验证
PR1 的复现测试(
ListView.columnIdentity.test.tsx)继续绿 —— 现在它同时覆盖 fold 和收敛后的读法两条保障。破坏面
全部 patch。仓内行为变化只发生在「同一列两个键不一致」和「非 ListView 下游表面的遗留单键列」上 —— 前者是缺陷,后者是修复(原来那列是空的)。
后续(PR3)
闸门 + 响亮化:
hasConflictingColumnIdentity已在 PR1 就位待用;fold 从镜像改为删除;台账条目处置,同步 objectstack#4115。关联:#3119(PR1)、objectstack#4115(台账)、#3090(判别法)、#2598(chokepoint 先例)。
Generated by Claude Code