@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,194 @@
1
+ // C4 external software systems discoverer — emits `c4_external_systems.csv`
2
+ // rows (per ADR 0155). External systems are C4 L1 peers the ecosystem
3
+ // integrates with but does not own; they are rendered `System_Ext(...)` on
4
+ // L1/L2 diagrams (and occasionally `Container_Ext(...)` for an external
5
+ // system's container). They are NOT c4_containers (not inside your system)
6
+ // and NOT c4_systems (no owning Product).
7
+ //
8
+ // We scan the same L1 + L2 files the c4-systems / c4-containers builders
9
+ // read, and pull every `System_Ext(id, "name", "desc")` and
10
+ // `Container_Ext(id, "name", "tech", "desc")` declaration. The row id
11
+ // derives from the DECLARED diagram entry id — `ext-{slug(entry-id)}`
12
+ // (schema 3.0.0, M-ADR 0203 §4.2) — so identity is the author's declared
13
+ // assertion, the same identity the aggregator adjudicates (M-ADR 0189
14
+ // §4.2). The same declared identity across SDDs collapses to one row at
15
+ // aggregation (the dedup-by-PK exception to ADR 0134). Two entries whose
16
+ // DISPLAY NAMES agree but whose entry ids differ are two rows — the
17
+ // same-entity-two-labels case made visible instead of silently merged;
18
+ // the 2.x name-slug ids map to these via the ext-id migration map.
19
+ //
20
+ // The returned `idByEntryId` map (diagram entry-id → row id) lets the
21
+ // system_external_dependency builder resolve `Rel(...)` endpoints.
22
+ import { readdir, readFile, stat } from "node:fs/promises";
23
+ import { join, sep } from "node:path";
24
+ import { C4_L1_CANDIDATES, SYNDICATED_PLATFORM_L1, findDomainScopedL1s, } from "./c4-l1-candidates.js";
25
+ import { extIdFromEntryId } from "../ext-id.js";
26
+ // `System_Ext(id, "name", "desc")` — no technology argument.
27
+ const SYSTEM_EXT_RE = /System_Ext\s*\(\s*(\w+)\s*,\s*"([^"]+)"\s*(?:,\s*"([^"]*)")?\s*\)/;
28
+ // `Container_Ext(id, "name", "tech", "desc")` — external container.
29
+ const CONTAINER_EXT_RE = /Container_Ext\s*\(\s*(\w+)\s*,\s*"([^"]+)"\s*(?:,\s*"([^"]*)")?\s*(?:,\s*"([^"]*)")?\s*\)/;
30
+ // Shares the candidate list with c4-systems / persona-c4system for DRY (one
31
+ // source of truth — see c4-l1-candidates.ts). NOTE: unlike those two, this
32
+ // builder does NOT pick one file — it UNIONS every existing L1 file (own +
33
+ // domain-scoped + syndicated) plus the L2 files and dedups externals by slug
34
+ // at aggregation, so the candidate ORDER is irrelevant here. The syndicated
35
+ // copy deliberately stays in the union (unlike identity sourcing, M-ADR 0189
36
+ // §4.5): a sibling's own diagram Rel(...) lines may reference entries the
37
+ // syndicated file declares, and dropping it would silently orphan those
38
+ // edges. Its dedup-by-slug makes the extra harvest idempotent at aggregation.
39
+ const L1_CANDIDATES = C4_L1_CANDIDATES;
40
+ const L2_BASENAME = "c4-l2-container.mermaid.md";
41
+ export async function buildC4ExternalSystems(inputs) {
42
+ const files = await collectC4DiagramFiles(inputs.sddPath);
43
+ const rows = [];
44
+ const idByEntryId = new Map();
45
+ const nameByEntryId = new Map();
46
+ const technologyByEntryId = new Map();
47
+ const descriptionByEntryId = new Map();
48
+ const seen = new Map();
49
+ for (const { abs, scope } of files) {
50
+ const content = await readFile(abs, "utf-8");
51
+ for (const ext of parseExternals(content)) {
52
+ // Identity is the declared entry id (schema 3.0.0). An entry id that
53
+ // normalises to nothing cannot key a row — announced, never silent
54
+ // (M-ADR 0133 §4.2c).
55
+ const id = extIdFromEntryId(ext.entryId);
56
+ if (id === "") {
57
+ inputs.findings.add("convention", `c4_external_systems: entry id "${ext.entryId}" ("${ext.name}") normalises to an empty slug — no row emitted; rename the diagram entry id`);
58
+ continue;
59
+ }
60
+ idByEntryId.set(ext.entryId, id);
61
+ if (!nameByEntryId.has(ext.entryId))
62
+ nameByEntryId.set(ext.entryId, ext.name);
63
+ if (!technologyByEntryId.has(ext.entryId) && ext.technology !== "") {
64
+ technologyByEntryId.set(ext.entryId, ext.technology);
65
+ }
66
+ if (!descriptionByEntryId.has(ext.entryId) && ext.description !== "") {
67
+ descriptionByEntryId.set(ext.entryId, ext.description);
68
+ }
69
+ const existing = seen.get(id);
70
+ if (existing) {
71
+ // Same declared identity referenced again — first row wins; flag
72
+ // attribute conflicts so authors can reconcile. Name joins the
73
+ // checked set at 3.0.0: with the id keyed on the entry id, one
74
+ // identity under two display names is exactly this conflict.
75
+ if ((ext.name !== "" &&
76
+ existing["name"] !== "" &&
77
+ existing["name"] !== ext.name) ||
78
+ (ext.technology !== "" &&
79
+ existing["technology"] !== "" &&
80
+ existing["technology"] !== ext.technology) ||
81
+ (ext.description !== "" &&
82
+ existing["description"] !== "" &&
83
+ existing["description"] !== ext.description)) {
84
+ inputs.findings.add("convention", `c4_external_systems: id "${id}" declared with conflicting attributes within ${inputs.manifest.sddId} — first-seen kept`);
85
+ }
86
+ continue;
87
+ }
88
+ const row = {
89
+ id,
90
+ name: ext.name,
91
+ technology: ext.technology,
92
+ description: ext.description,
93
+ // Adjudication provenance (M-ADR 0189 §4.1/§4.2, schema 2.30.0):
94
+ // the declared entry id is the identity the aggregator resolves;
95
+ // the scope records whether the first-seen declaration was
96
+ // ecosystem-context (l1) or scope-relative (l2).
97
+ declared_id: ext.entryId,
98
+ source_scope: scope,
99
+ source_sdd_id: "",
100
+ };
101
+ seen.set(id, row);
102
+ rows.push(row);
103
+ }
104
+ }
105
+ rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
106
+ return { rows, idByEntryId, nameByEntryId, technologyByEntryId, descriptionByEntryId };
107
+ }
108
+ function parseExternals(content) {
109
+ const out = [];
110
+ for (const raw of content.split(/\r?\n/)) {
111
+ const line = raw.trim();
112
+ const se = SYSTEM_EXT_RE.exec(line);
113
+ if (se) {
114
+ out.push({
115
+ entryId: se[1] ?? "",
116
+ name: se[2] ?? "",
117
+ technology: "",
118
+ description: se[3] ?? "",
119
+ });
120
+ continue;
121
+ }
122
+ const ce = CONTAINER_EXT_RE.exec(line);
123
+ if (ce) {
124
+ out.push({
125
+ entryId: ce[1] ?? "",
126
+ name: ce[2] ?? "",
127
+ technology: ce[3] ?? "",
128
+ description: ce[4] ?? "",
129
+ });
130
+ }
131
+ }
132
+ return out;
133
+ }
134
+ /**
135
+ * Every C4 diagram file an SDD can carry (all L1 candidates incl.
136
+ * domain-scoped, plus every L2), scope-tagged. Shared with
137
+ * system-external-dependency (F11c: that builder kept a private,
138
+ * pre-0.30.1-ordered copy MISSING the domain-scoped path — the fourth
139
+ * divergent copy of this list, and a silent drop inside adjudication:
140
+ * a Rel in a domain-scoped L1 was never harvested, so a converted
141
+ * candidate's relationship was recorded nowhere).
142
+ */
143
+ export async function collectC4DiagramFiles(sddPath) {
144
+ const out = [];
145
+ for (const rel of L1_CANDIDATES) {
146
+ const abs = join(sddPath, rel);
147
+ if (await isFile(abs)) {
148
+ out.push({ abs, scope: "l1", syndicated: rel === SYNDICATED_PLATFORM_L1 });
149
+ }
150
+ }
151
+ // Domain-scoped L1s (M-ADR 0189 §4.5) — union, like every other candidate.
152
+ for (const rel of await findDomainScopedL1s(sddPath)) {
153
+ out.push({ abs: join(sddPath, rel), scope: "l1", syndicated: false });
154
+ }
155
+ // Per-domain L2 files.
156
+ const domainsDir = join(sddPath, "docs", "domains");
157
+ try {
158
+ const entries = (await readdir(domainsDir, { withFileTypes: true }))
159
+ .filter((e) => e.isDirectory())
160
+ .map((e) => e.name)
161
+ .sort((a, b) => a.localeCompare(b));
162
+ for (const name of entries) {
163
+ const abs = join(sddPath, "docs", "domains", name, "architecture", "diagrams", L2_BASENAME);
164
+ if (await isFile(abs))
165
+ out.push({ abs, scope: "l2", syndicated: false });
166
+ }
167
+ }
168
+ catch (err) {
169
+ if (err.code !== "ENOENT")
170
+ throw err;
171
+ }
172
+ // Platform-wide + per-product L2.
173
+ for (const rel of [
174
+ join("docs", "platform", "diagrams", L2_BASENAME),
175
+ join("docs", L2_BASENAME),
176
+ ]) {
177
+ const abs = join(sddPath, rel);
178
+ if (await isFile(abs))
179
+ out.push({ abs, scope: "l2", syndicated: false });
180
+ }
181
+ void sep;
182
+ return out;
183
+ }
184
+ async function isFile(path) {
185
+ try {
186
+ return (await stat(path)).isFile();
187
+ }
188
+ catch (err) {
189
+ if (err.code === "ENOENT")
190
+ return false;
191
+ throw err;
192
+ }
193
+ }
194
+ //# sourceMappingURL=c4-external-systems.js.map
@@ -0,0 +1,107 @@
1
+ // Shared L1 context-diagram path resolution for the L1-reading C4 builders
2
+ // (c4-systems, persona-c4system, c4-external-systems).
3
+ //
4
+ // **Own-L1-first, syndicated-for-platform-root-only** (M-ADR 0189 §4.5).
5
+ // Under federation a sibling SDD typically carries a syndicated copy of the
6
+ // platform-root's L1 at `docs/platform/diagrams/c4-l1-context.mermaid.md`
7
+ // for cross-reference, AND may author its own L1. The sibling's own L1 must
8
+ // outrank the syndicated copy — and, per F7/WS8, the syndicated copy must
9
+ // not source a sibling's identity AT ALL: on two measured estates roughly
10
+ // half of all systems carried the platform's name and description because
11
+ // the fallback fired for every sibling that authored no own L1. A sibling
12
+ // with no own L1 resolves to "" — the caller derives the name from the
13
+ // manifest and emits a coverage finding (the M-ADR 0133 §4.2c rule, applied
14
+ // to columns instead of rows).
15
+ //
16
+ // This module is shared so the L1-reading builders cannot keep divergent
17
+ // copies. They drifted before: the c4-systems L1-priority fix (sdd-cli
18
+ // 0.30.1) reversed only c4-systems' private copy; persona-c4system kept the
19
+ // old syndicated-first order, so `persona_c4system` matched the platform's
20
+ // personas instead of the sibling's. c4-external-systems UNIONS every
21
+ // existing L1 file rather than picking one, so candidate order is irrelevant
22
+ // there; it shares these paths only to avoid a divergent copy.
23
+ // See sdd-method-gov/coordination/persona-c4system-l1-priority-bug-coordination.md.
24
+ import { readdir, stat } from "node:fs/promises";
25
+ import { join } from "node:path";
26
+ /** Own-L1 candidates usable by ANY SDD, in priority order. */
27
+ export const OWN_L1_CANDIDATES = [
28
+ "docs/c4-l1.md", // sibling's own L1 — highest priority
29
+ "docs/application/c4-l1.md",
30
+ "docs/diagrams/c4-l1-context.mermaid.md",
31
+ ];
32
+ /**
33
+ * The syndicated platform L1. For the platform-root SDD this file IS its own
34
+ * L1 (lowest-priority own candidate). For every other SDD it may not source
35
+ * identity (M-ADR 0189 §4.5).
36
+ */
37
+ export const SYNDICATED_PLATFORM_L1 = "docs/platform/diagrams/c4-l1-context.mermaid.md";
38
+ /**
39
+ * Full candidate list, retained for union-style consumers
40
+ * (c4-external-systems) where order and ownership are irrelevant.
41
+ */
42
+ export const C4_L1_CANDIDATES = [
43
+ ...OWN_L1_CANDIDATES,
44
+ SYNDICATED_PLATFORM_L1,
45
+ ];
46
+ const DOMAIN_L1_REL = ["architecture", "diagrams", "c4-l1-context.mermaid.md"];
47
+ /**
48
+ * Domain-scoped L1s: `docs/domains/<slug>/architecture/diagrams/
49
+ * c4-l1-context.mermaid.md` — the natural home for a promoted subsystem with
50
+ * one eponymous domain (M-ADR 0189 §4.5). Returned as repo-relative paths,
51
+ * sorted by domain slug.
52
+ */
53
+ export async function findDomainScopedL1s(sddPath) {
54
+ const domainsDir = join(sddPath, "docs", "domains");
55
+ let entries;
56
+ try {
57
+ entries = (await readdir(domainsDir, { withFileTypes: true }))
58
+ .filter((e) => e.isDirectory())
59
+ .map((e) => e.name)
60
+ .sort((a, b) => a.localeCompare(b));
61
+ }
62
+ catch (err) {
63
+ if (err.code === "ENOENT")
64
+ return [];
65
+ throw err;
66
+ }
67
+ const out = [];
68
+ for (const name of entries) {
69
+ const rel = join("docs", "domains", name, ...DOMAIN_L1_REL);
70
+ if (await isFile(join(sddPath, rel)))
71
+ out.push(rel);
72
+ }
73
+ return out;
74
+ }
75
+ /**
76
+ * Resolve the L1 diagram that may source THIS SDD's identity (name,
77
+ * description, personas): own candidates in priority order, then a unique
78
+ * domain-scoped L1, then — for the platform root only — the syndicated
79
+ * platform path (which is its own L1).
80
+ */
81
+ export async function resolveIdentityL1(sddPath, isPlatformRoot) {
82
+ for (const rel of OWN_L1_CANDIDATES) {
83
+ if (await isFile(join(sddPath, rel))) {
84
+ return { path: rel, ambiguousDomainL1Count: 0 };
85
+ }
86
+ }
87
+ const domainL1s = await findDomainScopedL1s(sddPath);
88
+ if (domainL1s.length === 1) {
89
+ return { path: domainL1s[0], ambiguousDomainL1Count: 0 };
90
+ }
91
+ const ambiguous = domainL1s.length > 1 ? domainL1s.length : 0;
92
+ if (isPlatformRoot && (await isFile(join(sddPath, SYNDICATED_PLATFORM_L1)))) {
93
+ return { path: SYNDICATED_PLATFORM_L1, ambiguousDomainL1Count: ambiguous };
94
+ }
95
+ return { path: "", ambiguousDomainL1Count: ambiguous };
96
+ }
97
+ async function isFile(path) {
98
+ try {
99
+ return (await stat(path)).isFile();
100
+ }
101
+ catch (err) {
102
+ if (err.code === "ENOENT")
103
+ return false;
104
+ throw err;
105
+ }
106
+ }
107
+ //# sourceMappingURL=c4-l1-candidates.js.map
@@ -0,0 +1,98 @@
1
+ // C4 L1 systems discoverer — emits at most one `c4_systems.csv` row.
2
+ //
3
+ // Per ADR 0134's federation model, each SDD emits its OWN c4_systems
4
+ // row when its manifest category is architectural. Categories map to
5
+ // the c4_system_kind enum (canonical-schema.yaml):
6
+ // platform-root → platform
7
+ // platform-subsystem → subsystem (M-ADR 0170 §4.2 promotion; schema 2.28.0)
8
+ // application-product → application
9
+ // integration-provider → integration
10
+ // Non-architectural categories (integration-umbrella, support-repo) emit
11
+ // zero rows — with a coverage finding, never silently (M-ADR 0133 §4.2c).
12
+ //
13
+ // The id is `{sddId}-system` to match the products row's product_id FK.
14
+ // Description is parsed from the L1 mermaid `System(...)` line when an
15
+ // identity-sourcing L1 exists (M-ADR 0189 §4.5 — own L1, or a unique
16
+ // domain-scoped L1, or the syndicated platform path for the platform root
17
+ // only), otherwise left empty (visible coverage gap) with the name derived
18
+ // from the manifest.
19
+ import { readFile } from "node:fs/promises";
20
+ import { join } from "node:path";
21
+ import { resolveIdentityL1 } from "./c4-l1-candidates.js";
22
+ const KIND_BY_CATEGORY = {
23
+ "platform-root": "platform",
24
+ "platform-subsystem": "subsystem",
25
+ "application-product": "application",
26
+ "integration-provider": "integration",
27
+ };
28
+ // Match `System(id, "name", "description")` — the L1 root-system entry.
29
+ // Permissive about whitespace and the optional description argument.
30
+ const SYSTEM_RE = /^System\s*\(\s*\w+\s*,\s*"([^"]+)"\s*(?:,\s*"([^"]*)")?\s*\)/;
31
+ export async function buildC4Systems(inputs) {
32
+ const kind = KIND_BY_CATEGORY[inputs.manifest.category];
33
+ if (kind === undefined) {
34
+ // Non-architectural (or unmapped) — no system row, but never silently:
35
+ // a builder returning [] without a finding is indistinguishable from
36
+ // one that found nothing to return (M-ADR 0133 §4.2c no-silent-drop).
37
+ inputs.findings.add("coverage", `c4_systems: category '${inputs.manifest.category}' has no system kind; no row emitted`);
38
+ return [];
39
+ }
40
+ const { diagramPath, name, description, ambiguousDomainL1Count } = await findL1(inputs.sddPath, inputs.manifest.category === "platform-root");
41
+ if (ambiguousDomainL1Count > 0) {
42
+ inputs.findings.add("convention", `c4_systems: ${ambiguousDomainL1Count} domain-scoped L1 diagrams found for ${inputs.manifest.sddId} — ambiguous, none used as the SDD's L1`);
43
+ }
44
+ if (diagramPath === "") {
45
+ inputs.findings.add("coverage", `c4_systems: no L1 diagram found for ${inputs.manifest.sddId} — diagram_path empty`);
46
+ }
47
+ const sddId = inputs.manifest.sddId;
48
+ return [
49
+ {
50
+ id: inputs.manifest.c4SystemId ?? `${sddId}-system`,
51
+ name: name !== "" ? name : titleCase(sddId),
52
+ product_id: sddId,
53
+ kind,
54
+ diagram_path: diagramPath,
55
+ description,
56
+ source_sdd_id: "",
57
+ },
58
+ ];
59
+ }
60
+ async function findL1(sddPath, isPlatformRoot) {
61
+ const resolved = await resolveIdentityL1(sddPath, isPlatformRoot);
62
+ if (resolved.path === "") {
63
+ return {
64
+ diagramPath: "",
65
+ name: "",
66
+ description: "",
67
+ ambiguousDomainL1Count: resolved.ambiguousDomainL1Count,
68
+ };
69
+ }
70
+ const content = await readFile(join(sddPath, resolved.path), "utf-8");
71
+ const parsed = parseSystemLine(content);
72
+ return {
73
+ diagramPath: resolved.path,
74
+ name: parsed?.name ?? "",
75
+ description: parsed?.description ?? "",
76
+ ambiguousDomainL1Count: resolved.ambiguousDomainL1Count,
77
+ };
78
+ }
79
+ function parseSystemLine(content) {
80
+ for (const raw of content.split(/\r?\n/)) {
81
+ const line = raw.trim();
82
+ if (line.startsWith("System_Ext") || line.startsWith("System_Boundary")) {
83
+ continue;
84
+ }
85
+ const m = SYSTEM_RE.exec(line);
86
+ if (m) {
87
+ return { name: m[1] ?? "", description: m[2] ?? "" };
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ function titleCase(slug) {
93
+ return slug
94
+ .split("-")
95
+ .map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
96
+ .join(" ");
97
+ }
98
+ //# sourceMappingURL=c4-systems.js.map