codegraph-engine 2.3.0__py3-none-any.whl → 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +1 -1
- codegraph/__main__.py +7 -0
- codegraph/agent_brain.py +253 -2
- codegraph/agent_capabilities.py +1348 -67
- codegraph/agent_rules.py +2 -1
- codegraph/api_drift.py +455 -0
- codegraph/architecture_rules.py +123 -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/changelog.py +170 -0
- codegraph/cli.py +1605 -62
- codegraph/cli_output.py +17 -2
- codegraph/context.py +135 -24
- 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/dataflow_propagation.py +118 -0
- codegraph/dead_code.py +387 -0
- codegraph/epistemic.py +15 -0
- codegraph/evidence_contract.py +34 -3
- codegraph/federation.py +245 -0
- codegraph/frameworks.py +234 -12
- 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/graph/traversal.py +150 -7
- codegraph/graph_csr.py +218 -0
- codegraph/incremental.py +150 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/classifier.py +5 -0
- codegraph/indexing/indexer.py +292 -38
- codegraph/indexing/languages/__init__.py +1 -0
- codegraph/indexing/languages/cpp.py +457 -0
- codegraph/indexing/languages/go.py +459 -0
- codegraph/indexing/languages/jvm.py +583 -0
- codegraph/indexing/languages/rust.py +507 -0
- codegraph/indexing/models.py +15 -1
- codegraph/indexing/parser.py +385 -9
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/protocols.py +220 -0
- codegraph/indexing/scanner.py +23 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +207 -39
- codegraph/lsp.py +318 -0
- codegraph/lsp_bridge/__init__.py +16 -0
- codegraph/lsp_bridge/bridge.py +102 -0
- codegraph/lsp_bridge/client.py +207 -0
- codegraph/lsp_bridge/models.py +71 -0
- codegraph/mcp/server.py +502 -29
- codegraph/memory/trajectory.py +282 -0
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/multi_agent/__init__.py +13 -0
- codegraph/multi_agent/coordinator.py +229 -0
- codegraph/multi_agent/models.py +76 -0
- codegraph/optimizer.py +42 -5
- codegraph/polymorphism.py +95 -0
- codegraph/pr_review.py +296 -0
- codegraph/process_lifecycle.py +50 -12
- codegraph/reachability.py +198 -0
- codegraph/reachability_index.py +74 -0
- codegraph/refactor/__init__.py +48 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/patcher.py +348 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/repair.py +233 -0
- codegraph/resolver.py +1357 -161
- codegraph/retrieval_policy.py +22 -1
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +7 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +25 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/otel_receiver.py +208 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/sandbox.py +215 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/security/supply_chain.py +188 -0
- codegraph/security/taint.py +421 -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/test_gen.py +437 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +876 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
- codegraph_engine-3.0.1.dist-info/RECORD +164 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""DI-Aware Deep Reachability & True Dead Code Tree-Shaking Engine for CodeGraph.
|
|
2
|
+
|
|
3
|
+
Traces downward from all legitimate repository entrypoint roots:
|
|
4
|
+
- HTTP Routes (FastAPI, Express, Spring, Gin, Axum, Crow, etc.)
|
|
5
|
+
- CLI Commands and main() entrypoints
|
|
6
|
+
- Scheduled tasks, Celery / async workers, and event handlers
|
|
7
|
+
- Traverses lexical calls, inheritance overrides, and Dependency Injection providers (@Autowired, Depends)
|
|
8
|
+
- Identifies true unreachable dead code with high confidence
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sqlite3
|
|
13
|
+
from collections import deque
|
|
14
|
+
from dataclasses import asdict, dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class UnreachableSymbol:
|
|
21
|
+
"""A symbol confirmed to be unreachable from any repository entrypoint."""
|
|
22
|
+
|
|
23
|
+
canonical_id: str
|
|
24
|
+
name: str
|
|
25
|
+
kind: str
|
|
26
|
+
file_path: str
|
|
27
|
+
line: int
|
|
28
|
+
confidence: str = "STATIC_VERIFIED"
|
|
29
|
+
|
|
30
|
+
def as_dict(self) -> dict[str, Any]:
|
|
31
|
+
return asdict(self)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ReachabilityReport:
|
|
36
|
+
"""Consolidated report of reachable vs dead code across the repository."""
|
|
37
|
+
|
|
38
|
+
workspace: str
|
|
39
|
+
total_symbols: int
|
|
40
|
+
reachable_count: int
|
|
41
|
+
unreachable_count: int
|
|
42
|
+
root_entrypoints_count: int
|
|
43
|
+
dead_code_percentage: float
|
|
44
|
+
unreachable_symbols: list[UnreachableSymbol] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
def as_dict(self) -> dict[str, Any]:
|
|
47
|
+
return {
|
|
48
|
+
"workspace": self.workspace,
|
|
49
|
+
"total_symbols": self.total_symbols,
|
|
50
|
+
"reachable_count": self.reachable_count,
|
|
51
|
+
"unreachable_count": self.unreachable_count,
|
|
52
|
+
"root_entrypoints_count": self.root_entrypoints_count,
|
|
53
|
+
"dead_code_percentage": round(self.dead_code_percentage, 2),
|
|
54
|
+
"unreachable_symbols": [s.as_dict() for s in self.unreachable_symbols[:100]],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def compute_di_aware_reachability(
|
|
59
|
+
repository: Path,
|
|
60
|
+
con: sqlite3.Connection,
|
|
61
|
+
) -> ReachabilityReport:
|
|
62
|
+
"""Compute reachability from roots through calls, inheritance, and DI bindings."""
|
|
63
|
+
repo = repository.resolve()
|
|
64
|
+
cur = con.cursor()
|
|
65
|
+
|
|
66
|
+
# 1. Fetch all symbols
|
|
67
|
+
cur.execute("SELECT id, name, canonical_id, kind, path, start_line FROM symbols;")
|
|
68
|
+
sym_rows = cur.fetchall()
|
|
69
|
+
all_symbols: dict[str, tuple[str, str, str, str, int]] = {}
|
|
70
|
+
for r in sym_rows:
|
|
71
|
+
cid = str(r[2]) if r[2] else str(r[0])
|
|
72
|
+
# (name, canonical_id, kind, file_path, line)
|
|
73
|
+
all_symbols[cid] = (str(r[1]), cid, str(r[3]), str(r[4]), int(r[5]))
|
|
74
|
+
|
|
75
|
+
if not all_symbols:
|
|
76
|
+
return ReachabilityReport(
|
|
77
|
+
workspace=str(repo),
|
|
78
|
+
total_symbols=0,
|
|
79
|
+
reachable_count=0,
|
|
80
|
+
unreachable_count=0,
|
|
81
|
+
root_entrypoints_count=0,
|
|
82
|
+
dead_code_percentage=0.0,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# 2. Collect root entrypoints
|
|
86
|
+
# Routes
|
|
87
|
+
root_nodes: set[str] = set()
|
|
88
|
+
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('framework_routes', 'routes') ORDER BY (name='framework_routes') DESC LIMIT 1;")
|
|
89
|
+
tbl_row = cur.fetchone()
|
|
90
|
+
if tbl_row:
|
|
91
|
+
routes_tbl = tbl_row[0]
|
|
92
|
+
cur.execute(f"SELECT handler_name, handler_canonical_id FROM {routes_tbl};")
|
|
93
|
+
for hname, hcid in cur.fetchall():
|
|
94
|
+
if hcid and hcid in all_symbols:
|
|
95
|
+
root_nodes.add(hcid)
|
|
96
|
+
elif hname:
|
|
97
|
+
for cid, data in all_symbols.items():
|
|
98
|
+
if data[0] == hname:
|
|
99
|
+
root_nodes.add(cid)
|
|
100
|
+
|
|
101
|
+
# CLI / Main entrypoints
|
|
102
|
+
for cid, data in all_symbols.items():
|
|
103
|
+
name_lower = data[0].lower()
|
|
104
|
+
if name_lower in ("main", "run", "cli", "app") or "cli" in data[3].lower():
|
|
105
|
+
root_nodes.add(cid)
|
|
106
|
+
|
|
107
|
+
# 3. Build Adjacency Graph from calls, inheritance, and DI bindings
|
|
108
|
+
adj: dict[str, set[str]] = {cid: set() for cid in all_symbols}
|
|
109
|
+
|
|
110
|
+
# Calls
|
|
111
|
+
cur.execute("PRAGMA table_info(calls);")
|
|
112
|
+
c_cols = {col[1] for col in cur.fetchall()}
|
|
113
|
+
if "source_symbol_id" in c_cols:
|
|
114
|
+
cur.execute("""
|
|
115
|
+
SELECT COALESCE(s.canonical_id, calls.source_symbol_id), calls.callee
|
|
116
|
+
FROM calls
|
|
117
|
+
LEFT JOIN symbols s ON (calls.source_symbol_id = s.id OR calls.source_symbol_id = s.canonical_id);
|
|
118
|
+
""")
|
|
119
|
+
else:
|
|
120
|
+
caller_col = "caller_canonical_id" if "caller_canonical_id" in c_cols else "source_path"
|
|
121
|
+
cur.execute(f"SELECT {caller_col}, callee FROM calls;")
|
|
122
|
+
for caller_cid, callee in cur.fetchall():
|
|
123
|
+
if caller_cid and caller_cid in adj and callee:
|
|
124
|
+
# Link to matching callee canonical_ids
|
|
125
|
+
for cid, data in all_symbols.items():
|
|
126
|
+
if data[0] == callee:
|
|
127
|
+
adj[caller_cid].add(cid)
|
|
128
|
+
|
|
129
|
+
# Inheritance: base class is reachable if subclass is used
|
|
130
|
+
try:
|
|
131
|
+
cur.execute("SELECT source_canonical_id, base_name FROM inheritance;")
|
|
132
|
+
for sub_cid, base_name in cur.fetchall():
|
|
133
|
+
if sub_cid and sub_cid in adj and base_name:
|
|
134
|
+
for cid, data in all_symbols.items():
|
|
135
|
+
if data[0] == base_name:
|
|
136
|
+
adj[sub_cid].add(cid)
|
|
137
|
+
except sqlite3.OperationalError:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
# Dependency Injection: if a service is injected, its provider is reached
|
|
141
|
+
cur.execute(
|
|
142
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('local_bindings', 'bindings') "
|
|
143
|
+
"ORDER BY (name='local_bindings') DESC LIMIT 1;"
|
|
144
|
+
)
|
|
145
|
+
b_row = cur.fetchone()
|
|
146
|
+
if b_row:
|
|
147
|
+
try:
|
|
148
|
+
cur.execute(f"SELECT scope, target_name FROM {b_row[0]} WHERE expr_kind LIKE '%DEPENDENCY%';")
|
|
149
|
+
for scope_cid, target_name in cur.fetchall():
|
|
150
|
+
if scope_cid and scope_cid in adj and target_name:
|
|
151
|
+
for cid, data in all_symbols.items():
|
|
152
|
+
if data[0].lower() == target_name.lower():
|
|
153
|
+
adj[scope_cid].add(cid)
|
|
154
|
+
except sqlite3.OperationalError:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
# 4. BFS Traversal from roots
|
|
158
|
+
reachable: set[str] = set()
|
|
159
|
+
queue = deque(root_nodes)
|
|
160
|
+
while queue:
|
|
161
|
+
curr = queue.popleft()
|
|
162
|
+
if curr in reachable:
|
|
163
|
+
continue
|
|
164
|
+
reachable.add(curr)
|
|
165
|
+
for neighbor in adj.get(curr, set()):
|
|
166
|
+
if neighbor not in reachable:
|
|
167
|
+
queue.append(neighbor)
|
|
168
|
+
|
|
169
|
+
# 5. Calculate dead code
|
|
170
|
+
unreachable_list: list[UnreachableSymbol] = []
|
|
171
|
+
for cid, (name, canon, kind, path, line) in all_symbols.items():
|
|
172
|
+
# Exclude tests and migrations from being counted as dead code
|
|
173
|
+
if cid not in reachable:
|
|
174
|
+
if not any(part in path.lower() for part in ("test", "spec", "migration", "fixture")):
|
|
175
|
+
unreachable_list.append(
|
|
176
|
+
UnreachableSymbol(
|
|
177
|
+
canonical_id=canon,
|
|
178
|
+
name=name,
|
|
179
|
+
kind=kind,
|
|
180
|
+
file_path=path,
|
|
181
|
+
line=line,
|
|
182
|
+
confidence="STATIC_VERIFIED",
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
total = len(all_symbols)
|
|
187
|
+
unreachable_count = len(unreachable_list)
|
|
188
|
+
dead_pct = (unreachable_count / total * 100.0) if total > 0 else 0.0
|
|
189
|
+
|
|
190
|
+
return ReachabilityReport(
|
|
191
|
+
workspace=str(repo),
|
|
192
|
+
total_symbols=total,
|
|
193
|
+
reachable_count=len(reachable),
|
|
194
|
+
unreachable_count=unreachable_count,
|
|
195
|
+
root_entrypoints_count=len(root_nodes),
|
|
196
|
+
dead_code_percentage=dead_pct,
|
|
197
|
+
unreachable_symbols=unreachable_list,
|
|
198
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""O(1) Transitive Reachability Bitset Index for CodeGraph.
|
|
2
|
+
|
|
3
|
+
Provides constant-time reachability queries using precomputed bitmask matrices:
|
|
4
|
+
- can_reach(source, target) evaluates in O(1) time via bitwise AND
|
|
5
|
+
- Eliminates recursive graph searches during blast-radius pre-flight checks
|
|
6
|
+
- Instant ancestor and descendant set intersections
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from codegraph.graph_csr import CSRGraph
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class TransitiveReachabilityIndex:
|
|
17
|
+
"""Precomputed bitmask index for O(1) reachability tests."""
|
|
18
|
+
|
|
19
|
+
node_count: int
|
|
20
|
+
node_to_id: dict[str, int]
|
|
21
|
+
id_to_node: list[str]
|
|
22
|
+
bitsets: list[int] # Python arbitrary-precision int used as high-speed bitset
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def build_from_csr(cls, csr: CSRGraph, max_nodes: int = 5000) -> TransitiveReachabilityIndex:
|
|
26
|
+
"""Compute transitive reachability bitsets via topological or BFS propagation."""
|
|
27
|
+
n = min(csr.node_count, max_nodes)
|
|
28
|
+
bitsets = [0] * n
|
|
29
|
+
|
|
30
|
+
# Initialize self reachability and direct neighbors
|
|
31
|
+
for i in range(n):
|
|
32
|
+
mask = 1 << i
|
|
33
|
+
start = csr.indptr[i]
|
|
34
|
+
end = csr.indptr[i + 1]
|
|
35
|
+
for edge_idx in range(start, end):
|
|
36
|
+
neighbor = csr.indices[edge_idx]
|
|
37
|
+
if neighbor < n:
|
|
38
|
+
mask |= (1 << neighbor)
|
|
39
|
+
bitsets[i] = mask
|
|
40
|
+
|
|
41
|
+
# Floyd-Warshall / transitive closure propagation over bitsets
|
|
42
|
+
for k in range(n):
|
|
43
|
+
k_mask = bitsets[k]
|
|
44
|
+
k_bit = 1 << k
|
|
45
|
+
for i in range(n):
|
|
46
|
+
if bitsets[i] & k_bit:
|
|
47
|
+
bitsets[i] |= k_mask
|
|
48
|
+
|
|
49
|
+
return cls(
|
|
50
|
+
node_count=n,
|
|
51
|
+
node_to_id={csr.id_to_node[i]: i for i in range(n)},
|
|
52
|
+
id_to_node=csr.id_to_node[:n],
|
|
53
|
+
bitsets=bitsets,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def can_reach(self, source_node: str, target_node: str) -> bool:
|
|
57
|
+
"""Evaluate if source_node can reach target_node in O(1) bitwise AND."""
|
|
58
|
+
s_id = self.node_to_id.get(source_node)
|
|
59
|
+
t_id = self.node_to_id.get(target_node)
|
|
60
|
+
if s_id is None or t_id is None:
|
|
61
|
+
return False
|
|
62
|
+
return bool(self.bitsets[s_id] & (1 << t_id))
|
|
63
|
+
|
|
64
|
+
def get_descendants(self, source_node: str) -> list[str]:
|
|
65
|
+
"""Return all nodes reachable from source in constant-time bit iteration."""
|
|
66
|
+
s_id = self.node_to_id.get(source_node)
|
|
67
|
+
if s_id is None:
|
|
68
|
+
return []
|
|
69
|
+
mask = self.bitsets[s_id]
|
|
70
|
+
reachable = []
|
|
71
|
+
for i in range(self.node_count):
|
|
72
|
+
if mask & (1 << i):
|
|
73
|
+
reachable.append(self.id_to_node[i])
|
|
74
|
+
return reachable
|
|
@@ -0,0 +1,48 @@
|
|
|
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.patcher import (
|
|
12
|
+
FilePatchSpec,
|
|
13
|
+
PatchHunk,
|
|
14
|
+
apply_semantic_patch,
|
|
15
|
+
plan_semantic_patch,
|
|
16
|
+
preview_semantic_patch,
|
|
17
|
+
)
|
|
18
|
+
from codegraph.refactor.renamer import (
|
|
19
|
+
execute_safe_rename,
|
|
20
|
+
plan_safe_rename,
|
|
21
|
+
preview_safe_rename,
|
|
22
|
+
)
|
|
23
|
+
from codegraph.refactor.transactions import (
|
|
24
|
+
RefactorLock,
|
|
25
|
+
RefactorLockError,
|
|
26
|
+
commit_refactor_transaction,
|
|
27
|
+
rollback_refactor,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"FilePatchSpec",
|
|
32
|
+
"PatchHunk",
|
|
33
|
+
"RefactorLock",
|
|
34
|
+
"RefactorLockError",
|
|
35
|
+
"RefactorPlan",
|
|
36
|
+
"RefactorResult",
|
|
37
|
+
"RefactorRisk",
|
|
38
|
+
"RefactorStatus",
|
|
39
|
+
"TokenReplacementSpan",
|
|
40
|
+
"apply_semantic_patch",
|
|
41
|
+
"commit_refactor_transaction",
|
|
42
|
+
"execute_safe_rename",
|
|
43
|
+
"plan_safe_rename",
|
|
44
|
+
"plan_semantic_patch",
|
|
45
|
+
"preview_safe_rename",
|
|
46
|
+
"preview_semantic_patch",
|
|
47
|
+
"rollback_refactor",
|
|
48
|
+
]
|
|
@@ -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
|
+
}
|