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)