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,130 @@
1
+ import type { CapabilityFact } from "../model/capability.ts";
2
+ import type { CoverageRecord } from "../model/coverage.ts";
3
+ import type { GraphEdge } from "../model/graph-edge.ts";
4
+ import type { CallsiteId, OperationId } from "../model/ids.ts";
5
+ import type { StableRoutineId } from "../model/stable-identity.ts";
6
+ import type { CallsiteEvidence, CapabilitySnapshot, OperationEvidence } from "../snapshot/types.ts";
7
+
8
+ /**
9
+ * The shared per-invocation index set used by `fingerprintQuery` and
10
+ * `reconstructWitnessPaths`. Built once from a `CapabilitySnapshot`.
11
+ *
12
+ * `routineDisplayById` is the full `Object::Routine` form (e.g.
13
+ * `Codeunit "Sales-Post"::Run`). Other display maps return shorter forms
14
+ * (`stableIdToDisplay` is whatever the identity table records for that id).
15
+ */
16
+ export interface FingerprintIndexes {
17
+ stableIdToDisplay: Map<string, string>;
18
+ /** Lowercase whitespace-normalized display → all routines that match. */
19
+ displayToStableIds: Map<string, StableRoutineId[]>;
20
+ routineDisplayById: Map<StableRoutineId, string>;
21
+ outgoingEdges: Map<StableRoutineId, readonly GraphEdge[]>;
22
+ factsByRoutine: Map<StableRoutineId, readonly CapabilityFact[]>;
23
+ directFactsByRoutine: Map<StableRoutineId, readonly CapabilityFact[]>;
24
+ coverageByRoutine: Map<StableRoutineId, CoverageRecord>;
25
+ callsiteById: Map<CallsiteId, CallsiteEvidence>;
26
+ operationById: Map<OperationId, OperationEvidence>;
27
+ /** Event-id → display for hop rendering; sourced from event declarations. */
28
+ eventDisplayById: Map<string, string>;
29
+ }
30
+
31
+ const ROUTINE_ID_SEPARATOR = "#"; // StableRoutineId encodes `${stableObjectId}#${signatureHash}`
32
+
33
+ function isRoutineStableId(id: string): boolean {
34
+ return id.includes(ROUTINE_ID_SEPARATOR);
35
+ }
36
+
37
+ /**
38
+ * Lowercase, trim, collapse internal whitespace. Used by both the index builder
39
+ * (key for `displayToStableIds`) and the query module's selector resolver.
40
+ * Shared so the two sides cannot silently drift apart on the normalization contract.
41
+ */
42
+ export function normalizeDisplayKey(s: string): string {
43
+ return s.trim().toLowerCase().replace(/\s+/g, " ");
44
+ }
45
+
46
+ /**
47
+ * Builds the shared per-invocation index set from a CapabilitySnapshot.
48
+ * Used by fingerprint query and witness path reconstruction.
49
+ */
50
+ export function buildFingerprintIndexes(snap: CapabilitySnapshot): FingerprintIndexes {
51
+ const stableIdToDisplay = new Map<string, string>();
52
+ const displayToStableIds = new Map<string, StableRoutineId[]>();
53
+ const routineDisplayById = new Map<StableRoutineId, string>();
54
+
55
+ for (let i = 0; i < snap.identities.stableIds.length; i++) {
56
+ const id = snap.identities.stableIds[i] ?? "";
57
+ const display = snap.identities.displayNames[i] ?? "";
58
+ if (id === "") continue;
59
+ stableIdToDisplay.set(id, display);
60
+ if (isRoutineStableId(id)) {
61
+ const rid = id as StableRoutineId;
62
+ routineDisplayById.set(rid, display);
63
+ const key = normalizeDisplayKey(display);
64
+ const list = displayToStableIds.get(key) ?? [];
65
+ list.push(rid);
66
+ displayToStableIds.set(key, list);
67
+ }
68
+ }
69
+
70
+ const outgoingEdges = new Map<StableRoutineId, GraphEdge[]>();
71
+ for (const edge of snap.typedEdges) {
72
+ const from = edge.from as unknown as StableRoutineId;
73
+ const list = outgoingEdges.get(from) ?? [];
74
+ list.push(edge);
75
+ outgoingEdges.set(from, list);
76
+ }
77
+
78
+ const factsByRoutine = new Map<StableRoutineId, CapabilityFact[]>();
79
+ const directFactsByRoutine = new Map<StableRoutineId, CapabilityFact[]>();
80
+ for (const fact of snap.capabilityFacts) {
81
+ const subject = fact.subject as unknown as StableRoutineId;
82
+ const list = factsByRoutine.get(subject) ?? [];
83
+ list.push(fact);
84
+ factsByRoutine.set(subject, list);
85
+ if (fact.provenance === "direct") {
86
+ const direct = directFactsByRoutine.get(subject) ?? [];
87
+ direct.push(fact);
88
+ directFactsByRoutine.set(subject, direct);
89
+ }
90
+ }
91
+
92
+ const coverageByRoutine = new Map<StableRoutineId, CoverageRecord>();
93
+ for (const rec of snap.coverage) {
94
+ coverageByRoutine.set(rec.subject as unknown as StableRoutineId, rec);
95
+ }
96
+
97
+ const callsiteById = new Map<CallsiteId, CallsiteEvidence>();
98
+ for (const cs of snap.callsiteIndex) {
99
+ callsiteById.set(cs.callsiteId, cs);
100
+ }
101
+
102
+ const operationById = new Map<OperationId, OperationEvidence>();
103
+ for (const op of snap.operationIndex) {
104
+ operationById.set(op.operationId, op);
105
+ }
106
+
107
+ const eventDisplayById = new Map<string, string>();
108
+ for (const decl of snap.eventDeclarations) {
109
+ if (decl.kind !== "publisher") continue;
110
+ // StableEventId format: `${stablePublisherObjectId}::${eventName}::${parameterShapeHash}`.
111
+ // Extract event name from middle segment. Publishers have the canonical name; subscribers
112
+ // carry back-pointers only and should not override this map.
113
+ const parts = decl.eventId.split("::");
114
+ const eventName = parts[1] ?? String(decl.eventId);
115
+ eventDisplayById.set(String(decl.eventId), eventName);
116
+ }
117
+
118
+ return {
119
+ stableIdToDisplay,
120
+ displayToStableIds,
121
+ routineDisplayById,
122
+ outgoingEdges: outgoingEdges as Map<StableRoutineId, readonly GraphEdge[]>,
123
+ factsByRoutine: factsByRoutine as Map<StableRoutineId, readonly CapabilityFact[]>,
124
+ directFactsByRoutine: directFactsByRoutine as Map<StableRoutineId, readonly CapabilityFact[]>,
125
+ coverageByRoutine,
126
+ callsiteById,
127
+ operationById,
128
+ eventDisplayById,
129
+ };
130
+ }
@@ -0,0 +1,543 @@
1
+ import type {
2
+ CapabilityConfidence,
3
+ CapabilityFact,
4
+ CapabilityOp,
5
+ CapabilityProvenance,
6
+ CapabilityResourceKind,
7
+ CapabilityVia,
8
+ ValueSource,
9
+ } from "../model/capability.ts";
10
+ import type { CoverageReason, CoverageStatus } from "../model/coverage.ts";
11
+ import type { CallsiteId } from "../model/ids.ts";
12
+ import type { RootKind } from "../model/root-classification.ts";
13
+ import type { StableObjectId, StableRoutineId } from "../model/stable-identity.ts";
14
+ import type { CapabilitySnapshot } from "../snapshot/types.ts";
15
+ import {
16
+ type FingerprintIndexes,
17
+ buildFingerprintIndexes,
18
+ normalizeDisplayKey,
19
+ } from "./fingerprint-indexes.ts";
20
+ import {
21
+ type WitnessDiagnostic,
22
+ type WitnessIndexes,
23
+ type WitnessPath,
24
+ reconstructWitnessPaths,
25
+ } from "./fingerprint-witness.ts";
26
+
27
+ export interface FingerprintFilters {
28
+ roots?: ReadonlySet<RootKind>;
29
+ routineSelectors?: readonly string[];
30
+ includeInherited: boolean;
31
+ witnessLimit: false | number | "all";
32
+ }
33
+
34
+ export interface FingerprintQueryResult {
35
+ blocks: readonly FingerprintBlock[];
36
+ diagnostics: readonly FingerprintQueryDiagnostic[];
37
+ summary: {
38
+ totalClassifications: number;
39
+ renderedBlocks: number;
40
+ rootsConfigIgnored: boolean;
41
+ };
42
+ }
43
+
44
+ export interface FingerprintBlock {
45
+ routineId: StableRoutineId;
46
+ objectId?: StableObjectId;
47
+ objectDisplay: string;
48
+ routineDisplay: string;
49
+ kinds: readonly RootKind[];
50
+ classificationSource: "ast" | "config" | "ast+config";
51
+ configEntryId?: string;
52
+ coverage: BlockCoverage;
53
+ families: readonly BlockCapabilityFamily[];
54
+ mayCommit: BlockCommitSummary;
55
+ dispatch: BlockDispatchSummary;
56
+ requiredPermissions: readonly PermissionLine[];
57
+ witnesses: readonly BlockWitness[];
58
+ }
59
+
60
+ export interface BlockCoverage {
61
+ status: CoverageStatus;
62
+ directStatus: CoverageStatus;
63
+ inheritedStatus: CoverageStatus;
64
+ reasons: readonly CoverageReason[];
65
+ unknownTargets: readonly string[];
66
+ unknownTargetDisplays: readonly string[];
67
+ inheritedExcluded: boolean;
68
+ }
69
+
70
+ export interface BlockCapabilityFamily {
71
+ kind: CapabilityResourceKind;
72
+ coneCoverage: CoverageStatus;
73
+ resources: readonly BlockResource[];
74
+ }
75
+
76
+ export interface BlockResource {
77
+ id?: string;
78
+ display: string;
79
+ source?: ValueSource;
80
+ confidence: CapabilityConfidence;
81
+ ops: readonly CapabilityOp[];
82
+ facts: readonly CapabilityFact[];
83
+ }
84
+
85
+ export interface BlockCommitSummary {
86
+ presence: "yes" | "no" | "unknown";
87
+ witnessFact?: CapabilityFact;
88
+ }
89
+
90
+ export interface BlockDispatchSummary {
91
+ resolved: readonly DispatchInstance[];
92
+ unresolved: readonly DispatchInstance[];
93
+ }
94
+
95
+ export interface DispatchInstance {
96
+ objectType: "Codeunit" | "Page" | "Report";
97
+ targetId?: string;
98
+ targetDisplay?: string;
99
+ targetIdSource?: ValueSource;
100
+ confidence: CapabilityConfidence;
101
+ modal?: boolean;
102
+ provenance: CapabilityProvenance;
103
+ via: CapabilityVia;
104
+ witnessCallsiteId?: CallsiteId;
105
+ }
106
+
107
+ export interface PermissionLine {
108
+ targetKind: "table" | "object";
109
+ targetId: string;
110
+ targetDisplay: string;
111
+ rights: string;
112
+ coverage: CoverageStatus;
113
+ }
114
+
115
+ export interface BlockWitness {
116
+ fact: CapabilityFact;
117
+ paths: readonly WitnessPath[];
118
+ truncated: boolean;
119
+ incomplete: boolean;
120
+ diagnostics: readonly WitnessDiagnostic[];
121
+ }
122
+
123
+ export type FingerprintQueryDiagnostic =
124
+ | { kind: "selector-unresolved"; selector: string; triedForms: readonly string[] }
125
+ | {
126
+ kind: "selector-ambiguous";
127
+ selector: string;
128
+ matchedForm: string;
129
+ candidates: readonly { stableId: StableRoutineId; display: string }[];
130
+ };
131
+
132
+ const SELECTOR_FORMS = ["stable-routine-id", "full-display", "two-segment", "one-segment"] as const;
133
+
134
+ const MAX_AMBIGUOUS_CANDIDATES = 16;
135
+
136
+ function resolveSelector(
137
+ selector: string,
138
+ indexes: FingerprintIndexes,
139
+ ): { matches: StableRoutineId[]; matchedForm: string } {
140
+ // Form 1: exact StableRoutineId match (case-sensitive).
141
+ if (indexes.routineDisplayById.has(selector as StableRoutineId)) {
142
+ return { matches: [selector as StableRoutineId], matchedForm: "stable-routine-id" };
143
+ }
144
+
145
+ // Form 2: full display name.
146
+ const key = normalizeDisplayKey(selector);
147
+ const full = indexes.displayToStableIds.get(key) ?? [];
148
+ if (full.length > 0) return { matches: full, matchedForm: "full-display" };
149
+
150
+ // Form 3: two-segment match — user omits the leading type-keyword prefix.
151
+ // Display format is always `TypeWord "ObjectName"::RoutineName` (the TypeWord
152
+ // is an unquoted AL keyword such as Codeunit, Page, Report). Strip the leading
153
+ // word-and-space to get `"ObjectName"::RoutineName` and compare. This approach
154
+ // is robust to "::" appearing inside a quoted object name, which
155
+ // `display.split("::")` and naive `lastIndexOf` cannot handle correctly.
156
+ const typeWordPrefix = /^\w+\s+/;
157
+ const two: StableRoutineId[] = [];
158
+ for (const [display, ids] of indexes.displayToStableIds) {
159
+ const stripped = display.replace(typeWordPrefix, "");
160
+ if (stripped !== display && stripped === key) two.push(...ids);
161
+ }
162
+ if (two.length > 0) return { matches: two, matchedForm: "two-segment" };
163
+
164
+ // Form 4: one-segment (`Routine`).
165
+ // The routine name follows the last "::" in the display. Using lastIndexOf
166
+ // instead of split is robust to "::" inside quoted object names, since the
167
+ // routine identifier itself is always unquoted and never contains "::".
168
+ const one: StableRoutineId[] = [];
169
+ for (const [display, ids] of indexes.displayToStableIds) {
170
+ const lastSep = display.lastIndexOf("::");
171
+ const last = lastSep < 0 ? display : display.slice(lastSep + 2);
172
+ if (normalizeDisplayKey(last) === key) one.push(...ids);
173
+ }
174
+ if (one.length > 0) return { matches: one, matchedForm: "one-segment" };
175
+
176
+ // No match — matchedForm is "" since it is not read when matches is empty.
177
+ return { matches: [], matchedForm: "" };
178
+ }
179
+
180
+ export function fingerprintQuery(
181
+ snap: CapabilitySnapshot,
182
+ filters: FingerprintFilters,
183
+ ): FingerprintQueryResult {
184
+ const indexes = buildFingerprintIndexes(snap);
185
+ const diagnostics: FingerprintQueryDiagnostic[] = [];
186
+
187
+ // Resolve --routine selectors.
188
+ // Consumed by block aggregation below to intersect with the root pool.
189
+ const resolvedSet = new Set<StableRoutineId>();
190
+ let anySelectorFailed = false;
191
+ if (filters.routineSelectors !== undefined && filters.routineSelectors.length > 0) {
192
+ for (const sel of filters.routineSelectors) {
193
+ const { matches, matchedForm } = resolveSelector(sel, indexes);
194
+ if (matches.length === 0) {
195
+ diagnostics.push({
196
+ kind: "selector-unresolved",
197
+ selector: sel,
198
+ triedForms: SELECTOR_FORMS,
199
+ });
200
+ anySelectorFailed = true;
201
+ continue;
202
+ }
203
+ if (matches.length >= 2) {
204
+ diagnostics.push({
205
+ kind: "selector-ambiguous",
206
+ selector: sel,
207
+ matchedForm,
208
+ candidates: matches.slice(0, MAX_AMBIGUOUS_CANDIDATES).map((id) => ({
209
+ stableId: id,
210
+ display: indexes.routineDisplayById.get(id) ?? "",
211
+ })),
212
+ });
213
+ anySelectorFailed = true;
214
+ continue;
215
+ }
216
+ const first = matches[0];
217
+ if (first !== undefined) resolvedSet.add(first);
218
+ }
219
+ }
220
+
221
+ if (anySelectorFailed) {
222
+ return {
223
+ blocks: [],
224
+ diagnostics,
225
+ summary: {
226
+ totalClassifications: snap.rootClassifications.length,
227
+ renderedBlocks: 0,
228
+ rootsConfigIgnored: snap.inputsMetadata?.rootsConfigIgnored === true,
229
+ },
230
+ };
231
+ }
232
+
233
+ // --- Build root pool, filter, aggregate ---
234
+ const rootPool = snap.rootClassifications.filter((r) => {
235
+ const id = r.routineId as unknown as StableRoutineId;
236
+ if (filters.roots !== undefined) {
237
+ const roots = filters.roots;
238
+ const intersects = r.kinds.some((k) => roots.has(k as RootKind));
239
+ if (!intersects) return false;
240
+ }
241
+ if (filters.routineSelectors !== undefined && filters.routineSelectors.length > 0) {
242
+ if (!resolvedSet.has(id)) return false;
243
+ }
244
+ return true;
245
+ });
246
+
247
+ const blocks: FingerprintBlock[] = [];
248
+ for (const root of rootPool) {
249
+ const rid = root.routineId as unknown as StableRoutineId;
250
+ const block = buildBlock(rid, root, snap, indexes, filters);
251
+ if (block !== undefined) blocks.push(block);
252
+ }
253
+ blocks.sort((a, b) => (a.routineId < b.routineId ? -1 : a.routineId > b.routineId ? 1 : 0));
254
+
255
+ return {
256
+ blocks,
257
+ diagnostics,
258
+ summary: {
259
+ totalClassifications: snap.rootClassifications.length,
260
+ renderedBlocks: blocks.length,
261
+ rootsConfigIgnored: snap.inputsMetadata?.rootsConfigIgnored === true,
262
+ },
263
+ };
264
+ }
265
+
266
+ const CAPABILITY_RESOURCE_KIND_ORDER: readonly CapabilityResourceKind[] = [
267
+ "table",
268
+ "event",
269
+ "codeunit",
270
+ "page",
271
+ "report",
272
+ "http",
273
+ "telemetry",
274
+ "isolated-storage",
275
+ "file",
276
+ "transaction",
277
+ "ui",
278
+ "background",
279
+ ];
280
+
281
+ function parseObjectIdFromRoutine(rid: StableRoutineId): StableObjectId | undefined {
282
+ const hashAt = rid.lastIndexOf("#");
283
+ if (hashAt <= 0) return undefined;
284
+ return rid.slice(0, hashAt) as StableObjectId;
285
+ }
286
+
287
+ function splitObjectAndRoutine(display: string): { object: string; routine: string } {
288
+ const idx = display.lastIndexOf("::");
289
+ if (idx < 0) return { object: "", routine: display };
290
+ return { object: display.slice(0, idx), routine: display.slice(idx + 2) };
291
+ }
292
+
293
+ function resolveResourceDisplay(
294
+ fact: CapabilityFact,
295
+ indexes: FingerprintIndexes,
296
+ ): { id?: string; display: string; source?: ValueSource } {
297
+ const raw = fact.resourceId;
298
+ if (raw !== undefined) {
299
+ const display = indexes.stableIdToDisplay.get(String(raw)) ?? String(raw);
300
+ return { id: String(raw), display };
301
+ }
302
+ if (fact.resourceArgSource !== undefined) {
303
+ return { display: renderValueSource(fact.resourceArgSource), source: fact.resourceArgSource };
304
+ }
305
+ return { display: "<unknown>" };
306
+ }
307
+
308
+ function renderValueSource(vs: ValueSource): string {
309
+ // Minimal renderer; extend if richer formatting is needed for
310
+ // table-field / constant-var / expression cases.
311
+ switch (vs.kind) {
312
+ case "literal":
313
+ return vs.value;
314
+ case "enum":
315
+ return vs.member !== undefined ? `${vs.enumName}.${vs.member}` : vs.enumName;
316
+ case "constant-var":
317
+ return vs.varName;
318
+ case "parameter":
319
+ return vs.varName;
320
+ case "table-field":
321
+ return `${String(vs.tableId)}.${vs.fieldName}`;
322
+ case "expression":
323
+ return "<expression>";
324
+ case "unknown":
325
+ return "<unknown>";
326
+ }
327
+ }
328
+
329
+ function buildBlock(
330
+ rid: StableRoutineId,
331
+ root: CapabilitySnapshot["rootClassifications"][number],
332
+ snap: CapabilitySnapshot,
333
+ indexes: FingerprintIndexes,
334
+ filters: FingerprintFilters,
335
+ ): FingerprintBlock | undefined {
336
+ const display = indexes.routineDisplayById.get(rid) ?? String(rid);
337
+ const split = splitObjectAndRoutine(display);
338
+
339
+ const rootFacts = indexes.factsByRoutine.get(rid) ?? [];
340
+ const renderedFacts = filters.includeInherited
341
+ ? rootFacts
342
+ : rootFacts.filter((f) => f.provenance === "direct");
343
+
344
+ const covRec = indexes.coverageByRoutine.get(rid);
345
+ const coverage: BlockCoverage = {
346
+ status: filters.includeInherited
347
+ ? (covRec?.inheritedStatus ?? "unknown")
348
+ : (covRec?.directStatus ?? "unknown"),
349
+ directStatus: covRec?.directStatus ?? "unknown",
350
+ inheritedStatus: covRec?.inheritedStatus ?? "unknown",
351
+ reasons: covRec?.reasons ?? [],
352
+ unknownTargets: covRec?.unknownTargets ?? [],
353
+ unknownTargetDisplays: (covRec?.unknownTargets ?? []).map(
354
+ (t) => indexes.stableIdToDisplay.get(t) ?? t,
355
+ ),
356
+ inheritedExcluded: !filters.includeInherited,
357
+ };
358
+
359
+ // --- families ---
360
+ const byKind = new Map<CapabilityResourceKind, CapabilityFact[]>();
361
+ for (const f of renderedFacts) {
362
+ // Dispatch facts go into the dispatch summary, not a family.
363
+ if (f.op === "execute" && f.extra?.kind === "dispatch") continue;
364
+ const arr = byKind.get(f.resourceKind) ?? [];
365
+ arr.push(f);
366
+ byKind.set(f.resourceKind, arr);
367
+ }
368
+ const families: BlockCapabilityFamily[] = [];
369
+ for (const kind of CAPABILITY_RESOURCE_KIND_ORDER) {
370
+ const facts = byKind.get(kind);
371
+ if (facts === undefined || facts.length === 0) continue;
372
+ const resByDisplay = new Map<string, BlockResource>();
373
+ for (const f of facts) {
374
+ const rd = resolveResourceDisplay(f, indexes);
375
+ const key = rd.id ?? rd.display;
376
+ const existing = resByDisplay.get(key);
377
+ if (existing === undefined) {
378
+ resByDisplay.set(key, {
379
+ id: rd.id,
380
+ display: rd.display,
381
+ source: rd.source,
382
+ confidence: f.confidence,
383
+ ops: [f.op],
384
+ facts: [f],
385
+ });
386
+ } else {
387
+ const opsSet = new Set(existing.ops);
388
+ opsSet.add(f.op);
389
+ resByDisplay.set(key, {
390
+ ...existing,
391
+ ops: [...opsSet],
392
+ facts: [...existing.facts, f],
393
+ });
394
+ }
395
+ }
396
+ const resources = [...resByDisplay.values()].sort((a, b) =>
397
+ a.display < b.display
398
+ ? -1
399
+ : a.display > b.display
400
+ ? 1
401
+ : a.id && b.id
402
+ ? a.id < b.id
403
+ ? -1
404
+ : 1
405
+ : 0,
406
+ );
407
+ families.push({ kind, coneCoverage: coverage.status, resources });
408
+ }
409
+
410
+ // --- mayCommit ---
411
+ const commitFact = renderedFacts.find((f) => f.op === "commit");
412
+ const mayCommit: BlockCommitSummary = {
413
+ presence:
414
+ commitFact !== undefined ? "yes" : coverage.directStatus === "complete" ? "no" : "unknown",
415
+ witnessFact: commitFact,
416
+ };
417
+
418
+ // --- dispatch ---
419
+ const resolvedDisp: DispatchInstance[] = [];
420
+ const unresolvedDisp: DispatchInstance[] = [];
421
+ for (const f of renderedFacts) {
422
+ if (f.op !== "execute") continue;
423
+ if (f.extra?.kind !== "dispatch") continue;
424
+ const extra = f.extra;
425
+ const targetId = f.resourceId !== undefined ? String(f.resourceId) : undefined;
426
+ const targetDisplay =
427
+ targetId !== undefined ? (indexes.stableIdToDisplay.get(targetId) ?? targetId) : undefined;
428
+ const inst: DispatchInstance = {
429
+ objectType: extra.objectType,
430
+ targetId,
431
+ targetDisplay,
432
+ targetIdSource: f.resourceArgSource,
433
+ confidence: f.confidence,
434
+ modal: extra.modal,
435
+ provenance: f.provenance,
436
+ via: f.via,
437
+ witnessCallsiteId: f.witnessCallsiteId,
438
+ };
439
+ if (targetId !== undefined) resolvedDisp.push(inst);
440
+ else unresolvedDisp.push(inst);
441
+ }
442
+ resolvedDisp.sort((a, b) => {
443
+ const aKey = a.targetId ?? "";
444
+ const bKey = b.targetId ?? "";
445
+ if (aKey !== bKey) return aKey < bKey ? -1 : 1;
446
+ return 0;
447
+ });
448
+ unresolvedDisp.sort((a, b) => {
449
+ // Unresolved instances have no targetId; sort by witnessCallsiteId for stability.
450
+ const aKey = String(a.witnessCallsiteId ?? "");
451
+ const bKey = String(b.witnessCallsiteId ?? "");
452
+ if (aKey !== bKey) return aKey < bKey ? -1 : 1;
453
+ return 0;
454
+ });
455
+ const dispatch: BlockDispatchSummary = {
456
+ resolved: resolvedDisp,
457
+ unresolved: unresolvedDisp,
458
+ };
459
+
460
+ // --- required permissions ---
461
+ const permMap = new Map<string, PermissionLine>();
462
+ for (const f of renderedFacts) {
463
+ if (f.resourceKind !== "table") continue;
464
+ if (f.resourceId === undefined) continue;
465
+ const target = String(f.resourceId);
466
+ const targetDisplay = indexes.stableIdToDisplay.get(target) ?? target;
467
+ const key = `table|${target}`;
468
+ const existing = permMap.get(key);
469
+ const right =
470
+ f.op === "read"
471
+ ? "R"
472
+ : f.op === "insert"
473
+ ? "W"
474
+ : f.op === "modify"
475
+ ? "M"
476
+ : f.op === "delete"
477
+ ? "D"
478
+ : "";
479
+ if (right === "") continue;
480
+ if (existing === undefined) {
481
+ permMap.set(key, {
482
+ targetKind: "table",
483
+ targetId: target,
484
+ targetDisplay,
485
+ rights: right,
486
+ coverage: coverage.status,
487
+ });
488
+ } else {
489
+ const rs = new Set(existing.rights.split(""));
490
+ rs.add(right);
491
+ permMap.set(key, { ...existing, rights: [...rs].sort().join("") });
492
+ }
493
+ }
494
+ const requiredPermissions = [...permMap.values()].sort((a, b) =>
495
+ a.targetDisplay < b.targetDisplay ? -1 : a.targetDisplay > b.targetDisplay ? 1 : 0,
496
+ );
497
+
498
+ // --- witnesses ---
499
+ const witnesses: BlockWitness[] = [];
500
+ if (filters.witnessLimit !== false) {
501
+ const witnessIndexes: WitnessIndexes = {
502
+ outgoingEdges: indexes.outgoingEdges,
503
+ directFactsByRoutine: indexes.directFactsByRoutine,
504
+ callsiteById: indexes.callsiteById,
505
+ operationById: indexes.operationById,
506
+ routineDisplayById: indexes.routineDisplayById,
507
+ stableIdToDisplay: indexes.stableIdToDisplay,
508
+ eventDisplayById: indexes.eventDisplayById,
509
+ coverageByRoutine: indexes.coverageByRoutine,
510
+ };
511
+ for (const fact of renderedFacts) {
512
+ const outcome = reconstructWitnessPaths({
513
+ rootId: rid,
514
+ fact,
515
+ limit: filters.witnessLimit,
516
+ indexes: witnessIndexes,
517
+ });
518
+ witnesses.push({
519
+ fact,
520
+ paths: outcome.paths,
521
+ truncated: outcome.truncated,
522
+ incomplete: outcome.incomplete,
523
+ diagnostics: outcome.diagnostics,
524
+ });
525
+ }
526
+ }
527
+
528
+ return {
529
+ routineId: rid,
530
+ objectId: parseObjectIdFromRoutine(rid),
531
+ objectDisplay: split.object,
532
+ routineDisplay: split.routine,
533
+ kinds: root.kinds as readonly RootKind[],
534
+ classificationSource: root.source,
535
+ configEntryId: root.configEntryId,
536
+ coverage,
537
+ families,
538
+ mayCommit,
539
+ dispatch,
540
+ requiredPermissions,
541
+ witnesses,
542
+ };
543
+ }