@feng0531/dsh-code-review 0.87.1 → 0.87.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/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
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.87.3 — 明确审核指令不再重复确认
6
+
7
+ - 明确要求审核且内部预览与当前授权范围、模型及发送目的地一致时,工具指导直接继续执行,不再要求第二次人工确认;仅预览或超出授权时仍暂停。
8
+ - 保留 confirmed 参数、快照与版本新鲜度、会话/目录/模型身份、一次性预览及宿主权限检查;私有模型工件仍需独立明确同意。
9
+
10
+ ## 0.87.2 — 导出案例回归修复
11
+
12
+ - 大文件稀疏、远距离编辑增加有界最短编辑回退,避免二维差异表超限后不必要地扩大为近全文包络;超预算仍明确受限。
13
+ - 文件/窗口主审在局部额度不足时保留同次检索链,等待其他阶段释放容量后有限续跑;不重置读取总账、轮数或截止时间,不重放已接受生成。
14
+ - 正常组主审仍合并调用;仅真实局部读取预算不足且尚未接受组成果时回退可续跑成员主审,保留已消费总账。风险规划开启时,组响应明确要求各成员提供 `risks`(允许空数组),消除可选提示与验收要求矛盾。
15
+ - 迁移对照保留旧公开校验入口与旧委托实现;新增删除导出/单例/不同名入口的有限词法导航,歧义与未证实因果仍为 uncertain。
16
+ - 预览返回有效截止时间,区分缺失、过期及模型/目录变化;后台摘要说明调用下限不是实际成本上限,阶段限制与全局余额分别展示。
17
+
5
18
  ## 0.87.1 — 修复阶段续审缓存中断
6
19
 
7
20
  - 主审、窗口归因和续审导入从同一批准原文重建包络坐标;包络仍不表示精确编辑或回归因果。
@@ -2,7 +2,7 @@
2
2
 
3
3
  供维护者核查实现依据。使用方法见 [README](README.md),开发优先级见 [能力清单](CORE-ALIGNMENT-TASKS.md)。
4
4
 
5
- 当前本轮对照固定为 alibaba/open-code-review 提交 `216dcf513593055a70165f793e96b52589d71b1b`,不随上游分支变化。下方文件索引仍是旧提交 `2d67596c961f80436deb2afa643efa2b1725d34c` 的历史阅读记录;未把旧记录冒充新提交逐文件复审。
5
+ 上一轮完整对照固定为 alibaba/open-code-review 提交 `216dcf513593055a70165f793e96b52589d71b1b`。本轮导出案例修复另核实上游 HEAD `8f9c8b771f65bed63a193e873f12cdffb050b748`,只补读 `internal/tool/file_read_diff.go` 的缺失差异导航和 `internal/diff/git.go` 的 tracked/gitignored 范围修复,不宣称完成该版本全量对照。差异算法参考旧固定版本 `internal/suggestdiff/diff.go` 与 `internal/diff/hunk.go`,不照搬无界二维表或忽略大小写/空白的对齐。下方文件索引仍是旧提交 `2d67596c961f80436deb2afa643efa2b1725d34c` 的历史阅读记录;未把旧记录冒充新提交逐文件复审。
6
6
 
7
7
  ## 对照结论
8
8
 
package/DEVELOPMENT.md CHANGED
@@ -10,7 +10,7 @@
10
10
  npm test
11
11
  node scripts/host-smoke.mjs <DSH-checkout>
12
12
 
13
- 宿主检查加载真实 DSH 工具运行时,但使用受控模型响应,不是 GUI 安装或真实模型质量测试。Windows SVN 需要支持 UTF-8 路径;CI 使用临时工具,不修改用户安装。
13
+ 宿主检查加载真实 DSH 工具运行时,但使用受控模型响应,不是 GUI 安装或真实模型质量测试。若安装路径位于 Electron `app.asar` 内,普通 Node 不识别归档虚拟路径;PowerShell 可用 `$env:ELECTRON_RUN_AS_NODE="1"; & "<安装目录>/DeepSeek Harness.exe" scripts/host-smoke.mjs "<安装目录>/resources/app.asar/dsh"` 运行归档感知的同一检查,不解包或修改宿主。Windows SVN 需要支持 UTF-8 路径;CI 使用临时工具,不修改用户安装。
14
14
 
15
15
  ## 会话与模式
16
16
 
@@ -18,6 +18,12 @@
18
18
 
19
19
  项目捕获完整清单最多 10000 节点、1000 选择节点、16 MiB。Git 显式选择以精确字面路径查询所选索引,单次查询最多 64 KiB、总计 2 MiB,不受未选索引数量影响;初次、执行前及末尾复核使用相同范围。局部清单不宣称全仓完整性或总数,只绑定所选索引和正文,未选索引变动不使其失效。公开工具的显式列表仍最多 200 项。不设默认总模型调用次数或执行秒表,初次捕获/复核均遵循宿主与用户取消。SVN 仍枚举本地完整已观察清单,项目模式拒绝不支持节点属性,不隐式继承固定历史审查的网络权限。
20
20
 
21
+ ## 预览与当前人类授权
22
+
23
+ - 明确要求“审核 r7364”等执行请求,在范围、模型和发送目的地明确且一致时就是当前审核授权;Agent 仍须先内部 preview,固定并核对 scope、route、budget 与权限,再使用真实 `previewId` 和 `confirmed:true` 执行,不重复询问确认。只要求预览或明确要求等待确认时,不得执行;范围、模型或发送目的地超出授权,或边界不明确时,先询问。SVN 远端读取等独立权限不由审核请求隐式授予。
24
+ - `confirmed:true` 是 Agent 对当前人类明确授权且本次预览匹配的声明,不是取消确认参数或无授权绕过。保留 preview→execute 两阶段与 service 确认参数,不新增一键执行 API;宿主/会话工具权限、模型发送许可、快照新鲜度、会话/目录/路由身份及预算校验均不变。
25
+ - 源变化、预览过期或身份变化须生成新 preview 并重新核对当前授权,不能将旧预览批准用于新快照。保存的后台记录、`resumeFromJobId`、自动续跑及 host 重启本身都不授予外发权限;显式续审须有当前人类授权和新预览,匹配时无需重复询问。私有模型工件的源码提示与独立明确同意要求保持不变。
26
+
21
27
  ## 变更审查调度与离线对照
22
28
 
23
29
  生产 changes 服务默认 `changesConcurrency=2`,配置范围 1~4;底层 `reviewSnapshot` 未传该选项时保持串行兼容。所有文件共用原批准快照 reader,不能按 worker 重置额度。业务分组先完成,交互 owner 等待此前成员链结束;窗口、轮次与复核仍串行依赖,固定索引归并结果。取消或 checkpoint 失败必须先排空所有在途链再释放锁。
@@ -42,11 +48,11 @@ JSON合同新增280字节传输提示防止原始换行/反斜线转义错误
42
48
 
43
49
  连续正文投递的索引仅保存本次批准且实际发送的坐标;当前 stage reader 重新取原文并扣字节,不共享旧 receipt。可选初始正文事务延迟到调用预算准入后,预算否决/取消/输入不适配必须回滚,未送入模型不发布 audit。交互内联风险缺失/非法不抹合法 findings,不补发规划。
44
50
 
45
- 迁移元数据是宿主生成的 side-qualified 导航,不是模型授权。跨文件 regression 只用已批准 contract ID、来源/角色/精确编辑和独立已校验 receipt,默认旧同主文件协议仍严格。C++ 仅在字面 include、唯一类声明/类外定义和明确 receiver 下产生保守线索,重载/继承/虚分派/宏不能猜测唯一绑定。
51
+ 迁移元数据是宿主生成的 side-qualified 导航,不是模型授权。`old-delegation` 保留旧公开入口、旧实现及新同源入口,非 uncertain 判定要求本代 receipt 完整覆盖三个函数。删除导出入口、单例调用与不同名新入口的 `deleted-export-route` 只交付词法候选导航,强制 uncertain;不证明类型、宏语义、实参等价或共享状态身份,复杂/歧义语法明确记录不可用。跨文件 regression 只用已批准 contract ID、来源/角色/精确编辑和独立已校验 receipt,默认旧同主文件协议仍严格。C++ 仅在字面 include、唯一类声明/类外定义和明确 receiver 下产生保守线索,重载/继承/虚分派/宏不能猜测唯一绑定。
46
52
 
