@sdd-method/author-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 (203) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +17 -0
  3. package/README.md +6 -2
  4. package/dist/author-bin.js +8 -0
  5. package/dist/author-program.js +99 -0
  6. package/dist/lib/adr-origin.js +59 -0
  7. package/dist/lib/agent-harness.js +37 -0
  8. package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
  9. package/dist/lib/api-surface/analyzers/library.js +95 -0
  10. package/dist/lib/api-surface/analyzers/serverless.js +336 -0
  11. package/dist/lib/api-surface/analyzers/spring.js +237 -0
  12. package/dist/lib/api-surface/analyzers/terraform.js +109 -0
  13. package/dist/lib/api-surface/detect.js +25 -0
  14. package/dist/lib/api-surface/emit.js +109 -0
  15. package/dist/lib/api-surface/file-set.js +43 -0
  16. package/dist/lib/api-surface/index.js +134 -0
  17. package/dist/lib/api-surface/types.js +17 -0
  18. package/dist/lib/briefing/index.js +13 -0
  19. package/dist/lib/briefing/render.js +132 -0
  20. package/dist/lib/briefing/variables.js +92 -0
  21. package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
  22. package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
  23. package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
  24. package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
  25. package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
  26. package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
  27. package/dist/lib/catalogue/archetype/resolver.js +103 -0
  28. package/dist/lib/catalogue/archetype/wireframe.js +418 -0
  29. package/dist/lib/catalogue/baseline-check.js +151 -0
  30. package/dist/lib/catalogue/build.js +1502 -0
  31. package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
  32. package/dist/lib/catalogue/builders/adrs.js +222 -0
  33. package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
  34. package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
  35. package/dist/lib/catalogue/builders/c4-components.js +229 -0
  36. package/dist/lib/catalogue/builders/c4-containers.js +240 -0
  37. package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
  38. package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
  39. package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
  40. package/dist/lib/catalogue/builders/c4-systems.js +98 -0
  41. package/dist/lib/catalogue/builders/capabilities.js +488 -0
  42. package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
  43. package/dist/lib/catalogue/builders/capability-edges.js +256 -0
  44. package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
  45. package/dist/lib/catalogue/builders/capability-spec.js +348 -0
  46. package/dist/lib/catalogue/builders/container-dependency.js +183 -0
  47. package/dist/lib/catalogue/builders/contracts.js +402 -0
  48. package/dist/lib/catalogue/builders/data-items.js +344 -0
  49. package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
  50. package/dist/lib/catalogue/builders/domain-models.js +175 -0
  51. package/dist/lib/catalogue/builders/domains.js +119 -0
  52. package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
  53. package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
  54. package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
  55. package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
  56. package/dist/lib/catalogue/builders/entity-slug.js +47 -0
  57. package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
  58. package/dist/lib/catalogue/builders/event-edges.js +327 -0
  59. package/dist/lib/catalogue/builders/events.js +168 -0
  60. package/dist/lib/catalogue/builders/evidence-class.js +43 -0
  61. package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
  62. package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
  63. package/dist/lib/catalogue/builders/feature-contract.js +77 -0
  64. package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
  65. package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
  66. package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
  67. package/dist/lib/catalogue/builders/feature-groups.js +63 -0
  68. package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
  69. package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
  70. package/dist/lib/catalogue/builders/features.js +300 -0
  71. package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
  72. package/dist/lib/catalogue/builders/glossary.js +204 -0
  73. package/dist/lib/catalogue/builders/implementations.js +214 -0
  74. package/dist/lib/catalogue/builders/index.js +64 -0
  75. package/dist/lib/catalogue/builders/integration-components.js +344 -0
  76. package/dist/lib/catalogue/builders/integrations.js +181 -0
  77. package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
  78. package/dist/lib/catalogue/builders/persona-container.js +133 -0
  79. package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
  80. package/dist/lib/catalogue/builders/personas.js +152 -0
  81. package/dist/lib/catalogue/builders/product-edges.js +106 -0
  82. package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
  83. package/dist/lib/catalogue/builders/products.js +162 -0
  84. package/dist/lib/catalogue/builders/quality-scores.js +635 -0
  85. package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
  86. package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
  87. package/dist/lib/catalogue/builders/repo-edges.js +340 -0
  88. package/dist/lib/catalogue/builders/repositories.js +458 -0
  89. package/dist/lib/catalogue/builders/scaffolds.js +197 -0
  90. package/dist/lib/catalogue/builders/screen-tree.js +222 -0
  91. package/dist/lib/catalogue/builders/screens.js +396 -0
  92. package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
  93. package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
  94. package/dist/lib/catalogue/builders/service-contract.js +74 -0
  95. package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
  96. package/dist/lib/catalogue/builders/services.js +232 -0
  97. package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
  98. package/dist/lib/catalogue/builders/user-journeys.js +253 -0
  99. package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
  100. package/dist/lib/catalogue/cli-version.js +10 -0
  101. package/dist/lib/catalogue/contract-id.js +49 -0
  102. package/dist/lib/catalogue/coverage-predicates.js +603 -0
  103. package/dist/lib/catalogue/coverage-render.js +552 -0
  104. package/dist/lib/catalogue/coverage-run.js +115 -0
  105. package/dist/lib/catalogue/csv-reader.js +97 -0
  106. package/dist/lib/catalogue/csv-writer.js +96 -0
  107. package/dist/lib/catalogue/data-plane-coherence.js +77 -0
  108. package/dist/lib/catalogue/ddl-generator.js +233 -0
  109. package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
  110. package/dist/lib/catalogue/engine-validator.js +239 -0
  111. package/dist/lib/catalogue/ext-id.js +76 -0
  112. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
  113. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
  114. package/dist/lib/catalogue/finding-identity.js +183 -0
  115. package/dist/lib/catalogue/findings.js +46 -0
  116. package/dist/lib/catalogue/fk-validator.js +136 -0
  117. package/dist/lib/catalogue/freshness.js +70 -0
  118. package/dist/lib/catalogue/gate-checks.js +338 -0
  119. package/dist/lib/catalogue/graph-projection.js +346 -0
  120. package/dist/lib/catalogue/impact-run.js +228 -0
  121. package/dist/lib/catalogue/manifest-loader.js +435 -0
  122. package/dist/lib/catalogue/platform-scope.js +7 -0
  123. package/dist/lib/catalogue/population-checks.js +87 -0
  124. package/dist/lib/catalogue/relationship-semantics.js +66 -0
  125. package/dist/lib/catalogue/reverse-product-quality.js +99 -0
  126. package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
  127. package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
  128. package/dist/lib/catalogue/scaffold-delta.js +261 -0
  129. package/dist/lib/catalogue/schema-compat.js +52 -0
  130. package/dist/lib/catalogue/schema-loader.js +87 -0
  131. package/dist/lib/catalogue/scores-coverage.js +573 -0
  132. package/dist/lib/catalogue/screen-validation-loader.js +89 -0
  133. package/dist/lib/catalogue/service-architecture-parser.js +193 -0
  134. package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
  135. package/dist/lib/catalogue/staleness-verdicts.js +345 -0
  136. package/dist/lib/catalogue/traceability-loader.js +350 -0
  137. package/dist/lib/catalogue/validate.js +200 -0
  138. package/dist/lib/catalogue/version.js +18 -0
  139. package/dist/lib/conformance/binding.js +70 -0
  140. package/dist/lib/conformance/finding.js +69 -0
  141. package/dist/lib/conformance/index.js +578 -0
  142. package/dist/lib/context-pull/index.js +191 -0
  143. package/dist/lib/context-pull/resolve.js +70 -0
  144. package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
  145. package/dist/lib/contracts-availability/index.js +267 -0
  146. package/dist/lib/generate-briefing/index.js +77 -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/pack/log.js +18 -0
  152. package/dist/lib/pack/schema.js +121 -0
  153. package/dist/lib/pack/seed.js +182 -0
  154. package/dist/lib/pack/validate.js +49 -0
  155. package/dist/lib/package-version.js +92 -0
  156. package/dist/lib/plan/index.js +69 -0
  157. package/dist/lib/plan/read-sdd.js +102 -0
  158. package/dist/lib/plan/render-template.js +84 -0
  159. package/dist/lib/repo-kind.js +129 -0
  160. package/dist/lib/scaffold/domain.js +234 -0
  161. package/dist/lib/scaffold/registry.js +136 -0
  162. package/dist/lib/scripts.js +7 -0
  163. package/dist/lib/sdd-index/repo-config.js +245 -0
  164. package/dist/lib/sdd-reader.js +91 -0
  165. package/dist/lib/snapshot/advance.js +454 -0
  166. package/dist/lib/snapshot/diff.js +190 -0
  167. package/dist/lib/snapshot/fold.js +208 -0
  168. package/dist/lib/snapshot/land.js +178 -0
  169. package/dist/lib/snapshot/manifest.js +206 -0
  170. package/dist/lib/snapshot/targets.js +169 -0
  171. package/dist/lib/snapshot/verify.js +91 -0
  172. package/dist/lib/sync/briefing-merge.js +106 -0
  173. package/dist/lib/sync/bundle-cache.js +109 -0
  174. package/dist/lib/sync/yaml-block-merge.js +120 -0
  175. package/dist/lib/validate/aggregate.js +51 -0
  176. package/dist/lib/validate/bundle-coherence.js +74 -0
  177. package/dist/lib/validate/dispatch.js +243 -0
  178. package/dist/lib/validate/index.js +110 -0
  179. package/dist/lib/validate/plan.js +54 -0
  180. package/dist/lib/validate/runner.js +85 -0
  181. package/dist/lib/validate/yaml-parse.js +112 -0
  182. package/dist/maintain-version.json +4 -0
  183. package/dist/verbs/api-surface.js +71 -0
  184. package/dist/verbs/catalogue-check.js +28 -0
  185. package/dist/verbs/catalogue-verify.js +114 -0
  186. package/dist/verbs/conformance.js +34 -0
  187. package/dist/verbs/context.js +39 -0
  188. package/dist/verbs/contracts.js +34 -0
  189. package/dist/verbs/generate-briefing.js +29 -0
  190. package/dist/verbs/list.js +45 -0
  191. package/dist/verbs/pack-validate.js +17 -0
  192. package/dist/verbs/path-option.js +25 -0
  193. package/dist/verbs/plan.js +42 -0
  194. package/dist/verbs/render.js +87 -0
  195. package/dist/verbs/scaffold.js +83 -0
  196. package/dist/verbs/snapshot.js +134 -0
  197. package/dist/verbs/validate.js +48 -0
  198. package/dist/verify-program.js +122 -0
  199. package/dist/verify-version.json +4 -0
  200. package/package.json +32 -4
  201. package/scripts/validate-app-dependencies.sh +664 -0
  202. package/scripts/validate-app-docs.sh +199 -0
  203. package/scripts/validate-integration-docs.sh +505 -0
