@sdd-method/maintain-cli 0.0.0-stage → 0.1.0-rc.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 (206) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +17 -0
  3. package/README.md +6 -2
  4. package/dist/lib/adr-origin.js +59 -0
  5. package/dist/lib/agent-harness.js +28 -0
  6. package/dist/lib/briefing/index.js +13 -0
  7. package/dist/lib/briefing/render.js +123 -0
  8. package/dist/lib/briefing/variables.js +92 -0
  9. package/dist/lib/bundle.js +123 -0
  10. package/dist/lib/catalogue/aggregate.js +2404 -0
  11. package/dist/lib/catalogue/aggregation-manifest-loader.js +173 -0
  12. package/dist/lib/catalogue/application-archetype/expectations.js +119 -0
  13. package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
  14. package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
  15. package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
  16. package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
  17. package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
  18. package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
  19. package/dist/lib/catalogue/archetype/resolver.js +103 -0
  20. package/dist/lib/catalogue/archetype/wireframe.js +418 -0
  21. package/dist/lib/catalogue/baseline-check.js +151 -0
  22. package/dist/lib/catalogue/build.js +1502 -0
  23. package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
  24. package/dist/lib/catalogue/builders/adrs.js +222 -0
  25. package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
  26. package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
  27. package/dist/lib/catalogue/builders/c4-components.js +229 -0
  28. package/dist/lib/catalogue/builders/c4-containers.js +240 -0
  29. package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
  30. package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
  31. package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
  32. package/dist/lib/catalogue/builders/c4-systems.js +98 -0
  33. package/dist/lib/catalogue/builders/capabilities.js +488 -0
  34. package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
  35. package/dist/lib/catalogue/builders/capability-edges.js +256 -0
  36. package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
  37. package/dist/lib/catalogue/builders/capability-spec.js +348 -0
  38. package/dist/lib/catalogue/builders/container-dependency.js +183 -0
  39. package/dist/lib/catalogue/builders/contracts.js +402 -0
  40. package/dist/lib/catalogue/builders/data-items.js +344 -0
  41. package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
  42. package/dist/lib/catalogue/builders/domain-models.js +175 -0
  43. package/dist/lib/catalogue/builders/domains.js +119 -0
  44. package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
  45. package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
  46. package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
  47. package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
  48. package/dist/lib/catalogue/builders/entity-slug.js +47 -0
  49. package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
  50. package/dist/lib/catalogue/builders/event-edges.js +327 -0
  51. package/dist/lib/catalogue/builders/events.js +168 -0
  52. package/dist/lib/catalogue/builders/evidence-class.js +43 -0
  53. package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
  54. package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
  55. package/dist/lib/catalogue/builders/feature-contract.js +77 -0
  56. package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
  57. package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
  58. package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
  59. package/dist/lib/catalogue/builders/feature-groups.js +63 -0
  60. package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
  61. package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
  62. package/dist/lib/catalogue/builders/features.js +300 -0
  63. package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
  64. package/dist/lib/catalogue/builders/glossary.js +204 -0
  65. package/dist/lib/catalogue/builders/implementations.js +214 -0
  66. package/dist/lib/catalogue/builders/index.js +64 -0
  67. package/dist/lib/catalogue/builders/integration-components.js +344 -0
  68. package/dist/lib/catalogue/builders/integrations.js +181 -0
  69. package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
  70. package/dist/lib/catalogue/builders/persona-container.js +133 -0
  71. package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
  72. package/dist/lib/catalogue/builders/personas.js +152 -0
  73. package/dist/lib/catalogue/builders/product-edges.js +106 -0
  74. package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
  75. package/dist/lib/catalogue/builders/products.js +162 -0
  76. package/dist/lib/catalogue/builders/quality-scores.js +635 -0
  77. package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
  78. package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
  79. package/dist/lib/catalogue/builders/repo-edges.js +340 -0
  80. package/dist/lib/catalogue/builders/repositories.js +458 -0
  81. package/dist/lib/catalogue/builders/scaffolds.js +197 -0
  82. package/dist/lib/catalogue/builders/screen-tree.js +222 -0
  83. package/dist/lib/catalogue/builders/screens.js +396 -0
  84. package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
  85. package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
  86. package/dist/lib/catalogue/builders/service-contract.js +74 -0
  87. package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
  88. package/dist/lib/catalogue/builders/services.js +232 -0
  89. package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
  90. package/dist/lib/catalogue/builders/user-journeys.js +253 -0
  91. package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
  92. package/dist/lib/catalogue/cli-version.js +10 -0
  93. package/dist/lib/catalogue/contract-id.js +49 -0
  94. package/dist/lib/catalogue/contract-path-resolution.js +149 -0
  95. package/dist/lib/catalogue/coverage-predicates.js +603 -0
  96. package/dist/lib/catalogue/coverage-render.js +552 -0
  97. package/dist/lib/catalogue/coverage-run.js +115 -0
  98. package/dist/lib/catalogue/csv-reader.js +97 -0
  99. package/dist/lib/catalogue/csv-writer.js +96 -0
  100. package/dist/lib/catalogue/data-item-coverage.js +142 -0
  101. package/dist/lib/catalogue/data-plane-coherence.js +77 -0
  102. package/dist/lib/catalogue/ddl-generator.js +233 -0
  103. package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
  104. package/dist/lib/catalogue/engine-validator.js +239 -0
  105. package/dist/lib/catalogue/ext-id.js +76 -0
  106. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
  107. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
  108. package/dist/lib/catalogue/finding-identity.js +183 -0
  109. package/dist/lib/catalogue/findings.js +46 -0
  110. package/dist/lib/catalogue/fk-validator.js +136 -0
  111. package/dist/lib/catalogue/freshness.js +69 -0
  112. package/dist/lib/catalogue/gate-checks.js +338 -0
  113. package/dist/lib/catalogue/graph-projection.js +346 -0
  114. package/dist/lib/catalogue/graph-run.js +64 -0
  115. package/dist/lib/catalogue/impact-run.js +228 -0
  116. package/dist/lib/catalogue/manifest-loader.js +435 -0
  117. package/dist/lib/catalogue/platform-scope.js +7 -0
  118. package/dist/lib/catalogue/population-checks.js +87 -0
  119. package/dist/lib/catalogue/relationship-semantics.js +66 -0
  120. package/dist/lib/catalogue/reverse-product-quality.js +99 -0
  121. package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
  122. package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
  123. package/dist/lib/catalogue/scaffold-delta.js +261 -0
  124. package/dist/lib/catalogue/schema-compat.js +52 -0
  125. package/dist/lib/catalogue/schema-loader.js +87 -0
  126. package/dist/lib/catalogue/scores-coverage.js +573 -0
  127. package/dist/lib/catalogue/screen-validation-loader.js +89 -0
  128. package/dist/lib/catalogue/service-architecture-parser.js +193 -0
  129. package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
  130. package/dist/lib/catalogue/staleness-verdicts.js +345 -0
  131. package/dist/lib/catalogue/traceability-loader.js +350 -0
  132. package/dist/lib/catalogue/validate.js +200 -0
  133. package/dist/lib/catalogue/version.js +18 -0
  134. package/dist/lib/ci-binding/index.js +209 -0
  135. package/dist/lib/ci-binding/module.js +204 -0
  136. package/dist/lib/conformance/binding.js +70 -0
  137. package/dist/lib/context-pull/index.js +151 -0
  138. package/dist/lib/context-pull/resolve.js +70 -0
  139. package/dist/lib/generate-briefing/index.js +77 -0
  140. package/dist/lib/hooks-seed/copilot.js +167 -0
  141. package/dist/lib/hooks-seed/entries.js +47 -0
  142. package/dist/lib/hooks-seed/index.js +17 -0
  143. package/dist/lib/hooks-seed/lifecycle-mapping.js +28 -0
  144. package/dist/lib/hooks-seed/merge.js +128 -0
  145. package/dist/lib/hooks-seed/types.js +20 -0
  146. package/dist/lib/init/ci-workflow.js +89 -0
  147. package/dist/lib/list/discover.js +100 -0
  148. package/dist/lib/list/format.js +84 -0
  149. package/dist/lib/list/index.js +16 -0
  150. package/dist/lib/lock-file.js +432 -0
  151. package/dist/lib/mcp-seed/entries.js +14 -0
  152. package/dist/lib/mcp-seed/index.js +28 -0
  153. package/dist/lib/mcp-seed/merge.js +99 -0
  154. package/dist/lib/mcp-seed/types.js +13 -0
  155. package/dist/lib/pack/builder.js +303 -0
  156. package/dist/lib/pack/index.js +87 -0
  157. package/dist/lib/pack/leakage.js +144 -0
  158. package/dist/lib/pack/log.js +18 -0
  159. package/dist/lib/pack/publish-transform.js +233 -0
  160. package/dist/lib/pack/reference-closure.js +125 -0
  161. package/dist/lib/pack/schema.js +121 -0
  162. package/dist/lib/pack/seed.js +182 -0
  163. package/dist/lib/pack/validate.js +49 -0
  164. package/dist/lib/package-version.js +92 -0
  165. package/dist/lib/repo-kind.js +129 -0
  166. package/dist/lib/scripts.js +7 -0
  167. package/dist/lib/snapshot/catalogue-diff.js +219 -0
  168. package/dist/lib/snapshot/manifest.js +206 -0
  169. package/dist/lib/sync/adr-transitions.js +250 -0
  170. package/dist/lib/sync/apply.js +91 -0
  171. package/dist/lib/sync/briefing-merge.js +90 -0
  172. package/dist/lib/sync/bundle-cache.js +109 -0
  173. package/dist/lib/sync/bundle-source.js +61 -0
  174. package/dist/lib/sync/config.js +72 -0
  175. package/dist/lib/sync/copilot-hooks-merge.js +108 -0
  176. package/dist/lib/sync/diff.js +251 -0
  177. package/dist/lib/sync/index.js +809 -0
  178. package/dist/lib/sync/settings-merge.js +216 -0
  179. package/dist/lib/sync/yaml-block-merge.js +120 -0
  180. package/dist/lib/validate/aggregate.js +51 -0
  181. package/dist/lib/validate/bundle-coherence.js +74 -0
  182. package/dist/lib/validate/dispatch.js +243 -0
  183. package/dist/lib/validate/index.js +110 -0
  184. package/dist/lib/validate/plan.js +54 -0
  185. package/dist/lib/validate/runner.js +85 -0
  186. package/dist/lib/validate/yaml-parse.js +112 -0
  187. package/dist/maintain-bin.js +8 -0
  188. package/dist/maintain-program.js +91 -0
  189. package/dist/maintain-version.json +4 -0
  190. package/dist/verbs/catalogue-maintain.js +110 -0
  191. package/dist/verbs/catalogue-verify.js +114 -0
  192. package/dist/verbs/context.js +39 -0
  193. package/dist/verbs/generate-briefing.js +29 -0
  194. package/dist/verbs/list.js +45 -0
  195. package/dist/verbs/pack-build.js +31 -0
  196. package/dist/verbs/pack-validate.js +17 -0
  197. package/dist/verbs/path-option.js +25 -0
  198. package/dist/verbs/render.js +87 -0
  199. package/dist/verbs/sync.js +98 -0
  200. package/dist/verbs/validate.js +48 -0
  201. package/dist/verify-program.js +122 -0
  202. package/dist/verify-version.json +4 -0
  203. package/package.json +32 -4
  204. package/scripts/validate-app-dependencies.sh +664 -0
  205. package/scripts/validate-app-docs.sh +199 -0
  206. package/scripts/validate-integration-docs.sh +505 -0
