specvector 0.0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -12
- package/package.json +28 -7
- package/src/agent/.gitkeep +0 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/loop.ts +221 -0
- package/src/agent/tools/find-symbol.ts +224 -0
- package/src/agent/tools/grep.ts +149 -0
- package/src/agent/tools/index.ts +9 -0
- package/src/agent/tools/list-dir.ts +191 -0
- package/src/agent/tools/outline.ts +259 -0
- package/src/agent/tools/read-file.ts +140 -0
- package/src/agent/types.ts +145 -0
- package/src/config/.gitkeep +0 -0
- package/src/config/index.ts +285 -0
- package/src/context/index.ts +11 -0
- package/src/context/linear.ts +201 -0
- package/src/github/.gitkeep +0 -0
- package/src/github/comment.ts +102 -0
- package/src/github/diff.ts +90 -0
- package/src/index.ts +247 -0
- package/src/llm/factory.ts +146 -0
- package/src/llm/index.ts +50 -0
- package/src/llm/ollama.ts +321 -0
- package/src/llm/openrouter.ts +348 -0
- package/src/llm/provider.ts +133 -0
- package/src/mcp/.gitkeep +0 -0
- package/src/mcp/index.ts +13 -0
- package/src/mcp/mcp-client.ts +382 -0
- package/src/mcp/types.ts +104 -0
- package/src/review/.gitkeep +0 -0
- package/src/review/diff-parser.ts +168 -0
- package/src/review/engine.ts +268 -0
- package/src/review/formatter.ts +168 -0
- package/src/tools/.gitkeep +0 -0
- package/src/types/diff.ts +65 -0
- package/src/types/llm.ts +126 -0
- package/src/types/result.ts +17 -0
- package/src/types/review.ts +111 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Engine - Connects diff parsing with AI agent for code review.
|
|
3
|
+
*
|
|
4
|
+
* This is the core orchestration layer that:
|
|
5
|
+
* 1. Takes a PR diff
|
|
6
|
+
* 2. Creates an agent with codebase tools
|
|
7
|
+
* 3. Asks the LLM to review the changes
|
|
8
|
+
* 4. Parses the response into structured findings
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createProvider } from "../llm";
|
|
12
|
+
import { createAgentLoop } from "../agent/loop";
|
|
13
|
+
import { createReadFileTool } from "../agent/tools/read-file";
|
|
14
|
+
import { createGrepTool } from "../agent/tools/grep";
|
|
15
|
+
import { createListDirTool } from "../agent/tools/list-dir";
|
|
16
|
+
import { createOutlineTool } from "../agent/tools/outline";
|
|
17
|
+
import { createFindSymbolTool } from "../agent/tools/find-symbol";
|
|
18
|
+
import { calculateStats, determineRecommendation } from "../types/review";
|
|
19
|
+
import type { ReviewResult, ReviewFinding, Severity } from "../types/review";
|
|
20
|
+
import type { Result } from "../types/result";
|
|
21
|
+
import { ok, err } from "../types/result";
|
|
22
|
+
import { loadConfig, getStrictnessModifier } from "../config";
|
|
23
|
+
import { getLinearContextForReview } from "../context";
|
|
24
|
+
|
|
25
|
+
/** Review engine configuration */
|
|
26
|
+
export interface ReviewConfig {
|
|
27
|
+
/** Provider to use (openrouter or ollama) */
|
|
28
|
+
provider?: "openrouter" | "ollama";
|
|
29
|
+
/** Model to use */
|
|
30
|
+
model?: string;
|
|
31
|
+
/** Working directory for tools */
|
|
32
|
+
workingDir: string;
|
|
33
|
+
/** Max iterations for agent */
|
|
34
|
+
maxIterations?: number;
|
|
35
|
+
/** Branch name for Linear ticket extraction */
|
|
36
|
+
branchName?: string;
|
|
37
|
+
/** PR title for Linear ticket extraction */
|
|
38
|
+
prTitle?: string;
|
|
39
|
+
/** PR body for Linear ticket extraction */
|
|
40
|
+
prBody?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Error type for review engine */
|
|
44
|
+
export interface ReviewError {
|
|
45
|
+
code: "PROVIDER_ERROR" | "AGENT_ERROR" | "PARSE_ERROR";
|
|
46
|
+
message: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run an AI-powered code review on a diff.
|
|
51
|
+
*/
|
|
52
|
+
export async function runReview(
|
|
53
|
+
diff: string,
|
|
54
|
+
diffSummary: string,
|
|
55
|
+
config: ReviewConfig
|
|
56
|
+
): Promise<Result<ReviewResult, ReviewError>> {
|
|
57
|
+
// Load config from file (falls back to defaults)
|
|
58
|
+
const fileConfig = await loadConfig(config.workingDir);
|
|
59
|
+
|
|
60
|
+
// Merge: explicit config > file config > env vars > defaults
|
|
61
|
+
const providerName = config.provider ||
|
|
62
|
+
fileConfig.provider ||
|
|
63
|
+
(process.env.SPECVECTOR_PROVIDER as "openrouter" | "ollama") ||
|
|
64
|
+
"openrouter";
|
|
65
|
+
|
|
66
|
+
const model = config.model ||
|
|
67
|
+
fileConfig.model ||
|
|
68
|
+
process.env.SPECVECTOR_MODEL ||
|
|
69
|
+
"anthropic/claude-sonnet-4.5";
|
|
70
|
+
|
|
71
|
+
const strictness = fileConfig.strictness || "normal";
|
|
72
|
+
|
|
73
|
+
// Log configuration
|
|
74
|
+
console.log(`🤖 Model: ${model}`);
|
|
75
|
+
console.log(`📏 Strictness: ${strictness}`);
|
|
76
|
+
|
|
77
|
+
const providerResult = createProvider({
|
|
78
|
+
provider: providerName,
|
|
79
|
+
model,
|
|
80
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!providerResult.ok) {
|
|
84
|
+
return err({
|
|
85
|
+
code: "PROVIDER_ERROR",
|
|
86
|
+
message: providerResult.error.message,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create tools
|
|
91
|
+
const tools = [
|
|
92
|
+
createReadFileTool({ workingDir: config.workingDir, maxFileSize: 100 * 1024 }),
|
|
93
|
+
createGrepTool({ workingDir: config.workingDir, maxResults: 30 }),
|
|
94
|
+
createListDirTool({ workingDir: config.workingDir, maxDepth: 2, maxFiles: 50 }),
|
|
95
|
+
createOutlineTool({ workingDir: config.workingDir }),
|
|
96
|
+
createFindSymbolTool({ workingDir: config.workingDir }),
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// Build system prompt with strictness modifier
|
|
100
|
+
const strictnessGuidance = getStrictnessModifier(strictness);
|
|
101
|
+
let systemPrompt = REVIEW_SYSTEM_PROMPT + `\n\n## Strictness Setting: ${strictness.toUpperCase()}\n${strictnessGuidance}`;
|
|
102
|
+
|
|
103
|
+
// Fetch Linear context if available
|
|
104
|
+
const linearResult = await getLinearContextForReview(
|
|
105
|
+
config.branchName,
|
|
106
|
+
config.prTitle,
|
|
107
|
+
config.prBody
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (linearResult.context) {
|
|
111
|
+
systemPrompt = linearResult.context + "\n\n" + systemPrompt;
|
|
112
|
+
// linearResult.ticketId available for future use (e.g., in review output)
|
|
113
|
+
console.log(`📎 Loaded Linear context for ticket: ${linearResult.ticketId}`);
|
|
114
|
+
} else if (linearResult.warning) {
|
|
115
|
+
console.warn(`⚠️ ${linearResult.warning}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create agent
|
|
119
|
+
const agent = createAgentLoop(providerResult.value, tools, {
|
|
120
|
+
maxIterations: config.maxIterations || fileConfig.maxIterations || 15,
|
|
121
|
+
timeoutMs: 3 * 60 * 1000, // 3 minutes
|
|
122
|
+
systemPrompt,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Build the review task
|
|
126
|
+
const task = buildReviewTask(diff, diffSummary);
|
|
127
|
+
|
|
128
|
+
// Run the agent
|
|
129
|
+
const agentResult = await agent.run(task);
|
|
130
|
+
|
|
131
|
+
if (!agentResult.ok) {
|
|
132
|
+
return err({
|
|
133
|
+
code: "AGENT_ERROR",
|
|
134
|
+
message: agentResult.error.message,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Parse the response into structured findings
|
|
139
|
+
const reviewResult = parseReviewResponse(agentResult.value, diffSummary);
|
|
140
|
+
|
|
141
|
+
return ok(reviewResult);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* System prompt for the code review agent.
|
|
146
|
+
*/
|
|
147
|
+
const REVIEW_SYSTEM_PROMPT = `You are a pragmatic code reviewer. Your job is to catch REAL problems, not nitpick.
|
|
148
|
+
|
|
149
|
+
## Tools Available
|
|
150
|
+
- read_file: Read source files to understand context
|
|
151
|
+
- grep: Search for patterns to find related code
|
|
152
|
+
- list_dir: Explore project structure
|
|
153
|
+
- get_outline: Get functions/classes in a file (fast overview)
|
|
154
|
+
- find_symbol: Find where a function or class is defined
|
|
155
|
+
|
|
156
|
+
## What to Look For (in priority order)
|
|
157
|
+
1. **CRITICAL**: Security vulnerabilities, data loss, crashes
|
|
158
|
+
2. **HIGH**: Bugs that WILL break functionality in production
|
|
159
|
+
3. **MEDIUM**: Significant code quality issues (not style nits)
|
|
160
|
+
|
|
161
|
+
## What NOT to Flag
|
|
162
|
+
- Style preferences or "I would do it differently"
|
|
163
|
+
- Theoretical performance issues without evidence
|
|
164
|
+
- Missing edge case tests for working code
|
|
165
|
+
- "Could be refactored" suggestions
|
|
166
|
+
- Code that works but isn't perfect
|
|
167
|
+
|
|
168
|
+
## Key Principle
|
|
169
|
+
Most PRs should have 0-2 findings. If you're finding 5+ issues, you're being too picky.
|
|
170
|
+
Only flag issues you'd actually block a PR for in a real code review.
|
|
171
|
+
|
|
172
|
+
## Response Format
|
|
173
|
+
SUMMARY: [1-2 sentences - is this code ready to merge?]
|
|
174
|
+
|
|
175
|
+
FINDINGS:
|
|
176
|
+
- [CRITICAL|HIGH|MEDIUM] [Category]: [Title]
|
|
177
|
+
[Brief description + how to fix]
|
|
178
|
+
File: [filename]
|
|
179
|
+
|
|
180
|
+
If the code is ready to merge, respond with:
|
|
181
|
+
SUMMARY: [Positive assessment]
|
|
182
|
+
FINDINGS: None
|
|
183
|
+
|
|
184
|
+
Maximum 3 findings. Focus on what matters.`;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Build the review task for the agent.
|
|
188
|
+
*/
|
|
189
|
+
function buildReviewTask(diff: string, diffSummary: string): string {
|
|
190
|
+
return `Please review this pull request.
|
|
191
|
+
|
|
192
|
+
## Changes Overview
|
|
193
|
+
${diffSummary}
|
|
194
|
+
|
|
195
|
+
## Diff
|
|
196
|
+
\`\`\`diff
|
|
197
|
+
${diff.slice(0, 15000)}
|
|
198
|
+
\`\`\`
|
|
199
|
+
${diff.length > 15000 ? "\n(diff truncated, use tools to read full files if needed)" : ""}
|
|
200
|
+
|
|
201
|
+
## Instructions
|
|
202
|
+
1. First, understand what the changes are doing
|
|
203
|
+
2. Use tools to explore related code if needed (find usages, read implementations)
|
|
204
|
+
3. Identify any issues with the changes
|
|
205
|
+
4. Provide your review in the specified format`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse the agent's response into structured findings.
|
|
210
|
+
*/
|
|
211
|
+
function parseReviewResponse(response: string, diffSummary: string): ReviewResult {
|
|
212
|
+
const findings: ReviewFinding[] = [];
|
|
213
|
+
|
|
214
|
+
// Extract summary
|
|
215
|
+
const summaryMatch = response.match(/SUMMARY:\s*(.+?)(?=\n\nFINDINGS:|$)/s);
|
|
216
|
+
const summary = summaryMatch?.[1]?.trim() ?? response.slice(0, 200);
|
|
217
|
+
|
|
218
|
+
// Extract findings
|
|
219
|
+
const findingsSection = response.match(/FINDINGS:\s*([\s\S]*?)$/);
|
|
220
|
+
const findingsText = findingsSection?.[1] ?? "";
|
|
221
|
+
|
|
222
|
+
if (findingsText && !findingsText.toLowerCase().includes("none")) {
|
|
223
|
+
// Parse each finding
|
|
224
|
+
const findingPattern = /\[?(CRITICAL|HIGH|MEDIUM|LOW)\]?\s*\[?([^\]:\n]+)\]?:\s*([^\n]+)\n([\s\S]*?)(?=\n\s*-\s*\[?(?:CRITICAL|HIGH|MEDIUM|LOW)|$)/gi;
|
|
225
|
+
|
|
226
|
+
let match;
|
|
227
|
+
while ((match = findingPattern.exec(findingsText)) !== null) {
|
|
228
|
+
const severity = match[1];
|
|
229
|
+
const category = match[2];
|
|
230
|
+
const title = match[3];
|
|
231
|
+
const body = match[4] ?? "";
|
|
232
|
+
|
|
233
|
+
if (!severity || !category || !title) continue;
|
|
234
|
+
|
|
235
|
+
// Parse suggestion and file from body
|
|
236
|
+
const suggestionMatch = body.match(/Suggestion:\s*(.+?)(?=\n|File:|$)/i);
|
|
237
|
+
const fileMatch = body.match(/File:\s*(.+?)(?=\n|$)/i);
|
|
238
|
+
const description = body
|
|
239
|
+
.replace(/Suggestion:[\s\S]*?(?=\n|File:|$)/i, '')
|
|
240
|
+
.replace(/File:[\s\S]*?(?=\n|$)/i, '')
|
|
241
|
+
.trim();
|
|
242
|
+
|
|
243
|
+
findings.push({
|
|
244
|
+
severity: severity.toUpperCase() as Severity,
|
|
245
|
+
category: category.trim(),
|
|
246
|
+
title: title.trim(),
|
|
247
|
+
description: description || title.trim(),
|
|
248
|
+
suggestion: suggestionMatch?.[1]?.trim(),
|
|
249
|
+
file: fileMatch?.[1]?.trim(),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Calculate stats
|
|
255
|
+
const filesReviewed = (diffSummary.match(/files? changed/i)?.[0] || "")
|
|
256
|
+
.match(/\d+/)?.[0] || "0";
|
|
257
|
+
|
|
258
|
+
const stats = calculateStats(findings);
|
|
259
|
+
const recommendation = determineRecommendation(stats);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
findings,
|
|
263
|
+
summary,
|
|
264
|
+
recommendation,
|
|
265
|
+
stats,
|
|
266
|
+
filesReviewed: parseInt(filesReviewed, 10) || 0,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter for review comments.
|
|
3
|
+
* Converts ReviewResult into markdown for PR comments.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReviewFinding, ReviewResult, Severity } from "../types/review";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Severity emoji indicators.
|
|
10
|
+
*/
|
|
11
|
+
const SEVERITY_EMOJI: Record<Severity, string> = {
|
|
12
|
+
CRITICAL: "🔴",
|
|
13
|
+
HIGH: "🟠",
|
|
14
|
+
MEDIUM: "🟡",
|
|
15
|
+
LOW: "🟢",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Recommendation emoji indicators.
|
|
20
|
+
*/
|
|
21
|
+
const RECOMMENDATION_EMOJI: Record<ReviewResult["recommendation"], string> = {
|
|
22
|
+
APPROVE: "✅",
|
|
23
|
+
REQUEST_CHANGES: "🔴",
|
|
24
|
+
COMMENT: "💬",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format a review result as a GitHub PR comment.
|
|
29
|
+
*/
|
|
30
|
+
export function formatReviewComment(result: ReviewResult): string {
|
|
31
|
+
const lines: string[] = [];
|
|
32
|
+
|
|
33
|
+
// Header
|
|
34
|
+
lines.push("## 🔍 SpecVector Code Review");
|
|
35
|
+
lines.push("");
|
|
36
|
+
|
|
37
|
+
// Recommendation badge
|
|
38
|
+
const recEmoji = RECOMMENDATION_EMOJI[result.recommendation];
|
|
39
|
+
const recText = formatRecommendation(result.recommendation);
|
|
40
|
+
lines.push(`**Recommendation:** ${recEmoji} ${recText}`);
|
|
41
|
+
lines.push("");
|
|
42
|
+
|
|
43
|
+
// Stats summary
|
|
44
|
+
lines.push(`**Files Reviewed:** ${result.filesReviewed}`);
|
|
45
|
+
if (result.stats.total > 0) {
|
|
46
|
+
const statParts: string[] = [];
|
|
47
|
+
if (result.stats.critical > 0) statParts.push(`${result.stats.critical} critical`);
|
|
48
|
+
if (result.stats.high > 0) statParts.push(`${result.stats.high} high`);
|
|
49
|
+
if (result.stats.medium > 0) statParts.push(`${result.stats.medium} medium`);
|
|
50
|
+
if (result.stats.low > 0) statParts.push(`${result.stats.low} low`);
|
|
51
|
+
lines.push(`**Issues Found:** ${statParts.join(", ")}`);
|
|
52
|
+
} else {
|
|
53
|
+
lines.push("**Issues Found:** None! 🎉");
|
|
54
|
+
}
|
|
55
|
+
lines.push("");
|
|
56
|
+
|
|
57
|
+
// Summary section
|
|
58
|
+
lines.push("### Summary");
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push(result.summary);
|
|
61
|
+
lines.push("");
|
|
62
|
+
|
|
63
|
+
// Findings by severity (only include sections with findings)
|
|
64
|
+
if (result.stats.critical > 0) {
|
|
65
|
+
lines.push("---");
|
|
66
|
+
lines.push("");
|
|
67
|
+
lines.push(`### ${SEVERITY_EMOJI.CRITICAL} Critical Issues (${result.stats.critical})`);
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push(...formatFindings(result.findings, "CRITICAL"));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (result.stats.high > 0) {
|
|
73
|
+
lines.push("---");
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push(`### ${SEVERITY_EMOJI.HIGH} High Issues (${result.stats.high})`);
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(...formatFindings(result.findings, "HIGH"));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (result.stats.medium > 0) {
|
|
81
|
+
lines.push("---");
|
|
82
|
+
lines.push("");
|
|
83
|
+
lines.push(`### ${SEVERITY_EMOJI.MEDIUM} Medium Issues (${result.stats.medium})`);
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push(...formatFindings(result.findings, "MEDIUM"));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (result.stats.low > 0) {
|
|
89
|
+
lines.push("---");
|
|
90
|
+
lines.push("");
|
|
91
|
+
lines.push(`### ${SEVERITY_EMOJI.LOW} Low Issues (${result.stats.low})`);
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push(...formatFindings(result.findings, "LOW"));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Footer
|
|
97
|
+
lines.push("---");
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push("*Powered by [SpecVector](https://github.com/Not-Diamond/specvector) — Context-aware AI code review*");
|
|
100
|
+
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Format findings for a specific severity level.
|
|
106
|
+
*/
|
|
107
|
+
function formatFindings(findings: ReviewFinding[], severity: Severity): string[] {
|
|
108
|
+
const lines: string[] = [];
|
|
109
|
+
const filtered = findings.filter((f) => f.severity === severity);
|
|
110
|
+
|
|
111
|
+
for (const finding of filtered) {
|
|
112
|
+
// Title with optional category
|
|
113
|
+
const categoryPrefix = finding.category ? `${finding.category}: ` : "";
|
|
114
|
+
lines.push(`#### ${categoryPrefix}${finding.title}`);
|
|
115
|
+
|
|
116
|
+
// File reference
|
|
117
|
+
if (finding.file) {
|
|
118
|
+
const lineRef = finding.line ? `:${finding.line}` : "";
|
|
119
|
+
lines.push(`\`${finding.file}${lineRef}\``);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push(finding.description);
|
|
124
|
+
|
|
125
|
+
// Suggestion
|
|
126
|
+
if (finding.suggestion) {
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push(`**Suggestion:** ${finding.suggestion}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
lines.push("");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Format recommendation as human-readable text.
|
|
139
|
+
*/
|
|
140
|
+
function formatRecommendation(rec: ReviewResult["recommendation"]): string {
|
|
141
|
+
switch (rec) {
|
|
142
|
+
case "APPROVE":
|
|
143
|
+
return "Approve";
|
|
144
|
+
case "REQUEST_CHANGES":
|
|
145
|
+
return "Request Changes";
|
|
146
|
+
case "COMMENT":
|
|
147
|
+
return "Comment";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format a compact summary for CLI output.
|
|
153
|
+
*/
|
|
154
|
+
export function formatReviewSummary(result: ReviewResult): string {
|
|
155
|
+
const recEmoji = RECOMMENDATION_EMOJI[result.recommendation];
|
|
156
|
+
const recText = formatRecommendation(result.recommendation);
|
|
157
|
+
|
|
158
|
+
const lines = [
|
|
159
|
+
`${recEmoji} ${recText}`,
|
|
160
|
+
"",
|
|
161
|
+
`Files Reviewed: ${result.filesReviewed}`,
|
|
162
|
+
`Issues: ${result.stats.critical} critical, ${result.stats.high} high, ${result.stats.medium} medium, ${result.stats.low} low`,
|
|
163
|
+
"",
|
|
164
|
+
result.summary,
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff parsing types for SpecVector code review.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A single hunk within a diff file, representing a contiguous change.
|
|
7
|
+
*/
|
|
8
|
+
export interface DiffHunk {
|
|
9
|
+
/** Starting line in the old file */
|
|
10
|
+
oldStart: number;
|
|
11
|
+
/** Number of lines in the old file */
|
|
12
|
+
oldLines: number;
|
|
13
|
+
/** Starting line in the new file */
|
|
14
|
+
newStart: number;
|
|
15
|
+
/** Number of lines in the new file */
|
|
16
|
+
newLines: number;
|
|
17
|
+
/** The raw content of the hunk including +/- prefixes */
|
|
18
|
+
content: string;
|
|
19
|
+
/** Individual lines with their change type */
|
|
20
|
+
changes: DiffChange[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A single line change within a hunk.
|
|
25
|
+
*/
|
|
26
|
+
export interface DiffChange {
|
|
27
|
+
type: "add" | "delete" | "normal";
|
|
28
|
+
content: string;
|
|
29
|
+
oldLineNumber?: number;
|
|
30
|
+
newLineNumber?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A single file within a diff.
|
|
35
|
+
*/
|
|
36
|
+
export interface DiffFile {
|
|
37
|
+
/** Original file path (null if new file) */
|
|
38
|
+
oldPath: string | null;
|
|
39
|
+
/** New file path (null if deleted file) */
|
|
40
|
+
newPath: string | null;
|
|
41
|
+
/** Change status */
|
|
42
|
+
status: "added" | "deleted" | "modified" | "renamed";
|
|
43
|
+
/** Whether this is a binary file */
|
|
44
|
+
binary: boolean;
|
|
45
|
+
/** Hunks containing the actual changes */
|
|
46
|
+
hunks: DiffHunk[];
|
|
47
|
+
/** Number of added lines */
|
|
48
|
+
additions: number;
|
|
49
|
+
/** Number of deleted lines */
|
|
50
|
+
deletions: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Complete parsed diff from a PR.
|
|
55
|
+
*/
|
|
56
|
+
export interface ParsedDiff {
|
|
57
|
+
/** All files in the diff */
|
|
58
|
+
files: DiffFile[];
|
|
59
|
+
/** Total lines added across all files */
|
|
60
|
+
totalAdditions: number;
|
|
61
|
+
/** Total lines deleted across all files */
|
|
62
|
+
totalDeletions: number;
|
|
63
|
+
/** Number of files changed */
|
|
64
|
+
filesChanged: number;
|
|
65
|
+
}
|
package/src/types/llm.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM types for SpecVector provider abstraction.
|
|
3
|
+
* Follows OpenAI/Anthropic message format conventions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Message roles in a conversation.
|
|
8
|
+
*/
|
|
9
|
+
export type MessageRole = "system" | "user" | "assistant" | "tool";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A single message in the conversation.
|
|
13
|
+
*/
|
|
14
|
+
export interface Message {
|
|
15
|
+
/** Role of the message sender */
|
|
16
|
+
role: MessageRole;
|
|
17
|
+
/** Content of the message (null for tool-calling assistant messages) */
|
|
18
|
+
content: string | null;
|
|
19
|
+
/** Tool calls made by the assistant */
|
|
20
|
+
tool_calls?: ToolCall[];
|
|
21
|
+
/** ID of the tool call this message responds to (for tool role) */
|
|
22
|
+
tool_call_id?: string;
|
|
23
|
+
/** Name of the tool that generated this message (for tool role) */
|
|
24
|
+
name?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A tool that can be called by the LLM.
|
|
29
|
+
*/
|
|
30
|
+
export interface Tool {
|
|
31
|
+
/** Unique name of the tool */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Human-readable description for the LLM */
|
|
34
|
+
description: string;
|
|
35
|
+
/** JSON Schema defining the tool's parameters */
|
|
36
|
+
parameters: JSONSchema;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* JSON Schema type (simplified for tool parameters).
|
|
41
|
+
*/
|
|
42
|
+
export interface JSONSchema {
|
|
43
|
+
type: "object";
|
|
44
|
+
properties: Record<string, JSONSchemaProperty>;
|
|
45
|
+
required?: string[];
|
|
46
|
+
additionalProperties?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* JSON Schema property definition.
|
|
51
|
+
*/
|
|
52
|
+
export interface JSONSchemaProperty {
|
|
53
|
+
type: "string" | "number" | "boolean" | "array" | "object";
|
|
54
|
+
description?: string;
|
|
55
|
+
enum?: string[];
|
|
56
|
+
items?: JSONSchemaProperty;
|
|
57
|
+
properties?: Record<string, JSONSchemaProperty>;
|
|
58
|
+
required?: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A tool call made by the assistant.
|
|
63
|
+
*/
|
|
64
|
+
export interface ToolCall {
|
|
65
|
+
/** Unique identifier for this tool call */
|
|
66
|
+
id: string;
|
|
67
|
+
/** Name of the tool to call */
|
|
68
|
+
name: string;
|
|
69
|
+
/** JSON-encoded arguments for the tool */
|
|
70
|
+
arguments: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Token usage statistics.
|
|
75
|
+
*/
|
|
76
|
+
export interface TokenUsage {
|
|
77
|
+
prompt_tokens: number;
|
|
78
|
+
completion_tokens: number;
|
|
79
|
+
total_tokens: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Response from a chat completion request.
|
|
84
|
+
*/
|
|
85
|
+
export interface ChatResponse {
|
|
86
|
+
/** Text content of the response (null if only tool calls) */
|
|
87
|
+
content: string | null;
|
|
88
|
+
/** Tool calls made by the assistant */
|
|
89
|
+
tool_calls?: ToolCall[];
|
|
90
|
+
/** Token usage statistics */
|
|
91
|
+
usage: TokenUsage;
|
|
92
|
+
/** Model that generated the response */
|
|
93
|
+
model: string;
|
|
94
|
+
/** Reason the response ended */
|
|
95
|
+
finish_reason: "stop" | "tool_calls" | "length" | "content_filter";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Options for a chat completion request.
|
|
100
|
+
*/
|
|
101
|
+
export interface ChatOptions {
|
|
102
|
+
/** Tools available for the model to call */
|
|
103
|
+
tools?: Tool[];
|
|
104
|
+
/** Sampling temperature (0-2, default 1) */
|
|
105
|
+
temperature?: number;
|
|
106
|
+
/** Maximum tokens to generate */
|
|
107
|
+
max_tokens?: number;
|
|
108
|
+
/** Stop sequences to end generation */
|
|
109
|
+
stop_sequences?: string[];
|
|
110
|
+
/** Force tool use */
|
|
111
|
+
tool_choice?: "auto" | "none" | { name: string };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Configuration for initializing a provider.
|
|
116
|
+
*/
|
|
117
|
+
export interface ProviderConfig {
|
|
118
|
+
/** Provider type */
|
|
119
|
+
provider: "openrouter" | "ollama";
|
|
120
|
+
/** Model identifier */
|
|
121
|
+
model: string;
|
|
122
|
+
/** API key (for OpenRouter) */
|
|
123
|
+
apiKey?: string;
|
|
124
|
+
/** Base URL (for Ollama, default http://localhost:11434) */
|
|
125
|
+
baseUrl?: string;
|
|
126
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result type for type-safe error handling.
|
|
3
|
+
* Use instead of throwing exceptions for expected errors.
|
|
4
|
+
*/
|
|
5
|
+
export type Result<T, E = Error> =
|
|
6
|
+
| { ok: true; value: T }
|
|
7
|
+
| { ok: false; error: E };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a success result
|
|
11
|
+
*/
|
|
12
|
+
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create an error result
|
|
16
|
+
*/
|
|
17
|
+
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
|