@sdd-method/maintain-cli 0.2.0 → 0.4.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 (85) hide show
  1. package/NOTICE +1 -1
  2. package/dist/lib/briefing/render.js +1 -2
  3. package/dist/lib/briefing/variables.js +1 -2
  4. package/dist/lib/catalogue/aggregate.js +38 -39
  5. package/dist/lib/catalogue/application-archetype/expectations.js +1 -1
  6. package/dist/lib/catalogue/archetype/wireframe.js +1 -1
  7. package/dist/lib/catalogue/build.js +17 -23
  8. package/dist/lib/catalogue/builders/application-conformance-coverage.js +1 -1
  9. package/dist/lib/catalogue/builders/c4-components.js +4 -5
  10. package/dist/lib/catalogue/builders/c4-containers.js +4 -4
  11. package/dist/lib/catalogue/builders/c4-external-systems.js +1 -1
  12. package/dist/lib/catalogue/builders/c4-l1-candidates.js +2 -2
  13. package/dist/lib/catalogue/builders/capabilities.js +2 -2
  14. package/dist/lib/catalogue/builders/capability-edges.js +4 -4
  15. package/dist/lib/catalogue/builders/capability-spec-walker.js +3 -3
  16. package/dist/lib/catalogue/builders/container-dependency.js +19 -20
  17. package/dist/lib/catalogue/builders/contracts.js +5 -6
  18. package/dist/lib/catalogue/builders/data-store-mapping.js +4 -4
  19. package/dist/lib/catalogue/builders/edge-label-merge.js +2 -2
  20. package/dist/lib/catalogue/builders/entities-manifest.js +1 -1
  21. package/dist/lib/catalogue/builders/entities-mermaid.js +3 -3
  22. package/dist/lib/catalogue/builders/entity-slug.js +11 -12
  23. package/dist/lib/catalogue/builders/event-catalog-walker.js +3 -3
  24. package/dist/lib/catalogue/builders/evidence-class.js +2 -2
  25. package/dist/lib/catalogue/builders/external-system-exemption.js +1 -1
  26. package/dist/lib/catalogue/builders/feature-contract.js +1 -3
  27. package/dist/lib/catalogue/builders/feature-grounding.js +1 -2
  28. package/dist/lib/catalogue/builders/feature-groups.js +0 -1
  29. package/dist/lib/catalogue/builders/feature-subscription-channels.js +2 -3
  30. package/dist/lib/catalogue/builders/feature-tree-walker.js +1 -1
  31. package/dist/lib/catalogue/builders/features.js +3 -4
  32. package/dist/lib/catalogue/builders/glossary-xref.js +1 -1
  33. package/dist/lib/catalogue/builders/persona-c4system.js +7 -7
  34. package/dist/lib/catalogue/builders/persona-container.js +14 -14
  35. package/dist/lib/catalogue/builders/personas.js +1 -2
  36. package/dist/lib/catalogue/builders/product-platform-dependencies.js +2 -2
  37. package/dist/lib/catalogue/builders/quality-scores.js +1 -1
  38. package/dist/lib/catalogue/builders/repositories.js +2 -2
  39. package/dist/lib/catalogue/builders/screen-tree.js +2 -2
  40. package/dist/lib/catalogue/builders/screens.js +3 -4
  41. package/dist/lib/catalogue/builders/system-external-dependency.js +7 -7
  42. package/dist/lib/catalogue/canonical-schema.yaml +23 -23
  43. package/dist/lib/catalogue/contract-path-resolution.js +3 -3
  44. package/dist/lib/catalogue/coverage-predicates.js +4 -4
  45. package/dist/lib/catalogue/coverage-run.js +1 -1
  46. package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
  47. package/dist/lib/catalogue/ext-id.js +4 -4
  48. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +1 -1
  49. package/dist/lib/catalogue/finding-identity.js +3 -3
  50. package/dist/lib/catalogue/freshness.js +1 -1
  51. package/dist/lib/catalogue/manifest-loader.js +2 -2
  52. package/dist/lib/catalogue/scaffold-delta.js +1 -1
  53. package/dist/lib/catalogue/schema-compat.js +1 -2
  54. package/dist/lib/catalogue/scores-coverage.js +1 -1
  55. package/dist/lib/catalogue/screen-validation-loader.js +1 -1
  56. package/dist/lib/catalogue/service-contract-aliases-loader.js +1 -1
  57. package/dist/lib/catalogue/staleness-verdicts.js +2 -2
  58. package/dist/lib/catalogue/traceability-loader.js +2 -3
  59. package/dist/lib/ci-binding/index.js +147 -35
  60. package/dist/lib/ci-binding/module.js +74 -2
  61. package/dist/lib/hooks-seed/entries.js +43 -21
  62. package/dist/lib/hooks-seed/index.js +1 -1
  63. package/dist/lib/init/ci-workflow.js +26 -2
  64. package/dist/lib/lock-file.js +11 -1
  65. package/dist/lib/pack/builder.js +56 -4
  66. package/dist/lib/pack/index.js +1 -0
  67. package/dist/lib/pack/publish-transform.js +1 -1
  68. package/dist/lib/pack/reference-closure.js +3 -3
  69. package/dist/lib/package-version.js +1 -1
  70. package/dist/lib/snapshot/manifest.js +1 -1
  71. package/dist/lib/sync/adr-transitions.js +1 -1
  72. package/dist/lib/sync/diff.js +1 -1
  73. package/dist/lib/sync/index.js +34 -5
  74. package/dist/lib/sync/settings-merge.js +1 -1
  75. package/dist/lib/validate/index.js +1 -1
  76. package/dist/lib/validate/plan.js +7 -4
  77. package/dist/lib/validate/yaml-parse.js +1 -1
  78. package/dist/maintain-program.js +1 -1
  79. package/dist/maintain-version.json +2 -2
  80. package/dist/verbs/pack-build.js +22 -0
  81. package/dist/verbs/sync.js +18 -1
  82. package/dist/verbs/validate.js +6 -1
  83. package/dist/verify-program.js +1 -1
  84. package/dist/verify-version.json +2 -2
  85. package/package.json +2 -6
