Files
clean-architecture-backend-…/docs/superpowers/plans/2026-08-15-five-adapter-runtime-remediation-index.md

19 KiB
Raw Permalink Blame History

Five-Adapter Runtime Remediation — Plan Index

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement the wave plans task-by-task. Steps use checkbox (- [ ]) syntax for tracking. Read this index first — its Global Constraints section is implicitly part of every task in every wave plan.

Goal: Ship a single app-bootstrap bootJar that carries the MongoDB, Messaging, Notification, JPA, and GraphQL runtime facades on one classpath, each governed by an explicit master env switch that defaults to false, with false meaning zero beans/sockets/threads/endpoints and true meaning startup-time fail-closed dependency validation.

Architecture: One activation authority per adapter. Each of the five adapters gets exactly one master-gated root auto-configuration registered in AutoConfiguration.imports; that root owns the master condition and imports every child configuration. The composition root's broad component scan and @ConfigurationPropertiesScan are narrowed so a leaf's stereotypes and @ConfigurationProperties cannot be discovered outside its root. Vendor Spring Boot auto-configuration (JPA/Flyway/Hikari, Mongo, GraphQL, Kafka/Rabbit) is blocked in the off state by AutoConfigurationImportFilters, following the mechanism MongoOptInAutoConfigurationImportFilter already establishes. Subordinate capabilities that consume an adapter (outbox relay, JDBC idempotency, distributed lock, notification store, DB readiness) are computed from the same dependency closure and fail closed at startup rather than at first request.

Tech Stack: Java 21, Spring Boot 4.0.0, Gradle 9.0.0 (multi-module, src/ as the Gradle root), JUnit 5 + AssertJ, ArchUnit 1.3.0, Testcontainers, Flyway, PostgreSQL 1618, MongoDB, Kafka/RabbitMQ, Keycloak, MinIO, Docker Compose 5.4.0 (spec floor: 2.24.4).

Spec: docs/superpowers/specs/2026-08-15-five-adapter-runtime-remediation-review-design.md


Baseline facts verified at HEAD 2f5d2fc

These were re-verified in this repository before the plans were written. Every wave argues from them; do not re-derive them from the spec's prose.

Fact Evidence
Registry has 44 modules; adapter-outbound-persistence-mongo has allowed_dependencies: [] and runtime_memberships: [] src/config/architecture/modules.json
adapter-inbound-graphql has runtime_memberships: [] same
All 24 messaging-* leaves have runtime_memberships: [] same
app-bootstrap.allowed_dependencies has 12 entries and lists neither mongo, graphql, nor any messaging-* platform leaf same
CaSkeletonApplication already excludes dev\.caskeleton\.bootstrap\.autoconfigure\..* from its component scan, but its @ConfigurationPropertiesScan has no such exclusion src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java:29-53,66-68
app-bootstrap registers 3 auto-configurations: fileserver, httpclient, jpa src/app-bootstrap/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
The JPA platform auto-configuration lives in app-bootstrap, not in the JPA leaf src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
Mongo registers 2 auto-configurations with no master-gated single root src/adapter/outbound/persistence-mongo/src/main/resources/META-INF/spring/...imports
Messaging starter registers 5 independent auto-configurations, none master-gated src/messaging/messaging-spring-boot-starter/src/main/resources/META-INF/spring/...imports
spring.profiles.active: ${SPRING_PROFILES_ACTIVE:local} — profileless boots as local src/app-bootstrap/src/main/resources/application.yml:22-24
ca-skeleton.outbox.relay-enabled: true is the shipped default src/app-bootstrap/src/main/resources/application.yml:539
APP_IDEMPOTENCY_PROVIDER default is jdbc src/app-bootstrap/src/main/resources/application.yml:352
management.endpoint.health.group.readiness.include: readinessState,db is static, with validate-group-membership: true src/app-bootstrap/src/main/resources/application.yml:248,278
src/.env is git-tracked; no .env.example and no .env.local.example exist git ls-files | grep '\.env'
logback-spring.xml reads SPRING_PROFILES_ACTIVE with defaultValue="local", independent of the real active profile src/app-bootstrap/src/main/resources/logback-spring.xml:8-9
scripts/ holds only 3 files; there is no compose verification or runtime-smoke script ls scripts/
infra/ has no keycloak/ or minio/ directory find infra -maxdepth 2 -type d
Full test fails on exactly one test with two offenders reproduced below

