Skip to content

Impact

impact

ImpactHook & DocImpactHook — blast-radius and doc-impact analysis.

Protocol hooks registered via axm.hooks entry points:

  • ast:impactImpactHook — calls analyze_impact, returns the complete impact report as HookResult metadata. Supports newline-separated symbol lists with max-score merge semantics.

  • ast:doc-impactDocImpactHook — calls analyze_doc_impact, returns doc_refs as HookResult metadata. Supports newline-separated symbol lists.

DocImpactHook

Run doc impact analysis on one or more symbols.

Reads path from params (or working_dir from context) and symbol from params. When symbol contains newline characters, each line is treated as a separate symbol.

Source code in packages/axm-ast/src/axm_ast/hooks/impact.py
Python
class DocImpactHook:
    """Run doc impact analysis on one or more symbols.

    Reads ``path`` from *params* (or ``working_dir`` from context)
    and ``symbol`` from *params*.  When *symbol* contains newline
    characters, each line is treated as a separate symbol.
    """

    def execute(self, context: dict[str, object], **params: object) -> HookResult:
        """Execute the hook action.

        Args:
            context: Session context dictionary.
            **params: Must include ``symbol`` (name to analyze).
                Optional ``path`` (overrides ``working_dir`` from context).

        Returns:
            HookResult with full report (``doc_refs``, ``undocumented``,
                ``stale_signatures``) in metadata on success.
        """
        symbol = params.get("symbol")
        if not symbol or not isinstance(symbol, str):
            return HookResult.fail("Missing required param 'symbol'")

        raw_path = params.get("path") or context.get("working_dir", ".")
        if not isinstance(raw_path, (str, Path)):
            return HookResult.fail(
                f"path must be str or Path, got {type(raw_path).__name__}"
            )
        working_dir = Path(raw_path)
        if not working_dir.is_dir():
            return HookResult.fail(f"working_dir not a directory: {working_dir}")

        try:
            from axm_ast.core.doc_impact import analyze_doc_impact

            symbols = [s.strip() for s in symbol.splitlines() if s.strip()]
            report = analyze_doc_impact(working_dir, symbols)
            return HookResult.ok(**cast("dict[str, object]", report))
        except Exception as exc:  # noqa: BLE001
            return HookResult.fail(f"Doc impact analysis failed: {exc}")
execute(context, **params)

Execute the hook action.

Parameters:

Name Type Description Default
context dict[str, object]

Session context dictionary.

required
**params object

Must include symbol (name to analyze). Optional path (overrides working_dir from context).

{}

Returns:

Type Description
HookResult

HookResult with full report (doc_refs, undocumented, stale_signatures) in metadata on success.

Source code in packages/axm-ast/src/axm_ast/hooks/impact.py
Python
def execute(self, context: dict[str, object], **params: object) -> HookResult:
    """Execute the hook action.

    Args:
        context: Session context dictionary.
        **params: Must include ``symbol`` (name to analyze).
            Optional ``path`` (overrides ``working_dir`` from context).

    Returns:
        HookResult with full report (``doc_refs``, ``undocumented``,
            ``stale_signatures``) in metadata on success.
    """
    symbol = params.get("symbol")
    if not symbol or not isinstance(symbol, str):
        return HookResult.fail("Missing required param 'symbol'")

    raw_path = params.get("path") or context.get("working_dir", ".")
    if not isinstance(raw_path, (str, Path)):
        return HookResult.fail(
            f"path must be str or Path, got {type(raw_path).__name__}"
        )
    working_dir = Path(raw_path)
    if not working_dir.is_dir():
        return HookResult.fail(f"working_dir not a directory: {working_dir}")

    try:
        from axm_ast.core.doc_impact import analyze_doc_impact

        symbols = [s.strip() for s in symbol.splitlines() if s.strip()]
        report = analyze_doc_impact(working_dir, symbols)
        return HookResult.ok(**cast("dict[str, object]", report))
    except Exception as exc:  # noqa: BLE001
        return HookResult.fail(f"Doc impact analysis failed: {exc}")

EnrichedImpactResult

Bases: ImpactResult

ImpactResult enriched with witness-friendly aliases.

Adds test_paths (alias for test_files) and packages (space-separated dirs from cross_package_impact) so that witness templates can extract them directly.

Source code in packages/axm-ast/src/axm_ast/hooks/impact.py
Python
class EnrichedImpactResult(ImpactResult, total=False):
    """ImpactResult enriched with witness-friendly aliases.

    Adds ``test_paths`` (alias for ``test_files``) and ``packages``
    (space-separated dirs from ``cross_package_impact``) so that
    witness templates can extract them directly.
    """

    test_paths: NotRequired[list[str]]
    packages: NotRequired[str]

ImpactHook dataclass

Run impact analysis on one or more symbols.

Reads path from params (or working_dir from context) and symbol from params. When symbol contains newline characters, each line is analyzed separately and results are merged (max score, concatenated lists, deduplicated modules/tests).

