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>
3.8 KiB
Design note — consumer-side contract parity (WHYNOT-WP-0003 · T09)
Status: design-only. Deferred — do not implement. This note captures the shape of the richer drift mode so the decision to build it is informed, not so it gets built now. The must-have is the snapshot diff (
drift, T05); this is the heavier, per-stack second mode.
What it is
The shipped drift check (T05) compares two manifests — the consumer's
adopted .whynot-design.lock against a target ir/manifest.json. It answers
"what changed in the contract between the version I adopted and this one?" It
never looks at the consumer's actual UI.
Contract parity is the inverse question: "does my live rendered UI still
match the contract I adopted?" It compares a consumer's live rendered
elements' observed attributes / properties against the IR component contracts
(ir/components/*.json) — the consumer-side mirror of the upstream adapter parity
(WHYNOT-WP-0002 · T08), which checks the Lit stack against the designbook
exemplars.
T05 (shipped): .whynot-design.lock ── snapshot diff ──▶ ir/manifest.json
(contract vs contract, version-to-version)
T09 (this note): live rendered elements ── parity ──▶ ir/components/*.json
(running UI vs contract, per-stack introspection)
Why it is heavier (the reason to defer)
Snapshot diff is pure data: hash vs hash, no runtime, no DOM, network-free, stack-agnostic. Contract parity needs to observe a running UI, which makes it fundamentally per-stack:
- Introspection substrate. You must render each component and read back its realised attributes/properties/slots — a browser/JSDOM/per-framework harness the consumer has to stand up. There is no framework-neutral way to enumerate "what props did this element actually accept."
- Coverage problem. A consumer renders components with its own prop combinations; parity can only check what the consumer actually mounts, so "no parity failures" ≠ "fully conformant." It needs a fixture/exemplar set, which re-introduces per-stack authoring.
- Non-portable props. React objects / render-props / callbacks
(
portable:falsein the IR) have no attribute form to observe — exactly the class the adapter contract already surfaces as drift. Parity would have to decide, per stack, what "matching" even means for them. - Maintenance surface. It is a second, stack-specific tool to keep in step with every IR shape change, for a check the snapshot diff already covers at the contract level.
Proposed shape (if/when built)
- A
paritysubcommand alongsidedrift, opt-in, requiring the consumer to declare a render harness + a fixture set (which elements, which prop combos). - Reuse of the adapter parity result shape (
adapters/ADAPTER_CONTRACT.md"Parity result — minimal machine shape") so upstream and downstream parity read identically, and the existing exit codes (0ok ·4parity failure). - Per-element issues drawn from the same vocabulary as adapter drift:
prop-missing,attribute-mismatch,variant-missing,removed-prop,non-portable.
Decision
Defer. Ship the snapshot diff (drift, T05) as the must-have downstream
check; record contract parity as a known, designed-but-unbuilt second mode. It
becomes worth building only when a consuming repo has a real conformance need
(e.g. an automated gate that its live UI has not silently diverged from an adopted
contract) — at which point this note is the starting blueprint. Tracked as the
go/defer decision recorded against this workplan via record_decision.
This also keeps the one-way constraint intact: like drift, a future
parity is read-only against the package and writes nothing back to
whynot-design — it only observes the consumer's own UI.