Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/CONTRACT-PIN.md
Original file line number Diff line number Diff line change
@@ -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.
67 changes: 64 additions & 3 deletions packages/contracts/scripts/check-sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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
Comment on lines 49 to +53
// 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",
},
},
];

Expand All @@ -57,17 +87,48 @@ 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);
} catch {
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) {
Expand Down
Loading