revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5 === stable scan includes experimental === 7- "dev.caskeleton.adapter.outbound.persistence.cache", 8- "dev.caskeleton.adapter.outbound.persistence.envers", 9: "dev.caskeleton.adapter.outbound.persistence.experimental", 10- "dev.caskeleton.adapter.outbound.persistence.failure", 11- "dev.caskeleton.adapter.outbound.persistence.hibernate", -- 30- "dev.caskeleton.adapter.outbound.persistence.cache", 31- "dev.caskeleton.adapter.outbound.persistence.envers", 32: "dev.caskeleton.adapter.outbound.persistence.experimental", 33- "dev.caskeleton.adapter.outbound.persistence.failure", 34- "dev.caskeleton.adapter.outbound.persistence.hibernate", === opt-in exemptions known by scan coverage test === /** Each opt-in capability: its package, its own configuration, and the switch that gates it. */ private record OptInCapability(String packageName, Class configuration, String switchPrefix) {} private static final List OPT_IN = List.of( new OptInCapability( PersistenceJpaConfig.OPT_IN_NOTIFICATION_PACKAGE, NotificationJpaPersistenceConfig.class, "ca-skeleton.notification.platform"), new OptInCapability( PersistenceJpaConfig.OPT_IN_FILESERVER_PACKAGE, FileserverJpaPersistenceConfig.class, "app.fileserver-platform")); /** Sub-packages that hold no JPA entity or repository and therefore need no scan. */ private static final Set NOT_SCANNED = Set.of( // Vendor-selection configuration only. "config", "h2", // The opt-in capabilities, each scanned by its own gated configuration instead. "notification", "fileserver"); === current experimental JPA stereotypes === === architecture rule checks type dependency, not scan strings === "the testkit carries ArchUnit and Testcontainers; a production edge would put both on" + " every deployment's runtime classpath") .check(PRODUCTION_CLASSES); } @Test @DisplayName("the Stable platform does not depend on experimental packages") void stablePlatformDoesNotDependOnExperimentalPackages() { noClasses() .that() .resideOutsideOfPackage(ROOT + ".experimental..") .should() .dependOnClassesThat() .resideInAPackage(ROOT + ".experimental..") .as("experimental capabilities are never reachable from Stable code (plan §3)") .because( "an experimental capability may change or be withdrawn; a Stable type referencing one" + " gives it a Stable contract nobody agreed to") .check(PRODUCTION_CLASSES); } @Test === plan constraint === **Architecture:** Experimental module은 Stable `jpa-core-api` 계약만 소비하며 Stable starter에 자동 포함되지 않는다. 각 기능은 명시적 feature flag와 별도 compatibility/failure suite를 요구한다. 실험 결과가 Stable 의미론과 충돌하면 Core를 왜곡하지 않고 capability 또는 별도 profile로 유지한다. **Tech Stack:** Stable 계획의 Java 21·Spring Boot 4.1·PostgreSQL Testcontainers 기반, PostgreSQL RLS, AbstractRoutingDataSource, tenant-specific DataSource registry, Jakarta Persistence 4.0 preview/final compatibility lane, Hibernate ORM 8 compatibility lane, PostgreSQL 19 compatibility lane. ## Global Constraints - Stable 계획 Task 1~53이 완료되고 Release Gate가 통과한 뒤 시작한다. - 모듈 루트는 `modules/jpa-experimental`이다. - Experimental module은 `jpa-spring-boot-starter`의 기본 dependency가 아니다. - 모든 기능은 `backend.jpa.experimental.*` feature flag를 요구한다. - Tenant ID와 consistency token은 metric label에 기록하지 않는다. - Tenant context 누락은 fail-closed다. - `readOnly=true`만으로 replica routing하지 않는다. - Lock query, write transaction, read-after-write pin은 primary를 사용한다.