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.

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