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:

  1. Writes all files atomically (lines [124‑135])
  2. Removes the preview from _pending_refactors to prevent replay
  3. Returns files_modified list and edits_applied count

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_tool generates graph‑aware previews with unique, expiring IDs stored in a thread‑safe registry
  • apply_refactor_tool enforces 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →