feat(graphql): GraphQL API 실행 플랫폼 구현 (Stable 48 + Advanced 19 Task)

설계 문서(specs/2026-08-12-graphql-api-execution-platform-design.md)와 두 실행 계획서에
선언된 create path 전량을 adapter-inbound-graphql leaf 안에 구현한다.

- 계획서 main 클래스 322개 전량, Task별 테스트 클래스 67개(Stable 48 + Advanced 19) 전량.
- 설계서의 Stable 16 + Advanced 12 "Gradle 모듈"은 modules.json 이 19개 leaf 정체성을
  소유하므로 bounded sub-package 로 매핑한다(선례: httpclient leaf). 모듈 경계는 문서가
  아니라 GraphQlStableModule/GraphQlAdvancedModule 값 선언 + GraphQlModuleBoundaryTest 의
  실제 소스 스캔으로 기계 검증한다.
- architecture/ 규칙은 리플렉션 + 단순명 매칭으로 구현한다. 인바운드 어댑터가 자신이
  금지하는 jakarta.persistence/spring-tx 에 의존해야 검사할 수 있다면 본말전도이기 때문.
- 부분 실패는 HTTP 200 + partial data, 요청 실패는 4xx. GraphQL over HTTP 초안 status 294 는
  의도적으로 미채택(초안 변경이 클라이언트를 깨뜨리므로).
- 요청 단위 DB 트랜잭션을 열지 않는다. 커서는 HMAC 서명된 버전 있는 keyset(상수 시간 비교).
- DataLoader 는 요청 스코프, 캐시 키는 actor/tenant sha256 지문으로 격리.
- Advanced capability 는 전부 기본 비활성. EXPERIMENTAL 등급은 명시 승인 없이 production
  활성화가 거부된다.
- spring-webflux 는 compileOnly(runtimeClasspath 제외) — MVC 배치가 WebFlux 런타임을
  물려받지 않도록. lockfile 이 스코프 제한을 고정.
- graphqlPerformanceTest 는 성능 태그가 0개면 실패한다. failOnNoDiscoveredTests 는 태그
  필터로 0건이 된 경우를 잡지 못해(Gradle 9.0.0 실측) 결과 검사를 추가했다. 증거 부재를
  통과로 위장하지 않기 위한 fail-closed.

검증: graphqlStableTest 404 / graphqlContractTest 9 / graphqlAdvancedTest 141 tests,
:adapter:inbound:graphql:check, verifyCleanArchitectureDependencies,
CleanArchitectureTest, verifyConfigurationPropertiesProcessor, verifyEnvKeys,
verifyPublicPathSnapshot 전부 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-14 15:41:38 +09:00
co-authored by Claude Opus 5
parent 3b5aee50e3
commit b074c1494e
448 changed files with 28244 additions and 13 deletions
@@ -0,0 +1,95 @@
// GraphQL API 실행 플랫폼 verification lanes.
//
// The GraphQL platform design package ships a 16-module Stable map and a 12-module Advanced map.
// This repository's registry (`src/config/architecture/modules.json`) owns exactly 19 leaf
// identities, so those maps are realised as bounded PACKAGES inside the single registered
// `adapter-inbound-graphql` leaf — the same mapping the httpclient capability already uses. The
// module identities, their allowed internal dependency edges and the Stable→Advanced isolation rule
// stay machine-checked through `GraphQlBuildModel` and `GraphQlModuleBoundaryTest`.
//
// Lanes (design §24, Stable plan Task 1 / Task 48):
// graphqlStableTest Stable platform unit + boundary tests (default lane)
// graphqlContractTest cross-module contract suites (@Tag("graphql-contract"))
// graphqlAdvancedTest Advanced/Experimental capability tests (@Tag("graphql-advanced"))
// graphqlPerformanceTest load/soak/fault scenarios (@Tag("graphql-performance"))
//
// `graphql-performance` is excluded from the default `test` task so external load and soak work can
// never run inside the unit lane.
ext.registerGraphQlPlatformTestLanes = { ->
String platformPackage = 'dev.caskeleton.adapter.inbound.graphql'
tasks.named('test') {
useJUnitPlatform {
excludeTags 'quarantine', 'graphql-performance'
}
}
Closure<Void> configureLane = { org.gradle.api.tasks.testing.Test lane ->
lane.group = 'verification'
lane.testClassesDirs = sourceSets.test.output.classesDirs
lane.classpath = sourceSets.test.runtimeClasspath
lane.jvmArgs '-Duser.timezone=UTC'
lane.outputs.upToDateWhen { false }
}
tasks.register('graphqlStableTest', Test) {
description = 'Runs the Stable GraphQL platform test lane (Stable plan Task 1-48).'
configureLane(it)
useJUnitPlatform {
excludeTags 'quarantine', 'graphql-performance', 'graphql-advanced'
}
filter {
includeTestsMatching "${platformPackage}.*"
failOnNoMatchingTests = true
}
failOnNoDiscoveredTests = true
}
tasks.register('graphqlContractTest', Test) {
description = 'Runs the GraphQL cross-module contract lane (Stable plan Task 47).'
configureLane(it)
useJUnitPlatform {
includeTags 'graphql-contract'
excludeTags 'quarantine'
}
failOnNoDiscoveredTests = true
}
tasks.register('graphqlAdvancedTest', Test) {
description = 'Runs the Advanced/Experimental GraphQL capability lane (Advanced plan Task 1-19).'
configureLane(it)
useJUnitPlatform {
includeTags 'graphql-advanced'
excludeTags 'quarantine'
}
failOnNoDiscoveredTests = true
}
tasks.register('graphqlPerformanceTest', Test) {
description = 'Runs the GraphQL load, soak and fault scenario lane (design §24.3, §24.4).'
configureLane(it)
useJUnitPlatform {
includeTags 'graphql-performance'
excludeTags 'quarantine'
}
// The Stable gate requires real load/fault evidence before a Stable release claim, so an
// empty run here is a missing-evidence condition rather than a pass.
//
// `failOnNoDiscoveredTests` alone does NOT cover this: it reacts to an empty candidate class
// scan, and this lane always scans a non-empty test tree that JUnit then tag-filters down to
// zero. Without the explicit result check below the lane reports BUILD SUCCESSFUL while
// proving nothing. Verified empirically on Gradle 9.0.0.
failOnNoDiscoveredTests = true
doLast {
File resultsDir = reports.junitXml.outputLocation.get().asFile
File[] executed = resultsDir.listFiles({ File file -> file.name.endsWith('.xml') } as FileFilter)
if (executed == null || executed.length == 0) {
throw new GradleException(
'graphqlPerformanceTest ran no scenario: the Stable release gate requires real ' +
'load, soak and fault evidence, so an empty performance lane is a missing-evidence ' +
'failure, not a pass. Register @Tag("graphql-performance") scenarios or run the ' +
'lane against the external load environment that owns them.')
}
}
}
}