merge: integrate notification production capability

# Conflicts:
#	docs/superpowers/plans/2026-07-28-notification-production-capability.md
#	src/adapter/outbound/persistence-jpa/build.gradle
#	src/adapter/outbound/persistence-jpa/gradle.lockfile
#	src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
#	src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
#	src/application-core/src/main/java/dev/caskeleton/application/notification/NotificationCapabilityCompatibilityValidator.java
#	src/application-core/src/main/java/dev/caskeleton/application/notification/NotificationDeliveryStorePort.java
#	src/application-core/src/main/java/dev/caskeleton/application/notification/NotificationDispatchUseCase.java
#	src/application-core/src/main/java/dev/caskeleton/application/notification/NotificationFrozenPlan.java
#	src/application-core/src/main/java/dev/caskeleton/application/notification/NotificationMaintenanceCommand.java
#	src/application-core/src/main/java/dev/caskeleton/application/notification/NotificationMaintenanceResult.java
#	src/application-core/src/main/java/dev/caskeleton/application/notification/NotificationMaintenanceStorePort.java
#	src/application-core/src/test/java/dev/caskeleton/application/notification/NotificationCapabilityCompatibilityValidatorTest.java
#	src/application-core/src/test/java/dev/caskeleton/application/notification/NotificationDispatchUseCaseTest.java
#	src/application-core/src/test/java/dev/caskeleton/application/notification/NotificationMaintenanceUseCaseTest.java
#	src/application-core/src/test/java/dev/caskeleton/application/notification/NotificationPlanningBoundaryTest.java
#	src/application-core/src/test/java/dev/caskeleton/application/notification/NotificationValueContractTest.java
#	src/application-core/src/test/java/dev/caskeleton/application/notification/ReconcileNotificationDeliveriesUseCaseTest.java
This commit is contained in:
donghyeon-ka
2026-08-01 00:11:53 +09:00
77 changed files with 7051 additions and 68 deletions
@@ -7,7 +7,13 @@
> RED/GREEN/verification 규칙을 수동으로 명시한다.
- 작성일: 2026-07-28
- 상태: proposed implementation plan, 구현 미착수
- 상태: Wave A/B 및 Task 8 구현 checkpoint를 main 통합. Task 9 이후는 미착수
- 원 작업 위치: `codex/notification-production-capability` 격리 worktree
- 통합 위치: `/home/donghyeon/workspace/clean-architecture-backend-template` (`main`)
- 현재 checkpoint: Wave A application/transaction foundation, Wave B catalog/render/provider
fake protocol, Task 8 local crypto primitive 구현. Task 9의 migration/persistence model과 이후
provider/runtime 범위는 미착수다. 아래 isolated-worktree 검증 기록과 main 통합 검증 결과는
서로 다른 증거로 구분한다.
- 설계 정본:
[Notification Production Capability Deep Design](../specs/2026-07-28-notification-production-capability-design.md)
- 비교한 실행 계획:
@@ -783,7 +789,7 @@ removing bindings, but these types must remain while compiled consumers exist.
### Wave A exit gate
- [ ] Run:
- [x] Run:
```bash
cd src && ./gradlew :application-core:check \
@@ -795,14 +801,11 @@ removing bindings, but these types must remain while compiled consumers exist.
--tests '*CleanArchitectureTest' --console=plain
```
Current evidence: the first command passes. The wildcard architecture command executes
`CleanArchitectureTest` successfully (59/59) but remains blocked by the unrelated
`DisabledAdapterArchitectureTest` Redis optional-bean gating violation. The exact
`dev.caskeleton.bootstrap.architecture.CleanArchitectureTest` command passes.
Isolated-worktree evidence: both commands pass, including the wildcard architecture test.
- [x] Confirm application bytecode/import scan contains no Spring, JPA, Slack, AWS, JSON or HTTP
provider type.
- [ ] Request an application/transaction boundary review before Wave B.
- [x] Request an application/transaction boundary review before Wave B.
- [x] Update the LLM Wiki branch-note with Wave A evidence and an explicit derived-document decision.
---
@@ -832,9 +835,10 @@ removing bindings, but these types must remain while compiled consumers exist.
- `src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCanonicalRouteCatalogTest.java`
- `src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCutoverRouteCatalogTest.java`
- [ ] RED cases: unknown/blank/duplicate local catalog entry; channel/provider mismatch;
- [x] RED cases: unknown/blank/duplicate local catalog entry; channel/provider mismatch;
durable + legacy/fail-open; receipt-required + unsupported provider; unsafe fallback after
indeterminate; target/retry/reconcile/amplification bound; cyclic binding.
indeterminate; target/retry/reconcile/amplification bound; unsupported non-`SINGLE` binding.
Initial R1 intentionally compiles only the three reviewed `SINGLE` cards.
- [ ] RED cutover-catalog cases:
`NotificationCanonicalRouteCatalog` is the retained key-only canonical SSOT and maps with the
trusted runtime target config to application-owned `NotificationCanonicalWriterRouteSet`.
@@ -858,7 +862,7 @@ removing bindings, but these types must remain while compiled consumers exist.
A catalog route
with no live legacy consumer is initialized as closed LEGACY predecessor and switched through
the audited protocol; it is never directly seeded canonical in PRE.
- [ ] Verify RED:
- [x] Verify RED:
```bash
cd src && ./gradlew :adapter:outbound:notification:test \
@@ -870,14 +874,14 @@ removing bindings, but these types must remain while compiled consumers exist.
Expected failure: canonical descriptors/compiler do not exist.
- [ ] Implement a pure, deterministic compiler over explicit input; do not inspect Spring beans,
- [x] Implement a pure, deterministic compiler over explicit input; do not inspect Spring beans,
application context, persistence schema or inbound adapters.
- [ ] Keep `expected-state`, exact actual/expected binding IDs, application mode/admission matching,
- [x] Keep `expected-state`, exact actual/expected binding IDs, application mode/admission matching,
store capability and ingress topology out of this sibling-local compiler. Task 17 passes
provider-neutral descriptors to the application compatibility validator for those checks.
- [ ] Register only the three initial card IDs. Legacy descriptors must explicitly advertise R0,
- [x] Register only the three initial card IDs. Legacy descriptors must explicitly advertise R0,
no durable/receipt capability.
- [ ] Emit a sorted immutable binding graph and manifest digest; unknown inputs fail closed.
- [x] Emit a sorted immutable binding graph and manifest digest; unknown inputs fail closed.
- [ ] Emit one immutable `NotificationCutoverRouteCatalog` and digest from the same route descriptor
inputs over the retained immutable `NotificationCanonicalRouteCatalog`. Bootstrap converts the
canonical catalog to `NotificationCanonicalWriterRouteSet` and the PRE decorator to
@@ -891,14 +895,16 @@ removing bindings, but these types must remain while compiled consumers exist.
route has zero permits. `NotificationCutoverRouteCatalogTest` freezes the digest algorithm and
proves the exact registry value that batch initialization must persist; after initialization,
PRE composition rejects any persisted/catalog mismatch instead of silently refreshing it.
- [ ] Implement `NotificationPlanPort` by converting the selected adapter-local compiled binding into
Static catalog/digest/key-set validation is implemented; persisted-registry comparison and
bootstrap enforcement remain explicitly deferred to Task 17.
- [x] Implement `NotificationPlanPort` by converting the selected adapter-local compiled binding into
an application-owned `NotificationFrozenPlan`. The conversion freezes policy/route/template/
renderer/provider-leg revisions and contains no credential, SDK, settings or adapter type.
- [ ] Derive the application-owned provider capability descriptor from the actual compiled cards,
- [x] Derive the application-owned provider capability descriptor from the actual compiled cards,
renderer and client capabilities. Do not reconstruct “actual” provider facts from the expected
bootstrap settings.
- [ ] Verify GREEN with the same command.
- [ ] Acceptance claim: local graph compatibility only, not actual composition/readiness.
- [x] Verify GREEN with the same command.
- [x] Acceptance claim: local graph compatibility only, not actual composition/readiness.
**Rollback checkpoint:** compiler can coexist dark with the legacy router until Task 18 canonical
composition succeeds.
@@ -923,24 +929,24 @@ composition succeeds.
- `src/adapter/outbound/notification/src/test/resources/notification/templates/email/contract-v1.html`
- `src/adapter/outbound/notification/src/test/resources/notification/templates/slack/contract-v1.txt`
- [ ] RED cases: checksum/revision drift; missing/unknown/unused parameter; exact locale fallback
- [x] RED cases: checksum/revision drift; missing/unknown/unused parameter; exact locale fallback
independent of JVM default; email header CR/LF; HTML text/attribute/URL escaping; Slack
mrkdwn/plain-text/mention escaping; output byte/block/depth limits; no file/network/reflection
include; redacted failures.
- [ ] Verify RED:
- [x] Verify RED:
```bash
cd src && ./gradlew :adapter:outbound:notification:test \
--tests '*NotificationTemplateRendererTest' --console=plain
```
- [ ] Implement checked-in resource loading by exact manifest/checksum. Keep business-specific
- [x] Implement checked-in resource loading by exact manifest/checksum. Keep business-specific
assets out of production main resources until a consuming project supplies a reviewed catalog;
use test resources only for the generic contract proof.
- [ ] Produce local email subject/text/HTML and Slack Block Kit through typed builders; never accept
- [x] Produce local email subject/text/HTML and Slack Block Kit through typed builders; never accept
caller-supplied arbitrary JSON or provider block objects.
- [ ] Verify GREEN with the same command.
- [ ] Acceptance claim: deterministic local render R1; no provider call.
- [x] Verify GREEN with the same command.
- [x] Acceptance claim: deterministic local render R1; no provider call.
**Rollback checkpoint:** retained intent template revisions prevent later asset deletion. Before
durable append, this task is independently reversible.
@@ -973,11 +979,11 @@ durable append, this task is independently reversible.
- `src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationReconciliationAdapterTest.java`
- `src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAdmissionReadinessAdapterTest.java`
- [ ] RED cases: `prepare` has no I/O; one authorization invokes client exactly once; deadline is
- [x] RED cases: `prepare` has no I/O; one authorization invokes client exactly once; deadline is
absolute/bounded; pre-wire validation maps to definitely-not-applied; possible write timeout
maps indeterminate; accepted response stores only opaque provider reference; SDK exceptions
never escape to application.
- [ ] Verify RED:
- [x] Verify RED:
```bash
cd src && ./gradlew :adapter:outbound:notification:test \
@@ -989,40 +995,40 @@ durable append, this task is independently reversible.
--console=plain
```
- [ ] Keep this SPI adapter-internal. Implement the application
- [x] Keep this SPI adapter-internal. Implement the application
`NotificationProviderAttemptPort` with compiled binding + renderer + internal client lookup.
- [ ] Keep secret material resolution, control-plane readiness and provider-local rate admission
- [x] Keep secret material resolution, control-plane readiness and provider-local rate admission
behind adapter-owned interfaces. Profiles contain secret references/generations only; readiness
snapshots contain bounded non-secret identity/capability facts.
- [ ] Secret acquisition returns a versioned `AutoCloseable` mutable byte/char handle. Acquire it per
- [x] Secret acquisition returns a versioned `AutoCloseable` mutable byte/char handle. Acquire it per
provider operation, close it on success/exception/cancellation, wipe on close, reject use after
close, and redact `toString`/exceptions. Never store the raw token in an adapter-owned
record/String/settings field; the wipe claim covers only the adapter-facing mutable copy.
- [ ] Implement the application-owned `NotificationAdmissionReadinessPort` with the adapter-internal
- [x] Implement the application-owned `NotificationAdmissionReadinessPort` with the adapter-internal
readiness probes. Application admission use cases must never import the internal probe type.
- [ ] Freeze the outbound binding matrix:
- [x] Freeze the outbound binding matrix:
`NotificationPlanPort -> NotificationPlanAdapter`,
`InlineNotificationAttemptPort -> InlineNotificationAttemptAdapter`,
`NotificationProviderAttemptPort -> NotificationProviderAttemptAdapter`,
`NotificationReconciliationPort -> NotificationReconciliationAdapter`,
`NotificationAdmissionReadinessPort -> NotificationAdmissionReadinessAdapter`.
Every implementation has a focused contract test before composition.
- [ ] Keep attempt correlation, optional provider operation key and post-response message reference
- [x] Keep attempt correlation, optional provider operation key and post-response message reference
as distinct types.
- [ ] Verify GREEN with the same command and:
- [x] Verify GREEN with the same command and:
```bash
cd src && ./gradlew :adapter:outbound:notification:check --console=plain
```
- [ ] Acceptance claim: deterministic fake protocol R1, no exact provider card qualification.
- [x] Acceptance claim: deterministic fake protocol R1, no exact provider card qualification.
**Rollback checkpoint:** no network resources are created until a canonical profile is bound in
Task 18.
### Wave B exit gate
- [ ] Run:
- [x] Run:
```bash
cd src && ./gradlew :application-core:check \
@@ -1031,10 +1037,11 @@ Task 18.
--console=plain
```
- [ ] Verify the notification leaf has no project dependency on persistence, inbound-web or
- [x] Verify the notification leaf has no project dependency on persistence, inbound-web or
httpclient.
- [ ] Request catalog/template/provider-SPI review.
- [ ] Update the LLM Wiki branch-note with Wave B evidence and an explicit derived-document decision.
- [x] Request catalog/template/provider-SPI review.
Final independent re-review: Blocker 0 / High 0.
- [x] Update the LLM Wiki branch-note with Wave B evidence and an explicit derived-document decision.
---