codegraph-engine 2.2.1__py3-none-any.whl → 3.0.0__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 (82) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/agent_brain.py +172 -2
  3. codegraph/agent_capabilities.py +564 -6
  4. codegraph/agent_rules.py +1 -1
  5. codegraph/api_drift.py +455 -0
  6. codegraph/async_queue/__init__.py +50 -0
  7. codegraph/async_queue/detectors/__init__.py +48 -0
  8. codegraph/async_queue/detectors/celery.py +160 -0
  9. codegraph/async_queue/detectors/kafka.py +168 -0
  10. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  11. codegraph/async_queue/detectors/redis.py +130 -0
  12. codegraph/async_queue/detectors/sqs.py +135 -0
  13. codegraph/async_queue/models.py +161 -0
  14. codegraph/async_queue/otel_models.py +74 -0
  15. codegraph/async_queue/otel_parser.py +337 -0
  16. codegraph/async_queue/reconciler.py +427 -0
  17. codegraph/async_queue/scanner.py +291 -0
  18. codegraph/async_queue/traversal.py +447 -0
  19. codegraph/branch_comparison.py +197 -0
  20. codegraph/change_impact.py +547 -0
  21. codegraph/cli.py +992 -3
  22. codegraph/cli_output.py +17 -2
  23. codegraph/context.py +38 -11
  24. codegraph/context_freshness.py +351 -0
  25. codegraph/database/__init__.py +14 -0
  26. codegraph/database/extractor.py +712 -17
  27. codegraph/database/interrogation.py +388 -2
  28. codegraph/database/schema_drift.py +862 -0
  29. codegraph/epistemic.py +7 -0
  30. codegraph/evidence_contract.py +21 -0
  31. codegraph/frameworks.py +171 -6
  32. codegraph/frameworks_nestjs.py +502 -0
  33. codegraph/frameworks_nextjs.py +337 -0
  34. codegraph/frameworks_react.py +457 -0
  35. codegraph/freshness.py +2 -0
  36. codegraph/frontend_html.py +109 -0
  37. codegraph/git.py +2 -0
  38. codegraph/git_state.py +545 -0
  39. codegraph/indexing/__init__.py +7 -1
  40. codegraph/indexing/indexer.py +291 -38
  41. codegraph/indexing/models.py +6 -0
  42. codegraph/indexing/parser.py +239 -0
  43. codegraph/indexing/pool.py +136 -0
  44. codegraph/indexing/scanner.py +6 -0
  45. codegraph/indexing/templates.py +418 -0
  46. codegraph/interrogation.py +128 -20
  47. codegraph/mcp/server.py +258 -27
  48. codegraph/models.py +7 -0
  49. codegraph/monorepo.py +3 -0
  50. codegraph/optimizer.py +42 -5
  51. codegraph/process_lifecycle.py +31 -13
  52. codegraph/refactor/__init__.py +36 -0
  53. codegraph/refactor/models.py +114 -0
  54. codegraph/refactor/renamer.py +844 -0
  55. codegraph/refactor/transactions.py +403 -0
  56. codegraph/resolver.py +1202 -159
  57. codegraph/retrieval_policy.py +6 -0
  58. codegraph/route_topology.py +172 -0
  59. codegraph/runtime/__init__.py +19 -0
  60. codegraph/runtime/collector.py +155 -0
  61. codegraph/runtime/db_watcher.py +191 -0
  62. codegraph/runtime/ingestor.py +30 -10
  63. codegraph/runtime/port_inspector.py +304 -0
  64. codegraph/runtime/runner.py +155 -6
  65. codegraph/runtime/trace_linker.py +175 -0
  66. codegraph/search/hybrid.py +26 -0
  67. codegraph/semantic_conflicts.py +270 -0
  68. codegraph/semantic_decorators.py +34 -3
  69. codegraph/structural_diff.py +542 -0
  70. codegraph/styles.py +144 -0
  71. codegraph/symbol_history.py +277 -0
  72. codegraph/tsconfig.py +236 -0
  73. codegraph/ui/__init__.py +6 -0
  74. codegraph/ui/server.py +715 -0
  75. codegraph/watcher.py +323 -0
  76. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +257 -39
  77. codegraph_engine-3.0.0.dist-info/RECORD +129 -0
  78. codegraph_engine-2.2.1.dist-info/RECORD +0 -87
  79. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
  80. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
  81. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
  82. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
