Skip to content

feat(core): 混合键响亮化 — 说清谁赢了 (#3104 PR3,收官) - #3124

Merged
os-zhuang merged 1 commit into
mainfrom
claude/identity-dual-read-field-priority-h5crvu
Jul 31, 2026
Merged

feat(core): 混合键响亮化 — 说清谁赢了 (#3104 PR3,收官)#3124
os-zhuang merged 1 commit into
mainfrom
claude/identity-dual-read-field-priority-h5crvu

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#3104 PR3(闸门 + 响亮化),本战役收官。接 #3119(PR1)、#3122(PR2),均已合。

PR1 把归一挂在 ingestion,PR2 把 22 处读点收敛到 columnIdentity()。这一件是能听见的那一半

响亮化:renderer 恢复了,不等于元数据是对的

fold 让一列的两半达成一致 —— 这是止血的部分。但悄悄地name 改写成 field 的值,同时也把「生产方正在发出自相矛盾的元数据」这件事藏了起来。所以这次恢复自己会开口:

[ObjectUI] Column carries two identities: `field: 'account'` and
`name: 'account_name'`. `field` wins — it is the only key `ListColumnSchema`
declares — and `name` has been rewritten to match, so the rendered column and
the requested field agree. Fix the producer: drop `name` and author `field`
only. (objectui#3104)

作者不用读源码就能照着改:谁赢、谁输、两边的值、以及下一步动什么。

刻意收窄的三点:

约束 为什么
只报真矛盾 { name: 'stage' } 是遗留,不是矛盾 —— 照常 stamp,不出声
warn once per(身份, 冲突拼写) 列在每次渲染都会被重新归一,刷屏的告警就是被静音的告警。按而不是按身份记忆,所以一列带两个不同的陈旧拼写会两条都报 —— 作者要修的是每一个生产方,不是第一个
NODE_ENV=production 下静默,且 fold 照常运行 上线的 app 不该有 console 噪音;有测试钉住「静默时仍然折叠」

resetColumnIdentityWarnings() 导出给测试用(沿用 actionKeys.ts:213 已有的 warn-once 先例)。

不加 lint 规则 —— 这是量出来的,不是拍的

issue 要求「评估 no-restricted-syntax 匹配 .field ?? .name 的误报率后决定是否叠 lint」。评估做了,结论很干脆:

家族归零之后,扫描器剩下的 12 处命中,12 处都是合法的。 误报率 100%,真阳性 0。

因为区别在于这两个键在那一层里意味着什么,而不在于表达式怎么写 —— 语法规则看不见这个。叠上去就是在 12 处正确代码上写 inline disable,而那恰好训练下一个作者去伸手拿 disable,正是让真的漏网的那个反射。

ratchet 能做 lint 做不到的事:每个点位带 verdictwhy,新命中必须被分流而不是被消音。「家族 = 0」这条断言,本身就等于「今天 lint 会报的每一处都是误报」。评估过程写进了 ratchet 的 header。

台账条目:查证后无需改动

#3104 要求「台账相关条目处置(ListColumn 等),同步 objectstack#4115」。查了 —— objectstack#4115 是「与 spec 同名的符号必须是 import 而不是本地声明」这条规则。ListColumn 的实况:

// packages/types/src/zod/objectql.zod.ts:73
export const ListColumnSchema = SpecListColumnSchema;   // 按引用 re-export

而且 spec-subschema-parity.test.ts:154 已经toBe(SpecListColumnSchema) 的 identity pin —— 也就是 #4115 特别强调的那条(「忠实的副本能通过所有值比较,引用同一性是唯一能区分 re-export 和 fork 的检查」)。

本来就合规,无需改动。 记在这里而不是去 #4115 下面刷一条评论。

mutation-test

变异 结果
M5 摘掉 warnOnConflictingIdentity 调用 🔴 4 failed(命名/去重/多拼写/经 fold 触发)

(M1–M4 分别在 PR1 / PR2 验过,闸门未变。)

验证

vitest  core+list+grid+detail+tree+view+types  →  188 files / 2663 tests 全绿
vitest  app-shell                              →  250 files / 2089 tests 全绿(5 skipped)
turbo   type-check                             →  3/3 successful
eslint  改动文件 --quiet                        →  0 error

我没做 fold 的「镜像 → 删除」

我在 PR1 描述里把它列成了 PR3 的活,但 issue 的 PR3 清单里没有这一项,而且我当时就写清了它的风险:columns 条目会越过包边界进入宿主应用和第三方 renderer,把 name 从它们脚下抽掉是我们没有清单的破坏性变更。PR2 收敛的是仓内消费者,这个前提只覆盖仓内。

真要删,需要单独一件:major/minor + 迁移说明 + 宿主侧的排查。现在这个状态是完整且自洽的 —— 缺陷已闭合(家族归零 + ratchet 守住),矛盾元数据会自己喊出来。

关联:#3119(PR1)、#3122(PR2)、objectstack#4115、#3090(判别法)、#2598(chokepoint 先例)。


Generated by Claude Code

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)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2pdPmf2yZSd4wFDs1NHY5
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 31, 2026 12:16pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-DoeLOleB.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.45KB 3.08KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 475.53KB 104.22KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.20KB 34.74KB
fields (index.js) 223.45KB 54.66KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.44KB 10.66KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 60.52KB 17.11KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.59KB 28.74KB
plugin-designer (index.js) 210.51KB 42.50KB
plugin-detail (index.js) 222.45KB 54.50KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.38KB 26.93KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 183.29KB 48.56KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 104.62KB 25.18KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.32KB 10.53KB
plugin-timeline (index.js) 25.75KB 7.32KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.54KB 20.39KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 3.47KB 1.54KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.45KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 1.08KB 0.64KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang marked this pull request as ready for review July 31, 2026 12:23
@os-zhuang
os-zhuang merged commit 7d9734d into main Jul 31, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/identity-dual-read-field-priority-h5crvu branch July 31, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants