al-sem 0.0.1

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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -0
  3. package/package.json +64 -0
  4. package/scripts/d40-diff.ts +44 -0
  5. package/scripts/fetch-native-parser.ts +179 -0
  6. package/scripts/precision-sample.ts +99 -0
  7. package/scripts/precision-study.ts +42 -0
  8. package/scripts/precision-tabulate.ts +52 -0
  9. package/src/cli/baseline.ts +31 -0
  10. package/src/cli/diff.ts +199 -0
  11. package/src/cli/events-chains.ts +56 -0
  12. package/src/cli/events-fanout.ts +87 -0
  13. package/src/cli/exit-code.ts +30 -0
  14. package/src/cli/fingerprint-indexes.ts +130 -0
  15. package/src/cli/fingerprint-query.ts +543 -0
  16. package/src/cli/fingerprint-witness.ts +493 -0
  17. package/src/cli/fingerprint.ts +292 -0
  18. package/src/cli/format-compact-json.ts +45 -0
  19. package/src/cli/format-events.ts +77 -0
  20. package/src/cli/format-fingerprint.ts +295 -0
  21. package/src/cli/format-html.ts +503 -0
  22. package/src/cli/format-json.ts +13 -0
  23. package/src/cli/format-policy.ts +95 -0
  24. package/src/cli/format-sarif.ts +186 -0
  25. package/src/cli/format-terminal.ts +153 -0
  26. package/src/cli/index.ts +566 -0
  27. package/src/cli/policy.ts +204 -0
  28. package/src/config/roots-config.ts +302 -0
  29. package/src/deps/cache-versions.ts +74 -0
  30. package/src/deps/canonical-json.ts +27 -0
  31. package/src/deps/dependency-artifact.ts +144 -0
  32. package/src/deps/dependency-cache.ts +262 -0
  33. package/src/deps/dependency-dag.ts +128 -0
  34. package/src/deps/dependency-package-discovery.ts +85 -0
  35. package/src/deps/dependency-pipeline.ts +483 -0
  36. package/src/deps/dependency-projection.ts +211 -0
  37. package/src/deps/dependency-resolver.ts +154 -0
  38. package/src/deps/workspace-dependencies.ts +114 -0
  39. package/src/detectors/capability-query.ts +145 -0
  40. package/src/detectors/confidence.ts +52 -0
  41. package/src/detectors/d1-db-op-in-loop.ts +457 -0
  42. package/src/detectors/d10-self-modifying-loop.ts +114 -0
  43. package/src/detectors/d11-modify-without-get.ts +129 -0
  44. package/src/detectors/d12-dead-integration-event.ts +81 -0
  45. package/src/detectors/d13-cross-app-internal-call.ts +105 -0
  46. package/src/detectors/d14-dead-routine.ts +151 -0
  47. package/src/detectors/d16-obsolete-routine-call.ts +94 -0
  48. package/src/detectors/d17-min-version-drift.ts +157 -0
  49. package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
  50. package/src/detectors/d19-unused-parameter.ts +116 -0
  51. package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
  52. package/src/detectors/d20-unreachable-after-exit.ts +92 -0
  53. package/src/detectors/d21-read-without-load.ts +128 -0
  54. package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
  55. package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
  56. package/src/detectors/d3-load-state.ts +72 -0
  57. package/src/detectors/d3-missing-setloadfields.ts +234 -0
  58. package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
  59. package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
  60. package/src/detectors/d34-commit-in-loop.ts +206 -0
  61. package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
  62. package/src/detectors/d36-late-setloadfields.ts +162 -0
  63. package/src/detectors/d37-validate-without-persist.ts +271 -0
  64. package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
  65. package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
  66. package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
  67. package/src/detectors/d40-transitive-load-missing.ts +217 -0
  68. package/src/detectors/d41-transitive-filter-loss.ts +200 -0
  69. package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
  70. package/src/detectors/d43-event-ishandled-skip.ts +257 -0
  71. package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
  72. package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
  73. package/src/detectors/d5-set-based-opportunity.ts +162 -0
  74. package/src/detectors/d7-recursive-event-expansion.ts +151 -0
  75. package/src/detectors/d8-commit-in-transaction.ts +132 -0
  76. package/src/detectors/d9-transaction-span-summary.ts +107 -0
  77. package/src/detectors/detector-context.ts +121 -0
  78. package/src/detectors/finding-grouping.ts +61 -0
  79. package/src/detectors/path-merge.ts +174 -0
  80. package/src/detectors/registry.ts +176 -0
  81. package/src/detectors/table-display.ts +42 -0
  82. package/src/diff/diff-abi.ts +195 -0
  83. package/src/diff/diff-capabilities.ts +179 -0
  84. package/src/diff/diff-engine.ts +146 -0
  85. package/src/diff/diff-events.ts +323 -0
  86. package/src/diff/diff-identity.ts +73 -0
  87. package/src/diff/diff-indexes.ts +199 -0
  88. package/src/diff/diff-permissions.ts +260 -0
  89. package/src/diff/diff-policy.ts +101 -0
  90. package/src/diff/diff-preflight.ts +66 -0
  91. package/src/diff/diff-renames.ts +104 -0
  92. package/src/diff/diff-schema.ts +232 -0
  93. package/src/diff/format-diff.ts +148 -0
  94. package/src/engine/attribute-parser.ts +50 -0
  95. package/src/engine/capability-cone.ts +531 -0
  96. package/src/engine/combined-graph.ts +357 -0
  97. package/src/engine/control-flow-walker.ts +1317 -0
  98. package/src/engine/dispatch-sites.ts +199 -0
  99. package/src/engine/effect-lattice.ts +81 -0
  100. package/src/engine/entry-points.ts +57 -0
  101. package/src/engine/event-flow.ts +524 -0
  102. package/src/engine/event-relay.ts +92 -0
  103. package/src/engine/op-classification.ts +92 -0
  104. package/src/engine/path-walker.ts +189 -0
  105. package/src/engine/reverse-call-graph.ts +23 -0
  106. package/src/engine/root-classifier-overlay.ts +194 -0
  107. package/src/engine/root-classifier.ts +135 -0
  108. package/src/engine/scc.ts +110 -0
  109. package/src/engine/source-anchor.ts +25 -0
  110. package/src/engine/summary-context.ts +104 -0
  111. package/src/engine/summary-engine.ts +296 -0
  112. package/src/engine/summary-runner.ts +560 -0
  113. package/src/engine/transaction-spans.ts +112 -0
  114. package/src/engine/uncertainty-util.ts +54 -0
  115. package/src/hash.ts +31 -0
  116. package/src/index/attribute-from-node.ts +141 -0
  117. package/src/index/callee-from-node.ts +181 -0
  118. package/src/index/capability/background.ts +90 -0
  119. package/src/index/capability/commit.ts +44 -0
  120. package/src/index/capability/dispatch.ts +164 -0
  121. package/src/index/capability/events.ts +65 -0
  122. package/src/index/capability/extractor.ts +124 -0
  123. package/src/index/capability/file-blob.ts +137 -0
  124. package/src/index/capability/http.ts +159 -0
  125. package/src/index/capability/hyperlink.ts +60 -0
  126. package/src/index/capability/isolated-storage.ts +179 -0
  127. package/src/index/capability/table.ts +113 -0
  128. package/src/index/capability/telemetry.ts +84 -0
  129. package/src/index/capability/ui.ts +55 -0
  130. package/src/index/capability/value-source.ts +202 -0
  131. package/src/index/expression-from-node.ts +117 -0
  132. package/src/index/indexer.ts +102 -0
  133. package/src/index/intraprocedural-body.ts +1467 -0
  134. package/src/index/intraprocedural-ops.ts +253 -0
  135. package/src/index/intraprocedural-refs.ts +188 -0
  136. package/src/index/object-indexer.ts +279 -0
  137. package/src/index/routine-indexer.ts +282 -0
  138. package/src/index/routine-signature.ts +46 -0
  139. package/src/index/variable-indexer.ts +134 -0
  140. package/src/index/variable-initializer-extractor.ts +155 -0
  141. package/src/index/variable-type-normalizer.ts +83 -0
  142. package/src/index.ts +267 -0
  143. package/src/mcp/server.ts +72 -0
  144. package/src/mcp/session.ts +49 -0
  145. package/src/mcp/tools/explain-path.ts +75 -0
  146. package/src/mcp/tools/get-analysis-health.ts +62 -0
  147. package/src/mcp/tools/get-finding.ts +47 -0
  148. package/src/mcp/tools/get-routine-summary.ts +126 -0
  149. package/src/mcp/tools/list-findings.ts +85 -0
  150. package/src/mcp/tools/list-hotspots.ts +78 -0
  151. package/src/mcp/tools/list-rollups.ts +103 -0
  152. package/src/mcp/tools/validators.ts +25 -0
  153. package/src/model/attributes.ts +120 -0
  154. package/src/model/callee.ts +45 -0
  155. package/src/model/capability.ts +187 -0
  156. package/src/model/coverage.ts +85 -0
  157. package/src/model/entities.ts +628 -0
  158. package/src/model/expression.ts +98 -0
  159. package/src/model/finding.ts +110 -0
  160. package/src/model/graph-edge.ts +93 -0
  161. package/src/model/graph.ts +62 -0
  162. package/src/model/identity.ts +81 -0
  163. package/src/model/ids.ts +90 -0
  164. package/src/model/index.ts +13 -0
  165. package/src/model/model.ts +51 -0
  166. package/src/model/permission.ts +76 -0
  167. package/src/model/root-classification.ts +116 -0
  168. package/src/model/stable-identity.ts +102 -0
  169. package/src/model/summary.ts +96 -0
  170. package/src/parser/ast.ts +82 -0
  171. package/src/parser/native/ffi.ts +145 -0
  172. package/src/parser/native/parse-index-pool.ts +148 -0
  173. package/src/parser/native/parse-index-worker.ts +94 -0
  174. package/src/parser/native/wrapper.ts +353 -0
  175. package/src/parser/parser-init.ts +43 -0
  176. package/src/perf/profiler.ts +66 -0
  177. package/src/policy/policy-default.yaml +83 -0
  178. package/src/policy/policy-engine.ts +339 -0
  179. package/src/policy/policy-loader.ts +257 -0
  180. package/src/policy/policy-schema.json +379 -0
  181. package/src/policy/policy-types.ts +81 -0
  182. package/src/policy/predicate-compiler.ts +151 -0
  183. package/src/policy/predicate-evaluator.ts +267 -0
  184. package/src/policy/predicate-fields.ts +439 -0
  185. package/src/projection/actionable-anchor.ts +48 -0
  186. package/src/projection/finding-filters.ts +44 -0
  187. package/src/projection/finding-fingerprint.ts +54 -0
  188. package/src/projection/finding-groups.ts +41 -0
  189. package/src/projection/finding-summary.ts +110 -0
  190. package/src/projection/rollup-findings.ts +105 -0
  191. package/src/providers/discover.ts +88 -0
  192. package/src/providers/external.ts +46 -0
  193. package/src/providers/types.ts +36 -0
  194. package/src/providers/workspace.ts +117 -0
  195. package/src/resolve/call-resolver.ts +117 -0
  196. package/src/resolve/coverage.ts +61 -0
  197. package/src/resolve/event-graph.ts +166 -0
  198. package/src/resolve/implicit-edges.ts +53 -0
  199. package/src/resolve/record-types.ts +36 -0
  200. package/src/resolve/resolver.ts +23 -0
  201. package/src/resolve/semantic-graph.ts +29 -0
  202. package/src/resolve/symbol-table.ts +69 -0
  203. package/src/snapshot/app-snapshot.ts +74 -0
  204. package/src/snapshot/compose.ts +100 -0
  205. package/src/snapshot/derive/callsite-evidence.ts +76 -0
  206. package/src/snapshot/derive/capability-facts.ts +70 -0
  207. package/src/snapshot/derive/contracts.ts +131 -0
  208. package/src/snapshot/derive/coverage.ts +35 -0
  209. package/src/snapshot/derive/event-declarations.ts +140 -0
  210. package/src/snapshot/derive/identity-table.ts +58 -0
  211. package/src/snapshot/derive/inputs.ts +91 -0
  212. package/src/snapshot/derive/operation-evidence.ts +70 -0
  213. package/src/snapshot/derive/permissions.ts +186 -0
  214. package/src/snapshot/derive/root-classifications.ts +56 -0
  215. package/src/snapshot/derive/schema.ts +130 -0
  216. package/src/snapshot/derive/typed-edges.ts +60 -0
  217. package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
  218. package/src/snapshot/deserialize.ts +40 -0
  219. package/src/snapshot/serialize-cbor-gz.ts +12 -0
  220. package/src/snapshot/serialize-cbor.ts +19 -0
  221. package/src/snapshot/serialize-json.ts +22 -0
  222. package/src/snapshot/shard.ts +134 -0
  223. package/src/snapshot/types.ts +181 -0
  224. package/src/symbols/app-manifest.ts +96 -0
  225. package/src/symbols/app-package-zip.ts +50 -0
  226. package/src/symbols/embedded-source-reader.ts +41 -0
  227. package/src/symbols/package-hash.ts +81 -0
  228. package/src/symbols/symbol-reader.ts +101 -0
  229. package/src/symbols/symbol-reference-parser.ts +378 -0
  230. package/src/symbols/symbol-reference-reader.ts +27 -0
  231. package/tsconfig.json +18 -0
