codegraph-engine 3.0.0__2-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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. 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
+ )