from __future__ import annotations from pathlib import Path import hashlib import re import sys import zipfile SCRIPT_DIR = Path(__file__).resolve().parent PACKAGE_DESIGN = SCRIPT_DIR / 'docs/superpowers/specs/2026-08-11-jpa-persistence-platform-design.md' if PACKAGE_DESIGN.exists(): ROOT = SCRIPT_DIR DESIGN = PACKAGE_DESIGN PLAN = ROOT / 'docs/superpowers/plans/2026-08-11-jpa-persistence-platform-implementation-plan.md' EXPANSION = ROOT / 'docs/superpowers/plans/2026-08-11-jpa-persistence-experimental-expansion-plan.md' VALIDATION = ROOT / 'VALIDATION.md' ZIP_PATH = ROOT.parent / 'jpa-superpowers-package.zip' else: ROOT = Path('/mnt/data') DESIGN = ROOT / 'jpa-persistence-platform-design.md' PLAN = ROOT / 'jpa-persistence-platform-implementation-plan.md' EXPANSION = ROOT / 'jpa-persistence-experimental-expansion-plan.md' VALIDATION = ROOT / 'jpa-superpowers-validation.md' ZIP_PATH = ROOT / 'jpa-superpowers-package.zip' checks: list[tuple[str, bool, str]] = [] def check(name: str, condition: bool, detail: str = '') -> None: checks.append((name, bool(condition), detail)) def sha256(path: Path) -> str: h = hashlib.sha256() with path.open('rb') as f: for chunk in iter(lambda: f.read(1024 * 1024), b''): h.update(chunk) return h.hexdigest() def task_chunks(text: str) -> list[tuple[int, str]]: matches = list(re.finditer(r'^### Task (\d+):', text, re.MULTILINE)) chunks: list[tuple[int, str]] = [] for i, match in enumerate(matches): end = matches[i + 1].start() if i + 1 < len(matches) else len(text) chunks.append((int(match.group(1)), text[match.start():end])) return chunks for path, label in [(DESIGN, '설계서'), (PLAN, 'Stable 계획서'), (EXPANSION, 'Experimental 계획서')]: check(f'{label} 존재', path.exists(), str(path)) if not all(path.exists() for path in [DESIGN, PLAN, EXPANSION]): print('missing required documents', file=sys.stderr) sys.exit(2) design = DESIGN.read_text(encoding='utf-8') plan = PLAN.read_text(encoding='utf-8') expansion = EXPANSION.read_text(encoding='utf-8') line_counts = { 'design': len(design.splitlines()), 'plan': len(plan.splitlines()), 'expansion': len(expansion.splitlines()), } check('설계서 최소 상세도', line_counts['design'] >= 2500, f"{line_counts['design']} lines") check('Stable 계획서 최소 상세도', line_counts['plan'] >= 4000, f"{line_counts['plan']} lines") check('Experimental 계획서 최소 상세도', line_counts['expansion'] >= 650, f"{line_counts['expansion']} lines") for text, label in [(design, '설계서'), (plan, 'Stable 계획서'), (expansion, 'Experimental 계획서')]: fences = len(re.findall(r'^```', text, re.MULTILINE)) check(f'{label} 코드 fence 균형', fences % 2 == 0, str(fences)) bad = re.findall(r'\b(?:TODO|TBD|FIXME|implement later|fill in details)\b', text, re.IGNORECASE) check(f'{label} placeholder 부재', not bad, ', '.join(sorted(set(bad)))) required_design_terms = [ 'GenericRepository', 'TransactionCompletionUnknownException', 'EvidenceAwareJpaTransactionManager', 'Application Service', 'OSIV', 'PostgreSQL 16·17·18', 'Hibernate 7.4 Collection Fetch Pagination', 'FOR UPDATE SKIP LOCKED', 'CREATE INDEX CONCURRENTLY', 'Flyway', 'Runtime·Migration·Admin', 'H2는 Local Convenience', '전체 Transaction Retry', 'J1 Standard Persistence', 'J4 Admin / Operations', '완료 정의', ] for term in required_design_terms: check(f'설계 핵심 계약: {term}', term in design) check('GenericRepository 실제 선언 부재', 'public interface GenericRepository' not in design + plan and 'interface GenericRepository<' not in design + plan) check('운영 ddl auto update 금지', '운영에서 `ddl-auto=update`' in plan) check('Completion Unknown 자동 Retry 금지', 'TransactionCompletionUnknownException' in plan and 'RetryDecision.reconcile' in plan and 'Never retry completion unknown' in plan) check('OSIV false 강제', 'spring.jpa.open-in-view must be false' in plan) check('PG16·17·18 Matrix', 'PG_16' in plan and 'PG_17' in plan and 'PG_18' in plan) check('Hibernate 7.4 fetch pagination gate', 'HibernateCollectionFetchPaginationContractTest' in plan) check('Flyway snapshot upgrade gate', 'FlywayUpgradeContractTest' in plan) check('Runtime role no DDL gate', 'runtimeRoleCanWriteRowsButCannotCreateTable' in plan) check('Stable 계획에 Experimental create 경로 부재', '- Create: `modules/jpa-experimental/' not in plan) stable_chunks = task_chunks(plan) exp_chunks = task_chunks(expansion) check('Stable Task 1~53 연속성', [n for n, _ in stable_chunks] == list(range(1, 54)), str([n for n, _ in stable_chunks])) check('Experimental Task 1~9 연속성', [n for n, _ in exp_chunks] == list(range(1, 10)), str([n for n, _ in exp_chunks])) check('Stable Task chunk 수', len(stable_chunks) == 53, str(len(stable_chunks))) check('Experimental Task chunk 수', len(exp_chunks) == 9, str(len(exp_chunks))) required_markers = [ '**Files:**', '**Interfaces:**', '**Implementation requirements:**', '**Step 1:', '**Step 2:', '**Step 3:', '**Step 4:', '**Step 5:', 'Expected:', 'git commit -m', ] for group_name, chunks in [('Stable', stable_chunks), ('Experimental', exp_chunks)]: for number, chunk in chunks: for marker in required_markers: check(f'{group_name} Task {number} 필수 항목: {marker}', marker in chunk) check(f'{group_name} Task {number} Gradle focused command', './gradlew ' in chunk and '--tests' in chunk) check(f'{group_name} Task {number} exact path', '*' not in '\n'.join( line for line in chunk.splitlines() if line.startswith(('- Create:', '- Modify:', '- Test:')))) create_pattern = re.compile(r'^- Create: `([^`]+)`', re.MULTILINE) stable_creates = create_pattern.findall(plan) exp_creates = create_pattern.findall(expansion) check('Stable Create 경로 중복 부재', len(stable_creates) == len(set(stable_creates)), str(len(stable_creates))) check('Experimental Create 경로 중복 부재', len(exp_creates) == len(set(exp_creates)), str(len(exp_creates))) check('Stable·Experimental Create 경로 충돌 부재', not (set(stable_creates) & set(exp_creates)), str(set(stable_creates) & set(exp_creates))) check('Experimental 계획은 Stable Task 1~53 이후 시작', 'Stable 계획 Task 1~53이 완료되고' in expansion, '') # Type/name consistency checks for high-risk cross-task contracts. for term in [ 'PersistenceOperationName', 'TransactionProfile', 'RetryProfile', 'JpaTransactionExecutor', 'JpaRetryPolicy', 'RetryDecision', 'JpaFailureContext', 'QueryName', 'KeysetPageRequest', 'KeysetSlice', 'TransactionCompletionUnknownException', 'PostgreSqlWorkClaimExecutor', 'FlywayValidationGate', 'JpaPlatformEndpoint', ]: check(f'공통 타입 일관성: {term}', plan.count(term) >= 2, str(plan.count(term))) check('Experimental Gradle 경로 정확성', ':modules:jpa-experimental:' in expansion) check('Replica annotation-only routing 금지', 'readOnly=true`만으로 replica routing하지 않는다' in expansion) check('RLS connection reuse 검증', 'pooledConnectionDoesNotLeakPriorTenantSetting' in expansion) check('Stable 승격 ADR gate', 'BLOCKED_MISSING_ADR' in expansion) # Source preservation check. check('심층 리서치 원문 부록 포함', '# 부록 A. 심층 리서치 원문 보존본' in design and '# JPA 관계형 영속성 플랫폼 심층 리서치' in design) passed = sum(1 for _, ok, _ in checks if ok) failed = len(checks) - passed status = 'PASS' if failed == 0 else 'FAIL' lines = [ '# JPA Superpowers 문서 정적 검증', '', f'- 결과: **{status}**', f'- 실행 검사: **{len(checks)}개**', f'- 통과: **{passed}개**', f'- 실패: **{failed}개**', f'- 설계서: **{line_counts["design"]:,}행**', f'- Stable 구현 계획서: **{line_counts["plan"]:,}행**', f'- Experimental 확장 계획서: **{line_counts["expansion"]:,}행**', f'- Stable Task: **{len(stable_chunks)}개**', f'- Experimental Task: **{len(exp_chunks)}개**', f'- Stable Create 경로: **{len(stable_creates)}개**', f'- Experimental Create 경로: **{len(exp_creates)}개**', f'- 설계 SHA-256: `{sha256(DESIGN)}`', f'- Stable 계획 SHA-256: `{sha256(PLAN)}`', f'- Experimental 계획 SHA-256: `{sha256(EXPANSION)}`', '', '## 검사 결과', '', '| 검사 | 결과 | 상세 |', '|---|---:|---|', ] for name, ok, detail in checks: safe_detail = detail.replace('|', '\\|').replace('\n', ' ')[:500] lines.append(f'| {name} | {"PASS" if ok else "FAIL"} | {safe_detail} |') VALIDATION.write_text('\n'.join(lines) + '\n', encoding='utf-8') print(f'{status}: {passed}/{len(checks)} checks passed') if failed: for name, ok, detail in checks: if not ok: print(f'FAIL: {name}: {detail}') sys.exit(1)