@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,206 @@
1
+ // Implementation-set snapshot manifests (`<area>/<baseline-id>/snapshot.yaml`)
2
+ // and the file-level primitives the `snapshot` verbs share.
3
+ //
4
+ // The shape is the method's, not this module's: reverse/artifact-conventions.md
5
+ // "Implementation-Set Snapshots". Where the conventions REQUIRE a companion
6
+ // field (`carried_reason` beside `carried_from`, `reverification` beside
7
+ // `reverified_at`), readManifest refuses a manifest without it — a carried member
8
+ // with no reason cannot be told apart from an unrecorded removal.
9
+ import { createHash } from "node:crypto";
10
+ import { readdir, readFile, stat } from "node:fs/promises";
11
+ import { join, relative, sep } from "node:path";
12
+ import { parse } from "yaml";
13
+ /** The line every later verb prints while a baseline carries unresolved credential findings. */
14
+ export function credentialNotice(m) {
15
+ const s = m.credential_scan;
16
+ if (!s || s.result === "clean")
17
+ return null;
18
+ return `CREDENTIAL FINDINGS on record for ${m.snapshot_id}: ${s.result} — ${s.disposition ?? "NO DISPOSITION"}`;
19
+ }
20
+ export class SnapshotError extends Error {
21
+ }
22
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
23
+ export function isIsoDate(s) {
24
+ if (typeof s !== "string" || !DATE.test(s))
25
+ return false;
26
+ const d = new Date(`${s}T00:00:00Z`);
27
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === s;
28
+ }
29
+ /** `name-branch@rev` → { stem: "name-branch", revision: "rev" }. */
30
+ export function splitFolder(folder) {
31
+ const at = folder.lastIndexOf("@");
32
+ if (at <= 0 || at === folder.length - 1)
33
+ return null;
34
+ return { stem: folder.slice(0, at), revision: folder.slice(at + 1) };
35
+ }
36
+ /** Member folders (`*@*` directories) directly under a baseline directory. */
37
+ export async function memberFolders(baselineDir) {
38
+ const out = [];
39
+ for (const e of await readdir(baselineDir, { withFileTypes: true })) {
40
+ if (e.isDirectory() && splitFolder(e.name) !== null)
41
+ out.push(e.name);
42
+ }
43
+ return out.sort();
44
+ }
45
+ /** Dated baseline ids under an area, ascending. */
46
+ export async function baselineIds(areaRoot) {
47
+ const out = [];
48
+ for (const e of await readdir(areaRoot, { withFileTypes: true })) {
49
+ if (e.isDirectory() && isIsoDate(e.name))
50
+ out.push(e.name);
51
+ }
52
+ return out.sort();
53
+ }
54
+ export async function exists(p) {
55
+ try {
56
+ await stat(p);
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ export function manifestPath(areaRoot, id) {
64
+ return join(areaRoot, id, "snapshot.yaml");
65
+ }
66
+ export async function readManifest(areaRoot, id) {
67
+ const p = manifestPath(areaRoot, id);
68
+ if (!(await exists(p)))
69
+ throw new SnapshotError(`no manifest at ${p}`);
70
+ const raw = parse(await readFile(p, "utf8"));
71
+ if (raw === null || typeof raw !== "object")
72
+ throw new SnapshotError(`${p}: not a mapping`);
73
+ const problems = [];
74
+ if (raw.snapshot_id !== id)
75
+ problems.push(`snapshot_id '${String(raw.snapshot_id)}' does not match its folder '${id}'`);
76
+ if (!isIsoDate(raw.observed_at))
77
+ problems.push("observed_at must be YYYY-MM-DD");
78
+ if (typeof raw.implementation !== "string" || raw.implementation === "")
79
+ problems.push("implementation missing");
80
+ if (raw.prior_snapshot !== null && raw.prior_snapshot !== undefined && !isIsoDate(raw.prior_snapshot)) {
81
+ problems.push("prior_snapshot must be a baseline id or null");
82
+ }
83
+ if (!Array.isArray(raw.members))
84
+ problems.push("members must be a list");
85
+ // A hand-written revision of digits only (`50834756022`) parses as a NUMBER, and
86
+ // a strict comparison with the folder's string suffix then calls an unchanged
87
+ // member changed. Identity fields are strings, whatever YAML made of them.
88
+ for (const m of raw.members ?? []) {
89
+ if (m && typeof m === "object") {
90
+ for (const f of ["canonical_repo_key", "exported_folder_name", "source_revision"]) {
91
+ if (typeof m[f] === "number")
92
+ m[f] = String(m[f]);
93
+ }
94
+ }
95
+ }
96
+ for (const r of raw.members_removed ?? []) {
97
+ if (r && typeof r.last_revision === "number")
98
+ r.last_revision = String(r.last_revision);
99
+ }
100
+ for (const [i, m] of (raw.members ?? []).entries()) {
101
+ const at = `members[${i}] (${m?.canonical_repo_key ?? "?"})`;
102
+ if (!m?.canonical_repo_key || !m.exported_folder_name || !m.source_revision) {
103
+ problems.push(`${at}: canonical_repo_key, exported_folder_name and source_revision are required`);
104
+ }
105
+ if (m?.carried_from !== undefined && !m.carried_reason)
106
+ problems.push(`${at}: carried_from requires carried_reason`);
107
+ if (m?.reverified_at !== undefined && !m.reverification)
108
+ problems.push(`${at}: reverified_at requires reverification`);
109
+ }
110
+ for (const [i, r] of (raw.members_removed ?? []).entries()) {
111
+ if (!r?.canonical_repo_key || !r.last_revision || !r.reason) {
112
+ problems.push(`members_removed[${i}]: canonical_repo_key, last_revision and reason are required`);
113
+ }
114
+ }
115
+ if (problems.length > 0)
116
+ throw new SnapshotError(`${p}:\n - ${problems.join("\n - ")}`);
117
+ return raw;
118
+ }
119
+ /** Where a member's bytes live: its own baseline, or the one it was carried from. */
120
+ export function memberBytesDir(areaRoot, manifest, m) {
121
+ return join(areaRoot, m.carried_from ?? manifest.snapshot_id, m.exported_folder_name);
122
+ }
123
+ /** relative path → sha256 for every file under a directory. */
124
+ export async function hashTree(root) {
125
+ const out = new Map();
126
+ async function walk(dir) {
127
+ for (const e of await readdir(dir, { withFileTypes: true })) {
128
+ const full = join(dir, e.name);
129
+ if (e.isDirectory()) {
130
+ if (e.name === ".git")
131
+ continue;
132
+ await walk(full);
133
+ }
134
+ else if (e.isFile()) {
135
+ const h = createHash("sha256").update(await readFile(full)).digest("hex");
136
+ out.set(relative(root, full).split(sep).join("/"), h);
137
+ }
138
+ }
139
+ }
140
+ await walk(root);
141
+ return out;
142
+ }
143
+ export function diffTrees(before, after) {
144
+ const added = [];
145
+ const removed = [];
146
+ const modified = [];
147
+ for (const [p, h] of after) {
148
+ const b = before.get(p);
149
+ if (b === undefined)
150
+ added.push(p);
151
+ else if (b !== h)
152
+ modified.push(p);
153
+ }
154
+ for (const p of before.keys())
155
+ if (!after.has(p))
156
+ removed.push(p);
157
+ return { added: added.sort(), removed: removed.sort(), modified: modified.sort() };
158
+ }
159
+ const BRANCH_SUFFIX = /-(master|main|develop|trunk)$/;
160
+ /**
161
+ * The repository ids an SDD declares: every `orchestration/repo-config/**\/<id>.yaml` file
162
+ * name and every id in its built `catalogues/repositories.csv`. Empty when neither exists.
163
+ */
164
+ export async function knownRepositoryIds(sddPath) {
165
+ const out = new Set();
166
+ const walk = async (dir) => {
167
+ if (!(await exists(dir)))
168
+ return;
169
+ for (const e of await readdir(dir, { withFileTypes: true })) {
170
+ if (e.isDirectory())
171
+ await walk(join(dir, e.name));
172
+ else if (/\.ya?ml$/.test(e.name))
173
+ out.add(e.name.replace(/\.ya?ml$/, ""));
174
+ }
175
+ };
176
+ await walk(join(sddPath, "orchestration", "repo-config"));
177
+ const csv = join(sddPath, "catalogues", "repositories.csv");
178
+ if (await exists(csv)) {
179
+ for (const line of (await readFile(csv, "utf8")).split("\n").slice(1)) {
180
+ const id = line.split(",")[0]?.trim();
181
+ if (id)
182
+ out.add(id);
183
+ }
184
+ }
185
+ return out;
186
+ }
187
+ /**
188
+ * A canonical_repo_key derived from a folder's stem, checked. Stripping a branch
189
+ * suffix only works for the four names it knows: a `-_dev` or `-release-1.X` branch
190
+ * leaves a key that is no repository id, and nothing downstream says so until a
191
+ * catalogue build cannot resolve it. With the SDD's ids known, such a key is a
192
+ * PROBLEM naming the nearest id; without them it is marked unverified.
193
+ */
194
+ export function deriveKey(folder, stem, known) {
195
+ const key = stem.replace(BRANCH_SUFFIX, "");
196
+ if (known.size === 0)
197
+ return { key, unverified: true };
198
+ if (known.has(key))
199
+ return { key };
200
+ const nearest = [...known].filter((id) => stem.startsWith(`${id}-`)).sort((a, b) => b.length - a.length)[0];
201
+ return {
202
+ key,
203
+ problem: `${folder}: derived canonical_repo_key '${key}' is not a repository id of this SDD — ${nearest ? `nearest is '${nearest}' (a branch suffix?): pass --key ${folder}=${nearest}` : `pass --key ${folder}=<repository id>`}`,
204
+ };
205
+ }
206
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1,250 @@
1
+ // ADR transitions (R6 item 12) — the bundle-declared rename / withdrawal /
2
+ // reassignment log, applied at sync.
3
+ //
4
+ // Every method-baseline bundle embeds `ADR_TRANSITIONS.yaml`. Its stated purpose:
5
+ // "Adopters apply transitions during sync to keep their inherited ADR set aligned
6
+ // with upstream without manual `git rm` / `git mv` cleanup." 95 transitions ship in
7
+ // each baseline today, so this is live, not theoretical.
8
+ //
9
+ // The bash reference implemented it; sdd-cli did not, and the gap was invisible
10
+ // because the parity oracle it was compared against also predated the feature.
11
+ //
12
+ // WHY REMOVAL-BY-OMISSION IS NOT AN ADEQUATE SUBSTITUTE. Sync already deletes any
13
+ // previously-managed path missing from the incoming bundle, which incidentally makes
14
+ // a rename look right (old path deleted, new path copied). But the three kinds have
15
+ // semantics that differ from delete-and-add in ways that matter:
16
+ //
17
+ // - rename: the file should MOVE, so an adopter's local edits travel with it
18
+ // and git sees a rename. Delete-plus-copy discards local edits and
19
+ // loses the history link. Under `--no-delete` it leaves BOTH paths.
20
+ // - withdrawal: the schema is explicit — "do NOT auto-delete the file (the
21
+ // adopter may still depend on the decision). Show the
22
+ // `adopter_action` text. Sync continues." Removal-by-omission does
23
+ // the opposite: it deletes silently. So withdrawn paths must be
24
+ // PROTECTED from removal, which is the one place this module has to
25
+ // reach into the rest of sync.
26
+ // - reassignment: an ADR number now means a different decision. There is no file
27
+ // operation at all — the only correct behaviour is to say so.
28
+ //
29
+ // Version-gated on the bundle_version previously applied FOR THIS BUNDLE, so a
30
+ // re-sync does not re-announce transitions the adopter has already taken.
31
+ import { access, mkdir, rename } from "node:fs/promises";
32
+ import { dirname, join, posix } from "node:path";
33
+ import { readFile } from "node:fs/promises";
34
+ import { parse as parseYaml } from "yaml";
35
+ const EMPTY = { renames: [], warnings: [], protectedFromRemoval: [], movedPaths: [] };
36
+ /**
37
+ * Compare a semver-ish pair, tolerating a leading `v`.
38
+ *
39
+ * The lock records `bundle_version` as `v1.2.0` while `introduced_in_version` is
40
+ * written bare (`1.3.0`). The bash implementation carries the same normalisation
41
+ * with a comment noting that without it "every rename transition is silently
42
+ * skipped" — worth keeping, because the failure is silent in both languages.
43
+ */
44
+ export function versionLessThan(a, b) {
45
+ const parts = (v) => v.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
46
+ const [x, y] = [parts(a), parts(b)];
47
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
48
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
49
+ if (d !== 0)
50
+ return d < 0;
51
+ }
52
+ return false;
53
+ }
54
+ async function exists(p) {
55
+ try {
56
+ await access(p);
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ /**
64
+ * Build the transition plan for this sync.
65
+ *
66
+ * @param bundleRootDir extracted bundle root (holds ADR_TRANSITIONS.yaml)
67
+ * @param targetRepo adopter repo root
68
+ * @param previousVersion bundle_version last applied for THIS bundle, or undefined
69
+ * on a first sync — in which case every transition is in
70
+ * scope, because the adopter has taken none of them.
71
+ */
72
+ export async function planAdrTransitions(bundleRootDir, targetRepo, previousVersion) {
73
+ const file = join(bundleRootDir, "ADR_TRANSITIONS.yaml");
74
+ if (!(await exists(file)))
75
+ return EMPTY;
76
+ let raw;
77
+ try {
78
+ raw = parseYaml(await readFile(file, "utf8"));
79
+ }
80
+ catch {
81
+ // A malformed transitions file must not block a sync: the payload is still
82
+ // valid and the adopter's tree is still correct without the cleanup.
83
+ return { renames: [], warnings: ["ADR_TRANSITIONS.yaml could not be parsed — transitions skipped"], protectedFromRemoval: [], movedPaths: [] };
84
+ }
85
+ const list = raw?.transitions;
86
+ if (!Array.isArray(list))
87
+ return EMPTY;
88
+ const renames = [];
89
+ const rawWarnings = [];
90
+ const protectedFromRemoval = [];
91
+ const movedPaths = [];
92
+ /**
93
+ * FIRST ADOPTION replays the bundle's whole transition history (ledger §3ae).
94
+ * A repo adopting a bundle for the first time receives its CURRENT state, so
95
+ * every transition in the log happened before this repo existed as an
96
+ * adopter — yet the log is filtered only by "newer than the last applied
97
+ * version", and there is no last applied version, so nothing is filtered.
98
+ * One estate met an ancient reassignment as a fresh warning carrying a
99
+ * colon-form path that read like rename residue to post-wave eyes.
100
+ *
101
+ * Labelled rather than suppressed, deliberately: the notices are TRUE, and a
102
+ * first adopter inheriting a decision that moved slots may still want to know.
103
+ * What was wrong was their apparent currency, not their content — so the fix
104
+ * is to say when they happened, not to hide them.
105
+ */
106
+ const firstAdoption = previousVersion === undefined || previousVersion === "";
107
+ for (const entry of list) {
108
+ if (typeof entry !== "object" || entry === null)
109
+ continue;
110
+ const kind = String(entry.kind ?? "");
111
+ const adrId = String(entry.adr_id ?? "");
112
+ const introduced = String(entry.introduced_in_version ?? "");
113
+ // Only what is newer than the last applied version. On a first sync
114
+ // (previousVersion undefined) nothing is filtered out.
115
+ if (previousVersion && introduced && !versionLessThan(previousVersion, introduced)) {
116
+ continue;
117
+ }
118
+ switch (kind) {
119
+ case "move":
120
+ case "rename": {
121
+ const fromPath = String(entry.from_path ?? "");
122
+ const toPath = String(entry.to_path ?? "");
123
+ if (!fromPath || !toPath)
124
+ continue;
125
+ const label = kind === "move" ? "move" : `ADR ${adrId} rename`;
126
+ // A rename to itself is a no-op and MUST be silent. Not hypothetical: 92 of
127
+ // the 93 rename entries shipping today have from_path === to_path, because
128
+ // the generator derives from_path from the canon name while the bundle's
129
+ // publish transform has already renamed the file — so by the time the log is
130
+ // embedded, the two have collapsed. Without this guard every one of those
131
+ // would find "both paths exist" (they are the same file) and emit a
132
+ // manual-review warning, burying the real notices in ~92 lines of noise.
133
+ if (fromPath === toPath)
134
+ continue;
135
+ movedPaths.push({ fromPath, toPath, label });
136
+ const fromExists = await exists(join(targetRepo, fromPath));
137
+ const toExists = await exists(join(targetRepo, toPath));
138
+ if (fromExists && toExists) {
139
+ // Both present: the adopter may have copied rather than moved, or has
140
+ // content at both names. Renaming over the top could destroy work.
141
+ rawWarnings.push(`[${label}] both paths exist in adopter repo; manual review needed: ${fromPath} → ${toPath}`);
142
+ }
143
+ else if (fromExists) {
144
+ renames.push({ adrId, fromPath, toPath });
145
+ }
146
+ // Only `toPath` present → already migrated, nothing to do.
147
+ break;
148
+ }
149
+ case "withdrawal": {
150
+ const withdrawnPath = String(entry.withdrawn_path ?? "");
151
+ if (!withdrawnPath)
152
+ continue;
153
+ // Protected regardless of presence: it must not be removed by omission on
154
+ // this sync even if a concurrent step would otherwise have swept it.
155
+ protectedFromRemoval.push(withdrawnPath);
156
+ if (await exists(join(targetRepo, withdrawnPath))) {
157
+ const action = String(entry.adopter_action ?? "").trim();
158
+ rawWarnings.push(`[ADR ${adrId} withdrawal] ${withdrawnPath} is still present.` +
159
+ (action ? ` ${action}` : ""));
160
+ }
161
+ break;
162
+ }
163
+ case "reassignment": {
164
+ const newDecision = String(entry.new_decision_path ?? "");
165
+ rawWarnings.push(`[ADR ${adrId} reassignment] slot now points to a different decision: ${newDecision}`);
166
+ break;
167
+ }
168
+ default:
169
+ break;
170
+ }
171
+ }
172
+ const warnings = firstAdoption
173
+ ? rawWarnings.map((w) => `${w} — HISTORICAL REPLAY: this transition predates this repo's adoption of the bundle, ` +
174
+ `so it is a record of what happened, not a change arriving now`)
175
+ : rawWarnings;
176
+ return { renames, warnings, protectedFromRemoval, movedPaths };
177
+ }
178
+ const CITATION_LINK = /\]\(([^)\s#]+)(?:#[^)\s]*)?\)/g;
179
+ /**
180
+ * Citations an adopter's OWN documents still make to a path that moved.
181
+ *
182
+ * A move strands its citations, not just the file: one estate held 33 citations
183
+ * resolving to stale copies and 25 dangling (four gate thresholds resolving to
184
+ * nothing) after a rubric reorg. Ruled 2026-09-29: report each, never rewrite —
185
+ * the quiet case (a citation that still resolves, to the wrong copy) is exactly
186
+ * the one an automatic rewrite could get wrong.
187
+ *
188
+ * A citation is the moved path written literally (repo-relative), or a Markdown
189
+ * link whose target resolves to it from the citing file. Method-managed files are
190
+ * excluded: the bundle already ships them corrected.
191
+ */
192
+ export async function findStrandedCitations(targetRepo, moved, managed, trackedTextFiles) {
193
+ if (moved.length === 0)
194
+ return [];
195
+ const out = [];
196
+ for (const file of trackedTextFiles) {
197
+ if (managed.has(file))
198
+ continue;
199
+ let text;
200
+ try {
201
+ text = await readFile(join(targetRepo, file), "utf8");
202
+ }
203
+ catch {
204
+ continue;
205
+ }
206
+ const lines = text.split("\n");
207
+ for (let i = 0; i < lines.length; i++) {
208
+ const line = lines[i];
209
+ for (const m of moved) {
210
+ let hit = line.includes(m.fromPath);
211
+ if (!hit && line.includes("](")) {
212
+ for (const link of line.matchAll(CITATION_LINK)) {
213
+ const target = decodeURIComponent(link[1]);
214
+ if (/^[a-z]+:/i.test(target))
215
+ continue;
216
+ if (posix.normalize(posix.join(posix.dirname(file), target)) === m.fromPath)
217
+ hit = true;
218
+ }
219
+ }
220
+ if (hit) {
221
+ out.push(`${file}:${i + 1}: cites ${m.fromPath}, which moved to ${m.toPath} (${m.label}) — update the citation`);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ return out;
227
+ }
228
+ /**
229
+ * Apply the planned renames. Uses a real move so an adopter's local edits travel
230
+ * with the file and git can see a rename, which is the whole point of doing this
231
+ * rather than letting delete-plus-copy approximate it.
232
+ */
233
+ export async function applyAdrRenames(plan, targetRepo) {
234
+ let applied = 0;
235
+ for (const r of plan.renames) {
236
+ const from = join(targetRepo, r.fromPath);
237
+ const to = join(targetRepo, r.toPath);
238
+ try {
239
+ await mkdir(dirname(to), { recursive: true });
240
+ await rename(from, to);
241
+ applied++;
242
+ }
243
+ catch {
244
+ // A rename that cannot be applied is not fatal: the file-copy phase will
245
+ // still place the new path, leaving the old one for the adopter to resolve.
246
+ }
247
+ }
248
+ return applied;
249
+ }
250
+ //# sourceMappingURL=adr-transitions.js.map
@@ -0,0 +1,91 @@
1
+ import { copyFile, mkdir, readdir, rm, rmdir } from "node:fs/promises";
2
+ import { dirname, join, sep } from "node:path";
3
+ export async function copyManagedFiles(incomingPaths, payloadDir, targetRepo) {
4
+ for (const p of incomingPaths) {
5
+ const src = join(payloadDir, p);
6
+ const dst = join(targetRepo, p);
7
+ await mkdir(dirname(dst), { recursive: true });
8
+ await copyFile(src, dst);
9
+ }
10
+ }
11
+ /**
12
+ * Withdraw method-managed files, then remove the directories the withdrawal
13
+ * itself emptied.
14
+ *
15
+ * Without the second half, `sync` left the skeleton of everything it had ever
16
+ * shipped behind: measured 156 empty directories across 13 reverse-estate repos
17
+ * (12 `docs/method/methods/skills/reverse-*` dirs each, from the v2.3.0 wave
18
+ * moving reverse skills out of the forward bases). Git does not track empty
19
+ * directories, so no commit ever carries them and no diff ever shows them —
20
+ * they are invisible to review and live only in working copies, which is exactly
21
+ * why they accumulated unnoticed.
22
+ *
23
+ * TWO CONDITIONS, and the second is the one that matters. A directory is pruned
24
+ * only if it is (1) empty, and (2) DERIVED FROM THE WITHDRAWAL SET — that is, it
25
+ * is an ancestor directory of a path this call actually removed. Emptiness alone
26
+ * is not sufficient: an adopter's own empty directory that happens to sit beside
27
+ * the withdrawal tree is not ours to delete, and deleting it would be an
28
+ * unrecoverable side effect of a sync that was asked only to withdraw files.
29
+ * The walk stops at the first directory failing either condition, so a parent
30
+ * that still holds adopter content survives with its remaining entries intact —
31
+ * the real-world shape being 12 emptied `reverse-*` dirs beside 3 populated
32
+ * non-reverse skills under one `skills/` parent, where the walk must stop at
33
+ * `skills/`.
34
+ *
35
+ * Returns the pruned directories, repo-relative and deepest-first, so the caller
36
+ * reports each one. A pruned directory is an action taken on an adopter's tree,
37
+ * and this codebase does not take silent actions.
38
+ */
39
+ export async function removeManagedFiles(paths, targetRepo) {
40
+ for (const p of paths) {
41
+ await rm(join(targetRepo, p), { force: true });
42
+ }
43
+ // Every directory the withdrawal set could legitimately have emptied: each
44
+ // removed path's ancestor chain, up to but excluding the repo root. Built
45
+ // from `paths` rather than from the filesystem, so nothing outside what this
46
+ // call withdrew is ever a candidate.
47
+ const candidates = new Set();
48
+ for (const p of paths) {
49
+ const parts = p.split(/[/\\]/).filter((s) => s !== "" && s !== ".");
50
+ for (let i = parts.length - 1; i > 0; i--) {
51
+ candidates.add(parts.slice(0, i).join(sep));
52
+ }
53
+ }
54
+ // Deepest first, so a child is always considered before its parent and a
55
+ // chain of emptied directories collapses in one pass.
56
+ //
57
+ // The name tiebreak is BYTE ORDER, not `localeCompare`, and that is a contract
58
+ // rather than a preference: `sync-method-baseline.sh` prunes and reports the
59
+ // same directories, and tests/parity asserts the two produce byte-identical
60
+ // stdout. `localeCompare` resolves through ICU while the script's `sort`
61
+ // resolves through glibc — two different collation engines, both sensitive to
62
+ // the ambient locale, so a byte-identical contract resting on them would pass
63
+ // or fail by environment. The script pins `LC_ALL=C`; this pins the same order
64
+ // here, so the tiebreak is defined by the method and not by the machine.
65
+ const ordered = [...candidates].sort((a, b) => b.split(sep).length - a.split(sep).length ||
66
+ (a < b ? -1 : a > b ? 1 : 0));
67
+ const pruned = [];
68
+ for (const rel of ordered) {
69
+ const abs = join(targetRepo, rel);
70
+ let entries;
71
+ try {
72
+ entries = await readdir(abs);
73
+ }
74
+ catch {
75
+ continue; // already gone, or never existed
76
+ }
77
+ if (entries.length > 0)
78
+ continue; // adopter content remains — stop here
79
+ try {
80
+ await rmdir(abs);
81
+ pruned.push(rel);
82
+ }
83
+ catch {
84
+ // Racing writer, permissions, or a mount point. A directory we could not
85
+ // remove is left alone rather than escalated: the files are withdrawn,
86
+ // which is what the caller asked for.
87
+ }
88
+ }
89
+ return pruned;
90
+ }
91
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Briefing-layer sync per ADR 0148.
3
+ *
4
+ * Two files participate, with different semantics:
5
+ *
6
+ * - `agent-instructions.md` — fully method-managed; unconditional
7
+ * overwrite on every sync. No marker-aware merge.
8
+ * - Harness wrapper (`CLAUDE.md` / `AGENTS.md` / …) — partially
9
+ * method-managed. The `<!-- method-managed: begin/end -->` block
10
+ * is overwritten; adopter content around it survives via the
11
+ * existing `writeMarkedBlockFile` machinery (same pattern as
12
+ * `.github/workflows/method-validation.yml`). Blank regions
13
+ * around the block are scaffold-filled from the template so
14
+ * template-shape changes (heading, adopter-guidance scaffold)
15
+ * propagate instead of drifting — non-blank adopter regions are
16
+ * never touched.
17
+ *
18
+ * Both files are rendered from templates that ship in the bundle at
19
+ * `<target>/orchestration/templates/briefing/...`. Older bundles
20
+ * pre-dating ADR 0148 won't carry the templates; this module skips
21
+ * gracefully with a log line in that case.
22
+ */
23
+ import { access, mkdir, writeFile } from "node:fs/promises";
24
+ import { dirname, join } from "node:path";
25
+ import { AGENT_INSTRUCTIONS_REL_PATH, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, harnessWrapperOutputPath, renderAgentInstructions, renderHarnessWrapper, resolveBriefingVariables, splitWrapperTemplate, } from "../briefing/index.js";
26
+ import { writeMarkedBlockFile } from "./yaml-block-merge.js";
27
+ export async function syncBriefingFiles(opts) {
28
+ const log = [];
29
+ const vars = await resolveBriefingVariables({
30
+ targetRepo: opts.targetRepo,
31
+ repoKindOverride: opts.repoKind,
32
+ baselineVersionOverride: opts.baselineVersion,
33
+ });
34
+ // 1. agent-instructions.md — unconditional overwrite.
35
+ const aiRender = await renderAgentInstructions(opts.targetRepo, vars);
36
+ if (aiRender === null) {
37
+ log.push(`Skipped ${AGENT_INSTRUCTIONS_REL_PATH}: bundle does not ship a briefing template (pre-ADR-0148 baseline).`);
38
+ // If the agent-instructions template is missing the wrapper one almost
39
+ // certainly is too — skip the rest as well.
40
+ return { log, refused: null };
41
+ }
42
+ if (aiRender.unresolved.length > 0) {
43
+ log.push(`Warning: agent-instructions.md has unresolved placeholders: ${aiRender.unresolved.join(", ")}`);
44
+ }
45
+ const aiPath = join(opts.targetRepo, AGENT_INSTRUCTIONS_REL_PATH);
46
+ await mkdir(dirname(aiPath), { recursive: true });
47
+ await writeFile(aiPath, aiRender.content, "utf8");
48
+ log.push(`Refreshed ${AGENT_INSTRUCTIONS_REL_PATH}.`);
49
+ // 2. Harness wrapper — marker-aware merge.
50
+ const wrapperRender = await renderHarnessWrapper(opts.targetRepo, opts.agentHarness, vars);
51
+ if (wrapperRender === null) {
52
+ log.push(`Skipped ${harnessWrapperOutputPath(opts.agentHarness)}: bundle does not ship a wrapper template for ${opts.agentHarness}.`);
53
+ return { log, refused: null };
54
+ }
55
+ if (wrapperRender.unresolved.length > 0) {
56
+ log.push(`Warning: ${harnessWrapperOutputPath(opts.agentHarness)} has unresolved placeholders: ${wrapperRender.unresolved.join(", ")}`);
57
+ }
58
+ const wrapperRel = harnessWrapperOutputPath(opts.agentHarness);
59
+ const wrapperPath = join(opts.targetRepo, wrapperRel);
60
+ // Mirror the ci-workflow sync pattern: when the target file does not
61
+ // yet exist, write the full rendered template (block + adopter
62
+ // section template). When it exists, write the method-managed block
63
+ // and scaffold-fill blank regions around it from the template —
64
+ // writeMarkedBlockFile preserves non-blank adopter content verbatim.
65
+ const wrapperExists = await fileExists(wrapperPath);
66
+ const templateParts = splitWrapperTemplate(wrapperRender.content);
67
+ const incoming = wrapperExists
68
+ ? templateParts.block
69
+ : wrapperRender.content;
70
+ const mergeResult = await writeMarkedBlockFile(wrapperPath, incoming, { begin: WRAPPER_BLOCK_BEGIN, end: WRAPPER_BLOCK_END }, opts.allowOverwrite, wrapperExists
71
+ ? { pre: templateParts.pre, post: templateParts.post }
72
+ : undefined);
73
+ if (mergeResult.refused) {
74
+ return { log, refused: wrapperRel };
75
+ }
76
+ if (mergeResult.changed) {
77
+ log.push(`Refreshed ${wrapperRel}.`);
78
+ }
79
+ return { log, refused: null };
80
+ }
81
+ async function fileExists(path) {
82
+ try {
83
+ await access(path);
84
+ return true;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ //# sourceMappingURL=briefing-merge.js.map