The one reproduced red test

$ cd src && ./gradlew :messaging:messaging-observability:test \
      --tests '*SecretLeakStaticScanTest*' --console=plain --no-daemon

SecretLeakStaticScanTest > noSensitiveIdentifierIsConcatenatedIntoAString() FAILED
java.lang.AssertionError: [a concatenated secret never reaches the redactor, so it must not be written at all]
Expecting empty but was: ["KafkaSecurityConfigurer.java:104 + oauth.credentialId());",
    "InMemoryAdminOperationJournal.java:110 existing.leaseToken() + 1,"]

Root cause, confirmed by reading the scanner: CONCATENATION_OPERAND captures a method call including its trailing () (src/messaging/messaging-observability/src/test/java/dev/caskeleton/messaging/observation/SecretLeakStaticScanTest.java:39-42), but DESCRIBES_RATHER_THAN_REVEALS anchors its safe suffixes with $ (same file, :47-49). So tail is the string credentialId(), the $ anchor never matches Id, and the safe-suffix exemption is dead for every method call. The second offender, existing.leaseToken() + 1, is numeric fencing — an integer increment, which cannot concatenate at all — and needs a separate exemption for numeric operands.

Environment capabilities confirmed

Tool Version Consequence
Docker Engine 29.7.2 Wave 3 Compose lanes are executable here
Docker Compose 5.4.0 above the spec's 2.24.4 floor, so !override merge semantics are available
JDK 21.0.11 matches the toolchain

Global Constraints

Every task in every wave plan implicitly includes this section.

Repository policy

  • Commit policy is human-only. Agents do not run git add, git commit, git amend, or git push. Where a wave task says "Commit", it means: stop, report the staged-file list and the proposed message to the human, and let them commit. (AGENTS.md:65)
  • The eight HARD-STOP conditions in AGENTS.md:17-24 outrank every instruction in these plans.
  • src/config/architecture/modules.json is the only source of a leaf's Gradle path, allowed dependency edges, and runtime memberships. Never infer them from a document.
  • Focused tests are derived as ./gradlew <gradle_path>:test --console=plain, read from that registry.
  • Non-trivial work ends with an LLM Wiki capture at /home/donghyeon/workspace/ai-tool/llm-wiki-private/raw/branch-notes/<branch-name>.md (AGENTS.md:79-90).
  • All Gradle commands run from src/, which is the Gradle root.

Activation contract (spec §5.1) — exact values, copied verbatim

Adapter canonical env Spring property default
JPA APP_PERSISTENCE_JPA_ENABLED ca-skeleton.persistence-jpa.enabled false
MongoDB APP_PERSISTENCE_MONGO_ENABLED ca-skeleton.persistence-mongo.enabled false
Messaging APP_MESSAGING_ENABLED app.messaging.enabled false
Notification APP_NOTIFICATION_PLATFORM_ENABLED ca-skeleton.notification.platform.enabled false
GraphQL APP_GRAPHQL_ENABLED backend.graphql.enabled false

Subordinate selectors registered alongside them:

env Spring property contract
APP_PERSISTENCE_MONGO_ACTIVE_PROFILE ca-skeleton.persistence-mongo.active-profile required non-blank when Mongo is on; selects exactly one profile
APP_GRAPHQL_DEPLOYMENT_MODE backend.graphql.deployment-mode required when GraphQL is on; one of LOCAL, DEV, PRODUCTION_INTERNAL, PRODUCTION_PUBLIC

GraphQL deployment mode is constrained by the runtime environment:

runtime environment permitted GraphQL mode
local LOCAL
dev DEV
prod exactly one operator-named value of PRODUCTION_INTERNAL or PRODUCTION_PUBLIC

TEST is test-source only. STAGING is not permitted in a shipped env key until a stage runtime environment exists.

Master scalar parsing rule (spec §5.1)

