fix(lint): validateFormLayout 走视图容器阶梯,两条规则不再对真实 app 全盘报绿 (#6251) - #6382
Conversation
`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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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
补记:CI
|
Fixes #6251
1. 前提复核 —— 成立,且比 issue 描述更强
按内容定位(不靠行号),
packages/lint/src/validate-form-layout.ts遍历入口原文:即只读容器自身的
sections,其余一律跳过。issue 的前提成立。复核中多测出一条 issue 没写的事实,方向一致但更严重 —— 唯一被读的那种形状,正是严格
ViewSchema会拒绝的形状:所以在
defineStack走过的 parsed 栈上,旧遍历能读到的站点数恒为 0。validateFormLayout在注册表里是input: 'parsed',而runAuthoringRules对parsed规则在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)判定方法,两轮:
\.sections|'sections'|"sections"全包命中,逐个人工判 surface;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.tssections(+ 根data)validate-visibility-predicates.tsformViewSites:自身 +form+formViews.*;pages 走walkPageComponentsvalidate-translatable-sections.tscollectViewSites阶梯:sections/form.sections/listViews.*.sections/formViews.*.sectionsvalidate-translation-references.tscollectViewRecord→addSections作用于formViews.*、view.form、view自身lint-view-refs.tsisAggregatedViewContainer+ spec 的expandViewContainerWithDiagnosticsvalidate-action-locations.tsview.list+view.listViews.*validate-action-name-refs.tsview.list+view.listViews.*validate-chart-bindings.tsview.list+view.listViews.*validate-searchable-fields.tsview.list+view.listViews.*searchableFields是 list 键validate-list-view-mode.tsview.list+view.listViews.*validate-functional-completeness.tscontainer.list+container.listViews.*validate-dashboard-action-refs.tsvalidate-view-containers.tsvalidate-page-field-bindings.ts/validate-react-page-props.tsproperties.sections,页面组件树validate-translatable-sections.ts:130,200、validate-translation-references.ts:291view.data/view.sections是完整阶梯里的一级,同一函数另有form/formViews.*级结论:
validate-form-layout.ts是包内该缺陷仅存的一例。没有拿不准需要另立单的同族命中。3. 遍历实现的来源 —— 照抄 #6248,不另造第三套
抄的是
validate-visibility-predicates.ts的formViewSites(#6248 落地):容器自身 +view.form+ 每个view.formViews具名项,返回{ form, path, surface }。本 PR 只加了一件 #6248 不需要的东西:每个站点继承的对象绑定(本规则要解析字段引用,可见性规则不需要)。两份 repo 内的实现有一处出入,按实测取舍并在代码里写明:
validate-translatable-sections.ts的collectViewSites额外走listViews.*.sections;formViewSites不走。判据取自 schema 而非印象:
ObjectListViewSchema = ListViewSchema.omit({userFilters}).extend(…),ListViewSchema不声明sections;spec 的expandViewContainerWithDiagnostics也把list/listViews.*归为 list 族、form/formViews.*归为 form 族。所以那一级只可能读到undefined—— 在那边不花钱,在这边也不买东西。取 #6248 的窄阶梯;两份实现在「可能装 section 的每一级」上完全一致。objects[].views两边都不走:object.zod.ts已具名墓碑化(「viewsis 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 绿,与申报逐条一致。
where标注子容器groups桶views报在真实键上list/listViews.*vitest 原文:
空绿自查(逐条,不只报有没有):全 16 条断言按「功能回退后是否仍通过、以及为什么通过」过了一遍,查出 1 条真空绿:
ObjectListViewSchema不带sections),不是本次修法。defineStack+normalizeStackInput,断言的是 fixture 本身合法,属结构守卫。按 key-vs-value 判据:这里守的是「容器形状是不是真实可授权面」并且规则要判值(字段引用),故要求 full safeParse 绿(经defineStack真解析),而不是只看unrecognized_keys。views[].sections,而真实 app 形状下views[]是视图容器 ——validateFormLayout两条规则在实测 stack 上不可达 #6251 之前就有的既有断言)在变异体下全绿 —— 那正是它们的职责,不计入自查发现。声明 C —— 不误报(真实 examples 实跑)
申报:修后新增报告必须都是真缺陷;若报出存量问题,如实报数量与样例,⛔ 不为让门变绿而放宽规则。
实测:把规则跑在三个真实 example 栈上(
normalizeStackInput后的真实配置,非合成 fixture),并同时跑 origin/main 版本做对照:两层结论:
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 表
pnpm --workspace-concurrency=2 --filter @objectstack/lint testpnpm --workspace-concurrency=2 --filter @objectstack/lint typecheckpnpm lintpnpm check:nul-bytesgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' (改动的三个文件)pnpm check:adr-anchorspnpm check:role-wordpnpm check:docs-audit-scopepnpm check:release-notes规则的既有测试就在
packages/lint/src/validate-form-layout.test.ts(未散落到packages/cli/test/)。消费半径也扫了:packages/cli/test/authoring-rule-command-parity.test.ts里formView()造的是sections: [],且断言只过滤severity === 'error',本族两条是warning,不受影响;packages/cli/test/doctor-refs.test.ts的sectionsfixture 喂的是findUnusedObjects,不是本规则。examples/**里没有任何真正写下的colSpan:键(只有一处注释提到它)。8. 不在本 PR 里
validate-visibility-predicates.ts(feat(lint): view/page 谓词裸标识符构建期闸门 —— 坏谓词发不出去 (#6128) #6248 刚验收的面)、authoring-rules.ts(docs(lint): 按实测改正normalized输入层的三条依据,并补回归 pin (#6073) #6340 刚落地)、packages/spec/**、examples/**(只读参考形状)、content/docs/releases/**。formViewSites、collectViewSites、本 PR)确有可合并空间,但合并必然要改 feat(lint): view/page 谓词裸标识符构建期闸门 —— 坏谓词发不出去 (#6128) #6248 刚验收的文件,超出本单授权;而只为本 PR 新建一个 helper 文件只会把三份变四份。已按 Prime Directive chore: version packages #10 另立观察单 lint: 视图容器阶梯遍历在 packages/lint 内已有三份实现,彼此按不同判据取舍 #6381(finding标签,未自我认领),不在此 PR 动手。walkPageComponents那一支)。文件原有注释即声明该 walker「刻意保持浅层,以免猜测任意组件的对象绑定」—— 本单只修容器阶梯,不扩到新 surface。warning,不 gating。views[].sections,而真实 app 形状下views[]是视图容器 ——validateFormLayout两条规则在实测 stack 上不可达 #6251 下评论。Generated by Claude Code