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,164 @@
1
+ // src/index/capability/dispatch.ts
2
+ //
3
+ // Phase 0b-β dispatch family extractor. Detects dynamic object-dispatch calls:
4
+ //
5
+ // object-run callees (pre-classified by the Callee model at L2):
6
+ // Codeunit.Run(targetId, ...) → execute, codeunit
7
+ // Page.Run(targetId, ...) → execute, page
8
+ // Report.Run(targetId, ...) → execute, report
9
+ //
10
+ // member callees (not pre-classified as object-run by the L2 indexer):
11
+ // Page.RunModal(targetId, ...) → execute, page, modal: true
12
+ // Report.Execute(targetId, ...) → execute, report
13
+ //
14
+ // Target-id argument (1st positional) is classified via classifyValueSource:
15
+ // literal / enum / database_reference → static
16
+ // parameter → userDynamic
17
+ // table-field → configDynamic
18
+ // expression / unknown → unresolved
19
+ //
20
+ // resourceId resolution is deferred — stable-identity map wiring through to
21
+ // extraction time is a Phase 1c follow-up.
22
+ //
23
+ // Never throws.
24
+
25
+ import type { ObjectRunKind } from "../../model/callee.ts";
26
+ import type {
27
+ CapabilityConfidence,
28
+ CapabilityFact,
29
+ DispatchExtra,
30
+ ValueSource,
31
+ } from "../../model/capability.ts";
32
+ import type { CoverageReason } from "../../model/coverage.ts";
33
+ import type { ExtractionContext } from "./extractor.ts";
34
+ import { classifyValueSource } from "./value-source.ts";
35
+
36
+ // ─── Member-callee dispatch table ────────────────────────────────────────────
37
+ // Covers methods that the L2 indexer does NOT pre-classify as "object-run"
38
+ // because they use non-canonical method names (RunModal, Execute).
39
+
40
+ interface MemberDispatchSpec {
41
+ objectType: ObjectRunKind;
42
+ modal?: true;
43
+ }
44
+
45
+ // Keys are lowercased "receiver|method" pairs.
46
+ const MEMBER_DISPATCH_MAP = new Map<string, MemberDispatchSpec>([
47
+ ["page|runmodal", { objectType: "Page", modal: true }],
48
+ ["report|execute", { objectType: "Report" }],
49
+ ]);
50
+
51
+ // ─── Public extractor ─────────────────────────────────────────────────────────
52
+
53
+ /**
54
+ * Phase 0b-β dispatch family extractor.
55
+ *
56
+ * Scans `callSites` for two callee shapes:
57
+ * - `"object-run"` — pre-classified by the L2 indexer for `.Run` methods on
58
+ * Codeunit / Page / Report. `objectKind` gives the target object type.
59
+ * - `"member"` — for `Page.RunModal` and `Report.Execute`, which the L2
60
+ * indexer emits as member callees because their method name differs from
61
+ * the canonical `.Run`.
62
+ *
63
+ * Emits one `CapabilityFact` (op: "execute") per matched call site.
64
+ *
65
+ * Never throws.
66
+ */
67
+ export function extractDispatch(ctx: ExtractionContext): {
68
+ facts: CapabilityFact[];
69
+ reasons: CoverageReason[];
70
+ } {
71
+ try {
72
+ const facts: CapabilityFact[] = [];
73
+
74
+ for (const cs of ctx.routine?.features?.callSites ?? []) {
75
+ const callee = cs.callee;
76
+ if (!callee) continue;
77
+
78
+ if (callee.kind === "object-run") {
79
+ // Pre-classified by the L2 indexer: Codeunit.Run, Page.Run, Report.Run
80
+ const objectType = callee.objectKind;
81
+ const targetArgSource = classifyTargetArg(cs.argumentInfos, 0, ctx);
82
+ const extra: DispatchExtra = { kind: "dispatch", objectType };
83
+ facts.push(buildFact(ctx, objectType, targetArgSource, extra, cs.id));
84
+ } else if (callee.kind === "member") {
85
+ // Page.RunModal / Report.Execute — not pre-classified as object-run
86
+ const key = `${callee.receiver.toLowerCase()}|${callee.method.toLowerCase()}`;
87
+ const spec = MEMBER_DISPATCH_MAP.get(key);
88
+ if (spec === undefined) continue;
89
+
90
+ const targetArgSource = classifyTargetArg(cs.argumentInfos, 0, ctx);
91
+ const extra: DispatchExtra = {
92
+ kind: "dispatch",
93
+ objectType: spec.objectType,
94
+ ...(spec.modal === true ? { modal: true } : {}),
95
+ };
96
+ facts.push(buildFact(ctx, spec.objectType, targetArgSource, extra, cs.id));
97
+ }
98
+ }
99
+
100
+ return { facts, reasons: [] };
101
+ } catch {
102
+ return { facts: [], reasons: ["extraction-failed"] };
103
+ }
104
+ }
105
+
106
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
107
+
108
+ function classifyTargetArg(
109
+ argumentInfos: readonly { kind: string; text: string }[] | undefined,
110
+ index: number,
111
+ ctx: ExtractionContext,
112
+ ): ValueSource {
113
+ const arg = argumentInfos?.[index];
114
+ return arg !== undefined ? classifyValueSource(arg as never, ctx) : { kind: "unknown" };
115
+ }
116
+
117
+ function buildFact(
118
+ ctx: ExtractionContext,
119
+ objectType: ObjectRunKind,
120
+ targetArgSource: ValueSource,
121
+ extra: DispatchExtra,
122
+ witnessCallsiteId: string,
123
+ ): CapabilityFact {
124
+ const resourceKind = objectTypeToResourceKind(objectType);
125
+ return {
126
+ subject: ctx.routine.id,
127
+ op: "execute",
128
+ resourceKind,
129
+ resourceArgSource: targetArgSource,
130
+ confidence: confidenceFromSource(targetArgSource),
131
+ provenance: "direct",
132
+ via: "self",
133
+ witnessCallsiteId,
134
+ extra,
135
+ };
136
+ }
137
+
138
+ function objectTypeToResourceKind(objectType: ObjectRunKind): "codeunit" | "page" | "report" {
139
+ switch (objectType) {
140
+ case "Codeunit":
141
+ return "codeunit";
142
+ case "Page":
143
+ return "page";
144
+ case "Report":
145
+ return "report";
146
+ }
147
+ }
148
+
149
+ function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
150
+ switch (vs.kind) {
151
+ case "literal":
152
+ case "enum":
153
+ return "static";
154
+ case "constant-var":
155
+ return confidenceFromSource(vs.initializer);
156
+ case "parameter":
157
+ return "userDynamic";
158
+ case "table-field":
159
+ return "configDynamic";
160
+ case "expression":
161
+ case "unknown":
162
+ return "unresolved";
163
+ }
164
+ }
@@ -0,0 +1,65 @@
1
+ // src/index/capability/events.ts
2
+ //
3
+ // Phase 0b-β events family extractor. Scope: SUBSCRIBE side only.
4
+ //
5
+ // A routine with an [EventSubscriber(...)] attribute is a subscriber to a
6
+ // published event. The structured attribute (already in routine.attributesParsed)
7
+ // tells us which publisher + event name; for this phase we emit a presence
8
+ // fact (op="subscribe", resourceKind="event") without resolving the publisher
9
+ // routine — that happens in Task 20 (event-dispatch composition).
10
+ //
11
+ // PUBLISH side: routines with [IntegrationEvent] / [BusinessEvent] /
12
+ // [InternalEvent] attributes emit NO fact here. publish facts arise on the
13
+ // CALLER of a publisher, via event-dispatch edges (Task 20) + capability
14
+ // composition (Task 22).
15
+ //
16
+ // Never throws.
17
+
18
+ import { findAttribute } from "../../model/attributes.ts";
19
+ import type { CapabilityFact, EventExtra } from "../../model/capability.ts";
20
+ import type { CoverageReason } from "../../model/coverage.ts";
21
+ import type { ExtractionContext } from "./extractor.ts";
22
+
23
+ /**
24
+ * Phase 0b-β events family extractor.
25
+ *
26
+ * Emits one `subscribe` CapabilityFact for routines with an
27
+ * [EventSubscriber(...)] attribute. Publisher-decorated routines
28
+ * ([IntegrationEvent] / [BusinessEvent] / [InternalEvent]) emit nothing —
29
+ * their publish facts arise from callers via event-dispatch edges (Task 20).
30
+ *
31
+ * Never throws.
32
+ */
33
+ export function extractEvents(ctx: ExtractionContext): {
34
+ facts: CapabilityFact[];
35
+ reasons: CoverageReason[];
36
+ } {
37
+ try {
38
+ const attrs = ctx.routine?.attributesParsed ?? [];
39
+
40
+ if (findAttribute(attrs, "EventSubscriber") === undefined) {
41
+ return { facts: [], reasons: [] };
42
+ }
43
+
44
+ // TODO Phase 0b-β.5: detect Business/Internal/Trigger via publisher's
45
+ // attribute lookup rather than defaulting to "Integration".
46
+ const extra: EventExtra = {
47
+ kind: "event",
48
+ eventClass: "Integration",
49
+ };
50
+
51
+ const fact: CapabilityFact = {
52
+ subject: ctx.routine.id,
53
+ op: "subscribe",
54
+ resourceKind: "event",
55
+ confidence: "static",
56
+ provenance: "direct",
57
+ via: "self",
58
+ extra,
59
+ };
60
+
61
+ return { facts: [fact], reasons: [] };
62
+ } catch {
63
+ return { facts: [], reasons: ["extraction-failed"] };
64
+ }
65
+ }
@@ -0,0 +1,124 @@
1
+ import type { CapabilityFact } from "../../model/capability.ts";
2
+ import type { CoverageReason, CoverageStatus } from "../../model/coverage.ts";
3
+ import type { Routine, VariableSymbol } from "../../model/entities.ts";
4
+ import type { Diagnostic } from "../../model/finding.ts";
5
+ import { extractBackground } from "./background.ts";
6
+ import { extractCommit } from "./commit.ts";
7
+ import { extractDispatch } from "./dispatch.ts";
8
+ import { extractEvents } from "./events.ts";
9
+ import { extractFileBlob } from "./file-blob.ts";
10
+ import { extractHttp } from "./http.ts";
11
+ import { extractHyperlink } from "./hyperlink.ts";
12
+ import { extractIsolatedStorage } from "./isolated-storage.ts";
13
+ import { extractTable } from "./table.ts";
14
+ import { extractTelemetry } from "./telemetry.ts";
15
+ import { extractUi } from "./ui.ts";
16
+
17
+ /**
18
+ * Context passed to capability extractors. Extractors emit diagnostics
19
+ * and coverage gaps via the sinks here — they MUST NEVER throw
20
+ * (engine-never-throws contract per CLAUDE.md, formalized for
21
+ * extractors in spec §3.10).
22
+ *
23
+ * Phase 0a defines the shape; Phase 0b populates `variables` and routes
24
+ * to family extractors. Phase 0a's orchestrator is a no-op shell.
25
+ */
26
+ export interface ExtractionContext {
27
+ routine: Routine;
28
+ /** Per-name lookup over routine.features.variables (lowercased keys).
29
+ * Built once by the orchestrator before dispatching to family extractors. */
30
+ variables: Map<string, VariableSymbol>;
31
+ /** Resolves a member-call's receiver name to its declared type.
32
+ * Returns "unknown" when the variable isn't in the index. */
33
+ receiverTypeOf(receiverName: string): string;
34
+ reportDiagnostic(d: Diagnostic): void;
35
+ reportCoverageGap(reason: CoverageReason, target?: string): void;
36
+ }
37
+
38
+ /**
39
+ * What an extractor (orchestrator or family) returns.
40
+ * - facts — the capability facts the extractor was able to recognize
41
+ * - status — "complete" = ran cleanly; "partial" = some nodes skipped
42
+ * due to grammar/parse gaps (diagnostics emitted);
43
+ * "unknown" = extractor itself errored on input shape it
44
+ * didn't recognize. Phase 0a shell returns "unknown".
45
+ * - reasons — coverage reasons contributing to non-"complete" status.
46
+ * Phase 0a shell returns ["extraction-failed"] — the
47
+ * shell hasn't extracted anything, so callers must treat
48
+ * the result as "we don't know".
49
+ *
50
+ * Extractors return `facts: []` only when the absence is KNOWN (no calls
51
+ * of the relevant family in the body). Phase 0a's shell returns
52
+ * `facts: []` because no extraction logic exists yet; the "unknown"
53
+ * status tells callers not to confuse this with "known absent".
54
+ */
55
+ export interface CapabilityExtractionResult {
56
+ facts: CapabilityFact[];
57
+ status: CoverageStatus;
58
+ reasons: CoverageReason[];
59
+ }
60
+
61
+ /**
62
+ * Phase 0b-β orchestrator. Dispatches across all 11 family extractors,
63
+ * aggregates facts + reasons, rolls up overall status.
64
+ *
65
+ * Status roll-up:
66
+ * - "complete" when no family reported reasons
67
+ * - "partial" when some families reported coverage reasons
68
+ * - "unknown" when the orchestrator itself threw (caught by outer try/catch)
69
+ *
70
+ * The orchestrator always rebuilds ctx.variables from routine.features.variables
71
+ * internally — callers pass any ctx (even a partially-constructed one); the
72
+ * orchestrator uses its own variable index for family dispatch. This ensures
73
+ * extraction is reproducible regardless of caller setup.
74
+ *
75
+ * Engine-never-throws contract per CLAUDE.md.
76
+ */
77
+ export function extractCapabilities(
78
+ routine: Routine,
79
+ ctx: ExtractionContext,
80
+ ): CapabilityExtractionResult {
81
+ try {
82
+ // Rebuild variables Map from routine.features (caller's may be stale).
83
+ const variables = new Map<string, VariableSymbol>();
84
+ for (const v of routine?.features?.variables ?? []) {
85
+ variables.set(v.name, v);
86
+ }
87
+ const dispatchCtx: ExtractionContext = {
88
+ routine,
89
+ variables,
90
+ receiverTypeOf: (name) => variables.get(name.toLowerCase())?.declaredType ?? "unknown",
91
+ reportDiagnostic: ctx.reportDiagnostic,
92
+ reportCoverageGap: ctx.reportCoverageGap,
93
+ };
94
+
95
+ const allFacts: CapabilityFact[] = [];
96
+ const allReasons: CoverageReason[] = [];
97
+
98
+ for (const extractor of [
99
+ extractTable,
100
+ extractCommit,
101
+ extractDispatch,
102
+ extractHttp,
103
+ extractTelemetry,
104
+ extractIsolatedStorage,
105
+ extractHyperlink,
106
+ extractFileBlob,
107
+ extractBackground,
108
+ extractUi,
109
+ extractEvents,
110
+ ]) {
111
+ const { facts, reasons } = extractor(dispatchCtx);
112
+ allFacts.push(...facts);
113
+ allReasons.push(...reasons);
114
+ }
115
+
116
+ const status: CoverageStatus = allReasons.length === 0 ? "complete" : "partial";
117
+ // Dedupe + sort reasons for determinism.
118
+ const dedupedReasons = Array.from(new Set(allReasons)).sort();
119
+
120
+ return { facts: allFacts, status, reasons: dedupedReasons };
121
+ } catch {
122
+ return { facts: [], status: "unknown", reasons: ["extraction-failed"] };
123
+ }
124
+ }
@@ -0,0 +1,137 @@
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
+ // Probe result: F.Create / F.WriteAllText / TB.CreateOutStream all land in
7
+ // callSites as member callees. F.Copy lands in recordOperations (op="Copy")
8
+ // because RECORD_OP_MAP matches "copy" regardless of receiver type.
9
+
10
+ /** Write-side File methods found in callSites (member callee). */
11
+ const FILE_CALLSITE_METHODS = new Set(["create", "writealltext"]);
12
+
13
+ /** Write-side TempBlob methods found in callSites (member callee). */
14
+ const TEMPBLOB_CALLSITE_METHODS = new Set(["createoutstream"]);
15
+
16
+ /** Write-side ops found in recordOperations for File-typed receivers. */
17
+ const FILE_RECORD_OPS = new Set(["copy"]);
18
+
19
+ /**
20
+ * Phase 0b-β file-blob family extractor. Detects write-side method calls on
21
+ * variables typed `File` or `Codeunit "Temp Blob"`.
22
+ *
23
+ * Maps to op="write-blob", resourceKind="file". Receiver type resolved via
24
+ * ctx.receiverTypeOf, keyed against the normalised declared-type strings:
25
+ * - File variables → "File"
26
+ * - TempBlob variables → 'Codeunit "Temp Blob"' (quotes preserved)
27
+ *
28
+ * Probed split: callSites handle Create / WriteAllText / CreateOutStream;
29
+ * recordOperations handles Copy (RECORD_OP_MAP picks it up by name
30
+ * independently of receiver type — the File-type guard filters it here).
31
+ *
32
+ * The first positional arg, when present (filename/path), is classified as a
33
+ * ValueSource and exposed via resourceArgSource. Methods with no filename arg
34
+ * (CreateOutStream, Copy-via-recordOp which carries no fieldArgumentInfos)
35
+ * receive { kind: "unknown" } → confidence "unresolved".
36
+ *
37
+ * Read-side methods (.CreateInStream, etc.) are deliberately out of scope.
38
+ *
39
+ * Never throws.
40
+ */
41
+ export function extractFileBlob(ctx: ExtractionContext): {
42
+ facts: CapabilityFact[];
43
+ reasons: CoverageReason[];
44
+ } {
45
+ try {
46
+ const facts: CapabilityFact[] = [];
47
+
48
+ // Branch A: callSites — File.Create, File.WriteAllText, TempBlob.CreateOutStream.
49
+ for (const cs of ctx.routine?.features?.callSites ?? []) {
50
+ const callee = cs.callee;
51
+ if (!callee || callee.kind !== "member") continue;
52
+ if (typeof callee.receiver !== "string" || typeof callee.method !== "string") continue;
53
+ const methodLc = callee.method.toLowerCase();
54
+ const receiverType = ctx.receiverTypeOf(callee.receiver);
55
+
56
+ const isFile = receiverType === "File" && FILE_CALLSITE_METHODS.has(methodLc);
57
+ const isTempBlob = isTempBlobType(receiverType) && TEMPBLOB_CALLSITE_METHODS.has(methodLc);
58
+ if (!isFile && !isTempBlob) continue;
59
+
60
+ // First arg is the filename/path (Create, WriteAllText) or an OutStream
61
+ // (CreateOutStream — no filename semantics). Classify either way;
62
+ // non-string args will resolve to "expression"/"unknown" → unresolved.
63
+ const argInfo = cs.argumentInfos?.[0];
64
+ const argSource: ValueSource =
65
+ argInfo !== undefined ? classifyValueSource(argInfo, ctx) : { kind: "unknown" };
66
+
67
+ facts.push({
68
+ subject: ctx.routine.id,
69
+ op: "write-blob",
70
+ resourceKind: "file",
71
+ resourceArgSource: argSource,
72
+ confidence: confidenceFromSource(argSource),
73
+ provenance: "direct",
74
+ via: "self",
75
+ witnessCallsiteId: cs.id,
76
+ });
77
+ }
78
+
79
+ // Branch B: recordOperations — File.Copy.
80
+ // RECORD_OP_MAP recognises "copy" by name regardless of receiver type,
81
+ // so it surfaces here. fieldArgumentInfos is absent for Copy (not in
82
+ // FIELD_ARGS_OPS), so arg source falls back to unknown → unresolved.
83
+ for (const ro of ctx.routine?.features?.recordOperations ?? []) {
84
+ const recv = ro.recordVariableName;
85
+ if (typeof recv !== "string") continue;
86
+ const receiverType = ctx.receiverTypeOf(recv);
87
+ if (receiverType !== "File") continue;
88
+ if (!FILE_RECORD_OPS.has(ro.op.toLowerCase())) continue;
89
+
90
+ const argInfo = ro.fieldArgumentInfos?.[0];
91
+ const argSource: ValueSource =
92
+ argInfo !== undefined ? classifyValueSource(argInfo, ctx) : { kind: "unknown" };
93
+
94
+ facts.push({
95
+ subject: ctx.routine.id,
96
+ op: "write-blob",
97
+ resourceKind: "file",
98
+ resourceArgSource: argSource,
99
+ confidence: confidenceFromSource(argSource),
100
+ provenance: "direct",
101
+ via: "self",
102
+ witnessOperationId: ro.id,
103
+ });
104
+ }
105
+
106
+ return { facts, reasons: [] };
107
+ } catch {
108
+ return { facts: [], reasons: ["extraction-failed"] };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Match the normalised declared-type for TempBlob variables.
114
+ * Probe result: `Codeunit "Temp Blob"` (with double-quotes around the object
115
+ * name, as the variable-symbol normaliser preserves them).
116
+ */
117
+ function isTempBlobType(t: string): boolean {
118
+ const lc = t.toLowerCase();
119
+ return lc.includes("temp blob") || lc === "tempblob";
120
+ }
121
+
122
+ function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
123
+ switch (vs.kind) {
124
+ case "literal":
125
+ case "enum":
126
+ return "static";
127
+ case "constant-var":
128
+ return confidenceFromSource(vs.initializer);
129
+ case "parameter":
130
+ return "userDynamic";
131
+ case "table-field":
132
+ return "configDynamic";
133
+ case "expression":
134
+ case "unknown":
135
+ return "unresolved";
136
+ }
137
+ }
@@ -0,0 +1,159 @@
1
+ import type {
2
+ CapabilityConfidence,
3
+ CapabilityFact,
4
+ HttpExtra,
5
+ ValueSource,
6
+ } from "../../model/capability.ts";
7
+ import type { CoverageReason } from "../../model/coverage.ts";
8
+ import type { CallsiteId, OperationId } from "../../model/ids.ts";
9
+ import type { ExtractionContext } from "./extractor.ts";
10
+ import { classifyValueSource } from "./value-source.ts";
11
+
12
+ const HTTP_METHOD_SET = new Set<HttpExtra["method"]>([
13
+ "Send",
14
+ "Get",
15
+ "Post",
16
+ "Put",
17
+ "Delete",
18
+ "Patch",
19
+ ]);
20
+
21
+ function isHttpMethod(v: string): v is HttpExtra["method"] {
22
+ return HTTP_METHOD_SET.has(v as HttpExtra["method"]);
23
+ }
24
+
25
+ function buildHttpFact(
26
+ ctx: ExtractionContext,
27
+ method: HttpExtra["method"],
28
+ urlSource: ValueSource,
29
+ bodyArgSource: ValueSource | undefined,
30
+ witness: { kind: "operation"; id: OperationId } | { kind: "callsite"; id: CallsiteId },
31
+ ): CapabilityFact {
32
+ const extra: HttpExtra = {
33
+ kind: "http",
34
+ method,
35
+ ...(bodyArgSource !== undefined ? { bodyArgSource } : {}),
36
+ };
37
+ const witnessFields =
38
+ witness.kind === "operation"
39
+ ? { witnessOperationId: witness.id }
40
+ : { witnessCallsiteId: witness.id };
41
+ return {
42
+ subject: ctx.routine.id,
43
+ op: "send",
44
+ resourceKind: "http",
45
+ resourceArgSource: urlSource,
46
+ confidence: confidenceFromSource(urlSource),
47
+ provenance: "direct",
48
+ via: "self",
49
+ ...witnessFields,
50
+ extra,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Phase 0b-β http family extractor. Scans both `recordOperations` and
56
+ * `callSites` for HttpClient method calls.
57
+ *
58
+ * Why both? `intraprocedural-ops.ts` recognizes any `Receiver.Get(...)` /
59
+ * `.Delete(...)` as a RecordOperation regardless of receiver type — so HTTP
60
+ * `.Get` and `.Delete` land in `recordOperations`. The other methods (`.Post`,
61
+ * `.Put`, `.Patch`, `.Send`) surface as member callsites. The receiver-type
62
+ * guard via `ctx.receiverTypeOf(recv) === "HttpClient"` identifies which calls
63
+ * are actually HTTP.
64
+ *
65
+ * URL ValueSource (1st arg for url-taking methods) drives confidence:
66
+ * literal/enum → static, parameter → userDynamic, table-field → configDynamic,
67
+ * expression/unknown → unresolved, constant-var → recurse on initializer.
68
+ *
69
+ * Body ValueSource (2nd arg for url-taking methods, 1st arg for `.Send`)
70
+ * captured in HttpExtra.bodyArgSource for Phase 6 taint propagation.
71
+ *
72
+ * `.Delete` has no fieldArgumentInfos (not in FIELD_ARGS_OPS in
73
+ * intraprocedural-ops.ts) — urlSource falls back to `{ kind: "unknown" }` and
74
+ * confidence to "unresolved". This is intentional: Delete takes the key-filter
75
+ * state already set on the record, not a URL argument.
76
+ *
77
+ * Never throws.
78
+ */
79
+ export function extractHttp(ctx: ExtractionContext): {
80
+ facts: CapabilityFact[];
81
+ reasons: CoverageReason[];
82
+ } {
83
+ try {
84
+ const facts: CapabilityFact[] = [];
85
+
86
+ // Branch A: HttpClient.Get / .Delete from recordOperations.
87
+ // `intraprocedural-ops.ts` maps `.get` and `.delete` via RECORD_OP_MAP
88
+ // regardless of the receiver's declared type — we filter here.
89
+ for (const ro of ctx.routine?.features?.recordOperations ?? []) {
90
+ const recv = ro.recordVariableName;
91
+ if (typeof recv !== "string") continue;
92
+ if (ctx.receiverTypeOf(recv) !== "HttpClient") continue;
93
+ const method = ro.op;
94
+ if (!isHttpMethod(method)) continue;
95
+
96
+ // Get captures fieldArgumentInfos (in FIELD_ARGS_OPS); Delete does not.
97
+ const urlInfo = ro.fieldArgumentInfos?.[0];
98
+ const urlSource: ValueSource =
99
+ urlInfo !== undefined ? classifyValueSource(urlInfo, ctx) : { kind: "unknown" };
100
+
101
+ const bodyInfo = ro.fieldArgumentInfos?.[1];
102
+ const bodyArgSource: ValueSource | undefined =
103
+ bodyInfo !== undefined ? classifyValueSource(bodyInfo, ctx) : undefined;
104
+
105
+ facts.push(
106
+ buildHttpFact(ctx, method, urlSource, bodyArgSource, { kind: "operation", id: ro.id }),
107
+ );
108
+ }
109
+
110
+ // Branch B: HttpClient.Post / .Put / .Patch / .Send from callSites.
111
+ // These are member callsites (callee.kind === "member") not caught by
112
+ // RECORD_OP_MAP. Callee fields: `receiver` (receiver expr text) and
113
+ // `method` (method name). Both confirmed from the Callee model type.
114
+ for (const cs of ctx.routine?.features?.callSites ?? []) {
115
+ const callee = cs.callee;
116
+ if (!callee || callee.kind !== "member") continue;
117
+ const recv = callee.receiver;
118
+ const member = callee.method;
119
+ if (ctx.receiverTypeOf(recv) !== "HttpClient") continue;
120
+ if (!isHttpMethod(member)) continue;
121
+
122
+ // .Send(Request, Response) — no URL arg; first arg is the request body.
123
+ // .Post/.Put/.Patch(Url, Request, Response) — first arg is URL, second is body.
124
+ const isSend = member === "Send";
125
+ const urlInfo = isSend ? undefined : cs.argumentInfos?.[0];
126
+ const bodyInfo = isSend ? cs.argumentInfos?.[0] : cs.argumentInfos?.[1];
127
+
128
+ const urlSource: ValueSource =
129
+ urlInfo !== undefined ? classifyValueSource(urlInfo, ctx) : { kind: "unknown" };
130
+ const bodyArgSource: ValueSource | undefined =
131
+ bodyInfo !== undefined ? classifyValueSource(bodyInfo, ctx) : undefined;
132
+
133
+ facts.push(
134
+ buildHttpFact(ctx, member, urlSource, bodyArgSource, { kind: "callsite", id: cs.id }),
135
+ );
136
+ }
137
+
138
+ return { facts, reasons: [] };
139
+ } catch {
140
+ return { facts: [], reasons: ["extraction-failed"] };
141
+ }
142
+ }
143
+
144
+ function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
145
+ switch (vs.kind) {
146
+ case "literal":
147
+ case "enum":
148
+ return "static";
149
+ case "constant-var":
150
+ return confidenceFromSource(vs.initializer);
151
+ case "parameter":
152
+ return "userDynamic";
153
+ case "table-field":
154
+ return "configDynamic";
155
+ case "expression":
156
+ case "unknown":
157
+ return "unresolved";
158
+ }
159
+ }