orchestrata_ultimate.refactor.ctok.edit_buffer

Reference › orchestrata_ultimate › refactor › ctok › edit_buffer

Byte-span edits and the safety gate that checks them. lossless_module applies an EditBuffer, and the module imports only the traceability decorator (REQ-110-124 and REQ-110-127).

EditBuffer.add(start, end, new_text) queues a replacement for a byte range and rejects a negative start or a start after the end. apply(source) first sorts the edits and refuses any span beyond the source length or overlapping its predecessor (ValueError), then splices them in reverse offset order so earlier edits do not shift later offsets; every byte outside an edit is unchanged. verify_edit(before, after, expect_equivalent=...) is the gate: both versions must parse, and if the edit claims to be formatting-only the two ASTs, rendered by stringify_ast, must be identical. If it claims to be a rename, the ASTs may differ only in identifier fields (Name.id, Attribute.attr, def and class names, arg.arg) and are compared with identifiers masked. A violation raises EditSafetyError. The comparison approach is derived from Black's AST equivalence check.

At a glance

Source src/orchestrata_ultimate/refactor/ctok/edit_buffer.py
Lines of code 235
Classes 2
Functions 3
Imports (internal) orchestrata_ultimate.core.base.traces
Imported by orchestrata_ultimate.refactor.ctok, orchestrata_ultimate.refactor.ctok.lossless_module
Tests that import it tests/refactor/ctok/test_edit_buffer.py, tests/refactor/ctok/test_lossless_module.py

Class diagram

Class diagram orchestrata_ultimate.refactor.ctok.edit_buffer

Dependency diagram

Dependency diagram orchestrata_ultimate.refactor.ctok.edit_buffer

API

Constants

