@sdd-method/sdd-cli 1.29.1 → 1.30.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 (97) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +2 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/catalogue/graph-run.d.ts +1 -28
  5. package/dist/lib/catalogue/graph-run.d.ts.map +1 -1
  6. package/dist/lib/catalogue/graph-run.js +3 -57
  7. package/dist/lib/catalogue/graph-run.js.map +1 -1
  8. package/dist/lib/catalogue/impact-run.d.ts +1 -1
  9. package/dist/lib/catalogue/impact-run.d.ts.map +1 -1
  10. package/dist/lib/catalogue/impact-run.js +1 -1
  11. package/dist/lib/catalogue/impact-run.js.map +1 -1
  12. package/dist/lib/catalogue/relationship-semantics.d.ts +29 -0
  13. package/dist/lib/catalogue/relationship-semantics.d.ts.map +1 -0
  14. package/dist/lib/catalogue/relationship-semantics.js +66 -0
  15. package/dist/lib/catalogue/relationship-semantics.js.map +1 -0
  16. package/dist/lib/pack/index.d.ts +1 -9
  17. package/dist/lib/pack/index.d.ts.map +1 -1
  18. package/dist/lib/pack/index.js +2 -53
  19. package/dist/lib/pack/index.js.map +1 -1
  20. package/dist/lib/pack/log.d.ts +6 -0
  21. package/dist/lib/pack/log.d.ts.map +1 -0
  22. package/dist/lib/pack/log.js +18 -0
  23. package/dist/lib/pack/log.js.map +1 -0
  24. package/dist/lib/pack/validate.d.ts +10 -0
  25. package/dist/lib/pack/validate.d.ts.map +1 -0
  26. package/dist/lib/pack/validate.js +49 -0
  27. package/dist/lib/pack/validate.js.map +1 -0
  28. package/dist/lib/snapshot/advance.d.ts +39 -0
  29. package/dist/lib/snapshot/advance.d.ts.map +1 -0
  30. package/dist/lib/snapshot/advance.js +253 -0
  31. package/dist/lib/snapshot/advance.js.map +1 -0
  32. package/dist/lib/snapshot/catalogue-diff.d.ts +21 -0
  33. package/dist/lib/snapshot/catalogue-diff.d.ts.map +1 -0
  34. package/dist/lib/snapshot/catalogue-diff.js +133 -0
  35. package/dist/lib/snapshot/catalogue-diff.js.map +1 -0
  36. package/dist/lib/snapshot/diff.d.ts +73 -0
  37. package/dist/lib/snapshot/diff.d.ts.map +1 -0
  38. package/dist/lib/snapshot/diff.js +177 -0
  39. package/dist/lib/snapshot/diff.js.map +1 -0
  40. package/dist/lib/snapshot/fold.d.ts +14 -0
  41. package/dist/lib/snapshot/fold.d.ts.map +1 -0
  42. package/dist/lib/snapshot/fold.js +106 -0
  43. package/dist/lib/snapshot/fold.js.map +1 -0
  44. package/dist/lib/snapshot/land.d.ts +21 -0
  45. package/dist/lib/snapshot/land.d.ts.map +1 -0
  46. package/dist/lib/snapshot/land.js +160 -0
  47. package/dist/lib/snapshot/land.js.map +1 -0
  48. package/dist/lib/snapshot/manifest.d.ts +59 -0
  49. package/dist/lib/snapshot/manifest.d.ts.map +1 -0
  50. package/dist/lib/snapshot/manifest.js +152 -0
  51. package/dist/lib/snapshot/manifest.js.map +1 -0
  52. package/dist/lib/snapshot/targets.d.ts +26 -0
  53. package/dist/lib/snapshot/targets.d.ts.map +1 -0
  54. package/dist/lib/snapshot/targets.js +121 -0
  55. package/dist/lib/snapshot/targets.js.map +1 -0
  56. package/dist/lib/snapshot/verify.d.ts +18 -0
  57. package/dist/lib/snapshot/verify.d.ts.map +1 -0
  58. package/dist/lib/snapshot/verify.js +82 -0
  59. package/dist/lib/snapshot/verify.js.map +1 -0
  60. package/dist/verbs/catalogue-verify.d.ts +6 -0
  61. package/dist/verbs/catalogue-verify.d.ts.map +1 -0
  62. package/dist/verbs/catalogue-verify.js +114 -0
  63. package/dist/verbs/catalogue-verify.js.map +1 -0
  64. package/dist/verbs/catalogue.d.ts.map +1 -1
  65. package/dist/verbs/catalogue.js +15 -90
  66. package/dist/verbs/catalogue.js.map +1 -1
  67. package/dist/verbs/pack-validate.d.ts +3 -0
  68. package/dist/verbs/pack-validate.d.ts.map +1 -0
  69. package/dist/verbs/pack-validate.js +17 -0
  70. package/dist/verbs/pack-validate.js.map +1 -0
  71. package/dist/verbs/pack.d.ts.map +1 -1
  72. package/dist/verbs/pack.js +3 -10
  73. package/dist/verbs/pack.js.map +1 -1
  74. package/dist/verbs/snapshot.d.ts +3 -0
  75. package/dist/verbs/snapshot.d.ts.map +1 -0
  76. package/dist/verbs/snapshot.js +119 -0
  77. package/dist/verbs/snapshot.js.map +1 -0
  78. package/dist/verify-bin.d.ts +3 -0
  79. package/dist/verify-bin.d.ts.map +1 -0
  80. package/dist/verify-bin.js +8 -0
  81. package/dist/verify-bin.js.map +1 -0
  82. package/dist/verify-program.d.ts +14 -0
  83. package/dist/verify-program.d.ts.map +1 -0
  84. package/dist/verify-program.js +115 -0
  85. package/dist/verify-program.js.map +1 -0
  86. package/package.json +3 -3
  87. package/scripts/build-verify-package.mjs +199 -0
  88. package/scripts/verify-acceptance.sh +178 -0
  89. package/dist/conformance-bin.d.ts +0 -3
  90. package/dist/conformance-bin.d.ts.map +0 -1
  91. package/dist/conformance-bin.js +0 -8
  92. package/dist/conformance-bin.js.map +0 -1
  93. package/dist/conformance-program.d.ts +0 -6
  94. package/dist/conformance-program.d.ts.map +0 -1
  95. package/dist/conformance-program.js +0 -59
  96. package/dist/conformance-program.js.map +0 -1
  97. package/scripts/build-conformance-package.mjs +0 -57
