Skip to content

RFC 001: Component Dictionary#87

Open
nathanacurtis wants to merge 2 commits into
mainfrom
rfc/001-component-dictionary
Open

RFC 001: Component Dictionary#87
nathanacurtis wants to merge 2 commits into
mainfrom
rfc/001-component-dictionary

Conversation

@nathanacurtis
Copy link
Copy Markdown
Member

@nathanacurtis nathanacurtis commented May 8, 2026

How to review

  1. Readrfc/001-component-dictionary/README.md (opens the rendered RFC in a new tab; close it to return here)
  2. Comment → use the comment box at the bottom of this page

That's the proposal. The companion documents and sketches in this PR are supporting material — useful if you want to dig into a specific architectural detail, but they are not asking for review at this stage. If you only have time for one document, read the README; the rest is optional context.

What the RFC proposes

A deterministic emitter registry inside specs-cli that fans each validated component contract into ~16 purpose-built files so each consumer (engineers, agentic coding tools, design-system reviewers, report scripts) gets the shape closest to its workflow.

The bet: the right shape for design-system data in the agentic-coding era is a blend of structured (YAML/JSON) and prose (MD) outputs deterministically projected from a single schema-validated contract.

What I'm asking for

Substantive engagement with the RFC's argument, posted as a Conversation comment:

  • Push back on the bet, the principles, the alternatives weighed, or the drawbacks acknowledged.
  • Flag anything in the proposal that would block adoption for your team.
  • Suggest what's missing that a serious reviewer would expect to see.

I am not asking for line-edits on the companion docs or sketches. Sketches are exploratory; companions are reference material. If you spot a narrow line-level note on the README itself worth leaving inline, the Files Changed tab supports it — but the comment box on this page is the right venue for substantive feedback on the proposal.

Status

Proposed. The RFC itemizes alternatives considered, drawbacks, decisions ratified, immediate unresolved questions, and future work — engage with each section as it suits you.

🤖 Generated with Claude Code

nathanacurtis and others added 2 commits May 8, 2026 15:20
…onary

Establishes rfcs/ as a sibling to adr/ for strategic, multi-decision
design proposals (broader than ADRs but not yet — or no longer —
discrete decisions). RFCs carry the why; ADRs carry the what.

RFC 0001: Component Dictionary

Proposes a deterministic emitter registry inside specs-cli that fans
each validated component contract into ~16 purpose-built files so
each consumer (engineers, agentic coding tools, design-system
reviewers, report scripts) gets the shape closest to its workflow.
Pipeline is pure: zero LLM calls, zero opinions, byte-reproducible
from the spec; the contract stays canonical and derivative files
are projections; inference is deferred to a separate downstream
stage. The bet: the right shape for design-system data in the
agentic-coding era is a blend of structured (YAML/JSON) and prose
(MD) outputs deterministically projected from a single
schema-validated contract.

Includes companion documents (script-inference-boundaries,
dictionary-composition, cli-reference, smoothing-backlog,
templates-appendix) and 14 illustrative sketches grounded in
button.yaml from the GitHub Primer demo set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| Comprehensive scripted reference for docs / review | `button.docs.md` | YAML |
| Any precision question — exact token, dimension, padding, effect, per-variant value | **YAML** | — |

**Rationale.** The contract (YAML in v1, ~3k tokens for Button) is canonical and complete but verbose and shaped for the schema, not the consumer. Each edge artifact reshapes the contract's data into a form closer to the consumer's workflow — React types, iOS enums, CSS rules, token graphs, structural index. Starting at the edge means less translation work for the consumer; falling back to the contract for precision means no answer is ever approximated. Edge artifacts should not restate values the contract carries authoritatively when a precision question has a single correct answer.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each edge artifact reshapes the contract's data into a form closer to the consumer's workflow

The only thing I'm having a hard time grappling with is how we might be producing an artifact that is closer to the consumer workflow, but it isn't precise enough to get the right outcome during inference and very likely acts as "corrupted" context.

To me it poses a question of "is it better to have false artifacts or no artifacts for LLM inference". If the cli allows for customizing the output via templates or configuration options I think this concern goes away.


This RFC scopes `specs generate` and its emitter registry. Everything above generate is existing CLI behavior; the agent smoothing pass downstream is a separate workstream and the scripts/inference boundary is a hard rule (see Principles).

Inside `specs generate`, each emitter is a Style-Dictionary-style `Selector → Transform → Format` pipeline: a pure function `(spec, options) => { filename, content }` with no I/O. The registry lives in **`specs-cli`**; custom emitters are deferred (see Decisions).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pure function approach would make supporting templates at config/invokation time really simple. That would allow consumers to produce whatever artifact that they would find useful.

