Structural, not typed and honest about it
impact does not type-check. It is not rust-analyzer, tsc, or gopls wearing a different hat. It resolves a call by name: exact qualified path first, then a shorter form, then a bare identifier.
When two symbols share a short name, it does not pick one and stay quiet. It reports both. A blast-radius tool should over-report, not under-report. A false positive is visible and easy to dismiss. A false negative is invisible and costs you later.
Every DIRECT / INDIRECT entry carries a confidence tier:
- Exact every hop from the thing you queried resolved unambiguously.
-
Heuristic some hop only matched a bare name that more than one candidate owns. A chain is only as trustworthy as its weakest hop.
Tree-text tags the weak ones inline (
caller::maybe_this [heuristic]).--min-confidence exact(CLI) ormin_confidence: "exact"(MCP) drops them when you only want what is certain.--explainfills in theviachain for indirect hits, so a heuristic entry is something you can verify instead of something you have to trust. ## File, symbol, or the diff you already have Three ways in. Same engine. A file. Everything declared in it.
impact query src/payment/service.rs
A specific change. Not English. A tiny grammar, so the same description always resolves the same way and an unrecognized one is a hard error, not a best-effort guess:
rename <path>
rename <path> to <path>
remove <path>
remove variant <Enum>::<Variant>
remove field <Type>.<field>
change signature of <path>
Same payments crate, asking what a signature change on save_payment hits:
$ impact change "change signature of repo::save_payment"
DIRECT
handlers::PaymentHandler::create_payment_route src/handlers.rs:6
repo::save_payment_persists src/repo.rs:7
INDIRECT
e2e_tests::creates_payment_route_end_to_end src/e2e_tests.rs:4
API
POST /payments
EVENTS
PaymentCreated
DATABASE
payments
TESTS
2 affected tests
e2e_tests::creates_payment_route_end_to_end src/e2e_tests.rs:4
repo::save_payment_persists src/repo.rs:7
The unit test that lives next to the function shows up this time. The file query seeded from declarations in repo.rs as a set; the change query seeded from one symbol. Same graph, different start node.
A unified diff. The combined blast radius of every symbol whose span the touched lines fall inside:
git diff | impact diff
One call, not one impact query per dirty file. The project has to be indexed against the working tree which is what git diff on uncommitted changes already describes.
JSON is there when a machine is reading (--json). Humans get the tree. CLI and MCP share one computation layer, so they cannot drift.
The agent does not get a chatbot. It gets a rule.
Registering an MCP server only gives an agent tools. It still needs telling when to call them.
impact install does both, for Cursor, Codex, Claude Code, and Claude Desktop: MCP server plus a hard rule.
impact install # all four clients, user (global) scope
impact install --client cursor --scope project
impact doctor # what is configured, and whether the rule is current
The rule is not "consider using impact." It is two triggers:
Before editing and before proposing a concrete rename / remove / signature change index if needed, then impact_file or impact_change. Treat a nonzero report as a checklist. Vague "here's roughly how I'd approach it" talk does not need it. A named target does.
After editing re-index, run the same query, confirm the blast radius you addressed still matches and nothing new appeared.
That is the product. The agent stops guessing what it will break.
MCP tools, if you wire them by hand:
| Tool | What it answers |
|---|---|
| impact_index | Build or refresh the local SQLite cache |
| impact_file | Blast radius of a file |
| impact_change | Blast radius of one grammar-shaped change |
| impact_diff | Blast radius of a unified diff |
claude mcp add impact -- impact mcp
Seven languages. Contracts where the shape is unambiguous.
Rust, TypeScript / TSX (React), JavaScript / JSX (React Native), Python, Go, Kotlin (Android), Swift.
The core is language-agnostic. Each language is a pluggable tree-sitter adapter. Every adapter after Rust added zero lines to impact-core that boundary is load-bearing, not a slide.
DIRECT / INDIRECT work across all seven. Contract detection is narrower, on purpose. impact only emits an API / event / table identity when the source shape is structurally certain:
-
API axum (Rust),
net/httpGo 1.22+ method-prefixed routes (HandleFunc("POST /payments", ...)), FastAPI / Flask decorators, Express / Fastify named-handler registrations. Same"{VERB} {path}"identity string, so a Go service and a Rust service registering the same route can match across a workspace. - Events / database Rust today (marker-trait or naming-suffix events; sqlx-family macros for tables).
-
Tests whatever convention a language actually agrees on:
#[test]/#[tokio::test], pytest'stestprefix,TestXxxin_test.go, JUnit@Test, XCTesttest*methods, and for JS/TS the one convention Jest / Vitest / Mocha share (.test./.spec./__tests__/). It will not pretendit()vstest()is settled. Inline Express handlers, method-less Go patterns, "maybe this identifier inside an arrow function is the route" no contract. Empty is honest. A guessed route is not.affected_testsis a list with file and line, not a count. An agent can run those tests instead of the whole suite. Caveat, because I will not sell you a compiler: a test only appears if it is a caller in the graph. A black-box CLI test that never names the function will not. ## Cross-repo, when the identity is real Sibling services do not share a call graph. They share contracts. Aworkspace.tomlregisters the other repos.--workspaceextends a report with which of them share an API route, event, or table identity tiered so a coincidence does not dress up as a dependency: -
Declared a
[[links]]entry names that exact contract. - Strong the two projects are linked in general.
-
Weak same identity, nothing declared. Always shown, always labeled. Two unrelated apps both exposing
POST /healthis a real situation. Deterministic given the same workspace and the same indexes. Not a fuzzy score. ## Fast enough that the agent can call it every time Release build, ~50k-line Rust + TypeScript workspace (207 files, 2,249 symbols), desktop CPU (Ryzen 7 7800X3D): | Operation | Time | |---|---| | Coldimpact index| ~1.6s | | Re-index, nothing changed (content-hash skip) | ~0.1s | | Warmimpact queryof one file | ~35–40ms | One machine, one project order of magnitude, not a guarantee. The number that matters in the agent loop is the middle one. The protocol says "index before the edit." You pay the cold parse once. After that, unchanged files are skipped. Cache lives in.impact/cache.sqlitenext to the project. Schema-versioned: a stale cache from an older binary gets wiped and rebuilt instead of being silently reused. Analytics, if you want them, are local (impact gain) and opt-out (IMPACT_NO_ANALYTICS=1). No network call, ever. ## Install it, then ask a real file what it breaks macOS / Linux (Homebrew):
brew install ancientice/impact/impact
impact install
macOS / Linux (script):
curl -fsSL https://raw.githubusercontent.com/AncientiCe/impact-rs/master/scripts/install.sh | sh
impact install
Windows (PowerShell):
irmhttps://raw.githubusercontent.com/AncientiCe/impact-rs/master/scripts/install.ps1|ieximpactinstall
From source, if you already have Rust:
cargo install --git https://github.com/AncientiCe/impact-rs --locked impact-cli
Not on crates.io yet. Prebuilt binaries ship for linux x86_64 / aarch64, macOS Intel / Apple Silicon, and Windows x86_64.
Then open a file you were going to let the agent touch, and run:
impact index .
impact query path/to/that/file
If the report is empty, you have a leaf. If it is not, you have a checklist and so does the agent.
Repo, releases, and the rule text impact install writes verbatim:
github.com/AncientiCe/impact-rs