@@ -0,0 +1,199 @@
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
+ const DIST = join(ROOT, "dist");
28
+ const OUT = join(ROOT, "build", "verify-cli");
29
+ const ENTRY = join(DIST, "verify-bin.js");
30
+
31
+ // Authoring and maintenance code that must never reach this package. Entry
32
+ // modules of excluded verbs plus the full CLI's own entrypoints.
33
+ const CANARIES = [
34
+ "cli.js",
35
+ "index.js",
36
+ "verbs/sync.js",
37
+ "verbs/catalogue.js",
38
+ "verbs/pack.js",
39
+ "verbs/mcp.js",
40
+ "verbs/init.js",
41
+ "verbs/render.js",
42
+ "verbs/metamodel.js",
43
+ "verbs/conformance.js",
44
+ "lib/sync/index.js",
45
+ "lib/catalogue/build.js",
46
+ "lib/catalogue/aggregate.js",
47
+ "lib/catalogue/graph-run.js",
48
+ "lib/catalogue/migration-map.js",
49
+ "lib/pack/builder.js",
50
+ ];
51
+
52
+ const die = (msg) => {
53
+ process.stderr.write(`build-verify-package: FAIL — ${msg}\n`);
54
+ process.exit(1);
55
+ };
56
+
57
+ if (!existsSync(ENTRY)) die(`${relative(ROOT, ENTRY)} missing — run \`npm run build\` first`);
58
+
59
+ // ---- 1. closure over imports ----------------------------------------------
60
+ // Line-based over tsc output, skipping comment lines. An earlier regex over the
61
+ // whole text read `// from "changed" produces…` in a comment as an import of a
62
+ // package called `changed` — prose parsed as code, the same failure a grep over
63
+ // workflow comments made the same day. Fail-closed caught it; this stops it.
64
+ const STATIC = /^\s*(?:import|export|\})[^\n]*?\bfrom\s*["']([^"']+)["']/;
65
+ const SIDE = /^\s*import\s*["']([^"']+)["']/;
66
+ const DYNAMIC = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
67
+ function specifiers(src) {
68
+ const out = [];
69
+ for (const line of src.split("\n")) {
70
+ const t = line.trimStart();
71
+ if (t.startsWith("//") || t.startsWith("*") || t.startsWith("/*")) continue;
72
+ const m = line.match(STATIC) ?? line.match(SIDE);
73
+ if (m) out.push(m[1]);
74
+ for (const d of line.matchAll(DYNAMIC)) out.push(d[1]);
75
+ }
76
+ return out;
77
+ }
78
+ const closure = new Set();
79
+ const bare = new Set();
80
+ const stack = [ENTRY];
81
+ while (stack.length) {
82
+ const f = stack.pop();
83
+ if (closure.has(f)) continue;
84
+ closure.add(f);
85
+ for (const spec of specifiers(readFileSync(f, "utf8"))) {
86
+ if (spec.startsWith(".")) {
87
+ const target = resolve(dirname(f), spec);
88
+ if (!existsSync(target)) die(`${relative(DIST, f)} imports '${spec}', which does not resolve`);
89
+ stack.push(target);
90
+ } else {
91
+ bare.add(spec);
92
+ }
93
+ }
94
+ }
95
+
96
+ // ---- 2. dependencies = only what the closure imports -----------------------
97
+ const main = JSON.parse(await readFile(join(ROOT, "package.json"), "utf8"));
98
+ const pkgName = (spec) =>
99
+ spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
100
+ const builtins = new Set(builtinModules);
101
+ const deps = {};
102
+ for (const spec of bare) {
103
+ if (spec.startsWith("node:") || builtins.has(spec) || builtins.has(spec.split("/")[0])) continue;
104
+ const name = pkgName(spec);
105
+ const range = main.dependencies?.[name];
106
+ if (!range) die(`closure imports '${spec}', which is not a declared dependency of sdd-cli`);
107
+ deps[name] = range;
108
+ }
109
+
110
+ // ---- 3 + 4. canaries -------------------------------------------------------
111
+ for (const c of CANARIES) {
112
+ const abs = join(DIST, c);
113
+ if (!existsSync(abs)) die(`canary '${c}' no longer exists in dist — the check would pass vacuously; update CANARIES`);
114
+ if (closure.has(abs)) die(`authoring module '${c}' is reachable from verify-bin.js — the boundary does not hold`);
115
+ }
116
+
117
+ // ---- bundled shell scripts -------------------------------------------------
118
+ // Some validators are shell scripts SHIPPED WITH the package (dispatch.ts:
119
+ // `kind: "bundled"`), resolved by lib/scripts.ts to <package-root>/scripts/.
120
+ // They are launched by path, not imported, so the closure above cannot see
121
+ // them. verify-cli 0.1.0 shipped without them and `app-docs` and
122
+ // `integration-docs` failed with exit 127 on every app and integration repo —
123
+ // found by a parity check against sdd-cli, not by the no-write acceptance,
124
+ // because a validator that cannot find its script writes nothing.
125
+ const bundled = new Set();
126
+ for (const f of closure) {
127
+ for (const m of readFileSync(f, "utf8").matchAll(/kind:\s*"bundled",\s*script:\s*"([^"]+)"/g)) {
128
+ const p = join(ROOT, "scripts", m[1]);
129
+ if (!existsSync(p)) die(`${relative(DIST, f)} declares bundled script '${m[1]}', which does not exist in scripts/`);
130
+ bundled.add(p);
131
+ }
132
+ }
133
+
134
+ // ---- assets: yaml/json data beside a closure module ------------------------
135
+ const assets = new Set();
136
+ for (const f of closure) {
137
+ const dir = dirname(f);
138
+ for (const name of readdirSync(dir)) {
139
+ const p = join(dir, name);
140
+ if (/\.(ya?ml|json)$/.test(name) && statSync(p).isFile()) assets.add(p);
141
+ }
142
+ }
143
+
144
+ // ---- assemble --------------------------------------------------------------
145
+ const version = process.env.VERIFY_VERSION ?? "0.1.0";
146
+ // Empty the directory, never remove it: a shell sitting in build/verify-cli
147
+ // (to run `npm publish`) keeps a handle on the directory itself, and deleting
148
+ // it leaves that shell in a path that no longer exists — npm then dies with
149
+ // ENOENT uv_cwd. That happened, twice, between a rebuild and a publish.
150
+ await mkdir(OUT, { recursive: true });
151
+ for (const entry of readdirSync(OUT)) await rm(join(OUT, entry), { recursive: true, force: true });
152
+ for (const f of [...closure, ...assets]) {
153
+ const dest = join(OUT, "dist", relative(DIST, f));
154
+ await mkdir(dirname(dest), { recursive: true });
155
+ await cp(f, dest);
156
+ }
157
+ for (const p of bundled) {
158
+ await mkdir(join(OUT, "scripts"), { recursive: true });
159
+ await cp(p, join(OUT, "scripts", relative(join(ROOT, "scripts"), p)));
160
+ }
161
+ for (const f of ["LICENSE", "NOTICE"]) await cp(join(ROOT, f), join(OUT, f));
162
+
163
+ if (main.publishConfig?.access !== "public") die("sdd-cli's package.json has no publishConfig.access=public — the verify package would publish private");
164
+
165
+ const pkg = {
166
+ name: "@sdd-method/verify-cli",
167
+ version,
168
+ description:
169
+ "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.",
170
+ license: main.license,
171
+ type: main.type,
172
+ bin: { "sdd-verify": "dist/verify-bin.js" },
173
+ engines: main.engines,
174
+ dependencies: deps,
175
+ repository: main.repository,
176
+ // Inherited, not assumed: a scoped package publishes PRIVATE by default, and
177
+ // the first publish of this one failed with E402 (private packages need a
178
+ // paid plan) because this field was missing. sdd-cli carries it; so must this.
179
+ publishConfig: main.publishConfig,
180
+ files: ["dist", "scripts", "README.md", "LICENSE", "NOTICE"],
181
+ };
182
+ await writeFile(join(OUT, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
183
+ await writeFile(
184
+ join(OUT, "README.md"),
185
+ `# @sdd-method/verify-cli\n\n` +
186
+ `\`sdd-verify\` checks an SDD repository and **cannot change it**.\n\n` +
187
+ `Verbs: \`validate\`, \`list\`, \`pack validate\`, \`catalogue validate\` (incl. \`--coverage\`), ` +
188
+ `\`catalogue impact\`, \`catalogue baseline-check\`, \`catalogue version\`.\n\n` +
189
+ `The package contains only the code those verbs reach. Authoring and maintenance verbs — ` +
190
+ `\`sync\`, \`catalogue build\`, \`init\`, \`scaffold\`, \`mcp\` and the rest — are not present in it, ` +
191
+ `and are in \`@sdd-method/sdd-cli\`. Invoking one fails with exit 1.\n`,
192
+ );
193
+
194
+ const kb = (fs) => Math.round(fs.reduce((n, f) => n + statSync(f).size, 0) / 1024);
195
+ process.stdout.write(
196
+ `assembled ${pkg.name}@${version} at build/verify-cli\n` +
197
+ ` modules ${closure.size} (${kb([...closure])} kB) · assets ${assets.size} · bundled scripts ${bundled.size} · deps ${Object.keys(deps).join(", ")}\n` +
198
+ ` canaries absent: ${CANARIES.length}/${CANARIES.length}\n`,
199
+ );
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env bash
2
+ # Acceptance for the verify-only CLI — M-ADR 0212 §4.3b.
3
+ #
4
+ # The property: sdd-verify CANNOT WRITE to the repository it is pointed at.
5
+ # Tested, not asserted — reading help text misfiled four verbs before this
6
+ # existed. For each repository: a fresh clone, every verb in every mode, and
7
+ # `git status --porcelain --ignored` must be empty after each one (ignored paths
8
+ # count: a verb that drops a cache into a gitignored directory still wrote).
9
+ #
10
+ # Fails closed on:
11
+ # - any write, by any verb, in any mode;
12
+ # - an excluded verb that does not exit 1 with the refusal message;
13
+ # - the CONTROL: the full CLI's `catalogue graph` must dirty a clone, or the
14
+ # detector cannot see a write and every PASS above is vacuous;
15
+ # - COVERAGE: a surface verb exercised on no repository. A missing input must
16
+ # not silently shrink the test.
17
+ #
18
+ # usage: verify-acceptance.sh --bin <sdd-verify entry> --control-bin <sdd-cli entry> <repo>...
19
+ set -uo pipefail
20
+
21
+ BIN=""; CONTROL=""; REPOS=()
22
+ while [ $# -gt 0 ]; do
23
+ case "$1" in
24
+ --bin) BIN="$2"; shift 2 ;;
25
+ --control-bin) CONTROL="$2"; shift 2 ;;
26
+ *) REPOS+=("$1"); shift ;;
27
+ esac
28
+ done
29
+ [ -n "$BIN" ] && [ -n "$CONTROL" ] && [ ${#REPOS[@]} -gt 0 ] || {
30
+ echo "usage: $0 --bin <sdd-verify> --control-bin <sdd-cli> <repo>..." >&2; exit 2; }
31
+
32
+ # Resolve both binaries to absolute paths BEFORE any cd. Each check runs from
33
+ # inside a clone, so a relative --control-bin resolved to nothing, the control
34
+ # exited 1 on every command, and every parity check reported a difference that
35
+ # was the harness, not the tool. Refuse to start rather than measure that.
36
+ abs() { [ -f "$1" ] || { echo "not found: $1" >&2; exit 2; }; echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"; }
37
+ BIN="$(abs "$BIN")" || exit 2; CONTROL="$(abs "$CONTROL")" || exit 2
38
+ node "$CONTROL" --version >/dev/null 2>&1 || { echo "control binary does not run: $CONTROL" >&2; exit 2; }
39
+ node "$BIN" --version >/dev/null 2>&1 || { echo "binary under test does not run: $BIN" >&2; exit 2; }
40
+
41
+ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
42
+ FAIL=0; RUNS=0
43
+ declare -A SEEN
44
+ SURFACE=("--version" "validate" "validate --strict" "list" "catalogue validate" \
45
+ "catalogue validate --strict" "catalogue validate --coverage" "catalogue impact" \
46
+ "catalogue baseline-check --findings" "catalogue baseline-check (stdin)" \
47
+ "catalogue version" "pack validate")
48
+
49
+ clean() { [ -z "$(git -C "$1" status --porcelain --ignored)" ]; }
50
+
51
+ # PARITY. No-write alone is not enough: a validator that cannot find its own
52
+ # script writes nothing and passes that test. verify-cli 0.1.0 shipped exactly
53
+ # that — `app-docs` and `integration-docs` exited 127 on every app and
54
+ # integration repo — and this check is what found it. Each surface run is
55
+ # repeated with the full CLI in the same clone; output and exit code must match.
56
+ # Normalised: ANSI colour, and the tool name in error prefixes. Exempt: the two
57
+ # version commands, which report each tool's own version by design.
58
+ # The tool's OWN version is normalised too, and only there — a version directly
59
+ # after `cli ` or `sdd-cli ` (catalogue headers print `cli <own version>`, and
60
+ # unknown-key notices say `sdd-cli <own version>`). A version inside a finding
61
+ # is left alone, so a real difference there still fails parity.
62
+ norm() { sed -e 's/\x1b\[[0-9;]*m//g' -e 's/sdd-verify/sdd-cli/g' \
63
+ -e 's/\(cli \|sdd-cli \)[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/\1<own-version>/g' "$1"; }
64
+ parity() { # parity <repo-dir> <label> <verify-rc> <args...> — compares $TMP/out with the control
65
+ local dir="$1" label="$2" vrc="$3"; shift 3
66
+ case "$label" in "--version"|"catalogue version") PAR="(version — n/a)"; return ;; esac
67
+ ( cd "$dir" && node "$CONTROL" "$@" >"$TMP/ctl" 2>&1 ); local crc=$?
68
+ git -C "$dir" clean -qfdx; git -C "$dir" checkout -q -- .
69
+ if [ "$vrc" = "$crc" ] && diff -q <(norm "$TMP/out") <(norm "$TMP/ctl") >/dev/null; then
70
+ PAR="= sdd-cli"
71
+ else
72
+ FAIL=1; PAR="⚠ DIFFERS from sdd-cli (exit $vrc vs $crc)"
73
+ diff <(norm "$TMP/out") <(norm "$TMP/ctl") | head -4 | sed 's/^/ /' >"$TMP/pardiff"
74
+ fi
75
+ }
76
+
77
+ run() { # run <repo-dir> <surface-label> <args...>
78
+ local dir="$1" label="$2"; shift 2
79
+ RUNS=$((RUNS+1)); : >"$TMP/pardiff"
80
+ ( cd "$dir" && node "$BIN" "$@" >"$TMP/out" 2>&1 ); local rc=$?
81
+ if clean "$dir"; then
82
+ SEEN["$label"]=1
83
+ parity "$dir" "$label" "$rc" "$@"
84
+ printf " %-46s exit %-3s clean %s\n" "$label" "$rc" "$PAR"
85
+ cat "$TMP/pardiff"
86
+ else
87
+ FAIL=1
88
+ printf " %-46s exit %-3s ⚠ WROTE:\n" "$label" "$rc"
89
+ git -C "$dir" status --porcelain --ignored | sed 's/^/ /' | head -8
90
+ git -C "$dir" clean -qfdx; git -C "$dir" checkout -q -- .
91
+ fi
92
+ }
93
+
94
+ refuse() { # refuse <repo-dir> <args...> — must exit 1, name sdd-cli, write nothing
95
+ local dir="$1"; shift
96
+ ( cd "$dir" && node "$BIN" "$@" >"$TMP/out" 2>&1 ); local rc=$?
97
+ if [ "$rc" -eq 1 ] && grep -q "not part of sdd-verify" "$TMP/out" && clean "$dir"; then
98
+ printf " %-46s refused (exit 1)\n" "✗ $*"
99
+ else
100
+ FAIL=1; printf " %-46s ⚠ NOT REFUSED — exit %s\n" "✗ $*" "$rc"; sed 's/^/ /' "$TMP/out" | head -3
101
+ fi
102
+ }
103
+
104
+ for repo in "${REPOS[@]}"; do
105
+ name="$(basename "$repo")"; dir="$TMP/$name"
106
+ git clone -q --no-local "$repo" "$dir" || { echo "clone failed: $repo"; FAIL=1; continue; }
107
+ echo "=== $name @ $(git -C "$dir" rev-parse --short HEAD) ==="
108
+ clean "$dir" || { echo " fresh clone is not clean — cannot test"; FAIL=1; continue; }
109
+
110
+ run "$dir" "--version" --version
111
+ run "$dir" "validate" validate --target-repo .
112
+ run "$dir" "validate --strict" validate --target-repo . --strict
113
+ run "$dir" "list" list --workspace-root .
114
+ if [ -d "$dir/catalogues" ]; then
115
+ run "$dir" "catalogue validate" catalogue validate --sdd-path .
116
+ run "$dir" "catalogue validate --strict" catalogue validate --sdd-path . --strict
117
+ run "$dir" "catalogue validate --coverage" catalogue validate --sdd-path . --coverage
118
+ [ -d "$dir/catalogues-aggregated" ] && run "$dir" "catalogue validate --coverage" \
119
+ catalogue validate --sdd-path . --coverage --tier aggregate --catalogues-dir catalogues-aggregated
120
+ node_id="$(awk -F, 'NR==2{print $1; exit}' "$dir/catalogues/services.csv" 2>/dev/null)"
121
+ [ -n "$node_id" ] && run "$dir" "catalogue impact" catalogue impact --sdd-path . --of "services/$node_id"
122
+ ( cd "$dir" && node "$BIN" catalogue validate --sdd-path . --coverage >"$TMP/findings.txt" 2>/dev/null )
123
+ clean "$dir" || { echo " ⚠ findings capture wrote"; FAIL=1; }
124
+ run "$dir" "catalogue baseline-check --findings" catalogue baseline-check --sdd-path . --findings "$TMP/findings.txt"
125
+ RUNS=$((RUNS+1))
126
+ ( cd "$dir" && node "$BIN" catalogue baseline-check --sdd-path . <"$TMP/findings.txt" >"$TMP/out" 2>&1 ); rc=$?
127
+ if clean "$dir"; then SEEN["catalogue baseline-check (stdin)"]=1; printf " %-46s exit %-3s clean\n" "catalogue baseline-check (stdin)" "$rc"
128
+ else FAIL=1; echo " catalogue baseline-check (stdin) ⚠ WROTE"; fi
129
+ run "$dir" "catalogue version" catalogue version
130
+ fi
131
+ for seed in "$dir"/orchestration/distribution/*seed*.yaml; do
132
+ [ -f "$seed" ] && run "$dir" "pack validate" pack validate --seed "orchestration/distribution/$(basename "$seed")"
133
+ done
134
+
135
+ refuse "$dir" sync
136
+ refuse "$dir" catalogue build
137
+ refuse "$dir" pack build --seed x
138
+ refuse "$dir" mcp serve
139
+ refuse "$dir" sync --help
140
+ done
141
+
142
+ # CONTROL — the detector must be able to see a write. Two parts, because a
143
+ # control that FAILS TO RUN is indistinguishable from a detector that cannot
144
+ # see, unless its exit code is checked. The first version of this script
145
+ # discarded both; `catalogue graph` errored on the first repo, wrote nothing,
146
+ # and the run reported the detector blind when the control had simply not run.
147
+ # 1. synthetic: a file written into a clone must show as dirty;
148
+ # 2. real: the full CLI's `catalogue graph` (writes by default) must EXIT 0
149
+ # on at least one repo and then dirty it.
150
+ ctl="$TMP/control"; git clone -q --no-local "${REPOS[0]}" "$ctl"
151
+ echo probe >"$ctl/.acceptance-probe"
152
+ if clean "$ctl"; then FAIL=1; echo "CONTROL ⚠ a synthetic write was not seen — the detector is blind"
153
+ else echo "CONTROL synthetic write seen"; fi
154
+ real_ok=0
155
+ for repo in "${REPOS[@]}"; do
156
+ c="$TMP/ctl-$(basename "$repo")"; git clone -q --no-local "$repo" "$c"
157
+ ( cd "$c" && node "$CONTROL" catalogue graph --sdd-path . >"$TMP/ctl.out" 2>&1 ); rc=$?
158
+ if [ "$rc" -ne 0 ]; then
159
+ echo "CONTROL catalogue graph on $(basename "$repo"): exit $rc, did not run — $(head -c 110 "$TMP/ctl.out")"
160
+ continue
161
+ fi
162
+ if clean "$c"; then FAIL=1; echo "CONTROL ⚠ catalogue graph exited 0 on $(basename "$repo") and the detector saw no write"
163
+ else echo "CONTROL catalogue graph on $(basename "$repo"): exit 0, $(git -C "$c" status --porcelain --ignored | wc -l) path(s) written and SEEN"; fi
164
+ real_ok=1; break
165
+ done
166
+ [ "$real_ok" -eq 1 ] || { FAIL=1; echo "CONTROL ⚠ INVALID — the authoring control ran successfully on no repository, so no write was ever demonstrated"; }
167
+
168
+ # COVERAGE — every surface item must have been exercised somewhere.
169
+ missing=()
170
+ for s in "${SURFACE[@]}"; do [ -n "${SEEN[$s]:-}" ] || missing+=("$s"); done
171
+ if [ ${#missing[@]} -gt 0 ]; then
172
+ FAIL=1; echo "COVERAGE ⚠ never exercised: ${missing[*]}"
173
+ else
174
+ echo "COVERAGE every surface verb and mode exercised (${#SURFACE[@]}/${#SURFACE[@]}) across $RUNS runs"
175
+ fi
176
+
177
+ [ "$FAIL" -eq 0 ] && echo "ACCEPTANCE PASS — sdd-verify wrote nothing, anywhere" || echo "ACCEPTANCE FAIL"
178
+ exit "$FAIL"
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
3
- //# sourceMappingURL=conformance-bin.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"conformance-bin.d.ts","sourceRoot":"","sources":["../src/conformance-bin.ts"],"names":[],"mappings":""}
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runConformance } from "./conformance-program.js";
3
- runConformance(process.argv.slice(2)).catch((err) => {
4
- const message = err instanceof Error ? err.message : String(err);
5
- process.stderr.write(`sdd-conformance: ${message}\n`);
6
- process.exit(1);
7
- });
8
- //# sourceMappingURL=conformance-bin.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"conformance-bin.js","sourceRoot":"","sources":["../src/conformance-bin.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC3D,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,OAAO,IAAI,CAAC,CAAC;IACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -1,6 +0,0 @@
1
- import { Command } from "commander";
2
- /** The verbs this binary exposes. Asserted by test, not by convention. */
3
- export declare const CONFORMANCE_VERBS: ReadonlyArray<string>;
4
- export declare function runConformance(argv: readonly string[]): Promise<void>;
5
- export declare function buildConformanceProgram(): Command;
6
- //# sourceMappingURL=conformance-program.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"conformance-program.d.ts","sourceRoot":"","sources":["../src/conformance-program.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,OAAO,EAAkB,MAAM,WAAW,CAAC;AASpD,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,MAAM,CAAgC,CAAC;AAErF,wBAAsB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,uBAAuB,IAAI,OAAO,CAwBjD"}
@@ -1,59 +0,0 @@
1
- // Second entrypoint: the CONFORMANCE surface only.
2
- //
3
- // M-ADR 0212 §4.6 Phase 0 — this file exists so the conformance client can be
4
- // published alongside the full CLI without copying a line of verb source. It
5
- // imports the SAME register functions the full program uses, so the two
6
- // binaries cannot drift in behaviour: there is one implementation and two
7
- // command surfaces over it. Adding a verb here is a deliberate act; nothing
8
- // arrives by inheritance.
9
- //
10
- // The surface is what automated pipelines were MEASURED to invoke — conformance
11
- // validation, distribution sync, packaging — and deliberately excludes the
12
- // authoring verbs (catalogue, metamodel, init, scaffold, MCP, render, plan),
13
- // which measurement showed pipelines never call.
14
- import { Command, CommanderError } from "commander";
15
- import { getPackageVersion } from "./lib/package-version.js";
16
- import { registerPack } from "./verbs/pack.js";
17
- import { registerSync } from "./verbs/sync.js";
18
- import { registerValidate } from "./verbs/validate.js";
19
- const SPEC_URL = "https://github.com/tomfosteruk/sdd-method/blob/main/docs/method/cli/sdd-cli-specification.md";
20
- /** The verbs this binary exposes. Asserted by test, not by convention. */
21
- export const CONFORMANCE_VERBS = ["sync", "validate", "pack"];
22
- export async function runConformance(argv) {
23
- const program = buildConformanceProgram();
24
- try {
25
- if (argv.length === 0) {
26
- program.outputHelp();
27
- return;
28
- }
29
- await program.parseAsync([...argv], { from: "user" });
30
- }
31
- catch (err) {
32
- if (err instanceof CommanderError) {
33
- if (err.code === "commander.help" ||
34
- err.code === "commander.helpDisplayed" ||
35
- err.code === "commander.version") {
36
- return;
37
- }
38
- }
39
- throw err;
40
- }
41
- }
42
- export function buildConformanceProgram() {
43
- const program = new Command();
44
- program
45
- .name("sdd-conformance")
46
- .description("Conformance surface for SDD repositories — validate, sync, pack. The authoring verbs live in sdd-cli.")
47
- .version(getPackageVersion(), "-v, --version")
48
- .addHelpText("after", `\nSpec: ${SPEC_URL}\nAuthoring verbs (catalogue, metamodel, init, scaffold, mcp) are in @sdd-method/sdd-cli.`)
49
- .exitOverride()
50
- .configureOutput({
51
- writeOut: (str) => process.stdout.write(str),
52
- writeErr: (str) => process.stderr.write(str),
53
- });
54
- registerSync(program);
55
- registerValidate(program);
56
- registerPack(program);
57
- return program;
58
- }
59
- //# sourceMappingURL=conformance-program.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"conformance-program.js","sourceRoot":"","sources":["../src/conformance-program.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,wEAAwE;AACxE,0EAA0E;AAC1E,4EAA4E;AAC5E,0BAA0B;AAC1B,EAAE;AACF,gFAAgF;AAChF,2EAA2E;AAC3E,6EAA6E;AAC7E,iDAAiD;AAEjD,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,QAAQ,GACZ,8FAA8F,CAAC;AAEjG,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAA0B,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;AAErF,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAuB;IAC1D,MAAM,OAAO,GAAG,uBAAuB,EAAE,CAAC;IAC1C,IAAI,CAAC;QACH,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,UAAU,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QACD,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACxD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,cAAc,EAAE,CAAC;YAClC,IACE,GAAG,CAAC,IAAI,KAAK,gBAAgB;gBAC7B,GAAG,CAAC,IAAI,KAAK,yBAAyB;gBACtC,GAAG,CAAC,IAAI,KAAK,mBAAmB,EAChC,CAAC;gBACD,OAAO;YACT,CAAC;QACH,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,iBAAiB,CAAC;SACvB,WAAW,CACV,uGAAuG,CACxG;SACA,OAAO,CAAC,iBAAiB,EAAE,EAAE,eAAe,CAAC;SAC7C,WAAW,CACV,OAAO,EACP,WAAW,QAAQ,2FAA2F,CAC/G;SACA,YAAY,EAAE;SACd,eAAe,CAAC;QACf,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;QAC5C,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;KAC7C,CAAC,CAAC;IAEL,YAAY,CAAC,OAAO,CAAC,CAAC;IACtB,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1B,YAAY,CAAC,OAAO,CAAC,CAAC;IAEtB,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -1,57 +0,0 @@
1
- // Assemble the publishable @sdd-method/conformance-cli package.
2
- //
3
- // M-ADR 0212 §4.6 condition 2 — ONE SOURCE, TWO ARTEFACTS. Nothing here is a
4
- // second editable copy: the compiled output is produced once by `npm run build`
5
- // and this script arranges it under a second package manifest. The conformance
6
- // binary imports the same compiled verb modules the full CLI does, so the two
7
- // cannot drift in behaviour; only the command surface differs.
8
- //
9
- // The assembled directory is generated, gitignored, and safe to delete.
10
-
11
- import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
12
- import { dirname, join } from "node:path";
13
- import { fileURLToPath } from "node:url";
14
-
15
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
16
- const OUT = join(ROOT, "build", "conformance-cli");
17
-
18
- const main = await readFile(join(ROOT, "package.json"), "utf8").then(JSON.parse);
19
-
20
- // The version is deliberately NOT inherited from the full CLI. The two
21
- // artefacts version independently: a catalogue-engine release must not appear
22
- // to move the conformance client, which is the whole point of separating them.
23
- const version = process.env.CONFORMANCE_VERSION ?? "0.1.0";
24
-
25
- const pkg = {
26
- name: "@sdd-method/conformance-cli",
27
- version,
28
- description:
29
- "Conformance surface for SDD repositories — validate, sync, pack. Built from the same source as @sdd-method/sdd-cli; the authoring verbs live there.",
30
- license: main.license,
31
- type: main.type,
32
- bin: { "sdd-conformance": "dist/conformance-bin.js" },
33
- engines: main.engines,
34
- dependencies: main.dependencies,
35
- repository: main.repository,
36
- files: ["dist", "scripts", "README.md", "LICENSE", "NOTICE"],
37
- };
38
-
39
- await rm(OUT, { recursive: true, force: true });
40
- await mkdir(OUT, { recursive: true });
41
- await cp(join(ROOT, "dist"), join(OUT, "dist"), { recursive: true });
42
- await cp(join(ROOT, "scripts"), join(OUT, "scripts"), { recursive: true });
43
- for (const f of ["LICENSE", "NOTICE"]) {
44
- await cp(join(ROOT, f), join(OUT, f));
45
- }
46
- await writeFile(
47
- join(OUT, "README.md"),
48
- `# @sdd-method/conformance-cli\n\n` +
49
- `The conformance surface of the SDD method CLI: \`sdd-conformance sync\`, \`validate\`, \`pack\`.\n\n` +
50
- `Built from the same source as \`@sdd-method/sdd-cli\` — the verbs are the same implementations, ` +
51
- `exposed under a smaller surface for automated pipelines, which were measured to invoke only these.\n\n` +
52
- `Authoring verbs — catalogue, metamodel, init, scaffold, mcp, render, plan — are in \`@sdd-method/sdd-cli\`.\n\n` +
53
- `This package is additive: it changes nothing about \`@sdd-method/sdd-cli\`, which continues to carry every verb.\n`,
54
- );
55
- await writeFile(join(OUT, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
56
-
57
- process.stdout.write(`assembled ${pkg.name}@${version} at build/conformance-cli\n`);