@j0hanz/code-review-analyst-mcp 1.2.1 → 1.3.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.
Files changed (49) hide show
  1. package/dist/index.js +12 -3
  2. package/dist/instructions.md +4 -146
  3. package/dist/lib/context-budget.d.ts +2 -2
  4. package/dist/lib/context-budget.js +12 -6
  5. package/dist/lib/diff-budget.js +6 -2
  6. package/dist/lib/diff-parser.js +31 -36
  7. package/dist/lib/env-config.d.ts +1 -0
  8. package/dist/lib/env-config.js +9 -3
  9. package/dist/lib/errors.d.ts +1 -0
  10. package/dist/lib/errors.js +5 -5
  11. package/dist/lib/gemini-schema.js +2 -1
  12. package/dist/lib/gemini.js +135 -67
  13. package/dist/lib/model-config.d.ts +14 -2
  14. package/dist/lib/model-config.js +30 -6
  15. package/dist/lib/tool-contracts.d.ts +245 -0
  16. package/dist/lib/tool-contracts.js +302 -0
  17. package/dist/lib/tool-factory.d.ts +5 -1
  18. package/dist/lib/tool-factory.js +48 -54
  19. package/dist/lib/tool-response.js +10 -12
  20. package/dist/lib/types.d.ts +3 -3
  21. package/dist/prompts/index.js +47 -41
  22. package/dist/resources/index.d.ts +1 -1
  23. package/dist/resources/index.js +80 -18
  24. package/dist/resources/instructions.d.ts +1 -0
  25. package/dist/resources/instructions.js +59 -0
  26. package/dist/resources/server-config.d.ts +1 -0
  27. package/dist/resources/server-config.js +70 -0
  28. package/dist/resources/tool-catalog.d.ts +1 -0
  29. package/dist/resources/tool-catalog.js +39 -0
  30. package/dist/resources/tool-info.d.ts +5 -0
  31. package/dist/resources/tool-info.js +122 -0
  32. package/dist/resources/workflows.d.ts +1 -0
  33. package/dist/resources/workflows.js +72 -0
  34. package/dist/schemas/inputs.d.ts +8 -0
  35. package/dist/schemas/inputs.js +20 -18
  36. package/dist/schemas/outputs.d.ts +17 -1
  37. package/dist/schemas/outputs.js +84 -52
  38. package/dist/server.js +25 -26
  39. package/dist/tools/analyze-complexity.d.ts +2 -0
  40. package/dist/tools/analyze-complexity.js +45 -0
  41. package/dist/tools/analyze-pr-impact.js +30 -25
  42. package/dist/tools/detect-api-breaking.d.ts +2 -0
  43. package/dist/tools/detect-api-breaking.js +42 -0
  44. package/dist/tools/generate-review-summary.js +26 -20
  45. package/dist/tools/generate-test-plan.js +34 -28
  46. package/dist/tools/index.js +9 -2
  47. package/dist/tools/inspect-code-quality.js +46 -40
  48. package/dist/tools/suggest-search-replace.js +34 -27
  49. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,6 +6,11 @@ import { createServer } from './server.js';
6
6
  const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM'];
7
7
  const ARG_OPTION_MODEL = 'model';
8
8
  const ARG_OPTION_MAX_DIFF_CHARS = 'max-diff-chars';
