docs(workplan): draft WHYNOT-WP-0003 downstream consumption

Versioned IR releases (Gitea npm) + a consumer-side drift-check so downstream
repos can pin whynot-design, inspect a version, and track changes at their own
pace. Status: proposed (awaiting review). Defaults: Gitea npm distribution +
snapshot-diff drift basis; live contract-parity deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 11:43:20 +02:00
parent 92877c4a65
commit d6b388771e

View File

@@ -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@<tag>` 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
```