> ## Documentation Index
> Fetch the complete documentation index at: https://aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# What does my working tree affect?

> Turn changed files into Bazel labels and reverse-dependency queries, and learn exactly where the approach stops working.

```shell theme={null}
git checkout step-3
git diff step-2 step-3
```

`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?

```shell theme={null}
printf '\n// a change\n' >> lib/log/log.go
aspect impact
```

```text theme={null}
→ 🎬 Running impact task
base:    main (f54a552456bbc8d7de678ebac87f7ad0c4a38ddd)
changed: 1 file(s) -> 1 queried label(s)
    //lib/log:log.go
affected tests: 33
//cmd/cron:cron_test
//cmd/server:server_test
//cmd/worker:worker_test
//internal/domain/audit:audit_test
...
```

**33 of 55.** That is the whole merge-base SHA on the `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:

```shell theme={null}
git checkout -- lib/log/log.go
printf '\n// a change\n' >> internal/domain/geo/model.go
aspect impact
```

```text theme={null}
→ 🎬 Running impact task
base:    main (f54a552456bbc8d7de678ebac87f7ad0c4a38ddd)
changed: 1 file(s) -> 1 queried label(s)
    //internal/domain/geo:model.go
affected tests: 4
```

**4 of 55.** Same command, same repository, same size of edit — 60% of the suite versus 7%. That difference is the entire argument for asking the question.

## 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`:

1. **Diff** against the merge base, including uncommitted and untracked files.
2. **Label** each path by walking up to its nearest `BUILD.bazel` — `lib/log/log.go` becomes `//lib/log:log.go`. Source files are real nodes in Bazel's target graph, which is why this works at all.
3. **Reverse-dep**: one `tests(rdeps(//..., <labels>))`.

Note what the code *doesn't* do:

```python theme={null}
return sorted({t.name: None for t in ctx.bazel.query(...)})
```

`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 a `README`, 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:

```shell theme={null}
git checkout -- .
cat >> lib/clock/BUILD.bazel <<'EOF'

go_test(
    name = "clock_smoke_test",
    srcs = ["clock_test.go"],
    embed = [":clock"],
)
EOF
git status --short
```

```text theme={null}
 M lib/clock/BUILD.bazel
```

A brand-new test target, and one changed path — the BUILD file. Now the miss:

```shell theme={null}
aspect impact --escalate=false
```

```text theme={null}
→ 🎬 Running impact task
base:    main (f54a552456bbc8d7de678ebac87f7ad0c4a38ddd)
changed: 1 file(s) -> 0 queried label(s)
affected tests: 0

→ ✅ Passed impact task in 86ms
```

Zero labels, zero tests, exit 0. You added a test and the tool told you nothing could break. Now the default, which escalates a changed BUILD file to `//<pkg>:all`:

```shell theme={null}
aspect impact
```

```text theme={null}
→ 🎬 Running impact task
base:    main (f54a552456bbc8d7de678ebac87f7ad0c4a38ddd)
changed: 1 file(s) -> 1 queried label(s)
    //lib/clock:all  (package escalation: changed BUILD file)
affected tests: 35
//cmd/cron:cron_test
...
//lib/clock:clock_smoke_test
```

`//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.

```shell theme={null}
git checkout -- .
```

## When it won't answer

```shell theme={null}
git checkout -- .
printf '\n' >> MODULE.bazel
aspect impact
```

```text theme={null}
→ 🎬 Running impact task
WARNING: 1 build-logic file(s) changed, so the real affected set is WIDER than what is listed below:
  MODULE.bazel
  A .bzl or MODULE.bazel change can alter any rule in the repo. Reverse-resolving that
  needs `rbuildfiles`, which standard `bazel query` does not provide (Sky Query only).
  Treat the selection as a floor and run the full suite before you push.
base:    main (f54a552456bbc8d7de678ebac87f7ad0c4a38ddd)
changed: 1 file(s) -> 0 queried label(s)
affected tests: 0

→ ✅ Passed impact task in 86ms
```

Read the bottom half, because it is the confusing part and it is deliberate. You were just told the real affected set is **wider** than what follows, and what follows is **zero** — under a green ✅, exit 0. `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.

```shell theme={null}
aspect impact --format=json | jq '{affected_count, complete, unresolvable}'
```

```text theme={null}
{
  "affected_count": 0,
  "complete": false,
  "unresolvable": [
    "MODULE.bazel"
  ]
}
```

A tool that knows when it doesn't know is fine for advice and useless as a gate — which is exactly the line being drawn.

<Warning>
  **A skipped file is the other kind of incomplete answer, and the terminal barely mentions it.** Add a source file Gazelle hasn't indexed yet:

  ```shell theme={null}
  git checkout -- .
  echo 'package clock' > lib/clock/newthing.go
  aspect impact
  ```

  ```text theme={null}
  → 🎬 Running impact task
  base:    main (f54a552456bbc8d7de678ebac87f7ad0c4a38ddd)
  changed: 1 file(s) -> 0 queried label(s)
      //lib/clock:newthing.go  SKIPPED: not a declared target (no rule lists it)
  affected tests: 0

  → ✅ Passed impact task in 254ms
  ```

  `affected tests: 0`, green ✅, exit 0 — on the terminal that single `SKIPPED` line is the whole warning, so read it. The JSON is blunter, because `complete` covers every way the selection can be a floor and not only unresolvable build logic:

  ```shell theme={null}
  aspect impact --format=json | jq '{affected_count, complete, skipped_not_a_target}'
  ```

  ```text theme={null}
  {
    "affected_count": 0,
    "complete": false,
    "skipped_not_a_target": [
      "//lib/clock:newthing.go"
    ]
  }
  ```

  A caller that branches on `complete` is therefore covered for all three cases — unresolvable build logic, a changed path no rule lists, and one outside every package. `skipped_not_a_target` and `skipped_no_package` are still worth reading, because they say *which* file to hand to Gazelle.

  ```shell theme={null}
  rm lib/clock/newthing.go
  ```
</Warning>

## This is not a CI gate

Be clear about what this is for. [bazel-diff](https://github.com/Tinder/bazel-diff) hashes the target graph at two revisions and diffs the hashes; [target-determinator](https://github.com/bazel-contrib/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

```shell theme={null}
git checkout -- .
printf '\n// a change\n' >> lib/queue/queue.go
aspect impact --run
```

`--run` hands the selection to `bazel test` and returns its exit code. Three tests, and you never named them.

```shell theme={null}
git checkout -- .
```

Next: a question about your repository that no off-the-shelf tool answers.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.