Files
whynot-design/adapters/ADAPTER_CONTRACT.md
tegwick 0d688ca94a feat(designbook): technology-neutral IR + stack-adapter pipeline (WHYNOT-WP-0002 T01-T06)
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>
2026-06-24 12:36:24 +02:00

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:

  1. 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.
  2. 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.
  3. 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 against ir/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

  1. Tokens regenerate fully. Running token generation twice on an unchanged ir/tokens.json yields a byte-identical file (no-op diff).
  2. 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.
  3. Behaviour is never overwritten. No adapter output replaces hand-authored behaviour. The strongest action against an existing component is a drift report.
  4. Reports are overwritten, not appended. Drift and parity outputs are snapshots of the current IR-vs-source state; each run replaces the previous.
  5. No network, no ir/ writes. Adapters are pure functions of ir/ + 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

  1. Create adapters/<stack>/ with a README.md pointing back to this contract.
  2. Implement token generation from ir/tokens.json → the stack's token form (full gen, deterministic).
  3. Implement stub generation from ir/components/<Name>.json using the prop→attribute map (respect attribute:false and portable:false).
  4. Implement drift detection against existing hand-authored sources → the drift report shape above.
  5. Implement make parity-<stack> → the parity result shape above, reusing the Playwright harness where the stack renders to HTML.
  6. Wire make adapt-<stack> and make parity-<stack>; honour the exit codes.

See adapters/lit/ for the reference implementation.