@feng0531/dsh-code-review 0.85.11 → 0.86.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 CHANGED
@@ -2,6 +2,24 @@
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.86.0 — 原生结构化输出、私有留存、断点续审与组主审
6
+
7
+ - 优先使用真实宿主原生结果工具并保留同次文本兼容;只接受未改写的完整 JSON,歧义失败关闭,证据校验不放宽。
8
+ - 后台私有保存有界原始输出,超限明确省略;显式隐私同意后可按所有者读取,不进入普通诊断或自动复用为证据。
9
+ - 完整快照与配置身份匹配后,重新预览、确认、授权才能复用已完成文件链;历史证据标注既往轮次,变更和项目模式均支持。
10
+ - 分组前置避免首文件链串行阻塞;小型完整精确变更组共享一次主审,保留独立交互证据及候选复核,输入预算与预览使用真实原生传输字节。
11
+ - 修复调用额度耗尽后丢弃已生成组结果,以及取消与落盘失败同时发生时丢失原取消身份的边界。
12
+
13
+ ## 0.85.13 — 修复大上下文下的无效分窗和额外规划
14
+
15
+ 0.85.12标签冻结后发现离线基准无baseline入口断言错误,未正式发布且不移动标签;本版修正入口并重新验证发布。
16
+
17
+ - 主审优先保留主文件全文,附带有关系的有界完整上下文;其他批准来源保留版本目录按需读取,防止整批上下文把小文件挤进窗口。
18
+ - 精确覆盖全部编辑的单批窗口可合并首轮风险问题;多批、包络差异和项目模式不放宽,保留独立候选复核。
19
+ - 搜索命中最多附读两段准确原文以减少一轮搜索到读取往返,原预算、身份/哈希和版本侧不变,超限回滚。
20
+ - 增加标准JSON转义提示和无源码纠正原因统计;预算否决不算执行。区分单阶段轮数与共享读取额度,保留真实partial。
21
+ - 大上下文离线夹具对照冻结v0.85.11总调用25→17,八次独立复核保留;不是实际耗时或召回率保证。
22
+
5
23
  ## 0.85.11 — 减少模型往返与证据预算准入
6
24
 
7
25
  - 完整单批变更将风险问题合并入首轮主审;保留多窗口与跨文件独立规划、后续独立轮及候选复核,缺失可选字段不补发调用。
@@ -9,7 +9,10 @@
9
9
  | Git/SVN变更采集 | Git工作副本净变化/暂存区/未暂存范围、固定历史端点、同仓库SVN分支比较 | 任意SVN状态均可审查、完整提交逐一覆盖 |
10
10
  | 范围与来源保护 | 先预览后确认,固定来源与哈希,执行前重新采集 | 普通源码一定没有秘密、模型提供方不保存数据 |
11
11
  | 上下文 | 显式文件、旧侧文件、最多三层字面依赖、限定目录引用方 | 完整语义依赖图、调用图或动态执行可达性 |
12
- | 输入与调度 | 差异窗口、分组和失败回退,共享预算 | 大仓库一定能在预算内完成 |
12
+ | 输入与调度 | 独立业务分组、2~4 完整文件组主审、差异窗口和发送前回退;原生工具/契约字节纳入共享输入预算 | 组主审替代专门交互证据、大仓库一定能完成或必然提速 |
13
+ | 结果传输 | 原生结果工具,同次文本兼容;仅原生模式接受完整 JSON 后明确尾注 | 修补损坏 JSON、猜字段/身份、无歧义解析所有输出 |
14
+ | 显式续审 | 新预览通过完整快照、配置和路由身份核验后复用已完成合格文件链 | 恢复旧授权、变化后无条件复用、未完成阶段已覆盖 |
15
+ | 私有模型工件 | 宿主任务目录外于业务工作区,有界保存文本/工具参数及失败工件,普通报告只给摘要 | 工件成为批准来源、失败草稿成为确认发现、Windows 模式位保证 ACL |
13
16
  | 问题复核 | 原文位置检查、模型反证与状态记录 | 缺陷必然成立、无漏报或无误报 |
14
17
  | 文件指导 | 语言、协议、配置与模板的保守检查提示 | 编译器、类型检查器或真实运行结果 |
15
18
 
@@ -20,6 +23,8 @@
20
23
  - 替换旧侧可采用用户明确选择的固定仓库基线,但这不等于恢复了丢失的本地BASE。
21
24
  - 所有测试和安装验证只证明对应机制的行为,不能外推真实模型质量。当前没有独立质量基准成绩;历史试跑见 [评测说明](evaluation/README.md)。
22
25
  - 不执行项目代码,不自动修复、提交、发布业务项目,也不建设独立模型计费或管理平台。
26
+ - 续审不复用任意未完成中间对话;旧发现保留 priorRun 来源,不能冒充本轮证据。工件可能含源码,默认限额和显式取回边界见 [README](README.md)。
27
+ - 本轮机制变更不等于完整上游能力/质量对齐;没有真实模型质量、速度或成本的新基准结论,也不以文档更新声明已发布。
23
28
 
24
29
  ## 如何查看版本证据
25
30
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  供维护者核查实现依据。使用方法见 [README](README.md),开发优先级见 [能力清单](CORE-ALIGNMENT-TASKS.md)。
4
4
 
5
- 参考版本固定为 alibaba/open-code-review 提交 2d67596c961f80436deb2afa643efa2b1725d34c,不随上游最新分支变化。
5
+ 当前本轮对照固定为 alibaba/open-code-review 提交 `375f64cd6f67a48a135da83cfcf59c52669a09cb`,不随上游分支变化。下方文件索引仍是旧提交 `2d67596c961f80436deb2afa643efa2b1725d34c` 的历史阅读记录;未把旧记录冒充新提交逐文件复审。
6
6
 
7
7
  ## 对照结论
8
8
 
@@ -25,7 +25,9 @@
25
25
  - 默认一次审查,可选三次,不照搬参考默认次数和对话继承方式。
26
26
  - 历史重命名按删除和新增表达;区间比较使用明确端点,不自动计算合并基点。
27
27
  - 参考中的风格偏好、无条件性能建议和错误绝对规则不作为缺陷标准。
28
- - 不建会话存储、断点续跑与豁免:参考的 reused/waived 覆盖集合与续跑流程属会话/历史管理,单次只读审查不纳入。
28
+ - 现已提供身份绑定的任务检查点与显式续审:新预览和新确认后,只复用完整快照/配置/路由一致的已完成合格文件链;不是上游任意会话/覆盖集合恢复,也没有 waived 豁免机制。
29
+ - 生产模型传输使用原生结果工具和同次文本兼容,专门交互校验仍独立;不宣称“整段 JSON 必须完全有效”本身优于上游。仅原生模式容忍完整 JSON 后明确自然语言尾注,歧义或损坏内容仍失败关闭。
30
+ - 本插件后台任务留存有界私有模型工件,普通诊断不公开原文。上游原始响应日志是通过 `OCR_RAW_LOGGING=1` 显式启用的选项,不能描述为无条件默认记录;两者的隐私和恢复边界不同。
29
31
  - 不做 pre-run token 预估警告:用共享输入字节与调用预算在运行中约束,实际覆盖以报告终态为准。
30
32
  - 未覆盖项保留 pending 并报 partial,不扫成 failed:诚实暴露未完成,不伪造终态。
31
33
 
package/DEVELOPMENT.md CHANGED
@@ -26,6 +26,16 @@
26
26
 
