287 lines
9.2 KiB
Python
Executable File
287 lines
9.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
|
|
if (ROOT / "docs").exists():
|
|
DESIGN = ROOT / "docs/superpowers/specs/2026-08-13-web-inbound-http-api-execution-platform-design.md"
|
|
STABLE = ROOT / "docs/superpowers/plans/2026-08-13-web-inbound-http-api-execution-platform-implementation-plan.md"
|
|
ADVANCED = ROOT / "docs/superpowers/plans/2026-08-13-web-advanced-capabilities-expansion-plan.md"
|
|
RESEARCH = ROOT / "research/source-web-deep-research.md"
|
|
else:
|
|
DESIGN = ROOT / "web-inbound-http-api-execution-platform-design.md"
|
|
STABLE = ROOT / "web-inbound-http-api-execution-platform-implementation-plan.md"
|
|
ADVANCED = ROOT / "web-advanced-capabilities-expansion-plan.md"
|
|
RESEARCH = ROOT / "붙여넣은 마크다운(1)(20260813-120656).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)
|
|
research = read(RESEARCH)
|
|
|
|
check("design line floor", len(design.splitlines()) >= 1000, str(len(design.splitlines())))
|
|
check("stable plan line floor", len(stable.splitlines()) >= 3000, str(len(stable.splitlines())))
|
|
check("advanced plan line floor", len(advanced.splitlines()) >= 900, str(len(advanced.splitlines())))
|
|
check("research line floor", len(research.splitlines()) >= 1000, str(len(research.splitlines())))
|
|
|
|
for name, text in [
|
|
("design", design),
|
|
("stable", stable),
|
|
("advanced", advanced),
|
|
]:
|
|
check(f"{name} markdown fence balanced", text.count("```") % 2 == 0, str(text.count("```")))
|
|
for forbidden in ["TODO", "TBD", "FIXME", "implement later", "fill in details"]:
|
|
check(f"{name} has no placeholder {forbidden}", forbidden not in text, forbidden)
|
|
|
|
def task_sections(text: str) -> list[tuple[int, str]]:
|
|
matches = list(re.finditer(r"(?m)^### Task (\d+): .+$", text))
|
|
result: list[tuple[int, str]] = []
|
|
for index, match in enumerate(matches):
|
|
start = match.start()
|
|
end = matches[index + 1].start() if index + 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) == 58, str(len(stable_tasks)))
|
|
check("advanced task count", len(advanced_tasks) == 19, str(len(advanced_tasks)))
|
|
check(
|
|
"stable task numbering consecutive",
|
|
[number for number, _ in stable_tasks] == list(range(1, 59)),
|
|
str([number for number, _ in stable_tasks]),
|
|
)
|
|
check(
|
|
"advanced task numbering consecutive",
|
|
[number for number, _ in advanced_tasks] == list(range(1, 20)),
|
|
str([number for number, _ in advanced_tasks]),
|
|
)
|
|
|
|
required_markers = [
|
|
"**Files:**",
|
|
"**Interfaces:**",
|
|
"**Implementation requirements:**",
|
|
"**Step 1: Write the failing test**",
|
|
"**Step 2: Run the focused test and verify the expected failure**",
|
|
"**Step 3: Implement the minimum production contract**",
|
|
"**Step 4: Run the task test and its module contract suite**",
|
|
"**Step 5: Commit the independently reviewable change**",
|
|
"git commit -m",
|
|
]
|
|
|
|
for plan_name, sections in [("stable", stable_tasks), ("advanced", advanced_tasks)]:
|
|
for number, section in sections:
|
|
for marker in required_markers:
|
|
check(
|
|
f"{plan_name} task {number} contains {marker}",
|
|
marker in section,
|
|
marker,
|
|
)
|
|
check(
|
|
f"{plan_name} task {number} has test path",
|
|
"- Test: `" in section,
|
|
"",
|
|
)
|
|
check(
|
|
f"{plan_name} task {number} has exact run command",
|
|
"Run: `" in section,
|
|
"",
|
|
)
|
|
check(
|
|
f"{plan_name} task {number} has expected failure",
|
|
"Expected:" in section and "FAIL" in section,
|
|
"",
|
|
)
|
|
check(
|
|
f"{plan_name} task {number} has expected pass",
|
|
"Expected: PASS" in section,
|
|
"",
|
|
)
|
|
|
|
def create_paths(text: str) -> list[str]:
|
|
return re.findall(r"(?m)^- Create: `([^`]+)`$", text)
|
|
|
|
stable_creates = create_paths(stable)
|
|
advanced_creates = create_paths(advanced)
|
|
|
|
check(
|
|
"stable create paths unique",
|
|
len(stable_creates) == len(set(stable_creates)),
|
|
f"{len(stable_creates)} paths",
|
|
)
|
|
check(
|
|
"advanced create paths unique",
|
|
len(advanced_creates) == len(set(advanced_creates)),
|
|
f"{len(advanced_creates)} paths",
|
|
)
|
|
check(
|
|
"stable and advanced create paths do not collide",
|
|
set(stable_creates).isdisjoint(set(advanced_creates)),
|
|
str(set(stable_creates) & set(advanced_creates)),
|
|
)
|
|
|
|
design_terms = [
|
|
"W1",
|
|
"W2",
|
|
"W3",
|
|
"W4",
|
|
"APPLICATION_COMMITTED",
|
|
"CLIENT_OBSERVATION_UNKNOWN",
|
|
"RFC 9457",
|
|
"OpenAPI 3.1.2",
|
|
"If-Match",
|
|
"Idempotency",
|
|
"202 Accepted",
|
|
"Tomcat",
|
|
"Jetty",
|
|
"Reactor Netty",
|
|
"Nginx",
|
|
"business mutation + authoritative evidence same DB transaction",
|
|
"Redis",
|
|
"Request Evidence",
|
|
"Application Evidence",
|
|
"Response Evidence",
|
|
]
|
|
for term in design_terms:
|
|
check(f"design contains key term: {term}", term in design, term)
|
|
|
|
stable_terms = [
|
|
"same-PostgreSQL-transaction",
|
|
"Application Commit 후 HTTP Response 유실 Fault Test",
|
|
"Redis Concurrent Gate와 Replay Cache Adapter",
|
|
"실제 Nginx Trusted Proxy",
|
|
"실제 Tomcat MVC HTTP 계약 Gate",
|
|
"Jetty MVC 호환성 Gate",
|
|
"실제 Reactor Netty WebFlux 계약 Gate",
|
|
"OpenAPI 3.1.2 Snapshot",
|
|
"OpenAPI Breaking Diff",
|
|
"Durable Operation",
|
|
"same-PostgreSQL-transaction",
|
|
"DB commit evidence의 유일한 source가 아니다",
|
|
"webStableCheck",
|
|
]
|
|
for term in stable_terms:
|
|
check(f"stable plan contains key term: {term}", term in stable, term)
|
|
|
|
advanced_terms = [
|
|
"Virtual Thread",
|
|
"Controlled Blocking Bridge",
|
|
"JSON Merge Patch RFC 7396",
|
|
"JSON Patch RFC 6902",
|
|
"MVC SSE",
|
|
"WebFlux SSE",
|
|
"NDJSON",
|
|
"JSON Text Sequence",
|
|
"Messaging-backed SSE Replay",
|
|
"OpenAPI 3.2 Experimental",
|
|
"RateLimit Draft",
|
|
"10k",
|
|
"rollback",
|
|
]
|
|
for term in advanced_terms:
|
|
check(f"advanced plan contains key term: {term}", term in advanced, term)
|
|
|
|
# Stable/advanced dependency boundary.
|
|
check(
|
|
"stable module map excludes modules/web-advanced",
|
|
"modules/web-advanced/" not in stable.split("## 1. Stable 파일·모듈 구조", 1)[1].split("## 2.", 1)[0],
|
|
"",
|
|
)
|
|
check(
|
|
"advanced plan requires stable completion",
|
|
"Stable Task 1~58" in advanced,
|
|
"",
|
|
)
|
|
|
|
# Evidence and idempotency invariants.
|
|
for text_name, text in [("design", design), ("stable", stable)]:
|
|
check(
|
|
f"{text_name} separates ETag and idempotency",
|
|
"ETag/If-Match" in text and "Idempotency" in text,
|
|
"",
|
|
)
|
|
check(
|
|
f"{text_name} says Redis is not sole commit evidence",
|
|
("sole DB commit evidence" in text)
|
|
or ("유일한 source" in text)
|
|
or ("유일한 Source" in text),
|
|
"",
|
|
)
|
|
check(
|
|
f"{text_name} includes commit-response-loss",
|
|
("Response Loss" in text)
|
|
or ("response-loss" in text)
|
|
or ("response write 전 TCP reset" in text),
|
|
"",
|
|
)
|
|
|
|
# No unsupported architecture in design/plan.
|
|
for name, text in [("design", design), ("stable", stable)]:
|
|
check(
|
|
f"{name} forbids controller transaction",
|
|
"Controller transaction" in text or "Controller @Transactional" in text or "Controller 또는 HTTP adapter에 업무 `@Transactional`" in text,
|
|
"",
|
|
)
|
|
check(
|
|
f"{name} forbids entity/document wire types",
|
|
"Entity/Document" in text or "Entity·Document" in text or "JPA Entity·MongoDB Document" in text,
|
|
"",
|
|
)
|
|
check(
|
|
f"{name} does not declare Idempotency-Key as final RFC",
|
|
"IETF 표준" not in text or "금지" in text,
|
|
"",
|
|
)
|
|
|
|
# Research grounding.
|
|
check(
|
|
"design title matches research topic",
|
|
"인바운드 HTTP API 실행 플랫폼" in design and "인바운드 HTTP API 실행 플랫폼" in research,
|
|
"",
|
|
)
|
|
check(
|
|
"research includes execution evidence chain",
|
|
"HTTP_RECEIVED" in research and "CLIENT_OBSERVATION_UNKNOWN" in research,
|
|
"",
|
|
)
|
|
check(
|
|
"research includes actual server matrix",
|
|
"MVC + Tomcat" in research and "WebFlux + Reactor Netty" in research,
|
|
"",
|
|
)
|
|
|
|
# Optional package integrity.
|
|
manifest = ROOT / "MANIFEST.sha256"
|
|
if manifest.exists():
|
|
for line in manifest.read_text(encoding="utf-8").splitlines():
|
|
if not line.strip():
|
|
continue
|
|
digest, relative = line.split(" ", 1)
|
|
target = ROOT / relative
|
|
actual = hashlib.sha256(target.read_bytes()).hexdigest() if target.exists() else ""
|
|
check(f"manifest: {relative}", actual == digest, actual)
|
|
|
|
passed = sum(1 for _, ok, _ in checks if ok)
|
|
failed = [(name, detail) for name, ok, detail in checks if not ok]
|
|
|
|
print(f"checks={len(checks)} passed={passed} failed={len(failed)}")
|
|
for name, detail in failed:
|
|
print(f"FAIL: {name} :: {detail}")
|
|
|
|
sys.exit(1 if failed else 0)
|