@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,61 @@
1
+ // repo_capability_exemption builder — M-ADR 0188 §4.3.
2
+ //
3
+ // Emits one row per repo-config declaring a `capability_exemption:` block:
4
+ //
5
+ // capability_exemption:
6
+ // reason: infrastructure # closed vocabulary, see canonical-schema.yaml
7
+ // note: "Terraform modules; provisions the estate, realises no capability."
8
+ //
9
+ // The table exists so that "this repository deliberately bears no capability"
10
+ // is a fact in the model rather than an absence in it. Under M-ADR 0188 §4.1
11
+ // every `repositories` row must resolve — by a `repo_capability` edge, or by a
12
+ // row here. FK integrity cannot express that: a missing edge is not a dangling
13
+ // reference, it is nothing, and nothing is what a referential check is blind
14
+ // to. Coverage is a participation constraint, checked, not declared.
15
+ //
16
+ // VALIDATION ORDER IS LOad-BEARING. Every rejection below drops the exemption
17
+ // row and leaves the repository unresolved — reported as an orphan by
18
+ // `check-repo-capability-coverage.sh`. It never drops the repository. An
19
+ // exemption that fails validation must not quietly succeed at the one thing an
20
+ // exemption does, which is remove a repository from the invariant's attention.
21
+ // Fail open on the row; fail closed on the repository.
22
+ export function buildRepoCapabilityExemption(inputs) {
23
+ const rows = [];
24
+ const seen = new Set();
25
+ for (const cfg of inputs.configs) {
26
+ const block = cfg.data.capability_exemption;
27
+ if (block === undefined || block === null)
28
+ continue;
29
+ const reason = typeof block.reason === "string" ? block.reason.trim() : "";
30
+ const note = typeof block.note === "string" ? block.note.trim() : "";
31
+ if (reason === "") {
32
+ inputs.findings.add("convention", `${cfg.relPath}: capability_exemption declares no reason — exemption dropped, repo stays unresolved (per M-ADR 0188 §4.2)`);
33
+ continue;
34
+ }
35
+ if (!inputs.allowedReasons.has(reason)) {
36
+ const allowed = [...inputs.allowedReasons].sort().join(", ");
37
+ inputs.findings.add("convention", `${cfg.relPath}: capability_exemption reason "${reason}" is outside the closed vocabulary (${allowed}) — exemption dropped, repo stays unresolved (per M-ADR 0188 §4.2)`);
38
+ continue;
39
+ }
40
+ // The note is what makes an exemption reviewable rather than merely
41
+ // countable. An unexplained suppression is a defect; an explained one is
42
+ // a decision. The method takes the lint-suppression position here.
43
+ if (note === "") {
44
+ inputs.findings.add("convention", `${cfg.relPath}: capability_exemption (${reason}) has an empty note — exemption dropped, repo stays unresolved (per M-ADR 0188 §4.2)`);
45
+ continue;
46
+ }
47
+ if (inputs.knownRepoIds !== undefined &&
48
+ !inputs.knownRepoIds.has(cfg.repoId)) {
49
+ continue;
50
+ }
51
+ const id = cfg.repoId;
52
+ if (seen.has(id)) {
53
+ inputs.findings.add("divergence", `${cfg.relPath}: duplicate capability_exemption for repo ${cfg.repoId} — first declaration wins`);
54
+ continue;
55
+ }
56
+ seen.add(id);
57
+ rows.push({ id, repo_id: cfg.repoId, reason, note });
58
+ }
59
+ return rows;
60
+ }
61
+ //# sourceMappingURL=repo-capability-exemption.js.map
@@ -0,0 +1,127 @@
1
+ // repo_dependency — a repository depends on another repository's code without either
2
+ // being a running service: a library linked at build time, or a template family it was
3
+ // generated from (M-ADR 0217, schema 3.20.0).
4
+ //
5
+ // Two sources, merged by id (M-ADR 0157 parity):
6
+ // DECLARED repo-config `depends_on: [{repo, kind, version?}]`; from_repo_id is the
7
+ // repository the repo-config describes. `kind` is required, so a provenance
8
+ // dependency is never recorded as a library by default.
9
+ // DERIVED a per-component integration manifest's `dependencies.sharedLibraries`
10
+ // (`repos/<member>/integration-manifest.yaml`), each entry kind `library`,
11
+ // from_repo_id the member directory. `none` is a declared absence.
12
+ // The declared row wins `version` and `declared_in` when both name one dependency.
13
+ //
14
+ // to_repo_id is EXTERNAL: it may belong to another SDD, so build reports what it cannot
15
+ // resolve and the aggregator fails what still resolves to nothing. An entry is never
16
+ // renamed or guessed to make it resolve.
17
+ import { readdir, readFile } from "node:fs/promises";
18
+ import { join } from "node:path";
19
+ import { parse as parseYaml } from "yaml";
20
+ export const REPO_DEPENDENCY_KINDS = ["library", "generated-from"];
21
+ const rowId = (d) => `${d.from}__${d.to}__${d.kind}`;
22
+ function declared(inputs) {
23
+ const out = [];
24
+ for (const cfg of inputs.configs) {
25
+ const entries = cfg.data.depends_on ?? [];
26
+ entries.forEach((raw, i) => {
27
+ const where = `${cfg.relPath}: depends_on[${i}]`;
28
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
29
+ inputs.findings.add("convention", `${where}: expected a mapping {repo, kind, version?} — entry skipped (M-ADR 0217 §4.2)`);
30
+ return;
31
+ }
32
+ const e = raw;
33
+ const repo = typeof e["repo"] === "string" ? e["repo"].trim() : "";
34
+ const kind = typeof e["kind"] === "string" ? e["kind"].trim() : "";
35
+ if (repo === "") {
36
+ inputs.findings.add("convention", `${where}: no repo — entry skipped (M-ADR 0217 §4.2)`);
37
+ return;
38
+ }
39
+ if (!REPO_DEPENDENCY_KINDS.includes(kind)) {
40
+ inputs.findings.add("convention", `${where}: kind "${kind}" is not one of ${REPO_DEPENDENCY_KINDS.join(" | ")} — entry skipped. ` +
41
+ `kind is required: a provenance dependency is never recorded as a library by default (M-ADR 0217 §4.2)`);
42
+ return;
43
+ }
44
+ const version = e["version"] === undefined || e["version"] === null ? "" : String(e["version"]).trim();
45
+ out.push({ from: cfg.repoId, to: repo, kind: kind, version, declaredIn: cfg.relPath });
46
+ });
47
+ }
48
+ return out;
49
+ }
50
+ async function derived(inputs) {
51
+ let dirs;
52
+ try {
53
+ dirs = (await readdir(join(inputs.sddPath, "repos"), { withFileTypes: true }))
54
+ .filter((e) => e.isDirectory())
55
+ .map((e) => e.name)
56
+ .sort();
57
+ }
58
+ catch (err) {
59
+ if (err.code === "ENOENT")
60
+ return [];
61
+ throw err;
62
+ }
63
+ const out = [];
64
+ for (const dir of dirs) {
65
+ const rel = `repos/${dir}/integration-manifest.yaml`;
66
+ let text;
67
+ try {
68
+ text = await readFile(join(inputs.sddPath, rel), "utf-8");
69
+ }
70
+ catch (err) {
71
+ if (err.code === "ENOENT")
72
+ continue;
73
+ throw err;
74
+ }
75
+ let doc;
76
+ try {
77
+ doc = parseYaml(text);
78
+ }
79
+ catch {
80
+ continue; // integration_component already reports an unparsable manifest
81
+ }
82
+ const deps = doc?.dependencies;
83
+ const libs = deps?.sharedLibraries;
84
+ if (libs === undefined || libs === null)
85
+ continue; // the field is optional; absence is no finding
86
+ if (!Array.isArray(libs)) {
87
+ inputs.findings.add("convention", `${rel}: dependencies.sharedLibraries is not a list — ignored (M-ADR 0217 §4.2)`);
88
+ continue;
89
+ }
90
+ const names = libs.map((l) => String(l).trim()).filter((l) => l !== "" && l !== "none");
91
+ if (names.length === 0)
92
+ continue; // `none` is a declared absence
93
+ // from_repo_id is an INTERNAL key: the member directory must be one of this SDD's
94
+ // repositories, or the row would fail the build's own FK check.
95
+ if (!inputs.knownRepoIds.has(dir)) {
96
+ inputs.findings.add("coverage", `repo_dependency: ${rel} declares ${names.length} shared librar${names.length === 1 ? "y" : "ies"} but its directory "${dir}" is no repositories.id in this SDD — not emitted. Catalogue the repository or correct the directory name.`);
97
+ continue;
98
+ }
99
+ for (const to of names)
100
+ out.push({ from: dir, to, kind: "library", version: "", declaredIn: rel });
101
+ }
102
+ return out;
103
+ }
104
+ export async function buildRepoDependencies(inputs) {
105
+ const byId = new Map();
106
+ for (const d of await derived(inputs))
107
+ if (!byId.has(rowId(d)))
108
+ byId.set(rowId(d), d);
109
+ // Declared second, so it overrides version and declared_in.
110
+ for (const d of declared(inputs)) {
111
+ const prior = byId.get(rowId(d));
112
+ byId.set(rowId(d), prior === undefined ? d : { ...d, version: d.version !== "" ? d.version : prior.version });
113
+ }
114
+ return [...byId.values()]
115
+ .sort((a, b) => rowId(a).localeCompare(rowId(b)))
116
+ .map((d) => ({
117
+ id: rowId(d),
118
+ from_repo_id: d.from,
119
+ to_repo_id: d.to,
120
+ kind: d.kind,
121
+ version: d.version,
122
+ declared_in: d.declaredIn,
123
+ source_sdd_id: "",
124
+ platform_sdd_id: "",
125
+ }));
126
+ }
127
+ //# sourceMappingURL=repo-dependency.js.map
@@ -0,0 +1,340 @@
1
+ // Repo-edge builders — repo_capability, repo_contract, feature_repo.
2
+ //
3
+ // Per ADR 0157 (Forward-Declared / Reverse-Derived Parity for Edge Catalogues):
4
+ // both edges ingest from forward-method declared sources AND reverse-method
5
+ // derived sources, merged by primary-key deduplication. The two paths are
6
+ // equal-status per the parity principle; neither displaces the other.
7
+ //
8
+ // Forward-method declared sources (typically authored by forward-method
9
+ // adopters in `orchestration/repo-config/<domain>/<repo>.yaml`):
10
+ //
11
+ // capabilities: # M-ADR 0188 §6.1
12
+ // - <capability spec_path, exactly as emitted in capabilities.csv>
13
+ //
14
+ // The spec_path layout is profile-dependent, NOT fixed — a reverse platform SDD emits
15
+ // `docs/domains/<domain>/product/<capability>.md`, a forward application-product may emit
16
+ // `docs/<capability>.md`. Match against capabilities.csv, never against a path convention.
17
+ //
18
+ // additional_context: # legacy, multi-purpose
19
+ // - docs/domains/<domain>/product/<capability>.md
20
+ // - docs/domains/<domain>/architecture/adr/ADR XXXX: ....md
21
+ //
22
+ // contracts:
23
+ // openapi: [<path-relative-to-contracts/openapi/>]
24
+ // proto: [<path-relative-to-contracts/proto/>]
25
+ // avro: [<path-relative-to-contracts/avro/>]
26
+ //
27
+ // Reverse-method derived sources (universal — work for any SDD with the
28
+ // underlying catalogue rows populated):
29
+ //
30
+ // repo_contract Path X (universal) — `service_contract ⋈
31
+ // services.primary_repo_id`. A service exposing a contract is implemented
32
+ // by a repo (its primary_repo_id) — the repo "owns" the contract by
33
+ // transitive inference. Works universally because primary_repo_id and
34
+ // service_contract are core-schema populated. Added per ADR 0157 §4.2
35
+ // amendment after an early-adopter 0/30 source_repo_id outcome.
36
+ // repo_contract Path Y — `contracts.source_repo_id` denormalisation
37
+ // (from OpenAPI `x-source-repo` hints per ADR 0110, or whatever
38
+ // populates the column). Augments Path X when adopters carry the hint.
39
+ // repo_capability Path X (universal) — `capability_service ⋈
40
+ // services.primary_repo_id`. Works in both forward and reverse method
41
+ // shapes because primary_repo_id is core-schema and capability_service
42
+ // is heavily authored.
43
+ // repo_capability Path Y (augments via ADR 0143 feature-grounding) —
44
+ // `capability_feature ⋈ feature_repo`. Adopters who have adopted the
45
+ // feature-grounding extractor get many additional rows.
46
+ //
47
+ // feature_repo: ADR 0143's feature-grounding extractor populates this in
48
+ // build.ts (10a). The forward declared `contributes_to.artifact_type:
49
+ // feature-set` path is documented in the metamodel but unused by any
50
+ // observed adopter — left empty here pending first-adopter authoring.
51
+ import { basename, sep } from "node:path";
52
+ import { normaliseAdditionalContextEntry, } from "./repositories.js";
53
+ /**
54
+ * feature_repo derivation — `feature_service ⋈ services.primary_repo_id`.
55
+ *
56
+ * The same shape as repo_capability's Path X one rung down the hierarchy: a
57
+ * feature implemented by a service is implemented in the repo that builds that
58
+ * service. Added per D8 of the catalogue-coverage-invariants work, and only
59
+ * become POSSIBLE once the app-product Feature Services table was wired into
60
+ * feature_service (0.115.0) — before that the join's left side was empty in the
61
+ * profile where feature_repo had no source at all.
62
+ *
63
+ * ⚠ IT KEYS ON `primary_repo_id`, A DECLARED FK — NOT ON THE `service.id ==
64
+ * repo.id` IDENTITY. That distinction is why M-ADR 0197 does not endanger it:
65
+ * 0197 breaks the identity, and Phase A explicitly preserves primary_repo_id on
66
+ * every declared entry. Path X is identity-VALUED today, not
67
+ * identity-DEPENDENT, and so is this. A first review of D8 flagged the opposite
68
+ * and was wrong.
69
+ *
70
+ * Caller supplies `existingIds` so this only ever ADDS: the ADR 0143
71
+ * feature-grounding extractor and the capability-spec walker own their rows and
72
+ * are authoritative where they overlap.
73
+ */
74
+ export function deriveFeatureRepoFromServices(inputs) {
75
+ const repoBySvc = new Map();
76
+ for (const s of inputs.serviceRows) {
77
+ const sid = String(s["id"] ?? "");
78
+ const rid = String(s["primary_repo_id"] ?? "");
79
+ if (sid !== "" && rid !== "")
80
+ repoBySvc.set(sid, rid);
81
+ }
82
+ const rows = [];
83
+ const seen = new Set(inputs.existingIds);
84
+ for (const fs of inputs.featureServiceRows) {
85
+ const featureId = String(fs["feature_id"] ?? "");
86
+ const svcId = String(fs["service_id"] ?? "");
87
+ if (featureId === "" || svcId === "")
88
+ continue;
89
+ const repoId = repoBySvc.get(svcId);
90
+ // A service with no primary_repo_id contributes nothing — silently, because
91
+ // repo_contract's Path X already reports that condition once per build and
92
+ // a second identical finding per feature would be noise, not signal.
93
+ if (repoId === undefined)
94
+ continue;
95
+ // Never emit a dangling repo reference. Measured across two estates at the
96
+ // time of writing: 0 of 1,221 derived rows referenced an unresolvable
97
+ // primary_repo_id, so this guard is a safety net rather than a filter.
98
+ if (!inputs.knownRepoIds.has(repoId))
99
+ continue;
100
+ const id = `${featureId}__${repoId}`;
101
+ if (seen.has(id))
102
+ continue;
103
+ seen.add(id);
104
+ rows.push({ id, feature_id: featureId, repo_id: repoId });
105
+ }
106
+ return rows;
107
+ }
108
+ export function buildRepoEdges(inputs) {
109
+ const repoCapability = collectRepoCapability(inputs);
110
+ const repoContract = collectRepoContract(inputs);
111
+ // feature_repo is populated later in build.ts by the ADR 0143
112
+ // feature-grounding extractor; this builder doesn't contribute.
113
+ const featureRepo = [];
114
+ return { repoCapability, repoContract, featureRepo };
115
+ }
116
+ function collectRepoCapability(inputs) {
117
+ const rows = [];
118
+ const seen = new Set();
119
+ const serviceRows = inputs.serviceRows ?? [];
120
+ const capabilityServiceRows = inputs.capabilityServiceRows ?? [];
121
+ const capabilityFeatureRows = inputs.capabilityFeatureRows ?? [];
122
+ const featureRepoRows = inputs.featureRepoRows ?? [];
123
+ const knownRepoIds = inputs.knownRepoIds;
124
+ const emit = (repoId, capId) => {
125
+ if (knownRepoIds !== undefined && !knownRepoIds.has(repoId))
126
+ return;
127
+ const id = `${repoId}__${capId}`;
128
+ if (seen.has(id))
129
+ return;
130
+ seen.add(id);
131
+ rows.push({ id, repo_id: repoId, capability_id: capId });
132
+ };
133
+ // Forward path A — declared in repo-config `capabilities[]` (M-ADR 0188 §6.1).
134
+ //
135
+ // This block exists because the older `additional_context[]` path below,
136
+ // though it works, cannot be validated: the field is multi-purpose, so an
137
+ // entry that resolves to no capability is indistinguishable from an ADR
138
+ // reference and must be skipped in silence. A typo'd capability path is
139
+ // therefore undetectable there — which is why, on the reference estate,
140
+ // 0 of 377 repo-configs used it to declare a capability and 264 repos
141
+ // resolved to nothing.
142
+ //
143
+ // `capabilities[]` is single-purpose. Every entry is an assertion that this
144
+ // repository realises that capability, so a non-match is an authoring error
145
+ // and is reported as one. Same edge, same table; the difference is that this
146
+ // surface can tell you when you got it wrong.
147
+ for (const cfg of inputs.configs) {
148
+ for (const rawEntry of cfg.data.capabilities ?? []) {
149
+ const cleaned = rawEntry.replace(/^\/+/, "").trim();
150
+ if (cleaned === "")
151
+ continue;
152
+ const capId = inputs.capabilityIdByPath.get(cleaned);
153
+ if (capId === undefined) {
154
+ inputs.findings.add("orphan", `${cfg.relPath}: capabilities entry "${cleaned}" did not match any capability spec_path (per M-ADR 0188 §6.1)`);
155
+ continue;
156
+ }
157
+ emit(cfg.repoId, capId);
158
+ }
159
+ }
160
+ // Forward path B (legacy) — declared in repo-config additional_context[].
161
+ for (const cfg of inputs.configs) {
162
+ const extras = cfg.data.additional_context ?? [];
163
+ for (const raw of extras) {
164
+ const reconstructed = normaliseAdditionalContextEntry(raw);
165
+ if (reconstructed === null)
166
+ continue;
167
+ const cleaned = reconstructed.replace(/^\/+/, "");
168
+ const capId = inputs.capabilityIdByPath.get(cleaned);
169
+ if (capId === undefined) {
170
+ // Not a capability spec path (likely an ADR file or freeform doc).
171
+ // additional_context is intentionally multi-purpose; only matches
172
+ // that resolve to capabilities count. Cannot warn here — see above.
173
+ continue;
174
+ }
175
+ emit(cfg.repoId, capId);
176
+ }
177
+ }
178
+ // Reverse path X (universal) — capability_service ⋈ services.primary_repo_id.
179
+ const repoBySvcId = new Map();
180
+ for (const s of serviceRows) {
181
+ const sid = String(s["id"]);
182
+ const repoId = String(s["primary_repo_id"] ?? "");
183
+ if (sid !== "" && repoId !== "")
184
+ repoBySvcId.set(sid, repoId);
185
+ }
186
+ // M-ADR 0157 §4.3 (amended): `repo_capability` means "repo CONTRIBUTES to
187
+ // capability". A capability_service row with role `dependency` says the
188
+ // capability DEPENDS ON the service — the inverse direction — so its repo
189
+ // does not contribute and Path X skips it. `primary`, `related` and
190
+ // `backend-gateway` all participate in delivering the capability and stay.
191
+ // Measured before the filter: 62% of one estate's repo_capability rows and
192
+ // one row of another's rested on non-primary roles; `related` was realising
193
+ // in every measured use, so only `dependency` is excluded.
194
+ let dependencyRowsSkipped = 0;
195
+ for (const cs of capabilityServiceRows) {
196
+ const svcId = String(cs["service_id"] ?? "");
197
+ const capId = String(cs["capability_id"] ?? "");
198
+ if (svcId === "" || capId === "")
199
+ continue;
200
+ if (String(cs["role"] ?? "").trim() === "dependency") {
201
+ dependencyRowsSkipped++;
202
+ continue;
203
+ }
204
+ const repoId = repoBySvcId.get(svcId);
205
+ if (repoId === undefined)
206
+ continue;
207
+ emit(repoId, capId);
208
+ }
209
+ if (dependencyRowsSkipped > 0) {
210
+ inputs.findings.add("coverage", `repo_capability: ${dependencyRowsSkipped} capability_service rows with role "dependency" excluded from Path X — a dependency is the inverse direction of "repo contributes to capability" (M-ADR 0157 §4.3 as amended); the relationship itself is unchanged in capability_service`);
211
+ }
212
+ // Surface a single summary finding when capability_service rows exist with
213
+ // services lacking primary_repo_id. The walker quietly skips those edges;
214
+ // this finding tells the reader why the universal path missed some.
215
+ let missingPrimaryRepoSvcs = 0;
216
+ for (const cs of capabilityServiceRows) {
217
+ const svcId = String(cs["service_id"] ?? "");
218
+ if (svcId === "")
219
+ continue;
220
+ if (!repoBySvcId.has(svcId))
221
+ missingPrimaryRepoSvcs++;
222
+ }
223
+ if (missingPrimaryRepoSvcs > 0) {
224
+ inputs.findings.add("orphan", `repo_capability: ${missingPrimaryRepoSvcs} capability_service rows reference services with empty primary_repo_id — reverse-method Path X (capability_service ⋈ services.primary_repo_id) skipped those edges (per ADR 0157 §4.3)`);
225
+ }
226
+ // Reverse path Y — capability_feature ⋈ feature_repo (transitive via ADR 0143).
227
+ const featuresToRepos = new Map();
228
+ for (const fr of featureRepoRows) {
229
+ const fid = String(fr["feature_id"] ?? "");
230
+ const rid = String(fr["repo_id"] ?? "");
231
+ if (fid === "" || rid === "")
232
+ continue;
233
+ if (!featuresToRepos.has(fid))
234
+ featuresToRepos.set(fid, new Set());
235
+ featuresToRepos.get(fid).add(rid);
236
+ }
237
+ for (const cf of capabilityFeatureRows) {
238
+ const fid = String(cf["feature_id"] ?? "");
239
+ const cid = String(cf["capability_id"] ?? "");
240
+ if (fid === "" || cid === "")
241
+ continue;
242
+ const repos = featuresToRepos.get(fid);
243
+ if (!repos)
244
+ continue;
245
+ for (const repoId of repos)
246
+ emit(repoId, cid);
247
+ }
248
+ return rows;
249
+ }
250
+ function collectRepoContract(inputs) {
251
+ const rows = [];
252
+ const seen = new Set();
253
+ const contractRows = inputs.contractRows ?? [];
254
+ const serviceRows = inputs.serviceRows ?? [];
255
+ const serviceContractRows = inputs.serviceContractRows ?? [];
256
+ const knownRepoIds = inputs.knownRepoIds;
257
+ const emit = (repoId, contractId) => {
258
+ if (knownRepoIds !== undefined && !knownRepoIds.has(repoId))
259
+ return;
260
+ const id = `${repoId}__${contractId}`;
261
+ if (seen.has(id))
262
+ return;
263
+ seen.add(id);
264
+ rows.push({ id, repo_id: repoId, contract_id: contractId });
265
+ };
266
+ // Forward path — declared in repo-config contracts.{openapi,proto,avro}[].
267
+ // Pre-compute suffix and basename indexes for fuzzy matching.
268
+ const suffixIndex = new Map();
269
+ const basenameIndex = new Map();
270
+ for (const [path, id] of inputs.contractIdByPath) {
271
+ const parts = path.split(sep);
272
+ if (parts.length >= 2) {
273
+ suffixIndex.set(parts.slice(-2).join("/"), id);
274
+ }
275
+ basenameIndex.set(basename(path), id);
276
+ }
277
+ for (const cfg of inputs.configs) {
278
+ const block = cfg.data.contracts;
279
+ if (!block)
280
+ continue;
281
+ for (const kind of ["openapi", "proto", "avro"]) {
282
+ const list = block[kind] ?? [];
283
+ for (const rawEntry of list) {
284
+ const entry = rawEntry.replace(/^\/+/, "");
285
+ const fullCandidate = `contracts/${kind}/${entry}`;
286
+ const id = inputs.contractIdByPath.get(fullCandidate)
287
+ ?? inputs.contractIdByPath.get(entry)
288
+ ?? suffixIndex.get(fullCandidate)
289
+ ?? suffixIndex.get(entry)
290
+ ?? basenameIndex.get(basename(entry));
291
+ if (id === undefined) {
292
+ inputs.findings.add("orphan", `${cfg.relPath}: contracts.${kind} entry "${entry}" did not match any contract`);
293
+ continue;
294
+ }
295
+ emit(cfg.repoId, id);
296
+ }
297
+ }
298
+ }
299
+ // Reverse path X (universal) — service_contract ⋈ services.primary_repo_id.
300
+ // A service exposing a contract is implemented by a repo; the repo "owns"
301
+ // the contract by transitive inference. Added per ADR 0157 §4.2 amendment
302
+ // (an early adopter's source_repo_id was 0/30 populated, so
303
+ // a derivation that depends only on it leaves them empty).
304
+ const repoBySvcId = new Map();
305
+ for (const s of serviceRows) {
306
+ const sid = String(s["id"]);
307
+ const repoId = String(s["primary_repo_id"] ?? "");
308
+ if (sid !== "" && repoId !== "")
309
+ repoBySvcId.set(sid, repoId);
310
+ }
311
+ let missingPrimaryRepoSvcs = 0;
312
+ for (const sc of serviceContractRows) {
313
+ const svcId = String(sc["service_id"] ?? "");
314
+ const ctrId = String(sc["contract_id"] ?? "");
315
+ if (svcId === "" || ctrId === "")
316
+ continue;
317
+ const repoId = repoBySvcId.get(svcId);
318
+ if (repoId === undefined) {
319
+ missingPrimaryRepoSvcs++;
320
+ continue;
321
+ }
322
+ emit(repoId, ctrId);
323
+ }
324
+ if (missingPrimaryRepoSvcs > 0) {
325
+ inputs.findings.add("orphan", `repo_contract: ${missingPrimaryRepoSvcs} service_contract rows reference services with empty primary_repo_id — reverse-method Path X (service_contract ⋈ services.primary_repo_id) skipped those edges (per ADR 0157 §4.2)`);
326
+ }
327
+ // Reverse path Y — contracts.source_repo_id denormalisation.
328
+ // Adopters who carry OpenAPI x-source-repo hints (per ADR 0110) or whose
329
+ // contracts builder otherwise populates source_repo_id get one row per
330
+ // (source_repo_id, contract.id) for free.
331
+ for (const c of contractRows) {
332
+ const cid = String(c["id"]);
333
+ const srcRepo = String(c["source_repo_id"] ?? "");
334
+ if (cid === "" || srcRepo === "")
335
+ continue;
336
+ emit(srcRepo, cid);
337
+ }
338
+ return rows;
339
+ }
340
+ //# sourceMappingURL=repo-edges.js.map