Found while implementing #4442 (PR #4457, the hasOwnProperty guard on mapDensity). Not fixed there: #4442's ruling scoped the work to the prototype-chain index and the prototype-member spellings, and this is a different defect class — coercion, not an unguarded prototype read. Same relationship #4442 itself had to #4440.
What
After #4442 lands, packages/react/src/spec-bridge/bridges/list-view.ts reads:
function mapDensity(rowHeight?: RowHeight) {
if (!rowHeight) return undefined;
if (!Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY, rowHeight)) return undefined;
return ROW_HEIGHT_TO_DENSITY[rowHeight];
}
The first guard rejects only falsy values. Any truthy non-string survives it, and both hasOwnProperty.call and the index then coerce the key with String(...) — so a value that is not a string at all can still select a density.
The core twin does not do this. packages/core/src/utils/normalize-list-view.ts:83 opens with a type guard, not a truthiness guard:
export function rowHeightToDensityMode(rowHeight: unknown): DensityMode | undefined {
if (typeof rowHeight !== 'string') return undefined;
...
So the two surfaces disagree again, on a different input family:
rowHeight |
core |
bridge |
['compact'] |
undefined |
'compact' |
new String('compact') |
undefined |
'compact' |
{ toString: () => 'compact' } |
undefined |
'compact' |
Measured through SpecBridge.transformListView (whose parameter is any — the untyped boundary a host's stored JSON actually crosses) while implementing #4442, and reproduced standalone against both function bodies.
Note the direction: this is the opposite of #4442. There the bridge leaked a function where core abstained; here the bridge invents a legitimate-looking density where core abstains. The second is quieter — nothing downstream can tell the value was fabricated from an array.
Why it matters beyond the two functions
packages/react/src/spec-bridge/__tests__/RowHeightDensityAgreement.test.ts is titled "core and the spec bridge give one answer", and after #4442 its off-spec lists are all strings. The invariant the file claims is therefore broader than the invariant it pins, and this is the gap. A JSON-authored view can hold an array or an object at rowHeight just as easily as a bad string.
Impact
No user hits this today, on the same evidence as #4442: the three-repo sweep in PR #4439, re-run at newer HEADs for #4440, found no off-spec rowHeight authored anywhere at all — let alone a non-string one. Filed as a defect rather than an observation because it is a live disagreement between two published surfaces that a dedicated pin file exists to prevent, but grading it is triage's call, not the filer's.
The fix, when someone takes it
Mirror core's opening guard, one line:
if (typeof rowHeight !== 'string') return undefined;
replacing the if (!rowHeight) truthiness check ('' is falsy and not an own property, so the empty-string case is unaffected either way). Worth extending the agreement pin's invariant loop to a non-string family (['compact'], new String('compact'), an object with a toString) in the same PR, since that is the assertion this currently breaks.
Generated by Claude Code
Found while implementing #4442 (PR #4457, the
hasOwnPropertyguard onmapDensity). Not fixed there: #4442's ruling scoped the work to the prototype-chain index and the prototype-member spellings, and this is a different defect class — coercion, not an unguarded prototype read. Same relationship #4442 itself had to #4440.What
After #4442 lands,
packages/react/src/spec-bridge/bridges/list-view.tsreads:The first guard rejects only falsy values. Any truthy non-string survives it, and both
hasOwnProperty.calland the index then coerce the key withString(...)— so a value that is not a string at all can still select a density.The core twin does not do this.
packages/core/src/utils/normalize-list-view.ts:83opens with a type guard, not a truthiness guard:So the two surfaces disagree again, on a different input family:
rowHeight['compact']undefined'compact'new String('compact')undefined'compact'{ toString: () => 'compact' }undefined'compact'Measured through
SpecBridge.transformListView(whose parameter isany— the untyped boundary a host's stored JSON actually crosses) while implementing #4442, and reproduced standalone against both function bodies.Note the direction: this is the opposite of #4442. There the bridge leaked a function where core abstained; here the bridge invents a legitimate-looking density where core abstains. The second is quieter — nothing downstream can tell the value was fabricated from an array.
Why it matters beyond the two functions
packages/react/src/spec-bridge/__tests__/RowHeightDensityAgreement.test.tsis titled "core and the spec bridge give one answer", and after #4442 its off-spec lists are all strings. The invariant the file claims is therefore broader than the invariant it pins, and this is the gap. A JSON-authored view can hold an array or an object atrowHeightjust as easily as a bad string.Impact
No user hits this today, on the same evidence as #4442: the three-repo sweep in PR #4439, re-run at newer HEADs for #4440, found no off-spec
rowHeightauthored anywhere at all — let alone a non-string one. Filed as a defect rather than an observation because it is a live disagreement between two published surfaces that a dedicated pin file exists to prevent, but grading it is triage's call, not the filer's.The fix, when someone takes it
Mirror core's opening guard, one line:
replacing the
if (!rowHeight)truthiness check (''is falsy and not an own property, so the empty-string case is unaffected either way). Worth extending the agreement pin's invariant loop to a non-string family (['compact'],new String('compact'), an object with atoString) in the same PR, since that is the assertion this currently breaks.Generated by Claude Code