@optave/codegraph 3.11.2 → 3.13.0
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/README.md +73 -37
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +2 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/batch.d.ts.map +1 -1
- package/dist/cli/commands/batch.js +1 -0
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -1
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/config.d.ts +3 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +272 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/triage.js +1 -1
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/shared/options.d.ts +2 -1
- package/dist/cli/shared/options.d.ts.map +1 -1
- package/dist/cli/shared/options.js +11 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +8 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts +2 -0
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +24 -2
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +16 -10
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
- package/dist/domain/graph/builder/call-resolver.js +251 -34
- package/dist/domain/graph/builder/call-resolver.js.map +1 -1
- package/dist/domain/graph/builder/cha.d.ts +69 -0
- package/dist/domain/graph/builder/cha.d.ts.map +1 -0
- package/dist/domain/graph/builder/cha.js +158 -0
- package/dist/domain/graph/builder/cha.js.map +1 -0
- package/dist/domain/graph/builder/context.d.ts +3 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +2 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +25 -1
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +178 -5
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +74 -2
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +37 -2
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +704 -34
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +3 -2
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +4 -0
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.js +783 -37
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +10 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/journal.js +1 -1
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/resolver/points-to.d.ts +53 -0
- package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
- package/dist/domain/graph/resolver/points-to.js +213 -0
- package/dist/domain/graph/resolver/points-to.js.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.js +476 -0
- package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
- package/dist/domain/parser.d.ts +12 -4
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +83 -20
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +35 -2
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
- package/dist/domain/wasm-worker-pool.js +34 -0
- package/dist/domain/wasm-worker-pool.js.map +1 -1
- package/dist/domain/wasm-worker-protocol.d.ts +15 -1
- package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
- package/dist/extractors/c.js +3 -3
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/clojure.js +1 -1
- package/dist/extractors/clojure.js.map +1 -1
- package/dist/extractors/cpp.d.ts.map +1 -1
- package/dist/extractors/cpp.js +45 -4
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/csharp.d.ts.map +1 -1
- package/dist/extractors/csharp.js +37 -8
- package/dist/extractors/csharp.js.map +1 -1
- package/dist/extractors/cuda.d.ts.map +1 -1
- package/dist/extractors/cuda.js +45 -4
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +6 -6
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/fsharp.js +1 -1
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/go.js +5 -5
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/haskell.js +1 -1
- package/dist/extractors/haskell.js.map +1 -1
- package/dist/extractors/helpers.d.ts +11 -0
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +40 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +10 -9
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts +2 -0
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +1812 -71
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/kotlin.js +5 -5
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/lua.js +1 -1
- package/dist/extractors/lua.js.map +1 -1
- package/dist/extractors/objc.js +3 -3
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/ocaml.js +1 -1
- package/dist/extractors/ocaml.js.map +1 -1
- package/dist/extractors/php.js +2 -2
- package/dist/extractors/php.js.map +1 -1
- package/dist/extractors/python.js +7 -7
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/ruby.js +2 -2
- package/dist/extractors/ruby.js.map +1 -1
- package/dist/extractors/scala.js +1 -1
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.js +1 -1
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/swift.js +4 -4
- package/dist/extractors/swift.js.map +1 -1
- package/dist/extractors/zig.js +4 -4
- package/dist/extractors/zig.js.map +1 -1
- package/dist/features/structure-query.d.ts +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +6 -6
- package/dist/features/structure-query.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/config.d.ts +85 -2
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +408 -19
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/infrastructure/native.d.ts +11 -0
- package/dist/infrastructure/native.d.ts.map +1 -1
- package/dist/infrastructure/native.js +78 -5
- package/dist/infrastructure/native.js.map +1 -1
- package/dist/infrastructure/registry.d.ts +27 -0
- package/dist/infrastructure/registry.d.ts.map +1 -1
- package/dist/infrastructure/registry.js +59 -1
- package/dist/infrastructure/registry.js.map +1 -1
- package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
- package/dist/presentation/queries-cli/overview.js +5 -0
- package/dist/presentation/queries-cli/overview.js.map +1 -1
- package/dist/presentation/structure.d.ts +1 -1
- package/dist/presentation/structure.d.ts.map +1 -1
- package/dist/presentation/structure.js +2 -2
- package/dist/presentation/structure.js.map +1 -1
- package/dist/types.d.ts +221 -0
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +7 -8
- package/src/cli/commands/audit.ts +2 -1
- package/src/cli/commands/batch.ts +1 -0
- package/src/cli/commands/build.ts +6 -1
- package/src/cli/commands/config.ts +353 -0
- package/src/cli/commands/triage.ts +1 -1
- package/src/cli/index.ts +10 -0
- package/src/cli/shared/options.ts +11 -1
- package/src/cli/types.ts +2 -0
- package/src/db/migrations.ts +8 -1
- package/src/domain/analysis/module-map.ts +29 -1
- package/src/domain/graph/builder/call-resolver.ts +263 -35
- package/src/domain/graph/builder/cha.ts +192 -0
- package/src/domain/graph/builder/context.ts +3 -0
- package/src/domain/graph/builder/helpers.ts +195 -5
- package/src/domain/graph/builder/incremental.ts +80 -1
- package/src/domain/graph/builder/pipeline.ts +49 -2
- package/src/domain/graph/builder/stages/build-edges.ts +867 -32
- package/src/domain/graph/builder/stages/detect-changes.ts +4 -2
- package/src/domain/graph/builder/stages/finalize.ts +4 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +910 -43
- package/src/domain/graph/builder/stages/resolve-imports.ts +15 -1
- package/src/domain/graph/journal.ts +1 -1
- package/src/domain/graph/resolver/points-to.ts +254 -0
- package/src/domain/graph/resolver/ts-resolver.ts +536 -0
- package/src/domain/parser.ts +86 -17
- package/src/domain/wasm-worker-entry.ts +35 -2
- package/src/domain/wasm-worker-pool.ts +22 -0
- package/src/domain/wasm-worker-protocol.ts +15 -0
- package/src/extractors/c.ts +3 -3
- package/src/extractors/clojure.ts +1 -1
- package/src/extractors/cpp.ts +47 -4
- package/src/extractors/csharp.ts +33 -9
- package/src/extractors/cuda.ts +47 -4
- package/src/extractors/elixir.ts +6 -6
- package/src/extractors/fsharp.ts +1 -1
- package/src/extractors/go.ts +5 -5
- package/src/extractors/haskell.ts +1 -1
- package/src/extractors/helpers.ts +43 -0
- package/src/extractors/java.ts +10 -9
- package/src/extractors/javascript.ts +1929 -72
- package/src/extractors/kotlin.ts +5 -5
- package/src/extractors/lua.ts +1 -1
- package/src/extractors/objc.ts +3 -3
- package/src/extractors/ocaml.ts +1 -1
- package/src/extractors/php.ts +2 -2
- package/src/extractors/python.ts +7 -7
- package/src/extractors/ruby.ts +2 -2
- package/src/extractors/scala.ts +1 -1
- package/src/extractors/solidity.ts +1 -1
- package/src/extractors/swift.ts +4 -4
- package/src/extractors/zig.ts +4 -4
- package/src/features/structure-query.ts +7 -7
- package/src/index.ts +5 -1
- package/src/infrastructure/config.ts +494 -20
- package/src/infrastructure/native.ts +87 -5
- package/src/infrastructure/registry.ts +82 -1
- package/src/presentation/queries-cli/overview.ts +15 -1
- package/src/presentation/structure.ts +3 -3
- package/src/types.ts +235 -0
- package/grammars/tree-sitter-erlang.wasm +0 -0
|
@@ -23,8 +23,46 @@ export interface CallNodeLookup {
|
|
|
23
23
|
|
|
24
24
|
export const RECEIVER_KINDS = new Set(['class', 'struct', 'interface', 'type', 'module']);
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Languages where bare `foo()` calls inside a class method are lexically scoped
|
|
28
|
+
* to the module, not the class — there is no implicit this/class binding.
|
|
29
|
+
* For these languages, the same-class fallback must not run for bare (no-receiver)
|
|
30
|
+
* calls that found no exact same-file match.
|
|
31
|
+
*/
|
|
32
|
+
const MODULE_SCOPED_BARE_CALL_EXTENSIONS = new Set([
|
|
33
|
+
'.js',
|
|
34
|
+
'.mjs',
|
|
35
|
+
'.cjs',
|
|
36
|
+
'.jsx',
|
|
37
|
+
'.ts',
|
|
38
|
+
'.tsx',
|
|
39
|
+
'.mts',
|
|
40
|
+
'.cts',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export function isModuleScopedLanguage(relPath: string): boolean {
|
|
44
|
+
const ext = relPath.slice(relPath.lastIndexOf('.'));
|
|
45
|
+
return MODULE_SCOPED_BARE_CALL_EXTENSIONS.has(ext);
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
// ── Shared resolution functions ──────────────────────────────────────────
|
|
27
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Callable definition kinds — variable/constant bindings are NOT callable
|
|
52
|
+
* in the function-as-enclosing-scope sense (they are local declarations, not
|
|
53
|
+
* function bodies). Top-level variable bindings (e.g. Haskell `main = do …`)
|
|
54
|
+
* are handled separately as a fallback tier.
|
|
55
|
+
*/
|
|
56
|
+
const CALLABLE_KINDS = new Set(['function', 'method']);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Variable-like binding kinds that may act as top-level callers when no
|
|
60
|
+
* enclosing function/method exists (e.g. Haskell top-level `main` is a
|
|
61
|
+
* `bind` node → kind `variable`). Local variable declarations inside a
|
|
62
|
+
* function body must NOT win over the enclosing function.
|
|
63
|
+
*/
|
|
64
|
+
const TOP_LEVEL_BINDING_KINDS = new Set(['variable', 'constant']);
|
|
65
|
+
|
|
28
66
|
export function findCaller(
|
|
29
67
|
lookup: CallNodeLookup,
|
|
30
68
|
call: { line: number },
|
|
@@ -36,25 +74,64 @@ export function findCaller(
|
|
|
36
74
|
}>,
|
|
37
75
|
relPath: string,
|
|
38
76
|
fileNodeRow: { id: number },
|
|
39
|
-
): { id: number } {
|
|
40
|
-
|
|
41
|
-
let
|
|
77
|
+
): { id: number; callerName: string | null } {
|
|
78
|
+
// Pass 1: find the narrowest enclosing function/method.
|
|
79
|
+
let fnCaller: { id: number } | null = null;
|
|
80
|
+
let fnCallerName: string | null = null;
|
|
81
|
+
let fnCallerSpan = Infinity;
|
|
82
|
+
|
|
83
|
+
// Pass 2: find the widest (outermost) enclosing variable/constant binding.
|
|
84
|
+
// Used as fallback when no function/method encloses the call site
|
|
85
|
+
// (e.g. Haskell `main = do …` is a `bind` node with kind `variable`).
|
|
86
|
+
// We pick the WIDEST span (outermost binding), not the narrowest, so that
|
|
87
|
+
// nested `let` bindings inside `main`'s do-block do not shadow `main`
|
|
88
|
+
// itself as the attributing caller. The outermost enclosing variable is
|
|
89
|
+
// the "function-like" top-level binding.
|
|
90
|
+
let varCaller: { id: number } | null = null;
|
|
91
|
+
let varCallerName: string | null = null;
|
|
92
|
+
let varCallerSpan = -1; // looking for WIDEST span, so start at -1
|
|
93
|
+
|
|
42
94
|
for (const def of definitions) {
|
|
43
95
|
if (def.line <= call.line) {
|
|
44
|
-
const end = def.endLine
|
|
96
|
+
const end = def.endLine ?? Infinity;
|
|
45
97
|
if (call.line <= end) {
|
|
46
|
-
const span = end - def.line;
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
98
|
+
const span = end === Infinity ? Infinity : end - def.line;
|
|
99
|
+
if (CALLABLE_KINDS.has(def.kind)) {
|
|
100
|
+
if (span < fnCallerSpan) {
|
|
101
|
+
const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
|
|
102
|
+
if (row) {
|
|
103
|
+
fnCaller = row;
|
|
104
|
+
fnCallerName = def.name;
|
|
105
|
+
fnCallerSpan = span;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else if (TOP_LEVEL_BINDING_KINDS.has(def.kind)) {
|
|
109
|
+
if (span > varCallerSpan) {
|
|
110
|
+
const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
|
|
111
|
+
if (row) {
|
|
112
|
+
varCaller = row;
|
|
113
|
+
varCallerName = def.name;
|
|
114
|
+
varCallerSpan = span;
|
|
115
|
+
}
|
|
52
116
|
}
|
|
53
117
|
}
|
|
54
118
|
}
|
|
55
119
|
}
|
|
56
120
|
}
|
|
57
|
-
|
|
121
|
+
|
|
122
|
+
// Prefer function/method enclosing scope over variable binding.
|
|
123
|
+
// If a function/method encloses the call, use it — local variable
|
|
124
|
+
// declarations inside the function body must not shadow it.
|
|
125
|
+
// Only fall back to a variable/constant binding when the call is at
|
|
126
|
+
// top-level scope (no enclosing function/method found), which handles
|
|
127
|
+
// languages like Haskell where `main` is a top-level `bind` node.
|
|
128
|
+
if (fnCaller) {
|
|
129
|
+
return { ...fnCaller, callerName: fnCallerName };
|
|
130
|
+
}
|
|
131
|
+
if (varCaller) {
|
|
132
|
+
return { ...varCaller, callerName: varCallerName };
|
|
133
|
+
}
|
|
134
|
+
return { ...fileNodeRow, callerName: null };
|
|
58
135
|
}
|
|
59
136
|
|
|
60
137
|
export function resolveByMethodOrGlobal(
|
|
@@ -62,17 +139,104 @@ export function resolveByMethodOrGlobal(
|
|
|
62
139
|
call: { name: string; receiver?: string | null },
|
|
63
140
|
relPath: string,
|
|
64
141
|
typeMap: Map<string, unknown>,
|
|
142
|
+
callerName?: string | null,
|
|
65
143
|
): ReadonlyArray<{ id: number; file: string }> {
|
|
66
144
|
if (call.receiver) {
|
|
67
|
-
|
|
68
|
-
|
|
145
|
+
// Strip "this." so `this.repo.method()` resolves via typeMap["repo"]
|
|
146
|
+
// (or the "this.repo" key seeded directly by the TSC property-declaration enricher).
|
|
147
|
+
const effectiveReceiver = call.receiver.startsWith('this.')
|
|
148
|
+
? call.receiver.slice('this.'.length)
|
|
149
|
+
: call.receiver;
|
|
150
|
+
// For this.prop receivers, prefer the class-scoped key (ClassName.prop) seeded by
|
|
151
|
+
// handlePropWriteTypeMap / handleFieldDefTypeMap — prevents false edges when multiple
|
|
152
|
+
// classes define the same property name (issues #1323, #1458).
|
|
153
|
+
// Class-scoped lookup runs first so bare fallback keys (confidence 0.6) don't shadow
|
|
154
|
+
// the correct per-class entry when callerName is available.
|
|
155
|
+
let typeEntry: unknown;
|
|
156
|
+
if (call.receiver.startsWith('this.') && callerName) {
|
|
157
|
+
const dotIdx = callerName.lastIndexOf('.');
|
|
158
|
+
if (dotIdx > -1) {
|
|
159
|
+
const callerClass = callerName.slice(0, dotIdx);
|
|
160
|
+
typeEntry = typeMap.get(`${callerClass}.${effectiveReceiver}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
typeEntry ??=
|
|
164
|
+
typeMap.get(effectiveReceiver) ??
|
|
165
|
+
typeMap.get(call.receiver) ??
|
|
166
|
+
// Phase 8.3f: callee-scoped rest-param key (`callee::restName`) to avoid
|
|
167
|
+
// same-name rest-binding collision across functions in the same file (#1358).
|
|
168
|
+
(callerName ? typeMap.get(`${callerName}::${effectiveReceiver}`) : undefined);
|
|
169
|
+
let typeName = typeEntry
|
|
69
170
|
? typeof typeEntry === 'string'
|
|
70
171
|
? typeEntry
|
|
71
172
|
: (typeEntry as { type?: string }).type
|
|
72
173
|
: null;
|
|
174
|
+
|
|
175
|
+
// Belt-and-suspenders fallback for inline new-expression receivers that
|
|
176
|
+
// extractReceiverName did not normalise (e.g. raw text leaked from an
|
|
177
|
+
// unhandled AST node type). extractReceiverName already handles the common
|
|
178
|
+
// `new_expression` / `parenthesized_expression(new_expression)` shapes by
|
|
179
|
+
// returning the constructor name directly, so this branch is exercised only
|
|
180
|
+
// by future node types or constructs that fall through to the raw-text path.
|
|
181
|
+
// The uppercase-initial restriction ([A-Z_$]) is a heuristic to distinguish
|
|
182
|
+
// constructors (PascalCase) from regular functions and avoids false positives
|
|
183
|
+
// on `(new xmlParser()).parse()` style calls.
|
|
184
|
+
if (!typeName && call.receiver) {
|
|
185
|
+
const m = /^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/.exec(call.receiver);
|
|
186
|
+
if (m?.[1]) typeName = m[1];
|
|
187
|
+
}
|
|
188
|
+
|
|
73
189
|
if (typeName) {
|
|
74
|
-
const typed = lookup
|
|
190
|
+
const typed = lookup
|
|
191
|
+
.byName(`${typeName}.${call.name}`)
|
|
192
|
+
.filter((n) => n.kind === 'method' && computeConfidence(relPath, n.file, null) >= 0.5);
|
|
75
193
|
if (typed.length > 0) return typed;
|
|
194
|
+
|
|
195
|
+
// Prototype alias: `Foo.prototype.bar = identifier` seeds typeMap['Foo.bar'] = { type: identifier }.
|
|
196
|
+
// Checked after the symbol-DB lookup so an actual method definition always wins.
|
|
197
|
+
const protoEntry = typeMap.get(`${typeName}.${call.name}`);
|
|
198
|
+
const protoTarget = protoEntry
|
|
199
|
+
? typeof protoEntry === 'string'
|
|
200
|
+
? protoEntry
|
|
201
|
+
: (protoEntry as { type?: string }).type
|
|
202
|
+
: null;
|
|
203
|
+
if (protoTarget) {
|
|
204
|
+
const resolved = lookup
|
|
205
|
+
.byName(protoTarget)
|
|
206
|
+
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
|
|
207
|
+
if (resolved.length > 0) return resolved;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Direct qualified method lookup: ClassName.staticMethod() or ClassName.instanceMethod()
|
|
212
|
+
// when the receiver is a class name with no typeMap entry. Handles static method calls
|
|
213
|
+
// like `C6.staticMethod()` or `D.d()` where the receiver IS the class.
|
|
214
|
+
// Matches both 'method' and 'function' kinds to cover field-initializer synthetic defs.
|
|
215
|
+
if (!typeName) {
|
|
216
|
+
const qualifiedName = `${effectiveReceiver}.${call.name}`;
|
|
217
|
+
const direct = lookup
|
|
218
|
+
.byName(qualifiedName)
|
|
219
|
+
.filter(
|
|
220
|
+
(n) =>
|
|
221
|
+
(n.kind === 'method' || n.kind === 'function') &&
|
|
222
|
+
computeConfidence(relPath, n.file, null) >= 0.5,
|
|
223
|
+
);
|
|
224
|
+
if (direct.length > 0) return direct;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Phase 8.3d: composite pts key — `obj.prop = fn` seeds typeMap['obj.prop'] = { type: 'fn' }.
|
|
228
|
+
// When a call site references `obj.prop` as a callback, resolve directly to the target fn.
|
|
229
|
+
const compositeEntry = typeMap.get(`${call.receiver}.${call.name}`);
|
|
230
|
+
const ptsTarget = compositeEntry
|
|
231
|
+
? typeof compositeEntry === 'string'
|
|
232
|
+
? compositeEntry
|
|
233
|
+
: (compositeEntry as { type?: string }).type
|
|
234
|
+
: null;
|
|
235
|
+
if (ptsTarget) {
|
|
236
|
+
const resolved = lookup
|
|
237
|
+
.byName(ptsTarget)
|
|
238
|
+
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
|
|
239
|
+
if (resolved.length > 0) return resolved;
|
|
76
240
|
}
|
|
77
241
|
}
|
|
78
242
|
if (
|
|
@@ -81,7 +245,68 @@ export function resolveByMethodOrGlobal(
|
|
|
81
245
|
call.receiver === 'self' ||
|
|
82
246
|
call.receiver === 'super'
|
|
83
247
|
) {
|
|
84
|
-
|
|
248
|
+
// Phase 8.3f: accessor this-dispatch via Object.defineProperty.
|
|
249
|
+
// When a plain function (no class prefix) is registered as a get/set accessor for `obj`
|
|
250
|
+
// via Object.defineProperty, typeMap seeds 'callerName:this' = 'obj'.
|
|
251
|
+
// We then resolve this.method() → typeMap['obj.method'] → the concrete definition.
|
|
252
|
+
// This runs before the broad exact-name lookup to avoid false positives from
|
|
253
|
+
// unrelated same-file definitions.
|
|
254
|
+
if (call.receiver === 'this' && callerName && !callerName.includes('.')) {
|
|
255
|
+
const accessorThisEntry = typeMap.get(`${callerName}:this`);
|
|
256
|
+
const objName = accessorThisEntry
|
|
257
|
+
? typeof accessorThisEntry === 'string'
|
|
258
|
+
? accessorThisEntry
|
|
259
|
+
: (accessorThisEntry as { type?: string }).type
|
|
260
|
+
: null;
|
|
261
|
+
if (objName) {
|
|
262
|
+
const objMethodEntry = typeMap.get(`${objName}.${call.name}`);
|
|
263
|
+
const targetFn = objMethodEntry
|
|
264
|
+
? typeof objMethodEntry === 'string'
|
|
265
|
+
? objMethodEntry
|
|
266
|
+
: (objMethodEntry as { type?: string }).type
|
|
267
|
+
: null;
|
|
268
|
+
if (targetFn) {
|
|
269
|
+
const resolved = lookup
|
|
270
|
+
.byName(targetFn)
|
|
271
|
+
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
|
|
272
|
+
if (resolved.length > 0) return resolved;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const exact = lookup
|
|
278
|
+
.byName(call.name)
|
|
279
|
+
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
|
|
280
|
+
if (exact.length > 0) return exact;
|
|
281
|
+
|
|
282
|
+
// Try same-class method lookup via callerName.
|
|
283
|
+
// e.g. `this.area()` inside `Shape.describe` → try `Shape.area`.
|
|
284
|
+
// Also covers no-receiver calls inside class methods, e.g. `IsValidEmail(x)` inside
|
|
285
|
+
// `Validators.ValidateUser` → try `Validators.IsValidEmail` (C#/Java static siblings).
|
|
286
|
+
// This seeds the initial edge that runChaPostPass later expands to subclass overrides.
|
|
287
|
+
//
|
|
288
|
+
// For JS/TS, bare (no-receiver) calls are module-scoped — there is no implicit class
|
|
289
|
+
// binding. Skip the same-class fallback for bare calls in those languages to prevent
|
|
290
|
+
// false positives (e.g. `flush()` inside `Processor.run` must not resolve to
|
|
291
|
+
// `Processor.flush`). this.method() calls are unaffected: they still reach the fallback
|
|
292
|
+
// because `call.receiver === 'this'` is truthy, not a bare call.
|
|
293
|
+
const isBareCall = !call.receiver;
|
|
294
|
+
if (callerName && !(isBareCall && isModuleScopedLanguage(relPath))) {
|
|
295
|
+
const dotIdx = callerName.lastIndexOf('.');
|
|
296
|
+
if (dotIdx > -1) {
|
|
297
|
+
// Extract only the segment immediately before the method name so that
|
|
298
|
+
// 'Namespace.ClassName.method' yields 'ClassName', not 'Namespace.ClassName'.
|
|
299
|
+
// Symbols are stored under their bare class name, not their qualified path.
|
|
300
|
+
const prevDot = callerName.lastIndexOf('.', dotIdx - 1);
|
|
301
|
+
const callerClass = callerName.slice(prevDot + 1, dotIdx);
|
|
302
|
+
const qualifiedName = `${callerClass}.${call.name}`;
|
|
303
|
+
const sameClass = lookup
|
|
304
|
+
.byName(qualifiedName)
|
|
305
|
+
.filter((t) => t.kind === 'method' && computeConfidence(relPath, t.file, null) >= 0.5);
|
|
306
|
+
if (sameClass.length > 0) return sameClass;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return exact; // empty
|
|
85
310
|
}
|
|
86
311
|
return [];
|
|
87
312
|
}
|
|
@@ -92,6 +317,7 @@ export function resolveCallTargets(
|
|
|
92
317
|
relPath: string,
|
|
93
318
|
importedNames: Map<string, string>,
|
|
94
319
|
typeMap: Map<string, unknown>,
|
|
320
|
+
callerName?: string | null,
|
|
95
321
|
): { targets: Array<{ id: number; file: string }>; importedFrom: string | undefined } {
|
|
96
322
|
const importedFrom = importedNames.get(call.name);
|
|
97
323
|
let targets: ReadonlyArray<{ id: number; file: string }> | undefined;
|
|
@@ -109,7 +335,7 @@ export function resolveCallTargets(
|
|
|
109
335
|
if (!targets || targets.length === 0) {
|
|
110
336
|
targets = lookup.byNameAndFile(call.name, relPath);
|
|
111
337
|
if (targets.length === 0) {
|
|
112
|
-
targets = resolveByMethodOrGlobal(lookup, call, relPath, typeMap);
|
|
338
|
+
targets = resolveByMethodOrGlobal(lookup, call, relPath, typeMap, callerName);
|
|
113
339
|
}
|
|
114
340
|
}
|
|
115
341
|
|
|
@@ -129,13 +355,17 @@ export function resolveCallTargets(
|
|
|
129
355
|
* Returns the edge tuple to insert, or null if nothing matched or the edge
|
|
130
356
|
* was already seen. Callers are responsible for the actual DB/array insert.
|
|
131
357
|
*
|
|
132
|
-
* Receiver resolution
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
358
|
+
* Receiver resolution:
|
|
359
|
+
* 1. Look up same-file nodes for `effectiveReceiver` (unfiltered by kind).
|
|
360
|
+
* 2. If any same-file node exists AND `effectiveReceiver` is not in `importedNames`
|
|
361
|
+
* (i.e. it is a locally-defined symbol, not an import artifact), apply
|
|
362
|
+
* RECEIVER_KINDS and return the filtered set — no global fallback.
|
|
363
|
+
* A local `function C(){}` means this file owns `C`; no cross-file class
|
|
364
|
+
* should win over it (issue #1539).
|
|
365
|
+
* 3. If the same-file node IS an import artifact (e.g. destructured require),
|
|
366
|
+
* or no same-file node exists at all, fall back to global candidates filtered
|
|
367
|
+
* by RECEIVER_KINDS. This preserves the pre-#1539 behaviour for cases where
|
|
368
|
+
* an imported name appears as kind='function' in the importer file.
|
|
139
369
|
*/
|
|
140
370
|
export function resolveReceiverEdge(
|
|
141
371
|
lookup: CallNodeLookup,
|
|
@@ -144,6 +374,7 @@ export function resolveReceiverEdge(
|
|
|
144
374
|
relPath: string,
|
|
145
375
|
typeMap: Map<string, unknown>,
|
|
146
376
|
seenCallEdges: Set<string>,
|
|
377
|
+
importedNames: ReadonlyMap<string, string>,
|
|
147
378
|
): { callerId: number; receiverId: number; confidence: number } | null {
|
|
148
379
|
const typeEntry = typeMap.get(call.receiver);
|
|
149
380
|
const typeName = typeEntry
|
|
@@ -156,18 +387,15 @@ export function resolveReceiverEdge(
|
|
|
156
387
|
? ((typeEntry as { confidence?: number }).confidence ?? null)
|
|
157
388
|
: null;
|
|
158
389
|
const effectiveReceiver = typeName || call.receiver;
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const sameFileCandidates =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
sameFileCandidates.length > 0
|
|
169
|
-
? sameFileCandidates
|
|
170
|
-
: lookup.byName(effectiveReceiver).filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
|
|
390
|
+
// Block global fallback only when the same-file node is a local definition,
|
|
391
|
+
// not when it's an import artifact (e.g. `const { C } = require(…)` seeds a
|
|
392
|
+
// kind='function' node in the importer but the real class lives elsewhere).
|
|
393
|
+
const sameFileAll = lookup.byNameAndFile(effectiveReceiver, relPath);
|
|
394
|
+
const isLocalDefinition = sameFileAll.length > 0 && !importedNames?.has(effectiveReceiver);
|
|
395
|
+
const sameFileCandidates = sameFileAll.filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
|
|
396
|
+
const candidates = isLocalDefinition
|
|
397
|
+
? sameFileCandidates
|
|
398
|
+
: lookup.byName(effectiveReceiver).filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
|
|
171
399
|
if (candidates.length === 0) return null;
|
|
172
400
|
const recvTarget = candidates[0]!;
|
|
173
401
|
const recvKey = `recv|${caller.id}|${recvTarget.id}`;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 8.5: Class Hierarchy Analysis (CHA) + Rapid Type Analysis (RTA)
|
|
3
|
+
*
|
|
4
|
+
* CHA resolves virtual/interface method dispatch to all known concrete
|
|
5
|
+
* implementations. RTA refines the CHA set by filtering out types that are
|
|
6
|
+
* never instantiated in the program (no `new X()` anywhere in the codebase).
|
|
7
|
+
*
|
|
8
|
+
* Used by:
|
|
9
|
+
* - buildFileCallEdges (WASM/JS path) — inline during per-file edge building
|
|
10
|
+
* - buildChaPostPass (native path) — JS post-pass on top of native edges
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtractorOutput } from '../../../types.js';
|
|
14
|
+
import type { CallNodeLookup } from './call-resolver.js';
|
|
15
|
+
|
|
16
|
+
// ── CHA context ──────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface ChaContext {
|
|
19
|
+
/** interface/class name → concrete classes that implement or extend it */
|
|
20
|
+
readonly implementors: ReadonlyMap<string, readonly string[]>;
|
|
21
|
+
/** class name → direct parent class name (from `extends`) */
|
|
22
|
+
readonly parents: ReadonlyMap<string, string>;
|
|
23
|
+
/** RTA: class names that appear in `new X()` anywhere in the project */
|
|
24
|
+
readonly instantiatedTypes: ReadonlySet<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const EMPTY_CHA_CONTEXT: ChaContext = {
|
|
28
|
+
implementors: new Map(),
|
|
29
|
+
parents: new Map(),
|
|
30
|
+
instantiatedTypes: new Set(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the CHA context from all parsed file symbols.
|
|
35
|
+
*
|
|
36
|
+
* Must be called AFTER cross-file return-type propagation so that typeMap
|
|
37
|
+
* confidence values reflect propagated types (used for RTA seeding).
|
|
38
|
+
*/
|
|
39
|
+
export function buildChaContext(fileSymbols: ReadonlyMap<string, ExtractorOutput>): ChaContext {
|
|
40
|
+
const implementors = new Map<string, string[]>();
|
|
41
|
+
const parents = new Map<string, string>();
|
|
42
|
+
const instantiatedTypes = new Set<string>();
|
|
43
|
+
|
|
44
|
+
for (const symbols of fileSymbols.values()) {
|
|
45
|
+
for (const cls of symbols.classes) {
|
|
46
|
+
if (cls.implements) {
|
|
47
|
+
let list = implementors.get(cls.implements);
|
|
48
|
+
if (!list) {
|
|
49
|
+
list = [];
|
|
50
|
+
implementors.set(cls.implements, list);
|
|
51
|
+
}
|
|
52
|
+
if (!list.includes(cls.name)) list.push(cls.name);
|
|
53
|
+
}
|
|
54
|
+
if (cls.extends) {
|
|
55
|
+
// child → parent (for this/super hierarchy walking)
|
|
56
|
+
if (!parents.has(cls.name)) parents.set(cls.name, cls.extends);
|
|
57
|
+
// parent → children (for CHA dispatch expansion via extends)
|
|
58
|
+
let list = implementors.get(cls.extends);
|
|
59
|
+
if (!list) {
|
|
60
|
+
list = [];
|
|
61
|
+
implementors.set(cls.extends, list);
|
|
62
|
+
}
|
|
63
|
+
if (!list.includes(cls.name)) list.push(cls.name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// RTA: Phase 8.5 dedicated newExpressions list (all `new X()` in the file)
|
|
68
|
+
if (symbols.newExpressions) {
|
|
69
|
+
for (const typeName of symbols.newExpressions) {
|
|
70
|
+
instantiatedTypes.add(typeName);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// RTA fallback: constructor-confidence typeMap entries (confidence >= 0.9)
|
|
74
|
+
// covers codebases that haven't been re-parsed since Phase 8.5 was added.
|
|
75
|
+
if (symbols.typeMap instanceof Map) {
|
|
76
|
+
for (const entry of symbols.typeMap.values()) {
|
|
77
|
+
if (typeof entry !== 'string' && entry.confidence >= 0.9) {
|
|
78
|
+
instantiatedTypes.add(entry.type);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { implementors, parents, instantiatedTypes };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── this / self / super resolution ──────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve `this.method()`, `self.method()`, or `super.method()` through the
|
|
91
|
+
* class hierarchy of the calling method.
|
|
92
|
+
*
|
|
93
|
+
* callerName must be a qualified method name ("ClassName.callerFn") for the
|
|
94
|
+
* class context to be determinable. Returns [] for plain functions.
|
|
95
|
+
*
|
|
96
|
+
* For `super`, resolution starts from the parent of the caller's class.
|
|
97
|
+
* For `this`/`self`, resolution starts from the caller's own class and walks
|
|
98
|
+
* up the inheritance chain (supporting inherited method lookup).
|
|
99
|
+
*
|
|
100
|
+
* When `callerFile` is provided, same-file method nodes are preferred: if the
|
|
101
|
+
* hierarchy walk finds a qualified method that exists in both the caller's own
|
|
102
|
+
* file AND in unrelated files (e.g. a class named `A` that appears in multiple
|
|
103
|
+
* fixture files), only the same-file nodes are returned. This prevents
|
|
104
|
+
* cross-fixture false edges caused by accidental name collisions across
|
|
105
|
+
* unrelated files in the same project build. When no same-file nodes exist,
|
|
106
|
+
* all found nodes are returned as before.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveThisDispatch(
|
|
109
|
+
methodName: string,
|
|
110
|
+
callerName: string | null,
|
|
111
|
+
receiver: 'this' | 'self' | 'super',
|
|
112
|
+
chaCtx: ChaContext,
|
|
113
|
+
lookup: CallNodeLookup,
|
|
114
|
+
callerFile?: string | null,
|
|
115
|
+
): ReadonlyArray<{ id: number; file: string }> {
|
|
116
|
+
if (!callerName) return [];
|
|
117
|
+
const dotIdx = callerName.indexOf('.');
|
|
118
|
+
if (dotIdx === -1) return [];
|
|
119
|
+
|
|
120
|
+
const callerClass = callerName.slice(0, dotIdx);
|
|
121
|
+
const startClass = receiver === 'super' ? chaCtx.parents.get(callerClass) : callerClass;
|
|
122
|
+
if (!startClass) return [];
|
|
123
|
+
|
|
124
|
+
// Walk up the hierarchy; the visited set guards against cycles in malformed data.
|
|
125
|
+
let current: string | undefined = startClass;
|
|
126
|
+
const visited = new Set<string>();
|
|
127
|
+
while (current && !visited.has(current)) {
|
|
128
|
+
visited.add(current);
|
|
129
|
+
const qualified = `${current}.${methodName}`;
|
|
130
|
+
const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
|
|
131
|
+
if (found.length > 0) {
|
|
132
|
+
// When the caller's file is known, prefer same-file nodes to avoid
|
|
133
|
+
// emitting cross-file edges to identically-named methods in unrelated
|
|
134
|
+
// files. Only fall back to the full set when no same-file node exists.
|
|
135
|
+
if (callerFile && found.some((n) => n.file === callerFile)) {
|
|
136
|
+
return found.filter((n) => n.file === callerFile);
|
|
137
|
+
}
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
current = chaCtx.parents.get(current);
|
|
141
|
+
}
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── CHA dispatch expansion ───────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* CHA + RTA: given a receiver type (class or interface), return all concrete
|
|
149
|
+
* method implementations reachable via the class hierarchy.
|
|
150
|
+
*
|
|
151
|
+
* Only returns methods on types that are actually instantiated somewhere in
|
|
152
|
+
* the project (RTA filter). Returns [] when no concrete instantiated type
|
|
153
|
+
* overrides the given method.
|
|
154
|
+
*
|
|
155
|
+
* BFS over the implementors map handles multi-level hierarchies (e.g.
|
|
156
|
+
* IFoo → AbstractFoo → ConcreteFoo) so that abstract intermediate classes
|
|
157
|
+
* are transparently skipped while their concrete subclasses are still reached.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveChaTargets(
|
|
160
|
+
typeName: string,
|
|
161
|
+
methodName: string,
|
|
162
|
+
chaCtx: ChaContext,
|
|
163
|
+
lookup: CallNodeLookup,
|
|
164
|
+
): ReadonlyArray<{ id: number; file: string }> {
|
|
165
|
+
const results: Array<{ id: number; file: string }> = [];
|
|
166
|
+
|
|
167
|
+
const queue: string[] = [typeName];
|
|
168
|
+
const visited = new Set<string>();
|
|
169
|
+
visited.add(typeName);
|
|
170
|
+
|
|
171
|
+
while (queue.length > 0) {
|
|
172
|
+
const current = queue.shift()!;
|
|
173
|
+
const children = chaCtx.implementors.get(current);
|
|
174
|
+
if (!children?.length) continue;
|
|
175
|
+
|
|
176
|
+
for (const cls of children) {
|
|
177
|
+
if (visited.has(cls)) continue;
|
|
178
|
+
visited.add(cls);
|
|
179
|
+
|
|
180
|
+
if (chaCtx.instantiatedTypes.has(cls)) {
|
|
181
|
+
const qualified = `${cls}.${methodName}`;
|
|
182
|
+
const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
|
|
183
|
+
results.push(...found);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Traverse even non-instantiated classes — they may have instantiated subclasses.
|
|
187
|
+
queue.push(cls);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
@@ -68,6 +68,8 @@ export class PipelineContext {
|
|
|
68
68
|
batchResolved!: Map<string, string> | null;
|
|
69
69
|
reexportMap!: Map<string, unknown[]>;
|
|
70
70
|
barrelOnlyFiles!: Set<string>;
|
|
71
|
+
/** Phase 8.4: cache for resolveBarrelExport results keyed as "barrelPath|symbolName". */
|
|
72
|
+
barrelExportCache: Map<string, string | null> = new Map();
|
|
71
73
|
|
|
72
74
|
// ── Node lookup (set by insertNodes / buildEdges stages) ───────────
|
|
73
75
|
nodesByName!: Map<string, NodeRow[]>;
|
|
@@ -88,6 +90,7 @@ export class PipelineContext {
|
|
|
88
90
|
edgeKind: string;
|
|
89
91
|
confidence: number;
|
|
90
92
|
dynamic: number;
|
|
93
|
+
technique: string | null;
|
|
91
94
|
}> = [];
|
|
92
95
|
|
|
93
96
|
// ── Misc state ─────────────────────────────────────────────────────
|