archy_impact
Identify which internal modules are impacted by changes to specific files before refactoring. Returns the blast radius and propagation cost to assess structural consequences.
Instructions
Given a list of changed file paths, return the internal modules that transitively import any of them (the blast radius). Use before refactoring or removing a module to see what would break. Files that don't resolve to any module in the graph are returned in unresolved. propagation_cost is the MacCormack-style blast-radius scalar: fraction of the project's internal module count that this edit set can reach (changed plus impacted, over total internal modules). Higher values mean the edit is more structurally consequential.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| files | Yes |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| changed | Yes | ||
| unresolved | Yes | ||
| impacted | Yes | ||
| propagation_cost | No |
Implementation Reference
- src/archy/mcp.py:380-398 (registration)Tool registration in FastMCP server - registers 'archy_impact' with its name, description, and signature (path + files list, returns Impact model).
@server.tool( name="archy_impact", description=( "Given a list of changed file paths, return the internal modules " "that transitively import any of them (the blast radius). Use " "before refactoring or removing a module to see what would break. " "Files that don't resolve to any module in the graph are returned " "in `unresolved`. `propagation_cost` is the MacCormack-style " "blast-radius scalar: fraction of the project's internal module " "count that this edit set can reach (changed plus impacted, over " "total internal modules). Higher values mean the edit is more " "structurally consequential." ), ) def archy_impact( path: str, files: list[str], ) -> Impact: return _run_impact(Path(path), files=[Path(f) for f in files]) - src/archy/mcp.py:662-665 (handler)Handler function that delegates to find_impact. Builds the import graph, resolves relative file paths against the project root, then calls find_impact.
def _run_impact(path: Path, *, files: list[Path]) -> Impact: graph = _load_graph(path, internal_only=True) resolved = [path / f if not f.is_absolute() else f for f in files] return find_impact(graph, resolved) - src/archy/impact.py:24-30 (schema)The Impact output model (pydantic BaseModel) with fields: changed (resolved modules), unresolved (files not in graph), impacted (modules transitively depending on changed ones), propagation_cost (MacCormack-style blast-radius scalar).
class Impact(BaseModel): model_config = ConfigDict(frozen=True) changed: tuple[str, ...] unresolved: tuple[str, ...] impacted: tuple[str, ...] propagation_cost: float = 0.0 - src/archy/impact.py:75-83 (helper)Helper function that builds a mapping from absolute file paths to module qualnames, used to resolve the input file list to graph nodes.
def _index_by_path(graph: nx.DiGraph) -> dict[Path, str]: out: dict[Path, str] = {} for qualname, data in graph.nodes(data=True): if data.get("external"): continue raw = data.get("path") if raw: out[Path(raw).resolve()] = qualname return out - src/archy/impact.py:33-72 (handler)Core implementation of blast-radius analysis. Resolves file paths to module qualnames, finds all transitive dependents (ancestors in the directed graph), computes propagation_cost as the fraction of internal modules reachable, and returns an Impact result.
def find_impact(graph: nx.DiGraph, files: list[Path]) -> Impact: """Resolve `files` to qualnames and return everything that depends on them. `impacted` is the set of internal modules with a directed path to any changed module (via `nx.ancestors`), minus the changed set itself. `propagation_cost` is `(|changed| + |impacted|) / N_internal`: the fraction of the project's internal module count that this edit set can reach (the two sets are disjoint by construction), a MacCormack- style blast-radius scalar. Output tuples are sorted for deterministic JSON. """ path_to_qualname = _index_by_path(graph) changed: set[str] = set() unresolved: list[str] = [] for f in files: resolved = f.resolve() qualname = path_to_qualname.get(resolved) if qualname is None: unresolved.append(str(f)) else: changed.add(qualname) impacted: set[str] = set() for q in changed: if q in graph: impacted |= nx.ancestors(graph, q) impacted -= changed impacted = {q for q in impacted if not graph.nodes[q].get("external")} internal_count = sum(1 for _, d in graph.nodes(data=True) if not d.get("external")) reachable = len(changed) + len(impacted) propagation_cost = (reachable / internal_count) if internal_count else 0.0 return Impact( changed=tuple(sorted(changed)), unresolved=tuple(sorted(unresolved)), impacted=tuple(sorted(impacted)), propagation_cost=propagation_cost, )