27
27
  运行 `node scripts/changes-speed-benchmark.mjs [baseline-directory]` 比较真实 review 入口上的 8 文件 SVN 受控候选/反驳夹具,包含 receipt 复核与最终精确原文校验;每次模型响应模拟 20ms。可先将冻结版本的 src 解包到独立目录作为 baseline。比较调用数、峰值并发和原文证据,不把离线计时或 mock 结论一致当作真实质量、token 成本或 45→12 分钟保证。`model-start/model-end` 检查点只记录计时、字节、状态及阶段,不写入源码或推理;各文件 `modelMetrics` 是提供方实际调用(含纠错)累计时间,并行时不可相加当作墙钟耗时。
28
28
 
29
+ ### 大上下文回归与纠正记账
30
+
31
+ 正式变更入口默认 `focusPrimaryContext:true`,纯准备阶段只附带直接关系、C#和调用方线索对应的完整批准上下文对象,序列化最多16KiB/输入四分之一,稳定原顺序保留新旧版本对象。其余来源保留批准目录按需读;不会因缺少检索能力隐藏正文。底层API默认false,设置false可回退。主文件、规则与需求不删减;即使旧输入能容纳整批上下文,也应用该附带上限。
32
+
33
+ `enableSearchSeeds:true` 可将模型搜索的精确命中附读最多两段,每检索链最多两次尝试(失败也占次数),每段最多24行。读后验证snapshot/source/hash,旧新侧不互换,完整已有正文/范围不重读,预留4calls/16KiB。精确下一轮及终答输入不适配时事务回滚;不缓存、不造receipt、不改变检索轮数。只在原始主审/综合scope启用,独立复核仍使用本次新receipt。配置false关闭,底层默认false。
34
+
35
+ `node scripts/context-review-benchmark.mjs [frozen-baseline-directory]` 在真实review编排上对照八文件、大批准上下文夹具。以v0.85.11冻结src为baseline,25→17调用,八个窗口主审转为八个全文主审并合并规划;八次独立反证复核保留、读取均16次、严格校验后的结果一致。输入加指令176968→129752字节,不含适配器追加合同,不是wire bytes。未传baseline时仅比较当前实现focus关闭/开启:单批窗口也已能合并,调用均17次,差异是窗口/完整正文传输,不声称该模式省8次。另有搜索→读取夹具在相同轮数和reader操作下从3次调用变2次;不能把脚本结果当作真实提速或召回保证。
36
+
37
+ JSON合同新增280字节传输提示防止原始换行/反斜线转义错误,不自动修补响应或替换身份。诊断聚合allowlist纠正原因:语法纠正仅对实际model-end计数,审查格式与检索协议在重试通过预算/取消准入后计数;被veto的请求不报已执行纠正。`primary-initial`也可能是复核/综合检索链初始调用。单阶段轮数与共享reader耗尽分别输出code;全部字段不含源码、原响应或未知提供方错误文本。
38
+
29
39
  ## 全项目并发与离线速度对照
30
40
 
31
41
  插件配置 `projectConcurrency` 默认为 8,范围 1~8;1 为串行回退。仅独立批准批次并行,各批仍有独立检索来源和账本,所有调用与 JSON 纠正共享原执行计数。该上限按单次执行生效,不是全宿主 RPM/TPM 限流器;提供方不支持并发或遇到 429 时应调低。取消/致命检查点错误必须等待全部在途模型清理后释放会话锁,后台 checkpoint 继续使用原串行持久化队列。
@@ -70,7 +80,7 @@ Git 路径和索引、SVN XML、规则文件与 SVN 链接记录仍按各自 UTF
70
80
 
71
81
  - 采集、导航、命令及模型审查默认不设插件内部秒表;内部API调用者仍可显式指定timeoutMs(0表示无内部截止)。用户/宿主取消仍传播并等待清理。
72
82
  - 每阶段检索默认最多三轮,`maxRetrievalRounds` 可显式设 0~50。变更模式正式入口默认 `readerBudgetPolicy:"scaled"`,可审文件数N决定同一快照的共享额度:`min(1000,max(50,N*4))` 次操作、`min(4MiB,max(256KiB,N*16KiB))` 序列化输出字节;预览和诊断显示具体额度。内部API/全项目批次默认固定50次/256 KiB,配置 `readerBudgetPolicy:"fixed"` 可回退。所有阶段、预取及最终证据回读共用原账本,不逐文件重置;单次64 KiB、read最多200行不变。
73
- - 正式变更入口默认 `riskPlanningMode:"auto"`,只对预算内完整单批首轮主审合并可选风险问题,`"separate"` 回退独立规划;完整多窗口计划复用仍保留。合并风险是同次主审的待证问题,不证明其引导了检索或已验证条件,报告单独计数;缺失/无效字段记录限制且不额外补规划。
83
+ - 正式变更入口默认 `riskPlanningMode:"auto"`,对预算内完整正文,或精确覆盖全部新旧编辑的单批窗口首轮主审合并可选风险问题,`"separate"` 回退独立规划;多批/包络差异/项目与后续独立轮次不合并,完整多窗口计划复用仍保留。合并风险是同次主审的待证问题,不证明其引导了检索或已验证条件,报告单独计数;缺失/无效字段记录限制且不额外补规划。
74
84
  - 缺失上下文按准确关系坐标预取最多4次、每次40行,尝试也有界;输入/输出失败事务回滚,至少保留4次读取及16 KiB余量,这不保证复核足额。不得复制已有正文、猜测旧新行号或跨生成复用receipt。预算耗尽的依赖读取阶段不准入,完整正文主审可继续;必要证据仍不可得时保留未完成。调用目的诊断区分初始、续轮、JSON纠正及预算终答;未提供模型的输出不得算作读取证据。
75
85
  - Git 工作区依赖链最多三层、512 个精确路径查询、20 个候选、手动入口与自动依赖共享 1 MiB 正文;文件读取与最终复核共享宿主/用户取消信号。
76
86
  - SVN工作副本全采集记录命令次数、共享1MiB元数据内存保护,不设默认命令次数或时间截止,含初始/重复采集、导航、属性和末尾验证;cat输出另受16MiB总硬上限约束。历史采集也有独立的元数据与正文内存保护,不以命令次数或时间中断。
package/README.md CHANGED
@@ -235,13 +235,19 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
235
235
 
236
236
  重启后可按原jobId读回保存记录,原运行标记interrupted,不自动继续外发。recovery给出已观察文件/窗口与未完成路径;它不是完整分母或新授权,补审必须重新预览确认。新预览的runtime包含实际加载版本和模块位置,排查不同工作区时分别核对,不根据另一台机器推断。
237
237
 
238
- 变更审查的独立文件链默认最多 2 条并行;配置 `changesConcurrency` 为 1~4(1 回退串行),跨文件综合等待相关前置主审结束。配置 `maxRetrievalRounds` 为 0~50,默认为 3,控制每阶段成功检索轮数(0 只允许最终答复)。配置不是 preview 参数,不改变模型或来源授权;并发仍共享同一份读取/调用预算,额度不足时完成顺序可能影响哪些任务未完成,不能保证与串行逐项结果相同。提供方限流时应降低并发。复核可预取批准快照中候选锚点附近的同侧原文,仍允许追加反证读取并逐字校验;不复用主审结论或其他会话凭据。完整变更计划可容纳时仅生成一次再分发到窗口,否则保留原分窗口计划并在预览估算中体现。
238
+ 变更审查的独立文件链默认最多 2 条并行;配置 `changesConcurrency` 为 1~4(1 回退串行),跨文件综合等待相关前置主审结束。配置 `maxRetrievalRounds` 为 0~50,默认为 3,限制每阶段检索请求回合数(不是成功读取次数;非法请求也会消耗回合)(0 只允许最终答复)。配置不是 preview 参数,不改变模型或来源授权;并发仍共享同一份读取/调用预算,额度不足时完成顺序可能影响哪些任务未完成,不能保证与串行逐项结果相同。提供方限流时应降低并发。复核可预取批准快照中候选锚点附近的同侧原文,仍允许追加反证读取并逐字校验;不复用主审结论或其他会话凭据。完整变更计划可容纳时仅生成一次再分发到窗口,否则保留原分窗口计划并在预览估算中体现。
239
239
 
