# `_schema/` — design-system contracts

This directory codifies the structural contracts for design systems.
`tokens.schema.ts` is the token contract that every tokenized brand under
`design-systems/<brand>/` must satisfy. `manifest.schema.ts` is the
project contract for folders that opt into the Design System Project
shape by adding `manifest.json`; legacy `DESIGN.md`-only folders remain
valid until they are migrated.

```
_schema/
├── manifest.schema.ts ← project manifest schema (TS, machine-enforced when present)
├── tokens.schema.ts   ← canonical token schema (TS, machine-enforced)
├── defaults.css       ← A2 fallback values (CSS, human reference)
└── AGENTS.md          ← this file
```

The TypeScript schemas are the source of truth. `defaults.css` is a
human-readable mirror of the A2 `fallback` fields in `tokens.schema.ts`
and exists so that reviewers can scan real CSS without parsing a TS
array — drift between the two is enforced by the `design-system: A2
defaults parity` guard. Manifest shape is enforced by
`scripts/check-design-system-manifests.ts` for any
`design-systems/<brand>/manifest.json` that exists.

## Project manifest contract

Design System Project folders use fixed v1 file names:

- `manifest.json` — machine-readable project entry.
- `DESIGN.md` — canonical design prose.
- `tokens.css` — canonical compiled tokens.
- `components.html` — optional standalone component fixture.
- `assets/` — optional brand assets.
- `preview/` — optional static preview pages.
- `USAGE.md` — optional agent-facing package guide.
- `components.manifest.json` — optional rebuildable cache derived from
  `components.html` and `tokens.css`.
- `fonts/` — optional webfont files.
- `source/` — optional importer evidence (`scanned-files.json`,
  `evidence.md`, `tokens.source.json`, and `snippets/INDEX.json`).

The manifest guard validates only folders that ship `manifest.json`; it
does not require the bundled catalog to migrate all at once. Rich import
fields are structural in PR0: when declared, paths must be safe and present,
JSON indexes must parse, and committed `components.manifest.json` files must
match a fresh derivation from `components.html` plus `tokens.css`. Runtime
prompt composition and picker behavior are unchanged until later PRs consume
those fields.

## Four layers, two questions

Every shared token answers two questions:

1. **Who decides the value?** — the brand author (Layer A) or the
   schema author (Layer B-slot, when the brand has no opinion).
2. **What happens if the brand omits it?** — required, fallback, or
   alias.

The four layers fall out of those answers:

| Layer | Who decides | If omitted | Examples |
| --- | --- | --- | --- |
| **A1-identity** | brand | guard fails | `--bg`, `--fg`, `--accent`, `--font-display` |
| **A1-structure** | brand | guard fails | type scale, `--container-max`, `--section-y-*` |
| **A2** | brand (with fallback) | guard fails today; derive script fills tomorrow | `--motion-fast`, `--success`, `--space-4`, `--font-mono` |
| **B-slot** | brand or schema-suggested alias | guard fails — brand must declare, either as `var(--sibling)` (collapsed) or independent value (richer) | `--fg-2 → var(--fg)`, `--surface-warm → var(--surface)` |

Brand-specific tokens that fall outside the shared schema are tracked
as **C-extensions** in `BRAND_EXTENSIONS` (per-brand allowlist) or
`BRAND_EXTENSION_PREFIXES` (global prefix allowlist for whole families
like `--tag-bg-*`).

## Why A2 fails the guard today (and might not later)

A2 conceptually means "optional with fallback" — but artifacts are
generated by agents pasting one brand's `:root` block into a single
`<style>`. There is no global stylesheet that loads alongside the
brand, so a missing `--motion-fast` resolves to nothing inside the
artifact and any `transition: var(--motion-fast)` rule silently breaks.

