diff --git a/.claude/rules/stack-and-commands.md b/.claude/rules/stack-and-commands.md index 0250439..1260e40 100644 --- a/.claude/rules/stack-and-commands.md +++ b/.claude/rules/stack-and-commands.md @@ -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 (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 diff --git a/Makefile b/Makefile index 5713549..eb2343f 100644 --- a/Makefile +++ b/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) diff --git a/designbook/README.md b/designbook/README.md index d0cc82c..eb5d8bc 100644 --- a/designbook/README.md +++ b/designbook/README.md @@ -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/.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 `` 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/.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 diff --git a/scripts/designbook-refresh.mjs b/scripts/designbook-refresh.mjs new file mode 100644 index 0000000..50a848b --- /dev/null +++ b/scripts/designbook-refresh.mjs @@ -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.");