codegraph-engine 3.0.0__2-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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
codegraph/resolver.py ADDED
@@ -0,0 +1,3968 @@
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 json
19
+ import posixpath
20
+ import re
21
+ import sqlite3
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+
25
+ from codegraph.binding_resolver import LocalBindingResolver
26
+ from codegraph.epistemic import RelationshipEvidenceClass
27
+ from codegraph.frameworks import RouteDetection
28
+ from codegraph.graph.models import GraphEdge
29
+ from codegraph.indexing.models import (
30
+ BindingRef,
31
+ CallRef,
32
+ ImportRef,
33
+ InheritanceRef,
34
+ Reference,
35
+ Symbol,
36
+ normalize_module,
37
+ )
38
+ from codegraph.monorepo import WorkspaceInfo
39
+ from codegraph.route_composer import ComposedMountEdge
40
+ from codegraph.route_topology import match_route_topology, normalize_route
41
+ from codegraph.tsconfig import TsConfigResolver
42
+
43
+ _TEST_FILE_RE = re.compile(
44
+ r"(^|[_/])test[_s]?[_/]|[_/]spec[_/]|test[_s]?\.(py|js|ts|jsx|tsx)$|spec\.(py|js|ts|jsx|tsx)$",
45
+ re.IGNORECASE,
46
+ )
47
+
48
+ DEFAULT_MAX_REEXPORT_DEPTH = 16
49
+ DEFAULT_MAX_WILDCARD_EXPANSIONS = 64
50
+
51
+
52
+ def resolve_module_to_path(
53
+ imported_module: str,
54
+ source_file: str,
55
+ known_files: set[str],
56
+ workspace: WorkspaceInfo | None = None,
57
+ tsconfig_resolver: TsConfigResolver | None = None,
58
+ ) -> str | None:
59
+ """Deterministically resolve an import specifier to a repository-relative file path.
60
+
61
+ Never guesses files outside `known_files`.
62
+ """
63
+ if not imported_module:
64
+ return None
65
+
66
+ # 0. Check workspace package mappings for cross-package imports
67
+ if workspace is not None:
68
+ target_pkg, entrypoint = workspace.resolve_package_import(imported_module, source_file)
69
+ if entrypoint:
70
+ clean_entry = entrypoint.replace("\\", "/").lstrip("./")
71
+ if clean_entry in known_files:
72
+ return clean_entry
73
+ for ext in (".ts", ".tsx", ".js", ".jsx", ".py"):
74
+ if f"{clean_entry}{ext}" in known_files:
75
+ return f"{clean_entry}{ext}"
76
+ if f"{clean_entry}/index{ext}" in known_files:
77
+ return f"{clean_entry}/index{ext}"
78
+ if f"{clean_entry}/__init__{ext}" in known_files:
79
+ return f"{clean_entry}/__init__{ext}"
80
+
81
+ # 0b. Check tsconfig.json / jsconfig.json path aliases
82
+ if tsconfig_resolver is not None:
83
+ alias_target = tsconfig_resolver.resolve_alias(imported_module, source_file, known_files)
84
+ if alias_target:
85
+ return alias_target
86
+
87
+ src_clean = source_file.replace("\\", "/").lstrip("./")
88
+ src_dir = posixpath.dirname(src_clean)
89
+
90
+ candidates: list[str] = []
91
+
92
+ if imported_module.startswith(".") or "/" in imported_module:
93
+ # Relative or path-based import (JS/TS or relative path)
94
+ if imported_module.startswith("."):
95
+ joined = posixpath.normpath(posixpath.join(src_dir, imported_module)) if src_dir else posixpath.normpath(imported_module)
96
+ else:
97
+ joined = posixpath.normpath(imported_module.lstrip("/"))
98
+
99
+ if joined.startswith(".."):
100
+ return None
101
+ if joined == ".":
102
+ joined = src_dir
103
+
104
+ candidates.append(joined)
105
+ for ext in (".ts", ".tsx", ".js", ".jsx", ".py", ".mjs", ".cjs"):
106
+ candidates.append(f"{joined}{ext}")
107
+ for idx_file in ("index.ts", "index.tsx", "index.js", "index.jsx", "__init__.py"):
108
+ candidates.append(posixpath.join(joined, idx_file) if joined else idx_file)
109
+ else:
110
+ # Dotted module specifier (Python or package root path)
111
+ mod_path = imported_module.replace(".", "/")
112
+ prefixes = [""]
113
+ if src_dir:
114
+ prefixes.append(f"{src_dir}/")
115
+ # Also try top-level package root (e.g. 'src/')
116
+ first_seg = src_dir.split("/")[0]
117
+ if f"{first_seg}/" not in prefixes:
118
+ prefixes.append(f"{first_seg}/")
119
+ if "src/" not in prefixes:
120
+ prefixes.append("src/")
121
+
122
+ for prefix in prefixes:
123
+ base = f"{prefix}{mod_path}"
124
+ candidates.append(f"{base}.py")
125
+ candidates.append(f"{base}/__init__.py")
126
+ for ext in (".ts", ".tsx", ".js", ".jsx"):
127
+ candidates.append(f"{base}{ext}")
128
+ candidates.append(f"{base}/index{ext}")
129
+
130
+ for cand in candidates:
131
+ norm = cand.lstrip("./")
132
+ if norm in known_files:
133
+ return norm
134
+ return None
135
+
136
+
137
+ @dataclass
138
+ class ResolutionOutput:
139
+ references: list[Reference]
140
+ edges: list[GraphEdge]
141
+ resolved_imports: dict[tuple[str, int, str], tuple[str | None, str | None]]
142
+ resolved_calls: dict[tuple[str, int, str], tuple[str | None, str]]
143
+ findings: list[dict[str, object]] = field(default_factory=list)
144
+
145
+
146
+
147
+ class ReferenceResolver:
148
+ """Resolves imports, aliases, re-exports, inheritance, calls, and routes across a repository."""
149
+
150
+ def __init__(
151
+ self,
152
+ symbols: list[Symbol],
153
+ imports: list[ImportRef],
154
+ calls: list[CallRef],
155
+ inheritance: list[InheritanceRef],
156
+ routes: list[RouteDetection],
157
+ known_files: set[str],
158
+ file_hashes: dict[str, str],
159
+ indexed_commit: str | None = None,
160
+ mount_edges: list[ComposedMountEdge] | None = None,
161
+ bindings: list[BindingRef] | None = None,
162
+ workspace: WorkspaceInfo | None = None,
163
+ tsconfig_resolver: TsConfigResolver | None = None,
164
+ repository: Path | None = None,
165
+ max_reexport_depth: int = 16,
166
+ max_wildcard_expansions: int = 64,
167
+ ) -> None:
168
+ self.symbols = symbols
169
+ self.imports = imports
170
+ self.calls = calls
171
+ self.inheritance = inheritance
172
+ self.routes = routes
173
+ self.mount_edges = mount_edges or []
174
+ self.bindings = bindings or []
175
+ self.known_files = known_files
176
+ self.file_hashes = file_hashes
177
+ self.indexed_commit = indexed_commit
178
+ self.workspace = workspace
179
+ self.tsconfig_resolver = tsconfig_resolver
180
+ self.repository = repository
181
+ self.max_reexport_depth = max_reexport_depth
182
+ self.max_wildcard_expansions = max_wildcard_expansions
183
+
184
+ # Memoization caches and budget tracking
185
+ self._import_target_cache: dict[tuple[str, str], tuple[str | None, str | None]] = {}
186
+ self._symbol_in_module_cache: dict[tuple[str, str], Symbol | None] = {}
187
+ self.budget_findings: list[dict[str, object]] = []
188
+ self._seen_budget_keys: set[tuple[str, str]] = set()
189
+
190
+ # Indexed lookup maps (O(1) lookup)
191
+ self.canonical_to_symbol: dict[str, Symbol] = {}
192
+ self.qualified_to_symbols: dict[str, list[Symbol]] = {}
193
+ self.module_to_symbols: dict[str, dict[str, Symbol]] = {}
194
+ self.short_to_symbols: dict[str, list[Symbol]] = {}
195
+ self.path_to_symbols: dict[str, list[Symbol]] = {}
196
+ self.file_to_module: dict[str, str] = {
197
+ f: normalize_module(f) for f in known_files
198
+ }
199
+ self.module_to_file: dict[str, str] = {
200
+ normalize_module(f): f for f in sorted(known_files)
201
+ }
202
+
203
+ for sym in symbols:
204
+ self.canonical_to_symbol[sym.canonical_id] = sym
205
+ self.qualified_to_symbols.setdefault(sym.qualified_name, []).append(sym)
206
+ mod_map = self.module_to_symbols.setdefault(sym.module, {})
207
+ mod_map[sym.qualified_name] = sym
208
+ mod_map.setdefault(sym.name, sym)
209
+ self.short_to_symbols.setdefault(sym.name, []).append(sym)
210
+ self.path_to_symbols.setdefault(sym.path, []).append(sym)
211
+
212
+ self.file_imports: dict[str, list[ImportRef]] = {}
213
+ for imp in imports:
214
+ self.file_imports.setdefault(imp.source_file, []).append(imp)
215
+
216
+ # Map (source_module, exported_name) -> (target_module, target_name)
217
+ self.reexport_map: dict[tuple[str, str], tuple[str, str]] = {}
218
+ self.wildcard_reexports: dict[str, list[str]] = {}
219
+ self._build_reexport_maps()
220
+
221
+ # Local deterministic binding and data-flow engine
222
+ self.binding_resolver = LocalBindingResolver(
223
+ bindings=self.bindings,
224
+ symbols=self.symbols,
225
+ imports=self.imports,
226
+ known_files=self.known_files,
227
+ resolve_symbol_in_module=self.resolve_symbol_in_module,
228
+ resolve_import_target=self._resolve_import_target_module,
229
+ )
230
+
231
+ def _record_budget_exceeded(
232
+ self,
233
+ module: str,
234
+ symbol_name: str,
235
+ file_path: str | None = None,
236
+ line: int = 1,
237
+ ) -> None:
238
+ b_key = (module, symbol_name)
239
+ if b_key in self._seen_budget_keys:
240
+ return
241
+ self._seen_budget_keys.add(b_key)
242
+ self.budget_findings.append(
243
+ {
244
+ "pattern": "WILDCARD_OR_REEXPORT_RESOLUTION",
245
+ "status": "UNKNOWN",
246
+ "reason": "resolution_budget_exceeded",
247
+ "module": module,
248
+ "symbol": symbol_name,
249
+ "file": file_path or self.module_to_file.get(module, ""),
250
+ "line": line,
251
+ }
252
+ )
253
+
254
+ @staticmethod
255
+ def resolve_symbol_diagnostic(con: sqlite3.Connection, query: str) -> dict[str, object]:
256
+ return resolve_symbol_diagnostic(con, query)
257
+
258
+ def _resolve_import_target_module(self, imp: ImportRef) -> tuple[str | None, str | None]:
259
+ """Return (resolved_file_path, resolved_module) for an ImportRef."""
260
+ if self.workspace is not None:
261
+ cache_ctx = imp.source_file
262
+ else:
263
+ cache_ctx = posixpath.dirname(imp.source_file.replace("\\", "/").lstrip("./"))
264
+ cache_key = (imp.imported_module, cache_ctx)
265
+ if cache_key in self._import_target_cache:
266
+ return self._import_target_cache[cache_key]
267
+
268
+ res = self._resolve_import_target_module_uncached(imp)
269
+ self._import_target_cache[cache_key] = res
270
+ return res
271
+
272
+ def _resolve_import_target_module_uncached(self, imp: ImportRef) -> tuple[str | None, str | None]:
273
+ target_path = resolve_module_to_path(
274
+ imp.imported_module,
275
+ imp.source_file,
276
+ self.known_files,
277
+ workspace=self.workspace,
278
+ tsconfig_resolver=self.tsconfig_resolver,
279
+ )
280
+ if target_path:
281
+ return target_path, normalize_module(target_path)
282
+
283
+ # Maybe imported_module is `pkg.mod.Symbol` where `pkg.mod` is a file
284
+ if "." in imp.imported_module and not imp.imported_module.startswith("."):
285
+ parent_mod, last_seg = imp.imported_module.rsplit(".", 1)
286
+ parent_path = resolve_module_to_path(
287
+ parent_mod,
288
+ imp.source_file,
289
+ self.known_files,
290
+ workspace=self.workspace,
291
+ tsconfig_resolver=self.tsconfig_resolver,
292
+ )
293
+ if parent_path:
294
+ return parent_path, normalize_module(parent_path)
295
+ # Check if normalized module exists directly
296
+ if imp.imported_module in self.module_to_file:
297
+ p = self.module_to_file[imp.imported_module]
298
+ return p, imp.imported_module
299
+ if f"src.{imp.imported_module}" in self.module_to_file:
300
+ mod = f"src.{imp.imported_module}"
301
+ return self.module_to_file[mod], mod
302
+ del last_seg
303
+
304
+ if imp.imported_module in self.module_to_file:
305
+ p = self.module_to_file[imp.imported_module]
306
+ return p, imp.imported_module
307
+ if f"src.{imp.imported_module}" in self.module_to_file:
308
+ mod = f"src.{imp.imported_module}"
309
+ return self.module_to_file[mod], mod
310
+
311
+ return None, None
312
+
313
+ def _build_reexport_maps(self) -> None:
314
+ for imp in self.imports:
315
+ if not imp.is_reexport:
316
+ continue
317
+ _target_path, target_mod = self._resolve_import_target_module(imp)
318
+ if not target_mod:
319
+ continue
320
+ src_mod = imp.source_module or normalize_module(imp.source_file)
321
+ if imp.imported_name == "*" and not imp.alias:
322
+ targets = self.wildcard_reexports.setdefault(src_mod, [])
323
+ if target_mod not in targets:
324
+ targets.append(target_mod)
325
+ else:
326
+ exported_nm = imp.exported_name or imp.alias or imp.imported_name or imp.local_name
327
+ orig_nm = imp.imported_name or exported_nm
328
+ if exported_nm and orig_nm:
329
+ self.reexport_map[(src_mod, exported_nm)] = (target_mod, orig_nm)
330
+
331
+ def resolve_symbol_in_module(
332
+ self,
333
+ module: str,
334
+ symbol_name: str,
335
+ visited: set[tuple[str, str]] | None = None,
336
+ depth: int = 0,
337
+ budget: list[int] | None = None,
338
+ ) -> Symbol | None:
339
+ """Resolve `symbol_name` in `module`, following re-export chains deterministically with bounded budgets."""
340
+ is_top_level = visited is None and depth == 0
341
+ cache_key = (module, symbol_name)
342
+ if is_top_level and cache_key in self._symbol_in_module_cache:
343
+ return self._symbol_in_module_cache[cache_key]
344
+
345
+ if budget is None:
346
+ budget = [0]
347
+
348
+ res = self._resolve_symbol_in_module_impl(
349
+ module, symbol_name, visited, depth, budget, root_module=module, root_symbol=symbol_name
350
+ )
351
+ if is_top_level:
352
+ self._symbol_in_module_cache[cache_key] = res
353
+ return res
354
+
355
+ def _resolve_symbol_in_module_impl(
356
+ self,
357
+ module: str,
358
+ symbol_name: str,
359
+ visited: set[tuple[str, str]] | None,
360
+ depth: int,
361
+ budget: list[int],
362
+ root_module: str,
363
+ root_symbol: str,
364
+ ) -> Symbol | None:
365
+ if depth > self.max_reexport_depth or budget[0] > self.max_wildcard_expansions:
366
+ self._record_budget_exceeded(root_module, root_symbol)
367
+ self._record_budget_exceeded(module, symbol_name)
368
+ return None
369
+
370
+ visited = visited or set()
371
+ key = (module, symbol_name)
372
+ if key in visited:
373
+ return None
374
+ visited.add(key)
375
+
376
+ # 1. Direct symbol in module
377
+ mod_syms = self.module_to_symbols.get(module)
378
+ if mod_syms and symbol_name in mod_syms:
379
+ return mod_syms[symbol_name]
380
+
381
+ # Check canonical ID directly
382
+ canon_cand = f"{module}.{symbol_name}"
383
+ if canon_cand in self.canonical_to_symbol:
384
+ return self.canonical_to_symbol[canon_cand]
385
+
386
+ # If symbol_name is dotted (e.g. AS.login or AuthService.login), resolve prefix first
387
+ if "." in symbol_name:
388
+ prefix, rest = symbol_name.split(".", 1)
389
+ base_sym = self._resolve_symbol_in_module_impl(
390
+ module, prefix, set(visited), depth + 1, budget, root_module, root_symbol
391
+ )
392
+ if base_sym:
393
+ method_canon = f"{base_sym.canonical_id}.{rest}"
394
+ if method_canon in self.canonical_to_symbol:
395
+ return self.canonical_to_symbol[method_canon]
396
+
397
+ # 2. Named re-export in this module
398
+ if key in self.reexport_map:
399
+ target_mod, orig_name = self.reexport_map[key]
400
+ return self._resolve_symbol_in_module_impl(
401
+ target_mod, orig_name, visited, depth + 1, budget, root_module, root_symbol
402
+ )
403
+
404
+ # 3. Wildcard re-exports in this module (bounded by max_wildcard_expansions)
405
+ for target_mod in self.wildcard_reexports.get(module, ()):
406
+ budget[0] += 1
407
+ if budget[0] > self.max_wildcard_expansions:
408
+ self._record_budget_exceeded(root_module, root_symbol)
409
+ self._record_budget_exceeded(module, symbol_name)
410
+ return None
411
+ found = self._resolve_symbol_in_module_impl(
412
+ target_mod, symbol_name, visited, depth + 1, budget, root_module, root_symbol
413
+ )
414
+ if found:
415
+ return found
416
+
417
+ # 4. Default export fallback: if symbol_name == "default" and module has a single class/function
418
+ if symbol_name == "default" and mod_syms:
419
+ top_level = [s for s in mod_syms.values() if not s.scope]
420
+ if len(top_level) == 1:
421
+ return top_level[0]
422
+
423
+ return None
424
+
425
+ def resolve_identifier_in_file(
426
+ self,
427
+ identifier: str,
428
+ source_file: str,
429
+ caller_canonical_id: str | None = None,
430
+ use_line: int | None = None,
431
+ ) -> tuple[Symbol | None, str, str]:
432
+ """Resolve an identifier or dotted expression inside `source_file`.
433
+
434
+ Returns (Symbol | None, confidence, resolution_reason).
435
+ """
436
+ src_mod = self.file_to_module.get(source_file) or normalize_module(source_file)
437
+
438
+ # Level 1: Lexical / local scope
439
+ if caller_canonical_id and caller_canonical_id in self.canonical_to_symbol:
440
+ caller_sym = self.canonical_to_symbol[caller_canonical_id]
441
+ # Check nested symbol inside caller or sibling in same scope
442
+ nested_canon = f"{caller_sym.canonical_id}.{identifier}"
443
+ if nested_canon in self.canonical_to_symbol:
444
+ return (
445
+ self.canonical_to_symbol[nested_canon],
446
+ "HIGH",
447
+ "Local nested symbol in lexical scope",
448
+ )
449
+ if caller_sym.scope:
450
+ sibling_canon = f"{src_mod}.{caller_sym.scope}.{identifier}"
451
+ if sibling_canon in self.canonical_to_symbol:
452
+ return (
453
+ self.canonical_to_symbol[sibling_canon],
454
+ "HIGH",
455
+ f"Class/scope member in {caller_sym.scope}",
456
+ )
457
+
458
+ # Same-module symbol
459
+ local_sym = self.resolve_symbol_in_module(src_mod, identifier)
460
+ if local_sym:
461
+ return local_sym, "HIGH", f"Defined in same module '{src_mod}'"
462
+ if (src_mod, identifier) in self._seen_budget_keys:
463
+ return None, "UNKNOWN", "resolution_budget_exceeded"
464
+
465
+ # Level 2: Exact canonical symbol match
466
+ if identifier in self.canonical_to_symbol:
467
+ return (
468
+ self.canonical_to_symbol[identifier],
469
+ "HIGH",
470
+ "Exact canonical symbol match",
471
+ )
472
+
473
+ # Level 3 & 4: Imported alias, named import, default import, or namespace/qualified import
474
+ imps = self.file_imports.get(source_file, [])
475
+ if "." in identifier:
476
+ prefix, rest = identifier.split(".", 1)
477
+ # Check if prefix is a local class in same module
478
+ local_cls = self.resolve_symbol_in_module(src_mod, prefix)
479
+ if local_cls:
480
+ member_canon = f"{local_cls.canonical_id}.{rest}"
481
+ if member_canon in self.canonical_to_symbol:
482
+ return (
483
+ self.canonical_to_symbol[member_canon],
484
+ "HIGH",
485
+ f"Resolved method '{rest}' on local class '{local_cls.canonical_id}'",
486
+ )
487
+
488
+ # Check if prefix is an imported symbol/alias or namespace
489
+ for imp in imps:
490
+ if imp.local_name == prefix or imp.alias == prefix or imp.name == prefix:
491
+ _tpath, target_mod = self._resolve_import_target_module(imp)
492
+ if target_mod:
493
+ if imp.import_type in ("namespace", "module") or imp.imported_name in (None, "*"):
494
+ sym = self.resolve_symbol_in_module(target_mod, rest)
495
+ if sym:
496
+ return (
497
+ sym,
498
+ "HIGH",
499
+ f"Resolved via namespace import '{prefix}' -> '{sym.canonical_id}'",
500
+ )
501
+ if (target_mod, rest) in self._seen_budget_keys:
502
+ return None, "UNKNOWN", "resolution_budget_exceeded"
503
+ else:
504
+ orig_cls = imp.imported_name or prefix
505
+ cls_sym = self.resolve_symbol_in_module(
506
+ target_mod, orig_cls
507
+ ) or self.resolve_symbol_in_module(target_mod, prefix)
508
+ if cls_sym:
509
+ method_canon = f"{cls_sym.canonical_id}.{rest}"
510
+ if method_canon in self.canonical_to_symbol:
511
+ return (
512
+ self.canonical_to_symbol[method_canon],
513
+ "HIGH",
514
+ f"Resolved via imported symbol '{prefix}' -> '{method_canon}'",
515
+ )
516
+ # Or target_mod is actually a package and orig_cls is a submodule
517
+ sub_sym = self.resolve_symbol_in_module(f"{target_mod}.{orig_cls}", rest)
518
+ if sub_sym:
519
+ return (
520
+ sub_sym,
521
+ "HIGH",
522
+ f"Resolved via submodule import '{prefix}.{rest}' -> '{sub_sym.canonical_id}'",
523
+ )
524
+ else:
525
+ wildcard_imps: list[ImportRef] = []
526
+ for imp in imps:
527
+ if imp.local_name == identifier or imp.alias == identifier:
528
+ _tpath, target_mod = self._resolve_import_target_module(imp)
529
+ if target_mod:
530
+ if imp.import_type == "default":
531
+ sym = self.resolve_symbol_in_module(
532
+ target_mod, imp.local_name
533
+ ) or self.resolve_symbol_in_module(target_mod, "default")
534
+ else:
535
+ target_name = imp.imported_name or identifier
536
+ sym = self.resolve_symbol_in_module(target_mod, target_name)
537
+ if sym:
538
+ return (
539
+ sym,
540
+ "HIGH",
541
+ f"Resolved via import '{imp.local_name}' from '{target_mod}'",
542
+ )
543
+ if (target_mod, imp.imported_name or identifier) in self._seen_budget_keys:
544
+ return None, "UNKNOWN", "resolution_budget_exceeded"
545
+ elif imp.imported_name == "*" and not imp.alias:
546
+ wildcard_imps.append(imp)
547
+
548
+ if wildcard_imps and not identifier.startswith("_"):
549
+ if len(wildcard_imps) > self.max_wildcard_expansions:
550
+ self._record_budget_exceeded(src_mod, identifier, file_path=source_file, line=use_line or 1)
551
+ return None, "UNKNOWN", "resolution_budget_exceeded"
552
+ wildcard_matches: dict[str, tuple[Symbol, str]] = {}
553
+ wc_budget = [0]
554
+ for w_imp in wildcard_imps:
555
+ _tpath, target_mod = self._resolve_import_target_module(w_imp)
556
+ if not target_mod:
557
+ continue
558
+ wc_budget[0] += 1
559
+ if wc_budget[0] > self.max_wildcard_expansions:
560
+ self._record_budget_exceeded(src_mod, identifier, file_path=source_file, line=use_line or 1)
561
+ return None, "UNKNOWN", "resolution_budget_exceeded"
562
+ w_sym = self.resolve_symbol_in_module(target_mod, identifier, budget=wc_budget)
563
+ if (target_mod, identifier) in self._seen_budget_keys or wc_budget[0] > self.max_wildcard_expansions:
564
+ self._record_budget_exceeded(src_mod, identifier, file_path=source_file, line=use_line or 1)
565
+ return None, "UNKNOWN", "resolution_budget_exceeded"
566
+ if w_sym:
567
+ wildcard_matches[w_sym.canonical_id] = (w_sym, target_mod)
568
+ if len(wildcard_matches) == 1:
569
+ only_sym, only_mod = next(iter(wildcard_matches.values()))
570
+ return (
571
+ only_sym,
572
+ "HIGH",
573
+ f"Resolved via wildcard import from '{only_mod}'",
574
+ )
575
+ if len(wildcard_matches) > 1:
576
+ return None, "UNKNOWN", "ambiguous_wildcard_import"
577
+
578
+ # Level 5: Exact qualified name match if unique across repository
579
+ if "." in identifier and identifier in self.qualified_to_symbols:
580
+ cands = self.qualified_to_symbols[identifier]
581
+ if len(cands) == 1:
582
+ return (
583
+ cands[0],
584
+ "MEDIUM",
585
+ f"Unique qualified symbol match '{cands[0].canonical_id}'",
586
+ )
587
+
588
+ # Level 6: Local Binding and Data-Flow resolution
589
+ caller_scope = ""
590
+ if caller_canonical_id and caller_canonical_id in self.canonical_to_symbol:
591
+ caller_scope = self.canonical_to_symbol[caller_canonical_id].scope
592
+
593
+ canon_id, ev_class, reason = self.binding_resolver.resolve(
594
+ file_path=source_file,
595
+ name=identifier,
596
+ scope=caller_scope,
597
+ use_line=use_line,
598
+ )
599
+ if canon_id:
600
+ if canon_id in self.canonical_to_symbol:
601
+ return self.canonical_to_symbol[canon_id], "HIGH", reason
602
+ # Dotted or submodule symbol
603
+ target_sym = self.resolve_symbol_in_module(src_mod, canon_id)
604
+ if target_sym:
605
+ return target_sym, "HIGH", reason
606
+
607
+ return None, "UNKNOWN", f"No matching local or imported symbol found ({reason if canon_id is None else ''})."
608
+
609
+ def resolve_all(self) -> ResolutionOutput:
610
+ """Run the full resolution pipeline and emit deduplicated References and GraphEdges."""
611
+ references: list[Reference] = []
612
+ edges: list[GraphEdge] = []
613
+ seen_refs: set[tuple[str, str | None, str, str, int]] = set()
614
+ seen_edges: set[tuple[str, str, str, str, int]] = set()
615
+ resolved_imports: dict[tuple[str, int, str], tuple[str | None, str | None]] = {}
616
+ resolved_calls: dict[tuple[str, int, str], tuple[str | None, str]] = {}
617
+
618
+ def add_ref(ref: Reference) -> None:
619
+ key = (
620
+ ref.source_symbol_id,
621
+ ref.target_symbol_id,
622
+ ref.relationship,
623
+ ref.path,
624
+ ref.start_line,
625
+ )
626
+ if key not in seen_refs:
627
+ seen_refs.add(key)
628
+ references.append(ref)
629
+
630
+ _NON_CODE_EXTS = (
631
+ ".html",
632
+ ".htm",
633
+ ".jinja",
634
+ ".jinja2",
635
+ ".css",
636
+ ".yaml",
637
+ ".yml",
638
+ ".json",
639
+ ".md",
640
+ ".markdown",
641
+ )
642
+
643
+ def add_edge(edge: GraphEdge) -> None:
644
+ fpath = str(edge.file or "")
645
+ if fpath.endswith(_NON_CODE_EXTS):
646
+ return
647
+ key = (
648
+ edge.source,
649
+ edge.target,
650
+ edge.relationship,
651
+ edge.file,
652
+ edge.start_line,
653
+ )
654
+ if key not in seen_edges:
655
+ seen_edges.add(key)
656
+ edges.append(edge)
657
+
658
+ def _find_caller_id_for_binding(b: BindingRef) -> str:
659
+ for s in self.path_to_symbols.get(b.file_path, ()):
660
+ if s.start_line <= b.line <= s.end_line:
661
+ return s.canonical_id
662
+ mod = self.file_to_module.get(b.file_path) or normalize_module(b.file_path)
663
+ return f"{mod}.{b.scope}" if b.scope else mod
664
+
665
+ # 0. Definition & Containment edges
666
+ for sym in self.symbols:
667
+ if sym.path.endswith(_NON_CODE_EXTS):
668
+ continue
669
+ f_hash = self.file_hashes.get(sym.path, "")
670
+ if sym.parent_symbol_id:
671
+ add_edge(
672
+ GraphEdge(
673
+ source=sym.parent_symbol_id,
674
+ target=sym.canonical_id,
675
+ relationship="CONTAINS",
676
+ confidence="HIGH",
677
+ file=sym.path,
678
+ start_line=sym.start_line,
679
+ end_line=sym.end_line,
680
+ evidence=f"Scope containment {sym.parent_symbol_id} -> {sym.canonical_id}",
681
+ )
682
+ )
683
+ else:
684
+ add_edge(
685
+ GraphEdge(
686
+ source=sym.path,
687
+ target=sym.canonical_id,
688
+ relationship="DEFINES",
689
+ confidence="HIGH",
690
+ file=sym.path,
691
+ start_line=sym.start_line,
692
+ end_line=sym.end_line,
693
+ evidence=f"File {sym.path} defines {sym.canonical_id}",
694
+ )
695
+ )
696
+ del f_hash
697
+
698
+ # 1. Resolve Imports & Re-exports
699
+ for imp in self.imports:
700
+ src_mod = imp.source_module or normalize_module(imp.source_file)
701
+ f_hash = self.file_hashes.get(imp.source_file, "")
702
+ target_path, target_mod = self._resolve_import_target_module(imp)
703
+ resolved_imports[(imp.source_file, imp.line, imp.local_name)] = (
704
+ target_path,
705
+ target_mod,
706
+ )
707
+
708
+ if imp.is_reexport:
709
+ exp_name = imp.exported_name or imp.alias or imp.imported_name or "*"
710
+ orig_name = imp.imported_name or exp_name
711
+ target_sym = (
712
+ self.resolve_symbol_in_module(target_mod, orig_name)
713
+ if (target_mod and orig_name != "*")
714
+ else None
715
+ )
716
+ target_id = (
717
+ target_sym.canonical_id
718
+ if target_sym
719
+ else (f"{target_mod}.{orig_name}" if target_mod else f"{imp.imported_module}.{orig_name}")
720
+ )
721
+ conf = "HIGH" if (target_sym or target_mod) else "LOW"
722
+ src_id = f"{src_mod}.{exp_name}" if exp_name != "*" else src_mod
723
+ add_ref(
724
+ Reference(
725
+ source_symbol_id=src_id,
726
+ target_symbol_id=target_id,
727
+ relationship="REEXPORTS",
728
+ confidence=conf,
729
+ path=imp.source_file,
730
+ start_line=imp.line,
731
+ end_line=imp.line,
732
+ evidence=f"Re-export '{exp_name}' from '{imp.imported_module}' -> '{target_id}'",
733
+ source_hash=f_hash,
734
+ indexed_commit=self.indexed_commit,
735
+ )
736
+ )
737
+ add_edge(
738
+ GraphEdge(
739
+ source=src_id,
740
+ target=target_id,
741
+ relationship="REEXPORTS",
742
+ confidence=conf,
743
+ file=imp.source_file,
744
+ start_line=imp.line,
745
+ end_line=imp.line,
746
+ evidence=f"Re-export '{exp_name}' from '{imp.imported_module}'",
747
+ )
748
+ )
749
+ else:
750
+ target_sym = None
751
+ if target_mod and imp.imported_name and imp.imported_name != "*":
752
+ target_sym = self.resolve_symbol_in_module(target_mod, imp.imported_name)
753
+ target_id = (
754
+ target_sym.canonical_id
755
+ if target_sym
756
+ else (target_mod or imp.imported_module)
757
+ )
758
+ conf = "HIGH"
759
+ add_ref(
760
+ Reference(
761
+ source_symbol_id=src_mod,
762
+ target_symbol_id=target_id,
763
+ relationship="IMPORTS",
764
+ confidence=conf,
765
+ path=imp.source_file,
766
+ start_line=imp.line,
767
+ end_line=imp.line,
768
+ evidence=f"Import '{imp.local_name}' from '{imp.imported_module}'",
769
+ source_hash=f_hash,
770
+ indexed_commit=self.indexed_commit,
771
+ )
772
+ )
773
+ add_edge(
774
+ GraphEdge(
775
+ source=imp.source_file,
776
+ target=target_path or imp.imported_module,
777
+ relationship="IMPORTS",
778
+ confidence="HIGH",
779
+ file=imp.source_file,
780
+ start_line=imp.line,
781
+ end_line=imp.line,
782
+ evidence=f"Import '{imp.imported_module}' at {imp.source_file}:{imp.line}",
783
+ )
784
+ )
785
+
786
+ # 2. Resolve Inheritance (EXTENDS / IMPLEMENTS) inside a map so inherited methods can also resolve
787
+ class_bases_map: dict[str, list[str]] = {}
788
+ for inh in self.inheritance:
789
+ f_hash = self.file_hashes.get(inh.source_file, "")
790
+ target_sym, conf, reason = self.resolve_identifier_in_file(
791
+ inh.base_name, inh.source_file, inh.source_canonical_id
792
+ )
793
+ target_id = target_sym.canonical_id if target_sym else inh.base_name
794
+ if target_sym:
795
+ class_bases_map.setdefault(inh.source_canonical_id, []).append(
796
+ target_sym.canonical_id
797
+ )
798
+ add_ref(
799
+ Reference(
800
+ source_symbol_id=inh.source_canonical_id,
801
+ target_symbol_id=target_id,
802
+ relationship=inh.relationship,
803
+ confidence=conf,
804
+ path=inh.source_file,
805
+ start_line=inh.line,
806
+ end_line=inh.line,
807
+ evidence=f"{inh.source_canonical_id} {inh.relationship} {target_id} ({reason})",
808
+ source_hash=f_hash,
809
+ indexed_commit=self.indexed_commit,
810
+ )
811
+ )
812
+ add_edge(
813
+ GraphEdge(
814
+ source=inh.source_canonical_id,
815
+ target=target_id,
816
+ relationship=inh.relationship,
817
+ confidence=conf,
818
+ file=inh.source_file,
819
+ start_line=inh.line,
820
+ end_line=inh.line,
821
+ evidence=f"{inh.source_canonical_id} {inh.relationship} {target_id}",
822
+ )
823
+ )
824
+
825
+ # 3. Resolve Call Sites (with shared receiver, local binding, and factory return resolution)
826
+ file_bindings_map: dict[str, list[BindingRef]] = {}
827
+ receiver_bindings_by_fts: dict[tuple[str, str, str], list[BindingRef]] = {}
828
+ call_return_bindings_by_file: dict[str, list[BindingRef]] = {}
829
+ _RECEIVER_EXPR_KINDS = frozenset(
830
+ ("CALL_RETURN", "TYPE_ANNOTATION", "IDENTIFIER", "ATTRIBUTE", "DYNAMIC", "DYNAMIC_CALL_RETURN")
831
+ )
832
+ for b_item in self.bindings:
833
+ file_bindings_map.setdefault(b_item.file_path, []).append(b_item)
834
+ if b_item.expr_kind in _RECEIVER_EXPR_KINDS:
835
+ receiver_bindings_by_fts.setdefault(
836
+ (b_item.file_path, b_item.target_name, b_item.scope), []
837
+ ).append(b_item)
838
+ if b_item.expr_kind == "CALL_RETURN" and not b_item.is_conditional:
839
+ call_return_bindings_by_file.setdefault(b_item.file_path, []).append(b_item)
840
+
841
+ def _lookup_method_on_class(class_canon: str, method_name: str) -> tuple[Symbol | None, str]:
842
+ direct_cand = f"{class_canon}.{method_name}"
843
+ if direct_cand in self.canonical_to_symbol:
844
+ return self.canonical_to_symbol[direct_cand], class_canon
845
+ visited_bases: set[str] = {class_canon}
846
+ base_queue: list[str] = list(class_bases_map.get(class_canon, []))
847
+ while base_queue and len(visited_bases) <= 15:
848
+ base_canon = base_queue.pop(0)
849
+ if base_canon in visited_bases:
850
+ continue
851
+ visited_bases.add(base_canon)
852
+ cand = f"{base_canon}.{method_name}"
853
+ if cand in self.canonical_to_symbol:
854
+ return self.canonical_to_symbol[cand], base_canon
855
+ base_queue.extend(class_bases_map.get(base_canon, []))
856
+ return None, class_canon
857
+
858
+ factory_return_cache: dict[str, tuple[Symbol | None, str]] = {}
859
+
860
+ def _resolve_factory_return_class(f_sym: Symbol, depth: int = 0) -> tuple[Symbol | None, str]:
861
+ if depth == 0 and f_sym.canonical_id in factory_return_cache:
862
+ return factory_return_cache[f_sym.canonical_id]
863
+ if depth > 5 or not f_sym.return_type:
864
+ return None, ""
865
+ raw_ret = f_sym.return_type.strip()
866
+ if raw_ret in ("<AMBIGUOUS>", "<DYNAMIC>") or "|" in raw_ret:
867
+ return None, ""
868
+ clean_ret = raw_ret.split("[")[0].strip().strip("'\"")
869
+ if not clean_ret:
870
+ return None, ""
871
+ ret_sym, _rc, _rr = self.resolve_identifier_in_file(
872
+ clean_ret, f_sym.path, f_sym.canonical_id
873
+ )
874
+ if not ret_sym:
875
+ cand_c = f"{f_sym.module}.{clean_ret}"
876
+ ret_sym = self.canonical_to_symbol.get(cand_c)
877
+ if not ret_sym:
878
+ if depth == 0:
879
+ factory_return_cache[f_sym.canonical_id] = (None, "")
880
+ return None, ""
881
+ if ret_sym.kind == "class":
882
+ res = (ret_sym, f"factory '{f_sym.canonical_id}()' -> '{ret_sym.canonical_id}'")
883
+ if depth == 0:
884
+ factory_return_cache[f_sym.canonical_id] = res
885
+ return res
886
+ if ret_sym.kind in ("function", "method"):
887
+ sub_cls, sub_rsn = _resolve_factory_return_class(ret_sym, depth + 1)
888
+ if sub_cls:
889
+ res = (sub_cls, f"factory '{f_sym.canonical_id}()' -> {sub_rsn}")
890
+ if depth == 0:
891
+ factory_return_cache[f_sym.canonical_id] = res
892
+ return res
893
+ if depth == 0:
894
+ factory_return_cache[f_sym.canonical_id] = (None, "")
895
+ return None, ""
896
+
897
+ def _resolve_receiver_class(
898
+ recv_part: str,
899
+ source_file: str,
900
+ caller_id: str,
901
+ caller_qname: str,
902
+ use_line: int,
903
+ depth: int = 0,
904
+ ) -> tuple[Symbol | None, Symbol | None, str]:
905
+ if depth > 5 or not recv_part:
906
+ return None, None, ""
907
+
908
+ f_bindings = file_bindings_map.get(source_file)
909
+ if f_bindings:
910
+ caller_sym_obj = self.canonical_to_symbol.get(caller_id)
911
+ eff_qname = caller_qname or (caller_sym_obj.qualified_name if caller_sym_obj else "")
912
+ enclosing_cls = (
913
+ caller_sym_obj.scope
914
+ if (caller_sym_obj and caller_sym_obj.scope)
915
+ else (eff_qname.rsplit(".", 1)[0] if "." in eff_qname else "")
916
+ )
917
+ candidate_scopes: list[str] = []
918
+ for sc_cand in (
919
+ eff_qname,
920
+ caller_id,
921
+ enclosing_cls,
922
+ f"{caller_sym_obj.module}.{enclosing_cls}" if (caller_sym_obj and enclosing_cls) else "",
923
+ "",
924
+ ):
925
+ if sc_cand not in candidate_scopes:
926
+ candidate_scopes.append(sc_cand)
927
+
928
+ matched_scope_bindings: list[BindingRef] = []
929
+ for sc in candidate_scopes:
930
+ sc_matches = receiver_bindings_by_fts.get((source_file, recv_part, sc))
931
+ if sc_matches:
932
+ prior = [b for b in sc_matches if b.line <= use_line]
933
+ matched_scope_bindings = prior if prior else (sc_matches if recv_part.startswith("self.") else [])
934
+ if matched_scope_bindings:
935
+ break
936
+
937
+ if matched_scope_bindings:
938
+ if any(b.expr_kind in ("DYNAMIC", "DYNAMIC_CALL_RETURN") or b.source_expr == "<DYNAMIC>" for b in matched_scope_bindings):
939
+ return None, None, "Dynamic receiver assignment"
940
+ if any(b.is_conditional for b in matched_scope_bindings):
941
+ return None, None, "Conditional receiver assignment"
942
+ call_or_id_exprs = {
943
+ b.source_expr for b in matched_scope_bindings
944
+ if b.expr_kind in ("CALL_RETURN", "IDENTIFIER", "ATTRIBUTE")
945
+ }
946
+ if len(call_or_id_exprs) > 1:
947
+ return None, None, "Conflicting receiver assignments in scope"
948
+
949
+ # Prefer CALL_RETURN / IDENTIFIER / ATTRIBUTE first (to preserve factory provenance), then TYPE_ANNOTATION
950
+ chosen_b = next(
951
+ (b for b in reversed(matched_scope_bindings) if b.expr_kind in ("CALL_RETURN", "IDENTIFIER", "ATTRIBUTE")),
952
+ matched_scope_bindings[-1],
953
+ )
954
+ if chosen_b.expr_kind == "TYPE_ANNOTATION":
955
+ ann_sym, _, _ = self.resolve_identifier_in_file(
956
+ chosen_b.source_expr, chosen_b.file_path, caller_id, use_line=chosen_b.line
957
+ )
958
+ if ann_sym and ann_sym.kind == "class":
959
+ return ann_sym, None, f"Type annotation '{chosen_b.source_expr}' -> '{ann_sym.canonical_id}'"
960
+ else:
961
+ src_expr = chosen_b.source_expr[:-2].strip() if chosen_b.source_expr.endswith("()") else chosen_b.source_expr.strip()
962
+ cal_sym, _, _ = self.resolve_identifier_in_file(
963
+ src_expr, chosen_b.file_path, caller_id, use_line=chosen_b.line
964
+ )
965
+ if not cal_sym and "." in src_expr:
966
+ f_recv, f_meth = src_expr.rsplit(".", 1)
967
+ f_cls_sym, _, _ = _resolve_receiver_class(
968
+ f_recv, chosen_b.file_path, caller_id, caller_qname, chosen_b.line, depth + 1
969
+ )
970
+ if f_cls_sym:
971
+ cal_sym, _ = _lookup_method_on_class(f_cls_sym.canonical_id, f_meth)
972
+ if cal_sym:
973
+ if cal_sym.kind == "class":
974
+ return cal_sym, None, f"Constructed instance of '{cal_sym.canonical_id}'"
975
+ if cal_sym.kind in ("function", "method"):
976
+ ret_cls, ret_rsn = _resolve_factory_return_class(cal_sym, depth + 1)
977
+ if ret_cls:
978
+ return ret_cls, cal_sym, f"Resolved via {ret_rsn}"
979
+
980
+ # Direct symbol lookup for receiver (class name or factory function name in chained call)
981
+ recv_sym, _rconf, _rreason = self.resolve_identifier_in_file(
982
+ recv_part, source_file, caller_id, use_line=use_line
983
+ )
984
+ if recv_sym:
985
+ if recv_sym.kind == "class":
986
+ return recv_sym, None, f"Direct receiver class '{recv_sym.canonical_id}'"
987
+ if recv_sym.kind in ("function", "method"):
988
+ ret_cls, ret_rsn = _resolve_factory_return_class(recv_sym, depth + 1)
989
+ if ret_cls:
990
+ return ret_cls, recv_sym, f"Resolved via {ret_rsn}"
991
+ elif "." in recv_part:
992
+ sub_recv, sub_meth = recv_part.rsplit(".", 1)
993
+ sub_cls, _, _ = _resolve_receiver_class(
994
+ sub_recv, source_file, caller_id, caller_qname, use_line, depth + 1
995
+ )
996
+ if sub_cls:
997
+ meth_sym, _ = _lookup_method_on_class(sub_cls.canonical_id, sub_meth)
998
+ if meth_sym:
999
+ ret_cls, ret_rsn = _resolve_factory_return_class(meth_sym, depth + 1)
1000
+ if ret_cls:
1001
+ return ret_cls, meth_sym, f"Resolved via {ret_rsn}"
1002
+
1003
+ # Fallback if parser substituted local variable with same-file factory's return type string
1004
+ cr_bindings = call_return_bindings_by_file.get(source_file)
1005
+ if cr_bindings:
1006
+ for b in reversed(cr_bindings):
1007
+ if b.line <= use_line:
1008
+ f_sym, _, _ = self.resolve_identifier_in_file(
1009
+ b.source_expr, b.file_path, caller_id, use_line=b.line
1010
+ )
1011
+ if f_sym and f_sym.kind in ("function", "method"):
1012
+ ret_cls, ret_rsn = _resolve_factory_return_class(f_sym, depth + 1)
1013
+ if ret_cls and (ret_cls.name == recv_part or ret_cls.qualified_name == recv_part):
1014
+ return ret_cls, f_sym, f"Resolved via {ret_rsn}"
1015
+
1016
+ return None, None, ""
1017
+
1018
+ for call in self.calls:
1019
+ f_hash = self.file_hashes.get(call.source_file, "")
1020
+ caller_id = call.caller_canonical_id or (self.file_to_module.get(call.source_file) or normalize_module(call.source_file))
1021
+ caller_sym_for_qname = self.canonical_to_symbol.get(caller_id)
1022
+ caller_qname = call.caller_symbol or (caller_sym_for_qname.qualified_name if caller_sym_for_qname else "")
1023
+ expr = call.qualified_callee or call.callee
1024
+ via_factory_sym: Symbol | None = None
1025
+ resolved_via_dataflow = False
1026
+ is_pytorch_forward = False
1027
+ is_concrete_module = False
1028
+
1029
+ target_sym, conf, reason = self.resolve_identifier_in_file(
1030
+ expr, call.source_file, caller_id, use_line=call.line
1031
+ )
1032
+
1033
+ # Check receiver resolution (local binding, self.<attr>, inherited method, or factory return type)
1034
+ if "." in expr:
1035
+ recv_part, method_part = expr.rsplit(".", 1)
1036
+ if not target_sym:
1037
+ recv_cls_sym, via_factory_sym, recv_reason = _resolve_receiver_class(
1038
+ recv_part, call.source_file, caller_id, caller_qname, call.line
1039
+ )
1040
+ if recv_cls_sym:
1041
+ meth_sym, owner_canon = _lookup_method_on_class(recv_cls_sym.canonical_id, method_part)
1042
+ if meth_sym:
1043
+ target_sym = meth_sym
1044
+ conf = "HIGH"
1045
+ resolved_via_dataflow = True
1046
+ if owner_canon != recv_cls_sym.canonical_id:
1047
+ reason = f"{recv_reason}; inherited method on '{owner_canon}'"
1048
+ else:
1049
+ reason = recv_reason
1050
+ # If not a method, check if expr or self.<attr> is an nn.Module submodule invocation (e.g. self.encoder(x))
1051
+ if not target_sym:
1052
+ submod_cls_sym, _, submod_reason = _resolve_receiver_class(
1053
+ expr, call.source_file, caller_id, caller_qname, call.line
1054
+ )
1055
+ if not submod_cls_sym:
1056
+ for b in self.bindings:
1057
+ if b.file_path == call.source_file and b.target_name == f"self.{method_part}":
1058
+ if b.expr_kind == "CALL_RETURN" and b.source_expr:
1059
+ s_cls, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, caller_id, use_line=b.line)
1060
+ if s_cls and s_cls.kind == "class":
1061
+ submod_cls_sym = s_cls
1062
+ submod_reason = f"submodule assignment 'self.{method_part} = {b.source_expr}()'"
1063
+ break
1064
+ elif b.expr_kind == "TYPE_ANNOTATION" and b.source_expr:
1065
+ s_cls, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, caller_id, use_line=b.line)
1066
+ if s_cls and s_cls.kind == "class":
1067
+ submod_cls_sym = s_cls
1068
+ submod_reason = f"submodule annotation 'self.{method_part}: {b.source_expr}'"
1069
+ break
1070
+ if submod_cls_sym:
1071
+ fwd_sym, _ = _lookup_method_on_class(submod_cls_sym.canonical_id, "forward")
1072
+ if fwd_sym:
1073
+ target_sym = fwd_sym
1074
+ resolved_via_dataflow = True
1075
+ is_pytorch_forward = True
1076
+ is_concrete_module = "Constructed instance" in submod_reason or "factory" in submod_reason or "submodule assignment" in submod_reason
1077
+ conf = "HIGH" if is_concrete_module else "MEDIUM"
1078
+ reason = f"PyTorch submodule forward dispatch via {submod_reason}"
1079
+ else:
1080
+ # Even if parser substituted receiver -> ClassName, check if it came from a local factory binding
1081
+ for b in call_return_bindings_by_file.get(call.source_file, ()):
1082
+ if b.scope in (caller_qname, caller_id) and b.line <= call.line:
1083
+ f_sym, _, _ = self.resolve_identifier_in_file(
1084
+ b.source_expr, b.file_path, caller_id, use_line=b.line
1085
+ )
1086
+ if f_sym and f_sym.kind in ("function", "method"):
1087
+ ret_cls, ret_rsn = _resolve_factory_return_class(f_sym)
1088
+ if ret_cls and target_sym.canonical_id.startswith(ret_cls.canonical_id + "."):
1089
+ via_factory_sym = f_sym
1090
+ resolved_via_dataflow = True
1091
+ reason = f"Resolved via {ret_rsn}"
1092
+ break
1093
+
1094
+ if target_sym and via_factory_sym:
1095
+ add_ref(
1096
+ Reference(
1097
+ source_symbol_id=caller_id,
1098
+ target_symbol_id=via_factory_sym.canonical_id,
1099
+ relationship="CALLS",
1100
+ confidence="HIGH",
1101
+ path=call.source_file,
1102
+ start_line=call.line,
1103
+ end_line=call.end_line,
1104
+ evidence=f"Call factory '{via_factory_sym.canonical_id}()' at {call.source_file}:{call.line}",
1105
+ source_hash=f_hash,
1106
+ indexed_commit=self.indexed_commit,
1107
+ )
1108
+ )
1109
+ add_edge(
1110
+ GraphEdge(
1111
+ source=caller_id,
1112
+ target=via_factory_sym.canonical_id,
1113
+ relationship="CALLS",
1114
+ confidence="HIGH",
1115
+ file=call.source_file,
1116
+ start_line=call.line,
1117
+ end_line=call.end_line,
1118
+ evidence=f"Resolved factory call {caller_id} -> {via_factory_sym.canonical_id} at {call.source_file}:{call.line}",
1119
+ )
1120
+ )
1121
+
1122
+ else:
1123
+ # expr has no dot (e.g. model(x))
1124
+ # Only dispatch to forward if target_sym is NOT already a class (constructor call Classifier() is __init__)
1125
+ if not target_sym or target_sym.kind not in ("function", "method", "class"):
1126
+ mod_cls_sym, _, mod_reason = _resolve_receiver_class(
1127
+ expr, call.source_file, caller_id, caller_qname, call.line
1128
+ )
1129
+ if mod_cls_sym and mod_cls_sym.kind == "class":
1130
+ fwd_sym, _ = _lookup_method_on_class(mod_cls_sym.canonical_id, "forward")
1131
+ if fwd_sym:
1132
+ target_sym = fwd_sym
1133
+ resolved_via_dataflow = True
1134
+ is_pytorch_forward = True
1135
+ is_concrete_module = "Constructed instance" in mod_reason or "factory" in mod_reason
1136
+ conf = "HIGH" if is_concrete_module else "MEDIUM"
1137
+ reason = f"PyTorch module forward dispatch via {mod_reason}"
1138
+
1139
+ if target_sym:
1140
+ resolved_calls[(call.source_file, call.line, call.callee)] = (
1141
+ target_sym.canonical_id,
1142
+ conf,
1143
+ )
1144
+ call_ev_cls = (
1145
+ RelationshipEvidenceClass.DATAFLOW_VERIFIED.value
1146
+ if resolved_via_dataflow
1147
+ else RelationshipEvidenceClass.AST_VERIFIED.value
1148
+ )
1149
+ add_ref(
1150
+ Reference(
1151
+ source_symbol_id=caller_id,
1152
+ target_symbol_id=target_sym.canonical_id,
1153
+ relationship="CALLS",
1154
+ confidence=conf,
1155
+ path=call.source_file,
1156
+ start_line=call.line,
1157
+ end_line=call.end_line,
1158
+ evidence=f"Call '{expr}()' at {call.source_file}:{call.line} -> {target_sym.canonical_id} ({reason})",
1159
+ source_hash=f_hash,
1160
+ indexed_commit=self.indexed_commit,
1161
+ )
1162
+ )
1163
+ add_edge(
1164
+ GraphEdge(
1165
+ source=caller_id,
1166
+ target=target_sym.canonical_id,
1167
+ relationship="CALLS",
1168
+ confidence=conf,
1169
+ file=call.source_file,
1170
+ start_line=call.line,
1171
+ end_line=call.end_line,
1172
+ evidence=f"Resolved call {caller_id} -> {target_sym.canonical_id} at {call.source_file}:{call.line} ({reason})",
1173
+ evidence_class=call_ev_cls,
1174
+ )
1175
+ )
1176
+ if is_pytorch_forward:
1177
+ fwd_ev_cls = (
1178
+ RelationshipEvidenceClass.AST_VERIFIED.value
1179
+ if is_concrete_module
1180
+ else RelationshipEvidenceClass.POSSIBLE.value
1181
+ )
1182
+ add_ref(
1183
+ Reference(
1184
+ source_symbol_id=caller_id,
1185
+ target_symbol_id=target_sym.canonical_id,
1186
+ relationship="DISPATCHES_FORWARD",
1187
+ confidence=conf,
1188
+ path=call.source_file,
1189
+ start_line=call.line,
1190
+ end_line=call.end_line,
1191
+ evidence=f"Module invocation '{expr}()' at {call.source_file}:{call.line} dispatches to {target_sym.canonical_id} ({reason})",
1192
+ source_hash=f_hash,
1193
+ indexed_commit=self.indexed_commit,
1194
+ )
1195
+ )
1196
+ add_edge(
1197
+ GraphEdge(
1198
+ source=caller_id,
1199
+ target=target_sym.canonical_id,
1200
+ relationship="DISPATCHES_FORWARD",
1201
+ confidence=conf,
1202
+ file=call.source_file,
1203
+ start_line=call.line,
1204
+ end_line=call.end_line,
1205
+ evidence=f"Resolved PyTorch forward dispatch {caller_id} -> {target_sym.canonical_id} at {call.source_file}:{call.line} ({reason})",
1206
+ evidence_class=fwd_ev_cls,
1207
+ reason="pytorch_forward_dispatch",
1208
+ )
1209
+ )
1210
+ elif "[" in expr and "]" in expr:
1211
+ dict_part, raw_k = expr.split("[", 1)
1212
+ raw_k = raw_k.rstrip("]")
1213
+ clean_k = raw_k.strip("'\"")
1214
+ idx_val = int(clean_k) if clean_k.isdigit() else None
1215
+ key_val = None if idx_val is not None else clean_k
1216
+ disp_target, disp_ev, disp_rsn = self.binding_resolver.resolve_subscript(
1217
+ call.source_file, dict_part, key=key_val, index=idx_val, use_line=call.line
1218
+ )
1219
+ if disp_target:
1220
+ add_ref(
1221
+ Reference(
1222
+ source_symbol_id=caller_id,
1223
+ target_symbol_id=disp_target,
1224
+ relationship="DISPATCHES_TO",
1225
+ confidence="HIGH",
1226
+ path=call.source_file,
1227
+ start_line=call.line,
1228
+ end_line=call.end_line,
1229
+ evidence=f"Dispatch call '{expr}()' -> {disp_target} ({disp_rsn})",
1230
+ source_hash=f_hash,
1231
+ indexed_commit=self.indexed_commit,
1232
+ )
1233
+ )
1234
+ add_edge(
1235
+ GraphEdge(
1236
+ source=caller_id,
1237
+ target=disp_target,
1238
+ relationship="DISPATCHES_TO",
1239
+ confidence="HIGH",
1240
+ file=call.source_file,
1241
+ start_line=call.line,
1242
+ end_line=call.end_line,
1243
+ evidence=f"Dispatch call '{expr}()' -> {disp_target} at {call.source_file}:{call.line}",
1244
+ evidence_class=disp_ev.value,
1245
+ reason=disp_rsn,
1246
+ )
1247
+ )
1248
+ else:
1249
+ resolved_calls[(call.source_file, call.line, call.callee)] = (
1250
+ None,
1251
+ "UNKNOWN",
1252
+ )
1253
+ add_ref(
1254
+ Reference(
1255
+ source_symbol_id=caller_id,
1256
+ target_symbol_id=None,
1257
+ relationship="UNRESOLVED_REFERENCE",
1258
+ confidence="UNKNOWN",
1259
+ path=call.source_file,
1260
+ start_line=call.line,
1261
+ end_line=call.end_line,
1262
+ evidence=f"Unresolved dispatch call '{expr}()' at {call.source_file}:{call.line} ({disp_rsn})",
1263
+ source_hash=f_hash,
1264
+ indexed_commit=self.indexed_commit,
1265
+ )
1266
+ )
1267
+ else:
1268
+ # Target cannot be statically resolved via lexical/import/module rules
1269
+ resolved_calls[(call.source_file, call.line, call.callee)] = (
1270
+ None,
1271
+ "UNKNOWN",
1272
+ )
1273
+ add_ref(
1274
+ Reference(
1275
+ source_symbol_id=caller_id,
1276
+ target_symbol_id=None,
1277
+ relationship="UNRESOLVED_REFERENCE",
1278
+ confidence="UNKNOWN",
1279
+ path=call.source_file,
1280
+ start_line=call.line,
1281
+ end_line=call.end_line,
1282
+ evidence=f"Unresolved call '{expr}()' at {call.source_file}:{call.line} ({reason})",
1283
+ source_hash=f_hash,
1284
+ indexed_commit=self.indexed_commit,
1285
+ )
1286
+ )
1287
+
1288
+ # 4. Framework Route -> Handler edges
1289
+ for rt in self.routes:
1290
+ f_hash = self.file_hashes.get(rt.file_path, "")
1291
+ handler_sym, conf, _reason = self.resolve_identifier_in_file(
1292
+ rt.handler_name, rt.file_path, use_line=rt.line
1293
+ )
1294
+ target_id = handler_sym.canonical_id if handler_sym else rt.handler_canonical_id
1295
+ edge_conf = "HIGH" if handler_sym else conf
1296
+ add_ref(
1297
+ Reference(
1298
+ source_symbol_id=rt.endpoint_id,
1299
+ target_symbol_id=target_id,
1300
+ relationship="ROUTES_TO",
1301
+ confidence=edge_conf,
1302
+ path=rt.file_path,
1303
+ start_line=rt.line,
1304
+ end_line=rt.line,
1305
+ evidence=rt.evidence,
1306
+ source_hash=f_hash,
1307
+ indexed_commit=self.indexed_commit,
1308
+ )
1309
+ )
1310
+ add_edge(
1311
+ GraphEdge(
1312
+ source=rt.endpoint_id,
1313
+ target=target_id,
1314
+ relationship="ROUTES_TO",
1315
+ confidence=edge_conf,
1316
+ file=rt.file_path,
1317
+ start_line=rt.line,
1318
+ end_line=rt.line,
1319
+ evidence=rt.evidence,
1320
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
1321
+ )
1322
+ )
1323
+
1324
+ # 5. Verified Router Mount edges (MOUNTS)
1325
+ for me in self.mount_edges:
1326
+ f_hash = self.file_hashes.get(me.file_path, "")
1327
+ add_ref(
1328
+ Reference(
1329
+ source_symbol_id=me.parent_canonical_id,
1330
+ target_symbol_id=me.child_canonical_id,
1331
+ relationship="MOUNTS",
1332
+ confidence="HIGH",
1333
+ path=me.file_path,
1334
+ start_line=me.line,
1335
+ end_line=me.line,
1336
+ evidence=me.evidence,
1337
+ source_hash=f_hash,
1338
+ indexed_commit=self.indexed_commit,
1339
+ )
1340
+ )
1341
+ add_edge(
1342
+ GraphEdge(
1343
+ source=me.parent_canonical_id,
1344
+ target=me.child_canonical_id,
1345
+ relationship="MOUNTS",
1346
+ confidence="HIGH",
1347
+ file=me.file_path,
1348
+ start_line=me.line,
1349
+ end_line=me.line,
1350
+ evidence=me.evidence,
1351
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
1352
+ reason="router_mount",
1353
+ )
1354
+ )
1355
+
1356
+ # Pre-group bindings by expr_kind and pre-index inheritance/routes for O(1) lookups
1357
+ bindings_by_kind: dict[str, list[BindingRef]] = {}
1358
+ event_listeners_by_key: dict[str, list[BindingRef]] = {}
1359
+ _EVENT_LISTENER_KINDS = frozenset(
1360
+ ("EVENT_ON", "SUBSCRIBE", "CALL_REGISTER", "REGISTRY_ADD", "REGISTRY_CALL", "DECORATOR_REGISTER")
1361
+ )
1362
+ for b in self.bindings:
1363
+ bindings_by_kind.setdefault(b.expr_kind, []).append(b)
1364
+ if b.expr_kind in _EVENT_LISTENER_KINDS and b.subscript_key:
1365
+ event_listeners_by_key.setdefault(b.subscript_key, []).append(b)
1366
+
1367
+ inheritance_by_source: dict[str, list[InheritanceRef]] = {}
1368
+ for inh in self.inheritance:
1369
+ inheritance_by_source.setdefault(inh.source_canonical_id, []).append(inh)
1370
+
1371
+ # 6. Local Bindings and Alias Chains (RESOLVES_TO)
1372
+ for b in (
1373
+ bindings_by_kind.get("IDENTIFIER", [])
1374
+ + bindings_by_kind.get("ATTRIBUTE", [])
1375
+ + bindings_by_kind.get("SUBSCRIPT", [])
1376
+ ):
1377
+ b_target_id, ev_class, reason = self.binding_resolver.resolve(
1378
+ file_path=b.file_path,
1379
+ name=b.target_name,
1380
+ scope=b.scope,
1381
+ use_line=b.line,
1382
+ )
1383
+ if b_target_id and ev_class == RelationshipEvidenceClass.DATAFLOW_VERIFIED:
1384
+ f_hash = self.file_hashes.get(b.file_path, "")
1385
+ src_mod = self.file_to_module.get(b.file_path) or normalize_module(b.file_path)
1386
+ source_id = f"{src_mod}.{b.scope}.{b.target_name}" if b.scope else f"{src_mod}.{b.target_name}"
1387
+ add_ref(
1388
+ Reference(
1389
+ source_symbol_id=source_id,
1390
+ target_symbol_id=b_target_id,
1391
+ relationship="RESOLVES_TO",
1392
+ confidence="HIGH",
1393
+ path=b.file_path,
1394
+ start_line=b.line,
1395
+ end_line=b.line,
1396
+ evidence=f"Local binding alias '{b.target_name}' -> '{b_target_id}' ({b.source_expr})",
1397
+ source_hash=f_hash,
1398
+ indexed_commit=self.indexed_commit,
1399
+ )
1400
+ )
1401
+ add_edge(
1402
+ GraphEdge(
1403
+ source=source_id,
1404
+ target=b_target_id,
1405
+ relationship="RESOLVES_TO",
1406
+ confidence="HIGH",
1407
+ file=b.file_path,
1408
+ start_line=b.line,
1409
+ end_line=b.line,
1410
+ evidence=f"Local binding alias '{b.target_name}' -> '{b_target_id}' ({b.source_expr})",
1411
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
1412
+ reason=reason,
1413
+ )
1414
+ )
1415
+
1416
+ # 7. Registries, Event Listeners, and Dispatch Registrations
1417
+ findings: list[dict[str, object]] = list(self.budget_findings)
1418
+
1419
+ # Pre-compute static overwrite state for DICT_ASSIGN bindings in the same (file_path, scope, target_name, subscript_key)
1420
+ dict_assign_groups: dict[tuple[str, str, str, str], list[BindingRef]] = {}
1421
+ for b in bindings_by_kind.get("DICT_ASSIGN", ()):
1422
+ if b.subscript_key and b.subscript_key != "<DYNAMIC>":
1423
+ g_key = (b.file_path, b.scope, b.target_name, b.subscript_key)
1424
+ dict_assign_groups.setdefault(g_key, []).append(b)
1425
+
1426
+ overwritten_dict_assign_lines: set[tuple[str, str, str, str, int]] = set()
1427
+ ambiguous_dict_assign_keys: set[tuple[str, str, str, str]] = set()
1428
+ for g_key, g_list in dict_assign_groups.items():
1429
+ if len(g_list) <= 1:
1430
+ continue
1431
+ g_sorted = sorted(g_list, key=lambda x: (x.line, x.column or 0))
1432
+ last_uncond_idx = -1
1433
+ for idx, item in enumerate(g_sorted):
1434
+ if not item.is_conditional:
1435
+ last_uncond_idx = idx
1436
+ if last_uncond_idx >= 0:
1437
+ for idx in range(last_uncond_idx):
1438
+ overwritten_dict_assign_lines.add((*g_key, g_sorted[idx].line))
1439
+ if last_uncond_idx < len(g_sorted) - 1:
1440
+ ambiguous_dict_assign_keys.add(g_key)
1441
+ else:
1442
+ ambiguous_dict_assign_keys.add(g_key)
1443
+
1444
+ for b in self.bindings:
1445
+ if b.expr_kind == "LIST_LITERAL" and b.list_entries:
1446
+ t_lower = b.target_name.lower()
1447
+ if t_lower in ("handlers", "registry", "callbacks", "plugins", "listeners", "handler_list", "plugin_list"):
1448
+ f_hash = self.file_hashes.get(b.file_path, "")
1449
+ source_id = f"{self.file_to_module.get(b.file_path) or normalize_module(b.file_path)}.{b.target_name}"
1450
+ for idx_elt, elt_expr in enumerate(b.list_entries):
1451
+ target_sym, _conf, _rsn = self.resolve_identifier_in_file(
1452
+ elt_expr, b.file_path, use_line=b.line
1453
+ )
1454
+ if not target_sym:
1455
+ continue
1456
+ target_canon = target_sym.canonical_id
1457
+ ev_class = RelationshipEvidenceClass.POSSIBLE if b.is_conditional else RelationshipEvidenceClass.DATAFLOW_VERIFIED
1458
+ edge_conf = "LOW" if b.is_conditional else "HIGH"
1459
+ edge_reason = "ambiguous_dispatch_assignment" if b.is_conditional else f"Statically verified list handler registration [{idx_elt}] -> '{target_canon}'"
1460
+ evidence_str = f"{b.target_name} DISPATCHES_TO {target_canon} (index={idx_elt})"
1461
+ add_ref(
1462
+ Reference(
1463
+ source_symbol_id=source_id,
1464
+ target_symbol_id=target_canon,
1465
+ relationship="DISPATCHES_TO",
1466
+ confidence=edge_conf,
1467
+ path=b.file_path,
1468
+ start_line=b.line,
1469
+ end_line=b.line,
1470
+ evidence=evidence_str,
1471
+ source_hash=f_hash,
1472
+ indexed_commit=self.indexed_commit,
1473
+ )
1474
+ )
1475
+ add_edge(
1476
+ GraphEdge(
1477
+ source=source_id,
1478
+ target=target_canon,
1479
+ relationship="DISPATCHES_TO",
1480
+ confidence=edge_conf,
1481
+ file=b.file_path,
1482
+ start_line=b.line,
1483
+ end_line=b.line,
1484
+ evidence=evidence_str,
1485
+ evidence_class=ev_class.value,
1486
+ reason=edge_reason,
1487
+ )
1488
+ )
1489
+ continue
1490
+
1491
+ if b.expr_kind not in ("CALL_REGISTER", "DICT_ASSIGN", "EVENT_ON", "SUBSCRIBE"):
1492
+ continue
1493
+
1494
+ if b.expr_kind == "DICT_ASSIGN" and b.subscript_key and b.subscript_key != "<DYNAMIC>":
1495
+ g_key = (b.file_path, b.scope, b.target_name, b.subscript_key)
1496
+ if (*g_key, b.line) in overwritten_dict_assign_lines:
1497
+ findings.append({
1498
+ "pattern": "DICT_ASSIGN",
1499
+ "status": "OVERWRITTEN",
1500
+ "reason": "overwritten_by_later_static_assignment",
1501
+ "file": b.file_path,
1502
+ "line": b.line,
1503
+ "target_name": b.target_name,
1504
+ "key": b.subscript_key,
1505
+ "source_expr": b.source_expr,
1506
+ })
1507
+ continue
1508
+
1509
+ f_hash = self.file_hashes.get(b.file_path, "")
1510
+ is_dynamic = (b.subscript_key == "<DYNAMIC>" or b.source_expr == "<DYNAMIC>")
1511
+
1512
+ if is_dynamic:
1513
+ reason = (
1514
+ "dynamic_dispatch"
1515
+ if b.expr_kind == "DICT_ASSIGN"
1516
+ else (
1517
+ "dynamic_event_name"
1518
+ if b.subscript_key == "<DYNAMIC>"
1519
+ else "dynamic_registration"
1520
+ )
1521
+ )
1522
+ findings.append({
1523
+ "pattern": b.expr_kind,
1524
+ "status": "UNKNOWN",
1525
+ "reason": reason,
1526
+ "file": b.file_path,
1527
+ "line": b.line,
1528
+ "target_name": b.target_name,
1529
+ })
1530
+ # Invariant: NEVER emit an edge with fake/unknown target canonical ID (Correction 2)
1531
+ continue
1532
+
1533
+ target_sym, _conf, _reason = self.resolve_identifier_in_file(
1534
+ b.source_expr, b.file_path, use_line=b.line
1535
+ )
1536
+
1537
+ if not target_sym:
1538
+ findings.append({
1539
+ "pattern": b.expr_kind,
1540
+ "status": "UNKNOWN",
1541
+ "reason": "unknown_registry_target",
1542
+ "file": b.file_path,
1543
+ "line": b.line,
1544
+ "target_name": b.target_name,
1545
+ "source_expr": b.source_expr,
1546
+ })
1547
+ # Invariant: NEVER emit an edge with fake/unknown target canonical ID (Correction 2)
1548
+ continue
1549
+
1550
+ # Target is statically grounded!
1551
+ target_canon = target_sym.canonical_id
1552
+ if b.expr_kind == "CALL_REGISTER":
1553
+ rel = "REGISTERS"
1554
+ elif b.expr_kind in ("EVENT_ON", "SUBSCRIBE"):
1555
+ rel = "EVENT_LISTENER"
1556
+ else:
1557
+ rel = "DISPATCHES_TO"
1558
+
1559
+ is_ambig_key = (
1560
+ b.expr_kind == "DICT_ASSIGN"
1561
+ and b.subscript_key is not None
1562
+ and (b.file_path, b.scope, b.target_name, b.subscript_key) in ambiguous_dict_assign_keys
1563
+ )
1564
+ if b.is_conditional or is_ambig_key:
1565
+ ev_class = RelationshipEvidenceClass.POSSIBLE
1566
+ edge_conf = "LOW"
1567
+ edge_reason = "ambiguous_dispatch_assignment"
1568
+ else:
1569
+ ev_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
1570
+ edge_conf = "HIGH"
1571
+ edge_reason = f"Statically verified registration of '{b.subscript_key}' -> '{target_canon}'"
1572
+
1573
+ source_id = f"{self.file_to_module.get(b.file_path) or normalize_module(b.file_path)}.{b.target_name}"
1574
+ evidence_str = f"{b.target_name} {rel} {target_canon} (key='{b.subscript_key}')"
1575
+
1576
+ add_ref(
1577
+ Reference(
1578
+ source_symbol_id=source_id,
1579
+ target_symbol_id=target_canon,
1580
+ relationship=rel,
1581
+ confidence=edge_conf,
1582
+ path=b.file_path,
1583
+ start_line=b.line,
1584
+ end_line=b.line,
1585
+ evidence=evidence_str,
1586
+ source_hash=f_hash,
1587
+ indexed_commit=self.indexed_commit,
1588
+ )
1589
+ )
1590
+ add_edge(
1591
+ GraphEdge(
1592
+ source=source_id,
1593
+ target=target_canon,
1594
+ relationship=rel,
1595
+ confidence=edge_conf,
1596
+ file=b.file_path,
1597
+ start_line=b.line,
1598
+ end_line=b.line,
1599
+ evidence=evidence_str,
1600
+ evidence_class=ev_class.value,
1601
+ reason=edge_reason,
1602
+ )
1603
+ )
1604
+
1605
+ # 8. Semantic Decorators (TASK_HANDLER, COMMAND_HANDLER, EVENT_LISTENER, REGISTERS)
1606
+ for b in bindings_by_kind.get("SEMANTIC_DECORATOR", ()):
1607
+ f_hash = self.file_hashes.get(b.file_path, "")
1608
+ is_dynamic = b.subscript_target is not None
1609
+ metadata: dict[str, object] = {}
1610
+ if b.subscript_key:
1611
+ try:
1612
+ metadata = json.loads(b.subscript_key)
1613
+ except Exception:
1614
+ metadata = {}
1615
+
1616
+ if is_dynamic:
1617
+ findings.append({
1618
+ "pattern": "SEMANTIC_DECORATOR",
1619
+ "role": b.attr_name,
1620
+ "status": "UNKNOWN",
1621
+ "reason": b.subscript_target or "dynamic_argument",
1622
+ "file": b.file_path,
1623
+ "line": b.line,
1624
+ "target_name": b.target_name,
1625
+ "decorator": b.source_expr,
1626
+ })
1627
+ # Invariant: Never emit edge for dynamic/unknown targets
1628
+ continue
1629
+
1630
+ rel = b.attr_name
1631
+ assert rel != "CALLS", f"Invariant violated: Semantic decorator {rel} cannot be CALLS"
1632
+
1633
+ target_canon = b.target_name
1634
+ meta_ev = str(metadata.get("evidence_class", ""))
1635
+ ev_class = (
1636
+ RelationshipEvidenceClass.POSSIBLE
1637
+ if b.is_conditional
1638
+ else (
1639
+ RelationshipEvidenceClass(meta_ev)
1640
+ if meta_ev in RelationshipEvidenceClass._value2member_map_
1641
+ else (
1642
+ RelationshipEvidenceClass.DATAFLOW_VERIFIED
1643
+ if b.base_expr == "registry"
1644
+ else RelationshipEvidenceClass.FRAMEWORK_VERIFIED
1645
+ )
1646
+ )
1647
+ )
1648
+ edge_conf = "LOW" if b.is_conditional else "HIGH"
1649
+ edge_reason = (
1650
+ f"Conditional registration via '@{b.source_expr}'"
1651
+ if b.is_conditional
1652
+ else f"Statically verified registration via '@{b.source_expr}'"
1653
+ )
1654
+
1655
+ if rel == "EVENT_LISTENER":
1656
+ events = metadata.get("events")
1657
+ if isinstance(events, list) and events:
1658
+ for ev in events:
1659
+ ev_str = str(ev)
1660
+ ev_sym, _conf, _ = self.resolve_identifier_in_file(
1661
+ ev_str, b.file_path, use_line=b.line
1662
+ )
1663
+ src_id = ev_sym.canonical_id if ev_sym else ev_str
1664
+ ev_evidence = f"Event listener '{target_canon}' registered for event '{ev_str}' via '@{b.source_expr}'"
1665
+
1666
+ add_ref(
1667
+ Reference(
1668
+ source_symbol_id=src_id,
1669
+ target_symbol_id=target_canon,
1670
+ relationship=rel,
1671
+ confidence=edge_conf,
1672
+ path=b.file_path,
1673
+ start_line=b.line,
1674
+ end_line=b.line,
1675
+ evidence=ev_evidence,
1676
+ source_hash=f_hash,
1677
+ indexed_commit=self.indexed_commit,
1678
+ )
1679
+ )
1680
+ add_edge(
1681
+ GraphEdge(
1682
+ source=src_id,
1683
+ target=target_canon,
1684
+ relationship=rel,
1685
+ confidence=edge_conf,
1686
+ file=b.file_path,
1687
+ start_line=b.line,
1688
+ end_line=b.line,
1689
+ evidence=ev_evidence,
1690
+ evidence_class=ev_class.value,
1691
+ reason=edge_reason,
1692
+ )
1693
+ )
1694
+ # Django signal sender linking: @receiver(signal, sender=Model)
1695
+ sender_val = metadata.get("sender")
1696
+ if sender_val:
1697
+ sender_str = str(sender_val)
1698
+ sender_sym, _, _ = self.resolve_identifier_in_file(
1699
+ sender_str, b.file_path, use_line=b.line
1700
+ )
1701
+ if not sender_sym and "." in sender_str:
1702
+ sender_sym, _, _ = self.resolve_identifier_in_file(
1703
+ sender_str.split(".")[-1], b.file_path, use_line=b.line
1704
+ )
1705
+ if not sender_sym:
1706
+ short_target = sender_str.split(".")[-1]
1707
+ if short_target in self.short_to_symbols:
1708
+ for s in self.short_to_symbols[short_target]:
1709
+ if s.kind == "class":
1710
+ sender_sym = s
1711
+ break
1712
+ if not sender_sym:
1713
+ sender_sym = self.short_to_symbols[short_target][0]
1714
+
1715
+ sender_canon = sender_sym.canonical_id if sender_sym else sender_str
1716
+ sig_evidence = f"Django signal '{ev_str}' from sender '{sender_str}' triggers listener '{target_canon}' via '@{b.source_expr}'"
1717
+ add_ref(
1718
+ Reference(
1719
+ source_symbol_id=sender_canon,
1720
+ target_symbol_id=target_canon,
1721
+ relationship="TRIGGERS_SIGNAL",
1722
+ confidence=edge_conf,
1723
+ path=b.file_path,
1724
+ start_line=b.line,
1725
+ end_line=b.line,
1726
+ evidence=sig_evidence,
1727
+ source_hash=f_hash,
1728
+ indexed_commit=self.indexed_commit,
1729
+ )
1730
+ )
1731
+ add_edge(
1732
+ GraphEdge(
1733
+ source=sender_canon,
1734
+ target=target_canon,
1735
+ relationship="TRIGGERS_SIGNAL",
1736
+ confidence=edge_conf,
1737
+ file=b.file_path,
1738
+ start_line=b.line,
1739
+ end_line=b.line,
1740
+ evidence=sig_evidence,
1741
+ evidence_class=ev_class.value,
1742
+ reason=f"django_signal:{ev_str}",
1743
+ )
1744
+ )
1745
+ lifecycle_method = "delete" if "delete" in ev_str.lower() else "save"
1746
+ save_source = f"{sender_canon}.{lifecycle_method}"
1747
+ add_edge(
1748
+ GraphEdge(
1749
+ source=save_source,
1750
+ target=target_canon,
1751
+ relationship="TRIGGERS_SIGNAL",
1752
+ confidence=edge_conf,
1753
+ file=b.file_path,
1754
+ start_line=b.line,
1755
+ end_line=b.line,
1756
+ evidence=sig_evidence,
1757
+ evidence_class=ev_class.value,
1758
+ reason=f"django_signal:{ev_str}",
1759
+ )
1760
+ )
1761
+ add_edge(
1762
+ GraphEdge(
1763
+ source=save_source,
1764
+ target=target_canon,
1765
+ relationship="CALLS",
1766
+ confidence=edge_conf,
1767
+ file=b.file_path,
1768
+ start_line=b.line,
1769
+ end_line=b.line,
1770
+ evidence=sig_evidence,
1771
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
1772
+ reason=f"django_signal:{ev_str}",
1773
+ )
1774
+ )
1775
+ add_edge(
1776
+ GraphEdge(
1777
+ source=target_canon,
1778
+ target=sender_canon,
1779
+ relationship="HANDLES_SIGNAL",
1780
+ confidence=edge_conf,
1781
+ file=b.file_path,
1782
+ start_line=b.line,
1783
+ end_line=b.line,
1784
+ evidence=f"Listener '{target_canon}' handles '{ev_str}' from '{sender_canon}'",
1785
+ evidence_class=ev_class.value,
1786
+ reason=f"django_signal:{ev_str}",
1787
+ )
1788
+ )
1789
+ else:
1790
+ src_id = b.source_expr
1791
+ ev_evidence = f"Event listener '{target_canon}' registered via '@{b.source_expr}'"
1792
+ add_ref(
1793
+ Reference(
1794
+ source_symbol_id=src_id,
1795
+ target_symbol_id=target_canon,
1796
+ relationship=rel,
1797
+ confidence=edge_conf,
1798
+ path=b.file_path,
1799
+ start_line=b.line,
1800
+ end_line=b.line,
1801
+ evidence=ev_evidence,
1802
+ source_hash=f_hash,
1803
+ indexed_commit=self.indexed_commit,
1804
+ )
1805
+ )
1806
+ add_edge(
1807
+ GraphEdge(
1808
+ source=src_id,
1809
+ target=target_canon,
1810
+ relationship=rel,
1811
+ confidence=edge_conf,
1812
+ file=b.file_path,
1813
+ start_line=b.line,
1814
+ end_line=b.line,
1815
+ evidence=ev_evidence,
1816
+ evidence_class=ev_class.value,
1817
+ reason=edge_reason,
1818
+ )
1819
+ )
1820
+
1821
+ elif rel == "COMMAND_HANDLER":
1822
+ cmd_name = str(metadata.get("command") or target_canon.split(".")[-1])
1823
+ source_id = f"{b.base_expr}:{cmd_name}" if b.base_expr else cmd_name
1824
+ ev_evidence = f"Command handler '{target_canon}' registered for command '{cmd_name}' via '@{b.source_expr}'"
1825
+ add_ref(
1826
+ Reference(
1827
+ source_symbol_id=source_id,
1828
+ target_symbol_id=target_canon,
1829
+ relationship=rel,
1830
+ confidence=edge_conf,
1831
+ path=b.file_path,
1832
+ start_line=b.line,
1833
+ end_line=b.line,
1834
+ evidence=ev_evidence,
1835
+ source_hash=f_hash,
1836
+ indexed_commit=self.indexed_commit,
1837
+ )
1838
+ )
1839
+ add_edge(
1840
+ GraphEdge(
1841
+ source=source_id,
1842
+ target=target_canon,
1843
+ relationship=rel,
1844
+ confidence=edge_conf,
1845
+ file=b.file_path,
1846
+ start_line=b.line,
1847
+ end_line=b.line,
1848
+ evidence=ev_evidence,
1849
+ evidence_class=ev_class.value,
1850
+ reason=edge_reason,
1851
+ )
1852
+ )
1853
+
1854
+ elif rel == "TASK_HANDLER":
1855
+ task_name = str(metadata.get("task_name") or target_canon)
1856
+ source_id = task_name if task_name != target_canon else b.source_expr
1857
+ ev_evidence = f"Task handler '{target_canon}' registered via '@{b.source_expr}'"
1858
+ add_ref(
1859
+ Reference(
1860
+ source_symbol_id=source_id,
1861
+ target_symbol_id=target_canon,
1862
+ relationship=rel,
1863
+ confidence=edge_conf,
1864
+ path=b.file_path,
1865
+ start_line=b.line,
1866
+ end_line=b.line,
1867
+ evidence=ev_evidence,
1868
+ source_hash=f_hash,
1869
+ indexed_commit=self.indexed_commit,
1870
+ )
1871
+ )
1872
+ add_edge(
1873
+ GraphEdge(
1874
+ source=source_id,
1875
+ target=target_canon,
1876
+ relationship=rel,
1877
+ confidence=edge_conf,
1878
+ file=b.file_path,
1879
+ start_line=b.line,
1880
+ end_line=b.line,
1881
+ evidence=ev_evidence,
1882
+ evidence_class=ev_class.value,
1883
+ reason=edge_reason,
1884
+ )
1885
+ )
1886
+
1887
+ elif rel == "REGISTERS":
1888
+ source_id = f"{self.file_to_module.get(b.file_path) or normalize_module(b.file_path)}.{b.source_expr.split('.')[0]}"
1889
+ ev_evidence = f"Registry handler '{target_canon}' registered via '@{b.source_expr}'"
1890
+ add_ref(
1891
+ Reference(
1892
+ source_symbol_id=source_id,
1893
+ target_symbol_id=target_canon,
1894
+ relationship=rel,
1895
+ confidence=edge_conf,
1896
+ path=b.file_path,
1897
+ start_line=b.line,
1898
+ end_line=b.line,
1899
+ evidence=ev_evidence,
1900
+ source_hash=f_hash,
1901
+ indexed_commit=self.indexed_commit,
1902
+ )
1903
+ )
1904
+ add_edge(
1905
+ GraphEdge(
1906
+ source=source_id,
1907
+ target=target_canon,
1908
+ relationship=rel,
1909
+ confidence=edge_conf,
1910
+ file=b.file_path,
1911
+ start_line=b.line,
1912
+ end_line=b.line,
1913
+ evidence=ev_evidence,
1914
+ evidence_class=ev_class.value,
1915
+ reason=edge_reason,
1916
+ )
1917
+ )
1918
+
1919
+ elif rel == "TOOL_HANDLER":
1920
+ source_id = f"@{b.source_expr}"
1921
+ ev_evidence = f"AI agent tool '{target_canon}' registered via '@{b.source_expr}'"
1922
+ add_ref(
1923
+ Reference(
1924
+ source_symbol_id=target_canon,
1925
+ target_symbol_id=source_id,
1926
+ relationship=rel,
1927
+ confidence=edge_conf,
1928
+ path=b.file_path,
1929
+ start_line=b.line,
1930
+ end_line=b.line,
1931
+ evidence=ev_evidence,
1932
+ source_hash=f_hash,
1933
+ indexed_commit=self.indexed_commit,
1934
+ )
1935
+ )
1936
+ add_edge(
1937
+ GraphEdge(
1938
+ source=target_canon,
1939
+ target=source_id,
1940
+ relationship=rel,
1941
+ confidence=edge_conf,
1942
+ file=b.file_path,
1943
+ start_line=b.line,
1944
+ end_line=b.line,
1945
+ evidence=ev_evidence,
1946
+ evidence_class=ev_class.value,
1947
+ reason=edge_reason,
1948
+ )
1949
+ )
1950
+
1951
+ # 8b. Django Signal Receivers via connect()
1952
+ for b in bindings_by_kind.get("DJANGO_SIGNAL_RECEIVER", ()):
1953
+ f_hash = self.file_hashes.get(b.file_path, "")
1954
+ receiver_sym, _, _ = self.resolve_identifier_in_file(
1955
+ b.target_name, b.file_path, use_line=b.line
1956
+ )
1957
+ if not receiver_sym and b.target_name in self.short_to_symbols:
1958
+ receiver_sym = self.short_to_symbols[b.target_name][0]
1959
+ target_canon = receiver_sym.canonical_id if receiver_sym else b.target_name
1960
+
1961
+ sig_name = b.base_expr
1962
+ sender_str = b.source_expr
1963
+ sender_sym = None
1964
+ if sender_str:
1965
+ sender_sym, _, _ = self.resolve_identifier_in_file(
1966
+ sender_str, b.file_path, use_line=b.line
1967
+ )
1968
+ if not sender_sym and "." in sender_str:
1969
+ sender_sym, _, _ = self.resolve_identifier_in_file(
1970
+ sender_str.split(".")[-1], b.file_path, use_line=b.line
1971
+ )
1972
+ if not sender_sym:
1973
+ short_s = sender_str.split(".")[-1]
1974
+ if short_s in self.short_to_symbols:
1975
+ for s in self.short_to_symbols[short_s]:
1976
+ if s.kind == "class":
1977
+ sender_sym = s
1978
+ break
1979
+ if not sender_sym:
1980
+ sender_sym = self.short_to_symbols[short_s][0]
1981
+
1982
+ sender_canon = sender_sym.canonical_id if sender_sym else (sender_str or sig_name)
1983
+ sig_ev = f"Signal '{sig_name}' connects sender '{sender_str or '*'}' to '{target_canon}'"
1984
+ add_ref(
1985
+ Reference(
1986
+ source_symbol_id=sender_canon,
1987
+ target_symbol_id=target_canon,
1988
+ relationship="TRIGGERS_SIGNAL",
1989
+ confidence="HIGH" if sender_sym else "MEDIUM",
1990
+ path=b.file_path,
1991
+ start_line=b.line,
1992
+ end_line=b.line,
1993
+ evidence=sig_ev,
1994
+ source_hash=f_hash,
1995
+ indexed_commit=self.indexed_commit,
1996
+ )
1997
+ )
1998
+ add_edge(
1999
+ GraphEdge(
2000
+ source=sender_canon,
2001
+ target=target_canon,
2002
+ relationship="TRIGGERS_SIGNAL",
2003
+ confidence="HIGH" if sender_sym else "MEDIUM",
2004
+ file=b.file_path,
2005
+ start_line=b.line,
2006
+ end_line=b.line,
2007
+ evidence=sig_ev,
2008
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2009
+ reason=f"django_signal:{sig_name}",
2010
+ )
2011
+ )
2012
+ lifecycle_method = "delete" if "delete" in sig_name.lower() else "save"
2013
+ add_edge(
2014
+ GraphEdge(
2015
+ source=f"{sender_canon}.{lifecycle_method}",
2016
+ target=target_canon,
2017
+ relationship="CALLS",
2018
+ confidence="HIGH" if sender_sym else "MEDIUM",
2019
+ file=b.file_path,
2020
+ start_line=b.line,
2021
+ end_line=b.line,
2022
+ evidence=sig_ev,
2023
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2024
+ reason=f"django_signal:{sig_name}",
2025
+ )
2026
+ )
2027
+ add_edge(
2028
+ GraphEdge(
2029
+ source=target_canon,
2030
+ target=sender_canon,
2031
+ relationship="HANDLES_SIGNAL",
2032
+ confidence="HIGH" if sender_sym else "MEDIUM",
2033
+ file=b.file_path,
2034
+ start_line=b.line,
2035
+ end_line=b.line,
2036
+ evidence=f"Listener '{target_canon}' handles '{sig_name}' from '{sender_canon}'",
2037
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2038
+ reason=f"django_signal:{sig_name}",
2039
+ )
2040
+ )
2041
+
2042
+ # 8c. Django Signal Dispatches via send() / send_robust()
2043
+ for b in bindings_by_kind.get("DJANGO_SIGNAL_SEND", ()):
2044
+ caller_id = _find_caller_id_for_binding(b)
2045
+ sig_name = b.target_name
2046
+ for b_rcv in bindings_by_kind.get("DJANGO_SIGNAL_RECEIVER", ()):
2047
+ if b_rcv.base_expr == sig_name or sig_name.endswith(b_rcv.base_expr):
2048
+ rcv_sym, _, _ = self.resolve_identifier_in_file(b_rcv.target_name, b_rcv.file_path, use_line=b_rcv.line)
2049
+ if not rcv_sym and b_rcv.target_name in self.short_to_symbols:
2050
+ rcv_sym = self.short_to_symbols[b_rcv.target_name][0]
2051
+ target_id = rcv_sym.canonical_id if rcv_sym else b_rcv.target_name
2052
+ ev_str = f"Caller dispatches signal '{sig_name}' triggering '{target_id}'"
2053
+ add_edge(
2054
+ GraphEdge(
2055
+ source=caller_id,
2056
+ target=target_id,
2057
+ relationship="TRIGGERS_SIGNAL",
2058
+ confidence="HIGH",
2059
+ file=b.file_path,
2060
+ start_line=b.line,
2061
+ end_line=b.line,
2062
+ evidence=ev_str,
2063
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2064
+ reason=f"signal_send:{sig_name}",
2065
+ )
2066
+ )
2067
+
2068
+ # 8d. Celery Task Dispatches via .delay() / .apply_async() / send_task()
2069
+ for b in bindings_by_kind.get("CELERY_DISPATCH", ()):
2070
+ caller_id = _find_caller_id_for_binding(b)
2071
+ task_name = b.target_name
2072
+ task_sym, _, _ = self.resolve_identifier_in_file(task_name, b.file_path, use_line=b.line)
2073
+ if not task_sym and task_name in self.short_to_symbols:
2074
+ task_sym = self.short_to_symbols[task_name][0]
2075
+ target_id = task_sym.canonical_id if task_sym else task_name
2076
+ f_hash = self.file_hashes.get(b.file_path, "")
2077
+ ev_str = f"Caller dispatches background Celery task '{task_name}' via .{b.base_expr}() at {b.file_path}:{b.line}"
2078
+ add_ref(
2079
+ Reference(
2080
+ source_symbol_id=caller_id,
2081
+ target_symbol_id=target_id,
2082
+ relationship="DISPATCHES_TASK",
2083
+ confidence="HIGH" if task_sym else "MEDIUM",
2084
+ path=b.file_path,
2085
+ start_line=b.line,
2086
+ end_line=b.line,
2087
+ evidence=ev_str,
2088
+ source_hash=f_hash,
2089
+ indexed_commit=self.indexed_commit,
2090
+ )
2091
+ )
2092
+ add_edge(
2093
+ GraphEdge(
2094
+ source=caller_id,
2095
+ target=target_id,
2096
+ relationship="DISPATCHES_TASK",
2097
+ confidence="HIGH" if task_sym else "MEDIUM",
2098
+ file=b.file_path,
2099
+ start_line=b.line,
2100
+ end_line=b.line,
2101
+ evidence=ev_str,
2102
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2103
+ reason=f"celery_dispatch:{b.base_expr}",
2104
+ )
2105
+ )
2106
+ add_edge(
2107
+ GraphEdge(
2108
+ source=caller_id,
2109
+ target=target_id,
2110
+ relationship="CALLS",
2111
+ confidence="HIGH" if task_sym else "MEDIUM",
2112
+ file=b.file_path,
2113
+ start_line=b.line,
2114
+ end_line=b.line,
2115
+ evidence=ev_str,
2116
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2117
+ reason=f"celery_dispatch:{b.base_expr}",
2118
+ )
2119
+ )
2120
+
2121
+ # 8e. Ray Remote Task Dispatches via .remote()
2122
+ for b in bindings_by_kind.get("RAY_REMOTE", ()):
2123
+ caller_id = _find_caller_id_for_binding(b)
2124
+ task_name = b.target_name
2125
+ task_sym, _, _ = self.resolve_identifier_in_file(task_name, b.file_path, use_line=b.line)
2126
+ if not task_sym and task_name in self.short_to_symbols:
2127
+ task_sym = self.short_to_symbols[task_name][0]
2128
+ target_id = task_sym.canonical_id if task_sym else task_name
2129
+ f_hash = self.file_hashes.get(b.file_path, "")
2130
+ ev_str = f"Caller dispatches remote Ray task '{task_name}' via .{b.base_expr}() at {b.file_path}:{b.line}"
2131
+ add_ref(
2132
+ Reference(
2133
+ source_symbol_id=caller_id,
2134
+ target_symbol_id=target_id,
2135
+ relationship="DISPATCHES_TASK",
2136
+ confidence="HIGH" if task_sym else "MEDIUM",
2137
+ path=b.file_path,
2138
+ start_line=b.line,
2139
+ end_line=b.line,
2140
+ evidence=ev_str,
2141
+ source_hash=f_hash,
2142
+ indexed_commit=self.indexed_commit,
2143
+ )
2144
+ )
2145
+ add_edge(
2146
+ GraphEdge(
2147
+ source=caller_id,
2148
+ target=target_id,
2149
+ relationship="DISPATCHES_TASK",
2150
+ confidence="HIGH" if task_sym else "MEDIUM",
2151
+ file=b.file_path,
2152
+ start_line=b.line,
2153
+ end_line=b.line,
2154
+ evidence=ev_str,
2155
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2156
+ reason=f"ray_remote:{b.base_expr}",
2157
+ )
2158
+ )
2159
+ add_edge(
2160
+ GraphEdge(
2161
+ source=caller_id,
2162
+ target=target_id,
2163
+ relationship="CALLS",
2164
+ confidence="HIGH" if task_sym else "MEDIUM",
2165
+ file=b.file_path,
2166
+ start_line=b.line,
2167
+ end_line=b.line,
2168
+ evidence=ev_str,
2169
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2170
+ reason=f"ray_remote:{b.base_expr}",
2171
+ )
2172
+ )
2173
+
2174
+ # 8f. Agent Tool Registrations (e.g. AgentExecutor(tools=[...]))
2175
+ for b in bindings_by_kind.get("AGENT_TOOL_REGISTRATION", ()):
2176
+ caller_id = _find_caller_id_for_binding(b)
2177
+ tool_name = b.target_name
2178
+ tool_sym, _, _ = self.resolve_identifier_in_file(tool_name, b.file_path, use_line=b.line)
2179
+ if not tool_sym and tool_name in self.short_to_symbols:
2180
+ tool_sym = self.short_to_symbols[tool_name][0]
2181
+ target_id = tool_sym.canonical_id if tool_sym else tool_name
2182
+ f_hash = self.file_hashes.get(b.file_path, "")
2183
+ ev_str = f"Agent '{b.base_expr}' registers tool '{tool_name}' at {b.file_path}:{b.line}"
2184
+ add_ref(
2185
+ Reference(
2186
+ source_symbol_id=caller_id,
2187
+ target_symbol_id=target_id,
2188
+ relationship="REGISTERS",
2189
+ confidence="HIGH" if tool_sym else "MEDIUM",
2190
+ path=b.file_path,
2191
+ start_line=b.line,
2192
+ end_line=b.line,
2193
+ evidence=ev_str,
2194
+ source_hash=f_hash,
2195
+ indexed_commit=self.indexed_commit,
2196
+ )
2197
+ )
2198
+ add_edge(
2199
+ GraphEdge(
2200
+ source=caller_id,
2201
+ target=target_id,
2202
+ relationship="REGISTERS",
2203
+ confidence="HIGH" if tool_sym else "MEDIUM",
2204
+ file=b.file_path,
2205
+ start_line=b.line,
2206
+ end_line=b.line,
2207
+ evidence=ev_str,
2208
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2209
+ reason=f"agent_tool_registration:{b.base_expr}",
2210
+ )
2211
+ )
2212
+
2213
+ # 8g. LCEL Pipeline Chains (prompt | llm | output_parser)
2214
+ for b in bindings_by_kind.get("LCEL_PIPELINE", ()):
2215
+ caller_id = _find_caller_id_for_binding(b)
2216
+ try:
2217
+ steps: list[str] = json.loads(b.source_expr)
2218
+ except Exception:
2219
+ steps = []
2220
+ f_hash = self.file_hashes.get(b.file_path, "")
2221
+ step_syms: list[Symbol | None] = []
2222
+ for s_name in steps:
2223
+ s_sym, _, _ = self.resolve_identifier_in_file(
2224
+ s_name, b.file_path, caller_canonical_id=caller_id, use_line=b.line
2225
+ )
2226
+ if not s_sym and self.binding_resolver:
2227
+ alias_t, _, _ = self.binding_resolver.resolve(
2228
+ b.file_path, s_name, scope=b.scope, use_line=b.line
2229
+ )
2230
+ if alias_t and alias_t in self.canonical_to_symbol:
2231
+ s_sym = self.canonical_to_symbol[alias_t]
2232
+ elif alias_t:
2233
+ s_sym, _, _ = self.resolve_identifier_in_file(alias_t, b.file_path, use_line=b.line)
2234
+ if not s_sym and alias_t in self.short_to_symbols:
2235
+ s_sym = self.short_to_symbols[alias_t][0]
2236
+ if not s_sym and s_name in self.short_to_symbols:
2237
+ s_sym = self.short_to_symbols[s_name][0]
2238
+ step_syms.append(s_sym)
2239
+
2240
+ # Link consecutive pipeline steps: step[i] -> PIPELINE_STEP -> step[i+1]
2241
+ for i in range(len(steps) - 1):
2242
+ sym_from = step_syms[i]
2243
+ sym_to = step_syms[i + 1]
2244
+ from_id = sym_from.canonical_id if sym_from is not None else steps[i]
2245
+ to_id = sym_to.canonical_id if sym_to is not None else steps[i + 1]
2246
+ ev_str = f"LCEL pipeline step '{steps[i]}' -> '{steps[i+1]}' at {b.file_path}:{b.line}"
2247
+ add_ref(
2248
+ Reference(
2249
+ source_symbol_id=from_id,
2250
+ target_symbol_id=to_id,
2251
+ relationship="PIPELINE_STEP",
2252
+ confidence="HIGH" if (sym_from is not None and sym_to is not None) else "MEDIUM",
2253
+ path=b.file_path,
2254
+ start_line=b.line,
2255
+ end_line=b.line,
2256
+ evidence=ev_str,
2257
+ source_hash=f_hash,
2258
+ indexed_commit=self.indexed_commit,
2259
+ )
2260
+ )
2261
+ add_edge(
2262
+ GraphEdge(
2263
+ source=from_id,
2264
+ target=to_id,
2265
+ relationship="PIPELINE_STEP",
2266
+ confidence="HIGH" if (step_syms[i] and step_syms[i+1]) else "MEDIUM",
2267
+ file=b.file_path,
2268
+ start_line=b.line,
2269
+ end_line=b.line,
2270
+ evidence=ev_str,
2271
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2272
+ reason="lcel_pipe",
2273
+ )
2274
+ )
2275
+
2276
+ # 8h. Python View -> Template Rendering (Flask, Django, FastAPI)
2277
+ for b in bindings_by_kind.get("RENDERS_TEMPLATE", ()):
2278
+ caller_id = _find_caller_id_for_binding(b)
2279
+ raw_tmpl = b.target_name
2280
+ resolved_tmpl = raw_tmpl
2281
+ if raw_tmpl not in self.known_files:
2282
+ for kf in self.known_files:
2283
+ if kf.endswith(f"/{raw_tmpl}") or kf == raw_tmpl:
2284
+ resolved_tmpl = kf
2285
+ break
2286
+ f_hash = self.file_hashes.get(b.file_path, "")
2287
+ ev_str = f"View renders HTML template '{raw_tmpl}' at {b.file_path}:{b.line}"
2288
+ add_ref(
2289
+ Reference(
2290
+ source_symbol_id=caller_id,
2291
+ target_symbol_id=resolved_tmpl,
2292
+ relationship="RENDERS_TEMPLATE",
2293
+ confidence="HIGH",
2294
+ path=b.file_path,
2295
+ start_line=b.line,
2296
+ end_line=b.line,
2297
+ evidence=ev_str,
2298
+ source_hash=f_hash,
2299
+ indexed_commit=self.indexed_commit,
2300
+ )
2301
+ )
2302
+ add_edge(
2303
+ GraphEdge(
2304
+ source=caller_id,
2305
+ target=resolved_tmpl,
2306
+ relationship="RENDERS_TEMPLATE",
2307
+ confidence="HIGH",
2308
+ file=b.file_path,
2309
+ start_line=b.line,
2310
+ end_line=b.line,
2311
+ evidence=ev_str,
2312
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2313
+ reason=f"template_render:{raw_tmpl}",
2314
+ )
2315
+ )
2316
+
2317
+ # 9. Dependency Injection & Configuration Intelligence
2318
+ # 9a. Statically grounded provider registration (PROVIDES)
2319
+ provider_map: dict[str, list[tuple[str, BindingRef, str]]] = {}
2320
+ for b in self.bindings:
2321
+ if b.expr_kind == "SEMANTIC_DECORATOR" and b.attr_name == "PROVIDES":
2322
+ if b.subscript_target is not None:
2323
+ findings.append({
2324
+ "pattern": "DI_PROVIDES",
2325
+ "status": "UNKNOWN",
2326
+ "reason": b.subscript_target or "dynamic_provider",
2327
+ "file": b.file_path,
2328
+ "line": b.line,
2329
+ "target_name": b.target_name,
2330
+ })
2331
+ continue
2332
+ dec_meta: dict[str, object] = {}
2333
+ if b.subscript_key:
2334
+ try:
2335
+ dec_meta = json.loads(b.subscript_key)
2336
+ except Exception:
2337
+ dec_meta = {}
2338
+ iface_raw = str(dec_meta.get("interface") or "")
2339
+ impl_canon = b.target_name
2340
+ iface_canon = impl_canon
2341
+ if iface_raw and iface_raw != impl_canon.split(".")[-1]:
2342
+ if_sym, _, _ = self.resolve_identifier_in_file(iface_raw, b.file_path, use_line=b.line)
2343
+ if not if_sym and len(self.short_to_symbols.get(iface_raw, [])) == 1:
2344
+ if_sym = self.short_to_symbols[iface_raw][0]
2345
+ if if_sym:
2346
+ iface_canon = if_sym.canonical_id
2347
+ provider_map.setdefault(iface_canon, []).append((impl_canon, b, "decorator_provider"))
2348
+ continue
2349
+
2350
+ if b.expr_kind != "DI_PROVIDES":
2351
+ continue
2352
+
2353
+ if (
2354
+ b.subscript_target == "dynamic_provider"
2355
+ or b.target_name == "<DYNAMIC>"
2356
+ or b.source_expr == "<DYNAMIC>"
2357
+ ):
2358
+ findings.append({
2359
+ "pattern": "DI_PROVIDES",
2360
+ "status": "UNKNOWN",
2361
+ "reason": "dynamic_provider",
2362
+ "file": b.file_path,
2363
+ "line": b.line,
2364
+ "target_name": b.target_name,
2365
+ })
2366
+ continue
2367
+
2368
+ iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
2369
+ if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
2370
+ iface_sym = self.short_to_symbols[b.target_name][0]
2371
+
2372
+ impl_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2373
+ if not impl_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2374
+ impl_sym = self.short_to_symbols[b.source_expr][0]
2375
+
2376
+ if not iface_sym and b.base_expr == "nestjs_provider" and impl_sym:
2377
+ iface_canon = b.target_name
2378
+ impl_canon = impl_sym.canonical_id
2379
+ provider_map.setdefault(iface_canon, []).append((impl_canon, b, "nestjs_provider"))
2380
+ continue
2381
+
2382
+ if not iface_sym or not impl_sym:
2383
+ findings.append({
2384
+ "pattern": "DI_PROVIDES",
2385
+ "status": "UNKNOWN",
2386
+ "reason": "dynamic_provider",
2387
+ "file": b.file_path,
2388
+ "line": b.line,
2389
+ "target_name": b.target_name,
2390
+ "source_expr": b.source_expr,
2391
+ })
2392
+ continue
2393
+
2394
+ iface_canon = iface_sym.canonical_id
2395
+ impl_canon = impl_sym.canonical_id
2396
+ # For 1-arg container.provide(Impl) / container.singleton(Impl), check if Impl extends a base interface
2397
+ if iface_canon == impl_canon and b.target_name == b.source_expr:
2398
+ for inh in inheritance_by_source.get(impl_canon, ()):
2399
+ base_sym, _, _ = self.resolve_identifier_in_file(inh.base_name, inh.source_file, use_line=inh.line)
2400
+ if not base_sym and len(self.short_to_symbols.get(inh.base_name, [])) == 1:
2401
+ base_sym = self.short_to_symbols[inh.base_name][0]
2402
+ if base_sym:
2403
+ iface_canon = base_sym.canonical_id
2404
+ break
2405
+ provider_map.setdefault(iface_canon, []).append((impl_canon, b, "provider_mapping"))
2406
+
2407
+ # Emit PROVIDES edges (Ambiguity: multiple distinct providers -> POSSIBLE)
2408
+ for iface_canon, reg_list in provider_map.items():
2409
+ distinct_impls = set(impl_c for impl_c, _, _ in reg_list)
2410
+ is_ambiguous = len(distinct_impls) > 1
2411
+
2412
+ for impl_canon, b, _ in reg_list:
2413
+ f_hash = self.file_hashes.get(b.file_path, "")
2414
+ if is_ambiguous:
2415
+ ev_class = RelationshipEvidenceClass.POSSIBLE
2416
+ edge_conf = "LOW"
2417
+ edge_reason = "multiple_providers"
2418
+ elif b.is_conditional:
2419
+ ev_class = RelationshipEvidenceClass.POSSIBLE
2420
+ edge_conf = "LOW"
2421
+ edge_reason = "conditional_provider"
2422
+ else:
2423
+ ev_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
2424
+ edge_conf = "HIGH"
2425
+ edge_reason = f"Statically verified provider mapping '{b.target_name}' -> '{impl_canon}'"
2426
+
2427
+ ev_str = f"Provider mapping: {iface_canon} PROVIDES {impl_canon} at {b.file_path}:{b.line}"
2428
+ add_ref(
2429
+ Reference(
2430
+ source_symbol_id=iface_canon,
2431
+ target_symbol_id=impl_canon,
2432
+ relationship="PROVIDES",
2433
+ confidence=edge_conf,
2434
+ path=b.file_path,
2435
+ start_line=b.line,
2436
+ end_line=b.line,
2437
+ evidence=ev_str,
2438
+ source_hash=f_hash,
2439
+ indexed_commit=self.indexed_commit,
2440
+ )
2441
+ )
2442
+ add_edge(
2443
+ GraphEdge(
2444
+ source=iface_canon,
2445
+ target=impl_canon,
2446
+ relationship="PROVIDES",
2447
+ confidence=edge_conf,
2448
+ file=b.file_path,
2449
+ start_line=b.line,
2450
+ end_line=b.line,
2451
+ evidence=ev_str,
2452
+ evidence_class=ev_class.value,
2453
+ reason=edge_reason,
2454
+ )
2455
+ )
2456
+
2457
+ # 9b. Dependency Resolution (RESOLVES_DEPENDENCY)
2458
+ for b in bindings_by_kind.get("DI_RESOLVES", ()):
2459
+ if b.subscript_target == "dynamic_provider" or b.target_name == "<DYNAMIC>":
2460
+ findings.append({
2461
+ "pattern": "DI_RESOLVES",
2462
+ "status": "UNKNOWN",
2463
+ "reason": "dynamic_provider",
2464
+ "file": b.file_path,
2465
+ "line": b.line,
2466
+ })
2467
+ continue
2468
+
2469
+ iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
2470
+ if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
2471
+ iface_sym = self.short_to_symbols[b.target_name][0]
2472
+
2473
+ if not iface_sym:
2474
+ findings.append({
2475
+ "pattern": "DI_RESOLVES",
2476
+ "status": "UNKNOWN",
2477
+ "reason": "unknown_dependency_interface",
2478
+ "file": b.file_path,
2479
+ "line": b.line,
2480
+ "target_name": b.target_name,
2481
+ })
2482
+ continue
2483
+
2484
+ iface_canon = iface_sym.canonical_id
2485
+ if iface_canon not in provider_map:
2486
+ findings.append({
2487
+ "pattern": "DI_RESOLVES",
2488
+ "status": "UNKNOWN",
2489
+ "reason": "no_provider_registered",
2490
+ "file": b.file_path,
2491
+ "line": b.line,
2492
+ "target_name": b.target_name,
2493
+ })
2494
+ continue
2495
+
2496
+ reg_list = provider_map[iface_canon]
2497
+ distinct_impls = set(impl_c for impl_c, _, _ in reg_list)
2498
+ is_ambiguous = len(distinct_impls) > 1
2499
+
2500
+ if is_ambiguous:
2501
+ findings.append({
2502
+ "pattern": "DI_RESOLVES",
2503
+ "status": "POSSIBLE",
2504
+ "reason": "multiple_providers",
2505
+ "file": b.file_path,
2506
+ "line": b.line,
2507
+ "target_name": b.target_name,
2508
+ })
2509
+
2510
+ for impl_canon, _orig_b, _ in reg_list:
2511
+ f_hash = self.file_hashes.get(b.file_path, "")
2512
+ if is_ambiguous:
2513
+ ev_class = RelationshipEvidenceClass.POSSIBLE
2514
+ edge_conf = "LOW"
2515
+ edge_reason = "multiple_providers"
2516
+ else:
2517
+ ev_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
2518
+ edge_conf = "HIGH"
2519
+ edge_reason = f"Deterministic dependency resolution '{iface_canon}' -> '{impl_canon}'"
2520
+
2521
+ ev_str = f"Dependency resolution: {iface_canon} RESOLVES_DEPENDENCY {impl_canon} at {b.file_path}:{b.line}"
2522
+ add_ref(
2523
+ Reference(
2524
+ source_symbol_id=iface_canon,
2525
+ target_symbol_id=impl_canon,
2526
+ relationship="RESOLVES_DEPENDENCY",
2527
+ confidence=edge_conf,
2528
+ path=b.file_path,
2529
+ start_line=b.line,
2530
+ end_line=b.line,
2531
+ evidence=ev_str,
2532
+ source_hash=f_hash,
2533
+ indexed_commit=self.indexed_commit,
2534
+ )
2535
+ )
2536
+ add_edge(
2537
+ GraphEdge(
2538
+ source=iface_canon,
2539
+ target=impl_canon,
2540
+ relationship="RESOLVES_DEPENDENCY",
2541
+ confidence=edge_conf,
2542
+ file=b.file_path,
2543
+ start_line=b.line,
2544
+ end_line=b.line,
2545
+ evidence=ev_str,
2546
+ evidence_class=ev_class.value,
2547
+ reason=edge_reason,
2548
+ )
2549
+ )
2550
+
2551
+ # 9c. FastAPI Depends(...) Injection (INJECTS)
2552
+ # 1. Collect modern FastAPI Annotated aliases (e.g. SessionDep = Annotated[Session, Depends(get_db)])
2553
+ alias_to_dep: dict[str, str] = {}
2554
+ alias_name_to_dep: dict[str, str] = {}
2555
+ for b in bindings_by_kind.get("DI_ALIAS_INJECTS", []):
2556
+ dep_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2557
+ if not dep_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2558
+ dep_sym = self.short_to_symbols[b.source_expr][0]
2559
+ if dep_sym:
2560
+ dep_canon = dep_sym.canonical_id
2561
+ alias_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
2562
+ alias_canon = alias_sym.canonical_id if alias_sym else f"{b.file_path}:{b.target_name}"
2563
+ alias_to_dep[alias_canon] = dep_canon
2564
+ alias_name_to_dep[b.target_name] = dep_canon
2565
+ ev_str = f"FastAPI Annotated Alias: '{alias_canon}' resolves to Depends({dep_canon}) at {b.file_path}:{b.line}"
2566
+ f_hash = self.file_hashes.get(b.file_path, "")
2567
+ add_ref(
2568
+ Reference(
2569
+ source_symbol_id=alias_canon,
2570
+ target_symbol_id=dep_canon,
2571
+ relationship="DEPENDS_ON",
2572
+ confidence="HIGH",
2573
+ path=b.file_path,
2574
+ start_line=b.line,
2575
+ end_line=b.line,
2576
+ evidence=ev_str,
2577
+ source_hash=f_hash,
2578
+ indexed_commit=self.indexed_commit,
2579
+ )
2580
+ )
2581
+ add_edge(
2582
+ GraphEdge(
2583
+ source=alias_canon,
2584
+ target=dep_canon,
2585
+ relationship="DEPENDS_ON",
2586
+ confidence="HIGH",
2587
+ file=b.file_path,
2588
+ start_line=b.line,
2589
+ end_line=b.line,
2590
+ evidence=ev_str,
2591
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2592
+ reason="fastapi_annotated_alias",
2593
+ )
2594
+ )
2595
+
2596
+ # 2. Resolve function parameters typed with Annotated aliases (e.g. session: SessionDep, current_user: CurrentUser)
2597
+ for b in bindings_by_kind.get("PARAM_TYPE_DI", []):
2598
+ param_type = b.source_expr.split("[")[0].strip().split(".")[-1]
2599
+ dep_injected_canon: str | None = alias_name_to_dep.get(param_type)
2600
+ if not dep_injected_canon:
2601
+ type_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2602
+ if type_sym and type_sym.canonical_id in alias_to_dep:
2603
+ dep_injected_canon = alias_to_dep[type_sym.canonical_id]
2604
+ elif type_sym and type_sym.name in alias_name_to_dep:
2605
+ dep_injected_canon = alias_name_to_dep[type_sym.name]
2606
+ if dep_injected_canon:
2607
+ handler_canon = b.target_name
2608
+ f_hash = self.file_hashes.get(b.file_path, "")
2609
+ ev_str = f"FastAPI Depends (via {param_type}): '{handler_canon}' INJECTS '{dep_injected_canon}' at {b.file_path}:{b.line}"
2610
+ add_ref(
2611
+ Reference(
2612
+ source_symbol_id=handler_canon,
2613
+ target_symbol_id=dep_injected_canon,
2614
+ relationship="INJECTS",
2615
+ confidence="HIGH",
2616
+ path=b.file_path,
2617
+ start_line=b.line,
2618
+ end_line=b.line,
2619
+ evidence=ev_str,
2620
+ source_hash=f_hash,
2621
+ indexed_commit=self.indexed_commit,
2622
+ )
2623
+ )
2624
+ add_edge(
2625
+ GraphEdge(
2626
+ source=handler_canon,
2627
+ target=dep_injected_canon,
2628
+ relationship="INJECTS",
2629
+ confidence="HIGH",
2630
+ file=b.file_path,
2631
+ start_line=b.line,
2632
+ end_line=b.line,
2633
+ evidence=ev_str,
2634
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2635
+ reason="fastapi_annotated_alias_injects",
2636
+ )
2637
+ )
2638
+ handler_short = handler_canon.split(".")[-1]
2639
+ for rt in self.routes:
2640
+ matches_handler = (
2641
+ rt.handler_canonical_id == handler_canon
2642
+ or (rt.handler_name == handler_short and rt.file_path == b.file_path)
2643
+ )
2644
+ if matches_handler:
2645
+ rt_ev = f"Route endpoint '{rt.endpoint_id}' INJECTS '{dep_injected_canon}' (via {param_type}) at {rt.file_path}:{rt.line}"
2646
+ add_ref(
2647
+ Reference(
2648
+ source_symbol_id=rt.endpoint_id,
2649
+ target_symbol_id=dep_injected_canon,
2650
+ relationship="INJECTS",
2651
+ confidence="HIGH",
2652
+ path=rt.file_path,
2653
+ start_line=rt.line,
2654
+ end_line=rt.line,
2655
+ evidence=rt_ev,
2656
+ source_hash=f_hash,
2657
+ indexed_commit=self.indexed_commit,
2658
+ )
2659
+ )
2660
+ add_edge(
2661
+ GraphEdge(
2662
+ source=rt.endpoint_id,
2663
+ target=dep_injected_canon,
2664
+ relationship="INJECTS",
2665
+ confidence="HIGH",
2666
+ file=rt.file_path,
2667
+ start_line=rt.line,
2668
+ end_line=rt.line,
2669
+ evidence=rt_ev,
2670
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2671
+ reason="fastapi_annotated_alias_injects",
2672
+ )
2673
+ )
2674
+
2675
+ # 3. Standard router and direct function Depends(...) bindings
2676
+ for b in (
2677
+ bindings_by_kind.get("ROUTER_DI_INJECTS", [])
2678
+ + bindings_by_kind.get("DI_INJECTS", [])
2679
+ ):
2680
+ if b.expr_kind == "ROUTER_DI_INJECTS":
2681
+ if b.subscript_target == "dynamic_provider" or b.source_expr == "<DYNAMIC>":
2682
+ findings.append({
2683
+ "pattern": "DI_INJECTS",
2684
+ "status": "UNKNOWN",
2685
+ "reason": "dynamic_provider",
2686
+ "file": b.file_path,
2687
+ "line": b.line,
2688
+ "target_name": b.target_name,
2689
+ })
2690
+ continue
2691
+
2692
+ dep_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2693
+ if not dep_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2694
+ dep_sym = self.short_to_symbols[b.source_expr][0]
2695
+ if not dep_sym:
2696
+ findings.append({
2697
+ "pattern": "DI_INJECTS",
2698
+ "status": "UNKNOWN",
2699
+ "reason": "unknown_dependency_target",
2700
+ "file": b.file_path,
2701
+ "line": b.line,
2702
+ "source_expr": b.source_expr,
2703
+ })
2704
+ continue
2705
+
2706
+ dep_canon = dep_sym.canonical_id
2707
+ router_var = b.target_name
2708
+ # Also resolve if router_var was imported into b.file_path (e.g. app.include_router(users_router, dependencies=[...]))
2709
+ router_sym, _, _ = self.resolve_identifier_in_file(router_var, b.file_path, use_line=b.line)
2710
+ target_router_names = {router_var}
2711
+ if router_sym:
2712
+ target_router_names.add(router_sym.name)
2713
+ for imp in self.file_imports.get(b.file_path, ()):
2714
+ if imp.local_name == router_var or imp.alias == router_var or imp.name == router_var:
2715
+ if imp.imported_name:
2716
+ target_router_names.add(imp.imported_name)
2717
+
2718
+ for rt in self.routes:
2719
+ matches_router = (
2720
+ rt.router_name in target_router_names
2721
+ or (router_var == "app" and rt.file_path == b.file_path)
2722
+ )
2723
+ if not matches_router:
2724
+ continue
2725
+ rt_f_hash = self.file_hashes.get(rt.file_path, "")
2726
+ if rt.handler_canonical_id:
2727
+ h_ev = f"Router/App Depends: '{rt.handler_canonical_id}' INJECTS '{dep_canon}' via '{router_var}' at {b.file_path}:{b.line}"
2728
+ add_ref(
2729
+ Reference(
2730
+ source_symbol_id=rt.handler_canonical_id,
2731
+ target_symbol_id=dep_canon,
2732
+ relationship="INJECTS",
2733
+ confidence="HIGH",
2734
+ path=b.file_path,
2735
+ start_line=b.line,
2736
+ end_line=b.line,
2737
+ evidence=h_ev,
2738
+ source_hash=rt_f_hash,
2739
+ indexed_commit=self.indexed_commit,
2740
+ )
2741
+ )
2742
+ add_edge(
2743
+ GraphEdge(
2744
+ source=rt.handler_canonical_id,
2745
+ target=dep_canon,
2746
+ relationship="INJECTS",
2747
+ confidence="HIGH",
2748
+ file=b.file_path,
2749
+ start_line=b.line,
2750
+ end_line=b.line,
2751
+ evidence=h_ev,
2752
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2753
+ reason="fastapi_router_depends",
2754
+ )
2755
+ )
2756
+ rt_ev = f"Route endpoint '{rt.endpoint_id}' INJECTS '{dep_canon}' via '{router_var}' at {b.file_path}:{b.line}"
2757
+ add_ref(
2758
+ Reference(
2759
+ source_symbol_id=rt.endpoint_id,
2760
+ target_symbol_id=dep_canon,
2761
+ relationship="INJECTS",
2762
+ confidence="HIGH",
2763
+ path=rt.file_path,
2764
+ start_line=rt.line,
2765
+ end_line=rt.line,
2766
+ evidence=rt_ev,
2767
+ source_hash=rt_f_hash,
2768
+ indexed_commit=self.indexed_commit,
2769
+ )
2770
+ )
2771
+ add_edge(
2772
+ GraphEdge(
2773
+ source=rt.endpoint_id,
2774
+ target=dep_canon,
2775
+ relationship="INJECTS",
2776
+ confidence="HIGH",
2777
+ file=rt.file_path,
2778
+ start_line=rt.line,
2779
+ end_line=rt.line,
2780
+ evidence=rt_ev,
2781
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2782
+ reason="fastapi_router_depends",
2783
+ )
2784
+ )
2785
+ continue
2786
+
2787
+ if b.subscript_target == "dynamic_provider" or b.source_expr == "<DYNAMIC>":
2788
+ findings.append({
2789
+ "pattern": "DI_INJECTS",
2790
+ "status": "UNKNOWN",
2791
+ "reason": "dynamic_provider",
2792
+ "file": b.file_path,
2793
+ "line": b.line,
2794
+ "target_name": b.target_name,
2795
+ })
2796
+ continue
2797
+
2798
+ dep_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2799
+ if not dep_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2800
+ dep_sym = self.short_to_symbols[b.source_expr][0]
2801
+
2802
+ if not dep_sym:
2803
+ findings.append({
2804
+ "pattern": "DI_INJECTS",
2805
+ "status": "UNKNOWN",
2806
+ "reason": "unknown_dependency_target",
2807
+ "file": b.file_path,
2808
+ "line": b.line,
2809
+ "source_expr": b.source_expr,
2810
+ })
2811
+ continue
2812
+
2813
+ dep_canon = dep_sym.canonical_id
2814
+ handler_canon = b.target_name
2815
+ f_hash = self.file_hashes.get(b.file_path, "")
2816
+ ev_str = f"FastAPI Depends: '{handler_canon}' INJECTS '{dep_canon}' at {b.file_path}:{b.line}"
2817
+
2818
+ add_ref(
2819
+ Reference(
2820
+ source_symbol_id=handler_canon,
2821
+ target_symbol_id=dep_canon,
2822
+ relationship="INJECTS",
2823
+ confidence="HIGH",
2824
+ path=b.file_path,
2825
+ start_line=b.line,
2826
+ end_line=b.line,
2827
+ evidence=ev_str,
2828
+ source_hash=f_hash,
2829
+ indexed_commit=self.indexed_commit,
2830
+ )
2831
+ )
2832
+ add_edge(
2833
+ GraphEdge(
2834
+ source=handler_canon,
2835
+ target=dep_canon,
2836
+ relationship="INJECTS",
2837
+ confidence="HIGH",
2838
+ file=b.file_path,
2839
+ start_line=b.line,
2840
+ end_line=b.line,
2841
+ evidence=ev_str,
2842
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2843
+ reason="fastapi_depends",
2844
+ )
2845
+ )
2846
+
2847
+ # Also link matching route endpoint_id -> dep_canon (scoped to this file or exact canonical ID)
2848
+ handler_short = handler_canon.split(".")[-1]
2849
+ for rt in self.routes:
2850
+ matches_handler = (
2851
+ rt.handler_canonical_id == handler_canon
2852
+ or (rt.handler_name == handler_short and rt.file_path == b.file_path)
2853
+ )
2854
+ if matches_handler:
2855
+ rt_ev = f"Route endpoint '{rt.endpoint_id}' INJECTS '{dep_canon}' at {rt.file_path}:{rt.line}"
2856
+ add_ref(
2857
+ Reference(
2858
+ source_symbol_id=rt.endpoint_id,
2859
+ target_symbol_id=dep_canon,
2860
+ relationship="INJECTS",
2861
+ confidence="HIGH",
2862
+ path=rt.file_path,
2863
+ start_line=rt.line,
2864
+ end_line=rt.line,
2865
+ evidence=rt_ev,
2866
+ source_hash=f_hash,
2867
+ indexed_commit=self.indexed_commit,
2868
+ )
2869
+ )
2870
+ add_edge(
2871
+ GraphEdge(
2872
+ source=rt.endpoint_id,
2873
+ target=dep_canon,
2874
+ relationship="INJECTS",
2875
+ confidence="HIGH",
2876
+ file=rt.file_path,
2877
+ start_line=rt.line,
2878
+ end_line=rt.line,
2879
+ evidence=rt_ev,
2880
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2881
+ reason="fastapi_depends",
2882
+ )
2883
+ )
2884
+
2885
+ # 9d. Constructor & @inject Type-Based Injection (INJECTS)
2886
+ for b in bindings_by_kind.get("CONSTRUCTOR_PARAM", ()):
2887
+ ann_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2888
+ if not ann_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2889
+ ann_sym = self.short_to_symbols[b.source_expr][0]
2890
+
2891
+ if not ann_sym:
2892
+ continue
2893
+
2894
+ ann_canon = ann_sym.canonical_id
2895
+ # Rule: Only create injection relationship when supported DI semantics exist!
2896
+ # i.e., provider mapping exists for this interface OR @inject decorator was present
2897
+ has_provider = ann_canon in provider_map
2898
+ is_inject_dec = (b.base_expr == "inject_decorator")
2899
+
2900
+ if not (has_provider or is_inject_dec):
2901
+ continue
2902
+
2903
+ f_hash = self.file_hashes.get(b.file_path, "")
2904
+ caller_canon = b.target_name
2905
+ ev_str = f"Constructor injection: '{caller_canon}' INJECTS '{ann_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
2906
+
2907
+ add_ref(
2908
+ Reference(
2909
+ source_symbol_id=caller_canon,
2910
+ target_symbol_id=ann_canon,
2911
+ relationship="INJECTS",
2912
+ confidence="HIGH",
2913
+ path=b.file_path,
2914
+ start_line=b.line,
2915
+ end_line=b.line,
2916
+ evidence=ev_str,
2917
+ source_hash=f_hash,
2918
+ indexed_commit=self.indexed_commit,
2919
+ )
2920
+ )
2921
+ add_edge(
2922
+ GraphEdge(
2923
+ source=caller_canon,
2924
+ target=ann_canon,
2925
+ relationship="INJECTS",
2926
+ confidence="HIGH",
2927
+ file=b.file_path,
2928
+ start_line=b.line,
2929
+ end_line=b.line,
2930
+ evidence=ev_str,
2931
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2932
+ reason="constructor_injection",
2933
+ )
2934
+ )
2935
+
2936
+ # 9d-nest. NestJS Constructor & @Inject Parameter Injection (INJECTS)
2937
+ for b in bindings_by_kind.get("NESTJS_INJECTS", ()):
2938
+ ann_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2939
+ if not ann_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2940
+ ann_sym = self.short_to_symbols[b.source_expr][0]
2941
+
2942
+ injected_canon: str | None = None
2943
+ if ann_sym:
2944
+ injected_canon = ann_sym.canonical_id
2945
+ elif b.source_expr in provider_map:
2946
+ injected_canon = provider_map[b.source_expr][0][0]
2947
+ elif b.source_expr:
2948
+ injected_canon = f"token.{b.source_expr}"
2949
+
2950
+ if not injected_canon:
2951
+ continue
2952
+
2953
+ f_hash = self.file_hashes.get(b.file_path, "")
2954
+ caller_canon = b.target_name
2955
+ ev_str = f"NestJS injection: '{caller_canon}' INJECTS '{injected_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
2956
+
2957
+ add_ref(
2958
+ Reference(
2959
+ source_symbol_id=caller_canon,
2960
+ target_symbol_id=injected_canon,
2961
+ relationship="INJECTS",
2962
+ confidence="HIGH",
2963
+ path=b.file_path,
2964
+ start_line=b.line,
2965
+ end_line=b.line,
2966
+ evidence=ev_str,
2967
+ source_hash=f_hash,
2968
+ indexed_commit=self.indexed_commit,
2969
+ )
2970
+ )
2971
+ add_edge(
2972
+ GraphEdge(
2973
+ source=caller_canon,
2974
+ target=injected_canon,
2975
+ relationship="INJECTS",
2976
+ confidence="HIGH",
2977
+ file=b.file_path,
2978
+ start_line=b.line,
2979
+ end_line=b.line,
2980
+ evidence=ev_str,
2981
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2982
+ reason="nestjs_injection",
2983
+ )
2984
+ )
2985
+
2986
+ # Link routes of this controller to the injected dependency
2987
+ for rt in self.routes:
2988
+ if rt.handler_canonical_id.startswith(f"{caller_canon}."):
2989
+ rt_ev = f"NestJS Route endpoint '{rt.endpoint_id}' INJECTS '{injected_canon}' via {caller_canon} at {rt.file_path}:{rt.line}"
2990
+ add_edge(
2991
+ GraphEdge(
2992
+ source=rt.endpoint_id,
2993
+ target=injected_canon,
2994
+ relationship="INJECTS",
2995
+ confidence="HIGH",
2996
+ file=rt.file_path,
2997
+ start_line=rt.line,
2998
+ end_line=rt.line,
2999
+ evidence=rt_ev,
3000
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3001
+ reason="nestjs_route_injection",
3002
+ )
3003
+ )
3004
+
3005
+ # 9d-ctrl. NestJS Controller Mounting (MOUNTS)
3006
+ for b in bindings_by_kind.get("NESTJS_CONTROLLER", ()):
3007
+ ctrl_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
3008
+ if not ctrl_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
3009
+ ctrl_sym = self.short_to_symbols[b.source_expr][0]
3010
+ if ctrl_sym:
3011
+ ctrl_canon = ctrl_sym.canonical_id
3012
+ add_edge(
3013
+ GraphEdge(
3014
+ source=b.target_name,
3015
+ target=ctrl_canon,
3016
+ relationship="MOUNTS",
3017
+ confidence="HIGH",
3018
+ file=b.file_path,
3019
+ start_line=b.line,
3020
+ end_line=b.line,
3021
+ evidence=f"NestJS Module '{b.target_name}' mounts controller '{ctrl_canon}'",
3022
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3023
+ reason="nestjs_module_mount",
3024
+ )
3025
+ )
3026
+
3027
+ # 9d-cycle. Static DI Cycle Detection (INJECTS / PROVIDES / RESOLVES_DEPENDENCY)
3028
+ di_adj: dict[str, list[tuple[str, str, int]]] = {}
3029
+ for ed in list(edges):
3030
+ if ed.relationship == "INJECTS" or (
3031
+ ed.relationship in ("PROVIDES", "RESOLVES_DEPENDENCY") and ed.source != ed.target
3032
+ ):
3033
+ if not ed.source.startswith("API_ENDPOINT:"):
3034
+ di_adj.setdefault(ed.source, []).append((ed.target, ed.file or "", ed.start_line or 1))
3035
+
3036
+ di_visit_state: dict[str, int] = {} # 0=unvisited, 1=visiting, 2=visited
3037
+ di_path_stack: list[str] = []
3038
+ reported_cycles: set[tuple[str, ...]] = set()
3039
+
3040
+ def _detect_di_cycles(u_node: str) -> None:
3041
+ di_visit_state[u_node] = 1
3042
+ di_path_stack.append(u_node)
3043
+ for v_node, ed_file, ed_line in di_adj.get(u_node, []):
3044
+ st = di_visit_state.get(v_node, 0)
3045
+ if st == 1:
3046
+ # Found cycle!
3047
+ if v_node in di_path_stack:
3048
+ c_idx = di_path_stack.index(v_node)
3049
+ cycle_seq = tuple(di_path_stack[c_idx:] + [v_node])
3050
+ else:
3051
+ cycle_seq = (u_node, v_node, u_node)
3052
+ if cycle_seq not in reported_cycles:
3053
+ reported_cycles.add(cycle_seq)
3054
+ cycle_str = " -> ".join(cycle_seq)
3055
+ findings.append({
3056
+ "pattern": "DI_CYCLE",
3057
+ "status": "CONFLICT",
3058
+ "reason": "di_dependency_cycle",
3059
+ "file": ed_file,
3060
+ "line": ed_line,
3061
+ "source": u_node,
3062
+ "target": v_node,
3063
+ "cycle": list(cycle_seq),
3064
+ })
3065
+ add_edge(
3066
+ GraphEdge(
3067
+ source=u_node,
3068
+ target=v_node,
3069
+ relationship="DI_CYCLE",
3070
+ confidence="HIGH",
3071
+ file=ed_file,
3072
+ start_line=ed_line,
3073
+ end_line=ed_line,
3074
+ evidence=f"Circular dependency detected: {cycle_str}",
3075
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
3076
+ reason="di_dependency_cycle",
3077
+ )
3078
+ )
3079
+ elif st == 0:
3080
+ _detect_di_cycles(v_node)
3081
+ di_path_stack.pop()
3082
+ di_visit_state[u_node] = 2
3083
+
3084
+ for start_n in sorted(di_adj.keys()):
3085
+ if di_visit_state.get(start_n, 0) == 0:
3086
+ _detect_di_cycles(start_n)
3087
+
3088
+ # 9e. Environment & Configuration References (CONFIGURES)
3089
+ for b in (
3090
+ bindings_by_kind.get("ENV_CONFIG", [])
3091
+ + bindings_by_kind.get("DYNAMIC_CONFIG", [])
3092
+ ):
3093
+ if (
3094
+ b.subscript_target in ("runtime_configuration", "dynamic_provider")
3095
+ or b.target_name == "<DYNAMIC>"
3096
+ ):
3097
+ findings.append({
3098
+ "pattern": b.expr_kind,
3099
+ "status": "UNKNOWN",
3100
+ "reason": b.subscript_target or "runtime_configuration",
3101
+ "file": b.file_path,
3102
+ "line": b.line,
3103
+ })
3104
+ continue
3105
+
3106
+ var_name = b.target_name
3107
+ target_scope = b.scope or (self.file_to_module.get(b.file_path) or normalize_module(b.file_path))
3108
+ f_hash = self.file_hashes.get(b.file_path, "")
3109
+ ev_str = f"Environment reference '{var_name}' CONFIGURES '{target_scope}' at {b.file_path}:{b.line}"
3110
+
3111
+ add_ref(
3112
+ Reference(
3113
+ source_symbol_id=var_name,
3114
+ target_symbol_id=target_scope,
3115
+ relationship="CONFIGURES",
3116
+ confidence="HIGH",
3117
+ path=b.file_path,
3118
+ start_line=b.line,
3119
+ end_line=b.line,
3120
+ evidence=ev_str,
3121
+ source_hash=f_hash,
3122
+ indexed_commit=self.indexed_commit,
3123
+ )
3124
+ )
3125
+ add_edge(
3126
+ GraphEdge(
3127
+ source=var_name,
3128
+ target=target_scope,
3129
+ relationship="CONFIGURES",
3130
+ confidence="HIGH",
3131
+ file=b.file_path,
3132
+ start_line=b.line,
3133
+ end_line=b.line,
3134
+ evidence=ev_str,
3135
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
3136
+ reason="environment_reference",
3137
+ )
3138
+ )
3139
+
3140
+ # 10. Test Intelligence & Deterministic Test Discovery
3141
+
3142
+ # 10a. Direct Test Calls (TESTS)
3143
+ # Any call originating from a test file to a production symbol
3144
+ for call in self.calls:
3145
+ if not _TEST_FILE_RE.search(call.source_file):
3146
+ continue
3147
+ t_caller_id: str = call.caller_canonical_id or (self.file_to_module.get(call.source_file) or normalize_module(call.source_file))
3148
+ callee_key = (call.source_file, call.line, call.callee)
3149
+ if callee_key not in resolved_calls:
3150
+ callee_key = (call.source_file, call.line, call.qualified_callee or call.callee)
3151
+ if callee_key in resolved_calls:
3152
+ res_canon, conf = resolved_calls[callee_key]
3153
+ if res_canon and res_canon != t_caller_id:
3154
+ t_target_canon: str = res_canon
3155
+ f_hash = self.file_hashes.get(call.source_file, "")
3156
+ ev_str = f"Test directly exercises symbol '{t_target_canon}' at {call.source_file}:{call.line}"
3157
+ add_ref(
3158
+ Reference(
3159
+ source_symbol_id=t_caller_id,
3160
+ target_symbol_id=t_target_canon,
3161
+ relationship="TESTS",
3162
+ confidence=conf,
3163
+ path=call.source_file,
3164
+ start_line=call.line,
3165
+ end_line=call.end_line,
3166
+ evidence=ev_str,
3167
+ source_hash=f_hash,
3168
+ indexed_commit=self.indexed_commit,
3169
+ )
3170
+ )
3171
+ add_edge(
3172
+ GraphEdge(
3173
+ source=t_caller_id,
3174
+ target=t_target_canon,
3175
+ relationship="TESTS",
3176
+ confidence=conf,
3177
+ file=call.source_file,
3178
+ start_line=call.line,
3179
+ end_line=call.end_line,
3180
+ evidence=ev_str,
3181
+ evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value,
3182
+ reason="direct_test_call",
3183
+ )
3184
+ )
3185
+
3186
+ # 10b. Test Route Intelligence (TESTS_ROUTE & handler coverage)
3187
+ for b in (
3188
+ bindings_by_kind.get("TEST_ROUTE", [])
3189
+ + bindings_by_kind.get("TEST_PAGE_GOTO", [])
3190
+ ):
3191
+ if b.expr_kind == "TEST_ROUTE":
3192
+ target_route = b.subscript_key
3193
+ http_method = b.source_expr.upper() # e.g. "POST", "GET"
3194
+ caller_id = _find_caller_id_for_binding(b)
3195
+
3196
+ if not target_route or target_route == "<DYNAMIC>":
3197
+ findings.append({
3198
+ "pattern": "TEST_ROUTE",
3199
+ "status": "UNKNOWN",
3200
+ "reason": "dynamic_test_route",
3201
+ "file": b.file_path,
3202
+ "line": b.line,
3203
+ })
3204
+ continue
3205
+
3206
+ clean_target = posixpath.normpath(target_route.split("?")[0])
3207
+ matched = [
3208
+ r for r in self.routes
3209
+ if (posixpath.normpath(r.route_path) == clean_target or posixpath.normpath(r.normalized_route) == clean_target)
3210
+ and (r.http_method == http_method or http_method in ("REQUEST", ""))
3211
+ ]
3212
+
3213
+ if not matched:
3214
+ matched = [
3215
+ r for r in self.routes
3216
+ if (r.route_path.rstrip("/") == clean_target.rstrip("/") or r.normalized_route.rstrip("/") == clean_target.rstrip("/"))
3217
+ and (r.http_method == http_method or http_method in ("REQUEST", ""))
3218
+ ]
3219
+
3220
+ if matched:
3221
+ for r in matched:
3222
+ f_hash = self.file_hashes.get(b.file_path, "")
3223
+ ev_str = f"Test client exercises route '{r.route_path}' [{r.http_method}] at {b.file_path}:{b.line}"
3224
+ add_ref(
3225
+ Reference(
3226
+ source_symbol_id=caller_id,
3227
+ target_symbol_id=r.endpoint_id,
3228
+ relationship="TESTS_ROUTE",
3229
+ confidence="HIGH",
3230
+ path=b.file_path,
3231
+ start_line=b.line,
3232
+ end_line=b.line,
3233
+ evidence=ev_str,
3234
+ source_hash=f_hash,
3235
+ indexed_commit=self.indexed_commit,
3236
+ )
3237
+ )
3238
+ add_edge(
3239
+ GraphEdge(
3240
+ source=caller_id,
3241
+ target=r.endpoint_id,
3242
+ relationship="TESTS_ROUTE",
3243
+ confidence="HIGH",
3244
+ file=b.file_path,
3245
+ start_line=b.line,
3246
+ end_line=b.line,
3247
+ evidence=ev_str,
3248
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3249
+ reason=f"http_client_test:{r.http_method} {r.route_path}",
3250
+ )
3251
+ )
3252
+ if r.handler_canonical_id:
3253
+ h_ev = f"Route test exercises handler '{r.handler_name}' via '{r.route_path}' at {b.file_path}:{b.line}"
3254
+ add_ref(
3255
+ Reference(
3256
+ source_symbol_id=caller_id,
3257
+ target_symbol_id=r.handler_canonical_id,
3258
+ relationship="TESTS",
3259
+ confidence="MEDIUM",
3260
+ path=b.file_path,
3261
+ start_line=b.line,
3262
+ end_line=b.line,
3263
+ evidence=h_ev,
3264
+ source_hash=f_hash,
3265
+ indexed_commit=self.indexed_commit,
3266
+ )
3267
+ )
3268
+ add_edge(
3269
+ GraphEdge(
3270
+ source=caller_id,
3271
+ target=r.handler_canonical_id,
3272
+ relationship="TESTS",
3273
+ confidence="MEDIUM",
3274
+ file=b.file_path,
3275
+ start_line=b.line,
3276
+ end_line=b.line,
3277
+ evidence=h_ev,
3278
+ evidence_class=RelationshipEvidenceClass.POSSIBLE.value,
3279
+ reason=f"route_handler_test:{r.route_path}",
3280
+ )
3281
+ )
3282
+ else:
3283
+ findings.append({
3284
+ "pattern": "TEST_ROUTE",
3285
+ "status": "UNKNOWN",
3286
+ "reason": f"unmatched_test_route:{clean_target}",
3287
+ "file": b.file_path,
3288
+ "line": b.line,
3289
+ })
3290
+
3291
+ elif b.expr_kind == "TEST_PAGE_GOTO":
3292
+ target_url = b.subscript_key
3293
+ caller_id = _find_caller_id_for_binding(b)
3294
+
3295
+ if not target_url or target_url == "<DYNAMIC>":
3296
+ findings.append({
3297
+ "pattern": "PLAYWRIGHT_ROUTE",
3298
+ "status": "UNKNOWN",
3299
+ "reason": "runtime_navigation",
3300
+ "file": b.file_path,
3301
+ "line": b.line,
3302
+ })
3303
+ continue
3304
+
3305
+ clean_url = target_url
3306
+ if "://" in clean_url:
3307
+ clean_url = "/" + clean_url.split("://", 1)[1].split("/", 1)[-1] if "/" in clean_url.split("://", 1)[1] else "/"
3308
+ clean_url = posixpath.normpath(clean_url.split("?")[0].split("#")[0])
3309
+
3310
+ matched = [
3311
+ r for r in self.routes
3312
+ if posixpath.normpath(r.route_path) == clean_url or posixpath.normpath(r.normalized_route) == clean_url
3313
+ or r.route_path.rstrip("/") == clean_url.rstrip("/")
3314
+ ]
3315
+ if matched:
3316
+ for r in matched:
3317
+ f_hash = self.file_hashes.get(b.file_path, "")
3318
+ ev_str = f"Playwright page.goto exercises route '{r.route_path}' at {b.file_path}:{b.line}"
3319
+ add_ref(
3320
+ Reference(
3321
+ source_symbol_id=caller_id,
3322
+ target_symbol_id=r.endpoint_id,
3323
+ relationship="TESTS_ROUTE",
3324
+ confidence="HIGH",
3325
+ path=b.file_path,
3326
+ start_line=b.line,
3327
+ end_line=b.line,
3328
+ evidence=ev_str,
3329
+ source_hash=f_hash,
3330
+ indexed_commit=self.indexed_commit,
3331
+ )
3332
+ )
3333
+ add_edge(
3334
+ GraphEdge(
3335
+ source=caller_id,
3336
+ target=r.endpoint_id,
3337
+ relationship="TESTS_ROUTE",
3338
+ confidence="HIGH",
3339
+ file=b.file_path,
3340
+ start_line=b.line,
3341
+ end_line=b.line,
3342
+ evidence=ev_str,
3343
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3344
+ reason=f"playwright_goto:{r.route_path}",
3345
+ )
3346
+ )
3347
+ if r.handler_canonical_id:
3348
+ add_edge(
3349
+ GraphEdge(
3350
+ source=caller_id,
3351
+ target=r.handler_canonical_id,
3352
+ relationship="TESTS",
3353
+ confidence="MEDIUM",
3354
+ file=b.file_path,
3355
+ start_line=b.line,
3356
+ end_line=b.line,
3357
+ evidence=ev_str,
3358
+ evidence_class=RelationshipEvidenceClass.POSSIBLE.value,
3359
+ reason=f"playwright_handler_test:{r.route_path}",
3360
+ )
3361
+ )
3362
+ else:
3363
+ findings.append({
3364
+ "pattern": "PLAYWRIGHT_ROUTE",
3365
+ "status": "POSSIBLE",
3366
+ "reason": f"unmatched_browser_url:{clean_url}",
3367
+ "file": b.file_path,
3368
+ "line": b.line,
3369
+ })
3370
+
3371
+ # 10c. DI / Provider Test Intelligence (TESTS_PROVIDER)
3372
+ fixture_defs: dict[str, str] = {}
3373
+ for b in bindings_by_kind.get("PYTEST_FIXTURE_DEF", ()):
3374
+ fixture_defs[b.target_name] = b.source_expr
3375
+
3376
+ for b in bindings_by_kind.get("TEST_FIXTURE_USE", ()):
3377
+ fix_name = b.target_name
3378
+ caller_id = _find_caller_id_for_binding(b)
3379
+
3380
+ target_prov_sym = None
3381
+ if fix_name in fixture_defs:
3382
+ ret_nm = fixture_defs[fix_name]
3383
+ target_prov_sym, _, _ = self.resolve_identifier_in_file(ret_nm, b.file_path, use_line=b.line)
3384
+ if not target_prov_sym and ret_nm in self.short_to_symbols:
3385
+ target_prov_sym = self.short_to_symbols[ret_nm][0]
3386
+ if not target_prov_sym:
3387
+ target_prov_sym, _, _ = self.resolve_identifier_in_file(fix_name, b.file_path, use_line=b.line)
3388
+ if not target_prov_sym and fix_name in self.short_to_symbols:
3389
+ target_prov_sym = self.short_to_symbols[fix_name][0]
3390
+
3391
+ if target_prov_sym:
3392
+ f_hash = self.file_hashes.get(b.file_path, "")
3393
+ ev_str = f"Test fixture '{fix_name}' provides '{target_prov_sym.canonical_id}' at {b.file_path}:{b.line}"
3394
+ add_ref(
3395
+ Reference(
3396
+ source_symbol_id=caller_id,
3397
+ target_symbol_id=target_prov_sym.canonical_id,
3398
+ relationship="TESTS_PROVIDER",
3399
+ confidence="HIGH",
3400
+ path=b.file_path,
3401
+ start_line=b.line,
3402
+ end_line=b.line,
3403
+ evidence=ev_str,
3404
+ source_hash=f_hash,
3405
+ indexed_commit=self.indexed_commit,
3406
+ )
3407
+ )
3408
+ add_edge(
3409
+ GraphEdge(
3410
+ source=caller_id,
3411
+ target=target_prov_sym.canonical_id,
3412
+ relationship="TESTS_PROVIDER",
3413
+ confidence="HIGH",
3414
+ file=b.file_path,
3415
+ start_line=b.line,
3416
+ end_line=b.line,
3417
+ evidence=ev_str,
3418
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
3419
+ reason=f"fixture_dependency_provider:{fix_name}",
3420
+ )
3421
+ )
3422
+
3423
+ # 10d. Event Dispatch & Registry Invocations in Tests
3424
+ for b in bindings_by_kind.get("EVENT_DISPATCH", ()):
3425
+ event_name = b.subscript_key
3426
+ caller_id = _find_caller_id_for_binding(b)
3427
+
3428
+ if not event_name or event_name == "<DYNAMIC>":
3429
+ findings.append({
3430
+ "pattern": "EVENT_DISPATCH",
3431
+ "status": "UNKNOWN",
3432
+ "reason": "dynamic_event_name",
3433
+ "file": b.file_path,
3434
+ "line": b.line,
3435
+ })
3436
+ continue
3437
+
3438
+ matched_listeners = event_listeners_by_key.get(event_name, ())
3439
+ for b2 in matched_listeners:
3440
+ target_handler, _, _ = self.resolve_identifier_in_file(b2.source_expr, b2.file_path, use_line=b2.line)
3441
+ if not target_handler and b2.source_expr in self.short_to_symbols:
3442
+ target_handler = self.short_to_symbols[b2.source_expr][0]
3443
+ if target_handler:
3444
+ f_hash = self.file_hashes.get(b.file_path, "")
3445
+ ev_str = f"Test dispatches event '{event_name}' exercising listener '{target_handler.canonical_id}' at {b.file_path}:{b.line}"
3446
+ add_ref(
3447
+ Reference(
3448
+ source_symbol_id=caller_id,
3449
+ target_symbol_id=target_handler.canonical_id,
3450
+ relationship="TESTS_EVENT_HANDLER",
3451
+ confidence="MEDIUM",
3452
+ path=b.file_path,
3453
+ start_line=b.line,
3454
+ end_line=b.line,
3455
+ evidence=ev_str,
3456
+ source_hash=f_hash,
3457
+ indexed_commit=self.indexed_commit,
3458
+ )
3459
+ )
3460
+ add_edge(
3461
+ GraphEdge(
3462
+ source=caller_id,
3463
+ target=target_handler.canonical_id,
3464
+ relationship="TESTS_EVENT_HANDLER",
3465
+ confidence="MEDIUM",
3466
+ file=b.file_path,
3467
+ start_line=b.line,
3468
+ end_line=b.line,
3469
+ evidence=ev_str,
3470
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3471
+ reason=f"event_dispatch_test:{event_name}",
3472
+ )
3473
+ )
3474
+
3475
+ # 12. Monorepo / Workspace package dependencies
3476
+ if self.workspace and self.workspace.dependencies:
3477
+ for dep in self.workspace.dependencies:
3478
+ src_pkg = self.workspace.packages.get(dep.source_package_id)
3479
+ edge_file = ""
3480
+ if src_pkg:
3481
+ for ep in src_pkg.entrypoints:
3482
+ if ep in self.known_files:
3483
+ edge_file = ep
3484
+ break
3485
+ if not edge_file and src_pkg.root_path:
3486
+ for kf in self.known_files:
3487
+ if kf.startswith(f"{src_pkg.root_path}/"):
3488
+ edge_file = kf
3489
+ break
3490
+ if not edge_file and src_pkg.manifest_path in self.known_files:
3491
+ edge_file = src_pkg.manifest_path
3492
+
3493
+ if edge_file:
3494
+ add_edge(
3495
+ GraphEdge(
3496
+ source=dep.source_package_id,
3497
+ target=dep.target_package_id,
3498
+ relationship="DEPENDS_ON_PACKAGE",
3499
+ confidence="HIGH",
3500
+ file=edge_file,
3501
+ start_line=1,
3502
+ end_line=1,
3503
+ evidence=dep.evidence or f"{dep.source_name} depends on {dep.target_name}",
3504
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3505
+ reason="workspace_package_manifest_dependency",
3506
+ )
3507
+ )
3508
+
3509
+ # 12. Frontend Web Graph (React, JSX, Hooks, Styles, HTML - Pillar 4)
3510
+ for b in bindings_by_kind.get("REACT_RENDERS", ()):
3511
+ child_name = b.target_name
3512
+ caller_id = _find_caller_id_for_binding(b)
3513
+ target_sym, _, _ = self.resolve_identifier_in_file(child_name, b.file_path, use_line=b.line)
3514
+ if not target_sym and child_name in self.short_to_symbols:
3515
+ candidates = [s for s in self.short_to_symbols[child_name] if s.kind in ("component", "function")]
3516
+ if candidates:
3517
+ target_sym = candidates[0]
3518
+ target_id = target_sym.canonical_id if target_sym else child_name
3519
+ ev_str = f"React component renders '<{child_name} />' at {b.file_path}:{b.line}"
3520
+ f_hash = self.file_hashes.get(b.file_path, "")
3521
+ add_ref(
3522
+ Reference(
3523
+ source_symbol_id=caller_id,
3524
+ target_symbol_id=target_id,
3525
+ relationship="RENDERS",
3526
+ confidence="HIGH" if target_sym else "MEDIUM",
3527
+ path=b.file_path,
3528
+ start_line=b.line,
3529
+ end_line=b.line,
3530
+ evidence=ev_str,
3531
+ source_hash=f_hash,
3532
+ indexed_commit=self.indexed_commit,
3533
+ )
3534
+ )
3535
+ add_edge(
3536
+ GraphEdge(
3537
+ source=caller_id,
3538
+ target=target_id,
3539
+ relationship="RENDERS",
3540
+ confidence="HIGH" if target_sym else "MEDIUM",
3541
+ file=b.file_path,
3542
+ start_line=b.line,
3543
+ end_line=b.line,
3544
+ evidence=ev_str,
3545
+ evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value if target_sym else RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3546
+ reason=f"react_jsx_render:{child_name}",
3547
+ )
3548
+ )
3549
+
3550
+ for b in bindings_by_kind.get("REACT_USES_HOOK", ()):
3551
+ hook_name = b.target_name
3552
+ caller_id = _find_caller_id_for_binding(b)
3553
+ target_sym, _, _ = self.resolve_identifier_in_file(hook_name, b.file_path, use_line=b.line)
3554
+ if not target_sym and hook_name in self.short_to_symbols:
3555
+ candidates = [s for s in self.short_to_symbols[hook_name] if s.kind in ("hook", "function")]
3556
+ if candidates:
3557
+ target_sym = candidates[0]
3558
+ target_id = target_sym.canonical_id if target_sym else hook_name
3559
+ ev_str = f"React component invokes hook '{hook_name}()' at {b.file_path}:{b.line}"
3560
+ f_hash = self.file_hashes.get(b.file_path, "")
3561
+ add_ref(
3562
+ Reference(
3563
+ source_symbol_id=caller_id,
3564
+ target_symbol_id=target_id,
3565
+ relationship="USES_HOOK",
3566
+ confidence="HIGH" if target_sym else "MEDIUM",
3567
+ path=b.file_path,
3568
+ start_line=b.line,
3569
+ end_line=b.line,
3570
+ evidence=ev_str,
3571
+ source_hash=f_hash,
3572
+ indexed_commit=self.indexed_commit,
3573
+ )
3574
+ )
3575
+ add_edge(
3576
+ GraphEdge(
3577
+ source=caller_id,
3578
+ target=target_id,
3579
+ relationship="USES_HOOK",
3580
+ confidence="HIGH" if target_sym else "MEDIUM",
3581
+ file=b.file_path,
3582
+ start_line=b.line,
3583
+ end_line=b.line,
3584
+ evidence=ev_str,
3585
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3586
+ reason=f"react_hook_call:{hook_name}",
3587
+ )
3588
+ )
3589
+
3590
+ for b in bindings_by_kind.get("REACT_IMPORTS_STYLE", ()):
3591
+ style_rel = b.target_name
3592
+ src_dir = posixpath.dirname(b.file_path.replace("\\", "/").lstrip("./"))
3593
+ resolved_style = posixpath.normpath(posixpath.join(src_dir, style_rel)) if style_rel.startswith(".") else style_rel
3594
+ if resolved_style not in self.known_files:
3595
+ for kf in self.known_files:
3596
+ if kf.endswith(f"/{style_rel}") or kf == style_rel:
3597
+ resolved_style = kf
3598
+ break
3599
+ caller_id = _find_caller_id_for_binding(b)
3600
+ ev_str = f"Component imports stylesheet '{style_rel}' at {b.file_path}:{b.line}"
3601
+ f_hash = self.file_hashes.get(b.file_path, "")
3602
+ add_ref(
3603
+ Reference(
3604
+ source_symbol_id=caller_id,
3605
+ target_symbol_id=resolved_style,
3606
+ relationship="IMPORTS_STYLE",
3607
+ confidence="HIGH",
3608
+ path=b.file_path,
3609
+ start_line=b.line,
3610
+ end_line=b.line,
3611
+ evidence=ev_str,
3612
+ source_hash=f_hash,
3613
+ indexed_commit=self.indexed_commit,
3614
+ )
3615
+ )
3616
+ add_edge(
3617
+ GraphEdge(
3618
+ source=caller_id,
3619
+ target=resolved_style,
3620
+ relationship="IMPORTS_STYLE",
3621
+ confidence="HIGH",
3622
+ file=b.file_path,
3623
+ start_line=b.line,
3624
+ end_line=b.line,
3625
+ evidence=ev_str,
3626
+ evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value,
3627
+ reason=f"react_style_import:{style_rel}",
3628
+ )
3629
+ )
3630
+
3631
+ for b in bindings_by_kind.get("REACT_USES_STYLE_CLASS", ()):
3632
+ cls_name = b.target_name
3633
+ caller_id = _find_caller_id_for_binding(b)
3634
+ ev_str = f"Component applies CSS class '.{cls_name}' at {b.file_path}:{b.line}"
3635
+ f_hash = self.file_hashes.get(b.file_path, "")
3636
+ add_ref(
3637
+ Reference(
3638
+ source_symbol_id=caller_id,
3639
+ target_symbol_id=f".{cls_name}",
3640
+ relationship="USES_STYLE_CLASS",
3641
+ confidence="HIGH",
3642
+ path=b.file_path,
3643
+ start_line=b.line,
3644
+ end_line=b.line,
3645
+ evidence=ev_str,
3646
+ source_hash=f_hash,
3647
+ indexed_commit=self.indexed_commit,
3648
+ )
3649
+ )
3650
+ add_edge(
3651
+ GraphEdge(
3652
+ source=caller_id,
3653
+ target=f".{cls_name}",
3654
+ relationship="USES_STYLE_CLASS",
3655
+ confidence="HIGH",
3656
+ file=b.file_path,
3657
+ start_line=b.line,
3658
+ end_line=b.line,
3659
+ evidence=ev_str,
3660
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3661
+ reason=f"css_class_use:.{cls_name}",
3662
+ )
3663
+ )
3664
+
3665
+ for b in bindings_by_kind.get("REACT_FETCH_ROUTE", ()):
3666
+ raw_url = b.target_name
3667
+ caller_id = _find_caller_id_for_binding(b)
3668
+ client_method = b.base_expr or "GET"
3669
+ norm_client = normalize_route(client_method, raw_url)
3670
+
3671
+ matched_routes: list[tuple[RouteDetection, str]] = []
3672
+ for r in self.routes:
3673
+ if (
3674
+ r.route_path == raw_url
3675
+ or r.normalized_route == raw_url
3676
+ or r.route_path.rstrip("/") == raw_url.rstrip("/")
3677
+ ):
3678
+ matched_routes.append((r, "EXACT"))
3679
+ continue
3680
+ norm_backend = normalize_route(r.http_method, r.route_path)
3681
+ is_matched, match_type = match_route_topology(
3682
+ norm_client, norm_backend, allow_proxy_prefix=True, match_methods=True
3683
+ )
3684
+ if is_matched:
3685
+ matched_routes.append((r, match_type))
3686
+
3687
+ f_hash = self.file_hashes.get(b.file_path, "")
3688
+ if matched_routes:
3689
+ for r, match_type in matched_routes:
3690
+ ev_str = f"Client fetches route '{r.route_path}' ({match_type}) at {b.file_path}:{b.line}"
3691
+ add_ref(
3692
+ Reference(
3693
+ source_symbol_id=caller_id,
3694
+ target_symbol_id=r.endpoint_id,
3695
+ relationship="FETCHES_ROUTE",
3696
+ confidence="HIGH",
3697
+ path=b.file_path,
3698
+ start_line=b.line,
3699
+ end_line=b.line,
3700
+ evidence=ev_str,
3701
+ source_hash=f_hash,
3702
+ indexed_commit=self.indexed_commit,
3703
+ )
3704
+ )
3705
+ add_edge(
3706
+ GraphEdge(
3707
+ source=caller_id,
3708
+ target=r.endpoint_id,
3709
+ relationship="FETCHES_ROUTE",
3710
+ confidence="HIGH",
3711
+ file=b.file_path,
3712
+ start_line=b.line,
3713
+ end_line=b.line,
3714
+ evidence=ev_str,
3715
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3716
+ reason=f"client_fetch:{r.route_path}",
3717
+ )
3718
+ )
3719
+ if r.handler_canonical_id:
3720
+ h_ev = f"Client invokes backend handler '{r.handler_canonical_id}' via {r.route_path} ({match_type}) at {b.file_path}:{b.line}"
3721
+ add_ref(
3722
+ Reference(
3723
+ source_symbol_id=caller_id,
3724
+ target_symbol_id=r.handler_canonical_id,
3725
+ relationship="CALLS",
3726
+ confidence="HIGH",
3727
+ path=b.file_path,
3728
+ start_line=b.line,
3729
+ end_line=b.line,
3730
+ evidence=h_ev,
3731
+ source_hash=f_hash,
3732
+ indexed_commit=self.indexed_commit,
3733
+ )
3734
+ )
3735
+ add_edge(
3736
+ GraphEdge(
3737
+ source=caller_id,
3738
+ target=r.handler_canonical_id,
3739
+ relationship="CALLS",
3740
+ confidence="HIGH",
3741
+ file=b.file_path,
3742
+ start_line=b.line,
3743
+ end_line=b.line,
3744
+ evidence=h_ev,
3745
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
3746
+ reason=f"client_handler_call:{r.route_path}",
3747
+ )
3748
+ )
3749
+ else:
3750
+ ev_str = f"Client fetches external/custom route '{raw_url}' at {b.file_path}:{b.line}"
3751
+ add_edge(
3752
+ GraphEdge(
3753
+ source=caller_id,
3754
+ target=raw_url,
3755
+ relationship="FETCHES_ROUTE",
3756
+ confidence="LOW",
3757
+ file=b.file_path,
3758
+ start_line=b.line,
3759
+ end_line=b.line,
3760
+ evidence=ev_str,
3761
+ evidence_class=RelationshipEvidenceClass.POSSIBLE.value,
3762
+ reason=f"client_fetch:{raw_url}",
3763
+ )
3764
+ )
3765
+
3766
+ for b in bindings_by_kind.get("HTML_LOADS_SCRIPT", ()):
3767
+ src_val = b.target_name
3768
+ target_file = src_val
3769
+ for kf in self.known_files:
3770
+ if kf.endswith(f"/{src_val}") or kf == src_val:
3771
+ target_file = kf
3772
+ break
3773
+ ev_str = f"HTML loads script '{src_val}' at {b.file_path}:{b.line}"
3774
+ f_hash = self.file_hashes.get(b.file_path, "")
3775
+ add_ref(
3776
+ Reference(
3777
+ source_symbol_id=b.file_path,
3778
+ target_symbol_id=target_file,
3779
+ relationship="LOADS_SCRIPT",
3780
+ confidence="HIGH",
3781
+ path=b.file_path,
3782
+ start_line=b.line,
3783
+ end_line=b.line,
3784
+ evidence=ev_str,
3785
+ source_hash=f_hash,
3786
+ indexed_commit=self.indexed_commit,
3787
+ )
3788
+ )
3789
+ add_edge(
3790
+ GraphEdge(
3791
+ source=b.file_path,
3792
+ target=target_file,
3793
+ relationship="LOADS_SCRIPT",
3794
+ confidence="HIGH",
3795
+ file=b.file_path,
3796
+ start_line=b.line,
3797
+ end_line=b.line,
3798
+ evidence=ev_str,
3799
+ evidence_class=RelationshipEvidenceClass.STATIC_VERIFIED.value,
3800
+ reason=f"html_script:{src_val}",
3801
+ )
3802
+ )
3803
+
3804
+ for b in bindings_by_kind.get("HTML_LOADS_STYLESHEET", ()):
3805
+ href_val = b.target_name
3806
+ target_file = href_val
3807
+ for kf in self.known_files:
3808
+ if kf.endswith(f"/{href_val}") or kf == href_val:
3809
+ target_file = kf
3810
+ break
3811
+ ev_str = f"HTML loads stylesheet '{href_val}' at {b.file_path}:{b.line}"
3812
+ f_hash = self.file_hashes.get(b.file_path, "")
3813
+ add_ref(
3814
+ Reference(
3815
+ source_symbol_id=b.file_path,
3816
+ target_symbol_id=target_file,
3817
+ relationship="LOADS_STYLESHEET",
3818
+ confidence="HIGH",
3819
+ path=b.file_path,
3820
+ start_line=b.line,
3821
+ end_line=b.line,
3822
+ evidence=ev_str,
3823
+ source_hash=f_hash,
3824
+ indexed_commit=self.indexed_commit,
3825
+ )
3826
+ )
3827
+ add_edge(
3828
+ GraphEdge(
3829
+ source=b.file_path,
3830
+ target=target_file,
3831
+ relationship="LOADS_STYLESHEET",
3832
+ confidence="HIGH",
3833
+ file=b.file_path,
3834
+ start_line=b.line,
3835
+ end_line=b.line,
3836
+ evidence=ev_str,
3837
+ evidence_class=RelationshipEvidenceClass.STATIC_VERIFIED.value,
3838
+ reason=f"html_stylesheet:{href_val}",
3839
+ )
3840
+ )
3841
+
3842
+ # Roblox & Luau Intelligence (Phases R2-R8)
3843
+ has_luau_or_roblox = any(
3844
+ f.endswith((".lua", ".luau")) for f in self.known_files
3845
+ ) or any(
3846
+ b.expr_kind.startswith("ROBLOX_") for b in self.bindings
3847
+ )
3848
+ if has_luau_or_roblox:
3849
+ from codegraph.roblox import resolve_roblox_intelligence
3850
+
3851
+ repo_path = Path(self.repository) if self.repository is not None else None
3852
+ resolve_roblox_intelligence(
3853
+ repository=repo_path,
3854
+ symbols=self.symbols,
3855
+ bindings=self.bindings,
3856
+ known_files=self.known_files,
3857
+ file_hashes=self.file_hashes,
3858
+ indexed_commit=self.indexed_commit,
3859
+ add_ref=add_ref,
3860
+ add_edge=add_edge,
3861
+ resolved_imports=resolved_imports,
3862
+ )
3863
+
3864
+ return ResolutionOutput(
3865
+ references=references,
3866
+ edges=edges,
3867
+ resolved_imports=resolved_imports,
3868
+ resolved_calls=resolved_calls,
3869
+ findings=findings,
3870
+ )
3871
+
3872
+
3873
+ def resolve_symbol_diagnostic(
3874
+ con: sqlite3.Connection,
3875
+ expression: str,
3876
+ context_file: str | None = None,
3877
+ ) -> dict[str, object]:
3878
+ """Provide structured diagnostic output for `codegraph resolve <expression>`."""
3879
+ expr = expression.strip()
3880
+ if not expr:
3881
+ return {
3882
+ "input": expr,
3883
+ "confidence": "UNKNOWN",
3884
+ "reason": "Empty symbol expression.",
3885
+ }
3886
+
3887
+ # 1. Direct canonical or qualified match in symbols table
3888
+ direct = con.execute(
3889
+ "SELECT canonical_id, qualified_name, module, path, start_line "
3890
+ "FROM symbols WHERE canonical_id=? OR qualified_name=? ORDER BY path, start_line LIMIT 1",
3891
+ (expr, expr),
3892
+ ).fetchone()
3893
+
3894
+ # Check if expr starts with an imported alias first (e.g., AS.login or authenticate)
3895
+ prefix = expr.split(".", 1)[0]
3896
+ rest = expr.split(".", 1)[1] if "." in expr else ""
3897
+
3898
+ imp_query = (
3899
+ "SELECT source_path, module, imported_module, name, imported_name, alias, local_name, resolved_module "
3900
+ "FROM imports WHERE (alias=? OR local_name=?)"
3901
+ )
3902
+ params: list[object] = [prefix, prefix]
3903
+ if context_file:
3904
+ imp_query += " AND source_path=?"
3905
+ params.append(context_file)
3906
+ imp_rows = con.execute(imp_query, params).fetchall()
3907
+
3908
+ for imp in imp_rows:
3909
+ target_mod = imp["resolved_module"] or imp["imported_module"] or imp["module"]
3910
+ orig_name = imp["imported_name"] or imp["name"] or prefix
3911
+ candidate_qname = f"{orig_name}.{rest}" if rest else orig_name
3912
+ sym_row = con.execute(
3913
+ "SELECT canonical_id, qualified_name, module, path, start_line "
3914
+ "FROM symbols WHERE (module=? OR module LIKE ?) AND (qualified_name=? OR name=?) "
3915
+ "ORDER BY path, start_line LIMIT 1",
3916
+ (target_mod, f"%.{target_mod}", candidate_qname, candidate_qname),
3917
+ ).fetchone()
3918
+ if sym_row:
3919
+ return {
3920
+ "input": expr,
3921
+ "alias": imp["alias"] or imp["local_name"],
3922
+ "resolved_module": target_mod,
3923
+ "resolved_symbol": sym_row["qualified_name"],
3924
+ "canonical_id": sym_row["canonical_id"],
3925
+ "confidence": "HIGH",
3926
+ "evidence": f"{sym_row['path']}:{sym_row['start_line']}",
3927
+ }
3928
+
3929
+ if direct:
3930
+ return {
3931
+ "input": expr,
3932
+ "alias": None,
3933
+ "resolved_module": direct["module"],
3934
+ "resolved_symbol": direct["qualified_name"],
3935
+ "canonical_id": direct["canonical_id"],
3936
+ "confidence": "HIGH",
3937
+ "evidence": f"{direct['path']}:{direct['start_line']}",
3938
+ }
3939
+
3940
+ # Check short name match
3941
+ short_rows = con.execute(
3942
+ "SELECT canonical_id, qualified_name, module, path, start_line FROM symbols WHERE name=?",
3943
+ (expr,),
3944
+ ).fetchall()
3945
+ if len(short_rows) == 1:
3946
+ r = short_rows[0]
3947
+ return {
3948
+ "input": expr,
3949
+ "alias": None,
3950
+ "resolved_module": r["module"],
3951
+ "resolved_symbol": r["qualified_name"],
3952
+ "canonical_id": r["canonical_id"],
3953
+ "confidence": "MEDIUM",
3954
+ "evidence": f"{r['path']}:{r['start_line']}",
3955
+ }
3956
+ if len(short_rows) > 1:
3957
+ return {
3958
+ "input": expr,
3959
+ "confidence": "LOW",
3960
+ "reason": f"Ambiguous symbol '{expr}' found in {len(short_rows)} modules: "
3961
+ + ", ".join(r["canonical_id"] for r in short_rows[:5]),
3962
+ }
3963
+
3964
+ return {
3965
+ "input": expr,
3966
+ "confidence": "UNKNOWN",
3967
+ "reason": "No matching imported symbol found.",
3968
+ }