255 lines
14 KiB
Markdown
255 lines
14 KiB
Markdown
# 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 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).
|