@unifan/pi-review-zh 1.0.3 → 1.0.4
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/agents/perf-review.md +67 -0
- package/index.ts +16 -7
- package/package.json +1 -1
- package/src/cli-args.ts +10 -19
- package/src/directive.ts +26 -15
- package/src/review-run.ts +11 -5
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: perf-review
|
|
3
|
+
package: pi-review
|
|
4
|
+
description: 性能与基准测试审查专家:专项排查内存泄漏、高频GC分配、算法复杂度退化、锁竞争,并主动探测执行基准测试(Benchmark)。
|
|
5
|
+
tools: read, grep, bash
|
|
6
|
+
systemPromptMode: replace
|
|
7
|
+
inheritProjectContext: false
|
|
8
|
+
inheritSkills: false
|
|
9
|
+
---
|
|
10
|
+
你是**性能与基准测试审查专家(Performance & Benchmark Reviewer)**。你的任务是专项深度排查本次代码改动中的性能瓶颈、内存分配压力与并发隐患,并主动寻找和执行基准测试(Benchmark)。
|
|
11
|
+
|
|
12
|
+
## 核心审查维度(跨语言与全栈)
|
|
13
|
+
|
|
14
|
+
1. **内存与 GC 分配(Memory & Allocation Pressure)**:
|
|
15
|
+
- **高频循环堆分配**:检查循环(Update / Tick / 帧循环 / 高频事件处理器)中是否存在 `new` 对象、隐式闭包分配、Lambda 捕获外部变量、LINQ、装箱/拆箱。
|
|
16
|
+
- **集合与缓冲区**:集合未预设初始容量导致的频繁扩容与重分配、频繁的数组拷贝、低效字符串拼接(应使用 StringBuilder / Span)。
|
|
17
|
+
- **内存泄漏**:未注销的事件监听(Event)、未释放的非托管资源/句柄/流/GPU 纹理、长生命周期单例持有大对象强引用。
|
|
18
|
+
|
|
19
|
+
2. **CPU 与算法复杂度(CPU & Complexity)**:
|
|
20
|
+
- **复杂度退化**:高频热点中 $O(N^2)$ 或更高复杂度的嵌套循环与线性查找(应替换为 Dictionary/HashMap/HashSet 索引)。
|
|
21
|
+
- **昂贵反射与深拷贝**:热点路径中频繁反射(Reflection)、动态解析、低效序列化。
|
|
22
|
+
- **主线程阻塞**:UI 线程或渲染主线程中执行同步大文件读取或阻塞性 I/O。
|
|
23
|
+
|
|
24
|
+
3. **并发与锁竞争(Concurrency & Contention)**:
|
|
25
|
+
- 锁粒度过大、在锁内执行耗时 I/O、无界队列、死锁与假共享(False Sharing)。
|
|
26
|
+
|
|
27
|
+
4. **基准测试探索与执行(Benchmark Runner)**:
|
|
28
|
+
- **自动探测**:主动检查工作区是否存在基准测试套件(如 `BenchmarkDotNet`、`go test -bench`、`cargo bench`、`pytest-benchmark`、`vitest bench` 等)。
|
|
29
|
+
- **有条件执行**:若存在现成的基准测试且安全可运行,使用 `bash` 运行一次并提取关键指标(单次耗时、单次分配字节数)。
|
|
30
|
+
- **基准代码生成**:若当前改动属于核心热点但缺乏 Benchmark,在报告中给出一段针对该场景的标准基准测试代码建议。
|
|
31
|
+
|
|
32
|
+
## 执行步骤
|
|
33
|
+
1. 读取任务中的 diff 文件与 manifest 清单。
|
|
34
|
+
2. 重点分析改动中涉及性能与资源管理的关键代码,必要时读取上下文文件。
|
|
35
|
+
3. 检查并运行基准测试(如适用)。
|
|
36
|
+
4. 输出格式规范的 Markdown 报告并在最末尾附带 JSON 块。
|
|
37
|
+
|
|
38
|
+
## 输出格式(必须使用中文撰写总结与描述)
|
|
39
|
+
|
|
40
|
+
请以 Markdown 格式输出最终回复:
|
|
41
|
+
|
|
42
|
+
## Summary
|
|
43
|
+
一句话中文性能总结(评定本次修改的性能影响:优秀 / 无明显影响 / 存在性能隐患 / 严重性能退化)。
|
|
44
|
+
|
|
45
|
+
## Findings
|
|
46
|
+
每个发现一行:`- [SEVERITY|category|confidence] 文件路径:行号 — 中文问题描述、分配分析与优化建议`。无问题写 `No findings.`。
|
|
47
|
+
|
|
48
|
+
## Benchmark Analysis
|
|
49
|
+
说明工作区中基准测试探测结果与运行数据。若无基准测试,可针对改动热点提供一段简短的 Benchmark 编写建议。
|
|
50
|
+
|
|
51
|
+
## Coverage
|
|
52
|
+
- Files checked: 检查的文件
|
|
53
|
+
- Commands run: 执行的基准测试或检查命令
|
|
54
|
+
- Limitations: 局限说明
|
|
55
|
+
|
|
56
|
+
然后在最末尾严格输出一个被 ```json 代码块包裹的 JSON:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"status": "ok",
|
|
61
|
+
"issues": [
|
|
62
|
+
{ "file": "src/Manager.cs", "line": 42, "category": "perf", "severity": "major", "confidence": 9, "evidence": "Update 循环中存在 new GC 堆分配,且高频调用 LINQ 查询", "fingerprint": "src/Manager.cs:42:perf:a1b2c3" }
|
|
63
|
+
],
|
|
64
|
+
"summary": "中文一句话性能总结",
|
|
65
|
+
"coverage": { "filesChecked": ["src/Manager.cs"], "commandsRun": [], "limitations": [] }
|
|
66
|
+
}
|
|
67
|
+
```
|
package/index.ts
CHANGED
|
@@ -25,18 +25,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
25
25
|
registerReviewReportTool(pi);
|
|
26
26
|
registerPiReviewRenderer(pi);
|
|
27
27
|
pi.registerCommand("review", {
|
|
28
|
-
description: "启动 AI 并发代码审查 (支持多专家子代理 + 门禁总裁判系统)。--lite =
|
|
28
|
+
description: "启动 AI 并发代码审查 (支持多专家子代理 + 门禁总裁判系统)。--lite = 极速单专家审查,--perf = 性能与基准测试审查。",
|
|
29
29
|
getArgumentCompletions: (prefix: string) => {
|
|
30
|
+
const options = [
|
|
31
|
+
{ value: "--lite", label: "--lite", description: "极速单专家审查 (无门禁,低延迟省 Token)" },
|
|
32
|
+
{ value: "--perf", label: "--perf", description: "专项性能与基准测试审查 (GC/内存分配/CPU/Benchmark)" },
|
|
33
|
+
{ value: "--gate-model", label: "--gate-model", description: "指定当前审查的门禁裁判模型" },
|
|
34
|
+
];
|
|
30
35
|
const trimmed = prefix.trimStart();
|
|
36
|
+
if (!trimmed) return options;
|
|
31
37
|
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
32
38
|
const last = tokens[tokens.length - 1] ?? "";
|
|
33
39
|
if (last.startsWith("--")) {
|
|
34
|
-
return
|
|
35
|
-
{ value: "--lite", label: "--lite", description: "极速单专家审查 (无门禁,低延迟省 Token)" },
|
|
36
|
-
{ value: "--gate-model", label: "--gate-model", description: "指定当前审查的门禁裁判模型" },
|
|
37
|
-
].filter((o) => o.value.startsWith(last));
|
|
40
|
+
return options.filter((o) => o.value.startsWith(last));
|
|
38
41
|
}
|
|
39
|
-
return
|
|
42
|
+
return options;
|
|
40
43
|
},
|
|
41
44
|
handler: async (args, ctx) => {
|
|
42
45
|
const notify = (msg: string, level: "info" | "warning" | "error" = "info") => {
|
|
@@ -61,7 +64,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
61
64
|
return;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
const prepared = await prepareRun({
|
|
67
|
+
const prepared = await prepareRun({
|
|
68
|
+
cwd: ctx.cwd,
|
|
69
|
+
input: parsed.input,
|
|
70
|
+
lite: parsed.lite,
|
|
71
|
+
perf: parsed.perf,
|
|
72
|
+
gateModel: parsed.gateModel,
|
|
73
|
+
});
|
|
65
74
|
if (!prepared) {
|
|
66
75
|
notify("没有检测到需要审查的内容 (未找到修改、PR 或非 Git 仓库)。", "info");
|
|
67
76
|
return;
|
package/package.json
CHANGED
package/src/cli-args.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse `/review` command arguments.
|
|
3
|
-
*
|
|
4
|
-
* Surface is intentionally minimal:
|
|
5
|
-
* - `--lite` fast single-agent review (no fan-out, no gate)
|
|
6
|
-
* - `--no-spawn` hidden dry-run (prints the resolved plan, no subprocess)
|
|
7
|
-
* - trailing freeform text → user review request (focus / requirements /
|
|
8
|
-
* PR url / context), injected into reviewers and gate via target.userContext
|
|
9
|
-
* (see src/prep.ts and src/git-input.ts)
|
|
10
|
-
*
|
|
11
|
-
* Removed flags (--threshold / --reviewer / --score-per-issue / --diff) are
|
|
12
|
-
* accepted-but-ignored for graceful degradation of old invocations: a valued
|
|
13
|
-
* legacy flag also consumes its next token so its value does not leak into
|
|
14
|
-
* `input`. Those capabilities now live in config.json (`/review-config`).
|
|
15
|
-
* `--gate-model <id>` remains an active per-run override.
|
|
16
3
|
*/
|
|
17
4
|
|
|
18
5
|
export interface ParsedReviewArgs {
|
|
@@ -22,11 +9,12 @@ export interface ParsedReviewArgs {
|
|
|
22
9
|
noSpawn: boolean;
|
|
23
10
|
/** Single-agent fast mode: one reviewer, no gate. */
|
|
24
11
|
lite: boolean;
|
|
12
|
+
/** Dedicated performance review mode: perf-reviewer with benchmark capabilities. */
|
|
13
|
+
perf: boolean;
|
|
25
14
|
/** Override the gate model for this run (otherwise config.gate.model). */
|
|
26
15
|
gateModel?: string;
|
|
27
16
|
}
|
|
28
17
|
|
|
29
|
-
/** Removed valued flags — silently skip flag + value to keep input clean. */
|
|
30
18
|
const LEGACY_VALUED_FLAGS = new Set([
|
|
31
19
|
"--threshold",
|
|
32
20
|
"--reviewer",
|
|
@@ -36,13 +24,13 @@ const LEGACY_VALUED_FLAGS = new Set([
|
|
|
36
24
|
|
|
37
25
|
export function parseReviewArgs(raw: string): ParsedReviewArgs {
|
|
38
26
|
const tokens = tokenize(raw);
|
|
39
|
-
const result: ParsedReviewArgs = { noSpawn: false, lite: false };
|
|
27
|
+
const result: ParsedReviewArgs = { noSpawn: false, lite: false, perf: false };
|
|
40
28
|
const inputParts: string[] = [];
|
|
41
29
|
|
|
42
30
|
for (let i = 0; i < tokens.length; i++) {
|
|
43
31
|
const t = tokens[i];
|
|
44
32
|
if (LEGACY_VALUED_FLAGS.has(t)) {
|
|
45
|
-
i++;
|
|
33
|
+
i++;
|
|
46
34
|
continue;
|
|
47
35
|
}
|
|
48
36
|
if (t === "--no-spawn") {
|
|
@@ -53,13 +41,17 @@ export function parseReviewArgs(raw: string): ParsedReviewArgs {
|
|
|
53
41
|
result.lite = true;
|
|
54
42
|
continue;
|
|
55
43
|
}
|
|
44
|
+
if (t === "--perf" || t === "--performance") {
|
|
45
|
+
result.perf = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
56
48
|
if (t === "--gate-model") {
|
|
57
49
|
const id = tokens[++i];
|
|
58
50
|
if (id) result.gateModel = id;
|
|
59
51
|
continue;
|
|
60
52
|
}
|
|
61
53
|
if (t.startsWith("-")) {
|
|
62
|
-
continue;
|
|
54
|
+
continue;
|
|
63
55
|
}
|
|
64
56
|
inputParts.push(t);
|
|
65
57
|
}
|
|
@@ -72,7 +64,6 @@ export function parseReviewArgs(raw: string): ParsedReviewArgs {
|
|
|
72
64
|
return result;
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
/** Split on whitespace preserving quoted segments. */
|
|
76
67
|
function tokenize(raw: string): string[] {
|
|
77
68
|
const out: string[] = [];
|
|
78
69
|
let cur = "";
|
|
@@ -102,4 +93,4 @@ function tokenize(raw: string): string[] {
|
|
|
102
93
|
}
|
|
103
94
|
if (cur.length > 0) out.push(cur);
|
|
104
95
|
return out;
|
|
105
|
-
}
|
|
96
|
+
}
|
package/src/directive.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface ReviewDirectiveInput {
|
|
|
22
22
|
threshold: number;
|
|
23
23
|
verdictPolicy?: "strict" | "legacy";
|
|
24
24
|
lite: boolean;
|
|
25
|
+
perf?: boolean;
|
|
25
26
|
gateEnabled?: boolean;
|
|
26
27
|
cwd: string;
|
|
27
28
|
workspacePath: string;
|
|
@@ -32,21 +33,24 @@ export interface ReviewDirectiveInput {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function buildReviewDirective(input: ReviewDirectiveInput): string {
|
|
35
|
-
const { target, reviewers, gateModel, gateThinking, threshold, lite, cwd, workspacePath, manifestPath, diffPath, workflowPath } = input;
|
|
36
|
+
const { target, reviewers, gateModel, gateThinking, threshold, lite, perf, cwd, workspacePath, manifestPath, diffPath, workflowPath } = input;
|
|
37
|
+
const isSingle = lite || Boolean(perf);
|
|
36
38
|
const policy = input.verdictPolicy ?? "strict";
|
|
37
|
-
const gateOn = !
|
|
39
|
+
const gateOn = !isSingle && input.gateEnabled !== false;
|
|
38
40
|
const budgets = input.budgets ?? resolveLeanBudgets();
|
|
39
41
|
const gateModelWithThinking = withThinkingSuffix(gateModel, gateThinking);
|
|
40
42
|
const blocks: string[] = [];
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
const modeLabel = perf ? " (性能与基准测试专属模式)" : lite ? " (极速单兵模式)" : "";
|
|
45
|
+
|
|
46
|
+
blocks.push(`# 代码审查流程${modeLabel}`);
|
|
43
47
|
blocks.push("");
|
|
44
48
|
if (target.userContext?.trim()) {
|
|
45
49
|
blocks.push(`**用户指令/侧重点:** ${target.userContext.trim()}`);
|
|
46
50
|
blocks.push("");
|
|
47
51
|
}
|
|
48
52
|
blocks.push(
|
|
49
|
-
`请对本次代码改动 (${target.label}) 进行审查。插件已准备好目标工作区、diff 文件及运行清单。你需要执行一次 workflowScript 启动 ${reviewers.length}
|
|
53
|
+
`请对本次代码改动 (${target.label}) 进行审查。插件已准备好目标工作区、diff 文件及运行清单。你需要执行一次 workflowScript 启动 ${reviewers.length} 个审查专家${modeLabel}${gateOn ? " + 门禁裁判长" : ""},随后调用 \`pi_review_report\` 工具完成报告渲染与归档。切勿在聊天中直接重复书写未加工的原始问题细节。`,
|
|
50
54
|
);
|
|
51
55
|
blocks.push("");
|
|
52
56
|
blocks.push("## 硬性规则(严禁违反)");
|
|
@@ -54,8 +58,8 @@ export function buildReviewDirective(input: ReviewDirectiveInput): string {
|
|
|
54
58
|
blocks.push("- **语言要求**:所有面向用户的输出(包括工作流待办清单、状态汇报、问题总结与回复)**必须使用纯正中文**。");
|
|
55
59
|
blocks.push("- 在本次审查中**只能且必须调用一次** `subagent` 工具:即第 2 步的 workflowScript 调用。");
|
|
56
60
|
blocks.push(
|
|
57
|
-
|
|
58
|
-
? "- 第 2 步必须是**单次** `subagent({ workflowScript, async:false, ... })` 调用,通过 `runs.all([...])`
|
|
61
|
+
isSingle
|
|
62
|
+
? "- 第 2 步必须是**单次** `subagent({ workflowScript, async:false, ... })` 调用,通过 `runs.all([...])` 并发执行专家——严禁多次调用。"
|
|
59
63
|
: gateOn
|
|
60
64
|
? "- 第 2 步必须是**单次** `subagent({ workflowScript, async:false, ... })` 调用,通过 `runs.all([...])` 并发执行**所有**专家,并通过 `runs.run(\"gate\", ...)` 运行门禁裁判长——严禁每个专家单独调用一次,严禁串行多波次调用。"
|
|
61
65
|
: "- 第 2 步必须是**单次** `subagent({ workflowScript, async:false, ... })` 调用,通过 `runs.all([...])` 并发执行**所有**专家——严禁多波次单独调用。",
|
|
@@ -89,11 +93,13 @@ export function buildReviewDirective(input: ReviewDirectiveInput): string {
|
|
|
89
93
|
const todoSteps = [
|
|
90
94
|
`确认插件准备的运行清单可读: ${manifestPath}`,
|
|
91
95
|
`确认目标工作区可读: ${workspacePath}`,
|
|
92
|
-
|
|
93
|
-
? "执行 workflowScript:
|
|
94
|
-
:
|
|
95
|
-
?
|
|
96
|
-
:
|
|
96
|
+
perf
|
|
97
|
+
? "执行 workflowScript: 启动专项性能与基准测试审查专家 (单次 subagent 调用)"
|
|
98
|
+
: lite
|
|
99
|
+
? "执行 workflowScript: 启动单兵极速审查专家 (单次 subagent 调用)"
|
|
100
|
+
: gateOn
|
|
101
|
+
? `执行 workflowScript: 启动 ${reviewers.length} 个并发审查专家 + 门禁裁判长 (单次 subagent 调用)`
|
|
102
|
+
: `执行 workflowScript: 启动 ${reviewers.length} 个并发审查专家 (单次 subagent 调用)`,
|
|
97
103
|
"调用 `pi_review_report` 工具提交审查结果并生成中文报告",
|
|
98
104
|
];
|
|
99
105
|
for (const s of todoSteps) blocks.push(`- [ ] ${s}`);
|
|
@@ -124,7 +130,7 @@ export function buildReviewDirective(input: ReviewDirectiveInput): string {
|
|
|
124
130
|
gateThinking,
|
|
125
131
|
gateModel,
|
|
126
132
|
budgets,
|
|
127
|
-
lite,
|
|
133
|
+
lite: isSingle,
|
|
128
134
|
gateEnabled: gateOn,
|
|
129
135
|
threshold,
|
|
130
136
|
verdictPolicy: policy,
|
|
@@ -159,8 +165,8 @@ export function buildReviewDirective(input: ReviewDirectiveInput): string {
|
|
|
159
165
|
blocks.push("## 第 2 步 — 执行代码审查(仅调用一次 subagent workflowScript)");
|
|
160
166
|
blocks.push("");
|
|
161
167
|
blocks.push(
|
|
162
|
-
|
|
163
|
-
? "
|
|
168
|
+
isSingle
|
|
169
|
+
? "该脚本会启动审查专家,输出包含 JSON 块的 Markdown 报告。"
|
|
164
170
|
: gateOn
|
|
165
171
|
? "该脚本会并发启动各个审查专家,随后将报告汇总给门禁裁判长,裁判长输出裁决报告与 JSON 块。"
|
|
166
172
|
: "该脚本会并发启动各个审查专家,输出 Markdown 报告。",
|
|
@@ -229,7 +235,7 @@ export function buildWorkflowScript(input: {
|
|
|
229
235
|
const gateOn = !lite && gateEnabled;
|
|
230
236
|
|
|
231
237
|
const READ_ONLY_PREFIX =
|
|
232
|
-
"只读任务(READ-ONLY
|
|
238
|
+
"只读任务(READ-ONLY)——仅执行审查分析与基准测试。严禁修改源码文件。仅返回审查发现。所有分析总结、问题描述与建议必须使用纯正中文。";
|
|
233
239
|
|
|
234
240
|
const lines: string[] = [];
|
|
235
241
|
lines.push("");
|
|
@@ -266,6 +272,11 @@ export function buildWorkflowScript(input: {
|
|
|
266
272
|
"若 change-profile.docsOnly 为 true,返回跳过状态:SKIPPED: docs-only。否则优先从 diff 本身分析,最多只读取 3 个额外上下文文件。",
|
|
267
273
|
);
|
|
268
274
|
}
|
|
275
|
+
if (r.id === "perf-review") {
|
|
276
|
+
taskParts.push(
|
|
277
|
+
"重点排查循环内高频GC分配、算法复杂度、锁竞争、内存泄漏,并主动探测和执行工作区中的Benchmark基准测试。",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
269
280
|
if (userContext?.trim()) {
|
|
270
281
|
taskParts.push(`用户需求: ${userContext.trim()}`);
|
|
271
282
|
}
|
package/src/review-run.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface PrepareRunInput {
|
|
|
47
47
|
input?: string;
|
|
48
48
|
/** Support `--lite` single-agent mode. */
|
|
49
49
|
lite?: boolean;
|
|
50
|
+
/** Support `--perf` performance & benchmark mode. */
|
|
51
|
+
perf?: boolean;
|
|
50
52
|
/** Optional per-run gate model override. */
|
|
51
53
|
gateModel?: string;
|
|
52
54
|
/** Set false for dry-runs — pruning is a side effect a dry run must not have. */
|
|
@@ -143,9 +145,12 @@ export async function prepareRun(input: PrepareRunInput): Promise<PreparedRun |
|
|
|
143
145
|
rulePaths,
|
|
144
146
|
historyAvailable: workspaceResult.historyAvailable,
|
|
145
147
|
};
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
:
|
|
148
|
+
const isSingle = input.lite || input.perf;
|
|
149
|
+
const reviewers = input.perf
|
|
150
|
+
? [{ id: "perf-review", label: "Performance Review", enabled: true, model: "inherit" }]
|
|
151
|
+
: input.lite
|
|
152
|
+
? [{ id: "lite-review", label: "Lite Review", enabled: true, model: "inherit" }]
|
|
153
|
+
: reviewersForRouting(target, config, profile);
|
|
149
154
|
const skippedReasons = adaptiveSkips(profile);
|
|
150
155
|
// The report tool uses this to reject findings that did not come from this
|
|
151
156
|
// run's roster (stale-artifact contamination guard).
|
|
@@ -165,11 +170,12 @@ export async function prepareRun(input: PrepareRunInput): Promise<PreparedRun |
|
|
|
165
170
|
target,
|
|
166
171
|
reviewers,
|
|
167
172
|
gateModel,
|
|
168
|
-
gateThinking:
|
|
173
|
+
gateThinking: isSingle ? undefined : config.gate.thinking,
|
|
169
174
|
gateEnabled: config.gate.enabled,
|
|
170
175
|
threshold: config.gate.threshold,
|
|
171
176
|
verdictPolicy: config.gate.verdictPolicy,
|
|
172
177
|
lite: Boolean(input.lite),
|
|
178
|
+
perf: Boolean(input.perf),
|
|
173
179
|
cwd,
|
|
174
180
|
workspacePath,
|
|
175
181
|
manifestPath,
|
|
@@ -179,7 +185,7 @@ export async function prepareRun(input: PrepareRunInput): Promise<PreparedRun |
|
|
|
179
185
|
});
|
|
180
186
|
// Remember which lanes adaptive routing dropped so the report can surface
|
|
181
187
|
// them as coverage rather than letting users wonder where a reviewer went.
|
|
182
|
-
if (!
|
|
188
|
+
if (!isSingle && config.routing.mode === "adaptive" && skippedReasons.length > 0) {
|
|
183
189
|
const skippedByRouting: Array<{ id: string; reason: string }> = [];
|
|
184
190
|
for (const [id, reason] of skippedReasons) {
|
|
185
191
|
if (!reviewers.some((r) => r.id === id)) skippedByRouting.push({ id, reason });
|