codegraph-engine 2.3.0__py3-none-any.whl → 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +1 -1
- codegraph/__main__.py +7 -0
- codegraph/agent_brain.py +253 -2
- codegraph/agent_capabilities.py +1348 -67
- codegraph/agent_rules.py +2 -1
- codegraph/api_drift.py +455 -0
- codegraph/architecture_rules.py +123 -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/change_impact.py +320 -110
- codegraph/changelog.py +170 -0
- codegraph/cli.py +1605 -62
- codegraph/cli_output.py +17 -2
- codegraph/context.py +135 -24
- codegraph/context_freshness.py +2 -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/dataflow_propagation.py +118 -0
- codegraph/dead_code.py +387 -0
- codegraph/epistemic.py +15 -0
- codegraph/evidence_contract.py +34 -3
- codegraph/federation.py +245 -0
- codegraph/frameworks.py +234 -12
- 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 +2 -0
- codegraph/graph/traversal.py +150 -7
- codegraph/graph_csr.py +218 -0
- codegraph/incremental.py +150 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/classifier.py +5 -0
- codegraph/indexing/indexer.py +292 -38
- codegraph/indexing/languages/__init__.py +1 -0
- codegraph/indexing/languages/cpp.py +457 -0
- codegraph/indexing/languages/go.py +459 -0
- codegraph/indexing/languages/jvm.py +583 -0
- codegraph/indexing/languages/rust.py +507 -0
- codegraph/indexing/models.py +15 -1
- codegraph/indexing/parser.py +385 -9
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/protocols.py +220 -0
- codegraph/indexing/scanner.py +23 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +207 -39
- codegraph/lsp.py +318 -0
- codegraph/lsp_bridge/__init__.py +16 -0
- codegraph/lsp_bridge/bridge.py +102 -0
- codegraph/lsp_bridge/client.py +207 -0
- codegraph/lsp_bridge/models.py +71 -0
- codegraph/mcp/server.py +502 -29
- codegraph/memory/trajectory.py +282 -0
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/multi_agent/__init__.py +13 -0
- codegraph/multi_agent/coordinator.py +229 -0
- codegraph/multi_agent/models.py +76 -0
- codegraph/optimizer.py +42 -5
- codegraph/polymorphism.py +95 -0
- codegraph/pr_review.py +296 -0
- codegraph/process_lifecycle.py +50 -12
- codegraph/reachability.py +198 -0
- codegraph/reachability_index.py +74 -0
- codegraph/refactor/__init__.py +48 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/patcher.py +348 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/repair.py +233 -0
- codegraph/resolver.py +1357 -161
- codegraph/retrieval_policy.py +22 -1
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +7 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +25 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/otel_receiver.py +208 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/sandbox.py +215 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/security/supply_chain.py +188 -0
- codegraph/security/taint.py +421 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +2 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +2 -0
- codegraph/test_gen.py +437 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +876 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
- codegraph_engine-3.0.1.dist-info/RECORD +164 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
codegraph/resolver.py
CHANGED
|
@@ -20,6 +20,7 @@ import posixpath
|
|
|
20
20
|
import re
|
|
21
21
|
import sqlite3
|
|
22
22
|
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
23
24
|
|
|
24
25
|
from codegraph.binding_resolver import LocalBindingResolver
|
|
25
26
|
from codegraph.epistemic import RelationshipEvidenceClass
|
|
@@ -36,6 +37,8 @@ from codegraph.indexing.models import (
|
|
|
36
37
|
)
|
|
37
38
|
from codegraph.monorepo import WorkspaceInfo
|
|
38
39
|
from codegraph.route_composer import ComposedMountEdge
|
|
40
|
+
from codegraph.route_topology import match_route_topology, normalize_route
|
|
41
|
+
from codegraph.tsconfig import TsConfigResolver
|
|
39
42
|
|
|
40
43
|
_TEST_FILE_RE = re.compile(
|
|
41
44
|
r"(^|[_/])test[_s]?[_/]|[_/]spec[_/]|test[_s]?\.(py|js|ts|jsx|tsx)$|spec\.(py|js|ts|jsx|tsx)$",
|
|
@@ -51,6 +54,7 @@ def resolve_module_to_path(
|
|
|
51
54
|
source_file: str,
|
|
52
55
|
known_files: set[str],
|
|
53
56
|
workspace: WorkspaceInfo | None = None,
|
|
57
|
+
tsconfig_resolver: TsConfigResolver | None = None,
|
|
54
58
|
) -> str | None:
|
|
55
59
|
"""Deterministically resolve an import specifier to a repository-relative file path.
|
|
56
60
|
|
|
@@ -74,6 +78,12 @@ def resolve_module_to_path(
|
|
|
74
78
|
if f"{clean_entry}/__init__{ext}" in known_files:
|
|
75
79
|
return f"{clean_entry}/__init__{ext}"
|
|
76
80
|
|
|
81
|
+
# 0b. Check tsconfig.json / jsconfig.json path aliases
|
|
82
|
+
if tsconfig_resolver is not None:
|
|
83
|
+
alias_target = tsconfig_resolver.resolve_alias(imported_module, source_file, known_files)
|
|
84
|
+
if alias_target:
|
|
85
|
+
return alias_target
|
|
86
|
+
|
|
77
87
|
src_clean = source_file.replace("\\", "/").lstrip("./")
|
|
78
88
|
src_dir = posixpath.dirname(src_clean)
|
|
79
89
|
|
|
@@ -150,6 +160,8 @@ class ReferenceResolver:
|
|
|
150
160
|
mount_edges: list[ComposedMountEdge] | None = None,
|
|
151
161
|
bindings: list[BindingRef] | None = None,
|
|
152
162
|
workspace: WorkspaceInfo | None = None,
|
|
163
|
+
tsconfig_resolver: TsConfigResolver | None = None,
|
|
164
|
+
repository: Path | None = None,
|
|
153
165
|
max_reexport_depth: int = 16,
|
|
154
166
|
max_wildcard_expansions: int = 64,
|
|
155
167
|
) -> None:
|
|
@@ -164,6 +176,8 @@ class ReferenceResolver:
|
|
|
164
176
|
self.file_hashes = file_hashes
|
|
165
177
|
self.indexed_commit = indexed_commit
|
|
166
178
|
self.workspace = workspace
|
|
179
|
+
self.tsconfig_resolver = tsconfig_resolver
|
|
180
|
+
self.repository = repository
|
|
167
181
|
self.max_reexport_depth = max_reexport_depth
|
|
168
182
|
self.max_wildcard_expansions = max_wildcard_expansions
|
|
169
183
|
|
|
@@ -257,7 +271,11 @@ class ReferenceResolver:
|
|
|
257
271
|
|
|
258
272
|
def _resolve_import_target_module_uncached(self, imp: ImportRef) -> tuple[str | None, str | None]:
|
|
259
273
|
target_path = resolve_module_to_path(
|
|
260
|
-
imp.imported_module,
|
|
274
|
+
imp.imported_module,
|
|
275
|
+
imp.source_file,
|
|
276
|
+
self.known_files,
|
|
277
|
+
workspace=self.workspace,
|
|
278
|
+
tsconfig_resolver=self.tsconfig_resolver,
|
|
261
279
|
)
|
|
262
280
|
if target_path:
|
|
263
281
|
return target_path, normalize_module(target_path)
|
|
@@ -266,7 +284,11 @@ class ReferenceResolver:
|
|
|
266
284
|
if "." in imp.imported_module and not imp.imported_module.startswith("."):
|
|
267
285
|
parent_mod, last_seg = imp.imported_module.rsplit(".", 1)
|
|
268
286
|
parent_path = resolve_module_to_path(
|
|
269
|
-
parent_mod,
|
|
287
|
+
parent_mod,
|
|
288
|
+
imp.source_file,
|
|
289
|
+
self.known_files,
|
|
290
|
+
workspace=self.workspace,
|
|
291
|
+
tsconfig_resolver=self.tsconfig_resolver,
|
|
270
292
|
)
|
|
271
293
|
if parent_path:
|
|
272
294
|
return parent_path, normalize_module(parent_path)
|
|
@@ -605,7 +627,23 @@ class ReferenceResolver:
|
|
|
605
627
|
seen_refs.add(key)
|
|
606
628
|
references.append(ref)
|
|
607
629
|
|
|
630
|
+
_NON_CODE_EXTS = (
|
|
631
|
+
".html",
|
|
632
|
+
".htm",
|
|
633
|
+
".jinja",
|
|
634
|
+
".jinja2",
|
|
635
|
+
".css",
|
|
636
|
+
".yaml",
|
|
637
|
+
".yml",
|
|
638
|
+
".json",
|
|
639
|
+
".md",
|
|
640
|
+
".markdown",
|
|
641
|
+
)
|
|
642
|
+
|
|
608
643
|
def add_edge(edge: GraphEdge) -> None:
|
|
644
|
+
fpath = str(edge.file or "")
|
|
645
|
+
if fpath.endswith(_NON_CODE_EXTS):
|
|
646
|
+
return
|
|
609
647
|
key = (
|
|
610
648
|
edge.source,
|
|
611
649
|
edge.target,
|
|
@@ -617,8 +655,17 @@ class ReferenceResolver:
|
|
|
617
655
|
seen_edges.add(key)
|
|
618
656
|
edges.append(edge)
|
|
619
657
|
|
|
658
|
+
def _find_caller_id_for_binding(b: BindingRef) -> str:
|
|
659
|
+
for s in self.path_to_symbols.get(b.file_path, ()):
|
|
660
|
+
if s.start_line <= b.line <= s.end_line:
|
|
661
|
+
return s.canonical_id
|
|
662
|
+
mod = self.file_to_module.get(b.file_path) or normalize_module(b.file_path)
|
|
663
|
+
return f"{mod}.{b.scope}" if b.scope else mod
|
|
664
|
+
|
|
620
665
|
# 0. Definition & Containment edges
|
|
621
666
|
for sym in self.symbols:
|
|
667
|
+
if sym.path.endswith(_NON_CODE_EXTS):
|
|
668
|
+
continue
|
|
622
669
|
f_hash = self.file_hashes.get(sym.path, "")
|
|
623
670
|
if sym.parent_symbol_id:
|
|
624
671
|
add_edge(
|
|
@@ -976,6 +1023,8 @@ class ReferenceResolver:
|
|
|
976
1023
|
expr = call.qualified_callee or call.callee
|
|
977
1024
|
via_factory_sym: Symbol | None = None
|
|
978
1025
|
resolved_via_dataflow = False
|
|
1026
|
+
is_pytorch_forward = False
|
|
1027
|
+
is_concrete_module = False
|
|
979
1028
|
|
|
980
1029
|
target_sym, conf, reason = self.resolve_identifier_in_file(
|
|
981
1030
|
expr, call.source_file, caller_id, use_line=call.line
|
|
@@ -998,6 +1047,35 @@ class ReferenceResolver:
|
|
|
998
1047
|
reason = f"{recv_reason}; inherited method on '{owner_canon}'"
|
|
999
1048
|
else:
|
|
1000
1049
|
reason = recv_reason
|
|
1050
|
+
# If not a method, check if expr or self.<attr> is an nn.Module submodule invocation (e.g. self.encoder(x))
|
|
1051
|
+
if not target_sym:
|
|
1052
|
+
submod_cls_sym, _, submod_reason = _resolve_receiver_class(
|
|
1053
|
+
expr, call.source_file, caller_id, caller_qname, call.line
|
|
1054
|
+
)
|
|
1055
|
+
if not submod_cls_sym:
|
|
1056
|
+
for b in self.bindings:
|
|
1057
|
+
if b.file_path == call.source_file and b.target_name == f"self.{method_part}":
|
|
1058
|
+
if b.expr_kind == "CALL_RETURN" and b.source_expr:
|
|
1059
|
+
s_cls, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, caller_id, use_line=b.line)
|
|
1060
|
+
if s_cls and s_cls.kind == "class":
|
|
1061
|
+
submod_cls_sym = s_cls
|
|
1062
|
+
submod_reason = f"submodule assignment 'self.{method_part} = {b.source_expr}()'"
|
|
1063
|
+
break
|
|
1064
|
+
elif b.expr_kind == "TYPE_ANNOTATION" and b.source_expr:
|
|
1065
|
+
s_cls, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, caller_id, use_line=b.line)
|
|
1066
|
+
if s_cls and s_cls.kind == "class":
|
|
1067
|
+
submod_cls_sym = s_cls
|
|
1068
|
+
submod_reason = f"submodule annotation 'self.{method_part}: {b.source_expr}'"
|
|
1069
|
+
break
|
|
1070
|
+
if submod_cls_sym:
|
|
1071
|
+
fwd_sym, _ = _lookup_method_on_class(submod_cls_sym.canonical_id, "forward")
|
|
1072
|
+
if fwd_sym:
|
|
1073
|
+
target_sym = fwd_sym
|
|
1074
|
+
resolved_via_dataflow = True
|
|
1075
|
+
is_pytorch_forward = True
|
|
1076
|
+
is_concrete_module = "Constructed instance" in submod_reason or "factory" in submod_reason or "submodule assignment" in submod_reason
|
|
1077
|
+
conf = "HIGH" if is_concrete_module else "MEDIUM"
|
|
1078
|
+
reason = f"PyTorch submodule forward dispatch via {submod_reason}"
|
|
1001
1079
|
else:
|
|
1002
1080
|
# Even if parser substituted receiver -> ClassName, check if it came from a local factory binding
|
|
1003
1081
|
for b in call_return_bindings_by_file.get(call.source_file, ()):
|
|
@@ -1041,6 +1119,23 @@ class ReferenceResolver:
|
|
|
1041
1119
|
)
|
|
1042
1120
|
)
|
|
1043
1121
|
|
|
1122
|
+
else:
|
|
1123
|
+
# expr has no dot (e.g. model(x))
|
|
1124
|
+
# Only dispatch to forward if target_sym is NOT already a class (constructor call Classifier() is __init__)
|
|
1125
|
+
if not target_sym or target_sym.kind not in ("function", "method", "class"):
|
|
1126
|
+
mod_cls_sym, _, mod_reason = _resolve_receiver_class(
|
|
1127
|
+
expr, call.source_file, caller_id, caller_qname, call.line
|
|
1128
|
+
)
|
|
1129
|
+
if mod_cls_sym and mod_cls_sym.kind == "class":
|
|
1130
|
+
fwd_sym, _ = _lookup_method_on_class(mod_cls_sym.canonical_id, "forward")
|
|
1131
|
+
if fwd_sym:
|
|
1132
|
+
target_sym = fwd_sym
|
|
1133
|
+
resolved_via_dataflow = True
|
|
1134
|
+
is_pytorch_forward = True
|
|
1135
|
+
is_concrete_module = "Constructed instance" in mod_reason or "factory" in mod_reason
|
|
1136
|
+
conf = "HIGH" if is_concrete_module else "MEDIUM"
|
|
1137
|
+
reason = f"PyTorch module forward dispatch via {mod_reason}"
|
|
1138
|
+
|
|
1044
1139
|
if target_sym:
|
|
1045
1140
|
resolved_calls[(call.source_file, call.line, call.callee)] = (
|
|
1046
1141
|
target_sym.canonical_id,
|
|
@@ -1078,6 +1173,40 @@ class ReferenceResolver:
|
|
|
1078
1173
|
evidence_class=call_ev_cls,
|
|
1079
1174
|
)
|
|
1080
1175
|
)
|
|
1176
|
+
if is_pytorch_forward:
|
|
1177
|
+
fwd_ev_cls = (
|
|
1178
|
+
RelationshipEvidenceClass.AST_VERIFIED.value
|
|
1179
|
+
if is_concrete_module
|
|
1180
|
+
else RelationshipEvidenceClass.POSSIBLE.value
|
|
1181
|
+
)
|
|
1182
|
+
add_ref(
|
|
1183
|
+
Reference(
|
|
1184
|
+
source_symbol_id=caller_id,
|
|
1185
|
+
target_symbol_id=target_sym.canonical_id,
|
|
1186
|
+
relationship="DISPATCHES_FORWARD",
|
|
1187
|
+
confidence=conf,
|
|
1188
|
+
path=call.source_file,
|
|
1189
|
+
start_line=call.line,
|
|
1190
|
+
end_line=call.end_line,
|
|
1191
|
+
evidence=f"Module invocation '{expr}()' at {call.source_file}:{call.line} dispatches to {target_sym.canonical_id} ({reason})",
|
|
1192
|
+
source_hash=f_hash,
|
|
1193
|
+
indexed_commit=self.indexed_commit,
|
|
1194
|
+
)
|
|
1195
|
+
)
|
|
1196
|
+
add_edge(
|
|
1197
|
+
GraphEdge(
|
|
1198
|
+
source=caller_id,
|
|
1199
|
+
target=target_sym.canonical_id,
|
|
1200
|
+
relationship="DISPATCHES_FORWARD",
|
|
1201
|
+
confidence=conf,
|
|
1202
|
+
file=call.source_file,
|
|
1203
|
+
start_line=call.line,
|
|
1204
|
+
end_line=call.end_line,
|
|
1205
|
+
evidence=f"Resolved PyTorch forward dispatch {caller_id} -> {target_sym.canonical_id} at {call.source_file}:{call.line} ({reason})",
|
|
1206
|
+
evidence_class=fwd_ev_cls,
|
|
1207
|
+
reason="pytorch_forward_dispatch",
|
|
1208
|
+
)
|
|
1209
|
+
)
|
|
1081
1210
|
elif "[" in expr and "]" in expr:
|
|
1082
1211
|
dict_part, raw_k = expr.split("[", 1)
|
|
1083
1212
|
raw_k = raw_k.rstrip("]")
|
|
@@ -1562,6 +1691,101 @@ class ReferenceResolver:
|
|
|
1562
1691
|
reason=edge_reason,
|
|
1563
1692
|
)
|
|
1564
1693
|
)
|
|
1694
|
+
# Django signal sender linking: @receiver(signal, sender=Model)
|
|
1695
|
+
sender_val = metadata.get("sender")
|
|
1696
|
+
if sender_val:
|
|
1697
|
+
sender_str = str(sender_val)
|
|
1698
|
+
sender_sym, _, _ = self.resolve_identifier_in_file(
|
|
1699
|
+
sender_str, b.file_path, use_line=b.line
|
|
1700
|
+
)
|
|
1701
|
+
if not sender_sym and "." in sender_str:
|
|
1702
|
+
sender_sym, _, _ = self.resolve_identifier_in_file(
|
|
1703
|
+
sender_str.split(".")[-1], b.file_path, use_line=b.line
|
|
1704
|
+
)
|
|
1705
|
+
if not sender_sym:
|
|
1706
|
+
short_target = sender_str.split(".")[-1]
|
|
1707
|
+
if short_target in self.short_to_symbols:
|
|
1708
|
+
for s in self.short_to_symbols[short_target]:
|
|
1709
|
+
if s.kind == "class":
|
|
1710
|
+
sender_sym = s
|
|
1711
|
+
break
|
|
1712
|
+
if not sender_sym:
|
|
1713
|
+
sender_sym = self.short_to_symbols[short_target][0]
|
|
1714
|
+
|
|
1715
|
+
sender_canon = sender_sym.canonical_id if sender_sym else sender_str
|
|
1716
|
+
sig_evidence = f"Django signal '{ev_str}' from sender '{sender_str}' triggers listener '{target_canon}' via '@{b.source_expr}'"
|
|
1717
|
+
add_ref(
|
|
1718
|
+
Reference(
|
|
1719
|
+
source_symbol_id=sender_canon,
|
|
1720
|
+
target_symbol_id=target_canon,
|
|
1721
|
+
relationship="TRIGGERS_SIGNAL",
|
|
1722
|
+
confidence=edge_conf,
|
|
1723
|
+
path=b.file_path,
|
|
1724
|
+
start_line=b.line,
|
|
1725
|
+
end_line=b.line,
|
|
1726
|
+
evidence=sig_evidence,
|
|
1727
|
+
source_hash=f_hash,
|
|
1728
|
+
indexed_commit=self.indexed_commit,
|
|
1729
|
+
)
|
|
1730
|
+
)
|
|
1731
|
+
add_edge(
|
|
1732
|
+
GraphEdge(
|
|
1733
|
+
source=sender_canon,
|
|
1734
|
+
target=target_canon,
|
|
1735
|
+
relationship="TRIGGERS_SIGNAL",
|
|
1736
|
+
confidence=edge_conf,
|
|
1737
|
+
file=b.file_path,
|
|
1738
|
+
start_line=b.line,
|
|
1739
|
+
end_line=b.line,
|
|
1740
|
+
evidence=sig_evidence,
|
|
1741
|
+
evidence_class=ev_class.value,
|
|
1742
|
+
reason=f"django_signal:{ev_str}",
|
|
1743
|
+
)
|
|
1744
|
+
)
|
|
1745
|
+
lifecycle_method = "delete" if "delete" in ev_str.lower() else "save"
|
|
1746
|
+
save_source = f"{sender_canon}.{lifecycle_method}"
|
|
1747
|
+
add_edge(
|
|
1748
|
+
GraphEdge(
|
|
1749
|
+
source=save_source,
|
|
1750
|
+
target=target_canon,
|
|
1751
|
+
relationship="TRIGGERS_SIGNAL",
|
|
1752
|
+
confidence=edge_conf,
|
|
1753
|
+
file=b.file_path,
|
|
1754
|
+
start_line=b.line,
|
|
1755
|
+
end_line=b.line,
|
|
1756
|
+
evidence=sig_evidence,
|
|
1757
|
+
evidence_class=ev_class.value,
|
|
1758
|
+
reason=f"django_signal:{ev_str}",
|
|
1759
|
+
)
|
|
1760
|
+
)
|
|
1761
|
+
add_edge(
|
|
1762
|
+
GraphEdge(
|
|
1763
|
+
source=save_source,
|
|
1764
|
+
target=target_canon,
|
|
1765
|
+
relationship="CALLS",
|
|
1766
|
+
confidence=edge_conf,
|
|
1767
|
+
file=b.file_path,
|
|
1768
|
+
start_line=b.line,
|
|
1769
|
+
end_line=b.line,
|
|
1770
|
+
evidence=sig_evidence,
|
|
1771
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
1772
|
+
reason=f"django_signal:{ev_str}",
|
|
1773
|
+
)
|
|
1774
|
+
)
|
|
1775
|
+
add_edge(
|
|
1776
|
+
GraphEdge(
|
|
1777
|
+
source=target_canon,
|
|
1778
|
+
target=sender_canon,
|
|
1779
|
+
relationship="HANDLES_SIGNAL",
|
|
1780
|
+
confidence=edge_conf,
|
|
1781
|
+
file=b.file_path,
|
|
1782
|
+
start_line=b.line,
|
|
1783
|
+
end_line=b.line,
|
|
1784
|
+
evidence=f"Listener '{target_canon}' handles '{ev_str}' from '{sender_canon}'",
|
|
1785
|
+
evidence_class=ev_class.value,
|
|
1786
|
+
reason=f"django_signal:{ev_str}",
|
|
1787
|
+
)
|
|
1788
|
+
)
|
|
1565
1789
|
else:
|
|
1566
1790
|
src_id = b.source_expr
|
|
1567
1791
|
ev_evidence = f"Event listener '{target_canon}' registered via '@{b.source_expr}'"
|
|
@@ -1692,164 +1916,568 @@ class ReferenceResolver:
|
|
|
1692
1916
|
)
|
|
1693
1917
|
)
|
|
1694
1918
|
|
|
1695
|
-
|
|
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}"
|
|
1919
|
+
elif rel == "TOOL_HANDLER":
|
|
1920
|
+
source_id = f"@{b.source_expr}"
|
|
1921
|
+
ev_evidence = f"AI agent tool '{target_canon}' registered via '@{b.source_expr}'"
|
|
1800
1922
|
add_ref(
|
|
1801
1923
|
Reference(
|
|
1802
|
-
source_symbol_id=
|
|
1803
|
-
target_symbol_id=
|
|
1804
|
-
relationship=
|
|
1924
|
+
source_symbol_id=target_canon,
|
|
1925
|
+
target_symbol_id=source_id,
|
|
1926
|
+
relationship=rel,
|
|
1805
1927
|
confidence=edge_conf,
|
|
1806
1928
|
path=b.file_path,
|
|
1807
1929
|
start_line=b.line,
|
|
1808
1930
|
end_line=b.line,
|
|
1809
|
-
evidence=
|
|
1931
|
+
evidence=ev_evidence,
|
|
1810
1932
|
source_hash=f_hash,
|
|
1811
1933
|
indexed_commit=self.indexed_commit,
|
|
1812
1934
|
)
|
|
1813
1935
|
)
|
|
1814
1936
|
add_edge(
|
|
1815
1937
|
GraphEdge(
|
|
1816
|
-
source=
|
|
1817
|
-
target=
|
|
1818
|
-
relationship=
|
|
1938
|
+
source=target_canon,
|
|
1939
|
+
target=source_id,
|
|
1940
|
+
relationship=rel,
|
|
1819
1941
|
confidence=edge_conf,
|
|
1820
1942
|
file=b.file_path,
|
|
1821
1943
|
start_line=b.line,
|
|
1822
1944
|
end_line=b.line,
|
|
1823
|
-
evidence=
|
|
1945
|
+
evidence=ev_evidence,
|
|
1824
1946
|
evidence_class=ev_class.value,
|
|
1825
1947
|
reason=edge_reason,
|
|
1826
1948
|
)
|
|
1827
1949
|
)
|
|
1828
1950
|
|
|
1829
|
-
#
|
|
1830
|
-
for b in bindings_by_kind.get("
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1951
|
+
# 8b. Django Signal Receivers via connect()
|
|
1952
|
+
for b in bindings_by_kind.get("DJANGO_SIGNAL_RECEIVER", ()):
|
|
1953
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
1954
|
+
receiver_sym, _, _ = self.resolve_identifier_in_file(
|
|
1955
|
+
b.target_name, b.file_path, use_line=b.line
|
|
1956
|
+
)
|
|
1957
|
+
if not receiver_sym and b.target_name in self.short_to_symbols:
|
|
1958
|
+
receiver_sym = self.short_to_symbols[b.target_name][0]
|
|
1959
|
+
target_canon = receiver_sym.canonical_id if receiver_sym else b.target_name
|
|
1960
|
+
|
|
1961
|
+
sig_name = b.base_expr
|
|
1962
|
+
sender_str = b.source_expr
|
|
1963
|
+
sender_sym = None
|
|
1964
|
+
if sender_str:
|
|
1965
|
+
sender_sym, _, _ = self.resolve_identifier_in_file(
|
|
1966
|
+
sender_str, b.file_path, use_line=b.line
|
|
1967
|
+
)
|
|
1968
|
+
if not sender_sym and "." in sender_str:
|
|
1969
|
+
sender_sym, _, _ = self.resolve_identifier_in_file(
|
|
1970
|
+
sender_str.split(".")[-1], b.file_path, use_line=b.line
|
|
1971
|
+
)
|
|
1972
|
+
if not sender_sym:
|
|
1973
|
+
short_s = sender_str.split(".")[-1]
|
|
1974
|
+
if short_s in self.short_to_symbols:
|
|
1975
|
+
for s in self.short_to_symbols[short_s]:
|
|
1976
|
+
if s.kind == "class":
|
|
1977
|
+
sender_sym = s
|
|
1978
|
+
break
|
|
1979
|
+
if not sender_sym:
|
|
1980
|
+
sender_sym = self.short_to_symbols[short_s][0]
|
|
1840
1981
|
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1982
|
+
sender_canon = sender_sym.canonical_id if sender_sym else (sender_str or sig_name)
|
|
1983
|
+
sig_ev = f"Signal '{sig_name}' connects sender '{sender_str or '*'}' to '{target_canon}'"
|
|
1984
|
+
add_ref(
|
|
1985
|
+
Reference(
|
|
1986
|
+
source_symbol_id=sender_canon,
|
|
1987
|
+
target_symbol_id=target_canon,
|
|
1988
|
+
relationship="TRIGGERS_SIGNAL",
|
|
1989
|
+
confidence="HIGH" if sender_sym else "MEDIUM",
|
|
1990
|
+
path=b.file_path,
|
|
1991
|
+
start_line=b.line,
|
|
1992
|
+
end_line=b.line,
|
|
1993
|
+
evidence=sig_ev,
|
|
1994
|
+
source_hash=f_hash,
|
|
1995
|
+
indexed_commit=self.indexed_commit,
|
|
1996
|
+
)
|
|
1997
|
+
)
|
|
1998
|
+
add_edge(
|
|
1999
|
+
GraphEdge(
|
|
2000
|
+
source=sender_canon,
|
|
2001
|
+
target=target_canon,
|
|
2002
|
+
relationship="TRIGGERS_SIGNAL",
|
|
2003
|
+
confidence="HIGH" if sender_sym else "MEDIUM",
|
|
2004
|
+
file=b.file_path,
|
|
2005
|
+
start_line=b.line,
|
|
2006
|
+
end_line=b.line,
|
|
2007
|
+
evidence=sig_ev,
|
|
2008
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2009
|
+
reason=f"django_signal:{sig_name}",
|
|
2010
|
+
)
|
|
2011
|
+
)
|
|
2012
|
+
lifecycle_method = "delete" if "delete" in sig_name.lower() else "save"
|
|
2013
|
+
add_edge(
|
|
2014
|
+
GraphEdge(
|
|
2015
|
+
source=f"{sender_canon}.{lifecycle_method}",
|
|
2016
|
+
target=target_canon,
|
|
2017
|
+
relationship="CALLS",
|
|
2018
|
+
confidence="HIGH" if sender_sym else "MEDIUM",
|
|
2019
|
+
file=b.file_path,
|
|
2020
|
+
start_line=b.line,
|
|
2021
|
+
end_line=b.line,
|
|
2022
|
+
evidence=sig_ev,
|
|
2023
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2024
|
+
reason=f"django_signal:{sig_name}",
|
|
2025
|
+
)
|
|
2026
|
+
)
|
|
2027
|
+
add_edge(
|
|
2028
|
+
GraphEdge(
|
|
2029
|
+
source=target_canon,
|
|
2030
|
+
target=sender_canon,
|
|
2031
|
+
relationship="HANDLES_SIGNAL",
|
|
2032
|
+
confidence="HIGH" if sender_sym else "MEDIUM",
|
|
2033
|
+
file=b.file_path,
|
|
2034
|
+
start_line=b.line,
|
|
2035
|
+
end_line=b.line,
|
|
2036
|
+
evidence=f"Listener '{target_canon}' handles '{sig_name}' from '{sender_canon}'",
|
|
2037
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2038
|
+
reason=f"django_signal:{sig_name}",
|
|
2039
|
+
)
|
|
2040
|
+
)
|
|
1844
2041
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2042
|
+
# 8c. Django Signal Dispatches via send() / send_robust()
|
|
2043
|
+
for b in bindings_by_kind.get("DJANGO_SIGNAL_SEND", ()):
|
|
2044
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2045
|
+
sig_name = b.target_name
|
|
2046
|
+
for b_rcv in bindings_by_kind.get("DJANGO_SIGNAL_RECEIVER", ()):
|
|
2047
|
+
if b_rcv.base_expr == sig_name or sig_name.endswith(b_rcv.base_expr):
|
|
2048
|
+
rcv_sym, _, _ = self.resolve_identifier_in_file(b_rcv.target_name, b_rcv.file_path, use_line=b_rcv.line)
|
|
2049
|
+
if not rcv_sym and b_rcv.target_name in self.short_to_symbols:
|
|
2050
|
+
rcv_sym = self.short_to_symbols[b_rcv.target_name][0]
|
|
2051
|
+
target_id = rcv_sym.canonical_id if rcv_sym else b_rcv.target_name
|
|
2052
|
+
ev_str = f"Caller dispatches signal '{sig_name}' triggering '{target_id}'"
|
|
2053
|
+
add_edge(
|
|
2054
|
+
GraphEdge(
|
|
2055
|
+
source=caller_id,
|
|
2056
|
+
target=target_id,
|
|
2057
|
+
relationship="TRIGGERS_SIGNAL",
|
|
2058
|
+
confidence="HIGH",
|
|
2059
|
+
file=b.file_path,
|
|
2060
|
+
start_line=b.line,
|
|
2061
|
+
end_line=b.line,
|
|
2062
|
+
evidence=ev_str,
|
|
2063
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2064
|
+
reason=f"signal_send:{sig_name}",
|
|
2065
|
+
)
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
# 8d. Celery Task Dispatches via .delay() / .apply_async() / send_task()
|
|
2069
|
+
for b in bindings_by_kind.get("CELERY_DISPATCH", ()):
|
|
2070
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2071
|
+
task_name = b.target_name
|
|
2072
|
+
task_sym, _, _ = self.resolve_identifier_in_file(task_name, b.file_path, use_line=b.line)
|
|
2073
|
+
if not task_sym and task_name in self.short_to_symbols:
|
|
2074
|
+
task_sym = self.short_to_symbols[task_name][0]
|
|
2075
|
+
target_id = task_sym.canonical_id if task_sym else task_name
|
|
2076
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2077
|
+
ev_str = f"Caller dispatches background Celery task '{task_name}' via .{b.base_expr}() at {b.file_path}:{b.line}"
|
|
2078
|
+
add_ref(
|
|
2079
|
+
Reference(
|
|
2080
|
+
source_symbol_id=caller_id,
|
|
2081
|
+
target_symbol_id=target_id,
|
|
2082
|
+
relationship="DISPATCHES_TASK",
|
|
2083
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2084
|
+
path=b.file_path,
|
|
2085
|
+
start_line=b.line,
|
|
2086
|
+
end_line=b.line,
|
|
2087
|
+
evidence=ev_str,
|
|
2088
|
+
source_hash=f_hash,
|
|
2089
|
+
indexed_commit=self.indexed_commit,
|
|
2090
|
+
)
|
|
2091
|
+
)
|
|
2092
|
+
add_edge(
|
|
2093
|
+
GraphEdge(
|
|
2094
|
+
source=caller_id,
|
|
2095
|
+
target=target_id,
|
|
2096
|
+
relationship="DISPATCHES_TASK",
|
|
2097
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2098
|
+
file=b.file_path,
|
|
2099
|
+
start_line=b.line,
|
|
2100
|
+
end_line=b.line,
|
|
2101
|
+
evidence=ev_str,
|
|
2102
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2103
|
+
reason=f"celery_dispatch:{b.base_expr}",
|
|
2104
|
+
)
|
|
2105
|
+
)
|
|
2106
|
+
add_edge(
|
|
2107
|
+
GraphEdge(
|
|
2108
|
+
source=caller_id,
|
|
2109
|
+
target=target_id,
|
|
2110
|
+
relationship="CALLS",
|
|
2111
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2112
|
+
file=b.file_path,
|
|
2113
|
+
start_line=b.line,
|
|
2114
|
+
end_line=b.line,
|
|
2115
|
+
evidence=ev_str,
|
|
2116
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2117
|
+
reason=f"celery_dispatch:{b.base_expr}",
|
|
2118
|
+
)
|
|
2119
|
+
)
|
|
2120
|
+
|
|
2121
|
+
# 8e. Ray Remote Task Dispatches via .remote()
|
|
2122
|
+
for b in bindings_by_kind.get("RAY_REMOTE", ()):
|
|
2123
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2124
|
+
task_name = b.target_name
|
|
2125
|
+
task_sym, _, _ = self.resolve_identifier_in_file(task_name, b.file_path, use_line=b.line)
|
|
2126
|
+
if not task_sym and task_name in self.short_to_symbols:
|
|
2127
|
+
task_sym = self.short_to_symbols[task_name][0]
|
|
2128
|
+
target_id = task_sym.canonical_id if task_sym else task_name
|
|
2129
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2130
|
+
ev_str = f"Caller dispatches remote Ray task '{task_name}' via .{b.base_expr}() at {b.file_path}:{b.line}"
|
|
2131
|
+
add_ref(
|
|
2132
|
+
Reference(
|
|
2133
|
+
source_symbol_id=caller_id,
|
|
2134
|
+
target_symbol_id=target_id,
|
|
2135
|
+
relationship="DISPATCHES_TASK",
|
|
2136
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2137
|
+
path=b.file_path,
|
|
2138
|
+
start_line=b.line,
|
|
2139
|
+
end_line=b.line,
|
|
2140
|
+
evidence=ev_str,
|
|
2141
|
+
source_hash=f_hash,
|
|
2142
|
+
indexed_commit=self.indexed_commit,
|
|
2143
|
+
)
|
|
2144
|
+
)
|
|
2145
|
+
add_edge(
|
|
2146
|
+
GraphEdge(
|
|
2147
|
+
source=caller_id,
|
|
2148
|
+
target=target_id,
|
|
2149
|
+
relationship="DISPATCHES_TASK",
|
|
2150
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2151
|
+
file=b.file_path,
|
|
2152
|
+
start_line=b.line,
|
|
2153
|
+
end_line=b.line,
|
|
2154
|
+
evidence=ev_str,
|
|
2155
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2156
|
+
reason=f"ray_remote:{b.base_expr}",
|
|
2157
|
+
)
|
|
2158
|
+
)
|
|
2159
|
+
add_edge(
|
|
2160
|
+
GraphEdge(
|
|
2161
|
+
source=caller_id,
|
|
2162
|
+
target=target_id,
|
|
2163
|
+
relationship="CALLS",
|
|
2164
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2165
|
+
file=b.file_path,
|
|
2166
|
+
start_line=b.line,
|
|
2167
|
+
end_line=b.line,
|
|
2168
|
+
evidence=ev_str,
|
|
2169
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2170
|
+
reason=f"ray_remote:{b.base_expr}",
|
|
2171
|
+
)
|
|
2172
|
+
)
|
|
2173
|
+
|
|
2174
|
+
# 8f. Agent Tool Registrations (e.g. AgentExecutor(tools=[...]))
|
|
2175
|
+
for b in bindings_by_kind.get("AGENT_TOOL_REGISTRATION", ()):
|
|
2176
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2177
|
+
tool_name = b.target_name
|
|
2178
|
+
tool_sym, _, _ = self.resolve_identifier_in_file(tool_name, b.file_path, use_line=b.line)
|
|
2179
|
+
if not tool_sym and tool_name in self.short_to_symbols:
|
|
2180
|
+
tool_sym = self.short_to_symbols[tool_name][0]
|
|
2181
|
+
target_id = tool_sym.canonical_id if tool_sym else tool_name
|
|
2182
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2183
|
+
ev_str = f"Agent '{b.base_expr}' registers tool '{tool_name}' at {b.file_path}:{b.line}"
|
|
2184
|
+
add_ref(
|
|
2185
|
+
Reference(
|
|
2186
|
+
source_symbol_id=caller_id,
|
|
2187
|
+
target_symbol_id=target_id,
|
|
2188
|
+
relationship="REGISTERS",
|
|
2189
|
+
confidence="HIGH" if tool_sym else "MEDIUM",
|
|
2190
|
+
path=b.file_path,
|
|
2191
|
+
start_line=b.line,
|
|
2192
|
+
end_line=b.line,
|
|
2193
|
+
evidence=ev_str,
|
|
2194
|
+
source_hash=f_hash,
|
|
2195
|
+
indexed_commit=self.indexed_commit,
|
|
2196
|
+
)
|
|
2197
|
+
)
|
|
2198
|
+
add_edge(
|
|
2199
|
+
GraphEdge(
|
|
2200
|
+
source=caller_id,
|
|
2201
|
+
target=target_id,
|
|
2202
|
+
relationship="REGISTERS",
|
|
2203
|
+
confidence="HIGH" if tool_sym else "MEDIUM",
|
|
2204
|
+
file=b.file_path,
|
|
2205
|
+
start_line=b.line,
|
|
2206
|
+
end_line=b.line,
|
|
2207
|
+
evidence=ev_str,
|
|
2208
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2209
|
+
reason=f"agent_tool_registration:{b.base_expr}",
|
|
2210
|
+
)
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
# 8g. LCEL Pipeline Chains (prompt | llm | output_parser)
|
|
2214
|
+
for b in bindings_by_kind.get("LCEL_PIPELINE", ()):
|
|
2215
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2216
|
+
try:
|
|
2217
|
+
steps: list[str] = json.loads(b.source_expr)
|
|
2218
|
+
except Exception:
|
|
2219
|
+
steps = []
|
|
2220
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2221
|
+
step_syms: list[Symbol | None] = []
|
|
2222
|
+
for s_name in steps:
|
|
2223
|
+
s_sym, _, _ = self.resolve_identifier_in_file(
|
|
2224
|
+
s_name, b.file_path, caller_canonical_id=caller_id, use_line=b.line
|
|
2225
|
+
)
|
|
2226
|
+
if not s_sym and self.binding_resolver:
|
|
2227
|
+
alias_t, _, _ = self.binding_resolver.resolve(
|
|
2228
|
+
b.file_path, s_name, scope=b.scope, use_line=b.line
|
|
2229
|
+
)
|
|
2230
|
+
if alias_t and alias_t in self.canonical_to_symbol:
|
|
2231
|
+
s_sym = self.canonical_to_symbol[alias_t]
|
|
2232
|
+
elif alias_t:
|
|
2233
|
+
s_sym, _, _ = self.resolve_identifier_in_file(alias_t, b.file_path, use_line=b.line)
|
|
2234
|
+
if not s_sym and alias_t in self.short_to_symbols:
|
|
2235
|
+
s_sym = self.short_to_symbols[alias_t][0]
|
|
2236
|
+
if not s_sym and s_name in self.short_to_symbols:
|
|
2237
|
+
s_sym = self.short_to_symbols[s_name][0]
|
|
2238
|
+
step_syms.append(s_sym)
|
|
2239
|
+
|
|
2240
|
+
# Link consecutive pipeline steps: step[i] -> PIPELINE_STEP -> step[i+1]
|
|
2241
|
+
for i in range(len(steps) - 1):
|
|
2242
|
+
sym_from = step_syms[i]
|
|
2243
|
+
sym_to = step_syms[i + 1]
|
|
2244
|
+
from_id = sym_from.canonical_id if sym_from is not None else steps[i]
|
|
2245
|
+
to_id = sym_to.canonical_id if sym_to is not None else steps[i + 1]
|
|
2246
|
+
ev_str = f"LCEL pipeline step '{steps[i]}' -> '{steps[i+1]}' at {b.file_path}:{b.line}"
|
|
2247
|
+
add_ref(
|
|
2248
|
+
Reference(
|
|
2249
|
+
source_symbol_id=from_id,
|
|
2250
|
+
target_symbol_id=to_id,
|
|
2251
|
+
relationship="PIPELINE_STEP",
|
|
2252
|
+
confidence="HIGH" if (sym_from is not None and sym_to is not None) else "MEDIUM",
|
|
2253
|
+
path=b.file_path,
|
|
2254
|
+
start_line=b.line,
|
|
2255
|
+
end_line=b.line,
|
|
2256
|
+
evidence=ev_str,
|
|
2257
|
+
source_hash=f_hash,
|
|
2258
|
+
indexed_commit=self.indexed_commit,
|
|
2259
|
+
)
|
|
2260
|
+
)
|
|
2261
|
+
add_edge(
|
|
2262
|
+
GraphEdge(
|
|
2263
|
+
source=from_id,
|
|
2264
|
+
target=to_id,
|
|
2265
|
+
relationship="PIPELINE_STEP",
|
|
2266
|
+
confidence="HIGH" if (step_syms[i] and step_syms[i+1]) else "MEDIUM",
|
|
2267
|
+
file=b.file_path,
|
|
2268
|
+
start_line=b.line,
|
|
2269
|
+
end_line=b.line,
|
|
2270
|
+
evidence=ev_str,
|
|
2271
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2272
|
+
reason="lcel_pipe",
|
|
2273
|
+
)
|
|
2274
|
+
)
|
|
2275
|
+
|
|
2276
|
+
# 8h. Python View -> Template Rendering (Flask, Django, FastAPI)
|
|
2277
|
+
for b in bindings_by_kind.get("RENDERS_TEMPLATE", ()):
|
|
2278
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2279
|
+
raw_tmpl = b.target_name
|
|
2280
|
+
resolved_tmpl = raw_tmpl
|
|
2281
|
+
if raw_tmpl not in self.known_files:
|
|
2282
|
+
for kf in self.known_files:
|
|
2283
|
+
if kf.endswith(f"/{raw_tmpl}") or kf == raw_tmpl:
|
|
2284
|
+
resolved_tmpl = kf
|
|
2285
|
+
break
|
|
2286
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2287
|
+
ev_str = f"View renders HTML template '{raw_tmpl}' at {b.file_path}:{b.line}"
|
|
2288
|
+
add_ref(
|
|
2289
|
+
Reference(
|
|
2290
|
+
source_symbol_id=caller_id,
|
|
2291
|
+
target_symbol_id=resolved_tmpl,
|
|
2292
|
+
relationship="RENDERS_TEMPLATE",
|
|
2293
|
+
confidence="HIGH",
|
|
2294
|
+
path=b.file_path,
|
|
2295
|
+
start_line=b.line,
|
|
2296
|
+
end_line=b.line,
|
|
2297
|
+
evidence=ev_str,
|
|
2298
|
+
source_hash=f_hash,
|
|
2299
|
+
indexed_commit=self.indexed_commit,
|
|
2300
|
+
)
|
|
2301
|
+
)
|
|
2302
|
+
add_edge(
|
|
2303
|
+
GraphEdge(
|
|
2304
|
+
source=caller_id,
|
|
2305
|
+
target=resolved_tmpl,
|
|
2306
|
+
relationship="RENDERS_TEMPLATE",
|
|
2307
|
+
confidence="HIGH",
|
|
2308
|
+
file=b.file_path,
|
|
2309
|
+
start_line=b.line,
|
|
2310
|
+
end_line=b.line,
|
|
2311
|
+
evidence=ev_str,
|
|
2312
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2313
|
+
reason=f"template_render:{raw_tmpl}",
|
|
2314
|
+
)
|
|
2315
|
+
)
|
|
2316
|
+
|
|
2317
|
+
# 9. Dependency Injection & Configuration Intelligence
|
|
2318
|
+
# 9a. Statically grounded provider registration (PROVIDES)
|
|
2319
|
+
provider_map: dict[str, list[tuple[str, BindingRef, str]]] = {}
|
|
2320
|
+
for b in self.bindings:
|
|
2321
|
+
if b.expr_kind == "SEMANTIC_DECORATOR" and b.attr_name == "PROVIDES":
|
|
2322
|
+
if b.subscript_target is not None:
|
|
2323
|
+
findings.append({
|
|
2324
|
+
"pattern": "DI_PROVIDES",
|
|
2325
|
+
"status": "UNKNOWN",
|
|
2326
|
+
"reason": b.subscript_target or "dynamic_provider",
|
|
2327
|
+
"file": b.file_path,
|
|
2328
|
+
"line": b.line,
|
|
2329
|
+
"target_name": b.target_name,
|
|
2330
|
+
})
|
|
2331
|
+
continue
|
|
2332
|
+
dec_meta: dict[str, object] = {}
|
|
2333
|
+
if b.subscript_key:
|
|
2334
|
+
try:
|
|
2335
|
+
dec_meta = json.loads(b.subscript_key)
|
|
2336
|
+
except Exception:
|
|
2337
|
+
dec_meta = {}
|
|
2338
|
+
iface_raw = str(dec_meta.get("interface") or "")
|
|
2339
|
+
impl_canon = b.target_name
|
|
2340
|
+
iface_canon = impl_canon
|
|
2341
|
+
if iface_raw and iface_raw != impl_canon.split(".")[-1]:
|
|
2342
|
+
if_sym, _, _ = self.resolve_identifier_in_file(iface_raw, b.file_path, use_line=b.line)
|
|
2343
|
+
if not if_sym and len(self.short_to_symbols.get(iface_raw, [])) == 1:
|
|
2344
|
+
if_sym = self.short_to_symbols[iface_raw][0]
|
|
2345
|
+
if if_sym:
|
|
2346
|
+
iface_canon = if_sym.canonical_id
|
|
2347
|
+
provider_map.setdefault(iface_canon, []).append((impl_canon, b, "decorator_provider"))
|
|
2348
|
+
continue
|
|
2349
|
+
|
|
2350
|
+
if b.expr_kind != "DI_PROVIDES":
|
|
2351
|
+
continue
|
|
2352
|
+
|
|
2353
|
+
if (
|
|
2354
|
+
b.subscript_target == "dynamic_provider"
|
|
2355
|
+
or b.target_name == "<DYNAMIC>"
|
|
2356
|
+
or b.source_expr == "<DYNAMIC>"
|
|
2357
|
+
):
|
|
2358
|
+
findings.append({
|
|
2359
|
+
"pattern": "DI_PROVIDES",
|
|
2360
|
+
"status": "UNKNOWN",
|
|
2361
|
+
"reason": "dynamic_provider",
|
|
2362
|
+
"file": b.file_path,
|
|
2363
|
+
"line": b.line,
|
|
2364
|
+
"target_name": b.target_name,
|
|
2365
|
+
})
|
|
2366
|
+
continue
|
|
2367
|
+
|
|
2368
|
+
iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
|
|
2369
|
+
if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
|
|
2370
|
+
iface_sym = self.short_to_symbols[b.target_name][0]
|
|
2371
|
+
|
|
2372
|
+
impl_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2373
|
+
if not impl_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2374
|
+
impl_sym = self.short_to_symbols[b.source_expr][0]
|
|
2375
|
+
|
|
2376
|
+
if not iface_sym and b.base_expr == "nestjs_provider" and impl_sym:
|
|
2377
|
+
iface_canon = b.target_name
|
|
2378
|
+
impl_canon = impl_sym.canonical_id
|
|
2379
|
+
provider_map.setdefault(iface_canon, []).append((impl_canon, b, "nestjs_provider"))
|
|
2380
|
+
continue
|
|
2381
|
+
|
|
2382
|
+
if not iface_sym or not impl_sym:
|
|
2383
|
+
findings.append({
|
|
2384
|
+
"pattern": "DI_PROVIDES",
|
|
2385
|
+
"status": "UNKNOWN",
|
|
2386
|
+
"reason": "dynamic_provider",
|
|
2387
|
+
"file": b.file_path,
|
|
2388
|
+
"line": b.line,
|
|
2389
|
+
"target_name": b.target_name,
|
|
2390
|
+
"source_expr": b.source_expr,
|
|
2391
|
+
})
|
|
2392
|
+
continue
|
|
2393
|
+
|
|
2394
|
+
iface_canon = iface_sym.canonical_id
|
|
2395
|
+
impl_canon = impl_sym.canonical_id
|
|
2396
|
+
# For 1-arg container.provide(Impl) / container.singleton(Impl), check if Impl extends a base interface
|
|
2397
|
+
if iface_canon == impl_canon and b.target_name == b.source_expr:
|
|
2398
|
+
for inh in inheritance_by_source.get(impl_canon, ()):
|
|
2399
|
+
base_sym, _, _ = self.resolve_identifier_in_file(inh.base_name, inh.source_file, use_line=inh.line)
|
|
2400
|
+
if not base_sym and len(self.short_to_symbols.get(inh.base_name, [])) == 1:
|
|
2401
|
+
base_sym = self.short_to_symbols[inh.base_name][0]
|
|
2402
|
+
if base_sym:
|
|
2403
|
+
iface_canon = base_sym.canonical_id
|
|
2404
|
+
break
|
|
2405
|
+
provider_map.setdefault(iface_canon, []).append((impl_canon, b, "provider_mapping"))
|
|
2406
|
+
|
|
2407
|
+
# Emit PROVIDES edges (Ambiguity: multiple distinct providers -> POSSIBLE)
|
|
2408
|
+
for iface_canon, reg_list in provider_map.items():
|
|
2409
|
+
distinct_impls = set(impl_c for impl_c, _, _ in reg_list)
|
|
2410
|
+
is_ambiguous = len(distinct_impls) > 1
|
|
2411
|
+
|
|
2412
|
+
for impl_canon, b, _ in reg_list:
|
|
2413
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2414
|
+
if is_ambiguous:
|
|
2415
|
+
ev_class = RelationshipEvidenceClass.POSSIBLE
|
|
2416
|
+
edge_conf = "LOW"
|
|
2417
|
+
edge_reason = "multiple_providers"
|
|
2418
|
+
elif b.is_conditional:
|
|
2419
|
+
ev_class = RelationshipEvidenceClass.POSSIBLE
|
|
2420
|
+
edge_conf = "LOW"
|
|
2421
|
+
edge_reason = "conditional_provider"
|
|
2422
|
+
else:
|
|
2423
|
+
ev_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
|
|
2424
|
+
edge_conf = "HIGH"
|
|
2425
|
+
edge_reason = f"Statically verified provider mapping '{b.target_name}' -> '{impl_canon}'"
|
|
2426
|
+
|
|
2427
|
+
ev_str = f"Provider mapping: {iface_canon} PROVIDES {impl_canon} at {b.file_path}:{b.line}"
|
|
2428
|
+
add_ref(
|
|
2429
|
+
Reference(
|
|
2430
|
+
source_symbol_id=iface_canon,
|
|
2431
|
+
target_symbol_id=impl_canon,
|
|
2432
|
+
relationship="PROVIDES",
|
|
2433
|
+
confidence=edge_conf,
|
|
2434
|
+
path=b.file_path,
|
|
2435
|
+
start_line=b.line,
|
|
2436
|
+
end_line=b.line,
|
|
2437
|
+
evidence=ev_str,
|
|
2438
|
+
source_hash=f_hash,
|
|
2439
|
+
indexed_commit=self.indexed_commit,
|
|
2440
|
+
)
|
|
2441
|
+
)
|
|
2442
|
+
add_edge(
|
|
2443
|
+
GraphEdge(
|
|
2444
|
+
source=iface_canon,
|
|
2445
|
+
target=impl_canon,
|
|
2446
|
+
relationship="PROVIDES",
|
|
2447
|
+
confidence=edge_conf,
|
|
2448
|
+
file=b.file_path,
|
|
2449
|
+
start_line=b.line,
|
|
2450
|
+
end_line=b.line,
|
|
2451
|
+
evidence=ev_str,
|
|
2452
|
+
evidence_class=ev_class.value,
|
|
2453
|
+
reason=edge_reason,
|
|
2454
|
+
)
|
|
2455
|
+
)
|
|
2456
|
+
|
|
2457
|
+
# 9b. Dependency Resolution (RESOLVES_DEPENDENCY)
|
|
2458
|
+
for b in bindings_by_kind.get("DI_RESOLVES", ()):
|
|
2459
|
+
if b.subscript_target == "dynamic_provider" or b.target_name == "<DYNAMIC>":
|
|
2460
|
+
findings.append({
|
|
2461
|
+
"pattern": "DI_RESOLVES",
|
|
2462
|
+
"status": "UNKNOWN",
|
|
2463
|
+
"reason": "dynamic_provider",
|
|
2464
|
+
"file": b.file_path,
|
|
2465
|
+
"line": b.line,
|
|
2466
|
+
})
|
|
2467
|
+
continue
|
|
2468
|
+
|
|
2469
|
+
iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
|
|
2470
|
+
if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
|
|
2471
|
+
iface_sym = self.short_to_symbols[b.target_name][0]
|
|
2472
|
+
|
|
2473
|
+
if not iface_sym:
|
|
2474
|
+
findings.append({
|
|
2475
|
+
"pattern": "DI_RESOLVES",
|
|
2476
|
+
"status": "UNKNOWN",
|
|
2477
|
+
"reason": "unknown_dependency_interface",
|
|
2478
|
+
"file": b.file_path,
|
|
2479
|
+
"line": b.line,
|
|
2480
|
+
"target_name": b.target_name,
|
|
1853
2481
|
})
|
|
1854
2482
|
continue
|
|
1855
2483
|
|
|
@@ -1921,6 +2549,130 @@ class ReferenceResolver:
|
|
|
1921
2549
|
)
|
|
1922
2550
|
|
|
1923
2551
|
# 9c. FastAPI Depends(...) Injection (INJECTS)
|
|
2552
|
+
# 1. Collect modern FastAPI Annotated aliases (e.g. SessionDep = Annotated[Session, Depends(get_db)])
|
|
2553
|
+
alias_to_dep: dict[str, str] = {}
|
|
2554
|
+
alias_name_to_dep: dict[str, str] = {}
|
|
2555
|
+
for b in bindings_by_kind.get("DI_ALIAS_INJECTS", []):
|
|
2556
|
+
dep_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2557
|
+
if not dep_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2558
|
+
dep_sym = self.short_to_symbols[b.source_expr][0]
|
|
2559
|
+
if dep_sym:
|
|
2560
|
+
dep_canon = dep_sym.canonical_id
|
|
2561
|
+
alias_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
|
|
2562
|
+
alias_canon = alias_sym.canonical_id if alias_sym else f"{b.file_path}:{b.target_name}"
|
|
2563
|
+
alias_to_dep[alias_canon] = dep_canon
|
|
2564
|
+
alias_name_to_dep[b.target_name] = dep_canon
|
|
2565
|
+
ev_str = f"FastAPI Annotated Alias: '{alias_canon}' resolves to Depends({dep_canon}) at {b.file_path}:{b.line}"
|
|
2566
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2567
|
+
add_ref(
|
|
2568
|
+
Reference(
|
|
2569
|
+
source_symbol_id=alias_canon,
|
|
2570
|
+
target_symbol_id=dep_canon,
|
|
2571
|
+
relationship="DEPENDS_ON",
|
|
2572
|
+
confidence="HIGH",
|
|
2573
|
+
path=b.file_path,
|
|
2574
|
+
start_line=b.line,
|
|
2575
|
+
end_line=b.line,
|
|
2576
|
+
evidence=ev_str,
|
|
2577
|
+
source_hash=f_hash,
|
|
2578
|
+
indexed_commit=self.indexed_commit,
|
|
2579
|
+
)
|
|
2580
|
+
)
|
|
2581
|
+
add_edge(
|
|
2582
|
+
GraphEdge(
|
|
2583
|
+
source=alias_canon,
|
|
2584
|
+
target=dep_canon,
|
|
2585
|
+
relationship="DEPENDS_ON",
|
|
2586
|
+
confidence="HIGH",
|
|
2587
|
+
file=b.file_path,
|
|
2588
|
+
start_line=b.line,
|
|
2589
|
+
end_line=b.line,
|
|
2590
|
+
evidence=ev_str,
|
|
2591
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2592
|
+
reason="fastapi_annotated_alias",
|
|
2593
|
+
)
|
|
2594
|
+
)
|
|
2595
|
+
|
|
2596
|
+
# 2. Resolve function parameters typed with Annotated aliases (e.g. session: SessionDep, current_user: CurrentUser)
|
|
2597
|
+
for b in bindings_by_kind.get("PARAM_TYPE_DI", []):
|
|
2598
|
+
param_type = b.source_expr.split("[")[0].strip().split(".")[-1]
|
|
2599
|
+
dep_injected_canon: str | None = alias_name_to_dep.get(param_type)
|
|
2600
|
+
if not dep_injected_canon:
|
|
2601
|
+
type_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2602
|
+
if type_sym and type_sym.canonical_id in alias_to_dep:
|
|
2603
|
+
dep_injected_canon = alias_to_dep[type_sym.canonical_id]
|
|
2604
|
+
elif type_sym and type_sym.name in alias_name_to_dep:
|
|
2605
|
+
dep_injected_canon = alias_name_to_dep[type_sym.name]
|
|
2606
|
+
if dep_injected_canon:
|
|
2607
|
+
handler_canon = b.target_name
|
|
2608
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2609
|
+
ev_str = f"FastAPI Depends (via {param_type}): '{handler_canon}' INJECTS '{dep_injected_canon}' at {b.file_path}:{b.line}"
|
|
2610
|
+
add_ref(
|
|
2611
|
+
Reference(
|
|
2612
|
+
source_symbol_id=handler_canon,
|
|
2613
|
+
target_symbol_id=dep_injected_canon,
|
|
2614
|
+
relationship="INJECTS",
|
|
2615
|
+
confidence="HIGH",
|
|
2616
|
+
path=b.file_path,
|
|
2617
|
+
start_line=b.line,
|
|
2618
|
+
end_line=b.line,
|
|
2619
|
+
evidence=ev_str,
|
|
2620
|
+
source_hash=f_hash,
|
|
2621
|
+
indexed_commit=self.indexed_commit,
|
|
2622
|
+
)
|
|
2623
|
+
)
|
|
2624
|
+
add_edge(
|
|
2625
|
+
GraphEdge(
|
|
2626
|
+
source=handler_canon,
|
|
2627
|
+
target=dep_injected_canon,
|
|
2628
|
+
relationship="INJECTS",
|
|
2629
|
+
confidence="HIGH",
|
|
2630
|
+
file=b.file_path,
|
|
2631
|
+
start_line=b.line,
|
|
2632
|
+
end_line=b.line,
|
|
2633
|
+
evidence=ev_str,
|
|
2634
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2635
|
+
reason="fastapi_annotated_alias_injects",
|
|
2636
|
+
)
|
|
2637
|
+
)
|
|
2638
|
+
handler_short = handler_canon.split(".")[-1]
|
|
2639
|
+
for rt in self.routes:
|
|
2640
|
+
matches_handler = (
|
|
2641
|
+
rt.handler_canonical_id == handler_canon
|
|
2642
|
+
or (rt.handler_name == handler_short and rt.file_path == b.file_path)
|
|
2643
|
+
)
|
|
2644
|
+
if matches_handler:
|
|
2645
|
+
rt_ev = f"Route endpoint '{rt.endpoint_id}' INJECTS '{dep_injected_canon}' (via {param_type}) at {rt.file_path}:{rt.line}"
|
|
2646
|
+
add_ref(
|
|
2647
|
+
Reference(
|
|
2648
|
+
source_symbol_id=rt.endpoint_id,
|
|
2649
|
+
target_symbol_id=dep_injected_canon,
|
|
2650
|
+
relationship="INJECTS",
|
|
2651
|
+
confidence="HIGH",
|
|
2652
|
+
path=rt.file_path,
|
|
2653
|
+
start_line=rt.line,
|
|
2654
|
+
end_line=rt.line,
|
|
2655
|
+
evidence=rt_ev,
|
|
2656
|
+
source_hash=f_hash,
|
|
2657
|
+
indexed_commit=self.indexed_commit,
|
|
2658
|
+
)
|
|
2659
|
+
)
|
|
2660
|
+
add_edge(
|
|
2661
|
+
GraphEdge(
|
|
2662
|
+
source=rt.endpoint_id,
|
|
2663
|
+
target=dep_injected_canon,
|
|
2664
|
+
relationship="INJECTS",
|
|
2665
|
+
confidence="HIGH",
|
|
2666
|
+
file=rt.file_path,
|
|
2667
|
+
start_line=rt.line,
|
|
2668
|
+
end_line=rt.line,
|
|
2669
|
+
evidence=rt_ev,
|
|
2670
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2671
|
+
reason="fastapi_annotated_alias_injects",
|
|
2672
|
+
)
|
|
2673
|
+
)
|
|
2674
|
+
|
|
2675
|
+
# 3. Standard router and direct function Depends(...) bindings
|
|
1924
2676
|
for b in (
|
|
1925
2677
|
bindings_by_kind.get("ROUTER_DI_INJECTS", [])
|
|
1926
2678
|
+ bindings_by_kind.get("DI_INJECTS", [])
|
|
@@ -2092,10 +2844,14 @@ class ReferenceResolver:
|
|
|
2092
2844
|
)
|
|
2093
2845
|
)
|
|
2094
2846
|
|
|
2095
|
-
# Also link matching route endpoint_id -> dep_canon
|
|
2847
|
+
# Also link matching route endpoint_id -> dep_canon (scoped to this file or exact canonical ID)
|
|
2096
2848
|
handler_short = handler_canon.split(".")[-1]
|
|
2097
2849
|
for rt in self.routes:
|
|
2098
|
-
|
|
2850
|
+
matches_handler = (
|
|
2851
|
+
rt.handler_canonical_id == handler_canon
|
|
2852
|
+
or (rt.handler_name == handler_short and rt.file_path == b.file_path)
|
|
2853
|
+
)
|
|
2854
|
+
if matches_handler:
|
|
2099
2855
|
rt_ev = f"Route endpoint '{rt.endpoint_id}' INJECTS '{dep_canon}' at {rt.file_path}:{rt.line}"
|
|
2100
2856
|
add_ref(
|
|
2101
2857
|
Reference(
|
|
@@ -2126,32 +2882,82 @@ class ReferenceResolver:
|
|
|
2126
2882
|
)
|
|
2127
2883
|
)
|
|
2128
2884
|
|
|
2129
|
-
# 9d. Constructor & @inject Type-Based Injection (INJECTS)
|
|
2130
|
-
for b in bindings_by_kind.get("CONSTRUCTOR_PARAM", ()):
|
|
2885
|
+
# 9d. Constructor & @inject Type-Based Injection (INJECTS)
|
|
2886
|
+
for b in bindings_by_kind.get("CONSTRUCTOR_PARAM", ()):
|
|
2887
|
+
ann_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2888
|
+
if not ann_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2889
|
+
ann_sym = self.short_to_symbols[b.source_expr][0]
|
|
2890
|
+
|
|
2891
|
+
if not ann_sym:
|
|
2892
|
+
continue
|
|
2893
|
+
|
|
2894
|
+
ann_canon = ann_sym.canonical_id
|
|
2895
|
+
# Rule: Only create injection relationship when supported DI semantics exist!
|
|
2896
|
+
# i.e., provider mapping exists for this interface OR @inject decorator was present
|
|
2897
|
+
has_provider = ann_canon in provider_map
|
|
2898
|
+
is_inject_dec = (b.base_expr == "inject_decorator")
|
|
2899
|
+
|
|
2900
|
+
if not (has_provider or is_inject_dec):
|
|
2901
|
+
continue
|
|
2902
|
+
|
|
2903
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2904
|
+
caller_canon = b.target_name
|
|
2905
|
+
ev_str = f"Constructor injection: '{caller_canon}' INJECTS '{ann_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
|
|
2906
|
+
|
|
2907
|
+
add_ref(
|
|
2908
|
+
Reference(
|
|
2909
|
+
source_symbol_id=caller_canon,
|
|
2910
|
+
target_symbol_id=ann_canon,
|
|
2911
|
+
relationship="INJECTS",
|
|
2912
|
+
confidence="HIGH",
|
|
2913
|
+
path=b.file_path,
|
|
2914
|
+
start_line=b.line,
|
|
2915
|
+
end_line=b.line,
|
|
2916
|
+
evidence=ev_str,
|
|
2917
|
+
source_hash=f_hash,
|
|
2918
|
+
indexed_commit=self.indexed_commit,
|
|
2919
|
+
)
|
|
2920
|
+
)
|
|
2921
|
+
add_edge(
|
|
2922
|
+
GraphEdge(
|
|
2923
|
+
source=caller_canon,
|
|
2924
|
+
target=ann_canon,
|
|
2925
|
+
relationship="INJECTS",
|
|
2926
|
+
confidence="HIGH",
|
|
2927
|
+
file=b.file_path,
|
|
2928
|
+
start_line=b.line,
|
|
2929
|
+
end_line=b.line,
|
|
2930
|
+
evidence=ev_str,
|
|
2931
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2932
|
+
reason="constructor_injection",
|
|
2933
|
+
)
|
|
2934
|
+
)
|
|
2935
|
+
|
|
2936
|
+
# 9d-nest. NestJS Constructor & @Inject Parameter Injection (INJECTS)
|
|
2937
|
+
for b in bindings_by_kind.get("NESTJS_INJECTS", ()):
|
|
2131
2938
|
ann_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2132
2939
|
if not ann_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2133
2940
|
ann_sym = self.short_to_symbols[b.source_expr][0]
|
|
2134
2941
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
is_inject_dec = (b.base_expr == "inject_decorator")
|
|
2942
|
+
injected_canon: str | None = None
|
|
2943
|
+
if ann_sym:
|
|
2944
|
+
injected_canon = ann_sym.canonical_id
|
|
2945
|
+
elif b.source_expr in provider_map:
|
|
2946
|
+
injected_canon = provider_map[b.source_expr][0][0]
|
|
2947
|
+
elif b.source_expr:
|
|
2948
|
+
injected_canon = f"token.{b.source_expr}"
|
|
2143
2949
|
|
|
2144
|
-
if not
|
|
2950
|
+
if not injected_canon:
|
|
2145
2951
|
continue
|
|
2146
2952
|
|
|
2147
2953
|
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2148
2954
|
caller_canon = b.target_name
|
|
2149
|
-
ev_str = f"
|
|
2955
|
+
ev_str = f"NestJS injection: '{caller_canon}' INJECTS '{injected_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
|
|
2150
2956
|
|
|
2151
2957
|
add_ref(
|
|
2152
2958
|
Reference(
|
|
2153
2959
|
source_symbol_id=caller_canon,
|
|
2154
|
-
target_symbol_id=
|
|
2960
|
+
target_symbol_id=injected_canon,
|
|
2155
2961
|
relationship="INJECTS",
|
|
2156
2962
|
confidence="HIGH",
|
|
2157
2963
|
path=b.file_path,
|
|
@@ -2165,18 +2971,59 @@ class ReferenceResolver:
|
|
|
2165
2971
|
add_edge(
|
|
2166
2972
|
GraphEdge(
|
|
2167
2973
|
source=caller_canon,
|
|
2168
|
-
target=
|
|
2974
|
+
target=injected_canon,
|
|
2169
2975
|
relationship="INJECTS",
|
|
2170
2976
|
confidence="HIGH",
|
|
2171
2977
|
file=b.file_path,
|
|
2172
2978
|
start_line=b.line,
|
|
2173
2979
|
end_line=b.line,
|
|
2174
2980
|
evidence=ev_str,
|
|
2175
|
-
evidence_class=RelationshipEvidenceClass.
|
|
2176
|
-
reason="
|
|
2981
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2982
|
+
reason="nestjs_injection",
|
|
2177
2983
|
)
|
|
2178
2984
|
)
|
|
2179
2985
|
|
|
2986
|
+
# Link routes of this controller to the injected dependency
|
|
2987
|
+
for rt in self.routes:
|
|
2988
|
+
if rt.handler_canonical_id.startswith(f"{caller_canon}."):
|
|
2989
|
+
rt_ev = f"NestJS Route endpoint '{rt.endpoint_id}' INJECTS '{injected_canon}' via {caller_canon} at {rt.file_path}:{rt.line}"
|
|
2990
|
+
add_edge(
|
|
2991
|
+
GraphEdge(
|
|
2992
|
+
source=rt.endpoint_id,
|
|
2993
|
+
target=injected_canon,
|
|
2994
|
+
relationship="INJECTS",
|
|
2995
|
+
confidence="HIGH",
|
|
2996
|
+
file=rt.file_path,
|
|
2997
|
+
start_line=rt.line,
|
|
2998
|
+
end_line=rt.line,
|
|
2999
|
+
evidence=rt_ev,
|
|
3000
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3001
|
+
reason="nestjs_route_injection",
|
|
3002
|
+
)
|
|
3003
|
+
)
|
|
3004
|
+
|
|
3005
|
+
# 9d-ctrl. NestJS Controller Mounting (MOUNTS)
|
|
3006
|
+
for b in bindings_by_kind.get("NESTJS_CONTROLLER", ()):
|
|
3007
|
+
ctrl_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
3008
|
+
if not ctrl_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
3009
|
+
ctrl_sym = self.short_to_symbols[b.source_expr][0]
|
|
3010
|
+
if ctrl_sym:
|
|
3011
|
+
ctrl_canon = ctrl_sym.canonical_id
|
|
3012
|
+
add_edge(
|
|
3013
|
+
GraphEdge(
|
|
3014
|
+
source=b.target_name,
|
|
3015
|
+
target=ctrl_canon,
|
|
3016
|
+
relationship="MOUNTS",
|
|
3017
|
+
confidence="HIGH",
|
|
3018
|
+
file=b.file_path,
|
|
3019
|
+
start_line=b.line,
|
|
3020
|
+
end_line=b.line,
|
|
3021
|
+
evidence=f"NestJS Module '{b.target_name}' mounts controller '{ctrl_canon}'",
|
|
3022
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3023
|
+
reason="nestjs_module_mount",
|
|
3024
|
+
)
|
|
3025
|
+
)
|
|
3026
|
+
|
|
2180
3027
|
# 9d-cycle. Static DI Cycle Detection (INJECTS / PROVIDES / RESOLVES_DEPENDENCY)
|
|
2181
3028
|
di_adj: dict[str, list[tuple[str, str, int]]] = {}
|
|
2182
3029
|
for ed in list(edges):
|
|
@@ -2291,12 +3138,6 @@ class ReferenceResolver:
|
|
|
2291
3138
|
)
|
|
2292
3139
|
|
|
2293
3140
|
# 10. Test Intelligence & Deterministic Test Discovery
|
|
2294
|
-
def _find_caller_id_for_binding(b: BindingRef) -> str:
|
|
2295
|
-
for s in self.path_to_symbols.get(b.file_path, ()):
|
|
2296
|
-
if s.start_line <= b.line <= s.end_line:
|
|
2297
|
-
return s.canonical_id
|
|
2298
|
-
mod = self.file_to_module.get(b.file_path) or normalize_module(b.file_path)
|
|
2299
|
-
return f"{mod}.{b.scope}" if b.scope else mod
|
|
2300
3141
|
|
|
2301
3142
|
# 10a. Direct Test Calls (TESTS)
|
|
2302
3143
|
# Any call originating from a test file to a production symbol
|
|
@@ -2665,6 +3506,361 @@ class ReferenceResolver:
|
|
|
2665
3506
|
)
|
|
2666
3507
|
)
|
|
2667
3508
|
|
|
3509
|
+
# 12. Frontend Web Graph (React, JSX, Hooks, Styles, HTML - Pillar 4)
|
|
3510
|
+
for b in bindings_by_kind.get("REACT_RENDERS", ()):
|
|
3511
|
+
child_name = b.target_name
|
|
3512
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3513
|
+
target_sym, _, _ = self.resolve_identifier_in_file(child_name, b.file_path, use_line=b.line)
|
|
3514
|
+
if not target_sym and child_name in self.short_to_symbols:
|
|
3515
|
+
candidates = [s for s in self.short_to_symbols[child_name] if s.kind in ("component", "function")]
|
|
3516
|
+
if candidates:
|
|
3517
|
+
target_sym = candidates[0]
|
|
3518
|
+
target_id = target_sym.canonical_id if target_sym else child_name
|
|
3519
|
+
ev_str = f"React component renders '<{child_name} />' at {b.file_path}:{b.line}"
|
|
3520
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3521
|
+
add_ref(
|
|
3522
|
+
Reference(
|
|
3523
|
+
source_symbol_id=caller_id,
|
|
3524
|
+
target_symbol_id=target_id,
|
|
3525
|
+
relationship="RENDERS",
|
|
3526
|
+
confidence="HIGH" if target_sym else "MEDIUM",
|
|
3527
|
+
path=b.file_path,
|
|
3528
|
+
start_line=b.line,
|
|
3529
|
+
end_line=b.line,
|
|
3530
|
+
evidence=ev_str,
|
|
3531
|
+
source_hash=f_hash,
|
|
3532
|
+
indexed_commit=self.indexed_commit,
|
|
3533
|
+
)
|
|
3534
|
+
)
|
|
3535
|
+
add_edge(
|
|
3536
|
+
GraphEdge(
|
|
3537
|
+
source=caller_id,
|
|
3538
|
+
target=target_id,
|
|
3539
|
+
relationship="RENDERS",
|
|
3540
|
+
confidence="HIGH" if target_sym else "MEDIUM",
|
|
3541
|
+
file=b.file_path,
|
|
3542
|
+
start_line=b.line,
|
|
3543
|
+
end_line=b.line,
|
|
3544
|
+
evidence=ev_str,
|
|
3545
|
+
evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value if target_sym else RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3546
|
+
reason=f"react_jsx_render:{child_name}",
|
|
3547
|
+
)
|
|
3548
|
+
)
|
|
3549
|
+
|
|
3550
|
+
for b in bindings_by_kind.get("REACT_USES_HOOK", ()):
|
|
3551
|
+
hook_name = b.target_name
|
|
3552
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3553
|
+
target_sym, _, _ = self.resolve_identifier_in_file(hook_name, b.file_path, use_line=b.line)
|
|
3554
|
+
if not target_sym and hook_name in self.short_to_symbols:
|
|
3555
|
+
candidates = [s for s in self.short_to_symbols[hook_name] if s.kind in ("hook", "function")]
|
|
3556
|
+
if candidates:
|
|
3557
|
+
target_sym = candidates[0]
|
|
3558
|
+
target_id = target_sym.canonical_id if target_sym else hook_name
|
|
3559
|
+
ev_str = f"React component invokes hook '{hook_name}()' at {b.file_path}:{b.line}"
|
|
3560
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3561
|
+
add_ref(
|
|
3562
|
+
Reference(
|
|
3563
|
+
source_symbol_id=caller_id,
|
|
3564
|
+
target_symbol_id=target_id,
|
|
3565
|
+
relationship="USES_HOOK",
|
|
3566
|
+
confidence="HIGH" if target_sym else "MEDIUM",
|
|
3567
|
+
path=b.file_path,
|
|
3568
|
+
start_line=b.line,
|
|
3569
|
+
end_line=b.line,
|
|
3570
|
+
evidence=ev_str,
|
|
3571
|
+
source_hash=f_hash,
|
|
3572
|
+
indexed_commit=self.indexed_commit,
|
|
3573
|
+
)
|
|
3574
|
+
)
|
|
3575
|
+
add_edge(
|
|
3576
|
+
GraphEdge(
|
|
3577
|
+
source=caller_id,
|
|
3578
|
+
target=target_id,
|
|
3579
|
+
relationship="USES_HOOK",
|
|
3580
|
+
confidence="HIGH" if target_sym else "MEDIUM",
|
|
3581
|
+
file=b.file_path,
|
|
3582
|
+
start_line=b.line,
|
|
3583
|
+
end_line=b.line,
|
|
3584
|
+
evidence=ev_str,
|
|
3585
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3586
|
+
reason=f"react_hook_call:{hook_name}",
|
|
3587
|
+
)
|
|
3588
|
+
)
|
|
3589
|
+
|
|
3590
|
+
for b in bindings_by_kind.get("REACT_IMPORTS_STYLE", ()):
|
|
3591
|
+
style_rel = b.target_name
|
|
3592
|
+
src_dir = posixpath.dirname(b.file_path.replace("\\", "/").lstrip("./"))
|
|
3593
|
+
resolved_style = posixpath.normpath(posixpath.join(src_dir, style_rel)) if style_rel.startswith(".") else style_rel
|
|
3594
|
+
if resolved_style not in self.known_files:
|
|
3595
|
+
for kf in self.known_files:
|
|
3596
|
+
if kf.endswith(f"/{style_rel}") or kf == style_rel:
|
|
3597
|
+
resolved_style = kf
|
|
3598
|
+
break
|
|
3599
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3600
|
+
ev_str = f"Component imports stylesheet '{style_rel}' at {b.file_path}:{b.line}"
|
|
3601
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3602
|
+
add_ref(
|
|
3603
|
+
Reference(
|
|
3604
|
+
source_symbol_id=caller_id,
|
|
3605
|
+
target_symbol_id=resolved_style,
|
|
3606
|
+
relationship="IMPORTS_STYLE",
|
|
3607
|
+
confidence="HIGH",
|
|
3608
|
+
path=b.file_path,
|
|
3609
|
+
start_line=b.line,
|
|
3610
|
+
end_line=b.line,
|
|
3611
|
+
evidence=ev_str,
|
|
3612
|
+
source_hash=f_hash,
|
|
3613
|
+
indexed_commit=self.indexed_commit,
|
|
3614
|
+
)
|
|
3615
|
+
)
|
|
3616
|
+
add_edge(
|
|
3617
|
+
GraphEdge(
|
|
3618
|
+
source=caller_id,
|
|
3619
|
+
target=resolved_style,
|
|
3620
|
+
relationship="IMPORTS_STYLE",
|
|
3621
|
+
confidence="HIGH",
|
|
3622
|
+
file=b.file_path,
|
|
3623
|
+
start_line=b.line,
|
|
3624
|
+
end_line=b.line,
|
|
3625
|
+
evidence=ev_str,
|
|
3626
|
+
evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value,
|
|
3627
|
+
reason=f"react_style_import:{style_rel}",
|
|
3628
|
+
)
|
|
3629
|
+
)
|
|
3630
|
+
|
|
3631
|
+
for b in bindings_by_kind.get("REACT_USES_STYLE_CLASS", ()):
|
|
3632
|
+
cls_name = b.target_name
|
|
3633
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3634
|
+
ev_str = f"Component applies CSS class '.{cls_name}' at {b.file_path}:{b.line}"
|
|
3635
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3636
|
+
add_ref(
|
|
3637
|
+
Reference(
|
|
3638
|
+
source_symbol_id=caller_id,
|
|
3639
|
+
target_symbol_id=f".{cls_name}",
|
|
3640
|
+
relationship="USES_STYLE_CLASS",
|
|
3641
|
+
confidence="HIGH",
|
|
3642
|
+
path=b.file_path,
|
|
3643
|
+
start_line=b.line,
|
|
3644
|
+
end_line=b.line,
|
|
3645
|
+
evidence=ev_str,
|
|
3646
|
+
source_hash=f_hash,
|
|
3647
|
+
indexed_commit=self.indexed_commit,
|
|
3648
|
+
)
|
|
3649
|
+
)
|
|
3650
|
+
add_edge(
|
|
3651
|
+
GraphEdge(
|
|
3652
|
+
source=caller_id,
|
|
3653
|
+
target=f".{cls_name}",
|
|
3654
|
+
relationship="USES_STYLE_CLASS",
|
|
3655
|
+
confidence="HIGH",
|
|
3656
|
+
file=b.file_path,
|
|
3657
|
+
start_line=b.line,
|
|
3658
|
+
end_line=b.line,
|
|
3659
|
+
evidence=ev_str,
|
|
3660
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3661
|
+
reason=f"css_class_use:.{cls_name}",
|
|
3662
|
+
)
|
|
3663
|
+
)
|
|
3664
|
+
|
|
3665
|
+
for b in bindings_by_kind.get("REACT_FETCH_ROUTE", ()):
|
|
3666
|
+
raw_url = b.target_name
|
|
3667
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3668
|
+
client_method = b.base_expr or "GET"
|
|
3669
|
+
norm_client = normalize_route(client_method, raw_url)
|
|
3670
|
+
|
|
3671
|
+
matched_routes: list[tuple[RouteDetection, str]] = []
|
|
3672
|
+
for r in self.routes:
|
|
3673
|
+
if (
|
|
3674
|
+
r.route_path == raw_url
|
|
3675
|
+
or r.normalized_route == raw_url
|
|
3676
|
+
or r.route_path.rstrip("/") == raw_url.rstrip("/")
|
|
3677
|
+
):
|
|
3678
|
+
matched_routes.append((r, "EXACT"))
|
|
3679
|
+
continue
|
|
3680
|
+
norm_backend = normalize_route(r.http_method, r.route_path)
|
|
3681
|
+
is_matched, match_type = match_route_topology(
|
|
3682
|
+
norm_client, norm_backend, allow_proxy_prefix=True, match_methods=True
|
|
3683
|
+
)
|
|
3684
|
+
if is_matched:
|
|
3685
|
+
matched_routes.append((r, match_type))
|
|
3686
|
+
|
|
3687
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3688
|
+
if matched_routes:
|
|
3689
|
+
for r, match_type in matched_routes:
|
|
3690
|
+
ev_str = f"Client fetches route '{r.route_path}' ({match_type}) at {b.file_path}:{b.line}"
|
|
3691
|
+
add_ref(
|
|
3692
|
+
Reference(
|
|
3693
|
+
source_symbol_id=caller_id,
|
|
3694
|
+
target_symbol_id=r.endpoint_id,
|
|
3695
|
+
relationship="FETCHES_ROUTE",
|
|
3696
|
+
confidence="HIGH",
|
|
3697
|
+
path=b.file_path,
|
|
3698
|
+
start_line=b.line,
|
|
3699
|
+
end_line=b.line,
|
|
3700
|
+
evidence=ev_str,
|
|
3701
|
+
source_hash=f_hash,
|
|
3702
|
+
indexed_commit=self.indexed_commit,
|
|
3703
|
+
)
|
|
3704
|
+
)
|
|
3705
|
+
add_edge(
|
|
3706
|
+
GraphEdge(
|
|
3707
|
+
source=caller_id,
|
|
3708
|
+
target=r.endpoint_id,
|
|
3709
|
+
relationship="FETCHES_ROUTE",
|
|
3710
|
+
confidence="HIGH",
|
|
3711
|
+
file=b.file_path,
|
|
3712
|
+
start_line=b.line,
|
|
3713
|
+
end_line=b.line,
|
|
3714
|
+
evidence=ev_str,
|
|
3715
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3716
|
+
reason=f"client_fetch:{r.route_path}",
|
|
3717
|
+
)
|
|
3718
|
+
)
|
|
3719
|
+
if r.handler_canonical_id:
|
|
3720
|
+
h_ev = f"Client invokes backend handler '{r.handler_canonical_id}' via {r.route_path} ({match_type}) at {b.file_path}:{b.line}"
|
|
3721
|
+
add_ref(
|
|
3722
|
+
Reference(
|
|
3723
|
+
source_symbol_id=caller_id,
|
|
3724
|
+
target_symbol_id=r.handler_canonical_id,
|
|
3725
|
+
relationship="CALLS",
|
|
3726
|
+
confidence="HIGH",
|
|
3727
|
+
path=b.file_path,
|
|
3728
|
+
start_line=b.line,
|
|
3729
|
+
end_line=b.line,
|
|
3730
|
+
evidence=h_ev,
|
|
3731
|
+
source_hash=f_hash,
|
|
3732
|
+
indexed_commit=self.indexed_commit,
|
|
3733
|
+
)
|
|
3734
|
+
)
|
|
3735
|
+
add_edge(
|
|
3736
|
+
GraphEdge(
|
|
3737
|
+
source=caller_id,
|
|
3738
|
+
target=r.handler_canonical_id,
|
|
3739
|
+
relationship="CALLS",
|
|
3740
|
+
confidence="HIGH",
|
|
3741
|
+
file=b.file_path,
|
|
3742
|
+
start_line=b.line,
|
|
3743
|
+
end_line=b.line,
|
|
3744
|
+
evidence=h_ev,
|
|
3745
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
3746
|
+
reason=f"client_handler_call:{r.route_path}",
|
|
3747
|
+
)
|
|
3748
|
+
)
|
|
3749
|
+
else:
|
|
3750
|
+
ev_str = f"Client fetches external/custom route '{raw_url}' at {b.file_path}:{b.line}"
|
|
3751
|
+
add_edge(
|
|
3752
|
+
GraphEdge(
|
|
3753
|
+
source=caller_id,
|
|
3754
|
+
target=raw_url,
|
|
3755
|
+
relationship="FETCHES_ROUTE",
|
|
3756
|
+
confidence="LOW",
|
|
3757
|
+
file=b.file_path,
|
|
3758
|
+
start_line=b.line,
|
|
3759
|
+
end_line=b.line,
|
|
3760
|
+
evidence=ev_str,
|
|
3761
|
+
evidence_class=RelationshipEvidenceClass.POSSIBLE.value,
|
|
3762
|
+
reason=f"client_fetch:{raw_url}",
|
|
3763
|
+
)
|
|
3764
|
+
)
|
|
3765
|
+
|
|
3766
|
+
for b in bindings_by_kind.get("HTML_LOADS_SCRIPT", ()):
|
|
3767
|
+
src_val = b.target_name
|
|
3768
|
+
target_file = src_val
|
|
3769
|
+
for kf in self.known_files:
|
|
3770
|
+
if kf.endswith(f"/{src_val}") or kf == src_val:
|
|
3771
|
+
target_file = kf
|
|
3772
|
+
break
|
|
3773
|
+
ev_str = f"HTML loads script '{src_val}' at {b.file_path}:{b.line}"
|
|
3774
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3775
|
+
add_ref(
|
|
3776
|
+
Reference(
|
|
3777
|
+
source_symbol_id=b.file_path,
|
|
3778
|
+
target_symbol_id=target_file,
|
|
3779
|
+
relationship="LOADS_SCRIPT",
|
|
3780
|
+
confidence="HIGH",
|
|
3781
|
+
path=b.file_path,
|
|
3782
|
+
start_line=b.line,
|
|
3783
|
+
end_line=b.line,
|
|
3784
|
+
evidence=ev_str,
|
|
3785
|
+
source_hash=f_hash,
|
|
3786
|
+
indexed_commit=self.indexed_commit,
|
|
3787
|
+
)
|
|
3788
|
+
)
|
|
3789
|
+
add_edge(
|
|
3790
|
+
GraphEdge(
|
|
3791
|
+
source=b.file_path,
|
|
3792
|
+
target=target_file,
|
|
3793
|
+
relationship="LOADS_SCRIPT",
|
|
3794
|
+
confidence="HIGH",
|
|
3795
|
+
file=b.file_path,
|
|
3796
|
+
start_line=b.line,
|
|
3797
|
+
end_line=b.line,
|
|
3798
|
+
evidence=ev_str,
|
|
3799
|
+
evidence_class=RelationshipEvidenceClass.STATIC_VERIFIED.value,
|
|
3800
|
+
reason=f"html_script:{src_val}",
|
|
3801
|
+
)
|
|
3802
|
+
)
|
|
3803
|
+
|
|
3804
|
+
for b in bindings_by_kind.get("HTML_LOADS_STYLESHEET", ()):
|
|
3805
|
+
href_val = b.target_name
|
|
3806
|
+
target_file = href_val
|
|
3807
|
+
for kf in self.known_files:
|
|
3808
|
+
if kf.endswith(f"/{href_val}") or kf == href_val:
|
|
3809
|
+
target_file = kf
|
|
3810
|
+
break
|
|
3811
|
+
ev_str = f"HTML loads stylesheet '{href_val}' at {b.file_path}:{b.line}"
|
|
3812
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3813
|
+
add_ref(
|
|
3814
|
+
Reference(
|
|
3815
|
+
source_symbol_id=b.file_path,
|
|
3816
|
+
target_symbol_id=target_file,
|
|
3817
|
+
relationship="LOADS_STYLESHEET",
|
|
3818
|
+
confidence="HIGH",
|
|
3819
|
+
path=b.file_path,
|
|
3820
|
+
start_line=b.line,
|
|
3821
|
+
end_line=b.line,
|
|
3822
|
+
evidence=ev_str,
|
|
3823
|
+
source_hash=f_hash,
|
|
3824
|
+
indexed_commit=self.indexed_commit,
|
|
3825
|
+
)
|
|
3826
|
+
)
|
|
3827
|
+
add_edge(
|
|
3828
|
+
GraphEdge(
|
|
3829
|
+
source=b.file_path,
|
|
3830
|
+
target=target_file,
|
|
3831
|
+
relationship="LOADS_STYLESHEET",
|
|
3832
|
+
confidence="HIGH",
|
|
3833
|
+
file=b.file_path,
|
|
3834
|
+
start_line=b.line,
|
|
3835
|
+
end_line=b.line,
|
|
3836
|
+
evidence=ev_str,
|
|
3837
|
+
evidence_class=RelationshipEvidenceClass.STATIC_VERIFIED.value,
|
|
3838
|
+
reason=f"html_stylesheet:{href_val}",
|
|
3839
|
+
)
|
|
3840
|
+
)
|
|
3841
|
+
|
|
3842
|
+
# Roblox & Luau Intelligence (Phases R2-R8)
|
|
3843
|
+
has_luau_or_roblox = any(
|
|
3844
|
+
f.endswith((".lua", ".luau")) for f in self.known_files
|
|
3845
|
+
) or any(
|
|
3846
|
+
b.expr_kind.startswith("ROBLOX_") for b in self.bindings
|
|
3847
|
+
)
|
|
3848
|
+
if has_luau_or_roblox:
|
|
3849
|
+
from codegraph.roblox import resolve_roblox_intelligence
|
|
3850
|
+
|
|
3851
|
+
repo_path = Path(self.repository) if self.repository is not None else None
|
|
3852
|
+
resolve_roblox_intelligence(
|
|
3853
|
+
repository=repo_path,
|
|
3854
|
+
symbols=self.symbols,
|
|
3855
|
+
bindings=self.bindings,
|
|
3856
|
+
known_files=self.known_files,
|
|
3857
|
+
file_hashes=self.file_hashes,
|
|
3858
|
+
indexed_commit=self.indexed_commit,
|
|
3859
|
+
add_ref=add_ref,
|
|
3860
|
+
add_edge=add_edge,
|
|
3861
|
+
resolved_imports=resolved_imports,
|
|
3862
|
+
)
|
|
3863
|
+
|
|
2668
3864
|
return ResolutionOutput(
|
|
2669
3865
|
references=references,
|
|
2670
3866
|
edges=edges,
|