codegraph-engine 2.3.0__py3-none-any.whl → 3.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/__main__.py +7 -0
  3. codegraph/agent_brain.py +253 -2
  4. codegraph/agent_capabilities.py +1348 -67
  5. codegraph/agent_rules.py +2 -1
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture_rules.py +123 -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/change_impact.py +320 -110
  22. codegraph/changelog.py +170 -0
  23. codegraph/cli.py +1605 -62
  24. codegraph/cli_output.py +17 -2
  25. codegraph/context.py +135 -24
  26. codegraph/context_freshness.py +2 -0
  27. codegraph/database/__init__.py +14 -0
  28. codegraph/database/extractor.py +712 -17
  29. codegraph/database/interrogation.py +388 -2
  30. codegraph/database/schema_drift.py +862 -0
  31. codegraph/dataflow_propagation.py +118 -0
  32. codegraph/dead_code.py +387 -0
  33. codegraph/epistemic.py +15 -0
  34. codegraph/evidence_contract.py +34 -3
  35. codegraph/federation.py +245 -0
  36. codegraph/frameworks.py +234 -12
  37. codegraph/frameworks_nestjs.py +502 -0
  38. codegraph/frameworks_nextjs.py +337 -0
  39. codegraph/frameworks_react.py +457 -0
  40. codegraph/freshness.py +2 -0
  41. codegraph/frontend_html.py +109 -0
  42. codegraph/git.py +2 -0
  43. codegraph/git_state.py +2 -0
  44. codegraph/graph/traversal.py +150 -7
  45. codegraph/graph_csr.py +218 -0
  46. codegraph/incremental.py +150 -0
  47. codegraph/indexing/__init__.py +7 -1
  48. codegraph/indexing/classifier.py +5 -0
  49. codegraph/indexing/indexer.py +292 -38
  50. codegraph/indexing/languages/__init__.py +1 -0
  51. codegraph/indexing/languages/cpp.py +457 -0
  52. codegraph/indexing/languages/go.py +459 -0
  53. codegraph/indexing/languages/jvm.py +583 -0
  54. codegraph/indexing/languages/rust.py +507 -0
  55. codegraph/indexing/models.py +15 -1
  56. codegraph/indexing/parser.py +385 -9
  57. codegraph/indexing/pool.py +136 -0
  58. codegraph/indexing/protocols.py +220 -0
  59. codegraph/indexing/scanner.py +23 -0
  60. codegraph/indexing/templates.py +418 -0
  61. codegraph/interrogation.py +207 -39
  62. codegraph/lsp.py +318 -0
  63. codegraph/lsp_bridge/__init__.py +16 -0
  64. codegraph/lsp_bridge/bridge.py +102 -0
  65. codegraph/lsp_bridge/client.py +207 -0
  66. codegraph/lsp_bridge/models.py +71 -0
  67. codegraph/mcp/server.py +502 -29
  68. codegraph/memory/trajectory.py +282 -0
  69. codegraph/models.py +7 -0
  70. codegraph/monorepo.py +3 -0
  71. codegraph/multi_agent/__init__.py +13 -0
  72. codegraph/multi_agent/coordinator.py +229 -0
  73. codegraph/multi_agent/models.py +76 -0
  74. codegraph/optimizer.py +42 -5
  75. codegraph/polymorphism.py +95 -0
  76. codegraph/pr_review.py +296 -0
  77. codegraph/process_lifecycle.py +50 -12
  78. codegraph/reachability.py +198 -0
  79. codegraph/reachability_index.py +74 -0
  80. codegraph/refactor/__init__.py +48 -0
  81. codegraph/refactor/models.py +114 -0
  82. codegraph/refactor/patcher.py +348 -0
  83. codegraph/refactor/renamer.py +844 -0
  84. codegraph/refactor/transactions.py +414 -0
  85. codegraph/repair.py +233 -0
  86. codegraph/resolver.py +1357 -161
  87. codegraph/retrieval_policy.py +22 -1
  88. codegraph/roblox.py +2174 -0
  89. codegraph/route_composer.py +7 -0
  90. codegraph/route_topology.py +172 -0
  91. codegraph/runtime/__init__.py +25 -0
  92. codegraph/runtime/collector.py +155 -0
  93. codegraph/runtime/db_watcher.py +191 -0
  94. codegraph/runtime/ingestor.py +30 -10
  95. codegraph/runtime/otel_receiver.py +208 -0
  96. codegraph/runtime/port_inspector.py +304 -0
  97. codegraph/runtime/runner.py +155 -6
  98. codegraph/runtime/trace_linker.py +175 -0
  99. codegraph/sandbox.py +215 -0
  100. codegraph/search/hybrid.py +26 -0
  101. codegraph/security/supply_chain.py +188 -0
  102. codegraph/security/taint.py +421 -0
  103. codegraph/semantic_decorators.py +34 -3
  104. codegraph/structural_diff.py +2 -0
  105. codegraph/styles.py +144 -0
  106. codegraph/symbol_history.py +2 -0
  107. codegraph/test_gen.py +437 -0
  108. codegraph/tsconfig.py +236 -0
  109. codegraph/ui/__init__.py +6 -0
  110. codegraph/ui/server.py +876 -0
  111. codegraph/watcher.py +323 -0
  112. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
  113. codegraph_engine-3.0.1.dist-info/RECORD +164 -0
  114. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
  115. codegraph_engine-2.3.0.dist-info/RECORD +0 -94
  116. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
  117. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
  118. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
