Files
clean-architecture-backend-…/docs/web-superpowers-package/validate_web_docs.py
T

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)