feat: 설계 문서 추가
This commit is contained in:
+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