14 KiB
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. Read2026-08-15-five-adapter-runtime-remediation-index.mdfirst. 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
(§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_INFRASTRUCTUREto quiet a checker are all forbidden.ROLE_INFRASTRUCTUREis 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 viaStartupWarningRecorder, 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 BeanPostProcessors 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
BeanPostProcessorCheckerrecords 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
profilefield equalsEnvironment#getActiveProfiles()[0]for each oflocal,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:
spring-boot.ls.problem.boot2.MISSING_CONFIGURATION_ANNOTATION: "IGNORE"— its own comment names the cause: two stereotype-free legacy shims inadapter:outbound:httpclient(OutboundHttpClientConfig,OutboundHttpResilienceConfig) that cannot take@Configurationbecause both composition roots component-scandev.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 toWARNING..vscode/jdt-compiler.prefs:21-24ignores 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.StartupWarningZeroTestgreen forlocal,dev,prod, all-off and each one-on combination.src/config/runtime/warning-allowlist.jsoncontains 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
profilefield equalsEnvironment#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_INFRASTRUCTUREmarking 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).