Found while verifying the ADR 058 wrapper re-expansion in the render path, against workspaces/eg in specs-testing.
Symptom
A collapsed-primitive component whose root content is bound to a prop comes back with a literal string instead of the binding:
# baseline — specs/Heading/variants.yaml
default:
elements:
root:
content: { $binding: "#/props/text" }
# round-trip
default:
elements:
root:
content: "Heading"
Why this is not simply "bindings on collapsed roots are unsupported"
Text — same shape, same prop name — round-trips the binding correctly:
| Component |
root anatomy |
baseline content |
round-trip content |
Text |
type: text, originalName: Text |
{$binding: "#/props/text"} |
{$binding: "#/props/text"} ✅ |
Heading |
type: text, originalName: Text |
{$binding: "#/props/text"} |
"Heading" ❌ |
So the mechanism works. Something specific to Heading prevents the TEXT component property reference from being attached to (or read back from) the recreated leaf.
What has been ruled out
- Not the collapse re-expansion itself — both components rebuild the wrapper and recreate the leaf, and both round-trip an identical
anatomy.root including $extensions.com.figma.originalName.
- Not a per-variant content override — neither component overrides
root.content in any variant; both bind only in default. Heading has 8 variants, Text has 54, so variant count is not the discriminator.
- Not a missing property —
props.text is present as a string prop on both sides for both components.
- No warning is emitted.
specs render specs/Heading completes with no ⚠ output at all, so the writer believes it attached the binding. Compare Icon, where the same class of failure does warn (binding "root.characters" → "name": no component property — skipping).
Candidate lead, unverified
Heading carries a code-only prop that Text does not:
props.role.$extensions:
com.figma:
type: VARIANT
source:
kind: codeOnlyProp
layer: Heading role
instanceOf: "Code only props /_ / Heading role"
That prop round-trips as an ordinary VARIANT prop (its $extensions are dropped, nullable: false is added) — expected, since code-only props are not implemented (#269). Whether the code-only prop's presence interferes with property creation or binding order for text is unverified and should be the first thing checked.
Reproduce
cd workspaces/eg
specs render specs/Heading --overwrite --page <id>
specs generate --from-bridge --node <returned nodeId>
Then compare default.elements.root.content against the baseline. Repeat with specs/Text for the passing case.
Verification
Heading's default.elements.root.content round-trips as {$binding: "#/props/text"}
Text continues to round-trip its binding unchanged
- If the writer genuinely cannot attach the reference in this case, it must warn rather than silently baking a literal
Part of #281.
Found while verifying the ADR 058 wrapper re-expansion in the render path, against
workspaces/eginspecs-testing.Symptom
A collapsed-primitive component whose root
contentis bound to a prop comes back with a literal string instead of the binding:Why this is not simply "bindings on collapsed roots are unsupported"
Text— same shape, same prop name — round-trips the binding correctly:Texttype: text,originalName: Text{$binding: "#/props/text"}{$binding: "#/props/text"}✅Headingtype: text,originalName: Text{$binding: "#/props/text"}"Heading"❌So the mechanism works. Something specific to
Headingprevents the TEXT component property reference from being attached to (or read back from) the recreated leaf.What has been ruled out
anatomy.rootincluding$extensions.com.figma.originalName.root.contentin any variant; both bind only indefault.Headinghas 8 variants,Texthas 54, so variant count is not the discriminator.props.textis present as astringprop on both sides for both components.specs render specs/Headingcompletes with no⚠output at all, so the writer believes it attached the binding. CompareIcon, where the same class of failure does warn (binding "root.characters" → "name": no component property — skipping).Candidate lead, unverified
Headingcarries a code-only prop thatTextdoes not:That prop round-trips as an ordinary VARIANT prop (its
$extensionsare dropped,nullable: falseis added) — expected, since code-only props are not implemented (#269). Whether the code-only prop's presence interferes with property creation or binding order fortextis unverified and should be the first thing checked.Reproduce
Then compare
default.elements.root.contentagainst the baseline. Repeat withspecs/Textfor the passing case.Verification
Heading'sdefault.elements.root.contentround-trips as{$binding: "#/props/text"}Textcontinues to round-trip its binding unchangedPart of #281.