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.