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'

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

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

Returns:

Type Description
dict[str, Any]

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
def analyze_impact(
    path: Path,
    symbol: str,
    *,
    project_root: Path | None = None,
    exclude_tests: bool = False,
) -> dict[str, Any]:
    """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.

    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: dict[str, Any] = {
        "symbol": symbol,
        "definition": definition,
        "callers": [
            {
                "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.
    result["score"] = score_impact(result)

    if exclude_tests:
        result["callers"] = [
            c for c in result["callers"] if not _is_test_module(c["module"])
        ]
        result["type_refs"] = [
            r for r in result["type_refs"] if not _is_test_module(r["module"])
        ]

    return result

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

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

Returns:

Type Description
dict[str, Any]

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
def analyze_impact_workspace(
    ws_path: Path,
    symbol: str,
    *,
    exclude_tests: bool = False,
) -> dict[str, Any]:
    """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.

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

    Example:
        >>> result = analyze_impact_workspace(Path("/ws"), "ToolResult")
        >>> result["score"]
        'HIGH'
    """
    from axm_ast.core.workspace import analyze_workspace

    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

    definition = None
    for pkg in ws.packages:
        defn = find_definition(pkg, symbol)
        if defn is not None:
            defn["package"] = pkg.name
            definition = defn
            break

    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: dict[str, Any] = {
        "symbol": symbol,
        "workspace": ws.name,
        "definition": definition,
        "callers": [
            {
                "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, definition, ws, ws_path)
    result["score"] = score_impact(result)

    if exclude_tests:
        result["callers"] = [
            c for c in result["callers"] if not _is_test_module(c["module"])
        ]

    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
dict[str, Any] | None

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

Source code in packages/axm-ast/src/axm_ast/core/impact.py
def find_definition(pkg: PackageInfo, symbol: str) -> dict[str, Any] | 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)

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

        if dotted is not None:
            class_name, method_path = dotted
            for cls in mod.classes:
                if cls.name == class_name:
                    result = _find_method_in_class(cls, method_path, mod_name)
                    if result is not None:
                        return result
        else:
            for fn in mod.functions:
                if fn.name == symbol:
                    return {
                        "module": mod_name,
                        "line": fn.line_start,
                        "kind": "function",
                        "signature": fn.signature,
                    }

            for cls in mod.classes:
                if cls.name == symbol:
                    return {
                        "module": mod_name,
                        "line": cls.line_start,
                        "kind": "class",
                        "name": cls.name,
                    }

    return None

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
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[dict[str, Any]]

List of dicts with function, module, line,

list[dict[str, Any]]

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

Source code in packages/axm-ast/src/axm_ast/core/impact.py
def find_type_refs(
    pkg: PackageInfo,
    type_name: str,
) -> list[dict[str, Any]]:
    """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[dict[str, Any]] = []

    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(
                    {
                        "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
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(result)

Score impact as LOW, MEDIUM, or HIGH.

Parameters:

Name Type Description Default
result dict[str, Any]

Dict with callers, reexports, affected_modules, and optionally git_coupled and type_refs.

required

Returns:

Type Description
str

Impact level string.

Source code in packages/axm-ast/src/axm_ast/core/impact.py
def score_impact(result: dict[str, Any]) -> str:
    """Score impact as LOW, MEDIUM, or HIGH.

    Args:
        result: Dict with callers, reexports, affected_modules,
            and optionally git_coupled and type_refs.

    Returns:
        Impact level string.
    """
    caller_count = len(result.get("callers", []))
    reexport_count = len(result.get("reexports", []))
    module_count = len(result.get("affected_modules", []))
    coupled_count = len(result.get("git_coupled", []))
    type_ref_count = len(result.get("type_refs", []))

    total = (
        caller_count
        + reexport_count * 2
        + module_count
        + coupled_count
        + type_ref_count
    )

    if total >= _IMPACT_HIGH_THRESHOLD:
        return "HIGH"
    if total >= _IMPACT_MEDIUM_THRESHOLD:
        return "MEDIUM"
    return "LOW"