parity_audit.py
1 from __future__ import annotations 2 3 import json 4 from dataclasses import dataclass 5 from pathlib import Path 6 7 ARCHIVE_ROOT = Path(__file__).resolve().parent.parent / 'archive' / 'claw_code_ts_snapshot' / 'src' 8 CURRENT_ROOT = Path(__file__).resolve().parent 9 REFERENCE_SURFACE_PATH = CURRENT_ROOT / 'reference_data' / 'archive_surface_snapshot.json' 10 COMMAND_SNAPSHOT_PATH = CURRENT_ROOT / 'reference_data' / 'commands_snapshot.json' 11 TOOL_SNAPSHOT_PATH = CURRENT_ROOT / 'reference_data' / 'tools_snapshot.json' 12 13 ARCHIVE_ROOT_FILES = { 14 'QueryEngine.ts': 'QueryEngine.py', 15 'Task.ts': 'task.py', 16 'Tool.ts': 'Tool.py', 17 'commands.ts': 'commands.py', 18 'context.ts': 'context.py', 19 'cost-tracker.ts': 'cost_tracker.py', 20 'costHook.ts': 'costHook.py', 21 'dialogLaunchers.tsx': 'dialogLaunchers.py', 22 'history.ts': 'history.py', 23 'ink.ts': 'ink.py', 24 'interactiveHelpers.tsx': 'interactiveHelpers.py', 25 'main.tsx': 'main.py', 26 'projectOnboardingState.ts': 'projectOnboardingState.py', 27 'query.ts': 'query.py', 28 'replLauncher.tsx': 'replLauncher.py', 29 'setup.ts': 'setup.py', 30 'tasks.ts': 'tasks.py', 31 'tools.ts': 'tools.py', 32 } 33 34 ARCHIVE_DIR_MAPPINGS = { 35 'assistant': 'assistant', 36 'bootstrap': 'bootstrap', 37 'bridge': 'bridge', 38 'buddy': 'buddy', 39 'cli': 'cli', 40 'commands': 'commands.py', 41 'components': 'components', 42 'constants': 'constants', 43 'context': 'context.py', 44 'coordinator': 'coordinator', 45 'entrypoints': 'entrypoints', 46 'hooks': 'hooks', 47 'ink': 'ink.py', 48 'keybindings': 'keybindings', 49 'memdir': 'memdir', 50 'migrations': 'migrations', 51 'moreright': 'moreright', 52 'native-ts': 'native_ts', 53 'outputStyles': 'outputStyles', 54 'plugins': 'plugins', 55 'query': 'query.py', 56 'remote': 'remote', 57 'schemas': 'schemas', 58 'screens': 'screens', 59 'server': 'server', 60 'services': 'services', 61 'skills': 'skills', 62 'state': 'state', 63 'tasks': 'tasks.py', 64 'tools': 'tools.py', 65 'types': 'types', 66 'upstreamproxy': 'upstreamproxy', 67 'utils': 'utils', 68 'vim': 'vim', 69 'voice': 'voice', 70 } 71 72 73 @dataclass(frozen=True) 74 class ParityAuditResult: 75 archive_present: bool 76 root_file_coverage: tuple[int, int] 77 directory_coverage: tuple[int, int] 78 total_file_ratio: tuple[int, int] 79 command_entry_ratio: tuple[int, int] 80 tool_entry_ratio: tuple[int, int] 81 missing_root_targets: tuple[str, ...] 82 missing_directory_targets: tuple[str, ...] 83 84 def to_markdown(self) -> str: 85 lines = ['# Parity Audit'] 86 if not self.archive_present: 87 lines.append('Local archive unavailable; parity audit cannot compare against the original snapshot.') 88 return '\n'.join(lines) 89 90 lines.extend([ 91 '', 92 f'Root file coverage: **{self.root_file_coverage[0]}/{self.root_file_coverage[1]}**', 93 f'Directory coverage: **{self.directory_coverage[0]}/{self.directory_coverage[1]}**', 94 f'Total Python files vs archived TS-like files: **{self.total_file_ratio[0]}/{self.total_file_ratio[1]}**', 95 f'Command entry coverage: **{self.command_entry_ratio[0]}/{self.command_entry_ratio[1]}**', 96 f'Tool entry coverage: **{self.tool_entry_ratio[0]}/{self.tool_entry_ratio[1]}**', 97 '', 98 'Missing root targets:', 99 ]) 100 if self.missing_root_targets: 101 lines.extend(f'- {item}' for item in self.missing_root_targets) 102 else: 103 lines.append('- none') 104 105 lines.extend(['', 'Missing directory targets:']) 106 if self.missing_directory_targets: 107 lines.extend(f'- {item}' for item in self.missing_directory_targets) 108 else: 109 lines.append('- none') 110 return '\n'.join(lines) 111 112 113 def _reference_surface() -> dict[str, object]: 114 return json.loads(REFERENCE_SURFACE_PATH.read_text()) 115 116 117 def _snapshot_count(path: Path) -> int: 118 return len(json.loads(path.read_text())) 119 120 121 def run_parity_audit() -> ParityAuditResult: 122 current_entries = {path.name for path in CURRENT_ROOT.iterdir()} 123 root_hits = [target for target in ARCHIVE_ROOT_FILES.values() if target in current_entries] 124 dir_hits = [target for target in ARCHIVE_DIR_MAPPINGS.values() if target in current_entries] 125 missing_roots = tuple(target for target in ARCHIVE_ROOT_FILES.values() if target not in current_entries) 126 missing_dirs = tuple(target for target in ARCHIVE_DIR_MAPPINGS.values() if target not in current_entries) 127 current_python_files = sum(1 for path in CURRENT_ROOT.rglob('*.py') if path.is_file()) 128 reference = _reference_surface() 129 return ParityAuditResult( 130 archive_present=ARCHIVE_ROOT.exists(), 131 root_file_coverage=(len(root_hits), len(ARCHIVE_ROOT_FILES)), 132 directory_coverage=(len(dir_hits), len(ARCHIVE_DIR_MAPPINGS)), 133 total_file_ratio=(current_python_files, int(reference['total_ts_like_files'])), 134 command_entry_ratio=(_snapshot_count(COMMAND_SNAPSHOT_PATH), int(reference['command_entry_count'])), 135 tool_entry_ratio=(_snapshot_count(TOOL_SNAPSHOT_PATH), int(reference['tool_entry_count'])), 136 missing_root_targets=missing_roots, 137 missing_directory_targets=missing_dirs, 138 )