Skip to content

refactor(lint): converge the triplicated collectionEntries and view binding ladder (#6662) - #7186

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6662-lint-helper-convergence
Aug 10, 2026
Merged

refactor(lint): converge the triplicated collectionEntries and view binding ladder (#6662)#7186
os-project-manager merged 1 commit into
mainfrom
claude/issue-6662-lint-helper-convergence

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

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), and
deliberately 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 holds
one of the three binding copies — is no longer held.

Premise re-verified against origin/main at 5087ac635 before 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 one

Coerces a collection authored either as an array or as a name-keyed map into
records carrying their config path.

file was
validate-form-layout.ts local copy, calls a local isRec
validate-translatable-sections.ts local copy, byte-identical to the above
validate-visibility-predicates.ts local copy, predicates open-coded inline

Now 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-name argument, and the non-records-are-skipped
contract that lets viewContainerSites open with the guard it documents as
unreachable.

2. The binding ladder objectName to object to data.object — 3 copies, now one

Byte-identical bodies under two names:

file was
validate-form-layout.ts boundObject
validate-translatable-sections.ts viewObjectName
validate-translation-references.ts viewObjectName

Now one exported viewObjectName in view-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 every
walker 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:

rule its own composition, unchanged
validate-form-layout site, then fall back to the container
validate-translatable-sections site, then the record, then the default list's binding
validate-translation-references container, then the record
validate-visibility-predicates consumes no binding at all

Not 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 not
folded 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.ts is untouched — its deeper ladder
(name, id, object, list.data.object, form.data.object) was judged
reasoned difference rather than drift by #6381, and the card puts it explicitly
out of scope.

Semantic equivalence: the isRec copies vs the inline-predicate copy

The card required this to be established, not assumed because the docblocks
matched. validate-visibility-predicates's copy differed in one load-bearing
way — its map-branch guard read v && typeof v === 'object' with no
!Array.isArray(v), where the other two called isRec, which has that third
clause.

Structural argument. The array branch returns unconditionally, so the map
guard 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 exactly
isRec(v). The element and value predicates in that copy are the literal body of
isRec inlined, 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 could
separate 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.ts under
"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/main
baseline — the file with its own local copies — over a generated corpus, compared
with JSON.stringify so order counts. The descent is held constant on both
sides, so only the two helpers this card converges vary.

[form-layout]            2520 runs, 2736 findings compared
[translatable-sections]  2520 runs, 2448 findings compared
[visibility-predicates]  2520 runs, 1152 findings compared
[translation-references] 2520 runs, 5288 findings compared
Test Files  1 passed (1)   Tests  4 passed (4)

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 the list-only fallback) with 7 rungs and 10
collection shapes (both authored shapes, junk entries, decorated arrays, empty,
and non-collections), over stack views, objects[].views and pages.

The harness was itself falsified before being trusted: with the data.object
rung dropped from the shared ladder, 3 of the 4 differentials went red — and
visibility-predicates correctly stayed green, because it consumes no
binding 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:

break predicted measured
shared collectionEntries reports a synthetic index for the map shape all map-path consumers red 5 failures in 3 files — the new pin plus the pre-existing map-key pins in validate-form-layout.test.ts and validate-visibility-predicates.test.ts
shared viewObjectName drops the data.object rung all three binding consumers red 37 failures in 6 files, spanning form-layout, translatable-sections, translation-references, plus the registry-wiring and reference-integrity suites

The 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/lint run, before and after.

BEFORE (origin/main @ 5087ac635)
  Test Files  68 passed (68)     Tests  1771 passed | 4 skipped (1775)

AFTER
  Test Files  69 passed (69)     Tests  1804 passed (1804)

All 1771 pre-existing tests still pass — no verdict, message, path or ordering
change anywhere. The delta is +1 file and +29 new tests.

pnpm --filter @objectstack/lint typecheck     clean (noUnusedLocals is on)
eslint packages/lint/src --no-inline-config   clean
node scripts/check-nul-bytes.mjs              OK (6595 files)

pnpm --filter @objectstack/cli test           # consumption radius
  Test Files  103 passed (103)   Tests  1116 passed (1116)

New tests — 29, all falsifiable as shown above:

  • collection-entries.test.ts (18): both authored shapes, index stability across
    skipped entries, record identity, map-key-as-name and the entry's own name
    winning, 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-string
    and wrong-type rungs, data that is not a record, name deliberately not a
    rung. 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-changeset

Judged, not defaulted — and it is the same verdict PR #6657 reached for the same
reason, on the same package.

packages/lint is published, so the question is real. But AGENTS.md asks for
a 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.ts is untouched, and both
view-walk.ts and collection-entries.ts are internal modules that the barrel
does not re-export — verified against the built artifact, where viewObjectName,
collectionEntries and CollectionEntry appear zero times in
dist/index.d.ts and dist/runtime.d.ts. There is nothing to write release
notes about.

Out of scope, honoured


Generated by Claude Code

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

vercel Bot commented Aug 10, 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 10, 2026 2:09am

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

2 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)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/v17.mdx (via @objectstack/lint)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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 the tests label Aug 10, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 10, 2026 02:31
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 58451f3 Aug 10, 2026
32 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6662-lint-helper-convergence branch August 10, 2026 02:51
os-project-manager pushed a commit that referenced this pull request Aug 10, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants