Skip to content

Diff

diff

MCP tool for structural branch diff.

DiffTool

Bases: AXMTool

Compare two git refs at symbol level.

Registered as ast_diff via axm.tools entry point. Uses git worktrees to avoid disturbing the working tree.

Source code in packages/axm-ast/src/axm_ast/tools/diff.py
Python
class DiffTool(AXMTool):
    """Compare two git refs at symbol level.

    Registered as ``ast_diff`` via axm.tools entry point.
    Uses git worktrees to avoid disturbing the working tree.
    """

    @property
    def name(self) -> str:
        """Return tool name for registry lookup."""
        return "ast_diff"

    @safe_execute
    def execute(
        self,
        *,
        path: str = ".",
        base: str = "",
        head: str = "",
        **kwargs: object,
    ) -> ToolResult:
        """Compare two branches at symbol level.

        Args:
            path: Path to package directory.
            base: Base git ref (branch, tag, commit).
            head: Head git ref (branch, tag, commit).

        Returns:
            ToolResult with structural diff data.
        """
        if not base:
            return ToolResult(success=False, error="base parameter is required")
        if not head:
            return ToolResult(success=False, error="head parameter is required")

        project_path = Path(path).resolve()
        if not project_path.is_dir():
            return ToolResult(success=False, error=f"Not a directory: {project_path}")

        from axm_ast.core.structural_diff import structural_diff

        try:
            result = structural_diff(project_path, base, head)
        except Exception as exc:  # noqa: BLE001
            return ToolResult(success=False, error=str(exc))

        if "error" in result:
            err = result["error"]
            return ToolResult(
                success=False,
                error=err if isinstance(err, str) else str(err),
            )

        data = cast("dict[str, object]", result)
        try:
            text: str | None = render_diff_text(data)
        except (KeyError, TypeError, AttributeError):
            text = None
        return ToolResult(success=True, data=data, text=text)
name property

Return tool name for registry lookup.

execute(*, path='.', base='', head='', **kwargs)

Compare two branches at symbol level.

Parameters:

Name Type Description Default
path str

Path to package directory.

'.'
base str

Base git ref (branch, tag, commit).

''
head str

Head git ref (branch, tag, commit).

''

Returns:

Type Description
ToolResult

ToolResult with structural diff data.

Source code in packages/axm-ast/src/axm_ast/tools/diff.py
Python
@safe_execute
def execute(
    self,
    *,
    path: str = ".",
    base: str = "",
    head: str = "",
    **kwargs: object,
) -> ToolResult:
    """Compare two branches at symbol level.

    Args:
        path: Path to package directory.
        base: Base git ref (branch, tag, commit).
        head: Head git ref (branch, tag, commit).

    Returns:
        ToolResult with structural diff data.
    """
    if not base:
        return ToolResult(success=False, error="base parameter is required")
    if not head:
        return ToolResult(success=False, error="head parameter is required")

    project_path = Path(path).resolve()
    if not project_path.is_dir():
        return ToolResult(success=False, error=f"Not a directory: {project_path}")

    from axm_ast.core.structural_diff import structural_diff

    try:
        result = structural_diff(project_path, base, head)
    except Exception as exc:  # noqa: BLE001
        return ToolResult(success=False, error=str(exc))

    if "error" in result:
        err = result["error"]
        return ToolResult(
            success=False,
            error=err if isinstance(err, str) else str(err),
        )

    data = cast("dict[str, object]", result)
    try:
        text: str | None = render_diff_text(data)
    except (KeyError, TypeError, AttributeError):
        text = None
    return ToolResult(success=True, data=data, text=text)

render_diff_text(data)

Render a structural diff payload as a compact changelog.

Parameters:

Name Type Description Default
data dict[str, object]

Payload from :func:structural_diff.

required

Returns:

Type Description
str

Changelog text grouped by file: + added, - removed,

str

~ changed (with before → after signatures).

Source code in packages/axm-ast/src/axm_ast/tools/diff_text.py
Python
def render_diff_text(data: dict[str, object]) -> str:
    """Render a structural diff payload as a compact changelog.

    Args:
        data: Payload from :func:`structural_diff`.

    Returns:
        Changelog text grouped by file: ``+`` added, ``-`` removed,
        ``~`` changed (with before → after signatures).
    """
    typed = cast("_DiffData", data)
    added = typed.get("added") or []
    removed = typed.get("removed") or []
    modified = typed.get("modified") or []
    summary = typed.get("summary") or {}
    n_add = summary.get("added", len(added))
    n_rem = summary.get("removed", len(removed))
    n_mod = summary.get("modified", len(modified))

    lines: list[str] = [f"ast_diff | +{n_add} -{n_rem} ~{n_mod}"]
    for file_name, bucket in _group_by_file(added, removed, modified):
        lines.append(f"## {file_name}")
        lines.extend(f"+ {_symbol_sig(s)}" for s in bucket.added)
        lines.extend(f"- {_symbol_sig(s)}" for s in bucket.removed)
        lines.extend(_render_modified(s) for s in bucket.modified)
    return "\n".join(lines)