240
- 大文件超出单次输入时按原行号分段。精确差异算不出时使用明确标注的包络,可能包含未变行,不声称精确归因。模型JSON错误会按当前阶段schema请求一次明确纠正,记录额外调用;检索请求非法时整批不执行并明确纠正,回填采用可追溯的滚动工作集、必要时延后重复窗口正文,不虚构证据。主审与复核默认各最多 3 轮检索;到限后只允许最终答复,保留未完成范围,不把已用完预算当成审核通过。不会给错误输出补括号、猜字段或当成无缺陷。
240
+ 大文件超出单次输入时按原行号分段。精确差异算不出时使用明确标注的包络,可能包含未变行,不声称精确归因。完整结束但 JSON 语法无效时会按当前阶段 schema 最多请求两次明确纠正,记录额外调用;检索请求非法时整批不执行并明确纠正,回填采用可追溯的滚动工作集、必要时延后重复窗口正文,不虚构证据。主审与复核默认各最多 3 轮检索;到限后只允许最终答复,保留未完成范围,不把已用完预算当成审核通过。不会给错误输出补括号、猜字段或当成无缺陷。
241
+
242
+ ### 显式续审与私有模型工件
243
+
244
+ 需要补审时,在**新预览**中传入 `resumeFromJobId`,例如 `{"resumeFromJobId":"原执行任务的真实jobId"}`;仍须展示新范围、重新确认并通过当前宿主权限。只复用同一会话、工作目录、模型路由/思考设置下,完整快照、配置和运行时身份均匹配的已完成且符合复用条件的文件链;项目模式还绑定对应批次。来源或配置变化不做猜测复用。未完成阶段重新执行,旧发现标记 `priorRun`,不伪造本轮读取凭据、模型调用或覆盖。
245
+
246
+ 后台审查会在上述宿主任务目录的 `artifacts` 子目录留存有界模型工件,包括失败/取消时已捕获的文本或工具参数;不含 reasoning。它们可能含源码,应按源码保护,不会自动加入提示词、当作已确认发现或在普通状态报告公开。默认单工件 1 MiB、每任务原文总量 8 MiB、最多 64 条记录;截断由工件标记,存储超限明确返回遗漏标记和无源码诊断。读取绑定原会话、目录、提供方、模型和思考设置。需要查看时,Agent 必须先提示“可能把源码/模型原文带入当前对话”,取得你的明确同意,再调用 `code_review_artifacts({jobId,confirmed:true})`;可用 `offset`/`limit` 分页,默认一次 1 条、最多 100 条。返回内容标为 `sourceBearing:true`、`untrusted:true`,不是批准证据;普通 `status` 不返回工件原文。目录/文件使用 0700/0600 权限模式及原子写入;Windows 模式位不等于额外 ACL 保证。
241
247
 
242
248
  ## 多个会话同时审查
243
249
 
244
- 可以同时打开多个DSH Agent会话:会话A审项目A,会话B审项目B,也可以分别审查同一项目。各会话的预览、执行锁、模型路由、取消和预算独立;A正在执行不会让B收到全局busy。一个会话内部仍串行,不能拿另一个会话的previewId执行。宿主停用插件会结束全部服务,单次取消只影响当前请求。
250
+ 可以同时打开多个DSH Agent会话:会话A审项目A,会话B审项目B,也可以分别审查同一项目。各会话的预览、执行锁、模型路由、取消和预算独立;A正在执行不会让B收到全局busy。一个会话不能同时启动两个独立审查任务,但任务内部可按配置并行;不能拿另一个会话的previewId执行。宿主停用插件会结束全部服务,单次取消只影响当前请求。
245
251
 
246
252
  ## 审查整个项目当前代码
247
253
 
@@ -258,7 +264,7 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
258
264
 
259
265
  这不是HEAD到工作区的差异:模型检查当前已有缺陷,不要求问题由最近提交引入。默认仍是 `reviewMode:"changes"` 的变更审查。
260
266
 
261
- 项目模式只接受reviewMode、repositoryPath和可选selectedPaths,不与历史revision、autoContext、规则或调用方参数混用。默认全部跟踪节点;不读取未跟踪/忽略文件、子模块、外部仓库及链接目标。目录只作清单记录,源码按批次审查;跨批次不宣称完整调用图或全程序语义分析。
267
+ 项目模式接受reviewMode、repositoryPath、可选selectedPaths和显式续审参数resumeFromJobId,不与历史revision、autoContext、规则或调用方参数混用。默认全部跟踪节点;不读取未跟踪/忽略文件、子模块、外部仓库及链接目标。目录只作清单记录,源码按批次审查;跨批次不宣称完整调用图或全程序语义分析。
262
268
 
263
269
  未指定选择时,清单上限 10000 节点,Git 索引输出上限 2 MiB。Git 显式 `selectedPaths` 只查询所选精确文件,不先枚举全仓,因此大仓库可用小范围预览;空数组不枚举索引,目录与通配符不会展开。此时 `inventoryScope:"selected"`、`inventoryComplete:false`、`selectionComplete:false`,`totalTrackedNodes:null` 表示全仓总数未知,`inventoryNodes` 仅为所选清单数;只绑定所选索引及正文,未选文件的变化不使该局部快照失效。单次选择运行时最多 1000 节点(公开工具参数显式列表最多 200 项)/16 MiB,12 节点一批,单文件 256 KiB;超限必须重新预览。SVN 仍枚举本地已观察清单,显式选择不绕过清单上限,稀疏/外部/链接及不支持属性显示限制,不证明远端完整覆盖。SVN 单次采集共享 8 MiB 元数据保护;Git 所选索引单次查询最多 64 KiB、合计 2 MiB。不按命令条数、插件秒表或模型调用次数中断;耗时受宿主/模型提供方限制,用户可随时取消。
264
270
 
@@ -291,7 +297,9 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
291
297
 
292
298
  每个候选问题会保留位置、触发条件、影响和复核结果。复核可能支持、反驳、无法确定或未完成;引用原文匹配也不等于缺陷一定成立。**没有发现问题不证明没有缺陷,读取过上下文也不等于审查了整个项目。**
293
299
 
294
- ### 模型协议错误与读取预算恢复
300
+ ### 模型结果传输与读取预算恢复
301
+
302
+ 生产入口启用原生结果工具 `submit_review_result`,只接收结构化结果,不执行模型工具产生的文件操作;同次调用仍兼容文本结果。底层适配器的原生模式默认关闭,由生产服务显式启用。只有原生模式允许提取开头完整、平衡且字节不变的 JSON 后面的明确自然语言尾注;多 JSON、含糊尾部、损坏 JSON 仍拒绝,不补字段或猜身份。原生模式的预览与发送前校验使用同一 UTF-8 测量:输入、阶段契约、格式指引和序列化工具 schema 均纳入,不把字节估算当 token 或费用保证。
295
303
 
296
304
  已启用的模型桥接对完整结束但 JSON 无效的响应最多请求两次明确纠正(共最多三次调用),不会修补损坏 JSON;取消、提供方错误、不完整结束或输出超限不作此类重试。主审响应中的文件身份、严重性、必填字段或行证据校验失败时,最多请求一次完整响应纠正,仍按原快照、输入及调用预算严格验证。每次额外调用和纠正事件都会计数记录,恢复失败仍标为失败,不代表发现了缺陷。
297
305
 
