codegraph-engine 2.1.1__py3-none-any.whl
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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/architecture.py +328 -0
- codegraph/audit.py +106 -0
- codegraph/cache.py +95 -0
- codegraph/cli.py +854 -0
- codegraph/config.py +43 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +1228 -0
- codegraph/epistemic.py +90 -0
- codegraph/errors.py +275 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +397 -0
- codegraph/frameworks.py +434 -0
- codegraph/freshness.py +295 -0
- codegraph/git.py +278 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +41 -0
- codegraph/graph/traversal.py +1291 -0
- codegraph/indexing/__init__.py +4 -0
- codegraph/indexing/classifier.py +274 -0
- codegraph/indexing/indexer.py +943 -0
- codegraph/indexing/models.py +338 -0
- codegraph/indexing/parser.py +1240 -0
- codegraph/indexing/scanner.py +200 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/interrogation.py +1582 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +736 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +289 -0
- codegraph/observability.py +151 -0
- codegraph/optimizer.py +372 -0
- codegraph/planner.py +417 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +363 -0
- codegraph/resolver.py +843 -0
- codegraph/resources/__init__.py +45 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +232 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +220 -0
- codegraph/search/__init__.py +23 -0
- codegraph/search/hybrid.py +301 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +3 -0
- codegraph/security/paths.py +35 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph_engine-2.1.1.dist-info/METADATA +334 -0
- codegraph_engine-2.1.1.dist-info/RECORD +62 -0
- codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
- codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
- codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-2.1.1.dist-info/top_level.txt +1 -0
codegraph/resolver.py
ADDED
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
"""Dedicated Reference & Symbol Resolution Engine.
|
|
2
|
+
|
|
3
|
+
Pipeline:
|
|
4
|
+
Parser IR (Symbols, Imports, Calls, Inheritance, Routes)
|
|
5
|
+
↓
|
|
6
|
+
Module Resolver (deterministic file/package resolution)
|
|
7
|
+
↓
|
|
8
|
+
Re-export Chain Resolver
|
|
9
|
+
↓
|
|
10
|
+
Symbol & Relationship Resolver (lexical -> canonical -> alias -> qualified -> module -> framework)
|
|
11
|
+
↓
|
|
12
|
+
Confidence & Source-Hash Evidence Assignment
|
|
13
|
+
↓
|
|
14
|
+
Deduplicated References & Graph Edges
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import posixpath
|
|
19
|
+
import sqlite3
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
from codegraph.frameworks import RouteDetection
|
|
23
|
+
from codegraph.graph.models import GraphEdge
|
|
24
|
+
from codegraph.indexing.models import (
|
|
25
|
+
CallRef,
|
|
26
|
+
ImportRef,
|
|
27
|
+
InheritanceRef,
|
|
28
|
+
Reference,
|
|
29
|
+
Symbol,
|
|
30
|
+
normalize_module,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_module_to_path(
|
|
35
|
+
imported_module: str,
|
|
36
|
+
source_file: str,
|
|
37
|
+
known_files: set[str],
|
|
38
|
+
) -> str | None:
|
|
39
|
+
"""Deterministically resolve an import specifier to a repository-relative file path.
|
|
40
|
+
|
|
41
|
+
Never guesses files outside `known_files`.
|
|
42
|
+
"""
|
|
43
|
+
if not imported_module:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
src_clean = source_file.replace("\\", "/").lstrip("./")
|
|
47
|
+
src_dir = posixpath.dirname(src_clean)
|
|
48
|
+
|
|
49
|
+
candidates: list[str] = []
|
|
50
|
+
|
|
51
|
+
if imported_module.startswith(".") or "/" in imported_module:
|
|
52
|
+
# Relative or path-based import (JS/TS or relative path)
|
|
53
|
+
if imported_module.startswith("."):
|
|
54
|
+
joined = posixpath.normpath(posixpath.join(src_dir, imported_module)) if src_dir else posixpath.normpath(imported_module)
|
|
55
|
+
else:
|
|
56
|
+
joined = posixpath.normpath(imported_module.lstrip("/"))
|
|
57
|
+
|
|
58
|
+
if joined.startswith(".."):
|
|
59
|
+
return None
|
|
60
|
+
if joined == ".":
|
|
61
|
+
joined = src_dir
|
|
62
|
+
|
|
63
|
+
candidates.append(joined)
|
|
64
|
+
for ext in (".ts", ".tsx", ".js", ".jsx", ".py", ".mjs", ".cjs"):
|
|
65
|
+
candidates.append(f"{joined}{ext}")
|
|
66
|
+
for idx_file in ("index.ts", "index.tsx", "index.js", "index.jsx", "__init__.py"):
|
|
67
|
+
candidates.append(posixpath.join(joined, idx_file) if joined else idx_file)
|
|
68
|
+
else:
|
|
69
|
+
# Dotted module specifier (Python or package root path)
|
|
70
|
+
mod_path = imported_module.replace(".", "/")
|
|
71
|
+
prefixes = [""]
|
|
72
|
+
if src_dir:
|
|
73
|
+
prefixes.append(f"{src_dir}/")
|
|
74
|
+
# Also try top-level package root (e.g. 'src/')
|
|
75
|
+
first_seg = src_dir.split("/")[0]
|
|
76
|
+
if f"{first_seg}/" not in prefixes:
|
|
77
|
+
prefixes.append(f"{first_seg}/")
|
|
78
|
+
if "src/" not in prefixes:
|
|
79
|
+
prefixes.append("src/")
|
|
80
|
+
|
|
81
|
+
for prefix in prefixes:
|
|
82
|
+
base = f"{prefix}{mod_path}"
|
|
83
|
+
candidates.append(f"{base}.py")
|
|
84
|
+
candidates.append(f"{base}/__init__.py")
|
|
85
|
+
for ext in (".ts", ".tsx", ".js", ".jsx"):
|
|
86
|
+
candidates.append(f"{base}{ext}")
|
|
87
|
+
candidates.append(f"{base}/index{ext}")
|
|
88
|
+
|
|
89
|
+
for cand in candidates:
|
|
90
|
+
norm = cand.lstrip("./")
|
|
91
|
+
if norm in known_files:
|
|
92
|
+
return norm
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ResolutionOutput:
|
|
98
|
+
references: list[Reference]
|
|
99
|
+
edges: list[GraphEdge]
|
|
100
|
+
resolved_imports: dict[tuple[str, int, str], tuple[str | None, str | None]]
|
|
101
|
+
resolved_calls: dict[tuple[str, int, str], tuple[str | None, str]]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ReferenceResolver:
|
|
105
|
+
"""Resolves imports, aliases, re-exports, inheritance, calls, and routes across a repository."""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
symbols: list[Symbol],
|
|
110
|
+
imports: list[ImportRef],
|
|
111
|
+
calls: list[CallRef],
|
|
112
|
+
inheritance: list[InheritanceRef],
|
|
113
|
+
routes: list[RouteDetection],
|
|
114
|
+
known_files: set[str],
|
|
115
|
+
file_hashes: dict[str, str],
|
|
116
|
+
indexed_commit: str | None = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
self.symbols = symbols
|
|
119
|
+
self.imports = imports
|
|
120
|
+
self.calls = calls
|
|
121
|
+
self.inheritance = inheritance
|
|
122
|
+
self.routes = routes
|
|
123
|
+
self.known_files = known_files
|
|
124
|
+
self.file_hashes = file_hashes
|
|
125
|
+
self.indexed_commit = indexed_commit
|
|
126
|
+
|
|
127
|
+
# Indexed lookup maps (O(1) lookup)
|
|
128
|
+
self.canonical_to_symbol: dict[str, Symbol] = {}
|
|
129
|
+
self.qualified_to_symbols: dict[str, list[Symbol]] = {}
|
|
130
|
+
self.module_to_symbols: dict[str, dict[str, Symbol]] = {}
|
|
131
|
+
self.short_to_symbols: dict[str, list[Symbol]] = {}
|
|
132
|
+
self.path_to_symbols: dict[str, list[Symbol]] = {}
|
|
133
|
+
self.file_to_module: dict[str, str] = {
|
|
134
|
+
f: normalize_module(f) for f in known_files
|
|
135
|
+
}
|
|
136
|
+
self.module_to_file: dict[str, str] = {
|
|
137
|
+
normalize_module(f): f for f in sorted(known_files)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for sym in symbols:
|
|
141
|
+
self.canonical_to_symbol[sym.canonical_id] = sym
|
|
142
|
+
self.qualified_to_symbols.setdefault(sym.qualified_name, []).append(sym)
|
|
143
|
+
mod_map = self.module_to_symbols.setdefault(sym.module, {})
|
|
144
|
+
mod_map[sym.qualified_name] = sym
|
|
145
|
+
mod_map.setdefault(sym.name, sym)
|
|
146
|
+
self.short_to_symbols.setdefault(sym.name, []).append(sym)
|
|
147
|
+
self.path_to_symbols.setdefault(sym.path, []).append(sym)
|
|
148
|
+
|
|
149
|
+
self.file_imports: dict[str, list[ImportRef]] = {}
|
|
150
|
+
for imp in imports:
|
|
151
|
+
self.file_imports.setdefault(imp.source_file, []).append(imp)
|
|
152
|
+
|
|
153
|
+
# Map (source_module, exported_name) -> (target_module, target_name)
|
|
154
|
+
self.reexport_map: dict[tuple[str, str], tuple[str, str]] = {}
|
|
155
|
+
self.wildcard_reexports: dict[str, list[str]] = {}
|
|
156
|
+
self._build_reexport_maps()
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def resolve_symbol_diagnostic(con: sqlite3.Connection, query: str) -> dict[str, object]:
|
|
160
|
+
return resolve_symbol_diagnostic(con, query)
|
|
161
|
+
|
|
162
|
+
def _resolve_import_target_module(self, imp: ImportRef) -> tuple[str | None, str | None]:
|
|
163
|
+
"""Return (resolved_file_path, resolved_module) for an ImportRef."""
|
|
164
|
+
target_path = resolve_module_to_path(
|
|
165
|
+
imp.imported_module, imp.source_file, self.known_files
|
|
166
|
+
)
|
|
167
|
+
if target_path:
|
|
168
|
+
return target_path, normalize_module(target_path)
|
|
169
|
+
|
|
170
|
+
# Maybe imported_module is `pkg.mod.Symbol` where `pkg.mod` is a file
|
|
171
|
+
if "." in imp.imported_module and not imp.imported_module.startswith("."):
|
|
172
|
+
parent_mod, last_seg = imp.imported_module.rsplit(".", 1)
|
|
173
|
+
parent_path = resolve_module_to_path(
|
|
174
|
+
parent_mod, imp.source_file, self.known_files
|
|
175
|
+
)
|
|
176
|
+
if parent_path:
|
|
177
|
+
return parent_path, normalize_module(parent_path)
|
|
178
|
+
# Check if normalized module exists directly
|
|
179
|
+
if imp.imported_module in self.module_to_file:
|
|
180
|
+
p = self.module_to_file[imp.imported_module]
|
|
181
|
+
return p, imp.imported_module
|
|
182
|
+
if f"src.{imp.imported_module}" in self.module_to_file:
|
|
183
|
+
mod = f"src.{imp.imported_module}"
|
|
184
|
+
return self.module_to_file[mod], mod
|
|
185
|
+
del last_seg
|
|
186
|
+
|
|
187
|
+
if imp.imported_module in self.module_to_file:
|
|
188
|
+
p = self.module_to_file[imp.imported_module]
|
|
189
|
+
return p, imp.imported_module
|
|
190
|
+
if f"src.{imp.imported_module}" in self.module_to_file:
|
|
191
|
+
mod = f"src.{imp.imported_module}"
|
|
192
|
+
return self.module_to_file[mod], mod
|
|
193
|
+
|
|
194
|
+
return None, None
|
|
195
|
+
|
|
196
|
+
def _build_reexport_maps(self) -> None:
|
|
197
|
+
for imp in self.imports:
|
|
198
|
+
if not imp.is_reexport:
|
|
199
|
+
continue
|
|
200
|
+
_target_path, target_mod = self._resolve_import_target_module(imp)
|
|
201
|
+
if not target_mod:
|
|
202
|
+
continue
|
|
203
|
+
src_mod = imp.source_module or normalize_module(imp.source_file)
|
|
204
|
+
if imp.imported_name == "*" and not imp.alias:
|
|
205
|
+
self.wildcard_reexports.setdefault(src_mod, []).append(target_mod)
|
|
206
|
+
else:
|
|
207
|
+
exported_nm = imp.exported_name or imp.alias or imp.imported_name or imp.local_name
|
|
208
|
+
orig_nm = imp.imported_name or exported_nm
|
|
209
|
+
if exported_nm and orig_nm:
|
|
210
|
+
self.reexport_map[(src_mod, exported_nm)] = (target_mod, orig_nm)
|
|
211
|
+
|
|
212
|
+
def resolve_symbol_in_module(
|
|
213
|
+
self,
|
|
214
|
+
module: str,
|
|
215
|
+
symbol_name: str,
|
|
216
|
+
visited: set[tuple[str, str]] | None = None,
|
|
217
|
+
) -> Symbol | None:
|
|
218
|
+
"""Resolve `symbol_name` in `module`, following re-export chains deterministically."""
|
|
219
|
+
visited = visited or set()
|
|
220
|
+
key = (module, symbol_name)
|
|
221
|
+
if key in visited:
|
|
222
|
+
return None
|
|
223
|
+
visited.add(key)
|
|
224
|
+
|
|
225
|
+
# 1. Direct symbol in module
|
|
226
|
+
mod_syms = self.module_to_symbols.get(module, {})
|
|
227
|
+
if symbol_name in mod_syms:
|
|
228
|
+
return mod_syms[symbol_name]
|
|
229
|
+
|
|
230
|
+
# Check canonical ID directly
|
|
231
|
+
canon_cand = f"{module}.{symbol_name}"
|
|
232
|
+
if canon_cand in self.canonical_to_symbol:
|
|
233
|
+
return self.canonical_to_symbol[canon_cand]
|
|
234
|
+
|
|
235
|
+
# If symbol_name is dotted (e.g. AS.login or AuthService.login), resolve prefix first
|
|
236
|
+
if "." in symbol_name:
|
|
237
|
+
prefix, rest = symbol_name.split(".", 1)
|
|
238
|
+
base_sym = self.resolve_symbol_in_module(module, prefix, visited=set(visited))
|
|
239
|
+
if base_sym:
|
|
240
|
+
method_canon = f"{base_sym.canonical_id}.{rest}"
|
|
241
|
+
if method_canon in self.canonical_to_symbol:
|
|
242
|
+
return self.canonical_to_symbol[method_canon]
|
|
243
|
+
|
|
244
|
+
# 2. Named re-export in this module
|
|
245
|
+
if key in self.reexport_map:
|
|
246
|
+
target_mod, orig_name = self.reexport_map[key]
|
|
247
|
+
return self.resolve_symbol_in_module(target_mod, orig_name, visited)
|
|
248
|
+
|
|
249
|
+
# 3. Wildcard re-exports in this module
|
|
250
|
+
for target_mod in self.wildcard_reexports.get(module, []):
|
|
251
|
+
found = self.resolve_symbol_in_module(target_mod, symbol_name, visited)
|
|
252
|
+
if found:
|
|
253
|
+
return found
|
|
254
|
+
|
|
255
|
+
# 4. Default export fallback: if symbol_name == "default" and module has a single class/function
|
|
256
|
+
if symbol_name == "default" and mod_syms:
|
|
257
|
+
top_level = [s for s in mod_syms.values() if not s.scope]
|
|
258
|
+
if len(top_level) == 1:
|
|
259
|
+
return top_level[0]
|
|
260
|
+
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
def resolve_identifier_in_file(
|
|
264
|
+
self,
|
|
265
|
+
identifier: str,
|
|
266
|
+
source_file: str,
|
|
267
|
+
caller_canonical_id: str | None = None,
|
|
268
|
+
) -> tuple[Symbol | None, str, str]:
|
|
269
|
+
"""Resolve an identifier or dotted expression inside `source_file`.
|
|
270
|
+
|
|
271
|
+
Returns (Symbol | None, confidence, resolution_reason).
|
|
272
|
+
"""
|
|
273
|
+
src_mod = normalize_module(source_file)
|
|
274
|
+
|
|
275
|
+
# Level 1: Lexical / local scope
|
|
276
|
+
if caller_canonical_id and caller_canonical_id in self.canonical_to_symbol:
|
|
277
|
+
caller_sym = self.canonical_to_symbol[caller_canonical_id]
|
|
278
|
+
# Check nested symbol inside caller or sibling in same scope
|
|
279
|
+
nested_canon = f"{caller_sym.canonical_id}.{identifier}"
|
|
280
|
+
if nested_canon in self.canonical_to_symbol:
|
|
281
|
+
return (
|
|
282
|
+
self.canonical_to_symbol[nested_canon],
|
|
283
|
+
"HIGH",
|
|
284
|
+
"Local nested symbol in lexical scope",
|
|
285
|
+
)
|
|
286
|
+
if caller_sym.scope:
|
|
287
|
+
sibling_canon = f"{src_mod}.{caller_sym.scope}.{identifier}"
|
|
288
|
+
if sibling_canon in self.canonical_to_symbol:
|
|
289
|
+
return (
|
|
290
|
+
self.canonical_to_symbol[sibling_canon],
|
|
291
|
+
"HIGH",
|
|
292
|
+
f"Class/scope member in {caller_sym.scope}",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Same-module symbol
|
|
296
|
+
local_sym = self.resolve_symbol_in_module(src_mod, identifier)
|
|
297
|
+
if local_sym:
|
|
298
|
+
return local_sym, "HIGH", f"Defined in same module '{src_mod}'"
|
|
299
|
+
|
|
300
|
+
# Level 2: Exact canonical symbol match
|
|
301
|
+
if identifier in self.canonical_to_symbol:
|
|
302
|
+
return (
|
|
303
|
+
self.canonical_to_symbol[identifier],
|
|
304
|
+
"HIGH",
|
|
305
|
+
"Exact canonical symbol match",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Level 3 & 4: Imported alias, named import, default import, or namespace/qualified import
|
|
309
|
+
imps = self.file_imports.get(source_file, [])
|
|
310
|
+
if "." in identifier:
|
|
311
|
+
prefix, rest = identifier.split(".", 1)
|
|
312
|
+
# Check if prefix is a local class in same module
|
|
313
|
+
local_cls = self.resolve_symbol_in_module(src_mod, prefix)
|
|
314
|
+
if local_cls:
|
|
315
|
+
member_canon = f"{local_cls.canonical_id}.{rest}"
|
|
316
|
+
if member_canon in self.canonical_to_symbol:
|
|
317
|
+
return (
|
|
318
|
+
self.canonical_to_symbol[member_canon],
|
|
319
|
+
"HIGH",
|
|
320
|
+
f"Resolved method '{rest}' on local class '{local_cls.canonical_id}'",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Check if prefix is an imported symbol/alias or namespace
|
|
324
|
+
for imp in imps:
|
|
325
|
+
if imp.local_name == prefix or imp.alias == prefix or imp.name == prefix:
|
|
326
|
+
_tpath, target_mod = self._resolve_import_target_module(imp)
|
|
327
|
+
if target_mod:
|
|
328
|
+
if imp.import_type in ("namespace", "module") or imp.imported_name in (None, "*"):
|
|
329
|
+
sym = self.resolve_symbol_in_module(target_mod, rest)
|
|
330
|
+
if sym:
|
|
331
|
+
return (
|
|
332
|
+
sym,
|
|
333
|
+
"HIGH",
|
|
334
|
+
f"Resolved via namespace import '{prefix}' -> '{sym.canonical_id}'",
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
orig_cls = imp.imported_name or prefix
|
|
338
|
+
cls_sym = self.resolve_symbol_in_module(
|
|
339
|
+
target_mod, orig_cls
|
|
340
|
+
) or self.resolve_symbol_in_module(target_mod, prefix)
|
|
341
|
+
if cls_sym:
|
|
342
|
+
method_canon = f"{cls_sym.canonical_id}.{rest}"
|
|
343
|
+
if method_canon in self.canonical_to_symbol:
|
|
344
|
+
return (
|
|
345
|
+
self.canonical_to_symbol[method_canon],
|
|
346
|
+
"HIGH",
|
|
347
|
+
f"Resolved via imported symbol '{prefix}' -> '{method_canon}'",
|
|
348
|
+
)
|
|
349
|
+
# Or target_mod is actually a package and orig_cls is a submodule
|
|
350
|
+
sub_sym = self.resolve_symbol_in_module(f"{target_mod}.{orig_cls}", rest)
|
|
351
|
+
if sub_sym:
|
|
352
|
+
return (
|
|
353
|
+
sub_sym,
|
|
354
|
+
"HIGH",
|
|
355
|
+
f"Resolved via submodule import '{prefix}.{rest}' -> '{sub_sym.canonical_id}'",
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
for imp in imps:
|
|
359
|
+
if imp.local_name == identifier or imp.alias == identifier:
|
|
360
|
+
_tpath, target_mod = self._resolve_import_target_module(imp)
|
|
361
|
+
if target_mod:
|
|
362
|
+
if imp.import_type == "default":
|
|
363
|
+
sym = self.resolve_symbol_in_module(
|
|
364
|
+
target_mod, imp.local_name
|
|
365
|
+
) or self.resolve_symbol_in_module(target_mod, "default")
|
|
366
|
+
else:
|
|
367
|
+
target_name = imp.imported_name or identifier
|
|
368
|
+
sym = self.resolve_symbol_in_module(target_mod, target_name)
|
|
369
|
+
if sym:
|
|
370
|
+
return (
|
|
371
|
+
sym,
|
|
372
|
+
"HIGH",
|
|
373
|
+
f"Resolved via import '{imp.local_name}' from '{target_mod}'",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Level 5: Exact qualified name match if unique across repository
|
|
377
|
+
if "." in identifier and identifier in self.qualified_to_symbols:
|
|
378
|
+
cands = self.qualified_to_symbols[identifier]
|
|
379
|
+
if len(cands) == 1:
|
|
380
|
+
return (
|
|
381
|
+
cands[0],
|
|
382
|
+
"MEDIUM",
|
|
383
|
+
f"Unique qualified symbol match '{cands[0].canonical_id}'",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return None, "UNKNOWN", "No matching local or imported symbol found."
|
|
387
|
+
|
|
388
|
+
def resolve_all(self) -> ResolutionOutput:
|
|
389
|
+
"""Run the full resolution pipeline and emit deduplicated References and GraphEdges."""
|
|
390
|
+
references: list[Reference] = []
|
|
391
|
+
edges: list[GraphEdge] = []
|
|
392
|
+
seen_refs: set[tuple[str, str | None, str, str, int]] = set()
|
|
393
|
+
seen_edges: set[tuple[str, str, str, str, int]] = set()
|
|
394
|
+
resolved_imports: dict[tuple[str, int, str], tuple[str | None, str | None]] = {}
|
|
395
|
+
resolved_calls: dict[tuple[str, int, str], tuple[str | None, str]] = {}
|
|
396
|
+
|
|
397
|
+
def add_ref(ref: Reference) -> None:
|
|
398
|
+
key = (
|
|
399
|
+
ref.source_symbol_id,
|
|
400
|
+
ref.target_symbol_id,
|
|
401
|
+
ref.relationship,
|
|
402
|
+
ref.path,
|
|
403
|
+
ref.start_line,
|
|
404
|
+
)
|
|
405
|
+
if key not in seen_refs:
|
|
406
|
+
seen_refs.add(key)
|
|
407
|
+
references.append(ref)
|
|
408
|
+
|
|
409
|
+
def add_edge(edge: GraphEdge) -> None:
|
|
410
|
+
key = (
|
|
411
|
+
edge.source,
|
|
412
|
+
edge.target,
|
|
413
|
+
edge.relationship,
|
|
414
|
+
edge.file,
|
|
415
|
+
edge.start_line,
|
|
416
|
+
)
|
|
417
|
+
if key not in seen_edges:
|
|
418
|
+
seen_edges.add(key)
|
|
419
|
+
edges.append(edge)
|
|
420
|
+
|
|
421
|
+
# 0. Definition & Containment edges
|
|
422
|
+
for sym in self.symbols:
|
|
423
|
+
f_hash = self.file_hashes.get(sym.path, "")
|
|
424
|
+
if sym.parent_symbol_id:
|
|
425
|
+
add_edge(
|
|
426
|
+
GraphEdge(
|
|
427
|
+
source=sym.parent_symbol_id,
|
|
428
|
+
target=sym.canonical_id,
|
|
429
|
+
relationship="CONTAINS",
|
|
430
|
+
confidence="HIGH",
|
|
431
|
+
file=sym.path,
|
|
432
|
+
start_line=sym.start_line,
|
|
433
|
+
end_line=sym.end_line,
|
|
434
|
+
evidence=f"Scope containment {sym.parent_symbol_id} -> {sym.canonical_id}",
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
add_edge(
|
|
439
|
+
GraphEdge(
|
|
440
|
+
source=sym.path,
|
|
441
|
+
target=sym.canonical_id,
|
|
442
|
+
relationship="DEFINES",
|
|
443
|
+
confidence="HIGH",
|
|
444
|
+
file=sym.path,
|
|
445
|
+
start_line=sym.start_line,
|
|
446
|
+
end_line=sym.end_line,
|
|
447
|
+
evidence=f"File {sym.path} defines {sym.canonical_id}",
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
del f_hash
|
|
451
|
+
|
|
452
|
+
# 1. Resolve Imports & Re-exports
|
|
453
|
+
for imp in self.imports:
|
|
454
|
+
src_mod = imp.source_module or normalize_module(imp.source_file)
|
|
455
|
+
f_hash = self.file_hashes.get(imp.source_file, "")
|
|
456
|
+
target_path, target_mod = self._resolve_import_target_module(imp)
|
|
457
|
+
resolved_imports[(imp.source_file, imp.line, imp.local_name)] = (
|
|
458
|
+
target_path,
|
|
459
|
+
target_mod,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if imp.is_reexport:
|
|
463
|
+
exp_name = imp.exported_name or imp.alias or imp.imported_name or "*"
|
|
464
|
+
orig_name = imp.imported_name or exp_name
|
|
465
|
+
target_sym = (
|
|
466
|
+
self.resolve_symbol_in_module(target_mod, orig_name)
|
|
467
|
+
if (target_mod and orig_name != "*")
|
|
468
|
+
else None
|
|
469
|
+
)
|
|
470
|
+
target_id = (
|
|
471
|
+
target_sym.canonical_id
|
|
472
|
+
if target_sym
|
|
473
|
+
else (f"{target_mod}.{orig_name}" if target_mod else f"{imp.imported_module}.{orig_name}")
|
|
474
|
+
)
|
|
475
|
+
conf = "HIGH" if (target_sym or target_mod) else "LOW"
|
|
476
|
+
src_id = f"{src_mod}.{exp_name}" if exp_name != "*" else src_mod
|
|
477
|
+
add_ref(
|
|
478
|
+
Reference(
|
|
479
|
+
source_symbol_id=src_id,
|
|
480
|
+
target_symbol_id=target_id,
|
|
481
|
+
relationship="REEXPORTS",
|
|
482
|
+
confidence=conf,
|
|
483
|
+
path=imp.source_file,
|
|
484
|
+
start_line=imp.line,
|
|
485
|
+
end_line=imp.line,
|
|
486
|
+
evidence=f"Re-export '{exp_name}' from '{imp.imported_module}' -> '{target_id}'",
|
|
487
|
+
source_hash=f_hash,
|
|
488
|
+
indexed_commit=self.indexed_commit,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
add_edge(
|
|
492
|
+
GraphEdge(
|
|
493
|
+
source=src_id,
|
|
494
|
+
target=target_id,
|
|
495
|
+
relationship="REEXPORTS",
|
|
496
|
+
confidence=conf,
|
|
497
|
+
file=imp.source_file,
|
|
498
|
+
start_line=imp.line,
|
|
499
|
+
end_line=imp.line,
|
|
500
|
+
evidence=f"Re-export '{exp_name}' from '{imp.imported_module}'",
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
else:
|
|
504
|
+
target_sym = None
|
|
505
|
+
if target_mod and imp.imported_name and imp.imported_name != "*":
|
|
506
|
+
target_sym = self.resolve_symbol_in_module(target_mod, imp.imported_name)
|
|
507
|
+
target_id = (
|
|
508
|
+
target_sym.canonical_id
|
|
509
|
+
if target_sym
|
|
510
|
+
else (target_mod or imp.imported_module)
|
|
511
|
+
)
|
|
512
|
+
conf = "HIGH"
|
|
513
|
+
add_ref(
|
|
514
|
+
Reference(
|
|
515
|
+
source_symbol_id=src_mod,
|
|
516
|
+
target_symbol_id=target_id,
|
|
517
|
+
relationship="IMPORTS",
|
|
518
|
+
confidence=conf,
|
|
519
|
+
path=imp.source_file,
|
|
520
|
+
start_line=imp.line,
|
|
521
|
+
end_line=imp.line,
|
|
522
|
+
evidence=f"Import '{imp.local_name}' from '{imp.imported_module}'",
|
|
523
|
+
source_hash=f_hash,
|
|
524
|
+
indexed_commit=self.indexed_commit,
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
add_edge(
|
|
528
|
+
GraphEdge(
|
|
529
|
+
source=imp.source_file,
|
|
530
|
+
target=target_path or imp.imported_module,
|
|
531
|
+
relationship="IMPORTS",
|
|
532
|
+
confidence="HIGH",
|
|
533
|
+
file=imp.source_file,
|
|
534
|
+
start_line=imp.line,
|
|
535
|
+
end_line=imp.line,
|
|
536
|
+
evidence=f"Import '{imp.imported_module}' at {imp.source_file}:{imp.line}",
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# 2. Resolve Inheritance (EXTENDS / IMPLEMENTS) inside a map so inherited methods can also resolve
|
|
541
|
+
class_bases_map: dict[str, list[str]] = {}
|
|
542
|
+
for inh in self.inheritance:
|
|
543
|
+
f_hash = self.file_hashes.get(inh.source_file, "")
|
|
544
|
+
target_sym, conf, reason = self.resolve_identifier_in_file(
|
|
545
|
+
inh.base_name, inh.source_file, inh.source_canonical_id
|
|
546
|
+
)
|
|
547
|
+
target_id = target_sym.canonical_id if target_sym else inh.base_name
|
|
548
|
+
if target_sym:
|
|
549
|
+
class_bases_map.setdefault(inh.source_canonical_id, []).append(
|
|
550
|
+
target_sym.canonical_id
|
|
551
|
+
)
|
|
552
|
+
add_ref(
|
|
553
|
+
Reference(
|
|
554
|
+
source_symbol_id=inh.source_canonical_id,
|
|
555
|
+
target_symbol_id=target_id,
|
|
556
|
+
relationship=inh.relationship,
|
|
557
|
+
confidence=conf,
|
|
558
|
+
path=inh.source_file,
|
|
559
|
+
start_line=inh.line,
|
|
560
|
+
end_line=inh.line,
|
|
561
|
+
evidence=f"{inh.source_canonical_id} {inh.relationship} {target_id} ({reason})",
|
|
562
|
+
source_hash=f_hash,
|
|
563
|
+
indexed_commit=self.indexed_commit,
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
add_edge(
|
|
567
|
+
GraphEdge(
|
|
568
|
+
source=inh.source_canonical_id,
|
|
569
|
+
target=target_id,
|
|
570
|
+
relationship=inh.relationship,
|
|
571
|
+
confidence=conf,
|
|
572
|
+
file=inh.source_file,
|
|
573
|
+
start_line=inh.line,
|
|
574
|
+
end_line=inh.line,
|
|
575
|
+
evidence=f"{inh.source_canonical_id} {inh.relationship} {target_id}",
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# 3. Resolve Call Sites
|
|
580
|
+
for call in self.calls:
|
|
581
|
+
f_hash = self.file_hashes.get(call.source_file, "")
|
|
582
|
+
caller_id = call.caller_canonical_id or normalize_module(call.source_file)
|
|
583
|
+
expr = call.qualified_callee or call.callee
|
|
584
|
+
|
|
585
|
+
target_sym, conf, reason = self.resolve_identifier_in_file(
|
|
586
|
+
expr, call.source_file, caller_id
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Check inherited method on base class if receiver was self/cls or local class
|
|
590
|
+
if not target_sym and "." in expr:
|
|
591
|
+
recv_part, method_part = expr.rsplit(".", 1)
|
|
592
|
+
recv_sym, _rconf, _rreason = self.resolve_identifier_in_file(
|
|
593
|
+
recv_part, call.source_file, caller_id
|
|
594
|
+
)
|
|
595
|
+
if recv_sym and recv_sym.canonical_id in class_bases_map:
|
|
596
|
+
for base_canon in class_bases_map[recv_sym.canonical_id]:
|
|
597
|
+
cand_canon = f"{base_canon}.{method_part}"
|
|
598
|
+
if cand_canon in self.canonical_to_symbol:
|
|
599
|
+
target_sym = self.canonical_to_symbol[cand_canon]
|
|
600
|
+
conf = "HIGH"
|
|
601
|
+
reason = f"Inherited method on base class '{base_canon}'"
|
|
602
|
+
break
|
|
603
|
+
|
|
604
|
+
# Conservative factory return type resolution
|
|
605
|
+
if not target_sym and recv_sym and recv_sym.kind in ("function", "method") and recv_sym.return_type:
|
|
606
|
+
clean_ret = recv_sym.return_type.split("[")[0].strip()
|
|
607
|
+
ret_sym, _ret_conf, _ret_reason = self.resolve_identifier_in_file(
|
|
608
|
+
clean_ret, recv_sym.path, recv_sym.canonical_id
|
|
609
|
+
)
|
|
610
|
+
target_class_canon = ret_sym.canonical_id if ret_sym else f"{recv_sym.module}.{clean_ret}"
|
|
611
|
+
cand_canon = f"{target_class_canon}.{method_part}"
|
|
612
|
+
if cand_canon in self.canonical_to_symbol:
|
|
613
|
+
target_sym = self.canonical_to_symbol[cand_canon]
|
|
614
|
+
conf = "HIGH"
|
|
615
|
+
reason = f"Resolved via factory return type '{recv_sym.canonical_id}() -> {cand_canon}'"
|
|
616
|
+
elif target_class_canon in class_bases_map:
|
|
617
|
+
for base_canon in class_bases_map[target_class_canon]:
|
|
618
|
+
cand_base_canon = f"{base_canon}.{method_part}"
|
|
619
|
+
if cand_base_canon in self.canonical_to_symbol:
|
|
620
|
+
target_sym = self.canonical_to_symbol[cand_base_canon]
|
|
621
|
+
conf = "HIGH"
|
|
622
|
+
reason = f"Resolved via factory return inherited method '{recv_sym.canonical_id}() -> {cand_base_canon}'"
|
|
623
|
+
break
|
|
624
|
+
|
|
625
|
+
if target_sym:
|
|
626
|
+
add_ref(
|
|
627
|
+
Reference(
|
|
628
|
+
source_symbol_id=caller_id,
|
|
629
|
+
target_symbol_id=recv_sym.canonical_id,
|
|
630
|
+
relationship="CALLS",
|
|
631
|
+
confidence="HIGH",
|
|
632
|
+
path=call.source_file,
|
|
633
|
+
start_line=call.line,
|
|
634
|
+
end_line=call.end_line,
|
|
635
|
+
evidence=f"Call factory '{recv_sym.canonical_id}()' at {call.source_file}:{call.line}",
|
|
636
|
+
source_hash=f_hash,
|
|
637
|
+
indexed_commit=self.indexed_commit,
|
|
638
|
+
)
|
|
639
|
+
)
|
|
640
|
+
add_edge(
|
|
641
|
+
GraphEdge(
|
|
642
|
+
source=caller_id,
|
|
643
|
+
target=recv_sym.canonical_id,
|
|
644
|
+
relationship="CALLS",
|
|
645
|
+
confidence="HIGH",
|
|
646
|
+
file=call.source_file,
|
|
647
|
+
start_line=call.line,
|
|
648
|
+
end_line=call.end_line,
|
|
649
|
+
evidence=f"Resolved factory call {caller_id} -> {recv_sym.canonical_id} at {call.source_file}:{call.line}",
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if target_sym:
|
|
654
|
+
resolved_calls[(call.source_file, call.line, call.callee)] = (
|
|
655
|
+
target_sym.canonical_id,
|
|
656
|
+
conf,
|
|
657
|
+
)
|
|
658
|
+
add_ref(
|
|
659
|
+
Reference(
|
|
660
|
+
source_symbol_id=caller_id,
|
|
661
|
+
target_symbol_id=target_sym.canonical_id,
|
|
662
|
+
relationship="CALLS",
|
|
663
|
+
confidence=conf,
|
|
664
|
+
path=call.source_file,
|
|
665
|
+
start_line=call.line,
|
|
666
|
+
end_line=call.end_line,
|
|
667
|
+
evidence=f"Call '{expr}()' at {call.source_file}:{call.line} -> {target_sym.canonical_id} ({reason})",
|
|
668
|
+
source_hash=f_hash,
|
|
669
|
+
indexed_commit=self.indexed_commit,
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
add_edge(
|
|
673
|
+
GraphEdge(
|
|
674
|
+
source=caller_id,
|
|
675
|
+
target=target_sym.canonical_id,
|
|
676
|
+
relationship="CALLS",
|
|
677
|
+
confidence=conf,
|
|
678
|
+
file=call.source_file,
|
|
679
|
+
start_line=call.line,
|
|
680
|
+
end_line=call.end_line,
|
|
681
|
+
evidence=f"Resolved call {caller_id} -> {target_sym.canonical_id} at {call.source_file}:{call.line}",
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
else:
|
|
685
|
+
# Target cannot be statically resolved via lexical/import/module rules
|
|
686
|
+
resolved_calls[(call.source_file, call.line, call.callee)] = (
|
|
687
|
+
None,
|
|
688
|
+
"UNKNOWN",
|
|
689
|
+
)
|
|
690
|
+
add_ref(
|
|
691
|
+
Reference(
|
|
692
|
+
source_symbol_id=caller_id,
|
|
693
|
+
target_symbol_id=None,
|
|
694
|
+
relationship="UNRESOLVED_REFERENCE",
|
|
695
|
+
confidence="UNKNOWN",
|
|
696
|
+
path=call.source_file,
|
|
697
|
+
start_line=call.line,
|
|
698
|
+
end_line=call.end_line,
|
|
699
|
+
evidence=f"Unresolved call '{expr}()' at {call.source_file}:{call.line} ({reason})",
|
|
700
|
+
source_hash=f_hash,
|
|
701
|
+
indexed_commit=self.indexed_commit,
|
|
702
|
+
)
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
# 4. Framework Route -> Handler edges
|
|
706
|
+
for rt in self.routes:
|
|
707
|
+
f_hash = self.file_hashes.get(rt.file_path, "")
|
|
708
|
+
handler_sym, conf, _reason = self.resolve_identifier_in_file(
|
|
709
|
+
rt.handler_name, rt.file_path
|
|
710
|
+
)
|
|
711
|
+
target_id = handler_sym.canonical_id if handler_sym else rt.handler_canonical_id
|
|
712
|
+
edge_conf = "HIGH" if handler_sym else conf
|
|
713
|
+
add_ref(
|
|
714
|
+
Reference(
|
|
715
|
+
source_symbol_id=rt.endpoint_id,
|
|
716
|
+
target_symbol_id=target_id,
|
|
717
|
+
relationship="ROUTES_TO",
|
|
718
|
+
confidence=edge_conf,
|
|
719
|
+
path=rt.file_path,
|
|
720
|
+
start_line=rt.line,
|
|
721
|
+
end_line=rt.line,
|
|
722
|
+
evidence=rt.evidence,
|
|
723
|
+
source_hash=f_hash,
|
|
724
|
+
indexed_commit=self.indexed_commit,
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
add_edge(
|
|
728
|
+
GraphEdge(
|
|
729
|
+
source=rt.endpoint_id,
|
|
730
|
+
target=target_id,
|
|
731
|
+
relationship="ROUTES_TO",
|
|
732
|
+
confidence=edge_conf,
|
|
733
|
+
file=rt.file_path,
|
|
734
|
+
start_line=rt.line,
|
|
735
|
+
end_line=rt.line,
|
|
736
|
+
evidence=rt.evidence,
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
return ResolutionOutput(
|
|
741
|
+
references=references,
|
|
742
|
+
edges=edges,
|
|
743
|
+
resolved_imports=resolved_imports,
|
|
744
|
+
resolved_calls=resolved_calls,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def resolve_symbol_diagnostic(
|
|
749
|
+
con: sqlite3.Connection,
|
|
750
|
+
expression: str,
|
|
751
|
+
context_file: str | None = None,
|
|
752
|
+
) -> dict[str, object]:
|
|
753
|
+
"""Provide structured diagnostic output for `codegraph resolve <expression>`."""
|
|
754
|
+
expr = expression.strip()
|
|
755
|
+
if not expr:
|
|
756
|
+
return {
|
|
757
|
+
"input": expr,
|
|
758
|
+
"confidence": "UNKNOWN",
|
|
759
|
+
"reason": "Empty symbol expression.",
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
# 1. Direct canonical or qualified match in symbols table
|
|
763
|
+
direct = con.execute(
|
|
764
|
+
"SELECT canonical_id, qualified_name, module, path, start_line "
|
|
765
|
+
"FROM symbols WHERE canonical_id=? OR qualified_name=? ORDER BY path, start_line LIMIT 1",
|
|
766
|
+
(expr, expr),
|
|
767
|
+
).fetchone()
|
|
768
|
+
|
|
769
|
+
# Check if expr starts with an imported alias first (e.g., AS.login or authenticate)
|
|
770
|
+
prefix = expr.split(".", 1)[0]
|
|
771
|
+
rest = expr.split(".", 1)[1] if "." in expr else ""
|
|
772
|
+
|
|
773
|
+
imp_query = (
|
|
774
|
+
"SELECT source_path, module, imported_module, name, imported_name, alias, local_name, resolved_module "
|
|
775
|
+
"FROM imports WHERE (alias=? OR local_name=?)"
|
|
776
|
+
)
|
|
777
|
+
params: list[object] = [prefix, prefix]
|
|
778
|
+
if context_file:
|
|
779
|
+
imp_query += " AND source_path=?"
|
|
780
|
+
params.append(context_file)
|
|
781
|
+
imp_rows = con.execute(imp_query, params).fetchall()
|
|
782
|
+
|
|
783
|
+
for imp in imp_rows:
|
|
784
|
+
target_mod = imp["resolved_module"] or imp["imported_module"] or imp["module"]
|
|
785
|
+
orig_name = imp["imported_name"] or imp["name"] or prefix
|
|
786
|
+
candidate_qname = f"{orig_name}.{rest}" if rest else orig_name
|
|
787
|
+
sym_row = con.execute(
|
|
788
|
+
"SELECT canonical_id, qualified_name, module, path, start_line "
|
|
789
|
+
"FROM symbols WHERE (module=? OR module LIKE ?) AND (qualified_name=? OR name=?) "
|
|
790
|
+
"ORDER BY path, start_line LIMIT 1",
|
|
791
|
+
(target_mod, f"%.{target_mod}", candidate_qname, candidate_qname),
|
|
792
|
+
).fetchone()
|
|
793
|
+
if sym_row:
|
|
794
|
+
return {
|
|
795
|
+
"input": expr,
|
|
796
|
+
"alias": imp["alias"] or imp["local_name"],
|
|
797
|
+
"resolved_module": target_mod,
|
|
798
|
+
"resolved_symbol": sym_row["qualified_name"],
|
|
799
|
+
"canonical_id": sym_row["canonical_id"],
|
|
800
|
+
"confidence": "HIGH",
|
|
801
|
+
"evidence": f"{sym_row['path']}:{sym_row['start_line']}",
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if direct:
|
|
805
|
+
return {
|
|
806
|
+
"input": expr,
|
|
807
|
+
"alias": None,
|
|
808
|
+
"resolved_module": direct["module"],
|
|
809
|
+
"resolved_symbol": direct["qualified_name"],
|
|
810
|
+
"canonical_id": direct["canonical_id"],
|
|
811
|
+
"confidence": "HIGH",
|
|
812
|
+
"evidence": f"{direct['path']}:{direct['start_line']}",
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
# Check short name match
|
|
816
|
+
short_rows = con.execute(
|
|
817
|
+
"SELECT canonical_id, qualified_name, module, path, start_line FROM symbols WHERE name=?",
|
|
818
|
+
(expr,),
|
|
819
|
+
).fetchall()
|
|
820
|
+
if len(short_rows) == 1:
|
|
821
|
+
r = short_rows[0]
|
|
822
|
+
return {
|
|
823
|
+
"input": expr,
|
|
824
|
+
"alias": None,
|
|
825
|
+
"resolved_module": r["module"],
|
|
826
|
+
"resolved_symbol": r["qualified_name"],
|
|
827
|
+
"canonical_id": r["canonical_id"],
|
|
828
|
+
"confidence": "MEDIUM",
|
|
829
|
+
"evidence": f"{r['path']}:{r['start_line']}",
|
|
830
|
+
}
|
|
831
|
+
if len(short_rows) > 1:
|
|
832
|
+
return {
|
|
833
|
+
"input": expr,
|
|
834
|
+
"confidence": "LOW",
|
|
835
|
+
"reason": f"Ambiguous symbol '{expr}' found in {len(short_rows)} modules: "
|
|
836
|
+
+ ", ".join(r["canonical_id"] for r in short_rows[:5]),
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return {
|
|
840
|
+
"input": expr,
|
|
841
|
+
"confidence": "UNKNOWN",
|
|
842
|
+
"reason": "No matching imported symbol found.",
|
|
843
|
+
}
|