@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,233 @@
1
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { ORIGIN_PREFIX_SRC, originPrefixForNumber, originPrefixForPath, } from "../adr-origin.js";
4
+ export { originPrefixForNumber };
5
+ /**
6
+ * Publish transform (sdd-method ADR 0183 / 0184). Produces the point-in-time,
7
+ * producer-agnostic *published* form of method markdown by subtraction from the
8
+ * canon, and gates the result with a fail-closed deny-class lint.
9
+ *
10
+ * The strip is a pure structural transform — it removes marked structures and
11
+ * never reads meaning, so it cannot misinterpret. The lint runs on the produced
12
+ * output and proves nothing leaked; it never decides what to strip. Authors
13
+ * carry the burden of placing evolution / producer content in the structured
14
+ * places (lineage regions, stripped fields), enforced at author/CI time.
15
+ *
16
+ * Enabled only for method-baseline builds (BuildOptions.publishTransform);
17
+ * adopter-pack builds are unaffected.
18
+ */
19
+ // Header fields stripped on publish (ADR 0184 — adr-metadata-schema.md).
20
+ const STRIPPED_FIELDS = [
21
+ "Status History",
22
+ "Acceptance Rationale",
23
+ "Supersedes",
24
+ "Superseded By",
25
+ "Amends",
26
+ "Source",
27
+ "Grounding",
28
+ "Introduced In",
29
+ ];
30
+ const STRIPPED_FIELD_RE = new RegExp(`^\\* \\*\\*(${STRIPPED_FIELDS.join("|")}):\\*\\*`);
31
+ const LINEAGE_START = "<!-- lineage:start -->";
32
+ const LINEAGE_END = "<!-- lineage:end -->";
33
+ // Published-surface deny-classes. Kept in lock-step with the reference gate in
34
+ // sdd-method orchestration/scripts/validate-adr-structure.sh (--publish-lint).
35
+ // A line carrying `pit-ok` is exempt; a file carrying `pit-ok-file` is exempt
36
+ // wholesale (a meta/versioning ADR whose deny-vocabulary IS its subject).
37
+ const DENY_TEMPORAL = [
38
+ /since retired/i,
39
+ /previously/i,
40
+ /formerly/i,
41
+ /supersed/i,
42
+ /no longer/i,
43
+ /pre-federation/i,
44
+ /used to/i,
45
+ /bumps? to/i,
46
+ /sdd-cli [0-9]/i,
47
+ /\bv[0-9]+\.[0-9]+\.[0-9]+/i,
48
+ ];
49
+ const DENY_PRODUCER = [
50
+ /sdd-method#/i,
51
+ /sdd-method-gov/i,
52
+ /sdd-method repo/i,
53
+ /the method repo/i,
54
+ /governance repo/i,
55
+ /synced from (sdd-method|the method)/i,
56
+ /bootstrap.?procedure/i,
57
+ ];
58
+ /** True when the markdown is an ADR with `Status: Accepted`. */
59
+ export function isAccepted(content) {
60
+ const m = content.match(/^\*\s+\*\*Status:\*\*\s*([A-Za-z]+)/m);
61
+ return m?.[1] === "Accepted";
62
+ }
63
+ /** Path is an ADR file (under an `adr/` or `platform-adr/` dir), bare or prefixed. */
64
+ export function isAdrPath(relPath) {
65
+ return new RegExp(`adr\\/${ORIGIN_PREFIX_SRC}ADR \\d`, "i").test(relPath);
66
+ }
67
+ /** Remove lineage regions + stripped-class header fields. Deterministic. */
68
+ export function stripStructures(content) {
69
+ const out = [];
70
+ let inLineage = false;
71
+ for (const line of content.split("\n")) {
72
+ if (line.includes(LINEAGE_START)) {
73
+ inLineage = true;
74
+ continue;
75
+ }
76
+ if (line.includes(LINEAGE_END)) {
77
+ inLineage = false;
78
+ continue;
79
+ }
80
+ if (inLineage)
81
+ continue;
82
+ if (STRIPPED_FIELD_RE.test(line))
83
+ continue;
84
+ out.push(line);
85
+ }
86
+ return out.join("\n");
87
+ }
88
+ /**
89
+ * Flatten cross-repo references to the flat in-namespace form (M-ADR 0184 §4.3):
90
+ * `sdd-method#<id>` becomes the canonical prefixed identifier for that number's
91
+ * band (`sdd-method#0174` → `M-ADR 0174`), never a bare `ADR` (M-ADR 0186 — no
92
+ * bare ADR on any SDD surface). Numbers outside the assigned bands fall back to
93
+ * the bare form.
94
+ */
95
+ export function flattenCrossRefs(content) {
96
+ return content.replace(/\bsdd-method#(\d{3,4})\b/g, (_m, num) => {
97
+ const p = originPrefixForNumber(num);
98
+ return p ? `${p} ${num}` : `ADR ${num}`;
99
+ });
100
+ }
101
+ /**
102
+ * Rewrite bare in-namespace `ADR NNNN` refs to their PRODUCER origin prefix by
103
+ * number band (`0xxx`→`M-ADR`, `9xxx`→`RA-ADR`). Adopter refs (`1000–8999`) are
104
+ * left bare here — their `P-`/`D-` prefix is scope-derived (M-ADR 0186 §4.3) and
105
+ * applied by the estate rename from a corpus map, not from the number alone.
106
+ * Matches the prose form ("ADR 0140") and the URL-encoded link-target form
107
+ * ("ADR%200140"), separator preserved, so renamed files and their hrefs stay in
108
+ * lock-step. The `(?<![\w-])` guard leaves an already-prefixed ref untouched
109
+ * (the `-` before `ADR` fails the lookbehind) — idempotent.
110
+ */
111
+ export function applyOriginPrefix(content) {
112
+ return content.replace(/(?<![\w-])ADR(%20| )(\d{3,4})(?!\d)/g, (m, sep, num) => {
113
+ const p = originPrefixForNumber(num);
114
+ return p ? `${p}${sep}${num}` : m;
115
+ });
116
+ }
117
+ /** @deprecated Back-compat alias for `applyOriginPrefix` (producer-band prefixing). */
118
+ export const applyMAdrPrefix = applyOriginPrefix;
119
+ /**
120
+ * The canonical filename for an ADR: the *basename* `ADR NNNN…` gains its origin
121
+ * prefix (`M-ADR`/`RA-ADR` by number band, `P-ADR`/`D-ADR` by scope — M-ADR 0186).
122
+ * The containing directory is never touched, only the leading `ADR ` of the
123
+ * basename; the directory is what determines an adopter ADR's scope. Returns the
124
+ * path unchanged when it is not an `ADR NNNN` file or the prefix is undeterminable.
125
+ * Idempotent: an already-prefixed name is left alone.
126
+ */
127
+ export function mAdrFilename(relPath) {
128
+ return relPath.replace(/(^|\/)ADR (\d{3,4})/, (m, pre, num) => {
129
+ const p = originPrefixForPath(relPath, num);
130
+ return p ? `${pre}${p} ${num}` : m;
131
+ });
132
+ }
133
+ /** The full published form of a markdown document. */
134
+ export function transformPublished(content, opts = {}) {
135
+ let c = stripStructures(content);
136
+ c = flattenCrossRefs(c);
137
+ if (opts.mAdrPrefix)
138
+ c = applyMAdrPrefix(c);
139
+ return c;
140
+ }
141
+ /** Scan already-published content for deny-class leaks. */
142
+ export function lintPublished(published, relPath) {
143
+ if (published.includes("pit-ok-file"))
144
+ return [];
145
+ const violations = [];
146
+ const lines = published.split("\n");
147
+ lines.forEach((line, i) => {
148
+ if (line.includes("pit-ok"))
149
+ return;
150
+ for (const re of DENY_TEMPORAL) {
151
+ if (re.test(line)) {
152
+ violations.push({
153
+ path: relPath,
154
+ cls: "temporal",
155
+ pattern: re.source,
156
+ line: i + 1,
157
+ text: line.slice(0, 140),
158
+ });
159
+ }
160
+ }
161
+ for (const re of DENY_PRODUCER) {
162
+ if (re.test(line)) {
163
+ violations.push({
164
+ path: relPath,
165
+ cls: "producer",
166
+ pattern: re.source,
167
+ line: i + 1,
168
+ text: line.slice(0, 140),
169
+ });
170
+ }
171
+ }
172
+ });
173
+ return violations;
174
+ }
175
+ /**
176
+ * Apply the publish transform to a staged payload in place: drop non-Accepted
177
+ * ADRs, transform every markdown file to its published form, and lint the
178
+ * result. Returns the retained paths + any violations (caller fails closed).
179
+ */
180
+ export async function applyPublishTransform(payloadRoot, paths, opts = {}) {
181
+ const keptPaths = [];
182
+ const droppedPaths = [];
183
+ const violations = [];
184
+ for (const p of paths) {
185
+ if (!p.endsWith(".md")) {
186
+ // Non-Markdown config/data (registry.yaml, seed lists, schemas): flatten
187
+ // cross-repo refs so the published config surface is producer-agnostic,
188
+ // and — under the origin-prefix rename — range-prefix bare in-namespace
189
+ // ADR refs so the config matches the renamed decision files. No lineage
190
+ // strip / Accepted filter / file rename applies here. Mirrors the method
191
+ // baseline builder's yaml/yml/txt handling.
192
+ if (/\.(ya?ml|txt)$/.test(p)) {
193
+ const abs = join(payloadRoot, p);
194
+ const content = await readFile(abs, "utf8");
195
+ let rewritten = flattenCrossRefs(content);
196
+ if (opts.mAdrPrefix)
197
+ rewritten = applyOriginPrefix(rewritten);
198
+ if (rewritten !== content)
199
+ await writeFile(abs, rewritten);
200
+ }
201
+ keptPaths.push(p);
202
+ continue;
203
+ }
204
+ const abs = join(payloadRoot, p);
205
+ const content = await readFile(abs, "utf8");
206
+ // Accepted-only: a non-Accepted ADR is not part of "what the method is".
207
+ if (isAdrPath(p) && !isAccepted(content)) {
208
+ await rm(abs, { force: true });
209
+ droppedPaths.push(p);
210
+ continue;
211
+ }
212
+ const published = transformPublished(content, opts);
213
+ violations.push(...lintPublished(published, p));
214
+ if (published !== content) {
215
+ await writeFile(abs, published);
216
+ }
217
+ // Under the M-ADR rename, the published *file* is renamed too so the
218
+ // filename matches its (rewritten) heading. Canon filenames stay `ADR`;
219
+ // this is published-output only. The renamed path is what flows into
220
+ // MANAGED_PATHS + checksums, so the lock + sync stay self-consistent.
221
+ let outPath = p;
222
+ if (opts.mAdrPrefix && isAdrPath(p)) {
223
+ const renamed = mAdrFilename(p);
224
+ if (renamed !== p) {
225
+ await rename(abs, join(payloadRoot, renamed));
226
+ outPath = renamed;
227
+ }
228
+ }
229
+ keptPaths.push(outPath);
230
+ }
231
+ return { keptPaths, droppedPaths, violations };
232
+ }
233
+ //# sourceMappingURL=publish-transform.js.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Reference-closure reporter — M-ADR 0151 §4.6.
3
+ *
4
+ * A context pack ships a SUBSET of its producer's corpus, so payload
5
+ * documents may reference files the pack does not carry. Policy: a link
6
+ * resolves in the repository that OWNS the document; a distributed copy
7
+ * referencing the wider corpus is informational. This reporter makes the
8
+ * property VISIBLE at cut time — the pack declares "ships N references
9
+ * that resolve only in the producer" instead of a consumer discovering it
10
+ * by resolving the whole corpus after distribution.
11
+ *
12
+ * Reporting-only, by design: the deliberate asymmetry with method-baseline
13
+ * bundles, whose seed lists ship whole corpora and therefore carry closure
14
+ * ENFORCEMENT (validate-link-closure.sh). A subset cannot satisfy closure
15
+ * without ceasing to be a subset.
16
+ *
17
+ * Parser hardening comes from the estate that measured this problem — the
18
+ * four traps their link audit hit are this module's controls: markdown
19
+ * targets containing SPACES (unencoded paths must not truncate at the first
20
+ * space), targets containing PARENTHESES (balance them; "…(ReBAC).md" broke
21
+ * every naive regex), URL-ENCODED targets (%20, %3A… decode before
22
+ * resolving), and never scanning outside the payload set (no cache dirs —
23
+ * guaranteed here by construction, since only staged payload paths are read).
24
+ */
25
+ import { readFile } from "node:fs/promises";
26
+ import { join } from "node:path";
27
+ import { posix } from "node:path";
28
+ /**
29
+ * Extract markdown link targets from one document. Handles `[text](target)`
30
+ * and `![alt](target)` with BALANCED parentheses inside the target, the
31
+ * `](<target>)` angle form, and `](target "title")` suffixes.
32
+ */
33
+ export function extractLinkTargets(content) {
34
+ const targets = [];
35
+ let i = 0;
36
+ for (;;) {
37
+ const at = content.indexOf("](", i);
38
+ if (at === -1)
39
+ break;
40
+ let j = at + 2;
41
+ let target;
42
+ if (content[j] === "<") {
43
+ const close = content.indexOf(">", j + 1);
44
+ if (close === -1)
45
+ break;
46
+ target = content.slice(j + 1, close);
47
+ i = close + 1;
48
+ }
49
+ else {
50
+ let depth = 1;
51
+ const start = j;
52
+ while (j < content.length && depth > 0) {
53
+ const c = content[j];
54
+ if (c === "(")
55
+ depth += 1;
56
+ else if (c === ")")
57
+ depth -= 1;
58
+ if (depth > 0)
59
+ j += 1;
60
+ }
61
+ if (depth !== 0)
62
+ break;
63
+ target = content.slice(start, j);
64
+ i = j + 1;
65
+ }
66
+ // Strip a `"title"` suffix (markdown allows `](url "title")`).
67
+ const title = target.match(/^(.*\S)\s+"[^"]*"$/s);
68
+ if (title)
69
+ target = title[1];
70
+ targets.push(target.trim());
71
+ }
72
+ return targets;
73
+ }
74
+ function decodeTarget(raw) {
75
+ try {
76
+ return decodeURIComponent(raw);
77
+ }
78
+ catch {
79
+ return undefined; // malformed escape — count as unresolvable via caller
80
+ }
81
+ }
82
+ /**
83
+ * Resolve every markdown link in the payload's .md files against the
84
+ * payload's own file set.
85
+ */
86
+ export async function checkReferenceClosure(payloadRoot, payloadPaths) {
87
+ const inPayload = new Set(payloadPaths.map((p) => posix.normalize(p)));
88
+ let total = 0;
89
+ let unresolvable = 0;
90
+ const misses = new Set();
91
+ for (const p of payloadPaths) {
92
+ if (!p.endsWith(".md"))
93
+ continue;
94
+ const content = await readFile(join(payloadRoot, p), "utf-8");
95
+ const dir = posix.dirname(p);
96
+ for (const rawTarget of extractLinkTargets(content)) {
97
+ // External and non-file targets are out of scope.
98
+ if (/^[a-z][a-z0-9+.-]*:/i.test(rawTarget))
99
+ continue; // http:, mailto:, …
100
+ const withoutAnchor = rawTarget.split("#")[0];
101
+ if (withoutAnchor === "")
102
+ continue; // pure anchor
103
+ const decoded = decodeTarget(withoutAnchor);
104
+ total += 1;
105
+ if (decoded === undefined) {
106
+ unresolvable += 1;
107
+ misses.add(rawTarget);
108
+ continue;
109
+ }
110
+ const resolved = decoded.startsWith("/")
111
+ ? posix.normalize(decoded.slice(1))
112
+ : posix.normalize(posix.join(dir, decoded));
113
+ if (!inPayload.has(resolved)) {
114
+ unresolvable += 1;
115
+ misses.add(resolved);
116
+ }
117
+ }
118
+ }
119
+ return {
120
+ totalReferences: total,
121
+ unresolvable,
122
+ unresolvableTargets: [...misses].sort(),
123
+ };
124
+ }
125
+ //# sourceMappingURL=reference-closure.js.map
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Zod schema for adopter pack seeds. Mirrors
4
+ * docs/method/foundations/schemas/context-pack-seed-schema.yaml.
5
+ *
6
+ * Used by `sdd-cli pack validate` for fast feedback during authoring.
7
+ * The structural parse in seed.ts already enforces presence of required
8
+ * fields; this schema layer adds value-shape checks (semver patterns,
9
+ * known forward-base bundle names, non-empty managed_paths).
10
+ */
11
+ const SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
12
+ const VERSIONED_RANGE_PATTERN = /^(\^|~|>=)?v?\d+\.\d+\.\d+$/;
13
+ // Exported so the canonical-schema conformance test can assert this vocabulary
14
+ // matches context-pack-seed-schema.yaml's `forward_base.bundle` enum. The two had
15
+ // drifted (canonical carried three values, this four) with nothing to detect it.
16
+ export const KNOWN_FORWARD_BASES = [
17
+ "app-baseline",
18
+ "integration-baseline",
19
+ "platform-baseline",
20
+ "reverse-baseline",
21
+ ];
22
+ export const PackSeedSchema = z.object({
23
+ version: z
24
+ .string()
25
+ .regex(SEMVER_PATTERN, "version must be semver (X.Y.Z)"),
26
+ scope: z
27
+ .string()
28
+ .regex(/^[a-z0-9-]+$/, "scope must be lower-kebab-case slug"),
29
+ // Optional per ADR 0176 §4.6: absent = profile-agnostic pack.
30
+ forwardBase: z
31
+ .object({
32
+ bundle: z.enum(KNOWN_FORWARD_BASES),
33
+ minimumVersion: z
34
+ .string()
35
+ .regex(/^v\d+\.\d+\.\d+$/, "forward_base.minimum_version must be vX.Y.Z"),
36
+ sourceRepo: z.string().min(1),
37
+ })
38
+ .optional(),
39
+ // Vendor-base pack pin (M-ADR 0175, seed schema 1.2.0). Present only on a
40
+ // vendor-base pack: names the vendor and the vendor version whose documented
41
+ // truth this pack's content encodes.
42
+ //
43
+ // The point of a PACK-LEVEL pin is that a consuming SDD does not restate it. A
44
+ // consumer pins the pack exactly (its bundles-list entry + sha256) and the
45
+ // vendor version it inherits is DERIVED from here. Recording it consumer-side
46
+ // as well would be a second source of truth that can silently disagree, and the
47
+ // pack is authoritative for its own vendor pin.
48
+ //
49
+ // `vendorVersion` is free-form by design: it carries either a formal vendor
50
+ // version or the anchor schema's `<vendor>-docs@YYYY-MM-DD` snapshot form, and
51
+ // vendors' own version strings are not the method's to constrain.
52
+ vendorBase: z
53
+ .object({
54
+ vendor: z
55
+ .string()
56
+ .regex(/^[a-z][a-z0-9-]*$/, "vendor_base.vendor must be a lower-kebab-case slug"),
57
+ vendorVersion: z.string().min(1, "vendor_base.vendor_version must be non-empty"),
58
+ })
59
+ .optional(),
60
+ minimumSddCliVersion: z
61
+ .string()
62
+ .regex(SEMVER_PATTERN, "minimum_sdd_cli_version must be semver (X.Y.Z)"),
63
+ description: z.string().min(1),
64
+ include: z.array(z.string()).optional(),
65
+ managedPaths: z
66
+ .array(z.string().min(1))
67
+ .min(1, "managed_paths must have at least one entry"),
68
+ forbiddenPaths: z.array(z.string()).optional(),
69
+ // ADR 0176 §4.3: scaffold-source path prefixes; each must cover at
70
+ // least one managed_paths entry (checked in the superRefine below).
71
+ scaffoldSourcePaths: z.array(z.string().min(1)).optional(),
72
+ requiresPack: z
73
+ .object({
74
+ name: z.string().min(1),
75
+ version: z
76
+ .string()
77
+ .regex(VERSIONED_RANGE_PATTERN, "requires_pack.version must be a semver range (e.g. ^v1.2.0)"),
78
+ })
79
+ .optional(),
80
+ sourcePath: z.string(),
81
+ });
82
+ const PackSeedSchemaRefined = PackSeedSchema.superRefine((seed, ctx) => {
83
+ // A vendor-base pack is PROFILE-AGNOSTIC (M-ADR 0176 §4.6, M-ADR 0175): its
84
+ // content is vendor-defined, so it layers over any baseline and must not pin
85
+ // one. Declaring both is a contradiction the producer should hear at validate
86
+ // time rather than discover when a consumer on a different baseline cannot
87
+ // apply it.
88
+ if (seed.vendorBase && seed.forwardBase) {
89
+ ctx.addIssue({
90
+ code: z.ZodIssueCode.custom,
91
+ path: ["forwardBase"],
92
+ message: "a vendor_base pack is profile-agnostic and must omit forward_base " +
93
+ "(M-ADR 0176 §4.6); remove forward_base, or remove vendor_base if this " +
94
+ "is not a vendor-base pack",
95
+ });
96
+ }
97
+ if (!seed.scaffoldSourcePaths)
98
+ return;
99
+ for (const [i, prefix] of seed.scaffoldSourcePaths.entries()) {
100
+ const covers = seed.managedPaths.some((mp) => mp.startsWith(prefix));
101
+ if (!covers) {
102
+ ctx.addIssue({
103
+ code: z.ZodIssueCode.custom,
104
+ path: ["scaffoldSourcePaths", i],
105
+ message: `scaffold_source_paths entry "${prefix}" matches no managed_paths entry`,
106
+ });
107
+ }
108
+ }
109
+ });
110
+ export function validateSeed(seed) {
111
+ const result = PackSeedSchemaRefined.safeParse(seed);
112
+ if (result.success) {
113
+ return { ok: true, errors: [] };
114
+ }
115
+ const errors = result.error.issues.map((issue) => ({
116
+ field: issue.path.join("."),
117
+ message: issue.message,
118
+ }));
119
+ return { ok: false, errors };
120
+ }
121
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,182 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ /**
5
+ * Adopter pack seed file. Per ADR 0151 + context-pack-seed-schema.yaml.
6
+ *
7
+ * Loaded by `sdd-cli pack build` and `sdd-cli pack validate` from
8
+ * `orchestration/distribution/{pack-name}-seed.yaml` in adopter repos.
9
+ *
10
+ * Includes are resolved before validation: the merged seed presents
11
+ * all `include`'d managed_paths + forbidden_paths concatenated with
12
+ * the host seed's own.
13
+ */
14
+ /**
15
+ * Authored seed keys, snake_case → the camelCase field they map to.
16
+ *
17
+ * THE SINGLE SOURCE OF TRUTH for "what may appear in a seed file". It lived only
18
+ * inside the canonical-conformance test, which meant the loader had no idea what a
19
+ * valid key set was — so an unrecognised key was dropped by the hand-mapping below
20
+ * without a word. An adopter following a schema that documents a field their sdd-cli
21
+ * predates got a clean validation and a silent no-op: the same failure M-ADR 0175's
22
+ * history records for `packaging`, and the same one fixed for `.sdd/sdd-manifest.yaml`
23
+ * in 0.110.1 — but by a different mechanism, which is why that fix did not cover this.
24
+ *
25
+ * The conformance test now imports this rather than declaring its own copy. Two
26
+ * hand-maintained lists of the same facts is how the lifecycle expectation drifted.
27
+ */
28
+ export const SEED_KEY_TO_FIELD = {
29
+ version: "version",
30
+ scope: "scope",
31
+ forward_base: "forwardBase",
32
+ vendor_base: "vendorBase",
33
+ minimum_sdd_cli_version: "minimumSddCliVersion",
34
+ description: "description",
35
+ include: "include",
36
+ managed_paths: "managedPaths",
37
+ forbidden_paths: "forbiddenPaths",
38
+ scaffold_source_paths: "scaffoldSourcePaths",
39
+ requires_pack: "requiresPack",
40
+ };
41
+ /**
42
+ * Top-level keys in a seed file that this build does not recognise.
43
+ *
44
+ * Deliberately TOP-LEVEL only, stated as a limit rather than left implied: nested
45
+ * blocks (`forward_base`, `vendor_base`, `requires_pack`) are hand-mapped field by
46
+ * field too, so an unknown key inside one is still dropped silently today.
47
+ */
48
+ export function unknownSeedKeys(raw) {
49
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
50
+ return [];
51
+ return Object.keys(raw)
52
+ .filter((k) => !(k in SEED_KEY_TO_FIELD))
53
+ .sort();
54
+ }
55
+ /**
56
+ * Load a seed file from disk, resolve its `include:` directives,
57
+ * and return a merged PackSeed. Throws if any include cannot be read
58
+ * or if circular includes are detected.
59
+ *
60
+ * NOTE: This does not validate the seed structure — only parses YAML
61
+ * and assembles the typed shape. Use `validateSeed` (separate module)
62
+ * for schema enforcement.
63
+ */
64
+ export async function loadSeed(seedPath) {
65
+ const absPath = isAbsolute(seedPath) ? seedPath : resolve(seedPath);
66
+ const visited = new Set();
67
+ return loadSeedRecursive(absPath, visited);
68
+ }
69
+ async function loadSeedRecursive(absPath, visited) {
70
+ if (visited.has(absPath)) {
71
+ throw new Error(`Circular include detected at ${absPath}`);
72
+ }
73
+ visited.add(absPath);
74
+ let raw;
75
+ try {
76
+ raw = await readFile(absPath, "utf8");
77
+ }
78
+ catch (err) {
79
+ throw new Error(`Cannot read seed file ${absPath}: ${err.message}`);
80
+ }
81
+ const parsed = parseYaml(raw);
82
+ if (parsed === null || typeof parsed !== "object") {
83
+ throw new Error(`Seed file ${absPath} is not a YAML object`);
84
+ }
85
+ const baseDir = dirname(absPath);
86
+ const includes = Array.isArray(parsed.include) ? parsed.include : [];
87
+ const includedManaged = [];
88
+ const includedForbidden = [];
89
+ const includedUnknown = [];
90
+ for (const inc of includes) {
91
+ if (typeof inc !== "string") {
92
+ throw new Error(`Non-string include entry in ${absPath}: ${String(inc)}`);
93
+ }
94
+ const incPath = isAbsolute(inc) ? inc : resolve(baseDir, inc);
95
+ const includedSeed = await loadSeedRecursive(incPath, visited);
96
+ includedManaged.push(...includedSeed.managedPaths);
97
+ if (includedSeed.forbiddenPaths) {
98
+ includedForbidden.push(...includedSeed.forbiddenPaths);
99
+ }
100
+ if (includedSeed.unknownKeys)
101
+ includedUnknown.push(...includedSeed.unknownKeys);
102
+ }
103
+ // Unknown keys from this file, plus everything its includes reported. An included
104
+ // seed's stray key is as silently dropped as the root's, so the queue is unioned.
105
+ const ownUnknown = unknownSeedKeys(parsed);
106
+ const own = parsed;
107
+ const managedPaths = [
108
+ ...includedManaged,
109
+ ...asStringArray(own.managed_paths, "managed_paths", absPath),
110
+ ];
111
+ const forbiddenPaths = includedForbidden.length > 0 || Array.isArray(own.forbidden_paths)
112
+ ? [
113
+ ...includedForbidden,
114
+ ...asStringArray(own.forbidden_paths, "forbidden_paths", absPath, true),
115
+ ]
116
+ : undefined;
117
+ const scaffoldSourcePaths = Array.isArray(own.scaffold_source_paths)
118
+ ? asStringArray(own.scaffold_source_paths, "scaffold_source_paths", absPath, true)
119
+ : undefined;
120
+ const seed = {
121
+ version: asString(own.version, "version", absPath),
122
+ scope: asString(own.scope, "scope", absPath),
123
+ // forward_base optional per ADR 0176 §4.6 (profile-agnostic packs omit it)
124
+ ...(own.forward_base && {
125
+ forwardBase: {
126
+ bundle: asString(own.forward_base.bundle, "forward_base.bundle", absPath),
127
+ minimumVersion: asString(own.forward_base.minimum_version, "forward_base.minimum_version", absPath),
128
+ sourceRepo: own.forward_base.source_repo
129
+ ? asString(own.forward_base.source_repo, "forward_base.source_repo", absPath)
130
+ : "sdd-method",
131
+ },
132
+ }),
133
+ // vendor_base present only on a vendor-base pack (M-ADR 0175); the
134
+ // profile-agnostic/forward_base contradiction is caught in validateSeed.
135
+ ...(own.vendor_base && {
136
+ vendorBase: {
137
+ vendor: asString(own.vendor_base.vendor, "vendor_base.vendor", absPath),
138
+ vendorVersion: asString(own.vendor_base.vendor_version, "vendor_base.vendor_version", absPath),
139
+ },
140
+ }),
141
+ minimumSddCliVersion: asString(own.minimum_sdd_cli_version, "minimum_sdd_cli_version", absPath),
142
+ description: asString(own.description, "description", absPath),
143
+ managedPaths,
144
+ sourcePath: absPath,
145
+ ...(ownUnknown.length + includedUnknown.length > 0
146
+ ? { unknownKeys: [...new Set([...includedUnknown, ...ownUnknown])].sort() }
147
+ : {}),
148
+ ...(includes.length > 0 && { include: includes }),
149
+ ...(forbiddenPaths && { forbiddenPaths }),
150
+ ...(scaffoldSourcePaths && scaffoldSourcePaths.length > 0 && { scaffoldSourcePaths }),
151
+ ...(own.requires_pack && {
152
+ requiresPack: {
153
+ name: asString(own.requires_pack.name, "requires_pack.name", absPath),
154
+ version: asString(own.requires_pack.version, "requires_pack.version", absPath),
155
+ },
156
+ }),
157
+ };
158
+ return seed;
159
+ }
160
+ function asString(value, field, file) {
161
+ if (typeof value !== "string" || value.length === 0) {
162
+ throw new Error(`Seed field "${field}" must be a non-empty string in ${file}`);
163
+ }
164
+ return value;
165
+ }
166
+ function asStringArray(value, field, file, allowEmpty = false) {
167
+ if (value === undefined || value === null) {
168
+ if (allowEmpty)
169
+ return [];
170
+ throw new Error(`Seed field "${field}" is required in ${file}`);
171
+ }
172
+ if (!Array.isArray(value)) {
173
+ throw new Error(`Seed field "${field}" must be a list in ${file}`);
174
+ }
175
+ for (const entry of value) {
176
+ if (typeof entry !== "string") {
177
+ throw new Error(`Seed field "${field}" entries must be strings in ${file}`);
178
+ }
179
+ }
180
+ return value;
181
+ }
182
+ //# sourceMappingURL=seed.js.map