@@ -0,0 +1,350 @@
1
+ // Traceability manifest loader — walks `docs/domains/**​/*.traceability.yaml`
2
+ // and returns parsed declarations.
3
+ //
4
+ // The traceability manifest is per-capability declarative metadata that
5
+ // the canonical builder reads to populate `capabilities.csv` and the
6
+ // five capability_* edge catalogues (services, adrs, features,
7
+ // contracts, events). One YAML per capability; the canonical layout is
8
+ // `docs/domains/{domain}/product/{capability}.traceability.yaml`.
9
+ //
10
+ // Schema reference: docs/method/methods/capability-traceability-manifest-template.yaml
11
+ // in sdd-method (per ADR 0047 / capability-spec-template.md).
12
+ import { readdir, readFile, stat } from "node:fs/promises";
13
+ import { join, relative, sep } from "node:path";
14
+ import { parse as parseYaml } from "yaml";
15
+ const DOMAINS_DIR_REL = join("docs", "domains");
16
+ /**
17
+ * Normalise an authored `feature_directory` value (string, list of strings,
18
+ * or absent) to a clean `string[]`. Non-string entries and empties are
19
+ * dropped. Lets a capability declare one or several feature dirs.
20
+ */
21
+ export function normalizeFeatureDirs(value) {
22
+ if (typeof value === "string") {
23
+ return value.trim() === "" ? [] : [value];
24
+ }
25
+ if (Array.isArray(value)) {
26
+ return value.filter((v) => typeof v === "string" && v.trim() !== "");
27
+ }
28
+ return [];
29
+ }
30
+ /**
31
+ * THE contract-kind list. Every consumer of `traceability.contracts` iterates
32
+ * THIS — never a hand-written array — because hand-copies are what let `graphql`
33
+ * be discoverable, mappable and loadable while the capability_contract EDGE
34
+ * builder silently ignored it. Measured cost: an estate authored 9 correct
35
+ * declarations, rebuilt, and got zero edges and 7 standing orphans.
36
+ *
37
+ * The list was hand-written at THREE consumer sites and `graphql` was missed at
38
+ * all three. A kind added here now reaches every consumer at once.
39
+ *
40
+ * Not in scope: repo-config's own `contracts` block (a different schema with its
41
+ * own three kinds) — that is not a stale copy of this list and must not be
42
+ * "fixed" with this helper.
43
+ */
44
+ export const CONTRACT_LIST_KEYS = [
45
+ "openapi",
46
+ "proto",
47
+ "avro",
48
+ "spicedb",
49
+ "graphql",
50
+ ];
51
+ /**
52
+ * Every contract path a contracts block declares, across all kinds and both
53
+ * entry forms (bare string, or `{path, external}` per D13).
54
+ */
55
+ export function declaredContractPaths(block) {
56
+ if (block === undefined)
57
+ return [];
58
+ const out = [];
59
+ for (const key of CONTRACT_LIST_KEYS) {
60
+ const list = block[key];
61
+ if (!Array.isArray(list))
62
+ continue;
63
+ for (const entry of list) {
64
+ if (typeof entry === "string")
65
+ out.push(entry);
66
+ else if (entry && typeof entry.path === "string")
67
+ out.push(entry.path);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+ export async function loadTraceabilityManifests(opts) {
73
+ const root = join(opts.sddPath, DOMAINS_DIR_REL);
74
+ let yamlPaths;
75
+ try {
76
+ yamlPaths = await collectYamlPaths(root, /\.traceability\.yaml$/);
77
+ }
78
+ catch (err) {
79
+ const code = err.code;
80
+ if (code === "ENOENT") {
81
+ // No domains dir → no traceability data; return empty.
82
+ return [];
83
+ }
84
+ throw err;
85
+ }
86
+ const manifests = [];
87
+ for (const path of yamlPaths.sort()) {
88
+ const text = await readFile(path, "utf-8");
89
+ let raw;
90
+ try {
91
+ raw = parseYaml(text);
92
+ }
93
+ catch (err) {
94
+ opts.findings.add("convention", `failed to parse YAML at ${relative(opts.sddPath, path)}: ${err.message}`);
95
+ continue;
96
+ }
97
+ if (!raw || typeof raw !== "object") {
98
+ opts.findings.add("convention", `${relative(opts.sddPath, path)}: not an object — skipped`);
99
+ continue;
100
+ }
101
+ const obj = raw;
102
+ const cap = obj["capability"] ?? {};
103
+ const capId = String(cap["id"] ?? "").trim();
104
+ if (capId === "") {
105
+ opts.findings.add("convention", `${relative(opts.sddPath, path)}: missing capability.id`);
106
+ continue;
107
+ }
108
+ const relPath = relative(opts.sddPath, path);
109
+ const domainFromPath = inferDomainFromPath(relPath);
110
+ const declaredDomain = String(cap["domain"] ?? "").trim();
111
+ if (declaredDomain && domainFromPath && declaredDomain !== domainFromPath) {
112
+ opts.findings.add("divergence", `${relPath}: capability.domain="${declaredDomain}" disagrees with path domain "${domainFromPath}"`);
113
+ }
114
+ const capability = {
115
+ id: capId,
116
+ domain: declaredDomain || domainFromPath,
117
+ ...(typeof cap["capability_spec"] === "string"
118
+ ? { capability_spec: cap["capability_spec"] }
119
+ : {}),
120
+ ...(normalizeFeatureDirs(cap["feature_directory"]).length > 0
121
+ ? { feature_directory: normalizeFeatureDirs(cap["feature_directory"]) }
122
+ : {}),
123
+ ...(typeof cap["status"] === "string" ? { status: cap["status"] } : {}),
124
+ ...(typeof cap["data_subset"] === "string"
125
+ ? { data_subset: cap["data_subset"] }
126
+ : {}),
127
+ ...(typeof cap["slice_scope"] === "string"
128
+ ? { slice_scope: cap["slice_scope"] }
129
+ : {}),
130
+ ...(typeof cap["evidence_class"] === "string"
131
+ ? { evidence_class: cap["evidence_class"] }
132
+ : {}),
133
+ ...(typeof cap["context"] === "string"
134
+ ? { context: cap["context"] }
135
+ : {}),
136
+ };
137
+ manifests.push({
138
+ path,
139
+ relPath,
140
+ domainFromPath,
141
+ capability,
142
+ traceability: extractTraceabilityBlock(obj["traceability"]),
143
+ data: raw,
144
+ });
145
+ }
146
+ return manifests;
147
+ }
148
+ function extractTraceabilityBlock(raw) {
149
+ if (!raw || typeof raw !== "object")
150
+ return {};
151
+ const t = raw;
152
+ const runway = t["runway"];
153
+ const adrs = t["adrs"];
154
+ const services = t["services"];
155
+ const contracts = t["contracts"];
156
+ const domainModel = t["domain_model"];
157
+ return {
158
+ ...(runway && typeof runway === "object" ? { runway: extractRunway(runway) } : {}),
159
+ ...(Array.isArray(adrs) ? { adrs: extractAdrs(adrs) } : {}),
160
+ ...(Array.isArray(services) ? { services: extractServices(services) } : {}),
161
+ ...(contracts && typeof contracts === "object"
162
+ ? { contracts: extractContracts(contracts) }
163
+ : {}),
164
+ ...(domainModel && typeof domainModel === "object"
165
+ ? extractDomainModel(domainModel)
166
+ : {}),
167
+ };
168
+ }
169
+ function extractDomainModel(raw) {
170
+ if (!raw || typeof raw !== "object")
171
+ return {};
172
+ const dm = raw;
173
+ const path = String(dm["path"] ?? "").trim();
174
+ if (path === "")
175
+ return {};
176
+ const version = dm["version"];
177
+ return {
178
+ domain_model: {
179
+ path,
180
+ ...(typeof version === "string" && version.trim() !== ""
181
+ ? { version }
182
+ : {}),
183
+ },
184
+ };
185
+ }
186
+ function extractRunway(raw) {
187
+ if (!raw || typeof raw !== "object")
188
+ return {};
189
+ const r = raw;
190
+ const enables = Array.isArray(r["enables"])
191
+ ? r["enables"].filter((e) => typeof e === "string")
192
+ : undefined;
193
+ return {
194
+ ...(typeof r["classification"] === "string"
195
+ ? { classification: r["classification"] }
196
+ : {}),
197
+ ...(enables ? { enables } : {}),
198
+ };
199
+ }
200
+ function extractAdrs(raw) {
201
+ const out = [];
202
+ for (const item of raw) {
203
+ if (!item || typeof item !== "object")
204
+ continue;
205
+ const o = item;
206
+ const id = String(o["id"] ?? "").trim();
207
+ if (!id)
208
+ continue;
209
+ out.push({
210
+ id,
211
+ ...(typeof o["path"] === "string" ? { path: o["path"] } : {}),
212
+ ...(typeof o["relationship"] === "string"
213
+ ? { relationship: o["relationship"] }
214
+ : {}),
215
+ ...(typeof o["notes"] === "string" ? { notes: o["notes"] } : {}),
216
+ });
217
+ }
218
+ return out;
219
+ }
220
+ function extractServices(raw) {
221
+ const out = [];
222
+ for (const item of raw) {
223
+ if (!item || typeof item !== "object")
224
+ continue;
225
+ const o = item;
226
+ const id = String(o["id"] ?? "").trim();
227
+ if (!id)
228
+ continue;
229
+ out.push({
230
+ id,
231
+ ...(typeof o["owner_domain"] === "string"
232
+ ? { owner_domain: o["owner_domain"] }
233
+ : {}),
234
+ ...(typeof o["owner_sdd"] === "string" && o["owner_sdd"].trim() !== ""
235
+ ? { owner_sdd: o["owner_sdd"].trim() }
236
+ : {}),
237
+ ...(typeof o["role"] === "string" ? { role: o["role"] } : {}),
238
+ ...(typeof o["status"] === "string" ? { status: o["status"] } : {}),
239
+ ...(typeof o["notes"] === "string" ? { notes: o["notes"] } : {}),
240
+ ...(typeof o["contract"] === "string" && o["contract"].trim() !== ""
241
+ ? { contract: o["contract"].trim() }
242
+ : {}),
243
+ ...(typeof o["interaction_pattern"] === "string" && o["interaction_pattern"].trim() !== ""
244
+ ? { interaction_pattern: o["interaction_pattern"].trim() }
245
+ : {}),
246
+ ...(typeof o["qualifier"] === "string" && o["qualifier"].trim() !== ""
247
+ ? { qualifier: o["qualifier"].trim() }
248
+ : {}),
249
+ ...(typeof o["evidence"] === "string" && o["evidence"].trim() !== ""
250
+ ? { evidence: o["evidence"].trim() }
251
+ : {}),
252
+ });
253
+ }
254
+ return out;
255
+ }
256
+ function extractContracts(raw) {
257
+ const o = raw;
258
+ // Keeps BOTH legal entry forms. The original version filtered to strings
259
+ // only — which silently discarded every object entry before any builder
260
+ // ran. Measured cost when an adopter marked 58 entries `external: true`
261
+ // for the D13 canary: 0 rows emitted AND the 56 orphan findings those
262
+ // entries used to raise disappeared — the declarations became invisible
263
+ // to row emission and to finding emission at once. The builder had been
264
+ // taught the object form (contracts.ts) while its only feeder had not:
265
+ // the F36 shape again, one surface ahead of the pipeline that supplies
266
+ // it. `spicedb` was likewise never extracted, so `.zed` declarations
267
+ // never reached any builder even as bare strings.
268
+ const arr = (key) => {
269
+ const v = o[key];
270
+ if (!Array.isArray(v))
271
+ return undefined;
272
+ return v.filter((e) => typeof e === "string" ||
273
+ (e !== null &&
274
+ typeof e === "object" &&
275
+ !Array.isArray(e) &&
276
+ typeof e["path"] === "string"));
277
+ };
278
+ const openapi = arr("openapi");
279
+ const proto = arr("proto");
280
+ const avro = arr("avro");
281
+ const spicedb = arr("spicedb");
282
+ const graphql = arr("graphql");
283
+ // `provenance` (3.5.0) MUST be extracted here or the builder that reads it
284
+ // sees nothing — this function reconstructs the block key-by-key rather than
285
+ // passing the parsed object through, which is precisely the F36 shape the
286
+ // comment above records. It was re-committed once during 3.5.0's own
287
+ // implementation: the builder was taught to read provenance while this feeder
288
+ // still dropped it, and only an end-to-end catalogue build caught it, because
289
+ // the builder's unit tests construct the manifest object directly and never
290
+ // traverse this function. Any future key added to this block needs a line here
291
+ // AND a loader-level test.
292
+ const provenance = typeof o["provenance"] === "string" ? o["provenance"].trim() : undefined;
293
+ return {
294
+ ...(openapi ? { openapi } : {}),
295
+ ...(proto ? { proto } : {}),
296
+ ...(avro ? { avro } : {}),
297
+ ...(spicedb ? { spicedb } : {}),
298
+ ...(graphql ? { graphql } : {}),
299
+ ...(provenance ? { provenance } : {}),
300
+ };
301
+ }
302
+ function inferDomainFromPath(relPath) {
303
+ const parts = relPath.split(sep);
304
+ const idx = parts.indexOf("domains");
305
+ if (idx === -1 || idx + 1 >= parts.length)
306
+ return "";
307
+ return parts[idx + 1] ?? "";
308
+ }
309
+ async function collectYamlPaths(dir, pattern) {
310
+ const out = [];
311
+ let entries;
312
+ try {
313
+ entries = await readdir(dir, { withFileTypes: true });
314
+ }
315
+ catch (err) {
316
+ const code = err.code;
317
+ if (code === "ENOENT")
318
+ return [];
319
+ throw err;
320
+ }
321
+ for (const entry of entries) {
322
+ const full = join(dir, entry.name);
323
+ if (entry.isDirectory()) {
324
+ out.push(...(await collectYamlPaths(full, pattern)));
325
+ }
326
+ else if (entry.isFile() && pattern.test(entry.name)) {
327
+ out.push(full);
328
+ }
329
+ }
330
+ return out;
331
+ }
332
+ /** Convenience: read a file's first non-empty line, returning the trimmed value. */
333
+ export async function readFirstNonEmptyLine(path) {
334
+ try {
335
+ const s = await stat(path);
336
+ if (!s.isFile())
337
+ return "";
338
+ }
339
+ catch {
340
+ return "";
341
+ }
342
+ const text = await readFile(path, "utf-8");
343
+ for (const line of text.split(/\r?\n/)) {
344
+ const t = line.trim();
345
+ if (t !== "")
346
+ return t;
347
+ }
348
+ return "";
349
+ }
350
+ //# sourceMappingURL=traceability-loader.js.map
@@ -0,0 +1,200 @@
1
+ // `sdd-cli catalogue validate` — re-validate an existing catalogues/
2
+ // directory without rebuilding. Two checks per the canonical CLI spec:
3
+ //
4
+ // 1. Column-shape check: every CSV's header row must match the
5
+ // canonical schema's declared columns (exact set + order).
6
+ // 2. FK validation: full pass over the loaded rows, internal mode
7
+ // (external FKs treated per their declared flag — same semantics
8
+ // as `catalogue build`). Adopters who want post-merge validation
9
+ // run `catalogue aggregate` instead.
10
+ //
11
+ // Outputs the same shape of findings as `build` so consumers can pipe
12
+ // either through the same parser. Exit 0 on PASS; non-zero on shape
13
+ // drift OR internal-FK FAIL.
14
+ import { readFile, stat } from "node:fs/promises";
15
+ import { join, resolve } from "node:path";
16
+ import { CLI_VERSION } from "./cli-version.js";
17
+ import { readCatalogueCsv } from "./csv-reader.js";
18
+ import { writeCatalogue } from "./csv-writer.js";
19
+ import { validatedPopulation, validateWithEngineToLog } from "./engine-validator.js";
20
+ import { FindingsLog } from "./findings.js";
21
+ import { PLATFORM_SCOPED_AT_ORGANISATION } from "./platform-scope.js";
22
+ import { loadCanonicalSchema } from "./schema-loader.js";
23
+ export async function runCatalogueValidate(inputs, write) {
24
+ const sddPath = resolve(inputs.sddPath ?? process.cwd());
25
+ // An organisation root is aggregate-only by construction (M-ADR 0208): its own
26
+ // catalogues/ is deliberately empty and catalogues-aggregated/ is all it produces.
27
+ // Validating the empty member directory reported every file missing — a check
28
+ // that could only fail on such a root. Validate what it actually publishes.
29
+ const orgRoot = await isOrganisationRoot(sddPath);
30
+ const dirName = orgRoot ? "catalogues-aggregated" : "catalogues";
31
+ const cataloguesDir = join(sddPath, dirName);
32
+ const schema = await loadCanonicalSchema();
33
+ write(`sdd-cli catalogue validate (canonical schema ${schema.schemaVersion}, cli ${CLI_VERSION})\n`);
34
+ if (orgRoot)
35
+ write(`organisation root: validating ${dirName}/ (an organisation root publishes only its aggregate)\n`);
36
+ // Existence check on catalogues/ — fail fast with a clear message.
37
+ try {
38
+ const s = await stat(cataloguesDir);
39
+ if (!s.isDirectory()) {
40
+ throw new Error(`catalogues/ at ${cataloguesDir} exists but is not a directory`);
41
+ }
42
+ }
43
+ catch (err) {
44
+ if (err.code === "ENOENT") {
45
+ throw new Error(`catalogues/ not found at ${cataloguesDir}\n` +
46
+ ` Run \`sdd-cli catalogue build\` before \`validate\`.`);
47
+ }
48
+ throw err;
49
+ }
50
+ const findings = new FindingsLog();
51
+ const rowsByCatalogue = new Map();
52
+ let columnShapeOk = true;
53
+ for (const cat of schema.catalogues) {
54
+ const filePath = join(cataloguesDir, `${cat.name}.csv`);
55
+ let parsed;
56
+ try {
57
+ parsed = await readCatalogueCsv(filePath);
58
+ }
59
+ catch (err) {
60
+ if (err.code === "ENOENT") {
61
+ // Missing file is a shape failure — every catalogue declared in
62
+ // the schema MUST have a CSV (headers-only acceptable).
63
+ findings.add("convention", `${dirName}/${cat.name}.csv missing — every schema catalogue must have a file (headers-only acceptable)`);
64
+ columnShapeOk = false;
65
+ rowsByCatalogue.set(cat.name, []);
66
+ continue;
67
+ }
68
+ throw err;
69
+ }
70
+ const expected = cat.columns.map((c) => c.name);
71
+ const drift = compareColumns(expected, parsed.columns);
72
+ if (drift !== null) {
73
+ findings.add("convention", `${dirName}/${cat.name}.csv: column shape drift — ${drift}`);
74
+ columnShapeOk = false;
75
+ }
76
+ rowsByCatalogue.set(cat.name, parsed.rows);
77
+ }
78
+ // Derive the SDD profile from products.csv so profile-external FKs (schema
79
+ // 2.23.0) resolve consistently with `catalogue build`. A per-SDD catalogue
80
+ // has one distinct profile → pass it (an app catalogue's capabilities.domain_id
81
+ // is then treated external, matching the build). An aggregated catalogue mixes
82
+ // profiles → leave undefined, so those FKs validate as internal and MUST
83
+ // resolve (the merged platform domains are present to satisfy them).
84
+ const profile = singleProfile(rowsByCatalogue.get("products") ?? []);
85
+ // FK + bound-enum validation on the embedded engine (M-ADR 0205 §4.4
86
+ // cutover; same semantics, findings text unchanged — the A2/A3 dual-run
87
+ // record is the evidence). Enum hard-fail behaviour per M-ADR 0203 §4.1
88
+ // is unchanged: an out-of-set value is an error-severity finding.
89
+ const { fk, enums } = await validateWithEngineToLog(schema, rowsByCatalogue, findings, {
90
+ ...(profile !== undefined ? { profile } : {}),
91
+ // At organisation tier `domains` is keyed (platform_sdd_id, id): two platforms may
92
+ // each own a domain of one name, exactly as the aggregator validates it.
93
+ ...(orgRoot ? { platformScopedTargets: PLATFORM_SCOPED_AT_ORGANISATION } : {}),
94
+ });
95
+ // Report.
96
+ const findingsText = findings.render();
97
+ if (findingsText)
98
+ write(`\n${findingsText}\n`);
99
+ if (columnShapeOk) {
100
+ write(`\nColumn-shape check: PASS (${validatedPopulation(rowsByCatalogue)})\n`);
101
+ }
102
+ else {
103
+ write(`\nColumn-shape check: FAIL\n`);
104
+ }
105
+ if (fk.internalPass) {
106
+ write(`FK validation (internal): PASS (${validatedPopulation(rowsByCatalogue)})\n`);
107
+ }
108
+ else {
109
+ write(`FK validation (internal): FAIL (${fk.internalFailures.length} dangling)\n`);
110
+ for (const f of fk.internalFailures) {
111
+ write(` ${f.catalogue}.${f.column}: row pk=${f.referencingRow} → ${f.target}=${f.value || "<empty>"}\n`);
112
+ }
113
+ }
114
+ if (enums.pass) {
115
+ write(`Enum validation (column_enums): PASS (${validatedPopulation(rowsByCatalogue)})\n`);
116
+ }
117
+ else {
118
+ write(`Enum validation (column_enums): FAIL (${enums.violations.length} out-of-set)\n`);
119
+ for (const v of enums.violations) {
120
+ write(` ${v.catalogue}.${v.column}: row pk=${v.rowPk} value "${v.value}" (allowed: ${v.allowed})\n`);
121
+ }
122
+ }
123
+ if (fk.externalFindings > 0) {
124
+ write(`External findings: ${fk.externalFindings} (validated by aggregator)\n`);
125
+ }
126
+ // Force-touch writeCatalogue to keep imports honest if a future
127
+ // refactor wants to re-emit a cleaned-up file from validate. Today
128
+ // validate is read-only.
129
+ void writeCatalogue;
130
+ const strictFail = inputs.strict === true && findings.count() > 0;
131
+ // Error-severity findings (the bound-enum FAIL path) fail validate by
132
+ // default, matching `catalogue build`'s exit-code contract.
133
+ const exitCode = !columnShapeOk || !fk.internalPass || !enums.pass || findings.errorCount() > 0 || strictFail
134
+ ? 1
135
+ : 0;
136
+ return {
137
+ exitCode,
138
+ schemaVersion: schema.schemaVersion,
139
+ internalFkPass: fk.internalPass,
140
+ columnShapeOk,
141
+ enumPass: enums.pass,
142
+ findingsCount: findings.count(),
143
+ };
144
+ }
145
+ /**
146
+ * Compare a CSV's actual column header against the schema's declared
147
+ * column order. Returns null if exactly equal (order included), or a
148
+ * one-line description of the drift otherwise.
149
+ */
150
+ /**
151
+ * The single distinct non-empty `profile` across products rows, or undefined
152
+ * when there are zero or multiple (an aggregated catalogue). Drives
153
+ * profile-external FK handling (schema 2.23.0) — see the call site.
154
+ */
155
+ function singleProfile(products) {
156
+ const profiles = new Set();
157
+ for (const p of products) {
158
+ const v = String(p["profile"] ?? "");
159
+ if (v !== "")
160
+ profiles.add(v);
161
+ }
162
+ return profiles.size === 1 ? [...profiles][0] : undefined;
163
+ }
164
+ function compareColumns(expected, actual) {
165
+ if (expected.length === actual.length) {
166
+ let equal = true;
167
+ for (let i = 0; i < expected.length; i++) {
168
+ if (expected[i] !== actual[i]) {
169
+ equal = false;
170
+ break;
171
+ }
172
+ }
173
+ if (equal)
174
+ return null;
175
+ }
176
+ const expectedSet = new Set(expected);
177
+ const actualSet = new Set(actual);
178
+ const missing = expected.filter((c) => !actualSet.has(c));
179
+ const extra = actual.filter((c) => !expectedSet.has(c));
180
+ if (missing.length === 0 && extra.length === 0) {
181
+ return `column order differs (expected: ${expected.join(",")} | got: ${actual.join(",")})`;
182
+ }
183
+ const parts = [];
184
+ if (missing.length > 0)
185
+ parts.push(`missing: ${missing.join(",")}`);
186
+ if (extra.length > 0)
187
+ parts.push(`unexpected: ${extra.join(",")}`);
188
+ return parts.join("; ");
189
+ }
190
+ /** True when the SDD's manifest declares it an organisation root (aggregate-only). */
191
+ async function isOrganisationRoot(sddPath) {
192
+ try {
193
+ const raw = await readFile(join(sddPath, ".sdd", "sdd-manifest.yaml"), "utf8");
194
+ return /^category:\s*"?organisation-root"?\s*$/m.test(raw);
195
+ }
196
+ catch {
197
+ return false;
198
+ }
199
+ }
200
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1,18 @@
1
+ // `sdd-cli catalogue version` — prints the canonical schema version
2
+ // and the sdd-cli version pin so adopters can verify their tooling
3
+ // matches the schema they expect.
4
+ import { CLI_VERSION } from "./cli-version.js";
5
+ import { SCHEMA_COMPAT } from "./schema-compat.js";
6
+ import { loadCanonicalSchema } from "./schema-loader.js";
7
+ export async function runCatalogueVersion(write) {
8
+ const schema = await loadCanonicalSchema();
9
+ write([
10
+ `sdd-cli ${CLI_VERSION}`,
11
+ `catalogue schema ${schema.schemaVersion}`,
12
+ `accepted catalogue range ${SCHEMA_COMPAT.catalogue_schema}`,
13
+ `accepted sdd_manifest ${SCHEMA_COMPAT.sdd_manifest}`,
14
+ `accepted aggregation ${SCHEMA_COMPAT.aggregation_manifest}`,
15
+ "",
16
+ ].join("\n"));
17
+ }
18
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1,70 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { parse as parseYaml } from "yaml";
5
+ export const BINDING_FILENAME = ".sdd/implementation-binding.yaml";
6
+ /** One binding per implementation, for an SDD that specifies several (M-ADR 0214 §4.9). */
7
+ export const BINDINGS_DIRNAME = ".sdd/implementation-bindings";
8
+ export function bindingPath(targetRepo) {
9
+ return resolve(targetRepo, BINDING_FILENAME);
10
+ }
11
+ /**
12
+ * Every binding file of the SDD, relative to its root: the single-implementation file
13
+ * when present, then each `<implementation>.yaml` in the bindings directory, sorted.
14
+ */
15
+ export function bindingFiles(targetRepo) {
16
+ const out = [];
17
+ if (existsSync(bindingPath(targetRepo)))
18
+ out.push(BINDING_FILENAME);
19
+ const dir = resolve(targetRepo, BINDINGS_DIRNAME);
20
+ if (existsSync(dir)) {
21
+ for (const f of readdirSync(dir).sort()) {
22
+ if (f.endsWith(".yaml") || f.endsWith(".yml"))
23
+ out.push(`${BINDINGS_DIRNAME}/${f}`);
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ export function hasBinding(targetRepo) {
29
+ return bindingFiles(targetRepo).length > 0;
30
+ }
31
+ /**
32
+ * Flatten the binding's implementation members into derived checks. Each member's `checks[]` become
33
+ * Check objects (run as-is by the extractor core); the member's repo/role/lifecycle ride alongside so
34
+ * the emitted finding records *which implementation* drifted. A member with no checks contributes none
35
+ * (e.g. a decommissioned as-is member you no longer validate).
36
+ */
37
+ export async function deriveChecksFromBinding(targetRepo) {
38
+ const derived = [];
39
+ // A check id is the conformance_finding primary key within the SDD, so one declared by two
40
+ // bindings would collide there: refused here, where both files can be named.
41
+ const declaredIn = new Map();
42
+ for (const file of bindingFiles(targetRepo)) {
43
+ for (const d of await deriveFromFile(resolve(targetRepo, file))) {
44
+ const prior = declaredIn.get(d.check.id);
45
+ if (prior !== undefined) {
46
+ throw new Error(`conformance check id '${d.check.id}' is declared by both ${prior} and ${file} — check ids are unique within the SDD (M-ADR 0214 §4.9)`);
47
+ }
48
+ declaredIn.set(d.check.id, file);
49
+ derived.push(d);
50
+ }
51
+ }
52
+ return derived;
53
+ }
54
+ async function deriveFromFile(path) {
55
+ const raw = parseYaml(await readFile(path, "utf-8"));
56
+ const derived = [];
57
+ for (const impl of raw?.implementations ?? []) {
58
+ const member = {
59
+ repo: impl.repo,
60
+ ...(impl.role ? { role: impl.role } : {}),
61
+ ...(impl.lifecycle ? { lifecycle: impl.lifecycle } : {}),
62
+ ...(impl.stack ? { stack: impl.stack } : {}),
63
+ };
64
+ for (const check of impl.checks ?? []) {
65
+ derived.push({ check, member });
66
+ }
67
+ }
68
+ return derived;
69
+ }
70
+ //# sourceMappingURL=binding.js.map