@contextrail/code-review-agent 0.1.1-alpha.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 (84) hide show
  1. package/LICENSE +26 -0
  2. package/MODEL_RECOMMENDATIONS.md +178 -0
  3. package/README.md +177 -0
  4. package/dist/config/defaults.d.ts +72 -0
  5. package/dist/config/defaults.js +113 -0
  6. package/dist/config/index.d.ts +34 -0
  7. package/dist/config/index.js +89 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +603 -0
  10. package/dist/llm/factory.d.ts +21 -0
  11. package/dist/llm/factory.js +50 -0
  12. package/dist/llm/index.d.ts +3 -0
  13. package/dist/llm/index.js +2 -0
  14. package/dist/llm/service.d.ts +38 -0
  15. package/dist/llm/service.js +191 -0
  16. package/dist/llm/types.d.ts +119 -0
  17. package/dist/llm/types.js +1 -0
  18. package/dist/logging/logger.d.ts +9 -0
  19. package/dist/logging/logger.js +52 -0
  20. package/dist/mcp/client.d.ts +429 -0
  21. package/dist/mcp/client.js +173 -0
  22. package/dist/mcp/mcp-tools.d.ts +292 -0
  23. package/dist/mcp/mcp-tools.js +40 -0
  24. package/dist/mcp/token-validation.d.ts +31 -0
  25. package/dist/mcp/token-validation.js +57 -0
  26. package/dist/mcp/tools-provider.d.ts +18 -0
  27. package/dist/mcp/tools-provider.js +24 -0
  28. package/dist/observability/index.d.ts +2 -0
  29. package/dist/observability/index.js +1 -0
  30. package/dist/observability/metrics.d.ts +48 -0
  31. package/dist/observability/metrics.js +86 -0
  32. package/dist/orchestrator/agentic-orchestrator.d.ts +29 -0
  33. package/dist/orchestrator/agentic-orchestrator.js +136 -0
  34. package/dist/orchestrator/prompts.d.ts +25 -0
  35. package/dist/orchestrator/prompts.js +98 -0
  36. package/dist/orchestrator/validation.d.ts +2 -0
  37. package/dist/orchestrator/validation.js +7 -0
  38. package/dist/orchestrator/writer.d.ts +4 -0
  39. package/dist/orchestrator/writer.js +17 -0
  40. package/dist/output/aggregator.d.ts +30 -0
  41. package/dist/output/aggregator.js +132 -0
  42. package/dist/output/prompts.d.ts +32 -0
  43. package/dist/output/prompts.js +153 -0
  44. package/dist/output/schema.d.ts +1515 -0
  45. package/dist/output/schema.js +224 -0
  46. package/dist/output/writer.d.ts +31 -0
  47. package/dist/output/writer.js +120 -0
  48. package/dist/review-inputs/chunking.d.ts +29 -0
  49. package/dist/review-inputs/chunking.js +113 -0
  50. package/dist/review-inputs/diff-summary.d.ts +52 -0
  51. package/dist/review-inputs/diff-summary.js +83 -0
  52. package/dist/review-inputs/file-patterns.d.ts +40 -0
  53. package/dist/review-inputs/file-patterns.js +182 -0
  54. package/dist/review-inputs/filtering.d.ts +31 -0
  55. package/dist/review-inputs/filtering.js +53 -0
  56. package/dist/review-inputs/git-diff-provider.d.ts +2 -0
  57. package/dist/review-inputs/git-diff-provider.js +42 -0
  58. package/dist/review-inputs/index.d.ts +46 -0
  59. package/dist/review-inputs/index.js +122 -0
  60. package/dist/review-inputs/path-validation.d.ts +10 -0
  61. package/dist/review-inputs/path-validation.js +37 -0
  62. package/dist/review-inputs/surrounding-context.d.ts +35 -0
  63. package/dist/review-inputs/surrounding-context.js +180 -0
  64. package/dist/review-inputs/triage.d.ts +57 -0
  65. package/dist/review-inputs/triage.js +81 -0
  66. package/dist/reviewers/executor.d.ts +41 -0
  67. package/dist/reviewers/executor.js +357 -0
  68. package/dist/reviewers/findings-merge.d.ts +9 -0
  69. package/dist/reviewers/findings-merge.js +131 -0
  70. package/dist/reviewers/iteration.d.ts +17 -0
  71. package/dist/reviewers/iteration.js +95 -0
  72. package/dist/reviewers/persistence.d.ts +17 -0
  73. package/dist/reviewers/persistence.js +55 -0
  74. package/dist/reviewers/progress-tracker.d.ts +115 -0
  75. package/dist/reviewers/progress-tracker.js +194 -0
  76. package/dist/reviewers/prompt.d.ts +42 -0
  77. package/dist/reviewers/prompt.js +246 -0
  78. package/dist/reviewers/tool-call-tracker.d.ts +18 -0
  79. package/dist/reviewers/tool-call-tracker.js +40 -0
  80. package/dist/reviewers/types.d.ts +12 -0
  81. package/dist/reviewers/types.js +1 -0
  82. package/dist/reviewers/validation-rules.d.ts +27 -0
  83. package/dist/reviewers/validation-rules.js +189 -0
  84. package/package.json +79 -0
