# Design-System Protocol

## Purpose

Define a deterministic, registry-first discipline for any agent or skill that
touches a visual surface (component, page, mockup, design review). The goal is
twofold:

1. **Visual coherence** across the whole product — every UI passes through the
   same canonical inventory of primitives + tokens, never reinvents one.
2. **Code economy** — the component registry is interrogated **before**
   implementation, so existing primitives are reused instead of duplicated.

This is the textual SSOT for the registry-first protocol. Skills (`ui-design`,
`frontend-design`, `design-system-init`), agents (`ui-expert`, `code-reviewer`),
and the `/design-review` command all reference this module instead of
duplicating the BLOCKING-read cascade.

## Scope

**In**: How to discover, reuse, and extend the project component registry
before producing or reviewing UI. Token contract enforcement. Authority matrix.
What to do when a primitive is missing.

**Out**: Aesthetic direction (see `${paths.ui_guidelines}` and
`identity.design_philosophy`), motion specs (see `motion-design` skill),
illustration assets (see `visual-designer` agent).

## Gating

This protocol is **conditional** on `features.has_design_system: true` in
`baldart.config.yml`. When the flag is `false` or missing, the registry reads
in step 1 below are SKIPPED — agents/skills still respect
`${paths.ui_guidelines}` but do not block on a registry that does not exist.

When the flag is `true`, the registry reads are **BLOCKING**: skipping them is
a protocol violation and the agent must refuse to proceed.

**One exception — the Functional Traceability Gate is unconditional.** The
gate defined in § "Functional Traceability Gate" below is the only part of this
module that is **not** gated on `features.has_design_system`. It concerns
*function provenance* (does each artifact in a mockup do something?), not the
visual registry, so it applies to every UI surface — design-system or not.

To bootstrap a registry on a project that does not yet have one, invoke the
`design-system-init` skill (`/design-system-init`).

## Canonical sources (resolve via `baldart.config.yml`)

- `${paths.design_system}/INDEX.md` — component index + Canonical Authority
  Matrix + quick MUST rules. Entry point for every UI task.
- `${paths.design_system}/tokens-reference.md` — human-readable token contract
  (colors, spacing, shadows, radii, motion). Single source of truth for any
  value that would otherwise be hardcoded.
- `${paths.design_system}/components/<Name>.md` — per-component spec (props,
  variants, accessibility, anti-patterns). One file per primitive.
- `${paths.design_system}/patterns/*.md` — opt-in pattern docs (theming,
  overlays, motion choreography, multi-tenant pairing rules).
- `${paths.ui_guidelines}` — brand voice, typography, philosophy. Always read.
- `${paths.components_primitives}` / `${paths.components_root}` — source roots
  where the registry primitives are actually implemented.

## BLOCKING pre-work (when `has_design_system: true`)

Before producing any new UI code, mockup, or design review:

1. **BLOCKING** — read `${paths.design_system}/INDEX.md`. Extract: the
   component index, the Authority Matrix for the surface you are about to
   touch, any pattern docs the index points to.
2. **BLOCKING** — read `${paths.ui_guidelines}` for visual language, typography,
   accessibility, and brand voice. The `identity.design_philosophy` (when set)
   is the lens through which these guidelines apply.
3. **BLOCKING** — read `${paths.design_system}/tokens-reference.md`. Treat it
   as the SSOT for every color / spacing / shadow / radius / duration value.
4. **BLOCKING per primitive in scope** — read the relevant
   `${paths.design_system}/components/<Name>.md`. For every component the task
   will create, modify, or visually depend on.
5. **BLOCKING per pattern in scope** — read the relevant
   `${paths.design_system}/patterns/<topic>.md` (e.g. theming pattern when
   `features.multi_tenant_theming: true`).

Skipping any of the above when the gating flag is `true` is a protocol
violation. The correct response to a missing file is to flag it and propose
adding it via `design-system-init` or a follow-up card — **not** to proceed
without it.

## Component Discovery Cascade

For every visual element the task introduces or touches, walk the cascade in
order. Stop at the first match.

1. **Registry hit** — `${paths.design_system}/INDEX.md` lists a primitive that
   covers the use case. Use the per-component spec to implement.
2. **Source-tree hit** — a component exists under
   `${paths.components_primitives}` (or `${paths.components_root}/shared/*`)
   that is not yet documented in the registry. Use it, then **propose adding
   it to the registry** as a follow-up — the registry is the canonical
   inventory and silent primitives create drift.