@@ -322,11 +330,15 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
322
330
 
323
331
  ## 调用效率与预算
324
332
 
325
- 变更模式默认将完整、单批文件的风险问题合入首轮主审,并只预取已批准且有准确位置的缺失上下文小片段;保留独立候选复核和跨窗口/跨文件综合。多窗口已按预算装批,不以丢弃窗口换取速度。报告中的合并问题不是独立风险规划或已验证条件。
333
+ 业务分组在文件链启动前独立完成,不以先跑完第一个文件作引导。适合的 2~4 个文件可合并为一次组主审:必须是精确变更、完整正文且可容纳的非项目模式成员,响应逐个返回原文件身份并通过各自校验。窗口化、超预算或不适合的组在发送前回退单文件;专门跨文件交互阶段及独立复核仍保留,组主审不等于跨文件证据已证明。可合并的风险假设是可选信息,缺失/无效会记录限制,不为它单独追加调用。
334
+
335
+ 变更模式默认优先保留主文件全文,仅附带有导入/调用等关系的完整上下文对象(序列化合计最多16KiB或输入上限四分之一,取较小值);其余批准来源保留版本目录按需读取,不表示不存在或已核查。完整正文,或经过精确新旧编辑、坐标和原文核对且覆盖全部编辑的单批窗口,可将风险问题合入首轮主审。多批窗口、包络差异和项目审查仍保留独立规划;保留独立候选复核和跨窗口/跨文件综合,不以丢弃窗口换取速度。报告中的合并问题不是独立风险规划或已验证条件。
336
+
337
+ 模型搜索的精确命中可同轮附带批准来源原文,每阶段最多2次额外读取尝试、每次最多24行;占用原共享额度并预留4次操作/16KiB输出,不猜测另侧坐标,不增加检索轮数。配置 `focusPrimaryContext:false`、`enableSearchSeeds:false` 可分别关闭上下文聚焦和搜索命中附读;无检索能力时不会隐藏正文。
326
338
 
327
339
  共享读取预算按本次可审文件数有界缩放,预览的 `execution.readerBudget` 显示具体值;所有阶段共用、不逐文件刷新,达到上限的必要证据阶段不再无效启动,并保留未完成范围。配置 `riskPlanningMode:"separate"`、`readerBudgetPolicy:"fixed"` 可回退原规划和固定读取额度,这些是插件配置,不是preview参数。更多额度不保证更快或完整覆盖;不建议用提高并发掩盖冗余调用。
328
340
 
329
- 诊断显示实际加载版本、阶段及调用目的计数、读取余量;累计调用耗时可能并发重叠,不能当作总耗时或纯提供方耗时。离线调用数减少不代表实际审核必然同比提速,不能用更快返回partial冒充审核完成。
341
+ 诊断显示实际加载版本、阶段及调用目的计数、读取余量,并区分单阶段检索轮数和共享读取额度。新增无源码的JSON纠正原因、协议/审查格式纠正计数及未合并规划原因;纠正计数以实际纠正调用为准;这些诊断不含原响应、源码片段或提供方错误文本,与下面说明的私有模型工件分开。`primary-initial` 是检索链初始调用,不只包含主审阶段。累计调用耗时可能并发重叠,不能当作总耗时或纯提供方耗时。离线调用数减少不代表实际审核必然同比提速,不能用更快返回partial冒充审核完成。
330
342
 
331
343
  ## 规模限制
332
344
 
package/RELEASE-NOTES.md CHANGED
@@ -1,19 +1,27 @@
1
- # v0.85.11 发布说明
1
+ # v0.86.0 发布说明
2
2
 
3
- 使用方式见 [README](README.md),完整变化见 [变更记录](CHANGELOG.md),预算与计时边界见 [开发说明](DEVELOPMENT.md)。
3
+ 使用方式见 [README](README.md),历史变化见 [变更记录](CHANGELOG.md),验证与发布门禁见 [开发说明](DEVELOPMENT.md)。
4
4
 
5
- ## 减少调用往返,保留证据和独立复核
5
+ ## 输出协议:减少格式往返,不放宽证据
6
6
 
7
- - 变更审查默认对完整正文可见、单批且预算可容纳的文件,将待证风险问题合并入首轮主审,减少一次独立规划调用;多窗口、跨文件综合、全项目模式与后续独立轮次仍保留原有策略。缺失或非法的可选风险字段不触发补规划,不删除有效候选,并明确披露;合并问题不是独立规划或已验证条件。
8
- - 仅在当前输入缺失上下文正文、批准关系有确定坐标时,预取最多四段、每段最多40行;不重复读取已有正文,不猜测同侧行号,不复用跨阶段凭据。精确输入不适配或预算不足时事务回滚,保留按需检索。预取只是局部片段,不代表已核查条件。
9
- - 阶段准入区分完整正文与依赖新读取的阶段:共享 reader 耗尽时,不再启动无法取得必要证据的规划、主审或综合;保留真实未完成状态。完整正文主审仍可运行,若再请求证据仍受原账本限制。
10
- - 变更模式的共享 reader 额度按批准范围显式缩放:至少50次操作/256 KiB,按可审文件数每项4次操作/16 KiB,硬上限1000次/4 MiB;所有阶段共用,不逐文件重置,预览显示具体值。单次64 KiB/200行、输入和检索轮次上限不变。内部API和全项目批次默认仍使用固定额度;可配置fixed回退。额度增加不等于耗时降低或覆盖保证。
11
- - 诊断区分阶段初始、检索续轮、JSON纠正、预算最终答复等实际调用目的,显示读取上限和余量,合并风险单独计数。检索输出未能进入模型输入时,不再被审计误记为已提供证据;先前实际提供的记录保持。
7
+ 生产入口优先使用真实 DSH 原生结果工具 submit_review_result,结构化提交作为数据交给现有审查、归因、定位和复核校验器,不执行模型返回的工具。宿主支持的 ToolSchema、工具调用增量/最终块以及 tool-calls 结束标记均已对照安装宿主声明和组装器核查;不使用宿主不存在的强制 toolChoice 参数。
12
8
 
13
- ## 验证与效果边界
9
+ 提供方仍返回文本时,允许同一次调用的严格 JSON/单一围栏兼容;原生模式额外允许从响应开头提取未修改的完整 JSON 容器并忽略无歧义普通尾注,不搜索任意分析段、不选择多个对象、不拼补身份、字段或源码。歧义和非法工具调用失败关闭;有界格式纠正仍显式计调用。预览、窗口准备及检索、可选预取、复核使用原生工具定义、指令合同和正文的精确输入字节测量,发送前再次硬校验,不用隐藏保留量交换正文。
14
10
 
15
- 新增离线回归覆盖规划合并与预览计数一致、独立复核保留、非法可选字段、后续轮隔离、确定坐标预取/历史侧/CRLF/事务回滚、reader耗尽后的上下文拒绝与完整正文准入、跨文件准入、输出淘汰与调用诊断。受控八文件回归证明独立规划16次调用可合并为8次主审;带候选的对照仍保留独立反证复核。同一当前实现的独立规划/自动合并对照脚本 `node scripts/call-reduction-benchmark.mjs` 还验证含业务分组、八次主审和八次独立复核的总调用25→17,读取均16次、校验后候选/反证结果一致;不是旧版与新版性能对照。上述均为脚本化模型调用结构验证,不证明真实缺陷召回不变,不承诺业务审核提速比例。
11
+ ## 私有留存与重新确认后的续审
16
12
 
