Skip to content

Index

axm_edit

axm-edit — Atomic batch file editing for AI agents.

Replace, create, and delete files in a single atomic operation.

Operation = Annotated[ReplaceOp | CreateOp | DeleteOp, Field(discriminator='op')] module-attribute

Discriminated union of all operation types.

BatchResult

Bases: BaseModel

Result of a batch edit operation.

Atomicity contract (see :func:axm_edit.core.engine.batch_apply): atomicity is guaranteed at validation (all-or-nothing — a validation error rejects the whole batch before any write); the apply phase is best-effort with automatic rollback-to-checkpoint, so on any mid-apply exception success is False, error is populated, checkpoint holds the targeted snapshot, and every touched path has been restored to its pre-batch state.

Attributes:

Name Type Description
success bool

Whether all operations were applied (False if a validation error rejected the batch or a mid-apply exception triggered a rollback).

checkpoint str | None

Targeted-path snapshot captured before apply, usable for rollback; present whenever the apply phase was entered.

applied int

Total number of individual edits applied.

summary dict[str, int]

Counts of modified, created, and deleted files.

error str | None

Human-readable error message on failure.

details list[ValidationError]

Detailed validation errors on failure.

Source code in packages/axm-edit/src/axm_edit/models/operations.py
Python
class BatchResult(BaseModel):  # type: ignore[explicit-any]  # pydantic synthesizes __init__(**data: Any)
    """Result of a batch edit operation.

    Atomicity contract (see :func:`axm_edit.core.engine.batch_apply`):
    atomicity is guaranteed at *validation* (all-or-nothing — a validation
    error rejects the whole batch before any write); the *apply* phase is
    best-effort with automatic rollback-to-checkpoint, so on any mid-apply
    exception ``success`` is ``False``, ``error`` is populated, ``checkpoint``
    holds the targeted snapshot, and every touched path has been restored to
    its pre-batch state.

    Attributes:
        success: Whether all operations were applied (``False`` if a
            validation error rejected the batch or a mid-apply exception
            triggered a rollback).
        checkpoint: Targeted-path snapshot captured before apply, usable for
            rollback; present whenever the apply phase was entered.
        applied: Total number of individual edits applied.
        summary: Counts of modified, created, and deleted files.
        error: Human-readable error message on failure.
        details: Detailed validation errors on failure.
    """

    success: bool
    checkpoint: str | None = None
    applied: int = 0
    summary: dict[str, int] = Field(
        default_factory=lambda: {"modified": 0, "created": 0, "deleted": 0},
    )
    error: str | None = None
    details: list[ValidationError] = Field(default_factory=list)
    lint_errors: list[str] = Field(default_factory=list)
    rollback_failed: bool = False

    model_config = {"extra": "forbid"}

CreateOp

Bases: BaseModel

Create a new file.

Fails if the file already exists unless overwrite is True.

Source code in packages/axm-edit/src/axm_edit/models/operations.py
Python
class CreateOp(BaseModel):  # type: ignore[explicit-any]  # pydantic synthesizes __init__(**data: Any)
    """Create a new file.

    Fails if the file already exists unless ``overwrite`` is True.
    """

    op: Literal["create"] = "create"
    file: str = Field(..., min_length=1, description="Relative path to the file")
    content: str = Field(..., description="Full file content")
    overwrite: bool = Field(default=False, description="Allow overwriting")

    model_config = {"extra": "forbid"}

DeleteOp

Bases: BaseModel

Delete an existing file.

Fails if the file does not exist.

Source code in packages/axm-edit/src/axm_edit/models/operations.py
Python
class DeleteOp(BaseModel):  # type: ignore[explicit-any]  # pydantic synthesizes __init__(**data: Any)
    """Delete an existing file.

    Fails if the file does not exist.
    """

    op: Literal["delete"] = "delete"
    file: str = Field(..., min_length=1, description="Relative path to the file")

    model_config = {"extra": "forbid"}

Edit

Bases: BaseModel

A single line-level edit within a replace operation.

Attributes:

Name Type Description
line int | None

Optional 1-indexed line hint in the original file. If provided, used as a starting point for searching old. If omitted, old is searched in the entire file.

old str

Expected content to find and replace (validation anchor). May contain \n for multi-line matches.

new str

Replacement content.

Source code in packages/axm-edit/src/axm_edit/models/operations.py
Python
class Edit(BaseModel):  # type: ignore[explicit-any]  # pydantic synthesizes __init__(**data: Any)
    """A single line-level edit within a replace operation.

    Attributes:
        line: Optional 1-indexed line hint in the **original** file.
              If provided, used as a starting point for searching ``old``.
              If omitted, ``old`` is searched in the entire file.
        old: Expected content to find and replace (validation anchor).
             May contain ``\\n`` for multi-line matches.
        new: Replacement content.
    """

    line: int | None = Field(
        default=None,
        ge=1,
        description="1-indexed line hint (optional — auto-search if omitted)",
    )
    old: str = Field(..., min_length=1, description="Expected content to find")
    new: str = Field(..., description="Replacement content")

    model_config = {"extra": "forbid"}