3. **External catalog candidate** — no in-repo match but a catalog primitive
   (shadcn, 21st.dev, Radix UI, etc.) fits the canonical stack. Use it,
   document it as a new entry in the registry.
4. **Create from scratch** — only after 1–3 fail. Any newly created primitive
   MUST be tokens-compliant and must ship with its per-component spec in the
   same change.

The cascade is identical for design (mockups, HTML prototypes) and for
implementation (component code).

## Token Contract

When `features.has_design_system: true`, the following are **forbidden** in
any code or mockup produced under this protocol:

- Hardcoded hex / rgb / hsl color literals (use the color tokens).
- Hardcoded shadow tuples (use the shadow tokens).
- Hardcoded border-width / border-radius numeric literals (use the radius and
  border tokens).
- Hardcoded spacing numbers outside the spacing scale (use the spacing tokens).
- Hardcoded motion durations or easings (use the motion tokens, or escalate to
  the `motion-design` skill).

The exact token consumption mechanism (CSS custom properties, Tailwind theme
extend, theme provider) is project-specific — defer to
`${paths.design_system}/tokens-reference.md` and the project's chosen
mechanism.

## Reference Tables (embed)

These tables are the SSOT for the *numeric ranges* every agent/skill should
cite verbatim instead of guessing. They complement
`${paths.design_system}/tokens-reference.md` (which holds the *project-specific*
values) by providing the canonical menu of options from which a project
typically derives its tokens.

### Type scale (base 16px)

| Ratio | Name | Use | Example sequence (rounded) |
|---|---|---|---|
| 1.125 | Major Second | Dense UI, dashboards | 12 / 14 / 16 / 18 / 20 / 23 / 26 |
| 1.200 | Minor Third | Default web app | 12 / 14 / 16 / 19 / 23 / 28 / 33 |
| 1.250 | Major Third | Marketing, landing | 13 / 16 / 20 / 25 / 31 / 39 / 49 |
| 1.333 | Perfect Fourth | Editorial, blog | 12 / 16 / 21 / 28 / 37 / 50 / 67 |
| 1.414 | Augmented Fourth | Display-led | 12 / 17 / 24 / 34 / 48 / 67 / 95 |
| 1.618 | Golden | Hero / poster | 10 / 16 / 26 / 42 / 68 / 110 |

Line-height pairing (inverse to size): 12–14px → 1.5 · 16–18px → 1.5 ·
20–24px → 1.35 · 28–40px → 1.2 · 48px+ → 1.1. Limit the scale to **6–9
deliberate steps**. More steps = chaos. Extra hierarchy comes from weight,
color, and spacing — not from new sizes.

### Color contrast targets (WCAG 2.x + APCA secondary gate)

| Case | WCAG 2.x ratio | APCA Lc | Notes |
|---|---|---|---|
| Body text (<18pt regular / <14pt bold) | ≥ 4.5 (AA), ≥ 7 (AAA) | ≥ 75 | Reading-flow text |
| Large text (≥18pt or ≥14pt bold) | ≥ 3 (AA), ≥ 4.5 (AAA) | ≥ 60 | Headlines, large UI labels |
| UI components / focus indicator | ≥ 3 | ≥ 45 | Borders, icons, focus rings |
| Disabled / decorative text | exempt | Lc 30–45 | Still must be distinguishable |
| Placeholder text | ≥ 4.5 if treated as content | ≥ 60 | Many a11y audits flag low-contrast placeholders |

WCAG 2.x remains the legal requirement. **APCA is the secondary gate**: it
captures perceived legibility considering weight, polarity, and size, so a
project that satisfies WCAG can still have specific text-on-background pairs
that fail APCA — those are real readability bugs.

### Spacing scale (base-4 vs base-8)

| Step | base-4 (px) | base-8 (px) | Typical use |
|---|---|---|---|
| 0 | 0 | 0 | reset |
| 1 | 4 | 8 | icon ↔ label inline |
| 2 | 8 | 16 | padding-x button sm |
| 3 | 12 | 24 | padding-x button md, gap between cards |
| 4 | 16 | 32 | section gap mobile |
| 5 | 24 | 48 | section gap desktop |
| 6 | 32 | 64 | hero spacing |
| 7 | 48 | 96 | landing section |
| 8 | 64 | 128 | landing hero |

**base-4** preferred for dense product UI (dashboards, IDE-like tools);
**base-8** preferred for marketing / editorial. **Never mix** the two scales
in the same product — pick one and derive every token from it. Arbitrary
values outside the scale (`padding: 13px`) are a token violation.

### Density tiers

