karajan-code 4.57.0 → 4.57.1
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/privacy/scan.js +17 -3
- package/src/review/diff-batches.js +64 -0
- package/src/review/third-party-review.js +35 -12
package/package.json
CHANGED
package/src/privacy/scan.js
CHANGED
|
@@ -81,6 +81,11 @@ const SEMVER_OP = String.raw`(?:[\^~]|[<>]=?|=)`;
|
|
|
81
81
|
const SEMVER_JOIN = String.raw`\s*(?:(?:\|\||-)\s*${SEMVER_OP}?|${SEMVER_OP})\s*`;
|
|
82
82
|
const SEMVER_VALUE = new RegExp(String.raw`^\s*${SEMVER_OP}?\s*${SEMVER_TOKEN}(?:${SEMVER_JOIN}${SEMVER_TOKEN})*\s*$`);
|
|
83
83
|
|
|
84
|
+
/** KJC-BUG-0380: a pair is discarded only when it is a real latitude, longitude. */
|
|
85
|
+
function inGpsRange(_match, lat, lng) {
|
|
86
|
+
return Math.abs(Number(lat)) <= 90 && Math.abs(Number(lng)) <= 180;
|
|
87
|
+
}
|
|
88
|
+
|
|
84
89
|
const CONTEXT_DISCARDS = [
|
|
85
90
|
{ type: "git-sha", re: /\b(?:[0-9a-f]{64}|[0-9a-f]{40})\b/g },
|
|
86
91
|
{ type: "doc-domain-email", re: /\b[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)*(?:example\.(?:com|org|net)|test|invalid|localhost|example)\b/g },
|
|
@@ -114,7 +119,12 @@ const CONTEXT_DISCARDS = [
|
|
|
114
119
|
// lookarounds instead of \b). Only that WHOLE shape with a real 20xx date and
|
|
115
120
|
// time: no card of 14 digits starts with 20 (Diners is 30/36/38), and a
|
|
116
121
|
// 16-digit card does not fit the slot.
|
|
117
|
-
|
|
122
|
+
// KJC-BUG-0380 (#2263): a latitude, longitude pair in test data read as a phone.
|
|
123
|
+
// Only the WHOLE pair: both with a decimal point and four or more decimals, in
|
|
124
|
+
// range (latitude up to 90, longitude up to 180), joined by a comma. A lone
|
|
125
|
+
// number or a pair out of range still warns.
|
|
126
|
+
{ type: "gps-pair", re: /(?<![\d.])(-?\d{1,3}\.\d{4,})\s*,\s*(-?\d{1,3}\.\d{4,})(?![\d.])/g, keep: inGpsRange },
|
|
127
|
+
{ type: "plan-id", re:/(?<![A-Za-z0-9])plan-20\d\d(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])(?:[01]\d|2[0-3])[0-5]\d[0-5]\d-[a-z0-9]{4}(?![A-Za-z0-9])/g },
|
|
118
128
|
];
|
|
119
129
|
|
|
120
130
|
/**
|
|
@@ -160,9 +170,13 @@ export function scanText(text, { list = loadPrivacyList(), source = "<text>" } =
|
|
|
160
170
|
re.lastIndex = m.index + m[0].length;
|
|
161
171
|
}
|
|
162
172
|
}
|
|
163
|
-
for (const { re } of CONTEXT_DISCARDS) {
|
|
173
|
+
for (const { re, keep } of CONTEXT_DISCARDS) {
|
|
164
174
|
re.lastIndex = 0;
|
|
165
|
-
probe = probe.replace(re, (m) => {
|
|
175
|
+
probe = probe.replace(re, (m, ...groups) => {
|
|
176
|
+
if (keep && !keep(m, ...groups)) return m;
|
|
177
|
+
discarded += 1;
|
|
178
|
+
return " ".repeat(m.length);
|
|
179
|
+
});
|
|
166
180
|
}
|
|
167
181
|
const red = redactPII(probe);
|
|
168
182
|
if (red.total > 0) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diff-batches (KJC-BUG-0378, #2263): a third-party PR can be far larger than
|
|
3
|
+
* what one reviewer prompt holds (the reviewer clips at DIFF_CLIP_CHARS). The
|
|
4
|
+
* diff is cut at each file and whole files are packed into batches that fit, so
|
|
5
|
+
* every batch is reviewed complete. A file larger than the limit, or files past
|
|
6
|
+
* the batch cap, are not reviewed and are named in the coverage, never hidden.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const FILE_HEAD = /^diff --git a\/(.+?) b\//;
|
|
10
|
+
|
|
11
|
+
/** The diff cut at each file, with the number of changed lines in each. */
|
|
12
|
+
export function splitByFile(diff) {
|
|
13
|
+
const parts = [];
|
|
14
|
+
for (const line of String(diff).split("\n")) {
|
|
15
|
+
const head = FILE_HEAD.exec(line);
|
|
16
|
+
if (head) parts.push({ file: head[1], lines: [] });
|
|
17
|
+
if (parts.length === 0) continue;
|
|
18
|
+
parts.at(-1).lines.push(line);
|
|
19
|
+
}
|
|
20
|
+
return parts.map(({ file, lines }) => ({
|
|
21
|
+
file,
|
|
22
|
+
text: lines.join("\n"),
|
|
23
|
+
changed: lines.filter((l) => /^[+-]/.test(l) && !/^(\+\+\+|---) /.test(l)).length,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whole files packed into batches of at most `limit` characters, at most
|
|
29
|
+
* `maxBatches` of them, and the coverage that leaves.
|
|
30
|
+
*/
|
|
31
|
+
export function batchDiff(diff, { limit, maxBatches }) {
|
|
32
|
+
const batches = [];
|
|
33
|
+
const unreviewed = [];
|
|
34
|
+
for (const part of splitByFile(diff)) {
|
|
35
|
+
const last = batches.at(-1);
|
|
36
|
+
if (part.text.length > limit) unreviewed.push(part);
|
|
37
|
+
else if (last && last.text.length + 1 + part.text.length <= limit) last.add(part);
|
|
38
|
+
else if (batches.length < maxBatches) batches.push(newBatch(part));
|
|
39
|
+
else unreviewed.push(part);
|
|
40
|
+
}
|
|
41
|
+
const reviewed = batches.flatMap((b) => b.parts);
|
|
42
|
+
const sum = (ps) => ps.reduce((n, p) => n + p.changed, 0);
|
|
43
|
+
return {
|
|
44
|
+
batches: batches.map((b) => ({ files: b.parts.map((p) => p.file), text: b.text })),
|
|
45
|
+
coverage: {
|
|
46
|
+
files: { reviewed: reviewed.length, total: reviewed.length + unreviewed.length },
|
|
47
|
+
lines: { reviewed: sum(reviewed), total: sum(reviewed) + sum(unreviewed) },
|
|
48
|
+
truncated: unreviewed.length > 0,
|
|
49
|
+
unreviewed: unreviewed.map((p) => p.file),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function newBatch(first) {
|
|
55
|
+
const batch = {
|
|
56
|
+
parts: [first],
|
|
57
|
+
text: first.text,
|
|
58
|
+
add(part) {
|
|
59
|
+
batch.parts.push(part);
|
|
60
|
+
batch.text = `${batch.text}\n${part.text}`;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return batch;
|
|
64
|
+
}
|
|
@@ -11,6 +11,11 @@ import { runOneShotReview } from "./one-shot-review.js";
|
|
|
11
11
|
import { prRequirementChecks } from "./pr-requirements.js";
|
|
12
12
|
import { prSecurityChecks } from "./pr-security.js";
|
|
13
13
|
import { sonarCheck } from "./pr-sonar.js";
|
|
14
|
+
import { batchDiff } from "./diff-batches.js";
|
|
15
|
+
import { DIFF_CLIP_CHARS } from "../prompts/diff-clip.js";
|
|
16
|
+
|
|
17
|
+
// KJC-BUG-0378: a reviewer call per batch; past this many, files are left out and named.
|
|
18
|
+
const MAX_REVIEW_BATCHES = 10;
|
|
14
19
|
|
|
15
20
|
const gitDiffOf = async (projectDir, range) => {
|
|
16
21
|
const res = await runCommand("git", ["diff", range], { cwd: projectDir });
|
|
@@ -23,27 +28,45 @@ const finding = (i) => ({
|
|
|
23
28
|
description: i.description ?? "", suggested_fix: i.suggested_fix ?? null,
|
|
24
29
|
});
|
|
25
30
|
|
|
26
|
-
/** The review as data: verdict, findings, suggestions and the
|
|
27
|
-
export async function thirdPartyReview({ range, config, projectDir, logger, task = null, gitDiff = gitDiffOf, run = runOneShotReview, requirements = prRequirementChecks, security = prSecurityChecks, sonar = sonarCheck }) {
|
|
31
|
+
/** The review as data: verdict, findings, suggestions, security checks and, apart, the house requirements. */
|
|
32
|
+
export async function thirdPartyReview({ range, config, projectDir, logger, task = null, gitDiff = gitDiffOf, run = runOneShotReview, requirements = prRequirementChecks, security = prSecurityChecks, sonar = sonarCheck, limit = DIFF_CLIP_CHARS, maxBatches = MAX_REVIEW_BATCHES }) {
|
|
28
33
|
const diff = await gitDiff(projectDir, range);
|
|
29
34
|
// KJC-TSK-1076/1077: the PR's requirements and its security, with no tokens; they never stop the review.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// KJC-BUG-0379 (#2263): the house requirements (size, commits, attribution, tests) travel apart:
|
|
36
|
+
// on another repo with other conventions they must not read as findings next to security.
|
|
37
|
+
const policyChecks = await requirements({ range, projectDir, config });
|
|
38
|
+
const checks = [...await security({ diff, projectDir, config }), await sonar({ diff, config, projectDir })];
|
|
39
|
+
// KJC-BUG-0378 (#2263): the whole diff, in batches of whole files that fit the reviewer's
|
|
40
|
+
// prompt; what does not fit is named in the coverage and the verdict cannot be approved.
|
|
41
|
+
const provider = resolveRole(config, "reviewer").provider || null;
|
|
42
|
+
const { batches, coverage } = batchDiff(diff, { limit, maxBatches });
|
|
43
|
+
if (coverage.files.total === 0) throw new Error(`nothing to review — ${range} has no file changes`);
|
|
44
|
+
const records = [];
|
|
45
|
+
for (const batch of batches) {
|
|
46
|
+
records.push(await run({ diff: batch.text, task, config, logger, projectDir, reviewerOverride: provider, persist: false }));
|
|
47
|
+
}
|
|
36
48
|
return {
|
|
37
49
|
mode: "third-party",
|
|
38
50
|
range,
|
|
39
|
-
reviewer:
|
|
40
|
-
verdict:
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
reviewer: records[0]?.reviewer ?? provider,
|
|
52
|
+
verdict: verdictOf(records, coverage),
|
|
53
|
+
coverage,
|
|
54
|
+
findings: records.flatMap((r) => (r.issues ?? []).map(finding)),
|
|
55
|
+
suggestions: records.flatMap((r) => r.suggestions ?? []),
|
|
43
56
|
checks,
|
|
57
|
+
policyChecks,
|
|
44
58
|
};
|
|
45
59
|
}
|
|
46
60
|
|
|
61
|
+
/** Rejected if any batch was; approved only when every file was reviewed; partial otherwise. */
|
|
62
|
+
function verdictOf(records, coverage) {
|
|
63
|
+
let status = "approved";
|
|
64
|
+
if (records.some((r) => r.verdict !== "approved")) status = "rejected";
|
|
65
|
+
else if (coverage.truncated) status = "partial";
|
|
66
|
+
const summary = records.map((r) => r.summary).filter(Boolean).join("\n");
|
|
67
|
+
return { status, approved: status === "approved", summary };
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
/**
|
|
48
71
|
* The CLI side: stdout carries exactly one JSON document, logs go to stderr.
|
|
49
72
|
* Exit 0 whatever the PR's verdict (the agent decides what to post); non-zero
|