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:
@@ -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 1–3 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
|
||||
|
||||
3
Makefile
3
Makefile
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
73
scripts/designbook-refresh.mjs
Normal file
73
scripts/designbook-refresh.mjs
Normal 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 (1–5, 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 (1–3) 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);
|
||||
}
|
||||
|
||||
// 1–3: 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.");
|
||||
Reference in New Issue
Block a user