| Tier | Row height | Button md padding-y | Table cell padding | Primary use |
|---|---|---|---|---|
| Compact | 28–32px | 4–6px | 6–8px | Power user, data tables, IDE-like surfaces |
| Cozy | 36–40px | 8–10px | 10–12px | Default web app |
| Comfortable | 44–48px | 12–14px | 14–16px | Mobile primary, consumer surfaces |
| Spacious | 56–64px | 16–20px | 20–24px | Marketing, onboarding |

**Touch-primary surfaces must use Comfortable or above** (44px = WCAG 2.5.5
enhanced target). Mixing tiers within the same view is allowed only when the
information density genuinely demands it (e.g. compact data table inside a
cozy app shell) — document the exception in the registry.

## Token Cascade: primitive → semantic → component

A mature token system has **three layers**, and components consume only the
top of the cascade:

1. **Primitive tokens** — raw palette / scale values (`color-blue-500`,
   `space-4`, `radius-md`). Stable, machine-generated, named by *what they
   are*. Components must **never reference these directly**.
2. **Semantic tokens** — purpose-named aliases (`color-action-primary`,
   `color-surface-elevated`, `color-text-on-action`, `space-section-gap`).
   Resolve to a primitive in the active theme. Named by *what they mean*.
   The theming layer (light / dark / high-contrast / multi-brand) swaps
   semantics → primitives — primitives themselves don't change per theme.
3. **Component tokens** (optional) — per-component aliases when a primitive
   appears in many components but a semantic is too generic
   (`button-bg-primary`, `card-shadow-elevated`). Resolve to a semantic.

**Why it matters**: components written against semantic tokens become
theme-agnostic and multi-brand-ready by construction. A brand swap touches
the semantic-layer mapping only, not the component code. Dark mode is a
semantic-layer concern (different primitive mappings per theme), **never**
implemented via `filter: invert()` or naïve color flips — that produces
washed-out brights, inverted shadows, and inverted images.

When the project's `tokens-reference.md` doesn't yet expose a semantic
layer, propose adding one rather than letting components reach into
primitives — that drift is the entry door to inconsistent theming.

For palette *generation*, prefer **OKLCH** (or LCH) over HSL: it's
perceptually uniform, so equal lightness numbers across hues read as equal
brightness, dark/light variants stay tonally consistent, and palette steps
land at predictable contrast levels. HSL `lighten()` / `darken()` produces
visually uneven steps because HSL lightness is not perceptual.

## Authority Matrix

`${paths.design_system}/INDEX.md` declares, for each primitive, who is
authoritative over which dimension:

- **Visual authority** — colors, spacing, shadows, radii: the registry +
  tokens-reference. Agents/skills may not deviate.
- **Behavioral authority** — props, variants, accessibility hooks: the
  per-component spec.
- **Composition authority** — how primitives are combined into patterns: the
  pattern docs.

When a task would violate any of these authorities, the agent must:

1. Stop and surface the conflict.
2. Propose a registry update (new variant, new token, new pattern) instead of
   bypassing the authority locally.

## When a primitive is missing

If the cascade reaches step 3 or 4 (no registry hit, no source-tree hit), the
agent has two choices:

- **Add to the registry** — preferred when the primitive is broadly useful.
  Create the per-component spec, the implementation under
  `${paths.components_primitives}/<Name>`, and an INDEX.md entry in the same
  change.
- **Local one-off** — only when the use case is genuinely surface-specific
  (e.g. a marketing-page hero illustration). Even then, prefer composing
  existing primitives over hand-rolling a new one. Justify the local choice
  in the change description.

Never silently introduce a new component that overlaps with an existing
registry primitive — that is the exact failure mode this protocol prevents.

## Functional Traceability Gate (BLOCKING — unconditional)

A mockup is a fidelity target, **not** a requirements oracle. Design tools
(Claude Design, Figma, the `ui-design`/`frontend-design` generators) routinely
add interactive-looking chrome — buttons, icons, menu entries, toggles, badges —
that no requirement asked for. Implementing a mockup **1:1 for fidelity**
materialises that chrome into real markup + dead handlers + unused icon imports,
and the cruft then propagates forever. This gate stops it at the boundary where
the leak happens: **before** any artifact becomes code.

**Gating: unconditional.** Unlike the registry cascade above, this gate runs on
every UI surface regardless of `features.has_design_system`. It is about whether
each artifact *does something*, not about tokens or the registry.

### The rule

> No interactive or iconographic artifact is implemented unless it traces to a
> function. Orphans are resolved — never implemented "to stay faithful to the
> mockup".

