- scripts/release.mjs + `make release VERSION=x.y.z`: guarded release cut — bumps package.json, relabels CHANGELOG [Unreleased] → [x.y.z], commits, and creates the annotated git tag. Refuses on dirty tree, existing tag, duplicate section, or empty [Unreleased]. Never pushes (outward step stays manual). - DesignSystemIntroduction.md §6: document the release ritual; §9: fix the stale bootstrap host + tag note. - README: bump the install pin to the first real tag (v0.3.0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
285 lines
11 KiB
Markdown
285 lines
11 KiB
Markdown
---
|
|
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@<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
|
|
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
|
|
```
|