@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,109 @@
1
+ import { appendFile, copyFile, mkdir, readFile, readdir, rm } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ /**
4
+ * Consumer-side bundle cache (ADR 0176 §4.2).
5
+ *
6
+ * Every consumed non-forward-base bundle is extracted to
7
+ * `<targetRepo>/.sdd/bundle-cache/<bundleName>@<version>/` — the
8
+ * resolution substrate for `sdd-cli scaffold domain --archetype` and
9
+ * the staging source for materialise-mode paths. The cache directory
10
+ * is gitignored (ensured at sync time).
11
+ *
12
+ * Cache layout per bundle: the bundle's payload tree, verbatim (so
13
+ * registry.yaml and scaffold-source files sit at their shipped paths).
14
+ * Stale versions of the same bundle name are pruned on refresh.
15
+ */
16
+ export const BUNDLE_CACHE_REL = ".sdd/bundle-cache";
17
+ /**
18
+ * The registry file name (ADR 0176 §4.1) — always cache-only. A bundle may
19
+ * ship its registry at the payload root or nested (a multi-bundle producer
20
+ * repo keeps one registry per bundle subtree, e.g. a vertical overlay's
21
+ * `verticals/fleet/registry.yaml`, because the producer-layout rule forbids
22
+ * path mapping and the root name can only be claimed once).
23
+ */
24
+ export const REGISTRY_FILE = "registry.yaml";
25
+ /**
26
+ * The application-archetype vocabulary file name (M-ADR 0191 §4.2) — always
27
+ * cache-only, like the registry. Carries the seedless classification
28
+ * vocabulary a bundle contributes (resolution data, not adopter-editable
29
+ * content). May sit at the payload root (core layer) or nested (a vertical
30
+ * overlay's `verticals/<v>/application-archetypes.yaml`), same as the
31
+ * registry.
32
+ */
33
+ export const VOCAB_FILE = "application-archetypes.yaml";
34
+ /**
35
+ * Split a bundle's managed paths into the set to materialise into the
36
+ * repo tree vs the set that stays cache-only (ADR 0176 §4.3):
37
+ * scaffold-source-prefixed paths, registry files, and the archetype
38
+ * vocabulary file are cache-only.
39
+ */
40
+ export function splitConsumptionModes(managedPaths, scaffoldSourcePaths) {
41
+ const prefixes = scaffoldSourcePaths ?? [];
42
+ const materialise = [];
43
+ const cacheOnly = [];
44
+ for (const p of managedPaths) {
45
+ const isScaffoldSource = prefixes.some((prefix) => p.startsWith(prefix));
46
+ const isRegistry = p === REGISTRY_FILE || p.endsWith(`/${REGISTRY_FILE}`);
47
+ const isVocab = p === VOCAB_FILE || p.endsWith(`/${VOCAB_FILE}`);
48
+ if (isScaffoldSource || isRegistry || isVocab) {
49
+ cacheOnly.push(p);
50
+ }
51
+ else {
52
+ materialise.push(p);
53
+ }
54
+ }
55
+ return { materialise, cacheOnly };
56
+ }
57
+ /**
58
+ * Extract a bundle's payload into the cache, replacing any previously
59
+ * cached version of the same bundle name (one cached version per
60
+ * bundle — the lock pins exactly one).
61
+ */
62
+ export async function cacheBundle(input) {
63
+ const cacheRoot = join(input.targetRepo, BUNDLE_CACHE_REL);
64
+ await prunePreviousVersions(cacheRoot, input.bundleName);
65
+ const dest = join(cacheRoot, `${input.bundleName}@${input.bundleVersion}`);
66
+ for (const p of input.managedPaths) {
67
+ const src = join(input.payloadDir, p);
68
+ const dst = join(dest, p);
69
+ await mkdir(dirname(dst), { recursive: true });
70
+ await copyFile(src, dst);
71
+ }
72
+ return dest;
73
+ }
74
+ async function prunePreviousVersions(cacheRoot, bundleName) {
75
+ let entries;
76
+ try {
77
+ entries = await readdir(cacheRoot, { withFileTypes: true });
78
+ }
79
+ catch {
80
+ return; // no cache yet
81
+ }
82
+ for (const e of entries) {
83
+ if (e.isDirectory() && e.name.startsWith(`${bundleName}@`)) {
84
+ await rm(join(cacheRoot, e.name), { recursive: true, force: true });
85
+ }
86
+ }
87
+ }
88
+ /**
89
+ * Ensure the consumer's .gitignore excludes the bundle cache. Appends
90
+ * a marked line when absent; leaves adopter content untouched otherwise.
91
+ */
92
+ export async function ensureCacheGitignored(targetRepo) {
93
+ const giPath = join(targetRepo, ".gitignore");
94
+ const line = `${BUNDLE_CACHE_REL}/`;
95
+ let existing = "";
96
+ try {
97
+ existing = await readFile(giPath, "utf8");
98
+ }
99
+ catch {
100
+ // no .gitignore yet — create one below
101
+ }
102
+ if (existing.split("\n").some((l) => l.trim() === line || l.trim() === BUNDLE_CACHE_REL)) {
103
+ return false;
104
+ }
105
+ const prefixNewline = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
106
+ await appendFile(giPath, `${prefixNewline}# sdd-cli bundle cache (ADR 0176) — extracted bundle payloads, not source\n${line}\n`);
107
+ return true;
108
+ }
109
+ //# sourceMappingURL=bundle-cache.js.map
@@ -0,0 +1,61 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ function isUrl(s) {
6
+ return /^https?:\/\//i.test(s);
7
+ }
8
+ async function sha256File(path) {
9
+ const buf = await readFile(path);
10
+ return createHash("sha256").update(buf).digest("hex");
11
+ }
12
+ /** Verify a tarball's sha256 against a pin; throw on mismatch. */
13
+ export async function verifySha256(localPath, expected) {
14
+ const actual = await sha256File(localPath);
15
+ if (actual.toLowerCase() !== expected.toLowerCase()) {
16
+ throw new Error(`bundle_sha256 mismatch for ${basename(localPath)}: ` +
17
+ `expected ${expected}, got ${actual}`);
18
+ }
19
+ }
20
+ /**
21
+ * Fetch a remote bundle to a temp file. Uses the global `fetch` (Node 18+).
22
+ * Exported for tests that stub the fetcher.
23
+ */
24
+ export async function fetchToTemp(url, fetchImpl = fetch) {
25
+ const res = await fetchImpl(url);
26
+ if (!res.ok) {
27
+ throw new Error(`Failed to fetch bundle ${url}: HTTP ${res.status}`);
28
+ }
29
+ const buf = Buffer.from(await res.arrayBuffer());
30
+ const dir = await mkdtemp(join(tmpdir(), "sdd-cli-bundle-src-"));
31
+ const dest = join(dir, basename(new URL(url).pathname) || "bundle.tar.gz");
32
+ await writeFile(dest, buf);
33
+ return dest;
34
+ }
35
+ /**
36
+ * Materialise a config bundle entry to a verified local tarball path.
37
+ * Callers MUST invoke the returned `cleanup` after the sync completes.
38
+ */
39
+ export async function materialiseBundle(entry, fetchImpl = fetch) {
40
+ if (isUrl(entry.source)) {
41
+ const localPath = await fetchToTemp(entry.source, fetchImpl);
42
+ const cleanup = async () => {
43
+ await rm(localPath, { force: true });
44
+ await rm(join(localPath, ".."), { recursive: true, force: true });
45
+ };
46
+ try {
47
+ if (entry.sha256)
48
+ await verifySha256(localPath, entry.sha256);
49
+ }
50
+ catch (err) {
51
+ await cleanup();
52
+ throw err;
53
+ }
54
+ return { localPath, cleanup };
55
+ }
56
+ // Local source: verify in place, no temp resources to release.
57
+ if (entry.sha256)
58
+ await verifySha256(entry.source, entry.sha256);
59
+ return { localPath: entry.source, cleanup: async () => { } };
60
+ }
61
+ //# sourceMappingURL=bundle-source.js.map
@@ -0,0 +1,72 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ function isUrl(s) {
5
+ return /^https?:\/\//i.test(s);
6
+ }
7
+ export async function loadSyncConfig(configPath) {
8
+ const absPath = isAbsolute(configPath) ? configPath : resolve(configPath);
9
+ let raw;
10
+ try {
11
+ raw = await readFile(absPath, "utf8");
12
+ }
13
+ catch (err) {
14
+ throw new Error(`Cannot read sync config ${absPath}: ${err.message}`);
15
+ }
16
+ const parsed = parseYaml(raw);
17
+ if (parsed === null || typeof parsed !== "object") {
18
+ throw new Error(`Sync config ${absPath} is not a YAML object`);
19
+ }
20
+ const version = typeof parsed.version === "string" ? parsed.version : undefined;
21
+ if (!version) {
22
+ throw new Error(`Sync config ${absPath} missing required field: version`);
23
+ }
24
+ if (!Array.isArray(parsed.bundles)) {
25
+ throw new Error(`Sync config ${absPath} field "bundles" must be a list`);
26
+ }
27
+ if (parsed.bundles.length === 0) {
28
+ throw new Error(`Sync config ${absPath} field "bundles" must not be empty`);
29
+ }
30
+ const configDir = dirname(absPath);
31
+ const bundleSource = typeof parsed.bundle_source === "string" ? parsed.bundle_source : undefined;
32
+ // Pre-resolve the base for name/version resolution: a URL stays as-is;
33
+ // a local dir is resolved relative to the config file.
34
+ const resolvedBase = bundleSource === undefined
35
+ ? undefined
36
+ : isUrl(bundleSource) || isAbsolute(bundleSource)
37
+ ? bundleSource
38
+ : resolve(configDir, bundleSource);
39
+ const bundles = [];
40
+ for (const [i, entry] of parsed.bundles.entries()) {
41
+ const rawBundle = entry;
42
+ const src = typeof rawBundle.source === "string" ? rawBundle.source : undefined;
43
+ const name = typeof rawBundle.name === "string" ? rawBundle.name : undefined;
44
+ const ver = typeof rawBundle.version === "string" ? rawBundle.version : undefined;
45
+ const sha256 = typeof rawBundle.sha256 === "string" ? rawBundle.sha256 : undefined;
46
+ let source;
47
+ if (src) {
48
+ // Explicit source: a URL stays as-is; a path is resolved against the
49
+ // config dir (absolute paths pass through).
50
+ source = isUrl(src) || isAbsolute(src) ? src : resolve(configDir, src);
51
+ }
52
+ else if (name && ver) {
53
+ if (resolvedBase === undefined) {
54
+ throw new Error(`Sync config ${absPath} bundles[${i}] uses name+version but no ` +
55
+ `top-level "bundle_source" base is set to resolve it against.`);
56
+ }
57
+ const rel = `${name}/${name}-${ver}.tar.gz`;
58
+ source = isUrl(resolvedBase)
59
+ ? `${resolvedBase.replace(/\/$/, "")}/${rel}`
60
+ : resolve(resolvedBase, name, `${name}-${ver}.tar.gz`);
61
+ }
62
+ else {
63
+ throw new Error(`Sync config ${absPath} bundles[${i}] missing required field: source ` +
64
+ `(or a name+version pair resolved against bundle_source)`);
65
+ }
66
+ bundles.push(sha256 === undefined ? { source } : { source, sha256 });
67
+ }
68
+ return bundleSource === undefined
69
+ ? { version, bundles }
70
+ : { version, bundleSource, bundles };
71
+ }
72
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `.github/hooks/*.json` merge pass during `sdd-cli sync` when the
3
+ * adopter opted into `--agent-harness github-copilot-cli`.
4
+ *
5
+ * Method-managed event entries are sentinel-tagged via their
6
+ * `command.bash` / `command.powershell` strings. Adopter-added events
7
+ * in the same `events[]` array survive sync. Drift inside a method-
8
+ * managed event refuses without `--allow-overwrite`, symmetric to the
9
+ * Claude Code settings-merge path.
10
+ */
11
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
12
+ import { dirname, join } from "node:path";
13
+ import { buildMethodManagedCopilotHooks, detectCopilotHookDrift, upsertCopilotHookFile, } from "../hooks-seed/copilot.js";
14
+ export async function syncMethodManagedCopilotHooks(opts) {
15
+ const surface = buildMethodManagedCopilotHooks();
16
+ const entries = Object.entries(surface);
17
+ // First pass: detect drift across all files. If any drifted file
18
+ // would be refused (no --allow-overwrite), we refuse the whole
19
+ // merge without writing — same all-or-nothing posture as the
20
+ // Claude Code settings merge.
21
+ const reads = [];
22
+ for (const [portableClass, { filePath, events }] of entries) {
23
+ const absPath = join(opts.targetRepo, filePath);
24
+ const existing = await readHookFile(absPath);
25
+ const drifted = detectCopilotHookDrift(existing, events);
26
+ reads.push({
27
+ portableClass,
28
+ relPath: filePath,
29
+ absPath,
30
+ existing,
31
+ owned: events,
32
+ drifted,
33
+ });
34
+ }
35
+ const driftedFiles = reads.filter((r) => r.drifted).map((r) => r.relPath);
36
+ if (driftedFiles.length > 0 && !opts.allowOverwrite) {
37
+ return {
38
+ files: reads.map((r) => ({
39
+ relPath: r.relPath,
40
+ portableClass: r.portableClass,
41
+ drifted: r.drifted,
42
+ wrote: false,
43
+ })),
44
+ driftedRefused: driftedFiles,
45
+ refused: true,
46
+ };
47
+ }
48
+ if (opts.checkOnly) {
49
+ return {
50
+ files: reads.map((r) => ({
51
+ relPath: r.relPath,
52
+ portableClass: r.portableClass,
53
+ drifted: r.drifted,
54
+ wrote: false,
55
+ })),
56
+ driftedRefused: [],
57
+ refused: false,
58
+ };
59
+ }
60
+ // Second pass: upsert + write.
61
+ const fileResults = [];
62
+ for (const r of reads) {
63
+ const merge = upsertCopilotHookFile(r.existing, r.owned);
64
+ let wrote = false;
65
+ if (merge.changed || r.existing === null) {
66
+ await mkdir(dirname(r.absPath), { recursive: true });
67
+ await writeFile(r.absPath, JSON.stringify(merge.file, null, 2) + "\n", "utf8");
68
+ wrote = true;
69
+ }
70
+ fileResults.push({
71
+ relPath: r.relPath,
72
+ portableClass: r.portableClass,
73
+ drifted: r.drifted,
74
+ wrote,
75
+ });
76
+ }
77
+ return { files: fileResults, driftedRefused: [], refused: false };
78
+ }
79
+ async function readHookFile(absPath) {
80
+ if (!(await fileExists(absPath)))
81
+ return null;
82
+ const raw = await readFile(absPath, "utf8");
83
+ if (raw.trim().length === 0)
84
+ return null;
85
+ try {
86
+ const parsed = JSON.parse(raw);
87
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
88
+ const obj = parsed;
89
+ if (Array.isArray(obj["events"])) {
90
+ return obj;
91
+ }
92
+ }
93
+ }
94
+ catch {
95
+ /* fall through — treat malformed as empty so the next write reseats it */
96
+ }
97
+ return null;
98
+ }
99
+ async function fileExists(path) {
100
+ try {
101
+ await access(path);
102
+ return true;
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ //# sourceMappingURL=copilot-hooks-merge.js.map
@@ -0,0 +1,251 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ export const PROTECTED_PREFIXES = ["content/"];
5
+ /** True when every conflict is provably untouched since the last sync. */
6
+ export function allPristine(c) {
7
+ return (c.pristine.length > 0 &&
8
+ c.edited.length === 0 &&
9
+ c.unclassifiable.length === 0);
10
+ }
11
+ /**
12
+ * Classify each conflicting path against the digests the previous sync recorded.
13
+ *
14
+ * Compares the file ON DISK to the digest of what was last APPLIED — not to the
15
+ * incoming payload, which `findConflicts` has already established differs. A
16
+ * match therefore means "untouched since the last sync", which is precisely the
17
+ * question `--allow-overwrite` should have been asking.
18
+ */
19
+ export async function classifyConflicts(conflicts, targetRepo, priorDigests) {
20
+ const pristine = [];
21
+ const edited = [];
22
+ const unclassifiable = [];
23
+ for (const p of conflicts) {
24
+ const recorded = priorDigests?.[p];
25
+ if (!recorded) {
26
+ unclassifiable.push(p);
27
+ continue;
28
+ }
29
+ let actual;
30
+ try {
31
+ actual = sha256Buffer(await readFile(join(targetRepo, p)));
32
+ }
33
+ catch (err) {
34
+ // Unreadable is not "pristine". Anything we cannot hash is undecidable.
35
+ if (err.code === "ENOENT")
36
+ continue;
37
+ unclassifiable.push(p);
38
+ continue;
39
+ }
40
+ if (actual === recorded)
41
+ pristine.push(p);
42
+ else
43
+ edited.push(p);
44
+ }
45
+ return { pristine, edited, unclassifiable };
46
+ }
47
+ /** sha256 of each path's content under `root`, keyed by path — what a sync records. */
48
+ export async function digestManagedPaths(paths, root) {
49
+ const digests = {};
50
+ for (const p of paths) {
51
+ try {
52
+ digests[p] = sha256Buffer(await readFile(join(root, p)));
53
+ }
54
+ catch (err) {
55
+ // A path with no readable file records no digest, rather than a wrong one.
56
+ if (err.code !== "ENOENT")
57
+ throw err;
58
+ }
59
+ }
60
+ return digests;
61
+ }
62
+ function sha256Buffer(buf) {
63
+ return createHash("sha256").update(buf).digest("hex");
64
+ }
65
+ export function findProtectedPaths(paths) {
66
+ return paths.filter((p) => PROTECTED_PREFIXES.some((prefix) => p.startsWith(prefix)));
67
+ }
68
+ export async function findMissingPayloadFiles(incomingPaths, payloadDir) {
69
+ const missing = [];
70
+ for (const p of incomingPaths) {
71
+ try {
72
+ await readFile(join(payloadDir, p));
73
+ }
74
+ catch (err) {
75
+ if (err.code === "ENOENT")
76
+ missing.push(p);
77
+ else
78
+ throw err;
79
+ }
80
+ }
81
+ return missing;
82
+ }
83
+ export async function findConflicts(incomingPaths, payloadDir, targetRepo) {
84
+ const conflicts = [];
85
+ for (const p of incomingPaths) {
86
+ const targetPath = join(targetRepo, p);
87
+ let targetContent;
88
+ try {
89
+ targetContent = await readFile(targetPath);
90
+ }
91
+ catch (err) {
92
+ if (err.code === "ENOENT")
93
+ continue;
94
+ throw err;
95
+ }
96
+ const bundleContent = await readFile(join(payloadDir, p));
97
+ if (!targetContent.equals(bundleContent)) {
98
+ conflicts.push(p);
99
+ }
100
+ }
101
+ return conflicts;
102
+ }
103
+ /**
104
+ * Paths this CLI GENERATES or MERGES rather than materialising from a payload,
105
+ * and which must therefore never be removed by omission from a payload.
106
+ *
107
+ * WHY THIS EXISTS — measured, not anticipated
108
+ * `.claude/settings.json` used to be payload-shipped and is now merge-managed.
109
+ * A repo whose last sync recorded it in `managed_paths` therefore hits this:
110
+ * the path is absent from the incoming `MANAGED_PATHS.txt`, so `computeRemovals`
111
+ * schedules it for deletion — and `removeManagedFiles` runs BEFORE
112
+ * `syncMethodManagedSettings`, whose `readSettings` returns `{}` for a file that
113
+ * is not there. So the merge's documented contract ("adopter-added entries and
114
+ * other top-level keys are preserved") is void on exactly the transition where
115
+ * ownership moved, and the adopter's `permissions`, `env` and their own
116
+ * `mcpServers` entries are lost silently, with exit 0 and a "Refreshed" log line
117
+ * that says "added" because it was seeding from empty.
118
+ *
119
+ * That was measured on a real repo copy in a 30-repo estate, one apply short of
120
+ * shipping. 30 of 65 repos were scheduled for the deletion.
121
+ *
122
+ * THE INVARIANT — a merge-managed path is never payload-managed, so its absence
123
+ * from a payload carries NO information about whether it should exist. Only the
124
+ * component that owns it may decide that. Enforced here rather than at the call
125
+ * site so a second caller cannot reintroduce the defect: this is a property of
126
+ * the paths, not of any one sync flow.
127
+ *
128
+ * ⚠ Ownership migrations (payload → generated) are the general hazard. Any future
129
+ * path that moves this way must be added here IN THE SAME CHANGE, or the first
130
+ * adopter to sync across the migration silently loses whatever they had.
131
+ */
132
+ export const GENERATED_NEVER_REMOVE = [
133
+ // merge-managed: settings-merge.ts (adopter keys preserved by contract)
134
+ ".claude/settings.json",
135
+ // rendered: init/ci-workflow.ts
136
+ ".github/workflows/method-validation.yml",
137
+ // merge-managed: briefing-merge.ts (wrapper keeps adopter content below markers)
138
+ "CLAUDE.md",
139
+ "AGENTS.md",
140
+ "agent-instructions.md",
141
+ ];
142
+ /** `.github/hooks/<event>.json` — rendered per-event by hooks-seed/copilot.ts. */
143
+ const GENERATED_PREFIXES = [".github/hooks/"];
144
+ /** True when this path's existence is owned by a generator, not by a payload. */
145
+ export function isGeneratedPath(p) {
146
+ return (GENERATED_NEVER_REMOVE.includes(p) ||
147
+ GENERATED_PREFIXES.some((prefix) => p.startsWith(prefix)));
148
+ }
149
+ export function computeRemovals(prevPaths, incomingPaths, options) {
150
+ if (options.noDelete)
151
+ return [];
152
+ const incoming = new Set(incomingPaths);
153
+ return prevPaths.filter((p) => !incoming.has(p) && !isGeneratedPath(p));
154
+ }
155
+ /**
156
+ * SHADOW COLLISIONS — an incoming managed file whose basename already exists at
157
+ * a SHALLOWER, unmanaged path in the adopter's repo (ledger §3ap).
158
+ *
159
+ * When the distributed surface MOVES a file, copies previously delivered at the
160
+ * old location are stranded as unmanaged files. They keep resolving, so nothing
161
+ * breaks loudly, and they go stale silently as their managed twins evolve. One
162
+ * estate carried six such shadows, every one stale — including a rubric its own
163
+ * scoring tooling was still reading, so a repaired anchor shipped and was
164
+ * simultaneously unreachable through the tools meant to apply it.
165
+ *
166
+ * The quiet failure is the one that needs machinery: a rename that breaks its
167
+ * citations announces itself, and this one does not. Flagging the collision as
168
+ * it is created beats a per-estate census finding it months later.
169
+ *
170
+ * ⚠ BASENAME EQUALITY IS NECESSARY AND NOWHERE NEAR SUFFICIENT — learned on
171
+ * this detector's FIRST LIVE RUN, from the estate that reported the class.
172
+ * It fired on a repo's own top-level `README.md` against the managed
173
+ * `docs/method/README.md`: 24KB "# {Adopter Product}" versus 10KB "# `docs/method/`",
174
+ * two lines in common, two documents that merely share a filename. That is not
175
+ * a stranded copy and never could be, and it would have fired in 24 of 40 repos.
176
+ *
177
+ * The defect was building the detector from the CLASS'S SYMPTOM instead of its
178
+ * cause. In every real instance the shadow was a FORMER COPY of the managed
179
+ * file — same basename AND same subject — left behind by a move. So common
180
+ * ancestry is the discriminator, and `looksRelated` supplies it: a candidate
181
+ * must resemble the managed file's content, not merely echo its name.
182
+ *
183
+ * `exists` and `read` are injected so this is testable without a filesystem,
184
+ * and so the rule itself can be read without reading a sync flow.
185
+ */
186
+ export function detectShadowCollisions(managedPaths, exists, read) {
187
+ const managedSet = new Set(managedPaths);
188
+ const seenBasename = new Map();
189
+ for (const rel of managedPaths) {
190
+ const base = rel.slice(rel.lastIndexOf("/") + 1);
191
+ if (!seenBasename.has(base))
192
+ seenBasename.set(base, rel);
193
+ }
194
+ const shadows = [];
195
+ for (const [base, managedRel] of seenBasename) {
196
+ const parts = managedRel.split("/");
197
+ // Walk the managed path's own ancestors, shallowest first: a shadow is the
198
+ // same basename ABOVE the file that now owns the name. A deeper twin is a
199
+ // different artefact, not a stranded copy.
200
+ for (let cut = 0; cut < parts.length - 1; cut++) {
201
+ const candidate = [...parts.slice(0, cut), base].join("/");
202
+ if (candidate === managedRel)
203
+ continue;
204
+ // A path the incoming bundle also manages is not a shadow — it is
205
+ // delivered content, and sync owns both.
206
+ if (managedSet.has(candidate))
207
+ continue;
208
+ if (exists(candidate) && looksRelated(candidate, managedRel, read)) {
209
+ shadows.push({ shadow: candidate, managed: managedRel });
210
+ break;
211
+ }
212
+ }
213
+ }
214
+ return shadows;
215
+ }
216
+ /**
217
+ * Do these two files look like copies of one another?
218
+ *
219
+ * Deliberately crude and deliberately CONSERVATIVE: a stranded copy diverges by
220
+ * drift, so it keeps most of its lines; two unrelated files sharing a name share
221
+ * almost none. Without a reader (no filesystem access) it returns FALSE — the
222
+ * detector stays silent rather than guessing, because a false shadow costs a
223
+ * reader's attention on every sync while a missed one is found by the standing
224
+ * per-estate census.
225
+ */
226
+ function looksRelated(shadowPath, managedPath, read) {
227
+ if (!read)
228
+ return false;
229
+ const a = read(shadowPath);
230
+ const b = read(managedPath);
231
+ if (a === null || b === null)
232
+ return false;
233
+ const lines = (t) => new Set(t
234
+ .split("\n")
235
+ .map((l) => l.trim())
236
+ .filter((l) => l.length > 20));
237
+ const A = lines(a);
238
+ const B = lines(b);
239
+ if (A.size === 0 || B.size === 0)
240
+ return false;
241
+ let shared = 0;
242
+ for (const l of A)
243
+ if (B.has(l))
244
+ shared++;
245
+ // Share of the SMALLER file's substantive lines. A stale copy of a file that
246
+ // has since grown is still a copy, so the smaller side is the right
247
+ // denominator — using the larger one would miss exactly the drifted shadows
248
+ // this exists to catch.
249
+ return shared / Math.min(A.size, B.size) >= 0.3;
250
+ }
251
+ //# sourceMappingURL=diff.js.map