Until the derive script (PR-B) lands and inlines `defaults.css` values
into every brand's `tokens.css`, the only safe contract is "every
brand must declare every A2 token". The `design-system: A2 required
tokens` guard enforces that strictly.

After the derive script ships, brand authors only need to write the
A1 tokens (and any A2 they want to override); the script populates A2
slots from `defaults.css`. The guard contract does not change — every
final `tokens.css` still contains every A2 token — but the work
shifts from human author to script.

## Why B-slot is required (and what the alias is for)

Same artifact-paste constraint applies to B-slot tokens. Shared
components reference richer tiers via `var(--fg-2)`, `var(--meta)`,
`var(--surface-warm)`, `var(--border-soft)` — if a brand omits the
slot, those references resolve to nothing and the artifact silently
breaks.

The `aliasTo` field on each B-slot entry is the **schema-suggested
default**, not a runtime fallback. A brand with no opinion on the
richer tier copies the alias verbatim into its `:root`:

```
--fg-2: var(--fg);                /* default brand: 2-level fg */
--surface-warm: var(--surface);   /* default brand: 2-level surface */
```

A brand that does have the richer tier binds an independent value:

```
--fg-2: #3d3d3a;                  /* kami brand: dark warm */
--surface-warm: #e8e6dc;          /* kami brand: warm sand */
```

Either form satisfies the `design-system: B-slot required tokens`
guard. The pre-derive-script contract is identical to A2: every
brand's `:root` declares every shared slot.

## C → B-slot → A2 promotion path

Brand-specific tokens start in `BRAND_EXTENSIONS[brand]`. They earn
promotion when a second brand needs the same name:

```
C-extension                    B-slot                       A2
(one brand declares it)        (multiple brands declare,    (every brand declares
                                some alias to a sibling)     with a sensible default)

kami: --leading-display    →   schema: --leading-display    →   schema: --leading-display
                               aliasTo: var(--leading-tight)    fallback: 1.1
```

Concrete promotion rules:

1. **C → B-slot** when **≥2 brands** declare a token of the same name
   *and* there is a meaningful sibling to alias to for brands that
   lack the richer tier. Move the entry from `BRAND_EXTENSIONS` to
   `TOKEN_SCHEMA` with `layer: "B-slot"` and `aliasTo: "var(--sibling)"`.
2. **C → A2** when **≥2 brands** declare a token of the same name
   *and* a defensible cross-brand fallback exists (no aliasing
   needed). Move to `TOKEN_SCHEMA` with `layer: "A2"` and a `fallback`,
   then mirror the value in `defaults.css`.
3. **B-slot → A2** when a B-slot starts being independently bound by
   ≥2 brands (instead of aliasing). Replace `aliasTo` with `fallback`
   and add a defaults.css declaration.
4. **A2 → A1** is rare. It happens when the previously-defaultable
   value turns out to be brand-determining — e.g. if a future brand
   redefines `--motion-base` from 200ms to 50ms because its identity
   is "instant", and that change ripples meaningfully through the
   brand voice. Drop the `fallback` and reclassify.

Demotion (A → B → C) is not currently supported. A token that is
genuinely no longer needed should be marked `@deprecated` in the
schema for one release and then deleted from every brand's
`tokens.css` in the same PR.

## When **not** to add a token

Schema growth has a cost — every new entry forces 138 brands to
declare or alias the new name when the derive script next runs.
Resist adding tokens that are:

- **Component-internal**: a `.btn-primary` background offset that no
  other component will ever read. Inline the value in the component
  rule.
- **One-off**: a single layout's hero crop ratio. Not a token.
- **Speculative**: "we might want a `--motion-slow` someday." Add it
  the first time a real interaction needs it, not before.
- **Already expressible**: a `--accent-tint-50` that resolves to
  `color-mix(in oklab, var(--accent), transparent 50%)`. Inline the
  `color-mix(...)` call until ≥2 components need the same tint with
  the same alpha, then promote to a token.

## Editing this directory

When you change `tokens.schema.ts`:

- Run `pnpm guard` and confirm both `default` and `kami` still pass
  every design-system sub-check.
- If you added an A2 entry: also update `defaults.css` with the
  matching declaration, byte-equivalent to the `fallback` field.
- If you renamed a token: bump every brand's `tokens.css` and the
  matching `components.html` `:root` paste in the same commit.
  Otherwise the drift guard will fail.
- If you removed a token from `TOKEN_SCHEMA` and the same name now
  appears in only one brand: add it to that brand's
  `BRAND_EXTENSIONS` entry so the unknown-token guard does not fail.

## Open questions deferred to PR-B

This PR codifies the schema and enforces it on hand-authored brands.
The following questions are intentionally not answered here:

- **How does the derive script source A1 values from `DESIGN.md`?**
  Some sections (color palette, type scale) parse cleanly; others
  (visual atmosphere, do's and don'ts) do not. A frontmatter or
  fenced-block convention will likely emerge.
- **What happens when a brand's `DESIGN.md` contradicts itself?**
  e.g. accents listed as both cobalt and indigo. The derive script
  will need a deterministic resolution (last-wins, manual override
  flag, or hard fail).
- **Are A2 fallback formulas stable when re-derived?** Bit-for-bit
  reproducibility of the script's output is required so that running
  the script twice on the same input does not churn 138 files.

These will be addressed in the PR that introduces
`scripts/derive-tokens-css.ts`.
