Files
clean-architecture-backend-…/docs/superpowers/plans/2026-08-15-wave4-warning-zero.md
T

255 lines
14 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 4 — Runtime Warning and IDE Error Zero 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 3 complete — the Compose lane matrix passes and each environment starts
> from its own env source.
**Goal:** Zero WARN and zero ERROR in `local`, `dev`, and `prod` startup logs with an **empty**
allowlist, plus a structured-log profile field that agrees with the real active profile, plus IDE
suppressions narrowed to the exact false positives they were written for.
**Architecture:** Every warning is fixed at its cause, never suppressed. The three runtime warning
families each have a different root cause and therefore a different fix: the Micrometer warnings are an
ordering problem (a filter installed after meters exist), the `BeanPostProcessorChecker` warnings are a
dependency-graph problem (a bean resolved too early), and the Flyway one is diagnosed before it is
fixed because "framework bug" and "application eager dependency" call for opposite responses. The
warning gate itself is the Wave 0 recorder, promoted from characterization to a blocking check.
**Tech Stack:** Micrometer `MeterRegistryCustomizer`/`MeterFilter`, Spring `ObjectProvider`, Logback
`springProfile`, Eclipse JDT preferences, Spring Tools LS settings.
**Spec:** [`2026-08-15-five-adapter-runtime-remediation-review-design.md`](../specs/2026-08-15-five-adapter-runtime-remediation-review-design.md)
(§9 in full, §11 Wave 4)
---
## Global Constraints
Inherited from the index. Wave 4 adds:
- **Fix the cause, never the symptom.** Lowering a log level, adding a logger exclusion, or marking a
bean `ROLE_INFRASTRUCTURE` to quiet a checker are all forbidden. `ROLE_INFRASTRUCTURE` is called out
by name in spec §9.2 because it looks like a fix and is a mute button.
- **The allowlist is empty by default and empty at the end.** A third-party warning that genuinely
cannot be removed during implementation may be quarantined in a registry entry carrying an owner, an
upstream issue link, and an expiry date — but the final warning-zero judgement requires **zero
allowlist entries** unless the user separately approves an exception.
- **A Gradle gate does not speak for the IDE.** IDE Problems zero is confirmed by a human, against a
named JDK, extension set, and settings file. Do not claim a Gradle task verified it.
- **Hikari leak-detection messages are not memory leaks.** They are a distinct diagnostic; connection
leaks and ThreadLocal/executor lifecycle get their own tests rather than being folded into this
wave's warning count.
---
## Baseline
Reproduced during the review and pinned by Wave 0 Task 6:
| Warning | Source | Task |
| --- | --- | --- |
| `BeanPostProcessorChecker` early instantiation of `RolePermissionPolicy`, `RolePermissionRegistry`, `AuthorizationAdapter` | authorization E2E bean-creation chain | 2 |
| ×2 "meter registered before MeterFilter added" | `MetricsContractConfig.java:17-50` installs filters in `@PostConstruct` | 1 |
| `BeanPostProcessorChecker` on a Flyway converter (dev only) | Boot/Flyway configuration ordering | 3 |
| structured-log `profile` field disagrees with the real active profile | `logback-spring.xml:8-9` reads `SPRING_PROFILES_ACTIVE` with `defaultValue="local"` | 4 |
Also confirmed green and to be kept green: `./gradlew help --warning-mode all` and
`./gradlew compileJava compileTestJava --warning-mode all` both succeed with no deprecation or
`-Werror` output. Java compilation already runs `-Werror -Xlint:deprecation -Xlint:unchecked`
(`src/build.gradle:353`).
---
## Task 1: Install meter filters before the registry has meters
**Files:**
- Modify: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/metrics/MetricsContractConfig.java`
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/metrics/SampleMetricsContractConfig.java`
- Test: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/metrics/MeterFilterOrderingTest.java`
**Context:** `MetricsContractConfig` fetches the registry in `@PostConstruct` and installs filters
then — by which point meters registered during earlier bean construction already exist, and Micrometer
warns that the filter cannot apply to them. The filter is not merely noisy; it is **partially
ineffective**, which is the real defect. A naming or tag policy that skips the meters registered before
it produces inconsistent metric names in production.
The same late-filter assembly is duplicated in `SampleMetricsContractConfig.java:18-41`. Fixing only
the app-bootstrap copy leaves the warning reproducible from the sample composition root, so both change
together.
The fix is `MeterRegistryCustomizer<MeterRegistry>` beans, which Boot applies at registry creation, with
explicit `@Order` where filters must compose in a defined sequence.
- [ ] **Step 1:** Write `MeterFilterOrderingTest` — a context asserting (a) zero Micrometer warnings
via `StartupWarningRecorder`, and (b) that a meter registered by the earliest-constructed bean
still carries the filter's effect, which is the assertion that proves the fix rather than the
silence.
- [ ] **Step 2:** Run to verify it fails.
- [ ] **Step 3:** Convert both configs to `MeterRegistryCustomizer`.
- [ ] **Step 4:** Run to verify it passes.
- [ ] **Step 5:** Run `./gradlew :app-bootstrap:test :sample-portfolio:test --console=plain --no-daemon`.
- [ ] **Step 6:** Commit.
---
## Task 2: Remove the authorization early-instantiation chain
**Files:**
- Create: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/security/AuthorizationBeanGraphTest.java`
- Modify: whichever configuration the reproduction identifies
**Context:** `RolePermissionPolicy`, `RolePermissionRegistry`, and `AuthorizationAdapter` are
instantiated before all `BeanPostProcessor`s are ready, so they are not eligible for post-processing —
meaning AOP, `@Transactional`, and metrics decoration may silently not apply to them. That is the harm;
the log line is only how it is visible.
**Diagnose before fixing.** Build a minimal context that reproduces the chain and identify which
consumer resolves `AuthorizationPort` eagerly — spec §9.2 points at an infrastructure advisor and a
Spring Data projection post-processor as the likely candidates, but *likely* is not a diagnosis. Only
then choose between deferring the lookup through `ObjectProvider`/`Supplier` and excluding an
unnecessary slice auto-configuration.
Forbidden: marking the beans `ROLE_INFRASTRUCTURE`. It silences the checker and leaves the beans
un-post-processed, which is the actual problem.
- [ ] **Steps 17:** minimal reproduction → named diagnosis recorded in the evidence log → fix →
assert zero `BeanPostProcessorChecker` records for these three types → full suite → commit.
---
## Task 3: Diagnose and fix the Flyway converter warning
**Files:**
- Create: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/migration/FlywayConfigurationOrderingTest.java`
- Modify: as the diagnosis dictates
**Context:** Appears on `dev` only. Pin the Boot/Flyway configuration creation order in a minimal
reproduction, then decide: a framework bug is reported upstream and quarantined with an owner and
expiry; an application eager dependency is fixed here. The two answers are opposite, so guessing costs
more than reproducing.
Note that Wave 1 Task 9 moved migration under the JPA capability root, so this warning now appears only
in JPA-on contexts — reproduce it there.
- [ ] **Steps 16:** reproduce → diagnose → fix or quarantine with owner/issue/expiry → assert → commit.
---
## Task 4: Make the log profile field agree with the active profile
**Files:**
- Modify: `src/app-bootstrap/src/main/resources/logback-spring.xml:8-9`
- Test: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/logging/LogProfileAgreementTest.java`
**Context:** Verified at HEAD: logback reads `SPRING_PROFILES_ACTIVE` with `defaultValue="local"`,
independently of Spring's resolved profile. Overriding the profile on the CLI to `prod` while a stale
`local` value sits in the environment produces production logs stamped `local` — the field that exists
precisely so somebody can tell which environment a log line came from, lying about it.
Wave 3 Task 1 removed the profile default from `application.yml`, so the remaining fallback here is the
last one. Replace the `springProperty` read with `spring.profiles.active` resolved by Spring, and remove
the `defaultValue` entirely — with Wave 3's validator, an absent profile can no longer reach a running
application, so a default would only mask a contradiction.
- [ ] **Steps 16:** TDD cycle; the test asserts the emitted `profile` field equals
`Environment#getActiveProfiles()[0]` for each of `local`, `dev`, `prod`.
---
## Task 5: Narrow the IDE suppressions
**Files:**
- Modify: `.vscode/settings.json`
- Modify: `.vscode/jdt-compiler.prefs`
**Context:** Two blanket suppressions, both currently global:
1. `spring-boot.ls.problem.boot2.MISSING_CONFIGURATION_ANNOTATION: "IGNORE"` — its own comment names
the cause: two stereotype-free legacy shims in `adapter:outbound:httpclient`
(`OutboundHttpClientConfig`, `OutboundHttpResilienceConfig`) that cannot take `@Configuration`
because both composition roots component-scan `dev.caskeleton.adapter`. **Wave 1 Task 3 narrowed
those scans**, which removes the reason: convert both shims to structural imports under their
capability root, then restore the setting to `WARNING`.
2. `.vscode/jdt-compiler.prefs:21-24` ignores three JDT warning categories. Build-versus-JDT
divergence is real and these are documented, so keep them — but confirm each is still needed by
flipping it back and observing the diagnostics, and record what each currently suppresses.
Note `.vscode/` is listed in `.gitignore`, so these files are local. Record the reviewed settings in
`docs/ide/vscode-baseline.md` so the human's IDE-zero confirmation is reproducible against a named
configuration rather than against whatever their editor happens to hold.
- [ ] **Steps 16:** convert the two shims to structural imports → restore the Spring LS setting to
`WARNING` → confirm zero new diagnostics → review the three JDT entries and document them →
commit.
---
## Task 6: Promote the warning recorder to a blocking gate
**Files:**
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/activation/StartupWarningZeroTest.java`
- Modify: `scripts/run-compose-runtime-smoke.sh`
- Create: `src/config/runtime/warning-allowlist.json`
**Context:** Extend `StartupWarningZeroTest` from the one all-off `local` case to all three
environments and to each one-on activation combination.
**Wave 0 recorded this test as green, and that is a measurement gap, not good news.** It boots
`WebApplicationType.NONE` with every adapter off, while the warnings in the baseline table were
observed under `bootRun` — a **web** application with JPA active. The extension must therefore use
`WebApplicationType.SERVLET` and JPA-on combinations, or the gate will keep passing while every
warning it exists to catch is still emitted. See
`docs/superpowers/plans/evidence/2026-08-15-wave0-baseline.md`, "Deviations", item 1. The allowlist file ships **empty**, with a
schema requiring `owner`, `upstreamIssue`, and `expiry` on any entry, and a check that fails an entry
whose expiry has passed — so a temporary quarantine cannot become permanent by being forgotten.
Wave 3's runtime-smoke wrapper already writes a warning/error summary per lane. Make a non-empty
summary fail the lane, so the gate covers real container startups and not only in-JVM tests.
- [ ] **Steps 17:** extend the test → add the allowlist schema and expiry check → make the wrapper
fail on non-empty → run the full matrix → confirm zero → commit.
---
## Task 7: Separate the resource-leak question from the warning question
**Files:**
- Create: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/resource/ConnectionLeakTest.java`
- Create: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/resource/ThreadLifecycleTest.java`
**Context:** Spec §9.2 item 5 is explicit that a Hikari leak-detection message is not a memory leak and
must not be treated as one. These two tests answer the question the message raises, on their own terms:
a connection acquired and not returned is detected as a leak; every executor, scheduler, and ThreadLocal
created by an adapter is released when its context closes.
The thread half reuses `AdapterActivationInventory.liveThreadNamesMatching` (Wave 0 Task 2) — start a
context with one adapter on, close it, and assert the adapter's threads are gone.
- [ ] **Steps 16:** TDD cycle for both.
---
## Wave 4 Exit Criteria
- [ ] `cd src && ./gradlew clean compileJava compileTestJava --warning-mode=fail --no-daemon --console=plain` — green.
- [ ] `cd src && ./gradlew test --warning-mode=fail --no-daemon --console=plain` — green.
- [ ] `StartupWarningZeroTest` green for `local`, `dev`, `prod`, all-off and each one-on combination.
- [ ] `src/config/runtime/warning-allowlist.json` contains **zero** entries.
- [ ] `./scripts/run-compose-runtime-smoke.sh --matrix src/config/runtime/compose-profile-contracts.json`
— every lane's warning/error summary is empty.
- [ ] The structured-log `profile` field equals `Environment#getActiveProfiles()[0]` in every smoke.
- [ ] `./gradlew wave0RedReport` — empty.
- [ ] A human has confirmed IDE Problems zero against the configuration recorded in
`docs/ide/vscode-baseline.md`, and that confirmation is recorded with the JDK and extension
versions used. **This is a human step; no Gradle task may claim it.**
## What Wave 4 explicitly does not do
- No log-level lowering, logger exclusion, or `ROLE_INFRASTRUCTURE` marking to reach silence.
- No allowlist entry without owner, upstream issue, and expiry — and none surviving to the exit check.
- No claim that a Gradle gate verified the IDE.
- No build-logic extraction (Wave 5).