@feng0531/dsh-code-review 0.85.13 → 0.86.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/CORE-ALIGNMENT-TASKS.md +8 -2
- package/CORE-SOURCE-INVENTORY.md +4 -2
- package/README.md +17 -7
- package/RELEASE-NOTES.md +12 -17
- package/index.mjs +2 -2
- package/package.json +1 -1
- package/src/background-review.mjs +51 -1
- package/src/coverage-plan.mjs +16 -2
- package/src/dsh-model.mjs +80 -15
- package/src/group-primary.mjs +40 -0
- package/src/input-budget.mjs +2 -0
- package/src/model-contract.mjs +1 -0
- package/src/model-json.mjs +23 -1
- package/src/model-tools.mjs +58 -0
- package/src/model-wire-budget.mjs +32 -0
- package/src/primary-seeds.mjs +7 -2
- package/src/project-execution.mjs +3 -2
- package/src/project-review-service.mjs +47 -10
- package/src/retrieval-loop.mjs +12 -10
- package/src/review-artifacts.mjs +117 -0
- package/src/review-diagnostics.mjs +21 -9
- package/src/review-jobs.mjs +6 -2
- package/src/review-reader-allocation.mjs +75 -0
- package/src/review-reader-policy.mjs +1 -1
- package/src/review-resume.mjs +53 -0
- package/src/review.mjs +185 -54
- package/src/search-seeds.mjs +2 -1
- package/src/service.mjs +25 -8
- package/src/snapshot-reader.mjs +62 -22
- package/src/verification-loop.mjs +2 -1
- package/src/verification-seeds.mjs +18 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.1 — 修复取证额度分配、重复读取与工件容量
|
|
6
|
+
|
|
7
|
+
- 同快照精确操作复用,重复交付仍扣字节并重新签发复核回执;根预算内为主审、综合及复核预留最低份额,释放闲置额度。
|
|
8
|
+
- 已校验组成员保留,组受限原因传播不重复计数;后续综合预算失败不改写已完成窗口成果。
|
|
9
|
+
- 工件默认数量上限64改为4096,单件1MiB/任务8MiB不变,超限仍明确标记。
|
|
10
|
+
- 终态摘要显示报告真实partial/覆盖与分页总量,跨文件综合不再硬编码误报超时;增加74文件规模化离线回归。
|
|
11
|
+
|
|
12
|
+
## 0.86.0 — 原生结构化输出、私有留存、断点续审与组主审
|
|
13
|
+
|
|
14
|
+
- 优先使用真实宿主原生结果工具并保留同次文本兼容;只接受未改写的完整 JSON,歧义失败关闭,证据校验不放宽。
|
|
15
|
+
- 后台私有保存有界原始输出,超限明确省略;显式隐私同意后可按所有者读取,不进入普通诊断或自动复用为证据。
|
|
16
|
+
- 完整快照与配置身份匹配后,重新预览、确认、授权才能复用已完成文件链;历史证据标注既往轮次,变更和项目模式均支持。
|
|
17
|
+
- 分组前置避免首文件链串行阻塞;小型完整精确变更组共享一次主审,保留独立交互证据及候选复核,输入预算与预览使用真实原生传输字节。
|
|
18
|
+
- 修复调用额度耗尽后丢弃已生成组结果,以及取消与落盘失败同时发生时丢失原取消身份的边界。
|
|
19
|
+
|
|
5
20
|
## 0.85.13 — 修复大上下文下的无效分窗和额外规划
|
|
6
21
|
|
|
7
22
|
0.85.12标签冻结后发现离线基准无baseline入口断言错误,未正式发布且不移动标签;本版修正入口并重新验证发布。
|
package/CORE-ALIGNMENT-TASKS.md
CHANGED
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
| Git/SVN变更采集 | Git工作副本净变化/暂存区/未暂存范围、固定历史端点、同仓库SVN分支比较 | 任意SVN状态均可审查、完整提交逐一覆盖 |
|
|
10
10
|
| 范围与来源保护 | 先预览后确认,固定来源与哈希,执行前重新采集 | 普通源码一定没有秘密、模型提供方不保存数据 |
|
|
11
11
|
| 上下文 | 显式文件、旧侧文件、最多三层字面依赖、限定目录引用方 | 完整语义依赖图、调用图或动态执行可达性 |
|
|
12
|
-
| 输入与调度 |
|
|
13
|
-
|
|
|
12
|
+
| 输入与调度 | 独立业务分组、2~4 完整文件组主审、差异窗口和发送前回退;原生工具/契约字节纳入共享输入预算 | 组主审替代专门交互证据、大仓库一定能完成或必然提速 |
|
|
13
|
+
| 结果传输 | 原生结果工具,同次文本兼容;仅原生模式接受完整 JSON 后明确尾注 | 修补损坏 JSON、猜字段/身份、无歧义解析所有输出 |
|
|
14
|
+
| 显式续审 | 新预览通过完整快照、配置和路由身份核验后复用已完成合格文件链 | 恢复旧授权、变化后无条件复用、未完成阶段已覆盖 |
|
|
15
|
+
| 私有模型工件 | 宿主任务目录外于业务工作区,有界保存文本/工具参数及失败工件,普通报告只给摘要 | 工件成为批准来源、失败草稿成为确认发现、Windows 模式位保证 ACL |
|
|
16
|
+
| 取证资源 | 同快照精确操作复用、每次交付计字节、阶段最低额度及闲置份额释放 | 根预算增加、每阶段一定有新读额度、缓存即本阶段证据 |
|
|
17
|
+
| 问题复核 | 原文位置检查、模型反证与状态记录、每次重新签发回执 | 缺陷必然成立、无漏报或无误报 |
|
|
14
18
|
| 文件指导 | 语言、协议、配置与模板的保守检查提示 | 编译器、类型检查器或真实运行结果 |
|
|
15
19
|
|
|
16
20
|
## 当前限制
|
|
@@ -20,6 +24,8 @@
|
|
|
20
24
|
- 替换旧侧可采用用户明确选择的固定仓库基线,但这不等于恢复了丢失的本地BASE。
|
|
21
25
|
- 所有测试和安装验证只证明对应机制的行为,不能外推真实模型质量。当前没有独立质量基准成绩;历史试跑见 [评测说明](evaluation/README.md)。
|
|
22
26
|
- 不执行项目代码,不自动修复、提交、发布业务项目,也不建设独立模型计费或管理平台。
|
|
27
|
+
- 续审不复用任意未完成中间对话;旧发现保留 priorRun 来源,不能冒充本轮证据。工件可能含源码,默认限额和显式取回边界见 [README](README.md)。
|
|
28
|
+
- 本轮机制变更不等于完整上游能力/质量对齐;没有真实模型质量、速度或成本的新基准结论,也不以文档更新声明已发布。
|
|
23
29
|
|
|
24
30
|
## 如何查看版本证据
|
|
25
31
|
|
package/CORE-SOURCE-INVENTORY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
供维护者核查实现依据。使用方法见 [README](README.md),开发优先级见 [能力清单](CORE-ALIGNMENT-TASKS.md)。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
当前本轮对照固定为 alibaba/open-code-review 提交 `375f64cd6f67a48a135da83cfcf59c52669a09cb`,不随上游分支变化。下方文件索引仍是旧提交 `2d67596c961f80436deb2afa643efa2b1725d34c` 的历史阅读记录;未把旧记录冒充新提交逐文件复审。
|
|
6
6
|
|
|
7
7
|
## 对照结论
|
|
8
8
|
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
- 默认一次审查,可选三次,不照搬参考默认次数和对话继承方式。
|
|
26
26
|
- 历史重命名按删除和新增表达;区间比较使用明确端点,不自动计算合并基点。
|
|
27
27
|
- 参考中的风格偏好、无条件性能建议和错误绝对规则不作为缺陷标准。
|
|
28
|
-
-
|
|
28
|
+
- 现已提供身份绑定的任务检查点与显式续审:新预览和新确认后,只复用完整快照/配置/路由一致的已完成合格文件链;不是上游任意会话/覆盖集合恢复,也没有 waived 豁免机制。
|
|
29
|
+
- 生产模型传输使用原生结果工具和同次文本兼容,专门交互校验仍独立;不宣称“整段 JSON 必须完全有效”本身优于上游。仅原生模式容忍完整 JSON 后明确自然语言尾注,歧义或损坏内容仍失败关闭。
|
|
30
|
+
- 本插件后台任务留存有界私有模型工件,普通诊断不公开原文。上游原始响应日志是通过 `OCR_RAW_LOGGING=1` 显式启用的选项,不能描述为无条件默认记录;两者的隐私和恢复边界不同。
|
|
29
31
|
- 不做 pre-run token 预估警告:用共享输入字节与调用预算在运行中约束,实际覆盖以报告终态为准。
|
|
30
32
|
- 未覆盖项保留 pending 并报 partial,不扫成 failed:诚实暴露未完成,不伪造终态。
|
|
31
33
|
|
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
|
|
238
|
+
变更审查的独立文件链默认最多 2 条并行;配置 `changesConcurrency` 为 1~4(1 回退串行),跨文件综合等待相关前置主审结束。配置 `maxRetrievalRounds` 为 0~50,默认为 3,限制每阶段检索请求回合数(不是成功读取次数;非法请求也会消耗回合)(0 只允许最终答复)。配置不是 preview 参数,不改变模型或来源授权;并发仍共享同一份读取/调用预算,额度不足时完成顺序可能影响哪些任务未完成,不能保证与串行逐项结果相同。提供方限流时应降低并发。复核可预取批准快照中候选锚点附近的同侧原文,仍允许追加反证读取并逐字校验;不复用主审结论或其他会话凭据。完整变更计划可容纳时仅生成一次再分发到窗口,否则保留原分窗口计划并在预览估算中体现。
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
大文件超出单次输入时按原行号分段。精确差异算不出时使用明确标注的包络,可能包含未变行,不声称精确归因。完整结束但 JSON 语法无效时会按当前阶段 schema 最多请求两次明确纠正,记录额外调用;检索请求非法时整批不执行并明确纠正,回填采用可追溯的滚动工作集、必要时延后重复窗口正文,不虚构证据。主审与复核默认各最多 3 轮检索;到限后只允许最终答复,保留未完成范围,不把已用完预算当成审核通过。不会给错误输出补括号、猜字段或当成无缺陷。
|
|
241
|
+
|
|
242
|
+
### 显式续审与私有模型工件
|
|
243
|
+
|
|
244
|
+
需要补审时,在**新预览**中传入 `resumeFromJobId`,例如 `{"resumeFromJobId":"原执行任务的真实jobId"}`;仍须展示新范围、重新确认并通过当前宿主权限。只复用同一会话、工作目录、模型路由/思考设置下,完整快照、配置和运行时身份均匹配的已完成且符合复用条件的文件链;项目模式还绑定对应批次。来源或配置变化不做猜测复用。未完成阶段重新执行,旧发现标记 `priorRun`,不伪造本轮读取凭据、模型调用或覆盖。
|
|
245
|
+
|
|
246
|
+
后台审查会在上述宿主任务目录的 `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 保证。
|
|
241
247
|
|
|
242
248
|
## 多个会话同时审查
|
|
243
249
|
|
|
244
|
-
可以同时打开多个DSH Agent会话:会话A审项目A,会话B审项目B,也可以分别审查同一项目。各会话的预览、执行锁、模型路由、取消和预算独立;A正在执行不会让B收到全局busy
|
|
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
|
-
|
|
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,13 +330,15 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
322
330
|
|
|
323
331
|
## 调用效率与预算
|
|
324
332
|
|
|
333
|
+
业务分组在文件链启动前独立完成,不以先跑完第一个文件作引导。适合的 2~4 个文件可合并为一次组主审:必须是精确变更、完整正文且可容纳的非项目模式成员,响应逐个返回原文件身份并通过各自校验。窗口化、超预算或不适合的组在发送前回退单文件;专门跨文件交互阶段及独立复核仍保留,组主审不等于跨文件证据已证明。可合并的风险假设是可选信息,缺失/无效会记录限制,不为它单独追加调用。
|
|
334
|
+
|
|
325
335
|
变更模式默认优先保留主文件全文,仅附带有导入/调用等关系的完整上下文对象(序列化合计最多16KiB或输入上限四分之一,取较小值);其余批准来源保留版本目录按需读取,不表示不存在或已核查。完整正文,或经过精确新旧编辑、坐标和原文核对且覆盖全部编辑的单批窗口,可将风险问题合入首轮主审。多批窗口、包络差异和项目审查仍保留独立规划;保留独立候选复核和跨窗口/跨文件综合,不以丢弃窗口换取速度。报告中的合并问题不是独立风险规划或已验证条件。
|
|
326
336
|
|
|
327
337
|
模型搜索的精确命中可同轮附带批准来源原文,每阶段最多2次额外读取尝试、每次最多24行;占用原共享额度并预留4次操作/16KiB输出,不猜测另侧坐标,不增加检索轮数。配置 `focusPrimaryContext:false`、`enableSearchSeeds:false` 可分别关闭上下文聚焦和搜索命中附读;无检索能力时不会隐藏正文。
|
|
328
338
|
|
|
329
|
-
共享读取预算按本次可审文件数有界缩放,预览的 `execution.readerBudget`
|
|
339
|
+
共享读取预算按本次可审文件数有界缩放,预览的 `execution.readerBudget` 显示具体值;所有阶段共用、不逐文件刷新。生产编排在根上限内按主审规模及综合/复核单元权重预留最低额度,未用份额释放;最低份额取整可能为零,不保证每个阶段都有新读额度。同一批准快照的相同操作结果可复用,`calls` 计实际结果生成、缓存命中另计;每次交付仍计序列化字节,复核回执重新签发,跨快照不复用。必要证据不足仍保留未完成范围,不以提前返回当作完成。配置 `riskPlanningMode:"separate"`、`readerBudgetPolicy:"fixed"` 可回退原规划和固定读取额度,这些是插件配置,不是preview参数。更多额度不保证更快或完整覆盖;不建议用提高并发掩盖冗余调用。
|
|
330
340
|
|
|
331
|
-
诊断显示实际加载版本、阶段及调用目的计数、读取余量,并区分单阶段检索轮数和共享读取额度。新增无源码的JSON
|
|
341
|
+
诊断显示实际加载版本、阶段及调用目的计数、读取余量,并区分单阶段检索轮数和共享读取额度。新增无源码的JSON纠正原因、协议/审查格式纠正计数及未合并规划原因;纠正计数以实际纠正调用为准;这些诊断不含原响应、源码片段或提供方错误文本,与下面说明的私有模型工件分开。`primary-initial` 是检索链初始调用,不只包含主审阶段。累计调用耗时可能并发重叠,不能当作总耗时或纯提供方耗时。离线调用数减少不代表实际审核必然同比提速,不能用更快返回partial冒充审核完成。
|
|
332
342
|
|
|
333
343
|
## 规模限制
|
|
334
344
|
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
# v0.
|
|
1
|
+
# v0.86.1 发布说明
|
|
2
2
|
|
|
3
|
-
使用方式见 [README](README.md)
|
|
3
|
+
使用方式见 [README](README.md),历史变化见 [变更记录](CHANGELOG.md),发布门禁见 [开发说明](DEVELOPMENT.md)。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 取证预算与成果保留
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- 合并规划不再仅限全文传输:单批窗口必须与批准文件重新计算的全部精确编辑、新旧坐标、分段原文及快照/文件身份完全匹配,才可合入首轮主审。多批窗口、包络差异、项目模式和后续独立轮次继续原策略,不用丢弃编辑换速度。合并风险仍不是独立规划或已验证条件;候选独立复核和跨窗口/文件综合保留。
|
|
9
|
-
- 模型搜索的精确命中可同轮附带原文,单阶段最多两次读取尝试、每次最多24行,失败也计尝试;验证来源/快照/哈希及版本侧,不猜测另侧坐标。所有读取共用原reader,预留4次操作/16KiB输出,精确输入放不下则事务回滚;不增加检索轮次、不造复核receipt、不重复已有正文。
|
|
10
|
-
- 生产变更入口默认启用上述聚焦和搜索命中附读;可用插件配置focusPrimaryContext:false和enableSearchSeeds:false关闭,底层API默认关闭。预览披露选项和上下文提供/递延数,按相同策略计算输入与最低调用。
|
|
7
|
+
生产编排为主审、跨窗口/跨文件综合及候选复核在同一个根读取预算内按权重预留最低额度,阶段结束释放闲置份额;不提高根调用或输出字节上限。最低份额取整可能为零,额度和检索轮数不足仍如实披露,不能保证所有来源或阶段完成。
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
同一批准快照内,相同读取/搜索/查找请求可复用原始操作结果,身份、版本侧、坐标、查询和上限均参与匹配。返回独立副本;每次交付仍扣序列化输出字节,复核回执重新签发,不继承其他阶段凭据。缓存和分配账本参与原子事务回滚;失败、取消、源身份变化不放宽边界。低层默认保持原语义,仅生产优化编排启用。
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
已经校验的组成员不因后续读取准入失败被丢弃;组级受限原因传播至所有成员,但共享事件只计一次。跨窗口/跨文件阶段预算失败单独记录,不把已完成主审/窗口成果改写成主审失败;复用资格仍拒绝未完成综合、受限阶段或未复核候选。
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
## 工件容量与终态呈现
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
默认工件数量上限从64改为4096,单记录1MiB、单任务8MiB不变;真实数量/字节超限、截断仍明确记录,不宣称无限留存。普通诊断不含原文;取回仍需源码隐私知情同意。
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
任务结束的状态摘要显示真实报告状态、覆盖、调用、读取余量及分页总量,明确后台 completed 不等于报告 complete;一次 terminal wait 仍返回整份报告。跨文件综合不再统一误写为超时,而显示已有原因及失败代码。
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
## 验证与边界
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
新增离线夹具覆盖74文件共享读取、早期贪婪请求与后续复核额度、组限制传播、已完成窗口保留、零新读额度下缓存交付与新回执、事务回滚及128个小工件全部留存。它们证明机制,不代表已重跑用户业务,也不构成真实模型速度、覆盖或质量基准。
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
不会自动安装、升级或重启当前DSH。更新并重新加载后,以报告显示v0.85.13确认实际加载。
|
|
23
|
+
正式发布须通过冻结标签 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, focusPrimaryContext: config.focusPrimaryContext ?? true, enableSearchSeeds: config.enableSearchSeeds ?? 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
|
@@ -15,10 +15,46 @@ const CANCEL_PARAMETERS = { type: 'object', additionalProperties: false, propert
|
|
|
15
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.';
|
|
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
|
+
const TERMINAL_STATUSES = ['completed', 'failed', 'cancelled', 'interrupted'];
|
|
19
|
+
const REPORT_STATUSES = ['completed', 'partial', 'failed', 'cancelled'];
|
|
20
|
+
const COVERAGE_FIELDS = ['completed', 'excluded', 'blocked', 'failed', 'cancelled', 'pending'];
|
|
21
|
+
const LIMIT_CODES = ['READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED', 'RETRIEVAL_ROUND_BUDGET_EXCEEDED'];
|
|
22
|
+
const count = value => Number.isSafeInteger(value) && value >= 0 ? value : '不可用';
|
|
23
|
+
function terminalSummary(value) {
|
|
24
|
+
const report = value.resultSummary;
|
|
25
|
+
const lines = ['审查任务已结束:' + value.status, 'jobId: ' + value.jobId];
|
|
26
|
+
if (value.progress) lines.push('elapsedMs: ' + count(value.progress.elapsedMs));
|
|
27
|
+
if (report && REPORT_STATUSES.includes(report.status)) {
|
|
28
|
+
lines.push('报告状态:' + report.status + ';任务结束不等于所有审查通过。');
|
|
29
|
+
lines.push('文件覆盖:' + COVERAGE_FIELDS.map(key => key + ' ' + count(report.coverage?.[key])).join(';'));
|
|
30
|
+
lines.push('总文件:' + count(report.totalFiles) + ';总发现:' + count(report.totalFindings) + ';限制项:' + count(report.totalLimitations));
|
|
31
|
+
lines.push('模型调用:' + count(report.modelCalls ?? report.diagnostics?.modelCalls));
|
|
32
|
+
const d = report.diagnostics;
|
|
33
|
+
if (d?.readerBudget) {
|
|
34
|
+
const b = d.readerBudget;
|
|
35
|
+
lines.push('共享读取预算:调用 ' + count(b.calls) + '/' + count(b.maxCalls) + ',输出字节 ' + count(b.outputBytes) + '/' + count(b.maxOutputBytes) + ';剩余调用 ' + count(b.remainingCalls) + ',剩余字节 ' + count(b.remainingOutputBytes));
|
|
36
|
+
}
|
|
37
|
+
const reasons = Array.isArray(d?.readerLimitReasons) ? d.readerLimitReasons : [];
|
|
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(';'));
|
|
40
|
+
if (d?.readerLimits) lines.push('每阶段最大检索轮次:' + count(d.readerLimits.maxRounds));
|
|
41
|
+
if (d?.artifacts) lines.push('私有工件计数:留存 ' + count(d.artifacts.retained) + ',未留存 ' + count(d.artifacts.omitted) + '(不包含原始正文)');
|
|
42
|
+
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
|
+
lines.push('完整报告:调用一次 code_review_wait(jobId 同上,waitFor=terminal)立即取得最终报告;无需继续轮询。');
|
|
44
|
+
} else if (report?.previewId) {
|
|
45
|
+
lines.push('预览已完成,尚未执行审查;请展示批准范围并取得用户确认后再执行。');
|
|
46
|
+
lines.push('预览文件总量:' + count(report.filePage?.total ?? report.files?.length) + ';计划最少调用:' + count(report.plan?.minimumCalls));
|
|
47
|
+
} else {
|
|
48
|
+
lines.push('未提供完整报告摘要;终态不会自动恢复或重新发送源码。请检查已保存检查点和未完成范围。');
|
|
49
|
+
}
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
18
53
|
function respond(value) {
|
|
19
54
|
if (value.finalMarkdown !== null && value.finalMarkdown !== undefined) {
|
|
20
55
|
return { json: JSON.stringify(value), markdown: value.finalMarkdown };
|
|
21
56
|
}
|
|
57
|
+
if (TERMINAL_STATUSES.includes(value.status)) return { json: JSON.stringify(value), markdown: terminalSummary(value) };
|
|
22
58
|
const p = value.progress;
|
|
23
59
|
if (p) {
|
|
24
60
|
const progress = p.total === null
|
|
@@ -35,11 +71,14 @@ async function startBackgroundJob(args, exec, jobs, service) {
|
|
|
35
71
|
if (args.phase === 'execute' && (args.confirmed !== true || typeof args.previewId !== 'string' || args.previewArgs !== undefined)) throw new Error('Confirmed preview required');
|
|
36
72
|
if (args.phase === 'preview' && (args.previewId !== undefined || args.confirmed !== undefined)) throw new Error('Preview cannot confirm execution');
|
|
37
73
|
|
|
38
|
-
return respond(await jobs.start(exec, async ({ signal, checkpoint }) => {
|
|
74
|
+
return respond(await jobs.start(exec, async ({ signal, checkpoint, artifact, saveResumeState }) => {
|
|
39
75
|
const original = exec.agent;
|
|
40
76
|
const sessionId = original.session.header.id ?? original.session.id;
|
|
41
77
|
const detached = {
|
|
42
78
|
...exec, agent: original, signal,
|
|
79
|
+
resolveReviewResume: id => jobs.resume(id, exec),
|
|
80
|
+
reviewResumeCheckpoint: saveResumeState,
|
|
81
|
+
onModelArtifact: artifact,
|
|
43
82
|
reviewCheckpoint: async value => {
|
|
44
83
|
const active = exec.agent;
|
|
45
84
|
if (active !== original || (active.session.header.id ?? active.session.id) !== sessionId) throw new Error('Background Agent/session changed');
|
|
@@ -82,6 +121,17 @@ export function registerBackgroundReview(ctx, service, { directory } = {}) {
|
|
|
82
121
|
},
|
|
83
122
|
});
|
|
84
123
|
|
|
124
|
+
ctx.tools.register({
|
|
125
|
+
name: 'code_review_artifacts',
|
|
126
|
+
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.',
|
|
127
|
+
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'] },
|
|
128
|
+
output: reviewToolOutput,
|
|
129
|
+
async execute(args, exec) {
|
|
130
|
+
if (args.confirmed !== true) throw new Error('Explicit consent required to expose source-bearing private model artifacts');
|
|
131
|
+
const result = await jobs.artifacts(args.jobId, exec, { offset: args.offset ?? 0, limit: args.limit ?? 1 });
|
|
132
|
+
return { json: JSON.stringify({ untrusted: true, sourceBearing: true, notice: 'Raw model output is diagnostic data only; not approved evidence or accepted findings.', ...result }), markdown: '私有原始模型响应:可能包含源码,仅供诊断;不是已批准证据或已接受发现。' };
|
|
133
|
+
},
|
|
134
|
+
});
|
|
85
135
|
ctx.on('dispose', () => jobs.dispose());
|
|
86
136
|
return jobs;
|
|
87
137
|
}
|
package/src/coverage-plan.mjs
CHANGED
|
@@ -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
|
-
|
|
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');
|
|
@@ -41,16 +43,16 @@ export function createDshModel(llm, route, {
|
|
|
41
43
|
const jsonIssues = new Set(['property-name', 'colon', 'separator', 'invalid-escape', 'control-character', 'unterminated-string', 'unexpected-end', 'trailing-content', 'syntax']);
|
|
42
44
|
const jsonIssue = diagnostic => jsonIssues.has(diagnostic?.syntaxCategory) ? diagnostic.syntaxCategory : undefined;
|
|
43
45
|
const purposes = new Set(['primary-initial', 'retrieval-followup', 'budget-finalization', 'json-correction', 'business-grouping', 'risk-planning', 'window-synthesis', 'file-interaction', 'verification']);
|
|
44
|
-
function checkInputBudget(request, contract) {
|
|
46
|
+
function checkInputBudget(request, contract, tools = []) {
|
|
45
47
|
if (request.maxInputBytes === undefined) return;
|
|
46
48
|
if (!Number.isSafeInteger(request.maxInputBytes) || request.maxInputBytes < 1) throw new Error('Invalid model input budget');
|
|
47
|
-
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);
|
|
48
50
|
if (wireInputBytes > request.maxInputBytes) {
|
|
49
51
|
throw Object.assign(new Error('Model wire input budget exceeded'), { code: 'MODEL_INPUT_BUDGET_EXCEEDED', diagnostic: { wireInputBytes, maxInputBytes: request.maxInputBytes } });
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
async function once({ instructions, input, signal, onModelEvent, attempt, callPurpose }, contract) {
|
|
55
|
+
async function once({ instructions, input, signal, onModelEvent, onModelArtifact, reviewPhase, attempt, callPurpose }, contract, tools) {
|
|
54
56
|
signal?.throwIfAborted();
|
|
55
57
|
let output = '';
|
|
56
58
|
let size = 0;
|
|
@@ -81,7 +83,7 @@ export function createDshModel(llm, route, {
|
|
|
81
83
|
...identity,
|
|
82
84
|
system: instructions + '\n' + contract,
|
|
83
85
|
messages: [{ role: 'user', content: [{ type: 'text', text: input }] }],
|
|
84
|
-
tools
|
|
86
|
+
tools, maxTokens, signal,
|
|
85
87
|
});
|
|
86
88
|
} finally {
|
|
87
89
|
await notify(onModelEvent, { ...event, action: 'model-start' });
|
|
@@ -93,7 +95,25 @@ export function createDshModel(llm, route, {
|
|
|
93
95
|
firstChunkMs ??= Math.max(0, performance.now() - startedAt);
|
|
94
96
|
signal?.throwIfAborted();
|
|
95
97
|
if (finished) throw failure('Model emitted data after finish', 'MODEL_PROTOCOL_ERROR');
|
|
96
|
-
if (isToolCall(chunk))
|
|
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
|
+
}
|
|
97
117
|
const delta = chunk.type === 'text-delta' || chunk.type === 'reasoning-delta';
|
|
98
118
|
const completed = chunk.type === 'block-end' &&
|
|
99
119
|
(chunk.block.type === 'text' || chunk.block.type === 'reasoning');
|
|
@@ -110,13 +130,13 @@ export function createDshModel(llm, route, {
|
|
|
110
130
|
// A shorter final snapshot must not refund previously consumed output budget.
|
|
111
131
|
const chargedBytes = Math.max(previous?.chargedBytes ?? 0, bytes);
|
|
112
132
|
size += chargedBytes - (previous?.chargedBytes ?? 0);
|
|
113
|
-
if (size > maxOutputBytes) throw failure('Model output limit exceeded', 'MODEL_OUTPUT_LIMIT');
|
|
114
133
|
blocks.set(key, { type, text, chargedBytes, closed: completed });
|
|
115
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');
|
|
116
136
|
}
|
|
117
137
|
if (chunk.type === 'finish') {
|
|
118
138
|
finishKind = chunk.reason.kind;
|
|
119
|
-
if (finishKind !== 'stop') {
|
|
139
|
+
if (finishKind !== 'stop' && !(tools.length && finishKind === 'tool-calls')) {
|
|
120
140
|
const error = failure('Model did not complete: ' + finishKind, 'MODEL_INCOMPLETE_OUTPUT');
|
|
121
141
|
Object.assign(error.diagnostic, providerDiagnostic(chunk.reason));
|
|
122
142
|
throw error;
|
|
@@ -142,11 +162,18 @@ export function createDshModel(llm, route, {
|
|
|
142
162
|
signal?.throwIfAborted();
|
|
143
163
|
if (!finished) throw failure('Model stream ended without finish', 'MODEL_INCOMPLETE_OUTPUT');
|
|
144
164
|
try {
|
|
145
|
-
const
|
|
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;
|
|
146
172
|
status = 'completed';
|
|
147
|
-
return { text, diagnostic: diagnostic() };
|
|
173
|
+
return { text, diagnostic: diagnostic(), transportKind };
|
|
148
174
|
} catch (error) {
|
|
149
175
|
error.diagnostic = { ...diagnostic(), ...error.diagnostic };
|
|
176
|
+
if (error.code !== 'MODEL_INVALID_JSON') throw error;
|
|
150
177
|
failureCode = 'MODEL_INVALID_JSON';
|
|
151
178
|
issue = jsonIssue(error.diagnostic);
|
|
152
179
|
invalidJsonErrors.add(error);
|
|
@@ -154,20 +181,57 @@ export function createDshModel(llm, route, {
|
|
|
154
181
|
throw error;
|
|
155
182
|
}
|
|
156
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
|
+
}
|
|
157
217
|
await notify(onModelEvent, {
|
|
158
218
|
...event, action: 'model-end', status: signal?.aborted ? 'cancelled' : status,
|
|
159
219
|
...(failureCode ? { failureCode } : {}), ...(issue ? { jsonIssue: issue } : {}),
|
|
160
220
|
durationMs: Math.max(0, performance.now() - startedAt), firstChunkMs,
|
|
161
221
|
visibleBytes: Buffer.byteLength(output), totalOutputBytes: size,
|
|
162
222
|
});
|
|
223
|
+
if (artifactError) { signal?.throwIfAborted(); throw artifactError; }
|
|
163
224
|
}
|
|
164
225
|
}
|
|
165
226
|
|
|
166
227
|
return async function invoke(request) {
|
|
167
228
|
// Snapshot these before an async budget/progress hook can mutate the request.
|
|
168
|
-
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 };
|
|
169
230
|
const onProtocolCorrection = request.onProtocolCorrection, onProtocolEvent = request.onProtocolEvent;
|
|
170
|
-
const
|
|
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 : '');
|
|
171
235
|
const correctionLimit = correctJson ? maxJsonCorrections : 0;
|
|
172
236
|
const attempts = [];
|
|
173
237
|
const prepared = attempt => ({ ...original, attempt, instructions: original.instructions +
|
|
@@ -175,10 +239,10 @@ export function createDshModel(llm, route, {
|
|
|
175
239
|
for (let attempt = 1; ; attempt++) {
|
|
176
240
|
original.signal?.throwIfAborted();
|
|
177
241
|
const next = prepared(attempt);
|
|
178
|
-
checkInputBudget(next, contract);
|
|
242
|
+
checkInputBudget(next, contract, tools);
|
|
179
243
|
let result;
|
|
180
244
|
try {
|
|
181
|
-
result = await once(next, contract);
|
|
245
|
+
result = await once(next, contract, tools);
|
|
182
246
|
original.signal?.throwIfAborted();
|
|
183
247
|
} catch (error) {
|
|
184
248
|
const metadata = failureMetadata.get(error) ?? {};
|
|
@@ -199,7 +263,7 @@ export function createDshModel(llm, route, {
|
|
|
199
263
|
}
|
|
200
264
|
original.signal?.throwIfAborted();
|
|
201
265
|
// Do not reserve or announce a correction that cannot fit the wire budget.
|
|
202
|
-
checkInputBudget(prepared(attempt + 1), contract);
|
|
266
|
+
checkInputBudget(prepared(attempt + 1), contract, tools);
|
|
203
267
|
// Exactly one reservation per extra call; a thrown budget/cancellation veto is final.
|
|
204
268
|
await onProtocolCorrection?.({
|
|
205
269
|
...structuredClone(metadata), attempts: structuredClone(attempts),
|
|
@@ -220,6 +284,7 @@ export function createDshModel(llm, route, {
|
|
|
220
284
|
});
|
|
221
285
|
original.signal?.throwIfAborted();
|
|
222
286
|
}
|
|
287
|
+
if (result.transportKind) await notify(onProtocolEvent, { action: 'json-transport-recovery', kind: result.transportKind, attempt });
|
|
223
288
|
return result.text;
|
|
224
289
|
}
|
|
225
290
|
};
|