Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 41 additions & 34 deletions Server/app/worker-service/services/review.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,21 @@ export type PRReviewJobData = {
userId?: string;
};

const SEVERITY_ORDER: Record<string, number> = { high: 0, medium: 1, low: 2 };
const SEVERITY_LABEL: Record<string, string> = {
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`;
Expand All @@ -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;
}
Expand All @@ -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<typeof createTokenAccumulator>,
): Promise<string | null> {
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);
Expand Down Expand Up @@ -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({
Expand Down
48 changes: 44 additions & 4 deletions Server/package/ai/gif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mood, string> = {
celebrate: "celebration confetti",
approve: "thumbs up approve",
minor: "almost there",
"needs-work": "needs more work",
};

const MOOD_HINT: Record<Mood, string> = {
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<string> {
const fallback = "code review";
const mood = resolveMood(context);
const fallback = MOOD_FALLBACK[mood];
const cleanSummary = summary?.trim();

if (!cleanSummary) return fallback;
Expand All @@ -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",
Expand Down
82 changes: 39 additions & 43 deletions Server/package/ai/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,64 +102,60 @@ 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": [
{
"file": "file path",
"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",
Expand Down
14 changes: 1 addition & 13 deletions Server/package/ast/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading