@feng0531/dsh-code-review 0.57.0 → 0.58.1

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 CHANGED
@@ -2,9 +2,17 @@
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.1 — SVN 根目录属性预览修复
6
+
7
+ 修复 SVN 合并产生根目录 mergeinfo 属性时,默认预览和指定文件预览均报 Invalid relation file 的问题。保留根目录属性变更和覆盖统计,仅允许有目录身份的根节点 `.`,普通路径穿越仍拒绝;上下文竞争条目保留目录身份。独立 SVN 三文件变更与根 mergeinfo 场景验证预览零模型发送和工作副本状态不变。全面 SVN 扩展另按支持矩阵分批推进,尚未宣称历史区间或所有特殊状态已支持。
8
+
9
+ ## 0.58.0 — 本地评测入口可靠性整理
10
+
11
+ 新增无需 Docker 的实验性 ReviewBench 本地入口与使用说明。要求显式本地仓库、模型与端点;复用严格 JSON 校验,拒绝截断和夹杂文本。未完成审查、不可映射的旧侧发现及超限结果明确失败并保存原报告,不伪装为空发现;同一行的不同问题不再误合并。mock 输出与可评分候选隔离,运行结果不覆盖。正常 DSH 工具行为、权限与预算不变;不代表真实模型质量已得到验证。454 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原产物 113,697 字节逐字节一致,双哈希、清单与冻结发布说明核验通过。
12
+
5
13
  ## 0.57.0 — 对齐结论与边界收尾
6
14
 
7
- 完成组合场景整体一致性验收与完整语义导航的源码对照收尾:范围内已对齐项清空「仍需补齐」;参考独有的会话存储、断点续跑、豁免与 token 预估记为有意不同;完整语义调用方、别名与跨目录实例上下文明确为需语义解析器的范围外。不改变审查行为或参数。
15
+ 完成组合场景整体一致性验收与完整语义导航的源码对照收尾:范围内已对齐项清空「仍需补齐」;参考独有的会话存储、断点续跑、豁免与 token 预估记为有意不同;完整语义调用方、别名与跨目录实例上下文明确为需语义解析器的范围外。不改变审查行为或参数。449 项完整回归与四组 CI 通过;npm、Release 与原产物 108,451 字节一致,双哈希和清单通过。
8
16
 
9
17
  ## 0.56.0 — 依赖清单与配置文件专用指导
10
18
 
@@ -25,14 +25,16 @@
25
25
 
26
26
  - 继承、原型链与动态分派:需要语言解析器或类型检查器做语义绑定,超出「只读批准快照、不放宽预算」的保守边界。
27
27
  - 完整语义调用方、别名与跨目录实例上下文:需要语义解析器;保守的命名/默认/命名空间成员/类实例方法导航已交付,其余不猜测。
28
- - SVN 历史区间等旧侧独有来源:历史/旧侧三层链、oldContextPaths、删除旧正文复用已交付;SVN 历史区间按设计不支持。
28
+ - SVN 全面支持扩展:用户已要求将工作副本特殊状态与固定 revision 历史审查纳入新目标。当前历史/旧侧三层链、oldContextPaths 和删除旧正文复用仅是 Git 已交付能力,不能算作 SVN 支持。SVN 当前缺口、批次与验收见 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md)。
29
29
  - 真实模型质量评测:目标明确后置,保存真实输出、人工裁决、分开统计,不作为对齐前提。
30
30
 
31
31
  各版交付结论见「当前验证结论」与 [版本变化](CHANGELOG.md)。
32
32
 
33
33
  ## 当前验证结论
34
34
 
35
- - 最近完整回归:449 项通过。
35
+ - 最近完整回归:454 项通过。
36
+ - v0.58.0 已正式交付实验性本地评测入口及可靠性整理。454 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 113,697 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明核验通过。正常 DSH 审查行为与预算不变;尚无真实模型基准评分。
37
+ - v0.57.0 已正式交付对齐结论与边界收尾。449 项回归与四组 CI 通过;npm、Release 与原包 108,451 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
36
38
  - v0.56.0 已正式交付依赖清单与配置文件专用指导。449 项回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 108,081 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
37
39
  - v0.55.0 已正式交付模板引擎与基础设施即代码专用指导。446 项回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 106,969 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
38
40
  - v0.54.0 已正式交付对齐结论与边界收尾。443 项回归、实际宿主与四组 CI 通过;npm、Release 与原包 104,761 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
package/RELEASE-NOTES.md CHANGED
@@ -1,12 +1,16 @@
1
- # v0.57.0 发布说明
1
+ # v0.58.1 发布说明
2
2
 
3
- ## 对齐结论与边界收尾
3
+ ## SVN 根目录属性预览修复
4
4
 
5
- 本轮完成最后两类差距的源码对照收尾:组合场景整体一致性验收在保守范围内已对齐(分组阈值、预算组合、覆盖终态与失败分类均已验收),参考独有但超出范围的只有会话存储、断点续跑、豁免与 token 预估;完整语义调用方、别名与跨目录实例上下文需要语义解析器,超出保守语法绑定边界。对照表相应「仍需补齐」项清空,「有意不同」按证据补记。
5
+ 修复 SVN 分支合并产生根目录 svn:mergeinfo 属性变更时,插件预览报 `Invalid relation file` 的问题。原问题既影响默认预览,也影响仅选择源码文件的预览,因为根目录条目仍保留在快照中。
6
6
 
7
- ## 限制
7
+ - 保留根目录属性变化与覆盖统计,不删除条目掩盖问题。
8
+ - 仅允许明确目录身份的根节点 `.`;普通文件的 `.`、`..` 路径仍被拒绝。
9
+ - 上下文规划保留目录身份,避免再次触发关系校验错误。
10
+ - 独立 SVN 测试覆盖三源码文件与根 mergeinfo 变更,验证两种预览均零模型发送、工作副本状态不变。
8
11
 
9
- - 只读批准快照,不新增读取、不放宽预算、不改变审查行为或参数。
10
- - 不使用版本或测试数量表示对齐百分比。
12
+ ## SVN 后续支持
11
13
 
