Skip to content

Impact

impact

Change impact analysis — who is affected when you modify a symbol.

Composes callers, graph, and search into a single "what breaks if I change X?" answer.

Example

from axm_ast.core.impact import analyze_impact result = analyze_impact(Path("src/axm_ast"), "analyze_package") print(result["score"]) 'HIGH'

CallerEntry

Bases: TypedDict

Caller record as serialized into the impact result dict.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
class CallerEntry(TypedDict):
    """Caller record as serialized into the impact result dict."""

    module: str
    line: int
    context: str | None
    call_expression: str
    # Optional defensive field consumed by ``render_impact_text`` —
    # never set by ``analyze_impact``; reserved for legacy callers.
    name: NotRequired[str]

DefinitionInfo

Bases: TypedDict

Resolved definition location for a symbol.

module, line and kind are always present; signature and name are only set for function/class lookups; package is added by workspace-level resolution.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
class DefinitionInfo(TypedDict):
    """Resolved definition location for a symbol.

    ``module``, ``line`` and ``kind`` are always present; ``signature`` and
    ``name`` are only set for function/class lookups; ``package`` is added
    by workspace-level resolution.
    """

    module: str
    line: int
    kind: str
    signature: NotRequired[str | None]
    name: NotRequired[str]
    package: NotRequired[str]

ImpactReport

Bases: BaseModel

Input shape consumed by :func:score_impact.

Captures only the inputs to scoring — display-only fields produced by :func:analyze_impact (symbol, definition, test_files …) are intentionally excluded.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
class ImpactReport(BaseModel):
    """Input shape consumed by :func:`score_impact`.

    Captures only the inputs to scoring — display-only fields produced by
    :func:`analyze_impact` (``symbol``, ``definition``, ``test_files`` …)
    are intentionally excluded.
    """

    model_config = ConfigDict(extra="forbid")

    # Scoring only consumes ``len()`` of each list, so element shape is
    # irrelevant here. ``list[object]`` accepts any Python value at runtime
    # (and forbids implicit ``Any`` propagation at type-check time).
    callers: list[object] = Field(default_factory=list)
    reexports: list[object] = Field(default_factory=list)
    affected_modules: list[object] = Field(default_factory=list)
    git_coupled: list[object] = Field(default_factory=list)
    type_refs: list[object] = Field(default_factory=list)

ImpactResult

Bases: TypedDict

Output shape of :func:analyze_impact / :func:analyze_impact_workspace.

total=False because workspace results omit cross_package_impact and test_files_by_import, and add workspace instead.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
class ImpactResult(TypedDict, total=False):
    """Output shape of :func:`analyze_impact` / :func:`analyze_impact_workspace`.

    ``total=False`` because workspace results omit ``cross_package_impact``
    and ``test_files_by_import``, and add ``workspace`` instead.
    """

    symbol: str
    workspace: str
    definition: DefinitionInfo | None
    callers: list[CallerEntry]
    type_refs: list[TypeRefEntry]
    reexports: list[str]
    affected_modules: list[str]
    test_files: list[str]
    test_files_by_import: list[str]
    # Heterogeneous payload from git_coupling (file/strength/co_changes).
    git_coupled: list[Mapping[str, object]]
    cross_package_impact: list[str]
    score: str
    # Set by tool-layer fallbacks (e.g. ``ImpactTool._analyze_single``
    # when the symbol cannot be resolved); the renderer surfaces it.
    error: NotRequired[str]

ImpactWeights

Bases: BaseModel

Configurable weights and thresholds for :func:score_impact.

Defaults match the module-level constants. Per-package overrides are loaded from [tool.axm-ast.impact] in pyproject.toml.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
class ImpactWeights(BaseModel):
    """Configurable weights and thresholds for :func:`score_impact`.

    Defaults match the module-level constants. Per-package overrides are
    loaded from ``[tool.axm-ast.impact]`` in ``pyproject.toml``.
    """

    model_config = ConfigDict(extra="forbid")

    caller_weight: int = CALLER_WEIGHT
    reexport_weight: int = REEXPORT_WEIGHT
    module_weight: int = MODULE_WEIGHT
    coupled_weight: int = COUPLED_WEIGHT
    typeref_weight: int = TYPEREF_WEIGHT
    high_threshold: int = IMPACT_HIGH_THRESHOLD
    medium_threshold: int = IMPACT_MEDIUM_THRESHOLD

TypeRefEntry

Bases: TypedDict

Single type-reference record (param/return/alias).

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
class TypeRefEntry(TypedDict):
    """Single type-reference record (param/return/alias)."""

    function: str
    module: str
    line: int
    ref_type: str

analyze_impact(path, symbol, *, project_root=None, exclude_tests=False, test_filter=None)

Full impact analysis for a symbol.

