diff --git a/Server/app/worker-service/services/review.service.ts b/Server/app/worker-service/services/review.service.ts index 61da9dc..2ef6f42 100644 --- a/Server/app/worker-service/services/review.service.ts +++ b/Server/app/worker-service/services/review.service.ts @@ -38,14 +38,21 @@ export type PRReviewJobData = { userId?: string; }; +const SEVERITY_ORDER: Record = { high: 0, medium: 1, low: 2 }; +const SEVERITY_LABEL: Record = { + high: "Should fix", + medium: "Worth fixing", + low: "nit", +}; + export function formatReviewComment(parsed: any, gifURL?: string | null): string { const score = parsed.score ?? "N/A"; const issues: any[] = parsed.issues ?? []; - const grouped = { - high: issues.filter((i) => i.severity === "high"), - medium: issues.filter((i) => i.severity === "medium"), - low: issues.filter((i) => i.severity === "low"), + const counts = { + high: issues.filter((i) => i.severity === "high").length, + medium: issues.filter((i) => i.severity === "medium").length, + low: issues.filter((i) => i.severity === "low").length, }; let md = `## Code Review\n\n`; @@ -54,46 +61,41 @@ export function formatReviewComment(parsed: any, gifURL?: string | null): string md += `![review-gif](${gifURL})\n\n`; } - md += `${parsed.summary}\n\n`; + if (parsed.summary) { + md += `${parsed.summary}\n\n`; + } - md += `Score: ${score}/100`; + md += `**Score: ${score}/100**`; if (issues.length > 0) { - md += ` (${grouped.high.length} high, ${grouped.medium.length} medium, ${grouped.low.length} low)`; + md += ` · ${counts.high} high, ${counts.medium} medium, ${counts.low} low`; } md += `\n\n`; if (issues.length === 0) { - md += `No issues found. This looks good to me.\n`; + md += `Nothing blocking from me here — this looks good to merge.\n\n`; + md += `---\nGenerated by CodeRefyn\n`; return md; } md += `---\n\n`; - function renderSection(title: string, arr: any[]) { - if (arr.length === 0) return ""; - - return ` -### ${title} - -${arr - .map( - (issue) => ` -- ${issue.file}:${issue.line} - - Category: ${issue.category} - - Issue: ${issue.problem} - - Suggestion: ${issue.fix} -`, - ) - .join("\n")} -`; - } + const sorted = [...issues].sort( + (a, b) => + (SEVERITY_ORDER[a.severity] ?? 3) - (SEVERITY_ORDER[b.severity] ?? 3), + ); - md += renderSection("High severity issues", grouped.high); - md += renderSection("Medium severity issues", grouped.medium); - md += renderSection("Low severity issues", grouped.low); + md += sorted + .map((issue) => { + const label = SEVERITY_LABEL[issue.severity] ?? "Note"; + const location = issue.line + ? `\`${issue.file}:${issue.line}\`` + : `\`${issue.file}\``; + const suggestion = issue.fix ? ` _Suggestion: ${issue.fix}_` : ""; + return `- ${location} — **${label}:** ${issue.problem}${suggestion}`; + }) + .join("\n"); - md += `\n---\n`; - md += `Generated by CodeRefyn\n`; + md += `\n\n---\nGenerated by CodeRefyn\n`; return md; } @@ -107,12 +109,17 @@ Once configured, all reviews will use your key and token usage will appear on th } async function getReviewGifUrl( - summary: string | null | undefined, + parsed: any, apiKey: string, usage: ReturnType, ): Promise { try { - const gifQuery = await getGifName(summary, apiKey, usage); + const issues: any[] = parsed?.issues ?? []; + const gifQuery = await getGifName(parsed?.summary, apiKey, usage, { + score: parsed?.score ?? null, + totalIssues: issues.length, + highSeverityCount: issues.filter((i) => i.severity === "high").length, + }); return await findGIF(gifQuery); } catch (error) { console.error("Skipping review GIF", error); @@ -210,7 +217,7 @@ export async function runPRReview(data: PRReviewJobData) { console.log("Failed to parse AI response"); return; } - const gifUrl = await getReviewGifUrl(parsedPrompt.summary, apiKey, tokenUsage); + const gifUrl = await getReviewGifUrl(parsedPrompt, apiKey, tokenUsage); const commentBody = formatReviewComment(parsedPrompt, gifUrl); await octokit.issues.createComment({ diff --git a/Server/package/ai/gif.ts b/Server/package/ai/gif.ts index bf69f53..cfadc6c 100644 --- a/Server/package/ai/gif.ts +++ b/Server/package/ai/gif.ts @@ -2,12 +2,51 @@ import Anthropic from "@anthropic-ai/sdk"; import { addUsage, createTokenAccumulator, type TokenAccumulator } from "./review.js"; import { CLAUDE_MODEL } from "./models.js"; +export type GifContext = { + score?: number | null; + highSeverityCount?: number; + totalIssues?: number; +}; + +type Mood = "celebrate" | "approve" | "minor" | "needs-work"; + +const MOOD_FALLBACK: Record = { + celebrate: "celebration confetti", + approve: "thumbs up approve", + minor: "almost there", + "needs-work": "needs more work", +}; + +const MOOD_HINT: Record = { + celebrate: + "The PR is clean with no issues — pick a happy, celebratory, approving vibe.", + approve: + "The PR is solid with only minor notes — pick a positive, encouraging vibe.", + minor: + "The PR is mostly fine but has a few things to tidy up — pick a lighthearted 'almost there' vibe.", + "needs-work": + "The PR has real problems to address — pick a friendly, lighthearted 'needs some work' reaction (never mean).", +}; + +function resolveMood(context?: GifContext): Mood { + const total = context?.totalIssues ?? 0; + const high = context?.highSeverityCount ?? 0; + const score = context?.score ?? null; + + if (high > 0 || (score !== null && score < 60)) return "needs-work"; + if (total === 0 && (score === null || score >= 90)) return "celebrate"; + if (total <= 2 && (score === null || score >= 80)) return "approve"; + return "minor"; +} + export async function getGifName( summary: string | null | undefined, apiKey: string, usage?: TokenAccumulator, + context?: GifContext, ): Promise { - const fallback = "code review"; + const mood = resolveMood(context); + const fallback = MOOD_FALLBACK[mood]; const cleanSummary = summary?.trim(); if (!cleanSummary) return fallback; @@ -16,9 +55,10 @@ export async function getGifName( const res = await anthropic.messages.create({ model: CLAUDE_MODEL, max_tokens: 40, - temperature: 0, - system: - "Turn a code review summary into a short Giphy search query. Return only 2 to 5 simple words, no punctuation.", + temperature: 0.4, + system: `Turn a code review into a short, fun Giphy search query that captures the MOOD of the review. +${MOOD_HINT[mood]} +Keep it relatable to developers. Return ONLY 2 to 5 simple words, no punctuation, no quotes.`, messages: [ { role: "user", diff --git a/Server/package/ai/review.ts b/Server/package/ai/review.ts index 7ee873a..a84f494 100644 --- a/Server/package/ai/review.ts +++ b/Server/package/ai/review.ts @@ -102,50 +102,46 @@ async function _callAI(prompt: string | null, apiKey: string, usage?: TokenAccum model: CLAUDE_MODEL, max_tokens: 4096, temperature: 0, - system: `You are a strict senior software engineer performing code reviews. - -Your job is to analyze git diffs and identify issues based on: - -1. General engineering best practices -2. Security vulnerabilities -3. Performance problems -4. Maintainability and readability -5. Custom repository-specific rules (highest priority) - -CRITICAL RULES: - -- Repository-specific rules OVERRIDE general best practices -- Be strict and critical, not polite -- Focus ONLY on real issues (no fluff) -- Do NOT hallucinate files or lines -- Only comment on changed code (diff) - -CLASSIFY every issue into: - -Categories: -- security -- performance -- quality -- maintainability - -Severity levels: -- high → can break system / security risk -- medium → bad practice / potential bug -- low → improvement / readability - -SCORING RULE: - -Start from 100 and deduct: -- high issue → -10 -- medium issue → -5 -- low issue → -2 - -FINAL OUTPUT: + system: `You are a friendly, pragmatic senior engineer reviewing a teammate's pull request. + +Write the way a thoughtful human reviewer leaves inline comments on a diff: short, specific, and collaborative. Not robotic, not harsh. + +TONE & STYLE (match this closely): +- Tie every comment to a concrete consequence ("this can throw when the session expires"), not a vague rule. +- Suggest a fix, don't just point out the problem. +- When the intent is unclear, ask a question instead of assuming ("Can you help me understand why...?"). +- Keep each comment to ONE or TWO short sentences. No essays, no walls of text. +- It is completely fine to find nothing major. Do NOT invent issues to look thorough. +- Acknowledge good work when it's there. Stay constructive, never strict or pedantic. + +WHAT TO REVIEW: +- Real bugs and correctness issues +- Security risks (injection, secrets, unsafe input) +- Performance problems (N+1, work inside loops, heavy ops) +- Readability & maintainability (naming, duplication, clarity) +- Repository-specific rules (provided separately) take priority over general preferences. + +RULES: +- Only comment on code that appears in the diff. Never hallucinate files or lines. +- Don't nitpick formatting or trivial style. +- Let severity signal how much something matters: + - high → likely bug or security risk, should fix before merge + - medium → worth fixing, potential issue or smell + - low → minor / nit / readability, non-blocking + +CATEGORIES: security | performance | quality | maintainability + +SCORING (start at 100, deduct per issue): +- high → -10, medium → -5, low → -2 + +For each issue: +- "problem": one short, conversational sentence on the issue and why it matters. +- "fix": one short, concrete suggestion. Return ONLY valid JSON in this exact format: { - "summary": "short technical summary", + "summary": "1-2 friendly sentences on the overall change; note what's good and the general direction", "score": number (0-100), "issues": [ { @@ -153,13 +149,13 @@ Return ONLY valid JSON in this exact format: "line": number, "category": "security | performance | quality | maintainability", "severity": "low | medium | high", - "problem": "clear explanation", - "fix": "actionable fix" + "problem": "short, human, consequence-focused note", + "fix": "short, concrete suggestion" } ] } -Do NOT include markdown, explanations, or text outside JSON.`, +Do NOT include markdown, explanations, or any text outside the JSON.`, messages: [ { role: "user", diff --git a/Server/package/ast/parser.ts b/Server/package/ast/parser.ts index d36f786..272bd21 100644 --- a/Server/package/ast/parser.ts +++ b/Server/package/ast/parser.ts @@ -3,19 +3,7 @@ import JavaScript from "tree-sitter-javascript"; import TypeScript from "tree-sitter-typescript"; import Python from "tree-sitter-python"; -export type ASTNodeType = - | "function" - | "class" - | "method" - | "interface" - | "type" - | "import" - | "export" - | "variable" - | "struct" - | "enum" - | "module" - | "file"; +export type ASTNodeType = | "function" | "class" | "method" | "interface" | "type" | "import" | "export" | "variable" | "struct" | "enum" | "module" | "file"; export type ASTCodeChunk = { id: string;