How the code‑review‑graph Refactoring Tool Previews and Applies Code Changes Safely
The code-review-graph tool uses a two‑stage preview‑then‑apply workflow with expiry‑based sessions, path‑traversal guards, and dry‑run diffs to ensure every refactor is verified before any file is modified.
This article explains how tirth8205/code-review-graph implements safe, reversible code refactoring through its Model Context Protocol (MCP) tools. The system separates preview generation from change application, giving developers full visibility into edits before they touch disk.
The Two‑Tool Architecture
The refactoring system is exposed through two coordinated tools in code_review_graph/tools/refactor_tools.py:
| Tool | Purpose |
|---|---|
refactor_tool |
Generates a preview of rename operations, dead‑code reports, or community‑driven suggestions |
apply_refactor_tool |
Commits a previously‑previewed edit set, with mandatory validation and optional dry‑run |
Both delegate to the core logic in code_review_graph/refactor.py, which orchestrates graph‑driven analysis and safe file operations.
Generating a Refactor Preview
When mode="rename" is requested, refactor_func invokes rename_preview (lines [70‑86] of refactor_tools.py). The preview pipeline executes four distinct phases:
Node Lookup via GraphStore
The system queries store.search_nodes(old_name, limit=10) to locate the target symbol, preferring exact matches over partial results (lines [73‑94] in refactor.py). The GraphStore—defined in code_review_graph/graph.py—maintains nodes for functions, classes, and variables with typed edges (CALLS, IMPORTS_FROM, INHERITS) that enable precise impact analysis.
Edit List Construction
For every match, the system collects:
- Definition sites where the symbol is declared
- Call sites where the symbol is invoked
- Import sites where the symbol is referenced
These are assembled into dictionaries containing file, line, old_text, new_text, and a confidence flag (lines [100‑155]).
Statistical Summary and Refactor ID
Confidence counters are aggregated (lines [158‑162]), and a short UUID (refactor_id = uuid.uuid4().hex[:8]) uniquely identifies the preview session (lines [162‑166]).
Thread‑Safe Storage
The preview dict is stored in the global _pending_refactors dictionary, guarded by _refactor_lock. Expired entries are purged via _cleanup_expired on every operation (lines [172‑176]):
# Simplified from code_review_graph/refactor.py
with _refactor_lock:
_pending_refactors[refactor_id] = {
"created_at": time.time(),
"edits": edit_list,
"summary": summary_dict
}
_cleanup_expired()
The response includes actionable next steps:
{
"status": "ok",
"summary": "Rename preview: Foo → Bar, 12 edit(s). Use apply_refactor_tool(refactor_id='a1b2c3d4') to apply.",
"refactor_id": "a1b2c3d4",
"edits": [
{"file": "src/foo.py", "line": 15, "old_text": "def Foo", "new_text": "def Bar", "confidence": 1.0}
]
}
Dead‑code (mode="dead_code") and suggestion (mode="suggest") modes follow identical patterns through find_dead_code and suggest_refactorings (lines [91‑120]).
Safely Applying Refactor Changes
apply_refactor_func delegates to apply_refactor in refactor.py, which enforces six sequential safety checks before any file modification:
| Safety Mechanism | Implementation Details |
|---|---|
| Repository root resolution | find_project_root() or user‑provided repo_root resolved to absolute Path (lines [59‑63] in refactor.py; also defined in code_review_graph/incremental.py) |
| Refactor ID validation | Pending preview fetched from _pending_refactors; missing IDs raise ValueError (lines [49‑57]) |
| Expiry enforcement | REFACTOR_EXPIRY_SECONDS = 600 (10 minutes) checked; expired previews removed and rejected (lines [54‑60]) |
| Path‑traversal protection | Every target file resolved and verified inside repo root via Path.relative_to; escaping paths abort operation (lines [71‑77]) |
| Dry‑run mode | When dry_run=True, difflib.unified_diff generates per‑file diffs without disk writes; preview preserved for subsequent real apply (lines [86‑101]) |
| Atomic multi‑edit handling | Edits grouped by file, applied to in‑memory copy, then written once; prevents later edits overwriting earlier ones (lines [90‑104]) |
Dry‑Run Example
from code_review_graph.main import apply_refactor_tool
dry = apply_refactor_tool(
refactor_id="9f3a1c7b",
repo_root="/path/to/repo",
dry_run=True
)
print(dry["would_modify"]) # => ["src/services/user_service.py", "tests/test_user_service.py"]
print(dry["diffs"]["src/services/user_service.py"])
The dry‑run response structure (lines [96‑101]):
{
"status": "ok",
"dry_run": true,
"applied": 0,
"edits_applied": 12,
"would_modify": ["src/foo.py", "tests/foo_test.py"],
"diffs": {
"src/foo.py": "@@ -1,4 +1,4 @@\n- old_name\n+ new_name\n"
}
}
Committing Changes
When dry_run=False, the system:
- Writes all files atomically (lines [124‑135])
- Removes the preview from
_pending_refactorsto prevent replay - Returns
files_modifiedlist andedits_appliedcount
Errors during write are logged and reported per‑file without halting the entire operation.
Complete Workflow Example
Step 1: Preview a Rename
from code_review_graph.main import refactor_tool
preview = refactor_tool(
mode="rename",
old_name="UserService",
new_name="AccountService",
repo_root="/path/to/repo"
)
refactor_id = preview["refactor_id"] # e.g., '9f3a1c7b'
print(preview["summary"]) # "Rename preview: UserService → AccountService, 12 edit(s)..."
Step 2: Verify with Dry‑Run
from code_review_graph.main import apply_refactor_tool
verification = apply_refactor_tool(
refactor_id=refactor_id,
repo_root="/path/to/repo",
dry_run=True
)
assert verification["status"] == "ok"
for path, diff in verification["diffs"].items():
print(f"\n=== {path} ===\n{diff}")
Step 3: Apply Changes
result = apply_refactor_tool(
refactor_id=refactor_id,
repo_root="/path/to/repo",
dry_run=False
)
print(f"Modified {len(result['files_modified'])} files")
# Preview automatically invalidated; refactor_id cannot be reused
Key Architectural Safeguards
The graph‑driven analysis in GraphStore ensures no reference is missed. All relationships—calls, imports, inheritance—are explicitly modeled, eliminating the blind spots of text‑only search.
The thread‑safe pending store uses threading.Lock to protect _pending_refactors. Concurrent users receive isolated preview sessions with automatic expiry cleanup.
The defense‑in‑depth validation combines time limits, path containment, and dry‑run verification—no single point of failure can bypass the preview requirement.
Summary
refactor_toolgenerates graph‑aware previews with unique, expiring IDs stored in a thread‑safe registryapply_refactor_toolenforces 10‑minute TTL, path‑traversal guards, and mandatory dry‑run option before any file modification- All edits are grouped by file and applied to memory first, preventing partial or conflicting writes
- The GraphStore enables precise impact analysis through explicitly modeled code relationships rather than fragile text matching
Frequently Asked Questions
What happens if I try to apply an expired refactor preview?
The operation fails with an error. apply_refactor checks REFACTOR_EXPIRY_SECONDS (600 seconds by default) and removes expired entries from _pending_refactors before validation. You must regenerate the preview using refactor_tool.
Can multiple developers use the refactoring tools simultaneously?
Yes. The _pending_refactors dictionary is protected by _refactor_lock, ensuring thread‑safe concurrent access. Each preview receives a unique UUID, so developers' sessions remain isolated even on shared infrastructure.
Why does dry‑run mode preserve the preview while real apply deletes it?
Dry‑run intentionally leaves the preview intact so you can inspect difflib.unified_diff output and immediately follow up with dry_run=False. The real apply path removes the entry from _pending_refactors to prevent accidental replay attacks or duplicate modifications.
Does the tool protect against directory traversal attacks?
Absolutely. Every target file path is resolved and verified to be within the repository root using Path.relative_to (lines [71‑77] in refactor.py). Any path that escapes the root—via ../ sequences or symlinks—triggers immediate abortion with an error.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →