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,54 @@
1
+ import type { Uncertainty } from "../model/summary.ts";
2
+
3
+ /** Stable string key for an Uncertainty — kind plus whichever id field it carries. */
4
+ export function uncertaintyKey(u: Uncertainty): string {
5
+ if ("callsiteId" in u) return `${u.kind}|${u.callsiteId}`;
6
+ if ("operationId" in u) return `${u.kind}|${u.operationId}`;
7
+ return `${u.kind}|${u.routineId}`;
8
+ }
9
+
10
+ /**
11
+ * De-duplicate a list of Uncertainty values by key (keep first seen), then sort by key.
12
+ * The sort uses `compareStrings` for locale-independent, byte-stable ordering.
13
+ */
14
+ export function dedupeUncertainties(list: Uncertainty[]): Uncertainty[] {
15
+ const byKey = new Map<string, Uncertainty>();
16
+ for (const u of list) byKey.set(uncertaintyKey(u), u);
17
+ return [...byKey.values()].sort((a, b) => compareStrings(uncertaintyKey(a), uncertaintyKey(b)));
18
+ }
19
+
20
+ /**
21
+ * Locale-independent string comparator. Returns -1 / 0 / 1 based on JS byte order,
22
+ * ensuring deterministic sort output across all environments regardless of locale settings.
23
+ */
24
+ export function compareStrings(a: string, b: string): number {
25
+ return a < b ? -1 : a > b ? 1 : 0;
26
+ }
27
+
28
+ /**
29
+ * Compare two strings naturally: split each into runs of letters and digits, compare
30
+ * digit runs numerically and letter runs lexicographically. Stable for equal prefixes.
31
+ * Used for detector ids ("d2" < "d10") and other dN-style keys.
32
+ */
33
+ export function compareNatural(a: string, b: string): number {
34
+ const re = /(\d+)|(\D+)/g;
35
+ const pa = a.match(re) ?? [];
36
+ const pb = b.match(re) ?? [];
37
+ const len = Math.min(pa.length, pb.length);
38
+ for (let i = 0; i < len; i++) {
39
+ const ta = pa[i] as string;
40
+ const tb = pb[i] as string;
41
+ const aIsNum = /^\d/.test(ta);
42
+ const bIsNum = /^\d/.test(tb);
43
+ if (aIsNum && bIsNum) {
44
+ const na = Number.parseInt(ta, 10);
45
+ const nb = Number.parseInt(tb, 10);
46
+ if (na !== nb) return na < nb ? -1 : 1;
47
+ } else if (aIsNum !== bIsNum) {
48
+ return aIsNum ? -1 : 1;
49
+ } else {
50
+ if (ta !== tb) return ta < tb ? -1 : 1;
51
+ }
52
+ }
53
+ return pa.length - pb.length;
54
+ }
package/src/hash.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ /** SHA-256 hex digest of a string. */
4
+ export function sha256Hex(input: string): string {
5
+ return createHash("sha256").update(input, "utf8").digest("hex");
6
+ }
7
+
8
+ /**
9
+ * SHA-256 hex digest of a raw byte slice. Equivalent to `sha256Hex(utf8-decode(bytes))`
10
+ * when `bytes` is valid UTF-8 (Node's createHash treats strings as UTF-8). Skips the
11
+ * intermediate JS string allocation for callers that already hold a byte buffer (e.g.
12
+ * tree-sitter Node text via `tree.sourceBytes.subarray(start, end)`).
13
+ */
14
+ export function sha256HexBytes(bytes: Uint8Array): string {
15
+ return createHash("sha256").update(bytes).digest("hex");
16
+ }
17
+
18
+ /**
19
+ * SHA-256 hex digest of an ordered list of strings.
20
+ * Uses a length-prefixed encoding so concatenation is unambiguous:
21
+ * ["ab","c"] and ["a","bc"] produce different digests.
22
+ */
23
+ export function sha256OfStrings(parts: string[]): string {
24
+ const h = createHash("sha256");
25
+ for (const part of parts) {
26
+ h.update(String(part.length));
27
+ h.update(":");
28
+ h.update(part, "utf8");
29
+ }
30
+ return h.digest("hex");
31
+ }
@@ -0,0 +1,141 @@
1
+ // src/index/attribute-from-node.ts
2
+ // Build a structured `AttributeInfo` from a tree-sitter `attribute_item` node.
3
+ //
4
+ // Grammar shape (tree-sitter-al):
5
+ // attribute_item
6
+ // '['
7
+ // attribute_content
8
+ // name: identifier
9
+ // arguments?: attribute_arguments
10
+ // '('
11
+ // attribute_argument_list?
12
+ // <_attribute_argument>, ...
13
+ // ')'
14
+ // ']'
15
+ //
16
+ // `_attribute_argument` is a closed choice — boolean / integer / string_literal /
17
+ // identifier / quoted_identifier / qualified_enum_value / database_reference /
18
+ // member_expression — all surfaced as typed nodes. We map each into a typed
19
+ // `AttributeArg`; consumers query through `findAttribute` / `stringArg` /
20
+ // `qualifiedArg` helpers instead of switching on positional indices.
21
+
22
+ import type { AttributeArg, AttributeArgKind, AttributeInfo } from "../model/attributes.ts";
23
+ import type { Node as SyntaxNode } from "../parser/native/wrapper.ts";
24
+
25
+ /** Strip surrounding single or double quotes from a string_literal / quoted_identifier. */
26
+ function stripQuoteChars(text: string): string {
27
+ if (text.length < 2) return text;
28
+ const first = text[0];
29
+ const last = text[text.length - 1];
30
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
31
+ return text.slice(1, -1);
32
+ }
33
+ return text;
34
+ }
35
+
36
+ /** Classify a grammar node type into the AttributeArgKind enum. */
37
+ function kindOf(nodeType: string): AttributeArgKind {
38
+ switch (nodeType) {
39
+ case "boolean":
40
+ case "integer":
41
+ case "string_literal":
42
+ case "identifier":
43
+ case "quoted_identifier":
44
+ case "qualified_enum_value":
45
+ case "database_reference":
46
+ case "member_expression":
47
+ return nodeType;
48
+ default:
49
+ return "unknown";
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Pull the unquoted/derived `value` and optional `qualifier`/`member` out of an arg node.
55
+ * Tree-sitter exposes the structurally-relevant pieces as field children
56
+ * (`enum_type`/`value` for `qualified_enum_value`, `keyword`/`table_name` for
57
+ * `database_reference`), so we read those directly — no further text shredding.
58
+ */
59
+ function deriveValueParts(
60
+ node: SyntaxNode,
61
+ kind: AttributeArgKind,
62
+ text: string,
63
+ ): { value?: string; qualifier?: string; member?: string } {
64
+ switch (kind) {
65
+ case "boolean":
66
+ case "integer":
67
+ case "identifier":
68
+ return { value: text };
69
+ case "string_literal":
70
+ case "quoted_identifier":
71
+ return { value: stripQuoteChars(text) };
72
+ case "qualified_enum_value": {
73
+ const qualifier = node.childForFieldName("enum_type")?.text;
74
+ const memberRaw = node.childForFieldName("value")?.text;
75
+ const member = memberRaw !== undefined ? stripQuoteChars(memberRaw) : undefined;
76
+ return {
77
+ value: member,
78
+ ...(qualifier !== undefined ? { qualifier } : {}),
79
+ ...(member !== undefined ? { member } : {}),
80
+ };
81
+ }
82
+ case "database_reference": {
83
+ const qualifier = node.childForFieldName("keyword")?.text;
84
+ const memberRaw = node.childForFieldName("table_name")?.text;
85
+ const member = memberRaw !== undefined ? stripQuoteChars(memberRaw) : undefined;
86
+ return {
87
+ value: member,
88
+ ...(qualifier !== undefined ? { qualifier } : {}),
89
+ ...(member !== undefined ? { member } : {}),
90
+ };
91
+ }
92
+ case "member_expression":
93
+ case "unknown":
94
+ return {};
95
+ }
96
+ }
97
+
98
+ /** Build an `AttributeArg` from a single argument node inside `attribute_argument_list`. */
99
+ function argFromNode(node: SyntaxNode): AttributeArg {
100
+ const kind = kindOf(node.type);
101
+ const text = node.text;
102
+ const parts = deriveValueParts(node, kind, text);
103
+ return {
104
+ kind,
105
+ text,
106
+ ...(parts.value !== undefined ? { value: parts.value } : {}),
107
+ ...(parts.qualifier !== undefined ? { qualifier: parts.qualifier } : {}),
108
+ ...(parts.member !== undefined ? { member: parts.member } : {}),
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Build an `AttributeInfo` from an `attribute_item` (or `var_attribute_item`) node.
114
+ *
115
+ * Returns null only when the grammar shape is unrecognizable — a parse error
116
+ * inside the attribute. In that case callers should fall back to the raw text
117
+ * (kept on `Routine.attributes`) for diagnostics; semantic consumers will see
118
+ * the routine as if the attribute were absent, matching prior regex-miss behavior.
119
+ */
120
+ export function attributeInfoFromNode(item: SyntaxNode): AttributeInfo | null {
121
+ const content = item.childForFieldName("attribute");
122
+ if (content === null) return null;
123
+ const nameNode = content.childForFieldName("name");
124
+ const name = nameNode?.text ?? "";
125
+ if (name === "") return null;
126
+
127
+ const args: AttributeArg[] = [];
128
+ const argsNode = content.childForFieldName("arguments");
129
+ if (argsNode !== null) {
130
+ // `attribute_arguments` wraps `(` `attribute_argument_list?` `)`. The list
131
+ // node is the single named child; its named children are the args.
132
+ const list = argsNode.namedChildren.find((c) => c?.type === "attribute_argument_list");
133
+ if (list) {
134
+ for (const child of list.namedChildren) {
135
+ if (child !== null) args.push(argFromNode(child));
136
+ }
137
+ }
138
+ }
139
+
140
+ return { name, args, raw: item.text };
141
+ }
@@ -0,0 +1,181 @@
1
+ // src/index/callee-from-node.ts
2
+ // Build a structured `Callee` from a tree-sitter `call_expression` node.
3
+ //
4
+ // Grammar shape (tree-sitter-al ≥ v2.5.2):
5
+ // call_expression
6
+ // function: identifier → bare procedure call
7
+ // | member_expression → method call OR object-run dispatch
8
+ // object: identifier / keyword_identifier / member_expression / ...
9
+ // member: identifier (`Run` for object-run; method name otherwise)
10
+ // argument_list
11
+ // <args>...
12
+ //
13
+ // Object-run dispatch (`Codeunit.Run(...)` / `Page.Run(...)` / `Report.Run(...)`)
14
+ // is unambiguous in the grammar because the object position is a
15
+ // `keyword_identifier` (wrapping `codeunit_keyword` / `page_keyword` /
16
+ // `report_keyword`) — distinct from a plain `identifier` like `Sales` in
17
+ // `Sales.Run()`. The first argument's shape — `database_reference` with a
18
+ // `table_name` of kind `quoted_identifier` (quoted name), `identifier`
19
+ // (unquoted name), or `integer` (numeric id) — pins down the target.
20
+ //
21
+ // No regex anywhere — every distinction comes from `node.type` / field children.
22
+
23
+ import type { Callee, ObjectRunKind } from "../model/callee.ts";
24
+ import type { Node as SyntaxNode } from "../parser/native/wrapper.ts";
25
+
26
+ /** Strip a single layer of surrounding double/single quotes. */
27
+ function stripQuoteChars(text: string): string {
28
+ if (text.length < 2) return text;
29
+ const first = text[0];
30
+ const last = text[text.length - 1];
31
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
32
+ return text.slice(1, -1);
33
+ }
34
+ return text;
35
+ }
36
+
37
+ /**
38
+ * True when `objNode` is a `keyword_identifier` whose inner keyword node is
39
+ * one of `codeunit_keyword` / `page_keyword` / `report_keyword`. Returns the
40
+ * properly-cased object-kind label, or null if not an object-run prefix.
41
+ *
42
+ * Grammar fact: object-run prefixes use `keyword_identifier` (an alias around
43
+ * the bare keyword tokens), while a variable named `Sales` uses plain
44
+ * `identifier`. The two are syntactically distinct, so this check cannot
45
+ * misclassify a regular variable as a `.Run` dispatch.
46
+ */
47
+ function objectRunKindOfReceiver(objNode: SyntaxNode): ObjectRunKind | null {
48
+ if (objNode.type !== "keyword_identifier") return null;
49
+ for (const child of objNode.namedChildren) {
50
+ if (child === null) continue;
51
+ switch (child.type) {
52
+ case "codeunit_keyword":
53
+ return "Codeunit";
54
+ case "page_keyword":
55
+ return "Page";
56
+ case "report_keyword":
57
+ return "Report";
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Classify the first argument of a `.Run(...)` call into the object-run target
65
+ * fields. The grammar surfaces three node kinds inside `database_reference`'s
66
+ * `table_name` field:
67
+ * - `quoted_identifier` → quoted name (e.g. `Codeunit::"Sales-Post"`)
68
+ * - `identifier` → unquoted name (e.g. `Codeunit::SalesPost`)
69
+ * - `integer` → numeric id (e.g. `Codeunit::80`)
70
+ *
71
+ * Anything else — a variable, a function call, an arithmetic expression —
72
+ * leaves the target dynamic (`targetRef: undefined`).
73
+ */
74
+ function classifyObjectRunFirstArg(
75
+ firstArg: SyntaxNode | undefined,
76
+ objectKind: ObjectRunKind,
77
+ ): { targetType: ObjectRunKind; targetRef?: string; targetIsName: boolean } {
78
+ if (firstArg === undefined || firstArg.type !== "database_reference") {
79
+ return { targetType: objectKind, targetRef: undefined, targetIsName: false };
80
+ }
81
+ const tableNameNode = firstArg.childForFieldName("table_name");
82
+ if (tableNameNode === null) {
83
+ return { targetType: objectKind, targetRef: undefined, targetIsName: false };
84
+ }
85
+ const text = tableNameNode.text;
86
+ switch (tableNameNode.type) {
87
+ case "integer":
88
+ return { targetType: objectKind, targetRef: text, targetIsName: false };
89
+ case "quoted_identifier":
90
+ return { targetType: objectKind, targetRef: stripQuoteChars(text), targetIsName: true };
91
+ default:
92
+ // identifier / quoted-identifier-like contextual-keyword forms — unquoted name.
93
+ return { targetType: objectKind, targetRef: text, targetIsName: true };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Find the first positional argument of a `call_expression`. The
99
+ * `argument_list` is a named child whose own named children are the args.
100
+ * Returns undefined for an empty argument list (`Foo()`).
101
+ */
102
+ function firstArgumentNode(callNode: SyntaxNode): SyntaxNode | undefined {
103
+ const argList = callNode.namedChildren.find((c) => c !== null && c.type === "argument_list");
104
+ if (argList === undefined || argList === null) return undefined;
105
+ for (const child of argList.namedChildren) {
106
+ if (child !== null) return child;
107
+ }
108
+ return undefined;
109
+ }
110
+
111
+ /**
112
+ * Build the `member`-shaped Callee for a `member_expression` (object + member),
113
+ * with the object-run dispatch upgrade when the receiver is a keyword. `callNode`
114
+ * is the enclosing `call_expression` when one exists — used to inspect the first
115
+ * argument for object-run targeting. When invoked on a parameterless method-call
116
+ * statement (`Customer.SetRecFilter;`, no surrounding `call_expression`), pass
117
+ * the `member_expression` itself as `callNode` and the target stays dynamic.
118
+ */
119
+ function calleeFromMemberExpression(memberExpr: SyntaxNode, callNode: SyntaxNode | null): Callee {
120
+ const objNode = memberExpr.childForFieldName("object") ?? memberExpr.namedChildren[0] ?? null;
121
+ const memberNode = memberExpr.childForFieldName("member") ?? memberExpr.namedChildren[1] ?? null;
122
+ if (objNode === null || memberNode === null) return { kind: "unknown" };
123
+
124
+ const memberLc = memberNode.text.toLowerCase();
125
+ const objectKind = objectRunKindOfReceiver(objNode);
126
+ if (objectKind !== null && memberLc === "run") {
127
+ const firstArg = callNode !== null ? firstArgumentNode(callNode) : undefined;
128
+ const parts = classifyObjectRunFirstArg(firstArg, objectKind);
129
+ return {
130
+ kind: "object-run",
131
+ objectKind,
132
+ targetType: parts.targetType,
133
+ ...(parts.targetRef !== undefined ? { targetRef: parts.targetRef } : {}),
134
+ targetIsName: parts.targetIsName,
135
+ };
136
+ }
137
+
138
+ return {
139
+ kind: "member",
140
+ receiver: objNode.text,
141
+ method: stripQuoteChars(memberNode.text),
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Classify a call expression or bare-method-call shape into a structured `Callee`.
147
+ *
148
+ * Accepts either:
149
+ * - a `call_expression` node (the common case — `Foo(a, b)`, `Sales.Post(x)`,
150
+ * `Codeunit.Run(Codeunit::80)`); or
151
+ * - a `member_expression` node in statement position (`Customer.SetRecFilter;`
152
+ * — parameterless method call written without parens).
153
+ *
154
+ * Returns `{ kind: "unknown" }` for malformed shapes — mirrors the prior regex
155
+ * behavior of falling through to unknown rather than throwing.
156
+ */
157
+ export function calleeFromNode(node: SyntaxNode): Callee {
158
+ if (node.type === "member_expression") {
159
+ return calleeFromMemberExpression(node, null);
160
+ }
161
+
162
+ if (node.type !== "call_expression") return { kind: "unknown" };
163
+
164
+ const funcNode = node.childForFieldName("function") ?? node.namedChildren[0] ?? null;
165
+ if (funcNode === null) return { kind: "unknown" };
166
+
167
+ if (funcNode.type === "identifier") {
168
+ return { kind: "bare", name: stripQuoteChars(funcNode.text) };
169
+ }
170
+
171
+ // `quoted_identifier` callee (rare — `"My Routine"`(...)) — still a bare call.
172
+ if (funcNode.type === "quoted_identifier") {
173
+ return { kind: "bare", name: stripQuoteChars(funcNode.text) };
174
+ }
175
+
176
+ if (funcNode.type === "member_expression") {
177
+ return calleeFromMemberExpression(funcNode, node);
178
+ }
179
+
180
+ return { kind: "unknown" };
181
+ }
@@ -0,0 +1,90 @@
1
+ import type { CapabilityConfidence, CapabilityFact, ValueSource } from "../../model/capability.ts";
2
+ import type { CoverageReason } from "../../model/coverage.ts";
3
+ import type { ExtractionContext } from "./extractor.ts";
4
+ import { classifyValueSource } from "./value-source.ts";
5
+
6
+ /**
7
+ * Phase 0b-β background family extractor. Detects background-job kickoff calls:
8
+ * - `TaskScheduler.CreateTask(CodeunitId, ...)` — member callee, receiver = "TaskScheduler"
9
+ * - `Session.StartSession(out NewSessionID, CodeunitId, ...)` — member callee, receiver = "Session"
10
+ * - `StartSession(out NewSessionID, CodeunitId, ...)` — bare callee
11
+ *
12
+ * Emits one CapabilityFact per match:
13
+ * op: "start", resourceKind: "background"
14
+ * resourceArgSource: classifyValueSource on the codeunit-id argument
15
+ * - arg[0] for TaskScheduler.CreateTask
16
+ * - arg[1] for Session.StartSession / bare StartSession (arg[0] is the OUT session-id var)
17
+ * confidence derived from codeunit-id ValueSource kind
18
+ * provenance: "direct", via: "self", witnessCallsiteId: cs.id
19
+ *
20
+ * No BackgroundExtra is defined — `extra` is omitted.
21
+ *
22
+ * Never throws.
23
+ */
24
+ export function extractBackground(ctx: ExtractionContext): {
25
+ facts: CapabilityFact[];
26
+ reasons: CoverageReason[];
27
+ } {
28
+ try {
29
+ const facts: CapabilityFact[] = [];
30
+ for (const cs of ctx.routine?.features?.callSites ?? []) {
31
+ const callee = cs.callee;
32
+ if (!callee) continue;
33
+
34
+ let codeunitArgIdx: number | undefined;
35
+
36
+ if (callee.kind === "member") {
37
+ const receiverLc = callee.receiver.toLowerCase();
38
+ const methodLc = callee.method.toLowerCase();
39
+ if (receiverLc === "taskscheduler" && methodLc === "createtask") {
40
+ codeunitArgIdx = 0;
41
+ } else if (receiverLc === "session" && methodLc === "startsession") {
42
+ codeunitArgIdx = 1;
43
+ }
44
+ } else if (callee.kind === "bare") {
45
+ if (typeof callee.name === "string" && callee.name.toLowerCase() === "startsession") {
46
+ codeunitArgIdx = 1;
47
+ }
48
+ }
49
+
50
+ if (codeunitArgIdx === undefined) continue;
51
+
52
+ const codeunitArgInfo = cs.argumentInfos?.[codeunitArgIdx];
53
+ const codeunitArgSource: ValueSource =
54
+ codeunitArgInfo !== undefined
55
+ ? classifyValueSource(codeunitArgInfo, ctx)
56
+ : { kind: "unknown" };
57
+
58
+ facts.push({
59
+ subject: ctx.routine.id,
60
+ op: "start",
61
+ resourceKind: "background",
62
+ resourceArgSource: codeunitArgSource,
63
+ confidence: confidenceFromSource(codeunitArgSource),
64
+ provenance: "direct",
65
+ via: "self",
66
+ witnessCallsiteId: cs.id,
67
+ });
68
+ }
69
+ return { facts, reasons: [] };
70
+ } catch {
71
+ return { facts: [], reasons: ["extraction-failed"] };
72
+ }
73
+ }
74
+
75
+ function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
76
+ switch (vs.kind) {
77
+ case "literal":
78
+ case "enum":
79
+ return "static";
80
+ case "constant-var":
81
+ return confidenceFromSource(vs.initializer);
82
+ case "parameter":
83
+ return "userDynamic";
84
+ case "table-field":
85
+ return "configDynamic";
86
+ case "expression":
87
+ case "unknown":
88
+ return "unresolved";
89
+ }
90
+ }
@@ -0,0 +1,44 @@
1
+ import type { CapabilityFact } from "../../model/capability.ts";
2
+ import type { CoverageReason } from "../../model/coverage.ts";
3
+ import type { ExtractionContext } from "./extractor.ts";
4
+
5
+ /**
6
+ * Phase 0b-β commit family extractor. Emits one CapabilityFact per Commit
7
+ * statement / call in the routine body.
8
+ *
9
+ * Detection: iterates ctx.routine.features.operationSites looking for
10
+ * kind === "commit". AL's `Commit;` and `Commit()` both produce an
11
+ * OperationSite with kind "commit" at L2 index time (confirmed by probe).
12
+ * They do NOT appear in callSites.
13
+ *
14
+ * The witness is witnessOperationId (the OperationSite.id), not
15
+ * witnessCallsiteId, because commit is classified as an operation rather
16
+ * than a general call.
17
+ *
18
+ * Never throws.
19
+ */
20
+ export function extractCommit(ctx: ExtractionContext): {
21
+ facts: CapabilityFact[];
22
+ reasons: CoverageReason[];
23
+ } {
24
+ try {
25
+ const facts: CapabilityFact[] = [];
26
+ const operationSites = ctx.routine?.features?.operationSites ?? [];
27
+ for (const op of operationSites) {
28
+ if (op.kind === "commit") {
29
+ facts.push({
30
+ subject: ctx.routine.id,
31
+ op: "commit",
32
+ resourceKind: "transaction",
33
+ confidence: "static",
34
+ provenance: "direct",
35
+ via: "self",
36
+ witnessOperationId: op.id,
37
+ });
38
+ }
39
+ }
40
+ return { facts, reasons: [] };
41
+ } catch {
42
+ return { facts: [], reasons: ["extraction-failed"] };
43
+ }
44
+ }