band-aider-pub/band_aider/judgement/code_editor.py
2026-10-17 11:57:37 -07:00

63 lines
2.1 KiB
Python

# SPDX-FileCopyrightText: 2026 Art Sea LLC
#
# SPDX-License-Identifier: AGPL-3.0-only
from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Protocol, runtime_checkable
if TYPE_CHECKING:
from band_aider.judgement.edit_request import AiderEditRequest
@dataclass(frozen=True)
class EditorRecord:
"""Keep one applied block verbatim; ``record.render()`` presents neutral history."""
summary: str
search: str
replace: str
def render(self) -> str:
"""Quote historical text without emitting executable editor control markers."""
tokens = ("<<<<<<< SEARCH", "=======", ">>>>>>> REPLACE", "FILE:",
"SUMMARY:", "NO_PATCH:", "[CODE_EDIT_OR_LEAVE_IT:LEAVE_IT]", "== BEGIN ", "== END ")
sections: list[str] = []
# Keep the stored payload exact while making protocol-shaped history inert in prompts.
for label, content in (("Summary", self.summary), ("Before", self.search), ("After", self.replace)):
# Escape the first character so even markers embedded in source cannot become control.
for token in tokens:
content = content.replace(token, f"\\u{ord(token[0]):04x}" + token[1:])
sections.append(f"{label}:\n{content}")
return "\n\n".join(sections)
@dataclass(frozen=True)
class CodeEditorResult:
"""Return edit effects and retained history; callers read ``result.records``."""
changed_paths: list[Path]
summary: str
records: list[EditorRecord]
@runtime_checkable
class CodeEditor(Protocol):
"""Apply a bounded request; ``editor.edit(request)`` returns its effects and history."""
@property
def inner(self) -> "CodeEditor | None":
"""Return the wrapped editor for decorators, or None for concrete editors."""
def edit(
self,
request: AiderEditRequest,
logger: logging.Logger | None = None,
) -> CodeEditorResult:
"""Apply one edit request and return changed paths, summary, and retained records."""
...