Skip to content

finding(components): five readProps() copies object-spread a degenerate config bag — the third channel objectui#6752 / objectui#6760 left open #6783

Description

@claude

Measured while implementing objectui#6760 (the properties hoist), outside its fence — that
card is scoped to packages/react/src/ — so filed separately. Observation-class in the same
sense objectui#6752 and objectui#6760 were: nothing is known to depend on it today.

The fact, measured on c6732825d

objectui#6752 stopped a degenerate props reaching the element as indexed React props;
objectui#6760 stopped a degenerate properties being hoisted into indexed node keys. Both
sites live in packages/react/src/SchemaRenderer.tsx. A third site reads the same two
authored bags and still object-spreads them, in a different package and on a different
channel — the config bag a renderer reads, not the React props it receives.

Five copies of readProps(), all in packages/components/src/renderers/basic/:

  • elements.tsx
  • data-list.tsx
  • text-input.tsx
  • record-picker.tsx
  • metadata-viewer.tsx

Each is the same body:

const fromProperties = (schema?.properties ?? {}) as T;
const fromProps = (schema?.props ?? {}) as T;
return { ...fromProps, ...fromProperties };

?? only replaces null / undefined, so a degenerate value passes straight into the
spread. Measured: Object.keys({ ...'not-a-bag' }) is ["0","1","2","3","4","5","6","7","8"].

The node reaches these renderers carrying the authored value — measured through the real
SchemaRenderer in objectui#6760's probe on the same commit: for
{ type, properties: 'not-a-bag' }, a renderer reading schema sees
schema.properties === 'not-a-bag', and after objectui#6760 that value is no longer
enumerated anywhere upstream. So the config bag one of these five renderers computes for that
node is { '0': 'n', '1': 'o', … } — nine keys nobody authored, which is exactly the shape
the two react-side cards removed on the other two channels.

Why it is worth recording rather than shipping quietly

The hazard is now closed on two of three channels for both spellings of one bag, and open on
the third. That asymmetry is the same thing objectui#6760's triage flagged about the previous
one: the reason a config-bag guard exists was measured to be channel-independent
(objectui#6752), so a site that answers the question differently is drift, not a local choice.
packages/react ended that drift for itself by converging on one exported isConfigBag
(objectui#6761); packages/components has five copies of a reader that never asks it.

Blast radius, honestly bounded

Same denominator as the sibling cards and the same census (objectui#6708 walked every JSON
document, every json doc fence and every TypeScript object literal): zero authored nodes
carry a degenerate config bag. Latent shape, not a live failure.

Arms, if anyone takes it

  • have the five readProps() copies ask the same question the react side asks, and converge
    them the way objectui#6761 converged its six (note @object-ui/components does not depend
    on @object-ui/react, so "import isConfigBag" is not automatically the shape — where the
    one definition should live is part of the card);
  • or rule that a renderer-side config-bag read may reinterpret whatever it is handed, and say
    so at the reader.

Not picked here — objectui#6760's fence is packages/react/src/ only.

Related: objectui#6760 (the hoist half), objectui#6752 (the props half), objectui#6761 (one
predicate in packages/react), objectui#6708 (the census).


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions