@feng0531/dsh-code-review 0.57.0 → 0.58.0
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/CHANGELOG.md +5 -1
- package/CORE-ALIGNMENT-TASKS.md +1 -0
- package/RELEASE-NOTES.md +11 -7
- package/evaluation/README.md +24 -0
- package/evaluation/REVIEWBENCH-LOCAL.md +47 -0
- package/package.json +2 -2
- package/scripts/review-bench-adapter.mjs +125 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
|
|
4
4
|
|
|
5
|
+
## 0.58.0 — 本地评测入口可靠性整理
|
|
6
|
+
|
|
7
|
+
新增无需 Docker 的实验性 ReviewBench 本地入口与使用说明。要求显式本地仓库、模型与端点;复用严格 JSON 校验,拒绝截断和夹杂文本。未完成审查、不可映射的旧侧发现及超限结果明确失败并保存原报告,不伪装为空发现;同一行的不同问题不再误合并。mock 输出与可评分候选隔离,运行结果不覆盖。正常 DSH 工具行为、权限与预算不变;不代表真实模型质量已得到验证。
|
|
8
|
+
|
|
5
9
|
## 0.57.0 — 对齐结论与边界收尾
|
|
6
10
|
|
|
7
|
-
完成组合场景整体一致性验收与完整语义导航的源码对照收尾:范围内已对齐项清空「仍需补齐」;参考独有的会话存储、断点续跑、豁免与 token 预估记为有意不同;完整语义调用方、别名与跨目录实例上下文明确为需语义解析器的范围外。不改变审查行为或参数。
|
|
11
|
+
完成组合场景整体一致性验收与完整语义导航的源码对照收尾:范围内已对齐项清空「仍需补齐」;参考独有的会话存储、断点续跑、豁免与 token 预估记为有意不同;完整语义调用方、别名与跨目录实例上下文明确为需语义解析器的范围外。不改变审查行为或参数。449 项完整回归与四组 CI 通过;npm、Release 与原产物 108,451 字节一致,双哈希和清单通过。
|
|
8
12
|
|
|
9
13
|
## 0.56.0 — 依赖清单与配置文件专用指导
|
|
10
14
|
|
package/CORE-ALIGNMENT-TASKS.md
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
## 当前验证结论
|
|
34
34
|
|
|
35
35
|
- 最近完整回归:449 项通过。
|
|
36
|
+
- v0.57.0 已正式交付对齐结论与边界收尾。449 项回归与四组 CI 通过;npm、Release 与原包 108,451 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
|
|
36
37
|
- v0.56.0 已正式交付依赖清单与配置文件专用指导。449 项回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 108,081 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
|
|
37
38
|
- v0.55.0 已正式交付模板引擎与基础设施即代码专用指导。446 项回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 106,969 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
|
|
38
39
|
- v0.54.0 已正式交付对齐结论与边界收尾。443 项回归、实际宿主与四组 CI 通过;npm、Release 与原包 104,761 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
# v0.
|
|
1
|
+
# v0.58.0 发布说明
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 本地评测入口可靠性整理
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
新增无需 Docker 的实验性 ReviewBench 本地评测入口和说明,复用现有审查核心。配置、模型调用、结果映射分开测试;模型和端点显式指定,不改变 DSH 宿主配置。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- 复用严格 JSON 校验,拒绝截断响应和夹杂文字;HTTP 错误不输出正文。
|
|
8
|
+
- 未完成审查、无法表达为 head 行号的发现以及超限结果明确失败,保存完整原报告,不静默丢弃或假装成功。
|
|
9
|
+
- 同一行的不同问题保留,只去除完全相同的导出记录;旧结果不会被覆盖。
|
|
10
|
+
- mock 只生成冒烟文件,不生成可评分候选。
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
- 不使用版本或测试数量表示对齐百分比。
|
|
12
|
+
## 使用与边界
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
正常 DSH 插件工具、权限和预算保持不变。实验入口采用单次直审,不启用检索、复核或业务分组;其结果不能代表完整插件配置。尚未完成真实模型、真实基准 PR 或 judge 评分验证。
|
|
15
|
+
|
|
16
|
+
本地入口说明见 [本地评测指南](evaluation/REVIEWBENCH-LOCAL.md),插件日常使用见 [README](README.md)。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 评测说明
|
|
2
|
+
|
|
3
|
+
这个目录用于复现已知行为、检查模型输入和整理少量试跑观察,不进入 npm 安装包。
|
|
4
|
+
|
|
5
|
+
## 样本
|
|
6
|
+
|
|
7
|
+
- [资源生命周期](lifecycle/README.md):异常后是否释放资源,有缺陷输入和保留保护的对照。
|
|
8
|
+
- [请求乱序](stale-result/README.md):旧请求晚完成是否覆盖最新选择。
|
|
9
|
+
- [历史规则检查](history-rule-extension/README.md):本项目曾经错误接受不合法文件后缀的真实模块级复现。
|
|
10
|
+
|
|
11
|
+
样本测试通过表示“预期行为已被复现”,包括缺陷行为;不表示模型已经找到缺陷或样本已经修复。
|
|
12
|
+
|
|
13
|
+
## 离线运行
|
|
14
|
+
|
|
15
|
+
node --test test/evaluation-*.test.mjs
|
|
16
|
+
node evaluation/preview.mjs
|
|
17
|
+
|
|
18
|
+
第二条命令只读取固定合成输入,校验内容指纹并计算初始输入大小,不调用模型。输出不含源码或答案标签;它也不是宿主执行授权。
|
|
19
|
+
|
|
20
|
+
## 真实模型观察
|
|
21
|
+
|
|
22
|
+
已有六次小规模观察见 [试跑结果](PILOT-RESULTS.md)。其中一次证据复核未完成,其他结果也不能外推成质量指标。新评测仍后置,开始条件见 [运行条件](PILOT-GATE.md)。
|
|
23
|
+
|
|
24
|
+
运行真实评测时,只提供单组源码和必要契约,不发送答案说明、测试断言或其他组实现。保留原始输出,并由人工分别裁决候选生成和复核。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# ReviewBench 本地评测入口(实验)
|
|
2
|
+
|
|
3
|
+
无需 Docker,不提交公开排行榜,也不改变 DSH 宿主的模型配置。此入口直接调用审查核心,采用 **single-pass-direct**:一次审查,不启用检索、复核或业务分组。因此它的成绩不能代表 DSH 插件完整配置的质量。
|
|
4
|
+
|
|
5
|
+
## 前置条件
|
|
6
|
+
|
|
7
|
+
- Node.js ≥22、Git、已安装本仓库依赖。
|
|
8
|
+
- 本地已有基准 PR 的仓库及完整 base/head 提交对象。
|
|
9
|
+
- 正式运行显式设置 OpenAI-compatible Chat Completions 端点和模型。只支持该协议,不能直接用 Anthropic Messages 的密钥或端点。
|
|
10
|
+
- 模型密钥通过环境变量提供,不要粘贴到会话、命令历史或提交到仓库。
|
|
11
|
+
|
|
12
|
+
## PowerShell 示例
|
|
13
|
+
|
|
14
|
+
```powershell
|
|
15
|
+
$env:RB_REPO_DIR = 'G:\benchmark-repo'
|
|
16
|
+
$env:RB_BASE = '<40位base SHA>'
|
|
17
|
+
$env:RB_HEAD = '<40位head SHA>'
|
|
18
|
+
$env:RB_NWO = 'owner/repo'
|
|
19
|
+
$env:RB_PR_NUMBER = '123'
|
|
20
|
+
$env:RB_AGENT = 'dsh-code-review-local'
|
|
21
|
+
$env:RB_OUT = 'G:\benchmark-output\fresh-run\findings.json'
|
|
22
|
+
$env:RB_CONFIG_MODEL = '<模型名>'
|
|
23
|
+
$env:RB_MODEL_BASE_URL = 'https://<可信端点>/v1'
|
|
24
|
+
# 在安全环境中预先设置 RB_CONFIG_API_KEY、OPENAI_API_KEY 或 DEEPSEEK_API_KEY。
|
|
25
|
+
node scripts/review-bench-adapter.mjs
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
输出父目录须存在,每次使用新的运行目录;已有结果不会被覆盖。读取 base/head 的唯一 merge-base,审查 merge-base 到 head 的固定对象;不修改被审查仓库,不读取黄金标注。
|
|
29
|
+
|
|
30
|
+
## 输出与失败规则
|
|
31
|
+
|
|
32
|
+
- `RB_OUT.report.json` 保存完整原报告、mock 标志、配置档与 merge-base,便于追溯覆盖与限制。
|
|
33
|
+
- 正式候选只在报告 completed 且所有选中文件 completed 时生成。partial/failed/cancelled、未完成文件不冒充空发现或成功。
|
|
34
|
+
- 旧侧行、文件级/属性级锚点无法无损表达成基准要求的 head 行号;当前明确失败并保留原报告,不静默丢弃或猜测位置。
|
|
35
|
+
- 不按行号合并不同问题;只去掉完全相同的导出记录。超过 200 条报错,不裁剪。
|
|
36
|
+
- 单次请求沿用 120 秒生命周期;整次运行另有 14 分钟截止,不放宽核心输入/调用预算。
|
|
37
|
+
- HTTP 错误不输出响应正文;完整 JSON 或单个完整 JSON fence 使用项目现有严格解析,截断和夹杂文本被拒绝。
|
|
38
|
+
|
|
39
|
+
## 离线冒烟
|
|
40
|
+
|
|
41
|
+
设置 `RB_MOCK=1` 后运行。只生成 `.report.json` 与 `.smoke.json`,不生成可评分候选 `RB_OUT`,避免离线结果误入正式评分。mock 不调用真实模型,也不证明检出质量。
|
|
42
|
+
|
|
43
|
+
## 验证边界
|
|
44
|
+
|
|
45
|
+
这里提供入口和局部回归,不包含下载全部 PR 的调度器,也不包含真实模型或 LLM judge 的评分结果。使用 ReviewBench 的独立 judge 前,需确认候选 PR 身份与基准语料一致,保持失败样本在总数中,并人工核验模型裁决。正式成绩、数据集完整性与生产配置效果仍需单独验证。
|
|
46
|
+
|
|
47
|
+
来源:[agent 契约](https://github.com/review-bench/ReviewBench/blob/main/AGENT_CONTRACT.md)、[评分输入](https://github.com/review-bench/ReviewBench/blob/main/docs/JUDGING_INPUT.md)。
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feng0531/dsh-code-review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
|
7
7
|
"main": "./index.mjs",
|
|
8
8
|
"exports": { ".": "./index.mjs", "./package.json": "./package.json", "./locale/*.json": "./locale/*.json" },
|
|
9
|
-
"files": ["index.mjs", "cordis.patch.yml", "src/", "locale/", "README.md", "CHANGELOG.md", "RELEASE-NOTES.md", "DEVELOPMENT.md", "CORE-ALIGNMENT-TASKS.md", "CORE-SOURCE-INVENTORY.md", "LICENSE", "LICENSES/"],
|
|
9
|
+
"files": ["scripts/review-bench-adapter.mjs", "evaluation/REVIEWBENCH-LOCAL.md", "index.mjs", "cordis.patch.yml", "src/", "locale/", "README.md", "CHANGELOG.md", "RELEASE-NOTES.md", "DEVELOPMENT.md", "CORE-ALIGNMENT-TASKS.md", "CORE-SOURCE-INVENTORY.md", "LICENSE", "LICENSES/"],
|
|
10
10
|
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
|
|
11
11
|
"repository": { "type": "git", "url": "https://github.com/f-e-n-g-0531/dsh-code-review.git" },
|
|
12
12
|
"type": "module",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Local-only benchmark entrypoint; does not load or change DSH host settings.
|
|
2
|
+
import { checked } from '../src/process.mjs';
|
|
3
|
+
import { captureSnapshot } from '../src/snapshot.mjs';
|
|
4
|
+
import { reviewSnapshot } from '../src/review.mjs';
|
|
5
|
+
import { modelJson } from '../src/model-json.mjs';
|
|
6
|
+
import { writeFile } from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const RESPONSE_BYTES = 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
export function benchmarkConfig(env) {
|
|
13
|
+
const base = env.RB_BASE, head = env.RB_HEAD;
|
|
14
|
+
if (![base, head].every(value => typeof value === 'string' && /^[0-9a-f]{40}$/.test(value))) throw new Error('RB_BASE/RB_HEAD must be full lowercase Git SHAs');
|
|
15
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(env.RB_NWO ?? '') || env.RB_NWO.split('/').some(part => part === '.' || part === '..')) throw new Error('Invalid RB_NWO');
|
|
16
|
+
if (!/^[1-9][0-9]*$/.test(env.RB_PR_NUMBER ?? '') || !Number.isSafeInteger(Number(env.RB_PR_NUMBER))) throw new Error('Invalid RB_PR_NUMBER');
|
|
17
|
+
if (!env.RB_REPO_DIR || !env.RB_OUT) throw new Error('Set RB_REPO_DIR and RB_OUT for local evaluation');
|
|
18
|
+
const mock = env.RB_MOCK === '1';
|
|
19
|
+
if (env.RB_MOCK !== undefined && !['0', '1'].includes(env.RB_MOCK)) throw new Error('Invalid RB_MOCK');
|
|
20
|
+
if (!mock && (!env.RB_CONFIG_MODEL || !env.RB_MODEL_BASE_URL)) throw new Error('Set an explicit RB_CONFIG_MODEL and RB_MODEL_BASE_URL');
|
|
21
|
+
let endpoint;
|
|
22
|
+
if (!mock) {
|
|
23
|
+
const url = new URL(env.RB_MODEL_BASE_URL);
|
|
24
|
+
if (url.username || url.password || url.search || url.hash || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))) throw new Error('Invalid model endpoint; use HTTPS or local HTTP without URL credentials');
|
|
25
|
+
endpoint = url.href.replace(/\/+$/, '') + '/chat/completions';
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
repo: path.resolve(env.RB_REPO_DIR), out: path.resolve(env.RB_OUT), base, head,
|
|
29
|
+
nwo: env.RB_NWO, prNumber: Number(env.RB_PR_NUMBER),
|
|
30
|
+
agent: env.RB_AGENT || 'dsh-code-review', mock, endpoint,
|
|
31
|
+
model: env.RB_CONFIG_MODEL,
|
|
32
|
+
apiKey: env.RB_CONFIG_API_KEY || env.OPENAI_API_KEY || env.DEEPSEEK_API_KEY || '',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function responseJson(response) {
|
|
37
|
+
if (!response.body) throw new Error('Model response has no body');
|
|
38
|
+
const reader = response.body.getReader(), chunks = [];
|
|
39
|
+
let size = 0;
|
|
40
|
+
try {
|
|
41
|
+
for (;;) {
|
|
42
|
+
const { done, value } = await reader.read();
|
|
43
|
+
if (done) break;
|
|
44
|
+
size += value.byteLength;
|
|
45
|
+
if (size > RESPONSE_BYTES) throw new Error('Model response exceeds byte limit');
|
|
46
|
+
chunks.push(Buffer.from(value));
|
|
47
|
+
}
|
|
48
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
49
|
+
} finally {
|
|
50
|
+
await reader.cancel().catch(() => {});
|
|
51
|
+
reader.releaseLock();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function benchmarkExecutor(config, fetchImpl = fetch) {
|
|
56
|
+
return async request => {
|
|
57
|
+
const response = await fetchImpl(config.endpoint, {
|
|
58
|
+
method: 'POST', redirect: 'error', signal: request.signal,
|
|
59
|
+
headers: { 'Content-Type': 'application/json', ...(config.apiKey ? { Authorization: 'Bearer ' + config.apiKey } : {}) },
|
|
60
|
+
body: JSON.stringify({ model: config.model, messages: [
|
|
61
|
+
{ role: 'system', content: request.instructions },
|
|
62
|
+
{ role: 'user', content: request.input },
|
|
63
|
+
], temperature: 0, max_tokens: 4096 }),
|
|
64
|
+
});
|
|
65
|
+
// Error bodies can contain credentials or source; never echo them.
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
await response.body?.cancel().catch(() => {});
|
|
68
|
+
throw new Error('Model HTTP status ' + response.status);
|
|
69
|
+
}
|
|
70
|
+
const data = await responseJson(response);
|
|
71
|
+
if (data?.choices?.length !== 1 || data.choices[0]?.finish_reason !== 'stop') throw new Error('Model response incomplete or ambiguous');
|
|
72
|
+
const content = data.choices[0]?.message?.content;
|
|
73
|
+
if (typeof content !== 'string' || !content) throw new Error('Model returned no text');
|
|
74
|
+
return JSON.parse(modelJson(content));
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function benchmarkFindings(snapshot, report, agent) {
|
|
79
|
+
if (report.status !== 'completed' || !report.files?.length || report.files.some(file => file.status !== 'completed')) throw new Error('Benchmark review incomplete; see saved report');
|
|
80
|
+
const paths = new Map(snapshot.files.map(file => [file.id, file.path]));
|
|
81
|
+
const seen = new Set(), findings = [];
|
|
82
|
+
for (const finding of report.findings) {
|
|
83
|
+
const anchor = finding.anchor, file = paths.get(finding.fileId);
|
|
84
|
+
if (!file || file.startsWith('/') || file.includes('\\') || file.split('/').some(part => ['', '.', '..'].includes(part))) throw new Error('Unrepresentable finding path');
|
|
85
|
+
// Do not invent target coordinates or silently remove a valid old-side issue.
|
|
86
|
+
if (anchor?.kind !== 'line' || anchor.side !== 'new' || !Number.isSafeInteger(anchor.start) || !Number.isSafeInteger(anchor.end) || anchor.start < 1 || anchor.end < anchor.start) throw new Error('Finding has no valid head-side line range; see saved report');
|
|
87
|
+
if (finding.verification && (finding.verification.status !== 'completed' || finding.verification.verdict !== 'supported')) throw new Error('Finding requires explicit adjudication; see saved report');
|
|
88
|
+
const message = [finding.title, finding.evidence, finding.trigger, finding.impact, finding.suggestion].filter(value => typeof value === 'string' && value.trim()).join(' ');
|
|
89
|
+
if (!message) throw new Error('Empty finding message');
|
|
90
|
+
const mapped = { file, start_line: anchor.start, end_line: anchor.end, message, producer: agent };
|
|
91
|
+
const identity = JSON.stringify(mapped);
|
|
92
|
+
if (!seen.has(identity)) { seen.add(identity); findings.push(mapped); }
|
|
93
|
+
}
|
|
94
|
+
if (findings.length > 200) throw new Error('Benchmark finding limit exceeded; no clipping performed');
|
|
95
|
+
return findings;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function runBenchmark(env = process.env) {
|
|
99
|
+
const config = benchmarkConfig(env);
|
|
100
|
+
const signal = AbortSignal.timeout(14 * 60 * 1000), started = Date.now();
|
|
101
|
+
const bases = (await checked('git', ['merge-base', '--all', config.base, config.head], { cwd: config.repo, signal })).toString('utf8').trim().split(/\r?\n/);
|
|
102
|
+
if (bases.length !== 1 || !/^[0-9a-f]{40}$/.test(bases[0])) throw new Error('Expected a unique merge base; no arbitrary base selected');
|
|
103
|
+
const snapshot = await captureSnapshot(config.repo, { baseRevision: bases[0], targetRevision: config.head, signal });
|
|
104
|
+
const executor = config.mock ? async () => ({ findings: [], limitations: [] }) : benchmarkExecutor(config);
|
|
105
|
+
console.log('profile=single-pass-direct mock=' + config.mock + ' model=' + (config.model || 'none'));
|
|
106
|
+
const report = await reviewSnapshot(snapshot, executor, { signal, reviewRounds: 1, maxCalls: 100, timeoutMs: 120_000 });
|
|
107
|
+
signal.throwIfAborted();
|
|
108
|
+
// Each run owns fresh output paths; never mix attempts or overwrite prior results.
|
|
109
|
+
await writeFile(config.out + '.report.json', JSON.stringify({ mock: config.mock, profile: 'single-pass-direct', mergeBase: bases[0], report }, null, 2) + '\n', { flag: 'wx' });
|
|
110
|
+
const findings = benchmarkFindings(snapshot, report, config.agent);
|
|
111
|
+
const output = {
|
|
112
|
+
pr: { repo: 'https://github.com/' + config.nwo, pr_number: config.prNumber, base: config.base, head: config.head },
|
|
113
|
+
agent: config.agent, findings, usage: { time_in_ms: Date.now() - started },
|
|
114
|
+
};
|
|
115
|
+
// Smoke output is intentionally not a scoreable candidate file.
|
|
116
|
+
await writeFile(config.mock ? config.out + '.smoke.json' : config.out, JSON.stringify(output, null, 2) + '\n', { flag: 'wx' });
|
|
117
|
+
return output;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
121
|
+
runBenchmark().catch(() => {
|
|
122
|
+
console.error('Benchmark adapter failed; no successful result implied. Check configuration and any saved report.');
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
});
|
|
125
|
+
}
|