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>
181 lines
7.8 KiB
Markdown
181 lines
7.8 KiB
Markdown
# whynot IR — Schema
|
|
|
|
> The **technology-neutral blueprint** for the whynot design language.
|
|
> Part of WHYNOT-WP-0002. The IR is the pivot between the canonical React
|
|
> designbook and every per-stack adapter (Lit is the reference adapter).
|
|
|
|
## Why an IR exists
|
|
|
|
Claude Design's `/design-sync` produces a **React-bound** designbook. A non-React
|
|
design system "has nothing for the design agent to build with." The IR breaks that
|
|
binding: React stays the *authoring surface*, but the IR — committed, diffable,
|
|
framework-free — is the actual contract that adapters project onto each stack.
|
|
|
|
**Directionality is one-way: React → IR → stacks.** Nothing writes back to `ir/`
|
|
except the extractor (`scripts/ir-extract.mjs`, T05). A change to the shared
|
|
language is made in Claude Design and re-propagated; the IR is never hand-edited.
|
|
|
|
## Layout
|
|
|
|
```
|
|
ir/
|
|
SCHEMA.md ← this file (narrative spec)
|
|
README.md ← the committed-blueprint decision + workflow
|
|
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
|
|
with `$value` and (optionally inherited) `$type`; groups nest; `$description`
|
|
carries documentation. This is a published standard rather than a bespoke shape,
|
|
so the token layer can feed Style Dictionary or any DTCG tool unchanged.
|
|
|
|
```json
|
|
{
|
|
"color": {
|
|
"$type": "color",
|
|
"ink": { "$value": "#0A0A0A", "$description": "Near-black. The only fill most of the time." },
|
|
"line": { "$value": "#E5E5E2", "$description": "Default 1px wireframe rule." }
|
|
}
|
|
}
|
|
```
|
|
|
|
> **Migration note.** The repo's current `tokens/*.json` use the older draft shape
|
|
> (`value`/`type`, no `$` prefix). The extractor (T05) normalises them to the
|
|
> `$`-prefixed DTCG shape on the way into `ir/tokens.json`. Validate with
|
|
> `schema/tokens.schema.json`.
|
|
|
|
## Component contract — `ir/components/<Name>.json`
|
|
|
|
Captures everything an adapter needs to scaffold a stub and detect drift, with no
|
|
framework assumptions. Validate with `schema/component.schema.json`. Fields:
|
|
|
|
| Field | Meaning |
|
|
|---|---|
|
|
| `name` | PascalCase canonical name (`Button`). |
|
|
| `tag` | Advisory custom-element tag (`wn-button`). |
|
|
| `group` | Designbook group (`atoms`, `chrome`, `form`). |
|
|
| `description` | Purpose, from the React `.prompt.md`. |
|
|
| `props[]` | Public inputs — see below. |
|
|
| `slots[]` | Named/default content slots. |
|
|
| `events[]` | Emitted events (e.g. `wn-dismiss`). |
|
|
| `variants[]` | Variant axes — named dimensions with discrete values. |
|
|
| `docsRef` | Path to source docs under `designbook/`. |
|
|
| `exemplarRef` | Path to the reference render under `ir/exemplars/`. |
|
|
|
|
### The prop → attribute mapping (the crux)
|
|
|
|
React props are camelCase properties; Lit/Vue/plain-HTML bind **attributes**
|
|
(kebab-case). Each prop therefore records **both** identities plus a portability
|
|
flag:
|
|
|
|
```json
|
|
{
|
|
"name": "iconEnd", // React prop, camelCase
|
|
"type": "string",
|
|
"attribute": "icon-end", // HTML attribute an attribute-driven adapter binds
|
|
"portable": true
|
|
}
|
|
```
|
|
|
|
- `attribute: false` means the prop is deliberately **not** an attribute
|
|
(property-only, or non-portable).
|
|
- `portable: false` marks props that don't map cleanly to an attribute — objects,
|
|
render props, callbacks. **Adapters MUST surface non-portable props as drift,
|
|
never silently drop them** (open risk in the workplan). Such props pair with
|
|
`type` ∈ {`object`, `function`, `node`}.
|
|
|
|
This mapping is exactly what the Lit elements already encode, e.g.
|
|
`iconEnd: { type: String, attribute: "icon-end" }` in `src/elements/atoms.js`.
|
|
|
|
## Worked exemplar — `Button`
|
|
|
|
Derived from the existing `<wn-button>` (`src/elements/atoms.js`) to show the
|
|
target shape the React extractor must produce:
|
|
|
|
```json
|
|
{
|
|
"name": "Button",
|
|
"tag": "wn-button",
|
|
"group": "atoms",
|
|
"description": "Primary action control. Renders a <button>, or an <a> when href is set.",
|
|
"props": [
|
|
{ "name": "variant", "type": "enum", "attribute": "variant", "enum": ["primary", "secondary", "ghost"], "default": "secondary" },
|
|
{ "name": "size", "type": "enum", "attribute": "size", "enum": ["sm", "md", "lg"], "default": "md" },
|
|
{ "name": "icon", "type": "string", "attribute": "icon" },
|
|
{ "name": "iconEnd", "type": "string", "attribute": "icon-end" },
|
|
{ "name": "type", "type": "string", "attribute": "type", "default": "button" },
|
|
{ "name": "disabled", "type": "boolean", "attribute": "disabled", "default": false },
|
|
{ "name": "href", "type": "string", "attribute": "href" }
|
|
],
|
|
"slots": [
|
|
{ "name": "default", "description": "Button label." }
|
|
],
|
|
"events": [],
|
|
"variants": [
|
|
{ "axis": "variant", "values": ["primary", "secondary", "ghost"], "default": "secondary" },
|
|
{ "axis": "size", "values": ["sm", "md", "lg"], "default": "md" }
|
|
],
|
|
"docsRef": "designbook/components/atoms/Button/Button.prompt.md",
|
|
"exemplarRef": "ir/exemplars/Button.html"
|
|
}
|
|
```
|
|
|
|
> The `enum` values for `variant` are illustrative of the contract shape; the
|
|
> authoritative values come from the React source at extraction time (T05).
|
|
|
|
## Validation
|
|
|
|
```bash
|
|
# once an extractor exists (T05):
|
|
node scripts/ir-validate.mjs # validates tokens.json + components/*.json against schema/
|
|
```
|
|
|
|
Until then, the schemas are usable with any draft-2020-12 validator (ajv, etc.).
|
|
The extractor (T05) MUST validate its output against these schemas before writing.
|