from pathlib import Path import re import sys ROOT = Path(__file__).resolve().parent DESIGN = ROOT / 'docs/superpowers/specs/2026-08-11-mongodb-document-persistence-platform-design.md' PLAN = ROOT / 'docs/superpowers/plans/2026-08-11-mongodb-document-persistence-platform-implementation-plan.md' ADV = ROOT / 'docs/superpowers/plans/2026-08-11-mongodb-advanced-capabilities-expansion-plan.md' checks = [] def check(name, condition, detail=''): checks.append((name, bool(condition), detail)) texts = {p.name: p.read_text(encoding='utf-8') for p in (DESIGN, PLAN, ADV)} design = texts[DESIGN.name] plan = texts[PLAN.name] adv = texts[ADV.name] check('design exists', DESIGN.exists()) check('stable plan exists', PLAN.exists()) check('advanced plan exists', ADV.exists()) check('design purpose', 'MongoDB 문서 영속성 플랫폼 설계서' in design) check('domain ownership', '도메인이 `@Document`, Repository' in design) check('no generic repository design', '범용 `CommonMongoRepository`를 만들지 않는다' in design) check('stable api strict', 'Stable API V1' in design and 'apiStrict=true' in design) check('local replica set', 'Single-node Replica Set' in design) check('standalone smoke only', 'Standalone은 smoke test' in design) check('bson manifest', 'BSON 표현 Manifest' in design) check('transaction retry separation', 'Transaction 본문 Retry와 Commit Retry를 분리' in design) check('change stream at least once', 'at-least-once projector' in design) check('ttl cleanup only', 'TTL은 물리 cleanup' in design) check('gridfs compatibility only', 'GridFS는 compatibility adapter' in design) check('driver native observability', 'Driver native ObservabilitySettings' in design) for label, text, expected in [('stable', plan, 50), ('advanced', adv, 15)]: nums = [int(x) for x in re.findall(r'^### Task (\d+):', text, re.M)] check(f'{label} task count', len(nums) == expected, f'{len(nums)}') check(f'{label} task sequence', nums == list(range(1, expected + 1)), str(nums[:3]) + '...' + str(nums[-3:])) sections = re.split(r'(?=^### Task \d+:)', text, flags=re.M)[1:] for idx, section in enumerate(sections, 1): for marker in ['**Files:**', '**Interfaces:**', '**Implementation requirements:**', '**Step 1:', '**Step 2:', '**Step 3:', '**Step 4:', '**Step 5:']: check(f'{label} task {idx} has {marker}', marker in section) check(f'{label} task {idx} has commit', 'git commit -m' in section) check(f'{label} task {idx} has expected result', 'Expected:' in section) for name, text in texts.items(): check(f'{name} code fences balanced', text.count('```') % 2 == 0, str(text.count('```'))) check(f'{name} no TODO markers', not re.search(r'\b(TODO|TBD|FIXME)\b', text)) check(f'{name} no wildcard create paths', not re.search(r'- Create: `[^`]*[\*?][^`]*`', text)) for label, text in [('stable', plan), ('advanced', adv)]: created = re.findall(r'- Create: `([^`]+)`', text) duplicates = sorted({p for p in created if created.count(p) > 1}) check(f'{label} no duplicate create paths', not duplicates, ', '.join(duplicates)) stable_created = set(re.findall(r'- Create: `([^`]+)`', plan)) advanced_created = set(re.findall(r'- Create: `([^`]+)`', adv)) check('stable and advanced create paths do not collide', not (stable_created & advanced_created), ', '.join(sorted(stable_created & advanced_created))) check('no real generic repository declaration', not re.search(r'public\s+interface\s+(Common|Generic)MongoRepository', design + plan + adv)) check('no public arbitrary run command', not re.search(r'public\s+[^\n]+\s+runCommand\s*\(', design + plan + adv)) check('stable starter excludes advanced', 'no advanced module' in plan.lower() and 'Stable Starter' in adv) check('unknown commit body retry forbidden', 'UnknownTransactionCommitResult' in plan and '업무 본문을 재실행하지 않는다' in plan) check('mongo seven and eight matrix', 'MongoDB 7.0' in plan and 'MongoDB 8.0' in plan) check('advanced actual topology gate', 'actual topology' in adv.lower() or '실제 topology' in adv) failed = [c for c in checks if not c[1]] for name, ok, detail in checks: print(('PASS' if ok else 'FAIL') + ' | ' + name + ((' | ' + detail) if detail else '')) print(f'SUMMARY | total={len(checks)} pass={len(checks)-len(failed)} fail={len(failed)}') sys.exit(1 if failed else 0)