ADR
Subcomponent `$ref` for `instanceOf`
Context
When a component contains subcomponents, anatomy items and elements that are instances of those subcomponents currently record instanceOf as a plain formatted string (e.g., "egdsRadioButtonFormLabel"). This is opaque — a consumer cannot distinguish a subcomponent reference from an arbitrary component name, nor can tooling follow the relationship programmatically.
The $ref pattern already exists in this package: ElementTypeRef uses { $ref: string } on AnatomyElement.type to express a machine-followable pointer to an external definition. The same pattern should apply to instanceOf when the target is a sibling subcomponent within the same spec.
The transformer spec 016-subcomponent-references defines the downstream behavior: when instanceOf matches a detected subcomponent, the serialized output should emit { $ref: "#/subcomponents/{key}" } instead of a plain string. This ADR records the types/schema change required to support that output.
Decision Drivers
- Additive-only change: Widening a union type preserves backward compatibility and avoids a MAJOR bump. Existing plain-string
instanceOfvalues remain valid. - Type ↔ schema symmetry: Both the TypeScript type and the JSON schema must reflect the new shape simultaneously (Constitution §I).
- No runtime logic: This package defines types and schema only. The
$refresolution logic belongs in downstream packages (Constitution §II). - Reuse existing patterns:
ElementTypeRefalready establishes the{ $ref: string }object shape. A newSubcomponentReftype should follow the same structure for consistency. - Schema-level validation for internal pointers: Unlike
ElementTypeRef(which targets arbitrary external URIs),SubcomponentRefalways points within the same document at#/subcomponents/{key}. The schema should enforce this with apatternconstraint so malformed references are caught at validation time. - PropBinding priority: On
Element.instanceOf,PropBinding(instance-swap binding) already occupies one union branch. The new$refshape is a third branch — the three are mutually exclusive at runtime.
Options Considered
Option A: Reuse ElementTypeRef for subcomponent references (Rejected)
Use the existing ElementTypeRef type directly on instanceOf.
Rejected because: ElementTypeRef is documented as referencing element type definitions (e.g., foundations#/definitions/glyph). Subcomponent references point to #/subcomponents/{key} — a different concept. Overloading the type conflates two distinct reference targets and makes schema documentation misleading.
Option B: New SubcomponentRef type with { $ref: string } (Selected)
Introduce a dedicated SubcomponentRef type structurally identical to ElementTypeRef but semantically scoped to subcomponent pointers. Widen instanceOf on both AnatomyElement and Element to accept it.
Pros:
- Clear semantic distinction between element-type references and subcomponent references
- Follows the established
{ $ref: string }pattern — no new structural concepts - Additive union widening — fully backward-compatible
Cons / Trade-offs:
- Two structurally identical
{ $ref: string }types exist. This is intentional: they represent different reference targets and may diverge in the future (e.g.,SubcomponentRefcould gain aversionfield).
Decision
Type changes (types/)
| File | Change | Bump |
|---|---|---|
Anatomy.ts | Add SubcomponentRef type; widen AnatomyElement.instanceOf to string | SubcomponentRef | MINOR |
Element.ts | Widen Element.instanceOf to string | PropBinding | SubcomponentRef | MINOR |
index.ts | Export SubcomponentRef | MINOR |
New type (types/Anatomy.ts):
# New typeSubcomponentRef: $ref: string # e.g., "#/subcomponents/formLabel"Before / after — AnatomyElement.instanceOf:
# BeforeinstanceOf?: string
# AfterinstanceOf?: string | SubcomponentRefBefore / after — Element.instanceOf:
# BeforeinstanceOf?: string | PropBinding
# AfterinstanceOf?: string | PropBinding | SubcomponentRefSchema changes (schema/)
| File | Change | Bump |
|---|---|---|
component.schema.json | Add SubcomponentRef definition; widen AnatomyElement.instanceOf and Element.instanceOf to oneOf including the new definition | MINOR |
New definition (schema/component.schema.json):
# #/definitions/SubcomponentRefSubcomponentRef: type: object description: "Reference to a subcomponent definition within the same spec." properties: $ref: type: string pattern: "^#/subcomponents/.+" description: "JSON Pointer to a subcomponent (e.g. '#/subcomponents/formLabel')" required: [$ref] additionalProperties: falseWhy
patternhere but not onElementTypeRef?ElementTypeReftargets arbitrary external URIs (e.g.,foundations#/definitions/glyph) where constraining the format would be overly restrictive.SubcomponentRefis internal-only — it always points to#/subcomponents/{key}within the same document, so the target space is small, well-defined, and appropriate for schema-level enforcement.
Before / after — AnatomyElement.instanceOf:
# BeforeinstanceOf: type: string
# AfterinstanceOf: oneOf: - type: string - $ref: "#/definitions/SubcomponentRef"Before / after — Element.instanceOf:
# BeforeinstanceOf: oneOf: - type: string - $ref: "#/definitions/PropBinding"
# AfterinstanceOf: oneOf: - type: string - $ref: "#/definitions/PropBinding" - $ref: "#/definitions/SubcomponentRef"Notes
SubcomponentRefis placed inAnatomy.tsalongsideElementTypeRefsince both are reference-object types used within anatomy/element contexts.- The
FigmaCodeOnlySource.instanceOffield (Props.tsline 24) is not widened — it records a raw component name for enum derivation, not a followable pointer. No change needed.
Type ↔ Schema Impact
- Symmetric: Yes
- Parity check:
SubcomponentReftype →#/definitions/SubcomponentRefschema definitionAnatomyElement.instanceOfunion widened in both type and schemaElement.instanceOfunion widened in both type and schema
Downstream Impact
| Consumer | Impact | Action required |
|---|---|---|
anova-kit | Recompile — instanceOf values may now be { $ref } objects instead of strings | Update any code that reads instanceOf to handle the SubcomponentRef shape |
Semver Decision
Version bump: 0.15.0 → 0.15.0 (no version change — this is a MINOR-compatible addition within the current release cycle)
Justification: All changes are additive: a new optional type (SubcomponentRef), a new schema definition, and union widening on existing optional fields. No existing valid values are invalidated. MINOR per Constitution §III and Versioning policy.
Consequences
- Consumers can programmatically distinguish subcomponent references from plain component names via the
{ $ref }object shape - The
$refpattern is now used for two distinct reference types: element types (ElementTypeRef) and subcomponents (SubcomponentRef), establishing{ $ref }as the standard reference mechanism in the spec - Downstream packages (
anova-transformer,anova-kit) that readinstanceOfmust handle the new union branch — but since the field is optional and additive, existing code continues to compile - Schema validators will accept both
"someString"and{ "$ref": "#/subcomponents/key" }forinstanceOffields