@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,378 @@
1
+ // glossary_xref + entity_glossary edge builders — emit cross-domain glossary
2
+ // relationships from `docs/platform/reference/glossary-alignment-manifest.yaml`.
3
+ //
4
+ // Per ADR 0158 (Glossary Cross-Reference Multi-Source Ingestion, schema 2.12.0),
5
+ // the builder ingests from FIVE walkers:
6
+ //
7
+ // 1. Forward declared pair-form (existing).
8
+ // aliases: [{source, target}]
9
+ // conflicts: [{source, target}]
10
+ // Emits glossary_xref rows with relation ∈ {alias-of, conflicts-with}.
11
+ //
12
+ // 2. Reverse-method cross_domain_terms (new).
13
+ // cross_domain_terms:
14
+ // - term: <name>
15
+ // owner: <domain>
16
+ // referenced_by: [<domain>, ...]
17
+ // One row per (referencer, owner) pair with relation=references.
18
+ //
19
+ // 3. Reverse-method multi-domain conflicts (new).
20
+ // conflicts:
21
+ // - term: <name>
22
+ // domains: [{domain, meaning}, ...]
23
+ // resolution: <text>
24
+ // Pair-wise expansion across domains[] with relation=conflicts-with.
25
+ // Disambiguated from #1 by per-entry field inspection.
26
+ //
27
+ // 4. Forward-method supersedes (new).
28
+ // models[].entities[] with status: superseded + superseded_by
29
+ // One row per supersedes mapping after resolving both endpoints'
30
+ // glossary_term against glossary_items.csv. Orphan finding when an
31
+ // endpoint doesn't resolve.
32
+ //
33
+ // 5. Forward-method entity-mapping (new, writes to entity_glossary).
34
+ // models[].entities[] with status ∈ {aligned, matched_direct} and
35
+ // non-empty glossary_term.
36
+ // One row per (domain, entity_name, glossary_term) tuple after dedup.
37
+ // Same-domain multi-diagram entries collapse to one row; cross-domain
38
+ // entries (e.g. platform-wide conceptual + party-domain detailed)
39
+ // emit one row per domain — legitimate cross-level cataloguing.
40
+ import { readFile } from "node:fs/promises";
41
+ import { join } from "node:path";
42
+ import { parse as parseYaml } from "yaml";
43
+ const MANIFEST_REL_PATH = join("docs", "platform", "reference", "glossary-alignment-manifest.yaml");
44
+ export async function buildGlossaryXref(inputs) {
45
+ // Profile gate per ADR 0158 §amendment: the manifest is platform-owned;
46
+ // sibling SDDs that syndicate it for documentation accessibility should
47
+ // not re-emit rows. Mirrors the entities-manifest profile gate.
48
+ if (inputs.profile !== "platform-profile") {
49
+ return { glossaryXref: [], entityGlossary: [] };
50
+ }
51
+ const path = join(inputs.sddPath, MANIFEST_REL_PATH);
52
+ let text;
53
+ try {
54
+ text = await readFile(path, "utf-8");
55
+ }
56
+ catch (err) {
57
+ const code = err.code;
58
+ if (code === "ENOENT")
59
+ return { glossaryXref: [], entityGlossary: [] };
60
+ throw err;
61
+ }
62
+ let raw;
63
+ try {
64
+ raw = parseYaml(text);
65
+ }
66
+ catch (err) {
67
+ inputs.findings.add("convention", `${MANIFEST_REL_PATH}: failed to parse YAML — ${err.message}`);
68
+ return { glossaryXref: [], entityGlossary: [] };
69
+ }
70
+ if (!raw || typeof raw !== "object")
71
+ return { glossaryXref: [], entityGlossary: [] };
72
+ const obj = raw;
73
+ const xrefRows = [];
74
+ const xrefSeen = new Set();
75
+ const entityRows = [];
76
+ const entitySeen = new Map();
77
+ const emitXref = (sourceId, targetId, relation) => {
78
+ if (sourceId === targetId)
79
+ return;
80
+ const id = `${sourceId}__${relation}__${targetId}`;
81
+ if (xrefSeen.has(id))
82
+ return;
83
+ xrefSeen.add(id);
84
+ xrefRows.push({ id, source_id: sourceId, target_id: targetId, relation });
85
+ };
86
+ // Walker 1 — forward declared pair-form aliases.
87
+ const aliasesRaw = obj["aliases"];
88
+ for (const pair of collectPairs(aliasesRaw)) {
89
+ const sourceId = resolveTerm(pair.source, inputs);
90
+ const targetId = resolveTerm(pair.target, inputs);
91
+ if (sourceId === undefined) {
92
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: alias-of source "${pair.source}" doesn't resolve to a known glossary item`);
93
+ continue;
94
+ }
95
+ if (targetId === undefined) {
96
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: alias-of target "${pair.target}" doesn't resolve to a known glossary item`);
97
+ continue;
98
+ }
99
+ emitXref(sourceId, targetId, "alias-of");
100
+ }
101
+ // Walkers 1+3 — `conflicts` key: disambiguated per-entry.
102
+ // Pair-form {source, target} → forward (alias-style conflicts-with).
103
+ // Multi-domain {term, domains[]} → reverse-method walker.
104
+ const conflictsRaw = obj["conflicts_with"] ?? obj["conflicts"];
105
+ if (Array.isArray(conflictsRaw)) {
106
+ for (const item of conflictsRaw) {
107
+ if (!item || typeof item !== "object")
108
+ continue;
109
+ const o = item;
110
+ const hasPairFields = "source" in o || "source_id" in o || "target" in o || "target_id" in o;
111
+ const hasReverseFields = "term" in o && "domains" in o && Array.isArray(o["domains"]);
112
+ if (hasPairFields && hasReverseFields) {
113
+ inputs.findings.add("divergence", `${MANIFEST_REL_PATH}: glossary-xref conflicts entry has mixed forward/reverse fields — skipped`);
114
+ continue;
115
+ }
116
+ if (hasPairFields) {
117
+ const source = String(o["source"] ?? o["source_id"] ?? "").trim();
118
+ const target = String(o["target"] ?? o["target_id"] ?? "").trim();
119
+ if (source === "" || target === "")
120
+ continue;
121
+ const sourceId = resolveTerm(source, inputs);
122
+ const targetId = resolveTerm(target, inputs);
123
+ if (sourceId === undefined) {
124
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: conflicts-with source "${source}" doesn't resolve to a known glossary item`);
125
+ continue;
126
+ }
127
+ if (targetId === undefined) {
128
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: conflicts-with target "${target}" doesn't resolve to a known glossary item`);
129
+ continue;
130
+ }
131
+ emitXref(sourceId, targetId, "conflicts-with");
132
+ }
133
+ else if (hasReverseFields) {
134
+ const term = String(o["term"] ?? "").trim();
135
+ const domains = o["domains"].map((d) => {
136
+ if (!d || typeof d !== "object")
137
+ return null;
138
+ return String(d["domain"] ?? "").trim();
139
+ }).filter((d) => d !== null && d !== "");
140
+ // Resolve each domain to its term-specific glossary id.
141
+ const resolvedIds = [];
142
+ for (const domain of domains) {
143
+ const id = inputs.idByDomainTerm.get(`${domain}:${term.toLowerCase()}`);
144
+ if (id === undefined) {
145
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: conflicts entry "${term}" domain "${domain}" — no domain-local glossary item`);
146
+ continue;
147
+ }
148
+ resolvedIds.push({ domain, id });
149
+ }
150
+ // Pair-wise emit across domains[i] × domains[j] for i<j.
151
+ for (let i = 0; i < resolvedIds.length; i++) {
152
+ for (let j = i + 1; j < resolvedIds.length; j++) {
153
+ emitXref(resolvedIds[i].id, resolvedIds[j].id, "conflicts-with");
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ // Walker 2 — reverse-method cross_domain_terms.
160
+ const crossDomainTermsRaw = obj["cross_domain_terms"];
161
+ if (Array.isArray(crossDomainTermsRaw)) {
162
+ for (const item of crossDomainTermsRaw) {
163
+ if (!item || typeof item !== "object")
164
+ continue;
165
+ const o = item;
166
+ const term = String(o["term"] ?? "").trim();
167
+ const owner = String(o["owner"] ?? "").trim();
168
+ if (term === "" || owner === "")
169
+ continue;
170
+ const ownerTermId = inputs.idByDomainTerm.get(`${owner}:${term.toLowerCase()}`);
171
+ if (ownerTermId === undefined) {
172
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: cross_domain_terms entry "${term}" owner "${owner}" doesn't resolve to a glossary item`);
173
+ continue;
174
+ }
175
+ const referencedByRaw = o["referenced_by"];
176
+ if (!Array.isArray(referencedByRaw))
177
+ continue;
178
+ for (const refDomain of referencedByRaw) {
179
+ const domain = String(refDomain ?? "").trim();
180
+ if (domain === "")
181
+ continue;
182
+ const referencerId = inputs.idByDomainTerm.get(`${domain}:${term.toLowerCase()}`);
183
+ if (referencerId === undefined) {
184
+ // Domain doesn't have its own glossary entry for the term — note
185
+ // this and skip (no edge can resolve when source is unknown).
186
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: cross_domain_terms entry "${term}" referencer "${domain}" has no domain-local glossary item — edge skipped`);
187
+ continue;
188
+ }
189
+ emitXref(referencerId, ownerTermId, "references");
190
+ }
191
+ }
192
+ }
193
+ // Walkers 4 + 5 — models[].entities[] (forward-method supersedes + entity-mapping).
194
+ const modelsRaw = obj["models"];
195
+ if (Array.isArray(modelsRaw)) {
196
+ // First pass: index all entities by name for supersedes resolution.
197
+ const entityByName = new Map();
198
+ for (const m of modelsRaw) {
199
+ if (!m || typeof m !== "object")
200
+ continue;
201
+ const mObj = m;
202
+ const domain = String(mObj["domain"] ?? "").trim();
203
+ const entitiesRaw = mObj["entities"];
204
+ if (!Array.isArray(entitiesRaw))
205
+ continue;
206
+ for (const e of entitiesRaw) {
207
+ if (!e || typeof e !== "object")
208
+ continue;
209
+ const eObj = e;
210
+ const name = String(eObj["name"] ?? "").trim();
211
+ const glossaryTerm = String(eObj["glossary_term"] ?? "").trim();
212
+ if (name === "" || glossaryTerm === "")
213
+ continue;
214
+ // First-seen wins; cross-domain entries don't overwrite. Used only
215
+ // by supersedes walker for back-link resolution.
216
+ if (!entityByName.has(name)) {
217
+ entityByName.set(name, { domain, glossaryTerm });
218
+ }
219
+ }
220
+ }
221
+ // Second pass: emit entity_glossary + supersedes edges.
222
+ for (const m of modelsRaw) {
223
+ if (!m || typeof m !== "object")
224
+ continue;
225
+ const mObj = m;
226
+ const domain = String(mObj["domain"] ?? "").trim();
227
+ const entitiesRaw = mObj["entities"];
228
+ if (!Array.isArray(entitiesRaw))
229
+ continue;
230
+ for (const e of entitiesRaw) {
231
+ if (!e || typeof e !== "object")
232
+ continue;
233
+ const eObj = e;
234
+ const name = String(eObj["name"] ?? "").trim();
235
+ const status = String(eObj["status"] ?? "").trim();
236
+ const glossaryTerm = String(eObj["glossary_term"] ?? "").trim();
237
+ if (status === "superseded") {
238
+ const supersededBy = String(eObj["superseded_by"] ?? "").trim();
239
+ if (supersededBy === "") {
240
+ inputs.findings.add("coverage", `${MANIFEST_REL_PATH}: superseded marker on ${name} has no superseded_by field`);
241
+ continue;
242
+ }
243
+ if (glossaryTerm === "") {
244
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: supersedes edge can't emit — ${name} has no glossary_term`);
245
+ continue;
246
+ }
247
+ const sourceId = inputs.idByDomainTerm.get(`${domain}:${glossaryTerm.toLowerCase()}`)
248
+ ?? resolveTermByName(glossaryTerm, inputs);
249
+ const targetEntity = entityByName.get(supersededBy);
250
+ const targetId = targetEntity === undefined
251
+ ? undefined
252
+ : inputs.idByDomainTerm.get(`${targetEntity.domain}:${targetEntity.glossaryTerm.toLowerCase()}`)
253
+ ?? resolveTermByName(targetEntity.glossaryTerm, inputs);
254
+ if (sourceId === undefined) {
255
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: supersedes edge can't emit — source "${glossaryTerm}" doesn't resolve to a glossary item`);
256
+ continue;
257
+ }
258
+ if (targetId === undefined) {
259
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: supersedes edge can't emit — target "${supersededBy}" (→ glossary "${targetEntity?.glossaryTerm ?? "?"}") doesn't resolve to a glossary item`);
260
+ continue;
261
+ }
262
+ emitXref(sourceId, targetId, "supersedes");
263
+ continue;
264
+ }
265
+ if ((status === "aligned" || status === "matched_direct") && glossaryTerm !== "") {
266
+ // Entity-mapping walker writes to entity_glossary.csv.
267
+ if (domain === "" || !inputs.knownDomainIds.has(domain)) {
268
+ // Domain not in this SDD's domains.csv — likely "platform-wide"
269
+ // or another scope identifier. Skip with a coverage finding.
270
+ inputs.findings.add("coverage", `${MANIFEST_REL_PATH}: entity ${name} domain "${domain}" not in known domains — entity_glossary row skipped`);
271
+ continue;
272
+ }
273
+ const glossaryItemId = inputs.idByDomainTerm.get(`${domain}:${glossaryTerm.toLowerCase()}`)
274
+ ?? resolveTermByName(glossaryTerm, inputs);
275
+ if (glossaryItemId === undefined) {
276
+ inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: entity ${name} glossary_term "${glossaryTerm}" doesn't resolve to a glossary item — entity_glossary row skipped`);
277
+ continue;
278
+ }
279
+ const entityNameSlug = slugify(name);
280
+ const id = `${domain}__${entityNameSlug}__${glossaryItemId}`;
281
+ const existing = entitySeen.get(id);
282
+ if (existing !== undefined) {
283
+ // Dedup: matched_direct wins over aligned; same status = no-op;
284
+ // conflicting statuses fire divergence finding.
285
+ const existingStatus = String(existing["status"]);
286
+ if (existingStatus === status)
287
+ continue;
288
+ const winner = status === "matched_direct" ? status : existingStatus;
289
+ if (existingStatus !== winner) {
290
+ entitySeen.set(id, {
291
+ ...existing,
292
+ status: winner,
293
+ });
294
+ }
295
+ inputs.findings.add("divergence", `${MANIFEST_REL_PATH}: entity ${name} in domain ${domain} has conflicting statuses across models (${existingStatus} vs ${status}) — keeping ${winner} (per ADR 0158)`);
296
+ continue;
297
+ }
298
+ const row = {
299
+ id,
300
+ domain_id: domain,
301
+ entity_name: name,
302
+ glossary_item_id: glossaryItemId,
303
+ status,
304
+ source_sdd_id: "",
305
+ };
306
+ entitySeen.set(id, row);
307
+ }
308
+ // status: missing or other → no edge emitted.
309
+ }
310
+ }
311
+ // Realise the deduplicated entity_glossary rows.
312
+ entityRows.push(...entitySeen.values());
313
+ }
314
+ // Surface a single convention finding when the manifest is present but
315
+ // zero walkers produced output. Older builders emitted this; preserved
316
+ // for adopters whose manifest sits in an entirely-unsupported shape.
317
+ if (xrefRows.length === 0 && entityRows.length === 0) {
318
+ const hasAnyKnownKey = "aliases" in obj
319
+ || "conflicts" in obj
320
+ || "conflicts_with" in obj
321
+ || "cross_domain_terms" in obj
322
+ || "models" in obj;
323
+ if (!hasAnyKnownKey) {
324
+ inputs.findings.add("convention", `${MANIFEST_REL_PATH}: present but no recognised top-level keys (aliases/conflicts/cross_domain_terms/models) — emitting 0 xref rows (per ADR 0158)`);
325
+ }
326
+ }
327
+ return { glossaryXref: xrefRows, entityGlossary: entityRows };
328
+ }
329
+ function collectPairs(raw) {
330
+ if (!Array.isArray(raw))
331
+ return [];
332
+ const out = [];
333
+ for (const item of raw) {
334
+ if (!item || typeof item !== "object")
335
+ continue;
336
+ const o = item;
337
+ const source = String(o["source"] ?? o["source_id"] ?? "").trim();
338
+ const target = String(o["target"] ?? o["target_id"] ?? "").trim();
339
+ if (source !== "" && target !== "")
340
+ out.push({ source, target });
341
+ }
342
+ return out;
343
+ }
344
+ function resolveTerm(raw, inputs) {
345
+ // Direct id (e.g. "asset-vehicle")
346
+ if (inputs.knownGlossaryIds.has(raw))
347
+ return raw;
348
+ // domain:term lookup
349
+ const colonIdx = raw.indexOf(":");
350
+ if (colonIdx > 0) {
351
+ const domain = raw.slice(0, colonIdx).trim().toLowerCase();
352
+ const term = raw.slice(colonIdx + 1).trim().toLowerCase();
353
+ return inputs.idByDomainTerm.get(`${domain}:${term}`);
354
+ }
355
+ return undefined;
356
+ }
357
+ function resolveTermByName(term, inputs) {
358
+ // Match term names case-insensitively against the (domain, term) index.
359
+ const needle = term.toLowerCase();
360
+ for (const [key, id] of inputs.idByDomainTerm) {
361
+ const colonIdx = key.indexOf(":");
362
+ if (colonIdx <= 0)
363
+ continue;
364
+ const keyTerm = key.slice(colonIdx + 1);
365
+ if (keyTerm === needle)
366
+ return id;
367
+ }
368
+ return undefined;
369
+ }
370
+ function slugify(s) {
371
+ return s
372
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
373
+ .toLowerCase()
374
+ .replace(/[^a-z0-9-]+/g, "-")
375
+ .replace(/-+/g, "-")
376
+ .replace(/^-|-$/g, "");
377
+ }
378
+ //# sourceMappingURL=glossary-xref.js.map
@@ -0,0 +1,204 @@
1
+ // Glossary discoverer — emits `glossary_items.csv` rows from
2
+ // `docs/domains/<domain>/architecture/glossary.md` "## Owned Terms"
3
+ // tables.
4
+ //
5
+ // Convention (per the catalogue-metamodel.md + an adopter platform's
6
+ // existing layout):
7
+ // ## Owned Terms
8
+ //
9
+ // | Term | Definition | Notes |
10
+ // |------|------------|-------|
11
+ // | <term> | <definition> | <optional notes> |
12
+ //
13
+ // Borrowed terms and deprecated-terms tables are intentionally NOT
14
+ // emitted — borrowed terms reference a canonical entry in another
15
+ // domain (which is already captured by that domain's Owned Terms),
16
+ // and deprecated terms are migration noise.
17
+ //
18
+ // Item id: `<domain>-<slug>` so terms in different domains can reuse
19
+ // the same word without collision.
20
+ import { readdir, readFile } from "node:fs/promises";
21
+ import { join, relative, sep } from "node:path";
22
+ const DOMAINS_DIR_REL = join("docs", "domains");
23
+ const SECTION_HEADING_RE = /^##\s+(Owned|Borrowed|Deprecated)\s+Terms?\s*$/i;
24
+ export async function buildGlossaryItems(inputs) {
25
+ const rows = [];
26
+ const idByDomainTerm = new Map();
27
+ const seen = new Set();
28
+ const glossaryFiles = await collectGlossaryFiles(inputs.sddPath);
29
+ for (const path of glossaryFiles) {
30
+ const relPath = relative(inputs.sddPath, path);
31
+ const domain = inferDomainFromPath(relPath);
32
+ if (domain === "")
33
+ continue;
34
+ if (!inputs.knownDomainIds.has(domain)) {
35
+ inputs.findings.add("orphan", `${relPath}: glossary path implies domain "${domain}" but no docs/domains/${domain}/ exists`);
36
+ continue;
37
+ }
38
+ const text = await readFile(path, "utf-8");
39
+ const ownedTerms = extractOwnedTerms(text);
40
+ for (const { term, definition, notes } of ownedTerms) {
41
+ const slug = slugify(term);
42
+ if (slug === "")
43
+ continue;
44
+ const id = `${domain}-${slug}`;
45
+ if (seen.has(id)) {
46
+ inputs.findings.add("divergence", `${relPath}: glossary item "${id}" already emitted (term="${term}")`);
47
+ continue;
48
+ }
49
+ seen.add(id);
50
+ rows.push({
51
+ id,
52
+ term,
53
+ definition,
54
+ owning_domain_id: domain,
55
+ canonical_term_id: "",
56
+ aliases: "",
57
+ evidence: "",
58
+ notes,
59
+ source_sdd_id: "",
60
+ });
61
+ idByDomainTerm.set(`${domain}:${term.toLowerCase()}`, id);
62
+ }
63
+ }
64
+ return { rows, idByDomainTerm };
65
+ }
66
+ /**
67
+ * Backfill `canonical_term_id` + `aliases` on glossary_items rows from the
68
+ * glossary_xref edges (computed downstream from the same alignment manifest).
69
+ *
70
+ * An `alias-of` / `references` edge means source_id is an alias/borrowing of
71
+ * the canonical target_id, so:
72
+ * - the alias/referencing row gets `canonical_term_id = target_id`;
73
+ * - the canonical row collects the alias term names into `aliases`
74
+ * (';'-separated, per schema).
75
+ *
76
+ * Most cross_domain_terms `referenced_by` entries don't resolve (the
77
+ * referencing domain has no own glossary row for the borrowed term — that's
78
+ * conceptual borrowing, not a duplicated entry), so only the genuinely-
79
+ * duplicated terms populate. That small count is the real data floor, not a
80
+ * gap (see source-gaps-backlog G-02).
81
+ */
82
+ export function backfillGlossaryRelations(rows, xrefRows) {
83
+ const termById = new Map(rows.map((r) => [String(r["id"]), String(r["term"])]));
84
+ const canonicalByAlias = new Map();
85
+ const aliasTermsByCanonical = new Map();
86
+ for (const x of xrefRows) {
87
+ const relation = String(x["relation"]);
88
+ if (relation !== "alias-of" && relation !== "references")
89
+ continue;
90
+ const aliasId = String(x["source_id"]);
91
+ const canonicalId = String(x["target_id"]);
92
+ if (aliasId === "" || canonicalId === "" || aliasId === canonicalId)
93
+ continue;
94
+ if (!canonicalByAlias.has(aliasId))
95
+ canonicalByAlias.set(aliasId, canonicalId);
96
+ const aliasTerm = termById.get(aliasId);
97
+ if (aliasTerm) {
98
+ const list = aliasTermsByCanonical.get(canonicalId) ?? [];
99
+ if (!list.includes(aliasTerm))
100
+ list.push(aliasTerm);
101
+ aliasTermsByCanonical.set(canonicalId, list);
102
+ }
103
+ }
104
+ if (canonicalByAlias.size === 0 && aliasTermsByCanonical.size === 0) {
105
+ return [...rows];
106
+ }
107
+ return rows.map((r) => {
108
+ const id = String(r["id"]);
109
+ const canonical = canonicalByAlias.get(id);
110
+ const aliasTerms = aliasTermsByCanonical.get(id);
111
+ if (canonical === undefined && aliasTerms === undefined)
112
+ return r;
113
+ return {
114
+ ...r,
115
+ ...(canonical !== undefined ? { canonical_term_id: canonical } : {}),
116
+ ...(aliasTerms !== undefined ? { aliases: aliasTerms.join(";") } : {}),
117
+ };
118
+ });
119
+ }
120
+ function extractOwnedTerms(text) {
121
+ const out = [];
122
+ let inOwnedSection = false;
123
+ let sawHeader = false;
124
+ for (const rawLine of text.split(/\r?\n/)) {
125
+ const line = rawLine.trimEnd();
126
+ const heading = SECTION_HEADING_RE.exec(line.trim());
127
+ if (heading) {
128
+ inOwnedSection = (heading[1] ?? "").toLowerCase() === "owned";
129
+ sawHeader = false;
130
+ continue;
131
+ }
132
+ if (!inOwnedSection)
133
+ continue;
134
+ // Header row: `| Term | Definition | ... |`
135
+ if (/^\s*\|\s*Term\b/i.test(line)) {
136
+ sawHeader = true;
137
+ continue;
138
+ }
139
+ // Separator row: `|---|---|...`
140
+ if (/^\s*\|[\s-:|]+\|\s*$/.test(line))
141
+ continue;
142
+ if (!sawHeader)
143
+ continue;
144
+ if (!line.trim().startsWith("|"))
145
+ continue;
146
+ const cells = splitTableRow(line);
147
+ if (cells.length < 2 || cells[0] === "")
148
+ continue;
149
+ out.push({
150
+ term: cells[0] ?? "",
151
+ definition: cells[1] ?? "",
152
+ notes: cells[2] ?? "",
153
+ });
154
+ }
155
+ return out;
156
+ }
157
+ function splitTableRow(line) {
158
+ // Trim outer pipes, split on |, strip cells. Doesn't try to handle
159
+ // escaped pipes — adopter convention says: avoid raw | in cells, use
160
+ // "or" instead. We honour that.
161
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
162
+ return trimmed.split("|").map((c) => c.trim());
163
+ }
164
+ async function collectGlossaryFiles(sddPath) {
165
+ const out = [];
166
+ const domainsDir = join(sddPath, DOMAINS_DIR_REL);
167
+ let domains;
168
+ try {
169
+ domains = await readdir(domainsDir, { withFileTypes: true });
170
+ }
171
+ catch {
172
+ return out;
173
+ }
174
+ for (const d of domains) {
175
+ if (!d.isDirectory())
176
+ continue;
177
+ const path = join(domainsDir, d.name, "architecture", "glossary.md");
178
+ try {
179
+ await readFile(path, "utf-8");
180
+ out.push(path);
181
+ }
182
+ catch (err) {
183
+ const code = err.code;
184
+ if (code !== "ENOENT")
185
+ throw err;
186
+ }
187
+ }
188
+ return out.sort();
189
+ }
190
+ function inferDomainFromPath(relPath) {
191
+ const parts = relPath.split(sep);
192
+ const idx = parts.indexOf("domains");
193
+ if (idx === -1 || idx + 1 >= parts.length)
194
+ return "";
195
+ return parts[idx + 1] ?? "";
196
+ }
197
+ function slugify(text) {
198
+ return text
199
+ .toLowerCase()
200
+ .replace(/&/g, " and ")
201
+ .replace(/[^a-z0-9]+/g, "-")
202
+ .replace(/^-+|-+$/g, "");
203
+ }
204
+ //# sourceMappingURL=glossary.js.map