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

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