11 KiB
id, type, title, domain, repo, status, owner, topic_slug, created, updated, state_hub_workstream_id
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|
| WHYNOT-WP-0003 | workplan | Downstream consumption: versioned IR releases + consumer drift-check | infotech | whynot-design | active | claude | custodian | 2026-06-27 | 2026-06-27 | 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:
- Pin a version — there are no git tags and the package is
private, so the only pin available is a raw commit SHA. - Inspect a version — there is no single "what's in this release" summary; you must read the repo.
- 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.
driftcompares 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 regeneratesir/; the manifest and index hang off this.package.jsonexportsmap (granular: atoms/form/layout/chrome/styles/tokens) andfilesallowlist — a strong publish foundation.CHANGELOG.md+ thepnpm checkgate (DesignSystemIntroduction.md§6 versioning discipline) — the version-bump habit to anchor tags to.adapters/ADAPTER_CONTRACT.mddrift-report + exit-code conventions — the downstream drift-check reuses the same shapes (0ok ·3drift).
Phase 0 — Versioning & release foundation
Release tagging + versioning discipline
id: WHYNOT-WP-0003-T01
status: done
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
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; fixrepository.url(currently the placeholdergitea.example.com) to the real Gitea remote. - Move
litfromdependenciestopeerDependencies(a component library must let the consumer's bundler dedupe to onelit— duplicate-lit is a real failure class). - Add
publishConfigpointing at the Gitea npm registry scope; document the consumer.npmrc(read-token) needed to install. Route the token viawarden route find(credential-routing.md) — never inline a secret. - Confirm a consumer can
npm i @whynot/design@<tag>from Gitea and thatir/, tokens, and CSS resolve through theexportsmap.
Dependency: T01 (a tag/version must exist to publish).
Phase 1 — IR version manifest (the diff anchor)
Generate ir/manifest.json
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
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
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
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
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)
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
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
.npmrctoken to install from a private-org registry; route ownership viawarden 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
driftreports a singletokensHashchange 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.
driftis 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:
make fix-consistency REPO=whynot-design