@@ -0,0 +1,180 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { isDiffInputs, isFileListInputs } from './index.js';
4
+ import { validateAndNormalizePath } from './path-validation.js';
5
+ const DEFAULT_MAX_TOKENS_PER_FILE = 20_000;
6
+ const DEFAULT_CONTEXT_LINES = 10;
7
+ /**
8
+ * Rough token estimation: ~4 characters per token.
9
+ * This is a conservative estimate for code/diff content.
10
+ */
11
+ const estimateTokens = (text) => {
12
+ return Math.ceil(text.length / 4);
13
+ };
14
+ /**
15
+ * Extract line numbers from a diff that have changes (additions or deletions).
16
+ * Returns a set of line numbers (1-indexed) in the new file version.
17
+ */
18
+ const extractChangedLines = (diff) => {
19
+ const changedLines = new Set();
20
+ const lines = diff.split('\n');
21
+ let currentLine = 0;
22
+ for (const line of lines) {
23
+ if (line.startsWith('@@')) {
24
+ // Parse hunk header: @@ -oldStart,oldCount +newStart,newCount @@
25
+ const match = line.match(/@@\s*-\d+(?:,\d+)?\s*\+(\d+)(?:,(\d+))?/);
26
+ if (match && match[1]) {
27
+ currentLine = parseInt(match[1], 10);
28
+ }
29
+ continue;
30
+ }
31
+ if (line.startsWith('+') && !line.startsWith('+++')) {
32
+ // Added line
33
+ changedLines.add(currentLine);
34
+ currentLine++;
35
+ }
36
+ else if (line.startsWith('-') && !line.startsWith('---')) {
37
+ // Deleted line (don't increment currentLine as it's in old version)
38
+ // But we still want context around deletions
39
+ if (currentLine > 0) {
40
+ changedLines.add(currentLine);
41
+ }
42
+ }
43
+ else if (!line.startsWith('\\')) {
44
+ // Context line (unchanged)
45
+ currentLine++;
46
+ }
47
+ }
48
+ return changedLines;
49
+ };
50
+ /**
51
+ * Extract windowed context around changed lines.
52
+ * Returns the file content with only the relevant windowed sections.
53
+ */
54
+ const extractWindowedContext = (fileContent, changedLines, contextLines) => {
55
+ const lines = fileContent.split('\n');
56
+ const includedLines = new Set();
57
+ // Mark changed lines and surrounding context
58
+ for (const changedLine of changedLines) {
59
+ const start = Math.max(1, changedLine - contextLines);
60
+ const end = Math.min(lines.length, changedLine + contextLines);
61
+ for (let i = start; i <= end; i++) {
62
+ includedLines.add(i);
63
+ }
64
+ }
65
+ // Build result with line numbers and ellipsis for gaps
66
+ const result = [];
67
+ let lastIncluded = 0;
68
+ for (let i = 1; i <= lines.length; i++) {
69
+ if (includedLines.has(i)) {
70
+ if (lastIncluded > 0 && i - lastIncluded > 1) {
71
+ // Add ellipsis for gap
72
+ result.push(`// ... (${i - lastIncluded - 1} lines omitted) ...`);
73
+ }
74
+ const line = lines[i - 1];
75
+ if (line !== undefined) {
76
+ result.push(line);
77
+ }
78
+ lastIncluded = i;
79
+ }
80
+ }
81
+ return result.join('\n');
82
+ };
83
+ /**
84
+ * Extract full file content or windowed excerpt based on token budget.
85
+ */
86
+ const extractFileContext = async (filePath, diff, config) => {
87
+ const maxTokens = config.maxTokensPerFile ?? DEFAULT_MAX_TOKENS_PER_FILE;
88
+ const contextLines = config.contextLines ?? DEFAULT_CONTEXT_LINES;
89
+ try {
90
+ // Validate path to prevent path traversal attacks
91
+ if (!config.basePath) {
92
+ throw new Error('basePath is required for path validation');
93
+ }
94
+ const validatedPath = validateAndNormalizePath(filePath, config.basePath);
95
+ const resolvedPath = path.resolve(config.basePath, validatedPath);
96
+ const fileContent = await readFile(resolvedPath, 'utf-8');
97
+ const fileTokens = estimateTokens(fileContent);
98
+ // If file fits within budget, return full content
99
+ if (fileTokens <= maxTokens) {
100
+ return fileContent;
101
+ }
102
+ // If we have a diff, extract windowed context around changed lines
103
+ if (diff) {
104
+ const changedLines = extractChangedLines(diff);
105
+ if (changedLines.size > 0) {
106
+ const windowed = extractWindowedContext(fileContent, changedLines, contextLines);
107
+ const windowedTokens = estimateTokens(windowed);
108
+ // If windowed context fits, return it
109
+ if (windowedTokens <= maxTokens) {
110
+ return windowed;
111
+ }
112
+ // Otherwise, return truncated version with note
113
+ const truncated = fileContent.slice(0, maxTokens * 4);
114
+ return `${truncated}\n// ... (file truncated due to token budget) ...`;
115
+ }
116
+ }
117
+ // No diff or no changed lines: return truncated version
118
+ const truncated = fileContent.slice(0, maxTokens * 4);
119
+ return `${truncated}\n// ... (file truncated due to token budget) ...`;
120
+ }
121
+ catch {
122
+ // File not found or unreadable - return null to indicate no context available
123
+ return null;
124
+ }
125
+ };
126
+ /**
127
+ * Extract surrounding context for diff review inputs.
128
+ */
129
+ const extractDiffContext = async (inputs, config) => {
130
+ const context = {};
131
+ for (const file of inputs.files) {
132
+ const diff = inputs.diffs[file];
133
+ const fileContext = await extractFileContext(file, diff, {
134
+ ...config,
135
+ basePath: config.basePath,
136
+ });
137
+ if (fileContext !== null) {
138
+ context[file] = fileContext;
139
+ }
140
+ }
141
+ return context;
142
+ };
143
+ /**
144
+ * Extract surrounding context for file-list review inputs.
145
+ */
146
+ const extractFileListContext = async (inputs, config) => {
147
+ if (!config.basePath) {
148
+ throw new Error('basePath is required for file-list mode context extraction');
149
+ }
150
+ const context = {};
151
+ for (const file of inputs.files) {
152
+ const fileContext = await extractFileContext(file, undefined, config);
153
+ if (fileContext !== null) {
154
+ context[file] = fileContext;
155
+ }
156
+ }
157
+ return context;
158
+ };
159
+ /**
160
+ * Extract surrounding code context for review inputs.
161
+ * Returns a map of file paths to their context (full file or windowed excerpt).
162
+ *
163
+ * @param inputs - Review inputs (diff or file-list)
164
+ * @param config - Context extraction configuration
165
+ * @returns Map of file paths to context strings (null values indicate unavailable context)
166
+ */
167
+ export const extractSurroundingContext = async (inputs, config = {}) => {
168
+ if (config.enabled === false) {
169
+ return {};
170
+ }
171
+ if (isDiffInputs(inputs)) {
172
+ return extractDiffContext(inputs, config);
173
+ }
174
+ if (isFileListInputs(inputs)) {
175
+ return extractFileListContext(inputs, config);
176
+ }
177
+ // TypeScript exhaustiveness check
178
+ const _exhaustive = inputs;
179
+ return _exhaustive;
180
+ };
@@ -0,0 +1,57 @@
1
+ import type { ReviewInputs } from './index.js';
2
+ /**
3
+ * Configuration for PR triage.
4
+ */
5
+ export type TriageConfig = {
6
+ /**
7
+ * Maximum number of files to consider a PR trivial.
8
+ * Default: DEFAULT_TRIAGE_MAX_FILES
9
+ */
10
+ maxFiles?: number;
11
+ /**
12
+ * Maximum total lines changed (added + removed) to consider a PR trivial.
13
+ * Default: DEFAULT_TRIAGE_MAX_TOTAL_LINES
14
+ */
15
+ maxTotalLines?: number;
16
+ /**
17
+ * File extensions considered documentation-only.
18
+ * Default: DEFAULT_TRIAGE_DOCS_ONLY_EXTENSIONS
19
+ */
20
+ docsOnlyExtensions?: ReadonlyArray<string>;
21
+ };
22
+ /**
23
+ * Result of triage analysis.
24
+ */
25
+ export type TriageResult = {
26
+ /**
27
+ * Whether the PR is considered trivial.
28
+ */
29
+ isTrivial: boolean;
30
+ /**
31
+ * Reason for the triage decision.
32
+ */
33
+ reason: string;
34
+ /**
35
+ * Whether the PR contains only documentation files.
36
+ */
37
+ isDocsOnly: boolean;
38
+ /**
39
+ * Number of files changed.
40
+ */
41
+ fileCount: number;
42
+ /**
43
+ * Total lines changed (added + removed).
44
+ */
45
+ totalLinesChanged: number;
46
+ };
47
+ /**
48
+ * Analyze review inputs to determine if the PR is trivial.
49
+ * A PR is considered trivial if:
50
+ * - It contains only documentation files, OR
51
+ * - It has few files AND small total diff size
52
+ *
53
+ * @param inputs - Review inputs to analyze
54
+ * @param config - Triage configuration
55
+ * @returns Triage result with decision and metadata
56
+ */
57
+ export declare const triagePr: (inputs: ReviewInputs, config?: TriageConfig) => TriageResult;
@@ -0,0 +1,81 @@
1
+ import path from 'node:path';
2
+ import { generateDiffSummary } from './diff-summary.js';
3
+ import { DEFAULT_TRIAGE_MAX_FILES, DEFAULT_TRIAGE_MAX_TOTAL_LINES, DEFAULT_TRIAGE_DOCS_ONLY_EXTENSIONS, } from '../config/defaults.js';
4
+ /**
5
+ * Check if a file extension is considered documentation-only.
6
+ *
7
+ * @param filePath - File path to check
8
+ * @param docsOnlyExtensions - List of documentation file extensions
9
+ * @returns True if file is documentation-only
10
+ */
11
+ const isDocsOnlyFile = (filePath, docsOnlyExtensions) => {
12
+ const ext = path.extname(filePath).toLowerCase();
13
+ // Check extension
14
+ if (docsOnlyExtensions.includes(ext)) {
15
+ return true;
16
+ }
17
+ // Check common documentation filenames (case-insensitive)
18
+ const docsFilenames = ['readme', 'changelog', 'license', 'contributing', 'authors', 'credits'];
19
+ const baseName = path.basename(filePath, ext).toLowerCase();
20
+ if (docsFilenames.includes(baseName)) {
21
+ return true;
22
+ }
23
+ return false;
24
+ };
25
+ /**
26
+ * Analyze review inputs to determine if the PR is trivial.
27
+ * A PR is considered trivial if:
28
+ * - It contains only documentation files, OR
29
+ * - It has few files AND small total diff size
30
+ *
31
+ * @param inputs - Review inputs to analyze
32
+ * @param config - Triage configuration
33
+ * @returns Triage result with decision and metadata
34
+ */
35
+ export const triagePr = (inputs, config = {}) => {
36
+ const maxFiles = config.maxFiles ?? DEFAULT_TRIAGE_MAX_FILES;
37
+ const maxTotalLines = config.maxTotalLines ?? DEFAULT_TRIAGE_MAX_TOTAL_LINES;
38
+ const docsOnlyExtensions = config.docsOnlyExtensions ?? DEFAULT_TRIAGE_DOCS_ONLY_EXTENSIONS;
39
+ const fileCount = inputs.files.length;
40
+ const summary = generateDiffSummary(inputs);
41
+ const totalLinesChanged = summary.totalAdded + summary.totalRemoved;
42
+ // Empty PRs are not trivial
43
+ if (fileCount === 0) {
44
+ return {
45
+ isTrivial: false,
46
+ reason: 'PR contains no files',
47
+ isDocsOnly: false,
48
+ fileCount: 0,
49
+ totalLinesChanged: 0,
50
+ };
51
+ }
52
+ // Check if all files are documentation-only
53
+ const allDocsOnly = inputs.files.every((file) => isDocsOnlyFile(file, docsOnlyExtensions));
54
+ if (allDocsOnly && fileCount > 0) {
55
+ return {
56
+ isTrivial: true,
57
+ reason: `PR contains only documentation files (${fileCount} file${fileCount === 1 ? '' : 's'})`,
58
+ isDocsOnly: true,
59
+ fileCount,
60
+ totalLinesChanged,
61
+ };
62
+ }
63
+ // Check if PR is small enough to be trivial
64
+ if (fileCount <= maxFiles && totalLinesChanged <= maxTotalLines) {
65
+ return {
66
+ isTrivial: true,
67
+ reason: `PR is small (${fileCount} file${fileCount === 1 ? '' : 's'}, ${totalLinesChanged} line${totalLinesChanged === 1 ? '' : 's'} changed)`,
68
+ isDocsOnly: false,
69
+ fileCount,
70
+ totalLinesChanged,
71
+ };
72
+ }
73
+ // PR is not trivial
74
+ return {
75
+ isTrivial: false,
76
+ reason: `PR exceeds trivial thresholds (${fileCount} file${fileCount === 1 ? '' : 's'}, ${totalLinesChanged} line${totalLinesChanged === 1 ? '' : 's'} changed)`,
77
+ isDocsOnly: false,
78
+ fileCount,
79
+ totalLinesChanged,
80
+ };
81
+ };
@@ -0,0 +1,41 @@
1
+ import type { ReviewInputs } from '../review-inputs/index.js';
2
+ import { type ChunkingConfig } from '../review-inputs/chunking.js';
3
+ import { type FilePatterns } from '../review-inputs/file-patterns.js';
4
+ import type { McpClient } from '../mcp/client.js';
5
+ import type { ReviewerFindings } from '../output/schema.js';
6
+ import type { Logger } from '../logging/logger.js';
7
+ /**
8
+ * Extract filePatterns from prompt metadata.
9
+ *
10
+ * @param metadata - Prompt metadata (may be undefined or have nested structure)
11
+ * @returns FilePatterns if found, undefined otherwise
12
+ */
13
+ export declare function extractFilePatterns(metadata: unknown): FilePatterns | undefined;
14
+ export type AgenticExecutorConfig = {
15
+ openRouterApiKey: string;
16
+ reviewerModel: string;
17
+ criticModel?: string;
18
+ maxSteps?: number;
19
+ maxIterations?: number;
20
+ chunking?: ChunkingConfig;
21
+ prDescription?: string;
22
+ reviewDomains?: string[];
23
+ };
24
+ export type AgenticExecutorDeps = {
25
+ mcpClient: McpClient;
26
+ config: AgenticExecutorConfig;
27
+ logger?: Logger;
28
+ };
29
+ /**
30
+ * Run reviewer loop with self-validation.
31
+ * Each iteration is a fresh generateText call (no history bleed).
32
+ * Based on context://agentic/execution/loop-orchestration
33
+ *
34
+ * @param reviewer - Reviewer name (e.g., 'reviewer-security')
35
+ * @param inputs - Review inputs (files, diffs)
36
+ * @param understanding - Orchestrator's understanding of changes
37
+ * @param outputDir - Base output directory
38
+ * @param deps - Dependencies (MCP client, config)
39
+ * @returns Reviewer findings
40
+ */
41
+ export declare const runReviewerLoop: (reviewer: string, inputs: ReviewInputs, understanding: string, outputDir: string, deps: AgenticExecutorDeps) => Promise<ReviewerFindings>;