ReplaceOp

Bases: BaseModel

Modify lines in an existing file.

All line numbers reference the file as originally read, before any edits are applied. The engine sorts edits bottom-to-top to avoid line-shift problems.

Source code in packages/axm-edit/src/axm_edit/models/operations.py
Python
class ReplaceOp(BaseModel):  # type: ignore[explicit-any]  # pydantic synthesizes __init__(**data: Any)
    """Modify lines in an existing file.

    All line numbers reference the file **as originally read**, before any
    edits are applied.  The engine sorts edits bottom-to-top to avoid
    line-shift problems.
    """

    op: Literal["replace"] = "replace"
    file: str = Field(..., min_length=1, description="Relative path to the file")
    edits: list[Edit] = Field(..., min_length=1, description="List of line edits")

    model_config = {"extra": "forbid"}

RollbackResult

Bases: BaseModel

Outcome of a best-effort rollback.

Rollback is a strict inverse: it only undoes what the batch did. It is also best-effort — every captured path is attempted even if an earlier one fails, so a partial rollback is fully observable instead of aborting mid-loop. ok is True only when the snapshot was well-formed and every captured path was restored.

Attributes:

Name Type Description
restored list[str]

Relative paths successfully restored to their pre-batch state.

unrestored list[str]

Relative paths that could not be restored (a filesystem error was raised while undoing them).

valid bool

Whether the snapshot was well-formed and parseable.

Source code in packages/axm-edit/src/axm_edit/models/operations.py
Python
class RollbackResult(BaseModel):  # type: ignore[explicit-any]  # pydantic synthesizes __init__(**data: Any)
    """Outcome of a best-effort rollback.

    Rollback is a *strict inverse*: it only undoes what the batch did. It is
    also best-effort — every captured path is attempted even if an earlier one
    fails, so a partial rollback is fully observable instead of aborting
    mid-loop. ``ok`` is ``True`` only when the snapshot was well-formed and
    every captured path was restored.

    Attributes:
        restored: Relative paths successfully restored to their pre-batch state.
        unrestored: Relative paths that could not be restored (a filesystem
            error was raised while undoing them).
        valid: Whether the snapshot was well-formed and parseable.
    """

    restored: list[str] = Field(default_factory=list)
    unrestored: list[str] = Field(default_factory=list)
    valid: bool = True

    model_config = {"extra": "forbid"}

    @property
    def ok(self) -> bool:
        """True iff the snapshot was valid and nothing failed to restore."""
        return self.valid and not self.unrestored
ok property

True iff the snapshot was valid and nothing failed to restore.

ValidationError

Bases: BaseModel

A single validation failure.

Source code in packages/axm-edit/src/axm_edit/models/operations.py
Python
class ValidationError(BaseModel):  # type: ignore[explicit-any]  # pydantic synthesizes __init__(**data: Any)
    """A single validation failure."""

    file: str
    line: int | None = None
    expected: str | None = None
    actual: str | None = None
    error: str | None = None

    model_config = {"extra": "forbid"}

batch_apply(root, operations)

Validate and apply a batch of file operations.

Atomicity contract:

  • Validation is the gate. All operations are validated first; if any fails the batch is rejected wholesale (success=False) before a single byte is written — a true all-or-nothing guarantee.
  • Apply is best-effort with automatic rollback. Once validation passes, a targeted checkpoint of every touched path is captured and the operations are applied. If any exception occurs mid-apply (anchor drift, a write_text / unlink / mkdir failure, a permission error, …) the partial work is rolled back to that checkpoint — touched files are restored to their pre-batch bytes and batch-created files (and the empty directories created for them) are removed — and a failing BatchResult is returned. The filesystem is not made transactional at the OS level; the rollback restores only the snapshotted paths.

Parameters:

Name Type Description Default
root Path

Project root directory (all paths are relative to this).

required
operations Sequence[Operation]

List of replace, create, and delete operations.

required

Returns:

Type Description
BatchResult

BatchResult with success status, a targeted-path snapshot

BatchResult

(checkpoint) for rollback, and a summary.

