codegraph-engine 2.3.0__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.
- codegraph/__init__.py +1 -1
- codegraph/agent_brain.py +172 -2
- codegraph/agent_capabilities.py +320 -3
- codegraph/agent_rules.py +1 -1
- codegraph/api_drift.py +455 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/change_impact.py +320 -110
- codegraph/cli.py +776 -25
- codegraph/cli_output.py +17 -2
- codegraph/context.py +38 -11
- codegraph/context_freshness.py +2 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +7 -0
- codegraph/evidence_contract.py +21 -0
- codegraph/frameworks.py +171 -6
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +2 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/indexer.py +291 -38
- codegraph/indexing/models.py +6 -0
- codegraph/indexing/parser.py +239 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +6 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +128 -20
- codegraph/mcp/server.py +190 -28
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/optimizer.py +42 -5
- codegraph/process_lifecycle.py +31 -13
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +403 -0
- codegraph/resolver.py +1202 -159
- codegraph/retrieval_policy.py +6 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +19 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +2 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +2 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +715 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +150 -38
- codegraph_engine-3.0.0.dist-info/RECORD +129 -0
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.3.0.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 =
|
|
448
|
+
max_files: int = 25,
|
|
446
449
|
max_lines: int = 500,
|
|
447
|
-
top_k: int =
|
|
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,58 @@ 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
|
+
|
|
959
1014
|
# -----------------------------------------------------------------------
|
|
960
1015
|
# Git Intelligence Tools (5 Unified Capabilities)
|
|
961
1016
|
# -----------------------------------------------------------------------
|
|
@@ -980,8 +1035,12 @@ def create_server(
|
|
|
980
1035
|
def get_change_impact(base: str = "HEAD~1", head: str = "HEAD", max_depth: int = 2, max_results: int = 50) -> dict[str, Any]:
|
|
981
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."""
|
|
982
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]
|
|
983
1042
|
with indexer.session() as con:
|
|
984
|
-
return get_deep_change_impact(indexer.repository, con, base=base, head=head, max_depth=max_depth, max_results=max_results).as_dict()
|
|
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()
|
|
985
1044
|
|
|
986
1045
|
def check_context_freshness(task: str = "", context_packet: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
987
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."""
|
|
@@ -1015,6 +1074,49 @@ def create_server(
|
|
|
1015
1074
|
with indexer.session() as con:
|
|
1016
1075
|
return impl_detect_semantic_conflicts(indexer.repository, base_branch=base_branch, head_branch=head_branch, con=con).as_dict()
|
|
1017
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
|
+
|
|
1018
1120
|
core_tools: dict[str, Any] = {
|
|
1019
1121
|
"resolve_symbol": resolve_symbol,
|
|
1020
1122
|
"search_symbols": search_symbols,
|
|
@@ -1040,10 +1142,16 @@ def create_server(
|
|
|
1040
1142
|
"detect_semantic_conflicts": detect_semantic_conflicts,
|
|
1041
1143
|
}
|
|
1042
1144
|
|
|
1145
|
+
refactor_tools: dict[str, Any] = {
|
|
1146
|
+
"safe_rename": safe_rename,
|
|
1147
|
+
"rollback_refactor": rollback_refactor,
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1043
1150
|
# Map of all available tools
|
|
1044
1151
|
all_tools: dict[str, Any] = {
|
|
1045
1152
|
**core_tools,
|
|
1046
1153
|
**git_intelligence_tools,
|
|
1154
|
+
**refactor_tools,
|
|
1047
1155
|
"search_code": search_code,
|
|
1048
1156
|
"read_file": read_file,
|
|
1049
1157
|
"find_symbol": find_symbol,
|
|
@@ -1087,6 +1195,12 @@ def create_server(
|
|
|
1087
1195
|
"ingest_runtime_traces": ingest_runtime_traces,
|
|
1088
1196
|
"get_runtime_trace": get_runtime_trace,
|
|
1089
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,
|
|
1090
1204
|
}
|
|
1091
1205
|
|
|
1092
1206
|
agent_names = {
|
|
@@ -1179,6 +1293,7 @@ def create_server(
|
|
|
1179
1293
|
|
|
1180
1294
|
get_graph_cache().clear()
|
|
1181
1295
|
get_parse_cache().clear()
|
|
1296
|
+
indexer.reset_pool()
|
|
1182
1297
|
return True
|
|
1183
1298
|
return False
|
|
1184
1299
|
|
|
@@ -1196,6 +1311,40 @@ def create_server(
|
|
|
1196
1311
|
|
|
1197
1312
|
app.tool(name=tool_name)(_make_guarded(target_fn))
|
|
1198
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
|
+
|
|
1199
1348
|
# -----------------------------------------------------------------------
|
|
1200
1349
|
# MCP Resources
|
|
1201
1350
|
# -----------------------------------------------------------------------
|
|
@@ -1208,47 +1357,60 @@ def create_server(
|
|
|
1208
1357
|
return json.dumps(get_capability_manifest(), indent=2)
|
|
1209
1358
|
|
|
1210
1359
|
@app.resource("codebase://status")
|
|
1211
|
-
def resource_status() -> str:
|
|
1360
|
+
async def resource_status() -> str:
|
|
1212
1361
|
"""Repository index status, generation, and file counts."""
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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)
|
|
1225
1377
|
|
|
1226
1378
|
@app.resource("codebase://architecture")
|
|
1227
|
-
def resource_architecture() -> str:
|
|
1379
|
+
async def resource_architecture() -> str:
|
|
1228
1380
|
"""High-level architectural overview."""
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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)
|
|
1232
1387
|
|
|
1233
1388
|
@app.resource("codebase://modules")
|
|
1234
|
-
def resource_modules() -> str:
|
|
1389
|
+
async def resource_modules() -> str:
|
|
1235
1390
|
"""List of indexed repository module paths."""
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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)
|
|
1239
1397
|
|
|
1240
1398
|
@app.resource("codebase://health")
|
|
1241
|
-
def resource_health() -> str:
|
|
1399
|
+
async def resource_health() -> str:
|
|
1242
1400
|
"""Database health and integrity report."""
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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)
|
|
1246
1407
|
|
|
1247
1408
|
@app.resource("codebase://resources")
|
|
1248
1409
|
def resource_resources() -> str:
|
|
1249
1410
|
"""Resource governor state, memory bounds, pressure, and activity mode."""
|
|
1250
1411
|
return json.dumps(indexer.governor.get_state().as_dict(), indent=2)
|
|
1251
1412
|
|
|
1413
|
+
|
|
1252
1414
|
# -----------------------------------------------------------------------
|
|
1253
1415
|
# MCP Prompts
|
|
1254
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
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
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):
|
codegraph/process_lifecycle.py
CHANGED
|
@@ -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
|
-
|
|
544
|
+
# PID 0 and 1 represent init / systemd / launchd / container root; always considered alive
|
|
545
|
+
return True
|
|
545
546
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
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
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Models and data structures for deterministic, syntax-validated, transaction-safe refactoring."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RefactorStatus(StrEnum):
|
|
10
|
+
"""Execution status of a refactoring plan or transaction."""
|
|
11
|
+
|
|
12
|
+
READY = "READY"
|
|
13
|
+
BLOCKED = "BLOCKED"
|
|
14
|
+
APPLIED = "APPLIED"
|
|
15
|
+
ROLLED_BACK = "ROLLED_BACK"
|
|
16
|
+
ERROR = "ERROR"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RefactorRisk(StrEnum):
|
|
20
|
+
"""Assessed risk of an AST refactoring operation."""
|
|
21
|
+
|
|
22
|
+
LOW = "LOW" # All affected relationships are FACT / deterministically verified.
|
|
23
|
+
MEDIUM = "MEDIUM" # POSSIBLE relationships exist but scope is bounded.
|
|
24
|
+
HIGH = "HIGH" # UNKNOWN / AMBIGUOUS / CONFLICT relationships could make rename incomplete.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class TokenReplacementSpan:
|
|
29
|
+
"""Exact byte/token coordinates for an AST-grounded replacement."""
|
|
30
|
+
|
|
31
|
+
path: str
|
|
32
|
+
start_line: int
|
|
33
|
+
start_col: int
|
|
34
|
+
end_line: int
|
|
35
|
+
end_col: int
|
|
36
|
+
old_token: str
|
|
37
|
+
new_token: str
|
|
38
|
+
evidence: str
|
|
39
|
+
symbol_id: str
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
return asdict(self)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class RefactorPlan:
|
|
47
|
+
"""Deterministic refactoring plan before in-memory preview or commit."""
|
|
48
|
+
|
|
49
|
+
transaction_id: str
|
|
50
|
+
target_symbol: str
|
|
51
|
+
new_name: str
|
|
52
|
+
files: list[str] = field(default_factory=list)
|
|
53
|
+
spans: list[TokenReplacementSpan] = field(default_factory=list)
|
|
54
|
+
risk: RefactorRisk = RefactorRisk.LOW
|
|
55
|
+
uncertainty_reasons: list[str] = field(default_factory=list)
|
|
56
|
+
affected_tests: list[str] = field(default_factory=list)
|
|
57
|
+
affected_routes: list[str] = field(default_factory=list)
|
|
58
|
+
affected_db_relationships: list[str] = field(default_factory=list)
|
|
59
|
+
original_file_hashes: dict[str, str] = field(default_factory=dict)
|
|
60
|
+
original_file_sizes: dict[str, int] = field(default_factory=dict)
|
|
61
|
+
original_file_mtimes: dict[str, float] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> dict[str, Any]:
|
|
64
|
+
return {
|
|
65
|
+
"transaction_id": self.transaction_id,
|
|
66
|
+
"target_symbol": self.target_symbol,
|
|
67
|
+
"new_name": self.new_name,
|
|
68
|
+
"files": sorted(self.files),
|
|
69
|
+
"spans": [s.to_dict() for s in sorted(self.spans, key=lambda x: (x.path, x.start_line, x.start_col))],
|
|
70
|
+
"risk": str(self.risk),
|
|
71
|
+
"uncertainty_reasons": sorted(self.uncertainty_reasons),
|
|
72
|
+
"affected_tests": sorted(self.affected_tests),
|
|
73
|
+
"affected_routes": sorted(self.affected_routes),
|
|
74
|
+
"affected_db_relationships": sorted(self.affected_db_relationships),
|
|
75
|
+
"original_file_hashes": dict(sorted(self.original_file_hashes.items())),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class RefactorResult:
|
|
81
|
+
"""Complete result of a refactor preview, apply, or rollback."""
|
|
82
|
+
|
|
83
|
+
status: RefactorStatus
|
|
84
|
+
target_symbol: str
|
|
85
|
+
new_name: str
|
|
86
|
+
risk: RefactorRisk
|
|
87
|
+
uncertainty_reasons: list[str] = field(default_factory=list)
|
|
88
|
+
files_changed: int = 0
|
|
89
|
+
symbols_changed: int = 0
|
|
90
|
+
spans_count: int = 0
|
|
91
|
+
tests_affected: list[str] = field(default_factory=list)
|
|
92
|
+
routes_affected: list[str] = field(default_factory=list)
|
|
93
|
+
db_affected: list[str] = field(default_factory=list)
|
|
94
|
+
rollback_id: str | None = None
|
|
95
|
+
diffs: dict[str, str] = field(default_factory=dict)
|
|
96
|
+
errors: list[str] = field(default_factory=list)
|
|
97
|
+
|
|
98
|
+
def to_dict(self) -> dict[str, Any]:
|
|
99
|
+
return {
|
|
100
|
+
"status": str(self.status),
|
|
101
|
+
"target_symbol": self.target_symbol,
|
|
102
|
+
"new_name": self.new_name,
|
|
103
|
+
"risk": str(self.risk),
|
|
104
|
+
"uncertainty_reasons": sorted(self.uncertainty_reasons),
|
|
105
|
+
"files_changed": self.files_changed,
|
|
106
|
+
"symbols_changed": self.symbols_changed,
|
|
107
|
+
"spans_count": self.spans_count,
|
|
108
|
+
"tests_affected": sorted(self.tests_affected),
|
|
109
|
+
"routes_affected": sorted(self.routes_affected),
|
|
110
|
+
"db_affected": sorted(self.db_affected),
|
|
111
|
+
"rollback_id": self.rollback_id,
|
|
112
|
+
"diffs": dict(sorted(self.diffs.items())),
|
|
113
|
+
"errors": sorted(self.errors),
|
|
114
|
+
}
|