Subissue of #204.
ADR-066 makes format.keys reversible so a spec produced with a lossy key format can be rendered back into Figma. Today every value other than SAFE is a one-way projection: Icon leading, Icon-leading, and Icon_leading all become icon-leading under KEBAB, and Cut & paste becomes cutPaste with nothing recording that a & was ever there. Since Figma layer and property names are the identity used to match existing nodes, render has no reliable way back.
The schema side gives the engine three things to consume:
format.figmaKeys (NONE | SENTENCE | TITLE, default NONE) — the convention the Figma file uses, and the target a formatted key reverses into. NONE declares no convention: the grammar is not evaluated, no com.figma.name is emitted for format divergence, and reversal is undefined. Everything below is opt-in.
SafeKeySentence / SafeKeyTitle — the grammar for names that survive every format.keys value: ASCII letters and digits, single-space separators, each word all letters or all digits, never digit-initial, casing matching figmaKeys
$extensions['com.figma'].name on anatomy elements and props — the Figma name, emitted only when the key cannot reconstruct it
Scope
Emit side (specs-from-figma)
- Evaluate each anatomy element name and prop name against the safe key grammar for the declared
format.figmaKeys, and emit com.figma.name only when it diverges
- Read
format.figmaKeys through config resolution rather than assuming sentence case
- Keep well-formed catalogs extension-free — a name inside the grammar must emit nothing
Render side (figma-from-specs)
- Reconstruct Figma names by re-formatting the spec key into
format.figmaKeys, preferring com.figma.name whenever it is present
- Resolve reference sites (
elements, propConfigurations) through the definition — references stay in formatted key space and are not duplicated
Rename
$extensions['com.figma'].originalName is renamed to .name. figma-from-specs/src/Elements/Elements.ts:131 reads it through an inline structural type, not the published type, so the rename will not fail compilation — it will silently read undefined and fall back to 'Icon'/'Text' for collapsed roots. Update that call site explicitly.
specs-from-figma/src/Component/Anatomy/AnatomyElement.ts writes the field and will fail to compile, which is the caught case.
Round-trip check
A component whose Figma names fall outside the safe grammar — inner capitals, symbols, hyphens, digit-leading words — should survive Figma → spec → Figma with layer and property names intact, under a lossy format.keys value.
Depends on
Schema side ships in @directededges/specs-schema 0.30.0 (PR #296). Emit side is implemented on specs-from-figma branch feat/lossless-keys; plugin settings on specs-plugin-2 branch feat/lossless-keys.
Test fixtures
The parity-test workspace in specs-testing exercises every branch of the rule. Generated with format.keys: CAMEL, format.figmaKeys: SENTENCE — 948 names across 69 components, 75 divergent in 52 of them. Run specs analyze keys for the current inventory at workspaces/<workspace>/specs/_analysis/keys.yaml.
One fixture per cause
| Component |
Surface |
Figma name |
Emitted key |
Exercises |
avatar |
prop |
A11y label |
a11yLabel |
mixed letter-digit — the boundary in a11y is unrecoverable. Also the highest-volume case: 42 components |
alert |
prop |
Full Bleed |
fullBleed |
casing on a prop — Title-cased in a Sentence-case file |
toolbar |
anatomy |
ACME Toolbar / Action |
acmeToolbarAction |
symbol — / deleted along with its word boundary |
paymentCardInput |
anatomy |
ACME Asset / Mark, ACME Icon glyph |
acmeAssetMarkAcmeIconGlyph |
symbol, multiple — / and , in one name |
paymentCardInput |
anatomy |
0000 0000 0000 0000 |
0000000000000000 |
digit-initial — a name that is entirely digits |
dialog.iosPrimaryAction |
anatomy |
Label␣␣␣ |
label |
separator — trailing whitespace, invisible in the Figma UI |
textArea |
anatomy |
x-figmacollapse |
xFigmacollapse |
already-a-key — kebab name, CAMEL destination, so it reformats and records rather than being retained |
Collapsed roots — the ADR-058 path
These emit com.figma.name on the root key regardless of figmaKeys, including under the NONE default, because wrapper-collapse provenance is an independent trigger. This is the render-side hazard: figma-from-specs/src/Elements/Elements.ts:131 reads this through an inline structural type, so the originalName → name rename fails silently rather than at compile time.
| Component |
Figma name |
Collapsed leaf type |
icon |
Icon glyph |
glyph root — the icon-glyph-as-root case |
text |
Text |
text root |
body |
Text |
text root |
heading |
Text |
text root |
Shape coverage
dialog — 4 divergent names, anatomy only (no props key in the report)
badge — props only (no anatomy key)
menu, pill, rating — 3 divergent each, spanning both surfaces in one component
button and most others — zero divergence, and must stay extension-free
Config sweep
Regenerate the same catalog at each figmaKeys value and diff:
| Value |
Expected |
NONE |
4 preserved names — the collapsed roots only. No format-divergence extensions anywhere |
SENTENCE |
149 preserved names. Keys identical to the NONE run — only extensions are added, never a changed key |
TITLE |
~500 preserved names. The library is sentence case, so declaring the wrong convention floods the output — this is the "declare the right convention or drown in noise" case |
Round-trip assertions
For each fixture above, Figma → spec → Figma must return the original layer or property name, not the reversal of the key:
A11y label must come back as A11y label, never A 11 y label
ACME Toolbar / Action must come back with its / intact
Label␣␣␣ must come back with trailing spaces preserved — a renderer that trims silently renames the layer
x-figmacollapse must come back as x-figmacollapse, not X figmacollapse
icon's collapsed root must recreate a leaf named Icon glyph, not the 'Icon' fallback
Subissue of #204.
ADR-066 makes
format.keysreversible so a spec produced with a lossy key format can be rendered back into Figma. Today every value other thanSAFEis a one-way projection:Icon leading,Icon-leading, andIcon_leadingall becomeicon-leadingunderKEBAB, andCut & pastebecomescutPastewith nothing recording that a&was ever there. Since Figma layer and property names are the identity used to match existing nodes, render has no reliable way back.The schema side gives the engine three things to consume:
format.figmaKeys(NONE|SENTENCE|TITLE, defaultNONE) — the convention the Figma file uses, and the target a formatted key reverses into.NONEdeclares no convention: the grammar is not evaluated, nocom.figma.nameis emitted for format divergence, and reversal is undefined. Everything below is opt-in.SafeKeySentence/SafeKeyTitle— the grammar for names that survive everyformat.keysvalue: ASCII letters and digits, single-space separators, each word all letters or all digits, never digit-initial, casing matchingfigmaKeys$extensions['com.figma'].nameon anatomy elements and props — the Figma name, emitted only when the key cannot reconstruct itScope
Emit side (
specs-from-figma)format.figmaKeys, and emitcom.figma.nameonly when it divergesformat.figmaKeysthrough config resolution rather than assuming sentence caseRender side (
figma-from-specs)format.figmaKeys, preferringcom.figma.namewhenever it is presentelements,propConfigurations) through the definition — references stay in formatted key space and are not duplicatedRename
$extensions['com.figma'].originalNameis renamed to.name.figma-from-specs/src/Elements/Elements.ts:131reads it through an inline structural type, not the published type, so the rename will not fail compilation — it will silently readundefinedand fall back to'Icon'/'Text'for collapsed roots. Update that call site explicitly.specs-from-figma/src/Component/Anatomy/AnatomyElement.tswrites the field and will fail to compile, which is the caught case.Round-trip check
A component whose Figma names fall outside the safe grammar — inner capitals, symbols, hyphens, digit-leading words — should survive Figma → spec → Figma with layer and property names intact, under a lossy
format.keysvalue.Depends on
Schema side ships in
@directededges/specs-schema0.30.0 (PR #296). Emit side is implemented onspecs-from-figmabranchfeat/lossless-keys; plugin settings onspecs-plugin-2branchfeat/lossless-keys.Test fixtures
The parity-test workspace in
specs-testingexercises every branch of the rule. Generated withformat.keys: CAMEL,format.figmaKeys: SENTENCE— 948 names across 69 components, 75 divergent in 52 of them. Runspecs analyze keysfor the current inventory atworkspaces/<workspace>/specs/_analysis/keys.yaml.One fixture per cause
avatarA11y labela11yLabela11yis unrecoverable. Also the highest-volume case: 42 componentsalertFull BleedfullBleedtoolbarACME Toolbar / ActionacmeToolbarAction/deleted along with its word boundarypaymentCardInputACME Asset / Mark, ACME Icon glyphacmeAssetMarkAcmeIconGlyph/and,in one namepaymentCardInput0000 0000 0000 00000000000000000000dialog.iosPrimaryActionLabel␣␣␣labeltextAreax-figmacollapsexFigmacollapseCAMELdestination, so it reformats and records rather than being retainedCollapsed roots — the ADR-058 path
These emit
com.figma.nameon therootkey regardless offigmaKeys, including under theNONEdefault, because wrapper-collapse provenance is an independent trigger. This is the render-side hazard:figma-from-specs/src/Elements/Elements.ts:131reads this through an inline structural type, so theoriginalName→namerename fails silently rather than at compile time.iconIcon glyphtextTextbodyTextheadingTextShape coverage
dialog— 4 divergent names, anatomy only (nopropskey in the report)badge— props only (noanatomykey)menu,pill,rating— 3 divergent each, spanning both surfaces in one componentbuttonand most others — zero divergence, and must stay extension-freeConfig sweep
Regenerate the same catalog at each
figmaKeysvalue and diff:NONESENTENCENONErun — only extensions are added, never a changed keyTITLERound-trip assertions
For each fixture above, Figma → spec → Figma must return the original layer or property name, not the reversal of the key:
A11y labelmust come back asA11y label, neverA 11 y labelACME Toolbar / Actionmust come back with its/intactLabel␣␣␣must come back with trailing spaces preserved — a renderer that trims silently renames the layerx-figmacollapsemust come back asx-figmacollapse, notX figmacollapseicon's collapsed root must recreate a leaf namedIcon glyph, not the'Icon'fallback