codeant-cli 0.4.9 → 0.4.11
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/package.json +1 -1
- package/src/commands/scans/index.js +4 -2
- package/src/commands/scans/lib/filters.js +5 -32
- package/src/commands/scans/lib/normalize.js +0 -2
- package/src/commands/scans/results.js +9 -14
- package/src/components/ScanCenter.js +2 -2
- package/src/index.js +7 -2
- package/src/scanCenter/handleSelectResultType.js +4 -3
- package/src/scans/fetchAdvancedScanResults.js +18 -13
- package/src/scans/fetchScanResults.js +14 -9
- package/src/utils/reviewApiHelper.js +38 -31
package/package.json
CHANGED
|
@@ -71,7 +71,8 @@ export default function registerScansCommands(program, { runCmd }) {
|
|
|
71
71
|
.option('--severity <list>', 'Filter by severity (e.g. critical,high)')
|
|
72
72
|
.option('--path <glob>', 'Filter by file path glob')
|
|
73
73
|
.option('--check <regex>', 'Filter by check ID or name (regex)')
|
|
74
|
-
.option('--
|
|
74
|
+
.option('--filter-dismissed', 'Exclude dismissed findings (default: false)')
|
|
75
|
+
.option('--no-false-positives', 'Exclude false positives (default: included)')
|
|
75
76
|
.option('--format <fmt>', 'Output format: json|sarif|csv|md|table (default: json)', 'json')
|
|
76
77
|
.option('--output <path>', 'Write output to file instead of stdout')
|
|
77
78
|
.option('--fields <list>', 'Project findings to subset of fields (comma-separated)')
|
|
@@ -96,7 +97,8 @@ export default function registerScansCommands(program, { runCmd }) {
|
|
|
96
97
|
severity: opts.severity,
|
|
97
98
|
path: opts.path,
|
|
98
99
|
check: opts.check,
|
|
99
|
-
|
|
100
|
+
filterDismissed: opts.filterDismissed || false,
|
|
101
|
+
includeFalsePositives: opts.falsePositives ?? true,
|
|
100
102
|
format: opts.format,
|
|
101
103
|
output: opts.output,
|
|
102
104
|
fields: opts.fields,
|
|
@@ -1,29 +1,22 @@
|
|
|
1
1
|
import { minimatch } from 'minimatch';
|
|
2
|
-
import { isDismissed, findDismissMatch } from './dismissMatch.js';
|
|
3
2
|
import { normalizeSeverity } from './normalize.js';
|
|
4
3
|
|
|
5
|
-
const SEV_RANK = { critical: 5, high: 4, medium: 3, low: 2, info: 1, unknown: 0 };
|
|
6
|
-
|
|
7
4
|
/**
|
|
8
|
-
* Apply
|
|
5
|
+
* Apply client-side filters to findings (returns new array).
|
|
6
|
+
* Dismissed / false-positive filtering is handled exclusively by the backend.
|
|
9
7
|
*
|
|
10
8
|
* @param {Array} findings - NormalizedFinding[]
|
|
11
9
|
* @param {object} opts
|
|
12
|
-
* @param {string[]|null} opts.severity
|
|
13
|
-
* @param {string|null} opts.pathGlob
|
|
10
|
+
* @param {string[]|null} opts.severity - allowed severity levels (e.g. ['critical','high'])
|
|
11
|
+
* @param {string|null} opts.pathGlob - minimatch glob for file_path
|
|
14
12
|
* @param {string|null} opts.checkRegex - regex applied to check_id + check_name
|
|
15
|
-
* @
|
|
16
|
-
* @param {boolean} opts.includeDismissed
|
|
17
|
-
* @returns {Array} filtered NormalizedFinding[] (dismissed field annotated)
|
|
13
|
+
* @returns {Array} filtered NormalizedFinding[]
|
|
18
14
|
*/
|
|
19
15
|
export function applyFilters(findings, {
|
|
20
16
|
severity = null,
|
|
21
17
|
pathGlob = null,
|
|
22
18
|
checkRegex = null,
|
|
23
|
-
dismissedAlerts = [],
|
|
24
|
-
includeDismissed = false,
|
|
25
19
|
} = {}) {
|
|
26
|
-
// Pre-compile regex — strip Python/PCRE inline flag (?i) and fold into JS flag
|
|
27
20
|
let checkRe = null;
|
|
28
21
|
if (checkRegex) {
|
|
29
22
|
try {
|
|
@@ -37,35 +30,15 @@ export function applyFilters(findings, {
|
|
|
37
30
|
}
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
// Severity set
|
|
41
33
|
const sevSet = severity && severity.length > 0
|
|
42
34
|
? new Set(severity.map((s) => normalizeSeverity(s)))
|
|
43
35
|
: null;
|
|
44
36
|
|
|
45
37
|
const result = [];
|
|
46
38
|
for (const f of findings) {
|
|
47
|
-
// Annotate dismiss status
|
|
48
|
-
const match = findDismissMatch(f, dismissedAlerts);
|
|
49
|
-
f.dismissed = match !== null;
|
|
50
|
-
if (match) {
|
|
51
|
-
f.dismiss_info = {
|
|
52
|
-
reason: match.reason_for_dismiss || null,
|
|
53
|
-
comment: match.comment_for_dismiss || null,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Filter dismissed
|
|
58
|
-
if (f.dismissed && !includeDismissed) continue;
|
|
59
|
-
|
|
60
|
-
// Filter by severity
|
|
61
39
|
if (sevSet && !sevSet.has(f.severity)) continue;
|
|
62
|
-
|
|
63
|
-
// Filter by path glob
|
|
64
40
|
if (pathGlob && !minimatch(f.file_path, pathGlob, { matchBase: true })) continue;
|
|
65
|
-
|
|
66
|
-
// Filter by check regex
|
|
67
41
|
if (checkRe && !checkRe.test(f.check_id) && !checkRe.test(f.check_name)) continue;
|
|
68
|
-
|
|
69
42
|
result.push(f);
|
|
70
43
|
}
|
|
71
44
|
return result;
|
|
@@ -7,7 +7,6 @@ import { paginate } from './lib/paginate.js';
|
|
|
7
7
|
import { emit } from './lib/emit.js';
|
|
8
8
|
import { progress, logError } from './lib/log.js';
|
|
9
9
|
import { FORMATTERS } from './formatters/index.js';
|
|
10
|
-
import { fetchDismissedAlerts } from '../../scans/fetchDismissedAlerts.js';
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* codeant scans results — full orchestration.
|
|
@@ -23,7 +22,8 @@ export async function runResults(opts = {}) {
|
|
|
23
22
|
severity,
|
|
24
23
|
path: pathGlob,
|
|
25
24
|
check: checkRegex,
|
|
26
|
-
|
|
25
|
+
filterDismissed = false,
|
|
26
|
+
includeFalsePositives = true,
|
|
27
27
|
format = 'json',
|
|
28
28
|
output: outputPath = null,
|
|
29
29
|
fields = null,
|
|
@@ -53,16 +53,12 @@ export async function runResults(opts = {}) {
|
|
|
53
53
|
// 2. Parse types
|
|
54
54
|
const categories = parseTypes(types);
|
|
55
55
|
|
|
56
|
-
// 3. Fetch in parallel
|
|
56
|
+
// 3. Fetch in parallel
|
|
57
57
|
progress(`fetching ${categories.map((c) => c.key).join(', ')}…`);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
: fetchDismissedAlerts(repo, 'security'),
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
const dismissedAlerts = dismissedResult.success ? (dismissedResult.dismissedAlerts ?? []) : [];
|
|
58
|
+
const fetchOpts = { filterDismissed, includeFalsePositives };
|
|
59
|
+
const settled = await Promise.allSettled(
|
|
60
|
+
categories.map((c) => c.fetcher(repo, scanMeta.commit_id, fetchOpts))
|
|
61
|
+
);
|
|
66
62
|
|
|
67
63
|
// 4. Collect findings + errors
|
|
68
64
|
const allFindings = [];
|
|
@@ -100,8 +96,6 @@ export async function runResults(opts = {}) {
|
|
|
100
96
|
severity: severityList,
|
|
101
97
|
pathGlob: pathGlob || null,
|
|
102
98
|
checkRegex: checkRegex || null,
|
|
103
|
-
dismissedAlerts,
|
|
104
|
-
includeDismissed,
|
|
105
99
|
});
|
|
106
100
|
|
|
107
101
|
// 6. Sort
|
|
@@ -115,7 +109,8 @@ export async function runResults(opts = {}) {
|
|
|
115
109
|
severity: severityList,
|
|
116
110
|
path: pathGlob || null,
|
|
117
111
|
check: checkRegex || null,
|
|
118
|
-
|
|
112
|
+
filter_dismissed: filterDismissed,
|
|
113
|
+
include_false_positives: includeFalsePositives,
|
|
119
114
|
};
|
|
120
115
|
|
|
121
116
|
// Rebuild summary from all filtered (pre-page) findings
|
|
@@ -153,7 +153,7 @@ function ErrorView({ message, onBack, canGoBack }) {
|
|
|
153
153
|
|
|
154
154
|
// ─── Main ScanCenter ─────────────────────────────────────────────────────────
|
|
155
155
|
|
|
156
|
-
export default function ScanCenter() {
|
|
156
|
+
export default function ScanCenter({ filterDismissed = false, includeFalsePositives = true }) {
|
|
157
157
|
const { exit } = useApp();
|
|
158
158
|
|
|
159
159
|
const [step, setStep] = useState(STEPS.LOADING);
|
|
@@ -198,7 +198,7 @@ export default function ScanCenter() {
|
|
|
198
198
|
|
|
199
199
|
// ── Step 5: result type selected → fetch exactly one endpoint ──
|
|
200
200
|
const handleSelectResultType = (item) =>
|
|
201
|
-
_handleSelectResultType({ STEPS, item, selectedRepo, selectedScan, setSelectedResultType, setStep, setLoadingMsg, setError, setResults });
|
|
201
|
+
_handleSelectResultType({ STEPS, item, selectedRepo, selectedScan, setSelectedResultType, setStep, setLoadingMsg, setError, setResults, filterDismissed, includeFalsePositives });
|
|
202
202
|
|
|
203
203
|
// ── Back navigation ──
|
|
204
204
|
const goBack = {
|
package/src/index.js
CHANGED
|
@@ -211,8 +211,13 @@ program
|
|
|
211
211
|
program
|
|
212
212
|
.command('scan-center')
|
|
213
213
|
.description('Browse scan results interactively')
|
|
214
|
-
.
|
|
215
|
-
|
|
214
|
+
.option('--filter-dismissed', 'Exclude dismissed alerts from results (default: false)')
|
|
215
|
+
.option('--no-false-positives', 'Exclude false positives from results (default: included)')
|
|
216
|
+
.action((options) => {
|
|
217
|
+
render(React.createElement(ScanCenter, {
|
|
218
|
+
filterDismissed: options.filterDismissed ?? false,
|
|
219
|
+
includeFalsePositives: options.falsePositives ?? true,
|
|
220
|
+
}));
|
|
216
221
|
});
|
|
217
222
|
|
|
218
223
|
program
|
|
@@ -2,7 +2,7 @@ import { fetchScanResults } from '../scans/fetchScanResults.js';
|
|
|
2
2
|
import { fetchAdvancedScanResults } from '../scans/fetchAdvancedScanResults.js';
|
|
3
3
|
import { fetchDismissedAlerts } from '../scans/fetchDismissedAlerts.js';
|
|
4
4
|
|
|
5
|
-
export async function handleSelectResultType({ STEPS, item, selectedRepo, selectedScan, setSelectedResultType, setStep, setLoadingMsg, setError, setResults }) {
|
|
5
|
+
export async function handleSelectResultType({ STEPS, item, selectedRepo, selectedScan, setSelectedResultType, setStep, setLoadingMsg, setError, setResults, filterDismissed = false, includeFalsePositives = true }) {
|
|
6
6
|
const rt = item.value;
|
|
7
7
|
setSelectedResultType(rt);
|
|
8
8
|
setStep(STEPS.LOADING);
|
|
@@ -10,12 +10,13 @@ export async function handleSelectResultType({ STEPS, item, selectedRepo, select
|
|
|
10
10
|
|
|
11
11
|
const repo = selectedRepo.full_name;
|
|
12
12
|
const commitId = selectedScan.commitId;
|
|
13
|
+
const fetchOpts = { filterDismissed, includeFalsePositives };
|
|
13
14
|
let res;
|
|
14
15
|
|
|
15
16
|
if (rt.kind === 'basic') {
|
|
16
|
-
res = await fetchScanResults(repo, commitId, rt.value);
|
|
17
|
+
res = await fetchScanResults(repo, commitId, rt.value, fetchOpts);
|
|
17
18
|
} else if (rt.kind === 'advanced') {
|
|
18
|
-
res = await fetchAdvancedScanResults(repo, commitId, rt.value);
|
|
19
|
+
res = await fetchAdvancedScanResults(repo, commitId, rt.value, fetchOpts);
|
|
19
20
|
} else if (rt.value === 'dismissed_alerts') {
|
|
20
21
|
const r = await fetchDismissedAlerts(repo, 'security');
|
|
21
22
|
res = r.success ? { success: true, issues: r.dismissedAlerts } : r;
|
|
@@ -200,9 +200,12 @@ function normalizeAdvancedIssue(item, resultType) {
|
|
|
200
200
|
* @param {string} repo - "org/repo-name"
|
|
201
201
|
* @param {string} commitId - 40-char commit SHA
|
|
202
202
|
* @param {string} resultType - one of ADVANCED_RESULT_TYPES values
|
|
203
|
+
* @param {{ filterDismissed?: boolean, includeFalsePositives?: boolean }} [opts]
|
|
203
204
|
* @returns {Promise<{ success: boolean, issues?: Array, healthyPackages?: Array, status?: string, error?: string }>}
|
|
204
205
|
*/
|
|
205
|
-
export async function fetchAdvancedScanResults(repo, commitId, resultType) {
|
|
206
|
+
export async function fetchAdvancedScanResults(repo, commitId, resultType, opts = {}) {
|
|
207
|
+
const { filterDismissed = false, includeFalsePositives = true } = opts;
|
|
208
|
+
|
|
206
209
|
if (!Object.values(ADVANCED_RESULT_TYPES).includes(resultType)) {
|
|
207
210
|
return {
|
|
208
211
|
success: false,
|
|
@@ -215,6 +218,8 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType) {
|
|
|
215
218
|
repo,
|
|
216
219
|
commit_id: commitId,
|
|
217
220
|
result_type: resultType,
|
|
221
|
+
filter_dismissed: filterDismissed,
|
|
222
|
+
include_false_positives: includeFalsePositives,
|
|
218
223
|
});
|
|
219
224
|
|
|
220
225
|
if (!response) {
|
|
@@ -294,8 +299,8 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType) {
|
|
|
294
299
|
}
|
|
295
300
|
}
|
|
296
301
|
|
|
297
|
-
// Filter secrets false positives
|
|
298
|
-
if (resultType === ADVANCED_RESULT_TYPES.SECRETS) {
|
|
302
|
+
// Filter secrets false positives only when caller explicitly excludes them
|
|
303
|
+
if (resultType === ADVANCED_RESULT_TYPES.SECRETS && !includeFalsePositives) {
|
|
299
304
|
issues = issues.filter((issue) => String(issue.confidence_score || '').toLowerCase() !== 'false_positive');
|
|
300
305
|
}
|
|
301
306
|
|
|
@@ -309,17 +314,17 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType) {
|
|
|
309
314
|
}
|
|
310
315
|
}
|
|
311
316
|
|
|
312
|
-
export const fetchScaResults = (repo, commitId) =>
|
|
313
|
-
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SCA);
|
|
317
|
+
export const fetchScaResults = (repo, commitId, opts) =>
|
|
318
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SCA, opts);
|
|
314
319
|
|
|
315
|
-
export const fetchSbomResults = (repo, commitId) =>
|
|
316
|
-
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SBOM);
|
|
320
|
+
export const fetchSbomResults = (repo, commitId, opts) =>
|
|
321
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SBOM, opts);
|
|
317
322
|
|
|
318
|
-
export const fetchSecretsResults = (repo, commitId) =>
|
|
319
|
-
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SECRETS);
|
|
323
|
+
export const fetchSecretsResults = (repo, commitId, opts) =>
|
|
324
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SECRETS, opts);
|
|
320
325
|
|
|
321
|
-
export const fetchIacResults = (repo, commitId) =>
|
|
322
|
-
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.IAC);
|
|
326
|
+
export const fetchIacResults = (repo, commitId, opts) =>
|
|
327
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.IAC, opts);
|
|
323
328
|
|
|
324
|
-
export const fetchDeadCodeResults = (repo, commitId) =>
|
|
325
|
-
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.DEAD_CODE);
|
|
329
|
+
export const fetchDeadCodeResults = (repo, commitId, opts) =>
|
|
330
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.DEAD_CODE, opts);
|
|
@@ -49,9 +49,12 @@ function extractCleanPath(fullPath, commitId, suffix = null) {
|
|
|
49
49
|
* @param {string} repo - "org/repo-name"
|
|
50
50
|
* @param {string} commitId - 40-char commit SHA
|
|
51
51
|
* @param {string} resultType - one of VALID_RESULT_TYPES values
|
|
52
|
+
* @param {{ filterDismissed?: boolean, includeFalsePositives?: boolean }} [opts]
|
|
52
53
|
* @returns {Promise<{ success: boolean, issues?: Array, status?: string, error?: string }>}
|
|
53
54
|
*/
|
|
54
|
-
export async function fetchScanResults(repo, commitId, resultType) {
|
|
55
|
+
export async function fetchScanResults(repo, commitId, resultType, opts = {}) {
|
|
56
|
+
const { filterDismissed = false, includeFalsePositives = true } = opts;
|
|
57
|
+
|
|
55
58
|
if (!Object.values(VALID_RESULT_TYPES).includes(resultType)) {
|
|
56
59
|
return {
|
|
57
60
|
success: false,
|
|
@@ -64,6 +67,8 @@ export async function fetchScanResults(repo, commitId, resultType) {
|
|
|
64
67
|
repo,
|
|
65
68
|
commit_id: commitId,
|
|
66
69
|
result_type: resultType,
|
|
70
|
+
filter_dismissed: filterDismissed,
|
|
71
|
+
include_false_positives: includeFalsePositives,
|
|
67
72
|
});
|
|
68
73
|
|
|
69
74
|
if (!response) {
|
|
@@ -102,14 +107,14 @@ export async function fetchScanResults(repo, commitId, resultType) {
|
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
export const fetchSastResults = (repo, commitId) =>
|
|
106
|
-
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.SECURITY_ISSUES);
|
|
110
|
+
export const fetchSastResults = (repo, commitId, opts) =>
|
|
111
|
+
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.SECURITY_ISSUES, opts);
|
|
107
112
|
|
|
108
|
-
export const fetchAntiPatternsResults = (repo, commitId) =>
|
|
109
|
-
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.ANTI_PATTERNS);
|
|
113
|
+
export const fetchAntiPatternsResults = (repo, commitId, opts) =>
|
|
114
|
+
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.ANTI_PATTERNS, opts);
|
|
110
115
|
|
|
111
|
-
export const fetchDocstringResults = (repo, commitId) =>
|
|
112
|
-
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.DOCSTRING);
|
|
116
|
+
export const fetchDocstringResults = (repo, commitId, opts) =>
|
|
117
|
+
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.DOCSTRING, opts);
|
|
113
118
|
|
|
114
|
-
export const fetchComplexFunctionsResults = (repo, commitId) =>
|
|
115
|
-
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.COMPLEX_FUNCTIONS);
|
|
119
|
+
export const fetchComplexFunctionsResults = (repo, commitId, opts) =>
|
|
120
|
+
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.COMPLEX_FUNCTIONS, opts);
|
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { minimatch } from 'minimatch';
|
|
5
5
|
|
|
6
|
-
const MAX_REVIEW_FILES =
|
|
6
|
+
const MAX_REVIEW_FILES = 15;
|
|
7
7
|
const MAX_FILE_LINES = 5000;
|
|
8
8
|
const MAX_FILE_SIZE_BYTES = 200 * 1024; // 200 KB
|
|
9
9
|
|
|
@@ -184,41 +184,20 @@ class ReviewApiHelper extends CommonApiHelper {
|
|
|
184
184
|
return true;
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
// Dedupe by filename, keeping all hunks
|
|
187
|
+
// Dedupe by filename, keeping all hunks (no cap yet — applied after size filtering)
|
|
188
188
|
const fileOrder = [];
|
|
189
189
|
const fileHunks = new Map();
|
|
190
|
-
let capped = false;
|
|
191
190
|
for (const d of reviewable) {
|
|
192
191
|
const f = d.filename_str;
|
|
193
192
|
if (!fileHunks.has(f)) {
|
|
194
|
-
if (fileOrder.length >= MAX_REVIEW_FILES) {
|
|
195
|
-
capped = true;
|
|
196
|
-
if (!seenSkipped.has(f)) {
|
|
197
|
-
dedupedSkipped.push({ file: f, reason: `exceeded ${MAX_REVIEW_FILES}-file limit` });
|
|
198
|
-
seenSkipped.add(f);
|
|
199
|
-
}
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
193
|
fileOrder.push(f);
|
|
203
194
|
fileHunks.set(f, []);
|
|
204
195
|
}
|
|
205
196
|
fileHunks.get(f).push(d);
|
|
206
197
|
}
|
|
207
198
|
|
|
208
|
-
// Reconstruct a combined diff string from the filtered hunks
|
|
209
|
-
const parts = [];
|
|
210
|
-
for (const f of fileOrder) {
|
|
211
|
-
const hunks = fileHunks.get(f);
|
|
212
|
-
// Build a file-level diff header + all hunk patches
|
|
213
|
-
const header = `diff --git a/${f} b/${f}\n--- a/${f}\n+++ b/${f}`;
|
|
214
|
-
const hunkPatches = hunks.map(h => h.patch_str).filter(Boolean);
|
|
215
|
-
if (hunkPatches.length > 0) {
|
|
216
|
-
parts.push(header + '\n' + hunkPatches.join('\n'));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
199
|
// Use head_file_str from diffs so file content matches the diff version (avoids working tree mismatch for --last-commit etc.)
|
|
221
|
-
|
|
200
|
+
// Filter out files that exceed size or line limits before applying the cap
|
|
222
201
|
const skippedLargeFiles = new Set();
|
|
223
202
|
for (const f of fileOrder) {
|
|
224
203
|
const hunks = fileHunks.get(f);
|
|
@@ -234,21 +213,49 @@ class ReviewApiHelper extends CommonApiHelper {
|
|
|
234
213
|
if (lineCount > MAX_FILE_LINES) {
|
|
235
214
|
skippedLargeFiles.add(f);
|
|
236
215
|
dedupedSkipped.push({ file: f, reason: `too large (${lineCount} lines, max ${MAX_FILE_LINES})` });
|
|
237
|
-
continue;
|
|
238
216
|
}
|
|
239
|
-
fileContents[f] = content;
|
|
240
217
|
}
|
|
241
218
|
}
|
|
242
219
|
|
|
243
|
-
//
|
|
244
|
-
|
|
220
|
+
// Apply MAX_REVIEW_FILES cap after all filtering (binary, deleted, pattern, size)
|
|
221
|
+
let capped = false;
|
|
222
|
+
const cappedFileOrder = [];
|
|
223
|
+
for (const f of fileOrder) {
|
|
224
|
+
if (skippedLargeFiles.has(f)) continue;
|
|
225
|
+
if (cappedFileOrder.length >= MAX_REVIEW_FILES) {
|
|
226
|
+
capped = true;
|
|
227
|
+
if (!seenSkipped.has(f)) {
|
|
228
|
+
dedupedSkipped.push({ file: f, reason: `exceeded ${MAX_REVIEW_FILES}-file limit` });
|
|
229
|
+
seenSkipped.add(f);
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
cappedFileOrder.push(f);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Build file contents for the capped set
|
|
237
|
+
const fileContents = {};
|
|
238
|
+
for (const f of cappedFileOrder) {
|
|
239
|
+
const content = fileHunks.get(f)[0]?.head_file_str || '';
|
|
240
|
+
if (content) fileContents[f] = content;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Reconstruct a combined diff string from the capped set
|
|
244
|
+
const parts = [];
|
|
245
|
+
for (const f of cappedFileOrder) {
|
|
246
|
+
const hunks = fileHunks.get(f);
|
|
247
|
+
const header = `diff --git a/${f} b/${f}\n--- a/${f}\n+++ b/${f}`;
|
|
248
|
+
const hunkPatches = hunks.map(h => h.patch_str).filter(Boolean);
|
|
249
|
+
if (hunkPatches.length > 0) {
|
|
250
|
+
parts.push(header + '\n' + hunkPatches.join('\n'));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
245
253
|
|
|
246
|
-
const reviewedFiles = fileOrder.filter(f => !skippedLargeFiles.has(f));
|
|
247
254
|
return {
|
|
248
|
-
diff_content:
|
|
255
|
+
diff_content: parts.join('\n'),
|
|
249
256
|
file_contents: fileContents,
|
|
250
257
|
_meta: {
|
|
251
|
-
reviewed_files:
|
|
258
|
+
reviewed_files: cappedFileOrder,
|
|
252
259
|
total_changed: allUniqueFiles.size,
|
|
253
260
|
skipped: dedupedSkipped,
|
|
254
261
|
capped,
|