Files

14 KiB
Raw Permalink Blame History

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 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 (§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 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 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).