""" GitHub Comment Formatter ========================= Converts our internal Finding and SynthesizedReview data structures into GitHub-flavored Markdown for posting as PR comments. Two types of output: 1. **Inline comments** โ€” one per finding, anchored to a specific file+line. These appear right next to the code, like a human reviewer's comments. 2. **Summary comment** โ€” a top-level PR comment with the Health Score, finding counts by severity, and an executive summary. Design decisions: - We use emoji prefixes for severity to make scanning fast (most devs skim reviews) - Each inline comment includes the agent name and category for traceability - CWE IDs are linked for security findings (so devs can learn about the vulnerability) - Suggested fixes use fenced code blocks for easy copy-paste """ from __future__ import annotations from app.models.findings import Finding, SynthesizedReview # Emoji and color mapping for severity levels SEVERITY_EMOJI = { "critical": "\U0001f6a8", # ๐Ÿšจ "high": "\U0001f7e0", # ๐ŸŸ  "medium": "\U0001f7e1", # ๐ŸŸก "low": "\u2139\ufe0f", # โ„น๏ธ } AGENT_EMOJI = { "security": "\U0001f512", # ๐Ÿ”’ "performance": "\u26a1", # โšก "style": "\u270f\ufe0f", # โœ๏ธ } def format_inline_comment(finding: Finding) -> str: """ Format a single Finding as a GitHub inline comment body. This Markdown will appear anchored to the specific file+line in the PR diff. Example output: ๐Ÿšจ **[CRITICAL โ€” Security] SQL Injection Risk** The query on line 47 constructs SQL via string interpolation. User input is directly embedded without sanitization. **Suggested fix:** ```python cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,)) ``` > ๐Ÿ”’ Security ยท CWE-89 ยท Confidence: 0.92 """ severity_emoji = SEVERITY_EMOJI.get(finding.severity, "") agent_emoji = AGENT_EMOJI.get(finding.agent, "") severity_upper = finding.severity.upper() agent_title = finding.agent.capitalize() # Build the comment body lines = [ f"{severity_emoji} **[{severity_upper} โ€” {agent_title}] {finding.title}**", "", finding.description, ] # Add suggested fix if present if finding.suggested_fix: lines.extend([ "", "**Suggested fix:**", "```", finding.suggested_fix, "```", ]) # Add metadata footer footer_parts = [f"{agent_emoji} {agent_title}"] if finding.cwe_id: footer_parts.append(f"[{finding.cwe_id}](https://cwe.mitre.org/data/definitions/{finding.cwe_id.split('-')[1]}.html)") footer_parts.append(f"Confidence: {finding.confidence:.2f}") lines.extend(["", f"> {' ยท '.join(footer_parts)}"]) return "\n".join(lines) def format_summary_comment(review: SynthesizedReview) -> str: """ Format the top-level PR summary comment with Health Score and finding overview. This is posted as a regular PR comment (not inline). It gives the PR author a quick overview without needing to look at every inline comment. The Health Score gauge uses block characters to create a visual progress bar in pure Unicode (works in GitHub Markdown without images). """ score = review.health_score # Determine overall status if score >= 80: status_emoji = "\u2705" # โœ… status_text = "Healthy" elif score >= 60: status_emoji = "\u26a0\ufe0f" # โš ๏ธ status_text = "Needs Attention" else: status_emoji = "\u274c" # โŒ status_text = "Action Required" # Build the visual health bar (20 segments) filled = round(score / 5) bar = "\u2588" * filled + "\u2591" * (20 - filled) # Count total findings total = ( review.critical_count + review.high_count + review.medium_count + review.low_count ) lines = [ f"## {status_emoji} Ninja Code Guard Review โ€” Health Score: {score}/100", "", f"`{bar}` **{score}**/100 โ€” {status_text}", "", "### Findings Summary", "", "| Severity | Count |", "|----------|-------|", f"| \U0001f6a8 Critical | {review.critical_count} |", f"| \U0001f7e0 High | {review.high_count} |", f"| \U0001f7e1 Medium | {review.medium_count} |", f"| \u2139\ufe0f Low | {review.low_count} |", f"| **Total** | **{total}** |", "", ] # Add recommendation rec_map = { "approve": "\u2705 **Recommendation: Approve** โ€” No critical issues found.", "request_changes": "\u26a0\ufe0f **Recommendation: Request Changes** โ€” Issues found that should be addressed.", "block": "\u274c **Recommendation: Block Merge** โ€” Critical issues must be resolved before merging.", } lines.append(rec_map.get(review.recommendation, "")) lines.append("") # Add executive summary lines.extend([ "### Executive Summary", "", review.executive_summary, "", ]) # Add detailed findings (so all info is visible even if inline comments fail) if review.findings: lines.append("### Detailed Findings") lines.append("") for _i, finding in enumerate(review.findings, 1): severity_emoji = SEVERITY_EMOJI.get(finding.severity, "") agent_emoji = AGENT_EMOJI.get(finding.agent, "") lines.append( f"
\n" f"{severity_emoji} [{finding.severity.upper()}] " f"{finding.title} โ€” {finding.file_path}:{finding.line_start}\n\n" f"{finding.description}\n" ) if finding.suggested_fix: lines.append(f"**Suggested fix:**\n```\n{finding.suggested_fix}\n```\n") footer_parts = [f"{agent_emoji} {finding.agent.capitalize()}"] if finding.cwe_id: cwe_num = finding.cwe_id.split("-")[-1] if "-" in finding.cwe_id else "" footer_parts.append(f"[{finding.cwe_id}](https://cwe.mitre.org/data/definitions/{cwe_num}.html)") footer_parts.append(f"Confidence: {finding.confidence:.2f}") lines.append(f"> {' ยท '.join(footer_parts)}\n") lines.append("
\n") lines.extend([ "---", "*Reviewed by [Ninja Code Guard](https://github.com/ninjacode911/ninja-code-guard) โ€” Multi-agent code review*", ]) return "\n".join(lines) def findings_to_review_comments(findings: list[Finding]) -> list[dict]: """ Convert a list of Findings into GitHub review comment dicts. Each dict has the structure that GitHub's Create Review API expects: - path: the file path relative to repo root - line: the line number in the NEW version of the file - body: the formatted Markdown comment Note: GitHub requires `line` to be within the diff hunk. If a finding references a line outside the diff, we skip it (GitHub API would reject it). We use `line` (not `position`) because position-based comments are deprecated. """ comments = [] for finding in findings: comment = { "path": finding.file_path, "line": finding.line_start, "side": "RIGHT", # RIGHT = new version of the file (what the PR introduces) "body": format_inline_comment(finding), } comments.append(comment) return comments