17
- Windows 本地完整回归936项:932通过、4项平台限定跳过、0失败;两路独立源码复核发现的交互准入问题已修复并复测,隔离安装通过。正式发布须通过冻结标签四矩阵 CI(Ubuntu/Windows × Node.js 22/24)、隔离安装,以及原始 Actions/npm/GitHub Release 安装包逐字节核验。
13
+ 后台任务在审查工作区外有界保存原始文本块/工具参数,包括失败和取消的输出,不保存 reasoning 块。默认单记录1MiB、单任务8MiB、最多64条;超限省略和截断明确记录,不宣称无限或完整捕获。引用与计数可以进入状态和脱敏诊断,原始输出不能自动成为新审查证据。读取 code_review_artifacts 必须获得源码隐私知情同意并显式 confirmed:true,绑定相同会话、工作区和模型路由,默认一次1条。Windows不把mode位宣称为额外ACL隔离。
18
14
 
19
- 没有重跑用户业务审核;不会自动安装、升级或重启当前 DSH。更新并重新加载后,以报告显示v0.85.11确认实际加载。预算不足仍披露partial,不能把更快返回未完成当作完整审核。
15
+ resumeFromJobId 重新捕获、预览并确认完全相同的快照、上下文/规则/需求、路由、运行时及执行配置,执行时仍复核并重新授权;旧批准永不恢复。只复用全部阶段及所属交互已完成、候选已完成独立复核的文件链,支持变更和项目批次。中断、窗口未完成、缺证据或未复核成果仍保留但不能冒充可复用完成链。旧发现标记 priorRun/reused,旧证据不是本轮新 receipt,旧调用不计入本轮。
16
+
17
+ ## 真正组主审与编排
18
+
19
+ 业务分组成为文件链调度前的独立阶段,不再先跑完整条首文件链。2至4个能完整装入输入预算且差异精确的变更文件,可以一次 group-primary 生成全部成员的主审结果,再按真实 fileId 与原始行号逐项严格校验、分发;不会仍为每个成员重复发起首轮主审。超预算、窗口/多批次、非精确差异、项目模式和多轮独立审查在发送前保持原路径。
20
+
21
+ 组内风险问题可以合并到本次生成,缺失/无效计划和未读来源明确披露,不补发隐藏规划调用。组主审不冒充跨文件证据:独立交互阶段及候选复核保留,不能把不合格 interaction 降级成普通发现绕过校验。调用预算耗尽时保留已经生成并校验的成员结果,把后续未完成阶段标为 partial,不丢弃已付费主审成果。失败组不隐藏补发全体单文件调用。
22
+
23
+ ## 验证与边界
24
+
25
+ 新增离线回归覆盖原生流、歧义拒绝、格式兼容零额外调用、精确输入边界、私有留存与跨身份拒绝、取消清理、部分完成续审、新确认/授权及变更失效、组主审覆盖与预算边界。独立源码审计发现的“组结果在额度边界被丢弃”已修复并有回归;取消与落盘失败同时出现时保留原取消身份。
26
+
27
+ 这轮对齐的是上述核心机制,不等于上游全部功能或模型质量完全一致;没有重跑用户业务审查,不承诺实际耗时、召回或全面覆盖。正式发布仍必须通过冻结标签 Ubuntu/Windows × Node.js22/24 四矩阵,以及 Actions、npm、GitHub Release 原始包逐字节、校验清单和冻结发布说明核验。不会自动安装、升级或重启当前 DSH。
package/index.mjs CHANGED
@@ -7,14 +7,14 @@ export const name = 'dsh-code-review';
7
7
  export const inject = ['tools', 'llm'];
8
8
 
