diff --git a/workplans/WHYNOT-WP-0003-downstream-consumption.md b/workplans/WHYNOT-WP-0003-downstream-consumption.md new file mode 100644 index 0000000..0fb61b0 --- /dev/null +++ b/workplans/WHYNOT-WP-0003-downstream-consumption.md @@ -0,0 +1,265 @@ +--- +id: WHYNOT-WP-0003 +type: workplan +title: "Downstream consumption: versioned IR releases + consumer drift-check" +domain: infotech +repo: whynot-design +status: proposed +owner: claude +topic_slug: custodian +created: "2026-06-27" +updated: "2026-06-27" +--- + +# 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: todo +priority: high +``` + +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 +``` + +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 +``` + +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 +``` + +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 +``` + +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 +``` + +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`. + +--- + +## Phase 3 — Inspectability + +## Generate ir/INDEX.md catalog + +```task +id: WHYNOT-WP-0003-T07 +status: todo +priority: medium +``` + +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 +``` + +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 +``` + +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 +```