@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,232 @@
1
+ // Services discoverer — emits `services.csv` rows derived 1:1 from
2
+ // repository rows whose service_type is a runtime kind, UNLESS the
3
+ // repository declares a `services:` block (M-ADR 0197), in which case the
4
+ // declared list REPLACES the single-service default for that repository
5
+ // only. Absent block: today's exact behaviour, unchanged. Present block
6
+ // (including an empty list): authoritative — every declared entry's
7
+ // `primary_repo_id` is set to the declaring repository's id, and the
8
+ // repository's own default-by-silence id disappears unless an entry
9
+ // re-declares it (continuity is the adopter's responsibility, per M-ADR
10
+ // 0197 §4.1 — a dangling reference to it resolves to an orphan finding
11
+ // elsewhere, not a crash here).
12
+ //
13
+ // Per ADR 0134 + Q7.1 resolution, runtime kinds are
14
+ // {api_service, worker, gateway, frontend, mobile} — read from the
15
+ // canonical schema's enums.service_type block where
16
+ // `emits_service_row: true`. The runtime-kind gate applies only to the
17
+ // DEFAULT projection; a declared entry is emitted regardless of the
18
+ // declaring repository's own service_type, because declaring it is
19
+ // itself the adopter's assertion that it is a service.
20
+ //
21
+ // This is a derived catalogue: services are 1:1 with their primary
22
+ // repository for the contextual adoption pattern, unless declared
23
+ // otherwise. Adopters whose services span multiple repos may extend this
24
+ // builder later.
25
+ //
26
+ // Per-service metadata (runtime_type, language, status, confidence,
27
+ // description) is read — faithfully, not inferred — from each service's
28
+ // `repos/<id>/docs/service-architecture.md` via the shared
29
+ // service-architecture-parser module.
30
+ //
31
+ // The `description` column has two authoring sources, honoured per the
32
+ // forward/reverse parity principle (ADR 0157, extended to node columns by
33
+ // ADR 0163):
34
+ // - forward: `orchestration/repo-config/<repo>.yaml` `description:` —
35
+ // the deliberate spec, authored by forward-method adopters.
36
+ // - reverse: the arch doc's `## Purpose` lead paragraph.
37
+ // When both are present for the same service the forward repo-config
38
+ // description wins (matching the ADR 0156 / 0162 merge convention) and a
39
+ // `divergence` finding records the conflict. In practice an adopter has
40
+ // only one of the two, so this is usually a plain coalesce.
41
+ //
42
+ // The four reverse-only columns (runtime_type, language, status,
43
+ // confidence) have no forward authoring source; forward-method adopters
44
+ // legitimately leave them empty (they declare service_type + dependencies
45
+ // instead). See catalogue-metamodel.md §Service.
46
+ import { runtimeServiceTypes, } from "../schema-loader.js";
47
+ import { EMPTY_METADATA, indexServiceArchitectures, readServiceMetadata, } from "../service-architecture-parser.js";
48
+ /**
49
+ * Validate one repo's raw `services:` value. Returns `null` when the block
50
+ * is absent, or present but not shaped like a list at all — both fall back
51
+ * to the default single-service projection, because a top-level shape
52
+ * violation reads as "not a valid declaration", not "declares zero
53
+ * services". Returns an array (possibly empty) when the block IS a list:
54
+ * that is a declaration, and it is authoritative even if every entry in it
55
+ * turned out invalid — a repository can legitimately declare it builds no
56
+ * cataloguable service.
57
+ *
58
+ * Per-entry problems (not an object, missing `id`, a duplicate `id` within
59
+ * this one block) drop only that entry, with an orphan/divergence finding
60
+ * naming the file and index — never the whole block, never the repository.
61
+ */
62
+ function parseDeclaredServices(raw, relPath, findings) {
63
+ if (raw === undefined)
64
+ return null;
65
+ if (!Array.isArray(raw)) {
66
+ findings.add("convention",
67
+ // Names the remedy, and specifically the `services: []` form: a BARE
68
+ // `services:` parses as null, which is not a list, so it lands here —
69
+ // and it is two characters away from the declaration an author meant.
70
+ `${relPath}: services: must be a list — block dropped, default single-service projection kept; write it as a list of entries (\`services:\` then \` - id: ...\`), or as \`services: []\` to declare that this repo deliberately emits no service row (M-ADR 0197). A bare \`services:\` with nothing after it is neither.`);
71
+ return null;
72
+ }
73
+ const seen = new Set();
74
+ const out = [];
75
+ for (let i = 0; i < raw.length; i++) {
76
+ const entry = raw[i];
77
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
78
+ findings.add("orphan", `${relPath}: services[${i}] is not an object — entry dropped`);
79
+ continue;
80
+ }
81
+ const o = entry;
82
+ const id = typeof o["id"] === "string" ? o["id"].trim() : "";
83
+ if (id === "") {
84
+ findings.add("orphan", `${relPath}: services[${i}] missing required "id" — entry dropped`);
85
+ continue;
86
+ }
87
+ if (seen.has(id)) {
88
+ findings.add("divergence", `${relPath}: services: declares "${id}" more than once — first occurrence kept`);
89
+ continue;
90
+ }
91
+ seen.add(id);
92
+ out.push({
93
+ id,
94
+ ...(typeof o["domain"] === "string" ? { domain: o["domain"] } : {}),
95
+ ...(typeof o["service_type"] === "string"
96
+ ? { service_type: o["service_type"] }
97
+ : {}),
98
+ ...(typeof o["description"] === "string"
99
+ ? { description: o["description"] }
100
+ : {}),
101
+ });
102
+ }
103
+ return out;
104
+ }
105
+ export async function buildServices(inputs) {
106
+ const runtime = runtimeServiceTypes(inputs.schema);
107
+ const validServiceTypes = canonicalServiceTypeValues(inputs.schema);
108
+ const declaredNonRepoIds = new Set();
109
+ // Index every service-architecture.md under repos/ by its repo slug (the
110
+ // directory above docs/). The layout is either repos/<repo>/docs/... or
111
+ // repos/<group>/<repo>/docs/..., so a recursive walk is required.
112
+ // Services whose architecture doc lives in a federated member SDD (rather
113
+ // than this SDD's own repos/) resolve to no entry here — their metadata is
114
+ // an aggregation-time concern (ADR 0163 P2), not a local build gap.
115
+ const archIndex = await indexServiceArchitectures(inputs.sddPath);
116
+ const repoDescriptions = inputs.repoDescriptions ?? new Map();
117
+ const declaredByRepo = inputs.declaredServicesByRepo ?? new Map();
118
+ const rows = [];
119
+ // Build-wide id set, so a declared entry colliding with an id already
120
+ // emitted (by any earlier repo, declared or default) is visible here
121
+ // rather than only surfacing later as a generic cross-catalogue PK
122
+ // conflict with no repo context. Genuinely differing payloads on the same
123
+ // id still hard-fail the build via dedupeKeyedTables (per M-ADR 0197's
124
+ // consumer sweep) — this is an earlier, better-attributed warning, not a
125
+ // replacement for that guard.
126
+ const emittedIds = new Map(); // id -> repoId that emitted it
127
+ for (const repo of inputs.repositories) {
128
+ const repoId = String(repo["id"] ?? "");
129
+ if (repoId === "")
130
+ continue;
131
+ const repoDocPath = String(repo["doc_path"] ?? "");
132
+ const repoDomainId = String(repo["domain_id"] ?? "");
133
+ const repoComponentType = String(repo["component_type"] ?? "");
134
+ const declared = parseDeclaredServices(declaredByRepo.get(repoId), repoDocPath || repoId, inputs.findings);
135
+ if (declared === null) {
136
+ // No declaration — the default single-service projection, unchanged.
137
+ if (!runtime.has(repoComponentType))
138
+ continue;
139
+ await emitOne(rows, emittedIds, archIndex, inputs, {
140
+ id: repoId,
141
+ name: repoId,
142
+ domainId: repoDomainId,
143
+ primaryRepoId: repoId,
144
+ componentType: repoComponentType,
145
+ forwardDescription: repoDescriptions.get(repoId) ?? "",
146
+ archKey: repoId,
147
+ });
148
+ continue;
149
+ }
150
+ // Declared — authoritative for this repository, regardless of the
151
+ // repository's own service_type. Zero valid entries is a legitimate
152
+ // outcome (a repository explicitly declaring it builds no service).
153
+ for (const entry of declared) {
154
+ const componentType = entry.service_type ?? repoComponentType;
155
+ if (componentType !== "" && !validServiceTypes.has(componentType)) {
156
+ inputs.findings.add("convention", `${repoDocPath || repoId}: services declares "${entry.id}" with service_type "${componentType}" not in canonical enum`);
157
+ }
158
+ await emitOne(rows, emittedIds, archIndex, inputs, {
159
+ id: entry.id,
160
+ name: entry.id,
161
+ domainId: entry.domain ?? repoDomainId,
162
+ primaryRepoId: repoId,
163
+ componentType,
164
+ forwardDescription: entry.description ?? "",
165
+ archKey: entry.id,
166
+ });
167
+ // M-ADR 0197 §4.6 is not implemented on the catalogue side yet — the
168
+ // forward event/contract builders still attribute only via the
169
+ // repository's own id (`cfg.repoId`), never via a declared entry.
170
+ // A declared entry whose id differs from the repository's own can
171
+ // therefore never receive event/contract rows today, by construction,
172
+ // regardless of any future primary-selection rule. This is the
173
+ // vacuous-signal candidate set (§4.7's fourth failure mode) — tracked
174
+ // here, where the declaration is parsed, rather than re-derived
175
+ // downstream.
176
+ if (entry.id !== repoId)
177
+ declaredNonRepoIds.add(entry.id);
178
+ }
179
+ }
180
+ return { rows, declaredNonRepoIds };
181
+ }
182
+ function canonicalServiceTypeValues(schema) {
183
+ const enumDef = schema.enums["service_type"];
184
+ if (!enumDef)
185
+ return new Set(runtimeServiceTypes(schema));
186
+ return new Set(enumDef.values.map((v) => v.value));
187
+ }
188
+ async function emitOne(rows, emittedIds, archIndex, inputs, args) {
189
+ const priorRepo = emittedIds.get(args.id);
190
+ if (priorRepo !== undefined && priorRepo !== args.primaryRepoId) {
191
+ inputs.findings.add("divergence", `services: id "${args.id}" declared by repo "${args.primaryRepoId}" collides with one already emitted by repo "${priorRepo}"`);
192
+ }
193
+ emittedIds.set(args.id, args.primaryRepoId);
194
+ const archPath = archIndex.get(args.archKey);
195
+ const meta = archPath ? await readServiceMetadata(archPath) : EMPTY_METADATA;
196
+ rows.push({
197
+ id: args.id,
198
+ name: args.name,
199
+ product_id: inputs.productId,
200
+ domain_id: args.domainId,
201
+ primary_repo_id: args.primaryRepoId,
202
+ runtime_type: meta.runtime_type,
203
+ language: meta.language,
204
+ component_type: args.componentType,
205
+ status: meta.status,
206
+ confidence: meta.confidence,
207
+ description: coalesceDescription(args.id, args.forwardDescription, meta.description, inputs.findings),
208
+ source_sdd_id: "",
209
+ });
210
+ }
211
+ /**
212
+ * Coalesce the forward (repo-config) and reverse (## Purpose) description
213
+ * sources. Forward wins when both are present; a `divergence` finding
214
+ * records the conflict so authors can reconcile. Whitespace-only values
215
+ * are treated as empty. Single-quotes/newlines normalised away for the
216
+ * comparison so cosmetic reformatting doesn't trip the finding.
217
+ */
218
+ function coalesceDescription(serviceId, forward, reverse, findings) {
219
+ const f = forward.trim();
220
+ const r = reverse.trim();
221
+ if (f === "")
222
+ return r;
223
+ if (r !== "" && normalise(f) !== normalise(r)) {
224
+ findings.add("divergence", `services: "${serviceId}" description differs between repo-config (forward) ` +
225
+ `and service-architecture.md ## Purpose (reverse) — repo-config kept`);
226
+ }
227
+ return f;
228
+ }
229
+ function normalise(s) {
230
+ return s.replace(/\s+/g, " ").trim().toLowerCase();
231
+ }
232
+ //# sourceMappingURL=services.js.map
@@ -0,0 +1,115 @@
1
+ // system_external_dependency edge builder — emits one row per
2
+ // `Rel(...)` line on an L1/L2 diagram that connects the SDD's own system
3
+ // (or one of its containers) to an external system (per ADR 0155).
4
+ //
5
+ // Resolution: the c4-external-systems builder supplies `externalIdByEntryId`
6
+ // (diagram entry-id → ext-... row id). For each `Rel(a, b, "label")` where
7
+ // exactly one endpoint is a known external entry-id, we emit an edge from
8
+ // the SDD's c4_systems row to that external system. Container-level external
9
+ // dependencies are intentionally collapsed to the system level — ADR 0155
10
+ // §4.2 keeps the edge system-scoped.
11
+ import { readFile } from "node:fs/promises";
12
+ import { collectC4DiagramFiles } from "./c4-external-systems.js";
13
+ import { mergeEdgeLabel } from "./edge-label-merge.js";
14
+ // Any C4-PlantUML relationship macro: Rel, Rel_U/D/L/R, BiRel, Rel_Back, etc.
15
+ // Capture the first two positional args (from, to) and an optional label.
16
+ const REL_RE = /^(?:Bi)?Rel(?:_[A-Za-z]+)?\s*\(\s*(\w+)\s*,\s*(\w+)\s*(?:,\s*"([^"]*)")?/;
17
+ // Diagram files come from the shared collector (F11c) — see
18
+ // c4-external-systems.ts / c4-l1-candidates.ts. This builder previously
19
+ // kept a private, pre-0.30.1-ordered copy missing the domain-scoped L1
20
+ // path, so Rel(...) lines there were never harvested.
21
+ export async function buildSystemExternalDependency(inputs) {
22
+ const systemId = inputs.manifest.c4SystemId ?? `${inputs.manifest.sddId}-system`;
23
+ if (!inputs.knownSystemIds.has(systemId)) {
24
+ // Non-architectural SDD — no c4_systems FK target. Skip silently.
25
+ return [];
26
+ }
27
+ if (inputs.externalIdByEntryId.size === 0)
28
+ return [];
29
+ const files = await collectC4DiagramFiles(inputs.sddPath);
30
+ const rows = [];
31
+ const seen = new Set();
32
+ const rowsById = new Map();
33
+ // F14: a Rel in the syndicated platform L1 is a statement about the
34
+ // PLATFORM. For any other SDD those lines minted the platform's
35
+ // dependencies (and even its persona interactions) as the sibling's own
36
+ // edges — measured at 28% of one estate's dependency graph and 35/35
37
+ // polluted interaction labels on another. §4.5's rule, applied to edges:
38
+ // the syndicated copy sources edges for the platform root only. The ROW
39
+ // harvest keeps it (endpoint resolution needs its declarations).
40
+ const isRoot = inputs.manifest.category === "platform-root";
41
+ const PERSON_DECL_RE = /Person(?:_Ext)?\s*\(\s*(\w+)\s*,/;
42
+ for (const { abs, syndicated } of files) {
43
+ if (syndicated && !isRoot)
44
+ continue;
45
+ // (F17) the non-external Rel endpoint may be a local container — carry
46
+ // its id so adjudication can rewrite this edge onto container_dependency
47
+ // instead of destroying it when the external endpoint proves internal.
48
+ const content = await readFile(abs, "utf-8");
49
+ // F30: a Person-sourced Rel must never enter this catalogue — its
50
+ // semantics are runtime calls between software, and attributing a
51
+ // human's Rel to the system erases the person-ness (which adjudication
52
+ // then launders into system_system_dependency). The persona-container
53
+ // builder owns the person tier's exits and announces every skip.
54
+ const personEntries = new Set();
55
+ for (const raw of content.split(/\r?\n/)) {
56
+ const pm = PERSON_DECL_RE.exec(raw.trim());
57
+ if (pm)
58
+ personEntries.add(pm[1] ?? "");
59
+ }
60
+ for (const rel of parseRels(content)) {
61
+ if (personEntries.has(rel.from) || personEntries.has(rel.to))
62
+ continue;
63
+ // F28: classification is CONTAINER-FIRST, uniformly with the
64
+ // container-dependency builder (F20 / ledger L6). An entry-id that
65
+ // resolves to a local container IS a container, whatever a sibling
66
+ // diagram called it — the old external-first read here meant a
67
+ // dual-use SOURCE made the pair look external↔external to this
68
+ // builder while looking container→external to its sibling, and the
69
+ // edge fell between them: claimed by neither, reported by nothing.
70
+ // L6's fix was applied to one builder and not swept into this one.
71
+ const fromIsContainer = inputs.containerIdByEntryId?.has(rel.from) ?? false;
72
+ const toIsContainer = inputs.containerIdByEntryId?.has(rel.to) ?? false;
73
+ const fromExt = fromIsContainer
74
+ ? undefined
75
+ : inputs.externalIdByEntryId.get(rel.from);
76
+ const toExt = toIsContainer
77
+ ? undefined
78
+ : inputs.externalIdByEntryId.get(rel.to);
79
+ // Exactly one endpoint external — otherwise it's internal↔internal or
80
+ // external↔external (neither is a system→external dependency).
81
+ if ((fromExt === undefined) === (toExt === undefined))
82
+ continue;
83
+ const externalSystemId = (fromExt ?? toExt);
84
+ const localEntry = fromExt === undefined ? rel.from : rel.to;
85
+ const fromContainerId = inputs.containerIdByEntryId?.get(localEntry) ?? "";
86
+ const id = `${systemId}__${externalSystemId}`;
87
+ const existing = rowsById.get(id);
88
+ if (existing !== undefined) {
89
+ mergeEdgeLabel(existing, rel.label, "system_external_dependency", inputs.findings);
90
+ continue;
91
+ }
92
+ seen.add(id);
93
+ rowsById.set(id, {
94
+ id,
95
+ c4_system_id: systemId,
96
+ external_system_id: externalSystemId,
97
+ interaction: rel.label,
98
+ from_container_id: fromContainerId,
99
+ });
100
+ rows.push(rowsById.get(id));
101
+ }
102
+ }
103
+ rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
104
+ return rows;
105
+ }
106
+ function parseRels(content) {
107
+ const out = [];
108
+ for (const raw of content.split(/\r?\n/)) {
109
+ const m = REL_RE.exec(raw.trim());
110
+ if (m)
111
+ out.push({ from: m[1] ?? "", to: m[2] ?? "", label: m[3] ?? "" });
112
+ }
113
+ return out;
114
+ }
115
+ //# sourceMappingURL=system-external-dependency.js.map
@@ -0,0 +1,253 @@
1
+ // User journeys discoverer — emits `user_journeys.csv` rows from
2
+ // `journeys/*.md` at the SDD root.
3
+ //
4
+ // Source convention (per docs/method/methods/user-journey-template.md):
5
+ // journeys/<slug>.md
6
+ // ---
7
+ // # User Journey: {Name}
8
+ // **App:** {app-name}
9
+ // **Feature:** [{feature-name}]({path/to/feature-spec.md})
10
+ // **Primary Persona:** {persona-name}
11
+ // **Supporting Personas:** {comma-separated names} (optional)
12
+ // ## Journey Goal
13
+ // ...
14
+ // ## Trigger
15
+ // ...
16
+ //
17
+ // Profile gating: journeys are an application-product profile concept.
18
+ // id = `{product_id}-{slug}`, slug from filename stem.
19
+ import { readdir, readFile } from "node:fs/promises";
20
+ import { basename, join, relative } from "node:path";
21
+ const JOURNEYS_DIR_REL = "journeys";
22
+ export async function buildUserJourneys(inputs) {
23
+ const idByPath = new Map();
24
+ const idBySlug = new Map();
25
+ const featurePathsById = new Map();
26
+ const relatedFeatureSlugsById = new Map();
27
+ const primaryPersonaById = new Map();
28
+ const supportingPersonasById = new Map();
29
+ if (inputs.profile !== "application-product") {
30
+ return {
31
+ rows: [],
32
+ idByPath,
33
+ idBySlug,
34
+ featurePathsById,
35
+ relatedFeatureSlugsById,
36
+ primaryPersonaById,
37
+ supportingPersonasById,
38
+ };
39
+ }
40
+ const dir = join(inputs.sddPath, JOURNEYS_DIR_REL);
41
+ let entries;
42
+ try {
43
+ entries = await readdir(dir, { withFileTypes: true });
44
+ }
45
+ catch (err) {
46
+ if (err.code !== "ENOENT")
47
+ throw err;
48
+ inputs.findings.add("coverage", `${JOURNEYS_DIR_REL}/ not found — emitting empty user_journeys.csv`);
49
+ return {
50
+ rows: [],
51
+ idByPath,
52
+ idBySlug,
53
+ featurePathsById,
54
+ relatedFeatureSlugsById,
55
+ primaryPersonaById,
56
+ supportingPersonasById,
57
+ };
58
+ }
59
+ const files = entries
60
+ .filter((e) => e.isFile() && e.name.endsWith(".md") && !isTemplate(e.name))
61
+ .map((e) => e.name)
62
+ .sort();
63
+ const rows = [];
64
+ for (const filename of files) {
65
+ const slug = basename(filename).replace(/\.md$/i, "");
66
+ const id = `${inputs.productId}-${slug}`;
67
+ const absPath = join(dir, filename);
68
+ const relPath = relative(inputs.sddPath, absPath);
69
+ const text = await readFile(absPath, "utf-8");
70
+ const name = extractName(text) || titleCase(slug);
71
+ const primaryPersona = extractField(text, "Primary Persona");
72
+ const supportingPersonas = extractList(text, "Supporting Personas");
73
+ const trigger = extractSectionParagraph(text, "Trigger");
74
+ const goal = extractSectionParagraph(text, "Journey Goal");
75
+ const featurePaths = extractFeaturePaths(text);
76
+ const relatedFeatureSlugs = extractRelatedFeatures(text);
77
+ rows.push({
78
+ id,
79
+ name,
80
+ product_id: inputs.productId,
81
+ primary_persona: primaryPersona,
82
+ supporting_personas: supportingPersonas.join(", "),
83
+ trigger,
84
+ goal,
85
+ source_sdd_id: "",
86
+ });
87
+ idByPath.set(relPath, id);
88
+ idBySlug.set(slug, id);
89
+ featurePathsById.set(id, featurePaths);
90
+ relatedFeatureSlugsById.set(id, relatedFeatureSlugs);
91
+ primaryPersonaById.set(id, primaryPersona);
92
+ supportingPersonasById.set(id, supportingPersonas);
93
+ }
94
+ return {
95
+ rows,
96
+ idByPath,
97
+ idBySlug,
98
+ featurePathsById,
99
+ relatedFeatureSlugsById,
100
+ primaryPersonaById,
101
+ supportingPersonasById,
102
+ };
103
+ }
104
+ function isTemplate(name) {
105
+ return /[{}]/.test(name) || name.toLowerCase().endsWith("-template.md");
106
+ }
107
+ function extractName(text) {
108
+ for (const raw of text.split(/\r?\n/)) {
109
+ const line = raw.trim();
110
+ if (line === "")
111
+ continue;
112
+ const m = /^#\s+(.+)$/.exec(line);
113
+ if (!m)
114
+ return "";
115
+ const stripped = (m[1] ?? "").trim();
116
+ const journeyPrefix = /^User Journey:\s*(.+)$/i.exec(stripped);
117
+ return journeyPrefix ? (journeyPrefix[1] ?? "").trim() : stripped;
118
+ }
119
+ return "";
120
+ }
121
+ function extractField(text, field) {
122
+ const re = new RegExp(`^\\s*[*\\-]?\\s*\\*\\*${escapeRegex(field)}:\\*\\*\\s*(.*?)\\s*$`, "im");
123
+ const m = re.exec(text);
124
+ if (!m || m[1] === undefined)
125
+ return "";
126
+ return m[1].trim();
127
+ }
128
+ function extractList(text, field) {
129
+ const value = extractField(text, field);
130
+ if (value === "")
131
+ return [];
132
+ return value
133
+ .split(/[,;]/)
134
+ .map((s) => s.trim())
135
+ .filter((s) => s !== "");
136
+ }
137
+ function extractSectionParagraph(text, heading) {
138
+ const lines = text.split(/\r?\n/);
139
+ const headingRe = new RegExp(`^##\\s+${escapeRegex(heading)}\\b`, "i");
140
+ let i = 0;
141
+ while (i < lines.length) {
142
+ if (headingRe.test(lines[i] ?? ""))
143
+ break;
144
+ i++;
145
+ }
146
+ if (i === lines.length)
147
+ return "";
148
+ i++;
149
+ while (i < lines.length && (lines[i] ?? "").trim() === "")
150
+ i++;
151
+ const para = [];
152
+ while (i < lines.length) {
153
+ const line = lines[i] ?? "";
154
+ if (line.trim() === "")
155
+ break;
156
+ if (line.startsWith("#") || line.startsWith("---"))
157
+ break;
158
+ para.push(line.trim());
159
+ i++;
160
+ }
161
+ return para.join(" ").trim().slice(0, 500);
162
+ }
163
+ /**
164
+ * Pull out feature spec paths from the `**Feature:** [Name](path)` field
165
+ * and from any `[name](path)` link inside `## Related Features` bullets.
166
+ * Returns relative paths as written.
167
+ */
168
+ function extractFeaturePaths(text) {
169
+ const out = [];
170
+ // Top-of-file `**Feature:** [Name](path)` link.
171
+ const featureLineRe = /\*\*Feature:\*\*\s*\[[^\]]+\]\(([^)]+)\)/i;
172
+ const m = featureLineRe.exec(text);
173
+ if (m && m[1])
174
+ out.push(m[1].trim());
175
+ // Bullets in `## Related Features` referencing files.
176
+ const lines = text.split(/\r?\n/);
177
+ let i = 0;
178
+ while (i < lines.length) {
179
+ if (/^##\s+related\s+features\b/i.test(lines[i] ?? ""))
180
+ break;
181
+ i++;
182
+ }
183
+ if (i < lines.length) {
184
+ i++;
185
+ while (i < lines.length) {
186
+ const line = lines[i] ?? "";
187
+ if (line.startsWith("#") || line.startsWith("---"))
188
+ break;
189
+ const linkMatch = /^[-*]\s+\[[^\]]+\]\(([^)]+)\)/.exec(line.trim());
190
+ if (linkMatch && linkMatch[1])
191
+ out.push(linkMatch[1].trim());
192
+ i++;
193
+ }
194
+ }
195
+ return out;
196
+ }
197
+ function extractRelatedFeatures(text) {
198
+ const lines = text.split(/\r?\n/);
199
+ let i = 0;
200
+ while (i < lines.length) {
201
+ if (/^##\s+related\s+features\b/i.test(lines[i] ?? ""))
202
+ break;
203
+ i++;
204
+ }
205
+ if (i === lines.length)
206
+ return [];
207
+ i++;
208
+ const out = [];
209
+ while (i < lines.length) {
210
+ const line = lines[i] ?? "";
211
+ if (line.startsWith("#") || line.startsWith("---"))
212
+ break;
213
+ const trimmed = line.trim();
214
+ // Markdown link bullet: `- [Name](path/<slug>/spec.md)` → derive slug from path.
215
+ const linkMatch = /^[-*]\s+\[[^\]]+\]\(([^)]+)\)/.exec(trimmed);
216
+ if (linkMatch && linkMatch[1]) {
217
+ const slug = slugFromPath(linkMatch[1]);
218
+ if (slug !== "")
219
+ out.push(slug);
220
+ }
221
+ else {
222
+ // Bare-slug bullet: `- feature-name` or `- \`feature-name\``.
223
+ const slugMatch = /^[-*]\s+`?([a-z0-9][a-z0-9-]*)`?(?:\s|$)/i.exec(trimmed);
224
+ if (slugMatch && slugMatch[1])
225
+ out.push(slugMatch[1].toLowerCase());
226
+ }
227
+ i++;
228
+ }
229
+ return out;
230
+ }
231
+ function slugFromPath(path) {
232
+ // Take the last meaningful path segment that looks like a slug. Common
233
+ // shapes: `features/<slug>/spec.md`, `features/<slug>.md`.
234
+ const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
235
+ const parts = cleaned.split("/");
236
+ for (let j = parts.length - 1; j >= 0; j--) {
237
+ const part = (parts[j] ?? "").replace(/\.md$/i, "");
238
+ if (/^[a-z0-9][a-z0-9-]*$/.test(part) && part !== "spec") {
239
+ return part;
240
+ }
241
+ }
242
+ return "";
243
+ }
244
+ function escapeRegex(s) {
245
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
246
+ }
247
+ function titleCase(slug) {
248
+ return slug
249
+ .split("-")
250
+ .map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
251
+ .join(" ");
252
+ }
253
+ //# sourceMappingURL=user-journeys.js.map