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,117 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join, relative, sep } from "node:path";
4
+ import { sha256OfStrings } from "../hash.ts";
5
+ import type { AppIdentity } from "../model/identity.ts";
6
+ import type { ProviderDiagnostic, ProviderResult, SourceProvider, SourceUnit } from "./types.ts";
7
+
8
+ /**
9
+ * Directory names the walk skips. Kept deliberately minimal: only directories
10
+ * that NEVER carry user-authored source. `node_modules` is the JS-tooling cache
11
+ * (large; never AL); `.alpackages` is read separately by the app-package
12
+ * reader as binary `.app` files (walking it as a directory would surface those
13
+ * binary payloads, which the `.al` extension filter ignores anyway, but
14
+ * skipping avoids the no-op walk on potentially thousands of entries).
15
+ *
16
+ * Everything else — including dot-prefixed directories the user puts in their
17
+ * workspace — is walked. A folder is just a folder; its name carries no
18
+ * semantic meaning for analysis.
19
+ */
20
+ const SKIP_DIR_EXACT: ReadonlySet<string> = new Set(["node_modules", ".alpackages"]);
21
+
22
+ /** Recursively list files under a directory. */
23
+ async function walk(dir: string): Promise<string[]> {
24
+ const out: string[] = [];
25
+ let entries: { name: string; isDirectory(): boolean }[];
26
+ try {
27
+ entries = await readdir(dir, { withFileTypes: true });
28
+ } catch {
29
+ return out;
30
+ }
31
+ for (const entry of entries) {
32
+ const full = join(dir, entry.name);
33
+ if (entry.isDirectory()) {
34
+ if (SKIP_DIR_EXACT.has(entry.name)) continue;
35
+ out.push(...(await walk(full)));
36
+ } else {
37
+ out.push(full);
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /** Enumerates .al files in a workspace directory and reads its app.json identity. */
44
+ export class WorkspaceProvider implements SourceProvider {
45
+ readonly name = "workspace" as const;
46
+
47
+ async collect(rootPath: string): Promise<ProviderResult> {
48
+ const diagnostics: ProviderDiagnostic[] = [];
49
+
50
+ // --- app.json identity ---
51
+ let appGuid = "unknown";
52
+ let appName = "unknown";
53
+ let appPublisher = "unknown";
54
+ let appVersion = "0.0.0.0";
55
+ try {
56
+ const appJsonRaw = readFileSync(join(rootPath, "app.json"), "utf8");
57
+ const appJson = JSON.parse(appJsonRaw) as {
58
+ id?: string;
59
+ name?: string;
60
+ publisher?: string;
61
+ version?: string;
62
+ };
63
+ appGuid = appJson.id ?? appGuid;
64
+ appName = appJson.name ?? appName;
65
+ appPublisher = appJson.publisher ?? appPublisher;
66
+ appVersion = appJson.version ?? appVersion;
67
+ } catch {
68
+ diagnostics.push({
69
+ severity: "warning",
70
+ message: `No readable app.json at ${rootPath} — workspace app identity unknown`,
71
+ sourceRef: rootPath,
72
+ });
73
+ }
74
+
75
+ // --- enumerate .al files ---
76
+ const allFiles = await walk(rootPath);
77
+ const units: SourceUnit[] = [];
78
+ const contents: string[] = [];
79
+ for (const absPath of allFiles) {
80
+ if (!absPath.toLowerCase().endsWith(".al")) continue;
81
+ let content: string;
82
+ try {
83
+ content = readFileSync(absPath, "utf8");
84
+ } catch (err) {
85
+ diagnostics.push({
86
+ severity: "warning",
87
+ message: `Could not read ${absPath}: ${(err as Error).message}`,
88
+ sourceRef: absPath,
89
+ });
90
+ continue;
91
+ }
92
+ const relativePath = relative(rootPath, absPath).split(sep).join("/");
93
+ units.push({
94
+ id: `ws:${relativePath}`,
95
+ kind: "source",
96
+ appGuid,
97
+ relativePath,
98
+ absolutePath: absPath,
99
+ content,
100
+ sourceProvider: "workspace",
101
+ analysisRole: "primary",
102
+ });
103
+ contents.push(content);
104
+ }
105
+
106
+ const identity: AppIdentity = {
107
+ appGuid,
108
+ publisher: appPublisher,
109
+ name: appName,
110
+ version: appVersion,
111
+ sourceAggregateHash: sha256OfStrings(contents.sort()),
112
+ sourceKind: "workspace",
113
+ };
114
+
115
+ return { units, apps: [identity], diagnostics };
116
+ }
117
+ }
@@ -0,0 +1,117 @@
1
+ import type { ObjectRunKind } from "../model/callee.ts";
2
+ import type { CallSite, Routine } from "../model/entities.ts";
3
+ import { type CallEdge, type DispatchKind, TREE_SITTER_EVIDENCE } from "../model/graph.ts";
4
+ import type { SemanticIndex } from "../model/model.ts";
5
+ import type { SymbolTable } from "./symbol-table.ts";
6
+
7
+ /** Map an object-run objectKind to its CallEdge dispatch kind. */
8
+ function objectRunDispatchKind(objectKind: ObjectRunKind): DispatchKind {
9
+ if (objectKind === "Page") return "page-run";
10
+ if (objectKind === "Report") return "report-run";
11
+ return "codeunit-run";
12
+ }
13
+
14
+ /**
15
+ * Upgrade a callsite's argumentBindings with callee-side var-ness once the callee
16
+ * is known. This upgrades `calleeParameterIsVar` and sets `bindingResolution` to
17
+ * "resolved" for any binding that was previously "unresolved-callee".
18
+ * Bindings with `bindingResolution === "non-record-arg"` are left untouched.
19
+ *
20
+ * See CallSite.argumentBindings JSDoc in entities.ts for the mutation contract:
21
+ * this function is the sole permitted writer to existing bindings, and
22
+ * re-entrant resolution is unsupported. The assertion below makes any future
23
+ * double-upgrade fail loudly instead of silently corrupting bindings.
24
+ */
25
+ function upgradeBindings(callSite: CallSite, calleeRoutine: Routine): void {
26
+ // Sanity: re-running upgradeBindings on the same callsite is unsound (see
27
+ // entities.ts CallSite.argumentBindings mutation contract). Cheap dev-time check:
28
+ for (const binding of callSite.argumentBindings) {
29
+ if (binding.bindingResolution === "resolved" || binding.bindingResolution === "ambiguous") {
30
+ throw new Error(
31
+ `call-resolver: argumentBindings for callsite ${callSite.id} already upgraded; re-entrant resolution is not supported`,
32
+ );
33
+ }
34
+ }
35
+ const calleeParams = calleeRoutine.parameters;
36
+ for (let i = 0; i < callSite.argumentBindings.length; i++) {
37
+ const binding = callSite.argumentBindings[i];
38
+ if (binding === undefined) continue;
39
+ if (binding.bindingResolution === "non-record-arg") continue;
40
+ const calleeParam = calleeParams[i];
41
+ if (calleeParam === undefined) continue; // arity mismatch — leave defaults
42
+ binding.calleeParameterIsVar = calleeParam.isVar;
43
+ binding.bindingResolution = "resolved";
44
+ }
45
+ }
46
+
47
+ /** Resolve one call site within `routine` into a CallEdge. */
48
+ function resolveCallSite(routine: Routine, callSite: CallSite, symbols: SymbolTable): CallEdge {
49
+ const base = {
50
+ from: routine.id,
51
+ callsiteId: callSite.id,
52
+ operationId: callSite.operationId,
53
+ provenance: [TREE_SITTER_EVIDENCE],
54
+ };
55
+ const callee = callSite.callee;
56
+
57
+ switch (callee.kind) {
58
+ case "bare": {
59
+ // A bare call resolves to a procedure in the SAME object (AL has no free functions).
60
+ const target = symbols.routineInObject(routine.objectId, callee.name);
61
+ if (target) {
62
+ // Phase 2: upgrade argumentBindings now that we know the callee signature.
63
+ upgradeBindings(callSite, target);
64
+ return { ...base, to: target.id, dispatchKind: "direct", resolution: "resolved" };
65
+ }
66
+ return { ...base, dispatchKind: "unresolved", resolution: "unknown" };
67
+ }
68
+ case "object-run": {
69
+ const dispatchKind = objectRunDispatchKind(callee.objectKind);
70
+ if (callee.targetRef === undefined) {
71
+ // Dynamic target (a variable) — known shape, unknown target.
72
+ return { ...base, dispatchKind: "dynamic", resolution: "unknown" };
73
+ }
74
+ const targetObject = callee.targetIsName
75
+ ? symbols.objectByTypeName(callee.targetType, callee.targetRef)
76
+ : symbols.objectByTypeNumber(callee.targetType, Number.parseInt(callee.targetRef, 10));
77
+ if (!targetObject) {
78
+ // Target named/numbered but not in indexed source (e.g. symbol-only dependency).
79
+ return { ...base, dispatchKind, resolution: "opaque" };
80
+ }
81
+ // Resolve to the object's entry routine: OnRun trigger for codeunits, else the
82
+ // first routine. If none, the edge is resolved-to-object but routine-less.
83
+ const entry =
84
+ symbols.routineInObject(targetObject.id, "OnRun") ??
85
+ symbols.routinesInObject(targetObject.id)[0];
86
+ if (entry) {
87
+ // Phase 2: upgrade argumentBindings now that we know the callee signature.
88
+ upgradeBindings(callSite, entry);
89
+ return { ...base, to: entry.id, dispatchKind, resolution: "resolved" };
90
+ }
91
+ return { ...base, dispatchKind, resolution: "opaque" };
92
+ }
93
+ case "member": {
94
+ // A method call on an instance variable. Phase 1 does not type-track non-record
95
+ // variables, so the receiver type is unknown -> unresolved method dispatch.
96
+ return { ...base, dispatchKind: "method", resolution: "unknown" };
97
+ }
98
+ default: {
99
+ return { ...base, dispatchKind: "unresolved", resolution: "unknown" };
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Resolve every call site in the index into a CallEdge. Exactly one edge per call site.
106
+ * Unresolved calls are DATA (a CallEdge with no `to` and a non-"resolved" resolution),
107
+ * never a silent gap.
108
+ */
109
+ export function resolveCalls(index: SemanticIndex, symbols: SymbolTable): CallEdge[] {
110
+ const edges: CallEdge[] = [];
111
+ for (const routine of index.routines) {
112
+ for (const callSite of routine.features.callSites) {
113
+ edges.push(resolveCallSite(routine, callSite, symbols));
114
+ }
115
+ }
116
+ return edges;
117
+ }
@@ -0,0 +1,61 @@
1
+ import type { Diagnostic } from "../model/finding.ts";
2
+ import type { CallEdge } from "../model/graph.ts";
3
+ import type { AnalysisCoverage, SemanticIndex } from "../model/model.ts";
4
+ import type { SourceUnit } from "../providers/types.ts";
5
+
6
+ /**
7
+ * Build the AnalysisCoverage record — the first-class "no silent clean" accounting.
8
+ * `sourceUnitsTotal` counts only `kind: "source"` units. `sourceUnitsParsed` subtracts
9
+ * units that produced a `warning`-severity index diagnostic (the throw path — a unit that
10
+ * parsed but had no object declaration produced an `info` diagnostic and still counts as
11
+ * parsed).
12
+ *
13
+ * `unresolvedCallsites` = every call edge whose target could not be determined
14
+ * (`resolution === "unknown"`). This covers `dispatchKind: "unresolved"`, `"method"` on
15
+ * untyped receivers, `"dynamic"` object-run calls, etc.
16
+ * `dynamicDispatchSites` = the dynamic-dispatch subset (`dispatchKind === "dynamic"`).
17
+ * Overlap between the two is intentional — dynamic sites are a named sub-category of
18
+ * unresolved.
19
+ */
20
+ export function buildCoverage(
21
+ index: SemanticIndex,
22
+ callGraph: CallEdge[],
23
+ units: SourceUnit[],
24
+ indexDiagnostics: Diagnostic[],
25
+ ): AnalysisCoverage {
26
+ const sourceUnits = units.filter((u) => u.kind === "source");
27
+ const failedUnitRefs = new Set(
28
+ indexDiagnostics
29
+ .filter(
30
+ (d): d is Diagnostic & { sourceRef: string } =>
31
+ d.stage === "index" && d.severity === "warning" && d.sourceRef !== undefined,
32
+ )
33
+ .map((d) => d.sourceRef),
34
+ );
35
+ const sourceUnitsParsed = sourceUnits.filter((u) => !failedUnitRefs.has(u.id)).length;
36
+
37
+ const opaqueApps = index.identity.apps
38
+ .filter((a) => a.sourceKind === "symbol-only")
39
+ .map((a) => a.appGuid);
40
+
41
+ const routinesBodyAvailable = index.routines.filter((r) => r.bodyAvailable).length;
42
+ const routinesParseIncomplete = index.routines.filter((r) => r.parseIncomplete).map((r) => r.id);
43
+
44
+ const unresolvedCallsites = callGraph
45
+ .filter((e) => e.resolution === "unknown")
46
+ .map((e) => e.callsiteId);
47
+ const dynamicDispatchSites = callGraph
48
+ .filter((e) => e.dispatchKind === "dynamic")
49
+ .map((e) => e.operationId);
50
+
51
+ return {
52
+ sourceUnitsTotal: sourceUnits.length,
53
+ sourceUnitsParsed,
54
+ routinesTotal: index.routines.length,
55
+ routinesBodyAvailable,
56
+ routinesParseIncomplete,
57
+ opaqueApps,
58
+ unresolvedCallsites,
59
+ dynamicDispatchSites,
60
+ };
61
+ }
@@ -0,0 +1,166 @@
1
+ import { sha256Hex } from "../hash.ts";
2
+ import { type AttributeInfo, findAttribute, qualifiedArg, stringArg } from "../model/attributes.ts";
3
+ import type { Routine } from "../model/entities.ts";
4
+ import {
5
+ type EventEdge,
6
+ type EventGraph,
7
+ type EventSymbol,
8
+ TREE_SITTER_EVIDENCE,
9
+ } from "../model/graph.ts";
10
+ import { encodeEventId, encodeObjectId } from "../model/ids.ts";
11
+ import type { SemanticIndex } from "../model/model.ts";
12
+ import type { SymbolTable } from "./symbol-table.ts";
13
+
14
+ /** Determine the event kind from a publisher routine's structured attributes. */
15
+ function publisherEventKind(attrs: readonly AttributeInfo[]): EventSymbol["eventKind"] {
16
+ if (findAttribute(attrs, "IntegrationEvent") !== undefined) return "integration";
17
+ if (findAttribute(attrs, "BusinessEvent") !== undefined) return "business";
18
+ return "unknown";
19
+ }
20
+
21
+ /**
22
+ * Read an `[EventSubscriber(ObjectType::X, X::"Y", 'EventName', 'ElementName', ...)]`
23
+ * attribute's target parts from the structured `AttributeInfo`, or null if no
24
+ * EventSubscriber is present.
25
+ *
26
+ * Args (positional, per AL spec):
27
+ * 0: ObjectType::<kind> (qualified_enum_value — `value` = the kind)
28
+ * 1: <kind>::<ref> or <kind>::"ref" (database_reference — `member` = the ref)
29
+ * 2: '<eventName>' (string_literal)
30
+ * 3: '<elementName>' (string_literal, may be empty)
31
+ *
32
+ * The grammar already distinguishes quoted vs unquoted target refs via
33
+ * `quoted_identifier` vs `identifier` inside `database_reference`, so the
34
+ * `.member` value is the unquoted ref regardless of source quoting.
35
+ */
36
+ function parseSubscriberAttribute(attrs: readonly AttributeInfo[]): {
37
+ targetObjectType: string;
38
+ targetRef: string;
39
+ eventName: string;
40
+ elementName: string;
41
+ } | null {
42
+ const attr = findAttribute(attrs, "EventSubscriber");
43
+ if (attr === undefined) return null;
44
+ const objectTypeArg = qualifiedArg(attr, 0);
45
+ const targetRefArg = qualifiedArg(attr, 1);
46
+ const eventName = stringArg(attr, 2);
47
+ const elementName = stringArg(attr, 3);
48
+ if (objectTypeArg === undefined || targetRefArg === undefined || eventName === undefined) {
49
+ return null;
50
+ }
51
+ return {
52
+ targetObjectType: objectTypeArg.member,
53
+ targetRef: targetRefArg.member,
54
+ eventName,
55
+ elementName: elementName ?? "",
56
+ };
57
+ }
58
+
59
+ /** Build the EventSymbol for a publisher routine. */
60
+ function buildEventSymbol(routine: Routine, publisherObjectId: string): EventSymbol {
61
+ return {
62
+ id: encodeEventId(publisherObjectId, routine.name),
63
+ publisherObjectId,
64
+ publisherRoutineId: routine.id,
65
+ eventName: routine.name,
66
+ eventKind: publisherEventKind(routine.attributesParsed),
67
+ signatureHash: routine.canonical.normalizedSignatureHash,
68
+ parameters: routine.parameters,
69
+ provenance: [TREE_SITTER_EVIDENCE],
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Build the event graph: EventSymbols from publisher routines, EventEdges from subscriber
75
+ * routines. A subscriber targeting an event al-sem cannot see (e.g. a Base App event in a
76
+ * symbol-only dependency) still produces an edge — with a synthesized eventId and a
77
+ * non-"resolved" resolution — never a silent gap.
78
+ */
79
+ export function buildEventGraph(index: SemanticIndex, symbols: SymbolTable): EventGraph {
80
+ const events: EventSymbol[] = [];
81
+ const eventById = new Map<string, EventSymbol>();
82
+ const objectById = new Map(index.objects.map((o) => [o.id, o]));
83
+
84
+ // --- publishers ---
85
+ for (const routine of index.routines) {
86
+ if (routine.kind !== "event-publisher") continue;
87
+ const symbol = buildEventSymbol(routine, routine.objectId);
88
+ events.push(symbol);
89
+ eventById.set(symbol.id, symbol);
90
+ }
91
+
92
+ // --- subscribers ---
93
+ const edges: EventEdge[] = [];
94
+ for (const routine of index.routines) {
95
+ if (routine.kind !== "event-subscriber") continue;
96
+ const target = parseSubscriberAttribute(routine.attributesParsed);
97
+ if (!target) continue;
98
+
99
+ const subscriberObject = objectById.get(routine.objectId);
100
+ // routine.objectId should always resolve to an indexed object; the "unknown"
101
+ // sentinel here signals an inconsistent index (a routine without its owning object).
102
+ const subscriberAppId = subscriberObject?.appGuid ?? "unknown";
103
+
104
+ // Resolve the target object by type + name (AL names are case-insensitive).
105
+ const targetObject = symbols.objectByTypeName(target.targetObjectType, target.targetRef);
106
+
107
+ let eventId: string;
108
+ let resolution: EventEdge["resolution"];
109
+
110
+ if (targetObject) {
111
+ eventId = encodeEventId(targetObject.id, target.eventName);
112
+ // "resolved" only if we also found the matching publisher symbol.
113
+ if (eventById.has(eventId)) {
114
+ resolution = "resolved";
115
+ } else {
116
+ resolution = "maybe";
117
+ // Synthesize a symbol for a target object whose publisher was not indexed.
118
+ // (We are already in the else-branch of `eventById.has(eventId)`, so the
119
+ // symbol is guaranteed absent — no redundant guard needed here.)
120
+ const synthesized: EventSymbol = {
121
+ id: eventId,
122
+ publisherObjectId: targetObject.id,
123
+ eventName: target.eventName,
124
+ eventKind: "unknown",
125
+ elementName: target.elementName !== "" ? target.elementName : undefined,
126
+ signatureHash: sha256Hex(eventId),
127
+ parameters: [],
128
+ provenance: [{ source: "tree-sitter", note: "publisher not indexed" }],
129
+ };
130
+ events.push(synthesized);
131
+ eventById.set(eventId, synthesized);
132
+ }
133
+ } else {
134
+ // Target object not in indexed source — synthesize a pseudo object id.
135
+ // NOTE: `${pseudoObjectId}:${targetRef}` is a non-conforming sentinel string,
136
+ // NOT a valid ObjectId — downstream consumers must not attempt to parse it.
137
+ const pseudoObjectId = encodeObjectId("unknown", target.targetObjectType, 0);
138
+ eventId = encodeEventId(`${pseudoObjectId}:${target.targetRef}`, target.eventName);
139
+ resolution = "unknown";
140
+ if (!eventById.has(eventId)) {
141
+ const synthesized: EventSymbol = {
142
+ id: eventId,
143
+ publisherObjectId: `${pseudoObjectId}:${target.targetRef}`,
144
+ eventName: target.eventName,
145
+ eventKind: "unknown",
146
+ elementName: target.elementName !== "" ? target.elementName : undefined,
147
+ signatureHash: sha256Hex(eventId),
148
+ parameters: [],
149
+ provenance: [{ source: "tree-sitter", note: "target object not in indexed source" }],
150
+ };
151
+ events.push(synthesized);
152
+ eventById.set(eventId, synthesized);
153
+ }
154
+ }
155
+
156
+ edges.push({
157
+ eventId,
158
+ subscriberRoutineId: routine.id,
159
+ subscriberAppId,
160
+ resolution,
161
+ provenance: [TREE_SITTER_EVIDENCE],
162
+ });
163
+ }
164
+
165
+ return { events, edges };
166
+ }
@@ -0,0 +1,53 @@
1
+ import type { RecordOpType } from "../model/entities.ts";
2
+ import { type CallEdge, type ResolutionQuality, TREE_SITTER_EVIDENCE } from "../model/graph.ts";
3
+ import type { SemanticIndex } from "../model/model.ts";
4
+ import type { SymbolTable } from "./symbol-table.ts";
5
+
6
+ /**
7
+ * Maps a trigger-invoking record op to (the table trigger name it invokes, the resolution
8
+ * quality of the edge). `Validate` always runs the field's OnValidate, so "resolved".
9
+ * `Insert`/`Modify`/`Delete` run the table trigger only when called with
10
+ * `RunTrigger = true`, which Phase 1 does not capture — so "maybe".
11
+ */
12
+ const TRIGGER_OPS: Partial<
13
+ Record<RecordOpType, { triggerName: string; resolution: ResolutionQuality }>
14
+ > = {
15
+ Validate: { triggerName: "OnValidate", resolution: "resolved" },
16
+ Insert: { triggerName: "OnInsert", resolution: "maybe" },
17
+ Modify: { triggerName: "OnModify", resolution: "maybe" },
18
+ Delete: { triggerName: "OnDelete", resolution: "maybe" },
19
+ };
20
+
21
+ /**
22
+ * Build implicit-trigger CallEdges. For each trigger-invoking record op whose record
23
+ * variable resolves to a table that IS in indexed source, emit an edge to that table's
24
+ * trigger routine. Tables al-sem cannot see (symbol-only dependencies, unknown tables)
25
+ * produce no edge — that absence is reflected in AnalysisCoverage, not invented here.
26
+ */
27
+ export function buildImplicitTriggerEdges(index: SemanticIndex, symbols: SymbolTable): CallEdge[] {
28
+ const edges: CallEdge[] = [];
29
+ for (const routine of index.routines) {
30
+ for (const op of routine.features.recordOperations) {
31
+ const mapping = TRIGGER_OPS[op.op];
32
+ if (!mapping) continue;
33
+ if (!op.tableId) continue; // table not resolved -> cannot find its trigger
34
+ const table = symbols.tableById(op.tableId);
35
+ if (!table) continue;
36
+ // The table's object id: tables are objects too. Look it up by type+number.
37
+ const tableObject = symbols.objectByTypeNumber("Table", table.tableNumber);
38
+ if (!tableObject) continue;
39
+ const trigger = symbols.routineInObject(tableObject.id, mapping.triggerName);
40
+ if (!trigger) continue;
41
+ edges.push({
42
+ from: routine.id,
43
+ to: trigger.id,
44
+ callsiteId: op.id, // the record-op's operation id doubles as the callsite ref
45
+ operationId: op.id,
46
+ dispatchKind: "implicit-trigger",
47
+ resolution: mapping.resolution,
48
+ provenance: [TREE_SITTER_EVIDENCE],
49
+ });
50
+ }
51
+ }
52
+ return edges;
53
+ }
@@ -0,0 +1,36 @@
1
+ import type { SemanticIndex } from "../model/model.ts";
2
+ import type { SymbolTable } from "./symbol-table.ts";
3
+
4
+ /**
5
+ * Back-fill `tableId` on record variables and record operations, and `recordVariableId`
6
+ * on record operations, by resolving table names against the SymbolTable. Mutates the
7
+ * index's routines in place (the established pattern — Phase 1's routine indexer also
8
+ * mutates `callSite.loopStack` in place). A record variable naming a table al-sem cannot
9
+ * see (e.g. a base-app table in a symbol-only dependency) is left with `tableId`
10
+ * undefined — never guessed.
11
+ */
12
+ export function resolveRecordTypes(index: SemanticIndex, symbols: SymbolTable): void {
13
+ for (const routine of index.routines) {
14
+ const { recordVariables, recordOperations } = routine.features;
15
+
16
+ // --- resolve record variables ---
17
+ // name (lowercased) -> the resolved variable, for matching operations below.
18
+ const varByName = new Map<string, (typeof recordVariables)[number]>();
19
+ for (const variable of recordVariables) {
20
+ varByName.set(variable.name.toLowerCase(), variable);
21
+ if (variable.tableName) {
22
+ const table = symbols.tableByName(variable.tableName);
23
+ if (table) variable.tableId = table.id;
24
+ }
25
+ }
26
+
27
+ // --- resolve record operations against their record variable ---
28
+ for (const op of recordOperations) {
29
+ const variable = varByName.get(op.recordVariableName.toLowerCase());
30
+ if (variable) {
31
+ op.recordVariableId = variable.id;
32
+ if (variable.tableId) op.tableId = variable.tableId;
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,23 @@
1
+ import type { Diagnostic } from "../model/finding.ts";
2
+ import type { SemanticIndex, SemanticModel } from "../model/model.ts";
3
+ import type { SourceUnit } from "../providers/types.ts";
4
+ import { buildCoverage } from "./coverage.ts";
5
+ import { resolveSemanticGraph } from "./semantic-graph.ts";
6
+
7
+ /**
8
+ * L3 orchestrator for the WORKSPACE pipeline: extend a SemanticIndex into a SemanticModel by
9
+ * resolving its semantic graphs (`resolveSemanticGraph`) and adding the workspace coverage
10
+ * record. `units` and `indexDiagnostics` feed only `buildCoverage`.
11
+ *
12
+ * Pass `[]` for both `units` and `indexDiagnostics` in unit tests that work from a
13
+ * pre-built index.
14
+ */
15
+ export function resolveModel(
16
+ index: SemanticIndex,
17
+ units: SourceUnit[],
18
+ indexDiagnostics: Diagnostic[],
19
+ ): SemanticModel {
20
+ const { callGraph, eventGraph } = resolveSemanticGraph(index);
21
+ const coverage = buildCoverage(index, callGraph, units, indexDiagnostics);
22
+ return { ...index, callGraph, eventGraph, coverage, rootClassifications: [] };
23
+ }
@@ -0,0 +1,29 @@
1
+ import type { CallEdge, EventGraph } from "../model/graph.ts";
2
+ import type { SemanticIndex } from "../model/model.ts";
3
+ import { resolveCalls } from "./call-resolver.ts";
4
+ import { buildEventGraph } from "./event-graph.ts";
5
+ import { buildImplicitTriggerEdges } from "./implicit-edges.ts";
6
+ import { resolveRecordTypes } from "./record-types.ts";
7
+ import { type SymbolTable, buildSymbolTable } from "./symbol-table.ts";
8
+
9
+ export interface SemanticGraphResult {
10
+ symbols: SymbolTable;
11
+ callGraph: CallEdge[];
12
+ eventGraph: EventGraph;
13
+ }
14
+
15
+ /**
16
+ * The shared graph-resolution core: build the symbol table, resolve record-variable table
17
+ * types in place, then build the call graph (call edges + implicit-trigger edges) and event
18
+ * graph. Used by both the workspace pipeline (`resolveModel`) and the dependency-mode
19
+ * pipeline. Does NOT build coverage — that is workspace-specific and stays in `resolveModel`.
20
+ */
21
+ export function resolveSemanticGraph(index: SemanticIndex): SemanticGraphResult {
22
+ const symbols = buildSymbolTable(index);
23
+ resolveRecordTypes(index, symbols);
24
+ const callEdges = resolveCalls(index, symbols);
25
+ const implicitEdges = buildImplicitTriggerEdges(index, symbols);
26
+ const callGraph = [...callEdges, ...implicitEdges];
27
+ const eventGraph = buildEventGraph(index, symbols);
28
+ return { symbols, callGraph, eventGraph };
29
+ }
@@ -0,0 +1,69 @@
1
+ import type { ObjectDecl, Routine, Table } from "../model/entities.ts";
2
+ import type { ObjectId } from "../model/ids.ts";
3
+ import type { SemanticIndex } from "../model/model.ts";
4
+
5
+ /**
6
+ * A read-only lookup index over a SemanticIndex. Built once, queried by every resolver.
7
+ * All name lookups are case-insensitive (AL identifiers are case-insensitive).
8
+ */
9
+ export interface SymbolTable {
10
+ objectByTypeNumber(objectType: string, objectNumber: number): ObjectDecl | undefined;
11
+ objectByTypeName(objectType: string, name: string): ObjectDecl | undefined;
12
+ tableByName(name: string): Table | undefined;
13
+ tableById(id: string): Table | undefined;
14
+ routineInObject(objectId: ObjectId, routineName: string): Routine | undefined;
15
+ routinesInObject(objectId: ObjectId): Routine[];
16
+ routineById(routineId: string): Routine | undefined;
17
+ }
18
+
19
+ export function buildSymbolTable(index: SemanticIndex): SymbolTable {
20
+ const byTypeNumber = new Map<string, ObjectDecl>();
21
+ const byTypeName = new Map<string, ObjectDecl>();
22
+ for (const o of index.objects) {
23
+ byTypeNumber.set(`${o.objectType.toLowerCase()}/${o.objectNumber}`, o);
24
+ byTypeName.set(`${o.objectType.toLowerCase()}/${o.name.toLowerCase()}`, o);
25
+ }
26
+
27
+ const tablesByName = new Map<string, Table>();
28
+ const tablesById = new Map<string, Table>();
29
+ for (const t of index.tables) {
30
+ tablesByName.set(t.name.toLowerCase(), t);
31
+ tablesById.set(t.id, t);
32
+ }
33
+
34
+ // Routines keyed by `${objectId}::${routineName.toLowerCase()}`, and grouped per object.
35
+ const routineByKey = new Map<string, Routine>();
36
+ const routinesByObject = new Map<string, Routine[]>();
37
+ const routinesById = new Map<string, Routine>();
38
+ for (const r of index.routines) {
39
+ routineByKey.set(`${r.objectId}::${r.name.toLowerCase()}`, r);
40
+ routinesById.set(r.id, r);
41
+ const list = routinesByObject.get(r.objectId);
42
+ if (list) list.push(r);
43
+ else routinesByObject.set(r.objectId, [r]);
44
+ }
45
+
46
+ return {
47
+ objectByTypeNumber(objectType, objectNumber) {
48
+ return byTypeNumber.get(`${objectType.toLowerCase()}/${objectNumber}`);
49
+ },
50
+ objectByTypeName(objectType, name) {
51
+ return byTypeName.get(`${objectType.toLowerCase()}/${name.toLowerCase()}`);
52
+ },
53
+ tableByName(name) {
54
+ return tablesByName.get(name.toLowerCase());
55
+ },
56
+ tableById(id) {
57
+ return tablesById.get(id);
58
+ },
59
+ routineInObject(objectId, routineName) {
60
+ return routineByKey.get(`${objectId}::${routineName.toLowerCase()}`);
61
+ },
62
+ routinesInObject(objectId) {
63
+ return routinesByObject.get(objectId) ?? [];
64
+ },
65
+ routineById(routineId) {
66
+ return routinesById.get(routineId);
67
+ },
68
+ };
69
+ }