# Outbox Transport-Only Cutover Implementation Plan **Goal:** Preserve canonical integration-event bytes and metadata inside the existing legacy `outbox_event` authority, then route canonical claimed rows through the messaging platform while legacy rows keep the current broker path. **Spec:** `docs/superpowers/specs/2026-09-18-outbox-transport-only-cutover-design.md` ## Constraints - Preserve all existing uncommitted work. No reset/checkout/stage/commit/amend/push. - Do not activate or switch to `POLLING_V2`. - Do not synthesize missing canonical metadata for legacy rows. - Do not re-encode a persisted canonical envelope. - One claimed row goes through exactly one publish branch. - Default configuration remains legacy-compatible and canonical transport is off. ### Task 1 — Split canonical and legacy append ports - [x] Create `LegacyOutboxAppendPort` with the current `NewOutboxEvent` signature. - [x] Change `OutboxAppendPort` to accept `ValidatedIntegrationEvent`. - [x] Move all current raw production consumers and their tests to `LegacyOutboxAppendPort`. - [x] Make `OutboxStoreAdapter` implement `LegacyOutboxAppendPort` only. - [x] Run `:application-core:test` and focused sample/outbox compile tests. ### Task 2 — Add additive canonical columns to `outbox_event` - [x] Add the next PostgreSQL migration after V12. - [x] Widen event/correlation identifiers as required. - [x] Add canonical metadata, exact `BYTEA`, hashes/revisions, and all-or-none check constraints. - [x] Extend `OutboxEventEntity` mappings. - [x] Update migration history expectations. - [x] Add real PostgreSQL integration assertions for legacy rows and canonical shape constraints. ### Task 3 — Implement canonical append adapter - [x] Add `CanonicalOutboxAppendAdapter`. - [x] Strictly validate UTF-8 compatibility projection. - [x] Persist every canonical field and exact `envelopeBytes`. - [x] Preserve old required columns for legacy relay/storage compatibility. - [x] Add unit tests for byte equality, field mapping and invalid UTF-8. - [x] Gate bean exposure on `ca-skeleton.outbox.canonical-transport-enabled=true`. ### Task 4 — Split the claimed row model - [x] Add sealed `ClaimedOutboxEvent`. - [x] Keep `OutboxEvent` as legacy subtype. - [x] Add `CanonicalClaimedOutboxEvent` carrying reconstructed `ValidatedIntegrationEvent`. - [x] Change `OutboxStorePort.claimBatch` to return the sealed type. - [x] Map all-canonical rows to canonical subtype and all-null rows to legacy subtype. - [x] Reject partial canonical rows. - [x] Update legacy relay tests without changing its state-machine semantics. ### Task 5 — Route canonical claims through the platform - [x] Update `OutboxMessagePublishPort` to accept `ClaimedOutboxEvent`. - [x] Extend `OutboxMessagePublishAdapter` with canonical `IntegrationEventPublishPort`. - [x] Legacy subtype uses only `MessageBroker`. - [x] Canonical subtype uses only the application canonical publish port and exact stored bytes. - [x] Add focused branch-isolation and outcome tests. ### Task 6 — Add explicit activation and composition validation - [x] Add `canonicalTransportEnabled` to `OutboxSettings` and `config/outbox.yml`, default false. - [x] Startup fails when canonical transport is enabled but no `IntegrationEventPublishPort` exists. - [x] Relay-enabled compatibility deployment still requires the legacy broker until a later zero-legacy-backlog proof. - [x] Default-off composition keeps the existing legacy path. - [x] Enabled composition exposes the canonical append/publish path without a second scheduler. - [x] Update configuration docs/SSOT. ### Task 7 — Regression and architecture verification - [x] `:application-core:check` - [x] `:adapter:outbound:persistence-jpa:check` - [x] focused PostgreSQL migration/outbox integration lane - [x] `:adapter:outbound:messaging:check` - [ ] `:sample-portfolio:check` — blocked by pre-existing `JpaLiveEventReplayAdapter` missing `Duration` wiring; the same 3 `SampleApplicationContextTest` failures reproduce on clean HEAD. - [x] focused sample outbox regression tests (`PosterEventPublisherTest`, `CreateWorkLogOutboxTest`, `WorkLogUseCasesTest`, `WorkLogAuthorizationContractTest`) - [x] `:app-bootstrap:architectureTest :app-bootstrap:systemTest` - [x] `verifyCleanArchitectureDependencies` - [x] `:app-bootstrap:verifyEnvKeys` - [x] static scans: no platform runtime/Kafka import in canonical bridge - [x] `git diff --check` - [x] no separate LLM Wiki branch-note warranted; spec, plan, module README/CLAUDE and configuration reference carry the implementation decision. ## Verification result Transport-only cutover implementation is complete for this slice. Passed: - `:application-core:check` - `:adapter:outbound:persistence-jpa:check` - `:adapter:outbound:persistence-jpa:postgresqlMigrationIntegrationTest` - `:adapter:outbound:messaging:check` - focused sample outbox regression tests - `:app-bootstrap:test` - focused `:app-bootstrap:integrationTest` outbox append + row-lifecycle contracts - `:app-bootstrap:architectureTest` - `:app-bootstrap:systemTest` - `verifyCleanArchitectureDependencies` - `:app-bootstrap:verifyEnvKeys` - `git diff --check` Known unrelated blocker: - full `:sample-portfolio:check` still fails only the 3 previously documented `SampleApplicationContextTest` cases because `JpaLiveEventReplayAdapter` requires an unbound `Duration` bean. This reproduces on clean HEAD and was not introduced by this cutover. Publication authority remains `LEGACY_POLLING`; no code path in this slice activates `POLLING_V2`.