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,1476 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import functools
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import anyio
10
+
11
+ from codegraph.architecture import get_architecture as arch_get_architecture
12
+ from codegraph.config import Settings
13
+ from codegraph.context import _resolve_task_or_query_input
14
+ from codegraph.context import get_context as compile_context
15
+ from codegraph.database import (
16
+ find_db_callers as db_find_db_callers,
17
+ )
18
+ from codegraph.database import (
19
+ find_db_columns as db_find_db_columns,
20
+ )
21
+ from codegraph.database import (
22
+ find_db_models as db_find_db_models,
23
+ )
24
+ from codegraph.database import (
25
+ find_db_queries as db_find_db_queries,
26
+ )
27
+ from codegraph.database import (
28
+ find_db_readers as db_find_db_readers,
29
+ )
30
+ from codegraph.database import (
31
+ find_db_relationships as db_find_db_relationships,
32
+ )
33
+ from codegraph.database import (
34
+ find_db_tables as db_find_db_tables,
35
+ )
36
+ from codegraph.database import (
37
+ find_db_writers as db_find_db_writers,
38
+ )
39
+ from codegraph.database import (
40
+ get_db_impact as db_get_db_impact,
41
+ )
42
+ from codegraph.database import (
43
+ get_db_schema as db_get_db_schema,
44
+ )
45
+ from codegraph.database import (
46
+ get_db_table as db_get_db_table,
47
+ )
48
+ from codegraph.errors import SecurityError
49
+ from codegraph.evidence import Evidence
50
+ from codegraph.evidence import verify_evidence as verify_evidence_fn
51
+ from codegraph.freshness import check_freshness
52
+ from codegraph.git import (
53
+ analyze_change_impact as git_analyze_change_impact,
54
+ )
55
+ from codegraph.git import (
56
+ changed_files,
57
+ )
58
+ from codegraph.git import (
59
+ get_file_history as git_get_file_history,
60
+ )
61
+ from codegraph.graph import (
62
+ analyze_impact as graph_analyze_impact,
63
+ )
64
+ from codegraph.graph import (
65
+ definition_edges,
66
+ import_edges,
67
+ )
68
+ from codegraph.graph import (
69
+ find_callees as graph_find_callees,
70
+ )
71
+ from codegraph.graph import (
72
+ find_callers as graph_find_callers,
73
+ )
74
+ from codegraph.graph import (
75
+ find_related_tests as graph_find_related_tests,
76
+ )
77
+ from codegraph.graph import (
78
+ get_call_graph as graph_get_call_graph,
79
+ )
80
+ from codegraph.graph import (
81
+ get_symbol as graph_get_symbol,
82
+ )
83
+ from codegraph.graph import (
84
+ trace_call as graph_trace_call,
85
+ )
86
+ from codegraph.indexing import Indexer
87
+ from codegraph.indexing.indexer import check_database_health
88
+ from codegraph.interrogation import (
89
+ get_architecture as interrogation_get_architecture,
90
+ )
91
+ from codegraph.interrogation import (
92
+ get_callees as interrogation_get_callees,
93
+ )
94
+ from codegraph.interrogation import (
95
+ get_callers as interrogation_get_callers,
96
+ )
97
+ from codegraph.interrogation import (
98
+ get_dependents as interrogation_get_dependents,
99
+ )
100
+ from codegraph.interrogation import (
101
+ get_file as interrogation_get_file,
102
+ )
103
+ from codegraph.interrogation import (
104
+ get_git_impact as interrogation_get_git_impact,
105
+ )
106
+ from codegraph.interrogation import (
107
+ get_imports as interrogation_get_imports,
108
+ )
109
+ from codegraph.interrogation import (
110
+ get_references as interrogation_get_references,
111
+ )
112
+ from codegraph.interrogation import (
113
+ get_symbol as interrogation_get_symbol,
114
+ )
115
+ from codegraph.interrogation import (
116
+ list_routes as interrogation_list_routes,
117
+ )
118
+ from codegraph.interrogation import (
119
+ resolve_symbol as interrogation_resolve_symbol,
120
+ )
121
+ from codegraph.interrogation import (
122
+ search_symbols as interrogation_search_symbols,
123
+ )
124
+ from codegraph.interrogation import (
125
+ trace_path as interrogation_trace_path,
126
+ )
127
+ from codegraph.memory import MemoryStore
128
+ from codegraph.planner import build_retrieval_plan
129
+ from codegraph.resources import TaskPriority
130
+ from codegraph.runtime import (
131
+ get_runtime_trace as runtime_get_runtime_trace,
132
+ )
133
+ from codegraph.runtime import (
134
+ ingest_runtime_traces as runtime_ingest_runtime_traces,
135
+ )
136
+ from codegraph.runtime import (
137
+ reconcile_static_runtime as runtime_reconcile_static_runtime,
138
+ )
139
+ from codegraph.search import search_code as search_code_repo
140
+ from codegraph.security import is_sensitive, safe_path, safe_read_text
141
+ from codegraph.task import normalize_task_spec
142
+
143
+
144
+ def _resolve_symbol_arg(
145
+ symbol: str = "",
146
+ canonical_id: str = "",
147
+ name: str = "",
148
+ tool_name: str = "tool",
149
+ ) -> str:
150
+ """Resolve canonical `symbol` parameter with backward-compatible `canonical_id`/`name` aliases."""
151
+ provided: list[tuple[str, str]] = []
152
+ if symbol and symbol.strip():
153
+ provided.append(("symbol", symbol.strip()))
154
+ if canonical_id and canonical_id.strip():
155
+ provided.append(("canonical_id", canonical_id.strip()))
156
+ if name and name.strip():
157
+ provided.append(("name", name.strip()))
158
+
159
+ if not provided:
160
+ raise ValueError(
161
+ f"{tool_name} requires a non-empty 'symbol' (or 'canonical_id' alias) argument."
162
+ )
163
+
164
+ distinct_values = {val for _, val in provided}
165
+ if len(distinct_values) > 1:
166
+ details = ", ".join(f"{k}={v!r}" for k, v in provided)
167
+ raise ValueError(
168
+ f"Conflicting symbol arguments passed to {tool_name} ({details}); "
169
+ "provide 'symbol' or identical alias values."
170
+ )
171
+
172
+ return provided[0][1]
173
+
174
+
175
+ def create_server(
176
+ repository: Path,
177
+ settings: Settings | None = None,
178
+ profile: str = "full",
179
+ host: str = "127.0.0.1",
180
+ port: int = 8765,
181
+ ) -> Any:
182
+ """Build an MCP FastMCP server lazily so normal CLI use needs no MCP dependency."""
183
+ try:
184
+ from mcp.server.fastmcp import FastMCP
185
+ except ImportError as exc:
186
+ raise RuntimeError("MCP support requires: pip install 'codegraph-engine[mcp]'") from exc
187
+
188
+ from codegraph.agent_rules import render_agent_rules
189
+
190
+ indexer = Indexer(repository, settings)
191
+ memory = MemoryStore(indexer.db_path)
192
+ app = FastMCP(
193
+ "CodeGraph MCP",
194
+ instructions=render_agent_rules("antigravity"),
195
+ host=host,
196
+ port=port,
197
+ )
198
+ max_read_bytes = indexer.settings.max_read_bytes
199
+
200
+ # -----------------------------------------------------------------------
201
+ # Tool Handlers
202
+ # -----------------------------------------------------------------------
203
+
204
+ def search_code(
205
+ query: str,
206
+ top_k: int = 10,
207
+ path_filter: str | None = None,
208
+ file_types: list[str] | None = None,
209
+ include_tests: bool = True,
210
+ include_configs: bool = True,
211
+ max_results: int = 20,
212
+ ) -> list[dict[str, object]]:
213
+ """Search literal text, strings, UI labels, HTML/Jinja templates, JS/TS/CSS/Python files, config keys, SQL fragments, and error messages across the repository. USE THIS when the target is NOT a known code symbol (e.g., 'Add Manual Entry', 'scanBtn', 'GROQ_API_KEY'). DO NOT use this for callers, callees, imports, or symbol definitions—use find_callers, find_callees, get_imports, or find_symbol instead. Returns deterministic text hits (`path`, `line`, `matched_text`, `snippet`, `file_category`); never fabricates semantic graph relationships."""
214
+ effective_limit = top_k if (top_k != 10 and max_results == 20) else (
215
+ max_results if max_results != 20 else top_k
216
+ )
217
+ if not query.strip() or not 1 <= effective_limit <= 100:
218
+ raise ValueError("query must be non-empty and top_k must be 1..100")
219
+ with indexer.session() as con:
220
+ return search_code_repo(
221
+ con,
222
+ query,
223
+ repo_path=indexer.repository,
224
+ path_filter=path_filter,
225
+ file_types=file_types,
226
+ include_tests=include_tests,
227
+ include_configs=include_configs,
228
+ max_results=effective_limit,
229
+ )
230
+
231
+ def read_file(path: str, start_line: int = 1, end_line: int = 200) -> dict[str, object]:
232
+ """Read a bounded line range (1..500 lines) of a non-sensitive repository file. Use AFTER CodeGraph tools identify the exact file and line span, or directly for trivial single-file edits. Blocks path traversal, sensitive credential files, binary files, and files exceeding max_read_bytes. Does not compute cross-file callers, DI bindings, or test links."""
233
+ if start_line < 1 or end_line < start_line or end_line - start_line > 500:
234
+ raise ValueError("line range must be 1..500 lines")
235
+ _, rel_posix, text = safe_read_text(
236
+ indexer.repository,
237
+ path,
238
+ max_read_bytes=max_read_bytes,
239
+ )
240
+ lines = text.splitlines()
241
+ return {
242
+ "file": rel_posix,
243
+ "path": rel_posix,
244
+ "start_line": start_line,
245
+ "end_line": min(end_line, len(lines)),
246
+ "content": "\n".join(lines[start_line - 1 : end_line]),
247
+ "truncated": end_line < len(lines) or start_line > 1,
248
+ }
249
+
250
+ def find_symbol(
251
+ symbol: str = "",
252
+ name: str = "",
253
+ canonical_id: str = "",
254
+ ) -> list[dict[str, object]]:
255
+ """Find function, method, or class definitions by `symbol` (or `name` / `canonical_id` alias). USE THIS INSTEAD OF grep/search_code when asking 'where is symbol X defined?'. DO NOT use search_code for symbol definition lookup. Returns AST-verified symbol definitions (`symbol`, `kind`, `file`, `start_line`, `end_line`). Does not return callers or dependency relationships."""
256
+ target = _resolve_symbol_arg(
257
+ symbol=symbol, canonical_id=canonical_id, name=name, tool_name="find_symbol"
258
+ )
259
+ with indexer.session() as con:
260
+ rows = con.execute(
261
+ "SELECT qualified_name AS symbol,kind,path AS file,start_line,end_line "
262
+ "FROM symbols WHERE canonical_id=? OR qualified_name=? OR name=? ORDER BY path,start_line LIMIT 100",
263
+ (target, target, target),
264
+ )
265
+ return [dict(row) for row in rows]
266
+
267
+ def get_symbol(
268
+ symbol: str = "",
269
+ canonical_id: str = "",
270
+ name: str = "",
271
+ ) -> dict[str, object]:
272
+ """Get authoritative details for a single symbol by `symbol` (or `canonical_id` alias). Use to inspect symbol declaration metadata. Does not traverse multi-hop call graphs."""
273
+ target = _resolve_symbol_arg(
274
+ symbol=symbol, canonical_id=canonical_id, name=name, tool_name="get_symbol"
275
+ )
276
+ with indexer.session() as con:
277
+ sym = graph_get_symbol(con, target)
278
+ if not sym:
279
+ return {"error": f"Symbol not found: {target}"}
280
+ return sym.as_dict()
281
+
282
+ def find_references(
283
+ symbol: str = "",
284
+ name: str = "",
285
+ canonical_id: str = "",
286
+ ) -> list[dict[str, object]]:
287
+ """Find textual references across indexed chunks by `symbol` (or `name` / `canonical_id` alias); use get_references for canonical evidence-backed symbol references. USE THIS when checking where a symbol identifier is mentioned across files. Results are explicitly not guaranteed semantic call edges."""
288
+ target = _resolve_symbol_arg(
289
+ symbol=symbol, canonical_id=canonical_id, name=name, tool_name="find_references"
290
+ )
291
+ short = target.split(":")[-1].split(".")[-1]
292
+ with indexer.session() as con:
293
+ return [
294
+ dict(row)
295
+ for row in con.execute(
296
+ "SELECT path AS file,symbol,start_line,end_line FROM chunks WHERE content LIKE ? ORDER BY path,start_line LIMIT 100",
297
+ (f"%{short}%",),
298
+ )
299
+ ]
300
+
301
+ def find_callers(
302
+ symbol: str = "",
303
+ canonical_id: str = "",
304
+ max_results: int = 20,
305
+ ) -> list[dict[str, object]]:
306
+ """Find functions and methods that call the specified `symbol` (accepts symbol name, qualified name, or `canonical_id` alias). USE THIS INSTEAD OF grep/search_code when asking 'who calls X?'. DO NOT use search_code for caller discovery. Returns verified `CALLS` and candidate `POSSIBLE_CALLS` with `confidence` and `evidence_class` (`AST_VERIFIED`, `DATAFLOW_VERIFIED`, `POSSIBLE`, `UNKNOWN`). Does not prove runtime execution of conditional branches."""
307
+ target = _resolve_symbol_arg(
308
+ symbol=symbol, canonical_id=canonical_id, tool_name="find_callers"
309
+ )
310
+ with indexer.session() as con:
311
+ return [dict(r) for r in graph_find_callers(con, target, max_results=max_results)]
312
+
313
+ def find_callees(
314
+ symbol: str = "",
315
+ canonical_id: str = "",
316
+ max_results: int = 20,
317
+ ) -> list[dict[str, object]]:
318
+ """Find functions and methods called by the specified `symbol` (accepts symbol name, qualified name, or `canonical_id` alias). USE THIS INSTEAD OF reading multiple files manually when asking 'what does X call?'. DO NOT use search_code for callee discovery. Returns verified `CALLS` and candidate `POSSIBLE_CALLS` with `confidence` and `evidence_class` (`AST_VERIFIED`, `DATAFLOW_VERIFIED`, `POSSIBLE`, `UNKNOWN`). Does not resolve dynamic string-based callbacks."""
319
+ target = _resolve_symbol_arg(
320
+ symbol=symbol, canonical_id=canonical_id, tool_name="find_callees"
321
+ )
322
+ with indexer.session() as con:
323
+ return [dict(r) for r in graph_find_callees(con, target, max_results=max_results)]
324
+
325
+ def get_call_graph(
326
+ symbol: str = "",
327
+ canonical_id: str = "",
328
+ depth: int = 2,
329
+ max_results: int = 100,
330
+ ) -> list[dict[str, object]]:
331
+ """Compute the multi-hop call graph rooted at `symbol` (or `canonical_id` alias) up to `depth`. Use when visualizing neighborhood call structure. Does not prove runtime reachability."""
332
+ target = _resolve_symbol_arg(
333
+ symbol=symbol, canonical_id=canonical_id, tool_name="get_call_graph"
334
+ )
335
+ with indexer.session() as con:
336
+ return [dict(e) for e in graph_get_call_graph(con, symbol=target, depth=depth, max_results=max_results)]
337
+
338
+ def get_dependency_graph(file_path: str | None = None) -> list[dict[str, object]]:
339
+ """List module import and dependency graph edges. Use for inspecting module-to-module import structure. Does not prove symbol-level call invocation."""
340
+ with indexer.session() as con:
341
+ if file_path:
342
+ rows = con.execute(
343
+ "SELECT source, target, relationship, confidence, evidence FROM graph_edges WHERE relationship='IMPORTS' AND (source=? OR file=?) ORDER BY source",
344
+ (file_path, file_path),
345
+ ).fetchall()
346
+ else:
347
+ rows = con.execute(
348
+ "SELECT source, target, relationship, confidence, evidence FROM graph_edges WHERE relationship='IMPORTS' ORDER BY source LIMIT 500"
349
+ ).fetchall()
350
+ return [dict(r) for r in rows]
351
+
352
+ def find_related_tests(
353
+ symbol: str = "",
354
+ canonical_id: str = "",
355
+ max_results: int = 10,
356
+ ) -> list[dict[str, object]]:
357
+ """Return test files and test functions statically linked to `symbol` (or `canonical_id` alias) via direct calls, imports, fixtures, or route invocation. USE THIS INSTEAD OF grep/search_code when asking 'what tests cover symbol X?' before or after a code change. Never fabricates coverage from lexical similarity alone."""
358
+ target = _resolve_symbol_arg(
359
+ symbol=symbol, canonical_id=canonical_id, tool_name="find_related_tests"
360
+ )
361
+ with indexer.session() as con:
362
+ return [dict(r) for r in graph_find_related_tests(con, target, max_results=max_results)]
363
+
364
+ def find_tests(
365
+ symbol: str = "",
366
+ canonical_id: str = "",
367
+ max_results: int = 10,
368
+ ) -> list[dict[str, object]]:
369
+ """Find test files and test functions statically linked to `symbol` (or `canonical_id` alias) via direct calls, imports, fixtures, or route invocation. USE THIS INSTEAD OF grep/search_code when asking 'what tests cover X?'. DO NOT guess test coverage from filename similarity alone. Returns verified test links (`TESTS_SYMBOL`, `TESTS_ROUTE`, `TESTS_PROVIDER`) with `confidence` and `evidence_class`."""
370
+ target = _resolve_symbol_arg(
371
+ symbol=symbol, canonical_id=canonical_id, tool_name="find_tests"
372
+ )
373
+ with indexer.session() as con:
374
+ return [dict(r) for r in graph_find_related_tests(con, target, max_results=max_results)]
375
+
376
+ def analyze_impact(
377
+ symbol: str = "",
378
+ canonical_id: str = "",
379
+ max_depth: int = 3,
380
+ ) -> dict[str, object]:
381
+ """Compute downstream callers, dependents, affected routes, and related tests if `symbol` (or `canonical_id` alias) is modified. Use before refactoring or changing a function/class signature. Does not prove runtime failure without inspecting call sites."""
382
+ target = _resolve_symbol_arg(
383
+ symbol=symbol, canonical_id=canonical_id, tool_name="analyze_impact"
384
+ )
385
+ with indexer.session() as con:
386
+ return graph_analyze_impact(con, target, max_depth=max_depth)
387
+
388
+ def compile_task(
389
+ query: str = "",
390
+ task: dict[str, Any] | str = "",
391
+ max_tokens: int = 20_000,
392
+ resource_mode: str = "BALANCED",
393
+ ) -> dict[str, object]:
394
+ """Normalize a `query` (or `task` alias), ground targets against repository symbols, detect ambiguities, and build a RetrievalPlan. Use before get_context when explicit ambiguity pre-checking is desired. Does not execute source retrieval."""
395
+ effective_task = _resolve_task_or_query_input(task=task, query=query)
396
+ with indexer.session() as con:
397
+ spec, ambiguities = normalize_task_spec(effective_task, con)
398
+ plan = build_retrieval_plan(
399
+ spec,
400
+ ambiguities,
401
+ con,
402
+ token_budget=max_tokens,
403
+ resource_mode=resource_mode,
404
+ )
405
+ entry_points: list[str] = []
406
+ for t in spec.priority_targets or spec.targets:
407
+ r_rows = con.execute(
408
+ "SELECT endpoint_id, handler_name FROM framework_routes WHERE route_path LIKE ? OR endpoint_id LIKE ?",
409
+ (f"%{t}%", f"%{t}%"),
410
+ ).fetchall()
411
+ for r in r_rows:
412
+ entry_points.append(r["endpoint_id"] or r["handler_name"])
413
+ amb_list = [a.as_dict() for a in ambiguities]
414
+ return {
415
+ "task_spec": spec.as_dict(),
416
+ "ambiguity": amb_list[0] if amb_list else None,
417
+ "ambiguities": amb_list,
418
+ "entry_points": entry_points,
419
+ "candidate_entry_points": entry_points,
420
+ "retrieval_plan": plan.as_dict(),
421
+ "recommended_next_step": "get_context",
422
+ }
423
+
424
+ def plan_retrieval(
425
+ query: str = "",
426
+ task: dict[str, Any] | str = "",
427
+ max_tokens: int = 20_000,
428
+ resource_mode: str = "BALANCED",
429
+ ) -> dict[str, object]:
430
+ """Construct a deterministic RetrievalPlan for a `query` (or `task` alias) without executing retrieval. Use to preview planned search and graph steps. Does not return code snippets."""
431
+ effective_task = _resolve_task_or_query_input(task=task, query=query)
432
+ with indexer.session() as con:
433
+ spec, ambiguities = normalize_task_spec(effective_task, con)
434
+ plan = build_retrieval_plan(
435
+ spec,
436
+ ambiguities,
437
+ con,
438
+ token_budget=max_tokens,
439
+ resource_mode=resource_mode,
440
+ )
441
+ return plan.as_dict()
442
+
443
+ def get_context(
444
+ query: str = "",
445
+ task: dict[str, Any] | str = "",
446
+ intent: str | None = None,
447
+ max_tokens: int = 4000,
448
+ max_files: int = 25,
449
+ max_lines: int = 500,
450
+ top_k: int = 25,
451
+ plan: dict[str, Any] | None = None,
452
+ mode: str = "BALANCED",
453
+ explain: bool = False,
454
+ resource_mode: str | None = None,
455
+ ) -> dict[str, object]:
456
+ """Compile a token-bounded, coverage-optimized ContextPacket for a `query` (or `task` alias: natural-language question, symbol/route target, or structured TaskSpec). Enforces strict pre-serialization boundaries (`max_tokens`, `max_files`, `max_lines`) and returns explicit budget metadata (`selected_tokens`, `candidate_tokens`, `selected_files`, `selected_lines`, `coverage_score`, `truncated`). Example: `get_context(query="Trace InventoryService.place_order callers and tests", intent="TRACE", max_tokens=4000, max_files=10, max_lines=300)`."""
457
+ effective_task = _resolve_task_or_query_input(task=task, query=query)
458
+ effective_mode = resource_mode or mode
459
+ with indexer.session() as con:
460
+ packet = compile_context(
461
+ con,
462
+ indexer.repository,
463
+ task=effective_task,
464
+ intent=intent,
465
+ max_tokens=max_tokens,
466
+ max_files=max_files,
467
+ max_lines=max_lines,
468
+ top_k=top_k,
469
+ plan=plan,
470
+ mode=effective_mode,
471
+ explain=explain,
472
+ )
473
+ return packet.as_dict()
474
+
475
+ def get_recent_changes(
476
+ since: str = "HEAD~10",
477
+ until: str = "HEAD",
478
+ ) -> list[dict[str, str]]:
479
+ """Return files changed between two commits or refs (read-only, sandboxed)."""
480
+ diffs = changed_files(indexer.repository, since=since, until=until)
481
+ return [d.as_dict() for d in diffs]
482
+
483
+ def get_file_history(
484
+ path: str,
485
+ n: int = 10,
486
+ ) -> list[dict[str, str]]:
487
+ """Return recent commits touching the specified repository file."""
488
+ return git_get_file_history(indexer.repository, path, n=n)
489
+
490
+ def analyze_change_impact(
491
+ since: str = "HEAD~1",
492
+ until: str = "HEAD",
493
+ ) -> dict[str, object]:
494
+ """Analyze downstream callers and tests affected by changes between since and until."""
495
+ with indexer.session() as con:
496
+ return git_analyze_change_impact(con, indexer.repository, since=since, until=until)
497
+
498
+ def get_repository_status() -> dict[str, object]:
499
+ """Get repository indexing status, freshness, symbol counts, and health (automatically hot-reloads in-memory caches when `.codegraph.sqlite3` is created or updated by `codegraph init` / `codegraph index`)."""
500
+ from codegraph.resources.cache import get_graph_cache, get_parse_cache
501
+
502
+ get_graph_cache().clear()
503
+ get_parse_cache().clear()
504
+ if not indexer.db_path.exists():
505
+ return {
506
+ "status": "error",
507
+ "repository": str(indexer.repository),
508
+ "generation": 0,
509
+ "freshness": "NOT_INDEXED",
510
+ "freshness_detail": "No CodeGraph index exists for this repository.",
511
+ "files": 0,
512
+ "files_indexed": 0,
513
+ "symbols": 0,
514
+ "symbols_indexed": 0,
515
+ "graph_edges": 0,
516
+ "framework_routes": 0,
517
+ "reloaded": True,
518
+ "error": {
519
+ "code": "INDEX_NOT_FOUND",
520
+ "message": "No CodeGraph index exists for this repository.",
521
+ "next_action": {
522
+ "command": "codegraph init",
523
+ "reason": "Initialize the repository before querying it.",
524
+ },
525
+ },
526
+ }
527
+ with indexer.session() as con:
528
+ report = check_freshness(indexer.repository, con)
529
+ file_count = int(con.execute("SELECT count(*) FROM files").fetchone()[0])
530
+ symbol_count = int(con.execute("SELECT count(*) FROM symbols").fetchone()[0])
531
+ edge_count = int(con.execute("SELECT count(*) FROM graph_edges").fetchone()[0])
532
+ route_count = int(con.execute("SELECT count(*) FROM framework_routes").fetchone()[0])
533
+ if file_count == 0:
534
+ return {
535
+ "status": "error",
536
+ "repository": str(indexer.repository),
537
+ "generation": report.index_generation,
538
+ "freshness": "NOT_INDEXED",
539
+ "freshness_detail": "No files are indexed in this repository.",
540
+ "files": 0,
541
+ "files_indexed": 0,
542
+ "symbols": 0,
543
+ "symbols_indexed": 0,
544
+ "graph_edges": 0,
545
+ "framework_routes": 0,
546
+ "reloaded": True,
547
+ "error": {
548
+ "code": "INDEX_NOT_FOUND",
549
+ "message": "No CodeGraph index exists for this repository.",
550
+ "next_action": {
551
+ "command": "codegraph init",
552
+ "reason": "Initialize the repository before querying it.",
553
+ },
554
+ },
555
+ }
556
+ return {
557
+ "status": "ok",
558
+ "repository": str(indexer.repository),
559
+ "generation": report.index_generation,
560
+ "freshness": report.status.value,
561
+ "freshness_detail": report.detail,
562
+ "files": file_count,
563
+ "files_indexed": file_count,
564
+ "symbols": symbol_count,
565
+ "symbols_indexed": symbol_count,
566
+ "graph_edges": edge_count,
567
+ "framework_routes": route_count,
568
+ "modified_files": report.modified_files,
569
+ "deleted_files": report.deleted_files,
570
+ "parse_failed_files": report.parse_failed_files,
571
+ "reloaded": True,
572
+ }
573
+
574
+ def trace_call(
575
+ symbol: str = "",
576
+ canonical_id: str = "",
577
+ depth: int = 2,
578
+ callers: bool = True,
579
+ callees: bool = False,
580
+ both: bool = False,
581
+ ) -> list[dict[str, object]]:
582
+ """Trace known definition, callers, and callees for `symbol` (or `canonical_id` alias) with explicit confidence and relationship labels. USE THIS when exploring the call neighborhood around a single symbol. Does not prove runtime execution of conditional branches."""
583
+ target = _resolve_symbol_arg(
584
+ symbol=symbol, canonical_id=canonical_id, tool_name="trace_call"
585
+ )
586
+ with indexer.session() as con:
587
+ return graph_trace_call(con, target, max_depth=depth, callers=callers, callees=callees, both=both)
588
+
589
+ def trace_flow(
590
+ symbol: str = "",
591
+ canonical_id: str = "",
592
+ depth: int = 2,
593
+ callers: bool = True,
594
+ callees: bool = False,
595
+ both: bool = False,
596
+ ) -> list[dict[str, object]]:
597
+ """Trace multi-hop upstream callers and downstream callees for `symbol` (or `canonical_id` alias) up to `depth`. USE THIS when exploring the bidirectional call flow around a single symbol. Returns verified and candidate call hops with explicit `confidence` and `evidence_class`. Does not prove runtime execution of conditional branches."""
598
+ target = _resolve_symbol_arg(
599
+ symbol=symbol, canonical_id=canonical_id, tool_name="trace_flow"
600
+ )
601
+ with indexer.session() as con:
602
+ return graph_trace_call(con, target, max_depth=depth, callers=callers, callees=callees, both=both)
603
+
604
+ def get_project_structure() -> list[str]:
605
+ """List indexed source paths only."""
606
+ with indexer.session() as con:
607
+ return [r[0] for r in con.execute("SELECT path FROM files ORDER BY path LIMIT 500")]
608
+
609
+ def get_dependencies() -> list[dict[str, str]]:
610
+ """List source imports as static evidence, without resolving external packages."""
611
+ with indexer.session() as con:
612
+ return [
613
+ dict(r)
614
+ for r in con.execute(
615
+ "SELECT source_path,imported FROM imports ORDER BY source_path,imported LIMIT 500"
616
+ )
617
+ ]
618
+
619
+ def get_file_symbols(path: str) -> list[dict[str, object]]:
620
+ """List extracted symbols in one repository-relative file."""
621
+ resolved = safe_path(indexer.repository, path)
622
+ relative = resolved.relative_to(indexer.repository).as_posix()
623
+ with indexer.session() as con:
624
+ rows = con.execute(
625
+ "SELECT qualified_name AS symbol,kind,start_line,end_line FROM symbols "
626
+ "WHERE path=? ORDER BY start_line LIMIT 200",
627
+ (relative,),
628
+ )
629
+ return [dict(row) for row in rows]
630
+
631
+ def get_graph(limit: int = 200) -> list[dict[str, object]]:
632
+ """Return bounded parser-confirmed file/symbol/import graph edges with evidence metadata."""
633
+ if not 1 <= limit <= 500:
634
+ raise ValueError("limit must be 1..500")
635
+ with indexer.session() as con:
636
+ edges = definition_edges(con, limit) + import_edges(con)
637
+ return [edge.as_dict() for edge in edges[:limit]]
638
+
639
+ def search_memory(query: str, limit: int = 20) -> list[dict[str, str]]:
640
+ """Search local repository memory. Memory never overrides current source evidence."""
641
+ if not 1 <= limit <= 100:
642
+ raise ValueError("limit must be 1..100")
643
+ return [{"key": key, "value": value} for key, value in memory.search(query, limit)]
644
+
645
+ def get_evidence(path: str, symbol: str | None = None) -> list[dict[str, object]]:
646
+ """Return source-derived evidence for a safe indexed file, optionally narrowed to one symbol."""
647
+ resolved = safe_path(indexer.repository, path)
648
+ relative = resolved.relative_to(indexer.repository)
649
+ if is_sensitive(relative):
650
+ raise SecurityError("sensitive files cannot be used as evidence")
651
+ with indexer.session() as con:
652
+ if symbol:
653
+ rows = con.execute(
654
+ "SELECT symbol,start_line,end_line,content FROM chunks WHERE path=? AND symbol=? LIMIT 20",
655
+ (relative.as_posix(), symbol),
656
+ )
657
+ else:
658
+ rows = con.execute(
659
+ "SELECT symbol,start_line,end_line,content FROM chunks WHERE path=? ORDER BY start_line LIMIT 50",
660
+ (relative.as_posix(),),
661
+ )
662
+ return [
663
+ Evidence(
664
+ relative.as_posix(),
665
+ row["start_line"],
666
+ row["start_line"] if row["end_line"] is None else row["end_line"],
667
+ row["symbol"],
668
+ row["content"][:900],
669
+ ).as_dict()
670
+ for row in rows
671
+ ]
672
+
673
+ def verify_evidence(
674
+ file_path: str,
675
+ start_line: int,
676
+ end_line: int,
677
+ expected_hash: str | None = None,
678
+ symbol: str | None = None,
679
+ ) -> dict[str, object]:
680
+ """Verify that a cited piece of evidence or code snippet actually exists, matches disk hash, and remains valid."""
681
+ ev = Evidence(
682
+ file=file_path,
683
+ start_line=start_line,
684
+ end_line=end_line,
685
+ symbol=symbol,
686
+ snippet="",
687
+ )
688
+ with indexer.session() as con:
689
+ res = verify_evidence_fn(con, indexer.repository, ev, expected_content_hash=expected_hash)
690
+ return res.as_dict()
691
+
692
+ def get_resource_status() -> dict[str, object]:
693
+ """Inspect current resource governor state, memory usage, pressure, and activity mode."""
694
+ return indexer.governor.get_state().as_dict()
695
+
696
+ # -----------------------------------------------------------------------
697
+ # Core Deterministic Interrogation Tools (13 Core Capabilities)
698
+ # -----------------------------------------------------------------------
699
+
700
+ def resolve_symbol(
701
+ symbol: str = "",
702
+ canonical_id: str = "",
703
+ name: str = "",
704
+ ) -> dict[str, Any]:
705
+ """Resolve a `symbol` (or `canonical_id` / `name` alias) into its canonical repository identity and file/line location. Use as the first step before relationship queries when exact symbol identity is unknown or potentially ambiguous. Returns canonical_id, ambiguity_state, and candidate alternatives. Does not prove runtime execution or dynamic monkey-patching."""
706
+ target = _resolve_symbol_arg(
707
+ symbol=symbol, canonical_id=canonical_id, name=name, tool_name="resolve_symbol"
708
+ )
709
+ with indexer.session() as con:
710
+ return interrogation_resolve_symbol(con, indexer.repository, symbol=target)
711
+
712
+ def search_symbols(query: str, top_k: int = 20) -> dict[str, Any]:
713
+ """Search indexed repository symbols by partial name or keyword with ranked relevance. Use when exact symbol spelling is unknown and you need candidate symbol definitions. Returns ranked symbol definitions with file paths and line spans. Does not return module import graphs or call relationships (use get_imports or get_dependents instead)."""
714
+ with indexer.session() as con:
715
+ return interrogation_search_symbols(con, indexer.repository, query, top_k=top_k)
716
+
717
+ def get_symbol_interrogation_wrapper(
718
+ symbol: str = "",
719
+ canonical_id: str = "",
720
+ name: str = "",
721
+ ) -> dict[str, Any]:
722
+ """Return authoritative signature, kind, parent scope, decorators, and bounded source snippet for `symbol` (or `canonical_id` alias). Use when you have a symbol name, qualified name, or canonical_id and need its declaration metadata. Does not traverse multi-hop call graphs."""
723
+ target = _resolve_symbol_arg(
724
+ symbol=symbol, canonical_id=canonical_id, name=name, tool_name="get_symbol"
725
+ )
726
+ with indexer.session() as con:
727
+ return interrogation_get_symbol(con, indexer.repository, symbol=target)
728
+
729
+ def get_file(
730
+ path: str,
731
+ start_line: int | None = None,
732
+ end_line: int | None = None,
733
+ max_lines: int = 200,
734
+ include_content: bool = False,
735
+ ) -> dict[str, Any]:
736
+ """Read bounded source lines (`start_line`, `end_line`, `max_lines`) and structural AST outline of a repository file (`.py`, `.html`, `.jinja`, `.js`, `.ts`, `.css`, `.yaml`, `.json`, `.md`). USE THIS after search_code, find_symbol, or find_routes identifies a target file and line range. DO NOT read entire huge files blindly when a targeted line range is known. Returns `path`, `start_line`, `end_line`, `content`, `truncated`, and AST symbols while blocking sensitive files and path traversal."""
737
+ with indexer.session() as con:
738
+ return interrogation_get_file(
739
+ con,
740
+ indexer.repository,
741
+ path,
742
+ include_content=include_content,
743
+ max_read_bytes=max_read_bytes,
744
+ start_line=start_line,
745
+ end_line=end_line,
746
+ max_lines=max_lines,
747
+ )
748
+
749
+ def get_references(
750
+ symbol: str = "",
751
+ canonical_id: str = "",
752
+ name: str = "",
753
+ ) -> dict[str, Any]:
754
+ """Return verified and candidate references to `symbol` (or `canonical_id` alias) across the repository with evidence_class labels. Use for cross-file reference, registration, dispatch, or DI binding lookup. Does not guarantee dynamic reflection targets when evidence_class is UNKNOWN or POSSIBLE."""
755
+ target = _resolve_symbol_arg(
756
+ symbol=symbol, canonical_id=canonical_id, name=name, tool_name="get_references"
757
+ )
758
+ with indexer.session() as con:
759
+ return interrogation_get_references(con, indexer.repository, symbol=target)
760
+
761
+ def get_callers(
762
+ symbol: str = "",
763
+ canonical_id: str = "",
764
+ ) -> dict[str, Any]:
765
+ """Return statically verified and candidate callers of `symbol` (accepts symbol name, qualified name, or `canonical_id` alias) with confidence and evidence_class. USE THIS INSTEAD OF grep/search_code for multi-file call-relationship and upstream impact questions. Does not prove runtime dispatch unless evidence_class indicates framework/dataflow verification."""
766
+ target = _resolve_symbol_arg(
767
+ symbol=symbol, canonical_id=canonical_id, tool_name="get_callers"
768
+ )
769
+ with indexer.session() as con:
770
+ return interrogation_get_callers(con, indexer.repository, symbol=target)
771
+
772
+ def get_callees(
773
+ symbol: str = "",
774
+ canonical_id: str = "",
775
+ ) -> dict[str, Any]:
776
+ """Return symbols called or invoked by `symbol` (accepts symbol name, qualified name, or `canonical_id` alias) with evidence_class classification. USE THIS INSTEAD OF reading multiple files manually to inspect downstream dependencies invoked by a function or handler. Does not resolve dynamic callbacks passed as opaque runtime arguments."""
777
+ target = _resolve_symbol_arg(
778
+ symbol=symbol, canonical_id=canonical_id, tool_name="get_callees"
779
+ )
780
+ with indexer.session() as con:
781
+ return interrogation_get_callees(con, indexer.repository, symbol=target)
782
+
783
+ def trace_path(
784
+ from_symbol: str = "",
785
+ to_symbol: str = "",
786
+ start_symbol: str = "",
787
+ target_symbol: str = "",
788
+ source_symbol: str = "",
789
+ max_depth: int = 5,
790
+ ) -> dict[str, Any]:
791
+ """Compute deterministic multi-hop relationship paths between two symbols (`from_symbol` -> `to_symbol`). USE THIS INSTEAD OF manual file-by-file hopping when tracing how an entrypoint, route, or caller reaches a downstream service or database function. Returns ordered hop edges with relationship types and `evidence_class` (`AST_VERIFIED`, `FRAMEWORK_VERIFIED`, `DATAFLOW_VERIFIED`, `POSSIBLE`, `UNKNOWN`). Does not prove runtime branch conditions along the path."""
792
+ src = _resolve_symbol_arg(
793
+ symbol=from_symbol,
794
+ canonical_id=start_symbol,
795
+ name=source_symbol,
796
+ tool_name="trace_path(from_symbol)",
797
+ )
798
+ tgt = _resolve_symbol_arg(
799
+ symbol=to_symbol,
800
+ canonical_id=target_symbol,
801
+ tool_name="trace_path(to_symbol)",
802
+ )
803
+ with indexer.session() as con:
804
+ return interrogation_trace_path(con, indexer.repository, src, tgt, max_depth=max_depth)
805
+
806
+ def get_imports(
807
+ symbol: str | None = None,
808
+ file: str | None = None,
809
+ canonical_id: str | None = None,
810
+ path: str | None = None,
811
+ ) -> dict[str, Any]:
812
+ """Return parser-extracted module and symbol imports for `file` (or `path` alias) or `symbol` (or `canonical_id` alias). Use for forward dependency and package boundary questions. Does not return reverse importers (use get_dependents for reverse dependencies)."""
813
+ if symbol and canonical_id and symbol.strip() and canonical_id.strip() and symbol.strip() != canonical_id.strip():
814
+ raise ValueError(f"Conflicting symbol arguments passed to get_imports: symbol={symbol!r}, canonical_id={canonical_id!r}")
815
+ if file and path and file.strip() and path.strip() and file.strip() != path.strip():
816
+ raise ValueError(f"Conflicting file arguments passed to get_imports: file={file!r}, path={path!r}")
817
+ eff_sym = (symbol or canonical_id or "").strip() or None
818
+ eff_file = (file or path or "").strip() or None
819
+ if not eff_sym and not eff_file:
820
+ raise ValueError("get_imports requires at least one of 'symbol' ('canonical_id') or 'file' ('path').")
821
+ with indexer.session() as con:
822
+ return interrogation_get_imports(con, indexer.repository, file=eff_file, canonical_id=eff_sym, symbol=eff_sym, path=eff_file)
823
+
824
+ def get_dependents(
825
+ symbol: str | None = None,
826
+ file: str | None = None,
827
+ canonical_id: str | None = None,
828
+ path: str | None = None,
829
+ ) -> dict[str, Any]:
830
+ """Return files, symbols, and packages that import or depend on `symbol` (or `canonical_id` alias) or `file` (or `path` alias). Use for blast-radius, change-impact, and package boundary analysis. Does not prove runtime failure without checking test and caller evidence."""
831
+ if symbol and canonical_id and symbol.strip() and canonical_id.strip() and symbol.strip() != canonical_id.strip():
832
+ raise ValueError(f"Conflicting symbol arguments passed to get_dependents: symbol={symbol!r}, canonical_id={canonical_id!r}")
833
+ if file and path and file.strip() and path.strip() and file.strip() != path.strip():
834
+ raise ValueError(f"Conflicting file arguments passed to get_dependents: file={file!r}, path={path!r}")
835
+ eff_sym = (symbol or canonical_id or "").strip() or None
836
+ eff_file = (file or path or "").strip() or None
837
+ if not eff_sym and not eff_file:
838
+ raise ValueError("get_dependents requires at least one of 'symbol' ('canonical_id') or 'file' ('path').")
839
+ with indexer.session() as con:
840
+ return interrogation_get_dependents(con, indexer.repository, canonical_id=eff_sym, file=eff_file, symbol=eff_sym, path=eff_file)
841
+
842
+ def list_routes(
843
+ framework: str | None = None,
844
+ method: str | None = None,
845
+ path: str | None = None,
846
+ ) -> dict[str, Any]:
847
+ """Return HTTP/RPC framework routes, mounted router prefixes (`MOUNTS`), methods, and handler symbols. USE THIS INSTEAD OF grep/search_code when locating API endpoints, route handlers, or mounted sub-applications. Does not prove runtime middleware authentication unless traced via get_context or trace_path."""
848
+ with indexer.session() as con:
849
+ return interrogation_list_routes(con, indexer.repository, framework=framework, method=method, path=path)
850
+
851
+ def find_routes(
852
+ framework: str | None = None,
853
+ method: str | None = None,
854
+ path: str | None = None,
855
+ ) -> dict[str, Any]:
856
+ """Find HTTP/RPC framework routes, mounted router prefixes (`MOUNTS`), methods, and handler symbols. USE THIS INSTEAD OF grep/search_code when asking 'which route handles /path?' or listing API endpoints. DO NOT grep for split router prefix strings. Returns `route_path`, `http_method`, `handler_name`, `canonical_id`, `file`, `line`, and `evidence_class` (`FRAMEWORK_VERIFIED`). Does not prove runtime middleware state."""
857
+ with indexer.session() as con:
858
+ return interrogation_list_routes(con, indexer.repository, framework=framework, method=method, path=path)
859
+
860
+ def get_architecture() -> dict[str, Any]:
861
+ """Return structural repository overview including modules, workspace packages (`DEPENDS_ON_PACKAGE`), entrypoints, and layer relationships. Use for high-level architecture, monorepo package boundary, and onboarding questions. Does not replace symbol-level evidence for specific bug fixes."""
862
+ with indexer.session() as con:
863
+ return interrogation_get_architecture(con, indexer.repository)
864
+
865
+ def get_git_impact(base: str = "HEAD~1", head: str = "HEAD") -> dict[str, Any]:
866
+ """Compute deterministic change impact between Git refs (`base`..`head`), including modified symbols, downstream callers, affected packages, and covering tests. Use for PR review, regression analysis, and pre-commit blast-radius checks. Does not execute tests; reports static test-to-symbol coverage edges."""
867
+ with indexer.session() as con:
868
+ return interrogation_get_git_impact(con, indexer.repository, base=base, head=head)
869
+
870
+ # -----------------------------------------------------------------------
871
+ # Database Deep Intelligence Tools (11 Tools)
872
+ # -----------------------------------------------------------------------
873
+
874
+ def find_db_tables(table: str = "", dialect: str = "", schema: str = "") -> dict[str, Any]:
875
+ """Find database tables discovered across ORM models, raw SQL queries, and migrations. Use instead of grep when asking 'which database tables exist in this repository?'. Returns canonical IDs (`db.<dialect>.<schema>.<table>`), columns, ORM models, and evidence classes (`FRAMEWORK_VERIFIED`, `STATIC_VERIFIED`, `POSSIBLE`, `UNKNOWN`). Does not connect to live databases or execute SQL."""
876
+ with indexer.session() as con:
877
+ return db_find_db_tables(con, indexer.repository, table=table, dialect=dialect, schema=schema)
878
+
879
+ def find_db_columns(table: str = "", column: str = "") -> dict[str, Any]:
880
+ """Find database columns, data types, nullability, primary keys, and foreign keys across tables and ORM models. Use instead of grep when locating where a table column is defined or mapped (`MAPS_TO_COLUMN`, `HAS_PRIMARY_KEY`, `FOREIGN_KEY_TO`). Does not inspect live database catalogs."""
881
+ with indexer.session() as con:
882
+ return db_find_db_columns(con, indexer.repository, table=table, column=column)
883
+
884
+ def find_db_models(model: str = "", table: str = "") -> dict[str, Any]:
885
+ """Find ORM models (SQLAlchemy, Flask-SQLAlchemy, Django ORM, SQLModel, Prisma) and their `MAPS_TO_TABLE` mappings. Use when asking which model class maps to a database table or vice versa. Returns model name, table, canonical ID, and `FRAMEWORK_VERIFIED` evidence. Does not import or execute application model modules."""
886
+ with indexer.session() as con:
887
+ return db_find_db_models(con, indexer.repository, model=model, table=table)
888
+
889
+ def find_db_queries(table: str = "", symbol: str = "", operation: str = "") -> dict[str, Any]:
890
+ """Find database queries (`SELECT`, `INSERT`, `UPDATE`, `DELETE`) across ORM calls, query builders, and raw SQL strings. Use when asking which queries touch a table or what SQL a function executes. Returns `READS_TABLE`, `WRITES_TABLE`, `POSSIBLE_TABLE`, or `UNKNOWN_TABLE` with redacted snippets. Never exposes SQL literal secret parameters."""
891
+ with indexer.session() as con:
892
+ return db_find_db_queries(con, indexer.repository, table=table, symbol=symbol, operation=operation)
893
+
894
+ def find_db_callers(table: str) -> dict[str, Any]:
895
+ """Find all functions, methods, and upstream HTTP routes (`HANDLED_BY`) that read from or write to a database table. Use instead of grep when asking 'which code or route reaches table X?'. Returns direct accessors, upstream routes, relationships (`READS_TABLE`, `WRITES_TABLE`), and evidence classes. Does not prove runtime query frequency."""
896
+ with indexer.session() as con:
897
+ return db_find_db_callers(con, indexer.repository, table=table)
898
+
899
+ def find_db_writers(table: str = "", column: str = "") -> dict[str, Any]:
900
+ """Find all symbols and queries that write (`INSERT`, `UPDATE`, `DELETE`) to a database table or column (`WRITES_TABLE`, `WRITES_COLUMN`). Use when investigating data mutations, state changes, or write blast radius. Returns writer symbols, operations, file:line citations, and redacted snippets. Does not execute database transactions."""
901
+ with indexer.session() as con:
902
+ return db_find_db_writers(con, indexer.repository, table=table, column=column)
903
+
904
+ def find_db_readers(table: str = "", column: str = "") -> dict[str, Any]:
905
+ """Find all symbols and queries that read (`SELECT`, `.query()`, `.objects.filter()`, `.findMany()`) from a database table or column (`READS_TABLE`, `READS_COLUMN`). Use when tracing where table data is consumed across services and views. Returns reader symbols, operations, and evidence classes. Does not prove runtime cache hits."""
906
+ with indexer.session() as con:
907
+ return db_find_db_readers(con, indexer.repository, table=table, column=column)
908
+
909
+ def find_db_relationships(table: str = "") -> dict[str, Any]:
910
+ """Find database schema and ORM relationships (`FOREIGN_KEY_TO`, `ORM_RELATION`, `MAPS_TO_TABLE`, `HAS_PRIMARY_KEY`, `HAS_INDEX`, `MIGRATES_TABLE`). Use when inspecting foreign keys, table joins, or model associations. Preserves explicit database relationship types and never collapses them into generic `DEPENDS_ON`."""
911
+ with indexer.session() as con:
912
+ return db_find_db_relationships(con, indexer.repository, table=table)
913
+
914
+ def get_db_table(table: str) -> dict[str, Any]:
915
+ """Return complete structural details for a database table including columns, primary keys, foreign keys, indexes, constraints, ORM models, readers, writers, migrations, and upstream routes. Use when inspecting a specific table's schema and code usage in one call. Does not query live database servers."""
916
+ with indexer.session() as con:
917
+ return db_get_db_table(con, indexer.repository, table=table)
918
+
919
+ def get_db_schema(dialect: str = "", schema: str = "") -> dict[str, Any]:
920
+ """Return a repository-wide database schema summary including all discovered tables, columns, ORM models, foreign keys, and migrations. Use for database architecture overviews or schema audits. Preserves `UNKNOWN` dialect/schema when not statically provable and never guesses database names."""
921
+ with indexer.session() as con:
922
+ return db_get_db_schema(con, indexer.repository, dialect=dialect, schema=schema)
923
+
924
+ def get_db_impact(table: str = "", column: str = "") -> dict[str, Any]:
925
+ """Compute bidirectional Code <-> Database change impact for a table or column, returning affected ORM models, readers, writers, upstream HTTP routes, migrations, and statically linked tests. Use before renaming or altering a database table or column. Does not execute database migrations or tests."""
926
+ with indexer.session() as con:
927
+ return db_get_db_impact(con, indexer.repository, table=table, column=column)
928
+
929
+ # -----------------------------------------------------------------------
930
+ # Runtime Observation & Reconciliation Tools (3 Tools)
931
+ # -----------------------------------------------------------------------
932
+
933
+ def ingest_runtime_traces(
934
+ source_path: str = "",
935
+ format: str = "auto",
936
+ payload: str = "",
937
+ max_events: int = 5000,
938
+ sample_rate: float = 1.0,
939
+ ) -> dict[str, Any]:
940
+ """Ingest optional runtime observation traces (OpenTelemetry JSON, structured JSON/JSONL events, or SQL query logs) into CodeGraph's `RUNTIME_OBSERVED` layer. Use when grounding static analysis with recorded runtime traces. Automatically strips headers/cookies/bodies, redacts SQL bind parameters and secrets, and never converts runtime observations into static `AST_VERIFIED` proof."""
941
+ with indexer.session() as con:
942
+ return runtime_ingest_runtime_traces(
943
+ con,
944
+ indexer.repository,
945
+ source_path=source_path,
946
+ format=format,
947
+ payload=payload,
948
+ max_events=max_events,
949
+ sample_rate=sample_rate,
950
+ )
951
+
952
+ def get_runtime_trace(route: str = "", symbol: str = "", table: str = "") -> dict[str, Any]:
953
+ """Retrieve aggregated runtime execution edges (`RUNTIME_OBSERVED`) and reverse maps (`table -> runtime writers/readers`, `route -> runtime tables`) with `observation_count`, `first_seen`, `last_seen`, and `runtime_generation`. Use when asking what actually happened at runtime. Does not treat unobserved paths as impossible."""
954
+ with indexer.session() as con:
955
+ return runtime_get_runtime_trace(con, indexer.repository, route=route, symbol=symbol, table=table)
956
+
957
+ def reconcile_static_runtime(symbol: str = "", route: str = "", table: str = "") -> dict[str, Any]:
958
+ """Reconcile static repository edges against ingested runtime observations, classifying edges into `CONFIRMED_RUNTIME_PATH`, `STATIC_RUNTIME_CONFLICT`, `NOT_OBSERVED_AT_RUNTIME`, and `RUNTIME_ONLY_OBSERVED`. Use when comparing static code analysis with runtime behavior. Never treats `NOT_OBSERVED_AT_RUNTIME` as proof that a path cannot execute."""
959
+ with indexer.session() as con:
960
+ return runtime_reconcile_static_runtime(con, indexer.repository, symbol=symbol, route=route, table=table)
961
+
962
+ def get_live_services() -> dict[str, Any]:
963
+ """Detect and attribute active localhost services, ports, PIDs, frameworks, and workspace sub-packages with zero idle CPU. Use when diagnosing running dev servers or multi-service architectures. Does not prove live HTTP response payloads or application layer health."""
964
+ from codegraph.runtime.port_inspector import (
965
+ discover_live_listening_ports,
966
+ discover_static_configured_ports,
967
+ )
968
+ live = [s.as_dict() for s in discover_live_listening_ports(indexer.repository)]
969
+ static = [s.as_dict() for s in discover_static_configured_ports(indexer.repository).values()]
970
+ return {"live_services": live, "configured_services": static, "count": len(live)}
971
+
972
+ def check_db_drift() -> dict[str, Any]:
973
+ """Detect database schema drift between migration files, live database state, and code ORM models with zero idle CPU. Flags unmapped tables, missing columns, and migration version state. Use when verifying whether database migrations are up to date with code models. Does not execute SQL data assertions or validate column constraints against live row data."""
974
+ from codegraph.runtime.db_watcher import detect_schema_drift
975
+ with indexer.session() as con:
976
+ return detect_schema_drift(con, indexer.repository).as_dict()
977
+
978
+ def get_distributed_trace(trace_id: str) -> dict[str, Any]:
979
+ """Stitch and reconstruct multi-service distributed execution trees across frontend, backend, and database boundaries for a specific W3C trace ID. Use when tracing cross-service requests or debugging multi-service latency bottlenecks. Does not capture unobserved code paths or untraced sidecar requests."""
980
+ from codegraph.runtime.trace_linker import link_distributed_trace
981
+ with indexer.session() as con:
982
+ res = link_distributed_trace(con, indexer.repository, trace_id)
983
+ return res.as_dict() if res else {"status": "not_found", "trace_id": trace_id}
984
+
985
+ def check_route_schema_drift(
986
+ route: str = "",
987
+ handler: str = "",
988
+ table: str | None = None,
989
+ schema: str | None = None,
990
+ ) -> dict[str, Any]:
991
+ """Detect schema validation drift between a route/handler input schema (Pydantic BaseModel, Django Form, DRF Serializer) and destination database table columns. Flags MISSING_REQUIRED_COLUMN, NULLABILITY_MISMATCH, TYPE_INCOMPATIBILITY, LENGTH_CONSTRAINT_DRIFT, and UNUSED_SCHEMA_FIELD. Does not execute live requests."""
992
+ from codegraph.database.schema_drift import detect_schema_drift as impl_detect_drift
993
+ target = route or handler
994
+ with indexer.session() as con:
995
+ return impl_detect_drift(
996
+ con,
997
+ indexer.repository,
998
+ route_or_handler=target,
999
+ target_table=table,
1000
+ schema_name=schema,
1001
+ )
1002
+
1003
+ def check_api_drift() -> dict[str, Any]:
1004
+ """Detect client-server API contract drift between frontend fetch or axios calls and backend route registrations. Flags orphaned client endpoints (404s), HTTP method mismatches (405s), and cross-language TypeScript-to-backend model drift. Use when validating frontend API calls against backend endpoints before deployment. Does not execute live HTTP requests or validate dynamic URL strings constructed via runtime interpolation."""
1005
+ from codegraph.api_drift import detect_api_contract_drift
1006
+ with indexer.session() as con:
1007
+ return detect_api_contract_drift(con, indexer.repository).as_dict()
1008
+
1009
+ def get_monorepo_packages() -> dict[str, Any]:
1010
+ """Catalog monorepo workspace packages, tools (pnpm, Turborepo, npm/yarn workspaces), entry points, and inter-package dependencies. Use when mapping multi-package repository structure and package boundaries. Does not prove build system execution order or runtime dependency loading."""
1011
+ from codegraph.monorepo import discover_workspace
1012
+ return discover_workspace(indexer.repository).as_dict()
1013
+
1014
+ # -----------------------------------------------------------------------
1015
+ # Git Intelligence Tools (5 Unified Capabilities)
1016
+ # -----------------------------------------------------------------------
1017
+
1018
+ def get_git_state() -> dict[str, Any]:
1019
+ """Track current branch, HEAD, indexed HEAD, working-tree modifications, staged files, and explicit freshness states (CLEAN, DIRTY, STALE, REINDEXING, ERROR). Use when asking if the index is up to date or what uncommitted changes exist. Does not perform a full repository scan when an incremental update is safe."""
1020
+ from codegraph.git_state import get_git_state as impl_get_git_state
1021
+ with indexer.session() as con:
1022
+ return impl_get_git_state(indexer.repository, con=con).as_dict()
1023
+
1024
+ def compare_git(base: str = "HEAD~1", head: str = "HEAD", branch_comparison: bool = False) -> dict[str, Any]:
1025
+ """Compare two Git revisions, commits, or branches structurally. Detects added/deleted/modified/renamed files, AST symbol changes, relationship diffs, routes, and affected tests. Use when comparing branches or commits structurally. Does not execute repository code."""
1026
+ if branch_comparison:
1027
+ from codegraph.branch_comparison import compare_branch_architecture
1028
+ with indexer.session() as con:
1029
+ return compare_branch_architecture(indexer.repository, base_branch=base, head_branch=head, con=con).as_dict()
1030
+ else:
1031
+ from codegraph.structural_diff import compare_revisions
1032
+ with indexer.session() as con:
1033
+ return compare_revisions(indexer.repository, base=base, head=head, con=con).as_dict()
1034
+
1035
+ def get_change_impact(base: str = "HEAD~1", head: str = "HEAD", max_depth: int = 2, max_results: int = 50) -> dict[str, Any]:
1036
+ """Compute deep downstream change impact across callers, callees, framework routes, covering tests, mutating database queries, and monorepo packages. Use when analyzing blast radius and ripple effects of git commits. Does not assume unobserved runtime paths are impossible."""
1037
+ from codegraph.change_impact import get_deep_change_impact
1038
+ target_sym = None
1039
+ clean_base = base.strip()
1040
+ if clean_base.startswith("symbol:"):
1041
+ target_sym = clean_base.split("symbol:", 1)[1]
1042
+ with indexer.session() as con:
1043
+ return get_deep_change_impact(indexer.repository, con, base=base, head=head, max_depth=max_depth, max_results=max_results, symbol=target_sym).as_dict()
1044
+
1045
+ def check_context_freshness(task: str = "", context_packet: dict[str, Any] | None = None) -> dict[str, Any]:
1046
+ """Validate whether a previously compiled context packet or task context is still VALID, PARTIALLY_STALE, or STALE. Use when checking if cached context is valid after edits. Does not invalidate context when only unrelated files change."""
1047
+ from codegraph.context_freshness import (
1048
+ check_context_freshness as impl_check_context_freshness,
1049
+ )
1050
+ with indexer.session() as con:
1051
+ target_packet = context_packet
1052
+ if target_packet is None and task:
1053
+ from codegraph.context import get_context as ctx_compile
1054
+ target_packet = ctx_compile(con, indexer.repository, task, mode="FAST").model_dump()
1055
+ if target_packet is None:
1056
+ return {
1057
+ "status": "UNKNOWN",
1058
+ "reason": "Neither context_packet nor task prompt was provided.",
1059
+ "recommended_action": "RECOMPILE_CONTEXT",
1060
+ }
1061
+ return impl_check_context_freshness(target_packet, indexer.repository, con=con).as_dict()
1062
+
1063
+ def trace_symbol_history(symbol: str, path: str = "", max_commits: int = 15) -> dict[str, Any]:
1064
+ """Trace deterministic symbol evolution across Git history: introduced, modified, moved across files, renamed, or deleted. Use when discovering when a symbol was added, moved, or renamed. Does not guess ambiguous renames without evidence."""
1065
+ from codegraph.symbol_history import trace_symbol_history as impl_trace_symbol_history
1066
+ with indexer.session() as con:
1067
+ return impl_trace_symbol_history(indexer.repository, symbol=symbol, path=path or None, con=con, max_commits=max_commits).as_dict()
1068
+
1069
+ def detect_semantic_conflicts(base_branch: str = "main", head_branch: str = "HEAD") -> dict[str, Any]:
1070
+ """Detect semantic and contract conflicts between Git branches (calling deleted symbols, broken call signatures, or missing route handlers). Use when verifying whether branches can be safely merged without semantic breakage. Does not report false conflicts when branches are compatible."""
1071
+ from codegraph.semantic_conflicts import (
1072
+ detect_semantic_conflicts as impl_detect_semantic_conflicts,
1073
+ )
1074
+ with indexer.session() as con:
1075
+ return impl_detect_semantic_conflicts(indexer.repository, base_branch=base_branch, head_branch=head_branch, con=con).as_dict()
1076
+
1077
+ def safe_rename(
1078
+ target: str,
1079
+ new_name: str,
1080
+ dry_run: bool = True,
1081
+ force_uncertain: bool = False,
1082
+ ) -> dict[str, Any]:
1083
+ """Perform a deterministic, syntax-validated, transaction-safe AST rename of a symbol across the repository.
1084
+ Follows the core invariant: DISCOVER -> PLAN -> PREVIEW -> VALIDATE -> COMMIT -> REINDEX -> VERIFY.
1085
+ When dry_run=True, performs zero disk mutation and returns preview diffs, risk assessment, affected tests, and affected routes.
1086
+ When dry_run=False, validates all preconditions and syntax in memory, commits atomically using sibling temp files and os.replace, persists a rollback manifest, and triggers an incremental index update.
1087
+ If risk != LOW (due to UNKNOWN/POSSIBLE/AMBIGUOUS references), the rename is BLOCKED unless force_uncertain=True."""
1088
+ from codegraph.refactor.renamer import execute_safe_rename
1089
+
1090
+ with indexer.session() as con:
1091
+ result = execute_safe_rename(
1092
+ repository=indexer.repository,
1093
+ con=con,
1094
+ target=target,
1095
+ new_name=new_name,
1096
+ dry_run=dry_run,
1097
+ force_uncertain=force_uncertain,
1098
+ )
1099
+ return result.to_dict()
1100
+
1101
+ def rollback_refactor(
1102
+ transaction_id: str,
1103
+ ) -> dict[str, Any]:
1104
+ """Atomically rollback an applied refactoring transaction using its rollback manifest ID.
1105
+ Verifies that files on disk have not been modified externally since the refactor before restoring original contents."""
1106
+ from codegraph.refactor.models import RefactorStatus
1107
+ from codegraph.refactor.transactions import rollback_refactor as do_rollback
1108
+
1109
+ result = do_rollback(
1110
+ repository=indexer.repository,
1111
+ transaction_id=transaction_id,
1112
+ )
1113
+ if result.status == RefactorStatus.ROLLED_BACK:
1114
+ try:
1115
+ indexer.index()
1116
+ except Exception:
1117
+ pass
1118
+ return result.to_dict()
1119
+
1120
+ core_tools: dict[str, Any] = {
1121
+ "resolve_symbol": resolve_symbol,
1122
+ "search_symbols": search_symbols,
1123
+ "get_symbol": get_symbol_interrogation_wrapper,
1124
+ "get_file": get_file,
1125
+ "get_references": get_references,
1126
+ "get_callers": get_callers,
1127
+ "get_callees": get_callees,
1128
+ "trace_path": trace_path,
1129
+ "get_imports": get_imports,
1130
+ "get_dependents": get_dependents,
1131
+ "list_routes": list_routes,
1132
+ "get_architecture": get_architecture,
1133
+ "get_git_impact": get_git_impact,
1134
+ }
1135
+
1136
+ git_intelligence_tools: dict[str, Any] = {
1137
+ "get_git_state": get_git_state,
1138
+ "compare_git": compare_git,
1139
+ "get_change_impact": get_change_impact,
1140
+ "check_context_freshness": check_context_freshness,
1141
+ "trace_symbol_history": trace_symbol_history,
1142
+ "detect_semantic_conflicts": detect_semantic_conflicts,
1143
+ }
1144
+
1145
+ refactor_tools: dict[str, Any] = {
1146
+ "safe_rename": safe_rename,
1147
+ "rollback_refactor": rollback_refactor,
1148
+ }
1149
+
1150
+ # Map of all available tools
1151
+ all_tools: dict[str, Any] = {
1152
+ **core_tools,
1153
+ **git_intelligence_tools,
1154
+ **refactor_tools,
1155
+ "search_code": search_code,
1156
+ "read_file": read_file,
1157
+ "find_symbol": find_symbol,
1158
+ "find_references": find_references,
1159
+ "find_callers": find_callers,
1160
+ "find_callees": find_callees,
1161
+ "get_call_graph": get_call_graph,
1162
+ "get_dependency_graph": get_dependency_graph,
1163
+ "find_related_tests": find_related_tests,
1164
+ "find_tests": find_tests,
1165
+ "find_routes": find_routes,
1166
+ "analyze_impact": analyze_impact,
1167
+ "compile_task": compile_task,
1168
+ "plan_retrieval": plan_retrieval,
1169
+ "get_context": get_context,
1170
+ "get_recent_changes": get_recent_changes,
1171
+ "get_file_history": get_file_history,
1172
+ "analyze_change_impact": analyze_change_impact,
1173
+ "get_repository_status": get_repository_status,
1174
+ "trace_call": trace_call,
1175
+ "trace_flow": trace_flow,
1176
+ "get_project_structure": get_project_structure,
1177
+ "get_dependencies": get_dependencies,
1178
+ "get_file_symbols": get_file_symbols,
1179
+ "get_graph": get_graph,
1180
+ "search_memory": search_memory,
1181
+ "get_evidence": get_evidence,
1182
+ "verify_evidence": verify_evidence,
1183
+ "get_resource_status": get_resource_status,
1184
+ "find_db_tables": find_db_tables,
1185
+ "find_db_columns": find_db_columns,
1186
+ "find_db_models": find_db_models,
1187
+ "find_db_queries": find_db_queries,
1188
+ "find_db_callers": find_db_callers,
1189
+ "find_db_writers": find_db_writers,
1190
+ "find_db_readers": find_db_readers,
1191
+ "find_db_relationships": find_db_relationships,
1192
+ "get_db_table": get_db_table,
1193
+ "get_db_schema": get_db_schema,
1194
+ "get_db_impact": get_db_impact,
1195
+ "ingest_runtime_traces": ingest_runtime_traces,
1196
+ "get_runtime_trace": get_runtime_trace,
1197
+ "reconcile_static_runtime": reconcile_static_runtime,
1198
+ "get_live_services": get_live_services,
1199
+ "check_db_drift": check_db_drift,
1200
+ "check_route_schema_drift": check_route_schema_drift,
1201
+ "get_distributed_trace": get_distributed_trace,
1202
+ "check_api_drift": check_api_drift,
1203
+ "get_monorepo_packages": get_monorepo_packages,
1204
+ }
1205
+
1206
+ agent_names = {
1207
+ # DISCOVERY
1208
+ "find_symbol",
1209
+ "search_code",
1210
+ "find_references",
1211
+ "find_callers",
1212
+ "find_callees",
1213
+ "find_tests",
1214
+ "find_routes",
1215
+ # DETAILS
1216
+ "get_symbol",
1217
+ "get_file",
1218
+ "get_context",
1219
+ "get_architecture",
1220
+ "get_git_impact",
1221
+ # GRAPH
1222
+ "trace_path",
1223
+ "trace_flow",
1224
+ }
1225
+ core_names = set(core_tools.keys())
1226
+ graph_names = core_names | {
1227
+ "get_context",
1228
+ "get_call_graph",
1229
+ "find_related_tests",
1230
+ "analyze_impact",
1231
+ }
1232
+ minimal_names = core_names | {
1233
+ "search_code",
1234
+ "read_file",
1235
+ "find_symbol",
1236
+ "compile_task",
1237
+ "get_context",
1238
+ "get_repository_status",
1239
+ "verify_evidence",
1240
+ "get_resource_status",
1241
+ }
1242
+ developer_names = minimal_names | {
1243
+ "find_references",
1244
+ "find_callers",
1245
+ "find_callees",
1246
+ "get_call_graph",
1247
+ "find_related_tests",
1248
+ "analyze_impact",
1249
+ "get_evidence",
1250
+ "trace_call",
1251
+ "get_graph",
1252
+ "plan_retrieval",
1253
+ "get_recent_changes",
1254
+ "get_file_history",
1255
+ "analyze_change_impact",
1256
+ }
1257
+
1258
+ if profile in ("agent", "default"):
1259
+ selected_names = agent_names
1260
+ elif profile == "core":
1261
+ selected_names = core_names
1262
+ elif profile == "graph":
1263
+ selected_names = graph_names
1264
+ elif profile == "minimal":
1265
+ selected_names = minimal_names
1266
+ elif profile == "developer":
1267
+ selected_names = developer_names
1268
+ else:
1269
+ selected_names = set(all_tools.keys())
1270
+
1271
+ _last_db_sig: list[tuple[int, int, int, int] | None] = [None]
1272
+
1273
+ def _compute_db_sig() -> tuple[int, int, int, int] | None:
1274
+ db_file = indexer.db_path
1275
+ if not db_file.exists():
1276
+ return None
1277
+ try:
1278
+ st = db_file.stat()
1279
+ wal_file = Path(f"{db_file}-wal")
1280
+ wal_mtime = wal_file.stat().st_mtime_ns if wal_file.exists() else 0
1281
+ return (st.st_ino, st.st_mtime_ns, st.st_size, wal_mtime)
1282
+ except OSError:
1283
+ return None
1284
+
1285
+ _last_db_sig[0] = _compute_db_sig()
1286
+
1287
+ def _ensure_hot_reloaded() -> bool:
1288
+ """Detect if `.codegraph.sqlite3` was created, replaced, or updated on disk (e.g. via `codegraph init`)."""
1289
+ cur_sig = _compute_db_sig()
1290
+ if cur_sig != _last_db_sig[0]:
1291
+ _last_db_sig[0] = cur_sig
1292
+ from codegraph.resources.cache import get_graph_cache, get_parse_cache
1293
+
1294
+ get_graph_cache().clear()
1295
+ get_parse_cache().clear()
1296
+ indexer.reset_pool()
1297
+ return True
1298
+ return False
1299
+
1300
+ for tool_name in sorted(selected_names):
1301
+ target_fn: Any = all_tools[tool_name]
1302
+
1303
+ def _make_guarded(fn: Any) -> Any:
1304
+ @functools.wraps(fn)
1305
+ def _guarded(*args: Any, **kwargs: Any) -> Any:
1306
+ _ensure_hot_reloaded()
1307
+ with indexer.governor.task_scope(TaskPriority.INTERACTIVE_HIGH):
1308
+ return fn(*args, **kwargs)
1309
+
1310
+ return _guarded
1311
+
1312
+ app.tool(name=tool_name)(_make_guarded(target_fn))
1313
+
1314
+ # FastMCP Event Loop Offload:
1315
+ # Ensure synchronous tools execute in worker threads so MCP asyncio loop is never blocked
1316
+ orig_call_tool = app._tool_manager.call_tool
1317
+
1318
+ async def _concurrent_call_tool(
1319
+ name: str,
1320
+ arguments: dict[str, Any],
1321
+ context: Any = None,
1322
+ convert_result: bool = False,
1323
+ ) -> Any:
1324
+ tool = app._tool_manager.get_tool(name)
1325
+ if tool is not None and not tool.is_async:
1326
+ def _invoke_in_thread() -> Any:
1327
+ return asyncio.run(
1328
+ orig_call_tool(
1329
+ name,
1330
+ arguments,
1331
+ context=context,
1332
+ convert_result=convert_result,
1333
+ )
1334
+ )
1335
+
1336
+ return await anyio.to_thread.run_sync(_invoke_in_thread)
1337
+
1338
+ return await orig_call_tool(
1339
+ name,
1340
+ arguments,
1341
+ context=context,
1342
+ convert_result=convert_result,
1343
+ )
1344
+
1345
+ app._tool_manager.call_tool = _concurrent_call_tool # type: ignore[method-assign]
1346
+
1347
+
1348
+ # -----------------------------------------------------------------------
1349
+ # MCP Resources
1350
+ # -----------------------------------------------------------------------
1351
+
1352
+ @app.resource("codebase://capabilities")
1353
+ def resource_capabilities() -> str:
1354
+ """Machine-readable CodeGraph capability manifest, tool selection rules, and evidence semantics."""
1355
+ from codegraph.agent_capabilities import get_capability_manifest
1356
+
1357
+ return json.dumps(get_capability_manifest(), indent=2)
1358
+
1359
+ @app.resource("codebase://status")
1360
+ async def resource_status() -> str:
1361
+ """Repository index status, generation, and file counts."""
1362
+ def _get_status() -> str:
1363
+ with indexer.session() as con:
1364
+ report = check_freshness(indexer.repository, con)
1365
+ return json.dumps(
1366
+ {
1367
+ "repository": str(indexer.repository),
1368
+ "generation": report.index_generation,
1369
+ "freshness": report.status.value,
1370
+ "files": con.execute("SELECT count(*) FROM files").fetchone()[0],
1371
+ "symbols": con.execute("SELECT count(*) FROM symbols").fetchone()[0],
1372
+ },
1373
+ indent=2,
1374
+ )
1375
+
1376
+ return await anyio.to_thread.run_sync(_get_status)
1377
+
1378
+ @app.resource("codebase://architecture")
1379
+ async def resource_architecture() -> str:
1380
+ """High-level architectural overview."""
1381
+ def _get_arch() -> str:
1382
+ with indexer.session() as con:
1383
+ arch = arch_get_architecture(con, indexer.repository)
1384
+ return json.dumps(arch, indent=2)
1385
+
1386
+ return await anyio.to_thread.run_sync(_get_arch)
1387
+
1388
+ @app.resource("codebase://modules")
1389
+ async def resource_modules() -> str:
1390
+ """List of indexed repository module paths."""
1391
+ def _get_modules() -> str:
1392
+ with indexer.session() as con:
1393
+ rows = con.execute("SELECT path FROM files ORDER BY path LIMIT 500").fetchall()
1394
+ return "\n".join(r[0] for r in rows)
1395
+
1396
+ return await anyio.to_thread.run_sync(_get_modules)
1397
+
1398
+ @app.resource("codebase://health")
1399
+ async def resource_health() -> str:
1400
+ """Database health and integrity report."""
1401
+ def _get_health() -> str:
1402
+ with indexer.session() as con:
1403
+ health = check_database_health(con)
1404
+ return json.dumps(health, indent=2)
1405
+
1406
+ return await anyio.to_thread.run_sync(_get_health)
1407
+
1408
+ @app.resource("codebase://resources")
1409
+ def resource_resources() -> str:
1410
+ """Resource governor state, memory bounds, pressure, and activity mode."""
1411
+ return json.dumps(indexer.governor.get_state().as_dict(), indent=2)
1412
+
1413
+
1414
+ # -----------------------------------------------------------------------
1415
+ # MCP Prompts
1416
+ # -----------------------------------------------------------------------
1417
+
1418
+ @app.prompt()
1419
+ def understand_repository() -> str:
1420
+ """Guide the AI coding agent to understand repository structure and components."""
1421
+ return (
1422
+ "1. Read `codebase://status` and `get_architecture()` to inspect the macro system layout.\n"
1423
+ "2. Identify primary entry points, endpoints, and data models.\n"
1424
+ "3. Compile a TaskSpec with intent='UNDERSTAND' and pass to `get_context()`."
1425
+ )
1426
+
1427
+ @app.prompt()
1428
+ def trace_request(endpoint: str = "") -> str:
1429
+ """Guide the AI coding agent to trace an HTTP route or entrypoint."""
1430
+ return (
1431
+ f"Trace route or endpoint: {endpoint}\n"
1432
+ "1. Call `compile_task` with intent='TRACE' and target set to the endpoint.\n"
1433
+ "2. Call `get_context` to receive the full call-path from route to data layer.\n"
1434
+ "3. Inspect callers, callees, and verified evidence."
1435
+ )
1436
+
1437
+ @app.prompt()
1438
+ def debug_issue(symptom: str = "") -> str:
1439
+ """Guide the AI coding agent to debug an issue using verified facts and recent changes."""
1440
+ return (
1441
+ f"Investigate symptom: {symptom}\n"
1442
+ "1. Call `compile_task` with intent='DEBUG' and relevant targets.\n"
1443
+ "2. Call `get_context` with token budget to retrieve entry points, recent git diffs, and related tests.\n"
1444
+ "3. Formulate hypotheses strictly based on cited source evidence."
1445
+ )
1446
+
1447
+ @app.prompt()
1448
+ def prepare_change(target: str = "") -> str:
1449
+ """Guide the AI coding agent to evaluate blast radius before changing code."""
1450
+ return (
1451
+ f"Target symbol or file: {target}\n"
1452
+ "1. Call `analyze_impact` to compute callers and affected modules.\n"
1453
+ "2. Call `find_related_tests` to identify regression verification targets.\n"
1454
+ "3. Call `get_context` with intent='CHANGE'."
1455
+ )
1456
+
1457
+ @app.prompt()
1458
+ def analyze_impact_prompt(target: str = "") -> str:
1459
+ """Guide the AI coding agent to compute reverse dependency blast radius."""
1460
+ return (
1461
+ f"Target: {target}\n"
1462
+ "1. Call `analyze_impact` for reverse references, callers, and dependents.\n"
1463
+ "2. Verify related tests and routes."
1464
+ )
1465
+
1466
+ @app.prompt()
1467
+ def review_change(since: str = "HEAD~1") -> str:
1468
+ """Guide the AI coding agent to review recent changes against affected tests."""
1469
+ return (
1470
+ f"Diff range: {since}..HEAD\n"
1471
+ "1. Call `analyze_change_impact` to identify changed symbols and callers.\n"
1472
+ "2. Run `get_context` with intent='REVIEW'."
1473
+ )
1474
+
1475
+ app._codegraph_indexer = indexer # type: ignore[attr-defined]
1476
+ return app