6.5.5 Incomplete Selector Evidence

An empty affected-target set is useful only when the selector had enough evidence to prove it. Deletes, renames, stale snapshots, missing ownership mappings, and incompatible schemas all create the same dangerous symptom: the head revision contains nothing that points to work which used to exist. Before CI treats “no affected targets” as permission to skip, the selector must prove that its comparison evidence is complete for the declared revisions and configuration universe.

Absence Has Several Meanings

Suppose //lib:parser owned lib/parser.cc in the base revision and was consumed by //app:binary. The head revision deletes both the source and its target. A head-only lookup finds no owner for the deleted path. That observation does not mean the change is irrelevant; it means the evidence that connected the old file to its consumers lives on the base side.

The same ambiguity appears in less obvious forms:

  • a rename looks like one deleted path plus one added path unless the comparison retains both identities;
  • a generated file-to-label map may be missing an entry because the file has no owner, because generation failed, or because the map covers a different revision;
  • a snapshot may parse correctly while representing the wrong root universe, configuration, or selector version;
  • a cached result may have the expected Git revisions but omit environment or configuration inputs that influenced how it was produced.

Two-revision tools illustrate the required shape. bazel-diff generates graph hashes before and after changing revision, then compares those snapshots; the old snapshot is not an optimization detail but part of the evidence.1 Target Determinator likewise compares configured-target information across Git revisions, while documenting that some user and machine inputs are outside its result-cache key.2 Neither implementation removes the need to define what makes evidence compatible with your selector contract.

6.5.2 Comparing Bazel Graphs Across Revisions explains how base and head snapshots establish impact. 6.5.4 Selecting Affected Targets Across Configurations adds the supported roots and configurations. Here the question is narrower: are the artifacts presented to that comparison really the artifacts the request requires?

Validate an Evidence Envelope Before Comparing

Treat every snapshot and auxiliary mapping as an envelope with identity, not as an anonymous file that happens to deserialize. At minimum, validate:

DimensionWhat must agree
revisionimmutable base or head identity matches the request
universerequested roots and repository scope are represented
configurationsupported flags, platforms, toolchains, and comparison mode match
producerselector implementation and schema versions are compatible
dependenciesrequired ownership maps, generated metadata, and external inputs are present
freshnessartifacts were produced for this identity and are not expired or superseded

“Compatible” does not always mean byte-for-byte identical metadata. A newer reader may deliberately support an older schema, for example. That compatibility must be an explicit, tested rule. Accepting an artifact merely because parsing succeeded turns schema evolution into an undetected change in meaning.

Perform these checks before interpreting target differences. If the base snapshot belongs to another commit, comparing it successfully is still a selector failure. If a file-to-label map is absent, an empty owner lookup is not evidence of no owner. If only one of three protected configurations is present, success in that configuration says nothing about the others.

A useful internal result separates the evidence verdict from the affected labels:

{
  "outcome": "unknown",
  "reason": "base ownership map is missing",
  "request": {
    "base_revision": "<immutable-base>",
    "head_revision": "<immutable-head>",
    "universe": "<declared-universe>"
  },
  "evidence": {
    "base_graph": "present",
    "head_graph": "present",
    "base_ownership": "missing",
    "head_ownership": "present"
  }
}

This is an interface sketch, not a required wire format. Its important property is that missing evidence produces a typed unknown result instead of silently becoming an empty affected set. 6.5.1 Affected-Target Service Contract defines that outcome in the selector's service contract.

Reconstruct Deletes and Renames from Both Sides

For a deleted path, begin with base-side ownership and graph evidence:

base: lib/parser.cc -> //lib:parser -> //app:binary
head: lib/parser.cc -> <absent>

The selector should trace the old owner through the base graph and then reconcile those consumers with the requested head-side roots. It must not ask only which current target owns lib/parser.cc, because deletion guarantees that the current lookup may return nothing.

A rename needs two related checks. First, handle the old path as a deletion so its former consumers cannot disappear from consideration. Second, handle the new path as an addition using head-side ownership. A version-control rename hint can improve the explanation, but correctness should not depend on rename detection: treating the operation as delete plus add remains conservative when similarity heuristics disagree.

