From 7de16f94efea27e9484bb3933a7849a40214db84 Mon Sep 17 00:00:00 2001 From: Mason Wyatt Date: Mon, 26 Oct 2026 19:36:30 -0400 Subject: [PATCH] bridge(RQ-N26-STACK-CONTRACT-DOC): RQ-N26-STACK-CONTRACT-DOC --- .../__tests__/fixtures/stack-manifest.v1.json | 10 +++++ .../core/src/__tests__/stack-manifest.test.ts | 33 ++++++++++++++++ packages/core/src/index.ts | 1 + packages/core/src/stack-manifest.ts | 39 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 packages/core/src/__tests__/fixtures/stack-manifest.v1.json create mode 100644 packages/core/src/__tests__/stack-manifest.test.ts create mode 100644 packages/core/src/stack-manifest.ts diff --git a/packages/core/src/__tests__/fixtures/stack-manifest.v1.json b/packages/core/src/__tests__/fixtures/stack-manifest.v1.json new file mode 100644 index 0000000..b38a1b3 --- /dev/null +++ b/packages/core/src/__tests__/fixtures/stack-manifest.v1.json @@ -0,0 +1,10 @@ +{ + "schema_version": 1, + "kind": "ashlr-stack-manifest", + "components": [ + { "id": "core", "name": "@ashlr/stack-core", "version": "0.2.2", "distribution": "npm" }, + { "id": "cli", "name": "@ashlr/stack", "version": "0.2.2", "distribution": "npm" }, + { "id": "mcp", "name": "ashlr-stack-mcp", "version": "0.2.2", "distribution": "npm" }, + { "id": "plugin", "name": "stack", "version": "0.1.0", "distribution": "claude-code-plugin" } + ] +} diff --git a/packages/core/src/__tests__/stack-manifest.test.ts b/packages/core/src/__tests__/stack-manifest.test.ts new file mode 100644 index 0000000..c75d8ab --- /dev/null +++ b/packages/core/src/__tests__/stack-manifest.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "bun:test"; +import cli from "../../../cli/package.json"; +import mcp from "../../../mcp/package.json"; +import plugin from "../../../plugin/.claude-plugin/plugin.json"; +import core from "../../package.json"; +import { STACK_MANIFEST, type StackManifest } from "../index.ts"; +import fixture from "./fixtures/stack-manifest.v1.json"; + +describe("Stack ecosystem manifest v1", () => { + test("public export preserves the fixture's JSON wire contract", () => { + const manifest: StackManifest = STACK_MANIFEST; + expect(JSON.parse(JSON.stringify(manifest))).toEqual(fixture); + }); + + test("every distributable matches its source name and version", () => { + const metadata = { core, cli, mcp, plugin }; + const manifest: StackManifest = STACK_MANIFEST; + expect(Object.keys(metadata).sort()).toEqual(manifest.components.map(({ id }) => id).sort()); + for (const component of manifest.components) { + expect(component.name).toBe(metadata[component.id].name); + expect(component.version).toBe(metadata[component.id].version); + expect(component.version).toMatch(/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/); + } + }); + + test("shared metadata cannot be mutated by consumers", () => { + expect(Object.isFrozen(STACK_MANIFEST)).toBe(true); + expect(Object.isFrozen(STACK_MANIFEST.components)).toBe(true); + for (const component of STACK_MANIFEST.components) { + expect(Object.isFrozen(component)).toBe(true); + } + }); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f5eb033..3c70b18 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,5 @@ export * from "./config.ts"; +export { STACK_MANIFEST, type StackManifest, type StackComponent } from "./stack-manifest.ts"; export { isEnabled as isTelemetryEnabled, promptFirstRun as promptTelemetryFirstRun, diff --git a/packages/core/src/stack-manifest.ts b/packages/core/src/stack-manifest.ts new file mode 100644 index 0000000..6862667 --- /dev/null +++ b/packages/core/src/stack-manifest.ts @@ -0,0 +1,39 @@ +/** A distributable Stack component, rather than an installed companion tool. */ +export interface StackComponent { + readonly id: "core" | "cli" | "mcp" | "plugin"; + readonly name: string; + readonly version: string; + readonly distribution: "npm" | "claude-code-plugin"; +} + +/** + * Source release metadata for ecosystem consumers. Versions do not establish + * registry publication or describe tools installed on the current machine. + * Additive fields may stay in v1; removals or renames require a schema bump. + */ +export interface StackManifest { + readonly schema_version: 1; + readonly kind: "ashlr-stack-manifest"; + readonly components: readonly StackComponent[]; +} + +/** + * Import from @ashlr/stack-core. This JSON-safe export performs no filesystem, + * network, credential or subprocess access. Keep versions in sync on release; + * the fixture contract test checks the package and plugin metadata for drift. + */ +export const STACK_MANIFEST = Object.freeze({ + schema_version: 1, + kind: "ashlr-stack-manifest", + components: Object.freeze([ + Object.freeze({ id: "core", name: "@ashlr/stack-core", version: "0.2.2", distribution: "npm" }), + Object.freeze({ id: "cli", name: "@ashlr/stack", version: "0.2.2", distribution: "npm" }), + Object.freeze({ id: "mcp", name: "ashlr-stack-mcp", version: "0.2.2", distribution: "npm" }), + Object.freeze({ + id: "plugin", + name: "stack", + version: "0.1.0", + distribution: "claude-code-plugin", + }), + ]), +} as const) satisfies StackManifest;