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,267 @@
1
+ import type { CapabilityFact } from "../model/capability.ts";
2
+ import type { Routine } from "../model/entities.ts";
3
+ import type { SemanticModel } from "../model/model.ts";
4
+ import type { Predicate, PredicateOperator, PredicateTrace, Tristate } from "./policy-types.ts";
5
+ import type { FieldEvalContext, FieldIndexes, FieldValue } from "./predicate-fields.ts";
6
+ import { getFieldDef } from "./predicate-fields.ts";
7
+
8
+ export type { Tristate } from "./policy-types.ts";
9
+
10
+ // ---- Kleene combinators ----
11
+ export function kleeneAnd(a: Tristate, b: Tristate): Tristate {
12
+ if (a === "false" || b === "false") return "false";
13
+ if (a === "unknown" || b === "unknown") return "unknown";
14
+ return "true";
15
+ }
16
+ export function kleeneOr(a: Tristate, b: Tristate): Tristate {
17
+ if (a === "true" || b === "true") return "true";
18
+ if (a === "unknown" || b === "unknown") return "unknown";
19
+ return "false";
20
+ }
21
+ export function kleeneNot(a: Tristate): Tristate {
22
+ if (a === "true") return "false";
23
+ if (a === "false") return "true";
24
+ return "unknown";
25
+ }
26
+
27
+ // ---- Predicate evaluation ----
28
+ export interface EvalResult {
29
+ result: Tristate;
30
+ trace: PredicateTrace;
31
+ }
32
+
33
+ export function evaluatePredicate(p: Predicate, ctx: FieldEvalContext): EvalResult {
34
+ switch (p.kind) {
35
+ case "field":
36
+ return evaluateField(p, ctx);
37
+ case "all":
38
+ return evaluateAll(p.children, ctx);
39
+ case "any":
40
+ return evaluateAny(p.children, ctx);
41
+ case "not": {
42
+ const inner = evaluatePredicate(p.child, ctx);
43
+ const result = kleeneNot(inner.result);
44
+ return { result, trace: { node: "not", result, children: [inner.trace] } };
45
+ }
46
+ }
47
+ }
48
+
49
+ function evaluateAll(children: readonly Predicate[], ctx: FieldEvalContext): EvalResult {
50
+ if (children.length === 0) {
51
+ // Empty conjunction is true by convention.
52
+ return { result: "true", trace: { node: "all", result: "true", children: [] } };
53
+ }
54
+ let acc: Tristate = "true";
55
+ const traces: PredicateTrace[] = [];
56
+ for (const c of children) {
57
+ const r = evaluatePredicate(c, ctx);
58
+ traces.push(r.trace);
59
+ acc = kleeneAnd(acc, r.result);
60
+ if (acc === "false") break; // short-circuit; remaining children may not eval.
61
+ }
62
+ return { result: acc, trace: { node: "all", result: acc, children: traces } };
63
+ }
64
+
65
+ function evaluateAny(children: readonly Predicate[], ctx: FieldEvalContext): EvalResult {
66
+ if (children.length === 0) {
67
+ // Empty disjunction is false by convention.
68
+ return { result: "false", trace: { node: "any", result: "false", children: [] } };
69
+ }
70
+ let acc: Tristate = "false";
71
+ const traces: PredicateTrace[] = [];
72
+ for (const c of children) {
73
+ const r = evaluatePredicate(c, ctx);
74
+ traces.push(r.trace);
75
+ acc = kleeneOr(acc, r.result);
76
+ if (acc === "true") break;
77
+ }
78
+ return { result: acc, trace: { node: "any", result: acc, children: traces } };
79
+ }
80
+
81
+ function evaluateField(
82
+ p: Extract<Predicate, { kind: "field" }>,
83
+ ctx: FieldEvalContext,
84
+ ): EvalResult {
85
+ const def = getFieldDef(p.field);
86
+ if (def === undefined) {
87
+ return {
88
+ result: "unknown",
89
+ trace: { node: "field", result: "unknown", field: p.field, unknownReason: "evaluator-error" },
90
+ };
91
+ }
92
+ let value: FieldValue;
93
+ try {
94
+ value = def.evaluate(ctx);
95
+ } catch {
96
+ return {
97
+ result: "unknown",
98
+ trace: { node: "field", result: "unknown", field: p.field, unknownReason: "evaluator-error" },
99
+ };
100
+ }
101
+ if (value.kind === "unknown") {
102
+ return {
103
+ result: "unknown",
104
+ trace: {
105
+ node: "field",
106
+ result: "unknown",
107
+ field: p.field,
108
+ expected: p.value,
109
+ unknownReason: value.reason,
110
+ },
111
+ };
112
+ }
113
+ const matched = matchOperator(p.operator, value.value, p.value);
114
+ const result: Tristate = matched ? "true" : "false";
115
+ return {
116
+ result,
117
+ trace: { node: "field", result, field: p.field, expected: p.value, actual: value.value },
118
+ };
119
+ }
120
+
121
+ function matchOperator(op: PredicateOperator, actual: unknown, expected: unknown): boolean {
122
+ switch (op) {
123
+ case "==":
124
+ return actual === expected;
125
+ case "in":
126
+ return (
127
+ Array.isArray(expected) &&
128
+ (Array.isArray(actual)
129
+ ? actual.some((a) => expected.includes(a))
130
+ : expected.includes(actual))
131
+ );
132
+ case "glob":
133
+ return typeof expected === "string" && globMatch(expected, actualToString(actual));
134
+ case "glob-in":
135
+ return (
136
+ Array.isArray(expected) &&
137
+ expected.some(
138
+ (g) =>
139
+ typeof g === "string" &&
140
+ (Array.isArray(actual)
141
+ ? actual.some((a) => globMatch(g, actualToString(a)))
142
+ : globMatch(g, actualToString(actual))),
143
+ )
144
+ );
145
+ }
146
+ }
147
+
148
+ function actualToString(v: unknown): string {
149
+ if (typeof v === "string") return v;
150
+ if (Array.isArray(v)) return v.join(",");
151
+ return String(v);
152
+ }
153
+
154
+ // Case-insensitive glob; * matches any chars, ? matches single char. Anchored.
155
+ export function globMatch(pattern: string, value: string): boolean {
156
+ const compiled = compileGlob(pattern);
157
+ return compiled.test(value);
158
+ }
159
+
160
+ const globCache = new Map<string, RegExp>();
161
+ function compileGlob(pattern: string): RegExp {
162
+ const cached = globCache.get(pattern);
163
+ if (cached !== undefined) return cached;
164
+ // Escape regex metachars except * and ?
165
+ const escaped = pattern
166
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
167
+ .replace(/\*/g, ".*")
168
+ .replace(/\?/g, ".");
169
+ const re = new RegExp(`^${escaped}$`, "i");
170
+ globCache.set(pattern, re);
171
+ return re;
172
+ }
173
+
174
+ // ---- No-trace evaluation (hot path) ----
175
+ //
176
+ // `evaluatePredicate` (above) always allocates a PredicateTrace tree. The engine
177
+ // only needs the tristate result for applicability pre-checks and the per-fact
178
+ // when/except loop, so these variants skip trace allocation entirely. The traced
179
+ // version is retained for `explain`.
180
+
181
+ type EvalMode = "full" | "applicability";
182
+
183
+ /** A frozen placeholder used only in applicability mode. CONTRACT: fact-scoped
184
+ * fields short-circuit to "unknown" BEFORE def.evaluate runs, and no
185
+ * routine/model-scoped evaluator reads ctx.fact — so these values are never
186
+ * observed. `subject` is undefined-as-never so any accidental future read of a
187
+ * fact field from a non-fact evaluator surfaces as undefined (⇒ unknown), not a
188
+ * plausible value. */
189
+ const PLACEHOLDER_FACT: CapabilityFact = Object.freeze({
190
+ subject: undefined as never,
191
+ op: "read",
192
+ resourceKind: "transaction",
193
+ confidence: "static",
194
+ provenance: "direct",
195
+ via: "self",
196
+ });
197
+
198
+ /** Routine/model context for applicability — no fact required. */
199
+ export interface ApplicabilityContext {
200
+ routine: Routine;
201
+ model: SemanticModel;
202
+ indexes?: FieldIndexes;
203
+ }
204
+
205
+ function fieldTristate(
206
+ p: Extract<Predicate, { kind: "field" }>,
207
+ ctx: FieldEvalContext,
208
+ mode: EvalMode,
209
+ ): Tristate {
210
+ const def = getFieldDef(p.field);
211
+ if (def === undefined) return "unknown";
212
+ // Applicability: a fact-scoped field cannot be decided without a fact, so it is
213
+ // "unknown" (never read; no fact is required). Routine/model fields evaluate normally.
214
+ if (mode === "applicability" && def.scope === "fact") return "unknown";
215
+ let value: FieldValue;
216
+ try {
217
+ value = def.evaluate(ctx);
218
+ } catch {
219
+ return "unknown";
220
+ }
221
+ if (value.kind === "unknown") return "unknown";
222
+ return matchOperator(p.operator, value.value, p.value) ? "true" : "false";
223
+ }
224
+
225
+ function evalTristate(p: Predicate, ctx: FieldEvalContext, mode: EvalMode): Tristate {
226
+ switch (p.kind) {
227
+ case "field":
228
+ return fieldTristate(p, ctx, mode);
229
+ case "all": {
230
+ if (p.children.length === 0) return "true";
231
+ let acc: Tristate = "true";
232
+ for (const c of p.children) {
233
+ acc = kleeneAnd(acc, evalTristate(c, ctx, mode));
234
+ if (acc === "false") return "false";
235
+ }
236
+ return acc;
237
+ }
238
+ case "any": {
239
+ if (p.children.length === 0) return "false";
240
+ let acc: Tristate = "false";
241
+ for (const c of p.children) {
242
+ acc = kleeneOr(acc, evalTristate(c, ctx, mode));
243
+ if (acc === "true") return "true";
244
+ }
245
+ return acc;
246
+ }
247
+ case "not":
248
+ return kleeneNot(evalTristate(p.child, ctx, mode));
249
+ }
250
+ }
251
+
252
+ /** Full evaluation, tristate only (no trace). Used per-fact in the engine. */
253
+ export function evaluateResult(p: Predicate, ctx: FieldEvalContext): Tristate {
254
+ return evalTristate(p, ctx, "full");
255
+ }
256
+
257
+ /** Partial evaluation over routine/model fields only (fact fields ⇒ unknown).
258
+ * `false` ⇒ no fact can satisfy P for this routine ⇒ rule not applicable. */
259
+ export function evaluateApplicability(p: Predicate, ctx: ApplicabilityContext): Tristate {
260
+ const full: FieldEvalContext = {
261
+ routine: ctx.routine,
262
+ fact: PLACEHOLDER_FACT,
263
+ model: ctx.model,
264
+ indexes: ctx.indexes,
265
+ };
266
+ return evalTristate(p, full, "applicability");
267
+ }
@@ -0,0 +1,439 @@
1
+ import type { CapabilityFact, CapabilityResourceKind } from "../model/capability.ts";
2
+ import type { ObjectDecl, Routine, Table } from "../model/entities.ts";
3
+ import type { EventSymbol } from "../model/graph.ts";
4
+ import type { EventId, ObjectId, RoutineId, TableId } from "../model/ids.ts";
5
+ import type { SemanticModel } from "../model/model.ts";
6
+
7
+ export type FieldValueShape =
8
+ | "enum"
9
+ | "enum-list"
10
+ | "glob"
11
+ | "glob-list"
12
+ | "string-exact"
13
+ | "string-list"
14
+ | "numeric"
15
+ | "tri-state";
16
+
17
+ export type FieldScope = "routine" | "fact" | "model";
18
+
19
+ export type FieldValue =
20
+ | { kind: "known"; value: string | readonly string[] | number | boolean | undefined }
21
+ | {
22
+ kind: "unknown";
23
+ reason:
24
+ | "field-not-applicable"
25
+ | "resource-id-unresolved"
26
+ | "no-root-classification"
27
+ | "evaluator-error";
28
+ };
29
+
30
+ export interface FieldIndexes {
31
+ objectsById: Map<ObjectId, ObjectDecl>;
32
+ rootKindsByRoutineId: Map<RoutineId, readonly string[]>;
33
+ tablesById: Map<TableId, Table>;
34
+ eventsById: Map<EventId, EventSymbol>;
35
+ }
36
+
37
+ export interface FieldEvalContext {
38
+ routine: Routine;
39
+ fact: CapabilityFact;
40
+ model: SemanticModel;
41
+ indexes?: FieldIndexes;
42
+ }
43
+
44
+ /** Build the per-run lookup bundle once; the engine threads it through every
45
+ * field evaluation so 108k-routine runs do Map.get instead of Array.find. */
46
+ export function buildFieldIndexes(model: SemanticModel): FieldIndexes {
47
+ const objectsById = new Map<ObjectId, ObjectDecl>();
48
+ for (const o of model.objects) objectsById.set(o.id, o);
49
+ const rootKindsByRoutineId = new Map<RoutineId, readonly string[]>();
50
+ for (const rc of model.rootClassifications) rootKindsByRoutineId.set(rc.routineId, rc.kinds);
51
+ const tablesById = new Map<TableId, Table>();
52
+ for (const t of model.tables) tablesById.set(t.id, t);
53
+ const eventsById = new Map<EventId, EventSymbol>();
54
+ for (const e of model.eventGraph.events) eventsById.set(e.id, e);
55
+ return { objectsById, rootKindsByRoutineId, tablesById, eventsById };
56
+ }
57
+
58
+ export interface PredicateFieldDef {
59
+ name: string;
60
+ scope: FieldScope;
61
+ valueShape: FieldValueShape;
62
+ enumValues?: readonly string[];
63
+ description: string;
64
+ evaluate(ctx: FieldEvalContext): FieldValue;
65
+ jsonSchemaFragment: unknown;
66
+ }
67
+
68
+ // ---- helpers ----
69
+ function known(value: string | readonly string[] | number | boolean | undefined): FieldValue {
70
+ return { kind: "known", value };
71
+ }
72
+
73
+ function unknown(
74
+ reason:
75
+ | "field-not-applicable"
76
+ | "resource-id-unresolved"
77
+ | "no-root-classification"
78
+ | "evaluator-error",
79
+ ): FieldValue {
80
+ return { kind: "unknown", reason };
81
+ }
82
+
83
+ // Resource-kind enum values mirror src/model/capability.ts CapabilityResourceKind.
84
+ const RESOURCE_KINDS: readonly CapabilityResourceKind[] = [
85
+ "table",
86
+ "event",
87
+ "codeunit",
88
+ "page",
89
+ "report",
90
+ "http",
91
+ "telemetry",
92
+ "isolated-storage",
93
+ "file",
94
+ "transaction",
95
+ "ui",
96
+ "background",
97
+ ];
98
+
99
+ const CAPABILITY_OPS = [
100
+ "read",
101
+ "insert",
102
+ "modify",
103
+ "delete",
104
+ "execute",
105
+ "publish",
106
+ "subscribe",
107
+ "send",
108
+ "log",
109
+ "store-read",
110
+ "store-write",
111
+ "store-delete",
112
+ "commit",
113
+ "open",
114
+ "write-blob",
115
+ "start",
116
+ "ui-confirm",
117
+ "ui-message",
118
+ "ui-error",
119
+ ] as const;
120
+
121
+ const CONFIDENCE_VALUES = [
122
+ "static",
123
+ "boundedDynamic",
124
+ "configDynamic",
125
+ "userDynamic",
126
+ "unresolved",
127
+ ] as const;
128
+
129
+ const VIA_VALUES = [
130
+ "self",
131
+ "call",
132
+ "object-run",
133
+ "event-dispatch",
134
+ "implicit-trigger",
135
+ "dependency",
136
+ ] as const;
137
+
138
+ const PROVENANCE_VALUES = ["direct", "inherited"] as const;
139
+
140
+ // Get the containing object for a routine, when present.
141
+ function objectOf(ctx: FieldEvalContext): ObjectDecl | undefined {
142
+ // When indexes are present they are authoritative (built from the same model);
143
+ // a miss is genuinely absent — never fall back to a linear scan.
144
+ if (ctx.indexes !== undefined) return ctx.indexes.objectsById.get(ctx.routine.objectId);
145
+ return ctx.model.objects.find((o) => o.id === ctx.routine.objectId);
146
+ }
147
+
148
+ // ---- registry entries ----
149
+ export const PREDICATE_FIELDS: readonly PredicateFieldDef[] = [
150
+ // ---- Routine-scope fields ----
151
+ {
152
+ name: "root.kinds",
153
+ scope: "routine",
154
+ valueShape: "enum-list",
155
+ description: "Root classification kinds for the routine (api-page, event-subscriber, etc.).",
156
+ evaluate(ctx) {
157
+ // Decision 2: classification is exhaustive — a routine with no classification is a
158
+ // non-root, so its root-kind set is empty (NOT unknown). This makes positive
159
+ // root.kinds constraints correctly `false` for non-roots, enabling the
160
+ // applicability skip. When indexes are present they are AUTHORITATIVE: a miss is
161
+ // `[]` at O(1) — never fall back to the linear rootClassifications scan (that
162
+ // fallback on the ~100k non-root routines is a multi-minute regression).
163
+ if (ctx.indexes !== undefined) {
164
+ return known(ctx.indexes.rootKindsByRoutineId.get(ctx.routine.id) ?? []);
165
+ }
166
+ const rc = ctx.model.rootClassifications.find((x) => x.routineId === ctx.routine.id);
167
+ return known(rc?.kinds ?? []);
168
+ },
169
+ jsonSchemaFragment: {
170
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
171
+ },
172
+ },
173
+ {
174
+ name: "routine.name",
175
+ scope: "routine",
176
+ valueShape: "glob",
177
+ description: "Routine name (case-insensitive glob match).",
178
+ evaluate(ctx) {
179
+ return known(ctx.routine.name);
180
+ },
181
+ jsonSchemaFragment: {
182
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
183
+ },
184
+ },
185
+ {
186
+ name: "routine.kind",
187
+ scope: "routine",
188
+ valueShape: "enum",
189
+ enumValues: ["procedure", "trigger", "event-publisher", "event-subscriber"],
190
+ description: "Routine kind.",
191
+ evaluate(ctx) {
192
+ return known(ctx.routine.kind);
193
+ },
194
+ jsonSchemaFragment: {
195
+ type: "string",
196
+ enum: ["procedure", "trigger", "event-publisher", "event-subscriber"],
197
+ },
198
+ },
199
+ {
200
+ name: "routine.accessModifier",
201
+ scope: "routine",
202
+ valueShape: "enum-list",
203
+ enumValues: ["public", "internal", "local", "protected"],
204
+ description: "Routine access modifier.",
205
+ // accessModifier is ProcedureAccessModifier ("local"|"internal"|"protected") | undefined.
206
+ // Absent/undefined means AL default ("public").
207
+ evaluate(ctx) {
208
+ return known(ctx.routine.accessModifier ?? "public");
209
+ },
210
+ jsonSchemaFragment: {
211
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
212
+ },
213
+ },
214
+ {
215
+ name: "object.name",
216
+ scope: "routine",
217
+ valueShape: "glob",
218
+ description: "Containing object name (case-insensitive glob).",
219
+ evaluate(ctx) {
220
+ const obj = objectOf(ctx);
221
+ if (obj === undefined) return unknown("field-not-applicable");
222
+ return known(obj.name);
223
+ },
224
+ jsonSchemaFragment: {
225
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
226
+ },
227
+ },
228
+ {
229
+ name: "object.kind",
230
+ scope: "routine",
231
+ valueShape: "enum-list",
232
+ enumValues: [
233
+ "codeunit",
234
+ "table",
235
+ "page",
236
+ "report",
237
+ "xmlport",
238
+ "query",
239
+ "enum",
240
+ "interface",
241
+ "permissionset",
242
+ ],
243
+ description: "Containing object kind.",
244
+ // objectType is verbatim from AL source (e.g. "Codeunit", "Table") — lowercase for matching.
245
+ evaluate(ctx) {
246
+ const obj = objectOf(ctx);
247
+ if (obj === undefined) return unknown("field-not-applicable");
248
+ return known(obj.objectType.toLowerCase());
249
+ },
250
+ jsonSchemaFragment: {
251
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
252
+ },
253
+ },
254
+ {
255
+ name: "object.appGuid",
256
+ scope: "routine",
257
+ valueShape: "string-exact",
258
+ description: "Containing object's app GUID (advanced; cross-app rules).",
259
+ evaluate(ctx) {
260
+ const obj = objectOf(ctx);
261
+ if (obj === undefined) return unknown("field-not-applicable");
262
+ return known(obj.appGuid);
263
+ },
264
+ jsonSchemaFragment: { type: "string" },
265
+ },
266
+
267
+ // ---- Fact-scope fields ----
268
+ {
269
+ name: "capability.op",
270
+ scope: "fact",
271
+ valueShape: "enum-list",
272
+ enumValues: CAPABILITY_OPS,
273
+ description: "Capability op (insert, commit, send, ui-confirm, etc.).",
274
+ evaluate(ctx) {
275
+ return known(ctx.fact.op);
276
+ },
277
+ jsonSchemaFragment: {
278
+ oneOf: [
279
+ { type: "string", enum: CAPABILITY_OPS },
280
+ { type: "array", items: { type: "string", enum: CAPABILITY_OPS } },
281
+ ],
282
+ },
283
+ },
284
+ {
285
+ name: "capability.resourceKind",
286
+ scope: "fact",
287
+ valueShape: "enum-list",
288
+ enumValues: RESOURCE_KINDS,
289
+ description: "Capability resource kind.",
290
+ evaluate(ctx) {
291
+ return known(ctx.fact.resourceKind);
292
+ },
293
+ jsonSchemaFragment: {
294
+ oneOf: [
295
+ { type: "string", enum: RESOURCE_KINDS },
296
+ { type: "array", items: { type: "string", enum: RESOURCE_KINDS } },
297
+ ],
298
+ },
299
+ },
300
+ {
301
+ name: "capability.resource.table.name",
302
+ scope: "fact",
303
+ valueShape: "glob",
304
+ description: "Target table name when resourceKind is 'table' (case-insensitive glob).",
305
+ evaluate(ctx) {
306
+ if (ctx.fact.resourceKind !== "table") return unknown("field-not-applicable");
307
+ if (typeof ctx.fact.resourceId !== "string") return unknown("resource-id-unresolved");
308
+ const tableId = ctx.fact.resourceId;
309
+ const table =
310
+ ctx.indexes !== undefined
311
+ ? ctx.indexes.tablesById.get(tableId)
312
+ : ctx.model.tables.find((t) => t.id === tableId);
313
+ if (table === undefined) return unknown("resource-id-unresolved");
314
+ return known(table.name);
315
+ },
316
+ jsonSchemaFragment: {
317
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
318
+ },
319
+ },
320
+ {
321
+ name: "capability.resource.event.name",
322
+ scope: "fact",
323
+ valueShape: "glob",
324
+ description: "Target event name when resourceKind is 'event'.",
325
+ evaluate(ctx) {
326
+ if (ctx.fact.resourceKind !== "event") return unknown("field-not-applicable");
327
+ if (typeof ctx.fact.resourceId !== "string") return unknown("resource-id-unresolved");
328
+ const eventId = ctx.fact.resourceId;
329
+ const ev =
330
+ ctx.indexes !== undefined
331
+ ? ctx.indexes.eventsById.get(eventId)
332
+ : ctx.model.eventGraph.events.find((e) => e.id === eventId);
333
+ if (ev === undefined) return unknown("resource-id-unresolved");
334
+ return known(ev.eventName);
335
+ },
336
+ jsonSchemaFragment: {
337
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
338
+ },
339
+ },
340
+ {
341
+ name: "capability.resource.http.method",
342
+ scope: "fact",
343
+ valueShape: "enum-list",
344
+ enumValues: ["Send", "Get", "Post", "Put", "Delete", "Patch"],
345
+ description: "HTTP method when resourceKind is 'http'.",
346
+ evaluate(ctx) {
347
+ if (ctx.fact.resourceKind !== "http") return unknown("field-not-applicable");
348
+ const extra = ctx.fact.extra;
349
+ if (extra?.kind !== "http") return unknown("field-not-applicable");
350
+ return known(extra.method);
351
+ },
352
+ jsonSchemaFragment: {
353
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
354
+ },
355
+ },
356
+ {
357
+ name: "capability.resource.ui.kind",
358
+ scope: "fact",
359
+ valueShape: "enum-list",
360
+ enumValues: ["confirm", "message", "error", "dialog", "modalPage", "requestPage"],
361
+ description: "UI kind (derived from op when ui-* ops are present).",
362
+ evaluate(ctx) {
363
+ if (ctx.fact.resourceKind !== "ui") return unknown("field-not-applicable");
364
+ switch (ctx.fact.op) {
365
+ case "ui-confirm":
366
+ return known("confirm");
367
+ case "ui-message":
368
+ return known("message");
369
+ case "ui-error":
370
+ return known("error");
371
+ default:
372
+ return unknown("field-not-applicable");
373
+ }
374
+ },
375
+ jsonSchemaFragment: {
376
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
377
+ },
378
+ },
379
+ {
380
+ name: "capability.confidence",
381
+ scope: "fact",
382
+ valueShape: "enum-list",
383
+ enumValues: CONFIDENCE_VALUES,
384
+ description:
385
+ "Capability confidence (static, boundedDynamic, configDynamic, userDynamic, unresolved).",
386
+ evaluate(ctx) {
387
+ return known(ctx.fact.confidence);
388
+ },
389
+ jsonSchemaFragment: {
390
+ oneOf: [
391
+ { type: "string", enum: CONFIDENCE_VALUES },
392
+ { type: "array", items: { type: "string", enum: CONFIDENCE_VALUES } },
393
+ ],
394
+ },
395
+ },
396
+ {
397
+ name: "capability.origin",
398
+ scope: "fact",
399
+ valueShape: "enum-list",
400
+ enumValues: PROVENANCE_VALUES,
401
+ description: "Capability origin: direct (this routine) or inherited (transitive).",
402
+ // Maps to CapabilityFact.provenance ("direct" | "inherited").
403
+ evaluate(ctx) {
404
+ return known(ctx.fact.provenance);
405
+ },
406
+ jsonSchemaFragment: {
407
+ oneOf: [
408
+ { type: "string", enum: PROVENANCE_VALUES },
409
+ { type: "array", items: { type: "string", enum: PROVENANCE_VALUES } },
410
+ ],
411
+ },
412
+ },
413
+ {
414
+ name: "capability.via",
415
+ scope: "fact",
416
+ valueShape: "enum-list",
417
+ enumValues: VIA_VALUES,
418
+ description: "Edge kind that brought the fact in.",
419
+ evaluate(ctx) {
420
+ return known(ctx.fact.via);
421
+ },
422
+ jsonSchemaFragment: {
423
+ oneOf: [
424
+ { type: "string", enum: VIA_VALUES },
425
+ { type: "array", items: { type: "string", enum: VIA_VALUES } },
426
+ ],
427
+ },
428
+ },
429
+ ] as const;
430
+
431
+ const fieldByName = new Map(PREDICATE_FIELDS.map((f) => [f.name, f] as const));
432
+
433
+ export function getFieldDef(name: string): PredicateFieldDef | undefined {
434
+ return fieldByName.get(name);
435
+ }
436
+
437
+ export function listFieldNames(): readonly string[] {
438
+ return PREDICATE_FIELDS.map((f) => f.name);
439
+ }