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,110 @@
1
+ import type { RoutineId } from "../model/ids.ts";
2
+
3
+ /** Minimal graph shape tarjanScc needs: sorted nodes + outgoing edges that carry `to`. */
4
+ export interface SccInputGraph {
5
+ nodes: readonly RoutineId[];
6
+ edgesByFrom: ReadonlyMap<RoutineId, readonly { to: RoutineId }[]>;
7
+ }
8
+
9
+ /** One strongly-connected component. `recursive` = size > 1 or has a self-edge. */
10
+ export interface Scc {
11
+ members: RoutineId[]; // sorted
12
+ recursive: boolean;
13
+ }
14
+
15
+ export interface SccResult {
16
+ /** SCCs in reverse-topological order: callees before callers. */
17
+ sccs: Scc[];
18
+ /** routineId -> index into `sccs`. */
19
+ sccIdByRoutine: Map<RoutineId, number>;
20
+ }
21
+
22
+ /**
23
+ * Tarjan's SCC algorithm over the combined graph. Iterative (no recursion — AL call graphs
24
+ * can be deep). Tarjan emits SCCs in reverse-topological order naturally, which is exactly
25
+ * the bottom-up order the summary engine wants. Node iteration follows `graph.nodes` (sorted)
26
+ * and edge iteration follows the pre-sorted edge lists, so the result is deterministic.
27
+ */
28
+ export function tarjanScc(graph: SccInputGraph): SccResult {
29
+ let nextIndex = 0;
30
+ const index = new Map<RoutineId, number>();
31
+ const lowlink = new Map<RoutineId, number>();
32
+ const onStack = new Set<RoutineId>();
33
+ const stack: RoutineId[] = [];
34
+ const rawSccs: RoutineId[][] = [];
35
+
36
+ // Explicit work stack: each frame is a node plus its next-child cursor.
37
+ for (const start of graph.nodes) {
38
+ if (index.has(start)) continue;
39
+ const work: { node: RoutineId; childIdx: number }[] = [{ node: start, childIdx: 0 }];
40
+
41
+ while (work.length > 0) {
42
+ const frame = work[work.length - 1];
43
+ if (frame === undefined) break;
44
+ const { node } = frame;
45
+
46
+ if (frame.childIdx === 0) {
47
+ index.set(node, nextIndex);
48
+ lowlink.set(node, nextIndex);
49
+ nextIndex++;
50
+ stack.push(node);
51
+ onStack.add(node);
52
+ }
53
+
54
+ const children = graph.edgesByFrom.get(node) ?? [];
55
+ if (frame.childIdx < children.length) {
56
+ const child = children[frame.childIdx];
57
+ frame.childIdx++;
58
+ if (child === undefined) continue;
59
+ const to = child.to;
60
+ if (!index.has(to)) {
61
+ work.push({ node: to, childIdx: 0 });
62
+ } else if (onStack.has(to)) {
63
+ const cur = lowlink.get(node) ?? 0;
64
+ const toIdx = index.get(to) ?? 0;
65
+ lowlink.set(node, Math.min(cur, toIdx));
66
+ }
67
+ continue;
68
+ }
69
+
70
+ // All children processed — settle this node.
71
+ if (lowlink.get(node) === index.get(node)) {
72
+ const members: RoutineId[] = [];
73
+ while (true) {
74
+ const w = stack.pop();
75
+ if (w === undefined) break;
76
+ onStack.delete(w);
77
+ members.push(w);
78
+ if (w === node) break;
79
+ }
80
+ rawSccs.push(members);
81
+ }
82
+ work.pop();
83
+ const parent = work[work.length - 1];
84
+ if (parent !== undefined) {
85
+ const pCur = lowlink.get(parent.node) ?? 0;
86
+ const nCur = lowlink.get(node) ?? 0;
87
+ lowlink.set(parent.node, Math.min(pCur, nCur));
88
+ }
89
+ }
90
+ }
91
+
92
+ // rawSccs is already in reverse-topological order (Tarjan property).
93
+ const sccs: Scc[] = [];
94
+ const sccIdByRoutine = new Map<RoutineId, number>();
95
+ for (const members of rawSccs) {
96
+ const sorted = [...members].sort();
97
+ let recursive = sorted.length > 1;
98
+ if (!recursive) {
99
+ const only = sorted[0];
100
+ if (only !== undefined) {
101
+ recursive = (graph.edgesByFrom.get(only) ?? []).some((e) => e.to === only);
102
+ }
103
+ }
104
+ const sccId = sccs.length;
105
+ sccs.push({ members: sorted, recursive });
106
+ for (const m of sorted) sccIdByRoutine.set(m, sccId);
107
+ }
108
+
109
+ return { sccs, sccIdByRoutine };
110
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Source-anchor ordering helpers shared by detectors that need to compare positions
3
+ * within a single routine. AL positions are `(startLine, startColumn)` zero-or-one-
4
+ * based pairs (whichever the parser emits — we only compare, never display).
5
+ *
6
+ * Extracted here so detectors d11/d21/d22/d33/d36/d37/d39/d40/d41/d42 share one
7
+ * definition; ten byte-identical copies had drifted into separate files and any
8
+ * future tweak (e.g. ordering by an `index` tiebreaker on equal positions) would
9
+ * have had to land in ten places.
10
+ */
11
+
12
+ /** Minimal shape accepted by `beforeAnchor` — just the line/column pair. */
13
+ export interface PositionLike {
14
+ range: { startLine: number; startColumn: number };
15
+ }
16
+
17
+ /**
18
+ * Strict "is `a` source-anchor positioned strictly before `b`" predicate.
19
+ * Returns `false` when the positions are equal (so callers can treat the
20
+ * comparison as a half-open "earlier than" check without re-handling ties).
21
+ */
22
+ export function beforeAnchor(a: PositionLike, b: PositionLike): boolean {
23
+ if (a.range.startLine !== b.range.startLine) return a.range.startLine < b.range.startLine;
24
+ return a.range.startColumn < b.range.startColumn;
25
+ }
@@ -0,0 +1,104 @@
1
+ // src/engine/summary-context.ts
2
+ // One-time O(1) lookup indexes for the per-routine summary fixed-point.
3
+ //
4
+ // The fixed-point composer touches `model.routines`, `model.callGraph`, `model.tables`,
5
+ // `model.eventGraph.events`, and `graph.uncertaintyEdges` once per edge per routine per
6
+ // iteration. Linear `.find()` over those arrays is the dominant cold-run cost on large
7
+ // dependencies (10k+ routines): a 50k-routine app turns each scan into ~50k ops, and
8
+ // composing 50k routines × ~3 edges = ~7.5 billion array scans.
9
+ //
10
+ // This context builds the lookups once at the top of `runSummaries` and threads them
11
+ // through `composeRoutine` / `baseIntraproceduralSummary` / `resolvePublishedEvent` /
12
+ // `computeRecordRoles` / `computeFieldEffects`. No behavior change — pure data
13
+ // access path swap (O(N²) → O(N)).
14
+
15
+ import type { Routine, Table } from "../model/entities.ts";
16
+ import type { CallEdge, EventSymbol } from "../model/graph.ts";
17
+ import type { CallsiteId, EventId, OperationId, RoutineId, TableId } from "../model/ids.ts";
18
+ import type { SemanticModel } from "../model/model.ts";
19
+ import type { CombinedGraph, UncertaintyEdge } from "./combined-graph.ts";
20
+
21
+ export interface SummaryContext {
22
+ routineById: Map<RoutineId, Routine>;
23
+ tableById: Map<TableId, Table>;
24
+ /**
25
+ * CallEdges keyed by `operationId`. The summary engine resolves a published event
26
+ * by following `operationId → CallEdge.to (publisher routine) → EventSymbol`. A
27
+ * single operationId has at most one resolved CallEdge in `model.callGraph`, so a
28
+ * `Map<OperationId, CallEdge>` is sufficient (no duplicates).
29
+ */
30
+ resolvedCallEdgeByOperation: Map<OperationId, CallEdge>;
31
+ /**
32
+ * CallEdges keyed by `callsiteId`. Used by the c1b exit-effect composition pass
33
+ * to look up the resolved edge for each call site without a full scan.
34
+ */
35
+ resolvedCallEdgeByCallsite: Map<CallsiteId, CallEdge>;
36
+ /** EventSymbol keyed by `publisherRoutineId` — used to recover the published event id. */
37
+ eventByPublisher: Map<RoutineId, EventSymbol>;
38
+ /**
39
+ * UncertaintyEdges keyed by `from`. composeRoutine previously did
40
+ * `for (ue of graph.uncertaintyEdges) if (ue.from !== routine.id) continue;` — a
41
+ * global scan per routine. With this index the loop only visits its own entries.
42
+ */
43
+ uncertaintyEdgesByFrom: Map<RoutineId, UncertaintyEdge[]>;
44
+ }
45
+
46
+ /**
47
+ * Build all SummaryContext indexes from a SemanticModel and (optionally) a CombinedGraph.
48
+ * If no graph is passed, `uncertaintyEdgesByFrom` is empty — callers that don't traverse
49
+ * uncertainty edges (e.g. test helpers calling `baseIntraproceduralSummary` directly)
50
+ * pay nothing for it.
51
+ *
52
+ * O(R + T + C + E) where R=routines, T=tables, C=callGraph edges, E=event symbols.
53
+ */
54
+ export function buildSummaryContext(model: SemanticModel, graph?: CombinedGraph): SummaryContext {
55
+ const routineById = new Map<RoutineId, Routine>();
56
+ for (const r of model.routines) routineById.set(r.id, r);
57
+
58
+ const tableById = new Map<TableId, Table>();
59
+ for (const t of model.tables) tableById.set(t.id, t);
60
+
61
+ // Mirror the old `resolvePublishedEvent` precondition: the matched CallEdge had to
62
+ // have `to !== undefined`. We only index those — unresolved edges can never satisfy
63
+ // the publisher-routine lookup downstream.
64
+ const resolvedCallEdgeByOperation = new Map<OperationId, CallEdge>();
65
+ const resolvedCallEdgeByCallsite = new Map<CallsiteId, CallEdge>();
66
+ for (const ce of model.callGraph) {
67
+ if (ce.to === undefined) continue;
68
+ // `model.callGraph` may have multiple edges per operationId in pathological cases
69
+ // (e.g. interface multi-target). The old `.find()` returned the FIRST match — we
70
+ // preserve that by keeping the first writer and ignoring later ones.
71
+ if (!resolvedCallEdgeByOperation.has(ce.operationId)) {
72
+ resolvedCallEdgeByOperation.set(ce.operationId, ce);
73
+ }
74
+ if (!resolvedCallEdgeByCallsite.has(ce.callsiteId)) {
75
+ resolvedCallEdgeByCallsite.set(ce.callsiteId, ce);
76
+ }
77
+ }
78
+
79
+ const eventByPublisher = new Map<RoutineId, EventSymbol>();
80
+ for (const sym of model.eventGraph.events) {
81
+ if (sym.publisherRoutineId === undefined) continue;
82
+ if (!eventByPublisher.has(sym.publisherRoutineId)) {
83
+ eventByPublisher.set(sym.publisherRoutineId, sym);
84
+ }
85
+ }
86
+
87
+ const uncertaintyEdgesByFrom = new Map<RoutineId, UncertaintyEdge[]>();
88
+ if (graph !== undefined) {
89
+ for (const ue of graph.uncertaintyEdges) {
90
+ const list = uncertaintyEdgesByFrom.get(ue.from);
91
+ if (list) list.push(ue);
92
+ else uncertaintyEdgesByFrom.set(ue.from, [ue]);
93
+ }
94
+ }
95
+
96
+ return {
97
+ routineById,
98
+ tableById,
99
+ resolvedCallEdgeByOperation,
100
+ resolvedCallEdgeByCallsite,
101
+ eventByPublisher,
102
+ uncertaintyEdgesByFrom,
103
+ };
104
+ }
@@ -0,0 +1,296 @@
1
+ import type { Routine } from "../model/entities.ts";
2
+ import type { EventId, FieldId, OperationId, RoutineId, TableId } from "../model/ids.ts";
3
+ import type { SemanticModel } from "../model/model.ts";
4
+ import type {
5
+ DbEffect,
6
+ EffectPresence,
7
+ FieldEffectSet,
8
+ RecordRoleSummary,
9
+ RoutineSummary,
10
+ } from "../model/summary.ts";
11
+ import { effectKeyOf, joinPresence } from "./effect-lattice.ts";
12
+ import { classifyOp, isDbTouchingClass, recordFlowRoleOf } from "./op-classification.ts";
13
+ import { type SummaryContext, buildSummaryContext } from "./summary-context.ts";
14
+ import { compareStrings } from "./uncertainty-util.ts";
15
+
16
+ /**
17
+ * Resolve which event a call site raises. Phase 1 does not emit `event-publish` operation
18
+ * sites — a published event is a CallEdge whose `to` routine has kind "event-publisher".
19
+ * Returns the EventId of the matching EventSymbol, or undefined if the operation is not an
20
+ * event publish.
21
+ *
22
+ * O(1) variant — used inside the fixed-point composer. The two `Map` lookups replace the
23
+ * old `model.callGraph.find()` + `model.eventGraph.events.find()` pair, which dominated
24
+ * cold-run cost on large dependencies (~50G+ ops on Base App).
25
+ */
26
+ export function resolvePublishedEventCtx(
27
+ operationId: OperationId,
28
+ ctx: SummaryContext,
29
+ ): EventId | undefined {
30
+ const edge = ctx.resolvedCallEdgeByOperation.get(operationId);
31
+ if (edge?.to === undefined) return undefined;
32
+ return ctx.eventByPublisher.get(edge.to)?.id;
33
+ }
34
+
35
+ /**
36
+ * Public, model-based form kept for callers outside the fixed-point (d2 detector, tests).
37
+ * Builds a one-shot SummaryContext per call — fine for cold call sites, NOT for hot loops.
38
+ */
39
+ export function resolvePublishedEvent(
40
+ operationId: OperationId,
41
+ model: SemanticModel,
42
+ ): EventId | undefined {
43
+ return resolvePublishedEventCtx(operationId, buildSummaryContext(model));
44
+ }
45
+
46
+ /**
47
+ * Derive a RecordRoleSummary per record parameter from a routine's intraprocedural
48
+ * features. Field names are resolved to FieldId via the parameter record's table. Unresolved
49
+ * field names are skipped here (they surface as D3 bailouts, not silent drops).
50
+ *
51
+ * O(1) variant — used inside the fixed-point composer. Replaces `model.tables.find()` per
52
+ * record parameter with a single `Map` lookup.
53
+ */
54
+ export function computeRecordRolesCtx(routine: Routine, ctx: SummaryContext): RecordRoleSummary[] {
55
+ const out: RecordRoleSummary[] = [];
56
+ for (const param of routine.parameters) {
57
+ if (!param.isRecord) continue;
58
+ const recVar = routine.features.recordVariables.find(
59
+ (rv) => rv.isParameter && rv.parameterIndex === param.index,
60
+ );
61
+ if (recVar === undefined) continue; // missing RecordVariable — skip rather than corrupt
62
+ const tableId = recVar.tableId;
63
+ const table = tableId !== undefined ? ctx.tableById.get(tableId) : undefined;
64
+
65
+ const resolveField = (fieldName: string): FieldId | undefined =>
66
+ table?.fields.find((f) => f.name.toLowerCase() === fieldName.toLowerCase())?.id;
67
+
68
+ const recVarName = recVar.name.toLowerCase();
69
+ // Phase 1's FieldAccess does not distinguish read vs. direct-assignment write, so
70
+ // readsFields is "all dot-notation field accesses" on this parameter. Targeted writes
71
+ // (Validate ops) feed writesFields separately below.
72
+ const readsFields: FieldId[] = [];
73
+ const writesFields: FieldId[] = [];
74
+ for (const fa of routine.features.fieldAccesses) {
75
+ if (fa.recordVariableName.toLowerCase() !== recVarName) continue;
76
+ const fid = resolveField(fa.fieldName);
77
+ if (fid !== undefined) readsFields.push(fid);
78
+ }
79
+
80
+ let mayResetFilters = false;
81
+ let mayChangeLoadFields = false;
82
+ let mayAssignRecord = false;
83
+ let loadsFromDbParam: EffectPresence = "no";
84
+ let initialisesParam: EffectPresence = "no";
85
+ let persistsCurrentRecord: EffectPresence = "no";
86
+ let setBasedDbWrites: EffectPresence = "no";
87
+ let validatesParam: EffectPresence = "no";
88
+ let copiesIntoParam: EffectPresence = "no";
89
+ let resetsFiltersOnParam: EffectPresence = "no";
90
+
91
+ for (const op of routine.features.recordOperations) {
92
+ if (op.recordVariableName.toLowerCase() !== recVarName) continue;
93
+ if (op.op === "Validate") {
94
+ for (const arg of op.fieldArguments ?? []) {
95
+ const fid = resolveField(arg);
96
+ if (fid !== undefined) writesFields.push(fid);
97
+ }
98
+ }
99
+ if (op.op === "Reset" || op.op === "Copy") mayResetFilters = true;
100
+ if (op.op === "SetLoadFields" || op.op === "AddLoadFields" || op.op === "Reset")
101
+ mayChangeLoadFields = true;
102
+ if (op.op === "Copy" || op.op === "TransferFields") mayAssignRecord = true;
103
+
104
+ // May-fact bootstrap — record-flow op roles.
105
+ const role = recordFlowRoleOf(op.op);
106
+ switch (role) {
107
+ case "loadsFromDb":
108
+ loadsFromDbParam = "yes";
109
+ break;
110
+ case "initialises":
111
+ initialisesParam = "yes";
112
+ break;
113
+ case "persistsCurrent":
114
+ persistsCurrentRecord = "yes";
115
+ break;
116
+ case "setBasedWrite":
117
+ setBasedDbWrites = "yes";
118
+ break;
119
+ case "validates":
120
+ validatesParam = "yes";
121
+ break;
122
+ case "copiesInto":
123
+ copiesIntoParam = "yes";
124
+ break;
125
+ case "resetsFilter":
126
+ resetsFiltersOnParam = "yes";
127
+ break;
128
+ }
129
+ }
130
+
131
+ // RecordRef / FieldRef / Variant params: detectable from the raw type text.
132
+ const mayUseRecordRef = /\b(RecordRef|FieldRef|Variant)\b/i.test(param.typeText);
133
+
134
+ out.push({
135
+ parameterIndex: param.index,
136
+ tableId: tableId ?? "unknown",
137
+ readsFields: mayUseRecordRef ? "unknown" : [...new Set(readsFields)].sort(),
138
+ writesFields: mayUseRecordRef ? "unknown" : [...new Set(writesFields)].sort(),
139
+ mayResetFilters,
140
+ mayChangeLoadFields,
141
+ mayAssignRecord,
142
+ mayUseRecordRef,
143
+ // Entry requirements — populated in Phase 4.
144
+ requiresLoadedAtEntry: "unknown",
145
+ requiredLoadedFieldsAtEntry: "unknown",
146
+ mutatesBeforeLoad: "unknown",
147
+ // Exit-effect may-facts — populated this phase.
148
+ persistsCurrentRecord,
149
+ setBasedDbWrites,
150
+ validatesParam,
151
+ copiesIntoParam,
152
+ resetsFiltersOnParam,
153
+ // Path-aware exit-effect facts — populated in Phase 6.
154
+ dirtyAtExit: "unknown",
155
+ currentLoadedFieldsAtExit: "unknown",
156
+ // Convenience derivations.
157
+ mutatesParam: joinPresence(
158
+ joinPresence(persistsCurrentRecord, validatesParam),
159
+ copiesIntoParam,
160
+ ),
161
+ loadsFromDbParam,
162
+ initialisesParam,
163
+ });
164
+ }
165
+ return out.sort((a, b) => a.parameterIndex - b.parameterIndex);
166
+ }
167
+
168
+ /**
169
+ * Public, model-based form kept for callers outside the fixed-point (tests, helpers).
170
+ * Builds a one-shot SummaryContext — fine for one-off calls, NOT for hot loops.
171
+ */
172
+ export function computeRecordRoles(routine: Routine, model: SemanticModel): RecordRoleSummary[] {
173
+ return computeRecordRolesCtx(routine, buildSummaryContext(model));
174
+ }
175
+
176
+ /**
177
+ * Build a routine's summary from its OWN intraprocedural features only — no callee
178
+ * composition. Recomputed every fixed-point pass (never inherited), so opaque /
179
+ * parse-incomplete facts are always re-applied.
180
+ *
181
+ * O(1)-lookup variant — used inside the fixed-point composer.
182
+ */
183
+ export function baseIntraproceduralSummaryCtx(
184
+ routine: Routine,
185
+ ctx: SummaryContext,
186
+ ): RoutineSummary {
187
+ const parameterRoles = computeRecordRolesCtx(routine, ctx);
188
+
189
+ // Opaque (.app symbol, no body) — unknown everything; the caller (which has the
190
+ // callsiteId) attaches the opaque-callee uncertainty, not this routine itself.
191
+ if (!routine.bodyAvailable) {
192
+ return {
193
+ routineId: routine.id,
194
+ dbEffects: [],
195
+ inRecursiveCycle: false,
196
+ hasUnresolvedCalls: true,
197
+ // the opaque-callee uncertainty is created at the call site, not here
198
+ uncertainties: [],
199
+ parameterRoles,
200
+ };
201
+ }
202
+
203
+ // Parse-incomplete — body present but unparseable; record the typed uncertainty.
204
+ if (routine.parseIncomplete) {
205
+ return {
206
+ routineId: routine.id,
207
+ dbEffects: [],
208
+ inRecursiveCycle: false,
209
+ hasUnresolvedCalls: true,
210
+ uncertainties: [{ kind: "parse-incomplete", routineId: routine.id }],
211
+ parameterRoles,
212
+ };
213
+ }
214
+
215
+ // Body available + parsed — derive direct facts from the operation stream.
216
+ const dbEffects: DbEffect[] = [];
217
+
218
+ for (const op of routine.features.recordOperations) {
219
+ const cls = classifyOp(op.op);
220
+ if (!isDbTouchingClass(cls)) continue; // state-only / trigger ops do not touch the DB
221
+ const tableId: TableId | "unknown" = op.tableId ?? "unknown";
222
+ const effect: DbEffect = {
223
+ effectKey: effectKeyOf({
224
+ op: op.op,
225
+ tableId,
226
+ operationId: op.id,
227
+ tempState: op.tempState,
228
+ }),
229
+ operationId: op.id,
230
+ op: op.op,
231
+ tableId,
232
+ recordVariableId: op.recordVariableId,
233
+ tempState: op.tempState,
234
+ via: "direct",
235
+ };
236
+ dbEffects.push(effect);
237
+ }
238
+
239
+ return {
240
+ routineId: routine.id,
241
+ dbEffects: dbEffects.sort((a, b) => compareStrings(a.effectKey, b.effectKey)),
242
+ inRecursiveCycle: false,
243
+ hasUnresolvedCalls: false,
244
+ uncertainties: [],
245
+ parameterRoles,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Public, model-based form. Builds a one-shot SummaryContext per call — fine for tests,
251
+ * NOT for hot loops. The fixed-point composer uses `baseIntraproceduralSummaryCtx` directly.
252
+ */
253
+ export function baseIntraproceduralSummary(routine: Routine, model: SemanticModel): RoutineSummary {
254
+ return baseIntraproceduralSummaryCtx(routine, buildSummaryContext(model));
255
+ }
256
+
257
+ // Orchestration moved to summary-runner.ts in Phase 2c. Re-exported so existing import
258
+ // sites (src/index.ts, detector tests) keep importing `computeSummaries` from here.
259
+ export { computeSummaries } from "./summary-runner.ts";
260
+
261
+ /**
262
+ * Lazy field-effects seam — compute the field-effect set for one routine on demand.
263
+ *
264
+ * This function is intentionally NOT yet consumed by D3. D3 requires window-scoped field
265
+ * accesses (accesses that occur between a retrieval op and the next invalidating op), which
266
+ * this routine-level snapshot cannot provide. `computeFieldEffects` is kept as the interface
267
+ * for future on-demand field-effect queries once those callers exist; do not wire it into D3
268
+ * until the window-scoping concern is resolved.
269
+ *
270
+ * Groups resolved field reads by record-variable name.
271
+ */
272
+ export function computeFieldEffectsCtx(routineId: RoutineId, ctx: SummaryContext): FieldEffectSet {
273
+ const routine = ctx.routineById.get(routineId);
274
+ const readsByRecordVariable: Record<string, string[]> = {};
275
+ if (routine === undefined) return { readsByRecordVariable };
276
+
277
+ for (const fa of routine.features.fieldAccesses) {
278
+ const recVar = routine.features.recordVariables.find(
279
+ (rv) => rv.name.toLowerCase() === fa.recordVariableName.toLowerCase(),
280
+ );
281
+ const table = recVar?.tableId !== undefined ? ctx.tableById.get(recVar.tableId) : undefined;
282
+ const fieldId = table?.fields.find(
283
+ (f) => f.name.toLowerCase() === fa.fieldName.toLowerCase(),
284
+ )?.id;
285
+ if (fieldId === undefined) continue;
286
+ const key = fa.recordVariableName;
287
+ const list = readsByRecordVariable[key] ?? [];
288
+ if (!list.includes(fieldId)) list.push(fieldId);
289
+ readsByRecordVariable[key] = list.sort();
290
+ }
291
+ return { readsByRecordVariable };
292
+ }
293
+
294
+ export function computeFieldEffects(routineId: RoutineId, model: SemanticModel): FieldEffectSet {
295
+ return computeFieldEffectsCtx(routineId, buildSummaryContext(model));
296
+ }