@sdd-method/sdd-cli 1.41.0 → 1.43.0

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 (189) hide show
  1. package/README.md +4 -57
  2. package/dist/author-bin.d.ts +3 -0
  3. package/dist/author-bin.d.ts.map +1 -0
  4. package/dist/author-bin.js +8 -0
  5. package/dist/author-bin.js.map +1 -0
  6. package/dist/author-program.d.ts +8 -0
  7. package/dist/author-program.d.ts.map +1 -0
  8. package/dist/author-program.js +99 -0
  9. package/dist/author-program.js.map +1 -0
  10. package/dist/index.js +2 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/agent-harness.d.ts +11 -8
  13. package/dist/lib/agent-harness.d.ts.map +1 -1
  14. package/dist/lib/agent-harness.js +17 -8
  15. package/dist/lib/agent-harness.js.map +1 -1
  16. package/dist/lib/api-surface/index.js +2 -2
  17. package/dist/lib/api-surface/index.js.map +1 -1
  18. package/dist/lib/briefing/index.d.ts +1 -1
  19. package/dist/lib/briefing/index.d.ts.map +1 -1
  20. package/dist/lib/briefing/index.js +1 -1
  21. package/dist/lib/briefing/index.js.map +1 -1
  22. package/dist/lib/briefing/render.d.ts +18 -1
  23. package/dist/lib/briefing/render.d.ts.map +1 -1
  24. package/dist/lib/briefing/render.js +17 -8
  25. package/dist/lib/briefing/render.js.map +1 -1
  26. package/dist/lib/catalogue/aggregation-manifest-loader.js +4 -4
  27. package/dist/lib/catalogue/aggregation-manifest-loader.js.map +1 -1
  28. package/dist/lib/catalogue/cli-version.d.ts +1 -1
  29. package/dist/lib/catalogue/cli-version.d.ts.map +1 -1
  30. package/dist/lib/catalogue/cli-version.js +3 -3
  31. package/dist/lib/catalogue/cli-version.js.map +1 -1
  32. package/dist/lib/catalogue/freshness.d.ts +27 -0
  33. package/dist/lib/catalogue/freshness.d.ts.map +1 -0
  34. package/dist/lib/catalogue/freshness.js +70 -0
  35. package/dist/lib/catalogue/freshness.js.map +1 -0
  36. package/dist/lib/catalogue/graph-run.js +1 -1
  37. package/dist/lib/catalogue/graph-run.js.map +1 -1
  38. package/dist/lib/ci-binding/index.d.ts +63 -0
  39. package/dist/lib/ci-binding/index.d.ts.map +1 -0
  40. package/dist/lib/ci-binding/index.js +220 -0
  41. package/dist/lib/ci-binding/index.js.map +1 -0
  42. package/dist/lib/ci-binding/module.d.ts +36 -0
  43. package/dist/lib/ci-binding/module.d.ts.map +1 -0
  44. package/dist/lib/ci-binding/module.js +204 -0
  45. package/dist/lib/ci-binding/module.js.map +1 -0
  46. package/dist/lib/context-pull/index.d.ts +45 -0
  47. package/dist/lib/context-pull/index.d.ts.map +1 -0
  48. package/dist/lib/context-pull/index.js +191 -0
  49. package/dist/lib/context-pull/index.js.map +1 -0
  50. package/dist/lib/context-pull/resolve.d.ts +46 -0
  51. package/dist/lib/context-pull/resolve.d.ts.map +1 -0
  52. package/dist/lib/context-pull/resolve.js +70 -0
  53. package/dist/lib/context-pull/resolve.js.map +1 -0
  54. package/dist/lib/context-pull/retired-mcp-setup.d.ts +7 -0
  55. package/dist/lib/context-pull/retired-mcp-setup.d.ts.map +1 -0
  56. package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
  57. package/dist/lib/context-pull/retired-mcp-setup.js.map +1 -0
  58. package/dist/lib/hooks-seed/entries.d.ts +15 -0
  59. package/dist/lib/hooks-seed/entries.d.ts.map +1 -1
  60. package/dist/lib/hooks-seed/entries.js +30 -1
  61. package/dist/lib/hooks-seed/entries.js.map +1 -1
  62. package/dist/lib/hooks-seed/index.d.ts +4 -9
  63. package/dist/lib/hooks-seed/index.d.ts.map +1 -1
  64. package/dist/lib/hooks-seed/index.js +4 -7
  65. package/dist/lib/hooks-seed/index.js.map +1 -1
  66. package/dist/lib/hooks-seed/merge.d.ts +7 -0
  67. package/dist/lib/hooks-seed/merge.d.ts.map +1 -1
  68. package/dist/lib/hooks-seed/merge.js +31 -0
  69. package/dist/lib/hooks-seed/merge.js.map +1 -1
  70. package/dist/lib/init/app.d.ts +0 -1
  71. package/dist/lib/init/app.d.ts.map +1 -1
  72. package/dist/lib/init/app.js +3 -7
  73. package/dist/lib/init/app.js.map +1 -1
  74. package/dist/lib/init/ci-workflow.d.ts +6 -8
  75. package/dist/lib/init/ci-workflow.d.ts.map +1 -1
  76. package/dist/lib/init/ci-workflow.js +57 -120
  77. package/dist/lib/init/ci-workflow.js.map +1 -1
  78. package/dist/lib/init/integration.d.ts +0 -1
  79. package/dist/lib/init/integration.d.ts.map +1 -1
  80. package/dist/lib/init/integration.js +3 -7
  81. package/dist/lib/init/integration.js.map +1 -1
  82. package/dist/lib/init/platform.d.ts +2 -10
  83. package/dist/lib/init/platform.d.ts.map +1 -1
  84. package/dist/lib/init/platform.js +12 -15
  85. package/dist/lib/init/platform.js.map +1 -1
  86. package/dist/lib/lock-file.d.ts +20 -0
  87. package/dist/lib/lock-file.d.ts.map +1 -1
  88. package/dist/lib/lock-file.js +25 -1
  89. package/dist/lib/lock-file.js.map +1 -1
  90. package/dist/lib/mcp/build-info.d.ts.map +1 -1
  91. package/dist/lib/mcp/build-info.js +3 -3
  92. package/dist/lib/mcp/build-info.js.map +1 -1
  93. package/dist/lib/pack/index.js +2 -2
  94. package/dist/lib/pack/index.js.map +1 -1
  95. package/dist/lib/pack/validate.js +2 -2
  96. package/dist/lib/pack/validate.js.map +1 -1
  97. package/dist/lib/package-version.d.ts +23 -0
  98. package/dist/lib/package-version.d.ts.map +1 -1
  99. package/dist/lib/package-version.js +47 -10
  100. package/dist/lib/package-version.js.map +1 -1
  101. package/dist/lib/plan/index.d.ts.map +1 -1
  102. package/dist/lib/plan/index.js +4 -4
  103. package/dist/lib/plan/index.js.map +1 -1
  104. package/dist/lib/plan/read-sdd.js +1 -1
  105. package/dist/lib/plan/render-template.d.ts +8 -0
  106. package/dist/lib/plan/render-template.d.ts.map +1 -1
  107. package/dist/lib/plan/render-template.js +27 -3
  108. package/dist/lib/plan/render-template.js.map +1 -1
  109. package/dist/lib/sdd-index/repo-config.d.ts +1 -1
  110. package/dist/lib/sdd-index/repo-config.js +1 -1
  111. package/dist/lib/sdd-reader.d.ts +35 -0
  112. package/dist/lib/sdd-reader.d.ts.map +1 -0
  113. package/dist/lib/sdd-reader.js +91 -0
  114. package/dist/lib/sdd-reader.js.map +1 -0
  115. package/dist/lib/sync/briefing-merge.d.ts +1 -1
  116. package/dist/lib/sync/briefing-merge.d.ts.map +1 -1
  117. package/dist/lib/sync/briefing-merge.js +50 -34
  118. package/dist/lib/sync/briefing-merge.js.map +1 -1
  119. package/dist/lib/sync/diff.js +1 -1
  120. package/dist/lib/sync/diff.js.map +1 -1
  121. package/dist/lib/sync/index.d.ts +16 -6
  122. package/dist/lib/sync/index.d.ts.map +1 -1
  123. package/dist/lib/sync/index.js +89 -116
  124. package/dist/lib/sync/index.js.map +1 -1
  125. package/dist/lib/sync/settings-merge.d.ts +26 -48
  126. package/dist/lib/sync/settings-merge.d.ts.map +1 -1
  127. package/dist/lib/sync/settings-merge.js +70 -162
  128. package/dist/lib/sync/settings-merge.js.map +1 -1
  129. package/dist/lib/validate/bundle-coherence.js +2 -2
  130. package/dist/lib/validate/bundle-coherence.js.map +1 -1
  131. package/dist/lib/validate/plan.d.ts +25 -0
  132. package/dist/lib/validate/plan.d.ts.map +1 -0
  133. package/dist/lib/validate/plan.js +54 -0
  134. package/dist/lib/validate/plan.js.map +1 -0
  135. package/dist/maintain-bin.d.ts +3 -0
  136. package/dist/maintain-bin.d.ts.map +1 -0
  137. package/dist/maintain-bin.js +8 -0
  138. package/dist/maintain-bin.js.map +1 -0
  139. package/dist/maintain-program.d.ts +8 -0
  140. package/dist/maintain-program.d.ts.map +1 -0
  141. package/dist/maintain-program.js +94 -0
  142. package/dist/maintain-program.js.map +1 -0
  143. package/dist/maintain-version.json +4 -0
  144. package/dist/verbs/catalogue-check.d.ts +10 -0
  145. package/dist/verbs/catalogue-check.d.ts.map +1 -0
  146. package/dist/verbs/catalogue-check.js +28 -0
  147. package/dist/verbs/catalogue-check.js.map +1 -0
  148. package/dist/verbs/catalogue-maintain.d.ts +6 -0
  149. package/dist/verbs/catalogue-maintain.d.ts.map +1 -0
  150. package/dist/verbs/catalogue-maintain.js +110 -0
  151. package/dist/verbs/catalogue-maintain.js.map +1 -0
  152. package/dist/verbs/catalogue.d.ts +5 -0
  153. package/dist/verbs/catalogue.d.ts.map +1 -1
  154. package/dist/verbs/catalogue.js +16 -80
  155. package/dist/verbs/catalogue.js.map +1 -1
  156. package/dist/verbs/context.d.ts +3 -0
  157. package/dist/verbs/context.d.ts.map +1 -0
  158. package/dist/verbs/context.js +39 -0
  159. package/dist/verbs/context.js.map +1 -0
  160. package/dist/verbs/pack-build.d.ts +3 -0
  161. package/dist/verbs/pack-build.d.ts.map +1 -0
  162. package/dist/verbs/pack-build.js +31 -0
  163. package/dist/verbs/pack-build.js.map +1 -0
  164. package/dist/verbs/pack.d.ts.map +1 -1
  165. package/dist/verbs/pack.js +2 -25
  166. package/dist/verbs/pack.js.map +1 -1
  167. package/dist/verbs/sync.d.ts.map +1 -1
  168. package/dist/verbs/sync.js +21 -0
  169. package/dist/verbs/sync.js.map +1 -1
  170. package/dist/verbs/validate.d.ts.map +1 -1
  171. package/dist/verbs/validate.js +26 -0
  172. package/dist/verbs/validate.js.map +1 -1
  173. package/dist/verify-program.d.ts +6 -1
  174. package/dist/verify-program.d.ts.map +1 -1
  175. package/dist/verify-program.js +12 -5
  176. package/dist/verify-program.js.map +1 -1
  177. package/dist/verify-version.json +2 -2
  178. package/package.json +6 -5
  179. package/scripts/author-acceptance.mjs +170 -0
  180. package/scripts/author-version.json +4 -0
  181. package/scripts/build-persona-package.mjs +309 -0
  182. package/scripts/copy-yaml-assets.mjs +4 -2
  183. package/scripts/dagger-parity.sh +74 -0
  184. package/scripts/maintain-acceptance.mjs +240 -0
  185. package/scripts/maintain-version.json +4 -0
  186. package/scripts/release-gate.mjs +9 -0
  187. package/scripts/verify-version.json +2 -2
  188. package/scripts/vitest-tmpdir.mjs +26 -0
  189. package/scripts/build-verify-package.mjs +0 -210
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+ // Acceptance for the maintain CLI — M-ADR 0218 §4.2–4.3.
3
+ //
4
+ // The property: every write sdd-maintain makes is DERIVED or METHOD-MANAGED, and is a
5
+ // deterministic function of its inputs. Tested, not asserted. For each workspace: fresh clones,
6
+ // each maintain verb run twice, and after each run
7
+ // (a) SCOPE: every path `git status --porcelain --ignored` reports is in the
8
+ // derived-and-managed set (the lock's own managed paths included);
9
+ // (b) DETERMINISM: the second run on the same commit leaves exactly the bytes the first did.
10
+ // Fails closed on: an out-of-set write, a second run that changes anything, a verb that crashes,
11
+ // and the CONTROL: a deliberate out-of-set write must be SEEN, or every PASS is vacuous.
12
+ // Verbs it cannot exercise in a workspace are listed under COVERAGE, never passed silently.
13
+ //
14
+ // (c) PARITY: the same verb run by the full sdd-cli (--control-bin) on a twin workspace leaves a
15
+ // byte-identical tree. Scope and determinism cannot see a verb that crashes or renders
16
+ // differently; verify-cli learned the same lesson (0.1.0 shipped missing bundled scripts).
17
+ // usage: maintain-acceptance.mjs --bin <maintain-bin.js> --control-bin <sdd-cli cli.js> --dist <sdd-method dist dir> <sdd-repo>...
18
+ // Each <sdd-repo> is cloned with the siblings its aggregation manifest names (paths kept),
19
+ // so `catalogue aggregate` runs on a platform root.
20
+
21
+ import { execFileSync, spawnSync } from "node:child_process";
22
+ import { createHash } from "node:crypto";
23
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, mkdirSync } from "node:fs";
24
+ import { tmpdir } from "node:os";
25
+ import { basename, dirname, join, relative, resolve } from "node:path";
26
+
27
+ const args = process.argv.slice(2);
28
+ const opt = (k) => { const i = args.indexOf(k); if (i === -1) return undefined; const v = args[i + 1]; args.splice(i, 2); return v; };
29
+ const BIN = resolve(opt("--bin") ?? die("--bin required"));
30
+ const DIST = resolve(opt("--dist") ?? die("--dist required"));
31
+ const CONTROL = resolve(opt("--control-bin") ?? die("--control-bin required (the full sdd-cli, for parity)"));
32
+ const REPOS = args.map((a) => resolve(a));
33
+ if (REPOS.length === 0) die("give at least one SDD repository");
34
+
35
+ function die(m) { process.stderr.write(`maintain-acceptance: ${m}\n`); process.exit(2); }
36
+ let FAIL = 0;
37
+ const fail = (m) => { FAIL++; process.stdout.write(` FAIL ${m}\n`); };
38
+ const ok = (m) => process.stdout.write(` ok ${m}\n`);
39
+ const exercised = new Set();
40
+
41
+ // ---- the derived-and-managed set (M-ADR 0218 §4.3) -------------------------------------------
42
+ const FIXED = [
43
+ /^method-baseline\.lock$/, /^\.sdd-repo-kind$/, // item 2
44
+ /^\.github\/workflows\/method-validation\.yml$/, // item 3 (block-merged)
45
+ /^\.gitlab-ci\.yml$/, /^\.jenkins\/method-validation\.Jenkinsfile$/, // item 3, the other CI bindings (M-ADR 0219 §4.4)
46
+ /^\.dagger\/method-validation\//, // item 3, the module every binding calls (M-ADR 0219 §4.3)
47
+ /^agent-instructions\.md$/, /^CLAUDE\.md$/, /^AGENTS\.md$/, // item 4
48
+ /^\.claude\/settings\.json$/, /^\.github\/hooks\//, /^\.github\/copilot\//, // item 5
49
+ /^catalogues\//, /^catalogues-aggregated\//, /^catalogues(-aggregated)?-graph\.jsonl$/, // item 7
50
+ /^\.sdd\/bundle-cache\//, // sync's cache of the last-applied bundle (managed by sync itself)
51
+ /^MCP_SETUP\.md$/, // item 9 (code repo)
52
+ ];
53
+ function lockManaged(repo) {
54
+ const p = join(repo, "method-baseline.lock");
55
+ if (!existsSync(p)) return new Set();
56
+ const out = new Set();
57
+ for (const line of readFileSync(p, "utf8").split("\n")) {
58
+ const m = /^ {6}- "?(?:[0-9a-f]{64} {2})?([^"]+)"?$/.exec(line);
59
+ if (m) out.add(m[1]);
60
+ }
61
+ return out;
62
+ }
63
+ function outOfSet(repo, managed) {
64
+ const st = spawnSync("git", ["-C", repo, "status", "--porcelain", "--ignored", "--untracked-files=all"], { encoding: "utf8" });
65
+ const bad = [];
66
+ for (const line of st.stdout.split("\n").filter(Boolean)) {
67
+ let p = line.slice(3);
68
+ if (p.includes(" -> ")) p = p.split(" -> ")[1];
69
+ p = p.replace(/^"|"$/g, "");
70
+ if (managed.has(p) || FIXED.some((rx) => rx.test(p))) continue;
71
+ bad.push(p);
72
+ }
73
+ return bad;
74
+ }
75
+ // A digest of every file git reports as changed, for the determinism check.
76
+ // Workspace roots are normalised so the twin (another temp path) can be compared: context pull, for
77
+ // one, writes the SDD's absolute path into what it renders.
78
+ function snapshot(repo, roots = []) {
79
+ const st = spawnSync("git", ["-C", repo, "status", "--porcelain", "--ignored", "--untracked-files=all"], { encoding: "utf8" });
80
+ const h = createHash("sha256");
81
+ for (const line of st.stdout.split("\n").filter(Boolean).sort()) {
82
+ const p = line.slice(3).split(" -> ").pop().replace(/^"|"$/g, "");
83
+ h.update(line.slice(0, 3) + p + "\0");
84
+ const abs = join(repo, p);
85
+ if (existsSync(abs) && statSync(abs).isFile()) {
86
+ let text = readFileSync(abs, "latin1");
87
+ for (const r of roots) text = text.split(r).join("<WS>");
88
+ h.update(text, "latin1");
89
+ }
90
+ }
91
+ return h.digest("hex");
92
+ }
93
+
94
+ // ---- workspace -------------------------------------------------------------------------------
95
+ // Each workspace clones a whole manifest tree (up to ~55k inodes), and /tmp is a tmpfs whose inode
96
+ // table is the real limit: one release's runs leaked ~450k and filled it. Removed on every exit,
97
+ // failures included; MAINTAIN_ACC_KEEP=1 keeps them for inspection.
98
+ const workspaces = [];
99
+ process.on("exit", () => {
100
+ if (process.env.MAINTAIN_ACC_KEEP) return void process.stderr.write(`maintain-acceptance: kept ${workspaces.join(" ")}\n`);
101
+ for (const ws of workspaces) rmSync(ws, { recursive: true, force: true });
102
+ });
103
+ function cloneTree(root) {
104
+ const ws = mkdtempSync(join(tmpdir(), "maintain-acc-"));
105
+ workspaces.push(ws);
106
+ const base = dirname(root);
107
+ const todo = [root];
108
+ const seen = new Set();
109
+ const missing = [];
110
+ while (todo.length) {
111
+ const src = todo.pop();
112
+ if (seen.has(src)) continue;
113
+ seen.add(src);
114
+ const dest = join(ws, relative(base, src));
115
+ mkdirSync(dirname(dest), { recursive: true });
116
+ execFileSync("git", ["clone", "-q", "--no-hardlinks", src, dest]);
117
+ const man = join(src, "orchestration", "aggregation-manifest.yaml");
118
+ if (existsSync(man)) {
119
+ for (const m of readFileSync(man, "utf8").matchAll(/path: "([^"]+)"/g)) {
120
+ let p = resolve(src, m[1]);
121
+ if (["catalogues", "catalogues-aggregated"].includes(basename(p))) p = dirname(p);
122
+ if (existsSync(join(p, ".git"))) todo.push(p);
123
+ else missing.push(relative(base, p));
124
+ }
125
+ }
126
+ }
127
+ return { ws, clone: join(ws, basename(root)), base, missing };
128
+ }
129
+ const run = (repo, verbArgs, bin = BIN) => spawnSync("node", [bin, ...verbArgs], { cwd: repo, encoding: "utf8" });
130
+ // The twin workspace's path for a path in the primary one (parity runs mirror every call).
131
+ let TWIN = null;
132
+ const twinOf = (p) => (TWIN && p.startsWith(TWIN.from) ? TWIN.to + p.slice(TWIN.from.length) : p);
133
+ function twice(label, repo, verbArgs, managedFn, { allowNonZero = false } = {}) {
134
+ const r1 = run(repo, verbArgs);
135
+ // An uncaught error (maintain-bin prints "sdd-maintain: <message>") is a CRASH, never a finding,
136
+ // whatever the exit code allowance: allowNonZero once let an ENOENT pass as render screen's "exit 1".
137
+ const crashed = /^sdd-maintain: /m.test(r1.stderr ?? "");
138
+ if (crashed || (r1.status !== 0 && !allowNonZero)) { fail(`${label}: ${crashed ? "CRASHED" : `exit ${r1.status}`} — ${(r1.stderr || r1.stdout).trim().split("\n").pop()}`); return; }
139
+ const bad1 = outOfSet(repo, managedFn());
140
+ if (bad1.length) fail(`${label}: wrote outside the derived-and-managed set: ${bad1.slice(0, 5).join(", ")}`);
141
+ const roots = TWIN ? [TWIN.from, TWIN.to] : [];
142
+ const s1 = snapshot(repo, roots);
143
+ const r2 = run(repo, verbArgs);
144
+ const s2 = snapshot(repo, roots);
145
+ // parity: the full CLI, same verb, same arguments, on the twin
146
+ const twinArgs = verbArgs.map((a) => twinOf(a));
147
+ const rc = run(twinOf(repo), twinArgs, CONTROL);
148
+ run(twinOf(repo), twinArgs, CONTROL); // keep the twin in step with the primary's second run
149
+ const sc = snapshot(twinOf(repo), roots);
150
+ const parity = sc === s2 && rc.status === r1.status;
151
+ if (!parity) fail(`${label}: PARITY — sdd-cli ${sc === s2 ? "" : "left a different tree, "}exited ${rc.status} where sdd-maintain exited ${r1.status}`);
152
+ if (s1 !== s2) fail(`${label}: NOT deterministic — the second run on the same commit changed the tree`);
153
+ else if (!bad1.length && parity) ok(`${label} (exit ${r1.status}): in scope, second run byte-identical, = sdd-cli`);
154
+ exercised.add(label.split(" @")[0]);
155
+ void r2;
156
+ }
157
+
158
+ // ---- control: the detector must see an out-of-set write --------------------------------------
159
+ {
160
+ const { clone } = cloneTree(REPOS[0]);
161
+ writeFileSync(join(clone, "docs", "CONTROL-out-of-set.md"), "x\n");
162
+ if (outOfSet(clone, lockManaged(clone)).includes("docs/CONTROL-out-of-set.md")) ok("CONTROL: an out-of-set write is seen");
163
+ else fail("CONTROL: an out-of-set write was NOT seen — every scope PASS below would be vacuous");
164
+ }
165
+
166
+ for (const repo of REPOS) {
167
+ const { clone, ws, missing } = cloneTree(repo);
168
+ const twin = cloneTree(repo);
169
+ TWIN = { from: ws, to: twin.ws };
170
+ const name = basename(repo);
171
+ process.stdout.write(`=== ${name} ===\n`);
172
+ const managed = () => lockManaged(clone);
173
+ // Bundle the lock is on: re-applying it is a real sync with nothing upstream changed.
174
+ const lock = readFileSync(join(clone, "method-baseline.lock"), "utf8");
175
+ const fwd = /- name: ([\w-]+)\n\s+bundle_version: "([^"]+)"(?:[\s\S]*?)is_forward_base: true/.exec(lock);
176
+ if (fwd) {
177
+ const tb = join(DIST, fwd[1], `${fwd[1]}-${fwd[2]}.tar.gz`);
178
+ // A lock that predates the CI digest (written before M-ADR 0218 §4.7) refuses its first sync on
179
+ // a newer CI render by design, and the wave passes --allow-overwrite exactly once; do the same.
180
+ const firstDigest = /^ci_workflow_block_sha256:/m.test(lock) ? [] : ["--allow-overwrite"];
181
+ if (existsSync(tb)) twice(`sync @${name}`, clone, ["sync", "--bundle", tb, "--target-repo", ".", ...firstDigest], managed);
182
+ else process.stdout.write(` skip sync: ${tb} not on disk\n`);
183
+ }
184
+ twice(`generate-briefing @${name}`, clone, ["generate-briefing", "--target-dir", "."], managed);
185
+ // A build may exit 1 on pre-existing estate findings; scope and determinism hold either way.
186
+ twice(`catalogue build @${name}`, clone, ["catalogue", "build", "--sdd-path", "."], managed, { allowNonZero: true });
187
+ twice(`catalogue graph @${name}`, clone, ["catalogue", "graph", "--sdd-path", "."], managed, { allowNonZero: true });
188
+ if (existsSync(join(clone, "orchestration", "aggregation-manifest.yaml")) && missing.length > 0) {
189
+ process.stdout.write(` skip catalogue aggregate: ${missing.length} manifest sibling(s) not on disk to clone (e.g. ${missing[0]})\n`);
190
+ } else if (existsSync(join(clone, "orchestration", "aggregation-manifest.yaml"))) {
191
+ // members first, so the aggregate reads current member catalogues
192
+ for (const d of readdirSync(ws, { recursive: true }).filter((p) => p.endsWith(".sdd-repo-kind"))) {
193
+ const member = join(ws, dirname(d));
194
+ if (member !== clone) {
195
+ run(member, ["catalogue", "build", "--sdd-path", "."]);
196
+ run(twinOf(member), ["catalogue", "build", "--sdd-path", "."], CONTROL);
197
+ }
198
+ }
199
+ twice(`catalogue aggregate @${name}`, clone, ["catalogue", "aggregate", "--sdd-path", "."], managed, { allowNonZero: true });
200
+ }
201
+ // catalogue diff: from HEAD with an empty delta, so attribution is trivially complete; it writes nothing into the SDD.
202
+ {
203
+ const delta = join(ws, `${name}-delta.yaml`);
204
+ writeFileSync(delta, "findings: []\n");
205
+ writeFileSync(twinOf(delta), "findings: []\n");
206
+ twice(`catalogue diff @${name}`, clone, ["catalogue", "diff", "--from", "HEAD", "--attribute", delta, "--sdd-path", "."], managed, { allowNonZero: true });
207
+ }
208
+ // pack build: every seed the SDD produces, to an output OUTSIDE its tracked content (§4.3 item 8).
209
+ const distDir = join(clone, "orchestration", "distribution");
210
+ if (existsSync(distDir)) {
211
+ const seed = readdirSync(distDir).find((f) => f.endsWith("-seed.yaml"));
212
+ if (seed) twice(`pack build @${name}`, clone, ["pack", "build", "--seed", join(distDir, seed), "--producer-repo", ".", "--out-dir", join(ws, "pack-out")], managed, { allowNonZero: true });
213
+ }
214
+ // render screen: the first screen spec, to an explicit --out outside the tree (§4.3 item 8).
215
+ const screens = join(clone, "screens");
216
+ if (existsSync(screens)) {
217
+ const spec = readdirSync(screens).find((f) => f.endsWith(".md"));
218
+ if (spec) twice(`render screen @${name}`, clone, ["render", "screen", join("screens", spec), "--out", join(ws, "screen.svg")], managed);
219
+ }
220
+ // context pull: a scratch code repo beside the clone, named for a repository this SDD catalogues.
221
+ const repoCsv = join(clone, "catalogues", "repositories.csv");
222
+ if (existsSync(repoCsv)) {
223
+ const id = readFileSync(repoCsv, "utf8").split("\n")[1]?.split(",")[0];
224
+ if (id && existsSync(join(clone, "orchestration/templates/briefing/code-repo/agent-instructions.md.hbs"))) {
225
+ const code = join(dirname(clone), id);
226
+ for (const c of [code, twinOf(code)]) {
227
+ mkdirSync(c, { recursive: true });
228
+ execFileSync("git", ["-C", c, "init", "-q"]);
229
+ }
230
+ twice(`context pull @${name}`, code, ["context", "pull", "--sdd", clone], () => new Set());
231
+ } else process.stdout.write(` skip context pull: ${id ? "the SDD's baseline has no code-repo templates yet" : "no repositories catalogued"}\n`);
232
+ }
233
+ }
234
+
235
+ const SURFACE = ["sync", "generate-briefing", "catalogue build", "catalogue graph", "catalogue aggregate", "context pull", "catalogue diff", "pack build", "render screen"];
236
+ const missing = SURFACE.filter((v) => !exercised.has(v));
237
+ process.stdout.write(`COVERAGE exercised: ${[...exercised].join(", ")}\n`);
238
+ if (missing.length) process.stdout.write(`COVERAGE NOT exercised here (needs inputs this run lacks): ${missing.join(", ")}\n`);
239
+ process.stdout.write(FAIL === 0 ? "ACCEPTANCE PASS — every maintain write in scope and deterministic\n" : `ACCEPTANCE FAIL (${FAIL})\n`);
240
+ process.exit(FAIL === 0 ? 0 : 1);
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "built_against_sdd_cli": "1.43.0"
4
+ }
@@ -39,6 +39,15 @@ if (process.argv.includes("--audit")) {
39
39
  process.exit(0);
40
40
  }
