Files
whynot-design/workplans/WHYNOT-WP-0003-downstream-consumption.md
tegwick a89bb563a0
Some checks failed
ci / check (push) Has been cancelled
ci / release (push) Has been cancelled
fix(showcase): break wn-breadcrumb slotchange infinite loop (WHYNOT-WP-0002 T11)
WnBreadcrumb._onSlot inserted separator <span>s into its own light DOM on
slotchange but cleaned up in the shadow DOM, so they were never removed — each
insertion re-fired slotchange, looping the main thread and wedging the showcase
page. Made _onSlot idempotent: exclude own separators when reading items, and
mutate only when separators are not already correct.

- Un-fixme the showcase visual test; add a warm-up full-page capture so
  deviceScaleFactor-2 sub-pixel snapping settles before the assertion. All 5
  visual tests pass.
- Remove the dead Google-Fonts @import from colors_and_type.css (token stacks are
  system-ui; webfont unused + a CI-flake source; no visual change).
- Unblocks WHYNOT-WP-0003 T08 (showcase = per-version visual catalog); both T11
  and T08 marked done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:10:41 +02:00

12 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:

  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

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: wait
priority: high
state_hub_task_id: "dbd3a2e6-0623-4efd-8293-399002e85ea2"

Make the package installable with a version pin:

  • Flip "private": truefalse; 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

id: WHYNOT-WP-0003-T03
status: done
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: done
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: done
priority: high
state_hub_task_id: "db7fcac0-f3fa-4df3-8f54-e0be731381aa"

Ship a bin entry (@whynot/designnpx @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: done
priority: medium
state_hub_task_id: "5a3c67d8-fd40-4847-a79f-e6fc6a608a1f"

Write a short consumer guide (pin → inspect → driftdrift --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: done
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: done
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.

Done 2026-06-27. WP-0002-T11 landed (breadcrumb infinite-loop fixed); the showcase page now renders deterministically and is covered by a passing visual test (showcase.png baseline). No new build — the existing page is the per-version visual catalog: served statically from any checkout/tag (pnpm showcase), it pairs with the machine-readable ir/manifest.json + ir/INDEX.md as the visual side of a version's inventory. Nothing further to build.


Phase 4 — Deferred: live contract-parity mode (design-only)

Sketch consumer-side contract parity

id: WHYNOT-WP-0003-T09
status: done
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:

make fix-consistency REPO=whynot-design