9
+ const PROCESS_ARGS_START_INDEX = 2;
10
+ const CLI_ENV_MAPPINGS = [
11
+ { option: ARG_OPTION_MODEL, envVar: 'GEMINI_MODEL' },
12
+ { option: ARG_OPTION_MAX_DIFF_CHARS, envVar: 'MAX_DIFF_CHARS' },
13
+ ];
9
14
  const CLI_OPTIONS = {
10
15
  [ARG_OPTION_MODEL]: {
11
16
  type: 'string',
@@ -20,14 +25,18 @@ function setStringEnv(name, value) {
20
25
  process.env[name] = value;
21
26
  }
22
27
  }
28
+ function applyCliEnvironmentOverrides(values) {
29
+ for (const mapping of CLI_ENV_MAPPINGS) {
30
+ setStringEnv(mapping.envVar, values[mapping.option]);
31
+ }
32
+ }
23
33
  function parseCommandLineArgs() {
24
34
  const { values } = parseArgs({
25
- args: process.argv.slice(2),
35
+ args: process.argv.slice(PROCESS_ARGS_START_INDEX),
26
36
  options: CLI_OPTIONS,
27
37
  strict: false,
28
38
  });
29
- setStringEnv('GEMINI_MODEL', values[ARG_OPTION_MODEL]);
30
- setStringEnv('MAX_DIFF_CHARS', values[ARG_OPTION_MAX_DIFF_CHARS]);
39
+ applyCliEnvironmentOverrides(values);
31
40
  }
32
41
  let shuttingDown = false;
33
42
  async function shutdown(server) {
@@ -1,149 +1,7 @@
1
1
  # CODE REVIEW ANALYST MCP INSTRUCTIONS
2
2
 
3
- These instructions are available as a resource (internal://instructions) or prompt (get-help). Load them when unsure about tool usage.
3
+ Runtime instructions are generated from `src/lib/tool-contracts.ts` by
4
+ `buildServerInstructions()` in `src/resources/instructions.ts`.
4
5
 
5
- ---
6
-
7
- ## CORE CAPABILITY
8
-
9
- - Domain: Gemini-powered code review analysis — accepts unified diffs and returns structured findings, impact assessments, test plans, and search/replace fixes.
10
- - Primary Resources: Unified diff text, structured JSON review results, impact assessments, test plans.
11
- - Tools: READ: `analyze_pr_impact`, `generate_review_summary`, `inspect_code_quality`, `suggest_search_replace`, `generate_test_plan`. WRITE: none.
12
-
13
- ---
14
-
15
- ## PROMPTS
16
-
17
- - `get-help`: Returns these instructions for quick recall.
18
- - `review-guide`: Guided workflow for a specific tool and focus area. Accepts `tool` and `focusArea` arguments with auto-completion.
19
-
20
- ---
21
-
22
- ## RESOURCES & RESOURCE LINKS
23
-
24
- - `internal://instructions`: This document.
25
-
26
- ---
27
-
28
- ## PROGRESS & TASKS
29
-
30
- - Include `_meta.progressToken` in requests to receive `notifications/progress` updates during Gemini processing.
31
- - Task-augmented tool calls are supported for all five tools:
32
- - Send `tools/call` with `task` to get a task id.
33
- - Poll `tasks/get` and fetch results via `tasks/result`.
34
- - Use `tasks/cancel` to abort.
35
- - Progress reports 4 steps: start → input validated → prompt prepared → model response received → completed.
36
- - Task data is stored in memory (30-minute TTL) and cleared on restart.
37
-
38
- ---
39
-
40
- ## THE "GOLDEN PATH" WORKFLOWS (CRITICAL)
41
-
42
- ### WORKFLOW A: FULL PR REVIEW
43
-
44
- 1. Call `generate_review_summary` with `diff` and `repository` to get a high-level summary, risk rating, and merge recommendation.
45
- 2. Call `inspect_code_quality` with the same `diff`, `repository`, and optionally `files` for context-aware deep review.
46
- 3. For each actionable finding, call `suggest_search_replace` with the `diff`, `findingTitle`, and `findingDetails` from step 2.
47
- NOTE: Keep `suggest_search_replace` scoped to one finding per call. Pre-check diff size < 120,000 chars before any call.
48
-
49
- ### WORKFLOW B: IMPACT ASSESSMENT
50
-
51
- 1. Call `analyze_pr_impact` with `diff` and `repository` to get severity, categories, breaking changes, and rollback complexity.
52
- 2. Call `generate_review_summary` with the same `diff` for a complementary merge recommendation.
53
- NOTE: Use `analyze_pr_impact` when you need categorization (breaking_change, api_change, etc.) and rollback assessment.
54
-
55
- ### WORKFLOW C: PATCH FROM FINDING
56
-
57
- 1. Call `inspect_code_quality` with `diff`, `repository`, and `focusAreas` to identify specific findings.
58
- 2. Pick one finding. Call `suggest_search_replace` with the same `diff`, plus `findingTitle` and `findingDetails` from that finding.
59
- 3. Validate the returned `blocks[]` before applying — `search` text must match file content exactly.
60
- NOTE: Never batch multiple findings into one `suggest_search_replace` call.
61
-
62
- ### WORKFLOW D: TEST COVERAGE
63
-
64
- 1. Call `generate_test_plan` with `diff`, `repository`, and optionally `testFramework` and `maxTestCases`.
65
- 2. Review `testCases[]` ordered by priority: `must_have` → `should_have` → `nice_to_have`.
66
- NOTE: Combine with `inspect_code_quality` when you need finding-aware test targeting.
67
-
68
- ---
69
-
70
- ## TOOL NUANCES & GOTCHAS
71
-
72
- `analyze_pr_impact`
73
-
74
- - Purpose: Assess impact severity, categories, breaking changes, and rollback complexity for a PR diff.
75
- - Input: `diff` (required), `repository` (required), `language` (optional, defaults to auto-detect).
76
- - Output: `severity`, `categories[]`, `breakingChanges[]`, `affectedAreas[]`, `rollbackComplexity`.
77
- - Side effects: Calls external Gemini API (Flash model); does not mutate local state.
78
-
79
- `generate_review_summary`
80
-
81
- - Purpose: Produce a concise PR summary with risk rating, key changes, and merge recommendation.
82
- - Input: `diff` (required), `repository` (required), `language` (optional).
83
- - Output: `summary`, `overallRisk`, `keyChanges[]`, `recommendation`, `stats` (computed locally from diff, not by Gemini).
84
- - Gotcha: `stats` (filesChanged, linesAdded, linesRemoved) are computed from diff parsing before the Gemini call — they are always accurate.
85
-
86
- `inspect_code_quality`
87
-
88
- - Purpose: Deep code review with optional full file context for cross-reference analysis.
89
- - Input: `diff` (required), `repository` (required), `language` (optional), `focusAreas` (optional, 1–12 items), `maxFindings` (optional, 1–25), `files` (optional, 1–20 files with `path` and `content`).
90
- - Output: `summary`, `overallRisk`, `findings[]`, `testsNeeded[]`, `contextualInsights[]`, `totalFindings`.
91
- - Gotcha: Uses Pro model with thinking — slower but higher quality than Flash-based tools. Timeout is 120 seconds.
92
- - Gotcha: Combined diff + file context must stay under `MAX_CONTEXT_CHARS` (default 500,000). Provide only relevant files.
93
- - Gotcha: `maxFindings` caps results AFTER Gemini returns. `totalFindings` shows the pre-cap count.
94
- - Limits: Max 20 files, each max 100,000 chars content.
95
-
96
- `suggest_search_replace`
97
-
98
- - Purpose: Generate verbatim search-and-replace blocks to fix one specific finding.
99
- - Input: `diff` (required), `findingTitle` (3–160 chars), `findingDetails` (10–3,000 chars).
100
- - Output: `summary`, `blocks[]` (each with `file`, `search`, `replace`, `explanation`), `validationChecklist[]`.
101
- - Gotcha: Uses Pro model with thinking (120s timeout). `search` blocks must match exact verbatim text in the file.
102
- - Gotcha: Scope each call to one finding. Multi-finding calls produce mixed patch intent.
103
-
104
- `generate_test_plan`
105
-
106
- - Purpose: Create an actionable test plan with pseudocode covering changes in the diff.
107
- - Input: `diff` (required), `repository` (required), `language` (optional), `testFramework` (optional, defaults to auto-detect), `maxTestCases` (optional, 1–30).
108
- - Output: `summary`, `testCases[]` (each with `name`, `type`, `file`, `description`, `pseudoCode`, `priority`), `coverageSummary`.
109
- - Gotcha: `maxTestCases` caps results AFTER Gemini returns. Uses Flash model with thinking budget.
110
-
111
- ---
112
-
113
- ## CROSS-FEATURE RELATIONSHIPS
114
-
115
- - Use `inspect_code_quality` findings (`title` + `explanation`) as `findingTitle` + `findingDetails` for `suggest_search_replace`.
116
- - Use `generate_review_summary` for quick triage before committing to the slower `inspect_code_quality`.
117
- - All tools share the same diff budget guard, Gemini client, retry policy, and concurrency limiter.
118
- - `inspect_code_quality` is the only tool that accepts `files` for full file context — all others analyze diff only.
119
- - All tool responses include both `structuredContent` and JSON-string `content` for client compatibility.
120
-
121
- ---
122
-
123
- ## CONSTRAINTS & LIMITATIONS
124
-
125
- - Transport: stdio only.
126
- - API credentials: Require `GEMINI_API_KEY` or `GOOGLE_API_KEY` environment variable.
127
- - Model selection: `GEMINI_MODEL` env var overrides the default (gemini-2.5-flash). Pro model tools (`inspect_code_quality`, `suggest_search_replace`) always use gemini-2.5-pro regardless.
128
- - Diff size: Runtime limit defaults to 120,000 chars (`MAX_DIFF_CHARS` env override).
129
- - Context size: Combined diff + files limit defaults to 500,000 chars (`MAX_CONTEXT_CHARS` env override). Only applies to `inspect_code_quality`.
130
- - Timeout: 60 seconds default (Flash tools), 120 seconds for Pro tools. Retry count: 1 with exponential backoff.
131
- - Max output tokens: 16,384 per Gemini call.
132
- - Concurrency: `MAX_CONCURRENT_CALLS` defaults to 10. Excess calls wait up to `MAX_CONCURRENT_CALLS_WAIT_MS` (default 2,000ms).
133
- - Safety: Gemini safety thresholds default to `BLOCK_NONE`. Override with `GEMINI_HARM_BLOCK_THRESHOLD` (`BLOCK_NONE`, `BLOCK_ONLY_HIGH`, `BLOCK_MEDIUM_AND_ABOVE`, `BLOCK_LOW_AND_ABOVE`).
134
- - Task TTL: 30 minutes. Task data is in-memory and lost on process restart.
135
-
136
- ---
137
-
138
- ## ERROR HANDLING STRATEGY
139
-
140
- - `E_INPUT_TOO_LARGE`: Diff or combined context exceeded budget. → Split the diff into smaller chunks or reduce the number of `files`. Not retryable.
141
- - `E_ANALYZE_IMPACT`: Impact analysis failed. → Check API key env vars, reduce diff size, and retry. Inspect `error.kind` for classification.
142
- - `E_REVIEW_SUMMARY`: Summary generation failed. → Check connectivity/model availability and retry with same diff.
143
- - `E_INSPECT_QUALITY`: Code quality inspection failed. → Reduce diff size or file context, verify API key, and retry.
144
- - `E_SUGGEST_SEARCH_REPLACE`: Search/replace generation failed. → Verify finding inputs are specific and retry with narrower details.
145
- - `E_GENERATE_TEST_PLAN`: Test plan generation failed. → Reduce diff size and retry.
146
- - Error `kind` values: `validation` (bad input, not retryable), `budget` (size exceeded, not retryable), `upstream` (Gemini API error, retryable), `timeout` (exceeded deadline, retryable), `cancelled` (request aborted, not retryable), `internal` (unexpected, not retryable).
147
- - Missing API key: Set `GEMINI_API_KEY` or `GOOGLE_API_KEY` env var and restart.
148
- - Gemini timeout: Reduce diff/context size or increase timeout via tool config.
149
- - Empty model response: Retry — Gemini occasionally returns empty bodies under load.
6
+ This file is kept as a packaging placeholder for build pipelines that expect an
7
+ `instructions.md` artifact in `dist/`.
@@ -3,6 +3,6 @@ interface FileContent {
3
3
  content: string;
4
4
  }
5
5
  export declare function resetMaxContextCharsCacheForTesting(): void;
6
- export declare function computeContextSize(diff: string, files?: FileContent[]): number;
7
- export declare function validateContextBudget(diff: string, files?: FileContent[]): ReturnType<typeof createErrorToolResponse> | undefined;
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
8
  export {};
@@ -4,6 +4,16 @@ const DEFAULT_MAX_CONTEXT_CHARS = 500_000;
4
4
  const MAX_CONTEXT_CHARS_ENV_VAR = 'MAX_CONTEXT_CHARS';
5
5
  const BUDGET_ERROR_META = { retryable: false, kind: 'budget' };
6
6
  const contextCharsConfig = createCachedEnvInt(MAX_CONTEXT_CHARS_ENV_VAR, DEFAULT_MAX_CONTEXT_CHARS);
7
+ function computeFilesSize(files) {
8
+ let fileSize = 0;
9
+ for (const file of files) {
10
+ fileSize += file.content.length;
11
+ }
12
+ return fileSize;
13
+ }
14
+ function createContextBudgetMessage(size, max) {
15
+ return `Combined context size ${size} chars exceeds limit of ${max} chars.`;
16
+ }
7
17
  export function resetMaxContextCharsCacheForTesting() {
8
18
  contextCharsConfig.reset();
9
19
  }
@@ -14,17 +24,13 @@ export function computeContextSize(diff, files) {
14
24
  if (!files || files.length === 0) {
15
25
  return diff.length;
16
26
  }
17
- let fileSize = 0;
18
- for (const file of files) {
19
- fileSize += file.content.length;
20
- }
21
- return diff.length + fileSize;
27
+ return diff.length + computeFilesSize(files);
22
28
  }
23
29
  export function validateContextBudget(diff, files) {
24
30
  const size = computeContextSize(diff, files);
25
31
  const max = getMaxContextChars();
26
32
  if (size > max) {
27
- return createErrorToolResponse('E_INPUT_TOO_LARGE', `Combined context size ${size} chars exceeds limit of ${max} chars.`, { providedChars: size, maxChars: max }, BUDGET_ERROR_META);
33
+ return createErrorToolResponse('E_INPUT_TOO_LARGE', createContextBudgetMessage(size, max), { providedChars: size, maxChars: max }, BUDGET_ERROR_META);
28
34
  }
29
35
  return undefined;
30
36
  }
@@ -11,7 +11,8 @@ export function resetMaxDiffCharsCacheForTesting() {
11
11
  diffCharsConfig.reset();
12
12
  }
13
13
  export function exceedsDiffBudget(diff) {
14
- return diff.length > getMaxDiffChars();
14
+ const maxChars = getMaxDiffChars();
15
+ return getDiffLength(diff) > maxChars;
15
16
  }
16
17
  function formatDiffBudgetError(diffLength, maxChars) {
17
18
  return `diff exceeds max allowed size (${numberFormatter.format(diffLength)} chars > ${numberFormatter.format(maxChars)} chars)`;
@@ -20,8 +21,11 @@ export function getDiffBudgetError(diffLength, maxChars = getMaxDiffChars()) {
20
21
  return formatDiffBudgetError(diffLength, maxChars);
21
22
  }
22
23
  const BUDGET_ERROR_META = { retryable: false, kind: 'budget' };
24
+ function getDiffLength(diff) {
25
+ return diff.length;
26
+ }
23
27
  export function validateDiffBudget(diff) {
24
- const providedChars = diff.length;
28
+ const providedChars = getDiffLength(diff);
25
29
  const maxChars = getMaxDiffChars();
26
30
  if (providedChars <= maxChars) {
27
31
  return undefined;
@@ -12,7 +12,6 @@ export function parseDiffFiles(diff) {
12
12
  return parseDiff(diff);
13
13
  }
14
14
  function cleanPath(path) {
15
- // Common git diff prefixes
16
15
  if (path.startsWith('a/') || path.startsWith('b/')) {
17
16
  return path.slice(2);
18
17
  }
@@ -33,28 +32,40 @@ function sortPaths(paths) {
33
32
  }
34
33
  return Array.from(paths).sort(PATH_SORTER);
35
34
  }
36
- export function computeDiffStatsAndSummaryFromFiles(files) {
37
- if (files.length === 0) {
38
- return {
39
- stats: EMPTY_STATS,
40
- summary: NO_FILES_CHANGED,
41
- };
42
- }
35
+ function buildDiffComputation(files) {
43
36
  let added = 0;
44
37
  let deleted = 0;
38
+ const paths = new Set();
45
39
  const summaries = new Array(files.length);
46
40
  let index = 0;
47
41
  for (const file of files) {
48
42
  added += file.additions;
49
43
  deleted += file.deletions;
50
44
  const path = resolveChangedPath(file);
45
+ if (path) {
46
+ paths.add(path);
47
+ }
51
48
  summaries[index] =
52
49
  `${path ?? UNKNOWN_PATH} (+${file.additions} -${file.deletions})`;
53
50
  index += 1;
54
51
  }
52
+ return { added, deleted, paths, summaries };
53
+ }
54
+ function buildStats(filesCount, added, deleted) {
55
+ return { files: filesCount, added, deleted };
56
+ }
57
+ export function computeDiffStatsAndSummaryFromFiles(files) {
58
+ if (files.length === 0) {
59
+ return {
60
+ stats: EMPTY_STATS,
61
+ summary: NO_FILES_CHANGED,
62
+ };
63
+ }
64
+ const computed = buildDiffComputation(files);
65
+ const stats = buildStats(files.length, computed.added, computed.deleted);
55
66
  return {
56
- stats: { files: files.length, added, deleted },
57
- summary: `${summaries.join(', ')} [${files.length} files, +${added} -${deleted}]`,
67
+ stats,
68
+ summary: `${computed.summaries.join(', ')} [${stats.files} files, +${stats.added} -${stats.deleted}]`,
58
69
  };
59
70
  }
60
71
  export function computeDiffStatsAndPathsFromFiles(files) {
@@ -64,45 +75,29 @@ export function computeDiffStatsAndPathsFromFiles(files) {
64
75
  paths: EMPTY_PATHS,
65
76
  };
66
77
  }
67
- let added = 0;
68
- let deleted = 0;
69
- const paths = new Set();
70
- for (const file of files) {
71
- added += file.additions;
72
- deleted += file.deletions;
73
- const path = resolveChangedPath(file);
74
- if (path) {
75
- paths.add(path);
76
- }
77
- }
78
+ const computed = buildDiffComputation(files);
78
79
  return {
79
- stats: { files: files.length, added, deleted },
80
- paths: sortPaths(paths),
80
+ stats: buildStats(files.length, computed.added, computed.deleted),
81
+ paths: sortPaths(computed.paths),
81
82
  };
82
83
  }
83
84
  /** Extract all unique changed file paths (renamed: returns new path). */
84
85
  export function extractChangedPathsFromFiles(files) {
85
- const paths = new Set();
86
- for (const file of files) {
87
- const path = resolveChangedPath(file);
88
- if (path) {
89
- paths.add(path);
90
- }
86
+ if (files.length === 0) {
87
+ return EMPTY_PATHS;
91
88
  }
92
- return sortPaths(paths);
89
+ return sortPaths(buildDiffComputation(files).paths);
93
90
  }
94
91
  /** Extract all unique changed file paths (renamed: returns new path). */
95
92
  export function extractChangedPaths(diff) {
96
93
  return extractChangedPathsFromFiles(parseDiffFiles(diff));
97
94
  }
98
95
  export function computeDiffStatsFromFiles(files) {
99
- let added = 0;
100
- let deleted = 0;
101
- for (const file of files) {
102
- added += file.additions;
103
- deleted += file.deletions;
96
+ if (files.length === 0) {
97
+ return EMPTY_STATS;
104
98
  }
105
- return { files: files.length, added, deleted };
99
+ const computed = buildDiffComputation(files);
100
+ return buildStats(files.length, computed.added, computed.deleted);
106
101
  }
107
102
  /** Count changed files, added lines, and deleted lines. */
108
103
  export function computeDiffStats(diff) {
@@ -2,4 +2,5 @@ export interface CachedEnvInt {
2
2
  get(): number;
3
3
  reset(): void;
4
4
  }
5
+ /** Creates a cached integer value from an environment variable, with a default fallback. */
5
6
  export declare function createCachedEnvInt(envVar: string, defaultValue: number): CachedEnvInt;
@@ -1,11 +1,18 @@
1
1
  function parsePositiveInteger(value) {
2
+ if (value.length === 0) {
3
+ return undefined;
4
+ }
2
5
  const parsed = Number.parseInt(value, 10);
3
6
  if (Number.isNaN(parsed) || parsed <= 0) {
4
7
  return undefined;
5
8
  }
6
9
  return parsed;
7
10
  }
8
- /// Creates a cached integer value from an environment variable, with a default fallback.
11
+ function resolveEnvInt(envVar, defaultValue) {
12
+ const envValue = process.env[envVar] ?? '';
13
+ return parsePositiveInteger(envValue) ?? defaultValue;
14
+ }
15
+ /** Creates a cached integer value from an environment variable, with a default fallback. */
9
16
  export function createCachedEnvInt(envVar, defaultValue) {
10
17
  let cached;
11
18
  return {
@@ -13,8 +20,7 @@ export function createCachedEnvInt(envVar, defaultValue) {
13
20
  if (cached !== undefined) {
14
21
  return cached;
15
22
  }
16
- const envValue = process.env[envVar] ?? '';
17
- cached = parsePositiveInteger(envValue) ?? defaultValue;
23
+ cached = resolveEnvInt(envVar, defaultValue);
18
24
  return cached;
19
25
  },
20
26
  reset() {
@@ -1,2 +1,3 @@
1
+ /** Matches transient upstream provider failures that are typically safe to retry. */
1
2
  export declare const RETRYABLE_UPSTREAM_ERROR_PATTERN: RegExp;
2
3
  export declare function getErrorMessage(error: unknown): string;
@@ -1,11 +1,11 @@
1
1
  import { inspect } from 'node:util';
2
- /// Guides for using specific tools in code review analysis.
2
+ /** Matches transient upstream provider failures that are typically safe to retry. */
3
3
  export const RETRYABLE_UPSTREAM_ERROR_PATTERN = /(429|500|502|503|504|rate.?limit|quota|overload|unavailable|gateway|timeout|timed.out|connection|reset|econn|enotfound|temporary|transient|invalid.json)/i;
4
- function isString(value) {
5
- return typeof value === 'string';
4
+ function isObjectRecord(value) {
5
+ return typeof value === 'object' && value !== null;
6
6
  }
7
7
  function hasStringProperty(value, key) {
8
- if (typeof value !== 'object' || value === null || !(key in value)) {
8
+ if (!isObjectRecord(value) || !(key in value)) {
9
9
  return false;
10
10
  }
11
11
  const record = value;
@@ -15,7 +15,7 @@ export function getErrorMessage(error) {
15
15
  if (hasStringProperty(error, 'message')) {
16
16
  return error.message;
17
17
  }
18
- if (isString(error)) {
18
+ if (typeof error === 'string') {
19
19
  return error;
20
20
  }
21
21
  return inspect(error, { depth: 3, breakLength: 120 });
@@ -39,8 +39,9 @@ function stripConstraintValue(value) {
39
39
  export function stripJsonSchemaConstraints(schema) {
40
40
  const result = {};
41
41
  for (const [key, value] of Object.entries(schema)) {
42
- if (CONSTRAINT_KEYS.has(key))
42
+ if (CONSTRAINT_KEYS.has(key)) {
43
43
  continue;
44
+ }
44
45
  // Relax integer → number so Gemini is not forced into integer-only
45
46
  // output; the stricter result schema still validates integrality.
46
47
  if (key === 'type' && value === INTEGER_JSON_TYPE) {