# 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/.json manifest.schema.json ← JSON Schema for manifest.json (version + hashes) tokens.json ← all design tokens, W3C DTCG format (emitted by T05) components/.json ← one contract per component (emitted by T05) exemplars/.{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/.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 `` (`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