Command: printf '%s\n' '--- persistence root ---'; sed -n '1,115p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/PersistenceJpaRootAutoConfiguration.java; printf '%s\n' '--- adapter component scan ---'; cat src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java; printf '%s\n' '--- runtime transaction beans ---'; sed -n '140,195p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java; printf '%s\n' '--- canonical/application references ---'; git grep -n -E '\b(SpringTransactionPort|PolicyTransactionPort|FullTransactionRetryCoordinator|SpringJpaTransactionExecutor)\b' -- 'src/**/src/main/java/**/*.java'
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:01:16Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Shows shipped persistence root imports, narrow adapter component scan, transaction bean factories, and exact production construction/reference sites for the two transaction stacks. Static wiring does not prove runtime invocation frequency.
--- stdout/stderr ---
--- persistence root ---
package dev.caskeleton.bootstrap.autoconfigure.persistencejpa;

import dev.caskeleton.adapter.outbound.persistence.config.JpaAdapterComponentsConfig;
import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings;
import dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig;
import dev.caskeleton.adapter.outbound.persistence.h2.H2PersistenceConfig;
import dev.caskeleton.adapter.outbound.persistence.notification.configuration.NotificationJpaPersistenceFacade;
import dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlPersistenceConfig;
import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaDataSourceProfileValidator;
import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfiguration;
import dev.caskeleton.bootstrap.autoconfigure.jpa.PostgreSqlVersionPolicy;
import dev.caskeleton.bootstrap.runtime.startup.MigrationStartupConfig;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;

/**
 * The one place that decides whether this application has relational persistence.
 *
 * <p>Every JPA configuration is reached through this import rather than through the component scan,
 * so a bean added to any of them next month is gated without anyone remembering to repeat a
 * condition. That is the difference between a capability that is off and a capability whose known
 * beans are individually conditioned.
 *
 * <p>Both vendor configurations are named here, and each is the leaf\'s real entry point: a vendor
 * config imports the core JPA config, not the reverse. Inverting that to give this class a single
 * import produced a package cycle inside the leaf, so the export surface admits the two vendor
 * packages instead. They are mutually exclusive on {@code ca-skeleton.persistence.vendor}, so
 * naming both lets the selector decide which one assembles while the decision about
 * <em>whether</em> there is a database stays here, in one place.
 *
 * <p><b>The notification facade is named here only now that the payload is protected at rest.</b>
 * Importing it makes {@code JpaNotificationRequestStore} reachable, and that store used to write
 * the accepted request's template variables to {@code notification_request.variables_payload} in
 * plaintext — caller content that can be a reset code, an order total or an address (NTF-INT-007).
 * It was wired here once before that was true; the at-rest contract test failed on exactly that
 * fact, which is what it was written to do, and the wiring was reverted rather than the test
 * relaxed. The envelope came first, {@code NotificationRecordMapper} now takes the protection as a
 * required constructor argument, and only then does this import become a wiring rather than an
 * exposure.
 *
 * <p>{@code FileserverJpaPersistenceConfig} is named for a related reason. Its six entities map six
 * {@code fs_*} tables that live only in {@code db/migration/jpa/fileserver}, a stream applied only
 * when that capability is on, while the primary Flyway location creates none of them. They used to
 * be in the unconditional entity scan, so {@code ddl-auto=validate} against real PostgreSQL failed
 * on {@code fs_cleanup_item} in every deployment that had switched the capability off — which was
 * every JPA-on Compose lane. The scan now carries the capability's own condition, and this is the
 * only place that knows both that the capability is on and that a JPA vendor is composed. Neither
 * the leaf's {@code config} package nor a component scan can register it: {@code config} may depend
 * on {@code api} alone, and the whole persistence package is excluded from the root's scan.
 */
@AutoConfiguration
@ConditionalOnProperty(
    prefix = "ca-skeleton.persistence-jpa",
    name = "enabled",
    havingValue = "true")
@Import({
  JpaAdapterComponentsConfig.class,
  PostgreSqlPersistenceConfig.class,
  H2PersistenceConfig.class,
  FileserverJpaPersistenceConfig.class,
  NotificationJpaPersistenceFacade.class,
  MigrationStartupConfig.class,
  JpaPlatformRuntimeAutoConfiguration.class
})
public class PersistenceJpaRootAutoConfiguration {