"Interactive or iconographic artifact" = any element a user could plausibly
expect to act on or read as a signal: buttons, links, menu items, tabs,
toggles, inputs, icons, badges, status dots, action affordances. Pure layout
and decorative-by-construction visuals (spacers, dividers, background art,
hero illustrations) are out of scope — they carry no implied behaviour.

### The oracle (where "a function" comes from), in order

1. **PRD UI Element Inventory** — the `function_ref` column (added by `/prd`
   Step 3d / the `ui-design` inventory). When present, this is the authoritative
   allowlist: each row names the AC / user story / handler / endpoint it serves,
   or is explicitly marked `decorative`.
2. **Backlog card spec / acceptance criteria** — when no inventory row exists,
   the artifact must map to an AC or an explicit behaviour in the card.
3. **Neither** — the artifact is an **orphan**. Do not guess a purpose for it.

### Classification + outcome (per artifact)

| Classification | Definition | Outcome |
|---|---|---|
| **Backed** | Traces to an inventory `function_ref`, an AC, or a real handler/endpoint | **Implement** — wire the real behaviour |
| **Decorative** | Legitimately present but carries no behaviour (e.g. a section icon, a brand mark) | **Static-only** — render it inert: no `onClick`, no stub handler, no route, no disabled-button placeholder. If it imports an icon purely for decoration, keep it minimal and labelled `aria-hidden` |
| **Orphan** | Interactive-looking, traces to nothing, not deliberately decorative | **Drop or escalate** — see below |

For every **orphan**, never decide silently (consistent with the framework's
`always-ask, never-assume` and "no silent AC deferral" contracts):

- **Drop** when the artifact is clearly unrequested chrome and removing it does
  not change any in-scope flow — the default for obvious leftover decoration
  rendered as a button.
- **Escalate** (one `AskUserQuestion` per orphan, or a single grouped report
  when run inside an autonomous workflow) when it is plausible the artifact
  *should* have a function that was simply never specified — i.e. dropping it
  might silently discard intended scope. The three offered resolutions are:
  *implement (needs an AC → routes to `/prd-add` or a follow-up card)*,
  *static-only*, or *drop*.

### Where it runs

1. **Design / generation time** (`ui-design`, `frontend-design`, `ui-expert`
   designing) — when producing a mockup, do not invent interactive chrome with
   no backing requirement; mark genuinely decorative elements as such in the
   inventory so the downstream gate is deterministic, not a guess.
2. **Implementation time** (`ui-expert` implementing) — **BLOCKING pre-work**:
   build the artifact list from the mockup, classify each row against the oracle,
   and resolve every orphan (drop / static-only / escalate) **before** writing
   code. Implementing an orphan as a live affordance is the exact failure mode
   this gate prevents.
3. **Review time** (`code-reviewer`, `/design-review`) — verify the shipped diff
   introduces no live affordance that traces to nothing. Each one is a
   `UI_ORPHAN_AFFORDANCE` HIGH finding (dead handler / unused icon import / button
   with no effect).

### Finding code

`UI_ORPHAN_AFFORDANCE` — an interactive or iconographic artifact that was
implemented (or is about to be) without a traceable function. HIGH severity:
it ships dead code and erodes the minimalism contract. The fix is always one of
the three outcomes above, never "leave it, it matches the mockup".

## Post-Intervention Coherence Check (BLOCKING completion gate)

The BLOCKING pre-work above enforces *reads* before any UI change. This
section enforces *writes* before completion. **No UI task is considered
complete** when `features.has_design_system: true` until the registry has
been verified against the change that was just made.

The check runs at end-of-task by the agent that performed the work (`ui-expert`,
`ui-design`, `frontend-design`, `code-reviewer`). It is **not** delegated to
the weekly `ds-drift` routine — that routine is a safety net, not the primary
mechanism. Drift introduced today must be reconciled today, not next Monday.

### What to verify

For each item in the diff that touched a visual surface, run these 4 checks:

1. **New primitive ⇒ new spec**: did the change introduce a component under
   `${paths.components_primitives}` that has no
   `${paths.design_system}/components/<Name>.md`? If yes, the spec MUST ship
   in the same change. Missing spec = `DS_INDEX_DRIFT` finding, task blocked.
2. **Modified primitive ⇒ updated spec**: did the change alter a primitive's
   props / variants / accessibility behavior? If yes, the corresponding
   `components/<Name>.md` MUST be updated to match. Stale spec =
   `DS_COMPONENT_STALE` finding, task blocked.
