kodevu 0.1.2 → 0.1.3
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/README.md +3 -5
- package/config.example.json +0 -1
- package/package.json +1 -1
- package/src/config.js +0 -1
- package/src/review-runner.js +137 -11
package/README.md
CHANGED
|
@@ -70,15 +70,12 @@ npx kodevu --config ./config.current.json --once
|
|
|
70
70
|
- `reviewPrompt`: saved into the report as review context
|
|
71
71
|
- `outputDir`: report output directory; default `./reports`
|
|
72
72
|
- `commandTimeoutMs`: timeout for a single review command execution in milliseconds
|
|
73
|
-
- `bootstrapToLatest`: if no state exists, start by reviewing only the current latest change instead of replaying the full history
|
|
74
73
|
- `maxRevisionsPerRun`: cap the number of pending changes per polling cycle
|
|
75
74
|
|
|
76
75
|
Internal defaults:
|
|
77
76
|
|
|
78
|
-
- review state is always stored in `./data/state.json
|
|
79
|
-
-
|
|
80
|
-
- command output is decoded as `utf8`
|
|
81
|
-
- debug logging is enabled only by passing `--debug` or `-d`
|
|
77
|
+
- review state is always stored in `./data/state.json`, and first run starts from the current latest change instead of replaying full history
|
|
78
|
+
- Kodevu invokes `git`, `svn`, and the configured reviewer CLI from `PATH`; debug logging is enabled only by passing `--debug` or `-d`
|
|
82
79
|
|
|
83
80
|
## Target Rules
|
|
84
81
|
|
|
@@ -91,6 +88,7 @@ Internal defaults:
|
|
|
91
88
|
|
|
92
89
|
- `reviewer: "codex"` uses `codex exec` with the diff embedded in the prompt.
|
|
93
90
|
- `reviewer: "gemini"` uses `gemini -p` in non-interactive mode.
|
|
91
|
+
- Large diffs are truncated before being sent to the reviewer or written into the report once they exceed the configured line or character limits.
|
|
94
92
|
- For Git targets and local SVN working copies, the reviewer command runs from the repository workspace so it can inspect related files beyond the diff when needed.
|
|
95
93
|
- For remote SVN URLs without a local working copy, the review still relies on the diff and change metadata only.
|
|
96
94
|
- SVN reports keep the `r123.md` naming style.
|
package/config.example.json
CHANGED
package/package.json
CHANGED
package/src/config.js
CHANGED
package/src/review-runner.js
CHANGED
|
@@ -4,6 +4,18 @@ import path from "node:path";
|
|
|
4
4
|
import { runCommand } from "./shell.js";
|
|
5
5
|
import { resolveRepositoryContext } from "./vcs-client.js";
|
|
6
6
|
|
|
7
|
+
const DIFF_LIMITS = {
|
|
8
|
+
review: {
|
|
9
|
+
maxLines: 4000,
|
|
10
|
+
maxChars: 120000
|
|
11
|
+
},
|
|
12
|
+
report: {
|
|
13
|
+
maxLines: 1500,
|
|
14
|
+
maxChars: 40000
|
|
15
|
+
},
|
|
16
|
+
tailLines: 200
|
|
17
|
+
};
|
|
18
|
+
|
|
7
19
|
function debugLog(config, message) {
|
|
8
20
|
if (config.debug) {
|
|
9
21
|
console.error(`[debug] ${message}`);
|
|
@@ -167,7 +179,106 @@ function getReviewWorkspaceRoot(config, backend, targetInfo) {
|
|
|
167
179
|
return config.baseDir;
|
|
168
180
|
}
|
|
169
181
|
|
|
170
|
-
function
|
|
182
|
+
function countLines(text) {
|
|
183
|
+
if (!text) {
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return text.split(/\r?\n/).length;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function trimBlockToChars(text, maxChars, keepTail = false) {
|
|
191
|
+
if (text.length <= maxChars) {
|
|
192
|
+
return text;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (maxChars <= 3) {
|
|
196
|
+
return ".".repeat(Math.max(maxChars, 0));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return keepTail ? `...${text.slice(-(maxChars - 3))}` : `${text.slice(0, maxChars - 3)}...`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function truncateDiffText(diffText, maxLines, maxChars, tailLines, purposeLabel) {
|
|
203
|
+
const normalizedDiff = diffText.replace(/\r\n/g, "\n");
|
|
204
|
+
const originalLineCount = countLines(normalizedDiff);
|
|
205
|
+
const originalCharCount = normalizedDiff.length;
|
|
206
|
+
|
|
207
|
+
if (originalLineCount <= maxLines && originalCharCount <= maxChars) {
|
|
208
|
+
return {
|
|
209
|
+
text: diffText,
|
|
210
|
+
wasTruncated: false,
|
|
211
|
+
originalLineCount,
|
|
212
|
+
originalCharCount,
|
|
213
|
+
outputLineCount: originalLineCount,
|
|
214
|
+
outputCharCount: originalCharCount
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const lines = normalizedDiff.split("\n");
|
|
219
|
+
const safeTailLines = Math.min(Math.max(tailLines, 0), Math.max(maxLines - 2, 0));
|
|
220
|
+
const headLineCount = Math.max(maxLines - safeTailLines - 1, 1);
|
|
221
|
+
let headBlock = lines.slice(0, headLineCount).join("\n");
|
|
222
|
+
let tailBlock = safeTailLines > 0 ? lines.slice(-safeTailLines).join("\n") : "";
|
|
223
|
+
const omittedLineCount = Math.max(originalLineCount - headLineCount - safeTailLines, 0);
|
|
224
|
+
const markerBlock = [
|
|
225
|
+
`... diff truncated for ${purposeLabel} ...`,
|
|
226
|
+
`original lines: ${originalLineCount}, original chars: ${originalCharCount}`,
|
|
227
|
+
`omitted lines: ${omittedLineCount}`
|
|
228
|
+
].join("\n");
|
|
229
|
+
|
|
230
|
+
let truncatedText = [headBlock, markerBlock, tailBlock].filter(Boolean).join("\n");
|
|
231
|
+
|
|
232
|
+
if (truncatedText.length > maxChars) {
|
|
233
|
+
const reservedChars = markerBlock.length + (tailBlock ? 2 : 1);
|
|
234
|
+
const remainingChars = Math.max(maxChars - reservedChars, 0);
|
|
235
|
+
const headBudget = tailBlock ? Math.floor(remainingChars * 0.7) : remainingChars;
|
|
236
|
+
const tailBudget = tailBlock ? Math.max(remainingChars - headBudget, 0) : 0;
|
|
237
|
+
headBlock = trimBlockToChars(headBlock, headBudget, false);
|
|
238
|
+
tailBlock = trimBlockToChars(tailBlock, tailBudget, true);
|
|
239
|
+
truncatedText = [headBlock, markerBlock, tailBlock].filter(Boolean).join("\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
text: truncatedText,
|
|
244
|
+
wasTruncated: true,
|
|
245
|
+
originalLineCount,
|
|
246
|
+
originalCharCount,
|
|
247
|
+
outputLineCount: countLines(truncatedText),
|
|
248
|
+
outputCharCount: truncatedText.length
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function prepareDiffPayloads(config, diffText) {
|
|
253
|
+
return {
|
|
254
|
+
review: truncateDiffText(
|
|
255
|
+
diffText,
|
|
256
|
+
DIFF_LIMITS.review.maxLines,
|
|
257
|
+
DIFF_LIMITS.review.maxChars,
|
|
258
|
+
DIFF_LIMITS.tailLines,
|
|
259
|
+
"reviewer input"
|
|
260
|
+
),
|
|
261
|
+
report: truncateDiffText(
|
|
262
|
+
diffText,
|
|
263
|
+
DIFF_LIMITS.report.maxLines,
|
|
264
|
+
DIFF_LIMITS.report.maxChars,
|
|
265
|
+
Math.min(DIFF_LIMITS.tailLines, DIFF_LIMITS.report.maxLines),
|
|
266
|
+
"report output"
|
|
267
|
+
)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function formatDiffHandling(diffPayload, label) {
|
|
272
|
+
return [
|
|
273
|
+
`- ${label} Original Lines: \`${diffPayload.originalLineCount}\``,
|
|
274
|
+
`- ${label} Original Chars: \`${diffPayload.originalCharCount}\``,
|
|
275
|
+
`- ${label} Included Lines: \`${diffPayload.outputLineCount}\``,
|
|
276
|
+
`- ${label} Included Chars: \`${diffPayload.outputCharCount}\``,
|
|
277
|
+
`- ${label} Truncated: \`${diffPayload.wasTruncated ? "yes" : "no"}\``
|
|
278
|
+
].join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildPrompt(config, backend, targetInfo, details, reviewDiffPayload) {
|
|
171
282
|
const fileList = details.changedPaths.map((item) => `${item.action} ${item.relativePath}`).join("\n");
|
|
172
283
|
const workspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
|
|
173
284
|
const canReadRelatedFiles = backend.kind === "git" || Boolean(targetInfo.workingCopyPath);
|
|
@@ -181,17 +292,19 @@ function buildPrompt(config, backend, targetInfo, details) {
|
|
|
181
292
|
? "Besides the diff below, you may read other related files in the workspace when needed to understand call sites, shared utilities, configuration, tests, or data flow. Do not modify files or rely on shell commands."
|
|
182
293
|
: "Review primarily from the diff below. Do not assume access to other local files, shell commands, or repository history.",
|
|
183
294
|
"Use plain text file references like path/to/file.js:123. Do not emit clickable workspace links.",
|
|
184
|
-
"Write the final review in Simplified Chinese.",
|
|
185
295
|
`Repository Type: ${backend.displayName}`,
|
|
186
296
|
`Change ID: ${details.displayId}`,
|
|
187
297
|
`Author: ${details.author}`,
|
|
188
298
|
`Date: ${details.date || "unknown"}`,
|
|
189
299
|
`Changed files:\n${fileList || "(none)"}`,
|
|
190
|
-
`Commit message:\n${details.message || "(empty)"}
|
|
300
|
+
`Commit message:\n${details.message || "(empty)"}`,
|
|
301
|
+
reviewDiffPayload.wasTruncated
|
|
302
|
+
? `Diff delivery note: the diff was truncated before being sent to the reviewer to stay within configured size limits. Original diff size was ${reviewDiffPayload.originalLineCount} lines / ${reviewDiffPayload.originalCharCount} chars, and the included excerpt is ${reviewDiffPayload.outputLineCount} lines / ${reviewDiffPayload.outputCharCount} chars. Use the changed file list and inspect related workspace files when needed.`
|
|
303
|
+
: `Diff delivery note: the full diff is included. Size is ${reviewDiffPayload.originalLineCount} lines / ${reviewDiffPayload.originalCharCount} chars.`
|
|
191
304
|
].join("\n\n");
|
|
192
305
|
}
|
|
193
306
|
|
|
194
|
-
function buildReport(config, backend, targetInfo, details,
|
|
307
|
+
function buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult) {
|
|
195
308
|
const lines = [
|
|
196
309
|
`# ${backend.displayName} Review Report: ${details.displayId}`,
|
|
197
310
|
"",
|
|
@@ -216,13 +329,18 @@ function buildReport(config, backend, targetInfo, details, diffText, reviewer, r
|
|
|
216
329
|
"## Review Context",
|
|
217
330
|
"",
|
|
218
331
|
"```text",
|
|
219
|
-
buildPrompt(config, backend, targetInfo, details),
|
|
332
|
+
buildPrompt(config, backend, targetInfo, details, diffPayloads.review),
|
|
220
333
|
"```",
|
|
221
334
|
"",
|
|
335
|
+
"## Diff Handling",
|
|
336
|
+
"",
|
|
337
|
+
formatDiffHandling(diffPayloads.review, "Reviewer Input"),
|
|
338
|
+
formatDiffHandling(diffPayloads.report, "Report Diff"),
|
|
339
|
+
"",
|
|
222
340
|
"## Diff",
|
|
223
341
|
"",
|
|
224
342
|
"```diff",
|
|
225
|
-
|
|
343
|
+
diffPayloads.report.text.trim() || "(empty diff)",
|
|
226
344
|
"```",
|
|
227
345
|
"",
|
|
228
346
|
`## ${reviewer.responseSectionTitle}`,
|
|
@@ -236,10 +354,12 @@ function buildReport(config, backend, targetInfo, details, diffText, reviewer, r
|
|
|
236
354
|
async function runReviewerPrompt(config, backend, targetInfo, details, diffText) {
|
|
237
355
|
const reviewer = REVIEWERS[config.reviewer];
|
|
238
356
|
const reviewWorkspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
|
|
239
|
-
const
|
|
357
|
+
const diffPayloads = prepareDiffPayloads(config, diffText);
|
|
358
|
+
const promptText = buildPrompt(config, backend, targetInfo, details, diffPayloads.review);
|
|
240
359
|
return {
|
|
241
360
|
reviewer,
|
|
242
|
-
|
|
361
|
+
diffPayloads,
|
|
362
|
+
result: await reviewer.run(config, reviewWorkspaceRoot, promptText, diffPayloads.review.text)
|
|
243
363
|
};
|
|
244
364
|
}
|
|
245
365
|
|
|
@@ -282,8 +402,14 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
282
402
|
}
|
|
283
403
|
|
|
284
404
|
const diffText = await backend.getChangeDiff(config, targetInfo, changeId);
|
|
285
|
-
const { reviewer, result: reviewerResult } = await runReviewerPrompt(
|
|
286
|
-
|
|
405
|
+
const { reviewer, diffPayloads, result: reviewerResult } = await runReviewerPrompt(
|
|
406
|
+
config,
|
|
407
|
+
backend,
|
|
408
|
+
targetInfo,
|
|
409
|
+
details,
|
|
410
|
+
diffText
|
|
411
|
+
);
|
|
412
|
+
const report = buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult);
|
|
287
413
|
const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
|
|
288
414
|
await writeTextFile(outputFile, report);
|
|
289
415
|
|
|
@@ -326,7 +452,7 @@ export async function runReviewCycle(config) {
|
|
|
326
452
|
|
|
327
453
|
let changeIdsToReview = [];
|
|
328
454
|
|
|
329
|
-
if (!lastReviewedId
|
|
455
|
+
if (!lastReviewedId) {
|
|
330
456
|
changeIdsToReview = [latestChangeId];
|
|
331
457
|
console.log(`Initialized state to review the latest ${backend.changeName} ${backend.formatChangeId(latestChangeId)} first.`);
|
|
332
458
|
} else {
|