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