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
9 changes: 4 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ jobs:
- name: Contract gates (A2UI 0.9.1 + 1.0) and scenario surfaces
run: pnpm --filter @dspack-studio/contracts build:catalogs
- name: Contract copies match the authoritative dspack main (sync check)
run: |
curl -sf https://raw.githubusercontent.com/aestheticfunction/dspack/main/examples/astryx.dspack.json \
| diff - packages/contracts/astryx.dspack.json
curl -sf https://raw.githubusercontent.com/aestheticfunction/dspack/main/examples/shadcn-ui.dspack.json \
| diff - packages/contracts/shadcn-ui.dspack.json
# THE INVARIANT (PHASE-NEXT P0): at every green commit on main, the
# canonical Astryx contract in dspack and the copy consumed here are
# byte-identical. Locally: pnpm --filter @dspack-studio/contracts check:sync
run: pnpm --filter @dspack-studio/contracts check:sync
- name: Astryx vocabulary drift check (report-only, local CLI)
run: pnpm --filter @dspack-studio/contracts drift-check
- name: Unit tests
Expand Down
57 changes: 33 additions & 24 deletions docs/PHASE-NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,21 @@ blocks the shipped experience.
imports only in their own renderer package.
- Published packages are consumed from npm, never vendored, with the single
documented exception of ds-mcp's build-time core bundle.
- `packages/contracts/astryx.dspack.json` byte-syncs with dspack main.
**This constraint is currently unmet** — see P0. The example-expansion
milestone authored governance (3 intents, 9 rules, 3 examples, 3
components) directly in the studio copy through reviewed PRs; upstream
`dspack/examples/astryx.dspack.json` still carries the 3-intent,
9-component edition. Restoring the sync is the roadmap's first item.
- **The synchronization invariant** (restored and formalized by P0): at
every green commit on main, the canonical Astryx contract in
`dspack/examples/` and the copy consumed in `packages/contracts/` are
**byte-identical** — enforced by `check:sync` in this repo's CI and by
dspack-gen's check-sync on its fixture copy. Repository
synchronization is absolute byte equality; design-system fidelity
(documented `x-drift` divergences from the Astryx component API,
checked by `drift-check.ts`) lives *inside* the synchronized artifact.
The two are never conflated: `x-drift` annotates the contract's
relationship to Astryx, not any repo-to-repo difference.
- **Contract-change merge protocol (the definition of done)**: the
canonical change lands in `dspack` first; the consuming dspack-studio
PR carries the byte-copied file (which keeps intent-plus-scenario PRs
atomic — the studio PR merges after its paired upstream PR);
synchronization checks are green in both repositories.
- All governance content (intents, rules, rationales, examples) is
owner-authored. This document specifies required shapes only.
- Public-facing copy follows AF brand voice: declarative, unhyped,
Expand All @@ -53,19 +62,19 @@ output pasted into the PR); fixtures ship only from real model runs;
copy is written from recorded events, never the reverse; new intents land
atomically with the scenario or surface that validates them.

## P0 — Contract re-sync upstream (restores a standing constraint)
## P0 — Contract re-sync upstream — **DELIVERED 2026-07-21**

The studio's Astryx contract is the current source of truth in fact but
not in declared direction: the byte-sync discipline names dspack main as
canonical, and the take-home view tells visitors the downloaded contract
"is the byte-synced copy of dspack main". Until upstream catches up, that
user-facing sentence over-claims.

Work: propagate the studio's `astryx.dspack.json` (and the unchanged
`shadcn-ui.dspack.json`) to `dspack/examples/` via a dspack-repo PR, then
re-assert byte-equality here. Owner decision folded in: whether the sync
direction stays studio-led during active milestones or reverts to
upstream-led between them. Small, mechanical, high honesty value.
The one-time studio → dspack catch-up landed as dspack #22 (the
milestone's governance consumed into the canonical copy; verified
superset, nothing upstream-only lost). Steady state is upstream-first:
canonical changes land in dspack, the studio consumes byte-identical
copies. Enforcement: `packages/contracts/scripts/check-sync.mjs`
(`pnpm --filter @dspack-studio/contracts check:sync`, `--write` to
re-sync locally), run by CI on every push and PR; dspack-gen's existing
Comment on lines +71 to +73
check-sync guards its fixture copy the same way. The invariant and the
contract-change merge protocol are stated in the standing constraints
above. The take-home view's "byte-synced copy of dspack main" sentence
is accurate again.

