# Application Outbox Failure Reporting — Harness-Free Design ## Context `application-core` currently carries Spring Boot and SLF4J only because `PublishPendingOutboxEventsUseCase` renders relay failures itself. That reverses the diagnostic dependency direction and also permits a duplicate WARN in `OutboxMessagePublishAdapter`. This change is harness-free: `src/config/architecture/modules.json`, Gradle, ArchUnit, and focused module tests are the policy and evidence authorities. No `.harness` files or public paths change. ## Boundary `application-core` owns a specific `OutboxRelayFailureReportPort` and an immutable `OutboxRelayFailureReport`. The report is an allowlist containing only: - `OperationalError code` - event, aggregate, and correlation identifiers - event type, attempt count, optional next-attempt time - the originating `RuntimeException` It never carries the payload, idempotency key, message template, severity, arbitrary fields, or the whole `OutboxEvent`. Factories and record invariants admit only retryable `OUTBOX_PUBLISH_FAILED` reports with a next-attempt time and terminal `OUTBOX_DEAD_LETTER` reports without one. `adapter:outbound:messaging` owns `Slf4jOutboxRelayFailureReportAdapter`. It maps the typed report to one canonical SLF4J 2 fluent ERROR with fixed key names and runbook links. Bootstrap only wires the port. ## Ordering and Failure Semantics The persisted FAILED or DEAD transition is authoritative: 1. broker publication fails; 2. the application calculates the transition; 3. the store transition succeeds inside `TransactionPort`; 4. only then is the typed report emitted. A transition failure propagates and emits no report. A reporter `RuntimeException` is contained by both the adapter and the use case, so it cannot change the relay outcome or prevent later events from running. Successful publication and `markPublished` failures emit no failure report. There is no production no-op reporter. `MessagingConfig` always contributes exactly one reporter bean, using the configured broker name or `disabled` when blank. `OutboxMessagePublishAdapter` becomes mapping/send-only: runtime failures propagate, checked failures are wrapped with their cause, and it emits no success or failure log. The general `OutboundMessagePublisher` retains its existing fail-open dependency logging. ## Structured ERROR Contract Every confirmed transition produces one ERROR with the common fields: `error.code`, `error.category`, `dependency_name`, `dependency_type=messaging`, `outcome`, `event_id`, `event_type`, `aggregate_id`, `correlation_id`, `attempt_count`, and `runbook_link`. Retryable failures additionally carry `next_attempt_at`. Mappings are: | Code | Outcome | Runbook | | --- | --- | --- | | `OUTBOX_PUBLISH_FAILED` | `FAILED` | `runbook://outbox/publish-failed` | | `OUTBOX_DEAD_LETTER` | `DEAD` | `runbook://outbox/dead-letter` | The originating exception is attached as the throwable. Payload, idempotency key, envelope data, message templates derived from the exception, and arbitrary exception fields are forbidden. The adapter's fail-open boundary also applies to invalid direct calls: `report(null)` must never throw. The focused structured-adapter test pins this behavior. ## Enforcement and Tests - Value tests enforce invariants and reflectively pin the exact record component allowlist. - Relay tests pin transition-before-report ordering, no-report paths, exact cardinality, and reporter containment. - Messaging tests capture Logback events and pin level, fields, throwable, and unsafe-data absence. - `verifyApplicationCoreDependencyPurity` rejects non-project production declarations and forbidden Spring/logging/metrics groups on resolved application classpaths. - `APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORK` bans SLF4J, JUL, Logback, Log4j, and Micrometer from the exact `dev.caskeleton.application..` scope. Its dedicated violation fixture also resides inside that scope, under `dev.caskeleton.application.architecture.violations`, proving the rule is non-vacuous. - `application-core` test dependencies are reduced to JUnit Jupiter and AssertJ; all other leaves keep the shared Spring Boot test baseline. ## Scope No public path, CI workflow, module-registry edge, payload shape, outbox persistence schema, or general publisher logging behavior changes. Agents do not stage, commit, amend, or push.