feat/v2-explain: Add --explain task flag and ActionSource decision-trace#20
Merged
vedanthvdev merged 1 commit intomasterfrom Apr 22, 2026
Merged
feat/v2-explain: Add --explain task flag and ActionSource decision-trace#20vedanthvdev merged 1 commit intomasterfrom
vedanthvdev merged 1 commit intomasterfrom
Conversation
Operators landing on a surprising affected-tests outcome (full suite when they expected a selection, or a skip when they expected a run) had no in-band way to ask "why?" — the engine's reasoning lived in debug logs that CI runners do not capture by default. This change adds an `--explain` flag to the `affectedTest` task that prints the full decision trace and exits without dispatching tests, alongside an `ActionSource` enum on `AffectedTestsConfig` so the trace can name which tier of the priority ladder (explicit `onXxx`, legacy boolean, mode default, pre-v2 hardcoded) picked each situation's action. The trace now surfaces the per-bucket file breakdown, the resolved situation + action, the action's source tier, and the full per-situation matrix on every run — closing the "why did my explicit setting not win?" debugging loop without requiring a code-spelunking session. The renderer is a pure function over `AffectedTestsConfig` and `AffectedTestsResult` so it is exercised by unit tests instead of needing a Gradle test runtime, and the engine's `AffectedTestsResult` now carries a `Buckets` record so explain output stays consistent with whatever the engine actually saw. README is updated with a sample trace and the new step in the Quick Start.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an
--explainflag to theaffectedTestGradle task that prints the full decision trace and exits without running tests. Closes the second of the deferred Phase 1 follow-ups from the v2 design doc.Operators landing on a surprising outcome (full suite when they expected a selection, a skip when they expected a run) had no in-band way to ask why — the engine's reasoning lived in debug logs that CI runners do not capture by default. This PR makes that reasoning a first-class output.
What's in the box
ActionSourceenum inaffected-tests-core— names which tier of the priority ladder picked each situation's action (EXPLICIT,LEGACY_BOOLEAN,MODE_DEFAULT,HARDCODED_DEFAULT). Resolved alongsideActioninAffectedTestsConfigso the two stay in lockstep.Bucketsrecord onAffectedTestsEngine.AffectedTestsResult— every result now carries the per-bucket file breakdown (ignored / out-of-scope / production / test / unmapped) the mapper produced, so the trace matches what the engine actually saw.--explaintask option onAffectedTestTask— when set, the task renders the trace via lifecycle log lines and returns before dispatching tests. Marked@Internalso flipping the flag never invalidates a cached execution.renderExplainTraceas a pure function over config + result, so unit tests exercise the format without spinning up a live Gradle runtime.Sample trace
```
=== Affected Tests — decision trace (--explain) ===
Base ref: origin/master
Mode: unset (effective: n/a (pre-v2 defaults))
Changed files: 3
Buckets:
ignored 1
out-of-scope 0
production .java 1
test .java 0
unmapped 1
ignored sample: README.md
production sample: src/main/java/com/example/Foo.java
unmapped sample: build.gradle
Situation: UNMAPPED_FILE
Action: FULL_SUITE (source: pre-v2 hardcoded default)
Outcome: FULL_SUITE — runAllOnNonJavaChange=true / onUnmappedFile=FULL_SUITE — non-Java or unmapped file in diff
Action matrix (situation → action [source]):
EMPTY_DIFF SKIPPED [pre-v2 hardcoded default]
ALL_FILES_IGNORED SKIPPED [pre-v2 hardcoded default]
ALL_FILES_OUT_OF_SCOPE SKIPPED [pre-v2 hardcoded default]
UNMAPPED_FILE FULL_SUITE [pre-v2 hardcoded default]
DISCOVERY_EMPTY SKIPPED [pre-v2 hardcoded default]
DISCOVERY_SUCCESS SELECTED [explicit onXxx setting]
=== end --explain ===
```
Tests
`./gradlew check` passes (25 unit tests + functional tests).
Test plan