fix: 설계 문서 제거
This commit is contained in:
@@ -1,25 +0,0 @@
|
|||||||
# Fileserver Superpowers Package
|
|
||||||
|
|
||||||
## 포함 파일
|
|
||||||
|
|
||||||
- `fileserver-platform-design.md` — Fileserver 플랫폼 설계 확정안
|
|
||||||
- `fileserver-platform-implementation-plan.md` — 33개 TDD 작업으로 분해한 구현 계획
|
|
||||||
- `VALIDATION.md` — 문서 정적 검증 결과
|
|
||||||
- `validate_fileserver_docs.py` — 검증 재실행 스크립트
|
|
||||||
|
|
||||||
## 저장소 배치 위치
|
|
||||||
|
|
||||||
```text
|
|
||||||
docs/superpowers/specs/2026-08-07-fileserver-platform-design.md
|
|
||||||
docs/superpowers/plans/2026-08-07-fileserver-platform-implementation-plan.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## 실행 순서
|
|
||||||
|
|
||||||
1. 실제 Backend Skeleton 구조와 root package를 대조한다.
|
|
||||||
2. 설계서의 모듈 경계를 저장소에 반영한다.
|
|
||||||
3. 구현 계획 Task 1부터 순서대로 실행한다.
|
|
||||||
4. 각 Task에서 실패 테스트를 확인한 뒤 구현한다.
|
|
||||||
5. Milestone A~D마다 전체 검증 Gate를 실행한다.
|
|
||||||
|
|
||||||
실행에는 `superpowers:subagent-driven-development` 방식이 권장된다.
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Fileserver Superpowers 문서 검증
|
|
||||||
|
|
||||||
**결과:** PASS
|
|
||||||
|
|
||||||
## 파일
|
|
||||||
|
|
||||||
- `fileserver-platform-design.md` — 1893 lines, 59904 bytes, SHA-256 `ee7b21277b254b9606a9ec6e34118a10fba3abbe818b43cbce9ae832102411e6`
|
|
||||||
- `fileserver-platform-implementation-plan.md` — 3422 lines, 131608 bytes, SHA-256 `9a443852ab3a7e4a2232c1b443d4cb8d3478a4954d70510173d3e0ac1d3d2125`
|
|
||||||
|
|
||||||
## 검증 항목
|
|
||||||
|
|
||||||
- [x] **fileserver-platform-design.md exists** — /mnt/data/fileserver-platform-design.md
|
|
||||||
- [x] **fileserver-platform-implementation-plan.md exists** — /mnt/data/fileserver-platform-implementation-plan.md
|
|
||||||
- [x] **design title** — True
|
|
||||||
- [x] **plan header** — required Superpowers header
|
|
||||||
- [x] **design code fences** — count=94
|
|
||||||
- [x] **plan code fences** — count=416
|
|
||||||
- [x] **design placeholder scan** — hits=[]
|
|
||||||
- [x] **plan placeholder scan** — hits=[]
|
|
||||||
- [x] **design section coverage** — missing=[]
|
|
||||||
- [x] **design topic: MVC** — missing=[]
|
|
||||||
- [x] **design topic: WebFlux** — missing=[]
|
|
||||||
- [x] **design topic: local/PVC/NFS** — missing=[]
|
|
||||||
- [x] **design topic: content/metadata separation** — missing=[]
|
|
||||||
- [x] **design topic: upload** — missing=[]
|
|
||||||
- [x] **design topic: download** — missing=[]
|
|
||||||
- [x] **design topic: publish** — missing=[]
|
|
||||||
- [x] **design topic: security** — missing=[]
|
|
||||||
- [x] **design topic: resumable** — missing=[]
|
|
||||||
- [x] **design topic: observability** — missing=[]
|
|
||||||
- [x] **blocking core port leakage** — hits=[]
|
|
||||||
- [x] **task count** — count=33
|
|
||||||
- [x] **task numbering** — numbers=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
|
|
||||||
- [x] **task block completeness** — {}
|
|
||||||
- [x] **unique create paths** — {}
|
|
||||||
- [x] **plan scope coverage** — missing=[]
|
|
||||||
- [x] **no Redis carryover** — search term=redis
|
|
||||||
- [x] **no deprecated nginx token design** — token mapper removed
|
|
||||||
|
|
||||||
## 검증 범위의 한계
|
|
||||||
|
|
||||||
- 현재 Backend Skeleton 저장소가 입력되지 않아 Gradle compilation, integration test, Nginx execution, PVC·NFS certification은 실행하지 않았다.
|
|
||||||
- 본 검증은 설계·계획 문서의 구조, 내부 일관성, 범위 추적성, 미확정 표식과 중복 경로를 확인한 정적 검증이다.
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,174 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# HTTP Client Superpowers 설계 패키지
|
|
||||||
|
|
||||||
이 패키지는 `Java/Spring 외부 HTTP Client 플랫폼 설계 심층 리서치`를 기반으로 작성한 설계서와 구현 계획서다.
|
|
||||||
|
|
||||||
## 파일
|
|
||||||
|
|
||||||
- `docs/superpowers/specs/2026-08-08-httpclient-platform-design.md`
|
|
||||||
- `docs/superpowers/plans/2026-08-08-httpclient-platform-implementation-plan.md`
|
|
||||||
- `VALIDATION.md`
|
|
||||||
- `validate_httpclient_docs.py`
|
|
||||||
|
|
||||||
## 구현 기준
|
|
||||||
|
|
||||||
- Java 21
|
|
||||||
- Gradle Kotlin DSL
|
|
||||||
- 공통 API는 Spring Framework 6.2 기준
|
|
||||||
- Spring Framework 7.0 호환성 검증
|
|
||||||
- Apache HttpClient 5 + RestClient
|
|
||||||
- JDK HttpClient + RestClient
|
|
||||||
- Reactor Netty + WebClient
|
|
||||||
- Jetty HTTP/3 Experimental
|
|
||||||
|
|
||||||
실제 Backend Skeleton 저장소가 제공되지 않았으므로 package 경로와 Gradle 구조는 설계서의 명시적 구현 가정이다. 구현 전 저장소의 기존 convention과 root package에 맞춰 경로만 조정하고 공개 계약과 정책 의미론은 유지한다.
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# HTTP Client Superpowers 문서 검증
|
|
||||||
|
|
||||||
**검증 결과:** PASS
|
|
||||||
|
|
||||||
## 검증 항목
|
|
||||||
|
|
||||||
- 설계서 존재 및 최소 구조: PASS
|
|
||||||
- 구현 계획서 존재 및 최소 구조: PASS
|
|
||||||
- Task 번호 연속성: PASS
|
|
||||||
- Task별 Files·Interfaces·Step 1~5·Expected·Commit: PASS
|
|
||||||
- Markdown code fence 균형: PASS
|
|
||||||
- Placeholder scan: PASS
|
|
||||||
- 중복 Create 경로: PASS
|
|
||||||
- 핵심 설계 범위: PASS
|
|
||||||
- 핵심 구현 범위: PASS
|
|
||||||
|
|
||||||
## 통계
|
|
||||||
|
|
||||||
- explicitly forbidden signature documented: ApacheHttpClient nativeApacheClient()
|
|
||||||
- explicitly forbidden signature documented: HttpClient nativeJdkClient()
|
|
||||||
- explicitly forbidden signature documented: WebClient.Builder mutableBuilder()
|
|
||||||
- explicitly forbidden signature documented: RestClient.Builder mutableBuilder()
|
|
||||||
- design lines=1956, bytes=64493
|
|
||||||
- plan lines=3635, bytes=158401
|
|
||||||
- tasks=38, create_paths=306
|
|
||||||
|
|
||||||
## 결론
|
|
||||||
|
|
||||||
- 설계 결정과 구현 작업의 정적 추적성이 확인됐다.
|
|
||||||
- 실제 저장소가 제공되지 않았으므로 Gradle compile, integration, fault, security, performance test는 아직 실행되지 않았다.
|
|
||||||
- 계획의 Java 21, Gradle Kotlin DSL, root package는 명시된 구현 가정이다.
|
|
||||||
-3635
File diff suppressed because it is too large
Load Diff
-1956
File diff suppressed because it is too large
Load Diff
@@ -1,148 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import zipfile
|
|
||||||
|
|
||||||
base = Path('/mnt/data')
|
|
||||||
design_path = base / 'httpclient-platform-design.md'
|
|
||||||
plan_path = base / 'httpclient-platform-implementation-plan.md'
|
|
||||||
errors = []
|
|
||||||
notes = []
|
|
||||||
|
|
||||||
def read(p):
|
|
||||||
if not p.exists():
|
|
||||||
errors.append(f'missing file: {p}')
|
|
||||||
return ''
|
|
||||||
return p.read_text(encoding='utf-8')
|
|
||||||
|
|
||||||
design = read(design_path)
|
|
||||||
plan = read(plan_path)
|
|
||||||
|
|
||||||
# Basic size and structure
|
|
||||||
if len(design.splitlines()) < 1200:
|
|
||||||
errors.append(f'design unexpectedly short: {len(design.splitlines())} lines')
|
|
||||||
if len(plan.splitlines()) < 2500:
|
|
||||||
errors.append(f'plan unexpectedly short: {len(plan.splitlines())} lines')
|
|
||||||
|
|
||||||
# Task continuity and task internals
|
|
||||||
matches = list(re.finditer(r'^### Task (\d+): (.+)$', plan, flags=re.M))
|
|
||||||
nums = [int(m.group(1)) for m in matches]
|
|
||||||
expected = list(range(1, (max(nums) if nums else 0) + 1))
|
|
||||||
if nums != expected:
|
|
||||||
errors.append(f'task numbers not continuous: {nums[:5]}...{nums[-5:] if nums else []}')
|
|
||||||
|
|
||||||
for i, m in enumerate(matches):
|
|
||||||
start = m.start()
|
|
||||||
end = matches[i+1].start() if i+1 < len(matches) else plan.find('\n## 3. Plan Self-Review Checklist', start)
|
|
||||||
if end == -1:
|
|
||||||
end = len(plan)
|
|
||||||
block = plan[start:end]
|
|
||||||
n = m.group(1)
|
|
||||||
for token in ['**Files:**', '**Interfaces:**', '**Step 1:', '**Step 2:', '**Step 3:', '**Step 4:', '**Step 5:']:
|
|
||||||
if token not in block:
|
|
||||||
errors.append(f'Task {n} missing {token}')
|
|
||||||
if 'git commit -m ' not in block:
|
|
||||||
errors.append(f'Task {n} missing commit command')
|
|
||||||
if 'Expected:' not in block:
|
|
||||||
errors.append(f'Task {n} missing expected result')
|
|
||||||
|
|
||||||
# Markdown fence balance
|
|
||||||
for name, text in [('design', design), ('plan', plan)]:
|
|
||||||
count = len(re.findall(r'^```', text, flags=re.M))
|
|
||||||
if count % 2:
|
|
||||||
errors.append(f'{name} has unbalanced code fences: {count}')
|
|
||||||
|
|
||||||
# Placeholder scan
|
|
||||||
patterns = {
|
|
||||||
'TBD': r'\bTBD\b',
|
|
||||||
'TODO': r'\bTODO\b',
|
|
||||||
'implement later': r'implement later',
|
|
||||||
'fill in': r'fill in',
|
|
||||||
'similar to task': r'similar to Task',
|
|
||||||
'placeholder': r'placeholder',
|
|
||||||
}
|
|
||||||
for name, text in [('design', design), ('plan', plan)]:
|
|
||||||
for label, pat in patterns.items():
|
|
||||||
if re.search(pat, text, flags=re.I):
|
|
||||||
errors.append(f'{name} contains placeholder pattern: {label}')
|
|
||||||
|
|
||||||
# Duplicate create path scan
|
|
||||||
create_paths = re.findall(r'^- Create: `([^`]+)`', plan, flags=re.M)
|
|
||||||
dupes = sorted({p for p in create_paths if create_paths.count(p) > 1})
|
|
||||||
if dupes:
|
|
||||||
errors.append(f'duplicate Create paths: {dupes}')
|
|
||||||
|
|
||||||
# Required design coverage
|
|
||||||
required_design_terms = [
|
|
||||||
'H1 Typed Service Client', 'H2 Generic Exchange', 'H3 Dynamic Target',
|
|
||||||
'ExecutionEvidence', 'BodyReplayability', 'OperationIdempotency',
|
|
||||||
'Named Client Profile', 'Apache HttpClient 5', 'Reactor Netty',
|
|
||||||
'Retry Coordinator', 'Circuit Breaker', 'Rate Limiter', 'Bulkhead',
|
|
||||||
'OAuth2', 'TLS', 'SSRF', 'Streaming', 'SSE', 'HTTP/3',
|
|
||||||
'Spring Framework 6.2', 'Spring 7', 'RestTemplate'
|
|
||||||
]
|
|
||||||
for term in required_design_terms:
|
|
||||||
if term not in design:
|
|
||||||
errors.append(f'design missing term: {term}')
|
|
||||||
|
|
||||||
required_plan_terms = [
|
|
||||||
'httpclient-core-api', 'httpclient-transport-apache', 'httpclient-transport-jdk',
|
|
||||||
'httpclient-transport-reactor-netty', 'httpclient-dynamic-target',
|
|
||||||
'httpclient-spring-boot-starter', 'HttpAmbiguousExecutionException',
|
|
||||||
'first response byte', 'DNS/IP Pinning', 'SingleFlightTokenLoader',
|
|
||||||
'httpClientStableContractTest', 'spring62CompatibilityTest',
|
|
||||||
'spring70CompatibilityTest'
|
|
||||||
]
|
|
||||||
for term in required_plan_terms:
|
|
||||||
if term not in plan:
|
|
||||||
errors.append(f'plan missing term: {term}')
|
|
||||||
|
|
||||||
# Core API should not deliberately expose native clients in design signatures.
|
|
||||||
for forbidden_signature in [
|
|
||||||
'ApacheHttpClient nativeApacheClient()',
|
|
||||||
'HttpClient nativeJdkClient()',
|
|
||||||
'WebClient.Builder mutableBuilder()',
|
|
||||||
'RestClient.Builder mutableBuilder()'
|
|
||||||
]:
|
|
||||||
# These appear in an explicit "do not provide" code block. Note rather than fail.
|
|
||||||
if forbidden_signature in design:
|
|
||||||
notes.append(f'explicitly forbidden signature documented: {forbidden_signature}')
|
|
||||||
|
|
||||||
# Record task count and file counts
|
|
||||||
notes.append(f'design lines={len(design.splitlines())}, bytes={len(design.encode())}')
|
|
||||||
notes.append(f'plan lines={len(plan.splitlines())}, bytes={len(plan.encode())}')
|
|
||||||
notes.append(f'tasks={len(nums)}, create_paths={len(create_paths)}')
|
|
||||||
|
|
||||||
report = base / 'httpclient-superpowers-validation.md'
|
|
||||||
status = 'PASS' if not errors else 'FAIL'
|
|
||||||
report_text = [
|
|
||||||
'# HTTP Client Superpowers 문서 검증', '',
|
|
||||||
f'**검증 결과:** {status}', '',
|
|
||||||
'## 검증 항목', '',
|
|
||||||
f'- 설계서 존재 및 최소 구조: {"PASS" if design else "FAIL"}',
|
|
||||||
f'- 구현 계획서 존재 및 최소 구조: {"PASS" if plan else "FAIL"}',
|
|
||||||
f'- Task 번호 연속성: {"PASS" if nums == expected else "FAIL"}',
|
|
||||||
f'- Task별 Files·Interfaces·Step 1~5·Expected·Commit: {"PASS" if not any("Task " in e for e in errors) else "FAIL"}',
|
|
||||||
f'- Markdown code fence 균형: {"PASS" if not any("code fences" in e for e in errors) else "FAIL"}',
|
|
||||||
f'- Placeholder scan: {"PASS" if not any("placeholder" in e for e in errors) else "FAIL"}',
|
|
||||||
f'- 중복 Create 경로: {"PASS" if not dupes else "FAIL"}',
|
|
||||||
f'- 핵심 설계 범위: {"PASS" if not any("design missing" in e for e in errors) else "FAIL"}',
|
|
||||||
f'- 핵심 구현 범위: {"PASS" if not any("plan missing" in e for e in errors) else "FAIL"}',
|
|
||||||
'', '## 통계', ''
|
|
||||||
]
|
|
||||||
report_text += [f'- {note}' for note in notes]
|
|
||||||
if errors:
|
|
||||||
report_text += ['', '## 오류', ''] + [f'- {e}' for e in errors]
|
|
||||||
else:
|
|
||||||
report_text += ['', '## 결론', '',
|
|
||||||
'- 설계 결정과 구현 작업의 정적 추적성이 확인됐다.',
|
|
||||||
'- 실제 저장소가 제공되지 않았으므로 Gradle compile, integration, fault, security, performance test는 아직 실행되지 않았다.',
|
|
||||||
'- 계획의 Java 21, Gradle Kotlin DSL, root package는 명시된 구현 가정이다.']
|
|
||||||
report.write_text('\n'.join(report_text) + '\n', encoding='utf-8')
|
|
||||||
|
|
||||||
print(status)
|
|
||||||
for note in notes:
|
|
||||||
print(note)
|
|
||||||
for e in errors:
|
|
||||||
print('ERROR:', e)
|
|
||||||
sys.exit(0 if not errors else 1)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Redis Wrapper 및 Typed API 설계 패키지
|
|
||||||
|
|
||||||
이 패키지는 Spring 기반 Backend Skeleton에서 Redis 자료구조와 명령을 폭넓게 제공하기 위한 설계서와 구현 계획서다.
|
|
||||||
|
|
||||||
## 문서
|
|
||||||
|
|
||||||
- `docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md`
|
|
||||||
- 범위와 비지원 범위
|
|
||||||
- Redis 버전·배포 모드
|
|
||||||
- 모듈과 의존 규칙
|
|
||||||
- 자료구조별 동기·Reactive Typed API
|
|
||||||
- R1~R4 명령 노출 정책
|
|
||||||
- permit·budget·Raw Gateway·Admin Plane
|
|
||||||
- namespace·직렬화·TTL·timeout·retry·오류·관측성·ACL
|
|
||||||
- Standalone·Sentinel·Cluster
|
|
||||||
- 테스트·CI·완료 정의
|
|
||||||
|
|
||||||
- `docs/superpowers/plans/2026-08-07-redis-wrapper-typed-api-implementation-plan.md`
|
|
||||||
- 27개 구현 작업
|
|
||||||
- 작업별 생성·수정 파일
|
|
||||||
- 작업 간 입력·출력 인터페이스
|
|
||||||
- 실패 테스트, 실행 명령, 최소 구현, 통과 검증, 커밋
|
|
||||||
- Redis 7.2·7.4·8.2·8.10 및 Sentinel·Cluster 테스트 작업
|
|
||||||
|
|
||||||
- `VALIDATION.md`
|
|
||||||
- 문서 구조와 계획 완전성에 대한 정적 검증 결과
|
|
||||||
|
|
||||||
## 핵심 결정
|
|
||||||
|
|
||||||
1. classic Redis 자료구조는 최대한 Typed API로 제공한다.
|
|
||||||
2. 고비용·Blocking·다중 키 명령은 permit와 `OperationBudget`을 요구한다.
|
|
||||||
3. Typed API에 아직 없는 R1·R2 명령은 승인형 Raw Gateway로 제공한다.
|
|
||||||
4. 운영 명령은 별도 Admin Plane, 파괴적 명령은 SDK 차단으로 분리한다.
|
|
||||||
5. command catalog는 Redis 공식 metadata에서 생성하고 조직 정책을 오버레이한다.
|
|
||||||
6. 동기와 Reactive API를 정식 지원하고 같은 내부 async primitive를 공유한다.
|
|
||||||
7. 일반·Blocking·Transaction·Pub/Sub·Admin 연결을 격리한다.
|
|
||||||
8. timeout 후 write는 자동 재시도하지 않고 실행 결과 불명을 표현한다.
|
|
||||||
|
|
||||||
## 적용 전제
|
|
||||||
|
|
||||||
현재 Backend Skeleton 저장소가 첨부되지 않아 경로와 Gradle 구조는 목표 구조로 확정했다. 실제 저장소에 적용할 때 기존 package naming, convention plugin, dependency management가 더 강한 기준을 이미 갖고 있다면 구조적 계약은 유지하면서 해당 규칙에 맞춘다.
|
|
||||||
|
|
||||||
입력 Markdown이 참조한 309행 Excel 워크북은 현재 작업 공간에 존재하지 않았다. 따라서 정확한 command matrix는 구현 과정에서 `COMMAND DOCS`, `COMMAND INFO`, `COMMAND GETKEYSANDFLAGS`를 읽어 재생성하고 정책 오버레이를 적용하도록 설계했다.
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# 정적 검증 결과
|
|
||||||
|
|
||||||
- **결과:** PASS
|
|
||||||
- **검사 수:** 29
|
|
||||||
- **설계서 SHA-256:** `e742ea78f4f40c2f5ed65093a71d2c85c27da52b0761374030a5ca64143aea63`
|
|
||||||
- **계획서 SHA-256:** `6592a37373a79bc2ccf9434fc3516d8273d9f9369e392d5e4f3360a46d6398c0`
|
|
||||||
|
|
||||||
| 검사 | 결과 | 세부 |
|
|
||||||
|---|---|---|
|
|
||||||
| 설계서 파일 존재 | PASS | docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md |
|
|
||||||
| 설계서 코드 펜스 균형 | PASS | fences=70 |
|
|
||||||
| 설계서 미확정 표식 없음 | PASS | none |
|
|
||||||
| 계획서 파일 존재 | PASS | docs/superpowers/plans/2026-08-07-redis-wrapper-typed-api-implementation-plan.md |
|
|
||||||
| 계획서 코드 펜스 균형 | PASS | fences=276 |
|
|
||||||
| 계획서 미확정 표식 없음 | PASS | none |
|
|
||||||
| 계획 작업 수 | PASS | 27 tasks |
|
|
||||||
| 모든 작업 Step 1 보유 | PASS | 27/27 |
|
|
||||||
| 모든 작업 Step 2 보유 | PASS | 27/27 |
|
|
||||||
| 모든 작업 Step 3 보유 | PASS | 27/27 |
|
|
||||||
| 모든 작업 Step 4 보유 | PASS | 27/27 |
|
|
||||||
| 모든 작업 Step 5 보유 | PASS | 27/27 |
|
|
||||||
| 모든 작업 **Files:** 보유 | PASS | 27/27 |
|
|
||||||
| 모든 작업 **Interfaces:** 보유 | PASS | 27/27 |
|
|
||||||
| 모든 작업 git commit -m 보유 | PASS | 27/27 |
|
|
||||||
| Create 경로 중복 없음 | PASS | none |
|
|
||||||
| Superpowers 계획 헤더 | PASS | required header present |
|
|
||||||
| 설계 입력 제약 명시 | PASS | missing workbook handled explicitly |
|
|
||||||
| Typed/Advanced/Raw/Admin 4단계 | PASS | four exposure tiers |
|
|
||||||
| classic 자료구조 범위 | PASS | all classic groups present |
|
|
||||||
| 동기·Reactive parity 계획 | PASS | API parity covered |
|
|
||||||
| permit 위조 검증 | PASS | provenance verification covered |
|
|
||||||
| 토폴로지 task 선행 등록 | PASS | test tasks available before contracts |
|
|
||||||
| 환경 파일 생명주기 일관성 | PASS | create once, extend twice |
|
|
||||||
| 잘못된 Persistent factory 없음 | PASS | constructor usage consistent |
|
|
||||||
| 공유 async primitive | PASS | sync/reactive executor share invocation |
|
|
||||||
| Raw 문자열 API 금지 | PASS | guardrail fixed |
|
|
||||||
| R4 차단 | PASS | blocked in plan and design |
|
|
||||||
| 완료 정의 존재 | PASS | definition and release task present |
|
|
||||||
-2233
File diff suppressed because it is too large
Load Diff
-1497
File diff suppressed because it is too large
Load Diff
@@ -1,135 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Fail when the HTTP Client Platform's code and documentation have drifted.
|
|
||||||
|
|
||||||
The design (§33 "Documentation") requires the support matrix, configuration reference, security
|
|
||||||
guide, runbook, and migration guide to match the code. Review cannot hold that line by itself, so
|
|
||||||
this verifier extracts the names that are part of the public contract -- stable exceptions, metric
|
|
||||||
names, configuration properties, startup violation codes, and transports -- and fails when one
|
|
||||||
exists in code but nowhere in the documentation.
|
|
||||||
|
|
||||||
It deliberately checks one direction only. A name documented but not yet implemented is a plan; a
|
|
||||||
name implemented but undocumented is a surprise for whoever is on call.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
PLATFORM = REPO_ROOT / "src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient"
|
|
||||||
BOOTSTRAP = REPO_ROOT / "src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/httpclient"
|
|
||||||
DOCS_DIR = REPO_ROOT / "docs/httpclient"
|
|
||||||
ENV_FIELD_MANIFEST = REPO_ROOT / "docs/httpclient/env-fields.yaml"
|
|
||||||
|
|
||||||
REQUIRED_DOCS = [
|
|
||||||
"support-matrix.md",
|
|
||||||
"configuration-reference.md",
|
|
||||||
"retry-and-ambiguity.md",
|
|
||||||
"security.md",
|
|
||||||
"streaming.md",
|
|
||||||
"operations.md",
|
|
||||||
"migration-guide.md",
|
|
||||||
"release-checklist.md",
|
|
||||||
"performance-baseline.md",
|
|
||||||
"repository-adaptation.md",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def read_docs() -> str:
|
|
||||||
return "\n".join(
|
|
||||||
(DOCS_DIR / name).read_text(encoding="utf-8") for name in REQUIRED_DOCS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def stable_exceptions() -> list[str]:
|
|
||||||
error_dir = PLATFORM / "api/error"
|
|
||||||
return sorted(
|
|
||||||
path.stem
|
|
||||||
for path in error_dir.glob("Http*Exception.java")
|
|
||||||
if path.stem != "HttpClientException"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def metric_names() -> list[str]:
|
|
||||||
source = (PLATFORM / "observation/HttpClientObservationNames.java").read_text(encoding="utf-8")
|
|
||||||
return sorted(set(re.findall(r'"(http\.client\.[a-z_.]+)"', source)))
|
|
||||||
|
|
||||||
|
|
||||||
def violation_codes() -> list[str]:
|
|
||||||
codes: set[str] = set()
|
|
||||||
for source_file in [
|
|
||||||
PLATFORM / "profile/ClientProfileValidator.java",
|
|
||||||
PLATFORM / "security/TlsPolicyValidator.java",
|
|
||||||
BOOTSTRAP / "HttpClientStartupValidator.java",
|
|
||||||
]:
|
|
||||||
source = source_file.read_text(encoding="utf-8")
|
|
||||||
codes.update(re.findall(r'"([A-Z][A-Z0-9_]{4,})"', source))
|
|
||||||
return sorted(codes)
|
|
||||||
|
|
||||||
|
|
||||||
def configuration_properties() -> list[str]:
|
|
||||||
"""Every leaf property under `app.httpclient`, nested and dynamic blocks included.
|
|
||||||
|
|
||||||
Read from the environment-field manifest rather than from the record source. The manifest is
|
|
||||||
derived from `HttpClientPlatformSettings` by `HttpClientEnvironmentKeys` and held to it in both
|
|
||||||
directions by `HttpClientPlatformEnvManifestTest`, so it cannot drift from the code; parsing the
|
|
||||||
record here a second time, with a regex, could only agree with it by luck. The previous version
|
|
||||||
of this function did exactly that and saw eighteen top-level names, which is why a nested pool,
|
|
||||||
timeout or TLS setting could be added and documented nowhere.
|
|
||||||
"""
|
|
||||||
names: set[str] = set()
|
|
||||||
for line in ENV_FIELD_MANIFEST.read_text(encoding="utf-8").splitlines():
|
|
||||||
stripped = line.strip()
|
|
||||||
if not stripped.startswith("- field:"):
|
|
||||||
continue
|
|
||||||
path = stripped[len("- field:") :].strip()
|
|
||||||
leaf = path.split(".")[-1]
|
|
||||||
# `clients[N]` and `allowed-hosts[M]` are documented by name, not by position.
|
|
||||||
names.add(re.sub(r"\[[NM]\]$", "", leaf))
|
|
||||||
return sorted(names)
|
|
||||||
|
|
||||||
|
|
||||||
def transports() -> list[str]:
|
|
||||||
source = (PLATFORM / "profile/TransportType.java").read_text(encoding="utf-8")
|
|
||||||
body = source[source.index("public enum TransportType") :]
|
|
||||||
return sorted(set(re.findall(r"^\s{2}([A-Z][A-Z_]*),?$", body, flags=re.MULTILINE)))
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
missing_docs = [name for name in REQUIRED_DOCS if not (DOCS_DIR / name).is_file()]
|
|
||||||
if missing_docs:
|
|
||||||
print("FAIL missing documentation file(s): " + ", ".join(missing_docs))
|
|
||||||
return 1
|
|
||||||
|
|
||||||
documentation = read_docs()
|
|
||||||
failures: list[str] = []
|
|
||||||
|
|
||||||
checks = {
|
|
||||||
"stable exception": stable_exceptions(),
|
|
||||||
"metric": metric_names(),
|
|
||||||
"startup violation code": violation_codes(),
|
|
||||||
"configuration property": configuration_properties(),
|
|
||||||
"transport": transports(),
|
|
||||||
}
|
|
||||||
for kind, names in checks.items():
|
|
||||||
for name in names:
|
|
||||||
if name not in documentation:
|
|
||||||
failures.append(f"{kind} '{name}' exists in code but is not documented")
|
|
||||||
|
|
||||||
if failures:
|
|
||||||
print(f"FAIL httpclient documentation drift ({len(failures)} finding(s)):")
|
|
||||||
for failure in failures:
|
|
||||||
print(" - " + failure)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
total = sum(len(names) for names in checks.values())
|
|
||||||
print(f"PASS httpclient documentation covers {total} code-derived name(s):")
|
|
||||||
for kind, names in checks.items():
|
|
||||||
print(f" {kind}: {len(names)}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
Reference in New Issue
Block a user