Skip to content

fix(lint): collectViewRecord 按运行时视图身份判定容器默认 list_views 键 (#6038) - #6424

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6038-collect-view-record-runtime-key
Aug 7, 2026
Merged

fix(lint): collectViewRecord 按运行时视图身份判定容器默认 list_views 键 (#6038)#6424
hotlong merged 1 commit into
mainfrom
claude/issue-6038-collect-view-record-runtime-key

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6038

#5164 裁 A(维护者 2026-08-06:_views 翻译键 canonical = 运行时组装器裸键)三段串行的第 2 棒 / lint 段。母单 #5164 已关闭,本单自身用 Fixes


1. 四项串行前置的逐项实测(本单第一项硬任务)

派单要求:⛔ 不采信「母单已关闭」这一个事实,动代码前在 origin/main(e15bf7e)上逐项实测。结论:四项全部落地,前置满足,且实测发现 main 上的实际状态与派单担心的方向相反 —— 不是「存量仍是 list 键、lint 收窄会判红存量」,而是第 1 棒已把两端都迁到 default,唯独 lint 不认 default,于是 main 上此刻就有 8 处误报。本 PR 是减红,不是增红。

# 前置交付 实测方法 实测结论
1 提取器 view.list.name ?? 'list'default os i18n extract examples/app-showcase/objectstack.config.ts --json --dry-run --filter '_views' ✅ 已落地,且优于派单词面:PR #6124 没有写死 'default',而是 defaultListViewKey() 向组装器 expandViewContainer 查询运行时身份(packages/cli/src/utils/i18n-extract.ts:210)。实测产出 _views.default(showcase_task._views.default.label = "All Tasks" 等)
2 showcase 5 个 _views.list 译文块随迁 grep examples/*/src 全量 ✅ 已落地。examples/app-showcase/src/system/translations/index.ts 现有 6 个 _views 块全部键为 default(project / task / contact / inquiry / business_unit / field_zoo);examples/**_viewslist:零残留
3 覆盖基线棘轮下调 pnpm check:i18n-coverage(改动前) ✅ 已落地,且实测无需下调(与 #6124 changeset 自述一致):EXIT=0,OK (12 config(s), 660 baselined untranslated string(s), none new)。两端同批改名,覆盖数不变
4 已发布 bundle 键变更的 conversion / release note .changeset/ 目录 ✅ 已落地且尚未消费:.changeset/views-translation-key-runtime-identity.md(@objectstack/cli: minor),含 BREAKING(已发布翻译包的键) 标注与 FROM → TO 三行对照表 + 一行修法

⛔ 未越界补做第 1 棒的任何活:packages/cli/**examples/** 译文本 PR 一字未动。


2. collectViewRecord 现行形状 → 改后形状

改前(packages/lint/src/validate-translation-references.ts:260):

const listBinding = isRec(view.list) ? bindingOf(view.list) : undefined;
if (isRec(view.list)) addView(listBinding, strName(view.list.name));

作者没写 list.name什么也不注册。而组装器(packages/spec/src/ui/view.zod.ts expandViewContainerWithDiagnostics,只读核实、⛔ 未改)给同一个视图的身份是 < object >.default

改后:

if (isRec(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));

defaultListViewKey() 向组装器查询,而不是第三次自行推导 —— 与第 1 棒 packages/cli/src/utils/i18n-extract.ts 的同名函数刻意成对(两者都只是组装器的薄读取器,而不是第三、第四份推导)。因此继承组装器仅有的三条规则:

  1. name 的默认列表键为 default(绝不是 list);带 name 的沿用作者的 name;
  2. 结构上({type,label,columns} 签名)与某个 listViews 条目相同的默认列表被折叠进该条目 —— 存活的是那个 listViews 键,被折叠掉的 list.name 不再合法(运行时解析不到它);
  3. 因命名冲突被改名的键(defaultdefault_2)按改名后判定,因为改名后的名字才是注册表键。

组装器行为实测(探针,非推断):

nameless list                                  => crm_lead.default[list,default]
listViews.default 先占 + 无名 list             => crm_lead.default[list] | crm_lead.default_2[list,default]
折叠(list 与 listViews.all 同签名)           => crm_lead.all[list,default]

顺带清理:该函数注释里「whether it contributes a _views name is the neighbouring question #5164 owns」的悬置已删除。⚠️ 没有只删句子了事 —— 换成了答案:默认 form 继续只贡献 _sections,理由是 _views.*列表视图约定(viewLabel / viewDescription 解析视图页签),i18n walker 对任何 form 视图都不产 _views 条目(i18n-extract.ts 原话),所以给默认 form 注册一个 _views 名等于让一个没有消费方的键变合法。行为不变,零变红风险。


3. 矛盾对 before / after 实跑取证

⚠️ 全部实跑,无一处推断。

否定方(本规则)

改动前后各跑一遍全部 12 个受棘轮覆盖的配置os lint --json,逐条 issue 做集合差:

examples_app-crm:            total 101->101  | +0 -0
examples_app-showcase:       total 518->512  | +0 -6
   GONE  translation-target-unknown | ...showcase_project._views.default
   GONE  translation-target-unknown | ...showcase_task._views.default
   GONE  translation-target-unknown | ...showcase_contact._views.default
   GONE  translation-target-unknown | ...showcase_inquiry._views.default
   GONE  translation-target-unknown | ...showcase_business_unit._views.default
   GONE  translation-target-unknown | ...showcase_field_zoo._views.default
examples_app-todo:           total 124->122  | +0 -2
   GONE  translation-target-unknown | ...todo_task._views.default   ("zh-CN")
   GONE  translation-target-unknown | ...todo_task._views.default   ("ja-JP")
(其余 9 个配置 +0 -0)
=== TOTAL added: 0  removed: 8

汇总口径:_views 键上的 translation-target-unknown 8 → 0

⚠️ showcase 上是 6 处,不是 #5164 记录的 5 处 —— showcase_field_zoo 是第 6 个同族实例;app-todo 另有 2 处(两个语言包),#5164 未记录。

要求方(同一次 os lint)

否定方的 8 条只是矛盾对的一半。要求方在译文存在时不发声,所以另用一个合成 stack(仅含一个无名默认 list、无译文)在同一个 os lint 里取证:

$ os lint probe.config.ts --json
warning | i18n/missing-view | translations.zh-CN.objects.probe_lead._views.default.label

给同一个 stack 补上 _views.default 译文后再跑:

total 1 errors 1 warnings 0
  error | security-owd-unset | objects[0].sharingModel     ← 与 i18n 无关(探针没设 sharingModel)

i18n findings 归零:要求方不再要,否定方不再否 —— 两条规则对同一个键给出同一答案。矛盾结构性消失,验收判据达成。(探针文件已删除,git status 只剩本 PR 的三个文件。)


4. 反向验证(先申报,后执行)

声明 A —— 矛盾消失

申报:修前 showcase 全量 os lint 存在 i18n 矛盾对,键为 objects.< object >._views.default,两条规则为 i18n/missing-view(要求方,packages/cli/src/utils/i18n-extract.ts)与 translation-target-unknown(否定方,本文件);修后该矛盾对为 0。

实测:成立。showcase 6 对 → 0(另发现 app-todo 2 对,一并归零)。两侧取证见上节。

声明 B —— 变异体

申报(执行前写下):把收窄退回 addView(listBinding, strName(view.list.name))、保留全部新断言,预测转红的是四条肯定式断言(default 合法 / 折叠体拒绝 list.name / 改名键 default_2 合法 / form fixture 的 default)加上配对孤儿计数那条;预测结构上不会红的是「具名默认列表沿用作者 name」(回归护栏,两侧同绿);并预判「拒绝旧 list 拼写」那条只会经由提示语断言转红,其 path 断言对本变异体是盲的。

实测:7 failed | 29 passed (36),与申报逐条吻合:

× accepts `default` for a nameless default list — the key the registry holds
× the same fixture still reports a key nothing declares (the green above is not an empty run)
× rejects the old `list` spelling — one key per view, and it is the runtime one
× a default list collapsed into a `listViews` entry contributes that entry's key, not its own `name`
× a collision-renamed default list is legal under the renamed key
× the default FORM contributes sections but no `_views` name — `_views.*` is a list convention
× accepts `_views.default` — the key this very surface ships (#6038)   ← 真实 showcase 元数据

断言极性逐条标注:

断言 极性 对本变异体
default 对无名默认列表合法 肯定式(toEqual([])) ✅ 红
同 fixture 仍报植入的未知键(空绿对照) 肯定式(计数 1) ✅ 红(变异体下变 2)
拒绝旧 list 拼写 path 断言=否定式,结构上不会红;hint 断言=肯定式 ✅ 红,但只经 hint:expected 'Match the key to the view's name (…' to contain 'default' —— 变异体下 Declared views 尾巴是空的。path 断言两侧同绿,这条如实标注
折叠体拒绝 list.name 肯定式(计数 1) ✅ 红
改名键 default_2 合法 肯定式(toEqual([])) ✅ 红
默认 form 不贡献 _views 肯定式(toEqual([]),fixture 含 default) ✅ 红
真实 showcase _views.default 合法 肯定式(toEqual([])) ✅ 红
具名默认列表沿用作者 name 回归护栏 两侧同绿 —— 结构上不会红,如实申报

空绿自查(本轮已有四位 dev 撞过这个坑):每一条 toEqual([]) 都在同一个 fixture 上配了一条植入坏键的肯定式断言,证明规则确实跑过、确实会产出 finding,而不是「因为什么都没产出所以绿」。逐条:无名默认列表 fixture 配 hot_leads(计数 1);具名列表 fixture 配 default(计数 1);折叠 fixture 配 all_leads(计数 1);改名 fixture 配 default_3(计数 1);默认 form fixture 配 form(计数 1);真实 showcase fixture 配 list(计数 1)。无一条真空绿。

声明 C —— 存量不误伤

申报:修后跑全量 os lint,新增的红必须为 0。

实测:成立。12 个配置全跑,added: 0。⛔ 未为了让门变绿放宽任何规则。

fixture 分诊(⚠️ 一处必须的改动,并已证明其必要性)

既有 fixture leadViews 的默认 list({type:'grid', name:'all_leads'})与 listViews.my_leads({type:'grid'})签名完全相同,组装器会把两者折叠 —— 也就是说 all_leads 根本不是运行时视图名,fixture 等于在断言「一个谁也解析不到的键是合法的」。按三分法这属于补声明(add declarations),不是 re-spell、也不是整体替换:给默认 listlabel: 'All Leads' 使其成为它自称的那个「独立的默认列表」。

必要性已实测(不是「看起来需要」):把这个 label 拿掉、保留本 PR 的实现,该 fixture 的两条既有测试双双转红(2 failed | 33 passed)。折叠语义本身另立独立测试单独 pin。

消费半径扫查

按规则的消费半径而非编辑包扫查 fixture:translation-target-unknown 的调用方为 os lint(经 packages/lint 导出)。全仓 grep _views.list / _views + list:,除历史 CHANGELOG(⛔ 不改)与本 PR 自身文件外零命中;packages/cli/test/i18n-extract-view-key-identity.test.ts 已由第 1 棒 pin 在 default 侧,与本 PR 同向。


5. changeset 级别与依据

.changeset/lint-views-key-runtime-identity.md —— @objectstack/lint: patch(真 changeset,⛔ 未打 skip-changeset;@objectstack/lint 是发布包)。

依据:

  • 仓内惯例「新增规则」minor、「既有规则的判定/遍历修正」patch。本单是既有规则的判定修正,无新规则、无 API 变化(validateTranslationReferences 签名与 rule id 均不变)。同族先例:.changeset/adr-0105-d6-unit-and-subordinates.md(@objectstack/lint: patch,同为既有规则词表/判定修正);
  • 对已发布行为确有可见变化,故 changeset 正文写了完整的四行判定对照表(哪些形状从报→不报、哪些从不报→报)。但变化全在 warning 层:本规则 findings 恒为 severity: 'warning',不改 os lint 退出码(showcase 496 条 warning 时 EXIT=0),也不在 check:i18n-coverage 棘轮口径内(该棘轮只数 i18n/ 前缀);全仓 12 配置实测新增 0 条,消费方 CI 不会因此变红 —— 不足以升 minor;
  • ⚠️ v17 窗口期 ⛔ 禁 major,已遵守。

6. 门禁 EXIT 表(均在 git add 之后跑,重活经 flock 串行 + 堆上限)

命令 EXIT 备注
包测试 pnpm --workspace-concurrency=2 --filter @objectstack/lint test 0 62 files / 1527 tests passed
包 typecheck pnpm --workspace-concurrency=2 --filter @objectstack/lint typecheck 0 tsc --noEmit
ESLint(全仓) pnpm lint 0 ⚠️ 4096 MB 下 OOM(EXIT=134),提到 8192 MB 后 EXIT=0;scoped eslint packages/lint 亦 0。仓库规模问题,与本改动无关
控制字节门 pnpm check:nul-bytes 0
控制字节自扫 grep -naP 控制字节类,扫三个改动文件 1(无命中)
i18n 门 pnpm check:i18n 0
i18n 覆盖棘轮 pnpm check:i18n-coverage 0 改动前后同值:12 config(s), 660 baselined, none new
CI 独有 pnpm check:type-check-debt 0 34 ledger entries re-measured, none above its recorded number。本 PR 未新增测试文件(只改既有 .test.ts),无 TS2835 风险
文档公式门 pnpm --filter @objectstack/lint run check:doc-formula-expressions 0
文档/技能门 pnpm check:doc-authoring 0
ADR 锚点门 pnpm check:adr-anchors 0

⛔ 无一条绕过。


7. 不在本 PR 里

8. 顺带发现(已另立观察单,未在本 PR 修)


Generated by Claude Code

…ime identity (#5164) (#6038)

`collectViewRecord` decided which `_views` name a container's default `list`
makes legal by reading `view.list.name` — and registered NOTHING when the
author wrote none. The composer that assigns the runtime view identity
(`expandViewContainer`, `packages/spec/src/ui/view.zod.ts`) names that very
same view `<object>.default`.

Leg 1 (#6124) moved the i18n extractor onto the composer's key, which turned a
latent disagreement into a live contradiction: in ONE `os lint` run
`i18n/missing-view` demanded `objects.<object>._views.default.label` while
`translation-target-unknown` called that key an orphan. Measured on main: 8
instances (app-showcase 6, app-todo 2) — no author action made both green.

This rule now ASKS the composer for the key too, inheriting its three rules:
a nameless default list is `default`; one that structurally restates a
`listViews` entry is collapsed into it (so the collapsed-away `list.name`
stops being legal — nothing resolves it); a collision-renamed key is judged
under the rename, because the rename is the registry key.

Measured across all 12 ratcheted configs: 0 new findings, 8 false positives
gone, `check:i18n-coverage` baseline unchanged. Leg 2 of 3; objectui
`viewSuffixes` is objectui#3502.

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

vercel Bot commented Aug 7, 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)
objectstack Ignored Ignored Aug 7, 2026 5:33pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@hotlong
hotlong marked this pull request as ready for review August 7, 2026 17:53
@hotlong
hotlong added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 7618ee8 Aug 7, 2026
25 checks passed
@hotlong
hotlong deleted the claude/issue-6038-collect-view-record-runtime-key branch August 7, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

packages/lint:collectViewRecord 收窄 _views 键到运行时裸键单拼写 —— #5164 裁 A 的 lint 段

2 participants