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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.57.0",
3
+ "version": "4.57.1",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -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
- { 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 },
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) => { discarded += 1; return " ".repeat(m.length); });
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 PR-requirement checks. */
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
- const checks = [...await requirements({ range, projectDir, config }), ...await security({ diff, projectDir, config }), await sonar({ diff, config, projectDir })];
31
- const record = await run({
32
- diff, task, config, logger, projectDir,
33
- reviewerOverride: resolveRole(config, "reviewer").provider || null,
34
- persist: false,
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: record.reviewer,
40
- verdict: { approved: record.verdict === "approved", summary: record.summary ?? "" },
41
- findings: (record.issues ?? []).map(finding),
42
- suggestions: record.suggestions ?? [],
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