# GraphQL Advanced Capability Expansion 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:** Stable GraphQL API 실행 플랫폼 위에 Persisted Operation, WebSocket·SSE Subscription, optional replay, Federation Subgraph, Code Generation과 제한된 Spring Data 호환 기능을 추가하고 RSocket·HTTP GET·Incremental Delivery를 격리된 Experimental capability로 검증한다. **Architecture:** Advanced capability는 `modules/graphql-advanced`에만 존재하며 Stable module의 public contract를 소비하되 Stable starter의 transitive dependency가 되지 않는다. 모든 capability는 `backend.graphql.advanced.*` feature flag와 capability별 release evidence를 요구하며, 장기 연결·schema composition·provider-specific behavior가 Stable query/mutation path를 오염시키지 않도록 분리한다. **Tech Stack:** Stable GraphQL platform, Java 21, Spring Boot 4.1 BOM, Spring for GraphQL 2.0, Boot-managed GraphQL Java v25, GraphQL Java DataLoader, Spring WebFlux WebSocket, Spring GraphQL SSE/RSocket, federation-jvm, Micrometer, JUnit 5, Reactor Test. ## Global Constraints - 이 계획은 Stable 구현 계획 Task `1–48`과 Stable Release Gate가 통과한 후 시작한다. - Advanced module root는 `modules/graphql-advanced`이다. - Root package는 `io.backend.skeleton.graphql.advanced`이다. - 모든 capability는 `backend.graphql.advanced.*` 아래의 명시적 feature flag를 요구한다. - Stable starter는 Advanced module에 compile·runtime dependency를 갖지 않는다. - Persisted Operation은 Parse cache 및 Response cache와 다른 기능이다. - WebSocket protocol은 `graphql-transport-ws`만 지원하고 `subscriptions-transport-ws`를 신규 지원하지 않는다. - GraphQL Subscription은 Messaging의 ACK·offset·replay·DLQ를 대체하지 않는다. - Slow consumer 기본 정책은 무음 event drop이 아니라 종료다. - SSE는 Distinct Connection subscription transport이며 Query·Mutation response mode가 아니다. - Replay extension의 durability·offset guarantee는 Messaging platform이 소유한다. - Single Executable Schema는 계속 Stable 기본값이다. - Federation은 Subgraph capability만 이 저장소에 구현하고 Router/Supergraph 운영은 별도 프로젝트가 소유한다. - Client/transport DTO code generation은 허용하지만 Domain Entity·Use Case·Repository generation은 금지한다. - Spring Data automatic GraphQL repository exposure는 allowlist compatibility module로만 제공한다. - RSocket, HTTP GET, Incremental Delivery는 승격 ADR 전까지 Experimental이다. - Advanced capability가 Stable HTTP POST, error, security, cost, DataLoader와 observability guardrail을 우회해서는 안 된다. - 모든 task는 red-green TDD와 독립 commit으로 끝난다. --- ## Advanced Module Map ```text modules/graphql-advanced/ ├── graphql-advanced-bootstrap ├── graphql-persisted-operation ├── graphql-websocket ├── graphql-subscription ├── graphql-sse ├── graphql-dataloader-chaining ├── graphql-federation ├── graphql-codegen ├── graphql-spring-data-compat ├── graphql-rsocket ├── graphql-http-draft └── graphql-incremental-delivery ``` ## Capability Classification | Capability | Initial grade | Promotion evidence | |---|---|---| | Persisted Operation | Advanced Stable | durable registry, block propagation, schema/usage gate | | WebSocket Subscription | Advanced Stable | auth, backpressure, soak, cancellation, drain | | SSE Subscription | Advanced | HTTP/2 connection scale, proxy behavior, auth | | Messaging-backed Replay | Advanced Extension | snapshot/live gap, history loss, authorization | | Chained DataLoader | Advanced | dispatch/query-count regression | | Federation Subgraph | Advanced | composition, router integration, failure/latency | | Client Codegen | Optional Stable Tooling | generated-source compatibility | | Spring Data Compat | Restricted Advanced | allowlist, projection, pagination/query guard | | RSocket | Experimental | explicit consumers and transport evidence | | HTTP GET | Experimental | draft, cache, CSRF and URL disclosure evidence | | Incremental Delivery | Experimental | engine/transport interoperability and client negotiation | ## Delivery Phases | Phase | Tasks | Result | |---|---:|---| | Boundary | 1 | Stable/Advanced dependency and feature flag isolation | | Persisted Operations | 2–4 | Registry, execution lookup and G4 admin | | Live Transport | 5–10 | WebSocket, auth, backpressure, ordering, SSE, replay | | Data·Schema Extensions | 11–15 | Chained loader, Federation, Codegen, Spring Data compat | | Experimental Transport | 16–18 | RSocket, HTTP GET, Incremental Delivery | | Promotion | 19 | Capability-specific release and promotion gate | --- ### Task 1: Advanced Module Boundary와 Feature Flag **Files:** - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedCapability.java` - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedFeatureFlags.java` - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedModuleGuard.java` - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedDependencyRules.java` - Test: `modules/graphql-advanced/graphql-advanced-bootstrap/src/test/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedModuleGuardTest.java` **Interfaces:** - Consumes: Stable GraphQL API 실행 플랫폼의 public contract와 Spring Boot environment. - Produces: Advanced·Experimental 모듈이 Stable starter에 자동 유입되지 않도록 하는 dependency·feature flag 경계. **Implementation requirements:** - 모든 Advanced capability는 `backend.graphql.advanced.*` 아래의 명시적 flag를 요구한다. - Stable starter는 advanced module에 compile/runtime dependency를 갖지 않는다. - Advanced module은 Stable public types를 소비할 수 있지만 Stable module을 수정하지 않는다. - Experimental capability는 production에서 별도 승인 profile 없이는 시작되지 않는다. - Capability 상태는 `ADVANCED_STABLE`, `EXPERIMENTAL`, `DISABLED`로 구분한다. - [ ] **Step 1: Write the failing test** ```java class GraphQlAdvancedModuleGuardTest { @org.junit.jupiter.api.Test void disabledCapabilityCannotStart() { var flags = GraphQlAdvancedFeatureFlags.disabled(); var guard = new GraphQlAdvancedModuleGuard(flags); org.assertj.core.api.Assertions.assertThatThrownBy( () -> guard.requireEnabled( GraphQlAdvancedCapability.PERSISTED_OPERATION)) .isInstanceOf( GraphQlAdvancedCapabilityDisabledException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-advanced-bootstrap:test --tests 'io.backend.skeleton.graphql.advanced.bootstrap.GraphQlAdvancedModuleGuardTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GraphQlAdvancedCapability { PERSISTED_OPERATION, WEBSOCKET_SUBSCRIPTION, SSE_SUBSCRIPTION, FEDERATION_SUBGRAPH, CODE_GENERATION, SPRING_DATA_COMPAT, RSOCKET, HTTP_GET, INCREMENTAL_DELIVERY } public record GraphQlAdvancedFeatureFlags( java.util.Set enabled) { public static GraphQlAdvancedFeatureFlags disabled() { return new GraphQlAdvancedFeatureFlags( java.util.Set.of()); } public boolean isEnabled( GraphQlAdvancedCapability capability) { return enabled.contains(capability); } } public final class GraphQlAdvancedModuleGuard { private final GraphQlAdvancedFeatureFlags flags; public GraphQlAdvancedModuleGuard( GraphQlAdvancedFeatureFlags flags) { this.flags = flags; } public void requireEnabled( GraphQlAdvancedCapability capability) { if (!flags.isEnabled(capability)) { throw new GraphQlAdvancedCapabilityDisabledException( capability.name()); } } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-advanced-bootstrap:test --tests 'io.backend.skeleton.graphql.advanced.bootstrap.GraphQlAdvancedModuleGuardTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedCapability.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedFeatureFlags.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedModuleGuard.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedDependencyRules.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/test/java/io/backend/skeleton/graphql/advanced/bootstrap/GraphQlAdvancedModuleGuardTest.java' git commit -m "build: isolate graphql advanced modules" ``` ### Task 2: Persisted Operation Model과 Registry **Files:** - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationId.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperation.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationStatus.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRegistry.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/InMemoryGraphQlPersistedOperationRegistry.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationConflictException.java` - Test: `modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRegistryTest.java` **Interfaces:** - Consumes: Stable schema contract hash, operation name, canonical document hash와 client profile. - Produces: 승인된 operation document의 versioned registry와 ACTIVE·DEPRECATED·BLOCKED lifecycle. **Implementation requirements:** - Registry record에 operation ID, operation name, SHA-256 document hash, canonical document, schema hash, allowed client profiles, maximum complexity와 variable bytes를 보존한다. - 같은 operation ID로 다른 document를 등록하면 conflict다. - BLOCKED operation은 cache에 남아 있어도 실행되지 않는다. - Raw variables와 credential을 registry에 저장하지 않는다. - Persistent implementation SPI를 제공하되 Stable DB 선택을 강제하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlPersistedOperationRegistryTest { @org.junit.jupiter.api.Test void sameIdWithDifferentDocumentIsConflict() { var registry = new InMemoryGraphQlPersistedOperationRegistry(); registry.register(GraphQlPersistedOperation.active( "get-order-v1", "GetOrder", "sha256:a", "query GetOrder { order { id } }", "schema-a")); org.assertj.core.api.Assertions.assertThatThrownBy( () -> registry.register( GraphQlPersistedOperation.active( "get-order-v1", "GetOrder", "sha256:b", "query GetOrder { order { status } }", "schema-a"))) .isInstanceOf( GraphQlPersistedOperationConflictException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.persisted.GraphQlPersistedOperationRegistryTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GraphQlPersistedOperationStatus { ACTIVE, DEPRECATED, BLOCKED } public record GraphQlPersistedOperation( GraphQlPersistedOperationId id, String operationName, String documentHash, String canonicalDocument, String schemaContractHash, java.util.Set allowedClientProfiles, long maximumComplexity, int maximumVariablesBytes, GraphQlPersistedOperationStatus status) { public static GraphQlPersistedOperation active( String id, String operationName, String documentHash, String canonicalDocument, String schemaHash) { return new GraphQlPersistedOperation( new GraphQlPersistedOperationId(id), operationName, documentHash, canonicalDocument, schemaHash, java.util.Set.of("FIRST_PARTY"), 10_000, 65_536, GraphQlPersistedOperationStatus.ACTIVE); } } public interface GraphQlPersistedOperationRegistry { void register(GraphQlPersistedOperation operation); java.util.Optional find( GraphQlPersistedOperationId id); } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.persisted.GraphQlPersistedOperationRegistryTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationId.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperation.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationStatus.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRegistry.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/InMemoryGraphQlPersistedOperationRegistry.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationConflictException.java' 'modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRegistryTest.java' git commit -m "feat: add graphql persisted operation registry" ``` ### Task 3: Persisted Operation Lookup과 Execution Interceptor **Files:** - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRequest.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationLookup.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationInterceptor.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationPolicy.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRejectedException.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedPreparsedBridge.java` - Test: `modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationInterceptorTest.java` **Interfaces:** - Consumes: Persisted registry, Stable request context, client policy, schema hash와 preparsed cache. - Produces: Operation ID로 canonical document를 복원하고 client·schema·variables·complexity를 재검증하는 interceptor. **Implementation requirements:** - Client가 operation ID와 임의 query를 동시에 보내면 hash 일치 여부를 검증하거나 profile에 따라 거부한다. - Registry의 allowed client profile과 현재 client가 일치해야 한다. - Schema contract hash가 현재 schema와 맞지 않으면 실행하지 않는다. - Persisted record의 maximum complexity·variables limit와 현재 client policy 중 더 엄격한 값을 사용한다. - Lookup 성공이 authorization 성공을 의미하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlPersistedOperationInterceptorTest { @org.junit.jupiter.api.Test void blockedOperationIsRejectedBeforeExecution() { var operation = GraphQlPersistedOperation.active( "dangerous-v1", "Dangerous", "sha256:a", "query Dangerous { expensive }", "schema-a"); operation = new GraphQlPersistedOperation( operation.id(), operation.operationName(), operation.documentHash(), operation.canonicalDocument(), operation.schemaContractHash(), operation.allowedClientProfiles(), operation.maximumComplexity(), operation.maximumVariablesBytes(), GraphQlPersistedOperationStatus.BLOCKED); org.assertj.core.api.Assertions.assertThatThrownBy( () -> GraphQlPersistedOperationPolicy.requireActive( operation)) .isInstanceOf( GraphQlPersistedOperationRejectedException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.persisted.GraphQlPersistedOperationInterceptorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public final class GraphQlPersistedOperationPolicy { public static GraphQlPersistedOperation requireActive( GraphQlPersistedOperation operation) { if (operation.status() != GraphQlPersistedOperationStatus.ACTIVE) { throw new GraphQlPersistedOperationRejectedException( "persisted operation is not active"); } return operation; } private GraphQlPersistedOperationPolicy() {} } public record GraphQlPersistedOperationRequest( GraphQlPersistedOperationId operationId, String suppliedDocumentHash, int variablesBytes) { } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.persisted.GraphQlPersistedOperationInterceptorTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRequest.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationLookup.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationInterceptor.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationPolicy.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationRejectedException.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedPreparsedBridge.java' 'modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/persisted/GraphQlPersistedOperationInterceptorTest.java' git commit -m "feat: execute registered graphql operations" ``` ### Task 4: G4 Persisted Operation Admin·Usage·Block Plane **Files:** - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAdminService.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationUsage.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAudit.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationBlockCommand.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationRemovalGate.java` - Create: `modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAdminAuthorization.java` - Test: `modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationRemovalGateTest.java` **Interfaces:** - Consumes: Persisted registry, schema usage observation, admin actor·audit port. - Produces: Operation 등록·deprecate·incident block·remove를 감사 가능한 G4 관리 작업으로 제공. **Implementation requirements:** - 일반 GraphQL resolver나 application credential은 admin service에 접근하지 않는다. - Block는 즉시 실행 경로에 반영되며 cache를 우회하지 않는다. - Removal은 최근 usage, schema compatibility와 owner 승인 evidence를 요구한다. - 모든 변경에 operator, reason, before/after, timestamp와 trace ID를 감사한다. - Raw variables와 credential은 audit에 포함하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlPersistedOperationRemovalGateTest { @org.junit.jupiter.api.Test void recentUsageBlocksRemoval() { var gate = new GraphQlPersistedOperationRemovalGate( java.time.Duration.ofDays(30)); var usage = new GraphQlPersistedOperationUsage( java.time.Instant.now().minus( java.time.Duration.ofDays(1)), 12); org.assertj.core.api.Assertions.assertThatThrownBy( () -> gate.verify(usage, java.time.Instant.now())) .isInstanceOf( GraphQlPersistedOperationRemovalRejectedException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.admin.GraphQlPersistedOperationRemovalGateTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GraphQlPersistedOperationUsage( java.time.Instant lastUsedAt, long executions) { } public final class GraphQlPersistedOperationRemovalGate { private final java.time.Duration quietPeriod; public GraphQlPersistedOperationRemovalGate( java.time.Duration quietPeriod) { this.quietPeriod = quietPeriod; } public void verify( GraphQlPersistedOperationUsage usage, java.time.Instant now) { if (usage.executions() > 0 && usage.lastUsedAt().plus(quietPeriod) .isAfter(now)) { throw new GraphQlPersistedOperationRemovalRejectedException( "persisted operation used within quiet period"); } } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-persisted-operation:test --tests 'io.backend.skeleton.graphql.advanced.admin.GraphQlPersistedOperationRemovalGateTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAdminService.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationUsage.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAudit.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationBlockCommand.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationRemovalGate.java' 'modules/graphql-advanced/graphql-persisted-operation/src/main/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationAdminAuthorization.java' 'modules/graphql-advanced/graphql-persisted-operation/src/test/java/io/backend/skeleton/graphql/advanced/admin/GraphQlPersistedOperationRemovalGateTest.java' git commit -m "feat: add graphql operation admin plane" ``` ### Task 5: `graphql-transport-ws` WebSocket Protocol Adapter **Files:** - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocol.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProperties.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketHandlerFactory.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketLifecycle.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketConnectionId.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocolError.java` - Test: `modules/graphql-advanced/graphql-websocket/src/test/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocolTest.java` **Interfaces:** - Consumes: Spring GraphQL WebSocket handler, Stable execution service와 advanced feature guard. - Produces: `graphql-transport-ws` connection_init·subscribe·complete lifecycle와 bounded connection policy. **Implementation requirements:** - 과거 `subscriptions-transport-ws`를 지원 protocol로 광고하지 않는다. - connection init timeout, idle timeout, maximum connection age와 maximum subscriptions per connection을 설정한다. - Protocol error는 해당 operation 또는 connection scope에 맞게 종료한다. - 서버 shutdown에서 신규 subscribe를 거부하고 기존 stream을 drain한다. - Connection ID와 operation ID를 metric label로 사용하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlWebSocketProtocolTest { @org.junit.jupiter.api.Test void connectionInitAfterDeadlineIsRejected() { var lifecycle = new GraphQlWebSocketLifecycle( java.time.Duration.ofSeconds(5), java.time.Instant.parse("2026-08-12T00:00:00Z")); org.assertj.core.api.Assertions.assertThatThrownBy( () -> lifecycle.onConnectionInit( java.time.Instant.parse( "2026-08-12T00:00:06Z"))) .isInstanceOf(GraphQlWebSocketProtocolError.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-websocket:test --tests 'io.backend.skeleton.graphql.advanced.websocket.GraphQlWebSocketProtocolTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GraphQlWebSocketProtocol { GRAPHQL_TRANSPORT_WS("graphql-transport-ws"); private final String subProtocol; GraphQlWebSocketProtocol(String subProtocol) { this.subProtocol = subProtocol; } public String subProtocol() { return subProtocol; } } public final class GraphQlWebSocketLifecycle { private final java.time.Duration initTimeout; private final java.time.Instant connectedAt; public GraphQlWebSocketLifecycle( java.time.Duration initTimeout, java.time.Instant connectedAt) { this.initTimeout = initTimeout; this.connectedAt = connectedAt; } public void onConnectionInit(java.time.Instant now) { if (now.isAfter(connectedAt.plus(initTimeout))) { throw new GraphQlWebSocketProtocolError( "connection_init timeout"); } } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-websocket:test --tests 'io.backend.skeleton.graphql.advanced.websocket.GraphQlWebSocketProtocolTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocol.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProperties.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketHandlerFactory.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketLifecycle.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketConnectionId.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocolError.java' 'modules/graphql-advanced/graphql-websocket/src/test/java/io/backend/skeleton/graphql/advanced/websocket/GraphQlWebSocketProtocolTest.java' git commit -m "feat: add graphql websocket protocol" ``` ### Task 6: WebSocket Authentication Expiry·Revocation Lifecycle **Files:** - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketAuthenticationInterceptor.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketPrincipal.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketCredentialExpiry.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketRevocationSignal.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlSubscriptionAuthorizationPolicy.java` - Create: `modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketCloseReason.java` - Test: `modules/graphql-advanced/graphql-websocket/src/test/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketAuthenticationInterceptorTest.java` **Interfaces:** - Consumes: connection_init payload, security adapter, actor·tenant context, credential expiry와 revocation signal. - Produces: Connection과 subscription에 actor context를 고정하고 expiry·revocation 시 fail-closed 종료하는 lifecycle. **Implementation requirements:** - connection_init payload의 credential을 metric·log에 남기지 않는다. - 인증된 actor·tenant를 이후 operation context에 전달한다. - Credential expiry가 오면 connection을 종료하고 silent refresh를 임의 구현하지 않는다. - Resource 권한이 변할 수 있는 sensitive subscription은 event delivery 전 재검증 profile을 지원한다. - 한 connection에서 tenant context를 operation마다 바꿀 수 없다. - [ ] **Step 1: Write the failing test** ```java class GraphQlWebSocketAuthenticationInterceptorTest { @org.junit.jupiter.api.Test void expiredCredentialClosesConnection() { var expiry = new GraphQlWebSocketCredentialExpiry( java.time.Instant.parse("2026-08-12T00:00:00Z")); org.assertj.core.api.Assertions.assertThat( expiry.isExpired( java.time.Instant.parse( "2026-08-12T00:00:01Z"))) .isTrue(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-websocket:test --tests 'io.backend.skeleton.graphql.advanced.security.GraphQlWebSocketAuthenticationInterceptorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GraphQlWebSocketCredentialExpiry( java.time.Instant expiresAt) { public boolean isExpired(java.time.Instant now) { return !now.isBefore(expiresAt); } } public record GraphQlWebSocketPrincipal( String actorFingerprint, String tenantFingerprint, java.time.Instant expiresAt) { } public enum GraphQlWebSocketCloseReason { AUTHENTICATION_FAILED, CREDENTIAL_EXPIRED, AUTHORIZATION_REVOKED, SERVER_DRAINING } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-websocket:test --tests 'io.backend.skeleton.graphql.advanced.security.GraphQlWebSocketAuthenticationInterceptorTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketAuthenticationInterceptor.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketPrincipal.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketCredentialExpiry.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketRevocationSignal.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlSubscriptionAuthorizationPolicy.java' 'modules/graphql-advanced/graphql-websocket/src/main/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketCloseReason.java' 'modules/graphql-advanced/graphql-websocket/src/test/java/io/backend/skeleton/graphql/advanced/security/GraphQlWebSocketAuthenticationInterceptorTest.java' git commit -m "feat: secure graphql websocket lifecycle" ``` ### Task 7: Subscription Source SPI와 Bounded Backpressure **Files:** - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionSource.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionEvent.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionContext.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionBufferPolicy.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSlowConsumerPolicy.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionDispatcher.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionTermination.java` - Test: `modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionBufferPolicyTest.java` **Interfaces:** - Consumes: Messaging/Application Publisher source, actor context, Reactor Publisher와 WebSocket/SSE transport. - Produces: 내구성 보장을 과장하지 않는 bounded live stream과 slow-consumer termination. **Implementation requirements:** - Messaging ACK·offset·DLQ·replay를 GraphQL subscription이 재구현하지 않는다. - Default slow-consumer 정책은 silent drop이 아니라 connection/operation 종료다. - Maximum buffered events와 maximum event bytes를 설정한다. - Subscriber cancel이 upstream source에 전파된다. - Integration Event를 GraphQL DTO로 변환하고 원본 broker message를 그대로 노출하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlSubscriptionBufferPolicyTest { @org.junit.jupiter.api.Test void defaultPolicyTerminatesSlowConsumer() { var policy = GraphQlSubscriptionBufferPolicy.defaultPolicy(); org.assertj.core.api.Assertions.assertThat( policy.slowConsumerPolicy()) .isEqualTo( GraphQlSlowConsumerPolicy.TERMINATE); org.assertj.core.api.Assertions.assertThat( policy.maximumBufferedEvents()).isGreaterThan(0); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.subscription.GraphQlSubscriptionBufferPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GraphQlSlowConsumerPolicy { TERMINATE, DROP_ALLOWED_FOR_TELEMETRY } public record GraphQlSubscriptionBufferPolicy( int maximumBufferedEvents, long maximumBufferedBytes, GraphQlSlowConsumerPolicy slowConsumerPolicy) { public static GraphQlSubscriptionBufferPolicy defaultPolicy() { return new GraphQlSubscriptionBufferPolicy( 128, 1_048_576, GraphQlSlowConsumerPolicy.TERMINATE); } } public interface GraphQlSubscriptionSource { org.reactivestreams.Publisher open( GraphQlSubscriptionContext context); } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.subscription.GraphQlSubscriptionBufferPolicyTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionSource.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionEvent.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionContext.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionBufferPolicy.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSlowConsumerPolicy.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionDispatcher.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionTermination.java' 'modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionBufferPolicyTest.java' git commit -m "feat: add bounded graphql subscriptions" ``` ### Task 8: Subscription Ordering·Cancellation·Shutdown Profiles **Files:** - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionOrderingProfile.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionExecutionPolicy.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionCancellation.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionDrainCoordinator.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionState.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionMetrics.java` - Test: `modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionExecutionPolicyTest.java` **Interfaces:** - Consumes: Subscription source, GraphQL Java subscription execution strategy와 transport lifecycle. - Produces: `LOW_LATENCY`·`ORDERED` profile, cancellation과 graceful shutdown drain. **Implementation requirements:** - `ORDERED`는 source order를 보존하지만 head-of-line blocking과 buffer 비용을 명시한다. - `LOW_LATENCY`는 completion 순서가 source 순서와 다를 수 있음을 계약한다. - Ordering profile은 operation catalog에 등록한다. - Shutdown에서 신규 subscription을 거부하고 bounded time 동안 기존 subscription을 drain한다. - Cancellation은 resolver nested publisher와 upstream source 모두에 전파한다. - [ ] **Step 1: Write the failing test** ```java class GraphQlSubscriptionExecutionPolicyTest { @org.junit.jupiter.api.Test void orderedProfileRequestsGraphQlJavaOrderingFlag() { var policy = GraphQlSubscriptionExecutionPolicy.ordered(); org.assertj.core.api.Assertions.assertThat( policy.profile()) .isEqualTo( GraphQlSubscriptionOrderingProfile.ORDERED); org.assertj.core.api.Assertions.assertThat( policy.keepSourceOrder()).isTrue(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.subscription.GraphQlSubscriptionExecutionPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public enum GraphQlSubscriptionOrderingProfile { LOW_LATENCY, ORDERED } public record GraphQlSubscriptionExecutionPolicy( GraphQlSubscriptionOrderingProfile profile, boolean keepSourceOrder) { public static GraphQlSubscriptionExecutionPolicy ordered() { return new GraphQlSubscriptionExecutionPolicy( GraphQlSubscriptionOrderingProfile.ORDERED, true); } public static GraphQlSubscriptionExecutionPolicy lowLatency() { return new GraphQlSubscriptionExecutionPolicy( GraphQlSubscriptionOrderingProfile.LOW_LATENCY, false); } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.subscription.GraphQlSubscriptionExecutionPolicyTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionOrderingProfile.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionExecutionPolicy.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionCancellation.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionDrainCoordinator.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionState.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionMetrics.java' 'modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/subscription/GraphQlSubscriptionExecutionPolicyTest.java' git commit -m "feat: add graphql subscription ordering profiles" ``` ### Task 9: SSE Distinct Connection Subscription Adapter **Files:** - Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseProperties.java` - Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseHandlerFactory.java` - Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseConnectionPolicy.java` - Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseHeartbeat.java` - Create: `modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseTermination.java` - Test: `modules/graphql-advanced/graphql-sse/src/test/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseConnectionPolicyTest.java` **Interfaces:** - Consumes: Spring `GraphQlSseHandler`, Stable execution service, subscription buffer and auth policy. - Produces: POST JSON + `Accept: text/event-stream` 기반 Distinct Connection subscription transport. **Implementation requirements:** - SSE를 Query·Mutation의 일반 response mode로 사용하지 않고 subscription-only로 제한한다. - Connection당 하나의 subscription이라는 Distinct Connection 특성을 문서·metric에 반영한다. - Heartbeat, idle timeout, maximum duration과 proxy buffering 요구를 설정한다. - WebSocket과 동일한 actor·tenant·authorization·cost policy를 사용한다. - HTTP/2 사용 여부와 연결 규모를 load test로 검증한다. - [ ] **Step 1: Write the failing test** ```java class GraphQlSseConnectionPolicyTest { @org.junit.jupiter.api.Test void acceptsOnlySubscriptionOperation() { var policy = GraphQlSseConnectionPolicy.standard(); org.assertj.core.api.Assertions.assertThat( policy.supports("subscription")).isTrue(); org.assertj.core.api.Assertions.assertThat( policy.supports("query")).isFalse(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-sse:test --tests 'io.backend.skeleton.graphql.advanced.sse.GraphQlSseConnectionPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public final class GraphQlSseConnectionPolicy { public static GraphQlSseConnectionPolicy standard() { return new GraphQlSseConnectionPolicy(); } public boolean supports(String operationType) { return "subscription".equals(operationType); } } public record GraphQlSseProperties( java.time.Duration heartbeatInterval, java.time.Duration idleTimeout, java.time.Duration maximumDuration) { } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-sse:test --tests 'io.backend.skeleton.graphql.advanced.sse.GraphQlSseConnectionPolicyTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseProperties.java' 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseHandlerFactory.java' 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseConnectionPolicy.java' 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseHeartbeat.java' 'modules/graphql-advanced/graphql-sse/src/main/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseTermination.java' 'modules/graphql-advanced/graphql-sse/src/test/java/io/backend/skeleton/graphql/advanced/sse/GraphQlSseConnectionPolicyTest.java' git commit -m "feat: add graphql sse subscription transport" ``` ### Task 10: Messaging-backed Subscription Replay Extension **Files:** - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSubscriptionCursor.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayPosition.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplaySource.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSnapshotLiveHandoff.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayAuthorization.java` - Create: `modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayGapException.java` - Test: `modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSnapshotLiveHandoffTest.java` **Interfaces:** - Consumes: Messaging replay/offset capability, snapshot query, actor authorization와 signed cursor. - Produces: GraphQL 표준 보장과 분리된 snapshot + live handoff 및 explicit replay cursor extension. **Implementation requirements:** - GraphQL Core에 resume 표준이 있다고 광고하지 않는다. - Replay durability와 offset 보장은 Messaging platform이 소유한다. - Snapshot sequence와 live source 시작 position 사이에 gap·duplicate가 없도록 handoff를 검증한다. - Cursor는 actor/client·subscription profile에 bind하고 서명한다. - Replay window 밖의 cursor는 history-lost error로 반환한다. - [ ] **Step 1: Write the failing test** ```java class GraphQlSnapshotLiveHandoffTest { @org.junit.jupiter.api.Test void gapBetweenSnapshotAndLivePositionIsRejected() { var handoff = new GraphQlSnapshotLiveHandoff( new GraphQlReplayPosition(10), new GraphQlReplayPosition(12)); org.assertj.core.api.Assertions.assertThatThrownBy( handoff::verifyContiguous) .isInstanceOf(GraphQlReplayGapException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.replay.GraphQlSnapshotLiveHandoffTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GraphQlReplayPosition(long sequence) { public GraphQlReplayPosition { if (sequence < 0) { throw new IllegalArgumentException( "sequence cannot be negative"); } } } public record GraphQlSnapshotLiveHandoff( GraphQlReplayPosition snapshotPosition, GraphQlReplayPosition liveStartPosition) { public void verifyContiguous() { if (liveStartPosition.sequence() > snapshotPosition.sequence() + 1) { throw new GraphQlReplayGapException( "snapshot and live stream contain a gap"); } } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-subscription:test --tests 'io.backend.skeleton.graphql.advanced.replay.GraphQlSnapshotLiveHandoffTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSubscriptionCursor.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayPosition.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplaySource.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSnapshotLiveHandoff.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayAuthorization.java' 'modules/graphql-advanced/graphql-subscription/src/main/java/io/backend/skeleton/graphql/advanced/replay/GraphQlReplayGapException.java' 'modules/graphql-advanced/graphql-subscription/src/test/java/io/backend/skeleton/graphql/advanced/replay/GraphQlSnapshotLiveHandoffTest.java' git commit -m "feat: add graphql subscription replay extension" ``` ### Task 11: GraphQL Java 25 Chained DataLoader Opt-in **Files:** - Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedDataLoaderPolicy.java` - Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderDependencyGraph.java` - Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderCycleDetector.java` - Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedDispatchConfigurer.java` - Create: `modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedLoaderMetrics.java` - Test: `modules/graphql-advanced/graphql-dataloader-chaining/src/test/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderCycleDetectorTest.java` **Interfaces:** - Consumes: Stable request-scoped DataLoader registry와 GraphQL Java 25 chained dispatch capability. - Produces: 명시적 dependency graph를 가진 opt-in chained loader execution과 cycle·dispatch regression gate. **Implementation requirements:** - Stable default에서는 chained dispatch를 활성화하지 않는다. - 각 loader dependency를 manifest에 선언한다. - Dependency cycle이 있으면 startup 실패다. - Dispatch 순서 변경이 query count·batch size·result ordering에 미치는 영향을 contract test로 검증한다. - Request scope, actor·tenant isolation과 maximum batch size 규칙을 그대로 유지한다. - [ ] **Step 1: Write the failing test** ```java class GraphQlDataLoaderCycleDetectorTest { @org.junit.jupiter.api.Test void detectsLoaderDependencyCycle() { var graph = new GraphQlDataLoaderDependencyGraph() .dependsOn("orders", "customers") .dependsOn("customers", "orders"); org.assertj.core.api.Assertions.assertThatThrownBy( () -> new GraphQlDataLoaderCycleDetector().verify(graph)) .isInstanceOf( GraphQlDataLoaderDependencyCycleException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-dataloader-chaining:test --tests 'io.backend.skeleton.graphql.advanced.chaining.GraphQlDataLoaderCycleDetectorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public final class GraphQlDataLoaderDependencyGraph { private final java.util.Map> edges = new java.util.LinkedHashMap<>(); public GraphQlDataLoaderDependencyGraph dependsOn( String loader, String dependency) { edges.computeIfAbsent(loader, ignored -> new java.util.LinkedHashSet<>()) .add(dependency); return this; } java.util.Map> edges() { return java.util.Collections.unmodifiableMap(edges); } } public record GraphQlChainedDataLoaderPolicy( boolean enabled, int maximumDepth) { } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-dataloader-chaining:test --tests 'io.backend.skeleton.graphql.advanced.chaining.GraphQlDataLoaderCycleDetectorTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedDataLoaderPolicy.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderDependencyGraph.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderCycleDetector.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedDispatchConfigurer.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/main/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlChainedLoaderMetrics.java' 'modules/graphql-advanced/graphql-dataloader-chaining/src/test/java/io/backend/skeleton/graphql/advanced/chaining/GraphQlDataLoaderCycleDetectorTest.java' git commit -m "feat: add opt in chained graphql dataloaders" ``` ### Task 12: Federation Subgraph Schema와 Entity Resolver **Files:** - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationProperties.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationSchemaFactory.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityKey.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityResolver.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationBatchResolver.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationCapability.java` - Test: `modules/graphql-advanced/graphql-federation/src/test/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityResolverTest.java` **Interfaces:** - Consumes: Stable SDL assembly, federation-jvm integration, Application query services와 DataLoader. - Produces: 별도 opt-in Subgraph schema, `@key` entity mapping과 batch reference resolution. **Implementation requirements:** - Single executable schema가 기본이며 federation flag가 없으면 federation wiring을 등록하지 않는다. - Entity reference resolver가 Repository를 직접 호출하지 않고 Application query service를 사용한다. - Entity key는 versioned contract이며 변경 시 breaking review를 요구한다. - Batch entity resolution은 요청 단위 DataLoader와 tenant context를 사용한다. - Federation router나 supergraph 운영은 이 모듈이 소유하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlFederationEntityResolverTest { @org.junit.jupiter.api.Test void rejectsRepresentationMissingDeclaredKey() { var key = new GraphQlFederationEntityKey( "Order", java.util.List.of("id")); var resolver = new GraphQlFederationEntityResolver(key); org.assertj.core.api.Assertions.assertThatThrownBy( () -> resolver.validateRepresentation( java.util.Map.of("__typename", "Order"))) .isInstanceOf( GraphQlFederationRepresentationException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-federation:test --tests 'io.backend.skeleton.graphql.advanced.federation.GraphQlFederationEntityResolverTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GraphQlFederationEntityKey( String typeName, java.util.List fields) { public GraphQlFederationEntityKey { fields = java.util.List.copyOf(fields); if (fields.isEmpty()) { throw new IllegalArgumentException( "federation entity key cannot be empty"); } } } public final class GraphQlFederationEntityResolver { private final GraphQlFederationEntityKey key; public GraphQlFederationEntityResolver( GraphQlFederationEntityKey key) { this.key = key; } public void validateRepresentation( java.util.Map representation) { for (String field : key.fields()) { if (!representation.containsKey(field)) { throw new GraphQlFederationRepresentationException( "missing federation entity key field"); } } } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-federation:test --tests 'io.backend.skeleton.graphql.advanced.federation.GraphQlFederationEntityResolverTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationProperties.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationSchemaFactory.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityKey.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityResolver.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationBatchResolver.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationCapability.java' 'modules/graphql-advanced/graphql-federation/src/test/java/io/backend/skeleton/graphql/advanced/federation/GraphQlFederationEntityResolverTest.java' git commit -m "feat: add graphql federation subgraph" ``` ### Task 13: Federation Composition·Deployment·Failure Gate **Files:** - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlSubgraphContract.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionResult.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionGate.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationUsageReport.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationLatencyBudget.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationDeploymentOrder.java` - Create: `modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationReleaseEvidence.java` - Test: `modules/graphql-advanced/graphql-federation/src/test/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionGateTest.java` **Interfaces:** - Consumes: Subgraph SDL, entity keys, router composition result, usage·latency·failure evidence. - Produces: Composition CI, cross-subgraph dependency·latency·partial failure 검증과 배포 순서 gate. **Implementation requirements:** - Composition 성공만으로 release를 승인하지 않고 entity key, owner, downstream dependency, latency budget을 검증한다. - Cross-subgraph N+1과 per-entity downstream call을 performance gate에서 탐지한다. - Subgraph schema가 router보다 먼저 또는 호환되지 않는 순서로 배포되지 않도록 deployment order를 검증한다. - 부분 장애에서 nullable boundary와 error ownership을 contract test로 검증한다. - Router 설정과 운영은 별도 프로젝트의 evidence로 입력받는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlFederationCompositionGateTest { @org.junit.jupiter.api.Test void compositionWithoutLatencyEvidenceIsRejected() { var evidence = new GraphQlFederationReleaseEvidence( true, true, false, true); org.assertj.core.api.Assertions.assertThatThrownBy( () -> new GraphQlFederationCompositionGate() .verify(evidence)) .isInstanceOf( GraphQlFederationReleaseRejectedException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-federation:test --tests 'io.backend.skeleton.graphql.advanced.composition.GraphQlFederationCompositionGateTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GraphQlFederationReleaseEvidence( boolean compositionPassed, boolean entityContractsPassed, boolean latencyPassed, boolean failureContractsPassed) { } public final class GraphQlFederationCompositionGate { public void verify( GraphQlFederationReleaseEvidence evidence) { if (!evidence.compositionPassed() || !evidence.entityContractsPassed() || !evidence.latencyPassed() || !evidence.failureContractsPassed()) { throw new GraphQlFederationReleaseRejectedException( "federation composition evidence incomplete"); } } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-federation:test --tests 'io.backend.skeleton.graphql.advanced.composition.GraphQlFederationCompositionGateTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlSubgraphContract.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionResult.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionGate.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationUsageReport.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationLatencyBudget.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationDeploymentOrder.java' 'modules/graphql-advanced/graphql-federation/src/main/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationReleaseEvidence.java' 'modules/graphql-advanced/graphql-federation/src/test/java/io/backend/skeleton/graphql/advanced/composition/GraphQlFederationCompositionGateTest.java' git commit -m "chore: add graphql federation composition gate" ``` ### Task 14: Client·Transport DTO Code Generation **Files:** - Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlCodegenProfile.java` - Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlScalarMapping.java` - Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedSourceBoundary.java` - Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlClientOperationGenerator.java` - Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlTransportTypeGenerator.java` - Create: `modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedCompatibilityGate.java` - Test: `modules/graphql-advanced/graphql-codegen/src/test/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedSourceBoundaryTest.java` **Interfaces:** - Consumes: SDL, operation documents, scalar manifest와 selected codegen engine. - Produces: Client request/response model과 transport-only type을 생성하되 domain·repository를 생성하지 않는 tooling. **Implementation requirements:** - Domain Entity, Application Use Case interface와 Repository를 생성하지 않는다. - Generated source는 별도 directory·package에 두고 사람이 작성한 domain code를 덮어쓰지 않는다. - Custom scalar, nullability, union, interface와 oneOf mapping을 명시한다. - Operation document를 schema와 함께 compile-time 검증한다. - Schema additive change가 generated client의 exhaustive enum/union source compatibility를 깨는지 별도 보고한다. - [ ] **Step 1: Write the failing test** ```java class GraphQlGeneratedSourceBoundaryTest { @org.junit.jupiter.api.Test void domainAndRepositoryGenerationAreForbidden() { var boundary = GraphQlGeneratedSourceBoundary.standard(); org.assertj.core.api.Assertions.assertThat( boundary.isAllowed("CLIENT_RESPONSE")).isTrue(); org.assertj.core.api.Assertions.assertThat( boundary.isAllowed("DOMAIN_ENTITY")).isFalse(); org.assertj.core.api.Assertions.assertThat( boundary.isAllowed("REPOSITORY")).isFalse(); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-codegen:test --tests 'io.backend.skeleton.graphql.advanced.codegen.GraphQlGeneratedSourceBoundaryTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public final class GraphQlGeneratedSourceBoundary { private static final java.util.Set ALLOWED = java.util.Set.of( "CLIENT_REQUEST", "CLIENT_RESPONSE", "TRANSPORT_INPUT", "TRANSPORT_OUTPUT"); public static GraphQlGeneratedSourceBoundary standard() { return new GraphQlGeneratedSourceBoundary(); } public boolean isAllowed(String generatedType) { return ALLOWED.contains(generatedType); } } public record GraphQlScalarMapping( String scalarName, String javaType, String codecId) { } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-codegen:test --tests 'io.backend.skeleton.graphql.advanced.codegen.GraphQlGeneratedSourceBoundaryTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlCodegenProfile.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlScalarMapping.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedSourceBoundary.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlClientOperationGenerator.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlTransportTypeGenerator.java' 'modules/graphql-advanced/graphql-codegen/src/main/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedCompatibilityGate.java' 'modules/graphql-advanced/graphql-codegen/src/test/java/io/backend/skeleton/graphql/advanced/codegen/GraphQlGeneratedSourceBoundaryTest.java' git commit -m "build: add graphql client code generation" ``` ### Task 15: Allowlisted Spring Data GraphQL Compatibility **Files:** - Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposure.java` - Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryAllowlist.java` - Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryArgumentPolicy.java` - Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryPaginationPolicy.java` - Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryProjectionPolicy.java` - Create: `modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposureValidator.java` - Test: `modules/graphql-advanced/graphql-spring-data-compat/src/test/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposureValidatorTest.java` **Interfaces:** - Consumes: Spring `@GraphQlRepository`, Querydsl/QBE repository, schema coordinate와 explicit allowlist. - Produces: 자동 DataFetcher 기능을 제한된 compatibility path로 제공하고 persistence model 자동 노출을 차단. **Implementation requirements:** - 등록되지 않은 repository는 GraphQL DataFetcher로 자동 노출하지 않는다. - 허용 filter, sort, projection과 maximum page size를 coordinate별로 정의한다. - 기본 offset pagination 20개 동작을 암묵적으로 사용하지 않고 explicit policy를 요구한다. - Entity/Document를 output으로 직접 반환하지 않고 승인된 projection만 허용한다. - Stable resolver·Application Service 경계를 대체하는 주류 API로 문서화하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlRepositoryExposureValidatorTest { @org.junit.jupiter.api.Test void unregisteredRepositoryIsRejected() { var validator = new GraphQlRepositoryExposureValidator( GraphQlRepositoryAllowlist.empty()); org.assertj.core.api.Assertions.assertThatThrownBy( () -> validator.verify( new GraphQlRepositoryExposure( "OrderRepository", "Query.orders"))) .isInstanceOf( GraphQlRepositoryExposureRejectedException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-spring-data-compat:test --tests 'io.backend.skeleton.graphql.advanced.compat.GraphQlRepositoryExposureValidatorTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GraphQlRepositoryExposure( String repositoryName, String schemaCoordinate) { } public final class GraphQlRepositoryAllowlist { private final java.util.Set repositoryNames; private GraphQlRepositoryAllowlist( java.util.Set repositoryNames) { this.repositoryNames = java.util.Set.copyOf(repositoryNames); } public static GraphQlRepositoryAllowlist empty() { return new GraphQlRepositoryAllowlist( java.util.Set.of()); } public boolean contains(String repositoryName) { return repositoryNames.contains(repositoryName); } } public final class GraphQlRepositoryExposureValidator { private final GraphQlRepositoryAllowlist allowlist; public GraphQlRepositoryExposureValidator( GraphQlRepositoryAllowlist allowlist) { this.allowlist = allowlist; } public void verify(GraphQlRepositoryExposure exposure) { if (!allowlist.contains(exposure.repositoryName())) { throw new GraphQlRepositoryExposureRejectedException( exposure.repositoryName()); } } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-spring-data-compat:test --tests 'io.backend.skeleton.graphql.advanced.compat.GraphQlRepositoryExposureValidatorTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposure.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryAllowlist.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryArgumentPolicy.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryPaginationPolicy.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryProjectionPolicy.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/main/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposureValidator.java' 'modules/graphql-advanced/graphql-spring-data-compat/src/test/java/io/backend/skeleton/graphql/advanced/compat/GraphQlRepositoryExposureValidatorTest.java' git commit -m "feat: add graphql spring data compat guard" ``` ### Task 16: RSocket GraphQL Experimental Transport **Files:** - Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketProperties.java` - Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketRoutePolicy.java` - Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketHandlerFactory.java` - Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketAuthentication.java` - Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketErrorMapper.java` - Create: `modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketCapability.java` - Test: `modules/graphql-advanced/graphql-rsocket/src/test/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketRoutePolicyTest.java` **Interfaces:** - Consumes: Spring GraphQL RSocket handler, Stable execution service와 experimental feature approval. - Produces: 내부 시스템용 request-response Query/Mutation과 request-stream Subscription experimental transport. **Implementation requirements:** - RSocket는 public default transport가 아니다. - Query·Mutation은 request-response, Subscription은 request-stream으로만 매핑한다. - Stable HTTP/WebSocket과 동일한 actor·tenant·cost·error policy를 적용한다. - Route·metadata MIME type·authentication을 allowlist한다. - Production 활성화에는 explicit consumer, load·failure evidence와 owner가 필요하다. - [ ] **Step 1: Write the failing test** ```java class GraphQlRSocketRoutePolicyTest { @org.junit.jupiter.api.Test void rejectsUnknownRoute() { var policy = new GraphQlRSocketRoutePolicy( java.util.Set.of("graphql")); org.assertj.core.api.Assertions.assertThatThrownBy( () -> policy.requireAllowed("admin.raw")) .isInstanceOf( GraphQlRSocketRouteRejectedException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-rsocket:test --tests 'io.backend.skeleton.graphql.advanced.rsocket.GraphQlRSocketRoutePolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public final class GraphQlRSocketRoutePolicy { private final java.util.Set allowedRoutes; public GraphQlRSocketRoutePolicy( java.util.Set allowedRoutes) { this.allowedRoutes = java.util.Set.copyOf(allowedRoutes); } public String requireAllowed(String route) { if (!allowedRoutes.contains(route)) { throw new GraphQlRSocketRouteRejectedException(route); } return route; } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-rsocket:test --tests 'io.backend.skeleton.graphql.advanced.rsocket.GraphQlRSocketRoutePolicyTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketProperties.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketRoutePolicy.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketHandlerFactory.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketAuthentication.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketErrorMapper.java' 'modules/graphql-advanced/graphql-rsocket/src/main/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketCapability.java' 'modules/graphql-advanced/graphql-rsocket/src/test/java/io/backend/skeleton/graphql/advanced/rsocket/GraphQlRSocketRoutePolicyTest.java' git commit -m "feat: add experimental graphql rsocket" ``` ### Task 17: GraphQL over HTTP GET Draft Profile **Files:** - Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetProfile.java` - Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetRequestParser.java` - Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetCachePolicy.java` - Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetCsrfPolicy.java` - Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetOperationPolicy.java` - Create: `modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpDraftCompatibilityReport.java` - Test: `modules/graphql-advanced/graphql-http-draft/src/test/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetOperationPolicyTest.java` **Interfaces:** - Consumes: GraphQL over HTTP Stage 2 Draft, Stable HTTP V1 policy와 experimental flag. - Produces: Query-only GET parsing, cache·CSRF 정책과 draft compatibility report. **Implementation requirements:** - Mutation과 Subscription을 GET으로 허용하지 않는다. - Query, operationName, variables, extensions의 URI 크기와 encoding을 제한한다. - Cookie credential 사용 시 CSRF와 cache behavior를 명시적으로 검증한다. - Raw query와 variables가 access log, referer 또는 metric에 노출될 위험을 문서·테스트한다. - Draft 변화가 Stable POST contract를 변경하지 않는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlHttpGetOperationPolicyTest { @org.junit.jupiter.api.Test void mutationOverGetIsRejected() { var policy = GraphQlHttpGetOperationPolicy.queryOnly(); org.assertj.core.api.Assertions.assertThatThrownBy( () -> policy.verify("mutation")) .isInstanceOf( GraphQlHttpGetRejectedException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-http-draft:test --tests 'io.backend.skeleton.graphql.advanced.get.GraphQlHttpGetOperationPolicyTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public final class GraphQlHttpGetOperationPolicy { public static GraphQlHttpGetOperationPolicy queryOnly() { return new GraphQlHttpGetOperationPolicy(); } public void verify(String operationType) { if (!"query".equals(operationType)) { throw new GraphQlHttpGetRejectedException( "GET supports query operations only"); } } } public record GraphQlHttpGetProfile( int maximumUriBytes, boolean sharedCacheAllowed, boolean csrfRequired) { } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-http-draft:test --tests 'io.backend.skeleton.graphql.advanced.get.GraphQlHttpGetOperationPolicyTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetProfile.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetRequestParser.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetCachePolicy.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetCsrfPolicy.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetOperationPolicy.java' 'modules/graphql-advanced/graphql-http-draft/src/main/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpDraftCompatibilityReport.java' 'modules/graphql-advanced/graphql-http-draft/src/test/java/io/backend/skeleton/graphql/advanced/get/GraphQlHttpGetOperationPolicyTest.java' git commit -m "feat: add experimental graphql http get" ``` ### Task 18: Incremental Delivery Experimental Profile **Files:** - Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalDeliveryCapability.java` - Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalDeliveryProfile.java` - Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalPatch.java` - Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalTransportPolicy.java` - Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCancellation.java` - Create: `modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCompatibilityGate.java` - Test: `modules/graphql-advanced/graphql-incremental-delivery/src/test/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCompatibilityGateTest.java` **Interfaces:** - Consumes: 실험 기능을 지원하는 GraphQL Java/Spring 조합, multipart/mixed 또는 streaming transport capability. - Produces: `@defer`·`@stream` 계열 기능을 Stable contract와 분리한 version-gated experimental profile. **Implementation requirements:** - September 2025 Stable schema contract에 존재하지 않거나 구현 조합이 안정화되지 않은 기능을 자동 활성화하지 않는다. - 초기 result와 후속 patch의 error·path·ordering·cancellation 계약을 별도로 검증한다. - Stable HTTP response byte budget과 timeout을 우회하지 않는다. - Client capability negotiation 없이 incremental response를 반환하지 않는다. - Compatibility gate가 실패하면 일반 non-incremental execution으로 silent fallback하지 않고 설정 오류로 차단한다. - [ ] **Step 1: Write the failing test** ```java class GraphQlIncrementalCompatibilityGateTest { @org.junit.jupiter.api.Test void unsupportedRuntimeCannotEnableIncrementalDelivery() { var gate = new GraphQlIncrementalCompatibilityGate(); org.assertj.core.api.Assertions.assertThatThrownBy( () -> gate.verify( new GraphQlIncrementalDeliveryCapability( false, false))) .isInstanceOf( GraphQlIncrementalDeliveryRejectedException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-incremental-delivery:test --tests 'io.backend.skeleton.graphql.advanced.incremental.GraphQlIncrementalCompatibilityGateTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GraphQlIncrementalDeliveryCapability( boolean engineSupported, boolean transportSupported) { } public final class GraphQlIncrementalCompatibilityGate { public void verify( GraphQlIncrementalDeliveryCapability capability) { if (!capability.engineSupported() || !capability.transportSupported()) { throw new GraphQlIncrementalDeliveryRejectedException( "incremental delivery runtime unsupported"); } } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-incremental-delivery:test --tests 'io.backend.skeleton.graphql.advanced.incremental.GraphQlIncrementalCompatibilityGateTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalDeliveryCapability.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalDeliveryProfile.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalPatch.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalTransportPolicy.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCancellation.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/main/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCompatibilityGate.java' 'modules/graphql-advanced/graphql-incremental-delivery/src/test/java/io/backend/skeleton/graphql/advanced/incremental/GraphQlIncrementalCompatibilityGateTest.java' git commit -m "feat: add experimental graphql incremental delivery" ``` ### Task 19: Advanced Capability Promotion·Soak·Release Gate **Files:** - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseEvidence.java` - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseGate.java` - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedPromotionDecision.java` - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedSoakScenario.java` - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedCompatibilityMatrix.java` - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedRunbookIndex.java` - Create: `modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseFailure.java` - Test: `modules/graphql-advanced/graphql-advanced-bootstrap/src/test/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseGateTest.java` **Interfaces:** - Consumes: Stable Task 1–48 release evidence와 Advanced capability별 composition·soak·security·compatibility evidence. - Produces: Advanced Stable 승격과 Experimental 유지·철회를 결정하는 capability별 release gate. **Implementation requirements:** - Stable Task 1–48의 release gate가 통과하지 않으면 Advanced 계획을 시작하지 않는다. - WebSocket·SSE는 장기 connection soak, slow consumer, auth expiry, cancellation, shutdown drain evidence를 요구한다. - Federation은 composition, router integration, cross-subgraph latency와 partial failure evidence를 요구한다. - Persisted Operation은 registry durability, block propagation, usage와 schema compatibility evidence를 요구한다. - RSocket·HTTP GET·Incremental Delivery는 명시적 승격 ADR 전까지 Experimental로 남는다. - [ ] **Step 1: Write the failing test** ```java class GraphQlAdvancedReleaseGateTest { @org.junit.jupiter.api.Test void advancedCannotReleaseBeforeStableBaseline() { var evidence = new GraphQlAdvancedReleaseEvidence( false, true, true, true, true); org.assertj.core.api.Assertions.assertThatThrownBy( () -> new GraphQlAdvancedReleaseGate().verify(evidence)) .isInstanceOf(GraphQlAdvancedReleaseFailure.class) .hasMessageContaining("stable"); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-advanced-bootstrap:test --tests 'io.backend.skeleton.graphql.advanced.release.GraphQlAdvancedReleaseGateTest' ``` Expected: FAIL because the production contract defined by this task does not exist or rejects the required invariant incorrectly. - [ ] **Step 3: Implement the smallest complete production contract** ```java public record GraphQlAdvancedReleaseEvidence( boolean stableBaselinePassed, boolean capabilityContractsPassed, boolean securityPassed, boolean soakPassed, boolean compatibilityPassed) { } public final class GraphQlAdvancedReleaseGate { public void verify(GraphQlAdvancedReleaseEvidence evidence) { if (!evidence.stableBaselinePassed()) { throw new GraphQlAdvancedReleaseFailure( "stable graphql baseline must pass first"); } if (!evidence.capabilityContractsPassed() || !evidence.securityPassed() || !evidence.soakPassed() || !evidence.compatibilityPassed()) { throw new GraphQlAdvancedReleaseFailure( "advanced graphql evidence incomplete"); } } } ``` Implement all listed files with the exact public names and invariants above. Keep persistence, provider, credential and dynamic identifier types outside the public contract. - [ ] **Step 4: Run the focused test and the owning suite** Run: ```bash ./gradlew :modules:graphql-advanced:graphql-advanced-bootstrap:test --tests 'io.backend.skeleton.graphql.advanced.release.GraphQlAdvancedReleaseGateTest' ./gradlew graphqlAdvancedTest ``` Expected: PASS for the focused test and the aggregate suite. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseEvidence.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseGate.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedPromotionDecision.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedSoakScenario.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedCompatibilityMatrix.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedRunbookIndex.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/main/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseFailure.java' 'modules/graphql-advanced/graphql-advanced-bootstrap/src/test/java/io/backend/skeleton/graphql/advanced/release/GraphQlAdvancedReleaseGateTest.java' git commit -m "chore: add graphql advanced release gate" ```