feat: jpa, messaging, notification, mongo, graphql 어댑터터 리펙토링
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# docs
|
||||
|
||||
저장소의 모든 문서는 이 디렉터리 아래에 있다. 어떤 문서를 어디에 두는지가 유일한 규칙이고,
|
||||
파일 목록은 디렉터리를 직접 읽는다. 개수를 여기에 적으면 다음 문서가 추가되는 순간 틀린 글이 된다.
|
||||
|
||||
## 어댑터별 운영 문서
|
||||
|
||||
각 어댑터의 지원 범위, 설정, 보안, 운영, 마이그레이션 문서다. 코드와 함께 갱신되어야 하는 문서이고,
|
||||
`docs/httpclient/` 는 `scripts/verify-httpclient-docs.py` 가 코드에서 뽑은 이름과 대조한다.
|
||||
|
||||
| 디렉터리 | 대상 |
|
||||
| --- | --- |
|
||||
| `fileserver/` | 파일 서버 어댑터 |
|
||||
| `httpclient/` | HTTP 클라이언트 플랫폼 |
|
||||
| `jpa/` | JPA·PostgreSQL 영속성 |
|
||||
| `messaging/` | 메시징 어댑터 |
|
||||
| `mongodb/` | MongoDB 문서 영속성 (`advanced/`, `runbooks/` 포함) |
|
||||
| `notification/` | 알림 전달 플랫폼 (`adr/` 포함) |
|
||||
| `redis/` | Redis 캐시·세션 |
|
||||
|
||||
## 횡단 문서
|
||||
|
||||
| 디렉터리 | 대상 |
|
||||
| --- | --- |
|
||||
| `adr/` | 아키텍처 결정 기록 |
|
||||
| `architecture/` | 공개 API 표면 스냅숏 |
|
||||
| `evidence/` | 작업 단계별 증거·체크포인트 |
|
||||
| `registries/` | env 키·에러 코드·메트릭·헤더 등 레지스트리 SSOT |
|
||||
| `reviews/` | 모듈 코드 리뷰 결과 |
|
||||
| `runbooks/` | 장애 코드별 대응 런북 (`template.md` 기준) |
|
||||
| `security/` | 공개 경로 스냅숏 |
|
||||
|
||||
## 설계와 계획
|
||||
|
||||
| 디렉터리 | 대상 |
|
||||
| --- | --- |
|
||||
| `superpowers/specs/` | 설계서. `YYYY-MM-DD-<주제>-design.md` |
|
||||
| `superpowers/plans/` | 구현·확장 계획서. `YYYY-MM-DD-<주제>-plan.md` |
|
||||
| `superpowers/packages/` | 외부에서 납품된 설계 패키지의 README와 정적 검증 결과 |
|
||||
|
||||
`superpowers/packages/<어댑터>/` 는 설계서가 처음 전달됐을 때의 안내와 `VALIDATION.md` 검증 이력을
|
||||
남긴 기록 보관소다. 설계서·계획서 본문은 전부 `specs/` 와 `plans/` 에 있으므로 이 디렉터리에서
|
||||
문서를 찾을 필요는 없다. 각 README 상단의 보존 안내가 무엇이 옮겨졌고 무엇이 제거됐는지 밝힌다.
|
||||
|
||||
계획서 본문에는 당시 계획한 경로와 명령이 그대로 남아 있다. 그중 일부는 실제 구현에서 다른 위치로
|
||||
조정됐고, 저장소에 어떻게 대응시켰는지는 각 어댑터의 `repository-adaptation.md` 또는
|
||||
`module-mapping.md` 가 기록한다. 계획서를 사후에 고치지 않는 이유는 그렇게 하면 계획의 기록이 아니라
|
||||
결과를 계획처럼 보이게 만든 글이 되기 때문이다.
|
||||
|
||||
## 여기에 없는 것
|
||||
|
||||
- 실행되는 검증 스크립트는 문서가 아니다. `scripts/` 와 `.github/scripts/` 에 있다.
|
||||
- 모듈 레지스트리·Gradle 정책은 `src/config/architecture/modules.json` 과 `src/build.gradle` 이 소유한다.
|
||||
- 각 모듈의 지역 규칙은 해당 모듈의 `src/**/CLAUDE.md` 가 소유한다.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-08-13
|
||||
- **Design source:** `mongodb-superpowers-package/.../2026-08-11-mongodb-document-persistence-platform-design.md` §1, §2 (D-01, D-04, D-05), §5, §6
|
||||
- **Design source:** `docs/superpowers/specs/2026-08-11-mongodb-document-persistence-platform-design.md` §1, §2 (D-01, D-04, D-05), §5, §6
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ where encryption and sharding are both expensive to reverse.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
bash scripts/verify-mongodb-advanced.sh
|
||||
```
|
||||
`scripts/verify-mongodb-advanced.sh` enforced this ADR until it was removed on 2026-08-15. The
|
||||
promotion evidence categories this ADR requires are therefore no longer checked by any automated
|
||||
gate; they are a review obligation until one is rebuilt. See `docs/mongodb/repository-adaptation.md`
|
||||
§5 for the Gradle lanes the script wrapped.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# split into capability artifacts.
|
||||
# Update only after review with:
|
||||
# ./gradlew :adapter:inbound:graphql:updateGraphQlApiSurface -PapproveGraphQlApiSurfaceChange
|
||||
# types: 391
|
||||
# types: 395
|
||||
dev.caskeleton.adapter.inbound.graphql.HealthGraphqlController
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.admin.GraphQlAdminPrincipal
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.admin.GraphQlPersistedOperationAdminAuthorization
|
||||
@@ -84,7 +84,7 @@ dev.caskeleton.adapter.inbound.graphql.advanced.persisted.OperationalStoreGraphQ
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.release.GraphQlAdvancedCompatibilityMatrix
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.release.GraphQlAdvancedPromotionDecision
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.release.GraphQlAdvancedReleaseEvidence
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.release.GraphQlAdvancedReleaseFailure
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.release.GraphQlAdvancedReleaseException
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.release.GraphQlAdvancedReleaseGate
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.release.GraphQlAdvancedRunbookIndex
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.release.GraphQlAdvancedSoakScenario
|
||||
@@ -137,7 +137,7 @@ dev.caskeleton.adapter.inbound.graphql.advanced.websocket.GraphQlWebSocketConnec
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.websocket.GraphQlWebSocketLifecycle
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.websocket.GraphQlWebSocketProperties
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.websocket.GraphQlWebSocketProtocol
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.websocket.GraphQlWebSocketProtocolError
|
||||
dev.caskeleton.adapter.inbound.graphql.advanced.websocket.GraphQlWebSocketProtocolException
|
||||
dev.caskeleton.adapter.inbound.graphql.api.GraphQlClientProfile
|
||||
dev.caskeleton.adapter.inbound.graphql.api.GraphQlClientProfileName
|
||||
dev.caskeleton.adapter.inbound.graphql.api.GraphQlOperationId
|
||||
@@ -152,14 +152,18 @@ dev.caskeleton.adapter.inbound.graphql.architecture.GraphQlResolverBoundaryRules
|
||||
dev.caskeleton.adapter.inbound.graphql.architecture.GraphQlReturnTypePolicy
|
||||
dev.caskeleton.adapter.inbound.graphql.architecture.GraphQlTransportTypeRules
|
||||
dev.caskeleton.adapter.inbound.graphql.architecture.GraphQlTypeGraph
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlActivationEnvironmentPostProcessor
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlDeploymentMode
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlOffAutoConfigurationImportFilter
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlPlatformActuatorEndpoint
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlPlatformAutoConfiguration
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlPlatformConfigurationException
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlPlatformConfigurationReport
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlPlatformEnvironment
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlPlatformProperties
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlPlatformRuntime
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlPlatformSettings
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlPlatformStartupValidator
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlRetiredSafetyAxis
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlRootAutoConfiguration
|
||||
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlRuntimeTransport
|
||||
dev.caskeleton.adapter.inbound.graphql.compat.GraphQlChangeKind
|
||||
dev.caskeleton.adapter.inbound.graphql.compat.GraphQlClientOwnerApproval
|
||||
@@ -195,8 +199,8 @@ dev.caskeleton.adapter.inbound.graphql.cost.GraphQlResponseNodeCounter
|
||||
dev.caskeleton.adapter.inbound.graphql.cost.GraphQlRuntimeBudget
|
||||
dev.caskeleton.adapter.inbound.graphql.cost.GraphQlRuntimeBudgetExceededException
|
||||
dev.caskeleton.adapter.inbound.graphql.cost.GraphQlRuntimeBudgetTracker
|
||||
dev.caskeleton.adapter.inbound.graphql.cost.GraphQlStructuralLimitException
|
||||
dev.caskeleton.adapter.inbound.graphql.cost.GraphQlStructuralLimitPolicy
|
||||
dev.caskeleton.adapter.inbound.graphql.cost.GraphQlStructuralLimitViolation
|
||||
dev.caskeleton.adapter.inbound.graphql.cost.GraphQlStructuralLimits
|
||||
dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchChunker
|
||||
dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchContext
|
||||
@@ -333,7 +337,7 @@ dev.caskeleton.adapter.inbound.graphql.release.GraphQlCompatibilityMatrix
|
||||
dev.caskeleton.adapter.inbound.graphql.release.GraphQlFaultScenario
|
||||
dev.caskeleton.adapter.inbound.graphql.release.GraphQlPerformanceScenario
|
||||
dev.caskeleton.adapter.inbound.graphql.release.GraphQlReleaseEvidence
|
||||
dev.caskeleton.adapter.inbound.graphql.release.GraphQlReleaseFailure
|
||||
dev.caskeleton.adapter.inbound.graphql.release.GraphQlReleaseException
|
||||
dev.caskeleton.adapter.inbound.graphql.release.GraphQlReleaseGate
|
||||
dev.caskeleton.adapter.inbound.graphql.release.GraphQlReleaseOverride
|
||||
dev.caskeleton.adapter.inbound.graphql.release.GraphQlReleaseReportWriter
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
# root yet.
|
||||
# Update only after review with:
|
||||
# ./gradlew :adapter:outbound:persistence-mongo:updateMongoApiSurface -PapproveMongoApiSurfaceChange
|
||||
# types: 341
|
||||
# types: 343
|
||||
dev.caskeleton.adapter.outbound.mongo.MongoOptInAutoConfigurationImportFilter
|
||||
dev.caskeleton.adapter.outbound.mongo.MongoPersistenceConfig
|
||||
dev.caskeleton.adapter.outbound.mongo.MongoPersistenceProperties
|
||||
dev.caskeleton.adapter.outbound.mongo.MongoPersistenceSettings
|
||||
dev.caskeleton.adapter.outbound.mongo.MongoRootAutoConfiguration
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.MongoAdvancedCapabilityFlags
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.MongoAdvancedCapabilityGuard
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.MongoAdvancedEntryPoint
|
||||
@@ -16,7 +17,7 @@ dev.caskeleton.adapter.outbound.mongo.advanced.MongoAdvancedPolicy
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.MongoAdvancedPromotionEvidence
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.MongoAdvancedPromotionGate
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.autoconfigure.MongoAdvancedConfiguration
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.autoconfigure.MongoAdvancedProperties
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.autoconfigure.MongoAdvancedSettings
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.bridge.MongoBridgeCheckpointPolicy
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.bridge.MongoBridgeOutboxPolicy
|
||||
dev.caskeleton.adapter.outbound.mongo.advanced.bridge.MongoChangeMessagingBridge
|
||||
@@ -143,7 +144,7 @@ dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoClientGenerationRegistr
|
||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoDriverObservabilityAutoConfiguration
|
||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatformAutoConfiguration
|
||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatformHealthIndicator
|
||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatformProperties
|
||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatformSettings
|
||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoProfileProperties
|
||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoStableReleaseEvidence
|
||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoStableReleaseGate
|
||||
@@ -166,6 +167,7 @@ dev.caskeleton.adapter.outbound.mongo.changestream.recovery.MongoChangeHistoryLo
|
||||
dev.caskeleton.adapter.outbound.mongo.changestream.recovery.MongoChangeStreamRecoveryDecision
|
||||
dev.caskeleton.adapter.outbound.mongo.changestream.recovery.MongoChangeStreamRecoveryPolicy
|
||||
dev.caskeleton.adapter.outbound.mongo.changestream.recovery.MongoInvalidateRecovery
|
||||
dev.caskeleton.adapter.outbound.mongo.client.MongoClientSettingsFactory
|
||||
dev.caskeleton.adapter.outbound.mongo.failure.DefaultMongoFailureClassifier
|
||||
dev.caskeleton.adapter.outbound.mongo.failure.DefaultMongoFailureTranslator
|
||||
dev.caskeleton.adapter.outbound.mongo.failure.MongoDriverFailureView
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# HTTP Client Platform — Repository Adaptation Contract
|
||||
|
||||
**Design source:** `httpclient-superpowers-package/docs/superpowers/specs/2026-08-08-httpclient-platform-design.md`
|
||||
**Plan source:** `httpclient-superpowers-package/docs/superpowers/plans/2026-08-08-httpclient-platform-implementation-plan.md`
|
||||
**Design source:** `docs/superpowers/specs/2026-08-08-httpclient-platform-design.md`
|
||||
**Plan source:** `docs/superpowers/plans/2026-08-08-httpclient-platform-implementation-plan.md`
|
||||
|
||||
The design package states its own adaptation rule:
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
# JPA Relational Persistence Platform — Repository Adaptation Contract
|
||||
|
||||
**Design source:** `jpa-superpowers-package/docs/superpowers/specs/2026-08-11-jpa-persistence-platform-design.md`
|
||||
(copied to `docs/superpowers/specs/`)
|
||||
**Stable plan source:** `jpa-superpowers-package/docs/superpowers/plans/2026-08-11-jpa-persistence-platform-implementation-plan.md`
|
||||
(copied to `docs/superpowers/plans/`)
|
||||
**Experimental plan source:** `jpa-superpowers-package/docs/superpowers/plans/2026-08-11-jpa-persistence-experimental-expansion-plan.md`
|
||||
(copied to `docs/superpowers/plans/`)
|
||||
**Design source:** `docs/superpowers/specs/2026-08-11-jpa-persistence-platform-design.md`
|
||||
**Stable plan source:** `docs/superpowers/plans/2026-08-11-jpa-persistence-platform-implementation-plan.md`
|
||||
**Experimental plan source:** `docs/superpowers/plans/2026-08-11-jpa-persistence-experimental-expansion-plan.md`
|
||||
|
||||
The design package states its own adaptation rule (§3.2): the assumed package paths and Gradle
|
||||
structure are explicit implementation *assumptions* made because the real Backend Skeleton
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| Topology | A real sharded cluster. A replica set cannot exercise routing. |
|
||||
| Server | MongoDB 7.0 or 8.0. |
|
||||
| Privilege | `MongoPrincipalRole.SHARD_ADMIN` for the admin plane; the application role is unchanged. |
|
||||
| Gate | `mongoShardedTest` lane with `MongoShardingContractSuite`. |
|
||||
| Gate | **Not promoted.** No `mongoShardedTest` lane is registered, and a sharded cluster is not an environment this repository stands up. Listed under `experimental_contracts` in `src/config/mongodb/release-contracts.json`; promoting it needs the lane, its required class, and protected-environment evidence to exist first. |
|
||||
|
||||
## Shard key
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Advanced capability sign-off
|
||||
|
||||
`scripts/verify-mongodb-advanced.sh` treats a file in this directory as the evidence that a review
|
||||
> **2026-08-15:** `scripts/verify-mongodb-advanced.sh` was removed, so nothing reads this directory
|
||||
> automatically any more. The files below are still the record that a review happened, but a missing
|
||||
> one no longer fails anything — a human has to check for it during promotion.
|
||||
|
||||
`scripts/verify-mongodb-advanced.sh` treated a file in this directory as the evidence that a review
|
||||
happened:
|
||||
|
||||
- `security.md` — per-capability privilege review, naming the roles granted and by whom.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# MongoDB Document Persistence Platform — Repository Adaptation Contract
|
||||
|
||||
**Design source:** `mongodb-superpowers-package/docs/superpowers/specs/2026-08-11-mongodb-document-persistence-platform-design.md`
|
||||
**Stable plan:** `mongodb-superpowers-package/docs/superpowers/plans/2026-08-11-mongodb-document-persistence-platform-implementation-plan.md`
|
||||
**Advanced plan:** `mongodb-superpowers-package/docs/superpowers/plans/2026-08-11-mongodb-advanced-capabilities-expansion-plan.md`
|
||||
**Design source:** `docs/superpowers/specs/2026-08-11-mongodb-document-persistence-platform-design.md`
|
||||
**Stable plan:** `docs/superpowers/plans/2026-08-11-mongodb-document-persistence-platform-implementation-plan.md`
|
||||
**Advanced plan:** `docs/superpowers/plans/2026-08-11-mongodb-advanced-capabilities-expansion-plan.md`
|
||||
|
||||
The design package declares its own module root (`modules/mongodb`) and root package
|
||||
(`io.backend.skeleton.mongodb`) as *implementation assumptions*, not as contract. This file is the
|
||||
@@ -96,7 +96,7 @@ otherwise. Being on the classpath is not being enabled.
|
||||
| `mongodb-migration-flamingock` depends on Flamingock | Adding an unvetted external dependency is out of scope for this task, and the design itself requires the public contract not to depend on Flamingock types | The adapter is provider-neutral: it consumes a platform-owned `FlamingockChangeUnitView`. Wiring an actual Flamingock distribution is a one-file change behind that view. |
|
||||
| Testkit as its own Gradle module | The design forbids production modules depending on the testkit | A dedicated `testkit` source set whose output is on the test compile/runtime classpaths only. ArchUnit rule `productionNeverDependsOnTestkit` enforces the direction. |
|
||||
| Per-task `git commit` | `AGENTS.md`: commit policy is `human-only` | Implementation is delivered unstaged; commits are the human's action. This is the only plan step intentionally not executed, and it is recorded here. |
|
||||
| `docs/mongodb/**`, `scripts/verify-mongodb-*.sh` | Repository already owns `docs/` and `scripts/` | Created at the same repository-relative paths. |
|
||||
| `docs/mongodb/**`, `scripts/verify-mongodb-*.sh` | Repository already owns `docs/` and `scripts/` | Created at the same repository-relative paths. The two gate scripts were later removed (2026-08-15); see §5. |
|
||||
|
||||
## 4. What is unchanged from the design
|
||||
|
||||
@@ -124,9 +124,21 @@ otherwise. Being on the classpath is not being enabled.
|
||||
|
||||
## 5. Verification
|
||||
|
||||
The two release-gate scripts (`scripts/verify-mongodb-platform.sh` and
|
||||
`scripts/verify-mongodb-advanced.sh`) were removed on 2026-08-15. They wrapped the Gradle lanes below
|
||||
and added two things Gradle does not do on its own: a lane that executed zero tests was reported as a
|
||||
failure rather than counted as a pass, and a `promotion.json` recording the commit, server image and
|
||||
contract-manifest hash. Neither exists until something replaces it, so a green run of the commands
|
||||
below is weaker evidence than the gate was.
|
||||
|
||||
From `src/`:
|
||||
|
||||
```bash
|
||||
bash scripts/verify-mongodb-platform.sh # Stable gate
|
||||
bash scripts/verify-mongodb-advanced.sh # Advanced gate (opt-in lanes)
|
||||
./gradlew :adapter:outbound:persistence-mongo:check --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies --console=plain
|
||||
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
|
||||
```
|
||||
|
||||
Both scripts run from the repository root and delegate to `src/gradlew`.
|
||||
The container-backed lanes the gate ran behind `MONGODB_DOCKER=1` — `mongoCompatibilityTest`,
|
||||
`mongoMigrationTest`, `mongoSecurityIntegrationTest`, `mongoReplicaSetTest`, `mongoFailoverTest`,
|
||||
`mongoPerformanceTest` — are now invoked by name or not at all.
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Notification payload at rest — threat model and decision (NTF-INT-007)
|
||||
|
||||
Wave 2 Task D6 offers two branches and requires that one be chosen and implemented fully:
|
||||
|
||||
- **(a) encryption** — a codec/port, ciphertext envelope, key id, rotation and history, row migration,
|
||||
and a decryption-failure contract;
|
||||
- **(b) restriction** — this document plus a static restriction proving the variable types cannot
|
||||
carry sensitive values.
|
||||
|
||||
The plan recommends (b) **"if and only if the variable types can genuinely be restricted to
|
||||
non-sensitive values"**. They cannot. This document records why, what that leaves, and when the
|
||||
remaining branch lands.
|
||||
|
||||
## What is stored, and where
|
||||
|
||||
`CanonicalNotificationPlanWriter.request(...)` puts `encoded.variablesPayload()` into
|
||||
`NotificationRequestRecord` verbatim. `JpaNotificationRequestStore` writes that record to
|
||||
`notification_request.variables_payload` with no transformation. There is no encryption anywhere on
|
||||
this path.
|
||||
|
||||
## Why the restriction branch is unavailable
|
||||
|
||||
Template variables are a closed algebra — `NotificationVariable` permits `TextValue`, `NumberValue`,
|
||||
`BooleanValue`, `NullValue`, `ListValue`, `ObjectValue` — which is a real improvement over the
|
||||
`Map<String, Object>` it replaced. But `TextValue` holds an arbitrary UTF-8 string of up to 8 KiB,
|
||||
and that is not an oversight to be tightened: **the variables are the recipient-specific content of
|
||||
the message**. A password-reset code, an order total, a delivery address, a patient's appointment
|
||||
time — those are what a notification is for.
|
||||
|
||||
A restriction to "non-sensitive values" would therefore be one of two things, and both are worse
|
||||
than the problem:
|
||||
|
||||
- **unenforceable** — a comment saying callers should not put sensitive data in a field designed to
|
||||
carry the message's content, which is a policy no type checks and no reviewer can see violated;
|
||||
- **enforced and useless** — a type that refuses free text, which does not restrict the capability so
|
||||
much as delete it.
|
||||
|
||||
The precondition on the plan's recommendation is false. Branch (b) is not available.
|
||||
|
||||
> **Status: branch (a) implemented at the storage boundary.** `NotificationPayloadProtection` is the
|
||||
> application-owned port, `AesGcmNotificationPayloadProtection` the AES-GCM implementation, and
|
||||
> `NotificationRecordMapper` applies it — as a **required** constructor argument, so a composition
|
||||
> cannot assemble the notification stores while leaving the payload in plaintext. What remains before
|
||||
> the facade can be imported is the row migration for any deployment that already has plaintext rows,
|
||||
> and the `local-notification-*` lanes. The analysis below is kept as written, because it is what the
|
||||
> decision rests on.
|
||||
|
||||
## Decision: branch (a), landing with the persistence wiring
|
||||
|
||||
Encryption is therefore the required branch. Its scope is unchanged from the plan: a codec behind an
|
||||
application port, a ciphertext envelope carrying its key id, key rotation with history so an old row
|
||||
stays readable, a migration for existing rows, and an explicit contract for what a decryption failure
|
||||
does to a request.
|
||||
|
||||
**It lands in the change unit that makes the write path reachable, and not before.** The reason is a
|
||||
fact the spec did not have: `NotificationJpaPersistenceFacade`, which assembles
|
||||
`JpaNotificationRequestStore`, is imported by nothing. The composition root's component scan excludes
|
||||
the persistence package by design, and no configuration imports the facade — so the notification
|
||||
capability has **no JPA persistence at all**, and no deployment currently writes this payload
|
||||
anywhere. The defect is real in the code and latent in the runtime.
|
||||
|
||||
Designing key rotation and a row migration for rows that no deployment produces would be building the
|
||||
migration before the table. Worse, it would settle the envelope's shape before the store that has to
|
||||
read it is wired, which is the order that produces an envelope the store cannot use.
|
||||
|
||||
**One correction, learned by trying it.** This section said the envelope "lands with the wiring".
|
||||
Wiring the facade first — to register the SMTP assembler — made
|
||||
`NotificationPayloadAtRestContractTest` fail on the case asserting the write path is reachable from
|
||||
no composition, which is exactly what that case is for. The wave forbids connecting wiring over a
|
||||
known security finding on a runtime path, so the wiring was reverted and the envelope built first.
|
||||
The honest ordering is **envelope before or with the wiring, never after**, and the contract test now
|
||||
enforces it by failing on the wiring alone.
|
||||
|
||||
### The envelope, and why it has a key id
|
||||
|
||||
```
|
||||
byte version always 1
|
||||
byte keyIdLength 1..255 UTF-8 bytes
|
||||
byte[] keyId
|
||||
byte[12] nonce
|
||||
byte[] ciphertext + GCM tag
|
||||
```
|
||||
|
||||
The key id is the reason there is a format at all. This repository's callback protection stores nonce
|
||||
and ciphertext and nothing else, so the day the active key changes, every row written under the
|
||||
previous one becomes unreadable and nothing in the row can say which key it needed — that is not a
|
||||
rotation story with a gap in it, it is the absence of one. `SecretMaterialProvider` already exposes
|
||||
`keyById`, so reading the id back and asking for that specific key makes rotation a change of default
|
||||
rather than a data migration. The version byte costs one byte and is what allows the format to change
|
||||
at all.
|
||||
|
||||
The header is passed as **AAD**, not merely prefixed: without that, the key id is attacker-editable
|
||||
and an envelope could be redirected at a key of the attacker's choosing.
|
||||
|
||||
A failed decryption throws `NotificationPayloadUnreadableException` rather than returning empty. A
|
||||
caller handed an empty payload renders every variable as nothing and sends "Hello , your code is " to
|
||||
a real person — the failure delivered instead of reported. All three causes (unknown key, wrong key,
|
||||
modified ciphertext) collapse into one message, because telling them apart tells an attacker which of
|
||||
the three they achieved.
|
||||
|
||||
## What must not be done instead
|
||||
|
||||
**Requiring `PAYLOAD_ENCRYPTION` in `INGEST_ONLY` is not a fix.** That secret is consumed by exactly
|
||||
one thing — `AesGcmCallbackPayloadProtection`, which protects raw callback bodies — and by nothing on
|
||||
the accept path. Demanding it would make a deployment supply a key that protects nothing while the
|
||||
payload it appears to be about stays in plaintext. The repository already has one defect of that
|
||||
exact shape: `backend.graphql.cursor.key-ids`, which production refuses to start without and which no
|
||||
code signs a cursor with (GQL-INT-003). Adding a second would make the pattern a habit.
|
||||
|
||||
## Consequence
|
||||
|
||||
Notification is **not promoted to Stable**, per the index's scope boundaries, until branch (a) is
|
||||
complete. The three notification Compose lanes stay non-blocking. `NotificationPayloadAtRestContractTest`
|
||||
holds every fact this decision rests on, so the decision expires automatically if any of them stops
|
||||
being true — in particular, the assertion that no encryption sits on the accept path fails the moment
|
||||
somebody adds one, which is the change this document is waiting for.
|
||||
@@ -15,7 +15,7 @@ when this page, the YAML tree and `docs/registries/env-keys.yaml` disagree.
|
||||
| Property | Environment variable | Default | Meaning |
|
||||
|---|---|---|---|
|
||||
| `enabled` | `APP_NOTIFICATION_PLATFORM_ENABLED` | `false` | Binds nothing at all while false: no runtime, no schema check, no scheduler thread, no secret required |
|
||||
| `mode` | `APP_NOTIFICATION_PLATFORM_MODE` | `SERVING` | `SERVING` refuses to start without a working provider; `ACCEPT_ONLY` stores requests and does not dispatch |
|
||||
| `mode` | `APP_NOTIFICATION_PLATFORM_MODE` | `SERVING` | `SERVING` refuses to start without a working provider; `INGEST_ONLY` stores requests and does not dispatch |
|
||||
|
||||
## Dispatch
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Notification Delivery Platform — module mapping
|
||||
|
||||
> Source design: `notification-superpowers-package/docs/superpowers/specs/2026-08-10-notification-platform-design.md`
|
||||
> Source design: `docs/superpowers/specs/2026-08-10-notification-platform-design.md`
|
||||
>
|
||||
> Source plan: `notification-superpowers-package/docs/superpowers/plans/2026-08-10-notification-platform-implementation-plan.md`
|
||||
> Source plan: `docs/superpowers/plans/2026-08-10-notification-platform-implementation-plan.md`
|
||||
|
||||
## Why a mapping exists
|
||||
|
||||
|
||||
+588
-11
@@ -20,12 +20,15 @@ env_keys:
|
||||
# === Profile / Identity (feature-env-driven-runtime-configuration) ===
|
||||
|
||||
- name: SPRING_PROFILES_ACTIVE
|
||||
# source: feature-env-driven-runtime-configuration D6 (2026-06-06)
|
||||
# Profile selector is Spring-native and sole (APP_PROFILE was dropped). Unset
|
||||
# -> local fallback in application.yml for early Boot profile binding.
|
||||
type: csv_list
|
||||
default: local
|
||||
allowed_values: [local, dev, staging, prod, sample]
|
||||
# source: feature-env-driven-runtime-configuration D6 (2026-06-06), amended by
|
||||
# five-adapter-runtime-remediation §7.1. Profile selector is Spring-native and sole
|
||||
# (APP_PROFILE was dropped). Exactly one value, not a CSV list: two environments cannot both
|
||||
# have their safety rules apply, and whichever lost did so silently.
|
||||
type: enum
|
||||
# No default. A profile that is guessed is a deployment nobody chose: a jar started
|
||||
# with none used to become local, which before persistence was gated also meant an
|
||||
# in-memory database that loses every write on restart.
|
||||
allowed_values: [local, dev, prod]
|
||||
classification: public-config
|
||||
required: true
|
||||
reload_policy: restart-only
|
||||
@@ -389,14 +392,19 @@ env_keys:
|
||||
- name: APP_DATASOURCE_CONNECTION_TIMEOUT
|
||||
# source: feature-env-driven-runtime-configuration "datasource/pool env"
|
||||
# + feature-persistence-failure-baseline "Hikari Alert Threshold: pool wait p99 > 100ms"
|
||||
type: duration
|
||||
default: 5s
|
||||
# unit: milliseconds. It feeds spring.datasource.hikari.connection-timeout, which binds onto
|
||||
# HikariConfig#setConnectionTimeout(long) — a duration shorthand such as "5s" does not bind and
|
||||
# fails the boot. This row said `duration` / `5s`, application.yml copied that default, and
|
||||
# every prod and dev deployment refused to start; five-adapter-runtime-remediation Wave 2 found
|
||||
# it in the prod-smoke lane. Corrected to what the property actually accepts.
|
||||
type: integer
|
||||
default: 5000
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: feature-env-driven-runtime-configuration
|
||||
validation: spring_duration_shorthand
|
||||
validation: positive_integer_milliseconds
|
||||
compatibility_impact: behavior-change
|
||||
required_test: env-contract:connection-timeout-set
|
||||
|
||||
@@ -4250,6 +4258,117 @@ env_keys:
|
||||
# default. Every key carries an inline default so a deployment that leaves the platform off
|
||||
# supplies nothing. Reference: docs/notification/configuration.md.
|
||||
|
||||
- name: APP_PERSISTENCE_JPA_ENABLED
|
||||
# source: five-adapter-runtime-remediation §5.1 — master switch for relational persistence.
|
||||
# false means no DataSource, no entity scan, no repositories, no Hibernate, no Flyway and no DB
|
||||
# health contributor; the old app.jpa-platform.enabled gated three add-on beans while reading
|
||||
# like this one and defaulting to on.
|
||||
type: boolean
|
||||
default: false
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-five-adapter-activation
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:persistence-jpa-disabled-safe
|
||||
- name: APP_PERSISTENCE_MONGO_ENABLED
|
||||
# source: five-adapter-runtime-remediation §5.1 — master switch for MongoDB persistence.
|
||||
type: boolean
|
||||
default: false
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-five-adapter-activation
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:persistence-mongo-disabled-safe
|
||||
- name: APP_PERSISTENCE_MONGO_ACTIVE_PROFILE
|
||||
# source: five-adapter-runtime-remediation §5.1 — selects exactly one Mongo profile. The runtime
|
||||
# builds one sync client and one pool; a profile present in the map but not selected has neither
|
||||
# its secret resolved nor a client created.
|
||||
type: string
|
||||
default: ""
|
||||
classification: public-config
|
||||
required: false
|
||||
required_when: APP_PERSISTENCE_MONGO_ENABLED=true
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-five-adapter-activation
|
||||
validation: nonblank-when-required
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:persistence-mongo-active-profile
|
||||
- name: APP_MESSAGING_ENABLED
|
||||
# source: five-adapter-runtime-remediation §5.1 — master switch for broker publication.
|
||||
# APP_MESSAGING_BROKER selects which transport and is no longer the de-facto switch.
|
||||
type: boolean
|
||||
default: false
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-five-adapter-activation
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:messaging-disabled-safe
|
||||
- name: APP_GRAPHQL_ENABLED
|
||||
# source: five-adapter-runtime-remediation §5.1 — master switch for the GraphQL transport.
|
||||
# false publishes no /graphql route, including the one Spring GraphQL would publish by itself.
|
||||
type: boolean
|
||||
default: false
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-five-adapter-activation
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:graphql-disabled-safe
|
||||
- name: APP_GRAPHQL_DEPLOYMENT_MODE
|
||||
# source: five-adapter-runtime-remediation §5.1 / GQL-INT-002 — replaces backend.graphql.production
|
||||
# and backend.graphql.environment, which defaulted to production=false with
|
||||
# environment=PRODUCTION_PUBLIC and let anonymous-principal and allow-by-default authorization
|
||||
# read one axis while the other claimed production.
|
||||
type: enum
|
||||
default: ""
|
||||
allowed_values: [LOCAL, DEV, PRODUCTION_INTERNAL, PRODUCTION_PUBLIC]
|
||||
classification: public-config
|
||||
required: false
|
||||
required_when: APP_GRAPHQL_ENABLED=true
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-five-adapter-activation
|
||||
validation: enum
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:graphql-deployment-mode
|
||||
- name: APP_OUTBOX_ENABLED
|
||||
# source: five-adapter-runtime-remediation §6.1 JPA-INT-004 — the outbox capability switch.
|
||||
# relay-enabled below only starts the scheduler; conflating the two meant a relay-off deployment
|
||||
# still assembled outbox metrics over a store port a database-less runtime does not have.
|
||||
type: boolean
|
||||
default: false
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-five-adapter-activation
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:outbox-capability-disabled-safe
|
||||
- name: APP_OUTBOX_RELAY_ENABLED
|
||||
# source: five-adapter-runtime-remediation §6.3 MSG-INT-001 — starts the relay scheduler.
|
||||
# Requires APP_OUTBOX_ENABLED, APP_PERSISTENCE_JPA_ENABLED and APP_MESSAGING_ENABLED with a
|
||||
# broker; the shipped default was true beside a blank broker, which refused every startup.
|
||||
type: boolean
|
||||
default: false
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-five-adapter-activation
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:outbox-relay-dependency
|
||||
- name: APP_NOTIFICATION_PLATFORM_ENABLED
|
||||
# source: NTF-025 — master switch for the notification delivery platform; false binds nothing at all
|
||||
type: boolean
|
||||
@@ -4263,11 +4382,469 @@ env_keys:
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-platform-disabled-safe
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY
|
||||
# source: NTF-INT-007 — Encrypts recipient contact points at rest — addresses and phone numbers.
|
||||
# Full row owned by secrets-classification.yaml. Bound by
|
||||
# ca-skeleton.notification.platform.secrets.contact-encryption-key; the platform decodes it at startup and
|
||||
# refuses to boot if it is blank, shorter than 32 bytes, or equal to another purpose's key.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: secret
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: base64_at_least_32_bytes_and_distinct_per_purpose
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY
|
||||
# source: NTF-INT-007 — Blind index over contact points, so a lookup never needs the plaintext.
|
||||
# Full row owned by secrets-classification.yaml. Bound by
|
||||
# ca-skeleton.notification.platform.secrets.contact-lookup-hmac-key; the platform decodes it at startup and
|
||||
# refuses to boot if it is blank, shorter than 32 bytes, or equal to another purpose's key.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: secret
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: base64_at_least_32_bytes_and_distinct_per_purpose
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY
|
||||
# source: NTF-INT-007 — Signs the callback URLs a provider posts delivery outcomes back to.
|
||||
# Full row owned by secrets-classification.yaml. Bound by
|
||||
# ca-skeleton.notification.platform.secrets.callback-signing-key; the platform decodes it at startup and
|
||||
# refuses to boot if it is blank, shorter than 32 bytes, or equal to another purpose's key.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: secret
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: base64_at_least_32_bytes_and_distinct_per_purpose
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY
|
||||
# source: NTF-INT-007 — Encrypts stored provider credentials, which are themselves secrets.
|
||||
# Full row owned by secrets-classification.yaml. Bound by
|
||||
# ca-skeleton.notification.platform.secrets.provider-credential-key; the platform decodes it at startup and
|
||||
# refuses to boot if it is blank, shorter than 32 bytes, or equal to another purpose's key.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: secret
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: base64_at_least_32_bytes_and_distinct_per_purpose
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY
|
||||
# source: NTF-INT-007 — Encrypts notification variables and retained callback bodies at rest.
|
||||
# Full row owned by secrets-classification.yaml. Bound by
|
||||
# ca-skeleton.notification.platform.secrets.payload-encryption-key; the platform decodes it at startup and
|
||||
# refuses to boot if it is blank, shorter than 32 bytes, or equal to another purpose's key.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: secret
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: base64_at_least_32_bytes_and_distinct_per_purpose
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY
|
||||
# source: NTF-INT-007 — Signs Web Push requests; the browser push service rejects anything else.
|
||||
# Full row owned by secrets-classification.yaml. Bound by
|
||||
# ca-skeleton.notification.platform.secrets.vapid-signing-key; the platform decodes it at startup and
|
||||
# refuses to boot if it is blank, shorter than 32 bytes, or equal to another purpose's key.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: secret
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: base64_at_least_32_bytes_and_distinct_per_purpose
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY
|
||||
# source: NTF-INT-007 — Keyed hash of provider request ids, which are provider-side identifiers.
|
||||
# Full row owned by secrets-classification.yaml. Bound by
|
||||
# ca-skeleton.notification.platform.secrets.provider-request-lookup-hmac-key; the platform decodes it at startup and
|
||||
# refuses to boot if it is blank, shorter than 32 bytes, or equal to another purpose's key.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: secret
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: base64_at_least_32_bytes_and_distinct_per_purpose
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY
|
||||
# source: NTF-INT-007 — Keyed fingerprint of callback bodies, used to detect replays.
|
||||
# Full row owned by secrets-classification.yaml. Bound by
|
||||
# ca-skeleton.notification.platform.secrets.callback-fingerprint-hmac-key; the platform decodes it at startup and
|
||||
# refuses to boot if it is blank, shorter than 32 bytes, or equal to another purpose's key.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: secret
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: base64_at_least_32_bytes_and_distinct_per_purpose
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY_ID
|
||||
# source: NTF-INT-007 — the id written into every envelope the CONTACT_ENCRYPTION key produces.
|
||||
# An identifier, not key material, so it is public-config; the material itself is the
|
||||
# APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY row above. Bound into
|
||||
# ca-skeleton.notification.platform.secrets.active-key-ids.CONTACT_ENCRYPTION.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank_when_platform_enabled
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY_ID
|
||||
# source: NTF-INT-007 — the id written into every envelope the CONTACT_LOOKUP_HMAC key produces.
|
||||
# An identifier, not key material, so it is public-config; the material itself is the
|
||||
# APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY row above. Bound into
|
||||
# ca-skeleton.notification.platform.secrets.active-key-ids.CONTACT_LOOKUP_HMAC.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank_when_platform_enabled
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY_ID
|
||||
# source: NTF-INT-007 — the id written into every envelope the CALLBACK_SIGNING key produces.
|
||||
# An identifier, not key material, so it is public-config; the material itself is the
|
||||
# APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY row above. Bound into
|
||||
# ca-skeleton.notification.platform.secrets.active-key-ids.CALLBACK_SIGNING.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank_when_platform_enabled
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY_ID
|
||||
# source: NTF-INT-007 — the id written into every envelope the PROVIDER_CREDENTIAL key produces.
|
||||
# An identifier, not key material, so it is public-config; the material itself is the
|
||||
# APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY row above. Bound into
|
||||
# ca-skeleton.notification.platform.secrets.active-key-ids.PROVIDER_CREDENTIAL.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank_when_platform_enabled
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY_ID
|
||||
# source: NTF-INT-007 — the id written into every envelope the PAYLOAD_ENCRYPTION key produces.
|
||||
# An identifier, not key material, so it is public-config; the material itself is the
|
||||
# APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY row above. Bound into
|
||||
# ca-skeleton.notification.platform.secrets.active-key-ids.PAYLOAD_ENCRYPTION.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank_when_platform_enabled
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY_ID
|
||||
# source: NTF-INT-007 — the id written into every envelope the VAPID_SIGNING key produces.
|
||||
# An identifier, not key material, so it is public-config; the material itself is the
|
||||
# APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY row above. Bound into
|
||||
# ca-skeleton.notification.platform.secrets.active-key-ids.VAPID_SIGNING.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank_when_platform_enabled
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY_ID
|
||||
# source: NTF-INT-007 — the id written into every envelope the PROVIDER_REQUEST_LOOKUP_HMAC key produces.
|
||||
# An identifier, not key material, so it is public-config; the material itself is the
|
||||
# APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY row above. Bound into
|
||||
# ca-skeleton.notification.platform.secrets.active-key-ids.PROVIDER_REQUEST_LOOKUP_HMAC.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank_when_platform_enabled
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY_ID
|
||||
# source: NTF-INT-007 — the id written into every envelope the CALLBACK_FINGERPRINT_HMAC key produces.
|
||||
# An identifier, not key material, so it is public-config; the material itself is the
|
||||
# APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY row above. Bound into
|
||||
# ca-skeleton.notification.platform.secrets.active-key-ids.CALLBACK_FINGERPRINT_HMAC.
|
||||
type: string
|
||||
default: null
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank_when_platform_enabled
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-secret-material-required
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_ENABLED
|
||||
# source: NTF-INT-001 — master switch of the shipped SMTP provider profile; false means assembly skips it entirely.
|
||||
type: boolean
|
||||
default: false
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_PRIMARY
|
||||
# source: NTF-INT-001 — whether this profile is the primary route for EMAIL; exactly one primary per channel.
|
||||
type: boolean
|
||||
default: true
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_ENVIRONMENT
|
||||
# source: NTF-INT-001 — the profile's declared environment, carried on every dispatch record.
|
||||
type: string
|
||||
default: local
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_CREDENTIAL_PROFILE
|
||||
# source: NTF-INT-001 — the credential profile the relay's credentials are resolved through.
|
||||
type: string
|
||||
default: default
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: non_blank
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_TIMEOUT
|
||||
# source: NTF-INT-001 — per-attempt provider timeout for this profile.
|
||||
type: duration
|
||||
default: 10s
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: duration_spring_shorthand
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_MAX_CONCURRENCY
|
||||
# source: NTF-INT-001 — how many attempts this profile may have in flight.
|
||||
type: int
|
||||
default: 4
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: positive_int
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_RATE_PER_SECOND
|
||||
# source: NTF-INT-001 — the profile's attempt rate limit.
|
||||
type: int
|
||||
default: 10
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: positive_int
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_TLS_MODE
|
||||
# source: NTF-INT-001 — transport security of the SMTP session; the type has no plaintext member.
|
||||
type: enum
|
||||
default: STARTTLS_REQUIRED
|
||||
allowed_values: [STARTTLS_REQUIRED, IMPLICIT_TLS]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: enum_in_allowed_values
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_SENDER_IDENTITY
|
||||
# source: NTF-INT-001 — the envelope sender every message is sent as.
|
||||
type: string
|
||||
default: no-reply@example.invalid
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: email_address
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_CONNECT_TIMEOUT
|
||||
# source: NTF-INT-001 — how long a connection attempt to the relay may take.
|
||||
type: duration
|
||||
default: 5s
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: duration_spring_shorthand
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_READ_TIMEOUT
|
||||
# source: NTF-INT-001 — how long a relay reply may take.
|
||||
type: duration
|
||||
default: 10s
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: duration_spring_shorthand
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_WRITE_TIMEOUT
|
||||
# source: NTF-INT-001 — how long a write to the relay may take.
|
||||
type: duration
|
||||
default: 10s
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: duration_spring_shorthand
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_SMTP_DISPATCH_CONCURRENCY
|
||||
# source: NTF-INT-001 — size of the bounded executor SMTP sends run on.
|
||||
type: int
|
||||
default: 4
|
||||
allowed_values: null
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: worktree-notification-platform
|
||||
validation: positive_int
|
||||
compatibility_impact: behavior-change
|
||||
required_test: adapter-contract:notification-smtp-provider-assembled
|
||||
|
||||
- name: APP_OPENAPI_DOCS_ENABLED
|
||||
# source: five-adapter-runtime-remediation §9 — whether /v3/api-docs is served; application-prod.yml pins it false. Stated rather than defaulted because
|
||||
# SpringDoc warns on every startup until a deployment decides, and a warning on every start is
|
||||
# one nobody reads.
|
||||
type: boolean
|
||||
default: true
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: main
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: env-contract:openapi-exposure-decided
|
||||
|
||||
- name: APP_OPENAPI_UI_ENABLED
|
||||
# source: five-adapter-runtime-remediation §9 — whether the Swagger UI is served; application-prod.yml pins it false. Stated rather than defaulted because
|
||||
# SpringDoc warns on every startup until a deployment decides, and a warning on every start is
|
||||
# one nobody reads.
|
||||
type: boolean
|
||||
default: true
|
||||
allowed_values: [true, false]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
owner_branch: main
|
||||
validation: boolean
|
||||
compatibility_impact: behavior-change
|
||||
required_test: env-contract:openapi-exposure-decided
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_MODE
|
||||
# source: NTF-025 — SERVING refuses to start without a working provider; ACCEPT_ONLY stores and does not dispatch
|
||||
# source: NTF-025 — SERVING refuses to start without a working provider; INGEST_ONLY stores and does not dispatch.
|
||||
# The constant is INGEST_ONLY. This row said ACCEPT_ONLY, a name NotificationPlatformMode has
|
||||
# never had, so an operator following the registry got a binding failure naming a value the
|
||||
# documentation does not mention. NotificationModeSsotTest derives the list below from the enum.
|
||||
type: enum
|
||||
default: SERVING
|
||||
allowed_values: [SERVING, ACCEPT_ONLY]
|
||||
allowed_values: [SERVING, INGEST_ONLY]
|
||||
classification: public-config
|
||||
required: false
|
||||
reload_policy: restart-only
|
||||
|
||||
@@ -226,6 +226,134 @@ secrets:
|
||||
|
||||
# === Tier 2: sensitive-config (token-bearing URL or id with exposure restriction) ===
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY
|
||||
# Encrypts recipient contact points at rest — addresses and phone numbers.
|
||||
# One of eight purpose-scoped keys. They must all differ: a single key reused across purposes
|
||||
# means a compromise of any one of them is a compromise of all eight, and the platform enforces
|
||||
# the distinction at startup rather than trusting the deployment to have noticed.
|
||||
classification: secret
|
||||
source: secret-manager
|
||||
rotation_policy: dual-read-restart-only
|
||||
prod_default: null
|
||||
required_when: ca-skeleton.notification.platform.enabled=true
|
||||
dev_sentinel_prefix: __LOCAL_DEV_
|
||||
owner_branch: worktree-notification-platform
|
||||
masking_rule: full
|
||||
compatibility_impact: behavior-change
|
||||
required_test: secrets-contract:notification-contact-encryption-no-leak
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY
|
||||
# Blind index over contact points, so a lookup never needs the plaintext.
|
||||
# One of eight purpose-scoped keys. They must all differ: a single key reused across purposes
|
||||
# means a compromise of any one of them is a compromise of all eight, and the platform enforces
|
||||
# the distinction at startup rather than trusting the deployment to have noticed.
|
||||
classification: secret
|
||||
source: secret-manager
|
||||
rotation_policy: dual-read-restart-only
|
||||
prod_default: null
|
||||
required_when: ca-skeleton.notification.platform.enabled=true
|
||||
dev_sentinel_prefix: __LOCAL_DEV_
|
||||
owner_branch: worktree-notification-platform
|
||||
masking_rule: full
|
||||
compatibility_impact: behavior-change
|
||||
required_test: secrets-contract:notification-contact-lookup-hmac-no-leak
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY
|
||||
# Signs the callback URLs a provider posts delivery outcomes back to.
|
||||
# One of eight purpose-scoped keys. They must all differ: a single key reused across purposes
|
||||
# means a compromise of any one of them is a compromise of all eight, and the platform enforces
|
||||
# the distinction at startup rather than trusting the deployment to have noticed.
|
||||
classification: secret
|
||||
source: secret-manager
|
||||
rotation_policy: dual-read-restart-only
|
||||
prod_default: null
|
||||
required_when: ca-skeleton.notification.platform.enabled=true
|
||||
dev_sentinel_prefix: __LOCAL_DEV_
|
||||
owner_branch: worktree-notification-platform
|
||||
masking_rule: full
|
||||
compatibility_impact: behavior-change
|
||||
required_test: secrets-contract:notification-callback-signing-no-leak
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY
|
||||
# Encrypts stored provider credentials, which are themselves secrets.
|
||||
# One of eight purpose-scoped keys. They must all differ: a single key reused across purposes
|
||||
# means a compromise of any one of them is a compromise of all eight, and the platform enforces
|
||||
# the distinction at startup rather than trusting the deployment to have noticed.
|
||||
classification: secret
|
||||
source: secret-manager
|
||||
rotation_policy: dual-read-restart-only
|
||||
prod_default: null
|
||||
required_when: ca-skeleton.notification.platform.enabled=true
|
||||
dev_sentinel_prefix: __LOCAL_DEV_
|
||||
owner_branch: worktree-notification-platform
|
||||
masking_rule: full
|
||||
compatibility_impact: behavior-change
|
||||
required_test: secrets-contract:notification-provider-credential-no-leak
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY
|
||||
# Encrypts notification variables and retained callback bodies at rest.
|
||||
# One of eight purpose-scoped keys. They must all differ: a single key reused across purposes
|
||||
# means a compromise of any one of them is a compromise of all eight, and the platform enforces
|
||||
# the distinction at startup rather than trusting the deployment to have noticed.
|
||||
classification: secret
|
||||
source: secret-manager
|
||||
rotation_policy: dual-read-restart-only
|
||||
prod_default: null
|
||||
required_when: ca-skeleton.notification.platform.enabled=true
|
||||
dev_sentinel_prefix: __LOCAL_DEV_
|
||||
owner_branch: worktree-notification-platform
|
||||
masking_rule: full
|
||||
compatibility_impact: behavior-change
|
||||
required_test: secrets-contract:notification-payload-encryption-no-leak
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY
|
||||
# Signs Web Push requests; the browser push service rejects anything else.
|
||||
# One of eight purpose-scoped keys. They must all differ: a single key reused across purposes
|
||||
# means a compromise of any one of them is a compromise of all eight, and the platform enforces
|
||||
# the distinction at startup rather than trusting the deployment to have noticed.
|
||||
classification: secret
|
||||
source: secret-manager
|
||||
rotation_policy: dual-read-restart-only
|
||||
prod_default: null
|
||||
required_when: ca-skeleton.notification.platform.enabled=true
|
||||
dev_sentinel_prefix: __LOCAL_DEV_
|
||||
owner_branch: worktree-notification-platform
|
||||
masking_rule: full
|
||||
compatibility_impact: behavior-change
|
||||
required_test: secrets-contract:notification-vapid-signing-no-leak
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY
|
||||
# Keyed hash of provider request ids, which are provider-side identifiers.
|
||||
# One of eight purpose-scoped keys. They must all differ: a single key reused across purposes
|
||||
# means a compromise of any one of them is a compromise of all eight, and the platform enforces
|
||||
# the distinction at startup rather than trusting the deployment to have noticed.
|
||||
classification: secret
|
||||
source: secret-manager
|
||||
rotation_policy: dual-read-restart-only
|
||||
prod_default: null
|
||||
required_when: ca-skeleton.notification.platform.enabled=true
|
||||
dev_sentinel_prefix: __LOCAL_DEV_
|
||||
owner_branch: worktree-notification-platform
|
||||
masking_rule: full
|
||||
compatibility_impact: behavior-change
|
||||
required_test: secrets-contract:notification-provider-request-lookup-hmac-no-leak
|
||||
|
||||
- name: APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY
|
||||
# Keyed fingerprint of callback bodies, used to detect replays.
|
||||
# One of eight purpose-scoped keys. They must all differ: a single key reused across purposes
|
||||
# means a compromise of any one of them is a compromise of all eight, and the platform enforces
|
||||
# the distinction at startup rather than trusting the deployment to have noticed.
|
||||
classification: secret
|
||||
source: secret-manager
|
||||
rotation_policy: dual-read-restart-only
|
||||
prod_default: null
|
||||
required_when: ca-skeleton.notification.platform.enabled=true
|
||||
dev_sentinel_prefix: __LOCAL_DEV_
|
||||
owner_branch: worktree-notification-platform
|
||||
masking_rule: full
|
||||
compatibility_impact: behavior-change
|
||||
required_test: secrets-contract:notification-callback-fingerprint-hmac-no-leak
|
||||
|
||||
- name: APP_NOTIFICATION_SLACK_WEBHOOK_URL
|
||||
# source: feature-integration-adapter-templates 2026-05-22
|
||||
# "Slack | disabled optional module | notification failure policy"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
> **보존 안내** — 이 파일은 저장소 루트 `fileserver-superpowers-package/` 에 있던 납품 패키지의 README다.
|
||||
> 2026-08-15 문서 정리에서 설계서·계획서는 `docs/superpowers/specs/` 와 `docs/superpowers/plans/` 로 통합했고,
|
||||
> 실행 불가 상태였던 `validate_fileserver_docs.py` 는 제거했다.
|
||||
> 아래 본문은 납품 시점의 기록이므로 제거된 파일을 가리키는 문장은 더 이상 유효하지 않다.
|
||||
|
||||
# Fileserver Superpowers Package
|
||||
|
||||
## 포함 파일
|
||||
|
||||
- `fileserver-platform-design.md` — Fileserver 플랫폼 설계 확정안
|
||||
- `fileserver-platform-implementation-plan.md` — 33개 TDD 작업으로 분해한 구현 계획
|
||||
- `VALIDATION.md` — 문서 정적 검증 결과
|
||||
- `validate_fileserver_docs.py` — 검증 재실행 스크립트
|
||||
|
||||
## 저장소 배치 위치
|
||||
|
||||
```text
|
||||
docs/superpowers/specs/2026-08-07-fileserver-platform-design.md
|
||||
docs/superpowers/plans/2026-08-07-fileserver-platform-implementation-plan.md
|
||||
```
|
||||
|
||||
## 실행 순서
|
||||
|
||||
1. 실제 Backend Skeleton 구조와 root package를 대조한다.
|
||||
2. 설계서의 모듈 경계를 저장소에 반영한다.
|
||||
3. 구현 계획 Task 1부터 순서대로 실행한다.
|
||||
4. 각 Task에서 실패 테스트를 확인한 뒤 구현한다.
|
||||
5. Milestone A~D마다 전체 검증 Gate를 실행한다.
|
||||
|
||||
실행에는 `superpowers:subagent-driven-development` 방식이 권장된다.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Fileserver Superpowers 문서 검증
|
||||
|
||||
**결과:** PASS
|
||||
|
||||
## 파일
|
||||
|
||||
- `fileserver-platform-design.md` — 1893 lines, 59904 bytes, SHA-256 `ee7b21277b254b9606a9ec6e34118a10fba3abbe818b43cbce9ae832102411e6`
|
||||
- `fileserver-platform-implementation-plan.md` — 3422 lines, 131608 bytes, SHA-256 `9a443852ab3a7e4a2232c1b443d4cb8d3478a4954d70510173d3e0ac1d3d2125`
|
||||
|
||||
## 검증 항목
|
||||
|
||||
- [x] **fileserver-platform-design.md exists** — /mnt/data/fileserver-platform-design.md
|
||||
- [x] **fileserver-platform-implementation-plan.md exists** — /mnt/data/fileserver-platform-implementation-plan.md
|
||||
- [x] **design title** — True
|
||||
- [x] **plan header** — required Superpowers header
|
||||
- [x] **design code fences** — count=94
|
||||
- [x] **plan code fences** — count=416
|
||||
- [x] **design placeholder scan** — hits=[]
|
||||
- [x] **plan placeholder scan** — hits=[]
|
||||
- [x] **design section coverage** — missing=[]
|
||||
- [x] **design topic: MVC** — missing=[]
|
||||
- [x] **design topic: WebFlux** — missing=[]
|
||||
- [x] **design topic: local/PVC/NFS** — missing=[]
|
||||
- [x] **design topic: content/metadata separation** — missing=[]
|
||||
- [x] **design topic: upload** — missing=[]
|
||||
- [x] **design topic: download** — missing=[]
|
||||
- [x] **design topic: publish** — missing=[]
|
||||
- [x] **design topic: security** — missing=[]
|
||||
- [x] **design topic: resumable** — missing=[]
|
||||
- [x] **design topic: observability** — missing=[]
|
||||
- [x] **blocking core port leakage** — hits=[]
|
||||
- [x] **task count** — count=33
|
||||
- [x] **task numbering** — numbers=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
|
||||
- [x] **task block completeness** — {}
|
||||
- [x] **unique create paths** — {}
|
||||
- [x] **plan scope coverage** — missing=[]
|
||||
- [x] **no Redis carryover** — search term=redis
|
||||
- [x] **no deprecated nginx token design** — token mapper removed
|
||||
|
||||
## 검증 범위의 한계
|
||||
|
||||
- 현재 Backend Skeleton 저장소가 입력되지 않아 Gradle compilation, integration test, Nginx execution, PVC·NFS certification은 실행하지 않았다.
|
||||
- 본 검증은 설계·계획 문서의 구조, 내부 일관성, 범위 추적성, 미확정 표식과 중복 경로를 확인한 정적 검증이다.
|
||||
@@ -0,0 +1,48 @@
|
||||
> **보존 안내** — 이 파일은 저장소 루트 `graphql-superpowers-package/` 에 있던 납품 패키지의 README다.
|
||||
> 2026-08-15 문서 정리에서 설계서·계획서는 `docs/superpowers/specs/` 와 `docs/superpowers/plans/` 로 통합했고,
|
||||
> `validate_graphql_docs.py` 와 `MANIFEST.sha256` 은 제거했다.
|
||||
> 아래 본문은 납품 시점의 기록이므로 제거된 파일을 가리키는 문장은 더 이상 유효하지 않다.
|
||||
|
||||
# GraphQL Superpowers 설계 패키지
|
||||
|
||||
이 패키지는 `GraphQL API 실행 플랫폼 심층 리서치`를 구현 기준선으로 변환한 설계서와 실행 계획서다.
|
||||
|
||||
## 문서
|
||||
|
||||
- `docs/superpowers/specs/2026-08-12-graphql-api-execution-platform-design.md`
|
||||
- Stable·Advanced 전체 아키텍처, 공개 계약, 경계, 실패 의미론, 테스트와 지원 등급
|
||||
- 입력 심층 리서치 원문을 추적 부록으로 포함
|
||||
- `docs/superpowers/plans/2026-08-12-graphql-api-execution-platform-implementation-plan.md`
|
||||
- Stable 구현 Task 1–48
|
||||
- `docs/superpowers/plans/2026-08-12-graphql-advanced-capabilities-expansion-plan.md`
|
||||
- Stable Release Gate 이후 실행하는 Advanced·Experimental Task 1–19
|
||||
- `VALIDATION.md`
|
||||
- 정적 검증 결과와 검증 범위
|
||||
- `validate_graphql_docs.py`
|
||||
- 패키지 내부 문서 재검증 스크립트
|
||||
- `MANIFEST.sha256`
|
||||
- 패키지 파일 무결성 목록
|
||||
|
||||
## 구현 순서
|
||||
|
||||
```text
|
||||
Stable Task 1–48
|
||||
→ Stable Release Gate
|
||||
→ Advanced Task 1–19
|
||||
→ Capability별 Promotion Gate
|
||||
```
|
||||
|
||||
## 명시적 전제
|
||||
|
||||
```text
|
||||
Java 21
|
||||
Gradle Kotlin DSL
|
||||
Spring Boot 4.1 BOM
|
||||
Spring for GraphQL 2.0
|
||||
Boot-managed GraphQL Java v25 계열
|
||||
Stable module root: modules/graphql
|
||||
Advanced module root: modules/graphql-advanced
|
||||
Root package: io.backend.skeleton.graphql
|
||||
```
|
||||
|
||||
실제 저장소에 적용할 때 기존 package·version catalog·module naming에 맞춰 경로만 조정하고, 문서의 공개 계약·불변 조건·테스트 의미는 유지한다.
|
||||
@@ -0,0 +1,98 @@
|
||||
# GraphQL Superpowers 문서 정적 검증 결과
|
||||
|
||||
- **검증 시각 기준:** 2026-08-12
|
||||
- **검증 대상:** 설계서 1개, Stable 구현 계획서 1개, Advanced·Experimental 확장 계획서 1개
|
||||
- **검증 명령:** `python3 validate_graphql_docs.py`
|
||||
- **결과:** **PASS**
|
||||
- **실행 검사:** 1,475
|
||||
- **통과:** 1,475
|
||||
- **실패:** 0
|
||||
|
||||
## 문서 규모
|
||||
|
||||
| 문서 | 행 수 | 크기 |
|
||||
|---|---:|---:|
|
||||
| GraphQL API 실행 플랫폼 설계서 | 2,553 | 93,359 bytes |
|
||||
| Stable 구현 계획서 | 4,560 | 209,041 bytes |
|
||||
| Advanced 확장 계획서 | 1,976 | 105,717 bytes |
|
||||
|
||||
## 계획 구조
|
||||
|
||||
| 항목 | Stable | Advanced |
|
||||
|---|---:|---:|
|
||||
| Task 수 | 48 | 19 |
|
||||
| Create 경로 수 | 227 | 113 |
|
||||
| Task 번호 연속성 | PASS | PASS |
|
||||
| 모든 Task의 `Files`·`Interfaces` | PASS | PASS |
|
||||
| 모든 Task의 Implementation Requirements | PASS | PASS |
|
||||
| 모든 Task의 Step 1–5 | PASS | PASS |
|
||||
| 실패·통과 예상 결과 | PASS | PASS |
|
||||
| Task별 Git commit 명령 | PASS | PASS |
|
||||
| Create 경로 중복 | 없음 | 없음 |
|
||||
| Stable·Advanced 경로 충돌 | 없음 | 없음 |
|
||||
|
||||
## 핵심 계약 검증
|
||||
|
||||
```text
|
||||
SDL-first external contract
|
||||
Single Executable Schema Stable default
|
||||
HTTP POST Stable profile
|
||||
application/graphql-response+json preferred
|
||||
Validation 이후 Field Error·Partial Data는 HTTP 200
|
||||
Draft 294는 Stable에서 제외
|
||||
JPA Entity·MongoDB Document 직접 노출 금지
|
||||
GraphQL Multipart Upload 미지원·Fileserver 사용
|
||||
request-wide database transaction 금지
|
||||
DataLoader request scope
|
||||
Finite Fetch Profile
|
||||
HMAC-signed cursor
|
||||
Mutation idempotency·expected version 분리
|
||||
Parser·shape·complexity·runtime response budget
|
||||
Actor·Field·Object·Tenant authorization
|
||||
Low-cardinality observability
|
||||
Stable/Advanced dependency isolation
|
||||
Persisted Operation·WebSocket·SSE·Federation 분리
|
||||
RSocket·HTTP GET·Incremental Delivery Experimental
|
||||
```
|
||||
|
||||
위 계약은 설계서와 계획서의 필수 문자열·모듈 경로·Task별 파일·테스트를 대조해 검증했습니다.
|
||||
|
||||
## 입력 리서치 추적성
|
||||
|
||||
- 첨부된 `GraphQL API 실행 플랫폼 심층 리서치` 원문 전체가 설계서의 `부록 B`에 포함되어 있습니다.
|
||||
- 설계 본문은 원문의 용어와 결론을 유지하면서 구현 판단을 Stable·Advanced·Experimental로 고정합니다.
|
||||
- 설계서와 입력 원문의 exact text 포함 검사를 별도로 통과했습니다.
|
||||
|
||||
## 패키지 검증 항목
|
||||
|
||||
```text
|
||||
문서 파일 존재
|
||||
Markdown code fence 균형
|
||||
Task 1–48 / 1–19 연속성
|
||||
Task별 테스트·명령·commit
|
||||
정확한 Create 경로
|
||||
Placeholder 금지
|
||||
Stable module에 WebSocket·Federation·Persisted Operation 경로 부재
|
||||
Advanced module에 feature flag와 capability 경로 존재
|
||||
금지 API pattern 부재
|
||||
문서 SHA-256 계산
|
||||
```
|
||||
|
||||
## 검증 범위의 한계
|
||||
|
||||
현재 PASS는 **문서의 정적 구조, 요구사항 추적성, 내부 계약과 실행 계획의 완결성**을 의미합니다. 실제 Backend Skeleton 저장소가 입력으로 제공되지 않았으므로 다음은 실행하지 않았습니다.
|
||||
|
||||
```text
|
||||
Gradle configuration·compile
|
||||
Spring Boot ApplicationContext 기동
|
||||
SchemaMappingInspector 실제 결과
|
||||
GraphQlTester HTTP·WebFlux contract
|
||||
JPA·MongoDB statement/query-count integration
|
||||
query bomb·complexity load test
|
||||
Virtual Thread·event-loop blocking test
|
||||
WebSocket·SSE soak test
|
||||
Federation composition·router integration
|
||||
actual Git commit
|
||||
```
|
||||
|
||||
실제 구현에서는 Stable Task 1–48을 먼저 수행해 Stable Release Gate를 통과한 뒤 Advanced Task 1–19를 시작해야 합니다.
|
||||
@@ -0,0 +1,28 @@
|
||||
> **보존 안내** — 이 파일은 저장소 루트 `httpclient-superpowers-package/` 에 있던 납품 패키지의 README다.
|
||||
> 2026-08-15 문서 정리에서 설계서·계획서는 `docs/superpowers/specs/` 와 `docs/superpowers/plans/` 로 통합했고,
|
||||
> 실행 불가 상태였던 `validate_httpclient_docs.py` 는 제거했다.
|
||||
> 아래 본문은 납품 시점의 기록이므로 제거된 파일을 가리키는 문장은 더 이상 유효하지 않다.
|
||||
|
||||
# HTTP Client Superpowers 설계 패키지
|
||||
|
||||
이 패키지는 `Java/Spring 외부 HTTP Client 플랫폼 설계 심층 리서치`를 기반으로 작성한 설계서와 구현 계획서다.
|
||||
|
||||
## 파일
|
||||
|
||||
- `docs/superpowers/specs/2026-08-08-httpclient-platform-design.md`
|
||||
- `docs/superpowers/plans/2026-08-08-httpclient-platform-implementation-plan.md`
|
||||
- `VALIDATION.md`
|
||||
- `validate_httpclient_docs.py`
|
||||
|
||||
## 구현 기준
|
||||
|
||||
- Java 21
|
||||
- Gradle Kotlin DSL
|
||||
- 공통 API는 Spring Framework 6.2 기준
|
||||
- Spring Framework 7.0 호환성 검증
|
||||
- Apache HttpClient 5 + RestClient
|
||||
- JDK HttpClient + RestClient
|
||||
- Reactor Netty + WebClient
|
||||
- Jetty HTTP/3 Experimental
|
||||
|
||||
실제 Backend Skeleton 저장소가 제공되지 않았으므로 package 경로와 Gradle 구조는 설계서의 명시적 구현 가정이다. 구현 전 저장소의 기존 convention과 root package에 맞춰 경로만 조정하고 공개 계약과 정책 의미론은 유지한다.
|
||||
@@ -0,0 +1,31 @@
|
||||
# HTTP Client Superpowers 문서 검증
|
||||
|
||||
**검증 결과:** PASS
|
||||
|
||||
## 검증 항목
|
||||
|
||||
- 설계서 존재 및 최소 구조: PASS
|
||||
- 구현 계획서 존재 및 최소 구조: PASS
|
||||
- Task 번호 연속성: PASS
|
||||
- Task별 Files·Interfaces·Step 1~5·Expected·Commit: PASS
|
||||
- Markdown code fence 균형: PASS
|
||||
- Placeholder scan: PASS
|
||||
- 중복 Create 경로: PASS
|
||||
- 핵심 설계 범위: PASS
|
||||
- 핵심 구현 범위: PASS
|
||||
|
||||
## 통계
|
||||
|
||||
- explicitly forbidden signature documented: ApacheHttpClient nativeApacheClient()
|
||||
- explicitly forbidden signature documented: HttpClient nativeJdkClient()
|
||||
- explicitly forbidden signature documented: WebClient.Builder mutableBuilder()
|
||||
- explicitly forbidden signature documented: RestClient.Builder mutableBuilder()
|
||||
- design lines=1956, bytes=64493
|
||||
- plan lines=3635, bytes=158401
|
||||
- tasks=38, create_paths=306
|
||||
|
||||
## 결론
|
||||
|
||||
- 설계 결정과 구현 작업의 정적 추적성이 확인됐다.
|
||||
- 실제 저장소가 제공되지 않았으므로 Gradle compile, integration, fault, security, performance test는 아직 실행되지 않았다.
|
||||
- 계획의 Java 21, Gradle Kotlin DSL, root package는 명시된 구현 가정이다.
|
||||
@@ -0,0 +1,73 @@
|
||||
> **보존 안내** — 이 파일은 저장소 루트 `jpa-superpowers-package/` 에 있던 납품 패키지의 README다.
|
||||
> 2026-08-15 문서 정리에서 설계서·계획서는 `docs/superpowers/specs/` 와 `docs/superpowers/plans/` 로 통합했고,
|
||||
> `validate_jpa_docs.py` 와 `MANIFEST.sha256` 은 제거했다.
|
||||
> 아래 본문은 납품 시점의 기록이므로 제거된 파일을 가리키는 문장은 더 이상 유효하지 않다.
|
||||
|
||||
# JPA 관계형 영속성 플랫폼 Superpowers 패키지
|
||||
|
||||
이 패키지는 Java/Spring Backend Skeleton의 JPA 관계형 영속성 플랫폼을 구현하기 위한 설계서, Stable 구현 계획서, Experimental 확장 계획서와 정적 검증 도구를 포함한다.
|
||||
|
||||
## 기준
|
||||
|
||||
- 기준일: 2026-08-11
|
||||
- Java 21
|
||||
- Gradle Kotlin DSL
|
||||
- Spring Boot 4.1 dependency management
|
||||
- Spring Data JPA 4.1
|
||||
- Jakarta Persistence 3.2
|
||||
- Hibernate ORM 7.4
|
||||
- PostgreSQL 16·17·18 Stable Matrix
|
||||
- H2는 Local Convenience이며 PostgreSQL 호환성 증거로 사용하지 않는다.
|
||||
|
||||
## 문서
|
||||
|
||||
```text
|
||||
docs/superpowers/specs/2026-08-11-jpa-persistence-platform-design.md
|
||||
docs/superpowers/plans/2026-08-11-jpa-persistence-platform-implementation-plan.md
|
||||
docs/superpowers/plans/2026-08-11-jpa-persistence-experimental-expansion-plan.md
|
||||
```
|
||||
|
||||
## 구현 경계
|
||||
|
||||
Stable 계획은 다음 원칙을 고정한다.
|
||||
|
||||
```text
|
||||
도메인이 Entity·Embeddable·Repository·Query를 소유한다.
|
||||
GenericRepository 또는 Spring Data CRUD 재포장을 만들지 않는다.
|
||||
Application Service가 Transaction 경계를 소유한다.
|
||||
Retry는 새 Persistence Context에서 전체 Use Case를 재실행한다.
|
||||
Commit 결과 불명은 자동 Retry하지 않고 reconciliation으로 처리한다.
|
||||
Flyway가 운영 Schema 변경의 Source of Truth다.
|
||||
PostgreSQL 16·17·18 실제 계약 테스트를 Release Gate로 사용한다.
|
||||
```
|
||||
|
||||
Experimental 계획은 다음 범위를 Stable과 분리한다.
|
||||
|
||||
```text
|
||||
Shared-schema Multi-tenancy
|
||||
PostgreSQL RLS
|
||||
Schema-per-tenant
|
||||
Database-per-tenant
|
||||
Consistency-aware Read Replica
|
||||
Jakarta Persistence 4.0
|
||||
Hibernate ORM 8
|
||||
PostgreSQL 19
|
||||
```
|
||||
|
||||
## 실행 순서
|
||||
|
||||
1. Stable 구현 계획 Task 1~53을 순서대로 수행한다.
|
||||
2. 각 Task에서 실패 테스트 → 실패 확인 → 최소 구현 → 통과 확인 → 커밋 순서를 따른다.
|
||||
3. Stable Release Gate가 통과한 뒤 Experimental Task 1~9를 시작한다.
|
||||
4. Experimental 기능은 별도 feature flag와 승격 ADR 없이 Stable Starter에 포함하지 않는다.
|
||||
|
||||
## 검증
|
||||
|
||||
압축을 해제한 패키지 루트에서 실행한다.
|
||||
|
||||
```bash
|
||||
python validate_jpa_docs.py
|
||||
sha256sum -c MANIFEST.sha256
|
||||
```
|
||||
|
||||
현재 `VALIDATION.md`는 문서 구조·Task 완결성·타입 일관성·경로 충돌·핵심 계약을 대상으로 한 정적 검증 결과다. 실제 Backend Skeleton 저장소의 Gradle compile, PostgreSQL integration, Flyway upgrade, failure injection, 성능 및 보안 테스트 결과는 아니다.
|
||||
@@ -0,0 +1,829 @@
|
||||
# JPA Superpowers 문서 정적 검증
|
||||
|
||||
- 결과: **PASS**
|
||||
- 실행 검사: **808개**
|
||||
- 통과: **808개**
|
||||
- 실패: **0개**
|
||||
- 설계서: **3,276행**
|
||||
- Stable 구현 계획서: **4,716행**
|
||||
- Experimental 확장 계획서: **771행**
|
||||
- Stable Task: **53개**
|
||||
- Experimental Task: **9개**
|
||||
- Stable Create 경로: **235개**
|
||||
- Experimental Create 경로: **37개**
|
||||
- 설계 SHA-256: `a28046eb1451d87e3ac9c3b1922c134b99fa9155d32c09c0f5bddf80f588aaec`
|
||||
- Stable 계획 SHA-256: `bb5d0d876e9a3232a3661cd50b3a4c0da0ea4c68f820526d8b34cb3bdcb43d86`
|
||||
- Experimental 계획 SHA-256: `1ac376309b161cf95b6b3def89284e509ce0191131a268833ca13bd50814ff09`
|
||||
|
||||
## 검사 결과
|
||||
|
||||
| 검사 | 결과 | 상세 |
|
||||
|---|---:|---|
|
||||
| 설계서 존재 | PASS | /mnt/data/jpa-superpowers-package/docs/superpowers/specs/2026-08-11-jpa-persistence-platform-design.md |
|
||||
| Stable 계획서 존재 | PASS | /mnt/data/jpa-superpowers-package/docs/superpowers/plans/2026-08-11-jpa-persistence-platform-implementation-plan.md |
|
||||
| Experimental 계획서 존재 | PASS | /mnt/data/jpa-superpowers-package/docs/superpowers/plans/2026-08-11-jpa-persistence-experimental-expansion-plan.md |
|
||||
| 설계서 최소 상세도 | PASS | 3276 lines |
|
||||
| Stable 계획서 최소 상세도 | PASS | 4716 lines |
|
||||
| Experimental 계획서 최소 상세도 | PASS | 771 lines |
|
||||
| 설계서 코드 fence 균형 | PASS | 262 |
|
||||
| 설계서 placeholder 부재 | PASS | |
|
||||
| Stable 계획서 코드 fence 균형 | PASS | 540 |
|
||||
| Stable 계획서 placeholder 부재 | PASS | |
|
||||
| Experimental 계획서 코드 fence 균형 | PASS | 94 |
|
||||
| Experimental 계획서 placeholder 부재 | PASS | |
|
||||
| 설계 핵심 계약: GenericRepository | PASS | |
|
||||
| 설계 핵심 계약: TransactionCompletionUnknownException | PASS | |
|
||||
| 설계 핵심 계약: EvidenceAwareJpaTransactionManager | PASS | |
|
||||
| 설계 핵심 계약: Application Service | PASS | |
|
||||
| 설계 핵심 계약: OSIV | PASS | |
|
||||
| 설계 핵심 계약: PostgreSQL 16·17·18 | PASS | |
|
||||
| 설계 핵심 계약: Hibernate 7.4 Collection Fetch Pagination | PASS | |
|
||||
| 설계 핵심 계약: FOR UPDATE SKIP LOCKED | PASS | |
|
||||
| 설계 핵심 계약: CREATE INDEX CONCURRENTLY | PASS | |
|
||||
| 설계 핵심 계약: Flyway | PASS | |
|
||||
| 설계 핵심 계약: Runtime·Migration·Admin | PASS | |
|
||||
| 설계 핵심 계약: H2는 Local Convenience | PASS | |
|
||||
| 설계 핵심 계약: 전체 Transaction Retry | PASS | |
|
||||
| 설계 핵심 계약: J1 Standard Persistence | PASS | |
|
||||
| 설계 핵심 계약: J4 Admin / Operations | PASS | |
|
||||
| 설계 핵심 계약: 완료 정의 | PASS | |
|
||||
| GenericRepository 실제 선언 부재 | PASS | |
|
||||
| 운영 ddl auto update 금지 | PASS | |
|
||||
| Completion Unknown 자동 Retry 금지 | PASS | |
|
||||
| OSIV false 강제 | PASS | |
|
||||
| PG16·17·18 Matrix | PASS | |
|
||||
| Hibernate 7.4 fetch pagination gate | PASS | |
|
||||
| Flyway snapshot upgrade gate | PASS | |
|
||||
| Runtime role no DDL gate | PASS | |
|
||||
| Stable 계획에 Experimental create 경로 부재 | PASS | |
|
||||
| Stable Task 1~53 연속성 | PASS | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53] |
|
||||
| Experimental Task 1~9 연속성 | PASS | [1, 2, 3, 4, 5, 6, 7, 8, 9] |
|
||||
| Stable Task chunk 수 | PASS | 53 |
|
||||
| Experimental Task chunk 수 | PASS | 9 |
|
||||
| Stable Task 1 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 1 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 1 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 1 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 1 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 1 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 1 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 1 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 1 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 1 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 1 Gradle focused command | PASS | |
|
||||
| Stable Task 1 exact path | PASS | |
|
||||
| Stable Task 2 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 2 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 2 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 2 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 2 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 2 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 2 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 2 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 2 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 2 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 2 Gradle focused command | PASS | |
|
||||
| Stable Task 2 exact path | PASS | |
|
||||
| Stable Task 3 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 3 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 3 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 3 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 3 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 3 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 3 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 3 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 3 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 3 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 3 Gradle focused command | PASS | |
|
||||
| Stable Task 3 exact path | PASS | |
|
||||
| Stable Task 4 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 4 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 4 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 4 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 4 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 4 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 4 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 4 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 4 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 4 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 4 Gradle focused command | PASS | |
|
||||
| Stable Task 4 exact path | PASS | |
|
||||
| Stable Task 5 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 5 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 5 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 5 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 5 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 5 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 5 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 5 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 5 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 5 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 5 Gradle focused command | PASS | |
|
||||
| Stable Task 5 exact path | PASS | |
|
||||
| Stable Task 6 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 6 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 6 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 6 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 6 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 6 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 6 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 6 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 6 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 6 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 6 Gradle focused command | PASS | |
|
||||
| Stable Task 6 exact path | PASS | |
|
||||
| Stable Task 7 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 7 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 7 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 7 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 7 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 7 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 7 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 7 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 7 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 7 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 7 Gradle focused command | PASS | |
|
||||
| Stable Task 7 exact path | PASS | |
|
||||
| Stable Task 8 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 8 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 8 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 8 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 8 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 8 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 8 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 8 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 8 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 8 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 8 Gradle focused command | PASS | |
|
||||
| Stable Task 8 exact path | PASS | |
|
||||
| Stable Task 9 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 9 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 9 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 9 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 9 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 9 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 9 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 9 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 9 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 9 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 9 Gradle focused command | PASS | |
|
||||
| Stable Task 9 exact path | PASS | |
|
||||
| Stable Task 10 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 10 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 10 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 10 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 10 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 10 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 10 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 10 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 10 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 10 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 10 Gradle focused command | PASS | |
|
||||
| Stable Task 10 exact path | PASS | |
|
||||
| Stable Task 11 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 11 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 11 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 11 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 11 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 11 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 11 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 11 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 11 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 11 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 11 Gradle focused command | PASS | |
|
||||
| Stable Task 11 exact path | PASS | |
|
||||
| Stable Task 12 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 12 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 12 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 12 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 12 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 12 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 12 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 12 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 12 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 12 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 12 Gradle focused command | PASS | |
|
||||
| Stable Task 12 exact path | PASS | |
|
||||
| Stable Task 13 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 13 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 13 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 13 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 13 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 13 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 13 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 13 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 13 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 13 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 13 Gradle focused command | PASS | |
|
||||
| Stable Task 13 exact path | PASS | |
|
||||
| Stable Task 14 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 14 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 14 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 14 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 14 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 14 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 14 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 14 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 14 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 14 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 14 Gradle focused command | PASS | |
|
||||
| Stable Task 14 exact path | PASS | |
|
||||
| Stable Task 15 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 15 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 15 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 15 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 15 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 15 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 15 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 15 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 15 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 15 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 15 Gradle focused command | PASS | |
|
||||
| Stable Task 15 exact path | PASS | |
|
||||
| Stable Task 16 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 16 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 16 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 16 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 16 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 16 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 16 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 16 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 16 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 16 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 16 Gradle focused command | PASS | |
|
||||
| Stable Task 16 exact path | PASS | |
|
||||
| Stable Task 17 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 17 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 17 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 17 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 17 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 17 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 17 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 17 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 17 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 17 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 17 Gradle focused command | PASS | |
|
||||
| Stable Task 17 exact path | PASS | |
|
||||
| Stable Task 18 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 18 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 18 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 18 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 18 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 18 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 18 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 18 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 18 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 18 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 18 Gradle focused command | PASS | |
|
||||
| Stable Task 18 exact path | PASS | |
|
||||
| Stable Task 19 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 19 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 19 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 19 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 19 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 19 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 19 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 19 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 19 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 19 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 19 Gradle focused command | PASS | |
|
||||
| Stable Task 19 exact path | PASS | |
|
||||
| Stable Task 20 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 20 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 20 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 20 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 20 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 20 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 20 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 20 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 20 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 20 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 20 Gradle focused command | PASS | |
|
||||
| Stable Task 20 exact path | PASS | |
|
||||
| Stable Task 21 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 21 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 21 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 21 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 21 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 21 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 21 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 21 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 21 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 21 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 21 Gradle focused command | PASS | |
|
||||
| Stable Task 21 exact path | PASS | |
|
||||
| Stable Task 22 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 22 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 22 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 22 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 22 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 22 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 22 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 22 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 22 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 22 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 22 Gradle focused command | PASS | |
|
||||
| Stable Task 22 exact path | PASS | |
|
||||
| Stable Task 23 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 23 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 23 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 23 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 23 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 23 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 23 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 23 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 23 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 23 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 23 Gradle focused command | PASS | |
|
||||
| Stable Task 23 exact path | PASS | |
|
||||
| Stable Task 24 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 24 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 24 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 24 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 24 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 24 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 24 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 24 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 24 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 24 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 24 Gradle focused command | PASS | |
|
||||
| Stable Task 24 exact path | PASS | |
|
||||
| Stable Task 25 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 25 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 25 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 25 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 25 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 25 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 25 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 25 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 25 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 25 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 25 Gradle focused command | PASS | |
|
||||
| Stable Task 25 exact path | PASS | |
|
||||
| Stable Task 26 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 26 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 26 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 26 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 26 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 26 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 26 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 26 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 26 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 26 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 26 Gradle focused command | PASS | |
|
||||
| Stable Task 26 exact path | PASS | |
|
||||
| Stable Task 27 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 27 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 27 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 27 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 27 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 27 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 27 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 27 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 27 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 27 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 27 Gradle focused command | PASS | |
|
||||
| Stable Task 27 exact path | PASS | |
|
||||
| Stable Task 28 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 28 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 28 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 28 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 28 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 28 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 28 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 28 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 28 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 28 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 28 Gradle focused command | PASS | |
|
||||
| Stable Task 28 exact path | PASS | |
|
||||
| Stable Task 29 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 29 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 29 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 29 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 29 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 29 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 29 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 29 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 29 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 29 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 29 Gradle focused command | PASS | |
|
||||
| Stable Task 29 exact path | PASS | |
|
||||
| Stable Task 30 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 30 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 30 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 30 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 30 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 30 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 30 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 30 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 30 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 30 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 30 Gradle focused command | PASS | |
|
||||
| Stable Task 30 exact path | PASS | |
|
||||
| Stable Task 31 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 31 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 31 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 31 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 31 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 31 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 31 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 31 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 31 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 31 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 31 Gradle focused command | PASS | |
|
||||
| Stable Task 31 exact path | PASS | |
|
||||
| Stable Task 32 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 32 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 32 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 32 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 32 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 32 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 32 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 32 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 32 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 32 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 32 Gradle focused command | PASS | |
|
||||
| Stable Task 32 exact path | PASS | |
|
||||
| Stable Task 33 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 33 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 33 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 33 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 33 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 33 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 33 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 33 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 33 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 33 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 33 Gradle focused command | PASS | |
|
||||
| Stable Task 33 exact path | PASS | |
|
||||
| Stable Task 34 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 34 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 34 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 34 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 34 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 34 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 34 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 34 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 34 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 34 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 34 Gradle focused command | PASS | |
|
||||
| Stable Task 34 exact path | PASS | |
|
||||
| Stable Task 35 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 35 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 35 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 35 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 35 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 35 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 35 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 35 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 35 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 35 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 35 Gradle focused command | PASS | |
|
||||
| Stable Task 35 exact path | PASS | |
|
||||
| Stable Task 36 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 36 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 36 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 36 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 36 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 36 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 36 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 36 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 36 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 36 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 36 Gradle focused command | PASS | |
|
||||
| Stable Task 36 exact path | PASS | |
|
||||
| Stable Task 37 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 37 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 37 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 37 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 37 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 37 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 37 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 37 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 37 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 37 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 37 Gradle focused command | PASS | |
|
||||
| Stable Task 37 exact path | PASS | |
|
||||
| Stable Task 38 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 38 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 38 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 38 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 38 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 38 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 38 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 38 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 38 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 38 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 38 Gradle focused command | PASS | |
|
||||
| Stable Task 38 exact path | PASS | |
|
||||
| Stable Task 39 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 39 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 39 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 39 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 39 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 39 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 39 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 39 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 39 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 39 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 39 Gradle focused command | PASS | |
|
||||
| Stable Task 39 exact path | PASS | |
|
||||
| Stable Task 40 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 40 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 40 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 40 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 40 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 40 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 40 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 40 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 40 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 40 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 40 Gradle focused command | PASS | |
|
||||
| Stable Task 40 exact path | PASS | |
|
||||
| Stable Task 41 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 41 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 41 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 41 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 41 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 41 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 41 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 41 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 41 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 41 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 41 Gradle focused command | PASS | |
|
||||
| Stable Task 41 exact path | PASS | |
|
||||
| Stable Task 42 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 42 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 42 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 42 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 42 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 42 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 42 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 42 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 42 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 42 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 42 Gradle focused command | PASS | |
|
||||
| Stable Task 42 exact path | PASS | |
|
||||
| Stable Task 43 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 43 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 43 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 43 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 43 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 43 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 43 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 43 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 43 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 43 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 43 Gradle focused command | PASS | |
|
||||
| Stable Task 43 exact path | PASS | |
|
||||
| Stable Task 44 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 44 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 44 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 44 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 44 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 44 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 44 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 44 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 44 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 44 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 44 Gradle focused command | PASS | |
|
||||
| Stable Task 44 exact path | PASS | |
|
||||
| Stable Task 45 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 45 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 45 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 45 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 45 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 45 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 45 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 45 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 45 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 45 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 45 Gradle focused command | PASS | |
|
||||
| Stable Task 45 exact path | PASS | |
|
||||
| Stable Task 46 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 46 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 46 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 46 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 46 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 46 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 46 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 46 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 46 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 46 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 46 Gradle focused command | PASS | |
|
||||
| Stable Task 46 exact path | PASS | |
|
||||
| Stable Task 47 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 47 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 47 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 47 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 47 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 47 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 47 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 47 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 47 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 47 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 47 Gradle focused command | PASS | |
|
||||
| Stable Task 47 exact path | PASS | |
|
||||
| Stable Task 48 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 48 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 48 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 48 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 48 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 48 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 48 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 48 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 48 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 48 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 48 Gradle focused command | PASS | |
|
||||
| Stable Task 48 exact path | PASS | |
|
||||
| Stable Task 49 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 49 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 49 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 49 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 49 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 49 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 49 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 49 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 49 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 49 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 49 Gradle focused command | PASS | |
|
||||
| Stable Task 49 exact path | PASS | |
|
||||
| Stable Task 50 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 50 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 50 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 50 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 50 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 50 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 50 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 50 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 50 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 50 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 50 Gradle focused command | PASS | |
|
||||
| Stable Task 50 exact path | PASS | |
|
||||
| Stable Task 51 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 51 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 51 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 51 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 51 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 51 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 51 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 51 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 51 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 51 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 51 Gradle focused command | PASS | |
|
||||
| Stable Task 51 exact path | PASS | |
|
||||
| Stable Task 52 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 52 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 52 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 52 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 52 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 52 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 52 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 52 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 52 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 52 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 52 Gradle focused command | PASS | |
|
||||
| Stable Task 52 exact path | PASS | |
|
||||
| Stable Task 53 필수 항목: **Files:** | PASS | |
|
||||
| Stable Task 53 필수 항목: **Interfaces:** | PASS | |
|
||||
| Stable Task 53 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Stable Task 53 필수 항목: **Step 1: | PASS | |
|
||||
| Stable Task 53 필수 항목: **Step 2: | PASS | |
|
||||
| Stable Task 53 필수 항목: **Step 3: | PASS | |
|
||||
| Stable Task 53 필수 항목: **Step 4: | PASS | |
|
||||
| Stable Task 53 필수 항목: **Step 5: | PASS | |
|
||||
| Stable Task 53 필수 항목: Expected: | PASS | |
|
||||
| Stable Task 53 필수 항목: git commit -m | PASS | |
|
||||
| Stable Task 53 Gradle focused command | PASS | |
|
||||
| Stable Task 53 exact path | PASS | |
|
||||
| Experimental Task 1 필수 항목: **Files:** | PASS | |
|
||||
| Experimental Task 1 필수 항목: **Interfaces:** | PASS | |
|
||||
| Experimental Task 1 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Experimental Task 1 필수 항목: **Step 1: | PASS | |
|
||||
| Experimental Task 1 필수 항목: **Step 2: | PASS | |
|
||||
| Experimental Task 1 필수 항목: **Step 3: | PASS | |
|
||||
| Experimental Task 1 필수 항목: **Step 4: | PASS | |
|
||||
| Experimental Task 1 필수 항목: **Step 5: | PASS | |
|
||||
| Experimental Task 1 필수 항목: Expected: | PASS | |
|
||||
| Experimental Task 1 필수 항목: git commit -m | PASS | |
|
||||
| Experimental Task 1 Gradle focused command | PASS | |
|
||||
| Experimental Task 1 exact path | PASS | |
|
||||
| Experimental Task 2 필수 항목: **Files:** | PASS | |
|
||||
| Experimental Task 2 필수 항목: **Interfaces:** | PASS | |
|
||||
| Experimental Task 2 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Experimental Task 2 필수 항목: **Step 1: | PASS | |
|
||||
| Experimental Task 2 필수 항목: **Step 2: | PASS | |
|
||||
| Experimental Task 2 필수 항목: **Step 3: | PASS | |
|
||||
| Experimental Task 2 필수 항목: **Step 4: | PASS | |
|
||||
| Experimental Task 2 필수 항목: **Step 5: | PASS | |
|
||||
| Experimental Task 2 필수 항목: Expected: | PASS | |
|
||||
| Experimental Task 2 필수 항목: git commit -m | PASS | |
|
||||
| Experimental Task 2 Gradle focused command | PASS | |
|
||||
| Experimental Task 2 exact path | PASS | |
|
||||
| Experimental Task 3 필수 항목: **Files:** | PASS | |
|
||||
| Experimental Task 3 필수 항목: **Interfaces:** | PASS | |
|
||||
| Experimental Task 3 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Experimental Task 3 필수 항목: **Step 1: | PASS | |
|
||||
| Experimental Task 3 필수 항목: **Step 2: | PASS | |
|
||||
| Experimental Task 3 필수 항목: **Step 3: | PASS | |
|
||||
| Experimental Task 3 필수 항목: **Step 4: | PASS | |
|
||||
| Experimental Task 3 필수 항목: **Step 5: | PASS | |
|
||||
| Experimental Task 3 필수 항목: Expected: | PASS | |
|
||||
| Experimental Task 3 필수 항목: git commit -m | PASS | |
|
||||
| Experimental Task 3 Gradle focused command | PASS | |
|
||||
| Experimental Task 3 exact path | PASS | |
|
||||
| Experimental Task 4 필수 항목: **Files:** | PASS | |
|
||||
| Experimental Task 4 필수 항목: **Interfaces:** | PASS | |
|
||||
| Experimental Task 4 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Experimental Task 4 필수 항목: **Step 1: | PASS | |
|
||||
| Experimental Task 4 필수 항목: **Step 2: | PASS | |
|
||||
| Experimental Task 4 필수 항목: **Step 3: | PASS | |
|
||||
| Experimental Task 4 필수 항목: **Step 4: | PASS | |
|
||||
| Experimental Task 4 필수 항목: **Step 5: | PASS | |
|
||||
| Experimental Task 4 필수 항목: Expected: | PASS | |
|
||||
| Experimental Task 4 필수 항목: git commit -m | PASS | |
|
||||
| Experimental Task 4 Gradle focused command | PASS | |
|
||||
| Experimental Task 4 exact path | PASS | |
|
||||
| Experimental Task 5 필수 항목: **Files:** | PASS | |
|
||||
| Experimental Task 5 필수 항목: **Interfaces:** | PASS | |
|
||||
| Experimental Task 5 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Experimental Task 5 필수 항목: **Step 1: | PASS | |
|
||||
| Experimental Task 5 필수 항목: **Step 2: | PASS | |
|
||||
| Experimental Task 5 필수 항목: **Step 3: | PASS | |
|
||||
| Experimental Task 5 필수 항목: **Step 4: | PASS | |
|
||||
| Experimental Task 5 필수 항목: **Step 5: | PASS | |
|
||||
| Experimental Task 5 필수 항목: Expected: | PASS | |
|
||||
| Experimental Task 5 필수 항목: git commit -m | PASS | |
|
||||
| Experimental Task 5 Gradle focused command | PASS | |
|
||||
| Experimental Task 5 exact path | PASS | |
|
||||
| Experimental Task 6 필수 항목: **Files:** | PASS | |
|
||||
| Experimental Task 6 필수 항목: **Interfaces:** | PASS | |
|
||||
| Experimental Task 6 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Experimental Task 6 필수 항목: **Step 1: | PASS | |
|
||||
| Experimental Task 6 필수 항목: **Step 2: | PASS | |
|
||||
| Experimental Task 6 필수 항목: **Step 3: | PASS | |
|
||||
| Experimental Task 6 필수 항목: **Step 4: | PASS | |
|
||||
| Experimental Task 6 필수 항목: **Step 5: | PASS | |
|
||||
| Experimental Task 6 필수 항목: Expected: | PASS | |
|
||||
| Experimental Task 6 필수 항목: git commit -m | PASS | |
|
||||
| Experimental Task 6 Gradle focused command | PASS | |
|
||||
| Experimental Task 6 exact path | PASS | |
|
||||
| Experimental Task 7 필수 항목: **Files:** | PASS | |
|
||||
| Experimental Task 7 필수 항목: **Interfaces:** | PASS | |
|
||||
| Experimental Task 7 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Experimental Task 7 필수 항목: **Step 1: | PASS | |
|
||||
| Experimental Task 7 필수 항목: **Step 2: | PASS | |
|
||||
| Experimental Task 7 필수 항목: **Step 3: | PASS | |
|
||||
| Experimental Task 7 필수 항목: **Step 4: | PASS | |
|
||||
| Experimental Task 7 필수 항목: **Step 5: | PASS | |
|
||||
| Experimental Task 7 필수 항목: Expected: | PASS | |
|
||||
| Experimental Task 7 필수 항목: git commit -m | PASS | |
|
||||
| Experimental Task 7 Gradle focused command | PASS | |
|
||||
| Experimental Task 7 exact path | PASS | |
|
||||
| Experimental Task 8 필수 항목: **Files:** | PASS | |
|
||||
| Experimental Task 8 필수 항목: **Interfaces:** | PASS | |
|
||||
| Experimental Task 8 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Experimental Task 8 필수 항목: **Step 1: | PASS | |
|
||||
| Experimental Task 8 필수 항목: **Step 2: | PASS | |
|
||||
| Experimental Task 8 필수 항목: **Step 3: | PASS | |
|
||||
| Experimental Task 8 필수 항목: **Step 4: | PASS | |
|
||||
| Experimental Task 8 필수 항목: **Step 5: | PASS | |
|
||||
| Experimental Task 8 필수 항목: Expected: | PASS | |
|
||||
| Experimental Task 8 필수 항목: git commit -m | PASS | |
|
||||
| Experimental Task 8 Gradle focused command | PASS | |
|
||||
| Experimental Task 8 exact path | PASS | |
|
||||
| Experimental Task 9 필수 항목: **Files:** | PASS | |
|
||||
| Experimental Task 9 필수 항목: **Interfaces:** | PASS | |
|
||||
| Experimental Task 9 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Experimental Task 9 필수 항목: **Step 1: | PASS | |
|
||||
| Experimental Task 9 필수 항목: **Step 2: | PASS | |
|
||||
| Experimental Task 9 필수 항목: **Step 3: | PASS | |
|
||||
| Experimental Task 9 필수 항목: **Step 4: | PASS | |
|
||||
| Experimental Task 9 필수 항목: **Step 5: | PASS | |
|
||||
| Experimental Task 9 필수 항목: Expected: | PASS | |
|
||||
| Experimental Task 9 필수 항목: git commit -m | PASS | |
|
||||
| Experimental Task 9 Gradle focused command | PASS | |
|
||||
| Experimental Task 9 exact path | PASS | |
|
||||
| Stable Create 경로 중복 부재 | PASS | 235 |
|
||||
| Experimental Create 경로 중복 부재 | PASS | 37 |
|
||||
| Stable·Experimental Create 경로 충돌 부재 | PASS | set() |
|
||||
| Experimental 계획은 Stable Task 1~53 이후 시작 | PASS | |
|
||||
| 공통 타입 일관성: PersistenceOperationName | PASS | 21 |
|
||||
| 공통 타입 일관성: TransactionProfile | PASS | 14 |
|
||||
| 공통 타입 일관성: RetryProfile | PASS | 5 |
|
||||
| 공통 타입 일관성: JpaTransactionExecutor | PASS | 14 |
|
||||
| 공통 타입 일관성: JpaRetryPolicy | PASS | 7 |
|
||||
| 공통 타입 일관성: RetryDecision | PASS | 8 |
|
||||
| 공통 타입 일관성: JpaFailureContext | PASS | 11 |
|
||||
| 공통 타입 일관성: QueryName | PASS | 22 |
|
||||
| 공통 타입 일관성: KeysetPageRequest | PASS | 4 |
|
||||
| 공통 타입 일관성: KeysetSlice | PASS | 7 |
|
||||
| 공통 타입 일관성: TransactionCompletionUnknownException | PASS | 7 |
|
||||
| 공통 타입 일관성: PostgreSqlWorkClaimExecutor | PASS | 2 |
|
||||
| 공통 타입 일관성: FlywayValidationGate | PASS | 8 |
|
||||
| 공통 타입 일관성: JpaPlatformEndpoint | PASS | 3 |
|
||||
| Experimental Gradle 경로 정확성 | PASS | |
|
||||
| Replica annotation-only routing 금지 | PASS | |
|
||||
| RLS connection reuse 검증 | PASS | |
|
||||
| Stable 승격 ADR gate | PASS | |
|
||||
| 심층 리서치 원문 부록 포함 | PASS | |
|
||||
@@ -0,0 +1,60 @@
|
||||
> **보존 안내** — 이 파일은 저장소 루트 `messaging-superpowers-package/` 에 있던 납품 패키지의 README다.
|
||||
> 2026-08-15 문서 정리에서 설계서·계획서는 `docs/superpowers/specs/` 와 `docs/superpowers/plans/` 로 통합했고,
|
||||
> `validate_messaging_docs.py` 와 `MANIFEST.sha256` 은 제거했다.
|
||||
> 아래 본문은 납품 시점의 기록이므로 제거된 파일을 가리키는 문장은 더 이상 유효하지 않다.
|
||||
|
||||
# Messaging Platform Superpowers Package
|
||||
|
||||
이 패키지는 `Java/Spring Messaging 플랫폼 심층 리서치`를 구현 기준으로 변환한 설계서와 구현 계획서다.
|
||||
|
||||
## 포함 파일
|
||||
|
||||
```text
|
||||
docs/superpowers/specs/2026-08-10-messaging-platform-design.md
|
||||
docs/superpowers/plans/2026-08-10-messaging-platform-implementation-plan.md
|
||||
VALIDATION.md
|
||||
validate_messaging_docs.py
|
||||
MANIFEST.sha256
|
||||
```
|
||||
|
||||
## 설계 경계
|
||||
|
||||
- 자체 `messaging-core`가 공개 계약을 소유한다.
|
||||
- Kafka 4.2+/4.3.x와 RabbitMQ 4.3.x는 Stable Adapter다.
|
||||
- Kafka Share Group, Pulsar 4.0 LTS·4.2, NATS JetStream 2.14.x는 Experimental Adapter다.
|
||||
- 공통 `EXACTLY_ONCE` 또는 전역 순서 옵션을 제공하지 않는다.
|
||||
- publish 결과는 `CONFIRMED`, `REJECTED`, `AMBIGUOUS`와 broker evidence로 표현한다.
|
||||
- M1 Typed, M2 Advanced, M3 Native Capability, M4 Admin Plane을 분리한다.
|
||||
- retry·DLQ·redrive에서 logical `messageId`를 유지한다.
|
||||
- DLQ publish 확인 전에 source message를 settlement하지 않는다.
|
||||
- Transactional Outbox, Inbox, Idempotent Consumer, Claim Check를 Reliability 모듈로 분리한다.
|
||||
|
||||
## 구현 가정
|
||||
|
||||
실제 Backend Skeleton 저장소가 입력에 포함되지 않았기 때문에 계획은 다음 기준을 사용한다.
|
||||
|
||||
- Java 21
|
||||
- Gradle Kotlin DSL
|
||||
- root package: `io.backend.skeleton.messaging`
|
||||
- module root: `modules/messaging`
|
||||
- PostgreSQL 16, Flyway, Spring JDBC/JPA
|
||||
- JUnit 5, AssertJ, ArchUnit, Testcontainers, Toxiproxy
|
||||
- Spring Framework 6.2 공통 호환선과 Spring 7.0 호환성 job
|
||||
|
||||
실제 저장소 구조가 다르면 파일 경로와 dependency catalog만 매핑한다. 공개 계약, 모듈 의존 방향, 신뢰성 상태 모델과 검증 기준은 그대로 유지한다.
|
||||
|
||||
## 문서 검증
|
||||
|
||||
패키지 루트에서 다음 명령을 실행한다.
|
||||
|
||||
```bash
|
||||
./validate_messaging_docs.py \
|
||||
docs/superpowers/specs/2026-08-10-messaging-platform-design.md \
|
||||
docs/superpowers/plans/2026-08-10-messaging-platform-implementation-plan.md
|
||||
```
|
||||
|
||||
`VALIDATION.md`는 문서 구조, Task 1~44 연속성, TDD 단계, 필수 설계 계약, placeholder, 중복 파일 경로를 확인한 정적 검증 결과다.
|
||||
|
||||
## 검증 한계
|
||||
|
||||
현재 검증은 설계·계획 문서에 대한 정적 검증이다. 실제 저장소가 제공되지 않아 Gradle compile, broker 통합 테스트, chaos test, 성능 인증은 실행하지 않았다. 구현 계획의 각 Task가 해당 실행 검증을 추가하도록 구성되어 있다.
|
||||
@@ -0,0 +1,432 @@
|
||||
# Messaging Superpowers 문서 정적 검증
|
||||
|
||||
- 설계서: `messaging-platform-design.md` — 2,608행, 74,649 bytes
|
||||
- 계획서: `messaging-platform-implementation-plan.md` — 5,056행, 200,320 bytes
|
||||
- Task 수: 44
|
||||
- Create 경로 수: 356
|
||||
- 검증 항목 수: 422
|
||||
|
||||
## 결과: PASS
|
||||
|
||||
- PASS: 설계서가 2,000행 이상이다
|
||||
- PASS: 구현 계획서가 4,000행 이상이다
|
||||
- PASS: 설계서 Markdown 코드 블록이 균형을 이룬다
|
||||
- PASS: 계획서 Markdown 코드 블록이 균형을 이룬다
|
||||
- PASS: 설계서에 unresolved todo marker가 없다
|
||||
- PASS: 계획서에 unresolved todo marker가 없다
|
||||
- PASS: 설계서에 unresolved tbd marker가 없다
|
||||
- PASS: 계획서에 unresolved tbd marker가 없다
|
||||
- PASS: 설계서에 unresolved fix marker가 없다
|
||||
- PASS: 계획서에 unresolved fix marker가 없다
|
||||
- PASS: 설계서에 placeholder ADR number가 없다
|
||||
- PASS: 계획서에 placeholder ADR number가 없다
|
||||
- PASS: 설계서에 wildcard build path가 없다
|
||||
- PASS: 계획서에 wildcard build path가 없다
|
||||
- PASS: 설계서에 deferred implementation phrase가 없다
|
||||
- PASS: 계획서에 deferred implementation phrase가 없다
|
||||
- PASS: 설계서가 필수 항목 'M1 Typed Messaging API'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'M2 Advanced API'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'M3 Native Capability'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'M4 Admin Plane'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'PublishCompletion'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'AMBIGUOUS'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'MessageEnvelope'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'DeliveryGuarantee'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'OrderingScope'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'Retry Policy Engine'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'DLQ·Parking·Redrive'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'Kafka Stable Adapter'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'RabbitMQ Stable Adapter'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'Transactional Outbox'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'Inbox와 Idempotent Consumer'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'Claim Check'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'Pulsar Experimental Adapter'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'NATS JetStream Experimental Adapter'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'Spring Cloud Stream Bridge'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'Security'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 'Observability'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 '호환성 인증 매트릭스'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 '비지원 범위'을 포함한다
|
||||
- PASS: 설계서가 필수 항목 '완료 정의'을 포함한다
|
||||
- PASS: 공통 DeliveryGuarantee가 두 가지 보장만 선언한다
|
||||
- PASS: 공통 DeliveryGuarantee enum에 EXACTLY_ONCE를 선언하지 않는다
|
||||
- PASS: 공통 OrderingScope enum에 GLOBAL을 선언하지 않는다
|
||||
- PASS: DLQ confirm 후 source settlement 순서를 명시한다
|
||||
- PASS: retry와 reliability에서 동일 message ID를 유지한다
|
||||
- PASS: Task 번호가 1부터 44까지 연속이다
|
||||
- PASS: Task 1가 '**Files:**'을 포함한다
|
||||
- PASS: Task 1가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 1가 'Step 1'을 포함한다
|
||||
- PASS: Task 1가 'Step 2'을 포함한다
|
||||
- PASS: Task 1가 'Step 3'을 포함한다
|
||||
- PASS: Task 1가 'Step 4'을 포함한다
|
||||
- PASS: Task 1가 'Step 5'을 포함한다
|
||||
- PASS: Task 1가 'git commit -m'을 포함한다
|
||||
- PASS: Task 2가 '**Files:**'을 포함한다
|
||||
- PASS: Task 2가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 2가 'Step 1'을 포함한다
|
||||
- PASS: Task 2가 'Step 2'을 포함한다
|
||||
- PASS: Task 2가 'Step 3'을 포함한다
|
||||
- PASS: Task 2가 'Step 4'을 포함한다
|
||||
- PASS: Task 2가 'Step 5'을 포함한다
|
||||
- PASS: Task 2가 'git commit -m'을 포함한다
|
||||
- PASS: Task 3가 '**Files:**'을 포함한다
|
||||
- PASS: Task 3가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 3가 'Step 1'을 포함한다
|
||||
- PASS: Task 3가 'Step 2'을 포함한다
|
||||
- PASS: Task 3가 'Step 3'을 포함한다
|
||||
- PASS: Task 3가 'Step 4'을 포함한다
|
||||
- PASS: Task 3가 'Step 5'을 포함한다
|
||||
- PASS: Task 3가 'git commit -m'을 포함한다
|
||||
- PASS: Task 4가 '**Files:**'을 포함한다
|
||||
- PASS: Task 4가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 4가 'Step 1'을 포함한다
|
||||
- PASS: Task 4가 'Step 2'을 포함한다
|
||||
- PASS: Task 4가 'Step 3'을 포함한다
|
||||
- PASS: Task 4가 'Step 4'을 포함한다
|
||||
- PASS: Task 4가 'Step 5'을 포함한다
|
||||
- PASS: Task 4가 'git commit -m'을 포함한다
|
||||
- PASS: Task 5가 '**Files:**'을 포함한다
|
||||
- PASS: Task 5가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 5가 'Step 1'을 포함한다
|
||||
- PASS: Task 5가 'Step 2'을 포함한다
|
||||
- PASS: Task 5가 'Step 3'을 포함한다
|
||||
- PASS: Task 5가 'Step 4'을 포함한다
|
||||
- PASS: Task 5가 'Step 5'을 포함한다
|
||||
- PASS: Task 5가 'git commit -m'을 포함한다
|
||||
- PASS: Task 6가 '**Files:**'을 포함한다
|
||||
- PASS: Task 6가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 6가 'Step 1'을 포함한다
|
||||
- PASS: Task 6가 'Step 2'을 포함한다
|
||||
- PASS: Task 6가 'Step 3'을 포함한다
|
||||
- PASS: Task 6가 'Step 4'을 포함한다
|
||||
- PASS: Task 6가 'Step 5'을 포함한다
|
||||
- PASS: Task 6가 'git commit -m'을 포함한다
|
||||
- PASS: Task 7가 '**Files:**'을 포함한다
|
||||
- PASS: Task 7가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 7가 'Step 1'을 포함한다
|
||||
- PASS: Task 7가 'Step 2'을 포함한다
|
||||
- PASS: Task 7가 'Step 3'을 포함한다
|
||||
- PASS: Task 7가 'Step 4'을 포함한다
|
||||
- PASS: Task 7가 'Step 5'을 포함한다
|
||||
- PASS: Task 7가 'git commit -m'을 포함한다
|
||||
- PASS: Task 8가 '**Files:**'을 포함한다
|
||||
- PASS: Task 8가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 8가 'Step 1'을 포함한다
|
||||
- PASS: Task 8가 'Step 2'을 포함한다
|
||||
- PASS: Task 8가 'Step 3'을 포함한다
|
||||
- PASS: Task 8가 'Step 4'을 포함한다
|
||||
- PASS: Task 8가 'Step 5'을 포함한다
|
||||
- PASS: Task 8가 'git commit -m'을 포함한다
|
||||
- PASS: Task 9가 '**Files:**'을 포함한다
|
||||
- PASS: Task 9가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 9가 'Step 1'을 포함한다
|
||||
- PASS: Task 9가 'Step 2'을 포함한다
|
||||
- PASS: Task 9가 'Step 3'을 포함한다
|
||||
- PASS: Task 9가 'Step 4'을 포함한다
|
||||
- PASS: Task 9가 'Step 5'을 포함한다
|
||||
- PASS: Task 9가 'git commit -m'을 포함한다
|
||||
- PASS: Task 10가 '**Files:**'을 포함한다
|
||||
- PASS: Task 10가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 10가 'Step 1'을 포함한다
|
||||
- PASS: Task 10가 'Step 2'을 포함한다
|
||||
- PASS: Task 10가 'Step 3'을 포함한다
|
||||
- PASS: Task 10가 'Step 4'을 포함한다
|
||||
- PASS: Task 10가 'Step 5'을 포함한다
|
||||
- PASS: Task 10가 'git commit -m'을 포함한다
|
||||
- PASS: Task 11가 '**Files:**'을 포함한다
|
||||
- PASS: Task 11가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 11가 'Step 1'을 포함한다
|
||||
- PASS: Task 11가 'Step 2'을 포함한다
|
||||
- PASS: Task 11가 'Step 3'을 포함한다
|
||||
- PASS: Task 11가 'Step 4'을 포함한다
|
||||
- PASS: Task 11가 'Step 5'을 포함한다
|
||||
- PASS: Task 11가 'git commit -m'을 포함한다
|
||||
- PASS: Task 12가 '**Files:**'을 포함한다
|
||||
- PASS: Task 12가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 12가 'Step 1'을 포함한다
|
||||
- PASS: Task 12가 'Step 2'을 포함한다
|
||||
- PASS: Task 12가 'Step 3'을 포함한다
|
||||
- PASS: Task 12가 'Step 4'을 포함한다
|
||||
- PASS: Task 12가 'Step 5'을 포함한다
|
||||
- PASS: Task 12가 'git commit -m'을 포함한다
|
||||
- PASS: Task 13가 '**Files:**'을 포함한다
|
||||
- PASS: Task 13가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 13가 'Step 1'을 포함한다
|
||||
- PASS: Task 13가 'Step 2'을 포함한다
|
||||
- PASS: Task 13가 'Step 3'을 포함한다
|
||||
- PASS: Task 13가 'Step 4'을 포함한다
|
||||
- PASS: Task 13가 'Step 5'을 포함한다
|
||||
- PASS: Task 13가 'git commit -m'을 포함한다
|
||||
- PASS: Task 14가 '**Files:**'을 포함한다
|
||||
- PASS: Task 14가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 14가 'Step 1'을 포함한다
|
||||
- PASS: Task 14가 'Step 2'을 포함한다
|
||||
- PASS: Task 14가 'Step 3'을 포함한다
|
||||
- PASS: Task 14가 'Step 4'을 포함한다
|
||||
- PASS: Task 14가 'Step 5'을 포함한다
|
||||
- PASS: Task 14가 'git commit -m'을 포함한다
|
||||
- PASS: Task 15가 '**Files:**'을 포함한다
|
||||
- PASS: Task 15가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 15가 'Step 1'을 포함한다
|
||||
- PASS: Task 15가 'Step 2'을 포함한다
|
||||
- PASS: Task 15가 'Step 3'을 포함한다
|
||||
- PASS: Task 15가 'Step 4'을 포함한다
|
||||
- PASS: Task 15가 'Step 5'을 포함한다
|
||||
- PASS: Task 15가 'git commit -m'을 포함한다
|
||||
- PASS: Task 16가 '**Files:**'을 포함한다
|
||||
- PASS: Task 16가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 16가 'Step 1'을 포함한다
|
||||
- PASS: Task 16가 'Step 2'을 포함한다
|
||||
- PASS: Task 16가 'Step 3'을 포함한다
|
||||
- PASS: Task 16가 'Step 4'을 포함한다
|
||||
- PASS: Task 16가 'Step 5'을 포함한다
|
||||
- PASS: Task 16가 'git commit -m'을 포함한다
|
||||
- PASS: Task 17가 '**Files:**'을 포함한다
|
||||
- PASS: Task 17가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 17가 'Step 1'을 포함한다
|
||||
- PASS: Task 17가 'Step 2'을 포함한다
|
||||
- PASS: Task 17가 'Step 3'을 포함한다
|
||||
- PASS: Task 17가 'Step 4'을 포함한다
|
||||
- PASS: Task 17가 'Step 5'을 포함한다
|
||||
- PASS: Task 17가 'git commit -m'을 포함한다
|
||||
- PASS: Task 18가 '**Files:**'을 포함한다
|
||||
- PASS: Task 18가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 18가 'Step 1'을 포함한다
|
||||
- PASS: Task 18가 'Step 2'을 포함한다
|
||||
- PASS: Task 18가 'Step 3'을 포함한다
|
||||
- PASS: Task 18가 'Step 4'을 포함한다
|
||||
- PASS: Task 18가 'Step 5'을 포함한다
|
||||
- PASS: Task 18가 'git commit -m'을 포함한다
|
||||
- PASS: Task 19가 '**Files:**'을 포함한다
|
||||
- PASS: Task 19가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 19가 'Step 1'을 포함한다
|
||||
- PASS: Task 19가 'Step 2'을 포함한다
|
||||
- PASS: Task 19가 'Step 3'을 포함한다
|
||||
- PASS: Task 19가 'Step 4'을 포함한다
|
||||
- PASS: Task 19가 'Step 5'을 포함한다
|
||||
- PASS: Task 19가 'git commit -m'을 포함한다
|
||||
- PASS: Task 20가 '**Files:**'을 포함한다
|
||||
- PASS: Task 20가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 20가 'Step 1'을 포함한다
|
||||
- PASS: Task 20가 'Step 2'을 포함한다
|
||||
- PASS: Task 20가 'Step 3'을 포함한다
|
||||
- PASS: Task 20가 'Step 4'을 포함한다
|
||||
- PASS: Task 20가 'Step 5'을 포함한다
|
||||
- PASS: Task 20가 'git commit -m'을 포함한다
|
||||
- PASS: Task 21가 '**Files:**'을 포함한다
|
||||
- PASS: Task 21가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 21가 'Step 1'을 포함한다
|
||||
- PASS: Task 21가 'Step 2'을 포함한다
|
||||
- PASS: Task 21가 'Step 3'을 포함한다
|
||||
- PASS: Task 21가 'Step 4'을 포함한다
|
||||
- PASS: Task 21가 'Step 5'을 포함한다
|
||||
- PASS: Task 21가 'git commit -m'을 포함한다
|
||||
- PASS: Task 22가 '**Files:**'을 포함한다
|
||||
- PASS: Task 22가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 22가 'Step 1'을 포함한다
|
||||
- PASS: Task 22가 'Step 2'을 포함한다
|
||||
- PASS: Task 22가 'Step 3'을 포함한다
|
||||
- PASS: Task 22가 'Step 4'을 포함한다
|
||||
- PASS: Task 22가 'Step 5'을 포함한다
|
||||
- PASS: Task 22가 'git commit -m'을 포함한다
|
||||
- PASS: Task 23가 '**Files:**'을 포함한다
|
||||
- PASS: Task 23가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 23가 'Step 1'을 포함한다
|
||||
- PASS: Task 23가 'Step 2'을 포함한다
|
||||
- PASS: Task 23가 'Step 3'을 포함한다
|
||||
- PASS: Task 23가 'Step 4'을 포함한다
|
||||
- PASS: Task 23가 'Step 5'을 포함한다
|
||||
- PASS: Task 23가 'git commit -m'을 포함한다
|
||||
- PASS: Task 24가 '**Files:**'을 포함한다
|
||||
- PASS: Task 24가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 24가 'Step 1'을 포함한다
|
||||
- PASS: Task 24가 'Step 2'을 포함한다
|
||||
- PASS: Task 24가 'Step 3'을 포함한다
|
||||
- PASS: Task 24가 'Step 4'을 포함한다
|
||||
- PASS: Task 24가 'Step 5'을 포함한다
|
||||
- PASS: Task 24가 'git commit -m'을 포함한다
|
||||
- PASS: Task 25가 '**Files:**'을 포함한다
|
||||
- PASS: Task 25가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 25가 'Step 1'을 포함한다
|
||||
- PASS: Task 25가 'Step 2'을 포함한다
|
||||
- PASS: Task 25가 'Step 3'을 포함한다
|
||||
- PASS: Task 25가 'Step 4'을 포함한다
|
||||
- PASS: Task 25가 'Step 5'을 포함한다
|
||||
- PASS: Task 25가 'git commit -m'을 포함한다
|
||||
- PASS: Task 26가 '**Files:**'을 포함한다
|
||||
- PASS: Task 26가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 26가 'Step 1'을 포함한다
|
||||
- PASS: Task 26가 'Step 2'을 포함한다
|
||||
- PASS: Task 26가 'Step 3'을 포함한다
|
||||
- PASS: Task 26가 'Step 4'을 포함한다
|
||||
- PASS: Task 26가 'Step 5'을 포함한다
|
||||
- PASS: Task 26가 'git commit -m'을 포함한다
|
||||
- PASS: Task 27가 '**Files:**'을 포함한다
|
||||
- PASS: Task 27가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 27가 'Step 1'을 포함한다
|
||||
- PASS: Task 27가 'Step 2'을 포함한다
|
||||
- PASS: Task 27가 'Step 3'을 포함한다
|
||||
- PASS: Task 27가 'Step 4'을 포함한다
|
||||
- PASS: Task 27가 'Step 5'을 포함한다
|
||||
- PASS: Task 27가 'git commit -m'을 포함한다
|
||||
- PASS: Task 28가 '**Files:**'을 포함한다
|
||||
- PASS: Task 28가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 28가 'Step 1'을 포함한다
|
||||
- PASS: Task 28가 'Step 2'을 포함한다
|
||||
- PASS: Task 28가 'Step 3'을 포함한다
|
||||
- PASS: Task 28가 'Step 4'을 포함한다
|
||||
- PASS: Task 28가 'Step 5'을 포함한다
|
||||
- PASS: Task 28가 'git commit -m'을 포함한다
|
||||
- PASS: Task 29가 '**Files:**'을 포함한다
|
||||
- PASS: Task 29가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 29가 'Step 1'을 포함한다
|
||||
- PASS: Task 29가 'Step 2'을 포함한다
|
||||
- PASS: Task 29가 'Step 3'을 포함한다
|
||||
- PASS: Task 29가 'Step 4'을 포함한다
|
||||
- PASS: Task 29가 'Step 5'을 포함한다
|
||||
- PASS: Task 29가 'git commit -m'을 포함한다
|
||||
- PASS: Task 30가 '**Files:**'을 포함한다
|
||||
- PASS: Task 30가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 30가 'Step 1'을 포함한다
|
||||
- PASS: Task 30가 'Step 2'을 포함한다
|
||||
- PASS: Task 30가 'Step 3'을 포함한다
|
||||
- PASS: Task 30가 'Step 4'을 포함한다
|
||||
- PASS: Task 30가 'Step 5'을 포함한다
|
||||
- PASS: Task 30가 'git commit -m'을 포함한다
|
||||
- PASS: Task 31가 '**Files:**'을 포함한다
|
||||
- PASS: Task 31가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 31가 'Step 1'을 포함한다
|
||||
- PASS: Task 31가 'Step 2'을 포함한다
|
||||
- PASS: Task 31가 'Step 3'을 포함한다
|
||||
- PASS: Task 31가 'Step 4'을 포함한다
|
||||
- PASS: Task 31가 'Step 5'을 포함한다
|
||||
- PASS: Task 31가 'git commit -m'을 포함한다
|
||||
- PASS: Task 32가 '**Files:**'을 포함한다
|
||||
- PASS: Task 32가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 32가 'Step 1'을 포함한다
|
||||
- PASS: Task 32가 'Step 2'을 포함한다
|
||||
- PASS: Task 32가 'Step 3'을 포함한다
|
||||
- PASS: Task 32가 'Step 4'을 포함한다
|
||||
- PASS: Task 32가 'Step 5'을 포함한다
|
||||
- PASS: Task 32가 'git commit -m'을 포함한다
|
||||
- PASS: Task 33가 '**Files:**'을 포함한다
|
||||
- PASS: Task 33가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 33가 'Step 1'을 포함한다
|
||||
- PASS: Task 33가 'Step 2'을 포함한다
|
||||
- PASS: Task 33가 'Step 3'을 포함한다
|
||||
- PASS: Task 33가 'Step 4'을 포함한다
|
||||
- PASS: Task 33가 'Step 5'을 포함한다
|
||||
- PASS: Task 33가 'git commit -m'을 포함한다
|
||||
- PASS: Task 34가 '**Files:**'을 포함한다
|
||||
- PASS: Task 34가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 34가 'Step 1'을 포함한다
|
||||
- PASS: Task 34가 'Step 2'을 포함한다
|
||||
- PASS: Task 34가 'Step 3'을 포함한다
|
||||
- PASS: Task 34가 'Step 4'을 포함한다
|
||||
- PASS: Task 34가 'Step 5'을 포함한다
|
||||
- PASS: Task 34가 'git commit -m'을 포함한다
|
||||
- PASS: Task 35가 '**Files:**'을 포함한다
|
||||
- PASS: Task 35가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 35가 'Step 1'을 포함한다
|
||||
- PASS: Task 35가 'Step 2'을 포함한다
|
||||
- PASS: Task 35가 'Step 3'을 포함한다
|
||||
- PASS: Task 35가 'Step 4'을 포함한다
|
||||
- PASS: Task 35가 'Step 5'을 포함한다
|
||||
- PASS: Task 35가 'git commit -m'을 포함한다
|
||||
- PASS: Task 36가 '**Files:**'을 포함한다
|
||||
- PASS: Task 36가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 36가 'Step 1'을 포함한다
|
||||
- PASS: Task 36가 'Step 2'을 포함한다
|
||||
- PASS: Task 36가 'Step 3'을 포함한다
|
||||
- PASS: Task 36가 'Step 4'을 포함한다
|
||||
- PASS: Task 36가 'Step 5'을 포함한다
|
||||
- PASS: Task 36가 'git commit -m'을 포함한다
|
||||
- PASS: Task 37가 '**Files:**'을 포함한다
|
||||
- PASS: Task 37가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 37가 'Step 1'을 포함한다
|
||||
- PASS: Task 37가 'Step 2'을 포함한다
|
||||
- PASS: Task 37가 'Step 3'을 포함한다
|
||||
- PASS: Task 37가 'Step 4'을 포함한다
|
||||
- PASS: Task 37가 'Step 5'을 포함한다
|
||||
- PASS: Task 37가 'git commit -m'을 포함한다
|
||||
- PASS: Task 38가 '**Files:**'을 포함한다
|
||||
- PASS: Task 38가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 38가 'Step 1'을 포함한다
|
||||
- PASS: Task 38가 'Step 2'을 포함한다
|
||||
- PASS: Task 38가 'Step 3'을 포함한다
|
||||
- PASS: Task 38가 'Step 4'을 포함한다
|
||||
- PASS: Task 38가 'Step 5'을 포함한다
|
||||
- PASS: Task 38가 'git commit -m'을 포함한다
|
||||
- PASS: Task 39가 '**Files:**'을 포함한다
|
||||
- PASS: Task 39가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 39가 'Step 1'을 포함한다
|
||||
- PASS: Task 39가 'Step 2'을 포함한다
|
||||
- PASS: Task 39가 'Step 3'을 포함한다
|
||||
- PASS: Task 39가 'Step 4'을 포함한다
|
||||
- PASS: Task 39가 'Step 5'을 포함한다
|
||||
- PASS: Task 39가 'git commit -m'을 포함한다
|
||||
- PASS: Task 40가 '**Files:**'을 포함한다
|
||||
- PASS: Task 40가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 40가 'Step 1'을 포함한다
|
||||
- PASS: Task 40가 'Step 2'을 포함한다
|
||||
- PASS: Task 40가 'Step 3'을 포함한다
|
||||
- PASS: Task 40가 'Step 4'을 포함한다
|
||||
- PASS: Task 40가 'Step 5'을 포함한다
|
||||
- PASS: Task 40가 'git commit -m'을 포함한다
|
||||
- PASS: Task 41가 '**Files:**'을 포함한다
|
||||
- PASS: Task 41가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 41가 'Step 1'을 포함한다
|
||||
- PASS: Task 41가 'Step 2'을 포함한다
|
||||
- PASS: Task 41가 'Step 3'을 포함한다
|
||||
- PASS: Task 41가 'Step 4'을 포함한다
|
||||
- PASS: Task 41가 'Step 5'을 포함한다
|
||||
- PASS: Task 41가 'git commit -m'을 포함한다
|
||||
- PASS: Task 42가 '**Files:**'을 포함한다
|
||||
- PASS: Task 42가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 42가 'Step 1'을 포함한다
|
||||
- PASS: Task 42가 'Step 2'을 포함한다
|
||||
- PASS: Task 42가 'Step 3'을 포함한다
|
||||
- PASS: Task 42가 'Step 4'을 포함한다
|
||||
- PASS: Task 42가 'Step 5'을 포함한다
|
||||
- PASS: Task 42가 'git commit -m'을 포함한다
|
||||
- PASS: Task 43가 '**Files:**'을 포함한다
|
||||
- PASS: Task 43가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 43가 'Step 1'을 포함한다
|
||||
- PASS: Task 43가 'Step 2'을 포함한다
|
||||
- PASS: Task 43가 'Step 3'을 포함한다
|
||||
- PASS: Task 43가 'Step 4'을 포함한다
|
||||
- PASS: Task 43가 'Step 5'을 포함한다
|
||||
- PASS: Task 43가 'git commit -m'을 포함한다
|
||||
- PASS: Task 44가 '**Files:**'을 포함한다
|
||||
- PASS: Task 44가 '**Interfaces:**'을 포함한다
|
||||
- PASS: Task 44가 'Step 1'을 포함한다
|
||||
- PASS: Task 44가 'Step 2'을 포함한다
|
||||
- PASS: Task 44가 'Step 3'을 포함한다
|
||||
- PASS: Task 44가 'Step 4'을 포함한다
|
||||
- PASS: Task 44가 'Step 5'을 포함한다
|
||||
- PASS: Task 44가 'git commit -m'을 포함한다
|
||||
- PASS: 중복된 Create 파일 경로가 없다
|
||||
- PASS: Create 파일 경로에 wildcard가 없다
|
||||
- PASS: 계획서가 필수 작업 'Kafka Producer Adapter와 Publish Evidence'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Kafka Consumer Group, Partition Coordinator'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Kafka Native Transaction Capability'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Kafka Share Group Experimental Adapter'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Rabbit Publisher Confirm·Return Evidence Adapter'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Rabbit Consumer Manual ACK'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Transactional Outbox Repository'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Inbox Transactional Idempotent Consumer'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Debezium Outbox Event Router'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Pulsar Experimental Adapter'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'NATS JetStream Experimental Adapter'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Spring Cloud Stream Optional Bridge'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Global Backpressure'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 'Cross-broker 장애·보안·Reliability Contract Suite'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 '성능 인증, Compatibility Matrix'을 포함한다
|
||||
- PASS: 계획서가 필수 작업 '지원 문서, Runbook, ADR, Release Gate'을 포함한다
|
||||
- PASS: 계획서가 message identity 보존을 명시한다
|
||||
- PASS: 계획서가 DLQ 실패 시 source ACK 금지를 명시한다
|
||||
- PASS: 계획서가 credential 분리를 명시한다
|
||||
- PASS: Stable chaos aggregate task가 계획에 존재한다
|
||||
- PASS: performance aggregate task가 계획에 존재한다
|
||||
- PASS: compatibility aggregate task가 계획에 존재한다
|
||||
@@ -0,0 +1,47 @@
|
||||
> **보존 안내** — 이 파일은 저장소 루트 `mongodb-superpowers-package/` 에 있던 납품 패키지의 README다.
|
||||
> 2026-08-15 문서 정리에서 설계서·계획서는 `docs/superpowers/specs/` 와 `docs/superpowers/plans/` 로 통합했고,
|
||||
> `validate_mongodb_docs.py` 와 `MANIFEST.sha256` 은 제거했다.
|
||||
> 아래 본문은 납품 시점의 기록이므로 제거된 파일을 가리키는 문장은 더 이상 유효하지 않다.
|
||||
|
||||
# MongoDB Superpowers 문서 패키지
|
||||
|
||||
이 패키지는 첨부된 `MongoDB 문서 영속성 플랫폼 심층 리서치`를 요구사항 원본으로 사용해 작성한 설계서와 구현 계획서다.
|
||||
|
||||
## 파일
|
||||
|
||||
```text
|
||||
docs/superpowers/specs/2026-08-11-mongodb-document-persistence-platform-design.md
|
||||
docs/superpowers/plans/2026-08-11-mongodb-document-persistence-platform-implementation-plan.md
|
||||
docs/superpowers/plans/2026-08-11-mongodb-advanced-capabilities-expansion-plan.md
|
||||
VALIDATION.md
|
||||
validate_mongodb_docs.py
|
||||
MANIFEST.sha256
|
||||
```
|
||||
|
||||
## 적용 가정
|
||||
|
||||
```text
|
||||
Java 21
|
||||
Gradle Kotlin DSL
|
||||
Spring Boot 4.1 BOM
|
||||
Spring Data MongoDB 5.1
|
||||
Boot-managed MongoDB Java Driver
|
||||
MongoDB 8.0 primary certification
|
||||
MongoDB 7.0 compatibility
|
||||
root package: io.backend.skeleton.mongodb
|
||||
stable modules: modules/mongodb
|
||||
advanced modules: modules/mongodb-advanced
|
||||
```
|
||||
|
||||
실제 Backend Skeleton 저장소 구조가 입력에 포함되지 않았으므로 파일 경로는 위 가정에 따라 확정했다. 적용 시 기존 version catalog와 공통 build logic에 경로만 매핑하고 공개 계약과 신뢰성 의미론은 유지한다.
|
||||
|
||||
## 실행 순서
|
||||
|
||||
```text
|
||||
Stable Implementation Plan Task 1~50
|
||||
→ Stable Release Gate
|
||||
→ Advanced Plan Task 1~15
|
||||
→ capability별 Promotion Gate
|
||||
```
|
||||
|
||||
Stable 구현 전에 Advanced 모듈을 Starter에 추가하지 않는다.
|
||||
@@ -0,0 +1,737 @@
|
||||
# MongoDB Superpowers 문서 정적 검증 결과
|
||||
|
||||
## 결과
|
||||
|
||||
```text
|
||||
SUMMARY | total=687 pass=687 fail=0
|
||||
```
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---:|
|
||||
| 설계서 행 수 | 1303 |
|
||||
| Stable 구현 계획서 행 수 | 3943 |
|
||||
| Advanced 구현 계획서 행 수 | 1139 |
|
||||
| Stable Task | 50 |
|
||||
| Advanced Task | 15 |
|
||||
| Stable Create 경로 | 252 |
|
||||
| Advanced Create 경로 | 77 |
|
||||
|
||||
## 검증 범위
|
||||
|
||||
```text
|
||||
설계 핵심 결정 존재
|
||||
Task 번호 연속성
|
||||
모든 Task의 Files·Interfaces·Implementation requirements
|
||||
모든 Task의 Step 1~5
|
||||
Task별 실패·통과 예상 결과와 Commit
|
||||
Create 경로 중복·충돌
|
||||
Markdown code fence 균형
|
||||
TODO·TBD·FIXME 부재
|
||||
범용 Mongo Repository 실제 선언 부재
|
||||
Runtime arbitrary runCommand public API 부재
|
||||
Stable API V1 strict와 Replica Set 기준
|
||||
Transaction body retry와 commit retry 분리
|
||||
Change Stream at-least-once 계약
|
||||
TTL cleanup-only 계약
|
||||
GridFS compatibility-only 계약
|
||||
Stable·Advanced dependency 격리
|
||||
MongoDB 7.0·8.0 호환 Matrix
|
||||
Advanced actual topology promotion gate
|
||||
```
|
||||
|
||||
## 전체 검사 로그
|
||||
|
||||
```text
|
||||
PASS | design exists
|
||||
PASS | stable plan exists
|
||||
PASS | advanced plan exists
|
||||
PASS | design purpose
|
||||
PASS | domain ownership
|
||||
PASS | no generic repository design
|
||||
PASS | stable api strict
|
||||
PASS | local replica set
|
||||
PASS | standalone smoke only
|
||||
PASS | bson manifest
|
||||
PASS | transaction retry separation
|
||||
PASS | change stream at least once
|
||||
PASS | ttl cleanup only
|
||||
PASS | gridfs compatibility only
|
||||
PASS | driver native observability
|
||||
PASS | stable task count | 50
|
||||
PASS | stable task sequence | [1, 2, 3]...[48, 49, 50]
|
||||
PASS | stable task 1 has **Files:**
|
||||
PASS | stable task 1 has **Interfaces:**
|
||||
PASS | stable task 1 has **Implementation requirements:**
|
||||
PASS | stable task 1 has **Step 1:
|
||||
PASS | stable task 1 has **Step 2:
|
||||
PASS | stable task 1 has **Step 3:
|
||||
PASS | stable task 1 has **Step 4:
|
||||
PASS | stable task 1 has **Step 5:
|
||||
PASS | stable task 1 has commit
|
||||
PASS | stable task 1 has expected result
|
||||
PASS | stable task 2 has **Files:**
|
||||
PASS | stable task 2 has **Interfaces:**
|
||||
PASS | stable task 2 has **Implementation requirements:**
|
||||
PASS | stable task 2 has **Step 1:
|
||||
PASS | stable task 2 has **Step 2:
|
||||
PASS | stable task 2 has **Step 3:
|
||||
PASS | stable task 2 has **Step 4:
|
||||
PASS | stable task 2 has **Step 5:
|
||||
PASS | stable task 2 has commit
|
||||
PASS | stable task 2 has expected result
|
||||
PASS | stable task 3 has **Files:**
|
||||
PASS | stable task 3 has **Interfaces:**
|
||||
PASS | stable task 3 has **Implementation requirements:**
|
||||
PASS | stable task 3 has **Step 1:
|
||||
PASS | stable task 3 has **Step 2:
|
||||
PASS | stable task 3 has **Step 3:
|
||||
PASS | stable task 3 has **Step 4:
|
||||
PASS | stable task 3 has **Step 5:
|
||||
PASS | stable task 3 has commit
|
||||
PASS | stable task 3 has expected result
|
||||
PASS | stable task 4 has **Files:**
|
||||
PASS | stable task 4 has **Interfaces:**
|
||||
PASS | stable task 4 has **Implementation requirements:**
|
||||
PASS | stable task 4 has **Step 1:
|
||||
PASS | stable task 4 has **Step 2:
|
||||
PASS | stable task 4 has **Step 3:
|
||||
PASS | stable task 4 has **Step 4:
|
||||
PASS | stable task 4 has **Step 5:
|
||||
PASS | stable task 4 has commit
|
||||
PASS | stable task 4 has expected result
|
||||
PASS | stable task 5 has **Files:**
|
||||
PASS | stable task 5 has **Interfaces:**
|
||||
PASS | stable task 5 has **Implementation requirements:**
|
||||
PASS | stable task 5 has **Step 1:
|
||||
PASS | stable task 5 has **Step 2:
|
||||
PASS | stable task 5 has **Step 3:
|
||||
PASS | stable task 5 has **Step 4:
|
||||
PASS | stable task 5 has **Step 5:
|
||||
PASS | stable task 5 has commit
|
||||
PASS | stable task 5 has expected result
|
||||
PASS | stable task 6 has **Files:**
|
||||
PASS | stable task 6 has **Interfaces:**
|
||||
PASS | stable task 6 has **Implementation requirements:**
|
||||
PASS | stable task 6 has **Step 1:
|
||||
PASS | stable task 6 has **Step 2:
|
||||
PASS | stable task 6 has **Step 3:
|
||||
PASS | stable task 6 has **Step 4:
|
||||
PASS | stable task 6 has **Step 5:
|
||||
PASS | stable task 6 has commit
|
||||
PASS | stable task 6 has expected result
|
||||
PASS | stable task 7 has **Files:**
|
||||
PASS | stable task 7 has **Interfaces:**
|
||||
PASS | stable task 7 has **Implementation requirements:**
|
||||
PASS | stable task 7 has **Step 1:
|
||||
PASS | stable task 7 has **Step 2:
|
||||
PASS | stable task 7 has **Step 3:
|
||||
PASS | stable task 7 has **Step 4:
|
||||
PASS | stable task 7 has **Step 5:
|
||||
PASS | stable task 7 has commit
|
||||
PASS | stable task 7 has expected result
|
||||
PASS | stable task 8 has **Files:**
|
||||
PASS | stable task 8 has **Interfaces:**
|
||||
PASS | stable task 8 has **Implementation requirements:**
|
||||
PASS | stable task 8 has **Step 1:
|
||||
PASS | stable task 8 has **Step 2:
|
||||
PASS | stable task 8 has **Step 3:
|
||||
PASS | stable task 8 has **Step 4:
|
||||
PASS | stable task 8 has **Step 5:
|
||||
PASS | stable task 8 has commit
|
||||
PASS | stable task 8 has expected result
|
||||
PASS | stable task 9 has **Files:**
|
||||
PASS | stable task 9 has **Interfaces:**
|
||||
PASS | stable task 9 has **Implementation requirements:**
|
||||
PASS | stable task 9 has **Step 1:
|
||||
PASS | stable task 9 has **Step 2:
|
||||
PASS | stable task 9 has **Step 3:
|
||||
PASS | stable task 9 has **Step 4:
|
||||
PASS | stable task 9 has **Step 5:
|
||||
PASS | stable task 9 has commit
|
||||
PASS | stable task 9 has expected result
|
||||
PASS | stable task 10 has **Files:**
|
||||
PASS | stable task 10 has **Interfaces:**
|
||||
PASS | stable task 10 has **Implementation requirements:**
|
||||
PASS | stable task 10 has **Step 1:
|
||||
PASS | stable task 10 has **Step 2:
|
||||
PASS | stable task 10 has **Step 3:
|
||||
PASS | stable task 10 has **Step 4:
|
||||
PASS | stable task 10 has **Step 5:
|
||||
PASS | stable task 10 has commit
|
||||
PASS | stable task 10 has expected result
|
||||
PASS | stable task 11 has **Files:**
|
||||
PASS | stable task 11 has **Interfaces:**
|
||||
PASS | stable task 11 has **Implementation requirements:**
|
||||
PASS | stable task 11 has **Step 1:
|
||||
PASS | stable task 11 has **Step 2:
|
||||
PASS | stable task 11 has **Step 3:
|
||||
PASS | stable task 11 has **Step 4:
|
||||
PASS | stable task 11 has **Step 5:
|
||||
PASS | stable task 11 has commit
|
||||
PASS | stable task 11 has expected result
|
||||
PASS | stable task 12 has **Files:**
|
||||
PASS | stable task 12 has **Interfaces:**
|
||||
PASS | stable task 12 has **Implementation requirements:**
|
||||
PASS | stable task 12 has **Step 1:
|
||||
PASS | stable task 12 has **Step 2:
|
||||
PASS | stable task 12 has **Step 3:
|
||||
PASS | stable task 12 has **Step 4:
|
||||
PASS | stable task 12 has **Step 5:
|
||||
PASS | stable task 12 has commit
|
||||
PASS | stable task 12 has expected result
|
||||
PASS | stable task 13 has **Files:**
|
||||
PASS | stable task 13 has **Interfaces:**
|
||||
PASS | stable task 13 has **Implementation requirements:**
|
||||
PASS | stable task 13 has **Step 1:
|
||||
PASS | stable task 13 has **Step 2:
|
||||
PASS | stable task 13 has **Step 3:
|
||||
PASS | stable task 13 has **Step 4:
|
||||
PASS | stable task 13 has **Step 5:
|
||||
PASS | stable task 13 has commit
|
||||
PASS | stable task 13 has expected result
|
||||
PASS | stable task 14 has **Files:**
|
||||
PASS | stable task 14 has **Interfaces:**
|
||||
PASS | stable task 14 has **Implementation requirements:**
|
||||
PASS | stable task 14 has **Step 1:
|
||||
PASS | stable task 14 has **Step 2:
|
||||
PASS | stable task 14 has **Step 3:
|
||||
PASS | stable task 14 has **Step 4:
|
||||
PASS | stable task 14 has **Step 5:
|
||||
PASS | stable task 14 has commit
|
||||
PASS | stable task 14 has expected result
|
||||
PASS | stable task 15 has **Files:**
|
||||
PASS | stable task 15 has **Interfaces:**
|
||||
PASS | stable task 15 has **Implementation requirements:**
|
||||
PASS | stable task 15 has **Step 1:
|
||||
PASS | stable task 15 has **Step 2:
|
||||
PASS | stable task 15 has **Step 3:
|
||||
PASS | stable task 15 has **Step 4:
|
||||
PASS | stable task 15 has **Step 5:
|
||||
PASS | stable task 15 has commit
|
||||
PASS | stable task 15 has expected result
|
||||
PASS | stable task 16 has **Files:**
|
||||
PASS | stable task 16 has **Interfaces:**
|
||||
PASS | stable task 16 has **Implementation requirements:**
|
||||
PASS | stable task 16 has **Step 1:
|
||||
PASS | stable task 16 has **Step 2:
|
||||
PASS | stable task 16 has **Step 3:
|
||||
PASS | stable task 16 has **Step 4:
|
||||
PASS | stable task 16 has **Step 5:
|
||||
PASS | stable task 16 has commit
|
||||
PASS | stable task 16 has expected result
|
||||
PASS | stable task 17 has **Files:**
|
||||
PASS | stable task 17 has **Interfaces:**
|
||||
PASS | stable task 17 has **Implementation requirements:**
|
||||
PASS | stable task 17 has **Step 1:
|
||||
PASS | stable task 17 has **Step 2:
|
||||
PASS | stable task 17 has **Step 3:
|
||||
PASS | stable task 17 has **Step 4:
|
||||
PASS | stable task 17 has **Step 5:
|
||||
PASS | stable task 17 has commit
|
||||
PASS | stable task 17 has expected result
|
||||
PASS | stable task 18 has **Files:**
|
||||
PASS | stable task 18 has **Interfaces:**
|
||||
PASS | stable task 18 has **Implementation requirements:**
|
||||
PASS | stable task 18 has **Step 1:
|
||||
PASS | stable task 18 has **Step 2:
|
||||
PASS | stable task 18 has **Step 3:
|
||||
PASS | stable task 18 has **Step 4:
|
||||
PASS | stable task 18 has **Step 5:
|
||||
PASS | stable task 18 has commit
|
||||
PASS | stable task 18 has expected result
|
||||
PASS | stable task 19 has **Files:**
|
||||
PASS | stable task 19 has **Interfaces:**
|
||||
PASS | stable task 19 has **Implementation requirements:**
|
||||
PASS | stable task 19 has **Step 1:
|
||||
PASS | stable task 19 has **Step 2:
|
||||
PASS | stable task 19 has **Step 3:
|
||||
PASS | stable task 19 has **Step 4:
|
||||
PASS | stable task 19 has **Step 5:
|
||||
PASS | stable task 19 has commit
|
||||
PASS | stable task 19 has expected result
|
||||
PASS | stable task 20 has **Files:**
|
||||
PASS | stable task 20 has **Interfaces:**
|
||||
PASS | stable task 20 has **Implementation requirements:**
|
||||
PASS | stable task 20 has **Step 1:
|
||||
PASS | stable task 20 has **Step 2:
|
||||
PASS | stable task 20 has **Step 3:
|
||||
PASS | stable task 20 has **Step 4:
|
||||
PASS | stable task 20 has **Step 5:
|
||||
PASS | stable task 20 has commit
|
||||
PASS | stable task 20 has expected result
|
||||
PASS | stable task 21 has **Files:**
|
||||
PASS | stable task 21 has **Interfaces:**
|
||||
PASS | stable task 21 has **Implementation requirements:**
|
||||
PASS | stable task 21 has **Step 1:
|
||||
PASS | stable task 21 has **Step 2:
|
||||
PASS | stable task 21 has **Step 3:
|
||||
PASS | stable task 21 has **Step 4:
|
||||
PASS | stable task 21 has **Step 5:
|
||||
PASS | stable task 21 has commit
|
||||
PASS | stable task 21 has expected result
|
||||
PASS | stable task 22 has **Files:**
|
||||
PASS | stable task 22 has **Interfaces:**
|
||||
PASS | stable task 22 has **Implementation requirements:**
|
||||
PASS | stable task 22 has **Step 1:
|
||||
PASS | stable task 22 has **Step 2:
|
||||
PASS | stable task 22 has **Step 3:
|
||||
PASS | stable task 22 has **Step 4:
|
||||
PASS | stable task 22 has **Step 5:
|
||||
PASS | stable task 22 has commit
|
||||
PASS | stable task 22 has expected result
|
||||
PASS | stable task 23 has **Files:**
|
||||
PASS | stable task 23 has **Interfaces:**
|
||||
PASS | stable task 23 has **Implementation requirements:**
|
||||
PASS | stable task 23 has **Step 1:
|
||||
PASS | stable task 23 has **Step 2:
|
||||
PASS | stable task 23 has **Step 3:
|
||||
PASS | stable task 23 has **Step 4:
|
||||
PASS | stable task 23 has **Step 5:
|
||||
PASS | stable task 23 has commit
|
||||
PASS | stable task 23 has expected result
|
||||
PASS | stable task 24 has **Files:**
|
||||
PASS | stable task 24 has **Interfaces:**
|
||||
PASS | stable task 24 has **Implementation requirements:**
|
||||
PASS | stable task 24 has **Step 1:
|
||||
PASS | stable task 24 has **Step 2:
|
||||
PASS | stable task 24 has **Step 3:
|
||||
PASS | stable task 24 has **Step 4:
|
||||
PASS | stable task 24 has **Step 5:
|
||||
PASS | stable task 24 has commit
|
||||
PASS | stable task 24 has expected result
|
||||
PASS | stable task 25 has **Files:**
|
||||
PASS | stable task 25 has **Interfaces:**
|
||||
PASS | stable task 25 has **Implementation requirements:**
|
||||
PASS | stable task 25 has **Step 1:
|
||||
PASS | stable task 25 has **Step 2:
|
||||
PASS | stable task 25 has **Step 3:
|
||||
PASS | stable task 25 has **Step 4:
|
||||
PASS | stable task 25 has **Step 5:
|
||||
PASS | stable task 25 has commit
|
||||
PASS | stable task 25 has expected result
|
||||
PASS | stable task 26 has **Files:**
|
||||
PASS | stable task 26 has **Interfaces:**
|
||||
PASS | stable task 26 has **Implementation requirements:**
|
||||
PASS | stable task 26 has **Step 1:
|
||||
PASS | stable task 26 has **Step 2:
|
||||
PASS | stable task 26 has **Step 3:
|
||||
PASS | stable task 26 has **Step 4:
|
||||
PASS | stable task 26 has **Step 5:
|
||||
PASS | stable task 26 has commit
|
||||
PASS | stable task 26 has expected result
|
||||
PASS | stable task 27 has **Files:**
|
||||
PASS | stable task 27 has **Interfaces:**
|
||||
PASS | stable task 27 has **Implementation requirements:**
|
||||
PASS | stable task 27 has **Step 1:
|
||||
PASS | stable task 27 has **Step 2:
|
||||
PASS | stable task 27 has **Step 3:
|
||||
PASS | stable task 27 has **Step 4:
|
||||
PASS | stable task 27 has **Step 5:
|
||||
PASS | stable task 27 has commit
|
||||
PASS | stable task 27 has expected result
|
||||
PASS | stable task 28 has **Files:**
|
||||
PASS | stable task 28 has **Interfaces:**
|
||||
PASS | stable task 28 has **Implementation requirements:**
|
||||
PASS | stable task 28 has **Step 1:
|
||||
PASS | stable task 28 has **Step 2:
|
||||
PASS | stable task 28 has **Step 3:
|
||||
PASS | stable task 28 has **Step 4:
|
||||
PASS | stable task 28 has **Step 5:
|
||||
PASS | stable task 28 has commit
|
||||
PASS | stable task 28 has expected result
|
||||
PASS | stable task 29 has **Files:**
|
||||
PASS | stable task 29 has **Interfaces:**
|
||||
PASS | stable task 29 has **Implementation requirements:**
|
||||
PASS | stable task 29 has **Step 1:
|
||||
PASS | stable task 29 has **Step 2:
|
||||
PASS | stable task 29 has **Step 3:
|
||||
PASS | stable task 29 has **Step 4:
|
||||
PASS | stable task 29 has **Step 5:
|
||||
PASS | stable task 29 has commit
|
||||
PASS | stable task 29 has expected result
|
||||
PASS | stable task 30 has **Files:**
|
||||
PASS | stable task 30 has **Interfaces:**
|
||||
PASS | stable task 30 has **Implementation requirements:**
|
||||
PASS | stable task 30 has **Step 1:
|
||||
PASS | stable task 30 has **Step 2:
|
||||
PASS | stable task 30 has **Step 3:
|
||||
PASS | stable task 30 has **Step 4:
|
||||
PASS | stable task 30 has **Step 5:
|
||||
PASS | stable task 30 has commit
|
||||
PASS | stable task 30 has expected result
|
||||
PASS | stable task 31 has **Files:**
|
||||
PASS | stable task 31 has **Interfaces:**
|
||||
PASS | stable task 31 has **Implementation requirements:**
|
||||
PASS | stable task 31 has **Step 1:
|
||||
PASS | stable task 31 has **Step 2:
|
||||
PASS | stable task 31 has **Step 3:
|
||||
PASS | stable task 31 has **Step 4:
|
||||
PASS | stable task 31 has **Step 5:
|
||||
PASS | stable task 31 has commit
|
||||
PASS | stable task 31 has expected result
|
||||
PASS | stable task 32 has **Files:**
|
||||
PASS | stable task 32 has **Interfaces:**
|
||||
PASS | stable task 32 has **Implementation requirements:**
|
||||
PASS | stable task 32 has **Step 1:
|
||||
PASS | stable task 32 has **Step 2:
|
||||
PASS | stable task 32 has **Step 3:
|
||||
PASS | stable task 32 has **Step 4:
|
||||
PASS | stable task 32 has **Step 5:
|
||||
PASS | stable task 32 has commit
|
||||
PASS | stable task 32 has expected result
|
||||
PASS | stable task 33 has **Files:**
|
||||
PASS | stable task 33 has **Interfaces:**
|
||||
PASS | stable task 33 has **Implementation requirements:**
|
||||
PASS | stable task 33 has **Step 1:
|
||||
PASS | stable task 33 has **Step 2:
|
||||
PASS | stable task 33 has **Step 3:
|
||||
PASS | stable task 33 has **Step 4:
|
||||
PASS | stable task 33 has **Step 5:
|
||||
PASS | stable task 33 has commit
|
||||
PASS | stable task 33 has expected result
|
||||
PASS | stable task 34 has **Files:**
|
||||
PASS | stable task 34 has **Interfaces:**
|
||||
PASS | stable task 34 has **Implementation requirements:**
|
||||
PASS | stable task 34 has **Step 1:
|
||||
PASS | stable task 34 has **Step 2:
|
||||
PASS | stable task 34 has **Step 3:
|
||||
PASS | stable task 34 has **Step 4:
|
||||
PASS | stable task 34 has **Step 5:
|
||||
PASS | stable task 34 has commit
|
||||
PASS | stable task 34 has expected result
|
||||
PASS | stable task 35 has **Files:**
|
||||
PASS | stable task 35 has **Interfaces:**
|
||||
PASS | stable task 35 has **Implementation requirements:**
|
||||
PASS | stable task 35 has **Step 1:
|
||||
PASS | stable task 35 has **Step 2:
|
||||
PASS | stable task 35 has **Step 3:
|
||||
PASS | stable task 35 has **Step 4:
|
||||
PASS | stable task 35 has **Step 5:
|
||||
PASS | stable task 35 has commit
|
||||
PASS | stable task 35 has expected result
|
||||
PASS | stable task 36 has **Files:**
|
||||
PASS | stable task 36 has **Interfaces:**
|
||||
PASS | stable task 36 has **Implementation requirements:**
|
||||
PASS | stable task 36 has **Step 1:
|
||||
PASS | stable task 36 has **Step 2:
|
||||
PASS | stable task 36 has **Step 3:
|
||||
PASS | stable task 36 has **Step 4:
|
||||
PASS | stable task 36 has **Step 5:
|
||||
PASS | stable task 36 has commit
|
||||
PASS | stable task 36 has expected result
|
||||
PASS | stable task 37 has **Files:**
|
||||
PASS | stable task 37 has **Interfaces:**
|
||||
PASS | stable task 37 has **Implementation requirements:**
|
||||
PASS | stable task 37 has **Step 1:
|
||||
PASS | stable task 37 has **Step 2:
|
||||
PASS | stable task 37 has **Step 3:
|
||||
PASS | stable task 37 has **Step 4:
|
||||
PASS | stable task 37 has **Step 5:
|
||||
PASS | stable task 37 has commit
|
||||
PASS | stable task 37 has expected result
|
||||
PASS | stable task 38 has **Files:**
|
||||
PASS | stable task 38 has **Interfaces:**
|
||||
PASS | stable task 38 has **Implementation requirements:**
|
||||
PASS | stable task 38 has **Step 1:
|
||||
PASS | stable task 38 has **Step 2:
|
||||
PASS | stable task 38 has **Step 3:
|
||||
PASS | stable task 38 has **Step 4:
|
||||
PASS | stable task 38 has **Step 5:
|
||||
PASS | stable task 38 has commit
|
||||
PASS | stable task 38 has expected result
|
||||
PASS | stable task 39 has **Files:**
|
||||
PASS | stable task 39 has **Interfaces:**
|
||||
PASS | stable task 39 has **Implementation requirements:**
|
||||
PASS | stable task 39 has **Step 1:
|
||||
PASS | stable task 39 has **Step 2:
|
||||
PASS | stable task 39 has **Step 3:
|
||||
PASS | stable task 39 has **Step 4:
|
||||
PASS | stable task 39 has **Step 5:
|
||||
PASS | stable task 39 has commit
|
||||
PASS | stable task 39 has expected result
|
||||
PASS | stable task 40 has **Files:**
|
||||
PASS | stable task 40 has **Interfaces:**
|
||||
PASS | stable task 40 has **Implementation requirements:**
|
||||
PASS | stable task 40 has **Step 1:
|
||||
PASS | stable task 40 has **Step 2:
|
||||
PASS | stable task 40 has **Step 3:
|
||||
PASS | stable task 40 has **Step 4:
|
||||
PASS | stable task 40 has **Step 5:
|
||||
PASS | stable task 40 has commit
|
||||
PASS | stable task 40 has expected result
|
||||
PASS | stable task 41 has **Files:**
|
||||
PASS | stable task 41 has **Interfaces:**
|
||||
PASS | stable task 41 has **Implementation requirements:**
|
||||
PASS | stable task 41 has **Step 1:
|
||||
PASS | stable task 41 has **Step 2:
|
||||
PASS | stable task 41 has **Step 3:
|
||||
PASS | stable task 41 has **Step 4:
|
||||
PASS | stable task 41 has **Step 5:
|
||||
PASS | stable task 41 has commit
|
||||
PASS | stable task 41 has expected result
|
||||
PASS | stable task 42 has **Files:**
|
||||
PASS | stable task 42 has **Interfaces:**
|
||||
PASS | stable task 42 has **Implementation requirements:**
|
||||
PASS | stable task 42 has **Step 1:
|
||||
PASS | stable task 42 has **Step 2:
|
||||
PASS | stable task 42 has **Step 3:
|
||||
PASS | stable task 42 has **Step 4:
|
||||
PASS | stable task 42 has **Step 5:
|
||||
PASS | stable task 42 has commit
|
||||
PASS | stable task 42 has expected result
|
||||
PASS | stable task 43 has **Files:**
|
||||
PASS | stable task 43 has **Interfaces:**
|
||||
PASS | stable task 43 has **Implementation requirements:**
|
||||
PASS | stable task 43 has **Step 1:
|
||||
PASS | stable task 43 has **Step 2:
|
||||
PASS | stable task 43 has **Step 3:
|
||||
PASS | stable task 43 has **Step 4:
|
||||
PASS | stable task 43 has **Step 5:
|
||||
PASS | stable task 43 has commit
|
||||
PASS | stable task 43 has expected result
|
||||
PASS | stable task 44 has **Files:**
|
||||
PASS | stable task 44 has **Interfaces:**
|
||||
PASS | stable task 44 has **Implementation requirements:**
|
||||
PASS | stable task 44 has **Step 1:
|
||||
PASS | stable task 44 has **Step 2:
|
||||
PASS | stable task 44 has **Step 3:
|
||||
PASS | stable task 44 has **Step 4:
|
||||
PASS | stable task 44 has **Step 5:
|
||||
PASS | stable task 44 has commit
|
||||
PASS | stable task 44 has expected result
|
||||
PASS | stable task 45 has **Files:**
|
||||
PASS | stable task 45 has **Interfaces:**
|
||||
PASS | stable task 45 has **Implementation requirements:**
|
||||
PASS | stable task 45 has **Step 1:
|
||||
PASS | stable task 45 has **Step 2:
|
||||
PASS | stable task 45 has **Step 3:
|
||||
PASS | stable task 45 has **Step 4:
|
||||
PASS | stable task 45 has **Step 5:
|
||||
PASS | stable task 45 has commit
|
||||
PASS | stable task 45 has expected result
|
||||
PASS | stable task 46 has **Files:**
|
||||
PASS | stable task 46 has **Interfaces:**
|
||||
PASS | stable task 46 has **Implementation requirements:**
|
||||
PASS | stable task 46 has **Step 1:
|
||||
PASS | stable task 46 has **Step 2:
|
||||
PASS | stable task 46 has **Step 3:
|
||||
PASS | stable task 46 has **Step 4:
|
||||
PASS | stable task 46 has **Step 5:
|
||||
PASS | stable task 46 has commit
|
||||
PASS | stable task 46 has expected result
|
||||
PASS | stable task 47 has **Files:**
|
||||
PASS | stable task 47 has **Interfaces:**
|
||||
PASS | stable task 47 has **Implementation requirements:**
|
||||
PASS | stable task 47 has **Step 1:
|
||||
PASS | stable task 47 has **Step 2:
|
||||
PASS | stable task 47 has **Step 3:
|
||||
PASS | stable task 47 has **Step 4:
|
||||
PASS | stable task 47 has **Step 5:
|
||||
PASS | stable task 47 has commit
|
||||
PASS | stable task 47 has expected result
|
||||
PASS | stable task 48 has **Files:**
|
||||
PASS | stable task 48 has **Interfaces:**
|
||||
PASS | stable task 48 has **Implementation requirements:**
|
||||
PASS | stable task 48 has **Step 1:
|
||||
PASS | stable task 48 has **Step 2:
|
||||
PASS | stable task 48 has **Step 3:
|
||||
PASS | stable task 48 has **Step 4:
|
||||
PASS | stable task 48 has **Step 5:
|
||||
PASS | stable task 48 has commit
|
||||
PASS | stable task 48 has expected result
|
||||
PASS | stable task 49 has **Files:**
|
||||
PASS | stable task 49 has **Interfaces:**
|
||||
PASS | stable task 49 has **Implementation requirements:**
|
||||
PASS | stable task 49 has **Step 1:
|
||||
PASS | stable task 49 has **Step 2:
|
||||
PASS | stable task 49 has **Step 3:
|
||||
PASS | stable task 49 has **Step 4:
|
||||
PASS | stable task 49 has **Step 5:
|
||||
PASS | stable task 49 has commit
|
||||
PASS | stable task 49 has expected result
|
||||
PASS | stable task 50 has **Files:**
|
||||
PASS | stable task 50 has **Interfaces:**
|
||||
PASS | stable task 50 has **Implementation requirements:**
|
||||
PASS | stable task 50 has **Step 1:
|
||||
PASS | stable task 50 has **Step 2:
|
||||
PASS | stable task 50 has **Step 3:
|
||||
PASS | stable task 50 has **Step 4:
|
||||
PASS | stable task 50 has **Step 5:
|
||||
PASS | stable task 50 has commit
|
||||
PASS | stable task 50 has expected result
|
||||
PASS | advanced task count | 15
|
||||
PASS | advanced task sequence | [1, 2, 3]...[13, 14, 15]
|
||||
PASS | advanced task 1 has **Files:**
|
||||
PASS | advanced task 1 has **Interfaces:**
|
||||
PASS | advanced task 1 has **Implementation requirements:**
|
||||
PASS | advanced task 1 has **Step 1:
|
||||
PASS | advanced task 1 has **Step 2:
|
||||
PASS | advanced task 1 has **Step 3:
|
||||
PASS | advanced task 1 has **Step 4:
|
||||
PASS | advanced task 1 has **Step 5:
|
||||
PASS | advanced task 1 has commit
|
||||
PASS | advanced task 1 has expected result
|
||||
PASS | advanced task 2 has **Files:**
|
||||
PASS | advanced task 2 has **Interfaces:**
|
||||
PASS | advanced task 2 has **Implementation requirements:**
|
||||
PASS | advanced task 2 has **Step 1:
|
||||
PASS | advanced task 2 has **Step 2:
|
||||
PASS | advanced task 2 has **Step 3:
|
||||
PASS | advanced task 2 has **Step 4:
|
||||
PASS | advanced task 2 has **Step 5:
|
||||
PASS | advanced task 2 has commit
|
||||
PASS | advanced task 2 has expected result
|
||||
PASS | advanced task 3 has **Files:**
|
||||
PASS | advanced task 3 has **Interfaces:**
|
||||
PASS | advanced task 3 has **Implementation requirements:**
|
||||
PASS | advanced task 3 has **Step 1:
|
||||
PASS | advanced task 3 has **Step 2:
|
||||
PASS | advanced task 3 has **Step 3:
|
||||
PASS | advanced task 3 has **Step 4:
|
||||
PASS | advanced task 3 has **Step 5:
|
||||
PASS | advanced task 3 has commit
|
||||
PASS | advanced task 3 has expected result
|
||||
PASS | advanced task 4 has **Files:**
|
||||
PASS | advanced task 4 has **Interfaces:**
|
||||
PASS | advanced task 4 has **Implementation requirements:**
|
||||
PASS | advanced task 4 has **Step 1:
|
||||
PASS | advanced task 4 has **Step 2:
|
||||
PASS | advanced task 4 has **Step 3:
|
||||
PASS | advanced task 4 has **Step 4:
|
||||
PASS | advanced task 4 has **Step 5:
|
||||
PASS | advanced task 4 has commit
|
||||
PASS | advanced task 4 has expected result
|
||||
PASS | advanced task 5 has **Files:**
|
||||
PASS | advanced task 5 has **Interfaces:**
|
||||
PASS | advanced task 5 has **Implementation requirements:**
|
||||
PASS | advanced task 5 has **Step 1:
|
||||
PASS | advanced task 5 has **Step 2:
|
||||
PASS | advanced task 5 has **Step 3:
|
||||
PASS | advanced task 5 has **Step 4:
|
||||
PASS | advanced task 5 has **Step 5:
|
||||
PASS | advanced task 5 has commit
|
||||
PASS | advanced task 5 has expected result
|
||||
PASS | advanced task 6 has **Files:**
|
||||
PASS | advanced task 6 has **Interfaces:**
|
||||
PASS | advanced task 6 has **Implementation requirements:**
|
||||
PASS | advanced task 6 has **Step 1:
|
||||
PASS | advanced task 6 has **Step 2:
|
||||
PASS | advanced task 6 has **Step 3:
|
||||
PASS | advanced task 6 has **Step 4:
|
||||
PASS | advanced task 6 has **Step 5:
|
||||
PASS | advanced task 6 has commit
|
||||
PASS | advanced task 6 has expected result
|
||||
PASS | advanced task 7 has **Files:**
|
||||
PASS | advanced task 7 has **Interfaces:**
|
||||
PASS | advanced task 7 has **Implementation requirements:**
|
||||
PASS | advanced task 7 has **Step 1:
|
||||
PASS | advanced task 7 has **Step 2:
|
||||
PASS | advanced task 7 has **Step 3:
|
||||
PASS | advanced task 7 has **Step 4:
|
||||
PASS | advanced task 7 has **Step 5:
|
||||
PASS | advanced task 7 has commit
|
||||
PASS | advanced task 7 has expected result
|
||||
PASS | advanced task 8 has **Files:**
|
||||
PASS | advanced task 8 has **Interfaces:**
|
||||
PASS | advanced task 8 has **Implementation requirements:**
|
||||
PASS | advanced task 8 has **Step 1:
|
||||
PASS | advanced task 8 has **Step 2:
|
||||
PASS | advanced task 8 has **Step 3:
|
||||
PASS | advanced task 8 has **Step 4:
|
||||
PASS | advanced task 8 has **Step 5:
|
||||
PASS | advanced task 8 has commit
|
||||
PASS | advanced task 8 has expected result
|
||||
PASS | advanced task 9 has **Files:**
|
||||
PASS | advanced task 9 has **Interfaces:**
|
||||
PASS | advanced task 9 has **Implementation requirements:**
|
||||
PASS | advanced task 9 has **Step 1:
|
||||
PASS | advanced task 9 has **Step 2:
|
||||
PASS | advanced task 9 has **Step 3:
|
||||
PASS | advanced task 9 has **Step 4:
|
||||
PASS | advanced task 9 has **Step 5:
|
||||
PASS | advanced task 9 has commit
|
||||
PASS | advanced task 9 has expected result
|
||||
PASS | advanced task 10 has **Files:**
|
||||
PASS | advanced task 10 has **Interfaces:**
|
||||
PASS | advanced task 10 has **Implementation requirements:**
|
||||
PASS | advanced task 10 has **Step 1:
|
||||
PASS | advanced task 10 has **Step 2:
|
||||
PASS | advanced task 10 has **Step 3:
|
||||
PASS | advanced task 10 has **Step 4:
|
||||
PASS | advanced task 10 has **Step 5:
|
||||
PASS | advanced task 10 has commit
|
||||
PASS | advanced task 10 has expected result
|
||||
PASS | advanced task 11 has **Files:**
|
||||
PASS | advanced task 11 has **Interfaces:**
|
||||
PASS | advanced task 11 has **Implementation requirements:**
|
||||
PASS | advanced task 11 has **Step 1:
|
||||
PASS | advanced task 11 has **Step 2:
|
||||
PASS | advanced task 11 has **Step 3:
|
||||
PASS | advanced task 11 has **Step 4:
|
||||
PASS | advanced task 11 has **Step 5:
|
||||
PASS | advanced task 11 has commit
|
||||
PASS | advanced task 11 has expected result
|
||||
PASS | advanced task 12 has **Files:**
|
||||
PASS | advanced task 12 has **Interfaces:**
|
||||
PASS | advanced task 12 has **Implementation requirements:**
|
||||
PASS | advanced task 12 has **Step 1:
|
||||
PASS | advanced task 12 has **Step 2:
|
||||
PASS | advanced task 12 has **Step 3:
|
||||
PASS | advanced task 12 has **Step 4:
|
||||
PASS | advanced task 12 has **Step 5:
|
||||
PASS | advanced task 12 has commit
|
||||
PASS | advanced task 12 has expected result
|
||||
PASS | advanced task 13 has **Files:**
|
||||
PASS | advanced task 13 has **Interfaces:**
|
||||
PASS | advanced task 13 has **Implementation requirements:**
|
||||
PASS | advanced task 13 has **Step 1:
|
||||
PASS | advanced task 13 has **Step 2:
|
||||
PASS | advanced task 13 has **Step 3:
|
||||
PASS | advanced task 13 has **Step 4:
|
||||
PASS | advanced task 13 has **Step 5:
|
||||
PASS | advanced task 13 has commit
|
||||
PASS | advanced task 13 has expected result
|
||||
PASS | advanced task 14 has **Files:**
|
||||
PASS | advanced task 14 has **Interfaces:**
|
||||
PASS | advanced task 14 has **Implementation requirements:**
|
||||
PASS | advanced task 14 has **Step 1:
|
||||
PASS | advanced task 14 has **Step 2:
|
||||
PASS | advanced task 14 has **Step 3:
|
||||
PASS | advanced task 14 has **Step 4:
|
||||
PASS | advanced task 14 has **Step 5:
|
||||
PASS | advanced task 14 has commit
|
||||
PASS | advanced task 14 has expected result
|
||||
PASS | advanced task 15 has **Files:**
|
||||
PASS | advanced task 15 has **Interfaces:**
|
||||
PASS | advanced task 15 has **Implementation requirements:**
|
||||
PASS | advanced task 15 has **Step 1:
|
||||
PASS | advanced task 15 has **Step 2:
|
||||
PASS | advanced task 15 has **Step 3:
|
||||
PASS | advanced task 15 has **Step 4:
|
||||
PASS | advanced task 15 has **Step 5:
|
||||
PASS | advanced task 15 has commit
|
||||
PASS | advanced task 15 has expected result
|
||||
PASS | 2026-08-11-mongodb-document-persistence-platform-design.md code fences balanced | 132
|
||||
PASS | 2026-08-11-mongodb-document-persistence-platform-design.md no TODO markers
|
||||
PASS | 2026-08-11-mongodb-document-persistence-platform-design.md no wildcard create paths
|
||||
PASS | 2026-08-11-mongodb-document-persistence-platform-implementation-plan.md code fences balanced | 504
|
||||
PASS | 2026-08-11-mongodb-document-persistence-platform-implementation-plan.md no TODO markers
|
||||
PASS | 2026-08-11-mongodb-document-persistence-platform-implementation-plan.md no wildcard create paths
|
||||
PASS | 2026-08-11-mongodb-advanced-capabilities-expansion-plan.md code fences balanced | 150
|
||||
PASS | 2026-08-11-mongodb-advanced-capabilities-expansion-plan.md no TODO markers
|
||||
PASS | 2026-08-11-mongodb-advanced-capabilities-expansion-plan.md no wildcard create paths
|
||||
PASS | stable no duplicate create paths
|
||||
PASS | advanced no duplicate create paths
|
||||
PASS | stable and advanced create paths do not collide
|
||||
PASS | no real generic repository declaration
|
||||
PASS | no public arbitrary run command
|
||||
PASS | stable starter excludes advanced
|
||||
PASS | unknown commit body retry forbidden
|
||||
PASS | mongo seven and eight matrix
|
||||
PASS | advanced actual topology gate
|
||||
SUMMARY | total=687 pass=687 fail=0
|
||||
```
|
||||
|
||||
## 검증 한계
|
||||
|
||||
현재 검증은 설계서와 구현 계획서의 정적 구조·일관성 검사다. 실제 Backend Skeleton 저장소가 제공되지 않았으므로 Gradle configuration, compile, Spring Context, MongoDB Replica Set, failover, sharded cluster, Atlas/KMS, performance와 Git commit은 실행하지 않았다.
|
||||
@@ -0,0 +1,65 @@
|
||||
> **보존 안내** — 이 파일은 저장소 루트 `notification-superpowers-package/` 에 있던 납품 패키지의 README다.
|
||||
> 2026-08-15 문서 정리에서 설계서·계획서는 `docs/superpowers/specs/` 와 `docs/superpowers/plans/` 로 통합했고,
|
||||
> 실행 불가 상태였던 `validate_notification_docs.py` 와 `MANIFEST.sha256` 은 제거했다.
|
||||
> 아래 본문은 납품 시점의 기록이므로 제거된 파일을 가리키는 문장은 더 이상 유효하지 않다.
|
||||
|
||||
# Notification Delivery Platform Superpowers Package
|
||||
|
||||
이 패키지는 Backend Skeleton의 `notification` 기술 모듈을 구현하기 위한 설계서와 TDD 구현 계획서다.
|
||||
|
||||
## 파일
|
||||
|
||||
```text
|
||||
docs/superpowers/specs/2026-08-10-notification-platform-design.md
|
||||
docs/superpowers/plans/2026-08-10-notification-platform-implementation-plan.md
|
||||
VALIDATION.md
|
||||
validate_notification_docs.py
|
||||
MANIFEST.sha256
|
||||
```
|
||||
|
||||
## 핵심 계약
|
||||
|
||||
```text
|
||||
NotificationRequest
|
||||
→ RecipientDelivery
|
||||
→ DeliveryAttempt
|
||||
→ ProviderEvent append-only ledger
|
||||
→ channel-specific projection
|
||||
→ SubmissionOutcome / DeliveryOutcome / EvidenceLevel
|
||||
```
|
||||
|
||||
- `submit()` 성공은 durable acceptance만 뜻한다.
|
||||
- Provider acceptance와 delivery/read evidence를 분리한다.
|
||||
- 결과를 확정할 수 없으면 `AMBIGUOUS`로 보존한다.
|
||||
- ambiguous attempt의 자동 retry와 cross-channel fallback은 기본 차단한다.
|
||||
- FCM은 FID를 primary target으로, registration token을 legacy compatibility로 둔다.
|
||||
- Callback은 signature 검증 후 원장에 append하고 중복·역순·누락을 처리한다.
|
||||
- Contact Point는 AES-256-GCM 암호화와 HMAC-SHA-256 equality fingerprint를 사용한다.
|
||||
|
||||
## 구현 기준 가정
|
||||
|
||||
실제 Backend Skeleton 저장소가 입력되지 않았기 때문에 다음 가정을 문서에 명시했다.
|
||||
|
||||
```text
|
||||
Java 21
|
||||
Gradle Kotlin DSL
|
||||
Spring Framework 6.2 common line + Spring 7.0 compatibility job
|
||||
PostgreSQL 16
|
||||
JPA + Flyway
|
||||
root package: io.backend.skeleton.notification
|
||||
module root: modules/notification
|
||||
```
|
||||
|
||||
실제 저장소에서는 dependency catalog와 package/file path를 기존 규칙에 맞게 매핑하되 공개 계약과 신뢰성 의미론은 유지한다.
|
||||
|
||||
## 실행
|
||||
|
||||
1. 설계서를 검토한다.
|
||||
2. 계획서 Task 1부터 순서대로 실행한다.
|
||||
3. 각 Task에서 failing test → red 확인 → 최소 구현 → green 확인 → commit 순서를 지킨다.
|
||||
4. Task마다 specification review와 code-quality review를 분리한다.
|
||||
5. 마지막 release gate에서 contract, chaos, security, performance, compatibility job을 모두 실행한다.
|
||||
|
||||
## 검증 범위
|
||||
|
||||
`VALIDATION.md`는 문서 구조, Task 연속성, 파일 경로 중복, 미확정 표현, 핵심 계약, ZIP CRC를 검증한 결과다. 실제 Gradle compile과 Provider integration은 Backend Skeleton 저장소에서 계획서의 명령을 실행해야 한다.
|
||||
@@ -0,0 +1,590 @@
|
||||
# Notification Superpowers 문서 정적 검증
|
||||
|
||||
- 결과: **PASS**
|
||||
- 실행 검사: **567개**
|
||||
- 통과: **567개**
|
||||
- 실패: **0개**
|
||||
- 설계서: **4,770행**
|
||||
- 구현 계획서: **5,325행**
|
||||
- 구현 Task: **50개**
|
||||
- Create 경로: **322개**
|
||||
- 설계 SHA-256: `fe7b57c6573ef7305033fe9402333d3c10f7a03ccf421dce5f353e2616ac8677`
|
||||
- 계획 SHA-256: `176b0c86e11e6d4d8440285dd723e5555616ff9a69a7a9419250c2ce2b67f565`
|
||||
|
||||
## 검사 결과
|
||||
|
||||
| 검사 | 결과 | 상세 |
|
||||
|---|---:|---|
|
||||
| 설계서 존재 | PASS | /mnt/data/notification-platform-design.md |
|
||||
| 구현 계획서 존재 | PASS | /mnt/data/notification-platform-implementation-plan.md |
|
||||
| 설계서 최소 상세도 | PASS | 4,770 lines |
|
||||
| 계획서 최소 상세도 | PASS | 5,325 lines |
|
||||
| 설계서 코드 fence 균형 | PASS | 482 |
|
||||
| 계획서 코드 fence 균형 | PASS | 308 |
|
||||
| 설계 핵심 계약: NotificationRequest | PASS | |
|
||||
| 설계 핵심 계약: RecipientDelivery | PASS | |
|
||||
| 설계 핵심 계약: DeliveryAttempt | PASS | |
|
||||
| 설계 핵심 계약: ProviderEvent | PASS | |
|
||||
| 설계 핵심 계약: EvidenceLevel | PASS | |
|
||||
| 설계 핵심 계약: SubmissionOutcome | PASS | |
|
||||
| 설계 핵심 계약: DeliveryOutcome | PASS | |
|
||||
| 설계 핵심 계약: AMBIGUOUS | PASS | |
|
||||
| 설계 핵심 계약: append-only | PASS | |
|
||||
| 설계 핵심 계약: FCM_FID | PASS | |
|
||||
| 설계 핵심 계약: FCM_REGISTRATION_TOKEN_LEGACY | PASS | |
|
||||
| 설계 핵심 계약: AES-256-GCM | PASS | |
|
||||
| 설계 핵심 계약: HMAC-SHA-256 | PASS | |
|
||||
| 설계 핵심 계약: FOR UPDATE SKIP LOCKED | PASS | |
|
||||
| 설계 핵심 계약: SMTP Adapter | PASS | |
|
||||
| 설계 핵심 계약: Amazon SES Adapter | PASS | |
|
||||
| 설계 핵심 계약: Twilio Adapter | PASS | |
|
||||
| 설계 핵심 계약: FCM Adapter | PASS | |
|
||||
| 설계 핵심 계약: APNs Adapter | PASS | |
|
||||
| 설계 핵심 계약: Web Push Adapter | PASS | |
|
||||
| 설계 핵심 계약: In-App Inbox | PASS | |
|
||||
| 설계 핵심 계약: Reconciliation | PASS | |
|
||||
| 설계 핵심 계약: N4 Admin Plane | PASS | |
|
||||
| 설계 핵심 계약: 비지원 범위 | PASS | |
|
||||
| 설계 핵심 계약: 완료 정의 | PASS | |
|
||||
| exactlyOnce 단순 옵션 금지 명시 | PASS | |
|
||||
| 금지 보장 부재: guaranteedDelivery=true | PASS | |
|
||||
| 금지 보장 부재: APNs HTTP 200 = DELIVERED | PASS | |
|
||||
| 금지 보장 부재: FCM send success = DEVICE_DELIVERED | PASS | |
|
||||
| Task 1~50 연속성 | PASS | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50] |
|
||||
| Task chunk 수 | PASS | 50 |
|
||||
| Task 1 필수 항목: **Files:** | PASS | |
|
||||
| Task 1 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 1 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 1 필수 항목: **Step 1: | PASS | |
|
||||
| Task 1 필수 항목: **Step 2: | PASS | |
|
||||
| Task 1 필수 항목: **Step 3: | PASS | |
|
||||
| Task 1 필수 항목: **Step 4: | PASS | |
|
||||
| Task 1 필수 항목: **Step 5: | PASS | |
|
||||
| Task 1 필수 항목: Expected: | PASS | |
|
||||
| Task 1 필수 항목: git commit -m | PASS | |
|
||||
| Task 2 필수 항목: **Files:** | PASS | |
|
||||
| Task 2 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 2 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 2 필수 항목: **Step 1: | PASS | |
|
||||
| Task 2 필수 항목: **Step 2: | PASS | |
|
||||
| Task 2 필수 항목: **Step 3: | PASS | |
|
||||
| Task 2 필수 항목: **Step 4: | PASS | |
|
||||
| Task 2 필수 항목: **Step 5: | PASS | |
|
||||
| Task 2 필수 항목: Expected: | PASS | |
|
||||
| Task 2 필수 항목: git commit -m | PASS | |
|
||||
| Task 3 필수 항목: **Files:** | PASS | |
|
||||
| Task 3 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 3 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 3 필수 항목: **Step 1: | PASS | |
|
||||
| Task 3 필수 항목: **Step 2: | PASS | |
|
||||
| Task 3 필수 항목: **Step 3: | PASS | |
|
||||
| Task 3 필수 항목: **Step 4: | PASS | |
|
||||
| Task 3 필수 항목: **Step 5: | PASS | |
|
||||
| Task 3 필수 항목: Expected: | PASS | |
|
||||
| Task 3 필수 항목: git commit -m | PASS | |
|
||||
| Task 4 필수 항목: **Files:** | PASS | |
|
||||
| Task 4 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 4 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 4 필수 항목: **Step 1: | PASS | |
|
||||
| Task 4 필수 항목: **Step 2: | PASS | |
|
||||
| Task 4 필수 항목: **Step 3: | PASS | |
|
||||
| Task 4 필수 항목: **Step 4: | PASS | |
|
||||
| Task 4 필수 항목: **Step 5: | PASS | |
|
||||
| Task 4 필수 항목: Expected: | PASS | |
|
||||
| Task 4 필수 항목: git commit -m | PASS | |
|
||||
| Task 5 필수 항목: **Files:** | PASS | |
|
||||
| Task 5 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 5 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 5 필수 항목: **Step 1: | PASS | |
|
||||
| Task 5 필수 항목: **Step 2: | PASS | |
|
||||
| Task 5 필수 항목: **Step 3: | PASS | |
|
||||
| Task 5 필수 항목: **Step 4: | PASS | |
|
||||
| Task 5 필수 항목: **Step 5: | PASS | |
|
||||
| Task 5 필수 항목: Expected: | PASS | |
|
||||
| Task 5 필수 항목: git commit -m | PASS | |
|
||||
| Task 6 필수 항목: **Files:** | PASS | |
|
||||
| Task 6 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 6 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 6 필수 항목: **Step 1: | PASS | |
|
||||
| Task 6 필수 항목: **Step 2: | PASS | |
|
||||
| Task 6 필수 항목: **Step 3: | PASS | |
|
||||
| Task 6 필수 항목: **Step 4: | PASS | |
|
||||
| Task 6 필수 항목: **Step 5: | PASS | |
|
||||
| Task 6 필수 항목: Expected: | PASS | |
|
||||
| Task 6 필수 항목: git commit -m | PASS | |
|
||||
| Task 7 필수 항목: **Files:** | PASS | |
|
||||
| Task 7 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 7 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 7 필수 항목: **Step 1: | PASS | |
|
||||
| Task 7 필수 항목: **Step 2: | PASS | |
|
||||
| Task 7 필수 항목: **Step 3: | PASS | |
|
||||
| Task 7 필수 항목: **Step 4: | PASS | |
|
||||
| Task 7 필수 항목: **Step 5: | PASS | |
|
||||
| Task 7 필수 항목: Expected: | PASS | |
|
||||
| Task 7 필수 항목: git commit -m | PASS | |
|
||||
| Task 8 필수 항목: **Files:** | PASS | |
|
||||
| Task 8 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 8 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 8 필수 항목: **Step 1: | PASS | |
|
||||
| Task 8 필수 항목: **Step 2: | PASS | |
|
||||
| Task 8 필수 항목: **Step 3: | PASS | |
|
||||
| Task 8 필수 항목: **Step 4: | PASS | |
|
||||
| Task 8 필수 항목: **Step 5: | PASS | |
|
||||
| Task 8 필수 항목: Expected: | PASS | |
|
||||
| Task 8 필수 항목: git commit -m | PASS | |
|
||||
| Task 9 필수 항목: **Files:** | PASS | |
|
||||
| Task 9 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 9 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 9 필수 항목: **Step 1: | PASS | |
|
||||
| Task 9 필수 항목: **Step 2: | PASS | |
|
||||
| Task 9 필수 항목: **Step 3: | PASS | |
|
||||
| Task 9 필수 항목: **Step 4: | PASS | |
|
||||
| Task 9 필수 항목: **Step 5: | PASS | |
|
||||
| Task 9 필수 항목: Expected: | PASS | |
|
||||
| Task 9 필수 항목: git commit -m | PASS | |
|
||||
| Task 10 필수 항목: **Files:** | PASS | |
|
||||
| Task 10 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 10 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 10 필수 항목: **Step 1: | PASS | |
|
||||
| Task 10 필수 항목: **Step 2: | PASS | |
|
||||
| Task 10 필수 항목: **Step 3: | PASS | |
|
||||
| Task 10 필수 항목: **Step 4: | PASS | |
|
||||
| Task 10 필수 항목: **Step 5: | PASS | |
|
||||
| Task 10 필수 항목: Expected: | PASS | |
|
||||
| Task 10 필수 항목: git commit -m | PASS | |
|
||||
| Task 11 필수 항목: **Files:** | PASS | |
|
||||
| Task 11 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 11 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 11 필수 항목: **Step 1: | PASS | |
|
||||
| Task 11 필수 항목: **Step 2: | PASS | |
|
||||
| Task 11 필수 항목: **Step 3: | PASS | |
|
||||
| Task 11 필수 항목: **Step 4: | PASS | |
|
||||
| Task 11 필수 항목: **Step 5: | PASS | |
|
||||
| Task 11 필수 항목: Expected: | PASS | |
|
||||
| Task 11 필수 항목: git commit -m | PASS | |
|
||||
| Task 12 필수 항목: **Files:** | PASS | |
|
||||
| Task 12 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 12 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 12 필수 항목: **Step 1: | PASS | |
|
||||
| Task 12 필수 항목: **Step 2: | PASS | |
|
||||
| Task 12 필수 항목: **Step 3: | PASS | |
|
||||
| Task 12 필수 항목: **Step 4: | PASS | |
|
||||
| Task 12 필수 항목: **Step 5: | PASS | |
|
||||
| Task 12 필수 항목: Expected: | PASS | |
|
||||
| Task 12 필수 항목: git commit -m | PASS | |
|
||||
| Task 13 필수 항목: **Files:** | PASS | |
|
||||
| Task 13 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 13 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 13 필수 항목: **Step 1: | PASS | |
|
||||
| Task 13 필수 항목: **Step 2: | PASS | |
|
||||
| Task 13 필수 항목: **Step 3: | PASS | |
|
||||
| Task 13 필수 항목: **Step 4: | PASS | |
|
||||
| Task 13 필수 항목: **Step 5: | PASS | |
|
||||
| Task 13 필수 항목: Expected: | PASS | |
|
||||
| Task 13 필수 항목: git commit -m | PASS | |
|
||||
| Task 14 필수 항목: **Files:** | PASS | |
|
||||
| Task 14 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 14 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 14 필수 항목: **Step 1: | PASS | |
|
||||
| Task 14 필수 항목: **Step 2: | PASS | |
|
||||
| Task 14 필수 항목: **Step 3: | PASS | |
|
||||
| Task 14 필수 항목: **Step 4: | PASS | |
|
||||
| Task 14 필수 항목: **Step 5: | PASS | |
|
||||
| Task 14 필수 항목: Expected: | PASS | |
|
||||
| Task 14 필수 항목: git commit -m | PASS | |
|
||||
| Task 15 필수 항목: **Files:** | PASS | |
|
||||
| Task 15 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 15 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 15 필수 항목: **Step 1: | PASS | |
|
||||
| Task 15 필수 항목: **Step 2: | PASS | |
|
||||
| Task 15 필수 항목: **Step 3: | PASS | |
|
||||
| Task 15 필수 항목: **Step 4: | PASS | |
|
||||
| Task 15 필수 항목: **Step 5: | PASS | |
|
||||
| Task 15 필수 항목: Expected: | PASS | |
|
||||
| Task 15 필수 항목: git commit -m | PASS | |
|
||||
| Task 16 필수 항목: **Files:** | PASS | |
|
||||
| Task 16 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 16 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 16 필수 항목: **Step 1: | PASS | |
|
||||
| Task 16 필수 항목: **Step 2: | PASS | |
|
||||
| Task 16 필수 항목: **Step 3: | PASS | |
|
||||
| Task 16 필수 항목: **Step 4: | PASS | |
|
||||
| Task 16 필수 항목: **Step 5: | PASS | |
|
||||
| Task 16 필수 항목: Expected: | PASS | |
|
||||
| Task 16 필수 항목: git commit -m | PASS | |
|
||||
| Task 17 필수 항목: **Files:** | PASS | |
|
||||
| Task 17 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 17 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 17 필수 항목: **Step 1: | PASS | |
|
||||
| Task 17 필수 항목: **Step 2: | PASS | |
|
||||
| Task 17 필수 항목: **Step 3: | PASS | |
|
||||
| Task 17 필수 항목: **Step 4: | PASS | |
|
||||
| Task 17 필수 항목: **Step 5: | PASS | |
|
||||
| Task 17 필수 항목: Expected: | PASS | |
|
||||
| Task 17 필수 항목: git commit -m | PASS | |
|
||||
| Task 18 필수 항목: **Files:** | PASS | |
|
||||
| Task 18 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 18 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 18 필수 항목: **Step 1: | PASS | |
|
||||
| Task 18 필수 항목: **Step 2: | PASS | |
|
||||
| Task 18 필수 항목: **Step 3: | PASS | |
|
||||
| Task 18 필수 항목: **Step 4: | PASS | |
|
||||
| Task 18 필수 항목: **Step 5: | PASS | |
|
||||
| Task 18 필수 항목: Expected: | PASS | |
|
||||
| Task 18 필수 항목: git commit -m | PASS | |
|
||||
| Task 19 필수 항목: **Files:** | PASS | |
|
||||
| Task 19 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 19 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 19 필수 항목: **Step 1: | PASS | |
|
||||
| Task 19 필수 항목: **Step 2: | PASS | |
|
||||
| Task 19 필수 항목: **Step 3: | PASS | |
|
||||
| Task 19 필수 항목: **Step 4: | PASS | |
|
||||
| Task 19 필수 항목: **Step 5: | PASS | |
|
||||
| Task 19 필수 항목: Expected: | PASS | |
|
||||
| Task 19 필수 항목: git commit -m | PASS | |
|
||||
| Task 20 필수 항목: **Files:** | PASS | |
|
||||
| Task 20 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 20 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 20 필수 항목: **Step 1: | PASS | |
|
||||
| Task 20 필수 항목: **Step 2: | PASS | |
|
||||
| Task 20 필수 항목: **Step 3: | PASS | |
|
||||
| Task 20 필수 항목: **Step 4: | PASS | |
|
||||
| Task 20 필수 항목: **Step 5: | PASS | |
|
||||
| Task 20 필수 항목: Expected: | PASS | |
|
||||
| Task 20 필수 항목: git commit -m | PASS | |
|
||||
| Task 21 필수 항목: **Files:** | PASS | |
|
||||
| Task 21 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 21 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 21 필수 항목: **Step 1: | PASS | |
|
||||
| Task 21 필수 항목: **Step 2: | PASS | |
|
||||
| Task 21 필수 항목: **Step 3: | PASS | |
|
||||
| Task 21 필수 항목: **Step 4: | PASS | |
|
||||
| Task 21 필수 항목: **Step 5: | PASS | |
|
||||
| Task 21 필수 항목: Expected: | PASS | |
|
||||
| Task 21 필수 항목: git commit -m | PASS | |
|
||||
| Task 22 필수 항목: **Files:** | PASS | |
|
||||
| Task 22 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 22 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 22 필수 항목: **Step 1: | PASS | |
|
||||
| Task 22 필수 항목: **Step 2: | PASS | |
|
||||
| Task 22 필수 항목: **Step 3: | PASS | |
|
||||
| Task 22 필수 항목: **Step 4: | PASS | |
|
||||
| Task 22 필수 항목: **Step 5: | PASS | |
|
||||
| Task 22 필수 항목: Expected: | PASS | |
|
||||
| Task 22 필수 항목: git commit -m | PASS | |
|
||||
| Task 23 필수 항목: **Files:** | PASS | |
|
||||
| Task 23 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 23 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 23 필수 항목: **Step 1: | PASS | |
|
||||
| Task 23 필수 항목: **Step 2: | PASS | |
|
||||
| Task 23 필수 항목: **Step 3: | PASS | |
|
||||
| Task 23 필수 항목: **Step 4: | PASS | |
|
||||
| Task 23 필수 항목: **Step 5: | PASS | |
|
||||
| Task 23 필수 항목: Expected: | PASS | |
|
||||
| Task 23 필수 항목: git commit -m | PASS | |
|
||||
| Task 24 필수 항목: **Files:** | PASS | |
|
||||
| Task 24 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 24 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 24 필수 항목: **Step 1: | PASS | |
|
||||
| Task 24 필수 항목: **Step 2: | PASS | |
|
||||
| Task 24 필수 항목: **Step 3: | PASS | |
|
||||
| Task 24 필수 항목: **Step 4: | PASS | |
|
||||
| Task 24 필수 항목: **Step 5: | PASS | |
|
||||
| Task 24 필수 항목: Expected: | PASS | |
|
||||
| Task 24 필수 항목: git commit -m | PASS | |
|
||||
| Task 25 필수 항목: **Files:** | PASS | |
|
||||
| Task 25 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 25 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 25 필수 항목: **Step 1: | PASS | |
|
||||
| Task 25 필수 항목: **Step 2: | PASS | |
|
||||
| Task 25 필수 항목: **Step 3: | PASS | |
|
||||
| Task 25 필수 항목: **Step 4: | PASS | |
|
||||
| Task 25 필수 항목: **Step 5: | PASS | |
|
||||
| Task 25 필수 항목: Expected: | PASS | |
|
||||
| Task 25 필수 항목: git commit -m | PASS | |
|
||||
| Task 26 필수 항목: **Files:** | PASS | |
|
||||
| Task 26 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 26 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 26 필수 항목: **Step 1: | PASS | |
|
||||
| Task 26 필수 항목: **Step 2: | PASS | |
|
||||
| Task 26 필수 항목: **Step 3: | PASS | |
|
||||
| Task 26 필수 항목: **Step 4: | PASS | |
|
||||
| Task 26 필수 항목: **Step 5: | PASS | |
|
||||
| Task 26 필수 항목: Expected: | PASS | |
|
||||
| Task 26 필수 항목: git commit -m | PASS | |
|
||||
| Task 27 필수 항목: **Files:** | PASS | |
|
||||
| Task 27 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 27 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 27 필수 항목: **Step 1: | PASS | |
|
||||
| Task 27 필수 항목: **Step 2: | PASS | |
|
||||
| Task 27 필수 항목: **Step 3: | PASS | |
|
||||
| Task 27 필수 항목: **Step 4: | PASS | |
|
||||
| Task 27 필수 항목: **Step 5: | PASS | |
|
||||
| Task 27 필수 항목: Expected: | PASS | |
|
||||
| Task 27 필수 항목: git commit -m | PASS | |
|
||||
| Task 28 필수 항목: **Files:** | PASS | |
|
||||
| Task 28 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 28 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 28 필수 항목: **Step 1: | PASS | |
|
||||
| Task 28 필수 항목: **Step 2: | PASS | |
|
||||
| Task 28 필수 항목: **Step 3: | PASS | |
|
||||
| Task 28 필수 항목: **Step 4: | PASS | |
|
||||
| Task 28 필수 항목: **Step 5: | PASS | |
|
||||
| Task 28 필수 항목: Expected: | PASS | |
|
||||
| Task 28 필수 항목: git commit -m | PASS | |
|
||||
| Task 29 필수 항목: **Files:** | PASS | |
|
||||
| Task 29 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 29 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 29 필수 항목: **Step 1: | PASS | |
|
||||
| Task 29 필수 항목: **Step 2: | PASS | |
|
||||
| Task 29 필수 항목: **Step 3: | PASS | |
|
||||
| Task 29 필수 항목: **Step 4: | PASS | |
|
||||
| Task 29 필수 항목: **Step 5: | PASS | |
|
||||
| Task 29 필수 항목: Expected: | PASS | |
|
||||
| Task 29 필수 항목: git commit -m | PASS | |
|
||||
| Task 30 필수 항목: **Files:** | PASS | |
|
||||
| Task 30 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 30 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 30 필수 항목: **Step 1: | PASS | |
|
||||
| Task 30 필수 항목: **Step 2: | PASS | |
|
||||
| Task 30 필수 항목: **Step 3: | PASS | |
|
||||
| Task 30 필수 항목: **Step 4: | PASS | |
|
||||
| Task 30 필수 항목: **Step 5: | PASS | |
|
||||
| Task 30 필수 항목: Expected: | PASS | |
|
||||
| Task 30 필수 항목: git commit -m | PASS | |
|
||||
| Task 31 필수 항목: **Files:** | PASS | |
|
||||
| Task 31 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 31 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 31 필수 항목: **Step 1: | PASS | |
|
||||
| Task 31 필수 항목: **Step 2: | PASS | |
|
||||
| Task 31 필수 항목: **Step 3: | PASS | |
|
||||
| Task 31 필수 항목: **Step 4: | PASS | |
|
||||
| Task 31 필수 항목: **Step 5: | PASS | |
|
||||
| Task 31 필수 항목: Expected: | PASS | |
|
||||
| Task 31 필수 항목: git commit -m | PASS | |
|
||||
| Task 32 필수 항목: **Files:** | PASS | |
|
||||
| Task 32 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 32 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 32 필수 항목: **Step 1: | PASS | |
|
||||
| Task 32 필수 항목: **Step 2: | PASS | |
|
||||
| Task 32 필수 항목: **Step 3: | PASS | |
|
||||
| Task 32 필수 항목: **Step 4: | PASS | |
|
||||
| Task 32 필수 항목: **Step 5: | PASS | |
|
||||
| Task 32 필수 항목: Expected: | PASS | |
|
||||
| Task 32 필수 항목: git commit -m | PASS | |
|
||||
| Task 33 필수 항목: **Files:** | PASS | |
|
||||
| Task 33 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 33 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 33 필수 항목: **Step 1: | PASS | |
|
||||
| Task 33 필수 항목: **Step 2: | PASS | |
|
||||
| Task 33 필수 항목: **Step 3: | PASS | |
|
||||
| Task 33 필수 항목: **Step 4: | PASS | |
|
||||
| Task 33 필수 항목: **Step 5: | PASS | |
|
||||
| Task 33 필수 항목: Expected: | PASS | |
|
||||
| Task 33 필수 항목: git commit -m | PASS | |
|
||||
| Task 34 필수 항목: **Files:** | PASS | |
|
||||
| Task 34 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 34 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 34 필수 항목: **Step 1: | PASS | |
|
||||
| Task 34 필수 항목: **Step 2: | PASS | |
|
||||
| Task 34 필수 항목: **Step 3: | PASS | |
|
||||
| Task 34 필수 항목: **Step 4: | PASS | |
|
||||
| Task 34 필수 항목: **Step 5: | PASS | |
|
||||
| Task 34 필수 항목: Expected: | PASS | |
|
||||
| Task 34 필수 항목: git commit -m | PASS | |
|
||||
| Task 35 필수 항목: **Files:** | PASS | |
|
||||
| Task 35 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 35 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 35 필수 항목: **Step 1: | PASS | |
|
||||
| Task 35 필수 항목: **Step 2: | PASS | |
|
||||
| Task 35 필수 항목: **Step 3: | PASS | |
|
||||
| Task 35 필수 항목: **Step 4: | PASS | |
|
||||
| Task 35 필수 항목: **Step 5: | PASS | |
|
||||
| Task 35 필수 항목: Expected: | PASS | |
|
||||
| Task 35 필수 항목: git commit -m | PASS | |
|
||||
| Task 36 필수 항목: **Files:** | PASS | |
|
||||
| Task 36 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 36 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 36 필수 항목: **Step 1: | PASS | |
|
||||
| Task 36 필수 항목: **Step 2: | PASS | |
|
||||
| Task 36 필수 항목: **Step 3: | PASS | |
|
||||
| Task 36 필수 항목: **Step 4: | PASS | |
|
||||
| Task 36 필수 항목: **Step 5: | PASS | |
|
||||
| Task 36 필수 항목: Expected: | PASS | |
|
||||
| Task 36 필수 항목: git commit -m | PASS | |
|
||||
| Task 37 필수 항목: **Files:** | PASS | |
|
||||
| Task 37 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 37 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 37 필수 항목: **Step 1: | PASS | |
|
||||
| Task 37 필수 항목: **Step 2: | PASS | |
|
||||
| Task 37 필수 항목: **Step 3: | PASS | |
|
||||
| Task 37 필수 항목: **Step 4: | PASS | |
|
||||
| Task 37 필수 항목: **Step 5: | PASS | |
|
||||
| Task 37 필수 항목: Expected: | PASS | |
|
||||
| Task 37 필수 항목: git commit -m | PASS | |
|
||||
| Task 38 필수 항목: **Files:** | PASS | |
|
||||
| Task 38 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 38 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 38 필수 항목: **Step 1: | PASS | |
|
||||
| Task 38 필수 항목: **Step 2: | PASS | |
|
||||
| Task 38 필수 항목: **Step 3: | PASS | |
|
||||
| Task 38 필수 항목: **Step 4: | PASS | |
|
||||
| Task 38 필수 항목: **Step 5: | PASS | |
|
||||
| Task 38 필수 항목: Expected: | PASS | |
|
||||
| Task 38 필수 항목: git commit -m | PASS | |
|
||||
| Task 39 필수 항목: **Files:** | PASS | |
|
||||
| Task 39 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 39 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 39 필수 항목: **Step 1: | PASS | |
|
||||
| Task 39 필수 항목: **Step 2: | PASS | |
|
||||
| Task 39 필수 항목: **Step 3: | PASS | |
|
||||
| Task 39 필수 항목: **Step 4: | PASS | |
|
||||
| Task 39 필수 항목: **Step 5: | PASS | |
|
||||
| Task 39 필수 항목: Expected: | PASS | |
|
||||
| Task 39 필수 항목: git commit -m | PASS | |
|
||||
| Task 40 필수 항목: **Files:** | PASS | |
|
||||
| Task 40 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 40 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 40 필수 항목: **Step 1: | PASS | |
|
||||
| Task 40 필수 항목: **Step 2: | PASS | |
|
||||
| Task 40 필수 항목: **Step 3: | PASS | |
|
||||
| Task 40 필수 항목: **Step 4: | PASS | |
|
||||
| Task 40 필수 항목: **Step 5: | PASS | |
|
||||
| Task 40 필수 항목: Expected: | PASS | |
|
||||
| Task 40 필수 항목: git commit -m | PASS | |
|
||||
| Task 41 필수 항목: **Files:** | PASS | |
|
||||
| Task 41 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 41 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 41 필수 항목: **Step 1: | PASS | |
|
||||
| Task 41 필수 항목: **Step 2: | PASS | |
|
||||
| Task 41 필수 항목: **Step 3: | PASS | |
|
||||
| Task 41 필수 항목: **Step 4: | PASS | |
|
||||
| Task 41 필수 항목: **Step 5: | PASS | |
|
||||
| Task 41 필수 항목: Expected: | PASS | |
|
||||
| Task 41 필수 항목: git commit -m | PASS | |
|
||||
| Task 42 필수 항목: **Files:** | PASS | |
|
||||
| Task 42 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 42 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 42 필수 항목: **Step 1: | PASS | |
|
||||
| Task 42 필수 항목: **Step 2: | PASS | |
|
||||
| Task 42 필수 항목: **Step 3: | PASS | |
|
||||
| Task 42 필수 항목: **Step 4: | PASS | |
|
||||
| Task 42 필수 항목: **Step 5: | PASS | |
|
||||
| Task 42 필수 항목: Expected: | PASS | |
|
||||
| Task 42 필수 항목: git commit -m | PASS | |
|
||||
| Task 43 필수 항목: **Files:** | PASS | |
|
||||
| Task 43 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 43 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 43 필수 항목: **Step 1: | PASS | |
|
||||
| Task 43 필수 항목: **Step 2: | PASS | |
|
||||
| Task 43 필수 항목: **Step 3: | PASS | |
|
||||
| Task 43 필수 항목: **Step 4: | PASS | |
|
||||
| Task 43 필수 항목: **Step 5: | PASS | |
|
||||
| Task 43 필수 항목: Expected: | PASS | |
|
||||
| Task 43 필수 항목: git commit -m | PASS | |
|
||||
| Task 44 필수 항목: **Files:** | PASS | |
|
||||
| Task 44 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 44 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 44 필수 항목: **Step 1: | PASS | |
|
||||
| Task 44 필수 항목: **Step 2: | PASS | |
|
||||
| Task 44 필수 항목: **Step 3: | PASS | |
|
||||
| Task 44 필수 항목: **Step 4: | PASS | |
|
||||
| Task 44 필수 항목: **Step 5: | PASS | |
|
||||
| Task 44 필수 항목: Expected: | PASS | |
|
||||
| Task 44 필수 항목: git commit -m | PASS | |
|
||||
| Task 45 필수 항목: **Files:** | PASS | |
|
||||
| Task 45 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 45 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 45 필수 항목: **Step 1: | PASS | |
|
||||
| Task 45 필수 항목: **Step 2: | PASS | |
|
||||
| Task 45 필수 항목: **Step 3: | PASS | |
|
||||
| Task 45 필수 항목: **Step 4: | PASS | |
|
||||
| Task 45 필수 항목: **Step 5: | PASS | |
|
||||
| Task 45 필수 항목: Expected: | PASS | |
|
||||
| Task 45 필수 항목: git commit -m | PASS | |
|
||||
| Task 46 필수 항목: **Files:** | PASS | |
|
||||
| Task 46 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 46 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 46 필수 항목: **Step 1: | PASS | |
|
||||
| Task 46 필수 항목: **Step 2: | PASS | |
|
||||
| Task 46 필수 항목: **Step 3: | PASS | |
|
||||
| Task 46 필수 항목: **Step 4: | PASS | |
|
||||
| Task 46 필수 항목: **Step 5: | PASS | |
|
||||
| Task 46 필수 항목: Expected: | PASS | |
|
||||
| Task 46 필수 항목: git commit -m | PASS | |
|
||||
| Task 47 필수 항목: **Files:** | PASS | |
|
||||
| Task 47 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 47 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 47 필수 항목: **Step 1: | PASS | |
|
||||
| Task 47 필수 항목: **Step 2: | PASS | |
|
||||
| Task 47 필수 항목: **Step 3: | PASS | |
|
||||
| Task 47 필수 항목: **Step 4: | PASS | |
|
||||
| Task 47 필수 항목: **Step 5: | PASS | |
|
||||
| Task 47 필수 항목: Expected: | PASS | |
|
||||
| Task 47 필수 항목: git commit -m | PASS | |
|
||||
| Task 48 필수 항목: **Files:** | PASS | |
|
||||
| Task 48 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 48 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 48 필수 항목: **Step 1: | PASS | |
|
||||
| Task 48 필수 항목: **Step 2: | PASS | |
|
||||
| Task 48 필수 항목: **Step 3: | PASS | |
|
||||
| Task 48 필수 항목: **Step 4: | PASS | |
|
||||
| Task 48 필수 항목: **Step 5: | PASS | |
|
||||
| Task 48 필수 항목: Expected: | PASS | |
|
||||
| Task 48 필수 항목: git commit -m | PASS | |
|
||||
| Task 49 필수 항목: **Files:** | PASS | |
|
||||
| Task 49 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 49 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 49 필수 항목: **Step 1: | PASS | |
|
||||
| Task 49 필수 항목: **Step 2: | PASS | |
|
||||
| Task 49 필수 항목: **Step 3: | PASS | |
|
||||
| Task 49 필수 항목: **Step 4: | PASS | |
|
||||
| Task 49 필수 항목: **Step 5: | PASS | |
|
||||
| Task 49 필수 항목: Expected: | PASS | |
|
||||
| Task 49 필수 항목: git commit -m | PASS | |
|
||||
| Task 50 필수 항목: **Files:** | PASS | |
|
||||
| Task 50 필수 항목: **Interfaces:** | PASS | |
|
||||
| Task 50 필수 항목: **Implementation requirements:** | PASS | |
|
||||
| Task 50 필수 항목: **Step 1: | PASS | |
|
||||
| Task 50 필수 항목: **Step 2: | PASS | |
|
||||
| Task 50 필수 항목: **Step 3: | PASS | |
|
||||
| Task 50 필수 항목: **Step 4: | PASS | |
|
||||
| Task 50 필수 항목: **Step 5: | PASS | |
|
||||
| Task 50 필수 항목: Expected: | PASS | |
|
||||
| Task 50 필수 항목: git commit -m | PASS | |
|
||||
| Create 경로 중복 없음 | PASS | |
|
||||
| Create 경로 충분성 | PASS | 322 |
|
||||
| 미확정 표현 없음: TODO | PASS | 0 |
|
||||
| 미확정 표현 없음: TBD | PASS | 0 |
|
||||
| 미확정 표현 없음: FIXME | PASS | 0 |
|
||||
| 미확정 표현 없음: fill in details | PASS | 0 |
|
||||
| 미확정 표현 없음: implement later | PASS | 0 |
|
||||
| 미확정 표현 없음: concrete assertion below | PASS | 0 |
|
||||
| 미확정 표현 없음: 유사 작업 참조 | PASS | 0 |
|
||||
| 계획 핵심 회귀 테스트: providerAcceptanceIsNotDelivery | PASS | |
|
||||
| 계획 핵심 회귀 테스트: concurrentSameRequestReturnsOneNotificationId | PASS | |
|
||||
| 계획 핵심 회귀 테스트: providerAcceptsThenResponseIsLostRecordsAmbiguousAndBlocksFallback | PASS | |
|
||||
| 계획 핵심 회귀 테스트: deliveredBeforeSentNeverDowngrades | PASS | |
|
||||
| 계획 핵심 회귀 테스트: fcmInstallationAndLegacyTokenAreDifferentTypes | PASS | |
|
||||
| 계획 핵심 회귀 테스트: http200IsProviderAcceptedNotDelivered | PASS | |
|
||||
| 계획 핵심 회귀 테스트: ttlHeaderIsRequiredAndAcceptanceIsNotDelivery | PASS | |
|
||||
| 계획 핵심 회귀 테스트: websocketFailureDoesNotRollbackInboxItem | PASS | |
|
||||
| 계획 핵심 회귀 테스트: metricTagsNeverContainHighCardinalityIdentifiers | PASS | |
|
||||
| 계획 핵심 회귀 테스트: acceptedThenResponseLossIsAmbiguousForEveryApplicableAdapter | PASS | |
|
||||
| 계획 핵심 회귀 테스트: notificationPerformanceTest | PASS | |
|
||||
| 설계·계획 날짜 일치 | PASS | |
|
||||
| Java 21 가정 명시 | PASS | |
|
||||
| 실제 저장소 부재 가정 명시 | PASS | |
|
||||
| Provider SDK 공개 금지 | PASS | |
|
||||
| Core async CompletionStage | PASS | |
|
||||
| FCM FID 우선 | PASS | |
|
||||
| Ambiguous fallback 금지 | PASS | |
|
||||
| ProviderEvent 원장 | PASS | |
|
||||
| ZIP CRC 무결성 | PASS | None |
|
||||
| ZIP 필수 파일 | PASS | [] |
|
||||
|
||||
## 검증 범위
|
||||
|
||||
- 이 검증은 Markdown 설계서와 구현 계획서의 구조·정합성·필수 계약·경로 중복·미확정 표현·패키지 CRC를 검사한다.
|
||||
- 실제 Backend Skeleton 저장소가 입력되지 않았으므로 Gradle compile, Provider sandbox, PostgreSQL integration, chaos, performance test 실행 결과는 포함하지 않는다.
|
||||
- 구현 시에는 계획의 각 Task가 지정한 red-green TDD 명령을 실제 저장소에서 실행해야 한다.
|
||||
@@ -0,0 +1,48 @@
|
||||
> **보존 안내** — 이 파일은 저장소 루트 `redis-superpowers-package/` 에 있던 납품 패키지의 README다.
|
||||
> 2026-08-15 문서 정리에서 설계서·계획서는 `docs/superpowers/specs/` 와 `docs/superpowers/plans/` 로 통합했고,
|
||||
> 이 패키지에는 검증 스크립트가 없었다.
|
||||
> 아래 본문은 납품 시점의 기록이므로 제거된 파일을 가리키는 문장은 더 이상 유효하지 않다.
|
||||
|
||||
# Redis Wrapper 및 Typed API 설계 패키지
|
||||
|
||||
이 패키지는 Spring 기반 Backend Skeleton에서 Redis 자료구조와 명령을 폭넓게 제공하기 위한 설계서와 구현 계획서다.
|
||||
|
||||
## 문서
|
||||
|
||||
- `docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md`
|
||||
- 범위와 비지원 범위
|
||||
- Redis 버전·배포 모드
|
||||
- 모듈과 의존 규칙
|
||||
- 자료구조별 동기·Reactive Typed API
|
||||
- R1~R4 명령 노출 정책
|
||||
- permit·budget·Raw Gateway·Admin Plane
|
||||
- namespace·직렬화·TTL·timeout·retry·오류·관측성·ACL
|
||||
- Standalone·Sentinel·Cluster
|
||||
- 테스트·CI·완료 정의
|
||||
|
||||
- `docs/superpowers/plans/2026-08-07-redis-wrapper-typed-api-implementation-plan.md`
|
||||
- 27개 구현 작업
|
||||
- 작업별 생성·수정 파일
|
||||
- 작업 간 입력·출력 인터페이스
|
||||
- 실패 테스트, 실행 명령, 최소 구현, 통과 검증, 커밋
|
||||
- Redis 7.2·7.4·8.2·8.10 및 Sentinel·Cluster 테스트 작업
|
||||
|
||||
- `VALIDATION.md`
|
||||
- 문서 구조와 계획 완전성에 대한 정적 검증 결과
|
||||
|
||||
## 핵심 결정
|
||||
|
||||
1. classic Redis 자료구조는 최대한 Typed API로 제공한다.
|
||||
2. 고비용·Blocking·다중 키 명령은 permit와 `OperationBudget`을 요구한다.
|
||||
3. Typed API에 아직 없는 R1·R2 명령은 승인형 Raw Gateway로 제공한다.
|
||||
4. 운영 명령은 별도 Admin Plane, 파괴적 명령은 SDK 차단으로 분리한다.
|
||||
5. command catalog는 Redis 공식 metadata에서 생성하고 조직 정책을 오버레이한다.
|
||||
6. 동기와 Reactive API를 정식 지원하고 같은 내부 async primitive를 공유한다.
|
||||
7. 일반·Blocking·Transaction·Pub/Sub·Admin 연결을 격리한다.
|
||||
8. timeout 후 write는 자동 재시도하지 않고 실행 결과 불명을 표현한다.
|
||||
|
||||
## 적용 전제
|
||||
|
||||
현재 Backend Skeleton 저장소가 첨부되지 않아 경로와 Gradle 구조는 목표 구조로 확정했다. 실제 저장소에 적용할 때 기존 package naming, convention plugin, dependency management가 더 강한 기준을 이미 갖고 있다면 구조적 계약은 유지하면서 해당 규칙에 맞춘다.
|
||||
|
||||
입력 Markdown이 참조한 309행 Excel 워크북은 현재 작업 공간에 존재하지 않았다. 따라서 정확한 command matrix는 구현 과정에서 `COMMAND DOCS`, `COMMAND INFO`, `COMMAND GETKEYSANDFLAGS`를 읽어 재생성하고 정책 오버레이를 적용하도록 설계했다.
|
||||
@@ -0,0 +1,38 @@
|
||||
# 정적 검증 결과
|
||||
|
||||
- **결과:** PASS
|
||||
- **검사 수:** 29
|
||||
- **설계서 SHA-256:** `e742ea78f4f40c2f5ed65093a71d2c85c27da52b0761374030a5ca64143aea63`
|
||||
- **계획서 SHA-256:** `6592a37373a79bc2ccf9434fc3516d8273d9f9369e392d5e4f3360a46d6398c0`
|
||||
|
||||
| 검사 | 결과 | 세부 |
|
||||
|---|---|---|
|
||||
| 설계서 파일 존재 | PASS | docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md |
|
||||
| 설계서 코드 펜스 균형 | PASS | fences=70 |
|
||||
| 설계서 미확정 표식 없음 | PASS | none |
|
||||
| 계획서 파일 존재 | PASS | docs/superpowers/plans/2026-08-07-redis-wrapper-typed-api-implementation-plan.md |
|
||||
| 계획서 코드 펜스 균형 | PASS | fences=276 |
|
||||
| 계획서 미확정 표식 없음 | PASS | none |
|
||||
| 계획 작업 수 | PASS | 27 tasks |
|
||||
| 모든 작업 Step 1 보유 | PASS | 27/27 |
|
||||
| 모든 작업 Step 2 보유 | PASS | 27/27 |
|
||||
| 모든 작업 Step 3 보유 | PASS | 27/27 |
|
||||
| 모든 작업 Step 4 보유 | PASS | 27/27 |
|
||||
| 모든 작업 Step 5 보유 | PASS | 27/27 |
|
||||
| 모든 작업 **Files:** 보유 | PASS | 27/27 |
|
||||
| 모든 작업 **Interfaces:** 보유 | PASS | 27/27 |
|
||||
| 모든 작업 git commit -m 보유 | PASS | 27/27 |
|
||||
| Create 경로 중복 없음 | PASS | none |
|
||||
| Superpowers 계획 헤더 | PASS | required header present |
|
||||
| 설계 입력 제약 명시 | PASS | missing workbook handled explicitly |
|
||||
| Typed/Advanced/Raw/Admin 4단계 | PASS | four exposure tiers |
|
||||
| classic 자료구조 범위 | PASS | all classic groups present |
|
||||
| 동기·Reactive parity 계획 | PASS | API parity covered |
|
||||
| permit 위조 검증 | PASS | provenance verification covered |
|
||||
| 토폴로지 task 선행 등록 | PASS | test tasks available before contracts |
|
||||
| 환경 파일 생명주기 일관성 | PASS | create once, extend twice |
|
||||
| 잘못된 Persistent factory 없음 | PASS | constructor usage consistent |
|
||||
| 공유 async primitive | PASS | sync/reactive executor share invocation |
|
||||
| Raw 문자열 API 금지 | PASS | guardrail fixed |
|
||||
| R4 차단 | PASS | blocked in plan and design |
|
||||
| 완료 정의 존재 | PASS | definition and release task present |
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3943
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4560
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,281 @@
|
||||
# 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
|
||||
`AutoConfigurationImportFilter`s, 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`](../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](2026-08-15-wave0-red-baseline.md) | 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](2026-08-15-wave1-activation-ssot.md) | 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](2026-08-15-wave2-module-on-path.md) | 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](2026-08-15-wave3-environment-and-infra.md) | 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](2026-08-15-wave4-warning-zero.md) | 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](2026-08-15-wave5-gradle-build-logic.md) | `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](2026-08-15-wave6-final-qualification.md) | 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`, 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.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,612 @@
|
||||
# Wave 2 — Module On-Path Blockers 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 1 complete — all-off boots on `local`, `dev`, and `prod`, and every
|
||||
> `FiveAdapterOffInventoryTest` case is green.
|
||||
|
||||
**Goal:** Close every per-module blocker that stands between "the switch turns it on" and "the thing
|
||||
it turns on actually works against real infrastructure", for all five adapters.
|
||||
|
||||
**Architecture:** Wave 1 decided whether a bean exists; Wave 2 decides what it does. Each adapter
|
||||
gets one publication/settings/assembly authority to match the one activation authority it now has:
|
||||
JPA validates the *resolved* `DataSource` rather than a parallel property namespace; Mongo builds
|
||||
exactly one sync client from the active profile's credential; Messaging replaces the fake sender with
|
||||
a real transport bridge and gains its Stable facade membership only in the change that proves a live
|
||||
broker round trip; Notification gains production provider assemblers and a frozen-route ingest
|
||||
contract; GraphQL collapses its two contradictory safety axes into one deployment mode carried
|
||||
through the real request path.
|
||||
|
||||
**Tech Stack:** Testcontainers (PostgreSQL 16/17/18, MongoDB replica set, Kafka, RabbitMQ, Mailpit),
|
||||
Spring GraphQL, Spring Security OAuth2 resource server, Flyway, Micrometer.
|
||||
|
||||
**Spec:** [`2026-08-15-five-adapter-runtime-remediation-review-design.md`](../specs/2026-08-15-five-adapter-runtime-remediation-review-design.md)
|
||||
(§6 in full, §11 Wave 2, §12.2, §12.3)
|
||||
|
||||
---
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Inherited from the index. Wave 2 adds:
|
||||
|
||||
- **Membership is earned, never granted in advance.** A Stable facade gains `runtime_memberships`
|
||||
and an `app-bootstrap` dependency **in the same change unit that proves it works against real
|
||||
infrastructure**. Wiring first and qualifying later ships a known-broken path.
|
||||
- **No fake in production.** A fixture, in-memory implementation, or test double must not be
|
||||
reachable from a production runtime path. Where the production implementation is absent, the
|
||||
capability stays off and its promotion claim is removed from the registry.
|
||||
- **Each finding is re-reproduced at current HEAD before it is fixed.** The five detailed module
|
||||
reviews below remain authoritative inputs, but they were written against a different HEAD; do not
|
||||
copy a failure forward without reproducing it.
|
||||
- `docs/reviews/2026-08-14-mongodb-module-code-review.md`
|
||||
- `docs/reviews/2026-08-14-messaging-module-code-review.md`
|
||||
- `docs/reviews/2026-08-14-notification-module-code-review.md`
|
||||
- `docs/reviews/2026-08-14-jpa-module-code-review.md`
|
||||
- `docs/reviews/2026-08-14-graphql-module-code-review.md`
|
||||
- **A P0 correctness or security finding on a runtime path is a prerequisite, not a follow-up.**
|
||||
Connecting wiring over a known data-loss path is forbidden (spec §11 Wave 2 closing note).
|
||||
|
||||
---
|
||||
|
||||
## Section A — JPA
|
||||
|
||||
### Task A1: Validate the resolved DataSource, not a parallel namespace (JPA-INT-002)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaDataSourceSettings.java`
|
||||
- Modify: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaDataSourceProfileValidator.java`
|
||||
- Modify: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/PersistenceJpaRootAutoConfiguration.java`
|
||||
- Test: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaResolvedDataSourceValidationTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `JpaDataSourceProfileValidator.validateResolved(DataSource)` — replaces
|
||||
`validateStable()`. Task A2 and Wave 3's `local-jpa` lane both consume it.
|
||||
|
||||
**Context:** Two defects, both verified in the spec against HEAD.
|
||||
|
||||
1. `JpaDataSourceSettings` binds `app.jpa-platform.datasource.*` while the pool that actually gets
|
||||
built comes from `spring.datasource.hikari.*` (`application.yml:25-50`). Two namespaces describing
|
||||
one pool means the validator can pass while the pool it validated is not the pool in use.
|
||||
2. `JpaDataSourceProfileValidator` is created as a bean but `validateStable` is never invoked at
|
||||
startup (`JpaPlatformRuntimeAutoConfiguration.java:93-99,159-165`). A validator nobody calls is a
|
||||
comment.
|
||||
|
||||
The fix removes the duplicate namespace entirely and validates the injected `DataSource` —
|
||||
`HikariDataSource#getMaximumPoolSize`, `getConnectionTimeout`, the resolved JDBC URL, and the product
|
||||
name and version read from `DatabaseMetaData`. Invocation moves into an `InitializingBean` inside the
|
||||
JPA root, so it runs exactly when JPA is on and never when it is off.
|
||||
|
||||
- [ ] **Step 1:** Write `JpaResolvedDataSourceValidationTest` — a Testcontainers PostgreSQL context
|
||||
asserting that (a) a pool whose `maximum-pool-size` violates the REQUIRES_NEW lower bound
|
||||
documented at `application.yml:33-37` fails startup naming `spring.datasource.hikari.maximum-pool-size`;
|
||||
(b) removing every `app.jpa-platform.datasource.*` property changes nothing, proving the
|
||||
namespace is dead; (c) an unreachable database fails startup rather than at first query.
|
||||
- [ ] **Step 2:** Run to verify it fails.
|
||||
- [ ] **Step 3:** Delete the `app.jpa-platform.datasource` binding from `JpaDataSourceSettings`;
|
||||
rewrite `JpaDataSourceProfileValidator` to take a `DataSource`; register the invocation in
|
||||
`PersistenceJpaRootAutoConfiguration`.
|
||||
- [ ] **Step 4:** Run to verify it passes.
|
||||
- [ ] **Step 5:** Run `./gradlew :adapter:outbound:persistence-jpa:test :app-bootstrap:test --console=plain --no-daemon`.
|
||||
- [ ] **Step 6:** Remove the now-dead `app.jpa-platform.datasource.*` rows from
|
||||
`docs/registries/env-keys.yaml`; run `./gradlew verifyEnvKeys`.
|
||||
- [ ] **Step 7:** Commit.
|
||||
|
||||
### Task A2: Separate local H2 from the default-off contract (JPA-INT-003)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app-bootstrap/src/main/resources/application-local.yml`
|
||||
- Create: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/autoconfigure/jpa/LocalJpaVendorParityTest.java`
|
||||
- Modify: `src/app-bootstrap/build.gradle` (a named non-release H2 developer task)
|
||||
|
||||
**Context:** H2 becomes repository/unit-test scope plus one explicitly-named developer convenience
|
||||
task. The user-facing completion criterion — "local JPA runtime smoke" — uses PostgreSQL, Flyway, and
|
||||
`ddl-auto=validate`, so local and dev share vendor semantics. Consequences to implement:
|
||||
|
||||
- `local` all-off boots with **no database at all** (Wave 1 already delivers this).
|
||||
- `local` + `APP_PERSISTENCE_JPA_ENABLED=true` requires the Compose PostgreSQL of Wave 3's
|
||||
`local-jpa` lane.
|
||||
- Profile absence must never resolve to H2/`create-drop`; Wave 3 Task 1 makes profile absence a
|
||||
startup error, and this task removes the H2 default that made absence dangerous.
|
||||
|
||||
- [ ] **Steps 1–6:** TDD cycle. The parity test asserts that `local` + JPA-on resolves the same
|
||||
vendor, migration mode, and schema policy as `dev` + JPA-on, differing only in address and
|
||||
credential.
|
||||
|
||||
---
|
||||
|
||||
## Section B — MongoDB
|
||||
|
||||
### Task B1: Move to the canonical Boot 4 namespace (MNG-INT-002, part 1)
|
||||
|
||||
**Files:**
|
||||
- Modify: every source and test referencing `spring.data.mongodb.*`
|
||||
- Test: `src/adapter/outbound/persistence-mongo/src/test/java/dev/caskeleton/adapter/outbound/mongo/MongoNamespaceContractTest.java`
|
||||
|
||||
**Context:** `spring.data.mongodb.*` is deprecated at error level in Spring Boot 4 metadata; the
|
||||
canonical namespace is `spring.mongodb.*`. `MongoPersistenceProperties`' own Javadoc points operators
|
||||
at the deprecated one. Building the new activation design on a namespace Boot reports as an error is
|
||||
building on sand.
|
||||
|
||||
- [ ] **Step 1:** Write a test asserting no production source or resource references
|
||||
`spring.data.mongodb.` — a source-tree scan, in the shape of the existing
|
||||
`SecretLeakStaticScanTest`.
|
||||
- [ ] **Steps 2–6:** migrate, run `./gradlew :adapter:outbound:persistence-mongo:test`, update the
|
||||
Javadoc, commit.
|
||||
|
||||
### Task B2: One SSOT from active profile to the real `MongoClientSettings` (MNG-INT-002, part 2)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/client/MongoClientSettingsFactory.java`
|
||||
- Modify: `MongoRootAutoConfiguration` to register it as a `MongoClientSettingsBuilderCustomizer`
|
||||
- Test: `.../client/MongoClientSettingsFactoryTest.java`
|
||||
|
||||
**Context:** Typed profile, credential resolver, TLS, Stable API, and pool/timeout policy exist but
|
||||
are not connected to the builder Boot actually uses. One factory consumes the active profile and the
|
||||
secret reference and produces the real settings; the test asserts on the built
|
||||
`MongoClientSettings`, not on the intermediate typed objects.
|
||||
|
||||
Cardinality contract, from index §Scope boundaries: exactly one sync client and one pool; zero
|
||||
reactive inventory; secrets resolved **only** for the active profile — a profile present in the map
|
||||
but not selected must not have its secret read or its client built.
|
||||
|
||||
- [ ] **Steps 1–6:** TDD cycle asserting: one client, one pool, zero reactive beans, and that a
|
||||
non-active profile's deliberately-invalid secret reference is never resolved.
|
||||
|
||||
### Task B3: Startup validation that cannot fail open (MNG-INT-003)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/autoconfigure/MongoPlatformAutoConfiguration.java`
|
||||
- Test: `.../autoconfigure/MongoStartupValidationTest.java`
|
||||
|
||||
**Context:** The startup check is created only when a `MongoTopologyProbe` bean is present
|
||||
(`MongoPlatformAutoConfiguration.java:137-183`), so a missing probe silently skips validation
|
||||
entirely. Additionally, transactions and change streams are hardcoded `true, true` in the capability
|
||||
flags, and absent admin credentials are treated as "no validation needed".
|
||||
|
||||
The fix, per index §Scope boundaries:
|
||||
- Mongo on ⇒ a probe is built from the live data-plane client, or the absence is a startup error.
|
||||
- Capability flags come from typed settings, never literals.
|
||||
- Admin credential/gateway belongs to a migration or deployment job's composition; the shipped
|
||||
application neither binds nor requires it.
|
||||
- `transactions` is a subordinate switch defaulting `false`; when on, the real replica-set capability
|
||||
of the data-plane credential is verified.
|
||||
- `change-streams` is experimental, always `false`, zero beans and zero threads. A replica-set
|
||||
qualification observing server capability is not evidence of shipped support.
|
||||
|
||||
- [ ] **Steps 1–6:** TDD cycle with one case per bullet.
|
||||
|
||||
### Task B4: Separate role from credential identity (MNG-INT-004)
|
||||
|
||||
**Files:**
|
||||
- Modify: the credential identity hash implementation
|
||||
- Test: `.../MongoCredentialIdentityTest.java`
|
||||
|
||||
**Context:** The identity hash includes the role, so the same secret reference used under two roles
|
||||
looks like two different credentials, defeating the separation it was meant to enforce.
|
||||
|
||||
- [ ] **Steps 1–5:** TDD cycle; the regression case is one secret reference under two roles, which
|
||||
must be detected as the same credential.
|
||||
|
||||
### Task B5: Resolve the three ghost release lanes
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/config/mongodb/release-contracts.json` **or** `src/adapter/outbound/persistence-mongo/build.gradle`
|
||||
- Modify: `docs/mongodb/advanced/sharding.md`, `scripts/verify-mongodb-advanced.sh`
|
||||
- Test: `ReleaseManifestTaskExistenceTest` (Wave 0 Task 9) is the arbiter
|
||||
|
||||
**Context:** Verified at HEAD: `release-contracts.json` names `mongoShardedTest` (line 30),
|
||||
`mongoAtlasTest` (38), and `mongoKmsTest` (46); `persistence-mongo/build.gradle` registers seven mongo
|
||||
lanes and none of those three. `scripts/verify-mongodb-advanced.sh:95` invokes `mongoShardedTest`, so
|
||||
that script currently cannot succeed either.
|
||||
|
||||
Two legitimate outcomes — choose one and record the decision in the plan's evidence log:
|
||||
|
||||
- **(a) Implement.** Register the three lanes with real required classes and protected-environment
|
||||
evidence, and include them in the Stable blocking set.
|
||||
- **(b) Demote.** Remove the Stable blocking claim from `release-contracts.json`, move the three to
|
||||
an explicit experimental/conditional promotion section, update `docs/mongodb/advanced/sharding.md`
|
||||
to stop describing an unrunnable gate, and make `verify-mongodb-advanced.sh` fail with a clear
|
||||
"not promoted" message rather than invoking a task that does not exist.
|
||||
|
||||
**Recommendation: (b).** Sharding, Atlas, and KMS each need a protected environment this repository
|
||||
does not have, and index §Scope boundaries already places them outside the shipped Stable runtime.
|
||||
Implementing them to satisfy a manifest entry would be the tail wagging the dog.
|
||||
|
||||
- [ ] **Steps 1–5:** apply the chosen outcome, run
|
||||
`./gradlew :app-bootstrap:test --tests '*ReleaseManifestTaskExistenceTest*'` to green, remove
|
||||
its `@Tag("wave0-red")`, commit.
|
||||
|
||||
---
|
||||
|
||||
## Section C — Messaging
|
||||
|
||||
### Task C1: Fix the secret scanner without weakening it (MSG-INT-005)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/messaging/messaging-observability/src/main/java/dev/caskeleton/messaging/observation/SecretConcatenationClassifier.java`
|
||||
- Modify: `src/messaging/messaging-observability/src/test/java/dev/caskeleton/messaging/observation/SecretLeakStaticScanTest.java`
|
||||
- Modify: `src/messaging/messaging-observability/src/test/java/dev/caskeleton/messaging/observation/SecretLeakScannerCharacterizationTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `SecretConcatenationClassifier.leaksASensitiveValue(String line)` — extracted from the
|
||||
test so the characterization can call the real thing.
|
||||
|
||||
**Context:** Root cause, confirmed by reading the scanner (index §The one reproduced red test):
|
||||
`CONCATENATION_OPERAND` captures a method call including its trailing `()`, while
|
||||
`DESCRIBES_RATHER_THAN_REVEALS` anchors with `$`. The safe-suffix exemption is therefore dead for
|
||||
every method call. Second offender: `existing.leaseToken() + 1` is arithmetic.
|
||||
|
||||
The two fixes:
|
||||
1. Strip a trailing `()` from `tail` before matching the safe-suffix pattern. Not: loosen the anchor —
|
||||
an unanchored `Id` would exempt `credentialIdentity`, which does carry the value.
|
||||
2. Treat an operand paired with a numeric literal as arithmetic. Detect it by inspecting the *other*
|
||||
side of the `+`: a decimal, hex, or floating literal makes the expression arithmetic.
|
||||
|
||||
Neither fix may weaken true-positive detection, which is what the characterization's first three
|
||||
cases exist to prove.
|
||||
|
||||
- [ ] **Step 1:** Extract the classifier to `src/main/java` unchanged, and point both tests at it.
|
||||
Run — the same 2 failures, now against the real class.
|
||||
- [ ] **Step 2:** Apply fix 1. Run — `methodCallWithSafeSuffixIsNotALeak` green, true positives still
|
||||
green.
|
||||
- [ ] **Step 3:** Apply fix 2. Run — `numericFencingIsNotALeak` green.
|
||||
- [ ] **Step 4:** Delete the duplicated classifier from
|
||||
`SecretLeakScannerCharacterizationTest` and have it call the extracted one, per that file's own
|
||||
Javadoc promise.
|
||||
- [ ] **Step 5:** Run `./gradlew :messaging:messaging-observability:test --console=plain --no-daemon`
|
||||
— the full module green, including `SecretLeakStaticScanTest`.
|
||||
- [ ] **Step 6:** Run `./gradlew test --console=plain --no-daemon --continue` — the repository-wide
|
||||
suite, which spec §3.1 recorded as failing on exactly this test. Record the result.
|
||||
- [ ] **Step 7:** Commit.
|
||||
|
||||
### Task C2: One publication authority and one settings owner (MSG-INT-002)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingSettings.java` (delete after migration)
|
||||
- Modify: `src/messaging/messaging-spring-boot-starter/src/main/java/dev/caskeleton/messaging/autoconfigure/MessagingProperties.java`
|
||||
- Modify: `src/config/architecture/modules.json`
|
||||
- Test: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/messaging/MessagingAuthorityContractTest.java`
|
||||
|
||||
**Context:** Three authorities share the `app.messaging` namespace today: `MessagingSettings`
|
||||
(adapter), the legacy `KafkaSender` config, and `MessagingProperties` (starter). The target:
|
||||
|
||||
```text
|
||||
app-bootstrap
|
||||
-> adapter:outbound:messaging # application port bridge
|
||||
-> messaging runtime starter # Stable runtime assembly
|
||||
adapter:outbound:messaging
|
||||
-> messaging-core-api # platform publish contract only
|
||||
messaging runtime starter
|
||||
-> selected platform implementation leaves
|
||||
```
|
||||
|
||||
Registry edges to add: `app-bootstrap -> messaging-spring-boot-starter` and
|
||||
`adapter-outbound-messaging -> messaging-core-api`.
|
||||
|
||||
Port discipline: the real application-owned contract is `OutboxMessagePublishPort`. The legacy generic
|
||||
`MessagePublisher` is an adapter-local type and must not leak into core. If general publish becomes a
|
||||
use-case need, define an application port first.
|
||||
|
||||
Forbidden: legacy and new publisher both emitting the same event (dual write).
|
||||
|
||||
- [x] **Characterization and the dual-write guard:** done. `MessagingAuthorityContractTest` records
|
||||
the two owners of `app.messaging` by name and asserts that exactly one production type
|
||||
implements `OutboxMessagePublishPort`, plus that the adapter-local `MessagePublisher` does not
|
||||
reach `application-core` or `domain-core`. A source scan, because no module sees both the
|
||||
adapter and the starter — which is the boundary working, not a gap in the test.
|
||||
- [ ] **The settings collapse moves into C3's change unit.** Deleting `MessagingSettings` means the
|
||||
adapter stops selecting a broker and becomes a port bridge over the platform's publish
|
||||
contract; it can only do that once the platform *has* a production publisher. Removing the
|
||||
binding first would leave the adapter unable to select anything, which is a worse state than
|
||||
the split it fixes. The guard above is what keeps the split honest until then, and it fails the
|
||||
moment a third owner appears or a second publisher starts emitting.
|
||||
|
||||
### Task C3: A real production sender, and membership earned by a live round trip (MSG-INT-003)
|
||||
|
||||
**Files:**
|
||||
- Create: the production Kafka and RabbitMQ sender implementations in the selected platform leaves
|
||||
- Modify: `src/config/architecture/modules.json`, `src/app-bootstrap/build.gradle`
|
||||
- Test: `src/app-bootstrap/src/test/java/.../MessagingLiveRoundTripQualificationTest.java` (Testcontainers Kafka)
|
||||
|
||||
**Context:** The legacy Kafka config requires a project-supplied `KafkaSender` that exists only as a
|
||||
test fake. A production app therefore has no sender at all, and every "messaging works" signal comes
|
||||
from a fixture.
|
||||
|
||||
**Membership rule, enforced here:** the starter and every internal leaf that actually resolves onto
|
||||
the runtime classpath gain `app-bootstrap` membership **in the same change unit** that turns
|
||||
`MessagingLiveRoundTripQualificationTest` green against a real broker. Leaves that are unsupported or
|
||||
unqualified are excluded from both the starter's dependencies and the registry.
|
||||
|
||||
Because Wave 1 Task 13 made the membership gate closure-based, adding the starter will surface every
|
||||
transitive leaf at once — that is intended, and each must be either recorded as a member or removed
|
||||
from the starter's dependency graph.
|
||||
|
||||
- [ ] **Steps 1–8:** TDD cycle ending with `./gradlew verifyRuntimeModuleMembership` green and
|
||||
`ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped` green with its tag removed.
|
||||
|
||||
### Task C4: One master-gated starter root (MSG-INT-004)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/messaging/messaging-spring-boot-starter/src/main/java/dev/caskeleton/messaging/autoconfigure/MessagingPlatformRootAutoConfiguration.java`
|
||||
- Modify: `src/messaging/messaging-spring-boot-starter/src/main/resources/META-INF/spring/...AutoConfiguration.imports`
|
||||
- Test: `.../MessagingStarterOffContractTest.java`
|
||||
|
||||
**Context:** Verified at HEAD, the starter registers five independent auto-configurations
|
||||
(`MessagingCoreAutoConfiguration`, `KafkaMessagingAutoConfiguration`,
|
||||
`RabbitMessagingAutoConfiguration`, `MessagingReliabilityAutoConfiguration`,
|
||||
`MessagingAdminAutoConfiguration`) and none carries a messaging master condition.
|
||||
|
||||
After this task `imports` holds exactly one entry — the root — which imports the selected provider and
|
||||
reliability children. Kafka and Rabbit must never assemble together merely because both client
|
||||
libraries are on the classpath; provider selection is a closed descriptor + registry, and an unknown
|
||||
or duplicate selection is a startup error.
|
||||
|
||||
The off test is full-context and includes starter imports **and** vendor Boot auto-configuration:
|
||||
zero beans, zero clients, zero threads.
|
||||
|
||||
- [ ] **Steps 1–7:** TDD cycle.
|
||||
|
||||
---
|
||||
|
||||
## Section D — Notification
|
||||
|
||||
### Task D1: Fix the mode SSOT drift (NTF-INT-002)
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/registries/env-keys.yaml:4266-4277`
|
||||
- Modify: every YAML, doc, and test using `ACCEPT_ONLY`
|
||||
- Test: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/notification/NotificationModeSsotTest.java`
|
||||
|
||||
**Context:** The Java enum is `SERVING|INGEST_ONLY`
|
||||
(`NotificationPlatformMode.java:11-23`); the env registry declares `SERVING|ACCEPT_ONLY`. Canonical
|
||||
name is `INGEST_ONLY`. The test asserts that the registry's enum values equal
|
||||
`NotificationPlatformMode.values()` — derived, so it cannot drift again.
|
||||
|
||||
- [ ] **Steps 1–5:** TDD cycle; smallest task in this wave, do it first so later tasks use one name.
|
||||
|
||||
### Task D2: Production provider assemblers (NTF-INT-001)
|
||||
|
||||
**Files:**
|
||||
- Create: production `ProviderRuntimeAssembler` implementations under
|
||||
`src/adapter/outbound/notification/src/main/java/.../platform/provider/`
|
||||
- Modify: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformProviderConfig.java`
|
||||
- Test: `.../NotificationServingAssemblyTest.java`
|
||||
|
||||
**Context:** Configuration assembles `List<ProviderRuntimeAssembler>` but production main source has
|
||||
no implementation (`NotificationPlatformProviderConfig.java:78-99`). `SERVING` therefore cannot work
|
||||
in production regardless of settings.
|
||||
|
||||
`SERVING` turns on only when the selected provider family has a real assembler, secret resolver,
|
||||
timeout/rate/permit policy, and readiness contributor. A provider without one is not documented as
|
||||
Stable — update the capability docs in the same change.
|
||||
|
||||
Reference provider for the Wave 3 `local-notification-serving` lane: SMTP via Mailpit.
|
||||
|
||||
- [ ] **Steps 1–7:** TDD cycle.
|
||||
|
||||
### Task D3: Mode-scoped worker lifecycle (NTF-INT-003)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java`
|
||||
- Test: `.../NotificationWorkerLifecycleTest.java`
|
||||
|
||||
**Context:** The worker config checks only master-enabled and unconditionally `start()`s background
|
||||
workers and the scheduler (`NotificationPlatformWorkerConfig.java:135-166`). `INGEST_ONLY` must start
|
||||
zero dispatch, recovery, and reconciliation threads.
|
||||
|
||||
Asserted on **live thread count**, via `AdapterActivationInventory.liveThreadNamesMatching`, not on
|
||||
bean absence — a worker bean that exists but was never started is acceptable; a thread is not.
|
||||
|
||||
- [ ] **Steps 1–6:** TDD cycle.
|
||||
|
||||
### Task D4: Retire the legacy selector namespace (NTF-INT-004)
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/registries/env-keys.yaml`, the notification YAML, and the optional-bean tests
|
||||
- Test: `.../NotificationLegacyNamespaceRetirementTest.java`
|
||||
|
||||
**Context:** The registry and YAML use provider selectors while the actual bean conditions mix
|
||||
`slack-webhook.enabled`, `google-email.enabled`, and `routes.*`. The delivery platform becomes the
|
||||
canonical runtime; the legacy R0 selector is isolated behind a migration shim that raises a naming
|
||||
migration error, then removed.
|
||||
|
||||
- [ ] **Steps 1–6:** TDD cycle; the negative assertion — that no legacy selector is documented as an
|
||||
activation key — is required by spec §6.4 and belongs in
|
||||
`MasterSwitchRegistryContractTest` (Wave 1 Task 12).
|
||||
|
||||
### Task D5: A frozen, non-empty route plan for INGEST_ONLY (NTF-INT-006)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/notification/src/main/java/.../CanonicalNotificationPlanWriter.java`
|
||||
- Modify: `.../PolicyRoutePlanner.java`
|
||||
- Create: versioned route-metadata loading in the notification root
|
||||
- Test: `src/app-bootstrap/src/test/java/.../NotificationIngestHandoffQualificationTest.java`
|
||||
|
||||
**Context:** The most consequential blocker in this section, and the one most easily mistaken for
|
||||
working. Today the plan writer freezes a provider-specific routing plan at accept time and stores an
|
||||
**empty** plan when the route catalog is empty (`CanonicalNotificationPlanWriter.java:82-128`,
|
||||
`PolicyRoutePlanner.java:50-76`). Dispatch consumes the stored snapshot verbatim
|
||||
(`NotificationDispatchService.java:298-307`). So a row accepted in `INGEST_ONLY` with no routes never
|
||||
becomes deliverable, no matter how the application is later restarted.
|
||||
|
||||
The contract:
|
||||
- `INGEST_ONLY` startup **requires** versioned route metadata — provider family/id, channel
|
||||
eligibility, route config version — readable **without** credentials or live provider beans.
|
||||
- Accept freezes a non-empty immutable plan plus its route version.
|
||||
- An empty route catalog or empty plan is rejected at startup or at the accept boundary, explicitly.
|
||||
- No automatic replan after accept. A policy change needing replan is a separate backfill/migration
|
||||
with operator approval, idempotency, and audit.
|
||||
- `SERVING` startup verifies the production assembler registry supports every stored provider
|
||||
ID/version; a mismatched row is never silently reinterpreted.
|
||||
- During `INGEST_ONLY`: zero provider credentials, zero provider runtime beans, zero workers.
|
||||
|
||||
Qualification, which is also Wave 3's `local-notification-handoff` lane:
|
||||
`INGEST_ONLY accept → process stop → SERVING restart → exactly one delivery on the same frozen
|
||||
route`, against a real database and provider fixture.
|
||||
|
||||
- [ ] **Steps 1–9:** TDD cycle, ending with the handoff qualification green against Testcontainers
|
||||
PostgreSQL + Mailpit.
|
||||
|
||||
### Task D6: Decide the at-rest payload sensitivity contract (NTF-INT-007)
|
||||
|
||||
**Files:**
|
||||
- Either: create the encryption codec/port, ciphertext envelope, key ID, rotation/history, row
|
||||
migration, and decryption-failure contract
|
||||
- Or: create `docs/notification/at-rest-threat-model.md` plus a static variable-type restriction
|
||||
- Test: `.../NotificationPayloadAtRestContractTest.java`
|
||||
|
||||
**Context:** The accept path stores `encoded.variablesPayload()` into the request row in plaintext
|
||||
(`CanonicalNotificationPlanWriter.java:60-79`). The `PAYLOAD_ENCRYPTION` key is consumed only by
|
||||
callback raw-payload protection (`AesGcmCallbackPayloadProtection.java:88-97`), so requiring it in
|
||||
`INGEST_ONLY` would demand a secret that protects nothing — do not paper over the gap that way.
|
||||
|
||||
Per index §Scope boundaries, Notification is **not promoted to Stable** until one of the two branches
|
||||
is complete. Neither branch is optional; pick one, implement it fully, and record the decision.
|
||||
|
||||
**Recommendation: the threat-model branch**, if and only if the variable types can genuinely be
|
||||
restricted to non-sensitive values. Application-level encryption without rotation and migration
|
||||
designed in is a larger commitment than this wave can honour, and a half-built envelope is worse than
|
||||
a documented restriction.
|
||||
|
||||
- [ ] **Steps 1–6:** implement the chosen branch fully; a partial implementation of either is a fail.
|
||||
|
||||
---
|
||||
|
||||
## Section E — GraphQL
|
||||
|
||||
### Task E1: Collapse the two safety axes into one deployment mode (GQL-INT-002)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlPlatformProperties.java`
|
||||
- Modify: `.../GraphQlPlatformStartupValidator.java`, `.../GraphQlPlatformAutoConfiguration.java`
|
||||
- Test: `.../GraphQlDeploymentModeContractTest.java`
|
||||
|
||||
**Context:** `GraphQlPlatformProperties` defaults to `production=false` **and**
|
||||
`environment=PRODUCTION_PUBLIC` simultaneously (`GraphQlPlatformProperties.java:32-54`). This is not a
|
||||
display inconsistency: anonymous principal handling, allow-by-default authorization, and part of
|
||||
request protection read only the boolean (`GraphQlPlatformStartupValidator.java:33-45`,
|
||||
`GraphQlPlatformAutoConfiguration.java:302-343,441-459`), so the enum can say production while the
|
||||
protections behave as if it is not — a configuration split-brain that bypasses production safety.
|
||||
|
||||
Both collapse into `backend.graphql.deployment-mode`, with the environment constraint from index
|
||||
§Global Constraints. Using either legacy key while GraphQL is on, alone or alongside the new one, is a
|
||||
migration error naming `APP_GRAPHQL_DEPLOYMENT_MODE`. With the master off, the detail namespace is
|
||||
neither bound nor validated.
|
||||
|
||||
- [x] **Steps 1–7:** done. `GraphQlPlatformEnvironment` → `GraphQlDeploymentMode`; `production` and
|
||||
`environment` are gone from the record and `production()` is derived from the mode;
|
||||
`GraphQlActivationEnvironmentPostProcessor` refuses both retired keys;
|
||||
`GraphQlDeploymentModeContractTest` (22 cases) and
|
||||
`GraphQlDeploymentModeRegistryParityTest` are green, and `local-graphql` passes end to end.
|
||||
|
||||
**Three amendments made while implementing, all recorded in
|
||||
[`evidence/2026-08-15-wave2-decisions.md`](evidence/2026-08-15-wave2-decisions.md):**
|
||||
|
||||
1. **Four modes, not six.** `TEST` and `STAGING` were unreachable from every runtime profile the
|
||||
composition-root validator permits, so they are gone. The registry parity test now derives its
|
||||
cases from the enum.
|
||||
2. **Boot's introspection default contradicted the platform's** — with the switch on and nothing else
|
||||
set, `spring.graphql.schema.introspection.enabled=true` against a platform console default of
|
||||
`false`, and the runtime validator correctly refused a contradiction nobody had configured. The
|
||||
same post-processor now contributes the platform's console values as the framework's defaults at
|
||||
the lowest precedence.
|
||||
3. **The Keycloak realm artifact could never have imported.** Keycloak rejects unknown fields, so the
|
||||
`_comment` and `_flowComment` annotation keys failed the whole import. Removed, rationale moved to
|
||||
`infra/keycloak/README.md`, and `verify-compose-profile-contracts.sh` now fails on any `_`-prefixed
|
||||
key in that artifact. The smoke client also could not read the 0600 secret (uid mismatch) and now
|
||||
runs as root in-container.
|
||||
|
||||
### Task E2: Prove the policy pipeline on the real request path (GQL-INT-003)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/inbound/graphql/src/test/java/.../GraphQlPolicyRequestPathTest.java`
|
||||
|
||||
**Context:** Auto-configuration and a startup validator existing is not evidence that cost, authz,
|
||||
cursor, and idempotency policies apply. The test uses a random-port `/graphql` and asserts a policy
|
||||
violation is rejected **before** the resolver or use case is invoked — verified with a spy on the use
|
||||
case that must record zero invocations — and that JWT actor/tenant context reaches the resolver.
|
||||
|
||||
- [ ] **Steps 1–6:** TDD cycle, one case per policy.
|
||||
|
||||
### Task E3: One blocking JWT composition lane (GQL-INT-004)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app-bootstrap/src/graphqlRuntimeQualificationTest/java/dev/caskeleton/bootstrap/graphql/GraphQlJwtRuntimeQualificationTest.java`
|
||||
- Modify: `src/app-bootstrap/build.gradle` (register `graphqlRuntimeQualification`)
|
||||
- Modify: `src/gradle/graphql-platform-conventions.gradle`
|
||||
- Modify: `src/build.gradle` (`conditionalTransportQualification`)
|
||||
- Modify: `.github/workflows/ci-quality-gates.yml`
|
||||
|
||||
**Context:** Three separate defects, all verified:
|
||||
|
||||
1. `ConditionalTransportCompositionContractTest` asserts only that GraphQL classes exist
|
||||
(`:15-46`) — class existence is not composition evidence.
|
||||
2. `GraphqlHttpBoundaryQualificationTest` authenticates with test-only Basic Auth
|
||||
(`:40-59,189-229`) — not the shipped JWT composition.
|
||||
3. CI runs `check verifyPublicPathSnapshot verifyDependencyLocks` and
|
||||
`conditionalTransportQualification`, and **never** `graphqlStableTest`
|
||||
(`.github/workflows/ci-quality-gates.yml:48-53`), so that lane's required-class guard protects
|
||||
nothing in CI.
|
||||
|
||||
The canonical task is `:app-bootstrap:graphqlRuntimeQualification`. It:
|
||||
- depends on `bootJar`;
|
||||
- forces required class
|
||||
`dev.caskeleton.bootstrap.graphql.GraphQlJwtRuntimeQualificationTest` in the
|
||||
`graphqlRuntimeQualificationTest` source set;
|
||||
- runs the produced jar as a **child process**;
|
||||
- obtains a client-credentials token from a Keycloak container importing the same tracked realm
|
||||
artifact Wave 3 Task 4 creates;
|
||||
- calls real HTTP `/graphql`;
|
||||
- writes JUnit XML to `app-bootstrap/build/test-results/graphqlRuntimeQualification` and sanitized
|
||||
process/claim/startup logs to `app-bootstrap/build/evidence/graphql-runtime/`;
|
||||
- rejects zero-discovery, any skip, and stale XML.
|
||||
|
||||
Then: replace the GraphQL Basic Auth leg of root `conditionalTransportQualification` with this task,
|
||||
leaving the gRPC and WebSocket legs untouched; keep `GraphqlHttpBoundaryQualificationTest` as a module
|
||||
contract test but stop aggregating it into release evidence; and change the CI quality job to run
|
||||
`:adapter:inbound:graphql:graphqlStableTest :app-bootstrap:graphqlRuntimeQualification
|
||||
conditionalTransportQualification` with a single dependency edge so the GraphQL task cannot execute
|
||||
twice.
|
||||
|
||||
**Ordering note:** this task depends on Wave 3 Task 4 (the Keycloak realm artifact). Either run Wave 3
|
||||
Task 4 early, or defer E3 to immediately after it. Record which you chose.
|
||||
|
||||
- [ ] **Steps 1–9:** TDD cycle ending with the lane green and the CI workflow updated.
|
||||
|
||||
---
|
||||
|
||||
## Wave 2 Exit Criteria
|
||||
|
||||
- [ ] `./gradlew test --console=plain --no-daemon` — the full ordinary suite green (the single
|
||||
pre-existing failure closed by Task C1).
|
||||
- [ ] `./gradlew wave0RedReport --console=plain --no-daemon` — only Wave 3 and Wave 4 entries remain.
|
||||
- [ ] Each adapter's one-on lane passes against real infrastructure:
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate -Pjpa.matrix.versions=16 --console=plain
|
||||
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate -Pjpa.matrix.versions=17 --console=plain
|
||||
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate -Pjpa.matrix.versions=18 --console=plain
|
||||
./gradlew :adapter:outbound:persistence-mongo:mongoStableContractTest \
|
||||
:adapter:outbound:persistence-mongo:mongoReplicaSetTest \
|
||||
:adapter:outbound:persistence-mongo:mongoFailoverTest \
|
||||
:adapter:outbound:persistence-mongo:mongoMigrationTest \
|
||||
:adapter:outbound:persistence-mongo:mongoCompatibilityTest \
|
||||
:adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest \
|
||||
:adapter:outbound:persistence-mongo:mongoPerformanceTest --console=plain
|
||||
./gradlew :adapter:inbound:graphql:graphqlStableTest \
|
||||
:app-bootstrap:graphqlRuntimeQualification conditionalTransportQualification --console=plain
|
||||
```
|
||||
- [ ] `./gradlew verifyRuntimeModuleMembership verifyCleanArchitectureDependencies verifyEnvKeys --console=plain` — green.
|
||||
- [ ] Every Section D and Section B decision (B5, D6) is recorded with its rationale in
|
||||
`docs/superpowers/plans/evidence/2026-08-15-wave2-decisions.md`.
|
||||
|
||||
## What Wave 2 explicitly does not do
|
||||
|
||||
- No `SPRING_PROFILES_ACTIVE` change, `.env` split, Compose file, Keycloak realm, or MinIO fixture
|
||||
(Wave 3) — except that Wave 2 Task E3 **consumes** Wave 3 Task 4's realm artifact.
|
||||
- No warning removal (Wave 4).
|
||||
- No build-logic extraction (Wave 5).
|
||||
- No promotion of Mongo reactive, change streams, sharding, Atlas, or KMS.
|
||||
- No promotion of Notification to Stable until D6 is complete.
|
||||
@@ -0,0 +1,444 @@
|
||||
# Wave 3 — Environment Separation and Infrastructure Smoke 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 2 complete — full `test` green, each adapter's one-on lane passing.
|
||||
> **Exception:** Task 4 (Keycloak realm) is a prerequisite of Wave 2 Task E3 and may be pulled
|
||||
> forward. If it was, mark it complete here and continue.
|
||||
|
||||
**Goal:** Separate example configuration from operator input, make a profileless deployment
|
||||
impossible, and build the Compose lane matrix — with its two canonical scripts, Keycloak realm, and
|
||||
MinIO round trip — that proves each activation combination actually runs.
|
||||
|
||||
**Architecture:** One JSON contract file (`config/runtime/compose-profile-contracts.json`) is the SSOT
|
||||
for every lane: its Compose profile, its file stack, its explicit Spring runtime, and its exact sorted
|
||||
service set. Two scripts are the only entry points — one static
|
||||
(`verify-compose-profile-contracts.sh`), one dynamic (`run-compose-runtime-smoke.sh`) — and CI calls
|
||||
those scripts rather than inlining fragments of them, so a lane cannot be half-run by a workflow that
|
||||
forgot a flag. Shared infrastructure lives in its own `docker-compose.infra.yml`, never mixed into an
|
||||
environment overlay. Each lane gets a unique Compose project, mode-0700 temp directory, mode-0600
|
||||
secret files, sanitized evidence, and a `trap`-driven teardown scoped to that project alone.
|
||||
|
||||
**Tech Stack:** Docker Compose ≥ 2.24.4 (this machine: 5.4.0), PostgreSQL 16 with TLS, MongoDB replica
|
||||
set, Kafka, Mailpit, MinIO + `mc`, Keycloak with `--import-realm`, Bash.
|
||||
|
||||
**Spec:** [`2026-08-15-five-adapter-runtime-remediation-review-design.md`](../specs/2026-08-15-five-adapter-runtime-remediation-review-design.md)
|
||||
(§7 in full, §11 Wave 3, §12.4)
|
||||
|
||||
---
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Inherited from the index. Wave 3 adds:
|
||||
|
||||
- **A secret value never reaches Git, rendered config, a command line, JUnit XML, or an evidence
|
||||
artifact.** Secrets are files created per run at mode `0600` and deleted on teardown.
|
||||
- **`up --wait` applies only to long-running services.** A one-shot client (`auth-smoke`,
|
||||
`object-storage-smoke`, `notification-smoke`, `minio-init`) is run with `run --rm` and must exit
|
||||
zero. A required one-shot that is missing, skipped, or non-zero fails the whole lane.
|
||||
- **Teardown is scoped.** `down --volumes --remove-orphans` runs against the lane's unique
|
||||
`COMPOSE_PROJECT_NAME` only. Never touch another project or a named volume outside the lane.
|
||||
- **Reference docs are authoritative for merge and import semantics**, not memory:
|
||||
- [Docker Compose merge rules](https://docs.docker.com/reference/compose-file/merge/)
|
||||
- [Keycloak realm import](https://www.keycloak.org/server/importExport)
|
||||
- **Static verification precedes dynamic.** `config` and `create` must pass before any `up`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### Created
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/config/runtime/compose-profile-contracts.json` | The lane SSOT: id, Compose profile, file stack, Spring runtime, exact sorted service set, blocking flag. |
|
||||
| `docker-compose.infra.yml` | Every shared infrastructure service and one-shot smoke client. Owns nothing environment-specific. |
|
||||
| `docker-compose.prod-smoke.yml` | TLS PostgreSQL, prod env source, secret references. Test-only. |
|
||||
| `scripts/verify-compose-profile-contracts.sh` | The only static entry point. |
|
||||
| `scripts/run-compose-runtime-smoke.sh` | The only dynamic entry point. |
|
||||
| `infra/keycloak/realms/ca-skeleton-realm.json` | Reproducible realm import. No secret values. |
|
||||
| `infra/keycloak/entrypoint.sh` | Reads the secret file, exports it, execs `kc.sh start-dev --import-realm`. |
|
||||
| `infra/keycloak/smoke/auth-smoke.sh` | The one-shot client-credentials + protected-endpoint assertion. |
|
||||
| `infra/minio/smoke/object-storage-smoke.sh` | upload → HEAD → download → delete → wrong-credential rejection. |
|
||||
| `infra/minio/init/bucket-bootstrap.sh` | Bucket and minimum policy creation. Not a substitute for the round trip. |
|
||||
| `infra/notification/smoke/notification-smoke.sh` | Accept/ingest, Mailpit assertion, duplicate check. |
|
||||
| `src/.env.example` | Public key catalog with empty placeholders. Tracked. |
|
||||
| `src/.env.local.example` | Local opt-in combination example. Tracked. |
|
||||
|
||||
### Modified
|
||||
|
||||
| File | Change |
|
||||
| --- | --- |
|
||||
| `src/app-bootstrap/src/main/resources/application.yml` | Remove the `${SPRING_PROFILES_ACTIVE:local}` fallback. |
|
||||
| `docs/registries/env-keys.yaml` | `SPRING_PROFILES_ACTIVE` becomes a defaultless enum `local\|dev\|prod`. |
|
||||
| `src/app-bootstrap/build.gradle` | Replace the `bootRun`-only `.env` parsing with a single loader; pass an explicit profile. |
|
||||
| `src/build.gradle` | Rewrite `verifyEnvKeys`'s input contract: registry + profile YAML + `.env.example` + generated metadata; never a gitignored operator `.env`. |
|
||||
| `docker-compose.yml` | App only; no infrastructure. |
|
||||
| `docker-compose.local.yml` | Local overlay; profiles for service selection; PostgreSQL moves to infra. |
|
||||
| `docker-compose.dev.yml` | `tmpfs: !override []` then exactly one `/var/tmp/heap` bind mount; owns `SPRING_PROFILES_ACTIVE=dev` and its env source. |
|
||||
| `.gitignore` | Ignore `src/.env*` except the two `.example` files. |
|
||||
| `src/.env` | **Untracked** (`git rm --cached`). It is operator input, not a build input. |
|
||||
| `src/app-bootstrap/src/main/resources/logback-spring.xml` | Profile field reads the real active profile. |
|
||||
| `.github/workflows/ci-quality-gates.yml` | Call the two scripts; do not inline their commands. |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Make a profileless deployment impossible
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app-bootstrap/src/main/resources/application.yml:22-24`
|
||||
- Modify: `docs/registries/env-keys.yaml` (`SPRING_PROFILES_ACTIVE` row)
|
||||
- Create: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/activation/RuntimeEnvironmentProfileValidator.java`
|
||||
- Modify: `src/app-bootstrap/build.gradle` (`bootRun` passes an explicit profile)
|
||||
- Modify/replace: `EnvProfileMatrixContractTest` and any profileless-permitting test
|
||||
- Test: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/activation/RuntimeEnvironmentProfileValidatorTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `RuntimeEnvironmentProfileValidator`, an `EnvironmentPostProcessor` ordered **after**
|
||||
`MasterSwitchEnvironmentPostProcessor` (Wave 1 Task 2). Tasks 5–8 assume exactly one environment
|
||||
profile is resolvable.
|
||||
|
||||
**Context:** Verified at HEAD: `spring.profiles.active: ${SPRING_PROFILES_ACTIVE:local}`. A bootJar
|
||||
started with no profile silently becomes `local`, which — before Wave 2 Task A2 — also meant H2 and
|
||||
`create-drop`. Deployable cardinality is exactly one.
|
||||
|
||||
Rules: missing ⇒ error; blank ⇒ error; unknown ⇒ error naming the three permitted values; multiple
|
||||
(`local,prod`) ⇒ error, and `SPRING_PROFILES_ACTIVE` is **not** treated as CSV; `test` ⇒ rejected for a
|
||||
deployable artifact, permitted only in a test-source context.
|
||||
|
||||
Feature selection must not be expressed as a supplementary Spring profile — the five master switches
|
||||
are for that. The validator therefore rejects any active profile outside the permitted set rather than
|
||||
ignoring extras.
|
||||
|
||||
- [ ] **Step 1:** Write the validator test — one case per rule above, plus one asserting that a
|
||||
test-source context may still use `test`.
|
||||
- [ ] **Step 2:** Run to verify it fails.
|
||||
- [ ] **Step 3:** Remove the `:local` fallback from `application.yml`; write the validator; register
|
||||
it in `META-INF/spring.factories` next to the master-switch post-processor.
|
||||
- [ ] **Step 4:** Update the `SPRING_PROFILES_ACTIVE` registry row to a defaultless enum.
|
||||
- [ ] **Step 5:** Make `bootRun` pass an explicit profile so the developer convenience path stays
|
||||
usable without reintroducing an implicit default.
|
||||
- [ ] **Step 6:** Replace `EnvProfileMatrixContractTest`'s local-fallback expectation with the new
|
||||
fail-closed contract. Do not delete coverage — rewrite it.
|
||||
- [ ] **Step 7:** Run
|
||||
`./gradlew :app-bootstrap:test --console=plain --no-daemon` and
|
||||
`./gradlew verifyEnvKeys --console=plain --no-daemon`.
|
||||
- [ ] **Step 8:** Commit.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Separate example configuration from operator input
|
||||
|
||||
**Files:**
|
||||
- Create: `src/.env.example`, `src/.env.local.example`
|
||||
- Modify: `.gitignore`
|
||||
- Untrack: `src/.env` (`git rm --cached src/.env` — the human runs this)
|
||||
- Modify: `src/build.gradle` (`verifyEnvKeys` input contract, lines ~2209-2229, ~2263-2276)
|
||||
- Modify: `src/app-bootstrap/build.gradle` (single env loader, replacing the `bootRun`-only parser at ~251-272)
|
||||
- Test: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/activation/EnvSourceSeparationTest.java`
|
||||
|
||||
**Context:** Verified at HEAD: `src/.env` **is tracked** (`git ls-files` lists it), there is no
|
||||
`.env.example`, and `.gitignore` contains only three unrelated lines. The tracked file carries
|
||||
`SPRING_PROFILES_ACTIVE=local`, `APP_DATASOURCE_DDL_AUTO=update`, and local credentials — which is why
|
||||
`dev` inherits `ddl-auto=update` and fails.
|
||||
|
||||
`verifyEnvKeys` currently **requires** `src/.env` to exist and compares required placeholders against
|
||||
it. That contract is false in both directions: it passes only when a real secret file is present, and
|
||||
it would pass with no example file at all. The new SSOT is the env registry, the profile YAMLs,
|
||||
`.env.example`, and the generated configuration metadata. Gitignored operator `.env*` files and real
|
||||
secret values leave the build inputs entirely.
|
||||
|
||||
`.gitignore` addition:
|
||||
|
||||
```gitignore
|
||||
src/.env*
|
||||
!src/.env.example
|
||||
!src/.env.local.example
|
||||
```
|
||||
|
||||
- [ ] **Step 1:** Write `EnvSourceSeparationTest` — asserts `.env.example` exists and every registry
|
||||
key appears in it; that no example value looks like a real secret (non-empty for a
|
||||
`classification: secret` row); and that `src/.env` is **not** tracked
|
||||
(`git ls-files --error-unmatch src/.env` must fail).
|
||||
- [ ] **Step 2:** Run to verify it fails.
|
||||
- [ ] **Step 3:** Generate `.env.example` from the registry — every key, secrets as empty
|
||||
placeholders or `secret://` references. Write `.env.local.example` with a documented local
|
||||
opt-in combination.
|
||||
- [ ] **Step 4:** Update `.gitignore`; ask the human to run `git rm --cached src/.env`.
|
||||
- [ ] **Step 5:** Rewrite `verifyEnvKeys`'s inputs; replace the `bootRun`-only `.env` parser with a
|
||||
loader that selects the file by environment, or with Spring's standard config import.
|
||||
- [ ] **Step 6:** Run `./gradlew verifyEnvKeys :app-bootstrap:test --console=plain --no-daemon`.
|
||||
- [ ] **Step 7:** Commit.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: The Compose contract SSOT and the static verifier
|
||||
|
||||
**Files:**
|
||||
- Create: `src/config/runtime/compose-profile-contracts.json`
|
||||
- Create: `scripts/verify-compose-profile-contracts.sh`
|
||||
- Modify: `docker-compose.yml`, `docker-compose.local.yml`, `docker-compose.dev.yml`
|
||||
- Create: `docker-compose.infra.yml`, `docker-compose.prod-smoke.yml`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: the contract file, whose schema is
|
||||
`{ "minimumComposeVersion": "2.24.4", "lanes": [ { "id", "composeProfile"|null, "files": [...],
|
||||
"springRuntime", "services": [...sorted], "blocking": true } ] }`.
|
||||
Task 7's runtime-smoke script and Wave 6's matrix both read it.
|
||||
|
||||
**Context:** The full lane table is spec §7.2 and is reproduced here as the exact content to encode.
|
||||
`base`, `infra`, `local`, `dev`, `prod-smoke` mean `docker-compose.yml`, `docker-compose.infra.yml`,
|
||||
`docker-compose.local.yml`, `docker-compose.dev.yml`, `docker-compose.prod-smoke.yml`, merged in the
|
||||
order listed.
|
||||
|
||||
| lane | Compose profile | file stack | Spring runtime | services (sorted) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `off-local` | — | base+local | `local` | `app` |
|
||||
| `off-dev` | — | base+dev | `dev` | `app` |
|
||||
| `off-prod` | — | base+prod-smoke | `prod` | `app` |
|
||||
| `local-jpa` | `local-jpa` | base+infra+local | `local` | `app,db` |
|
||||
| `local-mongo` | `local-mongo` | base+infra+local | `local` | `app,mongo,mongo-rs-init` |
|
||||
| `local-messaging` | `local-messaging` | base+infra+local | `local` | `app,kafka` |
|
||||
| `local-messaging-outbox` | `local-messaging-outbox` | base+infra+local | `local` | `app,db,kafka` |
|
||||
| `local-notification-ingest` | `local-notification-ingest` | base+infra+local | `local` | `app,db,notification-smoke` |
|
||||
| `local-notification-serving` | `local-notification-serving` | base+infra+local | `local` | `app,db,mailpit,notification-smoke` |
|
||||
| `local-notification-handoff` | `local-notification-handoff` | base+infra+local | `local` | `app,db,mailpit,notification-smoke` |
|
||||
| `local-graphql` | `local-graphql` | base+infra+local | `local` | `app,auth-smoke,keycloak` |
|
||||
| `shared-infra-local` | `shared-infra` | base+infra+local | `local` | `app,auth-smoke,db,keycloak,minio,minio-init,object-storage-smoke` |
|
||||
| `shared-infra-dev` | `shared-infra` | base+infra+dev | `dev` | `app,auth-smoke,db,keycloak,minio,minio-init,object-storage-smoke` |
|
||||
| `prod-smoke` | `prod-smoke` | base+infra+prod-smoke | `prod` | `app,auth-smoke,db,keycloak,minio,minio-init,object-storage-smoke` |
|
||||
| `all-adapters` | `all-adapters` | base+infra+local | `local` | `app,auth-smoke,db,kafka,keycloak,mailpit,mongo,mongo-rs-init,notification-smoke` |
|
||||
|
||||
Service membership rules:
|
||||
- `auth-smoke` ∈ `local-graphql`, `shared-infra`, `prod-smoke`, `all-adapters`.
|
||||
- `object-storage-smoke` ∈ `shared-infra`, `prod-smoke`.
|
||||
- `notification-smoke` ∈ the three local notification profiles and `all-adapters`.
|
||||
- `minio-init` bootstraps bucket and policy; it is **not** a substitute for the round trip.
|
||||
- A Compose profile selects services; it never implies a Spring profile.
|
||||
|
||||
The dev merge fix, using Compose ≥ 2.24.4 semantics: the dev overlay declares
|
||||
`tmpfs: !override []` to replace the base tmpfs, then declares the `/var/tmp/heap` bind mount exactly
|
||||
once. Do not assume an empty sequence auto-deletes the base sequence — verify target uniqueness in the
|
||||
merged JSON, which is what the script's final check does.
|
||||
|
||||
The verifier checks, per lane:
|
||||
1. `docker compose version --short` ≥ the contract's `minimumComposeVersion` (semver compare);
|
||||
2. `config --services` for the lane's file stack (with `--profile <name>`, omitted for the three off
|
||||
lanes) equals the contract's sorted set **exactly** — not a superset;
|
||||
3. the rendered `app` service's `SPRING_PROFILES_ACTIVE` equals the lane's `springRuntime`;
|
||||
4. `--profile '*' config --format json` renders, and every service's `volumes` + `tmpfs` targets are
|
||||
unique within that service.
|
||||
|
||||
- [ ] **Step 1:** Write the contract JSON encoding the table above.
|
||||
- [ ] **Step 2:** Restructure the Compose files: move PostgreSQL out of `docker-compose.local.yml`
|
||||
into `docker-compose.infra.yml`; add Mongo + `mongo-rs-init`, Kafka, Mailpit, MinIO +
|
||||
`minio-init` + `object-storage-smoke`, Keycloak + `auth-smoke`, `notification-smoke`; add
|
||||
Compose `profiles:` to each; create `docker-compose.prod-smoke.yml`; apply the `!override` fix
|
||||
to `docker-compose.dev.yml` and give it `SPRING_PROFILES_ACTIVE=dev` plus its env source.
|
||||
- [ ] **Step 3:** Write `scripts/verify-compose-profile-contracts.sh` implementing checks 1–4.
|
||||
`set -euo pipefail`; `jq` for JSON; exit non-zero with the lane id and the exact diff on any
|
||||
mismatch.
|
||||
- [ ] **Step 4:** Run `./scripts/verify-compose-profile-contracts.sh`. Expected: all 15 lanes pass.
|
||||
- [ ] **Step 5:** Run
|
||||
`cd src && ./gradlew :app-bootstrap:test --tests '*ComposeMergeCharacterizationTest*'`
|
||||
— the dev case is now green; remove its `@Tag("wave0-red")`.
|
||||
- [ ] **Step 6:** Pin the Compose minimum version in `README.md` and the CI workflow.
|
||||
- [ ] **Step 7:** Commit.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: The Keycloak realm and its acceptance
|
||||
|
||||
> **May be pulled forward** — Wave 2 Task E3 depends on this artifact.
|
||||
|
||||
**Files:**
|
||||
- Create: `infra/keycloak/realms/ca-skeleton-realm.json`
|
||||
- Create: `infra/keycloak/entrypoint.sh`
|
||||
- Create: `infra/keycloak/smoke/auth-smoke.sh`
|
||||
- Modify: `docker-compose.infra.yml`, `src/app-bootstrap/src/main/resources/application-local.yml`
|
||||
|
||||
**Context:** The realm defines `ca-skeleton-api` as a **confidential** client with client
|
||||
authentication and a service account enabled, and with standard flow and direct access grant
|
||||
**disabled**. The service account carries realm role `user` and client role `graphql-query`; an
|
||||
audience mapper puts `ca-skeleton-api` into `aud`. Authentication for smoke is OAuth 2.0
|
||||
`client_credentials` — one method, no alternatives. No test user, no password grant, no direct access
|
||||
grant.
|
||||
|
||||
The seven acceptance checks (spec §7.3):
|
||||
1. realm `ca-skeleton` imported;
|
||||
2. client/audience `ca-skeleton-api` exists;
|
||||
3. the application's roles and the role/permission claim mapping exist;
|
||||
4. a token is issued via the service account's client credentials;
|
||||
5. the token has non-blank `sub`, exact `iss`, `aud=ca-skeleton-api`, `realm_access.roles` containing
|
||||
`user`, and `resource_access.ca-skeleton-api.roles` containing `graphql-query`;
|
||||
6. public health succeeds unauthenticated; protected REST and GraphQL succeed only with a valid token;
|
||||
7. wrong realm, wrong audience, and expired token are rejected with the expected safe error contract.
|
||||
|
||||
**The issuer trap, and why one hostname is not enough.** `application-local.yml:68-76,142-145` defaults
|
||||
the issuer to `localhost:8081`. That resolves on the host and, inside the app container, points at the
|
||||
app itself. JWKS discovery is lazy (`JwtDecoderConfig.java:25-58`), so startup succeeds and the error
|
||||
only appears at the first protected request. Do not assume one hostname resolves everywhere:
|
||||
|
||||
- **bootJar qualification** (Wave 2 E3): inject Testcontainers' *mapped* Keycloak URL into both the
|
||||
token endpoint and the app issuer — the same single URL on both sides.
|
||||
- **Compose smoke**: put `app` and `auth-smoke` on the same network and inject
|
||||
`http://keycloak:8080/realms/ca-skeleton` into both.
|
||||
|
||||
A token obtained from one URL and validated against another is not evidence, and neither is a
|
||||
successful startup.
|
||||
|
||||
**Secret handling.** The qualification script creates a URL-safe random secret file at mode `0600` per
|
||||
run and mounts it as a Compose/Testcontainers secret. `entrypoint.sh` reads
|
||||
`/run/secrets/keycloak-graphql-smoke-client-secret`, exports it as a process-local
|
||||
`KEYCLOAK_GRAPHQL_SMOKE_CLIENT_SECRET`, and `exec`s
|
||||
`/opt/keycloak/bin/kc.sh start-dev --import-realm`. The realm JSON contains only the
|
||||
`${KEYCLOAK_GRAPHQL_SMOKE_CLIENT_SECRET}` reference. The file is removed on teardown.
|
||||
|
||||
- [ ] **Steps 1–8:** build the realm, entrypoint, and smoke script; wire the Compose service; run
|
||||
`./scripts/verify-compose-profile-contracts.sh` and then the `local-graphql` lane; confirm all
|
||||
seven checks; confirm no secret value appears in any rendered config or artifact
|
||||
(`grep -r` the evidence directory for the generated value must find nothing).
|
||||
|
||||
---
|
||||
|
||||
## Task 5: MinIO bucket bootstrap and a real object round trip
|
||||
|
||||
**Files:**
|
||||
- Create: `infra/minio/init/bucket-bootstrap.sh`, `infra/minio/smoke/object-storage-smoke.sh`
|
||||
- Modify: `docker-compose.infra.yml`
|
||||
|
||||
**Context:** MinIO readiness is not success. `minio-init` creates the test bucket and minimum policy;
|
||||
`object-storage-smoke` is a **black-box** one-shot that consumes the lane's endpoint, bucket, and
|
||||
secret file and performs, in order and with no step skippable:
|
||||
|
||||
1. upload known bytes to a random object key;
|
||||
2. HEAD and verify size and checksum;
|
||||
3. download and verify byte equality;
|
||||
4. delete and verify not-found;
|
||||
5. attempt the same operations with a deliberately wrong credential and verify rejection.
|
||||
|
||||
Results are written to `minio-roundtrip.json` in the lane's evidence directory, with no secrets.
|
||||
|
||||
Boundaries: the existing object-storage qualification owns its own Testcontainers and random
|
||||
credentials, so it is **not** evidence about this Compose service — keep it, but give the Compose lane
|
||||
a separate name and separate artifacts. Local or static credentials are never passed to `prod-smoke`.
|
||||
`minio-init` succeeding is never accepted in place of the round trip. Whether object storage joins the
|
||||
`app-bootstrap` runtime is a **separate decision** from the five master switches; if it does not, this
|
||||
smoke client is a release fixture, not a production bean.
|
||||
|
||||
The client image is pinned **by digest**.
|
||||
|
||||
- [ ] **Steps 1–6:** build both scripts, wire the services, run the `shared-infra-local` lane, verify
|
||||
the artifact, confirm no secret leaked, commit.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: The notification smoke client and the stateful handoff lane
|
||||
|
||||
**Files:**
|
||||
- Create: `infra/notification/smoke/notification-smoke.sh`
|
||||
- Modify: `docker-compose.infra.yml`
|
||||
|
||||
**Context:** Three lanes use this client. `local-notification-ingest` proves durable accept with zero
|
||||
provider beans and zero workers. `local-notification-serving` proves a real Mailpit delivery.
|
||||
|
||||
`local-notification-handoff` is a **composite stateful lane**, not two lanes concatenated. The same
|
||||
project, the same PostgreSQL service, and the same named volume persist across four phases:
|
||||
|
||||
1. Start DB and app with `INGEST_ONLY` phase env. Store an accept request using credential-free
|
||||
Mailpit route metadata. Record the request ID and route version in evidence. Confirm via the
|
||||
activation report that provider beans/calls and worker threads are all zero.
|
||||
2. Stop **only the app**, cleanly. Do not bring down the DB or the volume.
|
||||
3. In the same project, recreate the app with `SERVING` phase env and reference-provider settings
|
||||
(`--force-recreate`), and bring Mailpit to ready.
|
||||
4. Re-run the smoke with the phase-1 request ID and frozen route version. Assert exactly one Mailpit
|
||||
message, a terminal DB state, and the same route version. Wait at least one more dispatch poll
|
||||
window and assert duplicates are still zero.
|
||||
|
||||
Only after both phases and the intermediate app exit succeed does the lane proceed to shared evidence
|
||||
collection and teardown. Deleting the volume after phase 1, or copying rows into a second project, is
|
||||
not handoff evidence.
|
||||
|
||||
- [ ] **Steps 1–7:** build the client, encode the phases in the runtime-smoke wrapper (Task 7), run
|
||||
the lane, verify the evidence, commit.
|
||||
|
||||
---
|
||||
|
||||
## Task 7: The runtime-smoke wrapper
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/run-compose-runtime-smoke.sh`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `--matrix <contract.json>` (all blocking lanes, zero-discovery and zero-skip) and
|
||||
`--lane <id>` (focused reproduction only — never a substitute for a matrix run). Wave 6 runs the
|
||||
matrix form.
|
||||
|
||||
**Context:** The wrapper enforces this order internally so no human and no CI job can skip a step:
|
||||
|
||||
1. Create a per-lane, per-run `COMPOSE_PROJECT_NAME` and a mode-`0700` temp directory; write env and
|
||||
secret files at mode `0600`. **Fail** if the evidence directory already exists — never reuse one.
|
||||
2. Run `verify-compose-profile-contracts.sh`, then `config`, then `create`.
|
||||
3. `up --wait` the long-running services only; check app health/readiness and the resolved activation
|
||||
report from Wave 1's `adapteractivation` endpoint.
|
||||
4. `run --rm` each one-shot the lane declares (`auth-smoke`, `object-storage-smoke`,
|
||||
`notification-smoke`); for JPA lanes, assert the app's migration/schema/TLS report. A required
|
||||
one-shot that is missing, skipped, or non-zero fails the lane.
|
||||
5. Write to `src/app-bootstrap/build/evidence/runtime-smoke/<lane>/<run-id>/`: `manifest.json`, the
|
||||
Compose and service-set digest, activation/health, DB migration/TLS, sanitized Keycloak claims,
|
||||
the MinIO round trip, and a warning/error summary. Never a raw token, URI credential, secret value,
|
||||
or rendered secret.
|
||||
6. On **both** success and failure: collect sanitized logs and container exits **first**, then in a
|
||||
`trap` run `down --volumes --remove-orphans` against this project only, and delete the temp env and
|
||||
secret files. Never touch another project or an outside named volume.
|
||||
|
||||
The `local-notification-handoff` phase sequence from Task 6 lives here.
|
||||
|
||||
- [ ] **Steps 1–8:** write it, run `--lane off-local` first, then `--lane local-jpa`, then the full
|
||||
`--matrix`, verifying evidence and teardown each time. Confirm with
|
||||
`docker ps -a` and `docker volume ls` that nothing outside the lane's project was touched.
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Wire CI to the scripts
|
||||
|
||||
**Files:**
|
||||
- Modify: `.github/workflows/ci-quality-gates.yml`
|
||||
|
||||
**Context:** CI calls the two scripts and nothing else for Compose work. Verified at HEAD, the quality
|
||||
job runs `./gradlew check verifyPublicPathSnapshot verifyDependencyLocks` and
|
||||
`./gradlew conditionalTransportQualification`, with no Compose verification at all. Inlining wrapper
|
||||
fragments would let a workflow silently run a lane without its one-shots, and past evidence must never
|
||||
be aggregated as a current pass.
|
||||
|
||||
- [ ] **Steps 1–4:** add the two script invocations, run the workflow (or `act`/a branch push), confirm
|
||||
both execute and fail loudly on a deliberately broken lane, commit.
|
||||
|
||||
---
|
||||
|
||||
## Wave 3 Exit Criteria
|
||||
|
||||
- [ ] `./scripts/verify-compose-profile-contracts.sh` — all 15 lanes pass.
|
||||
- [ ] `./scripts/run-compose-runtime-smoke.sh --matrix src/config/runtime/compose-profile-contracts.json`
|
||||
— every blocking lane passes with zero discovery failures and zero skips, including
|
||||
`prod-smoke` actually starting TLS DB + app + Keycloak + MinIO and running both one-shots.
|
||||
- [ ] `git ls-files src/.env` returns nothing; `src/.env.example` and `src/.env.local.example` are
|
||||
tracked.
|
||||
- [ ] A profileless bootJar start fails; `local,prod` fails; `stage` fails; each of `local`, `dev`,
|
||||
`prod` succeeds.
|
||||
- [ ] `cd src && ./gradlew verifyEnvKeys --console=plain --no-daemon` — green with the new input
|
||||
contract, and green with `src/.env` absent.
|
||||
- [ ] `./gradlew wave0RedReport` — only the Wave 4 warning entry remains.
|
||||
- [ ] No secret value appears anywhere under
|
||||
`src/app-bootstrap/build/evidence/` (`grep -r` the generated values finds nothing).
|
||||
|
||||
## What Wave 3 explicitly does not do
|
||||
|
||||
- No warning removal (Wave 4) and no build-logic extraction (Wave 5).
|
||||
- No promotion of object storage into the `app-bootstrap` runtime — that decision is separate from the
|
||||
five master switches and is not made here.
|
||||
- No reuse of local MinIO or Keycloak credentials in `prod-smoke`.
|
||||
- No acceptance of `minio-init` success as round-trip evidence, of Keycloak readiness as realm
|
||||
evidence, or of a successful startup as issuer evidence.
|
||||
@@ -0,0 +1,254 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,239 @@
|
||||
# Wave 5 — Gradle Build Logic 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 4 complete — a fully green, warning-zero baseline whose task graph,
|
||||
> dependency graph, test selection, and evidence output have been captured as a comparison artifact.
|
||||
|
||||
**Goal:** Extract the duplicated source-set, test-lane, testkit, API-surface, evidence, registry, and
|
||||
dependency machinery into eight TestKit-tested convention plugins in a `build-logic` included build,
|
||||
so that root, settings, and leaf build files each hold only their own responsibility — with the task
|
||||
graph, dependency graph, test selection, and evidence semantics provably unchanged.
|
||||
|
||||
**Architecture:** A `build-logic` included build holds precompiled convention plugins. Settings and
|
||||
root stop re-implementing registry parsing against each other by sharing one typed parser/validator.
|
||||
Every extraction is one step, and every step is verified by diffing the captured baseline artifacts —
|
||||
because the failure mode this wave uniquely risks is a task quietly ceasing to exist and the build
|
||||
reporting green for work it no longer does.
|
||||
|
||||
**Tech Stack:** Gradle 9 included builds, precompiled script plugins (`build-logic/src/main/groovy`),
|
||||
Gradle TestKit.
|
||||
|
||||
**Spec:** [`2026-08-15-five-adapter-runtime-remediation-review-design.md`](../specs/2026-08-15-five-adapter-runtime-remediation-review-design.md)
|
||||
(§10 in full, §11 Wave 5, §14 closing paragraph)
|
||||
|
||||
---
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Inherited from the index. Wave 5 adds:
|
||||
|
||||
- **Never share a diff with a runtime change.** Spec §14 is explicit: build refactoring on top of a
|
||||
red or unverified baseline produces false evidence, because a task that stops existing looks the
|
||||
same as a task that passes.
|
||||
- **LOC is not the goal.** Do not hide logic to move a number. Completion is judged by whether the
|
||||
same source-set/`Test`/API-surface machinery is still copied into two or more leaves, and whether
|
||||
root, settings, and leaf hold only their stated responsibilities.
|
||||
- **Each extraction step is verified by artifact diff, not by "the build still works".**
|
||||
- **Provider semantics stay per-module.** Image, scenario, security requirement, and promotion meaning
|
||||
differ per provider and stay in each module's registry. Do not merge them into one over-general DSL —
|
||||
spec §8.2 names that as a pattern to avoid.
|
||||
- **Do not remove a dependency a provider actually uses.** The exclusion convention exists to make
|
||||
declared exclusions real, not to strip dependencies centrally.
|
||||
|
||||
---
|
||||
|
||||
## Measured baseline
|
||||
|
||||
| File | Lines |
|
||||
| --- | ---: |
|
||||
| `src/build.gradle` | 2,767 |
|
||||
| `src/settings.gradle` | 185 |
|
||||
| `src/app-bootstrap/build.gradle` | 273 |
|
||||
| `src/adapter/outbound/persistence-jpa/build.gradle` | 352 |
|
||||
| `src/adapter/outbound/persistence-mongo/build.gradle` | 346 |
|
||||
| `src/adapter/outbound/messaging/build.gradle` | 105 |
|
||||
| `src/adapter/outbound/notification/build.gradle` | 39 |
|
||||
| `src/adapter/inbound/graphql/build.gradle` | 241 |
|
||||
| `src/gradle/jpa-evidence.gradle` | 930 |
|
||||
|
||||
The duplication that matters, not the size:
|
||||
|
||||
- custom source set + `extendsFrom` + `Test` task + `failOnNoDiscoveredTests`, repeated per lane;
|
||||
- strict qualification lane registration and result-directory wiring, repeated;
|
||||
- testkit artifact/source-set wiring, repeated;
|
||||
- API-surface snapshot render/update/verify machinery, repeated;
|
||||
- registry parsing and validation implemented **twice**, once in settings and once in root
|
||||
verification;
|
||||
- release-evidence manifest and output handling, repeated;
|
||||
- dependency exclusion intent that drifts from the resolved graph — e.g. the notification build claims
|
||||
a YAML exclusion (`adapter/outbound/notification/build.gradle:21-33`) while SnakeYAML remains in its
|
||||
lockfile (`gradle.lockfile:165`).
|
||||
|
||||
And one policy violation: `settings.gradle:37-44` hardcodes `expectedModuleCount = 44`, with a comment
|
||||
arguing the count should be a deliberate decision — but the project policy (`AGENTS.md:52-55`) makes the
|
||||
registry the count's SSOT and `verifyDocumentedLeafCount` enforces it against documents. A number in
|
||||
the build file is the same drift the policy forbids in prose. Meanwhile `verifyDocumentedLeafCount`
|
||||
inspects only some root documents, so a stale count in a module `CLAUDE.md` — for example
|
||||
`src/app-bootstrap/CLAUDE.md`'s "19-leaf dependency list" — is not caught.
|
||||
|
||||
---
|
||||
|
||||
## Task 0: Capture the comparison baseline
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/capture-build-baseline.sh`
|
||||
- Create: `docs/superpowers/plans/evidence/2026-08-15-wave5-baseline/`
|
||||
|
||||
**Context:** This is the instrument the whole wave is judged by. Capture, from the green Wave 4 state:
|
||||
|
||||
1. `./gradlew tasks --all` — the full task list;
|
||||
2. per-module `./gradlew <path>:dependencies --configuration runtimeClasspath`;
|
||||
3. every lane's JUnit XML **class list** (not timings);
|
||||
4. every evidence directory's file list and manifest schema;
|
||||
5. `./gradlew <every architecture-wide verify task>` output.
|
||||
|
||||
Normalize timestamps, durations, and absolute paths, so a diff shows semantic change only.
|
||||
|
||||
- [ ] **Steps 1–4:** write the script, run it, commit the baseline, and prove the script is
|
||||
deterministic by running it twice and diffing (must be identical).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `ca.architecture-registry` — one parser for settings and root
|
||||
|
||||
**Files:**
|
||||
- Create: `build-logic/settings.gradle`, `build-logic/build.gradle`
|
||||
- Create: `build-logic/src/main/groovy/ca.architecture-registry.gradle`
|
||||
- Create: `build-logic/src/main/java/dev/caskeleton/buildlogic/registry/ModuleRegistry.java`
|
||||
- Create: `build-logic/src/test/java/dev/caskeleton/buildlogic/registry/ModuleRegistryTest.java`
|
||||
- Modify: `src/settings.gradle`, `src/build.gradle`
|
||||
|
||||
**Context:** First extraction because everything else reads the registry. One typed parser/validator,
|
||||
fail-closed on: a project directory with a `build.gradle` that the registry does not list; a
|
||||
`source_path` that does not exist; a duplicate ID or path; and drift between the resolved runtime
|
||||
project closure and declared memberships (Wave 1 Task 13 already made the closure the comparison input).
|
||||
|
||||
Remove `expectedModuleCount`. The registry is the count.
|
||||
|
||||
Extend `verifyDocumentedLeafCount` to cover tracked root `AGENTS.md`, root `CLAUDE.md`, and **all**
|
||||
`src/**/CLAUDE.md`, and wire it into `check` and CI. Prefer removing the duplicated number from each
|
||||
document over asserting it — a count that is not written cannot drift. Where a document genuinely needs
|
||||
the number, it must be generated.
|
||||
|
||||
- [ ] **Steps 1–8:** TestKit tests first (malformed registry, missing path, duplicate ID, membership
|
||||
drift), then the plugin, then delete both re-implementations, then diff against the baseline.
|
||||
|
||||
## Task 2: `ca.strict-test-lane`
|
||||
|
||||
**Files:** `build-logic/src/main/groovy/ca.strict-test-lane.gradle` + TestKit tests; then apply to the
|
||||
JPA, Mongo, GraphQL, messaging, and app-bootstrap leaves one at a time.
|
||||
|
||||
**Context:** The single largest duplication. The convention owns source set creation, configuration
|
||||
`extendsFrom`, the `Test` task, `failOnNoDiscoveredTests`, stale-XML deletion, required-class
|
||||
enforcement, and the results directory — the shape
|
||||
`src/gradle/graphql-platform-conventions.gradle:55-100` implements by hand today.
|
||||
|
||||
Semantics that must survive verbatim, because each was written against a real failure: stale JUnit XML
|
||||
is deleted before the lane runs (a deleted class would otherwise report as executed), and a lane that
|
||||
executes no test case for a required class fails with a message saying the lane is green only because
|
||||
the class is gone.
|
||||
|
||||
TestKit cases: empty lane fails; duplicate task registration fails; a required class with no executed
|
||||
test case fails; stale XML is removed.
|
||||
|
||||
- [ ] **Steps 1–9:** one leaf per step, diffing lane task names and JUnit XML class lists against the
|
||||
baseline after each.
|
||||
|
||||
## Task 3: `ca.api-surface`
|
||||
|
||||
Read-only `verify` plus an explicitly-named approved `update` task. The two must not be the same task
|
||||
with a flag — an update that runs by default silently blesses a surface change.
|
||||
|
||||
- [ ] **Steps 1–6:** TestKit tests, extraction, per-leaf application, diff.
|
||||
|
||||
## Task 4: `ca.testkit-publisher`
|
||||
|
||||
Testkit source set and consumable artifact, currently repeated. `app-bootstrap` consumes
|
||||
`project(path: ':adapter:outbound:persistence-jpa', configuration: 'jpaTestkit')`; the convention must
|
||||
keep that consumer contract byte-identical.
|
||||
|
||||
- [ ] **Steps 1–6.**
|
||||
|
||||
## Task 5: `ca.evidence`
|
||||
|
||||
Manifest and result schema, deterministic output ordering, and a no-empty-evidence rule. Applies to
|
||||
`src/gradle/jpa-evidence.gradle` (930 lines) and the Mongo/GraphQL equivalents.
|
||||
|
||||
Provider-specific promotion meaning stays in each module registry.
|
||||
|
||||
- [ ] **Steps 1–7.**
|
||||
|
||||
## Task 6: `ca.dependency-policy`
|
||||
|
||||
Common exclusions and constraints, plus verification that each configuration's **resolved graph and
|
||||
lockfile** match the declared intent. The notification/SnakeYAML case is the acceptance test: a build
|
||||
that declares an exclusion while the lockfile still carries the dependency must fail.
|
||||
|
||||
Guard rail: a dependency a provider genuinely uses directly is never centrally removed.
|
||||
|
||||
- [ ] **Steps 1–7.**
|
||||
|
||||
## Task 7: `ca.java-leaf`
|
||||
|
||||
Java 21 toolchain, encoding, compiler flags (`-Werror -Xlint:deprecation -Xlint:unchecked`, currently
|
||||
`src/build.gradle:353`), Error Prone, and the baseline test task.
|
||||
|
||||
- [ ] **Steps 1–6.**
|
||||
|
||||
## Task 8: `ca.optional-adapter`
|
||||
|
||||
Activation metadata plus disabled/on composition-contract wiring for the five adapters. This is the
|
||||
convention that makes Wave 1's off-invariant testing a build-level default rather than something each
|
||||
leaf remembers.
|
||||
|
||||
- [ ] **Steps 1–6.**
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Reduce the three build files to their responsibilities
|
||||
|
||||
**Files:** `src/settings.gradle`, `src/build.gradle`, each leaf `build.gradle`
|
||||
|
||||
Target responsibilities:
|
||||
|
||||
- `settings.gradle`: plugin management, root project name, and applying the registry settings plugin.
|
||||
Nothing else.
|
||||
- root `build.gradle`: shared plugin and version declarations plus architecture-wide lifecycle tasks.
|
||||
- leaf `build.gradle`: plugins, project and external dependencies, and that leaf's own semantic
|
||||
lane/matrix.
|
||||
|
||||
- [ ] **Steps 1–5:** reduce, run the full verification set, diff against the baseline, commit.
|
||||
|
||||
---
|
||||
|
||||
## Wave 5 Exit Criteria
|
||||
|
||||
- [ ] `./scripts/capture-build-baseline.sh` output **diffs clean** against the Wave 4 baseline for the
|
||||
task list, dependency graphs, JUnit XML class lists, and evidence manifests. A task that
|
||||
disappeared is a failure even if everything green stayed green.
|
||||
- [ ] `cd src && ./gradlew clean check --warning-mode=fail --no-daemon --console=plain` — green.
|
||||
- [ ] `./gradlew verifyCleanArchitectureDependencies verifyEnvKeys verifyRuntimeModuleMembership
|
||||
verifyPublicPathSnapshot verifyDocumentedLeafCount --console=plain` — green.
|
||||
- [ ] `build-logic`'s own TestKit suite is green and covers malformed registry, empty lane, duplicate
|
||||
task, and membership drift.
|
||||
- [ ] `expectedModuleCount` is gone from `src/settings.gradle`.
|
||||
- [ ] `verifyDocumentedLeafCount` covers root `AGENTS.md`, root `CLAUDE.md`, and every
|
||||
`src/**/CLAUDE.md`, and is wired into `check` and CI.
|
||||
- [ ] No source-set/`Test`/API-surface machinery is copied into two or more leaves.
|
||||
- [ ] `./scripts/run-compose-runtime-smoke.sh --matrix ...` — still green, proving the refactor did not
|
||||
change what actually runs.
|
||||
|
||||
## What Wave 5 explicitly does not do
|
||||
|
||||
- No runtime, configuration, or test-behaviour change in the same diff.
|
||||
- No LOC-driven relocation that hides logic.
|
||||
- No merging of provider-specific release matrices into one generic DSL.
|
||||
- No central removal of a dependency a provider uses directly.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Wave 6 — Final Qualification and Documentation Sync Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (this wave is
|
||||
> verification-heavy and benefits from one session holding the whole evidence set).
|
||||
> **Read [`2026-08-15-five-adapter-runtime-remediation-index.md`](2026-08-15-five-adapter-runtime-remediation-index.md)
|
||||
> first.**
|
||||
> **Entry criterion:** Wave 5 complete — build logic extracted with a clean baseline diff.
|
||||
|
||||
**Goal:** Run every blocking gate, the full activation matrix, and every environment smoke; reconcile
|
||||
generated metadata and documentation with the code; and produce the evidence set that lets each
|
||||
Definition-of-Done checkbox in spec §13 be ticked with a command and its output attached.
|
||||
|
||||
**Architecture:** Wave 6 adds no capability. It executes, records, and reconciles. Every claim is
|
||||
backed by a command and its output stored under
|
||||
`docs/superpowers/plans/evidence/2026-08-15-wave6-final/`. A checkbox without attached evidence stays
|
||||
unticked, and a gate that could not run is reported as not-run with its reason — never as passing.
|
||||
|
||||
**Spec:** [`2026-08-15-five-adapter-runtime-remediation-review-design.md`](../specs/2026-08-15-five-adapter-runtime-remediation-review-design.md)
|
||||
(§12 in full, §13, §11 Wave 6)
|
||||
|
||||
---
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Inherited from the index. Wave 6 adds:
|
||||
|
||||
- **Use `superpowers:verification-before-completion` before any completion claim.** Evidence precedes
|
||||
assertion, always.
|
||||
- **A not-run gate is reported as not-run.** Spec HARD-STOP 6 (`AGENTS.md:22`) makes claiming
|
||||
completion without the verification, or without naming why it could not run, a stop condition.
|
||||
- **No conclusion broader than its evidence** (HARD-STOP 7). "The matrix passed" requires the matrix,
|
||||
not a representative lane.
|
||||
- **The wave ends with the LLM Wiki capture** required by `AGENTS.md:79-90`, or with an explicit
|
||||
reported reason it was blocked.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Build and architecture gates
|
||||
|
||||
- [ ] Run and capture each:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew clean compileJava compileTestJava --warning-mode=fail --no-daemon --console=plain
|
||||
./gradlew test --warning-mode=fail --no-daemon --console=plain
|
||||
./gradlew check --warning-mode=fail --no-daemon --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies verifyEnvKeys \
|
||||
verifyRuntimeModuleMembership verifyPublicPathSnapshot \
|
||||
verifyDocumentedLeafCount --console=plain
|
||||
```
|
||||
|
||||
- [ ] Confirm `./gradlew wave0RedReport --console=plain --no-daemon` reports an **empty** red set,
|
||||
then delete the `wave0Red` lanes and the aggregate — the characterizations they tracked are now
|
||||
ordinary tests, and a permanent lane for an empty set is a lane that stops being read.
|
||||
|
||||
## Task 2: Focused module gates
|
||||
|
||||
Gradle paths are read from `src/config/architecture/modules.json`, never from memory.
|
||||
|
||||
- [ ] Run and capture:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:persistence-jpa:test --console=plain
|
||||
./gradlew :adapter:outbound:persistence-mongo:test --console=plain
|
||||
./gradlew :adapter:outbound:messaging:test --console=plain
|
||||
./gradlew :adapter:outbound:notification:test --console=plain
|
||||
./gradlew :adapter:inbound:graphql:test --console=plain
|
||||
./gradlew :adapter:inbound:graphql:graphqlStableTest \
|
||||
:app-bootstrap:graphqlRuntimeQualification \
|
||||
conditionalTransportQualification --console=plain
|
||||
```
|
||||
|
||||
- [ ] Run the messaging platform's Stable facade focused tests and its live-broker lane separately.
|
||||
Record explicitly that ordinary `test` does **not** substitute for the Docker-backed
|
||||
qualification.
|
||||
|
||||
- [ ] Persistence blocking lanes:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate -Pjpa.matrix.versions=16 --console=plain
|
||||
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate -Pjpa.matrix.versions=17 --console=plain
|
||||
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate -Pjpa.matrix.versions=18 --console=plain
|
||||
./gradlew :adapter:outbound:persistence-mongo:mongoStableContractTest \
|
||||
:adapter:outbound:persistence-mongo:mongoReplicaSetTest \
|
||||
:adapter:outbound:persistence-mongo:mongoFailoverTest \
|
||||
:adapter:outbound:persistence-mongo:mongoMigrationTest \
|
||||
:adapter:outbound:persistence-mongo:mongoCompatibilityTest \
|
||||
:adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest \
|
||||
:adapter:outbound:persistence-mongo:mongoPerformanceTest --console=plain
|
||||
```
|
||||
|
||||
- [ ] Confirm Wave 2 Task B5's decision is reflected: either the three lanes exist and run, or their
|
||||
Stable blocking claim is gone from `src/config/mongodb/release-contracts.json`,
|
||||
`docs/mongodb/advanced/sharding.md`, and `scripts/verify-mongodb-advanced.sh`. Attach the
|
||||
`ReleaseManifestTaskExistenceTest` result.
|
||||
|
||||
## Task 3: The activation matrix
|
||||
|
||||
Run each row and capture the resolved activation report, health, exit code, WARN/ERROR count, and the
|
||||
bean/thread inventory.
|
||||
|
||||
| # | Matrix | Expected |
|
||||
| --- | --- | --- |
|
||||
| 1 | five off | boots with no external infrastructure; health OK; adapter beans, resources, threads, endpoints all zero |
|
||||
| 2 | JPA only | PostgreSQL + Flyway + Hibernate `validate` succeed; no Mongo, GraphQL, broker, or provider |
|
||||
| 3 | Mongo only | replica set, client, topology, security succeed; no JPA entity, repository, or pool |
|
||||
| 4 | Messaging only | broker publish/consume succeeds; with relay off, no database needed |
|
||||
| 5 | Notification + JPA, `INGEST_ONLY` | durable accept on a frozen non-empty versioned route; provider credentials, calls, and workers all zero; after a `SERVING` restart, exactly one delivery on the same route |
|
||||
| 6 | Notification + JPA, `SERVING` | reference provider delivery and receipt succeed |
|
||||
| 7 | GraphQL only | schema endpoint plus security/policy pipeline succeed; no persistence resolver |
|
||||
| 8 | relay on, dependency missing | startup rejects, **naming the exact missing switch or provider** |
|
||||
| 9 | JPA + Mongo | distinct ports succeed; two implementations of one port is a startup rejection, not a `@Primary` pick |
|
||||
| 10 | five on | every dependency and endpoint ready; no silent fallback and no duplicate authority |
|
||||
|
||||
- [ ] Row 8 must be checked for the *name*, not merely for a failure. A generic "misconfiguration" is a
|
||||
fail.
|
||||
- [ ] Row 9's conflict case must fail; a bean-ordering or `@Primary` resolution is a fail.
|
||||
|
||||
## Task 4: Environment and runtime gates
|
||||
|
||||
- [ ] Compose artifacts, statically then dynamically:
|
||||
|
||||
```bash
|
||||
./scripts/verify-compose-profile-contracts.sh
|
||||
./scripts/run-compose-runtime-smoke.sh --matrix src/config/runtime/compose-profile-contracts.json
|
||||
```
|
||||
|
||||
- [ ] Confirm the matrix owned every lane: `off-local`, `off-dev`, `off-prod`, each local adapter lane,
|
||||
`shared-infra-local`, `shared-infra-dev`, `prod-smoke`, `all-adapters`.
|
||||
- [ ] Confirm `prod-smoke` actually **started** TLS PostgreSQL, the app, Keycloak, and MinIO and ran
|
||||
both one-shots — a `config`-only pass is a fail.
|
||||
- [ ] Confirm each run stored exit code, active profile, resolved activation report, and WARN/ERROR
|
||||
count as artifacts.
|
||||
- [ ] Confirm no dev or prod lane was made to pass with a local override. Spec §12.4 does not accept
|
||||
that as evidence for the profile.
|
||||
- [ ] Confirm teardown left no stray project or volume: `docker ps -a`, `docker volume ls`.
|
||||
|
||||
## Task 5: Documentation and metadata reconciliation
|
||||
|
||||
- [ ] Regenerate Spring configuration metadata and diff against the env registry and the YAMLs; any
|
||||
drift is a fail.
|
||||
- [ ] Confirm every registry env key appears in `src/.env.example` and that no example carries a real
|
||||
secret.
|
||||
- [ ] Confirm `docs/registries/env-keys.yaml` lists the five masters with `false` defaults and the two
|
||||
subordinate selectors with their `required-when` conditions.
|
||||
- [ ] Confirm no demoted key (`APP_MESSAGING_BROKER`, `APP_NOTIFICATION_SLACK_PROVIDER`,
|
||||
`APP_NOTIFICATION_EMAIL_PROVIDER`, `app.jpa-platform.enabled`) is documented anywhere as an
|
||||
activation switch.
|
||||
- [ ] Update `README.md` with the five switches, the Compose minimum version, and the two script entry
|
||||
points.
|
||||
- [ ] Update `src/app-bootstrap/CLAUDE.md`, replacing its "19-leaf dependency list" phrasing with a
|
||||
pointer to the registry — a count in prose is the drift `AGENTS.md:52-55` forbids.
|
||||
- [ ] Confirm the capability docs do not label as Stable anything the index's scope boundaries exclude:
|
||||
Mongo reactive, change streams, sharding, Atlas, KMS; Notification before NTF-INT-007 is closed.
|
||||
|
||||
## Task 6: Definition of Done
|
||||
|
||||
Walk spec §13 and tick each box **only** with attached evidence. Reproduced here as the checklist:
|
||||
|
||||
- [ ] Five runtime facades on one bootJar runtime classpath.
|
||||
- [ ] Five canonical master switches in registry, YAML, metadata, and docs, all defaulting `false`.
|
||||
- [ ] All-off `local`, `dev`, `prod` smoke succeeds with no external resources.
|
||||
- [ ] Each adapter's off invariant and on fail-closed contract pinned by full-context tests.
|
||||
- [ ] Mongo, GraphQL, and Messaging Stable facades' resolved runtime membership matches the registry.
|
||||
- [ ] The JPA switch controls the whole DataSource/Hikari/entity/repository/Hibernate/Flyway/DB
|
||||
health-and-metrics graph.
|
||||
- [ ] Messaging has a real broker bridge and a consistent relay dependency.
|
||||
- [ ] Notification `SERVING` works through production assemblers; `INGEST_ONLY` starts no worker.
|
||||
- [ ] Notification handoff proves, in one project and DB volume, `INGEST_ONLY` accept → restart →
|
||||
`SERVING` delivery exactly once on the same frozen route, with zero duplicates.
|
||||
- [ ] GraphQL policy and JWT context execute on the real `/graphql` request path.
|
||||
- [ ] GraphQL blocking qualification runs the bootJar JWT composition exactly once and uses neither
|
||||
class-existence nor test-only Basic Auth as release evidence.
|
||||
- [ ] `SPRING_PROFILES_ACTIVE` is exactly one of `local|dev|prod`; profileless, multiple, and unknown
|
||||
deployments are rejected.
|
||||
- [ ] GraphQL on requires one environment-permitted `APP_GRAPHQL_DEPLOYMENT_MODE`; the legacy
|
||||
boolean/enum split-brain is rejected.
|
||||
- [ ] Profiles and env example/secret sources are separated; real secret files are excluded from
|
||||
tracking, rendering, and evidence.
|
||||
- [ ] Compose minimum version, per-profile exact service sets, the whole merged model, and mount-target
|
||||
uniqueness are verified by the canonical script.
|
||||
- [ ] The runtime-smoke wrapper performs create, `up --wait`, required one-shots, sanitized evidence,
|
||||
and unique-project teardown with zero skips across `local`, `dev`, and `prod` blocking lanes.
|
||||
- [ ] PostgreSQL, Mongo, broker, MinIO, and Keycloak/realm smoke evidence exists.
|
||||
- [ ] The Keycloak realm provides a client-credentials-only service account, audience, and role claims,
|
||||
and real JWT-protected REST and GraphQL requests succeed against the same issuer URL per lane.
|
||||
- [ ] Full `test` and `check`, plus architecture, env, public-path, and strict qualification, all pass.
|
||||
- [ ] Zero Gradle, javac, Checkstyle, SpotBugs, and runtime-startup errors and warnings; zero allowlist
|
||||
entries; IDE Problems zero confirmed separately on the same toolchain.
|
||||
- [ ] The real active profile and the structured-log profile field agree; zero late-MeterFilter
|
||||
warnings.
|
||||
- [ ] After convention-plugin extraction, task selection, dependency graph, and evidence semantics are
|
||||
unchanged.
|
||||
- [ ] Every P0 blocker on a runtime path from the detailed module reviews is either closed or its
|
||||
capability is explicitly inactive/experimental.
|
||||
- [ ] Changed files, commands, results, not-run/blocked items, and evidence grades are captured in the
|
||||
LLM Wiki branch-note.
|
||||
|
||||
## Task 7: LLM Wiki capture
|
||||
|
||||
Per `AGENTS.md:79-90`:
|
||||
|
||||
- [ ] Create or update
|
||||
`/home/donghyeon/workspace/ai-tool/llm-wiki-private/raw/branch-notes/<branch-name>.md` with the
|
||||
implementation, changed files, decisions, verification commands, failures/blocks, and evidence
|
||||
grades.
|
||||
- [ ] Create derived documents where genuine material exists: `raw/errors/`, `raw/interviews/`,
|
||||
`raw/blog-topics/`. Each links upward via `## Parent`; the branch-note's `## Cluster / 묶음`
|
||||
links back.
|
||||
- [ ] Where no derived document is warranted, record that judgement explicitly ("추출할 별도 글감
|
||||
없음") rather than omitting the section.
|
||||
- [ ] Do not create `wiki/blog/`, `wiki/interview/`, `wiki/portfolio/`, `wiki/concepts/`, or
|
||||
`wiki/projects/` without an explicit canonical extraction request.
|
||||
|
||||
## Task 8: Final report
|
||||
|
||||
Per `AGENTS.md:239-251`, the closing response states: changed files; the core changes; verification
|
||||
commands run; verifications that failed or could not run, with reasons; the Wiki capture result; and
|
||||
remaining risks or follow-ups.
|
||||
|
||||
- [ ] Explicitly restate what remains **out of scope and not production-ready**, from the index's scope
|
||||
boundaries: Mongo reactive, change streams, sharding, Atlas, KMS; Notification's at-rest decision
|
||||
if the threat-model branch was chosen; object storage's runtime inclusion; Fileserver internals.
|
||||
|
||||
---
|
||||
|
||||
## Wave 6 Exit Criteria
|
||||
|
||||
- [ ] Every §13 checkbox above is ticked **with attached evidence**, or is explicitly reported as
|
||||
not-met with its reason.
|
||||
- [ ] `docs/superpowers/plans/evidence/2026-08-15-wave6-final/` holds the output of every command in
|
||||
Tasks 1–4.
|
||||
- [ ] The LLM Wiki branch-note exists and links its derived documents.
|
||||
- [ ] No completion, "all passing", or "production-ready" claim appears anywhere without the command
|
||||
output that supports it.
|
||||
@@ -0,0 +1,709 @@
|
||||
# Wave 0 — Red Baseline Evidence
|
||||
|
||||
- Repository HEAD at capture: `2f5d2fc21954286213c1474d19935f571ef896ea`
|
||||
- Captured: 2026-08-15
|
||||
- Toolchain: JDK 21.0.11, Gradle 9.0.0, Docker Engine 29.7.2, Docker Compose 5.4.0
|
||||
- Plan: [`2026-08-15-wave0-red-baseline.md`](../2026-08-15-wave0-red-baseline.md)
|
||||
|
||||
## Exit state
|
||||
|
||||
```
|
||||
$ cd src && ./gradlew wave0RedReport --console=plain --no-daemon
|
||||
BUILD SUCCESSFUL in 1m 7s
|
||||
```
|
||||
|
||||
**12 red, 1 unexpectedly green.** The red set matches the plan's expected table except for
|
||||
`StartupWarningZeroTest`, recorded as a deviation below.
|
||||
|
||||
| Red test | Closed by | Confirmed cause |
|
||||
| --- | --- | --- |
|
||||
| `SecretLeakScannerCharacterizationTest.methodCallWithSafeSuffixIsNotALeak` | Wave 2 C1 | safe-suffix `$` anchor cannot match past a captured `()` |
|
||||
| `SecretLeakScannerCharacterizationTest.numericFencingIsNotALeak` | Wave 2 C1 | every `+` read as string concatenation |
|
||||
| `FiveAdapterOffInventoryTest.jpaOffHoldsNothing` | Wave 1 T4/T9 | see JPA inventory below |
|
||||
| `FiveAdapterOffInventoryTest.messagingOffHoldsNothing` | Wave 1 T7 | see messaging inventory below |
|
||||
| `FiveAdapterOffInventoryTest.notificationOffHoldsNothing` | Wave 1 T3/T8 | see notification inventory below |
|
||||
| `ShippedRuntimeFacadePresenceTest.mongoFacadeIsShipped` | Wave 1 T5 | `ClassNotFoundException` — not on the runtime classpath |
|
||||
| `ShippedRuntimeFacadePresenceTest.graphQlFacadeIsShipped` | Wave 1 T6 | `ClassNotFoundException` |
|
||||
| `ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped` | Wave 2 C3 | `ClassNotFoundException` |
|
||||
| `DefaultProfileBootCharacterizationTest.localProfileStartsWithShippedDefaults` | Wave 1 T9 | relay-enabled with blank broker |
|
||||
| `DefaultProfileBootCharacterizationTest.devProfileStartsWithShippedDefaults` | Wave 1 T9 / Wave 3 T2 | same validator reached first |
|
||||
| `ComposeMergeCharacterizationTest.devStackRenders` | Wave 3 T3 | duplicate `/var/tmp/heap` mount target |
|
||||
| `ReleaseManifestTaskExistenceTest.mongoReleaseContractNamesOnlyRegisteredTasks` | Wave 2 B5 | three unregistered tasks |
|
||||
|
||||
Green as planned: `StartupWarningRecorderTest`, `RuntimeMembershipClasspathAgreementTest`,
|
||||
`ComposeMergeCharacterizationTest` base/local, `ShippedRuntimeFacadePresenceTest` JPA/notification,
|
||||
`FiveAdapterOffInventoryTest` Mongo/GraphQL (vacuously — see Task 4), the three non-red scanner cases,
|
||||
`DefaultProfileBootCharacterizationTest.prodProfileRefusesPlaintextJdbc`.
|
||||
|
||||
## Task 1 — secret scanner
|
||||
|
||||
```
|
||||
$ ./gradlew :messaging:messaging-observability:test --tests '*SecretLeakScannerCharacterizationTest*'
|
||||
SecretLeakScannerCharacterizationTest > RED: incrementing a fencing token is arithmetic, not concatenation FAILED
|
||||
SecretLeakScannerCharacterizationTest > RED: a method call whose name ends in a safe suffix is not a leak FAILED
|
||||
5 tests completed, 2 failed
|
||||
```
|
||||
|
||||
The pre-existing repository failure this characterizes:
|
||||
|
||||
```
|
||||
$ ./gradlew :messaging:messaging-observability:test --tests '*SecretLeakStaticScanTest*'
|
||||
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,"]
|
||||
```
|
||||
|
||||
## Task 3 — off-state inventories
|
||||
|
||||
Captured from the failure messages, with all five switches off on `local`. These are the exact type
|
||||
lists Wave 1 works down.
|
||||
|
||||
**JPA off** — a connection pool, the entity/repository scan, and the H2 vendor configuration all
|
||||
exist:
|
||||
|
||||
```
|
||||
com.zaxxer.hikari.HikariDataSource
|
||||
dev.caskeleton.adapter.outbound.persistence.audit.DomainContextAuditContextPort
|
||||
dev.caskeleton.adapter.outbound.persistence.config.PersistenceJpaConfig
|
||||
dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings
|
||||
dev.caskeleton.adapter.outbound.persistence.failure.PersistenceExceptionTranslator
|
||||
dev.caskeleton.adapter.outbound.persistence.failure.StandardSqlStateErrorMapping
|
||||
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileTransitionRepository
|
||||
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverCleanupRepository
|
||||
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverQuotaRepository
|
||||
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverRecoveryRepository
|
||||
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.JpaFileRepository
|
||||
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.JpaUploadSessionRepository
|
||||
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.UploadLeaseRepository
|
||||
dev.caskeleton.adapter.outbound.persistence.h2.H2IdempotencyClaimRepository
|
||||
dev.caskeleton.adapter.outbound.persistence.h2.H2LocalTimeoutConfigurer
|
||||
dev.caskeleton.adapter.outbound.persistence.h2.H2OutboxClaimRepository
|
||||
dev.caskeleton.adapter.outbound.persistence.h2.H2PersistenceConfig
|
||||
dev.caskeleton.adapter.outbound.persistence.h2.H2SqlStateErrorMapping
|
||||
(list truncated in the assertion message)
|
||||
```
|
||||
|
||||
Confirms JPA-INT-001 and the JPA half of §4.3's consumer table: the Fileserver repositories are on
|
||||
this list, which is why Wave 1 Task 4's `DataSourceRequirement` must name Fileserver as a relational
|
||||
consumer rather than treating the pool as JPA's alone.
|
||||
|
||||
**Messaging off**:
|
||||
|
||||
```
|
||||
dev.caskeleton.adapter.outbound.messaging.MessagingConfig
|
||||
dev.caskeleton.adapter.outbound.messaging.MessagingSettings
|
||||
dev.caskeleton.adapter.outbound.messaging.core.DisabledMessagePublisher
|
||||
dev.caskeleton.adapter.outbound.messaging.kafka.KafkaAdapterConfig
|
||||
dev.caskeleton.adapter.outbound.messaging.kafka.KafkaAdapterSettings
|
||||
dev.caskeleton.adapter.outbound.messaging.outbox.DisabledOutboxMessagePublisher
|
||||
dev.caskeleton.adapter.outbound.messaging.outbox.Slf4jOutboxRelayFailureReportAdapter
|
||||
```
|
||||
|
||||
Note `DisabledMessagePublisher` and `DisabledOutboxMessagePublisher`: the sentinel behaviour is
|
||||
correct, but off-invariant item 9 requires the **composition root** to supply it rather than the
|
||||
adapter. Wave 1 Task 7 moves it.
|
||||
|
||||
**Notification off** — settings bind with the master off, which is NTF-INT-005 exactly:
|
||||
|
||||
```
|
||||
dev.caskeleton.adapter.outbound.notification.NotificationConfig
|
||||
dev.caskeleton.adapter.outbound.notification.NotificationRoutesSettings
|
||||
dev.caskeleton.adapter.outbound.notification.core.RoutingNotifier
|
||||
dev.caskeleton.adapter.outbound.notification.email.google.GoogleEmailNotificationAdapterConfig
|
||||
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformSettings
|
||||
dev.caskeleton.adapter.outbound.notification.slack.webhook.SlackNotificationAdapterConfig
|
||||
dev.caskeleton.bootstrap.notification.NotificationPlatformSecretsConfig$NotificationSecretsSettings
|
||||
```
|
||||
|
||||
## Task 5 — default-profile boot
|
||||
|
||||
Both `local` and `dev` fail on the same validator, with the message it was written to give:
|
||||
|
||||
```
|
||||
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
|
||||
'outboxRelayBrokerRequirementValidator' defined in class path resource
|
||||
[dev/caskeleton/bootstrap/outbox/OutboxConfig.class]: ca-skeleton.outbox.relay-enabled is enabled
|
||||
but app.messaging.broker is blank, so every claimed outbox row would fail to publish and be
|
||||
exhausted to DEAD. Either configure a broker, or set ca-skeleton.outbox.relay-enabled=false so
|
||||
PENDING rows are preserved until one exists.
|
||||
```
|
||||
|
||||
`dev` reaches this validator before the `ddl-auto=update` conflict the spec recorded from `bootRun`,
|
||||
so the `ddl-auto` failure is currently **masked**. It will surface once Wave 1 Task 9 sets
|
||||
`relay-enabled: false`, and Wave 3 Task 2 closes it.
|
||||
|
||||
## Task 7 — Compose merge
|
||||
|
||||
```
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.dev.yml config
|
||||
services.app.volumes[1]: target /var/tmp/heap already mounted as services.app.tmpfs[1]
|
||||
```
|
||||
|
||||
Base and local render cleanly (`app`, and `app,db` respectively).
|
||||
|
||||
## Task 8 — runtime project closure
|
||||
|
||||
```
|
||||
$ ./gradlew :app-bootstrap:runtimeClasspathManifest
|
||||
$ cat app-bootstrap/build/architecture/runtime-project-closure.txt
|
||||
```
|
||||
|
||||
The manifest resolves and `RuntimeMembershipClasspathAgreementTest` is **green** — direct
|
||||
dependencies and the resolved closure agree today, because no build-only leaf is reachable. This is
|
||||
the gate that must stay green while Waves 1 and 2 add the Mongo, GraphQL, and messaging edges.
|
||||
|
||||
## Task 9 — release manifest
|
||||
|
||||
```
|
||||
ReleaseManifestTaskExistenceTest FAILED
|
||||
the Mongo release contract names task(s) that no build file registers, so a release manifest can
|
||||
report them green without ever running them
|
||||
missing: [mongoAtlasTest, mongoKmsTest, mongoShardedTest]
|
||||
registered: [mongoReplicaSetTest, mongoFailoverTest, mongoMigrationTest, mongoCompatibilityTest,
|
||||
mongoSecurityIntegrationTest, mongoPerformanceTest, mongoStableContractTest]
|
||||
```
|
||||
|
||||
`scripts/verify-mongodb-advanced.sh:95` also invokes `mongoShardedTest`, so that script cannot
|
||||
currently succeed either.
|
||||
|
||||
---
|
||||
|
||||
## Deviations from the plan
|
||||
|
||||
### 1. `StartupWarningZeroTest` is green, not red
|
||||
|
||||
The plan expected this red. It passes: an **all-off, non-web** `local` startup emits zero WARN and
|
||||
zero ERROR.
|
||||
|
||||
That is not a contradiction of spec §9.1 — the warnings recorded there
|
||||
(`BeanPostProcessorChecker` on the authorization beans, two Micrometer late-`MeterFilter` warnings,
|
||||
a Flyway converter warning on `dev`) were observed during `bootRun`, which is a **web** application
|
||||
with JPA active. This test is narrower than the configuration that produces them.
|
||||
|
||||
Consequence for Wave 4: `StartupWarningZeroTest` as written does not yet measure the warnings Wave 4
|
||||
must remove. Wave 4 Task 6 already calls for extending it to all three environments and every one-on
|
||||
combination; that extension must also use `WebApplicationType.SERVLET`, or the gate will keep
|
||||
passing while the warnings remain. Recorded here so the omission is not discovered as a surprise.
|
||||
|
||||
### 2. A test-harness defect surfaced first, and is not a production defect
|
||||
|
||||
Booting `CaSkeletonApplication` in-JVM from `app-bootstrap`'s own test source set fails before any
|
||||
adapter is examined:
|
||||
|
||||
```
|
||||
BeanDefinitionOverrideException: Invalid bean definition with name 'outboxEventJpaRepository'
|
||||
defined in ... @EnableJpaRepositories declared on PersistenceJpaConfig: ... there is already
|
||||
[...] defined in ... @EnableJpaRepositories declared on OutboxContainerTestSupport.OutboxRepositoryConfig
|
||||
```
|
||||
|
||||
Cause: `CaSkeletonApplication` component-scans `dev.caskeleton.bootstrap`; this module's test sources
|
||||
live in that package; so an in-JVM boot discovers a **test-only** configuration
|
||||
(`OutboxContainerTestSupport.OutboxRepositoryConfig`) that the shipped jar has never contained.
|
||||
|
||||
This is a property of the measurement, not of the product — `bootRun` is unaffected. Left alone it
|
||||
would have reported the same cause for every activation characterization and hidden the defects they
|
||||
exist to name. `ShippedCompositionHarness` registers a `TypeExcludeFilter` that drops candidates
|
||||
whose class file came from a test output directory, which is a rule about provenance rather than a
|
||||
class-name list somebody has to maintain.
|
||||
|
||||
**This is a workaround for measuring, not a fix.** The faithful instrument is running the produced
|
||||
jar as a child process, which is what Wave 2 Task E3 builds for GraphQL. If Wave 1's off-invariant
|
||||
work needs stronger evidence than the harness can give, promote the activation tests to that shape
|
||||
rather than trusting the exclusion.
|
||||
|
||||
### 3. A property-precedence trap worth remembering
|
||||
|
||||
`SpringApplicationBuilder#properties(String...)` contributes to `defaultProperties`, the
|
||||
lowest-precedence source, so the all-off set lost to `application.yml`'s `relay-enabled: true` and
|
||||
the "all-off" context died on the relay validator. The harness passes `--key=value` command-line
|
||||
arguments instead. Any later test that sets an all-off baseline must do the same.
|
||||
|
||||
## Files added by Wave 0
|
||||
|
||||
Production sources changed: **none**. Verified by inspection — every path below is a test source or
|
||||
a Gradle lane registration.
|
||||
|
||||
| File | Kind |
|
||||
| --- | --- |
|
||||
| `src/messaging/messaging-observability/src/test/java/.../SecretLeakScannerCharacterizationTest.java` | test |
|
||||
| `src/app-bootstrap/src/test/java/.../activation/AdapterActivationInventory.java` | test fixture |
|
||||
| `src/app-bootstrap/src/test/java/.../activation/ShippedCompositionHarness.java` | test fixture |
|
||||
| `src/app-bootstrap/src/test/java/.../activation/FiveAdapterOffInventoryTest.java` | test |
|
||||
| `src/app-bootstrap/src/test/java/.../activation/ShippedRuntimeFacadePresenceTest.java` | test |
|
||||
| `src/app-bootstrap/src/test/java/.../activation/DefaultProfileBootCharacterizationTest.java` | test |
|
||||
| `src/app-bootstrap/src/test/java/.../activation/StartupWarningRecorder.java` | test fixture |
|
||||
| `src/app-bootstrap/src/test/java/.../activation/StartupWarningRecorderTest.java` | test |
|
||||
| `src/app-bootstrap/src/test/java/.../activation/StartupWarningZeroTest.java` | test |
|
||||
| `src/app-bootstrap/src/test/java/.../compose/ComposeMergeCharacterizationTest.java` | test |
|
||||
| `src/app-bootstrap/src/test/java/.../registry/RuntimeMembershipClasspathAgreementTest.java` | test |
|
||||
| `src/app-bootstrap/src/test/java/.../registry/ReleaseManifestTaskExistenceTest.java` | test |
|
||||
| `src/app-bootstrap/build.gradle` | lane registration (`runtimeClasspathManifest`, `wave0Red`) |
|
||||
| `src/messaging/messaging-observability/build.gradle` | lane registration (`wave0Red`) |
|
||||
| `src/build.gradle` | lane registration (`wave0RedReport`) |
|
||||
|
||||
---
|
||||
|
||||
# Wave 1 progress note (same session)
|
||||
|
||||
## The repository-wide `test` is green
|
||||
|
||||
```
|
||||
$ cd src && ./gradlew test --console=plain --no-daemon --continue
|
||||
BUILD SUCCESSFUL in 3m 20s
|
||||
```
|
||||
|
||||
It was **red at HEAD** before this work, on `SecretLeakStaticScanTest` (spec §3.1). Two changes made
|
||||
it green, and neither is a suppression:
|
||||
|
||||
1. **The scanner defect is fixed** (Wave 2 C1, pulled forward because it was the only thing keeping
|
||||
the build red). A captured method call keeps its trailing `()`, so the safe-suffix exemption's `$`
|
||||
anchor never matched — stripping the call fixes it, where unanchoring the pattern would also have
|
||||
exempted `credentialIdentity`, which does carry the value. And a `+` with a numeric literal on
|
||||
either side is arithmetic, which cannot put a secret into a string. The three true-positive cases
|
||||
in the characterization are what prove the fix was not a weakening.
|
||||
2. **Wave 0's deliberately-red characterizations are excluded from the ordinary `test` task** and
|
||||
reported by `wave0RedReport` instead. A permanently red `test` is a build nobody can use, and a
|
||||
gate nobody can use stops catching the regressions it exists for. Wave 6 requires the tag set to
|
||||
be empty, so the exclusion cannot quietly become forgetting.
|
||||
|
||||
## Remaining red set: 4
|
||||
|
||||
| Red | Closed by |
|
||||
| --- | --- |
|
||||
| `ShippedRuntimeFacadePresenceTest.graphQlFacadeIsShipped` | Wave 1 T6 |
|
||||
| `ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped` | Wave 2 C3 |
|
||||
| `ComposeMergeCharacterizationTest.devStackRenders` | Wave 3 T3 |
|
||||
| `ReleaseManifestTaskExistenceTest.mongoReleaseContractNamesOnlyRegisteredTasks` | Wave 2 B5 |
|
||||
|
||||
Down from 12 at the Wave 0 baseline.
|
||||
|
||||
## Known defect left as found: the shared-contract scope rule
|
||||
|
||||
`CleanArchitectureTest.SHARED_CONTRACT_CONTAINS_ONLY_OPERATIONAL_CONTRACT_PACKAGES` has two faults
|
||||
that mask each other:
|
||||
|
||||
- its subject pattern is `..shared..`, which matches any package named `shared` anywhere rather than
|
||||
the shared-contract module;
|
||||
- its allowlist contains the bare module root `dev.caskeleton.shared` and the check matches by
|
||||
prefix, so **every** package inside the module is blessed automatically.
|
||||
|
||||
Net effect: the rule catches nothing inside the module it is named after, and does catch unrelated
|
||||
leaves that happen to have a `shared` package — which is how it surfaced, when the Mongo leaf joined
|
||||
the composition root's analysis scope.
|
||||
|
||||
Fixing it properly requires the violation fixture that proves the rule works to move inside
|
||||
`dev.caskeleton.shared`, which then puts the fixture in the rule's own analysis scope, which requires
|
||||
reworking how fixtures are scanned. That is a separate change from adapter activation, so the rule is
|
||||
left as found with the defect documented at the rule itself, and the Mongo tenancy package added to
|
||||
the allowlist to keep the build honest in the meantime. **This is technical debt, not a fix.**
|
||||
|
||||
## Corrections to the spec and to these plans, found by executing them
|
||||
|
||||
| Claim | Reality |
|
||||
| --- | --- |
|
||||
| `ca-skeleton.idempotency.provider` | The property does not exist. It is `ca-skeleton.capabilities.idempotency.provider`. |
|
||||
| `app.fileserver.enabled` | It is `app.fileserver-platform.enabled`. |
|
||||
| Notification/GraphQL selectors are legacy master aliases | They are subordinate settings that stay valid while the adapter is on. Treating them as aliases made every shipped configuration ambiguous. |
|
||||
| `AutoConfigurationImportFilter` registers in `.imports` | It registers in `META-INF/spring.factories`. Getting this wrong fails open silently — the filter simply never runs. |
|
||||
| The JPA root can import one exported entry | Inverting the vendor→config import to create one produced a package cycle. The composition root names both vendor configs instead, and the export surface admits them with that reason recorded. |
|
||||
|
||||
## Shipping Mongo pulled in reactive Mongo
|
||||
|
||||
Adding the Mongo leaf to `app-bootstrap` put `spring-boot-starter-data-mongodb-reactive` and
|
||||
`mongodb-driver-reactivestreams` on the runtime classpath. The index's scope boundaries exclude
|
||||
reactive Mongo from the shipped Stable runtime, and leaving the starter there would have let Boot
|
||||
build a second client and pool from the same URI as soon as the master switch went on. Both are
|
||||
excluded at the composition root rather than in the leaf, which still compiles the reactive paths for
|
||||
a future promotion.
|
||||
|
||||
---
|
||||
|
||||
# Session close — Wave 1 complete, Wave 2/3 partially landed
|
||||
|
||||
## Verified state
|
||||
|
||||
```
|
||||
$ cd src && ./gradlew test --console=plain --no-daemon --continue
|
||||
BUILD SUCCESSFUL in 2m 58s
|
||||
|
||||
$ ./gradlew verifyEnvKeys verifyCleanArchitectureDependencies \
|
||||
verifyRuntimeModuleMembership verifyPublicPathSnapshot
|
||||
verifyRuntimeModuleMembership: 2 runtime composition(s) match the registry
|
||||
BUILD SUCCESSFUL
|
||||
|
||||
$ ./gradlew wave0RedReport
|
||||
1 red remaining
|
||||
```
|
||||
|
||||
**12 red at the Wave 0 baseline → 1.** The survivor is
|
||||
`ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped`, which Wave 2 C3 closes: the
|
||||
messaging platform has no production sender, only a test fake, so giving it runtime membership now
|
||||
would ship a path that cannot work.
|
||||
|
||||
## Wave 1 — complete (13/13)
|
||||
|
||||
| Task | Delivered |
|
||||
| --- | --- |
|
||||
| T1 | `MasterSwitch` / `MasterSwitchParser` in `shared-contract` — five names in one place, strict parse |
|
||||
| T2 | `MasterSwitchEnvironmentPostProcessor` — rejects before any detail namespace binds |
|
||||
| T3 | Both composition-root scans narrowed; the properties scan names its packages because it has no `excludeFilters` |
|
||||
| T4 | `PersistenceJpaRootAutoConfiguration` + `DataSourceRequirement` + `JpaOffAutoConfigurationImportFilter` |
|
||||
| T5 | Mongo shipped, one authority, reactive starter excluded from the runtime |
|
||||
| T6 | GraphQL shipped, one authority, Boot GraphQL auto-configurations filtered |
|
||||
| T7 | Messaging bridge gated; disabled sentinels moved to the composition root |
|
||||
| T8 | `NotificationRootAutoConfiguration` — secrets and registries named rather than scanned |
|
||||
| T9 | Migration, outbox and idempotency under capability roots |
|
||||
| T10 | `CapabilityDependencyValidator` — 8 rules, each naming the exact missing switch |
|
||||
| T11 | `DatabaseReadinessGroupPostProcessor` — `db` membership derived from the capability closure |
|
||||
| T12 | 8 env registry rows + YAML binding + a contract test derived from the enum |
|
||||
| T13 | Membership gate reads the resolved runtime closure instead of declared dependencies |
|
||||
|
||||
Measured effect, JPA off: **~30 beans → 0** (pool, entity scan, repositories, Hibernate, Flyway, DB
|
||||
health all gone).
|
||||
|
||||
## Pulled forward from later waves
|
||||
|
||||
- **Wave 2 C1** — the secret scanner's two false positives fixed at the cause. This is what made a
|
||||
repository-wide green `test` possible; it had been red at HEAD.
|
||||
- **Wave 2 B5** — the three ghost Mongo lanes demoted to `experimental_contracts` rather than
|
||||
implemented, with the script and doc updated to match.
|
||||
- **Wave 3 T1** — profileless deploys refused, scoped to the deployable artifact so slice tests are
|
||||
unaffected.
|
||||
- **Wave 3 T3** — the dev Compose `tmpfs: !override []` fix; merge verified and mount targets checked
|
||||
for uniqueness in the merged model.
|
||||
|
||||
## The defaulting cascade, and what it cost
|
||||
|
||||
Removing the relay's blanket refusal exposed the failure underneath it, exactly as this document
|
||||
predicted — and then that one exposed a third. The sequence was:
|
||||
|
||||
1. relay-enabled with a blank broker (fixed in T9);
|
||||
2. `ddl-auto=update` against a Flyway-owned schema (fixed by changing the tracked `.env`);
|
||||
3. `logging.level.root` failing to bind, because **62 placeholders in `application.yml` had no inline
|
||||
default at all** and only `application-local.yml` pinned enough of them for one profile to start.
|
||||
|
||||
55 of those 62 now carry a default. The remaining seven are deliberate: the datasource URL, username
|
||||
and password, the application name, and the JWT issuer and audience — a default for any of them is a
|
||||
deployment running against something nobody chose. CORS allowed-origins was moved out of that set
|
||||
after the fact: CORS is off by default and an empty origin list is the safest value rather than an
|
||||
arbitrary one, so it defaults to empty.
|
||||
|
||||
One of the added defaults was itself wrong — `max-age-seconds` got `600s` while the field is a
|
||||
`long` — which is worth recording because it only surfaced through a real boot. A bulk defaulting
|
||||
pass needs a boot per profile to be believed.
|
||||
|
||||
## Still open
|
||||
|
||||
- `ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped` (Wave 2 C3).
|
||||
- The `..shared..` ArchUnit rule remains defective and documented at the rule itself; see the earlier
|
||||
note. Unchanged this session.
|
||||
- Waves 2 (remaining), 3 (Compose lane matrix, Keycloak, MinIO), 4, 5 and 6 are untouched.
|
||||
|
||||
---
|
||||
|
||||
# Continuation — Wave 3 T2 and Wave 4 T1 landed; a Wave 4 fix disproved
|
||||
|
||||
## Verified state
|
||||
|
||||
```
|
||||
$ cd src && ./gradlew test BUILD SUCCESSFUL
|
||||
$ ./gradlew verifyEnvKeys verifyCleanArchitectureDependencies \
|
||||
verifyRuntimeModuleMembership verifyPublicPathSnapshot
|
||||
BUILD SUCCESSFUL
|
||||
$ ./gradlew wave0RedReport 2 red
|
||||
```
|
||||
|
||||
## Wave 3 T2 — env source separation, complete
|
||||
|
||||
`src/.env` is untracked. `src/.env.example` (309 keys, generated from the registry, 12
|
||||
secret-classified keys left empty) and `src/.env.local.example` are tracked in its place, and
|
||||
`.gitignore` carries `src/.env*` with the two examples negated.
|
||||
|
||||
`verifyEnvKeys` now reads the example. Reading the real file made it false in **both** directions: it
|
||||
passed only where an operator's own environment file happened to exist, and it would have passed with
|
||||
no example at all — so the file an adopter actually copies was never verified, while a file full of
|
||||
real credentials was a build input. Proven by deleting `src/.env` and re-running: green.
|
||||
|
||||
Its rule B was inverted while retargeting. It required every key in the file to be an
|
||||
`application.yml` placeholder, which is true of a hand-maintained `.env` and false of a catalogue —
|
||||
most registry keys are bound by typed settings inside a leaf. Inverted to "every registered `APP_` key
|
||||
appears in the example", it now catches the drift that actually matters: a key added to the registry
|
||||
that never reached the file an adopter copies.
|
||||
|
||||
`SPRING_PROFILES_ACTIVE` was also corrected in the registry — `type: enum`, `required: true`, no
|
||||
default — to match Wave 3 T1.
|
||||
|
||||
## Wave 4 T1 — MeterFilter ordering, complete
|
||||
|
||||
`MetricsContractConfig` and `SampleMetricsContractConfig` both install their filters through a
|
||||
`MeterRegistryCustomizer` instead of a `@PostConstruct` that fetched the registry. The warning was the
|
||||
visible half of the real defect: a filter applies only to meters registered after it, so the
|
||||
cardinality and distribution policies were being applied to some meters and not others. Both
|
||||
composition roots changed together, because fixing one leaves the warning reproducible from the other.
|
||||
|
||||
The Boot 4 package is `org.springframework.boot.micrometer.metrics.autoconfigure`, found by inspecting
|
||||
the resolved jars rather than assumed.
|
||||
|
||||
## Wave 4 T4 — attempted, disproved, and left as found
|
||||
|
||||
The spec calls the structured log's `profile` field a drift: it reads `SPRING_PROFILES_ACTIVE`, so
|
||||
overriding the profile on the command line while a stale value sits in the environment stamps lines
|
||||
with the stale one. The drift is real.
|
||||
|
||||
The obvious fix — bind the field to `spring.profiles.active` — **does not work**, and an existing
|
||||
contract test said so. `LogProfileDriftCharacterizationTest` was written to settle the question by
|
||||
observation rather than argument, and it observed an empty string: Logback initialises before that
|
||||
property resolves. A field that says nothing is not an improvement on a field that is sometimes wrong.
|
||||
|
||||
The binding was reverted to what it was, with the reason recorded at the declaration, and the
|
||||
characterization kept as a tagged red. The fix needs a different mechanism — setting the logger
|
||||
context property from the resolved environment once it is ready, rather than declaring the source in
|
||||
XML — which is Wave 4's to build.
|
||||
|
||||
## Remaining red: 2
|
||||
|
||||
| Red | Closed by |
|
||||
| --- | --- |
|
||||
| `ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped` | Wave 2 C3 — no production sender exists, only a test fake |
|
||||
| `LogProfileDriftCharacterizationTest` | Wave 4 — needs the mechanism above |
|
||||
|
||||
---
|
||||
|
||||
# Continuation — Wave 3 Compose lane matrix (static half) and Keycloak realm
|
||||
|
||||
## Verified state
|
||||
|
||||
```
|
||||
$ cd src && ./gradlew test BUILD SUCCESSFUL
|
||||
$ ./gradlew verifyEnvKeys verifyCleanArchitectureDependencies \
|
||||
verifyRuntimeModuleMembership verifyPublicPathSnapshot BUILD SUCCESSFUL
|
||||
$ ./scripts/verify-compose-profile-contracts.sh
|
||||
all 15 lanes match src/config/runtime/compose-profile-contracts.json
|
||||
$ ./gradlew wave0RedReport 2 red
|
||||
```
|
||||
|
||||
## What landed
|
||||
|
||||
**`src/config/runtime/compose-profile-contracts.json`** — the lane SSOT. 15 lanes, each fixing a
|
||||
Compose profile, a file stack, an explicit Spring runtime, and the exact service set that stack must
|
||||
render. Exact rather than superset: a lane that quietly gains a service is a lane whose evidence
|
||||
describes a different stack than the one that ran.
|
||||
|
||||
**`docker-compose.infra.yml`** — every shared service, each carrying Compose profiles so nothing
|
||||
starts unless a lane names it: PostgreSQL, a single-node Mongo replica set with an idempotent
|
||||
initiator, Kafka, Mailpit, MinIO with bucket bootstrap, Keycloak, and the three one-shot smoke
|
||||
clients. Infrastructure no longer lives inside environment overlays, which is what let `local` stop
|
||||
meaning "the app plus a database".
|
||||
|
||||
**`docker-compose.prod-smoke.yml`** — a production-shaped runtime whose JDBC URL carries
|
||||
`sslmode=verify-full`, so the prod validators are satisfied rather than bypassed.
|
||||
|
||||
**`scripts/verify-compose-profile-contracts.sh`** — the static entry point, wired into the ordinary
|
||||
test suite so a lane cannot drift until somebody remembers to run a shell script. It checks the
|
||||
Compose version floor, the exact service set per lane, the rendered `SPRING_PROFILES_ACTIVE`, and
|
||||
mount-target uniqueness in the merged model.
|
||||
|
||||
**Keycloak realm** (`infra/keycloak/`) — `ca-skeleton-api` as a confidential client with a service
|
||||
account, standard flow and direct access grant off, an audience mapper and realm/client role mappers.
|
||||
The client secret is a `${...}` reference; the entrypoint reads it from a mounted secret file and
|
||||
execs `kc.sh`, so no value reaches Git, the rendered config, or `docker inspect`.
|
||||
|
||||
**Smoke clients** — `auth-smoke` (the seven realm checks, against the same issuer URL the app is
|
||||
given), `object-storage-smoke` (upload → HEAD → download → delete → wrong-credential rejection, none
|
||||
skippable), `notification-smoke` (three phases, so the handoff lane's accept and verify are the same
|
||||
client talking about the same request id).
|
||||
|
||||
## What the verifier caught immediately
|
||||
|
||||
Writing it was worth it before running anything. On first execution it failed four lanes:
|
||||
|
||||
- `off-local` rendered `app,db`, because the `db` service was still in the local overlay as well as
|
||||
in the new infra file;
|
||||
- three lanes could not render at all, because the local overlay's `depends_on: db` pointed at a
|
||||
service their profile does not enable — Compose refuses that outright.
|
||||
|
||||
Both are the same mistake: infrastructure declared where the environment is described. `db` now lives
|
||||
only in the infra file, and the `depends_on` is gone — ordering belongs to the runtime-smoke wrapper,
|
||||
which knows which services a lane actually starts.
|
||||
|
||||
## Two follow-on defects found and fixed
|
||||
|
||||
**Untracking `src/.env` broke local Compose on a fresh clone.** `docker compose config` failed
|
||||
outright because the local overlay declared `env_file: ./src/.env`. Marked `required: false`, and
|
||||
verified by moving the file aside: the stack renders. A convenience override had become a hard
|
||||
prerequisite for rendering the stack at all.
|
||||
|
||||
**The developer host-port contract moved with the service.**
|
||||
`DeveloperExperienceContractTest.localComposePublishesTheHostPortTheCommittedDatasourceUrlTargets`
|
||||
asserted against the local overlay. It now asserts against the infra file and additionally that the
|
||||
service carries the `local-jpa` profile — without which the port assertion would pass for a service
|
||||
no lane ever brings up.
|
||||
|
||||
## Still open in Wave 3
|
||||
|
||||
`scripts/run-compose-runtime-smoke.sh` — the dynamic half — is not written. Nothing here has been
|
||||
started; what is verified is that all 15 lanes render exactly what they claim, with the right Spring
|
||||
runtime and no mount collisions. The lanes have not been run, and this document does not claim they
|
||||
have.
|
||||
|
||||
---
|
||||
|
||||
# Continuation — the Compose lanes actually run
|
||||
|
||||
## Verified state
|
||||
|
||||
```
|
||||
$ cd src && ./gradlew test verifyEnvKeys verifyCleanArchitectureDependencies \
|
||||
verifyRuntimeModuleMembership verifyPublicPathSnapshot BUILD SUCCESSFUL
|
||||
$ ./scripts/verify-compose-profile-contracts.sh all 15 lanes match
|
||||
$ ./scripts/run-compose-runtime-smoke.sh --lane off-local passed
|
||||
$ ./scripts/run-compose-runtime-smoke.sh --lane off-dev passed
|
||||
$ ./scripts/run-compose-runtime-smoke.sh --lane off-prod passed
|
||||
```
|
||||
|
||||
Each lane's own report, fetched from the running application rather than asserted from the flags the
|
||||
lane passed in:
|
||||
|
||||
| lane | activeProfile | switches on | dataSourceRequiredBy |
|
||||
| --- | --- | --- | --- |
|
||||
| `off-local` | `local` | none | not required |
|
||||
| `off-dev` | `dev` | none | not required |
|
||||
| `off-prod` | `prod` | none | not required |
|
||||
|
||||
`docker ps -a` and `docker volume ls` show no surviving `casmoke` project or volume.
|
||||
|
||||
**This is the Wave 1 exit criterion, demonstrated for the first time in a real container:** all five
|
||||
adapters off, three environments, no infrastructure of any kind, and the application reporting so
|
||||
itself.
|
||||
|
||||
## What was built
|
||||
|
||||
`scripts/run-compose-runtime-smoke.sh` — the dynamic entry point. Unique project per lane, evidence
|
||||
directory that refuses to reuse a previous run's, static contract then `config` then `create`,
|
||||
`up --wait` on long-running services only, a bounded readiness poll, every declared one-shot with a
|
||||
non-zero exit failing the lane, sanitized evidence, and a `trap` teardown scoped to the lane's own
|
||||
project — logs collected before the teardown, not after.
|
||||
|
||||
`AdapterActivationEndpoint` / `AdapterActivationReport` — the application's own answer about what
|
||||
resolved on. A lane asserting on its own environment passes whenever it set the variables correctly,
|
||||
which is not the claim being made.
|
||||
|
||||
## Six defects the lanes found, in the order they surfaced
|
||||
|
||||
Each was invisible to every check that existed before, and none would have been found by reading.
|
||||
|
||||
1. **A stale image.** The app service declares both `build:` and `image:`, so Compose reused a tag
|
||||
from an older state of the repository — the first run failed on a class that no longer exists in
|
||||
the tree. The wrapper now builds explicitly. A lane running a stale image produces evidence about
|
||||
code nobody changed.
|
||||
2. **The actuator is on its own connector.** Fetching `8080/actuator` returned an empty file that
|
||||
read exactly like a failed assertion about the profile.
|
||||
3. **The activation endpoint was authenticated.** Management auth is JWT, so only a lane with an
|
||||
identity provider could have read it — excluding the all-off lanes, whose claim is the hardest to
|
||||
check any other way. It is now permit-all alongside health/info/prometheus, and
|
||||
`AdapterActivationReportShapeTest` holds it to property names and booleans so that stays true.
|
||||
`ManagementActuatorSecurityContractTest` records the allowlist decision rather than absorbing it.
|
||||
4. **`off-local` was passing by luck.** It read the developer's own `src/.env` for the seven
|
||||
deliberately-undefaulted values. The wrapper now generates them per run, so a lane reproduces
|
||||
anywhere rather than on the machine it was written on.
|
||||
5. **The dev overlay has no healthcheck**, so `up --wait` returned as soon as the container was
|
||||
created and the first fetch landed before startup finished. The wrapper polls with a bound rather
|
||||
than trusting `--wait` alone.
|
||||
6. **`environment:` beat `env_file:` in the prod overlay.** `APP_DATASOURCE_PASSWORD:
|
||||
"${APP_DATASOURCE_PASSWORD:-}"` read the host shell, not the lane's generated file, and injected
|
||||
an empty string — which the prod env validator then refused, correctly, about a value the lane had
|
||||
actually supplied.
|
||||
|
||||
## Not claimed
|
||||
|
||||
The twelve infrastructure-bearing lanes have not been run. `--matrix` exists and is untested against
|
||||
them; what is demonstrated is the three all-off lanes end to end and that all fifteen render exactly
|
||||
what they claim. Keycloak, MinIO, Mongo, Kafka and Mailpit have been written and rendered, not
|
||||
started.
|
||||
|
||||
---
|
||||
|
||||
# Continuation — the infrastructure lanes, and what running them found
|
||||
|
||||
## Verified state
|
||||
|
||||
```
|
||||
$ cd src && ./gradlew test verifyEnvKeys verifyCleanArchitectureDependencies \
|
||||
verifyRuntimeModuleMembership verifyPublicPathSnapshot BUILD SUCCESSFUL
|
||||
$ ./scripts/verify-compose-profile-contracts.sh all 15 lanes match
|
||||
$ ./scripts/run-compose-runtime-smoke.sh --matrix src/config/runtime/compose-profile-contracts.json
|
||||
all 4 blocking lanes passed
|
||||
```
|
||||
|
||||
No surviving `casmoke` container or volume.
|
||||
|
||||
| lane | activeProfile | switches on | vendor |
|
||||
| --- | --- | --- | --- |
|
||||
| `off-local` | local | none | none |
|
||||
| `off-dev` | dev | none | none |
|
||||
| `off-prod` | prod | none | none |
|
||||
| `local-mongo` | local | `persistence-mongo` | none |
|
||||
|
||||
`local-mongo` is the first adapter proven on against real infrastructure: a single-node replica set,
|
||||
the Mongo master switch on, no other switch on, and no relational connection required.
|
||||
|
||||
## The contract gained two assertions, because a green lane was not yet a meaningful one
|
||||
|
||||
**`expectedSwitchesOn`.** A lane named `local-jpa` that ran with JPA off would render the right
|
||||
services, start cleanly, and prove nothing. The wrapper now compares the switches the application
|
||||
reports against what the lane asked for.
|
||||
|
||||
**`expectedPersistenceVendor`.** `local-jpa` passed for a while against H2 while the PostgreSQL
|
||||
container it started sat untouched beside it — `application-local.yml` pinned an in-memory URL as a
|
||||
literal, which outranks any environment a caller supplies. Every other field in the report looked
|
||||
correct. The report now carries the vendor resolved from the JDBC URL, and the lane asserts it.
|
||||
|
||||
That pin was not unique. `application-local.yml` also pinned `app.messaging.broker: ""` and the two
|
||||
notification provider selectors, so `local-messaging` started Kafka, set `APP_MESSAGING_BROKER=kafka`
|
||||
and was refused by the dependency validator for a value it had supplied. All four are placeholders
|
||||
now; the defaults are unchanged, so a developer who sets nothing gets exactly what they got before.
|
||||
|
||||
## Four defects in the wrapper itself
|
||||
|
||||
1. **It reported success for lanes it never ran.** `docker compose exec` consumes stdin, and inside a
|
||||
plain `while read` loop it ate the remaining lanes — the first ran, the loop ended, and the script
|
||||
said all six passed. Reading on fd 3 fixes it; a ran-count guard makes a partial matrix a failure
|
||||
rather than a pass. A wrapper whose own success message is a false green is worse than no wrapper.
|
||||
2. **Compose project names reject uppercase**, so the run id is lowercased for the project while the
|
||||
evidence directory keeps the readable timestamp.
|
||||
3. **`env_file` lists merge and the later file wins.** The developer's optional `src/.env`, declared
|
||||
by the local overlay after the base, silently overrode the lane's own values. Lane settings now go
|
||||
into a generated `environment:` overlay, which beats every `env_file` regardless of order.
|
||||
4. **`up --wait` is not a readiness gate where no healthcheck exists** — the dev overlay has none, so
|
||||
the first fetch landed before startup finished. The wrapper polls with a bound.
|
||||
|
||||
## Two spec findings confirmed in a real composition, not inferred
|
||||
|
||||
**MSG-INT-003.** With a healthy Kafka and the broker selected, startup fails on a missing
|
||||
`KafkaSender` bean: the legacy Kafka configuration requires a project-supplied sender, and production
|
||||
has none — only the tests provide a fake. This is precisely why the messaging platform leaves must
|
||||
not get runtime membership before a real transport bridge exists.
|
||||
|
||||
**GQL-INT-002.** `APP_GRAPHQL_DEPLOYMENT_MODE` is registered and bound, but the platform still reads
|
||||
the old `production` boolean and `environment` enum, which default to `false` and `PRODUCTION_PUBLIC`.
|
||||
The startup validator therefore sees a production deployment with introspection enabled and refuses.
|
||||
|
||||
A third was found that the spec did not predict: **shipping GraphQL into the same context as the rest
|
||||
of the application produced two `Clock` beans**, because the platform's clock was conditioned on its
|
||||
own bean *name* rather than on the type. Every injection point wanting a `Clock` failed to start. It
|
||||
now backs off on the type, which is what auto-configuration is for — and this could not have happened
|
||||
while the leaf was build-only.
|
||||
|
||||
## Lanes marked not-blocking, with reasons recorded in the contract
|
||||
|
||||
`local-jpa`, `local-messaging-outbox`, the three notification lanes, `shared-infra-local`,
|
||||
`shared-infra-dev`, `prod-smoke` and `all-adapters` are blocked on an open JPA finding: the entity
|
||||
scan is unconditional while the Flyway migration streams are partitioned by capability, so
|
||||
`ddl-auto=validate` against real PostgreSQL fails on `fs_cleanup_item`. Scoping the entity scan to
|
||||
active capabilities is Wave 2 JPA work.
|
||||
|
||||
`local-messaging` is blocked on MSG-INT-003 and `local-graphql` on GQL-INT-002, both above.
|
||||
|
||||
Each carries its reason in `compose-profile-contracts.json` and each keeps its assertions, so the
|
||||
lanes fail loudly rather than passing against the wrong thing.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user