  /**
   * Runs the datasource validator, which nothing used to run (JPA-INT-002).
   *
   * <p>{@code JpaDataSourceProfileValidator} was created as a bean and never invoked: both of its
   * methods were reachable only from their own unit test. A validator nobody calls is a comment —
   * and this one would have failed every deployment if called, because the namespace it read was
   * supplied by nothing. The two defects cancelled, which is the only reason the application
   * started.
   *
   * <p>Invoked from here rather than from the platform configuration so it runs exactly when JPA is
   * on and never when it is off, and as an {@link InitializingBean} so the failure lands during
   * startup rather than at whoever sends the first request.
   *
   * <p>The vendor is read from the {@link Environment} rather than injected as {@code
   * PersistenceVendorSettings}, because that type is not a registered bean here: the composition
   * root's {@code @ConfigurationPropertiesScan} deliberately excludes the persistence package so an
   * optional capability cannot bind — or reject — its detail settings in a deployment that never
   * switched it on. The default matches {@code PostgreSqlPersistenceConfig}'s {@code matchIfMissing
   * = true}: an unset selector is PostgreSQL, which is the vendor this configuration was
   * unconditional about before the selector existed.
   *
   * <p>The validator is constructed here rather than injected. It is stateless, and the bean that
   * used to supply it comes from {@code JpaPlatformRuntimeAutoConfiguration}, which carries
   * {@code @ConditionalOnBean(DataSource.class)} on a plain {@code @Configuration} imported by this
   * root — a condition evaluated during configuration-class parsing, before the datasource bean
   * definition is registered. That class therefore drops out silently in the real application,
   * taking the whole JPA add-on layer with it. Depending on a bean from it would make this check
   * disappear for the same reason the thing it checks disappeared. The condition-ordering defect is
   * recorded separately; it is not this task's to fix quietly.
   *
   * @param dataSource the resolved datasource, injected so the database checked is the one in use
   * @param environment the resolved environment, for the vendor selector
   * @return the startup check
   */
  @Bean
  public InitializingBean jpaResolvedDataSourceCheck(
      DataSource dataSource, Environment environment) {
    JpaDataSourceProfileValidator validator =
        new JpaDataSourceProfileValidator(new PostgreSqlVersionPolicy());
    String vendor =
        environment.getProperty(PersistenceVendorSettings.VENDOR_PROPERTY, "postgresql").trim();
    return () -> validator.validateResolved(dataSource, "postgresql".equalsIgnoreCase(vendor));
  }
}
--- adapter component scan ---
package dev.caskeleton.adapter.outbound.persistence.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * Registers the JPA adapter's scanned components, which nothing registered (JPA-INT-006).
 *
 * <p>The composition root's {@code @ComponentScan} excludes {@code
 * dev.caskeleton.adapter.outbound.persistence.**} by regex, and that exclusion is correct: it is
 * what makes an optional capability optional, so a deployment with JPA off assembles no persistence
 * beans rather than assembling them and hoping each one remembered to carry the switch.
 *
 * <p>What was missing is the other half. Eight classes in this leaf are written as scanned
 * components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code
 * StandardSqlStateErrorMapping}, {@code DomainContextAuditContextPort}, the idempotency store and
 * its reaper, and the outbox store and its reaper — and once the broad scan stopped reaching them,
 * nothing else did. They are annotated {@code @Component} and {@code @Repository} and were beans in
 * no running application: {@code TransactionPort} in particular had no implementation at all, so
 * every use case that opens a transaction had no port to open it with.
 *
 * <p>It surfaced as an unsatisfied dependency the first time a capability that needs a transaction
 * was actually assembled — the notification orchestrator, in the local-notification-ingest lane —
 * rather than as anything a unit test could see, because each of these classes is constructed
 * directly by its own tests.
 *
 * <p>So the scan is restored, narrowed to the packages it should always have covered and reachable
 * only through {@code PersistenceJpaRootAutoConfiguration}, which carries the JPA master switch.
 * Off is still structural.
 *
 * <p>Two packages are deliberately absent:
 *
 * <ul>
 *   <li>{@code ..persistence.fileserver} — gated on its own capability switch, scanned by {@link
 *       dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig};
 *   <li>{@code ..persistence.notification} — assembled explicitly, bean by bean, by {@code
 *       NotificationJpaPersistenceFacade}.
 * </ul>
 *
 * <p>Components under these packages keep their own {@code @ConditionalOnProperty} guards; being
 * scanned makes them candidates, not unconditional beans.
 *
 * <p>{@code ..persistence.lock} is in the list for the same reason and with the same history.
 * {@code DistributedLockPersistenceConfig} owns both lock providers — the in-process registry and
 * the JDBC one — and was registered by nothing but a test calling {@code ctx.register(...)}. So a
 * single-instance deployment had no {@code DistributedLockPort} at all, and a multi-instance one
 * could not start: the composition root's own {@code DistributedLockConfig} asks for a bean
 * qualified {@code jdbcDistributedLock} that only that configuration declares.
 *
 * <p>Each package's {@code @ConfigurationProperties} type is enabled by a configuration inside that
 * same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
 * DistributedLockPersistenceConfig} for {@code LockSettings} — rather than from here. Enabling them
 * centrally would give {@code config} an edge to {@code lock} and {@code transaction} that the
 * module map does not grant it, and the map is right: this class knows which packages to scan, not
 * what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan}
 * excludes this tree as deliberately as {@code @ComponentScan} does.
 */
@Configuration(proxyBeanMethods = false)
@ComponentScan(
    basePackages = {
      "dev.caskeleton.adapter.outbound.persistence.audit",
      "dev.caskeleton.adapter.outbound.persistence.failure",
      "dev.caskeleton.adapter.outbound.persistence.idempotency",
      "dev.caskeleton.adapter.outbound.persistence.lock",
      "dev.caskeleton.adapter.outbound.persistence.outbox",
      "dev.caskeleton.adapter.outbound.persistence.transaction"
    })
