codegraph-engine 2.1.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 (62) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/architecture.py +328 -0
  4. codegraph/audit.py +106 -0
  5. codegraph/cache.py +95 -0
  6. codegraph/cli.py +854 -0
  7. codegraph/config.py +43 -0
  8. codegraph/constraints.py +238 -0
  9. codegraph/context.py +1228 -0
  10. codegraph/epistemic.py +90 -0
  11. codegraph/errors.py +275 -0
  12. codegraph/evidence/__init__.py +15 -0
  13. codegraph/evidence/citations.py +397 -0
  14. codegraph/frameworks.py +434 -0
  15. codegraph/freshness.py +295 -0
  16. codegraph/git.py +278 -0
  17. codegraph/graph/__init__.py +46 -0
  18. codegraph/graph/models.py +41 -0
  19. codegraph/graph/traversal.py +1291 -0
  20. codegraph/indexing/__init__.py +4 -0
  21. codegraph/indexing/classifier.py +274 -0
  22. codegraph/indexing/indexer.py +943 -0
  23. codegraph/indexing/models.py +338 -0
  24. codegraph/indexing/parser.py +1240 -0
  25. codegraph/indexing/scanner.py +200 -0
  26. codegraph/indexing/test_framework.py +116 -0
  27. codegraph/interrogation.py +1582 -0
  28. codegraph/llm/__init__.py +3 -0
  29. codegraph/llm/base.py +15 -0
  30. codegraph/llm/context.py +20 -0
  31. codegraph/mcp/__init__.py +3 -0
  32. codegraph/mcp/server.py +736 -0
  33. codegraph/memory/__init__.py +3 -0
  34. codegraph/memory/store.py +46 -0
  35. codegraph/models.py +289 -0
  36. codegraph/observability.py +151 -0
  37. codegraph/optimizer.py +372 -0
  38. codegraph/planner.py +417 -0
  39. codegraph/py.typed +1 -0
  40. codegraph/query_expansion.py +199 -0
  41. codegraph/ranking.py +363 -0
  42. codegraph/resolver.py +843 -0
  43. codegraph/resources/__init__.py +45 -0
  44. codegraph/resources/cache.py +117 -0
  45. codegraph/resources/coalescer.py +83 -0
  46. codegraph/resources/debouncer.py +98 -0
  47. codegraph/resources/governor.py +232 -0
  48. codegraph/resources/policy.py +123 -0
  49. codegraph/retrieval_policy.py +220 -0
  50. codegraph/search/__init__.py +23 -0
  51. codegraph/search/hybrid.py +301 -0
  52. codegraph/search/semantic.py +28 -0
  53. codegraph/security/__init__.py +3 -0
  54. codegraph/security/paths.py +35 -0
  55. codegraph/target_resolver.py +348 -0
  56. codegraph/task.py +637 -0
  57. codegraph_engine-2.1.1.dist-info/METADATA +334 -0
  58. codegraph_engine-2.1.1.dist-info/RECORD +62 -0
  59. codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
  60. codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
  61. codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
  62. codegraph_engine-2.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,736 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from codegraph.architecture import get_architecture as arch_get_architecture
