From a953a45792e9f15e80d5049b9f8d3c7e67d3fd51 Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Wed, 5 Aug 2026 19:17:04 -0400 Subject: [PATCH] build(contracts): pin the shadcn contract to v2.3.0 with provenance and teeth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dspack#35 merged the 32-component production contract, so check:sync — which follows dspack@main — went red on every branch here without a line of code changing. The harm is worse than a red check. check:sync runs in CI BEFORE the unit tests, so once upstream moved, unit tests, type checks and Playwright were all skipped rather than run. The repo stopped producing test evidence silently, which is precisely the condition under which renderer-parity work cannot be trusted — and renderer parity exists to make representation claims trustworthy. Following main would not widen coverage: the shadcn renderers, the emit profile and the scenario surfaces here were all built against v2.3.0, so it would break catalog builds and scenario surfaces, and couple unrelated work to a migration that cannot land until the emitter can represent the production catalog. So pin at the exact upstream commit this repo was built against (805732c, v2.3.0). The local copy is byte-identical to it — the same artifact dspack-emit pins, the same sha256 — so nothing is forked or re-authored to go green. A pin here is a stronger claim than tracking, not a weaker one. It verifies the pinned artifact's sha256 every run (a pinned ref must be immutable — a change means force-push or CDN mismatch, never a routine update), still fails on local drift, always reports how far behind main it sits so the 8-component contract can never read as current production coverage, and carries an explicit removal condition plus a tracking issue (#48). Verified all three states: clean pin exits 0; a mutated expected hash exits 1 with TAMPERED; a one-byte local edit exits 1. Refs #48 Co-Authored-By: Claude Opus 5 --- docs/CONTRACT-PIN.md | 51 +++++++++++++++++ packages/contracts/scripts/check-sync.mjs | 67 ++++++++++++++++++++++- 2 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 docs/CONTRACT-PIN.md diff --git a/docs/CONTRACT-PIN.md b/docs/CONTRACT-PIN.md new file mode 100644 index 0000000..3651b3b --- /dev/null +++ b/docs/CONTRACT-PIN.md @@ -0,0 +1,51 @@ +# The shadcn contract pin + +`packages/contracts/shadcn-ui.dspack.json` is **pinned to one upstream commit** instead of tracking `dspack@main`. This file is the record of that decision. + +A pin is not staleness. Staleness is silent and is discovered when something breaks; a pin names the exact bytes this repo was built against, verifies them by hash on every CI run, reports how far behind `main` it sits, and states what has to be true before it is removed. + +## The pin + +| | | +|---|---| +| **Pinned ref** | [`805732c154f0f214721c9934a450b0edb2656c99`](https://github.com/aestheticfunction/dspack/commit/805732c154f0f214721c9934a450b0edb2656c99) — *feat(shadcn): record-collection intent (v2.3.0)*, 2026‑07‑22 | +| **Pinned contract** | shadcn/ui **v2.3.0** — 8 components, 39 sub-components, 2 intents, 8 rules, 2 worked examples, 70 040 bytes | +| **sha256** | `ca19f8410a97f2004cf1d6f6dd2d7542abccfbb5430b756e0ccdc1ee954c7bb7` | +| **Current upstream** | shadcn/ui **v3.0.0** at [`48643ff`](https://github.com/aestheticfunction/dspack/commit/48643ff) (merged as [`b573637`](https://github.com/aestheticfunction/dspack/commit/b573637), dspack#35) — 32 components, 106 sub-components, 11 intents, 48 rules, 14 worked examples, 460 066 bytes | +| **Tracking issue** | aestheticfunction/dspack-studio#48 | + +The pinned copy is byte-identical to that upstream commit — the same artifact `dspack-emit` pins, verified by the same sha256. Nothing is forked: no contract content was copied, edited, or re-authored to make the check pass. + +> **The pinned contract is not current shadcn/ui coverage.** It describes 8 of the 32 components the design system's contract now governs. Do not cite this repo's catalogs, scenario surfaces, or renderer coverage as a statement about shadcn/ui support. + +## Why the pin exists + +dspack#35 merged the production contract (a 4× vocabulary expansion) on 2026‑08‑05. The shadcn renderers, the emit profile, and the scenario surfaces here were all built against v2.3.0. + +The sync check runs in CI **before** the unit tests, so once upstream moved, every downstream step — unit tests, type checks, Playwright — was skipped rather than run. That is the concrete harm: not a warning, but a repo whose test evidence silently stopped being produced. + +Following `main` would not have widened coverage; it would have broken catalog builds and scenario surfaces, and coupled unrelated work to a migration that cannot land yet. + +## Removal condition + +Replace the pin with the production contract only after **the dspack-emit representation foundation lands and the profile migration completes** — the emitter must be able to represent the production catalog before this repo consumes it. Tracked in aestheticfunction/dspack-emit#28, whose checklist is the gating work: + +- [ ] profile v2 schema + explicit `profileVersion` dispatch +- [ ] v1 directive desugaring into the internal Identity/Route/Collect model +- [ ] load-time validation of selectors and destinations +- [ ] surface fidelity reporting + `--strict-surface` +- [ ] sub-component coverage derived and enforced +- [ ] `functions` support on the profile/catalog path +- [ ] the restated parity invariant + +Then: remove the `pin` block, `node scripts/check-sync.mjs --write`, rebuild catalogs (`build:catalogs`), re-record scenario surfaces, and commit them together. + +## How the pin is enforced + +`packages/contracts/scripts/check-sync.mjs` runs in CI on every push and PR. For a pinned entry it: + +1. fetches the artifact at the pinned commit and **fails if its sha256 differs from the recorded hash** — a pinned ref must be immutable, so a change means a force-push, history rewrite or CDN mismatch, never a routine update; +2. fails if the local copy drifts from those exact bytes, as before; +3. fetches the tracked branch and **always prints how far behind the pin sits**, so it can never quietly read as current. + +Verified: clean pin exits 0; a mutated expected hash exits 1 with `TAMPERED`; a one-byte local edit exits 1. diff --git a/packages/contracts/scripts/check-sync.mjs b/packages/contracts/scripts/check-sync.mjs index cbfab53..1690a9b 100644 --- a/packages/contracts/scripts/check-sync.mjs +++ b/packages/contracts/scripts/check-sync.mjs @@ -16,15 +16,27 @@ * change lands in dspack first; the consuming dspack-studio PR carries * the byte-copied file; sync checks are green in both repositories. * + * An entry may carry a `pin`: a deliberate hold at one upstream commit rather + * than following a branch. A pin is a STRONGER claim than tracking, not a + * weaker one — it asserts the exact bytes this repo was built against, is + * verified by sha256 on every run, always reports how far behind the tracked + * branch it sits, and carries an explicit removal condition. Staleness hides; + * a pin announces itself. See docs/CONTRACT-PIN.md. + * * Boring by design: node builtins + global fetch, one retry, no deps. * `--write` re-syncs local copies from canonical. */ +import { createHash } from "node:crypto"; import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const RAW = "https://raw.githubusercontent.com/aestheticfunction/dspack"; + +const sha256 = (buf) => createHash("sha256").update(buf).digest("hex"); + const MANIFEST = [ { local: join(root, "astryx.dspack.json"), @@ -36,9 +48,27 @@ const MANIFEST = [ { local: join(root, "shadcn-ui.dspack.json"), label: "shadcn-ui.dspack.json", - source: - "https://raw.githubusercontent.com/aestheticfunction/dspack/main/examples/shadcn-ui.dspack.json", + source: `${RAW}/805732c154f0f214721c9934a450b0edb2656c99/examples/shadcn-ui.dspack.json`, note: "the shadcn contract — copy of the spec repo's source of truth", + // A DELIBERATE PIN, not staleness. dspack main now carries the + // 32-component production contract, but the shadcn renderers, the emit + // profile and the scenario surfaces here were all built against the + // 8-component v2.3.0 contract. Migrating before the emitter can represent + // the production catalog would break catalog builds and scenario surfaces + // rather than widen coverage. See docs/CONTRACT-PIN.md. + pin: { + ref: "805732c154f0f214721c9934a450b0edb2656c99", + version: "2.3.0", + // Teeth: a pinned ref must be immutable. If the bytes behind it change + // (force-push, history rewrite, CDN mismatch), fail loudly rather than + // quietly re-syncing to something this repo was never built for. + sha256: "ca19f8410a97f2004cf1d6f6dd2d7542abccfbb5430b756e0ccdc1ee954c7bb7", + tracks: `${RAW}/main/examples/shadcn-ui.dspack.json`, + removeWhen: + "the dspack-emit representation foundation lands and the profile migration completes — " + + "the emitter must be able to represent the production catalog first", + issue: "aestheticfunction/dspack-studio#48", + }, }, ]; @@ -57,9 +87,40 @@ async function fetchSource(url) { } } +/** A pinned ref must be immutable; report how far behind the tracked branch it sits. */ +async function reportPin(entry, source) { + const { pin } = entry; + const actual = sha256(source); + if (actual !== pin.sha256) { + console.error(`TAMPERED ${entry.label} the PINNED artifact itself changed`); + console.error(` ref ${pin.ref}`); + console.error(` expected sha256 ${pin.sha256}`); + console.error(` actual sha256 ${actual}`); + console.error(` a pinned commit must be immutable — investigate before re-syncing.`); + return false; + } + let ahead = "unavailable"; + try { + const head = await fetchSource(pin.tracks); + ahead = head.equals(source) + ? "none — main matches the pin; the pin can be lifted" + : `main has moved (v${JSON.parse(head.toString()).version}, ${head.length} bytes vs pinned ${source.length})`; + } catch { + /* offline: the pin still verifies against its own hash */ + } + console.log(`PINNED ${entry.label} v${pin.version} @ ${pin.ref.slice(0, 7)} (sha256 verified)`); + console.log(` upstream drift: ${ahead}`); + console.log(` NOT current production shadcn coverage — see docs/CONTRACT-PIN.md (${pin.issue})`); + return true; +} + let drifted = 0; for (const entry of MANIFEST) { const source = await fetchSource(entry.source); + if (entry.pin && !(await reportPin(entry, source))) { + drifted++; + continue; + } let local; try { local = readFileSync(entry.local); @@ -67,7 +128,7 @@ for (const entry of MANIFEST) { local = null; } if (local && source.equals(local)) { - console.log(`in sync ${entry.label}`); + if (!entry.pin) console.log(`in sync ${entry.label}`); continue; } if (write) {