Combines definition location, callers, re-exports, tests, and an impact score.

Parameters:

Name Type Description Default
path Path

Path to the package directory.

required
symbol str

Name of the symbol to analyze.

required
project_root Path | None

Project root (for test detection).

None
exclude_tests bool

If True, filter test callers and test type_refs from the output. The impact score is still computed on the full (unfiltered) caller set.

False
test_filter str | None

Filter mode for test callers. "none" excludes all test callers (same as exclude_tests), "all" keeps everything, "related" keeps only direct test callers. Takes precedence over exclude_tests when both are set.

None

Returns:

Type Description
ImpactResult

Complete impact analysis dict.

Example

result = analyze_impact(Path("src/axm_ast"), "analyze_package") result["score"] 'HIGH'

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
def analyze_impact(
    path: Path,
    symbol: str,
    *,
    project_root: Path | None = None,
    exclude_tests: bool = False,
    test_filter: str | None = None,
) -> ImpactResult:
    """Full impact analysis for a symbol.

    Combines definition location, callers, re-exports, tests,
    and an impact score.

    Args:
        path: Path to the package directory.
        symbol: Name of the symbol to analyze.
        project_root: Project root (for test detection).
        exclude_tests: If True, filter test callers and test
            type_refs from the output.  The impact score is still
            computed on the full (unfiltered) caller set.
        test_filter: Filter mode for test callers.  ``"none"``
            excludes all test callers (same as *exclude_tests*),
            ``"all"`` keeps everything, ``"related"`` keeps only
            direct test callers.  Takes precedence over
            *exclude_tests* when both are set.

    Returns:
        Complete impact analysis dict.

    Example:
        >>> result = analyze_impact(Path("src/axm_ast"), "analyze_package")
        >>> result["score"]
        'HIGH'
    """
    pkg = get_package(path)
    root = _resolve_project_root(path, project_root)

    # For dotted symbols (Class.method), resolve definition with full
    # path but search callers/tests by the bare method name — that is
    # what appears in actual source code (self.method(), obj.method()).
    dotted = _split_dotted_symbol(symbol)
    lookup_name = dotted[1].split(".")[-1] if dotted else symbol

    definition = find_definition(pkg, symbol)
    callers = find_callers(pkg, lookup_name)
    reexports = find_reexports(pkg, lookup_name)
    test_files = map_tests(lookup_name, root)

    type_refs = find_type_refs(pkg, lookup_name)
    type_ref_modules = {r["module"] for r in type_refs}
    affected_modules = list(
        {c.module for c in callers} | set(reexports) | type_ref_modules
    )

    result: ImpactResult = {
        "symbol": symbol,
        "definition": definition,
        "callers": [
            CallerEntry(
                module=c.module,
                line=c.line,
                context=c.context,
                call_expression=c.call_expression,
            )
            for c in callers
        ],
        "type_refs": type_refs,
        "reexports": reexports,
        "affected_modules": sorted(affected_modules),
        "test_files": [str(t.name) for t in test_files],
        "git_coupled": [],
        "score": "LOW",
    }

    _add_git_coupling(result, definition, pkg, root)
    _add_import_based_tests(result, definition, test_files, root)
    if root is not None:
        result["cross_package_impact"] = _find_cross_package_impact(
            path,
            pkg,
            lookup_name,
            root,
        )
    # Score on the FULL caller set before any filtering. A module that
    # both imports and calls the symbol shows up in *callers* and in
    # *reexports* (find_reexports flags any `from X import Y`); dropping
    # caller modules from the reexport scoring set avoids double-counting.
    weights = _load_impact_weights(path)
    caller_modules = {c["module"] for c in result["callers"]}
    scoring_reexports = [r for r in result["reexports"] if r not in caller_modules]
    # ``ImpactReport`` lists are typed ``list[object]`` (scoring only uses
    # ``len()``); widen each input list explicitly so list invariance does
    # not reject the more specific result types.
    report = ImpactReport(
        callers=list(result["callers"]),
        reexports=list(scoring_reexports),
        affected_modules=list(result["affected_modules"]),
        git_coupled=list(result["git_coupled"]),
        type_refs=list(result["type_refs"]),
    )
    result["score"] = score_impact(report, weights)

    effective = _resolve_effective_filter(test_filter, exclude_tests)
    _apply_test_filter(result, effective)

    return result

analyze_impact_workspace(ws_path, symbol, *, exclude_tests=False, test_filter=None)

Full impact analysis for a symbol across a workspace.

Searches all packages for definition, callers, re-exports, and test files. Module names include package prefix.

Parameters:

Name Type Description Default
ws_path Path

Path to workspace root.

required
symbol str

Name of the symbol to analyze.

required
exclude_tests bool

If True, filter test callers from the output. Score is computed on the full caller set.

