* Add design system import manifest schema * Generate hybrid design system imports * Read design system usage and cached manifests * Add design system pull-file tool * Show design system package evidence * Wire design system import semantics * Add design system package quality guard --------- Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
9.5 KiB
_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 fromcomponents.htmlandtokens.css.fonts/— optional webfont files.source/— optional importer evidence (scanned-files.json,evidence.md,tokens.source.json, andsnippets/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:
- Who decides the value? — the brand author (Layer A) or the schema author (Layer B-slot, when the brand has no opinion).
- 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:
- 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_EXTENSIONStoTOKEN_SCHEMAwithlayer: "B-slot"andaliasTo: "var(--sibling)". - 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_SCHEMAwithlayer: "A2"and afallback, then mirror the value indefaults.css. - B-slot → A2 when a B-slot starts being independently bound by
≥2 brands (instead of aliasing). Replace
aliasTowithfallbackand add a defaults.css declaration. - 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-basefrom 200ms to 50ms because its identity is "instant", and that change ripples meaningfully through the brand voice. Drop thefallbackand 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-primarybackground 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-slowsomeday." Add it the first time a real interaction needs it, not before. - Already expressible: a
--accent-tint-50that resolves tocolor-mix(in oklab, var(--accent), transparent 50%). Inline thecolor-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 guardand confirm bothdefaultandkamistill pass every design-system sub-check. - If you added an A2 entry: also update
defaults.csswith the matching declaration, byte-equivalent to thefallbackfield. - If you renamed a token: bump every brand's
tokens.cssand the matchingcomponents.html:rootpaste in the same commit. Otherwise the drift guard will fail. - If you removed a token from
TOKEN_SCHEMAand the same name now appears in only one brand: add it to that brand'sBRAND_EXTENSIONSentry 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.mdcontradicts 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.