Targets and packages can also be renamed or removed. In those cases, labels from the base graph may not exist at the head. The selector still needs their former reverse-dependency paths to determine which surviving requested roots may be affected. If it cannot map that evidence into the head universe, the result is unknown; dropping vanished labels from the calculation would manufacture a false negative.

think

Trace: A change renames lib/parser.cc to lib/syntax.cc. The head ownership map associates the new path with //lib:syntax, but the base ownership map is unavailable. Is selecting the head-side consumers enough?

Reveal

No. The selector can trace the addition, but it cannot establish which targets consumed the old path or whether the rename also removed an old edge. Without base ownership and graph evidence, it must return unknown unless an independently proven broader boundary can be selected.

Make Staleness a Compatibility Failure

Freshness is not simply file age. A snapshot created seconds ago can be stale for the request if it represents another commit, root set, configuration, Bazel invocation context, or producer version. Conversely, an older immutable snapshot can remain valid when all of those identities match and its retention contract permits reuse.

This distinction matters for caches around selector tools. Target Determinator explicitly notes that user and system bazelrc files, environment variables, and host hardware are not all represented in its result-cache key, and provides a way to bypass result caching.3 The operational lesson is not to disable caches routinely. It is to include every input your service claims to model in the evidence identity, and to reject or conservatively bypass reuse when that identity is incomplete.

Record why an artifact was rejected. “Snapshot stale” is less useful than “base snapshot producer schema 4 is incompatible with reader schema 6” or “ownership map head revision differs from requested head.” Precise reasons let operators distinguish a lagging producer, retention gap, mixed deployment, incomplete key, and corrupt artifact without first trusting the selector's target output.

Test Missing Evidence as a First-Class Outcome

Build a fixture with a base revision, a head revision, at least one surviving requested root, and explicit graph and ownership artifacts. Then exercise five cases:

  1. Delete a source or target that had a base-side consumer.
  2. Rename a path and change its ownership or dependency edge.
  3. Substitute a base snapshot from another revision.
  4. remove one required file-to-label mapping.
  5. produce one snapshot with an incompatible selector or schema version.

Each case must either select every affected root within the declared universe or return unknown. None may produce a successful unaffected result solely because a lookup returned no rows. Also test the clean negative case: compatible evidence for both revisions completes and finds no path to the requested roots. Without that control, a system that always returns unknown would satisfy the failure fixtures without providing useful selection.

The runnable selector-evidence-lab makes that contract concrete without pretending to be a universal Bazel selector. Its checked-in base/head cases trace deleted and renamed paths through two target graphs, and its Bazel verifier rejects a stale base identity, missing ownership map, or incompatible schema before it can return an empty result. Run bazel test //... from the project to exercise the affected, unaffected, and unknown boundaries together.

The next operational step is deliberately separate. This article detects when evidence cannot support a trustworthy decision; 6.5.6 Fail-Closed Target Selection defines the conservative CI fallback, shadow comparisons, and implementation evaluation that consume that verdict.

key takeaway

Deletes and renames require both sides of the revision boundary because the head may no longer contain the old owner, target, or dependency edge. Validate revision, universe, configuration, producer/schema, required mappings, and freshness before comparing labels; parsing successfully is not proof of compatibility.

Missing, stale, or incompatible evidence must produce explicit unknown, never silent unaffected. Prove that boundary with deletion, rename, stale-base, missing-map, and incompatible-version fixtures, plus a clean negative control that shows the selector can still return a trustworthy empty set.

Check your understanding · 3 questions

1.A compatible base/head comparison covers every requested root and configuration, and no changed evidence reaches a requested root. Which result is justified?

Select one answer

2.A changed path may be a rename. Which checks keep the selector from losing an affected root?

Select all that apply

3.Match each evidence condition to the result it permits:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
The base snapshot names a different immutable revision than the request
A required base file-to-label map is absent
The reader does not support the snapshot's producer/schema version
Compatible evidence completes the comparison and reaches no requested root
0 of 3 answered

Footnotes

  1. Improving CI efficiency with Bazel querying and bazel-diff — generation and comparison of graph hashes at starting and final revisions

  2. Target Determinator — cquery-based affected-target analysis — configured-target comparison across Git revisions and documented cache boundaries

  3. Target Determinator — cquery-based affected-target analysis — inputs omitted from result-cache keys and the --nocache_results diagnostic control