--- id: WHYNOT-WP-0003 type: workplan title: "Downstream consumption: versioned IR releases + consumer drift-check" domain: infotech repo: whynot-design status: active owner: claude topic_slug: custodian created: "2026-06-27" updated: "2026-06-27" state_hub_workstream_id: "41fed928-f44a-48f4-9870-120310fbf071" --- # Downstream consumption: versioned IR releases + consumer drift-check ## Problem whynot-design is the **upstream visual reference** for other repos. Those repos should follow up on changes **at their own pace** — not be force-synced. To do that a consuming repo needs three things it cannot do today: 1. **Pin a version** — there are no git tags and the package is `private`, so the only pin available is a raw commit SHA. 2. **Inspect a version** — there is no single "what's in this release" summary; you must read the repo. 3. **Get a grip on changes** — there is no tool that tells a consumer *what changed* between the version it adopted and a newer one, so drift is tracked by hand. The substrate already exists: `ir/` is the technology-neutral, committed, diffable contract (component contracts + W3C tokens + exemplars). A consumer never needs the Lit internals — it tracks the IR. The work is to **version it, summarise it, and give consumers a drift-check.** ## Approach Expose `ir/` as the consumer-facing contract and make it the unit of versioned consumption. This is the **inverse** of whynot-design's own upstream machinery (Claude Design → `designbook/` → `ir/`), now pointed downstream (`ir/` → consuming repos): ``` whynot-design (upstream reference) │ tag vX.Y.Z + publish to Gitea npm registry ▼ @whynot/design@X.Y.Z (consumer pins it; lockfile IS the pin) │ ships ir/ + ir/manifest.json (version + per-component/token hashes) ▼ consuming repo │ .whynot-design.lock (manifest hashes it has adopted) │ npx @whynot/design drift → added / changed / removed components + token diffs ▼ follow up at its own pace → npx @whynot/design drift --update (adopt new sync-point) ``` **Locked defaults for this workplan:** - **Distribution = Gitea npm registry.** The remote is Gitea (`coulomb/whynot-design`), which has a built-in npm registry. The consumer lockfile becomes the version pin — the lowest-friction form of "versioned import." - **Drift basis = snapshot diff.** `drift` compares the consumer's last-adopted IR manifest against a target version's manifest (version-to-version). Comparing a consumer's *live rendered UI* against the contracts ("contract parity") is the richer, per-stack, heavier mode and is **deferred** (T09, design-only). ## Builds on (already in this repo) - `ir/` — committed contracts (`ir/components/*.json`), `ir/tokens.json` (W3C DTCG), `ir/exemplars/*`, `ir/schema/*`, `ir/SCHEMA.md`. The consumer-facing contract. - `make ir` (`scripts/ir-extract.mjs`) — already regenerates `ir/`; the manifest and index hang off this. - `package.json` `exports` map (granular: atoms/form/layout/chrome/styles/tokens) and `files` allowlist — a strong publish foundation. - `CHANGELOG.md` + the `pnpm check` gate (`DesignSystemIntroduction.md` §6 versioning discipline) — the version-bump habit to anchor tags to. - `adapters/ADAPTER_CONTRACT.md` drift-report + exit-code conventions — the downstream drift-check reuses the same shapes (`0` ok · `3` drift). --- ## Phase 0 — Versioning & release foundation ## Release tagging + versioning discipline ```task id: WHYNOT-WP-0003-T01 status: progress priority: high state_hub_task_id: "ac6ee3c1-859d-49d4-b5dc-71bdcd2821f9" ``` Adopt semver git tags (`vX.Y.Z`) as the immutable version anchor, tied to the existing `CHANGELOG.md` `[Unreleased]` → versioned-section flow. Document the release ritual (bump `package.json` version, cut the CHANGELOG section, tag, publish) in `DesignSystemIntroduction.md` §6 and a `make release` helper that guards: refuses to tag if `[Unreleased]` is empty or the version is already tagged. Tag the current state as the first real anchor. ## Publish to the Gitea npm registry ```task id: WHYNOT-WP-0003-T02 status: todo priority: high state_hub_task_id: "dbd3a2e6-0623-4efd-8293-399002e85ea2" ``` Make the package installable with a version pin: - Flip `"private": true` → `false`; fix `repository.url` (currently the placeholder `gitea.example.com`) to the real Gitea remote. - Move `lit` from `dependencies` to `peerDependencies` (a component library must let the consumer's bundler dedupe to one `lit` — duplicate-lit is a real failure class). - Add `publishConfig` pointing at the Gitea npm registry scope; document the consumer `.npmrc` (read-token) needed to install. Route the token via `warden route find` (credential-routing.md) — never inline a secret. - Confirm a consumer can `npm i @whynot/design@` from Gitea and that `ir/`, tokens, and CSS resolve through the `exports` map. **Dependency:** T01 (a tag/version must exist to publish). --- ## Phase 1 — IR version manifest (the diff anchor) ## Generate ir/manifest.json ```task id: WHYNOT-WP-0003-T03 status: todo priority: high state_hub_task_id: "aaa6d20f-23d3-4467-ac6e-2c24067f1723" ``` Extend `make ir` to emit `ir/manifest.json`: `{ schemaVersion, designVersion, generatedAt, components: [{ name, hash }], tokensHash }`, where each `hash` is a deterministic content hash of the canonicalised contract / tokens JSON (stable across formatting, sensitive only to meaningful change). Commit it. This is both the at-a-glance inventory (inspection) and the O(1), exact anchor for cross-version diffing. Add a JSON Schema (`ir/schema/manifest.schema.json`) and document it in `ir/SCHEMA.md` / `ir/README.md`. Hash stability across extractor changes is governed by `schemaVersion` (bump on shape changes). --- ## Phase 2 — Consumer drift-check ## Define the consumer sync-point ```task id: WHYNOT-WP-0003-T04 status: todo priority: medium state_hub_task_id: "fe077343-8b6e-48e7-8eb7-a36cc96366c5" ``` Specify `.whynot-design.lock` — the small file a consuming repo commits to record which IR state it has adopted: `{ designVersion, adoptedAt, manifestHashes }` (the subset of `ir/manifest.json` the consumer has reconciled against). Document its lifecycle (created on first adopt, advanced by `drift --update`). This is the consumer-side equivalent of `designbook/.design-sync.json`. ## Build the `drift` CLI ```task id: WHYNOT-WP-0003-T05 status: todo priority: high state_hub_task_id: "db7fcac0-f3fa-4df3-8f54-e0be731381aa" ``` Ship a `bin` entry (`@whynot/design` → `npx @whynot/design drift`) that runs **in a consuming repo**: read the consumer's `.whynot-design.lock` and the installed (or a `--version`-targeted) `ir/manifest.json`, and report **added / changed / removed components + token changes**, grouped and human-readable, with `--json` for automation. Exit codes mirror the adapter contract: `0` in-sync · `3` drift detected · `2` usage/config error. `--update` adopts the target as the new sync-point (rewrites `.whynot-design.lock`). No network beyond the already-installed package. Reuse the diff/report shapes from `adapters/ADAPTER_CONTRACT.md` so upstream and downstream drift read the same. **Dependency:** T03 (manifest) + T04 (lock format). ## Consumer adoption guide + example fixture ```task id: WHYNOT-WP-0003-T06 status: todo priority: medium state_hub_task_id: "5a3c67d8-fd40-4847-a79f-e6fc6a608a1f" ``` Write a short consumer guide (pin → inspect → `drift` → `drift --update`) and a tiny example consuming-repo fixture under `examples/` (or `docs/`) that exercises the full loop against a fixed version, so the workflow is copy-pasteable. Cross-link from `README.md` and `MultiFrameworkSupport.md`. As part of this task, add a **"Tracking whynot-design from a consuming repo"** section to `README.md` (next to *Quick start*) — the README currently documents only *component usage*, not how a consumer pins a version, inspects it (`ir/INDEX.md` / `ir/manifest.json`), runs `drift`, and adopts the new sync-point at its own pace. Point it at the full guide above. While there, finish the install-line correctness the 2026-06-27 adhoc started: the `pnpm add …#v0.2.0` pin only resolves once T01 has cut the tag and T02 has published, so reconcile the README to the real, tagged registry coordinates as those land. --- ## Phase 3 — Inspectability ## Generate ir/INDEX.md catalog ```task id: WHYNOT-WP-0003-T07 status: todo priority: medium state_hub_task_id: "7159dcdc-55cf-4815-9ba2-0361266a7b8f" ``` Extend `make ir` to emit `ir/INDEX.md` — a human-readable catalog generated from the contracts: per component, its group, description, props/variants/slots/events summary, and a link to its exemplar. This makes a version browsable without cloning or running anything, complementing the machine-readable manifest. ## Showcase as visual catalog (depends on WP-0002 T11) ```task id: WHYNOT-WP-0003-T08 status: wait priority: low state_hub_task_id: "a0886a4f-cf27-44ef-b8c6-8e61ceda1f84" ``` The `examples/showcase` "every component" page is the visual catalog for a version, but it currently wedges the renderer (tracked as **WHYNOT-WP-0002-T11**). This task is just to confirm, once T11 lands, that the showcase is deployable/inspectable per version (e.g. served from a tag) — no new build, reuse the existing page. Blocked on WP-0002-T11. --- ## Phase 4 — Deferred: live contract-parity mode (design-only) ## Sketch consumer-side contract parity ```task id: WHYNOT-WP-0003-T09 status: todo priority: low state_hub_task_id: "e7704a1f-2011-41cb-9e77-c7a6bb2a05ac" ``` Design (do **not** implement) the richer drift mode: compare a consumer's *live rendered elements'* observed attributes/properties against the IR contracts — the consumer-side mirror of WP-0002 T08 parity, requiring per-stack introspection. Deliverable: a design note + a go/defer decision recorded as a `record_decision`, not code. Keeps the snapshot-diff default (T05) as the must-have while capturing the shape of the heavier mode. --- ## Open questions / risks - **Gitea npm read-token distribution** — consumers need an `.npmrc` token to install from a private-org registry; route ownership via `warden route` (credential-routing.md), never inline. Decide org-read vs per-consumer tokens (T02). - **Hash stability** — the manifest hash must be invariant to formatting and sensitive only to meaningful contract change; canonicalise JSON before hashing and gate shape changes behind `schemaVersion` (T03). - **Token diff granularity** — whether `drift` reports a single `tokensHash` change or per-token added/changed/removed; start coarse (one hash), refine if consumers ask (T05). - **Shared drift logic with WP-0002** — the upstream adapter drift and this downstream consumer drift compute the same kind of contract diff; factor the diff core so both reuse it rather than forking (T05). - **One-way constraint holds** — consumers read the IR; they never write back to whynot-design. `drift` is read-only against the package; only `.whynot-design.lock` (in the consumer repo) is written. ## Registering this workplan After review, register the workstream from `~/state-hub`: ```bash make fix-consistency REPO=whynot-design ```