Skip to content

Latest commit

Β 

History

History
226 lines (184 loc) Β· 10.7 KB

File metadata and controls

226 lines (184 loc) Β· 10.7 KB

_schema/ β€” design-system contracts

This directory codifies the structural contracts for design systems. tokens.schema.ts re-exports the token contract that every tokenized brand under design-systems/<brand>/ must satisfy. The canonical runtime copy lives in packages/contracts/src/design-systems/token-schema.ts so daemon importers and repo guards consume one schema. manifest.schema.ts is the project contract for Design System Project packages. Every bundled catalog entry now ships manifest.json, DESIGN.md, and tokens.css; the daemon keeps DESIGN.md-only discovery solely as a compatibility path for older or user-installed folders.

_schema/
β”œβ”€β”€ manifest.schema.ts ← project manifest schema (TS, machine-enforced when present)
β”œβ”€β”€ runtime.schema.ts  ← structured component/intent contract re-export
β”œβ”€β”€ tokens.schema.ts   ← token schema re-export (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 the token schema 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.
  • design-tokens.json β€” optional Design Tokens JSON derived from tokens.css + source/token-contract.report.json.
  • tailwind-v4.css β€” optional Tailwind v4 @theme CSS derived from tokens.css; it must not redefine source values independently.
  • 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, token-contract.report.json, and snippets/INDEX.json).
  • manifests/, components/, and rules/ β€” optional structured runtime graph, declared atomically through manifest.runtime.

The manifest guard validates every bundled package through its manifest.json. When rich fields are 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. These fields are also runtime inputs: catalog discovery consumes manifest metadata; prompt composition consumes usage, tokens, component information, import mode, craft bindings, and the manifest-derived pull index; package/static-file routes expose only declared preview and source files. Legacy DESIGN.md-only folders remain outside this manifest guard because they have no manifest to validate.

The runtime schema itself is canonical in packages/contracts/src/design-systems/runtime-schema.ts and re-exported here so the repository guard and daemon share schema versions and reference rules.

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 a future derive script 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 every bundled brand 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 the token schema:

  • Run pnpm guard and confirm the bundled catalog still passes 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 for the future derive script

The checked-in schema and guards enforce the final token contract today, but the following authoring-automation questions are intentionally not answered:

  • 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 the brand token files.

These remain future work for a change that introduces scripts/derive-tokens-css.ts; that script does not exist yet.