19 KiB
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 16–18, 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 rungit add,git commit,git amend, orgit 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-24outrank every instruction in these plans. src/config/architecture/modules.jsonis 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
trueandfalse, 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:
- its detail
@ConfigurationPropertiesare neither bound nor validated; - it owns zero production beans;
- no socket, client, connection pool, session, executor, scheduler, or watcher is created;
- JPA-off additionally means zero
DataSource/HikariDataSource, zeroEntityManagerFactory, zero Flyway, and zero DB health/metrics beans; - no migration and no schema validation runs;
- no health contributor and no actuator detail is registered;
- for an inbound adapter, no route, schema, or controller is exposed;
- an invalid detail setting left in the environment does not block startup;
- 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_DISABLEDwhen 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.4in docs and CI. config/runtime/compose-profile-contracts.jsonis the SSOT for lane → profile → file stack → Spring runtime → exact sorted service set.scripts/verify-compose-profile-contracts.shis the only static entry point;scripts/run-compose-runtime-smoke.shis 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 4–8 |
| 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 4–8 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 streams —
experimental, alwaysfalse, 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-bootstrapruntime 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-offare 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.