# Wave 5 — Gradle Build Logic Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development > (recommended) or superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax. > **Read [`2026-08-15-five-adapter-runtime-remediation-index.md`](2026-08-15-five-adapter-runtime-remediation-index.md) > first.** > **Entry criterion:** Wave 4 complete — a fully green, warning-zero baseline whose task graph, > dependency graph, test selection, and evidence output have been captured as a comparison artifact. **Goal:** Extract the duplicated source-set, test-lane, testkit, API-surface, evidence, registry, and dependency machinery into eight TestKit-tested convention plugins in a `build-logic` included build, so that root, settings, and leaf build files each hold only their own responsibility — with the task graph, dependency graph, test selection, and evidence semantics provably unchanged. **Architecture:** A `build-logic` included build holds precompiled convention plugins. Settings and root stop re-implementing registry parsing against each other by sharing one typed parser/validator. Every extraction is one step, and every step is verified by diffing the captured baseline artifacts — because the failure mode this wave uniquely risks is a task quietly ceasing to exist and the build reporting green for work it no longer does. **Tech Stack:** Gradle 9 included builds, precompiled script plugins (`build-logic/src/main/groovy`), Gradle TestKit. **Spec:** [`2026-08-15-five-adapter-runtime-remediation-review-design.md`](../specs/2026-08-15-five-adapter-runtime-remediation-review-design.md) (§10 in full, §11 Wave 5, §14 closing paragraph) --- ## Global Constraints Inherited from the index. Wave 5 adds: - **Never share a diff with a runtime change.** Spec §14 is explicit: build refactoring on top of a red or unverified baseline produces false evidence, because a task that stops existing looks the same as a task that passes. - **LOC is not the goal.** Do not hide logic to move a number. Completion is judged by whether the same source-set/`Test`/API-surface machinery is still copied into two or more leaves, and whether root, settings, and leaf hold only their stated responsibilities. - **Each extraction step is verified by artifact diff, not by "the build still works".** - **Provider semantics stay per-module.** Image, scenario, security requirement, and promotion meaning differ per provider and stay in each module's registry. Do not merge them into one over-general DSL — spec §8.2 names that as a pattern to avoid. - **Do not remove a dependency a provider actually uses.** The exclusion convention exists to make declared exclusions real, not to strip dependencies centrally. --- ## Measured baseline | File | Lines | | --- | ---: | | `src/build.gradle` | 2,767 | | `src/settings.gradle` | 185 | | `src/app-bootstrap/build.gradle` | 273 | | `src/adapter/outbound/persistence-jpa/build.gradle` | 352 | | `src/adapter/outbound/persistence-mongo/build.gradle` | 346 | | `src/adapter/outbound/messaging/build.gradle` | 105 | | `src/adapter/outbound/notification/build.gradle` | 39 | | `src/adapter/inbound/graphql/build.gradle` | 241 | | `src/gradle/jpa-evidence.gradle` | 930 | The duplication that matters, not the size: - custom source set + `extendsFrom` + `Test` task + `failOnNoDiscoveredTests`, repeated per lane; - strict qualification lane registration and result-directory wiring, repeated; - testkit artifact/source-set wiring, repeated; - API-surface snapshot render/update/verify machinery, repeated; - registry parsing and validation implemented **twice**, once in settings and once in root verification; - release-evidence manifest and output handling, repeated; - dependency exclusion intent that drifts from the resolved graph — e.g. the notification build claims a YAML exclusion (`adapter/outbound/notification/build.gradle:21-33`) while SnakeYAML remains in its lockfile (`gradle.lockfile:165`). And one policy violation: `settings.gradle:37-44` hardcodes `expectedModuleCount = 44`, with a comment arguing the count should be a deliberate decision — but the project policy (`AGENTS.md:52-55`) makes the registry the count's SSOT and `verifyDocumentedLeafCount` enforces it against documents. A number in the build file is the same drift the policy forbids in prose. Meanwhile `verifyDocumentedLeafCount` inspects only some root documents, so a stale count in a module `CLAUDE.md` — for example `src/app-bootstrap/CLAUDE.md`'s "19-leaf dependency list" — is not caught. --- ## Task 0: Capture the comparison baseline **Files:** - Create: `scripts/capture-build-baseline.sh` - Create: `docs/superpowers/plans/evidence/2026-08-15-wave5-baseline/` **Context:** This is the instrument the whole wave is judged by. Capture, from the green Wave 4 state: 1. `./gradlew tasks --all` — the full task list; 2. per-module `./gradlew :dependencies --configuration runtimeClasspath`; 3. every lane's JUnit XML **class list** (not timings); 4. every evidence directory's file list and manifest schema; 5. `./gradlew ` output. Normalize timestamps, durations, and absolute paths, so a diff shows semantic change only. - [ ] **Steps 1–4:** write the script, run it, commit the baseline, and prove the script is deterministic by running it twice and diffing (must be identical). --- ## Task 1: `ca.architecture-registry` — one parser for settings and root **Files:** - Create: `build-logic/settings.gradle`, `build-logic/build.gradle` - Create: `build-logic/src/main/groovy/ca.architecture-registry.gradle` - Create: `build-logic/src/main/java/dev/caskeleton/buildlogic/registry/ModuleRegistry.java` - Create: `build-logic/src/test/java/dev/caskeleton/buildlogic/registry/ModuleRegistryTest.java` - Modify: `src/settings.gradle`, `src/build.gradle` **Context:** First extraction because everything else reads the registry. One typed parser/validator, fail-closed on: a project directory with a `build.gradle` that the registry does not list; a `source_path` that does not exist; a duplicate ID or path; and drift between the resolved runtime project closure and declared memberships (Wave 1 Task 13 already made the closure the comparison input). Remove `expectedModuleCount`. The registry is the count. Extend `verifyDocumentedLeafCount` to cover tracked root `AGENTS.md`, root `CLAUDE.md`, and **all** `src/**/CLAUDE.md`, and wire it into `check` and CI. Prefer removing the duplicated number from each document over asserting it — a count that is not written cannot drift. Where a document genuinely needs the number, it must be generated. - [ ] **Steps 1–8:** TestKit tests first (malformed registry, missing path, duplicate ID, membership drift), then the plugin, then delete both re-implementations, then diff against the baseline. ## Task 2: `ca.strict-test-lane` **Files:** `build-logic/src/main/groovy/ca.strict-test-lane.gradle` + TestKit tests; then apply to the JPA, Mongo, GraphQL, messaging, and app-bootstrap leaves one at a time. **Context:** The single largest duplication. The convention owns source set creation, configuration `extendsFrom`, the `Test` task, `failOnNoDiscoveredTests`, stale-XML deletion, required-class enforcement, and the results directory — the shape `src/gradle/graphql-platform-conventions.gradle:55-100` implements by hand today. Semantics that must survive verbatim, because each was written against a real failure: stale JUnit XML is deleted before the lane runs (a deleted class would otherwise report as executed), and a lane that executes no test case for a required class fails with a message saying the lane is green only because the class is gone. TestKit cases: empty lane fails; duplicate task registration fails; a required class with no executed test case fails; stale XML is removed. - [ ] **Steps 1–9:** one leaf per step, diffing lane task names and JUnit XML class lists against the baseline after each. ## Task 3: `ca.api-surface` Read-only `verify` plus an explicitly-named approved `update` task. The two must not be the same task with a flag — an update that runs by default silently blesses a surface change. - [ ] **Steps 1–6:** TestKit tests, extraction, per-leaf application, diff. ## Task 4: `ca.testkit-publisher` Testkit source set and consumable artifact, currently repeated. `app-bootstrap` consumes `project(path: ':adapter:outbound:persistence-jpa', configuration: 'jpaTestkit')`; the convention must keep that consumer contract byte-identical. - [ ] **Steps 1–6.** ## Task 5: `ca.evidence` Manifest and result schema, deterministic output ordering, and a no-empty-evidence rule. Applies to `src/gradle/jpa-evidence.gradle` (930 lines) and the Mongo/GraphQL equivalents. Provider-specific promotion meaning stays in each module registry. - [ ] **Steps 1–7.** ## Task 6: `ca.dependency-policy` Common exclusions and constraints, plus verification that each configuration's **resolved graph and lockfile** match the declared intent. The notification/SnakeYAML case is the acceptance test: a build that declares an exclusion while the lockfile still carries the dependency must fail. Guard rail: a dependency a provider genuinely uses directly is never centrally removed. - [ ] **Steps 1–7.** ## Task 7: `ca.java-leaf` Java 21 toolchain, encoding, compiler flags (`-Werror -Xlint:deprecation -Xlint:unchecked`, currently `src/build.gradle:353`), Error Prone, and the baseline test task. - [ ] **Steps 1–6.** ## Task 8: `ca.optional-adapter` Activation metadata plus disabled/on composition-contract wiring for the five adapters. This is the convention that makes Wave 1's off-invariant testing a build-level default rather than something each leaf remembers. - [ ] **Steps 1–6.** --- ## Task 9: Reduce the three build files to their responsibilities **Files:** `src/settings.gradle`, `src/build.gradle`, each leaf `build.gradle` Target responsibilities: - `settings.gradle`: plugin management, root project name, and applying the registry settings plugin. Nothing else. - root `build.gradle`: shared plugin and version declarations plus architecture-wide lifecycle tasks. - leaf `build.gradle`: plugins, project and external dependencies, and that leaf's own semantic lane/matrix. - [ ] **Steps 1–5:** reduce, run the full verification set, diff against the baseline, commit. --- ## Wave 5 Exit Criteria - [ ] `./scripts/capture-build-baseline.sh` output **diffs clean** against the Wave 4 baseline for the task list, dependency graphs, JUnit XML class lists, and evidence manifests. A task that disappeared is a failure even if everything green stayed green. - [ ] `cd src && ./gradlew clean check --warning-mode=fail --no-daemon --console=plain` — green. - [ ] `./gradlew verifyCleanArchitectureDependencies verifyEnvKeys verifyRuntimeModuleMembership verifyPublicPathSnapshot verifyDocumentedLeafCount --console=plain` — green. - [ ] `build-logic`'s own TestKit suite is green and covers malformed registry, empty lane, duplicate task, and membership drift. - [ ] `expectedModuleCount` is gone from `src/settings.gradle`. - [ ] `verifyDocumentedLeafCount` covers root `AGENTS.md`, root `CLAUDE.md`, and every `src/**/CLAUDE.md`, and is wired into `check` and CI. - [ ] No source-set/`Test`/API-surface machinery is copied into two or more leaves. - [ ] `./scripts/run-compose-runtime-smoke.sh --matrix ...` — still green, proving the refactor did not change what actually runs. ## What Wave 5 explicitly does not do - No runtime, configuration, or test-behaviour change in the same diff. - No LOC-driven relocation that hides logic. - No merging of provider-specific release matrices into one generic DSL. - No central removal of a dependency a provider uses directly.