@@ -69,7 +69,9 @@ export async function buildPack(opts) {
69
69
  // 4. Checksums (over the published payload)
70
70
  const checksums = await computeChecksums(payload, publishedPaths);
71
71
  // 5. Manifest + managed_paths + checksums files
72
- const gitInfo = await readGitInfo(opts.producerRepo);
72
+ const gitInfo = opts.provenance
73
+ ? { commit: opts.provenance.commit, branch: opts.provenance.branch }
74
+ : await readGitInfo(opts.producerRepo);
73
75
  await writeFile(join(root, "MANAGED_PATHS.txt"), publishedPaths.join("\n") + "\n");
74
76
  await writeFile(join(root, "CHECKSUMS.sha256"), checksums);
75
77
  await writeFile(join(root, "BUNDLE_MANIFEST.yaml"), renderManifest({
@@ -79,12 +81,13 @@ export async function buildPack(opts) {
79
81
  managedPathCount: publishedPaths.length,
80
82
  sourceCommit: gitInfo.commit,
81
83
  sourceBranch: gitInfo.branch,
84
+ ...(opts.provenance && { sourceRepo: opts.provenance.repo, generatedAt: opts.provenance.generatedAt }),
82
85
  }));
83
86
  // 6. tar -czf
84
87
  await mkdir(outDir, { recursive: true });
85
88
  const tarballName = `${rootName}.tar.gz`;
86
89
  const tarballPath = join(outDir, tarballName);
87
- await execFile("tar", ["-czf", tarballPath, "-C", stage, rootName]);
90
+ await writeArchive(tarballPath, stage, rootName, await archiveEpoch(opts));
88
91
  // Also emit a sidecar checksum file for the tarball itself
89
92
  const tarballHash = await sha256File(tarballPath);
90
93
  const sidecar = `${tarballHash} ${tarballName}\n`;
@@ -210,8 +213,8 @@ function renderManifest(ctx) {
210
213
  // sdd-method/orchestration/templates/bundle/BUNDLE_MANIFEST.yaml.hbs.
211
214
  // We don't go through a Handlebars engine because that would be the
212
215
  // only template-rendered file in sdd-cli; everything else is direct.
213
- const generatedAt = new Date().toISOString();
214
- const sourceRepo = deriveSourceRepoName(ctx.producerRepo);
216
+ const generatedAt = ctx.generatedAt ?? new Date().toISOString();
217
+ const sourceRepo = ctx.sourceRepo ?? deriveSourceRepoName(ctx.producerRepo);
215
218
  const lines = [];
216
219
  lines.push(`bundle_name: "${ctx.packName}"`);
217
220
  lines.push(`bundle_version: "v${ctx.seed.version}"`);
@@ -256,6 +259,55 @@ function renderManifest(ctx) {
256
259
  }
257
260
  return lines.join("\n") + "\n";
258
261
  }
262
+ /**
263
+ * Member file times: the release record's time, else SOURCE_DATE_EPOCH, else the producer's HEAD
264
+ * commit time, else now (not a git tree, no record). Two cuts of one commit then agree.
265
+ */
266
+ async function archiveEpoch(opts) {
267
+ if (opts.provenance)
268
+ return Math.floor(Date.parse(opts.provenance.generatedAt) / 1000);
269
+ const env = process.env.SOURCE_DATE_EPOCH;
270
+ if (env && /^\d+$/.test(env))
271
+ return Number(env);
272
+ try {
273
+ const { stdout } = await execFile("git", ["-C", opts.producerRepo, "log", "-1", "--format=%ct", "HEAD"]);
274
+ if (/^\d+$/.test(stdout.trim()))
275
+ return Number(stdout.trim());
276
+ }
277
+ catch {
278
+ // Not a git working tree.
279
+ }
280
+ return Math.floor(Date.now() / 1000);
281
+ }
282
+ /**
283
+ * Write the tarball deterministically with GNU tar: sorted members, owner and group zeroed, modes
284
+ * normalised, fixed file times; gzip -n, so the header carries no name or time. Plain `tar -czf` stamped all of these from the machine, so a rebuild never matched.
285
+ * Without GNU tar (e.g. macOS's bsdtar) the archive is written the old way and is not reproducible.
286
+ */
287
+ async function writeArchive(tarballPath, stage, rootName, epoch) {
288
+ let gnu = false;
289
+ try {
290
+ gnu = (await execFile("tar", ["--version"])).stdout.includes("GNU tar");
291
+ }
292
+ catch {
293
+ gnu = false;
294
+ }
295
+ if (!gnu) {
296
+ await execFile("tar", ["-czf", tarballPath, "-C", stage, rootName]);
297
+ return;
298
+ }
299
+ const tarPath = `${tarballPath}.tar`;
300
+ try {
301
+ await execFile("tar", ["--sort=name", "--format=gnu", `--mtime=@${epoch}`, "--owner=0", "--group=0", "--numeric-owner",
302
+ "--mode=u+rwX,go+rX,go-w", "-cf", tarPath, "-C", stage, rootName], { env: { ...process.env, LC_ALL: "C" } });
303
+ // GNU gzip, not Node's zlib: Node bundles a zlib fork whose output can depend on the CPU, and
304
+ // the method's own bundle builder compresses with gzip too.
305
+ await execFile("sh", ["-c", 'gzip -n -9 -c "$1" > "$2"', "sh", tarPath, tarballPath]);
306
+ }
307
+ finally {
308
+ await rm(tarPath, { force: true });
309
+ }
310
+ }
259
311
  /**
260
312
  * Read git provenance from the producer working tree. Mirrors the
261
313
  * behaviour of sdd-method/orchestration/scripts/build-method-baseline-
@@ -53,6 +53,7 @@ export async function runPackBuild(opts, stdout = (s) => process.stdout.write(s)
53
53
  }),
54
54
  ...(publishTransform && { publishTransform: true }),
55
55
  ...(opts.originPrefix === true && { mAdrPrefix: true }),
56
+ ...(opts.provenance && { provenance: opts.provenance }),
56
57
  });
57
58
  }
58
59
  catch (err) {
@@ -48,7 +48,7 @@ const DENY_TEMPORAL = [
48
48
  ];
49
49
  const DENY_PRODUCER = [
50
50
  /sdd-method#/i,
51
- /sdd-method-gov/i,
51
+ /sdd-method-g[o]v/i, // the class keeps the literal name out of this file, which ships
52
52
  /sdd-method repo/i,
53
53
  /the method repo/i,
54
54
  /governance repo/i,
@@ -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 comes from the estate that measured this problem — the
18
- * four traps their link audit hit are this module's controls: markdown
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; "…(ReBAC).md" broke
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; ruled 2026-10-15, sdm-jts.6).
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 (`50834756022`) parses as a NUMBER, and
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 (ledger §3ae).
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
@@ -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 (ledger §3ap).
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
@@ -11,6 +11,7 @@ import { cacheBundle, ensureCacheGitignored, splitConsumptionModes, } from "./bu
11
11
  import { PROTECTED_PREFIXES, allPristine, classifyConflicts, computeRemovals, detectShadowCollisions, digestManagedPaths, findConflicts, findMissingPayloadFiles, findProtectedPaths, } from "./diff.js";
12
12
  import { applyAdrRenames, findStrandedCitations, planAdrTransitions, } from "./adr-transitions.js";
13
13
  import { syncMethodManagedSettings } from "./settings-merge.js";
14
+ import { CLI_SOURCES, NPM_PACKAGE_NAME } from "../hooks-seed/index.js";
14
15
  import { syncBriefingFiles } from "./briefing-merge.js";
15
16
  import { DEFAULT_AGENT_HARNESS } from "../agent-harness.js";
16
17
  import { BLOCK_BEGIN, BLOCK_END } from "../init/ci-workflow.js";
@@ -101,7 +102,30 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
101
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.`));
102
103
  return { exitCode: 1 };
103
104
  }
104
- const ciRunner = opts.ciRunner ?? lockRunner;
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.`));
111
+ const lockSource = prevLock?.cliSource;
112
+ if (lockSource !== undefined && !CLI_SOURCES.includes(lockSource)) {
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.`));
114
+ return { exitCode: 1 };
115
+ }
116
+ const lockPackage = prevLock?.cliPackage;
117
+ if (lockPackage !== undefined && !NPM_PACKAGE_NAME.test(lockPackage)) {
118
+ stdout(fail(`method-baseline.lock names cli_package '${lockPackage}', which is not an npm package name. Re-run with --cli-package <name> or --cli-package default.`));
119
+ return { exitCode: 1 };
120
+ }
121
+ const source = opts.cliSource ?? lockSource ?? "registry";
122
+ if (source === "installed" && typeof opts.cliPackage === "string") {
123
+ stdout(fail("--cli-package names an npm package for the registry source; the installed source runs sdd-verify from PATH. Drop --cli-package or choose --cli-source registry."));
124
+ return { exitCode: 1 };
125
+ }
126
+ // The installed source has no package; switching to it drops a recorded name.
127
+ const cliPackage = source === "installed" ? undefined : opts.cliPackage === undefined ? lockPackage : (opts.cliPackage ?? undefined);
128
+ const cliSource = source === "installed" ? { source } : { source, ...(cliPackage ? { package: cliPackage } : {}) };
105
129
  const classification = await classifyConflicts(conflicts, opts.targetRepo, prevEntry?.pathDigests);
106
130
  if (conflicts.length > 0) {
107
131
  reportClassification(stdout, conflicts.length, classification, prevEntry?.bundleVersion);
@@ -220,7 +244,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
220
244
  if (cacheOnlyPaths.length > 0) {
221
245
  stdout(info(`Cache-only (scaffold-source/registry) files: ${cacheOnlyPaths.length} — extracted to .sdd/bundle-cache, not materialised`));
222
246
  }
223
- // SHADOW-COLLISION FLAG (ledger §3ap, the durable move-rule's machinery).
247
+ // SHADOW-COLLISION FLAG (the durable move-rule's machinery).
224
248
  // When the distributed surface MOVES a file, previously-delivered copies at
225
249
  // the old location are stranded as unmanaged files: they keep resolving, so
226
250
  // nothing breaks loudly, and they silently go stale as their managed twins
@@ -251,7 +275,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
251
275
  }
252
276
  }
253
277
  stdout(info(`Conflicts: ${conflicts.length}`));
254
- // RELABEL (ledger §3y.2): the raw count mixes two different things and an
278
+ // RELABEL: the raw count mixes two different things and an
255
279
  // adopter's dry-run reported 31 where the artefacts on disk numbered 5.
256
280
  // Nothing was wrong with the mechanism — the LABEL claimed deletions for
257
281
  // lock entries whose paths were never materialised (cache-only payload
@@ -336,6 +360,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
336
360
  targetRepo: opts.targetRepo,
337
361
  allowOverwrite: false,
338
362
  checkOnly: true,
363
+ cliSource,
339
364
  ...(opts.agentHarness ? { agentHarness: opts.agentHarness } : {}),
340
365
  });
341
366
  if (probe.refused) {
@@ -500,6 +525,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
500
525
  const settingsResult = await syncMethodManagedSettings({
501
526
  targetRepo: opts.targetRepo,
502
527
  allowOverwrite: opts.allowOverwrite || migrateFrom !== null,
528
+ cliSource,
503
529
  ...(opts.agentHarness ? { agentHarness: opts.agentHarness } : {}),
504
530
  });
505
531
  if (settingsResult.refused) {
@@ -552,15 +578,18 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
552
578
  for (const line of briefingResult.log)
553
579
  stdout(info(line));
554
580
  // Record the wrapper block and the module exactly as they now stand on disk, so the next sync
555
- // can tell them untouched from edited (M-ADR 0218 §4.7), and the binding when it is not the default.
581
+ // can tell them untouched from edited (M-ADR 0218 §4.7), and the binding and the CLI source
582
+ // when they are not the defaults.
556
583
  const nextLock = upsertBundle(migrateFrom ? removeBundle(prevLock, migrateFrom.name) : prevLock, newEntry);
557
- const { ciBinding: _previousBinding, ciRunner: _previousRunner, ciWorkflowBlockSha256: _previousSha, ...lockBase } = nextLock;
584
+ const { ciBinding: _previousBinding, ciRunner: _previousRunner, ciWorkflowBlockSha256: _previousSha, cliSource: _previousSource, cliPackage: _previousPackage, ...lockBase } = nextLock;
558
585
  await writeLockFile(opts.targetRepo, {
559
586
  ...lockBase,
560
587
  ...(ciBinding !== DEFAULT_CI_BINDING ? { ciBinding } : {}),
561
588
  ...(ciRunner !== undefined && ciRunner.length > 0 ? { ciRunner: ciRunner.join(",") } : {}),
562
589
  ...(ci.wrapperSha !== null ? { ciWorkflowBlockSha256: ci.wrapperSha } : {}),
563
590
  ciModuleSha256: ci.moduleSha,
591
+ ...(cliSource.source !== "registry" ? { cliSource: cliSource.source } : {}),
592
+ ...(cliPackage !== undefined ? { cliPackage } : {}),
564
593
  });
565
594
  stdout(pass(`Method baseline synced to: ${opts.targetRepo}`));
566
595
  stdout(info(`Updated lock file: method-baseline.lock (lock_version: ${LOCK_VERSION})`));
@@ -67,7 +67,7 @@ export async function syncMethodManagedSettings(opts) {
67
67
  const fileExisted = await fileExists(absPath);
68
68
  const existing = await readSettings(absPath);
69
69
  const hooksActive = opts.agentHarness === "claude-code";
70
- const ownedHooks = hooksActive ? buildMethodManagedHooks() : undefined;
70
+ const ownedHooks = hooksActive ? buildMethodManagedHooks(opts.cliSource) : undefined;
71
71
  const hooksDrifted = hooksActive && ownedHooks
72
72
  ? detectHooksDrift(normaliseOwnHookRenders(existing, ownedHooks), ownedHooks)
73
73
  : [];
@@ -54,7 +54,7 @@ export async function runValidate(opts, stdout = (line) => process.stdout.write(
54
54
  // passed" with none were previously the same line (M-ADR 0200 §4.3). The
55
55
  // exit code is deliberately unchanged — the defect is the claim, not the
56
56
  // gating; whether an unexecutable validator should FAIL is the open design
57
- // question (post-3.0.0-followons.md A2), not this one.
57
+ // question, not this one.
58
58
  function verdict(summary, selected) {
59
59
  if (summary.exitCode !== 0)
60
60
  return fail("One or more validators failed");
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The method-managed validation job, as DATA (bead sdm-dd6.2; Dagger ruling sdm-s48, 2026-10-15).
2
+ * The method-managed validation job, as DATA (M-ADR 0219).
3
3
  *
4
4
  * One ordered plan per profile: every default validator, the platform's script-only checks, then the
5
5
  * advisory catalogue freshness check. Every executor renders or runs THIS: the GitHub workflow
@@ -25,7 +25,7 @@ const TITLES = {
25
25
  // CI has no platform checkout, so this step runs the app-only checks; the met gate needs the platform beside it.
26
26
  "app-dependencies": "Validate app dependencies (app-only checks; no platform checkout in CI)",
27
27
  };
28
- export function methodValidationPlan(profile) {
28
+ export function methodValidationPlan(profile, opts = {}) {
29
29
  const steps = validatorsForProfile(profile)
30
30
  .filter((v) => v.optional !== true)
31
31
  .map((v) => ({
@@ -38,12 +38,15 @@ export function methodValidationPlan(profile) {
38
38
  if (profile === "platform") {
39
39
  steps.push({ id: "cascade-completeness", title: "Validate cascade completeness", tool: "bash", args: ["orchestration/scripts/validate-cascade-completeness.sh", "all"], advisory: false }, { id: "architecture-alignment", title: "Validate architecture alignment", tool: "bash", args: ["orchestration/scripts/validate-architecture-alignment.sh"], advisory: false });
40
40
  }
41
+ const gating = opts.catalogueRebuild === true;
41
42
  steps.push({
42
43
  id: "catalogue-freshness",
43
- title: "Check catalogues reproduce from the specs (advisory)",
44
+ title: gating
45
+ ? "Check catalogues reproduce from the specs (gating: the pipeline rebuilt them)"
46
+ : "Check catalogues reproduce from the specs (advisory)",
44
47
  tool: "sdd-maintain",
45
48
  args: ["catalogue", "build", "--check", "--sdd-path", "."],
46
- advisory: true,
49
+ advisory: !gating,
47
50
  });
48
51
  return steps;
49
52
  }
@@ -10,7 +10,7 @@
10
10
  // with it certifies exactly the corruption a line-surgery transformer
11
11
  // produces. `yaml` v2 refuses duplicate keys (DUPLICATE_KEY) and the other
12
12
  // malformations a lenient parse glosses — e.g. a `: ` inside an unquoted
13
- // scalar (`path: (none — tel: URL)`), which no pattern rule catches.
13
+ // scalar (`path: (none — see: docs)`), which no pattern rule catches.
14
14
  //
15
15
  // Scope: the YAML files git TRACKS (`*.yaml`, `*.yml`) — the repo's sources,
16
16
  // not build output or dependencies. Outside a git work tree there is no
@@ -24,7 +24,7 @@ import { registerGenerateBriefing } from "./verbs/generate-briefing.js";
24
24
  import { registerPackBuild } from "./verbs/pack-build.js";
25
25
  import { registerRender } from "./verbs/render.js";
26
26
  import { registerSync } from "./verbs/sync.js";
27
- const SPEC_URL = "https://github.com/tomfosteruk/sdd-method/blob/main/docs/method/cli/sdd-cli-specification.md";
27
+ const SPEC_URL = "docs/method/cli/sdd-cli-specification.md (in the method distribution)";
28
28
  /** The verbs maintain adds to verify, as `<verb>` or `<verb> <subcommand>`. Asserted by test. */
29
29
  export const MAINTAIN_ONLY_SURFACE = [
30
30
  "sync",
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.2.0",
3
- "built_against_sdd_cli": "1.43.0"
2
+ "version": "0.4.0",
3
+ "built_against_sdd_cli": "1.45.0"
4
4
  }
@@ -13,7 +13,21 @@ export function registerPackBuild(pack) {
13
13
  .option("--forward-base-managed-paths <path>", "path to forward-base MANAGED_PATHS.txt for the leakage check (defaults to extracting from method-baseline.lock)")
14
14
  .option("--publish-transform", "apply the point-in-time / producer-agnostic publish transform (ADR 0183/0184): drop non-Accepted ADRs, strip lineage + stripped-class fields, flatten cross-repo refs, fail closed on leaks. For producer-published bundles.")
15
15
  .option("--origin-prefix", "with the publish transform, apply the range-aware origin prefix (ADR 0140 §4.1): 0NNN→M-ADR, 9NNN→RA-ADR, adopter 1NNN stays ADR; ADR files renamed to match. Implies --publish-transform. The RA-ADR bundle cutover switch.")
16
+ .option("--source-commit <sha>", "provenance from a release record: the commit the manifest records (with --source-branch, --source-repo and --generated-at)")
17
+ .option("--source-branch <name>", "provenance from a release record: the branch the manifest records")
18
+ .option("--source-repo <name>", "provenance from a release record: the repository name the manifest records, whatever the checkout is called")
19
+ .option("--generated-at <utc>", "provenance from a release record: the manifest's generated_at_utc (YYYY-MM-DDTHH:MM:SSZ), also every archive member's file time")
16
20
  .action(async (opts) => {
21
+ const rec = [opts.sourceCommit, opts.sourceBranch, opts.sourceRepo, opts.generatedAt];
22
+ const given = rec.filter((v) => v !== undefined).length;
23
+ if (given !== 0 && given !== 4) {
24
+ process.stderr.write(`--source-commit, --source-branch, --source-repo and --generated-at go together (got ${given} of 4)\n`);
25
+ process.exit(2);
26
+ }
27
+ if (opts.generatedAt !== undefined && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(opts.generatedAt)) {
28
+ process.stderr.write(`--generated-at must be UTC, YYYY-MM-DDTHH:MM:SSZ (got '${opts.generatedAt}')\n`);
29
+ process.exit(2);
30
+ }
17
31
  const { exitCode } = await runPackBuild({
18
32
  seedPath: opts.seed,
19
33
  producerRepo: opts.producerRepo ?? process.cwd(),
@@ -23,6 +37,14 @@ export function registerPackBuild(pack) {
23
37
  }),
24
38
  ...(opts.publishTransform && { publishTransform: true }),
25
39
  ...(opts.originPrefix && { originPrefix: true }),
40
+ ...(given === 4 && {
41
+ provenance: {
42
+ commit: opts.sourceCommit,
43
+ branch: opts.sourceBranch,
44
+ repo: opts.sourceRepo,
45
+ generatedAt: opts.generatedAt,
46
+ },
47
+ }),
26
48
  });
27
49
  if (exitCode !== 0)
28
50
  process.exit(exitCode);
@@ -3,6 +3,7 @@ import { loadSyncConfig } from "../lib/sync/config.js";
3
3
  import { materialiseBundle } from "../lib/sync/bundle-source.js";
4
4
  import { runSync } from "../lib/sync/index.js";
5
5
  import { CI_BINDINGS, CI_RUNNER_DEFAULT, isCiBinding, parseCiRunner } from "../lib/ci-binding/index.js";
6
+ import { CLI_SOURCES, DEFAULT_VERIFY_CLI_PACKAGE, NPM_PACKAGE_NAME } from "../lib/hooks-seed/index.js";
6
7
  import { resolvePathOption } from "./path-option.js";
7
8
  export function registerSync(program) {
8
9
  program
@@ -17,8 +18,10 @@ export function registerSync(program) {
17
18
  .option("--allow-overwrite", "accept overwrites of locally-modified method content")
18
19
  .option("--no-delete", "preserve files removed upstream")
19
20
  .option("--agent-harness <name>", `target agent harness for hooks + briefing. Default: claude-code. Supported: ${AGENT_HARNESSES.join(", ")}`)
20
- .option("--ci-binding <host>", `CI host whose wrapper calls the Method Validation module (M-ADR 0219): ${CI_BINDINGS.join(", ")}. Default: the binding the lock records, else github; a new choice is recorded in the lock`)
21
+ .option("--ci-binding <host>", `CI host whose wrapper calls the Method Validation module (M-ADR 0219): ${CI_BINDINGS.join(", ")}. local writes the module and no wrapper, for an estate that runs it with dagger call on its own machines, and removes the wrapper this tool wrote for the previous binding while it is unedited. Default: the binding the lock records, else github; a new choice is recorded in the lock`)
21
22
  .option("--ci-runner <labels>", `runner label(s) the CI wrapper targets, comma-separated (GitHub runs-on, GitLab tags, Jenkins agent label), e.g. a self-hosted runner. '${CI_RUNNER_DEFAULT}' returns to the host's default runner. Default: what the lock records; a new choice is recorded`)
23
+ .option("--cli-source <source>", `where the session-start hook finds the verify CLI: ${CLI_SOURCES.join(" (npx at the pinned version) or ")} (sdd-verify from PATH, for an org that installs the release's CLI tarballs; the hook names the version to install when it differs). Default: what the lock records, else registry; a new choice is recorded`)
24
+ .option("--cli-package <name>", `the verify CLI's npm package under the registry source, for a registry that rescopes it. 'default' returns to ${DEFAULT_VERIFY_CLI_PACKAGE}. Default: what the lock records; a new choice is recorded`)
22
25
  .action(async (opts) => {
23
26
  if (opts.ciBinding !== undefined && !isCiBinding(opts.ciBinding)) {
24
27
  process.stderr.write(`[FAIL] --ci-binding must be one of ${CI_BINDINGS.join(", ")}; got '${opts.ciBinding}'\n`);
@@ -34,6 +37,16 @@ export function registerSync(program) {
34
37
  }
35
38
  ciRunner = parsed;
36
39
  }
40
+ if (opts.cliSource !== undefined && !CLI_SOURCES.includes(opts.cliSource)) {
41
+ process.stderr.write(`[FAIL] --cli-source must be one of ${CLI_SOURCES.join(", ")}; got '${opts.cliSource}'\n`);
42
+ process.exit(1);
43
+ }
44
+ const cliSource = opts.cliSource;
45
+ if (opts.cliPackage !== undefined && opts.cliPackage !== "default" && !NPM_PACKAGE_NAME.test(opts.cliPackage)) {
46
+ process.stderr.write(`[FAIL] --cli-package takes an npm package name, or 'default'; got '${opts.cliPackage}'\n`);
47
+ process.exit(1);
48
+ }
49
+ const cliPackage = opts.cliPackage === undefined ? undefined : opts.cliPackage === "default" ? null : opts.cliPackage;
37
50
  let agentHarness;
38
51
  try {
39
52
  agentHarness = resolveAgentHarness(opts.agentHarness);
@@ -61,6 +74,8 @@ export function registerSync(program) {
61
74
  agentHarness,
62
75
  ...(ciBinding ? { ciBinding } : {}),
63
76
  ...(ciRunner ? { ciRunner } : {}),
77
+ ...(cliSource ? { cliSource } : {}),
78
+ ...(cliPackage !== undefined ? { cliPackage } : {}),
64
79
  ...(opts.replaceBundle ? { replaceBundle: opts.replaceBundle } : {}),
65
80
  ...(opts.yes === true ? { yes: true } : {}),
66
81
  });
@@ -90,6 +105,8 @@ export function registerSync(program) {
90
105
  agentHarness,
91
106
  ...(ciBinding ? { ciBinding } : {}),
92
107
  ...(ciRunner ? { ciRunner } : {}),
108
+ ...(cliSource ? { cliSource } : {}),
109
+ ...(cliPackage !== undefined ? { cliPackage } : {}),
93
110
  });
94
111
  if (exitCode !== 0) {
95
112
  await resolved.cleanup();
@@ -14,7 +14,12 @@ export function registerValidate(program) {
14
14
  .option("--strict", "treat warnings as failures")
15
15
  .option("--list", "print the method-managed validation job for this repo's profile as JSON (the plan every CI executor runs, the Dagger module included) and run nothing")
16
16
  .option("--profile <profile>", "with --list: the profile to list, instead of reading .sdd-repo-kind (platform | application | integration)")
17
+ .option("--catalogue-rebuild", "with --list: the pipeline rebuilt the catalogues on this run (M-ADR 0220 §4.4), so the catalogue freshness step gates instead of advising")
17
18
  .action(async (opts) => {
19
+ if (opts.catalogueRebuild === true && opts.list !== true) {
20
+ process.stderr.write("--catalogue-rebuild only applies with --list\n");
21
+ process.exit(2);
22
+ }
18
23
  if (opts.list === true) {
19
24
  let profile;
20
25
  if (opts.profile !== undefined) {
@@ -32,7 +37,7 @@ export function registerValidate(program) {
32
37
  }
33
38
  profile = profileFromRepoKind(kind);
34
39
  }
35
- process.stdout.write(JSON.stringify({ profile, steps: methodValidationPlan(profile) }, null, 2) + "\n");
40
+ process.stdout.write(JSON.stringify({ profile, steps: methodValidationPlan(profile, { catalogueRebuild: opts.catalogueRebuild === true }) }, null, 2) + "\n");
36
41
  return;
37
42
  }
38
43
  const { exitCode } = await runValidate({
@@ -29,7 +29,7 @@ import { registerCatalogueBaselineCheck, registerCatalogueImpact, registerCatalo
29
29
  import { registerList } from "./verbs/list.js";
30
30
  import { registerPackValidate } from "./verbs/pack-validate.js";
31
31
  import { registerValidate } from "./verbs/validate.js";
32
- const SPEC_URL = "https://github.com/tomfosteruk/sdd-method/blob/main/docs/method/cli/sdd-cli-specification.md";
32
+ const SPEC_URL = "docs/method/cli/sdd-cli-specification.md (in the method distribution)";
33
33
  /** The full surface, as `<verb>` or `<verb> <subcommand>`. Asserted by test. */
34
34
  export const VERIFY_SURFACE = [
35
35
  "validate",
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.2.0",
3
- "built_against_sdd_cli": "1.43.0"
2
+ "version": "1.4.0",
3
+ "built_against_sdd_cli": "1.45.0"
4
4
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sdd-method/maintain-cli",
3
- "version": "0.2.0",
4
- "sddCliVersion": "1.43.0",
3
+ "version": "0.4.0",
4
+ "sddCliVersion": "1.45.0",
5
5
  "description": "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.",
6
6
  "license": "Apache-2.0",
7
7
  "type": "module",
@@ -17,10 +17,6 @@
17
17
  "zod": "^4.3.6",
18
18
  "@duckdb/node-api": "^1.5.5-r.3"
19
19
  },
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/tomfosteruk/sdd-cli.git"
23
- },
24
20
  "publishConfig": {
25
21
  "access": "public"
26
22
  },