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,94 @@
1
+ // src/parser/native/parse-index-worker.ts
2
+ // Worker entry: parse + index a single AL source unit. The native parser and the JS
3
+ // indexers are CPU-bound; spreading them across a worker pool yields near-linear
4
+ // scaling on multi-core hosts when the workload is many independent files (e.g.
5
+ // Microsoft Base Application's 7,634 .al sources).
6
+ //
7
+ // Each worker holds its own native parser instance (each `Parser` wrapper instance
8
+ // owns an independent TSParser handle via `al_shim_parser_new`). Trees never cross
9
+ // the worker boundary — they're created, walked, and freed inside the worker. Only
10
+ // plain-JS results (objects, tables, routines, diagnostics) are postMessaged back.
11
+
12
+ import { sha256Hex } from "../../hash.ts";
13
+ import { indexObjects } from "../../index/object-indexer.ts";
14
+ import { indexRoutines } from "../../index/routine-indexer.ts";
15
+ import type { ObjectDecl, Table } from "../../model/entities.ts";
16
+ import { NativeParserUnavailableError, parseALSource } from "../parser-init.ts";
17
+
18
+ declare const self: Worker;
19
+
20
+ interface WorkerRequest {
21
+ jobId: number;
22
+ relativePath: string;
23
+ content: string;
24
+ appGuid: string;
25
+ sourceUnitId: string;
26
+ modelInstanceId: string;
27
+ }
28
+
29
+ interface WorkerResultOk {
30
+ jobId: number;
31
+ relativePath: string;
32
+ ok: true;
33
+ objects: ObjectDecl[];
34
+ tables: Table[];
35
+ routines: ReturnType<typeof indexRoutines>;
36
+ }
37
+
38
+ interface WorkerResultError {
39
+ jobId: number;
40
+ relativePath: string;
41
+ ok: false;
42
+ errorKind: "parser-unavailable" | "parse-error";
43
+ errorMessage: string;
44
+ }
45
+
46
+ export type WorkerResult = WorkerResultOk | WorkerResultError;
47
+
48
+ self.onmessage = async (event: MessageEvent<WorkerRequest>) => {
49
+ const { jobId, relativePath, content, appGuid, sourceUnitId, modelInstanceId } = event.data;
50
+ try {
51
+ const tree = await parseALSource(content);
52
+ try {
53
+ const sourceHash = sha256Hex(content);
54
+ const objResults = indexObjects({ tree, appGuid, sourceUnitId, modelInstanceId, sourceHash });
55
+ const objects: ObjectDecl[] = [];
56
+ const tables: Table[] = [];
57
+ let routines: ReturnType<typeof indexRoutines> = [];
58
+ for (const objResult of objResults) {
59
+ if (!objResult.object || !objResult.objectNode) continue;
60
+ objects.push(objResult.object);
61
+ if (objResult.table) tables.push(objResult.table);
62
+ routines = routines.concat(
63
+ indexRoutines({
64
+ objectNode: objResult.objectNode,
65
+ object: objResult.object,
66
+ sourceUnitId,
67
+ modelInstanceId,
68
+ }),
69
+ );
70
+ }
71
+ const result: WorkerResultOk = {
72
+ jobId,
73
+ relativePath,
74
+ ok: true,
75
+ objects,
76
+ tables,
77
+ routines,
78
+ };
79
+ self.postMessage(result);
80
+ } finally {
81
+ tree.delete();
82
+ }
83
+ } catch (err) {
84
+ const isParserDown = err instanceof NativeParserUnavailableError;
85
+ const result: WorkerResultError = {
86
+ jobId,
87
+ relativePath,
88
+ ok: false,
89
+ errorKind: isParserDown ? "parser-unavailable" : "parse-error",
90
+ errorMessage: (err as Error).message,
91
+ };
92
+ self.postMessage(result);
93
+ }
94
+ };
@@ -0,0 +1,353 @@
1
+ // src/parser/native/wrapper.ts
2
+ // JS façade around the al_shim FFI surface. API mirrors the subset of
3
+ // web-tree-sitter's Node/Tree/TreeCursor that al-sem's indexers use today.
4
+ // Memory model:
5
+ // - Parser handle: process-lifetime (cached in parser-init.ts).
6
+ // - Tree handle: owned by the Tree wrapper; freed by Tree.delete().
7
+ // Tree retains the source UTF-8 buffer for byte-accurate Node.text.
8
+ // - Node: lightweight (Uint8Array of NODE_SIZE bytes + back-ref to Tree).
9
+ // The wrapper interns numeric `id`s per-tree (collision-free).
10
+ // - TreeCursor: heap-allocated by shim; freed by TreeCursor.delete().
11
+
12
+ import { type Pointer, ptr } from "bun:ffi";
13
+ import { NativeParserUnavailableError, loadShim } from "./ffi.ts";
14
+
15
+ export { NativeParserUnavailableError };
16
+
17
+ const decoder = new TextDecoder("utf-8");
18
+ const encoder = new TextEncoder();
19
+
20
+ /**
21
+ * Per-process cache of UTF-8 encodings for tree-sitter field names. The indexer hits
22
+ * `childForFieldName` with the same handful of field names ("name", "function", "object",
23
+ * "member", etc.) millions of times across a workspace; without this cache each call
24
+ * pays an `encoder.encode()` UTF-8 round-trip + a Uint8Array allocation.
25
+ */
26
+ const FIELD_NAME_BYTES = new Map<string, Uint8Array>();
27
+ function fieldNameBytes(name: string): Uint8Array {
28
+ let bytes = FIELD_NAME_BYTES.get(name);
29
+ if (bytes === undefined) {
30
+ bytes = encoder.encode(name);
31
+ FIELD_NAME_BYTES.set(name, bytes);
32
+ }
33
+ return bytes;
34
+ }
35
+
36
+ // Module-level shim handle + NODE_SIZE constant. Lazy-init on first wrapper use.
37
+ let SHIM: ReturnType<typeof loadShim> | null = null;
38
+ let NODE_SIZE = 0;
39
+ function shim(): ReturnType<typeof loadShim> {
40
+ if (!SHIM) {
41
+ SHIM = loadShim();
42
+ NODE_SIZE = SHIM.symbols.al_shim_node_size();
43
+ if (NODE_SIZE === 0 || NODE_SIZE > 256) {
44
+ throw new NativeParserUnavailableError(
45
+ "abi-mismatch",
46
+ `al-sem: native parser reported invalid TSNode size ${NODE_SIZE}. Library may be wrong version.`,
47
+ );
48
+ }
49
+ }
50
+ return SHIM;
51
+ }
52
+
53
+ export class Parser {
54
+ private handle: Pointer;
55
+ private disposed = false;
56
+ constructor() {
57
+ const s = shim();
58
+ const h = s.symbols.al_shim_parser_new();
59
+ if (!h) throw new Error("al-sem: al_shim_parser_new returned null");
60
+ this.handle = h;
61
+ const lang = s.symbols.al_shim_language();
62
+ if (!lang) throw new Error("al-sem: al_shim_language returned null");
63
+ const ok = s.symbols.al_shim_parser_set_language(h, lang);
64
+ if (ok === 0) throw new Error("al-sem: al_shim_parser_set_language failed");
65
+ }
66
+
67
+ parse(source: string): Tree {
68
+ if (this.disposed) throw new Error("Parser is disposed");
69
+ const s = shim();
70
+ const bytes = encoder.encode(source);
71
+ const treePtr = s.symbols.al_shim_parse_utf8(this.handle, ptr(bytes), bytes.byteLength);
72
+ if (!treePtr) throw new Error("al-sem: parse returned null (tree-sitter halt)");
73
+ return new Tree(treePtr, bytes);
74
+ }
75
+
76
+ delete(): void {
77
+ if (this.disposed) return;
78
+ shim().symbols.al_shim_parser_delete(this.handle);
79
+ this.disposed = true;
80
+ }
81
+ }
82
+
83
+ export class Tree {
84
+ /** @internal */ readonly handle: Pointer;
85
+ /** @internal */ readonly sourceBytes: Uint8Array;
86
+ /** @internal */ readonly nodeIds = new Map<string, number>();
87
+ /** @internal */ nextId = 1;
88
+ /** @internal */ disposed = false;
89
+ private _root: Node | undefined;
90
+
91
+ constructor(handle: Pointer, sourceBytes: Uint8Array) {
92
+ this.handle = handle;
93
+ this.sourceBytes = sourceBytes;
94
+ }
95
+
96
+ get rootNode(): Node {
97
+ if (this.disposed) throw new Error("Tree is disposed");
98
+ if (!this._root) {
99
+ const buf = new Uint8Array(NODE_SIZE);
100
+ shim().symbols.al_shim_tree_root_node(this.handle, ptr(buf));
101
+ this._root = new Node(this, buf);
102
+ }
103
+ return this._root;
104
+ }
105
+
106
+ walk(): TreeCursor {
107
+ return new TreeCursor(this, this.rootNode);
108
+ }
109
+
110
+ delete(): void {
111
+ if (this.disposed) return;
112
+ shim().symbols.al_shim_tree_delete(this.handle);
113
+ this.disposed = true;
114
+ }
115
+ }
116
+
117
+ export class Node {
118
+ /** @internal */ readonly tree: Tree;
119
+ /** @internal */ readonly buf: Uint8Array;
120
+ private _children: Node[] | undefined;
121
+ private _namedChildren: Node[] | undefined;
122
+ private _id: number | undefined;
123
+ // FFI-getter caches. The underlying TSNode buffer is immutable for this wrapper's
124
+ // lifetime, so every getter result is safe to memoize. The hot path (indexRoutines)
125
+ // re-reads .type millions of times per cold run; caching here removed >1M FFI calls
126
+ // on Continia Delivery Network alone.
127
+ private _type: string | undefined;
128
+ private _startIndex: number | undefined;
129
+ private _endIndex: number | undefined;
130
+ private _startPosition: { row: number; column: number } | undefined;
131
+ private _endPosition: { row: number; column: number } | undefined;
132
+ private _childCount: number | undefined;
133
+ private _namedChildCount: number | undefined;
134
+ private _hasError: boolean | undefined;
135
+ private _isNamed: boolean | undefined;
136
+
137
+ constructor(tree: Tree, buf: Uint8Array) {
138
+ this.tree = tree;
139
+ this.buf = buf;
140
+ }
141
+
142
+ private check(): ReturnType<typeof loadShim> {
143
+ if (this.tree.disposed) throw new Error("Node references a disposed Tree");
144
+ return shim();
145
+ }
146
+
147
+ get type(): string {
148
+ if (this._type !== undefined) return this._type;
149
+ this._type = this.check().symbols.al_shim_node_type(ptr(this.buf))?.toString() ?? "";
150
+ return this._type;
151
+ }
152
+ get startIndex(): number {
153
+ if (this._startIndex !== undefined) return this._startIndex;
154
+ this._startIndex = this.check().symbols.al_shim_node_start_byte(ptr(this.buf));
155
+ return this._startIndex;
156
+ }
157
+ get endIndex(): number {
158
+ if (this._endIndex !== undefined) return this._endIndex;
159
+ this._endIndex = this.check().symbols.al_shim_node_end_byte(ptr(this.buf));
160
+ return this._endIndex;
161
+ }
162
+ get startPosition(): { row: number; column: number } {
163
+ if (this._startPosition !== undefined) return this._startPosition;
164
+ const s = this.check();
165
+ this._startPosition = {
166
+ row: s.symbols.al_shim_node_start_row(ptr(this.buf)),
167
+ column: s.symbols.al_shim_node_start_column(ptr(this.buf)),
168
+ };
169
+ return this._startPosition;
170
+ }
171
+ get endPosition(): { row: number; column: number } {
172
+ if (this._endPosition !== undefined) return this._endPosition;
173
+ const s = this.check();
174
+ this._endPosition = {
175
+ row: s.symbols.al_shim_node_end_row(ptr(this.buf)),
176
+ column: s.symbols.al_shim_node_end_column(ptr(this.buf)),
177
+ };
178
+ return this._endPosition;
179
+ }
180
+ get text(): string {
181
+ const start = this.startIndex;
182
+ const end = this.endIndex;
183
+ return decoder.decode(this.tree.sourceBytes.subarray(start, end));
184
+ }
185
+ /**
186
+ * Raw UTF-8 byte slice of the node's source range — no string allocation.
187
+ * Use for hashing or byte-level comparison; for human consumption use `.text`.
188
+ */
189
+ get textBytes(): Uint8Array {
190
+ return this.tree.sourceBytes.subarray(this.startIndex, this.endIndex);
191
+ }
192
+ get childCount(): number {
193
+ if (this._childCount !== undefined) return this._childCount;
194
+ this._childCount = this.check().symbols.al_shim_node_child_count(ptr(this.buf));
195
+ return this._childCount;
196
+ }
197
+ get namedChildCount(): number {
198
+ if (this._namedChildCount !== undefined) return this._namedChildCount;
199
+ this._namedChildCount = this.check().symbols.al_shim_node_named_child_count(ptr(this.buf));
200
+ return this._namedChildCount;
201
+ }
202
+
203
+ child(i: number): Node | null {
204
+ const s = this.check();
205
+ const out = new Uint8Array(NODE_SIZE);
206
+ s.symbols.al_shim_node_child(ptr(this.buf), i, ptr(out));
207
+ return s.symbols.al_shim_node_is_null(ptr(out)) !== 0 ? null : new Node(this.tree, out);
208
+ }
209
+ namedChild(i: number): Node | null {
210
+ const s = this.check();
211
+ const out = new Uint8Array(NODE_SIZE);
212
+ s.symbols.al_shim_node_named_child(ptr(this.buf), i, ptr(out));
213
+ return s.symbols.al_shim_node_is_null(ptr(out)) !== 0 ? null : new Node(this.tree, out);
214
+ }
215
+ childForFieldName(name: string): Node | null {
216
+ const s = this.check();
217
+ const nameBytes = fieldNameBytes(name);
218
+ const out = new Uint8Array(NODE_SIZE);
219
+ s.symbols.al_shim_node_child_by_field_name(
220
+ ptr(this.buf),
221
+ ptr(nameBytes),
222
+ nameBytes.byteLength,
223
+ ptr(out),
224
+ );
225
+ return s.symbols.al_shim_node_is_null(ptr(out)) !== 0 ? null : new Node(this.tree, out);
226
+ }
227
+
228
+ get children(): Node[] {
229
+ if (this._children) return this._children;
230
+ const s = this.check();
231
+ const n = this.childCount;
232
+ const out: Node[] = new Array(n);
233
+ // Tree-sitter contract: child(i) for i < childCount is never null. Skip the
234
+ // is_null FFI call we'd otherwise pay per child — collapses 2 FFI calls to 1
235
+ // per child for the millions of children walked during indexing.
236
+ for (let i = 0; i < n; i++) {
237
+ const buf = new Uint8Array(NODE_SIZE);
238
+ s.symbols.al_shim_node_child(ptr(this.buf), i, ptr(buf));
239
+ out[i] = new Node(this.tree, buf);
240
+ }
241
+ this._children = out;
242
+ return out;
243
+ }
244
+ get namedChildren(): Node[] {
245
+ if (this._namedChildren) return this._namedChildren;
246
+ const s = this.check();
247
+ const n = this.namedChildCount;
248
+ const out: Node[] = new Array(n);
249
+ // Same dead-is_null elimination as `children` above.
250
+ for (let i = 0; i < n; i++) {
251
+ const buf = new Uint8Array(NODE_SIZE);
252
+ s.symbols.al_shim_node_named_child(ptr(this.buf), i, ptr(buf));
253
+ out[i] = new Node(this.tree, buf);
254
+ }
255
+ this._namedChildren = out;
256
+ return out;
257
+ }
258
+
259
+ get parent(): Node | null {
260
+ const s = this.check();
261
+ const out = new Uint8Array(NODE_SIZE);
262
+ s.symbols.al_shim_node_parent(ptr(this.buf), ptr(out));
263
+ return s.symbols.al_shim_node_is_null(ptr(out)) !== 0 ? null : new Node(this.tree, out);
264
+ }
265
+ get previousSibling(): Node | null {
266
+ const s = this.check();
267
+ const out = new Uint8Array(NODE_SIZE);
268
+ s.symbols.al_shim_node_previous_sibling(ptr(this.buf), ptr(out));
269
+ return s.symbols.al_shim_node_is_null(ptr(out)) !== 0 ? null : new Node(this.tree, out);
270
+ }
271
+ get nextSibling(): Node | null {
272
+ const s = this.check();
273
+ const out = new Uint8Array(NODE_SIZE);
274
+ s.symbols.al_shim_node_next_sibling(ptr(this.buf), ptr(out));
275
+ return s.symbols.al_shim_node_is_null(ptr(out)) !== 0 ? null : new Node(this.tree, out);
276
+ }
277
+ get isNamed(): boolean {
278
+ if (this._isNamed !== undefined) return this._isNamed;
279
+ this._isNamed = this.check().symbols.al_shim_node_is_named(ptr(this.buf)) !== 0;
280
+ return this._isNamed;
281
+ }
282
+ get hasError(): boolean {
283
+ if (this._hasError !== undefined) return this._hasError;
284
+ this._hasError = this.check().symbols.al_shim_node_has_error(ptr(this.buf)) !== 0;
285
+ return this._hasError;
286
+ }
287
+
288
+ /** Per-tree numeric interning. Same TSNode bytes in the same tree → same id. */
289
+ get id(): number {
290
+ if (this._id !== undefined) return this._id;
291
+ // Key by binary-string of the buffer (collision-free; fast for ~32-byte buffers).
292
+ let key = "";
293
+ for (let i = 0; i < this.buf.byteLength; i++) key += String.fromCharCode(this.buf[i] ?? 0);
294
+ const existing = this.tree.nodeIds.get(key);
295
+ if (existing !== undefined) {
296
+ this._id = existing;
297
+ return existing;
298
+ }
299
+ const id = this.tree.nextId++;
300
+ this.tree.nodeIds.set(key, id);
301
+ this._id = id;
302
+ return id;
303
+ }
304
+
305
+ walk(): TreeCursor {
306
+ return new TreeCursor(this.tree, this);
307
+ }
308
+ }
309
+
310
+ export class TreeCursor {
311
+ /** @internal */ readonly tree: Tree;
312
+ private handle: Pointer;
313
+ private disposed = false;
314
+
315
+ constructor(tree: Tree, startNode: Node) {
316
+ this.tree = tree;
317
+ const h = shim().symbols.al_shim_cursor_new(ptr(startNode.buf));
318
+ if (!h) throw new Error("al-sem: al_shim_cursor_new returned null (cursor allocation failed)");
319
+ this.handle = h;
320
+ }
321
+
322
+ private check(): ReturnType<typeof loadShim> {
323
+ if (this.disposed) throw new Error("TreeCursor is disposed");
324
+ if (this.tree.disposed) throw new Error("TreeCursor references a disposed Tree");
325
+ return shim();
326
+ }
327
+
328
+ gotoFirstChild(): boolean {
329
+ return this.check().symbols.al_shim_cursor_goto_first_child(this.handle) !== 0;
330
+ }
331
+ gotoNextSibling(): boolean {
332
+ return this.check().symbols.al_shim_cursor_goto_next_sibling(this.handle) !== 0;
333
+ }
334
+ gotoParent(): boolean {
335
+ return this.check().symbols.al_shim_cursor_goto_parent(this.handle) !== 0;
336
+ }
337
+ get currentNode(): Node {
338
+ const s = this.check();
339
+ const out = new Uint8Array(NODE_SIZE);
340
+ s.symbols.al_shim_cursor_current_node(this.handle, ptr(out));
341
+ return new Node(this.tree, out);
342
+ }
343
+ get currentFieldName(): string | null {
344
+ const s = this.check();
345
+ const cs = s.symbols.al_shim_cursor_current_field_name(this.handle);
346
+ return cs ? cs.toString() : null;
347
+ }
348
+ delete(): void {
349
+ if (this.disposed) return;
350
+ shim().symbols.al_shim_cursor_delete(this.handle);
351
+ this.disposed = true;
352
+ }
353
+ }
@@ -0,0 +1,43 @@
1
+ // src/parser/parser-init.ts
2
+ // Native parser bootstrap. Lazy-loads the shim, caches the parser handle for
3
+ // the process lifetime, and surfaces a missing/incompatible native library as
4
+ // a single typed NativeParserUnavailableError — consumers (`indexer.ts`,
5
+ // `dependency-pipeline.ts`) catch it and emit one diagnostic instead of
6
+ // thousands.
7
+
8
+ import { NativeParserUnavailableError, Parser, type Tree } from "./native/wrapper.ts";
9
+
10
+ export { NativeParserUnavailableError } from "./native/wrapper.ts";
11
+ export type { Tree } from "./native/wrapper.ts";
12
+
13
+ let cachedParser: Parser | null = null;
14
+ let loadError: NativeParserUnavailableError | null = null;
15
+
16
+ function ensureParser(): Parser {
17
+ if (cachedParser) return cachedParser;
18
+ if (loadError) throw loadError;
19
+ try {
20
+ cachedParser = new Parser();
21
+ return cachedParser;
22
+ } catch (err) {
23
+ if (err instanceof NativeParserUnavailableError) {
24
+ loadError = err;
25
+ throw err;
26
+ }
27
+ // Unexpected: wrap so callers can still pattern-match.
28
+ loadError = new NativeParserUnavailableError(
29
+ "missing",
30
+ `unexpected parser init failure: ${(err as Error).message}`,
31
+ );
32
+ throw loadError;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Parse AL source code into a syntax tree. Native; sync internally; async-shaped
38
+ * for source-compat with existing callers. Throws `NativeParserUnavailableError`
39
+ * if the native parser cannot be loaded.
40
+ */
41
+ export async function parseALSource(source: string): Promise<Tree> {
42
+ return ensureParser().parse(source);
43
+ }
@@ -0,0 +1,66 @@
1
+ // Structured phase profiler — one sink for all the pipeline's phase timers.
2
+ //
3
+ // Two consumers:
4
+ // 1. `AL_SEM_PROFILE=1` — writes each phase to stderr (the existing ad-hoc behaviour).
5
+ // 2. The benchmark harness — calls `collectPhases(fn)` to capture every phase emitted while
6
+ // `fn` runs as a structured `PhaseSample[]`, with no stderr noise.
7
+ //
8
+ // Zero-cost when neither is active: `makeLap`/`recordPhase` short-circuit on a single boolean
9
+ // check before reading the clock, so leaving the timers in hot loops costs nothing in normal runs.
10
+
11
+ export interface PhaseSample {
12
+ /** Hierarchical label, e.g. "analyze:runDetectors", "ingest:Base Application:pool-parse". */
13
+ label: string;
14
+ ms: number;
15
+ }
16
+
17
+ // The active collection sink. Non-null only inside a `collectPhases` call.
18
+ let sink: PhaseSample[] | null = null;
19
+
20
+ /** True when phases should be recorded (a collector is active OR stderr profiling is on). */
21
+ export function profilingActive(): boolean {
22
+ return sink !== null || process.env.AL_SEM_PROFILE === "1";
23
+ }
24
+
25
+ /** Record one phase sample to the active collector and/or stderr. */
26
+ export function recordPhase(label: string, ms: number): void {
27
+ if (sink !== null) sink.push({ label, ms });
28
+ if (process.env.AL_SEM_PROFILE === "1") {
29
+ process.stderr.write(` [phase] ${label} ${ms.toFixed(0)}ms\n`);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Make a lap timer. Each call records the elapsed time since the previous lap (or since creation)
35
+ * under `prefix + label`. No-op (beyond resetting the clock) when profiling is inactive.
36
+ */
37
+ export function makeLap(prefix = ""): (label: string) => void {
38
+ let t = performance.now();
39
+ return (label: string): void => {
40
+ if (!profilingActive()) {
41
+ t = performance.now();
42
+ return;
43
+ }
44
+ const now = performance.now();
45
+ recordPhase(prefix + label, now - t);
46
+ t = now;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Run `fn` while collecting every phase it emits into a fresh sink. Returns the result plus the
52
+ * collected samples. Nestable: restores the outer sink afterward. Used by the benchmark harness.
53
+ */
54
+ export async function collectPhases<T>(
55
+ fn: () => Promise<T> | T,
56
+ ): Promise<{ result: T; phases: PhaseSample[] }> {
57
+ const prev = sink;
58
+ const mine: PhaseSample[] = [];
59
+ sink = mine;
60
+ try {
61
+ const result = await fn();
62
+ return { result, phases: mine };
63
+ } finally {
64
+ sink = prev;
65
+ }
66
+ }
@@ -0,0 +1,83 @@
1
+ version: 1
2
+ description: "al-sem bundled default policy — high-precision rules for common Business Central correctness/security concerns."
3
+ defaults:
4
+ onUnknown: fail-open
5
+ requireCoverage: partial
6
+ rules:
7
+ - id: no-commit-in-event-subscribers
8
+ title: "Event subscribers must not perform Commit"
9
+ severity: high
10
+ when:
11
+ root.kinds: event-subscriber
12
+ capability.op: commit
13
+ onUnknown: fail-closed
14
+ facts: any
15
+
16
+ - id: no-commit-in-triggers
17
+ title: "Database triggers must not perform Commit"
18
+ severity: high
19
+ when:
20
+ root.kinds: trigger-table
21
+ capability.op: commit
22
+ onUnknown: fail-closed
23
+ facts: any
24
+
25
+ - id: api-no-interactive-ui
26
+ title: "API/web-service roots must not use interactive UI"
27
+ severity: high
28
+ when:
29
+ root.kinds: [api-page, web-service-exposed]
30
+ capability.op: [ui-confirm, ui-message]
31
+ onUnknown: fail-closed
32
+ facts: any
33
+
34
+ - id: api-no-user-dynamic-dispatch
35
+ title: "API/web-service roots must not perform user-driven dynamic dispatch"
36
+ severity: high
37
+ when:
38
+ root.kinds: [api-page, web-service-exposed]
39
+ capability.op: execute
40
+ capability.confidence: [userDynamic, configDynamic]
41
+ onUnknown: fail-closed
42
+ facts: any
43
+
44
+ - id: no-http-from-table-triggers
45
+ title: "Database triggers should not perform HTTP calls"
46
+ severity: medium
47
+ when:
48
+ root.kinds: trigger-table
49
+ capability.resourceKind: http
50
+ facts: any
51
+
52
+ - id: install-codeunit-no-business-data-writes
53
+ title: "Install codeunits should not write outside setup/migration tables"
54
+ severity: medium
55
+ when:
56
+ root.kinds: install-codeunit
57
+ capability.op: [insert, modify, delete]
58
+ except:
59
+ any:
60
+ - capability.resource.table.name: "* Setup"
61
+ - capability.resource.table.name: "* Setup *"
62
+ - capability.resource.table.name: "* Migration *"
63
+ - capability.resource.table.name: "* Buffer"
64
+ facts: any
65
+
66
+ - id: exposed-root-no-isolated-storage-write
67
+ title: "API/web-service roots must not write to IsolatedStorage"
68
+ severity: high
69
+ when:
70
+ root.kinds: [api-page, web-service-exposed]
71
+ capability.op: [store-write, store-delete]
72
+ onUnknown: fail-closed
73
+ facts: any
74
+
75
+ - id: exposed-root-no-direct-ledger-write
76
+ title: "API/web-service roots must not directly write ledger tables"
77
+ severity: high
78
+ when:
79
+ root.kinds: [api-page, web-service-exposed]
80
+ capability.op: [insert, modify, delete]
81
+ capability.resource.table.name: "* Ledger Entry"
82
+ onUnknown: fail-closed
83
+ facts: any