240 lines
12 KiB
Markdown
240 lines
12 KiB
Markdown
# 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 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.
|