diff --git a/.claude/rules/stack-and-commands.md b/.claude/rules/stack-and-commands.md index 1260e40..0250439 100644 --- a/.claude/rules/stack-and-commands.md +++ b/.claude/rules/stack-and-commands.md @@ -18,38 +18,10 @@ 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/.custodian-brief.md b/.custodian-brief.md index ba036ef..b5cdc27 100644 --- a/.custodian-brief.md +++ b/.custodian-brief.md @@ -2,7 +2,7 @@ # Custodian Brief — whynot-design **Domain:** infotech -**Last synced:** 2026-06-30 07:54 UTC +**Last synced:** 2026-06-28 22:57 UTC **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* ## Active Workstreams diff --git a/.forgejo/workflows/ci-smoke.yaml b/.forgejo/workflows/ci-smoke.yaml deleted file mode 100644 index bd44c56..0000000 --- a/.forgejo/workflows/ci-smoke.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Canonical CI smoke template (tier 1 routing drill). -# Copy to: .forgejo/workflows/ci-smoke.yaml in consumer repos. -name: CI Smoke - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - host-smoke: - runs-on: self-hosted - steps: - - name: Routing probe (host runner) - run: | - set -eu - echo "repository=${GITHUB_REPOSITORY:-unknown}" - echo "sha=${GITHUB_SHA:-unknown}" - echo "runner=${RUNNER_NAME:-unknown}" - uname -a - - container-smoke: - runs-on: ubuntu-latest - steps: - - name: Routing probe (container label) - run: | - set -eu - echo "container-smoke ok for ${GITHUB_REPOSITORY:-unknown}" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3b1f325..25bd05c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,3 @@ __pycache__ # Editor .vscode .idea - -# Adapter parity render-smoke screenshots (regenerated by make parity-lit; the -# committed result is adapters/lit/parity/_parity.json) -adapters/lit/parity/*.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 9931009..d86ecb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,40 +6,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version ## [Unreleased] -### Added - -- **Lit adapter completed + refresh pipeline** (WHYNOT-WP-0002, Phases 3–6). The - one-way `React → designbook/ → ir/ → adapters/lit` pipeline is now end-to-end: - - `make adapt-lit` gains **component scaffold + drift** (T07): parses - `src/elements/*.js`, compares each `` to its IR contract, and writes - per-component drift reports + a machine roll-up (`adapters/lit/drift/`), with - write-once stubs (`adapters/lit/stubs/`) for new components — never overwriting - hand-authored sources. Severity split: actionable drift gates (exit 3); - `non-portable`/`prop-extra` are informational. - - `make parity-lit` (T08): renders every `` in a real browser and asserts - **contract + visual parity**, writing `adapters/lit/parity/_parity.json` - (exit 4 on failure). - - `make designbook-refresh` (T09): the refresh orchestrator - (check→pull→sync→ir→adapt-lit→drift-triage→parity) honouring the adapter - exit-code contract, plus a drift-resolution runbook in `designbook/README.md`. - - `make adapt-plain-css` (T10): a deliberately-unfinished second-adapter **smoke** - proving the IR/adapter seam — a non-Lit adapter consuming the same `ir/` with the - same contract shapes and zero `ir/` changes. -- **Drift triage resolved — `make designbook-refresh` is green** (0 drift, parity pass). - The three surfaced divergences were resolved without any stack→React back-edit: - a documented `TAG_OVERRIDES` in the extractor maps `PipelineStrip → wn-pipeline` - (the tag is an IR-projection detail); the drift detector now recognises a prop - honoured by a same-named named slot (`` `actions`); and an auditable - `adapters/lit/drift.accepted.json` registry records the intentional Sidebar - composition divergence (`current` key ↔ per-item `active`) as a justified, - non-gating note. - -## [0.4.1] — 2026-07-03 - -Republished to the coulomb Gitea npm registry through the native -`secrets-engine exec` pilot closeout (SECRETS-WP-0003). No package content -changes from `0.4.0`. - ## [0.4.0] — 2026-06-28 ### Fixed diff --git a/Makefile b/Makefile index 0aca222..a04c9d6 100644 --- a/Makefile +++ b/Makefile @@ -34,15 +34,6 @@ ir: ## Extract the technology-neutral IR (ir/) from the designbook mirror. One-w adapt-lit: ## Project the IR onto the Lit stack: regen tokens (full gen), scaffold + drift (T07). $(NODE) adapters/lit/adapt.mjs -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) - -adapt-plain-css: ## Second-adapter SMOKE: prove a non-Lit adapter consumes the same ir/ (WHYNOT-WP-0002 T10). - $(NODE) adapters/plain-css/adapt.mjs - recent-changes: ## Regenerate RecentChanges.md (alias of the reporter; --range supported). $(NODE) scripts/designbook-sync.mjs $(ARGS) diff --git a/RecentChanges.md b/RecentChanges.md index e8ce232..25c512a 100644 --- a/RecentChanges.md +++ b/RecentChanges.md @@ -2,7 +2,7 @@ Snapshot of the last designbook integration. Regenerated by `make designbook-sync`. -- Generated: 2026-06-30T07:48:39Z +- Generated: 2026-06-23T19:25:28Z - Compared: working tree (uncommitted) - Last /design-sync: 2026-06-23T19:25:28Z @@ -10,6 +10,48 @@ Snapshot of the last designbook integration. Regenerated by `make designbook-syn > into `CHANGELOG.md` under `## [Unreleased]` before releasing; that is the file CI > enforces (`pnpm check`). The designbook itself is synced via `/design-sync`, not this script. -## No changes +## Changed files -The design surface is unchanged since the last sync. \ No newline at end of file +### Designbook +- `ADDED ` designbook/_ds_bundle.js (+1396 / -0) +- `ADDED ` designbook/_ds_manifest.json (+1 / -0) +- `ADDED ` designbook/.design-pull.json (+18 / -0) +- `ADDED ` designbook/colors_and_type.css (+293 / -0) +- `ADDED ` designbook/preview/brand-iconography.html (+36 / -0) +- `ADDED ` designbook/preview/brand-lockups.html (+26 / -0) +- `ADDED ` designbook/preview/brand-logo.html (+21 / -0) +- `ADDED ` designbook/preview/brand-wireframe-motif.html (+43 / -0) +- `ADDED ` designbook/preview/colors-accent.html (+33 / -0) +- `ADDED ` designbook/preview/colors-borders.html (+25 / -0) +- `ADDED ` designbook/preview/colors-neutrals.html (+36 / -0) +- `ADDED ` designbook/preview/colors-signal.html (+25 / -0) +- `ADDED ` designbook/preview/colors-status-functional.html (+31 / -0) +- `ADDED ` designbook/preview/comp-buttons.html (+40 / -0) +- `ADDED ` designbook/preview/comp-empty-placeholder.html (+38 / -0) +- `ADDED ` designbook/preview/comp-inputs.html (+30 / -0) +- `ADDED ` designbook/preview/comp-labels-tags.html (+44 / -0) +- `ADDED ` designbook/preview/comp-left-nav.html (+85 / -0) +- `ADDED ` designbook/preview/comp-pipeline.html (+30 / -0) +- `ADDED ` designbook/preview/comp-prototype-card.html (+46 / -0) +- `ADDED ` designbook/preview/comp-topnav.html (+37 / -0) +- `ADDED ` designbook/preview/page-beta-invitation.html (+104 / -0) +- `ADDED ` designbook/preview/page-landing-auth.html (+224 / -0) +- `ADDED ` designbook/preview/page-prototype-detail.html (+158 / -0) +- `ADDED ` designbook/preview/page-signals-dashboard.html (+135 / -0) +- `ADDED ` designbook/preview/spacing-elevation.html (+26 / -0) +- `ADDED ` designbook/preview/spacing-radii.html (+24 / -0) +- `ADDED ` designbook/preview/spacing-scale.html (+28 / -0) +- `ADDED ` designbook/preview/type-body.html (+23 / -0) +- `ADDED ` designbook/preview/type-display.html (+23 / -0) +- `ADDED ` designbook/preview/type-headings.html (+20 / -0) +- `ADDED ` designbook/preview/type-mono-eyebrows.html (+42 / -0) +- `ADDED ` designbook/preview/type-serif-quote.html (+17 / -0) +- `ADDED ` designbook/REACT_CANONICAL_DECISION.md (+60 / -0) +- `ADDED ` designbook/styles.css (+9 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/Atoms.jsx (+102 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/Chrome.jsx (+163 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/data.jsx (+71 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/DocView.jsx (+102 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/index.html (+77 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/README.md (+31 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/Screens.jsx (+274 / -0) diff --git a/adapters/lit/README.md b/adapters/lit/README.md index 9c81187..828a2dd 100644 --- a/adapters/lit/README.md +++ b/adapters/lit/README.md @@ -13,35 +13,8 @@ Per the contract, an adapter is **scaffold + drift-detect**, never a rewrite: | Concern | Behaviour | Status | |---|---|---| | **Tokens** | **Fully generated** from `ir/tokens.json` into the `:root` block of `src/styles/colors_and_type.css`, between `@generated tokens` markers. Deterministic — re-running with an unchanged IR is a byte-identical no-op. The hand-authored type/utility CSS after the block is preserved. | **done (T06)** | -| **New component** | Generate a `` Lit stub (`adapters/lit/stubs/.js`) from the IR contract's prop→attribute map + a behaviour `TODO`. **Write-once** — into a staging dir, never the hand-authored tree; the human integrates it. | **done (T07)** | -| **Changed component** | Emit a **drift report** (`adapters/lit/drift/.md` + machine `_report.json`) — never overwrite the hand-authored element. | **done (T07)** | - -### Drift severity - -`make adapt-lit` exits `3` only on **actionable** drift — `prop-missing`, -`attribute-mismatch`, `variant-axis-missing`, `tag-mismatch`. **Informational** -issues do not gate: `non-portable` (React `style`/callbacks that inherently have -no attribute form — the Lit element is right to omit them) and `prop-extra` (the -Lit element is richer than the minimal React designbook). Resolve actionable drift -per `.claude/rules/designbook-propagation.md` (fix the stack, or change the language -in Claude Design and re-propagate — never a stack→React back-edit). - -## Parity — `make parity-lit` - -`adapters/lit/parity.mjs` renders every `` in a real browser (Playwright) and -writes `adapters/lit/parity/_parity.json` (the contract's parity-result shape): - -- **Contract parity** — each element must upgrade and carry no `attribute-mismatch` - vs its IR contract (computed statically, so no runtime type-coercion false - positives). A prop the element *lacks* is a coverage note (already surfaced as - drift), not a parity failure. -- **Visual parity** — a render smoke: the element renders non-empty with a positive - box; a screenshot is saved to `adapters/lit/parity/.png` (gitignored) as the - artifact. The `ir/exemplars/.html` are designbook **gallery cards** (a grid - of all variants), not single-component baselines, so an automated pixel diff - against them is not meaningful — per-component Lit appearance regression is owned - by the Playwright baseline suite (`tests/visual/`); the exemplar is the human - visual reference. Exit `4` on a contract or render failure. +| **New component** | Generate a `` Lit stub from the IR contract's prop→attribute map + a behaviour `TODO`. | T07 | +| **Changed component** | Emit a **drift report** (`adapters/lit/drift/.md`) — never overwrite the hand-authored element. | T07 | ## Directionality diff --git a/adapters/lit/adapt.mjs b/adapters/lit/adapt.mjs index aba1f6f..26bc388 100644 --- a/adapters/lit/adapt.mjs +++ b/adapters/lit/adapt.mjs @@ -20,7 +20,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync import { execSync } from "node:child_process"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import { parseLitElements, componentDrift, renderStub, loadAccepted } from "./scaffold.mjs"; +import { parseLitElements, componentDrift, renderStub } from "./scaffold.mjs"; const REPO = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const TOKENS_JSON = join(REPO, "ir", "tokens.json"); @@ -130,11 +130,10 @@ function runScaffold() { process.exit(2); } const byTag = parseLitElements(REPO); - const accepted = loadAccepted(REPO); const contracts = readdirSync(IR_COMPONENTS).filter((f) => f.endsWith(".json")) .map((f) => JSON.parse(readFileSync(join(IR_COMPONENTS, f), "utf8"))); - const results = contracts.map((c) => componentDrift(c, byTag, accepted)) + const results = contracts.map((c) => componentDrift(c, byTag)) .sort((a, b) => a.name.localeCompare(b.name)); // Per-component drift docs: snapshot, so wipe stale ones first (idempotency rule 4). diff --git a/adapters/lit/drift.accepted.json b/adapters/lit/drift.accepted.json deleted file mode 100644 index 38ed9b7..0000000 --- a/adapters/lit/drift.accepted.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "_comment": "Human-curated accepted divergences — the auditable output of drift triage (WHYNOT-WP-0002). An entry downgrades a specific drift issue to an informational, justified note so it does not gate make adapt-lit/parity-lit. Use ONLY for intentional React<->Lit modelling differences, never to silence a real defect. Keyed by component + drift kind + prop. See .claude/rules/designbook-propagation.md.", - "accepted": [ - { - "component": "Sidebar", - "kind": "prop-missing", - "prop": "current", - "rationale": "Composition divergence (intentional). The React Sidebar is monolithic and takes a `current` selection-key prop, comparing it against its own internal NAV_ITEMS. The Lit stack decomposes the sidebar into + + , modelling selection as per-item `active` state on the slotted children rather than a container-level key. There is no single `current` attribute to honour on ; the contract is satisfied compositionally. Reconcile upstream only if the React designbook is ever made composable." - }, - { - "component": "Sidebar", - "kind": "variant-axis-missing", - "prop": "current", - "rationale": "Same composition divergence as Sidebar.current above — the `current` variant axis is expressed as item-level `active` on , not as a property." - } - ] -} diff --git a/adapters/lit/drift/PageHeader.md b/adapters/lit/drift/PageHeader.md index d522ebe..16c3706 100644 --- a/adapters/lit/drift/PageHeader.md +++ b/adapters/lit/drift/PageHeader.md @@ -1,9 +1,9 @@ # Drift — PageHeader `` -**Status:** ok — ✓ in sync with the IR contract. +**Status:** drift — ⚠ drift detected — resolve per `.claude/rules/designbook-propagation.md`. | severity | kind | prop | detail | | --- | --- | --- | --- | -| info | prop-via-slot | `actions` | IR prop honoured by on (slotted content, not an attribute). | +| **drift** | prop-missing | `actions` | in IR (attribute 'actions'), absent on | | info | prop-extra | `hasActions` | on (attribute 'hasactions'), not in IR contract. | diff --git a/adapters/lit/drift/PipelineStrip.md b/adapters/lit/drift/PipelineStrip.md index 9c6fce0..25d2765 100644 --- a/adapters/lit/drift/PipelineStrip.md +++ b/adapters/lit/drift/PipelineStrip.md @@ -1,8 +1,8 @@ -# Drift — PipelineStrip `` +# Drift — PipelineStrip `` -**Status:** ok — ✓ in sync with the IR contract. +**Status:** drift — ⚠ drift detected — resolve per `.claude/rules/designbook-propagation.md`. | severity | kind | prop | detail | | --- | --- | --- | --- | -| info | prop-extra | `stages` | on (attribute 'stages'), not in IR contract. | +| **drift** | tag-mismatch | — | IR contract tag 'wn-pipeline-strip' has no element; 'wn-pipeline' looks like the hand-authored counterpart (rename — resolve in Claude Design or realign the element). | diff --git a/adapters/lit/drift/Sidebar.md b/adapters/lit/drift/Sidebar.md index e180adf..c52633d 100644 --- a/adapters/lit/drift/Sidebar.md +++ b/adapters/lit/drift/Sidebar.md @@ -1,11 +1,11 @@ # Drift — Sidebar `` -**Status:** ok — ✓ in sync with the IR contract. +**Status:** drift — ⚠ drift detected — resolve per `.claude/rules/designbook-propagation.md`. | severity | kind | prop | detail | | --- | --- | --- | --- | -| info | prop-missing | `current` | in IR (attribute 'current'), absent on | +| **drift** | prop-missing | `current` | in IR (attribute 'current'), absent on | +| **drift** | variant-axis-missing | `current` | IR variant axis 'current' (doc:) has no Lit property. | | info | non-portable | `onNav` | type=function; not attribute-mappable — handle explicitly, never drop. | -| info | variant-axis-missing | `current` | IR variant axis 'current' (doc:) has no Lit property. | | info | prop-extra | `activation` | on (attribute 'activation'), not in IR contract. | diff --git a/adapters/lit/drift/_report.json b/adapters/lit/drift/_report.json index 81c5b26..9f5fda7 100644 --- a/adapters/lit/drift/_report.json +++ b/adapters/lit/drift/_report.json @@ -1,7 +1,7 @@ { "stack": "lit", - "generatedAt": "2026-06-30T07:46:35.458Z", - "irRef": "756634c", + "generatedAt": "2026-06-30T07:01:40.219Z", + "irRef": "17f2ad9", "components": [ { "name": "Button", @@ -86,14 +86,14 @@ }, { "name": "PageHeader", - "status": "ok", + "status": "drift", "tag": "wn-page-header", "issues": [ { - "kind": "prop-via-slot", + "kind": "prop-missing", "prop": "actions", - "detail": "IR prop honoured by on (slotted content, not an attribute).", - "severity": "info" + "detail": "in IR (attribute 'actions'), absent on ", + "severity": "drift" }, { "kind": "prop-extra", @@ -105,28 +105,28 @@ }, { "name": "PipelineStrip", - "status": "ok", - "tag": "wn-pipeline", + "status": "drift", + "tag": "wn-pipeline-strip", "issues": [ { - "kind": "prop-extra", - "prop": "stages", - "detail": "on (attribute 'stages'), not in IR contract.", - "severity": "info" + "kind": "tag-mismatch", + "expected": "wn-pipeline-strip", + "actual": "wn-pipeline", + "detail": "IR contract tag 'wn-pipeline-strip' has no element; 'wn-pipeline' looks like the hand-authored counterpart (rename — resolve in Claude Design or realign the element).", + "severity": "drift" } ] }, { "name": "Sidebar", - "status": "ok", + "status": "drift", "tag": "wn-sidebar", "issues": [ { "kind": "prop-missing", "prop": "current", "detail": "in IR (attribute 'current'), absent on ", - "severity": "info", - "accepted": "Composition divergence (intentional). The React Sidebar is monolithic and takes a `current` selection-key prop, comparing it against its own internal NAV_ITEMS. The Lit stack decomposes the sidebar into + + , modelling selection as per-item `active` state on the slotted children rather than a container-level key. There is no single `current` attribute to honour on ; the contract is satisfied compositionally. Reconcile upstream only if the React designbook is ever made composable." + "severity": "drift" }, { "kind": "non-portable", @@ -138,8 +138,7 @@ "kind": "variant-axis-missing", "prop": "current", "detail": "IR variant axis 'current' (doc:) has no Lit property.", - "severity": "info", - "accepted": "Same composition divergence as Sidebar.current above — the `current` variant axis is expressed as item-level `active` on , not as a property." + "severity": "drift" }, { "kind": "prop-extra", diff --git a/adapters/lit/parity.mjs b/adapters/lit/parity.mjs deleted file mode 100644 index f0225fe..0000000 --- a/adapters/lit/parity.mjs +++ /dev/null @@ -1,174 +0,0 @@ -// ============================================================= -// adapters/lit/parity.mjs — make parity-lit (WHYNOT-WP-0002 · T08) -// -// The gate that confirms the Lit elements actually honour the IR contract. -// Renders each in a real browser (Playwright) and checks: -// -// (a) CONTRACT parity — for every IR-declared portable prop the element HAS, -// setting the IR-declared attribute must drive the property (no attribute -// contradiction). A prop the element lacks is a *coverage* note, not a -// contradiction — it is already surfaced by `make adapt-lit` drift (T07). -// (b) VISUAL parity — the element renders non-empty with a positive box; a -// screenshot is saved to adapters/lit/parity/.png as the artifact. -// -// On pixel-exact appearance: `ir/exemplars/.html` are designbook *gallery -// cards* (a grid of all variants), not single-component baselines, so a direct -// pixel diff against them is not meaningful. Per-component Lit appearance -// regression is owned by the Playwright baseline suite (tests/visual/). Visual -// parity here is a render smoke + artifact; the exemplar is the human reference. -// -// Result: adapters/lit/parity/_parity.json (adapters/ADAPTER_CONTRACT.md shape). -// Exit: 0 pass · 2 usage · 4 parity failure · 5 internal. -// ============================================================= -import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; -import { spawn } from "node:child_process"; -import { join, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { parseLitElements, componentDrift, loadAccepted } from "./scaffold.mjs"; - -const REPO = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); -const IR_COMPONENTS = join(REPO, "ir", "components"); -const OUT = join(REPO, "adapters", "lit", "parity"); -const PORT = 4399; - -async function loadChromium() { - for (const id of ["@playwright/test", "playwright", "playwright-core"]) { - try { return (await import(id)).chromium; } catch { /* next */ } - } - console.error("parity: Playwright not installed (need @playwright/test)."); process.exit(2); -} - -function waitForServer(url, tries = 30) { - return new Promise((resolve, reject) => { - const tick = (n) => fetch(url).then(() => resolve()).catch(() => { - if (n <= 0) return reject(new Error("server did not start")); - setTimeout(() => tick(n - 1), 200); - }); - tick(tries); - }); -} - -// Representative attribute values from the contract (to exercise rendering). -function fixtureAttrs(contract) { - const attrs = {}; - for (const p of contract.props || []) { - if (p.portable === false || p.attribute === false) continue; - if (p.type === "enum") attrs[p.attribute] = p.default ?? (p.enum && p.enum[0]) ?? ""; - else if (p.type === "number") attrs[p.attribute] = "1"; - else if (p.type === "boolean") attrs[p.attribute] = ""; - else attrs[p.attribute] = "Sample"; - } - return attrs; -} - -async function main() { - if (!existsSync(IR_COMPONENTS)) { console.error("No ir/components/ — run `make ir`."); process.exit(2); } - const contracts = readdirSync(IR_COMPONENTS).filter((f) => f.endsWith(".json")) - .map((f) => JSON.parse(readFileSync(join(IR_COMPONENTS, f), "utf8"))) - .sort((a, b) => a.name.localeCompare(b.name)); - - mkdirSync(OUT, { recursive: true }); - for (const f of readdirSync(OUT)) if (f.endsWith(".png")) rmSync(join(OUT, f)); - - const url = `http://127.0.0.1:${PORT}/examples/showcase/index.html`; - // Reuse an already-running static server (set by the caller / CI); only spawn - // and manage our own if none is up. Spawning is what some sandboxes dislike. - let server = null; - const alreadyUp = await fetch(url).then(() => true).catch(() => false); - if (!alreadyUp) { - server = spawn("python3", ["-m", "http.server", String(PORT), "--bind", "127.0.0.1"], - { cwd: REPO, stdio: "ignore" }); - } - const stopServer = () => { if (server) server.kill(); }; - const chromium = await loadChromium(); - let browser; - try { - await waitForServer(url); - browser = await chromium.launch({ args: ["--no-sandbox"] }); - const page = await browser.newPage({ viewport: { width: 800, height: 600 } }); - await page.route(/fonts\.(googleapis|gstatic)\.com/, (r) => r.abort()); - // The showcase page registers every wn-* element and loads components.css. - await page.goto(`http://127.0.0.1:${PORT}/examples/showcase/index.html`, { waitUntil: "commit" }); - await page.waitForFunction(() => !!customElements.get("wn-button"), null, { timeout: 8000 }); - await page.addStyleTag({ content: "#parity-stage{position:fixed;left:0;top:0;background:var(--paper);padding:16px;z-index:99999}" }); - - // Static contract analysis (precise attribute-name correctness, no runtime - // type-coercion false positives). The browser then confirms the element - // actually upgrades + renders — the thing static analysis cannot prove. - const byTag = parseLitElements(REPO); - const accepted = loadAccepted(REPO); - - const results = []; - for (const c of contracts) { - const tag = c.tag; - const drift = componentDrift(c, byTag, accepted); - const attrMismatch = drift.issues.filter((i) => i.kind === "attribute-mismatch" && i.severity === "drift"); - const missing = drift.issues.filter((i) => i.kind === "prop-missing" && i.severity === "drift"); - const hasDefaultSlot = (c.slots || []).some((s) => s.name === "default"); - - const observed = await page.evaluate(async ({ tag, attrs, hasDefaultSlot }) => { - if (!customElements.get(tag)) return { exists: false }; - let stage = document.getElementById("parity-stage"); - if (!stage) { stage = document.createElement("div"); stage.id = "parity-stage"; document.body.appendChild(stage); } - stage.innerHTML = ""; - const el = document.createElement(tag); - for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v); - if (hasDefaultSlot) el.textContent = "Sample"; - stage.appendChild(el); - await el.updateComplete?.catch(() => {}); - const r = el.getBoundingClientRect(); - const rendered = el.children.length > 0 || (el.textContent || "").trim().length > 0 || !!el.shadowRoot; - return { exists: true, rendered, rect: { w: Math.round(r.width), h: Math.round(r.height) } }; - }, { tag, attrs: fixtureAttrs(c), hasDefaultSlot }); - - if (!observed.exists) { - results.push({ name: c.name, contract: "skip", visual: "skip", diffRatio: null, - notes: `no <${tag}> element (see adapters/lit/drift/${c.name}.md)` }); - continue; - } - - // Screenshot the element box as the visual artifact. - try { - const h = await page.$("#parity-stage > *"); - if (h) await h.screenshot({ path: join(OUT, `${c.name}.png`) }); - } catch { /* non-fatal */ } - - const contract = attrMismatch.length ? "fail" : "pass"; - const visual = observed.rendered && (observed.rect.w > 0 || observed.rect.h > 0) ? "pass" : "fail"; - const notes = []; - if (attrMismatch.length) notes.push(`attribute-mismatch: ${attrMismatch.map((i) => `${i.prop} expected '${i.expected}' got '${i.actual}'`).join(", ")}`); - if (missing.length) notes.push(`coverage (drift, not gated): missing ${missing.map((i) => i.prop).join(", ")}`); - results.push({ name: c.name, contract, visual, diffRatio: null, box: observed.rect, notes: notes.join("; ") || "ok" }); - } - - const summary = { - total: results.length, - contractFail: results.filter((r) => r.contract === "fail").length, - visualFail: results.filter((r) => r.visual === "fail").length, - skipped: results.filter((r) => r.contract === "skip").length, - }; - const out = { stack: "lit", generatedAt: new Date().toISOString(), components: results, summary }; - writeFileSync(join(OUT, "_parity.json"), JSON.stringify(out, null, 2) + "\n"); - - console.log(`parity-lit: ${summary.total} components · contractFail=${summary.contractFail} · visualFail=${summary.visualFail} · skip=${summary.skipped}`); - for (const r of results) { - const flag = r.contract === "fail" || r.visual === "fail" ? "✗" : r.contract === "skip" ? "·" : "✓"; - console.log(` ${flag} ${r.name}: contract=${r.contract} visual=${r.visual}${r.notes && r.notes !== "ok" ? ` (${r.notes})` : ""}`); - } - - await browser.close(); - stopServer(); - if (summary.contractFail || summary.visualFail) { - console.log("parity-lit: FAILURE — see adapters/lit/parity/_parity.json. Exit 4."); - process.exit(4); - } - console.log("parity-lit: pass."); - } catch (e) { - if (browser) await browser.close().catch(() => {}); - stopServer(); - console.error("parity-lit: internal error —", e.message); - process.exit(5); - } -} - -main(); diff --git a/adapters/lit/parity/_parity.json b/adapters/lit/parity/_parity.json deleted file mode 100644 index 0af21a8..0000000 --- a/adapters/lit/parity/_parity.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "stack": "lit", - "generatedAt": "2026-06-30T07:49:18.063Z", - "components": [ - { - "name": "Button", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 82, - "h": 36 - }, - "notes": "ok" - }, - { - "name": "Eyebrow", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 45, - "h": 23 - }, - "notes": "ok" - }, - { - "name": "Icon", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 0, - "h": 23 - }, - "notes": "ok" - }, - { - "name": "PageHeader", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 117, - "h": 37 - }, - "notes": "ok" - }, - { - "name": "PipelineStrip", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 633, - "h": 76 - }, - "notes": "ok" - }, - { - "name": "Sidebar", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 273, - "h": 592 - }, - "notes": "ok" - }, - { - "name": "StageDot", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 56, - "h": 23 - }, - "notes": "ok" - }, - { - "name": "Stamp", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 63, - "h": 23 - }, - "notes": "ok" - }, - { - "name": "Tag", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 64, - "h": 24 - }, - "notes": "ok" - }, - { - "name": "TopNav", - "contract": "pass", - "visual": "pass", - "diffRatio": null, - "box": { - "w": 243, - "h": 57 - }, - "notes": "ok" - } - ], - "summary": { - "total": 10, - "contractFail": 0, - "visualFail": 0, - "skipped": 0 - } -} diff --git a/adapters/lit/scaffold.mjs b/adapters/lit/scaffold.mjs index c704e82..65cb3e0 100644 --- a/adapters/lit/scaffold.mjs +++ b/adapters/lit/scaffold.mjs @@ -57,13 +57,6 @@ function classProps(name, classes) { return { ...inherited, ...own }; } -function classSlots(name, classes) { - const cls = classes[name]; - if (!cls) return new Set(); - const inherited = cls.extends && classes[cls.extends] ? classSlots(cls.extends, classes) : new Set(); - return new Set([...inherited, ...(cls.slots || [])]); -} - export function parseLitElements(repo) { const classes = {}; // className → { propsBlock, extends, file } const defines = []; // { tag, className, file } @@ -81,17 +74,13 @@ export function parseLitElements(repo) { const nextClass = src.indexOf("class ", m.index + m[0].length); const propsBlock = propsAt !== -1 && (nextClass === -1 || propsAt < nextClass) ? balancedBlock(src, propsAt) : null; - // Named slots the element renders (e.g. ) — a prop can - // be honoured by a same-named slot rather than a reactive property. - const region = src.slice(m.index, nextClass === -1 ? undefined : nextClass); - const slots = [...region.matchAll(/ x[1]); - classes[className] = { propsBlock, extends: base, file, slots }; + classes[className] = { propsBlock, extends: base, file }; } const defRe = /customElements\.define\(\s*["']([\w-]+)["']\s*,\s*([A-Za-z_$][\w$]*)\s*\)/g; while ((m = defRe.exec(src))) defines.push({ tag: m[1], className: m[2], file }); } const byTag = {}; - for (const d of defines) byTag[d.tag] = { ...d, props: classProps(d.className, classes), slots: classSlots(d.className, classes) }; + for (const d of defines) byTag[d.tag] = { ...d, props: classProps(d.className, classes) }; return byTag; } @@ -107,28 +96,8 @@ export function litAttrOf(decl) { return decl.attribute === false ? null : decl.attribute; } -// Human-curated accepted divergences — the auditable output of drift triage for -// divergences that are intentional (e.g. a React monolithic prop modelled -// per-child in a composable Lit element). Keyed `::` → rationale. -// Read from adapters/lit/drift.accepted.json. An accepted issue is still listed in -// the report (marked "accepted: ") but downgraded to info, so it does not gate. -export function loadAccepted(repo) { - const path = join(repo, "adapters", "lit", "drift.accepted.json"); - if (!existsSync(path)) return {}; - try { - const out = {}; - for (const e of (JSON.parse(readFileSync(path, "utf8")).accepted || [])) - out[`${e.component}:${e.kind}:${e.prop ?? ""}`] = e.rationale || "accepted"; - return out; - } catch { return {}; } -} - -function classify(issues, component, accepted = {}) { - for (const i of issues) { - const key = `${component}:${i.kind}:${i.prop ?? ""}`; - if (accepted[key]) { i.severity = "info"; i.accepted = accepted[key]; } - else i.severity = ACTIONABLE.has(i.kind) ? "drift" : "info"; - } +function classify(issues) { + for (const i of issues) i.severity = ACTIONABLE.has(i.kind) ? "drift" : "info"; return issues.some((i) => i.severity === "drift") ? "drift" : "ok"; } @@ -140,7 +109,7 @@ function likelyCounterpart(tag, byTag) { return hit || null; } -export function componentDrift(contract, byTag, accepted = {}) { +export function componentDrift(contract, byTag) { const tag = contract.tag; const el = byTag[tag]; @@ -149,7 +118,7 @@ export function componentDrift(contract, byTag, accepted = {}) { if (alt) { const issues = [{ kind: "tag-mismatch", expected: tag, actual: alt, detail: `IR contract tag '${tag}' has no element; '${alt}' looks like the hand-authored counterpart (rename — resolve in Claude Design or realign the element).` }]; - return { name: contract.name, status: classify(issues, contract.name, accepted), tag, issues }; + return { name: contract.name, status: classify(issues), tag, issues }; } return { name: contract.name, status: "new", tag, issues: [ { kind: "no-counterpart", detail: `no <${tag}> in src/elements/ — stub generated.` }, @@ -158,7 +127,6 @@ export function componentDrift(contract, byTag, accepted = {}) { const issues = []; const litProps = el.props; - const litSlots = el.slots || new Set(); for (const p of contract.props || []) { if (p.portable === false) { issues.push({ kind: "non-portable", prop: p.name, @@ -168,13 +136,6 @@ export function componentDrift(contract, byTag, accepted = {}) { if (p.attribute === false) continue; // property-only by contract const lit = litProps[p.name]; if (!lit) { - // A content prop can be honoured by a same-named named slot (e.g. PageHeader - // `actions` → ) — that is satisfaction, not drift. - if (litSlots.has(p.name) || litSlots.has(p.attribute)) { - issues.push({ kind: "prop-via-slot", prop: p.name, - detail: `IR prop honoured by on <${tag}> (slotted content, not an attribute).` }); - continue; - } issues.push({ kind: "prop-missing", prop: p.name, detail: `in IR (attribute '${p.attribute}'), absent on <${tag}>` }); continue; @@ -196,7 +157,7 @@ export function componentDrift(contract, byTag, accepted = {}) { detail: `on <${tag}> (attribute '${litAttrOf(litProps[name]) ?? "(property-only)"}'), not in IR contract.` }); } - return { name: contract.name, status: classify(issues, contract.name, accepted), tag, issues }; + return { name: contract.name, status: classify(issues), tag, issues }; } // ---------- Stub generation (write-once) ---------- diff --git a/adapters/plain-css/README.md b/adapters/plain-css/README.md deleted file mode 100644 index f982e7a..0000000 --- a/adapters/plain-css/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# plain-css adapter — second-adapter smoke (WHYNOT-WP-0002 · T10) - -> **This is not a finished stack.** It exists to prove the **IR/adapter boundary**: -> that a second, non-Lit adapter can consume the *same* `ir/` and honour the *same* -> [`adapters/ADAPTER_CONTRACT.md`](../ADAPTER_CONTRACT.md). The deliverable is -> confidence in the seam, so the architecture can later be lifted into a -> coulomb-level tool — **not** a usable plain-CSS component kit. - -Run with **`make adapt-plain-css`** (`adapters/plain-css/adapt.mjs`). - -## What it proves - -| Contract concern | This adapter | Same as Lit? | -|---|---|---| -| Input is `ir/` only | reads `ir/tokens.json` + `ir/components/*.json` | ✓ identical inputs | -| Tokens fully generated | → `adapters/plain-css/tokens.css` (CSS custom properties, deterministic no-op re-run) | ✓ same discipline, different target | -| New component → stub | write-once class stub `adapters/plain-css/stubs/.css` (base + variant modifier classes from the contract) | ✓ write-once, into a staging dir | -| Drift roll-up | `adapters/plain-css/_report.json` in the contract's report shape (`stack`, `generatedAt`, `components[]`) | ✓ portable shape | -| Exit codes | `0` ok · `2` usage · `3` new/drift · `5` internal | ✓ shared convention | - -Because plain-CSS has no hand-authored source, **every** IR component reports -`status: "new"` and gets a stub — exactly the contract's new-component path. That a -totally different stack reuses the same IR, the same report shape, and the same exit -codes — with **zero changes to `ir/`** — is the proof the boundary holds. - -## What it deliberately does NOT do - -No real CSS appearance, no parity, no full component set, no integration into the -repo's build. Finishing a plain-CSS (or Vue/Svelte/…) stack is future work; the seam -is what T10 validates. See `DesignSystemIntroduction.md` §5.1 and the Lit reference -adapter (`adapters/lit/`) for the full implementation. diff --git a/adapters/plain-css/_report.json b/adapters/plain-css/_report.json deleted file mode 100644 index 895bade..0000000 --- a/adapters/plain-css/_report.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "stack": "plain-css", - "generatedAt": "2026-06-30T07:48:30.643Z", - "components": [ - { - "name": "Button", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-button class — stub generated." - } - ] - }, - { - "name": "Eyebrow", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-eyebrow class — stub generated." - } - ] - }, - { - "name": "Icon", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-icon class — stub generated." - } - ] - }, - { - "name": "PageHeader", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-page-header class — stub generated." - } - ] - }, - { - "name": "PipelineStrip", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-pipeline class — stub generated." - } - ] - }, - { - "name": "Sidebar", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-sidebar class — stub generated." - } - ] - }, - { - "name": "StageDot", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-stage-dot class — stub generated." - } - ] - }, - { - "name": "Stamp", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-stamp class — stub generated." - } - ] - }, - { - "name": "Tag", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-tag class — stub generated." - } - ] - }, - { - "name": "TopNav", - "status": "new", - "issues": [ - { - "kind": "no-counterpart", - "detail": "no .wn-top-nav class — stub generated." - } - ] - } - ] -} diff --git a/adapters/plain-css/adapt.mjs b/adapters/plain-css/adapt.mjs deleted file mode 100644 index 940e07e..0000000 --- a/adapters/plain-css/adapt.mjs +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env node -// ============================================================= -// adapters/plain-css/adapt.mjs — second-adapter SMOKE (WHYNOT-WP-0002 · T10) -// -// NOT a finished stack. This exists only to prove the IR/adapter *boundary*: -// a non-Lit adapter consuming the very same ir/ and honouring the same -// adapters/ADAPTER_CONTRACT.md (token full-gen + stub/drift + exit codes). -// The deliverable is confidence in the seam, not a usable plain-CSS kit. -// -// It does two contract things from ir/ and nothing Lit-specific: -// • tokens → fully generated CSS custom properties (deterministic no-op re-run) -// • components → write-once class stubs + a drift roll-up in the contract shape -// (every component is "new" here — plain-CSS has no existing source to drift). -// -// Exit codes (shared contract): 0 ok · 2 usage · 3 drift/new · 5 internal. -// Run: `make adapt-plain-css`. -// ============================================================= -import { readFileSync, readdirSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; -import { join, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; - -const REPO = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); -const IR = join(REPO, "ir"); -const HERE = join(REPO, "adapters", "plain-css"); -const STUBS = join(HERE, "stubs"); - -const refToVar = (v) => { - const m = /^\{[A-Za-z0-9]+\.([A-Za-z0-9-]+)\}$/.exec(String(v).trim()); - return m ? `var(--${m[1]})` : v; -}; - -function generateTokens() { - const tokens = JSON.parse(readFileSync(join(IR, "tokens.json"), "utf8")); - const lines = ["/* @generated by adapters/plain-css (WHYNOT-WP-0002 T10) from ir/tokens.json — DO NOT EDIT. */", ":root {"]; - let n = 0; - for (const [group, entries] of Object.entries(tokens)) { - lines.push(` /* ${group} */`); - for (const [key, tok] of Object.entries(entries)) { - if (key === "$type") continue; - lines.push(` --${key}: ${refToVar(tok.$value)};`); n++; - } - } - lines.push("}", ""); - const out = join(HERE, "tokens.css"); - const body = lines.join("\n"); - const before = existsSync(out) ? readFileSync(out, "utf8") : null; - if (before === body) { console.log(`tokens: up to date (${n} custom properties, no change).`); return; } - writeFileSync(out, body); - console.log(`tokens: regenerated ${n} custom properties → adapters/plain-css/tokens.css`); -} - -function renderClassStub(c) { - const base = c.tag; // reuse the contract tag as the base class name - const lines = [ - `/* @generated STUB by adapters/plain-css (T10) from ir/components/${c.name}.json — write-once. */`, - `/* ${c.name}: ${c.description} */`, - `.${base} { /* TODO: base appearance per ir/exemplars/${c.name}.html */ }`, - ]; - for (const v of c.variants || []) { - for (const val of v.values) lines.push(`.${base}--${val} { /* ${v.axis}=${val} */ }`); - } - return lines.join("\n") + "\n"; -} - -function scaffold() { - const contracts = readdirSync(join(IR, "components")).filter((f) => f.endsWith(".json")) - .map((f) => JSON.parse(readFileSync(join(IR, "components", f), "utf8"))) - .sort((a, b) => a.name.localeCompare(b.name)); - - // Plain-CSS has no hand-authored source, so every IR component is "new". - const components = contracts.map((c) => ({ name: c.name, status: "new", - issues: [{ kind: "no-counterpart", detail: `no .${c.tag} class — stub generated.` }] })); - - mkdirSync(STUBS, { recursive: true }); - let stubbed = 0; - for (const c of contracts) { - const out = join(STUBS, `${c.tag}.css`); - if (!existsSync(out)) { writeFileSync(out, renderClassStub(c)); stubbed++; } - } - // Drift roll-up — same shape as the Lit adapter's, proving the contract is portable. - const reportPath = join(HERE, "_report.json"); - let generatedAt = new Date().toISOString(); - if (existsSync(reportPath)) { - try { const prev = JSON.parse(readFileSync(reportPath, "utf8")); - if (JSON.stringify(prev.components) === JSON.stringify(components) && prev.generatedAt) generatedAt = prev.generatedAt; - } catch { /* fresh */ } - } - writeFileSync(reportPath, JSON.stringify({ stack: "plain-css", generatedAt, components }, null, 2) + "\n"); - console.log(`scaffold: ${components.length} components, all new (${stubbed} stub${stubbed === 1 ? "" : "s"} written) → adapters/plain-css/stubs/`); - return components.length > 0; -} - -function main() { - if (!existsSync(join(IR, "tokens.json"))) { console.error("No ir/ — run `make ir` first."); process.exit(2); } - generateTokens(); - const isNew = scaffold(); - console.log("\nadapt-plain-css: SMOKE only — proves a non-Lit adapter consumes the same ir/ and"); - console.log("emits the same contract shapes. Not a finished stack (see adapters/plain-css/README.md)."); - if (isNew) process.exit(3); // new components present (contract: 3) — expected for a fresh stack -} - -main(); diff --git a/adapters/plain-css/stubs/wn-button.css b/adapters/plain-css/stubs/wn-button.css deleted file mode 100644 index 9c6f492..0000000 --- a/adapters/plain-css/stubs/wn-button.css +++ /dev/null @@ -1,6 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/Button.json — write-once. */ -/* Button: Button — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ -.wn-button { /* TODO: base appearance per ir/exemplars/Button.html */ } -.wn-button--secondary { /* variant=secondary */ } -.wn-button--primary { /* variant=primary */ } -.wn-button--ghost { /* variant=ghost */ } diff --git a/adapters/plain-css/stubs/wn-eyebrow.css b/adapters/plain-css/stubs/wn-eyebrow.css deleted file mode 100644 index f9a13ed..0000000 --- a/adapters/plain-css/stubs/wn-eyebrow.css +++ /dev/null @@ -1,3 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/Eyebrow.json — write-once. */ -/* Eyebrow: Eyebrow — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ -.wn-eyebrow { /* TODO: base appearance per ir/exemplars/Eyebrow.html */ } diff --git a/adapters/plain-css/stubs/wn-icon.css b/adapters/plain-css/stubs/wn-icon.css deleted file mode 100644 index 13a448e..0000000 --- a/adapters/plain-css/stubs/wn-icon.css +++ /dev/null @@ -1,3 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/Icon.json — write-once. */ -/* Icon: Icon — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ -.wn-icon { /* TODO: base appearance per ir/exemplars/Icon.html */ } diff --git a/adapters/plain-css/stubs/wn-page-header.css b/adapters/plain-css/stubs/wn-page-header.css deleted file mode 100644 index 15c8c07..0000000 --- a/adapters/plain-css/stubs/wn-page-header.css +++ /dev/null @@ -1,3 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/PageHeader.json — write-once. */ -/* PageHeader: PageHeader — extracted from designbook ui_kits/whynot-control/Chrome.jsx. */ -.wn-page-header { /* TODO: base appearance per ir/exemplars/PageHeader.html */ } diff --git a/adapters/plain-css/stubs/wn-pipeline.css b/adapters/plain-css/stubs/wn-pipeline.css deleted file mode 100644 index 1004460..0000000 --- a/adapters/plain-css/stubs/wn-pipeline.css +++ /dev/null @@ -1,3 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/PipelineStrip.json — write-once. */ -/* PipelineStrip: PipelineStrip — extracted from designbook ui_kits/whynot-control/Chrome.jsx. */ -.wn-pipeline { /* TODO: base appearance per ir/exemplars/PipelineStrip.html */ } diff --git a/adapters/plain-css/stubs/wn-sidebar.css b/adapters/plain-css/stubs/wn-sidebar.css deleted file mode 100644 index ea51343..0000000 --- a/adapters/plain-css/stubs/wn-sidebar.css +++ /dev/null @@ -1,4 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/Sidebar.json — write-once. */ -/* Sidebar: Sidebar — extracted from designbook ui_kits/whynot-control/Chrome.jsx. */ -.wn-sidebar { /* TODO: base appearance per ir/exemplars/Sidebar.html */ } -.wn-sidebar--doc: { /* current=doc: */ } diff --git a/adapters/plain-css/stubs/wn-stage-dot.css b/adapters/plain-css/stubs/wn-stage-dot.css deleted file mode 100644 index e481bac..0000000 --- a/adapters/plain-css/stubs/wn-stage-dot.css +++ /dev/null @@ -1,3 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/StageDot.json — write-once. */ -/* StageDot: StageDot — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ -.wn-stage-dot { /* TODO: base appearance per ir/exemplars/StageDot.html */ } diff --git a/adapters/plain-css/stubs/wn-stamp.css b/adapters/plain-css/stubs/wn-stamp.css deleted file mode 100644 index 2f5592c..0000000 --- a/adapters/plain-css/stubs/wn-stamp.css +++ /dev/null @@ -1,3 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/Stamp.json — write-once. */ -/* Stamp: Stamp — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ -.wn-stamp { /* TODO: base appearance per ir/exemplars/Stamp.html */ } diff --git a/adapters/plain-css/stubs/wn-tag.css b/adapters/plain-css/stubs/wn-tag.css deleted file mode 100644 index 958ad3a..0000000 --- a/adapters/plain-css/stubs/wn-tag.css +++ /dev/null @@ -1,3 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/Tag.json — write-once. */ -/* Tag: Tag — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ -.wn-tag { /* TODO: base appearance per ir/exemplars/Tag.html */ } diff --git a/adapters/plain-css/stubs/wn-top-nav.css b/adapters/plain-css/stubs/wn-top-nav.css deleted file mode 100644 index cc2a9e4..0000000 --- a/adapters/plain-css/stubs/wn-top-nav.css +++ /dev/null @@ -1,3 +0,0 @@ -/* @generated STUB by adapters/plain-css (T10) from ir/components/TopNav.json — write-once. */ -/* TopNav: TopNav — extracted from designbook ui_kits/whynot-control/Chrome.jsx. */ -.wn-top-nav { /* TODO: base appearance per ir/exemplars/TopNav.html */ } diff --git a/adapters/plain-css/tokens.css b/adapters/plain-css/tokens.css deleted file mode 100644 index 9d08898..0000000 --- a/adapters/plain-css/tokens.css +++ /dev/null @@ -1,91 +0,0 @@ -/* @generated by adapters/plain-css (WHYNOT-WP-0002 T10) from ir/tokens.json — DO NOT EDIT. */ -:root { - /* color */ - --ink: #0A0A0A; - --ink-2: #1F1F1F; - --ink-3: #5C5C5C; - --ink-4: #8A8A8A; - --ink-5: #B5B5B3; - --line: #E5E5E2; - --line-strong: #C9C9C5; - --line-soft: #F0F0EC; - --paper: #FFFFFF; - --paper-2: #FAFAF7; - --paper-3: #F4F4EF; - --fg-1: var(--ink); - --fg-2: var(--ink-3); - --fg-3: var(--ink-4); - --fg-mute: var(--ink-5); - --fg-on-dark: #FAFAF7; - --bg-1: var(--paper); - --bg-2: var(--paper-2); - --bg-3: var(--paper-3); - --bg-invert: var(--ink); - --border: var(--line); - --border-strong: var(--line-strong); - --border-soft: var(--line-soft); - --hi: #FFE14A; - --hi-2: #FFD400; - --hi-ink: #1A1500; - --status-raw: #B5B5B3; - --status-weak: #8A8A8A; - --status-medium: #5C5C5C; - --status-strong: #0A0A0A; - --status-commercial: #FFD400; - --status-error: #B33A2E; - --status-error-bg: #FCF3F1; - --status-warn: #C28000; - --status-warn-bg: #FFFCEB; - --status-success: #2F6B3A; - --status-success-bg: #F2F7F2; - --status-info: #2E5C8A; - --status-info-bg: #F2F5FA; - /* fontFamily */ - --ff-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - --ff-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace; - --ff-serif: ui-serif, Georgia, "Times New Roman", serif; - /* fontSize */ - --fs-xs: 11px; - --fs-sm: 13px; - --fs-base: 15px; - --fs-md: 17px; - --fs-lg: 20px; - --fs-xl: 24px; - --fs-2xl: 32px; - --fs-3xl: 44px; - --fs-4xl: 64px; - --fs-5xl: 96px; - /* lineHeight */ - --lh-tight: 1.05; - --lh-snug: 1.25; - --lh-base: 1.5; - --lh-loose: 1.7; - /* letterSpacing */ - --tr-tight: -0.02em; - --tr-snug: -0.01em; - --tr-base: 0em; - --tr-mono: 0.02em; - --tr-label: 0.08em; - /* space */ - --sp-1: 4px; - --sp-2: 8px; - --sp-3: 12px; - --sp-4: 16px; - --sp-5: 24px; - --sp-6: 32px; - --sp-7: 48px; - --sp-8: 64px; - --sp-9: 96px; - --sp-10: 128px; - /* radius */ - --r-0: 0px; - --r-1: 2px; - --r-2: 4px; - --r-3: 8px; - --r-pill: 999px; - /* shadow */ - --shadow-0: none; - --shadow-1: 0 1px 0 var(--line); - --shadow-2: 0 1px 0 var(--line-strong); - --shadow-3: 0 4px 12px -6px rgba(10,10,10,0.10); -} diff --git a/designbook/README.md b/designbook/README.md index eb5d8bc..d0cc82c 100644 --- a/designbook/README.md +++ b/designbook/README.md @@ -8,32 +8,6 @@ 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/ir/INDEX.md b/ir/INDEX.md index 9be4ac7..e267b2a 100644 --- a/ir/INDEX.md +++ b/ir/INDEX.md @@ -1,7 +1,7 @@ # whynot-design IR catalog -**designVersion** `0.4.0` · **components** 10 · **generated** 2026-06-30T07:46:35.138Z +**designVersion** `0.4.0` · **components** 10 · **generated** 2026-06-28T22:41:24.992Z Machine-readable companion: [`manifest.json`](./manifest.json) (per-component + token hashes). @@ -96,7 +96,7 @@ PageHeader — extracted from designbook ui_kits/whynot-control/Chrome.jsx. **Contract:** [`components/PageHeader.json`](./components/PageHeader.json) · **hash** `sha256:93e12068e2f58f10` · **exemplar:** [`exemplars/PageHeader.html`](./exemplars/PageHeader.html) -### PipelineStrip `` +### PipelineStrip `` PipelineStrip — extracted from designbook ui_kits/whynot-control/Chrome.jsx. @@ -104,7 +104,7 @@ PipelineStrip — extracted from designbook ui_kits/whynot-control/Chrome.jsx. | --- | --- | --- | --- | | `activeIdx` | `active-idx` | number | `3` | -**Contract:** [`components/PipelineStrip.json`](./components/PipelineStrip.json) · **hash** `sha256:167717c21cceff79` · **exemplar:** [`exemplars/PipelineStrip.html`](./exemplars/PipelineStrip.html) +**Contract:** [`components/PipelineStrip.json`](./components/PipelineStrip.json) · **hash** `sha256:89c40afe4742d64e` · **exemplar:** [`exemplars/PipelineStrip.html`](./exemplars/PipelineStrip.html) ### Sidebar `` diff --git a/ir/components/PipelineStrip.json b/ir/components/PipelineStrip.json index 865ee4b..92d1972 100644 --- a/ir/components/PipelineStrip.json +++ b/ir/components/PipelineStrip.json @@ -1,6 +1,6 @@ { "name": "PipelineStrip", - "tag": "wn-pipeline", + "tag": "wn-pipeline-strip", "group": "chrome", "description": "PipelineStrip — extracted from designbook ui_kits/whynot-control/Chrome.jsx.", "props": [ diff --git a/ir/manifest.json b/ir/manifest.json index 91a5511..7c65435 100644 --- a/ir/manifest.json +++ b/ir/manifest.json @@ -1,7 +1,7 @@ { "schemaVersion": "1.0.0", "designVersion": "0.4.0", - "generatedAt": "2026-06-30T07:46:35.138Z", + "generatedAt": "2026-06-28T22:41:24.992Z", "tokensHash": "sha256:426f565a9ce6c36f", "components": [ { @@ -27,7 +27,7 @@ { "name": "PipelineStrip", "group": "chrome", - "hash": "sha256:167717c21cceff79" + "hash": "sha256:89c40afe4742d64e" }, { "name": "Sidebar", diff --git a/package.json b/package.json index 18ef31d..2d60729 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@whynot/design", - "version": "0.4.1", + "version": "0.4.0", "description": "The neutral, mostly-black-and-white visual language for whynot — prototype cards, signal records, beta plans, decision documents, and any other deliberately-unfinished artefact. Ships tokens, CSS, and Lit-based web components consumable from React, Django, Vue, plain HTML, or anywhere a custom element runs.", "private": false, "type": "module", diff --git a/registry/capabilities/capability.design.whynot-system.md b/registry/capabilities/capability.design.whynot-system.md deleted file mode 100644 index 8bffa32..0000000 --- a/registry/capabilities/capability.design.whynot-system.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -id: capability.design.whynot-system -name: Whynot Design System -summary: 'Framework-agnostic visual language for whynot prototype/market-signal artefacts: design tokens, - drop-in CSS, Lit-based web components usable from React/Django/Vue/plain HTML, and Django template adapters.' -owner: whynot-design -status: draft -domain: consumer -tags: -- design-system -- web-components -- frontend -maturity: - discovery: - current: D3 - target: D4 - confidence: medium - rationale: README documents a clear three-layer architecture (tokens, CSS, Lit web components) plus - a companion DesignSystemIntroduction.md describing propagation pipeline, versioning, and A1 staging - relative to whynot-control. - availability: - current: A2 - target: A3 - confidence: high - rationale: 'Published as `@whynot/design` (not private: `"private": false` in package.json), ships - a CLI (`whynot-design` bin), tokens/CSS/JS source, and Django adapters — genuinely consumable across - frameworks today, not just planned.' -external_evidence: - completeness: - level: C2 - confidence: low - basis: scope_vs_intent_and_consumer_expectations - satisfied_expectations: - - framework-agnostic Lit web components (same `` usable in React/Django/Vue/plain HTML) - - design tokens as source of truth - - Django adapter partials - - package is publicly consumable (not private) - broken_expectations: [] - out_of_scope_expectations: [] - reliability: - level: R1 - confidence: low - basis: consumer_quality_signals - known_reliability_risks: - - cross-framework consumption claims not independently verified in every target framework during this - sweep -discovery: - intent: Give whynot's prototype and market-signal artefacts a consistent, deliberately-unfinished visual - language, consumable identically across any framework via framework-agnostic web components rather - than per-framework reimplementation. - includes: - - design tokens (tokens/*.json) - - drop-in CSS (src/styles/*.css) - - Lit-based web components (src/elements/*.js) - - Django adapter partials - excludes: - - whynot-control's product/business logic itself (see whynot-control) - assumptions: [] - use_cases: [] - research_memos: [] -availability: - current_level: A2 - target_level: A3 - current_artifacts: - - '`@whynot/design` npm package (public)' - - '`whynot-design` CLI' - target_artifacts: [] - consumption_modes: - - library import (npm) - - cli - - django template adapter -relations: - depends_on: [] - supports: [] - related_to: [] -evidence: - documentation: - - README.md - - DesignSystemIntroduction.md - tests: - - tests/ - - test-results/ - consumer_feedback: [] - bug_reports: [] - incidents: [] -consumer_guidance: - recommended_for: - - whynot-family apps needing a consistent, framework-agnostic visual language across React/Django/Vue/plain - HTML - not_recommended_for: - - consumers wanting a themed (non-neutral) visual language, or needing whynot-control's business logic - itself - known_limitations: - - cross-framework consumption not independently re-verified in every target framework this sweep -promotion_history: [] ---- - -# Whynot Design System - -## Overview - -`@whynot/design` is the framework-agnostic visual language for whynot's prototype and market-signal artefacts: design tokens as source of truth, drop-in CSS, and Lit-based web components that work identically across React, Django, Vue, and plain HTML, plus Django adapter partials — publicly published, not a private/internal-only package. - -## Assessment notes - -### Discovery - -README documents a clear three-layer architecture (tokens, CSS, Lit web components) plus a companion DesignSystemIntroduction.md describing propagation pipeline, versioning, and A1 staging relative to whynot-control. - -### Availability - -Published as `@whynot/design` (not private: `"private": false` in package.json), ships a CLI (`whynot-design` bin), tokens/CSS/JS source, and Django adapters — genuinely consumable across frameworks today, not just planned. - -### Completeness - -First-pass honest assessment from the REUSE-WP-0017 coverage campaign -(reuse-surface). No external consumer feedback exists yet; levels reflect -scope-vs-intent documentation quality, not internal code quality. - -### Reliability - -No production consumer telemetry exists yet; reliability level is -intentionally conservative pending REUSE-WP-0019 reuse-telemetry evidence. - -## Promotion checklist - -- [x] ID follows `capability..` pattern -- [x] Maturity enums match `specs/CapabilityMaturityStandard.md` -- [x] `external_evidence` is populated separately from `maturity` -- [ ] Relations reference valid capability IDs (none yet) -- [x] Index entry added in `registry/indexes/capabilities.yaml` diff --git a/registry/indexes/capabilities.yaml b/registry/indexes/capabilities.yaml index b226cfe..f944e47 100644 --- a/registry/indexes/capabilities.yaml +++ b/registry/indexes/capabilities.yaml @@ -1,22 +1,4 @@ version: 1 -updated: '2026-07-06' +updated: '2026-06-16' domain: helix_forge -capabilities: -- id: capability.design.whynot-system - name: Whynot Design System - summary: 'Framework-agnostic visual language for whynot prototype/market-signal artefacts: design tokens, - drop-in CSS, Lit-based web components usable from React/Django/Vue/plain HTML, and Django template - adapters.' - vector: D3 / A2 / C2 / R1 - domain: consumer - status: draft - owner: whynot-design - path: registry/capabilities/capability.design.whynot-system.md - tags: - - design-system - - web-components - - frontend - consumption_modes: - - library import (npm) - - cli - - django template adapter +capabilities: [] diff --git a/scripts/designbook-refresh.mjs b/scripts/designbook-refresh.mjs deleted file mode 100644 index 50a848b..0000000 --- a/scripts/designbook-refresh.mjs +++ /dev/null @@ -1,73 +0,0 @@ -#!/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."); diff --git a/scripts/ir-extract.mjs b/scripts/ir-extract.mjs index 84e0f52..5158d0f 100644 --- a/scripts/ir-extract.mjs +++ b/scripts/ir-extract.mjs @@ -38,18 +38,6 @@ const COMPONENT_SOURCES = ["Atoms.jsx", "Chrome.jsx"]; const log = (...a) => console.log(...a); const kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); -// The custom-element tag is an IR-*projection* detail, not something the React -// designbook dictates (React components have no tag). The default mapping is -// `wn-${kebab(Name)}`, but a component's canonical tag can differ from a literal -// kebab of its React name. These overrides record those decisions in one place -// (resolving a tag-mismatch drift the governance-correct way — at the projection, -// not by renaming the hand-authored stack element). The component *name* stays -// faithful to React; only the projected tag is overridden. -const TAG_OVERRIDES = { - PipelineStrip: "wn-pipeline", // React "PipelineStrip"; the established web-component tag is wn-pipeline. -}; -const tagFor = (name) => TAG_OVERRIDES[name] || `wn-${kebab(name)}`; - // ---------- Tokens: _ds_manifest.json tokens[] → W3C DTCG ---------- // Each manifest token: { name:"--ink", value:"#0A0A0A", kind:"color", definedIn }. // Group + DTCG $type are decided by name prefix (kind is a fallback hint). @@ -216,7 +204,7 @@ function extractComponents() { const card = matchExemplar(name, cards); const contract = { name, - tag: tagFor(name), + tag: `wn-${kebab(name)}`, group, description: `${name} — extracted from designbook ui_kits/${KIT}/${file}.`, props, diff --git a/workplans/WHYNOT-WP-0002-designbook-stack-adapters.md b/workplans/WHYNOT-WP-0002-designbook-stack-adapters.md index afa5f16..4cc0df9 100644 --- a/workplans/WHYNOT-WP-0002-designbook-stack-adapters.md +++ b/workplans/WHYNOT-WP-0002-designbook-stack-adapters.md @@ -4,39 +4,14 @@ type: workplan title: "Technology-neutral designbook with stack adapters (Lit reference)" domain: infotech repo: whynot-design -status: finished +status: proposed owner: claude topic_slug: custodian created: "2026-06-22" -updated: "2026-06-30" +updated: "2026-06-22" state_hub_workstream_id: "0a3511c1-1771-438b-9364-104d8f0de2f8" --- -> **Completed 2026-06-30.** All 11 tasks done. The full pipeline is in place: -> `Claude Design (React) → designbook/ → ir/ → adapters/lit` with `make ir`, -> `make adapt-lit` (tokens + scaffold + drift), `make parity-lit`, the -> `make designbook-refresh` orchestrator, and a plain-css second-adapter smoke -> proving the IR seam. Parity passes (contractFail=0, visualFail=0). -> -> **Drift triage — all three resolved 2026-06-30; `make designbook-refresh` is GREEN -> (0 drift, parity pass).** Each was resolved the governance-correct way (no stack→React -> back-edit, no `ir/` hand-edit): -> 1. **PipelineStrip** — the web-component *tag* is an IR-projection detail (React has -> no tags), so a documented `TAG_OVERRIDES` in `scripts/ir-extract.mjs` maps -> `PipelineStrip → wn-pipeline` (the component *name* stays faithful to React). Tag -> now matches the element; parity tests it. -> 2. **PageHeader.actions** — `actions` is rendered as a *node* in React -> (`{actions &&
{actions}
}`), i.e. slotted content, and `` -> honours it via ``. The drift detector now recognises a prop -> satisfied by a same-named named slot (`prop-via-slot`, informational). -> 3. **Sidebar.current** — a genuine composition divergence (React monolithic `current` -> key ↔ Lit per-item `active` on composable ``). Recorded as a -> justified, auditable **accepted divergence** in `adapters/lit/drift.accepted.json` -> — still listed in the report, downgraded to info, does not gate. -> -> The drift machinery now distinguishes real defects from intentional modelling -> differences, which is the point of the gate. - # Technology-neutral designbook with stack adapters (Lit reference) ## Problem @@ -206,7 +181,7 @@ re-running is a no-op when nothing changed. Add `make adapt-lit` (tokens portion ```task id: WHYNOT-WP-0002-T07 -status: done +status: todo priority: high state_hub_task_id: "00ed1aff-7724-4e90-9a51-fd58699480ca" ``` @@ -226,7 +201,7 @@ hand-authored element**. Map React prop types → Lit property declarations. Wir ```task id: WHYNOT-WP-0002-T08 -status: done +status: todo priority: medium state_hub_task_id: "1f52ca1f-64a6-4643-992f-f0b4812461a0" ``` @@ -289,7 +264,7 @@ visual tests for determinism. ```task id: WHYNOT-WP-0002-T09 -status: done +status: todo priority: high state_hub_task_id: "07e60a34-0c62-4f8b-848b-d3b8d4292a18" ``` @@ -317,7 +292,7 @@ human-in-loop runbook for step 6. Document in `.claude/rules/stack-and-commands. ```task id: WHYNOT-WP-0002-T10 -status: done +status: todo priority: low state_hub_task_id: "483de131-f580-4031-85df-72cf70a45679" ```