codegraph-engine 3.0.0__1-py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -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/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Repository-scoped refactor transaction manager and atomic rollback engine."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import time
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import fcntl
|
|
15
|
+
except ImportError: # pragma: no cover - Windows
|
|
16
|
+
fcntl = None # type: ignore[assignment]
|
|
17
|
+
|
|
18
|
+
from codegraph.errors import SecurityError
|
|
19
|
+
from codegraph.refactor.models import (
|
|
20
|
+
RefactorPlan,
|
|
21
|
+
RefactorResult,
|
|
22
|
+
RefactorRisk,
|
|
23
|
+
RefactorStatus,
|
|
24
|
+
)
|
|
25
|
+
from codegraph.security.paths import is_sensitive_path, safe_path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RefactorLockError(Exception):
|
|
29
|
+
"""Raised when repository refactoring lock cannot be acquired."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RefactorLock:
|
|
33
|
+
"""Repository-scoped file lock for atomic refactor operations."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, repository: Path, timeout_seconds: float = 5.0) -> None:
|
|
36
|
+
self.repository = repository
|
|
37
|
+
self.lock_dir = repository / ".codegraph"
|
|
38
|
+
self.lock_file = self.lock_dir / "refactor.lock"
|
|
39
|
+
self.timeout_seconds = timeout_seconds
|
|
40
|
+
self._fd: int | None = None
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> RefactorLock:
|
|
43
|
+
self.acquire()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
47
|
+
self.release()
|
|
48
|
+
|
|
49
|
+
def acquire(self) -> None:
|
|
50
|
+
self.lock_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
start = time.time()
|
|
52
|
+
while True:
|
|
53
|
+
try:
|
|
54
|
+
if fcntl is not None:
|
|
55
|
+
self._fd = os.open(
|
|
56
|
+
str(self.lock_file),
|
|
57
|
+
os.O_RDWR | os.O_CREAT | os.O_TRUNC,
|
|
58
|
+
0o644,
|
|
59
|
+
)
|
|
60
|
+
fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
61
|
+
else:
|
|
62
|
+
self._fd = os.open(
|
|
63
|
+
str(self.lock_file),
|
|
64
|
+
os.O_RDWR | os.O_CREAT | os.O_EXCL,
|
|
65
|
+
0o644,
|
|
66
|
+
)
|
|
67
|
+
# Write current PID into lock file
|
|
68
|
+
os.write(self._fd, f"{os.getpid()}:{datetime.now(UTC).isoformat()}".encode())
|
|
69
|
+
return
|
|
70
|
+
except (OSError, BlockingIOError) as err:
|
|
71
|
+
if self._fd is not None:
|
|
72
|
+
try:
|
|
73
|
+
os.close(self._fd)
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
76
|
+
self._fd = None
|
|
77
|
+
if time.time() - start >= self.timeout_seconds:
|
|
78
|
+
raise RefactorLockError(
|
|
79
|
+
f"CONCURRENT_REFACTOR_IN_PROGRESS: Could not acquire refactor lock for {self.repository} "
|
|
80
|
+
f"within {self.timeout_seconds}s."
|
|
81
|
+
) from err
|
|
82
|
+
time.sleep(0.05)
|
|
83
|
+
|
|
84
|
+
def release(self) -> None:
|
|
85
|
+
if self._fd is not None:
|
|
86
|
+
try:
|
|
87
|
+
if fcntl is not None:
|
|
88
|
+
fcntl.flock(self._fd, fcntl.LOCK_UN)
|
|
89
|
+
os.close(self._fd)
|
|
90
|
+
except OSError:
|
|
91
|
+
pass
|
|
92
|
+
self._fd = None
|
|
93
|
+
if self.lock_file.exists():
|
|
94
|
+
try:
|
|
95
|
+
self.lock_file.unlink()
|
|
96
|
+
except OSError:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def compute_sha256(content: str | bytes) -> str:
|
|
101
|
+
"""Compute deterministic SHA-256 hash."""
|
|
102
|
+
if isinstance(content, str):
|
|
103
|
+
content = content.encode("utf-8")
|
|
104
|
+
return hashlib.sha256(content).hexdigest()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_manifests_dir(repository: Path) -> Path:
|
|
108
|
+
"""Get or create the transaction manifests storage directory."""
|
|
109
|
+
manifests_dir = repository / ".codegraph" / "refactor_manifests"
|
|
110
|
+
manifests_dir.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
return manifests_dir
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def write_manifest(repository: Path, manifest_data: dict[str, Any]) -> Path:
|
|
115
|
+
"""Write a persistent transaction manifest."""
|
|
116
|
+
manifests_dir = get_manifests_dir(repository)
|
|
117
|
+
manifest_path = manifests_dir / f"{manifest_data['transaction_id']}.json"
|
|
118
|
+
with open(manifest_path, "w", encoding="utf-8") as f:
|
|
119
|
+
json.dump(manifest_data, f, indent=2)
|
|
120
|
+
return manifest_path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def read_manifest(repository: Path, transaction_id: str) -> dict[str, Any] | None:
|
|
124
|
+
"""Read a transaction manifest by ID."""
|
|
125
|
+
manifests_dir = get_manifests_dir(repository)
|
|
126
|
+
manifest_path = manifests_dir / f"{transaction_id}.json"
|
|
127
|
+
if not manifest_path.exists():
|
|
128
|
+
return None
|
|
129
|
+
try:
|
|
130
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
131
|
+
data = json.load(f)
|
|
132
|
+
if isinstance(data, dict):
|
|
133
|
+
return data
|
|
134
|
+
return None
|
|
135
|
+
except Exception:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def commit_refactor_transaction(
|
|
140
|
+
repository: Path,
|
|
141
|
+
plan: RefactorPlan,
|
|
142
|
+
modified_buffers: dict[str, str],
|
|
143
|
+
) -> RefactorResult:
|
|
144
|
+
"""Atomically commit all modified file buffers to disk with full rollback manifest.
|
|
145
|
+
|
|
146
|
+
Invariants:
|
|
147
|
+
1. Repository-scoped lock acquired.
|
|
148
|
+
2. All original file hashes re-validated against plan.
|
|
149
|
+
3. Safe path and boundary validation enforced.
|
|
150
|
+
4. Modified contents written to sibling temporary files.
|
|
151
|
+
5. Atomic replacement using os.replace().
|
|
152
|
+
6. Rollback manifest persisted.
|
|
153
|
+
"""
|
|
154
|
+
repo_root = repository.resolve()
|
|
155
|
+
temp_files_created: list[Path] = []
|
|
156
|
+
original_contents: dict[str, str] = {}
|
|
157
|
+
replacement_hashes: dict[str, str] = {}
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
with RefactorLock(repo_root):
|
|
161
|
+
# 1. Precondition check: verify file existence, safety, and unchanged hashes
|
|
162
|
+
for rel_path in plan.files:
|
|
163
|
+
if is_sensitive_path(rel_path):
|
|
164
|
+
return RefactorResult(
|
|
165
|
+
status=RefactorStatus.BLOCKED,
|
|
166
|
+
target_symbol=plan.target_symbol,
|
|
167
|
+
new_name=plan.new_name,
|
|
168
|
+
risk=RefactorRisk.HIGH,
|
|
169
|
+
errors=[f"SENSITIVE_FILE_ACCESS_DENIED: File '{rel_path}' is sensitive."],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
abs_path = safe_path(repo_root, rel_path)
|
|
174
|
+
except SecurityError as sec_err:
|
|
175
|
+
return RefactorResult(
|
|
176
|
+
status=RefactorStatus.BLOCKED,
|
|
177
|
+
target_symbol=plan.target_symbol,
|
|
178
|
+
new_name=plan.new_name,
|
|
179
|
+
risk=RefactorRisk.HIGH,
|
|
180
|
+
errors=[f"SECURITY_ERROR: {sec_err}"],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if not abs_path.exists() or not abs_path.is_file():
|
|
184
|
+
return RefactorResult(
|
|
185
|
+
status=RefactorStatus.ERROR,
|
|
186
|
+
target_symbol=plan.target_symbol,
|
|
187
|
+
new_name=plan.new_name,
|
|
188
|
+
risk=RefactorRisk.HIGH,
|
|
189
|
+
errors=[f"FILE_NOT_FOUND: Target file '{rel_path}' does not exist on disk."],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
current_bytes = abs_path.read_bytes()
|
|
193
|
+
current_hash = hashlib.sha256(current_bytes).hexdigest()
|
|
194
|
+
expected_hash = plan.original_file_hashes.get(rel_path)
|
|
195
|
+
|
|
196
|
+
if expected_hash and current_hash != expected_hash:
|
|
197
|
+
return RefactorResult(
|
|
198
|
+
status=RefactorStatus.ERROR,
|
|
199
|
+
target_symbol=plan.target_symbol,
|
|
200
|
+
new_name=plan.new_name,
|
|
201
|
+
risk=RefactorRisk.HIGH,
|
|
202
|
+
errors=[
|
|
203
|
+
f"FILE_CHANGED_SINCE_PLAN: File '{rel_path}' has been modified externally since plan generation. Please re-plan."
|
|
204
|
+
],
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
original_contents[rel_path] = current_bytes.decode("utf-8")
|
|
209
|
+
except UnicodeDecodeError:
|
|
210
|
+
original_contents[rel_path] = current_bytes.decode("latin1")
|
|
211
|
+
|
|
212
|
+
# 2. Stage all modifications to sibling temporary files
|
|
213
|
+
for rel_path in plan.files:
|
|
214
|
+
abs_path = safe_path(repo_root, rel_path)
|
|
215
|
+
parent_dir = abs_path.parent
|
|
216
|
+
temp_file = parent_dir / f".tmp_cg_refactor_{plan.transaction_id}_{abs_path.name}"
|
|
217
|
+
temp_files_created.append(temp_file)
|
|
218
|
+
|
|
219
|
+
new_text = modified_buffers[rel_path]
|
|
220
|
+
replacement_hashes[rel_path] = compute_sha256(new_text)
|
|
221
|
+
|
|
222
|
+
# Write content to sibling temp file preserving exact line endings
|
|
223
|
+
temp_file.write_bytes(new_text.encode("utf-8"))
|
|
224
|
+
|
|
225
|
+
# Preserve original permissions
|
|
226
|
+
try:
|
|
227
|
+
shutil.copymode(abs_path, temp_file)
|
|
228
|
+
except OSError:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
# 3. All temp files written successfully — atomically replace original files
|
|
232
|
+
for rel_path in plan.files:
|
|
233
|
+
abs_path = safe_path(repo_root, rel_path)
|
|
234
|
+
temp_file = abs_path.parent / f".tmp_cg_refactor_{plan.transaction_id}_{abs_path.name}"
|
|
235
|
+
os.replace(temp_file, abs_path)
|
|
236
|
+
|
|
237
|
+
# 4. Save transaction manifest
|
|
238
|
+
manifest = {
|
|
239
|
+
"transaction_id": plan.transaction_id,
|
|
240
|
+
"target_symbol": plan.target_symbol,
|
|
241
|
+
"new_name": plan.new_name,
|
|
242
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
243
|
+
"status": "APPLIED",
|
|
244
|
+
"files": plan.files,
|
|
245
|
+
"original_hashes": plan.original_file_hashes,
|
|
246
|
+
"replacement_hashes": replacement_hashes,
|
|
247
|
+
"original_contents": original_contents,
|
|
248
|
+
"spans_count": len(plan.spans),
|
|
249
|
+
}
|
|
250
|
+
write_manifest(repo_root, manifest)
|
|
251
|
+
|
|
252
|
+
return RefactorResult(
|
|
253
|
+
status=RefactorStatus.APPLIED,
|
|
254
|
+
target_symbol=plan.target_symbol,
|
|
255
|
+
new_name=plan.new_name,
|
|
256
|
+
risk=plan.risk,
|
|
257
|
+
uncertainty_reasons=plan.uncertainty_reasons,
|
|
258
|
+
files_changed=len(plan.files),
|
|
259
|
+
symbols_changed=1,
|
|
260
|
+
spans_count=len(plan.spans),
|
|
261
|
+
tests_affected=plan.affected_tests,
|
|
262
|
+
routes_affected=plan.affected_routes,
|
|
263
|
+
db_affected=plan.affected_db_relationships,
|
|
264
|
+
rollback_id=plan.transaction_id,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
except RefactorLockError as lock_err:
|
|
268
|
+
return RefactorResult(
|
|
269
|
+
status=RefactorStatus.BLOCKED,
|
|
270
|
+
target_symbol=plan.target_symbol,
|
|
271
|
+
new_name=plan.new_name,
|
|
272
|
+
risk=RefactorRisk.HIGH,
|
|
273
|
+
errors=[str(lock_err)],
|
|
274
|
+
)
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
# Clean up any leftover temp files
|
|
277
|
+
for tmp in temp_files_created:
|
|
278
|
+
if tmp.exists():
|
|
279
|
+
try:
|
|
280
|
+
tmp.unlink()
|
|
281
|
+
except OSError:
|
|
282
|
+
pass
|
|
283
|
+
return RefactorResult(
|
|
284
|
+
status=RefactorStatus.ERROR,
|
|
285
|
+
target_symbol=plan.target_symbol,
|
|
286
|
+
new_name=plan.new_name,
|
|
287
|
+
risk=RefactorRisk.HIGH,
|
|
288
|
+
errors=[f"TRANSACTION_FAILED: {exc}"],
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def rollback_refactor(repository: Path, transaction_id: str) -> RefactorResult:
|
|
293
|
+
"""Atomically rollback an applied refactoring transaction.
|
|
294
|
+
|
|
295
|
+
Invariants:
|
|
296
|
+
1. Repository-scoped lock acquired.
|
|
297
|
+
2. Manifest must exist and be in APPLIED status.
|
|
298
|
+
3. Current file hashes must match expected post-refactor hashes.
|
|
299
|
+
If any file has changed externally, rollback is BLOCKED.
|
|
300
|
+
4. Restores original contents atomically using os.replace().
|
|
301
|
+
"""
|
|
302
|
+
repo_root = repository.resolve()
|
|
303
|
+
|
|
304
|
+
manifest = read_manifest(repo_root, transaction_id)
|
|
305
|
+
if not manifest:
|
|
306
|
+
return RefactorResult(
|
|
307
|
+
status=RefactorStatus.ERROR,
|
|
308
|
+
target_symbol="",
|
|
309
|
+
new_name="",
|
|
310
|
+
risk=RefactorRisk.HIGH,
|
|
311
|
+
errors=[f"ROLLBACK_MANIFEST_NOT_FOUND: No transaction manifest found for '{transaction_id}'."],
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if manifest.get("status") == "ROLLED_BACK":
|
|
315
|
+
return RefactorResult(
|
|
316
|
+
status=RefactorStatus.BLOCKED,
|
|
317
|
+
target_symbol=manifest.get("target_symbol", ""),
|
|
318
|
+
new_name=manifest.get("new_name", ""),
|
|
319
|
+
risk=RefactorRisk.LOW,
|
|
320
|
+
errors=[f"TRANSACTION_ALREADY_ROLLED_BACK: Transaction '{transaction_id}' was already rolled back."],
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
files = manifest.get("files", [])
|
|
324
|
+
replacement_hashes = manifest.get("replacement_hashes", {})
|
|
325
|
+
original_contents = manifest.get("original_contents", {})
|
|
326
|
+
|
|
327
|
+
temp_files_created: list[Path] = []
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
with RefactorLock(repo_root):
|
|
331
|
+
# 1. Verify that current file hashes match expected replacement hashes
|
|
332
|
+
for rel_path in files:
|
|
333
|
+
abs_path = safe_path(repo_root, rel_path)
|
|
334
|
+
if not abs_path.exists():
|
|
335
|
+
return RefactorResult(
|
|
336
|
+
status=RefactorStatus.ERROR,
|
|
337
|
+
target_symbol=manifest.get("target_symbol", ""),
|
|
338
|
+
new_name=manifest.get("new_name", ""),
|
|
339
|
+
risk=RefactorRisk.HIGH,
|
|
340
|
+
errors=[f"FILE_NOT_FOUND_FOR_ROLLBACK: Target file '{rel_path}' no longer exists on disk."],
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
current_hash = compute_sha256(abs_path.read_bytes())
|
|
344
|
+
expected_hash = replacement_hashes.get(rel_path)
|
|
345
|
+
|
|
346
|
+
if expected_hash and current_hash != expected_hash:
|
|
347
|
+
return RefactorResult(
|
|
348
|
+
status=RefactorStatus.BLOCKED,
|
|
349
|
+
target_symbol=manifest.get("target_symbol", ""),
|
|
350
|
+
new_name=manifest.get("new_name", ""),
|
|
351
|
+
risk=RefactorRisk.HIGH,
|
|
352
|
+
errors=[
|
|
353
|
+
f"ROLLBACK_BLOCKED_EXTERNAL_MODIFICATION: File '{rel_path}' was modified externally after the refactor was applied. "
|
|
354
|
+
f"Rollback cannot safely overwrite subsequent changes."
|
|
355
|
+
],
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# 2. Stage original contents to sibling temporary files
|
|
359
|
+
for rel_path in files:
|
|
360
|
+
abs_path = safe_path(repo_root, rel_path)
|
|
361
|
+
temp_file = abs_path.parent / f".tmp_cg_rollback_{transaction_id}_{abs_path.name}"
|
|
362
|
+
temp_files_created.append(temp_file)
|
|
363
|
+
|
|
364
|
+
orig_text = original_contents.get(rel_path, "")
|
|
365
|
+
temp_file.write_bytes(orig_text.encode("utf-8"))
|
|
366
|
+
try:
|
|
367
|
+
shutil.copymode(abs_path, temp_file)
|
|
368
|
+
except OSError:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
# 3. Atomically restore original files
|
|
372
|
+
for rel_path in files:
|
|
373
|
+
abs_path = safe_path(repo_root, rel_path)
|
|
374
|
+
temp_file = abs_path.parent / f".tmp_cg_rollback_{transaction_id}_{abs_path.name}"
|
|
375
|
+
os.replace(temp_file, abs_path)
|
|
376
|
+
|
|
377
|
+
# 4. Mark manifest as ROLLED_BACK
|
|
378
|
+
manifest["status"] = "ROLLED_BACK"
|
|
379
|
+
manifest["rolled_back_at"] = datetime.now(UTC).isoformat()
|
|
380
|
+
write_manifest(repo_root, manifest)
|
|
381
|
+
|
|
382
|
+
return RefactorResult(
|
|
383
|
+
status=RefactorStatus.ROLLED_BACK,
|
|
384
|
+
target_symbol=manifest.get("target_symbol", ""),
|
|
385
|
+
new_name=manifest.get("new_name", ""),
|
|
386
|
+
risk=RefactorRisk.LOW,
|
|
387
|
+
files_changed=len(files),
|
|
388
|
+
symbols_changed=1,
|
|
389
|
+
spans_count=manifest.get("spans_count", 0),
|
|
390
|
+
rollback_id=transaction_id,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
except RefactorLockError as lock_err:
|
|
394
|
+
return RefactorResult(
|
|
395
|
+
status=RefactorStatus.BLOCKED,
|
|
396
|
+
target_symbol=manifest.get("target_symbol", ""),
|
|
397
|
+
new_name=manifest.get("new_name", ""),
|
|
398
|
+
risk=RefactorRisk.HIGH,
|
|
399
|
+
errors=[str(lock_err)],
|
|
400
|
+
)
|
|
401
|
+
except Exception as exc:
|
|
402
|
+
for tmp in temp_files_created:
|
|
403
|
+
if tmp.exists():
|
|
404
|
+
try:
|
|
405
|
+
tmp.unlink()
|
|
406
|
+
except OSError:
|
|
407
|
+
pass
|
|
408
|
+
return RefactorResult(
|
|
409
|
+
status=RefactorStatus.ERROR,
|
|
410
|
+
target_symbol=manifest.get("target_symbol", ""),
|
|
411
|
+
new_name=manifest.get("new_name", ""),
|
|
412
|
+
risk=RefactorRisk.HIGH,
|
|
413
|
+
errors=[f"ROLLBACK_FAILED: {exc}"],
|
|
414
|
+
)
|