feat: web, websocket 어댑터 추가 구현
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
44ba9931722364a53fcb3b5f31a1d539eabcaf42db775f5a33fb558f558c7504 README.md
|
||||
d064f0ac6c3be0e5c76ef22454db2a97e1d78ed287bd22f4c125f19aba3ad8e3 VALIDATION.md
|
||||
1ef15812f33dc998a6332b87523ed5942ba46d79d984a0ca776b05bb9247a06a docs/superpowers/plans/2026-08-12-graphql-advanced-capabilities-expansion-plan.md
|
||||
5ae70b53e22cdb852b2bb0df171dec868bfe99b15bb8e71fb2b0b3431cd7e2cd docs/superpowers/plans/2026-08-12-graphql-api-execution-platform-implementation-plan.md
|
||||
8d0203203f6bfe4b2e18625eff23bb308ba6454703a4ca4cd3236dab31ecafc3 docs/superpowers/specs/2026-08-12-graphql-api-execution-platform-design.md
|
||||
8048fe6a536de67d2cf5b0df05d35128f2c68ba8f0dd615831b40430fc76277b validate_graphql_docs.py
|
||||
@@ -0,0 +1,43 @@
|
||||
# GraphQL Superpowers 설계 패키지
|
||||
|
||||
이 패키지는 `GraphQL API 실행 플랫폼 심층 리서치`를 구현 기준선으로 변환한 설계서와 실행 계획서다.
|
||||
|
||||
## 문서
|
||||
|
||||
- `docs/superpowers/specs/2026-08-12-graphql-api-execution-platform-design.md`
|
||||
- Stable·Advanced 전체 아키텍처, 공개 계약, 경계, 실패 의미론, 테스트와 지원 등급
|
||||
- 입력 심층 리서치 원문을 추적 부록으로 포함
|
||||
- `docs/superpowers/plans/2026-08-12-graphql-api-execution-platform-implementation-plan.md`
|
||||
- Stable 구현 Task 1–48
|
||||
- `docs/superpowers/plans/2026-08-12-graphql-advanced-capabilities-expansion-plan.md`
|
||||
- Stable Release Gate 이후 실행하는 Advanced·Experimental Task 1–19
|
||||
- `VALIDATION.md`
|
||||
- 정적 검증 결과와 검증 범위
|
||||
- `validate_graphql_docs.py`
|
||||
- 패키지 내부 문서 재검증 스크립트
|
||||
- `MANIFEST.sha256`
|
||||
- 패키지 파일 무결성 목록
|
||||
|
||||
## 구현 순서
|
||||
|
||||
```text
|
||||
Stable Task 1–48
|
||||
→ Stable Release Gate
|
||||
→ Advanced Task 1–19
|
||||
→ Capability별 Promotion Gate
|
||||
```
|
||||
|
||||
## 명시적 전제
|
||||
|
||||
```text
|
||||
Java 21
|
||||
Gradle Kotlin DSL
|
||||
Spring Boot 4.1 BOM
|
||||
Spring for GraphQL 2.0
|
||||
Boot-managed GraphQL Java v25 계열
|
||||
Stable module root: modules/graphql
|
||||
Advanced module root: modules/graphql-advanced
|
||||
Root package: io.backend.skeleton.graphql
|
||||
```
|
||||
|
||||
실제 저장소에 적용할 때 기존 package·version catalog·module naming에 맞춰 경로만 조정하고, 문서의 공개 계약·불변 조건·테스트 의미는 유지한다.
|
||||
@@ -0,0 +1,98 @@
|
||||
# GraphQL Superpowers 문서 정적 검증 결과
|
||||
|
||||
- **검증 시각 기준:** 2026-08-12
|
||||
- **검증 대상:** 설계서 1개, Stable 구현 계획서 1개, Advanced·Experimental 확장 계획서 1개
|
||||
- **검증 명령:** `python3 validate_graphql_docs.py`
|
||||
- **결과:** **PASS**
|
||||
- **실행 검사:** 1,475
|
||||
- **통과:** 1,475
|
||||
- **실패:** 0
|
||||
|
||||
## 문서 규모
|
||||
|
||||
| 문서 | 행 수 | 크기 |
|
||||
|---|---:|---:|
|
||||
| GraphQL API 실행 플랫폼 설계서 | 2,553 | 93,359 bytes |
|
||||
| Stable 구현 계획서 | 4,560 | 209,041 bytes |
|
||||
| Advanced 확장 계획서 | 1,976 | 105,717 bytes |
|
||||
|
||||
## 계획 구조
|
||||
|
||||
| 항목 | Stable | Advanced |
|
||||
|---|---:|---:|
|
||||
| Task 수 | 48 | 19 |
|
||||
| Create 경로 수 | 227 | 113 |
|
||||
| Task 번호 연속성 | PASS | PASS |
|
||||
| 모든 Task의 `Files`·`Interfaces` | PASS | PASS |
|
||||
| 모든 Task의 Implementation Requirements | PASS | PASS |
|
||||
| 모든 Task의 Step 1–5 | PASS | PASS |
|
||||
| 실패·통과 예상 결과 | PASS | PASS |
|
||||
| Task별 Git commit 명령 | PASS | PASS |
|
||||
| Create 경로 중복 | 없음 | 없음 |
|
||||
| Stable·Advanced 경로 충돌 | 없음 | 없음 |
|
||||
|
||||
## 핵심 계약 검증
|
||||
|
||||
```text
|
||||
SDL-first external contract
|
||||
Single Executable Schema Stable default
|
||||
HTTP POST Stable profile
|
||||
application/graphql-response+json preferred
|
||||
Validation 이후 Field Error·Partial Data는 HTTP 200
|
||||
Draft 294는 Stable에서 제외
|
||||
JPA Entity·MongoDB Document 직접 노출 금지
|
||||
GraphQL Multipart Upload 미지원·Fileserver 사용
|
||||
request-wide database transaction 금지
|
||||
DataLoader request scope
|
||||
Finite Fetch Profile
|
||||
HMAC-signed cursor
|
||||
Mutation idempotency·expected version 분리
|
||||
Parser·shape·complexity·runtime response budget
|
||||
Actor·Field·Object·Tenant authorization
|
||||
Low-cardinality observability
|
||||
Stable/Advanced dependency isolation
|
||||
Persisted Operation·WebSocket·SSE·Federation 분리
|
||||
RSocket·HTTP GET·Incremental Delivery Experimental
|
||||
```
|
||||
|
||||
위 계약은 설계서와 계획서의 필수 문자열·모듈 경로·Task별 파일·테스트를 대조해 검증했습니다.
|
||||
|
||||
## 입력 리서치 추적성
|
||||
|
||||
- 첨부된 `GraphQL API 실행 플랫폼 심층 리서치` 원문 전체가 설계서의 `부록 B`에 포함되어 있습니다.
|
||||
- 설계 본문은 원문의 용어와 결론을 유지하면서 구현 판단을 Stable·Advanced·Experimental로 고정합니다.
|
||||
- 설계서와 입력 원문의 exact text 포함 검사를 별도로 통과했습니다.
|
||||
|
||||
## 패키지 검증 항목
|
||||
|
||||
```text
|
||||
문서 파일 존재
|
||||
Markdown code fence 균형
|
||||
Task 1–48 / 1–19 연속성
|
||||
Task별 테스트·명령·commit
|
||||
정확한 Create 경로
|
||||
Placeholder 금지
|
||||
Stable module에 WebSocket·Federation·Persisted Operation 경로 부재
|
||||
Advanced module에 feature flag와 capability 경로 존재
|
||||
금지 API pattern 부재
|
||||
문서 SHA-256 계산
|
||||
```
|
||||
|
||||
## 검증 범위의 한계
|
||||
|
||||
현재 PASS는 **문서의 정적 구조, 요구사항 추적성, 내부 계약과 실행 계획의 완결성**을 의미합니다. 실제 Backend Skeleton 저장소가 입력으로 제공되지 않았으므로 다음은 실행하지 않았습니다.
|
||||
|
||||
```text
|
||||
Gradle configuration·compile
|
||||
Spring Boot ApplicationContext 기동
|
||||
SchemaMappingInspector 실제 결과
|
||||
GraphQlTester HTTP·WebFlux contract
|
||||
JPA·MongoDB statement/query-count integration
|
||||
query bomb·complexity load test
|
||||
Virtual Thread·event-loop blocking test
|
||||
WebSocket·SSE soak test
|
||||
Federation composition·router integration
|
||||
actual Git commit
|
||||
```
|
||||
|
||||
실제 구현에서는 Stable Task 1–48을 먼저 수행해 Stable Release Gate를 통과한 뒤 Advanced Task 1–19를 시작해야 합니다.
|
||||
+1976
File diff suppressed because it is too large
Load Diff
+4560
File diff suppressed because it is too large
Load Diff
+2553
File diff suppressed because it is too large
Load Diff
+249
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import hashlib
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
DESIGN = ROOT / "docs/superpowers/specs/2026-08-12-graphql-api-execution-platform-design.md"
|
||||
STABLE = ROOT / "docs/superpowers/plans/2026-08-12-graphql-api-execution-platform-implementation-plan.md"
|
||||
ADVANCED = ROOT / "docs/superpowers/plans/2026-08-12-graphql-advanced-capabilities-expansion-plan.md"
|
||||
|
||||
checks: list[tuple[str, bool, str]] = []
|
||||
|
||||
def check(name: str, condition: bool, detail: str = "") -> None:
|
||||
checks.append((name, bool(condition), detail))
|
||||
|
||||
def read(path: Path) -> str:
|
||||
check(f"file exists: {path.name}", path.exists(), str(path))
|
||||
return path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
|
||||
design = read(DESIGN)
|
||||
stable = read(STABLE)
|
||||
advanced = read(ADVANCED)
|
||||
|
||||
# Basic document integrity
|
||||
check("design line floor", len(design.splitlines()) >= 2000, str(len(design.splitlines())))
|
||||
check("stable plan line floor", len(stable.splitlines()) >= 4000, str(len(stable.splitlines())))
|
||||
check("advanced plan line floor", len(advanced.splitlines()) >= 1500, str(len(advanced.splitlines())))
|
||||
for label, text in [("design", design), ("stable", stable), ("advanced", advanced)]:
|
||||
check(f"{label} code fences balanced", text.count("```") % 2 == 0, str(text.count("```")))
|
||||
for marker in ["TODO", "TBD", "FIXME", "implement later", "fill in details"]:
|
||||
check(f"{label} no placeholder {marker}", marker.lower() not in text.lower())
|
||||
|
||||
# Design required sections and source traceability
|
||||
required_design_terms = [
|
||||
"# GraphQL API 실행 플랫폼 설계서",
|
||||
"GraphQL Platform owns",
|
||||
"Domain/Application owns",
|
||||
"G1 Standard GraphQL API",
|
||||
"G2 Advanced Execution",
|
||||
"G3 GraphQL Extension",
|
||||
"G4 Admin Plane",
|
||||
"SDL",
|
||||
"September 2025",
|
||||
"application/graphql-response+json",
|
||||
"HTTP `200`",
|
||||
"GraphQlRequestContext",
|
||||
"DataLoader",
|
||||
"GraphQlFetchProfile",
|
||||
"HMAC",
|
||||
"Idempotency",
|
||||
"Partial Data",
|
||||
"Persisted Operation",
|
||||
"Subscription",
|
||||
"Federation",
|
||||
"GraphQL Multipart Upload",
|
||||
"Fileserver",
|
||||
"부록 B. 입력 심층 리서치 원문",
|
||||
"# GraphQL API 실행 플랫폼 심층 리서치",
|
||||
]
|
||||
for term in required_design_terms:
|
||||
check(f"design contains {term}", term in design)
|
||||
|
||||
# Critical design invariants
|
||||
critical_pairs = [
|
||||
("field error uses HTTP 200", "field error" in design.lower() and "HTTP `200`" in design),
|
||||
("no draft 294 stable", "294" in design and "Stable" in design),
|
||||
("dataloader request scope", "request" in design.lower() and "DataLoader" in design),
|
||||
("cursor HMAC", "Cursor" in design and "HMAC" in design),
|
||||
("no multipart upload", "Multipart Upload" in design and "Fileserver" in design),
|
||||
("single schema default", "Single Executable Schema" in design),
|
||||
("request-wide transaction prohibited", "request-wide" in design.lower() and "transaction" in design.lower()),
|
||||
("entity/document boundary", "JPA Entity" in design and "MongoDB Document" in design),
|
||||
]
|
||||
for name, condition in critical_pairs:
|
||||
check(name, condition)
|
||||
|
||||
# Plan headers and global constraints
|
||||
stable_header_terms = [
|
||||
"# GraphQL API 실행 플랫폼 Implementation Plan",
|
||||
"REQUIRED SUB-SKILL",
|
||||
"**Goal:**",
|
||||
"**Architecture:**",
|
||||
"**Tech Stack:**",
|
||||
"## Global Constraints",
|
||||
"Stable Task",
|
||||
]
|
||||
advanced_header_terms = [
|
||||
"# GraphQL Advanced Capability Expansion Implementation Plan",
|
||||
"REQUIRED SUB-SKILL",
|
||||
"backend.graphql.advanced.*",
|
||||
"Stable 구현 계획 Task `1–48`",
|
||||
]
|
||||
for term in stable_header_terms:
|
||||
check(f"stable header contains {term}", term in stable)
|
||||
for term in advanced_header_terms:
|
||||
check(f"advanced header contains {term}", term in advanced)
|
||||
|
||||
# Task sequence and per-task structure
|
||||
def task_sections(text: str) -> list[tuple[int, str]]:
|
||||
matches = list(re.finditer(r"^### Task (\d+): .+$", text, re.MULTILINE))
|
||||
result = []
|
||||
for i, match in enumerate(matches):
|
||||
start = match.start()
|
||||
end = matches[i+1].start() if i+1 < len(matches) else len(text)
|
||||
result.append((int(match.group(1)), text[start:end]))
|
||||
return result
|
||||
|
||||
stable_tasks = task_sections(stable)
|
||||
advanced_tasks = task_sections(advanced)
|
||||
check("stable task count", len(stable_tasks) == 48, str(len(stable_tasks)))
|
||||
check("advanced task count", len(advanced_tasks) == 19, str(len(advanced_tasks)))
|
||||
check("stable task sequence", [n for n, _ in stable_tasks] == list(range(1, 49)))
|
||||
check("advanced task sequence", [n for n, _ in advanced_tasks] == list(range(1, 20)))
|
||||
|
||||
def validate_tasks(label: str, tasks: list[tuple[int, str]]) -> None:
|
||||
required = [
|
||||
"**Files:**",
|
||||
"**Interfaces:**",
|
||||
"**Implementation requirements:**",
|
||||
"**Step 1: Write the failing test**",
|
||||
"**Step 2: Run the focused test and verify the failure**",
|
||||
"**Step 3: Implement the smallest complete production contract**",
|
||||
"**Step 4: Run the focused test and the owning suite**",
|
||||
"**Step 5: Commit the independently reviewable change**",
|
||||
"Expected: FAIL",
|
||||
"Expected: PASS",
|
||||
"git commit -m",
|
||||
]
|
||||
for number, section in tasks:
|
||||
for token in required:
|
||||
check(f"{label} task {number} contains {token}", token in section)
|
||||
check(f"{label} task {number} has test path", "- Test: `" in section)
|
||||
check(f"{label} task {number} has production file", "- Create: `" in section)
|
||||
check(f"{label} task {number} fences balanced", section.count("```") % 2 == 0)
|
||||
check(f"{label} task {number} has gradle test", "./gradlew" in section and ":test" in section)
|
||||
|
||||
validate_tasks("stable", stable_tasks)
|
||||
validate_tasks("advanced", advanced_tasks)
|
||||
|
||||
# Create paths
|
||||
def create_paths(text: str) -> list[str]:
|
||||
return re.findall(r"^- Create: `([^`]+)`$", text, re.MULTILINE)
|
||||
|
||||
stable_paths = create_paths(stable)
|
||||
advanced_paths = create_paths(advanced)
|
||||
check("stable create paths exist", len(stable_paths) >= 150, str(len(stable_paths)))
|
||||
check("advanced create paths exist", len(advanced_paths) >= 80, str(len(advanced_paths)))
|
||||
check("stable create paths unique", len(stable_paths) == len(set(stable_paths)))
|
||||
check("advanced create paths unique", len(advanced_paths) == len(set(advanced_paths)))
|
||||
check("stable and advanced paths disjoint", set(stable_paths).isdisjoint(advanced_paths))
|
||||
for index, path in enumerate(stable_paths, 1):
|
||||
check(f"stable create path {index} exact", "*" not in path and "..." not in path and (path.startswith("modules/graphql/") or path.startswith("build-logic/")))
|
||||
for index, path in enumerate(advanced_paths, 1):
|
||||
check(f"advanced create path {index} exact", "*" not in path and "..." not in path and path.startswith("modules/graphql-advanced/"))
|
||||
|
||||
# Stable/Advanced separation
|
||||
for forbidden in [
|
||||
"modules/graphql/graphql-websocket/",
|
||||
"modules/graphql/graphql-federation/",
|
||||
"modules/graphql/graphql-persisted-operation/",
|
||||
"modules/graphql/graphql-rsocket/",
|
||||
]:
|
||||
check(f"stable excludes {forbidden}", forbidden not in stable)
|
||||
|
||||
for required in [
|
||||
"modules/graphql-advanced/graphql-persisted-operation/",
|
||||
"modules/graphql-advanced/graphql-websocket/",
|
||||
"modules/graphql-advanced/graphql-subscription/",
|
||||
"modules/graphql-advanced/graphql-federation/",
|
||||
"modules/graphql-advanced/graphql-rsocket/",
|
||||
]:
|
||||
check(f"advanced includes {required}", required in advanced)
|
||||
|
||||
# Stable coverage
|
||||
stable_required_terms = [
|
||||
"GraphQlRequestContext",
|
||||
"GraphQlClientPolicy",
|
||||
"GraphQlSchemaContract",
|
||||
"SchemaMappingInspector",
|
||||
"@oneOf",
|
||||
"GraphQlHttpProfile",
|
||||
"application/graphql-response+json",
|
||||
"GraphQlExecutionProfile",
|
||||
"GraphQlWireError",
|
||||
"GraphQlTenantIsolationPolicy",
|
||||
"GraphQlParserLimits",
|
||||
"GraphQlComplexityCalculator",
|
||||
"GraphQlRuntimeBudget",
|
||||
"GraphQlPreparsedCacheKey",
|
||||
"GraphQlBatchPolicy",
|
||||
"GraphQlFetchProfile",
|
||||
"HmacGraphQlCursorCodec",
|
||||
"GraphQlConnection",
|
||||
"GraphQlMutationIdempotencyContext",
|
||||
"GraphQlMetricCardinalityPolicy",
|
||||
"GraphQlPlatformStartupValidator",
|
||||
"GraphQlReleaseGate",
|
||||
]
|
||||
for term in stable_required_terms:
|
||||
check(f"stable coverage {term}", term in stable)
|
||||
|
||||
advanced_required_terms = [
|
||||
"GraphQlPersistedOperation",
|
||||
"GraphQlWebSocketProtocol",
|
||||
"GraphQlSubscriptionBufferPolicy",
|
||||
"GraphQlSubscriptionOrderingProfile",
|
||||
"GraphQlSseConnectionPolicy",
|
||||
"GraphQlReplayPosition",
|
||||
"GraphQlDataLoaderDependencyGraph",
|
||||
"GraphQlFederationEntityKey",
|
||||
"GraphQlFederationCompositionGate",
|
||||
"GraphQlGeneratedSourceBoundary",
|
||||
"GraphQlRepositoryAllowlist",
|
||||
"GraphQlRSocketRoutePolicy",
|
||||
"GraphQlHttpGetOperationPolicy",
|
||||
"GraphQlIncrementalCompatibilityGate",
|
||||
"GraphQlAdvancedReleaseGate",
|
||||
]
|
||||
for term in advanced_required_terms:
|
||||
check(f"advanced coverage {term}", term in advanced)
|
||||
|
||||
# Prohibited API patterns
|
||||
prohibited_patterns = [
|
||||
(r"interface\s+GenericGraphQlRepository", "no generic graphql repository"),
|
||||
(r"public\s+.*\bEntityManager\b", "no public entity manager"),
|
||||
(r"public\s+.*\bMongoTemplate\b", "no public mongo template"),
|
||||
(r"scalar\s+Upload\b", "no upload scalar declaration"),
|
||||
(r"@Transactional\s+.*GraphQL request", "no request-wide transaction implementation"),
|
||||
]
|
||||
for pattern, name in prohibited_patterns:
|
||||
check(name, re.search(pattern, stable, re.IGNORECASE | re.MULTILINE) is None)
|
||||
|
||||
# File hashes can be printed for package evidence
|
||||
for path in [DESIGN, STABLE, ADVANCED]:
|
||||
if path.exists():
|
||||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
check(f"sha256 computed: {path.name}", len(digest) == 64, digest)
|
||||
|
||||
failed = [(n, d) for n, ok, d in checks if not ok]
|
||||
print(f"CHECKS={len(checks)}")
|
||||
print(f"PASSED={len(checks)-len(failed)}")
|
||||
print(f"FAILED={len(failed)}")
|
||||
for name, detail in failed:
|
||||
print(f"FAIL: {name}" + (f" :: {detail}" if detail else ""))
|
||||
|
||||
sys.exit(1 if failed else 0)
|
||||
Reference in New Issue
Block a user