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.
- package/LICENSE +21 -0
- package/README.md +361 -0
- package/package.json +64 -0
- package/scripts/d40-diff.ts +44 -0
- package/scripts/fetch-native-parser.ts +179 -0
- package/scripts/precision-sample.ts +99 -0
- package/scripts/precision-study.ts +42 -0
- package/scripts/precision-tabulate.ts +52 -0
- package/src/cli/baseline.ts +31 -0
- package/src/cli/diff.ts +199 -0
- package/src/cli/events-chains.ts +56 -0
- package/src/cli/events-fanout.ts +87 -0
- package/src/cli/exit-code.ts +30 -0
- package/src/cli/fingerprint-indexes.ts +130 -0
- package/src/cli/fingerprint-query.ts +543 -0
- package/src/cli/fingerprint-witness.ts +493 -0
- package/src/cli/fingerprint.ts +292 -0
- package/src/cli/format-compact-json.ts +45 -0
- package/src/cli/format-events.ts +77 -0
- package/src/cli/format-fingerprint.ts +295 -0
- package/src/cli/format-html.ts +503 -0
- package/src/cli/format-json.ts +13 -0
- package/src/cli/format-policy.ts +95 -0
- package/src/cli/format-sarif.ts +186 -0
- package/src/cli/format-terminal.ts +153 -0
- package/src/cli/index.ts +566 -0
- package/src/cli/policy.ts +204 -0
- package/src/config/roots-config.ts +302 -0
- package/src/deps/cache-versions.ts +74 -0
- package/src/deps/canonical-json.ts +27 -0
- package/src/deps/dependency-artifact.ts +144 -0
- package/src/deps/dependency-cache.ts +262 -0
- package/src/deps/dependency-dag.ts +128 -0
- package/src/deps/dependency-package-discovery.ts +85 -0
- package/src/deps/dependency-pipeline.ts +483 -0
- package/src/deps/dependency-projection.ts +211 -0
- package/src/deps/dependency-resolver.ts +154 -0
- package/src/deps/workspace-dependencies.ts +114 -0
- package/src/detectors/capability-query.ts +145 -0
- package/src/detectors/confidence.ts +52 -0
- package/src/detectors/d1-db-op-in-loop.ts +457 -0
- package/src/detectors/d10-self-modifying-loop.ts +114 -0
- package/src/detectors/d11-modify-without-get.ts +129 -0
- package/src/detectors/d12-dead-integration-event.ts +81 -0
- package/src/detectors/d13-cross-app-internal-call.ts +105 -0
- package/src/detectors/d14-dead-routine.ts +151 -0
- package/src/detectors/d16-obsolete-routine-call.ts +94 -0
- package/src/detectors/d17-min-version-drift.ts +157 -0
- package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
- package/src/detectors/d19-unused-parameter.ts +116 -0
- package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
- package/src/detectors/d20-unreachable-after-exit.ts +92 -0
- package/src/detectors/d21-read-without-load.ts +128 -0
- package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
- package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
- package/src/detectors/d3-load-state.ts +72 -0
- package/src/detectors/d3-missing-setloadfields.ts +234 -0
- package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
- package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
- package/src/detectors/d34-commit-in-loop.ts +206 -0
- package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
- package/src/detectors/d36-late-setloadfields.ts +162 -0
- package/src/detectors/d37-validate-without-persist.ts +271 -0
- package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
- package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
- package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
- package/src/detectors/d40-transitive-load-missing.ts +217 -0
- package/src/detectors/d41-transitive-filter-loss.ts +200 -0
- package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
- package/src/detectors/d43-event-ishandled-skip.ts +257 -0
- package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
- package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
- package/src/detectors/d5-set-based-opportunity.ts +162 -0
- package/src/detectors/d7-recursive-event-expansion.ts +151 -0
- package/src/detectors/d8-commit-in-transaction.ts +132 -0
- package/src/detectors/d9-transaction-span-summary.ts +107 -0
- package/src/detectors/detector-context.ts +121 -0
- package/src/detectors/finding-grouping.ts +61 -0
- package/src/detectors/path-merge.ts +174 -0
- package/src/detectors/registry.ts +176 -0
- package/src/detectors/table-display.ts +42 -0
- package/src/diff/diff-abi.ts +195 -0
- package/src/diff/diff-capabilities.ts +179 -0
- package/src/diff/diff-engine.ts +146 -0
- package/src/diff/diff-events.ts +323 -0
- package/src/diff/diff-identity.ts +73 -0
- package/src/diff/diff-indexes.ts +199 -0
- package/src/diff/diff-permissions.ts +260 -0
- package/src/diff/diff-policy.ts +101 -0
- package/src/diff/diff-preflight.ts +66 -0
- package/src/diff/diff-renames.ts +104 -0
- package/src/diff/diff-schema.ts +232 -0
- package/src/diff/format-diff.ts +148 -0
- package/src/engine/attribute-parser.ts +50 -0
- package/src/engine/capability-cone.ts +531 -0
- package/src/engine/combined-graph.ts +357 -0
- package/src/engine/control-flow-walker.ts +1317 -0
- package/src/engine/dispatch-sites.ts +199 -0
- package/src/engine/effect-lattice.ts +81 -0
- package/src/engine/entry-points.ts +57 -0
- package/src/engine/event-flow.ts +524 -0
- package/src/engine/event-relay.ts +92 -0
- package/src/engine/op-classification.ts +92 -0
- package/src/engine/path-walker.ts +189 -0
- package/src/engine/reverse-call-graph.ts +23 -0
- package/src/engine/root-classifier-overlay.ts +194 -0
- package/src/engine/root-classifier.ts +135 -0
- package/src/engine/scc.ts +110 -0
- package/src/engine/source-anchor.ts +25 -0
- package/src/engine/summary-context.ts +104 -0
- package/src/engine/summary-engine.ts +296 -0
- package/src/engine/summary-runner.ts +560 -0
- package/src/engine/transaction-spans.ts +112 -0
- package/src/engine/uncertainty-util.ts +54 -0
- package/src/hash.ts +31 -0
- package/src/index/attribute-from-node.ts +141 -0
- package/src/index/callee-from-node.ts +181 -0
- package/src/index/capability/background.ts +90 -0
- package/src/index/capability/commit.ts +44 -0
- package/src/index/capability/dispatch.ts +164 -0
- package/src/index/capability/events.ts +65 -0
- package/src/index/capability/extractor.ts +124 -0
- package/src/index/capability/file-blob.ts +137 -0
- package/src/index/capability/http.ts +159 -0
- package/src/index/capability/hyperlink.ts +60 -0
- package/src/index/capability/isolated-storage.ts +179 -0
- package/src/index/capability/table.ts +113 -0
- package/src/index/capability/telemetry.ts +84 -0
- package/src/index/capability/ui.ts +55 -0
- package/src/index/capability/value-source.ts +202 -0
- package/src/index/expression-from-node.ts +117 -0
- package/src/index/indexer.ts +102 -0
- package/src/index/intraprocedural-body.ts +1467 -0
- package/src/index/intraprocedural-ops.ts +253 -0
- package/src/index/intraprocedural-refs.ts +188 -0
- package/src/index/object-indexer.ts +279 -0
- package/src/index/routine-indexer.ts +282 -0
- package/src/index/routine-signature.ts +46 -0
- package/src/index/variable-indexer.ts +134 -0
- package/src/index/variable-initializer-extractor.ts +155 -0
- package/src/index/variable-type-normalizer.ts +83 -0
- package/src/index.ts +267 -0
- package/src/mcp/server.ts +72 -0
- package/src/mcp/session.ts +49 -0
- package/src/mcp/tools/explain-path.ts +75 -0
- package/src/mcp/tools/get-analysis-health.ts +62 -0
- package/src/mcp/tools/get-finding.ts +47 -0
- package/src/mcp/tools/get-routine-summary.ts +126 -0
- package/src/mcp/tools/list-findings.ts +85 -0
- package/src/mcp/tools/list-hotspots.ts +78 -0
- package/src/mcp/tools/list-rollups.ts +103 -0
- package/src/mcp/tools/validators.ts +25 -0
- package/src/model/attributes.ts +120 -0
- package/src/model/callee.ts +45 -0
- package/src/model/capability.ts +187 -0
- package/src/model/coverage.ts +85 -0
- package/src/model/entities.ts +628 -0
- package/src/model/expression.ts +98 -0
- package/src/model/finding.ts +110 -0
- package/src/model/graph-edge.ts +93 -0
- package/src/model/graph.ts +62 -0
- package/src/model/identity.ts +81 -0
- package/src/model/ids.ts +90 -0
- package/src/model/index.ts +13 -0
- package/src/model/model.ts +51 -0
- package/src/model/permission.ts +76 -0
- package/src/model/root-classification.ts +116 -0
- package/src/model/stable-identity.ts +102 -0
- package/src/model/summary.ts +96 -0
- package/src/parser/ast.ts +82 -0
- package/src/parser/native/ffi.ts +145 -0
- package/src/parser/native/parse-index-pool.ts +148 -0
- package/src/parser/native/parse-index-worker.ts +94 -0
- package/src/parser/native/wrapper.ts +353 -0
- package/src/parser/parser-init.ts +43 -0
- package/src/perf/profiler.ts +66 -0
- package/src/policy/policy-default.yaml +83 -0
- package/src/policy/policy-engine.ts +339 -0
- package/src/policy/policy-loader.ts +257 -0
- package/src/policy/policy-schema.json +379 -0
- package/src/policy/policy-types.ts +81 -0
- package/src/policy/predicate-compiler.ts +151 -0
- package/src/policy/predicate-evaluator.ts +267 -0
- package/src/policy/predicate-fields.ts +439 -0
- package/src/projection/actionable-anchor.ts +48 -0
- package/src/projection/finding-filters.ts +44 -0
- package/src/projection/finding-fingerprint.ts +54 -0
- package/src/projection/finding-groups.ts +41 -0
- package/src/projection/finding-summary.ts +110 -0
- package/src/projection/rollup-findings.ts +105 -0
- package/src/providers/discover.ts +88 -0
- package/src/providers/external.ts +46 -0
- package/src/providers/types.ts +36 -0
- package/src/providers/workspace.ts +117 -0
- package/src/resolve/call-resolver.ts +117 -0
- package/src/resolve/coverage.ts +61 -0
- package/src/resolve/event-graph.ts +166 -0
- package/src/resolve/implicit-edges.ts +53 -0
- package/src/resolve/record-types.ts +36 -0
- package/src/resolve/resolver.ts +23 -0
- package/src/resolve/semantic-graph.ts +29 -0
- package/src/resolve/symbol-table.ts +69 -0
- package/src/snapshot/app-snapshot.ts +74 -0
- package/src/snapshot/compose.ts +100 -0
- package/src/snapshot/derive/callsite-evidence.ts +76 -0
- package/src/snapshot/derive/capability-facts.ts +70 -0
- package/src/snapshot/derive/contracts.ts +131 -0
- package/src/snapshot/derive/coverage.ts +35 -0
- package/src/snapshot/derive/event-declarations.ts +140 -0
- package/src/snapshot/derive/identity-table.ts +58 -0
- package/src/snapshot/derive/inputs.ts +91 -0
- package/src/snapshot/derive/operation-evidence.ts +70 -0
- package/src/snapshot/derive/permissions.ts +186 -0
- package/src/snapshot/derive/root-classifications.ts +56 -0
- package/src/snapshot/derive/schema.ts +130 -0
- package/src/snapshot/derive/typed-edges.ts +60 -0
- package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
- package/src/snapshot/deserialize.ts +40 -0
- package/src/snapshot/serialize-cbor-gz.ts +12 -0
- package/src/snapshot/serialize-cbor.ts +19 -0
- package/src/snapshot/serialize-json.ts +22 -0
- package/src/snapshot/shard.ts +134 -0
- package/src/snapshot/types.ts +181 -0
- package/src/symbols/app-manifest.ts +96 -0
- package/src/symbols/app-package-zip.ts +50 -0
- package/src/symbols/embedded-source-reader.ts +41 -0
- package/src/symbols/package-hash.ts +81 -0
- package/src/symbols/symbol-reader.ts +101 -0
- package/src/symbols/symbol-reference-parser.ts +378 -0
- package/src/symbols/symbol-reference-reader.ts +27 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { RoutineId } from "../model/ids.ts";
|
|
2
|
+
|
|
3
|
+
/** Minimal graph shape tarjanScc needs: sorted nodes + outgoing edges that carry `to`. */
|
|
4
|
+
export interface SccInputGraph {
|
|
5
|
+
nodes: readonly RoutineId[];
|
|
6
|
+
edgesByFrom: ReadonlyMap<RoutineId, readonly { to: RoutineId }[]>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** One strongly-connected component. `recursive` = size > 1 or has a self-edge. */
|
|
10
|
+
export interface Scc {
|
|
11
|
+
members: RoutineId[]; // sorted
|
|
12
|
+
recursive: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SccResult {
|
|
16
|
+
/** SCCs in reverse-topological order: callees before callers. */
|
|
17
|
+
sccs: Scc[];
|
|
18
|
+
/** routineId -> index into `sccs`. */
|
|
19
|
+
sccIdByRoutine: Map<RoutineId, number>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tarjan's SCC algorithm over the combined graph. Iterative (no recursion — AL call graphs
|
|
24
|
+
* can be deep). Tarjan emits SCCs in reverse-topological order naturally, which is exactly
|
|
25
|
+
* the bottom-up order the summary engine wants. Node iteration follows `graph.nodes` (sorted)
|
|
26
|
+
* and edge iteration follows the pre-sorted edge lists, so the result is deterministic.
|
|
27
|
+
*/
|
|
28
|
+
export function tarjanScc(graph: SccInputGraph): SccResult {
|
|
29
|
+
let nextIndex = 0;
|
|
30
|
+
const index = new Map<RoutineId, number>();
|
|
31
|
+
const lowlink = new Map<RoutineId, number>();
|
|
32
|
+
const onStack = new Set<RoutineId>();
|
|
33
|
+
const stack: RoutineId[] = [];
|
|
34
|
+
const rawSccs: RoutineId[][] = [];
|
|
35
|
+
|
|
36
|
+
// Explicit work stack: each frame is a node plus its next-child cursor.
|
|
37
|
+
for (const start of graph.nodes) {
|
|
38
|
+
if (index.has(start)) continue;
|
|
39
|
+
const work: { node: RoutineId; childIdx: number }[] = [{ node: start, childIdx: 0 }];
|
|
40
|
+
|
|
41
|
+
while (work.length > 0) {
|
|
42
|
+
const frame = work[work.length - 1];
|
|
43
|
+
if (frame === undefined) break;
|
|
44
|
+
const { node } = frame;
|
|
45
|
+
|
|
46
|
+
if (frame.childIdx === 0) {
|
|
47
|
+
index.set(node, nextIndex);
|
|
48
|
+
lowlink.set(node, nextIndex);
|
|
49
|
+
nextIndex++;
|
|
50
|
+
stack.push(node);
|
|
51
|
+
onStack.add(node);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const children = graph.edgesByFrom.get(node) ?? [];
|
|
55
|
+
if (frame.childIdx < children.length) {
|
|
56
|
+
const child = children[frame.childIdx];
|
|
57
|
+
frame.childIdx++;
|
|
58
|
+
if (child === undefined) continue;
|
|
59
|
+
const to = child.to;
|
|
60
|
+
if (!index.has(to)) {
|
|
61
|
+
work.push({ node: to, childIdx: 0 });
|
|
62
|
+
} else if (onStack.has(to)) {
|
|
63
|
+
const cur = lowlink.get(node) ?? 0;
|
|
64
|
+
const toIdx = index.get(to) ?? 0;
|
|
65
|
+
lowlink.set(node, Math.min(cur, toIdx));
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// All children processed — settle this node.
|
|
71
|
+
if (lowlink.get(node) === index.get(node)) {
|
|
72
|
+
const members: RoutineId[] = [];
|
|
73
|
+
while (true) {
|
|
74
|
+
const w = stack.pop();
|
|
75
|
+
if (w === undefined) break;
|
|
76
|
+
onStack.delete(w);
|
|
77
|
+
members.push(w);
|
|
78
|
+
if (w === node) break;
|
|
79
|
+
}
|
|
80
|
+
rawSccs.push(members);
|
|
81
|
+
}
|
|
82
|
+
work.pop();
|
|
83
|
+
const parent = work[work.length - 1];
|
|
84
|
+
if (parent !== undefined) {
|
|
85
|
+
const pCur = lowlink.get(parent.node) ?? 0;
|
|
86
|
+
const nCur = lowlink.get(node) ?? 0;
|
|
87
|
+
lowlink.set(parent.node, Math.min(pCur, nCur));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// rawSccs is already in reverse-topological order (Tarjan property).
|
|
93
|
+
const sccs: Scc[] = [];
|
|
94
|
+
const sccIdByRoutine = new Map<RoutineId, number>();
|
|
95
|
+
for (const members of rawSccs) {
|
|
96
|
+
const sorted = [...members].sort();
|
|
97
|
+
let recursive = sorted.length > 1;
|
|
98
|
+
if (!recursive) {
|
|
99
|
+
const only = sorted[0];
|
|
100
|
+
if (only !== undefined) {
|
|
101
|
+
recursive = (graph.edgesByFrom.get(only) ?? []).some((e) => e.to === only);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const sccId = sccs.length;
|
|
105
|
+
sccs.push({ members: sorted, recursive });
|
|
106
|
+
for (const m of sorted) sccIdByRoutine.set(m, sccId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { sccs, sccIdByRoutine };
|
|
110
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-anchor ordering helpers shared by detectors that need to compare positions
|
|
3
|
+
* within a single routine. AL positions are `(startLine, startColumn)` zero-or-one-
|
|
4
|
+
* based pairs (whichever the parser emits — we only compare, never display).
|
|
5
|
+
*
|
|
6
|
+
* Extracted here so detectors d11/d21/d22/d33/d36/d37/d39/d40/d41/d42 share one
|
|
7
|
+
* definition; ten byte-identical copies had drifted into separate files and any
|
|
8
|
+
* future tweak (e.g. ordering by an `index` tiebreaker on equal positions) would
|
|
9
|
+
* have had to land in ten places.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Minimal shape accepted by `beforeAnchor` — just the line/column pair. */
|
|
13
|
+
export interface PositionLike {
|
|
14
|
+
range: { startLine: number; startColumn: number };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Strict "is `a` source-anchor positioned strictly before `b`" predicate.
|
|
19
|
+
* Returns `false` when the positions are equal (so callers can treat the
|
|
20
|
+
* comparison as a half-open "earlier than" check without re-handling ties).
|
|
21
|
+
*/
|
|
22
|
+
export function beforeAnchor(a: PositionLike, b: PositionLike): boolean {
|
|
23
|
+
if (a.range.startLine !== b.range.startLine) return a.range.startLine < b.range.startLine;
|
|
24
|
+
return a.range.startColumn < b.range.startColumn;
|
|
25
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/engine/summary-context.ts
|
|
2
|
+
// One-time O(1) lookup indexes for the per-routine summary fixed-point.
|
|
3
|
+
//
|
|
4
|
+
// The fixed-point composer touches `model.routines`, `model.callGraph`, `model.tables`,
|
|
5
|
+
// `model.eventGraph.events`, and `graph.uncertaintyEdges` once per edge per routine per
|
|
6
|
+
// iteration. Linear `.find()` over those arrays is the dominant cold-run cost on large
|
|
7
|
+
// dependencies (10k+ routines): a 50k-routine app turns each scan into ~50k ops, and
|
|
8
|
+
// composing 50k routines × ~3 edges = ~7.5 billion array scans.
|
|
9
|
+
//
|
|
10
|
+
// This context builds the lookups once at the top of `runSummaries` and threads them
|
|
11
|
+
// through `composeRoutine` / `baseIntraproceduralSummary` / `resolvePublishedEvent` /
|
|
12
|
+
// `computeRecordRoles` / `computeFieldEffects`. No behavior change — pure data
|
|
13
|
+
// access path swap (O(N²) → O(N)).
|
|
14
|
+
|
|
15
|
+
import type { Routine, Table } from "../model/entities.ts";
|
|
16
|
+
import type { CallEdge, EventSymbol } from "../model/graph.ts";
|
|
17
|
+
import type { CallsiteId, EventId, OperationId, RoutineId, TableId } from "../model/ids.ts";
|
|
18
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
19
|
+
import type { CombinedGraph, UncertaintyEdge } from "./combined-graph.ts";
|
|
20
|
+
|
|
21
|
+
export interface SummaryContext {
|
|
22
|
+
routineById: Map<RoutineId, Routine>;
|
|
23
|
+
tableById: Map<TableId, Table>;
|
|
24
|
+
/**
|
|
25
|
+
* CallEdges keyed by `operationId`. The summary engine resolves a published event
|
|
26
|
+
* by following `operationId → CallEdge.to (publisher routine) → EventSymbol`. A
|
|
27
|
+
* single operationId has at most one resolved CallEdge in `model.callGraph`, so a
|
|
28
|
+
* `Map<OperationId, CallEdge>` is sufficient (no duplicates).
|
|
29
|
+
*/
|
|
30
|
+
resolvedCallEdgeByOperation: Map<OperationId, CallEdge>;
|
|
31
|
+
/**
|
|
32
|
+
* CallEdges keyed by `callsiteId`. Used by the c1b exit-effect composition pass
|
|
33
|
+
* to look up the resolved edge for each call site without a full scan.
|
|
34
|
+
*/
|
|
35
|
+
resolvedCallEdgeByCallsite: Map<CallsiteId, CallEdge>;
|
|
36
|
+
/** EventSymbol keyed by `publisherRoutineId` — used to recover the published event id. */
|
|
37
|
+
eventByPublisher: Map<RoutineId, EventSymbol>;
|
|
38
|
+
/**
|
|
39
|
+
* UncertaintyEdges keyed by `from`. composeRoutine previously did
|
|
40
|
+
* `for (ue of graph.uncertaintyEdges) if (ue.from !== routine.id) continue;` — a
|
|
41
|
+
* global scan per routine. With this index the loop only visits its own entries.
|
|
42
|
+
*/
|
|
43
|
+
uncertaintyEdgesByFrom: Map<RoutineId, UncertaintyEdge[]>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build all SummaryContext indexes from a SemanticModel and (optionally) a CombinedGraph.
|
|
48
|
+
* If no graph is passed, `uncertaintyEdgesByFrom` is empty — callers that don't traverse
|
|
49
|
+
* uncertainty edges (e.g. test helpers calling `baseIntraproceduralSummary` directly)
|
|
50
|
+
* pay nothing for it.
|
|
51
|
+
*
|
|
52
|
+
* O(R + T + C + E) where R=routines, T=tables, C=callGraph edges, E=event symbols.
|
|
53
|
+
*/
|
|
54
|
+
export function buildSummaryContext(model: SemanticModel, graph?: CombinedGraph): SummaryContext {
|
|
55
|
+
const routineById = new Map<RoutineId, Routine>();
|
|
56
|
+
for (const r of model.routines) routineById.set(r.id, r);
|
|
57
|
+
|
|
58
|
+
const tableById = new Map<TableId, Table>();
|
|
59
|
+
for (const t of model.tables) tableById.set(t.id, t);
|
|
60
|
+
|
|
61
|
+
// Mirror the old `resolvePublishedEvent` precondition: the matched CallEdge had to
|
|
62
|
+
// have `to !== undefined`. We only index those — unresolved edges can never satisfy
|
|
63
|
+
// the publisher-routine lookup downstream.
|
|
64
|
+
const resolvedCallEdgeByOperation = new Map<OperationId, CallEdge>();
|
|
65
|
+
const resolvedCallEdgeByCallsite = new Map<CallsiteId, CallEdge>();
|
|
66
|
+
for (const ce of model.callGraph) {
|
|
67
|
+
if (ce.to === undefined) continue;
|
|
68
|
+
// `model.callGraph` may have multiple edges per operationId in pathological cases
|
|
69
|
+
// (e.g. interface multi-target). The old `.find()` returned the FIRST match — we
|
|
70
|
+
// preserve that by keeping the first writer and ignoring later ones.
|
|
71
|
+
if (!resolvedCallEdgeByOperation.has(ce.operationId)) {
|
|
72
|
+
resolvedCallEdgeByOperation.set(ce.operationId, ce);
|
|
73
|
+
}
|
|
74
|
+
if (!resolvedCallEdgeByCallsite.has(ce.callsiteId)) {
|
|
75
|
+
resolvedCallEdgeByCallsite.set(ce.callsiteId, ce);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const eventByPublisher = new Map<RoutineId, EventSymbol>();
|
|
80
|
+
for (const sym of model.eventGraph.events) {
|
|
81
|
+
if (sym.publisherRoutineId === undefined) continue;
|
|
82
|
+
if (!eventByPublisher.has(sym.publisherRoutineId)) {
|
|
83
|
+
eventByPublisher.set(sym.publisherRoutineId, sym);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const uncertaintyEdgesByFrom = new Map<RoutineId, UncertaintyEdge[]>();
|
|
88
|
+
if (graph !== undefined) {
|
|
89
|
+
for (const ue of graph.uncertaintyEdges) {
|
|
90
|
+
const list = uncertaintyEdgesByFrom.get(ue.from);
|
|
91
|
+
if (list) list.push(ue);
|
|
92
|
+
else uncertaintyEdgesByFrom.set(ue.from, [ue]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
routineById,
|
|
98
|
+
tableById,
|
|
99
|
+
resolvedCallEdgeByOperation,
|
|
100
|
+
resolvedCallEdgeByCallsite,
|
|
101
|
+
eventByPublisher,
|
|
102
|
+
uncertaintyEdgesByFrom,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type { Routine } from "../model/entities.ts";
|
|
2
|
+
import type { EventId, FieldId, OperationId, RoutineId, TableId } from "../model/ids.ts";
|
|
3
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
4
|
+
import type {
|
|
5
|
+
DbEffect,
|
|
6
|
+
EffectPresence,
|
|
7
|
+
FieldEffectSet,
|
|
8
|
+
RecordRoleSummary,
|
|
9
|
+
RoutineSummary,
|
|
10
|
+
} from "../model/summary.ts";
|
|
11
|
+
import { effectKeyOf, joinPresence } from "./effect-lattice.ts";
|
|
12
|
+
import { classifyOp, isDbTouchingClass, recordFlowRoleOf } from "./op-classification.ts";
|
|
13
|
+
import { type SummaryContext, buildSummaryContext } from "./summary-context.ts";
|
|
14
|
+
import { compareStrings } from "./uncertainty-util.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve which event a call site raises. Phase 1 does not emit `event-publish` operation
|
|
18
|
+
* sites — a published event is a CallEdge whose `to` routine has kind "event-publisher".
|
|
19
|
+
* Returns the EventId of the matching EventSymbol, or undefined if the operation is not an
|
|
20
|
+
* event publish.
|
|
21
|
+
*
|
|
22
|
+
* O(1) variant — used inside the fixed-point composer. The two `Map` lookups replace the
|
|
23
|
+
* old `model.callGraph.find()` + `model.eventGraph.events.find()` pair, which dominated
|
|
24
|
+
* cold-run cost on large dependencies (~50G+ ops on Base App).
|
|
25
|
+
*/
|
|
26
|
+
export function resolvePublishedEventCtx(
|
|
27
|
+
operationId: OperationId,
|
|
28
|
+
ctx: SummaryContext,
|
|
29
|
+
): EventId | undefined {
|
|
30
|
+
const edge = ctx.resolvedCallEdgeByOperation.get(operationId);
|
|
31
|
+
if (edge?.to === undefined) return undefined;
|
|
32
|
+
return ctx.eventByPublisher.get(edge.to)?.id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Public, model-based form kept for callers outside the fixed-point (d2 detector, tests).
|
|
37
|
+
* Builds a one-shot SummaryContext per call — fine for cold call sites, NOT for hot loops.
|
|
38
|
+
*/
|
|
39
|
+
export function resolvePublishedEvent(
|
|
40
|
+
operationId: OperationId,
|
|
41
|
+
model: SemanticModel,
|
|
42
|
+
): EventId | undefined {
|
|
43
|
+
return resolvePublishedEventCtx(operationId, buildSummaryContext(model));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Derive a RecordRoleSummary per record parameter from a routine's intraprocedural
|
|
48
|
+
* features. Field names are resolved to FieldId via the parameter record's table. Unresolved
|
|
49
|
+
* field names are skipped here (they surface as D3 bailouts, not silent drops).
|
|
50
|
+
*
|
|
51
|
+
* O(1) variant — used inside the fixed-point composer. Replaces `model.tables.find()` per
|
|
52
|
+
* record parameter with a single `Map` lookup.
|
|
53
|
+
*/
|
|
54
|
+
export function computeRecordRolesCtx(routine: Routine, ctx: SummaryContext): RecordRoleSummary[] {
|
|
55
|
+
const out: RecordRoleSummary[] = [];
|
|
56
|
+
for (const param of routine.parameters) {
|
|
57
|
+
if (!param.isRecord) continue;
|
|
58
|
+
const recVar = routine.features.recordVariables.find(
|
|
59
|
+
(rv) => rv.isParameter && rv.parameterIndex === param.index,
|
|
60
|
+
);
|
|
61
|
+
if (recVar === undefined) continue; // missing RecordVariable — skip rather than corrupt
|
|
62
|
+
const tableId = recVar.tableId;
|
|
63
|
+
const table = tableId !== undefined ? ctx.tableById.get(tableId) : undefined;
|
|
64
|
+
|
|
65
|
+
const resolveField = (fieldName: string): FieldId | undefined =>
|
|
66
|
+
table?.fields.find((f) => f.name.toLowerCase() === fieldName.toLowerCase())?.id;
|
|
67
|
+
|
|
68
|
+
const recVarName = recVar.name.toLowerCase();
|
|
69
|
+
// Phase 1's FieldAccess does not distinguish read vs. direct-assignment write, so
|
|
70
|
+
// readsFields is "all dot-notation field accesses" on this parameter. Targeted writes
|
|
71
|
+
// (Validate ops) feed writesFields separately below.
|
|
72
|
+
const readsFields: FieldId[] = [];
|
|
73
|
+
const writesFields: FieldId[] = [];
|
|
74
|
+
for (const fa of routine.features.fieldAccesses) {
|
|
75
|
+
if (fa.recordVariableName.toLowerCase() !== recVarName) continue;
|
|
76
|
+
const fid = resolveField(fa.fieldName);
|
|
77
|
+
if (fid !== undefined) readsFields.push(fid);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let mayResetFilters = false;
|
|
81
|
+
let mayChangeLoadFields = false;
|
|
82
|
+
let mayAssignRecord = false;
|
|
83
|
+
let loadsFromDbParam: EffectPresence = "no";
|
|
84
|
+
let initialisesParam: EffectPresence = "no";
|
|
85
|
+
let persistsCurrentRecord: EffectPresence = "no";
|
|
86
|
+
let setBasedDbWrites: EffectPresence = "no";
|
|
87
|
+
let validatesParam: EffectPresence = "no";
|
|
88
|
+
let copiesIntoParam: EffectPresence = "no";
|
|
89
|
+
let resetsFiltersOnParam: EffectPresence = "no";
|
|
90
|
+
|
|
91
|
+
for (const op of routine.features.recordOperations) {
|
|
92
|
+
if (op.recordVariableName.toLowerCase() !== recVarName) continue;
|
|
93
|
+
if (op.op === "Validate") {
|
|
94
|
+
for (const arg of op.fieldArguments ?? []) {
|
|
95
|
+
const fid = resolveField(arg);
|
|
96
|
+
if (fid !== undefined) writesFields.push(fid);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (op.op === "Reset" || op.op === "Copy") mayResetFilters = true;
|
|
100
|
+
if (op.op === "SetLoadFields" || op.op === "AddLoadFields" || op.op === "Reset")
|
|
101
|
+
mayChangeLoadFields = true;
|
|
102
|
+
if (op.op === "Copy" || op.op === "TransferFields") mayAssignRecord = true;
|
|
103
|
+
|
|
104
|
+
// May-fact bootstrap — record-flow op roles.
|
|
105
|
+
const role = recordFlowRoleOf(op.op);
|
|
106
|
+
switch (role) {
|
|
107
|
+
case "loadsFromDb":
|
|
108
|
+
loadsFromDbParam = "yes";
|
|
109
|
+
break;
|
|
110
|
+
case "initialises":
|
|
111
|
+
initialisesParam = "yes";
|
|
112
|
+
break;
|
|
113
|
+
case "persistsCurrent":
|
|
114
|
+
persistsCurrentRecord = "yes";
|
|
115
|
+
break;
|
|
116
|
+
case "setBasedWrite":
|
|
117
|
+
setBasedDbWrites = "yes";
|
|
118
|
+
break;
|
|
119
|
+
case "validates":
|
|
120
|
+
validatesParam = "yes";
|
|
121
|
+
break;
|
|
122
|
+
case "copiesInto":
|
|
123
|
+
copiesIntoParam = "yes";
|
|
124
|
+
break;
|
|
125
|
+
case "resetsFilter":
|
|
126
|
+
resetsFiltersOnParam = "yes";
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// RecordRef / FieldRef / Variant params: detectable from the raw type text.
|
|
132
|
+
const mayUseRecordRef = /\b(RecordRef|FieldRef|Variant)\b/i.test(param.typeText);
|
|
133
|
+
|
|
134
|
+
out.push({
|
|
135
|
+
parameterIndex: param.index,
|
|
136
|
+
tableId: tableId ?? "unknown",
|
|
137
|
+
readsFields: mayUseRecordRef ? "unknown" : [...new Set(readsFields)].sort(),
|
|
138
|
+
writesFields: mayUseRecordRef ? "unknown" : [...new Set(writesFields)].sort(),
|
|
139
|
+
mayResetFilters,
|
|
140
|
+
mayChangeLoadFields,
|
|
141
|
+
mayAssignRecord,
|
|
142
|
+
mayUseRecordRef,
|
|
143
|
+
// Entry requirements — populated in Phase 4.
|
|
144
|
+
requiresLoadedAtEntry: "unknown",
|
|
145
|
+
requiredLoadedFieldsAtEntry: "unknown",
|
|
146
|
+
mutatesBeforeLoad: "unknown",
|
|
147
|
+
// Exit-effect may-facts — populated this phase.
|
|
148
|
+
persistsCurrentRecord,
|
|
149
|
+
setBasedDbWrites,
|
|
150
|
+
validatesParam,
|
|
151
|
+
copiesIntoParam,
|
|
152
|
+
resetsFiltersOnParam,
|
|
153
|
+
// Path-aware exit-effect facts — populated in Phase 6.
|
|
154
|
+
dirtyAtExit: "unknown",
|
|
155
|
+
currentLoadedFieldsAtExit: "unknown",
|
|
156
|
+
// Convenience derivations.
|
|
157
|
+
mutatesParam: joinPresence(
|
|
158
|
+
joinPresence(persistsCurrentRecord, validatesParam),
|
|
159
|
+
copiesIntoParam,
|
|
160
|
+
),
|
|
161
|
+
loadsFromDbParam,
|
|
162
|
+
initialisesParam,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return out.sort((a, b) => a.parameterIndex - b.parameterIndex);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Public, model-based form kept for callers outside the fixed-point (tests, helpers).
|
|
170
|
+
* Builds a one-shot SummaryContext — fine for one-off calls, NOT for hot loops.
|
|
171
|
+
*/
|
|
172
|
+
export function computeRecordRoles(routine: Routine, model: SemanticModel): RecordRoleSummary[] {
|
|
173
|
+
return computeRecordRolesCtx(routine, buildSummaryContext(model));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Build a routine's summary from its OWN intraprocedural features only — no callee
|
|
178
|
+
* composition. Recomputed every fixed-point pass (never inherited), so opaque /
|
|
179
|
+
* parse-incomplete facts are always re-applied.
|
|
180
|
+
*
|
|
181
|
+
* O(1)-lookup variant — used inside the fixed-point composer.
|
|
182
|
+
*/
|
|
183
|
+
export function baseIntraproceduralSummaryCtx(
|
|
184
|
+
routine: Routine,
|
|
185
|
+
ctx: SummaryContext,
|
|
186
|
+
): RoutineSummary {
|
|
187
|
+
const parameterRoles = computeRecordRolesCtx(routine, ctx);
|
|
188
|
+
|
|
189
|
+
// Opaque (.app symbol, no body) — unknown everything; the caller (which has the
|
|
190
|
+
// callsiteId) attaches the opaque-callee uncertainty, not this routine itself.
|
|
191
|
+
if (!routine.bodyAvailable) {
|
|
192
|
+
return {
|
|
193
|
+
routineId: routine.id,
|
|
194
|
+
dbEffects: [],
|
|
195
|
+
inRecursiveCycle: false,
|
|
196
|
+
hasUnresolvedCalls: true,
|
|
197
|
+
// the opaque-callee uncertainty is created at the call site, not here
|
|
198
|
+
uncertainties: [],
|
|
199
|
+
parameterRoles,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Parse-incomplete — body present but unparseable; record the typed uncertainty.
|
|
204
|
+
if (routine.parseIncomplete) {
|
|
205
|
+
return {
|
|
206
|
+
routineId: routine.id,
|
|
207
|
+
dbEffects: [],
|
|
208
|
+
inRecursiveCycle: false,
|
|
209
|
+
hasUnresolvedCalls: true,
|
|
210
|
+
uncertainties: [{ kind: "parse-incomplete", routineId: routine.id }],
|
|
211
|
+
parameterRoles,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Body available + parsed — derive direct facts from the operation stream.
|
|
216
|
+
const dbEffects: DbEffect[] = [];
|
|
217
|
+
|
|
218
|
+
for (const op of routine.features.recordOperations) {
|
|
219
|
+
const cls = classifyOp(op.op);
|
|
220
|
+
if (!isDbTouchingClass(cls)) continue; // state-only / trigger ops do not touch the DB
|
|
221
|
+
const tableId: TableId | "unknown" = op.tableId ?? "unknown";
|
|
222
|
+
const effect: DbEffect = {
|
|
223
|
+
effectKey: effectKeyOf({
|
|
224
|
+
op: op.op,
|
|
225
|
+
tableId,
|
|
226
|
+
operationId: op.id,
|
|
227
|
+
tempState: op.tempState,
|
|
228
|
+
}),
|
|
229
|
+
operationId: op.id,
|
|
230
|
+
op: op.op,
|
|
231
|
+
tableId,
|
|
232
|
+
recordVariableId: op.recordVariableId,
|
|
233
|
+
tempState: op.tempState,
|
|
234
|
+
via: "direct",
|
|
235
|
+
};
|
|
236
|
+
dbEffects.push(effect);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
routineId: routine.id,
|
|
241
|
+
dbEffects: dbEffects.sort((a, b) => compareStrings(a.effectKey, b.effectKey)),
|
|
242
|
+
inRecursiveCycle: false,
|
|
243
|
+
hasUnresolvedCalls: false,
|
|
244
|
+
uncertainties: [],
|
|
245
|
+
parameterRoles,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Public, model-based form. Builds a one-shot SummaryContext per call — fine for tests,
|
|
251
|
+
* NOT for hot loops. The fixed-point composer uses `baseIntraproceduralSummaryCtx` directly.
|
|
252
|
+
*/
|
|
253
|
+
export function baseIntraproceduralSummary(routine: Routine, model: SemanticModel): RoutineSummary {
|
|
254
|
+
return baseIntraproceduralSummaryCtx(routine, buildSummaryContext(model));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Orchestration moved to summary-runner.ts in Phase 2c. Re-exported so existing import
|
|
258
|
+
// sites (src/index.ts, detector tests) keep importing `computeSummaries` from here.
|
|
259
|
+
export { computeSummaries } from "./summary-runner.ts";
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Lazy field-effects seam — compute the field-effect set for one routine on demand.
|
|
263
|
+
*
|
|
264
|
+
* This function is intentionally NOT yet consumed by D3. D3 requires window-scoped field
|
|
265
|
+
* accesses (accesses that occur between a retrieval op and the next invalidating op), which
|
|
266
|
+
* this routine-level snapshot cannot provide. `computeFieldEffects` is kept as the interface
|
|
267
|
+
* for future on-demand field-effect queries once those callers exist; do not wire it into D3
|
|
268
|
+
* until the window-scoping concern is resolved.
|
|
269
|
+
*
|
|
270
|
+
* Groups resolved field reads by record-variable name.
|
|
271
|
+
*/
|
|
272
|
+
export function computeFieldEffectsCtx(routineId: RoutineId, ctx: SummaryContext): FieldEffectSet {
|
|
273
|
+
const routine = ctx.routineById.get(routineId);
|
|
274
|
+
const readsByRecordVariable: Record<string, string[]> = {};
|
|
275
|
+
if (routine === undefined) return { readsByRecordVariable };
|
|
276
|
+
|
|
277
|
+
for (const fa of routine.features.fieldAccesses) {
|
|
278
|
+
const recVar = routine.features.recordVariables.find(
|
|
279
|
+
(rv) => rv.name.toLowerCase() === fa.recordVariableName.toLowerCase(),
|
|
280
|
+
);
|
|
281
|
+
const table = recVar?.tableId !== undefined ? ctx.tableById.get(recVar.tableId) : undefined;
|
|
282
|
+
const fieldId = table?.fields.find(
|
|
283
|
+
(f) => f.name.toLowerCase() === fa.fieldName.toLowerCase(),
|
|
284
|
+
)?.id;
|
|
285
|
+
if (fieldId === undefined) continue;
|
|
286
|
+
const key = fa.recordVariableName;
|
|
287
|
+
const list = readsByRecordVariable[key] ?? [];
|
|
288
|
+
if (!list.includes(fieldId)) list.push(fieldId);
|
|
289
|
+
readsByRecordVariable[key] = list.sort();
|
|
290
|
+
}
|
|
291
|
+
return { readsByRecordVariable };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function computeFieldEffects(routineId: RoutineId, model: SemanticModel): FieldEffectSet {
|
|
295
|
+
return computeFieldEffectsCtx(routineId, buildSummaryContext(model));
|
|
296
|
+
}
|