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.
Files changed (62) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/architecture.py +328 -0
  4. codegraph/audit.py +106 -0
  5. codegraph/cache.py +95 -0
  6. codegraph/cli.py +854 -0
  7. codegraph/config.py +43 -0
  8. codegraph/constraints.py +238 -0
  9. codegraph/context.py +1228 -0
  10. codegraph/epistemic.py +90 -0
  11. codegraph/errors.py +275 -0
  12. codegraph/evidence/__init__.py +15 -0
  13. codegraph/evidence/citations.py +397 -0
  14. codegraph/frameworks.py +434 -0
  15. codegraph/freshness.py +295 -0
  16. codegraph/git.py +278 -0
  17. codegraph/graph/__init__.py +46 -0
  18. codegraph/graph/models.py +41 -0
  19. codegraph/graph/traversal.py +1291 -0
  20. codegraph/indexing/__init__.py +4 -0
  21. codegraph/indexing/classifier.py +274 -0
  22. codegraph/indexing/indexer.py +943 -0
  23. codegraph/indexing/models.py +338 -0
  24. codegraph/indexing/parser.py +1240 -0
  25. codegraph/indexing/scanner.py +200 -0
  26. codegraph/indexing/test_framework.py +116 -0
  27. codegraph/interrogation.py +1582 -0
  28. codegraph/llm/__init__.py +3 -0
  29. codegraph/llm/base.py +15 -0
  30. codegraph/llm/context.py +20 -0
  31. codegraph/mcp/__init__.py +3 -0
  32. codegraph/mcp/server.py +736 -0
  33. codegraph/memory/__init__.py +3 -0
  34. codegraph/memory/store.py +46 -0
  35. codegraph/models.py +289 -0
  36. codegraph/observability.py +151 -0
  37. codegraph/optimizer.py +372 -0
  38. codegraph/planner.py +417 -0
  39. codegraph/py.typed +1 -0
  40. codegraph/query_expansion.py +199 -0
  41. codegraph/ranking.py +363 -0
  42. codegraph/resolver.py +843 -0
  43. codegraph/resources/__init__.py +45 -0
  44. codegraph/resources/cache.py +117 -0
  45. codegraph/resources/coalescer.py +83 -0
  46. codegraph/resources/debouncer.py +98 -0
  47. codegraph/resources/governor.py +232 -0
  48. codegraph/resources/policy.py +123 -0
  49. codegraph/retrieval_policy.py +220 -0
  50. codegraph/search/__init__.py +23 -0
  51. codegraph/search/hybrid.py +301 -0
  52. codegraph/search/semantic.py +28 -0
  53. codegraph/security/__init__.py +3 -0
  54. codegraph/security/paths.py +35 -0
  55. codegraph/target_resolver.py +348 -0
  56. codegraph/task.py +637 -0
  57. codegraph_engine-2.1.1.dist-info/METADATA +334 -0
  58. codegraph_engine-2.1.1.dist-info/RECORD +62 -0
  59. codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
  60. codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
  61. codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
  62. 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
+ }