codegraph-engine 2.2.1__py3-none-any.whl → 3.0.0__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 (82) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/agent_brain.py +172 -2
  3. codegraph/agent_capabilities.py +564 -6
  4. codegraph/agent_rules.py +1 -1
  5. codegraph/api_drift.py +455 -0
  6. codegraph/async_queue/__init__.py +50 -0
  7. codegraph/async_queue/detectors/__init__.py +48 -0
  8. codegraph/async_queue/detectors/celery.py +160 -0
  9. codegraph/async_queue/detectors/kafka.py +168 -0
  10. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  11. codegraph/async_queue/detectors/redis.py +130 -0
  12. codegraph/async_queue/detectors/sqs.py +135 -0
  13. codegraph/async_queue/models.py +161 -0
  14. codegraph/async_queue/otel_models.py +74 -0
  15. codegraph/async_queue/otel_parser.py +337 -0
  16. codegraph/async_queue/reconciler.py +427 -0
  17. codegraph/async_queue/scanner.py +291 -0
  18. codegraph/async_queue/traversal.py +447 -0
  19. codegraph/branch_comparison.py +197 -0
  20. codegraph/change_impact.py +547 -0
  21. codegraph/cli.py +992 -3
  22. codegraph/cli_output.py +17 -2
  23. codegraph/context.py +38 -11
  24. codegraph/context_freshness.py +351 -0
  25. codegraph/database/__init__.py +14 -0
  26. codegraph/database/extractor.py +712 -17
  27. codegraph/database/interrogation.py +388 -2
  28. codegraph/database/schema_drift.py +862 -0
  29. codegraph/epistemic.py +7 -0
  30. codegraph/evidence_contract.py +21 -0
  31. codegraph/frameworks.py +171 -6
  32. codegraph/frameworks_nestjs.py +502 -0
  33. codegraph/frameworks_nextjs.py +337 -0
  34. codegraph/frameworks_react.py +457 -0
  35. codegraph/freshness.py +2 -0
  36. codegraph/frontend_html.py +109 -0
  37. codegraph/git.py +2 -0
  38. codegraph/git_state.py +545 -0
  39. codegraph/indexing/__init__.py +7 -1
  40. codegraph/indexing/indexer.py +291 -38
  41. codegraph/indexing/models.py +6 -0
  42. codegraph/indexing/parser.py +239 -0
  43. codegraph/indexing/pool.py +136 -0
  44. codegraph/indexing/scanner.py +6 -0
  45. codegraph/indexing/templates.py +418 -0
  46. codegraph/interrogation.py +128 -20
  47. codegraph/mcp/server.py +258 -27
  48. codegraph/models.py +7 -0
  49. codegraph/monorepo.py +3 -0
  50. codegraph/optimizer.py +42 -5
  51. codegraph/process_lifecycle.py +31 -13
  52. codegraph/refactor/__init__.py +36 -0
  53. codegraph/refactor/models.py +114 -0
  54. codegraph/refactor/renamer.py +844 -0
  55. codegraph/refactor/transactions.py +403 -0
  56. codegraph/resolver.py +1202 -159
  57. codegraph/retrieval_policy.py +6 -0
  58. codegraph/route_topology.py +172 -0
  59. codegraph/runtime/__init__.py +19 -0
  60. codegraph/runtime/collector.py +155 -0
  61. codegraph/runtime/db_watcher.py +191 -0
  62. codegraph/runtime/ingestor.py +30 -10
  63. codegraph/runtime/port_inspector.py +304 -0
  64. codegraph/runtime/runner.py +155 -6
  65. codegraph/runtime/trace_linker.py +175 -0
  66. codegraph/search/hybrid.py +26 -0
  67. codegraph/semantic_conflicts.py +270 -0
  68. codegraph/semantic_decorators.py +34 -3
  69. codegraph/structural_diff.py +542 -0
  70. codegraph/styles.py +144 -0
  71. codegraph/symbol_history.py +277 -0
  72. codegraph/tsconfig.py +236 -0
  73. codegraph/ui/__init__.py +6 -0
  74. codegraph/ui/server.py +715 -0
  75. codegraph/watcher.py +323 -0
  76. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +257 -39
  77. codegraph_engine-3.0.0.dist-info/RECORD +129 -0
  78. codegraph_engine-2.2.1.dist-info/RECORD +0 -87
  79. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
  80. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
  81. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
  82. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
codegraph/resolver.py CHANGED
@@ -36,6 +36,8 @@ from codegraph.indexing.models import (
36
36
  )
37
37
  from codegraph.monorepo import WorkspaceInfo
38
38
  from codegraph.route_composer import ComposedMountEdge
39
+ from codegraph.route_topology import match_route_topology, normalize_route
40
+ from codegraph.tsconfig import TsConfigResolver
39
41
 
40
42
  _TEST_FILE_RE = re.compile(
41
43
  r"(^|[_/])test[_s]?[_/]|[_/]spec[_/]|test[_s]?\.(py|js|ts|jsx|tsx)$|spec\.(py|js|ts|jsx|tsx)$",
@@ -51,6 +53,7 @@ def resolve_module_to_path(
51
53
  source_file: str,
52
54
  known_files: set[str],
53
55
  workspace: WorkspaceInfo | None = None,
56
+ tsconfig_resolver: TsConfigResolver | None = None,
54
57
  ) -> str | None:
55
58
  """Deterministically resolve an import specifier to a repository-relative file path.
56
59
 
@@ -74,6 +77,12 @@ def resolve_module_to_path(
74
77
  if f"{clean_entry}/__init__{ext}" in known_files:
75
78
  return f"{clean_entry}/__init__{ext}"
76
79
 
80
+ # 0b. Check tsconfig.json / jsconfig.json path aliases
81
+ if tsconfig_resolver is not None:
82
+ alias_target = tsconfig_resolver.resolve_alias(imported_module, source_file, known_files)
83
+ if alias_target:
84
+ return alias_target
85
+
77
86
  src_clean = source_file.replace("\\", "/").lstrip("./")
78
87
  src_dir = posixpath.dirname(src_clean)
79
88
 
@@ -150,6 +159,7 @@ class ReferenceResolver:
150
159
  mount_edges: list[ComposedMountEdge] | None = None,
151
160
  bindings: list[BindingRef] | None = None,
152
161
  workspace: WorkspaceInfo | None = None,
162
+ tsconfig_resolver: TsConfigResolver | None = None,
153
163
  max_reexport_depth: int = 16,
154
164
  max_wildcard_expansions: int = 64,
155
165
  ) -> None:
@@ -164,6 +174,7 @@ class ReferenceResolver:
164
174
  self.file_hashes = file_hashes
165
175
  self.indexed_commit = indexed_commit
166
176
  self.workspace = workspace
177
+ self.tsconfig_resolver = tsconfig_resolver
167
178
  self.max_reexport_depth = max_reexport_depth
168
179
  self.max_wildcard_expansions = max_wildcard_expansions
169
180
 
@@ -257,7 +268,11 @@ class ReferenceResolver:
257
268
 
258
269
  def _resolve_import_target_module_uncached(self, imp: ImportRef) -> tuple[str | None, str | None]:
259
270
  target_path = resolve_module_to_path(
260
- imp.imported_module, imp.source_file, self.known_files, workspace=self.workspace
271
+ imp.imported_module,
272
+ imp.source_file,
273
+ self.known_files,
274
+ workspace=self.workspace,
275
+ tsconfig_resolver=self.tsconfig_resolver,
261
276
  )
262
277
  if target_path:
263
278
  return target_path, normalize_module(target_path)
@@ -266,7 +281,11 @@ class ReferenceResolver:
266
281
  if "." in imp.imported_module and not imp.imported_module.startswith("."):
267
282
  parent_mod, last_seg = imp.imported_module.rsplit(".", 1)
268
283
  parent_path = resolve_module_to_path(
269
- parent_mod, imp.source_file, self.known_files, workspace=self.workspace
284
+ parent_mod,
285
+ imp.source_file,
286
+ self.known_files,
287
+ workspace=self.workspace,
288
+ tsconfig_resolver=self.tsconfig_resolver,
270
289
  )
271
290
  if parent_path:
272
291
  return parent_path, normalize_module(parent_path)
@@ -605,7 +624,23 @@ class ReferenceResolver:
605
624
  seen_refs.add(key)
606
625
  references.append(ref)
607
626
 
627
+ _NON_CODE_EXTS = (
628
+ ".html",
629
+ ".htm",
630
+ ".jinja",
631
+ ".jinja2",
632
+ ".css",
633
+ ".yaml",
634
+ ".yml",
635
+ ".json",
636
+ ".md",
637
+ ".markdown",
638
+ )
639
+
608
640
  def add_edge(edge: GraphEdge) -> None:
641
+ fpath = str(edge.file or "")
642
+ if fpath.endswith(_NON_CODE_EXTS):
643
+ return
609
644
  key = (
610
645
  edge.source,
611
646
  edge.target,
@@ -617,8 +652,17 @@ class ReferenceResolver:
617
652
  seen_edges.add(key)
618
653
  edges.append(edge)
619
654
 
655
+ def _find_caller_id_for_binding(b: BindingRef) -> str:
656
+ for s in self.path_to_symbols.get(b.file_path, ()):
657
+ if s.start_line <= b.line <= s.end_line:
658
+ return s.canonical_id
659
+ mod = self.file_to_module.get(b.file_path) or normalize_module(b.file_path)
660
+ return f"{mod}.{b.scope}" if b.scope else mod
661
+
620
662
  # 0. Definition & Containment edges
621
663
  for sym in self.symbols:
664
+ if sym.path.endswith(_NON_CODE_EXTS):
665
+ continue
622
666
  f_hash = self.file_hashes.get(sym.path, "")
623
667
  if sym.parent_symbol_id:
624
668
  add_edge(
@@ -976,6 +1020,8 @@ class ReferenceResolver:
976
1020
  expr = call.qualified_callee or call.callee
977
1021
  via_factory_sym: Symbol | None = None
978
1022
  resolved_via_dataflow = False
1023
+ is_pytorch_forward = False
1024
+ is_concrete_module = False
979
1025
 
980
1026
  target_sym, conf, reason = self.resolve_identifier_in_file(
981
1027
  expr, call.source_file, caller_id, use_line=call.line
@@ -998,6 +1044,35 @@ class ReferenceResolver:
998
1044
  reason = f"{recv_reason}; inherited method on '{owner_canon}'"
999
1045
  else:
1000
1046
  reason = recv_reason
1047
+ # If not a method, check if expr or self.<attr> is an nn.Module submodule invocation (e.g. self.encoder(x))
1048
+ if not target_sym:
1049
+ submod_cls_sym, _, submod_reason = _resolve_receiver_class(
1050
+ expr, call.source_file, caller_id, caller_qname, call.line
1051
+ )
1052
+ if not submod_cls_sym:
1053
+ for b in self.bindings:
1054
+ if b.file_path == call.source_file and b.target_name == f"self.{method_part}":
1055
+ if b.expr_kind == "CALL_RETURN" and b.source_expr:
1056
+ s_cls, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, caller_id, use_line=b.line)
1057
+ if s_cls and s_cls.kind == "class":
1058
+ submod_cls_sym = s_cls
1059
+ submod_reason = f"submodule assignment 'self.{method_part} = {b.source_expr}()'"
1060
+ break
1061
+ elif b.expr_kind == "TYPE_ANNOTATION" and b.source_expr:
1062
+ s_cls, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, caller_id, use_line=b.line)
1063
+ if s_cls and s_cls.kind == "class":
1064
+ submod_cls_sym = s_cls
1065
+ submod_reason = f"submodule annotation 'self.{method_part}: {b.source_expr}'"
1066
+ break
1067
+ if submod_cls_sym:
1068
+ fwd_sym, _ = _lookup_method_on_class(submod_cls_sym.canonical_id, "forward")
1069
+ if fwd_sym:
1070
+ target_sym = fwd_sym
1071
+ resolved_via_dataflow = True
1072
+ is_pytorch_forward = True
1073
+ is_concrete_module = "Constructed instance" in submod_reason or "factory" in submod_reason or "submodule assignment" in submod_reason
1074
+ conf = "HIGH" if is_concrete_module else "MEDIUM"
1075
+ reason = f"PyTorch submodule forward dispatch via {submod_reason}"
1001
1076
  else:
1002
1077
  # Even if parser substituted receiver -> ClassName, check if it came from a local factory binding
1003
1078
  for b in call_return_bindings_by_file.get(call.source_file, ()):
@@ -1041,6 +1116,23 @@ class ReferenceResolver:
1041
1116
  )
1042
1117
  )
1043
1118
 
1119
+ else:
1120
+ # expr has no dot (e.g. model(x))
1121
+ # Only dispatch to forward if target_sym is NOT already a class (constructor call Classifier() is __init__)
1122
+ if not target_sym or target_sym.kind not in ("function", "method", "class"):
1123
+ mod_cls_sym, _, mod_reason = _resolve_receiver_class(
1124
+ expr, call.source_file, caller_id, caller_qname, call.line
1125
+ )
1126
+ if mod_cls_sym and mod_cls_sym.kind == "class":
1127
+ fwd_sym, _ = _lookup_method_on_class(mod_cls_sym.canonical_id, "forward")
1128
+ if fwd_sym:
1129
+ target_sym = fwd_sym
1130
+ resolved_via_dataflow = True
1131
+ is_pytorch_forward = True
1132
+ is_concrete_module = "Constructed instance" in mod_reason or "factory" in mod_reason
1133
+ conf = "HIGH" if is_concrete_module else "MEDIUM"
1134
+ reason = f"PyTorch module forward dispatch via {mod_reason}"
1135
+
1044
1136
  if target_sym:
1045
1137
  resolved_calls[(call.source_file, call.line, call.callee)] = (
1046
1138
  target_sym.canonical_id,
@@ -1078,6 +1170,40 @@ class ReferenceResolver:
1078
1170
  evidence_class=call_ev_cls,
1079
1171
  )
1080
1172
  )
1173
+ if is_pytorch_forward:
1174
+ fwd_ev_cls = (
1175
+ RelationshipEvidenceClass.AST_VERIFIED.value
1176
+ if is_concrete_module
1177
+ else RelationshipEvidenceClass.POSSIBLE.value
1178
+ )
1179
+ add_ref(
1180
+ Reference(
1181
+ source_symbol_id=caller_id,
1182
+ target_symbol_id=target_sym.canonical_id,
1183
+ relationship="DISPATCHES_FORWARD",
1184
+ confidence=conf,
1185
+ path=call.source_file,
1186
+ start_line=call.line,
1187
+ end_line=call.end_line,
1188
+ evidence=f"Module invocation '{expr}()' at {call.source_file}:{call.line} dispatches to {target_sym.canonical_id} ({reason})",
1189
+ source_hash=f_hash,
1190
+ indexed_commit=self.indexed_commit,
1191
+ )
1192
+ )
1193
+ add_edge(
1194
+ GraphEdge(
1195
+ source=caller_id,
1196
+ target=target_sym.canonical_id,
1197
+ relationship="DISPATCHES_FORWARD",
1198
+ confidence=conf,
1199
+ file=call.source_file,
1200
+ start_line=call.line,
1201
+ end_line=call.end_line,
1202
+ evidence=f"Resolved PyTorch forward dispatch {caller_id} -> {target_sym.canonical_id} at {call.source_file}:{call.line} ({reason})",
1203
+ evidence_class=fwd_ev_cls,
1204
+ reason="pytorch_forward_dispatch",
1205
+ )
1206
+ )
1081
1207
  elif "[" in expr and "]" in expr:
1082
1208
  dict_part, raw_k = expr.split("[", 1)
1083
1209
  raw_k = raw_k.rstrip("]")
@@ -1562,6 +1688,101 @@ class ReferenceResolver:
1562
1688
  reason=edge_reason,
1563
1689
  )
1564
1690
  )
1691
+ # Django signal sender linking: @receiver(signal, sender=Model)
1692
+ sender_val = metadata.get("sender")
1693
+ if sender_val:
1694
+ sender_str = str(sender_val)
1695
+ sender_sym, _, _ = self.resolve_identifier_in_file(
1696
+ sender_str, b.file_path, use_line=b.line
1697
+ )
1698
+ if not sender_sym and "." in sender_str:
1699
+ sender_sym, _, _ = self.resolve_identifier_in_file(
1700
+ sender_str.split(".")[-1], b.file_path, use_line=b.line
1701
+ )
1702
+ if not sender_sym:
1703
+ short_target = sender_str.split(".")[-1]
1704
+ if short_target in self.short_to_symbols:
1705
+ for s in self.short_to_symbols[short_target]:
1706
+ if s.kind == "class":
1707
+ sender_sym = s
1708
+ break
1709
+ if not sender_sym:
1710
+ sender_sym = self.short_to_symbols[short_target][0]
1711
+
1712
+ sender_canon = sender_sym.canonical_id if sender_sym else sender_str
1713
+ sig_evidence = f"Django signal '{ev_str}' from sender '{sender_str}' triggers listener '{target_canon}' via '@{b.source_expr}'"
1714
+ add_ref(
1715
+ Reference(
1716
+ source_symbol_id=sender_canon,
1717
+ target_symbol_id=target_canon,
1718
+ relationship="TRIGGERS_SIGNAL",
1719
+ confidence=edge_conf,
1720
+ path=b.file_path,
1721
+ start_line=b.line,
1722
+ end_line=b.line,
1723
+ evidence=sig_evidence,
1724
+ source_hash=f_hash,
1725
+ indexed_commit=self.indexed_commit,
1726
+ )
1727
+ )
1728
+ add_edge(
1729
+ GraphEdge(
1730
+ source=sender_canon,
1731
+ target=target_canon,
1732
+ relationship="TRIGGERS_SIGNAL",
1733
+ confidence=edge_conf,
1734
+ file=b.file_path,
1735
+ start_line=b.line,
1736
+ end_line=b.line,
1737
+ evidence=sig_evidence,
1738
+ evidence_class=ev_class.value,
1739
+ reason=f"django_signal:{ev_str}",
1740
+ )
1741
+ )
1742
+ lifecycle_method = "delete" if "delete" in ev_str.lower() else "save"
1743
+ save_source = f"{sender_canon}.{lifecycle_method}"
1744
+ add_edge(
1745
+ GraphEdge(
1746
+ source=save_source,
1747
+ target=target_canon,
1748
+ relationship="TRIGGERS_SIGNAL",
1749
+ confidence=edge_conf,
1750
+ file=b.file_path,
1751
+ start_line=b.line,
1752
+ end_line=b.line,
1753
+ evidence=sig_evidence,
1754
+ evidence_class=ev_class.value,
1755
+ reason=f"django_signal:{ev_str}",
1756
+ )
1757
+ )
1758
+ add_edge(
1759
+ GraphEdge(
1760
+ source=save_source,
1761
+ target=target_canon,
1762
+ relationship="CALLS",
1763
+ confidence=edge_conf,
1764
+ file=b.file_path,
1765
+ start_line=b.line,
1766
+ end_line=b.line,
1767
+ evidence=sig_evidence,
1768
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
1769
+ reason=f"django_signal:{ev_str}",
1770
+ )
1771
+ )
1772
+ add_edge(
1773
+ GraphEdge(
1774
+ source=target_canon,
1775
+ target=sender_canon,
1776
+ relationship="HANDLES_SIGNAL",
1777
+ confidence=edge_conf,
1778
+ file=b.file_path,
1779
+ start_line=b.line,
1780
+ end_line=b.line,
1781
+ evidence=f"Listener '{target_canon}' handles '{ev_str}' from '{sender_canon}'",
1782
+ evidence_class=ev_class.value,
1783
+ reason=f"django_signal:{ev_str}",
1784
+ )
1785
+ )
1565
1786
  else:
1566
1787
  src_id = b.source_expr
1567
1788
  ev_evidence = f"Event listener '{target_canon}' registered via '@{b.source_expr}'"
@@ -1692,164 +1913,568 @@ class ReferenceResolver:
1692
1913
  )
1693
1914
  )
1694
1915
 
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}"
1916
+ elif rel == "TOOL_HANDLER":
1917
+ source_id = f"@{b.source_expr}"
1918
+ ev_evidence = f"AI agent tool '{target_canon}' registered via '@{b.source_expr}'"
1800
1919
  add_ref(
1801
1920
  Reference(
1802
- source_symbol_id=iface_canon,
1803
- target_symbol_id=impl_canon,
1804
- relationship="PROVIDES",
1921
+ source_symbol_id=target_canon,
1922
+ target_symbol_id=source_id,
1923
+ relationship=rel,
1805
1924
  confidence=edge_conf,
1806
1925
  path=b.file_path,
1807
1926
  start_line=b.line,
1808
1927
  end_line=b.line,
1809
- evidence=ev_str,
1928
+ evidence=ev_evidence,
1810
1929
  source_hash=f_hash,
1811
1930
  indexed_commit=self.indexed_commit,
1812
1931
  )
1813
1932
  )
1814
1933
  add_edge(
1815
1934
  GraphEdge(
1816
- source=iface_canon,
1817
- target=impl_canon,
1818
- relationship="PROVIDES",
1935
+ source=target_canon,
1936
+ target=source_id,
1937
+ relationship=rel,
1819
1938
  confidence=edge_conf,
1820
1939
  file=b.file_path,
1821
1940
  start_line=b.line,
1822
1941
  end_line=b.line,
1823
- evidence=ev_str,
1942
+ evidence=ev_evidence,
1824
1943
  evidence_class=ev_class.value,
1825
1944
  reason=edge_reason,
1826
1945
  )
1827
1946
  )
1828
1947
 
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
1948
+ # 8b. Django Signal Receivers via connect()
1949
+ for b in bindings_by_kind.get("DJANGO_SIGNAL_RECEIVER", ()):
1950
+ f_hash = self.file_hashes.get(b.file_path, "")
1951
+ receiver_sym, _, _ = self.resolve_identifier_in_file(
1952
+ b.target_name, b.file_path, use_line=b.line
1953
+ )
1954
+ if not receiver_sym and b.target_name in self.short_to_symbols:
1955
+ receiver_sym = self.short_to_symbols[b.target_name][0]
1956
+ target_canon = receiver_sym.canonical_id if receiver_sym else b.target_name
1957
+
1958
+ sig_name = b.base_expr
1959
+ sender_str = b.source_expr
1960
+ sender_sym = None
1961
+ if sender_str:
1962
+ sender_sym, _, _ = self.resolve_identifier_in_file(
1963
+ sender_str, b.file_path, use_line=b.line
1964
+ )
1965
+ if not sender_sym and "." in sender_str:
1966
+ sender_sym, _, _ = self.resolve_identifier_in_file(
1967
+ sender_str.split(".")[-1], b.file_path, use_line=b.line
1968
+ )
1969
+ if not sender_sym:
1970
+ short_s = sender_str.split(".")[-1]
1971
+ if short_s in self.short_to_symbols:
1972
+ for s in self.short_to_symbols[short_s]:
1973
+ if s.kind == "class":
1974
+ sender_sym = s
1975
+ break
1976
+ if not sender_sym:
1977
+ sender_sym = self.short_to_symbols[short_s][0]
1840
1978
 
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]
1979
+ sender_canon = sender_sym.canonical_id if sender_sym else (sender_str or sig_name)
1980
+ sig_ev = f"Signal '{sig_name}' connects sender '{sender_str or '*'}' to '{target_canon}'"
1981
+ add_ref(
1982
+ Reference(
1983
+ source_symbol_id=sender_canon,
1984
+ target_symbol_id=target_canon,
1985
+ relationship="TRIGGERS_SIGNAL",
1986
+ confidence="HIGH" if sender_sym else "MEDIUM",
1987
+ path=b.file_path,
1988
+ start_line=b.line,
1989
+ end_line=b.line,
1990
+ evidence=sig_ev,
1991
+ source_hash=f_hash,
1992
+ indexed_commit=self.indexed_commit,
1993
+ )
1994
+ )
1995
+ add_edge(
1996
+ GraphEdge(
1997
+ source=sender_canon,
1998
+ target=target_canon,
1999
+ relationship="TRIGGERS_SIGNAL",
2000
+ confidence="HIGH" if sender_sym else "MEDIUM",
2001
+ file=b.file_path,
2002
+ start_line=b.line,
2003
+ end_line=b.line,
2004
+ evidence=sig_ev,
2005
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2006
+ reason=f"django_signal:{sig_name}",
2007
+ )
2008
+ )
2009
+ lifecycle_method = "delete" if "delete" in sig_name.lower() else "save"
2010
+ add_edge(
2011
+ GraphEdge(
2012
+ source=f"{sender_canon}.{lifecycle_method}",
2013
+ target=target_canon,
2014
+ relationship="CALLS",
2015
+ confidence="HIGH" if sender_sym else "MEDIUM",
2016
+ file=b.file_path,
2017
+ start_line=b.line,
2018
+ end_line=b.line,
2019
+ evidence=sig_ev,
2020
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2021
+ reason=f"django_signal:{sig_name}",
2022
+ )
2023
+ )
2024
+ add_edge(
2025
+ GraphEdge(
2026
+ source=target_canon,
2027
+ target=sender_canon,
2028
+ relationship="HANDLES_SIGNAL",
2029
+ confidence="HIGH" if sender_sym else "MEDIUM",
2030
+ file=b.file_path,
2031
+ start_line=b.line,
2032
+ end_line=b.line,
2033
+ evidence=f"Listener '{target_canon}' handles '{sig_name}' from '{sender_canon}'",
2034
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2035
+ reason=f"django_signal:{sig_name}",
2036
+ )
2037
+ )
1844
2038
 
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,
2039
+ # 8c. Django Signal Dispatches via send() / send_robust()
2040
+ for b in bindings_by_kind.get("DJANGO_SIGNAL_SEND", ()):
2041
+ caller_id = _find_caller_id_for_binding(b)
2042
+ sig_name = b.target_name
2043
+ for b_rcv in bindings_by_kind.get("DJANGO_SIGNAL_RECEIVER", ()):
2044
+ if b_rcv.base_expr == sig_name or sig_name.endswith(b_rcv.base_expr):
2045
+ rcv_sym, _, _ = self.resolve_identifier_in_file(b_rcv.target_name, b_rcv.file_path, use_line=b_rcv.line)
2046
+ if not rcv_sym and b_rcv.target_name in self.short_to_symbols:
2047
+ rcv_sym = self.short_to_symbols[b_rcv.target_name][0]
2048
+ target_id = rcv_sym.canonical_id if rcv_sym else b_rcv.target_name
2049
+ ev_str = f"Caller dispatches signal '{sig_name}' triggering '{target_id}'"
2050
+ add_edge(
2051
+ GraphEdge(
2052
+ source=caller_id,
2053
+ target=target_id,
2054
+ relationship="TRIGGERS_SIGNAL",
2055
+ confidence="HIGH",
2056
+ file=b.file_path,
2057
+ start_line=b.line,
2058
+ end_line=b.line,
2059
+ evidence=ev_str,
2060
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2061
+ reason=f"signal_send:{sig_name}",
2062
+ )
2063
+ )
2064
+
2065
+ # 8d. Celery Task Dispatches via .delay() / .apply_async() / send_task()
2066
+ for b in bindings_by_kind.get("CELERY_DISPATCH", ()):
2067
+ caller_id = _find_caller_id_for_binding(b)
2068
+ task_name = b.target_name
2069
+ task_sym, _, _ = self.resolve_identifier_in_file(task_name, b.file_path, use_line=b.line)
2070
+ if not task_sym and task_name in self.short_to_symbols:
2071
+ task_sym = self.short_to_symbols[task_name][0]
2072
+ target_id = task_sym.canonical_id if task_sym else task_name
2073
+ f_hash = self.file_hashes.get(b.file_path, "")
2074
+ ev_str = f"Caller dispatches background Celery task '{task_name}' via .{b.base_expr}() at {b.file_path}:{b.line}"
2075
+ add_ref(
2076
+ Reference(
2077
+ source_symbol_id=caller_id,
2078
+ target_symbol_id=target_id,
2079
+ relationship="DISPATCHES_TASK",
2080
+ confidence="HIGH" if task_sym else "MEDIUM",
2081
+ path=b.file_path,
2082
+ start_line=b.line,
2083
+ end_line=b.line,
2084
+ evidence=ev_str,
2085
+ source_hash=f_hash,
2086
+ indexed_commit=self.indexed_commit,
2087
+ )
2088
+ )
2089
+ add_edge(
2090
+ GraphEdge(
2091
+ source=caller_id,
2092
+ target=target_id,
2093
+ relationship="DISPATCHES_TASK",
2094
+ confidence="HIGH" if task_sym else "MEDIUM",
2095
+ file=b.file_path,
2096
+ start_line=b.line,
2097
+ end_line=b.line,
2098
+ evidence=ev_str,
2099
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2100
+ reason=f"celery_dispatch:{b.base_expr}",
2101
+ )
2102
+ )
2103
+ add_edge(
2104
+ GraphEdge(
2105
+ source=caller_id,
2106
+ target=target_id,
2107
+ relationship="CALLS",
2108
+ confidence="HIGH" if task_sym else "MEDIUM",
2109
+ file=b.file_path,
2110
+ start_line=b.line,
2111
+ end_line=b.line,
2112
+ evidence=ev_str,
2113
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2114
+ reason=f"celery_dispatch:{b.base_expr}",
2115
+ )
2116
+ )
2117
+
2118
+ # 8e. Ray Remote Task Dispatches via .remote()
2119
+ for b in bindings_by_kind.get("RAY_REMOTE", ()):
2120
+ caller_id = _find_caller_id_for_binding(b)
2121
+ task_name = b.target_name
2122
+ task_sym, _, _ = self.resolve_identifier_in_file(task_name, b.file_path, use_line=b.line)
2123
+ if not task_sym and task_name in self.short_to_symbols:
2124
+ task_sym = self.short_to_symbols[task_name][0]
2125
+ target_id = task_sym.canonical_id if task_sym else task_name
2126
+ f_hash = self.file_hashes.get(b.file_path, "")
2127
+ ev_str = f"Caller dispatches remote Ray task '{task_name}' via .{b.base_expr}() at {b.file_path}:{b.line}"
2128
+ add_ref(
2129
+ Reference(
2130
+ source_symbol_id=caller_id,
2131
+ target_symbol_id=target_id,
2132
+ relationship="DISPATCHES_TASK",
2133
+ confidence="HIGH" if task_sym else "MEDIUM",
2134
+ path=b.file_path,
2135
+ start_line=b.line,
2136
+ end_line=b.line,
2137
+ evidence=ev_str,
2138
+ source_hash=f_hash,
2139
+ indexed_commit=self.indexed_commit,
2140
+ )
2141
+ )
2142
+ add_edge(
2143
+ GraphEdge(
2144
+ source=caller_id,
2145
+ target=target_id,
2146
+ relationship="DISPATCHES_TASK",
2147
+ confidence="HIGH" if task_sym else "MEDIUM",
2148
+ file=b.file_path,
2149
+ start_line=b.line,
2150
+ end_line=b.line,
2151
+ evidence=ev_str,
2152
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2153
+ reason=f"ray_remote:{b.base_expr}",
2154
+ )
2155
+ )
2156
+ add_edge(
2157
+ GraphEdge(
2158
+ source=caller_id,
2159
+ target=target_id,
2160
+ relationship="CALLS",
2161
+ confidence="HIGH" if task_sym else "MEDIUM",
2162
+ file=b.file_path,
2163
+ start_line=b.line,
2164
+ end_line=b.line,
2165
+ evidence=ev_str,
2166
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2167
+ reason=f"ray_remote:{b.base_expr}",
2168
+ )
2169
+ )
2170
+
2171
+ # 8f. Agent Tool Registrations (e.g. AgentExecutor(tools=[...]))
2172
+ for b in bindings_by_kind.get("AGENT_TOOL_REGISTRATION", ()):
2173
+ caller_id = _find_caller_id_for_binding(b)
2174
+ tool_name = b.target_name
2175
+ tool_sym, _, _ = self.resolve_identifier_in_file(tool_name, b.file_path, use_line=b.line)
2176
+ if not tool_sym and tool_name in self.short_to_symbols:
2177
+ tool_sym = self.short_to_symbols[tool_name][0]
2178
+ target_id = tool_sym.canonical_id if tool_sym else tool_name
2179
+ f_hash = self.file_hashes.get(b.file_path, "")
2180
+ ev_str = f"Agent '{b.base_expr}' registers tool '{tool_name}' at {b.file_path}:{b.line}"
2181
+ add_ref(
2182
+ Reference(
2183
+ source_symbol_id=caller_id,
2184
+ target_symbol_id=target_id,
2185
+ relationship="REGISTERS",
2186
+ confidence="HIGH" if tool_sym else "MEDIUM",
2187
+ path=b.file_path,
2188
+ start_line=b.line,
2189
+ end_line=b.line,
2190
+ evidence=ev_str,
2191
+ source_hash=f_hash,
2192
+ indexed_commit=self.indexed_commit,
2193
+ )
2194
+ )
2195
+ add_edge(
2196
+ GraphEdge(
2197
+ source=caller_id,
2198
+ target=target_id,
2199
+ relationship="REGISTERS",
2200
+ confidence="HIGH" if tool_sym else "MEDIUM",
2201
+ file=b.file_path,
2202
+ start_line=b.line,
2203
+ end_line=b.line,
2204
+ evidence=ev_str,
2205
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2206
+ reason=f"agent_tool_registration:{b.base_expr}",
2207
+ )
2208
+ )
2209
+
2210
+ # 8g. LCEL Pipeline Chains (prompt | llm | output_parser)
2211
+ for b in bindings_by_kind.get("LCEL_PIPELINE", ()):
2212
+ caller_id = _find_caller_id_for_binding(b)
2213
+ try:
2214
+ steps: list[str] = json.loads(b.source_expr)
2215
+ except Exception:
2216
+ steps = []
2217
+ f_hash = self.file_hashes.get(b.file_path, "")
2218
+ step_syms: list[Symbol | None] = []
2219
+ for s_name in steps:
2220
+ s_sym, _, _ = self.resolve_identifier_in_file(
2221
+ s_name, b.file_path, caller_canonical_id=caller_id, use_line=b.line
2222
+ )
2223
+ if not s_sym and self.binding_resolver:
2224
+ alias_t, _, _ = self.binding_resolver.resolve(
2225
+ b.file_path, s_name, scope=b.scope, use_line=b.line
2226
+ )
2227
+ if alias_t and alias_t in self.canonical_to_symbol:
2228
+ s_sym = self.canonical_to_symbol[alias_t]
2229
+ elif alias_t:
2230
+ s_sym, _, _ = self.resolve_identifier_in_file(alias_t, b.file_path, use_line=b.line)
2231
+ if not s_sym and alias_t in self.short_to_symbols:
2232
+ s_sym = self.short_to_symbols[alias_t][0]
2233
+ if not s_sym and s_name in self.short_to_symbols:
2234
+ s_sym = self.short_to_symbols[s_name][0]
2235
+ step_syms.append(s_sym)
2236
+
2237
+ # Link consecutive pipeline steps: step[i] -> PIPELINE_STEP -> step[i+1]
2238
+ for i in range(len(steps) - 1):
2239
+ sym_from = step_syms[i]
2240
+ sym_to = step_syms[i + 1]
2241
+ from_id = sym_from.canonical_id if sym_from is not None else steps[i]
2242
+ to_id = sym_to.canonical_id if sym_to is not None else steps[i + 1]
2243
+ ev_str = f"LCEL pipeline step '{steps[i]}' -> '{steps[i+1]}' at {b.file_path}:{b.line}"
2244
+ add_ref(
2245
+ Reference(
2246
+ source_symbol_id=from_id,
2247
+ target_symbol_id=to_id,
2248
+ relationship="PIPELINE_STEP",
2249
+ confidence="HIGH" if (sym_from is not None and sym_to is not None) else "MEDIUM",
2250
+ path=b.file_path,
2251
+ start_line=b.line,
2252
+ end_line=b.line,
2253
+ evidence=ev_str,
2254
+ source_hash=f_hash,
2255
+ indexed_commit=self.indexed_commit,
2256
+ )
2257
+ )
2258
+ add_edge(
2259
+ GraphEdge(
2260
+ source=from_id,
2261
+ target=to_id,
2262
+ relationship="PIPELINE_STEP",
2263
+ confidence="HIGH" if (step_syms[i] and step_syms[i+1]) else "MEDIUM",
2264
+ file=b.file_path,
2265
+ start_line=b.line,
2266
+ end_line=b.line,
2267
+ evidence=ev_str,
2268
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2269
+ reason="lcel_pipe",
2270
+ )
2271
+ )
2272
+
2273
+ # 8h. Python View -> Template Rendering (Flask, Django, FastAPI)
2274
+ for b in bindings_by_kind.get("RENDERS_TEMPLATE", ()):
2275
+ caller_id = _find_caller_id_for_binding(b)
2276
+ raw_tmpl = b.target_name
2277
+ resolved_tmpl = raw_tmpl
2278
+ if raw_tmpl not in self.known_files:
2279
+ for kf in self.known_files:
2280
+ if kf.endswith(f"/{raw_tmpl}") or kf == raw_tmpl:
2281
+ resolved_tmpl = kf
2282
+ break
2283
+ f_hash = self.file_hashes.get(b.file_path, "")
2284
+ ev_str = f"View renders HTML template '{raw_tmpl}' at {b.file_path}:{b.line}"
2285
+ add_ref(
2286
+ Reference(
2287
+ source_symbol_id=caller_id,
2288
+ target_symbol_id=resolved_tmpl,
2289
+ relationship="RENDERS_TEMPLATE",
2290
+ confidence="HIGH",
2291
+ path=b.file_path,
2292
+ start_line=b.line,
2293
+ end_line=b.line,
2294
+ evidence=ev_str,
2295
+ source_hash=f_hash,
2296
+ indexed_commit=self.indexed_commit,
2297
+ )
2298
+ )
2299
+ add_edge(
2300
+ GraphEdge(
2301
+ source=caller_id,
2302
+ target=resolved_tmpl,
2303
+ relationship="RENDERS_TEMPLATE",
2304
+ confidence="HIGH",
2305
+ file=b.file_path,
2306
+ start_line=b.line,
2307
+ end_line=b.line,
2308
+ evidence=ev_str,
2309
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2310
+ reason=f"template_render:{raw_tmpl}",
2311
+ )
2312
+ )
2313
+
2314
+ # 9. Dependency Injection & Configuration Intelligence
2315
+ # 9a. Statically grounded provider registration (PROVIDES)
2316
+ provider_map: dict[str, list[tuple[str, BindingRef, str]]] = {}
2317
+ for b in self.bindings:
2318
+ if b.expr_kind == "SEMANTIC_DECORATOR" and b.attr_name == "PROVIDES":
2319
+ if b.subscript_target is not None:
2320
+ findings.append({
2321
+ "pattern": "DI_PROVIDES",
2322
+ "status": "UNKNOWN",
2323
+ "reason": b.subscript_target or "dynamic_provider",
2324
+ "file": b.file_path,
2325
+ "line": b.line,
2326
+ "target_name": b.target_name,
2327
+ })
2328
+ continue
2329
+ dec_meta: dict[str, object] = {}
2330
+ if b.subscript_key:
2331
+ try:
2332
+ dec_meta = json.loads(b.subscript_key)
2333
+ except Exception:
2334
+ dec_meta = {}
2335
+ iface_raw = str(dec_meta.get("interface") or "")
2336
+ impl_canon = b.target_name
2337
+ iface_canon = impl_canon
2338
+ if iface_raw and iface_raw != impl_canon.split(".")[-1]:
2339
+ if_sym, _, _ = self.resolve_identifier_in_file(iface_raw, b.file_path, use_line=b.line)
2340
+ if not if_sym and len(self.short_to_symbols.get(iface_raw, [])) == 1:
2341
+ if_sym = self.short_to_symbols[iface_raw][0]
2342
+ if if_sym:
2343
+ iface_canon = if_sym.canonical_id
2344
+ provider_map.setdefault(iface_canon, []).append((impl_canon, b, "decorator_provider"))
2345
+ continue
2346
+
2347
+ if b.expr_kind != "DI_PROVIDES":
2348
+ continue
2349
+
2350
+ if (
2351
+ b.subscript_target == "dynamic_provider"
2352
+ or b.target_name == "<DYNAMIC>"
2353
+ or b.source_expr == "<DYNAMIC>"
2354
+ ):
2355
+ findings.append({
2356
+ "pattern": "DI_PROVIDES",
2357
+ "status": "UNKNOWN",
2358
+ "reason": "dynamic_provider",
2359
+ "file": b.file_path,
2360
+ "line": b.line,
2361
+ "target_name": b.target_name,
2362
+ })
2363
+ continue
2364
+
2365
+ iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
2366
+ if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
2367
+ iface_sym = self.short_to_symbols[b.target_name][0]
2368
+
2369
+ impl_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2370
+ if not impl_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2371
+ impl_sym = self.short_to_symbols[b.source_expr][0]
2372
+
2373
+ if not iface_sym and b.base_expr == "nestjs_provider" and impl_sym:
2374
+ iface_canon = b.target_name
2375
+ impl_canon = impl_sym.canonical_id
2376
+ provider_map.setdefault(iface_canon, []).append((impl_canon, b, "nestjs_provider"))
2377
+ continue
2378
+
2379
+ if not iface_sym or not impl_sym:
2380
+ findings.append({
2381
+ "pattern": "DI_PROVIDES",
2382
+ "status": "UNKNOWN",
2383
+ "reason": "dynamic_provider",
2384
+ "file": b.file_path,
2385
+ "line": b.line,
2386
+ "target_name": b.target_name,
2387
+ "source_expr": b.source_expr,
2388
+ })
2389
+ continue
2390
+
2391
+ iface_canon = iface_sym.canonical_id
2392
+ impl_canon = impl_sym.canonical_id
2393
+ # For 1-arg container.provide(Impl) / container.singleton(Impl), check if Impl extends a base interface
2394
+ if iface_canon == impl_canon and b.target_name == b.source_expr:
2395
+ for inh in inheritance_by_source.get(impl_canon, ()):
2396
+ base_sym, _, _ = self.resolve_identifier_in_file(inh.base_name, inh.source_file, use_line=inh.line)
2397
+ if not base_sym and len(self.short_to_symbols.get(inh.base_name, [])) == 1:
2398
+ base_sym = self.short_to_symbols[inh.base_name][0]
2399
+ if base_sym:
2400
+ iface_canon = base_sym.canonical_id
2401
+ break
2402
+ provider_map.setdefault(iface_canon, []).append((impl_canon, b, "provider_mapping"))
2403
+
2404
+ # Emit PROVIDES edges (Ambiguity: multiple distinct providers -> POSSIBLE)
2405
+ for iface_canon, reg_list in provider_map.items():
2406
+ distinct_impls = set(impl_c for impl_c, _, _ in reg_list)
2407
+ is_ambiguous = len(distinct_impls) > 1
2408
+
2409
+ for impl_canon, b, _ in reg_list:
2410
+ f_hash = self.file_hashes.get(b.file_path, "")
2411
+ if is_ambiguous:
2412
+ ev_class = RelationshipEvidenceClass.POSSIBLE
2413
+ edge_conf = "LOW"
2414
+ edge_reason = "multiple_providers"
2415
+ elif b.is_conditional:
2416
+ ev_class = RelationshipEvidenceClass.POSSIBLE
2417
+ edge_conf = "LOW"
2418
+ edge_reason = "conditional_provider"
2419
+ else:
2420
+ ev_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
2421
+ edge_conf = "HIGH"
2422
+ edge_reason = f"Statically verified provider mapping '{b.target_name}' -> '{impl_canon}'"
2423
+
2424
+ ev_str = f"Provider mapping: {iface_canon} PROVIDES {impl_canon} at {b.file_path}:{b.line}"
2425
+ add_ref(
2426
+ Reference(
2427
+ source_symbol_id=iface_canon,
2428
+ target_symbol_id=impl_canon,
2429
+ relationship="PROVIDES",
2430
+ confidence=edge_conf,
2431
+ path=b.file_path,
2432
+ start_line=b.line,
2433
+ end_line=b.line,
2434
+ evidence=ev_str,
2435
+ source_hash=f_hash,
2436
+ indexed_commit=self.indexed_commit,
2437
+ )
2438
+ )
2439
+ add_edge(
2440
+ GraphEdge(
2441
+ source=iface_canon,
2442
+ target=impl_canon,
2443
+ relationship="PROVIDES",
2444
+ confidence=edge_conf,
2445
+ file=b.file_path,
2446
+ start_line=b.line,
2447
+ end_line=b.line,
2448
+ evidence=ev_str,
2449
+ evidence_class=ev_class.value,
2450
+ reason=edge_reason,
2451
+ )
2452
+ )
2453
+
2454
+ # 9b. Dependency Resolution (RESOLVES_DEPENDENCY)
2455
+ for b in bindings_by_kind.get("DI_RESOLVES", ()):
2456
+ if b.subscript_target == "dynamic_provider" or b.target_name == "<DYNAMIC>":
2457
+ findings.append({
2458
+ "pattern": "DI_RESOLVES",
2459
+ "status": "UNKNOWN",
2460
+ "reason": "dynamic_provider",
2461
+ "file": b.file_path,
2462
+ "line": b.line,
2463
+ })
2464
+ continue
2465
+
2466
+ iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
2467
+ if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
2468
+ iface_sym = self.short_to_symbols[b.target_name][0]
2469
+
2470
+ if not iface_sym:
2471
+ findings.append({
2472
+ "pattern": "DI_RESOLVES",
2473
+ "status": "UNKNOWN",
2474
+ "reason": "unknown_dependency_interface",
2475
+ "file": b.file_path,
2476
+ "line": b.line,
2477
+ "target_name": b.target_name,
1853
2478
  })
1854
2479
  continue
1855
2480
 
@@ -2126,32 +2751,82 @@ class ReferenceResolver:
2126
2751
  )
2127
2752
  )
2128
2753
 
2129
- # 9d. Constructor & @inject Type-Based Injection (INJECTS)
2130
- for b in bindings_by_kind.get("CONSTRUCTOR_PARAM", ()):
2754
+ # 9d. Constructor & @inject Type-Based Injection (INJECTS)
2755
+ for b in bindings_by_kind.get("CONSTRUCTOR_PARAM", ()):
2756
+ ann_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2757
+ if not ann_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2758
+ ann_sym = self.short_to_symbols[b.source_expr][0]
2759
+
2760
+ if not ann_sym:
2761
+ continue
2762
+
2763
+ ann_canon = ann_sym.canonical_id
2764
+ # Rule: Only create injection relationship when supported DI semantics exist!
2765
+ # i.e., provider mapping exists for this interface OR @inject decorator was present
2766
+ has_provider = ann_canon in provider_map
2767
+ is_inject_dec = (b.base_expr == "inject_decorator")
2768
+
2769
+ if not (has_provider or is_inject_dec):
2770
+ continue
2771
+
2772
+ f_hash = self.file_hashes.get(b.file_path, "")
2773
+ caller_canon = b.target_name
2774
+ ev_str = f"Constructor injection: '{caller_canon}' INJECTS '{ann_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
2775
+
2776
+ add_ref(
2777
+ Reference(
2778
+ source_symbol_id=caller_canon,
2779
+ target_symbol_id=ann_canon,
2780
+ relationship="INJECTS",
2781
+ confidence="HIGH",
2782
+ path=b.file_path,
2783
+ start_line=b.line,
2784
+ end_line=b.line,
2785
+ evidence=ev_str,
2786
+ source_hash=f_hash,
2787
+ indexed_commit=self.indexed_commit,
2788
+ )
2789
+ )
2790
+ add_edge(
2791
+ GraphEdge(
2792
+ source=caller_canon,
2793
+ target=ann_canon,
2794
+ relationship="INJECTS",
2795
+ confidence="HIGH",
2796
+ file=b.file_path,
2797
+ start_line=b.line,
2798
+ end_line=b.line,
2799
+ evidence=ev_str,
2800
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2801
+ reason="constructor_injection",
2802
+ )
2803
+ )
2804
+
2805
+ # 9d-nest. NestJS Constructor & @Inject Parameter Injection (INJECTS)
2806
+ for b in bindings_by_kind.get("NESTJS_INJECTS", ()):
2131
2807
  ann_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2132
2808
  if not ann_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2133
2809
  ann_sym = self.short_to_symbols[b.source_expr][0]
2134
2810
 
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")
2811
+ injected_canon: str | None = None
2812
+ if ann_sym:
2813
+ injected_canon = ann_sym.canonical_id
2814
+ elif b.source_expr in provider_map:
2815
+ injected_canon = provider_map[b.source_expr][0][0]
2816
+ elif b.source_expr:
2817
+ injected_canon = f"token.{b.source_expr}"
2143
2818
 
2144
- if not (has_provider or is_inject_dec):
2819
+ if not injected_canon:
2145
2820
  continue
2146
2821
 
2147
2822
  f_hash = self.file_hashes.get(b.file_path, "")
2148
2823
  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}"
2824
+ ev_str = f"NestJS injection: '{caller_canon}' INJECTS '{injected_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
2150
2825
 
2151
2826
  add_ref(
2152
2827
  Reference(
2153
2828
  source_symbol_id=caller_canon,
2154
- target_symbol_id=ann_canon,
2829
+ target_symbol_id=injected_canon,
2155
2830
  relationship="INJECTS",
2156
2831
  confidence="HIGH",
2157
2832
  path=b.file_path,
@@ -2165,18 +2840,59 @@ class ReferenceResolver:
2165
2840
  add_edge(
2166
2841
  GraphEdge(
2167
2842
  source=caller_canon,
2168
- target=ann_canon,
2843
+ target=injected_canon,
2169
2844
  relationship="INJECTS",
2170
2845
  confidence="HIGH",
2171
2846
  file=b.file_path,
2172
2847
  start_line=b.line,
2173
2848
  end_line=b.line,
2174
2849
  evidence=ev_str,
2175
- evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
2176
- reason="constructor_injection",
2850
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2851
+ reason="nestjs_injection",
2177
2852
  )
2178
2853
  )
2179
2854
 
2855
+ # Link routes of this controller to the injected dependency
2856
+ for rt in self.routes:
2857
+ if rt.handler_canonical_id.startswith(f"{caller_canon}."):
2858
+ rt_ev = f"NestJS Route endpoint '{rt.endpoint_id}' INJECTS '{injected_canon}' via {caller_canon} at {rt.file_path}:{rt.line}"
2859
+ add_edge(
2860
+ GraphEdge(
2861
+ source=rt.endpoint_id,
2862
+ target=injected_canon,
2863
+ relationship="INJECTS",
2864
+ confidence="HIGH",
2865
+ file=rt.file_path,
2866
+ start_line=rt.line,
2867
+ end_line=rt.line,
2868
+ evidence=rt_ev,
2869
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2870
+ reason="nestjs_route_injection",
2871
+ )
2872
+ )
2873
+
2874
+ # 9d-ctrl. NestJS Controller Mounting (MOUNTS)
2875
+ for b in bindings_by_kind.get("NESTJS_CONTROLLER", ()):
2876
+ ctrl_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
2877
+ if not ctrl_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
2878
+ ctrl_sym = self.short_to_symbols[b.source_expr][0]
2879
+ if ctrl_sym:
2880
+ ctrl_canon = ctrl_sym.canonical_id
2881
+ add_edge(
2882
+ GraphEdge(
2883
+ source=b.target_name,
2884
+ target=ctrl_canon,
2885
+ relationship="MOUNTS",
2886
+ confidence="HIGH",
2887
+ file=b.file_path,
2888
+ start_line=b.line,
2889
+ end_line=b.line,
2890
+ evidence=f"NestJS Module '{b.target_name}' mounts controller '{ctrl_canon}'",
2891
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
2892
+ reason="nestjs_module_mount",
2893
+ )
2894
+ )
2895
+
2180
2896
  # 9d-cycle. Static DI Cycle Detection (INJECTS / PROVIDES / RESOLVES_DEPENDENCY)
2181
2897
  di_adj: dict[str, list[tuple[str, str, int]]] = {}
2182
2898
  for ed in list(edges):
@@ -2291,12 +3007,6 @@ class ReferenceResolver:
2291
3007
  )
2292
3008
 
2293
3009
  # 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
3010
 
2301
3011
  # 10a. Direct Test Calls (TESTS)
2302
3012
  # Any call originating from a test file to a production symbol
@@ -2665,6 +3375,339 @@ class ReferenceResolver:
2665
3375
  )
2666
3376
  )
2667
3377
 
3378
+ # 12. Frontend Web Graph (React, JSX, Hooks, Styles, HTML - Pillar 4)
3379
+ for b in bindings_by_kind.get("REACT_RENDERS", ()):
3380
+ child_name = b.target_name
3381
+ caller_id = _find_caller_id_for_binding(b)
3382
+ target_sym, _, _ = self.resolve_identifier_in_file(child_name, b.file_path, use_line=b.line)
3383
+ if not target_sym and child_name in self.short_to_symbols:
3384
+ candidates = [s for s in self.short_to_symbols[child_name] if s.kind in ("component", "function")]
3385
+ if candidates:
3386
+ target_sym = candidates[0]
3387
+ target_id = target_sym.canonical_id if target_sym else child_name
3388
+ ev_str = f"React component renders '<{child_name} />' at {b.file_path}:{b.line}"
3389
+ f_hash = self.file_hashes.get(b.file_path, "")
3390
+ add_ref(
3391
+ Reference(
3392
+ source_symbol_id=caller_id,
3393
+ target_symbol_id=target_id,
3394
+ relationship="RENDERS",
3395
+ confidence="HIGH" if target_sym else "MEDIUM",
3396
+ path=b.file_path,
3397
+ start_line=b.line,
3398
+ end_line=b.line,
3399
+ evidence=ev_str,
3400
+ source_hash=f_hash,
3401
+ indexed_commit=self.indexed_commit,
3402
+ )
3403
+ )
3404
+ add_edge(
3405
+ GraphEdge(
3406
+ source=caller_id,
3407
+ target=target_id,
3408
+ relationship="RENDERS",
3409
+ confidence="HIGH" if target_sym else "MEDIUM",
3410
+ file=b.file_path,
3411
+ start_line=b.line,
3412
+ end_line=b.line,
3413
+ evidence=ev_str,
3414
+ evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value if target_sym else RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3415
+ reason=f"react_jsx_render:{child_name}",
3416
+ )
3417
+ )
3418
+
3419
+ for b in bindings_by_kind.get("REACT_USES_HOOK", ()):
3420
+ hook_name = b.target_name
3421
+ caller_id = _find_caller_id_for_binding(b)
3422
+ target_sym, _, _ = self.resolve_identifier_in_file(hook_name, b.file_path, use_line=b.line)
3423
+ if not target_sym and hook_name in self.short_to_symbols:
3424
+ candidates = [s for s in self.short_to_symbols[hook_name] if s.kind in ("hook", "function")]
3425
+ if candidates:
3426
+ target_sym = candidates[0]
3427
+ target_id = target_sym.canonical_id if target_sym else hook_name
3428
+ ev_str = f"React component invokes hook '{hook_name}()' at {b.file_path}:{b.line}"
3429
+ f_hash = self.file_hashes.get(b.file_path, "")
3430
+ add_ref(
3431
+ Reference(
3432
+ source_symbol_id=caller_id,
3433
+ target_symbol_id=target_id,
3434
+ relationship="USES_HOOK",
3435
+ confidence="HIGH" if target_sym else "MEDIUM",
3436
+ path=b.file_path,
3437
+ start_line=b.line,
3438
+ end_line=b.line,
3439
+ evidence=ev_str,
3440
+ source_hash=f_hash,
3441
+ indexed_commit=self.indexed_commit,
3442
+ )
3443
+ )
3444
+ add_edge(
3445
+ GraphEdge(
3446
+ source=caller_id,
3447
+ target=target_id,
3448
+ relationship="USES_HOOK",
3449
+ confidence="HIGH" if target_sym else "MEDIUM",
3450
+ file=b.file_path,
3451
+ start_line=b.line,
3452
+ end_line=b.line,
3453
+ evidence=ev_str,
3454
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3455
+ reason=f"react_hook_call:{hook_name}",
3456
+ )
3457
+ )
3458
+
3459
+ for b in bindings_by_kind.get("REACT_IMPORTS_STYLE", ()):
3460
+ style_rel = b.target_name
3461
+ src_dir = posixpath.dirname(b.file_path.replace("\\", "/").lstrip("./"))
3462
+ resolved_style = posixpath.normpath(posixpath.join(src_dir, style_rel)) if style_rel.startswith(".") else style_rel
3463
+ if resolved_style not in self.known_files:
3464
+ for kf in self.known_files:
3465
+ if kf.endswith(f"/{style_rel}") or kf == style_rel:
3466
+ resolved_style = kf
3467
+ break
3468
+ caller_id = _find_caller_id_for_binding(b)
3469
+ ev_str = f"Component imports stylesheet '{style_rel}' at {b.file_path}:{b.line}"
3470
+ f_hash = self.file_hashes.get(b.file_path, "")
3471
+ add_ref(
3472
+ Reference(
3473
+ source_symbol_id=caller_id,
3474
+ target_symbol_id=resolved_style,
3475
+ relationship="IMPORTS_STYLE",
3476
+ confidence="HIGH",
3477
+ path=b.file_path,
3478
+ start_line=b.line,
3479
+ end_line=b.line,
3480
+ evidence=ev_str,
3481
+ source_hash=f_hash,
3482
+ indexed_commit=self.indexed_commit,
3483
+ )
3484
+ )
3485
+ add_edge(
3486
+ GraphEdge(
3487
+ source=caller_id,
3488
+ target=resolved_style,
3489
+ relationship="IMPORTS_STYLE",
3490
+ confidence="HIGH",
3491
+ file=b.file_path,
3492
+ start_line=b.line,
3493
+ end_line=b.line,
3494
+ evidence=ev_str,
3495
+ evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value,
3496
+ reason=f"react_style_import:{style_rel}",
3497
+ )
3498
+ )
3499
+
3500
+ for b in bindings_by_kind.get("REACT_USES_STYLE_CLASS", ()):
3501
+ cls_name = b.target_name
3502
+ caller_id = _find_caller_id_for_binding(b)
3503
+ ev_str = f"Component applies CSS class '.{cls_name}' at {b.file_path}:{b.line}"
3504
+ f_hash = self.file_hashes.get(b.file_path, "")
3505
+ add_ref(
3506
+ Reference(
3507
+ source_symbol_id=caller_id,
3508
+ target_symbol_id=f".{cls_name}",
3509
+ relationship="USES_STYLE_CLASS",
3510
+ confidence="HIGH",
3511
+ path=b.file_path,
3512
+ start_line=b.line,
3513
+ end_line=b.line,
3514
+ evidence=ev_str,
3515
+ source_hash=f_hash,
3516
+ indexed_commit=self.indexed_commit,
3517
+ )
3518
+ )
3519
+ add_edge(
3520
+ GraphEdge(
3521
+ source=caller_id,
3522
+ target=f".{cls_name}",
3523
+ relationship="USES_STYLE_CLASS",
3524
+ confidence="HIGH",
3525
+ file=b.file_path,
3526
+ start_line=b.line,
3527
+ end_line=b.line,
3528
+ evidence=ev_str,
3529
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3530
+ reason=f"css_class_use:.{cls_name}",
3531
+ )
3532
+ )
3533
+
3534
+ for b in bindings_by_kind.get("REACT_FETCH_ROUTE", ()):
3535
+ raw_url = b.target_name
3536
+ caller_id = _find_caller_id_for_binding(b)
3537
+ client_method = b.base_expr or "GET"
3538
+ norm_client = normalize_route(client_method, raw_url)
3539
+
3540
+ matched_routes: list[tuple[RouteDetection, str]] = []
3541
+ for r in self.routes:
3542
+ if (
3543
+ r.route_path == raw_url
3544
+ or r.normalized_route == raw_url
3545
+ or r.route_path.rstrip("/") == raw_url.rstrip("/")
3546
+ ):
3547
+ matched_routes.append((r, "EXACT"))
3548
+ continue
3549
+ norm_backend = normalize_route(r.http_method, r.route_path)
3550
+ is_matched, match_type = match_route_topology(
3551
+ norm_client, norm_backend, allow_proxy_prefix=True, match_methods=True
3552
+ )
3553
+ if is_matched:
3554
+ matched_routes.append((r, match_type))
3555
+
3556
+ f_hash = self.file_hashes.get(b.file_path, "")
3557
+ if matched_routes:
3558
+ for r, match_type in matched_routes:
3559
+ ev_str = f"Client fetches route '{r.route_path}' ({match_type}) at {b.file_path}:{b.line}"
3560
+ add_ref(
3561
+ Reference(
3562
+ source_symbol_id=caller_id,
3563
+ target_symbol_id=r.endpoint_id,
3564
+ relationship="FETCHES_ROUTE",
3565
+ confidence="HIGH",
3566
+ path=b.file_path,
3567
+ start_line=b.line,
3568
+ end_line=b.line,
3569
+ evidence=ev_str,
3570
+ source_hash=f_hash,
3571
+ indexed_commit=self.indexed_commit,
3572
+ )
3573
+ )
3574
+ add_edge(
3575
+ GraphEdge(
3576
+ source=caller_id,
3577
+ target=r.endpoint_id,
3578
+ relationship="FETCHES_ROUTE",
3579
+ confidence="HIGH",
3580
+ file=b.file_path,
3581
+ start_line=b.line,
3582
+ end_line=b.line,
3583
+ evidence=ev_str,
3584
+ evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
3585
+ reason=f"client_fetch:{r.route_path}",
3586
+ )
3587
+ )
3588
+ if r.handler_canonical_id:
3589
+ h_ev = f"Client invokes backend handler '{r.handler_canonical_id}' via {r.route_path} ({match_type}) at {b.file_path}:{b.line}"
3590
+ add_ref(
3591
+ Reference(
3592
+ source_symbol_id=caller_id,
3593
+ target_symbol_id=r.handler_canonical_id,
3594
+ relationship="CALLS",
3595
+ confidence="HIGH",
3596
+ path=b.file_path,
3597
+ start_line=b.line,
3598
+ end_line=b.line,
3599
+ evidence=h_ev,
3600
+ source_hash=f_hash,
3601
+ indexed_commit=self.indexed_commit,
3602
+ )
3603
+ )
3604
+ add_edge(
3605
+ GraphEdge(
3606
+ source=caller_id,
3607
+ target=r.handler_canonical_id,
3608
+ relationship="CALLS",
3609
+ confidence="HIGH",
3610
+ file=b.file_path,
3611
+ start_line=b.line,
3612
+ end_line=b.line,
3613
+ evidence=h_ev,
3614
+ evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
3615
+ reason=f"client_handler_call:{r.route_path}",
3616
+ )
3617
+ )
3618
+ else:
3619
+ ev_str = f"Client fetches external/custom route '{raw_url}' at {b.file_path}:{b.line}"
3620
+ add_edge(
3621
+ GraphEdge(
3622
+ source=caller_id,
3623
+ target=raw_url,
3624
+ relationship="FETCHES_ROUTE",
3625
+ confidence="LOW",
3626
+ file=b.file_path,
3627
+ start_line=b.line,
3628
+ end_line=b.line,
3629
+ evidence=ev_str,
3630
+ evidence_class=RelationshipEvidenceClass.POSSIBLE.value,
3631
+ reason=f"client_fetch:{raw_url}",
3632
+ )
3633
+ )
3634
+
3635
+ for b in bindings_by_kind.get("HTML_LOADS_SCRIPT", ()):
3636
+ src_val = b.target_name
3637
+ target_file = src_val
3638
+ for kf in self.known_files:
3639
+ if kf.endswith(f"/{src_val}") or kf == src_val:
3640
+ target_file = kf
3641
+ break
3642
+ ev_str = f"HTML loads script '{src_val}' at {b.file_path}:{b.line}"
3643
+ f_hash = self.file_hashes.get(b.file_path, "")
3644
+ add_ref(
3645
+ Reference(
3646
+ source_symbol_id=b.file_path,
3647
+ target_symbol_id=target_file,
3648
+ relationship="LOADS_SCRIPT",
3649
+ confidence="HIGH",
3650
+ path=b.file_path,
3651
+ start_line=b.line,
3652
+ end_line=b.line,
3653
+ evidence=ev_str,
3654
+ source_hash=f_hash,
3655
+ indexed_commit=self.indexed_commit,
3656
+ )
3657
+ )
3658
+ add_edge(
3659
+ GraphEdge(
3660
+ source=b.file_path,
3661
+ target=target_file,
3662
+ relationship="LOADS_SCRIPT",
3663
+ confidence="HIGH",
3664
+ file=b.file_path,
3665
+ start_line=b.line,
3666
+ end_line=b.line,
3667
+ evidence=ev_str,
3668
+ evidence_class=RelationshipEvidenceClass.STATIC_VERIFIED.value,
3669
+ reason=f"html_script:{src_val}",
3670
+ )
3671
+ )
3672
+
3673
+ for b in bindings_by_kind.get("HTML_LOADS_STYLESHEET", ()):
3674
+ href_val = b.target_name
3675
+ target_file = href_val
3676
+ for kf in self.known_files:
3677
+ if kf.endswith(f"/{href_val}") or kf == href_val:
3678
+ target_file = kf
3679
+ break
3680
+ ev_str = f"HTML loads stylesheet '{href_val}' at {b.file_path}:{b.line}"
3681
+ f_hash = self.file_hashes.get(b.file_path, "")
3682
+ add_ref(
3683
+ Reference(
3684
+ source_symbol_id=b.file_path,
3685
+ target_symbol_id=target_file,
3686
+ relationship="LOADS_STYLESHEET",
3687
+ confidence="HIGH",
3688
+ path=b.file_path,
3689
+ start_line=b.line,
3690
+ end_line=b.line,
3691
+ evidence=ev_str,
3692
+ source_hash=f_hash,
3693
+ indexed_commit=self.indexed_commit,
3694
+ )
3695
+ )
3696
+ add_edge(
3697
+ GraphEdge(
3698
+ source=b.file_path,
3699
+ target=target_file,
3700
+ relationship="LOADS_STYLESHEET",
3701
+ confidence="HIGH",
3702
+ file=b.file_path,
3703
+ start_line=b.line,
3704
+ end_line=b.line,
3705
+ evidence=ev_str,
3706
+ evidence_class=RelationshipEvidenceClass.STATIC_VERIFIED.value,
3707
+ reason=f"html_stylesheet:{href_val}",
3708
+ )
3709
+ )
3710
+
2668
3711
  return ResolutionOutput(
2669
3712
  references=references,
2670
3713
  edges=edges,