Entry 08Verification · static analysis2026

Which user journeys could this change have broken?

Every test-impact tool on the market answers in tests, and every one of them needs coverage from a previous run to do it. testgraph answers in user journeys, from a static call graph, on a repository that has never run its suite once.

answers injourneys
coverage requirednone
dependencies0
idle MCP server15.2 MB

The question a test suite cannot answer

An agent finishes a change and has to decide whether it is done. Running the whole suite is the safe answer and the slow one; running nothing is fast and dishonest. The tools built for that gap — SeaLights, Datadog’s test impact analysis, pytest-testmon, Ekstazi — all narrow the suite by watching which tests touched which lines on a previous run.

That mechanism is also their prerequisite. Coverage is something you have only after you have run the thing, which means none of them can say anything at all about a repository on its first day, or about the parts of a repository nobody has exercised yet.

And they answer the wrong unit. test_resolve_backfill failing tells you a test failed. It does not tell you that a claimant can no longer submit a signed form.

How it answers

  • A journey registry names each user-facing flow and the symbols it enters through — a route handler, a scheduler sweep, a CLI entry point.
  • Seeds — the changed line ranges in a diff are mapped to the symbols that own them.
  • Reverse walk — the call graph is walked backwards, transitively, to every symbol that depends on a seed.
  • Rank — the journeys whose entry symbols land in that set are reported, ordered by fan-in.
git push
testgraph[…]: 11 journey(s) this push could break,
d9174b7e1..0305ada6f, ranked:

  [ 23] J1   claimant submits a signed form
  [ 16] J10  staff regenerates and downloads the signed PDF
  …

Real hook output from a private repository, its name removed. The two figures are the fan-in rank and the journey’s identifier in that repository’s registry.

Confidence is the strongest path that reached the journey: the maximum, over every path, of the weakest edge on that path. A chain is only as trustworthy as its worst hop, but one solid route through is enough. A journey reached only through weak or synthesized edges is labelled VERIFY MANUALLY — it is never silently dropped from the list.

Recall-first, and what that costs

The failure modes are not symmetric. Flagging a journey that turns out to be fine costs a few minutes. Silently dropping a journey a change really did break costs a production incident and the user’s trust in the tool at the same time. So testgraph over-selects on purpose: edit a shared config file and it fans out to almost everything, because that is the truth about a shared config file.

The same asymmetry drives the integrity guard. Before answering, testgraph refuses to run off a stale or corrupted index — a wrong graph produces a confidently wrong “you don’t need to test that”, which is the one output a test selector must never produce. Seeds come from line ranges, so an index built before the code moved resolves the diff against spans that no longer mean anything. If a changed file’s bytes still disagree with the indexed copy after a re-sync, the answer degrades to RECALL DEGRADED and names the file.

What I will not claim

The obvious pitch is suite reduction, and I am not making it. The regression-test-selection literature is directionally unkind to static selection: measured against a dynamic baseline, the static tool STARTS reduced suites 68.3% against Ekstazi’s 84.1%, and carried a 5.9% safety-violation rate doing it. Dynamic beat static on both size and safety.

testgraph has selectivity numbers of its own across four repositories, and they are not published here, because it has no measured missed-fault rate at those scales to put beside them. Recall 1.00 was established on exactly one target — five hand-labelled real commits and roughly twenty seeded mutation sites — and a recall figure from one repository is not a property of the tool.

What is defensible is narrower and still decisive: it answers cold. Zero coverage, zero CI, zero prior runs. That is a positioning advantage rather than a moat — an incumbent could collect coverage on build one, or bolt static analysis on — and it is not free either, because a maintained journey registry is real hand-authored judgement that nothing derives for you.

A selector nobody calls cannot be wrong

  • It runs on push. pre-push is the only git hook handed a real base — the remote sha is exactly the “what they don’t have yet” boundary the question wants.
  • It never fails a push. Every path exits zero: a blocked integrity guard, a missing index, a missing registry, a traceback, a timeout. Advice that can stop a push stops being advice.
  • It records itself. Every run appends a row to a local ledger, which is how “is anything actually calling this?” gets answered with a number instead of a guess.
  • Agents can ask it directly. An MCP server exposes the ranked journeys as a tool — stdlib only, lazily imported, 15.2 MB resident when idle against 62–69 MB for a typical SDK-based server. That gap is multiplied by every editor window you have open, not paid once.

The other half of that ledger is what running a journey then found. Joined on repository and commit, the two halves produce the one number the project has so far asserted rather than measured: a journey that failed on a commit whose selection never named it.

Try it

Static analysis Test selection Python MCP
install
pip install testgraph

python3 -m testgraph.propose --repo <path>   # draft a journey registry
python3 -m testgraph.select  --repo <path>   # rank the journeys a diff touches

MIT licensed. A drafted registry is marked unapproved until a human reads it — an unapproved registry runs, loudly, but never silently.

Questions or a use case in mind? Email is best — [email protected] · Back to portfolio