codegraph/resolver.py CHANGED
@@ -20,6 +20,7 @@ import posixpath
20
20
  import re
21
21
  import sqlite3
22
22
  from dataclasses import dataclass, field
23
+ from pathlib import Path
23
24
 
24
25
  from codegraph.binding_resolver import LocalBindingResolver
25
26
  from codegraph.epistemic import RelationshipEvidenceClass
@@ -36,6 +37,8 @@ from codegraph.indexing.models import (
36
37
  )
37
38
  from codegraph.monorepo import WorkspaceInfo
38
39
  from codegraph.route_composer import ComposedMountEdge
40
+ from codegraph.route_topology import match_route_topology, normalize_route
41
+ from codegraph.tsconfig import TsConfigResolver
39
42
 
40
43
  _TEST_FILE_RE = re.compile(
41
44
  r"(^|[_/])test[_s]?[_/]|[_/]spec[_/]|test[_s]?\.(py|js|ts|jsx|tsx)$|spec\.(py|js|ts|jsx|tsx)$",
@@ -51,6 +54,7 @@ def resolve_module_to_path(
51
54
  source_file: str,
52
55
  known_files: set[str],
53
56
  workspace: WorkspaceInfo | None = None,
57
+ tsconfig_resolver: TsConfigResolver | None = None,
54
58
  ) -> str | None:
55
59
  """Deterministically resolve an import specifier to a repository-relative file path.
56
60
 
@@ -74,6 +78,12 @@ def resolve_module_to_path(
74
78
  if f"{clean_entry}/__init__{ext}" in known_files:
75
79
  return f"{clean_entry}/__init__{ext}"
76
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
+
77
87
  src_clean = source_file.replace("\\", "/").lstrip("./")
78
88
  src_dir = posixpath.dirname(src_clean)
79
89
 
@@ -150,6 +160,8 @@ class ReferenceResolver:
150
160
  mount_edges: list[ComposedMountEdge] | None = None,
151
161
  bindings: list[BindingRef] | None = None,
152
162
  workspace: WorkspaceInfo | None = None,
163
+ tsconfig_resolver: TsConfigResolver | None = None,
164
+ repository: Path | None = None,
153
165
  max_reexport_depth: int = 16,
154
166
  max_wildcard_expansions: int = 64,
155
167
  ) -> None:
@@ -164,6 +176,8 @@ class ReferenceResolver:
164
176
  self.file_hashes = file_hashes
165
177
  self.indexed_commit = indexed_commit
166
178
  self.workspace = workspace
179
+ self.tsconfig_resolver = tsconfig_resolver
180
+ self.repository = repository
167
181
  self.max_reexport_depth = max_reexport_depth
168
182
  self.max_wildcard_expansions = max_wildcard_expansions
169
183
 
@@ -257,7 +271,11 @@ class ReferenceResolver:
257
271
 
258
272
  def _resolve_import_target_module_uncached(self, imp: ImportRef) -> tuple[str | None, str | None]:
259
273
  target_path = resolve_module_to_path(
260
- imp.imported_module, imp.source_file, self.known_files, workspace=self.workspace
274
+ imp.imported_module,
275
+ imp.source_file,
276
+ self.known_files,
277
+ workspace=self.workspace,
278
+ tsconfig_resolver=self.tsconfig_resolver,
261
279
  )
262
280
  if target_path:
263
281
  return target_path, normalize_module(target_path)
@@ -266,7 +284,11 @@ class ReferenceResolver:
266
284
  if "." in imp.imported_module and not imp.imported_module.startswith("."):
267
285
  parent_mod, last_seg = imp.imported_module.rsplit(".", 1)
268
286
  parent_path = resolve_module_to_path(
269
- parent_mod, imp.source_file, self.known_files, workspace=self.workspace
287
+ parent_mod,
288
+ imp.source_file,
289
+ self.known_files,
290
+ workspace=self.workspace,
291
+ tsconfig_resolver=self.tsconfig_resolver,
270
292
  )
271
293
  if parent_path:
272
294
  return parent_path, normalize_module(parent_path)
@@ -605,7 +627,23 @@ class ReferenceResolver:
605
627
  seen_refs.add(key)
606
628
  references.append(ref)
607
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
+
608
643
  def add_edge(edge: GraphEdge) -> None:
644
+ fpath = str(edge.file or "")
645
+ if fpath.endswith(_NON_CODE_EXTS):
646
+ return
609
647
  key = (
610
648
  edge.source,
611
649
  edge.target,
@@ -617,8 +655,17 @@ class ReferenceResolver:
617
655
  seen_edges.add(key)
618
656
  edges.append(edge)
619
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
+
620
665
  # 0. Definition & Containment edges
621
666
  for sym in self.symbols:
667
+ if sym.path.endswith(_NON_CODE_EXTS):
668
+ continue
622
669
  f_hash = self.file_hashes.get(sym.path, "")
623
670
  if sym.parent_symbol_id:
624
671
  add_edge(
@@ -976,6 +1023,8 @@ class ReferenceResolver:
976
1023
  expr = call.qualified_callee or call.callee
977
1024
  via_factory_sym: Symbol | None = None
978
1025
  resolved_via_dataflow = False
1026
+ is_pytorch_forward = False
1027
+ is_concrete_module = False
979
1028
 
980
1029
  target_sym, conf, reason = self.resolve_identifier_in_file(
981
1030
  expr, call.source_file, caller_id, use_line=call.line
@@ -998,6 +1047,35 @@ class ReferenceResolver:
998
1047
  reason = f"{recv_reason}; inherited method on '{owner_canon}'"
999
1048
  else:
1000
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}"
1001
1079
  else:
1002
1080
  # Even if parser substituted receiver -> ClassName, check if it came from a local factory binding
1003
1081
  for b in call_return_bindings_by_file.get(call.source_file, ()):
@@ -1041,6 +1119,23 @@ class ReferenceResolver:
1041
1119
  )
1042
1120
  )
1043
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
+
1044
1139
  if target_sym:
1045
1140
  resolved_calls[(call.source_file, call.line, call.callee)] = (
1046
1141
  target_sym.canonical_id,
@@ -1078,6 +1173,40 @@ class ReferenceResolver:
1078
1173
  evidence_class=call_ev_cls,
1079
1174
  )
1080
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
+ )
1081
1210
  elif "[" in expr and "]" in expr:
1082
1211
  dict_part, raw_k = expr.split("[", 1)
1083
1212
  raw_k = raw_k.rstrip("]")
@@ -1562,6 +1691,101 @@ class ReferenceResolver:
1562
1691
  reason=edge_reason,
1563
1692
  )
1564
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
+ )
1565
1789
  else:
1566
1790
  src_id = b.source_expr
1567
1791
  ev_evidence = f"Event listener '{target_canon}' registered via '@{b.source_expr}'"
@@ -1692,164 +1916,568 @@ class ReferenceResolver:
1692
1916
  )
1693
1917
  )
1694
1918
 
1695
- # 9. Dependency Injection & Configuration Intelligence
1696
- # 9a. Statically grounded provider registration (PROVIDES)
1697
- provider_map: dict[str, list[tuple[str, BindingRef, str]]] = {}
1698
- for b in self.bindings:
1699
- if b.expr_kind == "SEMANTIC_DECORATOR" and b.attr_name == "PROVIDES":
1700
- if b.subscript_target is not None:
1701
- findings.append({
1702
- "pattern": "DI_PROVIDES",
1703
- "status": "UNKNOWN",
1704
- "reason": b.subscript_target or "dynamic_provider",
1705
- "file": b.file_path,
1706
- "line": b.line,
1707
- "target_name": b.target_name,
1708
- })
1709
- continue
1710
- dec_meta: dict[str, object] = {}
1711
- if b.subscript_key:
1712
- try:
1713
- dec_meta = json.loads(b.subscript_key)
1714
- except Exception:
1715
- dec_meta = {}
1716
- iface_raw = str(dec_meta.get("interface") or "")
1717
- impl_canon = b.target_name
1718
- iface_canon = impl_canon
1719
- if iface_raw and iface_raw != impl_canon.split(".")[-1]:
1720
- if_sym, _, _ = self.resolve_identifier_in_file(iface_raw, b.file_path, use_line=b.line)
1721
- if not if_sym and len(self.short_to_symbols.get(iface_raw, [])) == 1:
1722
- if_sym = self.short_to_symbols[iface_raw][0]
1723
- if if_sym:
1724
- iface_canon = if_sym.canonical_id
1725
- provider_map.setdefault(iface_canon, []).append((impl_canon, b, "decorator_provider"))
1726
- continue
1727
-
1728
- if b.expr_kind != "DI_PROVIDES":
1729
- continue
1730
-
1731
- if (
1732
- b.subscript_target == "dynamic_provider"
1733
- or b.target_name == "<DYNAMIC>"
1734
- or b.source_expr == "<DYNAMIC>"
1735
- ):
1736
- findings.append({
1737
- "pattern": "DI_PROVIDES",
1738
- "status": "UNKNOWN",
1739
- "reason": "dynamic_provider",
1740
- "file": b.file_path,
1741
- "line": b.line,
1742
- "target_name": b.target_name,
1743
- })
1744
- continue
1745
-
1746
- iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
1747
- if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
1748
- iface_sym = self.short_to_symbols[b.target_name][0]
1749
-
1750
- impl_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
1751
- if not impl_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
1752
- impl_sym = self.short_to_symbols[b.source_expr][0]
1753
-
1754
- if not iface_sym or not impl_sym:
1755
- findings.append({
1756
- "pattern": "DI_PROVIDES",
1757
- "status": "UNKNOWN",
1758
- "reason": "dynamic_provider",
1759
- "file": b.file_path,
1760
- "line": b.line,
1761
- "target_name": b.target_name,
1762
- "source_expr": b.source_expr,
1763
- })
1764
- continue
1765
-
1766
- iface_canon = iface_sym.canonical_id
1767
- impl_canon = impl_sym.canonical_id
1768
- # For 1-arg container.provide(Impl) / container.singleton(Impl), check if Impl extends a base interface
1769
- if iface_canon == impl_canon and b.target_name == b.source_expr:
1770
- for inh in inheritance_by_source.get(impl_canon, ()):
1771
- base_sym, _, _ = self.resolve_identifier_in_file(inh.base_name, inh.source_file, use_line=inh.line)
1772
- if not base_sym and len(self.short_to_symbols.get(inh.base_name, [])) == 1:
1773
- base_sym = self.short_to_symbols[inh.base_name][0]
1774
- if base_sym:
1775
- iface_canon = base_sym.canonical_id
1776
- break
1777
- provider_map.setdefault(iface_canon, []).append((impl_canon, b, "provider_mapping"))
1778
-
1779
- # Emit PROVIDES edges (Ambiguity: multiple distinct providers -> POSSIBLE)
1780
- for iface_canon, reg_list in provider_map.items():
1781
- distinct_impls = set(impl_c for impl_c, _, _ in reg_list)
1782
- is_ambiguous = len(distinct_impls) > 1
1783
-
1784
- for impl_canon, b, _ in reg_list:
1785
- f_hash = self.file_hashes.get(b.file_path, "")
1786
- if is_ambiguous:
1787
- ev_class = RelationshipEvidenceClass.POSSIBLE
1788
- edge_conf = "LOW"
1789
- edge_reason = "multiple_providers"
1790
- elif b.is_conditional:
1791
- ev_class = RelationshipEvidenceClass.POSSIBLE
1792
- edge_conf = "LOW"
1793
- edge_reason = "conditional_provider"
1794
- else:
1795
- ev_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
1796
- edge_conf = "HIGH"
1797
- edge_reason = f"Statically verified provider mapping '{b.target_name}' -> '{impl_canon}'"
1798
-
1799
- ev_str = f"Provider mapping: {iface_canon} PROVIDES {impl_canon} at {b.file_path}:{b.line}"
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}'"
1800
1922
  add_ref(
1801
1923
  Reference(
1802
- source_symbol_id=iface_canon,
1803
- target_symbol_id=impl_canon,
1804
- relationship="PROVIDES",
1924
+ source_symbol_id=target_canon,
1925
+ target_symbol_id=source_id,
1926
+ relationship=rel,
1805
1927
  confidence=edge_conf,
1806
1928
  path=b.file_path,
1807
1929
  start_line=b.line,
1808
1930
  end_line=b.line,
1809
- evidence=ev_str,
1931
+ evidence=ev_evidence,
1810
1932
  source_hash=f_hash,
1811
1933
  indexed_commit=self.indexed_commit,
1812
1934
  )
1813
1935
  )
1814
1936
  add_edge(
1815
1937
  GraphEdge(
1816
- source=iface_canon,
1817
- target=impl_canon,
1818
- relationship="PROVIDES",
1938
+ source=target_canon,
1939
+ target=source_id,
1940
+ relationship=rel,
1819
1941
  confidence=edge_conf,
1820
1942
  file=b.file_path,
1821
1943
  start_line=b.line,
1822
1944
  end_line=b.line,
1823
- evidence=ev_str,
1945
+ evidence=ev_evidence,
1824
1946
  evidence_class=ev_class.value,
1825
1947
  reason=edge_reason,
1826
1948
  )
1827
1949
  )
1828
1950
 
1829
- # 9b. Dependency Resolution (RESOLVES_DEPENDENCY)
1830
- for b in bindings_by_kind.get("DI_RESOLVES", ()):
1831
- if b.subscript_target == "dynamic_provider" or b.target_name == "<DYNAMIC>":
1832
- findings.append({
1833
- "pattern": "DI_RESOLVES",
1834
- "status": "UNKNOWN",
1835
- "reason": "dynamic_provider",
1836
- "file": b.file_path,
1837
- "line": b.line,
1838
- })
1839
- continue
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]
1840
1981
 
1841
- iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
1842
- if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
1843
- iface_sym = self.short_to_symbols[b.target_name][0]
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
+ )
1844
2041
 
1845
- if not iface_sym:
1846
- findings.append({
1847
- "pattern": "DI_RESOLVES",
1848
- "status": "UNKNOWN",
1849
- "reason": "unknown_dependency_interface",
1850
- "file": b.file_path,
1851
- "line": b.line,
1852
- "target_name": b.target_name,
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,
1853
2481
  })
1854
2482
  continue
1855
2483
 
@@ -1921,6 +2549,130 @@ class ReferenceResolver:
1921
2549
  )
1922
2550
 
1923
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
1924
2676
  for b in (
1925
2677
  bindings_by_kind.get("ROUTER_DI_INJECTS", [])
1926
2678
  + bindings_by_kind.get("DI_INJECTS", [])
@@ -2092,10 +2844,14 @@ class ReferenceResolver:
2092
2844
  )
2093
2845
  )
2094
2846
 
2095
- # Also link matching route endpoint_id -> dep_canon
2847
+ # Also link matching route endpoint_id -> dep_canon (scoped to this file or exact canonical ID)
2096
2848
  handler_short = handler_canon.split(".")[-1]
2097
2849
  for rt in self.routes:
2098
- if rt.handler_canonical_id == handler_canon or rt.handler_name == handler_short:
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:
2099
2855
  rt_ev = f"Route endpoint '{rt.endpoint_id}' INJECTS '{dep_canon}' at {rt.file_path}:{rt.line}"
2100
2856
  add_ref(
2101
2857
  Reference(
@@ -2126,32 +2882,82 @@ class ReferenceResolver:
2126
2882
  )
2127
2883
  )
2128
2884
 
2129
- # 9d. Constructor & @inject Type-Based Injection (INJECTS)
2130
- for b in bindings_by_kind.get("CONSTRUCTOR_PARAM", ()):
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", ()):
2131
2938
  ann_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2132
2939
  if not ann_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2133
2940
  ann_sym = self.short_to_symbols[b.source_expr][0]
2134
2941
 
2135
- if not ann_sym:
2136
- continue
2137
-
2138
- ann_canon = ann_sym.canonical_id
2139
- # Rule: Only create injection relationship when supported DI semantics exist!
2140
- # i.e., provider mapping exists for this interface OR @inject decorator was present
2141
- has_provider = ann_canon in provider_map
2142
- is_inject_dec = (b.base_expr == "inject_decorator")
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}"
2143
2949
 
2144
- if not (has_provider or is_inject_dec):
2950
+ if not injected_canon:
2145
2951
  continue
2146
2952
 
2147
2953
  f_hash = self.file_hashes.get(b.file_path, "")
2148
2954
  caller_canon = b.target_name
2149
- ev_str = f"Constructor injection: '{caller_canon}' INJECTS '{ann_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
2955
+ ev_str = f"NestJS injection: '{caller_canon}' INJECTS '{injected_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
2150
2956
 
2151
2957
  add_ref(
2152
2958
  Reference(
2153
2959
  source_symbol_id=caller_canon,
2154
- target_symbol_id=ann_canon,
2960
+ target_symbol_id=injected_canon,
2155
2961
  relationship="INJECTS",
2156
2962
  confidence="HIGH",
2157
2963
  path=b.file_path,
@@ -2165,18 +2971,59 @@ class ReferenceResolver:
2165
2971
  add_edge(
2166
2972
  GraphEdge(
2167
2973
  source=caller_canon,
2168
- target=ann_canon,
2974
+ target=injected_canon,
2169
2975
  relationship="INJECTS",
2170
2976
  confidence="HIGH",
2171
2977
  file=b.file_path,
2172
2978
  start_line=b.line,
2173
2979
  end_line=b.line,
2174
2980
  evidence=ev_str,
2175
- evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2176
- reason="constructor_injection",
2981
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2982
+ reason="nestjs_injection",
2177
2983
  )
2178
2984
  )
2179
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
+
2180
3027
  # 9d-cycle. Static DI Cycle Detection (INJECTS / PROVIDES / RESOLVES_DEPENDENCY)
2181
3028
  di_adj: dict[str, list[tuple[str, str, int]]] = {}
2182
3029
  for ed in list(edges):
@@ -2291,12 +3138,6 @@ class ReferenceResolver:
2291
3138
  )
2292
3139
 
2293
3140
  # 10. Test Intelligence & Deterministic Test Discovery
2294
- def _find_caller_id_for_binding(b: BindingRef) -> str:
2295
- for s in self.path_to_symbols.get(b.file_path, ()):
2296
- if s.start_line <= b.line <= s.end_line:
2297
- return s.canonical_id
2298
- mod = self.file_to_module.get(b.file_path) or normalize_module(b.file_path)
2299
- return f"{mod}.{b.scope}" if b.scope else mod
2300
3141
 
2301
3142
  # 10a. Direct Test Calls (TESTS)
2302
3143
  # Any call originating from a test file to a production symbol
@@ -2665,6 +3506,361 @@ class ReferenceResolver:
2665
3506
  )
2666
3507
  )
2667
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
+
2668
3864
  return ResolutionOutput(
2669
3865
  references=references,
2670
3866
  edges=edges,