refactor(lint): converge the three view-container ladder traversals onto one shared walker (#6381) - #6657
Merged
os-project-manager merged 1 commit intoAug 8, 2026
Conversation
…nto one shared walker (#6381) The descent from a `views[]` entry down to the records that can actually carry `sections` had three independent implementations in `packages/lint`: - `formViewSites` in validate-visibility-predicates.ts (#6248) - `collectViewSites` in validate-translatable-sections.ts - `formViewSites` in validate-form-layout.ts (#6251, a verbatim copy of the first) The same rung was measured MISSING twice in two consecutive issues (#6128 → #6248, then #6251), each time fixed in one copy only. Copying the fixed walker was the cheapest move each time; the copy count is the argument for one source. `page-walk.ts` (#3583) is the in-package model and is followed here. `view-walk.ts` yields the UNION of the three ladders as typed sites — `self` / `form` / `listView` / `formView` — and each consumer filters: - `viewContainerSites()` — the full ladder. Consumed by validate-translatable-sections, which needs the `listViews.*` rung to reach an object's own `listViews` container (declared as part of its section face in its own module docblock, and pinned by its tests). - `formViewSites()` — the same ladder minus `listViews.*`, a FILTER and not a second ladder. Consumed by validate-visibility-predicates and validate-form-layout, whose tests pin that they do NOT walk list views. The union, not the intersection, is deliberate. The narrowest common shape would have deleted the `listViews.*` rung, and a schema proof was NOT sufficient licence to do so: `ObjectListViewSchema` (view.zod.ts:1864-1865, over `ListViewSchema` at :1067) declares no `sections` — the only declaration of `sections`/`groups` in the file is `FormViewSchema`'s at :1649-1650 — so on a schema-VALID stack that rung reads `undefined`, but `os lint` runs authoring rules over the NORMALIZED stack, and the third rule documents that surface. The `form` rung (#5415, the default anchor that is neither a `formViews.*` entry nor the record's own) is likewise kept and now carries its reasoning in one place. Binding resolution is deliberately NOT folded in: the three consumers compose their fallbacks differently and a refactor that changes a verdict is a failed refactor. One equivalence WAS proven and collapsed, in validate-translatable-sections: the entry's own site resolved `recordObject ?? listBinding` while sub-containers resolved `viewObjectName(sub) ?? recordObject ?? listBinding`; for the entry `viewObjectName(view)` IS `recordObject`, so the sub-container formula returns the same answer and the two branches are now one expression. Refactor-grade evidence: a differential harness (temporary, not committed) ran both the converged rules and their `origin/main` baselines over 222 generated stacks covering every rung, both collection shapes, object-embedded views, unnamed containers, junk rungs and every binding shape — 888 rule runs, 3295 findings compared with JSON.stringify so ORDER counts. Byte-identical throughout. Emission order is preserved by walking `self → form → listViews.* → formViews.*`, which the `listView` filter reduces to the two form rules' exact previous order. Reverse verification, direction predicted before each run: - drop the `formViews.*` rung → predicted all three rules red: 22 failures across all three suites AND all three differential baselines, from one edit. - drop the `form` rung (#5415) → predicted all three red: 15 failures, same spread. - drop the `listViews` rung → predicted ASYMMETRIC: only translatable-sections red. Measured 6 failures; validate-visibility-predicates.test.ts and validate-form-layout.test.ts both passed, and only the translatable differential diverged. That is the filter proving it is a filter, and the rung proving it is live for exactly one consumer. No changeset: no verdict, message, path or ordering changes, and `view-walk.ts` is internal — not exported from index.ts, matching `flow-walk.ts`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 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:
|
os-project-manager
marked this pull request as ready for review
August 8, 2026 08:57
os-project-manager
enabled auto-merge
August 8, 2026 08:57
os-project-manager
deleted the
claude/issue-6381-view-ladder-convergence
branch
August 8, 2026 09:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6381
What this is
The descent from a
views[]entry down to the records that can actually carrysectionshad three independent implementations inpackages/lint:formViewSitesvalidate-visibility-predicates.ts(#6248)form+formViews.*collectViewSitesvalidate-translatable-sections.tsform+listViews.*+formViews.*formViewSitesvalidate-form-layout.ts(#6251)form+formViews.*The same rung was measured missing twice in two consecutive issues (#6128 to
#6248, then #6251), and each time it was fixed in one copy only. Copying the
fixed walker was the cheapest move each time; the copy count is the argument for
a single source.
page-walk.ts(#3583) is the in-package model and is followedhere — a new internal
packages/lint/src/view-walk.ts.Premise re-verified against
origin/mainat487a197b5before implementing:all three implementations still existed and were still separate.
#6616landedin
validate-translatable-sections.tsearlier today but touched only theheading-spelling read at the bottom of the file, disjoint from the ladder.
The union, not the intersection
view-walk.tsyields the union of the three ladders as typed sites(
self/form/listView/formView), and each consumer filters:viewContainerSites()— the full ladder. Consumed byvalidate-translatable-sections, which needs thelistViews.*rung to reachan object's own
listViewscontainer. That surface is declared as part of therule's section face in its own module docblock and pinned by its tests.
formViewSites()— the same ladder minuslistViews.*. A filter over theone ladder, not a second ladder, so a broken rung breaks every consumer at
once. Consumed by
validate-visibility-predicatesandvalidate-form-layout,whose tests pin that they do not walk list views.
Flattening to the narrowest common shape would have deleted the
listViews.*rung. The schema proof was NOT sufficient licence to do that. The proof itself
holds —
ObjectListViewSchema(view.zod.ts:1864-1865, overListViewSchemaat
:1067) declares nosections, and the only declaration ofsections/groupsin the file isFormViewSchema's at:1649-1650, so on aschema-valid stack that rung reads
undefined— and it is recorded in thewalker's docblock so the filter carries its reasoning. But
os lintrunsauthoring rules over the normalized stack, not a parsed one, and the third
rule documents that surface deliberately. Dropping it would have been the "next
person edits only one copy" failure this card exists to prevent, inverted.
The
formrung is kept for the reason #5415 established — the default anchorthat is neither a
formViews.*entry nor the record's own — and that reasoningnow lives in one place instead of three.
Binding resolution: deliberately not folded in
The three consumers compose their binding fallbacks differently
(
validate-form-layoutfalls back to the container;validate-translatable-sectionsfalls back to the container and then to thedefault
list's binding;validate-visibility-predicatesneeds no binding atall). A refactor that changes a verdict is a failed refactor, so those stay in
their own files.
One equivalence was proven and collapsed, inside
validate-translatable-sections: the entry's own site resolvedrecordObject ?? listBindingwhile sub-containers resolvedviewObjectName(sub) ?? recordObject ?? listBinding. For the entry,viewObjectName(view)isrecordObject, so the sub-container formulareturns the same answer on it — the two branches are now one expression, and the
proof is written next to it.
Verification
Refactor-grade differential. A temporary harness (not committed) ran both
the converged rules and their
origin/mainbaselines over 222 generatedstacks — every rung, both collection shapes (array and name-keyed map),
object-embedded views, object-level
listViews, unnamed artifact-shapedcontainers, non-record junk rungs, and every binding shape including the
list-only fallback. 888 rule runs, 3295 findings compared withJSON.stringify, so order counts:Byte-identical throughout. Emission order is preserved by walking
self -> form -> listViews.* -> formViews.*; thelistViewfilter reduces thatto the two form rules' exact previous order.
Reverse verification, direction predicted before each run.
formViews.*rungformrung (#5415)listViewsrungvalidate-visibility-predicates.test.tsandvalidate-form-layout.test.tsboth passed, and only the translatable differential divergedThe third row is the one worth reading: it is the filter proving it is a filter,
and the rung proving it is live for exactly one consumer. If the convergence had
not actually happened, rows one and two would have shown a single rule going red.
Suites.
New pins in
packages/lint/src/view-walk.test.ts(13 tests): the ladder's rungs,order, path/surface/kind, the non-record guards, the filter's identity property,
and a "one ladder, three consumers" table that feeds one fixture per rung to all
three rules at once.
Why
skip-changesetMeasured, not assumed: no verdict, message, path or ordering changes anywhere
(the differential above), and
view-walk.tsis internal — not exported fromindex.ts, matching the existingflow-walk.tsprecedent. Nothing a lintconsumer can observe changes, so there is nothing to write release notes about.
Out of scope, honoured
collectViewRecordinvalidate-translation-references.ts— adifferent-purpose fourth implementation; lint:
collectViewRecord的 listViews/formViews 分支收「map key + 内层 name」两种拼写,而组装器只认 map key —— 冲突改名时两者恰好相反 #6422 is in the maintainer decisionbox. Not touched.
lint-view-refs.ts— deliberately consumes spec'sexpandViewContainerWithDiagnostics; its binding difference is reasoned, notdrift. Not touched.
packages/specschemas were read only, never modified.Generated by Claude Code