47
53
  精确完整组的根可选 interactions 通道独立于合法 primary 结果验收。仅本代读取覆盖全部成员编辑、source/snapshot/hash 及跨文件引用验收成功后替代匹配专门生成;非法或缺失不触发隐藏主审重跑,候选仍用独立复核。原生 schema、文本合同和可选开销进入真实 wire 预算,预览 minimumCalls 是有条件下限并披露 fallbackMinimumCalls。
48
54
 
49
- 额度让出不抢 live unit;其他阶段 release 后有限 FIFO 继续,共享原调用/输出总账和绝对截止。schema2 只保存身份校验过的 primary generation,不持久化成可复用的新证据 receipt;恢复只省生成,交互/综合及验证重新从安全边界执行。没有进展保留 pending/partial,取消不自动变为完成。
55
+ 额度让出不抢 live unit;文件/窗口 primary 在同次执行以内存闭包保留待执行请求、已送结果、检索轮数和单次纠正许可,其他阶段 release 后有限 FIFO 继续,共享原调用/输出总账和绝对截止。正常 group-primary 仍一次合并生成;仅尚未接受组生成且真实局部分配失败、root 可承接请求时,明确回退成员 primary。预留成员 fallback floor 会降低当时组可用额度,成功组立即释放 floor;不重放已接受组,不给新总账。schema2 只保存身份校验过的 primary generation,不持久化成可复用的新证据 receipt;恢复只省生成,交互/综合及验证重新从安全边界执行。没有进展保留 pending/partial,取消不自动变为完成。
50
56
 
51
57
  受控集成验证入口为 `test/ocr-refactor-integration.test.mjs`、`test/ocr-complete-policy.test.mjs` 及 group/migration/stage 专项测试;其中开关对照 7→5、组通道 5→3 是固定离线响应的模型调用数,不是提供方速度或召回保证。正式发布仍执行下文完整门禁,不把文档更新或本地通过当作已发布。
52
58
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DSH Code Review
2
2
 
3
- 在 **DeepSeek Harness(DSH)的 Agent 对话里**审查 Git / SVN 代码变更。插件会先列出审查范围,得到确认后,再用当前 Agent 选择的模型生成中文问题报告。
3
+ 在 **DeepSeek Harness(DSH)的 Agent 对话里**审查 Git / SVN 代码变更。插件先预览并核对审查范围,再在当前人类明确授权内,用当前 Agent 选择的模型生成中文问题报告;已明确要求审核且预览匹配时,不重复询问确认。
4
4
 
5
5
  **只读:不修改代码,不自动修复,不提交、回滚或更新你的业务工作副本。**
6
6
 
@@ -53,7 +53,9 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
53
53
  确认刚才展示的预览范围和模型,使用该 previewId 执行只读审查。
54
54
  ```
55
55
 
56
- 插件使用预览返回的 ID 执行。默认预览有效期约 **5 分钟**;文件、模型或会话目录变化后,需要重新预览和确认。宿主权限不允许时,确认也不能绕过限制。
56
+ 以上是“只预览,等我确认”的流程。若你已明确要求“审核 r7364”(SVN 远端访问仍须单独授权),Agent 应先内部预览,固定并核对范围、模型路由、预算与权限;预览符合当前明确授权时,直接执行,不再要求你重复确认。只要求预览或明确要求等待确认时不得执行;范围、模型或发送目的地超出授权,或授权边界不明确时,先询问。
57
+
58
+ 插件使用预览返回的 ID 执行。默认预览有效期约 **5 分钟**,返回 `expiresAt` 与 `validForMs`;到期、文件、模型或会话目录变化后,需要新预览并重新核对当前授权,不能复用旧预览批准。`confirmed:true` 表示当前人类已明确授权且本次预览与授权一致,不是无授权绕过。计划最少调用只是初始下限,不是总调用数或耗时上限,后续取证、复核与综合会增加工作量。会话工具权限与宿主安全校验照旧,确认不能绕过限制。
57
59
 
58
60
  ### 后台执行如何等待
59
61
 
@@ -67,7 +69,7 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
67
69
 
68
70
  ## 按场景直接使用
69
71
 
70
- 下面的自然语言可以直接发给 Agent。每个场景都应先预览、展示范围,再确认执行。
72
+ 下面的自然语言可以直接发给 Agent。其中只要求预览或明确要求等待确认的示例,不得直接执行;若直接明确要求审核,则按上面的授权规则内部核对预览后执行,不重复询问。
71
73
 
72
74
  ### 只审查几个文件(Git / SVN 工作副本)
73
75
 
@@ -224,16 +226,16 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
224
226
 
225
227
  ## 长任务:后台预览、审查与取回结果
226
228
 
227
- 全项目或慢仓库建议使用后台工具,不让一次外层调用等待整个项目。仍先预览,再由你确认外发;启动返回jobId不等于审核完成。
229
+ 全项目或慢仓库建议使用后台工具,不让一次外层调用等待整个项目。仍先预览并核对当前明确授权,匹配时直接执行;仅要求预览则停在预览,超授权则询问。启动返回jobId不等于审核完成。
228
230
 
229
231
  1. 调用code_review_start,phase设为preview,previewArgs填原预览参数(例如reviewMode=project)。
230
232
  2. 用code_review_status和jobId查询;完成后读取resultSummary中的预览范围、模型和previewId。
231
- 3. 展示预览,确认后调用code_review_start,phase设为execute,传previewId与confirmed=true。
233
+ 3. 内部检查预览的范围、模型、发送目的地、预算和权限;当前人类已明确要求匹配的审核时,不重复询问,调用code_review_start,phase设为execute,传previewId与confirmed=true。仅要求预览或等待确认时先展示并等待;超授权时先询问。
232
234
  4. 持续调用code_review_wait等待进度变化或终态,不因启动返回任务ID就结束用户请求。可展示阶段与“处理数/总数”,不分批汇报问题;抓取阶段分母未知则不显示虚假百分比。等待返回仍running就继续等待,不重新启动。任务结束后取回并统一交付一份最终报告。查询审查任务;offset/limit分页取回检查点、文件状态和发现;单个检查点内的大清单用checkpointIndex指定其零基索引,再用offset/limit取回内部全部条目。可调用code_review_cancel取消。
233
235
 
234
236
  报告保存在宿主profile的data/dsh-code-review/jobs目录;没有profile环境时使用用户主目录.dsh下的数据目录,不写入业务仓库。内容可能含源码片段,应按源码敏感级别保护。本次启动调用结束不自动取消后台任务;明确取消、停用插件或宿主退出会停止它。
235
237
 
236
- 重启后可按原jobId读回保存记录,原运行标记interrupted,不自动继续外发。recovery给出已观察文件/窗口与未完成路径;它不是完整分母或新授权,补审必须重新预览确认。新预览的runtime包含实际加载版本和模块位置,排查不同工作区时分别核对,不根据另一台机器推断。
238
+ 重启后可按原jobId读回保存记录,原运行标记interrupted,不自动继续外发。recovery给出已观察文件/窗口与未完成路径;它不是完整分母或新授权;宿主重启及自动恢复本身不授权外发,补审必须有当前明确授权并重新预览核对。新预览的runtime包含实际加载版本和模块位置,排查不同工作区时分别核对,不根据另一台机器推断。
237
239
 
238
240
  变更审查的独立文件链默认最多 2 条并行;配置 `changesConcurrency` 为 1~4(1 回退串行),跨文件综合等待相关前置主审结束。配置 `maxRetrievalRounds` 为 0~50,默认为 3,限制每阶段检索请求回合数(不是成功读取次数;非法请求也会消耗回合)(0 只允许最终答复)。配置不是 preview 参数,不改变模型或来源授权;并发仍共享同一份读取/调用预算,额度不足时完成顺序可能影响哪些任务未完成,不能保证与串行逐项结果相同。提供方限流时应降低并发。复核可预取批准快照中候选锚点附近的同侧原文,仍允许追加反证读取并逐字校验;不复用主审结论或其他会话凭据。完整变更计划可容纳时仅生成一次再分发到窗口,否则保留原分窗口计划并在预览估算中体现。
239
241
 
@@ -241,7 +243,7 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
241
243
 
242
244
  ### 显式续审与私有模型工件
243
245
 
244
- 需要补审时,在**新预览**中传入 `resumeFromJobId`,例如 `{"resumeFromJobId":"原执行任务的真实jobId"}`;仍须展示新范围、重新确认并通过当前宿主权限。只复用同一会话、工作目录、模型路由/思考设置下,完整快照、配置和运行时身份均匹配的已完成且符合复用条件的文件链;项目模式还绑定对应批次。来源或配置变化不做猜测复用。旧发现标记 `priorRun`,不伪造本轮读取凭据、模型调用或覆盖。变更模式开启阶段续审时,schema2 检查点还可保存已接受但所在文件链未完成的 primary 生成;它只省去匹配的主审生成,综合、交互和复核在新安全边界执行,复核重新取得独立回执。预览分别显示可复用文件、生成及仍需的新鲜复核;不能把保存了正文或候选当作阶段完成。
246
+ 需要补审时,在**新预览**中传入 `resumeFromJobId`,例如 `{"resumeFromJobId":"原执行任务的真实jobId"}`;仍须核对新范围与当前明确续审授权并通过当前宿主权限;不能复用旧批准,明确要求匹配的补审时无需再重复询问。只复用同一会话、工作目录、模型路由/思考设置下,完整快照、配置和运行时身份均匹配的已完成且符合复用条件的文件链;项目模式还绑定对应批次。来源或配置变化不做猜测复用。旧发现标记 `priorRun`,不伪造本轮读取凭据、模型调用或覆盖。变更模式开启阶段续审时,schema2 检查点还可保存已接受但所在文件链未完成的 primary 生成;它只省去匹配的主审生成,综合、交互和复核在新安全边界执行,复核重新取得独立回执。预览分别显示可复用文件、生成及仍需的新鲜复核;不能把保存了正文或候选当作阶段完成。
245
247
 
246
248
  后台审查会在上述宿主任务目录的 `artifacts` 子目录留存有界模型工件,包括失败/取消时已捕获的文本或工具参数;不含 reasoning。它们可能含源码,应按源码保护,不会自动加入提示词、当作已确认发现或在普通状态报告公开。默认单工件 1 MiB、每任务原文总量 8 MiB、最多 4096 条记录;截断由工件标记,存储超限明确返回遗漏标记和无源码诊断。读取绑定原会话、目录、提供方、模型和思考设置。需要查看时,Agent 必须先提示“可能把源码/模型原文带入当前对话”,取得你的明确同意,再调用 `code_review_artifacts({jobId,confirmed:true})`;可用 `offset`/`limit` 分页,默认一次 1 条、最多 100 条。返回内容标为 `sourceBearing:true`、`untrusted:true`,不是批准证据;普通 `status` 不返回工件原文。目录/文件使用 0700/0600 权限模式及原子写入;Windows 模式位不等于额外 ACL 保证。
247
249
 
@@ -277,8 +279,8 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
277
279
  普通使用者只需上面的对话请求,不必手工构造 ID。需要精确控制时,让 Agent 按下列顺序调用:
278
280
 
279
281
  1. 调用 `code_review_preview`,参数可以是 `{}` 或上面的 JSON 示例。
280
- 2. 展示返回的仓库、模型、文件与限制,保留返回的 `previewId`。**此时还没有完成审查。**
281
- 3. 用户确认后,调用 `code_review_execute`:
282
+ 2. 内部核对返回的仓库、模型路由、发送目的地、文件、预算与权限,保留返回的 `previewId`;只预览或需确认时向用户展示。**此时还没有完成审查。**
283
+ 3. 当前人类已明确要求审核且预览与授权一致时,直接调用 `code_review_execute`,不重复询问;只预览、等待确认或超授权时不得直接执行:
282
284
 
283
285
  ```json