Name Type Value
_IDENTIFIER_FIELDS dict[type[ast.AST], frozenset[str]] {ast.Name: frozenset({'id'}), ast.arg: frozenset({'arg'}), ast.FunctionDef: fro…

Classes

EditSafetyError

Bases: ValueError · Decorators: traces(spec='110', story='US-110-009', reqs=['REQ-110-127']) · Line 49

Raised when an edit violates its claimed AST-equivalence contract.

EditBuffer (dataclass)

Decorators: traces(spec='110', story='US-110-009', reqs=['REQ-110-124']), dataclass(slots=True) · Line 55

Accumulates byte-span edits and applies them as splices.

Attribute Type Default
_edits list[tuple[int, int, bytes]] field(default_factory=list)
Method Signature Summary
add add(start: int, end: int, new_text: bytes) -> None Accumulate a span edit.
apply apply(source: bytes) -> bytes Splice all accumulated edits over source.

Functions

Function Signature Summary
stringify_ast stringify_ast(source: bytes) -> str A normalized, position-independent string dump of source's AST.
verify_edit traces verify_edit(before: bytes, after: bytes, *, expect_equivalent: bool) -> None The edit-safety gate: prove an edit didn't do more than it claimed.
_stringify_ast traces _stringify_ast(node: ast.AST, parent_stack: list[ast.AST], *, mask_identifiers: bool = False) -> Iterator[str] Yield lines of a position-independent AST dump (see stringify_ast).

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/refactor/ctok/edit_buffer.py

"""EditBuffer -- span-splice edits + the Black-derived edit-safety gate.

P2 of the ctok-parser contract (plans/contracts/ctok-parser.contract.json,
REQ-110-124/127): ``EditBuffer`` accumulates byte-span edits and applies
them as splices, leaving every untouched byte identical to the original
(REQ-110-124). ``stringify_ast``/``verify_edit`` are our own small port of
Black's ``assert_equivalent`` machinery (``black/parsing.py
stringify_ast``, ``black/__init__.py assert_equivalent``) -- a
position-independent AST dump used to prove an edit didn't change program
semantics. We do NOT import Black; ``ast`` node ``_fields`` already
excludes ``lineno``/``col_offset``/``end_lineno``/``end_col_offset``
(those live in ``_attributes``), so walking only ``_fields`` gives
position-independence for free, exactly as Black's original does.
"""

from __future__ import annotations

import ast
from collections.abc import Iterator
from dataclasses import dataclass, field

from orchestrata_ultimate.core.base.traces import traces

# ast (node type, field name) pairs that hold a rename-able identifier.
# `verify_edit`'s rename mode masks these out before comparing -- any
# OTHER difference between before/after (including type changes here,
# since a masked field only fires for `str` values) fails the gate.
# Every ``ast`` field typed ``identifier`` in the Python ASDL grammar: a rename
# may legitimately touch ANY of these, so ``verify_edit`` masks them all when
# proving an edit "differs only in identifiers". Omitting a field (e.g. the
# import-name fields below) falsely rejects a valid rename that reaches an import
# statement. (``Global.names``/``Nonlocal.names`` are ``list[identifier]`` and are
# already skipped wholesale by the string dump, so they need no entry here.)
_IDENTIFIER_FIELDS: dict[type[ast.AST], frozenset[str]] = {
    ast.Name: frozenset({"id"}),
    ast.arg: frozenset({"arg"}),
    ast.FunctionDef: frozenset({"name"}),
    ast.AsyncFunctionDef: frozenset({"name"}),
    ast.ClassDef: frozenset({"name"}),
    ast.Attribute: frozenset({"attr"}),
    ast.alias: frozenset({"name", "asname"}),  # import / from-import names
    ast.ImportFrom: frozenset({"module"}),  # `from <module> import ...`
    ast.keyword: frozenset({"arg"}),  # call keyword-argument names
    ast.ExceptHandler: frozenset({"name"}),  # `except E as <name>`
}


@traces(spec="110", story="US-110-009", reqs=["REQ-110-127"])
class EditSafetyError(ValueError):
    """Raised when an edit violates its claimed AST-equivalence contract."""


@traces(spec="110", story="US-110-009", reqs=["REQ-110-124"])
@dataclass(slots=True)
class EditBuffer:
    """Accumulates byte-span edits and applies them as splices.

    Attributes:
        _edits: ``(start, end, new_text)`` triples in the order they were
            added; add order does not matter for ``apply`` (it re-sorts).
    """

    _edits: list[tuple[int, int, bytes]] = field(default_factory=list)

    @traces(spec="110", story="US-110-009", reqs=["REQ-110-124"])
    def add(self, start: int, end: int, new_text: bytes) -> None:
        """Accumulate a span edit.

        Args:
            start: Inclusive start byte offset.
            end: Exclusive end byte offset.
            new_text: Replacement bytes for ``[start, end)``.

        Raises:
            ValueError: ``start`` is negative or ``start > end``.
        """
        if start < 0 or start > end:
            raise ValueError(f"invalid edit span [{start}, {end})")
        self._edits.append((start, end, new_text))

    @traces(spec="110", story="US-110-009", reqs=["REQ-110-124"])
    def apply(self, source: bytes) -> bytes:
        """Splice all accumulated edits over ``source``.

        Edits are applied in reverse offset order so earlier splices never
        invalidate the byte offsets of later ones; every byte outside an
        edit span is byte-identical to ``source`` (REQ-110-124).

        Args:
            source: The original bytes to splice.

        Returns:
            The spliced bytes.

        Raises:
            ValueError: an edit span is out of bounds for ``source``, or
                two edit spans overlap.
        """
        ordered = sorted(self._edits, key=lambda edit: edit[0])
        prev_end = 0
        for start, end, _ in ordered:
            if end > len(source):
                raise ValueError(
                    f"edit span [{start}, {end}) exceeds {len(source)}-byte source"
                )
            if start < prev_end:
                raise ValueError(
                    f"overlapping edits: span [{start}, {end}) starts before "
                    f"the prior edit ends at {prev_end}"
                )
            prev_end = end

        buf = bytearray(source)
        for start, end, new_text in sorted(
            ordered, key=lambda edit: edit[0], reverse=True
        ):
            buf[start:end] = new_text
        return bytes(buf)


def stringify_ast(source: bytes) -> str:
    """A normalized, position-independent string dump of ``source``'s AST.

    Adapted from Black's ``black.parsing.stringify_ast`` /
    ``assert_equivalent`` (we do not import Black): walks only
    ``node._fields`` (never ``_attributes``, where ``lineno``/
    ``col_offset`` live), so two ASTs that differ only in source position
    dump identically.

    Args:
        source: Python source bytes.

    Returns:
        A multi-line string uniquely determined by the AST's shape and
        field values -- not by formatting, whitespace, or position.

    Raises:
        EditSafetyError: ``source`` does not parse as Python.
    """
    try:
        tree = ast.parse(source)
    except SyntaxError as exc:
        raise EditSafetyError(f"source failed to parse: {exc}") from exc
    return "\n".join(_stringify_ast(tree, []))


@traces(spec="110", story="US-110-009", reqs=["REQ-110-127"])
def verify_edit(before: bytes, after: bytes, *, expect_equivalent: bool) -> None:
    """The edit-safety gate: prove an edit didn't do more than it claimed.

    Args:
        before: Source bytes before the edit.
        after: Source bytes after the edit.
        expect_equivalent: If True, the edit claims to be formatting-only /
            no-semantic-change: ``before`` and ``after`` must produce an
            IDENTICAL AST (REQ-110-127). If False, the edit claims to be a
            rename: the ASTs may differ, but ONLY in identifier fields
            (``Name.id``, ``Attribute.attr``,
            ``FunctionDef``/``AsyncFunctionDef``/``ClassDef.name``,
            ``arg.arg``) -- any other structural or literal difference
            fails the gate.

    Raises:
        EditSafetyError: either source fails to parse, or the ASTs differ
            more than ``expect_equivalent`` permits.
    """
    try:
        before_tree = ast.parse(before)
    except SyntaxError as exc:
        raise EditSafetyError(f"'before' source failed to parse: {exc}") from exc
    try:
        after_tree = ast.parse(after)
    except SyntaxError as exc:
        raise EditSafetyError(f"edit produced invalid syntax: {exc}") from exc

    if expect_equivalent:
        before_dump = "\n".join(_stringify_ast(before_tree, []))
        after_dump = "\n".join(_stringify_ast(after_tree, []))
        if before_dump != after_dump:
            raise EditSafetyError(
                "edit claimed to be formatting-only / no-semantic-change but "
                "the AST changed"
            )
        return

    before_masked = "\n".join(_stringify_ast(before_tree, [], mask_identifiers=True))
    after_masked = "\n".join(_stringify_ast(after_tree, [], mask_identifiers=True))
    if before_masked != after_masked:
        raise EditSafetyError(
            "rename edit changed the AST in more than the renamed identifiers"
        )


@traces(spec="110", story="US-110-009", reqs=["REQ-110-127"])
def _stringify_ast(
    node: ast.AST, parent_stack: list[ast.AST], *, mask_identifiers: bool = False
) -> Iterator[str]:
    """Yield lines of a position-independent AST dump (see ``stringify_ast``)."""
    yield f"{'    ' * len(parent_stack)}{node.__class__.__name__}("

    for name in sorted(node._fields):
        try:
            value: object = getattr(node, name)
        except AttributeError:
            continue

        yield f"{'    ' * (len(parent_stack) + 1)}{name}="

        if isinstance(value, list):
            for item in value:
                if isinstance(item, ast.AST):
                    parent_stack.append(node)
                    yield from _stringify_ast(
                        item, parent_stack, mask_identifiers=mask_identifiers
                    )
                    parent_stack.pop()
        elif isinstance(value, ast.AST):
            parent_stack.append(node)
            yield from _stringify_ast(
                value, parent_stack, mask_identifiers=mask_identifiers
            )
            parent_stack.pop()
        else:
            masked = (
                mask_identifiers
                and isinstance(value, str)
                and name in _IDENTIFIER_FIELDS.get(type(node), frozenset())
            )
            shown: object = "<IDENTIFIER>" if masked else value
            yield (
                f"{'    ' * (len(parent_stack) + 1)}{shown!r},  # "
                f"{value.__class__.__name__}"
            )

    yield f"{'    ' * len(parent_stack)})  # /{node.__class__.__name__}"