@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,229 @@
1
+ // C4 L3 components discoverer — emits `c4_components.csv` rows from
2
+ // C4-PlantUML-style mermaid in:
3
+ // - docs/domains/<slug>/architecture/diagrams/c4-l3-*.mermaid.md
4
+ // - docs/platform/diagrams/c4-l3-*.mermaid.md
5
+ // - docs/c4-l3-*.mermaid.md (per-product, application/integration SDDs)
6
+ //
7
+ // Mirrors c4-containers.ts at one level down. Source convention per
8
+ // `coordination/c4-l3-walker-and-path-convention-coordination.md`
9
+ // (sdd-method-gov, 2026-05-29). The walker is granularity-agnostic — both
10
+ // per-container files (one `Container_Boundary` block per file) and
11
+ // per-domain rollup files (multiple `Container_Boundary` blocks per file)
12
+ // produce identical row output. Adopters pick the file shape that suits
13
+ // their authoring workflow.
14
+ //
15
+ // Shape rules:
16
+ // Container_Boundary(local_id, "Container Display Name") {
17
+ // Component(component_id, "Component Name", "tech", "description")
18
+ // Component(...)
19
+ // }
20
+ //
21
+ // container_id resolution: the walker slugifies the Container_Boundary's
22
+ // display name and matches against `c4_containers.id` rows that end in
23
+ // `-{slug}` within this SDD's system. Unmatched containers fire
24
+ // `[finding] orphan` and the contained Components are skipped.
25
+ //
26
+ // service_id is denormalised from the resolved container's service_id —
27
+ // populated when the parent container represents a runtime service,
28
+ // empty otherwise.
29
+ //
30
+ // Row id shape: `{container_id}-{component-slug}` per schema 2.15.0
31
+ // description.
32
+ import { readdir, readFile } from "node:fs/promises";
33
+ import { join, sep } from "node:path";
34
+ const L3_PREFIX = "c4-l3-";
35
+ const L3_SUFFIX = ".mermaid.md";
36
+ // Boundary opener — only Container_Boundary at L3. System_Boundary is L2's domain.
37
+ const BOUNDARY_OPEN_RE = /^Container_Boundary\s*\(\s*\w+\s*,\s*"([^"]+)"\s*\)\s*\{/;
38
+ // C4-PlantUML component entries inside Container_Boundary blocks.
39
+ const COMPONENT_RE = /Component\s*\(\s*(\w+)\s*,\s*"([^"]+)"\s*(?:,\s*"([^"]*)")?\s*(?:,\s*"([^"]*)")?\s*\)/;
40
+ export async function buildC4Components(inputs) {
41
+ // No containers → nothing to attach components to. Skip silently; the
42
+ // L2 walker would have logged the absence.
43
+ if (inputs.c4ContainerRows.length === 0)
44
+ return [];
45
+ // Index containers by suffix-after-system-prefix for slug-of-name matching.
46
+ // c4_containers.id shape per c4-containers.ts:93 is `{systemId}-{slug}`.
47
+ // The suffix-match lets us resolve `Container_Boundary(_, "Asset Twin")`
48
+ // (slug = "asset-twin") to `{anything}-asset-twin`.
49
+ const containerBySuffix = new Map();
50
+ for (const row of inputs.c4ContainerRows) {
51
+ const id = String(row["id"] ?? "");
52
+ if (id === "")
53
+ continue;
54
+ // The full id IS the suffix in the trivial case; also index by the
55
+ // trailing `-<slug>` portion in case the system prefix varies.
56
+ containerBySuffix.set(id, row);
57
+ const dash = id.lastIndexOf("-");
58
+ if (dash > 0) {
59
+ const tail = id.slice(dash + 1);
60
+ // Only add tail-indexing when it's unique enough to be useful.
61
+ // (Multiple containers with the same tail across systems is the
62
+ // ambiguous case; we keep the first-seen and rely on orphan-finding
63
+ // for the others.)
64
+ if (!containerBySuffix.has(tail))
65
+ containerBySuffix.set(tail, row);
66
+ }
67
+ }
68
+ const files = await collectL3Files(inputs.sddPath);
69
+ const rows = [];
70
+ const seen = new Set();
71
+ for (const file of files) {
72
+ const content = await readFile(file.absPath, "utf-8");
73
+ for (const block of parseContainerBoundaries(content)) {
74
+ const boundarySlug = slugify(block.displayName);
75
+ const containerRow = resolveContainer(boundarySlug, containerBySuffix);
76
+ if (containerRow === undefined) {
77
+ inputs.findings.add("orphan", `c4_components: ${file.relPath}: Container_Boundary "${block.displayName}" doesn't resolve to a c4_containers row — ${block.components.length} component(s) skipped`);
78
+ continue;
79
+ }
80
+ const containerId = String(containerRow["id"]);
81
+ const denormalisedServiceId = String(containerRow["service_id"] ?? "");
82
+ for (const c of block.components) {
83
+ const componentSlug = slugify(c.name);
84
+ if (componentSlug === "")
85
+ continue;
86
+ const id = `${containerId}-${componentSlug}`;
87
+ if (seen.has(id))
88
+ continue;
89
+ seen.add(id);
90
+ rows.push({
91
+ id,
92
+ name: c.name,
93
+ container_id: containerId,
94
+ service_id: denormalisedServiceId,
95
+ path: "",
96
+ responsibility: c.description,
97
+ source_sdd_id: "",
98
+ });
99
+ }
100
+ }
101
+ }
102
+ rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
103
+ return rows;
104
+ }
105
+ function resolveContainer(slug, containerBySuffix) {
106
+ if (slug === "")
107
+ return undefined;
108
+ // Try the trailing-segment index first (the common case where slug is the
109
+ // tail of `{systemId}-{slug}`); fall back to scanning all rows for any
110
+ // id ending in `-{slug}`.
111
+ const tailHit = containerBySuffix.get(slug);
112
+ if (tailHit !== undefined)
113
+ return tailHit;
114
+ for (const [_, row] of containerBySuffix) {
115
+ if (String(row["id"]).endsWith(`-${slug}`))
116
+ return row;
117
+ }
118
+ return undefined;
119
+ }
120
+ async function collectL3Files(sddPath) {
121
+ const out = [];
122
+ // 1. Per-domain L3 files: docs/domains/<dom>/architecture/diagrams/c4-l3-*.mermaid.md
123
+ const domainsDir = join(sddPath, "docs", "domains");
124
+ let domainEntries;
125
+ try {
126
+ domainEntries = (await readdir(domainsDir, { withFileTypes: true }))
127
+ .filter((e) => e.isDirectory())
128
+ .map((e) => ({ name: e.name }));
129
+ }
130
+ catch (err) {
131
+ if (err.code !== "ENOENT")
132
+ throw err;
133
+ domainEntries = [];
134
+ }
135
+ for (const entry of domainEntries.sort((a, b) => a.name.localeCompare(b.name))) {
136
+ const diagramsDir = join(sddPath, "docs", "domains", entry.name, "architecture", "diagrams");
137
+ const files = await listMatching(diagramsDir, L3_PREFIX, L3_SUFFIX);
138
+ for (const filename of files.sort()) {
139
+ const abs = join(diagramsDir, filename);
140
+ out.push({
141
+ absPath: abs,
142
+ relPath: join("docs", "domains", entry.name, "architecture", "diagrams", filename)
143
+ .split(sep)
144
+ .join("/"),
145
+ });
146
+ }
147
+ }
148
+ // 2. Platform-wide L3: docs/platform/diagrams/c4-l3-*.mermaid.md
149
+ const platformDir = join(sddPath, "docs", "platform", "diagrams");
150
+ const platformFiles = await listMatching(platformDir, L3_PREFIX, L3_SUFFIX);
151
+ for (const filename of platformFiles.sort()) {
152
+ const abs = join(platformDir, filename);
153
+ out.push({
154
+ absPath: abs,
155
+ relPath: join("docs", "platform", "diagrams", filename).split(sep).join("/"),
156
+ });
157
+ }
158
+ // 3. Per-product L3 (application-product / integration-product SDDs).
159
+ const productDir = join(sddPath, "docs");
160
+ const productFiles = await listMatching(productDir, L3_PREFIX, L3_SUFFIX);
161
+ for (const filename of productFiles.sort()) {
162
+ const abs = join(productDir, filename);
163
+ out.push({
164
+ absPath: abs,
165
+ relPath: join("docs", filename).split(sep).join("/"),
166
+ });
167
+ }
168
+ return out;
169
+ }
170
+ async function listMatching(dir, prefix, suffix) {
171
+ try {
172
+ const entries = await readdir(dir, { withFileTypes: true });
173
+ return entries
174
+ .filter((e) => e.isFile() && e.name.startsWith(prefix) && e.name.endsWith(suffix))
175
+ .map((e) => e.name);
176
+ }
177
+ catch (err) {
178
+ if (err.code === "ENOENT")
179
+ return [];
180
+ throw err;
181
+ }
182
+ }
183
+ function parseContainerBoundaries(content) {
184
+ const out = [];
185
+ const lines = content.split(/\r?\n/);
186
+ let i = 0;
187
+ while (i < lines.length) {
188
+ const line = (lines[i] ?? "").trim();
189
+ const open = BOUNDARY_OPEN_RE.exec(line);
190
+ if (!open) {
191
+ i++;
192
+ continue;
193
+ }
194
+ const displayName = open[1] ?? "";
195
+ const components = [];
196
+ i++;
197
+ let depth = 1; // we just saw `{`
198
+ while (i < lines.length && depth > 0) {
199
+ const inner = (lines[i] ?? "").trim();
200
+ if (inner.endsWith("{"))
201
+ depth++;
202
+ else if (inner === "}") {
203
+ depth--;
204
+ i++;
205
+ continue;
206
+ }
207
+ const m = COMPONENT_RE.exec(inner);
208
+ if (m) {
209
+ components.push({
210
+ entryId: m[1] ?? "",
211
+ name: m[2] ?? "",
212
+ technology: m[3] ?? "",
213
+ description: m[4] ?? "",
214
+ });
215
+ }
216
+ i++;
217
+ }
218
+ out.push({ displayName, components });
219
+ }
220
+ return out;
221
+ }
222
+ function slugify(s) {
223
+ return s
224
+ .toLowerCase()
225
+ .replace(/\s*\([^)]+\)/g, "")
226
+ .replace(/[^a-z0-9]+/g, "-")
227
+ .replace(/^-+|-+$/g, "");
228
+ }
229
+ //# sourceMappingURL=c4-components.js.map
@@ -0,0 +1,240 @@
1
+ // C4 L2 containers discoverer — emits `c4_containers.csv` rows from
2
+ // C4-PlantUML-style mermaid in:
3
+ // - docs/domains/<slug>/architecture/diagrams/c4-l2-container.mermaid.md
4
+ // - docs/platform/diagrams/c4-l2-container.mermaid.md
5
+ // - docs/c4-l2-container.mermaid.md (per-product, application/integration SDDs)
6
+ //
7
+ // Per schema 2.0.0:
8
+ // - system_id (required FK to c4_systems.id) — for single-system
9
+ // ecosystems we use {sddId}-system. Multi-system support (parsing a
10
+ // `**System:** <id>` metadata line) is a future hook.
11
+ // - domain_id (nullable, denormalised) derived from the L2 file path:
12
+ // docs/domains/<slug>/... → <slug>; platform-wide L2 → empty.
13
+ // - service_id (optional) — opportunistic match on the C4 entry's id
14
+ // or a slug of its display name to a known service id.
15
+ //
16
+ // We only consider entries inside System_Boundary or Container_Boundary
17
+ // blocks — mirrors the python reference and excludes Person/System_Ext
18
+ // declarations that decorate the diagram.
19
+ //
20
+ // Dedupe is by row id; first-seen wins (a container appearing in
21
+ // multiple L2 files becomes one row).
22
+ import { readdir, readFile, stat } from "node:fs/promises";
23
+ import { join, sep } from "node:path";
24
+ const L2_BASENAME = "c4-l2-container.mermaid.md";
25
+ // Boundary opener: `System_Boundary(id, "name") {` or `Container_Boundary(...)`.
26
+ const BOUNDARY_OPEN_RE = /^(System_Boundary|Container_Boundary)\s*\(\s*\w+/;
27
+ // C4-PlantUML container entries inside boundaries. Per ADR 0155 the
28
+ // canonical c4_container_type is Application | Data Store only:
29
+ // Container(id, "name", "tech", "desc") → Application
30
+ // ContainerDb(id, "name", "tech", "desc") → Data Store
31
+ // ContainerQueue(id, "name", "tech", "desc") → Data Store (a running
32
+ // broker/queue is a
33
+ // container, not "infra")
34
+ // `Container_Ext` is intentionally NOT matched here — external containers
35
+ // are catalogued as `c4_external_systems` rows by the c4-external-systems
36
+ // builder, never as internal container rows.
37
+ const ENTRY_RE = /(Container|ContainerDb|ContainerQueue)\s*\(\s*(\w+)\s*,\s*"([^"]+)"\s*(?:,\s*"([^"]*)")?\s*(?:,\s*"([^"]*)")?\s*\)/;
38
+ const TYPE_BY_KIND = {
39
+ Container: "Application",
40
+ ContainerDb: "Data Store",
41
+ ContainerQueue: "Data Store",
42
+ };
43
+ export async function buildC4Containers(inputs) {
44
+ const systemId = inputs.manifest.c4SystemId ?? `${inputs.manifest.sddId}-system`;
45
+ if (!inputs.knownSystemIds.has(systemId)) {
46
+ // No system row, so no FK target for container rows. Never silent when
47
+ // that drops authored content, and never on the strength of a sibling
48
+ // builder's log (M-ADR 0133 §4.2c no-silent-drop): if L2 diagrams
49
+ // exist, THIS builder says it is dropping them.
50
+ const orphanedL2 = await collectL2Files(inputs.sddPath);
51
+ if (orphanedL2.length > 0) {
52
+ inputs.findings.add("coverage", `c4_containers: no c4_systems row '${systemId}' to own containers; ` +
53
+ `${orphanedL2.length} L2 diagram file(s) present but not catalogued`);
54
+ }
55
+ return [];
56
+ }
57
+ const files = await collectL2Files(inputs.sddPath);
58
+ const rows = [];
59
+ const seen = new Set();
60
+ const rowById = new Map();
61
+ for (const file of files) {
62
+ const content = await readFile(file.absPath, "utf-8");
63
+ for (const entry of parseBoundaryEntries(content)) {
64
+ const slug = slugify(entry.name);
65
+ if (slug === "")
66
+ continue;
67
+ const id = `${systemId}-${slug}`;
68
+ if (!inputs.containerIdByEntryIdOut?.has(entry.entryId)) {
69
+ inputs.containerIdByEntryIdOut?.set(entry.entryId, id);
70
+ }
71
+ if (!inputs.containerNameByEntryIdOut?.has(entry.entryId)) {
72
+ inputs.containerNameByEntryIdOut?.set(entry.entryId, entry.name);
73
+ }
74
+ if (!inputs.containerTechByEntryIdOut?.has(entry.entryId) && entry.technology !== "") {
75
+ inputs.containerTechByEntryIdOut?.set(entry.entryId, entry.technology);
76
+ }
77
+ const collapsed = rowById.get(id);
78
+ if (collapsed !== undefined) {
79
+ // F21: dedup-by-id is first-seen and was SILENT — two containers
80
+ // sharing a display name collapse into one row, and every edge
81
+ // referencing either binds to the merge. When their technology
82
+ // strings flatly contradict, that is two different things wearing
83
+ // one name, and the collapse is the loudest available signal.
84
+ if (techContradicts(String(collapsed["technology"] ?? ""), entry.technology)) {
85
+ inputs.findings.add("convention", `c4_containers: two containers share the display name "${entry.name}" with contradictory ` +
86
+ `technologies ("${String(collapsed["technology"])}" vs "${entry.technology}") — collapsed ` +
87
+ `into one row "${id}" — either different referents wearing one name (rename one) ` +
88
+ `or a misused technology field on a shared reference (correct it) (F21)`);
89
+ }
90
+ continue;
91
+ }
92
+ seen.add(id);
93
+ const serviceId = inputs.knownServiceIds.has(entry.entryId)
94
+ ? entry.entryId
95
+ : inputs.knownServiceIds.has(slug)
96
+ ? slug
97
+ : "";
98
+ const domainId = file.domainId !== "" && inputs.knownDomainIds.has(file.domainId)
99
+ ? file.domainId
100
+ : "";
101
+ if (file.domainId !== "" && domainId === "") {
102
+ inputs.findings.add("orphan", `c4_containers: L2 file at ${file.relPath} references unknown domain "${file.domainId}" — domain_id left empty`);
103
+ }
104
+ const row = {
105
+ id,
106
+ name: entry.name,
107
+ system_id: systemId,
108
+ domain_id: domainId,
109
+ service_id: serviceId,
110
+ type: TYPE_BY_KIND[entry.kind] ?? "",
111
+ technology: entry.technology,
112
+ description: entry.description,
113
+ source_sdd_id: "",
114
+ };
115
+ rowById.set(id, row);
116
+ rows.push(row);
117
+ }
118
+ }
119
+ rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
120
+ return rows;
121
+ }
122
+ async function collectL2Files(sddPath) {
123
+ const out = [];
124
+ // 1. Per-domain L2 files.
125
+ const domainsDir = join(sddPath, "docs", "domains");
126
+ let domainEntries;
127
+ try {
128
+ domainEntries = (await readdir(domainsDir, { withFileTypes: true }))
129
+ .filter((e) => e.isDirectory())
130
+ .map((e) => ({ name: e.name }));
131
+ }
132
+ catch (err) {
133
+ if (err.code !== "ENOENT")
134
+ throw err;
135
+ domainEntries = [];
136
+ }
137
+ for (const entry of domainEntries.sort((a, b) => a.name.localeCompare(b.name))) {
138
+ const rel = join("docs", "domains", entry.name, "architecture", "diagrams", L2_BASENAME);
139
+ const abs = join(sddPath, rel);
140
+ if (await isFile(abs)) {
141
+ out.push({
142
+ absPath: abs,
143
+ relPath: rel.split(sep).join("/"),
144
+ domainId: entry.name,
145
+ });
146
+ }
147
+ }
148
+ // 2. Platform-wide L2.
149
+ const platformRel = join("docs", "platform", "diagrams", L2_BASENAME);
150
+ const platformAbs = join(sddPath, platformRel);
151
+ if (await isFile(platformAbs)) {
152
+ out.push({
153
+ absPath: platformAbs,
154
+ relPath: platformRel.split(sep).join("/"),
155
+ domainId: "",
156
+ });
157
+ }
158
+ // 3. Per-product L2 (application-product / integration-product SDDs).
159
+ // Lives at the SDD docs/ root; the system is the SDD itself (one
160
+ // c4_systems row per SDD), so the boundary in the L2 is the product
161
+ // boundary. domain_id is left empty — federated app/integration
162
+ // products do not have a primary domain per ADR 0136.
163
+ const productRel = join("docs", L2_BASENAME);
164
+ const productAbs = join(sddPath, productRel);
165
+ if (await isFile(productAbs)) {
166
+ out.push({
167
+ absPath: productAbs,
168
+ relPath: productRel.split(sep).join("/"),
169
+ domainId: "",
170
+ });
171
+ }
172
+ return out;
173
+ }
174
+ async function isFile(path) {
175
+ try {
176
+ const s = await stat(path);
177
+ return s.isFile();
178
+ }
179
+ catch (err) {
180
+ if (err.code === "ENOENT")
181
+ return false;
182
+ throw err;
183
+ }
184
+ }
185
+ function parseBoundaryEntries(content) {
186
+ const out = [];
187
+ const stack = [];
188
+ for (const raw of content.split(/\r?\n/)) {
189
+ const line = raw.trim();
190
+ if (BOUNDARY_OPEN_RE.test(line) && line.includes("{")) {
191
+ stack.push("boundary");
192
+ continue;
193
+ }
194
+ if (line === "}" && stack.length > 0) {
195
+ stack.pop();
196
+ continue;
197
+ }
198
+ if (stack.length === 0)
199
+ continue;
200
+ const m = ENTRY_RE.exec(line);
201
+ if (!m)
202
+ continue;
203
+ out.push({
204
+ kind: m[1] ?? "",
205
+ entryId: m[2] ?? "",
206
+ name: m[3] ?? "",
207
+ technology: m[4] ?? "",
208
+ description: m[5] ?? "",
209
+ });
210
+ }
211
+ return out;
212
+ }
213
+ function slugify(s) {
214
+ return s
215
+ .toLowerCase()
216
+ .replace(/\s*\([^)]+\)/g, "")
217
+ .replace(/[^a-z0-9]+/g, "-")
218
+ .replace(/^-+|-+$/g, "");
219
+ }
220
+ /**
221
+ * F21: two technology strings flatly contradict when both are present and
222
+ * share no token — ".NET 6 (in Linton)" vs "Java" needs no NLP. Absent or
223
+ * overlapping strings are NOT a contradiction (silence, not a guess).
224
+ */
225
+ export function techContradicts(a, b) {
226
+ const tokens = (s) => new Set(s
227
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
228
+ .toLowerCase()
229
+ .split(/[^a-z0-9.+#]+/)
230
+ .filter((t) => t !== ""));
231
+ const ta = tokens(a);
232
+ const tb = tokens(b);
233
+ if (ta.size === 0 || tb.size === 0)
234
+ return false;
235
+ for (const t of ta)
236
+ if (tb.has(t))
237
+ return false;
238
+ return true;
239
+ }
240
+ //# sourceMappingURL=c4-containers.js.map
@@ -0,0 +1,87 @@
1
+ // Phase 3 C4-to-entity roll-up walkers per ADR 0159 §4.8.
2
+ // Both are pure transitive joins over already-FK-enforced inputs.
3
+ //
4
+ // buildC4ContainerEntity:
5
+ // service_entity ⋈ c4_containers.service_id
6
+ // → emit (container_id, entity_id, relation) inheriting relation from
7
+ // upstream service_entity. Dedup by (container_id, entity_id, relation).
8
+ //
9
+ // buildC4SystemEntity:
10
+ // c4_container_entity ⋈ c4_containers.system_id
11
+ // → aggregate (system_id, entity_id) with container_count.
12
+ //
13
+ // Both handle empty inputs gracefully (an adopter's pre-lock state has zero
14
+ // service_entity edges → both roll-up outputs are empty; no errors).
15
+ export function buildC4ContainerEntity(inputs) {
16
+ // Index containers by service_id.
17
+ const containersByServiceId = new Map();
18
+ for (const c of inputs.c4Containers) {
19
+ const sid = String(c["service_id"] ?? "");
20
+ if (!sid)
21
+ continue;
22
+ const cid = String(c["id"]);
23
+ const list = containersByServiceId.get(sid) ?? [];
24
+ list.push(cid);
25
+ containersByServiceId.set(sid, list);
26
+ }
27
+ const rowsByPk = new Map();
28
+ for (const se of inputs.serviceEntityRows) {
29
+ const sid = String(se["service_id"]);
30
+ const eid = String(se["entity_id"]);
31
+ const rel = String(se["relation"] ?? "");
32
+ const containers = containersByServiceId.get(sid);
33
+ if (!containers)
34
+ continue;
35
+ for (const cid of containers) {
36
+ const pk = `${cid}__${eid}`;
37
+ if (rowsByPk.has(pk))
38
+ continue; // first-relation-wins on dup
39
+ rowsByPk.set(pk, {
40
+ id: pk,
41
+ container_id: cid,
42
+ entity_id: eid,
43
+ relation: rel,
44
+ source_sdd_id: "",
45
+ });
46
+ }
47
+ }
48
+ return [...rowsByPk.values()];
49
+ }
50
+ export function buildC4SystemEntity(inputs) {
51
+ // Index containers by id for system lookup.
52
+ const containerById = new Map();
53
+ for (const c of inputs.c4Containers) {
54
+ containerById.set(String(c["id"]), c);
55
+ }
56
+ const counts = new Map(); // key: `${system_id}|${entity_id}`
57
+ const systemByKey = new Map();
58
+ for (const ce of inputs.containerEntityRows) {
59
+ const c = containerById.get(String(ce["container_id"]));
60
+ if (!c)
61
+ continue;
62
+ const sysId = String(c["system_id"] ?? "");
63
+ if (!sysId)
64
+ continue;
65
+ const eid = String(ce["entity_id"]);
66
+ const key = `${sysId}|${eid}`;
67
+ counts.set(key, (counts.get(key) ?? 0) + 1);
68
+ if (!systemByKey.has(key)) {
69
+ systemByKey.set(key, { system_id: sysId, entity_id: eid });
70
+ }
71
+ }
72
+ const rows = [];
73
+ for (const [key, count] of counts) {
74
+ const e = systemByKey.get(key);
75
+ if (!e)
76
+ continue;
77
+ rows.push({
78
+ id: `${e.system_id}__${e.entity_id}`,
79
+ system_id: e.system_id,
80
+ entity_id: e.entity_id,
81
+ container_count: count,
82
+ source_sdd_id: "",
83
+ });
84
+ }
85
+ return rows;
86
+ }
87
+ //# sourceMappingURL=c4-entity-rollups.js.map