kodevu 0.1.0 → 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 +18 -33
- package/config.example.json +0 -1
- package/package.json +1 -1
- package/src/config.js +47 -5
- package/src/git-client.js +1 -0
- package/src/index.js +37 -3
- package/src/review-runner.js +159 -13
- package/src/shell.js +27 -1
- package/src/svn-client.js +6 -5
package/README.md
CHANGED
|
@@ -18,62 +18,48 @@ A Node.js tool that polls new SVN revisions or Git commits, fetches each change
|
|
|
18
18
|
## Setup
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
|
|
22
|
-
copy config.example.json config.json
|
|
21
|
+
npx kodevu init
|
|
23
22
|
```
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
This creates `config.json` in the current directory from the packaged `config.example.json`.
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
If you want a different path:
|
|
28
27
|
|
|
29
28
|
```bash
|
|
30
|
-
|
|
31
|
-
copy config.example.json config.json
|
|
29
|
+
npx kodevu init --config ./config.current.json
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
Then edit `config.json` and set `target`.
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
`config.json` is the default config file. If you do not pass `--config`, Kodevu will load `./config.json` from the current directory.
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
npm run once
|
|
40
|
-
```
|
|
36
|
+
## Run
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
Run one cycle:
|
|
43
39
|
|
|
44
40
|
```bash
|
|
45
|
-
kodevu --once
|
|
41
|
+
npx kodevu --once
|
|
46
42
|
```
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
Run one cycle with debug logs:
|
|
49
45
|
|
|
50
46
|
```bash
|
|
51
|
-
npx kodevu --once --
|
|
47
|
+
npx kodevu --once --debug
|
|
52
48
|
```
|
|
53
49
|
|
|
54
50
|
Start the scheduler:
|
|
55
51
|
|
|
56
52
|
```bash
|
|
57
|
-
|
|
53
|
+
npx kodevu
|
|
58
54
|
```
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
Use a custom config path only when needed:
|
|
61
57
|
|
|
62
58
|
```bash
|
|
63
|
-
kodevu --config ./config.json
|
|
59
|
+
npx kodevu --config ./config.current.json --once
|
|
64
60
|
```
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
node src/index.js --config ./config.json --once
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Equivalent `npx` usage:
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
npx kodevu --config ./config.json --once
|
|
76
|
-
```
|
|
62
|
+
`--debug` / `-d` is a CLI-only switch. It is not read from `config.json`.
|
|
77
63
|
|
|
78
64
|
## Config
|
|
79
65
|
|
|
@@ -84,14 +70,12 @@ npx kodevu --config ./config.json --once
|
|
|
84
70
|
- `reviewPrompt`: saved into the report as review context
|
|
85
71
|
- `outputDir`: report output directory; default `./reports`
|
|
86
72
|
- `commandTimeoutMs`: timeout for a single review command execution in milliseconds
|
|
87
|
-
- `bootstrapToLatest`: if no state exists, start by reviewing only the current latest change instead of replaying the full history
|
|
88
73
|
- `maxRevisionsPerRun`: cap the number of pending changes per polling cycle
|
|
89
74
|
|
|
90
75
|
Internal defaults:
|
|
91
76
|
|
|
92
|
-
- review state is always stored in `./data/state.json
|
|
93
|
-
-
|
|
94
|
-
- command output is decoded as `utf8`
|
|
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`
|
|
95
79
|
|
|
96
80
|
## Target Rules
|
|
97
81
|
|
|
@@ -104,6 +88,7 @@ Internal defaults:
|
|
|
104
88
|
|
|
105
89
|
- `reviewer: "codex"` uses `codex exec` with the diff embedded in the prompt.
|
|
106
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.
|
|
107
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.
|
|
108
93
|
- For remote SVN URLs without a local working copy, the review still relies on the diff and change metadata only.
|
|
109
94
|
- SVN reports keep the `r123.md` naming style.
|
package/config.example.json
CHANGED
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
|
|
4
6
|
const defaultConfig = {
|
|
5
7
|
vcs: "auto",
|
|
@@ -10,20 +12,28 @@ const defaultConfig = {
|
|
|
10
12
|
commandTimeoutMs: 600000,
|
|
11
13
|
reviewPrompt:
|
|
12
14
|
"请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
|
|
13
|
-
bootstrapToLatest: true,
|
|
14
15
|
maxRevisionsPerRun: 20
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export function parseCliArgs(argv) {
|
|
18
19
|
const args = {
|
|
20
|
+
command: "run",
|
|
19
21
|
configPath: "config.json",
|
|
20
22
|
once: false,
|
|
21
|
-
|
|
23
|
+
debug: false,
|
|
24
|
+
help: false,
|
|
25
|
+
commandExplicitlySet: false
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
for (let index = 0; index < argv.length; index += 1) {
|
|
25
29
|
const value = argv[index];
|
|
26
30
|
|
|
31
|
+
if (value === "init" && !args.commandExplicitlySet && index === 0) {
|
|
32
|
+
args.command = "init";
|
|
33
|
+
args.commandExplicitlySet = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
if (value === "--once") {
|
|
28
38
|
args.once = true;
|
|
29
39
|
continue;
|
|
@@ -34,17 +44,27 @@ export function parseCliArgs(argv) {
|
|
|
34
44
|
continue;
|
|
35
45
|
}
|
|
36
46
|
|
|
47
|
+
if (value === "--debug" || value === "-d") {
|
|
48
|
+
args.debug = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
if (value === "--config" || value === "-c") {
|
|
38
|
-
|
|
53
|
+
const configPath = argv[index + 1];
|
|
54
|
+
if (!configPath || configPath.startsWith("-")) {
|
|
55
|
+
throw new Error(`Missing value for ${value}`);
|
|
56
|
+
}
|
|
57
|
+
args.configPath = configPath;
|
|
39
58
|
index += 1;
|
|
40
59
|
continue;
|
|
41
60
|
}
|
|
42
61
|
}
|
|
43
62
|
|
|
63
|
+
delete args.commandExplicitlySet;
|
|
44
64
|
return args;
|
|
45
65
|
}
|
|
46
66
|
|
|
47
|
-
export async function loadConfig(configPath) {
|
|
67
|
+
export async function loadConfig(configPath, cliArgs = {}) {
|
|
48
68
|
const absoluteConfigPath = path.resolve(configPath);
|
|
49
69
|
const raw = await fs.readFile(absoluteConfigPath, "utf8");
|
|
50
70
|
const config = {
|
|
@@ -62,6 +82,7 @@ export async function loadConfig(configPath) {
|
|
|
62
82
|
|
|
63
83
|
config.vcs = String(config.vcs || "auto").toLowerCase();
|
|
64
84
|
config.reviewer = String(config.reviewer || "codex").toLowerCase();
|
|
85
|
+
config.debug = Boolean(cliArgs.debug);
|
|
65
86
|
|
|
66
87
|
if (!["auto", "svn", "git"].includes(config.vcs)) {
|
|
67
88
|
throw new Error(`"vcs" must be one of "auto", "svn", or "git" in ${absoluteConfigPath}`);
|
|
@@ -93,11 +114,14 @@ export function printHelp() {
|
|
|
93
114
|
console.log(`Kodevu
|
|
94
115
|
|
|
95
116
|
Usage:
|
|
117
|
+
kodevu init
|
|
118
|
+
npx kodevu init
|
|
96
119
|
kodevu [--config config.json] [--once]
|
|
97
120
|
npx kodevu [--config config.json] [--once]
|
|
98
121
|
|
|
99
122
|
Options:
|
|
100
|
-
--config, -c Path to config json. Default: ./config.json
|
|
123
|
+
--config, -c Path to config json. Default: ./config.json in the current directory
|
|
124
|
+
--debug, -d Print extra debug information to the console
|
|
101
125
|
--once Run one polling cycle and exit
|
|
102
126
|
--help, -h Show help
|
|
103
127
|
|
|
@@ -107,3 +131,21 @@ Config highlights:
|
|
|
107
131
|
target Repository target path (Git) or SVN working copy / URL
|
|
108
132
|
`);
|
|
109
133
|
}
|
|
134
|
+
|
|
135
|
+
export async function initConfig(targetPath = "config.json") {
|
|
136
|
+
const absoluteTargetPath = path.resolve(targetPath);
|
|
137
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
138
|
+
const templatePath = path.join(packageRoot, "config.example.json");
|
|
139
|
+
|
|
140
|
+
await fs.mkdir(path.dirname(absoluteTargetPath), { recursive: true });
|
|
141
|
+
try {
|
|
142
|
+
await fs.copyFile(templatePath, absoluteTargetPath, fsConstants.COPYFILE_EXCL);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error?.code === "EEXIST") {
|
|
145
|
+
throw new Error(`Config file already exists: ${absoluteTargetPath}`);
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return absoluteTargetPath;
|
|
151
|
+
}
|
package/src/git-client.js
CHANGED
package/src/index.js
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import cron from "node-cron";
|
|
4
|
-
import { loadConfig, parseCliArgs, printHelp } from "./config.js";
|
|
4
|
+
import { initConfig, loadConfig, parseCliArgs, printHelp } from "./config.js";
|
|
5
5
|
import { runReviewCycle } from "./review-runner.js";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
let cliArgs;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
cliArgs = parseCliArgs(process.argv.slice(2));
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(error?.message || String(error));
|
|
13
|
+
printHelp();
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
if (cliArgs.help) {
|
|
10
18
|
printHelp();
|
|
11
19
|
process.exit(0);
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
if (cliArgs.command === "init") {
|
|
23
|
+
try {
|
|
24
|
+
const createdPath = await initConfig(cliArgs.configPath);
|
|
25
|
+
console.log(`Created config: ${createdPath}`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(error?.stack || String(error));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const config = await loadConfig(cliArgs.configPath, cliArgs);
|
|
34
|
+
|
|
35
|
+
if (config.debug) {
|
|
36
|
+
console.error(
|
|
37
|
+
`[debug] Loaded config: ${JSON.stringify({
|
|
38
|
+
configPath: config.configPath,
|
|
39
|
+
vcs: config.vcs,
|
|
40
|
+
reviewer: config.reviewer,
|
|
41
|
+
target: config.target,
|
|
42
|
+
pollCron: config.pollCron,
|
|
43
|
+
outputDir: config.outputDir,
|
|
44
|
+
debug: config.debug,
|
|
45
|
+
once: cliArgs.once
|
|
46
|
+
})}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
15
49
|
|
|
16
50
|
let running = false;
|
|
17
51
|
|
package/src/review-runner.js
CHANGED
|
@@ -4,6 +4,24 @@ 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
|
+
|
|
19
|
+
function debugLog(config, message) {
|
|
20
|
+
if (config.debug) {
|
|
21
|
+
console.error(`[debug] ${message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
const REVIEWERS = {
|
|
8
26
|
codex: {
|
|
9
27
|
displayName: "Codex",
|
|
@@ -29,7 +47,8 @@ const REVIEWERS = {
|
|
|
29
47
|
cwd: workingDir,
|
|
30
48
|
input: [promptText, "Unified diff:", diffText].join("\n\n"),
|
|
31
49
|
allowFailure: true,
|
|
32
|
-
timeoutMs: config.commandTimeoutMs
|
|
50
|
+
timeoutMs: config.commandTimeoutMs,
|
|
51
|
+
debug: config.debug
|
|
33
52
|
});
|
|
34
53
|
|
|
35
54
|
let message = "";
|
|
@@ -58,7 +77,8 @@ const REVIEWERS = {
|
|
|
58
77
|
cwd: workingDir,
|
|
59
78
|
input: ["Unified diff:", diffText].join("\n\n"),
|
|
60
79
|
allowFailure: true,
|
|
61
|
-
timeoutMs: config.commandTimeoutMs
|
|
80
|
+
timeoutMs: config.commandTimeoutMs,
|
|
81
|
+
debug: config.debug
|
|
62
82
|
});
|
|
63
83
|
|
|
64
84
|
return {
|
|
@@ -159,7 +179,106 @@ function getReviewWorkspaceRoot(config, backend, targetInfo) {
|
|
|
159
179
|
return config.baseDir;
|
|
160
180
|
}
|
|
161
181
|
|
|
162
|
-
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) {
|
|
163
282
|
const fileList = details.changedPaths.map((item) => `${item.action} ${item.relativePath}`).join("\n");
|
|
164
283
|
const workspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
|
|
165
284
|
const canReadRelatedFiles = backend.kind === "git" || Boolean(targetInfo.workingCopyPath);
|
|
@@ -173,17 +292,19 @@ function buildPrompt(config, backend, targetInfo, details) {
|
|
|
173
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."
|
|
174
293
|
: "Review primarily from the diff below. Do not assume access to other local files, shell commands, or repository history.",
|
|
175
294
|
"Use plain text file references like path/to/file.js:123. Do not emit clickable workspace links.",
|
|
176
|
-
"Write the final review in Simplified Chinese.",
|
|
177
295
|
`Repository Type: ${backend.displayName}`,
|
|
178
296
|
`Change ID: ${details.displayId}`,
|
|
179
297
|
`Author: ${details.author}`,
|
|
180
298
|
`Date: ${details.date || "unknown"}`,
|
|
181
299
|
`Changed files:\n${fileList || "(none)"}`,
|
|
182
|
-
`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.`
|
|
183
304
|
].join("\n\n");
|
|
184
305
|
}
|
|
185
306
|
|
|
186
|
-
function buildReport(config, backend, targetInfo, details,
|
|
307
|
+
function buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult) {
|
|
187
308
|
const lines = [
|
|
188
309
|
`# ${backend.displayName} Review Report: ${details.displayId}`,
|
|
189
310
|
"",
|
|
@@ -208,13 +329,18 @@ function buildReport(config, backend, targetInfo, details, diffText, reviewer, r
|
|
|
208
329
|
"## Review Context",
|
|
209
330
|
"",
|
|
210
331
|
"```text",
|
|
211
|
-
buildPrompt(config, backend, targetInfo, details),
|
|
332
|
+
buildPrompt(config, backend, targetInfo, details, diffPayloads.review),
|
|
212
333
|
"```",
|
|
213
334
|
"",
|
|
335
|
+
"## Diff Handling",
|
|
336
|
+
"",
|
|
337
|
+
formatDiffHandling(diffPayloads.review, "Reviewer Input"),
|
|
338
|
+
formatDiffHandling(diffPayloads.report, "Report Diff"),
|
|
339
|
+
"",
|
|
214
340
|
"## Diff",
|
|
215
341
|
"",
|
|
216
342
|
"```diff",
|
|
217
|
-
|
|
343
|
+
diffPayloads.report.text.trim() || "(empty diff)",
|
|
218
344
|
"```",
|
|
219
345
|
"",
|
|
220
346
|
`## ${reviewer.responseSectionTitle}`,
|
|
@@ -228,10 +354,12 @@ function buildReport(config, backend, targetInfo, details, diffText, reviewer, r
|
|
|
228
354
|
async function runReviewerPrompt(config, backend, targetInfo, details, diffText) {
|
|
229
355
|
const reviewer = REVIEWERS[config.reviewer];
|
|
230
356
|
const reviewWorkspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
|
|
231
|
-
const
|
|
357
|
+
const diffPayloads = prepareDiffPayloads(config, diffText);
|
|
358
|
+
const promptText = buildPrompt(config, backend, targetInfo, details, diffPayloads.review);
|
|
232
359
|
return {
|
|
233
360
|
reviewer,
|
|
234
|
-
|
|
361
|
+
diffPayloads,
|
|
362
|
+
result: await reviewer.run(config, reviewWorkspaceRoot, promptText, diffPayloads.review.text)
|
|
235
363
|
};
|
|
236
364
|
}
|
|
237
365
|
|
|
@@ -274,8 +402,14 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
274
402
|
}
|
|
275
403
|
|
|
276
404
|
const diffText = await backend.getChangeDiff(config, targetInfo, changeId);
|
|
277
|
-
const { reviewer, result: reviewerResult } = await runReviewerPrompt(
|
|
278
|
-
|
|
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);
|
|
279
413
|
const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
|
|
280
414
|
await writeTextFile(outputFile, report);
|
|
281
415
|
|
|
@@ -294,10 +428,18 @@ export async function runReviewCycle(config) {
|
|
|
294
428
|
await ensureDir(config.outputDir);
|
|
295
429
|
|
|
296
430
|
const { backend, targetInfo } = await resolveRepositoryContext(config);
|
|
431
|
+
debugLog(
|
|
432
|
+
config,
|
|
433
|
+
`Resolved repository context: backend=${backend.kind} target=${targetInfo.targetDisplay || config.target} stateKey=${targetInfo.stateKey}`
|
|
434
|
+
);
|
|
297
435
|
const latestChangeId = await backend.getLatestChangeId(config, targetInfo);
|
|
298
436
|
const stateFile = await loadState(config.stateFilePath);
|
|
299
437
|
const projectState = getProjectState(stateFile, targetInfo);
|
|
300
438
|
let lastReviewedId = readLastReviewedId(projectState, backend, targetInfo);
|
|
439
|
+
debugLog(
|
|
440
|
+
config,
|
|
441
|
+
`Checkpoint status: latest=${backend.formatChangeId(latestChangeId)} lastReviewed=${lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)"}`
|
|
442
|
+
);
|
|
301
443
|
|
|
302
444
|
if (lastReviewedId) {
|
|
303
445
|
const checkpointIsValid = await backend.isValidCheckpoint(config, targetInfo, lastReviewedId, latestChangeId);
|
|
@@ -310,7 +452,7 @@ export async function runReviewCycle(config) {
|
|
|
310
452
|
|
|
311
453
|
let changeIdsToReview = [];
|
|
312
454
|
|
|
313
|
-
if (!lastReviewedId
|
|
455
|
+
if (!lastReviewedId) {
|
|
314
456
|
changeIdsToReview = [latestChangeId];
|
|
315
457
|
console.log(`Initialized state to review the latest ${backend.changeName} ${backend.formatChangeId(latestChangeId)} first.`);
|
|
316
458
|
} else {
|
|
@@ -323,6 +465,8 @@ export async function runReviewCycle(config) {
|
|
|
323
465
|
);
|
|
324
466
|
}
|
|
325
467
|
|
|
468
|
+
debugLog(config, `Planned ${changeIdsToReview.length} ${backend.changeName}(s) for this cycle.`);
|
|
469
|
+
|
|
326
470
|
if (changeIdsToReview.length === 0) {
|
|
327
471
|
const lastKnownId = lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)";
|
|
328
472
|
console.log(`No new ${backend.changeName}s. Last reviewed: ${lastKnownId}`);
|
|
@@ -332,11 +476,13 @@ export async function runReviewCycle(config) {
|
|
|
332
476
|
console.log(`Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`);
|
|
333
477
|
|
|
334
478
|
for (const changeId of changeIdsToReview) {
|
|
479
|
+
debugLog(config, `Starting review for ${backend.formatChangeId(changeId)}.`);
|
|
335
480
|
const result = await reviewChange(config, backend, targetInfo, changeId);
|
|
336
481
|
console.log(`Reviewed ${backend.formatChangeId(changeId)}: ${result.outputFile}`);
|
|
337
482
|
const nextProjectState = buildStateSnapshot(backend, targetInfo, changeId);
|
|
338
483
|
await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectState));
|
|
339
484
|
stateFile.projects[targetInfo.stateKey] = nextProjectState;
|
|
485
|
+
debugLog(config, `Saved checkpoint for ${backend.formatChangeId(changeId)} to ${config.stateFilePath}.`);
|
|
340
486
|
}
|
|
341
487
|
|
|
342
488
|
const remainingChanges = await backend.getPendingChangeIds(
|
package/src/shell.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import iconv from "iconv-lite";
|
|
3
3
|
|
|
4
|
+
function debugLog(enabled, message) {
|
|
5
|
+
if (enabled) {
|
|
6
|
+
console.error(`[debug] ${message}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function summarizeOutput(text) {
|
|
11
|
+
if (!text) {
|
|
12
|
+
return "(empty)";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
16
|
+
return normalized.length > 400 ? `${normalized.slice(0, 400)}...` : normalized;
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
export async function runCommand(command, args = [], options = {}) {
|
|
5
20
|
const {
|
|
6
21
|
cwd,
|
|
@@ -9,9 +24,15 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
9
24
|
encoding = "utf8",
|
|
10
25
|
allowFailure = false,
|
|
11
26
|
timeoutMs = 0,
|
|
12
|
-
trim = false
|
|
27
|
+
trim = false,
|
|
28
|
+
debug = false
|
|
13
29
|
} = options;
|
|
14
30
|
|
|
31
|
+
debugLog(
|
|
32
|
+
debug,
|
|
33
|
+
`run: ${command} ${args.join(" ")}${cwd ? ` | cwd=${cwd}` : ""}${timeoutMs > 0 ? ` | timeoutMs=${timeoutMs}` : ""}`
|
|
34
|
+
);
|
|
35
|
+
|
|
15
36
|
return await new Promise((resolve, reject) => {
|
|
16
37
|
const child = spawn(command, args, {
|
|
17
38
|
cwd,
|
|
@@ -51,6 +72,11 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
51
72
|
stderr: trim ? stderr.trim() : stderr
|
|
52
73
|
};
|
|
53
74
|
|
|
75
|
+
debugLog(
|
|
76
|
+
debug,
|
|
77
|
+
`exit: ${command} code=${result.code} timedOut=${result.timedOut} stdout=${summarizeOutput(result.stdout)} stderr=${summarizeOutput(result.stderr)}`
|
|
78
|
+
);
|
|
79
|
+
|
|
54
80
|
if ((result.code !== 0 || result.timedOut) && !allowFailure) {
|
|
55
81
|
const error = new Error(
|
|
56
82
|
`Command failed: ${command} ${args.join(" ")}\n${result.stderr || result.stdout}`.trim()
|
package/src/svn-client.js
CHANGED
|
@@ -38,7 +38,8 @@ function repoPathFromUrl(rootUrl, url) {
|
|
|
38
38
|
export async function getTargetInfo(config) {
|
|
39
39
|
const result = await runCommand(SVN_COMMAND, ["info", "--xml", config.target], {
|
|
40
40
|
encoding: COMMAND_ENCODING,
|
|
41
|
-
trim: true
|
|
41
|
+
trim: true,
|
|
42
|
+
debug: config.debug
|
|
42
43
|
});
|
|
43
44
|
const parsed = xmlParser.parse(result.stdout);
|
|
44
45
|
const entry = parsed?.info?.entry;
|
|
@@ -70,7 +71,7 @@ export async function getLatestRevision(config, targetInfo) {
|
|
|
70
71
|
const result = await runCommand(
|
|
71
72
|
SVN_COMMAND,
|
|
72
73
|
["log", "--xml", "-r", "HEAD:1", "-l", "1", getRemoteTarget(targetInfo, config)],
|
|
73
|
-
{ encoding: COMMAND_ENCODING, trim: true }
|
|
74
|
+
{ encoding: COMMAND_ENCODING, trim: true, debug: config.debug }
|
|
74
75
|
);
|
|
75
76
|
const parsed = xmlParser.parse(result.stdout);
|
|
76
77
|
const entry = parsed?.log?.logentry;
|
|
@@ -100,7 +101,7 @@ export async function getPendingRevisions(config, targetInfo, startExclusive, en
|
|
|
100
101
|
`${startRevision}:${endInclusive}`,
|
|
101
102
|
getRemoteTarget(targetInfo, config)
|
|
102
103
|
],
|
|
103
|
-
{ encoding: COMMAND_ENCODING, trim: true }
|
|
104
|
+
{ encoding: COMMAND_ENCODING, trim: true, debug: config.debug }
|
|
104
105
|
);
|
|
105
106
|
const parsed = xmlParser.parse(result.stdout);
|
|
106
107
|
|
|
@@ -115,7 +116,7 @@ export async function getRevisionDiff(config, revision) {
|
|
|
115
116
|
const result = await runCommand(
|
|
116
117
|
SVN_COMMAND,
|
|
117
118
|
["diff", "--git", "--internal-diff", "--ignore-properties", "-c", String(revision), config.target],
|
|
118
|
-
{ encoding: COMMAND_ENCODING, trim: false }
|
|
119
|
+
{ encoding: COMMAND_ENCODING, trim: false, debug: config.debug }
|
|
119
120
|
);
|
|
120
121
|
|
|
121
122
|
return result.stdout;
|
|
@@ -145,7 +146,7 @@ export async function getRevisionDetails(config, targetInfo, revision) {
|
|
|
145
146
|
const result = await runCommand(
|
|
146
147
|
SVN_COMMAND,
|
|
147
148
|
["log", "--xml", "-v", "-c", String(revision), getRemoteTarget(targetInfo, config)],
|
|
148
|
-
{ encoding: COMMAND_ENCODING, trim: true }
|
|
149
|
+
{ encoding: COMMAND_ENCODING, trim: true, debug: config.debug }
|
|
149
150
|
);
|
|
150
151
|
const parsed = xmlParser.parse(result.stdout);
|
|
151
152
|
const entry = parsed?.log?.logentry;
|