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

# Publishes @aestheticfunction/ds-mcp to npm when a version tag is pushed.
# Mirrors dspack-gen's release workflow (PHASE-NEXT WS0, task 0.5): with the
# pin-drift check in test.yml, a red check leads to a re-pin PR and the
# release is one merge plus one tag away.
#
# Auth: npm trusted publishing (OIDC) — no NPM_TOKEN secret in the repo.
# One-time setup on npmjs.com (package settings -> Trusted publisher):
# repository aestheticfunction/ds-mcp, workflow release.yml.
# Provenance attestations are generated automatically for trusted publishes.
#
# Guards:
# - tag must equal the package.json version (prevents accidental mismatches)
# - full test suite runs before publish (build is pretest, so the vendored
# core bundle published is the one the suite just validated)
# - publishing an already-published version fails the job (npm rejects dupes)

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. Node 20's npm 10 skips the token exchange entirely
# and the unauthenticated PUT fails as E404 (dspack-gen v0.1.1's
# first release run hit exactly this).
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 test
- name: Publish (public, with provenance)
run: npm publish --access public
13 changes: 13 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,16 @@ jobs:
- run: npm test
- name: Copy drift check (dspack-gen#7)
run: node scripts/check-sync.mjs

core-pin-drift:
# Report-only posture: red means dspack-gen published a release whose
# src/core differs from the vendored-core pin — the fix is the re-pin
# ritual (README). API failures warn and pass; real drift fails.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Vendored-core pin matches dspack-gen's latest release
run: node scripts/check-core-pin.mjs
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Changelog

## 0.3.1

The vendored `@aestheticfunction/dspack-gen/core` bundle moves from commit
`f651433` to the v0.1.1 release tag (`374e1cd`), picking up the 0.1.1
generation-schema fixes for grammar-constrained decoders: array-typed
contract props emit as arrays (with contract-declared `items` passthrough),
and node properties declare `component, id, props, text, children` in that
order. The published 0.3.0 served the pre-fix schema from
`get-generation-context`; a grammar-constrained decoder driven by that
schema could not emit node text after props and degraded array props to
strings.

- Re-pin: `@aestheticfunction/dspack-gen` devDependency ->
`374e1cd` (the v0.1.1 tag commit), vendor bundle rebuilt.
- New golden-context test: `get-generation-context` output byte-compares
against dspack-gen's own compiler golden
(`fixtures/golden/context/shadcn.destructive-action.json`), synced as a
tracked byte copy in `scripts/check-sync.mjs`. Byte-level on purpose:
grammar-constrained decoders enforce property order, which deepEqual
cannot see. The test fails on the 0.3.0 bundle with the pre-fix
signature (`text` declared before `props`).
- New pin-drift CI check: `scripts/check-core-pin.mjs` compares the pinned
commit's shipped `src/core` files (blob-exact, via the git trees API)
against dspack-gen's latest release tag and fails loudly on drift.
Test files are excluded; GitHub API failures warn and pass (report-only
posture); real drift always fails.
- Release workflow: npm trusted publishing (OIDC) on version tags,
mirroring dspack-gen's. Requires one-time trusted-publisher registration
on npmjs.com.

## 0.3.0

Baseline for this changelog. Self-contained npm package: dspack-gen's
`core` subpath bundled at build time (`dist/vendor/dspack-gen-core.js`)
from a commit-pinned devDependency; generation tools
(`get-generation-context`, `validate-ui`) backed by the bundle; network
boundary enforced by test.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,16 @@ the guarantee.
dspack-gen is a **build-time** dependency: its `core` subpath is bundled
into the published package (`dist/vendor/dspack-gen-core.js`) from a
commit-pinned devDependency, so installing ds-mcp from npm pulls nothing
from git. Updating governance semantics means re-pinning that commit,
rebuilding, and republishing ds-mcp.
from git. The default posture is **tag-pinned**: the pin is the commit of
dspack-gen's latest release tag, so the vendored core corresponds exactly
to a published dspack-gen version. CI watches this two ways:
`scripts/check-core-pin.mjs` fails loudly when a newer dspack-gen release
changes shipped `src/core` files (test files are excluded — the documented
escape for intentional ahead-of-release pins), and the golden-context test
byte-compares `get-generation-context` output against dspack-gen's own
compiler golden. Updating governance semantics means re-pinning to the new
release tag commit, `npm install`, `npm run build`, verifying the golden
tests, and republishing ds-mcp.

## Requirements

Expand Down
19 changes: 11 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aestheticfunction/ds-mcp",
"version": "0.3.0",
"version": "0.3.1",
"description": "MCP server for exposing dspack design system corpora to AI coding agents",
"license": "Apache-2.0",
"type": "module",
Expand All @@ -14,6 +14,7 @@
"examples/",
"docs/",
"README.md",
"CHANGELOG.md",
"LICENSE",
"SECURITY.md"
],
Expand Down Expand Up @@ -62,7 +63,7 @@
"zod": "^3.25 || ^4.0"
},
"devDependencies": {
"@aestheticfunction/dspack-gen": "git+https://github.com/aestheticfunction/dspack-gen.git#f6514330964ed7b62371e17eec4cdadaeeef9d52",
"@aestheticfunction/dspack-gen": "git+https://github.com/aestheticfunction/dspack-gen.git#374e1cd91f02701c0d4829ece0a79dd9b381f67b",
"@types/minimist": "^1.2.5",
"esbuild": "^0.25.0",
"@types/node": "^20.0.0",
Expand Down
121 changes: 121 additions & 0 deletions scripts/check-core-pin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env node
/**
* Vendored-core pin drift check (PHASE-NEXT WS0, report-only posture).
*
* ds-mcp bundles @aestheticfunction/dspack-gen/core at build time from a
* commit-pinned devDependency. The invariant this job watches: the pinned
* commit corresponds exactly to dspack-gen's latest published release, so
* the schema and linter semantics ds-mcp serves are the ones consumers get
* from `npm install @aestheticfunction/dspack-gen`. When a new dspack-gen
* release changes anything under src/core/, this check fails loudly and the
* fix is the documented re-pin ritual: move the pin to the new release tag,
* rebuild, republish ds-mcp.
*
* The diff excludes *.test.ts — test files never ship in the esbuild vendor
* bundle, which is the documented escape for intentional ahead-of-release
* pins. The default posture is tag-pinned, where this check is green by
* construction.
*
* Report-only discipline: a GitHub API failure logs a warning and exits 0
* (infrastructure noise never blocks unrelated work); a real content diff
* always exits 1.
*
* Boring by design: node builtins + global fetch, one retry, no deps.
*/
import { readFileSync } from "node:fs";

const REPO = "aestheticfunction/dspack-gen";
const API = `https://api.github.com/repos/${REPO}`;

async function gh(path) {
for (let attempt = 1; ; attempt++) {
try {
const response = await fetch(`${API}${path}`, {
headers: { accept: "application/vnd.github+json", "user-agent": "ds-mcp-check-core-pin" },
});
Comment on lines +33 to +35
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt >= 2) throw new Error(`GET ${path}: ${error.message ?? error}`);
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
}

const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
const spec = pkg.devDependencies?.["@aestheticfunction/dspack-gen"] ?? "";
const pinned = spec.split("#")[1];
if (!/^[0-9a-f]{40}$/.test(pinned ?? "")) {
console.error(`could not read a 40-char commit pin from the devDependency spec: ${spec}`);
process.exit(1);
}

let tags;
try {
tags = await gh("/tags?per_page=100");
} catch (error) {
console.warn(`WARN GitHub API unavailable (${error.message}); pin-drift check skipped this run.`);
process.exit(0);
}

const releases = tags
.filter((t) => /^v\d+\.\d+\.\d+$/.test(t.name))
.sort((a, b) => {
const va = a.name.slice(1).split(".").map(Number);
const vb = b.name.slice(1).split(".").map(Number);
return vb[0] - va[0] || vb[1] - va[1] || vb[2] - va[2];
});
if (releases.length === 0) {
console.warn("WARN no release tags found on dspack-gen; pin-drift check skipped this run.");
process.exit(0);
}
const latest = releases[0];
console.log(`pinned commit: ${pinned}`);
console.log(`latest dspack-gen release: ${latest.name} (${latest.commit.sha})`);

if (latest.commit.sha === pinned) {
console.log("in sync the pin is exactly the latest release tag.");
process.exit(0);
}

// Compare the two src/core trees blob by blob. The compare API is NOT used:
// its file list caps at 300 entries and silently truncates larger diffs —
// the exact silent-drift class this check exists to prevent. Two recursive
// tree fetches are exact at any diff size.
async function coreTree(ref) {
const tree = await gh(`/git/trees/${ref}?recursive=1`);
if (tree.truncated) throw new Error(`tree listing for ${ref} was truncated by the API`);
const files = new Map();
for (const entry of tree.tree) {
if (entry.type !== "blob") continue;
if (!entry.path.startsWith("src/core/") || entry.path.endsWith(".test.ts")) continue;
files.set(entry.path, entry.sha);
}
return files;
}

let pinnedCore, latestCore;
try {
[pinnedCore, latestCore] = await Promise.all([coreTree(pinned), coreTree(latest.commit.sha)]);
} catch (error) {
console.warn(`WARN GitHub API unavailable (${error.message}); pin-drift check skipped this run.`);
process.exit(0);
}

const coreDiff = [...new Set([...pinnedCore.keys(), ...latestCore.keys()])]
.filter((path) => pinnedCore.get(path) !== latestCore.get(path))
.sort();

if (coreDiff.length === 0) {
console.log(
`in sync pin differs from ${latest.name} but no shipped src/core files differ ` +
"(test-only or non-core drift; the vendor bundle is unaffected).",
);
process.exit(0);
}

console.error(`DRIFT the vendored core pin diverges from dspack-gen ${latest.name} in shipped src/core files:`);
for (const f of coreDiff) console.error(` ${f}`);
console.error(" fix: re-pin the devDependency to the release tag commit, npm install,");
console.error(" npm run build, verify the golden-context test, release a new ds-mcp version.");
process.exit(1);
6 changes: 6 additions & 0 deletions scripts/check-sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ const MANIFEST = [
"https://raw.githubusercontent.com/aestheticfunction/dspack-gen/main/fixtures/golden/violating/F1-dialog-for-delete.expected.json",
note: "F1 expected findings — the deliberate validator-semantics coupling to dspack-gen",
},
{
local: "src/tests/fixtures/shadcn.destructive-action.context.json",
source:
"https://raw.githubusercontent.com/aestheticfunction/dspack-gen/main/fixtures/golden/context/shadcn.destructive-action.json",
note: "golden generation context — pins get-generation-context's served output to dspack-gen's compiler",
},
];

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