@sdd-method/maintain-cli 0.3.0 → 0.5.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/lib/briefing/render.js +1 -2
- package/dist/lib/briefing/variables.js +1 -2
- package/dist/lib/catalogue/aggregate.js +35 -36
- package/dist/lib/catalogue/application-archetype/expectations.js +1 -1
- package/dist/lib/catalogue/archetype/wireframe.js +1 -1
- package/dist/lib/catalogue/build.js +17 -23
- package/dist/lib/catalogue/builders/application-conformance-coverage.js +1 -1
- package/dist/lib/catalogue/builders/c4-components.js +4 -5
- package/dist/lib/catalogue/builders/c4-containers.js +3 -3
- package/dist/lib/catalogue/builders/c4-entity-rollups.js +1 -1
- package/dist/lib/catalogue/builders/c4-external-systems.js +1 -1
- package/dist/lib/catalogue/builders/c4-l1-candidates.js +1 -1
- package/dist/lib/catalogue/builders/capabilities.js +2 -2
- package/dist/lib/catalogue/builders/capability-edges.js +4 -4
- package/dist/lib/catalogue/builders/capability-spec-walker.js +3 -3
- package/dist/lib/catalogue/builders/container-dependency.js +18 -19
- package/dist/lib/catalogue/builders/contracts.js +5 -6
- package/dist/lib/catalogue/builders/data-store-mapping.js +3 -3
- package/dist/lib/catalogue/builders/edge-label-merge.js +2 -2
- package/dist/lib/catalogue/builders/entities-manifest.js +1 -1
- package/dist/lib/catalogue/builders/entities-mermaid.js +3 -3
- package/dist/lib/catalogue/builders/entity-slug.js +11 -12
- package/dist/lib/catalogue/builders/event-catalog-walker.js +3 -3
- package/dist/lib/catalogue/builders/event-edges.js +1 -1
- package/dist/lib/catalogue/builders/evidence-class.js +2 -2
- package/dist/lib/catalogue/builders/external-system-exemption.js +1 -1
- package/dist/lib/catalogue/builders/feature-contract.js +1 -3
- package/dist/lib/catalogue/builders/feature-grounding.js +1 -2
- package/dist/lib/catalogue/builders/feature-groups.js +0 -1
- package/dist/lib/catalogue/builders/feature-subscription-channels.js +2 -3
- package/dist/lib/catalogue/builders/feature-tree-walker.js +1 -1
- package/dist/lib/catalogue/builders/features.js +3 -4
- package/dist/lib/catalogue/builders/glossary-xref.js +1 -1
- package/dist/lib/catalogue/builders/persona-c4system.js +7 -7
- package/dist/lib/catalogue/builders/persona-container.js +14 -14
- package/dist/lib/catalogue/builders/personas.js +1 -2
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +2 -2
- package/dist/lib/catalogue/builders/quality-scores.js +1 -1
- package/dist/lib/catalogue/builders/repositories.js +2 -2
- package/dist/lib/catalogue/builders/screen-tree.js +1 -1
- package/dist/lib/catalogue/builders/screens.js +3 -4
- package/dist/lib/catalogue/builders/system-external-dependency.js +7 -7
- package/dist/lib/catalogue/canonical-schema.yaml +23 -23
- package/dist/lib/catalogue/contract-path-resolution.js +3 -3
- package/dist/lib/catalogue/coverage-predicates.js +4 -4
- package/dist/lib/catalogue/coverage-run.js +1 -1
- package/dist/lib/catalogue/dedupe-keyed-tables.js +2 -3
- package/dist/lib/catalogue/ext-id.js +4 -4
- package/dist/lib/catalogue/feature-grounding-aliases-loader.js +1 -1
- package/dist/lib/catalogue/finding-identity.js +3 -3
- package/dist/lib/catalogue/freshness.js +1 -1
- package/dist/lib/catalogue/manifest-loader.js +2 -2
- package/dist/lib/catalogue/scaffold-delta.js +1 -1
- package/dist/lib/catalogue/schema-compat.js +1 -2
- package/dist/lib/catalogue/screen-validation-loader.js +1 -1
- package/dist/lib/catalogue/staleness-verdicts.js +2 -2
- package/dist/lib/catalogue/traceability-loader.js +2 -3
- package/dist/lib/ci-binding/index.js +108 -33
- package/dist/lib/ci-binding/module.js +18 -8
- package/dist/lib/init/ci-workflow.js +2 -0
- package/dist/lib/lock-file.js +1 -1
- package/dist/lib/pack/reference-closure.js +3 -3
- package/dist/lib/package-version.js +1 -1
- package/dist/lib/snapshot/manifest.js +1 -1
- package/dist/lib/sync/adr-transitions.js +1 -1
- package/dist/lib/sync/config.js +8 -1
- package/dist/lib/sync/diff.js +1 -1
- package/dist/lib/sync/index.js +8 -3
- package/dist/lib/validate/behaviour-store.js +351 -0
- package/dist/lib/validate/dispatch.js +18 -0
- package/dist/lib/validate/index.js +1 -1
- package/dist/lib/validate/plan.js +3 -1
- package/dist/lib/validate/yaml-parse.js +1 -1
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/sync.js +1 -1
- package/dist/verify-version.json +2 -2
- package/package.json +4 -6
- package/scripts/validate-app-dependencies.sh +5 -5
- package/scripts/validate-integration-docs.sh +1 -1
- package/LICENSE +0 -201
- package/NOTICE +0 -17
|
@@ -8,14 +8,21 @@
|
|
|
8
8
|
* wrote is pristine and re-renders with no override, and only an edited copy needs --allow-overwrite.
|
|
9
9
|
* The binding is remembered in the lock too (`ci_binding:`, absent meaning github), so an estate
|
|
10
10
|
* chooses it once.
|
|
11
|
+
*
|
|
12
|
+
* `local` is the binding with no host: the module is written and nothing calls it but people, for an
|
|
13
|
+
* estate that validates on its own machines only (the module is run with `dagger call` there). Sync
|
|
14
|
+
* removes the wrapper this tool wrote for the previous binding when its block still hashes to the
|
|
15
|
+
* lock's digest; an edited one is drift, and a file without markers is the adopter's and stays.
|
|
11
16
|
*/
|
|
12
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
13
18
|
import { createHash } from "node:crypto";
|
|
14
19
|
import { dirname, join } from "node:path";
|
|
15
20
|
import { BLOCK_BEGIN, BLOCK_END, WORKFLOW_REL_PATH, renderMethodManagedWorkflowBlock, renderMethodValidationWorkflow, } from "../init/ci-workflow.js";
|
|
16
21
|
import { detectBlockDrift, writeMarkedBlockFile } from "../sync/yaml-block-merge.js";
|
|
17
22
|
import { DAGGER_VERSION, MODULE_CALL, MODULE_DIR, REBUILD_FLAG, REBUILD_SCRIPT, moduleSha256, renderModuleFiles } from "./module.js";
|
|
18
|
-
|
|
23
|
+
/** The bindings that render a wrapper for a CI host. */
|
|
24
|
+
export const HOST_BINDINGS = ["github", "gitlab", "jenkins"];
|
|
25
|
+
export const CI_BINDINGS = [...HOST_BINDINGS, "local"];
|
|
19
26
|
/** M-ADR 0219 §8.2 (ruled 2026-10-16): what every SDD has before it chooses. */
|
|
20
27
|
export const DEFAULT_CI_BINDING = "github";
|
|
21
28
|
/**
|
|
@@ -31,6 +38,9 @@ export function parseCiRunner(s) {
|
|
|
31
38
|
export function isCiBinding(s) {
|
|
32
39
|
return CI_BINDINGS.includes(s);
|
|
33
40
|
}
|
|
41
|
+
export function isHostBinding(s) {
|
|
42
|
+
return HOST_BINDINGS.includes(s);
|
|
43
|
+
}
|
|
34
44
|
const HASH = { begin: BLOCK_BEGIN, end: BLOCK_END };
|
|
35
45
|
// Groovy has no '#' comments.
|
|
36
46
|
const SLASH = { begin: "// method-managed: begin", end: "// method-managed: end" };
|
|
@@ -84,6 +94,8 @@ function gitlabBlock(profile, runner) {
|
|
|
84
94
|
' DOCKER_TLS_CERTDIR: "/certs"',
|
|
85
95
|
' DOCKER_TLS_VERIFY: "1"',
|
|
86
96
|
' DOCKER_CERT_PATH: "/certs/client"',
|
|
97
|
+
// Full history: the behaviour-store stale check diffs living specs against each change's agreed commit.
|
|
98
|
+
' GIT_DEPTH: "0"',
|
|
87
99
|
" rules:",
|
|
88
100
|
' - if: $CI_PIPELINE_SOURCE == "merge_request_event"',
|
|
89
101
|
" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH",
|
|
@@ -168,8 +180,41 @@ export function blockSha256(text, markers) {
|
|
|
168
180
|
export async function syncCiBinding(o) {
|
|
169
181
|
const failures = [];
|
|
170
182
|
const log = [];
|
|
183
|
+
const was = o.prevLock?.ciBinding;
|
|
184
|
+
const sameBinding = (was ?? DEFAULT_CI_BINDING) === o.binding;
|
|
185
|
+
if (o.binding === "local") {
|
|
186
|
+
// No host. A switch removes the wrapper this tool wrote for the previous binding, but only when its
|
|
187
|
+
// block still hashes to the lock's digest: an edited block is drift, a file without markers is the
|
|
188
|
+
// adopter's and stays.
|
|
189
|
+
if (o.runner !== undefined && o.runner.length > 0) {
|
|
190
|
+
failures.push("--ci-runner names where a CI host runs the job; the local binding has no host. Drop --ci-runner (or choose a host binding).");
|
|
191
|
+
}
|
|
192
|
+
let removal = null;
|
|
193
|
+
const previous = sameBinding ? null : was === undefined ? DEFAULT_CI_BINDING : isHostBinding(was) ? was : null;
|
|
194
|
+
if (previous !== null) {
|
|
195
|
+
const prevHost = HOST_WRAPPERS[previous];
|
|
196
|
+
const abs = join(o.targetRepo, prevHost.relPath);
|
|
197
|
+
const text = await tryRead(abs);
|
|
198
|
+
const sha = blockSha256(text, prevHost.markers);
|
|
199
|
+
if (text !== null && sha === null) {
|
|
200
|
+
log.push(`${prevHost.relPath} has no method-managed markers, so it is the adopter's: left in place.`);
|
|
201
|
+
}
|
|
202
|
+
else if (text !== null && (sha === o.prevLock?.ciWorkflowBlockSha256 || o.allowDrift)) {
|
|
203
|
+
removal = { abs, rel: prevHost.relPath, text, host: prevHost };
|
|
204
|
+
}
|
|
205
|
+
else if (text !== null) {
|
|
206
|
+
failures.push(`${prevHost.relPath}: the method-managed block was edited since the last sync (it no longer matches the lock's digest), so switching to the local binding will not remove it. Re-run with --allow-overwrite to remove the block; adopter content outside the markers is kept.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const mod = await checkModule(o, failures, log);
|
|
210
|
+
if (o.checkOnly || failures.length > 0)
|
|
211
|
+
return { failures, log, wrapperSha: null, moduleSha: mod.shaOnDisk };
|
|
212
|
+
await writeModule(o, mod, log, "; the local binding calls it from no CI host");
|
|
213
|
+
if (removal !== null)
|
|
214
|
+
await removeWrapper(removal, o.profile, log);
|
|
215
|
+
return { failures, log, wrapperSha: null, moduleSha: mod.shaRendered };
|
|
216
|
+
}
|
|
171
217
|
const host = HOST_WRAPPERS[o.binding];
|
|
172
|
-
const sameBinding = (o.prevLock?.ciBinding ?? DEFAULT_CI_BINDING) === o.binding;
|
|
173
218
|
// 1. The host wrapper.
|
|
174
219
|
const wrapperAbs = join(o.targetRepo, host.relPath);
|
|
175
220
|
const existing = await tryRead(wrapperAbs);
|
|
@@ -187,16 +232,44 @@ export async function syncCiBinding(o) {
|
|
|
187
232
|
else if (markerless && !o.allowOverwrite) {
|
|
188
233
|
failures.push(`${host.relPath} exists without method-managed markers — refusing to overwrite adopter content. Re-run with --allow-overwrite to replace the file.`);
|
|
189
234
|
}
|
|
190
|
-
// 2. The module
|
|
235
|
+
// 2. The module.
|
|
236
|
+
const mod = await checkModule(o, failures, log);
|
|
237
|
+
if (o.checkOnly || failures.length > 0) {
|
|
238
|
+
return { failures, log, wrapperSha: null, moduleSha: mod.shaOnDisk };
|
|
239
|
+
}
|
|
240
|
+
const written = await writeMarkedBlockFile(wrapperAbs, existing === null ? host.file(o.profile, o.runner) : block, host.markers, o.allowOverwrite);
|
|
241
|
+
if (written.changed)
|
|
242
|
+
log.push(`${existing === null ? "Wrote" : "Refreshed"} ${host.relPath}.`);
|
|
243
|
+
await writeModule(o, mod, log, "");
|
|
244
|
+
// A binding switch between hosts leaves the previous host's file alone: it is the adopter's to delete
|
|
245
|
+
// once the new host runs, and deleting it here would remove a job some branch protection may still
|
|
246
|
+
// require. (From local there is no previous host file.)
|
|
247
|
+
if (!sameBinding && (was === undefined || isHostBinding(was))) {
|
|
248
|
+
const prevHost = HOST_WRAPPERS[was ?? DEFAULT_CI_BINDING];
|
|
249
|
+
if ((await tryRead(join(o.targetRepo, prevHost.relPath))) !== null) {
|
|
250
|
+
log.push(`CI binding is now ${o.binding}; ${prevHost.relPath} (the previous binding) is left in place. Delete it once ${host.relPath} runs.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
failures,
|
|
255
|
+
log,
|
|
256
|
+
wrapperSha: blockSha256(await tryRead(wrapperAbs), host.markers),
|
|
257
|
+
moduleSha: mod.shaRendered,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The module: every file it renders, compared as one set. Pristine when the files on disk hash to the
|
|
262
|
+
* lock's digest. The render may list a file the last sync never wrote (the generator gained one): it is
|
|
263
|
+
* absent, not edited, so the digest over the files present decides. A file the adopter deleted still
|
|
264
|
+
* changes that digest, so it is still drift.
|
|
265
|
+
*/
|
|
266
|
+
async function checkModule(o, failures, log) {
|
|
191
267
|
const rendered = renderModuleFiles();
|
|
192
268
|
const onDisk = [];
|
|
193
269
|
for (const [rel] of rendered)
|
|
194
270
|
onDisk.push([rel, await tryRead(join(o.targetRepo, MODULE_DIR, rel))]);
|
|
195
271
|
const present = onDisk.some(([, c]) => c !== null);
|
|
196
272
|
const matches = rendered.every(([rel, c], i) => onDisk[i][0] === rel && onDisk[i][1] === c);
|
|
197
|
-
// Pristine when the files on disk hash to the lock's digest. The render may list a file the last sync
|
|
198
|
-
// never wrote (the generator gained one): it is absent, not edited, so the digest over the files present
|
|
199
|
-
// decides. A file the adopter deleted still changes that digest, so it is still drift.
|
|
200
273
|
const lockSha = o.prevLock?.ciModuleSha256;
|
|
201
274
|
const modulePristine = lockSha !== undefined &&
|
|
202
275
|
(moduleSha256(onDisk) === lockSha || moduleSha256(onDisk.filter(([, c]) => c !== null)) === lockSha);
|
|
@@ -206,35 +279,37 @@ export async function syncCiBinding(o) {
|
|
|
206
279
|
if (present && !matches && !modulePristine && !o.allowDrift) {
|
|
207
280
|
failures.push(`${MODULE_DIR}/: the method-managed module differs from what this sdd-cli renders. Either the generator changed since the last sync (expected after a CLI upgrade) or a file was hand-edited. Re-run with --allow-overwrite to re-render it.`);
|
|
208
281
|
}
|
|
209
|
-
|
|
210
|
-
|
|
282
|
+
return { rendered, present, matches, shaOnDisk: moduleSha256(onDisk), shaRendered: moduleSha256(rendered) };
|
|
283
|
+
}
|
|
284
|
+
async function writeModule(o, mod, log, note) {
|
|
285
|
+
if (mod.matches)
|
|
286
|
+
return;
|
|
287
|
+
for (const [rel, content] of mod.rendered) {
|
|
288
|
+
const abs = join(o.targetRepo, MODULE_DIR, rel);
|
|
289
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
290
|
+
await writeFile(abs, content, "utf8");
|
|
211
291
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
292
|
+
log.push(`${mod.present ? "Refreshed" : "Wrote"} ${MODULE_DIR}/ (the Method Validation module, M-ADR 0219)${note}.`);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Remove a wrapper's method-managed block. The file goes with it when what remains is this tool's own
|
|
296
|
+
* template text (or nothing), since then no line of it is the adopter's; otherwise the rest is kept.
|
|
297
|
+
*/
|
|
298
|
+
async function removeWrapper(r, profile, log) {
|
|
299
|
+
const cut = (text) => {
|
|
300
|
+
const b = text.indexOf(r.host.markers.begin);
|
|
301
|
+
const e = text.indexOf(r.host.markers.end, b) + r.host.markers.end.length;
|
|
302
|
+
return (text.slice(0, b) + text.slice(e)).replace(/^\n+/, "");
|
|
303
|
+
};
|
|
304
|
+
const rest = cut(r.text);
|
|
305
|
+
if (rest.trim() === "" || rest.trim() === cut(r.host.file(profile)).trim()) {
|
|
306
|
+
await rm(r.abs);
|
|
307
|
+
log.push(`Removed ${r.rel}: the local binding has no CI host, and the file held only this tool's wrapper.`);
|
|
222
308
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const was = o.prevLock?.ciBinding;
|
|
227
|
-
const previous = HOST_WRAPPERS[was !== undefined && isCiBinding(was) ? was : DEFAULT_CI_BINDING];
|
|
228
|
-
if ((await tryRead(join(o.targetRepo, previous.relPath))) !== null) {
|
|
229
|
-
log.push(`CI binding is now ${o.binding}; ${previous.relPath} (the previous binding) is left in place. Delete it once ${host.relPath} runs.`);
|
|
230
|
-
}
|
|
309
|
+
else {
|
|
310
|
+
await writeFile(r.abs, rest, "utf8");
|
|
311
|
+
log.push(`Removed the method-managed block from ${r.rel}; the adopter content outside it is kept.`);
|
|
231
312
|
}
|
|
232
|
-
return {
|
|
233
|
-
failures,
|
|
234
|
-
log,
|
|
235
|
-
wrapperSha: blockSha256(await tryRead(wrapperAbs), host.markers),
|
|
236
|
-
moduleSha: moduleSha256(rendered),
|
|
237
|
-
};
|
|
238
313
|
}
|
|
239
314
|
async function tryRead(path) {
|
|
240
315
|
try {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
import { getMaintainCliVersion, getVerifyCliVersion } from "../package-version.js";
|
|
15
|
+
import { OPENSPEC_VERSION } from "../validate/behaviour-store.js";
|
|
15
16
|
/** Where the module lives in an SDD (M-ADR 0219 §4.3, ruled 2026-10-16). */
|
|
16
17
|
export const MODULE_DIR = ".dagger/method-validation";
|
|
17
18
|
/**
|
|
@@ -27,7 +28,7 @@ export const REBUILD_SCRIPT = `${MODULE_DIR}/catalogue-rebuild.sh`;
|
|
|
27
28
|
/**
|
|
28
29
|
* Left in the git directory by REBUILD_SCRIPT when the rebuild ran, whether or not it pushed. Every
|
|
29
30
|
* wrapper turns it into the module's --catalogue-rebuild, so the freshness step gates exactly where
|
|
30
|
-
* the rebuild ran
|
|
31
|
+
* the rebuild ran and the wrapper holds no condition of its own.
|
|
31
32
|
*/
|
|
32
33
|
export const REBUILD_RAN_MARKER = "sdd-catalogue-rebuild-ran";
|
|
33
34
|
/** What every wrapper appends to MODULE_CALL: --catalogue-rebuild when, and only when, the rebuild ran. */
|
|
@@ -37,8 +38,9 @@ const VERIFY_PIN = "@@VERIFY_CLI_VERSION@@";
|
|
|
37
38
|
// scan would read this text as an import of the CLI's own and refuse a dependency it does not declare.
|
|
38
39
|
const DAGGER_SDK = "@@DAGGER_SDK@@";
|
|
39
40
|
const MAINTAIN_PIN = "@@MAINTAIN_CLI_VERSION@@";
|
|
41
|
+
const OPENSPEC_PIN = "@@OPENSPEC_VERSION@@";
|
|
40
42
|
// The module's own source. String.raw keeps it byte-for-byte, so it holds no backtick; its only
|
|
41
|
-
// substitutions are the
|
|
43
|
+
// substitutions are the four markers above, which renderModuleFiles replaces.
|
|
42
44
|
const INDEX_TS = String.raw `/**
|
|
43
45
|
* Method Validation (M-ADR 0219): the method-managed validation job, as one Dagger module. Every CI
|
|
44
46
|
* host calls it, and none keeps a step list of its own. Written by sdd-cli sync and method-managed:
|
|
@@ -57,6 +59,8 @@ import { dag, Container, Directory, File, object, func, ReturnType } from ${DAGG
|
|
|
57
59
|
// Pinned, so a run is reproducible and every tool the validators shell out to is named here.
|
|
58
60
|
const NODE_IMAGE = "node:20-bookworm"
|
|
59
61
|
const YQ_VERSION = "v4.44.3"
|
|
62
|
+
// The behaviour-store checks run OpenSpec at exactly the version the method depends on (M-ADR 0223 ruling 2), telemetry off.
|
|
63
|
+
const OPENSPEC_VERSION = "${OPENSPEC_PIN}"
|
|
60
64
|
|
|
61
65
|
interface PlanStep {
|
|
62
66
|
id: string
|
|
@@ -69,8 +73,8 @@ interface PlanStep {
|
|
|
69
73
|
@object()
|
|
70
74
|
export class MethodValidation {
|
|
71
75
|
/**
|
|
72
|
-
* The toolchain: Node, git, bash, python3, ripgrep, a pinned yq,
|
|
73
|
-
* maintain CLIs. A tarball, when given, replaces the published package (a release candidate).
|
|
76
|
+
* The toolchain: Node, git, bash, python3, ripgrep, a pinned yq, the pinned OpenSpec CLI, and the
|
|
77
|
+
* exact-pinned verify and maintain CLIs. A tarball, when given, replaces the published package (a release candidate).
|
|
74
78
|
*/
|
|
75
79
|
@func()
|
|
76
80
|
toolchain(
|
|
@@ -99,7 +103,11 @@ export class MethodValidation {
|
|
|
99
103
|
c = c.withFile("/pkgs/maintain-cli.tgz", maintainTarball)
|
|
100
104
|
pkgs.push("/pkgs/maintain-cli.tgz")
|
|
101
105
|
} else pkgs.push("@sdd-method/maintain-cli@" + maintainVersion)
|
|
102
|
-
|
|
106
|
+
pkgs.push("@fission-ai/openspec@" + OPENSPEC_VERSION)
|
|
107
|
+
return c
|
|
108
|
+
.withEnvVariable("OPENSPEC_TELEMETRY", "0")
|
|
109
|
+
.withEnvVariable("DO_NOT_TRACK", "1")
|
|
110
|
+
.withExec(["npm", "install", "-g", "--silent", ...pkgs])
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
/**
|
|
@@ -130,10 +138,12 @@ export class MethodValidation {
|
|
|
130
138
|
for (const step of plan.steps) {
|
|
131
139
|
const ran = base.withExec([step.tool, ...step.args], { expect: ReturnType.Any })
|
|
132
140
|
const code = await ran.exitCode()
|
|
133
|
-
|
|
141
|
+
// A step that ran over an empty population (no behaviour store, say) reports N/A, never PASS.
|
|
142
|
+
const measuredNothing = code === 0 && /^\[(N\/A|SKIP)\] /m.test(await ran.stdout()) && !/^\[PASS\] (?!All validators)/m.test(await ran.stdout())
|
|
143
|
+
const verdict = code === 0 ? (measuredNothing ? "N/A" : "PASS") : step.advisory ? "ADVISORY" : "FAIL"
|
|
134
144
|
if (code !== 0 && !step.advisory) failed++
|
|
135
145
|
lines.push(verdict.padEnd(8) + " exit " + code + " " + step.id + " (" + step.title + ")")
|
|
136
|
-
if (code !== 0) {
|
|
146
|
+
if (code !== 0 || measuredNothing) {
|
|
137
147
|
const out = (await ran.stdout()) + (await ran.stderr())
|
|
138
148
|
lines.push(...out.trim().split("\n").slice(-6).map((l) => " " + l))
|
|
139
149
|
}
|
|
@@ -257,7 +267,7 @@ export function renderModuleFiles(pins = { verify: getVerifyCliVersion(), mainta
|
|
|
257
267
|
["catalogue-rebuild.sh", REBUILD_SH],
|
|
258
268
|
["dagger.json", DAGGER_JSON],
|
|
259
269
|
["package.json", PACKAGE_JSON],
|
|
260
|
-
["src/index.ts", INDEX_TS.split(VERIFY_PIN).join(pins.verify).split(MAINTAIN_PIN).join(pins.maintain).split(DAGGER_SDK).join('"@dagger.io' + '/dagger"')],
|
|
270
|
+
["src/index.ts", INDEX_TS.split(VERIFY_PIN).join(pins.verify).split(MAINTAIN_PIN).join(pins.maintain).split(OPENSPEC_PIN).join(OPENSPEC_VERSION).split(DAGGER_SDK).join('"@dagger.io' + '/dagger"')],
|
|
261
271
|
["tsconfig.json", TSCONFIG_JSON],
|
|
262
272
|
["yarn.lock", YARN_LOCK],
|
|
263
273
|
];
|
|
@@ -93,6 +93,8 @@ function buildMethodManagedBlock(profile, runner) {
|
|
|
93
93
|
// The branch itself when rebuilding, so the rebuild lands on it; otherwise the event's own ref.
|
|
94
94
|
" ref: ${{ steps.catalogue-token.outputs.token != '' && (github.head_ref || github.ref_name) || '' }}",
|
|
95
95
|
" persist-credentials: false",
|
|
96
|
+
// Full history: the behaviour-store stale check diffs living specs against each change's agreed commit.
|
|
97
|
+
" fetch-depth: 0",
|
|
96
98
|
" - name: Install Dagger",
|
|
97
99
|
` run: curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR="$RUNNER_TEMP/dagger" DAGGER_VERSION=${DAGGER_VERSION} sh && echo "$RUNNER_TEMP/dagger" >> "$GITHUB_PATH"`,
|
|
98
100
|
" - name: Rebuild catalogues",
|
package/dist/lib/lock-file.js
CHANGED
|
@@ -215,7 +215,7 @@ function parseVersioned(raw) {
|
|
|
215
215
|
// a YAML reader parses 113 of 325 entries in one real lock as MAPPINGS
|
|
216
216
|
// rather than strings — silently, with no error. Quoting on write is the
|
|
217
217
|
// fix; reading both forms ships first so no lock can be written that an
|
|
218
|
-
// older reader cannot parse.
|
|
218
|
+
// older reader cannot parse.
|
|
219
219
|
const entry = stripQuotes(line.slice(" - ".length));
|
|
220
220
|
const withDigest = DIGEST_ENTRY_RE.exec(entry);
|
|
221
221
|
if (withDigest) {
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* ENFORCEMENT (validate-link-closure.sh). A subset cannot satisfy closure
|
|
15
15
|
* without ceasing to be a subset.
|
|
16
16
|
*
|
|
17
|
-
* Parser hardening
|
|
18
|
-
*
|
|
17
|
+
* Parser hardening covers the four traps a real link audit hit, which are
|
|
18
|
+
* this module's controls: markdown
|
|
19
19
|
* targets containing SPACES (unencoded paths must not truncate at the first
|
|
20
|
-
* space), targets containing PARENTHESES (balance them; "…(
|
|
20
|
+
* space), targets containing PARENTHESES (balance them; "…(v2).md" broke
|
|
21
21
|
* every naive regex), URL-ENCODED targets (%20, %3A… decode before
|
|
22
22
|
* resolving), and never scanning outside the payload set (no cache dirs —
|
|
23
23
|
* guaranteed here by construction, since only staged payload paths are read).
|
|
@@ -83,7 +83,7 @@ export function getVerifyCliVersion() {
|
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* The @sdd-method/maintain-cli version released WITH this sdd-cli. The managed CI workflow's
|
|
86
|
-
* catalogue freshness step installs exactly this version (M-ADR 0218 §5
|
|
86
|
+
* catalogue freshness step installs exactly this version (M-ADR 0218 §5).
|
|
87
87
|
* Read the same way as the verify version, for the same reason (dist/ copy, never scripts/ live).
|
|
88
88
|
*/
|
|
89
89
|
export function getMaintainCliVersion() {
|
|
@@ -82,7 +82,7 @@ export async function readManifest(areaRoot, id) {
|
|
|
82
82
|
}
|
|
83
83
|
if (!Array.isArray(raw.members))
|
|
84
84
|
problems.push("members must be a list");
|
|
85
|
-
// A hand-written revision of digits only (`
|
|
85
|
+
// A hand-written revision of digits only (`12345678901`) parses as a NUMBER, and
|
|
86
86
|
// a strict comparison with the folder's string suffix then calls an unchanged
|
|
87
87
|
// member changed. Identity fields are strings, whatever YAML made of them.
|
|
88
88
|
for (const m of raw.members ?? []) {
|
|
@@ -90,7 +90,7 @@ export async function planAdrTransitions(bundleRootDir, targetRepo, previousVers
|
|
|
90
90
|
const protectedFromRemoval = [];
|
|
91
91
|
const movedPaths = [];
|
|
92
92
|
/**
|
|
93
|
-
* FIRST ADOPTION replays the bundle's whole transition history
|
|
93
|
+
* FIRST ADOPTION replays the bundle's whole transition history.
|
|
94
94
|
* A repo adopting a bundle for the first time receives its CURRENT state, so
|
|
95
95
|
* every transition in the log happened before this repo existed as an
|
|
96
96
|
* adopter — yet the log is filtered only by "newer than the last applied
|
package/dist/lib/sync/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
4
|
import { parse as parseYaml } from "yaml";
|
|
@@ -57,7 +58,7 @@ export async function loadSyncConfig(configPath) {
|
|
|
57
58
|
const rel = `${name}/${name}-${ver}.tar.gz`;
|
|
58
59
|
source = isUrl(resolvedBase)
|
|
59
60
|
? `${resolvedBase.replace(/\/$/, "")}/${rel}`
|
|
60
|
-
: resolve(resolvedBase, name, `${name}-${ver}
|
|
61
|
+
: localBundle(resolve(resolvedBase, name), `${name}-${ver}`);
|
|
61
62
|
}
|
|
62
63
|
else {
|
|
63
64
|
throw new Error(`Sync config ${absPath} bundles[${i}] missing required field: source ` +
|
|
@@ -69,4 +70,10 @@ export async function loadSyncConfig(configPath) {
|
|
|
69
70
|
? { version, bundles }
|
|
70
71
|
: { version, bundleSource, bundles };
|
|
71
72
|
}
|
|
73
|
+
// <dir>/<stem>.tar.gz, or <dir>/<stem>.tgz when only that exists (same bytes, another name).
|
|
74
|
+
function localBundle(dir, stem) {
|
|
75
|
+
const tarGz = resolve(dir, `${stem}.tar.gz`);
|
|
76
|
+
const tgz = resolve(dir, `${stem}.tgz`);
|
|
77
|
+
return !existsSync(tarGz) && existsSync(tgz) ? tgz : tarGz;
|
|
78
|
+
}
|
|
72
79
|
//# sourceMappingURL=config.js.map
|
package/dist/lib/sync/diff.js
CHANGED
|
@@ -154,7 +154,7 @@ export function computeRemovals(prevPaths, incomingPaths, options) {
|
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
156
|
* SHADOW COLLISIONS — an incoming managed file whose basename already exists at
|
|
157
|
-
* a SHALLOWER, unmanaged path in the adopter's repo
|
|
157
|
+
* a SHALLOWER, unmanaged path in the adopter's repo.
|
|
158
158
|
*
|
|
159
159
|
* When the distributed surface MOVES a file, copies previously delivered at the
|
|
160
160
|
* old location are stranded as unmanaged files. They keep resolving, so nothing
|
package/dist/lib/sync/index.js
CHANGED
|
@@ -102,7 +102,12 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
102
102
|
stdout(fail(`method-baseline.lock names ci_runner '${prevLock.ciRunner}', which is not a list of runner labels. Re-run with --ci-runner <label[,label]> or --ci-runner default.`));
|
|
103
103
|
return { exitCode: 1 };
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
// The local binding has no host to run on: a runner the lock recorded for an earlier host binding is
|
|
106
|
+
// dropped (and the lock line with it); only an explicit --ci-runner with local is refused.
|
|
107
|
+
const droppedRunner = ciBinding === "local" && opts.ciRunner === undefined && lockRunner !== undefined && lockRunner.length > 0;
|
|
108
|
+
const ciRunner = ciBinding === "local" && opts.ciRunner === undefined ? undefined : opts.ciRunner ?? lockRunner;
|
|
109
|
+
if (droppedRunner)
|
|
110
|
+
stdout(info(`ci_runner '${prevLock.ciRunner}' dropped: the local binding has no CI host to run on.`));
|
|
106
111
|
const lockSource = prevLock?.cliSource;
|
|
107
112
|
if (lockSource !== undefined && !CLI_SOURCES.includes(lockSource)) {
|
|
108
113
|
stdout(fail(`method-baseline.lock names cli_source '${lockSource}', which is not one of ${CLI_SOURCES.join(", ")}. Re-run with --cli-source <source> to choose one.`));
|
|
@@ -239,7 +244,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
239
244
|
if (cacheOnlyPaths.length > 0) {
|
|
240
245
|
stdout(info(`Cache-only (scaffold-source/registry) files: ${cacheOnlyPaths.length} — extracted to .sdd/bundle-cache, not materialised`));
|
|
241
246
|
}
|
|
242
|
-
// SHADOW-COLLISION FLAG (
|
|
247
|
+
// SHADOW-COLLISION FLAG (the durable move-rule's machinery).
|
|
243
248
|
// When the distributed surface MOVES a file, previously-delivered copies at
|
|
244
249
|
// the old location are stranded as unmanaged files: they keep resolving, so
|
|
245
250
|
// nothing breaks loudly, and they silently go stale as their managed twins
|
|
@@ -270,7 +275,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
270
275
|
}
|
|
271
276
|
}
|
|
272
277
|
stdout(info(`Conflicts: ${conflicts.length}`));
|
|
273
|
-
// RELABEL
|
|
278
|
+
// RELABEL: the raw count mixes two different things and an
|
|
274
279
|
// adopter's dry-run reported 31 where the artefacts on disk numbered 5.
|
|
275
280
|
// Nothing was wrong with the mechanism — the LABEL claimed deletions for
|
|
276
281
|
// lock entries whose paths were never materialised (cache-only payload
|