Master scalars are parsed before any detail @ConfigurationProperties binds, and strictly:

  • unset ⇒ false;
  • the only accepted raw values are true and false, case-insensitive, with no surrounding whitespace;
  • yes, 1, on, empty string, and any typo are a configuration error, never a silent off;
  • canonical and legacy key both present ⇒ rejected as ambiguity, even when the values agree;
  • legacy key alone ⇒ migration error that names the replacement key.

The early validator must not bind the detail namespace, or it breaks the off invariant it exists to protect.

Off invariant (spec §5.2) — the acceptance shape for every "off" test

With its master switch false, an adapter must satisfy all of the following in a full-context test:

  1. its detail @ConfigurationProperties are neither bound nor validated;
  2. it owns zero production beans;
  3. no socket, client, connection pool, session, executor, scheduler, or watcher is created;
  4. JPA-off additionally means zero DataSource/HikariDataSource, zero EntityManagerFactory, zero Flyway, and zero DB health/metrics beans;
  5. no migration and no schema validation runs;
  6. no health contributor and no actuator detail is registered;
  7. for an inbound adapter, no route, schema, or controller is exposed;
  8. an invalid detail setting left in the environment does not block startup;
  9. where the application requires a port bean unconditionally, the disabled sentinel is supplied by the composition root, not by the adapter, and fails fast with ADAPTER_DISABLED when called.

This is implemented by structural gating — one root auto-configuration owning the master condition and importing children — never by repeating @ConditionalOnProperty on each bean.

Profile cardinality (spec §7.1)

A deployable runtime has exactly one environment profile. SPRING_PROFILES_ACTIVE becomes an enum local|dev|prod with no default. Missing, blank, unknown, and multi-value (local,prod) are all startup failures. Feature selection is never expressed as a supplementary Spring profile — that is what the five master switches are for. The test profile is test-source only; a release artifact booting under test is rejected.

Evidence rules

  • A finding is not closed by an auto-configuration existing; it is closed by a test that exercises the real path.
  • Class-existence assertions and test-only Basic Auth never count as release evidence.
  • Secret values must not appear in Git, rendered config, command lines, JUnit XML, or evidence artifacts.
  • A blocking lane that discovers zero tests, skips a test, or reads a stale XML fails.
  • Never claim "complete" / "all passing" / "production-ready" without the corresponding command output. Use superpowers:verification-before-completion.

Compose contract (spec §7.2)

  • Minimum Docker Compose version pinned at 2.24.4 in docs and CI.
  • config/runtime/compose-profile-contracts.json is the SSOT for lane → profile → file stack → Spring runtime → exact sorted service set.
  • scripts/verify-compose-profile-contracts.sh is the only static entry point; scripts/run-compose-runtime-smoke.sh is the only dynamic entry point. CI must not inline fragments of either.

Wave map

Each wave is a separate plan that produces working, testable software on its own. Execute them in order; a wave's exit criterion is the entry criterion of the next.

Wave Plan Delivers Exit criterion
0 wave0-red-baseline Characterization tests that pin every current defect as an explicit, named red Every spec §2 failure is reproduced by a test that fails for the documented reason
1 wave1-activation-ssot Five canonical switches, structural gating, classpath/registry alignment, dependency closure validators all-off boots on local, dev, and prod with no external infrastructure
2 wave2-module-on-path Per-adapter on-path blockers closed (JPA-INT-001..4, MNG-INT-001..5, MSG-INT-001..5, NTF-INT-001..7, GQL-INT-001..4) Each adapter's one-on lane passes against real infrastructure
3 wave3-environment-and-infra Env-source separation, profileless fail-closed, Compose contract SSOT + both scripts, Keycloak realm, MinIO round trip The full Compose lane matrix passes zero-skip with evidence
4 wave4-warning-zero MeterFilter ordering, BeanPostProcessor early-instantiation removal, Flyway warning root cause, IDE suppression narrowing, log/profile agreement local, dev, prod startup logs contain zero WARN and zero ERROR, with an empty allowlist
5 wave5-gradle-build-logic build-logic included build with eight TestKit-tested convention plugins; duplicated source-set/lane/API-surface machinery removed Task graph, dependency graph, test selection, and evidence output are byte-identical to the Wave 4 baseline
6 wave6-final-qualification Full clean check, the activation matrix, every environment smoke, doc/metadata drift checks, Wiki capture Every Definition-of-Done checkbox in spec §13 is ticked with attached evidence

