coderoast 0.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.
@@ -0,0 +1,584 @@
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.runFixItAgent = runFixItAgent;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const gemini_client_1 = require("./gemini-client");
11
+ const code_analysis_agent_1 = require("./code-analysis-agent");
12
+ const FIXABLE_SIGNALS = new Set(["longFunctions", "duplicateBlocks"]);
13
+ const MAX_FIXES = 2;
14
+ function toAbsolutePath(rootPath, relativePath) {
15
+ const parts = relativePath.split("/");
16
+ return node_path_1.default.resolve(rootPath, node_path_1.default.join(...parts));
17
+ }
18
+ function normalizeDiffPath(value) {
19
+ const cleaned = value.replace(/^\s+|\s+$/g, "");
20
+ if (cleaned === "/dev/null") {
21
+ return cleaned;
22
+ }
23
+ return cleaned.replace(/^a\//, "").replace(/^b\//, "");
24
+ }
25
+ function parseHunkHeader(line) {
26
+ const match = /@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$/.exec(line);
27
+ if (!match) {
28
+ return null;
29
+ }
30
+ return {
31
+ oldStart: Number(match[1]),
32
+ oldLines: Number(match[2] ?? "1"),
33
+ newStart: Number(match[3]),
34
+ newLines: Number(match[4] ?? "1"),
35
+ lines: [],
36
+ };
37
+ }
38
+ function parseUnifiedDiff(diff) {
39
+ const lines = diff.split(/\r?\n/);
40
+ const patches = [];
41
+ let current = null;
42
+ let currentHunk = null;
43
+ for (let i = 0; i < lines.length; i += 1) {
44
+ const rawLine = lines[i];
45
+ const line = rawLine.replace(/\r$/, "");
46
+ if (line.startsWith("```")) {
47
+ continue;
48
+ }
49
+ if (line.startsWith("diff --git")) {
50
+ current = null;
51
+ currentHunk = null;
52
+ continue;
53
+ }
54
+ if (line.startsWith("index ")) {
55
+ continue;
56
+ }
57
+ if (line.startsWith("--- ")) {
58
+ const oldPath = normalizeDiffPath(line.slice(4));
59
+ let nextIndex = i + 1;
60
+ while (nextIndex < lines.length) {
61
+ const candidate = (lines[nextIndex] ?? "").replace(/\r$/, "");
62
+ if (candidate.startsWith("+++ ")) {
63
+ break;
64
+ }
65
+ if (candidate.startsWith("--- ") ||
66
+ candidate.startsWith("@@ ") ||
67
+ candidate.startsWith("diff --git")) {
68
+ break;
69
+ }
70
+ if (candidate.startsWith("index ") || candidate.trim().length === 0) {
71
+ nextIndex += 1;
72
+ continue;
73
+ }
74
+ nextIndex += 1;
75
+ }
76
+ const next = lines[nextIndex] ?? "";
77
+ if (!next.startsWith("+++ ")) {
78
+ throw new Error("Malformed diff header.");
79
+ }
80
+ const newPath = normalizeDiffPath(next.slice(4));
81
+ const filePath = newPath !== "/dev/null" ? newPath : oldPath;
82
+ current = { filePath, hunks: [] };
83
+ patches.push(current);
84
+ currentHunk = null;
85
+ i = nextIndex;
86
+ continue;
87
+ }
88
+ if (line.startsWith("@@ ")) {
89
+ if (!current) {
90
+ throw new Error("Hunk found before file header.");
91
+ }
92
+ const hunk = parseHunkHeader(line);
93
+ if (!hunk) {
94
+ throw new Error("Malformed hunk header.");
95
+ }
96
+ current.hunks.push(hunk);
97
+ currentHunk = hunk;
98
+ continue;
99
+ }
100
+ if (!currentHunk) {
101
+ continue;
102
+ }
103
+ if (line.startsWith("\")) {
104
+ continue;
105
+ }
106
+ currentHunk.lines.push(line);
107
+ }
108
+ return patches;
109
+ }
110
+ function hasPatchChanges(patches) {
111
+ for (const patch of patches) {
112
+ for (const hunk of patch.hunks) {
113
+ for (const line of hunk.lines) {
114
+ if (line.startsWith("+") || line.startsWith("-")) {
115
+ if (line.startsWith("+++ ") || line.startsWith("--- ")) {
116
+ continue;
117
+ }
118
+ return true;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+ function applyPatchToContent(content, patch) {
126
+ const lines = content.split(/\r?\n/);
127
+ const output = [];
128
+ let cursor = 0;
129
+ for (const hunk of patch.hunks) {
130
+ const startIndex = hunk.oldStart - 1;
131
+ if (startIndex < 0 || startIndex > lines.length) {
132
+ throw new Error("Hunk start out of range.");
133
+ }
134
+ output.push(...lines.slice(cursor, startIndex));
135
+ cursor = startIndex;
136
+ for (const line of hunk.lines) {
137
+ if (line.startsWith(" ")) {
138
+ output.push(line.slice(1));
139
+ cursor += 1;
140
+ }
141
+ else if (line.startsWith("-")) {
142
+ cursor += 1;
143
+ }
144
+ else if (line.startsWith("+")) {
145
+ output.push(line.slice(1));
146
+ }
147
+ else {
148
+ throw new Error("Unexpected diff line.");
149
+ }
150
+ }
151
+ }
152
+ output.push(...lines.slice(cursor));
153
+ return output.join("\n");
154
+ }
155
+ function stripMarkdownFences(text) {
156
+ return text
157
+ .split(/\r?\n/)
158
+ .filter((line) => !line.trimStart().startsWith("```"))
159
+ .join("\n");
160
+ }
161
+ function ensureDiffHeaders(text, fallbackFile) {
162
+ const lines = text.split(/\r?\n/);
163
+ const hasHeader = lines.some((line) => line.startsWith("--- "));
164
+ const hasHunk = lines.some((line) => line.startsWith("@@ "));
165
+ if (hasHeader || !hasHunk || !fallbackFile) {
166
+ return text;
167
+ }
168
+ return [`--- a/${fallbackFile}`, `+++ b/${fallbackFile}`, ...lines].join("\n");
169
+ }
170
+ async function writeDebugPatch(issueId, label, contents) {
171
+ const dir = node_path_1.default.join(node_os_1.default.tmpdir(), "coderoast-fix-debug");
172
+ await promises_1.default.mkdir(dir, { recursive: true });
173
+ const filePath = node_path_1.default.join(dir, `issue-${issueId}-${label}.txt`);
174
+ await promises_1.default.writeFile(filePath, contents);
175
+ return filePath;
176
+ }
177
+ async function applyPatchesToOverrides(rootPath, patches) {
178
+ const overrides = {};
179
+ const byFile = new Map();
180
+ for (const patch of patches) {
181
+ const list = byFile.get(patch.filePath) ?? [];
182
+ list.push(patch);
183
+ byFile.set(patch.filePath, list);
184
+ }
185
+ for (const [filePath, filePatches] of byFile) {
186
+ const absolutePath = toAbsolutePath(rootPath, filePath);
187
+ const original = await promises_1.default.readFile(absolutePath, "utf8");
188
+ let content = overrides[filePath] ?? original;
189
+ for (const patch of filePatches) {
190
+ content = applyPatchToContent(content, patch);
191
+ }
192
+ overrides[filePath] = content;
193
+ }
194
+ return overrides;
195
+ }
196
+ function buildMetricDelta(before, after) {
197
+ return {
198
+ maxFunctionLength: after.metrics.maxFunctionLength - before.metrics.maxFunctionLength,
199
+ avgFunctionLength: Math.round((after.metrics.avgFunctionLength - before.metrics.avgFunctionLength) * 100) / 100,
200
+ duplicateBlocks: after.metrics.duplicateBlocks - before.metrics.duplicateBlocks,
201
+ totalFunctions: after.metrics.totalFunctions - before.metrics.totalFunctions,
202
+ };
203
+ }
204
+ function lineInRanges(line, ranges) {
205
+ return ranges.some((range) => line >= range.startLine && line <= range.endLine);
206
+ }
207
+ function patchWithinEvidence(patch, allowed) {
208
+ for (const filePatch of patch) {
209
+ const ranges = allowed[filePatch.filePath];
210
+ if (!ranges || ranges.length === 0) {
211
+ return { ok: false, reason: `Patch touches non-evidence file: ${filePatch.filePath}` };
212
+ }
213
+ for (const hunk of filePatch.hunks) {
214
+ let oldLine = hunk.oldStart;
215
+ for (const line of hunk.lines) {
216
+ if (line.startsWith("-")) {
217
+ if (!lineInRanges(oldLine, ranges)) {
218
+ return {
219
+ ok: false,
220
+ reason: `Patch removes line ${oldLine} outside evidence in ${filePatch.filePath}`,
221
+ };
222
+ }
223
+ oldLine += 1;
224
+ }
225
+ else if (line.startsWith("+")) {
226
+ if (!lineInRanges(oldLine, ranges)) {
227
+ return {
228
+ ok: false,
229
+ reason: `Patch adds line outside evidence near ${oldLine} in ${filePatch.filePath}`,
230
+ };
231
+ }
232
+ }
233
+ else if (line.startsWith(" ")) {
234
+ oldLine += 1;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ return { ok: true };
240
+ }
241
+ function buildAllowedRanges(issue) {
242
+ return issue.evidence.reduce((acc, item) => {
243
+ acc[item.file] = acc[item.file] ?? [];
244
+ acc[item.file].push({ startLine: item.startLine, endLine: item.endLine });
245
+ return acc;
246
+ }, {});
247
+ }
248
+ function summarizeEvidence(issue) {
249
+ return issue.evidence
250
+ .map((item) => `${item.file}:${item.startLine}-${item.endLine}`)
251
+ .join("; ");
252
+ }
253
+ function buildNumberedSnippet(content, startLine, endLine) {
254
+ const lines = content.split(/\r?\n/);
255
+ const slice = lines.slice(startLine - 1, endLine);
256
+ return slice
257
+ .map((line, index) => {
258
+ const lineNo = startLine + index;
259
+ return `${lineNo.toString().padStart(4, " ")} | ${line}`;
260
+ })
261
+ .join("\n");
262
+ }
263
+ async function readSnippet(rootPath, item) {
264
+ const absolutePath = toAbsolutePath(rootPath, item.file);
265
+ const content = await promises_1.default.readFile(absolutePath, "utf8");
266
+ return {
267
+ file: item.file,
268
+ startLine: item.startLine,
269
+ endLine: item.endLine,
270
+ text: buildNumberedSnippet(content, item.startLine, item.endLine),
271
+ };
272
+ }
273
+ function selectDuplicateEvidence(issue) {
274
+ const byFile = new Map();
275
+ for (const item of issue.evidence) {
276
+ const list = byFile.get(item.file) ?? [];
277
+ list.push(item);
278
+ byFile.set(item.file, list);
279
+ }
280
+ for (const items of byFile.values()) {
281
+ if (items.length >= 2) {
282
+ return items.slice(0, 2);
283
+ }
284
+ }
285
+ return [];
286
+ }
287
+ function buildLongFunctionPrompt(issue, snippets) {
288
+ const templates = snippets.map((snippet) => {
289
+ const length = Math.max(1, snippet.endLine - snippet.startLine + 1);
290
+ return [
291
+ `--- a/${snippet.file}`,
292
+ `+++ b/${snippet.file}`,
293
+ `@@ -${snippet.startLine},${length} +${snippet.startLine},${length} @@`,
294
+ " <unchanged line>",
295
+ "-<line to remove>",
296
+ "+<line to add>",
297
+ " <unchanged line>",
298
+ ].join("\n");
299
+ });
300
+ return [
301
+ "You are a code fixer. Output ONLY a unified diff.",
302
+ "Return a complete unified diff with ---/+++ headers and @@ hunk headers.",
303
+ "Do not wrap the diff in markdown fences or add commentary.",
304
+ "Use the strict template below. Replace placeholders with real code.",
305
+ "Choose one template and output only the filled diff.",
306
+ "If you change line counts, adjust the @@ header lengths accordingly.",
307
+ "You must only modify lines within the evidence line ranges provided.",
308
+ "Do not add new files. Do not edit outside the ranges.",
309
+ `Goal: reduce function length below ${code_analysis_agent_1.LONG_FUNCTION_LOC} lines.`,
310
+ "Ensure the diff includes at least one added or removed line.",
311
+ "",
312
+ `Issue type: ${issue.type}`,
313
+ `Signal: ${issue.signal}`,
314
+ `Evidence: ${summarizeEvidence(issue)}`,
315
+ "",
316
+ "Evidence snippets (with line numbers):",
317
+ ...snippets.map((snippet) => `File: ${snippet.file} (${snippet.startLine}-${snippet.endLine})\n${snippet.text}`),
318
+ "",
319
+ "Strict diff template (fill in one of these):",
320
+ ...templates,
321
+ ].join("\n");
322
+ }
323
+ function buildDuplicatePrompt(issue, snippets) {
324
+ const templates = snippets.map((snippet) => {
325
+ const length = Math.max(1, snippet.endLine - snippet.startLine + 1);
326
+ return [
327
+ `--- a/${snippet.file}`,
328
+ `+++ b/${snippet.file}`,
329
+ `@@ -${snippet.startLine},${length} +${snippet.startLine},${length} @@`,
330
+ " <unchanged line>",
331
+ "-<line to remove>",
332
+ "+<line to add>",
333
+ " <unchanged line>",
334
+ ].join("\n");
335
+ });
336
+ return [
337
+ "You are a code fixer. Output ONLY a unified diff.",
338
+ "Return a complete unified diff with ---/+++ headers and @@ hunk headers.",
339
+ "Do not wrap the diff in markdown fences or add commentary.",
340
+ "Use the strict template below. Replace placeholders with real code.",
341
+ "Choose one template and output only the filled diff.",
342
+ "If you change line counts, adjust the @@ header lengths accordingly.",
343
+ "You must only modify lines within the evidence line ranges provided.",
344
+ "Do not add new files. Do not edit outside the ranges.",
345
+ "Goal: eliminate duplication by changing one occurrence.",
346
+ "Ensure the diff includes at least one added or removed line.",
347
+ "",
348
+ `Issue type: ${issue.type}`,
349
+ `Signal: ${issue.signal}`,
350
+ `Evidence: ${summarizeEvidence(issue)}`,
351
+ `Target file: ${snippets[0]?.file ?? "unknown"}`,
352
+ "",
353
+ "Evidence snippets (with line numbers):",
354
+ ...snippets.map((snippet) => `File: ${snippet.file} (${snippet.startLine}-${snippet.endLine})\n${snippet.text}`),
355
+ "",
356
+ "Strict diff template (fill in one of these):",
357
+ ...templates,
358
+ ].join("\n");
359
+ }
360
+ function buildRetryPrompt(basePrompt, reason) {
361
+ return [
362
+ `Previous output was invalid: ${reason}`,
363
+ "You MUST return ONLY a valid unified diff.",
364
+ "Do not include diff --git or index lines.",
365
+ "Do not include markdown or commentary.",
366
+ "Use the strict template exactly.",
367
+ "",
368
+ basePrompt,
369
+ ].join("\n");
370
+ }
371
+ function findMaxLongFunctionInRange(longFunctions, file, range) {
372
+ const matches = longFunctions.filter((fn) => fn.file === file &&
373
+ fn.startLine <= range.endLine &&
374
+ fn.endLine >= range.startLine &&
375
+ fn.length >= code_analysis_agent_1.LONG_FUNCTION_LOC);
376
+ if (matches.length === 0) {
377
+ return 0;
378
+ }
379
+ return Math.max(...matches.map((fn) => fn.length));
380
+ }
381
+ function verifyLongFunctionFix(before, after, evidence) {
382
+ const range = { startLine: evidence.startLine, endLine: evidence.endLine };
383
+ const beforeMax = findMaxLongFunctionInRange(before.signals.longFunctions, evidence.file, range);
384
+ const afterMax = findMaxLongFunctionInRange(after.signals.longFunctions, evidence.file, range);
385
+ if (beforeMax === 0) {
386
+ return { ok: false, message: "No long function found in evidence range." };
387
+ }
388
+ const afterLabel = afterMax === 0 ? `< ${code_analysis_agent_1.LONG_FUNCTION_LOC}` : `${afterMax}`;
389
+ const details = `longFunctionLength: ${beforeMax} -> ${afterLabel}`;
390
+ if (afterMax === 0 || afterMax < beforeMax) {
391
+ return { ok: true, message: "Long function length reduced.", details };
392
+ }
393
+ return { ok: false, message: "Long function length did not improve.", details };
394
+ }
395
+ function verifyDuplicateFix(before, after) {
396
+ const details = `duplicateBlocks: ${before.metrics.duplicateBlocks} -> ${after.metrics.duplicateBlocks}`;
397
+ if (after.metrics.duplicateBlocks < before.metrics.duplicateBlocks) {
398
+ return { ok: true, message: "Duplicate blocks reduced.", details };
399
+ }
400
+ return { ok: false, message: "Duplicate blocks did not improve.", details };
401
+ }
402
+ async function generatePatch(apiKey, model, prompt) {
403
+ return (0, gemini_client_1.callGemini)({
404
+ apiKey,
405
+ model,
406
+ prompt,
407
+ temperature: 0,
408
+ maxOutputTokens: 900,
409
+ });
410
+ }
411
+ function classifyGeminiError(error) {
412
+ const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
413
+ const match = /Gemini API error (\d+)/i.exec(message);
414
+ const code = match ? Number(match[1]) : undefined;
415
+ return { code, message };
416
+ }
417
+ function buildQuotaMessage(message) {
418
+ if (message.includes("RESOURCE_EXHAUSTED") || message.includes("quota")) {
419
+ return "Fix-It skipped: Gemini quota exceeded. Try again later.";
420
+ }
421
+ return message;
422
+ }
423
+ async function attemptFix(issueId, issue, config, scan, analysis) {
424
+ if (!issue.evidenceComplete) {
425
+ return null;
426
+ }
427
+ const apiKey = (0, gemini_client_1.getGeminiApiKey)();
428
+ if (!apiKey) {
429
+ return null;
430
+ }
431
+ if (config.fixLimit === 0) {
432
+ return null;
433
+ }
434
+ const model = process.env.GEMINI_MODEL ?? "gemini-2.5-flash";
435
+ const rootPath = node_path_1.default.resolve(config.path);
436
+ let prompt = "";
437
+ let evidenceItems = [];
438
+ if (issue.signal === "longFunctions") {
439
+ evidenceItems = issue.evidence.slice(0, 1);
440
+ if (evidenceItems.length === 0) {
441
+ return null;
442
+ }
443
+ const snippets = await Promise.all(evidenceItems.map((item) => readSnippet(rootPath, item)));
444
+ prompt = buildLongFunctionPrompt(issue, snippets);
445
+ }
446
+ else if (issue.signal === "duplicateBlocks") {
447
+ evidenceItems = selectDuplicateEvidence(issue);
448
+ if (evidenceItems.length < 2) {
449
+ return null;
450
+ }
451
+ const snippets = await Promise.all(evidenceItems.map((item) => readSnippet(rootPath, item)));
452
+ prompt = buildDuplicatePrompt(issue, snippets);
453
+ }
454
+ else {
455
+ return null;
456
+ }
457
+ let patchText = "";
458
+ let patches = [];
459
+ const allowedRanges = buildAllowedRanges(issue);
460
+ const fallbackFile = evidenceItems[0]?.file;
461
+ const debugPaths = [];
462
+ try {
463
+ patchText = await generatePatch(apiKey, model, prompt);
464
+ patchText = ensureDiffHeaders(stripMarkdownFences(patchText), fallbackFile);
465
+ if (config.fixDebug) {
466
+ const debugPath = await writeDebugPatch(issueId, "raw", patchText);
467
+ console.warn(`[Fix-It Debug] saved response to ${debugPath}`);
468
+ debugPaths.push(debugPath);
469
+ }
470
+ try {
471
+ patches = parseUnifiedDiff(patchText);
472
+ if (patches.length === 0) {
473
+ throw new Error("Empty patch response.");
474
+ }
475
+ if (!hasPatchChanges(patches)) {
476
+ throw new Error("Patch contains no changes.");
477
+ }
478
+ }
479
+ catch (error) {
480
+ const reason = error instanceof Error ? error.message : "Invalid patch.";
481
+ const retryPrompt = buildRetryPrompt(prompt, reason);
482
+ patchText = await generatePatch(apiKey, model, retryPrompt);
483
+ patchText = ensureDiffHeaders(stripMarkdownFences(patchText), fallbackFile);
484
+ if (config.fixDebug) {
485
+ const debugPath = await writeDebugPatch(issueId, "retry", patchText);
486
+ console.warn(`[Fix-It Debug] saved response to ${debugPath}`);
487
+ debugPaths.push(debugPath);
488
+ }
489
+ patches = parseUnifiedDiff(patchText);
490
+ if (patches.length === 0) {
491
+ throw new Error("Empty patch response.");
492
+ }
493
+ if (!hasPatchChanges(patches)) {
494
+ throw new Error("Patch contains no changes.");
495
+ }
496
+ }
497
+ }
498
+ catch (error) {
499
+ const info = classifyGeminiError(error);
500
+ return {
501
+ issueId,
502
+ issueType: issue.type,
503
+ signal: issue.signal,
504
+ files: Object.keys(allowedRanges),
505
+ patch: "",
506
+ verified: false,
507
+ verificationMessage: buildQuotaMessage(info.message),
508
+ debugPaths: debugPaths.length > 0 ? debugPaths : undefined,
509
+ };
510
+ }
511
+ const guard = patchWithinEvidence(patches, allowedRanges);
512
+ if (!guard.ok) {
513
+ return {
514
+ issueId,
515
+ issueType: issue.type,
516
+ signal: issue.signal,
517
+ files: Object.keys(allowedRanges),
518
+ patch: "",
519
+ verified: false,
520
+ verificationMessage: guard.reason ?? "Patch rejected by evidence guard.",
521
+ debugPaths: debugPaths.length > 0 ? debugPaths : undefined,
522
+ };
523
+ }
524
+ const overrides = {};
525
+ for (const filePatch of patches) {
526
+ const absolutePath = toAbsolutePath(rootPath, filePatch.filePath);
527
+ const content = await promises_1.default.readFile(absolutePath, "utf8");
528
+ overrides[filePatch.filePath] = applyPatchToContent(content, filePatch);
529
+ }
530
+ const updatedAnalysis = await (0, code_analysis_agent_1.runCodeAnalysisAgent)(config, scan, overrides);
531
+ let verification = {
532
+ ok: false,
533
+ message: "No verification available.",
534
+ };
535
+ if (issue.signal === "longFunctions" && evidenceItems[0]) {
536
+ verification = verifyLongFunctionFix(analysis, updatedAnalysis, evidenceItems[0]);
537
+ }
538
+ else if (issue.signal === "duplicateBlocks") {
539
+ verification = verifyDuplicateFix(analysis, updatedAnalysis);
540
+ }
541
+ return {
542
+ issueId,
543
+ issueType: issue.type,
544
+ signal: issue.signal,
545
+ files: Object.keys(allowedRanges),
546
+ patch: patchText.trim(),
547
+ verified: verification.ok,
548
+ verificationMessage: verification.message,
549
+ verificationDetails: verification.details,
550
+ debugPaths: debugPaths.length > 0 ? debugPaths : undefined,
551
+ };
552
+ }
553
+ async function buildPreviewSummary(config, scan, analysis, suggestions) {
554
+ const verifiedPatches = suggestions
555
+ .filter((suggestion) => suggestion.verified && suggestion.patch)
556
+ .flatMap((suggestion) => parseUnifiedDiff(suggestion.patch));
557
+ if (verifiedPatches.length === 0) {
558
+ return { before: analysis.metrics, note: "No verified patches to compare yet." };
559
+ }
560
+ const rootPath = node_path_1.default.resolve(config.path);
561
+ const overrides = await applyPatchesToOverrides(rootPath, verifiedPatches);
562
+ const after = await (0, code_analysis_agent_1.runCodeAnalysisAgent)(config, scan, overrides);
563
+ const delta = buildMetricDelta(analysis, after);
564
+ return {
565
+ before: analysis.metrics,
566
+ after: after.metrics,
567
+ delta,
568
+ note: "Preview metrics are based on verified patch candidates.",
569
+ };
570
+ }
571
+ async function runFixItAgent(config, scan, analysis, insights) {
572
+ const suggestions = [];
573
+ const candidates = insights.issues.filter((issue) => FIXABLE_SIGNALS.has(issue.signal));
574
+ const maxFixes = config.fixLimit ?? MAX_FIXES;
575
+ for (const [index, issue] of candidates.slice(0, maxFixes).entries()) {
576
+ const suggestion = await attemptFix(index + 1, issue, config, scan, analysis);
577
+ if (!suggestion) {
578
+ continue;
579
+ }
580
+ suggestions.push(suggestion);
581
+ }
582
+ const previewSummary = await buildPreviewSummary(config, scan, analysis, suggestions);
583
+ return { suggestions, previewSummary };
584
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setGeminiClientFactoryForTests = setGeminiClientFactoryForTests;
4
+ exports.getGeminiApiKey = getGeminiApiKey;
5
+ exports.callGemini = callGemini;
6
+ exports.callGeminiNarrator = callGeminiNarrator;
7
+ const genai_1 = require("@google/genai");
8
+ const defaultFactory = (options) => new genai_1.GoogleGenAI({
9
+ apiKey: options.apiKey,
10
+ apiVersion: options.apiVersion,
11
+ });
12
+ let createClient = defaultFactory;
13
+ function setGeminiClientFactoryForTests(factory) {
14
+ createClient = factory ?? defaultFactory;
15
+ }
16
+ function getGeminiApiKey() {
17
+ return process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
18
+ }
19
+ function buildClient(apiKey) {
20
+ const apiVersion = process.env.GEMINI_API_VERSION;
21
+ return createClient({ apiKey, apiVersion });
22
+ }
23
+ function extractText(response) {
24
+ const text = typeof response?.text === "string" ? response.text.trim() : "";
25
+ if (!text) {
26
+ throw new Error("Gemini response missing text.");
27
+ }
28
+ return text;
29
+ }
30
+ async function callGemini(params) {
31
+ const client = buildClient(params.apiKey);
32
+ const response = await client.models.generateContent({
33
+ model: params.model,
34
+ contents: params.prompt,
35
+ config: {
36
+ temperature: params.temperature ?? 0.2,
37
+ maxOutputTokens: params.maxOutputTokens ?? 512,
38
+ },
39
+ });
40
+ return extractText(response);
41
+ }
42
+ async function callGeminiNarrator(params) {
43
+ return callGemini(params);
44
+ }