284
286
  {"previewId":"替换为本次预览真实返回的ID","confirmed":true}
@@ -312,7 +314,7 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
312
314
  | Agent 没有调用代码审查工具,只给了普通文字分析 | 明确要求调用 `code_review_preview`;若工具不可用,检查插件是否启用并已加载到当前会话 |
313
315
  | 找不到仓库 / 选错仓库 | 检查会话工作目录;多仓库时用 `repositoryPath` 指定子仓库根目录 |
314
316
  | 新文件没有出现 | 用 `selectedPaths` 显式选择未跟踪文件;路径相对于仓库 |
315
- | 预览过期、文件已变化、模型已变化 | 重新预览,核对新范围后再确认,不复用旧 ID |
317
+ | 预览过期、文件已变化、模型已变化 | 新预览并重新核对当前授权,不复用旧 ID 或旧批准;超出授权时询问 |
316
318
  | SVN 历史提示需要网络许可 | 确认可以访问仓库后,预览参数增加 `svnAllowNetwork:true` |
317
319
  | SVN 登录、权限或版本不存在 | 检查 SVN 本身的访问权限和版本号;不要把密码写进对话或仓库 URL |
318
320
  | 文件被阻断或报告 partial | 阅读具体原因;缩小变更批次、补充允许的上下文,或处理真实冲突后重新预览。不能把排除文件视为通过 |
package/RELEASE-NOTES.md CHANGED
@@ -1,21 +1,17 @@
1
- # v0.87.1 发布说明
1
+ # v0.87.3 发布说明
2
2
 
3
- 使用方式见 [README](README.md),历史变化见 [变更记录](CHANGELOG.md),发布门禁见 [开发说明](DEVELOPMENT.md)。
3
+ ## 审核授权与内部预览
4
4
 
5
- ## 修复阶段续审中断
5
+ 明确要求审核(例如“用 dsh-code-review 审核 r7364”)且内部预览与当前授权范围、模型及发送目的地一致时,工具描述和后台预览完成提示指导 Agent 直接继续执行,不再机械要求第二次确认。只要求预览、要求等待,或范围/模型/目的地超出授权时仍须取得授权。
6
6
 
7
- 修复 v0.87.0 中主审已接受的结果被续审缓存再次拒绝,并在文件收尾扩大为整个任务中止的问题。主审、窗口归因及续审校验现从同一批准原文重建大文件包络坐标;包络仍只是保守区域,不代表精确编辑或回归因果。
7
+ 此次修复的是 Agent 工具指引与预览完成提示,不新增一键自动执行 API,也不在后台自行推断自然语言授权。Agent 仍须捕获并核对预览,再传 confirmed=true。
8
8
 
9
- 组成员与组级限制合并后按宿主有界结果保存;模型成员及组级响应各自最多50条的限制不变。阶段生成先校验再入缓存,拒绝或容量不足不污染此前有效记录,也不省略当前候选的独立复核。不能继续的候选明确保持未完成,不通过重新生成掩盖失败。
9
+ ## 保留的安全边界
10
10
 
11
- ## 续审容量与安全边界
11
+ 服务层确认参数、一次性预览、快照新鲜度、有效期、会话/目录/模型身份、宿主工具权限及独立远端许可不变。重启、保存任务或旧批准本身不授权再次外发。私有模型工件仍需展示隐私警告并取得独立明确同意,不能借审核请求读取。
12
12
 
13
- 续审导出超过总字节或节点容量时,按完整记录保留适配部分,披露未保存数量;不截断发现或假装已保存全部进度。省略主审或完整文件链时不继承相关继续状态。未保存部分在后续新授权审查中重新执行。
13
+ ## 验证
14
14
 
15
- 批准快照、身份与原始坐标校验仍严格。非法导入、伪造窗口和属性、访问器等不安全数据继续拒绝;旧复核回执不作为新证据。真实落盘错误及用户取消不被容量退化吞掉。
15
+ 补充同步/后台工具描述与终态预览提示回归测试,覆盖明确授权直接继续、仅预览不执行、超授权需询问及私有工件独立同意。发布以全量测试和四矩阵标签 CI 为门禁。
16
16
 
17
- ## 验证与交付
18
-
19
- 新增实际执行回归覆盖包络窗口开启/关闭阶段继续、非空候选、51/101条合法合并限制、缓存拒绝后继续处理其他文件、断点复用主审但重新独立复核,以及容量、取消和落盘失败。修复冻结验收完整回归1249项:1245通过、0失败、4跳过;关键组合66/66通过,宿主离线smoke通过。这些不是业务项目重跑或真实模型质量、速度保证。
20
-
21
- 正式发布还须通过该版本冻结标签 Ubuntu/Windows × Node.js22/24 四矩阵,并核验 Actions、npm 与 GitHub Release 的同一原始安装包及校验清单。发布不会自动安装、升级或重启当前 DSH。
17
+ 安装与使用见 [README](README.md),历史变化见 [变更记录](CHANGELOG.md),发布门禁见 [开发说明](DEVELOPMENT.md)。发布不等于自动升级或重启当前桌面宿主。
package/index.mjs CHANGED
@@ -6,10 +6,10 @@ import { reviewToolOutput } from './src/tool-view.mjs';
6
6
  export const name = 'dsh-code-review';
7
7
  export const inject = ['tools', 'llm'];
8
8
 
9
- const PREVIEW_DESCRIPTION = 'Preview Git/SVN working-copy or Git commit/range review scope and model destination without sending source to a model. repositoryPath selects an explicit child repository relative to session workspace; file paths are repository-relative. Each session reviews one selected repository independently; reviewMode=project selects tracked current source instead of changes; changeScope=staged or unstaged narrows a Git working-copy review to the index or to unstaged edits. Show the preview before execution.';
9
+ const PREVIEW_DESCRIPTION = 'Preview Git/SVN working-copy or Git commit/range review scope and model destination without sending source to a model. repositoryPath selects an explicit child repository relative to session workspace; file paths are repository-relative. Each session reviews one selected repository independently; reviewMode=project selects tracked current source instead of changes; changeScope=staged or unstaged narrows a Git working-copy review to the index or to unstaged edits. Always capture and inspect the preview internally before execution. An explicit user request to review the selected code already authorizes execution within that request: proceed without asking the user to confirm the same review again. Ask only if the preview exceeds the requested scope or changes the authorized model/destination, or authorization is unclear. Never execute if the user requested preview only or explicitly asked to wait for confirmation. Preserve host tool permissions and all snapshot/identity checks.';
10
10
  const PREVIEW_PARAMETERS = { type: 'object', additionalProperties: false, properties: { resumeFromJobId: { type: 'string', description: 'Explicit terminal job to reuse identity-matching completed chains. Requires a fresh captured preview, confirmation and authorization; does not restore old approval.' }, reviewMode: {type:'string',enum:['changes','project'],description:'project reviews all tracked current files, including unchanged files, in bounded batches with shared session budget. Does not include ignored/untracked/external contents. Each DSH session owns independent preview, lock and model budget.'}, changeScope: {type:'string',enum:['worktree','staged','unstaged'],description:'Git working-copy comparison scope. worktree (default) compares HEAD to the working tree (net change); staged compares HEAD to the index so only staged content is reviewed; unstaged compares the index to the working tree so only unstaged edits are reviewed and staged content is the old side. Requires a Git working copy; mutually exclusive with Git history, SVN parameters and project mode.'}, svnReplacementBaseRevision: {type:'string',description:'Working-copy file replacement only: explicitly compare to this fixed repository revision at the target URL. Not local BASE and not proof of the removed-node revision. Requires separate remote opt-in when applicable; identity and properties frozen in preview.'}, svnReplacementAllowNetwork: {type:'boolean',description:'Explicit permission to read the selected working-copy target URL at svnReplacementBaseRevision. Does not permit arbitrary URLs, updates, merges or writes.'}, svnBasePath: {type:'string',description:'Explicit old endpoint path relative to the current SVN repository root; requires svnTargetPath and fixed revision range. Not an arbitrary URL. Same repository UUID required.'}, svnTargetPath: {type:'string',description:'Explicit new endpoint repository-relative path paired with svnBasePath. Each side content/context stays at its URL/revision; shared capture budgets and remote opt-in apply.'}, svnEffectiveMergeinfo: { type: 'boolean', description: 'Explicit SVN opt-in to bounded BASE/working or fixed-history ancestor mergeinfo property reads on each selected side. Offline for working copies; history uses existing remote permission and budgets. Mixed BASE revisions, unsafe ancestors or incomplete URL boundaries remain unknown. No branch source reads or merge attribution.' }, svnRevision: { type: 'string', description: 'SVN fixed numeric revision reviewed against revision minus one. Requires SVN working-copy root; no HEAD/date aliases.' }, svnBaseRevision: { type: 'string', description: 'SVN fixed numeric old endpoint; requires svnTargetRevision, mutually exclusive with svnRevision and Git revision parameters.' }, svnTargetRevision: { type: 'string', description: 'SVN fixed numeric new endpoint, greater than old endpoint. Fixed repository objects, not working-copy text. Supports explicitly selected target context with verified baseline provenance and target rules; explicit old-only context is also supported with fixed SVN provenance; autoContext follows at most three independent fixed-side dependency layers with bounded parent listings and shared capture budgets; callerScopePaths supports bounded fixed-side SVN historical literal discovery; only matched sides captured, never complete caller coverage.' }, svnAllowNetwork: { type: 'boolean', description: 'Explicit opt-in to read the working-copy repository URL at fixed SVN revisions during preview/execute; preview sends no source to a model. Required for non-file repository URLs; default denies remote access. Does not allow checkout/update/merge or arbitrary URLs.' }, reviewRounds: { type: 'integer', minimum: 1, maximum: 3, description: 'Explicit independent review passes, default 1. Preview discloses extra sends; later passes omit the first risk plan. Shares existing timeout, call and retrieval budgets, not provider reasoning effort. Bound to this preview; execute cannot override.' }, oldContextPaths: { type: 'array', maxItems: 20, items: { type: 'string' }, description: 'Explicit Git/SVN historical baseline-only context paths. Git uses fixed blobs; SVN uses UUID/URL/peg/operative revision/hash. Shares 20 context slots. No target text is inferred; selected old changes reuse original identity, excluded changes cannot reenter. Unsupported on working reviews.' }, callerScopePaths: { type: 'array', minItems: 1, maxItems: 4, items: { type: 'string' }, description: 'Explicit non-root repository-relative directories for bounded tracked literal reverse import/include discovery. Git/SVN working review or fixed Git/SVN historical review. SVN uses offline normal nodes, rejects unsafe ancestor states and special/keyword properties. Historical sides share scan budgets and use object blobs only. Preview reads candidate bodies, including nonmatches; only captured matching context is sent. Navigation only, not complete caller coverage.' }, businessRequirement: { type: 'string', maxLength: 16384, description: 'Optional business constraint text up to 16KiB UTF8; untrusted data, not evidence or permission. Bound to preview and sent in review phases.' }, commit: { type: 'string', description: 'Git commit versus sole parent; root empty, merge requires range.' }, baseRevision: { type: 'string', description: 'Git old endpoint; requires targetRevision, mutually exclusive with commit.' }, targetRevision: { type: 'string', description: 'Git new endpoint, exact tree endpoints not merge-base. Explicit context uses target tree with available baseline provenance; oldContextPaths is baseline-only. Historical caller discovery scans fixed sides; explicit rules use target-tree blobs.' }, repositoryPath: { type: 'string', description: 'Workspace-relative explicit child Git/SVN repository root, forward slashes; omit for current repository.' }, selectedPaths: { type: 'array', items: { type: 'string' }, maxItems: 200 }, autoContext: { type: 'boolean', description: 'Opt in to tracked literal import/include context shown in preview. Working Git follows changed and explicitly selected context sources at most 3 dependency layers under shared 512 exact probes, 20 candidates/context slots, 1MiB source and 30s verification budget. Git history follows at most 3 baseline/target dependency layers with shared limits, object reads only; SVN follows at most 3 safe offline current-side layers with exact path probes and node/ancestor identity verification, sharing limits; no SVN historical review. Not semantic binding or complete definitions.' }, contextPaths: { type: 'array', items: { type: 'string' }, maxItems: 20 }, rulePaths: { type: 'array', items: { type: 'string' }, maxItems: 4 } } };
