/ tools / spec-verify / output_writer.py
output_writer.py
  1  """
  2  Output Writer
  3  
  4  Writes verification findings to todo.cspec files.
  5  """
  6  
  7  from datetime import datetime
  8  from pathlib import Path
  9  from typing import Optional
 10  
 11  try:
 12      from . import config
 13      from .verifier import VerificationResult
 14      from .providers.base import Finding
 15  except ImportError:
 16      import config
 17      from verifier import VerificationResult
 18      from providers.base import Finding
 19  
 20  
 21  def write_todo_cspec(result: VerificationResult) -> Optional[Path]:
 22      """
 23      Write verification findings to a todo.cspec file.
 24  
 25      Args:
 26          result: Verification result with findings
 27  
 28      Returns:
 29          Path to written file, or None if no findings
 30      """
 31      if not result.findings:
 32          return None
 33  
 34      output_dir = Path(__file__).parent / config.OUTPUT_DIR
 35      output_dir.mkdir(parents=True, exist_ok=True)
 36  
 37      filename = f"{result.repo}-{result.component}{config.OUTPUT_SUFFIX}"
 38      output_path = output_dir / filename
 39  
 40      content = format_todo_cspec(result)
 41  
 42      with open(output_path, 'w') as f:
 43          f.write(content)
 44  
 45      return output_path
 46  
 47  
 48  def format_todo_cspec(result: VerificationResult) -> str:
 49      """
 50      Format verification result as todo.cspec content.
 51  
 52      Args:
 53          result: Verification result
 54  
 55      Returns:
 56          Formatted cspec content
 57      """
 58      lines = [
 59          "# Auto-generated by spec-verify",
 60          "# Delete this file after issues are resolved",
 61          f"generated: {result.timestamp.strftime('%Y-%m-%dT%H:%M:%SZ')}",
 62          f"repo: {result.repo}",
 63          f"component: {result.component}",
 64          f"verified_by: {result.verified_by}",
 65          "",
 66          "findings:",
 67      ]
 68  
 69      for i, finding in enumerate(result.findings, 1):
 70          finding_id = f"{result.component}-F{i:03d}"
 71          lines.extend([
 72              f"  - id: {finding_id}",
 73              f"    severity: {finding.severity}",
 74              f"    type: {finding.finding_type}",
 75              f"    spec_file: {finding.spec_file}",
 76              f"    code_file: {finding.code_file}",
 77              f"    spec_says: \"{finding.spec_value}\"",
 78              f"    code_has: \"{finding.code_value}\"",
 79              f"    action: reconcile_spec_and_code",
 80              "",
 81          ])
 82  
 83      lines.append("status: pending  # pending | investigating | resolved")
 84  
 85      return "\n".join(lines)
 86  
 87  
 88  def write_summary(results: list[VerificationResult]) -> Path:
 89      """
 90      Write summary of all verification results.
 91  
 92      Args:
 93          results: List of verification results
 94  
 95      Returns:
 96          Path to summary file
 97      """
 98      output_dir = Path(__file__).parent / config.OUTPUT_DIR
 99      output_dir.mkdir(parents=True, exist_ok=True)
100  
101      summary_path = output_dir / "summary.cspec"
102  
103      total_findings = sum(len(r.findings) for r in results)
104      critical_count = sum(
105          1 for r in results
106          for f in r.findings
107          if f.severity == config.SEVERITY_CRITICAL
108      )
109      high_count = sum(
110          1 for r in results
111          for f in r.findings
112          if f.severity == config.SEVERITY_HIGH
113      )
114  
115      lines = [
116          "# Spec-Verify Summary",
117          f"# Generated: {datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')}",
118          "",
119          "summary:",
120          f"  total_repos: {len(set(r.repo for r in results))}",
121          f"  total_components: {len(results)}",
122          f"  total_findings: {total_findings}",
123          f"  critical: {critical_count}",
124          f"  high: {high_count}",
125          "",
126          "by_repo:",
127      ]
128  
129      # Group by repo
130      repos = {}
131      for result in results:
132          if result.repo not in repos:
133              repos[result.repo] = []
134          repos[result.repo].append(result)
135  
136      for repo, repo_results in repos.items():
137          repo_findings = sum(len(r.findings) for r in repo_results)
138          lines.append(f"  {repo}:")
139          lines.append(f"    components: {len(repo_results)}")
140          lines.append(f"    findings: {repo_findings}")
141  
142          for result in repo_results:
143              if result.findings:
144                  lines.append(f"    - {result.component}: {len(result.findings)} issues")
145  
146      lines.append("")
147      lines.append("# Run `cat output/*.todo.cspec` for details")
148  
149      with open(summary_path, 'w') as f:
150          f.write("\n".join(lines))
151  
152      return summary_path
153  
154  
155  def print_summary(results: list[VerificationResult]) -> None:
156      """Print verification summary to console."""
157      total_findings = sum(len(r.findings) for r in results)
158      repos_checked = len(set(r.repo for r in results))
159      components_checked = len(results)
160  
161      print("\n" + "=" * 50)
162      print("SPEC-VERIFY SUMMARY")
163      print("=" * 50)
164      print(f"Repos checked: {repos_checked}")
165      print(f"Components checked: {components_checked}")
166      print(f"Total findings: {total_findings}")
167  
168      if total_findings > 0:
169          print("\nFindings by severity:")
170          for severity in [config.SEVERITY_CRITICAL, config.SEVERITY_HIGH,
171                          config.SEVERITY_MEDIUM, config.SEVERITY_LOW]:
172              count = sum(
173                  1 for r in results
174                  for f in r.findings
175                  if f.severity == severity
176              )
177              if count > 0:
178                  print(f"  {severity}: {count}")
179  
180          print("\nOutput files written to:")
181          print(f"  {Path(__file__).parent / config.OUTPUT_DIR}/")
182      else:
183          print("\nNo issues found!")
184  
185      print("=" * 50)