Author the design language once in the canonical React designbook and project it
one-way onto each stack: React -> designbook/ -> ir/ -> adapters/<stack>/.
Phase 0 — contracts & governance (T01-T03):
- ir/SCHEMA.md + ir/schema/{component,tokens}.schema.json — neutral IR contract
(W3C DTCG tokens; React prop -> HTML attribute mapping; non-portable props flagged).
- adapters/ADAPTER_CONTRACT.md — inputs, drift-report + parity-result shapes,
idempotency rules, CI exit codes (0 ok / 2 usage / 3 drift / 4 parity / 5 internal).
- .claude/rules/designbook-propagation.md + DesignSystemIntroduction.md §5.1 —
one-way directionality + drift-resolution workflow.
T04 — canonical React designbook + the missing pull tool:
- The bundled /design-sync skill only PUSHES repo->cloud; it cannot populate
designbook/. Added scripts/designbook_pull.py + `make designbook-pull`, which drives
the local claude binary headless (acceptEdits) so DesignSync fetch+write runs in a
subprocess (contents never hit the orchestrator's context). Pulled 44 files;
excludes the _whynot-design-seed/ self-copy. Corrected the docs that wrongly called
/design-sync the pull.
T05 — IR extractor (scripts/ir-extract.mjs + `make ir`):
- ir/tokens.json (80 tokens, DTCG, var() -> {ref} alias resolution); ir/components/*.json
(10 contracts parsed from .jsx signatures: enum/boolean/number inference, prop->attr
map, style/callback marked non-portable); ir/exemplars/*.
T06 — Lit token adapter (adapters/lit/ + `make adapt-lit`):
- Full-gen tokens into src/styles/colors_and_type.css :root (marker-bounded, idempotent
no-op on re-run; hand-authored type CSS preserved).
NOTE: token regen synced Lit to canonical React — fonts IBM Plex -> system stacks and 8
status tokens added. This is a VISUAL change: review and run `pnpm test:visual:update`
before merge. Remaining: T07 scaffold+drift, T08 parity, T09 runbook, T10 2nd-adapter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.8 KiB
Adapter Contract
The contract every whynot stack adapter implements, so a new stack (Vue, Angular, Svelte, plain-CSS, …) is a drop-in. Part of WHYNOT-WP-0002. Lit (
adapters/lit/) is the reference implementation. Django (adapters/django/) predates the IR and is a hand-authored Layer-3 partial set, not an IR adapter — it is exempt until/unless migrated.
An adapter is scaffold + drift-detect, not full behavioural codegen:
- tokens → fully generated (deterministic, re-run is a no-op when unchanged),
- new component → a stub is generated (skeleton + typed inputs + a behaviour TODO),
- changed component → a drift report is emitted; the hand-authored source is never overwritten,
- appearance → verified against the designbook's own exemplars (parity).
Behaviour stays hand-authored per stack. The adapter keeps each component's contract and appearance aligned; it does not own its behaviour.
Inputs
The sole input is the committed IR (ir/, produced one-way from the React
designbook — see ir/SCHEMA.md):
| Input | What the adapter reads it for |
|---|---|
ir/tokens.json |
Token generation (W3C DTCG → stack-native token form). |
ir/components/<Name>.json |
Per-component contract: props, prop→attribute map, slots, events, variants. |
ir/exemplars/<Name>.{html,png} |
Reference render for visual parity. |
ir/schema/ |
The schemas the adapter may re-validate inputs against. |
An adapter MUST NOT write to ir/. Flow is one-way: React → IR → stacks.
Outputs
An adapter produces exactly three kinds of output:
-
Generated artifacts into the stack's own source tree.
- Tokens: fully generated, deterministic (e.g. Lit →
src/styles/colors_and_type.css). - New-component stubs: written to the stack's component tree, marked generated,
with a behaviour
TODO. A stub is created only when no hand-authored counterpart exists.
- Tokens: fully generated, deterministic (e.g. Lit →
-
A machine-readable drift report — one file per drifted component plus a roll-up. Lit writes these to
adapters/lit/drift/<Name>.md(human view) backed by a machine-readable summary. The report enumerates, per component:- props present in IR but missing on the element (and vice-versa),
- prop→attribute mismatches,
- missing / extra / renamed variants,
- removed props,
- non-portable props (
portable:false) surfaced explicitly — never dropped.
-
A parity result — a single structured outcome per
make parity-<stack>covering (a) contract parity (observed attributes/properties vs IR) and (b) visual parity (rendered component diffed againstir/exemplars/<Name>).
Drift report — minimal machine shape
{
"stack": "lit",
"generatedAt": "<iso8601>",
"irRef": "<git-sha-or-mtime of ir/>",
"components": [
{
"name": "Button",
"status": "drift", // "ok" | "new" | "drift" | "removed"
"issues": [
{ "kind": "prop-missing", "prop": "tone", "detail": "in IR, absent on <wn-button>" },
{ "kind": "attribute-mismatch","prop": "iconEnd", "expected": "icon-end", "actual": "iconend" },
{ "kind": "non-portable", "prop": "renderLabel", "detail": "type=function; cannot map to attribute" }
]
}
]
}
Parity result — minimal machine shape
{
"stack": "lit",
"generatedAt": "<iso8601>",
"components": [
{ "name": "Button", "contract": "pass", "visual": "pass", "diffRatio": 0.0009 }
],
"summary": { "total": 1, "contractFail": 0, "visualFail": 0 }
}
Idempotency rules
- Tokens regenerate fully. Running token generation twice on an unchanged
ir/tokens.jsonyields a byte-identical file (no-op diff). - Stubs are write-once. A stub is generated only when no hand-authored source exists. Once a human has touched a component, the adapter never re-writes it — it emits drift instead.
- Behaviour is never overwritten. No adapter output replaces hand-authored behaviour. The strongest action against an existing component is a drift report.
- Reports are overwritten, not appended. Drift and parity outputs are snapshots of the current IR-vs-source state; each run replaces the previous.
- No network, no
ir/writes. Adapters are pure functions ofir/+ the stack source tree.
Exit codes (for CI)
Every adapter command (make adapt-<stack>, make parity-<stack>) follows the
same convention so pipelines can branch uniformly:
| Code | Meaning |
|---|---|
0 |
Success. Tokens/stubs generated; no drift; parity passed. |
2 |
Usage / configuration error (bad args, missing ir/, malformed input). |
3 |
Drift detected. Generation succeeded but one or more components drifted from the IR. Non-fatal by default; use to gate a review. |
4 |
Parity failure. A contract or visual parity check failed. |
5 |
Internal adapter error (unexpected exception). |
Codes are additive in spirit but a command returns the highest applicable
code (e.g. drift + parity failure → 4). make designbook-refresh (T09) treats
3 as "stop for human drift triage" and 4 as "fail".
Implementing a new adapter — checklist
- Create
adapters/<stack>/with aREADME.mdpointing back to this contract. - Implement token generation from
ir/tokens.json→ the stack's token form (full gen, deterministic). - Implement stub generation from
ir/components/<Name>.jsonusing the prop→attribute map (respectattribute:falseandportable:false). - Implement drift detection against existing hand-authored sources → the drift report shape above.
- Implement
make parity-<stack>→ the parity result shape above, reusing the Playwright harness where the stack renders to HTML. - Wire
make adapt-<stack>andmake parity-<stack>; honour the exit codes.
See adapters/lit/ for the reference implementation.