149 lines
6.1 KiB
Python
149 lines
6.1 KiB
Python
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)
|