## P1 — shadcn contract parity (owner-authored, upstream)

Expand Down Expand Up @@ -130,9 +139,9 @@ for any documented divergence) is established and mechanical.

## The first task

As always, the highest-leverage first task is the one that restores a
stated invariant: **P0's byte-equality check**. Sync the contract
upstream, then add the studio-side assertion that
`packages/contracts/astryx.dspack.json` byte-matches the dspack-main
copy, failing output first. Everything else on this roadmap can proceed
in any order after it.
P0 delivered the invariant-restoring first task (sync landed, check
wired, failing output captured first). The next milestone's
highest-leverage opening move belongs to **P1**: the owner-authored
shadcn intent extension upstream in `dspack/examples/shadcn-ui.dspack.json`
— which now flows through the P0 protocol by construction: canonical
change first, byte-copied consumption second, both sync checks green.
1 change: 1 addition & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"scripts": {
"build:catalogs": "tsx src/build.ts",
"drift-check": "tsx src/drift-check.ts",
"check:sync": "node scripts/check-sync.mjs",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json"
},
Expand Down
85 changes: 85 additions & 0 deletions packages/contracts/scripts/check-sync.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env node
/**
* Contract-copy sync check (PHASE-NEXT P0), the dspack-gen check-sync
* pattern verbatim: the studio deliberately carries byte-copies of the
* canonical contracts in dspack/examples, and the price of copies is
* silent drift — this script makes drift loud.
*
* THE INVARIANT: at every green commit on main, the canonical Astryx
* contract in dspack and the consumed copy here are BYTE-IDENTICAL.
* Repository synchronization is absolute byte equality; design-system
* fidelity (documented `x-drift` divergences from the Astryx component
* API) lives INSIDE the synchronized artifact and is checked separately
* by drift-check.ts — the two must never be conflated.
*
* Contract-change merge protocol (the definition of done): canonical
* change lands in dspack first; the consuming dspack-studio PR carries
* the byte-copied file; sync checks are green in both repositories.
*
* Boring by design: node builtins + global fetch, one retry, no deps.
* `--write` re-syncs local copies from canonical.
*/
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 MANIFEST = [
{
local: join(root, "astryx.dspack.json"),
label: "astryx.dspack.json",
source:
"https://raw.githubusercontent.com/aestheticfunction/dspack/main/examples/astryx.dspack.json",
note: "the Astryx contract — copy of the spec repo's source of truth",
},
{
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",
note: "the shadcn contract — copy of the spec repo's source of truth",
},
];

const write = process.argv.includes("--write");

async function fetchSource(url) {
for (let attempt = 1; ; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return Buffer.from(await response.arrayBuffer());
} catch (error) {
if (attempt >= 2) throw new Error(`fetching ${url}: ${error.message ?? error}`);
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
}

let drifted = 0;
for (const entry of MANIFEST) {
const source = await fetchSource(entry.source);
let local;
try {
local = readFileSync(entry.local);
} catch {
local = null;
}
if (local && source.equals(local)) {
console.log(`in sync ${entry.label}`);
continue;
}
if (write) {
writeFileSync(entry.local, source);
console.log(`SYNCED ${entry.label} <- ${entry.source}`);
console.log(` rebuild catalogs (build:catalogs) before committing.`);
} else {
drifted++;
console.error(`DRIFT ${entry.label} (${entry.note})`);
console.error(` differs from ${entry.source}`);
console.error(` canonical change lands in dspack first; then either`);
console.error(` node scripts/check-sync.mjs --write, or copy this file upstream via a dspack PR.`);
}
Comment on lines +81 to +83
}
if (drifted > 0) process.exit(1);
Loading