3. **New / changed token ⇒ updated reference**: did the change introduce or
   modify a token (color, spacing, shadow, radius, motion)? If yes,
   `${paths.design_system}/tokens-reference.md` MUST reflect it. Stale
   reference = `DS_TOKENS_DRIFT` finding, task blocked.
4. **Surfaced silent primitive ⇒ added to INDEX**: during the work, did you
   reuse a primitive that exists in the source tree but is **not** in
   `INDEX.md`? If yes, add it to the INDEX in the same change (or open a
   follow-up card if the registry entry requires design judgement beyond
   the current scope). Silent primitive = `DS_INDEX_DRIFT` finding.

### Decision matrix

| Situation | Action |
|---|---|
| Spec / token update is trivial (variant added, prop renamed, token value tweaked) | Apply inline — same commit as the code change |
| Spec / token update requires design judgement (new variant semantics, breaking change, accessibility decision) | Open a follow-up card; **document the gap explicitly** in the change description; flag for `code-reviewer` |
| Reused undocumented primitive that is broadly useful | Add INDEX entry inline; if a full spec needs design work, scaffold a stub spec marked `status: needs-review` |
| Reused undocumented primitive that is surface-specific | Add INDEX entry with `scope: local` and a 2-line note explaining why no full spec is needed |
| Introduced a primitive that **duplicates** an existing registry entry | **Stop**. This is the failure mode the cascade exists to prevent. Revert and reuse the existing primitive, or escalate to refactor the existing primitive if the new requirement is legitimately broader |

### Reporting

When any of checks 1–4 trip, the agent must surface them explicitly to the
user before declaring task done — never silent. The recommended phrasing:

> Registry coherence check: <N> finding(s)
> - `DS_INDEX_DRIFT`: <Name> created without INDEX entry → resolved inline
> - `DS_COMPONENT_STALE`: <Name>.md not updated for variant `xyz` → follow-up CARD-NNNN opened
> - `DS_TOKENS_DRIFT`: `--color-action-secondary` added without tokens-reference entry → resolved inline

This makes drift **visible at the source of introduction**, which is the
only place it can be cheaply reconciled.

### Interaction with the weekly routine

The `ds-drift` routine (`framework/routines/ds-drift.routine.yml`) still
runs weekly and catches what slips through (e.g. changes made by tools that
bypass the agents, or by humans editing directly). But the per-intervention
check above is **first-line**: if it fires correctly, the weekly routine
should find very little to report.

## Bootstrap (when no registry exists)

If `features.has_design_system: false`:

- The BLOCKING reads above are skipped.
- Skills still respect `${paths.ui_guidelines}` and the project's existing
  components under `${paths.components_primitives}`.
- Recommend `/design-system-init` to the user when the absence of a registry
  is creating obvious drift (token hardcoding, duplicate primitives, divergent
  styling across pages).

The `design-system-init` skill scaffolds `INDEX.md`, `tokens-reference.md`,
and a `components/<Name>.md` file for every primitive already in the codebase,
then flips `features.has_design_system: true` in `baldart.config.yml`.

## Consumers of this protocol

The following files reference this module instead of duplicating its rules:

- `framework/.claude/agents/ui-expert.md`
- `framework/.claude/agents/code-reviewer.md`
- `framework/.claude/skills/ui-design/SKILL.md`
- `framework/.claude/skills/frontend-design/SKILL.md`
- `framework/.claude/skills/design-system-init/SKILL.md`
- `framework/.claude/commands/design-review.md`
- `framework/agents/design-review.md`

Additional consumers of the **Functional Traceability Gate** specifically
(unconditional, registry-independent):

- `framework/.claude/agents/visual-fidelity-verifier.md` (`resolved_orphans`
  carve-out — a dropped orphan is expected-absent, not `component-missing`)
- `framework/.claude/skills/e2e-review/SKILL.md` (passes `resolved_orphans`)
- `framework/.claude/skills/prd/references/discovery-phase.md` (mockup-intake
  affordance classification — the oracle's origin)
- `framework/.claude/skills/prd/references/ui-design-phase.md` +
  `framework/.claude/skills/ui-design/references/inventory.md` (the
  `function_ref` allowlist the gate enforces)

When changing the BLOCKING cascade, change it here — not in the consumers.

## See also

- `framework/agents/project-context.md` — always-ask / never-assume contract
  and the `baldart.config.yml` resolution flow.
- `framework/agents/design-review.md` — design-review workflow and report
  template (uses this protocol's step 0).
- `framework/.claude/skills/design-system-init/SKILL.md` — bootstrap a
  registry on a project that does not yet have one.
