@j0hanz/code-review-analyst-mcp 1.6.2 → 1.6.4
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 +11 -13
- package/dist/index.js +36 -1
- package/dist/lib/diff-store.d.ts +1 -1
- package/dist/lib/diff.d.ts +30 -0
- package/dist/lib/diff.js +189 -0
- package/dist/lib/gemini.js +12 -11
- package/dist/lib/tool-contracts.d.ts +3 -9
- package/dist/lib/tool-contracts.js +2 -11
- package/dist/lib/tool-factory.js +19 -17
- package/dist/lib/types.d.ts +0 -4
- package/dist/resources/server-config.js +0 -4
- package/dist/resources/tool-catalog.js +0 -1
- package/dist/schemas/inputs.d.ts +0 -8
- package/dist/schemas/inputs.js +0 -15
- package/dist/schemas/outputs.js +1 -1
- package/dist/tools/analyze-pr-impact.js +1 -1
- package/dist/tools/generate-diff.js +1 -2
- package/dist/tools/generate-test-plan.js +1 -1
- package/dist/tools/inspect-code-quality.d.ts +0 -2
- package/dist/tools/inspect-code-quality.js +5 -51
- package/dist/tools/suggest-search-replace.js +1 -1
- package/package.json +1 -1
- package/dist/lib/cli.d.ts +0 -1
- package/dist/lib/cli.js +0 -35
- package/dist/lib/context-budget.d.ts +0 -8
- package/dist/lib/context-budget.js +0 -36
- package/dist/lib/diff-budget.d.ts +0 -6
- package/dist/lib/diff-budget.js +0 -31
- package/dist/lib/diff-cleaner.d.ts +0 -12
- package/dist/lib/diff-cleaner.js +0 -76
- package/dist/lib/diff-parser.d.ts +0 -34
- package/dist/lib/diff-parser.js +0 -116
package/README.md
CHANGED
|
@@ -305,21 +305,20 @@ Summarize a pull request diff and assess high-level risk using the Flash model.
|
|
|
305
305
|
|
|
306
306
|
### `inspect_code_quality`
|
|
307
307
|
|
|
308
|
-
Deep-dive code review
|
|
308
|
+
Deep-dive code review using the Pro model with thinking (16K token budget).
|
|
309
309
|
|
|
310
|
-
| Parameter | Type | Required | Description
|
|
311
|
-
| ------------- | ---------- | -------- |
|
|
312
|
-
| `diff` | `string` | Yes | Unified diff text.
|
|
313
|
-
| `repository` | `string` | Yes | Repository identifier (e.g. `org/repo`).
|
|
314
|
-
| `language` | `string` | No | Primary language hint.
|
|
315
|
-
| `focusAreas` | `string[]` | No | Areas to inspect: security, correctness, etc.
|
|
316
|
-
| `maxFindings` | `number` | No | Maximum findings to return (1-25).
|
|
317
|
-
| `files` | `object[]` | No | Full file contents (`path`, `content`) for context. |
|
|
310
|
+
| Parameter | Type | Required | Description |
|
|
311
|
+
| ------------- | ---------- | -------- | --------------------------------------------- |
|
|
312
|
+
| `diff` | `string` | Yes | Unified diff text. |
|
|
313
|
+
| `repository` | `string` | Yes | Repository identifier (e.g. `org/repo`). |
|
|
314
|
+
| `language` | `string` | No | Primary language hint. |
|
|
315
|
+
| `focusAreas` | `string[]` | No | Areas to inspect: security, correctness, etc. |
|
|
316
|
+
| `maxFindings` | `number` | No | Maximum findings to return (1-25). |
|
|
318
317
|
|
|
319
318
|
**Returns:** `summary`, `overallRisk` (low/medium/high/critical), `findings[]` (severity, file, line, title, explanation, recommendation), `testsNeeded[]`, `contextualInsights[]`.
|
|
320
319
|
|
|
321
320
|
> [!NOTE]
|
|
322
|
-
>
|
|
321
|
+
> Diff size bounded by `MAX_DIFF_CHARS` (default 120,000). Expect 60-120s latency due to deep thinking.
|
|
323
322
|
|
|
324
323
|
### `suggest_search_replace`
|
|
325
324
|
|
|
@@ -378,7 +377,6 @@ Create a test plan covering the changes in the diff using the Flash model with t
|
|
|
378
377
|
| `GEMINI_MODEL` | Override default model selection | — | No |
|
|
379
378
|
| `GEMINI_HARM_BLOCK_THRESHOLD` | Safety threshold (BLOCK_NONE, BLOCK_ONLY_HIGH, etc.) | `BLOCK_NONE` | No |
|
|
380
379
|
| `MAX_DIFF_CHARS` | Max chars for diff input | `120000` | No |
|
|
381
|
-
| `MAX_CONTEXT_CHARS` | Max combined context for inspection | `500000` | No |
|
|
382
380
|
| `MAX_CONCURRENT_CALLS` | Max concurrent Gemini requests | `10` | No |
|
|
383
381
|
| `MAX_CONCURRENT_BATCH_CALLS` | Max concurrent inline batch requests | `2` | No |
|
|
384
382
|
| `MAX_CONCURRENT_CALLS_WAIT_MS` | Max wait time for a free Gemini slot | `2000` | No |
|
|
@@ -407,7 +405,7 @@ Create a test plan covering the changes in the diff using the Flash model with t
|
|
|
407
405
|
|
|
408
406
|
### Deep Code Inspection
|
|
409
407
|
|
|
410
|
-
1. Call `inspect_code_quality` with the diff
|
|
408
|
+
1. Call `inspect_code_quality` with the cached diff.
|
|
411
409
|
2. Use `focusAreas` to target specific concerns (security, performance).
|
|
412
410
|
3. Review `findings` and `contextualInsights`.
|
|
413
411
|
|
|
@@ -457,7 +455,7 @@ The pipeline runs lint, type-check, test, and build, then publishes to three tar
|
|
|
457
455
|
| Issue | Solution |
|
|
458
456
|
| ------------------------------------------ | ------------------------------------------------------------------------------------ |
|
|
459
457
|
| `Missing GEMINI_API_KEY or GOOGLE_API_KEY` | Set one of the API key env vars in your MCP client config. |
|
|
460
|
-
| `E_INPUT_TOO_LARGE` | Diff
|
|
458
|
+
| `E_INPUT_TOO_LARGE` | Diff exceeds budget. Split into smaller diffs. |
|
|
461
459
|
| `Gemini request timed out` | Pro model tasks may take 60-120s. Increase your client timeout. |
|
|
462
460
|
| `Too many concurrent Gemini calls` | Reduce parallel tool calls or increase `MAX_CONCURRENT_CALLS`. |
|
|
463
461
|
| No tool output visible | Ensure your MCP client is not swallowing `stderr` — the server uses stdio transport. |
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import {
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
4
|
import { getErrorMessage } from './lib/errors.js';
|
|
5
5
|
import { createServer } from './server.js';
|
|
6
6
|
const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
7
|
+
// --- CLI Parsing ---
|
|
8
|
+
const ARG_OPTION_MODEL = 'model';
|
|
9
|
+
const ARG_OPTION_MAX_DIFF_CHARS = 'max-diff-chars';
|
|
10
|
+
const PROCESS_ARGS_START_INDEX = 2;
|
|
11
|
+
const CLI_ENV_MAPPINGS = [
|
|
12
|
+
{ option: ARG_OPTION_MODEL, envVar: 'GEMINI_MODEL' },
|
|
13
|
+
{ option: ARG_OPTION_MAX_DIFF_CHARS, envVar: 'MAX_DIFF_CHARS' },
|
|
14
|
+
];
|
|
15
|
+
const CLI_OPTIONS = {
|
|
16
|
+
[ARG_OPTION_MODEL]: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
short: 'm',
|
|
19
|
+
},
|
|
20
|
+
[ARG_OPTION_MAX_DIFF_CHARS]: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
function setStringEnv(name, value) {
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
process.env[name] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function applyCliEnvironmentOverrides(values) {
|
|
30
|
+
for (const mapping of CLI_ENV_MAPPINGS) {
|
|
31
|
+
setStringEnv(mapping.envVar, values[mapping.option]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function parseCommandLineArgs() {
|
|
35
|
+
const { values } = parseArgs({
|
|
36
|
+
args: process.argv.slice(PROCESS_ARGS_START_INDEX),
|
|
37
|
+
options: CLI_OPTIONS,
|
|
38
|
+
strict: false,
|
|
39
|
+
});
|
|
40
|
+
applyCliEnvironmentOverrides(values);
|
|
41
|
+
}
|
|
7
42
|
let shuttingDown = false;
|
|
8
43
|
async function shutdown(server) {
|
|
9
44
|
if (shuttingDown) {
|
package/dist/lib/diff-store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import type { ParsedFile } from './diff
|
|
2
|
+
import type { ParsedFile } from './diff.js';
|
|
3
3
|
import { createErrorToolResponse } from './tool-response.js';
|
|
4
4
|
export declare const DIFF_RESOURCE_URI = "diff://current";
|
|
5
5
|
export interface DiffStats {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { File as ParsedFile } from 'parse-diff';
|
|
2
|
+
import { createErrorToolResponse } from './tool-response.js';
|
|
3
|
+
export type { ParsedFile };
|
|
4
|
+
export declare function getMaxDiffChars(): number;
|
|
5
|
+
export declare function resetMaxDiffCharsCacheForTesting(): void;
|
|
6
|
+
export declare function exceedsDiffBudget(diff: string): boolean;
|
|
7
|
+
export declare function getDiffBudgetError(diffLength: number, maxChars?: number): string;
|
|
8
|
+
export declare function validateDiffBudget(diff: string): ReturnType<typeof createErrorToolResponse> | undefined;
|
|
9
|
+
export declare const NOISY_EXCLUDE_PATHSPECS: readonly [":(exclude)package-lock.json", ":(exclude)yarn.lock", ":(exclude)pnpm-lock.yaml", ":(exclude)bun.lockb", ":(exclude)*.lock", ":(exclude)dist/", ":(exclude)build/", ":(exclude)out/", ":(exclude).next/", ":(exclude)coverage/", ":(exclude)*.min.js", ":(exclude)*.min.css", ":(exclude)*.map"];
|
|
10
|
+
export declare function cleanDiff(raw: string): string;
|
|
11
|
+
export declare function isEmptyDiff(diff: string): boolean;
|
|
12
|
+
export interface DiffStats {
|
|
13
|
+
files: number;
|
|
14
|
+
added: number;
|
|
15
|
+
deleted: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseDiffFiles(diff: string): ParsedFile[];
|
|
18
|
+
export declare function computeDiffStatsAndSummaryFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
19
|
+
stats: DiffStats;
|
|
20
|
+
summary: string;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function computeDiffStatsAndPathsFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
23
|
+
stats: DiffStats;
|
|
24
|
+
paths: string[];
|
|
25
|
+
}>;
|
|
26
|
+
export declare function extractChangedPathsFromFiles(files: readonly ParsedFile[]): string[];
|
|
27
|
+
export declare function extractChangedPaths(diff: string): string[];
|
|
28
|
+
export declare function computeDiffStatsFromFiles(files: readonly ParsedFile[]): Readonly<DiffStats>;
|
|
29
|
+
export declare function computeDiffStats(diff: string): Readonly<DiffStats>;
|
|
30
|
+
export declare function formatFileSummary(files: ParsedFile[]): string;
|
package/dist/lib/diff.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import parseDiff from 'parse-diff';
|
|
2
|
+
import { createCachedEnvInt } from './env-config.js';
|
|
3
|
+
import { createErrorToolResponse } from './tool-response.js';
|
|
4
|
+
// --- Diff Budget ---
|
|
5
|
+
const DEFAULT_MAX_DIFF_CHARS = 120_000;
|
|
6
|
+
const MAX_DIFF_CHARS_ENV_VAR = 'MAX_DIFF_CHARS';
|
|
7
|
+
const numberFormatter = new Intl.NumberFormat('en-US');
|
|
8
|
+
const diffCharsConfig = createCachedEnvInt(MAX_DIFF_CHARS_ENV_VAR, DEFAULT_MAX_DIFF_CHARS);
|
|
9
|
+
export function getMaxDiffChars() {
|
|
10
|
+
return diffCharsConfig.get();
|
|
11
|
+
}
|
|
12
|
+
export function resetMaxDiffCharsCacheForTesting() {
|
|
13
|
+
diffCharsConfig.reset();
|
|
14
|
+
}
|
|
15
|
+
export function exceedsDiffBudget(diff) {
|
|
16
|
+
return diff.length > getMaxDiffChars();
|
|
17
|
+
}
|
|
18
|
+
export function getDiffBudgetError(diffLength, maxChars = getMaxDiffChars()) {
|
|
19
|
+
return `diff exceeds max allowed size (${numberFormatter.format(diffLength)} chars > ${numberFormatter.format(maxChars)} chars)`;
|
|
20
|
+
}
|
|
21
|
+
const BUDGET_ERROR_META = { retryable: false, kind: 'budget' };
|
|
22
|
+
export function validateDiffBudget(diff) {
|
|
23
|
+
const providedChars = diff.length;
|
|
24
|
+
const maxChars = getMaxDiffChars();
|
|
25
|
+
if (providedChars <= maxChars) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return createErrorToolResponse('E_INPUT_TOO_LARGE', getDiffBudgetError(providedChars, maxChars), { providedChars, maxChars }, BUDGET_ERROR_META);
|
|
29
|
+
}
|
|
30
|
+
// --- Diff Cleaner ---
|
|
31
|
+
export const NOISY_EXCLUDE_PATHSPECS = [
|
|
32
|
+
':(exclude)package-lock.json',
|
|
33
|
+
':(exclude)yarn.lock',
|
|
34
|
+
':(exclude)pnpm-lock.yaml',
|
|
35
|
+
':(exclude)bun.lockb',
|
|
36
|
+
':(exclude)*.lock',
|
|
37
|
+
':(exclude)dist/',
|
|
38
|
+
':(exclude)build/',
|
|
39
|
+
':(exclude)out/',
|
|
40
|
+
':(exclude).next/',
|
|
41
|
+
':(exclude)coverage/',
|
|
42
|
+
':(exclude)*.min.js',
|
|
43
|
+
':(exclude)*.min.css',
|
|
44
|
+
':(exclude)*.map',
|
|
45
|
+
];
|
|
46
|
+
const BINARY_FILE_LINE = /^Binary files .+ differ$/m;
|
|
47
|
+
const GIT_BINARY_PATCH = /^GIT binary patch/m;
|
|
48
|
+
const HAS_HUNK = /^@@/m;
|
|
49
|
+
const HAS_OLD_MODE = /^old mode /m;
|
|
50
|
+
function shouldKeepSection(section) {
|
|
51
|
+
if (!section.trim())
|
|
52
|
+
return false;
|
|
53
|
+
if (BINARY_FILE_LINE.test(section))
|
|
54
|
+
return false;
|
|
55
|
+
if (GIT_BINARY_PATCH.test(section))
|
|
56
|
+
return false;
|
|
57
|
+
if (HAS_OLD_MODE.test(section) && !HAS_HUNK.test(section))
|
|
58
|
+
return false;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
function processSection(raw, start, end, sections) {
|
|
62
|
+
if (end > start) {
|
|
63
|
+
const section = raw.slice(start, end);
|
|
64
|
+
if (shouldKeepSection(section)) {
|
|
65
|
+
sections.push(section);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function cleanDiff(raw) {
|
|
70
|
+
if (!raw)
|
|
71
|
+
return '';
|
|
72
|
+
const sections = [];
|
|
73
|
+
let lastIndex = 0;
|
|
74
|
+
let nextIndex = raw.startsWith('diff --git ')
|
|
75
|
+
? 0
|
|
76
|
+
: raw.indexOf('\ndiff --git ');
|
|
77
|
+
if (nextIndex === -1) {
|
|
78
|
+
processSection(raw, 0, raw.length, sections);
|
|
79
|
+
return sections.join('').trim();
|
|
80
|
+
}
|
|
81
|
+
while (nextIndex !== -1) {
|
|
82
|
+
const matchIndex = nextIndex === 0 ? 0 : nextIndex + 1; // +1 to skip \n
|
|
83
|
+
processSection(raw, lastIndex, matchIndex, sections);
|
|
84
|
+
lastIndex = matchIndex;
|
|
85
|
+
nextIndex = raw.indexOf('\ndiff --git ', lastIndex);
|
|
86
|
+
}
|
|
87
|
+
processSection(raw, lastIndex, raw.length, sections);
|
|
88
|
+
return sections.join('').trim();
|
|
89
|
+
}
|
|
90
|
+
export function isEmptyDiff(diff) {
|
|
91
|
+
return diff.trim().length === 0;
|
|
92
|
+
}
|
|
93
|
+
// --- Diff Parser ---
|
|
94
|
+
const UNKNOWN_PATH = 'unknown';
|
|
95
|
+
const NO_FILES_CHANGED = 'No files changed.';
|
|
96
|
+
const EMPTY_PATHS = [];
|
|
97
|
+
const EMPTY_STATS = Object.freeze({ files: 0, added: 0, deleted: 0 });
|
|
98
|
+
const PATH_SORTER = (left, right) => left.localeCompare(right);
|
|
99
|
+
export function parseDiffFiles(diff) {
|
|
100
|
+
return diff ? parseDiff(diff) : [];
|
|
101
|
+
}
|
|
102
|
+
function cleanPath(path) {
|
|
103
|
+
if (path.startsWith('a/') || path.startsWith('b/')) {
|
|
104
|
+
return path.slice(2);
|
|
105
|
+
}
|
|
106
|
+
return path;
|
|
107
|
+
}
|
|
108
|
+
function resolveChangedPath(file) {
|
|
109
|
+
if (file.to && file.to !== '/dev/null')
|
|
110
|
+
return cleanPath(file.to);
|
|
111
|
+
if (file.from && file.from !== '/dev/null')
|
|
112
|
+
return cleanPath(file.from);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
function sortPaths(paths) {
|
|
116
|
+
return Array.from(paths).sort(PATH_SORTER);
|
|
117
|
+
}
|
|
118
|
+
function calculateStats(files) {
|
|
119
|
+
let added = 0;
|
|
120
|
+
let deleted = 0;
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
added += file.additions;
|
|
123
|
+
deleted += file.deletions;
|
|
124
|
+
}
|
|
125
|
+
return { files: files.length, added, deleted };
|
|
126
|
+
}
|
|
127
|
+
function getUniquePaths(files) {
|
|
128
|
+
const paths = new Set();
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const path = resolveChangedPath(file);
|
|
131
|
+
if (path)
|
|
132
|
+
paths.add(path);
|
|
133
|
+
}
|
|
134
|
+
return paths;
|
|
135
|
+
}
|
|
136
|
+
export function computeDiffStatsAndSummaryFromFiles(files) {
|
|
137
|
+
if (files.length === 0) {
|
|
138
|
+
return { stats: EMPTY_STATS, summary: NO_FILES_CHANGED };
|
|
139
|
+
}
|
|
140
|
+
let added = 0;
|
|
141
|
+
let deleted = 0;
|
|
142
|
+
const summaries = [];
|
|
143
|
+
const MAX_SUMMARY_FILES = 40;
|
|
144
|
+
let i = 0;
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
added += file.additions;
|
|
147
|
+
deleted += file.deletions;
|
|
148
|
+
if (i < MAX_SUMMARY_FILES) {
|
|
149
|
+
const path = resolveChangedPath(file) ?? UNKNOWN_PATH;
|
|
150
|
+
summaries.push(`${path} (+${file.additions} -${file.deletions})`);
|
|
151
|
+
}
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
if (files.length > MAX_SUMMARY_FILES) {
|
|
155
|
+
summaries.push(`... and ${files.length - MAX_SUMMARY_FILES} more files`);
|
|
156
|
+
}
|
|
157
|
+
const stats = { files: files.length, added, deleted };
|
|
158
|
+
return {
|
|
159
|
+
stats,
|
|
160
|
+
summary: `${summaries.join(', ')} [${stats.files} files, +${stats.added} -${stats.deleted}]`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export function computeDiffStatsAndPathsFromFiles(files) {
|
|
164
|
+
if (files.length === 0) {
|
|
165
|
+
return { stats: EMPTY_STATS, paths: EMPTY_PATHS };
|
|
166
|
+
}
|
|
167
|
+
const stats = calculateStats(files);
|
|
168
|
+
const paths = sortPaths(getUniquePaths(files));
|
|
169
|
+
return { stats, paths };
|
|
170
|
+
}
|
|
171
|
+
export function extractChangedPathsFromFiles(files) {
|
|
172
|
+
if (files.length === 0)
|
|
173
|
+
return EMPTY_PATHS;
|
|
174
|
+
return sortPaths(getUniquePaths(files));
|
|
175
|
+
}
|
|
176
|
+
export function extractChangedPaths(diff) {
|
|
177
|
+
return extractChangedPathsFromFiles(parseDiffFiles(diff));
|
|
178
|
+
}
|
|
179
|
+
export function computeDiffStatsFromFiles(files) {
|
|
180
|
+
if (files.length === 0)
|
|
181
|
+
return EMPTY_STATS;
|
|
182
|
+
return calculateStats(files);
|
|
183
|
+
}
|
|
184
|
+
export function computeDiffStats(diff) {
|
|
185
|
+
return computeDiffStatsFromFiles(parseDiffFiles(diff));
|
|
186
|
+
}
|
|
187
|
+
export function formatFileSummary(files) {
|
|
188
|
+
return computeDiffStatsAndSummaryFromFiles(files).summary;
|
|
189
|
+
}
|
package/dist/lib/gemini.js
CHANGED
|
@@ -522,24 +522,28 @@ async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestS
|
|
|
522
522
|
return;
|
|
523
523
|
settled = true;
|
|
524
524
|
clearTimeout(deadlineTimer);
|
|
525
|
-
|
|
526
|
-
requestSignal.removeEventListener('abort', onAbort);
|
|
527
|
-
}
|
|
525
|
+
detachAbortListener();
|
|
528
526
|
acquireSlot();
|
|
529
527
|
resolve();
|
|
530
528
|
};
|
|
531
529
|
waiters.push(waiter);
|
|
532
|
-
const
|
|
533
|
-
if (settled)
|
|
534
|
-
return;
|
|
535
|
-
settled = true;
|
|
530
|
+
const removeWaiter = () => {
|
|
536
531
|
const idx = waiters.indexOf(waiter);
|
|
537
532
|
if (idx !== -1) {
|
|
538
533
|
waiters.splice(idx, 1);
|
|
539
534
|
}
|
|
535
|
+
};
|
|
536
|
+
const detachAbortListener = () => {
|
|
540
537
|
if (requestSignal) {
|
|
541
538
|
requestSignal.removeEventListener('abort', onAbort);
|
|
542
539
|
}
|
|
540
|
+
};
|
|
541
|
+
const deadlineTimer = setTimeout(() => {
|
|
542
|
+
if (settled)
|
|
543
|
+
return;
|
|
544
|
+
settled = true;
|
|
545
|
+
removeWaiter();
|
|
546
|
+
detachAbortListener();
|
|
543
547
|
reject(new Error(formatConcurrencyLimitErrorMessage(limit, waitLimitMs)));
|
|
544
548
|
}, waitLimitMs);
|
|
545
549
|
deadlineTimer.unref();
|
|
@@ -547,10 +551,7 @@ async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestS
|
|
|
547
551
|
if (settled)
|
|
548
552
|
return;
|
|
549
553
|
settled = true;
|
|
550
|
-
|
|
551
|
-
if (idx !== -1) {
|
|
552
|
-
waiters.splice(idx, 1);
|
|
553
|
-
}
|
|
554
|
+
removeWaiter();
|
|
554
555
|
clearTimeout(deadlineTimer);
|
|
555
556
|
reject(new Error('Gemini request was cancelled.'));
|
|
556
557
|
};
|
|
@@ -103,7 +103,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
103
103
|
readonly crossToolFlow: readonly ["Use before deep review to decide whether Pro analysis is needed."];
|
|
104
104
|
}, {
|
|
105
105
|
readonly name: "inspect_code_quality";
|
|
106
|
-
readonly purpose: "Deep code review over the cached diff
|
|
106
|
+
readonly purpose: "Deep code review over the cached diff.";
|
|
107
107
|
readonly model: "gemini-3-pro-preview";
|
|
108
108
|
readonly timeoutMs: 120000;
|
|
109
109
|
readonly thinkingLevel: "high";
|
|
@@ -134,17 +134,11 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
134
134
|
readonly required: false;
|
|
135
135
|
readonly constraints: "1-25";
|
|
136
136
|
readonly description: "Post-generation cap applied to findings.";
|
|
137
|
-
}, {
|
|
138
|
-
readonly name: "files";
|
|
139
|
-
readonly type: "object[]";
|
|
140
|
-
readonly required: false;
|
|
141
|
-
readonly constraints: "1-20 files, 100K chars/file";
|
|
142
|
-
readonly description: "Optional short excerpts for supplementary context only; avoid full files — the diff is the primary source.";
|
|
143
137
|
}];
|
|
144
138
|
readonly outputShape: "{summary, overallRisk, findings[], testsNeeded[], contextualInsights[], totalFindings}";
|
|
145
|
-
readonly gotchas: readonly ["Requires generate_diff to be called first.", "
|
|
139
|
+
readonly gotchas: readonly ["Requires generate_diff to be called first.", "maxFindings caps output after generation."];
|
|
146
140
|
readonly crossToolFlow: readonly ["findings[].title -> suggest_search_replace.findingTitle", "findings[].explanation -> suggest_search_replace.findingDetails"];
|
|
147
|
-
readonly constraints: readonly ["
|
|
141
|
+
readonly constraints: readonly ["Diff budget bounded by MAX_DIFF_CHARS."];
|
|
148
142
|
}, {
|
|
149
143
|
readonly name: "suggest_search_replace";
|
|
150
144
|
readonly purpose: "Generate verbatim search/replace fix blocks for one finding.";
|
|
@@ -118,7 +118,7 @@ export const TOOL_CONTRACTS = [
|
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
name: 'inspect_code_quality',
|
|
121
|
-
purpose: 'Deep code review over the cached diff
|
|
121
|
+
purpose: 'Deep code review over the cached diff.',
|
|
122
122
|
model: PRO_MODEL,
|
|
123
123
|
timeoutMs: DEFAULT_TIMEOUT_PRO_MS,
|
|
124
124
|
thinkingLevel: PRO_THINKING_LEVEL,
|
|
@@ -154,26 +154,17 @@ export const TOOL_CONTRACTS = [
|
|
|
154
154
|
constraints: '1-25',
|
|
155
155
|
description: 'Post-generation cap applied to findings.',
|
|
156
156
|
},
|
|
157
|
-
{
|
|
158
|
-
name: 'files',
|
|
159
|
-
type: 'object[]',
|
|
160
|
-
required: false,
|
|
161
|
-
constraints: '1-20 files, 100K chars/file',
|
|
162
|
-
description: 'Optional short excerpts for supplementary context only; avoid full files — the diff is the primary source.',
|
|
163
|
-
},
|
|
164
157
|
],
|
|
165
158
|
outputShape: '{summary, overallRisk, findings[], testsNeeded[], contextualInsights[], totalFindings}',
|
|
166
159
|
gotchas: [
|
|
167
160
|
'Requires generate_diff to be called first.',
|
|
168
|
-
'Combined diff + file context is bounded by MAX_CONTEXT_CHARS.',
|
|
169
161
|
'maxFindings caps output after generation.',
|
|
170
|
-
'files[] is token-expensive — omit unless the diff lacks critical structural context (e.g. class hierarchy, imports). Never pass full files.',
|
|
171
162
|
],
|
|
172
163
|
crossToolFlow: [
|
|
173
164
|
'findings[].title -> suggest_search_replace.findingTitle',
|
|
174
165
|
'findings[].explanation -> suggest_search_replace.findingDetails',
|
|
175
166
|
],
|
|
176
|
-
constraints: ['
|
|
167
|
+
constraints: ['Diff budget bounded by MAX_DIFF_CHARS.'],
|
|
177
168
|
},
|
|
178
169
|
{
|
|
179
170
|
name: 'suggest_search_replace',
|
package/dist/lib/tool-factory.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { DefaultOutputSchema } from '../schemas/outputs.js';
|
|
3
|
-
import { validateDiffBudget } from './diff-budget.js';
|
|
4
3
|
import { createNoDiffError, getDiff } from './diff-store.js';
|
|
4
|
+
import { validateDiffBudget } from './diff.js';
|
|
5
5
|
import { createCachedEnvInt } from './env-config.js';
|
|
6
6
|
import { getErrorMessage, RETRYABLE_UPSTREAM_ERROR_PATTERN } from './errors.js';
|
|
7
7
|
import { stripJsonSchemaConstraints } from './gemini-schema.js';
|
|
@@ -28,6 +28,23 @@ const DEFAULT_SCHEMA_RETRY_ERROR_CHARS = 1_500;
|
|
|
28
28
|
const schemaRetryErrorCharsConfig = createCachedEnvInt('MAX_SCHEMA_RETRY_ERROR_CHARS', DEFAULT_SCHEMA_RETRY_ERROR_CHARS);
|
|
29
29
|
const DETERMINISTIC_JSON_RETRY_NOTE = 'Deterministic JSON mode: keep key names exactly as schema-defined and preserve stable field ordering.';
|
|
30
30
|
const JSON_PARSE_ERROR_PATTERN = /model produced invalid json/i;
|
|
31
|
+
function buildToolAnnotations(annotations) {
|
|
32
|
+
if (!annotations) {
|
|
33
|
+
return {
|
|
34
|
+
readOnlyHint: true,
|
|
35
|
+
idempotentHint: true,
|
|
36
|
+
openWorldHint: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const annotationOverrides = { ...annotations };
|
|
40
|
+
delete annotationOverrides.destructiveHint;
|
|
41
|
+
return {
|
|
42
|
+
readOnlyHint: !annotations.destructiveHint,
|
|
43
|
+
idempotentHint: !annotations.destructiveHint,
|
|
44
|
+
openWorldHint: true,
|
|
45
|
+
...annotationOverrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
31
48
|
function createGeminiResponseSchema(config) {
|
|
32
49
|
const sourceSchema = config.geminiSchema ?? config.resultSchema;
|
|
33
50
|
return stripJsonSchemaConstraints(z.toJSONSchema(sourceSchema));
|
|
@@ -225,7 +242,6 @@ function createFailureStatusMessage(outcome, errorMessage) {
|
|
|
225
242
|
async function sendSingleStepProgress(extra, toolName, context, current, state) {
|
|
226
243
|
await sendTaskProgress(extra, {
|
|
227
244
|
current,
|
|
228
|
-
total: 1,
|
|
229
245
|
message: current === 0
|
|
230
246
|
? formatProgressStep(toolName, context, state)
|
|
231
247
|
: formatProgressCompletion(toolName, context, state),
|
|
@@ -234,14 +250,12 @@ async function sendSingleStepProgress(extra, toolName, context, current, state)
|
|
|
234
250
|
async function reportProgressStepUpdate(reportProgress, toolName, context, current, metadata) {
|
|
235
251
|
await reportProgress({
|
|
236
252
|
current,
|
|
237
|
-
total: TASK_PROGRESS_TOTAL,
|
|
238
253
|
message: formatProgressStep(toolName, context, metadata),
|
|
239
254
|
});
|
|
240
255
|
}
|
|
241
256
|
async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome) {
|
|
242
257
|
await reportProgress({
|
|
243
258
|
current: TASK_PROGRESS_TOTAL,
|
|
244
|
-
total: TASK_PROGRESS_TOTAL,
|
|
245
259
|
message: formatProgressCompletion(toolName, context, outcome),
|
|
246
260
|
});
|
|
247
261
|
}
|
|
@@ -515,19 +529,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
515
529
|
description: config.description,
|
|
516
530
|
inputSchema: config.inputSchema,
|
|
517
531
|
outputSchema: DefaultOutputSchema,
|
|
518
|
-
annotations:
|
|
519
|
-
readOnlyHint: !config.annotations?.destructiveHint,
|
|
520
|
-
idempotentHint: !config.annotations?.destructiveHint,
|
|
521
|
-
openWorldHint: true,
|
|
522
|
-
...(() => {
|
|
523
|
-
if (!config.annotations) {
|
|
524
|
-
return {};
|
|
525
|
-
}
|
|
526
|
-
const annotationOverrides = { ...config.annotations };
|
|
527
|
-
delete annotationOverrides.destructiveHint;
|
|
528
|
-
return annotationOverrides;
|
|
529
|
-
})(),
|
|
530
|
-
},
|
|
532
|
+
annotations: buildToolAnnotations(config.annotations),
|
|
531
533
|
}, {
|
|
532
534
|
createTask: async (input, extra) => {
|
|
533
535
|
const task = await extra.taskStore.createTask({
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
export type JsonObject = Record<string, unknown>;
|
|
2
2
|
export type GeminiLogHandler = (level: string, data: unknown) => Promise<void>;
|
|
3
|
-
export interface GeminiFunctionCallingContext {
|
|
4
|
-
readonly modelParts: readonly unknown[];
|
|
5
|
-
}
|
|
6
3
|
export interface GeminiRequestExecutionOptions {
|
|
7
4
|
maxRetries?: number;
|
|
8
5
|
timeoutMs?: number;
|
|
@@ -13,7 +10,6 @@ export interface GeminiRequestExecutionOptions {
|
|
|
13
10
|
signal?: AbortSignal;
|
|
14
11
|
onLog?: GeminiLogHandler;
|
|
15
12
|
responseKeyOrdering?: readonly string[];
|
|
16
|
-
functionCallingContext?: GeminiFunctionCallingContext;
|
|
17
13
|
batchMode?: 'off' | 'inline';
|
|
18
14
|
}
|
|
19
15
|
export interface GeminiStructuredRequestOptions extends GeminiRequestExecutionOptions {
|
|
@@ -2,7 +2,6 @@ import { createCachedEnvInt } from '../lib/env-config.js';
|
|
|
2
2
|
import { FLASH_MODEL } from '../lib/model-config.js';
|
|
3
3
|
import { getToolContracts } from '../lib/tool-contracts.js';
|
|
4
4
|
const DEFAULT_MAX_DIFF_CHARS = 120_000;
|
|
5
|
-
const DEFAULT_MAX_CONTEXT_CHARS = 500_000;
|
|
6
5
|
const DEFAULT_MAX_CONCURRENT_CALLS = 10;
|
|
7
6
|
const DEFAULT_CONCURRENT_WAIT_MS = 2_000;
|
|
8
7
|
const DEFAULT_SAFETY_THRESHOLD = 'BLOCK_NONE';
|
|
@@ -10,7 +9,6 @@ const GEMINI_HARM_BLOCK_THRESHOLD_ENV_VAR = 'GEMINI_HARM_BLOCK_THRESHOLD';
|
|
|
10
9
|
const GEMINI_MODEL_ENV_VAR = 'GEMINI_MODEL';
|
|
11
10
|
const GEMINI_BATCH_MODE_ENV_VAR = 'GEMINI_BATCH_MODE';
|
|
12
11
|
const diffCharsConfig = createCachedEnvInt('MAX_DIFF_CHARS', DEFAULT_MAX_DIFF_CHARS);
|
|
13
|
-
const contextCharsConfig = createCachedEnvInt('MAX_CONTEXT_CHARS', DEFAULT_MAX_CONTEXT_CHARS);
|
|
14
12
|
const concurrentCallsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS', DEFAULT_MAX_CONCURRENT_CALLS);
|
|
15
13
|
const concurrentBatchCallsConfig = createCachedEnvInt('MAX_CONCURRENT_BATCH_CALLS', 2);
|
|
16
14
|
const concurrentWaitConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS_WAIT_MS', DEFAULT_CONCURRENT_WAIT_MS);
|
|
@@ -34,7 +32,6 @@ function formatThinkingLevel(level) {
|
|
|
34
32
|
}
|
|
35
33
|
export function buildServerConfig() {
|
|
36
34
|
const maxDiffChars = diffCharsConfig.get();
|
|
37
|
-
const maxContextChars = contextCharsConfig.get();
|
|
38
35
|
const maxConcurrent = concurrentCallsConfig.get();
|
|
39
36
|
const maxConcurrentBatch = concurrentBatchCallsConfig.get();
|
|
40
37
|
const concurrentWaitMs = concurrentWaitConfig.get();
|
|
@@ -54,7 +51,6 @@ export function buildServerConfig() {
|
|
|
54
51
|
| Limit | Value | Env |
|
|
55
52
|
|-------|-------|-----|
|
|
56
53
|
| Diff limit | ${formatNumber(maxDiffChars)} chars | \`MAX_DIFF_CHARS\` |
|
|
57
|
-
| Context limit (inspect) | ${formatNumber(maxContextChars)} chars | \`MAX_CONTEXT_CHARS\` |
|
|
58
54
|
| Concurrency limit | ${maxConcurrent} | \`MAX_CONCURRENT_CALLS\` |
|
|
59
55
|
| Batch concurrency limit | ${maxConcurrentBatch} | \`MAX_CONCURRENT_BATCH_CALLS\` |
|
|
60
56
|
| Wait timeout | ${formatNumber(concurrentWaitMs)}ms | \`MAX_CONCURRENT_CALLS_WAIT_MS\` |
|
|
@@ -6,7 +6,6 @@ const TOOL_CATALOG_CONTENT = `# Tool Catalog Details
|
|
|
6
6
|
- \`language\`: Primary language hint (auto-detects). All tools except \`suggest_search_replace\`.
|
|
7
7
|
- \`focusAreas\`: Focus tags (security, performance, etc.). \`inspect_code_quality\` only.
|
|
8
8
|
- \`maxFindings\`: Output cap (1–25). \`inspect_code_quality\` only.
|
|
9
|
-
- \`files\`: File context (max 20 files, 100K chars/file). \`inspect_code_quality\` only.
|
|
10
9
|
- \`testFramework\`: Framework hint. \`generate_test_plan\` only.
|
|
11
10
|
- \`maxTestCases\`: Output cap (1–30). \`generate_test_plan\` only.
|
|
12
11
|
|
package/dist/schemas/inputs.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export declare const FileContextSchema: z.ZodObject<{
|
|
3
|
-
path: z.ZodString;
|
|
4
|
-
content: z.ZodString;
|
|
5
|
-
}, z.core.$strict>;
|
|
6
2
|
export declare const AnalyzePrImpactInputSchema: z.ZodObject<{
|
|
7
3
|
repository: z.ZodString;
|
|
8
4
|
language: z.ZodOptional<z.ZodString>;
|
|
@@ -16,10 +12,6 @@ export declare const InspectCodeQualityInputSchema: z.ZodObject<{
|
|
|
16
12
|
language: z.ZodOptional<z.ZodString>;
|
|
17
13
|
focusAreas: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
18
14
|
maxFindings: z.ZodOptional<z.ZodNumber>;
|
|
19
|
-
files: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
20
|
-
path: z.ZodString;
|
|
21
|
-
content: z.ZodString;
|
|
22
|
-
}, z.core.$strict>>>;
|
|
23
15
|
}, z.core.$strict>;
|
|
24
16
|
export declare const SuggestSearchReplaceInputSchema: z.ZodObject<{
|
|
25
17
|
findingTitle: z.ZodString;
|
package/dist/schemas/inputs.js
CHANGED
|
@@ -2,11 +2,6 @@ import { z } from 'zod';
|
|
|
2
2
|
const INPUT_LIMITS = {
|
|
3
3
|
repository: { min: 1, max: 200 },
|
|
4
4
|
language: { min: 2, max: 32 },
|
|
5
|
-
fileContext: {
|
|
6
|
-
path: { min: 1, max: 500 },
|
|
7
|
-
content: { min: 0, max: 100_000 },
|
|
8
|
-
maxItems: 20,
|
|
9
|
-
},
|
|
10
5
|
focusArea: { min: 2, max: 80, maxItems: 12 },
|
|
11
6
|
maxFindings: { min: 1, max: 25 },
|
|
12
7
|
findingTitle: { min: 3, max: 160 },
|
|
@@ -31,10 +26,6 @@ function createRepositorySchema() {
|
|
|
31
26
|
function createOptionalBoundedInteger(min, max, description) {
|
|
32
27
|
return z.number().int().min(min).max(max).optional().describe(description);
|
|
33
28
|
}
|
|
34
|
-
export const FileContextSchema = z.strictObject({
|
|
35
|
-
path: createBoundedString(INPUT_LIMITS.fileContext.path.min, INPUT_LIMITS.fileContext.path.max, 'Repo-relative path (e.g. src/utils.ts).'),
|
|
36
|
-
content: createBoundedString(INPUT_LIMITS.fileContext.content.min, INPUT_LIMITS.fileContext.content.max, 'Relevant excerpt (changed lines + context). Avoid full files.'),
|
|
37
|
-
});
|
|
38
29
|
export const AnalyzePrImpactInputSchema = z.strictObject({
|
|
39
30
|
repository: createRepositorySchema(),
|
|
40
31
|
language: createLanguageSchema(),
|
|
@@ -53,12 +44,6 @@ export const InspectCodeQualityInputSchema = z.strictObject({
|
|
|
53
44
|
.optional()
|
|
54
45
|
.describe('Review focus areas. Tags: security, correctness, performance, regressions, tests, maintainability, concurrency.'),
|
|
55
46
|
maxFindings: createOptionalBoundedInteger(INPUT_LIMITS.maxFindings.min, INPUT_LIMITS.maxFindings.max, 'Max findings (1-25). Default: 10.'),
|
|
56
|
-
files: z
|
|
57
|
-
.array(FileContextSchema)
|
|
58
|
-
.min(1)
|
|
59
|
-
.max(INPUT_LIMITS.fileContext.maxItems)
|
|
60
|
-
.optional()
|
|
61
|
-
.describe('File content (changed files). Omit if large. Diff is primary source.'),
|
|
62
47
|
});
|
|
63
48
|
export const SuggestSearchReplaceInputSchema = z.strictObject({
|
|
64
49
|
findingTitle: createBoundedString(INPUT_LIMITS.findingTitle.min, INPUT_LIMITS.findingTitle.max, 'Exact finding title from inspect_code_quality.'),
|
package/dist/schemas/outputs.js
CHANGED
|
@@ -108,7 +108,7 @@ const CODE_QUALITY_SHARED_FIELDS = {
|
|
|
108
108
|
.max(30)
|
|
109
109
|
.describe('Findings (severity desc).'),
|
|
110
110
|
testsNeeded: createBoundedStringArray(1, 300, 0, 12, 'Test cases needed to validate this change.'),
|
|
111
|
-
contextualInsights: createBoundedStringArray(1, 500, 0, 5, 'Cross-file insights
|
|
111
|
+
contextualInsights: createBoundedStringArray(1, 500, 0, 5, 'Cross-file insights from diff analysis.'),
|
|
112
112
|
};
|
|
113
113
|
export const PrImpactResultSchema = z.strictObject({
|
|
114
114
|
severity: z.enum(QUALITY_RISK_LEVELS).describe('Overall severity.'),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff
|
|
1
|
+
import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff.js';
|
|
2
2
|
import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
|
|
3
3
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
4
4
|
import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { cleanDiff, isEmptyDiff, NOISY_EXCLUDE_PATHSPECS, } from '../lib/diff-cleaner.js';
|
|
5
|
-
import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
6
4
|
import { DIFF_RESOURCE_URI, storeDiff } from '../lib/diff-store.js';
|
|
5
|
+
import { cleanDiff, computeDiffStatsFromFiles, isEmptyDiff, NOISY_EXCLUDE_PATHSPECS, parseDiffFiles, } from '../lib/diff.js';
|
|
7
6
|
import { wrapToolHandler } from '../lib/tool-factory.js';
|
|
8
7
|
import { createErrorToolResponse, createToolResponse, } from '../lib/tool-response.js';
|
|
9
8
|
import { DefaultOutputSchema } from '../schemas/outputs.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computeDiffStatsAndPathsFromFiles } from '../lib/diff
|
|
1
|
+
import { computeDiffStatsAndPathsFromFiles } from '../lib/diff.js';
|
|
2
2
|
import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
|
|
3
3
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
4
4
|
import { GenerateTestPlanInputSchema } from '../schemas/inputs.js';
|
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
export declare function sanitizePath(path: string): string;
|
|
3
|
-
export declare function sanitizeContent(content: string): string;
|
|
4
2
|
export declare function registerInspectCodeQualityTool(server: McpServer): void;
|
|
@@ -1,61 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff-parser.js';
|
|
1
|
+
import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff.js';
|
|
3
2
|
import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
|
|
4
3
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
4
|
import { InspectCodeQualityInputSchema } from '../schemas/inputs.js';
|
|
6
5
|
import { CodeQualityOutputSchema, CodeQualityResultSchema, } from '../schemas/outputs.js';
|
|
7
6
|
const DEFAULT_FOCUS_AREAS = 'General';
|
|
8
|
-
const FILE_CONTEXT_HEADING = '\nFull File Context:\n';
|
|
9
|
-
const PATH_ESCAPE_REPLACEMENTS = {
|
|
10
|
-
'"': '\\"',
|
|
11
|
-
'\n': ' ',
|
|
12
|
-
'\r': ' ',
|
|
13
|
-
'<': '<',
|
|
14
|
-
'>': '>',
|
|
15
|
-
};
|
|
16
|
-
const PATH_ESCAPE_PATTERN = /["\n\r<>]/g;
|
|
17
7
|
const SYSTEM_INSTRUCTION = `
|
|
18
8
|
Principal Engineer Code Review.
|
|
19
|
-
Source: Unified diff
|
|
9
|
+
Source: Unified diff.
|
|
20
10
|
Goal: Identify bugs, security, performance, maintainability.
|
|
21
11
|
Constraint: Ignore style/formatting. Prioritize correctness/failure modes.
|
|
22
12
|
Return strict JSON.
|
|
23
13
|
`;
|
|
24
14
|
const TOOL_CONTRACT = requireToolContract('inspect_code_quality');
|
|
25
|
-
export function sanitizePath(path) {
|
|
26
|
-
return path.replace(PATH_ESCAPE_PATTERN, (match) => {
|
|
27
|
-
return PATH_ESCAPE_REPLACEMENTS[match];
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
export function sanitizeContent(content) {
|
|
31
|
-
return content
|
|
32
|
-
.replaceAll('<<END_FILE>>', '<END_FILE_ESCAPED>')
|
|
33
|
-
.replaceAll('<<FILE', '<FILE');
|
|
34
|
-
}
|
|
35
15
|
function formatOptionalLine(label, value) {
|
|
36
16
|
return value === undefined ? '' : `\n${label}: ${value}`;
|
|
37
17
|
}
|
|
38
18
|
function capFindings(findings, maxFindings) {
|
|
39
19
|
return findings.slice(0, maxFindings ?? findings.length);
|
|
40
20
|
}
|
|
41
|
-
function formatFileContext(files) {
|
|
42
|
-
if (!files || files.length === 0) {
|
|
43
|
-
return '';
|
|
44
|
-
}
|
|
45
|
-
return (FILE_CONTEXT_HEADING +
|
|
46
|
-
files
|
|
47
|
-
.map((file) => `
|
|
48
|
-
<<FILE path="${sanitizePath(file.path)}">>
|
|
49
|
-
${sanitizeContent(file.content)}
|
|
50
|
-
<<END_FILE>>
|
|
51
|
-
`)
|
|
52
|
-
.join('\n'));
|
|
53
|
-
}
|
|
54
21
|
export function registerInspectCodeQualityTool(server) {
|
|
55
22
|
registerStructuredToolTask(server, {
|
|
56
23
|
name: 'inspect_code_quality',
|
|
57
24
|
title: 'Inspect Code Quality',
|
|
58
|
-
description: 'Deep code review. Prerequisite: generate_diff. Auto-infer repo/language/focus. Operates
|
|
25
|
+
description: 'Deep code review. Prerequisite: generate_diff. Auto-infer repo/language/focus. Operates on the cached diff.',
|
|
59
26
|
inputSchema: InspectCodeQualityInputSchema,
|
|
60
27
|
fullInputSchema: InspectCodeQualityInputSchema,
|
|
61
28
|
resultSchema: CodeQualityOutputSchema,
|
|
@@ -65,15 +32,7 @@ export function registerInspectCodeQualityTool(server) {
|
|
|
65
32
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
66
33
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
67
34
|
...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
|
|
68
|
-
progressContext: (input) => {
|
|
69
|
-
const fileCount = input.files?.length;
|
|
70
|
-
return fileCount ? `+${fileCount} files` : '';
|
|
71
|
-
},
|
|
72
35
|
requiresDiff: true,
|
|
73
|
-
validateInput: (input, ctx) => {
|
|
74
|
-
// Diff presence and budget checked by requiresDiff: true
|
|
75
|
-
return validateContextBudget(ctx.diffSlot?.diff ?? '', input.files);
|
|
76
|
-
},
|
|
77
36
|
formatOutcome: (result) => `${result.findings.length} findings, risk: ${result.overallRisk}`,
|
|
78
37
|
formatOutput: (result) => {
|
|
79
38
|
const count = result.findings.length;
|
|
@@ -90,25 +49,20 @@ export function registerInspectCodeQualityTool(server) {
|
|
|
90
49
|
const diff = ctx.diffSlot?.diff ?? '';
|
|
91
50
|
const parsedFiles = ctx.diffSlot?.parsedFiles ?? [];
|
|
92
51
|
const { summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(parsedFiles);
|
|
93
|
-
const fileContext = formatFileContext(input.files);
|
|
94
52
|
const languageLine = formatOptionalLine('Language', input.language);
|
|
95
53
|
const maxFindingsLine = formatOptionalLine('Max Findings', input.maxFindings);
|
|
96
|
-
const noFilesNote = !input.files?.length
|
|
97
|
-
? '\nNote: No file excerpts provided. Review based on diff only; leave contextualInsights empty.'
|
|
98
|
-
: '';
|
|
99
54
|
return {
|
|
100
55
|
systemInstruction: SYSTEM_INSTRUCTION,
|
|
101
56
|
prompt: `
|
|
102
57
|
Repository: ${input.repository}${languageLine}
|
|
103
|
-
Focus Areas: ${input.focusAreas?.join(', ') ?? DEFAULT_FOCUS_AREAS}${maxFindingsLine}
|
|
58
|
+
Focus Areas: ${input.focusAreas?.join(', ') ?? DEFAULT_FOCUS_AREAS}${maxFindingsLine}
|
|
104
59
|
Changed Files:
|
|
105
60
|
${fileSummary}
|
|
106
61
|
|
|
107
62
|
Diff:
|
|
108
63
|
${diff}
|
|
109
|
-
${fileContext}
|
|
110
64
|
|
|
111
|
-
Based on the diff
|
|
65
|
+
Based on the diff above, perform a deep code review focusing on the specified areas.
|
|
112
66
|
`,
|
|
113
67
|
};
|
|
114
68
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extractChangedPathsFromFiles } from '../lib/diff
|
|
1
|
+
import { extractChangedPathsFromFiles } from '../lib/diff.js';
|
|
2
2
|
import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
|
|
3
3
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
4
4
|
import { SuggestSearchReplaceInputSchema } from '../schemas/inputs.js';
|
package/package.json
CHANGED
package/dist/lib/cli.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function parseCommandLineArgs(): void;
|
package/dist/lib/cli.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { parseArgs } from 'node:util';
|
|
2
|
-
const ARG_OPTION_MODEL = 'model';
|
|
3
|
-
const ARG_OPTION_MAX_DIFF_CHARS = 'max-diff-chars';
|
|
4
|
-
const PROCESS_ARGS_START_INDEX = 2;
|
|
5
|
-
const CLI_ENV_MAPPINGS = [
|
|
6
|
-
{ option: ARG_OPTION_MODEL, envVar: 'GEMINI_MODEL' },
|
|
7
|
-
{ option: ARG_OPTION_MAX_DIFF_CHARS, envVar: 'MAX_DIFF_CHARS' },
|
|
8
|
-
];
|
|
9
|
-
const CLI_OPTIONS = {
|
|
10
|
-
[ARG_OPTION_MODEL]: {
|
|
11
|
-
type: 'string',
|
|
12
|
-
short: 'm',
|
|
13
|
-
},
|
|
14
|
-
[ARG_OPTION_MAX_DIFF_CHARS]: {
|
|
15
|
-
type: 'string',
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
function setStringEnv(name, value) {
|
|
19
|
-
if (typeof value === 'string') {
|
|
20
|
-
process.env[name] = value;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function applyCliEnvironmentOverrides(values) {
|
|
24
|
-
for (const mapping of CLI_ENV_MAPPINGS) {
|
|
25
|
-
setStringEnv(mapping.envVar, values[mapping.option]);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export function parseCommandLineArgs() {
|
|
29
|
-
const { values } = parseArgs({
|
|
30
|
-
args: process.argv.slice(PROCESS_ARGS_START_INDEX),
|
|
31
|
-
options: CLI_OPTIONS,
|
|
32
|
-
strict: false,
|
|
33
|
-
});
|
|
34
|
-
applyCliEnvironmentOverrides(values);
|
|
35
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { createErrorToolResponse } from './tool-response.js';
|
|
2
|
-
interface FileContent {
|
|
3
|
-
content: string;
|
|
4
|
-
}
|
|
5
|
-
export declare function resetMaxContextCharsCacheForTesting(): void;
|
|
6
|
-
export declare function computeContextSize(diff: string, files?: readonly FileContent[]): number;
|
|
7
|
-
export declare function validateContextBudget(diff: string, files?: readonly FileContent[]): ReturnType<typeof createErrorToolResponse> | undefined;
|
|
8
|
-
export {};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { createCachedEnvInt } from './env-config.js';
|
|
2
|
-
import { createErrorToolResponse } from './tool-response.js';
|
|
3
|
-
const DEFAULT_MAX_CONTEXT_CHARS = 500_000;
|
|
4
|
-
const MAX_CONTEXT_CHARS_ENV_VAR = 'MAX_CONTEXT_CHARS';
|
|
5
|
-
const BUDGET_ERROR_META = { retryable: false, kind: 'budget' };
|
|
6
|
-
const contextCharsConfig = createCachedEnvInt(MAX_CONTEXT_CHARS_ENV_VAR, DEFAULT_MAX_CONTEXT_CHARS);
|
|
7
|
-
function computeFilesSize(files) {
|
|
8
|
-
let total = 0;
|
|
9
|
-
for (const file of files) {
|
|
10
|
-
total += file.content.length;
|
|
11
|
-
}
|
|
12
|
-
return total;
|
|
13
|
-
}
|
|
14
|
-
function createContextBudgetMessage(size, max) {
|
|
15
|
-
return `Combined context size ${size} chars exceeds limit of ${max} chars.`;
|
|
16
|
-
}
|
|
17
|
-
export function resetMaxContextCharsCacheForTesting() {
|
|
18
|
-
contextCharsConfig.reset();
|
|
19
|
-
}
|
|
20
|
-
function getMaxContextChars() {
|
|
21
|
-
return contextCharsConfig.get();
|
|
22
|
-
}
|
|
23
|
-
export function computeContextSize(diff, files) {
|
|
24
|
-
if (!files || files.length === 0) {
|
|
25
|
-
return diff.length;
|
|
26
|
-
}
|
|
27
|
-
return diff.length + computeFilesSize(files);
|
|
28
|
-
}
|
|
29
|
-
export function validateContextBudget(diff, files) {
|
|
30
|
-
const size = computeContextSize(diff, files);
|
|
31
|
-
const max = getMaxContextChars();
|
|
32
|
-
if (size > max) {
|
|
33
|
-
return createErrorToolResponse('E_INPUT_TOO_LARGE', createContextBudgetMessage(size, max), { providedChars: size, maxChars: max }, BUDGET_ERROR_META);
|
|
34
|
-
}
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { createErrorToolResponse } from './tool-response.js';
|
|
2
|
-
export declare function getMaxDiffChars(): number;
|
|
3
|
-
export declare function resetMaxDiffCharsCacheForTesting(): void;
|
|
4
|
-
export declare function exceedsDiffBudget(diff: string): boolean;
|
|
5
|
-
export declare function getDiffBudgetError(diffLength: number, maxChars?: number): string;
|
|
6
|
-
export declare function validateDiffBudget(diff: string): ReturnType<typeof createErrorToolResponse> | undefined;
|
package/dist/lib/diff-budget.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { createCachedEnvInt } from './env-config.js';
|
|
2
|
-
import { createErrorToolResponse } from './tool-response.js';
|
|
3
|
-
const DEFAULT_MAX_DIFF_CHARS = 120_000;
|
|
4
|
-
const MAX_DIFF_CHARS_ENV_VAR = 'MAX_DIFF_CHARS';
|
|
5
|
-
const numberFormatter = new Intl.NumberFormat('en-US');
|
|
6
|
-
const diffCharsConfig = createCachedEnvInt(MAX_DIFF_CHARS_ENV_VAR, DEFAULT_MAX_DIFF_CHARS);
|
|
7
|
-
export function getMaxDiffChars() {
|
|
8
|
-
return diffCharsConfig.get();
|
|
9
|
-
}
|
|
10
|
-
export function resetMaxDiffCharsCacheForTesting() {
|
|
11
|
-
diffCharsConfig.reset();
|
|
12
|
-
}
|
|
13
|
-
export function exceedsDiffBudget(diff) {
|
|
14
|
-
const maxChars = getMaxDiffChars();
|
|
15
|
-
return diff.length > maxChars;
|
|
16
|
-
}
|
|
17
|
-
function formatDiffBudgetError(diffLength, maxChars) {
|
|
18
|
-
return `diff exceeds max allowed size (${numberFormatter.format(diffLength)} chars > ${numberFormatter.format(maxChars)} chars)`;
|
|
19
|
-
}
|
|
20
|
-
export function getDiffBudgetError(diffLength, maxChars = getMaxDiffChars()) {
|
|
21
|
-
return formatDiffBudgetError(diffLength, maxChars);
|
|
22
|
-
}
|
|
23
|
-
const BUDGET_ERROR_META = { retryable: false, kind: 'budget' };
|
|
24
|
-
export function validateDiffBudget(diff) {
|
|
25
|
-
const providedChars = diff.length;
|
|
26
|
-
const maxChars = getMaxDiffChars();
|
|
27
|
-
if (providedChars <= maxChars) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
return createErrorToolResponse('E_INPUT_TOO_LARGE', getDiffBudgetError(providedChars, maxChars), { providedChars, maxChars }, BUDGET_ERROR_META);
|
|
31
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export declare const NOISY_EXCLUDE_PATHSPECS: readonly [":(exclude)package-lock.json", ":(exclude)yarn.lock", ":(exclude)pnpm-lock.yaml", ":(exclude)bun.lockb", ":(exclude)*.lock", ":(exclude)dist/", ":(exclude)build/", ":(exclude)out/", ":(exclude).next/", ":(exclude)coverage/", ":(exclude)*.min.js", ":(exclude)*.min.css", ":(exclude)*.map"];
|
|
2
|
-
/**
|
|
3
|
-
* Split raw unified diff into per-file sections and strip:
|
|
4
|
-
* - Binary file sections ("Binary files a/... and b/... differ")
|
|
5
|
-
* - GIT binary patch sections
|
|
6
|
-
* - Mode-only sections (permission changes with no content hunks)
|
|
7
|
-
*
|
|
8
|
-
* Does NOT modify content lines (+ / - / space) to preserve verbatim
|
|
9
|
-
* accuracy required by suggest_search_replace.
|
|
10
|
-
*/
|
|
11
|
-
export declare function cleanDiff(raw: string): string;
|
|
12
|
-
export declare function isEmptyDiff(diff: string): boolean;
|
package/dist/lib/diff-cleaner.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
export const NOISY_EXCLUDE_PATHSPECS = [
|
|
2
|
-
':(exclude)package-lock.json',
|
|
3
|
-
':(exclude)yarn.lock',
|
|
4
|
-
':(exclude)pnpm-lock.yaml',
|
|
5
|
-
':(exclude)bun.lockb',
|
|
6
|
-
':(exclude)*.lock',
|
|
7
|
-
':(exclude)dist/',
|
|
8
|
-
':(exclude)build/',
|
|
9
|
-
':(exclude)out/',
|
|
10
|
-
':(exclude).next/',
|
|
11
|
-
':(exclude)coverage/',
|
|
12
|
-
':(exclude)*.min.js',
|
|
13
|
-
':(exclude)*.min.css',
|
|
14
|
-
':(exclude)*.map',
|
|
15
|
-
];
|
|
16
|
-
// Regex patterns to identify noisy diff file sections.
|
|
17
|
-
const BINARY_FILE_LINE = /^Binary files .+ differ$/m;
|
|
18
|
-
const GIT_BINARY_PATCH = /^GIT binary patch/m;
|
|
19
|
-
const HAS_HUNK = /^@@/m;
|
|
20
|
-
const HAS_OLD_MODE = /^old mode /m;
|
|
21
|
-
function shouldKeepSection(section) {
|
|
22
|
-
if (!section.trim()) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
if (BINARY_FILE_LINE.test(section)) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
if (GIT_BINARY_PATCH.test(section)) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
if (HAS_OLD_MODE.test(section) && !HAS_HUNK.test(section)) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
function processSection(raw, start, end, sections) {
|
|
37
|
-
if (end > start) {
|
|
38
|
-
const section = raw.slice(start, end);
|
|
39
|
-
if (shouldKeepSection(section)) {
|
|
40
|
-
sections.push(section);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Split raw unified diff into per-file sections and strip:
|
|
46
|
-
* - Binary file sections ("Binary files a/... and b/... differ")
|
|
47
|
-
* - GIT binary patch sections
|
|
48
|
-
* - Mode-only sections (permission changes with no content hunks)
|
|
49
|
-
*
|
|
50
|
-
* Does NOT modify content lines (+ / - / space) to preserve verbatim
|
|
51
|
-
* accuracy required by suggest_search_replace.
|
|
52
|
-
*/
|
|
53
|
-
export function cleanDiff(raw) {
|
|
54
|
-
if (!raw)
|
|
55
|
-
return '';
|
|
56
|
-
const sections = [];
|
|
57
|
-
let lastIndex = 0;
|
|
58
|
-
let nextIndex = raw.startsWith('diff --git ')
|
|
59
|
-
? 0
|
|
60
|
-
: raw.indexOf('\ndiff --git ');
|
|
61
|
-
if (nextIndex === -1) {
|
|
62
|
-
processSection(raw, 0, raw.length, sections);
|
|
63
|
-
return sections.join('').trim();
|
|
64
|
-
}
|
|
65
|
-
while (nextIndex !== -1) {
|
|
66
|
-
const matchIndex = nextIndex === 0 ? 0 : nextIndex + 1; // +1 to skip \n
|
|
67
|
-
processSection(raw, lastIndex, matchIndex, sections);
|
|
68
|
-
lastIndex = matchIndex;
|
|
69
|
-
nextIndex = raw.indexOf('\ndiff --git ', lastIndex);
|
|
70
|
-
}
|
|
71
|
-
processSection(raw, lastIndex, raw.length, sections);
|
|
72
|
-
return sections.join('').trim();
|
|
73
|
-
}
|
|
74
|
-
export function isEmptyDiff(diff) {
|
|
75
|
-
return diff.trim().length === 0;
|
|
76
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { File as ParsedFile } from 'parse-diff';
|
|
2
|
-
export type { ParsedFile };
|
|
3
|
-
interface DiffStats {
|
|
4
|
-
files: number;
|
|
5
|
-
added: number;
|
|
6
|
-
deleted: number;
|
|
7
|
-
}
|
|
8
|
-
/** Parse unified diff string into structured file list. */
|
|
9
|
-
export declare function parseDiffFiles(diff: string): ParsedFile[];
|
|
10
|
-
export declare function computeDiffStatsAndSummaryFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
11
|
-
stats: DiffStats;
|
|
12
|
-
summary: string;
|
|
13
|
-
}>;
|
|
14
|
-
export declare function computeDiffStatsAndPathsFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
15
|
-
stats: DiffStats;
|
|
16
|
-
paths: string[];
|
|
17
|
-
}>;
|
|
18
|
-
/** Extract all unique changed file paths (renamed: returns new path). */
|
|
19
|
-
export declare function extractChangedPathsFromFiles(files: readonly ParsedFile[]): string[];
|
|
20
|
-
/** Extract all unique changed file paths (renamed: returns new path). */
|
|
21
|
-
export declare function extractChangedPaths(diff: string): string[];
|
|
22
|
-
export declare function computeDiffStatsFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
23
|
-
files: number;
|
|
24
|
-
added: number;
|
|
25
|
-
deleted: number;
|
|
26
|
-
}>;
|
|
27
|
-
/** Count changed files, added lines, and deleted lines. */
|
|
28
|
-
export declare function computeDiffStats(diff: string): Readonly<{
|
|
29
|
-
files: number;
|
|
30
|
-
added: number;
|
|
31
|
-
deleted: number;
|
|
32
|
-
}>;
|
|
33
|
-
/** Generate human-readable summary of changed files and line counts. */
|
|
34
|
-
export declare function formatFileSummary(files: ParsedFile[]): string;
|
package/dist/lib/diff-parser.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import parseDiff from 'parse-diff';
|
|
2
|
-
const UNKNOWN_PATH = 'unknown';
|
|
3
|
-
const NO_FILES_CHANGED = 'No files changed.';
|
|
4
|
-
const EMPTY_PATHS = [];
|
|
5
|
-
const EMPTY_STATS = Object.freeze({ files: 0, added: 0, deleted: 0 });
|
|
6
|
-
const PATH_SORTER = (left, right) => left.localeCompare(right);
|
|
7
|
-
/** Parse unified diff string into structured file list. */
|
|
8
|
-
export function parseDiffFiles(diff) {
|
|
9
|
-
return diff ? parseDiff(diff) : [];
|
|
10
|
-
}
|
|
11
|
-
function cleanPath(path) {
|
|
12
|
-
if (path.startsWith('a/') || path.startsWith('b/')) {
|
|
13
|
-
return path.slice(2);
|
|
14
|
-
}
|
|
15
|
-
return path;
|
|
16
|
-
}
|
|
17
|
-
function resolveChangedPath(file) {
|
|
18
|
-
if (file.to && file.to !== '/dev/null') {
|
|
19
|
-
return cleanPath(file.to);
|
|
20
|
-
}
|
|
21
|
-
if (file.from && file.from !== '/dev/null') {
|
|
22
|
-
return cleanPath(file.from);
|
|
23
|
-
}
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
function sortPaths(paths) {
|
|
27
|
-
return Array.from(paths).sort(PATH_SORTER);
|
|
28
|
-
}
|
|
29
|
-
function calculateStats(files) {
|
|
30
|
-
let added = 0;
|
|
31
|
-
let deleted = 0;
|
|
32
|
-
for (const file of files) {
|
|
33
|
-
added += file.additions;
|
|
34
|
-
deleted += file.deletions;
|
|
35
|
-
}
|
|
36
|
-
return { files: files.length, added, deleted };
|
|
37
|
-
}
|
|
38
|
-
function getUniquePaths(files) {
|
|
39
|
-
const paths = new Set();
|
|
40
|
-
for (const file of files) {
|
|
41
|
-
const path = resolveChangedPath(file);
|
|
42
|
-
if (path) {
|
|
43
|
-
paths.add(path);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return paths;
|
|
47
|
-
}
|
|
48
|
-
export function computeDiffStatsAndSummaryFromFiles(files) {
|
|
49
|
-
if (files.length === 0) {
|
|
50
|
-
return {
|
|
51
|
-
stats: EMPTY_STATS,
|
|
52
|
-
summary: NO_FILES_CHANGED,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
let added = 0;
|
|
56
|
-
let deleted = 0;
|
|
57
|
-
const summaries = [];
|
|
58
|
-
const MAX_SUMMARY_FILES = 40;
|
|
59
|
-
let i = 0;
|
|
60
|
-
for (const file of files) {
|
|
61
|
-
added += file.additions;
|
|
62
|
-
deleted += file.deletions;
|
|
63
|
-
if (i < MAX_SUMMARY_FILES) {
|
|
64
|
-
const path = resolveChangedPath(file) ?? UNKNOWN_PATH;
|
|
65
|
-
summaries.push(`${path} (+${file.additions} -${file.deletions})`);
|
|
66
|
-
}
|
|
67
|
-
i++;
|
|
68
|
-
}
|
|
69
|
-
if (files.length > MAX_SUMMARY_FILES) {
|
|
70
|
-
summaries.push(`... and ${files.length - MAX_SUMMARY_FILES} more files`);
|
|
71
|
-
}
|
|
72
|
-
const stats = { files: files.length, added, deleted };
|
|
73
|
-
return {
|
|
74
|
-
stats,
|
|
75
|
-
summary: `${summaries.join(', ')} [${stats.files} files, +${stats.added} -${stats.deleted}]`,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
export function computeDiffStatsAndPathsFromFiles(files) {
|
|
79
|
-
if (files.length === 0) {
|
|
80
|
-
return {
|
|
81
|
-
stats: EMPTY_STATS,
|
|
82
|
-
paths: EMPTY_PATHS,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
const stats = calculateStats(files);
|
|
86
|
-
const paths = sortPaths(getUniquePaths(files));
|
|
87
|
-
return {
|
|
88
|
-
stats,
|
|
89
|
-
paths,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
/** Extract all unique changed file paths (renamed: returns new path). */
|
|
93
|
-
export function extractChangedPathsFromFiles(files) {
|
|
94
|
-
if (files.length === 0) {
|
|
95
|
-
return EMPTY_PATHS;
|
|
96
|
-
}
|
|
97
|
-
return sortPaths(getUniquePaths(files));
|
|
98
|
-
}
|
|
99
|
-
/** Extract all unique changed file paths (renamed: returns new path). */
|
|
100
|
-
export function extractChangedPaths(diff) {
|
|
101
|
-
return extractChangedPathsFromFiles(parseDiffFiles(diff));
|
|
102
|
-
}
|
|
103
|
-
export function computeDiffStatsFromFiles(files) {
|
|
104
|
-
if (files.length === 0) {
|
|
105
|
-
return EMPTY_STATS;
|
|
106
|
-
}
|
|
107
|
-
return calculateStats(files);
|
|
108
|
-
}
|
|
109
|
-
/** Count changed files, added lines, and deleted lines. */
|
|
110
|
-
export function computeDiffStats(diff) {
|
|
111
|
-
return computeDiffStatsFromFiles(parseDiffFiles(diff));
|
|
112
|
-
}
|
|
113
|
-
/** Generate human-readable summary of changed files and line counts. */
|
|
114
|
-
export function formatFileSummary(files) {
|
|
115
|
-
return computeDiffStatsAndSummaryFromFiles(files).summary;
|
|
116
|
-
}
|