Patch Engine¶
Safe code modifications with preview and rollback.
Overview¶
The Patch Engine modifies existing code safely:
- Preview changes before applying
- Automatic backups for rollback
- Syntax validation before save
- Conflict detection for concurrent changes
from aksara.ai import PatchEngine
engine = PatchEngine()
patch = await engine.create_patch(
file="models.py",
instruction="Add 'is_featured' boolean field to Post model"
)
await engine.apply(patch)
Quick Start¶
Create a Patch¶
from aksara.ai import PatchEngine
engine = PatchEngine()
# Create patch from instruction
patch = await engine.create_patch(
file="models.py",
instruction="Add email_verified boolean field to User"
)
print(patch.diff) # See what will change
Preview Changes¶
# Dry run - see changes without applying
result = await engine.apply(patch, dry_run=True)
print(result.diff)
print(result.before)
print(result.after)
Apply Changes¶
# Apply the patch
result = await engine.apply(patch)
if result.success:
print("Patch applied successfully")
else:
print(f"Failed: {result.error}")
Rollback¶
Creating Patches¶
From Instruction¶
patch = await engine.create_patch(
file="models.py",
instruction="Add phone_number field after email in User model"
)
From Code Diff¶
patch = await engine.create_patch_from_diff(
file="models.py",
old_code="email = fields.Email()",
new_code="""email = fields.Email()
phone = fields.String(max_length=20, null=True)"""
)
From Template¶
patch = await engine.create_patch_from_template(
file="models.py",
template="add_field",
params={
"model": "User",
"field_name": "phone",
"field_type": "String",
"field_args": {"max_length": 20, "null": True},
}
)
Multiple Files¶
patches = await engine.create_patches(
instructions=[
("models.py", "Add status field to Order"),
("serializers.py", "Include status in OrderSerializer"),
("viewsets.py", "Add filter by status to OrderViewSet"),
]
)
# Apply all
for patch in patches:
await engine.apply(patch)
Patch Object¶
Properties¶
patch = await engine.create_patch(...)
print(patch.file) # Target file path
print(patch.instruction) # Original instruction
print(patch.diff) # Unified diff format
print(patch.hunks) # Individual change hunks
print(patch.additions) # Lines added
print(patch.deletions) # Lines removed
print(patch.is_valid) # Syntax check passed
Diff Format¶
print(patch.diff)
# --- models.py (original)
# +++ models.py (modified)
# @@ -10,6 +10,7 @@
# class User(Model):
# email = fields.Email(unique=True)
# + phone = fields.String(max_length=20, null=True)
# name = fields.String(max_length=100)
Hunks¶
for hunk in patch.hunks:
print(f"Lines {hunk.start}-{hunk.end}")
print(f"Context: {hunk.context}")
print(f"Changes: {hunk.changes}")
Applying Patches¶
Basic Apply¶
result = await engine.apply(patch)
print(result.success) # True/False
print(result.file) # Modified file
print(result.backup_path) # Backup location
Dry Run¶
result = await engine.apply(patch, dry_run=True)
# No file changes, just preview
print(result.would_succeed)
print(result.diff)
Force Apply¶
With Backup¶
# Custom backup location
result = await engine.apply(
patch,
backup_dir="/tmp/backups",
)
print(f"Backup at: {result.backup_path}")
Validation¶
Syntax Validation¶
Patches are validated before applying:
patch = await engine.create_patch(
file="models.py",
instruction="Add invalid syntax"
)
if not patch.is_valid:
print(f"Invalid: {patch.validation_error}")
Import Validation¶
patch = await engine.create_patch(
file="viewsets.py",
instruction="Add permission class",
validate_imports=True, # Check imports exist
)
Test Validation¶
# Run tests after applying
result = await engine.apply(
patch,
run_tests=True,
test_command="pytest tests/test_models.py",
)
if not result.tests_passed:
await engine.rollback()
Rollback¶
Rollback Last¶
Rollback Specific¶
# By patch ID
await engine.rollback(patch_id="abc123")
# By file
await engine.rollback(file="models.py")
Rollback Multiple¶
View History¶
history = await engine.get_history()
for entry in history:
print(f"{entry.id}: {entry.file} - {entry.instruction}")
print(f" Applied: {entry.applied_at}")
print(f" Backup: {entry.backup_path}")
Conflict Handling¶
Detection¶
result = await engine.apply(patch)
if result.has_conflicts:
print("Conflicts detected:")
for conflict in result.conflicts:
print(f" Line {conflict.line}: {conflict.reason}")
Resolution¶
# Auto-resolve (use AI)
result = await engine.apply(
patch,
resolve_conflicts="auto",
)
# Manual resolution
if result.has_conflicts:
resolved = await engine.resolve_conflicts(
patch,
resolutions={
"line_15": "keep_theirs",
"line_20": "keep_ours",
}
)
await engine.apply(resolved)
Merge Strategy¶
result = await engine.apply(
patch,
merge_strategy="ours", # Keep our changes on conflict
# or "theirs", "union", "manual"
)
Batch Operations¶
Apply Multiple Patches¶
patches = [
await engine.create_patch("models.py", "Add field A"),
await engine.create_patch("models.py", "Add field B"),
await engine.create_patch("serializers.py", "Add fields to serializer"),
]
# Apply all or none
result = await engine.apply_batch(
patches,
atomic=True, # Rollback all if any fails
)
Transaction Mode¶
async with engine.transaction() as tx:
await tx.apply(patch1)
await tx.apply(patch2)
await tx.apply(patch3)
# All applied on exit, or all rolled back on error
CLI Usage¶
Create and Apply¶
Preview Only¶
Interactive Mode¶
aksara ai patch models.py "Add phone field" --interactive
Preview:
+ phone = fields.String(max_length=20, null=True)
Apply this change? [y/N/e(dit)]
From File¶
Integration¶
With Codegen¶
from aksara.ai import Codegen, PatchEngine
gen = Codegen()
engine = PatchEngine()
# Generate new code
new_code = await gen.model("Comment with text, author, post")
# Create patch to add it
patch = await engine.create_patch_from_code(
file="models.py",
code=new_code,
position="end", # Add at end of file
)
await engine.apply(patch)
With Planner¶
from aksara.ai import Planner, PatchEngine
planner = Planner()
engine = PatchEngine()
# Create plan
plan = await planner.create("Add tagging system")
# Execute patches from plan
for step in plan.steps:
if step.type == "patch":
patch = await engine.create_patch(
file=step.file,
instruction=step.instruction,
)
await engine.apply(patch)
Configuration¶
# settings.py
AKSARA = {
"AI_PATCH_ENGINE": {
# Validation
"validate_syntax": True,
"validate_imports": True,
"run_tests_after": False,
# Backups
"create_backups": True,
"backup_dir": ".aksara/backups",
"max_backup_age_days": 30,
# Conflicts
"default_merge_strategy": "manual",
# Safety
"require_dry_run_first": False,
"max_changes_per_patch": 100, # Lines
},
}
Best Practices¶
1. Always Preview First¶
result = await engine.apply(patch, dry_run=True)
print(result.diff) # Review before applying
await engine.apply(patch)
2. Use Atomic Batches¶
# Don't leave partial changes
async with engine.transaction():
await engine.apply(model_patch)
await engine.apply(serializer_patch)
await engine.apply(viewset_patch)
3. Keep Patches Small¶
# Better: multiple small patches
patch1 = await engine.create_patch(file, "Add field A")
patch2 = await engine.create_patch(file, "Add field B")
# Worse: one large patch
patch = await engine.create_patch(file, "Add fields A, B, C, D, E")