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.