False
test_filter str | None

Filter mode for test callers. "none" excludes all, "all" keeps everything, "related" keeps only direct test callers. Takes precedence over exclude_tests when both are set.

None

Returns:

Type Description
ImpactResult

Complete impact analysis dict (workspace-scoped).

Example

result = analyze_impact_workspace(Path("/ws"), "ToolResult") result["score"] 'HIGH'

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
def analyze_impact_workspace(
    ws_path: Path,
    symbol: str,
    *,
    exclude_tests: bool = False,
    test_filter: str | None = None,
) -> ImpactResult:
    """Full impact analysis for a symbol across a workspace.

    Searches all packages for definition, callers, re-exports,
    and test files. Module names include package prefix.

    Args:
        ws_path: Path to workspace root.
        symbol: Name of the symbol to analyze.
        exclude_tests: If True, filter test callers from the
            output.  Score is computed on the full caller set.
        test_filter: Filter mode for test callers.  ``"none"``
            excludes all, ``"all"`` keeps everything, ``"related"``
            keeps only direct test callers.  Takes precedence
            over *exclude_tests* when both are set.

    Returns:
        Complete impact analysis dict (workspace-scoped).

    Example:
        >>> result = analyze_impact_workspace(Path("/ws"), "ToolResult")
        >>> result["score"]
        'HIGH'
    """
    ws = analyze_workspace(ws_path)

    # For dotted symbols, definition uses full path but lookups
    # use the bare method name (what appears in source code).
    dotted = _split_dotted_symbol(symbol)
    lookup_name = dotted[1].split(".")[-1] if dotted else symbol

    callers = find_callers_workspace(ws, lookup_name)
    reexports = _collect_workspace_reexports(ws, lookup_name)
    test_files = _collect_workspace_tests(ws, lookup_name)
    affected_modules = sorted({c.module for c in callers} | set(reexports))

    result: ImpactResult = {
        "symbol": symbol,
        "workspace": ws.name,
        "definition": _find_workspace_definition(ws, symbol),
        "callers": [
            CallerEntry(
                module=c.module,
                line=c.line,
                context=c.context,
                call_expression=c.call_expression,
            )
            for c in callers
        ],
        "reexports": reexports,
        "affected_modules": affected_modules,
        "test_files": test_files,
        "git_coupled": [],
        "score": "LOW",
    }

    _add_workspace_git_coupling(result, result["definition"], ws, ws_path)
    weights = _load_impact_weights(ws_path)
    caller_modules = {c["module"] for c in result["callers"]}
    scoring_reexports = [r for r in result["reexports"] if r not in caller_modules]
    # See analyze_impact: widen invariant lists for ImpactReport.
    report = ImpactReport(
        callers=list(result["callers"]),
        reexports=list(scoring_reexports),
        affected_modules=list(result["affected_modules"]),
        git_coupled=list(result["git_coupled"]),
    )
    result["score"] = score_impact(report, weights)

    effective = _resolve_effective_test_filter(test_filter, exclude_tests)
    _apply_caller_test_filter(result, effective)

    return result

find_definition(pkg, symbol)

Locate where a symbol is defined.

Supports dotted paths like ClassName.method to find methods within class bodies. For deeply nested paths like Outer.Inner.method, resolution is best-effort.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required
symbol str

Name of the function/class to find. May be dotted (e.g. "MyClass.my_method").

required

Returns:

Type Description
DefinitionInfo | None

Dict with module, line, kind — or None if not found.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
def find_definition(pkg: PackageInfo, symbol: str) -> DefinitionInfo | None:
    """Locate where a symbol is defined.

    Supports dotted paths like ``ClassName.method`` to find
    methods within class bodies.  For deeply nested paths like
    ``Outer.Inner.method``, resolution is best-effort.

    Args:
        pkg: Analyzed package info.
        symbol: Name of the function/class to find.  May be
            dotted (e.g. ``"MyClass.my_method"``).

    Returns:
        Dict with module, line, kind — or None if not found.
    """
    dotted = _split_dotted_symbol(symbol)
    if dotted is not None:
        return _find_dotted_definition(pkg, dotted[0], dotted[1])
    return _find_plain_definition(pkg, symbol)

find_reexports(pkg, symbol)

Find modules that re-export a symbol via all or imports.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required
symbol str

Name to search for in exports.

required

Returns:

Type Description
list[str]

List of module names that re-export the symbol.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
def find_reexports(pkg: PackageInfo, symbol: str) -> list[str]:
    """Find modules that re-export a symbol via __all__ or imports.

    Args:
        pkg: Analyzed package info.
        symbol: Name to search for in exports.

    Returns:
        List of module names that re-export the symbol.
    """
    reexports: list[str] = []

    for mod in pkg.modules:
        mod_name = module_dotted_name(mod.path, pkg.root)
        if _is_defined_in_module(mod, symbol):
            continue
        if _is_reexported_via_all(mod, symbol):
            reexports.append(mod_name)
        elif _is_reexported_via_import(mod, symbol):
            reexports.append(mod_name)

    return reexports

