@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.
- package/README.md +4 -57
- package/dist/author-bin.d.ts +3 -0
- package/dist/author-bin.d.ts.map +1 -0
- package/dist/author-bin.js +8 -0
- package/dist/author-bin.js.map +1 -0
- package/dist/author-program.d.ts +8 -0
- package/dist/author-program.d.ts.map +1 -0
- package/dist/author-program.js +99 -0
- package/dist/author-program.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-harness.d.ts +11 -8
- package/dist/lib/agent-harness.d.ts.map +1 -1
- package/dist/lib/agent-harness.js +17 -8
- package/dist/lib/agent-harness.js.map +1 -1
- package/dist/lib/api-surface/index.js +2 -2
- package/dist/lib/api-surface/index.js.map +1 -1
- package/dist/lib/briefing/index.d.ts +1 -1
- package/dist/lib/briefing/index.d.ts.map +1 -1
- package/dist/lib/briefing/index.js +1 -1
- package/dist/lib/briefing/index.js.map +1 -1
- package/dist/lib/briefing/render.d.ts +18 -1
- package/dist/lib/briefing/render.d.ts.map +1 -1
- package/dist/lib/briefing/render.js +17 -8
- package/dist/lib/briefing/render.js.map +1 -1
- package/dist/lib/catalogue/aggregation-manifest-loader.js +4 -4
- package/dist/lib/catalogue/aggregation-manifest-loader.js.map +1 -1
- package/dist/lib/catalogue/cli-version.d.ts +1 -1
- package/dist/lib/catalogue/cli-version.d.ts.map +1 -1
- package/dist/lib/catalogue/cli-version.js +3 -3
- package/dist/lib/catalogue/cli-version.js.map +1 -1
- package/dist/lib/catalogue/freshness.d.ts +27 -0
- package/dist/lib/catalogue/freshness.d.ts.map +1 -0
- package/dist/lib/catalogue/freshness.js +70 -0
- package/dist/lib/catalogue/freshness.js.map +1 -0
- package/dist/lib/catalogue/graph-run.js +1 -1
- package/dist/lib/catalogue/graph-run.js.map +1 -1
- package/dist/lib/ci-binding/index.d.ts +63 -0
- package/dist/lib/ci-binding/index.d.ts.map +1 -0
- package/dist/lib/ci-binding/index.js +220 -0
- package/dist/lib/ci-binding/index.js.map +1 -0
- package/dist/lib/ci-binding/module.d.ts +36 -0
- package/dist/lib/ci-binding/module.d.ts.map +1 -0
- package/dist/lib/ci-binding/module.js +204 -0
- package/dist/lib/ci-binding/module.js.map +1 -0
- package/dist/lib/context-pull/index.d.ts +45 -0
- package/dist/lib/context-pull/index.d.ts.map +1 -0
- package/dist/lib/context-pull/index.js +191 -0
- package/dist/lib/context-pull/index.js.map +1 -0
- package/dist/lib/context-pull/resolve.d.ts +46 -0
- package/dist/lib/context-pull/resolve.d.ts.map +1 -0
- package/dist/lib/context-pull/resolve.js +70 -0
- package/dist/lib/context-pull/resolve.js.map +1 -0
- package/dist/lib/context-pull/retired-mcp-setup.d.ts +7 -0
- package/dist/lib/context-pull/retired-mcp-setup.d.ts.map +1 -0
- package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
- package/dist/lib/context-pull/retired-mcp-setup.js.map +1 -0
- package/dist/lib/hooks-seed/entries.d.ts +15 -0
- package/dist/lib/hooks-seed/entries.d.ts.map +1 -1
- package/dist/lib/hooks-seed/entries.js +30 -1
- package/dist/lib/hooks-seed/entries.js.map +1 -1
- package/dist/lib/hooks-seed/index.d.ts +4 -9
- package/dist/lib/hooks-seed/index.d.ts.map +1 -1
- package/dist/lib/hooks-seed/index.js +4 -7
- package/dist/lib/hooks-seed/index.js.map +1 -1
- package/dist/lib/hooks-seed/merge.d.ts +7 -0
- package/dist/lib/hooks-seed/merge.d.ts.map +1 -1
- package/dist/lib/hooks-seed/merge.js +31 -0
- package/dist/lib/hooks-seed/merge.js.map +1 -1
- package/dist/lib/init/app.d.ts +0 -1
- package/dist/lib/init/app.d.ts.map +1 -1
- package/dist/lib/init/app.js +3 -7
- package/dist/lib/init/app.js.map +1 -1
- package/dist/lib/init/ci-workflow.d.ts +6 -8
- package/dist/lib/init/ci-workflow.d.ts.map +1 -1
- package/dist/lib/init/ci-workflow.js +57 -120
- package/dist/lib/init/ci-workflow.js.map +1 -1
- package/dist/lib/init/integration.d.ts +0 -1
- package/dist/lib/init/integration.d.ts.map +1 -1
- package/dist/lib/init/integration.js +3 -7
- package/dist/lib/init/integration.js.map +1 -1
- package/dist/lib/init/platform.d.ts +2 -10
- package/dist/lib/init/platform.d.ts.map +1 -1
- package/dist/lib/init/platform.js +12 -15
- package/dist/lib/init/platform.js.map +1 -1
- package/dist/lib/lock-file.d.ts +20 -0
- package/dist/lib/lock-file.d.ts.map +1 -1
- package/dist/lib/lock-file.js +25 -1
- package/dist/lib/lock-file.js.map +1 -1
- package/dist/lib/mcp/build-info.d.ts.map +1 -1
- package/dist/lib/mcp/build-info.js +3 -3
- package/dist/lib/mcp/build-info.js.map +1 -1
- package/dist/lib/pack/index.js +2 -2
- package/dist/lib/pack/index.js.map +1 -1
- package/dist/lib/pack/validate.js +2 -2
- package/dist/lib/pack/validate.js.map +1 -1
- package/dist/lib/package-version.d.ts +23 -0
- package/dist/lib/package-version.d.ts.map +1 -1
- package/dist/lib/package-version.js +47 -10
- package/dist/lib/package-version.js.map +1 -1
- package/dist/lib/plan/index.d.ts.map +1 -1
- package/dist/lib/plan/index.js +4 -4
- package/dist/lib/plan/index.js.map +1 -1
- package/dist/lib/plan/read-sdd.js +1 -1
- package/dist/lib/plan/render-template.d.ts +8 -0
- package/dist/lib/plan/render-template.d.ts.map +1 -1
- package/dist/lib/plan/render-template.js +27 -3
- package/dist/lib/plan/render-template.js.map +1 -1
- package/dist/lib/sdd-index/repo-config.d.ts +1 -1
- package/dist/lib/sdd-index/repo-config.js +1 -1
- package/dist/lib/sdd-reader.d.ts +35 -0
- package/dist/lib/sdd-reader.d.ts.map +1 -0
- package/dist/lib/sdd-reader.js +91 -0
- package/dist/lib/sdd-reader.js.map +1 -0
- package/dist/lib/sync/briefing-merge.d.ts +1 -1
- package/dist/lib/sync/briefing-merge.d.ts.map +1 -1
- package/dist/lib/sync/briefing-merge.js +50 -34
- package/dist/lib/sync/briefing-merge.js.map +1 -1
- package/dist/lib/sync/diff.js +1 -1
- package/dist/lib/sync/diff.js.map +1 -1
- package/dist/lib/sync/index.d.ts +16 -6
- package/dist/lib/sync/index.d.ts.map +1 -1
- package/dist/lib/sync/index.js +89 -116
- package/dist/lib/sync/index.js.map +1 -1
- package/dist/lib/sync/settings-merge.d.ts +26 -48
- package/dist/lib/sync/settings-merge.d.ts.map +1 -1
- package/dist/lib/sync/settings-merge.js +70 -162
- package/dist/lib/sync/settings-merge.js.map +1 -1
- package/dist/lib/validate/bundle-coherence.js +2 -2
- package/dist/lib/validate/bundle-coherence.js.map +1 -1
- package/dist/lib/validate/plan.d.ts +25 -0
- package/dist/lib/validate/plan.d.ts.map +1 -0
- package/dist/lib/validate/plan.js +54 -0
- package/dist/lib/validate/plan.js.map +1 -0
- package/dist/maintain-bin.d.ts +3 -0
- package/dist/maintain-bin.d.ts.map +1 -0
- package/dist/maintain-bin.js +8 -0
- package/dist/maintain-bin.js.map +1 -0
- package/dist/maintain-program.d.ts +8 -0
- package/dist/maintain-program.d.ts.map +1 -0
- package/dist/maintain-program.js +94 -0
- package/dist/maintain-program.js.map +1 -0
- package/dist/maintain-version.json +4 -0
- package/dist/verbs/catalogue-check.d.ts +10 -0
- package/dist/verbs/catalogue-check.d.ts.map +1 -0
- package/dist/verbs/catalogue-check.js +28 -0
- package/dist/verbs/catalogue-check.js.map +1 -0
- package/dist/verbs/catalogue-maintain.d.ts +6 -0
- package/dist/verbs/catalogue-maintain.d.ts.map +1 -0
- package/dist/verbs/catalogue-maintain.js +110 -0
- package/dist/verbs/catalogue-maintain.js.map +1 -0
- package/dist/verbs/catalogue.d.ts +5 -0
- package/dist/verbs/catalogue.d.ts.map +1 -1
- package/dist/verbs/catalogue.js +16 -80
- package/dist/verbs/catalogue.js.map +1 -1
- package/dist/verbs/context.d.ts +3 -0
- package/dist/verbs/context.d.ts.map +1 -0
- package/dist/verbs/context.js +39 -0
- package/dist/verbs/context.js.map +1 -0
- package/dist/verbs/pack-build.d.ts +3 -0
- package/dist/verbs/pack-build.d.ts.map +1 -0
- package/dist/verbs/pack-build.js +31 -0
- package/dist/verbs/pack-build.js.map +1 -0
- package/dist/verbs/pack.d.ts.map +1 -1
- package/dist/verbs/pack.js +2 -25
- package/dist/verbs/pack.js.map +1 -1
- package/dist/verbs/sync.d.ts.map +1 -1
- package/dist/verbs/sync.js +21 -0
- package/dist/verbs/sync.js.map +1 -1
- package/dist/verbs/validate.d.ts.map +1 -1
- package/dist/verbs/validate.js +26 -0
- package/dist/verbs/validate.js.map +1 -1
- package/dist/verify-program.d.ts +6 -1
- package/dist/verify-program.d.ts.map +1 -1
- package/dist/verify-program.js +12 -5
- package/dist/verify-program.js.map +1 -1
- package/dist/verify-version.json +2 -2
- package/package.json +6 -5
- package/scripts/author-acceptance.mjs +170 -0
- package/scripts/author-version.json +4 -0
- package/scripts/build-persona-package.mjs +309 -0
- package/scripts/copy-yaml-assets.mjs +4 -2
- package/scripts/dagger-parity.sh +74 -0
- package/scripts/maintain-acceptance.mjs +240 -0
- package/scripts/maintain-version.json +4 -0
- package/scripts/release-gate.mjs +9 -0
- package/scripts/verify-version.json +2 -2
- package/scripts/vitest-tmpdir.mjs +26 -0
- package/scripts/build-verify-package.mjs +0 -210
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Acceptance for the author CLI — M-ADR 0220 §4.3, the scope layer.
|
|
3
|
+
//
|
|
4
|
+
// The property: no author verb writes into the MAINTAIN-ONLY part of the derived-and-managed set —
|
|
5
|
+
// the lock and the managed payload it records (0218 §4.3 items 1–2), the CI wrapper and its module
|
|
6
|
+
// (item 3), the harness settings and hooks (item 5), ADR transitions (item 6, which land in
|
|
7
|
+
// lock-managed ADR files), the derived model (item 7) or a pack (item 8). Tested, not asserted: each
|
|
8
|
+
// SDD is cloned fresh, every author verb this run can exercise is run, and after each run every path
|
|
9
|
+
// `git status --porcelain --ignored` reports is checked against that part of the set.
|
|
10
|
+
//
|
|
11
|
+
// Also, per verb: (b) PARITY, the full sdd-cli (--control-bin) on a twin clone leaves the same tree
|
|
12
|
+
// and exit code, because scope cannot see a verb that crashes or renders differently; (c) a CRASH
|
|
13
|
+
// (sdd-author: <message> on stderr) fails whatever the exit code. Fails closed on the CONTROL: a
|
|
14
|
+
// write into the maintain-only part must be SEEN, or every PASS is vacuous. Verbs it cannot exercise
|
|
15
|
+
// are listed under COVERAGE, never passed silently. Author verbs need not be deterministic (that is
|
|
16
|
+
// maintain's property), so a verb runs once per clone.
|
|
17
|
+
//
|
|
18
|
+
// usage: author-acceptance.mjs --bin <author-bin.js> --control-bin <sdd-cli cli.js> <sdd-repo>...
|
|
19
|
+
|
|
20
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const opt = (k) => { const i = args.indexOf(k); if (i === -1) return undefined; const v = args[i + 1]; args.splice(i, 2); return v; };
|
|
28
|
+
const BIN = resolve(opt("--bin") ?? die("--bin required"));
|
|
29
|
+
const CONTROL = resolve(opt("--control-bin") ?? die("--control-bin required (the full sdd-cli, for parity)"));
|
|
30
|
+
const REPOS = args.map((a) => resolve(a));
|
|
31
|
+
if (REPOS.length === 0) die("give at least one SDD repository");
|
|
32
|
+
|
|
33
|
+
function die(m) { process.stderr.write(`author-acceptance: ${m}\n`); process.exit(2); }
|
|
34
|
+
let FAIL = 0;
|
|
35
|
+
const fail = (m) => { FAIL++; process.stdout.write(` FAIL ${m}\n`); };
|
|
36
|
+
const ok = (m) => process.stdout.write(` ok ${m}\n`);
|
|
37
|
+
const exercised = new Set();
|
|
38
|
+
|
|
39
|
+
// ---- the maintain-only part of the set (M-ADR 0220 §4.3) -------------------------------------
|
|
40
|
+
const MAINTAIN_ONLY = [
|
|
41
|
+
/^method-baseline\.lock$/, /^\.sdd-repo-kind$/, // items 1–2
|
|
42
|
+
/^\.github\/workflows\/method-validation\.yml$/, /^\.gitlab-ci\.yml$/,
|
|
43
|
+
/^\.jenkins\/method-validation\.Jenkinsfile$/, /^\.dagger\/method-validation\//, // item 3
|
|
44
|
+
/^\.claude\/settings\.json$/, /^\.github\/hooks\//, // item 5
|
|
45
|
+
/^catalogues\//, /^catalogues-aggregated\//, /^catalogues(-aggregated)?-graph\.jsonl$/, // item 7
|
|
46
|
+
/^\.sdd\/bundle-cache\//,
|
|
47
|
+
];
|
|
48
|
+
function lockManaged(repo) {
|
|
49
|
+
const p = join(repo, "method-baseline.lock");
|
|
50
|
+
if (!existsSync(p)) return new Set();
|
|
51
|
+
const out = new Set();
|
|
52
|
+
for (const line of readFileSync(p, "utf8").split("\n")) {
|
|
53
|
+
const m = /^ {6}- "?(?:[0-9a-f]{64} {2})?([^"]+)"?$/.exec(line);
|
|
54
|
+
if (m) out.add(m[1]);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function changed(repo) {
|
|
59
|
+
const st = spawnSync("git", ["-C", repo, "status", "--porcelain", "--ignored", "--untracked-files=all"], { encoding: "utf8" });
|
|
60
|
+
return st.stdout.split("\n").filter(Boolean).map((l) => l.slice(3).split(" -> ").pop().replace(/^"|"$/g, ""));
|
|
61
|
+
}
|
|
62
|
+
const intoMaintainOnly = (repo, managed) => changed(repo).filter((p) => managed.has(p) || MAINTAIN_ONLY.some((rx) => rx.test(p)));
|
|
63
|
+
function snapshot(repo, roots) {
|
|
64
|
+
const h = createHash("sha256");
|
|
65
|
+
for (const p of changed(repo).sort()) {
|
|
66
|
+
h.update(p + "\0");
|
|
67
|
+
const abs = join(repo, p);
|
|
68
|
+
if (existsSync(abs) && statSync(abs).isFile()) {
|
|
69
|
+
let text = readFileSync(abs, "latin1");
|
|
70
|
+
for (const r of roots) text = text.split(r).join("<WS>");
|
|
71
|
+
h.update(text, "latin1");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return h.digest("hex");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- workspace (removed on every exit; AUTHOR_ACC_KEEP=1 keeps it) ----------------------------
|
|
78
|
+
const workspaces = [];
|
|
79
|
+
process.on("exit", () => {
|
|
80
|
+
if (process.env.AUTHOR_ACC_KEEP) return void process.stderr.write(`author-acceptance: kept ${workspaces.join(" ")}\n`);
|
|
81
|
+
for (const ws of workspaces) rmSync(ws, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
function fresh(repo) {
|
|
84
|
+
const ws = mkdtempSync(join(tmpdir(), "author-acc-"));
|
|
85
|
+
workspaces.push(ws);
|
|
86
|
+
const clone = join(ws, basename(repo));
|
|
87
|
+
execFileSync("git", ["clone", "-q", "--no-hardlinks", repo, clone]);
|
|
88
|
+
return { ws, clone };
|
|
89
|
+
}
|
|
90
|
+
const run = (cwd, verbArgs, bin) => spawnSync("node", [bin, ...verbArgs], { cwd, encoding: "utf8" });
|
|
91
|
+
|
|
92
|
+
// One verb on a fresh pair of clones: the author's run is scope-checked, the control's is its twin.
|
|
93
|
+
function check(label, repo, verbArgsFor, opts = {}) {
|
|
94
|
+
const a = fresh(repo);
|
|
95
|
+
const b = fresh(repo);
|
|
96
|
+
try {
|
|
97
|
+
checkPair(label, a, b, verbArgsFor, opts);
|
|
98
|
+
} finally {
|
|
99
|
+
if (!process.env.AUTHOR_ACC_KEEP) for (const w of [a.ws, b.ws]) { rmSync(w, { recursive: true, force: true }); workspaces.splice(workspaces.indexOf(w), 1); }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function checkPair(label, a, b, verbArgsFor, { cwdFor = (c) => c, inspect = (c) => c, allowNonZero = true } = {}) {
|
|
103
|
+
const r = run(cwdFor(a.clone, a.ws), verbArgsFor(a.clone, a.ws), BIN);
|
|
104
|
+
const crashed = /^sdd-author: /m.test(r.stderr ?? "");
|
|
105
|
+
if (crashed || (r.status !== 0 && !allowNonZero)) {
|
|
106
|
+
fail(`${label}: ${crashed ? "CRASHED" : `exit ${r.status}`} — ${(r.stderr || r.stdout).trim().split("\n").pop()}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const target = inspect(a.clone, a.ws);
|
|
110
|
+
const bad = intoMaintainOnly(target, lockManaged(a.clone));
|
|
111
|
+
if (bad.length) fail(`${label}: wrote into the maintain-only set: ${bad.slice(0, 5).join(", ")}`);
|
|
112
|
+
const rc = run(cwdFor(b.clone, b.ws), verbArgsFor(b.clone, b.ws), CONTROL);
|
|
113
|
+
const roots = [a.ws, b.ws];
|
|
114
|
+
const parity = snapshot(target, roots) === snapshot(inspect(b.clone, b.ws), roots) && rc.status === r.status;
|
|
115
|
+
if (!parity) fail(`${label}: PARITY — sdd-cli left a different tree or exited ${rc.status} where sdd-author exited ${r.status}`);
|
|
116
|
+
if (!bad.length && parity) ok(`${label} (exit ${r.status}; wrote ${changed(target).length} path(s)): none in the maintain-only set, = sdd-cli`);
|
|
117
|
+
exercised.add(label.split(" @")[0]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- control: the detector must see a write into the maintain-only set -----------------------
|
|
121
|
+
{
|
|
122
|
+
const { clone } = fresh(REPOS[0]);
|
|
123
|
+
mkdirSync(join(clone, "catalogues"), { recursive: true });
|
|
124
|
+
writeFileSync(join(clone, "catalogues", "CONTROL.csv"), "x\n");
|
|
125
|
+
writeFileSync(join(clone, "method-baseline.lock"), readFileSync(join(clone, "method-baseline.lock"), "utf8") + "# control\n");
|
|
126
|
+
const seen = intoMaintainOnly(clone, lockManaged(clone));
|
|
127
|
+
if (seen.includes("catalogues/CONTROL.csv") && seen.includes("method-baseline.lock")) ok("CONTROL: writes into the maintain-only set are seen");
|
|
128
|
+
else fail(`CONTROL: a write into the maintain-only set was NOT seen (${seen.join(", ")}) — every scope PASS below would be vacuous`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const repo of REPOS) {
|
|
132
|
+
const name = basename(repo);
|
|
133
|
+
process.stdout.write(`=== ${name} ===\n`);
|
|
134
|
+
const kind = existsSync(join(repo, ".sdd-repo-kind")) ? readFileSync(join(repo, ".sdd-repo-kind"), "utf8").trim() : "";
|
|
135
|
+
check(`catalogue check @${name}`, repo, () => ["catalogue", "check", "--sdd-path", "."]);
|
|
136
|
+
check(`generate-briefing @${name}`, repo, () => ["generate-briefing", "--target-dir", "."]);
|
|
137
|
+
check(`scaffold domain @${name}`, repo, () => ["scaffold", "domain", "author-acc-probe", "--target-repo", "."]);
|
|
138
|
+
check(`plan @${name}`, repo, () => ["plan", "--pilot", "--service", "acc-probe", "--capability", "acc-probe", "--domain", "acc-probe", "--workspace-root", "."]);
|
|
139
|
+
check(`contracts attest @${name}`, repo, () => ["contracts", "attest", "--target-repo", ".", "--refresh"]);
|
|
140
|
+
check(`conformance @${name}`, repo, (_c, ws) => ["conformance", "--target-repo", ".", "--emit-finding", join(ws, "findings")]);
|
|
141
|
+
check(`api-surface @${name}`, repo, (_c, ws) => ["api-surface", "--source", ".", "--out", join(ws, "surface.yaml")]);
|
|
142
|
+
const screens = join(repo, "screens");
|
|
143
|
+
if (existsSync(screens)) {
|
|
144
|
+
const spec = readdirSync(screens).find((f) => f.endsWith(".md"));
|
|
145
|
+
if (spec) check(`render screen @${name}`, repo, (_c, ws) => ["render", "screen", join("screens", spec), "--out", join(ws, "screen.svg")]);
|
|
146
|
+
}
|
|
147
|
+
// context pull: a scratch code repository beside the clone, named for a repository this SDD catalogues.
|
|
148
|
+
const repoCsv = join(repo, "catalogues", "repositories.csv");
|
|
149
|
+
if (existsSync(repoCsv) && existsSync(join(repo, "orchestration/templates/briefing/code-repo/agent-instructions.md.hbs"))) {
|
|
150
|
+
const id = readFileSync(repoCsv, "utf8").split("\n")[1]?.split(",")[0];
|
|
151
|
+
if (id) {
|
|
152
|
+
const codeOf = (ws) => {
|
|
153
|
+
const c = join(ws, id);
|
|
154
|
+
if (!existsSync(c)) { mkdirSync(c, { recursive: true }); execFileSync("git", ["-C", c, "init", "-q"]); }
|
|
155
|
+
return c;
|
|
156
|
+
};
|
|
157
|
+
// Scope is checked on the SDD clone (the code repository is not an SDD); parity on the code repository.
|
|
158
|
+
check(`context pull @${name}`, repo, (c) => ["context", "pull", "--sdd", c], { cwdFor: (_c, ws) => codeOf(ws), inspect: (c, ws) => { const bad = intoMaintainOnly(c, lockManaged(c)); if (bad.length) fail(`context pull @${name}: wrote into the SDD's maintain-only set: ${bad.join(", ")}`); return codeOf(ws); } });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
void kind;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const SURFACE = ["catalogue check", "generate-briefing", "scaffold domain", "plan", "contracts attest", "conformance", "api-surface", "render screen", "context pull",
|
|
165
|
+
"snapshot fold", "snapshot land", "snapshot verify", "snapshot diff", "snapshot targets", "snapshot advance"];
|
|
166
|
+
const missing = SURFACE.filter((v) => !exercised.has(v));
|
|
167
|
+
process.stdout.write(`COVERAGE exercised: ${[...exercised].join(", ")}\n`);
|
|
168
|
+
if (missing.length) process.stdout.write(`COVERAGE NOT exercised here (needs inputs this run lacks, e.g. a snapshot store): ${missing.join(", ")}\n`);
|
|
169
|
+
process.stdout.write(FAIL === 0 ? "ACCEPTANCE PASS — no author write reached the maintain-only set\n" : `ACCEPTANCE FAIL (${FAIL})\n`);
|
|
170
|
+
process.exit(FAIL === 0 ? 0 : 1);
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Assemble a publishable persona package — @sdd-method/verify-cli (M-ADR 0212 §4.3b) or
|
|
2
|
+
// @sdd-method/maintain-cli (M-ADR 0218) or @sdd-method/author-cli (M-ADR 0220). Usage: build-persona-package.mjs [verify|maintain|author]
|
|
3
|
+
// (default verify). One script, so both packages hold the same boundary discipline.
|
|
4
|
+
//
|
|
5
|
+
// ONE SOURCE, and this time a genuinely smaller ARTEFACT. The conformance
|
|
6
|
+
// package copied the whole of dist/ under a second manifest, so its "excluded"
|
|
7
|
+
// verbs were one `node dist/cli.js …` away. This script ships only the import
|
|
8
|
+
// CLOSURE of the persona's entrypoint (dist/verify-bin.js, dist/maintain-bin.js): an excluded verb's code is not in the package
|
|
9
|
+
// to be reached by any path.
|
|
10
|
+
//
|
|
11
|
+
// Fails closed on four conditions, each of which would otherwise let the
|
|
12
|
+
// boundary pass while not holding:
|
|
13
|
+
// 1. a relative import in the closure that does not resolve to a file;
|
|
14
|
+
// 2. a bare import that is neither a Node built-in nor a declared dependency;
|
|
15
|
+
// 3. a CANARY — a known authoring module — present in the closure;
|
|
16
|
+
// 4. a canary that no longer EXISTS in dist. A canary that was renamed or
|
|
17
|
+
// deleted cannot be absent from anything, so the check above would pass
|
|
18
|
+
// vacuously; this makes it fail instead, and asks for the list to follow.
|
|
19
|
+
//
|
|
20
|
+
// The generated directory is gitignored and safe to delete.
|
|
21
|
+
|
|
22
|
+
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
24
|
+
import { builtinModules } from "node:module";
|
|
25
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
29
|
+
// SDD_CLI_DIST: check a build made elsewhere. `dist/` is what a linked global `sdd-cli`
|
|
30
|
+
// runs, so building it to run this check silently put unreleased code in front of every
|
|
31
|
+
// session on the machine (2026-10-01, about nine hours). `npm run verify:boundary` builds
|
|
32
|
+
// into build/check-dist and checks that instead; only a release should write dist/.
|
|
33
|
+
const DIST = process.env.SDD_CLI_DIST ? resolve(process.env.SDD_CLI_DIST) : join(ROOT, "dist");
|
|
34
|
+
const PERSONA = process.argv[2] ?? "verify";
|
|
35
|
+
// Modules that must never reach a package. Each list names modules that EXIST, or the check below
|
|
36
|
+
// fails rather than passing vacuously. ADMIN: sdd-cli's alone (M-ADR 0220 §4.2). AUTHOR_ONLY: the
|
|
37
|
+
// author's own verbs, canaries for verify and maintain. MAINTAIN_WRITERS: the sync engine and the
|
|
38
|
+
// model and pack writers, canaries for verify and author. The catalogue BUILDER is not one for the
|
|
39
|
+
// author: `catalogue check` builds into temporary space through it (0220 §4.3 names the writers).
|
|
40
|
+
const ADMIN_CANARIES = [
|
|
41
|
+
"cli.js",
|
|
42
|
+
"index.js",
|
|
43
|
+
"verbs/init.js",
|
|
44
|
+
"verbs/metamodel.js",
|
|
45
|
+
"verbs/catalogue.js",
|
|
46
|
+
"lib/catalogue/migration-map.js",
|
|
47
|
+
];
|
|
48
|
+
const AUTHOR_ONLY_MODULES = [
|
|
49
|
+
"verbs/conformance.js",
|
|
50
|
+
"verbs/scaffold.js",
|
|
51
|
+
"verbs/plan.js",
|
|
52
|
+
"verbs/contracts.js",
|
|
53
|
+
"verbs/api-surface.js",
|
|
54
|
+
"verbs/snapshot.js",
|
|
55
|
+
"lib/snapshot/land.js",
|
|
56
|
+
];
|
|
57
|
+
const MAINTAIN_WRITERS = [
|
|
58
|
+
"verbs/sync.js",
|
|
59
|
+
"verbs/catalogue-maintain.js",
|
|
60
|
+
"verbs/pack.js",
|
|
61
|
+
"verbs/pack-build.js",
|
|
62
|
+
"lib/sync/index.js",
|
|
63
|
+
"lib/catalogue/aggregate.js",
|
|
64
|
+
"lib/catalogue/graph-run.js",
|
|
65
|
+
"lib/snapshot/catalogue-diff.js",
|
|
66
|
+
"lib/pack/builder.js",
|
|
67
|
+
];
|
|
68
|
+
const AUTHOR_CANARIES = [...ADMIN_CANARIES, ...AUTHOR_ONLY_MODULES];
|
|
69
|
+
const PERSONAS = {
|
|
70
|
+
verify: {
|
|
71
|
+
entry: "verify-bin.js",
|
|
72
|
+
out: "verify-cli",
|
|
73
|
+
versionFile: "verify-version.json",
|
|
74
|
+
versionEnv: "VERIFY_VERSION",
|
|
75
|
+
name: "@sdd-method/verify-cli",
|
|
76
|
+
bin: "sdd-verify",
|
|
77
|
+
canaries: [
|
|
78
|
+
...AUTHOR_CANARIES,
|
|
79
|
+
"verbs/sync.js",
|
|
80
|
+
"verbs/pack.js",
|
|
81
|
+
"verbs/render.js",
|
|
82
|
+
"lib/sync/index.js",
|
|
83
|
+
"lib/catalogue/build.js",
|
|
84
|
+
"lib/catalogue/aggregate.js",
|
|
85
|
+
"lib/catalogue/graph-run.js",
|
|
86
|
+
"lib/pack/builder.js",
|
|
87
|
+
],
|
|
88
|
+
description:
|
|
89
|
+
"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.",
|
|
90
|
+
readme:
|
|
91
|
+
`# @sdd-method/verify-cli\n\n` +
|
|
92
|
+
`\`sdd-verify\` checks an SDD repository and **cannot change it**.\n\n` +
|
|
93
|
+
`Verbs: \`validate\`, \`list\`, \`pack validate\`, \`catalogue validate\` (incl. \`--coverage\`), ` +
|
|
94
|
+
`\`catalogue impact\`, \`catalogue baseline-check\`, \`catalogue version\`.\n\n` +
|
|
95
|
+
`The package contains only the code those verbs reach. Authoring and maintenance verbs — ` +
|
|
96
|
+
`\`sync\`, \`catalogue build\`, \`init\`, \`scaffold\` and the rest — are not present in it, ` +
|
|
97
|
+
`and are in \`@sdd-method/sdd-cli\`. Invoking one fails with exit 1.\n`,
|
|
98
|
+
},
|
|
99
|
+
author: {
|
|
100
|
+
entry: "author-bin.js",
|
|
101
|
+
out: "author-cli",
|
|
102
|
+
versionFile: "author-version.json",
|
|
103
|
+
versionEnv: "AUTHOR_VERSION",
|
|
104
|
+
name: "@sdd-method/author-cli",
|
|
105
|
+
bin: "sdd-author",
|
|
106
|
+
canaries: [...ADMIN_CANARIES, ...MAINTAIN_WRITERS],
|
|
107
|
+
description:
|
|
108
|
+
"Re-author one SDD's content — scaffold, plan, contracts, conformance, api-surface, snapshots, briefings, catalogue check — plus every read-only verify verb. Ships only the code those verbs reach; it cannot sync the baseline or rebuild the catalogues. Maintenance is in @sdd-method/maintain-cli, administration in @sdd-method/sdd-cli.",
|
|
109
|
+
readme:
|
|
110
|
+
`# @sdd-method/author-cli\n\n` +
|
|
111
|
+
`\`sdd-author\` is for the people and agents who own one SDD's content. It re-authors that content and **cannot move the baseline or rebuild the model** (M-ADR 0220).\n\n` +
|
|
112
|
+
`Verbs: everything in \`@sdd-method/verify-cli\`, plus \`scaffold domain\`, \`plan\`, \`contracts attest\`, \`conformance\`, \`api-surface\`, ` +
|
|
113
|
+
`the \`snapshot\` verbs, \`render screen\`, \`generate-briefing\`, \`context pull\` and \`catalogue check\`.\n\n` +
|
|
114
|
+
`\`catalogue check\` reports whether the committed catalogues are what the committed specifications build to, and writes nothing. ` +
|
|
115
|
+
`Rebuilding them (\`catalogue build\`), \`sync\` and \`pack build\` are in \`@sdd-method/maintain-cli\`; \`init\` and the administrator's verbs are in \`@sdd-method/sdd-cli\`. ` +
|
|
116
|
+
`They are not present in this package, and invoking one fails with exit 1.\n`,
|
|
117
|
+
},
|
|
118
|
+
maintain: {
|
|
119
|
+
entry: "maintain-bin.js",
|
|
120
|
+
out: "maintain-cli",
|
|
121
|
+
versionFile: "maintain-version.json",
|
|
122
|
+
versionEnv: "MAINTAIN_VERSION",
|
|
123
|
+
name: "@sdd-method/maintain-cli",
|
|
124
|
+
bin: "sdd-maintain",
|
|
125
|
+
canaries: AUTHOR_CANARIES,
|
|
126
|
+
description:
|
|
127
|
+
"Bring an SDD repository current and rebuild its derived model — sync, briefing, catalogue build and aggregate, pack build, context pull — plus every read-only verify verb. Ships only the code those verbs reach; it cannot author specification content. Authoring is in @sdd-method/sdd-cli.",
|
|
128
|
+
readme:
|
|
129
|
+
`# @sdd-method/maintain-cli\n\n` +
|
|
130
|
+
`\`sdd-maintain\` brings an SDD repository current and rebuilds its derived model, and **cannot author specification content** (M-ADR 0218).\n\n` +
|
|
131
|
+
`Verbs: everything in \`@sdd-method/verify-cli\`, plus \`sync\`, \`generate-briefing\`, \`context pull\`, ` +
|
|
132
|
+
`\`catalogue build\`, \`catalogue aggregate\`, \`catalogue graph\`, \`catalogue diff\`, \`pack build\`, \`render screen\`.\n\n` +
|
|
133
|
+
`Each writes only derived or method-managed content, and two runs on the same commit write the same bytes. ` +
|
|
134
|
+
`Authoring verbs — \`init\`, \`scaffold\`, \`plan\`, \`conformance\` and the rest — are not present in it, ` +
|
|
135
|
+
`and are in \`@sdd-method/sdd-cli\`. Invoking one fails with exit 1.\n`,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
const P = PERSONAS[PERSONA];
|
|
139
|
+
if (!P) {
|
|
140
|
+
process.stderr.write(`build-persona-package: FAIL — unknown persona '${PERSONA}' (verify | maintain | author)\n`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const OUT = join(ROOT, "build", P.out);
|
|
144
|
+
const ENTRY = join(DIST, P.entry);
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
const die = (msg) => {
|
|
148
|
+
process.stderr.write(`build-verify-package: FAIL — ${msg}\n`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (!existsSync(ENTRY)) die(`${relative(ROOT, ENTRY)} missing — run \`npm run build\` first`);
|
|
153
|
+
|
|
154
|
+
// ---- 1. closure over imports ----------------------------------------------
|
|
155
|
+
// Line-based over tsc output, skipping comment lines. An earlier regex over the
|
|
156
|
+
// whole text read `// from "changed" produces…` in a comment as an import of a
|
|
157
|
+
// package called `changed` — prose parsed as code, the same failure a grep over
|
|
158
|
+
// workflow comments made the same day. Fail-closed caught it; this stops it.
|
|
159
|
+
const STATIC = /^\s*(?:import|export|\})[^\n]*?\bfrom\s*["']([^"']+)["']/;
|
|
160
|
+
const SIDE = /^\s*import\s*["']([^"']+)["']/;
|
|
161
|
+
const DYNAMIC = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
162
|
+
function specifiers(src) {
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const line of src.split("\n")) {
|
|
165
|
+
const t = line.trimStart();
|
|
166
|
+
if (t.startsWith("//") || t.startsWith("*") || t.startsWith("/*")) continue;
|
|
167
|
+
const m = line.match(STATIC) ?? line.match(SIDE);
|
|
168
|
+
if (m) out.push(m[1]);
|
|
169
|
+
for (const d of line.matchAll(DYNAMIC)) out.push(d[1]);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
const closure = new Set();
|
|
174
|
+
const bare = new Set();
|
|
175
|
+
const stack = [ENTRY];
|
|
176
|
+
while (stack.length) {
|
|
177
|
+
const f = stack.pop();
|
|
178
|
+
if (closure.has(f)) continue;
|
|
179
|
+
closure.add(f);
|
|
180
|
+
for (const spec of specifiers(readFileSync(f, "utf8"))) {
|
|
181
|
+
if (spec.startsWith(".")) {
|
|
182
|
+
const target = resolve(dirname(f), spec);
|
|
183
|
+
if (!existsSync(target)) die(`${relative(DIST, f)} imports '${spec}', which does not resolve`);
|
|
184
|
+
stack.push(target);
|
|
185
|
+
} else {
|
|
186
|
+
bare.add(spec);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---- 2. dependencies = only what the closure imports -----------------------
|
|
192
|
+
const main = JSON.parse(await readFile(join(ROOT, "package.json"), "utf8"));
|
|
193
|
+
const pkgName = (spec) =>
|
|
194
|
+
spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
|
|
195
|
+
const builtins = new Set(builtinModules);
|
|
196
|
+
const deps = {};
|
|
197
|
+
for (const spec of bare) {
|
|
198
|
+
if (spec.startsWith("node:") || builtins.has(spec) || builtins.has(spec.split("/")[0])) continue;
|
|
199
|
+
const name = pkgName(spec);
|
|
200
|
+
const range = main.dependencies?.[name];
|
|
201
|
+
if (!range) die(`closure imports '${spec}', which is not a declared dependency of sdd-cli`);
|
|
202
|
+
deps[name] = range;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---- 3 + 4. canaries -------------------------------------------------------
|
|
206
|
+
for (const c of P.canaries) {
|
|
207
|
+
const abs = join(DIST, c);
|
|
208
|
+
if (!existsSync(abs)) die(`canary '${c}' no longer exists in dist — the check would pass vacuously; update CANARIES`);
|
|
209
|
+
if (closure.has(abs)) die(`excluded module '${c}' is reachable from ${P.entry} — the ${PERSONA} boundary does not hold`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- bundled shell scripts -------------------------------------------------
|
|
213
|
+
// Some validators are shell scripts SHIPPED WITH the package (dispatch.ts:
|
|
214
|
+
// `kind: "bundled"`), resolved by lib/scripts.ts to <package-root>/scripts/.
|
|
215
|
+
// They are launched by path, not imported, so the closure above cannot see
|
|
216
|
+
// them. verify-cli 0.1.0 shipped without them and `app-docs` and
|
|
217
|
+
// `integration-docs` failed with exit 127 on every app and integration repo —
|
|
218
|
+
// found by a parity check against sdd-cli, not by the no-write acceptance,
|
|
219
|
+
// because a validator that cannot find its script writes nothing.
|
|
220
|
+
const bundled = new Set();
|
|
221
|
+
for (const f of closure) {
|
|
222
|
+
for (const m of readFileSync(f, "utf8").matchAll(/kind:\s*"bundled",\s*script:\s*"([^"]+)"/g)) {
|
|
223
|
+
const p = join(ROOT, "scripts", m[1]);
|
|
224
|
+
if (!existsSync(p)) die(`${relative(DIST, f)} declares bundled script '${m[1]}', which does not exist in scripts/`);
|
|
225
|
+
bundled.add(p);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- assets: yaml/json data beside a closure module ------------------------
|
|
230
|
+
// Data a module reads by path at runtime is invisible to the import closure. Beside the module, and
|
|
231
|
+
// in DATA-ONLY subdirectories under it (no .js inside): maintain-cli's first build missed
|
|
232
|
+
// lib/catalogue/archetype/maps/*.yaml, so `render screen` crashed with ENOENT where sdd-cli rendered.
|
|
233
|
+
const assets = new Set();
|
|
234
|
+
const dataOnlyFiles = (dir) => {
|
|
235
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
236
|
+
if (entries.some((e) => e.isFile() && e.name.endsWith(".js"))) return [];
|
|
237
|
+
return entries.flatMap((e) => {
|
|
238
|
+
const p = join(dir, e.name);
|
|
239
|
+
if (e.isDirectory()) return dataOnlyFiles(p);
|
|
240
|
+
return /\.(ya?ml|json)$/.test(e.name) ? [p] : [];
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
for (const f of closure) {
|
|
244
|
+
const dir = dirname(f);
|
|
245
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
246
|
+
const p = join(dir, e.name);
|
|
247
|
+
if (e.isFile() && /\.(ya?ml|json)$/.test(e.name)) assets.add(p);
|
|
248
|
+
else if (e.isDirectory()) for (const d of dataOnlyFiles(p)) assets.add(d);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---- assemble --------------------------------------------------------------
|
|
253
|
+
// The version is COMMITTED (scripts/<persona>-version.json), not defaulted. Defaulting to "0.1.0" let a build
|
|
254
|
+
// of new source carry an old version silently; verify-cli then fell two sdd-cli releases behind unnoticed.
|
|
255
|
+
// Bump the file in every sdd-cli release commit (minor with the CLI's minor; patch for fixes).
|
|
256
|
+
const versionFile = JSON.parse(readFileSync(join(ROOT, "scripts", P.versionFile), "utf8"));
|
|
257
|
+
const version = versionFile.version;
|
|
258
|
+
if (!/^\d+\.\d+\.\d+$/.test(version ?? "")) die(`scripts/${P.versionFile} has no semver \`version\``);
|
|
259
|
+
// The engine this package is built from. Every engine-meaning check reads it (getSddCliVersion):
|
|
260
|
+
// a bundle's floor and catalogue stamps. A stale built_against in the version file
|
|
261
|
+
// would ship a package claiming an engine it is not, so the build refuses it.
|
|
262
|
+
if (versionFile.built_against_sdd_cli !== main.version)
|
|
263
|
+
die(`scripts/${P.versionFile} says built_against_sdd_cli ${versionFile.built_against_sdd_cli}, but this sdd-cli is ${main.version}; update the file`);
|
|
264
|
+
if (process.env[P.versionEnv] && process.env[P.versionEnv] !== version)
|
|
265
|
+
die(`${P.versionEnv}=${process.env[P.versionEnv]} disagrees with scripts/${P.versionFile} (${version}); edit the file`);
|
|
266
|
+
// Empty the directory, never remove it: a shell sitting in build/verify-cli
|
|
267
|
+
// (to run `npm publish`) keeps a handle on the directory itself, and deleting
|
|
268
|
+
// it leaves that shell in a path that no longer exists — npm then dies with
|
|
269
|
+
// ENOENT uv_cwd. That happened, twice, between a rebuild and a publish.
|
|
270
|
+
await mkdir(OUT, { recursive: true });
|
|
271
|
+
for (const entry of readdirSync(OUT)) await rm(join(OUT, entry), { recursive: true, force: true });
|
|
272
|
+
for (const f of [...closure, ...assets]) {
|
|
273
|
+
const dest = join(OUT, "dist", relative(DIST, f));
|
|
274
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
275
|
+
await cp(f, dest);
|
|
276
|
+
}
|
|
277
|
+
for (const p of bundled) {
|
|
278
|
+
await mkdir(join(OUT, "scripts"), { recursive: true });
|
|
279
|
+
await cp(p, join(OUT, "scripts", relative(join(ROOT, "scripts"), p)));
|
|
280
|
+
}
|
|
281
|
+
for (const f of ["LICENSE", "NOTICE"]) await cp(join(ROOT, f), join(OUT, f));
|
|
282
|
+
|
|
283
|
+
if (main.publishConfig?.access !== "public") die(`sdd-cli's package.json has no publishConfig.access=public — ${P.name} would publish private`);
|
|
284
|
+
|
|
285
|
+
const pkg = {
|
|
286
|
+
name: P.name,
|
|
287
|
+
version,
|
|
288
|
+
sddCliVersion: main.version,
|
|
289
|
+
description: P.description,
|
|
290
|
+
license: main.license,
|
|
291
|
+
type: main.type,
|
|
292
|
+
bin: { [P.bin]: `dist/${P.entry}` },
|
|
293
|
+
engines: main.engines,
|
|
294
|
+
dependencies: deps,
|
|
295
|
+
repository: main.repository,
|
|
296
|
+
// Inherited, not assumed: a scoped package publishes PRIVATE by default, and
|
|
297
|
+
// the first publish of verify-cli failed with E402 because this was missing.
|
|
298
|
+
publishConfig: main.publishConfig,
|
|
299
|
+
files: ["dist", "scripts", "README.md", "LICENSE", "NOTICE"],
|
|
300
|
+
};
|
|
301
|
+
await writeFile(join(OUT, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
|
|
302
|
+
await writeFile(join(OUT, "README.md"), P.readme);
|
|
303
|
+
|
|
304
|
+
const kb = (fs) => Math.round(fs.reduce((n, f) => n + statSync(f).size, 0) / 1024);
|
|
305
|
+
process.stdout.write(
|
|
306
|
+
`assembled ${pkg.name}@${version} at build/${P.out}\n` +
|
|
307
|
+
` modules ${closure.size} (${kb([...closure])} kB) · assets ${assets.size} · bundled scripts ${bundled.size} · deps ${Object.keys(deps).join(", ")}\n` +
|
|
308
|
+
` canaries absent: ${P.canaries.length}/${P.canaries.length}\n`,
|
|
309
|
+
);
|
|
@@ -37,6 +37,8 @@ for await (const srcFile of walk(SRC)) {
|
|
|
37
37
|
}
|
|
38
38
|
// The verify-cli version the CI workflow pins (getVerifyCliVersion). Copied, not read live from
|
|
39
39
|
// scripts/, so it moves with dist/ and never ahead of it.
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
for (const persona of ["verify", "maintain"]) {
|
|
41
|
+
await copyFile(join(__dirname, `${persona}-version.json`), join(DEST, `${persona}-version.json`));
|
|
42
|
+
copied++;
|
|
43
|
+
}
|
|
42
44
|
console.log(`copy-yaml-assets: ${copied} file(s) copied to dist/`);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dagger-parity.sh — per-step parity between the Method Validation Dagger module and the same plan
|
|
3
|
+
# (sdd-verify validate --list) run directly with the same CLIs (sdm-dd6.3; Dagger ruling sdm-s48).
|
|
4
|
+
# Run it on a release candidate: build the persona packages (npm run verify:boundary), then
|
|
5
|
+
# scripts/dagger-parity.sh <sdd-repo>... It packs them unless PKGS names a directory already
|
|
6
|
+
# holding the tarballs. Needs the dagger CLI and a container engine (M-ADR 0219 §4.5).
|
|
7
|
+
set -u
|
|
8
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
if [ -n "${SCRATCH:-}" ]; then SP=$SCRATCH; else SP=$(mktemp -d); trap 'rm -rf "$SP"' EXIT; fi
|
|
10
|
+
CLI=$ROOT/build
|
|
11
|
+
PKGS=${PKGS:-$SP/pkgs}
|
|
12
|
+
if [ ! -d "$PKGS" ]; then
|
|
13
|
+
mkdir -p "$PKGS"
|
|
14
|
+
(cd "$CLI/verify-cli" && npm pack --silent --pack-destination "$PKGS" >/dev/null)
|
|
15
|
+
(cd "$CLI/maintain-cli" && npm pack --silent --pack-destination "$PKGS" >/dev/null)
|
|
16
|
+
fi
|
|
17
|
+
VV=$(node -p "require('$CLI/verify-cli/package.json').version"); MV=$(node -p "require('$CLI/maintain-cli/package.json').version")
|
|
18
|
+
# The module exactly as sync renders it (M-ADR 0219 §6), from the built maintain-cli, with the
|
|
19
|
+
# candidate versions as its pins.
|
|
20
|
+
MOD=$SP/module
|
|
21
|
+
node --input-type=module -e "
|
|
22
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
24
|
+
const { renderModuleFiles } = await import('$CLI/maintain-cli/dist/lib/ci-binding/module.js');
|
|
25
|
+
for (const [rel, c] of renderModuleFiles({ verify: '$VV', maintain: '$MV' })) {
|
|
26
|
+
const p = join('$MOD', rel); mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, c);
|
|
27
|
+
}"
|
|
28
|
+
VERIFY="node $CLI/verify-cli/dist/verify-bin.js"
|
|
29
|
+
MAINTAIN="node $CLI/maintain-cli/dist/maintain-bin.js"
|
|
30
|
+
OUT=$SP/dagger-spike-out; mkdir -p "$OUT"
|
|
31
|
+
FAIL=0
|
|
32
|
+
for repo in "$@"; do
|
|
33
|
+
name=$(basename "$repo")
|
|
34
|
+
W=$(mktemp -d -p "$SP")
|
|
35
|
+
git clone -q --no-hardlinks "$repo" "$W/$name"
|
|
36
|
+
sha=$(git -C "$W/$name" rev-parse --short=8 HEAD)
|
|
37
|
+
echo "=== $name @ $sha"
|
|
38
|
+
# 1. Dagger, on Docker
|
|
39
|
+
start=$(date +%s)
|
|
40
|
+
( cd "$MOD" && DAGGER_NO_NAG=1 timeout 1500 dagger -q call method-validation \
|
|
41
|
+
--source="$W/$name" --verify-version=$VV --maintain-version=$MV \
|
|
42
|
+
--verify-tarball="$PKGS/sdd-method-verify-cli-$VV.tgz" --maintain-tarball="$PKGS/sdd-method-maintain-cli-$MV.tgz" ) > "$OUT/$name.dagger.log" 2>&1
|
|
43
|
+
drc=$?; secs=$(( $(date +%s) - start ))
|
|
44
|
+
sed "s/\x1b\[[0-9;]*m//g" "$OUT/$name.dagger.log" | grep -oE "^ *(PASS|FAIL|ADVISORY) +exit [0-9]+ [a-z0-9-]+" | sed "s/^ *//" | awk '{print $4, $3}' | sort > "$OUT/$name.dagger.steps"
|
|
45
|
+
# 2. The same plan, directly on the host (a second clone, so neither run sees the other)
|
|
46
|
+
git clone -q --no-hardlinks "$repo" "$W/direct"
|
|
47
|
+
( cd "$W/direct" && $VERIFY validate --list ) > "$OUT/$name.plan.json"
|
|
48
|
+
python3 - "$OUT/$name.plan.json" > "$OUT/$name.cmds" <<'PY'
|
|
49
|
+
import json, sys
|
|
50
|
+
for s in json.load(open(sys.argv[1]))["steps"]:
|
|
51
|
+
print(s["id"], s["tool"], " ".join(s["args"]))
|
|
52
|
+
PY
|
|
53
|
+
: > "$OUT/$name.direct.steps"
|
|
54
|
+
while read -r id tool args; do
|
|
55
|
+
case "$tool" in
|
|
56
|
+
sdd-verify) cmd="$VERIFY $args" ;;
|
|
57
|
+
sdd-maintain) cmd="$MAINTAIN $args" ;;
|
|
58
|
+
bash) cmd="bash $args" ;;
|
|
59
|
+
esac
|
|
60
|
+
( cd "$W/direct" && eval "$cmd" ) > "$OUT/$name.direct.$id.log" 2>&1
|
|
61
|
+
echo "$id $?" >> "$OUT/$name.direct.steps"
|
|
62
|
+
done < "$OUT/$name.cmds"
|
|
63
|
+
sort -o "$OUT/$name.direct.steps" "$OUT/$name.direct.steps"
|
|
64
|
+
n=$(wc -l < "$OUT/$name.direct.steps")
|
|
65
|
+
if [ "$(wc -l < "$OUT/$name.dagger.steps")" -ne "$n" ]; then
|
|
66
|
+
echo " FAIL dagger reported $(wc -l < "$OUT/$name.dagger.steps") steps, the plan has $n (dagger exit $drc; see $name.dagger.log)"; FAIL=$((FAIL+1))
|
|
67
|
+
elif diff -q "$OUT/$name.dagger.steps" "$OUT/$name.direct.steps" >/dev/null; then
|
|
68
|
+
echo " PARITY $n/$n steps, identical exit codes (dagger exit $drc, ${secs}s); $(awk '$2!=0' "$OUT/$name.direct.steps" | wc -l) step(s) non-zero in both"
|
|
69
|
+
else
|
|
70
|
+
echo " FAIL step exit codes differ:"; diff "$OUT/$name.dagger.steps" "$OUT/$name.direct.steps" | sed 's/^/ /'; FAIL=$((FAIL+1))
|
|
71
|
+
fi
|
|
72
|
+
rm -rf "$W" # the logs are in $OUT; two clones per repo add up across an estate
|
|
73
|
+
done
|
|
74
|
+
[ $FAIL -eq 0 ] && echo "SPIKE PARITY: PASS" || echo "SPIKE PARITY: FAIL ($FAIL)"
|