ADR
Consolidate Token Format Configuration into `tokens`
Context
Config.format currently carries three separate fields that together govern how resolved token references are serialized in output:
variables: 'NAME_WITH_COLLECTION' | 'NAME' | 'OBJECT'— controls the naming shape of variable referencessimplifyVariables: boolean— whether variable reference objects are reduced to a name stringsimplifyStyles: boolean— whether style reference objects are reduced to a name string
# Current shape — types/Config.ts → format objectvariables: NAME_WITH_COLLECTION # e.g. "DS Color/Text/Primary"simplifyVariables: true # emit name string instead of full VariableStyle objectsimplifyStyles: true # emit name string instead of full FigmaStyle objectThese three fields interact implicitly: variables only applies when simplifyVariables is false, and simplifyStyles shares no coupling to variables at all. Consumers must read all three to determine the actual output shape, and many combinations are semantically undefined (e.g., variables: 'OBJECT' with simplifyVariables: true).
ADR 006 (006-token-references) is replacing VariableStyle and FigmaStyle with a single TokenReference type aligned to the W3C Design Tokens Community Group (DTCG) format. That change introduces a new output surface — the TokenReference object — with five meaningfully distinct serialization profiles:
- Token object (
$tokenpath and$type— platform-neutral, no tool metadata) - Token name string (token path only, no wrapper object)
- Token object with Figma extensions (
$token,$type, plus$extensions."com.figma"metadata) - Figma name string (variable or style name in Figma-native slash-delimited format)
- Custom (enterprise-defined projection, transformer-owned)
Three boolean/enum fields cannot cleanly express five mutually exclusive profiles. A single discriminated enum replaces the implicit interaction surface with a single explicit configuration axis.
Decision Drivers
- Type–schema sync: Removing
variables,simplifyVariables, andsimplifyStylesand addingtokensrequires symmetric changes in bothtypes/Config.tsandschema/component.schema.jsonbefore publish (Constitution I). - No runtime logic: This package declares shapes only. The
tokensenum names the profile; the transformer applies it (Constitution II). - MAJOR for breaking changes: Removing three required fields from
Config.formatbreaks every consumer that constructs or pattern-matches aConfigobject. A MAJOR bump is required (Constitution III). - Minimal, intentional API surface: Five named profiles are fewer moving parts than three interacting fields. The enum surface is smaller and produces no undefined cross-product (Constitution III).
- Optionality over prescription:
DEFAULT_CONFIGexists to provide well-reasoned defaults for every field. A field with a documented default should be optional in the type — omitting it is equivalent to stating the default. Requiring fields whose absence has a clear meaning imposes unnecessary ceremony on callers.tokensshould be optional; its absence meansTOKEN. This raises a broader question — whether allformatfields should be optional for the same reason — that is noted in Consequences but scoped out of this ADR. - DTCG alignment: Output should be forward-compatible with the DTCG Candidate Recommendation. The
TOKENprofile is a first-class option whose$token/$typeshape follows DTCG conventions, not a post-processing concern. - Enterprise extensibility: Enterprise consumers may require token output in bespoke formats (e.g., a proprietary token registry schema). The configuration surface must be able to signal “use an externally-supplied format”, with the transformer responsible for its implementation. This package declares the signal only; no format logic is permitted here (Constitution II).
- Stability of
DEFAULT_CONFIG:tokensdefaults toTOKENto align with ADR 006’s direction.
Options Considered
Option A: Replace variables, simplifyVariables, simplifyStyles with a single optional tokens field, including CUSTOM (Selected)
Remove all three fields. Add tokens?: 'TOKEN' | 'TOKEN_NAME' | 'TOKEN_FIGMA_EXTENSIONS' | 'FIGMA_NAME' | 'CUSTOM' as an optional field in Config.format. The CUSTOM value is a reserved signal; its output projection is fully defined by the transformer receiving it. DEFAULT_CONFIG provides 'TOKEN' as the default.
Pros:
- Eliminates the undefined cross-product (e.g.,
variables: 'OBJECT'+simplifyVariables: true) - Single field is unambiguous — one read gives the full output contract
- Mirrors the five real output profiles introduced by
TokenReferencein ADR 006 - Making
tokensoptional reduces ceremony for callers;DEFAULT_CONFIGdocuments the intent - Reduces
required[]count in schema from 6 to 3 (net -3;tokensmoves to optional)
Cons / Trade-offs:
- Breaking change: all consumers that set any of the removed fields must update (MAJOR bump required)
- Loses the historical
NAMEoption (no-collection variable name); subsumed byTOKEN_NAME CUSTOMis open-ended by design; this package cannot validate that a transformer correctly implements it
Option D: Allow CUSTOM as a fifth enum value for enterprise-defined output profiles (Evaluated — included in Option A)
An enterprise integrator may need to emit token references in a format that matches a proprietary token registry or build pipeline schema that is neither DTCG nor Figma-shaped. A fifth value CUSTOM in the tokens enum signals this intent to the transformer without requiring this package to define the format.
How CUSTOM interacts with TokenReference (ADR 006):
TokenReference carries an open $extensions block (additionalProperties: true) as defined in ADR 006. The $extensions key is designed for reverse-domain-name namespaced tool metadata (e.g., "com.figma", per DTCG §5.2.3). A custom format implementation in the transformer may:
- Populate a new
$extensionsnamespace (e.g.,"com.enterprise.studio") and project the fullTokenReferenceobject through a custom mapping - Emit the
$tokendot-path under a different key - Collapse the reference to a bespoke scalar — treating
TokenReferenceas an intermediate representation that the transformer projects into the enterprise shape
In all cases, TokenReference remains the canonical typed shape produced by the transformer’s resolution stage. CUSTOM is purely a serialization profile instruction applied at the output stage. This separation preserves Constitution II: no logic ships in types/; the enum value is a pure declaration.
Pros:
- Explicit, enumerable signal — parseable by tooling, documentable, and visible in
anova-kitconfig scaffolding - Preserves
TokenReferenceas the canonical intermediate; only the final projection is custom - Open
$extensionsonTokenReferencealready provides the envelope for custom namespace metadata without a type change
Cons / Trade-offs:
CUSTOMis a semantic placeholder: this package cannot validate correct transformer behavior for it- Enterprises using
CUSTOMimplicitly depend on transformer-side configuration not tracked here; the config is incomplete without it
Disposition: Included in Option A. CUSTOM is a fifth enum value in the tokens field; Option A’s enum is extended rather than treated as a separate decision branch.
Option E: Limit the core tokens enum to platform-neutral profiles; treat TOKEN_FIGMA_EXTENSIONS/FIGMA_NAME as tool-specific extensions (Rejected)
The anova package is moving toward platform-independent output per ADR 006. One interpretation of that direction is that the core tokens enum should contain only platform-neutral values — TOKEN, TOKEN_NAME, CUSTOM — and that Figma-specific output formats should be handled as a tool extension outside this schema (e.g., declared in anova-transformer’s own config layer).
Evaluation:
anova’s primary intent is to be a source-of-truth independent of any design tool. It emerged from Figma conversion, but the schema exists to describe UI component structure as a stand-alone contract — a future or enterprise implementation that does not use Figma at all should be able to consume and produce valid anova output using only TOKEN profiles. Figma is not intrinsic to the schema; it is the current dominant source.
That said, TOKEN_FIGMA_EXTENSIONS and FIGMA_NAME are not in the enum because Figma is the source — they are there because Figma-native tooling is a real, durable consumer category. Consumers whose entire pipeline is Figma-native (e.g., passing output directly to a Figma REST API or a Figma plugin) have a legitimate need for a named, documented profile that emits token references in the shape those tools expect. That need does not disappear as anova matures toward platform independence; it represents a named output surface that exists alongside TOKEN, not instead of it.
The question for Option E is not “is Figma first-class to anova?” but “should named profiles for real consumer categories live in the shared config contract, or be delegated to CUSTOM?” Delegating them to CUSTOM would make anova-kit scaffolding unable to enumerate or describe these profiles without out-of-band transformer knowledge — fragmenting the config contract in a way that would affect every Figma-native consumer.
Rejected because:
TOKEN_FIGMA_EXTENSIONS/FIGMA_NAMErepresent a real, named consumer category (Figma-native tooling) that warrants a first-class config value, regardless of whether Figma is the source tool- Moving them into
CUSTOMwould fragment the config contract:anova-kitcannot scaffold or documentCUSTOMprofiles without out-of-band transformer knowledge, breaking the principle thatConfigis self-describing (Constitution I) - The platform-independent intent of anova is served by
TOKENbeing the default and the canonical path forward; Figma profiles are an additive, named escape for Figma-native consumers — not a constraint on anova’s independence
Option B: Keep existing three fields, add tokens as an additional optional field (Rejected)
Retain variables, simplifyVariables, simplifyStyles alongside a new optional tokens hint.
Rejected because: Two overlapping configuration axes for the same concern violates the minimal-API principle (Constitution III). Consumers would face an ambiguous precedence question when both old and new fields are present. The undefined cross-product is preserved, not resolved.
Option C: Remove only the simplifyVariables / simplifyStyles booleans, extend variables enum (Rejected)
Keep variables and extend its enum to include TOKEN, TOKEN_NAME, TOKEN_FIGMA_EXTENSIONS, FIGMA_NAME. Drop the two boolean flags.
Rejected because: The field name variables implies it only configures variable references. Style references (FigmaStyle / TokenReference from named styles) are a distinct surface governed by the same profile decision. A field named variables owning both is a misleading public API (Constitution III).
Decision
Remove variables, simplifyVariables, and simplifyStyles from Config.format. Add an optional tokens field with DEFAULT_CONFIG providing 'TOKEN'.
Type changes (types/)
| File | Change | Bump |
|---|---|---|
Config.ts | Removed field format.variables | MAJOR |
Config.ts | Removed field format.simplifyVariables | MAJOR |
Config.ts | Removed field format.simplifyStyles | MAJOR |
Config.ts | Added optional field format.tokens?: 'TOKEN' | 'TOKEN_NAME' | 'TOKEN_FIGMA_EXTENSIONS' | 'FIGMA_NAME' | 'CUSTOM' | MINOR |
Config.ts | Updated DEFAULT_CONFIG.format.tokens to 'TOKEN' | MINOR |
Example — new shape (types/Config.ts):
# Beforeformat: output: JSON | YAML keys: SAFE | CAMEL | SNAKE | KEBAB | PASCAL | TRAIN layout: LAYOUT | PARENT_CHILDREN | BOTH variables: NAME_WITH_COLLECTION | NAME | OBJECT # removed simplifyVariables: boolean # removed simplifyStyles: boolean # removed
# Afterformat: output: JSON | YAML keys: SAFE | CAMEL | SNAKE | KEBAB | PASCAL | TRAIN layout: LAYOUT | PARENT_CHILDREN | BOTH tokens?: TOKEN | TOKEN_NAME | TOKEN_FIGMA_EXTENSIONS | FIGMA_NAME | CUSTOM # optional; default TOKENProfile semantics (informational — transformer applies these):
| Value | Token reference output shape |
|---|---|
TOKEN | TokenReference object with $token (dot-delimited path) and $type. No tool-specific metadata. Figma slash separators (/) are transformed to periods (.) per DTCG §6.7.2. |
TOKEN_NAME | Token path string only (the $token value without the wrapper object), dot-delimited per DTCG §6.7.2. Figma "DS Color/Text/Primary" → "DS Color.Text.Primary". |
TOKEN_FIGMA_EXTENSIONS | Same TokenReference object as TOKEN, plus $extensions: { "com.figma": { id, name, collectionName?, rawValue? } }. Preserves Figma extraction provenance for consumers that need source-tool traceability. |
FIGMA_NAME | Figma variable or style name as a slash-delimited string in Figma-native format. Variables include collection: "DS Color/Text/Primary". Named styles use their Figma name: "Shadow/Medium". |
CUSTOM | Defined entirely by the transformer; TokenReference is the canonical intermediate. Custom projection may populate a new $extensions namespace, remap $token, or collapse to an enterprise scalar. |
Output examples by profile and type
Each example shows how a token reference appears in the component spec output for representative property types. Inline composites (e.g., Shadow, Typography) contain leaf-level token references; composite-level references replace the entire inline object with a single reference.
TOKEN (default) — { $token, $type }:
# color — backgroundColorbackgroundColor: $token: "DS Color.Text.Primary" $type: color
# gradient — whole gradient referenced by named stylebackgroundColor: $token: "DS Gradient.Brand.Hero" $type: gradient
# gradient stop color — leaf reference inside inline gradientbackgroundColor: type: LINEAR angle: 180 stops: - position: 0 color: $token: "DS Color.Brand.Start" $type: color - position: 1 color: $token: "DS Color.Brand.End" $type: color
# shadow — composite reference (replaces entire Effects object)effects: $token: "DS Effects.Shadow.Medium" $type: shadow
# shadow — leaf references inside inline Shadoweffects: shadows: - visible: true offsetX: 0 offsetY: $token: "DS Number.Shadow.OffsetY" $type: number blur: $token: "DS Number.Shadow.Blur" $type: number spread: 0 color: $token: "DS Color.Shadow.Default" $type: color
# typography — composite reference (replaces entire Typography object)typography: $token: "DS Type.Body.Medium" $type: typography
# typography — leaf references inside inline Typographytypography: fontSize: $token: "DS Number.Font.Size.MD" $type: number lineHeight: $token: "DS Number.Line.Height.Normal" $type: number fontFamily: "Inter" fontStyle: 400TOKEN_NAME — dot-delimited $token path string only:
# colorbackgroundColor: "DS Color.Text.Primary"
# gradient (composite)backgroundColor: "DS Gradient.Brand.Hero"
# gradient stop color (leaf)backgroundColor: type: LINEAR angle: 180 stops: - position: 0 color: "DS Color.Brand.Start" - position: 1 color: "DS Color.Brand.End"
# shadow (composite)effects: "DS Effects.Shadow.Medium"
# shadow (leaf)effects: shadows: - visible: true offsetX: 0 offsetY: "DS Number.Shadow.OffsetY" blur: "DS Number.Shadow.Blur" spread: 0 color: "DS Color.Shadow.Default"
# typography (composite)typography: "DS Type.Body.Medium"
# typography (leaf)typography: fontSize: "DS Number.Font.Size.MD" lineHeight: "DS Number.Line.Height.Normal" fontFamily: "Inter" fontStyle: 400TOKEN_FIGMA_EXTENSIONS — { $token, $type, $extensions }:
# colorbackgroundColor: $token: "DS Color.Text.Primary" $type: color $extensions: com.figma: id: "VariableID:123:456" name: "Text/Primary" collectionName: "DS Color"
# gradient (composite — named style, no collectionName)backgroundColor: $token: "DS Gradient.Brand.Hero" $type: gradient $extensions: com.figma: id: "S:abc123" name: "Brand/Hero"
# shadow (composite — named style)effects: $token: "DS Effects.Shadow.Medium" $type: shadow $extensions: com.figma: id: "S:def456" name: "Shadow/Medium"
# shadow (leaf — variable reference with rawValue)effects: shadows: - visible: true offsetX: 0 offsetY: $token: "DS Number.Shadow.OffsetY" $type: number $extensions: com.figma: id: "VariableID:789:012" name: "Shadow/OffsetY" collectionName: "DS Number" rawValue: 4 blur: $token: "DS Number.Shadow.Blur" $type: number $extensions: com.figma: id: "VariableID:789:013" name: "Shadow/Blur" collectionName: "DS Number" rawValue: 8 spread: 0 color: $token: "DS Color.Shadow.Default" $type: color $extensions: com.figma: id: "VariableID:123:789" name: "Shadow/Default" collectionName: "DS Color"
# typography (composite — named style)typography: $token: "DS Type.Body.Medium" $type: typography $extensions: com.figma: id: "S:typ789" name: "Body/Medium"
# typography (leaf)typography: fontSize: $token: "DS Number.Font.Size.MD" $type: number $extensions: com.figma: id: "VariableID:456:001" name: "Font/Size/MD" collectionName: "DS Number" rawValue: 16 lineHeight: $token: "DS Number.Line.Height.Normal" $type: number $extensions: com.figma: id: "VariableID:456:002" name: "Line/Height/Normal" collectionName: "DS Number" rawValue: 1.5 fontFamily: "Inter" fontStyle: 400FIGMA_NAME — Figma-native slash-delimited name string:
# color (variable — includes collection prefix)backgroundColor: "DS Color/Text/Primary"
# gradient (named style — style name only)backgroundColor: "Brand/Hero"
# gradient stop color (leaf variable)backgroundColor: type: LINEAR angle: 180 stops: - position: 0 color: "DS Color/Brand/Start" - position: 1 color: "DS Color/Brand/End"
# shadow (named style)effects: "Shadow/Medium"
# shadow (leaf variables)effects: shadows: - visible: true offsetX: 0 offsetY: "DS Number/Shadow/OffsetY" blur: "DS Number/Shadow/Blur" spread: 0 color: "DS Color/Shadow/Default"
# typography (named style)typography: "Body/Medium"
# typography (leaf variables)typography: fontSize: "DS Number/Font/Size/MD" lineHeight: "DS Number/Line/Height/Normal" fontFamily: "Inter" fontStyle: 400Updated DEFAULT_CONFIG (types/Config.ts):
# Beforeformat: output: JSON keys: SAFE layout: LAYOUT variables: NAME_WITH_COLLECTION simplifyVariables: true simplifyStyles: true
# Afterformat: output: JSON keys: SAFE layout: LAYOUT tokens: TOKENSchema changes (schema/)
| File | Change | Bump |
|---|---|---|
component.schema.json | Removed property format.variables from #/definitions/Config/properties/format/properties | MAJOR |
component.schema.json | Removed property format.simplifyVariables from same | MAJOR |
component.schema.json | Removed property format.simplifyStyles from same | MAJOR |
component.schema.json | Added property format.tokens with enum ["TOKEN", "TOKEN_NAME", "TOKEN_FIGMA_EXTENSIONS", "FIGMA_NAME", "CUSTOM"] and default: "TOKEN" | MINOR |
component.schema.json | Updated format.required[] to ["output", "keys", "layout"] — tokens is not required | MAJOR |
styles.schema.json | Updated description referencing simplifyVariables / simplifyStyles config fields | PATCH |
Example — new shape (schema/component.schema.json, format object):
# Before — format.propertiesvariables: type: string enum: [NAME_WITH_COLLECTION, NAME, OBJECT]simplifyVariables: type: booleansimplifyStyles: type: boolean# required: [output, keys, layout, variables, simplifyVariables, simplifyStyles]
# After — format.propertiestokens: type: string enum: [TOKEN, TOKEN_NAME, TOKEN_FIGMA_EXTENSIONS, FIGMA_NAME, CUSTOM] default: "TOKEN" description: "Token reference serialization profile. Optional; defaults to TOKEN. CUSTOM delegates projection entirely to the transformer."# required: [output, keys, layout] ← tokens omitted; it has a schema defaultNotes
TOKENis the default.tokensis optional; a config object that omits it is valid and behaves identically totokens: 'TOKEN'. This is the firstformatfield to be optional — the same rationale (each field has a well-defined default inDEFAULT_CONFIG) applies tooutput,keys, andlayoutas well, but making those optional is scoped out of this ADR.TOKENandTOKEN_NAMEare platform-neutral — they carry no Figma-specific metadata.TOKEN_FIGMA_EXTENSIONSadds Figma provenance ($extensions."com.figma") for consumers that need source-tool traceability without abandoning theTokenReferenceshape.FIGMA_NAMEemits the Figma-native slash-delimited name string. For variables this includes the collection prefix (e.g.,"DS Color/Text/Primary"); for named styles it is the style name only (e.g.,"Body/Medium"). This profile serves consumers whose downstream tooling is Figma-native.FIGMA_NAMEandTOKEN_FIGMA_EXTENSIONSare not a statement that anova is Figma-dependent — anova’s primary intent is a platform-independent source of truth, withTOKENas the canonical default. Figma profiles are additive named escapes for Figma-native consumers that coexist with, and do not constrain, that independence.- The prior
variables: 'NAME'option (name without collection) has no directtokensequivalent. It is subsumed byTOKEN_NAME. Consumers that relied on'NAME'must migrate. CUSTOMis a first-class enum member, not a string escape hatch. It is statically typed; the transformer branches on it explicitly. TheTokenReferenceshape produced by ADR 006 is the intermediate representation in all cases —CUSTOMonly affects how that representation is projected into final output. The open$extensionsblock onTokenReference(per ADR 006) provides the envelope for any new tool-namespace metadata a custom projection needs to attach.
Schema scope: why the non-TOKEN profile shapes are not defined in styles.schema.json
component.schema.json defines format.tokens as a string enum — purely config input, not output shape. The output contract lives in styles.schema.json, and the profiles map to it as follows:
| Profile | Output shape | Schema coverage |
|---|---|---|
TOKEN | TokenReference object: { $token, $type } | Covered by #/definitions/TokenReference (ADR 006) |
TOKEN_NAME | Plain string: "DS Color.Text.Primary" | Covered by existing string arm in Style/ColorStyle oneOf; indistinguishable from a literal string — accepted |
TOKEN_FIGMA_EXTENSIONS | TokenReference object with $extensions."com.figma" | Covered by #/definitions/TokenReference — $extensions is additionalProperties: true (ADR 006) |
FIGMA_NAME | Plain string: "DS Color/Text/Primary" | Same as TOKEN_NAME — covered by string arm |
CUSTOM | Transformer-defined | Not schema-validated by design |
TOKEN_NAME / FIGMA_NAME and the string arm: These profiles collapse TokenReference to a plain string. They already parse against the string arm of Style’s oneOf, so no additional schema work is required. The schema cannot distinguish a token path from a literal string value — this ambiguity is intentional and mirrors how the current simplifyVariables: true output behaves today.
TOKEN_FIGMA_EXTENSIONS and TokenReference: This profile emits the same TokenReference shape as TOKEN, with a populated $extensions."com.figma" block. Because $extensions is declared with additionalProperties: true in ADR 006, the Figma metadata passes schema validation without any new definitions. The com.figma namespace content (id, name, collectionName?, rawValue?) is not individually schema-validated — its shape is owned by the Figma API surface, not this package.
Type ↔ Schema Impact
- Symmetric: Yes
- Parity check:
Config.format.tokens?(optional union string literal, five members) ↔format.properties.tokensincomponent.schema.json(string enum withdefault: "TOKEN", absent fromrequired[])- Removed
variables,simplifyVariables,simplifyStylesfrom both type and schema simultaneously styles.schema.jsondescription update is documentation-only (PATCH); no new output shape definitions are added — see Schema scope note above
Downstream Impact
| Consumer | Impact | Action required |
|---|---|---|
anova-kit | Breaking — Config type consumed by CLI init command and config parsing utilities | Replace variables, simplifyVariables, simplifyStyles config fields with tokens in CLI config reading, defaults, and any documentation |
Semver Decision
MAJOR — Three required fields are removed from a published type and its corresponding schema. The addition of tokens as an optional field is additive (MINOR), but it does not offset the MAJOR character of the removals. Per Constitution III, removing named fields from a type is a breaking change regardless of whether new fields are added in the same commit. The version bump from 0.11.0 targets the next appropriate boundary; given the pre-1.0 position of the package and the bundling intent with v0.11.0, this change is expected to ship as part of the 0.11.0 breaking release bundle alongside ADR 006.
Consequences
Config.formatdrops from six required fields to three (output,keys,layout) —tokensis optional with a documented default.- The undefined cross-product of
variables×simplifyVariables×simplifyStylesis eliminated. - Token reference output format is a single named concept in both code and documentation.
anova-transformercan switch on one field (format.tokens) rather than branching across three.anova-kitCLI must update its config schema, default generation, andinitscaffolding.- Consumers using
variables: 'NAME'(name without collection) have no direct equivalent and must evaluateTOKEN_NAMEorFIGMA_NAMEas the closest substitutes. DEFAULT_CONFIGchanges fromsimplifyVariables: true+simplifyStyles: true+variables: 'NAME_WITH_COLLECTION'totokens: 'TOKEN'.CUSTOMproduces schema-unvalidated output by design — it is enterprise-owned. It does not warrant new definitions instyles.schema.json.FIGMA_NAMEandTOKEN_FIGMA_EXTENSIONSare long-lived first-class profiles for Figma-native consumers. They are additive named escapes alongsideTOKEN— not a constraint on anova’s platform-independent intent.CUSTOMenables enterprise consumers to define bespoke token output formats inanova-transformerwithout forking this package’s type contract. Thetokensfield signals the intent;TokenReference(ADR 006) is always the canonical intermediate shape the transformer receives before applying the custom projection.- Making
tokensoptional exposes a design question about the remainingformatfields (output,keys,layout): each also has a well-defined default inDEFAULT_CONFIGand is equally a candidate for optionality. That broader refactor is out of scope for this ADR and should be tracked as a follow-up MINOR change.