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,176 @@
1
+ import type { CombinedGraph } from "../engine/combined-graph.ts";
2
+ import { compareNatural, compareStrings } from "../engine/uncertainty-util.ts";
3
+ import { roleOf } from "../model/entities.ts";
4
+ import type { DetectorStats, Diagnostic, Finding } from "../model/finding.ts";
5
+ import type { SemanticModel } from "../model/model.ts";
6
+ import { profilingActive, recordPhase } from "../perf/profiler.ts";
7
+ import { detectD1 } from "./d1-db-op-in-loop.ts";
8
+ import { detectD2 } from "./d2-event-fanout-in-loop.ts";
9
+ import { detectD3 } from "./d3-missing-setloadfields.ts";
10
+ import { detectD4 } from "./d4-repeated-lookup-in-loop.ts";
11
+ import { detectD5 } from "./d5-set-based-opportunity.ts";
12
+ import { detectD7 } from "./d7-recursive-event-expansion.ts";
13
+ import { detectD8 } from "./d8-commit-in-transaction.ts";
14
+ import { detectD9 } from "./d9-transaction-span-summary.ts";
15
+ import { detectD10 } from "./d10-self-modifying-loop.ts";
16
+ import { detectD11 } from "./d11-modify-without-get.ts";
17
+ import { detectD12 } from "./d12-dead-integration-event.ts";
18
+ import { detectD13 } from "./d13-cross-app-internal-call.ts";
19
+ import { detectD14 } from "./d14-dead-routine.ts";
20
+ import { detectD16 } from "./d16-obsolete-routine-call.ts";
21
+ import { detectD17 } from "./d17-min-version-drift.ts";
22
+ import { detectD18 } from "./d18-constant-filter-in-loop.ts";
23
+ import { detectD19 } from "./d19-unused-parameter.ts";
24
+ import { detectD20 } from "./d20-unreachable-after-exit.ts";
25
+ import { detectD21 } from "./d21-read-without-load.ts";
26
+ import { detectD22 } from "./d22-flowfield-without-calcfields.ts";
27
+ import { detectD29 } from "./d29-subscriber-modify-on-event-record.ts";
28
+ import { detectD32 } from "./d32-constant-boolean-parameter.ts";
29
+ import { detectD33 } from "./d33-unfiltered-bulk-write.ts";
30
+ import { detectD34 } from "./d34-commit-in-loop.ts";
31
+ import { detectD35 } from "./d35-commit-in-event-subscriber.ts";
32
+ import { detectD36 } from "./d36-late-setloadfields.ts";
33
+ import { detectD37 } from "./d37-validate-without-persist.ts";
34
+ import { detectD38 } from "./d38-subscriber-to-obsolete-event.ts";
35
+ import { detectD39 } from "./d39-record-left-dirty-across-chain.ts";
36
+ // D40 is intentionally NOT in DEFAULT_DETECTORS — opt-in only via
37
+ // `--detector d40-transitive-load-missing`. See the d40 module's JSDoc for the rationale
38
+ // (Phase 4 straight-line walker produces a high false-positive rate dominated by the
39
+ // loop-loaded record class; Phase 6's full walker closes it).
40
+ import { detectD40 } from "./d40-transitive-load-missing.ts";
41
+ import { detectD41 } from "./d41-transitive-filter-loss.ts";
42
+ import { detectD42 } from "./d42-cross-call-wrong-setloadfields.ts";
43
+ import { detectD43 } from "./d43-event-ishandled-skip.ts";
44
+ import { detectD44 } from "./d44-event-multi-subscriber-overlap.ts";
45
+ import { detectD45 } from "./d45-event-transitive-table-exposure.ts";
46
+ import type { DetectorContext } from "./detector-context.ts";
47
+ import { buildDetectorContext } from "./detector-context.ts";
48
+
49
+ /** A detector: a pure query over the summarised model + combined graph. */
50
+ export interface Detector {
51
+ name: string;
52
+ run(
53
+ model: SemanticModel,
54
+ graph: CombinedGraph,
55
+ ctx: DetectorContext,
56
+ ): { findings: Finding[]; stats: DetectorStats };
57
+ }
58
+
59
+ /** The default detector registry. */
60
+ export const DEFAULT_DETECTORS: Detector[] = [
61
+ { name: "d1-db-op-in-loop", run: detectD1 },
62
+ { name: "d2-event-fanout-in-loop", run: detectD2 },
63
+ { name: "d3-missing-setloadfields", run: detectD3 },
64
+ { name: "d4-repeated-lookup-in-loop", run: detectD4 },
65
+ { name: "d5-set-based-opportunity", run: detectD5 },
66
+ { name: "d7-recursive-event-expansion", run: detectD7 },
67
+ { name: "d8-commit-in-transaction", run: detectD8 },
68
+ { name: "d9-transaction-span-summary", run: detectD9 },
69
+ { name: "d10-self-modifying-loop", run: detectD10 },
70
+ { name: "d11-modify-without-get", run: detectD11 },
71
+ { name: "d12-dead-integration-event", run: detectD12 },
72
+ { name: "d13-cross-app-internal-call", run: detectD13 },
73
+ { name: "d14-dead-routine", run: detectD14 },
74
+ { name: "d16-obsolete-routine-call", run: detectD16 },
75
+ { name: "d17-min-version-drift", run: detectD17 },
76
+ { name: "d18-constant-filter-in-loop", run: detectD18 },
77
+ { name: "d19-unused-parameter", run: detectD19 },
78
+ { name: "d20-unreachable-after-exit", run: detectD20 },
79
+ { name: "d21-read-without-load", run: detectD21 },
80
+ { name: "d22-flowfield-without-calcfields", run: detectD22 },
81
+ { name: "d29-subscriber-modify-on-event-record", run: detectD29 },
82
+ { name: "d32-constant-boolean-parameter", run: detectD32 },
83
+ { name: "d33-unfiltered-bulk-write", run: detectD33 },
84
+ { name: "d34-commit-in-loop", run: detectD34 },
85
+ { name: "d35-commit-in-event-subscriber", run: detectD35 },
86
+ { name: "d36-late-setloadfields", run: detectD36 },
87
+ { name: "d37-validate-without-persist", run: detectD37 },
88
+ { name: "d38-subscriber-to-obsolete-event", run: detectD38 },
89
+ { name: "d39-record-left-dirty-across-chain", run: detectD39 },
90
+ { name: "d41-transitive-filter-loss", run: detectD41 },
91
+ { name: "d42-cross-call-wrong-setloadfields", run: detectD42 },
92
+ { name: "d43-event-ishandled-skip", run: detectD43 },
93
+ { name: "d44-event-multi-subscriber-overlap", run: detectD44 },
94
+ { name: "d45-event-transitive-table-exposure", run: detectD45 },
95
+ ];
96
+
97
+ /**
98
+ * Opt-in detectors — not in the default registry. Surfaced by `--detector <name>`
99
+ * (CLI) and `analyzeWorkspace({ detectors: [...DEFAULT_DETECTORS, ...OPT_IN_DETECTORS] })`
100
+ * (library) for callers that explicitly want them.
101
+ *
102
+ * Currently: D40 (transitive-load-missing) — Phase 6 graduates it back to the default
103
+ * registry once the full statement-tree walker eliminates the loop-loaded FP class.
104
+ */
105
+ export const OPT_IN_DETECTORS: Detector[] = [
106
+ { name: "d40-transitive-load-missing", run: detectD40 },
107
+ ];
108
+
109
+ /** All known detectors — default + opt-in. Useful for CLI surface enumeration. */
110
+ export const ALL_DETECTORS: Detector[] = [...DEFAULT_DETECTORS, ...OPT_IN_DETECTORS];
111
+
112
+ function primaryLocationKey(f: Finding): string {
113
+ const a = f.primaryLocation;
114
+ return `${a.sourceUnitId}:${a.range.startLine}:${a.range.startColumn}`;
115
+ }
116
+
117
+ /**
118
+ * Run every detector in isolation. A detector that throws does not kill the run — it becomes
119
+ * a `Diagnostic(stage: "detect")` and the rest still run. The combined Finding[] is sorted
120
+ * by (detector, primaryLocationKey, rootCauseKey) for deterministic output.
121
+ */
122
+ export function runDetectors(
123
+ model: SemanticModel,
124
+ graph: CombinedGraph,
125
+ detectors: Detector[] = DEFAULT_DETECTORS,
126
+ ): { findings: Finding[]; diagnostics: Diagnostic[]; detectorStats: DetectorStats[] } {
127
+ const findings: Finding[] = [];
128
+ const diagnostics: Diagnostic[] = [];
129
+ const detectorStats: DetectorStats[] = [];
130
+ const _tCtx = profilingActive() ? performance.now() : 0;
131
+ const ctx = buildDetectorContext(model, graph);
132
+ if (profilingActive()) recordPhase("detect:buildDetectorContext", performance.now() - _tCtx);
133
+ for (const detector of detectors) {
134
+ const _td = profilingActive() ? performance.now() : 0;
135
+ try {
136
+ const result = detector.run(model, graph, ctx);
137
+ findings.push(...result.findings);
138
+ detectorStats.push(result.stats);
139
+ if (profilingActive()) recordPhase(`detect:${detector.name}`, performance.now() - _td);
140
+ } catch (err) {
141
+ diagnostics.push({
142
+ severity: "warning",
143
+ stage: "detect",
144
+ message: `Detector "${detector.name}" threw: ${err instanceof Error ? err.message : String(err)}`,
145
+ });
146
+ }
147
+ }
148
+ diagnostics.push(...ctx.diagnostics);
149
+ const roleByRoutineId = new Map(model.routines.map((r) => [r.id, roleOf(r)]));
150
+ const scoped = findings.filter((f) => {
151
+ const primaryRole = roleByRoutineId.get(f.primaryLocation.enclosingRoutineId) ?? "primary";
152
+ if (primaryRole === "primary") return true;
153
+ // Detector flagged a primary-app actionable anchor (e.g. D1 whose terminal DB op
154
+ // is deep in a dependency, but the user's loop is in primary code).
155
+ if (f.actionableAnchor !== undefined) {
156
+ const anchorRole = roleByRoutineId.get(f.actionableAnchor.enclosingRoutineId) ?? "primary";
157
+ if (anchorRole === "primary") return true;
158
+ }
159
+ return false;
160
+ });
161
+ scoped.sort((a, b) => {
162
+ if (a.detector !== b.detector) return compareNatural(a.detector, b.detector);
163
+ const la = primaryLocationKey(a);
164
+ const lb = primaryLocationKey(b);
165
+ if (la !== lb) return compareStrings(la, lb);
166
+ return compareStrings(a.rootCauseKey, b.rootCauseKey);
167
+ });
168
+ if (process.env.AL_SEM_DETECTOR_STATS === "1") {
169
+ for (const s of detectorStats) {
170
+ process.stderr.write(
171
+ `[detector-stats] ${s.detector}: candidates=${s.candidatesConsidered} emitted=${s.findingsEmitted} skipped=${JSON.stringify(s.skipped)}\n`,
172
+ );
173
+ }
174
+ }
175
+ return { findings: scoped, diagnostics, detectorStats };
176
+ }
@@ -0,0 +1,42 @@
1
+ import type { Routine, Table } from "../model/entities.ts";
2
+ import type { TableId } from "../model/ids.ts";
3
+
4
+ /**
5
+ * Render a record-operation's target table for human consumption.
6
+ *
7
+ * Three tiers, in order of preference:
8
+ * 1. `op.tableId` resolves in `tableById` → return the table's NAME (e.g.
9
+ * `"Customer"`, `"CDC Template Field"`). This is what the user sees in
10
+ * their IDE.
11
+ * 2. `op.tableId` is undefined or unresolved AND the receiving record-variable
12
+ * has a declared `tableName` text → return that text, suffixed with
13
+ * `(type not loaded)`. This is the common case when the type lives in a
14
+ * dependency we couldn't load (e.g. opaque Continia deps, AL-language's
15
+ * `.dependencies/` cache that al-sem doesn't crawl). The user still gets
16
+ * the type they wrote in source.
17
+ * 3. Variable name itself, prefixed with `var `, as a last-resort identity
18
+ * (e.g. `var DocGroup`).
19
+ * 4. The string `"unknown table"`.
20
+ *
21
+ * Replaces the previous behavior of rendering the internal table id verbatim
22
+ * (e.g. `"table 6225286"` or
23
+ * `"f4b69b55-c90d-4937-8f53-2742898fa948/table/6175301"`) — useless to humans
24
+ * and the dominant UX paper-cut surfaced by the DC/Cloud precision study.
25
+ */
26
+ export function describeTable(
27
+ op: { tableId?: TableId; recordVariableName: string },
28
+ routine: Routine | undefined,
29
+ tableById: Map<TableId, Table>,
30
+ ): string {
31
+ if (op.tableId !== undefined) {
32
+ const table = tableById.get(op.tableId);
33
+ if (table) return table.name;
34
+ }
35
+ if (routine !== undefined) {
36
+ const lc = op.recordVariableName.toLowerCase();
37
+ const rv = routine.features.recordVariables.find((v) => v.name.toLowerCase() === lc);
38
+ if (rv?.tableName && rv.tableName !== "") return `${rv.tableName} (type not loaded)`;
39
+ }
40
+ if (op.recordVariableName !== "") return `var ${op.recordVariableName}`;
41
+ return "unknown table";
42
+ }
@@ -0,0 +1,195 @@
1
+ import type { ContractFact } from "../snapshot/types.ts";
2
+ import { DiffCategory, DiffKind, computeDiffFingerprint } from "./diff-identity.ts";
3
+ import type { DiffIndexes } from "./diff-indexes.ts";
4
+
5
+ export interface DiffEngineOptions {
6
+ coveragePolicy: "loose" | "strict";
7
+ }
8
+
9
+ export interface AbiDetails {
10
+ kind:
11
+ | DiffKind.ObjectRemoved
12
+ | DiffKind.ObjectAccessibilityNarrowed
13
+ | DiffKind.ProcedureRemoved
14
+ | DiffKind.ProcedureSignatureChanged
15
+ | DiffKind.ProcedureVarDirectionChanged
16
+ | DiffKind.ProcedureObsoletionRegressed
17
+ | DiffKind.ProcedureObsoletionProgressed
18
+ | DiffKind.ObjectAdded
19
+ | DiffKind.ProcedureAdded;
20
+ oldAccessibility?: string;
21
+ newAccessibility?: string;
22
+ oldObsoleteState?: string;
23
+ newObsoleteState?: string;
24
+ oldSignatureHash?: string;
25
+ newSignatureHash?: string;
26
+ }
27
+
28
+ export interface AbiFinding {
29
+ id: string;
30
+ category: DiffCategory.ABI;
31
+ kind: DiffKind;
32
+ severity: "critical" | "high" | "medium" | "low" | "info";
33
+ subject: {
34
+ normalizedStableId: string;
35
+ oldOriginalStableId?: string;
36
+ newStableId?: string;
37
+ displayName: string;
38
+ };
39
+ comparisonCone: readonly string[];
40
+ details: AbiDetails;
41
+ }
42
+
43
+ const SEVERITY: Partial<Record<DiffKind, AbiFinding["severity"]>> = {
44
+ [DiffKind.ObjectRemoved]: "critical",
45
+ [DiffKind.ObjectAccessibilityNarrowed]: "critical",
46
+ [DiffKind.ProcedureRemoved]: "critical",
47
+ [DiffKind.ProcedureSignatureChanged]: "critical",
48
+ [DiffKind.ProcedureVarDirectionChanged]: "critical",
49
+ [DiffKind.ProcedureObsoletionRegressed]: "high",
50
+ [DiffKind.ProcedureObsoletionProgressed]: "info",
51
+ [DiffKind.ObjectAdded]: "info",
52
+ [DiffKind.ProcedureAdded]: "info",
53
+ };
54
+
55
+ /**
56
+ * Visibility lattice order: local < internal < protected < public.
57
+ * Narrowing = moving to a less visible level (higher index → lower index).
58
+ */
59
+ const VISIBILITY_ORDER = ["local", "internal", "protected", "public"];
60
+
61
+ function isNarrowed(oldVisibility: string | undefined, newVisibility: string | undefined): boolean {
62
+ const o = VISIBILITY_ORDER.indexOf(oldVisibility ?? "");
63
+ const n = VISIBILITY_ORDER.indexOf(newVisibility ?? "");
64
+ return o > n && n >= 0;
65
+ }
66
+
67
+ /**
68
+ * Returns true when the contract fact's kind represents a procedure-like
69
+ * surface (routine, event-publisher) rather than a plain object or interface.
70
+ */
71
+ function isProcedureLike(fact: ContractFact): boolean {
72
+ return fact.kind === "routine" || fact.kind === "event-publisher";
73
+ }
74
+
75
+ function makeFinding(
76
+ kind: AbiDetails["kind"],
77
+ subjectId: string,
78
+ indexes: DiffIndexes,
79
+ details: AbiDetails,
80
+ ): AbiFinding {
81
+ const origin = indexes.originByNormalized.get(subjectId);
82
+ const display =
83
+ indexes.newDisplayByStableId.get(subjectId) ??
84
+ indexes.oldDisplayByStableId.get(subjectId) ??
85
+ subjectId;
86
+ return {
87
+ id: computeDiffFingerprint({
88
+ category: DiffCategory.ABI,
89
+ kind,
90
+ normalizedStableId: subjectId,
91
+ }),
92
+ category: DiffCategory.ABI,
93
+ kind,
94
+ severity: SEVERITY[kind] ?? "medium",
95
+ subject: {
96
+ normalizedStableId: subjectId,
97
+ oldOriginalStableId: origin?.oldOriginalStableId,
98
+ newStableId: origin?.newStableId,
99
+ displayName: display,
100
+ },
101
+ comparisonCone: [subjectId],
102
+ details,
103
+ };
104
+ }
105
+
106
+ export function diffAbi(
107
+ _oldSnap: unknown,
108
+ _newSnap: unknown,
109
+ indexes: DiffIndexes,
110
+ _opts: DiffEngineOptions,
111
+ ): AbiFinding[] {
112
+ const out: AbiFinding[] = [];
113
+
114
+ // Walk OLD facts — detect removals and in-place changes.
115
+ for (const [subject, oldFact] of indexes.oldContractsBySubject) {
116
+ const newFact = indexes.newContractsBySubject.get(subject);
117
+ if (newFact === undefined) {
118
+ // Removal — distinguish object vs procedure-like.
119
+ const removalKind = isProcedureLike(oldFact)
120
+ ? DiffKind.ProcedureRemoved
121
+ : DiffKind.ObjectRemoved;
122
+ out.push(
123
+ makeFinding(removalKind as AbiDetails["kind"], subject, indexes, {
124
+ kind: removalKind as AbiDetails["kind"],
125
+ }),
126
+ );
127
+ continue;
128
+ }
129
+
130
+ // Compare in place — visibility, obsoletion, signature.
131
+ const oldVisibility = oldFact.visibility;
132
+ const newVisibility = newFact.visibility;
133
+ if (isNarrowed(oldVisibility, newVisibility)) {
134
+ out.push(
135
+ makeFinding(DiffKind.ObjectAccessibilityNarrowed, subject, indexes, {
136
+ kind: DiffKind.ObjectAccessibilityNarrowed,
137
+ oldAccessibility: oldVisibility,
138
+ newAccessibility: newVisibility,
139
+ }),
140
+ );
141
+ }
142
+
143
+ // Obsoletion transition: flat `obsoleteState` field ("Pending" | "Removed").
144
+ const oldObs = oldFact.obsoleteState;
145
+ const newObs = newFact.obsoleteState;
146
+ if (oldObs !== newObs) {
147
+ const order: Record<string, number> = { Pending: 1, Removed: 2 };
148
+ const oldRank = oldObs !== undefined ? (order[oldObs] ?? 0) : 0;
149
+ const newRank = newObs !== undefined ? (order[newObs] ?? 0) : 0;
150
+ if (newRank > oldRank) {
151
+ out.push(
152
+ makeFinding(DiffKind.ProcedureObsoletionProgressed, subject, indexes, {
153
+ kind: DiffKind.ProcedureObsoletionProgressed,
154
+ oldObsoleteState: oldObs,
155
+ newObsoleteState: newObs,
156
+ }),
157
+ );
158
+ } else if (newRank < oldRank) {
159
+ out.push(
160
+ makeFinding(DiffKind.ProcedureObsoletionRegressed, subject, indexes, {
161
+ kind: DiffKind.ProcedureObsoletionRegressed,
162
+ oldObsoleteState: oldObs,
163
+ newObsoleteState: newObs,
164
+ }),
165
+ );
166
+ }
167
+ }
168
+
169
+ // Signature fingerprint change (routines and event-publishers carry this).
170
+ const oldSig = oldFact.signatureFingerprint;
171
+ const newSig = newFact.signatureFingerprint;
172
+ if (oldSig !== "" && newSig !== "" && oldSig !== newSig) {
173
+ out.push(
174
+ makeFinding(DiffKind.ProcedureSignatureChanged, subject, indexes, {
175
+ kind: DiffKind.ProcedureSignatureChanged,
176
+ oldSignatureHash: oldSig,
177
+ newSignatureHash: newSig,
178
+ }),
179
+ );
180
+ }
181
+ }
182
+
183
+ // Walk NEW facts to find additions.
184
+ for (const [subject, newFact] of indexes.newContractsBySubject) {
185
+ if (indexes.oldContractsBySubject.has(subject)) continue;
186
+ const addKind = isProcedureLike(newFact) ? DiffKind.ProcedureAdded : DiffKind.ObjectAdded;
187
+ out.push(
188
+ makeFinding(addKind as AbiDetails["kind"], subject, indexes, {
189
+ kind: addKind as AbiDetails["kind"],
190
+ }),
191
+ );
192
+ }
193
+
194
+ return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
195
+ }
@@ -0,0 +1,179 @@
1
+ import type { CapabilityFact, CapabilityResourceKind } from "../model/capability.ts";
2
+ import type { DiffEngineOptions } from "./diff-abi.ts";
3
+ import { DiffCategory, DiffKind, computeDiffFingerprint } from "./diff-identity.ts";
4
+ import type { DiffIndexes } from "./diff-indexes.ts";
5
+
6
+ export interface CapabilityDetails {
7
+ kind:
8
+ | DiffKind.CapabilityGainedWrite
9
+ | DiffKind.CapabilityGainedRead
10
+ | DiffKind.CapabilityGainedCommit
11
+ | DiffKind.CapabilityGainedHttp
12
+ | DiffKind.CapabilityGainedTelemetry
13
+ | DiffKind.CapabilityGainedIsolatedStorage
14
+ | DiffKind.CapabilityGainedFile
15
+ | DiffKind.CapabilityGainedDynamicDispatch
16
+ | DiffKind.CapabilityGainedEventPublish
17
+ | DiffKind.CapabilityLost
18
+ | DiffKind.CapabilityLostUnderCoverage;
19
+ resourceKind: string;
20
+ resourceId?: string;
21
+ op: string;
22
+ }
23
+
24
+ export interface CapabilityFinding {
25
+ id: string;
26
+ category: DiffCategory.Capabilities;
27
+ kind: DiffKind;
28
+ severity: "critical" | "high" | "medium" | "low" | "info";
29
+ subject: {
30
+ normalizedStableId: string;
31
+ oldOriginalStableId?: string;
32
+ newStableId?: string;
33
+ displayName: string;
34
+ };
35
+ comparisonCone: readonly string[];
36
+ details: CapabilityDetails;
37
+ }
38
+
39
+ const SEVERITY: Partial<Record<DiffKind, CapabilityFinding["severity"]>> = {
40
+ [DiffKind.CapabilityGainedCommit]: "high",
41
+ [DiffKind.CapabilityGainedDynamicDispatch]: "high",
42
+ [DiffKind.CapabilityGainedWrite]: "medium",
43
+ [DiffKind.CapabilityGainedRead]: "medium",
44
+ [DiffKind.CapabilityGainedHttp]: "medium",
45
+ [DiffKind.CapabilityGainedTelemetry]: "medium",
46
+ [DiffKind.CapabilityGainedIsolatedStorage]: "medium",
47
+ [DiffKind.CapabilityGainedFile]: "medium",
48
+ [DiffKind.CapabilityGainedEventPublish]: "medium",
49
+ [DiffKind.CapabilityLost]: "medium",
50
+ [DiffKind.CapabilityLostUnderCoverage]: "low",
51
+ };
52
+
53
+ function gainedKindFor(fact: CapabilityFact): DiffKind | undefined {
54
+ const kind = fact.resourceKind as CapabilityResourceKind;
55
+ const op = fact.op;
56
+ if (kind === "table") {
57
+ if (op === "read") return DiffKind.CapabilityGainedRead;
58
+ if (op === "insert" || op === "modify" || op === "delete")
59
+ return DiffKind.CapabilityGainedWrite;
60
+ }
61
+ if (kind === "transaction" && op === "commit") return DiffKind.CapabilityGainedCommit;
62
+ if (kind === "http") return DiffKind.CapabilityGainedHttp;
63
+ if (kind === "telemetry") return DiffKind.CapabilityGainedTelemetry;
64
+ if (kind === "isolated-storage") return DiffKind.CapabilityGainedIsolatedStorage;
65
+ if (kind === "file") return DiffKind.CapabilityGainedFile;
66
+ if (kind === "event" && op === "publish") return DiffKind.CapabilityGainedEventPublish;
67
+ if ((kind === "codeunit" || kind === "page" || kind === "report") && op === "execute") {
68
+ const extra = (fact as { extra?: { kind?: string } }).extra;
69
+ const dispatchUnresolved = extra?.kind === "dispatch" && fact.resourceId === undefined;
70
+ if (dispatchUnresolved) return DiffKind.CapabilityGainedDynamicDispatch;
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ /** Key for matching facts across snapshots. */
76
+ function factKey(fact: CapabilityFact): string {
77
+ return `${fact.op}|${fact.resourceKind}|${String(fact.resourceId ?? "")}`;
78
+ }
79
+
80
+ function makeFinding(
81
+ kind: CapabilityDetails["kind"],
82
+ subjectId: string,
83
+ indexes: DiffIndexes,
84
+ details: CapabilityDetails,
85
+ secondaryKey: string,
86
+ comparisonCone: readonly string[],
87
+ ): CapabilityFinding {
88
+ const origin = indexes.originByNormalized.get(subjectId);
89
+ const display =
90
+ indexes.newDisplayByStableId.get(subjectId) ??
91
+ indexes.oldDisplayByStableId.get(subjectId) ??
92
+ subjectId;
93
+ return {
94
+ id: computeDiffFingerprint({
95
+ category: DiffCategory.Capabilities,
96
+ kind,
97
+ normalizedStableId: subjectId,
98
+ secondaryKey,
99
+ }),
100
+ category: DiffCategory.Capabilities,
101
+ kind,
102
+ severity: SEVERITY[kind] ?? "medium",
103
+ subject: {
104
+ normalizedStableId: subjectId,
105
+ oldOriginalStableId: origin?.oldOriginalStableId,
106
+ newStableId: origin?.newStableId,
107
+ displayName: display,
108
+ },
109
+ comparisonCone,
110
+ details,
111
+ };
112
+ }
113
+
114
+ export function diffCapabilities(
115
+ _oldSnap: unknown,
116
+ _newSnap: unknown,
117
+ indexes: DiffIndexes,
118
+ _opts: DiffEngineOptions,
119
+ ): CapabilityFinding[] {
120
+ const out: CapabilityFinding[] = [];
121
+
122
+ const allSubjects = new Set<string>();
123
+ for (const s of indexes.oldCapabilityFactsBySubject.keys()) allSubjects.add(s);
124
+ for (const s of indexes.newCapabilityFactsBySubject.keys()) allSubjects.add(s);
125
+
126
+ for (const subject of allSubjects) {
127
+ const oldFacts = indexes.oldCapabilityFactsBySubject.get(subject) ?? [];
128
+ const newFacts = indexes.newCapabilityFactsBySubject.get(subject) ?? [];
129
+ const oldByKey = new Map<string, CapabilityFact>();
130
+ const newByKey = new Map<string, CapabilityFact>();
131
+ for (const f of oldFacts) oldByKey.set(factKey(f), f);
132
+ for (const f of newFacts) newByKey.set(factKey(f), f);
133
+
134
+ // Gains: in new, not in old.
135
+ for (const [key, fact] of newByKey) {
136
+ if (oldByKey.has(key)) continue;
137
+ const gainKind = gainedKindFor(fact);
138
+ if (gainKind === undefined) continue;
139
+ out.push(
140
+ makeFinding(
141
+ gainKind as CapabilityDetails["kind"],
142
+ subject,
143
+ indexes,
144
+ {
145
+ kind: gainKind as CapabilityDetails["kind"],
146
+ resourceKind: fact.resourceKind,
147
+ resourceId: fact.resourceId !== undefined ? String(fact.resourceId) : undefined,
148
+ op: fact.op,
149
+ },
150
+ key,
151
+ [subject],
152
+ ),
153
+ );
154
+ }
155
+
156
+ // Losses: in old, not in new. Emit provisional CapabilityLost; policy
157
+ // post-pass downgrades to CapabilityLostUnderCoverage when applicable.
158
+ for (const [key, fact] of oldByKey) {
159
+ if (newByKey.has(key)) continue;
160
+ out.push(
161
+ makeFinding(
162
+ DiffKind.CapabilityLost,
163
+ subject,
164
+ indexes,
165
+ {
166
+ kind: DiffKind.CapabilityLost,
167
+ resourceKind: fact.resourceKind,
168
+ resourceId: fact.resourceId !== undefined ? String(fact.resourceId) : undefined,
169
+ op: fact.op,
170
+ },
171
+ key,
172
+ [subject],
173
+ ),
174
+ );
175
+ }
176
+ }
177
+
178
+ return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
179
+ }