9
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.';
10
- const PREVIEW_PARAMETERS = { type: 'object', additionalProperties: false, properties: { 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 } } };
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
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
12
  const EXECUTE_PARAMETERS = { type: 'object', additionalProperties: false, properties: { previewId: { type: 'string' }, confirmed: { type: 'boolean' } }, 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.
16
16
  // Host tool policy owns permission; do not request a second approval here.
17
- const service = createReviewService(ctx.llm, { allowModelSending: true, enableRiskPlanning: true, enableBusinessGrouping: true, enableAnchorCorrection: true, projectConcurrency: config.projectConcurrency ?? DEFAULT_PROJECT_CONCURRENCY, riskPlanningMode: config.riskPlanningMode ?? 'auto', readerBudgetPolicy: config.readerBudgetPolicy ?? 'scaled', optimizeReviewCalls: true, enablePrimarySeeds: true, changesConcurrency: config.changesConcurrency ?? 2, maxRetrievalRounds: config.maxRetrievalRounds ?? 3, authorize: async () => true });
17
+ const service = createReviewService(ctx.llm, { allowModelSending: true, nativeOutputTools: true, enableGroupPrimary: true, enableRiskPlanning: true, enableBusinessGrouping: true, enableAnchorCorrection: true, projectConcurrency: config.projectConcurrency ?? DEFAULT_PROJECT_CONCURRENCY, riskPlanningMode: config.riskPlanningMode ?? 'auto', readerBudgetPolicy: config.readerBudgetPolicy ?? 'scaled', optimizeReviewCalls: true, enablePrimarySeeds: true, focusPrimaryContext: config.focusPrimaryContext ?? true, enableSearchSeeds: config.enableSearchSeeds ?? true, changesConcurrency: config.changesConcurrency ?? 2, maxRetrievalRounds: config.maxRetrievalRounds ?? 3, authorize: async () => true });
18
18
  ctx.tools.register({
19
19
  name: 'code_review_preview', description: PREVIEW_DESCRIPTION, parameters: PREVIEW_PARAMETERS, output: reviewToolOutput,
20
20
  async execute(args, exec) { const preview = await service.preview(args, exec); return { json: JSON.stringify(preview), markdown: '' }; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.85.11",
3
+ "version": "0.86.0",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -35,11 +35,14 @@ async function startBackgroundJob(args, exec, jobs, service) {
35
35
  if (args.phase === 'execute' && (args.confirmed !== true || typeof args.previewId !== 'string' || args.previewArgs !== undefined)) throw new Error('Confirmed preview required');
36
36
  if (args.phase === 'preview' && (args.previewId !== undefined || args.confirmed !== undefined)) throw new Error('Preview cannot confirm execution');
37
37
 
38
- return respond(await jobs.start(exec, async ({ signal, checkpoint }) => {
38
+ return respond(await jobs.start(exec, async ({ signal, checkpoint, artifact, saveResumeState }) => {
39
39
  const original = exec.agent;
40
40
  const sessionId = original.session.header.id ?? original.session.id;
41
41
  const detached = {
42
42
  ...exec, agent: original, signal,
43
+ resolveReviewResume: id => jobs.resume(id, exec),
44
+ reviewResumeCheckpoint: saveResumeState,
45
+ onModelArtifact: artifact,
43
46
  reviewCheckpoint: async value => {
44
47
  const active = exec.agent;
45
48
  if (active !== original || (active.session.header.id ?? active.session.id) !== sessionId) throw new Error('Background Agent/session changed');
@@ -82,6 +85,17 @@ export function registerBackgroundReview(ctx, service, { directory } = {}) {
82
85
  },
83
86
  });
84
87
 
88
+ ctx.tools.register({
89
+ name: 'code_review_artifacts',
90
+ description: 'Explicitly read private source-bearing model-output artifacts for an owned review job. Never call automatically, forward as review evidence, or include in diagnostics. Show the source-bearing privacy warning and obtain user consent before confirmed=true. Omitted/truncated records are explicit and do not prove review completion.',
91
+ parameters: { type: 'object', additionalProperties: false, properties: { jobId: { type: 'string' }, confirmed: { type: 'boolean' }, offset: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 100 } }, required: ['jobId', 'confirmed'] },
92
+ output: reviewToolOutput,
93
+ async execute(args, exec) {
94
+ if (args.confirmed !== true) throw new Error('Explicit consent required to expose source-bearing private model artifacts');
95
+ const result = await jobs.artifacts(args.jobId, exec, { offset: args.offset ?? 0, limit: args.limit ?? 1 });
96
+ return { json: JSON.stringify({ untrusted: true, sourceBearing: true, notice: 'Raw model output is diagnostic data only; not approved evidence or accepted findings.', ...result }), markdown: '私有原始模型响应:可能包含源码,仅供诊断;不是已批准证据或已接受发现。' };
97
+ },
98
+ });
85
99
  ctx.on('dispose', () => jobs.dispose());
86
100
  return jobs;
87
101
  }
@@ -6,6 +6,7 @@ import {localBusinessGroups} from './business-groups.mjs';
6
6
  import { prepareInteractionInput } from './interaction-input.mjs';
7
7
  import { initialInputBudget } from './input-budget.mjs';
8
8
  import { preparePrimaryInput } from './primary-input.mjs';
9
+ import { prepareGroupPrimary } from './group-primary.mjs';
9
10
 
10
11
  // Metadata only. No retained payload, file reads, model calls, or approval grants.
11
12
  export function buildCoveragePlan(input, options) {
@@ -31,10 +32,23 @@ export function buildCoveragePlan(input, options) {
31
32
  const planningCalls = riskPlanning.required && !prepared.inlineRisk ? (canonical?.cacheable ? 1 : primaryCalls) : 0;
32
33
  return { ...item, ...prepared.metadata, riskPlanning, status: prepared.budget.fits ? 'ready' : 'input-blocked', minimumCalls: prepared.budget.fits ? (primaryCalls * rounds + planningCalls + (prepared.synthesis?.status === 'ready' ? rounds : 0)) : 0 };
33
34
  });
34
- const local = options.enableBusinessGrouping === true ? localBusinessGroups(input.files) : null;
35
+ const local = options.resumeState?.businessGrouping ?? (options.enableBusinessGrouping === true ? localBusinessGroups(input.files) : null);
35
36
  const groups = [...(options.grouping?.groups ?? [])];
36
37
  for(const group of local?.groups ?? [])if(!groups.some(g=>[...g.fileIds].sort().join(',')===[...group.fileIds].sort().join(',')))groups.push(group);
37
38
  const interactions = options.scope && groups.length ? groups.filter(g => g.fileIds.length > 1).map(group => { const prepared = prepareInteractionInput(input, group, payload => initialInputBudget({ instructions: options.instructions, input: payload }, options.scope, options.maxInputBytes)); const riskPlanning = options.enableRiskPlanning === true ? options.skipSmallRiskPlans === true ? riskPlanningDecision(input.files.filter(f=>group.fileIds.includes(f.id))) : {required:true,reason:'unconditional'} : {required:false,reason:'disabled'}; return { groupId: group.id, fileIds: prepared.fileIds, riskPlanning, status: prepared.status, reason: prepared.reason, minimumCalls: prepared.status === 'ready' ? (rounds + (riskPlanning.required ? 1 : 0)) : 0 }; }) : [];
38
39
  const businessGrouping = options.enableBusinessGrouping === true && input.files.filter(f => f.eligibility === 'reviewable').length > 1 ? local ? {minimumCalls:0,strategy:local.strategy,churn:local.churn,notice:'本地调度无语义依赖证明。'} : { minimumCalls: 1, dynamicInteractionCalls: 'unknown-until-execution', maximumAdditionalInitialCalls: Math.floor(input.files.filter(f => f.eligibility === 'reviewable').length / 2) * (rounds + (options.enableRiskPlanning === true ? 1 : 0)), notice: '模型分组假设仅调度,额外综合动态且不保证预算或覆盖。' } : null;
39
- return { ...(businessGrouping ? { businessGrouping } : {}), interactions, snapshotId: input.id, reviewRounds:rounds, maxInputBytes: options.maxInputBytes, items, minimumCalls: (businessGrouping?.minimumCalls ?? 0) + items.reduce((sum,item) => sum + item.minimumCalls, 0) + interactions.reduce((sum,item) => sum + item.minimumCalls, 0), initialRequestsOnly: true };
40
+ for (const interaction of interactions) if (interaction.fileIds.every(id => options.resumeState?.completed?.some(row => row.fileId === id))) { interaction.minimumCalls = 0; interaction.reused = true; }
41
+ const primaryGroups = [];
42
+ if (options.enableGroupPrimary === true && rounds === 1 && options.scope) {
43
+ const seen = new Set();
44
+ for (const group of local?.groups ?? options.grouping?.groups ?? []) {
45
+ if (group.fileIds.some(id => seen.has(id) || options.resumeState?.completed?.some(row => row.fileId === id))) continue;
46
+ const prepared = prepareGroupPrimary(input, group, options);
47
+ if (!prepared) continue;
48
+ primaryGroups.push({ groupId: group.id, fileIds: prepared.fileIds, minimumCalls: 1 });
49
+ for (const id of group.fileIds) { seen.add(id); const item = items.find(item => item.fileId === id); item.primaryMethod = 'group-primary'; item.primaryGroupId = group.id; item.minimumCalls = id === group.fileIds[0] ? 1 : 0; }
50
+ }
51
+ }
52
+ for (const row of options.resumeState?.completed ?? []) { const item = items.find(item => item.fileId === row.fileId); if (item) { item.minimumCalls = 0; item.reused = true; } }
53
+ return { ...(options.enableGroupPrimary ? { primaryGroups, primaryGroupNotice: businessGrouping?.dynamicInteractionCalls ? '执行时模型分组可能改变组主审单元,当前为静态估计而非确定调用承诺。' : '相同完整正文单元在预算内合并,专门交互证据阶段仍独立执行。' } : {}), ...(businessGrouping ? { businessGrouping } : {}), interactions, snapshotId: input.id, reviewRounds:rounds, maxInputBytes: options.maxInputBytes, items, minimumCalls: (businessGrouping?.minimumCalls ?? 0) + items.reduce((sum,item) => sum + item.minimumCalls, 0) + interactions.reduce((sum,item) => sum + item.minimumCalls, 0), initialRequestsOnly: true };
40
54
  }
package/src/dsh-model.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import { modelContract } from './model-contract.mjs';
2
+ import { modelResultTools, validateNativeResult, RESULT_TOOL_NAME, NATIVE_RESULT_INSTRUCTIONS } from './model-tools.mjs';
3
+ import { createHash, randomUUID } from 'node:crypto';
2
4
  import { modelJson } from './model-json.mjs';
3
5
  import { jsonCorrectionHint, providerDiagnostic } from './model-diagnostics.mjs';
4
6
 
@@ -14,7 +16,7 @@ const isToolCall = chunk =>
14
16
  /** Isolated host calls. Only completed, locally rejected JSON can trigger bounded correction. */
15
17
  export function createDshModel(llm, route, {
16
18
  maxTokens = 4096, maxOutputBytes = 128 * 1024, correctJson = false,
17
- maxJsonCorrections = MAX_JSON_CORRECTIONS,
19
+ maxJsonCorrections = MAX_JSON_CORRECTIONS, useNativeTools = false,
18
20
  } = {}) {
19
21
  if (!llm || typeof llm.stream !== 'function' || !route?.provider || !route?.model) {
20
22
  throw new Error('DSH provider and model are required');
@@ -38,17 +40,19 @@ export function createDshModel(llm, route, {
38
40
  try { await callback?.(event); } catch { /* Metrics are not model/budget authority. */ }
39
41
  }
40
42
 
43
+ const jsonIssues = new Set(['property-name', 'colon', 'separator', 'invalid-escape', 'control-character', 'unterminated-string', 'unexpected-end', 'trailing-content', 'syntax']);
44
+ const jsonIssue = diagnostic => jsonIssues.has(diagnostic?.syntaxCategory) ? diagnostic.syntaxCategory : undefined;
41
45
  const purposes = new Set(['primary-initial', 'retrieval-followup', 'budget-finalization', 'json-correction', 'business-grouping', 'risk-planning', 'window-synthesis', 'file-interaction', 'verification']);
42
- function checkInputBudget(request, contract) {
46
+ function checkInputBudget(request, contract, tools = []) {
43
47
  if (request.maxInputBytes === undefined) return;
44
48
  if (!Number.isSafeInteger(request.maxInputBytes) || request.maxInputBytes < 1) throw new Error('Invalid model input budget');
45
- const wireInputBytes = Buffer.byteLength(request.instructions + '\n' + contract) + Buffer.byteLength(request.input);
49
+ const wireInputBytes = Buffer.byteLength(request.instructions + '\n' + contract) + Buffer.byteLength(request.input) + (tools.length ? Buffer.byteLength(JSON.stringify(tools)) : 0);
46
50
  if (wireInputBytes > request.maxInputBytes) {
47
51
  throw Object.assign(new Error('Model wire input budget exceeded'), { code: 'MODEL_INPUT_BUDGET_EXCEEDED', diagnostic: { wireInputBytes, maxInputBytes: request.maxInputBytes } });
48
52
  }
49
53
  }
50
54
 
51
- async function once({ instructions, input, signal, onModelEvent, attempt, callPurpose }, contract) {
55
+ async function once({ instructions, input, signal, onModelEvent, onModelArtifact, reviewPhase, attempt, callPurpose }, contract, tools) {
52
56
  signal?.throwIfAborted();
53
57
  let output = '';
54
58
  let size = 0;
@@ -57,7 +61,7 @@ export function createDshModel(llm, route, {
57
61
  const blocks = new Map();
58
62
  let finished = false;
59
63
  let finishKind = 'missing';
60
- let startedAt, firstChunkMs = null, status = 'failed';
64
+ let startedAt, firstChunkMs = null, status = 'failed', failureCode, issue;
61
65
  const purpose = attempt > 1 ? 'json-correction' : purposes.has(callPurpose) ? callPurpose : undefined;
62
66
  const event = { attempt, inputBytes: Buffer.byteLength(input), ...(purpose ? { callPurpose: purpose } : {}) };
63
67
  const diagnostic = () => ({
@@ -65,6 +69,7 @@ export function createDshModel(llm, route, {
65
69
  inputBytes: Buffer.byteLength(input), requestedMaxTokens: maxTokens, maxOutputBytes,
66
70
  });
67
71
  const failure = (message, code) => {
72
+ failureCode = code;
68
73
  const error = Object.assign(new Error(message), { code, diagnostic: diagnostic() });
69
74
  failureMetadata.set(error, { code, diagnostic: error.diagnostic });
70
75
  return error;
@@ -78,7 +83,7 @@ export function createDshModel(llm, route, {
78
83
  ...identity,
79
84
  system: instructions + '\n' + contract,
80
85
  messages: [{ role: 'user', content: [{ type: 'text', text: input }] }],
81
- tools: [], maxTokens, signal,
86
+ tools, maxTokens, signal,
82
87
  });
83
88
  } finally {
84
89
  await notify(onModelEvent, { ...event, action: 'model-start' });
@@ -90,7 +95,25 @@ export function createDshModel(llm, route, {
90
95
  firstChunkMs ??= Math.max(0, performance.now() - startedAt);
91
96
  signal?.throwIfAborted();
92
97
  if (finished) throw failure('Model emitted data after finish', 'MODEL_PROTOCOL_ERROR');
93
- if (isToolCall(chunk)) throw failure('Review model attempted a tool call', 'MODEL_PROTOCOL_ERROR');
98
+ if (isToolCall(chunk)) {
99
+ if (!tools.length) throw failure('Review model attempted a tool call', 'MODEL_PROTOCOL_ERROR');
100
+ const key = chunk.index;
101
+ if (!Number.isSafeInteger(key) || key < 0) throw failure('Invalid tool block index', 'MODEL_PROTOCOL_ERROR');
102
+ const previous = blocks.get(key);
103
+ if (previous?.closed) continue;
104
+ if (previous && previous.type !== 'tool-call') throw failure('Model changed block type', 'MODEL_PROTOCOL_ERROR');
105
+ const completed = chunk.type === 'block-end';
106
+ const id = completed ? chunk.block.id : chunk.id ?? previous?.id;
107
+ const name = completed ? chunk.block.name : chunk.name || previous?.name;
108
+ const args = completed ? chunk.block.arguments : (previous?.arguments ?? '') + (chunk.argumentsDelta ?? '');
109
+ if (typeof args !== 'string' || (previous?.id && id && previous.id !== id) || (previous?.name && name && previous.name !== name)) throw failure('Tool identity changed', 'MODEL_PROTOCOL_ERROR');
110
+ const bytes = Buffer.byteLength(args), chargedBytes = Math.max(previous?.chargedBytes ?? 0, bytes);
111
+ size += chargedBytes - (previous?.chargedBytes ?? 0);
112
+ blocks.set(key, { type: 'tool-call', id, name, arguments: args, chargedBytes, closed: completed });
113
+ if (size > maxOutputBytes) throw failure('Model output limit exceeded', 'MODEL_OUTPUT_LIMIT');
114
+ if ((name && name !== RESULT_TOOL_NAME) || [...blocks.values()].filter(b => b.type === 'tool-call').length > 1) throw failure('Unexpected result tool', 'MODEL_PROTOCOL_ERROR');
115
+ continue;
116
+ }
94
117
  const delta = chunk.type === 'text-delta' || chunk.type === 'reasoning-delta';
95
118
  const completed = chunk.type === 'block-end' &&
96
119
  (chunk.block.type === 'text' || chunk.block.type === 'reasoning');
@@ -107,13 +130,13 @@ export function createDshModel(llm, route, {
107
130
  // A shorter final snapshot must not refund previously consumed output budget.
108
131
  const chargedBytes = Math.max(previous?.chargedBytes ?? 0, bytes);
109
132
  size += chargedBytes - (previous?.chargedBytes ?? 0);
110
- if (size > maxOutputBytes) throw failure('Model output limit exceeded', 'MODEL_OUTPUT_LIMIT');
111
133
  blocks.set(key, { type, text, chargedBytes, closed: completed });
112
134
  output = [...blocks.values()].filter(block => block.type === 'text').map(block => block.text).join('');
135
+ if (size > maxOutputBytes) throw failure('Model output limit exceeded', 'MODEL_OUTPUT_LIMIT');
113
136
  }
114
137
  if (chunk.type === 'finish') {
115
138
  finishKind = chunk.reason.kind;
116
- if (finishKind !== 'stop') {
139
+ if (finishKind !== 'stop' && !(tools.length && finishKind === 'tool-calls')) {
117
140
  const error = failure('Model did not complete: ' + finishKind, 'MODEL_INCOMPLETE_OUTPUT');
118
141
  Object.assign(error.diagnostic, providerDiagnostic(chunk.reason));
119
142
  throw error;
@@ -139,29 +162,76 @@ export function createDshModel(llm, route, {
139
162
  signal?.throwIfAborted();
140
163
  if (!finished) throw failure('Model stream ended without finish', 'MODEL_INCOMPLETE_OUTPUT');
141
164
  try {
142
- const text = modelJson(output);
165
+ const calls = [...blocks.values()].filter(b => b.type === 'tool-call');
166
+ if (calls.length && (calls.length !== 1 || calls[0].name !== RESULT_TOOL_NAME || typeof calls[0].id !== 'string' || !calls[0].id || output.trim())) throw failure('Ambiguous native result', 'MODEL_PROTOCOL_ERROR');
167
+ if (!calls.length && finishKind === 'tool-calls') throw failure('Missing native result', 'MODEL_PROTOCOL_ERROR');
168
+ const raw = calls.length ? calls[0].arguments : output;
169
+ const text = modelJson(raw, { allowTrailingProse: !!tools.length && !calls.length });
170
+ if (calls.length && !validateNativeResult(text, input, instructions)) throw failure('Wrong native result stage', 'MODEL_PROTOCOL_ERROR');
171
+ const transportKind = calls.length ? 'native-tool' : text !== raw.trim() && !raw.trim().startsWith('```') ? 'trailing-prose' : tools.length ? 'text-compatible' : undefined;
143
172
  status = 'completed';
144
- return { text, diagnostic: diagnostic() };
173
+ return { text, diagnostic: diagnostic(), transportKind };
145
174
  } catch (error) {
146
175
  error.diagnostic = { ...diagnostic(), ...error.diagnostic };
176
+ if (error.code !== 'MODEL_INVALID_JSON') throw error;
177
+ failureCode = 'MODEL_INVALID_JSON';
178
+ issue = jsonIssue(error.diagnostic);
147
179
  invalidJsonErrors.add(error);
148
180
  failureMetadata.set(error, { code: error.code, diagnostic: error.diagnostic });
149
181
  throw error;
150
182
  }
151
183
  } finally {
184
+ let artifactError;
185
+ if (onModelArtifact) {
186
+ try {
187
+ const captured = []; let remaining = 1024 * 1024 - 4096, truncated = false;
188
+ for (const block of blocks.values()) {
189
+ if (!['text', 'tool-call'].includes(block.type)) continue;
190
+ const field = block.type === 'text' ? 'text' : 'arguments';
191
+ const raw = block[field] ?? '', encoded = Buffer.from(raw);
192
+ let content = raw;
193
+ // JSON string escapes may need six bytes per input byte. Reserve that
194
+ // pessimistic size, trim only at a valid UTF-8 boundary, never repair.
195
+ const cap = Math.max(0, Math.floor((remaining - 2048) / 6));
196
+ if (encoded.length > cap) {
197
+ let end = cap;
198
+ while (end > 0) { try { content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(encoded.subarray(0, end)); break; } catch { end--; } }
199
+ if (!end) content = '';
200
+ truncated = true;
201
+ }
202
+ const item = block.type === 'text' ? { type: 'text', text: content } : { type: 'tool-call', id: String(block.id ?? '').slice(0, 200), name: String(block.name ?? '').slice(0, 100), arguments: content };
203
+ remaining = Math.max(0, remaining - Buffer.byteLength(JSON.stringify(item)) - 1);
204
+ captured.push(item);
205
+ if (remaining < 4096 || truncated) { truncated = true; break; }
206
+ }
207
+ const raw = JSON.stringify(captured);
208
+ await onModelArtifact({ id: randomUUID(), format: 'dsh-model-output-v1', attempt,
209
+ ...(purposes.has(reviewPhase) ? { phase: reviewPhase } : {}), ...(purpose ? { callPurpose: purpose } : {}),
210
+ status: signal?.aborted ? 'cancelled' : status, bytes: Buffer.byteLength(raw), sha256: createHash('sha256').update(raw).digest('hex'),
211
+ blocks: captured, truncated, untrusted: true });
212
+ } catch {
213
+ status = 'failed'; failureCode = 'MODEL_ARTIFACT_FAILED';
214
+ artifactError = Object.assign(new Error('Private model artifact persistence failed'), { code: 'MODEL_ARTIFACT_FAILED' });
215
+ }
216
+ }
152
217
  await notify(onModelEvent, {
153
218
  ...event, action: 'model-end', status: signal?.aborted ? 'cancelled' : status,
219
+ ...(failureCode ? { failureCode } : {}), ...(issue ? { jsonIssue: issue } : {}),
154
220
  durationMs: Math.max(0, performance.now() - startedAt), firstChunkMs,
155
221
  visibleBytes: Buffer.byteLength(output), totalOutputBytes: size,
156
222
  });
223
+ if (artifactError) { signal?.throwIfAborted(); throw artifactError; }
157
224
  }
158
225
  }
159
226
 
160
227
  return async function invoke(request) {
161
228
  // Snapshot these before an async budget/progress hook can mutate the request.
162
- const original = { instructions: request.instructions, input: request.input, signal: request.signal, onModelEvent: request.onModelEvent, callPurpose: request.callPurpose, maxInputBytes: request.maxInputBytes };
229
+ const original = { instructions: request.instructions, input: request.input, signal: request.signal, onModelEvent: request.onModelEvent, onModelArtifact: request.onModelArtifact, reviewPhase: request.reviewPhase, callPurpose: request.callPurpose, maxInputBytes: request.maxInputBytes };
163
230
  const onProtocolCorrection = request.onProtocolCorrection, onProtocolEvent = request.onProtocolEvent;
164
- const contract = modelContract(original.input, original.instructions);
231
+ const native = request.nativeOutputTools ?? useNativeTools;
232
+ if (typeof native !== 'boolean') throw new Error('nativeOutputTools must be boolean');
233
+ const tools = native ? modelResultTools(original.input, original.instructions) : [];
234
+ const contract = modelContract(original.input, original.instructions) + (tools.length ? NATIVE_RESULT_INSTRUCTIONS : '');
165
235
  const correctionLimit = correctJson ? maxJsonCorrections : 0;
166
236
  const attempts = [];
167
237
  const prepared = attempt => ({ ...original, attempt, instructions: original.instructions +
@@ -169,10 +239,10 @@ export function createDshModel(llm, route, {
169
239
  for (let attempt = 1; ; attempt++) {
170
240
  original.signal?.throwIfAborted();
171
241
  const next = prepared(attempt);
172
- checkInputBudget(next, contract);
242
+ checkInputBudget(next, contract, tools);
173
243
  let result;
174
244
  try {
175
- result = await once(next, contract);
245
+ result = await once(next, contract, tools);
176
246
  original.signal?.throwIfAborted();
177
247
  } catch (error) {
178
248
  const metadata = failureMetadata.get(error) ?? {};
@@ -193,11 +263,12 @@ export function createDshModel(llm, route, {
193
263
  }
194
264
  original.signal?.throwIfAborted();
195
265
  // Do not reserve or announce a correction that cannot fit the wire budget.
196
- checkInputBudget(prepared(attempt + 1), contract);
266
+ checkInputBudget(prepared(attempt + 1), contract, tools);
197
267
  // Exactly one reservation per extra call; a thrown budget/cancellation veto is final.
198
268
  await onProtocolCorrection?.({
199
269
  ...structuredClone(metadata), attempts: structuredClone(attempts),
200
- action: 'explicit-json-correction', attempt: attempt + 1,
270
+ action: 'explicit-json-correction', type: 'json-syntax', code: 'MODEL_INVALID_JSON',
271
+ ...(jsonIssue(metadata.diagnostic) ? { jsonIssue: jsonIssue(metadata.diagnostic) } : {}), attempt: attempt + 1,
201
272
  correction: attempt, maxJsonCorrections: correctionLimit,
202
273
  notice: 'Additional model response requested; original output not repaired or accepted.',
203
274
  });
@@ -213,6 +284,7 @@ export function createDshModel(llm, route, {
213
284
  });
214
285
  original.signal?.throwIfAborted();
215
286
  }
287
+ if (result.transportKind) await notify(onProtocolEvent, { action: 'json-transport-recovery', kind: result.transportKind, attempt });
216
288
  return result.text;
217
289
  }
218
290
  };