feat(consumer): versioned IR manifest + drift-check (WHYNOT-WP-0003 T03-T07,T09)
Make ir/ the unit of versioned downstream consumption so consuming repos can pin a version, inspect it, and follow changes at their own pace. - T03 ir/manifest.json: per-version inventory + diff anchor with deterministic sha256-over-canonicalised-JSON hashes; no-churn generatedAt; manifest schema. - T07 ir/INDEX.md: human-readable catalog generated by make ir. - T04 .whynot-design.lock sync-point format + lock schema. - T05 npx @whynot/design drift: consumer drift-check (bin entry), exit 0/2/3, --json/--update/--manifest/--version/--lock. - T06 CONSUMING.md guide + examples/consumer-fixture/ runnable demo; README + MultiFrameworkSupport cross-links; fix README version pin (@0.3.0 not @v0.3.0). - T09 CONSUMER_CONTRACT_PARITY.md design-only note (live-UI parity deferred). T02 (publish) and T08 (showcase, blocked on WP-0002 T11) remain wait. Repo stays in dev mode; no outward publish performed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
130
ir/INDEX.md
Normal file
130
ir/INDEX.md
Normal file
@@ -0,0 +1,130 @@
|
||||
<!-- GENERATED by scripts/ir-extract.mjs (make ir) — do not hand-edit. -->
|
||||
# whynot-design IR catalog
|
||||
|
||||
**designVersion** `0.3.0` · **components** 10 · **generated** 2026-06-27T17:28:08.913Z
|
||||
|
||||
Machine-readable companion: [`manifest.json`](./manifest.json) (per-component + token hashes).
|
||||
|
||||
## atoms
|
||||
|
||||
### Button `<wn-button>`
|
||||
|
||||
Button — extracted from designbook ui_kits/whynot-control/Atoms.jsx.
|
||||
|
||||
| prop | attribute | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| `variant` | `variant` | enum(secondary \| primary \| ghost) | `secondary` |
|
||||
| `icon` | `icon` | boolean | — |
|
||||
|
||||
_Non-portable (React-only) props:_ `onClick`, `style`.
|
||||
|
||||
**Slots:** `default`
|
||||
**Events:** `wn-click`
|
||||
**Variants:** variant (secondary/primary/ghost)
|
||||
**Contract:** [`components/Button.json`](./components/Button.json) · **hash** `sha256:4a32713049e433dd` · **exemplar:** [`exemplars/Button.html`](./exemplars/Button.html)
|
||||
|
||||
### Eyebrow `<wn-eyebrow>`
|
||||
|
||||
Eyebrow — extracted from designbook ui_kits/whynot-control/Atoms.jsx.
|
||||
|
||||
_Non-portable (React-only) props:_ `style`.
|
||||
|
||||
**Slots:** `default`
|
||||
**Contract:** [`components/Eyebrow.json`](./components/Eyebrow.json) · **hash** `sha256:56baa59a49b5f32c` · **exemplar:** [`exemplars/Eyebrow.html`](./exemplars/Eyebrow.html)
|
||||
|
||||
### Icon `<wn-icon>`
|
||||
|
||||
Icon — extracted from designbook ui_kits/whynot-control/Atoms.jsx.
|
||||
|
||||
| prop | attribute | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| `name` | `name` | string | — |
|
||||
| `size` | `size` | number | `16` |
|
||||
|
||||
_Non-portable (React-only) props:_ `style`.
|
||||
|
||||
**Contract:** [`components/Icon.json`](./components/Icon.json) · **hash** `sha256:2557fbffb3aa6ee1`
|
||||
|
||||
### StageDot `<wn-stage-dot>`
|
||||
|
||||
StageDot — extracted from designbook ui_kits/whynot-control/Atoms.jsx.
|
||||
|
||||
| prop | attribute | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| `level` | `level` | string | `S2` |
|
||||
| `label` | `label` | string | — |
|
||||
|
||||
_Non-portable (React-only) props:_ `style`.
|
||||
|
||||
**Contract:** [`components/StageDot.json`](./components/StageDot.json) · **hash** `sha256:f6f7790aa886261e` · **exemplar:** [`exemplars/StageDot.html`](./exemplars/StageDot.html)
|
||||
|
||||
### Stamp `<wn-stamp>`
|
||||
|
||||
Stamp — extracted from designbook ui_kits/whynot-control/Atoms.jsx.
|
||||
|
||||
_Non-portable (React-only) props:_ `style`.
|
||||
|
||||
**Slots:** `default`
|
||||
**Contract:** [`components/Stamp.json`](./components/Stamp.json) · **hash** `sha256:0b32f43ed19ac470`
|
||||
|
||||
### Tag `<wn-tag>`
|
||||
|
||||
Tag — extracted from designbook ui_kits/whynot-control/Atoms.jsx.
|
||||
|
||||
| prop | attribute | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| `active` | `active` | boolean | — |
|
||||
| `draft` | `draft` | boolean | — |
|
||||
|
||||
_Non-portable (React-only) props:_ `style`.
|
||||
|
||||
**Slots:** `default`
|
||||
**Contract:** [`components/Tag.json`](./components/Tag.json) · **hash** `sha256:91ee34eac1457016` · **exemplar:** [`exemplars/Tag.html`](./exemplars/Tag.html)
|
||||
|
||||
## chrome
|
||||
|
||||
### PageHeader `<wn-page-header>`
|
||||
|
||||
PageHeader — extracted from designbook ui_kits/whynot-control/Chrome.jsx.
|
||||
|
||||
| prop | attribute | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| `eyebrow` | `eyebrow` | boolean | — |
|
||||
| `title` | `title` | string | — |
|
||||
| `lede` | `lede` | boolean | — |
|
||||
| `actions` | `actions` | boolean | — |
|
||||
|
||||
**Contract:** [`components/PageHeader.json`](./components/PageHeader.json) · **hash** `sha256:93e12068e2f58f10` · **exemplar:** [`exemplars/PageHeader.html`](./exemplars/PageHeader.html)
|
||||
|
||||
### PipelineStrip `<wn-pipeline-strip>`
|
||||
|
||||
PipelineStrip — extracted from designbook ui_kits/whynot-control/Chrome.jsx.
|
||||
|
||||
| prop | attribute | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| `activeIdx` | `active-idx` | number | `3` |
|
||||
|
||||
**Contract:** [`components/PipelineStrip.json`](./components/PipelineStrip.json) · **hash** `sha256:89c40afe4742d64e` · **exemplar:** [`exemplars/PipelineStrip.html`](./exemplars/PipelineStrip.html)
|
||||
|
||||
### Sidebar `<wn-sidebar>`
|
||||
|
||||
Sidebar — extracted from designbook ui_kits/whynot-control/Chrome.jsx.
|
||||
|
||||
| prop | attribute | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| `current` | `current` | enum(doc:) | — |
|
||||
|
||||
_Non-portable (React-only) props:_ `onNav`.
|
||||
|
||||
**Events:** `wn-nav`
|
||||
**Variants:** current (doc:)
|
||||
**Contract:** [`components/Sidebar.json`](./components/Sidebar.json) · **hash** `sha256:8340b292fff7a80d` · **exemplar:** [`exemplars/Sidebar.html`](./exemplars/Sidebar.html)
|
||||
|
||||
### TopNav `<wn-top-nav>`
|
||||
|
||||
TopNav — extracted from designbook ui_kits/whynot-control/Chrome.jsx.
|
||||
|
||||
_Non-portable (React-only) props:_ `onNew`.
|
||||
|
||||
**Events:** `wn-new`
|
||||
**Contract:** [`components/TopNav.json`](./components/TopNav.json) · **hash** `sha256:32ebc6e46db38f93` · **exemplar:** [`exemplars/TopNav.html`](./exemplars/TopNav.html)
|
||||
11
ir/README.md
11
ir/README.md
@@ -16,8 +16,19 @@ What is committed:
|
||||
- `tokens.json` — all tokens, W3C DTCG format.
|
||||
- `components/<Name>.json` — one contract per component.
|
||||
- `exemplars/<Name>.{png,html}` — reference renders from the designbook preview.
|
||||
- `manifest.json` — the per-version inventory + diff anchor: `{ schemaVersion,
|
||||
designVersion, generatedAt, tokensHash, components: [{ name, group, hash }] }`,
|
||||
where each hash is a deterministic content hash (sha256 over canonicalised JSON).
|
||||
This is what a consuming repo pins against and what the `drift` check compares
|
||||
(WHYNOT-WP-0003). Validated by `schema/manifest.schema.json`.
|
||||
- `INDEX.md` — the human-readable catalog, generated from the same contracts:
|
||||
per component its group, description, props/variants/slots/events, and a link
|
||||
to its exemplar. Browse a version without cloning or running anything.
|
||||
- `schema/` + `SCHEMA.md` — the contract definitions (this is what T01 delivered).
|
||||
|
||||
`manifest.json` and `INDEX.md` are generated by the same extractor as the rest of
|
||||
`ir/` and share its one-way rule — **do not hand-edit them.**
|
||||
|
||||
## Direction of flow
|
||||
|
||||
```
|
||||
|
||||
39
ir/SCHEMA.md
39
ir/SCHEMA.md
@@ -24,11 +24,50 @@ ir/
|
||||
schema/
|
||||
tokens.schema.json ← JSON Schema for tokens.json (W3C DTCG)
|
||||
component.schema.json ← JSON Schema for each components/<Name>.json
|
||||
manifest.schema.json ← JSON Schema for manifest.json (version + hashes)
|
||||
tokens.json ← all design tokens, W3C DTCG format (emitted by T05)
|
||||
components/<Name>.json ← one contract per component (emitted by T05)
|
||||
exemplars/<Name>.{png,html} ← reference render from the designbook (emitted by T05)
|
||||
manifest.json ← per-version inventory + diff anchor (WHYNOT-WP-0003 T03)
|
||||
INDEX.md ← human-readable catalog (WHYNOT-WP-0003 T07)
|
||||
```
|
||||
|
||||
## Version manifest (`manifest.json`) — the diff anchor
|
||||
|
||||
`ir/manifest.json` is the unit of **versioned consumption**. A consuming repo never
|
||||
reads the Lit internals; it pins a published `@whynot/design@X.Y.Z` and tracks the
|
||||
manifest. Shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"designVersion": "0.3.0",
|
||||
"generatedAt": "2026-06-27T17:28:08.913Z",
|
||||
"tokensHash": "sha256:426f565a9ce6c36f",
|
||||
"components": [
|
||||
{ "name": "Button", "group": "atoms", "hash": "sha256:4a32713049e433dd" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **Each `hash`** is `sha256:` + the first 16 hex chars of a sha256 over the
|
||||
*canonicalised* contract (keys sorted recursively, no insignificant whitespace).
|
||||
Canonicalisation makes the hash invariant to formatting and sensitive **only** to
|
||||
meaningful change — re-running `make ir` after a no-op edit yields the same hash.
|
||||
- **`tokensHash`** is a single coarse hash of the whole token set. Per-token diff
|
||||
granularity is a deliberate later refinement (start coarse, refine if consumers ask).
|
||||
- **`schemaVersion`** governs hash stability: any extractor change that would alter
|
||||
existing hashes *without* a real design change must bump it, so a consumer can tell
|
||||
a re-canonicalisation apart from a genuine design move. It is distinct from
|
||||
`designVersion` (the package/tag version) and from the component schema's own shape.
|
||||
- **`generatedAt`** is informational and never hashed; it is reused from the prior
|
||||
manifest when nothing hashed changed, so a no-op `make ir` produces no git churn.
|
||||
|
||||
The downstream consumer `drift` check (WHYNOT-WP-0003 T05) compares a target
|
||||
manifest against the consumer's adopted `.whynot-design.lock` and reports added /
|
||||
changed / removed components plus token changes — the mirror of the upstream adapter
|
||||
drift, sharing the `0` ok · `3` drift exit-code convention.
|
||||
|
||||
## Tokens — `ir/tokens.json`
|
||||
|
||||
Adopts the **W3C Design Tokens Community Group** format: every token is an object
|
||||
|
||||
58
ir/manifest.json
Normal file
58
ir/manifest.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"designVersion": "0.3.0",
|
||||
"generatedAt": "2026-06-27T17:28:08.913Z",
|
||||
"tokensHash": "sha256:426f565a9ce6c36f",
|
||||
"components": [
|
||||
{
|
||||
"name": "Button",
|
||||
"group": "atoms",
|
||||
"hash": "sha256:4a32713049e433dd"
|
||||
},
|
||||
{
|
||||
"name": "Eyebrow",
|
||||
"group": "atoms",
|
||||
"hash": "sha256:56baa59a49b5f32c"
|
||||
},
|
||||
{
|
||||
"name": "Icon",
|
||||
"group": "atoms",
|
||||
"hash": "sha256:2557fbffb3aa6ee1"
|
||||
},
|
||||
{
|
||||
"name": "PageHeader",
|
||||
"group": "chrome",
|
||||
"hash": "sha256:93e12068e2f58f10"
|
||||
},
|
||||
{
|
||||
"name": "PipelineStrip",
|
||||
"group": "chrome",
|
||||
"hash": "sha256:89c40afe4742d64e"
|
||||
},
|
||||
{
|
||||
"name": "Sidebar",
|
||||
"group": "chrome",
|
||||
"hash": "sha256:8340b292fff7a80d"
|
||||
},
|
||||
{
|
||||
"name": "StageDot",
|
||||
"group": "atoms",
|
||||
"hash": "sha256:f6f7790aa886261e"
|
||||
},
|
||||
{
|
||||
"name": "Stamp",
|
||||
"group": "atoms",
|
||||
"hash": "sha256:0b32f43ed19ac470"
|
||||
},
|
||||
{
|
||||
"name": "Tag",
|
||||
"group": "atoms",
|
||||
"hash": "sha256:91ee34eac1457016"
|
||||
},
|
||||
{
|
||||
"name": "TopNav",
|
||||
"group": "chrome",
|
||||
"hash": "sha256:32ebc6e46db38f93"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
ir/schema/lock.schema.json
Normal file
48
ir/schema/lock.schema.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://whynot.design/ir/schema/lock.schema.json",
|
||||
"title": "whynot-design — Consumer Sync-Point Lock",
|
||||
"description": ".whynot-design.lock lives in a CONSUMING repo (not in whynot-design) and records which IR state that repo has adopted. It is the consumer-side mirror of designbook/.design-sync.json. Created on first adopt and advanced only by `whynot-design drift --update`. The drift check compares this against a target ir/manifest.json to report what changed since the consumer's adopted sync-point.",
|
||||
"type": "object",
|
||||
"required": ["designVersion", "adoptedAt", "manifestSchemaVersion", "manifestHashes"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"designVersion": {
|
||||
"type": "string",
|
||||
"description": "The @whynot/design version whose manifest was adopted (e.g. matches the installed package / tag vX.Y.Z)."
|
||||
},
|
||||
"adoptedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO-8601 timestamp this sync-point was adopted (first adopt or last `drift --update`)."
|
||||
},
|
||||
"manifestSchemaVersion": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
|
||||
"description": "The adopted manifest's schemaVersion. If a target manifest has a different schemaVersion the drift check warns that hashes may not be directly comparable."
|
||||
},
|
||||
"manifestHashes": {
|
||||
"type": "object",
|
||||
"description": "The subset of ir/manifest.json the consumer has reconciled against.",
|
||||
"required": ["tokens", "components"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tokens": {
|
||||
"$ref": "#/$defs/hash",
|
||||
"description": "Adopted value of the manifest's tokensHash."
|
||||
},
|
||||
"components": {
|
||||
"type": "object",
|
||||
"description": "Map of component name → adopted content hash.",
|
||||
"additionalProperties": { "$ref": "#/$defs/hash" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256:[0-9a-f]{16}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
ir/schema/manifest.schema.json
Normal file
50
ir/schema/manifest.schema.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://whynot.design/ir/schema/manifest.schema.json",
|
||||
"title": "whynot IR — Version Manifest",
|
||||
"description": "ir/manifest.json — the per-version inventory and diff anchor. Generated only by scripts/ir-extract.mjs (make ir). Each hash is a deterministic content hash (sha256 over canonicalised JSON: keys sorted, no insignificant whitespace) so it is invariant to formatting and sensitive only to meaningful contract/token change. The consumer drift-check (WHYNOT-WP-0003 T05) compares two manifests, or a manifest against a consumer's .whynot-design.lock.",
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "designVersion", "generatedAt", "tokensHash", "components"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "string",
|
||||
"description": "Shape version of THIS manifest. Bumped when the manifest layout or the hashing scheme changes, so a consumer can tell a re-canonicalisation apart from a real design change.",
|
||||
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
},
|
||||
"designVersion": {
|
||||
"type": "string",
|
||||
"description": "The package.json version this manifest was extracted at (e.g. the semver tied to a git tag vX.Y.Z)."
|
||||
},
|
||||
"generatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO-8601 timestamp of extraction. Reused from the prior manifest when nothing hashed changed, so a no-op `make ir` does not churn the committed file. Informational only — never part of any hash."
|
||||
},
|
||||
"tokensHash": {
|
||||
"$ref": "#/$defs/hash",
|
||||
"description": "Content hash of the full canonicalised ir/tokens.json. A single coarse hash by design (token-level diff granularity is a deferred refinement)."
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"description": "One entry per component contract, sorted by name.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "group", "hash"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": { "type": "string", "pattern": "^[A-Z][A-Za-z0-9]*$" },
|
||||
"group": { "type": "string", "description": "The component's group (atoms, chrome, …)." },
|
||||
"hash": { "$ref": "#/$defs/hash", "description": "Content hash of the canonicalised ir/components/<name>.json contract." }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256:[0-9a-f]{16}$",
|
||||
"description": "Algorithm-prefixed truncated digest: 'sha256:' + first 16 hex chars of sha256(canonical JSON)."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user