Found while implementing #6665 (the non-array node-level data diagnostic). Measured on that
card's merge-base but outside its fence, so filed separately.
The fact
#6665's four-leg re-measurement was run on 5967be095 through the real SchemaRenderer
inside a SchemaRendererProvider holding { customers: [ 2 records ] }, identical columns
in every leg, reading tbody td. Leg 2 is this card:
| node |
rendered body |
{ "type": "data-table", "data": "${data.customers}", ... } |
No results found (that is #6665) |
{ "type": "data-table", "props": { "data": "${data.customers}" }, ... } |
No results found |
{ "type": "data-table", "properties": { "data": "${data.customers}" }, ... } |
the two rows |
{ "type": "data-table", "data": [ 2 literal records ], ... } |
the two rows |
The interesting pair is legs 2 and 3: the SAME key, the SAME value, one envelope apart, and
only properties puts rows on screen.
The mechanism
SchemaRenderer HOISTS properties.* onto the node (minus HOIST_PROTECTED_KEYS), which is
why leg 3 works — the hoisted data is a real array by the time DataTableRenderer
destructures it. props is not hoisted; it is spread as React props on the created element.
DataTableRenderer is declared as ({ schema }) and reads nothing else, so a key written
under props never reaches it.
That is not specific to data-table. It applies to EVERY renderer that reads only schema —
which is the normal shape for the component renderers, as distinct from the element:* family
whose readProps() merges { ...schema.props, ...schema.properties }. So the same key
spelled props is honoured in one renderer family and silently dropped in the other.
props is documented as the annotated legacy alias for the config bag, and the 2026-08-18
ruling recorded on #5123 settled precedence between the two bags (properties wins on both
channels). Neither says the alias is INERT for component renderers, which is what the
measurement above shows.
Why it is not #6665
#6665 was fenced to node-level data, and its diagnostic is deliberately silent here: with
the key under props, node-level data is genuinely absent, so there is nothing for a
data-table-level predicate to honestly say. That silence is pinned by a test in that PR
(does NOT reach into the props envelope) rather than left to be rediscovered. Widening that
predicate to reach into props would patch one component against a repo-wide shape.
What a taker would need to decide first
Whether this is a defect or the declared design. Both readings are available today:
If it is a defect, the arms are the familiar three: hoist props like properties (a
behaviour change on published components, so a ruling), diagnose it at the SchemaRenderer
tier (no behaviour change, one place, covers every renderer), or refuse it at parse (blocked
on the .passthrough() ceiling, #5155 / #6269).
Refs: #6665 (where it was measured) - #6575 (the diagnostic precedent) - #5123 (the two-bag
precedence ruling) - #4795 (the properties authoring-channel contract question) - #5155 /
#6269 (the .passthrough() ceiling).
Generated by Claude Code
Found while implementing #6665 (the non-array node-level
datadiagnostic). Measured on thatcard's merge-base but outside its fence, so filed separately.
The fact
#6665's four-leg re-measurement was run on
5967be095through the realSchemaRendererinside a
SchemaRendererProviderholding{ customers: [ 2 records ] }, identicalcolumnsin every leg, reading
tbody td. Leg 2 is this card:{ "type": "data-table", "data": "${data.customers}", ... }No results found(that is #6665){ "type": "data-table", "props": { "data": "${data.customers}" }, ... }No results found{ "type": "data-table", "properties": { "data": "${data.customers}" }, ... }{ "type": "data-table", "data": [ 2 literal records ], ... }The interesting pair is legs 2 and 3: the SAME key, the SAME value, one envelope apart, and
only
propertiesputs rows on screen.The mechanism
SchemaRendererHOISTSproperties.*onto the node (minusHOIST_PROTECTED_KEYS), which iswhy leg 3 works — the hoisted
datais a real array by the timeDataTableRendererdestructures it.
propsis not hoisted; it is spread as React props on the created element.DataTableRendereris declared as({ schema })and reads nothing else, so a key writtenunder
propsnever reaches it.That is not specific to
data-table. It applies to EVERY renderer that reads onlyschema—which is the normal shape for the component renderers, as distinct from the
element:*familywhose
readProps()merges{ ...schema.props, ...schema.properties }. So the same keyspelled
propsis honoured in one renderer family and silently dropped in the other.propsis documented as the annotated legacy alias for the config bag, and the 2026-08-18ruling recorded on #5123 settled precedence between the two bags (
propertieswins on bothchannels). Neither says the alias is INERT for component renderers, which is what the
measurement above shows.
Why it is not #6665
#6665 was fenced to node-level
data, and its diagnostic is deliberately silent here: withthe key under
props, node-leveldatais genuinely absent, so there is nothing for adata-table-level predicate to honestly say. That silence is pinned by a test in that PR
(
does NOT reach into the props envelope) rather than left to be rediscovered. Widening thatpredicate to reach into
propswould patch one component against a repo-wide shape.What a taker would need to decide first
Whether this is a defect or the declared design. Both readings are available today:
(
BaseSchemais.passthrough()with[key: string]: any), and the result is a silentdrop with a correct-looking header, the same success-receipt shape
data-tableaccepts abindand silently renders its header over an empty body #6575 anddata-table: a${...}expression authored in node-leveldatais not evaluated and renders an empty body silently #6665 exist toremove.
propsis the React-prop channel, component renderers readschema, and thealias was only ever meant for the
readProps()family.If it is a defect, the arms are the familiar three: hoist
propslikeproperties(abehaviour change on published components, so a ruling), diagnose it at the
SchemaRenderertier (no behaviour change, one place, covers every renderer), or refuse it at parse (blocked
on the
.passthrough()ceiling, #5155 / #6269).Refs: #6665 (where it was measured) - #6575 (the diagnostic precedent) - #5123 (the two-bag
precedence ruling) - #4795 (the
propertiesauthoring-channel contract question) - #5155 /#6269 (the
.passthrough()ceiling).Generated by Claude Code