@stacksjs/buddy 0.70.162 → 0.70.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +1 -0
  3. package/dist/commands/ai-context.d.ts +13 -0
  4. package/dist/commands/ai-context.js +29 -0
  5. package/dist/commands/create.js +1 -1
  6. package/dist/commands/deploy.d.ts +11 -1
  7. package/dist/commands/deploy.js +29 -28
  8. package/dist/commands/dev.js +44 -14
  9. package/dist/commands/docs/buddy-commands.d.ts +8 -0
  10. package/dist/commands/docs/buddy-commands.js +116 -0
  11. package/dist/commands/docs/buddy-commands.test.d.ts +1 -0
  12. package/dist/commands/docs/buddy-commands.test.js +38 -0
  13. package/dist/commands/docs/generated-artifacts.d.ts +3 -0
  14. package/dist/commands/docs/generated-artifacts.js +69 -0
  15. package/dist/commands/docs/generated-artifacts.test.d.ts +1 -0
  16. package/dist/commands/docs/generated-artifacts.test.js +19 -0
  17. package/dist/commands/docs/links.d.ts +18 -0
  18. package/dist/commands/docs/links.js +83 -0
  19. package/dist/commands/docs/links.test.d.ts +1 -0
  20. package/dist/commands/docs/links.test.js +73 -0
  21. package/dist/commands/docs.d.ts +7 -0
  22. package/dist/commands/docs.js +24 -0
  23. package/dist/commands/doctor.js +8 -0
  24. package/dist/commands/env.d.ts +14 -0
  25. package/dist/commands/env.js +73 -7
  26. package/dist/commands/generate.js +1 -1
  27. package/dist/commands/index.d.ts +2 -0
  28. package/dist/commands/index.js +2 -0
  29. package/dist/commands/mail.js +1 -1
  30. package/dist/commands/migrate.js +9 -7
  31. package/dist/commands/protocol/craft-evidence.d.ts +76 -0
  32. package/dist/commands/protocol/craft-evidence.js +98 -0
  33. package/dist/commands/protocol/craft-evidence.test.d.ts +1 -0
  34. package/dist/commands/protocol/craft-evidence.test.js +20 -0
  35. package/dist/commands/protocol/desktop-lifecycle-report.d.ts +16 -0
  36. package/dist/commands/protocol/desktop-lifecycle-report.js +57 -0
  37. package/dist/commands/protocol/desktop-lifecycle-report.test.d.ts +1 -0
  38. package/dist/commands/protocol/desktop-lifecycle-report.test.js +32 -0
  39. package/dist/commands/protocol/desktop-support.d.ts +1 -0
  40. package/dist/commands/protocol/desktop-support.js +32 -0
  41. package/dist/commands/protocol/desktop-workflow.test.d.ts +1 -0
  42. package/dist/commands/protocol/desktop-workflow.test.js +33 -0
  43. package/dist/commands/protocol/driver-registry.d.ts +1 -0
  44. package/dist/commands/protocol/driver-registry.js +71 -0
  45. package/dist/commands/protocol/pantry-evidence.d.ts +67 -0
  46. package/dist/commands/protocol/pantry-evidence.js +144 -0
  47. package/dist/commands/protocol/pantry-evidence.test.d.ts +1 -0
  48. package/dist/commands/protocol/pantry-evidence.test.js +29 -0
  49. package/dist/commands/protocol/release-manifest.d.ts +30 -0
  50. package/dist/commands/protocol/release-manifest.js +102 -0
  51. package/dist/commands/protocol/release-manifest.test.d.ts +1 -0
  52. package/dist/commands/protocol/release-manifest.test.js +34 -0
  53. package/dist/commands/protocol/run-conformance.d.ts +21 -0
  54. package/dist/commands/protocol/run-conformance.js +358 -0
  55. package/dist/commands/protocol/run-conformance.test.d.ts +1 -0
  56. package/dist/commands/protocol/run-conformance.test.js +52 -0
  57. package/dist/commands/protocol/run-driver-contracts.d.ts +7 -0
  58. package/dist/commands/protocol/run-driver-contracts.js +94 -0
  59. package/dist/commands/protocol/run-driver-contracts.test.d.ts +1 -0
  60. package/dist/commands/protocol/run-driver-contracts.test.js +18 -0
  61. package/dist/commands/protocol/source-manifest.d.ts +25 -0
  62. package/dist/commands/protocol/source-manifest.js +113 -0
  63. package/dist/commands/protocol/sync-suite.d.ts +8 -0
  64. package/dist/commands/protocol/sync-suite.js +137 -0
  65. package/dist/commands/protocol/sync-suite.test.d.ts +1 -0
  66. package/dist/commands/protocol/sync-suite.test.js +44 -0
  67. package/dist/commands/protocol.d.ts +8 -0
  68. package/dist/commands/protocol.js +60 -0
  69. package/dist/commands/publish.js +149 -0
  70. package/dist/commands/run-tool.d.ts +7 -0
  71. package/dist/commands/run-tool.js +9 -0
  72. package/dist/commands/seed.js +15 -40
  73. package/dist/commands/serve.d.ts +18 -5
  74. package/dist/commands/serve.js +21 -2
  75. package/dist/commands/setup-ai.d.ts +39 -0
  76. package/dist/commands/setup-ai.js +121 -0
  77. package/dist/commands/setup.js +22 -0
  78. package/dist/lazy-commands.js +28 -1
  79. package/dist/project-setup.js +1 -0
  80. package/package.json +43 -43
