Skip to content

feat/v2-explain: Add --explain task flag and ActionSource decision-trace#20

Merged
vedanthvdev merged 1 commit intomasterfrom
feat/v2-explain
Apr 22, 2026
Merged

feat/v2-explain: Add --explain task flag and ActionSource decision-trace#20
vedanthvdev merged 1 commit intomasterfrom
feat/v2-explain

Conversation

@vedanthvdev
Copy link
Copy Markdown
Owner

Summary

Adds an --explain flag to the affectedTest Gradle 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

  • ActionSource enum in affected-tests-core — names which tier of the priority ladder picked each situation's action (EXPLICIT, LEGACY_BOOLEAN, MODE_DEFAULT, HARDCODED_DEFAULT). Resolved alongside Action in AffectedTestsConfig so the two stay in lockstep.
  • Buckets record on AffectedTestsEngine.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.
  • --explain task option on AffectedTestTask — when set, the task renders the trace via lifecycle log lines and returns before dispatching tests. Marked @Internal so flipping the flag never invalidates a cached execution.
  • renderExplainTrace as a pure function over config + result, so unit tests exercise the format without spinning up a live Gradle runtime.
  • README gains a third Quick Start step with a sample trace.

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

  • `AffectedTestsConfigTest#actionSourceReflectsResolutionTierOrdering` — pins the explicit > legacy > mode > hardcoded ordering for `actionSourceFor()`.
  • `AffectedTestTaskExplainFormatTest` (new) — eight cases covering header/footer, bucket breakdown + truncation, every action source tier, the full action matrix in stable order, and the three outcome shapes (FULL_SUITE / SKIPPED / SELECTED).
  • Existing `AffectedTestTaskLogFormatTest` updated for the new `Buckets` field on `AffectedTestsResult`.

`./gradlew check` passes (25 unit tests + functional tests).

Test plan

  • `./gradlew check` green locally
  • Pilot in modulr-security-service to verify trace renders cleanly in real CI logs
  • Wire the `--explain` flag into team docs once merged

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.
@vedanthvdev vedanthvdev merged commit 7db6418 into master Apr 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant