feat: 설계 문서 추가
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user