parity_audit.py 5.1 KB

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