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:
265
workplans/WHYNOT-WP-0003-downstream-consumption.md
Normal file
265
workplans/WHYNOT-WP-0003-downstream-consumption.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user