/ src / parity_audit.py
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      )