# 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` 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 1–7:** 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 1–6:** 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 1–6:** 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 1–6:** 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 1–7:** 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 1–6:** 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).