Files
whynot-design/ir/SCHEMA.md
tegwick 2de30beb7b
Some checks failed
ci / check (push) Has been cancelled
ci / release (push) Has been cancelled
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>
2026-06-27 19:35:45 +02:00

7.8 KiB

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:

{
  "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.

{
  "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:

{
  "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:

{
  "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

# 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.