11
- const EXECUTE_DESCRIPTION = 'Execute a previewed read-only code review using the current DSH model. Sends the approved snapshot to the current Agent model under DSH tool execution policy; no separate plugin approval prompt. Never modifies the reviewed repository.';
12
- const EXECUTE_PARAMETERS = { type: 'object', additionalProperties: false, properties: { previewId: { type: 'string' }, confirmed: { type: 'boolean' } }, required: ['previewId', 'confirmed'] };
11
+ const EXECUTE_DESCRIPTION = 'Execute a previewed read-only code review using the current DSH model. Sends the approved snapshot to the current Agent model under DSH tool execution policy; no separate plugin approval prompt. confirmed=true attests that the current user explicitly requested this review and the inspected preview matches that authorization; it does not require a second confirmation message. Never execute a preview-only or explicit wait-for-confirmation request, or exceed authorized scope/model/destination. Never modifies the reviewed repository.';
12
+ const EXECUTE_PARAMETERS = { type: 'object', additionalProperties: false, properties: { previewId: { type: 'string' }, confirmed: { type: 'boolean', description: 'True only when the current user explicitly authorized this review and the inspected preview matches the requested scope/model/destination. The original explicit review request is sufficient; do not ask again for the same review. Preview-only or explicit wait-for-confirmation requests do not authorize execution.' } }, required: ['previewId', 'confirmed'] };
13
13
 
