@sdd-method/sdd-cli 1.40.0 → 1.42.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/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.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/catalogue/build.d.ts.map +1 -1
- package/dist/lib/catalogue/build.js +6 -0
- package/dist/lib/catalogue/build.js.map +1 -1
- package/dist/lib/catalogue/builders/integration-components.js +3 -3
- package/dist/lib/catalogue/builders/integrations.d.ts +2 -0
- package/dist/lib/catalogue/builders/integrations.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/integrations.js +3 -0
- package/dist/lib/catalogue/builders/integrations.js.map +1 -1
- package/dist/lib/catalogue/builders/products.d.ts +6 -0
- package/dist/lib/catalogue/builders/products.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/products.js +9 -2
- package/dist/lib/catalogue/builders/products.js.map +1 -1
- package/dist/lib/catalogue/builders/repo-dependency.d.ts +12 -0
- package/dist/lib/catalogue/builders/repo-dependency.d.ts.map +1 -0
- package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
- package/dist/lib/catalogue/builders/repo-dependency.js.map +1 -0
- package/dist/lib/catalogue/builders/repositories.d.ts +1 -0
- package/dist/lib/catalogue/builders/repositories.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/repositories.js +4 -0
- package/dist/lib/catalogue/builders/repositories.js.map +1 -1
- package/dist/lib/catalogue/canonical-schema.yaml +66 -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 +26 -0
- package/dist/lib/catalogue/freshness.d.ts.map +1 -0
- package/dist/lib/catalogue/freshness.js +69 -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/catalogue/manifest-loader.d.ts +2 -0
- package/dist/lib/catalogue/manifest-loader.d.ts.map +1 -1
- package/dist/lib/catalogue/manifest-loader.js +4 -0
- package/dist/lib/catalogue/manifest-loader.js.map +1 -1
- package/dist/lib/catalogue/rubric-coverage-map.yaml +10 -0
- 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 +39 -0
- package/dist/lib/context-pull/index.d.ts.map +1 -0
- package/dist/lib/context-pull/index.js +151 -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/init/app.d.ts.map +1 -1
- package/dist/lib/init/app.js +3 -0
- 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 +69 -122
- package/dist/lib/init/ci-workflow.js.map +1 -1
- package/dist/lib/init/integration.d.ts.map +1 -1
- package/dist/lib/init/integration.js +3 -0
- package/dist/lib/init/integration.js.map +1 -1
- package/dist/lib/init/platform.d.ts.map +1 -1
- package/dist/lib/init/platform.js +6 -2
- 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 +24 -0
- package/dist/lib/package-version.d.ts.map +1 -1
- package/dist/lib/package-version.js +68 -1
- package/dist/lib/package-version.js.map +1 -1
- package/dist/lib/sync/index.d.ts +13 -0
- package/dist/lib/sync/index.d.ts.map +1 -1
- package/dist/lib/sync/index.js +69 -46
- package/dist/lib/sync/index.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 +91 -0
- package/dist/maintain-program.js.map +1 -0
- package/dist/maintain-version.json +4 -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 +14 -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 +11 -4
- package/dist/verify-program.js.map +1 -1
- package/dist/verify-version.json +4 -0
- package/package.json +6 -5
- package/scripts/{build-verify-package.mjs → build-persona-package.mjs} +112 -46
- package/scripts/copy-yaml-assets.mjs +6 -0
- 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 +60 -0
- package/scripts/verify-version.json +2 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
// Assemble
|
|
1
|
+
// Assemble a publishable persona package — @sdd-method/verify-cli (M-ADR 0212 §4.3b) or
|
|
2
|
+
// @sdd-method/maintain-cli (M-ADR 0218). Usage: build-persona-package.mjs [verify|maintain]
|
|
3
|
+
// (default verify). One script, so both packages hold the same boundary discipline.
|
|
2
4
|
//
|
|
3
5
|
// ONE SOURCE, and this time a genuinely smaller ARTEFACT. The conformance
|
|
4
6
|
// package copied the whole of dist/ under a second manifest, so its "excluded"
|
|
5
7
|
// 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
|
|
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
|
|
7
9
|
// to be reached by any path.
|
|
8
10
|
//
|
|
9
11
|
// Fails closed on four conditions, each of which would otherwise let the
|
|
@@ -29,29 +31,85 @@ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
29
31
|
// session on the machine (2026-10-01, about nine hours). `npm run verify:boundary` builds
|
|
30
32
|
// into build/check-dist and checks that instead; only a release should write dist/.
|
|
31
33
|
const DIST = process.env.SDD_CLI_DIST ? resolve(process.env.SDD_CLI_DIST) : join(ROOT, "dist");
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
const CANARIES = [
|
|
34
|
+
const PERSONA = process.argv[2] ?? "verify";
|
|
35
|
+
// Modules that must never reach a package. Authoring for both; maintain's own verbs are
|
|
36
|
+
// canaries only for verify (they are maintain's payload). Each list names modules that EXIST,
|
|
37
|
+
// or the check below fails rather than passing vacuously.
|
|
38
|
+
const AUTHOR_CANARIES = [
|
|
38
39
|
"cli.js",
|
|
39
40
|
"index.js",
|
|
40
|
-
"verbs/sync.js",
|
|
41
|
-
"verbs/catalogue.js",
|
|
42
|
-
"verbs/pack.js",
|
|
43
41
|
"verbs/mcp.js",
|
|
44
42
|
"verbs/init.js",
|
|
45
|
-
"verbs/render.js",
|
|
46
43
|
"verbs/metamodel.js",
|
|
47
44
|
"verbs/conformance.js",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
45
|
+
"verbs/scaffold.js",
|
|
46
|
+
"verbs/plan.js",
|
|
47
|
+
"verbs/contracts.js",
|
|
48
|
+
"verbs/api-surface.js",
|
|
49
|
+
"verbs/snapshot.js",
|
|
50
|
+
"verbs/catalogue.js",
|
|
52
51
|
"lib/catalogue/migration-map.js",
|
|
53
|
-
"lib/
|
|
52
|
+
"lib/mcp/server.js",
|
|
53
|
+
"lib/snapshot/land.js",
|
|
54
54
|
];
|
|
55
|
+
const PERSONAS = {
|
|
56
|
+
verify: {
|
|
57
|
+
entry: "verify-bin.js",
|
|
58
|
+
out: "verify-cli",
|
|
59
|
+
versionFile: "verify-version.json",
|
|
60
|
+
versionEnv: "VERIFY_VERSION",
|
|
61
|
+
name: "@sdd-method/verify-cli",
|
|
62
|
+
bin: "sdd-verify",
|
|
63
|
+
canaries: [
|
|
64
|
+
...AUTHOR_CANARIES,
|
|
65
|
+
"verbs/sync.js",
|
|
66
|
+
"verbs/pack.js",
|
|
67
|
+
"verbs/render.js",
|
|
68
|
+
"lib/sync/index.js",
|
|
69
|
+
"lib/catalogue/build.js",
|
|
70
|
+
"lib/catalogue/aggregate.js",
|
|
71
|
+
"lib/catalogue/graph-run.js",
|
|
72
|
+
"lib/pack/builder.js",
|
|
73
|
+
],
|
|
74
|
+
description:
|
|
75
|
+
"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.",
|
|
76
|
+
readme:
|
|
77
|
+
`# @sdd-method/verify-cli\n\n` +
|
|
78
|
+
`\`sdd-verify\` checks an SDD repository and **cannot change it**.\n\n` +
|
|
79
|
+
`Verbs: \`validate\`, \`list\`, \`pack validate\`, \`catalogue validate\` (incl. \`--coverage\`), ` +
|
|
80
|
+
`\`catalogue impact\`, \`catalogue baseline-check\`, \`catalogue version\`.\n\n` +
|
|
81
|
+
`The package contains only the code those verbs reach. Authoring and maintenance verbs — ` +
|
|
82
|
+
`\`sync\`, \`catalogue build\`, \`init\`, \`scaffold\`, \`mcp\` and the rest — are not present in it, ` +
|
|
83
|
+
`and are in \`@sdd-method/sdd-cli\`. Invoking one fails with exit 1.\n`,
|
|
84
|
+
},
|
|
85
|
+
maintain: {
|
|
86
|
+
entry: "maintain-bin.js",
|
|
87
|
+
out: "maintain-cli",
|
|
88
|
+
versionFile: "maintain-version.json",
|
|
89
|
+
versionEnv: "MAINTAIN_VERSION",
|
|
90
|
+
name: "@sdd-method/maintain-cli",
|
|
91
|
+
bin: "sdd-maintain",
|
|
92
|
+
canaries: AUTHOR_CANARIES,
|
|
93
|
+
description:
|
|
94
|
+
"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.",
|
|
95
|
+
readme:
|
|
96
|
+
`# @sdd-method/maintain-cli\n\n` +
|
|
97
|
+
`\`sdd-maintain\` brings an SDD repository current and rebuilds its derived model, and **cannot author specification content** (M-ADR 0218).\n\n` +
|
|
98
|
+
`Verbs: everything in \`@sdd-method/verify-cli\`, plus \`sync\`, \`generate-briefing\`, \`context pull\`, ` +
|
|
99
|
+
`\`catalogue build\`, \`catalogue aggregate\`, \`catalogue graph\`, \`catalogue diff\`, \`pack build\`, \`render screen\`.\n\n` +
|
|
100
|
+
`Each writes only derived or method-managed content, and two runs on the same commit write the same bytes. ` +
|
|
101
|
+
`Authoring verbs — \`init\`, \`scaffold\`, \`plan\`, \`mcp\`, \`conformance\` and the rest — are not present in it, ` +
|
|
102
|
+
`and are in \`@sdd-method/sdd-cli\`. Invoking one fails with exit 1.\n`,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const P = PERSONAS[PERSONA];
|
|
106
|
+
if (!P) {
|
|
107
|
+
process.stderr.write(`build-persona-package: FAIL — unknown persona '${PERSONA}' (verify | maintain)\n`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const OUT = join(ROOT, "build", P.out);
|
|
111
|
+
const ENTRY = join(DIST, P.entry);
|
|
112
|
+
|
|
55
113
|
|
|
56
114
|
const die = (msg) => {
|
|
57
115
|
process.stderr.write(`build-verify-package: FAIL — ${msg}\n`);
|
|
@@ -112,10 +170,10 @@ for (const spec of bare) {
|
|
|
112
170
|
}
|
|
113
171
|
|
|
114
172
|
// ---- 3 + 4. canaries -------------------------------------------------------
|
|
115
|
-
for (const c of
|
|
173
|
+
for (const c of P.canaries) {
|
|
116
174
|
const abs = join(DIST, c);
|
|
117
175
|
if (!existsSync(abs)) die(`canary '${c}' no longer exists in dist — the check would pass vacuously; update CANARIES`);
|
|
118
|
-
if (closure.has(abs)) die(`
|
|
176
|
+
if (closure.has(abs)) die(`excluded module '${c}' is reachable from ${P.entry} — the ${PERSONA} boundary does not hold`);
|
|
119
177
|
}
|
|
120
178
|
|
|
121
179
|
// ---- bundled shell scripts -------------------------------------------------
|
|
@@ -136,24 +194,42 @@ for (const f of closure) {
|
|
|
136
194
|
}
|
|
137
195
|
|
|
138
196
|
// ---- assets: yaml/json data beside a closure module ------------------------
|
|
197
|
+
// Data a module reads by path at runtime is invisible to the import closure. Beside the module, and
|
|
198
|
+
// in DATA-ONLY subdirectories under it (no .js inside): maintain-cli's first build missed
|
|
199
|
+
// lib/catalogue/archetype/maps/*.yaml, so `render screen` crashed with ENOENT where sdd-cli rendered.
|
|
139
200
|
const assets = new Set();
|
|
201
|
+
const dataOnlyFiles = (dir) => {
|
|
202
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
203
|
+
if (entries.some((e) => e.isFile() && e.name.endsWith(".js"))) return [];
|
|
204
|
+
return entries.flatMap((e) => {
|
|
205
|
+
const p = join(dir, e.name);
|
|
206
|
+
if (e.isDirectory()) return dataOnlyFiles(p);
|
|
207
|
+
return /\.(ya?ml|json)$/.test(e.name) ? [p] : [];
|
|
208
|
+
});
|
|
209
|
+
};
|
|
140
210
|
for (const f of closure) {
|
|
141
211
|
const dir = dirname(f);
|
|
142
|
-
for (const
|
|
143
|
-
const p = join(dir, name);
|
|
144
|
-
if (/\.(ya?ml|json)$/.test(name)
|
|
212
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
213
|
+
const p = join(dir, e.name);
|
|
214
|
+
if (e.isFile() && /\.(ya?ml|json)$/.test(e.name)) assets.add(p);
|
|
215
|
+
else if (e.isDirectory()) for (const d of dataOnlyFiles(p)) assets.add(d);
|
|
145
216
|
}
|
|
146
217
|
}
|
|
147
218
|
|
|
148
219
|
// ---- assemble --------------------------------------------------------------
|
|
149
|
-
// The version is COMMITTED (scripts
|
|
220
|
+
// The version is COMMITTED (scripts/<persona>-version.json), not defaulted. Defaulting to "0.1.0" let a build
|
|
150
221
|
// of new source carry an old version silently; verify-cli then fell two sdd-cli releases behind unnoticed.
|
|
151
222
|
// 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",
|
|
223
|
+
const versionFile = JSON.parse(readFileSync(join(ROOT, "scripts", P.versionFile), "utf8"));
|
|
153
224
|
const version = versionFile.version;
|
|
154
|
-
if (!/^\d+\.\d+\.\d+$/.test(version ?? "")) die(
|
|
155
|
-
|
|
156
|
-
|
|
225
|
+
if (!/^\d+\.\d+\.\d+$/.test(version ?? "")) die(`scripts/${P.versionFile} has no semver \`version\``);
|
|
226
|
+
// The engine this package is built from. Every engine-meaning check reads it (getSddCliVersion):
|
|
227
|
+
// a bundle's floor, catalogue stamps, the MCP identity. A stale built_against in the version file
|
|
228
|
+
// would ship a package claiming an engine it is not, so the build refuses it.
|
|
229
|
+
if (versionFile.built_against_sdd_cli !== main.version)
|
|
230
|
+
die(`scripts/${P.versionFile} says built_against_sdd_cli ${versionFile.built_against_sdd_cli}, but this sdd-cli is ${main.version}; update the file`);
|
|
231
|
+
if (process.env[P.versionEnv] && process.env[P.versionEnv] !== version)
|
|
232
|
+
die(`${P.versionEnv}=${process.env[P.versionEnv]} disagrees with scripts/${P.versionFile} (${version}); edit the file`);
|
|
157
233
|
// Empty the directory, never remove it: a shell sitting in build/verify-cli
|
|
158
234
|
// (to run `npm publish`) keeps a handle on the directory itself, and deleting
|
|
159
235
|
// it leaves that shell in a path that no longer exists — npm then dies with
|
|
@@ -171,40 +247,30 @@ for (const p of bundled) {
|
|
|
171
247
|
}
|
|
172
248
|
for (const f of ["LICENSE", "NOTICE"]) await cp(join(ROOT, f), join(OUT, f));
|
|
173
249
|
|
|
174
|
-
if (main.publishConfig?.access !== "public") die(
|
|
250
|
+
if (main.publishConfig?.access !== "public") die(`sdd-cli's package.json has no publishConfig.access=public — ${P.name} would publish private`);
|
|
175
251
|
|
|
176
252
|
const pkg = {
|
|
177
|
-
name:
|
|
253
|
+
name: P.name,
|
|
178
254
|
version,
|
|
179
|
-
|
|
180
|
-
|
|
255
|
+
sddCliVersion: main.version,
|
|
256
|
+
description: P.description,
|
|
181
257
|
license: main.license,
|
|
182
258
|
type: main.type,
|
|
183
|
-
bin: {
|
|
259
|
+
bin: { [P.bin]: `dist/${P.entry}` },
|
|
184
260
|
engines: main.engines,
|
|
185
261
|
dependencies: deps,
|
|
186
262
|
repository: main.repository,
|
|
187
263
|
// Inherited, not assumed: a scoped package publishes PRIVATE by default, and
|
|
188
|
-
// the first publish of
|
|
189
|
-
// paid plan) because this field was missing. sdd-cli carries it; so must this.
|
|
264
|
+
// the first publish of verify-cli failed with E402 because this was missing.
|
|
190
265
|
publishConfig: main.publishConfig,
|
|
191
266
|
files: ["dist", "scripts", "README.md", "LICENSE", "NOTICE"],
|
|
192
267
|
};
|
|
193
268
|
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
|
-
);
|
|
269
|
+
await writeFile(join(OUT, "README.md"), P.readme);
|
|
204
270
|
|
|
205
271
|
const kb = (fs) => Math.round(fs.reduce((n, f) => n + statSync(f).size, 0) / 1024);
|
|
206
272
|
process.stdout.write(
|
|
207
|
-
`assembled ${pkg.name}@${version} at build
|
|
273
|
+
`assembled ${pkg.name}@${version} at build/${P.out}\n` +
|
|
208
274
|
` modules ${closure.size} (${kb([...closure])} kB) · assets ${assets.size} · bundled scripts ${bundled.size} · deps ${Object.keys(deps).join(", ")}\n` +
|
|
209
|
-
` canaries absent: ${
|
|
275
|
+
` canaries absent: ${P.canaries.length}/${P.canaries.length}\n`,
|
|
210
276
|
);
|
|
@@ -35,4 +35,10 @@ for await (const srcFile of walk(SRC)) {
|
|
|
35
35
|
await copyFile(srcFile, destFile);
|
|
36
36
|
copied++;
|
|
37
37
|
}
|
|
38
|
+
// The verify-cli version the CI workflow pins (getVerifyCliVersion). Copied, not read live from
|
|
39
|
+
// scripts/, so it moves with dist/ and never ahead of it.
|
|
40
|
+
for (const persona of ["verify", "maintain"]) {
|
|
41
|
+
await copyFile(join(__dirname, `${persona}-version.json`), join(DEST, `${persona}-version.json`));
|
|
42
|
+
copied++;
|
|
43
|
+
}
|
|
38
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)"
|
|
@@ -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,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Release gate for @sdd-method/sdd-cli. Run by `prepublishOnly`, so `npm publish` refuses unless the
|
|
3
|
+
// version being published is traceable: the tree is clean, HEAD is on origin/main, and the tag
|
|
4
|
+
// v<version> is on origin and points at HEAD. A publish whose commit was never pushed, or never tagged,
|
|
5
|
+
// cannot be traced back to its source afterwards; this makes both impossible rather than remembered.
|
|
6
|
+
//
|
|
7
|
+
// node scripts/release-gate.mjs the pre-publish gate (exit 1 on any failure)
|
|
8
|
+
// node scripts/release-gate.mjs --audit every version on npm has its tag on origin
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
16
|
+
const run = (cmd, args) => execFileSync(cmd, args, { cwd: root, encoding: "utf-8" }).trim();
|
|
17
|
+
|
|
18
|
+
// The commit a tag names on origin (an annotated tag's peeled target wins), or "" when absent.
|
|
19
|
+
function remoteTags() {
|
|
20
|
+
const out = new Map();
|
|
21
|
+
for (const line of run("git", ["ls-remote", "--tags", "origin"]).split("\n")) {
|
|
22
|
+
const m = /^([0-9a-f]+)\s+refs\/tags\/(.+?)(\^\{\})?$/.exec(line);
|
|
23
|
+
if (m && (m[3] || !out.has(m[2]))) out.set(m[2], m[1]);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fail = (msg) => {
|
|
29
|
+
console.error(`[FAIL] release-gate: ${msg}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (process.argv.includes("--audit")) {
|
|
34
|
+
const published = JSON.parse(run("npm", ["view", pkg.name, "versions", "--json"]));
|
|
35
|
+
const tags = remoteTags();
|
|
36
|
+
const missing = published.filter((v) => !tags.has(`v${v}`));
|
|
37
|
+
if (missing.length) fail(`${missing.length} of ${published.length} published versions have no tag on origin: ${missing.map((v) => `v${v}`).join(" ")}`);
|
|
38
|
+
console.log(`[PASS] release-gate --audit: all ${published.length} published versions are tagged on origin`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
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"]) {
|
|
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
|
+
|
|
51
|
+
const tag = `v${pkg.version}`;
|
|
52
|
+
if (run("git", ["status", "--porcelain", "--untracked-files=no"]) !== "") fail("the working tree has uncommitted changes to tracked files");
|
|
53
|
+
const head = run("git", ["rev-parse", "HEAD"]);
|
|
54
|
+
run("git", ["fetch", "-q", "origin", "main"]);
|
|
55
|
+
const main = run("git", ["rev-parse", "origin/main"]);
|
|
56
|
+
if (head !== main) fail(`HEAD ${head.slice(0, 7)} is not origin/main ${main.slice(0, 7)} — push main, and publish from the release commit`);
|
|
57
|
+
const at = remoteTags().get(tag);
|
|
58
|
+
if (!at) fail(`${tag} is not on origin — git tag ${tag} && git push origin ${tag}`);
|
|
59
|
+
if (at !== head) fail(`${tag} on origin names ${at.slice(0, 7)}, not HEAD ${head.slice(0, 7)}`);
|
|
60
|
+
console.log(`[PASS] release-gate: ${tag} at ${head.slice(0, 7)}, pushed and tagged on origin`);
|