@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,346 @@
1
+ /**
2
+ * Graph projection — `sdd-cli catalogue graph` (M-ADR 0205 §4.6, Phase C0b).
3
+ *
4
+ * Derives a typed node/edge list (JSON Lines) from built catalogues. The
5
+ * projection is DERIVED, never authored, never a second source of truth:
6
+ * regenerated on every run, digest-stamped, not committed. The derivation
7
+ * rule is the C0a rule table (catalogue-engine-evolution plan), keyed on
8
+ * the rubric-coverage-map's `kind` × the schema's FK arity:
9
+ *
10
+ * entity (less `applications`) → nodes, id = "<catalogue>/<pk>"
11
+ * entity FK columns → edges typed "<catalogue>.<column>"
12
+ * edge, 2 FKs → edges, declaration order = source→target;
13
+ * a row with an empty nullable endpoint
14
+ * degrades to a property record, never an edge
15
+ * edge, 3 FKs → edges with declared endpoint pairs
16
+ * (THREE_FK_ENDPOINTS below)
17
+ * edge, 1 FK → property records on the FK owner
18
+ * `applications` → excluded (C0a ruling 2026-08-06: filtered
19
+ * view of products — its rows ARE product
20
+ * rows; emitting it creates edge-less
21
+ * shadow nodes beside the products nodes)
22
+ * finding / enum kinds → excluded (diagnostics / vocabularies)
23
+ *
24
+ * Emission reads the PARSED CSV ROWS, deliberately not an engine readback:
25
+ * the engine's load TRY_CASTs typed columns and silently nulls unparseable
26
+ * cells (a real, measured case: prose in a date-typed column). A projection
27
+ * that inherits cast smoothing is no longer a faithful derivation of the
28
+ * CSV canon. C1's impact queries run on the engine; this emission does not.
29
+ *
30
+ * Every edge endpoint and property owner MUST resolve to a node record in
31
+ * the same export — a dangling reference here is a defect in the projection,
32
+ * not a finding about the estate (the FK layer already reports those). A
33
+ * candidate whose endpoint does not resolve is OMITTED and COUNTED, loudly,
34
+ * in the header and summary: `omitted_external` for external /
35
+ * external-for-profiles FKs (expected at member tier, resolving at
36
+ * aggregate), `omitted_unresolved` for the rest (mirrors an FK-layer
37
+ * finding). Stub nodes are never emitted — inventing nodes is the failure
38
+ * mode the C0a rule exists to prevent.
39
+ */
40
+ import { createHash } from "node:crypto";
41
+ /** Node-kind catalogues excluded from projection by C0a ruling. */
42
+ const EXCLUDED_NODE_CATALOGUES = new Set(["applications"]);
43
+ /**
44
+ * Declared endpoint pairs for the three 3-FK edge catalogues (C0a).
45
+ * `product_platform_dependencies` is SERVICE-FIRST — the aggregator already
46
+ * derives product→capability (cross_sdd_platform_capability) by joining
47
+ * target_service_id, so a capability-first rule would emit the same
48
+ * relationship twice; its resolution is per-row in projectRow below.
49
+ */
50
+ const THREE_FK_ENDPOINTS = {
51
+ system_external_dependency: ["c4_system_id", "external_system_id"],
52
+ cross_sdd_platform_capability: [
53
+ "consuming_product_id",
54
+ "platform_capability_id",
55
+ ],
56
+ product_platform_dependencies: null, // per-row: target_service_id, else target_capability_id
57
+ // schema 3.9.0 (M-ADR 0211 §4.1): the third FK, contract_id, is a PROPERTY of the
58
+ // capability→service dependency — the contract it is against — not an endpoint. The
59
+ // edge stays capability→service; the contract rides as a property.
60
+ capability_service: ["capability_id", "service_id"],
61
+ // schema 3.9.0 (M-ADR 0211) added entity_id as a third FK. M-ADR 0158 decides the
62
+ // edge: "entity_glossary is entity-to-term". domain_id is the row's scope, a
63
+ // property; a row with no entity_id degrades to a property of its glossary term.
64
+ entity_glossary: ["entity_id", "glossary_item_id"],
65
+ // schema 3.18.0 (M-ADR 0216): coverage is feature→integration; limiting_item_id is the
66
+ // PROPERTY that says why an unsupported pair is unsupported, not an endpoint.
67
+ feature_integration_support: ["feature_id", "integration_id"],
68
+ };
69
+ /**
70
+ * The catalogues a tier reads when no directory is named. The aggregate tier's
71
+ * data is the aggregator's output, `catalogues-aggregated/`: defaulting it to the
72
+ * member `catalogues/` made `--tier aggregate` run the aggregate rules over MEMBER
73
+ * rows — measured at a platform root, 1 reachable node instead of 2,316, a clean-
74
+ * looking miss of every cross-SDD consumer.
75
+ */
76
+ export function defaultCataloguesDir(tier) {
77
+ return tier === "aggregate" ? "catalogues-aggregated" : "catalogues";
78
+ }
79
+ /**
80
+ * Kinds must cover every schema catalogue — a catalogue without a kind
81
+ * cannot be classified and the projection must not guess (the map is
82
+ * guarded method-side by the entity-scoring-coverage correspondence
83
+ * instance; this is the consumer-side half of that guarantee).
84
+ */
85
+ export function assertKindCoverage(schema, kinds) {
86
+ const missing = schema.catalogues
87
+ .map((c) => c.name)
88
+ .filter((n) => !kinds.has(n));
89
+ if (missing.length > 0) {
90
+ throw new Error(`rubric-coverage-map has no kind for: ${missing.join(", ")} — ` +
91
+ `sync the vendored map (it must cover every schema catalogue).`);
92
+ }
93
+ }
94
+ export function projectGraph(schema, kinds, rowsByCatalogue, tier,
95
+ /**
96
+ * When provided (M-ADR 0206 §4.6), every edge record is stamped with its
97
+ * declared `class` and RESOLVED `impact_flow` (role-conditional flows
98
+ * resolve per row), so consumers need no map lookup. Additive fields; the
99
+ * export regenerates per run, so the format change costs one restated
100
+ * determinism proof.
101
+ */
102
+ semantics) {
103
+ assertKindCoverage(schema, kinds);
104
+ const stampFor = (type, props) => {
105
+ if (semantics === undefined)
106
+ return {};
107
+ const decl = type.includes(".")
108
+ ? semantics.entityFks.get(type)
109
+ : semantics.edges.get(type);
110
+ if (decl === undefined) {
111
+ throw new Error(`graph: relationship ${type} has no declaration in the ` +
112
+ `rubric-coverage-map (M-ADR 0206) — sync the vendored map.`);
113
+ }
114
+ const flow = decl.impactFlow ??
115
+ decl.impactFlowBy.values[(props[decl.impactFlowBy.column] ?? "").trim()];
116
+ if (flow === undefined) {
117
+ throw new Error(`graph: ${type} resolves flow by column '${decl.impactFlowBy.column}' ` +
118
+ `but the row value is not in the declared map.`);
119
+ }
120
+ return { class: decl.class, impact_flow: flow };
121
+ };
122
+ const nodeCats = schema.catalogues.filter((c) => kinds.get(c.name) === "entity" && !EXCLUDED_NODE_CATALOGUES.has(c.name));
123
+ // `annotation` (M-ADR 0133 §4.2b; kinds corrected at schema 3.9.0): a single-FK assertion
124
+ // ABOUT one entity. It takes the same path as a 1-FK edge — a property record on the FK
125
+ // owner — so it joins the edge set here rather than vanishing from the projection.
126
+ const edgeCats = schema.catalogues.filter((c) => kinds.get(c.name) === "edge" || kinds.get(c.name) === "annotation");
127
+ // Pass 1 — the node set. Endpoint resolution below is against THIS
128
+ // export's nodes, which is exactly the per-export invariant.
129
+ const nodeIds = new Set();
130
+ const nodeLines = [];
131
+ for (const cat of nodeCats) {
132
+ for (const row of rowsByCatalogue.get(cat.name) ?? []) {
133
+ const key = cell(row, cat.primaryKey);
134
+ const id = `${cat.name}/${key}`;
135
+ nodeIds.add(id);
136
+ nodeLines.push(jsonLine({
137
+ t: "node",
138
+ id,
139
+ catalogue: cat.name,
140
+ key,
141
+ properties: propsOf(cat, row, new Set([cat.primaryKey])),
142
+ }));
143
+ }
144
+ }
145
+ const resolveTarget = (fk, value) => {
146
+ const [refCat, refCol] = fk.references.split(".");
147
+ // The rule resolves FK values against node PKs; a reference to a
148
+ // non-PK column would resolve against the wrong namespace. All 127
149
+ // parsed FKs reference `.id` today — guard it rather than assume it.
150
+ const refDef = schema.catalogues.find((c) => c.name === refCat);
151
+ if (refDef && refDef.primaryKey !== refCol) {
152
+ throw new Error(`FK ${fk.column} references non-PK column ${fk.references} — ` +
153
+ `the projection cannot resolve non-PK references.`);
154
+ }
155
+ const target = `${refCat}/${value}`;
156
+ return nodeIds.has(target) ? target : null;
157
+ };
158
+ const isExternal = (fk) => fk.external || (fk.externalForProfiles ?? []).length > 0;
159
+ const edgeLines = [];
160
+ const propertyLines = [];
161
+ const omissions = { external: 0, unresolved: 0 };
162
+ const emitEdge = (cat, row, type, sourceFk, targetFk) => {
163
+ const sv = cell(row, sourceFk.column);
164
+ const tv = cell(row, targetFk.column);
165
+ const source = resolveTarget(sourceFk, sv);
166
+ const target = resolveTarget(targetFk, tv);
167
+ if (source === null || target === null) {
168
+ const failedFk = source === null ? sourceFk : targetFk;
169
+ if (isExternal(failedFk))
170
+ omissions.external += 1;
171
+ else
172
+ omissions.unresolved += 1;
173
+ return;
174
+ }
175
+ const props = propsOf(cat, row, new Set([sourceFk.column, targetFk.column]));
176
+ edgeLines.push(jsonLine({
177
+ t: "edge",
178
+ type,
179
+ ...stampFor(type, props),
180
+ source,
181
+ target,
182
+ properties: props,
183
+ }));
184
+ };
185
+ const emitProperty = (cat, row, ownerFk) => {
186
+ const ov = cell(row, ownerFk.column);
187
+ const owner = ov === "" ? null : resolveTarget(ownerFk, ov);
188
+ if (owner === null) {
189
+ if (ov !== "" && isExternal(ownerFk))
190
+ omissions.external += 1;
191
+ else
192
+ omissions.unresolved += 1;
193
+ return;
194
+ }
195
+ propertyLines.push(jsonLine({
196
+ t: "property",
197
+ owner,
198
+ catalogue: cat.name,
199
+ via: ownerFk.column,
200
+ properties: propsOf(cat, row, new Set([ownerFk.column])),
201
+ }));
202
+ };
203
+ // Pass 2a — edges from edge catalogues, schema declaration order.
204
+ for (const cat of edgeCats) {
205
+ const fks = cat.foreignKeys;
206
+ for (const row of rowsByCatalogue.get(cat.name) ?? []) {
207
+ if (fks.length === 1) {
208
+ // Attribute table the metamodel calls an edge: the other "end" is
209
+ // an enum/key/name — a property record, never an edge.
210
+ emitProperty(cat, row, fks[0]);
211
+ continue;
212
+ }
213
+ let sourceFk;
214
+ let targetFk;
215
+ if (fks.length === 2) {
216
+ [sourceFk, targetFk] = fks;
217
+ }
218
+ else if (cat.name === "product_platform_dependencies") {
219
+ sourceFk = fks.find((f) => f.column === "product_id");
220
+ const svc = cell(row, "target_service_id");
221
+ const cap = cell(row, "target_capability_id");
222
+ const targetCol = svc !== ""
223
+ ? "target_service_id"
224
+ : cap !== ""
225
+ ? "target_capability_id"
226
+ : null;
227
+ if (targetCol === null) {
228
+ // Neither target declared: property-only row on the product.
229
+ emitProperty(cat, row, sourceFk);
230
+ continue;
231
+ }
232
+ targetFk = fks.find((f) => f.column === targetCol);
233
+ }
234
+ else {
235
+ const pair = THREE_FK_ENDPOINTS[cat.name];
236
+ if (pair === undefined || pair === null) {
237
+ throw new Error(`edge catalogue ${cat.name} has ${fks.length} FKs and no declared ` +
238
+ `endpoint pair — extend THREE_FK_ENDPOINTS (a C0a-level decision, ` +
239
+ `not a default).`);
240
+ }
241
+ sourceFk = fks.find((f) => f.column === pair[0]);
242
+ targetFk = fks.find((f) => f.column === pair[1]);
243
+ }
244
+ if (sourceFk === undefined || targetFk === undefined) {
245
+ throw new Error(`edge catalogue ${cat.name}: declared endpoint column missing from ` +
246
+ `schema foreign_keys — schema and rule table disagree.`);
247
+ }
248
+ const sv = cell(row, sourceFk.column);
249
+ const tv = cell(row, targetFk.column);
250
+ if (sv === "" || tv === "") {
251
+ // Empty nullable endpoint: degrade to a property record on the
252
+ // present endpoint (screen_action is the live case — measured
253
+ // 2026-08-06: all 85+1,652 rows on both estates have no event_id).
254
+ const present = sv !== "" ? sourceFk : tv !== "" ? targetFk : null;
255
+ if (present === null) {
256
+ omissions.unresolved += 1; // no endpoint at all — counted, never guessed
257
+ continue;
258
+ }
259
+ emitProperty(cat, row, present);
260
+ continue;
261
+ }
262
+ emitEdge(cat, row, cat.name, sourceFk, targetFk);
263
+ }
264
+ }
265
+ // Pass 2b — edges carried on node rows (41 FK columns on 22 catalogues;
266
+ // 34–40% of all edges on the measured estates — the third of the graph
267
+ // the naive "edge catalogues = edges" rule drops).
268
+ for (const cat of nodeCats) {
269
+ for (const fk of cat.foreignKeys) {
270
+ for (const row of rowsByCatalogue.get(cat.name) ?? []) {
271
+ const value = cell(row, fk.column);
272
+ if (value === "")
273
+ continue;
274
+ const source = `${cat.name}/${cell(row, cat.primaryKey)}`;
275
+ const target = resolveTarget(fk, value);
276
+ if (target === null) {
277
+ if (isExternal(fk))
278
+ omissions.external += 1;
279
+ else
280
+ omissions.unresolved += 1;
281
+ continue;
282
+ }
283
+ edgeLines.push(jsonLine({
284
+ t: "edge",
285
+ type: `${cat.name}.${fk.column}`,
286
+ ...stampFor(`${cat.name}.${fk.column}`, {}),
287
+ source,
288
+ target,
289
+ properties: {},
290
+ }));
291
+ }
292
+ }
293
+ }
294
+ const body = [...nodeLines, ...edgeLines, ...propertyLines];
295
+ const bodyText = body.map((l) => `${l}\n`).join("");
296
+ const digest = createHash("sha256").update(bodyText, "utf8").digest("hex");
297
+ const counts = {
298
+ nodes: nodeLines.length,
299
+ edges: edgeLines.length,
300
+ properties: propertyLines.length,
301
+ omittedExternal: omissions.external,
302
+ omittedUnresolved: omissions.unresolved,
303
+ };
304
+ const header = jsonLine({
305
+ t: "header",
306
+ format: "sdd-graph/1",
307
+ schema_version: schema.schemaVersion,
308
+ tier,
309
+ nodes: counts.nodes,
310
+ edges: counts.edges,
311
+ properties: counts.properties,
312
+ omitted_external: counts.omittedExternal,
313
+ omitted_unresolved: counts.omittedUnresolved,
314
+ digest: `sha256:${digest}`,
315
+ });
316
+ return { text: `${header}\n${bodyText}`, counts, digest };
317
+ }
318
+ /**
319
+ * Row columns as a properties object: schema column order (deterministic),
320
+ * empty string omitted (the schema's own convention — empty means absent),
321
+ * excluded columns (PK / endpoint / owner) carried structurally instead.
322
+ * Values stay the strings the CSV canon holds — no cast, no smoothing.
323
+ */
324
+ function propsOf(cat, row, exclude) {
325
+ const out = {};
326
+ for (const col of cat.columns) {
327
+ if (exclude.has(col.name))
328
+ continue;
329
+ const v = row[col.name];
330
+ const s = v === null || v === undefined ? "" : String(v);
331
+ if (s === "")
332
+ continue;
333
+ out[col.name] = s;
334
+ }
335
+ return out;
336
+ }
337
+ /** JSON.stringify with stable insertion-order keys — the whole line contract. */
338
+ function jsonLine(obj) {
339
+ return JSON.stringify(obj);
340
+ }
341
+ /** One cell as the trimmed string the CSV canon holds ('' = absent). */
342
+ function cell(row, column) {
343
+ const v = row[column];
344
+ return v === null || v === undefined ? "" : String(v).trim();
345
+ }
346
+ //# sourceMappingURL=graph-projection.js.map
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Transitive impact analysis — `sdd-cli catalogue impact` (M-ADR 0206 §4.6).
3
+ *
4
+ * Impact traverses the graph-projection edge set BY DECLARED SEMANTICS: every
5
+ * relationship carries an impact-flow token in the rubric-coverage-map
6
+ * (M-ADR 0206 §4.1) naming which endpoint RECEIVES impact — `toward-source`
7
+ * (dependency shape: a change at the target propagates to the source),
8
+ * `toward-target` (membership shape: a part's change reaches its whole),
9
+ * `both` (realisation), `none` (xref, lineage records, attributes). One
10
+ * relationship (`service_event`) resolves its flow per row from a named
11
+ * column (§4.2). The closure is the set of nodes impact can reach from the
12
+ * seed along declared propagation directions, computed by recursive SQL
13
+ * (`WITH RECURSIVE`) on the Phase A engine.
14
+ *
15
+ * THE CO-DIRECTIONALITY INVARIANT (§4.4, a condition of 0206's acceptance):
16
+ * the Phase C mirror-dedup heuristic is retired because under the declared
17
+ * tokens every true-mirror pair propagates the SAME direction. That premise
18
+ * is exercised, not assumed: buildImpactPropagation asserts that no node
19
+ * pair receives propagation in BOTH directions from two DIFFERENT
20
+ * single-direction relationship types, and fails loudly naming the pair and
21
+ * types. A single relationship declaring `both` is deliberate bidirectional
22
+ * coupling and is exempt; a self-referential type contributing both
23
+ * directions from different rows (a hierarchy cycle) is likewise exempt.
24
+ * A future token edit that would silently bring the undirected flood back
25
+ * trips this assertion instead.
26
+ *
27
+ * Cycle discipline: the recursion carries NO depth column — recursive UNION
28
+ * deduplicates whole rows, and a depth column makes every lap of a cycle a
29
+ * "new" row: the query that looks more informative is the query that never
30
+ * terminates. As a bare id-set the closure saturates and stops.
31
+ */
32
+ import { existsSync } from "node:fs";
33
+ import { join, resolve } from "node:path";
34
+ import { readCatalogueCsv } from "./csv-reader.js";
35
+ import { loadCanonicalSchema } from "./schema-loader.js";
36
+ import { loadCatalogueKinds, loadRelationshipSemantics, } from "./relationship-semantics.js";
37
+ import { defaultCataloguesDir, projectGraph, } from "./graph-projection.js";
38
+ function resolveFlow(edge, semantics) {
39
+ const decl = edge.type.includes(".")
40
+ ? semantics.entityFks.get(edge.type)
41
+ : semantics.edges.get(edge.type);
42
+ if (decl === undefined) {
43
+ throw new Error(`impact: relationship ${edge.type} has no declaration in the ` +
44
+ `rubric-coverage-map (M-ADR 0206) — sync the vendored map.`);
45
+ }
46
+ if (decl.impactFlow !== undefined)
47
+ return decl.impactFlow;
48
+ const by = decl.impactFlowBy;
49
+ const value = (edge.properties?.[by.column] ?? "").trim();
50
+ const flow = by.values[value];
51
+ if (flow === undefined) {
52
+ throw new Error(`impact: ${edge.type} resolves flow by column '${by.column}' but row ` +
53
+ `value '${value}' is not in the declared map — extend the ` +
54
+ `declaration or fix the row.`);
55
+ }
56
+ return flow;
57
+ }
58
+ /**
59
+ * Resolve every declared edge into propagation steps per its flow, and
60
+ * exercise the §4.4 co-directionality invariant.
61
+ */
62
+ export function buildImpactPropagation(edges, semantics) {
63
+ const out = [];
64
+ const byFlow = new Map();
65
+ let flowNone = 0;
66
+ const pairs = new Map();
67
+ const notePair = (from, to, type) => {
68
+ const [a, b] = from < to ? [from, to] : [to, from];
69
+ const key = `${a}␟${b}`;
70
+ let rec = pairs.get(key);
71
+ if (rec === undefined) {
72
+ rec = { ab: new Set(), ba: new Set() };
73
+ pairs.set(key, rec);
74
+ }
75
+ (from === a ? rec.ab : rec.ba).add(type);
76
+ };
77
+ for (const e of edges) {
78
+ const flow = resolveFlow(e, semantics);
79
+ byFlow.set(flow, (byFlow.get(flow) ?? 0) + 1);
80
+ if (flow === "none") {
81
+ flowNone += 1;
82
+ continue;
83
+ }
84
+ if (flow === "toward-source" || flow === "both") {
85
+ out.push({ from: e.target, to: e.source, type: e.type });
86
+ notePair(e.target, e.source, e.type);
87
+ }
88
+ if (flow === "toward-target" || flow === "both") {
89
+ out.push({ from: e.source, to: e.target, type: e.type });
90
+ notePair(e.source, e.target, e.type);
91
+ }
92
+ }
93
+ // §4.4 invariant, refined on measured data (2026-08-06): a pair fails only
94
+ // when one type contributes EXCLUSIVELY a→b against another contributing
95
+ // EXCLUSIVELY b→a. A type that itself spans both directions on the pair —
96
+ // opposing role-rows of a role-conditional relationship (a service that
97
+ // publishes AND consumes the same event: five real pairs on the reverse
98
+ // estate, one service, all session-broadcast shaped), a self-referential
99
+ // hierarchy cycle, or a declared `both` flow — has DECLARED that pair
100
+ // bidirectional, so other types aligning with either direction are
101
+ // consistent with declarations, not drift. The flipped-token flood shape
102
+ // (two single-direction declarations opposing) still fails, loudly.
103
+ for (const [key, rec] of pairs) {
104
+ const abOnly = [...rec.ab].filter((t) => !rec.ba.has(t));
105
+ const baOnly = [...rec.ba].filter((t) => !rec.ab.has(t));
106
+ if (abOnly.length > 0 && baOnly.length > 0) {
107
+ const [a, b] = key.split("␟");
108
+ throw new Error(`impact: counter-directional reciprocal pair (M-ADR 0206 §4.4): ` +
109
+ `${abOnly[0]} propagates ${a} -> ${b} while ${baOnly[0]} propagates ` +
110
+ `${b} -> ${a}, and neither declares the pair bidirectional. The ` +
111
+ `declared tokens have re-created an undirected pair; fix the ` +
112
+ `classification before trusting any closure.`);
113
+ }
114
+ }
115
+ return { edges: out, flowNone, byFlow };
116
+ }
117
+ function sqlStringLiteral(value) {
118
+ return `'${value.replace(/'/g, "''")}'`;
119
+ }
120
+ /**
121
+ * Closure of `seed` over resolved propagation edges, on the engine.
122
+ * Exported for the fixture controls; the verb goes through runCatalogueImpact.
123
+ */
124
+ export async function computeImpactClosure(propagation, seed) {
125
+ const { DuckDBInstance } = await import("@duckdb/node-api");
126
+ const instance = await DuckDBInstance.create(":memory:");
127
+ const conn = await instance.connect();
128
+ try {
129
+ await conn.run(`CREATE TABLE prop (f VARCHAR, t VARCHAR);`);
130
+ const CHUNK = 500;
131
+ for (let i = 0; i < propagation.length; i += CHUNK) {
132
+ const tuples = propagation
133
+ .slice(i, i + CHUNK)
134
+ .map((e) => `(${sqlStringLiteral(e.from)}, ${sqlStringLiteral(e.to)})`)
135
+ .join(",\n");
136
+ await conn.run(`INSERT INTO prop VALUES\n${tuples};`);
137
+ }
138
+ const seedLit = sqlStringLiteral(seed);
139
+ // CYCLE GUARD: the recursion carries NO depth column, deliberately.
140
+ // Recursive UNION deduplicates whole rows; a depth column makes every
141
+ // lap around a cycle a "new" row (same id, bigger depth), so the query
142
+ // that looks more informative is the query that never terminates — the
143
+ // catalogue graph is not a DAG. As a bare id-set the closure saturates
144
+ // and stops.
145
+ const reader = await conn.runAndReadAll(`WITH RECURSIVE reach(id) AS (\n` +
146
+ ` SELECT t FROM prop WHERE f = ${seedLit}\n` +
147
+ ` UNION\n` +
148
+ ` SELECT p.t FROM prop p JOIN reach r ON p.f = r.id\n` +
149
+ `)\n` +
150
+ `SELECT DISTINCT id FROM reach WHERE id <> ${seedLit} ORDER BY id;`);
151
+ return reader.getRows().map((row) => String(row[0]));
152
+ }
153
+ finally {
154
+ conn.disconnectSync();
155
+ }
156
+ }
157
+ export async function runCatalogueImpact(inputs, write) {
158
+ const sddRoot = resolve(inputs.sddPath ?? process.cwd());
159
+ const tier = inputs.tier ?? "member";
160
+ const dir = resolve(sddRoot, inputs.cataloguesDir ?? defaultCataloguesDir(tier));
161
+ if (!existsSync(dir)) {
162
+ write(`catalogues directory not found at ${dir}\n` +
163
+ ` Run \`sdd-cli catalogue build\` (or aggregate) first.\n`);
164
+ return { exitCode: 2 };
165
+ }
166
+ const schema = await loadCanonicalSchema();
167
+ const kinds = await loadCatalogueKinds();
168
+ const semantics = await loadRelationshipSemantics();
169
+ const rowsByCatalogue = new Map();
170
+ for (const cat of schema.catalogues) {
171
+ const filePath = join(dir, `${cat.name}.csv`);
172
+ if (!existsSync(filePath))
173
+ continue;
174
+ rowsByCatalogue.set(cat.name, (await readCatalogueCsv(filePath)).rows);
175
+ }
176
+ const projection = projectGraph(schema, kinds, rowsByCatalogue, tier);
177
+ const lines = projection.text.trimEnd().split("\n");
178
+ const nodeIds = new Set();
179
+ const edges = [];
180
+ for (const line of lines) {
181
+ const o = JSON.parse(line);
182
+ if (o.t === "node")
183
+ nodeIds.add(o.id);
184
+ else if (o.t === "edge")
185
+ edges.push({
186
+ source: o.source,
187
+ target: o.target,
188
+ type: o.type,
189
+ ...(o.properties !== undefined ? { properties: o.properties } : {}),
190
+ });
191
+ }
192
+ if (!nodeIds.has(inputs.of)) {
193
+ write(`node not found in the projection: ${inputs.of}\n` +
194
+ ` Node ids have the form "<catalogue>/<id>", e.g. contracts/orders-api.\n`);
195
+ return { exitCode: 2 };
196
+ }
197
+ const prop = buildImpactPropagation(edges, semantics);
198
+ const closure = await computeImpactClosure(prop.edges, inputs.of);
199
+ const byCatalogue = new Map();
200
+ for (const id of closure) {
201
+ const cat = id.slice(0, id.indexOf("/"));
202
+ byCatalogue.set(cat, (byCatalogue.get(cat) ?? 0) + 1);
203
+ }
204
+ write(`impact closure of ${inputs.of} (${tier} tier, schema ${schema.schemaVersion})\n`);
205
+ // Required stated lines (M-ADR 0206 §4.6): what the number means, and what
206
+ // was traversed under each declared flow — visible per run, not
207
+ // discoverable from the code.
208
+ write(` semantics: change impact over classified relationships (M-ADR 0206) —\n` +
209
+ ` each relationship traversed per its declared impact-flow; the\n` +
210
+ ` co-directionality invariant is asserted on every run\n`);
211
+ const flowCounts = [...prop.byFlow.entries()]
212
+ .sort()
213
+ .map(([f, n]) => `${f}=${n}`)
214
+ .join(" ");
215
+ write(` declared edges by resolved flow: ${flowCounts}\n`);
216
+ write(` reachable nodes: ${closure.length}\n`);
217
+ for (const [cat, n] of [...byCatalogue.entries()].sort()) {
218
+ write(` ${cat}: ${n}\n`);
219
+ }
220
+ for (const id of closure) {
221
+ write(` ${id}\n`);
222
+ }
223
+ return {
224
+ exitCode: 0,
225
+ result: { seed: inputs.of, closure, byCatalogue },
226
+ };
227
+ }
228
+ //# sourceMappingURL=impact-run.js.map