codegraph-engine 3.0.0__2-py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -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/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +868 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1993 @@
|
|
|
1
|
+
"""Graph traversal and query engine: call graph, dependency graph, impact analysis, tests.
|
|
2
|
+
|
|
3
|
+
Confidence Vocabulary:
|
|
4
|
+
HIGH — parser-confirmed, symbol-resolved, source-backed (verified)
|
|
5
|
+
MEDIUM — structurally inferred (unique match, partial resolution, same module)
|
|
6
|
+
LOW — textual/heuristic match (unverified)
|
|
7
|
+
UNKNOWN — unresolved reference / dynamic call
|
|
8
|
+
|
|
9
|
+
Edge Classifications:
|
|
10
|
+
STRUCTURAL: DEFINES, CONTAINS, EXPORTS, REEXPORTS
|
|
11
|
+
SEMANTIC: IMPORTS, CALLS, EXTENDS, IMPLEMENTS, HANDLED_BY, TESTS
|
|
12
|
+
UNCERTAIN: POSSIBLE_CALLS
|
|
13
|
+
|
|
14
|
+
Architectural Invariant:
|
|
15
|
+
Graph is a projection of resolved source facts and references.
|
|
16
|
+
Heuristics are never promoted to verified CALLS.
|
|
17
|
+
Inverses (e.g. A --CALLS--> B queryable as CALLED_BY) do not duplicate independent facts.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
import sqlite3
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from codegraph.evidence.citations import get_repository_from_con, verify_source_hash
|
|
26
|
+
from codegraph.freshness import index_generation
|
|
27
|
+
from codegraph.indexing.models import Symbol
|
|
28
|
+
from codegraph.resources import get_global_governor, get_graph_cache
|
|
29
|
+
|
|
30
|
+
from .models import GraphEdge
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AttrDict(dict[str, Any]):
|
|
34
|
+
"""Dict subclass that provides attribute access for ergonomics and JSON compatibility."""
|
|
35
|
+
|
|
36
|
+
def __getattr__(self, name: str) -> Any:
|
|
37
|
+
try:
|
|
38
|
+
return self[name]
|
|
39
|
+
except KeyError:
|
|
40
|
+
raise AttributeError(f"'AttrDict' object has no attribute '{name}'") from None
|
|
41
|
+
|
|
42
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
43
|
+
self[name] = value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_NOISY_UNRESOLVED_NAMES = frozenset({
|
|
47
|
+
"size", "shape", "view", "transpose", "squeeze", "unsqueeze", "reshape", "item", "dim",
|
|
48
|
+
"get", "keys", "values", "items", "append", "extend", "pop", "insert", "clear", "copy",
|
|
49
|
+
"format", "split", "strip", "lstrip", "rstrip", "join", "replace", "startswith", "endswith",
|
|
50
|
+
"lower", "upper", "count", "index", "find", "update", "add", "remove", "discard",
|
|
51
|
+
"encode", "decode", "read", "write", "close", "flush", "seek", "tell",
|
|
52
|
+
"print", "len", "range", "str", "int", "float", "bool", "dict", "list", "set", "tuple",
|
|
53
|
+
"isinstance", "issubclass", "hasattr", "getattr", "setattr", "delattr", "type", "id",
|
|
54
|
+
"min", "max", "sum", "all", "any", "zip", "enumerate", "map", "filter", "sorted", "reversed",
|
|
55
|
+
"cat", "stack", "zeros", "ones", "empty", "tensor", "from_numpy", "device", "to", "backward",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Symbol Query API
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_symbol(con: sqlite3.Connection, canonical_id: str) -> Symbol | None:
|
|
65
|
+
"""Retrieve a single symbol by its exact canonical ID."""
|
|
66
|
+
row = con.execute(
|
|
67
|
+
"SELECT name, qualified_name, kind, path, start_line, end_line, decorators, "
|
|
68
|
+
"id, canonical_id, language, module, scope, signature, content_hash, "
|
|
69
|
+
"parent_symbol_id, visibility, return_type, parameter_count, documentation "
|
|
70
|
+
"FROM symbols WHERE canonical_id=? OR id=? LIMIT 1",
|
|
71
|
+
(canonical_id, canonical_id),
|
|
72
|
+
).fetchone()
|
|
73
|
+
if not row:
|
|
74
|
+
return None
|
|
75
|
+
return _row_to_symbol(row)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find_symbol_exact(con: sqlite3.Connection, qualified_name: str) -> list[Symbol]:
|
|
79
|
+
"""Retrieve symbols matching exact canonical_id or qualified_name."""
|
|
80
|
+
rows = con.execute(
|
|
81
|
+
"SELECT name, qualified_name, kind, path, start_line, end_line, decorators, "
|
|
82
|
+
"id, canonical_id, language, module, scope, signature, content_hash, "
|
|
83
|
+
"parent_symbol_id, visibility, return_type, parameter_count, documentation "
|
|
84
|
+
"FROM symbols WHERE canonical_id=? OR qualified_name=? "
|
|
85
|
+
"ORDER BY path, start_line",
|
|
86
|
+
(qualified_name, qualified_name),
|
|
87
|
+
).fetchall()
|
|
88
|
+
return [_row_to_symbol(r) for r in rows]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def find_symbols(
|
|
92
|
+
con: sqlite3.Connection, name: str, max_results: int = 50
|
|
93
|
+
) -> list[Symbol]:
|
|
94
|
+
"""Find symbols by canonical ID, qualified name, or short name with deterministic ranking.
|
|
95
|
+
|
|
96
|
+
- If query has dots and matches canonical_id, returns exact canonical match.
|
|
97
|
+
- If query has dots and matches qualified_name, returns qualified matches.
|
|
98
|
+
- If query has no dots, returns symbols where name = query, preserving all distinct modules.
|
|
99
|
+
"""
|
|
100
|
+
clean = name.strip()
|
|
101
|
+
if not clean:
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
# 1. Exact canonical ID match
|
|
105
|
+
canon_rows = con.execute(
|
|
106
|
+
"SELECT name, qualified_name, kind, path, start_line, end_line, decorators, "
|
|
107
|
+
"id, canonical_id, language, module, scope, signature, content_hash, "
|
|
108
|
+
"parent_symbol_id, visibility, return_type, parameter_count, documentation "
|
|
109
|
+
"FROM symbols WHERE canonical_id=? ORDER BY path, start_line LIMIT ?",
|
|
110
|
+
(clean, max_results),
|
|
111
|
+
).fetchall()
|
|
112
|
+
if canon_rows:
|
|
113
|
+
return [_row_to_symbol(r) for r in canon_rows]
|
|
114
|
+
|
|
115
|
+
# 2. Qualified name match if dotted
|
|
116
|
+
if "." in clean:
|
|
117
|
+
q_rows = con.execute(
|
|
118
|
+
"SELECT name, qualified_name, kind, path, start_line, end_line, decorators, "
|
|
119
|
+
"id, canonical_id, language, module, scope, signature, content_hash, "
|
|
120
|
+
"parent_symbol_id, visibility, return_type, parameter_count, documentation "
|
|
121
|
+
"FROM symbols WHERE qualified_name=? OR canonical_id LIKE ? "
|
|
122
|
+
"ORDER BY path, start_line LIMIT ?",
|
|
123
|
+
(clean, f"%.{clean}", max_results),
|
|
124
|
+
).fetchall()
|
|
125
|
+
if q_rows:
|
|
126
|
+
return [_row_to_symbol(r) for r in q_rows]
|
|
127
|
+
|
|
128
|
+
# 3. Short name match
|
|
129
|
+
short_rows = con.execute(
|
|
130
|
+
"SELECT name, qualified_name, kind, path, start_line, end_line, decorators, "
|
|
131
|
+
"id, canonical_id, language, module, scope, signature, content_hash, "
|
|
132
|
+
"parent_symbol_id, visibility, return_type, parameter_count, documentation "
|
|
133
|
+
"FROM symbols WHERE name=? ORDER BY path, start_line, canonical_id LIMIT ?",
|
|
134
|
+
(clean, max_results),
|
|
135
|
+
).fetchall()
|
|
136
|
+
return [_row_to_symbol(r) for r in short_rows]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
find_symbol = find_symbols
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _row_to_symbol(r: sqlite3.Row) -> Symbol:
|
|
143
|
+
dec_list = [d for d in str(r["decorators"]).split(",") if d]
|
|
144
|
+
return Symbol(
|
|
145
|
+
id=str(r["id"]),
|
|
146
|
+
canonical_id=str(r["canonical_id"]),
|
|
147
|
+
name=str(r["name"]),
|
|
148
|
+
qualified_name=str(r["qualified_name"]),
|
|
149
|
+
kind=str(r["kind"]),
|
|
150
|
+
start_line=int(r["start_line"]),
|
|
151
|
+
end_line=int(r["end_line"]),
|
|
152
|
+
file_path=str(r["path"]),
|
|
153
|
+
decorators=dec_list,
|
|
154
|
+
language=str(r["language"]),
|
|
155
|
+
module=str(r["module"]),
|
|
156
|
+
path=str(r["path"]),
|
|
157
|
+
scope=str(r["scope"]),
|
|
158
|
+
signature=str(r["signature"]),
|
|
159
|
+
content_hash=str(r["content_hash"]),
|
|
160
|
+
parent_symbol_id=r["parent_symbol_id"],
|
|
161
|
+
visibility=str(r["visibility"]),
|
|
162
|
+
return_type=r["return_type"],
|
|
163
|
+
parameter_count=r["parameter_count"],
|
|
164
|
+
documentation=r["documentation"],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# References & Call Graph API
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def find_references(
|
|
174
|
+
con: sqlite3.Connection,
|
|
175
|
+
canonical_id: str,
|
|
176
|
+
max_results: int = 100,
|
|
177
|
+
) -> list[AttrDict]:
|
|
178
|
+
"""Return verified and pending references targeting the given symbol."""
|
|
179
|
+
repo = get_repository_from_con(con)
|
|
180
|
+
rows = con.execute(
|
|
181
|
+
"SELECT source_symbol_id, target_symbol_id, relationship, confidence, "
|
|
182
|
+
"path, start_line, end_line, evidence, source_hash, indexed_commit, evidence_status "
|
|
183
|
+
"FROM 'references' "
|
|
184
|
+
"WHERE target_symbol_id=? OR target_symbol_id LIKE ? "
|
|
185
|
+
"ORDER BY confidence = 'HIGH' DESC, path, start_line LIMIT ?",
|
|
186
|
+
(canonical_id, f"%.{canonical_id}", max_results),
|
|
187
|
+
).fetchall()
|
|
188
|
+
|
|
189
|
+
results: list[AttrDict] = []
|
|
190
|
+
for r in rows:
|
|
191
|
+
st = verify_source_hash(repo, r["path"], r["source_hash"])
|
|
192
|
+
results.append(
|
|
193
|
+
AttrDict(
|
|
194
|
+
source_symbol_id=r["source_symbol_id"],
|
|
195
|
+
target_symbol_id=r["target_symbol_id"],
|
|
196
|
+
source=r["source_symbol_id"],
|
|
197
|
+
target=r["target_symbol_id"],
|
|
198
|
+
relationship=r["relationship"],
|
|
199
|
+
confidence=r["confidence"] if st == "current" else "LOW",
|
|
200
|
+
path=r["path"],
|
|
201
|
+
file=r["path"],
|
|
202
|
+
start_line=r["start_line"],
|
|
203
|
+
end_line=r["end_line"],
|
|
204
|
+
line=r["start_line"],
|
|
205
|
+
evidence=r["evidence"],
|
|
206
|
+
source_hash=r["source_hash"],
|
|
207
|
+
indexed_commit=r["indexed_commit"],
|
|
208
|
+
evidence_status=st,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
return results
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def find_callers(
|
|
215
|
+
con: sqlite3.Connection,
|
|
216
|
+
symbol: str,
|
|
217
|
+
max_results: int = 50,
|
|
218
|
+
) -> list[AttrDict]:
|
|
219
|
+
"""Return verified callers from resolved references, falling back to static calls table.
|
|
220
|
+
|
|
221
|
+
Verified callers carry confidence 'HIGH' or 'MEDIUM' and relationship 'CALLS'.
|
|
222
|
+
Unverified textual matches carry confidence 'LOW' and relationship 'POSSIBLE_CALLS'.
|
|
223
|
+
"""
|
|
224
|
+
gov = get_global_governor()
|
|
225
|
+
max_results = min(max_results, gov.policy.max_graph_nodes_per_query)
|
|
226
|
+
gen = index_generation(con)
|
|
227
|
+
cache = get_graph_cache()
|
|
228
|
+
cache_key = (gen, "callers", symbol, max_results)
|
|
229
|
+
cached = cache.get(cache_key)
|
|
230
|
+
if cached is not None and isinstance(cached, list):
|
|
231
|
+
return cached
|
|
232
|
+
|
|
233
|
+
repo = get_repository_from_con(con)
|
|
234
|
+
clean_sym = symbol.strip()
|
|
235
|
+
short_name = clean_sym.split(":")[-1].split(".")[-1]
|
|
236
|
+
results: list[AttrDict] = []
|
|
237
|
+
seen: set[tuple[str, str | None, int]] = set()
|
|
238
|
+
resolved_lines: set[tuple[str, int]] = set()
|
|
239
|
+
|
|
240
|
+
# Resolve symbol to canonical IDs when present in symbols table
|
|
241
|
+
target_canons: list[str] = []
|
|
242
|
+
try:
|
|
243
|
+
sym_rows = con.execute(
|
|
244
|
+
"SELECT canonical_id, qualified_name FROM symbols "
|
|
245
|
+
"WHERE canonical_id=? OR qualified_name=? OR name=? "
|
|
246
|
+
"ORDER BY (canonical_id=?) DESC, (qualified_name=?) DESC",
|
|
247
|
+
(clean_sym, clean_sym, clean_sym, clean_sym, clean_sym),
|
|
248
|
+
).fetchall()
|
|
249
|
+
exact_canons = [str(r["canonical_id"]) for r in sym_rows if r["canonical_id"] == clean_sym]
|
|
250
|
+
exact_qnames = [str(r["canonical_id"]) for r in sym_rows if r["qualified_name"] == clean_sym]
|
|
251
|
+
if exact_canons:
|
|
252
|
+
target_canons = exact_canons
|
|
253
|
+
elif exact_qnames:
|
|
254
|
+
target_canons = exact_qnames
|
|
255
|
+
elif sym_rows and "." not in clean_sym:
|
|
256
|
+
target_canons = [str(r["canonical_id"]) for r in sym_rows]
|
|
257
|
+
except Exception:
|
|
258
|
+
target_canons = []
|
|
259
|
+
|
|
260
|
+
# 1. Resolved CALLS edges from references table
|
|
261
|
+
if target_canons:
|
|
262
|
+
ph = ",".join("?" for _ in target_canons)
|
|
263
|
+
rows = con.execute(
|
|
264
|
+
"SELECT source_symbol_id, target_symbol_id, relationship, confidence, "
|
|
265
|
+
"path, start_line, end_line, evidence, source_hash "
|
|
266
|
+
"FROM 'references' "
|
|
267
|
+
f"WHERE relationship='CALLS' AND (target_symbol_id IN ({ph}) OR target_symbol_id=? OR target_symbol_id LIKE ?) "
|
|
268
|
+
"ORDER BY confidence = 'HIGH' DESC, path, start_line LIMIT ?",
|
|
269
|
+
[*target_canons, clean_sym, f"%.{clean_sym}", max_results],
|
|
270
|
+
).fetchall()
|
|
271
|
+
elif "." in clean_sym:
|
|
272
|
+
rows = con.execute(
|
|
273
|
+
"SELECT source_symbol_id, target_symbol_id, relationship, confidence, "
|
|
274
|
+
"path, start_line, end_line, evidence, source_hash "
|
|
275
|
+
"FROM 'references' "
|
|
276
|
+
"WHERE relationship='CALLS' AND (target_symbol_id=? OR target_symbol_id LIKE ?) "
|
|
277
|
+
"ORDER BY confidence = 'HIGH' DESC, path, start_line LIMIT ?",
|
|
278
|
+
(clean_sym, f"%.{clean_sym}", max_results),
|
|
279
|
+
).fetchall()
|
|
280
|
+
else:
|
|
281
|
+
rows = con.execute(
|
|
282
|
+
"SELECT source_symbol_id, target_symbol_id, relationship, confidence, "
|
|
283
|
+
"path, start_line, end_line, evidence, source_hash "
|
|
284
|
+
"FROM 'references' "
|
|
285
|
+
"WHERE relationship='CALLS' AND (target_symbol_id=? OR target_symbol_id LIKE ? OR target_symbol_id LIKE ?) "
|
|
286
|
+
"ORDER BY confidence = 'HIGH' DESC, path, start_line LIMIT ?",
|
|
287
|
+
(clean_sym, f"%.{clean_sym}", f"%.{short_name}", max_results),
|
|
288
|
+
).fetchall()
|
|
289
|
+
|
|
290
|
+
for r in rows:
|
|
291
|
+
key = (r["path"], r["source_symbol_id"], r["start_line"])
|
|
292
|
+
if key in seen:
|
|
293
|
+
continue
|
|
294
|
+
seen.add(key)
|
|
295
|
+
resolved_lines.add((r["path"], int(r["start_line"])))
|
|
296
|
+
st = verify_source_hash(repo, r["path"], r["source_hash"])
|
|
297
|
+
ev_text = str(r["evidence"] or "")
|
|
298
|
+
ev_cls = "DATAFLOW_VERIFIED" if ("resolved via" in ev_text.lower() or "factory" in ev_text.lower()) else "AST_VERIFIED"
|
|
299
|
+
results.append(
|
|
300
|
+
AttrDict(
|
|
301
|
+
file=r["path"],
|
|
302
|
+
path=r["path"],
|
|
303
|
+
symbol=r["source_symbol_id"],
|
|
304
|
+
caller=r["source_symbol_id"],
|
|
305
|
+
canonical_id=r["source_symbol_id"],
|
|
306
|
+
source_symbol_id=r["source_symbol_id"],
|
|
307
|
+
callee=short_name,
|
|
308
|
+
target_symbol_id=r["target_symbol_id"],
|
|
309
|
+
line=r["start_line"],
|
|
310
|
+
start_line=r["start_line"],
|
|
311
|
+
end_line=r["end_line"],
|
|
312
|
+
relationship="CALLS",
|
|
313
|
+
evidence_class=ev_cls,
|
|
314
|
+
confidence=r["confidence"] if st == "current" else "LOW",
|
|
315
|
+
evidence=ev_text,
|
|
316
|
+
evidence_status=st,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# 2. Check static calls table (for unresolved or partially resolved calls)
|
|
321
|
+
if len(results) < max_results:
|
|
322
|
+
rem = max_results - len(results)
|
|
323
|
+
if target_canons:
|
|
324
|
+
ph = ",".join("?" for _ in target_canons)
|
|
325
|
+
call_rows = con.execute(
|
|
326
|
+
"SELECT source_path, callee, qualified_callee, line, confidence, source_symbol_id, resolved_symbol_id "
|
|
327
|
+
f"FROM calls WHERE resolved_symbol_id IN ({ph}) OR qualified_callee=? OR (resolved_symbol_id IS NULL AND callee=?) LIMIT ?",
|
|
328
|
+
[*target_canons, clean_sym, short_name, rem * 2],
|
|
329
|
+
).fetchall()
|
|
330
|
+
else:
|
|
331
|
+
call_rows = con.execute(
|
|
332
|
+
"SELECT source_path, callee, qualified_callee, line, confidence, source_symbol_id, resolved_symbol_id "
|
|
333
|
+
"FROM calls WHERE callee=? OR qualified_callee=? OR resolved_symbol_id=? LIMIT ?",
|
|
334
|
+
(short_name, clean_sym, clean_sym, rem * 2),
|
|
335
|
+
).fetchall()
|
|
336
|
+
|
|
337
|
+
target_canon_set = set(target_canons)
|
|
338
|
+
for cr in call_rows:
|
|
339
|
+
if (cr["source_path"], int(cr["line"])) in resolved_lines:
|
|
340
|
+
continue
|
|
341
|
+
if cr["resolved_symbol_id"] and target_canon_set and cr["resolved_symbol_id"] not in target_canon_set:
|
|
342
|
+
continue
|
|
343
|
+
key = (cr["source_path"], cr["source_symbol_id"], cr["line"])
|
|
344
|
+
if key in seen:
|
|
345
|
+
continue
|
|
346
|
+
seen.add(key)
|
|
347
|
+
conf = cr["confidence"] or "LOW"
|
|
348
|
+
rel = "CALLS" if conf in ("HIGH", "MEDIUM") else "POSSIBLE_CALLS"
|
|
349
|
+
ev_cls = "AST_VERIFIED" if rel == "CALLS" else ("UNKNOWN" if conf == "UNKNOWN" else "POSSIBLE")
|
|
350
|
+
results.append(
|
|
351
|
+
AttrDict(
|
|
352
|
+
file=cr["source_path"],
|
|
353
|
+
path=cr["source_path"],
|
|
354
|
+
symbol=cr["source_symbol_id"] or cr["source_path"],
|
|
355
|
+
caller=cr["source_symbol_id"] or cr["source_path"],
|
|
356
|
+
canonical_id=cr["source_symbol_id"] or "",
|
|
357
|
+
source_symbol_id=cr["source_symbol_id"],
|
|
358
|
+
callee=cr["callee"],
|
|
359
|
+
qualified_callee=cr["qualified_callee"],
|
|
360
|
+
target_symbol_id=cr["resolved_symbol_id"],
|
|
361
|
+
line=cr["line"],
|
|
362
|
+
start_line=cr["line"],
|
|
363
|
+
end_line=cr["line"],
|
|
364
|
+
relationship=rel,
|
|
365
|
+
evidence_class=ev_cls,
|
|
366
|
+
confidence=conf,
|
|
367
|
+
evidence=f"Static call site '{cr['callee']}()' in {cr['source_path']}:{cr['line']}",
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
if len(results) >= max_results:
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
final_callers = results[:max_results]
|
|
374
|
+
cache.set(cache_key, final_callers)
|
|
375
|
+
return final_callers
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
get_callers = find_callers
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def find_callees(
|
|
382
|
+
con: sqlite3.Connection,
|
|
383
|
+
symbol: str,
|
|
384
|
+
max_results: int = 50,
|
|
385
|
+
) -> list[AttrDict]:
|
|
386
|
+
"""Return callees called by the given symbol's body."""
|
|
387
|
+
gov = get_global_governor()
|
|
388
|
+
max_results = min(max_results, gov.policy.max_graph_nodes_per_query)
|
|
389
|
+
gen = index_generation(con)
|
|
390
|
+
cache = get_graph_cache()
|
|
391
|
+
cache_key = (gen, "callees", symbol, max_results)
|
|
392
|
+
cached = cache.get(cache_key)
|
|
393
|
+
if cached is not None and isinstance(cached, list):
|
|
394
|
+
return cached
|
|
395
|
+
|
|
396
|
+
repo = get_repository_from_con(con)
|
|
397
|
+
clean_sym = symbol.strip()
|
|
398
|
+
short_name = clean_sym.split(":")[-1].split(".")[-1]
|
|
399
|
+
results: list[AttrDict] = []
|
|
400
|
+
seen: set[tuple[str, str | None, int]] = set()
|
|
401
|
+
|
|
402
|
+
source_canons: list[str] = []
|
|
403
|
+
try:
|
|
404
|
+
sym_rows = con.execute(
|
|
405
|
+
"SELECT canonical_id, qualified_name FROM symbols "
|
|
406
|
+
"WHERE canonical_id=? OR qualified_name=? OR name=? "
|
|
407
|
+
"ORDER BY (canonical_id=?) DESC, (qualified_name=?) DESC",
|
|
408
|
+
(clean_sym, clean_sym, clean_sym, clean_sym, clean_sym),
|
|
409
|
+
).fetchall()
|
|
410
|
+
exact_canons = [str(r["canonical_id"]) for r in sym_rows if r["canonical_id"] == clean_sym]
|
|
411
|
+
exact_qnames = [str(r["canonical_id"]) for r in sym_rows if r["qualified_name"] == clean_sym]
|
|
412
|
+
if exact_canons:
|
|
413
|
+
source_canons = exact_canons
|
|
414
|
+
elif exact_qnames:
|
|
415
|
+
source_canons = exact_qnames
|
|
416
|
+
elif sym_rows and "." not in clean_sym:
|
|
417
|
+
source_canons = [str(r["canonical_id"]) for r in sym_rows]
|
|
418
|
+
except Exception:
|
|
419
|
+
source_canons = []
|
|
420
|
+
|
|
421
|
+
# 1. Check resolved references where source_symbol_id matches symbol
|
|
422
|
+
if source_canons:
|
|
423
|
+
ph = ",".join("?" for _ in source_canons)
|
|
424
|
+
rows = con.execute(
|
|
425
|
+
"SELECT source_symbol_id, target_symbol_id, relationship, confidence, "
|
|
426
|
+
"path, start_line, end_line, evidence, source_hash "
|
|
427
|
+
"FROM 'references' "
|
|
428
|
+
f"WHERE (source_symbol_id IN ({ph}) OR source_symbol_id=? OR source_symbol_id LIKE ?) "
|
|
429
|
+
"AND relationship IN ('CALLS', 'UNRESOLVED_REFERENCE') "
|
|
430
|
+
"ORDER BY confidence = 'HIGH' DESC, path, start_line LIMIT ?",
|
|
431
|
+
[*source_canons, clean_sym, f"%.{clean_sym}", max_results],
|
|
432
|
+
).fetchall()
|
|
433
|
+
elif "." in clean_sym:
|
|
434
|
+
rows = con.execute(
|
|
435
|
+
"SELECT source_symbol_id, target_symbol_id, relationship, confidence, "
|
|
436
|
+
"path, start_line, end_line, evidence, source_hash "
|
|
437
|
+
"FROM 'references' "
|
|
438
|
+
"WHERE (source_symbol_id=? OR source_symbol_id LIKE ?) "
|
|
439
|
+
"AND relationship IN ('CALLS', 'UNRESOLVED_REFERENCE') "
|
|
440
|
+
"ORDER BY confidence = 'HIGH' DESC, path, start_line LIMIT ?",
|
|
441
|
+
(clean_sym, f"%.{clean_sym}", max_results),
|
|
442
|
+
).fetchall()
|
|
443
|
+
else:
|
|
444
|
+
rows = con.execute(
|
|
445
|
+
"SELECT source_symbol_id, target_symbol_id, relationship, confidence, "
|
|
446
|
+
"path, start_line, end_line, evidence, source_hash "
|
|
447
|
+
"FROM 'references' "
|
|
448
|
+
"WHERE (source_symbol_id=? OR source_symbol_id LIKE ? OR source_symbol_id LIKE ?) "
|
|
449
|
+
"AND relationship IN ('CALLS', 'UNRESOLVED_REFERENCE') "
|
|
450
|
+
"ORDER BY confidence = 'HIGH' DESC, path, start_line LIMIT ?",
|
|
451
|
+
(clean_sym, f"%.{clean_sym}", f"%.{short_name}", max_results),
|
|
452
|
+
).fetchall()
|
|
453
|
+
|
|
454
|
+
for r in rows:
|
|
455
|
+
key = (r["path"], r["target_symbol_id"], r["start_line"])
|
|
456
|
+
if key in seen:
|
|
457
|
+
continue
|
|
458
|
+
callee_nm = r["target_symbol_id"].split(".")[-1] if r["target_symbol_id"] else "unresolved"
|
|
459
|
+
# Prune noisy standard primitive / builtin / tensor method calls that clutter the graph with UNKNOWN noise
|
|
460
|
+
if r["relationship"] == "UNRESOLVED_REFERENCE" and (
|
|
461
|
+
callee_nm in _NOISY_UNRESOLVED_NAMES or callee_nm.startswith("_")
|
|
462
|
+
):
|
|
463
|
+
continue
|
|
464
|
+
seen.add(key)
|
|
465
|
+
st = verify_source_hash(repo, r["path"], r["source_hash"])
|
|
466
|
+
ev_text = str(r["evidence"] or "")
|
|
467
|
+
ev_cls = (
|
|
468
|
+
"UNKNOWN"
|
|
469
|
+
if r["relationship"] == "UNRESOLVED_REFERENCE"
|
|
470
|
+
else ("DATAFLOW_VERIFIED" if ("resolved via" in ev_text.lower() or "factory" in ev_text.lower()) else "AST_VERIFIED")
|
|
471
|
+
)
|
|
472
|
+
results.append(
|
|
473
|
+
AttrDict(
|
|
474
|
+
callee=callee_nm,
|
|
475
|
+
qualified_callee=r["target_symbol_id"],
|
|
476
|
+
canonical_id=r["target_symbol_id"],
|
|
477
|
+
target_symbol_id=r["target_symbol_id"],
|
|
478
|
+
source_symbol_id=r["source_symbol_id"],
|
|
479
|
+
file=r["path"],
|
|
480
|
+
path=r["path"],
|
|
481
|
+
line=r["start_line"],
|
|
482
|
+
start_line=r["start_line"],
|
|
483
|
+
end_line=r["end_line"],
|
|
484
|
+
relationship=r["relationship"],
|
|
485
|
+
evidence_class=ev_cls,
|
|
486
|
+
confidence=r["confidence"] if st == "current" else "LOW",
|
|
487
|
+
evidence=ev_text,
|
|
488
|
+
evidence_status=st,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# 2. Check chunks / calls table if references did not contain calls
|
|
493
|
+
if not results:
|
|
494
|
+
chunk = con.execute(
|
|
495
|
+
"SELECT path, start_line, end_line, content FROM chunks "
|
|
496
|
+
"WHERE symbol=? OR symbol LIKE ? LIMIT 1",
|
|
497
|
+
(clean_sym, f"%.{clean_sym}"),
|
|
498
|
+
).fetchone()
|
|
499
|
+
if chunk:
|
|
500
|
+
call_rows = con.execute(
|
|
501
|
+
"SELECT callee, qualified_callee, resolved_symbol_id, line, confidence FROM calls "
|
|
502
|
+
"WHERE source_path=? AND line >= ? AND line <= ? LIMIT ?",
|
|
503
|
+
(chunk["path"], chunk["start_line"], chunk["end_line"], max_results),
|
|
504
|
+
).fetchall()
|
|
505
|
+
for cr in call_rows:
|
|
506
|
+
conf = cr["confidence"] or "LOW"
|
|
507
|
+
rel = "CALLS" if conf in ("HIGH", "MEDIUM") else "POSSIBLE_CALLS"
|
|
508
|
+
results.append(
|
|
509
|
+
AttrDict(
|
|
510
|
+
callee=cr["callee"],
|
|
511
|
+
qualified_callee=cr["resolved_symbol_id"] or cr["qualified_callee"],
|
|
512
|
+
canonical_id=cr["resolved_symbol_id"],
|
|
513
|
+
target_symbol_id=cr["resolved_symbol_id"],
|
|
514
|
+
line=cr["line"],
|
|
515
|
+
start_line=cr["line"],
|
|
516
|
+
end_line=cr["line"],
|
|
517
|
+
file=chunk["path"],
|
|
518
|
+
path=chunk["path"],
|
|
519
|
+
relationship=rel,
|
|
520
|
+
evidence_class="AST_VERIFIED" if rel == "CALLS" else ("UNKNOWN" if conf == "UNKNOWN" else "POSSIBLE"),
|
|
521
|
+
confidence=conf,
|
|
522
|
+
evidence=f"Call site in {chunk['path']}:{cr['line']}",
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
final_callees = results[:max_results]
|
|
527
|
+
cache.set(cache_key, final_callees)
|
|
528
|
+
return final_callees
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
get_callees = find_callees
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def find_implementations(
|
|
535
|
+
con: sqlite3.Connection,
|
|
536
|
+
canonical_id: str,
|
|
537
|
+
max_results: int = 50,
|
|
538
|
+
) -> list[AttrDict]:
|
|
539
|
+
"""Find classes or interfaces that EXTENDS or IMPLEMENTS canonical_id."""
|
|
540
|
+
short = canonical_id.split(".")[-1]
|
|
541
|
+
rows = con.execute(
|
|
542
|
+
"SELECT source_symbol_id, target_symbol_id, relationship, confidence, path, start_line, end_line, evidence "
|
|
543
|
+
"FROM 'references' "
|
|
544
|
+
"WHERE relationship IN ('EXTENDS', 'IMPLEMENTS') AND (target_symbol_id=? OR target_symbol_id LIKE ? OR target_symbol_id=?) "
|
|
545
|
+
"ORDER BY path, start_line LIMIT ?",
|
|
546
|
+
(canonical_id, f"%.{canonical_id}", short, max_results),
|
|
547
|
+
).fetchall()
|
|
548
|
+
|
|
549
|
+
return [
|
|
550
|
+
AttrDict(
|
|
551
|
+
implementor=r["source_symbol_id"],
|
|
552
|
+
target=r["target_symbol_id"],
|
|
553
|
+
relationship=r["relationship"],
|
|
554
|
+
confidence=r["confidence"],
|
|
555
|
+
file=r["path"],
|
|
556
|
+
line=r["start_line"],
|
|
557
|
+
evidence=r["evidence"],
|
|
558
|
+
)
|
|
559
|
+
for r in rows
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def get_call_graph(
|
|
564
|
+
con: sqlite3.Connection,
|
|
565
|
+
symbol: str | None = None,
|
|
566
|
+
depth: int = 2,
|
|
567
|
+
max_results: int = 100,
|
|
568
|
+
) -> list[AttrDict]:
|
|
569
|
+
"""Return call graph edges up to `depth` hops."""
|
|
570
|
+
if depth < 1:
|
|
571
|
+
depth = 1
|
|
572
|
+
if depth > 5:
|
|
573
|
+
depth = 5
|
|
574
|
+
|
|
575
|
+
edges: list[AttrDict] = []
|
|
576
|
+
visited: set[tuple[str, str]] = set()
|
|
577
|
+
|
|
578
|
+
if symbol:
|
|
579
|
+
frontier = {symbol}
|
|
580
|
+
for _ in range(depth):
|
|
581
|
+
if not frontier:
|
|
582
|
+
break
|
|
583
|
+
next_frontier: set[str] = set()
|
|
584
|
+
for sym in frontier:
|
|
585
|
+
callers = find_callers(con, sym, max_results=max_results)
|
|
586
|
+
for c in callers:
|
|
587
|
+
src = c.get("source_symbol_id") or c.get("file")
|
|
588
|
+
tgt = c.get("target_symbol_id") or sym
|
|
589
|
+
edge_key = (str(src), str(tgt))
|
|
590
|
+
if edge_key not in visited:
|
|
591
|
+
visited.add(edge_key)
|
|
592
|
+
edges.append(
|
|
593
|
+
AttrDict(
|
|
594
|
+
source=src,
|
|
595
|
+
target=tgt,
|
|
596
|
+
relationship=c.get("relationship", "CALLS"),
|
|
597
|
+
confidence=c.get("confidence", "HIGH"),
|
|
598
|
+
file=c.get("file"),
|
|
599
|
+
line=c.get("line"),
|
|
600
|
+
evidence=c.get("evidence", ""),
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
if src and str(src) not in frontier:
|
|
604
|
+
next_frontier.add(str(src))
|
|
605
|
+
frontier = next_frontier
|
|
606
|
+
return edges[:max_results]
|
|
607
|
+
else:
|
|
608
|
+
rows = con.execute(
|
|
609
|
+
"SELECT source, target, relationship, confidence, file, start_line, evidence "
|
|
610
|
+
"FROM graph_edges WHERE relationship IN ('CALLS', 'POSSIBLE_CALLS', 'HANDLED_BY', 'ROUTES_TO') "
|
|
611
|
+
"LIMIT ?",
|
|
612
|
+
(max_results,),
|
|
613
|
+
).fetchall()
|
|
614
|
+
return [
|
|
615
|
+
AttrDict(
|
|
616
|
+
source=r["source"],
|
|
617
|
+
target=r["target"],
|
|
618
|
+
relationship=r["relationship"],
|
|
619
|
+
confidence=r["confidence"],
|
|
620
|
+
file=r["file"],
|
|
621
|
+
line=r["start_line"],
|
|
622
|
+
evidence=r["evidence"],
|
|
623
|
+
)
|
|
624
|
+
for r in rows
|
|
625
|
+
]
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
# ---------------------------------------------------------------------------
|
|
629
|
+
# Dependency & Import Graph API
|
|
630
|
+
# ---------------------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def get_dependency_graph(
|
|
634
|
+
con: sqlite3.Connection,
|
|
635
|
+
path: str | None = None,
|
|
636
|
+
depth: int = 2,
|
|
637
|
+
max_results: int = 200,
|
|
638
|
+
) -> list[dict[str, object]]:
|
|
639
|
+
"""Return import dependency edges, optionally scoped to a starting file."""
|
|
640
|
+
if depth < 1:
|
|
641
|
+
depth = 1
|
|
642
|
+
if depth > 5:
|
|
643
|
+
depth = 5
|
|
644
|
+
|
|
645
|
+
if path:
|
|
646
|
+
visited: set[str] = set()
|
|
647
|
+
frontier = {path}
|
|
648
|
+
edges: list[dict[str, object]] = []
|
|
649
|
+
for _ in range(depth):
|
|
650
|
+
if not frontier:
|
|
651
|
+
break
|
|
652
|
+
next_frontier: set[str] = set()
|
|
653
|
+
for src in frontier:
|
|
654
|
+
if src in visited:
|
|
655
|
+
continue
|
|
656
|
+
visited.add(src)
|
|
657
|
+
rows = con.execute(
|
|
658
|
+
"SELECT module, resolved_path FROM imports WHERE source_path=? LIMIT ?",
|
|
659
|
+
(src, max_results),
|
|
660
|
+
).fetchall()
|
|
661
|
+
for row in rows:
|
|
662
|
+
edges.append(
|
|
663
|
+
{
|
|
664
|
+
"source": src,
|
|
665
|
+
"target": row["module"],
|
|
666
|
+
"resolved_target": row["resolved_path"],
|
|
667
|
+
"relationship": "IMPORTS",
|
|
668
|
+
"confidence": "HIGH",
|
|
669
|
+
"evidence": f"Import '{row['module']}' in {src}",
|
|
670
|
+
}
|
|
671
|
+
)
|
|
672
|
+
if row["resolved_path"] and row["resolved_path"] not in visited:
|
|
673
|
+
next_frontier.add(row["resolved_path"])
|
|
674
|
+
frontier = next_frontier - visited
|
|
675
|
+
return edges[:max_results]
|
|
676
|
+
else:
|
|
677
|
+
rows = con.execute(
|
|
678
|
+
"SELECT source_path, module, resolved_path FROM imports LIMIT ?", (max_results,)
|
|
679
|
+
).fetchall()
|
|
680
|
+
return [
|
|
681
|
+
{
|
|
682
|
+
"source": r["source_path"],
|
|
683
|
+
"target": r["module"],
|
|
684
|
+
"resolved_target": r["resolved_path"],
|
|
685
|
+
"relationship": "IMPORTS",
|
|
686
|
+
"confidence": "HIGH",
|
|
687
|
+
"evidence": "Parser-extracted import declaration",
|
|
688
|
+
}
|
|
689
|
+
for r in rows
|
|
690
|
+
]
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def find_importers(
|
|
694
|
+
con: sqlite3.Connection,
|
|
695
|
+
module: str,
|
|
696
|
+
max_results: int = 100,
|
|
697
|
+
) -> list[dict[str, object]]:
|
|
698
|
+
"""Return files that import a given module or file."""
|
|
699
|
+
rows = con.execute(
|
|
700
|
+
"SELECT source_path, module, name, alias, resolved_path FROM imports "
|
|
701
|
+
"WHERE module=? OR resolved_module=? OR resolved_path=? OR full_name LIKE ? "
|
|
702
|
+
"ORDER BY source_path LIMIT ?",
|
|
703
|
+
(module, module, module, f"{module}%", max_results),
|
|
704
|
+
).fetchall()
|
|
705
|
+
return [
|
|
706
|
+
{
|
|
707
|
+
"importer": r["source_path"],
|
|
708
|
+
"module": r["module"],
|
|
709
|
+
"name": r["name"],
|
|
710
|
+
"alias": r["alias"],
|
|
711
|
+
"resolved_path": r["resolved_path"],
|
|
712
|
+
"confidence": "HIGH",
|
|
713
|
+
"evidence": "Parser-extracted import declaration",
|
|
714
|
+
}
|
|
715
|
+
for r in rows
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# ---------------------------------------------------------------------------
|
|
720
|
+
# Test Intelligence API
|
|
721
|
+
# ---------------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
_TEST_FILE_RE = re.compile(
|
|
724
|
+
r"(^|[_/])test[_s]?[_/]|[_/]spec[_/]|test[_s]?\.(py|js|ts|jsx|tsx)$|spec\.(py|js|ts|jsx|tsx)$",
|
|
725
|
+
re.IGNORECASE,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def find_related_tests(
|
|
730
|
+
con: sqlite3.Connection,
|
|
731
|
+
symbol_or_path: str,
|
|
732
|
+
max_results: int = 20,
|
|
733
|
+
) -> list[dict[str, object]]:
|
|
734
|
+
"""Find test files and test symbols statically linked to a symbol, route, or file with verified provenance."""
|
|
735
|
+
gov = get_global_governor()
|
|
736
|
+
max_results = min(max_results, gov.policy.max_graph_nodes_per_query)
|
|
737
|
+
gen = index_generation(con)
|
|
738
|
+
cache = get_graph_cache()
|
|
739
|
+
cache_key = (gen, "related_tests", symbol_or_path, max_results)
|
|
740
|
+
cached = cache.get(cache_key)
|
|
741
|
+
if cached is not None and isinstance(cached, list):
|
|
742
|
+
return cached
|
|
743
|
+
|
|
744
|
+
clean_target = symbol_or_path.strip()
|
|
745
|
+
path_only = clean_target
|
|
746
|
+
for method in ("POST ", "GET ", "PUT ", "DELETE ", "PATCH ", "HEAD ", "OPTIONS "):
|
|
747
|
+
if path_only.startswith(method):
|
|
748
|
+
path_only = path_only[len(method):].strip()
|
|
749
|
+
break
|
|
750
|
+
tests: list[dict[str, object]] = []
|
|
751
|
+
seen: set[tuple[str, str | None]] = set() # (file, test_symbol)
|
|
752
|
+
|
|
753
|
+
def add_test(
|
|
754
|
+
file_path: str,
|
|
755
|
+
test_sym: str | None,
|
|
756
|
+
target_sym: str,
|
|
757
|
+
rel: str,
|
|
758
|
+
classification: str,
|
|
759
|
+
confidence: str,
|
|
760
|
+
evidence_class: str,
|
|
761
|
+
reason: str,
|
|
762
|
+
evidence: str,
|
|
763
|
+
start_line: int = 1,
|
|
764
|
+
) -> None:
|
|
765
|
+
key = (file_path, test_sym)
|
|
766
|
+
if key in seen:
|
|
767
|
+
return
|
|
768
|
+
seen.add(key)
|
|
769
|
+
tests.append(
|
|
770
|
+
{
|
|
771
|
+
"file": file_path,
|
|
772
|
+
"symbol": test_sym,
|
|
773
|
+
"test_symbol": test_sym,
|
|
774
|
+
"target_symbol": target_sym,
|
|
775
|
+
"relationship": rel,
|
|
776
|
+
"classification": classification,
|
|
777
|
+
"confidence": confidence,
|
|
778
|
+
"evidence_class": evidence_class,
|
|
779
|
+
"reason": reason,
|
|
780
|
+
"evidence": evidence,
|
|
781
|
+
"line": start_line,
|
|
782
|
+
"start_line": start_line,
|
|
783
|
+
}
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
# 1. Target identification
|
|
787
|
+
target_canonical_ids: set[str] = set()
|
|
788
|
+
target_routes: list[sqlite3.Row] = []
|
|
789
|
+
|
|
790
|
+
# Check if target is an API route or endpoint ID
|
|
791
|
+
route_rows = con.execute(
|
|
792
|
+
"SELECT endpoint_id, framework, http_method, route_path, handler_name, handler_canonical_id, file_path, line, evidence "
|
|
793
|
+
"FROM framework_routes WHERE endpoint_id = ? OR route_path = ? OR normalized_route = ? OR route_path = ?",
|
|
794
|
+
(
|
|
795
|
+
clean_target,
|
|
796
|
+
clean_target,
|
|
797
|
+
clean_target,
|
|
798
|
+
path_only,
|
|
799
|
+
),
|
|
800
|
+
).fetchall()
|
|
801
|
+
target_routes.extend(route_rows)
|
|
802
|
+
|
|
803
|
+
# Check if target is in symbols table
|
|
804
|
+
sym_rows = con.execute(
|
|
805
|
+
"SELECT canonical_id, qualified_name, name, path, start_line FROM symbols "
|
|
806
|
+
"WHERE canonical_id = ? OR qualified_name = ? OR name = ?",
|
|
807
|
+
(clean_target, clean_target, clean_target),
|
|
808
|
+
).fetchall()
|
|
809
|
+
for s_row in sym_rows:
|
|
810
|
+
target_canonical_ids.add(str(s_row["canonical_id"]))
|
|
811
|
+
|
|
812
|
+
if not target_canonical_ids and not target_routes:
|
|
813
|
+
# Check if target is a file
|
|
814
|
+
f_row = con.execute("SELECT path FROM files WHERE path = ?", (clean_target,)).fetchone()
|
|
815
|
+
if f_row:
|
|
816
|
+
file_syms = con.execute("SELECT canonical_id FROM symbols WHERE path = ?", (clean_target,)).fetchall()
|
|
817
|
+
for fs in file_syms:
|
|
818
|
+
target_canonical_ids.add(str(fs["canonical_id"]))
|
|
819
|
+
else:
|
|
820
|
+
target_canonical_ids.add(clean_target)
|
|
821
|
+
|
|
822
|
+
# Also check if any target_canonical_id is an API route handler
|
|
823
|
+
for c_id in list(target_canonical_ids):
|
|
824
|
+
c_short = c_id.split(".")[-1]
|
|
825
|
+
hr_rows = con.execute(
|
|
826
|
+
"SELECT endpoint_id, framework, http_method, route_path, handler_name, handler_canonical_id, file_path, line, evidence "
|
|
827
|
+
"FROM framework_routes WHERE handler_canonical_id = ? OR handler_name = ?",
|
|
828
|
+
(c_id, c_short),
|
|
829
|
+
).fetchall()
|
|
830
|
+
for hr in hr_rows:
|
|
831
|
+
if hr["endpoint_id"] not in [r["endpoint_id"] for r in target_routes]:
|
|
832
|
+
target_routes.append(hr)
|
|
833
|
+
|
|
834
|
+
# 2. Collect tests
|
|
835
|
+
|
|
836
|
+
# A. Route tests (TESTS_ROUTE edges)
|
|
837
|
+
for r in target_routes:
|
|
838
|
+
ep_id = r["endpoint_id"]
|
|
839
|
+
rt_edges = con.execute(
|
|
840
|
+
"SELECT source, target, relationship, confidence, file, start_line, evidence, evidence_class, reason "
|
|
841
|
+
"FROM graph_edges WHERE target = ? AND relationship IN ('TESTS_ROUTE', 'TESTS')",
|
|
842
|
+
(ep_id,),
|
|
843
|
+
).fetchall()
|
|
844
|
+
for e in rt_edges:
|
|
845
|
+
add_test(
|
|
846
|
+
file_path=e["file"],
|
|
847
|
+
test_sym=e["source"],
|
|
848
|
+
target_sym=ep_id,
|
|
849
|
+
rel="TESTS_ROUTE",
|
|
850
|
+
classification="VERIFIED_TEST",
|
|
851
|
+
confidence="HIGH",
|
|
852
|
+
evidence_class=e["evidence_class"] or "FRAMEWORK_VERIFIED",
|
|
853
|
+
reason=e["reason"] or f"http_client_test:{r['http_method']} {r['route_path']}",
|
|
854
|
+
evidence=e["evidence"] or f"Test exercises route {r['route_path']}",
|
|
855
|
+
start_line=e["start_line"],
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
# B. Event / Registry Handler tests (TESTS_EVENT_HANDLER)
|
|
859
|
+
for c_id in target_canonical_ids:
|
|
860
|
+
c_short = c_id.split(".")[-1]
|
|
861
|
+
dispatch_tests = con.execute(
|
|
862
|
+
"SELECT source, target, relationship, confidence, file, start_line, evidence, evidence_class, reason "
|
|
863
|
+
"FROM graph_edges WHERE (target = ? OR target LIKE ?) AND relationship = 'TESTS_EVENT_HANDLER'",
|
|
864
|
+
(c_id, f"%.{c_short}"),
|
|
865
|
+
).fetchall()
|
|
866
|
+
for dte in dispatch_tests:
|
|
867
|
+
add_test(
|
|
868
|
+
file_path=dte["file"],
|
|
869
|
+
test_sym=dte["source"],
|
|
870
|
+
target_sym=c_id,
|
|
871
|
+
rel="TESTS_EVENT_HANDLER",
|
|
872
|
+
classification="POSSIBLE_TEST",
|
|
873
|
+
confidence="MEDIUM",
|
|
874
|
+
evidence_class="FRAMEWORK_VERIFIED",
|
|
875
|
+
reason=dte["reason"] or "event_dispatch_test",
|
|
876
|
+
evidence=dte["evidence"] or f"Test dispatches event exercising {c_id}",
|
|
877
|
+
start_line=dte["start_line"],
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
# C. DI Provider tests (TESTS_PROVIDER)
|
|
881
|
+
for c_id in target_canonical_ids:
|
|
882
|
+
c_short = c_id.split(".")[-1]
|
|
883
|
+
di_edges = con.execute(
|
|
884
|
+
"SELECT source, target FROM graph_edges WHERE (source = ? OR target = ?) AND relationship IN ('PROVIDES', 'RESOLVES_DEPENDENCY', 'INJECTS')",
|
|
885
|
+
(c_id, c_id),
|
|
886
|
+
).fetchall()
|
|
887
|
+
related_prov_canons = {c_id}
|
|
888
|
+
for de in di_edges:
|
|
889
|
+
related_prov_canons.add(de["source"])
|
|
890
|
+
related_prov_canons.add(de["target"])
|
|
891
|
+
|
|
892
|
+
for prov in related_prov_canons:
|
|
893
|
+
prov_edges = con.execute(
|
|
894
|
+
"SELECT source, target, relationship, confidence, file, start_line, evidence, evidence_class, reason "
|
|
895
|
+
"FROM graph_edges WHERE (target = ? OR target LIKE ?) AND relationship = 'TESTS_PROVIDER'",
|
|
896
|
+
(prov, f"%.{prov.split('.')[-1]}"),
|
|
897
|
+
).fetchall()
|
|
898
|
+
for pe in prov_edges:
|
|
899
|
+
add_test(
|
|
900
|
+
file_path=pe["file"],
|
|
901
|
+
test_sym=pe["source"],
|
|
902
|
+
target_sym=prov,
|
|
903
|
+
rel="TESTS_PROVIDER",
|
|
904
|
+
classification="VERIFIED_TEST",
|
|
905
|
+
confidence="HIGH",
|
|
906
|
+
evidence_class="DATAFLOW_VERIFIED",
|
|
907
|
+
reason=pe["reason"] or "dependency_provider_test",
|
|
908
|
+
evidence=pe["evidence"] or f"Test fixture provides {prov}",
|
|
909
|
+
start_line=pe["start_line"],
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
# D. Direct symbol tests via graph_edges and references (TESTS, TESTS_SYMBOL, CALLS)
|
|
913
|
+
for c_id in target_canonical_ids:
|
|
914
|
+
c_short = c_id.split(".")[-1]
|
|
915
|
+
edge_rows = con.execute(
|
|
916
|
+
"SELECT source, target, relationship, confidence, file, start_line, evidence, evidence_class, reason "
|
|
917
|
+
"FROM graph_edges WHERE (target = ? OR target LIKE ?) AND relationship IN ('TESTS', 'TESTS_SYMBOL')",
|
|
918
|
+
(c_id, f"%.{c_short}"),
|
|
919
|
+
).fetchall()
|
|
920
|
+
for e in edge_rows:
|
|
921
|
+
if _TEST_FILE_RE.search(e["file"]):
|
|
922
|
+
add_test(
|
|
923
|
+
file_path=e["file"],
|
|
924
|
+
test_sym=e["source"],
|
|
925
|
+
target_sym=c_id,
|
|
926
|
+
rel="TESTS_SYMBOL",
|
|
927
|
+
classification="VERIFIED_TEST",
|
|
928
|
+
confidence=e["confidence"] or "HIGH",
|
|
929
|
+
evidence_class=e["evidence_class"] or "AST_VERIFIED",
|
|
930
|
+
reason=e["reason"] or "direct_test_call",
|
|
931
|
+
evidence=e["evidence"] or f"Test exercises {c_id}",
|
|
932
|
+
start_line=e["start_line"],
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
ref_rows = con.execute(
|
|
936
|
+
"SELECT path, source_symbol_id, start_line, evidence FROM 'references' "
|
|
937
|
+
"WHERE (target_symbol_id = ? OR target_symbol_id LIKE ? OR target_symbol_id = ?) "
|
|
938
|
+
"AND relationship IN ('CALLS', 'TESTS')",
|
|
939
|
+
(c_id, f"%.{c_short}", c_short),
|
|
940
|
+
).fetchall()
|
|
941
|
+
for r in ref_rows:
|
|
942
|
+
if _TEST_FILE_RE.search(r["path"]):
|
|
943
|
+
add_test(
|
|
944
|
+
file_path=r["path"],
|
|
945
|
+
test_sym=r["source_symbol_id"],
|
|
946
|
+
target_sym=c_id,
|
|
947
|
+
rel="TESTS_SYMBOL",
|
|
948
|
+
classification="VERIFIED_TEST",
|
|
949
|
+
confidence="HIGH",
|
|
950
|
+
evidence_class="AST_VERIFIED",
|
|
951
|
+
reason="direct_test_call",
|
|
952
|
+
evidence=f"Test verifies symbol: {r['evidence']}",
|
|
953
|
+
start_line=r["start_line"],
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
# E. Test imports target symbol specifically
|
|
957
|
+
for c_id in target_canonical_ids:
|
|
958
|
+
c_short = c_id.split(".")[-1]
|
|
959
|
+
imp_rows = con.execute(
|
|
960
|
+
"SELECT DISTINCT source_path, line FROM imports "
|
|
961
|
+
"WHERE (imported_name = ? OR local_name = ? OR name = ?)",
|
|
962
|
+
(c_short, c_short, c_short),
|
|
963
|
+
).fetchall()
|
|
964
|
+
for imp in imp_rows:
|
|
965
|
+
p = imp["source_path"]
|
|
966
|
+
if _TEST_FILE_RE.search(p):
|
|
967
|
+
add_test(
|
|
968
|
+
file_path=p,
|
|
969
|
+
test_sym=None,
|
|
970
|
+
target_sym=c_id,
|
|
971
|
+
rel="TEST_IMPORTS_SYMBOL",
|
|
972
|
+
classification="VERIFIED_TEST",
|
|
973
|
+
confidence="HIGH",
|
|
974
|
+
evidence_class="AST_VERIFIED",
|
|
975
|
+
reason="test_imports_target_symbol",
|
|
976
|
+
evidence=f"Test file specifically imports target '{c_short}'",
|
|
977
|
+
start_line=imp["line"] or 1,
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
# F. Indirect test helper calls
|
|
981
|
+
for c_id in target_canonical_ids:
|
|
982
|
+
direct_callers = con.execute(
|
|
983
|
+
"SELECT source_path, source_symbol_id, line FROM calls "
|
|
984
|
+
"WHERE (qualified_callee = ? OR callee = ?) AND (source_path LIKE '%test%' OR source_path LIKE '%spec%')",
|
|
985
|
+
(c_id, c_id.split(".")[-1]),
|
|
986
|
+
).fetchall()
|
|
987
|
+
for dc in direct_callers:
|
|
988
|
+
helper_canon = dc["source_symbol_id"]
|
|
989
|
+
if helper_canon and not helper_canon.split(".")[-1].startswith("test_"):
|
|
990
|
+
outer_calls = con.execute(
|
|
991
|
+
"SELECT source_path, source_symbol_id, line FROM calls WHERE (qualified_callee = ? OR callee = ?)",
|
|
992
|
+
(helper_canon, helper_canon.split(".")[-1]),
|
|
993
|
+
).fetchall()
|
|
994
|
+
for oc in outer_calls:
|
|
995
|
+
if _TEST_FILE_RE.search(oc["source_path"]):
|
|
996
|
+
add_test(
|
|
997
|
+
file_path=oc["source_path"],
|
|
998
|
+
test_sym=oc["source_symbol_id"],
|
|
999
|
+
target_sym=c_id,
|
|
1000
|
+
rel="TEST_HELPER_CALL",
|
|
1001
|
+
classification="POSSIBLE_TEST",
|
|
1002
|
+
confidence="MEDIUM",
|
|
1003
|
+
evidence_class="AST_VERIFIED",
|
|
1004
|
+
reason=f"indirect_test_helper:{helper_canon.split('.')[-1]}",
|
|
1005
|
+
evidence=f"Test calls helper '{helper_canon}' which calls target",
|
|
1006
|
+
start_line=oc["line"] or 1,
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
# G. Imports containing module fallback
|
|
1010
|
+
module_name = (
|
|
1011
|
+
clean_target.replace("/", ".").rsplit(".", 1)[0]
|
|
1012
|
+
if "." in clean_target
|
|
1013
|
+
else clean_target
|
|
1014
|
+
)
|
|
1015
|
+
if not tests:
|
|
1016
|
+
mod_imps = con.execute(
|
|
1017
|
+
"SELECT DISTINCT source_path FROM imports "
|
|
1018
|
+
"WHERE module = ? OR resolved_module = ?",
|
|
1019
|
+
(module_name, module_name),
|
|
1020
|
+
).fetchall()
|
|
1021
|
+
for mi in mod_imps:
|
|
1022
|
+
p = mi["source_path"]
|
|
1023
|
+
if _TEST_FILE_RE.search(p):
|
|
1024
|
+
add_test(
|
|
1025
|
+
file_path=p,
|
|
1026
|
+
test_sym=None,
|
|
1027
|
+
target_sym=clean_target,
|
|
1028
|
+
rel="TEST_IMPORTS_MODULE",
|
|
1029
|
+
classification="VERIFIED_TEST",
|
|
1030
|
+
confidence="HIGH",
|
|
1031
|
+
evidence_class="AST_VERIFIED",
|
|
1032
|
+
reason="test_imports_module",
|
|
1033
|
+
evidence=f"Test file imports module '{module_name}'",
|
|
1034
|
+
start_line=1,
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
if not tests:
|
|
1038
|
+
return [
|
|
1039
|
+
{
|
|
1040
|
+
"result": "no_static_link_found",
|
|
1041
|
+
"detail": (
|
|
1042
|
+
f"No statically linked test found for '{symbol_or_path}'. "
|
|
1043
|
+
"This does not mean tests are completely absent — dynamic test frameworks "
|
|
1044
|
+
"or naming conventions outside our patterns may cover this symbol."
|
|
1045
|
+
),
|
|
1046
|
+
}
|
|
1047
|
+
]
|
|
1048
|
+
|
|
1049
|
+
final_tests = tests[:max_results]
|
|
1050
|
+
cache.set(cache_key, final_tests)
|
|
1051
|
+
return final_tests
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def find_affected_tests(
|
|
1055
|
+
con: sqlite3.Connection,
|
|
1056
|
+
changed_targets: list[str],
|
|
1057
|
+
max_results: int = 50,
|
|
1058
|
+
) -> list[dict[str, object]]:
|
|
1059
|
+
"""Return tests affected by changes to any of `changed_targets`."""
|
|
1060
|
+
affected: list[dict[str, object]] = []
|
|
1061
|
+
seen: set[str] = set()
|
|
1062
|
+
for target in changed_targets:
|
|
1063
|
+
res = find_related_tests(con, target, max_results=max_results)
|
|
1064
|
+
for t in res:
|
|
1065
|
+
if "result" not in t:
|
|
1066
|
+
f = str(t.get("file", ""))
|
|
1067
|
+
sym = str(t.get("symbol", ""))
|
|
1068
|
+
key = f"{f}:{sym}"
|
|
1069
|
+
if key not in seen:
|
|
1070
|
+
seen.add(key)
|
|
1071
|
+
affected.append(t)
|
|
1072
|
+
return affected[:max_results]
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
# ---------------------------------------------------------------------------
|
|
1076
|
+
# Impact Analysis Engine
|
|
1077
|
+
# ---------------------------------------------------------------------------
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def analyze_impact(
|
|
1081
|
+
con: sqlite3.Connection,
|
|
1082
|
+
symbol_or_file: str,
|
|
1083
|
+
max_depth: int = 3,
|
|
1084
|
+
max_results: int = 100,
|
|
1085
|
+
) -> dict[str, object]:
|
|
1086
|
+
"""Perform a bounded reverse-graph impact analysis across 5 explicit categories:
|
|
1087
|
+
DIRECT, FRAMEWORK, SEMANTIC, DI, and TEST.
|
|
1088
|
+
"""
|
|
1089
|
+
clean_target = symbol_or_file.strip()
|
|
1090
|
+
short_name = clean_target.split(".")[-1]
|
|
1091
|
+
is_file = "/" in clean_target or clean_target.endswith((".py", ".js", ".ts", ".jsx", ".tsx"))
|
|
1092
|
+
|
|
1093
|
+
direct_callers: list[dict[str, object]] = []
|
|
1094
|
+
transitive_callers: list[dict[str, object]] = []
|
|
1095
|
+
dependencies: list[dict[str, object]] = []
|
|
1096
|
+
dependent_modules: list[dict[str, object]] = []
|
|
1097
|
+
related_apis: list[dict[str, object]] = []
|
|
1098
|
+
related_tests = find_related_tests(con, clean_target, max_results=20)
|
|
1099
|
+
|
|
1100
|
+
# Structured impact items list & 5-category buckets
|
|
1101
|
+
impact_items: list[dict[str, object]] = []
|
|
1102
|
+
seen_impact: set[tuple[str, str | None, str]] = set() # (category, source, relationship)
|
|
1103
|
+
|
|
1104
|
+
def add_impact_item(
|
|
1105
|
+
source: str,
|
|
1106
|
+
target: str | None,
|
|
1107
|
+
relationship: str,
|
|
1108
|
+
category: str, # DIRECT | FRAMEWORK | SEMANTIC | DI | TEST
|
|
1109
|
+
evidence_class: str,
|
|
1110
|
+
reason: str,
|
|
1111
|
+
impact_reason: str,
|
|
1112
|
+
file_path: str | None,
|
|
1113
|
+
start_line: int | None = None,
|
|
1114
|
+
end_line: int | None = None,
|
|
1115
|
+
rank: int = 5,
|
|
1116
|
+
freshness: str = "FRESH",
|
|
1117
|
+
) -> None:
|
|
1118
|
+
key = (category, source, relationship)
|
|
1119
|
+
if key in seen_impact:
|
|
1120
|
+
return
|
|
1121
|
+
seen_impact.add(key)
|
|
1122
|
+
impact_items.append(
|
|
1123
|
+
{
|
|
1124
|
+
"source": source,
|
|
1125
|
+
"target": target,
|
|
1126
|
+
"relationship": relationship,
|
|
1127
|
+
"category": category,
|
|
1128
|
+
"evidence_class": evidence_class,
|
|
1129
|
+
"reason": reason,
|
|
1130
|
+
"impact_reason": impact_reason,
|
|
1131
|
+
"file": file_path,
|
|
1132
|
+
"start_line": start_line,
|
|
1133
|
+
"end_line": end_line,
|
|
1134
|
+
"rank": rank,
|
|
1135
|
+
"freshness": freshness,
|
|
1136
|
+
}
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
# Check if symbol exists in database
|
|
1140
|
+
target_sym = con.execute(
|
|
1141
|
+
"SELECT canonical_id, qualified_name, name, path, start_line, end_line FROM symbols "
|
|
1142
|
+
"WHERE canonical_id = ? OR qualified_name = ? OR name = ? LIMIT 1",
|
|
1143
|
+
(clean_target, clean_target, clean_target),
|
|
1144
|
+
).fetchone()
|
|
1145
|
+
|
|
1146
|
+
target_file = con.execute("SELECT path FROM files WHERE path = ?", (clean_target,)).fetchone()
|
|
1147
|
+
|
|
1148
|
+
# Handle deleted/unknown symbol gracefully
|
|
1149
|
+
if not target_sym and not target_file and not is_file:
|
|
1150
|
+
add_impact_item(
|
|
1151
|
+
source=clean_target,
|
|
1152
|
+
target=None,
|
|
1153
|
+
relationship="UNKNOWN",
|
|
1154
|
+
category="DIRECT",
|
|
1155
|
+
evidence_class="UNKNOWN",
|
|
1156
|
+
reason="Symbol not found in current index (may be deleted or renamed)",
|
|
1157
|
+
impact_reason="deleted_symbol",
|
|
1158
|
+
file_path=None,
|
|
1159
|
+
rank=8,
|
|
1160
|
+
freshness="STALE",
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
# 1. Direct callers (DIRECT)
|
|
1164
|
+
direct = find_callers(con, clean_target, max_results=max_results)
|
|
1165
|
+
for d in direct:
|
|
1166
|
+
f_p = d.get("file")
|
|
1167
|
+
s_id = d.get("symbol")
|
|
1168
|
+
ln = d.get("line")
|
|
1169
|
+
direct_callers.append(
|
|
1170
|
+
{
|
|
1171
|
+
"file": f_p,
|
|
1172
|
+
"symbol": s_id,
|
|
1173
|
+
"line": ln,
|
|
1174
|
+
"relationship": d.get("relationship", "CALLS"),
|
|
1175
|
+
"confidence": d.get("confidence", "HIGH"),
|
|
1176
|
+
"label": "verified" if d.get("confidence") == "HIGH" else "possible",
|
|
1177
|
+
"evidence": d.get("evidence", ""),
|
|
1178
|
+
}
|
|
1179
|
+
)
|
|
1180
|
+
add_impact_item(
|
|
1181
|
+
source=str(s_id or f_p),
|
|
1182
|
+
target=clean_target,
|
|
1183
|
+
relationship="CALLS",
|
|
1184
|
+
category="DIRECT",
|
|
1185
|
+
evidence_class="AST_VERIFIED",
|
|
1186
|
+
reason=f"Direct call: {s_id} calls {clean_target}",
|
|
1187
|
+
impact_reason="direct_caller",
|
|
1188
|
+
file_path=str(f_p) if f_p else None,
|
|
1189
|
+
start_line=int(ln) if ln else None,
|
|
1190
|
+
rank=2,
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
# 2. Transitive callers (DIRECT, depth 2+)
|
|
1194
|
+
if max_depth > 1:
|
|
1195
|
+
seen_transitive = {(str(c_dict.get("file")), str(c_dict.get("symbol"))) for c_dict in direct_callers}
|
|
1196
|
+
for caller_entry in direct_callers[:15]:
|
|
1197
|
+
src_sym = caller_entry.get("symbol") or caller_entry.get("file")
|
|
1198
|
+
if src_sym:
|
|
1199
|
+
second = find_callers(con, str(src_sym), max_results=10)
|
|
1200
|
+
for s in second:
|
|
1201
|
+
key = (str(s.get("file")), str(s.get("symbol")))
|
|
1202
|
+
if key not in seen_transitive:
|
|
1203
|
+
seen_transitive.add(key)
|
|
1204
|
+
transitive_callers.append(
|
|
1205
|
+
{
|
|
1206
|
+
"file": s.get("file"),
|
|
1207
|
+
"symbol": s.get("symbol"),
|
|
1208
|
+
"line": s.get("line"),
|
|
1209
|
+
"relationship": s.get("relationship", "CALLS"),
|
|
1210
|
+
"confidence": s.get("confidence", "LOW"),
|
|
1211
|
+
"label": "possible",
|
|
1212
|
+
"evidence": s.get("evidence", ""),
|
|
1213
|
+
}
|
|
1214
|
+
)
|
|
1215
|
+
add_impact_item(
|
|
1216
|
+
source=str(s.get("symbol") or s.get("file")),
|
|
1217
|
+
target=str(src_sym),
|
|
1218
|
+
relationship="CALLS",
|
|
1219
|
+
category="DIRECT",
|
|
1220
|
+
evidence_class="POSSIBLE",
|
|
1221
|
+
reason=f"Transitive call through {src_sym}",
|
|
1222
|
+
impact_reason="indirect_dependent",
|
|
1223
|
+
file_path=str(s.get("file")) if s.get("file") else None,
|
|
1224
|
+
start_line=int(s["line"]) if s.get("line") else None,
|
|
1225
|
+
rank=7,
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
# 3. Dependencies & Importers (DIRECT)
|
|
1229
|
+
if is_file:
|
|
1230
|
+
deps = get_dependency_graph(con, clean_target, depth=1, max_results=50)
|
|
1231
|
+
for dep_entry in deps:
|
|
1232
|
+
dependencies.append(
|
|
1233
|
+
{
|
|
1234
|
+
"target": dep_entry.get("target"),
|
|
1235
|
+
"resolved_target": dep_entry.get("resolved_target"),
|
|
1236
|
+
"relationship": "IMPORTS",
|
|
1237
|
+
"label": "verified",
|
|
1238
|
+
"evidence": dep_entry.get("evidence", ""),
|
|
1239
|
+
}
|
|
1240
|
+
)
|
|
1241
|
+
mod_name = clean_target.replace("/", ".").rsplit(".", 1)[0]
|
|
1242
|
+
importers = find_importers(con, mod_name, max_results=50)
|
|
1243
|
+
for imp in importers:
|
|
1244
|
+
dependent_modules.append(
|
|
1245
|
+
{
|
|
1246
|
+
"importer": imp.get("importer"),
|
|
1247
|
+
"module": imp.get("module"),
|
|
1248
|
+
"label": "verified",
|
|
1249
|
+
"evidence": imp.get("evidence", ""),
|
|
1250
|
+
}
|
|
1251
|
+
)
|
|
1252
|
+
add_impact_item(
|
|
1253
|
+
source=str(imp.get("importer")),
|
|
1254
|
+
target=mod_name,
|
|
1255
|
+
relationship="IMPORTS",
|
|
1256
|
+
category="DIRECT",
|
|
1257
|
+
evidence_class="AST_VERIFIED",
|
|
1258
|
+
reason=f"Direct import of module '{mod_name}'",
|
|
1259
|
+
impact_reason="direct_import",
|
|
1260
|
+
file_path=str(imp.get("importer")),
|
|
1261
|
+
rank=6,
|
|
1262
|
+
)
|
|
1263
|
+
else:
|
|
1264
|
+
defn = target_sym
|
|
1265
|
+
if defn:
|
|
1266
|
+
deps = get_dependency_graph(con, defn["path"], depth=1, max_results=50)
|
|
1267
|
+
for dep_entry in deps:
|
|
1268
|
+
dependencies.append(
|
|
1269
|
+
{
|
|
1270
|
+
"target": dep_entry.get("target"),
|
|
1271
|
+
"resolved_target": dep_entry.get("resolved_target"),
|
|
1272
|
+
"relationship": "IMPORTS",
|
|
1273
|
+
"label": "verified",
|
|
1274
|
+
"evidence": dep_entry.get("evidence", ""),
|
|
1275
|
+
}
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
# 4. Related APIs & Framework Routes (FRAMEWORK)
|
|
1279
|
+
target_canon = target_sym["canonical_id"] if target_sym else clean_target
|
|
1280
|
+
route_rows = con.execute(
|
|
1281
|
+
"SELECT endpoint_id, framework, http_method, route_path, handler_canonical_id, file_path, line, evidence "
|
|
1282
|
+
"FROM framework_routes WHERE handler_canonical_id = ? OR handler_canonical_id LIKE ? OR handler_name = ?",
|
|
1283
|
+
(target_canon, f"%{short_name}%", short_name),
|
|
1284
|
+
).fetchall()
|
|
1285
|
+
for rr in route_rows:
|
|
1286
|
+
related_apis.append(
|
|
1287
|
+
{
|
|
1288
|
+
"endpoint_id": rr["endpoint_id"],
|
|
1289
|
+
"framework": rr["framework"],
|
|
1290
|
+
"http_method": rr["http_method"],
|
|
1291
|
+
"route": rr["route_path"],
|
|
1292
|
+
"handler": rr["handler_canonical_id"],
|
|
1293
|
+
"file": rr["file_path"],
|
|
1294
|
+
"line": rr["line"],
|
|
1295
|
+
"label": "verified",
|
|
1296
|
+
"evidence": rr["evidence"],
|
|
1297
|
+
}
|
|
1298
|
+
)
|
|
1299
|
+
add_impact_item(
|
|
1300
|
+
source=rr["endpoint_id"],
|
|
1301
|
+
target=rr["handler_canonical_id"],
|
|
1302
|
+
relationship="ROUTES_TO",
|
|
1303
|
+
category="FRAMEWORK",
|
|
1304
|
+
evidence_class="FRAMEWORK_VERIFIED",
|
|
1305
|
+
reason=f"Route {rr['route_path']} [{rr['http_method']}] routed to handler",
|
|
1306
|
+
impact_reason="route_handler",
|
|
1307
|
+
file_path=rr["file_path"],
|
|
1308
|
+
start_line=rr["line"],
|
|
1309
|
+
rank=3,
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
# 5. Semantic Registries & Event Handlers (SEMANTIC)
|
|
1313
|
+
sem_rows = con.execute(
|
|
1314
|
+
"SELECT source, target, relationship, confidence, file, start_line, evidence, evidence_class, reason "
|
|
1315
|
+
"FROM graph_edges WHERE (target = ? OR target LIKE ?) "
|
|
1316
|
+
"AND relationship IN ('REGISTERS', 'EVENT_LISTENER', 'TASK_HANDLER', 'COMMAND_HANDLER', 'DISPATCHES_TO')",
|
|
1317
|
+
(target_canon, f"%{short_name}"),
|
|
1318
|
+
).fetchall()
|
|
1319
|
+
for sr in sem_rows:
|
|
1320
|
+
rel = sr["relationship"]
|
|
1321
|
+
imp_reason = (
|
|
1322
|
+
"registered_handler"
|
|
1323
|
+
if rel == "REGISTERS"
|
|
1324
|
+
else ("event_listener" if rel == "EVENT_LISTENER" else ("dispatches_to" if rel == "DISPATCHES_TO" else "semantic_handler"))
|
|
1325
|
+
)
|
|
1326
|
+
add_impact_item(
|
|
1327
|
+
source=sr["source"],
|
|
1328
|
+
target=sr["target"],
|
|
1329
|
+
relationship=rel,
|
|
1330
|
+
category="SEMANTIC",
|
|
1331
|
+
evidence_class=sr["evidence_class"] or "FRAMEWORK_VERIFIED",
|
|
1332
|
+
reason=sr["reason"] or f"Semantic registration: {sr['source']} {rel} {sr['target']}",
|
|
1333
|
+
impact_reason=imp_reason,
|
|
1334
|
+
file_path=sr["file"],
|
|
1335
|
+
start_line=sr["start_line"],
|
|
1336
|
+
rank=5,
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
# 6. Dependency Injection & Providers (DI) + Roblox Knit/Flamework Services & Remotes
|
|
1340
|
+
di_rows = con.execute(
|
|
1341
|
+
"SELECT source, target, relationship, confidence, file, start_line, evidence, evidence_class, reason "
|
|
1342
|
+
"FROM graph_edges WHERE (source = ? OR target = ? OR source LIKE ? OR target LIKE ?) "
|
|
1343
|
+
"AND relationship IN ('INJECTS', 'PROVIDES', 'RESOLVES_DEPENDENCY', 'PROVIDES_SERVICE', 'GETS_SERVICE', "
|
|
1344
|
+
"'CLIENT_DISPATCHES_REMOTE', 'SERVER_HANDLES_REMOTE', 'REQUIRES_MODULE', "
|
|
1345
|
+
"'CONFIGURES_PERSISTENCE', 'READS_PERSISTENCE', 'WRITES_PERSISTENCE')",
|
|
1346
|
+
(target_canon, target_canon, f"%{short_name}", f"%{short_name}"),
|
|
1347
|
+
).fetchall()
|
|
1348
|
+
for dr in di_rows:
|
|
1349
|
+
rel = dr["relationship"]
|
|
1350
|
+
cat_bucket = (
|
|
1351
|
+
"FRAMEWORK"
|
|
1352
|
+
if rel in ("CLIENT_DISPATCHES_REMOTE", "SERVER_HANDLES_REMOTE", "CONFIGURES_PERSISTENCE", "READS_PERSISTENCE", "WRITES_PERSISTENCE")
|
|
1353
|
+
else ("DIRECT" if rel == "REQUIRES_MODULE" else "DI")
|
|
1354
|
+
)
|
|
1355
|
+
imp_reason = (
|
|
1356
|
+
"dependency_injected"
|
|
1357
|
+
if rel == "INJECTS"
|
|
1358
|
+
else ("dependency_provider" if rel in ("PROVIDES", "PROVIDES_SERVICE") else rel.lower())
|
|
1359
|
+
)
|
|
1360
|
+
add_impact_item(
|
|
1361
|
+
source=dr["source"],
|
|
1362
|
+
target=dr["target"],
|
|
1363
|
+
relationship=rel,
|
|
1364
|
+
category=cat_bucket,
|
|
1365
|
+
evidence_class=dr["evidence_class"] or "DATAFLOW_VERIFIED",
|
|
1366
|
+
reason=dr["reason"] or f"Relationship: {dr['source']} {rel} {dr['target']}",
|
|
1367
|
+
impact_reason=imp_reason,
|
|
1368
|
+
file_path=dr["file"],
|
|
1369
|
+
start_line=dr["start_line"],
|
|
1370
|
+
rank=4,
|
|
1371
|
+
)
|
|
1372
|
+
# If target_canon is a Remote or connected to a Remote, also traverse the other side of the Remote
|
|
1373
|
+
if rel == "SERVER_HANDLES_REMOTE" and (dr["target"] == target_canon or dr["target"].endswith(f".{short_name}")):
|
|
1374
|
+
remote_id = dr["source"]
|
|
1375
|
+
client_senders = con.execute(
|
|
1376
|
+
"SELECT source, target, relationship, confidence, file, start_line, evidence, evidence_class, reason "
|
|
1377
|
+
"FROM graph_edges WHERE target = ? AND relationship = 'CLIENT_DISPATCHES_REMOTE'",
|
|
1378
|
+
(remote_id,),
|
|
1379
|
+
).fetchall()
|
|
1380
|
+
for cs in client_senders:
|
|
1381
|
+
add_impact_item(
|
|
1382
|
+
source=cs["source"],
|
|
1383
|
+
target=remote_id,
|
|
1384
|
+
relationship="CLIENT_DISPATCHES_REMOTE",
|
|
1385
|
+
category="FRAMEWORK",
|
|
1386
|
+
evidence_class=cs["evidence_class"] or "ROJO_VERIFIED",
|
|
1387
|
+
reason=cs["reason"] or f"Client dispatches remote {remote_id} handled by {target_canon}",
|
|
1388
|
+
impact_reason="remote_client_sender",
|
|
1389
|
+
file_path=cs["file"],
|
|
1390
|
+
start_line=cs["start_line"],
|
|
1391
|
+
rank=3,
|
|
1392
|
+
)
|
|
1393
|
+
elif rel == "CLIENT_DISPATCHES_REMOTE" and (dr["source"] == target_canon or dr["source"].endswith(f".{short_name}")):
|
|
1394
|
+
remote_id = dr["target"]
|
|
1395
|
+
server_handlers = con.execute(
|
|
1396
|
+
"SELECT source, target, relationship, confidence, file, start_line, evidence, evidence_class, reason "
|
|
1397
|
+
"FROM graph_edges WHERE source = ? AND relationship = 'SERVER_HANDLES_REMOTE'",
|
|
1398
|
+
(remote_id,),
|
|
1399
|
+
).fetchall()
|
|
1400
|
+
for sh in server_handlers:
|
|
1401
|
+
add_impact_item(
|
|
1402
|
+
source=remote_id,
|
|
1403
|
+
target=sh["target"],
|
|
1404
|
+
relationship="SERVER_HANDLES_REMOTE",
|
|
1405
|
+
category="FRAMEWORK",
|
|
1406
|
+
evidence_class=sh["evidence_class"] or "ROJO_VERIFIED",
|
|
1407
|
+
reason=sh["reason"] or f"Server handles remote {remote_id} dispatched by {target_canon}",
|
|
1408
|
+
impact_reason="remote_server_handler",
|
|
1409
|
+
file_path=sh["file"],
|
|
1410
|
+
start_line=sh["start_line"],
|
|
1411
|
+
rank=3,
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
# 7. Related Tests (TEST)
|
|
1415
|
+
for t in related_tests:
|
|
1416
|
+
if "result" not in t:
|
|
1417
|
+
f_p = t.get("file")
|
|
1418
|
+
t_sym = t.get("symbol") or t.get("test_symbol") or f_p
|
|
1419
|
+
add_impact_item(
|
|
1420
|
+
source=str(t_sym),
|
|
1421
|
+
target=clean_target,
|
|
1422
|
+
relationship=str(t.get("relationship", "TESTS")),
|
|
1423
|
+
category="TEST",
|
|
1424
|
+
evidence_class=str(t.get("evidence_class", "AST_VERIFIED")),
|
|
1425
|
+
reason=str(t.get("reason", "test_exercises_target")),
|
|
1426
|
+
impact_reason="direct_test" if "SYMBOL" in str(t.get("relationship", "")) else "route_test",
|
|
1427
|
+
file_path=str(f_p) if f_p else None,
|
|
1428
|
+
start_line=int(str(t["line"])) if t.get("line") is not None else None,
|
|
1429
|
+
rank=1,
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
# Sort impact items deterministically by (rank, category, file, line, source)
|
|
1433
|
+
impact_items.sort(
|
|
1434
|
+
key=lambda x: (
|
|
1435
|
+
int(str(x.get("rank") or 99)),
|
|
1436
|
+
str(x.get("category") or ""),
|
|
1437
|
+
str(x.get("file") or ""),
|
|
1438
|
+
int(str(x.get("start_line") or 0)),
|
|
1439
|
+
str(x.get("source") or ""),
|
|
1440
|
+
)
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
# Group into categories dictionary
|
|
1444
|
+
categories: dict[str, list[dict[str, object]]] = {
|
|
1445
|
+
"DIRECT": [],
|
|
1446
|
+
"FRAMEWORK": [],
|
|
1447
|
+
"SEMANTIC": [],
|
|
1448
|
+
"DI": [],
|
|
1449
|
+
"TEST": [],
|
|
1450
|
+
}
|
|
1451
|
+
for item in impact_items:
|
|
1452
|
+
cat = str(item.get("category", "DIRECT"))
|
|
1453
|
+
if cat in categories:
|
|
1454
|
+
categories[cat].append(item)
|
|
1455
|
+
|
|
1456
|
+
return {
|
|
1457
|
+
"subject": clean_target,
|
|
1458
|
+
"direct_callers": direct_callers[:max_results],
|
|
1459
|
+
"transitive_callers": transitive_callers[:max_results],
|
|
1460
|
+
"dependencies": dependencies,
|
|
1461
|
+
"dependent_modules": dependent_modules,
|
|
1462
|
+
"related_apis": related_apis,
|
|
1463
|
+
"related_tests": related_tests,
|
|
1464
|
+
"impact_items": impact_items,
|
|
1465
|
+
"categories": categories,
|
|
1466
|
+
"label_legend": {
|
|
1467
|
+
"verified": "Parser-confirmed relationship with concrete source evidence",
|
|
1468
|
+
"inferred": "Structurally inferred relationship",
|
|
1469
|
+
"possible": "Heuristic match — not statically verified",
|
|
1470
|
+
},
|
|
1471
|
+
"note": "Static analysis cannot confirm runtime behavior. Do not assert failure solely from static data.",
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
# ---------------------------------------------------------------------------
|
|
1476
|
+
# Backward-Compatible Public Trace / Import / Definition APIs
|
|
1477
|
+
# ---------------------------------------------------------------------------
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
def trace_call(
|
|
1481
|
+
con: sqlite3.Connection,
|
|
1482
|
+
symbol: str,
|
|
1483
|
+
max_depth: int = 2,
|
|
1484
|
+
callers: bool = True,
|
|
1485
|
+
callees: bool = False,
|
|
1486
|
+
both: bool = False,
|
|
1487
|
+
) -> list[dict[str, object]]:
|
|
1488
|
+
"""Return definition, callers, and/or callees with explicit confidence and relationship labels.
|
|
1489
|
+
|
|
1490
|
+
Hard bound: max_depth is clamped to max 5.
|
|
1491
|
+
Explicit labels: CALLER, CALLEE, DEFINES, IMPORTS, HANDLED_BY, plus Roblox network/service/persistence edges.
|
|
1492
|
+
"""
|
|
1493
|
+
effective_depth = max(1, min(max_depth, 5))
|
|
1494
|
+
if both:
|
|
1495
|
+
do_callers = True
|
|
1496
|
+
do_callees = True
|
|
1497
|
+
elif callees and not callers:
|
|
1498
|
+
do_callers = False
|
|
1499
|
+
do_callees = True
|
|
1500
|
+
elif callers and not callees:
|
|
1501
|
+
do_callers = True
|
|
1502
|
+
do_callees = False
|
|
1503
|
+
elif not callers and not callees:
|
|
1504
|
+
do_callers = True
|
|
1505
|
+
do_callees = True
|
|
1506
|
+
else:
|
|
1507
|
+
do_callers = callers
|
|
1508
|
+
do_callees = callees
|
|
1509
|
+
|
|
1510
|
+
definitions = con.execute(
|
|
1511
|
+
"SELECT canonical_id, qualified_name, path, start_line, end_line FROM symbols "
|
|
1512
|
+
"WHERE canonical_id=? OR qualified_name=? OR name=?",
|
|
1513
|
+
(symbol, symbol, symbol),
|
|
1514
|
+
).fetchall()
|
|
1515
|
+
|
|
1516
|
+
output: list[dict[str, object]] = []
|
|
1517
|
+
seen: set[tuple[str, str, int, str]] = set()
|
|
1518
|
+
|
|
1519
|
+
# 1. Definitions
|
|
1520
|
+
for d in definitions:
|
|
1521
|
+
key = (d["qualified_name"], d["path"], d["start_line"], "DEFINES")
|
|
1522
|
+
if key not in seen:
|
|
1523
|
+
seen.add(key)
|
|
1524
|
+
output.append(
|
|
1525
|
+
{
|
|
1526
|
+
"symbol": d["qualified_name"],
|
|
1527
|
+
"canonical_id": d["canonical_id"],
|
|
1528
|
+
"file": d["path"],
|
|
1529
|
+
"start_line": d["start_line"],
|
|
1530
|
+
"end_line": d["end_line"],
|
|
1531
|
+
"relationship": "DEFINES",
|
|
1532
|
+
"confidence": "HIGH",
|
|
1533
|
+
"evidence": "Parser-extracted symbol definition",
|
|
1534
|
+
}
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1537
|
+
# 2. Handled-by routes
|
|
1538
|
+
try:
|
|
1539
|
+
route_rows = con.execute(
|
|
1540
|
+
"SELECT endpoint_id, file_path, line FROM framework_routes "
|
|
1541
|
+
"WHERE handler_name=? OR handler_canonical_id=?",
|
|
1542
|
+
(symbol, symbol),
|
|
1543
|
+
).fetchall()
|
|
1544
|
+
for rr in route_rows:
|
|
1545
|
+
key = (rr["endpoint_id"], rr["file_path"], rr["line"], "HANDLED_BY")
|
|
1546
|
+
if key not in seen:
|
|
1547
|
+
seen.add(key)
|
|
1548
|
+
output.append(
|
|
1549
|
+
{
|
|
1550
|
+
"symbol": rr["endpoint_id"],
|
|
1551
|
+
"file": rr["file_path"],
|
|
1552
|
+
"start_line": rr["line"],
|
|
1553
|
+
"end_line": rr["line"],
|
|
1554
|
+
"relationship": "HANDLED_BY",
|
|
1555
|
+
"confidence": "HIGH",
|
|
1556
|
+
"evidence": f"Route definition for {rr['endpoint_id']}",
|
|
1557
|
+
}
|
|
1558
|
+
)
|
|
1559
|
+
except sqlite3.OperationalError:
|
|
1560
|
+
pass
|
|
1561
|
+
|
|
1562
|
+
# 3. Callers traversal
|
|
1563
|
+
if do_callers:
|
|
1564
|
+
current_symbols = [symbol]
|
|
1565
|
+
visited_callers: set[str] = set()
|
|
1566
|
+
for depth_step in range(effective_depth):
|
|
1567
|
+
next_symbols: list[str] = []
|
|
1568
|
+
for sym in current_symbols:
|
|
1569
|
+
if sym in visited_callers:
|
|
1570
|
+
continue
|
|
1571
|
+
visited_callers.add(sym)
|
|
1572
|
+
caller_items = find_callers(con, sym, max_results=20)
|
|
1573
|
+
for c in caller_items:
|
|
1574
|
+
c_sym = str(c.get("symbol") or "")
|
|
1575
|
+
c_file = str(c.get("file") or "")
|
|
1576
|
+
c_line = int(c.get("start_line") or 1)
|
|
1577
|
+
key = (c_sym, c_file, c_line, "CALLER")
|
|
1578
|
+
if key not in seen:
|
|
1579
|
+
seen.add(key)
|
|
1580
|
+
output.append(
|
|
1581
|
+
{
|
|
1582
|
+
"symbol": c_sym,
|
|
1583
|
+
"file": c_file,
|
|
1584
|
+
"start_line": c_line,
|
|
1585
|
+
"end_line": int(c.get("end_line") or c_line),
|
|
1586
|
+
"relationship": "CALLER",
|
|
1587
|
+
"confidence": str(c.get("confidence", "LOW")),
|
|
1588
|
+
"evidence": str(c.get("evidence", f"Call reference to '{sym}'")),
|
|
1589
|
+
"depth": depth_step + 1,
|
|
1590
|
+
}
|
|
1591
|
+
)
|
|
1592
|
+
if c_sym:
|
|
1593
|
+
next_symbols.append(c_sym)
|
|
1594
|
+
|
|
1595
|
+
# Also traverse incoming Roblox edges (CLIENT_DISPATCHES_REMOTE, SERVER_HANDLES_REMOTE, GETS_SERVICE, REQUIRES_MODULE)
|
|
1596
|
+
r_in_rows = con.execute(
|
|
1597
|
+
"SELECT source, target, relationship, confidence, file, start_line, end_line, evidence, evidence_class "
|
|
1598
|
+
"FROM graph_edges WHERE (target=? OR target LIKE ?) AND relationship IN ("
|
|
1599
|
+
"'CLIENT_DISPATCHES_REMOTE', 'SERVER_HANDLES_REMOTE', 'GETS_SERVICE', 'PROVIDES_SERVICE', 'REQUIRES_MODULE'"
|
|
1600
|
+
")",
|
|
1601
|
+
(sym, f"%.{sym.split('.')[-1]}"),
|
|
1602
|
+
).fetchall()
|
|
1603
|
+
for r_in in r_in_rows:
|
|
1604
|
+
src_s = str(r_in["source"])
|
|
1605
|
+
rel_s = str(r_in["relationship"])
|
|
1606
|
+
r_file = str(r_in["file"] or "")
|
|
1607
|
+
r_line = int(r_in["start_line"] or 1)
|
|
1608
|
+
key = (src_s, r_file, r_line, rel_s)
|
|
1609
|
+
if key not in seen:
|
|
1610
|
+
seen.add(key)
|
|
1611
|
+
output.append(
|
|
1612
|
+
{
|
|
1613
|
+
"symbol": src_s,
|
|
1614
|
+
"target": str(r_in["target"]),
|
|
1615
|
+
"file": r_file,
|
|
1616
|
+
"start_line": r_line,
|
|
1617
|
+
"end_line": int(r_in["end_line"] or r_line),
|
|
1618
|
+
"relationship": rel_s,
|
|
1619
|
+
"evidence_class": str(r_in["evidence_class"] or "ROJO_VERIFIED"),
|
|
1620
|
+
"confidence": str(r_in["confidence"] or "HIGH"),
|
|
1621
|
+
"evidence": str(r_in["evidence"] or ""),
|
|
1622
|
+
"depth": depth_step + 1,
|
|
1623
|
+
}
|
|
1624
|
+
)
|
|
1625
|
+
if src_s:
|
|
1626
|
+
next_symbols.append(src_s)
|
|
1627
|
+
current_symbols = next_symbols
|
|
1628
|
+
if not current_symbols:
|
|
1629
|
+
break
|
|
1630
|
+
|
|
1631
|
+
# 4. Callees traversal
|
|
1632
|
+
if do_callees:
|
|
1633
|
+
current_symbols = [symbol]
|
|
1634
|
+
for d in definitions:
|
|
1635
|
+
if d["canonical_id"] not in current_symbols:
|
|
1636
|
+
current_symbols.append(str(d["canonical_id"]))
|
|
1637
|
+
child_methods = con.execute(
|
|
1638
|
+
"SELECT canonical_id FROM symbols WHERE parent_symbol_id=?",
|
|
1639
|
+
(d["canonical_id"],),
|
|
1640
|
+
).fetchall()
|
|
1641
|
+
for cm in child_methods:
|
|
1642
|
+
if str(cm["canonical_id"]) not in current_symbols:
|
|
1643
|
+
current_symbols.append(str(cm["canonical_id"]))
|
|
1644
|
+
visited_callees: set[str] = set()
|
|
1645
|
+
for depth_step in range(effective_depth):
|
|
1646
|
+
next_symbols = []
|
|
1647
|
+
for sym in current_symbols:
|
|
1648
|
+
if sym in visited_callees:
|
|
1649
|
+
continue
|
|
1650
|
+
visited_callees.add(sym)
|
|
1651
|
+
callee_items = find_callees(con, sym, max_results=20)
|
|
1652
|
+
for c in callee_items:
|
|
1653
|
+
c_sym = str(c.get("qualified_callee") or c.get("callee") or "")
|
|
1654
|
+
c_file = str(c.get("file") or "")
|
|
1655
|
+
c_line = int(c.get("start_line") or c.get("line") or 1)
|
|
1656
|
+
key = (c_sym, c_file, c_line, "CALLEE")
|
|
1657
|
+
if key not in seen:
|
|
1658
|
+
seen.add(key)
|
|
1659
|
+
output.append(
|
|
1660
|
+
{
|
|
1661
|
+
"symbol": c_sym,
|
|
1662
|
+
"file": c_file,
|
|
1663
|
+
"start_line": c_line,
|
|
1664
|
+
"end_line": int(c.get("end_line") or c_line),
|
|
1665
|
+
"relationship": "CALLEE",
|
|
1666
|
+
"confidence": str(c.get("confidence", "LOW")),
|
|
1667
|
+
"evidence": str(c.get("evidence", f"Direct callee of '{sym}'")),
|
|
1668
|
+
"depth": depth_step + 1,
|
|
1669
|
+
}
|
|
1670
|
+
)
|
|
1671
|
+
if c_sym:
|
|
1672
|
+
next_symbols.append(c_sym)
|
|
1673
|
+
|
|
1674
|
+
# Also traverse outgoing Roblox edges (CLIENT_DISPATCHES_REMOTE, SERVER_HANDLES_REMOTE, GETS_SERVICE, REQUIRES_MODULE, PERSISTENCE)
|
|
1675
|
+
r_out_rows = con.execute(
|
|
1676
|
+
"SELECT source, target, relationship, confidence, file, start_line, end_line, evidence, evidence_class "
|
|
1677
|
+
"FROM graph_edges WHERE (source=? OR source LIKE ?) AND relationship IN ("
|
|
1678
|
+
"'CLIENT_DISPATCHES_REMOTE', 'SERVER_HANDLES_REMOTE', 'GETS_SERVICE', 'PROVIDES_SERVICE', "
|
|
1679
|
+
"'REQUIRES_MODULE', 'CONFIGURES_PERSISTENCE', 'READS_PERSISTENCE', 'WRITES_PERSISTENCE'"
|
|
1680
|
+
")",
|
|
1681
|
+
(sym, f"%.{sym.split('.')[-1]}"),
|
|
1682
|
+
).fetchall()
|
|
1683
|
+
for r_out in r_out_rows:
|
|
1684
|
+
tgt_s = str(r_out["target"])
|
|
1685
|
+
rel_s = str(r_out["relationship"])
|
|
1686
|
+
r_file = str(r_out["file"] or "")
|
|
1687
|
+
r_line = int(r_out["start_line"] or 1)
|
|
1688
|
+
key = (tgt_s, r_file, r_line, rel_s)
|
|
1689
|
+
if key not in seen:
|
|
1690
|
+
seen.add(key)
|
|
1691
|
+
output.append(
|
|
1692
|
+
{
|
|
1693
|
+
"symbol": tgt_s,
|
|
1694
|
+
"source": str(r_out["source"]),
|
|
1695
|
+
"file": r_file,
|
|
1696
|
+
"start_line": r_line,
|
|
1697
|
+
"end_line": int(r_out["end_line"] or r_line),
|
|
1698
|
+
"relationship": rel_s,
|
|
1699
|
+
"evidence_class": str(r_out["evidence_class"] or "ROJO_VERIFIED"),
|
|
1700
|
+
"confidence": str(r_out["confidence"] or "HIGH"),
|
|
1701
|
+
"evidence": str(r_out["evidence"] or ""),
|
|
1702
|
+
"depth": depth_step + 1,
|
|
1703
|
+
}
|
|
1704
|
+
)
|
|
1705
|
+
if tgt_s:
|
|
1706
|
+
next_symbols.append(tgt_s)
|
|
1707
|
+
current_symbols = next_symbols
|
|
1708
|
+
if not current_symbols:
|
|
1709
|
+
break
|
|
1710
|
+
|
|
1711
|
+
return output
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
def find_parallel_implementations(
|
|
1715
|
+
con: sqlite3.Connection,
|
|
1716
|
+
symbol: str,
|
|
1717
|
+
max_results: int = 5,
|
|
1718
|
+
) -> list[dict[str, Any]]:
|
|
1719
|
+
"""Discover parallel implementations sharing algorithmic or domain roles."""
|
|
1720
|
+
clean = symbol.split(".")[-1]
|
|
1721
|
+
row = con.execute(
|
|
1722
|
+
"SELECT canonical_id, qualified_name, name, path, kind, parameter_count, return_type, module "
|
|
1723
|
+
"FROM symbols WHERE canonical_id=? OR qualified_name=? OR name=? LIMIT 1",
|
|
1724
|
+
(symbol, symbol, clean),
|
|
1725
|
+
).fetchone()
|
|
1726
|
+
if not row:
|
|
1727
|
+
return []
|
|
1728
|
+
|
|
1729
|
+
source_path = row["path"]
|
|
1730
|
+
source_param_count = row["parameter_count"]
|
|
1731
|
+
source_kind = row["kind"]
|
|
1732
|
+
source_name = row["name"]
|
|
1733
|
+
source_canon = row["canonical_id"]
|
|
1734
|
+
|
|
1735
|
+
results: list[dict[str, Any]] = []
|
|
1736
|
+
seen: set[str] = {source_canon, source_name}
|
|
1737
|
+
|
|
1738
|
+
# Strategy A: Check shared callers
|
|
1739
|
+
shared_callee_rows = con.execute(
|
|
1740
|
+
"SELECT DISTINCT c2.callee, c2.qualified_callee, c2.source_path, s.canonical_id, s.path, s.start_line, s.end_line "
|
|
1741
|
+
"FROM calls c1 "
|
|
1742
|
+
"JOIN calls c2 ON c1.source_path = c2.source_path "
|
|
1743
|
+
"JOIN symbols s ON (s.canonical_id = c2.qualified_callee OR s.name = c2.callee) "
|
|
1744
|
+
"WHERE (c1.callee=? OR c1.qualified_callee=?) AND c2.callee != ? "
|
|
1745
|
+
"AND s.kind=? "
|
|
1746
|
+
"LIMIT ?",
|
|
1747
|
+
(source_name, source_canon, source_name, source_kind, max_results),
|
|
1748
|
+
).fetchall()
|
|
1749
|
+
|
|
1750
|
+
for sc in shared_callee_rows:
|
|
1751
|
+
canon = sc["canonical_id"]
|
|
1752
|
+
if canon in seen:
|
|
1753
|
+
continue
|
|
1754
|
+
seen.add(canon)
|
|
1755
|
+
results.append(
|
|
1756
|
+
{
|
|
1757
|
+
"symbol": sc["qualified_callee"] or sc["callee"],
|
|
1758
|
+
"canonical_id": canon,
|
|
1759
|
+
"file": sc["path"],
|
|
1760
|
+
"start_line": sc["start_line"],
|
|
1761
|
+
"end_line": sc["end_line"],
|
|
1762
|
+
"relationship": "PARALLEL_IMPLEMENTATION",
|
|
1763
|
+
"confidence": "MEDIUM",
|
|
1764
|
+
"evidence": f"Parallel implementation: shared caller context with '{source_name}'",
|
|
1765
|
+
}
|
|
1766
|
+
)
|
|
1767
|
+
|
|
1768
|
+
# Strategy B: Sibling functions in same file/module with matching parameter count
|
|
1769
|
+
if len(results) < max_results and source_param_count is not None and source_param_count > 0:
|
|
1770
|
+
sibling_rows = con.execute(
|
|
1771
|
+
"SELECT canonical_id, qualified_name, name, path, start_line, end_line "
|
|
1772
|
+
"FROM symbols WHERE path=? AND kind=? AND parameter_count=? AND canonical_id != ? "
|
|
1773
|
+
"LIMIT ?",
|
|
1774
|
+
(source_path, source_kind, source_param_count, source_canon, max_results - len(results)),
|
|
1775
|
+
).fetchall()
|
|
1776
|
+
for sib in sibling_rows:
|
|
1777
|
+
canon = sib["canonical_id"]
|
|
1778
|
+
if canon in seen:
|
|
1779
|
+
continue
|
|
1780
|
+
seen.add(canon)
|
|
1781
|
+
results.append(
|
|
1782
|
+
{
|
|
1783
|
+
"symbol": sib["qualified_name"],
|
|
1784
|
+
"canonical_id": canon,
|
|
1785
|
+
"file": sib["path"],
|
|
1786
|
+
"start_line": sib["start_line"],
|
|
1787
|
+
"end_line": sib["end_line"],
|
|
1788
|
+
"relationship": "PARALLEL_IMPLEMENTATION",
|
|
1789
|
+
"confidence": "MEDIUM",
|
|
1790
|
+
"evidence": f"Parallel implementation: sibling {source_kind} with matching signature in {source_path}",
|
|
1791
|
+
}
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
return results[:max_results]
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
def get_graph_summary(con: sqlite3.Connection) -> dict[str, object]:
|
|
1798
|
+
"""Return bounded repository graph summary statistics."""
|
|
1799
|
+
node_counts_by_type: dict[str, int] = {}
|
|
1800
|
+
for r in con.execute("SELECT kind, count(*) FROM symbols GROUP BY kind").fetchall():
|
|
1801
|
+
node_counts_by_type[r[0]] = r[1]
|
|
1802
|
+
|
|
1803
|
+
edge_counts_by_type: dict[str, int] = {
|
|
1804
|
+
"IMPORTS": con.execute("SELECT count(*) FROM imports").fetchone()[0],
|
|
1805
|
+
"CALLS": con.execute("SELECT count(*) FROM calls").fetchone()[0],
|
|
1806
|
+
"EXTENDS": con.execute("SELECT count(*) FROM inheritance").fetchone()[0],
|
|
1807
|
+
}
|
|
1808
|
+
try:
|
|
1809
|
+
edge_counts_by_type["HANDLED_BY"] = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
|
|
1810
|
+
except sqlite3.OperationalError:
|
|
1811
|
+
pass
|
|
1812
|
+
|
|
1813
|
+
modules = [
|
|
1814
|
+
r[0]
|
|
1815
|
+
for r in con.execute(
|
|
1816
|
+
"SELECT DISTINCT module FROM symbols WHERE module != '' ORDER BY module LIMIT 50"
|
|
1817
|
+
).fetchall()
|
|
1818
|
+
]
|
|
1819
|
+
|
|
1820
|
+
dependencies = [
|
|
1821
|
+
{"module": r[0], "count": r[1]}
|
|
1822
|
+
for r in con.execute(
|
|
1823
|
+
"SELECT module, count(*) AS c FROM imports GROUP BY module ORDER BY c DESC LIMIT 20"
|
|
1824
|
+
).fetchall()
|
|
1825
|
+
]
|
|
1826
|
+
|
|
1827
|
+
route_count = 0
|
|
1828
|
+
try:
|
|
1829
|
+
route_count = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
|
|
1830
|
+
except sqlite3.OperationalError:
|
|
1831
|
+
pass
|
|
1832
|
+
|
|
1833
|
+
test_count = 0
|
|
1834
|
+
try:
|
|
1835
|
+
test_count = con.execute(
|
|
1836
|
+
"SELECT count(*) FROM files WHERE category='TEST' OR path LIKE 'tests/%' OR path LIKE 'test/%'"
|
|
1837
|
+
).fetchone()[0]
|
|
1838
|
+
except sqlite3.OperationalError:
|
|
1839
|
+
pass
|
|
1840
|
+
|
|
1841
|
+
return {
|
|
1842
|
+
"node_counts": node_counts_by_type,
|
|
1843
|
+
"edge_counts": edge_counts_by_type,
|
|
1844
|
+
"modules": modules,
|
|
1845
|
+
"top_dependencies": dependencies,
|
|
1846
|
+
"route_count": route_count,
|
|
1847
|
+
"test_count": test_count,
|
|
1848
|
+
"external_service_count": len([d for d in dependencies if "." not in d["module"] and "/" not in d["module"]]),
|
|
1849
|
+
"total_symbols": sum(node_counts_by_type.values()),
|
|
1850
|
+
"total_edges": sum(edge_counts_by_type.values()),
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
def get_focused_graph(
|
|
1855
|
+
con: sqlite3.Connection,
|
|
1856
|
+
module: str | None = None,
|
|
1857
|
+
symbol: str | None = None,
|
|
1858
|
+
depth: int = 2,
|
|
1859
|
+
) -> dict[str, object]:
|
|
1860
|
+
"""Return bounded focused graph for a module or symbol."""
|
|
1861
|
+
bounded_depth = max(1, min(depth, 5))
|
|
1862
|
+
nodes: list[dict[str, object]] = []
|
|
1863
|
+
edges: list[dict[str, object]] = []
|
|
1864
|
+
seen_nodes: set[str] = set()
|
|
1865
|
+
seen_edges: set[tuple[str, str, str]] = set()
|
|
1866
|
+
|
|
1867
|
+
if symbol:
|
|
1868
|
+
tr = trace_call(con, symbol, max_depth=bounded_depth, both=True)
|
|
1869
|
+
for item in tr[:100]:
|
|
1870
|
+
sym_name = str(item.get("symbol") or "")
|
|
1871
|
+
if sym_name and sym_name not in seen_nodes:
|
|
1872
|
+
seen_nodes.add(sym_name)
|
|
1873
|
+
nodes.append({
|
|
1874
|
+
"id": sym_name,
|
|
1875
|
+
"file": item.get("file"),
|
|
1876
|
+
"line": item.get("start_line"),
|
|
1877
|
+
"relationship": item.get("relationship"),
|
|
1878
|
+
})
|
|
1879
|
+
rel = str(item.get("relationship") or "")
|
|
1880
|
+
if rel in ("CALLER", "CALLEE", "HANDLED_BY"):
|
|
1881
|
+
src = sym_name if rel == "CALLEE" else symbol
|
|
1882
|
+
tgt = symbol if rel == "CALLEE" else sym_name
|
|
1883
|
+
edge_key = (src, tgt, rel)
|
|
1884
|
+
if edge_key not in seen_edges:
|
|
1885
|
+
seen_edges.add(edge_key)
|
|
1886
|
+
edges.append({
|
|
1887
|
+
"source": src,
|
|
1888
|
+
"target": tgt,
|
|
1889
|
+
"relationship": rel,
|
|
1890
|
+
"confidence": item.get("confidence", "HIGH"),
|
|
1891
|
+
"evidence": item.get("evidence", ""),
|
|
1892
|
+
})
|
|
1893
|
+
elif module:
|
|
1894
|
+
rows = con.execute(
|
|
1895
|
+
"SELECT canonical_id, qualified_name, kind, path, start_line, end_line "
|
|
1896
|
+
"FROM symbols WHERE module=? OR path LIKE ? LIMIT 50",
|
|
1897
|
+
(module, f"%{module}%"),
|
|
1898
|
+
).fetchall()
|
|
1899
|
+
for r in rows:
|
|
1900
|
+
cid = r["canonical_id"]
|
|
1901
|
+
if cid not in seen_nodes:
|
|
1902
|
+
seen_nodes.add(cid)
|
|
1903
|
+
nodes.append({
|
|
1904
|
+
"id": cid,
|
|
1905
|
+
"name": r["qualified_name"],
|
|
1906
|
+
"kind": r["kind"],
|
|
1907
|
+
"file": r["path"],
|
|
1908
|
+
"line": r["start_line"],
|
|
1909
|
+
})
|
|
1910
|
+
for n in nodes[:30]:
|
|
1911
|
+
sym = str(n.get("name") or "")
|
|
1912
|
+
callees = find_callees(con, sym, max_results=10)
|
|
1913
|
+
for c in callees:
|
|
1914
|
+
c_sym = str(c.get("qualified_callee") or c.get("callee") or "")
|
|
1915
|
+
if c_sym in seen_nodes:
|
|
1916
|
+
edge_key = (sym, c_sym, "CALLS")
|
|
1917
|
+
if edge_key not in seen_edges:
|
|
1918
|
+
seen_edges.add(edge_key)
|
|
1919
|
+
edges.append({
|
|
1920
|
+
"source": sym,
|
|
1921
|
+
"target": c_sym,
|
|
1922
|
+
"relationship": "CALLS",
|
|
1923
|
+
"confidence": str(c.get("confidence", "HIGH")),
|
|
1924
|
+
"evidence": str(c.get("evidence", "")),
|
|
1925
|
+
})
|
|
1926
|
+
|
|
1927
|
+
return {
|
|
1928
|
+
"module": module,
|
|
1929
|
+
"symbol": symbol,
|
|
1930
|
+
"depth": bounded_depth,
|
|
1931
|
+
"nodes": sorted(nodes, key=lambda n: str(n.get("id"))),
|
|
1932
|
+
"edges": sorted(edges, key=lambda e: (str(e.get("source")), str(e.get("target")))),
|
|
1933
|
+
"node_count": len(nodes),
|
|
1934
|
+
"edge_count": len(edges),
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
|
|
1938
|
+
def import_edges(con: sqlite3.Connection) -> list[GraphEdge]:
|
|
1939
|
+
"""Return parser-extracted import relationships with evidence metadata."""
|
|
1940
|
+
rows = con.execute(
|
|
1941
|
+
"SELECT source_path, module, resolved_path, line FROM imports ORDER BY source_path, line"
|
|
1942
|
+
).fetchall()
|
|
1943
|
+
return [
|
|
1944
|
+
GraphEdge(
|
|
1945
|
+
source=r["source_path"],
|
|
1946
|
+
target=r["resolved_path"] or r["module"],
|
|
1947
|
+
relationship="IMPORTS",
|
|
1948
|
+
confidence="HIGH",
|
|
1949
|
+
file=r["source_path"],
|
|
1950
|
+
start_line=r["line"],
|
|
1951
|
+
end_line=r["line"],
|
|
1952
|
+
evidence=f"Parser-extracted import '{r['module']}'",
|
|
1953
|
+
)
|
|
1954
|
+
for r in rows
|
|
1955
|
+
]
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
def definition_edges(con: sqlite3.Connection, limit: int = 500) -> list[GraphEdge]:
|
|
1959
|
+
"""Return parser-confirmed definition and containment edges."""
|
|
1960
|
+
rows = con.execute(
|
|
1961
|
+
"SELECT canonical_id, qualified_name, kind, path, start_line, end_line, parent_symbol_id "
|
|
1962
|
+
"FROM symbols ORDER BY path, start_line LIMIT ?",
|
|
1963
|
+
(limit,),
|
|
1964
|
+
).fetchall()
|
|
1965
|
+
edges: list[GraphEdge] = []
|
|
1966
|
+
for r in rows:
|
|
1967
|
+
if r["parent_symbol_id"]:
|
|
1968
|
+
edges.append(
|
|
1969
|
+
GraphEdge(
|
|
1970
|
+
source=r["parent_symbol_id"],
|
|
1971
|
+
target=r["canonical_id"],
|
|
1972
|
+
relationship="CONTAINS",
|
|
1973
|
+
confidence="HIGH",
|
|
1974
|
+
file=r["path"],
|
|
1975
|
+
start_line=r["start_line"],
|
|
1976
|
+
end_line=r["end_line"],
|
|
1977
|
+
evidence=f"Containment {r['parent_symbol_id']} -> {r['canonical_id']}",
|
|
1978
|
+
)
|
|
1979
|
+
)
|
|
1980
|
+
else:
|
|
1981
|
+
edges.append(
|
|
1982
|
+
GraphEdge(
|
|
1983
|
+
source=r["path"],
|
|
1984
|
+
target=r["canonical_id"],
|
|
1985
|
+
relationship="DEFINES",
|
|
1986
|
+
confidence="HIGH",
|
|
1987
|
+
file=r["path"],
|
|
1988
|
+
start_line=r["start_line"],
|
|
1989
|
+
end_line=r["end_line"],
|
|
1990
|
+
evidence=f"File {r['path']} defines {r['canonical_id']}",
|
|
1991
|
+
)
|
|
1992
|
+
)
|
|
1993
|
+
return edges
|