fix: 설계 문서 제거

This commit is contained in:
DongHyeonka
2026-08-19 15:56:29 +09:00
parent 6eec8a0656
commit ab0447a0f9
15 changed files with 0 additions and 15296 deletions
-25
View File
@@ -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)
-23
View File
@@ -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는 명시된 구현 가정이다.
@@ -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)
-43
View File
@@ -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`를 읽어 재생성하고 정책 오버레이를 적용하도록 설계했다.
-38
View File
@@ -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 |
-135
View File
@@ -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())