Files
clean-architecture-backend-…/docs/superpowers/plans/2026-08-15-wave5-gradle-build-logic.md
T

240 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <path>: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 <every architecture-wide verify task>` output.
Normalize timestamps, durations, and absolute paths, so a diff shows semantic change only.
- [ ] **Steps 14:** 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 18:** 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 19:** 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 16:** 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 16.**
## 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 17.**
## 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 17.**
## 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 16.**
## 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 16.**
---
## 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 15:** 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.