117 lines
11 KiB
Markdown
117 lines
11 KiB
Markdown
# Notification Delivery Platform — module mapping
|
|
|
|
> Source design: `notification-superpowers-package/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`
|
|
|
|
## 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.
|