Skip to content

Architecture

Overview

axm-ast follows a layered architecture: CLI → core engines → models. The core layer is entirely I/O-free — it operates on Pydantic models produced by tree-sitter parsing.

graph TD
    subgraph "User Interface"
        CLI["CLI (cyclopts)"]
    end

    subgraph "Core Engines"
        Parser["Parser (tree-sitter)"]
        Analyzer["Analyzer"]
        Cache["Cache"]
        Ranker["Ranker (PageRank)"]
        Callers["Caller Analysis"]
        Context["Context (one-shot)"]
        Impact["Impact Analysis"]
        GitCoupling["Git Coupling"]
        StructDiff["Structural Diff"]
        Workspace["Workspace"]
        Docs["Docs Discovery"]
        Formatters["Formatters"]
    end

    subgraph "Models (Pydantic)"
        ModuleInfo["ModuleInfo"]
        PackageInfo["PackageInfo"]
        WorkspaceInfo["WorkspaceInfo"]
        CallSite["CallSite"]
    end

    subgraph "External"
        TreeSitter["tree-sitter-python"]
        FS["File System"]
        PyProject["pyproject.toml"]
        Git["git log"]
    end

    CLI --> Cache
    CLI --> Context
    CLI --> Impact
    CLI --> Callers
    CLI --> Formatters
    Cache --> Analyzer
    Cache --> PackageInfo
    Analyzer --> Parser
    Ranker --> PackageInfo
    Callers --> Parser
    Context --> Cache
    Context --> Ranker
    Impact --> Callers
    Impact --> Cache
    Impact --> GitCoupling
    GitCoupling --> Git
    StructDiff --> Git
    StructDiff --> Analyzer
    Parser --> TreeSitter
    Parser --> FS
    Parser --> ModuleInfo
    Analyzer --> PackageInfo
    Callers --> CallSite
    Context --> PyProject
    CLI --> Docs
    CLI --> Workspace
    Workspace --> Cache
    Workspace --> Callers
    Workspace --> Impact
    Workspace --> WorkspaceInfo
    Docs --> FS

Layers

1. CLI (cli.py)

Cyclopts-based commands with input validation and formatted output (text + JSON). Each command follows the pattern: parse arguments → call core → format output.

2. Core Engines (core/)

Independent, composable analysis engines:

Engine Purpose Key Function
parser.py Tree-sitter AST parsing → ModuleInfo extract_module_info()
analyzer.py Package discovery (auto-detects src-layout), import graph (absolute + relative), search, stubs analyze_package()
cache.py Thread-safe caching of PackageInfo — avoids redundant parsing get_package(), clear_cache()
ranker.py PageRank symbol importance rank_symbols()
callers.py Call-site detection and non-call reference extraction (dynamic dispatch patterns: dict values, list/tuple/set elements, keyword arguments, default parameters, positional arguments, and forward references inside string-typed annotations — x: "Foo", def f() -> "Bar", list["Baz"], cast("Qux", v) — restricted to real type positions, so Annotated[T, *meta] metadata, Literal[...] args, log strings, and docstrings do not pollute the ref set). Shares tree-sitter walker primitives (is_call_node, update_context, extract_call_site, node_text_safe) with flows.py via the internal _call_helpers module — exposed without underscore prefix so cross-module imports remain compliant with the no-private-cross-module-import rule find_callers(), find_callers_workspace(), extract_references()
context.py One-shot project dump build_context()
impact.py Change blast radius (callers + reexports + tests + git coupling + cross-package). Workspace analysis delegates to extracted helpers (_find_workspace_definition, _resolve_effective_test_filter, _apply_caller_test_filter). Scoring weights and LOW/MEDIUM/HIGH thresholds are overridable per-package via [tool.axm-ast.impact] in pyproject.toml (ImpactWeights) analyze_impact(), find_definition(), analyze_impact_workspace(), score_impact()
git_coupling.py Git co-change coupling analysis (6-month history) git_coupled_files()
structural_diff.py Symbol-level branch diff via git worktrees structural_diff()
workspace.py Multi-package workspace detection and analysis; expands glob patterns in [tool.uv.workspace] members and resolves project names for dependency edges detect_workspace(), analyze_workspace()
docs.py Documentation tree discovery discover_docs()
dead_code.py Dead code detection with test/lazy-import/base-class/intra-module-ref/namespace-module scanning; namespace resolution uses _resolve_import_stems for both bare and from-imports; respects .gitignore via _discover_py_files find_dead_code(), DeadSymbol
flows.py Entry point detection, BFS flow tracing, source enrichment, workspace-level callee search. Exports VALID_DETAILS (frozenset) — the accepted detail values ("trace", "source", "compact"); trace_flow() returns (steps, truncated) where truncated is True when frontier nodes at max_depth had unexpanded children; raises ValueError for invalid detail values or when the entry symbol is not found in the package. Internal helpers: _get_callees (callee lookup from index or package scan), _check_frontier_truncated (frontier truncation detection) find_entry_points(), trace_flow(), find_callees_workspace(), VALID_DETAILS

3. Formatters (formatters.py)

Output formatting with multiple detail levels:

Function Purpose
format_text() Human-readable text (summary / detailed)
format_compressed() AI-friendly compressed view (excludes test modules). compress and explicit detail are mutually exclusive in DescribeTool
format_json() Machine-readable JSON
format_toc() Table-of-contents: module names + counts only
filter_modules() Case-insensitive substring filter on module names
format_mermaid() Mermaid dependency graph

3b. Text Renderers (tools/describe_text.py)

Compact text rendering for DescribeTool output, following the same pattern as tools/inspect_text.py:

Function Purpose
render_describe_text() Dispatcher — selects renderer by detail level (toc, summary, detailed)

The renderer produces token-efficient output suitable for ToolResult.text, stripping def prefixes from signatures and skipping empty modules.

4. Models (models/)

Pydantic models for structured data exchange between layers:

Model Purpose
ModuleInfo Full introspection result for a single module
PackageInfo Full introspection result for a package
FunctionKind StrEnum classifying callables: function, method, property, classmethod, staticmethod, abstract
FunctionInfo Function metadata (params, return type, decorators, kind)
ClassInfo Class metadata (bases, methods, docstring)
ParameterInfo Function parameter (name, type, default)
VariableInfo Module-level variable / constant
ImportInfo Import statement (absolute/relative, names)
CallSite Call-site location (module, line, context)
WorkspaceInfo Multi-package workspace (packages, dependency edges)

5. Hooks (hooks/)

Protocol hooks registered via axm.hooks entry points. These are called by axm-engine as pre/post-hooks in protocol execution.

Hook Entry Point Purpose
TraceSourceHook ast:trace-source Run trace_flow(detail="source") and inject trace into session context. Unpacks (steps, truncated) tuple but currently discards the truncation flag
SourceBodyHook ast:source-body Extract symbol source bodies and return as a grouped markdown string (symbols=<str>) with a files list of relative paths. Supports dotted names via three resolution strategies: _resolve_as_class_method (Class.method, delegates to _build_method_body), _resolve_as_nested_class (Outer.Inner.method), and _resolve_as_module_symbol (module.func). Extraction logic lives in _run_extraction (with _dedup_symbols to remove methods already covered by their parent class); formatting in _format_as_markdown.
FileHeaderHook ast:file-header Extract file-level header (module docstring, __all__, top-level imports) and inject into session context

Design Decisions

Decision Rationale
tree-sitter for parsing Fast, incremental, handles broken files gracefully
Pydantic models Validation, serialization, JSON output for free
PageRank for ranking Graph-based importance adapts to any project structure
Composable engines impact = callers + analyzer + ranker + test mapping + git coupling
Session cache PackageCache avoids redundant tree-sitter parsing across chained tool calls
Workspace auto-detect [tool.uv.workspace] triggers multi-package mode transparently
src/ layout PEP 621 best practice, no import conflicts

Tool Error Handling

All axm_ast.tools.* AXMTool.execute() methods are wrapped with the @safe_execute decorator from axm_ast.tools._base. The decorator centralizes the failure boundary: any uncaught Exception is logged at WARNING (with exc_info=True) on the calling module's logger and converted into ToolResult(success=False, error=str(exc)), so callers never see a raised exception.

For inner helpers that return a dict instead of ToolResult (e.g. batch sub-results in tools/impact.py), the decorator does not fit; those sites instead call log_and_fallback(logger, exc, fallback), which applies the same logging policy and returns the supplied fallback.

The single residual except Exception lives inside safe_execute itself and is annotated # noqa: BLE001 — final boundary to document that it is the intentional last line of defense.