codegraph-engine 3.0.0__1-py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +859 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. 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