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,357 @@
|
|
|
1
|
+
import type { ObjectRunKind } from "../model/callee.ts";
|
|
2
|
+
import type { ValueSource } from "../model/capability.ts";
|
|
3
|
+
import type { CallSite } from "../model/entities.ts";
|
|
4
|
+
import type { GraphEdge } from "../model/graph-edge.ts";
|
|
5
|
+
import type { DispatchKind, ResolutionQuality } from "../model/graph.ts";
|
|
6
|
+
import type { CallsiteId, EventId, ObjectId, OperationId, RoutineId } from "../model/ids.ts";
|
|
7
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
8
|
+
import type { Uncertainty } from "../model/summary.ts";
|
|
9
|
+
import { compareStrings } from "./uncertainty-util.ts";
|
|
10
|
+
|
|
11
|
+
/** The origin kind of a combined-graph edge. */
|
|
12
|
+
export type CombinedEdgeKind =
|
|
13
|
+
| "direct"
|
|
14
|
+
| "method"
|
|
15
|
+
| "codeunit-run"
|
|
16
|
+
| "report-run"
|
|
17
|
+
| "page-run"
|
|
18
|
+
| "interface"
|
|
19
|
+
| "implicit-trigger"
|
|
20
|
+
| "event-dispatch"
|
|
21
|
+
| "dynamic";
|
|
22
|
+
|
|
23
|
+
/** A resolved routine -> routine edge in the combined graph. */
|
|
24
|
+
export interface CombinedEdge {
|
|
25
|
+
from: RoutineId;
|
|
26
|
+
to: RoutineId;
|
|
27
|
+
kind: CombinedEdgeKind;
|
|
28
|
+
callsiteId?: CallsiteId; // present for call-derived edges
|
|
29
|
+
operationId?: OperationId; // present for call-derived edges
|
|
30
|
+
eventId?: EventId; // present for event-dispatch edges
|
|
31
|
+
subscriberAppId?: string; // present for event-dispatch edges
|
|
32
|
+
resolution: ResolutionQuality;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** An uncertainty attached to a routine because one of its call sites had no resolved target. */
|
|
36
|
+
export interface UncertaintyEdge {
|
|
37
|
+
from: RoutineId;
|
|
38
|
+
uncertainty: Uncertainty;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The combined call + event + implicit-trigger graph the summary engine and path-walker traverse. */
|
|
42
|
+
export interface CombinedGraph {
|
|
43
|
+
nodes: RoutineId[]; // sorted
|
|
44
|
+
edgesByFrom: Map<RoutineId, CombinedEdge[]>; // each list sorted
|
|
45
|
+
uncertaintyEdges: UncertaintyEdge[]; // sorted
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The object-run dispatch kinds that map to typed GraphEdge object-run variants. */
|
|
49
|
+
const OBJECT_RUN_KINDS: ReadonlySet<DispatchKind> = new Set<DispatchKind>([
|
|
50
|
+
"codeunit-run",
|
|
51
|
+
"page-run",
|
|
52
|
+
"report-run",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/** Map a call-graph dispatchKind to the GraphEdge objectType field. */
|
|
56
|
+
function dispatchKindToObjectType(
|
|
57
|
+
dispatchKind: DispatchKind,
|
|
58
|
+
): "Codeunit" | "Page" | "Report" | undefined {
|
|
59
|
+
if (dispatchKind === "codeunit-run") return "Codeunit";
|
|
60
|
+
if (dispatchKind === "page-run") return "Page";
|
|
61
|
+
if (dispatchKind === "report-run") return "Report";
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Build a ValueSource for an object-run callsite's target id. */
|
|
66
|
+
function objectRunTargetIdSource(callSite: CallSite): ValueSource {
|
|
67
|
+
if (callSite.callee.kind !== "object-run") return { kind: "unknown" };
|
|
68
|
+
const c = callSite.callee;
|
|
69
|
+
if (c.targetRef === undefined) {
|
|
70
|
+
// Dynamic — no static info about which object
|
|
71
|
+
return { kind: "expression" };
|
|
72
|
+
}
|
|
73
|
+
if (c.targetIsName) {
|
|
74
|
+
// Named object — treat as an enum reference (objectKind::"name")
|
|
75
|
+
return { kind: "enum", enumName: c.objectKind, member: c.targetRef };
|
|
76
|
+
}
|
|
77
|
+
// Numeric object id — literal
|
|
78
|
+
return { kind: "literal", value: c.targetRef };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Derive the target ObjectId for an object-run callee when the targetRef is
|
|
83
|
+
* statically known but the routine entry wasn't resolved (opaque dep). Returns
|
|
84
|
+
* undefined when the target is fully dynamic.
|
|
85
|
+
*/
|
|
86
|
+
function objectRunTargetObject(
|
|
87
|
+
callSite: CallSite,
|
|
88
|
+
objectsByTypeNumber: Map<string, ObjectId>,
|
|
89
|
+
objectsByTypeName: Map<string, ObjectId>,
|
|
90
|
+
): ObjectId | undefined {
|
|
91
|
+
if (callSite.callee.kind !== "object-run") return undefined;
|
|
92
|
+
const c = callSite.callee;
|
|
93
|
+
if (c.targetRef === undefined) return undefined;
|
|
94
|
+
if (c.targetIsName) {
|
|
95
|
+
return objectsByTypeName.get(`${c.targetType.toLowerCase()}/${c.targetRef.toLowerCase()}`);
|
|
96
|
+
}
|
|
97
|
+
return objectsByTypeNumber.get(`${c.targetType.toLowerCase()}/${c.targetRef}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// CallGraph dispatchKinds that become resolved routine->routine edges (when `to` is set).
|
|
101
|
+
const EDGE_KINDS: ReadonlySet<DispatchKind> = new Set<DispatchKind>([
|
|
102
|
+
"direct",
|
|
103
|
+
"method",
|
|
104
|
+
"codeunit-run",
|
|
105
|
+
"report-run",
|
|
106
|
+
"page-run",
|
|
107
|
+
"interface",
|
|
108
|
+
"implicit-trigger",
|
|
109
|
+
"dynamic",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
function edgeSortKey(e: CombinedEdge): string {
|
|
113
|
+
return `${e.kind}|${e.callsiteId ?? e.operationId ?? e.eventId ?? ""}|${e.to}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function uncertaintySortKey(ue: UncertaintyEdge): string {
|
|
117
|
+
const u = ue.uncertainty;
|
|
118
|
+
const ref = "callsiteId" in u ? u.callsiteId : "operationId" in u ? u.operationId : u.routineId;
|
|
119
|
+
return `${ue.from}|${u.kind}|${ref}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build the typed `GraphEdge[]` for the Phase 0b-β typed edge layer.
|
|
124
|
+
*
|
|
125
|
+
* Kinds emitted here:
|
|
126
|
+
* direct-call — resolved bare/direct procedure→procedure call
|
|
127
|
+
* object-run-resolved — Codeunit/Page/Report.Run with a resolved entry routine
|
|
128
|
+
* object-run-unresolved — Codeunit/Page/Report.Run with dynamic or opaque target,
|
|
129
|
+
* or a `dynamic` dispatchKind (variable-typed Run)
|
|
130
|
+
* event-dispatch — composed publisher→subscriber edge (Task 20)
|
|
131
|
+
*
|
|
132
|
+
* Implicit-trigger and dependency-export edges are deferred to Tasks 21+.
|
|
133
|
+
*/
|
|
134
|
+
function buildTypedEdges(model: SemanticModel): GraphEdge[] {
|
|
135
|
+
// --- look-up maps built from the model ---
|
|
136
|
+
// callsite id → CallSite (for sourceAnchor + callee details)
|
|
137
|
+
const callSiteById = new Map<CallsiteId, CallSite>();
|
|
138
|
+
for (const routine of model.routines) {
|
|
139
|
+
for (const cs of routine.features.callSites) {
|
|
140
|
+
callSiteById.set(cs.id, cs);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// routine id → Routine (for sourceAnchor on publisher/subscriber)
|
|
144
|
+
const routineById = new Map<RoutineId, (typeof model.routines)[number]>();
|
|
145
|
+
for (const routine of model.routines) {
|
|
146
|
+
routineById.set(routine.id, routine);
|
|
147
|
+
}
|
|
148
|
+
// routine id → objectId (for resolved object-run target)
|
|
149
|
+
const objectIdByRoutineId = new Map<RoutineId, ObjectId>();
|
|
150
|
+
for (const routine of model.routines) {
|
|
151
|
+
objectIdByRoutineId.set(routine.id, routine.objectId);
|
|
152
|
+
}
|
|
153
|
+
// object type+number / type+name → objectId (for unresolved object-run target)
|
|
154
|
+
const objectsByTypeNumber = new Map<string, ObjectId>();
|
|
155
|
+
const objectsByTypeName = new Map<string, ObjectId>();
|
|
156
|
+
for (const obj of model.objects) {
|
|
157
|
+
objectsByTypeNumber.set(`${obj.objectType.toLowerCase()}/${obj.objectNumber}`, obj.id);
|
|
158
|
+
objectsByTypeName.set(`${obj.objectType.toLowerCase()}/${obj.name.toLowerCase()}`, obj.id);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const typedEdges: GraphEdge[] = [];
|
|
162
|
+
|
|
163
|
+
for (const ce of model.callGraph) {
|
|
164
|
+
if (ce.dispatchKind === "event-dispatch" || ce.dispatchKind === "implicit-trigger") continue;
|
|
165
|
+
|
|
166
|
+
const callSite = callSiteById.get(ce.callsiteId);
|
|
167
|
+
if (callSite === undefined) continue; // opaque (dependency-only) callsite — skip
|
|
168
|
+
|
|
169
|
+
const sourceAnchor = callSite.sourceAnchor;
|
|
170
|
+
|
|
171
|
+
if (ce.to !== undefined) {
|
|
172
|
+
// Resolved edges
|
|
173
|
+
if (ce.dispatchKind === "direct" || ce.dispatchKind === "method") {
|
|
174
|
+
typedEdges.push({
|
|
175
|
+
kind: "direct-call",
|
|
176
|
+
callsiteId: ce.callsiteId,
|
|
177
|
+
from: ce.from,
|
|
178
|
+
to: ce.to,
|
|
179
|
+
sourceAnchor,
|
|
180
|
+
});
|
|
181
|
+
} else if (OBJECT_RUN_KINDS.has(ce.dispatchKind)) {
|
|
182
|
+
const objectType = dispatchKindToObjectType(ce.dispatchKind);
|
|
183
|
+
if (objectType === undefined) continue;
|
|
184
|
+
// Resolved: target routine is known; derive its object id
|
|
185
|
+
const targetObject = objectIdByRoutineId.get(ce.to);
|
|
186
|
+
if (targetObject === undefined) continue; // should not happen in a sound model
|
|
187
|
+
typedEdges.push({
|
|
188
|
+
kind: "object-run-resolved",
|
|
189
|
+
callsiteId: ce.callsiteId,
|
|
190
|
+
from: ce.from,
|
|
191
|
+
to: ce.to,
|
|
192
|
+
targetObject,
|
|
193
|
+
objectType,
|
|
194
|
+
sourceAnchor,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// interface / unresolved with a `to` — skip (should be rare; not a typed kind yet)
|
|
198
|
+
} else {
|
|
199
|
+
// Unresolved edges
|
|
200
|
+
if (OBJECT_RUN_KINDS.has(ce.dispatchKind)) {
|
|
201
|
+
// Static target exists but the entry routine isn't in the index (opaque dep)
|
|
202
|
+
const objectType = dispatchKindToObjectType(ce.dispatchKind);
|
|
203
|
+
if (objectType === undefined) continue;
|
|
204
|
+
const targetIdSource = objectRunTargetIdSource(callSite);
|
|
205
|
+
const targetObject = objectRunTargetObject(
|
|
206
|
+
callSite,
|
|
207
|
+
objectsByTypeNumber,
|
|
208
|
+
objectsByTypeName,
|
|
209
|
+
);
|
|
210
|
+
typedEdges.push({
|
|
211
|
+
kind: "object-run-unresolved",
|
|
212
|
+
callsiteId: ce.callsiteId,
|
|
213
|
+
from: ce.from,
|
|
214
|
+
...(targetObject !== undefined ? { targetObject } : {}),
|
|
215
|
+
targetIdSource,
|
|
216
|
+
objectType,
|
|
217
|
+
sourceAnchor,
|
|
218
|
+
});
|
|
219
|
+
} else if (ce.dispatchKind === "dynamic") {
|
|
220
|
+
// Dynamic dispatch — we don't know objectType without checking the callee
|
|
221
|
+
if (callSite.callee.kind === "object-run") {
|
|
222
|
+
const objectType = callSite.callee.objectKind as "Codeunit" | "Page" | "Report";
|
|
223
|
+
const targetIdSource = objectRunTargetIdSource(callSite);
|
|
224
|
+
typedEdges.push({
|
|
225
|
+
kind: "object-run-unresolved",
|
|
226
|
+
callsiteId: ce.callsiteId,
|
|
227
|
+
from: ce.from,
|
|
228
|
+
targetIdSource,
|
|
229
|
+
objectType,
|
|
230
|
+
sourceAnchor,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// dynamic member call (method dispatch) — not an object-run; skip
|
|
234
|
+
}
|
|
235
|
+
// interface / unresolved bare call — skip (not yet a typed kind)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- event-dispatch edges (Task 20): bipartite publisher → subscriber composition ---
|
|
240
|
+
const subsByEventTyped = new Map<EventId, typeof model.eventGraph.edges>();
|
|
241
|
+
for (const ee of model.eventGraph.edges) {
|
|
242
|
+
const list = subsByEventTyped.get(ee.eventId);
|
|
243
|
+
if (list) list.push(ee);
|
|
244
|
+
else subsByEventTyped.set(ee.eventId, [ee]);
|
|
245
|
+
}
|
|
246
|
+
for (const sym of model.eventGraph.events) {
|
|
247
|
+
if (sym.publisherRoutineId === undefined) continue;
|
|
248
|
+
const publisherRoutine = routineById.get(sym.publisherRoutineId);
|
|
249
|
+
if (publisherRoutine === undefined) continue;
|
|
250
|
+
for (const ee of subsByEventTyped.get(sym.id) ?? []) {
|
|
251
|
+
const subscriberRoutine = routineById.get(ee.subscriberRoutineId);
|
|
252
|
+
if (subscriberRoutine === undefined) continue;
|
|
253
|
+
typedEdges.push({
|
|
254
|
+
kind: "event-dispatch",
|
|
255
|
+
from: sym.publisherRoutineId,
|
|
256
|
+
to: ee.subscriberRoutineId,
|
|
257
|
+
eventId: sym.id,
|
|
258
|
+
publishAnchor: publisherRoutine.sourceAnchor,
|
|
259
|
+
subscriberAnchor: subscriberRoutine.sourceAnchor,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return typedEdges;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Build the combined graph + populate `model.typedEdges` as a side effect.
|
|
269
|
+
*
|
|
270
|
+
* Idempotent on already-populated models — `analyzeWorkspace` calls this
|
|
271
|
+
* before returning the model, so downstream consumers receive a populated
|
|
272
|
+
* `model.typedEdges`. Re-calling produces an identical result; the call is
|
|
273
|
+
* safe but wasteful. Don't re-invoke from CLI runners or detectors.
|
|
274
|
+
*
|
|
275
|
+
* Resolved CallEdges (and event-dispatch hops derived from the event graph)
|
|
276
|
+
* become CombinedEdges; to-less CallEdges become UncertaintyEdges. The
|
|
277
|
+
* `event-dispatch` dispatchKind on CallEdges is intentionally skipped here —
|
|
278
|
+
* event hops are generated once from `model.eventGraph` to avoid double counting.
|
|
279
|
+
*/
|
|
280
|
+
export function buildCombinedGraph(model: SemanticModel): CombinedGraph {
|
|
281
|
+
const edges: CombinedEdge[] = [];
|
|
282
|
+
const uncertaintyEdges: UncertaintyEdge[] = [];
|
|
283
|
+
|
|
284
|
+
// --- call-derived edges + uncertainty records ---
|
|
285
|
+
for (const ce of model.callGraph) {
|
|
286
|
+
if (ce.dispatchKind === "event-dispatch") continue; // event hops come from the event graph
|
|
287
|
+
if (ce.to !== undefined) {
|
|
288
|
+
if (EDGE_KINDS.has(ce.dispatchKind)) {
|
|
289
|
+
edges.push({
|
|
290
|
+
from: ce.from,
|
|
291
|
+
to: ce.to,
|
|
292
|
+
kind: ce.dispatchKind as CombinedEdgeKind,
|
|
293
|
+
callsiteId: ce.callsiteId,
|
|
294
|
+
operationId: ce.operationId,
|
|
295
|
+
resolution: ce.resolution,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
// to-less edge -> typed uncertainty on the `from` routine
|
|
301
|
+
if (ce.dispatchKind === "interface") {
|
|
302
|
+
uncertaintyEdges.push({
|
|
303
|
+
from: ce.from,
|
|
304
|
+
uncertainty: { kind: "interface-dispatch", callsiteId: ce.callsiteId },
|
|
305
|
+
});
|
|
306
|
+
} else if (ce.dispatchKind === "dynamic") {
|
|
307
|
+
uncertaintyEdges.push({
|
|
308
|
+
from: ce.from,
|
|
309
|
+
uncertainty: { kind: "dynamic-dispatch", operationId: ce.operationId },
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
uncertaintyEdges.push({
|
|
313
|
+
from: ce.from,
|
|
314
|
+
uncertainty: { kind: "unresolved-call", callsiteId: ce.callsiteId },
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// --- event-dispatch edges: publisher routine -> subscriber routine ---
|
|
320
|
+
const subsByEvent = new Map<EventId, typeof model.eventGraph.edges>();
|
|
321
|
+
for (const ee of model.eventGraph.edges) {
|
|
322
|
+
const list = subsByEvent.get(ee.eventId);
|
|
323
|
+
if (list) list.push(ee);
|
|
324
|
+
else subsByEvent.set(ee.eventId, [ee]);
|
|
325
|
+
}
|
|
326
|
+
for (const sym of model.eventGraph.events) {
|
|
327
|
+
if (sym.publisherRoutineId === undefined) continue;
|
|
328
|
+
for (const ee of subsByEvent.get(sym.id) ?? []) {
|
|
329
|
+
edges.push({
|
|
330
|
+
from: sym.publisherRoutineId,
|
|
331
|
+
to: ee.subscriberRoutineId,
|
|
332
|
+
kind: "event-dispatch",
|
|
333
|
+
eventId: sym.id,
|
|
334
|
+
subscriberAppId: ee.subscriberAppId,
|
|
335
|
+
resolution: ee.resolution,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- assemble: sorted nodes, sorted edge lists, sorted uncertainty edges ---
|
|
341
|
+
const nodes = model.routines.map((r) => r.id).sort();
|
|
342
|
+
const edgesByFrom = new Map<RoutineId, CombinedEdge[]>();
|
|
343
|
+
for (const e of edges) {
|
|
344
|
+
const list = edgesByFrom.get(e.from);
|
|
345
|
+
if (list) list.push(e);
|
|
346
|
+
else edgesByFrom.set(e.from, [e]);
|
|
347
|
+
}
|
|
348
|
+
for (const list of edgesByFrom.values()) {
|
|
349
|
+
list.sort((a, b) => compareStrings(edgeSortKey(a), edgeSortKey(b)));
|
|
350
|
+
}
|
|
351
|
+
uncertaintyEdges.sort((a, b) => compareStrings(uncertaintySortKey(a), uncertaintySortKey(b)));
|
|
352
|
+
|
|
353
|
+
// --- typed GraphEdge[] (Phase 0b-β Task 19) — attached to the model in-place ---
|
|
354
|
+
model.typedEdges = buildTypedEdges(model);
|
|
355
|
+
|
|
356
|
+
return { nodes, edgesByFrom, uncertaintyEdges };
|
|
357
|
+
}
|