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
59 changes: 59 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: release

# Publishes @aestheticfunction/dspack-spec (schemas, spec, reference
# contracts, and the dspack-validate bin) when a version tag is pushed.
# Mirrors ds-mcp's release workflow. The bin is a front-end over
# scripts/validate.mjs — the single validator implementation
# (rfc/dx3-bootstrap-design.md §4); this workflow distributes it, never
# forks it.
#
# Auth: npm trusted publishing (OIDC) — no NPM_TOKEN secret in the repo.
# One-time setup on npmjs.com (package settings -> Trusted publisher):
# repository aestheticfunction/dspack, workflow release.yml.
#
# Guards:
# - tag must equal the package.json version
# - full validation (positive + negative fixtures + --file both ways)
# runs before publish

on:
push:
tags:
- "v*"

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # OIDC for npm trusted publishing / provenance
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
# Node 24 bundles npm >= 11.5, the minimum for OIDC trusted
# publishing (see dspack-gen v0.1.1's E404 incident).
node-version: 24
cache: npm
registry-url: https://registry.npmjs.org
- name: Verify tag matches package.json version
run: |
pkg="$(node -p "require('./package.json').version")"
tag="${GITHUB_REF_NAME#v}"
if [ "$pkg" != "$tag" ]; then
echo "tag v$tag does not match package.json version $pkg" >&2
exit 1
fi
- run: npm ci
- run: npm run validate
- run: npm run validate -- --fixtures negative
- name: File mode accepts a valid document
run: npm run validate -- --file examples/shadcn-ui.dspack.json
- name: File mode rejects an invalid document
run: |
if npm run validate -- --file fixtures/negative/rule-unknown-component-ref.dspack.json; then
echo "file mode unexpectedly accepted a negative fixture" >&2
exit 1
fi
- name: Publish (public, with provenance)
run: npm publish --access public
10 changes: 10 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,13 @@ jobs:
run: npm run validate
- name: Negative fixtures (must all be rejected)
run: npm run validate -- --fixtures negative
- name: File mode accepts a valid document
run: npm run validate -- --file examples/shadcn-ui.dspack.json
- name: File mode rejects an invalid document
# Guards the fail-first behavior: the pre---file harness silently
# ignored the flag and exited 0 on this exact invocation.
Comment on lines +25 to +26
run: |
if npm run validate -- --file fixtures/negative/rule-unknown-component-ref.dspack.json; then
echo "file mode unexpectedly accepted a negative fixture" >&2
exit 1
fi
71 changes: 35 additions & 36 deletions ADOPTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ there.)
| # | Step | Mode |
|---|------|------|
| 1 | [Extract a snapshot from your codebase](#step-1--extract-a-snapshot) | 🤖 Automated |
| 2 | [Set the version to the current spec](#step-2--set-the-version) | 🤖 Automated |
| 2 | [~~Set the version to the current spec~~](#step-2--set-the-version) — retired; snapshots declare it | 🤖 Automated |
| 3 | [Review the snapshot against reality](#step-3--review-the-snapshot) | 👀 Human review |
| 4 | [Author component guidance](#step-4--author-component-guidance) | ⚖️ Design decision |
| 5 | [Define categories and tag components](#step-5--define-categories) | ⚖️ Design decision |
Expand All @@ -40,7 +40,7 @@ there.)
| 11 | [Serve it to agents](#step-11--serve-it-to-agents) | 🤖 Automated |
| 12 | [Generate, lint, and render](#step-12--generate-lint-render) | 🤖 Automated (rendering profile: ⚖️) |

Steps 1–2 assume a React + Tailwind/shadcn or Vue 3 + Vuetify 3 codebase
Step 1 assumes a React + Tailwind/shadcn or Vue 3 + Vuetify 3 codebase
(what [dspack-export](https://github.com/aestheticfunction/dspack-export)
currently supports). For any other stack, skip to
[Writing the snapshot by hand](#writing-the-snapshot-by-hand) — a valid
Expand All @@ -53,17 +53,17 @@ was written entirely by hand.

### Step 1 · Extract a snapshot — 🤖 Automated

dspack-export is experimental and not yet on npm; install from source:

```bash
git clone https://github.com/aestheticfunction/dspack-export
cd dspack-export && npm install && npm run build && npm link
npm install -g @aestheticfunction/dspack-export

cd /path/to/your/design-system
dspack-export init # detects conventions, writes dspack-export.config.json
dspack-export generate --config dspack-export.config.json
```

(Installing from a source clone — `npm install && npm run build && npm link`
in a checkout — works identically.)

You get a `<name>.dspack.json` (named from your `package.json` name, or a
placeholder if there isn't one) containing everything observable from
source: `components` with props (cva variant enums and defaults on React;
Expand All @@ -74,27 +74,26 @@ what generated it. If your tokens live in Figma or Tokens Studio, export them
to a DTCG file and point the config's `tokens` field at it — the import is
file-based; no tool integration or network involved.

Two properties of the output matter later:
Three properties of the output matter later:

- It is **deterministic** — same input, same bytes — so you can commit it and
diff regenerations.
- Its metadata warns that hand-authored sections **will be overwritten on
regeneration**. Treat the snapshot as a one-shot starting point: commit it,
then enrich the committed copy. Do not regenerate over your enrichments.

### Step 2 · Set the version to the current spec — 🤖 Automated

The snapshot declares `"dspack": "0.2"`. The spec's versions are strictly
additive — a v0.2-shaped document is a valid v0.4 document — so update the
label (and, if present, the `$schema` pointer) to the current version:

```json
{ "dspack": "0.4", ... }
```

That is the whole step. Nothing else changes. The governance blocks you add
in Phase 2 (`categories`, `intents`, `rules`, `examples`) exist only from
v0.3/v0.4, which is why the label moves before you start writing them.
- It carries a non-semantic `metadata["x-bootstrap"]` **ledger** recording
which sections the tool generated (with content hashes) and which
governance surfaces await your authorship. It affects nothing downstream;
once your contract is fully yours, delete it — after which the tool
refuses to touch the file at all.
- **Regeneration never destroys your work — guaranteed, not advised.** The
workflow is: commit the snapshot, then enrich the committed copy. If you
run `generate` against a file containing anything you authored (or any
file without the ledger), it refuses, says why, and points you at
`--out` for writing a fresh snapshot elsewhere to compare by hand.

### Step 2 · Set the version — retired

Nothing to do: snapshots declare the current spec version (`"dspack":
"0.4"`) directly. (This step existed when the tool emitted the older v0.2
shape; the number is kept so later steps keep their names.)

### Step 3 · Review the snapshot — 👀 Human review

Expand Down Expand Up @@ -261,24 +260,24 @@ reference for what each block looks like fully populated.

### Step 10 · Validate — 🤖 Automated

The validation harness lives in this repository and runs over
`examples/*.dspack.json`. Point it at your contract by dropping the file
into a clean checkout's `examples/` directory:
```bash
npx -p @aestheticfunction/dspack-spec dspack-validate --file your-system.dspack.json
```

Or, from a checkout of this repository:

```bash
git clone https://github.com/aestheticfunction/dspack
cd dspack && npm ci
cp /path/to/your-system.dspack.json examples/
npm run validate
npm run validate -- --file /path/to/your-system.dspack.json
```

You get, per document: schema validation for its declared version,
back-compat (the additive guarantee), governance consistency (unique ids;
intent, component, and example references resolve; every example surface
passes S1 + S2), and categories consistency. Fix what it reports; re-run
until green. (Your file stays local — `examples/` in your checkout is just
where the harness looks. A standalone `validate --file` entry point is a
known gap on the roadmap.)
Both are the same validator — the published bin is a front-end over this
repository's harness, never a fork. You get, per document: schema
validation for its declared version, back-compat (the additive guarantee),
governance consistency (unique ids; intent, component, and example
references resolve; every example surface passes S1 + S2), and categories
consistency. Fix what it reports; re-run until green.

### Step 11 · Serve it to agents — 🤖 Automated

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Think of it as OpenAPI for design systems.

> Part of the [dspack ecosystem](https://github.com/aestheticfunction) — the organization profile has the full map of how the repositories fit together.
>
> **Kind:** specification (spec, schemas, examples, validation harness — not an npm package) · **Audience:** design-system practitioners and tool implementers · **Neighbors:** consumed by [ds-mcp](https://github.com/aestheticfunction/ds-mcp), [dspack-gen](https://github.com/aestheticfunction/dspack-gen), [dspack-emit](https://github.com/aestheticfunction/dspack-emit), and [dspack-studio](https://github.com/aestheticfunction/dspack-studio); bootstrapped from code by [dspack-export](https://github.com/aestheticfunction/dspack-export)
> **Kind:** specification (spec, schemas, examples, validation harness; npm `@aestheticfunction/dspack-spec` ships the `dspack-validate` bin) · **Audience:** design-system practitioners and tool implementers · **Neighbors:** consumed by [ds-mcp](https://github.com/aestheticfunction/ds-mcp), [dspack-gen](https://github.com/aestheticfunction/dspack-gen), [dspack-emit](https://github.com/aestheticfunction/dspack-emit), and [dspack-studio](https://github.com/aestheticfunction/dspack-studio); bootstrapped from code by [dspack-export](https://github.com/aestheticfunction/dspack-export)
>
> **Adopting dspack for an existing design system?** The complete journey — snapshot, governance authoring, validation, serving — is walked step by step in [ADOPTING.md](./ADOPTING.md).

Expand Down
17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"name": "@aestheticfunction/dspack-spec",
"version": "0.4.0",
"private": true,
"description": "Validation harness for the dspack specification artifacts: schemas, examples, and negative fixtures.",
"description": "The dspack specification: spec documents, JSON Schemas, reference example contracts, and the validation harness (bin: dspack-validate).",
"type": "module",
"license": "Apache-2.0",
"scripts": {
Expand All @@ -14,5 +13,17 @@
},
"engines": {
"node": ">=20.0.0"
}
},
"bin": {
"dspack-validate": "scripts/validate.mjs"
},
"files": [
"schema",
"scripts/validate.mjs",
"spec",
"examples/README.md",
"examples/shadcn-ui.dspack.json",
"examples/astryx.dspack.json",
"ADOPTING.md"
]
}
26 changes: 23 additions & 3 deletions scripts/validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
* exits 0 iff every fixture is rejected (each must fail schema validation
* or a consistency check). A fixture that unexpectedly passes is a harness
* defect and fails the run.
*
* File mode (`npm run validate -- --file <path> [<path>...]`, also the
* `dspack-validate` bin):
* Runs the identical validation (schema, back-compat, governance
* consistency, categories) over the named document(s) instead of
* examples/. This is the standalone surface from rfc/dx3-bootstrap-design
* §4 — a front-end over the same function, never a second validator.
*/
import { readFileSync, readdirSync } from "node:fs";
import { join, dirname, basename } from "node:path";
Expand Down Expand Up @@ -321,6 +328,18 @@ function listDocs(dir) {
function main() {
const args = process.argv.slice(2);
const negativeMode = args.includes("--fixtures") && args[args.indexOf("--fixtures") + 1] === "negative";
// --file <path> [<path>...]: every argument after the flag that does not
// start with "-" names a document to validate.
let fileArgs = null;
const fileFlag = args.indexOf("--file");
if (fileFlag !== -1) {
fileArgs = [];
for (let i = fileFlag + 1; i < args.length && !args[i].startsWith("-"); i++) fileArgs.push(args[i]);
if (fileArgs.length === 0) {
console.error("usage: validate --file <path.dspack.json> [<path>...]");
process.exit(1);
}
}
Comment on lines 328 to +342

const { validators, failures } = compileSchemas();
if (!validators.has(SURFACE_SCHEMA)) failures.push(`${SURFACE_SCHEMA}: missing`);
Expand Down Expand Up @@ -355,7 +374,7 @@ function main() {
return;
}

const docs = listDocs(EXAMPLES_DIR);
const docs = fileArgs ?? listDocs(EXAMPLES_DIR);
if (docs.length === 0) {
console.error(`no examples found in ${EXAMPLES_DIR}`);
process.exit(1);
Comment on lines +377 to 380
Expand Down Expand Up @@ -404,11 +423,12 @@ function main() {
console.log(` ✔ ${basename(path)} (dspack ${doc.dspack})`);
}
}
const label = fileArgs ? "documents" : "examples";
if (failed) {
console.error(`examples FAIL (${failed} document(s) invalid)`);
console.error(`${label} FAIL (${failed} document(s) invalid)`);
process.exit(1);
}
console.log(`examples PASS (${docs.length} document(s))`);
console.log(`${label} PASS (${docs.length} document(s))`);
}

main();
Loading