diff --git a/repohealth-cli/src/repohealth/reporters/json_reporter.py b/repohealth-cli/src/repohealth/reporters/json_reporter.py new file mode 100644 index 0000000..30e9a78 --- /dev/null +++ b/repohealth-cli/src/repohealth/reporters/json_reporter.py @@ -0,0 +1,130 @@ +import json + +from repohealth.analyzers.risk_analyzer import DiversificationSuggestion, Hotspot +from repohealth.models.file_stats import FileAnalysis +from repohealth.models.result import RepositoryResult + + +class JSONReporter: + """Reporter for JSON output.""" + + def __init__(self, indent: int = 2): + """Initialize the reporter. + + Args: + indent: JSON indentation level. + """ + self.indent = indent + + def generate(self, result: RepositoryResult) -> str: + """Generate JSON output from a result. + + Args: + result: RepositoryResult to convert. + + Returns: + JSON string. + """ + output = { + "version": "1.0", + "repository": result.repository_path, + "analyzed_at": result.analyzed_at.isoformat(), + "files_analyzed": result.files_analyzed, + "summary": { + "files_analyzed": result.files_analyzed, + "total_commits": result.total_commits, + "unique_authors": result.unique_authors, + "overall_bus_factor": round(result.overall_bus_factor, 2), + "gini_coefficient": round(result.gini_coefficient, 3), + "overall_risk": result.risk_summary.get("overall_risk", "unknown") + }, + "risk_summary": result.risk_summary, + "files": result.files, + "hotspots": result.hotspots, + "suggestions": result.suggestions, + "metadata": result.metadata + } + + indent = self.indent if self.indent else None + return json.dumps(output, indent=indent, default=str) + + def save(self, result: RepositoryResult, file_path: str) -> None: + """Save JSON output to a file. + + Args: + result: RepositoryResult to save. + file_path: Path to output file. + """ + json_str = self.generate(result) + + with open(file_path, 'w') as f: + f.write(json_str) + + def generate_file_dict(self, analysis: FileAnalysis) -> dict: + """Convert a FileAnalysis to a dictionary. + + Args: + analysis: FileAnalysis to convert. + + Returns: + Dictionary representation. + """ + return { + "path": analysis.path, + "total_commits": analysis.total_commits, + "num_authors": analysis.num_authors, + "author_commits": analysis.author_commits, + "gini_coefficient": round(analysis.gini_coefficient, 3), + "bus_factor": round(analysis.bus_factor, 2), + "risk_level": analysis.risk_level, + "top_author_share": round(analysis.top_author_share, 3), + "module": analysis.module, + "extension": analysis.extension, + "first_commit": ( + analysis.first_commit.isoformat() + if analysis.first_commit else None + ), + "last_commit": ( + analysis.last_commit.isoformat() + if analysis.last_commit else None + ) + } + + def generate_hotspot_dict(self, hotspot: Hotspot) -> dict: + """Convert a Hotspot to a dictionary. + + Args: + hotspot: Hotspot to convert. + + Returns: + Dictionary representation. + """ + return { + "file_path": hotspot.file_path, + "risk_level": hotspot.risk_level, + "bus_factor": round(hotspot.bus_factor, 2), + "top_author": hotspot.top_author, + "top_author_share": round(hotspot.top_author_share, 3), + "total_commits": hotspot.total_commits, + "num_authors": hotspot.num_authors, + "module": hotspot.module, + "suggestion": hotspot.suggestion + } + + def generate_suggestion_dict(self, suggestion: DiversificationSuggestion) -> dict: + """Convert a DiversificationSuggestion to a dictionary. + + Args: + suggestion: Suggestion to convert. + + Returns: + Dictionary representation. + """ + return { + "file_path": suggestion.file_path, + "current_author": suggestion.current_author, + "suggested_authors": suggestion.suggested_authors, + "priority": suggestion.priority, + "reason": suggestion.reason, + "action": suggestion.action + }