refactor(lint): converge the triplicated collectionEntries and view binding ladder (#6662) - #7186
Conversation
… binding ladder (#6662) #6381 converged the "views[] entry to its real form/view sites" DESCENT onto one shared walker (`view-walk.ts`), and deliberately left two smaller helpers used by the very same rules at three copies each. This is that follow-up, now that #6422 has closed and `validate-translation-references.ts` is no longer held. 1. `collectionEntries` — 3 copies, now one (`collection-entries.ts`): validate-form-layout.ts / validate-translatable-sections.ts (byte-identical) validate-visibility-predicates.ts (same function, predicates open-coded) 2. The binding ladder `objectName -> object -> data.object` — 3 copies under two names, now one (`viewObjectName`, exported from `view-walk.ts`): boundObject in validate-form-layout.ts viewObjectName in validate-translatable-sections.ts viewObjectName in validate-translation-references.ts Only the BASE ladder is shared. Each rule's fallback COMPOSITION stays in its own file, because they differ on purpose and #6657 preserved that deliberately: form-layout falls back to the container, translatable-sections to the container and then to the default `list`'s binding, translation-references to the record, and visibility-predicates needs no binding at all. `lint-view-refs.ts` is untouched: its deeper ladder was judged reasoned difference rather than drift by #6381. Verdicts are unchanged, measured rather than asserted: a temporary differential (not committed) ran all four rules against their origin/main baselines over 2520 generated stacks each -- 10,080 rule runs, 11,624 findings compared with JSON.stringify so order counts -- byte-identical throughout. Co-Authored-By: Claude Opus 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
|
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
…helper #7186 landed `packages/lint/src/collection-entries.ts` on main after this branch was cut. The local 15-line duplicate documented as "folds into that helper when it lands" now does — same semantics, same array/name-keyed-map handling, same walk order, so no verdict or path changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
Fixes #6662
What this is
#6381 / PR #6657 converged the "views[] entry down to its real form/view sites"
descent onto one shared walker (
packages/lint/src/view-walk.ts), anddeliberately left two smaller helpers used by the very same rules at three
copies each. This is that follow-up. The card's window is open: #6422 closed
completed on 2026-08-08, so
validate-translation-references.ts— which holdsone of the three binding copies — is no longer held.
Premise re-verified against
origin/mainat5087ac635before implementing:both helpers were still at three copies each, exactly as the card and triage
recorded. Located by content, never by line number — this card's coordinates had
already drifted twice.
1.
collectionEntries— 3 copies, now oneCoerces a collection authored either as an array or as a name-keyed map into
records carrying their config path.
validate-form-layout.tsisRecvalidate-translatable-sections.tsvalidate-visibility-predicates.tsNow one shared
packages/lint/src/collection-entries.ts. The three docblocks'reasoning is merged there rather than dropped — the path-not-a-synthetic-index
argument, the map-key-becomes-
nameargument, and the non-records-are-skippedcontract that lets
viewContainerSitesopen with the guard it documents asunreachable.
2. The binding ladder
objectNametoobjecttodata.object— 3 copies, now oneByte-identical bodies under two names:
validate-form-layout.tsboundObjectvalidate-translatable-sections.tsviewObjectNamevalidate-translation-references.tsviewObjectNameNow one exported
viewObjectNameinview-walk.ts. The majority spelling wins,and it is also the CLI i18n extractor's (
packages/cli/src/utils/i18n-extract.ts)and the spec resolver's (
packages/spec/src/system/i18n-resolver.ts), so everywalker in the repo now agrees on which object a record binds to by reading the
same four lines.
Only the BASE ladder is shared — composition is preserved
This is the constraint that shaped the change. #6657 preserved the per-rule
fallback composition on purpose, and so does this:
validate-form-layoutvalidate-translatable-sectionslist's bindingvalidate-translation-referencesvalidate-visibility-predicatesNot one of those expressions changed. What moved is the single rung each of them
starts from.
view-walk.ts's module docblock previously said the binding was notfolded in at all; it now distinguishes the shared base rung from the composition
that stays with each consumer, so the next reader is not told something untrue.
lint-view-refs.tsis untouched — its deeper ladder(
name,id,object,list.data.object,form.data.object) was judgedreasoned difference rather than drift by #6381, and the card puts it explicitly
out of scope.
Semantic equivalence: the
isReccopies vs the inline-predicate copyThe card required this to be established, not assumed because the docblocks
matched.
validate-visibility-predicates's copy differed in one load-bearingway — its map-branch guard read
v && typeof v === 'object'with no!Array.isArray(v), where the other two calledisRec, which has that thirdclause.
Structural argument. The array branch
returns unconditionally, so the mapguard is only ever evaluated on a value that is already not an array. At that
point
!Array.isArray(v)is trivially true, which makes the inline guard exactlyisRec(v). The element and value predicates in that copy are the literal body ofisRecinlined, character for character.Empirical check. A differential probe ran both bodies, transcribed verbatim
from
origin/main, over a 35-input corpus chosen for the classes that couldseparate them: arrays (the class the missing clause is about), decorated arrays
carrying non-index own keys, sparse arrays with holes, name-keyed maps, prototype-less
objects, and every exotic
typeof x === 'object'value (Date, RegExp, Map, Set,functions, classes, null, bigint, Symbol). Identical output on every input,
including reference identity of the record handed back on the array branch.
Both facts are now pinned in
collection-entries.test.tsunder"an array is never enumerated as a map".
Verification
Refactor-grade differential (temporary harness, not committed — the same
instrument PR #6657 used). Each converged rule ran against its
origin/mainbaseline — the file with its own local copies — over a generated corpus, compared
with
JSON.stringifyso order counts. The descent is held constant on bothsides, so only the two helpers this card converges vary.
10,080 rule runs, 11,624 findings, byte-identical throughout. The corpus crosses
12 binding shapes (including precedence pairs, empty strings, wrong types,
non-record
data,name-only, and thelist-only fallback) with 7 rungs and 10collection shapes (both authored shapes, junk entries, decorated arrays, empty,
and non-collections), over stack views,
objects[].viewsandpages.The harness was itself falsified before being trusted: with the
data.objectrung dropped from the shared ladder, 3 of the 4 differentials went red — and
visibility-predicatescorrectly stayed green, because it consumes nobinding at all. That asymmetry is the harness proving it compares something real.
Reverse verification, direction predicted before each run. Both predictions
were "red across every consumer at once", and both held:
collectionEntriesreports a synthetic index for the map shapevalidate-form-layout.test.tsandvalidate-visibility-predicates.test.tsviewObjectNamedrops thedata.objectrungThe second row is the one worth reading: one edit, and every consumer of the
ladder goes red together. That is the property the convergence buys, and the
failure it prevents — before #6381 the same rung had to be fixed three times, and
twice it was fixed in only one place (#6128 / #6248, then #6251).
Suites — the full
packages/lintrun, before and after.All 1771 pre-existing tests still pass — no verdict, message, path or ordering
change anywhere. The delta is +1 file and +29 new tests.
New tests — 29, all falsifiable as shown above:
collection-entries.test.ts(18): both authored shapes, index stability acrossskipped entries, record identity, map-key-as-
nameand the entry's ownnamewinning, non-collections, and the array-is-never-a-map equivalence pins. Then a
"one coercion, three consumers" table feeding one fixture to all three rules
at once.
view-walk.test.ts(+11): the ladder's rung order and precedence, empty-stringand wrong-type rungs,
datathat is not a record,namedeliberately not arung. Then a "one base ladder, three binding consumers" table binding through
the deepest rung only — the one a hand-written ladder is likeliest to drop —
each asserted through an observable that exists only when the binding resolved,
with its negative half, so "the finding fired" cannot pass for a rule that
resolved the wrong object or none.
Why
skip-changesetJudged, not defaulted — and it is the same verdict PR #6657 reached for the same
reason, on the same package.
packages/lintis published, so the question is real. But AGENTS.md asks fora changeset on "a feature or functional improvement", and notes that pure bug
fixes do not need one. This is neither: it is an internal refactor with a
measured zero-behaviour delta (the differential above, plus 1771 unchanged
pre-existing verdicts).
And nothing a consumer can observe changes.
index.tsis untouched, and bothview-walk.tsandcollection-entries.tsare internal modules that the barreldoes not re-export — verified against the built artifact, where
viewObjectName,collectionEntriesandCollectionEntryappear zero times indist/index.d.tsanddist/runtime.d.ts. There is nothing to write releasenotes about.
Out of scope, honoured
lint-view-refs.ts— reasoned difference, not drift (lint: 视图容器阶梯遍历在 packages/lint 内已有三份实现,彼此按不同判据取舍 #6381). Not touched.neighbouring
asArraycopies in these same files are a separate shape and wereleft alone.
Generated by Claude Code