Baking in robust defaults as like in your "proposed outputs" table still makes sense in a world where the CLI supports templating as I'm suggesting.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you have a "Template-driven emitters" document which describes an approach to this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments aren't a contract, they're just documentation.

I'd advocate for stronger typing. In fact that's something most React component devs miss, which leaves users guessing what props are usable together or not. This can also lead to runtime errors when an edge case isn't officially supported but ended up being shipped in the wild.

I gave it a go (I tested it, it compiles!):

// Pure types + slot signatures. No framework imports.
// Defaults and slot visibility rules are encoded as `as const` data so that
// tooling, codegen, and runtime validators can read them — comments would rot.

// ---------------------------------------------------------------------------
// Public unions
// ---------------------------------------------------------------------------

export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'invisible';
export type ButtonSize    = 'small' | 'medium' | 'large';
export type ButtonState   = 'rest' | 'focus' | 'hover' | 'pressed' | 'disabled' | 'inactive';
export type ButtonAlign   = 'center' | 'start';

// ---------------------------------------------------------------------------
// Props shape
// ---------------------------------------------------------------------------

export interface ButtonProps {
  variant?:        ButtonVariant;
  size?:           ButtonSize;
  state?:          ButtonState;
  alignContent?:   ButtonAlign;
  counter?:        boolean;
  dropdown?:       boolean;
  leadingVisual?:  string | null;
  trailingVisual?: string | null;
}

// ---------------------------------------------------------------------------
// Defaults
//
// `as const`              — preserves literal types so consumers see
//                           `ButtonDefaults.variant` as 'secondary', not string.
// `satisfies Required<…>` — every prop must have a default and every default
//                           must match its declared union. A missing or drifted
//                           default fails at compile time.
// ---------------------------------------------------------------------------

export const ButtonDefaults = {
  variant:        'secondary',
  size:           'medium',
  state:          'rest',
  alignContent:   'center',
  counter:        false,
  dropdown:       false,
  leadingVisual:  null,
  trailingVisual: null,
} as const satisfies Required<ButtonProps>;

// ---------------------------------------------------------------------------
// Slots shape
// ---------------------------------------------------------------------------

export interface ButtonSlots {
  search?:         unknown;
  button:          string;
  counterLabel?:   unknown;
  trailingVisual?: unknown;
  dropdown?:       unknown;
}

// ---------------------------------------------------------------------------
// Slot visibility rules
//
// Replaces the `/* visible when ... */` prose. The `prop` field is narrowed
// to props whose declared type matches the predicate, so e.g. `whenTrue`
// cannot reference a string prop and a typo in the prop name is rejected.
// ---------------------------------------------------------------------------

type PropKeysOfType<T> = {
  [K in keyof ButtonProps]-?: NonNullable<ButtonProps[K]> extends T ? K : never;
}[keyof ButtonProps];

type BooleanPropKey  = PropKeysOfType<boolean>;
type NullablePropKey = {
  [K in keyof ButtonProps]-?: null extends ButtonProps[K] ? K : never;
}[keyof ButtonProps];

export type SlotVisibility =
  | { kind: 'always' }
  | { kind: 'whenTrue';    prop: BooleanPropKey  }
  | { kind: 'whenNotNull'; prop: NullablePropKey };

export const ButtonSlotRules = {
  search:         { kind: 'whenNotNull', prop: 'leadingVisual'  },
  button:         { kind: 'always' },
  counterLabel:   { kind: 'whenTrue',    prop: 'counter'        },
  trailingVisual: { kind: 'whenNotNull', prop: 'trailingVisual' },
  dropdown:       { kind: 'whenTrue',    prop: 'dropdown'       },
} as const satisfies Record<keyof ButtonSlots, SlotVisibility>;

@kaelig
Copy link
Copy Markdown

kaelig commented May 14, 2026

The bet makes sense to me – and I think it stands fine as a v1.

Consider tackling these (non of them are blockers):

  1. category/intent-based dictionary: action, input, container, navigation, feedback, overlay… to orient agents in finding components that solve specific cases without having to go through every guessed potential candidate component. Example: "build a multi-select dropdown".
  2. anti-pattern / anti-component mapping ("don't use X for this, use Y"), for example: "Don't use a button, use a link when the action opens a new tab or updates the URL"
  3. Extra layer that solves for composition (for patterns, not just single components)
  4. Feedback loops: how do team-specific changes travel back upstream?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants