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.
- codegraph/__init__.py +1 -1
- codegraph/agent_brain.py +172 -2
- codegraph/agent_capabilities.py +564 -6
- codegraph/agent_rules.py +1 -1
- codegraph/api_drift.py +455 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +992 -3
- codegraph/cli_output.py +17 -2
- codegraph/context.py +38 -11
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +7 -0
- codegraph/evidence_contract.py +21 -0
- codegraph/frameworks.py +171 -6
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +545 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/indexer.py +291 -38
- codegraph/indexing/models.py +6 -0
- codegraph/indexing/parser.py +239 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +6 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +128 -20
- codegraph/mcp/server.py +258 -27
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/optimizer.py +42 -5
- codegraph/process_lifecycle.py +31 -13
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +403 -0
- codegraph/resolver.py +1202 -159
- codegraph/retrieval_policy.py +6 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +19 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +715 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +257 -39
- codegraph_engine-3.0.0.dist-info/RECORD +129 -0
- codegraph_engine-2.2.1.dist-info/RECORD +0 -87
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {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,
|
|
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,
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
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=
|
|
1803
|
-
target_symbol_id=
|
|
1804
|
-
relationship=
|
|
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=
|
|
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=
|
|
1817
|
-
target=
|
|
1818
|
-
relationship=
|
|
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=
|
|
1942
|
+
evidence=ev_evidence,
|
|
1824
1943
|
evidence_class=ev_class.value,
|
|
1825
1944
|
reason=edge_reason,
|
|
1826
1945
|
)
|
|
1827
1946
|
)
|
|
1828
1947
|
|
|
1829
|
-
#
|
|
1830
|
-
for b in bindings_by_kind.get("
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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
|
|
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"
|
|
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=
|
|
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=
|
|
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.
|
|
2176
|
-
reason="
|
|
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,
|