@@ -0,0 +1,809 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { access, readFile } from "node:fs/promises";
4
+ import { loadBundle } from "../bundle.js";
5
+ import { getSddCliVersion } from "../package-version.js";
6
+ import { LOCK_VERSION, findBundle, findForwardBase, hasForwardBase, pathToBundleMap, readLockFile, removeBundle, upsertBundle, writeLockFile, } from "../lock-file.js";
7
+ import { REPO_KIND_FILE, profileFromBundleName, repoKindFromProfile, writeRepoKind, } from "../repo-kind.js";
8
+ import { join } from "node:path";
9
+ import { copyManagedFiles, removeManagedFiles } from "./apply.js";
10
+ import { cacheBundle, ensureCacheGitignored, splitConsumptionModes, } from "./bundle-cache.js";
11
+ import { PROTECTED_PREFIXES, allPristine, classifyConflicts, computeRemovals, detectShadowCollisions, digestManagedPaths, findConflicts, findMissingPayloadFiles, findProtectedPaths, } from "./diff.js";
12
+ import { applyAdrRenames, findStrandedCitations, planAdrTransitions, } from "./adr-transitions.js";
13
+ import { syncMethodManagedSettings } from "./settings-merge.js";
14
+ import { syncMethodManagedCopilotHooks } from "./copilot-hooks-merge.js";
15
+ import { syncBriefingFiles } from "./briefing-merge.js";
16
+ import { DEFAULT_AGENT_HARNESS } from "../agent-harness.js";
17
+ import { BLOCK_BEGIN, BLOCK_END } from "../init/ci-workflow.js";
18
+ import { blockSha256, CI_BINDINGS, DEFAULT_CI_BINDING, isCiBinding, syncCiBinding, } from "../ci-binding/index.js";
19
+ import { BRIEFING_TEMPLATE_ROOT, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, harnessWrapperOutputPath, } from "../briefing/index.js";
20
+ const SECTION_RULE = "═".repeat(59);
21
+ /**
22
+ * Report the pristine/edited/unclassifiable split for a conflict set.
23
+ *
24
+ * The message this replaces said only "Found N conflicting managed file(s)",
25
+ * which reads as "you have local edits at risk" — and twice the truth was the
26
+ * opposite. Each branch below states what is actually known, and the
27
+ * undecidable case says so plainly rather than defaulting to alarm.
28
+ */
29
+ function reportClassification(stdout, total, c, priorVersion) {
30
+ const from = priorVersion ? ` (match last-applied ${priorVersion})` : "";
31
+ if (c.edited.length === 0 && c.unclassifiable.length === 0) {
32
+ stdout(pass(`${total} differ, ${c.pristine.length} pristine${from} — safe to apply, ` +
33
+ `--allow-overwrite not required`));
34
+ return;
35
+ }
36
+ if (c.unclassifiable.length === total) {
37
+ // The accepted non-retroactive case. Silence here would recreate exactly the
38
+ // "you have local edits at risk" misread this work exists to remove.
39
+ stdout(info(`${total} differ, cannot classify: no per-path digests recorded` +
40
+ (priorVersion ? ` for ${priorVersion}` : "") +
41
+ ` — re-run after one sync to enable pristine detection`));
42
+ return;
43
+ }
44
+ stdout(info(`${total} differ: ${c.pristine.length} pristine${from}, ` +
45
+ `${c.edited.length} locally edited, ${c.unclassifiable.length} not classifiable`));
46
+ }
47
+ export async function runSync(opts, stdout = (line) => process.stdout.write(line)) {
48
+ if (!(await exists(opts.bundle))) {
49
+ stdout(fail(`Bundle not found: ${opts.bundle}`));
50
+ return { exitCode: 1 };
51
+ }
52
+ if (!(await exists(opts.targetRepo))) {
53
+ stdout(fail(`Target repo not found: ${opts.targetRepo}`));
54
+ return { exitCode: 1 };
55
+ }
56
+ stdout(section("Sync Method Baseline"));
57
+ const bundle = await loadBundle(opts.bundle);
58
+ try {
59
+ // Enforce minimum sdd-cli version declared by the bundle manifest.
60
+ // Bundles built before v0.13.2 omit the field; for those, no check
61
+ // applies. Forward-baseline bundles starting at v0.6.0 (sdd-method
62
+ // v0.9.0, schema relocation) declare a minimum so adopters whose
63
+ // installed sdd-cli predates the relocation cannot apply a bundle
64
+ // that depends on schema-from-sdd-cli rather than schema-from-bundle.
65
+ const minVersionError = checkMinimumSddCliVersion(bundle.manifest);
66
+ if (minVersionError !== null) {
67
+ stdout(fail(minVersionError));
68
+ return { exitCode: 1 };
69
+ }
70
+ if (bundle.manifest.minimumSddCliVersion) {
71
+ stdout(info(`sdd-cli ${getSddCliVersion()} satisfies bundle minimum ${bundle.manifest.minimumSddCliVersion}`));
72
+ }
73
+ const missing = await findMissingPayloadFiles(bundle.managedPaths, bundle.payloadDir);
74
+ if (missing.length > 0) {
75
+ stdout(fail(`Missing payload file listed in MANAGED_PATHS.txt: ${missing[0]}`));
76
+ return { exitCode: 1 };
77
+ }
78
+ // ADR 0176 §4.3: split consumption modes. Scaffold-source paths and
79
+ // the registry file extract to the bundle cache only; everything
80
+ // else materialises into the repo tree as before.
81
+ const { materialise: materialisePaths, cacheOnly: cacheOnlyPaths } = splitConsumptionModes(bundle.managedPaths, bundle.manifest.scaffoldSourcePaths);
82
+ const protectedIncoming = findProtectedPaths(bundle.managedPaths);
83
+ if (protectedIncoming.length > 0) {
84
+ stdout(fail(`Incoming bundle attempts to manage protected product-owned path: ${protectedIncoming[0]}`));
85
+ stdout(fail(`Method baseline bundles must not manage files under: ${PROTECTED_PREFIXES.join(" ")}`));
86
+ return { exitCode: 1 };
87
+ }
88
+ const conflicts = await findConflicts(materialisePaths, bundle.payloadDir, opts.targetRepo);
89
+ // Read the lock BEFORE gating on conflicts: lock_version 3's per-path
90
+ // digests are what turn "N files differ" into "N files differ and every one
91
+ // is untouched", which decides whether --allow-overwrite is warranted at all.
92
+ const prevLock = await readLockFile(opts.targetRepo);
93
+ const prevEntry = findBundle(prevLock, bundle.manifest.bundleName);
94
+ const lockBinding = prevLock?.ciBinding;
95
+ if (lockBinding !== undefined && !isCiBinding(lockBinding)) {
96
+ stdout(fail(`method-baseline.lock names ci_binding '${lockBinding}', which is not one of ${CI_BINDINGS.join(", ")}. Re-run with --ci-binding <binding> to choose one.`));
97
+ return { exitCode: 1 };
98
+ }
99
+ const ciBinding = opts.ciBinding ?? lockBinding ?? DEFAULT_CI_BINDING;
100
+ const classification = await classifyConflicts(conflicts, opts.targetRepo, prevEntry?.pathDigests);
101
+ if (conflicts.length > 0) {
102
+ reportClassification(stdout, conflicts.length, classification, prevEntry?.bundleVersion);
103
+ }
104
+ // A conflict set that is provably pristine is not a risk, so the flag is not
105
+ // required for it. Genuine edits, and anything undecidable, still are.
106
+ const safeWithoutFlag = allPristine(classification);
107
+ if (conflicts.length > 0 && !opts.allowOverwrite && !safeWithoutFlag) {
108
+ const blocking = [...classification.edited, ...classification.unclassifiable];
109
+ stdout(fail(`Re-run with --allow-overwrite to apply. ${blocking.length} of ${conflicts.length} ` +
110
+ `conflicting file(s) are not provably untouched:`));
111
+ for (const p of blocking) {
112
+ const why = classification.edited.includes(p) ? "locally edited" : "not classifiable";
113
+ stdout(` - ${p} (${why})\n`);
114
+ }
115
+ return { exitCode: 1 };
116
+ }
117
+ // Validate protected prefixes across ALL previously-locked paths.
118
+ if (prevLock) {
119
+ for (const b of prevLock.bundles) {
120
+ const protectedPrev = findProtectedPaths(b.managedPaths);
121
+ if (protectedPrev.length > 0) {
122
+ stdout(fail(`Existing lock file contains protected product-owned path: ${protectedPrev[0]} (bundle: ${b.name})`));
123
+ stdout(fail("Resolve this manually before refreshing the method baseline."));
124
+ return { exitCode: 1 };
125
+ }
126
+ }
127
+ }
128
+ // Additive bundles require a forward base already in the lock.
129
+ // For adopter packs (ADR 0151) the requires_forward_base field
130
+ // declares which sdd-method bundle (and minimum version) is needed.
131
+ const incomingBundleName = bundle.manifest.bundleName;
132
+ const incomingIsForward = bundle.manifest.isForwardBase;
133
+ if (!incomingIsForward && !hasForwardBase(prevLock, incomingBundleName)) {
134
+ stdout(fail(`${incomingBundleName} is an additive extension. Sync a forward base first.`));
135
+ stdout(fail(`Forward bases: platform-baseline, app-baseline, integration-baseline, reverse-baseline.`));
136
+ return { exitCode: 1 };
137
+ }
138
+ // Bundle migration (--replace-bundle): validate the request before any
139
+ // gate that would misread the overlap as a defect.
140
+ const migrateFrom = opts.replaceBundle
141
+ ? findBundle(prevLock, opts.replaceBundle)
142
+ : null;
143
+ if (opts.replaceBundle) {
144
+ if (opts.replaceBundle === incomingBundleName) {
145
+ stdout(fail(`--replace-bundle ${opts.replaceBundle}: replacing a bundle with itself is a plain sync — drop the flag.`));
146
+ return { exitCode: 1 };
147
+ }
148
+ if (!migrateFrom) {
149
+ stdout(fail(`--replace-bundle ${opts.replaceBundle}: the lock has no bundle of that name. Nothing to migrate.`));
150
+ return { exitCode: 1 };
151
+ }
152
+ if (migrateFrom.isForwardBase !== incomingIsForward) {
153
+ stdout(fail(`--replace-bundle: '${opts.replaceBundle}' (forward base: ${migrateFrom.isForwardBase}) and '${incomingBundleName}' (forward base: ${incomingIsForward}) are different layer kinds — a migration swaps like for like.`));
154
+ return { exitCode: 1 };
155
+ }
156
+ }
157
+ // Adopter pack requires_forward_base check: the named forward base
158
+ // must be in the lock at a compatible version. Per ADR 0151 §4.2.
159
+ if (!incomingIsForward && bundle.manifest.requiresForwardBase) {
160
+ const err = checkRequiresForwardBase(prevLock, bundle.manifest.requiresForwardBase, incomingBundleName);
161
+ if (err !== null) {
162
+ stdout(fail(err));
163
+ return { exitCode: 1 };
164
+ }
165
+ }
166
+ // Adopter pack requires_pack check: vertical extensions name a base
167
+ // pack they depend on. Per ADR 0151 §4.3.
168
+ if (!incomingIsForward && bundle.manifest.requiresPack) {
169
+ const err = checkRequiresPack(prevLock, bundle.manifest.requiresPack, incomingBundleName);
170
+ if (err !== null) {
171
+ stdout(fail(err));
172
+ return { exitCode: 1 };
173
+ }
174
+ }
175
+ // Cross-bundle ownership: a path managed by another bundle in the
176
+ // lock cannot also be claimed by this bundle. Under --replace-bundle the
177
+ // outgoing bundle's claims are released — migration is the overlap
178
+ // guard's sanctioned inverse, not its removal: a path owned by any THIRD
179
+ // bundle still refuses below.
180
+ const otherBundlePaths = pathToBundleMap(prevLock, incomingBundleName);
181
+ if (migrateFrom) {
182
+ for (const p of migrateFrom.managedPaths) {
183
+ if (otherBundlePaths.get(p) === migrateFrom.name)
184
+ otherBundlePaths.delete(p);
185
+ }
186
+ }
187
+ for (const p of bundle.managedPaths) {
188
+ const owner = otherBundlePaths.get(p);
189
+ if (owner !== undefined) {
190
+ stdout(fail(`Path '${p}' is already managed by bundle '${owner}'; cannot also be claimed by '${incomingBundleName}'.`));
191
+ stdout(fail("Method baselines must not overlap. Fix upstream and rebuild."));
192
+ return { exitCode: 1 };
193
+ }
194
+ }
195
+ // ADR transitions (bundle-declared rename / withdrawal / reassignment).
196
+ // Planned BEFORE removals because a withdrawal's whole contract is that the
197
+ // file is preserved and reported rather than deleted — and removal-by-omission
198
+ // would otherwise sweep it, since a withdrawn ADR is no longer in the bundle.
199
+ const adrTransitions = await planAdrTransitions(bundle.rootDir, opts.targetRepo, prevEntry?.bundleVersion);
200
+ // Removals are scoped to THIS bundle's previously-managed paths — plus,
201
+ // under --replace-bundle, the outgoing bundle's paths, so its orphans
202
+ // (old-only, unclaimed by the incoming bundle) are removed in the same
203
+ // pass rather than left as untracked strays.
204
+ const prevBundleEntry = findBundle(prevLock, incomingBundleName);
205
+ const prevPaths = [
206
+ ...(prevBundleEntry?.managedPaths ?? []),
207
+ ...(migrateFrom?.managedPaths ?? []),
208
+ ];
209
+ const withdrawnKeep = new Set(adrTransitions.protectedFromRemoval);
210
+ const removals = computeRemovals(prevPaths, materialisePaths, {
211
+ noDelete: !opts.delete,
212
+ }).filter((p) => !withdrawnKeep.has(p));
213
+ stdout(info(`Bundle: ${incomingBundleName} (version: ${bundle.manifest.bundleVersion})`));
214
+ stdout(info(`Incoming managed files: ${bundle.managedPaths.length}`));
215
+ if (cacheOnlyPaths.length > 0) {
216
+ stdout(info(`Cache-only (scaffold-source/registry) files: ${cacheOnlyPaths.length} — extracted to .sdd/bundle-cache, not materialised`));
217
+ }
218
+ // SHADOW-COLLISION FLAG (ledger §3ap, the durable move-rule's machinery).
219
+ // When the distributed surface MOVES a file, previously-delivered copies at
220
+ // the old location are stranded as unmanaged files: they keep resolving, so
221
+ // nothing breaks loudly, and they silently go stale as their managed twins
222
+ // evolve. One estate carried six such shadows, all stale — one of them a
223
+ // rubric its own scoring tooling was still reading, so a repaired anchor
224
+ // shipped and was simultaneously unreachable.
225
+ //
226
+ // The quiet failure is the one that needs machinery; a rename that breaks
227
+ // its citations announces itself. This flags the collision at the moment it
228
+ // is created — an incoming managed path whose BASENAME already exists at a
229
+ // shallower, non-managed path — rather than leaving it to a per-estate
230
+ // census to find later.
231
+ const shadows = detectShadowCollisions(materialisePaths, (rel) => existsSync(join(opts.targetRepo, rel)), (rel) => {
232
+ try {
233
+ return readFileSync(join(opts.targetRepo, rel), "utf-8");
234
+ }
235
+ catch {
236
+ return null;
237
+ }
238
+ });
239
+ if (shadows.length > 0) {
240
+ stdout(info(`Shadow collisions: ${shadows.length} — an incoming managed file shares its name with an ` +
241
+ `existing UNMANAGED file at a shallower path. These are not conflicts and nothing is ` +
242
+ `overwritten, but a stale twin that still resolves is read by tooling and by people ` +
243
+ `without either noticing (M-ADR 0200 move-rule):`));
244
+ for (const sh of shadows) {
245
+ stdout(info(` ${sh.shadow} (shadowed by managed ${sh.managed})`));
246
+ }
247
+ }
248
+ stdout(info(`Conflicts: ${conflicts.length}`));
249
+ // RELABEL (ledger §3y.2): the raw count mixes two different things and an
250
+ // adopter's dry-run reported 31 where the artefacts on disk numbered 5.
251
+ // Nothing was wrong with the mechanism — the LABEL claimed deletions for
252
+ // lock entries whose paths were never materialised (cache-only payload
253
+ // members). An alarming label on a harmless mechanism is a stop-generator,
254
+ // and this one generated a stop.
255
+ const removalsOnDisk = [];
256
+ for (const rel of removals) {
257
+ if (existsSync(join(opts.targetRepo, rel)))
258
+ removalsOnDisk.push(rel);
259
+ }
260
+ const lockOnly = removals.length - removalsOnDisk.length;
261
+ stdout(info(`Removals from previous sync of this bundle: ${removals.length}` +
262
+ (lockOnly > 0
263
+ ? ` — ${removalsOnDisk.length} file(s) on disk to delete, ${lockOnly} lock entr(ies) only ` +
264
+ `(paths never materialised here, e.g. cache-only payload members; nothing is deleted for these)`
265
+ : ` (all present on disk)`)));
266
+ stdout(info(`ADR renames to apply: ${adrTransitions.renames.length}`));
267
+ stdout(info(`ADR transition warnings: ${adrTransitions.warnings.length}`));
268
+ if (adrTransitions.warnings.length > 0) {
269
+ stdout(info("ADR transition notices:"));
270
+ for (const w of adrTransitions.warnings)
271
+ stdout(` ! ${w}\n`);
272
+ }
273
+ // A move strands its citations as well as its file (ruled 2026-09-29): report
274
+ // each citation in the adopter's own documents to a moved path. Never
275
+ // rewritten; printed in dry-run too, so the figure a reviewer approves is the
276
+ // figure the run states.
277
+ const stranded = await findStrandedCitations(opts.targetRepo, adrTransitions.movedPaths, new Set(materialisePaths), await trackedTextFiles(opts.targetRepo));
278
+ if (stranded.length > 0) {
279
+ stdout(warn(`Citations to moved paths: ${stranded.length} — the adopter's own documents still name ` +
280
+ `a path the method moved. Update each (not rewritten automatically):`));
281
+ for (const s of stranded)
282
+ stdout(` ! ${s}\n`);
283
+ }
284
+ stdout(info(`Protected ownership prefixes: ${PROTECTED_PREFIXES.join(" ")}`));
285
+ // Migration three-way split — always printed, dry-run or not, so the
286
+ // figures a reviewer approves are the figures the real run states.
287
+ if (migrateFrom) {
288
+ const incomingSet = new Set(bundle.managedPaths);
289
+ const shared = migrateFrom.managedPaths.filter((p) => incomingSet.has(p));
290
+ const arriving = bundle.managedPaths.filter((p) => !migrateFrom.managedPaths.includes(p));
291
+ const orphaned = migrateFrom.managedPaths.filter((p) => !incomingSet.has(p));
292
+ stdout(info(`Migration ${migrateFrom.name} → ${incomingBundleName}:`));
293
+ stdout(info(` shared (ownership transfers): ${shared.length}`));
294
+ stdout(info(` arriving (new to this repo): ${arriving.length}`));
295
+ stdout(info(` orphaned (removed unless --no-delete): ${orphaned.length}`));
296
+ for (const p of orphaned)
297
+ stdout(` - ${p}\n`);
298
+ if (!opts.dryRun && opts.yes !== true) {
299
+ stdout(fail(`--replace-bundle is destructive (drops the '${migrateFrom.name}' lock block). ` +
300
+ `Review the split above (use --dry-run first), then re-run with --yes.`));
301
+ return { exitCode: 1 };
302
+ }
303
+ }
304
+ // PRE-FLIGHT: evaluate every merge-managed refusal BEFORE the first
305
+ // write. The same gates exist downstream, but they used to fire after
306
+ // the payload had been applied — a refused sync left the repo holding
307
+ // the new bundle's content under the OLD lock, and the next run's
308
+ // digest comparison then classified every method-written file as an
309
+ // adopter edit. A gate that fires after 46 writes is not a gate.
310
+ // Measured on a live estate: 46 files written, then exit 1, no warning
311
+ // that anything had been written. The checks here are read-only and
312
+ // mirror the real gates exactly; those stay in place as defence in
313
+ // depth. All of them no-op under --allow-overwrite, hence the guard.
314
+ // Under --replace-bundle the method-managed settings/workflow blocks were
315
+ // rendered for the OUTGOING bundle's profile; the incoming profile renders
316
+ // different blocks, so drift against them is EXPECTED, not adopter edits.
317
+ // The merges below re-render method-managed content only — adopter entries
318
+ // outside the markers are preserved either way — so migration overrides
319
+ // this gate without touching --allow-overwrite's file-conflict semantics.
320
+ // The dry run evaluates this too: it used to return before the pre-flight
321
+ // ran, so on a live estate the dry run PASSED on 70 repos whose apply the
322
+ // pre-flight then refused on all 70 — a plan reported as clean that could
323
+ // not be performed. One function, both paths.
324
+ const preflightFailures = async () => {
325
+ const failures = [];
326
+ if (incomingIsForward && !opts.allowOverwrite && !migrateFrom) {
327
+ const preflightProfile = profileFromBundleName(incomingBundleName);
328
+ if (preflightProfile) {
329
+ if (opts.agentHarness === "github-copilot-cli") {
330
+ const probe = await syncMethodManagedCopilotHooks({
331
+ targetRepo: opts.targetRepo,
332
+ allowOverwrite: false,
333
+ checkOnly: true,
334
+ });
335
+ if (probe.refused) {
336
+ failures.push(`.github/hooks/*.json has adopter-modified method entries (${probe.driftedRefused.join(", ")}). Re-run with --allow-overwrite to rewrite them.`);
337
+ }
338
+ }
339
+ else {
340
+ const probe = await syncMethodManagedSettings({
341
+ targetRepo: opts.targetRepo,
342
+ profile: preflightProfile,
343
+ allowOverwrite: false,
344
+ checkOnly: true,
345
+ hadPreviousSync: prevLock !== null,
346
+ ...(opts.agentHarness ? { agentHarness: opts.agentHarness } : {}),
347
+ });
348
+ if (probe.refused) {
349
+ const conflicts = [];
350
+ if (probe.drifted.length > 0)
351
+ conflicts.push(`mcpServers: ${probe.drifted.join(", ")}`);
352
+ if (probe.hooksDrifted.length > 0)
353
+ conflicts.push(`hooks: ${probe.hooksDrifted.join(", ")}`);
354
+ failures.push(`.claude/settings.json has adopter-modified method entries (${conflicts.join("; ")}). Re-run with --allow-overwrite to rewrite them.`);
355
+ }
356
+ }
357
+ // The CI host wrapper and the Method Validation module (M-ADR 0219): the same check the
358
+ // write path runs, read-only.
359
+ const ciProbe = await syncCiBinding({
360
+ targetRepo: opts.targetRepo,
361
+ profile: preflightProfile,
362
+ binding: ciBinding,
363
+ prevLock,
364
+ allowDrift: false,
365
+ allowOverwrite: false,
366
+ checkOnly: true,
367
+ });
368
+ failures.push(...ciProbe.failures);
369
+ // Briefing wrapper — only refusable when this bundle ships briefing
370
+ // templates; pre-ADR-0148 baselines skip the briefing pass entirely.
371
+ const shipsBriefing = bundle.managedPaths.some((p) => p.startsWith(`${BRIEFING_TEMPLATE_ROOT}/`));
372
+ if (shipsBriefing) {
373
+ const wrapperRel = harnessWrapperOutputPath(opts.agentHarness ?? DEFAULT_AGENT_HARNESS);
374
+ const preflightWrapper = await tryReadFile(join(opts.targetRepo, wrapperRel));
375
+ if (preflightWrapper !== null &&
376
+ preflightWrapper !== "" &&
377
+ !(preflightWrapper.includes(WRAPPER_BLOCK_BEGIN) &&
378
+ preflightWrapper.includes(WRAPPER_BLOCK_END))) {
379
+ failures.push(`${wrapperRel} has adopter edits outside the method-managed markers. Re-run with --allow-overwrite to replace the file.`);
380
+ }
381
+ }
382
+ }
383
+ }
384
+ return failures;
385
+ };
386
+ if (opts.dryRun) {
387
+ const wouldRefuse = await preflightFailures();
388
+ if (wouldRefuse.length > 0) {
389
+ for (const f of wouldRefuse)
390
+ stdout(fail(`the apply would REFUSE: ${f}`));
391
+ stdout(info("Dry run: the plan below cannot be performed as it stands — the apply's pre-flight refuses it (see above). Exit 1."));
392
+ }
393
+ stdout(info("Dry run enabled. No files modified."));
394
+ if (conflicts.length > 0) {
395
+ stdout(info("Conflicting files:"));
396
+ for (const p of conflicts)
397
+ stdout(` - ${p}\n`);
398
+ }
399
+ if (removals.length > 0) {
400
+ stdout(info("Files that would be removed:"));
401
+ for (const p of removals)
402
+ stdout(` - ${p}\n`);
403
+ }
404
+ return { exitCode: wouldRefuse.length > 0 ? 1 : 0 };
405
+ }
406
+ const failures = await preflightFailures();
407
+ if (failures.length > 0) {
408
+ for (const f of failures)
409
+ stdout(fail(f));
410
+ stdout(info(
411
+ // The figures above describe the PLAN, and they print before this
412
+ // line because they are computed before the gate runs. An estate
413
+ // sweep keyed on them read 39 correct plans as 39 successes while
414
+ // every repo was untouched, so the outcome restates them in their
415
+ // own vocabulary rather than leaving the reader to infer that a
416
+ // plan reported is a plan performed.
417
+ `Pre-flight refusal: NOTHING was written — no files copied, no removals, lock unchanged. The ${bundle.managedPaths.length} incoming managed file(s), ${conflicts.length} conflict(s) and ${removals.length} removal(s) reported above are the plan that was ABANDONED, not work performed. Resolve the above (or re-run with --allow-overwrite) and sync again.`));
418
+ return { exitCode: 1 };
419
+ }
420
+ // Before the copy: move renamed ADRs so the adopter's own edits travel with the
421
+ // file and git records a rename, instead of the copy phase creating the new path
422
+ // and removal deleting the old one.
423
+ await applyAdrRenames(adrTransitions, opts.targetRepo);
424
+ await copyManagedFiles(materialisePaths, bundle.payloadDir, opts.targetRepo);
425
+ // Each pruned directory is named, not counted: it is an action taken on the
426
+ // adopter's tree, and git cannot show it afterwards because empty
427
+ // directories are untracked. The sync output is the only place it can ever
428
+ // be seen.
429
+ const prunedDirs = await removeManagedFiles(removals, opts.targetRepo);
430
+ if (prunedDirs.length > 0) {
431
+ stdout(info(`Emptied directories removed: ${prunedDirs.length} (withdrawal left them empty)`));
432
+ for (const d of prunedDirs)
433
+ stdout(` - ${d}/\n`);
434
+ }
435
+ // ADR 0176 §4.2: cache non-forward-base bundles (the resolution
436
+ // substrate for scaffold --archetype). Forward bases are not cached
437
+ // — they carry no archetype registries and are large.
438
+ if (!bundle.manifest.isForwardBase) {
439
+ await cacheBundle({
440
+ targetRepo: opts.targetRepo,
441
+ bundleName: bundle.manifest.bundleName,
442
+ bundleVersion: bundle.manifest.bundleVersion,
443
+ managedPaths: bundle.managedPaths,
444
+ payloadDir: bundle.payloadDir,
445
+ });
446
+ const ignored = await ensureCacheGitignored(opts.targetRepo);
447
+ stdout(info(`Bundle cached: .sdd/bundle-cache/${bundle.manifest.bundleName}@${bundle.manifest.bundleVersion}${ignored ? " (.gitignore updated)" : ""}`));
448
+ }
449
+ const bundleSha = await sha256File(opts.bundle);
450
+ // Per-path digests (lock_version 3), taken from the PAYLOAD rather than the
451
+ // repo after copying: the payload is definitionally what this sync applied,
452
+ // and hashing the repo afterwards would silently absorb any concurrent local
453
+ // write into the "as applied" record — making a later edit look pristine.
454
+ // Cache-only paths are excluded because they never land in the tree, so a
455
+ // digest for them could never be compared against anything.
456
+ const pathDigests = await digestManagedPaths(materialisePaths, bundle.payloadDir);
457
+ const newEntry = {
458
+ name: incomingBundleName,
459
+ bundleVersion: bundle.manifest.bundleVersion,
460
+ sourceCommit: bundle.manifest.sourceCommit,
461
+ bundleSha256: bundleSha,
462
+ // Re-applying the SAME bundle (same sha256) keeps the time it was first applied, so a repeat
463
+ // sync writes the same lock bytes: sync is a function of its inputs, not of the clock
464
+ // (M-ADR 0218 §4.2, determinism). Found by maintain-cli's acceptance run, where the second
465
+ // sync on one commit changed nothing but this timestamp. A different bundle gets a new time.
466
+ syncedAtUtc: prevBundleEntry !== null && prevBundleEntry !== undefined && prevBundleEntry.bundleSha256 === bundleSha
467
+ ? prevBundleEntry.syncedAtUtc
468
+ : nowUtcIso(),
469
+ isForwardBase: incomingIsForward,
470
+ managedPaths: [...bundle.managedPaths],
471
+ pathDigests,
472
+ // Adopter-pack provenance per ADR 0151 — fields only present for
473
+ // non-forward-base bundles produced by adopter pack builders.
474
+ ...(bundle.manifest.producedByAdopter && {
475
+ producedByAdopter: bundle.manifest.producedByAdopter,
476
+ }),
477
+ ...(bundle.manifest.scope && { scope: bundle.manifest.scope }),
478
+ ...(bundle.manifest.requiresForwardBase && {
479
+ requiresForwardBase: bundle.manifest.requiresForwardBase,
480
+ }),
481
+ ...(bundle.manifest.requiresPack && {
482
+ requiresPack: bundle.manifest.requiresPack,
483
+ }),
484
+ };
485
+ // Additive layers (e.g. reverse-extension-baseline) own only the
486
+ // reverse-method content. They do NOT set .sdd-repo-kind, refresh
487
+ // .claude/settings.json, or rewrite the CI workflow — the forward
488
+ // base owns those.
489
+ if (!incomingIsForward) {
490
+ await writeLockFile(opts.targetRepo, upsertBundle(migrateFrom ? removeBundle(prevLock, migrateFrom.name) : prevLock, newEntry));
491
+ stdout(pass(`Method baseline synced to: ${opts.targetRepo}`));
492
+ stdout(info(`Updated lock file: method-baseline.lock (lock_version: ${LOCK_VERSION})`));
493
+ stdout(info("Additive layer applied; .sdd-repo-kind unchanged."));
494
+ return { exitCode: 0 };
495
+ }
496
+ // Forward base path: settings, workflow, repo-kind all belong here.
497
+ const profile = profileFromBundleName(incomingBundleName);
498
+ if (!profile) {
499
+ // Should be unreachable: forward bases all map to a profile.
500
+ throw new Error(`Forward-base bundle "${incomingBundleName}" has no profile mapping`);
501
+ }
502
+ const repoKind = repoKindFromProfile(profile);
503
+ await writeRepoKind(opts.targetRepo, repoKind);
504
+ // Harness-aware merge of agent-harness wiring per ADR 0141 §4.6.
505
+ // claude-code keeps the .claude/settings.json path (MCP + Claude
506
+ // hooks). github-copilot-cli writes .github/hooks/*.json instead;
507
+ // its MCP wiring lands separately under ADR 0150 Phase 3.
508
+ if (opts.agentHarness === "github-copilot-cli") {
509
+ const copilotResult = await syncMethodManagedCopilotHooks({
510
+ targetRepo: opts.targetRepo,
511
+ allowOverwrite: opts.allowOverwrite || migrateFrom !== null,
512
+ });
513
+ if (copilotResult.refused) {
514
+ stdout(fail(`.github/hooks/*.json has adopter-modified method entries (${copilotResult.driftedRefused.join(", ")}). Re-run with --allow-overwrite to rewrite them.`));
515
+ return { exitCode: 1 };
516
+ }
517
+ const written = copilotResult.files.filter((f) => f.wrote);
518
+ if (written.length > 0) {
519
+ stdout(info(`Refreshed ${written.map((f) => f.relPath).join(", ")}.`));
520
+ }
521
+ }
522
+ else {
523
+ const settingsResult = await syncMethodManagedSettings({
524
+ targetRepo: opts.targetRepo,
525
+ profile,
526
+ allowOverwrite: opts.allowOverwrite || migrateFrom !== null,
527
+ // A lock is the only sound evidence the managed entries were seeded
528
+ // before, which is what makes a re-seed distinguishable from a first
529
+ // sync into an adopter's own settings file.
530
+ hadPreviousSync: prevLock !== null,
531
+ ...(opts.agentHarness ? { agentHarness: opts.agentHarness } : {}),
532
+ });
533
+ if (settingsResult.refused) {
534
+ const conflicts = [];
535
+ if (settingsResult.drifted.length > 0)
536
+ conflicts.push(`mcpServers: ${settingsResult.drifted.join(", ")}`);
537
+ if (settingsResult.hooksDrifted.length > 0)
538
+ conflicts.push(`hooks: ${settingsResult.hooksDrifted.join(", ")}`);
539
+ stdout(fail(`.claude/settings.json has adopter-modified method entries (${conflicts.join("; ")}). Re-run with --allow-overwrite to rewrite them.`));
540
+ return { exitCode: 1 };
541
+ }
542
+ // Drift-class line of its own, ahead of the refresh summary: these
543
+ // entries were seeded by an earlier sync and had been removed since.
544
+ // Restoring them is not the news — that they went missing is, and the
545
+ // same ids reappearing here every sync means something is removing them
546
+ // repeatedly.
547
+ if (settingsResult.reseeded.length > 0) {
548
+ stdout(info(`${settingsResult.path}: RE-SEEDED ${settingsResult.reseeded.length} method-managed entr${settingsResult.reseeded.length === 1 ? "y" : "ies"} (${settingsResult.reseeded.join(", ")}) — present after a previous sync, absent now. Restored; if this recurs, something in this repo is removing them.`));
549
+ }
550
+ if (settingsResult.wroteFile) {
551
+ const labels = [];
552
+ if (settingsResult.added.length > 0)
553
+ labels.push(`mcp added: ${settingsResult.added.join(", ")}`);
554
+ if (settingsResult.updated.length > 0)
555
+ labels.push(`mcp updated: ${settingsResult.updated.join(", ")}`);
556
+ if (settingsResult.hooksChangedEvents.length > 0)
557
+ labels.push(`hooks: ${settingsResult.hooksChangedEvents.join(", ")}`);
558
+ stdout(info(`Refreshed ${settingsResult.path} (${labels.join("; ") || "no changes"}).`));
559
+ }
560
+ }
561
+ // The CI host wrapper and the Method Validation module it calls (M-ADR 0219). Under
562
+ // --replace-bundle the blocks were rendered for the outgoing profile, so drift is expected.
563
+ const ci = await syncCiBinding({
564
+ targetRepo: opts.targetRepo,
565
+ profile,
566
+ binding: ciBinding,
567
+ prevLock,
568
+ allowDrift: opts.allowOverwrite || migrateFrom !== null,
569
+ allowOverwrite: opts.allowOverwrite,
570
+ checkOnly: false,
571
+ });
572
+ if (ci.failures.length > 0) {
573
+ for (const f of ci.failures)
574
+ stdout(fail(f));
575
+ return { exitCode: 1 };
576
+ }
577
+ for (const line of ci.log)
578
+ stdout(info(line));
579
+ // Briefing layer per ADR 0148: agent-instructions.md (unconditional
580
+ // overwrite) + harness wrapper (marker-aware merge). Templates ship
581
+ // in the bundle's orchestration/templates/briefing/; gracefully skip
582
+ // when an older bundle without them is being synced.
583
+ const briefingResult = await syncBriefingFiles({
584
+ targetRepo: opts.targetRepo,
585
+ repoKind: repoKindFromProfile(profile),
586
+ baselineVersion: bundle.manifest.bundleVersion,
587
+ agentHarness: opts.agentHarness ?? DEFAULT_AGENT_HARNESS,
588
+ allowOverwrite: opts.allowOverwrite || migrateFrom !== null,
589
+ });
590
+ if (briefingResult.refused) {
591
+ stdout(fail(`${briefingResult.refused} has adopter edits outside the method-managed markers. Re-run with --allow-overwrite to replace the file.`));
592
+ return { exitCode: 1 };
593
+ }
594
+ for (const line of briefingResult.log)
595
+ stdout(info(line));
596
+ // Record the wrapper block and the module exactly as they now stand on disk, so the next sync
597
+ // can tell them untouched from edited (M-ADR 0218 §4.7), and the binding when it is not the default.
598
+ const nextLock = upsertBundle(migrateFrom ? removeBundle(prevLock, migrateFrom.name) : prevLock, newEntry);
599
+ const { ciBinding: _previousBinding, ciWorkflowBlockSha256: _previousSha, ...lockBase } = nextLock;
600
+ await writeLockFile(opts.targetRepo, {
601
+ ...lockBase,
602
+ ...(ciBinding !== DEFAULT_CI_BINDING ? { ciBinding } : {}),
603
+ ...(ci.wrapperSha !== null ? { ciWorkflowBlockSha256: ci.wrapperSha } : {}),
604
+ ciModuleSha256: ci.moduleSha,
605
+ });
606
+ stdout(pass(`Method baseline synced to: ${opts.targetRepo}`));
607
+ stdout(info(`Updated lock file: method-baseline.lock (lock_version: ${LOCK_VERSION})`));
608
+ stdout(info(`Set repo kind marker: ${REPO_KIND_FILE} = ${repoKind}`));
609
+ return { exitCode: 0 };
610
+ }
611
+ finally {
612
+ await bundle.cleanup();
613
+ }
614
+ }
615
+ /** sha256 of the GitHub workflow's method-managed block (begin marker through end marker), or null without one. */
616
+ export function workflowBlockSha256(text) {
617
+ return blockSha256(text, { begin: BLOCK_BEGIN, end: BLOCK_END });
618
+ }
619
+ function section(title) {
620
+ return `\n${SECTION_RULE}\n ${title}\n${SECTION_RULE}\n\n`;
621
+ }
622
+ function info(msg) {
623
+ return `[INFO] ${msg}\n`;
624
+ }
625
+ function warn(msg) {
626
+ return `[WARN] ${msg}\n`;
627
+ }
628
+ function pass(msg) {
629
+ return `[PASS] ${msg}\n`;
630
+ }
631
+ function fail(msg) {
632
+ return `[FAIL] ${msg}\n`;
633
+ }
634
+ async function exists(path) {
635
+ try {
636
+ await access(path);
637
+ return true;
638
+ }
639
+ catch {
640
+ return false;
641
+ }
642
+ }
643
+ /**
644
+ * If the bundle manifest declares a minimum sdd-cli version, verify the
645
+ * installed version satisfies it. Returns null on success or no
646
+ * declared minimum; returns a formatted error message on failure.
647
+ *
648
+ * Comparison is structural semver (major.minor.patch) — installed >=
649
+ * minimum at each segment. Pre-release identifiers (e.g. "0.14.0-rc.1")
650
+ * are stripped before comparison; the minimum field always declares a
651
+ * stable release version.
652
+ */
653
+ export function checkMinimumSddCliVersion(manifest, installedOverride) {
654
+ const required = manifest.minimumSddCliVersion;
655
+ if (!required || required.length === 0)
656
+ return null;
657
+ const installed = installedOverride ?? getSddCliVersion();
658
+ if (versionAtLeast(installed, required))
659
+ return null;
660
+ return [
661
+ `Bundle ${manifest.bundleName} ${manifest.bundleVersion} requires sdd-cli >= ${required}`,
662
+ `but installed sdd-cli is ${installed}.`,
663
+ `Remediation: npm install -g @sdd-method/sdd-cli@^${required}`,
664
+ ].join(" ");
665
+ }
666
+ /**
667
+ * Verify `requires_forward_base` declared by an adopter pack against
668
+ * the consumer's lock. Returns null on success or a formatted error.
669
+ */
670
+ export function checkRequiresForwardBase(prevLock, required, incomingBundleName) {
671
+ const fwd = findForwardBase(prevLock, incomingBundleName);
672
+ if (fwd === null) {
673
+ return (`${incomingBundleName} requires forward base ${required.bundle} ` +
674
+ `>= ${required.minimumVersion}, but the consumer lock has no forward base.`);
675
+ }
676
+ if (fwd.name !== required.bundle) {
677
+ return (`${incomingBundleName} requires forward base ${required.bundle}, ` +
678
+ `but the consumer lock has ${fwd.name} as its forward base.`);
679
+ }
680
+ if (!versionAtLeast(fwd.bundleVersion, required.minimumVersion)) {
681
+ return (`${incomingBundleName} requires ${required.bundle} >= ${required.minimumVersion}, ` +
682
+ `but the consumer lock has ${fwd.name} ${fwd.bundleVersion}.`);
683
+ }
684
+ return null;
685
+ }
686
+ /**
687
+ * Verify `requires_pack` declared by a vertical extension against the
688
+ * consumer's lock. Returns null on success or a formatted error.
689
+ *
690
+ * @public exported for unit testing.
691
+ *
692
+ * Version semantics: caret-range default (`^vX.Y.Z` matches `X.x.y` ≥
693
+ * `X.Y.Z` for X >= 1; for `^v0.Y.Z` only the same minor — `0.Y.x`).
694
+ * Exact pin (`vX.Y.Z`) matches only that version. Tilde (`~vX.Y.Z`)
695
+ * matches `X.Y.x`. `>=vX.Y.Z` matches anything ≥ that version.
696
+ */
697
+ export function checkRequiresPack(prevLock, required, incomingBundleName) {
698
+ const target = findBundle(prevLock, required.name);
699
+ if (target === null) {
700
+ return (`${incomingBundleName} requires pack ${required.name} ${required.version}, ` +
701
+ `but no such pack is in the consumer lock.`);
702
+ }
703
+ if (!rangeSatisfies(target.bundleVersion, required.version)) {
704
+ return (`${incomingBundleName} requires ${required.name} ${required.version}, ` +
705
+ `but the consumer lock has ${target.name} ${target.bundleVersion} (range not satisfied).`);
706
+ }
707
+ return null;
708
+ }
709
+ /**
710
+ * Semver range satisfaction. Supports caret (^), tilde (~), >= and
711
+ * exact-pin (no prefix). Versions may carry a leading 'v'.
712
+ *
713
+ * Pre-1.0 caret narrows to the same minor: ^v0.1.x matches only 0.1.y
714
+ * (per npm convention). ^v1.2.x matches 1.x.y >= 1.2.x.
715
+ */
716
+ export function rangeSatisfies(version, range) {
717
+ const stripV = (s) => s.replace(/^v/, "");
718
+ const parse = (s) => parseSimpleSemver(stripV(s));
719
+ let prefix = "=";
720
+ let body = range;
721
+ if (range.startsWith("^")) {
722
+ prefix = "^";
723
+ body = range.slice(1);
724
+ }
725
+ else if (range.startsWith("~")) {
726
+ prefix = "~";
727
+ body = range.slice(1);
728
+ }
729
+ else if (range.startsWith(">=")) {
730
+ prefix = ">=";
731
+ body = range.slice(2);
732
+ }
733
+ const v = parse(version);
734
+ const r = parse(body);
735
+ if (v === null || r === null)
736
+ return false;
737
+ switch (prefix) {
738
+ case "=":
739
+ return v.major === r.major && v.minor === r.minor && v.patch === r.patch;
740
+ case ">=":
741
+ return versionAtLeast(stripV(version), stripV(body));
742
+ case "~":
743
+ // Same major.minor, patch >= r.patch
744
+ return v.major === r.major && v.minor === r.minor && v.patch >= r.patch;
745
+ case "^":
746
+ // Pre-1.0: same major.minor (narrow caret)
747
+ if (r.major === 0) {
748
+ return v.major === 0 && v.minor === r.minor && v.patch >= r.patch;
749
+ }
750
+ // Stable: same major, version >= range
751
+ return v.major === r.major && versionAtLeast(stripV(version), stripV(body));
752
+ }
753
+ }
754
+ function versionAtLeast(installed, minimum) {
755
+ const i = parseSimpleSemver(installed);
756
+ const m = parseSimpleSemver(minimum);
757
+ if (i === null || m === null)
758
+ return false;
759
+ if (i.major !== m.major)
760
+ return i.major > m.major;
761
+ if (i.minor !== m.minor)
762
+ return i.minor > m.minor;
763
+ return i.patch >= m.patch;
764
+ }
765
+ function parseSimpleSemver(s) {
766
+ // Strip optional `v` prefix and any pre-release / build suffix.
767
+ const cleaned = s.replace(/^v/, "").replace(/[-+].*$/, "");
768
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(cleaned);
769
+ if (!m)
770
+ return null;
771
+ return {
772
+ major: parseInt(m[1], 10),
773
+ minor: parseInt(m[2], 10),
774
+ patch: parseInt(m[3], 10),
775
+ };
776
+ }
777
+ async function tryReadFile(path) {
778
+ try {
779
+ await access(path);
780
+ return await readFile(path, "utf8");
781
+ }
782
+ catch {
783
+ return null;
784
+ }
785
+ }
786
+ async function sha256File(path) {
787
+ const buf = await readFile(path);
788
+ return createHash("sha256").update(buf).digest("hex");
789
+ }
790
+ function nowUtcIso() {
791
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
792
+ }
793
+ /**
794
+ * The adopter's git-tracked Markdown and YAML — the documents that can cite a
795
+ * moved path. Outside a git work tree there is no honest scope: nothing is
796
+ * scanned, rather than guessing at the whole directory.
797
+ */
798
+ async function trackedTextFiles(targetRepo) {
799
+ const { execFile } = await import("node:child_process");
800
+ const { promisify } = await import("node:util");
801
+ try {
802
+ const { stdout } = await promisify(execFile)("git", ["-C", targetRepo, "ls-files", "-z", "--", "*.md", "*.yaml", "*.yml"], { maxBuffer: 64 * 1024 * 1024 });
803
+ return stdout.split("\0").filter((p) => p !== "");
804
+ }
805
+ catch {
806
+ return [];
807
+ }
808
+ }
809
+ //# sourceMappingURL=index.js.map