Snapshot of the in-flight state that already existed, identically, in both this worktree and the main checkout before this session began: the initial HTTP Client platform implementation (previously untracked), the redis-lab removal, and the JPA / object-storage / notification integration work. Kept separate from this session's HTTP Client review response, which lands in the following commit, so the two bodies of work stay reviewable apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
175 lines
7.1 KiB
Python
175 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
ROOT = Path('/mnt/data')
|
|
DESIGN = ROOT / 'fileserver-platform-design.md'
|
|
PLAN = ROOT / 'fileserver-platform-implementation-plan.md'
|
|
|
|
errors: list[str] = []
|
|
checks: list[tuple[str, bool, str]] = []
|
|
|
|
|
|
def add(name: str, ok: bool, detail: str) -> None:
|
|
checks.append((name, ok, detail))
|
|
if not ok:
|
|
errors.append(f'{name}: {detail}')
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
for path in (DESIGN, PLAN):
|
|
add(f'{path.name} exists', path.exists(), str(path))
|
|
|
|
if errors:
|
|
print('\n'.join(errors), file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
design = DESIGN.read_text(encoding='utf-8')
|
|
plan = PLAN.read_text(encoding='utf-8')
|
|
|
|
add('design title', design.startswith('# Fileserver Platform 설계서'), True.__str__())
|
|
add('plan header', plan.startswith('# Fileserver Platform Implementation Plan\n\n> **For agentic workers:**'), 'required Superpowers header')
|
|
add('design code fences', design.count('```') % 2 == 0, f"count={design.count('```')}")
|
|
add('plan code fences', plan.count('```') % 2 == 0, f"count={plan.count('```')}")
|
|
|
|
for label, text in [('design', design), ('plan', plan)]:
|
|
forbidden = [r'\bTBD\b', r'\bTODO\b', r'implement later', r'fill in details', r'Similar to Task']
|
|
hits = [p for p in forbidden if re.search(p, text, re.I)]
|
|
add(f'{label} placeholder scan', not hits, f'hits={hits}')
|
|
|
|
required_design_sections = [
|
|
'## 5. 지원 매트릭스',
|
|
'## 6. 전체 아키텍처',
|
|
'## 9. 상태 머신과 invariant',
|
|
'## 10. Metadata Store 설계',
|
|
'## 11. Content Store Port',
|
|
'## 12. Local Filesystem Adapter',
|
|
'## 14. Publish와 완료 처리',
|
|
'## 15. Upload Application 설계',
|
|
'## 19. HTTP API',
|
|
'## 20. Range와 Conditional Request',
|
|
'## 21. Spring MVC Adapter',
|
|
'## 22. Spring WebFlux Adapter',
|
|
'## 23. Nginx 전송 위임',
|
|
'## 24. 재개 가능한 업로드',
|
|
'## 27. 보안 정책',
|
|
'## 28. 다중 인스턴스와 NFS',
|
|
'## 30. 관측성',
|
|
'## 33. 테스트 전략',
|
|
'## 37. 완료 정의',
|
|
]
|
|
missing_sections = [s for s in required_design_sections if s not in design]
|
|
add('design section coverage', not missing_sections, f'missing={missing_sections}')
|
|
|
|
source_topics = {
|
|
'MVC': ['Spring MVC Adapter', 'MvcTransferExecutorProperties'],
|
|
'WebFlux': ['Spring WebFlux Adapter', 'DataBuffer'],
|
|
'local/PVC/NFS': ['Kubernetes PVC', 'NFSv4.1', 'Local Filesystem Adapter'],
|
|
'content/metadata separation': ['Content Store Port', 'Metadata Store 설계'],
|
|
'upload': ['Upload Application 설계', 'multipart', 'application/octet-stream'],
|
|
'download': ['Range와 Conditional Request', 'ETag', 'If-Range'],
|
|
'publish': ['ATOMIC_MOVE_REQUIRED', 'METADATA_POINTER', 'AmbiguousCompletionException'],
|
|
'security': ['traversal', 'symlink', 'READY gate'],
|
|
'resumable': ['tus 1.0 Stable', 'draft-12 Experimental'],
|
|
'observability': ['Metric', 'Trace', 'Audit'],
|
|
}
|
|
for topic, needles in source_topics.items():
|
|
missing = [n for n in needles if n not in design]
|
|
add(f'design topic: {topic}', not missing, f'missing={missing}')
|
|
|
|
# Core Port snippet must not expose adapter types.
|
|
port_match = re.search(r'### 11\.2 Blocking SPI\n(.*?)### 11\.3 Async SPI', design, re.S)
|
|
port_text = port_match.group(1) if port_match else ''
|
|
forbidden_port_types = ['java.nio.file.Path', 'org.springframework.core.io.Resource', 'DataBuffer', 'Flux<']
|
|
port_hits = [x for x in forbidden_port_types if x in port_text]
|
|
add('blocking core port leakage', bool(port_match) and not port_hits, f'hits={port_hits}')
|
|
|
|
# Task structure.
|
|
task_matches = list(re.finditer(r'^### Task (\d+):', plan, re.M))
|
|
task_numbers = [int(m.group(1)) for m in task_matches]
|
|
add('task count', len(task_numbers) == 33, f'count={len(task_numbers)}')
|
|
add('task numbering', task_numbers == list(range(1, 34)), f'numbers={task_numbers}')
|
|
|
|
missing_task_blocks: dict[int, list[str]] = {}
|
|
for idx, match in enumerate(task_matches):
|
|
end = task_matches[idx + 1].start() if idx + 1 < len(task_matches) else plan.find('\n## 3.', match.start())
|
|
segment = plan[match.start():end]
|
|
required = [
|
|
'**Files:**', '**Interfaces:**', '**Step 1:', '**Step 2:',
|
|
'**Step 3:', '**Step 4:', '**Step 5:', 'Expected:', 'git commit'
|
|
]
|
|
missing = [item for item in required if item not in segment]
|
|
if missing:
|
|
missing_task_blocks[int(match.group(1))] = missing
|
|
add('task block completeness', not missing_task_blocks, json.dumps(missing_task_blocks, ensure_ascii=False))
|
|
|
|
create_paths = re.findall(r'^- Create: `([^`]+)`', plan, re.M)
|
|
duplicates = {path: count for path, count in Counter(create_paths).items() if count > 1}
|
|
add('unique create paths', not duplicates, json.dumps(duplicates, ensure_ascii=False))
|
|
|
|
required_plan_topics = [
|
|
'Task 10: Storage capability probe',
|
|
'Task 11: Streaming append',
|
|
'Task 13: Atomic move와 metadata pointer publish',
|
|
'Task 18: HTTP Range',
|
|
'Task 21: Spring WebFlux raw·multipart upload',
|
|
'Task 23: Nginx `X-Accel-Redirect`',
|
|
'Task 26: 다중 인스턴스 writer lease',
|
|
'Task 27: tus 1.0 Stable',
|
|
'Task 28: HTTPbis resumable upload draft-12 Experimental',
|
|
'Task 29: HTTP Problem Detail과 보안 hardening',
|
|
'Task 32: Filesystem, HTTP, fault, performance Testkit',
|
|
'Task 33: CI matrix',
|
|
]
|
|
missing_plan_topics = [x for x in required_plan_topics if x not in plan]
|
|
add('plan scope coverage', not missing_plan_topics, f'missing={missing_plan_topics}')
|
|
|
|
add('no Redis carryover', 'redis' not in design.lower() and 'redis' not in plan.lower(), 'search term=redis')
|
|
add('no deprecated nginx token design', 'DelegatedPathToken' not in design + plan and 'opaque-token' not in design + plan, 'token mapper removed')
|
|
|
|
status = 'PASS' if not errors else 'FAIL'
|
|
report = ROOT / 'fileserver-superpowers-validation.md'
|
|
lines = [
|
|
'# Fileserver Superpowers 문서 검증',
|
|
'',
|
|
f'**결과:** {status}',
|
|
'',
|
|
'## 파일',
|
|
'',
|
|
f'- `{DESIGN.name}` — {len(design.splitlines())} lines, {len(design.encode())} bytes, SHA-256 `{sha256(DESIGN)}`',
|
|
f'- `{PLAN.name}` — {len(plan.splitlines())} lines, {len(plan.encode())} bytes, SHA-256 `{sha256(PLAN)}`',
|
|
'',
|
|
'## 검증 항목',
|
|
'',
|
|
]
|
|
for name, ok, detail in checks:
|
|
lines.append(f"- [{'x' if ok else ' '}] **{name}** — {detail}")
|
|
|
|
lines += [
|
|
'',
|
|
'## 검증 범위의 한계',
|
|
'',
|
|
'- 현재 Backend Skeleton 저장소가 입력되지 않아 Gradle compilation, integration test, Nginx execution, PVC·NFS certification은 실행하지 않았다.',
|
|
'- 본 검증은 설계·계획 문서의 구조, 내부 일관성, 범위 추적성, 미확정 표식과 중복 경로를 확인한 정적 검증이다.',
|
|
]
|
|
report.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
|
|
|
print(json.dumps({
|
|
'status': status,
|
|
'errors': errors,
|
|
'checks': len(checks),
|
|
'design_lines': len(design.splitlines()),
|
|
'plan_lines': len(plan.splitlines()),
|
|
'task_count': len(task_numbers),
|
|
'report': str(report),
|
|
}, ensure_ascii=False, indent=2))
|
|
|
|
raise SystemExit(0 if not errors else 1)
|