How FileEditor Performs AST‑Based Surgical Code Patching in code‑graph‑rag

FileEditor uses tree‑sitter AST parsing and byte‑level node offsets to apply precise, language‑aware code edits without line‑number guessing.

The FileEditor tool in the code‑graph‑rag repository enables retrieval‑augmented generation systems to modify source files with surgical precision. By anchoring edits to AST node boundaries rather than fragile line numbers, it ensures patches survive reflows, comment changes, and surrounding code motion. This article breaks down the four‑stage pipeline implemented across tools/file_editor.py and its supporting modules.

Parse Stage: From Source Bytes to Tree‑Sitter AST

Every edit begins with deterministic parsing. The codebase_rag.utils.source_extraction module wraps tree‑sitter to return a syntax tree where each node carries exact byte start/end offsets into the original file.

from codebase_rag.utils.source_extraction import TreeSitterParser

parser = TreeSitterParser.for_language("python")
tree = parser.parse_bytes(source_bytes)  # Root node spans entire file

Unlike regex or line‑based approaches, this guarantees that structural boundaries—function bodies, class definitions, import blocks—are identified correctly regardless of formatting style.

Locate Stage: Resolving Qualified Names to AST Nodes

With the AST in hand, FileEditor delegates node lookup to codebase_rag.graph_updater. The find_node_by_qualified_name method traverses the hierarchy using scoped identifiers like my_pkg.my_mod.my_func.

from codebase_rag.graph_updater import GraphUpdater

updater = GraphUpdater(ast_cache)
target_node = updater.find_node_by_qualified_name("utils.helper.process_data")

# Returns: node with .start_byte and .end_byte attributes

This resolution is cache‑aware: the BoundedASTCache stores parsed trees indexed by file path, so repeated edits to the same file avoid redundant parsing.

Diff Stage: Byte‑Level Patch Generation

The core surgical operation lives in tools/file_editor.py. The apply_patch_to_file method:

  1. Extracts the original source slice using node.start_byte and node.end_byte
  2. Generates a difflib.unified_diff between old and new text
  3. Applies the diff in‑place while preserving surrounding whitespace and comments
from tools.file_editor import FileEditor

editor = FileEditor(project_root="/path/to/project")

result = editor.replace_code_surgically(
    qualified_name="utils.helper.process_data",
    new_source="""
def process_data(items):
    # Optimized path

    return [x.value for x in items if x.active]
"""
)

Because the patch boundaries derive from AST offsets, the edit never drifts—even if imports are added above or docstrings reformatted below the target function.

Verify Stage: Post‑Edit AST Validation

FileEditor re‑patches defensively. After writing the file, it:

  • Re‑parses via source_extraction.parse_file
  • Verifies the modified node's type matches expectations
  • Rolls back and raises on syntax corruption

# Simplified validation logic from file_editor.py

new_tree = parser.parse_file(file_path)
new_node = find_corresponding_node(new_tree, qualified_name)
assert new_node.type == original_node.type, "AST structure corrupted"

This atomicity ensures the codebase remains valid after every operation.

Practical Examples

Rename a Function Implementation

editor.replace_code_surgically(
    qualified_name="analytics.metrics.compute_score",
    new_source="""
def compute_score(predictions, labels):
    return f1_score(labels, predictions, average="weighted")
"""
)

The AST node for compute_score is located, its entire definition replaced, and the file re‑parsed to confirm the function still exists.

Insert an Import Statement

editor.edit_file(
    file_path="src/main.py",
    before_node="import os",  # AST node reference, not line number

    new_source="import pathlib\n",
)

before_node resolves to an actual import node; the insertion offset is computed from node.start_byte, ensuring correct placement even with multiple import groups.

Delete an Obsolete Constant

editor.replace_code_surgically(
    qualified_name="config.LEGACY_TIMEOUT",
    new_source="",  # Empty replacement = deletion

)

The AST node spanning the constant definition is excised precisely, leaving surrounding assignments intact.

Key Source Files

File Purpose
tools/file_editor.py FileEditor class, apply_patch_to_file, high‑level edit API
codebase_rag/utils/source_extraction.py Tree‑sitter parsing, byte‑offset extraction
codebase_rag/graph_updater.py Node lookup by qualified name, cache invalidation
codebase_rag/graph_loader.py Bulk AST loading into BoundedASTCache
codebase_rag/types_defs.py ASTCacheProtocol, node type definitions

Summary

  • AST parsing via tree‑sitter eliminates line‑number fragility
  • Byte‑level offsets from source_extraction anchor every edit
  • GraphUpdater resolves qualified names to concrete nodes
  • apply_patch_to_file generates and applies unified diffs surgically
  • Re‑parsing validation guarantees syntactic integrity after changes
  • Cache invalidation keeps the RAG index synchronized with file state

Frequently Asked Questions

What makes AST‑based patching "surgical"?

Traditional tools rely on line numbers or pattern matching, which break when code is reformatted. AST‑based patching uses byte offsets tied to syntax nodes, so edits target exact structural boundaries—functions, classes, imports—regardless of surrounding changes.

Does FileEditor support languages other than Python?

The architecture is language‑agnostic through tree‑sitter. TreeSitterParser.for_language() accepts any grammar the tree‑sitter ecosystem provides. The code‑graph‑rag repository currently optimizes for Python, but the source_extraction module is designed for multi‑language extension.

How does FileEditor handle patch conflicts?

Edits are atomic: the file is read, modified, and written within a single operation. If re‑parsing fails, the original file is restored. Concurrent modifications are not handled—FileEditor assumes single‑process access or external file locking.

What happens to the RAG index after a file edit?

GraphUpdater invalidates affected entries in BoundedASTCache and triggers incremental re‑indexing. Subsequent retrievals see the updated AST immediately, keeping the knowledge graph consistent with the source codebase.

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 →