14
14
  export function apply(ctx, config = {}) {
15
15
  // This entry runs only when the host invokes the registered tool.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.87.1",
3
+ "version": "0.87.3",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -3,8 +3,8 @@ import os from 'node:os';
3
3
  import { createReviewJobs } from './review-jobs.mjs';
4
4
  import { reviewToolOutput } from './tool-view.mjs';
5
5
 
6
- const START_DESCRIPTION = 'Start a durable background preview or confirmed review. Returns a job ID immediately, NOT task completion. Keep the user request active using code_review_wait until terminal status; never final-answer only that a task started. Preview phase never sends source. For execute supply the approved previewId and confirmed=true. Use code_review_status to collect progress/results; code_review_cancel to stop. Does not automatically resume after host restart or grant new source approval.';
7
- const START_PARAMETERS = { type: 'object', additionalProperties: false, properties: { phase: { type: 'string', enum: ['preview', 'execute'] }, previewArgs: { type: 'object', description: 'Same parameters as code_review_preview; do not include execute fields.' }, previewId: { type: 'string' }, confirmed: { type: 'boolean' } }, required: ['phase'] };
6
+ const START_DESCRIPTION = 'Start a durable background preview or confirmed review. Returns a job ID immediately, NOT task completion. Keep the user request active using code_review_wait until terminal status; never final-answer only that a task started. Preview phase never sends source. Always inspect the captured preview internally. An explicit current user request to review the selected code authorizes execution when the preview matches its scope/model/destination: proceed with phase=execute, previewId and confirmed=true without asking for duplicate confirmation. If the user requested preview only or explicitly asked to wait for confirmation, do not execute. Ask for scope expansion, a changed authorized model/destination, or unclear authorization. Host permissions and snapshot/identity checks still apply. Use code_review_status to collect progress/results; code_review_cancel to stop. Does not automatically resume after host restart or grant new source approval.';
7
+ const START_PARAMETERS = { type: 'object', additionalProperties: false, properties: { phase: { type: 'string', enum: ['preview', 'execute'] }, previewArgs: { type: 'object', description: 'Same parameters as code_review_preview; do not include execute fields.' }, previewId: { type: 'string' }, confirmed: { type: 'boolean', description: 'Attest current explicit review authorization after inspecting the preview. A matching explicit review request is sufficient; do not ask again. Preview-only or explicit wait-for-confirmation is not execution authorization.' } }, required: ['phase'] };
8
8
 
9
9
  const STATUS_DESCRIPTION = 'Read a background review task owned by this session, directory and model. Returns saved progress or final preview/report. completed task means the tool returned; inspect nested report coverage/status. Interrupted tasks retain checkpoints but require new preview and approval to resend.';
10
10
  const STATUS_PARAMETERS = { type: 'object', additionalProperties: false, properties: { jobId: { type: 'string' }, offset: { type: 'integer', minimum: 0 }, checkpointIndex: { type: 'integer', minimum: 0, description: 'Read one checkpoint nested files/findings page; offset/limit apply within it.' }, limit: { type: 'integer', minimum: 1, maximum: 100 } }, required: ['jobId'] };
@@ -12,7 +12,7 @@ const STATUS_PARAMETERS = { type: 'object', additionalProperties: false, propert
12
12
  const CANCEL_DESCRIPTION = 'Cancel a background review task owned by this session. Does not discard saved checkpoints. No source is sent by cancellation.';
13
13
  const CANCEL_PARAMETERS = { type: 'object', additionalProperties: false, properties: { jobId: { type: 'string' } }, required: ['jobId'] };
14
14
 
15
- const WAIT_DESCRIPTION = 'Wait for review progress or terminal result without ending the user request. Prefer waitFor=terminal to ignore intermediate checkpoints; progress preserves checkpoint-by-checkpoint compatibility. Reuse the exact returned waitRevision as after; never guess increments or call status after every event. Timeout defaults to 60000ms and is bounded to 60000ms; changed=false means timeout, not completion or cancellation. Call again while queued/running. Preview completion requires confirmation; execute completion returns one final consolidated report.';
15
+ const WAIT_DESCRIPTION = 'Wait for review progress or terminal result without ending the user request. Prefer waitFor=terminal to ignore intermediate checkpoints; progress preserves checkpoint-by-checkpoint compatibility. Reuse the exact returned waitRevision as after; never guess increments or call status after every event. Timeout defaults to 60000ms and is bounded to 60000ms; changed=false means timeout, not completion or cancellation. Call again while queued/running. Preview completion is not review completion. If the current user explicitly requested the matching review, proceed to execute with confirmed=true without asking again; preview-only, explicit wait-for-confirmation or out-of-authorization scope/model/destination requires user authorization; execute completion returns one final consolidated report.';
16
16
  const WAIT_PARAMETERS = { type: 'object', additionalProperties: false, properties: { jobId: { type: 'string' }, after: { type: 'integer', minimum: 0, description: 'Exact waitRevision returned by start/status/wait; do not increment it yourself.' }, waitFor: { type: 'string', enum: ['progress', 'terminal'], default: 'progress', description: 'Use terminal to wait through intermediate saves; progress wakes on each durable checkpoint.' }, timeoutMs: { type: 'integer', minimum: 1, maximum: 60000, default: 60000 } }, required: ['jobId'] };
17
17
 
18
18
  const TERMINAL_STATUSES = ['completed', 'failed', 'cancelled', 'interrupted'];
@@ -36,14 +36,19 @@ function terminalSummary(value) {
36
36
  }
37
37
  const reasons = Array.isArray(d?.readerLimitReasons) ? d.readerLimitReasons : [];
38
38
  const bounded = LIMIT_CODES.flatMap(code => { const row = reasons.find(item => item?.code === code); return row ? [code + ' ' + count(row.calls)] : []; });
39
- if (bounded.length) lines.push('读取限制原因:' + bounded.join(';'));
39
+ if (bounded.length) {
40
+ lines.push('读取限制原因:' + bounded.join(';'));
41
+ if (d?.readerBudget?.remainingCalls > 0 && d?.readerBudget?.remainingOutputBytes > 0) lines.push('全局读取预算仍有余额;阶段分配或检索轮次限制仍可能使范围未完成,不能据余额认定可以继续或已经通过。');
42
+ }
40
43
  if (d?.readerLimits) lines.push('每阶段最大检索轮次:' + count(d.readerLimits.maxRounds));
41
44
  if (d?.artifacts) lines.push('私有工件计数:留存 ' + count(d.artifacts.retained) + ',未留存 ' + count(d.artifacts.omitted) + '(不包含原始正文)');
42
45
  if (value.resultPage) lines.push('当前页:文件 ' + (value.resultPage.files?.length ?? 0) + '/' + count(value.resultPage.totalFiles) + ',发现 ' + (value.resultPage.findings?.length ?? 0) + '/' + count(value.resultPage.totalFindings) + ';code_review_status 的 offset/limit 仅切换结果页。');
43
46
  lines.push('完整报告:调用一次 code_review_wait(jobId 同上,waitFor=terminal)立即取得最终报告;无需继续轮询。');
44
47
  } else if (report?.previewId) {
45
- lines.push('预览已完成,尚未执行审查;请展示批准范围并取得用户确认后再执行。');
48
+ lines.push('预览已完成,尚未执行审查;请先内部核对范围、模型与发送目的地。用户已明确要求审核且预览符合该授权时,直接以 confirmed=true 继续执行,不要重复询问确认。仅要求预览时不得执行;明确要求等待确认时也不得执行。超出原授权范围、改变授权模型/发送目的地或授权边界不明确时才询问。宿主权限及快照/身份校验仍须通过。');
46
49
  lines.push('预览文件总量:' + count(report.filePage?.total ?? report.files?.length) + ';计划最少调用:' + count(report.plan?.minimumCalls));
50
+ lines.push('最少调用仅为初始计划下限,不是总调用数或耗时上限;后续取证、复核及综合会增加工作量。');
51
+ if (typeof report.expiresAt === 'string' && Number.isFinite(Date.parse(report.expiresAt))) lines.push('预览有效至:' + new Date(report.expiresAt).toISOString() + ';过期后须重新预览并核对当前授权,不得复用旧预览或恢复旧批准。');
47
52
  } else {
48
53
  lines.push('未提供完整报告摘要;终态不会自动恢复或重新发送源码。请检查已保存检查点和未完成范围。');
49
54
  }
@@ -4,6 +4,33 @@ const cpp = name => /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/.test(name);
4
4
  const id = '[A-Za-z_][A-Za-z0-9_]*';
5
5
  const signature = params => params.trim().replace(/\s+/g, ' ').replace(/\s*([*&])\s*/g, '$1');
6
6
 
7
+ // Evidence coordinates only, never resolved C++ call or reachability identities.
8
+ export function cppExportRouteDefinitions(text) {
9
+ if (typeof text !== 'string' || Buffer.byteLength(text) > 256 * 1024 || /\/\*|R"|\\\r?\n|[\x60]/.test(text)) return [];
10
+ const lines = text.split('\n').map(line => line.replace(/\r$/, '').replace(/\/\/.*$/, ''));
11
+ if (lines.some(line => /^\s*#\s*(?!include\b)/.test(line) || /\b(?:namespace|template|using|typedef)\b/.test(line))) return [];
12
+ const output = [];
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const match = /^\s*(?:(extern\s+"C")\s+(?:[A-Z_]\w*\s+)?)?(void|bool|int)\s+((?:[A-Za-z_]\w*::)?[A-Za-z_]\w*)\s*\(([^(){};]*)\)\s*(\{)?\s*$/.exec(lines[i]);
15
+ if (!match || !match[1] && !match[3].includes('::')) continue;
16
+ const params = match[4].split(',').map(p => p.trim());
17
+ if (!params.every(p => /^(?:const\s+)?[A-Za-z_]\w*\s*[*&]?\s+[A-Za-z_]\w*$/.test(p))) continue;
18
+ const brace = match[5] ? i : i + 1;
19
+ if (!match[5] && !/^\s*\{\s*$/.test(lines[brace] ?? '')) continue;
20
+ let depth = 1, end;
21
+ for (let j = brace + 1; j < lines.length; j++) {
22
+ if (/['"\\]/.test(lines[j])) break;
23
+ for (const c of lines[j]) { if (c === '{') depth++; else if (c === '}') depth--; }
24
+ if (depth === 0) { end = j + 1; break; }
25
+ if (depth < 0) break;
26
+ }
27
+ if (end) output.push({ symbol: match[3], exported: Boolean(match[1]), params: signature(match[4]),
28
+ names: params.map(p => /([A-Za-z_]\w*)$/.exec(p)[1]), start: i + 1, bodyStartLine: brace + 2, end, lines: lines.slice(brace + 1, end - 1) });
29
+ if (output.length > 100) return [];
30
+ }
31
+ return output;
32
+ }
33
+
7
34
  function linesOf(text, header) {
8
35
  if (typeof text !== 'string' || Buffer.byteLength(text) > 256 * 1024 || /\/\*|R"|\\\r?\n|[\x60]/.test(text)) return undefined;
9
36
  const lines = text.split('\n').map(line => line.replace(/\r$/, ''));
@@ -36,6 +63,9 @@ function headers(entry) {
36
63
  if (!plain) continue;
37
64
  if (/^(?:public|private|protected):$/.test(plain)) { access = plain.slice(0, -1); continue; }
38
65
  const method = /^(void|bool|int)\s+([A-Za-z_]\w*)\s*\(([^;{}]*)\)\s*;\s*$/.exec(plain);
66
+ // Plain fields do not bind calls; they only allow the containing declaration
67
+ // to remain navigable. Initializers, aliases and function pointers stay unsupported.
68
+ if (!method && /^(?:bool|int|float|double|[A-Za-z_]\w*\s*[*&]?)\s+[A-Za-z_]\w*\s*;$/.test(plain)) continue;
39
69
  if (!method || /[=<>]|\bconst\b/.test(method[3])) { methods = undefined; break; }
40
70
  methods.push({ name: method[2], returnType: method[1], params: signature(method[3]), line: j + 1, access });
41
71
  }
@@ -1,11 +1,14 @@
1
1
  import { preparePrimaryInput } from './primary-input.mjs';
2
2
  import { initialInputBudget } from './input-budget.mjs';
3
- import { INLINE_RISK_INSTRUCTIONS } from './prompts.mjs';
4
3
  import { validateRiskPlan } from './risk-plan.mjs';
5
4
  import { GROUP_INTERACTION_INSTRUCTIONS } from './group-interaction.mjs';
6
5
 
7
6
  export const GROUP_PRIMARY_INSTRUCTIONS = '批准的group-primary输入包含多个完整主文件。逐个审核所有reviews成员的新旧正文,并结合组内其他文件分析;不能仅审核一个文件。返回 {"reviews":[{"fileId":"原成员身份","findings":[],"limitations":[]}],"limitations":[]},每个成员恰好一项,包括没有发现的成员。各finding仍需原审查指令全部字段、精确原行anchor及该fileId本次变化归因;不能用其他成员的身份或坐标替代。缺证据写limitations,不声明跨文件综合已完成;专门交互阶段仍独立校验。可按批准检索协议先返回requests。';
8
7
 
8
+ // Group output has a different envelope from single-file inline review. Keep the
9
+ // required risk field on each member, without inheriting optional/omit guidance.
10
+ const GROUP_PRIMARY_RISK_INSTRUCTIONS = '每个reviews成员必须包含risks字段(数组,可为[]),禁止省略或放到顶层;没有真实风险或精确编辑时返回risks:[],不要为满足字段要求编造风险。组风险可引用本组所有成员的精确编辑,不增加规划调用。风险仅为待证假设,不是证据、缺陷或扩大范围/读取权限的授权;sourceIds不证明已读。先审核全部成员编辑与反证,再报告findings。中间可按批准检索协议返回requests,但不得与最终结果混用。Risk shape: {id:"r1",fileIds:["reviews[].fileId"],editRefs:[{fileId:"reviews[].fileId",editId:"changes.edits[].id"}],question:"Risk?",trigger:"Condition",impact:"Failure",checks:["Countercheck"],sourceIds:[]}. Exactly these 8 keys, all required. Each member risks: 0..20; serialized {risks} JSON UTF-8<=65536 bytes. id/question/trigger/impact and each check: nonblank string, JS length<=2000; id unique within each member risks. fileIds: 1..4 unique approved group member IDs. editRefs: 1..20 unique objects with ONLY string fileId/editId; fileId in this risk.fileIds and editId owned by that file. checks: 1..8 unique strings. sourceIds: 0..8 unique catalog IDs (no catalog: []), never paths. Copy real IDs, never guess.';
11
+
9
12
  export function prepareGroupPrimary(snapshot, group, { instructions, scope, maxInputBytes, focusPrimaryContext = false, enableRiskPlanning = false, enableGroupInteractions = false, migrationContracts = [] } = {}) {
10
13
  if (typeof enableGroupInteractions !== 'boolean') throw new Error('Invalid group interaction option');
11
14
  if (!scope || snapshot.projectScope || !Array.isArray(group?.fileIds) || group.fileIds.length < 2 || group.fileIds.length > 4 || new Set(group.fileIds).size !== group.fileIds.length) return null;
@@ -18,7 +21,8 @@ export function prepareGroupPrimary(snapshot, group, { instructions, scope, maxI
18
21
  for (const item of reviews) { delete item.context; delete item.rules; delete item.businessRequirement; delete item.requirementNotice; }
19
22
  const value = { snapshotId: snapshot.id, sourceMode: 'group-primary', groupId: group.id, reviews, context, rules: snapshot.rules ?? [], ...(snapshot.businessRequirement !== undefined ? { businessRequirement: snapshot.businessRequirement } : {}) };
20
23
  let payload = JSON.stringify(value);
21
- let combined = instructions + '\n' + GROUP_PRIMARY_INSTRUCTIONS + (enableRiskPlanning ? '\n各成员可附带risks;组风险可引用所有成员精确编辑,仍只是假设,不增加规划调用。' + INLINE_RISK_INSTRUCTIONS.replace('Final JSON requires findings/limitations;', 'Each reviews member requires findings/limitations;') : '');
24
+ const groupInstructions = enableRiskPlanning ? GROUP_PRIMARY_INSTRUCTIONS.replace('"findings":[],"limitations":[]}', '"findings":[],"limitations":[],"risks":[]}') + '\n' + GROUP_PRIMARY_RISK_INSTRUCTIONS : GROUP_PRIMARY_INSTRUCTIONS;
25
+ let combined = instructions + '\n' + groupInstructions;
22
26
  if (!initialInputBudget({ input: payload, instructions: combined }, scope, maxInputBytes).fits) return null;
23
27
  const contracts = migrationContracts.filter(contract => contract.candidateFileIds?.some(id => group.fileIds.includes(id)));
24
28
  if (contracts.length) { const proposed = JSON.stringify({ ...JSON.parse(payload), migrationContracts: contracts, migrationNotice: 'Host navigation hypotheses only. Compare the old public gate, new entry and callers under the same trigger. Neither semantics nor causality are established; independent verification is required.' }); if (initialInputBudget({ input: proposed, instructions: combined }, scope, maxInputBytes).fits) payload = proposed; }
package/src/line-diff.mjs CHANGED
@@ -1,5 +1,46 @@
1
1
  import { changeRegion } from './change-region.mjs';
2
2
 
3
+ // Myers frontier search avoids quadratic rectangles for sparse, distant edits.
4
+ // Charge both retained trace cells and equality probes to the same hard budget;
5
+ // exhaustion discards the entire script, never promoting a partial alignment.
6
+ function boundedMatches(a, b, offset, n, m, budget) {
7
+ const trace = [];
8
+ let used = 0;
9
+ for (let d = 0; d <= n + m; d++) {
10
+ if (used + d + 1 > budget) return null;
11
+ used += d + 1;
12
+ const row = new Int32Array(d + 1), previous = trace.at(-1);
13
+ for (let p = 0; p <= d; p++) {
14
+ const k = 2 * p - d;
15
+ let x = d === 0 ? 0 : p === 0 ? previous[0] : p === d ? previous[p - 1] + 1
16
+ : previous[p - 1] < previous[p] ? previous[p] : previous[p - 1] + 1;
17
+ let y = x - k;
18
+ while (x < n && y < m) {
19
+ if (++used > budget) return null;
20
+ if (a[offset + x] !== b[offset + y]) break;
21
+ x++; y++;
22
+ }
23
+ row[p] = x;
24
+ if (x >= n && y >= m) {
25
+ const matches = [];
26
+ for (let depth = d; depth > 0; depth--) {
27
+ const prev = trace[depth - 1], index = (x - y + depth) / 2;
28
+ const down = index === 0 || (index !== depth && prev[index - 1] < prev[index]);
29
+ const prevX = prev[down ? index : index - 1];
30
+ const prevY = prevX - (x - y + (down ? 1 : -1));
31
+ const count = Math.min(x - prevX, y - prevY);
32
+ if (count > 0) matches.push([x - count, y - count, count]);
33
+ x = prevX; y = prevY;
34
+ }
35
+ if (x > 0) matches.push([0, 0, x]);
36
+ return matches.reverse();
37
+ }
38
+ }
39
+ trace.push(row);
40
+ }
41
+ return null;
42
+ }
43
+
3
44
  export function lineDiff(before, after, { maxCells = 200000, ...options } = {}) {
4
45
  if (!Number.isSafeInteger(maxCells) || maxCells < 1 || maxCells > 2000000) throw new RangeError('Invalid cell limit');
5
46
  const region = changeRegion(before, after, options);
@@ -7,7 +48,22 @@ export function lineDiff(before, after, { maxCells = 200000, ...options } = {})
7
48
  const split = text => text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
8
49
  const a = split(before), b = split(after);
9
50
  const offset = region.old.start - 1, n = region.old.count, m = region.new.count;
10
- if ((n + 1) * (m + 1) > maxCells) return { status: 'limited', reason: 'Diff cell limit exceeded', envelope: region, edits: [] };
51
+ if ((n + 1) * (m + 1) > maxCells) {
52
+ const matches = boundedMatches(a, b, offset, n, m, maxCells);
53
+ if (!matches) return { status: 'limited', reason: 'Diff cell limit exceeded', envelope: region, edits: [] };
54
+ const edits = [];
55
+ let i = 0, j = 0;
56
+ for (const [nextI, nextJ, count] of [...matches, [n, m, 0]]) {
57
+ if (nextI > i || nextJ > j) edits.push({
58
+ old: { start: offset + i + 1, count: nextI - i },
59
+ new: { start: offset + j + 1, count: nextJ - j },
60
+ removed: a.slice(offset + i, offset + nextI).join(''),
61
+ added: b.slice(offset + j, offset + nextJ).join(''),
62
+ });
63
+ i = nextI + count; j = nextJ + count;
64
+ }
65
+ return { status: 'changed', precision: 'exact', edits };
66
+ }
11
67
  const width = m + 1, table = new Uint32Array((n + 1) * width);
12
68
  for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) {
13
69
  table[i * width + j] = a[offset + i] === b[offset + j] ? 1 + table[(i + 1) * width + j + 1] : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
@@ -3,7 +3,7 @@ import { createRetrievalScope } from './retrieval-scope.mjs';
3
3
  import { inferImportRelations } from './import-relations.mjs';
4
4
  import { inferCppRelations } from './cpp-relations.mjs';
5
5
  import { buildReviewGroups } from './review-groups.mjs';
6
- import { cppMigrationBindings } from './cpp-migration-bindings.mjs';
6
+ import { cppMigrationBindings, cppExportRouteDefinitions } from './cpp-migration-bindings.mjs';
7
7
 
8
8
  const NOTICE = 'Conservative syntax navigation only; not semantic migration, call reachability, authorization, read coverage or causality proof.';
9
9
  const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
@@ -148,9 +148,93 @@ export function buildMigrationContracts(snapshot, { maxContracts = 32, maxGroupF
148
148
  }
149
149
  }
150
150
  }
151
+ // An old public route may stop delegating altogether. Retain that old-side
152
+ // edge and compare only the same approved file/function coordinates on the
153
+ // new side; a matching name in another file is never a migration binding.
154
+ for (const bridge of graph.old) {
155
+ const enclosing = (definitions.get('old:' + bridge.from) ?? []).filter(d => bridge.callLine > d.start && bridge.callLine < d.end);
156
+ if (enclosing.length !== 1 || bridge.from === bridge.to) continue;
157
+ const previous = enclosing[0];
158
+ const oldText = byId.get(bridge.from)?.left?.text;
159
+ if (!oldText || !oldText.split('\n').slice(previous.start, bridge.callLine - 1).some(line => /^\s*if\s*\(/.test(line))) continue;
160
+ const next = (definitions.get('new:' + bridge.from) ?? []).filter(d => d.symbol === previous.symbol);
161
+ const target = (definitions.get('old:' + bridge.to) ?? []).find(d => d.start === bridge.declarationLine);
162
+ const oldRole = role(bridge.from, 'old', previous), newRole = next.length === 1 ? role(bridge.from, 'new', next[0]) : undefined;
163
+ const targetSource = source(bridge.to, 'old');
164
+ if (!oldRole || !newRole || !target || !targetSource) continue;
165
+ const key = JSON.stringify(['old-delegation', bridge.from, previous.symbol, bridge.to, bridge.symbol]);
166
+ if (seen.has(key)) continue;
167
+ seen.add(key);
168
+ if (result.contracts.length >= maxContracts) { unresolved.add('Migration contract limit reached; additional chains remain unresolved.'); continue; }
169
+ const baseline = { ...(byId.has(bridge.to) ? { fileId: bridge.to } : {}), sourceId: targetSource.id, side: targetSource.side,
170
+ path: byId.has(bridge.to) ? byId.get(bridge.to).oldPath ?? byId.get(bridge.to).path : targetSource.path,
171
+ sourcePath: targetSource.path, symbol: target.symbol, line: target.start, endLine: target.end, editIds: [] };
172
+ Object.assign(oldRole, { startLine: previous.start, endLine: previous.end });
173
+ Object.assign(newRole, { startLine: next[0].start, endLine: next[0].end });
174
+ baseline.startLine = target.start;
175
+ const callers = [caller(bridge, 'old')];
176
+ const navigationBindings = [...(previous.navigation ? [previous.navigation] : []), ...(next[0].navigation ? [next[0].navigation] : []),
177
+ ...(target.navigation ? [target.navigation] : []), ...(bridge.navigationBindings ?? [])];
178
+ result.contracts.push({ id: 'mc' + (result.contracts.length + 1), kind: 'old-delegation', snapshotId: snapshot.id,
179
+ candidateFileIds: [bridge.from], old: [oldRole], new: [newRole], baseline: [baseline], callers, navigationBindings,
180
+ sourceIds: [...new Set([oldRole.sourceId, newRole.sourceId, baseline.sourceId, ...callers.map(c => c.sourceId)])],
181
+ status: 'navigation-only', causality: 'unverified',
182
+ unresolved: ['Same-file entry continuity is syntax-only; delegation removal, guard ordering and shared-state effects require independent evidence.'] });
183
+ }
184
+ // Deleted exports cannot be paired by spelling or filename. Retain a bounded
185
+ // candidate route only when explicit singleton spelling, full parameter signature,
186
+ // a shared assignment and forwarding syntax independently connect all three roles.
187
+ const routeDefs = { old: [], new: [] };
188
+ for (const side of ['old', 'new']) for (const file of snapshot.files.filter(f => f.eligibility === 'reviewable' && /\.(?:cpp|cc|cxx)$/.test(f.path))) {
189
+ const text = (side === 'old' ? file.left : file.right)?.text;
190
+ const parsed = cppExportRouteDefinitions(text);
191
+ routeDefs[side].push(...parsed.map(def => ({ ...def, fileId: file.id })));
192
+ if (side === 'old') {
193
+ const starts = new Set(parsed.filter(d => d.exported).map(d => d.start));
194
+ for (const [index, line] of (text ?? '').split('\n').entries()) if (/extern\s+"C"[^\n]*\(/.test(line) && !starts.has(index + 1))
195
+ unresolved.add('Old public export syntax unsupported at ' + (file.oldPath ?? file.path) + ':L' + (index + 1) + '; old entry navigation unavailable.');
196
+ }
197
+ }
198
+ if (routeDefs.old.length > 2000 || routeDefs.new.length > 2000) {
199
+ unresolved.add('Deleted old public export route definition budget exceeded; candidate navigation unavailable.');
200
+ routeDefs.old = []; routeDefs.new = [];
201
+ }
202
+ for (const previous of routeDefs.old.filter(d => d.exported)) {
203
+ const oldRole = role(previous.fileId, 'old', previous);
204
+ if (!oldRole || routeDefs.new.some(d => d.symbol === previous.symbol && d.params === previous.params && d.exported)) continue;
205
+ const missing = 'Deleted old public entry ' + oldRole.path + ':L' + previous.start + ' (' + previous.symbol + '): ';
206
+ const calls = previous.lines.flatMap((line, index) => {
207
+ const m = /^\s*([A-Za-z_]\w*)::GetInstance\(\)->([A-Za-z_]\w*)\(([^;{}]*)\);\s*$/.exec(line);
208
+ return m ? [{ symbol: m[1] + '::' + m[2], args: m[3].split(',').map(a => a.trim()), line: previous.bodyStartLine + index }] : [];
209
+ });
210
+ if (calls.length !== 1) { unresolved.add(missing + 'singleton delegation unsupported or ambiguous.'); continue; }
211
+ const call = calls[0];
212
+ const targets = routeDefs.old.filter(d => d.symbol === call.symbol && d.params === previous.params && d.names.length === call.args.length);
213
+ if (targets.length !== 1 || !call.args.every(a => /^[A-Za-z_]\w*(?:\.c_str\(\))?$/.test(a))) {
214
+ unresolved.add(missing + 'old implementation identity/signature unresolved.'); continue;
215
+ }
216
+ const target = targets[0];
217
+ const assignments = target.lines.map(line => /^\s*([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*)\s*;\s*$/.exec(line)).filter(Boolean)
218
+ .filter(m => target.names.includes(m[2]));
219
+ const next = routeDefs.new.filter(d => !d.exported && d.params === previous.params && assignments.some(m => d.lines.some(line => line.trim() === m[1] + ' = ' + m[2] + ';')) &&
220
+ d.lines.some(line => line.trim() === 'return ' + d.symbol.split('::').at(-1) + '(' + d.names.slice(0, -1).join(', ') + ');'));
221
+ if (next.length !== 1) { unresolved.add(missing + 'new entry candidate unsupported or ambiguous; no same-name fallback.'); continue; }
222
+ const current = next[0], newRole = role(current.fileId, 'new', current), src = source(target.fileId, 'old');
223
+ if (!newRole || !src) { unresolved.add(missing + 'changed candidate or historical implementation source unavailable.'); continue; }
224
+ if (result.contracts.length >= maxContracts) { unresolved.add(missing + 'contract budget exceeded.'); continue; }
225
+ Object.assign(oldRole, { startLine: previous.start, endLine: previous.end });
226
+ Object.assign(newRole, { startLine: current.start, endLine: current.end });
227
+ const baseline = { fileId: target.fileId, sourceId: src.id, side: 'old', path: byId.get(target.fileId).oldPath ?? byId.get(target.fileId).path,
228
+ sourcePath: src.path, symbol: target.symbol, line: target.start, startLine: target.start, endLine: target.end, editIds: [] };
229
+ result.contracts.push({ id: 'mc' + (result.contracts.length + 1), kind: 'deleted-export-route', snapshotId: snapshot.id,
230
+ old: [oldRole], new: [newRole], baseline: [baseline], candidateFileIds: [...new Set([previous.fileId, current.fileId])],
231
+ callers: [{ ...oldRole, line: call.line, callLine: call.line, editIds: [], targetFileId: target.fileId, targetSymbol: target.symbol }],
232
+ navigationBindings: [], sourceIds: [...new Set([oldRole.sourceId, newRole.sourceId, baseline.sourceId])], status: 'navigation-only', causality: 'unverified',
233
+ unresolved: ['Deleted export route is lexical candidate navigation only; singleton resolution, macros, transformed arguments, shared-state identity and cross-file causality are unsupported. Non-uncertain classification is not permitted.'] });
234
+ }
151
235
  for (const contract of result.contracts) contract.sourceIds = [...new Set([...contract.sourceIds, ...contract.navigationBindings.map(row => row.sourceId)])];
152
- const edges = result.contracts.flatMap(c => [{ from: c.old[0].fileId, to: c.new[0].fileId, reason: 'migration-contract:' + c.id },
153
- ...c.callers.filter(row => row.fileId && row.fileId !== row.targetFileId).map(row => ({ from: row.fileId, to: row.targetFileId, reason: 'migration-caller:' + c.id }))]);
236
+ const edges = result.contracts.flatMap(c => [...(c.old[0].fileId !== c.new[0].fileId ? [{ from: c.old[0].fileId, to: c.new[0].fileId, reason: 'migration-contract:' + c.id }] : []),
237
+ ...c.callers.filter(row => row.fileId && byId.has(row.targetFileId) && row.fileId !== row.targetFileId).map(row => ({ from: row.fileId, to: row.targetFileId, reason: 'migration-caller:' + c.id }))]);
154
238
  const involved = new Set(edges.flatMap(e => [e.from, e.to]));
155
239
  const grouped = buildReviewGroups(snapshot.files.filter(f => involved.has(f.id)), edges, { maxFiles: maxGroupFiles });
156
240
  result.groups = grouped.groups; result.links = grouped.links;