public class JpaAdapterComponentsConfig {}
--- runtime transaction beans ---
  }

  /**
   * The programmatic transaction boundary, over whichever manager the application installed.
   *
   * <p>The composed vendor's translator is taken when one exists. Without it the executor's chain
   * has no SQLSTATE stage, so a serialization failure or deadlock stays a raw {@code
   * DataAccessException}, never becomes the {@code JpaPersistenceException} the retry coordinator
   * catches, and contention is not retried at all.
   *
   * @param vendorFailures the composed vendor's SQLSTATE translator, when a vendor registered one
   */
  @Bean
  @ConditionalOnMissingBean
  @ConditionalOnBean(PlatformTransactionManager.class)
  public SpringJpaTransactionExecutor jpaTransactionExecutor(
      PlatformTransactionManager transactionManager,
      Clock clock,
      ObjectProvider<VendorFailureTranslator> vendorFailures) {
    JpaTransactionAutoConfiguration composition = new JpaTransactionAutoConfiguration(clock);
    VendorFailureTranslator vendor = vendorFailures.getIfAvailable();
    return vendor == null
        ? composition.transactionExecutor(transactionManager)
        : composition.transactionExecutor(transactionManager, vendor);
  }

  /** The whole-transaction retry coordinator. */
  @Bean
  @ConditionalOnMissingBean
  @ConditionalOnBean(SpringJpaTransactionExecutor.class)
  public FullTransactionRetryCoordinator jpaRetryCoordinator(
      SpringJpaTransactionExecutor executor, RetryEventListener retryListener, Clock clock) {
    return new JpaTransactionAutoConfiguration(clock)
        .retryCoordinator(
            executor,
            dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile
                .boundedContention("jpa-platform-default", 3),
            retryListener);
  }

  /**
   * The retry listener.
   *
   * <p>The interface's methods all have defaults, so an empty implementation is the honest "no
   * observer wired" value; an application that wants retry telemetry supplies its own bean.
   */
  @Bean
  @ConditionalOnMissingBean
  public RetryEventListener jpaRetryEventListener() {
    return new RetryEventListener() {};
  }

  /** Runs the dangerous-configuration guard during refresh. */
  @Bean
  public InitializingBean jpaPlatformStartupCheck(
      JpaDangerousConfigurationGuard guard, Environment environment) {
--- canonical/application references ---
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java:15: * components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:30:public final class FullTransactionRetryCoordinator {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:32:  private final SpringJpaTransactionExecutor transactionExecutor;
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:50:  public FullTransactionRetryCoordinator(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:51:      SpringJpaTransactionExecutor transactionExecutor,
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java:9: * <p>{@link SpringTransactionPort} takes it as a constructor argument, and the composition root's
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:21: * <p>This class does not retry. Retry lives in {@link FullTransactionRetryCoordinator}, which calls
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:25:public final class SpringJpaTransactionExecutor implements JpaTransactionExecutor {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:32:  public SpringJpaTransactionExecutor(PlatformTransactionManager transactionManager, Clock clock) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:41:  public SpringJpaTransactionExecutor(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:8:import dev.caskeleton.application.transaction.PolicyTransactionPort;
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:26: * Spring-backed {@link PolicyTransactionPort}: one pre-built {@link TransactionTemplate} per mode,
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:31:public class SpringTransactionPort implements PolicyTransactionPort {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:39:  public SpringTransactionPort(PlatformTransactionManager transactionManager) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:49:  SpringTransactionPort(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:61:  public SpringTransactionPort(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:77:  SpringTransactionPort(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:90:  SpringTransactionPort(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:105:  SpringTransactionPort(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:7:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:8:import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:155:  public SpringJpaTransactionExecutor jpaTransactionExecutor(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:169:  @ConditionalOnBean(SpringJpaTransactionExecutor.class)
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:170:  public FullTransactionRetryCoordinator jpaRetryCoordinator(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:171:      SpringJpaTransactionExecutor executor, RetryEventListener retryListener, Clock clock) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:9:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:13:import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:37: * PolicyTransactionPort.inTransaction(TransactionRequest, Supplier)}; the retry coordinator below
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:66:  public SpringJpaTransactionExecutor transactionExecutor(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:68:    return new SpringJpaTransactionExecutor(transactionManager, clock);
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:82:  public SpringJpaTransactionExecutor transactionExecutor(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:84:    return new SpringJpaTransactionExecutor(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:99:  public FullTransactionRetryCoordinator retryCoordinator(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:100:      SpringJpaTransactionExecutor executor,
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:107:  public FullTransactionRetryCoordinator retryCoordinator(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:108:      SpringJpaTransactionExecutor executor, JpaRetryPolicy policy, RetryEventListener listener) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:109:    return new FullTransactionRetryCoordinator(
src/application-core/src/main/java/dev/caskeleton/application/transaction/PolicyTransactionPort.java:10:public interface PolicyTransactionPort extends TransactionPort {
src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionPort.java:8: * {@code SpringTransactionPort}) implements it over Spring's {@code PlatformTransactionManager}.

Exit code: 0
