kodevu 0.1.47 → 0.1.49

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": "kodevu",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
package/src/config.js CHANGED
@@ -56,7 +56,7 @@ function normalizeOutputFormats(outputFormats) {
56
56
  return normalized.length === 0 ? ["markdown"] : normalized;
57
57
  }
58
58
 
59
- function detectLanguage() {
59
+ export function detectLanguage() {
60
60
  const envLang = (process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "").toLowerCase();
61
61
  const intlLocale = (() => {
62
62
  try {
@@ -66,12 +66,19 @@ function detectLanguage() {
66
66
  }
67
67
  })();
68
68
 
69
- const locales = [envLang, intlLocale].filter(Boolean);
69
+ const locales = [envLang, intlLocale].filter(l => l && l !== "und");
70
+
71
+ // 1. Search for Chinese in any source first, to avoid "fake" English defaults in some shells on Windows
70
72
  for (const loc of locales) {
71
73
  if (loc.startsWith("zh")) return "zh";
74
+ }
75
+
76
+ // 2. Search for English
77
+ for (const loc of locales) {
72
78
  if (loc.startsWith("en")) return "en";
73
79
  }
74
80
 
81
+ // 3. Fallback to the first part of the first detected locale, or "en"
75
82
  return locales[0]?.split(/[._-]/)[0] || "en";
76
83
  }
77
84
 
package/src/logger.js CHANGED
@@ -6,7 +6,6 @@ class Logger {
6
6
  constructor() {
7
7
  this.config = null;
8
8
  this.logFile = null;
9
- this.progressDisplay = null;
10
9
  this.initialized = false;
11
10
  }
12
11
 
@@ -31,10 +30,6 @@ class Logger {
31
30
  }
32
31
  }
33
32
 
34
- setProgressDisplay(pd) {
35
- this.progressDisplay = pd;
36
- }
37
-
38
33
  info(message) {
39
34
  this._log("INFO", message);
40
35
  }
@@ -78,14 +73,10 @@ class Logger {
78
73
  // If it's debug and debug mode is off, skip console
79
74
  if (isDebug && !this.config?.debug) return;
80
75
 
81
- if (this.progressDisplay) {
82
- this.progressDisplay.log(logLine);
76
+ if (isError || isWarn) {
77
+ console.error(logLine);
83
78
  } else {
84
- if (isError || isWarn) {
85
- console.error(logLine);
86
- } else {
87
- console.log(logLine);
88
- }
79
+ console.log(logLine);
89
80
  }
90
81
  }
91
82
 
@@ -1,5 +1,3 @@
1
- import { logger } from "./logger.js";
2
-
3
1
  function clampProgress(value) {
4
2
  if (!Number.isFinite(value)) {
5
3
  return 0;
@@ -8,89 +6,34 @@ function clampProgress(value) {
8
6
  return Math.max(0, Math.min(1, value));
9
7
  }
10
8
 
11
- class ProgressItem {
12
- constructor(display, label) {
13
- this.display = display;
14
- this.label = label;
15
- this.progress = 0;
16
- this.stage = "";
17
- this.active = false;
18
- this.lastStatusLine = "";
19
- }
20
-
21
- start(stage = "starting") {
22
- this.active = true;
23
- this.stage = stage;
24
- logger.debug(`${this.label} batch start: ${stage}`);
25
- this.writeStatus();
26
- }
27
-
28
- update(progress, stage) {
29
- this.progress = clampProgress(progress);
30
-
31
- if (stage) {
32
- this.stage = stage;
33
- logger.debug(`${this.label} stage: ${stage} (${Math.round(this.progress * 100)}%)`);
34
- }
35
-
36
- this.writeStatus();
37
- }
9
+ function formatStatusLine(label, progress, stage) {
10
+ const pct = `${Math.round(progress * 100)}`.padStart(3, " ");
11
+ return `[progress] ${pct}% ${label}${stage ? ` | ${stage}` : ""}`;
12
+ }
38
13
 
39
- log(message) {
40
- // We don't log to file here because usually progress.log() is called alongside logger.info()
41
- // or we want the caller to decide whether it goes to the log file.
42
- this.display.writeLine(message);
43
- }
14
+ export function createProgressReporter(label, options = {}) {
15
+ const stream = options.stream || process.stdout;
16
+ let lastStatusLine = "";
44
17
 
45
- succeed(message) {
46
- const finalMsg = message || `${this.label} complete`;
47
- this.finish("[done]", 1, finalMsg);
48
- logger.debug(`${this.label} batch succeed: ${finalMsg}`);
18
+ function writeLine(message) {
19
+ stream.write(`${message}\n`);
49
20
  }
50
21
 
51
- fail(message) {
52
- const finalMsg = message || `${this.label} failed`;
53
- this.finish("[fail]", this.progress, finalMsg);
54
- logger.error(`${this.label} batch fail: ${finalMsg}`);
55
- }
22
+ return {
23
+ update(progress, stage = "") {
24
+ const line = formatStatusLine(label, clampProgress(progress), stage);
56
25
 
57
- finish(prefix, progress, message) {
58
- this.progress = clampProgress(progress);
59
- this.active = false;
60
- this.display.writeLine(`${prefix} ${message}`);
61
- }
26
+ if (line === lastStatusLine) {
27
+ return;
28
+ }
62
29
 
63
- writeStatus() {
64
- const line = this.buildStatusLine();
30
+ lastStatusLine = line;
31
+ writeLine(line);
32
+ },
65
33
 
66
- if (line === this.lastStatusLine) {
67
- return;
34
+ finish(status, message) {
35
+ const prefix = status === "fail" ? "[fail]" : "[done]";
36
+ writeLine(`${prefix} ${message || label}`);
68
37
  }
69
-
70
- this.lastStatusLine = line;
71
- this.display.writeLine(line);
72
- }
73
-
74
- buildStatusLine() {
75
- const pct = `${Math.round(this.progress * 100)}`.padStart(3, " ");
76
- return `[progress] ${pct}% ${this.label}${this.stage ? ` | ${this.stage}` : ""}`;
77
- }
78
- }
79
-
80
- export class ProgressDisplay {
81
- constructor(options = {}) {
82
- this.stream = options.stream || process.stdout;
83
- }
84
-
85
- createItem(label) {
86
- return new ProgressItem(this, label);
87
- }
88
-
89
- writeLine(message) {
90
- this.stream.write(`${message}\n`);
91
- }
92
-
93
- log(message) {
94
- this.writeLine(message);
95
- }
38
+ };
96
39
  }
package/src/prompts.js ADDED
@@ -0,0 +1,248 @@
1
+ import { formatDate } from "./utils.js";
2
+
3
+ const LOCALIZED_DATA = {
4
+ en: {
5
+ displayName: "English",
6
+ systemRole: `You are a Senior Software Engineer and Code Review Expert with over 10 years of experience. Your task is to perform a rigorous and high-quality review of the following code changes.
7
+ Your goals are to:
8
+ 1. Identify bugs, logical flaws, and potential regression risks.
9
+ 2. Spot performance bottlenecks, memory leaks, or unnecessary computations.
10
+ 3. Check for security vulnerabilities (e.g., injection, authorization issues, sensitive data leaks).
11
+ 4. Evaluate maintainability, readability, and adherence to best practices.
12
+ 5. Verify coverage of necessary unit tests and boundary conditions.`,
13
+ outputFormat: `Your output must be structured using the following Markdown headers:
14
+ ### 1. Summary
15
+ Briefly describe the purpose and impact of this change.
16
+ ### 2. Critical Issues
17
+ List bugs, security risks, or problems that could cause crashes or logical errors. Include file names and line numbers.
18
+ ### 3. Suggestions
19
+ List points for improvement regarding code style, performance, or architectural design.
20
+ ### 4. Conclusion
21
+ Summarize with a "Pass" or "Needs Revision". If no clear flaws are found, state "No clear flaws found" and mention any residual risks.`,
22
+ constraints: `Note: You are in a read-only review mode. Do not attempt to call any external tools or MCP (Model Context Protocol) tools. Do not act as if you are "applying the patch" or "executing code". Provide only textual analysis and feedback.`,
23
+ phrases: {
24
+ workspaceRoot: "Workspace context (read-only):",
25
+ noWorkspace: "No local repository workspace is available for this review run.",
26
+ besidesDiff: "You can read related files in the workspace to understand call sites, shared utilities, configuration, or data flow.",
27
+ reviewFromDiff: "Review primarily based on the provided diff. Do not assume access to other local files or shell commands. You MUST NOT call any MCP tools.",
28
+ fileRefs: "Reference files using plain text like 'path/to/file.js:123'. Do not generate clickable workspace links.",
29
+ repoType: "Repository Type",
30
+ changeId: "Change ID",
31
+ author: "Author",
32
+ date: "Date",
33
+ changedFiles: "Changed files",
34
+ commitMessage: "Commit message",
35
+ diffNoteTruncated: "Note: The diff was truncated to fit size limits. Original: {originalLineCount} lines / {originalCharCount} chars. Included: {outputLineCount} lines / {outputCharCount} chars.",
36
+ diffNoteFull: "Note: Full diff provided ({originalLineCount} lines / {originalCharCount} chars).",
37
+ langRule: "--- LANGUAGE RULE ---\nYour entire response must be in {langName}. No other language allowed.",
38
+ outputDirective: "--- BEGIN REVIEW ---\nNow output your COMPLETE code review. Cover ALL four sections (Summary, Critical Issues, Suggestions, Conclusion). Do NOT ask clarifying questions, do NOT acknowledge these instructions, do NOT say you are ready. Start your response directly with the review content."
39
+ }
40
+ },
41
+ zh: {
42
+ displayName: "Simplified Chinese (简体中文)",
43
+ systemRole: `你是一位拥有 10 年以上经验的高级软件架构师和代码审查专家。你的任务是对以下代码变更进行严格且高质量的审查。
44
+ 你的目标是:
45
+ 1. 发现代码中的 Bug、逻辑缺陷和潜在的回归风险。
46
+ 2. 识别性能瓶颈、内存泄漏或不必要的计算。
47
+ 3. 检查安全漏洞(如注入、越权、敏感信息泄露等)。
48
+ 4. 评估代码的可维护性、可读性和是否符合最佳实践。
49
+ 5. 检查是否涵盖了必要的单元测试和边界条件。`,
50
+ outputFormat: `你的输出格式必须清晰,请使用以下 Markdown 结构:
51
+ ### 1. 变更总结 (Summary)
52
+ 简要描述这次提交的主要目的和影响面。
53
+ ### 2. 核心缺陷 (Critical Issues)
54
+ 列出 Bug、安全隐患或会导致程序崩溃/逻辑错误的问题。请注明文件名和行号。
55
+ ### 3. 改进建议 (Suggestions)
56
+ 列出关于代码风格、性能优化或架构设计的改进点。
57
+ ### 4. 审查结论 (Conclusion)
58
+ 如果发现明显缺陷,总结修复建议;如果未发现明显缺陷,请说明“未发现明显缺陷”并指出可能的残留风险。`,
59
+ constraints: `注意:你正处于只读审查模式,禁止调用任何外部工具或 MCP (Model Context Protocol) 工具。请勿表现出“正在应用补丁”或“准备执行代码”的行为。只需提供文字审查分析。`,
60
+ phrases: {
61
+ workspaceRoot: "只读工作区上下文:",
62
+ noWorkspace: "此审查运行没有可用的本地仓库工作区。",
63
+ besidesDiff: "你可以阅读工作区中的其他文件以了解调用点、工具类、配置或数据流。",
64
+ reviewFromDiff: "主要根据提供的 Diff 进行审查。不要假设可以访问其他文件或执行 Shell 命令。你绝不能调用任何 MCP 工具。",
65
+ fileRefs: "使用纯文本引用文件,如 'path/to/file.js:123'。不要生成可点击的链接。",
66
+ repoType: "仓库类型",
67
+ changeId: "变更 ID",
68
+ author: "作者",
69
+ date: "日期",
70
+ changedFiles: "已变更文件",
71
+ commitMessage: "提交信息",
72
+ diffNoteTruncated: "注意:Diff 已截断。原始:{originalLineCount} 行 / {originalCharCount} 字符。包含:{outputLineCount} 行 / {outputCharCount} 字符。",
73
+ diffNoteFull: "注意:包含完整 Diff ({originalLineCount} 行 / {originalCharCount} 字符)。",
74
+ langRule: "--- 语言规则 ---\n你必须完全使用 {langName} 进行回复。不得使用其他语言进行解释或总结。",
75
+ outputDirective: "--- 开始输出审查结果 ---\n请立即输出完整的代码审查结果,必须包含全部四个章节(变更总结、核心缺陷、改进建议、审查结论)。不要提问,不要确认收到指令,不要说准备好了,直接以审查内容开始输出。"
76
+ }
77
+ },
78
+ "zh-tw": {
79
+ displayName: "Traditional Chinese (繁體中文)",
80
+ systemRole: `你是一位擁有 10 年以上經驗的高級軟體架構師和代碼審查專家。你的任務是對以下代碼變更進行嚴格且高質量的審查。
81
+ 你的目標是:
82
+ 1. 發現代碼中的 Bug、邏輯缺陷和潛在的回歸風險。
83
+ 2. 識別性能瓶頸、記憶體洩漏或不必要的計算。
84
+ 3. 檢查安全漏洞(如注入、越權、敏感信息洩露等)。
85
+ 4. 評估代碼的可維護性、可讀性和是否符合最佳實踐。
86
+ 5. 檢查是否涵蓋了必要的单元測試和邊界條件。`,
87
+ outputFormat: `你的輸出格式必須清晰,請使用以下 Markdown 結構:
88
+ ### 1. 變更總結 (Summary)
89
+ 簡要描述這次提交的主要目的和影響面。
90
+ ### 2. 核心缺陷 (Critical Issues)
91
+ 列出 Bug、安全隱患或會導致程序崩潰/邏輯錯誤的問題。請註明檔案名和行號。
92
+ ### 3. 改進建議 (Suggestions)
93
+ 列出關於代碼風格、性能優化或架構設計的改進點。
94
+ ### 4. 審查結論 (Conclusion)
95
+ 如果發現明顯缺陷,總結修復建議;如果未發現明顯缺陷,請說明「未發現明顯缺陷」並指出可能的殘留風險。`,
96
+ constraints: `注意:你正處於唯讀審查模式,禁止調用任何外部工具或 MCP (Model Context Protocol) 工具。請勿表現出「正在應用補丁」或「準備執行代碼」的行為。只需提供文字審查分析。`,
97
+ phrases: {
98
+ workspaceRoot: "唯讀工作區上下文:",
99
+ noWorkspace: "此審查運行沒有可用的本地倉庫工作區。",
100
+ besidesDiff: "你可以閱讀工作區中的其他文件以了解調用點、工具類、配置或資料流。",
101
+ reviewFromDiff: "主要根據提供的 Diff 進行審查。不要假設可以訪問其他文件或執行 Shell 命令。你絕不能調用任何 MCP 工具。",
102
+ fileRefs: "使用純文本引用文件,如 'path/to/file.js:123'。不要生成可點擊的連結。",
103
+ repoType: "倉庫類型",
104
+ changeId: "變更 ID",
105
+ author: "作者",
106
+ date: "日期",
107
+ changedFiles: "已變更文件",
108
+ commitMessage: "提交信息",
109
+ diffNoteTruncated: "注意:Diff 已截斷。原始:{originalLineCount} 行 / {originalCharCount} 字符。包含:{outputLineCount} 行 / {outputCharCount} 字符。",
110
+ diffNoteFull: "注意:包含完整 Diff ({originalLineCount} 行 / {originalCharCount} 字符)。",
111
+ langRule: "--- 語言規則 ---\n你必須完全使用 {langName} 進行回覆。不得使用其他語言進行解釋或總結。",
112
+ outputDirective: "--- 開始輸出審查結果 ---\n請立即輸出完整的代碼審查結果,必須包含全部四個章節(變更總結、核心缺陷、改進建議、審查結論)。不要提問,不要確認指令,不要說準備好了,直接以審查內容開始輸出。"
113
+ }
114
+ }
115
+ };
116
+
117
+ function getLangKey(lang) {
118
+ const lowArg = (lang || "en").toLowerCase();
119
+ if (lowArg.startsWith("zh")) {
120
+ return (lowArg === "zh-tw" || lowArg === "zh-hk") ? "zh-tw" : "zh";
121
+ }
122
+ return "en";
123
+ }
124
+
125
+ function getLanguageDisplayName(lang) {
126
+ const key = getLangKey(lang);
127
+ const data = LOCALIZED_DATA[key];
128
+ if (data && lang.toLowerCase().startsWith("zh")) return data.displayName;
129
+
130
+ const extras = {
131
+ en: "English",
132
+ jp: "Japanese (日本語)", ja: "Japanese (日本語)",
133
+ kr: "Korean (한국어)", ko: "Korean (한국어)",
134
+ fr: "French (Français)", de: "German (Deutsch)",
135
+ es: "Spanish (Español)", it: "Italian (Italiano)",
136
+ ru: "Russian (Русский)"
137
+ };
138
+ const low = (lang || "en").toLowerCase();
139
+ return extras[low] || extras[low.split("-")[0]] || lang || "English";
140
+ }
141
+
142
+ function getPhrase(key, lang, placeholders = {}) {
143
+ const langKey = getLangKey(lang);
144
+ let phrase = LOCALIZED_DATA[langKey]?.phrases[key] || LOCALIZED_DATA.en.phrases[key];
145
+ if (!phrase) return "";
146
+ for (const [k, v] of Object.entries(placeholders)) {
147
+ phrase = phrase.replace(`{${k}}`, v);
148
+ }
149
+ return phrase;
150
+ }
151
+
152
+ export class PromptBuilder {
153
+ constructor(config, backend, targetInfo, details) {
154
+ this.config = config;
155
+ this.backend = backend;
156
+ this.targetInfo = targetInfo;
157
+ this.details = details;
158
+ this.lang = config.resolvedLang || "en";
159
+ this.langKey = getLangKey(this.lang);
160
+ this.data = LOCALIZED_DATA[this.langKey] || LOCALIZED_DATA.en;
161
+ }
162
+
163
+ getLangInstruction() {
164
+ const langName = getLanguageDisplayName(this.lang);
165
+ const lowLang = this.lang.toLowerCase();
166
+ let instruction = `CRITICAL: YOUR ENTIRE RESPONSE MUST BE IN ${langName.toUpperCase()}.`;
167
+ if (lowLang.startsWith("zh")) {
168
+ if (lowLang === "zh-tw" || lowLang === "zh-hk") {
169
+ instruction += "\n請務必完全使用繁體中文進行回覆。";
170
+ } else {
171
+ instruction += "\n请务必完全使用简体中文进行回复。";
172
+ }
173
+ }
174
+ return instruction;
175
+ }
176
+
177
+ getMetadata() {
178
+ const fileList = this.details.changedPaths.map((item) => `${item.action} ${item.relativePath}`).join("\n");
179
+ return [
180
+ `${getPhrase("repoType", this.lang)}: ${this.backend.displayName}`,
181
+ `${getPhrase("changeId", this.lang)}: ${this.details.displayId}`,
182
+ `${getPhrase("author", this.lang)}: ${this.details.author}`,
183
+ `${getPhrase("date", this.lang)}: ${formatDate(this.details.date) || "unknown"}`,
184
+ `${getPhrase("changedFiles", this.lang)}:\n${fileList || "(none)"}`,
185
+ `${getPhrase("commitMessage", this.lang)}:\n${this.details.message || "(empty)"}`
186
+ ].join("\n");
187
+ }
188
+
189
+ getEnvironment(workspaceRoot, canReadRelatedFiles) {
190
+ return canReadRelatedFiles
191
+ ? `${getPhrase("workspaceRoot", this.lang)} ${workspaceRoot}\n${getPhrase("besidesDiff", this.lang)}`
192
+ : getPhrase("noWorkspace", this.lang);
193
+ }
194
+
195
+ build(diffPayload) {
196
+ const workspaceRoot = (this.backend.kind === "git" ? this.targetInfo.repoRootPath : this.targetInfo.workingCopyPath) || this.config.baseDir;
197
+ const canReadRelatedFiles = this.backend.kind === "git" || Boolean(this.targetInfo.workingCopyPath);
198
+ const langName = getLanguageDisplayName(this.lang);
199
+
200
+ const diffNote = diffPayload.wasTruncated
201
+ ? getPhrase("diffNoteTruncated", this.lang, {
202
+ originalLineCount: diffPayload.originalLineCount,
203
+ originalCharCount: diffPayload.originalCharCount,
204
+ outputLineCount: diffPayload.outputLineCount,
205
+ outputCharCount: diffPayload.outputCharCount
206
+ })
207
+ : getPhrase("diffNoteFull", this.lang, {
208
+ originalLineCount: diffPayload.originalLineCount,
209
+ originalCharCount: diffPayload.originalCharCount
210
+ });
211
+
212
+ const sections = [
213
+ "## Instructions",
214
+ this.getLangInstruction(),
215
+ this.data.systemRole,
216
+ this.data.outputFormat,
217
+ this.data.constraints,
218
+ this.config.prompt ? `### Additional User Instructions:\n${this.config.prompt}` : null,
219
+ "## Change Context",
220
+ this.getMetadata(),
221
+ diffNote,
222
+ "## Environment",
223
+ this.getEnvironment(workspaceRoot, canReadRelatedFiles),
224
+ getPhrase("reviewFromDiff", this.lang),
225
+ getPhrase("fileRefs", this.lang),
226
+ "## Final Rule",
227
+ getPhrase("langRule", this.lang, { langName }),
228
+ getPhrase("outputDirective", this.lang)
229
+ ];
230
+
231
+ let prompt = sections.filter(Boolean).join("\n\n");
232
+
233
+ // Reviewer-specific adjustments
234
+ if (this.config.reviewer === "copilot") {
235
+ // For Copilot, we reinforce the tool isolation even more
236
+ prompt += "\n\nCRITICAL FOR COPILOT: Do not use any MCP tools. Do not use @workspace. Use only the provided diff and the files you can read via provided paths.";
237
+ }
238
+
239
+ return prompt;
240
+ }
241
+ }
242
+
243
+ export function buildReviewPrompt(config, backend, targetInfo, details, diffPayload) {
244
+ const builder = new PromptBuilder(config, backend, targetInfo, details);
245
+ return builder.build(diffPayload);
246
+ }
247
+
248
+ export { LOCALIZED_DATA, getPhrase, getLanguageDisplayName };
@@ -1,222 +1,13 @@
1
1
  import { formatDate } from "./utils.js";
2
-
3
- const LOCALIZED_DATA = {
4
- en: {
5
- displayName: "English",
6
- coreInstruction: `You are a Senior Software Engineer and Code Review Expert with over 10 years of experience. Your task is to perform a rigorous and high-quality review of the following code changes.
7
- Your goals are to:
8
- 1. Identify bugs, logical flaws, and potential regression risks.
9
- 2. Spot performance bottlenecks, memory leaks, or unnecessary computations.
10
- 3. Check for security vulnerabilities (e.g., injection, authorization issues, sensitive data leaks).
11
- 4. Evaluate maintainability, readability, and adherence to best practices.
12
- 5. Verify coverage of necessary unit tests and boundary conditions.
13
-
14
- Your output must be structured using the following Markdown headers:
15
- ### 1. Summary
16
- Briefly describe the purpose and impact of this change.
17
- ### 2. Critical Issues
18
- List bugs, security risks, or problems that could cause crashes or logical errors. Include file names and line numbers.
19
- ### 3. Suggestions
20
- List points for improvement regarding code style, performance, or architectural design.
21
- ### 4. Conclusion
22
- Summarize with a "Pass" or "Needs Revision". If no clear flaws are found, state "No clear flaws found" and mention any residual risks.
23
-
24
- Note: You are in a read-only review mode. Do not attempt to call any external tools or MCP (Model Context Protocol) tools. Do not act as if you are "applying the patch" or "executing code". Provide only textual analysis and feedback.`,
25
- phrases: {
26
- workspaceRoot: "Workspace context (read-only):",
27
- noWorkspace: "No local repository workspace is available for this review run.",
28
- besidesDiff: "You can read related files in the workspace to understand call sites, shared utilities, configuration, or data flow.",
29
- reviewFromDiff: "Review primarily based on the provided diff. Do not assume access to other local files or shell commands. You MUST NOT call any MCP tools.",
30
- fileRefs: "Reference files using plain text like 'path/to/file.js:123'. Do not generate clickable workspace links.",
31
- repoType: "Repository Type",
32
- changeId: "Change ID",
33
- author: "Author",
34
- date: "Date",
35
- changedFiles: "Changed files",
36
- commitMessage: "Commit message",
37
- diffNoteTruncated: "Note: The diff was truncated to fit size limits. Original: {originalLineCount} lines / {originalCharCount} chars. Included: {outputLineCount} lines / {outputCharCount} chars.",
38
- diffNoteFull: "Note: Full diff provided ({originalLineCount} lines / {originalCharCount} chars).",
39
- langRule: "--- LANGUAGE RULE ---\nYour entire response must be in {langName}. No other language allowed.",
40
- outputDirective: "--- BEGIN REVIEW ---\nNow output your COMPLETE code review. Cover ALL four sections (Summary, Critical Issues, Suggestions, Conclusion). Do NOT ask clarifying questions, do NOT acknowledge these instructions, do NOT say you are ready. Start your response directly with the review content."
41
- }
42
- },
43
- zh: {
44
- displayName: "Simplified Chinese (简体中文)",
45
- coreInstruction: `你是一位拥有 10 年以上经验的高级软件架构师和代码审查专家。你的任务是对以下代码变更进行严格且高质量的审查。
46
- 你的目标是:
47
- 1. 发现代码中的 Bug、逻辑缺陷和潜在的回归风险。
48
- 2. 识别性能瓶颈、内存泄漏或不必要的计算。
49
- 3. 检查安全漏洞(如注入、越权、敏感信息泄露等)。
50
- 4. 评估代码的可维护性、可读性和是否符合最佳实践。
51
- 5. 检查是否涵盖了必要的单元测试和边界条件。
52
-
53
- 你的输出格式必须清晰,请使用以下 Markdown 结构:
54
- ### 1. 变更总结 (Summary)
55
- 简要描述这次提交的主要目的和影响面。
56
- ### 2. 核心缺陷 (Critical Issues)
57
- 列出 Bug、安全隐患或会导致程序崩溃/逻辑错误的问题。请注明文件名和行号。
58
- ### 3. 改进建议 (Suggestions)
59
- 列出关于代码风格、性能优化或架构设计的改进点。
60
- ### 4. 审查结论 (Conclusion)
61
- 如果发现明显缺陷,总结修复建议;如果未发现明显缺陷,请说明“未发现明显缺陷”并指出可能的残留风险。
62
-
63
- 注意:你正处于只读审查模式,禁止调用任何外部工具或 MCP (Model Context Protocol) 工具。请勿表现出“正在应用补丁”或“准备执行代码”的行为。只需提供文字审查分析。`,
64
- phrases: {
65
- workspaceRoot: "只读工作区上下文:",
66
- noWorkspace: "此审查运行没有可用的本地仓库工作区。",
67
- besidesDiff: "你可以阅读工作区中的其他文件以了解调用点、工具类、配置或数据流。",
68
- reviewFromDiff: "主要根据提供的 Diff 进行审查。不要假设可以访问其他文件或执行 Shell 命令。你绝不能调用任何 MCP 工具。",
69
- fileRefs: "使用纯文本引用文件,如 'path/to/file.js:123'。不要生成可点击的链接。",
70
- repoType: "仓库类型",
71
- changeId: "变更 ID",
72
- author: "作者",
73
- date: "日期",
74
- changedFiles: "已变更文件",
75
- commitMessage: "提交信息",
76
- diffNoteTruncated: "注意:Diff 已截断。原始:{originalLineCount} 行 / {originalCharCount} 字符。包含:{outputLineCount} 行 / {outputCharCount} 字符。",
77
- diffNoteFull: "注意:包含完整 Diff ({originalLineCount} 行 / {originalCharCount} 字符)。",
78
- langRule: "--- 语言规则 ---\n你必须完全使用 {langName} 进行回复。不得使用其他语言进行解释或总结。",
79
- outputDirective: "--- 开始输出审查结果 ---\n请立即输出完整的代码审查结果,必须包含全部四个章节(变更总结、核心缺陷、改进建议、审查结论)。不要提问,不要确认收到指令,不要说准备好了,直接以审查内容开始输出。"
80
- }
81
- },
82
- "zh-tw": {
83
- displayName: "Traditional Chinese (繁體中文)",
84
- coreInstruction: `你是一位擁有 10 年以上經驗的高級軟體架構師和代碼審查專家。你的任務是對以下代碼變更進行嚴格且高質量的審查。
85
- 你的目標是:
86
- 1. 發現代碼中的 Bug、邏輯缺陷和潛在的回歸風險。
87
- 2. 識別性能瓶頸、記憶體洩漏或不必要的計算。
88
- 3. 檢查安全漏洞(如注入、越權、敏感信息洩露等)。
89
- 4. 評估代碼的可維護性、可讀性和是否符合最佳實踐。
90
- 5. 檢查是否涵蓋了必要的單元測試和邊界條件。
91
-
92
- 你的輸出格式必須清晰,請使用以下 Markdown 結構:
93
- ### 1. 變更總結 (Summary)
94
- 簡要描述這次提交的主要目的和影響面。
95
- ### 2. 核心缺陷 (Critical Issues)
96
- 列出 Bug、安全隱患或會導致程序崩潰/邏輯錯誤的問題。請註明檔案名和行號。
97
- ### 3. 改進建議 (Suggestions)
98
- 列出關於代碼風格、性能優化或架構設計的改進點。
99
- ### 4. 審查結論 (Conclusion)
100
- 如果發現明顯缺陷,總結修復建議;如果未發現明顯缺陷,請說明「未發現明顯缺陷」並指出可能的殘留風險。
101
-
102
- 注意:你正處於唯讀審查模式,禁止調用任何外部工具或 MCP (Model Context Protocol) 工具。請勿表現出「正在應用補丁」或「準備執行代碼」的行為。只需提供文字審查分析。`,
103
- phrases: {
104
- workspaceRoot: "唯讀工作區上下文:",
105
- noWorkspace: "此審查運行沒有可用的本地倉庫工作區。",
106
- besidesDiff: "你可以閱讀工作區中的其他文件以了解調用點、工具類、配置或資料流。",
107
- reviewFromDiff: "主要根據提供的 Diff 進行審查。不要假設可以訪問其他文件或執行 Shell 命令。你絕不能調用任何 MCP 工具。",
108
- fileRefs: "使用純文本引用文件,如 'path/to/file.js:123'。不要生成可點擊的連結。",
109
- repoType: "倉庫類型",
110
- changeId: "變更 ID",
111
- author: "作者",
112
- date: "日期",
113
- changedFiles: "已變更文件",
114
- commitMessage: "提交信息",
115
- diffNoteTruncated: "注意:Diff 已截斷。原始:{originalLineCount} 行 / {originalCharCount} 字符。包含:{outputLineCount} 行 / {outputCharCount} 字符。",
116
- diffNoteFull: "注意:包含完整 Diff ({originalLineCount} 行 / {originalCharCount} 字符)。",
117
- langRule: "--- 語言規則 ---\n你必須完全使用 {langName} 進行回覆。不得使用其他語言進行解釋或總結。",
118
- outputDirective: "--- 開始輸出審查結果 ---\n請立即輸出完整的代碼審查結果,必須包含全部四個章節(變更總結、核心缺陷、改進建議、審查結論)。不要提問,不要確認收到指令,不要說準備好了,直接以審查內容開始輸出。"
119
- }
120
- }
121
- };
122
-
123
- function getLangKey(lang) {
124
- const lowArg = (lang || "en").toLowerCase();
125
- if (lowArg.startsWith("zh")) {
126
- return (lowArg === "zh-tw" || lowArg === "zh-hk") ? "zh-tw" : "zh";
127
- }
128
- return "en";
129
- }
130
-
131
- export function getCoreReviewInstruction(lang) {
132
- return LOCALIZED_DATA[getLangKey(lang)].coreInstruction;
133
- }
2
+ import { PromptBuilder, getPhrase, getLanguageDisplayName } from "./prompts.js";
134
3
 
135
4
  export function getReviewWorkspaceRoot(config, backend, targetInfo) {
136
5
  return (backend.kind === "git" ? targetInfo.repoRootPath : targetInfo.workingCopyPath) || config.baseDir;
137
6
  }
138
7
 
139
- function getLanguageDisplayName(lang) {
140
- if (!lang) return "English";
141
- const low = lang.toLowerCase();
142
- const data = LOCALIZED_DATA[getLangKey(lang)];
143
- if (data && low.startsWith("zh")) return data.displayName;
144
-
145
- const extras = {
146
- jp: "Japanese (日本語)", ja: "Japanese (日本語)",
147
- kr: "Korean (한국어)", ko: "Korean (한국어)",
148
- fr: "French (Français)", de: "German (Deutsch)",
149
- es: "Spanish (Español)", it: "Italian (Italiano)",
150
- ru: "Russian (Русский)"
151
- };
152
- return extras[low] || extras[low.split("-")[0]] || lang;
153
- }
154
-
155
- function getPhrase(key, lang, placeholders = {}) {
156
- let phrase = LOCALIZED_DATA[getLangKey(lang)].phrases[key] || LOCALIZED_DATA.en.phrases[key];
157
- for (const [k, v] of Object.entries(placeholders)) {
158
- phrase = phrase.replace(`{${k}}`, v);
159
- }
160
- return phrase;
161
- }
162
-
163
8
  export function buildPrompt(config, backend, targetInfo, details, reviewDiffPayload) {
164
- const fileList = details.changedPaths.map((item) => `${item.action} ${item.relativePath}`).join("\n");
165
- const workspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
166
- const canReadRelatedFiles = backend.kind === "git" || Boolean(targetInfo.workingCopyPath);
167
-
168
- const lang = config.resolvedLang || "en";
169
- const langName = getLanguageDisplayName(lang);
170
- const lowLang = lang.toLowerCase();
171
-
172
- let langInstruction = `CRITICAL: YOUR ENTIRE RESPONSE MUST BE IN ${langName.toUpperCase()}.`;
173
- if (lowLang.startsWith("zh")) {
174
- if (lowLang === "zh-tw" || lowLang === "zh-hk") {
175
- langInstruction += "\n請務必完全使用繁體中文進行回覆。";
176
- } else {
177
- langInstruction += "\n请务必完全使用简体中文进行回复。";
178
- }
179
- }
180
-
181
- const metadata = [
182
- `${getPhrase("repoType", lang)}: ${backend.displayName}`,
183
- `${getPhrase("changeId", lang)}: ${details.displayId}`,
184
- `${getPhrase("author", lang)}: ${details.author}`,
185
- `${getPhrase("date", lang)}: ${formatDate(details.date) || "unknown"}`,
186
- `${getPhrase("changedFiles", lang)}:\n${fileList || "(none)"}`,
187
- `${getPhrase("commitMessage", lang)}:\n${details.message || "(empty)"}`
188
- ].join("\n");
189
-
190
- const diffNote = reviewDiffPayload.wasTruncated
191
- ? getPhrase("diffNoteTruncated", lang, {
192
- originalLineCount: reviewDiffPayload.originalLineCount,
193
- originalCharCount: reviewDiffPayload.originalCharCount,
194
- outputLineCount: reviewDiffPayload.outputLineCount,
195
- outputCharCount: reviewDiffPayload.outputCharCount
196
- })
197
- : getPhrase("diffNoteFull", lang, {
198
- originalLineCount: reviewDiffPayload.originalLineCount,
199
- originalCharCount: reviewDiffPayload.originalCharCount
200
- });
201
-
202
- const sections = [
203
- langInstruction,
204
- getCoreReviewInstruction(lang),
205
- config.prompt ? `### Additional User Instructions:\n${config.prompt}` : null,
206
- "### Change Context:",
207
- metadata,
208
- diffNote,
209
- "### Environment:",
210
- canReadRelatedFiles
211
- ? `${getPhrase("workspaceRoot", lang)} ${workspaceRoot}\n${getPhrase("besidesDiff", lang)}`
212
- : getPhrase("noWorkspace", lang),
213
- getPhrase("reviewFromDiff", lang),
214
- getPhrase("fileRefs", lang),
215
- getPhrase("langRule", lang, { langName }),
216
- getPhrase("outputDirective", lang)
217
- ];
218
-
219
- return sections.filter(Boolean).join("\n\n");
9
+ const builder = new PromptBuilder(config, backend, targetInfo, details);
10
+ return builder.build(reviewDiffPayload);
220
11
  }
221
12
 
222
13
  export function formatTokenUsage(tokenUsage) {
@@ -262,14 +53,15 @@ export function shouldWriteFormat(config, format) {
262
53
 
263
54
  export function buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
264
55
  const stderrText = reviewerResult.stderr?.trim();
56
+ const lang = config.resolvedLang || "en";
265
57
  const lines = [
266
58
  `# ${backend.displayName} Review Report: ${details.displayId}`,
267
59
  "",
268
- `- Repository Type: \`${backend.displayName}\``,
60
+ `- ${getPhrase("repoType", lang)}: \`${backend.displayName}\``,
269
61
  `- Target: \`${targetInfo.targetDisplay || config.target}\``,
270
- `- Change ID: \`${details.displayId}\``,
271
- `- Author: \`${details.author}\``,
272
- `- Commit Date: \`${formatDate(details.date)}\``,
62
+ `- ${getPhrase("changeId", lang)}: \`${details.displayId}\``,
63
+ `- ${getPhrase("author", lang)}: \`${details.author}\``,
64
+ `- ${getPhrase("date", lang)}: \`${formatDate(details.date)}\``,
273
65
  `- Generated At: \`${formatDate(new Date())}\``,
274
66
  `- Reviewer: \`${reviewer.displayName}\``,
275
67
  `- Reviewer Exit Code: \`${reviewerResult.code}\``,
@@ -279,11 +71,11 @@ export function buildReport(config, backend, targetInfo, details, diffPayloads,
279
71
  "",
280
72
  formatTokenUsage(tokenUsage),
281
73
  "",
282
- "## Changed Files",
74
+ `## ${getPhrase("changedFiles", lang)}`,
283
75
  "",
284
76
  formatChangedPaths(details.changedPaths),
285
77
  "",
286
- "## Commit Message",
78
+ `## ${getPhrase("commitMessage", lang)}`,
287
79
  "",
288
80
  details.message ? "```text\n" + details.message + "\n```" : "_Empty_",
289
81
  "",
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { ProgressDisplay } from "./progress-ui.js";
2
+ import { createProgressReporter } from "./progress-ui.js";
3
3
  import { resolveRepositoryContext } from "./vcs-client.js";
4
4
  import { logger } from "./logger.js";
5
5
  import {
@@ -172,10 +172,8 @@ export async function runReviewCycle(config) {
172
172
  }
173
173
 
174
174
  logger.info(`Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`);
175
- const progressDisplay = new ProgressDisplay();
176
- logger.setProgressDisplay(progressDisplay);
177
- const progress = progressDisplay.createItem(`${backend.displayName} ${backend.changeName} batch`);
178
- progress.start("0/" + changeIdsToReview.length + " completed");
175
+ const progress = createProgressReporter(`${backend.displayName} ${backend.changeName} batch`);
176
+ progress.update(0, `0/${changeIdsToReview.length} completed`);
179
177
 
180
178
  for (const [index, changeId] of changeIdsToReview.entries()) {
181
179
  logger.debug(`Starting review for ${backend.formatChangeId(changeId)}.`);
@@ -187,11 +185,13 @@ export async function runReviewCycle(config) {
187
185
  };
188
186
 
189
187
  try {
190
- await reviewChange(config, backend, targetInfo, changeId, { update: syncOverallProgress, log: (message) => progress.log(message) });
188
+ await reviewChange(config, backend, targetInfo, changeId, { update: syncOverallProgress });
191
189
  updateOverallProgress(progress, index + 1, changeIdsToReview.length, 0, `finished ${displayId}`);
192
190
  } catch (error) {
193
- progress.fail(`failed at ${displayId} (${index}/${changeIdsToReview.length} completed)`);
191
+ progress.finish("fail", `failed at ${displayId} (${index}/${changeIdsToReview.length} completed)`);
194
192
  throw error;
195
193
  }
196
194
  }
195
+
196
+ progress.finish("done", `${backend.displayName} ${backend.changeName} batch complete`);
197
197
  }