211 lines
8.9 KiB
Python
211 lines
8.9 KiB
Python
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)
|