Introduces a small graph executor under packages/ai/src/graph and a code-review pipeline (analyze, summarize, compose) wired through new prompt builders. Re-exported from the package index. Co-authored-by: Cursor <cursoragent@cursor.com>
139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
/**
|
|
* Prompts used by the code-review graph in `graph/code-review.ts`.
|
|
*
|
|
* The graph runs a small ensemble of focused steps; each step gets a tightly
|
|
* scoped user prompt while sharing one reviewer system prompt, so the model
|
|
* keeps a consistent voice and rubric across nodes.
|
|
*/
|
|
|
|
export const CODE_REVIEW_SYSTEM_PROMPT = `You are a senior staff engineer performing a code review.
|
|
|
|
Operating principles:
|
|
- Be precise. Cite the file and (when possible) the symbol or hunk you are referring to.
|
|
- Prefer correctness > security > performance > readability > style.
|
|
- Do not invent code that is not in the diff. If something is unclear, say so explicitly.
|
|
- When suggesting a change, show the smallest reasonable patch (a few lines) rather than rewriting whole files.
|
|
- Severity levels: "blocker" (must fix before merge), "major" (should fix), "minor" (nice to fix), "nit" (style/preference).
|
|
- Never fabricate test results, runtime behavior, or external API contracts.`;
|
|
|
|
export type FileChangeInput = {
|
|
path: string;
|
|
status: "added" | "modified" | "deleted" | "renamed";
|
|
oldPath?: string;
|
|
/** Unified diff for this file, or full file contents for added files. */
|
|
patch: string;
|
|
};
|
|
|
|
function fenceForPath(path: string): string {
|
|
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
switch (ext) {
|
|
case "ts":
|
|
case "tsx":
|
|
return "ts";
|
|
case "js":
|
|
case "jsx":
|
|
return "js";
|
|
case "py":
|
|
return "python";
|
|
case "go":
|
|
return "go";
|
|
case "rs":
|
|
return "rust";
|
|
case "json":
|
|
return "json";
|
|
case "md":
|
|
return "markdown";
|
|
default:
|
|
return "diff";
|
|
}
|
|
}
|
|
|
|
export function buildSummarizeChangesPrompt(files: FileChangeInput[]): string {
|
|
const fileList = files
|
|
.map((f) => `- ${f.status.toUpperCase()} ${f.path}${f.oldPath ? ` (was ${f.oldPath})` : ""}`)
|
|
.join("\n");
|
|
|
|
const bodies = files
|
|
.map(
|
|
(f) =>
|
|
`### ${f.path}\n\n\`\`\`${fenceForPath(f.path)}\n${f.patch.trim()}\n\`\`\``,
|
|
)
|
|
.join("\n\n");
|
|
|
|
return `Summarize the intent of this change set in 2-4 sentences. Then list the most affected modules.
|
|
|
|
Files:
|
|
${fileList}
|
|
|
|
Diffs:
|
|
${bodies}
|
|
|
|
Respond with:
|
|
1) "Summary:" — what the change does and why (inferred).
|
|
2) "Affected modules:" — bullet list of high-level areas (e.g., "auth/session", "tasks/api").`;
|
|
}
|
|
|
|
export function buildAnalyzeFilePrompt(file: FileChangeInput, summary: string): string {
|
|
return `Review the following file change against the change-set summary below. Identify concrete issues only — do not list praise or generic advice.
|
|
|
|
Change-set summary:
|
|
${summary}
|
|
|
|
File: ${file.path} (${file.status}${file.oldPath ? `, was ${file.oldPath}` : ""})
|
|
|
|
\`\`\`${fenceForPath(file.path)}
|
|
${file.patch.trim()}
|
|
\`\`\`
|
|
|
|
For each issue, choose exactly one category: "correctness", "security", "performance", "readability", "style", "test-coverage".
|
|
For each issue, choose exactly one severity: "blocker", "major", "minor", "nit".
|
|
If there are no issues, return an empty list.`;
|
|
}
|
|
|
|
export function buildComposeReviewPrompt(input: {
|
|
summary: string;
|
|
affectedModules: string[];
|
|
prioritizedIssues: Array<{
|
|
file: string;
|
|
severity: string;
|
|
category: string;
|
|
title: string;
|
|
detail: string;
|
|
suggestion?: string;
|
|
}>;
|
|
}): string {
|
|
const issueLines = input.prioritizedIssues
|
|
.map(
|
|
(i, idx) =>
|
|
`${idx + 1}. [${i.severity}/${i.category}] ${i.file} — ${i.title}\n ${i.detail}${i.suggestion ? `\n Suggested: ${i.suggestion}` : ""}`,
|
|
)
|
|
.join("\n\n");
|
|
|
|
const modules = input.affectedModules.length
|
|
? input.affectedModules.map((m) => `- ${m}`).join("\n")
|
|
: "- (none identified)";
|
|
|
|
return `Compose the final review as Markdown. Audience: the PR author.
|
|
|
|
Required structure:
|
|
# Code Review
|
|
|
|
## Summary
|
|
${input.summary}
|
|
|
|
## Affected modules
|
|
${modules}
|
|
|
|
## Findings
|
|
(For each finding below, render as a Markdown subsection with severity/category badges.
|
|
Group findings by severity in this order: blocker, major, minor, nit. Skip groups that are empty.)
|
|
|
|
Findings to render:
|
|
${issueLines || "(none)"}
|
|
|
|
## Verdict
|
|
End with one of: "Approve", "Approve with comments", "Request changes", or "Block".
|
|
Pick the strictest verdict justified by the findings (any blocker => "Block"; any major => "Request changes").
|
|
|
|
Do not add any sections beyond those listed.`;
|
|
}
|