12 KiB
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. Read2026-08-15-five-adapter-runtime-remediation-index.mdfirst. 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
(§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+Testtask +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:
./gradlew tasks --all— the full task list;- per-module
./gradlew <path>:dependencies --configuration runtimeClasspath; - every lane's JUnit XML class list (not timings);
- every evidence directory's file list and manifest schema;
./gradlew <every architecture-wide verify task>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.shoutput 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.expectedModuleCountis gone fromsrc/settings.gradle.verifyDocumentedLeafCountcovers rootAGENTS.md, rootCLAUDE.md, and everysrc/**/CLAUDE.md, and is wired intocheckand 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.