Skip to content

Architecture

Overview

axm-ast follows a layered architecture: CLI → core engines → models. The core includes filesystem discovery, parsing, cache invalidation and git subprocesses; model transformations and renderers are the pure parts. Analysis preserves the target source, while structural diff creates temporary git worktrees.

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, return values (return some_func / return self.method — a function returned as a value, not called), 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). At the core level, plain-name resolution in find_definition() raises ValueError("Multiple symbols match '...': ...") when a non-dotted name maps to several top-level definitions — surfacing the ambiguity instead of silently picking the first homonym, mirroring inspect's disambiguation error; the ast_impact tool expands ambiguous bare names into per-definition reports 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. detect_workspace() delegates uv-workspace resolution to the canonical leaf resolver axm_ingot.uv.resolve_workspace and projects the resulting ResolvedWorkspace (root + name) onto a Pydantic WorkspaceInfo (the model stays defined in axm-ast; ingot never sees Pydantic). analyze_workspace() then expands glob patterns in [tool.uv.workspace] members and resolves project names for inter-package dependency edges (AST-level analysis, kept in axm-ast) 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). DescribeTool permits compression only with its default summary detail
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. AXM Tools (tools/)

Request/response analysis surfaces are registered through the axm.tools entry-point group. They are available through MCP, the axm CLI, and tool_node DAG composition without an adapter layer. The deprecated axm.hooks adapters have been removed; protocol and DAG consumers call the corresponding AST tools directly.

Capability Tool
Project context ast_context
Flow tracing, including source detail ast_flows
Symbol source inspection ast_inspect
Impact and documentation impact ast_impact, ast_doc_impact
File headers ast_file_header

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.

Some tool implementations also catch exceptions locally before the shared boundary. A structured failure reports an analysis error; it is not proof that extraction was exhaustive.

Language backends

The backend registry dispatches by suffix. Python is built in; optional TypeScript extraction uses shared models with less complete metadata. Node project selection, supported suffixes and cache limitations are documented in scope and languages.