@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,152 @@
1
+ // Personas discoverer — emits `personas.csv` rows from `personas/*.md`
2
+ // at the SDD root.
3
+ //
4
+ // Source convention (per docs/method/methods/persona-template.md):
5
+ // personas/<slug>.md
6
+ // ---
7
+ // # Persona: {Name}
8
+ // **App:** {app-name}
9
+ // ## Summary
10
+ // {1-2 sentences}
11
+ //
12
+ // Cross-profile path (per coordination/persona-walker-platform-product-gap-
13
+ // coordination.md, 2026-05-29): the walker reads `personas/` at SDD root
14
+ // for every profile. Each SDD's personas/ carries personas at that SDD's
15
+ // scope — for application-product that's app-specific personas, for
16
+ // platform-product that's platform-wide / cross-app personas surfaced as
17
+ // Person() actors on the L1 context diagram. integration-product can use
18
+ // the same shape if a partner-side persona is ever needed (rare).
19
+ //
20
+ // id = `{product_id}-{slug}`, slug from filename stem.
21
+ // name = H1 (with the optional `Persona:` prefix stripped).
22
+ // description = first non-empty paragraph under `## Summary`.
23
+ // role = empty (template doesn't carry a single-token role today; the
24
+ // column stays available for ecosystems that want to populate it).
25
+ import { readdir, readFile } from "node:fs/promises";
26
+ import { basename, join, relative } from "node:path";
27
+ const PERSONAS_DIR_REL = "personas";
28
+ export async function buildPersonas(inputs) {
29
+ const idByPath = new Map();
30
+ const idBySlug = new Map();
31
+ const idByName = new Map();
32
+ const relatedFeaturesById = new Map();
33
+ const dir = join(inputs.sddPath, PERSONAS_DIR_REL);
34
+ let entries;
35
+ try {
36
+ entries = await readdir(dir, { withFileTypes: true });
37
+ }
38
+ catch (err) {
39
+ if (err.code !== "ENOENT")
40
+ throw err;
41
+ inputs.findings.add("coverage", `${PERSONAS_DIR_REL}/ not found — emitting empty personas.csv`);
42
+ return { rows: [], idByPath, idBySlug, idByName, relatedFeaturesById };
43
+ }
44
+ const files = entries
45
+ .filter((e) => e.isFile() && e.name.endsWith(".md") && !isTemplate(e.name))
46
+ .map((e) => e.name)
47
+ .sort();
48
+ const rows = [];
49
+ for (const filename of files) {
50
+ const slug = basename(filename).replace(/\.md$/i, "");
51
+ const id = `${inputs.productId}-${slug}`;
52
+ const absPath = join(dir, filename);
53
+ const relPath = relative(inputs.sddPath, absPath);
54
+ const text = await readFile(absPath, "utf-8");
55
+ const name = extractName(text) || titleCase(slug);
56
+ const description = extractSummary(text);
57
+ const relatedFeatures = extractRelatedFeatures(text);
58
+ rows.push({
59
+ id,
60
+ name,
61
+ product_id: inputs.productId,
62
+ role: "",
63
+ description,
64
+ source_sdd_id: "",
65
+ });
66
+ idByPath.set(relPath, id);
67
+ idBySlug.set(slug, id);
68
+ idByName.set(normaliseName(name), id);
69
+ relatedFeaturesById.set(id, relatedFeatures);
70
+ }
71
+ return { rows, idByPath, idBySlug, idByName, relatedFeaturesById };
72
+ }
73
+ function isTemplate(name) {
74
+ return /[{}]/.test(name) || name.toLowerCase().endsWith("-template.md");
75
+ }
76
+ function extractName(text) {
77
+ for (const raw of text.split(/\r?\n/)) {
78
+ const line = raw.trim();
79
+ if (line === "")
80
+ continue;
81
+ // Only the top-level H1 counts as a name. Skip non-heading lines and
82
+ // sub-headings (## ...).
83
+ const m = /^#\s+(.+)$/.exec(line);
84
+ if (!m)
85
+ return "";
86
+ const stripped = (m[1] ?? "").trim();
87
+ const personaPrefix = /^Persona:\s*(.+)$/i.exec(stripped);
88
+ return personaPrefix ? (personaPrefix[1] ?? "").trim() : stripped;
89
+ }
90
+ return "";
91
+ }
92
+ function extractSummary(text) {
93
+ const lines = text.split(/\r?\n/);
94
+ let i = 0;
95
+ // Find `## Summary` heading.
96
+ while (i < lines.length) {
97
+ if (/^##\s+summary\b/i.test(lines[i] ?? ""))
98
+ break;
99
+ i++;
100
+ }
101
+ if (i === lines.length)
102
+ return "";
103
+ i++;
104
+ // Skip blank lines.
105
+ while (i < lines.length && (lines[i] ?? "").trim() === "")
106
+ i++;
107
+ // Capture first non-empty paragraph until blank line.
108
+ const para = [];
109
+ while (i < lines.length) {
110
+ const line = lines[i] ?? "";
111
+ if (line.trim() === "")
112
+ break;
113
+ if (line.startsWith("#") || line.startsWith("---"))
114
+ break;
115
+ para.push(line.trim());
116
+ i++;
117
+ }
118
+ return para.join(" ").trim().slice(0, 500);
119
+ }
120
+ function extractRelatedFeatures(text) {
121
+ const lines = text.split(/\r?\n/);
122
+ let i = 0;
123
+ while (i < lines.length) {
124
+ if (/^##\s+related\s+features\b/i.test(lines[i] ?? ""))
125
+ break;
126
+ i++;
127
+ }
128
+ if (i === lines.length)
129
+ return [];
130
+ i++;
131
+ const out = [];
132
+ while (i < lines.length) {
133
+ const line = lines[i] ?? "";
134
+ if (line.startsWith("#") || line.startsWith("---"))
135
+ break;
136
+ const m = /^[-*]\s+`?([a-z0-9][a-z0-9-]*)`?(?:\s|$)/i.exec(line.trim());
137
+ if (m && m[1])
138
+ out.push(m[1].toLowerCase());
139
+ i++;
140
+ }
141
+ return out;
142
+ }
143
+ function normaliseName(name) {
144
+ return name.toLowerCase().replace(/\s+/g, " ").trim();
145
+ }
146
+ function titleCase(slug) {
147
+ return slug
148
+ .split("-")
149
+ .map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
150
+ .join(" ");
151
+ }
152
+ //# sourceMappingURL=personas.js.map
@@ -0,0 +1,106 @@
1
+ // Product-edge builders — emit:
2
+ // - product_capability (product depends on / consumes capability)
3
+ // - product_service (product depends on / consumes service)
4
+ //
5
+ // Source: `app-platform-dependencies.yaml` at the SDD root for
6
+ // application-product profile SDDs. Per the schema, both edges'
7
+ // referenced columns are external FKs (capability_id and service_id
8
+ // are owned by the platform-root SDD, not by the consuming app SDD).
9
+ // The per-SDD builder logs `[finding] external` for each unresolved
10
+ // reference; the aggregator validates after merge per ADR 0134 §4.3.
11
+ //
12
+ // Profile gating: only application-product SDDs read this file. Other
13
+ // profiles emit zero rows (no convention defined).
14
+ import { readFile } from "node:fs/promises";
15
+ import { join } from "node:path";
16
+ import { parse as parseYaml } from "yaml";
17
+ import { z } from "zod";
18
+ const REL_PATH = "app-platform-dependencies.yaml";
19
+ const DependencyEntrySchema = z.object({
20
+ capability_id: z.string().optional(),
21
+ service_id: z.string().optional(),
22
+ target_locus: z.string().optional(),
23
+ required_stability: z.string().optional(),
24
+ notes: z.string().optional(),
25
+ });
26
+ const FileSchema = z.object({
27
+ app_id: z.string().optional(),
28
+ platform_dependencies: z.array(DependencyEntrySchema).optional(),
29
+ });
30
+ export async function buildProductEdges(inputs) {
31
+ if (inputs.profile !== "application-product") {
32
+ return { productCapability: [], productService: [] };
33
+ }
34
+ const path = join(inputs.sddPath, REL_PATH);
35
+ let text;
36
+ try {
37
+ text = await readFile(path, "utf-8");
38
+ }
39
+ catch (err) {
40
+ if (err.code !== "ENOENT")
41
+ throw err;
42
+ inputs.findings.add("coverage", `${REL_PATH} not found — emitting empty product_capability + product_service`);
43
+ return { productCapability: [], productService: [] };
44
+ }
45
+ let raw;
46
+ try {
47
+ raw = parseYaml(text);
48
+ }
49
+ catch (err) {
50
+ inputs.findings.add("convention", `${REL_PATH}: failed to parse YAML — ${err.message}`);
51
+ return { productCapability: [], productService: [] };
52
+ }
53
+ const parsed = FileSchema.safeParse(raw);
54
+ if (!parsed.success) {
55
+ inputs.findings.add("convention", `${REL_PATH}: shape violation — ${parsed.error.issues.map((i) => i.path.join(".") + ": " + i.message).join("; ")}`);
56
+ return { productCapability: [], productService: [] };
57
+ }
58
+ const productCapability = [];
59
+ const productService = [];
60
+ const seenCap = new Set();
61
+ const seenSvc = new Set();
62
+ for (const dep of parsed.data.platform_dependencies ?? []) {
63
+ // M-ADR 0204: an off-platform entry produces NO platform edges — its
64
+ // service_id slot holds an external-system id, and emitting it into
65
+ // product_service would recreate at this table the dangling reference the
66
+ // locus field exists to prevent. The dependency is fully represented in
67
+ // product_platform_dependencies with its locus.
68
+ const locusRaw = (dep.target_locus ?? "").trim().toLowerCase();
69
+ if (locusRaw === "external" || locusRaw === "legacy")
70
+ continue;
71
+ const capabilityId = (dep.capability_id ?? "").trim();
72
+ const serviceId = (dep.service_id ?? "").trim();
73
+ const requiredStability = (dep.required_stability ?? "").trim();
74
+ const notes = (dep.notes ?? "").trim();
75
+ if (capabilityId !== "") {
76
+ const id = `${inputs.productId}__${capabilityId}`;
77
+ if (!seenCap.has(id)) {
78
+ seenCap.add(id);
79
+ productCapability.push({
80
+ id,
81
+ product_id: inputs.productId,
82
+ capability_id: capabilityId,
83
+ role: "consumes",
84
+ notes,
85
+ });
86
+ inputs.findings.add("external", `product_capability: ${id} → capability_id="${capabilityId}" (external FK; aggregator validates)`);
87
+ }
88
+ }
89
+ if (serviceId !== "") {
90
+ const id = `${inputs.productId}__${serviceId}`;
91
+ if (!seenSvc.has(id)) {
92
+ seenSvc.add(id);
93
+ productService.push({
94
+ id,
95
+ product_id: inputs.productId,
96
+ service_id: serviceId,
97
+ role: "consumes",
98
+ required_stability: requiredStability,
99
+ });
100
+ inputs.findings.add("external", `product_service: ${id} → service_id="${serviceId}" (external FK; aggregator validates)`);
101
+ }
102
+ }
103
+ }
104
+ return { productCapability, productService };
105
+ }
106
+ //# sourceMappingURL=product-edges.js.map
@@ -0,0 +1,405 @@
1
+ // product_platform_dependencies builder — emits per-SDD rows recording a
2
+ // product's consumption of a platform-side (capability, service) pair.
3
+ //
4
+ // Multi-source ingestion (ADR 0157 parity principle; same shape as ADR 0156
5
+ // events ingestion). Two equal-status authoring sources, merged by PK:
6
+ //
7
+ // 1. FORWARD — `app-platform-dependencies.yaml` at the SDD root, the same
8
+ // file `product-edges.ts` already reads for product_capability /
9
+ // product_service. Its `platform_dependencies:` entries carry
10
+ // capability_id + service_id (+ required_stability), which is exactly a
11
+ // (product, target-capability, target-service) triple. Forward-method
12
+ // consuming SDDs author this today; pre-fix the builder read it into
13
+ // product_capability / product_service but never into this edge.
14
+ //
15
+ // 2. REVERSE — `reverse-evidence/feature-grounding-evidence.yaml`'s
16
+ // `platform_dependencies:` block (the original schema-2.9.0 source),
17
+ // richer shape with domain / contract_type / contract_path / consumed_by.
18
+ //
19
+ // platform_dependencies:
20
+ // - capability_id: <target-capability-slug> # platform-side capability
21
+ // service_id: <target-service-slug> # platform-side service
22
+ // domain: <domain-hint> # optional (reverse shape)
23
+ // contract_type: rest | graphql | openapi | grpc | event | other
24
+ // required_stability: locked | provisional | unknown
25
+ // contract_path: <relative-path-in-platform-sdd> # optional (reverse)
26
+ // consumed_by: # optional (reverse)
27
+ // - <service-name-within-this-product>
28
+ //
29
+ // Each entry emits one row keyed by `(product_id, target_capability_id,
30
+ // target_service_id)` — or, for an ASYNC shared-stream entry with no realiser
31
+ // service (M-ADR 0192), `(product_id, target_capability_id, contract_path)`.
32
+ // The aggregator subsequently joins synchronous rows against platform-side
33
+ // `capability_service` to derive `cross_sdd_platform_capability` edges (per
34
+ // ADR 0134, federation); async rows resolve via the capability leg (no
35
+ // realiser service to join on) and skip the F18 realiser-join.
36
+ //
37
+ // Cross-SDD references (target_capability_id, target_service_id) are
38
+ // declared `external: true` in the schema — per-SDD FK validation
39
+ // doesn't run them.
40
+ //
41
+ // Merge rule:
42
+ // - Forward (app-platform-dependencies.yaml) is read first and WINS on PK
43
+ // conflict (matches per-adopter authoring intent). A reverse entry whose
44
+ // PK was already claimed by forward emits `divergence` and is dropped.
45
+ // - Within a single source, a duplicate PK emits `divergence` and keeps
46
+ // the first.
47
+ // - Reverse: missing capability_id → skip, log `convention`. A synchronous
48
+ // entry missing service_id → skip, log `convention`. An async entry
49
+ // (contract_type in the M-ADR 0192 set) may omit service_id but must carry
50
+ // contract_path to key on, else `convention`. Forward: a synchronous entry
51
+ // with only the capability is a valid product_capability / product_service
52
+ // row but cannot form this edge — skipped silently; an async entry missing
53
+ // its contract_path logs `convention`.
54
+ //
55
+ // Schema 2.36.0: target_service_id is conditionally nullable — empty for async
56
+ // shared-stream rows, which key on contract_path (M-ADR 0192).
57
+ import { readFile, readdir, stat } from "node:fs/promises";
58
+ import { join, relative, sep } from "node:path";
59
+ import { parse as parseYaml } from "yaml";
60
+ const FORWARD_REL_PATH = "app-platform-dependencies.yaml";
61
+ /**
62
+ * Contract types that denote an ASYNC shared-stream dependency (M-ADR 0192).
63
+ * A dependency of one of these kinds binds to a stream contract published by a
64
+ * capability, not to a synchronous realiser service — so it characteristically
65
+ * has no `service_id`, and the F18 realiser-join does not apply to it at
66
+ * aggregation. Kept lowercase; callers normalise before testing.
67
+ */
68
+ export const ASYNC_CONTRACT_TYPES = new Set([
69
+ "event",
70
+ "event-stream",
71
+ "stream",
72
+ ]);
73
+ /** True when `contractType` denotes an async shared-stream dependency (M-ADR 0192). */
74
+ export function isAsyncContractType(contractType) {
75
+ return ASYNC_CONTRACT_TYPES.has(contractType.trim().toLowerCase());
76
+ }
77
+ // M-ADR 0204: closed three-value enum. `platform` (or absent) keeps every
78
+ // pre-0204 behaviour; external/legacy rows target something OFF-platform, carry
79
+ // empty FK columns, and name their target in target_external_system_id instead.
80
+ export const TARGET_LOCUS_VALUES = new Set([
81
+ "platform",
82
+ "external",
83
+ "legacy",
84
+ ]);
85
+ /**
86
+ * Resolve an entry's target_locus. Invalid values are reported and treated as
87
+ * `platform` — the SAFEST reading, because a platform row is fully validated
88
+ * (FKs, resolution, realiser join); treating an unknown locus as off-platform
89
+ * would silently exempt the row from every check, which is the failure mode
90
+ * this whole field exists to end.
91
+ */
92
+ export function resolveTargetLocus(raw, findings, where) {
93
+ const v = String(raw ?? "").trim().toLowerCase();
94
+ if (v === "")
95
+ return "platform";
96
+ if (TARGET_LOCUS_VALUES.has(v))
97
+ return v;
98
+ findings.add("convention", `${where}: invalid target_locus "${v}" (allowed: platform | external | legacy) — treated as platform, so the row stays fully validated`);
99
+ return "platform";
100
+ }
101
+ /**
102
+ * Emit an off-platform (M-ADR 0204 external|legacy) row. FK columns stay
103
+ * EMPTY — the target is not the platform, so nothing may join or FK-validate
104
+ * against it; the target's identity goes to target_external_system_id (the
105
+ * authored service_id slot, routed). A capability_id present on such an entry
106
+ * is reported and dropped rather than carried: §4.3, and the measured instance
107
+ * where that slot held the product's own feature slug is why.
108
+ * Returns the row id, or null (with a finding) when the entry binds to nothing.
109
+ */
110
+ function buildOffPlatformRow(inputs, locus, dep, where) {
111
+ const capabilityId = String(dep["capability_id"] ?? "").trim();
112
+ const externalId = String(dep["service_id"] ?? "").trim();
113
+ const contractPath = optString(dep["contract_path"]);
114
+ if (capabilityId !== "") {
115
+ inputs.findings.add("convention", `${where}: target_locus="${locus}" entry carries capability_id="${capabilityId}" — an off-platform target has no platform capability (M-ADR 0204 §4.3); dropped from the row, keep the intent in notes`);
116
+ }
117
+ const keyLeg = externalId !== "" ? externalId : contractPath;
118
+ if (keyLeg === "") {
119
+ inputs.findings.add("convention", `${where}: target_locus="${locus}" entry names neither an external-system id (service_id) nor a contract_path — it binds to nothing and cannot be recorded`);
120
+ return null;
121
+ }
122
+ const consumedBy = dep["consumed_by"];
123
+ const consumedByJoined = Array.isArray(consumedBy)
124
+ ? consumedBy.map((v) => String(v).trim()).filter((v) => v !== "").join(",")
125
+ : typeof consumedBy === "string"
126
+ ? consumedBy.trim()
127
+ : "";
128
+ return {
129
+ id: `${inputs.productId}__${locus}__${keyLeg}`,
130
+ product_id: inputs.productId,
131
+ target_capability_id: "",
132
+ target_locus: locus,
133
+ target_external_system_id: externalId,
134
+ target_service_id: "",
135
+ target_domain: optString(dep["domain"]),
136
+ contract_type: optString(dep["contract_type"]),
137
+ required_stability: optString(dep["required_stability"]),
138
+ contract_path: contractPath,
139
+ consumed_by_services: consumedByJoined,
140
+ source_sdd_id: "",
141
+ };
142
+ }
143
+ function resolveKeyLeg(serviceId, contractType, contractPath) {
144
+ if (serviceId !== "")
145
+ return { ok: true, keyLeg: serviceId };
146
+ if (!isAsyncContractType(contractType)) {
147
+ return { ok: false, reason: "sync-single-sided" };
148
+ }
149
+ if (contractPath === "")
150
+ return { ok: false, reason: "async-no-contract-path" };
151
+ return { ok: true, keyLeg: contractPath };
152
+ }
153
+ export async function buildProductPlatformDependencies(inputs) {
154
+ const rows = [];
155
+ // Tracks which source claimed each PK, so divergence messages are accurate.
156
+ const seen = new Map();
157
+ if (inputs.productId === "") {
158
+ // No product id means no rows can be emitted — product_platform_dependencies
159
+ // is product-scoped. Return empty silently (the orchestrator decides
160
+ // whether to log this as a finding).
161
+ return rows;
162
+ }
163
+ // 1. FORWARD source — app-platform-dependencies.yaml at SDD root.
164
+ await readForwardSource(inputs, rows, seen);
165
+ // 2. REVERSE source — reverse-evidence/feature-grounding-evidence.yaml.
166
+ const evidenceFiles = await collectEvidenceFiles(inputs.sddPath);
167
+ for (const file of evidenceFiles) {
168
+ const relPath = relative(inputs.sddPath, file);
169
+ let text;
170
+ try {
171
+ text = await readFile(file, "utf-8");
172
+ }
173
+ catch {
174
+ inputs.findings.add("convention", `${relPath}: cannot read evidence file`);
175
+ continue;
176
+ }
177
+ let parsed;
178
+ try {
179
+ parsed = parseYaml(text);
180
+ }
181
+ catch (err) {
182
+ const msg = err instanceof Error ? err.message : String(err);
183
+ inputs.findings.add("convention", `${relPath}: invalid YAML — ${msg}`);
184
+ continue;
185
+ }
186
+ if (!parsed || typeof parsed !== "object")
187
+ continue;
188
+ const deps = parsed["platform_dependencies"];
189
+ if (!Array.isArray(deps))
190
+ continue;
191
+ for (const d of deps) {
192
+ if (!d || typeof d !== "object")
193
+ continue;
194
+ const dep = d;
195
+ const locus = resolveTargetLocus(dep["target_locus"], inputs.findings, relPath);
196
+ if (locus !== "platform") {
197
+ const offRow = buildOffPlatformRow(inputs, locus, dep, relPath);
198
+ if (offRow !== null) {
199
+ const claimed = seen.get(String(offRow.id));
200
+ if (claimed !== undefined) {
201
+ inputs.findings.add("divergence", `${relPath}: duplicate platform_dependencies row id="${offRow.id}"`);
202
+ }
203
+ else {
204
+ seen.set(String(offRow.id), "reverse");
205
+ rows.push(offRow);
206
+ }
207
+ }
208
+ continue;
209
+ }
210
+ const capabilityId = String(dep["capability_id"] ?? "").trim();
211
+ const serviceId = String(dep["service_id"] ?? "").trim();
212
+ const contractType = optString(dep["contract_type"]);
213
+ const contractPath = optString(dep["contract_path"]);
214
+ if (capabilityId === "") {
215
+ inputs.findings.add("convention", `${relPath}: platform_dependencies entry missing required capability_id (service_id="${serviceId}")`);
216
+ continue;
217
+ }
218
+ // M-ADR 0192: an async shared-stream entry legitimately has no service_id
219
+ // (no single realiser) and keys on its stream contract_path instead.
220
+ const keyRes = resolveKeyLeg(serviceId, contractType, contractPath);
221
+ if (!keyRes.ok) {
222
+ inputs.findings.add("convention", keyRes.reason === "async-no-contract-path"
223
+ ? `${relPath}: async platform_dependencies entry for capability "${capabilityId}" (contract_type="${contractType}") has neither service_id nor contract_path — cannot key the stream dependency`
224
+ : `${relPath}: platform_dependencies entry for capability "${capabilityId}" missing required service_id (a synchronous dependency must name its realiser service)`);
225
+ continue;
226
+ }
227
+ const rowId = `${inputs.productId}__${capabilityId}__${keyRes.keyLeg}`;
228
+ const claimedBy = seen.get(rowId);
229
+ if (claimedBy === "forward") {
230
+ inputs.findings.add("divergence", `${relPath}: platform_dependencies id="${rowId}" also declared forward in ${FORWARD_REL_PATH} — forward wins; reverse entry dropped`);
231
+ continue;
232
+ }
233
+ if (claimedBy === "reverse") {
234
+ inputs.findings.add("divergence", `${relPath}: duplicate platform_dependencies row id="${rowId}" — same (product_id, target_capability_id, target_service_id) already emitted`);
235
+ continue;
236
+ }
237
+ seen.set(rowId, "reverse");
238
+ const consumedBy = dep["consumed_by"];
239
+ const consumedByJoined = Array.isArray(consumedBy)
240
+ ? consumedBy
241
+ .map((v) => String(v).trim())
242
+ .filter((v) => v !== "")
243
+ .join(",")
244
+ : typeof consumedBy === "string"
245
+ ? consumedBy.trim()
246
+ : "";
247
+ rows.push({
248
+ id: rowId,
249
+ product_id: inputs.productId,
250
+ target_capability_id: capabilityId,
251
+ target_locus: "platform",
252
+ target_external_system_id: "",
253
+ target_service_id: serviceId,
254
+ target_domain: optString(dep["domain"]),
255
+ contract_type: contractType,
256
+ required_stability: optString(dep["required_stability"]),
257
+ contract_path: contractPath,
258
+ consumed_by_services: consumedByJoined,
259
+ source_sdd_id: "",
260
+ });
261
+ }
262
+ }
263
+ return rows;
264
+ }
265
+ /**
266
+ * Forward source: `app-platform-dependencies.yaml` at the SDD root. Reads its
267
+ * `platform_dependencies:` block — the same block product-edges.ts consumes
268
+ * for product_capability / product_service. Only entries carrying BOTH
269
+ * capability_id and service_id can form this edge; entries with just one are
270
+ * valid product_capability / product_service rows and are skipped silently
271
+ * here. File presence (not profile) is the gate — the file only exists in
272
+ * consuming SDDs.
273
+ */
274
+ async function readForwardSource(inputs, rows, seen) {
275
+ const path = join(inputs.sddPath, FORWARD_REL_PATH);
276
+ let text;
277
+ try {
278
+ text = await readFile(path, "utf-8");
279
+ }
280
+ catch (err) {
281
+ if (err.code === "ENOENT")
282
+ return;
283
+ inputs.findings.add("convention", `${FORWARD_REL_PATH}: cannot read file`);
284
+ return;
285
+ }
286
+ let parsed;
287
+ try {
288
+ parsed = parseYaml(text);
289
+ }
290
+ catch (err) {
291
+ const msg = err instanceof Error ? err.message : String(err);
292
+ inputs.findings.add("convention", `${FORWARD_REL_PATH}: invalid YAML — ${msg}`);
293
+ return;
294
+ }
295
+ if (!parsed || typeof parsed !== "object")
296
+ return;
297
+ const deps = parsed["platform_dependencies"];
298
+ if (!Array.isArray(deps))
299
+ return;
300
+ for (const d of deps) {
301
+ if (!d || typeof d !== "object")
302
+ continue;
303
+ const dep = d;
304
+ const locus = resolveTargetLocus(dep["target_locus"], inputs.findings, FORWARD_REL_PATH);
305
+ if (locus !== "platform") {
306
+ const offRow = buildOffPlatformRow(inputs, locus, dep, FORWARD_REL_PATH);
307
+ if (offRow !== null) {
308
+ if (seen.has(String(offRow.id))) {
309
+ inputs.findings.add("divergence", `${FORWARD_REL_PATH}: duplicate platform_dependencies row id="${offRow.id}"`);
310
+ }
311
+ else {
312
+ seen.set(String(offRow.id), "forward");
313
+ rows.push(offRow);
314
+ }
315
+ }
316
+ continue;
317
+ }
318
+ const capabilityId = String(dep["capability_id"] ?? "").trim();
319
+ const serviceId = String(dep["service_id"] ?? "").trim();
320
+ const contractType = optString(dep["contract_type"]);
321
+ const contractPath = optString(dep["contract_path"]);
322
+ // Entry needs a capability plus a key leg to form this edge. A synchronous
323
+ // entry keys on service_id; a single-sided synchronous entry is a valid
324
+ // product_capability / product_service row (emitted by product-edges.ts)
325
+ // and is skipped silently here. An async shared-stream entry (M-ADR 0192)
326
+ // keys on its stream contract_path instead of a realiser service.
327
+ if (capabilityId === "")
328
+ continue;
329
+ const keyRes = resolveKeyLeg(serviceId, contractType, contractPath);
330
+ if (!keyRes.ok) {
331
+ if (keyRes.reason === "async-no-contract-path") {
332
+ inputs.findings.add("convention", `${FORWARD_REL_PATH}: async platform_dependencies entry for capability "${capabilityId}" (contract_type="${contractType}") has neither service_id nor contract_path — cannot key the stream dependency`);
333
+ }
334
+ // sync-single-sided → skip silently (legitimate product_capability /
335
+ // product_service row, not an error).
336
+ continue;
337
+ }
338
+ const rowId = `${inputs.productId}__${capabilityId}__${keyRes.keyLeg}`;
339
+ if (seen.has(rowId)) {
340
+ inputs.findings.add("divergence", `${FORWARD_REL_PATH}: duplicate platform_dependencies row id="${rowId}" — same (product_id, target_capability_id, target_service_id/contract_path) already emitted`);
341
+ continue;
342
+ }
343
+ seen.set(rowId, "forward");
344
+ rows.push({
345
+ id: rowId,
346
+ product_id: inputs.productId,
347
+ target_capability_id: capabilityId,
348
+ target_locus: "platform",
349
+ target_external_system_id: "",
350
+ target_service_id: serviceId,
351
+ target_domain: optString(dep["domain"]),
352
+ contract_type: contractType,
353
+ required_stability: optString(dep["required_stability"]),
354
+ contract_path: contractPath,
355
+ consumed_by_services: "",
356
+ source_sdd_id: "",
357
+ });
358
+ }
359
+ }
360
+ function optString(value) {
361
+ return typeof value === "string" ? value.trim() : "";
362
+ }
363
+ async function collectEvidenceFiles(sddPath) {
364
+ const out = [];
365
+ await walk(sddPath, sddPath, out);
366
+ return out;
367
+ }
368
+ async function walk(root, current, out) {
369
+ let entries;
370
+ try {
371
+ entries = await readdir(current, { withFileTypes: true });
372
+ }
373
+ catch {
374
+ return;
375
+ }
376
+ for (const e of entries) {
377
+ if (e.name === "node_modules" ||
378
+ e.name === ".git" ||
379
+ e.name === "source" ||
380
+ e.name === "catalogues" ||
381
+ e.name === "dist") {
382
+ continue;
383
+ }
384
+ const abs = join(current, e.name);
385
+ if (e.isDirectory()) {
386
+ if (e.name === "reverse-evidence") {
387
+ const candidate = join(abs, "feature-grounding-evidence.yaml");
388
+ try {
389
+ const s = await stat(candidate);
390
+ if (s.isFile())
391
+ out.push(candidate);
392
+ }
393
+ catch {
394
+ // No file in this reverse-evidence dir — skip silently.
395
+ }
396
+ continue;
397
+ }
398
+ const rel = relative(root, abs);
399
+ if (rel.split(sep).length > 8)
400
+ continue;
401
+ await walk(root, abs, out);
402
+ }
403
+ }
404
+ }
405
+ //# sourceMappingURL=product-platform-dependencies.js.map