@@ -0,0 +1,357 @@
1
+ import type { ObjectRunKind } from "../model/callee.ts";
2
+ import type { ValueSource } from "../model/capability.ts";
3
+ import type { CallSite } from "../model/entities.ts";
4
+ import type { GraphEdge } from "../model/graph-edge.ts";
5
+ import type { DispatchKind, ResolutionQuality } from "../model/graph.ts";
6
+ import type { CallsiteId, EventId, ObjectId, OperationId, RoutineId } from "../model/ids.ts";
7
+ import type { SemanticModel } from "../model/model.ts";
8
+ import type { Uncertainty } from "../model/summary.ts";
9
+ import { compareStrings } from "./uncertainty-util.ts";
10
+
11
+ /** The origin kind of a combined-graph edge. */
12
+ export type CombinedEdgeKind =
13
+ | "direct"
14
+ | "method"
15
+ | "codeunit-run"
16
+ | "report-run"
17
+ | "page-run"
18
+ | "interface"
19
+ | "implicit-trigger"
20
+ | "event-dispatch"
21
+ | "dynamic";
22
+
23
+ /** A resolved routine -> routine edge in the combined graph. */
24
+ export interface CombinedEdge {
25
+ from: RoutineId;
26
+ to: RoutineId;
27
+ kind: CombinedEdgeKind;
28
+ callsiteId?: CallsiteId; // present for call-derived edges
29
+ operationId?: OperationId; // present for call-derived edges
30
+ eventId?: EventId; // present for event-dispatch edges
31
+ subscriberAppId?: string; // present for event-dispatch edges
32
+ resolution: ResolutionQuality;
33
+ }
34
+
35
+ /** An uncertainty attached to a routine because one of its call sites had no resolved target. */
36
+ export interface UncertaintyEdge {
37
+ from: RoutineId;
38
+ uncertainty: Uncertainty;
39
+ }
40
+
41
+ /** The combined call + event + implicit-trigger graph the summary engine and path-walker traverse. */
42
+ export interface CombinedGraph {
43
+ nodes: RoutineId[]; // sorted
44
+ edgesByFrom: Map<RoutineId, CombinedEdge[]>; // each list sorted
45
+ uncertaintyEdges: UncertaintyEdge[]; // sorted
46
+ }
47
+
48
+ /** The object-run dispatch kinds that map to typed GraphEdge object-run variants. */
49
+ const OBJECT_RUN_KINDS: ReadonlySet<DispatchKind> = new Set<DispatchKind>([
50
+ "codeunit-run",
51
+ "page-run",
52
+ "report-run",
53
+ ]);
54
+
55
+ /** Map a call-graph dispatchKind to the GraphEdge objectType field. */
56
+ function dispatchKindToObjectType(
57
+ dispatchKind: DispatchKind,
58
+ ): "Codeunit" | "Page" | "Report" | undefined {
59
+ if (dispatchKind === "codeunit-run") return "Codeunit";
60
+ if (dispatchKind === "page-run") return "Page";
61
+ if (dispatchKind === "report-run") return "Report";
62
+ return undefined;
63
+ }
64
+
65
+ /** Build a ValueSource for an object-run callsite's target id. */
66
+ function objectRunTargetIdSource(callSite: CallSite): ValueSource {
67
+ if (callSite.callee.kind !== "object-run") return { kind: "unknown" };
68
+ const c = callSite.callee;
69
+ if (c.targetRef === undefined) {
70
+ // Dynamic — no static info about which object
71
+ return { kind: "expression" };
72
+ }
73
+ if (c.targetIsName) {
74
+ // Named object — treat as an enum reference (objectKind::"name")
75
+ return { kind: "enum", enumName: c.objectKind, member: c.targetRef };
76
+ }
77
+ // Numeric object id — literal
78
+ return { kind: "literal", value: c.targetRef };
79
+ }
80
+
81
+ /**
82
+ * Derive the target ObjectId for an object-run callee when the targetRef is
83
+ * statically known but the routine entry wasn't resolved (opaque dep). Returns
84
+ * undefined when the target is fully dynamic.
85
+ */
86
+ function objectRunTargetObject(
87
+ callSite: CallSite,
88
+ objectsByTypeNumber: Map<string, ObjectId>,
89
+ objectsByTypeName: Map<string, ObjectId>,
90
+ ): ObjectId | undefined {
91
+ if (callSite.callee.kind !== "object-run") return undefined;
92
+ const c = callSite.callee;
93
+ if (c.targetRef === undefined) return undefined;
94
+ if (c.targetIsName) {
95
+ return objectsByTypeName.get(`${c.targetType.toLowerCase()}/${c.targetRef.toLowerCase()}`);
96
+ }
97
+ return objectsByTypeNumber.get(`${c.targetType.toLowerCase()}/${c.targetRef}`);
98
+ }
99
+
100
+ // CallGraph dispatchKinds that become resolved routine->routine edges (when `to` is set).
101
+ const EDGE_KINDS: ReadonlySet<DispatchKind> = new Set<DispatchKind>([
102
+ "direct",
103
+ "method",
104
+ "codeunit-run",
105
+ "report-run",
106
+ "page-run",
107
+ "interface",
108
+ "implicit-trigger",
109
+ "dynamic",
110
+ ]);
111
+
112
+ function edgeSortKey(e: CombinedEdge): string {
113
+ return `${e.kind}|${e.callsiteId ?? e.operationId ?? e.eventId ?? ""}|${e.to}`;
114
+ }
115
+
116
+ function uncertaintySortKey(ue: UncertaintyEdge): string {
117
+ const u = ue.uncertainty;
118
+ const ref = "callsiteId" in u ? u.callsiteId : "operationId" in u ? u.operationId : u.routineId;
119
+ return `${ue.from}|${u.kind}|${ref}`;
120
+ }
121
+
122
+ /**
123
+ * Build the typed `GraphEdge[]` for the Phase 0b-β typed edge layer.
124
+ *
125
+ * Kinds emitted here:
126
+ * direct-call — resolved bare/direct procedure→procedure call
127
+ * object-run-resolved — Codeunit/Page/Report.Run with a resolved entry routine
128
+ * object-run-unresolved — Codeunit/Page/Report.Run with dynamic or opaque target,
129
+ * or a `dynamic` dispatchKind (variable-typed Run)
130
+ * event-dispatch — composed publisher→subscriber edge (Task 20)
131
+ *
132
+ * Implicit-trigger and dependency-export edges are deferred to Tasks 21+.
133
+ */
134
+ function buildTypedEdges(model: SemanticModel): GraphEdge[] {
135
+ // --- look-up maps built from the model ---
136
+ // callsite id → CallSite (for sourceAnchor + callee details)
137
+ const callSiteById = new Map<CallsiteId, CallSite>();
138
+ for (const routine of model.routines) {
139
+ for (const cs of routine.features.callSites) {
140
+ callSiteById.set(cs.id, cs);
141
+ }
142
+ }
143
+ // routine id → Routine (for sourceAnchor on publisher/subscriber)
144
+ const routineById = new Map<RoutineId, (typeof model.routines)[number]>();
145
+ for (const routine of model.routines) {
146
+ routineById.set(routine.id, routine);
147
+ }
148
+ // routine id → objectId (for resolved object-run target)
149
+ const objectIdByRoutineId = new Map<RoutineId, ObjectId>();
150
+ for (const routine of model.routines) {
151
+ objectIdByRoutineId.set(routine.id, routine.objectId);
152
+ }
153
+ // object type+number / type+name → objectId (for unresolved object-run target)
154
+ const objectsByTypeNumber = new Map<string, ObjectId>();
155
+ const objectsByTypeName = new Map<string, ObjectId>();
156
+ for (const obj of model.objects) {
157
+ objectsByTypeNumber.set(`${obj.objectType.toLowerCase()}/${obj.objectNumber}`, obj.id);
158
+ objectsByTypeName.set(`${obj.objectType.toLowerCase()}/${obj.name.toLowerCase()}`, obj.id);
159
+ }
160
+
161
+ const typedEdges: GraphEdge[] = [];
162
+
163
+ for (const ce of model.callGraph) {
164
+ if (ce.dispatchKind === "event-dispatch" || ce.dispatchKind === "implicit-trigger") continue;
165
+
166
+ const callSite = callSiteById.get(ce.callsiteId);
167
+ if (callSite === undefined) continue; // opaque (dependency-only) callsite — skip
168
+
169
+ const sourceAnchor = callSite.sourceAnchor;
170
+
171
+ if (ce.to !== undefined) {
172
+ // Resolved edges
173
+ if (ce.dispatchKind === "direct" || ce.dispatchKind === "method") {
174
+ typedEdges.push({
175
+ kind: "direct-call",
176
+ callsiteId: ce.callsiteId,
177
+ from: ce.from,
178
+ to: ce.to,
179
+ sourceAnchor,
180
+ });
181
+ } else if (OBJECT_RUN_KINDS.has(ce.dispatchKind)) {
182
+ const objectType = dispatchKindToObjectType(ce.dispatchKind);
183
+ if (objectType === undefined) continue;
184
+ // Resolved: target routine is known; derive its object id
185
+ const targetObject = objectIdByRoutineId.get(ce.to);
186
+ if (targetObject === undefined) continue; // should not happen in a sound model
187
+ typedEdges.push({
188
+ kind: "object-run-resolved",
189
+ callsiteId: ce.callsiteId,
190
+ from: ce.from,
191
+ to: ce.to,
192
+ targetObject,
193
+ objectType,
194
+ sourceAnchor,
195
+ });
196
+ }
197
+ // interface / unresolved with a `to` — skip (should be rare; not a typed kind yet)
198
+ } else {
199
+ // Unresolved edges
200
+ if (OBJECT_RUN_KINDS.has(ce.dispatchKind)) {
201
+ // Static target exists but the entry routine isn't in the index (opaque dep)
202
+ const objectType = dispatchKindToObjectType(ce.dispatchKind);
203
+ if (objectType === undefined) continue;
204
+ const targetIdSource = objectRunTargetIdSource(callSite);
205
+ const targetObject = objectRunTargetObject(
206
+ callSite,
207
+ objectsByTypeNumber,
208
+ objectsByTypeName,
209
+ );
210
+ typedEdges.push({
211
+ kind: "object-run-unresolved",
212
+ callsiteId: ce.callsiteId,
213
+ from: ce.from,
214
+ ...(targetObject !== undefined ? { targetObject } : {}),
215
+ targetIdSource,
216
+ objectType,
217
+ sourceAnchor,
218
+ });
219
+ } else if (ce.dispatchKind === "dynamic") {
220
+ // Dynamic dispatch — we don't know objectType without checking the callee
221
+ if (callSite.callee.kind === "object-run") {
222
+ const objectType = callSite.callee.objectKind as "Codeunit" | "Page" | "Report";
223
+ const targetIdSource = objectRunTargetIdSource(callSite);
224
+ typedEdges.push({
225
+ kind: "object-run-unresolved",
226
+ callsiteId: ce.callsiteId,
227
+ from: ce.from,
228
+ targetIdSource,
229
+ objectType,
230
+ sourceAnchor,
231
+ });
232
+ }
233
+ // dynamic member call (method dispatch) — not an object-run; skip
234
+ }
235
+ // interface / unresolved bare call — skip (not yet a typed kind)
236
+ }
237
+ }
238
+
239
+ // --- event-dispatch edges (Task 20): bipartite publisher → subscriber composition ---
240
+ const subsByEventTyped = new Map<EventId, typeof model.eventGraph.edges>();
241
+ for (const ee of model.eventGraph.edges) {
242
+ const list = subsByEventTyped.get(ee.eventId);
243
+ if (list) list.push(ee);
244
+ else subsByEventTyped.set(ee.eventId, [ee]);
245
+ }
246
+ for (const sym of model.eventGraph.events) {
247
+ if (sym.publisherRoutineId === undefined) continue;
248
+ const publisherRoutine = routineById.get(sym.publisherRoutineId);
249
+ if (publisherRoutine === undefined) continue;
250
+ for (const ee of subsByEventTyped.get(sym.id) ?? []) {
251
+ const subscriberRoutine = routineById.get(ee.subscriberRoutineId);
252
+ if (subscriberRoutine === undefined) continue;
253
+ typedEdges.push({
254
+ kind: "event-dispatch",
255
+ from: sym.publisherRoutineId,
256
+ to: ee.subscriberRoutineId,
257
+ eventId: sym.id,
258
+ publishAnchor: publisherRoutine.sourceAnchor,
259
+ subscriberAnchor: subscriberRoutine.sourceAnchor,
260
+ });
261
+ }
262
+ }
263
+
264
+ return typedEdges;
265
+ }
266
+
267
+ /**
268
+ * Build the combined graph + populate `model.typedEdges` as a side effect.
269
+ *
270
+ * Idempotent on already-populated models — `analyzeWorkspace` calls this
271
+ * before returning the model, so downstream consumers receive a populated
272
+ * `model.typedEdges`. Re-calling produces an identical result; the call is
273
+ * safe but wasteful. Don't re-invoke from CLI runners or detectors.
274
+ *
275
+ * Resolved CallEdges (and event-dispatch hops derived from the event graph)
276
+ * become CombinedEdges; to-less CallEdges become UncertaintyEdges. The
277
+ * `event-dispatch` dispatchKind on CallEdges is intentionally skipped here —
278
+ * event hops are generated once from `model.eventGraph` to avoid double counting.
279
+ */
280
+ export function buildCombinedGraph(model: SemanticModel): CombinedGraph {
281
+ const edges: CombinedEdge[] = [];
282
+ const uncertaintyEdges: UncertaintyEdge[] = [];
283
+
284
+ // --- call-derived edges + uncertainty records ---
285
+ for (const ce of model.callGraph) {
286
+ if (ce.dispatchKind === "event-dispatch") continue; // event hops come from the event graph
287
+ if (ce.to !== undefined) {
288
+ if (EDGE_KINDS.has(ce.dispatchKind)) {
289
+ edges.push({
290
+ from: ce.from,
291
+ to: ce.to,
292
+ kind: ce.dispatchKind as CombinedEdgeKind,
293
+ callsiteId: ce.callsiteId,
294
+ operationId: ce.operationId,
295
+ resolution: ce.resolution,
296
+ });
297
+ }
298
+ continue;
299
+ }
300
+ // to-less edge -> typed uncertainty on the `from` routine
301
+ if (ce.dispatchKind === "interface") {
302
+ uncertaintyEdges.push({
303
+ from: ce.from,
304
+ uncertainty: { kind: "interface-dispatch", callsiteId: ce.callsiteId },
305
+ });
306
+ } else if (ce.dispatchKind === "dynamic") {
307
+ uncertaintyEdges.push({
308
+ from: ce.from,
309
+ uncertainty: { kind: "dynamic-dispatch", operationId: ce.operationId },
310
+ });
311
+ } else {
312
+ uncertaintyEdges.push({
313
+ from: ce.from,
314
+ uncertainty: { kind: "unresolved-call", callsiteId: ce.callsiteId },
315
+ });
316
+ }
317
+ }
318
+
319
+ // --- event-dispatch edges: publisher routine -> subscriber routine ---
320
+ const subsByEvent = new Map<EventId, typeof model.eventGraph.edges>();
321
+ for (const ee of model.eventGraph.edges) {
322
+ const list = subsByEvent.get(ee.eventId);
323
+ if (list) list.push(ee);
324
+ else subsByEvent.set(ee.eventId, [ee]);
325
+ }
326
+ for (const sym of model.eventGraph.events) {
327
+ if (sym.publisherRoutineId === undefined) continue;
328
+ for (const ee of subsByEvent.get(sym.id) ?? []) {
329
+ edges.push({
330
+ from: sym.publisherRoutineId,
331
+ to: ee.subscriberRoutineId,
332
+ kind: "event-dispatch",
333
+ eventId: sym.id,
334
+ subscriberAppId: ee.subscriberAppId,
335
+ resolution: ee.resolution,
336
+ });
337
+ }
338
+ }
339
+
340
+ // --- assemble: sorted nodes, sorted edge lists, sorted uncertainty edges ---
341
+ const nodes = model.routines.map((r) => r.id).sort();
342
+ const edgesByFrom = new Map<RoutineId, CombinedEdge[]>();
343
+ for (const e of edges) {
344
+ const list = edgesByFrom.get(e.from);
345
+ if (list) list.push(e);
346
+ else edgesByFrom.set(e.from, [e]);
347
+ }
348
+ for (const list of edgesByFrom.values()) {
349
+ list.sort((a, b) => compareStrings(edgeSortKey(a), edgeSortKey(b)));
350
+ }
351
+ uncertaintyEdges.sort((a, b) => compareStrings(uncertaintySortKey(a), uncertaintySortKey(b)));
352
+
353
+ // --- typed GraphEdge[] (Phase 0b-β Task 19) — attached to the model in-place ---
354
+ model.typedEdges = buildTypedEdges(model);
355
+
356
+ return { nodes, edgesByFrom, uncertaintyEdges };
357
+ }