41
41
 
42
+ // M-ADR 0218 §4.6: the persona packages are released WITH sdd-cli, in one change-set. A release that
43
+ // bumps sdd-cli without re-stamping a persona's version file would build a package that claims an
44
+ // engine it is not, so the publish refuses (the build refuses it too).
45
+ for (const persona of ["verify", "maintain", "author"]) {
46
+ const f = JSON.parse(readFileSync(join(root, "scripts", `${persona}-version.json`), "utf-8"));
47
+ if (f.built_against_sdd_cli !== pkg.version)
48
+ fail(`scripts/${persona}-version.json says built_against_sdd_cli ${f.built_against_sdd_cli}, but this release is ${pkg.version}; bump the persona's version and re-stamp it in the release commit`);
49
+ }
50
+
42
51
  const tag = `v${pkg.version}`;
43
52
  if (run("git", ["status", "--porcelain", "--untracked-files=no"]) !== "") fail("the working tree has uncommitted changes to tracked files");
44
53
  const head = run("git", ["rev-parse", "HEAD"]);
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.0.0",
3
- "built_against_sdd_cli": "1.41.0"
2
+ "version": "1.2.0",
3
+ "built_against_sdd_cli": "1.43.0"
4
4
  }
@@ -0,0 +1,26 @@
1
+ // Gives each test run its own temp directory and removes it whole when the run ends.
2
+ //
3
+ // Every fixture is created under os.tmpdir(), which reads TMPDIR. Setting TMPDIR here, in the
4
+ // main process before any worker starts, sends every fixture of this run (and of the bash
5
+ // scripts the tests spawn, which honour TMPDIR via mktemp) into one directory, whatever prefix
6
+ // its test chose. vitest-teardown.mjs could only sweep the prefixes it listed: 3 of the 129 the
7
+ // tests used, so the rest accumulated in /tmp (14,203 directories on 2026-10-16). A concurrent
8
+ // run on the same machine has its own directory, so neither run can touch the other's fixtures.
9
+ import { mkdtempSync, rmSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import sweepLegacy from "./vitest-teardown.mjs";
13
+
14
+ export default function setup() {
15
+ const original = process.env.TMPDIR;
16
+ const dir = mkdtempSync(join(tmpdir(), "sdd-cli-vitest-"));
17
+ process.env.TMPDIR = dir;
18
+ return async () => {
19
+ if (process.env.SDD_CLI_KEEP_TMP) process.stdout.write(`vitest-tmpdir: kept ${dir}\n`);
20
+ else rmSync(dir, { recursive: true, force: true });
21
+ // Fixtures from runs before this setup existed sit directly in /tmp; sweep them by prefix and age.
22
+ if (original === undefined) delete process.env.TMPDIR;
23
+ else process.env.TMPDIR = original;
24
+ await sweepLegacy();
25
+ };
26
+ }
@@ -1,210 +0,0 @@
1
- // Assemble the publishable @sdd-method/verify-cli package — M-ADR 0212 §4.3b.
2
- //
3
- // ONE SOURCE, and this time a genuinely smaller ARTEFACT. The conformance
4
- // package copied the whole of dist/ under a second manifest, so its "excluded"
5
- // verbs were one `node dist/cli.js …` away. This script ships only the import
6
- // CLOSURE of dist/verify-bin.js: an excluded verb's code is not in the package
7
- // to be reached by any path.
8
- //
9
- // Fails closed on four conditions, each of which would otherwise let the
10
- // boundary pass while not holding:
11
- // 1. a relative import in the closure that does not resolve to a file;
12
- // 2. a bare import that is neither a Node built-in nor a declared dependency;
13
- // 3. a CANARY — a known authoring module — present in the closure;
14
- // 4. a canary that no longer EXISTS in dist. A canary that was renamed or
15
- // deleted cannot be absent from anything, so the check above would pass
16
- // vacuously; this makes it fail instead, and asks for the list to follow.
17
- //
18
- // The generated directory is gitignored and safe to delete.
19
-
20
- import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
21
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
22
- import { builtinModules } from "node:module";
23
- import { dirname, join, relative, resolve } from "node:path";
24
- import { fileURLToPath } from "node:url";
25
-
26
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
27
- // SDD_CLI_DIST: check a build made elsewhere. `dist/` is what a linked global `sdd-cli`
28
- // runs, so building it to run this check silently put unreleased code in front of every
29
- // session on the machine (2026-10-01, about nine hours). `npm run verify:boundary` builds
30
- // into build/check-dist and checks that instead; only a release should write dist/.
31
- const DIST = process.env.SDD_CLI_DIST ? resolve(process.env.SDD_CLI_DIST) : join(ROOT, "dist");
32
- const OUT = join(ROOT, "build", "verify-cli");
33
- const ENTRY = join(DIST, "verify-bin.js");
34
-
35
- // Authoring and maintenance code that must never reach this package. Entry
36
- // modules of excluded verbs plus the full CLI's own entrypoints.
37
- const CANARIES = [
38
- "cli.js",
39
- "index.js",
40
- "verbs/sync.js",
41
- "verbs/catalogue.js",
42
- "verbs/pack.js",
43
- "verbs/mcp.js",
44
- "verbs/init.js",
45
- "verbs/render.js",
46
- "verbs/metamodel.js",
47
- "verbs/conformance.js",
48
- "lib/sync/index.js",
49
- "lib/catalogue/build.js",
50
- "lib/catalogue/aggregate.js",
51
- "lib/catalogue/graph-run.js",
52
- "lib/catalogue/migration-map.js",
53
- "lib/pack/builder.js",
54
- ];
55
-
56
- const die = (msg) => {
57
- process.stderr.write(`build-verify-package: FAIL — ${msg}\n`);
58
- process.exit(1);
59
- };
60
-
61
- if (!existsSync(ENTRY)) die(`${relative(ROOT, ENTRY)} missing — run \`npm run build\` first`);
62
-
63
- // ---- 1. closure over imports ----------------------------------------------
64
- // Line-based over tsc output, skipping comment lines. An earlier regex over the
65
- // whole text read `// from "changed" produces…` in a comment as an import of a
66
- // package called `changed` — prose parsed as code, the same failure a grep over
67
- // workflow comments made the same day. Fail-closed caught it; this stops it.
68
- const STATIC = /^\s*(?:import|export|\})[^\n]*?\bfrom\s*["']([^"']+)["']/;
69
- const SIDE = /^\s*import\s*["']([^"']+)["']/;
70
- const DYNAMIC = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
71
- function specifiers(src) {
72
- const out = [];
73
- for (const line of src.split("\n")) {
74
- const t = line.trimStart();
75
- if (t.startsWith("//") || t.startsWith("*") || t.startsWith("/*")) continue;
76
- const m = line.match(STATIC) ?? line.match(SIDE);
77
- if (m) out.push(m[1]);
78
- for (const d of line.matchAll(DYNAMIC)) out.push(d[1]);
79
- }
80
- return out;
81
- }
82
- const closure = new Set();
83
- const bare = new Set();
84
- const stack = [ENTRY];
85
- while (stack.length) {
86
- const f = stack.pop();
87
- if (closure.has(f)) continue;
88
- closure.add(f);
89
- for (const spec of specifiers(readFileSync(f, "utf8"))) {
90
- if (spec.startsWith(".")) {
91
- const target = resolve(dirname(f), spec);
92
- if (!existsSync(target)) die(`${relative(DIST, f)} imports '${spec}', which does not resolve`);
93
- stack.push(target);
94
- } else {
95
- bare.add(spec);
96
- }
97
- }
98
- }
99
-
100
- // ---- 2. dependencies = only what the closure imports -----------------------
101
- const main = JSON.parse(await readFile(join(ROOT, "package.json"), "utf8"));
102
- const pkgName = (spec) =>
103
- spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
104
- const builtins = new Set(builtinModules);
105
- const deps = {};
106
- for (const spec of bare) {
107
- if (spec.startsWith("node:") || builtins.has(spec) || builtins.has(spec.split("/")[0])) continue;
108
- const name = pkgName(spec);
109
- const range = main.dependencies?.[name];
110
- if (!range) die(`closure imports '${spec}', which is not a declared dependency of sdd-cli`);
111
- deps[name] = range;
112
- }
113
-
114
- // ---- 3 + 4. canaries -------------------------------------------------------
115
- for (const c of CANARIES) {
116
- const abs = join(DIST, c);
117
- if (!existsSync(abs)) die(`canary '${c}' no longer exists in dist — the check would pass vacuously; update CANARIES`);
118
- if (closure.has(abs)) die(`authoring module '${c}' is reachable from verify-bin.js — the boundary does not hold`);
119
- }
120
-
121
- // ---- bundled shell scripts -------------------------------------------------
122
- // Some validators are shell scripts SHIPPED WITH the package (dispatch.ts:
123
- // `kind: "bundled"`), resolved by lib/scripts.ts to <package-root>/scripts/.
124
- // They are launched by path, not imported, so the closure above cannot see
125
- // them. verify-cli 0.1.0 shipped without them and `app-docs` and
126
- // `integration-docs` failed with exit 127 on every app and integration repo —
127
- // found by a parity check against sdd-cli, not by the no-write acceptance,
128
- // because a validator that cannot find its script writes nothing.
129
- const bundled = new Set();
130
- for (const f of closure) {
131
- for (const m of readFileSync(f, "utf8").matchAll(/kind:\s*"bundled",\s*script:\s*"([^"]+)"/g)) {
132
- const p = join(ROOT, "scripts", m[1]);
133
- if (!existsSync(p)) die(`${relative(DIST, f)} declares bundled script '${m[1]}', which does not exist in scripts/`);
134
- bundled.add(p);
135
- }
136
- }
137
-
138
- // ---- assets: yaml/json data beside a closure module ------------------------
139
- const assets = new Set();
140
- for (const f of closure) {
141
- const dir = dirname(f);
142
- for (const name of readdirSync(dir)) {
143
- const p = join(dir, name);
144
- if (/\.(ya?ml|json)$/.test(name) && statSync(p).isFile()) assets.add(p);
145
- }
146
- }
147
-
148
- // ---- assemble --------------------------------------------------------------
149
- // The version is COMMITTED (scripts/verify-version.json), not defaulted. Defaulting to "0.1.0" let a build
150
- // of new source carry an old version silently; verify-cli then fell two sdd-cli releases behind unnoticed.
151
- // Bump the file in every sdd-cli release commit (minor with the CLI's minor; patch for fixes).
152
- const versionFile = JSON.parse(readFileSync(join(ROOT, "scripts", "verify-version.json"), "utf8"));
153
- const version = versionFile.version;
154
- if (!/^\d+\.\d+\.\d+$/.test(version ?? "")) die("scripts/verify-version.json has no semver `version`");
155
- if (process.env.VERIFY_VERSION && process.env.VERIFY_VERSION !== version)
156
- die(`VERIFY_VERSION=${process.env.VERIFY_VERSION} disagrees with scripts/verify-version.json (${version}); edit the file`);
157
- // Empty the directory, never remove it: a shell sitting in build/verify-cli
158
- // (to run `npm publish`) keeps a handle on the directory itself, and deleting
159
- // it leaves that shell in a path that no longer exists — npm then dies with
160
- // ENOENT uv_cwd. That happened, twice, between a rebuild and a publish.
161
- await mkdir(OUT, { recursive: true });
162
- for (const entry of readdirSync(OUT)) await rm(join(OUT, entry), { recursive: true, force: true });
163
- for (const f of [...closure, ...assets]) {
164
- const dest = join(OUT, "dist", relative(DIST, f));
165
- await mkdir(dirname(dest), { recursive: true });
166
- await cp(f, dest);
167
- }
168
- for (const p of bundled) {
169
- await mkdir(join(OUT, "scripts"), { recursive: true });
170
- await cp(p, join(OUT, "scripts", relative(join(ROOT, "scripts"), p)));
171
- }
172
- for (const f of ["LICENSE", "NOTICE"]) await cp(join(ROOT, f), join(OUT, f));
173
-
174
- if (main.publishConfig?.access !== "public") die("sdd-cli's package.json has no publishConfig.access=public — the verify package would publish private");
175
-
176
- const pkg = {
177
- name: "@sdd-method/verify-cli",
178
- version,
179
- description:
180
- "Verify an SDD repository without changing it — validate, list, and read-only catalogue and pack checks. Ships only the code those verbs reach; it cannot write to the repository it is pointed at. Authoring and maintenance are in @sdd-method/sdd-cli.",
181
- license: main.license,
182
- type: main.type,
183
- bin: { "sdd-verify": "dist/verify-bin.js" },
184
- engines: main.engines,
185
- dependencies: deps,
186
- repository: main.repository,
187
- // Inherited, not assumed: a scoped package publishes PRIVATE by default, and
188
- // the first publish of this one failed with E402 (private packages need a
189
- // paid plan) because this field was missing. sdd-cli carries it; so must this.
190
- publishConfig: main.publishConfig,
191
- files: ["dist", "scripts", "README.md", "LICENSE", "NOTICE"],
192
- };
193
- await writeFile(join(OUT, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
194
- await writeFile(
195
- join(OUT, "README.md"),
196
- `# @sdd-method/verify-cli\n\n` +
197
- `\`sdd-verify\` checks an SDD repository and **cannot change it**.\n\n` +
198
- `Verbs: \`validate\`, \`list\`, \`pack validate\`, \`catalogue validate\` (incl. \`--coverage\`), ` +
199
- `\`catalogue impact\`, \`catalogue baseline-check\`, \`catalogue version\`.\n\n` +
200
- `The package contains only the code those verbs reach. Authoring and maintenance verbs — ` +
201
- `\`sync\`, \`catalogue build\`, \`init\`, \`scaffold\`, \`mcp\` and the rest — are not present in it, ` +
202
- `and are in \`@sdd-method/sdd-cli\`. Invoking one fails with exit 1.\n`,
203
- );
204
-
205
- const kb = (fs) => Math.round(fs.reduce((n, f) => n + statSync(f).size, 0) / 1024);
206
- process.stdout.write(
207
- `assembled ${pkg.name}@${version} at build/verify-cli\n` +
208
- ` modules ${closure.size} (${kb([...closure])} kB) · assets ${assets.size} · bundled scripts ${bundled.size} · deps ${Object.keys(deps).join(", ")}\n` +
209
- ` canaries absent: ${CANARIES.length}/${CANARIES.length}\n`,
210
- );