ADR-001 — Component Metadata Schema for Machine-Readable Design System
Date: 2026-06-11
Amended: 2026-10-11
Status: accepted
Context
The design system roadmap includes developer-triggered agentic moments — component scaffolding and layout generation among them — that read structured per-component context to do their work. (These were originally framed as “Phase 3 agentic loops”; the lite-agentic model later replaced always-on loops with a fixed set of developer-triggered moments, now Phase 7.) For those to work, agents need structured context about each component — not just its props and styles, but its purpose, constraints, anti-patterns, and relationships.
Without a shared schema defined upfront, each component would accumulate ad-hoc documentation in inconsistent shapes, making it impossible to build reliable tooling on top.
Two format options were evaluated:
- Markdown spec files — human-readable, low friction to write, used by hvpandya.com’s approach
- JSON metadata files — machine-readable, validates against a schema, higher initial cost to author
Research by Indeed (Diana Wolosin, Into Design Systems 2025) benchmarked 8 MCP configurations across 1,056 prompts and found JSON reduced token cost by ~80% vs Markdown with equal or better accuracy. The reason: JSON is unambiguous — explicit keys, explicit values, no parsing required.
The schema structure was synthesised from three sources:
- giorris.dev — base structure (
usage,variants,aiHints.selectionCriteria,antiPatterns) - hvpandya.com —
anatomyandtokens(explicit list of semantic token paths per component) - LLM Component Schema Standard (petrilahdelma) —
generativeRulesfor context-aware agent instructions
Decision
All components in packages/components/src/ will ship with a co-located ComponentName.metadata.json file validated against packages/components/component.schema.json.
The schema defines seven required top-level properties: component, usage, variants, states, tokens, accessibility, and relationships.
Key design choices:
status(draft/stable/deprecated) lives insidecomponent.status, not as a top-level section — status is a property of the component itself, not a parallel concernusagesplits agent guidance intokeywords(intent-matching terms) andwhen(scenario guidance)usage.antiPatternsrequires all three fields (scenario,reason,alternative) — partial guidance is not useful to an agentrelationshipsis the richest section for layout generation:acceptsandcontainedByenforce parent-child constraints;neverPairWithprevents illegal sibling combinations;compositionPatternsprovides named, tested starting points;layoutBehaviordescribes fill/hug/fixed width and display typetokenscategorises semantic paths by type (color,spacing,typography,borderRadius,other) — aligns with the three-layer token model and supports deprecation analysisfigmaNodeIdis an optional field insidecomponentto support Figma ↔ component traceabilityadditionalProperties: falseis enforced throughout — schema drift is caught at validation time
Deliberately excluded (do not re-add without a new ADR)
These came from the source inspirations, or from an early draft of the schema, and were intentionally left out. They are recorded so they are not reintroduced as oversights:
anatomy— part-by-part structural documentation. Human-readable and doesn’t reduce agent token cost; it lives in Storybook stories instead.aiHints/generativeRules— catch-all sections for agent instructions. Guidance is instead distributed where it carries structural meaning:usage.keywords,usage.when,relationships.compositionPatterns, andrelationships.layoutBehavior. (aiHintswas briefly present in the schema and removed in commit30e769d.)useCases— a flat scenario array. Replaced by theusage.keywords(intent-matching) +usage.when(scenario guidance) split, which serves both component selection and layout generation.
The schema must be defined and reviewed before any component is built so that the first component (Button) establishes the pattern correctly.
Consequences
- Every new component requires a metadata file — this adds authoring overhead per component
- The schema becomes a contract: changes to it are breaking for any tooling built on top (MCP server, Airtable sync, Storybook metadata panel)
- Agents consuming this system get reliable, token-efficient context without needing to parse implementation code
- The
tokenssection creates a living map of which semantic tokens are actually in use — useful for deprecation analysis - The expanded
relationshipssection is the primary input for the layout generation agentic moment —accepts/containedByare structural rules an agent must not violate - Excluding
anatomyreduces authoring overhead without losing machine-readable value; human-readable anatomy can live in Storybook stories instead - Schema validation can be added to CI as a lightweight check alongside the token build
Amendment (2026-06-17) — Named variant axes, unified taxonomy, enforced validation
A foundation review before Phase 5b found the schema and the committed metadata had drifted: 7 of 11 metadata files failed the schema, and the schema could not express a component with more than one variant axis (Button had bolted on illegal top-level sizes/shapes keys). Three refinements:
-
variantsis now a map of named axes. It changed from a single{ options, default, purpose }object to an object keyed by axis name (variant,size,shape, …), each axis holding{ options, default, purpose }. A component with one visual axis uses a single key namedvariant; a component with no visual variation declaresvariantwith onedefaultoption;defaultmay benullfor an axis that is off unless set (e.g. Buttonshapefor icon-only mode). Rationale: Button (variant + size + shape), TextField (icon + size + shape), and every Phase 5b/5c component (Avatarsize, Chipstate, PaginationArrowsdirection+state) have multiple independent axes. The old single-axis shape could not represent them honestly. -
One taxonomy, enforced.
component.category∈atom | molecule | organism | layoutandcomponent.type∈interactive | display | container | inputare applied consistently across all files. Earlier files used a parallel ad-hoc vocabulary (primitive,typography,media,action) that validated against nothing. Mapping: layout primitives →layout/container; Text/Heading/Icon →atom/display; Button →atom/interactive; form components and Card keepmoleculewithinput/container. -
Validation is enforced in CI — realising the final consequence above.
scripts/validate-metadata.js(ajv, draft 2020-12) validates every metadata file plus the example against the schema and checks thatcomponent.namematches its folder;.github/workflows/components-check.ymlruns it alongside typecheck and build on every PR touching components. Schema/metadata drift can no longer merge silently.
accessibility.notes (optional string) was also added — several components carried guidance the role/ariaAttributes/keyboardInteractions fields could not hold.
The contract warning above still holds: this was a breaking change to the variants shape, so all 11 metadata files plus component.metadata.example.json and the component-scaffold command prompt were migrated in the same change.
Amendment (2026-07-23) — relationships renamed to composition (recorded retroactively)
Commit d89782a (2026-06-24) reshaped the relationship section without a matching amendment here; this records it so the ADR prose matches the live schema:
relationships→composition. The top-level section is namedcomposition; the seven required properties arecomponent,usage,variants,states,tokens,accessibility,composition.compositionPatternsmoved tousage.patterns. Named, proven compositions live underusagealongsidekeywords/when/antiPatterns— they are guidance for choosing and combining the component, not structural constraints.neverPairWithdropped, absorbed intousage.antiPatterns. Five files had populated it (the three Card variants’ mutual-exclusion rules, Chip↔Button, Image↔Avatar). Those constraints survive as anti-patterns — e.g. Card’s “Using Card as a sibling of CardVertical or CardHorizontal in the same layout group” — where each carries areasonandalternativeinstead of a bare name list; self-nesting rules moved tolayoutBehavior.canNest. Per the “deliberately excluded” convention above, do not re-add it without a new ADR.
composedOf (internal library dependencies) has been part of the section since the original schema. References to relationships elsewhere in this ADR are preserved as written; read them as composition.
Amendment (2026-10-07) — tokens lists what the component itself reads, enforced
tokens was hand-written and never checked against the code. Issue #117 found 11 of 27 components out of step with their CSS Modules. Seven left out tokens their CSS reads (AppHeader missed nine). Others listed tokens their CSS never reads, either stale (Text listed font.weight.medium; its CSS uses bold) or applied by a child component (CardVertical listed color.text.subtle, which reaches it through <Text color="subtle">).
Decision. tokens.* lists exactly the tokens the component itself reads: var(--ds-*) in its CSS Module or its index.tsx. A TSX template prefix counts, so Avatar’s var(--ds-size-avatar-${size}) covers size.avatar.*. A token applied by a child component (a Text color, a Stack gap, a Heading size) belongs to the child’s metadata, not the parent’s.
scripts/validate-metadata.js enforces both directions. A token read by the CSS Module but not listed fails, and so does a listed token that neither file reads.
Why this rule: it is the only one a script can check. It also keeps the token map complete for deprecation analysis: every token a component renders is read by some component’s CSS, so it is listed in that component’s metadata. The route a child token takes is recorded where it is useful to a builder, in the component spec’s anatomy[].builtOnProps (ADR-027).
Alternative considered: waiting for ADR-027 to move tokens into the spec, where spec:validate already enforces this for the two pilot components. That would leave 25 components unchecked until the pilot is decided. If ADR-027 is accepted, the check moves with the section.
Amendment (2026-10-11) — states, tokens and accessibility move to the spec file
ADR-027 accepted the spec file after the harness arm (2026-10-08) and listed this move as its first follow-up. Metadata keeps what /layout-generation reads: component, usage, variants, composition. The sections only the builder moments read (/component-scaffold, /review-component, a11y-coverage.js) go where they are read.
Decision.
- A component with a
<Name>.spec.jsonkeepsstatesandaccessibilityin the spec, and has notokenslist: its tokens are the ones its specstylesreference, whichspec:validatealready checks against the CSS Module.scripts/validate-metadata.jsfails if such a metadata file still carries any of the three sections. - A component without a spec keeps all three in metadata, still required and still checked as before, including the 2026-10-07 rule that
tokenslists what the component itself reads.component.schema.jsonno longer lists them as required; the validator makes them required exactly when there is no spec. - Readers go through
readComponentMetadatainscripts/lib.js, which returns the metadata withtokens(derived from the spec’s styles, grouped by the same five categories) andaccessibilitytaken from the spec.a11y-coverage.js,component-risk.js,handoff-tidy.js,generate-pattern-schema.js,sense-component.jsand the Airtable component push use it, so none of them cares which file holds the data. Only Button and CardVertical have a spec, so the move is complete for those two now and finishes for the other 25 with the ADR-027 backfill.
The anatomy exclusion is reversed for the spec file only. Metadata still has no anatomy; the spec’s anatomy section is the place for it (ADR-027). The “Deliberately excluded” list above stands for metadata.
Consequences.
- The rule that
tokenslists only what the component itself reads now holds by construction for spec components: the list is derived, never written. CardVertical’s six unread tokens (color.text.*,font.family.headline-serifand others) are gone from its token list, and so from its Airtable row on the next push. npm run metadata:derive-tokensrefuses components that have a spec.- Measured on the two migrated components: metadata, which
/layout-generationreads for all 27 components, shrinks by 27% (Button, 9.9 KB to 7.2 KB) and 21% (CardVertical, 21.1 KB to 16.6 KB). A builder moment reading both files sees only 10% and 5% less (19.4 KB to 17.4 KB, and 39.3 KB to 37.4 KB), because the spec absorbsaccessibility. The +31% to +55% overhead ADR-027 measured came mostly from sections that stay in the spec; it falls away only as the spec’s own size does. - The required-until-spec rule is temporary. When the backfill gives every component a spec,
states,tokensandaccessibilityleave the metadata schema and the validator check goes with them.
Amendment (2026-10-11) — The transitional rule ends: metadata has no states, tokens or accessibility
The earlier amendment of this date kept the three sections in metadata for components without a spec. Every component now has a spec (ADR-027 amendment of this date), so the exception is gone.
component.schema.jsonno longer definesstates,tokensoraccessibility. Metadata that carries them fails the schema, andscripts/validate-metadata.jssays where they went. The required top-level properties arecomponent,usage,variantsandcomposition.- The token list is the set of tokens a spec’s
stylesreference.spec:validatechecks it against the CSS Module and now also againstindex.tsx, because a size read through a template (var(--ds-size-avatar-${size})) is a real read.npm run metadata:derive-tokensand its script are deleted. readComponentMetadatainscripts/lib.jsstays as the one reader that returns metadata withtokensandaccessibilitymerged in from the spec. The a11y gate, the review-risk script, the pattern aggregate, the component snapshot and the Airtable push use it.- The metadata that
/layout-generationreads for all 27 components went from 151 KB to 113 KB (−25%). - Of the 27 token lists, one changed when derived from the specs: Avatar had listed
size.avatar.smandsize.avatar.md, which onlyindex.tsxreads. Its spec now states them assizerules, so nothing is lost. - The 2026-10-07 rule that
tokenslists only what the component itself reads holds by construction for all components now. There is nothing to hand-write and nothing to keep in step.