12
- 使用方法见 [README](README.md)。
14
+ 按新的全面 SVN 目标,复制/移动/替换、关键字与换行、固定 revision 历史审查及历史上下文将分批实现。现阶段并非所有 SVN 状态都可审查;见 [支持矩阵](SVN-SUPPORT-MATRIX.md)。
15
+
16
+ 本版本未对用户 r7315 的实际临时副本进行审查,不宣称该提交已由插件通过。日常使用见 [README](README.md)。
@@ -0,0 +1,43 @@
1
+ # SVN 审查支持矩阵与实施顺序
2
+
3
+ 本矩阵按用户新的「全面支持 SVN」要求扩展目标。此前把 SVN 历史审查列为设计不支持,不再作为此任务的完成理由。支持状态须由实际测试确认,不以规划、发布数量或版本号代替验收。
4
+
5
+ ## 不变的约束
6
+
7
+ - 不对用户工作副本执行 checkout/update/switch/merge/revert/cleanup/commit;集成测试在独立本地仓库和临时工作副本运行。
8
+ - 预览不发送源码,执行必须绑定同一预览、模型、工作目录与快照身份。
9
+ - 本地工作副本模式默认离线;新增远端历史读取必须显式选择并在预览中披露网络访问和来源。
10
+ - 历史来源固定仓库 UUID、规范 URL、peg revision、operative revision、节点路径与内容哈希;禁止把较新工作树当作历史正文。
11
+ - 文件/字节/上下文/精确探测/调用/超时预算继续共享。超限、不可解码、不可定位和权限失败明确保留未完成状态。
12
+
13
+ ## 当前盘点(不是全部支持)
14
+
15
+ | 场景 | 当前证据/限制 | 实施批次 |
16
+ |---|---|---|
17
+ | 常规文件修改、普通新增、删除 | 已有 BASE/工作字节与属性捕获测试 | 维持并扩充组合验收 |
18
+ | 子目录属性变更 | 已有独立属性审查测试 | 第一批 |
19
+ | 根目录属性/mergeinfo | 新测试复现 `Invalid relation file`;本地补丁通过指定三文件和默认预览,0 模型发送,456 项回归通过;尚未发布 | 第一批先发布此修复 |
20
+ | 复制/移动、copyfrom 及目录结构变更 | 现有采集拒绝复制和目录结构变更;不能说已支持 | 第二批:明确旧侧来源和后代覆盖 |
21
+ | 替换、删除再新增、文件/目录类型变化 | 当前 replaced 被拒绝 | 第二批:区分 BASE 旧节点与目标节点 |
22
+ | 缺失、文本/属性/树冲突、阻塞 | 当前多数仅统一 blocked | 第三批:明确状态诊断;不能合成不存在的工作侧或声称审核通过 |
23
+ | svn:keywords、svn:eol-style | keywords 当前阻断;换行保留原文但无规范化映射 | 第三批:双侧可验证映射,保留原字节和定位 |
24
+ | svn:special/链接 | 当前阻断,不跟随链接取正文 | 第三批:如支持只审链接目标数据;不沿链接读取文件 |
25
+ | switched、externals、稀疏/混合 revision | 多数拒绝;调用方/依赖链要求祖先 normal | 第四批:明确仓库边界与独立来源,禁止隐式跨仓读取 |
26
+ | 固定 revision 单提交 | 未实现;Git commit 参数不能冒充 SVN revision | 第五批:新增明确 SVN 参数与冻结身份 |
27
+ | 固定 revision 区间 | 未实现;不能通过临时重建工作树冒充原生历史能力 | 第五批:固定树端点与逐路径差异 |
28
+ | 分支复制、分支间比较、合并来源 | mergeinfo 当前仅作为属性文本;不证明源码归属或被合并提交均已审查 | 第六批:URL/peg/operative revision 与 mergeinfo 导航披露 |
29
+ | 当前三层依赖与限定目录调用方 | 已有安全 normal 节点的离线有界测试;根属性修改可能使祖先检查拒绝,需单独验证 | 第四批补祖先/混合状态组合 |
30
+ | 历史两侧上下文与调用方 | 未实现 | 第六批:逐侧固定版本,不能回退至工作副本 |
31
+ | 真正 r7314→r7315 样本 | 用户未提供隔离副本路径;本地类似复现不是该样本验收 | 获得路径后只读生成预览,不自动发送模型 |
32
+
33
+ ## 每批发布门禁
34
+
35
+ 1. 独立测试副本复现,确认 capture→关系推断→预览→执行的覆盖和拒绝路径。
36
+ 2. 专项、全量串行回归、实际宿主、隔离安装通过。
37
+ 3. 提交/推送/冻结版本标签;四矩阵 CI 通过后唯一 Actions 发布。
38
+ 4. 保存原始 Actions 产物,核验 npm/Release 逐字节、SHA-256、SHA-512、清单与冻结发布说明。
39
+ 5. 写入该批验证结论;更新本矩阵,剩余项继续保留未完成。
40
+
41
+ ## 收尾标准
42
+
43
+ 每行均有明确支持模式、失败处理、源码位置和测试证据。诊断未完成不等于场景已经可审查;任何保守拒绝须单列原因,不能把拒绝范围自动从目标中删除。真实模型质量另行评测,不由采集/发布测试推断。
@@ -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.57.0",
3
+ "version": "0.58.1",
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", "SVN-SUPPORT-MATRIX.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
+ }
@@ -22,7 +22,7 @@ export function inferContextRelations(input, file) {
22
22
  const primary = { ...file, id: 'primary' };
23
23
  const entries = contexts.map((c, i) => ({ id: 'context-' + i, path: c.path, eligibility: 'reviewable', left: { text: '' }, right: { text: c.text } }));
24
24
  // Other selected paths participate as opaque competitors, not context.
25
- const competitors = input.files.filter(f => f.id !== file.id).map((f, i) => ({ id: 'selected-' + i, path: f.path, eligibility: 'blocked' }));
25
+ const competitors = input.files.filter(f => f.id !== file.id).map((f, i) => ({ id: 'selected-' + i, path: f.path, ...(f.kind === 'dir' ? {kind: 'dir'} : {}), eligibility: 'blocked' }));
26
26
  const all = [primary, ...entries, ...competitors];
27
27
  if (all.length > 200) return [];
28
28
  const byId = new Map(all.map(f => [f.id, f.path]));
@@ -5,7 +5,7 @@ export function inferCppRelations(files) {
5
5
  if (!Array.isArray(files) || files.length > 200) throw new Error('Invalid C++ relation input');
6
6
  const byPath = new Map(), ids = new Set();
7
7
  for (const file of files) {
8
- if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.includes(':') || file.path.startsWith('/') || file.path.split('/').some(s => !s || s === '.' || s === '..') || byPath.has(file.path) || !['reviewable','excluded','blocked'].includes(file.eligibility)) throw new Error('Invalid C++ relation file');
8
+ if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.includes(':') || file.path.startsWith('/') || (!(file.path === '.' && file.kind === 'dir') && file.path.split('/').some(s => !s || s === '.' || s === '..')) || byPath.has(file.path) || !['reviewable','excluded','blocked'].includes(file.eligibility)) throw new Error('Invalid C++ relation file');
9
9
  ids.add(file.id); byPath.set(file.path,file);
10
10
  }
