Found while analysing the render round-trip corpus in specs-testing (workspace workspaces/eg, a 69-component the component library). Two symptoms that look unrelated share one root cause: Figma variant property names are flattened into a formatted key with no inverse, and the original name is never preserved.
The workspace uses format.keys: CAMEL (workspaces/eg/specs.config.yaml).
Root cause
Utilities.formatKey(str, 'CAMEL') lowercases the first word and TitleCases the rest, collapsing separators and case. It is lossy and not invertible:
"Action 1 appearance" → "action1Appearance"
"Header overlaid" → "headerOverlaid"
Nothing in the spec records the original Figma name. props.action1Appearance carries type / default / enum / nullable and no $extensions pointing back at Figma. So the render path has no way to reconstruct "Action 1 appearance" — "Action 1 Appearance" is an equally plausible inverse and still wrong.
Symptom A — render writes formatted keys back to Figma as property names
packages/figma-from-specs/src/Variants/Variants.ts:
function variantFrameName(config) {
return Object.keys(config).map(k => `${k}=${config[k]}`).join(', ');
}
The spec key goes onto the Figma variant frame verbatim, so the rendered component gets a property literally named action1Appearance instead of Action 1 appearance. Reading it back re-formats idempotently, so the round-trip looks self-consistent while diverging from the source library.
packages/figma-from-specs/src/Props/Props.ts has the same shape — owner.addComponentProperty(name, …) with the raw spec key — so this is not limited to variant properties.
Observed in round-trip diffs:
"Validation=Invalid" → "validation=Invalid"
"Platform=iOS" → "platform=iOS"
"Count=2, Action 1 appearance=Primary, Layout direction=Horizontal" → "action1Appearance=Primary, count=2, layoutDirection=Horizontal"
"Appearance=Tonal, Size=M, Disabled=False, State=Hover, Elevated=False" → "appearance=Tonal, disabled=false, elevated=False, size=M, state=Hover"
This is not cosmetic. The renaming also reorders variants, which cascades: FavoriteButton reported 13 differences that were entirely variants[0] and variants[1] swapping places, and ButtonExperimentalTonal's size ladder (XS→S→L→XL) landed one index late throughout, generating dozens of spurious dropped/added rows.
Note the asymmetry: PropConfigurations.matchFormatted and Subcomponents both thread keyFormat and format-match deliberately when reading. Variant and property creation do not.
Symptom B — instanceOf encodes a variant selection as one unparseable token
Some instanceOf values name a specific variant member of a component set rather than the set itself, with the variant selection flattened into the same camelCase token:
workspaces/eg/specs/Sheet/variants.yaml → instanceOf: ToolbarAndroidSCloseFalseBaseOverlay
workspaces/eg/specs/Alert/* → instanceOf: LinkOnOverlayMFalseRestStart
- also
ToolbarAndroidSCloseTrueBaseTertiary, and ButtonEndVisual (no component or set of that name exists; "End visual" is only a variant property value)
The Figma file has COMPONENT_SETs named Toolbar and Link, whose children are variant members named e.g. Platform=Android, Header=Toolbar, Header overlaid=True. So two distinct facts — which component, and which variant — are collapsed into one string that cannot be parsed back apart.
The correct representation already exists in the schema:
instanceOf: Toolbar
propConfigurations:
platform: Android
header: Toolbar
headerOverlaid: true
Two consequences:
Why one issue
Both symptoms are the same lossy transform applied at different points. Fixing the encoding without fixing the write direction leaves rendered components misnamed; fixing the write direction without fixing the encoding leaves instance references unresolvable. The underlying decision is the same one: how does a spec preserve the original Figma property name?
Options worth weighing before implementing:
- Carry the raw name in
$extensions.com.figma on the prop, and have render use it
- Have render reconcile against the target file's existing property names rather than writing new ones
- Emit
instanceOf + propConfigurations rather than a flattened variant-member name (needed regardless, for Symptom B)
This likely warrants an ADR rather than a direct patch — it touches the schema's contract about what a spec records.
Reach
- Symptom A: 4 of 8 cleanly-measured components showed the renaming directly; the ordering cascade was visible in most of the 13 components that completed a round-trip
- Symptom B: 3 flattened variant-member references, plus
ButtonEndVisual, out of 48 instance edges in workspaces/eg/specs/_analysis/dependencies.graph.yaml
Verification
- A rendered component set's Figma property names match the source library's (
Action 1 appearance, not action1Appearance)
detectedIn strings round-trip unchanged
- Variant order round-trips unchanged, eliminating the
dropped/added pair storms
instanceOf values resolve to a component plus an explicit propConfigurations block, and the rendered instance selects the correct variant rather than the set default
Part of #281.
Found while analysing the render round-trip corpus in
specs-testing(workspaceworkspaces/eg, a 69-component the component library). Two symptoms that look unrelated share one root cause: Figma variant property names are flattened into a formatted key with no inverse, and the original name is never preserved.The workspace uses
format.keys: CAMEL(workspaces/eg/specs.config.yaml).Root cause
Utilities.formatKey(str, 'CAMEL')lowercases the first word and TitleCases the rest, collapsing separators and case. It is lossy and not invertible:"Action 1 appearance"→"action1Appearance""Header overlaid"→"headerOverlaid"Nothing in the spec records the original Figma name.
props.action1Appearancecarriestype/default/enum/nullableand no$extensionspointing back at Figma. So the render path has no way to reconstruct"Action 1 appearance"—"Action 1 Appearance"is an equally plausible inverse and still wrong.Symptom A — render writes formatted keys back to Figma as property names
packages/figma-from-specs/src/Variants/Variants.ts:The spec key goes onto the Figma variant frame verbatim, so the rendered component gets a property literally named
action1Appearanceinstead ofAction 1 appearance. Reading it back re-formats idempotently, so the round-trip looks self-consistent while diverging from the source library.packages/figma-from-specs/src/Props/Props.tshas the same shape —owner.addComponentProperty(name, …)with the raw spec key — so this is not limited to variant properties.Observed in round-trip diffs:
"Validation=Invalid"→"validation=Invalid""Platform=iOS"→"platform=iOS""Count=2, Action 1 appearance=Primary, Layout direction=Horizontal"→"action1Appearance=Primary, count=2, layoutDirection=Horizontal""Appearance=Tonal, Size=M, Disabled=False, State=Hover, Elevated=False"→"appearance=Tonal, disabled=false, elevated=False, size=M, state=Hover"This is not cosmetic. The renaming also reorders variants, which cascades:
FavoriteButtonreported 13 differences that were entirelyvariants[0]andvariants[1]swapping places, andButtonExperimentalTonal's size ladder (XS→S→L→XL) landed one index late throughout, generating dozens of spuriousdropped/addedrows.Note the asymmetry:
PropConfigurations.matchFormattedandSubcomponentsboth threadkeyFormatand format-match deliberately when reading. Variant and property creation do not.Symptom B —
instanceOfencodes a variant selection as one unparseable tokenSome
instanceOfvalues name a specific variant member of a component set rather than the set itself, with the variant selection flattened into the same camelCase token:workspaces/eg/specs/Sheet/variants.yaml→instanceOf: ToolbarAndroidSCloseFalseBaseOverlayworkspaces/eg/specs/Alert/*→instanceOf: LinkOnOverlayMFalseRestStartToolbarAndroidSCloseTrueBaseTertiary, andButtonEndVisual(no component or set of that name exists; "End visual" is only a variant property value)The Figma file has
COMPONENT_SETs namedToolbarandLink, whose children are variant members named e.g.Platform=Android, Header=Toolbar, Header overlaid=True. So two distinct facts — which component, and which variant — are collapsed into one string that cannot be parsed back apart.The correct representation already exists in the schema:
Two consequences:
specs analyze dependenciesreports themexternal: trueand the render path skips the instance (see Render: instance elements are skipped when the subcomponent is missing from the instance manifest #276)InstanceElement.resolveEntryimports the set and takes.defaultVariant, so the variant selection would be dropped anywayWhy one issue
Both symptoms are the same lossy transform applied at different points. Fixing the encoding without fixing the write direction leaves rendered components misnamed; fixing the write direction without fixing the encoding leaves instance references unresolvable. The underlying decision is the same one: how does a spec preserve the original Figma property name?
Options worth weighing before implementing:
$extensions.com.figmaon the prop, and have render use itinstanceOf+propConfigurationsrather than a flattened variant-member name (needed regardless, for Symptom B)This likely warrants an ADR rather than a direct patch — it touches the schema's contract about what a spec records.
Reach
ButtonEndVisual, out of 48instanceedges inworkspaces/eg/specs/_analysis/dependencies.graph.yamlVerification
Action 1 appearance, notaction1Appearance)detectedInstrings round-trip unchangeddropped/addedpair stormsinstanceOfvalues resolve to a component plus an explicitpropConfigurationsblock, and the rendered instance selects the correct variant rather than the set defaultPart of #281.