@contextrail/code-review-agent 0.1.1 → 0.1.2-alpha.2
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/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +48 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/parser.d.ts +3 -0
- package/dist/cli/parser.js +144 -0
- package/dist/cli/types.d.ts +17 -0
- package/dist/cli/types.js +1 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +16 -0
- package/dist/errors/error-utils.d.ts +2 -0
- package/dist/errors/error-utils.js +37 -0
- package/dist/index.js +40 -578
- package/dist/lifecycle.d.ts +4 -0
- package/dist/lifecycle.js +52 -0
- package/dist/orchestrator/agentic-orchestrator.d.ts +2 -0
- package/dist/orchestrator/agentic-orchestrator.js +9 -13
- package/dist/orchestrator/writer.js +1 -1
- package/dist/output/aggregator.d.ts +2 -1
- package/dist/output/aggregator.js +3 -12
- package/dist/output/schema.d.ts +86 -86
- package/dist/output/summary-logger.d.ts +3 -0
- package/dist/output/summary-logger.js +81 -0
- package/dist/pipeline.d.ts +25 -0
- package/dist/pipeline.js +276 -0
- package/dist/prompts/blocks.d.ts +25 -0
- package/dist/prompts/blocks.js +129 -0
- package/dist/prompts/decision.d.ts +15 -0
- package/dist/prompts/decision.js +30 -0
- package/dist/prompts/index.d.ts +5 -0
- package/dist/prompts/index.js +5 -0
- package/dist/{orchestrator/prompts.d.ts → prompts/orchestrator.d.ts} +2 -3
- package/dist/{orchestrator/prompts.js → prompts/orchestrator.js} +2 -3
- package/dist/prompts/reviewer.d.ts +12 -0
- package/dist/prompts/reviewer.js +107 -0
- package/dist/{output/prompts.d.ts → prompts/synthesis.d.ts} +1 -16
- package/dist/{output/prompts.js → prompts/synthesis.js} +0 -30
- package/dist/review-inputs/filtering.d.ts +14 -0
- package/dist/review-inputs/filtering.js +97 -8
- package/dist/review-inputs/index.js +59 -13
- package/dist/reviewers/executor.d.ts +2 -0
- package/dist/reviewers/executor.js +1 -8
- package/dist/reviewers/prompt.d.ts +2 -28
- package/dist/reviewers/prompt.js +5 -235
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReviewInputs } from '../review-inputs/index.js';
|
|
2
|
+
import type { ReviewerFindings } from '../output/schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build a critic prompt that challenges findings from a previous iteration.
|
|
5
|
+
* The critic pass validates findings, removes false positives, and enforces evidence/standards.
|
|
6
|
+
*/
|
|
7
|
+
export declare const buildCriticPrompt: (inputs: ReviewInputs, understanding: string, findings: ReviewerFindings, prDescription?: string, reviewDomains?: string[]) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Build a standard reviewer prompt for initial review (iteration 1).
|
|
10
|
+
*/
|
|
11
|
+
export declare const buildStandardReviewPrompt: (inputs: ReviewInputs, understanding: string, prDescription?: string, reviewDomains?: string[]) => string;
|
|
12
|
+
export declare const buildReviewerUserMessage: (inputs: ReviewInputs, understanding: string, iteration: number, findings: ReviewerFindings | null, prDescription?: string, reviewDomains?: string[]) => string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isDiffInputs } from '../review-inputs/index.js';
|
|
2
|
+
import { SEVERITY_VALIDATION_RULES, CONTEXTRAIL_TOOLING_BLOCK, OUTPUT_CONTRACT_BLOCK, FINAL_CHECKLIST_BLOCK, } from './blocks.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build a critic prompt that challenges findings from a previous iteration.
|
|
5
|
+
* The critic pass validates findings, removes false positives, and enforces evidence/standards.
|
|
6
|
+
*/
|
|
7
|
+
export const buildCriticPrompt = (inputs, understanding, findings, prDescription, reviewDomains) => {
|
|
8
|
+
const diffBlock = isDiffInputs(inputs)
|
|
9
|
+
? `\nDiffs:\n${Object.entries(inputs.diffs)
|
|
10
|
+
.map(([file, diff]) => `\n## ${file}\n\`\`\`diff\n${diff}\n\`\`\``)
|
|
11
|
+
.join('\n')}`
|
|
12
|
+
: '';
|
|
13
|
+
const contextBlock = inputs.context && Object.keys(inputs.context).length > 0
|
|
14
|
+
? `\n\nSurrounding Code Context:\n${Object.entries(inputs.context)
|
|
15
|
+
.map(([file, context]) => `\n## ${file}\n\`\`\`\n${context}\n\`\`\``)
|
|
16
|
+
.join('\n')}`
|
|
17
|
+
: '';
|
|
18
|
+
const prDescriptionBlock = prDescription ? `\n\nPR Description:\n${prDescription}\n` : '';
|
|
19
|
+
const reviewDomainsBlock = reviewDomains && reviewDomains.length > 0
|
|
20
|
+
? `\n\nReview Focus Domains:\n${reviewDomains.map((domain) => `- ${domain}`).join('\n')}\n`
|
|
21
|
+
: '';
|
|
22
|
+
return `You are performing a CRITIC PASS to validate and challenge findings from the previous iteration.
|
|
23
|
+
|
|
24
|
+
Your role is to:
|
|
25
|
+
1. Challenge each finding - require concrete evidence from the code or ContextRail standards
|
|
26
|
+
2. Remove false positives - findings that lack evidence or don't violate actual standards
|
|
27
|
+
3. Enforce ContextRail standards - each finding MUST be grounded in retrieved contexts
|
|
28
|
+
4. Validate severity - ensure severity matches the actual risk level using the severity validation rules below
|
|
29
|
+
5. Only keep findings that are:
|
|
30
|
+
- Supported by evidence in the code/diffs
|
|
31
|
+
- Linked to specific ContextRail standards (contextIdsUsed or contextIdsViolated)
|
|
32
|
+
- Accurately categorized by severity
|
|
33
|
+
|
|
34
|
+
${SEVERITY_VALIDATION_RULES}
|
|
35
|
+
${CONTEXTRAIL_TOOLING_BLOCK}
|
|
36
|
+
|
|
37
|
+
Context:
|
|
38
|
+
${understanding}
|
|
39
|
+
${prDescriptionBlock}${reviewDomainsBlock}
|
|
40
|
+
Files:
|
|
41
|
+
${inputs.files.map((f) => `- ${f}`).join('\n')}
|
|
42
|
+
${diffBlock}${contextBlock}
|
|
43
|
+
|
|
44
|
+
Previous findings (iteration 1) - CRITICALLY REVIEW THESE:
|
|
45
|
+
${JSON.stringify(findings.findings, null, 2)}
|
|
46
|
+
|
|
47
|
+
For each finding, ask yourself:
|
|
48
|
+
- Is there concrete evidence in the code/diffs that supports this finding?
|
|
49
|
+
- Does this finding reference specific ContextRail standards (contextIdsUsed or contextIdsViolated)?
|
|
50
|
+
- Is the severity appropriate for the actual risk? (Apply severity validation rules above)
|
|
51
|
+
- Could this be a false positive that should be removed?
|
|
52
|
+
- Should the severity be downgraded based on the validation rules?
|
|
53
|
+
|
|
54
|
+
Output requirements:
|
|
55
|
+
- Remove any findings that lack evidence
|
|
56
|
+
- Remove findings that claim ContextRail impact but do not include retrieved context attribution
|
|
57
|
+
- Keep only findings that are well-supported and properly attributed
|
|
58
|
+
- Validate severity using the rules above - downgrade if evidence doesn't support the assigned level
|
|
59
|
+
- Set validated: true only if all remaining findings meet these criteria
|
|
60
|
+
- Always include notes field: use a string when you have notes, or null when none
|
|
61
|
+
- If you remove findings or downgrade severity, explain why in notes
|
|
62
|
+
- In notes, summarize which ContextRail contexts were retrieved (or state that none were relevant after tool lookup)
|
|
63
|
+
|
|
64
|
+
${OUTPUT_CONTRACT_BLOCK}
|
|
65
|
+
${FINAL_CHECKLIST_BLOCK}`;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Build a standard reviewer prompt for initial review (iteration 1).
|
|
69
|
+
*/
|
|
70
|
+
export const buildStandardReviewPrompt = (inputs, understanding, prDescription, reviewDomains) => {
|
|
71
|
+
const diffBlock = isDiffInputs(inputs)
|
|
72
|
+
? `\nDiffs:\n${Object.entries(inputs.diffs)
|
|
73
|
+
.map(([file, diff]) => `\n## ${file}\n\`\`\`diff\n${diff}\n\`\`\``)
|
|
74
|
+
.join('\n')}`
|
|
75
|
+
: '';
|
|
76
|
+
const contextBlock = inputs.context && Object.keys(inputs.context).length > 0
|
|
77
|
+
? `\n\nSurrounding Code Context:\n${Object.entries(inputs.context)
|
|
78
|
+
.map(([file, context]) => `\n## ${file}\n\`\`\`\n${context}\n\`\`\``)
|
|
79
|
+
.join('\n')}`
|
|
80
|
+
: '';
|
|
81
|
+
const prDescriptionBlock = prDescription ? `\n\nPR Description:\n${prDescription}\n` : '';
|
|
82
|
+
const reviewDomainsBlock = reviewDomains && reviewDomains.length > 0
|
|
83
|
+
? `\n\nReview Focus Domains:\n${reviewDomains.map((domain) => `- ${domain}`).join('\n')}\n`
|
|
84
|
+
: '';
|
|
85
|
+
return `Review these changes using the reviewer prompt guidance.
|
|
86
|
+
|
|
87
|
+
Context:
|
|
88
|
+
${understanding}
|
|
89
|
+
${prDescriptionBlock}${reviewDomainsBlock}
|
|
90
|
+
Files:
|
|
91
|
+
${inputs.files.map((f) => `- ${f}`).join('\n')}
|
|
92
|
+
${diffBlock}${contextBlock}
|
|
93
|
+
|
|
94
|
+
Please review these changes and provide findings.
|
|
95
|
+
|
|
96
|
+
${CONTEXTRAIL_TOOLING_BLOCK}
|
|
97
|
+
${OUTPUT_CONTRACT_BLOCK}
|
|
98
|
+
${FINAL_CHECKLIST_BLOCK}`;
|
|
99
|
+
};
|
|
100
|
+
export const buildReviewerUserMessage = (inputs, understanding, iteration, findings, prDescription, reviewDomains) => {
|
|
101
|
+
// Use critic pass for iteration 2+
|
|
102
|
+
if (iteration > 1 && findings) {
|
|
103
|
+
return buildCriticPrompt(inputs, understanding, findings, prDescription, reviewDomains);
|
|
104
|
+
}
|
|
105
|
+
// Use standard review for iteration 1
|
|
106
|
+
return buildStandardReviewPrompt(inputs, understanding, prDescription, reviewDomains);
|
|
107
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReviewerResult } from '
|
|
1
|
+
import type { ReviewerResult } from '../output/schema.js';
|
|
2
2
|
/**
|
|
3
3
|
* Generate a human-readable pre-summary from reviewer results.
|
|
4
4
|
* Includes severity counts and top findings per reviewer.
|
|
@@ -15,18 +15,3 @@ export declare const generatePreSummary: (reviewerResults: ReviewerResult[]) =>
|
|
|
15
15
|
* @returns Synthesis prompt string
|
|
16
16
|
*/
|
|
17
17
|
export declare const buildSynthesisPrompt: (reviewerResults: ReviewerResult[]) => string;
|
|
18
|
-
export declare const buildReviewDecisionPrompt: (understanding: string, synthesisResult: {
|
|
19
|
-
findings: Array<{
|
|
20
|
-
severity: string;
|
|
21
|
-
title: string;
|
|
22
|
-
description: string;
|
|
23
|
-
}>;
|
|
24
|
-
contradictions: Array<{
|
|
25
|
-
context: string;
|
|
26
|
-
}>;
|
|
27
|
-
compoundRisks: Array<{
|
|
28
|
-
description: string;
|
|
29
|
-
affectedFindings: string[];
|
|
30
|
-
severity: string;
|
|
31
|
-
}>;
|
|
32
|
-
}) => string;
|
|
@@ -121,33 +121,3 @@ Output requirements:
|
|
|
121
121
|
- \`notes\`: Always include this field; use a string when useful, otherwise \`null\`
|
|
122
122
|
`;
|
|
123
123
|
};
|
|
124
|
-
export const buildReviewDecisionPrompt = (understanding, synthesisResult) => {
|
|
125
|
-
const findingsSummary = synthesisResult.findings.length > 0
|
|
126
|
-
? synthesisResult.findings
|
|
127
|
-
.map((f) => `- [${f.severity.toUpperCase()}] ${f.title}: ${f.description}`)
|
|
128
|
-
.join('\n')
|
|
129
|
-
: '- None';
|
|
130
|
-
const contradictionsText = synthesisResult.contradictions.length > 0
|
|
131
|
-
? `\n\nContradictions:\n${synthesisResult.contradictions.map((c) => `- ${c.context}`).join('\n')}`
|
|
132
|
-
: '';
|
|
133
|
-
const compoundRisksText = synthesisResult.compoundRisks.length > 0
|
|
134
|
-
? `\n\nCompound Risks:\n${synthesisResult.compoundRisks.map((r) => `- [${r.severity.toUpperCase()}] ${r.description}`).join('\n')}`
|
|
135
|
-
: '';
|
|
136
|
-
return `You are making a final review decision based on synthesized findings from multiple reviewers.
|
|
137
|
-
|
|
138
|
-
Return "approve" only when there are no critical or major findings.
|
|
139
|
-
Return "request-changes" when there are any critical/major findings.
|
|
140
|
-
Hard rule: if synthesized findings contain zero critical/major items, decision MUST be "approve".
|
|
141
|
-
|
|
142
|
-
Provide:
|
|
143
|
-
- decision: approve | request-changes
|
|
144
|
-
- summary: 2-4 sentences, plain language
|
|
145
|
-
- rationale: explain why the decision was made, referencing key findings or lack thereof
|
|
146
|
-
|
|
147
|
-
Review context:
|
|
148
|
-
${understanding}
|
|
149
|
-
|
|
150
|
-
Synthesized findings:
|
|
151
|
-
${findingsSummary}${contradictionsText}${compoundRisksText}
|
|
152
|
-
`;
|
|
153
|
-
};
|
|
@@ -8,7 +8,21 @@ export type FilteringConfig = {
|
|
|
8
8
|
* Default: DEFAULT_EXCLUDE_PATTERNS
|
|
9
9
|
*/
|
|
10
10
|
excludePatterns?: ReadonlyArray<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Patterns to explicitly re-include after exclusion matching.
|
|
13
|
+
* Intended for `.gitignore` negation rules (`!pattern`).
|
|
14
|
+
*/
|
|
15
|
+
includePatterns?: ReadonlyArray<string>;
|
|
16
|
+
};
|
|
17
|
+
export type GitignorePatterns = {
|
|
18
|
+
excludePatterns: string[];
|
|
19
|
+
includePatterns: string[];
|
|
11
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Parse root `.gitignore` contents into exclude/include glob sets.
|
|
23
|
+
* Negations (`!pattern`) become include patterns used to re-include files.
|
|
24
|
+
*/
|
|
25
|
+
export declare const parseGitignoreContent: (contents: string) => GitignorePatterns;
|
|
12
26
|
/**
|
|
13
27
|
* Filter files based on exclude patterns.
|
|
14
28
|
*
|
|
@@ -1,22 +1,102 @@
|
|
|
1
1
|
import { DEFAULT_EXCLUDE_PATTERNS } from '../config/defaults.js';
|
|
2
2
|
import { fileMatchesPatterns } from './file-patterns.js';
|
|
3
3
|
/**
|
|
4
|
-
* Check if a file path matches any
|
|
4
|
+
* Check if a file path matches any pattern set.
|
|
5
5
|
* Uses minimatch for secure glob pattern matching (prevents ReDoS).
|
|
6
6
|
*
|
|
7
7
|
* @param filePath - File path to check (relative or absolute)
|
|
8
8
|
* @param patterns - Glob patterns to match against
|
|
9
|
-
* @returns True if file
|
|
9
|
+
* @returns True if file matches at least one pattern
|
|
10
10
|
*/
|
|
11
|
-
const
|
|
12
|
-
// Convert patterns to FilePatterns format for consistent matching
|
|
11
|
+
const matchesIncludePatternSet = (filePath, patterns) => {
|
|
12
|
+
// Convert patterns to FilePatterns format for consistent matching.
|
|
13
13
|
const filePatterns = {
|
|
14
|
-
|
|
14
|
+
include: [...patterns],
|
|
15
15
|
};
|
|
16
16
|
// Use fileMatchesPatterns which handles minimatch safely
|
|
17
|
-
// If file matches
|
|
17
|
+
// If file matches include patterns, it should be included
|
|
18
|
+
return fileMatchesPatterns(filePath, filePatterns);
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Check if a file path matches any exclude pattern.
|
|
22
|
+
*
|
|
23
|
+
* @param filePath - File path to check
|
|
24
|
+
* @param patterns - Exclude patterns
|
|
25
|
+
* @returns True when file is matched by exclusion rules
|
|
26
|
+
*/
|
|
27
|
+
const matchesExcludePatternSet = (filePath, patterns) => {
|
|
28
|
+
const filePatterns = {
|
|
29
|
+
exclude: [...patterns],
|
|
30
|
+
};
|
|
31
|
+
// fileMatchesPatterns returns false when exclude matches, so invert it here.
|
|
18
32
|
return !fileMatchesPatterns(filePath, filePatterns);
|
|
19
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Convert a root `.gitignore` rule into a minimatch-style glob pattern.
|
|
36
|
+
* This is intentionally low-lift (root file only) and does not attempt full git parity.
|
|
37
|
+
*/
|
|
38
|
+
const gitignoreRuleToGlob = (rule) => {
|
|
39
|
+
let pattern = rule.trim();
|
|
40
|
+
if (!pattern) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Keep escaped literals for leading comment/negation markers.
|
|
44
|
+
if (pattern.startsWith('\\#')) {
|
|
45
|
+
pattern = pattern.slice(1);
|
|
46
|
+
}
|
|
47
|
+
else if (pattern.startsWith('#')) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (pattern.startsWith('\\!')) {
|
|
51
|
+
pattern = pattern.slice(1);
|
|
52
|
+
}
|
|
53
|
+
// Normalize separators for cross-platform matching.
|
|
54
|
+
pattern = pattern.replace(/\\/g, '/');
|
|
55
|
+
const isRootAnchored = pattern.startsWith('/');
|
|
56
|
+
if (isRootAnchored) {
|
|
57
|
+
pattern = pattern.slice(1);
|
|
58
|
+
}
|
|
59
|
+
const isDirectoryPattern = pattern.endsWith('/');
|
|
60
|
+
if (isDirectoryPattern) {
|
|
61
|
+
pattern = pattern.slice(0, -1);
|
|
62
|
+
}
|
|
63
|
+
if (!pattern) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const hasSlash = pattern.includes('/');
|
|
67
|
+
let glob = hasSlash ? (isRootAnchored ? pattern : `**/${pattern}`) : `**/${pattern}`;
|
|
68
|
+
if (isDirectoryPattern) {
|
|
69
|
+
glob = `${glob}/**`;
|
|
70
|
+
}
|
|
71
|
+
return glob;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Parse root `.gitignore` contents into exclude/include glob sets.
|
|
75
|
+
* Negations (`!pattern`) become include patterns used to re-include files.
|
|
76
|
+
*/
|
|
77
|
+
export const parseGitignoreContent = (contents) => {
|
|
78
|
+
const excludePatterns = [];
|
|
79
|
+
const includePatterns = [];
|
|
80
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
81
|
+
const line = rawLine.trim();
|
|
82
|
+
if (!line) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const isNegation = line.startsWith('!') && !line.startsWith('\\!');
|
|
86
|
+
const normalizedRule = isNegation ? line.slice(1) : line;
|
|
87
|
+
const glob = gitignoreRuleToGlob(normalizedRule);
|
|
88
|
+
if (!glob) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (isNegation) {
|
|
92
|
+
includePatterns.push(glob);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
excludePatterns.push(glob);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { excludePatterns, includePatterns };
|
|
99
|
+
};
|
|
20
100
|
/**
|
|
21
101
|
* Filter files based on exclude patterns.
|
|
22
102
|
*
|
|
@@ -25,8 +105,17 @@ const matchesExcludePattern = (filePath, patterns) => {
|
|
|
25
105
|
* @returns Filtered array of file paths
|
|
26
106
|
*/
|
|
27
107
|
export const filterFiles = (files, config = {}) => {
|
|
28
|
-
const
|
|
29
|
-
|
|
108
|
+
const excludePatterns = config.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS;
|
|
109
|
+
const includePatterns = config.includePatterns ?? [];
|
|
110
|
+
return files.filter((file) => {
|
|
111
|
+
const isExcluded = matchesExcludePatternSet(file, excludePatterns);
|
|
112
|
+
if (!isExcluded) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
// `.gitignore` negations can re-include previously excluded files.
|
|
116
|
+
const isReIncluded = includePatterns.length > 0 && matchesIncludePatternSet(file, includePatterns);
|
|
117
|
+
return isReIncluded;
|
|
118
|
+
});
|
|
30
119
|
};
|
|
31
120
|
/**
|
|
32
121
|
* Filter diff inputs by removing excluded files and their diffs.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { gitDiffProvider } from './git-diff-provider.js';
|
|
4
|
-
import {
|
|
4
|
+
import { DEFAULT_EXCLUDE_PATTERNS } from '../config/defaults.js';
|
|
5
|
+
import { filterDiffInputs, filterFiles, parseGitignoreContent } from './filtering.js';
|
|
5
6
|
export { triagePr } from './triage.js';
|
|
6
7
|
export const isDiffInputs = (inputs) => inputs.mode === 'diff';
|
|
7
8
|
export const isFileListInputs = (inputs) => inputs.mode === 'file-list';
|
|
@@ -45,6 +46,43 @@ const validateFileList = async (files, basePath) => {
|
|
|
45
46
|
const normalizeFiles = (files) => {
|
|
46
47
|
return files.map((file) => file.trim()).filter(Boolean);
|
|
47
48
|
};
|
|
49
|
+
const mergeFilteringWithRootGitignore = async (filtering, repoRoot) => {
|
|
50
|
+
if (!repoRoot) {
|
|
51
|
+
return filtering;
|
|
52
|
+
}
|
|
53
|
+
const gitignorePath = path.join(repoRoot, '.gitignore');
|
|
54
|
+
try {
|
|
55
|
+
const gitignoreContents = await readFile(gitignorePath, 'utf-8');
|
|
56
|
+
if (typeof gitignoreContents !== 'string' || gitignoreContents.length === 0) {
|
|
57
|
+
return filtering;
|
|
58
|
+
}
|
|
59
|
+
const parsed = parseGitignoreContent(gitignoreContents);
|
|
60
|
+
if (parsed.excludePatterns.length === 0 && parsed.includePatterns.length === 0) {
|
|
61
|
+
return filtering;
|
|
62
|
+
}
|
|
63
|
+
const excludePatterns = filtering?.excludePatterns
|
|
64
|
+
? [...filtering.excludePatterns, ...parsed.excludePatterns]
|
|
65
|
+
: [...DEFAULT_EXCLUDE_PATTERNS, ...parsed.excludePatterns];
|
|
66
|
+
const includePatterns = [...(filtering?.includePatterns ?? []), ...parsed.includePatterns];
|
|
67
|
+
return {
|
|
68
|
+
...filtering,
|
|
69
|
+
excludePatterns,
|
|
70
|
+
includePatterns,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
const code = typeof error === 'object' && error !== null && 'code' in error
|
|
76
|
+
? String(error.code)
|
|
77
|
+
: undefined;
|
|
78
|
+
const isMissingFileMessage = message.toLowerCase().includes('no such file') || message.toLowerCase().includes('file not found');
|
|
79
|
+
// Missing .gitignore is expected in many repos; keep defaults without failing.
|
|
80
|
+
if (code === 'ENOENT' || isMissingFileMessage) {
|
|
81
|
+
return filtering;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Failed to read root .gitignore at "${gitignorePath}": ${message}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
48
86
|
export const buildReviewInputs = async (options, deps) => {
|
|
49
87
|
if (options.mode === 'diff') {
|
|
50
88
|
const diffProvider = deps?.diffProvider ?? gitDiffProvider;
|
|
@@ -53,8 +91,9 @@ export const buildReviewInputs = async (options, deps) => {
|
|
|
53
91
|
from: options.from,
|
|
54
92
|
to: options.to,
|
|
55
93
|
});
|
|
94
|
+
const mergedFiltering = await mergeFilteringWithRootGitignore(options.filtering, options.repoPath);
|
|
56
95
|
const normalizedFiles = normalizeFiles(result.files);
|
|
57
|
-
const { files, diffs } = filterDiffInputs(normalizedFiles, result.diffs,
|
|
96
|
+
const { files, diffs } = filterDiffInputs(normalizedFiles, result.diffs, mergedFiltering);
|
|
58
97
|
if (files.length === 0) {
|
|
59
98
|
throw new Error('Diff mode produced no files to review after filtering.');
|
|
60
99
|
}
|
|
@@ -76,8 +115,9 @@ export const buildReviewInputs = async (options, deps) => {
|
|
|
76
115
|
}
|
|
77
116
|
return inputs;
|
|
78
117
|
}
|
|
118
|
+
const mergedFiltering = await mergeFilteringWithRootGitignore(options.filtering, options.basePath);
|
|
79
119
|
const normalizedFiles = normalizeFiles(options.files);
|
|
80
|
-
const files = filterFiles(normalizedFiles,
|
|
120
|
+
const files = filterFiles(normalizedFiles, mergedFiltering);
|
|
81
121
|
if (files.length === 0) {
|
|
82
122
|
throw new Error('File list produced no files to review after filtering.');
|
|
83
123
|
}
|
|
@@ -108,15 +148,21 @@ export const buildReviewInputs = async (options, deps) => {
|
|
|
108
148
|
return inputs;
|
|
109
149
|
};
|
|
110
150
|
export const persistReviewInputs = async (inputs, outputDir) => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
151
|
+
try {
|
|
152
|
+
await mkdir(outputDir, { recursive: true });
|
|
153
|
+
const filesPayload = JSON.stringify({ mode: inputs.mode, files: inputs.files }, null, 2);
|
|
154
|
+
await writeFile(path.join(outputDir, 'files.json'), filesPayload);
|
|
155
|
+
if (inputs.mode !== 'diff') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const [filePath, diff] of Object.entries(inputs.diffs)) {
|
|
159
|
+
const diffPath = path.join(outputDir, 'diff', `${filePath}.diff`);
|
|
160
|
+
await mkdir(path.dirname(diffPath), { recursive: true });
|
|
161
|
+
await writeFile(diffPath, diff);
|
|
162
|
+
}
|
|
116
163
|
}
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
await writeFile(diffPath, diff);
|
|
164
|
+
catch (error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
throw new Error(`Failed to persist review inputs to "${outputDir}": ${message}`);
|
|
121
167
|
}
|
|
122
168
|
};
|
|
@@ -4,6 +4,7 @@ import { type FilePatterns } from '../review-inputs/file-patterns.js';
|
|
|
4
4
|
import type { McpClient } from '../mcp/client.js';
|
|
5
5
|
import type { ReviewerFindings } from '../output/schema.js';
|
|
6
6
|
import type { Logger } from '../logging/logger.js';
|
|
7
|
+
import type { LlmService } from '../llm/service.js';
|
|
7
8
|
/**
|
|
8
9
|
* Extract filePatterns from prompt metadata.
|
|
9
10
|
*
|
|
@@ -23,6 +24,7 @@ export type AgenticExecutorConfig = {
|
|
|
23
24
|
};
|
|
24
25
|
export type AgenticExecutorDeps = {
|
|
25
26
|
mcpClient: McpClient;
|
|
27
|
+
llmService: LlmService;
|
|
26
28
|
config: AgenticExecutorConfig;
|
|
27
29
|
logger?: Logger;
|
|
28
30
|
};
|
|
@@ -9,7 +9,6 @@ import { runReviewerIteration } from './iteration.js';
|
|
|
9
9
|
import { logCompletion, logContinuation, logIteration, writeFindings, writeTokenMetrics } from './persistence.js';
|
|
10
10
|
import { mergeIterationTracking, mergeTokenUsage } from './tool-call-tracker.js';
|
|
11
11
|
import { mergeChunkFindings } from './findings-merge.js';
|
|
12
|
-
import { createLlmService } from '../llm/factory.js';
|
|
13
12
|
const formatIssuePath = (pathSegments) => {
|
|
14
13
|
if (!Array.isArray(pathSegments) || pathSegments.length === 0) {
|
|
15
14
|
return '(root)';
|
|
@@ -115,7 +114,7 @@ export function extractFilePatterns(metadata) {
|
|
|
115
114
|
* @returns Reviewer findings
|
|
116
115
|
*/
|
|
117
116
|
export const runReviewerLoop = async (reviewer, inputs, understanding, outputDir, deps) => {
|
|
118
|
-
const { mcpClient, config, logger } = deps;
|
|
117
|
+
const { mcpClient, llmService, config, logger } = deps;
|
|
119
118
|
const maxIterations = config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
120
119
|
const reviewersDir = path.join(outputDir, 'reviewers');
|
|
121
120
|
const reviewerDir = path.join(reviewersDir, reviewer);
|
|
@@ -193,12 +192,6 @@ export const runReviewerLoop = async (reviewer, inputs, understanding, outputDir
|
|
|
193
192
|
else {
|
|
194
193
|
logger?.debug(`[${reviewer}] No file pattern scope found in prompt metadata; reviewing all ${inputs.files.length} files`);
|
|
195
194
|
}
|
|
196
|
-
// Create LLM service with MCP tools
|
|
197
|
-
const { service: llmService } = createLlmService({
|
|
198
|
-
openRouterApiKey: config.openRouterApiKey,
|
|
199
|
-
mcpClient,
|
|
200
|
-
logger,
|
|
201
|
-
});
|
|
202
195
|
const maxSteps = config.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
203
196
|
let findings = null;
|
|
204
197
|
let iteration = 0;
|
|
@@ -1,34 +1,9 @@
|
|
|
1
|
-
import { type ReviewInputs } from '../review-inputs/index.js';
|
|
2
|
-
import type { ReviewerFindings } from '../output/schema.js';
|
|
3
1
|
export type PromptMessage = {
|
|
4
2
|
role: 'system' | 'user' | 'assistant';
|
|
5
3
|
content: string;
|
|
6
4
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* This block is injected into reviewer system prompts to ensure consistent severity assignment.
|
|
10
|
-
*/
|
|
11
|
-
export declare const SEVERITY_CALIBRATION_BLOCK = "\n## Severity Calibration\n\nYou must assign severity levels accurately based on the following definitions and examples:\n\n### Critical\n**Definition**: Blocks deployment, active exploit path, or data corruption risk. Findings that could lead to security breaches, data loss, or system compromise.\n\n**Examples**:\n- SQL injection vulnerability: `const query = `SELECT * FROM users WHERE id = ${userInput}`;` (user input directly interpolated)\n- Authentication bypass: `if (user.role === 'admin' || user.id === 1) { grantAccess(); }` (hardcoded admin check)\n- Missing input validation on sensitive operations: `await db.delete(userId);` (no validation that userId belongs to requester)\n\n**When to use**: Only when there is a clear exploit path or risk of data corruption/integrity violation.\n\n### Major\n**Definition**: Significant bug, breaks functionality, or violates established patterns. Findings that cause incorrect behavior or violate architectural standards.\n\n**Examples**:\n- Missing error handling: `const data = await fetch(url); return data.json();` (no try-catch, will crash on network error)\n- Tight coupling: `import { DatabaseConnection } from './db'; class UserService { private db = new DatabaseConnection(); }` (direct instantiation, violates dependency injection)\n- Race condition: `let count = 0; async function increment() { count++; await save(count); }` (non-atomic increment)\n\n**When to use**: When functionality is broken or architectural patterns are violated, but no immediate security/data risk.\n\n### Minor\n**Definition**: Code quality issue, convention violation, or maintainability concern. Findings that don't break functionality but reduce code quality.\n\n**Examples**:\n- Magic numbers: `if (user.age > 18) { ... }` (should be `const MIN_ADULT_AGE = 18`)\n- Inconsistent naming: `function getUserData() { ... }` but `function fetch_user_info() { ... }` (mixed naming conventions)\n- Missing JSDoc: `function calculateTotal(items) { ... }` (no documentation for complex logic)\n\n**When to use**: Code quality, readability, or maintainability issues that don't affect functionality.\n\n### Info\n**Definition**: Observation, suggestion, or educational note. Findings that provide helpful context or suggestions without indicating a problem.\n\n**Examples**:\n- Performance suggestion: `// Consider caching this query result if called frequently`\n- Pattern suggestion: `// This could use the Repository pattern for better testability`\n- Documentation opportunity: `// This algorithm implements the Fisher-Yates shuffle`\n\n**When to use**: Helpful suggestions or observations that don't indicate actual problems.\n";
|
|
12
|
-
/**
|
|
13
|
-
* Severity validation rules for critic pass (iteration 2+).
|
|
14
|
-
* These rules help the critic validate and potentially downgrade severity.
|
|
15
|
-
*/
|
|
16
|
-
export declare const SEVERITY_VALIDATION_RULES = "\n## Severity Validation Rules\n\nDuring the critic pass, validate each finding's severity against these rules:\n\n1. **Critical findings MUST have exploit path or data integrity evidence**\n - If a critical finding lacks a clear exploit path or data corruption risk described in the rationale, downgrade to major\n - Example: \"SQL injection\" without showing how user input reaches the query \u2192 downgrade to major\n\n2. **Major findings MUST demonstrate functional impact**\n - If a major finding doesn't show how functionality is broken or patterns violated, downgrade to minor\n - Example: \"Missing error handling\" without showing what breaks \u2192 downgrade to minor\n\n3. **Minor findings MUST indicate code quality impact**\n - If a minor finding is just a style preference without maintainability impact, consider downgrade to info\n - Example: \"Use const instead of let\" without explaining why \u2192 consider info\n\n4. **Downgrade rules**:\n - Critical \u2192 Major: No exploit path or data integrity risk described\n - Major \u2192 Minor: No functional impact demonstrated\n - Minor \u2192 Info: No code quality/maintainability impact shown\n\n5. **Never upgrade severity** - Only downgrade if evidence doesn't support the assigned level\n";
|
|
17
|
-
/**
|
|
18
|
-
* ContextRail tooling workflow requirements.
|
|
19
|
-
* Forces reviewers to ground findings in retrieved contexts instead of guessing IDs/titles.
|
|
20
|
-
*/
|
|
21
|
-
export declare const CONTEXTRAIL_TOOLING_BLOCK = "\n## ContextRail Standards Workflow (Required)\n\nWhen you identify potential issues, you MUST ground them in retrieved ContextRail standards:\n\n1. Use `search_contexts` first to discover relevant standards for this change.\n2. Use `get_context` for each context you plan to cite in findings.\n3. Use `resolve_dependencies` when cited contexts have required dependencies.\n4. Perform at least one `search_contexts` call before finalizing your findings.\n\nAttribution rules:\n- NEVER invent context IDs or titles.\n- Only include `contextIdsUsed`, `contextIdsViolated`, and `contextTitles` from contexts you actually retrieved.\n- If no relevant ContextRail standard exists after tool lookup, set those fields to `null` (not empty arrays) and explain that briefly in `rationale`.\n";
|
|
22
|
-
/**
|
|
23
|
-
* Compact output contract optimized for structured-output reliability.
|
|
24
|
-
* Keeps formatting constraints in one place for both standard and critic prompts.
|
|
25
|
-
*/
|
|
26
|
-
export declare const OUTPUT_CONTRACT_BLOCK = "\n## Output Contract (Strict)\n\nReturn a JSON object with:\n- `findings`: array\n- `validated`: boolean\n- `notes`: string | null\n\nFor each finding:\n- Required keys: `severity`, `title`, `description`, `rationale`\n- Optional-but-required-by-schema keys: `suggestedFix`, `file`, `line`, `endLine`, `contextIdsUsed`, `contextIdsViolated`, `contextTitles`\n\nSchema compatibility rules:\n- Include all keys (never omit)\n- Use `null` when a value is not available\n- Use `null` (not empty arrays) for context attribution fields when no standards apply\n";
|
|
27
|
-
/**
|
|
28
|
-
* Final guardrail checklist near generation point.
|
|
29
|
-
* Repeats only non-negotiables to reduce mid-prompt loss on long inputs.
|
|
30
|
-
*/
|
|
31
|
-
export declare const FINAL_CHECKLIST_BLOCK = "\n## Final Checklist (Must Pass)\n1. Every finding is supported by concrete code/diff evidence.\n2. ContextRail workflow was used (`search_contexts` -> `get_context` -> `resolve_dependencies` as needed).\n3. Context attribution fields only reference retrieved contexts (or are `null`).\n4. Severity is calibrated to evidence.\n5. Output strictly matches the JSON contract, and `notes` summarizes retrieved contexts (or none found).\n";
|
|
5
|
+
export { SEVERITY_CALIBRATION_BLOCK, SEVERITY_VALIDATION_RULES, CONTEXTRAIL_TOOLING_BLOCK, OUTPUT_CONTRACT_BLOCK, FINAL_CHECKLIST_BLOCK, } from '../prompts/blocks.js';
|
|
6
|
+
export { buildCriticPrompt, buildStandardReviewPrompt, buildReviewerUserMessage } from '../prompts/reviewer.js';
|
|
32
7
|
export declare const buildPromptMessages: (promptResult: {
|
|
33
8
|
messages: {
|
|
34
9
|
role: string;
|
|
@@ -39,4 +14,3 @@ export declare const buildPromptMessages: (promptResult: {
|
|
|
39
14
|
}[];
|
|
40
15
|
metadata?: unknown;
|
|
41
16
|
}) => PromptMessage[];
|
|
42
|
-
export declare const buildReviewerUserMessage: (inputs: ReviewInputs, understanding: string, iteration: number, findings: ReviewerFindings | null, prDescription?: string, reviewDomains?: string[]) => string;
|