9
+ from codegraph.config import Settings
10
+ from codegraph.context import get_context as compile_context
11
+ from codegraph.errors import SecurityError
12
+ from codegraph.evidence import Evidence
13
+ from codegraph.evidence import verify_evidence as verify_evidence_fn
14
+ from codegraph.freshness import check_freshness
15
+ from codegraph.git import (
16
+ analyze_change_impact as git_analyze_change_impact,
17
+ )
18
+ from codegraph.git import (
19
+ changed_files,
20
+ )
21
+ from codegraph.git import (
22
+ get_file_history as git_get_file_history,
23
+ )
24
+ from codegraph.graph import (
25
+ analyze_impact as graph_analyze_impact,
26
+ )
27
+ from codegraph.graph import (
28
+ definition_edges,
29
+ import_edges,
30
+ )
31
+ from codegraph.graph import (
32
+ find_callees as graph_find_callees,
33
+ )
34
+ from codegraph.graph import (
35
+ find_callers as graph_find_callers,
36
+ )
37
+ from codegraph.graph import (
38
+ find_related_tests as graph_find_related_tests,
39
+ )
40
+ from codegraph.graph import (
41
+ get_call_graph as graph_get_call_graph,
42
+ )
43
+ from codegraph.graph import (
44
+ get_symbol as graph_get_symbol,
45
+ )
46
+ from codegraph.graph import (
47
+ trace_call as graph_trace_call,
48
+ )
49
+ from codegraph.indexing import Indexer
50
+ from codegraph.indexing.indexer import check_database_health
51
+ from codegraph.interrogation import (
52
+ get_architecture as interrogation_get_architecture,
53
+ )
54
+ from codegraph.interrogation import (
55
+ get_callees as interrogation_get_callees,
56
+ )
57
+ from codegraph.interrogation import (
58
+ get_callers as interrogation_get_callers,
59
+ )
60
+ from codegraph.interrogation import (
61
+ get_dependents as interrogation_get_dependents,
62
+ )
63
+ from codegraph.interrogation import (
64
+ get_file as interrogation_get_file,
65
+ )
66
+ from codegraph.interrogation import (
67
+ get_git_impact as interrogation_get_git_impact,
68
+ )
69
+ from codegraph.interrogation import (
70
+ get_imports as interrogation_get_imports,
71
+ )
72
+ from codegraph.interrogation import (
73
+ get_references as interrogation_get_references,
74
+ )
75
+ from codegraph.interrogation import (
76
+ get_symbol as interrogation_get_symbol,
77
+ )
78
+ from codegraph.interrogation import (
79
+ list_routes as interrogation_list_routes,
80
+ )
81
+ from codegraph.interrogation import (
82
+ resolve_symbol as interrogation_resolve_symbol,
83
+ )
84
+ from codegraph.interrogation import (
85
+ search_symbols as interrogation_search_symbols,
86
+ )
87
+ from codegraph.interrogation import (
88
+ trace_path as interrogation_trace_path,
89
+ )
90
+ from codegraph.memory import MemoryStore
91
+ from codegraph.planner import build_retrieval_plan
92
+ from codegraph.resources import TaskPriority
93
+ from codegraph.search import search
94
+ from codegraph.security import is_sensitive, safe_path
95
+ from codegraph.task import normalize_task_spec
96
+
97
+
98
+ def create_server(
99
+ repository: Path,
100
+ settings: Settings | None = None,
101
+ profile: str = "full",
102
+ ) -> Any:
103
+ """Build an MCP FastMCP server lazily so normal CLI use needs no MCP dependency."""
104
+ try:
105
+ from mcp.server.fastmcp import FastMCP
106
+ except ImportError as exc:
107
+ raise RuntimeError("MCP support requires: pip install 'codegraph-mcp[mcp]'") from exc
108
+
109
+ indexer = Indexer(repository, settings)
110
+ memory = MemoryStore(indexer.db_path)
111
+ app = FastMCP("CodeGraph MCP")
112
+
113
+ # -----------------------------------------------------------------------
114
+ # Tool Handlers
115
+ # -----------------------------------------------------------------------
116
+
117
+ def search_code(query: str, top_k: int = 10) -> list[dict[str, object]]:
118
+ """Find relevant code snippets and symbols across indexed files using lexical search. (Do NOT use for import or dependency questions; use get_imports or get_dependents instead)."""
119
+ if not query.strip() or not 1 <= top_k <= 100:
120
+ raise ValueError("query must be non-empty and top_k must be 1..100")
121
+ with indexer.session() as con:
122
+ return [item.as_dict() for item in search(con, query, top_k)]
123
+
124
+ def read_file(path: str, start_line: int = 1, end_line: int = 200) -> dict[str, object]:
125
+ """Read a bounded, non-sensitive repository file by repository-relative path."""
126
+ if start_line < 1 or end_line < start_line or end_line - start_line > 500:
127
+ raise ValueError("line range must be 1..500 lines")
128
+ resolved = safe_path(indexer.repository, path)
129
+ relative = resolved.relative_to(indexer.repository)
130
+ if is_sensitive(relative):
131
+ raise SecurityError("sensitive files cannot be read")
132
+ lines = resolved.read_text(encoding="utf-8", errors="replace").splitlines()
133
+ return {
134
+ "file": relative.as_posix(),
135
+ "start_line": start_line,
136
+ "end_line": min(end_line, len(lines)),
137
+ "content": "\n".join(lines[start_line - 1 : end_line]),
138
+ }
139
+
140
+ def find_symbol(name: str) -> list[dict[str, object]]:
141
+ """Find function, method, or class definitions by name."""
142
+ if not name.strip():
143
+ raise ValueError("symbol name must be non-empty")
144
+ with indexer.session() as con:
145
+ rows = con.execute(
146
+ "SELECT qualified_name AS symbol,kind,path AS file,start_line,end_line "
147
+ "FROM symbols WHERE name=? OR qualified_name=? ORDER BY path,start_line LIMIT 100",
148
+ (name, name),
149
+ )
150
+ return [dict(row) for row in rows]
151
+
152
+ def get_symbol(symbol: str) -> dict[str, object]:
153
+ """Get authoritative details for a single symbol by name or canonical ID."""
154
+ if not symbol.strip():
155
+ raise ValueError("symbol must be non-empty")
156
+ with indexer.session() as con:
157
+ sym = graph_get_symbol(con, symbol)
158
+ if not sym:
159
+ return {"error": f"Symbol not found: {symbol}"}
160
+ return sym.as_dict()
161
+
162
+ def find_references(name: str) -> list[dict[str, object]]:
163
+ """Find textual references; results are explicitly not guaranteed semantic call edges."""
164
+ if not name.strip():
165
+ raise ValueError("reference name must be non-empty")
166
+ with indexer.session() as con:
167
+ return [
168
+ dict(row)
169
+ for row in con.execute(
170
+ "SELECT path AS file,symbol,start_line,end_line FROM chunks WHERE content LIKE ? ORDER BY path,start_line LIMIT 100",
171
+ (f"%{name}%",),
172
+ )
173
+ ]
174
+
175
+ def find_callers(symbol: str, max_results: int = 20) -> list[dict[str, object]]:
176
+ """Find functions and methods that call the specified symbol."""
177
+ if not symbol.strip():
178
+ raise ValueError("symbol must be non-empty")
179
+ with indexer.session() as con:
180
+ return [dict(r) for r in graph_find_callers(con, symbol, max_results=max_results)]
181
+
182
+ def find_callees(symbol: str, max_results: int = 20) -> list[dict[str, object]]:
183
+ """Find functions and methods called by the specified symbol."""
184
+ if not symbol.strip():
185
+ raise ValueError("symbol must be non-empty")
186
+ with indexer.session() as con:
187
+ return [dict(r) for r in graph_find_callees(con, symbol, max_results=max_results)]
188
+
189
+ def get_call_graph(symbol: str, depth: int = 2, max_results: int = 100) -> list[dict[str, object]]:
190
+ """Compute the call graph rooted at the given symbol."""
191
+ if not symbol.strip():
192
+ raise ValueError("symbol must be non-empty")
193
+ with indexer.session() as con:
194
+ return [dict(e) for e in graph_get_call_graph(con, symbol=symbol, depth=depth, max_results=max_results)]
195
+
196
+ def get_dependency_graph(file_path: str | None = None) -> list[dict[str, object]]:
197
+ """List module import and dependency graph edges."""
198
+ with indexer.session() as con:
199
+ if file_path:
200
+ rows = con.execute(
201
+ "SELECT source, target, relationship, confidence, evidence FROM graph_edges WHERE relationship='IMPORTS' AND (source=? OR file=?) ORDER BY source",
202
+ (file_path, file_path),
203
+ ).fetchall()
204
+ else:
205
+ rows = con.execute(
206
+ "SELECT source, target, relationship, confidence, evidence FROM graph_edges WHERE relationship='IMPORTS' ORDER BY source LIMIT 500"
207
+ ).fetchall()
208
+ return [dict(r) for r in rows]
209
+
210
+ def find_related_tests(symbol: str, max_results: int = 10) -> list[dict[str, object]]:
211
+ """Find test files and test functions related to a symbol."""
212
+ if not symbol.strip():
213
+ raise ValueError("symbol must be non-empty")
214
+ with indexer.session() as con:
215
+ return [dict(r) for r in graph_find_related_tests(con, symbol, max_results=max_results)]
216
+
217
+ def analyze_impact(symbol: str, max_depth: int = 3) -> dict[str, object]:
218
+ """Analyze downstream impact and blast radius if a symbol is modified."""
219
+ if not symbol.strip():
220
+ raise ValueError("symbol must be non-empty")
221
+ with indexer.session() as con:
222
+ return graph_analyze_impact(con, symbol, max_depth=max_depth)
223
+
224
+ def compile_task(
225
+ task: dict[str, Any] | str,
226
+ max_tokens: int = 20_000,
227
+ resource_mode: str = "BALANCED",
228
+ ) -> dict[str, object]:
229
+ """Normalize a task, ground targets against repository symbols, detect ambiguities, and build a RetrievalPlan."""
230
+ with indexer.session() as con:
231
+ spec, ambiguities = normalize_task_spec(task, con)
232
+ plan = build_retrieval_plan(
233
+ spec,
234
+ ambiguities,
235
+ con,
236
+ token_budget=max_tokens,
237
+ resource_mode=resource_mode,
238
+ )
239
+ entry_points: list[str] = []
240
+ for t in spec.priority_targets or spec.targets:
241
+ r_rows = con.execute(
242
+ "SELECT endpoint_id, handler_name FROM framework_routes WHERE route_path LIKE ? OR endpoint_id LIKE ?",
243
+ (f"%{t}%", f"%{t}%"),
244
+ ).fetchall()
245
+ for r in r_rows:
246
+ entry_points.append(r["endpoint_id"] or r["handler_name"])
247
+ amb_list = [a.as_dict() for a in ambiguities]
248
+ return {
249
+ "task_spec": spec.as_dict(),
250
+ "ambiguity": amb_list[0] if amb_list else None,
251
+ "ambiguities": amb_list,
252
+ "entry_points": entry_points,
253
+ "candidate_entry_points": entry_points,
254
+ "retrieval_plan": plan.as_dict(),
255
+ "recommended_next_step": "get_context",
256
+ }
257
+
258
+ def plan_retrieval(
259
+ task: dict[str, Any] | str,
260
+ max_tokens: int = 20_000,
261
+ resource_mode: str = "BALANCED",
262
+ ) -> dict[str, object]:
263
+ """Construct a deterministic RetrievalPlan for a task without executing retrieval."""
264
+ with indexer.session() as con:
265
+ spec, ambiguities = normalize_task_spec(task, con)
266
+ plan = build_retrieval_plan(
267
+ spec,
268
+ ambiguities,
269
+ con,
270
+ token_budget=max_tokens,
271
+ resource_mode=resource_mode,
272
+ )
273
+ return plan.as_dict()
274
+
275
+ def get_context(
276
+ task: dict[str, Any] | str,
277
+ intent: str | None = None,
278
+ max_tokens: int = 20000,
279
+ top_k: int = 15,
280
+ plan: dict[str, Any] | None = None,
281
+ mode: str = "BALANCED",
282
+ explain: bool = False,
283
+ resource_mode: str | None = None,
284
+ ) -> dict[str, object]:
285
+ """Compile a complete, ranked, verified context packet for an AI agent task."""
286
+ if isinstance(task, str) and not task.strip():
287
+ raise ValueError("task must be non-empty")
288
+ effective_mode = resource_mode or mode
289
+ with indexer.session() as con:
290
+ packet = compile_context(
291
+ con,
292
+ indexer.repository,
293
+ task=task,
294
+ intent=intent,
295
+ max_tokens=max_tokens,
296
+ top_k=top_k,
297
+ plan=plan,
298
+ mode=effective_mode,
299
+ explain=explain,
300
+ )
301
+ return packet.as_dict()
302
+
303
+ def get_recent_changes(
304
+ since: str = "HEAD~10",
305
+ until: str = "HEAD",
306
+ ) -> list[dict[str, str]]:
307
+ """Return files changed between two commits or refs (read-only, sandboxed)."""
308
+ diffs = changed_files(indexer.repository, since=since, until=until)
309
+ return [d.as_dict() for d in diffs]
310
+
311
+ def get_file_history(
312
+ path: str,
313
+ n: int = 10,
314
+ ) -> list[dict[str, str]]:
315
+ """Return recent commits touching the specified repository file."""
316
+ return git_get_file_history(indexer.repository, path, n=n)
317
+
318
+ def analyze_change_impact(
319
+ since: str = "HEAD~1",
320
+ until: str = "HEAD",
321
+ ) -> dict[str, object]:
322
+ """Analyze downstream callers and tests affected by changes between since and until."""
323
+ with indexer.session() as con:
324
+ return git_analyze_change_impact(con, indexer.repository, since=since, until=until)
325
+
326
+ def get_repository_status() -> dict[str, object]:
327
+ """Get repository indexing status, freshness, symbol counts, and health."""
328
+ with indexer.session() as con:
329
+ report = check_freshness(indexer.repository, con)
330
+ file_count = con.execute("SELECT count(*) FROM files").fetchone()[0]
331
+ symbol_count = con.execute("SELECT count(*) FROM symbols").fetchone()[0]
332
+ edge_count = con.execute("SELECT count(*) FROM graph_edges").fetchone()[0]
333
+ route_count = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
334
+ return {
335
+ "repository": str(indexer.repository),
336
+ "generation": report.index_generation,
337
+ "freshness": report.status.value,
338
+ "freshness_detail": report.detail,
339
+ "files_indexed": file_count,
340
+ "symbols_indexed": symbol_count,
341
+ "graph_edges": edge_count,
342
+ "framework_routes": route_count,
343
+ "modified_files": report.modified_files,
344
+ "deleted_files": report.deleted_files,
345
+ "parse_failed_files": report.parse_failed_files,
346
+ }
347
+
348
+ def trace_call(
349
+ symbol: str,
350
+ depth: int = 2,
351
+ callers: bool = True,
352
+ callees: bool = False,
353
+ both: bool = False,
354
+ ) -> list[dict[str, object]]:
355
+ """Trace known definition, callers, and callees with explicit confidence and relationship labels."""
356
+ with indexer.session() as con:
357
+ return graph_trace_call(con, symbol, max_depth=depth, callers=callers, callees=callees, both=both)
358
+
359
+ def get_project_structure() -> list[str]:
360
+ """List indexed source paths only."""
361
+ with indexer.session() as con:
362
+ return [r[0] for r in con.execute("SELECT path FROM files ORDER BY path LIMIT 500")]
363
+
364
+ def get_dependencies() -> list[dict[str, str]]:
365
+ """List source imports as static evidence, without resolving external packages."""
366
+ with indexer.session() as con:
367
+ return [
368
+ dict(r)
369
+ for r in con.execute(
370
+ "SELECT source_path,imported FROM imports ORDER BY source_path,imported LIMIT 500"
371
+ )
372
+ ]
373
+
374
+ def get_file_symbols(path: str) -> list[dict[str, object]]:
375
+ """List extracted symbols in one repository-relative file."""
376
+ resolved = safe_path(indexer.repository, path)
377
+ relative = resolved.relative_to(indexer.repository).as_posix()
378
+ with indexer.session() as con:
379
+ rows = con.execute(
380
+ "SELECT qualified_name AS symbol,kind,start_line,end_line FROM symbols "
381
+ "WHERE path=? ORDER BY start_line LIMIT 200",
382
+ (relative,),
383
+ )
384
+ return [dict(row) for row in rows]
385
+
386
+ def get_graph(limit: int = 200) -> list[dict[str, object]]:
387
+ """Return bounded parser-confirmed file/symbol/import graph edges with evidence metadata."""
388
+ if not 1 <= limit <= 500:
389
+ raise ValueError("limit must be 1..500")
390
+ with indexer.session() as con:
391
+ edges = definition_edges(con, limit) + import_edges(con)
392
+ return [edge.as_dict() for edge in edges[:limit]]
393
+
394
+ def search_memory(query: str, limit: int = 20) -> list[dict[str, str]]:
395
+ """Search local repository memory. Memory never overrides current source evidence."""
396
+ if not 1 <= limit <= 100:
397
+ raise ValueError("limit must be 1..100")
398
+ return [{"key": key, "value": value} for key, value in memory.search(query, limit)]
399
+
400
+ def get_evidence(path: str, symbol: str | None = None) -> list[dict[str, object]]:
401
+ """Return source-derived evidence for a safe indexed file, optionally narrowed to one symbol."""
402
+ resolved = safe_path(indexer.repository, path)
403
+ relative = resolved.relative_to(indexer.repository)
404
+ if is_sensitive(relative):
405
+ raise SecurityError("sensitive files cannot be used as evidence")
406
+ with indexer.session() as con:
407
+ if symbol:
408
+ rows = con.execute(
409
+ "SELECT symbol,start_line,end_line,content FROM chunks WHERE path=? AND symbol=? LIMIT 20",
410
+ (relative.as_posix(), symbol),
411
+ )
412
+ else:
413
+ rows = con.execute(
414
+ "SELECT symbol,start_line,end_line,content FROM chunks WHERE path=? ORDER BY start_line LIMIT 50",
415
+ (relative.as_posix(),),
416
+ )
417
+ return [
418
+ Evidence(
419
+ relative.as_posix(),
420
+ row["start_line"],
421
+ row["end_line"],
422
+ row["symbol"],
423
+ row["content"][:900],
424
+ ).as_dict()
425
+ for row in rows
426
+ ]
427
+
428
+ def verify_evidence(
429
+ file_path: str,
430
+ start_line: int,
431
+ end_line: int,
432
+ expected_hash: str | None = None,
433
+ symbol: str | None = None,
434
+ ) -> dict[str, object]:
435
+ """Verify that a cited piece of evidence or code snippet actually exists, matches disk hash, and remains valid."""
436
+ ev = Evidence(
437
+ file=file_path,
438
+ start_line=start_line,
439
+ end_line=end_line,
440
+ symbol=symbol,
441
+ snippet="",
442
+ )
443
+ with indexer.session() as con:
444
+ res = verify_evidence_fn(con, indexer.repository, ev, expected_content_hash=expected_hash)
445
+ return res.as_dict()
446
+
447
+ def get_resource_status() -> dict[str, object]:
448
+ """Inspect current resource governor state, memory usage, pressure, and activity mode."""
449
+ return indexer.governor.get_state().as_dict()
450
+
451
+ # -----------------------------------------------------------------------
452
+ # Core Deterministic Interrogation Tools (13 Core Capabilities)
453
+ # -----------------------------------------------------------------------
454
+
455
+ def resolve_symbol(name: str) -> dict[str, Any]:
456
+ """Determine whether an exact/canonical symbol exists and return its location and identity."""
457
+ with indexer.session() as con:
458
+ return interrogation_resolve_symbol(con, indexer.repository, name)
459
+
460
+ def search_symbols(query: str, top_k: int = 20) -> dict[str, Any]:
461
+ """Search indexed repository symbols using an explicit search term. (Do NOT use for discovering module imports or dependencies; use get_imports or get_dependents instead)."""
462
+ with indexer.session() as con:
463
+ return interrogation_search_symbols(con, indexer.repository, query, top_k=top_k)
464
+
465
+ def get_symbol_interrogation_wrapper(canonical_id: str = "", symbol: str = "") -> dict[str, Any]:
466
+ """Return complete structured information for a known canonical symbol."""
467
+ sym_id = canonical_id or symbol
468
+ with indexer.session() as con:
469
+ return interrogation_get_symbol(con, indexer.repository, sym_id)
470
+
471
+ def get_file(path: str, include_content: bool = False) -> dict[str, Any]:
472
+ """Return the structural AST representation of an indexed file."""
473
+ with indexer.session() as con:
474
+ return interrogation_get_file(con, indexer.repository, path, include_content=include_content)
475
+
476
+ def get_references(canonical_id: str) -> dict[str, Any]:
477
+ """Return all known references to a canonical symbol with evidence."""
478
+ with indexer.session() as con:
479
+ return interrogation_get_references(con, indexer.repository, canonical_id)
480
+
481
+ def get_callers(canonical_id: str) -> dict[str, Any]:
482
+ """Return symbols that call the specified symbol with explicit evidence."""
483
+ with indexer.session() as con:
484
+ return interrogation_get_callers(con, indexer.repository, canonical_id)
485
+
486
+ def get_callees(canonical_id: str) -> dict[str, Any]:
487
+ """Return symbols called by the specified symbol with call-type classification."""
488
+ with indexer.session() as con:
489
+ return interrogation_get_callees(con, indexer.repository, canonical_id)
490
+
491
+ def trace_path(
492
+ from_symbol: str = "",
493
+ to_symbol: str = "",
494
+ start_symbol: str = "",
495
+ target_symbol: str = "",
496
+ max_depth: int = 5,
497
+ ) -> dict[str, Any]:
498
+ """Find a deterministic relationship path between two known symbols."""
499
+ src = from_symbol or start_symbol
500
+ tgt = to_symbol or target_symbol
501
+ with indexer.session() as con:
502
+ return interrogation_trace_path(con, indexer.repository, src, tgt, max_depth=max_depth)
503
+
504
+ def get_imports(file: str | None = None, canonical_id: str | None = None) -> dict[str, Any]:
505
+ """What this file/module imports: return parser-extracted imports and imported symbols for a file or canonical symbol."""
506
+ with indexer.session() as con:
507
+ return interrogation_get_imports(con, indexer.repository, file=file, canonical_id=canonical_id)
508
+
509
+ def get_dependents(canonical_id: str | None = None, file: str | None = None) -> dict[str, Any]:
510
+ """What imports or depends on this file/module: reverse dependency query returning files and symbols that import or depend on the target."""
511
+ with indexer.session() as con:
512
+ return interrogation_get_dependents(con, indexer.repository, canonical_id=canonical_id, file=file)
513
+
514
+ def list_routes(
515
+ framework: str | None = None,
516
+ method: str | None = None,
517
+ path: str | None = None,
518
+ ) -> dict[str, Any]:
519
+ """Expose application routes discovered from the repository."""
520
+ with indexer.session() as con:
521
+ return interrogation_list_routes(con, indexer.repository, framework=framework, method=method, path=path)
522
+
523
+ def get_architecture() -> dict[str, Any]:
524
+ """Return a structural overview of the repository."""
525
+ with indexer.session() as con:
526
+ return interrogation_get_architecture(con, indexer.repository)
527
+
528
+ def get_git_impact(base: str = "HEAD~1", head: str = "HEAD") -> dict[str, Any]:
529
+ """Determine code affected by Git changes between base and head."""
530
+ with indexer.session() as con:
531
+ return interrogation_get_git_impact(con, indexer.repository, base=base, head=head)
532
+
533
+ core_tools: dict[str, Any] = {
534
+ "resolve_symbol": resolve_symbol,
535
+ "search_symbols": search_symbols,
536
+ "get_symbol": get_symbol_interrogation_wrapper,
537
+ "get_file": get_file,
538
+ "get_references": get_references,
539
+ "get_callers": get_callers,
540
+ "get_callees": get_callees,
541
+ "trace_path": trace_path,
542
+ "get_imports": get_imports,
543
+ "get_dependents": get_dependents,
544
+ "list_routes": list_routes,
545
+ "get_architecture": get_architecture,
546
+ "get_git_impact": get_git_impact,
547
+ }
548
+
549
+ # Map of all available tools
550
+ all_tools: dict[str, Any] = {
551
+ **core_tools,
552
+ "search_code": search_code,
553
+ "read_file": read_file,
554
+ "find_symbol": find_symbol,
555
+ "find_references": find_references,
556
+ "find_callers": find_callers,
557
+ "find_callees": find_callees,
558
+ "get_call_graph": get_call_graph,
559
+ "get_dependency_graph": get_dependency_graph,
560
+ "find_related_tests": find_related_tests,
561
+ "analyze_impact": analyze_impact,
562
+ "compile_task": compile_task,
563
+ "plan_retrieval": plan_retrieval,
564
+ "get_context": get_context,
565
+ "get_recent_changes": get_recent_changes,
566
+ "get_file_history": get_file_history,
567
+ "analyze_change_impact": analyze_change_impact,
568
+ "get_repository_status": get_repository_status,
569
+ "trace_call": trace_call,
570
+ "get_project_structure": get_project_structure,
571
+ "get_dependencies": get_dependencies,
572
+ "get_file_symbols": get_file_symbols,
573
+ "get_graph": get_graph,
574
+ "search_memory": search_memory,
575
+ "get_evidence": get_evidence,
576
+ "verify_evidence": verify_evidence,
577
+ "get_resource_status": get_resource_status,
578
+ }
579
+
580
+ core_names = set(core_tools.keys())
581
+ minimal_names = core_names | {
582
+ "search_code",
583
+ "read_file",
584
+ "find_symbol",
585
+ "compile_task",
586
+ "get_context",
587
+ "get_repository_status",
588
+ "verify_evidence",
589
+ "get_resource_status",
590
+ }
591
+ developer_names = minimal_names | {
592
+ "find_references",
593
+ "find_callers",
594
+ "find_callees",
595
+ "get_call_graph",
596
+ "find_related_tests",
597
+ "analyze_impact",
598
+ "get_evidence",
599
+ "trace_call",
600
+ "get_graph",
601
+ "plan_retrieval",
602
+ "get_recent_changes",
603
+ "get_file_history",
604
+ "analyze_change_impact",
605
+ }
606
+
607
+ if profile == "core":
608
+ selected_names = core_names
609
+ elif profile == "minimal":
610
+ selected_names = minimal_names
611
+ elif profile == "developer":
612
+ selected_names = developer_names
613
+ else:
614
+ selected_names = set(all_tools.keys())
615
+
616
+ for tool_name in sorted(selected_names):
617
+ target_fn: Any = all_tools[tool_name]
618
+
619
+ def _make_guarded(fn: Any) -> Any:
620
+ @functools.wraps(fn)
621
+ def _guarded(*args: Any, **kwargs: Any) -> Any:
622
+ with indexer.governor.task_scope(TaskPriority.INTERACTIVE_HIGH):
623
+ return fn(*args, **kwargs)
624
+
625
+ return _guarded
626
+
627
+ app.tool(name=tool_name)(_make_guarded(target_fn))
628
+
629
+ # -----------------------------------------------------------------------
630
+ # MCP Resources
631
+ # -----------------------------------------------------------------------
632
+
633
+ @app.resource("codebase://status")
634
+ def resource_status() -> str:
635
+ """Repository index status, generation, and file counts."""
636
+ with indexer.session() as con:
637
+ report = check_freshness(indexer.repository, con)
638
+ return json.dumps(
639
+ {
640
+ "repository": str(indexer.repository),
641
+ "generation": report.index_generation,
642
+ "freshness": report.status.value,
643
+ "files": con.execute("SELECT count(*) FROM files").fetchone()[0],
644
+ "symbols": con.execute("SELECT count(*) FROM symbols").fetchone()[0],
645
+ },
646
+ indent=2,
647
+ )
648
+
649
+ @app.resource("codebase://architecture")
650
+ def resource_architecture() -> str:
651
+ """High-level architectural overview."""
652
+ with indexer.session() as con:
653
+ arch = arch_get_architecture(con, indexer.repository)
654
+ return json.dumps(arch, indent=2)
655
+
656
+ @app.resource("codebase://modules")
657
+ def resource_modules() -> str:
658
+ """List of indexed repository module paths."""
659
+ with indexer.session() as con:
660
+ rows = con.execute("SELECT path FROM files ORDER BY path LIMIT 500").fetchall()
661
+ return "\n".join(r[0] for r in rows)
662
+
663
+ @app.resource("codebase://health")
664
+ def resource_health() -> str:
665
+ """Database health and integrity report."""
666
+ with indexer.session() as con:
667
+ health = check_database_health(con)
668
+ return json.dumps(health, indent=2)
669
+
670
+ @app.resource("codebase://resources")
671
+ def resource_resources() -> str:
672
+ """Resource governor state, memory bounds, pressure, and activity mode."""
673
+ return json.dumps(indexer.governor.get_state().as_dict(), indent=2)
674
+
675
+ # -----------------------------------------------------------------------
676
+ # MCP Prompts
677
+ # -----------------------------------------------------------------------
678
+
679
+ @app.prompt()
680
+ def understand_repository() -> str:
681
+ """Guide the AI coding agent to understand repository structure and components."""
682
+ return (
683
+ "1. Read `codebase://status` and `get_architecture()` to inspect the macro system layout.\n"
684
+ "2. Identify primary entry points, endpoints, and data models.\n"
685
+ "3. Compile a TaskSpec with intent='UNDERSTAND' and pass to `get_context()`."
686
+ )
687
+
688
+ @app.prompt()
689
+ def trace_request(endpoint: str = "") -> str:
690
+ """Guide the AI coding agent to trace an HTTP route or entrypoint."""
691
+ return (
692
+ f"Trace route or endpoint: {endpoint}\n"
693
+ "1. Call `compile_task` with intent='TRACE' and target set to the endpoint.\n"
694
+ "2. Call `get_context` to receive the full call-path from route to data layer.\n"
695
+ "3. Inspect callers, callees, and verified evidence."
696
+ )
697
+
698
+ @app.prompt()
699
+ def debug_issue(symptom: str = "") -> str:
700
+ """Guide the AI coding agent to debug an issue using verified facts and recent changes."""
701
+ return (
702
+ f"Investigate symptom: {symptom}\n"
703
+ "1. Call `compile_task` with intent='DEBUG' and relevant targets.\n"
704
+ "2. Call `get_context` with token budget to retrieve entry points, recent git diffs, and related tests.\n"
705
+ "3. Formulate hypotheses strictly based on cited source evidence."
706
+ )
707
+
708
+ @app.prompt()
709
+ def prepare_change(target: str = "") -> str:
710
+ """Guide the AI coding agent to evaluate blast radius before changing code."""
711
+ return (
712
+ f"Target symbol or file: {target}\n"
713
+ "1. Call `analyze_impact` to compute callers and affected modules.\n"
714
+ "2. Call `find_related_tests` to identify regression verification targets.\n"
715
+ "3. Call `get_context` with intent='CHANGE'."
716
+ )
717
+
718
+ @app.prompt()
719
+ def analyze_impact_prompt(target: str = "") -> str:
720
+ """Guide the AI coding agent to compute reverse dependency blast radius."""
721
+ return (
722
+ f"Target: {target}\n"
723
+ "1. Call `analyze_impact` for reverse references, callers, and dependents.\n"
724
+ "2. Verify related tests and routes."
725
+ )
726
+
727
+ @app.prompt()
728
+ def review_change(since: str = "HEAD~1") -> str:
729
+ """Guide the AI coding agent to review recent changes against affected tests."""
730
+ return (
731
+ f"Diff range: {since}..HEAD\n"
732
+ "1. Call `analyze_change_impact` to identify changed symbols and callers.\n"
733
+ "2. Run `get_context` with intent='REVIEW'."
734
+ )
735
+
736
+ return app