jstar-reviewer 2.0.3 → 2.1.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 +7 -4
- package/bin/jstar.js +63 -26
- package/dist/scripts/config.js +91 -0
- package/dist/scripts/dashboard.js +217 -0
- package/dist/scripts/detective.js +150 -0
- package/dist/scripts/gemini-embedding.js +95 -0
- package/dist/scripts/indexer.js +123 -0
- package/dist/scripts/local-embedding.js +49 -0
- package/dist/scripts/mock-llm.js +22 -0
- package/dist/scripts/reviewer.js +296 -0
- package/dist/scripts/types.js +14 -0
- package/package.json +9 -8
- package/scripts/config.ts +42 -2
- package/scripts/indexer.ts +9 -11
- package/scripts/reviewer.ts +12 -1
- package/setup.js +1 -1
package/README.md
CHANGED
|
@@ -37,10 +37,13 @@ curl -fsSL https://raw.githubusercontent.com/JStaRFilms/jstar-code-review/v2.0.0
|
|
|
37
37
|
|
|
38
38
|
### After Install:
|
|
39
39
|
|
|
40
|
-
1.
|
|
41
|
-
2.
|
|
42
|
-
3. Run `jstar init` to build the brain
|
|
43
|
-
4.
|
|
40
|
+
1. **Check Config**: The tool now **auto-creates** `.env.example` and `.jstar/` when you run it.
|
|
41
|
+
2. **Add Keys**: Copy `.env.example` → `.env.local` and add your `GOOGLE_API_KEY` and `GROQ_API_KEY`.
|
|
42
|
+
3. **Index**: Run `jstar init` (or `pnpm run index:init`) to build the brain.
|
|
43
|
+
4. **Review**: Stage changes (`git add`) and run `jstar review` (or `pnpm run review`).
|
|
44
|
+
|
|
45
|
+
For a detailed walkthrough, see **[ONBOARDING.md](./ONBOARDING.md)**.
|
|
46
|
+
|
|
44
47
|
|
|
45
48
|
---
|
|
46
49
|
|
package/bin/jstar.js
CHANGED
|
@@ -70,27 +70,51 @@ function commandExists(cmd) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function runScript(scriptName) {
|
|
73
|
+
// Look for transpiled files in dist/
|
|
74
|
+
// .ts becomes .js in dist
|
|
75
|
+
const jsName = scriptName.replace('.ts', '.js');
|
|
76
|
+
const distDir = path.join(__dirname, '..', 'dist', 'scripts');
|
|
77
|
+
const jsPath = path.join(distDir, jsName);
|
|
78
|
+
|
|
79
|
+
// Fallback for local development if dist doesn't exist
|
|
73
80
|
const scriptsDir = path.join(__dirname, '..', 'scripts');
|
|
74
|
-
const
|
|
81
|
+
const tsPath = path.join(scriptsDir, scriptName);
|
|
82
|
+
|
|
83
|
+
const isWin = process.platform === 'win32';
|
|
84
|
+
|
|
85
|
+
if (fs.existsSync(jsPath)) {
|
|
86
|
+
log(`${COLORS.dim}Running ${jsName}...${COLORS.reset}`);
|
|
87
|
+
const child = spawn('node', [jsPath, ...process.argv.slice(3)], {
|
|
88
|
+
cwd: process.cwd(),
|
|
89
|
+
stdio: 'inherit',
|
|
90
|
+
shell: isWin,
|
|
91
|
+
env: {
|
|
92
|
+
...process.env,
|
|
93
|
+
JSTAR_CWD: process.cwd()
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
child.on('close', (code) => process.exit(code || 0));
|
|
98
|
+
child.on('error', (err) => {
|
|
99
|
+
log(`${COLORS.red}Error running script: ${err.message}${COLORS.reset}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
75
104
|
|
|
76
|
-
if (!fs.existsSync(
|
|
105
|
+
if (!fs.existsSync(tsPath)) {
|
|
77
106
|
log(`${COLORS.red}Error: Script not found: ${scriptPath}${COLORS.reset}`);
|
|
78
107
|
process.exit(1);
|
|
79
108
|
}
|
|
80
109
|
|
|
81
|
-
//
|
|
110
|
+
// Fallback to ts-node if dist is not built (mostly for local development)
|
|
82
111
|
const hasPnpm = commandExists('pnpm');
|
|
83
112
|
const runner = hasPnpm ? 'pnpm' : 'npx';
|
|
84
113
|
const runnerArgs = hasPnpm ? ['dlx', 'ts-node'] : ['ts-node'];
|
|
85
114
|
|
|
86
|
-
log(`${COLORS.dim}Using ${runner} to run ${scriptName}...${COLORS.reset}`);
|
|
115
|
+
log(`${COLORS.dim}Using ${runner} (fallback) to run ${scriptName}...${COLORS.reset}`);
|
|
87
116
|
|
|
88
|
-
|
|
89
|
-
// spawn without shell: true often fails with EINVAL or ENOENT on Windows.
|
|
90
|
-
// We use shell: true only on Windows for reliability.
|
|
91
|
-
const isWin = process.platform === 'win32';
|
|
92
|
-
|
|
93
|
-
const child = spawn(runner, [...runnerArgs, scriptPath, ...process.argv.slice(3)], {
|
|
117
|
+
const child = spawn(runner, [...runnerArgs, tsPath, ...process.argv.slice(3)], {
|
|
94
118
|
cwd: process.cwd(),
|
|
95
119
|
stdio: 'inherit',
|
|
96
120
|
shell: isWin,
|
|
@@ -110,35 +134,48 @@ function runScript(scriptName) {
|
|
|
110
134
|
});
|
|
111
135
|
}
|
|
112
136
|
|
|
137
|
+
const REQUIRED_ENV_VARS = {
|
|
138
|
+
'GOOGLE_API_KEY': '# Required: Google API key for Gemini embeddings\nGOOGLE_API_KEY=your_google_api_key_here',
|
|
139
|
+
'GROQ_API_KEY': '# Required: Groq API key for LLM reviews\nGROQ_API_KEY=your_groq_api_key_here',
|
|
140
|
+
'REVIEW_MODEL_NAME': '# Optional: Override the default model\n# REVIEW_MODEL_NAME=moonshotai/kimi-k2-instruct-0905'
|
|
141
|
+
};
|
|
142
|
+
|
|
113
143
|
function createSetupFiles() {
|
|
114
144
|
const cwd = process.cwd();
|
|
115
145
|
|
|
116
|
-
// Create .jstar directory
|
|
146
|
+
// 1. Create .jstar directory
|
|
117
147
|
const jstarDir = path.join(cwd, '.jstar');
|
|
118
148
|
if (!fs.existsSync(jstarDir)) {
|
|
119
149
|
fs.mkdirSync(jstarDir, { recursive: true });
|
|
120
150
|
log(`${COLORS.green}✓${COLORS.reset} Created .jstar/`);
|
|
121
151
|
}
|
|
122
152
|
|
|
123
|
-
// Create .env.example
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
// 2. Create/Update .env.example
|
|
154
|
+
const envExamplePath = path.join(cwd, '.env.example');
|
|
155
|
+
if (fs.existsSync(envExamplePath)) {
|
|
156
|
+
let content = fs.readFileSync(envExamplePath, 'utf-8');
|
|
157
|
+
let addedKeys = [];
|
|
158
|
+
|
|
159
|
+
for (const [key, template] of Object.entries(REQUIRED_ENV_VARS)) {
|
|
160
|
+
if (!content.includes(key)) {
|
|
161
|
+
content += '\n' + template + '\n';
|
|
162
|
+
addedKeys.push(key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
133
165
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
166
|
+
if (addedKeys.length > 0) {
|
|
167
|
+
fs.writeFileSync(envExamplePath, content);
|
|
168
|
+
log(`${COLORS.green}✓${COLORS.reset} Updated .env.example with missing keys: ${addedKeys.join(', ')}`);
|
|
169
|
+
} else {
|
|
170
|
+
log(`${COLORS.dim} .env.example already exists and is up to date${COLORS.reset}`);
|
|
171
|
+
}
|
|
138
172
|
} else {
|
|
139
|
-
|
|
173
|
+
const initialEnv = "# J-Star Code Reviewer\n" + Object.values(REQUIRED_ENV_VARS).join("\n") + "\n";
|
|
174
|
+
fs.writeFileSync(envExamplePath, initialEnv);
|
|
175
|
+
log(`${COLORS.green}✓${COLORS.reset} Created .env.example`);
|
|
140
176
|
}
|
|
141
177
|
|
|
178
|
+
|
|
142
179
|
// Update .gitignore
|
|
143
180
|
const gitignorePath = path.join(cwd, '.gitignore');
|
|
144
181
|
const gitignoreAdditions = `
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.Config = void 0;
|
|
40
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
// --- Auto-Setup Logic ---
|
|
44
|
+
const REQUIRED_ENV_VARS = {
|
|
45
|
+
'GOOGLE_API_KEY': '# Required: Google API key for Gemini embeddings\nGOOGLE_API_KEY=your_google_api_key_here',
|
|
46
|
+
'GROQ_API_KEY': '# Required: Groq API key for LLM reviews\nGROQ_API_KEY=your_groq_api_key_here',
|
|
47
|
+
'REVIEW_MODEL_NAME': '# Optional: Override the default model\n# REVIEW_MODEL_NAME=moonshotai/kimi-k2-instruct-0905'
|
|
48
|
+
};
|
|
49
|
+
function ensureSetup() {
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
const jstarDir = path.join(cwd, ".jstar");
|
|
52
|
+
const envExamplePath = path.join(cwd, ".env.example");
|
|
53
|
+
// 1. Ensure .jstar exists
|
|
54
|
+
if (!fs.existsSync(jstarDir)) {
|
|
55
|
+
fs.mkdirSync(jstarDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
// 2. Ensure .env.example is up to date
|
|
58
|
+
if (fs.existsSync(envExamplePath)) {
|
|
59
|
+
let content = fs.readFileSync(envExamplePath, 'utf-8');
|
|
60
|
+
let missing = false;
|
|
61
|
+
for (const [key, template] of Object.entries(REQUIRED_ENV_VARS)) {
|
|
62
|
+
if (!content.includes(key)) {
|
|
63
|
+
content += `\n${template}\n`;
|
|
64
|
+
missing = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (missing) {
|
|
68
|
+
fs.writeFileSync(envExamplePath, content);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const initialEnv = "# J-Star Code Reviewer\n" + Object.values(REQUIRED_ENV_VARS).join("\n") + "\n";
|
|
73
|
+
fs.writeFileSync(envExamplePath, initialEnv);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Run setup check
|
|
77
|
+
ensureSetup();
|
|
78
|
+
// Load .env.local first, then .env
|
|
79
|
+
dotenv_1.default.config({ path: ".env.local" });
|
|
80
|
+
dotenv_1.default.config();
|
|
81
|
+
/**
|
|
82
|
+
* Default fallback values.
|
|
83
|
+
*/
|
|
84
|
+
const DEFAULT_MODEL = "moonshotai/kimi-k2-instruct-0905";
|
|
85
|
+
exports.Config = {
|
|
86
|
+
MODEL_NAME: process.env.REVIEW_MODEL_NAME || DEFAULT_MODEL,
|
|
87
|
+
DEFAULT_SEVERITY: 'P2_MEDIUM',
|
|
88
|
+
THRESHOLDS: {
|
|
89
|
+
MEDIUM: 5
|
|
90
|
+
}
|
|
91
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* J-Star Dashboard Renderer
|
|
4
|
+
* Generates professional markdown dashboard from review findings
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.renderDashboard = renderDashboard;
|
|
8
|
+
exports.determineStatus = determineStatus;
|
|
9
|
+
exports.generateRecommendation = generateRecommendation;
|
|
10
|
+
const config_1 = require("./config");
|
|
11
|
+
const SEVERITY_EMOJI = {
|
|
12
|
+
'P0_CRITICAL': '🛑',
|
|
13
|
+
'P1_HIGH': '⚠️',
|
|
14
|
+
'P2_MEDIUM': '📝',
|
|
15
|
+
'LGTM': '✅'
|
|
16
|
+
};
|
|
17
|
+
const SEVERITY_LABEL = {
|
|
18
|
+
'P0_CRITICAL': 'CRITICAL',
|
|
19
|
+
'P1_HIGH': 'HIGH',
|
|
20
|
+
'P2_MEDIUM': 'MEDIUM',
|
|
21
|
+
'LGTM': 'PASSED'
|
|
22
|
+
};
|
|
23
|
+
function getStatusEmoji(status) {
|
|
24
|
+
switch (status) {
|
|
25
|
+
case 'CRITICAL_FAILURE': return '🔴';
|
|
26
|
+
case 'NEEDS_REVIEW': return '🟡';
|
|
27
|
+
case 'APPROVED': return '🟢';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function formatDate() {
|
|
31
|
+
return new Date().toISOString().split('T')[0];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Renders a single issue row for the markdown table.
|
|
35
|
+
* Includes fallback handling for unexpected severity values to ensure
|
|
36
|
+
* the dashboard renders gracefully even if parsing produced invalid data.
|
|
37
|
+
*/
|
|
38
|
+
function renderIssueRow(file, issue, severity) {
|
|
39
|
+
// Fallback to '❓' and raw severity if the value is not in our known maps
|
|
40
|
+
const emoji = SEVERITY_EMOJI[severity] ?? '❓';
|
|
41
|
+
const label = SEVERITY_LABEL[severity] ?? severity;
|
|
42
|
+
return `| \`${file}\` | ${emoji} **${label}** | ${issue.title} |`;
|
|
43
|
+
}
|
|
44
|
+
function renderFixPrompt(issue) {
|
|
45
|
+
if (!issue.fixPrompt)
|
|
46
|
+
return '';
|
|
47
|
+
return `
|
|
48
|
+
<details>
|
|
49
|
+
<summary>🤖 <strong>Fix Prompt:</strong> ${issue.title}</summary>
|
|
50
|
+
|
|
51
|
+
\`\`\`
|
|
52
|
+
${issue.fixPrompt}
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
</details>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Validates that the report object has the required structure.
|
|
60
|
+
* This guards against runtime errors from malformed LLM responses or corrupted data.
|
|
61
|
+
*/
|
|
62
|
+
function validateReport(report) {
|
|
63
|
+
if (!report || typeof report !== 'object')
|
|
64
|
+
return false;
|
|
65
|
+
const r = report;
|
|
66
|
+
// Check required fields exist
|
|
67
|
+
if (typeof r.status !== 'string')
|
|
68
|
+
return false;
|
|
69
|
+
if (typeof r.recommendedAction !== 'string')
|
|
70
|
+
return false;
|
|
71
|
+
if (!Array.isArray(r.findings))
|
|
72
|
+
return false;
|
|
73
|
+
if (!r.metrics || typeof r.metrics !== 'object')
|
|
74
|
+
return false;
|
|
75
|
+
// Validate metrics structure
|
|
76
|
+
const m = r.metrics;
|
|
77
|
+
const requiredMetrics = ['filesScanned', 'totalTokens', 'violations', 'critical', 'high', 'medium', 'lgtm'];
|
|
78
|
+
for (const key of requiredMetrics) {
|
|
79
|
+
if (typeof m[key] !== 'number')
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
function renderDashboard(report) {
|
|
85
|
+
// Runtime validation to catch malformed reports early
|
|
86
|
+
if (!validateReport(report)) {
|
|
87
|
+
throw new Error('Invalid DashboardReport: missing or malformed required fields');
|
|
88
|
+
}
|
|
89
|
+
const { metrics, findings, status, recommendedAction } = report;
|
|
90
|
+
// Group findings by severity
|
|
91
|
+
const critical = findings.filter(f => f.severity === 'P0_CRITICAL');
|
|
92
|
+
const high = findings.filter(f => f.severity === 'P1_HIGH');
|
|
93
|
+
const medium = findings.filter(f => f.severity === 'P2_MEDIUM');
|
|
94
|
+
const lgtm = findings.filter(f => f.severity === 'LGTM');
|
|
95
|
+
let md = `# 📊 J-STAR CODE REVIEW DASHBOARD
|
|
96
|
+
|
|
97
|
+
**Date:** \`${formatDate()}\` | **Reviewer:** \`Detective Engine & Judge\` | **Status:** ${getStatusEmoji(status)} **${status.replace('_', ' ')}**
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 1. 📈 EXECUTIVE SUMMARY
|
|
102
|
+
|
|
103
|
+
| Metric | Value | Status |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| **Files Scanned** | **${metrics.filesScanned}** | 🔎 Complete |
|
|
106
|
+
| **Total Tokens** | **~${metrics.totalTokens.toLocaleString()}** | ⚖️ Processed |
|
|
107
|
+
| **Total Violations** | **${metrics.violations}** | ${metrics.violations > 0 ? '🚨 Action Required' : '✅ Clean'} |
|
|
108
|
+
| **Critical (P0)** | **${metrics.critical}** | ${metrics.critical > 0 ? '🛑 **BLOCKER**' : '✅ None'} |
|
|
109
|
+
| **High (P1)** | **${metrics.high}** | ${metrics.high > 0 ? '⚠️ Needs Fix' : '✅ None'} |
|
|
110
|
+
| **Medium (P2)** | **${metrics.medium}** | ${metrics.medium > 0 ? '📝 Review' : '✅ None'} |
|
|
111
|
+
| **Passed (LGTM)** | **${metrics.lgtm}** | ✅ Clean |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
`;
|
|
116
|
+
// Critical Section
|
|
117
|
+
if (critical.length > 0) {
|
|
118
|
+
md += `## 2. 🛑 CRITICAL SECURITY VULNERABILITIES (P0)
|
|
119
|
+
|
|
120
|
+
> **These files contain blockers that must be fixed before any merge.**
|
|
121
|
+
|
|
122
|
+
| File | Severity | Issue |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
`;
|
|
125
|
+
for (const finding of critical) {
|
|
126
|
+
for (const issue of finding.issues) {
|
|
127
|
+
md += renderIssueRow(finding.file, issue, 'P0_CRITICAL') + '\n';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
md += '\n### 🤖 Fix Prompts (P0)\n\n';
|
|
131
|
+
for (const finding of critical) {
|
|
132
|
+
for (const issue of finding.issues) {
|
|
133
|
+
md += renderFixPrompt(issue);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
md += '\n---\n\n';
|
|
137
|
+
}
|
|
138
|
+
// High Section
|
|
139
|
+
if (high.length > 0) {
|
|
140
|
+
md += `## 3. ⚠️ HIGH PRIORITY ISSUES (P1)
|
|
141
|
+
|
|
142
|
+
> **Architecture and logic issues requiring significant attention.**
|
|
143
|
+
|
|
144
|
+
| File | Severity | Issue |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
`;
|
|
147
|
+
for (const finding of high) {
|
|
148
|
+
for (const issue of finding.issues) {
|
|
149
|
+
md += renderIssueRow(finding.file, issue, 'P1_HIGH') + '\n';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
md += '\n### 🤖 Fix Prompts (P1)\n\n';
|
|
153
|
+
for (const finding of high) {
|
|
154
|
+
for (const issue of finding.issues) {
|
|
155
|
+
md += renderFixPrompt(issue);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
md += '\n---\n\n';
|
|
159
|
+
}
|
|
160
|
+
// Medium Section
|
|
161
|
+
if (medium.length > 0) {
|
|
162
|
+
md += `## 4. 📝 MEDIUM PRIORITY ISSUES (P2)
|
|
163
|
+
|
|
164
|
+
> **Code quality and maintenance items.**
|
|
165
|
+
|
|
166
|
+
| File | Severity | Issue |
|
|
167
|
+
| --- | --- | --- |
|
|
168
|
+
`;
|
|
169
|
+
for (const finding of medium) {
|
|
170
|
+
for (const issue of finding.issues) {
|
|
171
|
+
md += renderIssueRow(finding.file, issue, 'P2_MEDIUM') + '\n';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
md += '\n---\n\n';
|
|
175
|
+
}
|
|
176
|
+
// LGTM Section
|
|
177
|
+
if (lgtm.length > 0) {
|
|
178
|
+
md += `## 5. ✅ PASSED REVIEW (LGTM)
|
|
179
|
+
|
|
180
|
+
> **No issues found in these files.**
|
|
181
|
+
|
|
182
|
+
`;
|
|
183
|
+
for (const finding of lgtm) {
|
|
184
|
+
md += `- \`${finding.file}\`\n`;
|
|
185
|
+
}
|
|
186
|
+
md += '\n---\n\n';
|
|
187
|
+
}
|
|
188
|
+
// Recommended Action
|
|
189
|
+
md += `## 🎯 RECOMMENDED ACTION
|
|
190
|
+
|
|
191
|
+
> ${recommendedAction}
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
*Generated by J-Star Code Reviewer v2*
|
|
196
|
+
`;
|
|
197
|
+
return md;
|
|
198
|
+
}
|
|
199
|
+
function determineStatus(metrics) {
|
|
200
|
+
if (metrics.critical > 0)
|
|
201
|
+
return 'CRITICAL_FAILURE';
|
|
202
|
+
if (metrics.high > 0 || metrics.medium > config_1.Config.THRESHOLDS.MEDIUM)
|
|
203
|
+
return 'NEEDS_REVIEW';
|
|
204
|
+
return 'APPROVED';
|
|
205
|
+
}
|
|
206
|
+
function generateRecommendation(metrics) {
|
|
207
|
+
if (metrics.critical > 0) {
|
|
208
|
+
return `**BLOCK MERGE:** Fix ${metrics.critical} critical issue(s) immediately. Review P0 fix prompts above.`;
|
|
209
|
+
}
|
|
210
|
+
if (metrics.high > 0) {
|
|
211
|
+
return `**Request Changes:** Address ${metrics.high} high-priority issue(s) before merging.`;
|
|
212
|
+
}
|
|
213
|
+
if (metrics.medium > 0) {
|
|
214
|
+
return `**Approve with Notes:** ${metrics.medium} medium issues found. Consider fixing in follow-up PR.`;
|
|
215
|
+
}
|
|
216
|
+
return `**Approve:** All files passed review. Ship it! 🚀`;
|
|
217
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.Detective = void 0;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
43
|
+
const RULES = [
|
|
44
|
+
{
|
|
45
|
+
id: 'SEC-001',
|
|
46
|
+
severity: 'high',
|
|
47
|
+
message: 'Possible Hardcoded Secret detected',
|
|
48
|
+
pattern: /(api_key|secret|password|token)\s*[:=]\s*['"`][a-zA-Z0-9_\-\.]{10,}['"`]/i
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'ARCH-001',
|
|
52
|
+
severity: 'medium',
|
|
53
|
+
message: 'Avoid using console.log in production code',
|
|
54
|
+
pattern: /console\.log\(/
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
// File-level rules that check the whole content
|
|
58
|
+
const FILE_RULES = [
|
|
59
|
+
{
|
|
60
|
+
id: 'ARCH-002',
|
|
61
|
+
severity: 'high',
|
|
62
|
+
message: 'Next.js "use client" must be at the very top of the file',
|
|
63
|
+
pattern: /^(?!['"]use client['"]).*['"]use client['"]/s,
|
|
64
|
+
filePattern: /\.tsx?$/
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
class Detective {
|
|
68
|
+
constructor(directory) {
|
|
69
|
+
this.directory = directory;
|
|
70
|
+
this.violations = [];
|
|
71
|
+
}
|
|
72
|
+
async scan() {
|
|
73
|
+
this.walk(this.directory);
|
|
74
|
+
return this.violations;
|
|
75
|
+
}
|
|
76
|
+
walk(dir) {
|
|
77
|
+
if (!fs.existsSync(dir))
|
|
78
|
+
return;
|
|
79
|
+
const files = fs.readdirSync(dir);
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const filePath = path.join(dir, file);
|
|
82
|
+
const stat = fs.statSync(filePath);
|
|
83
|
+
if (stat.isDirectory()) {
|
|
84
|
+
if (file !== 'node_modules' && file !== '.git' && file !== '.jstar') {
|
|
85
|
+
this.walk(filePath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.checkFile(filePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
checkFile(filePath) {
|
|
94
|
+
if (!filePath.match(/\.(ts|tsx|js|jsx)$/))
|
|
95
|
+
return;
|
|
96
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
97
|
+
const lines = content.split('\n');
|
|
98
|
+
// Line-based rules
|
|
99
|
+
for (const rule of RULES) {
|
|
100
|
+
if (rule.filePattern && !filePath.match(rule.filePattern))
|
|
101
|
+
continue;
|
|
102
|
+
lines.forEach((line, index) => {
|
|
103
|
+
if (rule.pattern.test(line)) {
|
|
104
|
+
this.addViolation(filePath, index + 1, rule);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// File-based rules
|
|
109
|
+
for (const rule of FILE_RULES) {
|
|
110
|
+
if (rule.filePattern && !filePath.match(rule.filePattern))
|
|
111
|
+
continue;
|
|
112
|
+
if (rule.pattern.test(content)) {
|
|
113
|
+
this.addViolation(filePath, 1, rule);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
addViolation(filePath, line, rule) {
|
|
118
|
+
this.violations.push({
|
|
119
|
+
file: path.relative(process.cwd(), filePath),
|
|
120
|
+
line,
|
|
121
|
+
message: rule.message,
|
|
122
|
+
severity: rule.severity,
|
|
123
|
+
code: rule.id
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
report() {
|
|
127
|
+
if (this.violations.length === 0) {
|
|
128
|
+
console.log(chalk_1.default.green("✅ Detective Engine: No violations found."));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.log(chalk_1.default.red(`🚨 Detective Engine found ${this.violations.length} violations:`));
|
|
132
|
+
// Only show first 10 to avoid wall of text
|
|
133
|
+
const total = this.violations.length;
|
|
134
|
+
const toShow = this.violations.slice(0, 10);
|
|
135
|
+
toShow.forEach(v => {
|
|
136
|
+
const color = v.severity === 'high' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
137
|
+
console.log(color(`[${v.code}] ${v.file}:${v.line} - ${v.message}`));
|
|
138
|
+
});
|
|
139
|
+
if (total > 10) {
|
|
140
|
+
console.log(chalk_1.default.dim(`... and ${total - 10} more.`));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.Detective = Detective;
|
|
145
|
+
// CLI Integration
|
|
146
|
+
if (require.main === module) {
|
|
147
|
+
const detective = new Detective(path.join(process.cwd(), 'src'));
|
|
148
|
+
detective.scan();
|
|
149
|
+
detective.report();
|
|
150
|
+
}
|