find_type_refs(pkg, type_name)

Find functions that reference a type in their signatures.

Scans all function parameters, return types, and module-level variable annotations for occurrences of type_name using word-boundary matching.

Handles compound types: list[X], dict[str, X], X | None, Optional[X], nested generics, and string annotations ("X").

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required
type_name str

Exact type name to search for (e.g. "MyModel").

required

Returns:

Type Description
list[TypeRefEntry]

List of dicts with function, module, line,

list[TypeRefEntry]

and ref_type ("param", "return", or "alias").

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
def find_type_refs(
    pkg: PackageInfo,
    type_name: str,
) -> list[TypeRefEntry]:
    """Find functions that reference a type in their signatures.

    Scans all function parameters, return types, and module-level
    variable annotations for occurrences of *type_name* using
    word-boundary matching.

    Handles compound types: ``list[X]``, ``dict[str, X]``,
    ``X | None``, ``Optional[X]``, nested generics, and
    string annotations (``"X"``).

    Args:
        pkg: Analyzed package info.
        type_name: Exact type name to search for (e.g. ``"MyModel"``).

    Returns:
        List of dicts with ``function``, ``module``, ``line``,
        and ``ref_type`` (``"param"``, ``"return"``, or ``"alias"``).
    """
    pattern = _type_name_pattern(type_name)
    refs: list[TypeRefEntry] = []

    for mod in pkg.modules:
        mod_name = module_dotted_name(mod.path, pkg.root)

        refs.extend(
            _scan_functions_for_type(
                mod.functions,
                mod_name,
                pattern,
            )
        )

        for cls in mod.classes:
            refs.extend(
                _scan_functions_for_type(
                    cls.methods,
                    mod_name,
                    pattern,
                    class_name=cls.name,
                )
            )

        # Module-level type aliases (e.g. ``type Foo = X``).
        for var in mod.variables:
            ann = var.annotation or ""
            val = var.value_repr or ""
            if pattern.search(ann) or pattern.search(val):
                refs.append(
                    TypeRefEntry(
                        function=var.name,
                        module=mod_name,
                        line=var.line,
                        ref_type="alias",
                    )
                )

    return refs

map_tests(symbol, project_root)

Find test files that reference a given symbol.

Scans tests/ directory for test_*.py files containing the symbol name.

Parameters:

Name Type Description Default
symbol str

Name to search for in test files.

required
project_root Path

Root of the project.

required

Returns:

Type Description
list[Path]

List of test file paths that reference the symbol.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
def map_tests(symbol: str, project_root: Path) -> list[Path]:
    """Find test files that reference a given symbol.

    Scans ``tests/`` directory for test_*.py files containing
    the symbol name.

    Args:
        symbol: Name to search for in test files.
        project_root: Root of the project.

    Returns:
        List of test file paths that reference the symbol.
    """
    tests_dir = project_root / "tests"
    if not tests_dir.is_dir():
        return []

    matching: list[Path] = []
    for test_file in sorted(tests_dir.glob("test_*.py")):
        try:
            content = test_file.read_text(encoding="utf-8")
            if symbol in content:
                matching.append(test_file)
        except OSError:
            continue

    return matching

score_impact(report, weights=None)

Score impact as LOW, MEDIUM, or HIGH.

Parameters:

Name Type Description Default
report ImpactReport | ImpactResult

ImpactReport (or dict with the same keys) describing the symbol's caller / reexport / module footprint.

required
weights ImpactWeights | None

Optional override; defaults to module-level weights.

None

Returns:

Type Description
str

Impact level string.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
Python
def score_impact(
    report: ImpactReport | ImpactResult,
    weights: ImpactWeights | None = None,
) -> str:
    """Score impact as LOW, MEDIUM, or HIGH.

    Args:
        report: ``ImpactReport`` (or dict with the same keys) describing
            the symbol's caller / reexport / module footprint.
        weights: Optional override; defaults to module-level weights.

    Returns:
        Impact level string.
    """
    impact = _coerce_report(report)
    w = weights or ImpactWeights()

    total = (
        len(impact.callers) * w.caller_weight
        + len(impact.reexports) * w.reexport_weight
        + len(impact.affected_modules) * w.module_weight
        + len(impact.git_coupled) * w.coupled_weight
        + len(impact.type_refs) * w.typeref_weight
    )

    if total >= w.high_threshold:
        return "HIGH"
    if total >= w.medium_threshold:
        return "MEDIUM"
    return "LOW"