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,199 @@
1
+ /**
2
+ * enumerateDispatchSites — bridges the event-flow substrate with D43's dispatch-site
3
+ * guard analysis.
4
+ *
5
+ * For each call-site in the model where a non-publisher routine calls an event-publisher
6
+ * routine, this function produces a `DispatchSite` record capturing:
7
+ * - Which publisher and event are involved.
8
+ * - Which caller routine + call-site makes the dispatch.
9
+ * - The caller's actual argument bound to the publisher's IsHandled formal (if any).
10
+ * - Any post-call `conditionReference` entries in the caller body that reference the
11
+ * IsHandled actual — these are the guards D43 reasons about.
12
+ * - The set of tables the caller writes (transitively) when a post-call guard exists.
13
+ *
14
+ * ## Argument-binding strategy (fallback documented here)
15
+ *
16
+ * Phase 4's `CallArgumentBinding` carries a `sourceVariableName` for record-typed
17
+ * arguments, but Boolean arguments resolve as `bindingResolution: "non-record-arg"` with
18
+ * no `sourceVariableName`. Consequently, for a Boolean IsHandled formal we fall back to
19
+ * the raw `argumentTexts[handledFormalIndex]` value, lowercased. This is sound for
20
+ * canonical AL where the caller passes a declared local or parameter by its bare
21
+ * identifier — `OnBeforePost(Rec, IsHandled)` → text `"IsHandled"` → lowercased
22
+ * `"ishandled"`. The fallback is documented in `DispatchSiteHandledActual.variableName`.
23
+ */
24
+ import { writesTablesOf } from "../detectors/capability-query.ts";
25
+ import type { ConditionReference } from "../model/entities.ts";
26
+ import type { CallsiteId, EventId, RoutineId, TableId } from "../model/ids.ts";
27
+ import type { SemanticModel } from "../model/model.ts";
28
+ import { type EventFlowIndexes, IS_HANDLED_RE } from "./event-flow.ts";
29
+ import { compareStrings } from "./uncertainty-util.ts";
30
+
31
+ export interface DispatchSiteHandledActual {
32
+ /**
33
+ * Lowercased name of the caller's local/global variable passed by-var into the
34
+ * publisher's IsHandled formal. Derived from the raw `argumentTexts` entry when
35
+ * `argumentBindings` cannot carry Boolean-scalar identity (the common fallback path).
36
+ */
37
+ variableName: string;
38
+ /** 0-based index of the publisher's IsHandled formal among its parameters. */
39
+ formalIndex: number;
40
+ }
41
+
42
+ export interface DispatchSite {
43
+ publisherRoutine: RoutineId;
44
+ eventId: EventId;
45
+ callerRoutine: RoutineId;
46
+ callsiteId: CallsiteId;
47
+ /**
48
+ * Set when the publisher has a `var Boolean` formal matching IS_HANDLED_RE AND the
49
+ * caller binds a recognizable identifier to that formal.
50
+ */
51
+ handledActual?: DispatchSiteHandledActual;
52
+ /**
53
+ * Caller's `conditionReferences` whose identifier matches `handledActual.variableName`
54
+ * and whose `referenceAnchor` is AFTER the callsite's start position (line / column).
55
+ */
56
+ postCallGuards: readonly ConditionReference[];
57
+ /**
58
+ * Tables written by the caller's reachable cone when a post-call guard exists.
59
+ * Initial cut: full caller-summary writes — branch-level slice is deferred to T3+.
60
+ * Empty when there is no post-call guard or when the caller has no summary.
61
+ */
62
+ guardedTablesWritten: readonly TableId[];
63
+ }
64
+
65
+ export function enumerateDispatchSites(
66
+ model: SemanticModel,
67
+ _ix: EventFlowIndexes,
68
+ ): readonly DispatchSite[] {
69
+ const out: DispatchSite[] = [];
70
+
71
+ // Index routines by id for O(1) lookup.
72
+ const routineById = new Map(model.routines.map((r) => [r.id, r] as const));
73
+
74
+ // Collect the set of event-publisher routine ids together with their associated
75
+ // event id (from the event graph). We prefer the event graph as the authoritative
76
+ // source because it carries the stable EventId.
77
+ const publisherEventId = new Map<RoutineId, EventId>();
78
+ for (const ev of model.eventGraph.events) {
79
+ if (ev.publisherRoutineId !== undefined) {
80
+ publisherEventId.set(ev.publisherRoutineId, ev.id);
81
+ }
82
+ }
83
+
84
+ // Walk typed edges. We look for edges whose target is an event-publisher routine.
85
+ // Relevant edge kinds: "direct-call", "object-run-resolved", "dependency-export".
86
+ // "event-dispatch" edges are publisher→subscriber (not caller→publisher), so they
87
+ // are explicitly skipped — they don't carry a callsiteId.
88
+ const typedEdges = model.typedEdges ?? [];
89
+ for (const edge of typedEdges) {
90
+ if (
91
+ edge.kind !== "direct-call" &&
92
+ edge.kind !== "object-run-resolved" &&
93
+ edge.kind !== "dependency-export"
94
+ ) {
95
+ continue;
96
+ }
97
+
98
+ const publisherEventEntry = publisherEventId.get(edge.to);
99
+ if (publisherEventEntry === undefined) continue; // target is not a publisher
100
+
101
+ const caller = routineById.get(edge.from);
102
+ const publisher = routineById.get(edge.to);
103
+ if (!caller || !publisher) continue;
104
+
105
+ // Find the callsite in the caller's features that matches the edge's callsiteId.
106
+ const callSite = caller.features.callSites.find((cs) => cs.id === edge.callsiteId);
107
+ if (!callSite) continue;
108
+
109
+ // Find the publisher's IsHandled-shaped formal: var Boolean matching IS_HANDLED_RE.
110
+ // ParameterSymbol uses `isVar` (boolean) and `typeText` (raw type string).
111
+ let handledFormalIndex = -1;
112
+ for (let i = 0; i < publisher.parameters.length; i++) {
113
+ const p = publisher.parameters[i];
114
+ if (p === undefined) continue;
115
+ if (!p.isVar) continue;
116
+ if (!/^boolean$/i.test(p.typeText)) continue;
117
+ if (!IS_HANDLED_RE.test(p.name)) continue;
118
+ handledFormalIndex = i;
119
+ break;
120
+ }
121
+
122
+ // Identify the caller's actual variable bound to the IsHandled formal.
123
+ //
124
+ // Primary path: check argumentBindings for a sourceVariableName (works when the
125
+ // binding resolution succeeded and the argument is not a record). For Boolean
126
+ // formals, `bindingResolution` is typically "non-record-arg" with no
127
+ // `sourceVariableName`, so this path usually returns nothing.
128
+ //
129
+ // Fallback path: read `argumentTexts[handledFormalIndex]` directly. The raw text
130
+ // is the bare identifier the caller passed (e.g. "IsHandled"), and lowercasing it
131
+ // gives the canonical name for conditionReference matching.
132
+ let handledActual: DispatchSiteHandledActual | undefined;
133
+ if (handledFormalIndex >= 0) {
134
+ // Primary: try argumentBindings.sourceVariableName.
135
+ const binding = callSite.argumentBindings[handledFormalIndex];
136
+ const nameFromBinding =
137
+ binding !== undefined &&
138
+ binding.sourceKind !== "unknown" &&
139
+ typeof binding.sourceVariableName === "string" &&
140
+ binding.sourceVariableName.length > 0
141
+ ? binding.sourceVariableName
142
+ : undefined;
143
+
144
+ // Fallback: raw argumentTexts entry, lowercased.
145
+ const nameFromText =
146
+ nameFromBinding === undefined
147
+ ? (callSite.argumentTexts[handledFormalIndex]?.trim().toLowerCase() ?? undefined)
148
+ : undefined;
149
+
150
+ const varName = nameFromBinding ?? nameFromText;
151
+ if (varName !== undefined && varName.length > 0) {
152
+ handledActual = { variableName: varName, formalIndex: handledFormalIndex };
153
+ }
154
+ }
155
+
156
+ // Collect post-call conditionReferences from the CALLER's features.
157
+ // A reference is "post-call" when its referenceAnchor is strictly after the
158
+ // callsite's start position (same source unit, later line OR same line later column).
159
+ const postCallGuards: ConditionReference[] = [];
160
+ if (handledActual) {
161
+ const callRow = callSite.sourceAnchor.range.startLine;
162
+ const callCol = callSite.sourceAnchor.range.startColumn;
163
+ const actualLower = handledActual.variableName.toLowerCase();
164
+ for (const cref of caller.features.conditionReferences ?? []) {
165
+ if (cref.identifier !== actualLower) continue;
166
+ const r = cref.referenceAnchor.range;
167
+ if (r.startLine < callRow) continue;
168
+ if (r.startLine === callRow && r.startColumn <= callCol) continue;
169
+ postCallGuards.push(cref);
170
+ }
171
+ }
172
+
173
+ // Guarded writes: full caller transitive writes when at least one post-call guard
174
+ // exists. Branch-level slicing (then vs else) is deferred to T3.
175
+ let guardedTablesWritten: TableId[] = [];
176
+ if (postCallGuards.length > 0 && caller.summary !== undefined) {
177
+ guardedTablesWritten = writesTablesOf(caller.summary);
178
+ }
179
+
180
+ out.push({
181
+ publisherRoutine: publisher.id,
182
+ eventId: publisherEventEntry,
183
+ callerRoutine: caller.id,
184
+ callsiteId: edge.callsiteId,
185
+ handledActual,
186
+ postCallGuards,
187
+ guardedTablesWritten,
188
+ });
189
+ }
190
+
191
+ out.sort(
192
+ (a, b) =>
193
+ compareStrings(a.eventId, b.eventId) ||
194
+ compareStrings(a.callerRoutine, b.callerRoutine) ||
195
+ compareStrings(a.callsiteId, b.callsiteId),
196
+ );
197
+
198
+ return out;
199
+ }
@@ -0,0 +1,81 @@
1
+ import type { TempState } from "../model/entities.ts";
2
+ import type { TableId } from "../model/ids.ts";
3
+ import type { DbEffect, EffectPresence } from "../model/summary.ts";
4
+ import { compareStrings } from "./uncertainty-util.ts";
5
+
6
+ // --- tri-state presence: no < unknown < yes ---
7
+ const PRESENCE_RANK: Record<EffectPresence, number> = { no: 0, unknown: 1, yes: 2 };
8
+ const PRESENCE_BY_RANK: EffectPresence[] = ["no", "unknown", "yes"];
9
+
10
+ /** Lattice join: the more-informative presence wins (yes > unknown > no). Monotone. */
11
+ export function joinPresence(a: EffectPresence, b: EffectPresence): EffectPresence {
12
+ const rank = Math.max(PRESENCE_RANK[a], PRESENCE_RANK[b]);
13
+ return PRESENCE_BY_RANK[rank] ?? "unknown";
14
+ }
15
+
16
+ /** Set union of two `writesTables` values; `"unknown"` absorbs. Result is sorted. */
17
+ export function unionTables(
18
+ a: TableId[] | "unknown",
19
+ b: TableId[] | "unknown",
20
+ ): TableId[] | "unknown" {
21
+ if (a === "unknown" || b === "unknown") return "unknown";
22
+ return [...new Set([...a, ...b])].sort();
23
+ }
24
+
25
+ /** Normalise a TempState to a short stable key fragment. */
26
+ function tempStateKey(t: TempState): string {
27
+ if (t.kind === "known") return t.value ? "t" : "f";
28
+ if (t.kind === "parameter-dependent") return `p${t.parameterIndex}`;
29
+ return "u";
30
+ }
31
+
32
+ /**
33
+ * Stable, path-insensitive effect key. Deliberately EXCLUDES `via` — two DbEffects for the
34
+ * same operation are the same fact regardless of how they propagated. Used to de-dupe.
35
+ */
36
+ export function effectKeyOf(
37
+ e: Pick<DbEffect, "op" | "tableId" | "operationId" | "tempState">,
38
+ ): string {
39
+ return `${e.op}|${e.tableId}|${e.operationId}|${tempStateKey(e.tempState)}`;
40
+ }
41
+
42
+ // --- via precedence: most specific wins ---
43
+ const VIA_RANK: Record<DbEffect["via"], number> = {
44
+ direct: 4,
45
+ "implicit-trigger": 3,
46
+ "event-subscriber": 2,
47
+ dynamic: 1,
48
+ inherited: 0,
49
+ };
50
+
51
+ /** Merge two `via` tags, keeping the most specific (direct > implicit-trigger > event-subscriber > dynamic > inherited). */
52
+ export function mergeVia(a: DbEffect["via"], b: DbEffect["via"]): DbEffect["via"] {
53
+ return VIA_RANK[a] >= VIA_RANK[b] ? a : b;
54
+ }
55
+
56
+ /**
57
+ * Concatenate and de-dupe DbEffects by `effectKey`. The key is always recomputed via
58
+ * `effectKeyOf` — a caller-supplied `effectKey` is treated as a write target only, never
59
+ * trusted as input, so a divergent caller scheme can't cause silent dedupe failures. When
60
+ * two effects share a key, `via` is merged by precedence. Result is sorted by
61
+ * `(effectKey, operationId)` for determinism.
62
+ */
63
+ export function mergeDbEffects(...lists: DbEffect[][]): DbEffect[] {
64
+ const byKey = new Map<string, DbEffect>();
65
+ for (const list of lists) {
66
+ for (const e of list) {
67
+ const key = effectKeyOf(e);
68
+ const normalized: DbEffect = { ...e, effectKey: key };
69
+ const existing = byKey.get(key);
70
+ if (existing) {
71
+ byKey.set(key, { ...existing, via: mergeVia(existing.via, normalized.via) });
72
+ } else {
73
+ byKey.set(key, normalized);
74
+ }
75
+ }
76
+ }
77
+ return [...byKey.values()].sort((a, b) => {
78
+ if (a.effectKey !== b.effectKey) return compareStrings(a.effectKey, b.effectKey);
79
+ return compareStrings(a.operationId, b.operationId);
80
+ });
81
+ }
@@ -0,0 +1,57 @@
1
+ import { roleOf } from "../model/entities.ts";
2
+ import type { RoutineId } from "../model/ids.ts";
3
+ import type { SemanticModel } from "../model/model.ts";
4
+
5
+ /**
6
+ * Identify primary-app routines that BC dispatches to without an in-app caller — the root
7
+ * set for reachability analysis. An entry point is a primary-app routine whose `kind` is
8
+ * `trigger` (OnRun, OnOpenPage, OnValidate, OnInsert, etc.) or `event-subscriber`.
9
+ *
10
+ * Used by D8 (transaction-span roots). For D14's forward-reachability question, use
11
+ * `findReachableRoots` instead — it adds non-local procedures as roots, because they
12
+ * may be called from outside the app.
13
+ */
14
+ export function findEntryPoints(model: SemanticModel): RoutineId[] {
15
+ const out: RoutineId[] = [];
16
+ for (const r of model.routines) {
17
+ if (roleOf(r) !== "primary") continue;
18
+ if (r.kind === "event-subscriber" || r.kind === "trigger") {
19
+ out.push(r.id);
20
+ }
21
+ }
22
+ return out.sort();
23
+ }
24
+
25
+ /**
26
+ * Root set for "is this routine ever invoked?" reachability analysis (D14 dead-routine).
27
+ * Adds to `findEntryPoints` the procedures we cannot prove are app-scoped:
28
+ * - default-access (public): callable from any app that declares us as a dependency;
29
+ * - `protected`: callable from overriding codeunits;
30
+ * - `internal`: callable only from apps named in this workspace's `internalsVisibleTo`.
31
+ *
32
+ * `internal` is added as a root only when `internalReachableExternally` is true (some
33
+ * app is granted access). With no `internalsVisibleTo` entries, `internal` is
34
+ * effectively app-scoped, so its dead-routine reachability collapses to the `local`
35
+ * case — D14 then flags `internal` procedures with no in-app caller.
36
+ *
37
+ * D8's transaction-span computation deliberately does NOT use this — its roots are
38
+ * trigger/event boundaries (the natural unit of work), not every exported procedure.
39
+ */
40
+ export function findReachableRoots(
41
+ model: SemanticModel,
42
+ opts: { internalReachableExternally: boolean } = { internalReachableExternally: false },
43
+ ): RoutineId[] {
44
+ const out: RoutineId[] = [];
45
+ for (const r of model.routines) {
46
+ if (roleOf(r) !== "primary") continue;
47
+ if (r.kind === "event-subscriber" || r.kind === "trigger") {
48
+ out.push(r.id);
49
+ continue;
50
+ }
51
+ if (r.kind !== "procedure") continue;
52
+ if (r.accessModifier === "local") continue;
53
+ if (r.accessModifier === "internal" && !opts.internalReachableExternally) continue;
54
+ out.push(r.id);
55
+ }
56
+ return out.sort();
57
+ }