Design patterns (spec §8) — where each one lands

Spec §8 is a constraint on how the waves are built, not a deliverable of its own. It is mapped here so no executor treats it as unassigned.

Pattern to apply Where
Conditional auto-configuration as a plugin boundary — one root condition owns the whole adapter graph Wave 1 Tasks 48
Strategy + registry — provider selection is a closed descriptor plus a real implementation registry; unknown or duplicate rejected at startup Wave 2 C4 (broker), D2 (notification provider)
Factory / Builder — one factory composes secret, TLS, pool, and lifecycle together Wave 2 B2 (Mongo client), C3 (broker client), D2 (provider)
State machine + fencing — durable transitions guarded by owner/fencing token and DB compare-and-set Wave 2 D5 (notification delivery), C2/C3 (outbox, settlement)
Typed settings + validator — no scattered @Value, no duplicate namespace; validate the resolved runtime object Wave 2 A1 (resolved DataSource), Wave 1 Task 10
Decorator — metrics, redaction, retry only at boundaries, never altering core behaviour Wave 4 Task 1
Pattern to avoid Enforced by
The same @ConditionalOnProperty copied onto every adapter bean Wave 1's structural gating; index §Off invariant closing paragraph
A plain factory named ...AutoConfiguration mixed with real auto-configuration Wave 1 Tasks 48 convert imported factories to @Configuration
ObjectProvider absence silently becoming a no-op, hiding missing production wiring Wave 2 C3 (no fake sender), D2 (no assembler ⇒ capability stays off)
@Primary resolving a JPA/Mongo implementation clash by accident Wave 1 Task 10's ambiguity rejection
A fake or in-memory implementation offered as a production runtime fallback Wave 2 Global Constraints ("No fake in production")
One over-general DSL merging release matrices whose provider meanings differ Wave 5 Global Constraints
Moving build.gradle content into apply from: files while leaving the duplicated model Wave 5 Task 9 exit criteria

The aim is not more patterns. It is one activation authority, one publication authority, one settings SSOT, and a real execution path.

Dependency ordering rationale

Wave 5 is deliberately last-but-one and never shares a diff with runtime changes: moving build logic on top of a red or unverified baseline produces a task graph that looks green because a task silently stopped existing (spec §14). Wave 3 depends on Wave 1 because a Compose lane cannot assert an activation report that does not exist yet. Wave 2's per-module fixes depend on Wave 1's single activation authority, or each module invents its own.


Scope boundaries carried from spec §14

These plans approve an assemblable artifact that is off by default. They do not approve every internal algorithm of the five platforms as production-ready. The following stay explicitly out of scope and must not be silently promoted:

  • Mongo reactive support — the reactive starter/auto-configuration is removed from the production runtime or blocked even when the master is on. Not listed as supported.
  • Mongo change streamsexperimental, always false, zero beans and zero threads. A replica-set qualification observing that the server could support change streams is not evidence of shipped support.
  • Mongo transactions — a typed subordinate switch defaulting to false; when on, the real replica-set capability of the data-plane credential is verified.
  • mongoShardedTest, mongoAtlasTest, mongoKmsTest — the Mongo release registry points at tasks and classes that do not exist. Either implement them with protected-environment evidence, or remove their Stable blocking claim and demote them to explicit experimental/conditional promotion. A green release manifest naming a task that does not exist is not permitted.
  • Notification at-rest payload sensitivity (NTF-INT-007) — Notification is not promoted to Stable until either application-level encryption is implemented end to end (codec/port, ciphertext envelope, key ID, rotation/history, row migration, decryption failure contract) or a written threat model justifies restricted variable types plus storage-level encryption. Plaintext storage is not approved by default.
  • Object storage inclusion in the app-bootstrap runtime is a separate decision from the five master switches. If it is not included, the MinIO smoke client is a release fixture only, never a production bean.
  • Fileserver internals are not redesigned. Only the composition consumers that break all-off are gated or turned into dependency errors; module hardening stays a separate spec.

Each wave plan restates the boundary that applies to it, so an executor reading one plan in isolation cannot promote something this index excluded.