refactor: 빌드 로직 개선, gradle 파일 경량화

This commit is contained in:
donghyeon-ka
2026-09-18 15:22:30 +09:00
parent ace8aaaef6
commit 60b6a319e7
397 changed files with 19320 additions and 8406 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ Docker-dependent lanes fail closed rather than skipping, matching the existing
| `settings.gradle.kts` module registration | Fail-closed 19-leaf registry | No registry change: leaf identity, Gradle path, allowed dependencies, and runtime memberships are unchanged. |
| `infra/jpa/{postgres,roles,toxiproxy}` | Repository already owns `infra/` | Created at the same repository-relative paths. |
| `docs/jpa/**`, `docs/adr/ADR-JPA-*`, `.github/workflows/jpa-*.yml` | Repository already owns `docs/` and `.github/workflows/` | Created at the same repository-relative paths. |
| `build.gradle.kts` release aggregate `jpaReleaseGate` | Root is `src/build.gradle` | Registered there against the repository lane names in §3. |
| release blocking aggregate | `.github/workflows/jpa-release.yml` | CI names the blocking JPA lanes directly; Gradle only defines how each lane runs. |
| Per-task `git add` + `git commit` | `AGENTS.md`: commit policy is `human-only`; agents do not stage, commit, amend, or push | Implementation is delivered unstaged. This is the only plan step intentionally not executed, and it is recorded here. |
| Querydsl as an optional module dependency | Querydsl is not part of this repository's dependency set | `querydsl` is implemented against the plan's contracts with the Querydsl types kept behind `compileOnly`, so the Stable runtime classpath never carries Querydsl and a deployment opting in adds the artifact itself. |
| Hibernate Envers as a module dependency | Envers is not part of this repository's dependency set | Same treatment as Querydsl: `compileOnly` + explicit opt-in, matching the plan's "Envers is opt-in and never enabled by a global base class". |
+23
View File
@@ -13,6 +13,29 @@
> 어떤 binder도 그것을 읽지 않았다 — 문서대로 설정한 배포는 아무것도 바뀌지 않았고 아무 말도 듣지
> 못했다 (MSG-008).
## Application publish bridge identity
Application의 canonical integration event를 platform publish pipeline으로 보낼 때는
`app.messaging.producer-id`를 명시한다. 같은 값의 환경변수 이름은
`APP_MESSAGING_PRODUCER_ID`다. 이 값은 host/pod 이름이 아니라 배포와 무관하게 유지되는 논리적
producing-service identity다.
값이 없으면 `IntegrationEventPublishPort` bridge 자체를 만들지 않는다. `spring.application.name`이나
현재 process 이름으로 추론하지 않는다. 기존 legacy `OutboxEvent`/realtime 경로는 별도 cutover가
끝날 때까지 `app.messaging.broker` 경로를 유지한다.
## Outbox canonical transport-only cutover
`APP_OUTBOX_CANONICAL_TRANSPORT_ENABLED` / `ca-skeleton.outbox.canonical-transport-enabled`
기존 `outbox_event` writer/claim/status authority를 유지한 채 canonical row의 **transport만** platform
publish path로 보내는 compatibility gate다. 기본값은 `false`이며 `POLLING_V2`를 활성화하지 않는다.
`true`일 때는 `OutboxAppendPort``ValidatedIntegrationEvent`의 exact envelope bytes와 canonical
metadata를 기존 outbox row에 저장하고, claim된 canonical row는 `IntegrationEventPublishPort`로 간다.
legacy row는 계속 `MessageBroker`를 사용한다. 따라서 mixed-row compatibility 기간에는 relay가 켜져
있다면 `app.messaging.broker`도 계속 필요하며, canonical path를 위해 `IntegrationEventPublishPort`
추가로 필요하다. legacy backlog가 0이라는 별도 증거 없이 broker 요구를 제거하지 않는다.
## Destination profile
```yaml
+31
View File
@@ -3325,6 +3325,21 @@ env_keys:
compatibility_impact: behavior-change
required_test: adapter-contract:messaging-broker-selection
- name: APP_MESSAGING_PRODUCER_ID
# source: canonical messaging platform bridge 2026-09-18
# Explicit logical producing-service identity for IntegrationEventPublishPort.
# Blank/absent = canonical platform bridge is not exposed; identity is never inferred.
type: string
default: null
allowed_values: null
classification: public-config
required: false
reload_policy: restart-only
owner_branch: feature-integration-adapter-templates
validation: none
compatibility_impact: additive
required_test: adapter-contract:messaging-platform-producer-id
- name: APP_MESSAGING_KAFKA_BROKERS
# source: feature-domain-event-outbox-contract — "Kafka는 optional integration adapter"
# (broker 활성화 시 endpoint 필요)
@@ -4358,6 +4373,22 @@ env_keys:
validation: boolean
compatibility_impact: behavior-change
required_test: adapter-contract:outbox-capability-disabled-safe
- name: APP_OUTBOX_CANONICAL_TRANSPORT_ENABLED
# source: MSG-015 transport-only cutover 2026-09-18
# Enables canonical outbox rows/platform transport without switching publication authority.
type: boolean
default: false
allowed_values:
- "true"
- "false"
classification: public-config
required: false
reload_policy: restart-only
owner_branch: feature-integration-adapter-templates
validation: boolean
compatibility_impact: additive
required_test: app-bootstrap:outbox-canonical-transport-gate
- 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
@@ -0,0 +1,102 @@
# JPA Evidence Gradle Model Decoupling Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove execution-time Gradle `Project`/`Task`/`TaskState` access from JPA evidence generation without changing evidence semantics.
**Architecture:** A shared `JpaEvidenceExecutionService` consumes Gradle task-completion events for non-Test task claims. The JPA evidence plugin snapshots/configures JUnit result directories, provenance, environment/profile values, and dependency versions as typed task inputs. `GenerateJpaEvidenceManifestsTask` becomes a pure evidence assembler over those inputs plus filesystem/exec services.
**Tech Stack:** Java 21, Gradle 9 BuildService + Tooling Events, JUnit 6/TestKit, Jackson 3.
**Spec:** `docs/superpowers/specs/2026-09-17-jpa-evidence-gradle-model-decoupling-design.md`
## Global Constraints
- Preserve readiness-card schema and task names.
- Preserve JUnit XML as test evidence.
- Preserve task-claim meaning: only a successful producer task covers a task claim.
- Preserve evidence grade, blocker, hashing, prerequisite, output, candidate/R2 semantics.
- No execution-time `Project`, `Task`, or `TaskState` access in `GenerateJpaEvidenceManifestsTask`.
- Do not stage, commit, amend, reset, or push existing worktree changes.
---
### Task 1: Task completion evidence service
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceExecutionService.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTaskOutcome.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaEvidenceExecutionServiceTest.java`
**Interfaces:**
- Produces: `JpaEvidenceExecutionService.outcome(String taskPath)` and `completedSuccessfully(String taskPath)`.
- Consumes: Gradle `TaskFinishEvent` via `OperationCompletionListener`.
- [ ] Write tests for success, failure, skipped, and unknown task paths.
- [ ] Verify tests fail because the service/model do not exist.
- [ ] Implement the typed outcome model and thread-safe service.
- [ ] Verify focused tests pass.
### Task 2: Typed JUnit evidence inputs
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTestResultLocator.java`
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTaskSupport.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTestResultLocatorTest.java`
**Interfaces:**
- Consumes: `Map<String, String>` task-path to repository-relative/absolute JUnit XML directory.
- Produces: `JpaGeneratedTestResult read(String taskPath)` without `Project` or `Test`.
- [ ] Write a failing test that creates JUnit XML under a temporary directory and resolves it by task path.
- [ ] Implement file-based result lookup using `JUnitEvidenceReader`.
- [ ] Remove the `readJUnitResult(Project, Test)` helper once no caller remains.
- [ ] Verify focused tests pass.
### Task 3: Generator typed input surface
**Files:**
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/GenerateJpaEvidenceManifestsTask.java`
- Test: extend `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaEvidencePluginTypeTest.java`
**Interfaces:**
- Add typed properties for profile/CI/artifact/topology/provenance/dependency versions/JUnit result directories.
- Add an internal/service reference to `JpaEvidenceExecutionService`.
- [ ] Add reflection/type tests asserting the new task properties exist and no generator source contains `getProject()` or `Task.getState()` usage.
- [ ] Verify the test fails against the current generator.
- [ ] Add the typed properties and service reference.
- [ ] Replace Project/Task/TaskState/configuration/extra-property reads with typed inputs/service lookups.
- [ ] Verify focused tests pass.
### Task 4: Plugin wiring
**Files:**
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidencePlugin.java`
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTaskSupport.java`
- Test: add `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaEvidencePluginFunctionalTest.java`
**Interfaces:**
- Register shared execution service and task-completion listener.
- Configure generator typed inputs.
- Configure JUnit result-directory mapping for active readiness/support Test tasks.
- Preserve existing `dependsOn` producer graph.
- [ ] Write TestKit fixture asserting typed generator inputs and task wiring.
- [ ] Verify RED.
- [ ] Register/wire the service and all generator properties.
- [ ] Resolve dependency versions and release provenance during configuration/plugin wiring rather than task action.
- [ ] Verify TestKit GREEN.
### Task 5: Regression and Gradle 10-preparation verification
**Files:**
- Modify only if verification exposes a regression.
- [ ] Run `src/build-tools` full `check --warning-mode=fail`.
- [ ] Run root `verifyJpaReadinessRegistry verifyJpaReleaseGateTasks --warning-mode=fail`.
- [ ] Run `:adapter:outbound:persistence-jpa:check --warning-mode=fail`.
- [ ] Run candidate evidence generation with `--warning-mode=all`; verify there is no `Task.project`/execution-time project deprecation from JPA evidence tooling.
- [ ] Run adapter procedural-Groovy scan and confirm no regression.
- [ ] Run `git diff --check`.
- [ ] Record any environment-only Docker/Testcontainers limitation separately from code correctness.
@@ -0,0 +1,46 @@
# JPA Leaf Verification Java Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove the remaining procedural JPA leaf verification logic from Groovy while deleting a redundant verifier-of-verifier task.
**Architecture:** Keep the security scenario as a declarative `strictTestLanes.requires(...)` contract and remove `verifyJpaSecurityFixtures` from both the leaf and readiness registry. Move the real PostgreSQL `set_config` source-safety rule into a typed task/verifier owned by the already-applied `ca.jpa-evidence` Java plugin.
**Tech Stack:** Java 21, Gradle 9 binary plugins/tasks, JUnit 6.
**Spec:** `docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md`
## Global Constraints
- Preserve the `verifyJpaSqlConstructionSafety` task name because readiness registry/evidence tooling references it.
- Preserve the PostgreSQL security method selector on `postgresqlSecurityBaselineIntegrationTest`.
- Remove `verifyJpaSecurityFixtures` only together with its readiness-card support-task reference.
- Do not add production dependencies or change JPA runtime behavior.
- Do not stage, commit, amend, or push.
---
### Task 1: Typed SQL construction safety verifier
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaSqlConstructionSafetyVerifier.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaSqlConstructionSafetyResult.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaSqlConstructionSafetyVerifierTest.java`
- [ ] **Step 1:** Write RED tests for parameterized `set_config`, non-parameterized `set_config`, comments, and nested source paths.
- [ ] **Step 2:** Implement the minimal typed verifier preserving the current line-based rule.
- [ ] **Step 3:** Run the focused verifier tests to GREEN.
### Task 2: Java task ownership and redundant task removal
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/VerifyJpaSqlConstructionSafetyTask.java`
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidencePlugin.java`
- Modify: `src/adapter/outbound/persistence-jpa/build.gradle`
- Modify: `src/config/jpa/readiness-cards.yaml`
- [ ] **Step 1:** Register `verifyJpaSqlConstructionSafety` as a typed task from `JpaEvidencePlugin`.
- [ ] **Step 2:** Delete the Groovy implementation of `verifyJpaSqlConstructionSafety`.
- [ ] **Step 3:** Delete `verifyJpaSecurityFixtures` and remove it from the security card support tasks while keeping the `requires(...)` selector.
- [ ] **Step 4:** Verify readiness registry and SQL-safety tasks.
- [ ] **Step 5:** Run `:adapter:outbound:persistence-jpa:check`, build-tools tests, and `git diff --check`.
@@ -0,0 +1,19 @@
# JPA Test Lanes Java Convention Plan
**Goal:** Remove the remaining Map-driven JPA lane factories from Groovy and make lane metadata compile-time checked Java records.
**Architecture:** `ca.jpa-test-lanes` lives in build-logic and configures `ca.strict-test-lane`. Typed record lists own the PostgreSQL readiness and tagged platform lane metadata. The JPA leaf keeps source-set/dependency declarations plus the explicit cross-project task edge.
## Constraints
- Preserve all 14 PostgreSQL readiness task names and selectors.
- Preserve the security method selector in addition to its class selector.
- Preserve five tagged platform lanes and the pool contract lane.
- Preserve UTC JVM args, PostgreSQL evidence image property, and `jpa.matrix.versions` default `16`.
- Keep release orchestration outside the leaf.
- Do not stage, commit, amend, or push.
## Steps
- [ ] RED TestKit contract for registered lanes, typed selector metadata, and property defaults.
- [ ] Implement Java record-backed `JpaTestLanesPlugin` and register `ca.jpa-test-lanes`.
- [ ] Apply plugin in persistence-jpa and remove both Groovy `Map` factories plus pool-lane Groovy configuration.
- [ ] Run focused build-logic tests, JPA check, adapter dynamic-model scan, and broad verification.
@@ -0,0 +1,61 @@
# Mongo Gradle Verification Java Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove Mongo JUnit/XML and release-manifest verification algorithms from the Groovy leaf build script and move them into typed Java build tooling without changing task names or verification semantics.
**Architecture:** `ca.mongo-verification` lives in `src/build-tools` because the checks are repository certification tooling, not reusable compilation conventions. The plugin registers the two existing verification task names; normal Java verifier/parser classes own XML/JSON parsing and return typed records, while `persistence-mongo/build.gradle` keeps only plugin/lane/dependency declarations and `check` wiring.
**Tech Stack:** Java 21, Gradle 9 binary plugins/tasks, Jackson 3, JUnit 6, JUnit XML.
**Spec:** `docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md`
## Global Constraints
- Preserve `verifyMongoTestLaneDisjointness` and `verifyMongoReleaseContractLanes` task names and report paths.
- Preserve the existing `test` + `mongoStableContractTest` dependency graph.
- Do not add or resolve new production dependencies in the Mongo leaf.
- Keep Groovy only as declarative build DSL; no JSON/XML parsing or `doLast` verification algorithm remains in the leaf.
- Do not stage, commit, amend, or push; repository policy is human-only commits.
---
### Task 1: Typed Mongo verification core
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoLaneDisjointnessVerifier.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoLaneDisjointnessResult.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoReleaseContract.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoReleaseContractManifestParser.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoReleaseContractLaneVerifier.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoReleaseContractLaneResult.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/mongo/MongoLaneDisjointnessVerifierTest.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/mongo/MongoReleaseContractLaneVerifierTest.java`
**Interfaces:**
- Consumes: Gradle JUnit XML result directories and `config/mongodb/release-contracts.json`.
- Produces: typed result records used by Gradle task classes.
- [ ] **Step 1: Write failing verifier tests** covering disjoint lanes, overlap failure data, manifest filtering to hermetic lanes, missing result XML, and minimum-executed checks.
- [ ] **Step 2: Run** `cd src/build-tools && ../gradlew test --tests 'dev.caskeleton.buildtools.mongo.*' --console=plain` and confirm RED from missing production types.
- [ ] **Step 3: Implement minimal typed records/parsers/verifiers** using fail-closed XML parsing and Jackson 3 JSON tree parsing.
- [ ] **Step 4: Run the focused tests again** and confirm PASS.
### Task 2: Binary plugin/task ownership and leaf cleanup
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoVerificationPlugin.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/VerifyMongoTestLaneDisjointnessTask.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/VerifyMongoReleaseContractLanesTask.java`
- Modify: `src/build-tools/build.gradle`
- Modify: `src/adapter/outbound/persistence-mongo/build.gradle`
**Interfaces:**
- Consumes: typed verifiers from Task 1.
- Produces: existing task names `verifyMongoTestLaneDisjointness`, `verifyMongoReleaseContractLanes` with unchanged report paths.
- [ ] **Step 1: Register `ca.mongo-verification`** and the two typed tasks in Java.
- [ ] **Step 2: Apply the plugin in the Mongo leaf and delete both Groovy `tasks.register { doLast { ... } }` implementations.**
- [ ] **Step 3: Run** `cd src && ./gradlew :adapter:outbound:persistence-mongo:verifyMongoTestLaneDisjointness :adapter:outbound:persistence-mongo:verifyMongoReleaseContractLanes --console=plain`.
- [ ] **Step 4: Run** `cd src && ./gradlew :adapter:outbound:persistence-mongo:check --console=plain`.
- [ ] **Step 5: Run** `cd src/build-tools && ../gradlew test --console=plain` and `git diff --check`.
@@ -0,0 +1,29 @@
# Redis Topology Lane Java Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Remove the last adapter-level procedural Groovy test lifecycle logic by moving Redis topology qualification into a typed Java convention plugin.
**Architecture:** `ca.redis-topology-lane` belongs in `build-logic`: it configures the default test exclusion, declares the strict `redisTopologyTest` lane, validates mode/properties, forwards topology system properties, and verifies executed required classes/no skips. The Redis leaf keeps only plugin and dependency declarations.
**Tech Stack:** Java 21, Gradle 9, JUnit Platform test tasks.
**Spec:** `docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md`
## Constraints
- Preserve supported modes: standalone, sentinel, cluster, tls.
- Preserve TLS deployment-mode mapping to standalone.
- Preserve required property and required executed-class semantics.
- Preserve fail-closed behavior for unknown mode and skipped topology tests.
- Keep `redisTopologyTest` opt-in; do not add it to normal `check`.
- Do not stage, commit, amend, or push.
### Task 1: Typed topology contract
- [ ] Write failing Java tests for mode validation, required properties, class coverage, and skipped-test rejection.
- [ ] Implement typed contract/result records and get focused tests GREEN.
### Task 2: Java convention plugin
- [ ] Implement `RedisTopologyLanePlugin` using `StrictTestLaneExtension` and a Java `TestListener` tracker.
- [ ] Register `ca.redis-topology-lane` in build-logic.
- [ ] Apply it in cache-redis and remove the Groovy topology lifecycle/configuration block.
- [ ] Verify build-logic tests, Redis unit `check`, task configuration, invalid-mode fail-closed behavior, and `git diff --check`.
@@ -0,0 +1,93 @@
# Messaging Platform Bridge Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Connect the canonical application integration-event publish boundary to the messaging platform without re-encoding bytes or introducing broker-specific ownership in app-bootstrap.
**Architecture:** Add an application-owned publish port, implement it in `adapter/outbound/messaging/platformbridge`, and publish canonical pre-encoded envelopes through `EncodedMessagePublisher`. The bridge preserves identity/routing/evidence and maps platform publish evidence explicitly into application outcomes.
**Tech Stack:** Java 21, Gradle 9, Spring Boot auto-configuration, JUnit 5, AssertJ.
**Spec:** `docs/superpowers/specs/2026-09-18-messaging-platform-bridge-design.md`
## Global Constraints
- Preserve existing uncommitted changes; do not reset, stage, commit, amend, or push.
- Never invent missing canonical identity, timestamp, trace, tenant, schema, or routing values.
- Never bypass `DefaultMessagePublisher` through transport SPI or native Kafka clients.
- Preserve exact `ValidatedIntegrationEvent.envelopeBytes()`.
- Fail closed before send when event or causation identity is not UUIDv7.
- Legacy outbox storage/relay migration is outside this plan.
---
### Task 1: Application-owned canonical publish port
**Files:**
- Create: `application-core/src/main/java/dev/caskeleton/application/messaging/event/IntegrationEventPublishPort.java`
- Test: existing bridge test compile contract
**Interfaces:**
- Produces: `CompletionStage<OutboxPublishOutcome> publish(ValidatedIntegrationEvent event)`
- [x] Create the minimal application-owned interface.
- [x] Run the focused bridge test and verify remaining failures are platform dependencies/adapter implementation, not the port.
### Task 2: Outbound messaging platform API dependency
**Files:**
- Modify: `adapter/outbound/messaging/build.gradle`
- Modify: `adapter/outbound/messaging/gradle.lockfile` through Gradle lock writing
**Interfaces:**
- Consumes: `:messaging:messaging-core-api`, `:messaging:messaging-schema-api`.
- [x] Add only the platform API dependencies required by the bridge.
- [x] Refresh this module's locks.
- [x] Re-run focused bridge test and verify the missing type set is reduced to bridge production code.
### Task 3: Canonical platform bridge
**Files:**
- Create: `adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/platformbridge/PlatformIntegrationEventPublishAdapter.java`
- Test: `adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/platformbridge/PlatformIntegrationEventPublishAdapterTest.java`
**Interfaces:**
- Consumes: `IntegrationEventPublishPort`, `EncodedMessagePublisher`.
- Produces: canonical application-to-platform anti-corruption bridge.
- [x] Implement UUIDv7 parsing that rejects incompatible identity before publisher invocation.
- [x] Map canonical metadata and exact bytes into `MessageEnvelope<EncodedMessage>`.
- [x] Preserve non-first-class evidence in bounded `x-ca-*` headers.
- [x] Map `PublishResult` using transmission evidence.
- [x] Run all three focused bridge tests to GREEN.
### Task 4: Spring ownership while preserving the legacy seam
**Files:**
- Modify: `adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingSettings.java`
- Modify: `adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/autoconfigure/MessagingBridgeRootAutoConfiguration.java`
- Test: focused auto-configuration ownership test
**Interfaces:**
- Consumes: Spring-provided `EncodedMessagePublisher`, explicit `app.messaging.producer-id`.
- Produces: `IntegrationEventPublishPort` bean for canonical events.
- [x] Add explicit `producerId` to the existing `app.messaging` adapter settings.
- [x] Register the canonical bridge only when `app.messaging.producer-id` is explicitly present.
- [x] Keep `KafkaSender` / `KafkaMessageBroker` as a documented transitional dependency of legacy outbox/realtime only.
- [x] Add a Spring test proving producer-id present => one canonical bridge bean, absent => no canonical bridge bean.
- [x] Run outbound messaging tests.
### Task 5: Platform and composition regression verification
**Files:** no new production files unless a test exposes a real defect.
- [x] Run `:messaging:messaging-runtime-core:test`.
- [x] Run `:messaging:messaging-spring-boot-starter:test`.
- [x] Run `:adapter:outbound:messaging:check --warning-mode=fail`.
- [x] Run `:app-bootstrap:architectureTest :app-bootstrap:systemTest --warning-mode=fail`.
- [x] Search for direct app-bootstrap Kafka producer ownership.
- [x] Run `git diff --check`.
- [x] Report any remaining legacy outbox cutover blocker explicitly rather than inventing a migration.
@@ -0,0 +1,105 @@
# 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`.
@@ -0,0 +1,150 @@
# JPA Evidence Gradle Model Decoupling Design
## Context
`GenerateJpaEvidenceManifestsTask` currently performs evidence generation after its producer tasks run. Its semantic contract is useful, but the task action reaches back into the live Gradle model through `getProject()`, resolves configurations, locates `Task` instances, reads `Test` report locations, inspects `TaskState`, and reads root extra properties.
Gradle 9 deprecates `Task.project` access at execution time and Gradle 10 will reject it. More importantly, the current task mixes two responsibilities:
1. Gradle configuration/model discovery.
2. Pure evidence assembly from producer results.
The refactor must separate those concerns without weakening evidence claims.
## Goals
- Preserve the current readiness-card and evidence-manifest semantics.
- Remove execution-time `Project`, `Task`, and `TaskState` access from `GenerateJpaEvidenceManifestsTask`.
- Preserve JUnit XML as the source of truth for test execution evidence.
- Preserve successful non-Test task execution as the source of truth for `task-claims` such as architecture/configuration claims.
- Represent generator inputs with typed Gradle properties rather than hidden project lookups.
- Keep producer task names and readiness-card schema unchanged.
- Remain compatible with `--warning-mode=fail` on Gradle 9 and prepare the evidence lane for Gradle 10.
## Non-goals
- Do not redesign the readiness-card schema.
- Do not change evidence grades, prerequisite semantics, content hashing, R1/R2 rules, or output layout.
- Do not introduce marker files into every producer task.
- Do not move release orchestration into the persistence-JPA leaf.
- Do not add new runtime dependencies to application modules.
## Architecture
### 1. Build service owns task completion outcomes
Introduce `JpaEvidenceExecutionService`, a Gradle shared build service implementing `OperationCompletionListener`.
The plugin registers it through `BuildEventsListenerRegistry.onTaskCompletion(...)` so the service receives `TaskFinishEvent` events without the generator querying `TaskState`.
The service stores a thread-safe typed outcome for each task path:
```text
Task path
-> SUCCESS
-> FAILED
-> SKIPPED
```
Only `SUCCESS` satisfies an evidence `task-claim`. Failed or skipped producers do not cover the claim.
The service is build-scoped and contains no `Project` reference.
### 2. Test evidence remains file-based
JUnit evidence already has a durable output: Gradle's JUnit XML result directory. The plugin resolves every readiness/support `Test` task during configuration and supplies a typed mapping:
```text
absolute task path -> JUnit XML result directory
```
The generator reads those directories directly with `JUnitEvidenceReader`; it never locates a `Test` object.
Non-Test support tasks continue to participate in the task graph but do not produce JUnit evidence.
### 3. Configuration-derived values become task inputs
The plugin supplies these inputs before execution:
- evidence profile
- CI job
- artifact location
- topology
- PostgreSQL image
- source revision
- traceable version
- resolved PostgreSQL JDBC version
- resolved Hibernate ORM version
- resolved Flyway version
- repository-relative evidence output location used by the candidate default
- JUnit result-directory mapping
The generator reads only its properties/files plus the execution service.
`releaseProvenance` is the preferred source for revision/version. The existing extra-property compatibility bridge is no longer read by the generator.
### 4. Dependency-version discovery stays in plugin configuration
The JPA evidence plugin owns the Gradle `Configuration` object. It derives the three relevant resolved module versions and writes them into typed task properties before the generator executes.
This keeps dependency-graph access out of the task action. The existing coordinates remain unchanged:
- `org.postgresql:postgresql`
- `org.hibernate.orm:hibernate-core`
- `org.flywaydb:flyway-core`
### 5. Generator becomes an evidence assembler
The generator task action may use:
- its declared Gradle properties/files
- `ExecOperations` for git/docker commands already owned by the task
- `FileSystemOperations`
- `JpaEvidenceExecutionService`
- pure parser/verifier/helper classes
It must not call:
```java
getProject()
Project.findProject(...)
Task.getState()
TaskContainer.findByName(...)
ConfigurationContainer.getByName(...)
ExtraPropertiesExtension.get(...)
```
### 6. Evidence semantics
For a readiness card:
- `evidence.scenarios` are covered only by selectors found in JUnit XML.
- `evidence.task-claims` are covered only when the build service reports the named task completed successfully in the current build.
- `no-skip` remains based on JUnit result counts.
- prerequisite manifest ordering and hashing remain unchanged.
- candidate/R2 blockers remain unchanged.
The primary foundation card still obtains architecture/configuration coverage from successful execution of its declared producer tasks; the mechanism changes from `TaskState` lookup to task-finish events, not the meaning.
## Error handling
- A readiness task expected to produce JUnit evidence but missing from the configured result mapping is a hard failure.
- A configured JUnit result directory that contains no usable result remains subject to the existing JUnit evidence validation.
- A task claim with no successful completion event is simply uncovered and therefore becomes missing required evidence when that claim is required.
- Unsupported evidence profile remains a hard failure.
- Missing immutable image digest/dependency versions retain the existing blocker behavior.
## Testing
1. Unit-test task-event classification in `JpaEvidenceExecutionService`.
2. Unit-test pure JUnit result lookup from configured task-path/directory inputs.
3. TestKit: apply `ca.jpa-evidence` in a fixture and verify the generator task exposes typed inputs without execution-time project lookup.
4. Existing JPA evidence verifier tests must remain green.
5. Run `build-tools:check --warning-mode=fail`.
6. Run `verifyJpaReadinessRegistry verifyJpaReleaseGateTasks --warning-mode=fail`.
7. Run the affected JPA leaf `check`.
8. Run a candidate evidence lane far enough to confirm no `Task.project` deprecation is emitted; environment-dependent Docker/Testcontainers failure may be reported separately from Gradle-model warnings.
## Migration boundary
This change only decouples evidence generation from the live Gradle model. It does not alter the readiness registry, producer tasks, JUnit test suites, manifest schema, release workflow, or evidence verification policy.
@@ -0,0 +1,94 @@
# Messaging Platform Bridge Design
## Goal
Replace the application-specific broker seam with one canonical anti-corruption bridge:
```
application-core IntegrationEventPublishPort
-> adapter/outbound/messaging/platformbridge
-> messaging-schema-api EncodedMessagePublisher
-> messaging-runtime-core DefaultMessagePublisher
-> messaging transport/runtime
```
The bridge must preserve canonical event identity and exact encoded bytes while reusing the platform's destination resolution, authorization, admission, runtime leasing, transport normalization, and observation pipeline.
## Scope
This phase introduces and verifies the canonical bridge. It does **not** migrate the legacy outbox storage/relay rows, because `OutboxEvent` does not retain the schema/order/tenant metadata required to reconstruct `ValidatedIntegrationEvent` without invention.
## Application boundary
Create `IntegrationEventPublishPort` in `application-core`.
Signature:
```java
CompletionStage<OutboxPublishOutcome> publish(ValidatedIntegrationEvent event);
```
The application package depends only on its own canonical event model and application outcome vocabulary.
## Adapter bridge
`PlatformIntegrationEventPublishAdapter` lives under:
```
adapter/outbound/messaging/platformbridge
```
It depends on `EncodedMessagePublisher`, never on a concrete broker client, runtime-core implementation, or transport SPI.
Mapping rules:
- `logicalDestinationId` -> platform `DestinationName`.
- `contractId` -> platform `MessageType`.
- `payloadVersion` -> `SchemaVersion`.
- event and causation identities must parse as UUIDv7; values are preserved exactly. Incompatible identities fail closed before the platform publisher is called.
- `occurredAt` is used for both `producedAt` and `occurredAt` until the application canonical model carries a separate production timestamp. The bridge never invents a new timestamp.
- producer is an explicit constructor/configuration value.
- correlation, partition key, tenant, aggregate order and exact envelope bytes are preserved.
- trace context is explicitly absent (`TraceContext.none()`) until the application model owns canonical trace context.
- exact `envelopeBytes` become `EncodedMessage` bytes; no re-encoding occurs.
- schema/catalog/binding/envelope evidence that has no first-class platform field is preserved as bounded `x-ca-*` headers.
- the schema reference subject is the canonical contract id and version is the canonical payload version.
## Outcome mapping
Mapping is based on completion **and transmission evidence**, not enum name similarity:
- CONFIRMED -> `OutboxPublishOutcome.CONFIRMED`.
- AMBIGUOUS -> `OutboxPublishOutcome.AMBIGUOUS`.
- REJECTED + NOT_TRANSMITTED -> `REJECTED_BEFORE_SEND`.
- REJECTED + any evidence that bytes may have left the process -> `REJECTED_AFTER_BROKER`.
Bridge preparation failures are definite pre-send rejection.
## Platform boundary
`EncodedMessagePublisher` is owned by `messaging-schema-api`, because `EncodedMessage` is owned there and the dependency direction remains acyclic.
`DefaultMessagePublisher` implements both `MessagePublisher` and `EncodedMessagePublisher`. The encoded path skips only codec lookup/encoding; destination resolution, access policy, admission, runtime lease, transport send, deadline handling, result normalization and observation are shared with the normal publish path.
The starter exposes one `DefaultMessagePublisher` singleton, which therefore satisfies both public interfaces.
## Spring ownership
`MessagingBridgeRootAutoConfiguration` owns the bridge bean when an `EncodedMessagePublisher` is present **and** `app.messaging.producer-id` is explicitly configured. Producer identity is never inferred from `spring.application.name` or invented. Application bootstrap must not construct Kafka producer clients or implement broker-specific send behavior.
The existing `KafkaSender` / `KafkaMessageBroker` path remains temporarily for the legacy `OutboxEvent` and realtime publishers, which do not yet carry enough canonical metadata to enter the new bridge without invention. It is explicitly transitional and is removed only with the legacy outbox/realtime cutover. The new canonical bridge never calls it.
## Verification
Required checks:
1. `DefaultMessagePublisherTest`: pre-encoded publish preserves bytes and skips codec while still exercising central pipeline.
2. `PlatformIntegrationEventPublishAdapterTest`: golden mapping, outcome mapping, fail-closed identity behavior.
3. outbound messaging module tests/check.
4. messaging runtime/starter tests.
5. app-bootstrap system test and architecture test after adding the canonical bridge while retaining the documented legacy seam.
6. search proving app-bootstrap has no direct native Kafka sender configuration.
7. dependency/build lock refresh only where dependency ownership changed.
8. `git diff --check`.
@@ -0,0 +1,194 @@
# Outbox Transport-Only Cutover Design
## Status
Approved implementation slice for MSG-015 transport-only cutover.
This design deliberately does **not** activate `POLLING_V2` and does not migrate the publication authority to the v2 delivery tables. The existing `outbox_event` writer/store/claim/status authority remains the only active authority. The change makes that legacy authority capable of carrying a canonical integration event without losing the exact platform envelope.
## Goal
Support both row generations under one legacy relay authority:
```text
business transaction
-> legacy NewOutboxEvent -> legacy row
-> canonical ValidatedIntegrationEvent -> canonical-compatible row
one OutboxStorePort claim authority
-> legacy claimed row -> MessageBroker compatibility path
-> canonical claimed row -> IntegrationEventPublishPort -> messaging platform
```
A row is published through exactly one branch. There is no dual write and no second relay scheduler.
## Application boundaries
### Canonical append
`OutboxAppendPort` becomes the canonical durable append boundary:
```java
void append(ValidatedIntegrationEvent event);
```
### Legacy append
Raw R0 payload append moves to an explicitly named compatibility port:
```java
LegacyOutboxAppendPort
void append(NewOutboxEvent event);
```
Existing sample/durable-operation code that still emits raw `NewOutboxEvent` uses only the legacy port. New canonical code must not call the legacy port.
### Claimed row model
The relay-facing row is a sealed application model:
```text
ClaimedOutboxEvent
|- OutboxEvent // legacy R0 claim model retained for compatibility
`- CanonicalClaimedOutboxEvent // reconstructs one ValidatedIntegrationEvent
```
`OutboxStorePort.claimBatch` returns `List<ClaimedOutboxEvent>`.
Common relay state is exposed by the sealed interface: event id, event type, aggregate id, occurred-at, status and attempt count. The canonical subtype also exposes the exact `ValidatedIntegrationEvent`.
A persisted row with a **partial** canonical metadata set is corrupt and fails closed during mapping. It is never downgraded to the legacy path.
## Storage compatibility projection
The existing PostgreSQL `outbox_event` remains authoritative. Add a forward migration after current legacy V12 that:
- widens `event_id` to `varchar(96)`;
- widens `correlation_id` to `varchar(128)`;
- adds nullable canonical columns to preserve existing rows;
- adds an all-or-none canonical-shape check;
- stores exact canonical envelope bytes in `bytea`;
- keeps the legacy required columns for the rollback window.
Canonical required columns:
```text
contract_id
envelope_version
payload_version
logical_destination
tenant_scope
aggregate_type
aggregate_sequence
event_index
partition_key
envelope_bytes
content_type
schema_set_hash
envelope_sha256
envelope_schema_hash
payload_schema_hash
contract_catalog_revision
destination_binding_revision
```
`causation_id` is optional by the application contract.
Existing legacy columns remain populated for canonical rows with this compatibility projection:
```text
event_id = canonical event id
aggregate_id = canonical aggregate id
event_type = contract id
payload = exact envelope bytes decoded as strict UTF-8
occurred_at = canonical occurred-at
status = PENDING
attempt_count = 0
next_attempt_at = occurred-at
correlation_id = canonical correlation id
idempotency_key = event id
```
The canonical encoder currently emits a UTF-8 JSON envelope. The append adapter verifies strict UTF-8 round-trip before storing the compatibility text. Invalid UTF-8 fails the business transaction; replacement characters are forbidden.
`partitionKeyBytes` is not stored separately because the canonical model already requires it to be exactly the US-ASCII bytes of `partitionKeyText`. The claimed model reconstructs those bytes from the stored canonical text.
## Persistence adapters
`OutboxStoreAdapter` remains the legacy claim/status store and implements `LegacyOutboxAppendPort`, not `OutboxAppendPort`.
A separate `CanonicalOutboxAppendAdapter` implements `OutboxAppendPort`. It participates in the caller's existing write transaction exactly like the legacy adapter and never opens a local transaction.
Both write the same `outbox_event` table; they are alternative semantic inputs, not dual writers for one business fact.
## Activation
Introduce:
```text
ca-skeleton.outbox.canonical-transport-enabled=false
```
Default remains false.
When false:
- existing legacy append/relay behavior is unchanged;
- canonical append bean is not exposed;
- canonical relay routing is not considered an active deployment capability.
When true:
- canonical append bean is exposed;
- startup requires an `IntegrationEventPublishPort`;
- the relay publisher can route canonical claimed rows to that port;
- legacy rows continue through `MessageBroker`;
- while mixed legacy rows may still exist, a relay-enabled deployment still requires the legacy broker. Canonical transport is an additional route, not permission to strand legacy backlog.
The gate is a compatibility/cutover gate only. It does not change DB publication authority and does not activate `POLLING_V2`.
## Publish routing
`OutboxMessagePublishPort` remains the one relay publish port and accepts `ClaimedOutboxEvent`.
Implementation behavior:
- `OutboxEvent` -> existing `OutboxEnvelopeJson` + `MessageBroker`.
- `CanonicalClaimedOutboxEvent` -> exact stored `ValidatedIntegrationEvent` -> `IntegrationEventPublishPort`.
The canonical branch blocks on the returned `CompletionStage` only at this legacy compatibility boundary, because the current legacy relay port is synchronous. The platform result is mapped unchanged into `OutboxPublishOutcome`.
The bridge does not re-encode canonical bytes.
If canonical transport is disabled or the canonical publisher is absent, canonical publication fails closed before broker/platform transmission. Startup validation prevents the normal configured case from reaching that state.
## Outcome policy
The existing legacy relay state machine remains authoritative in this slice:
- CONFIRMED -> mark PUBLISHED.
- AMBIGUOUS -> retryable legacy FAILED flow.
- REJECTED_BEFORE_SEND / REJECTED_AFTER_BROKER -> existing definite-refusal DEAD behavior.
This is intentionally the existing compatibility semantics. The richer v2 per-attempt state machine is a later storage-authority cutover.
## Non-goals
This slice does not:
- switch `OutboxPublicationAuthority` to `POLLING_V2`;
- mutate/reconcile `outbox_event_log_v2` or `outbox_delivery_v2`;
- implement CDC;
- remove `MessageBroker`, `KafkaSender`, `NewOutboxEvent`, `OutboxEvent`, or the legacy scheduler;
- migrate old rows into canonical rows;
- invent tenant, trace, schema or routing metadata for old rows.
## Verification
Required:
1. application port split compiles and old raw producers use `LegacyOutboxAppendPort`;
2. migration integration proves additive columns, exact BYTEA, constraints and legacy compatibility;
3. canonical append adapter round-trips every canonical field and exact bytes;
4. partial canonical row mapping fails closed;
5. legacy row mapping remains unchanged;
6. relay unit test proves canonical row invokes only `IntegrationEventPublishPort`;
7. legacy row invokes only `MessageBroker`;
8. canonical bytes reaching `PlatformIntegrationEventPublishAdapter` are byte-identical;
9. startup rejects canonical transport enabled without `IntegrationEventPublishPort`;
10. default-off composition preserves current behavior;
11. architecture/dependency checks and `git diff --check` pass.