@@ -0,0 +1,94 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { arch, platform } from "node:os";
4
+ import { resolve } from "node:path";
5
+ import { capabilityRegistry } from "../../../../config/src/capabilities";
6
+ const root = resolve(import.meta.dir, "../../../../../../.."), reportRoot = resolve(process.env.PROTOCOL_REPORT_DIR || resolve(root, ".github/protocol/reports"));
7
+ export function planDriverContracts(drivers) {
8
+ const tests = [...new Set(drivers.filter((driver) => driver.status === "supported").flatMap((driver) => driver.testEvidence))].sort(), records = drivers.map((driver) => ({
9
+ ...driver,
10
+ execution: driver.status === "supported" ? "run" : "not-run",
11
+ reason: driver.status === "supported" ? null : `Maturity is ${driver.status}; prerequisites and missing evidence are reported without promotion.`
12
+ }));
13
+ return { tests, records };
14
+ }
15
+ function sqliteVersion() {
16
+ const database = new Database(":memory:");
17
+ try {
18
+ return database.query("select sqlite_version() as version").get().version;
19
+ } finally {
20
+ database.close();
21
+ }
22
+ }
23
+ function markdown(report) {
24
+ const rows = report.drivers.map((driver) => `| ${driver.category} | ${driver.name} | ${driver.maturity} | ${driver.execution} | ${driver.topology} | ${driver.reason || "Retained test paths passed."} |`).join(`
25
+ `);
26
+ return `# Stacks driver contract matrix
27
+
28
+ - Source: \`${report.sourceRevision}\`
29
+ - Runtime: Bun \`${report.environment.runtime.version}\` on \`${report.environment.platform.os}/${report.environment.platform.architecture}\`
30
+ - SQLite: \`${report.environment.services.sqlite}\`
31
+ - Redis: \`${report.environment.services.redis.version}\` at \`${report.environment.services.redis.topology}\`
32
+ - Test files: ${report.testRun.files.length}
33
+ - Result: **${report.testRun.status}**
34
+
35
+ | Category | Driver | Maturity | Execution | Topology | Evidence boundary |
36
+ | --- | --- | --- | --- | --- | --- |
37
+ ${rows}
38
+ `;
39
+ }
40
+ export function runDriverContracts() {
41
+ const plan = planDriverContracts(capabilityRegistry);
42
+ if (!plan.tests.length)
43
+ throw Error("No supported driver contracts were selected");
44
+ const revisionResult = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: root });
45
+ if (revisionResult.exitCode !== 0)
46
+ throw Error("Could not resolve the Stacks revision");
47
+ const started = performance.now(), result = Bun.spawnSync(["bun", "test", ...plan.tests], { cwd: root, stdout: "inherit", stderr: "inherit" }), durationMs = Math.round(performance.now() - started), status = result.exitCode === 0 ? "pass" : "fail", report = {
48
+ schemaVersion: "1.0.0",
49
+ sourceRevision: revisionResult.stdout.toString().trim(),
50
+ environment: {
51
+ runtime: { name: "bun", version: Bun.version },
52
+ platform: { os: platform(), architecture: arch() },
53
+ ci: process.env.GITHUB_ACTIONS === "true" ? "github-actions" : "local",
54
+ services: {
55
+ sqlite: sqliteVersion(),
56
+ redis: {
57
+ version: process.env.REDIS_SERVICE_VERSION || "unreported",
58
+ url: process.env.REDIS_URL ? "redis://127.0.0.1:6379/0" : "not provisioned",
59
+ topology: "single-node loopback process provisioned by Pantry Action"
60
+ },
61
+ otherExternal: "not provisioned; partial/experimental drivers are not promoted"
62
+ }
63
+ },
64
+ testRun: { status, durationMs, files: plan.tests },
65
+ drivers: plan.records.map((driver) => ({
66
+ category: driver.category,
67
+ name: driver.name,
68
+ maturity: driver.status,
69
+ execution: driver.execution === "run" ? status : driver.execution,
70
+ topology: driver.topology,
71
+ prerequisites: driver.prerequisites,
72
+ evidence: driver.testEvidence,
73
+ limitations: driver.limitations,
74
+ reason: driver.reason
75
+ }))
76
+ };
77
+ mkdirSync(reportRoot, { recursive: !0 });
78
+ writeFileSync(resolve(reportRoot, "driver-matrix.json"), `${JSON.stringify(report, null, 2)}
79
+ `);
80
+ writeFileSync(resolve(reportRoot, "driver-matrix.md"), markdown(report));
81
+ if (status === "fail")
82
+ throw Error("Supported driver contract tests failed");
83
+ console.log(`Driver contract matrix passed ${plan.tests.length} retained test files`);
84
+ }
85
+ export async function run() {
86
+ try {
87
+ runDriverContracts();
88
+ } catch (error) {
89
+ console.error(error instanceof Error ? error.message : String(error));
90
+ process.exit(1);
91
+ }
92
+ }
93
+ if (import.meta.main)
94
+ await run();
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { planDriverContracts } from "./run-driver-contracts";
3
+ describe("driver contract matrix planning", () => {
4
+ it("runs only supported evidence and reports all other maturity states explicitly", () => {
5
+ const plan = planDriverContracts([
6
+ { category: "cache", name: "memory", status: "supported", implementation: "memory.ts", testEvidence: ["memory.test.ts"], topology: "memory", prerequisites: [], limitations: [] },
7
+ { category: "cache", name: "redis", status: "partial", implementation: "redis.ts", testEvidence: ["redis.test.ts"], topology: "redis", prerequisites: ["Redis"], limitations: ["No live evidence"] },
8
+ { category: "queue", name: "sqs", status: "unsupported", implementation: null, testEvidence: [], topology: "sqs", prerequisites: ["AWS"], limitations: ["Reserved"] }
9
+ ]);
10
+ expect(plan.tests).toEqual(["memory.test.ts"]);
11
+ expect(plan.records.map((record) => [record.name, record.execution])).toEqual([
12
+ ["memory", "run"],
13
+ ["redis", "not-run"],
14
+ ["sqs", "not-run"]
15
+ ]);
16
+ expect(plan.records[1].reason).toContain("partial");
17
+ });
18
+ });
@@ -0,0 +1,25 @@
1
+ export declare function buildSourceManifest(requestedRevision: string): SourceManifest;
2
+ export declare function run(): Promise<void>;
3
+ declare interface PackageEntry { name: string, path: string, version: string, publishable: boolean }
4
+ declare interface SourceManifest {
5
+ schemaVersion: '1.0.0'
6
+ repository: string
7
+ sourceRevision: string
8
+ sourceTree: string
9
+ sourceDigest: string
10
+ generatedAt: string
11
+ classification: {
12
+ status: 'reference-implementation'
13
+ conformance: 'unverified'
14
+ notes: string
15
+ }
16
+ prerequisites: Record<string, string>
17
+ packages: PackageEntry[]
18
+ inventory: {
19
+ totalFiles: number
20
+ totalBytes: number
21
+ categories: Record<string, { files: number, bytes: number }>
22
+ extensions: Record<string, number>
23
+ }
24
+ protocolSuite: { version: string, rfcsRevision: string }
25
+ }
@@ -0,0 +1,113 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, extname, resolve } from "node:path";
5
+ const root = resolve(import.meta.dir, "../../../../../../.."), outputPath = resolve(root, ".github/protocol/evidence/source-manifest.json");
6
+ function runGit(...arguments_) {
7
+ try {
8
+ return execFileSync("git", arguments_, { cwd: root, maxBuffer: 67108864 });
9
+ } catch (error) {
10
+ throw Error(`git ${arguments_.join(" ")} failed: ${error instanceof Error ? error.message : String(error)}`);
11
+ }
12
+ }
13
+ function revision(value) {
14
+ return runGit("rev-parse", `${value}^{commit}`).toString().trim();
15
+ }
16
+ function showJson(sourceRevision, path) {
17
+ return JSON.parse(runGit("show", `${sourceRevision}:${path}`).toString());
18
+ }
19
+ function category(path) {
20
+ if (/(^|\/)(test|tests|__tests__)(\/|$)|\.(test|spec)\.[^.]+$/.test(path))
21
+ return "tests";
22
+ if (path.startsWith("protocol/suite/"))
23
+ return "vendored-protocol";
24
+ if (/(^|\/)(dist|generated|coverage)(\/|$)|\.d\.ts$/.test(path))
25
+ return "generated";
26
+ if (/\.(md|mdx|rst|txt)$/.test(path) || /(^|\/)docs\//.test(path))
27
+ return "documentation";
28
+ if (/(^|\/)(config|configs|\.github)(\/|$)|(^|\/)(tsconfig|package|bun\.lock|pantry)\b/.test(path))
29
+ return "configuration";
30
+ if (/\.(ts|tsx|js|jsx|vue|svelte|zig|rs|go|py|sh)$/.test(path))
31
+ return "source";
32
+ return "assets-and-data";
33
+ }
34
+ function addCount(record, key, bytes) {
35
+ const current = record[key] || { files: 0, bytes: 0 };
36
+ record[key] = { files: current.files + 1, bytes: current.bytes + bytes };
37
+ }
38
+ export function buildSourceManifest(requestedRevision) {
39
+ const sourceRevision = revision(requestedRevision), sourceTree = runGit("rev-parse", `${sourceRevision}^{tree}`).toString().trim(), treeListing = runGit("ls-tree", "-r", "-l", sourceRevision).toString(), lines = treeListing.trim().split(`
40
+ `).filter(Boolean), categories = {}, extensions = {};
41
+ let totalBytes = 0;
42
+ const paths = [];
43
+ for (const line of lines) {
44
+ const match = line.match(/^\d+\s+blob\s+[0-9a-f]+\s+(\d+)\t(.+)$/);
45
+ if (!match)
46
+ continue;
47
+ const bytes = Number(match[1]), path = match[2];
48
+ if (path == null)
49
+ continue;
50
+ paths.push(path);
51
+ totalBytes += bytes;
52
+ addCount(categories, category(path), bytes);
53
+ const extension = extname(path).toLowerCase() || "[none]";
54
+ extensions[extension] = (extensions[extension] || 0) + 1;
55
+ }
56
+ const packages = paths.filter((path) => path === "package.json" || path.endsWith("/package.json")).flatMap((path) => {
57
+ const pkg = showJson(sourceRevision, path);
58
+ if (!pkg.name || !pkg.version)
59
+ return [];
60
+ return [{ name: pkg.name, path, version: pkg.version, publishable: pkg.private !== !0 }];
61
+ }).sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)), rootPackage = showJson(sourceRevision, "package.json"), suiteLock = showJson(sourceRevision, "protocol/suite.lock.json");
62
+ return {
63
+ schemaVersion: "1.0.0",
64
+ repository: "https://github.com/stacksjs/stacks",
65
+ sourceRevision,
66
+ sourceTree,
67
+ sourceDigest: `sha256:${createHash("sha256").update(treeListing).digest("hex")}`,
68
+ generatedAt: "derived-from-immutable-git-objects",
69
+ classification: {
70
+ status: "reference-implementation",
71
+ conformance: "unverified",
72
+ notes: "Reference implementation status does not imply a protocol profile claim; consult the report for per-requirement evidence."
73
+ },
74
+ prerequisites: rootPackage.system || {},
75
+ packages,
76
+ inventory: {
77
+ totalFiles: lines.length,
78
+ totalBytes,
79
+ categories: Object.fromEntries(Object.entries(categories).sort(([a], [b]) => a.localeCompare(b))),
80
+ extensions: Object.fromEntries(Object.entries(extensions).sort(([a], [b]) => a.localeCompare(b)))
81
+ },
82
+ protocolSuite: { version: suiteLock.protocolVersion, rfcsRevision: suiteLock.rfcsRevision }
83
+ };
84
+ }
85
+ function render(manifest) {
86
+ return `${JSON.stringify(manifest, null, 2)}
87
+ `;
88
+ }
89
+ export async function run() {
90
+ const mode = process.argv.includes("--write") ? "write" : process.argv.includes("--check") ? "check" : null;
91
+ if (!mode) {
92
+ console.error("usage: bun storage/framework/core/buddy/src/commands/protocol/source-manifest.ts --write [--revision <ref>] | --check");
93
+ process.exit(2);
94
+ }
95
+ if (mode === "write") {
96
+ const index = process.argv.indexOf("--revision"), requestedRevision = index === -1 ? "HEAD" : process.argv[index + 1];
97
+ if (!requestedRevision)
98
+ throw Error("--revision requires a Git ref");
99
+ const manifest = buildSourceManifest(requestedRevision);
100
+ mkdirSync(dirname(outputPath), { recursive: !0 });
101
+ writeFileSync(outputPath, render(manifest));
102
+ console.log(`Wrote source manifest for ${manifest.sourceRevision} (${manifest.inventory.totalFiles} files, ${manifest.packages.length} packages)`);
103
+ } else {
104
+ if (!existsSync(outputPath))
105
+ throw Error("source manifest is missing; run bun run protocol:manifest");
106
+ const current = JSON.parse(readFileSync(outputPath, "utf8")), expected = render(buildSourceManifest(current.sourceRevision));
107
+ if (readFileSync(outputPath, "utf8") !== expected)
108
+ throw Error(`source manifest is stale or modified; run bun storage/framework/core/buddy/src/commands/protocol/source-manifest.ts --write --revision ${current.sourceRevision}`);
109
+ console.log(`Source manifest matches ${current.sourceRevision}`);
110
+ }
111
+ }
112
+ if (import.meta.main)
113
+ await run();
@@ -0,0 +1,8 @@
1
+ /** Requirement ids that appear more than once in `ids` (sorted, deduped). */
2
+ export declare function duplicateRequirementIds(ids: string[]): string[];
3
+ /**
4
+ * Requirement ids referenced by a fixture that are absent from `catalogIds`,
5
+ * formatted `fixtureId -> requirementId`.
6
+ */
7
+ export declare function danglingFixtureRequirements(fixtures: Array<{ id: string, requirements?: string[] }>, catalogIds: Set<string>): string[];
8
+ export declare function run(): Promise<void>;
@@ -0,0 +1,137 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, relative, resolve } from "node:path";
4
+ const root = resolve(import.meta.dir, "../../../../../../.."), suiteRoot = resolve(root, ".github/protocol/suite/1.0-draft"), lockPath = resolve(root, ".github/protocol/suite.lock.json");
5
+ function digest(data) {
6
+ return `sha256:${createHash("sha256").update(data).digest("hex")}`;
7
+ }
8
+ function filesBelow(directory) {
9
+ return readdirSync(directory, { withFileTypes: !0 }).flatMap((entry) => {
10
+ const path = resolve(directory, entry.name);
11
+ return entry.isDirectory() ? filesBelow(path) : [path];
12
+ }).sort();
13
+ }
14
+ function argument(name) {
15
+ const index = process.argv.indexOf(name);
16
+ return index === -1 ? void 0 : process.argv[index + 1];
17
+ }
18
+ function gitRevision(repository) {
19
+ const result = Bun.spawnSync(["git", "-C", repository, "rev-parse", "HEAD"]);
20
+ if (result.exitCode !== 0)
21
+ throw Error(`Could not resolve the RFC repository revision: ${result.stderr.toString().trim()}`);
22
+ return result.stdout.toString().trim();
23
+ }
24
+ function writeSnapshot(sourceRepository) {
25
+ const sourceRoot = resolve(sourceRepository, "protocol/1.0-draft");
26
+ if (!existsSync(resolve(sourceRoot, "catalog.json")))
27
+ throw Error(`No protocol suite found at ${sourceRoot}`);
28
+ rmSync(suiteRoot, { force: !0, recursive: !0 });
29
+ const files = {}, sourceFiles = [
30
+ ...filesBelow(sourceRoot).map((sourcePath) => ({ sourcePath, path: relative(sourceRoot, sourcePath) })),
31
+ ...["LICENSE-SPECIFICATION.md", "LICENSE-FIXTURES.md"].map((path) => ({ sourcePath: resolve(sourceRepository, path), path }))
32
+ ];
33
+ for (const { sourcePath, path } of sourceFiles) {
34
+ const outputPath = resolve(suiteRoot, path), contents = readFileSync(sourcePath);
35
+ mkdirSync(dirname(outputPath), { recursive: !0 });
36
+ writeFileSync(outputPath, contents);
37
+ files[path] = digest(contents);
38
+ }
39
+ const lock = {
40
+ protocolVersion: "1.0-draft",
41
+ rfcsRevision: gitRevision(sourceRepository),
42
+ sourceRepository: "https://github.com/stacksjs/rfcs",
43
+ files
44
+ };
45
+ mkdirSync(dirname(lockPath), { recursive: !0 });
46
+ writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}
47
+ `);
48
+ console.log(`Pinned ${Object.keys(files).length} protocol files at ${lock.rfcsRevision}`);
49
+ }
50
+ function checkSnapshot() {
51
+ if (!existsSync(lockPath))
52
+ throw Error(".github/protocol/suite.lock.json is missing; run bun run protocol:sync");
53
+ const lock = JSON.parse(readFileSync(lockPath, "utf8"));
54
+ if (!/^[0-9a-f]{40}$/.test(lock.rfcsRevision))
55
+ throw Error("suite lock has an invalid RFC revision");
56
+ const actualFiles = filesBelow(suiteRoot).map((path) => relative(suiteRoot, path)), expectedFiles = Object.keys(lock.files).sort(), unexpected = actualFiles.filter((path) => !lock.files[path]), missing = expectedFiles.filter((path) => !actualFiles.includes(path)), changed = expectedFiles.filter((path) => {
57
+ const absolutePath = resolve(suiteRoot, path);
58
+ return existsSync(absolutePath) && digest(readFileSync(absolutePath)) !== lock.files[path];
59
+ }), errors = [
60
+ ...missing.map((path) => `missing: ${path}`),
61
+ ...unexpected.map((path) => `unexpected: ${path}`),
62
+ ...changed.map((path) => `modified: ${path}`)
63
+ ];
64
+ if (errors.length > 0)
65
+ throw Error(`Vendored protocol suite does not match its lock:
66
+ ${errors.join(`
67
+ `)}`);
68
+ console.log(`Protocol suite matches ${basename(lock.sourceRepository)}@${lock.rfcsRevision} (${actualFiles.length} files)`);
69
+ }
70
+ export function duplicateRequirementIds(ids) {
71
+ const seen = new Set, duplicates = new Set;
72
+ for (const id of ids)
73
+ if (seen.has(id))
74
+ duplicates.add(id);
75
+ else
76
+ seen.add(id);
77
+ return [...duplicates].sort();
78
+ }
79
+ function checkCatalog() {
80
+ const catalogPath = resolve(suiteRoot, "catalog.json");
81
+ if (!existsSync(catalogPath))
82
+ throw Error(`protocol catalog missing at ${relative(root, catalogPath)}; run bun run protocol:sync`);
83
+ const catalog = JSON.parse(readFileSync(catalogPath, "utf8")), requirements = Array.isArray(catalog.requirements) ? catalog.requirements : [];
84
+ if (requirements.length === 0)
85
+ throw Error("protocol catalog has no requirements");
86
+ const ids = requirements.map((requirement) => requirement?.id), missing = ids.filter((id) => typeof id !== "string" || id.length === 0).length;
87
+ if (missing > 0)
88
+ throw Error(`protocol catalog has ${missing} requirement(s) without a string id`);
89
+ const duplicates = duplicateRequirementIds(ids);
90
+ if (duplicates.length > 0)
91
+ throw Error(`protocol catalog has duplicate requirement id(s): ${duplicates.join(", ")}`);
92
+ console.log(`Protocol catalog: ${ids.length} requirement ids, all unique`);
93
+ }
94
+ export function danglingFixtureRequirements(fixtures, catalogIds) {
95
+ const dangling = [];
96
+ for (const fixture of fixtures)
97
+ for (const requirement of fixture.requirements ?? [])
98
+ if (!catalogIds.has(requirement))
99
+ dangling.push(`${fixture.id} -> ${requirement}`);
100
+ return dangling;
101
+ }
102
+ function checkFixtures() {
103
+ const fixturesPath = resolve(suiteRoot, "fixtures/conformance.json");
104
+ if (!existsSync(fixturesPath))
105
+ throw Error(`protocol fixture corpus missing at ${relative(root, fixturesPath)}; run bun run protocol:sync`);
106
+ const corpus = JSON.parse(readFileSync(fixturesPath, "utf8")), fixtures = Array.isArray(corpus.fixtures) ? corpus.fixtures : [];
107
+ if (fixtures.length === 0)
108
+ throw Error("protocol fixture corpus has no fixtures");
109
+ const ids = fixtures.map((fixture) => fixture?.id), missing = ids.filter((id) => typeof id !== "string" || id.length === 0).length;
110
+ if (missing > 0)
111
+ throw Error(`protocol fixture corpus has ${missing} fixture(s) without a string id`);
112
+ const duplicates = duplicateRequirementIds(ids);
113
+ if (duplicates.length > 0)
114
+ throw Error(`protocol fixture corpus has duplicate fixture id(s): ${duplicates.join(", ")}`);
115
+ const catalog = JSON.parse(readFileSync(resolve(suiteRoot, "catalog.json"), "utf8")), catalogIds = new Set((catalog.requirements ?? []).map((requirement) => requirement?.id).filter((id) => typeof id === "string")), normalized = fixtures.map((fixture) => ({
116
+ id: String(fixture?.id),
117
+ requirements: Array.isArray(fixture?.requirements) ? fixture.requirements.filter((requirement) => typeof requirement === "string") : []
118
+ })), dangling = danglingFixtureRequirements(normalized, catalogIds);
119
+ if (dangling.length > 0)
120
+ throw Error(`protocol fixtures reference unknown requirement id(s): ${dangling.slice(0, 10).join(", ")}`);
121
+ console.log(`Protocol fixtures: ${ids.length} fixtures, ids unique, all requirement refs resolve`);
122
+ }
123
+ export async function run() {
124
+ if (process.argv.includes("--write")) {
125
+ const source = resolve(argument("--source") || process.env.STACKS_RFC_SOURCE || resolve(root, "../rfcs"));
126
+ writeSnapshot(source);
127
+ } else if (process.argv.includes("--check")) {
128
+ checkSnapshot();
129
+ checkCatalog();
130
+ checkFixtures();
131
+ } else {
132
+ console.error("usage: bun storage/framework/core/buddy/src/commands/protocol/sync-suite.ts --write [--source ../rfcs] | --check");
133
+ process.exit(2);
134
+ }
135
+ }
136
+ if (import.meta.main)
137
+ await run();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { describe, expect, it } from "bun:test";
4
+ import { danglingFixtureRequirements, duplicateRequirementIds } from "./sync-suite";
5
+ describe("protocol requirement-id uniqueness (stacksjs/stacks#2050)", () => {
6
+ it("reports no duplicates for a unique list", () => {
7
+ expect(duplicateRequirementIds(["CORE-CONV-01", "CORE-CONV-02", "CORE-MVA-01"])).toEqual([]);
8
+ });
9
+ it("reports each duplicated id once, sorted", () => {
10
+ expect(duplicateRequirementIds(["B-1", "A-1", "B-1", "A-1", "A-1", "C-1"])).toEqual(["A-1", "B-1"]);
11
+ });
12
+ it("handles an empty list", () => {
13
+ expect(duplicateRequirementIds([])).toEqual([]);
14
+ });
15
+ it("the vendored catalog.json has unique requirement ids", () => {
16
+ const ids = JSON.parse(readFileSync(resolve(import.meta.dir, "../../../../../../../.github/protocol/suite/1.0-draft/catalog.json"), "utf8")).requirements.map((requirement) => requirement.id);
17
+ expect(ids.length).toBeGreaterThan(0);
18
+ expect(duplicateRequirementIds(ids)).toEqual([]);
19
+ });
20
+ });
21
+ describe("protocol fixture-corpus integrity (stacksjs/stacks#2051)", () => {
22
+ const catalogIds = new Set(["CORE-CONV-01", "CORE-CONV-02", "CORE-MVA-01"]);
23
+ it("reports no dangling refs when every requirement resolves", () => {
24
+ expect(danglingFixtureRequirements([
25
+ { id: "fixture.a", requirements: ["CORE-CONV-01", "CORE-CONV-02"] },
26
+ { id: "fixture.b", requirements: ["CORE-MVA-01"] }
27
+ ], catalogIds)).toEqual([]);
28
+ });
29
+ it("reports each unknown requirement ref as `fixture -> requirement`", () => {
30
+ expect(danglingFixtureRequirements([
31
+ { id: "fixture.a", requirements: ["CORE-CONV-01", "CORE-GONE-99"] },
32
+ { id: "fixture.b", requirements: ["NOPE-01"] }
33
+ ], catalogIds)).toEqual(["fixture.a -> CORE-GONE-99", "fixture.b -> NOPE-01"]);
34
+ });
35
+ it("tolerates fixtures with no requirements", () => {
36
+ expect(danglingFixtureRequirements([{ id: "fixture.a" }], catalogIds)).toEqual([]);
37
+ });
38
+ it("the vendored fixture corpus has unique ids and no dangling requirement refs", () => {
39
+ const base = resolve(import.meta.dir, "../../../../../../../.github/protocol/suite/1.0-draft"), corpus = JSON.parse(readFileSync(resolve(base, "fixtures/conformance.json"), "utf8")), catalog = JSON.parse(readFileSync(resolve(base, "catalog.json"), "utf8")), realIds = new Set(catalog.requirements.map((requirement) => requirement.id));
40
+ expect(corpus.fixtures.length).toBeGreaterThan(0);
41
+ expect(duplicateRequirementIds(corpus.fixtures.map((fixture) => fixture.id))).toEqual([]);
42
+ expect(danglingFixtureRequirements(corpus.fixtures, realIds)).toEqual([]);
43
+ });
44
+ });
@@ -0,0 +1,8 @@
1
+ import type { CLI } from '@stacksjs/types';
2
+ /**
3
+ * Framework-repo `buddy protocol:*` commands. The governance tooling itself
4
+ * lives beside this file under `commands/protocol/`; the protocol suite +
5
+ * evidence DATA stays under `.github/protocol/`. These wrap the tooling so it
6
+ * is discoverable via the CLI (`buddy protocol:conformance`).
7
+ */
8
+ export declare function protocol(buddy: CLI): void;
@@ -0,0 +1,60 @@
1
+ import { run as runConformance } from "./protocol/run-conformance";
2
+ import { run as runCraft } from "./protocol/craft-evidence";
3
+ import { run as runDesktop } from "./protocol/desktop-support";
4
+ import { run as runDrivers } from "./protocol/driver-registry";
5
+ import { run as runDriverContracts } from "./protocol/run-driver-contracts";
6
+ import { run as runPantry } from "./protocol/pantry-evidence";
7
+ import { run as runRelease } from "./protocol/release-manifest";
8
+ import { run as runManifest } from "./protocol/source-manifest";
9
+ import { run as runSync } from "./protocol/sync-suite";
10
+ import { runTool } from "./run-tool";
11
+ export function protocol(buddy) {
12
+ buddy.command("protocol:conformance", "Generate the Stacks protocol conformance report").action(async () => {
13
+ await runConformance();
14
+ });
15
+ buddy.command("protocol:sync", "Sync the vendored protocol suite from stacksjs/rfcs").option("--source <path>", "Path to a local rfcs checkout", { default: void 0 }).action(async (options) => {
16
+ await runTool(runSync, "--write", ...options.source ? ["--source", options.source] : []);
17
+ });
18
+ buddy.command("protocol:check", "Verify the vendored protocol suite is pinned + internally consistent").action(async () => {
19
+ await runTool(runSync, "--check");
20
+ });
21
+ buddy.command("protocol:manifest", "Write the protocol source manifest").option("--revision <ref>", "Source revision to pin (default HEAD)", { default: void 0 }).action(async (options) => {
22
+ await runTool(runManifest, "--write", ...options.revision ? ["--revision", options.revision] : []);
23
+ });
24
+ buddy.command("protocol:manifest:check", "Verify the protocol source manifest is current").action(async () => {
25
+ await runTool(runManifest, "--check");
26
+ });
27
+ buddy.command("protocol:release", "Write the protocol release manifest").option("--tag <tag>", "Release tag", { default: void 0 }).action(async (options) => {
28
+ await runTool(runRelease, "--write", ...options.tag ? ["--tag", options.tag] : []);
29
+ });
30
+ buddy.command("protocol:release:check", "Verify the protocol release manifest is current").action(async () => {
31
+ await runTool(runRelease, "--check");
32
+ });
33
+ buddy.command("protocol:drivers", "Write the driver capability registry evidence").action(async () => {
34
+ await runTool(runDrivers, "--write");
35
+ });
36
+ buddy.command("protocol:drivers:check", "Verify the driver capability registry evidence").action(async () => {
37
+ await runTool(runDrivers, "--check");
38
+ });
39
+ buddy.command("protocol:drivers:test", "Run the driver contract suite").action(async () => {
40
+ await runTool(runDriverContracts);
41
+ });
42
+ buddy.command("protocol:desktop", "Write the desktop support matrix evidence").action(async () => {
43
+ await runTool(runDesktop, "--write");
44
+ });
45
+ buddy.command("protocol:desktop:check", "Verify the desktop support matrix evidence").action(async () => {
46
+ await runTool(runDesktop, "--check");
47
+ });
48
+ buddy.command("protocol:pantry", "Write the pantry evidence").action(async () => {
49
+ await runTool(runPantry, "--write");
50
+ });
51
+ buddy.command("protocol:pantry:check", "Verify the pantry evidence").action(async () => {
52
+ await runTool(runPantry, "--check");
53
+ });
54
+ buddy.command("protocol:craft", "Write the Craft evidence").action(async () => {
55
+ await runTool(runCraft, "--write");
56
+ });
57
+ buddy.command("protocol:craft:check", "Verify the Craft evidence").action(async () => {
58
+ await runTool(runCraft, "--check");
59
+ });
60
+ }