Source code in packages/axm-ast/src/axm_ast/hooks/impact.py
Python
@dataclass
class ImpactHook:
    """Run impact analysis on one or more symbols.

    Reads ``path`` from *params* (or ``working_dir`` from context)
    and ``symbol`` from *params*.  When *symbol* contains newline
    characters, each line is analyzed separately and results are
    merged (max score, concatenated lists, deduplicated modules/tests).
    """

    def execute(self, context: dict[str, object], **params: object) -> HookResult:
        """Execute the hook action.

        Args:
            context: Session context dictionary.
            **params: Must include ``symbol`` (name to analyze).
                Optional ``path`` (overrides ``working_dir`` from context).
                Optional ``detail`` (``"compact"`` for short format).

        Returns:
            HookResult with ``impact`` dict and ``packages`` in metadata.
            ``text`` is populated with a human-readable render via
            ``render_impact_text`` (single symbol) or
            ``render_impact_batch_text`` (multiple symbols).
            In compact mode, ``text`` is *None* and ``impact`` holds
            a pre-formatted string instead.
        """
        parsed = _parse_impact_params(context, params)
        if isinstance(parsed, HookResult):
            return parsed
        working_dir, symbol, symbols, exclude_tests, detail = parsed

        try:
            from axm_ast.core.impact import analyze_impact
            from axm_ast.tools.impact import (
                render_impact_batch_text,
                render_impact_text,
            )

            text: str | None = None
            if len(symbols) == 1:
                single_report: ImpactResult = analyze_impact(
                    working_dir,
                    symbols[0],
                    project_root=working_dir.parent,
                    exclude_tests=exclude_tests,
                )
            else:

                def _analyze(sym: str) -> ImpactResult:
                    return analyze_impact(
                        working_dir,
                        sym,
                        project_root=working_dir.parent,
                        exclude_tests=exclude_tests,
                    )

                with ThreadPoolExecutor(max_workers=min(len(symbols), 4)) as pool:
                    reports: list[ImpactResult] = list(pool.map(_analyze, symbols))

                if detail == "compact":
                    from axm_ast.tools.impact import format_impact_compact

                    return HookResult.ok(
                        impact=format_impact_compact(
                            cast("list[Mapping[str, object]]", reports)
                        ),
                    )
                text = render_impact_batch_text(reports)
                merged = _merge_impact_reports(symbol, reports)
                # The merged dict is a superset of ImpactResult (adds
                # ``definitions``), but downstream consumers only read
                # ImpactResult-shaped fields plus the enrichment additions.
                single_report = cast("ImpactResult", merged)

            if detail == "compact":
                from axm_ast.tools.impact import format_impact_compact

                return HookResult.ok(
                    impact=format_impact_compact(
                        cast("Mapping[str, object]", single_report)
                    )
                )

            if len(symbols) == 1:
                text = render_impact_text(single_report)

            enriched = _enrich_report(single_report)
            return HookResult(
                success=True,
                metadata={
                    "impact": enriched,
                    "packages": enriched.get("packages", ""),
                },
                text=text,
            )
        except Exception as exc:  # noqa: BLE001
            return HookResult.fail(f"Impact analysis failed: {exc}")
execute(context, **params)

Execute the hook action.

Parameters:

Name Type Description Default
context dict[str, object]

Session context dictionary.

required
**params object

Must include symbol (name to analyze). Optional path (overrides working_dir from context). Optional detail ("compact" for short format).

{}

Returns:

Type Description
HookResult

HookResult with impact dict and packages in metadata.

HookResult

text is populated with a human-readable render via

HookResult

render_impact_text (single symbol) or

HookResult

render_impact_batch_text (multiple symbols).

HookResult

In compact mode, text is None and impact holds

HookResult

a pre-formatted string instead.

Source code in packages/axm-ast/src/axm_ast/hooks/impact.py
Python
def execute(self, context: dict[str, object], **params: object) -> HookResult:
    """Execute the hook action.

    Args:
        context: Session context dictionary.
        **params: Must include ``symbol`` (name to analyze).
            Optional ``path`` (overrides ``working_dir`` from context).
            Optional ``detail`` (``"compact"`` for short format).

    Returns:
        HookResult with ``impact`` dict and ``packages`` in metadata.
        ``text`` is populated with a human-readable render via
        ``render_impact_text`` (single symbol) or
        ``render_impact_batch_text`` (multiple symbols).
        In compact mode, ``text`` is *None* and ``impact`` holds
        a pre-formatted string instead.
    """
    parsed = _parse_impact_params(context, params)
    if isinstance(parsed, HookResult):
        return parsed
    working_dir, symbol, symbols, exclude_tests, detail = parsed

    try:
        from axm_ast.core.impact import analyze_impact
        from axm_ast.tools.impact import (
            render_impact_batch_text,
            render_impact_text,
        )

        text: str | None = None
        if len(symbols) == 1:
            single_report: ImpactResult = analyze_impact(
                working_dir,
                symbols[0],
                project_root=working_dir.parent,
                exclude_tests=exclude_tests,
            )
        else:

            def _analyze(sym: str) -> ImpactResult:
                return analyze_impact(
                    working_dir,
                    sym,
                    project_root=working_dir.parent,
                    exclude_tests=exclude_tests,
                )

            with ThreadPoolExecutor(max_workers=min(len(symbols), 4)) as pool:
                reports: list[ImpactResult] = list(pool.map(_analyze, symbols))

            if detail == "compact":
                from axm_ast.tools.impact import format_impact_compact

                return HookResult.ok(
                    impact=format_impact_compact(
                        cast("list[Mapping[str, object]]", reports)
                    ),
                )
            text = render_impact_batch_text(reports)
            merged = _merge_impact_reports(symbol, reports)
            # The merged dict is a superset of ImpactResult (adds
            # ``definitions``), but downstream consumers only read
            # ImpactResult-shaped fields plus the enrichment additions.
            single_report = cast("ImpactResult", merged)

        if detail == "compact":
            from axm_ast.tools.impact import format_impact_compact

            return HookResult.ok(
                impact=format_impact_compact(
                    cast("Mapping[str, object]", single_report)
                )
            )

        if len(symbols) == 1:
            text = render_impact_text(single_report)

        enriched = _enrich_report(single_report)
        return HookResult(
            success=True,
            metadata={
                "impact": enriched,
                "packages": enriched.get("packages", ""),
            },
            text=text,
        )
    except Exception as exc:  # noqa: BLE001
        return HookResult.fail(f"Impact analysis failed: {exc}")