Skip to content

fix(lint): validateFormLayout 走视图容器阶梯,两条规则不再对真实 app 全盘报绿 (#6251) - #6382

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-6251-form-layout-view-containers
Aug 7, 2026
Merged

fix(lint): validateFormLayout 走视图容器阶梯,两条规则不再对真实 app 全盘报绿 (#6251)#6382
hotlong merged 3 commits into
mainfrom
claude/issue-6251-form-layout-view-containers

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6251

1. 前提复核 —— 成立,且比 issue 描述更强

按内容定位(不靠行号),packages/lint/src/validate-form-layout.ts 遍历入口原文:

const views = asArray(stack.views);
for (let i = 0; i < views.length; i++) {
  const view = views[i];
  if (!view || typeof view !== 'object') continue;
  const sections = Array.isArray(view.sections) ? view.sections : null;
  if (!sections) continue; // only form views carry a sections array

即只读容器自身的 sections,其余一律跳过。issue 的前提成立。

复核中多测出一条 issue 没写的事实,方向一致但更严重 —— 唯一被读的那种形状,正是严格 ViewSchema 会拒绝的形状:

ViewSchema.safeParse({ name:'v', data:{object:'task'}, sections:[…] }).success = false
issues = [{ code: 'unrecognized_keys',
            message: 'Unrecognized key(s) on this view container: `data`, `sections`. …
                      The container's own keys are `list`, `form`, `listViews`, `formViews`.' }]

ViewSchema.safeParse({ name:'v', object:'task', form:{…}, formViews:{ edit:{…} } }).success = true

所以在 defineStack 走过的 parsed 栈上,旧遍历能读到的站点数恒为 0。

⚠️不能据此把「条目自身即裸表单视图」那一支当幽灵删掉,这是本单最容易踩反的一步:validateFormLayout 在注册表里是 input: 'parsed',而 runAuthoringRulesparsed 规则在 os lint交的是 normalized 栈(stack = rule.input === 'normalized' ? run.normalized : (run.parsed ?? run.normalized))—— os lint 从不 parse。所以裸表单那一支在「raw 配置 + os lint」这道门上是真实可达的,保留;这一判断连同依据写进了代码注释,免得下一位读者按「schema 会拒 ⇒ 幽灵」再删一次。

2. 全包扫描(packages/lint/src/,不只判 validate-form-layout.ts)

判定方法,两轮:

  1. \.sections|'sections'|"sections" 全包命中,逐个人工判 surface;
  2. 「只读容器自身键」的等价形状扫描 —— 直接搜视图级键的根部读法:
    grep -nE "view\.(columns|filters|sort|viewKind|type|fields|searchableFields|actions|bulkActionDefs|conditionalFormatting|sections|groups|subforms|data)\b" *.ts(去掉测试文件)。
    全包只剩 5 行命中:2 行在 validate-form-layout.ts 自己身上(即本缺陷,view.sections + view.data),另 3 行是两个阶梯实现里的其中一级,见下表最后一行。
文件 views[] 的方式 同族缺陷? 判定依据
validate-form-layout.ts 只读容器根 sections(+ 根 data) 是,本 PR 修 见 §1 实测
validate-visibility-predicates.ts formViewSites:自身 + form + formViews.*;pages 走 walkPageComponents 否,#6248 已修 不动(刚验收)
validate-translatable-sections.ts collectViewSites 阶梯:sections / form.sections / listViews.*.sections / formViews.*.sections 已走全阶梯
validate-translation-references.ts collectViewRecordaddSections 作用于 formViews.*view.formview 自身 已走全阶梯
lint-view-refs.ts isAggregatedViewContainer + spec 的 expandViewContainerWithDiagnostics 用的是 spec 的官方展开
validate-action-locations.ts view.list + view.listViews.* 只关心 list 族放置
validate-action-name-refs.ts view.list + view.listViews.* 同上
validate-chart-bindings.ts view.list + view.listViews.* 图表只在 list 族
validate-searchable-fields.ts view.list + view.listViews.* searchableFields 是 list 键
validate-list-view-mode.ts view.list + view.listViews.* 规则本身就是 list 族
validate-functional-completeness.ts container.list + container.listViews.* 文件内已注明「表单视图无布局绑定契约」
validate-dashboard-action-refs.ts 只收容器名 不读容器内部
validate-view-containers.ts 判的就是容器形状本身 主体即容器
validate-page-field-bindings.ts / validate-react-page-props.ts properties.sections,页面组件树 另一个 surface
(等价形状扫描剩余命中)validate-translatable-sections.ts:130,200validate-translation-references.ts:291 view.data / view.sections完整阶梯里的一级,同一函数另有 form / formViews.* 上下文即证

结论:validate-form-layout.ts 是包内该缺陷仅存的一例。没有拿不准需要另立单的同族命中。

3. 遍历实现的来源 —— 照抄 #6248,不另造第三套

抄的是 validate-visibility-predicates.tsformViewSites(#6248 落地):容器自身 + view.form + 每个 view.formViews 具名项,返回 { form, path, surface }。本 PR 只加了一件 #6248 不需要的东西:每个站点继承的对象绑定(本规则要解析字段引用,可见性规则不需要)。

两份 repo 内的实现有一处出入,按实测取舍并在代码里写明:

判据取自 schema 而非印象:ObjectListViewSchema = ListViewSchema.omit({userFilters}).extend(…),ListViewSchema 不声明 sections;spec 的 expandViewContainerWithDiagnostics 也把 list / listViews.* 归为 list 族、form / formViews.* 归为 form 族。所以那一级只可能读到 undefined —— 在那边不花钱,在这边也不买东西。#6248 的窄阶梯;两份实现在「可能装 section 的每一级」上完全一致。

objects[].views 两边都不走:object.zod.ts 已具名墓碑化(「views is not an ObjectSchema field」),读它只能对 schema 本就拒绝的栈生效 —— #4984 / #5017 清掉的那种幽灵分支。

4. 三位置对照实测(同一个坏表单,三个位置)

位置 修前 修后
views[0].sections(条目自身即裸表单) form-field-unknown@views[0].sections[0].fields[0] 同左
views[0].form.sections(容器默认表单) [] form-field-unknown@views[0].form.sections[0].fields[0]
views[0].formViews.edit.sections(具名表单视图) [] form-field-unknown@views[0].formViews.edit.sections[0].fields[0]

5. 反向验证 A / B / C(先申报,后执行)

声明 A —— 不可达 → 可达

申报:修前只有第一个位置报,修后三个都报。
实测:与 §4 表格一致,修前修后各跑一次取证,完全命中。

声明 B —— 变异体(遍历退回 origin/main 形状,保留全部新断言)

申报(执行前写死):6 条转红 / 3 条按设计不转红。
实测:6 红 3 绿,与申报逐条一致。

# 断言 极性 申报 实测
1 三位置同报 肯定式 ✅ 红
2 where 标注子容器 肯定式 ✅ 红
3 子容器继承容器绑定 肯定式 ✅ 红
4 读 legacy groups 肯定式 ✅ 红
5 map 形态 views 报在真实键上 肯定式 ✅ 红
6 真实 app 形状上确实报了 肯定式 ✅ 红
7 不走 list / listViews.* 否定式 不红(遍历被削空后平凡成立) ✅ 绿
8 干净 app 栈报 0 否定式 不红,且是空绿 ✅ 绿
9 fixture 能通过严格 schema 结构守卫 不红(测的是 schema 不是规则) ✅ 绿

vitest 原文:

❯ src/validate-form-layout.test.ts (16 tests | 6 failed) 68ms
     × reports the SAME broken form in all three placements
     × names the sub-container in `where`, so two forms under one view are distinguishable
     × a sub-container INHERITS the container binding when it declares none
     × reads the legacy `groups` bucket too — measured NOT folded into `sections` at parse
     × reports a map-shaped `views` at the key it sits at, not a synthetic index
     × reports every planted defect on that stack — this is the assertion #6251 exists for
 Test Files  1 failed (1)
      Tests  6 failed | 10 passed (16)

空绿自查(逐条,不只报有没有):全 16 条断言按「功能回退后是否仍通过、以及为什么通过」过了一遍,查出 1 条真空绿:

声明 C —— 不误报(真实 examples 实跑)

申报:修后新增报告必须都是真缺陷;若报出存量问题,如实报数量与样例,⛔ 不为让门变绿而放宽规则。
实测:把规则跑在三个真实 example 栈上(normalizeStackInput 后的真实配置,非合成 fixture),并同时跑 origin/main 版本做对照:

### app-showcase: 6 container(s) | form sites: root-sections=0 (read before) + form/formViews=10 (read ONLY after)
    findings before=0  after=0
### app-crm:      3 container(s) | form sites: root-sections=0 (read before) + form/formViews=4  (read ONLY after)
    findings before=0  after=0
### app-todo:     1 container(s) | form sites: root-sections=0 (read before) + form/formViews=0  (read ONLY after)
    findings before=0  after=0

两层结论:

  1. 0 误报,也没有存量真缺陷被翻出来 —— 三个 example 的表单本来就是干净的,无需任何放宽。
  2. 顺带把「幽灵」量化了:三个 app 在条目根部的表单站点数合计 0,在 form / formViews 下合计 14。旧遍历在所有出货 example 上无物可读,报绿正是因为读了 0 个站点。

6. Changeset 级别依据

@objectstack/lint 是发布包 ⇒ 走真 changeset(skip-changeset)。

级别 patch。依据:本单是既有规则的遍历/判定修正,没有新增规则、没有新增导出、没有改 severity / rule id / 消息文案。仓内同类先例即 patch:flow-lint-loop-body-descent.md(flow 规则族补 loop 体下降)、body-write-lint-message-driver-truth.md。v17 窗口期禁 major,本单也够不上 minor。

7. 门禁 EXIT 表

命令 EXIT
包测试 pnpm --workspace-concurrency=2 --filter @objectstack/lint test 0(62 files / 1528 tests passed)
包类型检查 pnpm --workspace-concurrency=2 --filter @objectstack/lint typecheck 0
仓库 ESLint(CI 的 ESLint job 主步) pnpm lint 0
控制字节门 pnpm check:nul-bytes 0(scanned 6016 tracked text files)
控制字节自扫(超出门禁范围) grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' (改动的三个文件) 1 = 无命中
ADR 锚点 pnpm check:adr-anchors 0
role 词 ratchet pnpm check:role-word 0
docs-audit scope pnpm check:docs-audit-scope 0
release-notes 漂移 pnpm check:release-notes 0

规则的既有测试就在 packages/lint/src/validate-form-layout.test.ts(未散落到 packages/cli/test/)。消费半径也扫了:packages/cli/test/authoring-rule-command-parity.test.tsformView() 造的是 sections: [],且断言只过滤 severity === 'error',本族两条是 warning,不受影响;packages/cli/test/doctor-refs.test.tssections fixture 喂的是 findUnusedObjects,不是本规则。examples/** 里没有任何真正写下的 colSpan: 键(只有一处注释提到它)。

8. 不在本 PR 里


Generated by Claude Code

claude added 2 commits August 7, 2026 15:45
`form-field-unknown` / `absolute-colspan-discouraged` 只读 `views[]` 条目
自身的 `sections`,其余一律 `continue`。但 `views[]` 是视图**容器** ——
`ViewSchema` 自有键只有 name/label/object/list/form/listViews/formViews,
表单 `sections` 在下一层的 `form` 与 `formViews.<key>` 下。于是唯一被读
的那种形状,恰恰是严格 `ViewSchema` 会拒绝的形状(实测报
`unrecognized_keys` 并点名 `sections`),真实 app 出货的形状一个都没被检查。

三个 example app 实测:app-showcase / app-crm / app-todo 在条目根部有 0 个
表单站点,在 form / formViews.<key> 下有 14 个 —— 旧遍历在它们身上无物可读,
报绿正是因为什么都没读到(#4984 / #5009 的幽灵检查族)。

遍历直接照抄 #6248 落在同包 `validate-visibility-predicates.ts` 里的
`formViewSites`,不另造第三套;list / listViews.<key> 是
`ObjectListViewSchema`,按 schema 不带 sections,故不走;`objects[].views`
已被 `object.zod.ts` 具名墓碑化,同样不走。另补:legacy `groups` 桶(实测
parse 阶段并未折叠进 sections)、finding 的 `where` 标注子容器、子容器缺
`data.object` 时继承容器绑定、map 形态的 `views` 报在真实键上。

两条规则的严重级别、消息与提示一字未改;三个 example app 上新增 finding 数
为 0,即不引入误报。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
反向验证声明 B 的空绿自查查出:「干净 app 栈报 0」在遍历被退回后照样通过 ——
因为什么都没读到,不是因为没有缺陷。断言保留(误报守卫仍有价值),但把它
只在与「真实 app 形状上确实报了」配对时才成立这件事写进文件,免得日后
后者被削弱而前者被当成独立保障。

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 4:10pm

Request Review

@github-actions github-actions Bot added the size/l label 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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026
CI 的 `check:type-check-debt` 把 `packages/lint` 的**测试层**重新纳入 tsc 后
测得 48 个 raw error,超过 TEST_DEBT 冻结的 42(+6)。包自己的
`pnpm typecheck` 看不到这个 —— 它的 tsconfig 把 `**/*.test.ts` 排除在外。

根因是一处 TS2835:`./validate-form-layout` 缺 `.js` 扩展名,在
`moduleResolution: node16` 下解析失败,导入即退化为 `any`,下游每个
`.map(f => …)` 都变成 TS7006。补上扩展名(本包其余测试文件本就都是这个写法,
这一处是异类)后该文件 9 个 error 全清,包的测试层从 48 降到 39 —— 低于
冻结值,ratchet 只降不升,按门的规则属 ℹ 而非 error。

TEST_DEBT 条目保持 42 不动:门明确「改进不必为记账付费」,且并发改动下
下调数字容易与他人的测量赛跑。

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

hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

补记:CI TypeScript Type Check 首跑转红,已修(第三个 commit)

正文 §7 的门禁表是本地跑出来的,漏了一道只有 CI 会跑的门 —— 如实补记,不改写原表。

红的内容:pnpm check:type-check-debt

• @objectstack/lint: TEST_DEBT records 42 raw tsc error(s), `tsc --noEmit` now reports 48 (+6).
  TEST_DEBT is frozen debt, not a permission slip -- the ledger is a ratchet and may only shrink

为什么本地 pnpm --filter @objectstack/lint typecheck 是绿的:该包的 tsconfig.json**/*.test.ts / **/*.spec.ts 排除在外,所以包自己的 tsc --noEmit 根本看不见测试层。CI 的门会生成一个 extends 原配置、去掉测试 glob 的兄弟 tsconfig 重新测量,这才是 48 的来源。本地复现方式即照做一遍。

根因是一处,不是六处:validate-form-layout.test.ts 的相对导入 './validate-form-layout'.js 扩展名 —— 在 moduleResolution: node16 下这是 TS2835,导入随即退化为 any,下游每一个 .map(f => …) 都报 TS7006。本包其余测试文件本来就都写 .js(validate-visibility-predicates.test.tsvalidate-translatable-sections.test.tsauthoring-rule-input-tier.test.ts …),这一处是异类

修法:补上扩展名。该文件 9 个 error 全清(其中 3 个是我之前就存在的存量,顺带一并消失 —— 同一行修复的必然结果,不是顺手扩围)。包的测试层 48 → 39,低于冻结值 42。

TEST_DEBT 条目保持 42 不动:门对下降明确判为 ℹ 而非 error(「an improvement must not have to pay a bookkeeping toll to land」),且并发改动下调数字容易与他人的测量赛跑。要不要把 42 收到 39 是台账清扫的事,不该由这个缺陷单顺手决定。

修后 CI 全绿(24 个 check,逐条 conclusion 而非聚合状态):ESLint ✅ success、TypeScript Type Check ✅ success、Check Changeset ✅ success(带真 changeset,首跑即绿)、Test Core 3 分片 + 汇总 ✅、Dogfood Regression Gate 3 分片 + 汇总 ✅、Build Core / Dogfood Verify CLI / Temporal Conformance ✅;Console Pin Gate / Build Docs 为 skipped。无 skip-changeset 标签(本单是发布包的真 changeset,不适用)。


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review August 7, 2026 16:46
@hotlong
hotlong added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 01fd9e1 Aug 7, 2026
25 checks passed
@hotlong
hotlong deleted the claude/issue-6251-form-layout-view-containers branch August 7, 2026 17:02
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/l tests tooling

Projects

None yet

2 participants