feat(refresh): make designbook-refresh orchestrator + drift-triage runbook (WHYNOT-WP-0002 T09)

scripts/designbook-refresh.mjs chains the automatable steps
(check->pull->sync->ir->adapt-lit->parity) and stops for the human drift-triage
step, propagating the adapter-contract exit codes (3=stop for triage, 4=parity
fail). Gating steps call the node scripts directly so make doesn't collapse the
3/4 codes to 2. Best-effort steps (check/pull/sync) warn and continue; --no-pull
/--no-check/--no-parity flags. Documented the loop in stack-and-commands.md and a
step-6 drift-resolution runbook in designbook/README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 09:19:44 +02:00
parent e4e3fe069c
commit 7cf524137f
4 changed files with 130 additions and 0 deletions

View File

@@ -18,10 +18,38 @@ npx playwright test -g "inbox" # run a single visual test by name
make # list make targets (help is the default goal)
make designbook-sync # after a /design-sync pull, record changes + last-sync time → RecentChanges.md
make designbook-check # ask Claude Design (via llm-connect) if the cloud is newer; warn if mirror is stale
make ir # extract the technology-neutral IR (ir/) from designbook/ (React → IR)
make adapt-lit # project IR onto Lit: regen tokens + scaffold stubs + drift reports (exit 3 on drift)
make parity-lit # render every <wn-*> (Playwright) + assert contract/visual parity (exit 4 on fail)
make designbook-refresh # the refresh loop: check→pull→sync→ir→adapt-lit→(drift triage)→parity. ARGS="--no-pull" etc.
make recent-changes # regenerate RecentChanges.md (alias; ARGS="--range main..HEAD" supported)
make sync-styles # = node scripts/sync-shared-styles.mjs
```
### Keeping Lit current with the designbook — the refresh loop (WHYNOT-WP-0002)
`make designbook-refresh` is the single routine that re-propagates a cloud designbook
change down to the Lit stack. It runs the automatable steps and **stops for you** when
drift needs a human decision:
```
1 designbook-check → has the cloud moved? (best-effort: needs llm-connect)
2 designbook-pull → pull React designbook→designbook/ (best-effort: needs `claude` binary)
3 designbook-sync → record the diff → RecentChanges.md
4 ir → re-extract ir/ (review the git diff — the blueprint change)
5 adapt-lit → regen tokens, scaffold new stubs, emit drift reports
6 you resolve drift ← STOP if step 5 exits 3 (adapters/lit/drift/*.md)
7 parity-lit → confirm contract + visual parity
```
Exit codes propagate the adapter contract: **3** = stop for drift triage (step 6),
**4** = parity failure. Steps 13 are best-effort (a network/`claude`/llm-connect
gap warns and continues; the IR just re-extracts from the current mirror). After
resolving drift, re-run `make designbook-refresh --no-pull` (via `ARGS=`) to re-check
and reach parity. Drift resolution itself is governed by
`.claude/rules/designbook-propagation.md` (fix the stack, or change the language in
Claude Design and re-propagate — never a stack→React back-edit).
There is no unit-test suite — correctness is verified by full-page Playwright screenshot diffs of the two `examples/` pages (`tests/visual/ui-kit.spec.mjs`, `maxDiffPixelRatio: 0.005`). Any visual change needs `pnpm test:visual:update` + baseline review.
## Integrating the designbook

View File

@@ -37,6 +37,9 @@ adapt-lit: ## Project the IR onto the Lit stack: regen tokens (full gen), scaffo
parity-lit: ## Confirm Lit elements honour the IR contract + render (browser). Exit 4 on parity failure.
$(NODE) adapters/lit/parity.mjs
designbook-refresh: ## Refresh routine: check->pull->sync->ir->adapt-lit->(drift triage)->parity. ARGS=--no-pull etc.
$(NODE) scripts/designbook-refresh.mjs $(ARGS)
recent-changes: ## Regenerate RecentChanges.md (alias of the reporter; --range supported).
$(NODE) scripts/designbook-sync.mjs $(ARGS)

View File

@@ -8,6 +8,32 @@ build scripts; `tokens/` and `src/styles/` in the repo root are *derived* from i
See `DesignSystemIntroduction.md` §1 (three places) and §5 (the atelier → repo hop),
and `RecentChanges.md` (regenerated by `make designbook-sync`) for the last diff.
## Refresh runbook — propagating a designbook change to Lit (WHYNOT-WP-0002)
When the cloud designbook moves, run **`make designbook-refresh`** — it chains
check → pull → record → `make ir``make adapt-lit` → (drift triage) → `make
parity-lit` and stops when a human decision is needed. See
`.claude/rules/stack-and-commands.md` for the step list and
`.claude/rules/designbook-propagation.md` for the one-way governance.
**Step 6 — resolving drift (the human step).** When `make adapt-lit` exits `3`,
the refresh halts and points you at `adapters/lit/drift/<Name>.md`. For each
**actionable** issue (informational `non-portable`/`prop-extra` are not gated):
| drift kind | what it means | how to resolve |
|---|---|---|
| `attribute-mismatch` | the Lit property reflects a different attribute than the IR contract | rename the Lit `attribute:` to match the IR, or — if the *language* is what's wrong — change it in Claude Design and re-propagate |
| `prop-missing` | the IR contract has a prop the `<wn-*>` element lacks | add the reactive property + behaviour to the element, **or** if the element models it differently (e.g. a slot, or state on a child), change the React designbook so the contract matches reality |
| `variant-axis-missing` | an IR variant axis has no backing Lit property | add the variant property, or correct the axis in Claude Design |
| `tag-mismatch` | the IR contract's tag has no element; a near-named one exists (e.g. `wn-pipeline-strip` vs the hand-authored `wn-pipeline`) | decide the canonical name **in Claude Design** and re-propagate, then realign the element — do not silently rename only the stack |
**Never** resolve drift by editing `ir/` or back-editing React from the stack —
that desyncs the canonical source (see `designbook-propagation.md`). After
resolving, re-run `make designbook-refresh --no-pull` to confirm `adapt-lit` is
clean and `parity-lit` passes (exit `0`). New components get a write-once stub in
`adapters/lit/stubs/<Name>.js` — move it into `src/elements/`, implement the
behaviour, register it, and re-run.
## How it syncs
The designbook is a cloud project of type `PROJECT_TYPE_DESIGN_SYSTEM`. Sync is

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env node
// =============================================================
// designbook-refresh.mjs — the refresh orchestrator (WHYNOT-WP-0002 · T09)
//
// One routine to keep the Lit stack current with the canonical React designbook.
// Runs the *automatable* steps (15, 7) and stops for the *human* step (6) when
// drift is detected, honouring the adapter-contract exit codes:
//
// 1. make designbook-check — has the cloud designbook moved? (best-effort)
// 2. make designbook-pull — pull the React designbook → designbook/ (best-effort)
// 3. make designbook-sync — record the diff → RecentChanges.md (best-effort)
// 4. make ir — re-extract the IR (the blueprint) (gate)
// 5. make adapt-lit — tokens + scaffold + drift (drift gate → 3)
// 6. (human) resolve drift — adapters/lit/drift/*.md ← STOP here on drift
// 7. make parity-lit — contract + visual parity (parity gate → 4)
//
// Best-effort steps (13) need network / the local `claude` binary / llm-connect;
// their failure warns and continues (the IR re-extracts from the current mirror).
// Steps 4/5/7 are deterministic and gate. Exit: highest applicable adapter code —
// 0 ok · 2 usage · 3 drift (stop for triage) · 4 parity failure · 5 internal.
//
// Flags: --no-check --no-pull --no-parity (skip the matching step)
// =============================================================
import { spawnSync } from "node:child_process";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
const args = new Set(process.argv.slice(2));
function run(label, cmd, { gate = false } = {}) {
console.log(`\n\x1b[1m▶ ${label}\x1b[0m (${cmd})`);
const r = spawnSync(cmd, { cwd: REPO, shell: true, stdio: "inherit" });
const code = r.status == null ? 5 : r.status;
if (code !== 0 && !gate) console.log(`\x1b[33m ↪ step exited ${code} — best-effort, continuing.\x1b[0m`);
return code;
}
function done(code, msg) {
console.log(`\n\x1b[1m${code === 0 ? "✓" : "■"} designbook-refresh: ${msg}\x1b[0m`);
process.exit(code);
}
// 13: best-effort (never abort the refresh).
if (!args.has("--no-check")) run("1/7 cloud-ahead check", "make designbook-check");
if (!args.has("--no-pull")) run("2/7 pull React designbook", "make designbook-pull");
run("3/7 record diff (RecentChanges.md)", "make designbook-sync");
// Gating steps call the node scripts DIRECTLY (not via make, which collapses any
// recipe failure to exit 2 and would hide the adapter's 3/4 drift/parity codes).
const N = JSON.stringify(process.execPath);
// 4: IR extraction — deterministic gate; without it nothing downstream is valid.
const irCode = run("4/7 extract IR", `${N} scripts/ir-extract.mjs`, { gate: true });
if (irCode !== 0) done(irCode === 5 ? 5 : 2, `IR extraction failed (exit ${irCode}). Fix the designbook/ source before refreshing.`);
// 5: adapt-lit — drift gate. Exit 3 ⇒ stop for human triage (step 6).
const adaptCode = run("5/7 adapt Lit (tokens + scaffold + drift)", `${N} adapters/lit/adapt.mjs`, { gate: true });
if (adaptCode === 3) {
done(3, "DRIFT detected (step 6 is yours). Resolve adapters/lit/drift/*.md per " +
".claude/rules/designbook-propagation.md, then re-run `make designbook-refresh --no-pull` to re-check + run parity.");
}
if (adaptCode !== 0) done(adaptCode === 2 ? 2 : 5, `adapt-lit failed (exit ${adaptCode}).`);
// 6: human drift resolution — only reached when adapt-lit is clean.
// 7: parity gate.
if (args.has("--no-parity")) done(0, "tokens + scaffold clean; parity skipped (--no-parity).");
const parityCode = run("7/7 parity (contract + visual)", `${N} adapters/lit/parity.mjs`, { gate: true });
if (parityCode === 4) done(4, "PARITY FAILURE — see adapters/lit/parity/_parity.json.");
if (parityCode !== 0) done(parityCode === 2 ? 2 : 5, `parity-lit failed (exit ${parityCode}).`);
done(0, "in sync — IR extracted, Lit adapted with no drift, parity passed.");