Files

117 lines
11 KiB
Markdown

# Notification Delivery Platform — module mapping
> Source design: `docs/superpowers/specs/2026-08-10-notification-platform-design.md`
>
> Source plan: `docs/superpowers/plans/2026-08-10-notification-platform-implementation-plan.md`
## Why a mapping exists
The plan was written against a hypothetical repository (`modules/notification/**`, root package
`io.backend.skeleton.notification`, 31 Gradle projects). This repository is a fail-closed
19-leaf Clean Architecture template: `src/settings.gradle` rejects any registry that does not
contain exactly the 19 modules in `src/config/architecture/modules.json`, and
`verifyCleanArchitectureDependencies` rejects any project edge outside `allowed_dependencies`.
Creating 31 new Gradle projects would violate HARD-STOP #5 of `AGENTS.md`. The package README
anticipates this and instructs the implementer to map dependency catalog and package/file paths onto
the host repository's rules while preserving the public contracts and reliability semantics.
Every logical module of the plan is therefore implemented as a **package** inside the registered leaf
that owns its responsibility. No public contract, evidence rule, or reliability semantic is dropped.
## Logical module → registered leaf
| Plan module | Registered leaf | Package |
|---|---|---|
| `notification-core-api` | `application-core` | `dev.caskeleton.application.notification.platform.api` |
| `notification-content-api` | `application-core` | `…platform.api.content` |
| `notification-contact-api` | `application-core` | `…platform.contact` |
| `notification-template-api` | `application-core` | `…platform.template` |
| `notification-policy` | `application-core` | `…platform.policy` |
| `notification-provider-spi` | `application-core` | `…platform.provider` |
| `notification-callback-api` | `application-core` | `…platform.callback` |
| `notification-email-api` | `application-core` | `…platform.email` |
| `notification-sms-api` | `application-core` | `…platform.sms` |
| `notification-push-api` | `application-core` | `…platform.push` |
| `notification-webpush` (API half) | `application-core` | `…platform.webpush` |
| `notification-inbox-api` | `application-core` | `…platform.inbox` |
| `notification-admin-api` | `application-core` | `…platform.admin` |
| `notification-security` (ports + redaction) | `application-core` | `…platform.security` |
| `notification-observability` (ports) | `application-core` | `…platform.observation` |
| `notification-dispatch-runtime` | `adapter:outbound:notification` | `dev.caskeleton.adapter.outbound.notification.platform.dispatch` |
| `notification-security` (AES-GCM/HMAC impl) | `adapter:outbound:notification` | `…platform.security` |
| `notification-template-thymeleaf` (reference renderer) | `adapter:outbound:notification` | `…platform.template` |
| `notification-email-smtp` | `adapter:outbound:notification` | `…platform.provider.smtp` |
| `notification-email-ses` | `adapter:outbound:notification` | `…platform.provider.ses` |
| `notification-sms-twilio` | `adapter:outbound:notification` | `…platform.provider.twilio` |
| `notification-push-fcm` | `adapter:outbound:notification` | `…platform.provider.fcm` |
| `notification-push-apns` | `adapter:outbound:notification` | `…platform.provider.apns` |
| `notification-webpush` (transport + crypto) | `adapter:outbound:notification` | `…platform.provider.webpush` |
| `notification-webhook-extension` | `adapter:outbound:notification` | `…platform.provider.webhook` |
| `notification-observability` (Micrometer impl) | `adapter:outbound:notification` | `…platform.observation` |
| `notification-admin-runtime` | `adapter:outbound:notification` | `…platform.admin` |
| `notification-reactor` | `adapter:outbound:notification` | `…platform.reactor` |
| `notification-spring-boot-starter` | `adapter:outbound:notification` (+ `app-bootstrap` wiring) | `…platform.autoconfigure` |
| `notification-persistence-jpa` | `adapter:outbound:persistence-jpa` | `dev.caskeleton.adapter.outbound.persistence.notification.platform` |
| `notification-inbox-jpa` | `adapter:outbound:persistence-jpa` | `…persistence.notification.platform.inbox` |
| `notification-callback-mvc` | `adapter:inbound:web` | `dev.caskeleton.adapter.inbound.web.notification.platform.callback` |
| `notification-callback-webflux` | `adapter:inbound:web` | `…callback.reactive` |
| `notification-testkit` | test source sets of the owning leaves | `…platform.testkit` |
## Dependency-direction consequences
The plan's module DAG (`*-api``provider-spi`/`policy` → runtime/adapters → starter) is preserved
by the leaf DAG that the registry already enforces:
```text
application-core (all *-api, provider SPI, policy, callback contracts)
↑ ↑ ↑
adapter:outbound:notification adapter:outbound:persistence-jpa adapter:inbound:web
↑ ↑ ↑
app-bootstrap
```
Two plan edges cannot be expressed as project edges in this repository, and are replaced by ports:
1. `notification-email-ses`, `notification-sms-twilio`, `notification-push-fcm`,
`notification-push-apns`, `notification-webpush`, `notification-webhook-extension`
`httpclient platform`.
`adapter-outbound-notification` is not allowed to depend on `adapter-outbound-httpclient`.
The provider adapters therefore call
`dev.caskeleton.adapter.outbound.notification.platform.provider.http.NotificationHttpGateway`,
an adapter-local port with a JDK `java.net.http.HttpClient` default implementation.
`app-bootstrap` sees both leaves and is the supported place to substitute an implementation backed
by the HTTP Client Platform (TLS/timeout/circuit-breaker/SSRF/dynamic-target policy reuse).
2. `notification-inbox-jpa``optional messaging outbox integration`.
`adapter-outbound-persistence-jpa` may not depend on `adapter-outbound-messaging`; the inbox
publishes through the existing persistence outbox tables plus the
`NotificationInboxSignalPort` application port, and `app-bootstrap` binds the relay.
## Commit policy
`AGENTS.md` pins commit policy to `human-only`. Step 5 (`git add` / `git commit`) of every plan task
is therefore intentionally **not** executed by the agent; the working tree carries the change and the
human owner commits.
## R1 public type disposition (NOTIF-ADR-005)
NOTIF-ADR-005 makes `..notification.platform..` canonical and keeps the R1 namespace in place
without new consumers. The ADR owns the rule; this table owns the list, so the two do not drift
apart by being written twice.
`NOTIFICATION_R1_AND_PLATFORM_DO_NOT_DEPEND_ON_EACH_OTHER` in `CleanArchitectureTest` enforces the
boundary: production dependencies between the two namespaces are zero, and the only permitted
exception is `dev.caskeleton.application.notification.compatibility.r1`.
| Disposition | Meaning | Types |
| --- | --- | --- |
| `replace` (27) | the platform has an equivalent; new consumers use it | `ApplyNotificationReceiptCommand`, `ApplyNotificationReceiptResult`, `ApplyNotificationReceiptUseCase`, `Channel`, `InlineNotificationAttemptPort`, `NormalizedNotificationReceiptCommand`, `NotificationAttemptId`, `NotificationDeliveryId`, `NotificationDeliveryStorePort`, `NotificationDispatchCommand`, `NotificationDispatchResult`, `NotificationDispatchUseCase`, `NotificationEvidenceTrustSnapshot`, `NotificationFrozenPlan`, `NotificationPlanPort`, `NotificationPlanningResult`, `NotificationProviderAttemptPort`, `NotificationReceiptEventId`, `NotificationReceiptFact`, `NotificationReceiptIngressCapabilityDescriptor`, `NotificationReceiptProjection`, `NotificationReceiptStorePort`, `NotificationSignedEvidenceHeader`, `NotificationWriterInventoryEvidence`, `NotificationWriterInventoryEvidenceVerifierPort`, `ProviderAttemptOutcome`, `TargetAttemptOutcome` |
| `bridge` (28) | conversion needed if an R1 caller remains; conversion lives only in the ACL | `InitializeNotificationWriterFencesCommand`, `InitializeNotificationWriterFencesOperation`, `InitializeNotificationWriterFencesResult`, `InitializeNotificationWriterFencesUseCase`, `NotificationAdmissionGateCommand`, `NotificationAdmissionGateUseCase`, `NotificationAppendResult`, `NotificationLegacyWriterPermitCommand`, `NotificationLegacyWriterPermitResult`, `NotificationLegacyWriterPermitUseCase`, `NotificationMaintenanceCommand`, `NotificationMaintenanceResult`, `NotificationMaintenanceUseCase`, `NotificationOperationsSnapshot`, `NotificationOperationsSnapshotQuery`, `NotificationOperationsSnapshotUseCase`, `NotificationRequestResult`, `ReconcileNotificationDeliveriesCommand`, `ReconcileNotificationDeliveriesResult`, `ReconcileNotificationDeliveriesUseCase`, `SwitchNotificationWriterOwnershipCommand`, `SwitchNotificationWriterOwnershipOperation`, `SwitchNotificationWriterOwnershipResult`, `SwitchNotificationWriterOwnershipUseCase`, `TerminalizeExpiredNotificationWriterPermitsCommand`, `TerminalizeExpiredNotificationWriterPermitsOperation`, `TerminalizeExpiredNotificationWriterPermitsResult`, `TerminalizeExpiredNotificationWriterPermitsUseCase` |
| `retain` (45) | a concern the platform does not cover; left as it is | `ConsentCheckMode`, `EmailRecipientReference`, `Notification`, `NotificationAdmissionClass`, `NotificationAdmissionReadinessPort`, `NotificationApplicationException`, `NotificationCanonicalWriterFenceGuard`, `NotificationCanonicalWriterFencePort`, `NotificationCanonicalWriterRouteSet`, `NotificationCapabilityCompatibilityValidator`, `NotificationChannel`, `NotificationFaultScope`, `NotificationIntentAppendPort`, `NotificationIntentDraft`, `NotificationIntentId`, `NotificationKindId`, `NotificationKindPolicy`, `NotificationMaintenanceStorePort`, `NotificationMode`, `NotificationOperationsSnapshotPort`, `NotificationPort`, `NotificationProviderCapabilityDescriptor`, `NotificationReasonCode`, `NotificationRecipientReference`, `NotificationReconciliationPort`, `NotificationRouteId`, `NotificationRouteStrategy`, `NotificationStoreCapabilityDescriptor`, `NotificationTechnicalSuppressionPort`, `NotificationTemplateParameters`, `NotificationTemplateRef`, `NotificationTemplateValue`, `NotificationWriterCutoverPort`, `NotificationWriterOwnership`, `NotificationWriterQuiescenceAttestationPort`, `NotificationWriterRouteSet`, `RecordNotificationWriterQuiescenceAttestationCommand`, `RecordNotificationWriterQuiescenceAttestationOperation`, `RecordNotificationWriterQuiescenceAttestationResult`, `RecordNotificationWriterQuiescenceAttestationUseCase`, `RetryDisposition`, `SignedNotificationWriterInventoryManifest`, `SignedNotificationWriterQuiescenceManifest`, `SlackAudienceReference`, `SubmissionCertainty` |
Total: 100 public types, every one classified.
No type carries `@Deprecated(forRemoval = true)`: no removal release is fixed, and
`forRemoval` without a date is a promise the codebase cannot keep. The boundary is
enforced by the ArchUnit rule instead.