ADR
Remove variantNames, add emptyVariants, make Config.include fields optional
Context
The anova-plugin UI exposes an “Include layered variants without elements” checkbox setting (DATA_EMPTY_VARIANTS in src/UI/App/Settings/VariantsChildren.ts), but toggling it has no effect on the transformer output. The setting is purely cosmetic — it persists in localStorage but is never passed through to the transformer pipeline.
Currently, the Config.include type in types/Config.ts defines three required boolean fields:
include: { variantNames: boolean; invalidVariants: boolean; invalidCombinations: boolean;}There is no emptyVariants field (or equivalent) for the transformer to read. Additionally:
- Missing field: The
emptyVariantssetting cannot be mapped to the config - Dead feature:
variantNamesis no longer used by any consumer and serves no purpose in current output - Inconsistent pattern: All three fields are required, which deviates from the broader Config pattern where most fields are optional with defaults specified in
DEFAULT_CONFIG
This ADR addresses all three issues by:
- Removing the unused
variantNamesfield (breaking change) - Adding the missing
emptyVariantsfield (additive) - Making remaining
includefields optional for consistency (non-breaking)
Decision Drivers
- Remove dead code:
variantNamesserves no purpose and should be removed to reduce API surface - Accept breaking change: Removing a field requires MAJOR version bump per Constitution III
- Add missing functionality:
emptyVariantsneeded to support plugin UI and variant filtering - Type ↔ schema symmetry: Both
types/Config.tsandschema/component.schema.jsonmust be updated in lockstep per Constitution I - No runtime logic: This package defines the contract only; filtering logic belongs in
anova-transformerper Constitution II - Explicit defaults:
DEFAULT_CONFIGmust specify default values to ensure consistent behavior - Consistency with Config pattern: Optional fields with defaults are the established norm (e.g.,
tokens?,inferNumberProps?,slotConstraints?) - Internal consistency: The
includesection should follow a uniform pattern — all fields optional with defaults
Options Considered
Option A: Remove variantNames, add emptyVariants, make remaining fields optional (Selected)
Remove the unused variantNames field entirely, add new field emptyVariants?: boolean, and make remaining fields (invalidVariants, invalidCombinations) optional. All fields default to their current DEFAULT_CONFIG values when absent.
Pros:
- Removes dead code —
variantNamesis not used by any consumer - Reduces API surface and cognitive load
- Adds needed
emptyVariantsfunctionality - Creates consistency within the
includesection — all remaining fields follow the same optional pattern - Follows established Config pattern of optional fields with sensible defaults
- Serialized configs can omit fields when using default behavior, reducing output size
- Forces consumers to explicitly handle the breaking change, ensuring they’re aware of the removal
Cons / Trade-offs:
- Breaking change — consumers using
variantNamesmust update (but since it’s unused, impact should be minimal) - Requires MAJOR version bump
- Consumers must remove
variantNamesfrom their config construction code
Option B: Keep variantNames but make it optional (Rejected)
Make variantNames optional instead of removing it, treating it as deprecated.
Rejected because: Keeping unused fields creates technical debt and API clutter. If the field serves no purpose, it should be removed rather than deprecated. A MAJOR bump is acceptable since we’re already in v0.x where breaking changes are expected.
Option C: Add Config.processing.filterEmptyVariants: boolean (Rejected)
Place the field under processing rather than include, since it affects the processing pipeline.
Rejected because: The field controls what to include in output, not how to process input. It parallels invalidVariants and invalidCombinations, both of which live in include. Separating conceptually related fields undermines discoverability and consistency.
Decision
Type changes (types/)
| File | Change | Bump |
|---|---|---|
Config.ts | Remove variantNames field from Config.include interface | MAJOR |
Config.ts | Make invalidVariants, invalidCombinations optional in Config.include interface | MINOR |
Config.ts | Add new optional field emptyVariants?: boolean to Config.include interface | MINOR |
Config.ts | Remove variantNames: false from DEFAULT_CONFIG.include | MAJOR |
Config.ts | Add emptyVariants: false to DEFAULT_CONFIG.include | MINOR |
Example — new shape (types/Config.ts):
// Beforeinclude: { variantNames: boolean; invalidVariants: boolean; invalidCombinations: boolean;}
// Afterinclude: { // variantNames removed — breaking change invalidVariants?: boolean; invalidCombinations?: boolean; emptyVariants?: boolean; // NEW}Default values (DEFAULT_CONFIG):
// Beforeinclude: { variantNames: false, invalidVariants: false, invalidCombinations: true,}
// Afterinclude: { // variantNames removed invalidVariants: false, invalidCombinations: true, emptyVariants: false, // NEW}Rationale for emptyVariants default: Setting emptyVariants: false (exclude empty variants) aligns with the principle of minimal output — consumers typically want only semantically meaningful variants. Users can opt in to including empty variants when debugging or analyzing variant coverage. This matches the plugin’s current default behavior.
Schema changes (schema/)
| File | Change | Bump |
|---|---|---|
component.schema.json | Remove variantNames property from #/definitions/Config/properties/include/properties | MAJOR |
component.schema.json | Remove all fields from #/definitions/Config/properties/include/required array (make remaining fields optional) | MINOR |
component.schema.json | Add optional emptyVariants property to #/definitions/Config/properties/include/properties | MINOR |
Example — new shape (schema/component.schema.json):
// Before"include": { "type": "object", "properties": { "variantNames": { "type": "boolean" }, "invalidVariants": { "type": "boolean" }, "invalidCombinations": { "type": "boolean" } }, "required": [ "variantNames", "invalidVariants", "invalidCombinations" ], "additionalProperties": false}
// After"include": { "type": "object", "properties": { // "variantNames" removed — breaking change "invalidVariants": { "type": "boolean", "description": "Include invalid variants. Defaults to false when absent." }, "invalidCombinations": { "type": "boolean", "description": "Include invalid combinations. Defaults to true when absent." }, "emptyVariants": { "type": "boolean", "description": "When false, exclude layered variants that contain no elements from output. When true, include all variants regardless of element presence. Defaults to false when absent." } }, "required": [], // All fields now optional "additionalProperties": false}Notes
- Scope expansion: This ADR initially focused on adding
emptyVariants, but was expanded to:- Remove the unused
variantNamesfield (breaking change) - Make remaining
includefields optional for consistency with the broader Config pattern
- Remove the unused
variantNamesremoval rationale: The field is not used by any consumer and serves no purpose in current output. Removing it reduces API surface and eliminates dead code.- All remaining
includefields follow the same pattern: optional in the type/schema, explicit defaults inDEFAULT_CONFIG - The field name
emptyVariantsfollows the “include” semantics:false= exclude empty variants from output,true= include them - “Empty variant” is defined as: a layered variant (when
processing.details = 'LAYERED') that contains no elements in its diff from the default variant - The filtering logic implementation is deferred to
anova-transformer(separate issue) - The plugin UI mapping is deferred to
anova-plugin(separate issue)
Type ↔ Schema Impact
- Symmetric: Yes
- Parity check:
variantNamesremoved from both TypeScript type and JSON schemaemptyVariantsadded to both: TypeScript as optional field, JSON schema as optional property (not inrequiredarray)invalidVariantsandinvalidCombinationschanged to optional in both TypeScript type and JSON schema- Both maintain
additionalProperties: falseto prevent drift - Field count: 3 → 3 (removed 1, added 1, kept 2)
Downstream Impact
| Consumer | Impact | Action required |
|---|---|---|
anova-kit | Breaking: Must remove variantNames from CLI; must add emptyVariants support | Remove variantNames references; add emptyVariants flag to CLI options |
anova-transformer | Breaking: Must remove variantNames handling; must implement emptyVariants filtering | Remove dead variantNames code; implement empty variant filtering logic |
anova-plugin | Breaking: Must remove variantNames from config mapping; must wire emptyVariants to UI | Remove variantNames from settingsToModelConfig(); map DATA_EMPTY_VARIANTS to config.include.emptyVariants |
Semver Decision
Version bump: [CURRENT] → [NEXT MAJOR] (MAJOR)
Justification: Per Constitution III, removing a field from an exported type is a breaking change and requires a MAJOR version bump.
Breaking changes:
- Removing
variantNamesfrom the TypeScript type breaks any consumer code that references it - Removing
variantNamesfrom the schema breaks validation for any serialized specs that include it
Non-breaking changes (would be MINOR on their own):
- Adding
emptyVariantsas optional field is additive - Making
invalidVariantsandinvalidCombinationsoptional maintains backward compatibility
Impact analysis:
- Consumers that reference
config.include.variantNameswill get TypeScript compilation errors (caught at build time) - Consumers that construct
Configobjects withvariantNameswill get TypeScript errors - Existing serialized specs with
variantNameswill fail schema validation until regenerated - Since
variantNameswas not used in practice, real-world impact should be minimal
Consequences
Positive
- Dead code removed:
variantNameseliminated from the API, reducing surface area and cognitive load - Consistent pattern: All remaining
Config.includefields follow the optional-with-defaults pattern - New functionality:
emptyVariantsenables filtering of layered variants without elements - Smaller output: Serialized configs can omit
includefields when using default values - Clear example:
Config.includedemonstrates the optional-with-defaults pattern for future Config additions
Breaking changes
- Compilation errors: Consumers referencing
config.include.variantNameswill get TypeScript errors (caught at build time) - Schema validation: Existing serialized specs containing
variantNameswill fail validation until regenerated - Code updates required: Consumers must remove
variantNamesfrom config construction code
Downstream work
anova-transformer: Must remove anyvariantNameshandling logic and implementemptyVariantsfilteringanova-plugin: Must removevariantNamesfrom config mapping and wireemptyVariantsto UIanova-kit: Must removevariantNamesfrom CLI options and handleemptyVariants
Migration path
- Consumers should search codebase for
variantNamesreferences and remove them - Regenerate any cached/serialized specs to remove
variantNamesand conform to new schema - No runtime fallback needed since field was unused