claude-git-hooks 2.33.0 → 2.34.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/CHANGELOG.md +32 -0
- package/CLAUDE.md +53 -12
- package/bin/claude-hooks +1 -0
- package/lib/cli-metadata.js +9 -0
- package/lib/commands/helpers.js +69 -2
- package/lib/commands/install.js +24 -0
- package/lib/commands/lint.js +187 -0
- package/lib/config.js +7 -0
- package/lib/hooks/pre-commit.js +97 -31
- package/lib/utils/claude-client.js +177 -37
- package/lib/utils/judge.js +11 -9
- package/lib/utils/linter-runner.js +443 -0
- package/lib/utils/tool-runner.js +418 -0
- package/package.json +69 -69
- package/templates/config.advanced.example.json +38 -0
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -33,6 +33,7 @@ import { getVersion } from '../utils/package-info.js';
|
|
|
33
33
|
import logger from '../utils/logger.js';
|
|
34
34
|
import { getConfig } from '../config.js';
|
|
35
35
|
import { recordMetric } from '../utils/metrics.js';
|
|
36
|
+
import { runLinters, displayLintResults, lintIssuesToAnalysisDetails } from '../utils/linter-runner.js';
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Configuration loaded from lib/config.js
|
|
@@ -144,11 +145,45 @@ const main = async () => {
|
|
|
144
145
|
process.exit(0);
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
// Step 3:
|
|
148
|
+
// Step 3: Run linters (fast, deterministic — before Claude analysis)
|
|
149
|
+
// Unfixable lint issues are forwarded to the judge for semantic resolution
|
|
150
|
+
let unfixableLintDetails = [];
|
|
151
|
+
if (config.linting?.enabled !== false) {
|
|
152
|
+
logger.info('🔍 Running linters...');
|
|
153
|
+
const lintStartTime = Date.now();
|
|
154
|
+
const filePaths = validFiles.map((f) => (typeof f === 'string' ? f : f.path));
|
|
155
|
+
const lintResult = runLinters(filePaths, config, presetName);
|
|
156
|
+
displayLintResults(lintResult);
|
|
157
|
+
|
|
158
|
+
// Record lint metric
|
|
159
|
+
recordMetric('linting.completed', {
|
|
160
|
+
preset: presetName,
|
|
161
|
+
fileCount: filePaths.length,
|
|
162
|
+
totalErrors: lintResult.totalErrors,
|
|
163
|
+
totalWarnings: lintResult.totalWarnings,
|
|
164
|
+
totalFixed: lintResult.totalFixed,
|
|
165
|
+
toolsRun: lintResult.results.filter((r) => !r.skipped).length,
|
|
166
|
+
toolsSkipped: lintResult.results.filter((r) => r.skipped).length,
|
|
167
|
+
duration: Date.now() - lintStartTime
|
|
168
|
+
}).catch((err) => {
|
|
169
|
+
logger.debug('pre-commit - main', 'Lint metrics recording failed', err);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Forward all remaining lint issues (errors + warnings) to the judge
|
|
173
|
+
// The judge decides whether to fix, dismiss, or block — no hard exit here
|
|
174
|
+
if (lintResult.totalErrors > 0 || lintResult.totalWarnings > 0) {
|
|
175
|
+
unfixableLintDetails = lintIssuesToAnalysisDetails(lintResult);
|
|
176
|
+
logger.debug('pre-commit - main', 'Forwarding unfixable lint issues to judge', {
|
|
177
|
+
count: unfixableLintDetails.length
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Step 4: Build file data using shared engine
|
|
148
183
|
logger.debug('pre-commit - main', 'Building file data for analysis');
|
|
149
184
|
const filesData = buildFilesData(validFiles, { staged: true });
|
|
150
185
|
|
|
151
|
-
// Step
|
|
186
|
+
// Step 5: Log analysis configuration
|
|
152
187
|
logger.info(`Sending ${filesData.length} files for review...`);
|
|
153
188
|
|
|
154
189
|
// Display analysis routing hint
|
|
@@ -156,51 +191,82 @@ const main = async () => {
|
|
|
156
191
|
logger.info('⚡ Intelligent orchestration: grouping files and assigning models');
|
|
157
192
|
}
|
|
158
193
|
|
|
159
|
-
// Step
|
|
194
|
+
// Step 6: Run analysis using shared engine
|
|
160
195
|
const result = await runAnalysis(filesData, config, {
|
|
161
196
|
hook: 'pre-commit',
|
|
162
197
|
saveDebug: config.system.debug
|
|
163
198
|
});
|
|
164
199
|
|
|
165
|
-
// Step
|
|
200
|
+
// Step 7: Display results using shared function
|
|
166
201
|
displayResults(result);
|
|
167
202
|
|
|
168
|
-
// Step
|
|
203
|
+
// Step 8: Merge unfixable lint issues into analysis result for the judge
|
|
204
|
+
if (unfixableLintDetails.length > 0) {
|
|
205
|
+
if (!Array.isArray(result.details)) {
|
|
206
|
+
result.details = [];
|
|
207
|
+
}
|
|
208
|
+
result.details.push(...unfixableLintDetails);
|
|
209
|
+
|
|
210
|
+
// Update issue counts
|
|
211
|
+
for (const detail of unfixableLintDetails) {
|
|
212
|
+
const sev = (detail.severity || 'minor').toLowerCase();
|
|
213
|
+
if (result.issues[sev] !== undefined) {
|
|
214
|
+
result.issues[sev]++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
logger.info(`📋 ${unfixableLintDetails.length} unfixable lint issue(s) forwarded to judge`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Step 9: Judge — auto-fix all issues (Claude + lint)
|
|
169
222
|
if (config.judge?.enabled !== false && hasAnyIssues(result)) {
|
|
223
|
+
let judgeModule;
|
|
170
224
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
225
|
+
judgeModule = await import('../utils/judge.js');
|
|
226
|
+
} catch (importErr) {
|
|
227
|
+
logger.warning(`Judge module import failed: ${importErr.message}`);
|
|
228
|
+
logger.warning('Commit blocked — judge module unavailable');
|
|
229
|
+
result.QUALITY_GATE = 'FAILED';
|
|
230
|
+
result.approved = false;
|
|
231
|
+
judgeModule = null;
|
|
232
|
+
}
|
|
173
233
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
234
|
+
if (judgeModule) {
|
|
235
|
+
try {
|
|
236
|
+
const { judgeAndFix } = judgeModule;
|
|
237
|
+
const judgeResult = await judgeAndFix(result, filesData, config);
|
|
238
|
+
|
|
239
|
+
// Update result with remaining issues (fixed + false positives removed)
|
|
240
|
+
result.blockingIssues = judgeResult.remainingIssues.filter((i) =>
|
|
241
|
+
['blocker', 'critical'].includes((i.severity || '').toLowerCase())
|
|
242
|
+
);
|
|
243
|
+
result.details = judgeResult.remainingIssues;
|
|
244
|
+
|
|
245
|
+
// Recalculate quality gate — pass only if ALL issues resolved
|
|
246
|
+
if (judgeResult.remainingIssues.length === 0) {
|
|
247
|
+
result.issues = {
|
|
248
|
+
blocker: 0,
|
|
249
|
+
critical: 0,
|
|
250
|
+
major: 0,
|
|
251
|
+
minor: 0,
|
|
252
|
+
info: 0
|
|
253
|
+
};
|
|
254
|
+
result.QUALITY_GATE = 'PASSED';
|
|
255
|
+
result.approved = true;
|
|
256
|
+
} else {
|
|
257
|
+
result.QUALITY_GATE = 'FAILED';
|
|
258
|
+
result.approved = false;
|
|
259
|
+
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
logger.warning(`Judge execution failed: ${err.message}`);
|
|
262
|
+
logger.warning('Commit blocked — judge could not verify issues');
|
|
192
263
|
result.QUALITY_GATE = 'FAILED';
|
|
193
264
|
result.approved = false;
|
|
194
265
|
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
logger.warning(`Judge failed: ${err.message}`);
|
|
197
|
-
logger.warning('Commit blocked — judge could not verify issues');
|
|
198
|
-
result.QUALITY_GATE = 'FAILED';
|
|
199
|
-
result.approved = false;
|
|
200
266
|
}
|
|
201
267
|
}
|
|
202
268
|
|
|
203
|
-
// Step
|
|
269
|
+
// Step 10: Check quality gate
|
|
204
270
|
const qualityGatePassed = result.QUALITY_GATE === 'PASSED';
|
|
205
271
|
const approved = result.approved !== false;
|
|
206
272
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - claude-diagnostics: Error detection and formatting
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { spawn, execSync } from 'child_process';
|
|
18
|
+
import { spawn, execSync, execFileSync } from 'child_process';
|
|
19
19
|
import fs from 'fs/promises';
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import os from 'os';
|
|
@@ -70,6 +70,37 @@ const isWSLAvailable = () => {
|
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Get running WSL distros, excluding Docker-internal ones
|
|
75
|
+
* Why: The default WSL distro may be docker-desktop (which doesn't have Claude).
|
|
76
|
+
* We need to find the actual Linux distro (e.g., Ubuntu) where Claude is installed.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} wslPath - Absolute path to wsl.exe
|
|
79
|
+
* @returns {string[]} List of running non-Docker distro names
|
|
80
|
+
*/
|
|
81
|
+
export const getRunningWSLDistros = (wslPath) => {
|
|
82
|
+
try {
|
|
83
|
+
// Use execFileSync to bypass cmd.exe shell quoting — args passed directly to wsl.exe
|
|
84
|
+
const raw = execFileSync(wslPath, ['-l', '--running', '-q'], {
|
|
85
|
+
timeout: 5000,
|
|
86
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
87
|
+
});
|
|
88
|
+
// Why UTF-16LE: wsl.exe is a native Windows executable that outputs Unicode in Windows' default
|
|
89
|
+
// encoding (UTF-16LE), not UTF-8. The null bytes (\0) are BOM artifacts and padding characters
|
|
90
|
+
// from the UTF-16LE encoding that must be stripped to get clean distro names.
|
|
91
|
+
const output = raw.toString('utf16le');
|
|
92
|
+
return output
|
|
93
|
+
.split(/\r?\n/)
|
|
94
|
+
.map((s) => s.replace(/\0/g, '').trim())
|
|
95
|
+
.filter((s) => s && !s.toLowerCase().includes('docker'));
|
|
96
|
+
} catch (e) {
|
|
97
|
+
logger.debug('claude-client - getRunningWSLDistros', 'Failed to list WSL distros', {
|
|
98
|
+
error: e.message
|
|
99
|
+
});
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
73
104
|
/**
|
|
74
105
|
* Get Claude command configuration for current platform
|
|
75
106
|
* Why: On Windows, try native Claude first, then WSL as fallback
|
|
@@ -112,11 +143,12 @@ const getClaudeCommand = () => {
|
|
|
112
143
|
// Node 24: Resolve wsl.exe absolute path to avoid shell: true
|
|
113
144
|
const wslPath = which('wsl');
|
|
114
145
|
if (wslPath) {
|
|
146
|
+
const wslCheckTimeout = config.system.wslCheckTimeout || 15000;
|
|
147
|
+
|
|
148
|
+
// Strategy 1: Direct `wsl claude` (Claude in default non-interactive PATH)
|
|
149
|
+
// Use execFileSync to bypass cmd.exe — args passed directly to wsl.exe without quote mangling
|
|
115
150
|
try {
|
|
116
|
-
|
|
117
|
-
// Increased timeout from 5s to 15s to handle system load better
|
|
118
|
-
const wslCheckTimeout = config.system.wslCheckTimeout || 15000;
|
|
119
|
-
execSync(`"${wslPath}" claude --version`, {
|
|
151
|
+
execFileSync(wslPath, ['claude', '--version'], {
|
|
120
152
|
stdio: 'ignore',
|
|
121
153
|
timeout: wslCheckTimeout
|
|
122
154
|
});
|
|
@@ -125,11 +157,9 @@ const getClaudeCommand = () => {
|
|
|
125
157
|
});
|
|
126
158
|
return { command: wslPath, args: ['claude'] };
|
|
127
159
|
} catch (wslError) {
|
|
128
|
-
// Differentiate error types for accurate user feedback
|
|
129
160
|
const errorMsg = wslError.message || '';
|
|
130
|
-
const wslCheckTimeout = config.system.wslCheckTimeout || 15000;
|
|
131
161
|
|
|
132
|
-
// Timeout: Transient system load issue
|
|
162
|
+
// Timeout: Transient system load issue — don't try fallback
|
|
133
163
|
if (errorMsg.includes('ETIMEDOUT')) {
|
|
134
164
|
throw new ClaudeClientError(
|
|
135
165
|
'Timeout connecting to WSL - system under heavy load',
|
|
@@ -146,30 +176,132 @@ const getClaudeCommand = () => {
|
|
|
146
176
|
);
|
|
147
177
|
}
|
|
148
178
|
|
|
149
|
-
// Not
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
179
|
+
// Not a timeout — try login-shell fallback below
|
|
180
|
+
logger.debug(
|
|
181
|
+
'claude-client - getClaudeCommand',
|
|
182
|
+
'Direct wsl claude failed, trying login-shell resolution',
|
|
183
|
+
{ error: errorMsg }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Strategy 2: WSL login-shell resolution (Claude installed via nvm or custom PATH)
|
|
188
|
+
// Why: `wsl claude` runs a non-interactive shell that doesn't source .bashrc/.profile,
|
|
189
|
+
// so Claude installed via nvm or user-scoped npm prefix won't be in PATH.
|
|
190
|
+
// `bash -lc "which claude"` sources the login profile to resolve the full path.
|
|
191
|
+
// Why execFileSync: cmd.exe strips inner quotes from `bash -lc "which claude"`,
|
|
192
|
+
// causing bash to receive `which` and `claude` as separate args. execFileSync bypasses
|
|
193
|
+
// cmd.exe and passes arguments directly, preserving multi-word argument boundaries.
|
|
194
|
+
try {
|
|
195
|
+
const resolvedPath = execFileSync(
|
|
196
|
+
wslPath,
|
|
197
|
+
['bash', '-lc', 'which claude'],
|
|
198
|
+
{
|
|
199
|
+
encoding: 'utf8',
|
|
200
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
201
|
+
timeout: wslCheckTimeout
|
|
202
|
+
}
|
|
203
|
+
).trim();
|
|
204
|
+
|
|
205
|
+
if (resolvedPath && resolvedPath.startsWith('/')) {
|
|
206
|
+
// Verify: run the resolved path via login shell (node may also require login shell)
|
|
207
|
+
execFileSync(wslPath, ['bash', '-lc', `${resolvedPath} --version`], {
|
|
208
|
+
stdio: 'ignore',
|
|
209
|
+
timeout: wslCheckTimeout
|
|
159
210
|
});
|
|
211
|
+
logger.debug(
|
|
212
|
+
'claude-client - getClaudeCommand',
|
|
213
|
+
'Using WSL Claude CLI via login-shell resolution',
|
|
214
|
+
{ wslPath, claudePath: resolvedPath }
|
|
215
|
+
);
|
|
216
|
+
// Return loginShell so executeClaude wraps the call in bash -lc
|
|
217
|
+
return { command: wslPath, args: [], loginShell: { path: resolvedPath } };
|
|
160
218
|
}
|
|
219
|
+
} catch (loginShellError) {
|
|
220
|
+
logger.debug(
|
|
221
|
+
'claude-client - getClaudeCommand',
|
|
222
|
+
'WSL login-shell resolution failed',
|
|
223
|
+
{ error: loginShellError.message }
|
|
224
|
+
);
|
|
225
|
+
}
|
|
161
226
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
227
|
+
// Strategy 3: Non-default WSL distro (e.g., default is docker-desktop)
|
|
228
|
+
// Why: If the default WSL distro is docker-desktop or another minimal distro,
|
|
229
|
+
// Claude may be installed in a different distro like Ubuntu.
|
|
230
|
+
const distros = getRunningWSLDistros(wslPath);
|
|
231
|
+
logger.debug(
|
|
232
|
+
'claude-client - getClaudeCommand',
|
|
233
|
+
'Trying non-default WSL distros',
|
|
234
|
+
{ distros }
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
for (const distro of distros) {
|
|
238
|
+
// Try direct call in this distro
|
|
239
|
+
try {
|
|
240
|
+
execFileSync(wslPath, ['-d', distro, 'claude', '--version'], {
|
|
241
|
+
stdio: 'ignore',
|
|
242
|
+
timeout: wslCheckTimeout
|
|
243
|
+
});
|
|
244
|
+
logger.debug(
|
|
245
|
+
'claude-client - getClaudeCommand',
|
|
246
|
+
'Using WSL Claude CLI in distro',
|
|
247
|
+
{ wslPath, distro }
|
|
248
|
+
);
|
|
249
|
+
return { command: wslPath, args: ['-d', distro, 'claude'] };
|
|
250
|
+
} catch (distroError) {
|
|
251
|
+
// Try login-shell resolution in this distro
|
|
252
|
+
try {
|
|
253
|
+
const resolved = execFileSync(
|
|
254
|
+
wslPath,
|
|
255
|
+
['-d', distro, 'bash', '-lc', 'which claude'],
|
|
256
|
+
{
|
|
257
|
+
encoding: 'utf8',
|
|
258
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
259
|
+
timeout: wslCheckTimeout
|
|
260
|
+
}
|
|
261
|
+
).trim();
|
|
262
|
+
|
|
263
|
+
if (resolved && resolved.startsWith('/')) {
|
|
264
|
+
execFileSync(
|
|
265
|
+
wslPath,
|
|
266
|
+
['-d', distro, 'bash', '-lc', `${resolved} --version`],
|
|
267
|
+
{
|
|
268
|
+
stdio: 'ignore',
|
|
269
|
+
timeout: wslCheckTimeout
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
logger.debug(
|
|
273
|
+
'claude-client - getClaudeCommand',
|
|
274
|
+
'Using WSL Claude CLI via login-shell in distro',
|
|
275
|
+
{ wslPath, distro, claudePath: resolved }
|
|
276
|
+
);
|
|
277
|
+
// Return loginShell so executeClaude wraps the call in bash -lc
|
|
278
|
+
return {
|
|
279
|
+
command: wslPath,
|
|
280
|
+
args: ['-d', distro],
|
|
281
|
+
loginShell: { path: resolved }
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
} catch (distroLoginError) {
|
|
285
|
+
logger.debug(
|
|
286
|
+
'claude-client - getClaudeCommand',
|
|
287
|
+
`Distro ${distro} does not have Claude`,
|
|
288
|
+
{ error: distroLoginError.message }
|
|
289
|
+
);
|
|
170
290
|
}
|
|
171
|
-
}
|
|
291
|
+
}
|
|
172
292
|
}
|
|
293
|
+
|
|
294
|
+
// All strategies failed
|
|
295
|
+
throw new ClaudeClientError('Claude CLI not found in WSL', {
|
|
296
|
+
context: {
|
|
297
|
+
platform: 'Windows',
|
|
298
|
+
wslPath,
|
|
299
|
+
error: 'Claude not found via direct call or login-shell resolution',
|
|
300
|
+
triedDistros: distros,
|
|
301
|
+
suggestion:
|
|
302
|
+
'Install Claude in WSL: wsl -e bash -c "npm install -g @anthropic-ai/claude-cli"'
|
|
303
|
+
}
|
|
304
|
+
});
|
|
173
305
|
}
|
|
174
306
|
|
|
175
307
|
throw new ClaudeClientError('Claude CLI not found in Windows or WSL', {
|
|
@@ -212,18 +344,26 @@ const getClaudeCommand = () => {
|
|
|
212
344
|
const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = null } = {}) =>
|
|
213
345
|
new Promise((resolve, reject) => {
|
|
214
346
|
// Get platform-specific command
|
|
215
|
-
const { command, args } = getClaudeCommand();
|
|
347
|
+
const { command, args, loginShell } = getClaudeCommand();
|
|
216
348
|
|
|
217
|
-
//
|
|
349
|
+
// Build final args — handling differs for login-shell (WSL via bash -lc) vs direct invocation
|
|
218
350
|
const finalArgs = [...args];
|
|
219
|
-
if (
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
finalArgs.push('
|
|
351
|
+
if (loginShell) {
|
|
352
|
+
// Why: When claude is found via WSL login-shell (e.g. nvm-installed node), it must be
|
|
353
|
+
// executed inside bash --login so that .profile/.bashrc set up the correct PATH.
|
|
354
|
+
// All CLI flags are embedded in the bash -c command string (no spaces in flag values).
|
|
355
|
+
const claudeParts = [loginShell.path];
|
|
356
|
+
if (allowedTools.length > 0) claudeParts.push(`--allowedTools ${allowedTools.join(',')}`);
|
|
357
|
+
if (model) claudeParts.push(`--model ${model}`);
|
|
358
|
+
finalArgs.push('bash', '-lc', claudeParts.join(' '));
|
|
359
|
+
} else {
|
|
360
|
+
if (allowedTools.length > 0) {
|
|
361
|
+
// Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
|
|
362
|
+
finalArgs.push('--allowedTools', allowedTools.join(','));
|
|
363
|
+
}
|
|
364
|
+
if (model) {
|
|
365
|
+
finalArgs.push('--model', model);
|
|
366
|
+
}
|
|
227
367
|
}
|
|
228
368
|
|
|
229
369
|
// CRITICAL FIX: Windows .cmd/.bat file handling
|
package/lib/utils/judge.js
CHANGED
|
@@ -182,17 +182,19 @@ const judgeAndFix = async (analysisResult, filesData, config) => {
|
|
|
182
182
|
const resolvedIndices = new Set();
|
|
183
183
|
const verdicts = [];
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
// Cap at allIssues.length — LLM may hallucinate extra fixes beyond the input count
|
|
186
|
+
const fixCount = Math.min(fixes.length, allIssues.length);
|
|
187
|
+
|
|
188
|
+
if (fixes.length > allIssues.length) {
|
|
189
|
+
logger.debug('judge', `LLM returned ${fixes.length} fixes for ${allIssues.length} issues — ignoring ${fixes.length - allIssues.length} extra entries`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < fixCount; i++) {
|
|
186
193
|
const verdict = fixes[i];
|
|
187
194
|
const num = i + 1;
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
? (matchedIssue.severity || 'unknown').toUpperCase()
|
|
192
|
-
: 'UNKNOWN';
|
|
193
|
-
const desc = matchedIssue
|
|
194
|
-
? matchedIssue.description || matchedIssue.message || 'No description'
|
|
195
|
-
: verdict.explanation || 'Unknown issue';
|
|
195
|
+
const matchedIssue = allIssues[i];
|
|
196
|
+
const severity = (matchedIssue.severity || 'unknown').toUpperCase();
|
|
197
|
+
const desc = matchedIssue.description || matchedIssue.message || 'No description';
|
|
196
198
|
|
|
197
199
|
if (verdict.assessment === 'FALSE_POSITIVE') {
|
|
198
200
|
falsePositiveCount++;
|