aspect changed told you which files you touched. That’s a step on the way to the question you actually have: which tests could my change break?
Predict before you run
lib/log is near the bottom of the stack and nearly everything is above it. There are 55 tests in this repository. How many do you think a one-line change to lib/log affects?
base: line, not an abbreviation — the task prints what git merge-base handed it. Yours will be a different commit.
Now a package nothing much depends on:
How it works
.aspect/impact.axl is 294 lines, and you do not need to read them in order. The shape is _impl near the bottom — nineteen lines that do nothing but call the others, and you have the whole algorithm. Then read the two functions that exist because the obvious version is wrong: _classify, which sorts changed paths into the three things they can be, and _declared, which decides which of them Bazel actually knows about. Skim the rest: _emit_human and _emit_json are formatting, and the module docstring at the top is this page in prose.
Three steps, and all three are in _impl:
- Diff against the merge base, including uncommitted and untracked files.
- Label each path by walking up to its nearest
BUILD.bazel—lib/log/log.gobecomes//lib/log:log.go. Source files are real nodes in Bazel’s target graph, which is why this works at all. - Reverse-dep: one
tests(rdeps(//..., <labels>)).
ctx.bazel.query returns objects, not text. No --output=label, no splitting lines, no regex. t.name is the label.
Where it breaks
Two things in that file exist because the obvious version is wrong. Not every changed path is a target. Edit aREADME, a go.mod, or add a source file Gazelle hasn’t indexed, and rdeps fails outright with no such target — taking the whole query down. So _declared runs a cheap query of its own first, //<pkg>:* over the touched packages, which lists every target Bazel actually declares there; the candidate labels are then kept or dropped by dict membership in Starlark. Note the division of labour: Bazel answers “what exists”, and the filtering happens in AXL. (The one real Bazel intersect in this course is in the next section, in policy.axl.) Anything dropped is reported as skipped rather than silently discarded, because “my file isn’t in any srcs” usually means somebody forgot to run Gazelle.
A changed BUILD file reaches nothing. Package loading is not a target-graph edge, so rdeps on a BUILD.bazel returns empty. The case that bites is adding a test over a source file you didn’t otherwise touch: the only changed path is the BUILD file, so the new test is invisible. Build exactly that:
//<pkg>:all:
//lib/clock:all names the new rule directly, and //lib/clock:clock_smoke_test is in the 35.
Now look at the cost, because the page owes you that too. 35 of 55 — 64% of the suite — from a BUILD-file edit that added one test to a leaf package. :all is package-granular: it names //lib/clock:clock as well, so you inherit everything above lib/clock, which is almost everything. A source edit to lib/clock/clock.go selects 34 on its own, so escalation bought you exactly one test here and charged you nothing extra — but only because lib/clock holds nothing unrelated. In a package with several independent targets, escalating one BUILD edit pulls in the dependents of all of them.
That is the trade, and it is why escalation is the default: a selection that is too wide wastes CPU, and one that is too narrow tells you a test can’t break when it can. --escalate=false exists only to show you the narrow version failing.
When it won’t answer
MODULE.bazel is not a label, so it contributes no labels; no labels means no rdeps query; no query means no tests. The floor is zero, and zero is an honest floor.
A .bzl or MODULE.bazel change can rewrite any rule in the repository, and standard bazel query has no reverse lookup for them. The task could have widened to //... and printed 55. It doesn’t, because a number you invented is worse than a warning you have to read: --format=json carries "complete": false alongside "affected_count": 0, and a caller that cares can branch on it.
This is not a CI gate
Be clear about what this is for. bazel-diff hashes the target graph at two revisions and diffs the hashes; target-determinator configures the graph at both commits and compares. Both are more correct than this. Both also need two revisions, which means two checkouts and two full analyses. You cannot run either against uncommitted work. That’s the gap this fills: what does my dirty tree affect, right now, in one query — the question you have while you’re still typing, which no amount of CI tooling answers.Your turn
--run hands the selection to bazel test and returns its exit code. Three tests, and you never named them.

