feat: 설계 문서 추가

This commit is contained in:
DongHyeonka
2026-08-13 21:09:52 +09:00
parent 0a6dd0e419
commit 3b5aee50e3
33 changed files with 45861 additions and 0 deletions
@@ -0,0 +1,210 @@
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)