250 lines
9.8 KiB
Python
Executable File
250 lines
9.8 KiB
Python
Executable File
#!/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)
|