jstar-reviewer 2.0.0

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.
@@ -0,0 +1,295 @@
1
+ import { generateText } from "ai";
2
+ import { createGroq } from "@ai-sdk/groq";
3
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
4
+ import * as path from "path";
5
+ import * as fs from "fs";
6
+ import chalk from "chalk";
7
+ import simpleGit from "simple-git";
8
+ import { Config } from "./config";
9
+ import { Detective } from "./detective";
10
+ import { GeminiEmbedding } from "./gemini-embedding";
11
+ import { MockLLM } from "./mock-llm";
12
+ import { FileFinding, DashboardReport, LLMReviewResponse, EMPTY_REVIEW } from "./types";
13
+ import { renderDashboard, determineStatus, generateRecommendation } from "./dashboard";
14
+ import {
15
+ VectorStoreIndex,
16
+ storageContextFromDefaults,
17
+ MetadataMode,
18
+ serviceContextFromDefaults
19
+ } from "llamaindex";
20
+
21
+ const google = createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_API_KEY });
22
+ const groq = createGroq({ apiKey: process.env.GROQ_API_KEY });
23
+
24
+ const embedModel = new GeminiEmbedding();
25
+ const llm = new MockLLM();
26
+ const serviceContext = serviceContextFromDefaults({ embedModel, llm: llm as any });
27
+
28
+ const STORAGE_DIR = path.join(process.cwd(), ".jstar", "storage");
29
+ const SOURCE_DIR = path.join(process.cwd(), "scripts");
30
+ const OUTPUT_FILE = path.join(process.cwd(), ".jstar", "last-review.md");
31
+ const git = simpleGit();
32
+
33
+ // --- Config ---
34
+ const MODEL_NAME = Config.MODEL_NAME;
35
+ const MAX_TOKENS_PER_REQUEST = 8000;
36
+ const CHARS_PER_TOKEN = 4;
37
+ const DELAY_BETWEEN_CHUNKS_MS = 2000;
38
+
39
+ // --- Helpers ---
40
+ function estimateTokens(text: string): number {
41
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
42
+ }
43
+
44
+ const EXCLUDED_PATTERNS = [
45
+ /pnpm-lock\.yaml/,
46
+ /package-lock\.json/,
47
+ /yarn\.lock/,
48
+ /\.env/,
49
+ /\.json$/,
50
+ /\.txt$/,
51
+ /\.md$/,
52
+ /node_modules/,
53
+ /\.jstar\//,
54
+ ];
55
+
56
+ function shouldSkipFile(fileName: string): boolean {
57
+ return EXCLUDED_PATTERNS.some(pattern => pattern.test(fileName));
58
+ }
59
+
60
+ function chunkDiffByFile(diff: string): string[] {
61
+ return diff.split(/(?=^diff --git)/gm).filter(Boolean);
62
+ }
63
+
64
+ function sleep(ms: number): Promise<void> {
65
+ return new Promise(resolve => setTimeout(resolve, ms));
66
+ }
67
+
68
+ function parseReviewResponse(text: string): LLMReviewResponse {
69
+ try {
70
+ // Try to extract JSON from the response
71
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
72
+ if (jsonMatch) {
73
+ const parsed = JSON.parse(jsonMatch[0]);
74
+
75
+ // Validate structure
76
+ if (
77
+ parsed &&
78
+ typeof parsed === 'object' &&
79
+ Array.isArray(parsed.issues) &&
80
+ ['P0_CRITICAL', 'P1_HIGH', 'P2_MEDIUM', 'LGTM'].includes(parsed.severity)
81
+ ) {
82
+ return {
83
+ severity: parsed.severity,
84
+ issues: parsed.issues
85
+ };
86
+ }
87
+ }
88
+ } catch (e) {
89
+ // Parse failed, try to extract from markdown
90
+ }
91
+
92
+ // Fallback: If "LGTM" in text, it's clean
93
+ if (text.includes('LGTM') || text.includes('āœ…')) {
94
+ return { severity: 'LGTM', issues: [] };
95
+ }
96
+
97
+ // Otherwise, assume there are issues (treat as medium)
98
+ return {
99
+ severity: Config.DEFAULT_SEVERITY,
100
+ issues: [{
101
+ title: 'Review Notes',
102
+ description: text.slice(0, 500),
103
+ fixPrompt: 'Review the file and address the issues mentioned above.'
104
+ }]
105
+ };
106
+ }
107
+
108
+ // --- Main ---
109
+ async function main() {
110
+ console.log(chalk.blue("šŸ•µļø J-Star Reviewer: Analyzing your changes...\n"));
111
+
112
+ // 0. Detective
113
+ console.log(chalk.blue("šŸ”Ž Running Detective Engine..."));
114
+ const detective = new Detective(SOURCE_DIR);
115
+ await detective.scan();
116
+ detective.report();
117
+
118
+ // 1. Get the Diff
119
+ const diff = await git.diff(["--staged"]);
120
+ if (!diff) {
121
+ console.log(chalk.green("\nāœ… No staged changes to review. (Did you 'git add'?)"));
122
+ return;
123
+ }
124
+
125
+ // 2. Load the Brain
126
+ if (!fs.existsSync(STORAGE_DIR)) {
127
+ console.error(chalk.red("āŒ Local Brain not found. Run 'pnpm run index:init' first."));
128
+ return;
129
+ }
130
+ const storageContext = await storageContextFromDefaults({ persistDir: STORAGE_DIR });
131
+ const index = await VectorStoreIndex.init({ storageContext, serviceContext });
132
+
133
+ // 3. Retrieval
134
+ const retriever = index.asRetriever({ similarityTopK: 1 });
135
+ const keywords = (diff.match(/import .* from ['"](.*)['"]/g) || [])
136
+ .map(s => s.replace(/import .* from ['"](.*)['"]/, '$1'))
137
+ .join(" ").slice(0, 300) || "general context";
138
+ const contextNodes = await retriever.retrieve(keywords);
139
+ const relatedContext = contextNodes.map(n => n.node.getContent(MetadataMode.NONE).slice(0, 1500)).join("\n");
140
+
141
+ console.log(chalk.yellow(`\n🧠 Found ${contextNodes.length} context chunk.`));
142
+
143
+ // 4. Chunk the Diff
144
+ const fileChunks = chunkDiffByFile(diff);
145
+ const totalTokens = estimateTokens(diff);
146
+ console.log(chalk.dim(` Total diff: ~${totalTokens} tokens across ${fileChunks.length} files.`));
147
+
148
+ // 5. Structured JSON Prompt
149
+ const systemPrompt = `You are J-Star, a Senior Code Reviewer. Be direct and professional.
150
+
151
+ Analyze the Git Diff and return a JSON response with this EXACT structure:
152
+ {
153
+ "severity": "P0_CRITICAL" | "P1_HIGH" | "P2_MEDIUM" | "LGTM",
154
+ "issues": [
155
+ {
156
+ "title": "Short issue title",
157
+ "description": "Detailed description of the problem",
158
+ "line": 42,
159
+ "fixPrompt": "A specific prompt an AI can use to fix this issue"
160
+ }
161
+ ]
162
+ }
163
+
164
+ SEVERITY GUIDE:
165
+ - P0_CRITICAL: Security vulnerabilities, data leaks, auth bypass, SQL injection
166
+ - P1_HIGH: Missing validation, race conditions, architectural violations
167
+ - P2_MEDIUM: Code quality, missing types, cleanup needed
168
+ - LGTM: No issues found (return empty issues array)
169
+
170
+ IMPORTANT:
171
+ - Return ONLY valid JSON, no markdown or explanation
172
+ - Each issue MUST have a fixPrompt that explains exactly how to fix it
173
+ - If the file is clean, return {"severity": "LGTM", "issues": []}
174
+
175
+ Context: ${relatedContext.slice(0, 800)}`;
176
+
177
+ const findings: FileFinding[] = [];
178
+ let chunkIndex = 0;
179
+ let skippedCount = 0;
180
+
181
+ console.log(chalk.blue("\nāš–ļø Sending to Judge...\n"));
182
+
183
+ for (const chunk of fileChunks) {
184
+ chunkIndex++;
185
+ const fileName = chunk.match(/diff --git a\/(.+?) /)?.[1] || `Chunk ${chunkIndex}`;
186
+
187
+ // Skip excluded files
188
+ if (shouldSkipFile(fileName)) {
189
+ console.log(chalk.dim(` ā­ļø Skipping ${fileName} (excluded)`));
190
+ skippedCount++;
191
+ continue;
192
+ }
193
+
194
+ const chunkTokens = estimateTokens(chunk) + estimateTokens(systemPrompt);
195
+
196
+ // Skip huge files
197
+ if (chunkTokens > MAX_TOKENS_PER_REQUEST) {
198
+ console.log(chalk.yellow(` āš ļø Skipping ${fileName} (too large: ~${chunkTokens} tokens)`));
199
+ findings.push({
200
+ file: fileName,
201
+ severity: Config.DEFAULT_SEVERITY,
202
+ issues: [{
203
+ title: 'File too large for review',
204
+ description: `This file has ~${chunkTokens} tokens which exceeds the limit.`,
205
+ fixPrompt: 'Consider splitting this file into smaller modules.'
206
+ }]
207
+ });
208
+ continue;
209
+ }
210
+
211
+ process.stdout.write(chalk.dim(` šŸ“„ ${fileName}...`));
212
+
213
+ try {
214
+ const { text } = await generateText({
215
+ model: groq(MODEL_NAME),
216
+ system: systemPrompt,
217
+ prompt: `REVIEW THIS DIFF:\n\n${chunk}`,
218
+ temperature: 0.1,
219
+ });
220
+
221
+ const response = parseReviewResponse(text);
222
+ findings.push({
223
+ file: fileName,
224
+ severity: response.severity,
225
+ issues: response.issues
226
+ });
227
+
228
+ const emoji = response.severity === 'LGTM' ? 'āœ…' :
229
+ response.severity === 'P0_CRITICAL' ? 'šŸ›‘' :
230
+ response.severity === 'P1_HIGH' ? 'āš ļø' : 'šŸ“';
231
+ console.log(` ${emoji}`);
232
+
233
+ } catch (error: any) {
234
+ console.log(chalk.red(` āŒ (${error.message.slice(0, 50)})`));
235
+ findings.push({
236
+ file: fileName,
237
+ severity: Config.DEFAULT_SEVERITY,
238
+ issues: [{
239
+ title: 'Review failed',
240
+ description: error.message,
241
+ fixPrompt: 'Retry the review or check manually.'
242
+ }]
243
+ });
244
+ }
245
+
246
+ // Rate limit delay
247
+ if (chunkIndex < fileChunks.length) {
248
+ await sleep(DELAY_BETWEEN_CHUNKS_MS);
249
+ }
250
+ }
251
+
252
+ // 6. Build Dashboard Report
253
+ const metrics = {
254
+ filesScanned: fileChunks.length - skippedCount,
255
+ totalTokens,
256
+ violations: findings.reduce((sum, f) => sum + f.issues.length, 0),
257
+ critical: findings.filter(f => f.severity === 'P0_CRITICAL').length,
258
+ high: findings.filter(f => f.severity === 'P1_HIGH').length,
259
+ medium: findings.filter(f => f.severity === 'P2_MEDIUM').length,
260
+ lgtm: findings.filter(f => f.severity === 'LGTM').length,
261
+ };
262
+
263
+ const report: DashboardReport = {
264
+ date: new Date().toISOString().split('T')[0],
265
+ reviewer: 'Detective Engine & Judge',
266
+ status: determineStatus(metrics),
267
+ metrics,
268
+ findings,
269
+ recommendedAction: generateRecommendation(metrics)
270
+ };
271
+
272
+ // 7. Render and Save Dashboard
273
+ const dashboard = renderDashboard(report);
274
+
275
+ // Ensure .jstar directory exists
276
+ fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true });
277
+ fs.writeFileSync(OUTPUT_FILE, dashboard);
278
+
279
+ console.log("\n" + chalk.bold.green("šŸ“Š DASHBOARD GENERATED"));
280
+ console.log(chalk.dim(` Saved to: ${OUTPUT_FILE}`));
281
+ console.log("\n" + chalk.bold.white("─".repeat(50)));
282
+
283
+ // Print summary to console
284
+ const statusEmoji = report.status === 'APPROVED' ? '🟢' :
285
+ report.status === 'NEEDS_REVIEW' ? '🟔' : 'šŸ”“';
286
+ console.log(`\n${statusEmoji} Status: ${report.status.replace('_', ' ')}`);
287
+ console.log(` šŸ›‘ Critical: ${metrics.critical}`);
288
+ console.log(` āš ļø High: ${metrics.high}`);
289
+ console.log(` šŸ“ Medium: ${metrics.medium}`);
290
+ console.log(` āœ… LGTM: ${metrics.lgtm}`);
291
+ console.log(`\nšŸ’” ${report.recommendedAction}`);
292
+ console.log(chalk.dim(`\nšŸ“„ Full report: ${OUTPUT_FILE}`));
293
+ }
294
+
295
+ main().catch(console.error);
@@ -0,0 +1,61 @@
1
+ /**
2
+ * J-Star Reviewer Types
3
+ * Structured types for dashboard output
4
+ */
5
+
6
+ export type Severity = 'P0_CRITICAL' | 'P1_HIGH' | 'P2_MEDIUM' | 'LGTM';
7
+
8
+ export interface ReviewIssue {
9
+ title: string;
10
+ description: string;
11
+ line?: number;
12
+ fixPrompt: string;
13
+ }
14
+
15
+ export interface FileFinding {
16
+ file: string;
17
+ severity: Severity;
18
+ issues: ReviewIssue[];
19
+ }
20
+
21
+ export interface DashboardReport {
22
+ date: string;
23
+ reviewer: string;
24
+ status: 'CRITICAL_FAILURE' | 'NEEDS_REVIEW' | 'APPROVED';
25
+ metrics: {
26
+ filesScanned: number;
27
+ totalTokens: number;
28
+ violations: number;
29
+ critical: number;
30
+ high: number;
31
+ medium: number;
32
+ lgtm: number;
33
+ };
34
+ findings: FileFinding[];
35
+ recommendedAction: string;
36
+ }
37
+
38
+ /**
39
+ * Schema for LLM response (per-file review)
40
+ */
41
+ export interface LLMReviewResponse {
42
+ severity: Severity;
43
+ issues: {
44
+ title: string;
45
+ description: string;
46
+ line?: number;
47
+ fixPrompt: string;
48
+ }[];
49
+ }
50
+
51
+ /**
52
+ * Default empty response for parse failures
53
+ */
54
+ export const EMPTY_REVIEW: LLMReviewResponse = {
55
+ severity: 'LGTM',
56
+ issues: []
57
+ };
58
+
59
+
60
+
61
+