11
11
  const edges = [];
@@ -11,7 +11,7 @@ export function inferTestRelations(files) {
11
11
  if (!Array.isArray(files) || files.length > 200) throw new Error('Invalid relation input');
12
12
  const paths = new Set(), ids = new Set(), implementations = new Map(), tests = [];
13
13
  for (const file of files) {
14
- if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.split('/').some(p => !p || p === '.' || p === '..') || paths.has(file.path) || !['reviewable', 'blocked', 'excluded'].includes(file.eligibility)) throw new Error('Invalid relation file');
14
+ if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || (!(file.path === '.' && file.kind === 'dir') && file.path.split('/').some(p => !p || p === '.' || p === '..')) || paths.has(file.path) || !['reviewable', 'blocked', 'excluded'].includes(file.eligibility)) throw new Error('Invalid relation file');
15
15
  ids.add(file.id); paths.add(file.path);
16
16
  if (file.eligibility !== 'reviewable') continue;
17
17
  const match = /^(.*)\.(mjs|cjs|js|jsx|ts|tsx)$/.exec(file.path);
@@ -12,7 +12,7 @@ export function inferImportRelations(files) {
12
12
  if (!Array.isArray(files) || files.length > 200) throw new Error('Invalid import relation input');
13
13
  const byPath = new Map(), ids = new Set();
14
14
  for (const file of files) {
15
- if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.startsWith('/') || file.path.includes(':') || file.path.split('/').some(p => !p || p === '.' || p === '..') || byPath.has(file.path) || !['reviewable', 'blocked', 'excluded'].includes(file.eligibility)) throw new Error('Invalid import relation file');
15
+ if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.startsWith('/') || file.path.includes(':') || (!(file.path === '.' && file.kind === 'dir') && file.path.split('/').some(p => !p || p === '.' || p === '..')) || byPath.has(file.path) || !['reviewable', 'blocked', 'excluded'].includes(file.eligibility)) throw new Error('Invalid import relation file');
16
16
  ids.add(file.id); byPath.set(file.path, file);
17
17
  }
18
18
  const edges = [];
package/src/svn.mjs CHANGED
@@ -38,7 +38,7 @@ export async function captureSvn(cwd, options = {}) {
38
38
  signal?.throwIfAborted();
39
39
  const name = entry['@_path'].replaceAll('\\', '/');
40
40
  const state = entry['wc-status'], status = state['@_item'];
41
- const item = { id: hash(name), path: name, rawStatus: status, baseRevision: state['@_revision'], eligibility: 'reviewable', properties: [] };
41
+ const item = { id: hash(name), path: name, ...(name === '.' ? {kind: 'dir'} : {}), rawStatus: status, baseRevision: state['@_revision'], eligibility: 'reviewable', properties: [] };
42
42
  files.push(item);
43
43
  if (isSecretPath(name)) { item.eligibility='excluded'; item.reason='Credential path excluded'; continue; }
44
44
  if ((selectedPaths && !selectedPaths.includes(name)) || (status === 'unversioned' && !selectedPaths?.includes(name))) {