codegraph/mcp/server.py CHANGED
@@ -1,10 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import functools
4
5
  import json
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
9
+ import anyio
10
+
8
11
  from codegraph.architecture import get_architecture as arch_get_architecture
9
12
  from codegraph.config import Settings
10
13
  from codegraph.context import _resolve_task_or_query_input
@@ -442,9 +445,9 @@ def create_server(
442
445
  task: dict[str, Any] | str = "",
443
446
  intent: str | None = None,
444
447
  max_tokens: int = 4000,
445
- max_files: int = 15,
448
+ max_files: int = 25,
446
449
  max_lines: int = 500,
447
- top_k: int = 15,
450
+ top_k: int = 25,
448
451
  plan: dict[str, Any] | None = None,
449
452
  mode: str = "BALANCED",
450
453
  explain: bool = False,
@@ -956,6 +959,164 @@ def create_server(
956
959
  with indexer.session() as con:
957
960
  return runtime_reconcile_static_runtime(con, indexer.repository, symbol=symbol, route=route, table=table)
958
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
+
959
1120
  core_tools: dict[str, Any] = {
960
1121
  "resolve_symbol": resolve_symbol,
961
1122
  "search_symbols": search_symbols,
@@ -972,9 +1133,25 @@ def create_server(
972
1133
  "get_git_impact": get_git_impact,
973
1134
  }
974
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
+
975
1150
  # Map of all available tools
976
1151
  all_tools: dict[str, Any] = {
977
1152
  **core_tools,
1153
+ **git_intelligence_tools,
1154
+ **refactor_tools,
978
1155
  "search_code": search_code,
979
1156
  "read_file": read_file,
980
1157
  "find_symbol": find_symbol,
@@ -1018,6 +1195,12 @@ def create_server(
1018
1195
  "ingest_runtime_traces": ingest_runtime_traces,
1019
1196
  "get_runtime_trace": get_runtime_trace,
1020
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,
1021
1204
  }
1022
1205
 
1023
1206
  agent_names = {
@@ -1110,6 +1293,7 @@ def create_server(
1110
1293
 
1111
1294
  get_graph_cache().clear()
1112
1295
  get_parse_cache().clear()
1296
+ indexer.reset_pool()
1113
1297
  return True
1114
1298
  return False
1115
1299
 
@@ -1127,6 +1311,40 @@ def create_server(
1127
1311
 
1128
1312
  app.tool(name=tool_name)(_make_guarded(target_fn))
1129
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
+
1130
1348
  # -----------------------------------------------------------------------
1131
1349
  # MCP Resources
1132
1350
  # -----------------------------------------------------------------------
@@ -1139,47 +1357,60 @@ def create_server(
1139
1357
  return json.dumps(get_capability_manifest(), indent=2)
1140
1358
 
1141
1359
  @app.resource("codebase://status")
1142
- def resource_status() -> str:
1360
+ async def resource_status() -> str:
1143
1361
  """Repository index status, generation, and file counts."""
1144
- with indexer.session() as con:
1145
- report = check_freshness(indexer.repository, con)
1146
- return json.dumps(
1147
- {
1148
- "repository": str(indexer.repository),
1149
- "generation": report.index_generation,
1150
- "freshness": report.status.value,
1151
- "files": con.execute("SELECT count(*) FROM files").fetchone()[0],
1152
- "symbols": con.execute("SELECT count(*) FROM symbols").fetchone()[0],
1153
- },
1154
- indent=2,
1155
- )
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)
1156
1377
 
1157
1378
  @app.resource("codebase://architecture")
1158
- def resource_architecture() -> str:
1379
+ async def resource_architecture() -> str:
1159
1380
  """High-level architectural overview."""
1160
- with indexer.session() as con:
1161
- arch = arch_get_architecture(con, indexer.repository)
1162
- return json.dumps(arch, indent=2)
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)
1163
1387
 
1164
1388
  @app.resource("codebase://modules")
1165
- def resource_modules() -> str:
1389
+ async def resource_modules() -> str:
1166
1390
  """List of indexed repository module paths."""
1167
- with indexer.session() as con:
1168
- rows = con.execute("SELECT path FROM files ORDER BY path LIMIT 500").fetchall()
1169
- return "\n".join(r[0] for r in rows)
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)
1170
1397
 
1171
1398
  @app.resource("codebase://health")
1172
- def resource_health() -> str:
1399
+ async def resource_health() -> str:
1173
1400
  """Database health and integrity report."""
1174
- with indexer.session() as con:
1175
- health = check_database_health(con)
1176
- return json.dumps(health, indent=2)
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)
1177
1407
 
1178
1408
  @app.resource("codebase://resources")
1179
1409
  def resource_resources() -> str:
1180
1410
  """Resource governor state, memory bounds, pressure, and activity mode."""
1181
1411
  return json.dumps(indexer.governor.get_state().as_dict(), indent=2)
1182
1412
 
1413
+
1183
1414
  # -----------------------------------------------------------------------
1184
1415
  # MCP Prompts
1185
1416
  # -----------------------------------------------------------------------
codegraph/models.py CHANGED
@@ -56,6 +56,13 @@ EdgeType = Literal[
56
56
  "EVENT_LISTENER",
57
57
  "TASK_HANDLER",
58
58
  "COMMAND_HANDLER",
59
+ "DISPATCHES_TASK",
60
+ "TRIGGERS_SIGNAL",
61
+ "HANDLES_SIGNAL",
62
+ "RENDERS_TEMPLATE",
63
+ "DISPATCHES_FORWARD",
64
+ "TOOL_HANDLER",
65
+ "PIPELINE_STEP",
59
66
  "TESTS",
60
67
  "TESTS_ROUTE",
61
68
  "TESTS_SYMBOL",
codegraph/monorepo.py CHANGED
@@ -920,3 +920,6 @@ def detect_workspace(
920
920
  unresolved_dependencies=unresolved_dependencies,
921
921
  ambiguous_packages=ambiguous_packages,
922
922
  )
923
+
924
+
925
+ discover_workspace = detect_workspace
codegraph/optimizer.py CHANGED
@@ -41,7 +41,7 @@ _CONFIDENCE_RANK = {
41
41
 
42
42
  # Hard upper safety limits enforced even when omitted or exceeded
43
43
  HARD_MAX_TOKENS = 20_000
44
- HARD_MAX_FILES = 30
44
+ HARD_MAX_FILES = 40
45
45
  HARD_MAX_LINES = 1_500
46
46
  HARD_MAX_SYMBOLS = 50
47
47
  HARD_MAX_RELATIONSHIPS = 60
@@ -54,7 +54,7 @@ class ContextBudgetSpec:
54
54
  max_tokens: int = 20_000
55
55
  max_symbols: int = 25
56
56
  max_relationships: int = 30
57
- max_files: int = 15
57
+ max_files: int = 25
58
58
  max_lines: int = 500
59
59
  max_depth: int = 3
60
60
  max_package_depth: int = 2
@@ -94,7 +94,7 @@ class ContextBudget:
94
94
  tool_calls: int = 1
95
95
  max_symbols: int = 25
96
96
  max_relationships: int = 30
97
- max_files: int = 15
97
+ max_files: int = 25
98
98
  max_lines: int = 500
99
99
  max_depth: int = 3
100
100
  max_package_depth: int = 2
@@ -374,6 +374,17 @@ def compute_item_utility(
374
374
  dim_unseen = selected_dimensions is not None and dim not in selected_dimensions and dim != "GENERAL"
375
375
  coverage_factor = 1.3 if (layer_unseen or dim_unseen) else 1.0
376
376
 
377
+ # Priority boost for task-critical items over tangential ones
378
+ priority_dim_boost = 0.0
379
+ if dim == "TARGET" or item.relevance_score >= 0.80:
380
+ priority_dim_boost = 0.20
381
+ elif dim in ("ROUTE", "CALLER"):
382
+ priority_dim_boost = 0.12
383
+ elif dim == "CALLEE":
384
+ priority_dim_boost = 0.08
385
+ elif dim in ("TEST", "GIT", "PACKAGE", "GENERAL") and item.relevance_score < 0.50:
386
+ priority_dim_boost = -0.08
387
+
377
388
  # Redundancy penalty: heavily discount items sharing duplicate keys with already selected items
378
389
  redundancy_penalty = 0.5 if item.duplicate_key in selected_duplicate_keys else 0.0
379
390
 
@@ -384,6 +395,7 @@ def compute_item_utility(
384
395
  + (fresh_factor * 0.15)
385
396
  + (epistemic_factor * 0.10)
386
397
  + pkg_adj
398
+ + priority_dim_boost
387
399
  ) * coverage_factor
388
400
 
389
401
  final_score = max(0.01, utility - redundancy_penalty - conf_penalty - art_penalty - dist_penalty)
@@ -572,7 +584,7 @@ def optimize_context_budget(
572
584
  """Deterministically select the highest-utility, coverage-preserving subset of candidates."""
573
585
  raw_max_symbols = budget_spec.max_symbols if budget_spec else 25
574
586
  raw_max_relationships = budget_spec.max_relationships if budget_spec else 30
575
- raw_max_files = budget_spec.max_files if budget_spec else 15
587
+ raw_max_files = budget_spec.max_files if budget_spec else 25
576
588
  raw_max_lines = budget_spec.max_lines if budget_spec else 500
577
589
  max_depth = budget_spec.max_depth if budget_spec else 3
578
590
  max_package_depth = budget_spec.max_package_depth if budget_spec else 2
@@ -767,7 +779,32 @@ def optimize_context_budget(
767
779
  )
768
780
  break
769
781
 
770
- # Pass 1b: Architectural Layer Coverage pass — ensure distinct layers
782
+ # Pass 1b: Task-Critical Candidates Priority pass — ensure all direct targets,
783
+ # routes, and direct callers/callees are admitted into the file budget before
784
+ # tangential layer coverage files consume file slots.
785
+ for item in sorted_candidates:
786
+ if item in selected or _is_excluded(item, task_spec):
787
+ continue
788
+ if item.duplicate_key in selected_duplicate_keys:
789
+ continue
790
+ dim = item.infer_dimension()
791
+ is_task_critical = (
792
+ dim in ("TARGET", "ROUTE", "CALLER")
793
+ or item.relevance_score >= 0.70
794
+ or (dim == "CALLEE" and item.relevance_score >= 0.50)
795
+ )
796
+ if is_task_critical:
797
+ if _can_fit(item) or (dim == "TARGET" and len(selected) == 0 and _trim_to_fit(item)):
798
+ _accept(
799
+ item,
800
+ is_coverage_fill=False,
801
+ why=[
802
+ f"Task-critical priority candidate: dimension '{dim}'",
803
+ f"Relevance: {item.relevance_score:.2f}, Evidence: {item.evidence_quality:.2f}",
804
+ ],
805
+ )
806
+
807
+ # Pass 1c: Architectural Layer Coverage pass — ensure distinct layers
771
808
  # (e.g. ENTRYPOINT, SERVICE, DATA, TEST, GIT) get represented if candidates exist
772
809
  for item in sorted_candidates:
773
810
  if item in selected or _is_excluded(item, task_spec):
@@ -541,16 +541,28 @@ def is_parent_alive(
541
541
  ) -> bool:
542
542
  """Return True if the parent process `parent_pid` is still alive and owns this child."""
543
543
  if parent_pid <= 1:
544
- return False
544
+ # PID 0 and 1 represent init / systemd / launchd / container root; always considered alive
545
+ return True
545
546
 
546
- if sys.platform != "win32":
547
- actual_ppid = current_ppid if current_ppid is not None else os.getppid()
548
- if actual_ppid != parent_pid:
549
- # Reparented to PID 1 / launchd / systemd --user
550
- return False
547
+ # 1. Direct probe whether parent PID exists via os.kill(pid, 0)
548
+ try:
549
+ os.kill(parent_pid, 0)
550
+ except ProcessLookupError:
551
+ # ESRCH: Process does not exist; parent is definitely dead
552
+ return False
553
+ except PermissionError:
554
+ # EPERM: Process exists, but caller lacks permissions to signal it (e.g. sandbox/different user)
555
+ return True
556
+ except OSError:
557
+ # Other OS error: safe fallback is alive
558
+ return True
551
559
 
552
560
  live = get_live_process_info(parent_pid)
553
- if live is None or live.is_zombie:
561
+ if live is None:
562
+ # If OS inspection was restricted (e.g. sandbox blocking proc_pidinfo),
563
+ # but os.kill succeeded above, the process is alive.
564
+ return True
565
+ if live.is_zombie:
554
566
  return False
555
567
 
556
568
  if parent_create_token and live.create_token and live.create_token != parent_create_token:
@@ -964,16 +976,22 @@ class MCPLifecycleController:
964
976
 
965
977
  def _watchdog_loop(self) -> None:
966
978
  """Low-overhead event-backed watchdog detecting parent termination or closed stdin."""
979
+ disable_parent_watchdog = (
980
+ os.environ.get("CODEGRAPH_DISABLE_PARENT_WATCHDOG", "").strip().lower()
981
+ in ("1", "true", "yes")
982
+ or self.record.parent_pid <= 1
983
+ )
967
984
  while not self._stop_event.wait(timeout=self.poll_interval_sec):
968
985
  if _is_stream_closed_or_eof(sys.stdin):
969
986
  self.shutdown("stdin_closed")
970
987
  return
971
- if not is_parent_alive(
972
- self.record.parent_pid,
973
- self.record.parent_create_token,
974
- ):
975
- self.shutdown("parent_process_terminated")
976
- return
988
+ if not disable_parent_watchdog:
989
+ if not is_parent_alive(
990
+ self.record.parent_pid,
991
+ self.record.parent_create_token,
992
+ ):
993
+ self.shutdown("parent_process_terminated")
994
+ return
977
995
 
978
996
  def _atexit_cleanup(self) -> None:
979
997
  self.shutdown("atexit")
@@ -0,0 +1,36 @@
1
+ """Deterministic, syntax-validated, transaction-safe refactoring engine for CodeGraph MCP."""
2
+ from __future__ import annotations
3
+
4
+ from codegraph.refactor.models import (
5
+ RefactorPlan,
6
+ RefactorResult,
7
+ RefactorRisk,
8
+ RefactorStatus,
9
+ TokenReplacementSpan,
10
+ )
11
+ from codegraph.refactor.renamer import (
12
+ execute_safe_rename,
13
+ plan_safe_rename,
14
+ preview_safe_rename,
15
+ )
16
+ from codegraph.refactor.transactions import (
17
+ RefactorLock,
18
+ RefactorLockError,
19
+ commit_refactor_transaction,
20
+ rollback_refactor,
21
+ )
22
+
23
+ __all__ = [
24
+ "RefactorLock",
25
+ "RefactorLockError",
26
+ "RefactorPlan",
27
+ "RefactorResult",
28
+ "RefactorRisk",
29
+ "RefactorStatus",
30
+ "TokenReplacementSpan",
31
+ "commit_refactor_transaction",
32
+ "execute_safe_rename",
33
+ "plan_safe_rename",
34
+ "preview_safe_rename",
35
+ "rollback_refactor",
36
+ ]