Skip to content

Round trip: preserve Figma names through lossy key formats (ADR-066) #298

Description

@nathanacurtis

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 originalNamename 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    generatorspecs-from-figma processing engine

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions