Skip to content

refactor(lint): converge the three view-container ladder traversals onto one shared walker (#6381) - #6657

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6381-view-ladder-convergence
Aug 8, 2026
Merged

refactor(lint): converge the three view-container ladder traversals onto one shared walker (#6381)#6657
os-project-manager merged 1 commit into
mainfrom
claude/issue-6381-view-ladder-convergence

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6381

What this is

The descent from a views[] entry down to the records that can actually carry
sections had three independent implementations in packages/lint:

implementation file rungs walked
formViewSites validate-visibility-predicates.ts (#6248) self + form + formViews.*
collectViewSites validate-translatable-sections.ts self + form + listViews.* + formViews.*
formViewSites validate-form-layout.ts (#6251) self + 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 followed
here — a new internal packages/lint/src/view-walk.ts.

Premise re-verified against origin/main at 487a197b5 before implementing:
all three implementations still existed and were still separate. #6616 landed
in validate-translatable-sections.ts earlier today but touched only the
heading-spelling read at the bottom of the file, disjoint from the ladder.

The union, not the intersection

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. That surface is declared as part of the
    rule's section face in its own module docblock and pinned by its tests.
  • formViewSites() — the same ladder minus listViews.*. A filter over the
    one ladder
    , not a second ladder, so a broken rung breaks every consumer at
    once. Consumed by validate-visibility-predicates and validate-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, over ListViewSchema
at :1067) declares no sections, and 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 — and it is recorded in the
walker's docblock so the filter carries its reasoning. But os lint runs
authoring 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 form rung is kept for the reason #5415 established — the default anchor
that is neither a formViews.* entry nor the record's own — and that reasoning
now lives in one place instead of three.

Binding resolution: deliberately not folded in

The three consumers compose their binding fallbacks differently
(validate-form-layout falls back to the container;
validate-translatable-sections falls back to the container and then to the
default list's binding; validate-visibility-predicates needs no binding at
all). 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 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 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/main baselines over 222 generated
stacks
— every rung, both collection shapes (array and name-keyed map),
object-embedded views, object-level listViews, unnamed artifact-shaped
containers, non-record junk rungs, and every binding shape including the
list-only fallback. 888 rule runs, 3295 findings compared with
JSON.stringify, so order counts:

[visibility]   444 runs, 1660 findings compared
[form-layout]  222 runs, 1312 findings compared
[translatable] 222 runs,  323 findings compared
Test Files  1 passed (1)   Tests  4 passed (4)

Byte-identical throughout. Emission order is preserved by walking
self -> form -> listViews.* -> formViews.*; the listView filter reduces that
to the two form rules' exact previous order.

Reverse verification, direction predicted before each run.

break predicted measured
drop the formViews.* rung all three rules red 22 failures across all three suites AND all three differential baselines, from one edit
drop the form rung (#5415) all three rules red 15 failures, same spread
drop the listViews rung asymmetric — only translatable-sections red 6 failures; validate-visibility-predicates.test.ts and validate-form-layout.test.ts both passed, and only the translatable differential diverged

The 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.

pnpm --filter @objectstack/lint test
  Test Files  63 passed (63)      Tests  1608 passed | 4 skipped (1612)

pnpm --filter @objectstack/lint typecheck
  (clean)

pnpm --filter @objectstack/cli test        # consumption radius: the CLI runs these rules
  Test Files  94 passed (94)      Tests  978 passed (978)

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-changeset

Measured, not assumed: no verdict, message, path or ordering changes anywhere
(the differential above), and view-walk.ts is internal — not exported from
index.ts, matching the existing flow-walk.ts precedent. Nothing a lint
consumer can observe changes, so there is nothing to write release notes about.

Out of scope, honoured


Generated by Claude Code

…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
@vercel

vercel Bot commented Aug 8, 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 8, 2026 8:56am

Request Review

@os-project-manager os-project-manager added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 8, 2026 — with Claude
@github-actions github-actions Bot added the size/l label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 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

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint: 视图容器阶梯遍历在 packages/lint 内已有三份实现,彼此按不同判据取舍

2 participants