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,60 @@
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-β hyperlink family extractor. Detects bare `Hyperlink(url)` calls.
8
+ *
9
+ * Emits: op="open", resourceKind="ui", resourceArgSource = classifyValueSource(urlArg),
10
+ * confidence derived from URL ValueSource kind. Never throws.
11
+ */
12
+ export function extractHyperlink(ctx: ExtractionContext): {
13
+ facts: CapabilityFact[];
14
+ reasons: CoverageReason[];
15
+ } {
16
+ try {
17
+ const facts: CapabilityFact[] = [];
18
+ for (const cs of ctx.routine?.features?.callSites ?? []) {
19
+ const callee = cs.callee;
20
+ if (!callee || callee.kind !== "bare") continue;
21
+ if (typeof callee.name !== "string") continue;
22
+ if (callee.name.toLowerCase() !== "hyperlink") continue;
23
+
24
+ const urlInfo = cs.argumentInfos?.[0];
25
+ const urlSource: ValueSource =
26
+ urlInfo !== undefined ? classifyValueSource(urlInfo, ctx) : { kind: "unknown" };
27
+
28
+ facts.push({
29
+ subject: ctx.routine.id,
30
+ op: "open",
31
+ resourceKind: "ui",
32
+ resourceArgSource: urlSource,
33
+ confidence: confidenceFromSource(urlSource),
34
+ provenance: "direct",
35
+ via: "self",
36
+ witnessCallsiteId: cs.id,
37
+ });
38
+ }
39
+ return { facts, reasons: [] };
40
+ } catch {
41
+ return { facts: [], reasons: ["extraction-failed"] };
42
+ }
43
+ }
44
+
45
+ function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
46
+ switch (vs.kind) {
47
+ case "literal":
48
+ case "enum":
49
+ return "static";
50
+ case "constant-var":
51
+ return confidenceFromSource(vs.initializer);
52
+ case "parameter":
53
+ return "userDynamic";
54
+ case "table-field":
55
+ return "configDynamic";
56
+ case "expression":
57
+ case "unknown":
58
+ return "unresolved";
59
+ }
60
+ }
@@ -0,0 +1,179 @@
1
+ import type {
2
+ CapabilityConfidence,
3
+ CapabilityFact,
4
+ CapabilityOp,
5
+ StorageExtra,
6
+ ValueSource,
7
+ } from "../../model/capability.ts";
8
+ import type { CoverageReason } from "../../model/coverage.ts";
9
+ import type { CallsiteId, OperationId } from "../../model/ids.ts";
10
+ import type { ExtractionContext } from "./extractor.ts";
11
+ import { classifyValueSource } from "./value-source.ts";
12
+
13
+ /**
14
+ * Maps IsolatedStorage method names (lowercase) to their capability op.
15
+ * Methods not in this map are ignored.
16
+ */
17
+ const ISOLATED_STORAGE_OPS = new Map<string, CapabilityOp>([
18
+ ["get", "store-read"],
19
+ ["getencrypted", "store-read"],
20
+ ["contains", "store-read"],
21
+ ["set", "store-write"],
22
+ ["setencrypted", "store-write"],
23
+ ["delete", "store-delete"],
24
+ ]);
25
+
26
+ /**
27
+ * Maps a DataScope enum text (e.g. "DataScope::Company") to the StorageExtra
28
+ * scope literal. Falls back to "unknown" for unrecognized values.
29
+ */
30
+ function parseDataScope(text: string): StorageExtra["scope"] {
31
+ const lower = text.toLowerCase();
32
+ if (lower.includes("::company")) return "Company";
33
+ if (lower.includes("::user")) return "User";
34
+ if (lower.includes("::module")) return "Module";
35
+ return "unknown";
36
+ }
37
+
38
+ function buildStorageFact(
39
+ ctx: ExtractionContext,
40
+ op: CapabilityOp,
41
+ keySource: ValueSource,
42
+ extra: StorageExtra,
43
+ witness: { kind: "operation"; id: OperationId } | { kind: "callsite"; id: CallsiteId },
44
+ ): CapabilityFact {
45
+ const witnessFields =
46
+ witness.kind === "operation"
47
+ ? { witnessOperationId: witness.id }
48
+ : { witnessCallsiteId: witness.id };
49
+ return {
50
+ subject: ctx.routine.id,
51
+ op,
52
+ resourceKind: "isolated-storage",
53
+ resourceArgSource: keySource,
54
+ confidence: confidenceFromSource(keySource),
55
+ provenance: "direct",
56
+ via: "self",
57
+ ...witnessFields,
58
+ extra,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Phase 0b-β isolated-storage family extractor. Detects IsolatedStorage
64
+ * system reference method calls.
65
+ *
66
+ * IsolatedStorage is a SYSTEM REFERENCE — not a declared variable. The gate
67
+ * is `receiver.toLowerCase() === "isolatedstorage"` (member callee) or
68
+ * `recordVariableName.toLowerCase() === "isolatedstorage"` (recordOperation).
69
+ *
70
+ * Why both branches? `intraprocedural-ops.ts` RECORD_OP_MAP maps `.Get` and
71
+ * `.Delete` to RecordOperation regardless of receiver type — so these two
72
+ * methods land in `recordOperations`. The remaining methods (`.Set`,
73
+ * `.SetEncrypted`, `.GetEncrypted`, `.Contains`) land in `callSites` as
74
+ * member callees.
75
+ *
76
+ * Op mapping:
77
+ * .Get, .GetEncrypted, .Contains → store-read
78
+ * .Set, .SetEncrypted → store-write
79
+ * .Delete → store-delete
80
+ *
81
+ * `resourceArgSource` is classifyValueSource on the key argument (arg[0] for
82
+ * callSites, fieldArgumentInfos[0] for recordOperations). `.Delete` has no
83
+ * fieldArgumentInfos so key falls back to `{ kind: "unknown" }`.
84
+ *
85
+ * `StorageExtra` carries:
86
+ * keyArgSource — same as resourceArgSource
87
+ * valueArgSource — arg[1] for .Set/.SetEncrypted (callSites only)
88
+ * scope — arg[2] for .Set/.SetEncrypted; parsed from DataScope enum text
89
+ *
90
+ * Never throws.
91
+ */
92
+ export function extractIsolatedStorage(ctx: ExtractionContext): {
93
+ facts: CapabilityFact[];
94
+ reasons: CoverageReason[];
95
+ } {
96
+ try {
97
+ const facts: CapabilityFact[] = [];
98
+
99
+ // Branch A: IsolatedStorage.Get / .Delete from recordOperations.
100
+ // intraprocedural-ops.ts recognises Get/Delete via RECORD_OP_MAP
101
+ // regardless of receiver type — filter by recordVariableName here.
102
+ for (const ro of ctx.routine?.features?.recordOperations ?? []) {
103
+ const recv = ro.recordVariableName;
104
+ if (typeof recv !== "string") continue;
105
+ if (recv.toLowerCase() !== "isolatedstorage") continue;
106
+
107
+ const method = ro.op.toLowerCase();
108
+ const op = ISOLATED_STORAGE_OPS.get(method);
109
+ if (op === undefined) continue;
110
+
111
+ const keyInfo = ro.fieldArgumentInfos?.[0];
112
+ const keySource: ValueSource =
113
+ keyInfo !== undefined ? classifyValueSource(keyInfo, ctx) : { kind: "unknown" };
114
+
115
+ const extra: StorageExtra = {
116
+ kind: "storage",
117
+ keyArgSource: keySource,
118
+ };
119
+
120
+ facts.push(buildStorageFact(ctx, op, keySource, extra, { kind: "operation", id: ro.id }));
121
+ }
122
+
123
+ // Branch B: IsolatedStorage.Set / .SetEncrypted / .GetEncrypted / .Contains
124
+ // from callSites (member callees not caught by RECORD_OP_MAP).
125
+ for (const cs of ctx.routine?.features?.callSites ?? []) {
126
+ const callee = cs.callee;
127
+ if (!callee || callee.kind !== "member") continue;
128
+ if (callee.receiver.toLowerCase() !== "isolatedstorage") continue;
129
+
130
+ const method = callee.method.toLowerCase();
131
+ const op = ISOLATED_STORAGE_OPS.get(method);
132
+ if (op === undefined) continue;
133
+
134
+ const keyInfo = cs.argumentInfos?.[0];
135
+ const keySource: ValueSource =
136
+ keyInfo !== undefined ? classifyValueSource(keyInfo, ctx) : { kind: "unknown" };
137
+
138
+ const extra: StorageExtra = {
139
+ kind: "storage",
140
+ keyArgSource: keySource,
141
+ };
142
+
143
+ // For write methods: capture value arg (arg[1]) and scope arg (arg[2]).
144
+ if (op === "store-write") {
145
+ const valueInfo = cs.argumentInfos?.[1];
146
+ if (valueInfo !== undefined) {
147
+ extra.valueArgSource = classifyValueSource(valueInfo, ctx);
148
+ }
149
+ const scopeInfo = cs.argumentInfos?.[2];
150
+ if (scopeInfo !== undefined) {
151
+ extra.scope = parseDataScope(scopeInfo.text ?? "");
152
+ }
153
+ }
154
+
155
+ facts.push(buildStorageFact(ctx, op, keySource, extra, { kind: "callsite", id: cs.id }));
156
+ }
157
+
158
+ return { facts, reasons: [] };
159
+ } catch {
160
+ return { facts: [], reasons: ["extraction-failed"] };
161
+ }
162
+ }
163
+
164
+ function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
165
+ switch (vs.kind) {
166
+ case "literal":
167
+ case "enum":
168
+ return "static";
169
+ case "constant-var":
170
+ return confidenceFromSource(vs.initializer);
171
+ case "parameter":
172
+ return "userDynamic";
173
+ case "table-field":
174
+ return "configDynamic";
175
+ case "expression":
176
+ case "unknown":
177
+ return "unresolved";
178
+ }
179
+ }
@@ -0,0 +1,113 @@
1
+ import type { CapabilityFact, CapabilityOp, TableExtra } from "../../model/capability.ts";
2
+ import type { CoverageReason } from "../../model/coverage.ts";
3
+ import type { RecordOpType } from "../../model/entities.ts";
4
+ import type { ExtractionContext } from "./extractor.ts";
5
+
6
+ /**
7
+ * Map an AL RecordOpType to a CapabilityOp.
8
+ *
9
+ * Returns undefined for state-only / filter ops (SetRange, SetFilter, Init,
10
+ * SetLoadFields, AddLoadFields, SetCurrentKey, Reset, LockTable) — these are
11
+ * NOT capability-relevant per spec §3.1 substrate-discipline. Phase 1
12
+ * detectors that need filter / load-field state read RecordOperation directly.
13
+ */
14
+ function mapOp(op: RecordOpType): CapabilityOp | undefined {
15
+ switch (op) {
16
+ case "Get":
17
+ case "Find":
18
+ case "FindFirst":
19
+ case "FindLast":
20
+ case "FindSet":
21
+ case "IsEmpty":
22
+ case "Count":
23
+ case "CountApprox":
24
+ case "Next":
25
+ case "CalcFields":
26
+ case "CalcSums":
27
+ case "TestField":
28
+ return "read";
29
+ case "Modify":
30
+ case "ModifyAll":
31
+ case "Validate":
32
+ case "Copy":
33
+ case "TransferFields":
34
+ return "modify";
35
+ case "Insert":
36
+ return "insert";
37
+ case "Delete":
38
+ case "DeleteAll":
39
+ return "delete";
40
+ case "Init":
41
+ case "SetRange":
42
+ case "SetFilter":
43
+ case "SetLoadFields":
44
+ case "AddLoadFields":
45
+ case "SetCurrentKey":
46
+ case "Reset":
47
+ case "LockTable":
48
+ return undefined;
49
+ default: {
50
+ // Exhaustiveness guard — adding a RecordOpType variant must extend
51
+ // this switch. The `never` cast triggers tsc if a new variant slips in.
52
+ const _exhaustive: never = op;
53
+ return _exhaustive;
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Phase 0b-β table family extractor.
60
+ *
61
+ * Iterates `ctx.routine.features.recordOperations` (already indexed by L2)
62
+ * and maps each RecordOperation to a CapabilityFact. State-only ops are
63
+ * skipped (undefined mapOp result).
64
+ *
65
+ * Confidence:
66
+ * - "static" when tableId is present (resolved from .app symbol deps)
67
+ * - "unresolved" when tableId is absent (pure workspace, no dep resolution)
68
+ *
69
+ * TableExtra carries opSubtype, recordVariableId, and tempState — per spec
70
+ * §3.1, fieldArguments are NOT mirrored into extra (they stay on RecordOperation
71
+ * for Phase 1 detectors to read directly).
72
+ *
73
+ * Never throws.
74
+ */
75
+ export function extractTable(ctx: ExtractionContext): {
76
+ facts: CapabilityFact[];
77
+ reasons: CoverageReason[];
78
+ } {
79
+ try {
80
+ const facts: CapabilityFact[] = [];
81
+ const recordOps = ctx.routine?.features?.recordOperations ?? [];
82
+
83
+ for (const op of recordOps) {
84
+ const capOp = mapOp(op.op);
85
+ if (capOp === undefined) continue;
86
+
87
+ const extra: TableExtra = {
88
+ kind: "table",
89
+ opSubtype: op.op,
90
+ recordVariableId: op.recordVariableId,
91
+ tempState: op.tempState,
92
+ };
93
+
94
+ const fact: CapabilityFact = {
95
+ subject: ctx.routine.id,
96
+ op: capOp,
97
+ resourceKind: "table",
98
+ resourceId: op.tableId,
99
+ confidence: op.tableId !== undefined ? "static" : "unresolved",
100
+ provenance: "direct",
101
+ via: "self",
102
+ witnessOperationId: op.id,
103
+ extra,
104
+ };
105
+
106
+ facts.push(fact);
107
+ }
108
+
109
+ return { facts, reasons: [] };
110
+ } catch {
111
+ return { facts: [], reasons: ["extraction-failed"] };
112
+ }
113
+ }
@@ -0,0 +1,84 @@
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-β telemetry family extractor. Detects:
8
+ * - `Session.LogMessage(eventId, message, ...)` — member callee, receiver = "Session"
9
+ * - `LogMessage(eventId, message, ...)` — bare callee
10
+ *
11
+ * Emits one CapabilityFact per match:
12
+ * op: "log", resourceKind: "telemetry"
13
+ * resourceArgSource: classifyValueSource on eventId (1st positional arg)
14
+ * confidence derived from eventId ValueSource kind (same pattern as http.ts)
15
+ * provenance: "direct", via: "self"
16
+ * witnessCallsiteId: cs.id (both bare + member calls land in callSites)
17
+ *
18
+ * No TelemetryExtra defined in src/model/capability.ts — `extra` is omitted.
19
+ *
20
+ * Never throws.
21
+ */
22
+ export function extractTelemetry(ctx: ExtractionContext): {
23
+ facts: CapabilityFact[];
24
+ reasons: CoverageReason[];
25
+ } {
26
+ try {
27
+ const facts: CapabilityFact[] = [];
28
+ for (const cs of ctx.routine?.features?.callSites ?? []) {
29
+ const callee = cs.callee;
30
+ if (!callee) continue;
31
+
32
+ let matches = false;
33
+ if (callee.kind === "member") {
34
+ // Session.LogMessage — receiver name compared case-insensitively
35
+ if (
36
+ callee.receiver.toLowerCase() === "session" &&
37
+ callee.method.toLowerCase() === "logmessage"
38
+ ) {
39
+ matches = true;
40
+ }
41
+ } else if (callee.kind === "bare") {
42
+ if (callee.name.toLowerCase() === "logmessage") {
43
+ matches = true;
44
+ }
45
+ }
46
+ if (!matches) continue;
47
+
48
+ const eventIdInfo = cs.argumentInfos?.[0];
49
+ const eventIdSource: ValueSource =
50
+ eventIdInfo !== undefined ? classifyValueSource(eventIdInfo, ctx) : { kind: "unknown" };
51
+
52
+ facts.push({
53
+ subject: ctx.routine.id,
54
+ op: "log",
55
+ resourceKind: "telemetry",
56
+ resourceArgSource: eventIdSource,
57
+ confidence: confidenceFromSource(eventIdSource),
58
+ provenance: "direct",
59
+ via: "self",
60
+ witnessCallsiteId: cs.id,
61
+ });
62
+ }
63
+ return { facts, reasons: [] };
64
+ } catch {
65
+ return { facts: [], reasons: ["extraction-failed"] };
66
+ }
67
+ }
68
+
69
+ function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
70
+ switch (vs.kind) {
71
+ case "literal":
72
+ case "enum":
73
+ return "static";
74
+ case "constant-var":
75
+ return confidenceFromSource(vs.initializer);
76
+ case "parameter":
77
+ return "userDynamic";
78
+ case "table-field":
79
+ return "configDynamic";
80
+ case "expression":
81
+ case "unknown":
82
+ return "unresolved";
83
+ }
84
+ }
@@ -0,0 +1,55 @@
1
+ import type { CapabilityFact, CapabilityOp } from "../../model/capability.ts";
2
+ import type { CoverageReason } from "../../model/coverage.ts";
3
+ import type { ExtractionContext } from "./extractor.ts";
4
+
5
+ const UI_PRIMITIVES: Record<
6
+ string,
7
+ Extract<CapabilityOp, "ui-confirm" | "ui-message" | "ui-error">
8
+ > = {
9
+ confirm: "ui-confirm",
10
+ message: "ui-message",
11
+ error: "ui-error",
12
+ };
13
+
14
+ /**
15
+ * Phase 0b-β ui family extractor. Detects bare UI primitive calls:
16
+ * Confirm(...) → ui-confirm
17
+ * Message(...) → ui-message
18
+ * Error(...) → ui-error
19
+ *
20
+ * Presence facts — no resourceArgSource (message text isn't a resource id),
21
+ * no extra. Confidence always "static" (the primitive itself is the resource).
22
+ *
23
+ * Note: Error is already handled by D20 for control-flow semantics; this
24
+ * UI extraction is orthogonal — UI capability is independent of CFG semantics.
25
+ *
26
+ * Never throws.
27
+ */
28
+ export function extractUi(ctx: ExtractionContext): {
29
+ facts: CapabilityFact[];
30
+ reasons: CoverageReason[];
31
+ } {
32
+ try {
33
+ const facts: CapabilityFact[] = [];
34
+ for (const cs of ctx.routine?.features?.callSites ?? []) {
35
+ const callee = cs.callee;
36
+ if (!callee || callee.kind !== "bare") continue;
37
+ if (typeof callee.name !== "string") continue;
38
+ const op = UI_PRIMITIVES[callee.name.toLowerCase()];
39
+ if (op === undefined) continue;
40
+
41
+ facts.push({
42
+ subject: ctx.routine.id,
43
+ op,
44
+ resourceKind: "ui",
45
+ confidence: "static",
46
+ provenance: "direct",
47
+ via: "self",
48
+ witnessCallsiteId: cs.id,
49
+ });
50
+ }
51
+ return { facts, reasons: [] };
52
+ } catch {
53
+ return { facts: [], reasons: ["extraction-failed"] };
54
+ }
55
+ }
@@ -0,0 +1,202 @@
1
+ // src/index/capability/value-source.ts
2
+ //
3
+ // Classify an `ExpressionInfo` (a serialized, tree-sitter-derived expression
4
+ // summary) into a `ValueSource`. Used by every capability family extractor when
5
+ // capturing resource arguments: URL for HTTP, key for IsolatedStorage, target id
6
+ // for object-run dispatch, etc.
7
+ //
8
+ // Chases `constant-var` chains by looking up the identifier name in
9
+ // `ctx.variables` and inspecting the `VariableSymbol.initializer` captured by
10
+ // Phase 0b-α's `extractInitializer`. Chase depth is capped at MAX_CHASE_DEPTH
11
+ // (3) to prevent infinite recursion on circular initializer chains.
12
+ //
13
+ // MUST NEVER throw — the outer `try/catch` in `classifyValueSource` returns
14
+ // `{kind: "unknown"}` on any internal error, per the engine-never-throws
15
+ // contract (CLAUDE.md §"The engine never throws").
16
+
17
+ import type { ValueSource } from "../../model/capability.ts";
18
+ import type { ExpressionInfo } from "../../model/expression.ts";
19
+ import type { ExtractionContext } from "./extractor.ts";
20
+
21
+ const MAX_CHASE_DEPTH = 3;
22
+
23
+ /**
24
+ * Classify an `ExpressionInfo` as a `ValueSource`. Phase 0b-β capability
25
+ * extractors call this for every resource argument (HTTP URL, IsolatedStorage
26
+ * key, dispatch target id, etc.) so downstream consumers can reason about
27
+ * provenance (literal vs config-table vs user-input).
28
+ *
29
+ * Pass `null` or `undefined` to get `{kind: "unknown"}` — never throws.
30
+ */
31
+ export function classifyValueSource(
32
+ info: ExpressionInfo | null | undefined,
33
+ ctx: ExtractionContext,
34
+ ): ValueSource {
35
+ try {
36
+ return classifyAtDepth(info, ctx, 0);
37
+ } catch {
38
+ return { kind: "unknown" };
39
+ }
40
+ }
41
+
42
+ function classifyAtDepth(
43
+ info: ExpressionInfo | null | undefined,
44
+ ctx: ExtractionContext,
45
+ depth: number,
46
+ ): ValueSource {
47
+ if (info === null || info === undefined) {
48
+ return { kind: "unknown" };
49
+ }
50
+
51
+ switch (info.kind) {
52
+ // ── Literal forms ──────────────────────────────────────────────────────
53
+ case "string_literal":
54
+ // `info.value` is the content between the single quotes (set by
55
+ // expressionInfoFromNode / deriveParts for string_literal).
56
+ return { kind: "literal", value: info.value ?? stripSingleQuotes(info.text) };
57
+
58
+ case "integer":
59
+ case "decimal":
60
+ case "boolean":
61
+ return { kind: "literal", value: info.value ?? info.text.trim() };
62
+
63
+ // ── Enum / database reference ─────────────────────────────────────────
64
+ // Both `qualified_enum_value` (e.g. `MyEnum::Value`) and
65
+ // `database_reference` (e.g. `Codeunit::"Job Q Codeunit"`) have the
66
+ // same shape: qualifier = LHS of `::`, member/value = RHS. Both are
67
+ // statically-known references, so both map to ValueSource "enum".
68
+ case "qualified_enum_value":
69
+ case "database_reference": {
70
+ const enumName = info.qualifier !== undefined ? stripDoubleQuotes(info.qualifier) : "";
71
+ const member = info.member ?? info.value ?? "";
72
+ return { kind: "enum", enumName, member };
73
+ }
74
+
75
+ // ── Identifier — parameter, constant-var, or chase ────────────────────
76
+ case "identifier":
77
+ case "quoted_identifier": {
78
+ const name = info.value !== undefined ? info.value.toLowerCase() : info.text.toLowerCase();
79
+ return classifyIdentifier(name, ctx, depth);
80
+ }
81
+
82
+ // ── Member expression — potential table-field ─────────────────────────
83
+ case "member_expression": {
84
+ return classifyMemberExpression(info.text, ctx);
85
+ }
86
+
87
+ // ── Unary expression (e.g. -3) — literal when operand is numeric ──────
88
+ case "unary_expression":
89
+ if (info.value !== undefined) return { kind: "literal", value: info.value };
90
+ return { kind: "expression" };
91
+
92
+ // ── Call / anything else → expression ─────────────────────────────────
93
+ default:
94
+ return { kind: "expression" };
95
+ }
96
+ }
97
+
98
+ // ─── Identifier resolution ──────────────────────────────────────────────────
99
+
100
+ function classifyIdentifier(nameLower: string, ctx: ExtractionContext, depth: number): ValueSource {
101
+ const sym = ctx.variables.get(nameLower);
102
+ if (sym === undefined) {
103
+ // Not in scope — treat as an opaque expression reference.
104
+ return { kind: "expression" };
105
+ }
106
+
107
+ if (sym.isParameter) {
108
+ return {
109
+ kind: "parameter",
110
+ index: sym.parameterIndex ?? 0,
111
+ varName: nameLower,
112
+ };
113
+ }
114
+
115
+ // Local variable — see if we can chase the initializer.
116
+ const init = sym.initializer;
117
+ if (init === undefined || init.kind === "unknown" || init.kind === "expression") {
118
+ // No initializer captured or it's already opaque — emit constant-var.
119
+ return { kind: "constant-var", varName: nameLower, initializer: init ?? { kind: "unknown" } };
120
+ }
121
+
122
+ if (depth >= MAX_CHASE_DEPTH) {
123
+ // Depth cap hit — return constant-var with the raw initializer but don't recurse.
124
+ return { kind: "constant-var", varName: nameLower, initializer: init };
125
+ }
126
+
127
+ // Chase one hop deeper for `constant-var` (var-to-var alias).
128
+ if (init.kind === "constant-var") {
129
+ const deeper = classifyIdentifier(init.varName, ctx, depth + 1);
130
+ if (deeper.kind === "literal" || deeper.kind === "enum" || deeper.kind === "parameter") {
131
+ // Successfully resolved to a concrete source — return it directly so
132
+ // callers see the root kind rather than a chain of constant-var wrappers.
133
+ return deeper;
134
+ }
135
+ return { kind: "constant-var", varName: nameLower, initializer: deeper };
136
+ }
137
+
138
+ // Initializer is already a resolved kind (literal / enum / parameter / table-field).
139
+ return init;
140
+ }
141
+
142
+ // ─── Member expression (potential table-field) ──────────────────────────────
143
+
144
+ /**
145
+ * Parse a member-expression text of the form `Receiver.Field` or
146
+ * `Receiver."Quoted Field"` and classify it as a `table-field` when the
147
+ * receiver resolves to a record-typed variable with a known tableId.
148
+ * Falls back to `expression` when the receiver is not a known record variable.
149
+ *
150
+ * The text splitting relies on the first `.` being the separator between the
151
+ * receiver identifier and the field member. This is safe because:
152
+ * - AL receiver names are bare identifiers (no dots).
153
+ * - `member_expression` nodes in the grammar always have exactly one `.`
154
+ * separator at the top level.
155
+ */
156
+ function classifyMemberExpression(text: string, ctx: ExtractionContext): ValueSource {
157
+ const dotIdx = text.indexOf(".");
158
+ if (dotIdx === -1) {
159
+ return { kind: "expression" };
160
+ }
161
+
162
+ const receiverRaw = text.slice(0, dotIdx).trim();
163
+ const fieldRaw = text.slice(dotIdx + 1).trim();
164
+ const receiverLower = receiverRaw.toLowerCase();
165
+
166
+ const sym = ctx.variables.get(receiverLower);
167
+ if (sym === undefined) {
168
+ return { kind: "expression" };
169
+ }
170
+
171
+ // Is the receiver a record variable?
172
+ const declType = sym.declaredType.toLowerCase();
173
+ const isRecord =
174
+ declType.startsWith("record ") || declType === "record" || declType.startsWith("recordref");
175
+
176
+ if (!isRecord) {
177
+ // Member call on a non-record (e.g. HttpClient.Get) — expression.
178
+ return { kind: "expression" };
179
+ }
180
+
181
+ const fieldName = stripDoubleQuotes(fieldRaw);
182
+ const tableId = sym.tableId ?? "unknown";
183
+ return { kind: "table-field", tableId, fieldName };
184
+ }
185
+
186
+ // ─── Quote helpers ───────────────────────────────────────────────────────────
187
+
188
+ function stripSingleQuotes(s: string): string {
189
+ const t = s.trim();
190
+ if (t.length >= 2 && t[0] === "'" && t[t.length - 1] === "'") {
191
+ return t.slice(1, -1);
192
+ }
193
+ return t;
194
+ }
195
+
196
+ function stripDoubleQuotes(s: string): string {
197
+ const t = s.trim();
198
+ if (t.length >= 2 && t[0] === '"' && t[t.length - 1] === '"') {
199
+ return t.slice(1, -1);
200
+ }
201
+ return t;
202
+ }