jstar-reviewer 2.1.3 → 2.2.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.
- package/README.md +2 -2
- package/bin/jstar.js +15 -4
- package/dist/scripts/chat.js +150 -0
- package/dist/scripts/config.js +6 -2
- package/dist/scripts/core/critique.js +137 -0
- package/dist/scripts/core/debate.js +95 -0
- package/dist/scripts/detective.js +5 -4
- package/dist/scripts/gemini-embedding.js +2 -2
- package/dist/scripts/indexer.js +4 -3
- package/dist/scripts/reviewer.js +139 -43
- package/dist/scripts/session.js +273 -0
- package/dist/scripts/ui/interaction.js +43 -0
- package/dist/scripts/utils/logger.js +110 -0
- package/package.json +14 -10
- package/scripts/chat.ts +130 -0
- package/scripts/config.ts +6 -2
- package/scripts/core/critique.ts +162 -0
- package/scripts/core/debate.ts +111 -0
- package/scripts/detective.ts +5 -4
- package/scripts/gemini-embedding.ts +2 -2
- package/scripts/indexer.ts +4 -3
- package/scripts/reviewer.ts +154 -43
- package/scripts/session.ts +312 -0
- package/scripts/types.ts +9 -0
- package/scripts/ui/interaction.ts +38 -0
- package/scripts/utils/logger.ts +118 -0
- package/setup.js +5 -5
- package/scripts/local-embedding.ts +0 -55
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Logger = void 0;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
/**
|
|
9
|
+
* Logger Utility
|
|
10
|
+
* Centralizes all CLI output to support both human-readable (TTY) and machine-readable (JSON) modes.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* Logger.init(); // Auto-detects --json flag
|
|
14
|
+
* Logger.info("Starting..."); // Suppressed in JSON mode
|
|
15
|
+
* Logger.json({ status: "ok" }); // Only outputs in JSON mode
|
|
16
|
+
*/
|
|
17
|
+
let jsonMode = false;
|
|
18
|
+
exports.Logger = {
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the logger.
|
|
21
|
+
* Auto-detects --json or --headless flags from process.argv.
|
|
22
|
+
*/
|
|
23
|
+
init() {
|
|
24
|
+
jsonMode = process.argv.includes('--json') || process.argv.includes('--headless');
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* Check if we are in headless/JSON mode.
|
|
28
|
+
*/
|
|
29
|
+
isHeadless() {
|
|
30
|
+
return jsonMode;
|
|
31
|
+
},
|
|
32
|
+
/**
|
|
33
|
+
* Alias for isHeadless for backwards compatibility.
|
|
34
|
+
*/
|
|
35
|
+
isJsonMode() {
|
|
36
|
+
return jsonMode;
|
|
37
|
+
},
|
|
38
|
+
/**
|
|
39
|
+
* Standard informational message (suppressed in JSON mode).
|
|
40
|
+
*/
|
|
41
|
+
info(message) {
|
|
42
|
+
if (!jsonMode) {
|
|
43
|
+
console.log(message);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* Success message with green styling (suppressed in JSON mode).
|
|
48
|
+
*/
|
|
49
|
+
success(message) {
|
|
50
|
+
if (!jsonMode) {
|
|
51
|
+
console.log(chalk_1.default.green(message));
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Warning message with yellow styling (suppressed in JSON mode).
|
|
56
|
+
*/
|
|
57
|
+
warn(message) {
|
|
58
|
+
if (!jsonMode) {
|
|
59
|
+
console.log(chalk_1.default.yellow(message));
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* Error message - always outputs to stderr.
|
|
64
|
+
*/
|
|
65
|
+
error(message) {
|
|
66
|
+
console.error(chalk_1.default.red(message));
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Dim/faded message for secondary info (suppressed in JSON mode).
|
|
70
|
+
*/
|
|
71
|
+
dim(message) {
|
|
72
|
+
if (!jsonMode) {
|
|
73
|
+
console.log(chalk_1.default.dim(message));
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
/**
|
|
77
|
+
* Write inline (no newline) for progress indicators (suppressed in JSON mode).
|
|
78
|
+
* Alias: progress()
|
|
79
|
+
*/
|
|
80
|
+
inline(message) {
|
|
81
|
+
if (!jsonMode) {
|
|
82
|
+
process.stdout.write(message);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* Progress indicator - writes inline without newline.
|
|
87
|
+
*/
|
|
88
|
+
progress(message) {
|
|
89
|
+
if (!jsonMode) {
|
|
90
|
+
process.stdout.write(message);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
/**
|
|
94
|
+
* Output structured JSON to stdout.
|
|
95
|
+
* Only outputs in JSON mode. For API/AI consumption.
|
|
96
|
+
*/
|
|
97
|
+
json(data) {
|
|
98
|
+
if (jsonMode) {
|
|
99
|
+
console.log(JSON.stringify(data, null, 2));
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* Output a single-line JSON object (for streaming events).
|
|
104
|
+
*/
|
|
105
|
+
jsonLine(data) {
|
|
106
|
+
if (jsonMode) {
|
|
107
|
+
console.log(JSON.stringify(data));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jstar-reviewer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Local-First, Context-Aware AI Code Reviewer - Works with any language",
|
|
5
5
|
"bin": {
|
|
6
6
|
"jstar": "bin/jstar.js"
|
|
7
7
|
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"index:init": "ts-node scripts/indexer.ts --init",
|
|
11
|
+
"index:watch": "ts-node scripts/indexer.ts --watch",
|
|
12
|
+
"review": "ts-node scripts/reviewer.ts",
|
|
13
|
+
"chat": "ts-node scripts/chat.ts",
|
|
14
|
+
"detect": "ts-node scripts/detective.ts",
|
|
15
|
+
"prepare": "husky install"
|
|
16
|
+
},
|
|
8
17
|
"keywords": [
|
|
9
18
|
"code-review",
|
|
10
19
|
"ai",
|
|
@@ -29,13 +38,15 @@
|
|
|
29
38
|
"chalk": "^4.1.2",
|
|
30
39
|
"dotenv": "^16.3.1",
|
|
31
40
|
"llamaindex": "^0.1.0",
|
|
41
|
+
"prompts": "^2.4.2",
|
|
32
42
|
"simple-git": "^3.20.0"
|
|
33
43
|
},
|
|
34
44
|
"devDependencies": {
|
|
35
45
|
"@types/node": "^20.0.0",
|
|
46
|
+
"@types/prompts": "^2.4.9",
|
|
47
|
+
"husky": "^8.0.3",
|
|
36
48
|
"ts-node": "^10.9.1",
|
|
37
|
-
"typescript": "^5.2.2"
|
|
38
|
-
"husky": "^8.0.3"
|
|
49
|
+
"typescript": "^5.2.2"
|
|
39
50
|
},
|
|
40
51
|
"engines": {
|
|
41
52
|
"node": ">=18"
|
|
@@ -54,12 +65,5 @@
|
|
|
54
65
|
"type": "commonjs",
|
|
55
66
|
"bugs": {
|
|
56
67
|
"url": "https://github.com/JStaRFilms/jstar-code-review/issues"
|
|
57
|
-
},
|
|
58
|
-
"scripts": {
|
|
59
|
-
"build": "tsc",
|
|
60
|
-
"index:init": "ts-node scripts/indexer.ts --init",
|
|
61
|
-
"index:watch": "ts-node scripts/indexer.ts --watch",
|
|
62
|
-
"review": "ts-node scripts/reviewer.ts",
|
|
63
|
-
"detect": "ts-node scripts/detective.ts"
|
|
64
68
|
}
|
|
65
69
|
}
|
package/scripts/chat.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { startInteractiveSession, startHeadlessSession } from "./session";
|
|
2
|
+
import {
|
|
3
|
+
VectorStoreIndex,
|
|
4
|
+
storageContextFromDefaults,
|
|
5
|
+
serviceContextFromDefaults
|
|
6
|
+
} from "llamaindex";
|
|
7
|
+
import { GeminiEmbedding } from "./gemini-embedding";
|
|
8
|
+
import { MockLLM } from "./mock-llm";
|
|
9
|
+
import { Logger } from "./utils/logger";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import { SessionState, DashboardReport } from "./types";
|
|
14
|
+
import { renderDashboard, determineStatus, generateRecommendation } from "./dashboard";
|
|
15
|
+
|
|
16
|
+
const STORAGE_DIR = path.join(process.cwd(), ".jstar", "storage");
|
|
17
|
+
const SESSION_FILE = path.join(process.cwd(), ".jstar", "session.json");
|
|
18
|
+
const OUTPUT_FILE = path.join(process.cwd(), ".jstar", "last-review.md");
|
|
19
|
+
|
|
20
|
+
const embedModel = new GeminiEmbedding();
|
|
21
|
+
const llm = new MockLLM();
|
|
22
|
+
const serviceContext = serviceContextFromDefaults({ embedModel, llm: llm as any });
|
|
23
|
+
|
|
24
|
+
async function loadSession(): Promise<SessionState | null> {
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(SESSION_FILE, 'utf-8');
|
|
27
|
+
return JSON.parse(content);
|
|
28
|
+
} catch (e: any) {
|
|
29
|
+
if (e.code === 'ENOENT') {
|
|
30
|
+
return null; // File doesn't exist
|
|
31
|
+
}
|
|
32
|
+
Logger.error(`Failed to load session: ${e.message}`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
// Initialize logger mode
|
|
39
|
+
Logger.init();
|
|
40
|
+
|
|
41
|
+
Logger.info(chalk.bold.magenta("\n💬 J-Star Chat: Resuming Session...\n"));
|
|
42
|
+
|
|
43
|
+
// 1. Load Session
|
|
44
|
+
const session = await loadSession();
|
|
45
|
+
if (!session) {
|
|
46
|
+
Logger.error(chalk.red("❌ No active session found."));
|
|
47
|
+
Logger.info(chalk.yellow("Run 'jstar review' first to analyze the codebase."));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Logger.info(chalk.dim(` 📅 Loaded session from: ${session.date}`));
|
|
52
|
+
Logger.info(chalk.dim(` 🔍 Loaded ${session.findings.reduce((acc, f) => acc + f.issues.length, 0)} issues.`));
|
|
53
|
+
|
|
54
|
+
// 2. Load Brain (Fast)
|
|
55
|
+
if (!fs.existsSync(STORAGE_DIR)) {
|
|
56
|
+
Logger.error(chalk.red("❌ Local Brain not found. Run 'pnpm index:init' first."));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const storageContext = await storageContextFromDefaults({ persistDir: STORAGE_DIR });
|
|
60
|
+
const index = await VectorStoreIndex.init({ storageContext, serviceContext });
|
|
61
|
+
|
|
62
|
+
// 3. Start Chat (Headless or Interactive)
|
|
63
|
+
let updatedFindings;
|
|
64
|
+
let hasUpdates;
|
|
65
|
+
|
|
66
|
+
if (Logger.isHeadless()) {
|
|
67
|
+
// Headless mode: stdin/stdout JSON protocol
|
|
68
|
+
const result = await startHeadlessSession(session.findings, index);
|
|
69
|
+
updatedFindings = result.updatedFindings;
|
|
70
|
+
hasUpdates = result.hasUpdates;
|
|
71
|
+
} else {
|
|
72
|
+
// Normal TUI mode
|
|
73
|
+
const result = await startInteractiveSession(session.findings, index);
|
|
74
|
+
updatedFindings = result.updatedFindings;
|
|
75
|
+
hasUpdates = result.hasUpdates;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 4. Update Session & Report if changed
|
|
79
|
+
if (hasUpdates) {
|
|
80
|
+
Logger.info(chalk.blue("\n🔄 Updating Session & Dashboard..."));
|
|
81
|
+
|
|
82
|
+
// Recalculate metrics based on new findings
|
|
83
|
+
const newMetrics = {
|
|
84
|
+
...session.metrics, // keep files/tokens same
|
|
85
|
+
violations: updatedFindings.reduce((sum, f) => sum + f.issues.length, 0),
|
|
86
|
+
critical: updatedFindings.filter(f => f.severity === 'P0_CRITICAL').length,
|
|
87
|
+
high: updatedFindings.filter(f => f.severity === 'P1_HIGH').length,
|
|
88
|
+
medium: updatedFindings.filter(f => f.severity === 'P2_MEDIUM').length,
|
|
89
|
+
lgtm: updatedFindings.filter(f => f.severity === 'LGTM').length,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Save Session
|
|
93
|
+
const newSession: SessionState = {
|
|
94
|
+
date: new Date().toISOString().split('T')[0],
|
|
95
|
+
findings: updatedFindings,
|
|
96
|
+
metrics: newMetrics
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(newSession, null, 2));
|
|
101
|
+
} catch (err: any) {
|
|
102
|
+
Logger.error(`Failed to save session: ${err.message}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Save Dashboard
|
|
107
|
+
const report: DashboardReport = {
|
|
108
|
+
date: newSession.date,
|
|
109
|
+
reviewer: 'J-Star Chat',
|
|
110
|
+
status: determineStatus(newMetrics),
|
|
111
|
+
metrics: newMetrics,
|
|
112
|
+
findings: updatedFindings,
|
|
113
|
+
recommendedAction: generateRecommendation(newMetrics)
|
|
114
|
+
};
|
|
115
|
+
const dashboard = renderDashboard(report);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
fs.writeFileSync(OUTPUT_FILE, dashboard);
|
|
119
|
+
} catch (err: any) {
|
|
120
|
+
Logger.error(`Failed to save dashboard: ${err.message}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Logger.info(chalk.bold.green("✅ Saved."));
|
|
125
|
+
} else {
|
|
126
|
+
Logger.info(chalk.dim(" No changes made."));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main().catch(console.error);
|
package/scripts/config.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Severity } from "./types";
|
|
|
5
5
|
|
|
6
6
|
// --- Auto-Setup Logic ---
|
|
7
7
|
const REQUIRED_ENV_VARS = {
|
|
8
|
-
'
|
|
8
|
+
'GEMINI_API_KEY': '# Required: Gemini API key (or GOOGLE_API_KEY)\nGEMINI_API_KEY=your_gemini_api_key_here',
|
|
9
9
|
'GROQ_API_KEY': '# Required: Groq API key for LLM reviews\nGROQ_API_KEY=your_groq_api_key_here',
|
|
10
10
|
'REVIEW_MODEL_NAME': '# Optional: Override the default model\n# REVIEW_MODEL_NAME=moonshotai/kimi-k2-instruct-0905'
|
|
11
11
|
};
|
|
@@ -55,9 +55,13 @@ const DEFAULT_MODEL = "moonshotai/kimi-k2-instruct-0905";
|
|
|
55
55
|
|
|
56
56
|
export const Config = {
|
|
57
57
|
MODEL_NAME: process.env.REVIEW_MODEL_NAME || DEFAULT_MODEL,
|
|
58
|
+
CRITIQUE_MODEL_NAME: process.env.CRITIQUE_MODEL_NAME || process.env.REVIEW_MODEL_NAME || DEFAULT_MODEL,
|
|
58
59
|
DEFAULT_SEVERITY: 'P2_MEDIUM' as Severity,
|
|
59
60
|
THRESHOLDS: {
|
|
60
61
|
MEDIUM: 5
|
|
61
|
-
}
|
|
62
|
+
},
|
|
63
|
+
// Smart Review Settings
|
|
64
|
+
CONFIDENCE_THRESHOLD: 3, // Minimum confidence (1-5) to include an issue
|
|
65
|
+
ENABLE_SELF_CRITIQUE: true, // Enable second-pass validation
|
|
62
66
|
};
|
|
63
67
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Critique Module
|
|
3
|
+
* Second-pass validation to filter out false positives
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { generateText } from "ai";
|
|
7
|
+
import { createGroq } from "@ai-sdk/groq";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { Logger } from "../utils/logger";
|
|
10
|
+
import { Config } from "../config";
|
|
11
|
+
import { FileFinding, ReviewIssue } from "../types";
|
|
12
|
+
|
|
13
|
+
const groq = createGroq({ apiKey: process.env.GROQ_API_KEY });
|
|
14
|
+
|
|
15
|
+
interface CritiqueResult {
|
|
16
|
+
issueTitle: string;
|
|
17
|
+
verdict: 'VALID' | 'FALSE_POSITIVE' | 'NEEDS_CONTEXT';
|
|
18
|
+
reason: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface CritiqueResponse {
|
|
22
|
+
results: CritiqueResult[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Runs a self-critique pass on the initial findings.
|
|
27
|
+
* Returns filtered findings with only validated issues.
|
|
28
|
+
*/
|
|
29
|
+
export async function critiqueFindings(
|
|
30
|
+
findings: FileFinding[],
|
|
31
|
+
diff: string
|
|
32
|
+
): Promise<FileFinding[]> {
|
|
33
|
+
// Collect all issues across all files for batch critique
|
|
34
|
+
const allIssues: { file: string; issue: ReviewIssue; index: number }[] = [];
|
|
35
|
+
findings.forEach((f, fIdx) => {
|
|
36
|
+
f.issues.forEach((issue, iIdx) => {
|
|
37
|
+
allIssues.push({ file: f.file, issue, index: fIdx * 1000 + iIdx });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (allIssues.length === 0) {
|
|
42
|
+
return findings;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Logger.info(chalk.blue("\n🔍 Self-Critique Pass: Validating findings...\n"));
|
|
46
|
+
|
|
47
|
+
// Build the critique prompt
|
|
48
|
+
const issueList = allIssues.map((item, idx) =>
|
|
49
|
+
`[${idx}] File: ${item.file}\n Title: ${item.issue.title}\n Description: ${item.issue.description}`
|
|
50
|
+
).join("\n\n");
|
|
51
|
+
|
|
52
|
+
const systemPrompt = `You are a code review validator. Your job is to filter out FALSE POSITIVES.
|
|
53
|
+
|
|
54
|
+
For each issue below, decide:
|
|
55
|
+
- VALID: This is a real problem that should be reported
|
|
56
|
+
- FALSE_POSITIVE: This is NOT a real issue (test mock, intentional pattern, already handled, etc.)
|
|
57
|
+
- NEEDS_CONTEXT: Can't determine without more code context (treat as valid)
|
|
58
|
+
|
|
59
|
+
Return JSON with this structure:
|
|
60
|
+
{
|
|
61
|
+
"results": [
|
|
62
|
+
{ "issueTitle": "...", "verdict": "VALID" | "FALSE_POSITIVE" | "NEEDS_CONTEXT", "reason": "..." }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
IMPORTANT:
|
|
67
|
+
- Be SKEPTICAL. If the code looks intentional, it's probably not a bug.
|
|
68
|
+
- Test files, mocks, and stubs are NOT bugs.
|
|
69
|
+
- "Missing error handling" in utility modules may be intentional.
|
|
70
|
+
- Return ONLY valid JSON.`;
|
|
71
|
+
|
|
72
|
+
const userPrompt = `ORIGINAL DIFF:
|
|
73
|
+
\`\`\`
|
|
74
|
+
${diff.slice(0, 4000)}
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
ISSUES TO VALIDATE:
|
|
78
|
+
${issueList}`;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const { text } = await generateText({
|
|
82
|
+
model: groq(Config.CRITIQUE_MODEL_NAME),
|
|
83
|
+
system: systemPrompt,
|
|
84
|
+
prompt: userPrompt,
|
|
85
|
+
temperature: 0.1,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Parse critique response
|
|
89
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
90
|
+
if (!jsonMatch) {
|
|
91
|
+
Logger.warn(chalk.yellow(" ⚠️ Could not parse critique response, keeping all issues"));
|
|
92
|
+
return findings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let response: CritiqueResponse;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
98
|
+
// Validate structure
|
|
99
|
+
if (!parsed || !Array.isArray(parsed.results)) {
|
|
100
|
+
throw new Error("Invalid response structure");
|
|
101
|
+
}
|
|
102
|
+
response = parsed;
|
|
103
|
+
} catch (parseError) {
|
|
104
|
+
Logger.warn(chalk.yellow(" ⚠️ Invalid JSON from critique, keeping all issues"));
|
|
105
|
+
return findings;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Build a map of verdicts by title
|
|
109
|
+
const verdictMap = new Map<string, CritiqueResult>();
|
|
110
|
+
for (const result of response.results) {
|
|
111
|
+
verdictMap.set(result.issueTitle.toLowerCase(), result);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Filter findings
|
|
115
|
+
const filteredFindings: FileFinding[] = [];
|
|
116
|
+
let removedCount = 0;
|
|
117
|
+
|
|
118
|
+
for (const finding of findings) {
|
|
119
|
+
const validIssues: ReviewIssue[] = [];
|
|
120
|
+
|
|
121
|
+
for (const issue of finding.issues) {
|
|
122
|
+
const verdict = verdictMap.get(issue.title.toLowerCase());
|
|
123
|
+
|
|
124
|
+
if (verdict?.verdict === 'FALSE_POSITIVE') {
|
|
125
|
+
removedCount++;
|
|
126
|
+
Logger.info(chalk.dim(` ❌ Removed: "${issue.title}" (${verdict.reason})`));
|
|
127
|
+
} else {
|
|
128
|
+
validIssues.push(issue);
|
|
129
|
+
if (verdict?.verdict === 'VALID') {
|
|
130
|
+
Logger.info(chalk.green(` ✓ Kept: "${issue.title}"`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (validIssues.length > 0) {
|
|
136
|
+
filteredFindings.push({
|
|
137
|
+
...finding,
|
|
138
|
+
issues: validIssues,
|
|
139
|
+
// Upgrade to LGTM if no issues remain? No, keep severity for record
|
|
140
|
+
});
|
|
141
|
+
} else if (finding.issues.length > 0) {
|
|
142
|
+
// All issues were false positives - mark as LGTM
|
|
143
|
+
filteredFindings.push({
|
|
144
|
+
...finding,
|
|
145
|
+
severity: 'LGTM',
|
|
146
|
+
issues: []
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
filteredFindings.push(finding);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Logger.info(chalk.blue(`\n 📊 Self-Critique: ${removedCount} false positive(s) removed\n`));
|
|
154
|
+
|
|
155
|
+
return filteredFindings;
|
|
156
|
+
|
|
157
|
+
} catch (error: any) {
|
|
158
|
+
Logger.warn(chalk.yellow(` ⚠️ Self-critique failed: ${error.message.slice(0, 100)}`));
|
|
159
|
+
Logger.warn(chalk.yellow(" Keeping all issues as fallback."));
|
|
160
|
+
return findings;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { VectorStoreIndex, MetadataMode } from "llamaindex";
|
|
2
|
+
import { generateText } from "ai";
|
|
3
|
+
import { createGroq } from "@ai-sdk/groq";
|
|
4
|
+
import { Config } from "../config";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
const groq = createGroq({ apiKey: process.env.GROQ_API_KEY });
|
|
8
|
+
|
|
9
|
+
export interface DebateResult {
|
|
10
|
+
text: string;
|
|
11
|
+
severity: 'P0_CRITICAL' | 'P1_HIGH' | 'P2_MEDIUM' | 'LGTM' | 'UNCHANGED';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function debateIssue(
|
|
15
|
+
issueTitle: string,
|
|
16
|
+
issueDescription: string,
|
|
17
|
+
fileName: string,
|
|
18
|
+
userArgument: string,
|
|
19
|
+
index: VectorStoreIndex
|
|
20
|
+
): Promise<DebateResult> {
|
|
21
|
+
|
|
22
|
+
// Validate API key before making any calls
|
|
23
|
+
if (!process.env.GROQ_API_KEY) {
|
|
24
|
+
throw new Error("GROQ_API_KEY is required for debate mode. Please set it in your .env.local file.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(chalk.dim(" 🧠 Thinking... (Consulting the Brain)"));
|
|
28
|
+
|
|
29
|
+
// 1. Extract keywords/context
|
|
30
|
+
const query = `${userArgument} ${issueTitle}`;
|
|
31
|
+
|
|
32
|
+
// 2. Retrieve new context
|
|
33
|
+
const retriever = index.asRetriever({ similarityTopK: 2 });
|
|
34
|
+
const contextNodes = await retriever.retrieve(query);
|
|
35
|
+
const newContext = contextNodes.map(n => n.node.getContent(MetadataMode.NONE)).join("\n\n").slice(0, 2000);
|
|
36
|
+
|
|
37
|
+
if (newContext.length >= 2000) {
|
|
38
|
+
console.log(chalk.yellow(" ⚠️ Context truncated to 2000 chars"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sources = contextNodes.map(n => n.node.metadata?.['file_name']).filter(Boolean).join(', ');
|
|
42
|
+
if (sources) {
|
|
43
|
+
console.log(chalk.dim(` 🔍 Found relevant context from: ${sources}`));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3. Ask the Judge
|
|
47
|
+
const systemPrompt = `You are a Senior Code Reviewer in a debate with a developer.
|
|
48
|
+
|
|
49
|
+
ORIGINAL FINDING: "${issueTitle} - ${issueDescription}" in file ${fileName}.
|
|
50
|
+
USER DEFENSE: "${userArgument}"
|
|
51
|
+
|
|
52
|
+
NEW CONTEXT FOUND IN REPO:
|
|
53
|
+
${newContext}
|
|
54
|
+
|
|
55
|
+
TASK:
|
|
56
|
+
Analyze the USER INPUT.
|
|
57
|
+
|
|
58
|
+
1. **IS IT A QUESTION?** (e.g., "What does this mean?", "Why is this wrong?")
|
|
59
|
+
- If yes, **EXPLAIN** the technical reasoning behind the finding.
|
|
60
|
+
- Reference the specific code/context.
|
|
61
|
+
- Do NOT withdraw the issue (Severity: UNCHANGED).
|
|
62
|
+
- Tone: Educational and helpful.
|
|
63
|
+
|
|
64
|
+
2. **IS IT A DEFENSE/ARGUMENT?** (e.g., "This is handled in utils.ts", "It's a false positive because...")
|
|
65
|
+
- Evaluate if the user is correct based on the NEW CONTEXT.
|
|
66
|
+
- If user is RIGHT: Apologize and withdraw (Severity: LGTM).
|
|
67
|
+
- If user is WRONG: Explain why, citing the context. (Severity: UNCHANGED).
|
|
68
|
+
|
|
69
|
+
RETURN JSON:
|
|
70
|
+
{
|
|
71
|
+
"response": "Conversational response (explanation or verdict).",
|
|
72
|
+
"severity": "P0_CRITICAL" | "P1_HIGH" | "P2_MEDIUM" | "LGTM" | "UNCHANGED"
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const { text } = await generateText({
|
|
78
|
+
model: groq(Config.MODEL_NAME),
|
|
79
|
+
system: systemPrompt,
|
|
80
|
+
prompt: "What is your verdict?",
|
|
81
|
+
temperature: 0.2,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
85
|
+
if (jsonMatch) {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
88
|
+
// Validate expected structure
|
|
89
|
+
if (parsed && typeof parsed.response === 'string' && parsed.severity) {
|
|
90
|
+
return {
|
|
91
|
+
text: parsed.response,
|
|
92
|
+
severity: parsed.severity
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
} catch (parseError) {
|
|
96
|
+
// JSON parse failed, fall through to default response
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
text: text,
|
|
102
|
+
severity: 'UNCHANGED'
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
} catch (error: any) {
|
|
106
|
+
return {
|
|
107
|
+
text: `Failed to debate: ${error.message}`,
|
|
108
|
+
severity: 'UNCHANGED'
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
package/scripts/detective.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
+
import { Logger } from './utils/logger';
|
|
4
5
|
|
|
5
6
|
interface Violation {
|
|
6
7
|
file: string;
|
|
@@ -109,22 +110,22 @@ export class Detective {
|
|
|
109
110
|
|
|
110
111
|
report() {
|
|
111
112
|
if (this.violations.length === 0) {
|
|
112
|
-
|
|
113
|
+
Logger.info(chalk.green("✅ Detective Engine: No violations found."));
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
Logger.info(chalk.red(`🚨 Detective Engine found ${this.violations.length} violations:`));
|
|
117
118
|
// Only show first 10 to avoid wall of text
|
|
118
119
|
const total = this.violations.length;
|
|
119
120
|
const toShow = this.violations.slice(0, 10);
|
|
120
121
|
|
|
121
122
|
toShow.forEach(v => {
|
|
122
123
|
const color = v.severity === 'high' ? chalk.red : chalk.yellow;
|
|
123
|
-
|
|
124
|
+
Logger.info(color(`[${v.code}] ${v.file}:${v.line} - ${v.message}`));
|
|
124
125
|
});
|
|
125
126
|
|
|
126
127
|
if (total > 10) {
|
|
127
|
-
|
|
128
|
+
Logger.dim(`... and ${total - 10} more.`);
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
}
|
|
@@ -5,9 +5,9 @@ export class GeminiEmbedding {
|
|
|
5
5
|
private model: any;
|
|
6
6
|
|
|
7
7
|
constructor() {
|
|
8
|
-
const apiKey = process.env.GOOGLE_API_KEY;
|
|
8
|
+
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
9
9
|
if (!apiKey) {
|
|
10
|
-
throw new Error("
|
|
10
|
+
throw new Error("GEMINI_API_KEY is missing from environment variables.");
|
|
11
11
|
}
|
|
12
12
|
this.genAI = new GoogleGenerativeAI(apiKey);
|
|
13
13
|
// User requested 'text-embedding-004', which has better rate limits
|
package/scripts/indexer.ts
CHANGED
|
@@ -50,8 +50,9 @@ function getSourceDir(): string {
|
|
|
50
50
|
|
|
51
51
|
async function main() {
|
|
52
52
|
// 0. Environment Validation
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
54
|
+
if (!geminiKey) {
|
|
55
|
+
console.error(chalk.red("❌ Missing GEMINI_API_KEY (or GOOGLE_API_KEY)!"));
|
|
55
56
|
console.log(chalk.yellow("\nPlease ensure you have a .env.local file. Check .env.example for a template.\n"));
|
|
56
57
|
process.exit(1);
|
|
57
58
|
}
|
|
@@ -126,7 +127,7 @@ async function main() {
|
|
|
126
127
|
} catch (e: any) {
|
|
127
128
|
console.error(chalk.red("❌ Indexing Failed:"), e.message);
|
|
128
129
|
if (e.message.includes("API") || e.message.includes("key")) {
|
|
129
|
-
console.log(chalk.yellow("👉 Tip: Make sure you have
|
|
130
|
+
console.log(chalk.yellow("👉 Tip: Make sure you have GEMINI_API_KEY in your .env.local file."));
|
|
130
131
|
}
|
|
131
132
|
process.exit(1);
|
|
132
133
|
}
|