Source code in packages/axm-edit/src/axm_edit/core/engine.py
Python
def batch_apply(root: Path, operations: Sequence[Operation]) -> BatchResult:
    """Validate and apply a batch of file operations.

    Atomicity contract:

    * **Validation is the gate.** All operations are validated first; if any
      fails the batch is rejected wholesale (``success=False``) before a
      single byte is written — a true all-or-nothing guarantee.
    * **Apply is best-effort with automatic rollback.** Once validation
      passes, a targeted checkpoint of every touched path is captured and the
      operations are applied. If *any* exception occurs mid-apply (anchor
      drift, a ``write_text`` / ``unlink`` / ``mkdir`` failure, a permission
      error, …) the partial work is rolled back to that checkpoint — touched
      files are restored to their pre-batch bytes and batch-created files (and
      the empty directories created for them) are removed — and a failing
      ``BatchResult`` is returned. The filesystem is *not* made transactional
      at the OS level; the rollback restores only the snapshotted paths.

    Args:
        root: Project root directory (all paths are relative to this).
        operations: List of replace, create, and delete operations.

    Returns:
        BatchResult with success status, a targeted-path snapshot
        (``checkpoint``) for rollback, and a summary.
    """
    root = root.resolve()
    grouped = _group_operations(root, operations)
    resolved_by_file, errors = _validate_all(root, grouped)

    if errors:
        return BatchResult(
            success=False,
            error="Validation failed",
            details=errors,
        )

    checkpoint = create_checkpoint(root, operations)

    try:
        total_applied = 0
        for file_rel, resolved in resolved_by_file.items():
            total_applied += _apply_replace(root, file_rel, resolved)
        total_applied += _apply_creates_deletes(root, grouped.creates, grouped.deletes)
    except _AnchorDriftError as drift:
        rb = rollback(root, checkpoint)
        return BatchResult(
            success=False,
            checkpoint=checkpoint,
            error="Apply aborted: file drifted between validation and apply",
            details=[drift.detail],
            rollback_failed=not rb.ok,
        )
    except Exception as exc:  # noqa: BLE001 - any apply failure must roll back
        rb = rollback(root, checkpoint)
        return BatchResult(
            success=False,
            checkpoint=checkpoint,
            error=f"Apply aborted and rolled back: {exc}",
            rollback_failed=not rb.ok,
        )

    return BatchResult(
        success=True,
        checkpoint=checkpoint,
        applied=total_applied,
        summary={
            "modified": len(resolved_by_file),
            "created": len(grouped.creates),
            "deleted": len(grouped.deletes),
        },
    )

rollback(root, checkpoint)

Restore exactly the paths captured by checkpoint to their prior state.

Rollback is a strict inverse of the batch and best-effort: for each snapshotted path a file that existed is rewritten with its original bytes, a file that did not exist before is removed, and only the directories the batch itself created (recorded in the snapshot) are pruned — a pre-existing directory is never removed. Every captured path is attempted even if an earlier one fails, so a partial rollback is fully reported. No git command is run.

Parameters:

Name Type Description Default
root Path

Project root directory.

required
checkpoint str

The JSON snapshot returned by :func:create_checkpoint.

required

Returns:

Name Type Description
A RollbackResult

class:~axm_edit.models.operations.RollbackResult listing the

RollbackResult

paths restored and those that could not be restored. RollbackResult.ok

RollbackResult

is True only on a well-formed snapshot with no per-path failure;

RollbackResult

a malformed snapshot yields valid=False.

Source code in packages/axm-edit/src/axm_edit/core/checkpoint.py
Python
def rollback(root: Path, checkpoint: str) -> RollbackResult:
    """Restore exactly the paths captured by *checkpoint* to their prior state.

    Rollback is a *strict inverse* of the batch and best-effort: for each
    snapshotted path a file that existed is rewritten with its original bytes,
    a file that did not exist before is removed, and only the directories the
    batch itself created (recorded in the snapshot) are pruned — a
    pre-existing directory is never removed. Every captured path is attempted
    even if an earlier one fails, so a partial rollback is fully reported. No
    git command is run.

    Args:
        root: Project root directory.
        checkpoint: The JSON snapshot returned by :func:`create_checkpoint`.

    Returns:
        A :class:`~axm_edit.models.operations.RollbackResult` listing the
        paths restored and those that could not be restored. ``RollbackResult.ok``
        is ``True`` only on a well-formed snapshot with no per-path failure;
        a malformed snapshot yields ``valid=False``.
    """
    root = root.resolve()
    try:
        payload = json.loads(checkpoint)
        entries = payload["entries"]
    except (ValueError, TypeError, KeyError):
        return RollbackResult(valid=False)
    if not isinstance(entries, dict):
        return RollbackResult(valid=False)

    created_dirs = _read_created_dirs(payload)
    restored: list[str] = []
    unrestored: list[str] = []
    for rel, encoded in entries.items():
        target = _resolve_within(root, rel)
        if target is None:
            continue
        try:
            _restore_one(target, encoded, root, created_dirs)
            restored.append(rel)
        except OSError:
            unrestored.append(rel)
    return RollbackResult(restored=restored, unrestored=unrestored)