Skip to content

finding: sixteen lint collection readers still hold a private copy of the recordsOf coercion #15728

Description

@claude

Found while implementing #15636 (the 23-reader sweep); filed as a finding only, not claimed.

#15552 re-pointed the stack.objects readers onto the shared, guarded recordsOf; #15636 re-pointed 22 more. Sixteen modules in packages/lint/src still declare a PRIVATE copy of the same coercion. None of them crashes today, which is exactly why they are worth a card: they are the mechanism that produced the defect, not the defect.

Measured

On 1c1421401, packages/lint/src held 40 module-level declarations of the shape (v: unknown) returning AnyRec[] — one canonical (recordsOf in object-graph.ts) and 39 copies. After #15636 lands there are 17 copies. One of those (validate-chart-bindings.ts) is deferred to #15575, which owns that file. The other sixteen are this finding:

Twelve carry the function form with the array-branch filter added LOCALLY, in two different spellings (v.filter((x): x is AnyRec => isRec(x)) and v.filter((x): x is AnyRec => !!x && typeof x === 'object')):

validate-action-body-writes.ts, validate-ai-agent-authoring.ts, validate-ai-surface-affinity.ts, validate-ai-tool-references.ts, validate-flow-node-writes.ts, validate-hook-body-writes.ts, validate-nav-object-servability.ts, validate-nav-target-refs.ts, validate-page-visualization-bindings.ts, validate-readonly-action-writes.ts, validate-rule-compilability.ts, validate-view-page-refs.ts

Four carry an arrow form that handles ONLY the list shape and casts it unchecked — const asArray = (v: unknown): AnyRec[] => (Array.isArray(v) ? (v as AnyRec[]) : []); — reading stack.pages:

validate-jsx-pages.ts, validate-page-source-styling.ts, validate-react-page-props.ts, validate-react-pages.ts

Why the four page walks do not throw today

Each guards at the CALL SITE rather than at the reader: const page = pages[p]; if (!page || page.kind !== 'react') continue;. A null member is skipped by !page, and 42 / 'x' / [] all read .kind as undefined and are skipped too. So the crash class is closed for them — by a guard three lines downstream of the unchecked cast, in four independent places, none of which the reader itself promises. Move the guard back into the reader and the four call-site checks become belt-and-braces instead of load-bearing.

The map shape is also silently unsupported in those four: a pages: authored as a name-keyed map reads as an empty list, so every page lint passes vacuously. recordsOf handles both shapes, so re-pointing closes that too — worth confirming against the page schema before assuming it is a bug rather than a deliberate narrowing.

Suggested shape

Delete the sixteen copies and re-point their call sites onto recordsOf from ./object-graph.js, the same edit #15552 and #15636 made. Then delete the corresponding rows from COPY_LEDGER (and, for the four arrow copies, from UNGUARDED_ALLOWANCE) in packages/lint/src/collection-coercion-single-copy.test.ts — that test is exact in both directions, so it fails while a row names a file that no longer holds a copy, and the cleanup is not finished until the rows are gone.

Context


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions