@feng0531/dsh-code-review 0.87.2 → 0.87.4

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,17 @@
2
2
 
3
3
  本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
4
4
 
5
+ ## 0.87.4 — 默认交付人能读懂的审核报告
6
+
7
+ - 默认主文先给是否查完、问题卡、未完成原因和下一步;完整证据与执行台账移到同一报告折叠附录,不要求使用者手动拼接批次。
8
+ - 按原文聚合限制并保留归属和记录;区分轮数、读取分配/共享额度、输入预算及依赖,空发现不暗示通过。
9
+ - 严格区分复核支持、待核实、原有和已反驳;保留项目冲突候选及分页/过期/复用限制,修正 SVN 历史标签。仅本地呈现,不改变 JSON、取证或证据门槛。
10
+
11
+ ## 0.87.3 — 明确审核指令不再重复确认
12
+
13
+ - 明确要求审核且内部预览与当前授权范围、模型及发送目的地一致时,工具指导直接继续执行,不再要求第二次人工确认;仅预览或超出授权时仍暂停。
14
+ - 保留 confirmed 参数、快照与版本新鲜度、会话/目录/模型身份、一次性预览及宿主权限检查;私有模型工件仍需独立明确同意。
15
+
5
16
  ## 0.87.2 — 导出案例回归修复
6
17
 
7
18
  - 大文件稀疏、远距离编辑增加有界最短编辑回退,避免二维差异表超限后不必要地扩大为近全文包络;超预算仍明确受限。
package/DEVELOPMENT.md CHANGED
@@ -18,6 +18,12 @@
18
18
 
19
19
  项目捕获完整清单最多 10000 节点、1000 选择节点、16 MiB。Git 显式选择以精确字面路径查询所选索引,单次查询最多 64 KiB、总计 2 MiB,不受未选索引数量影响;初次、执行前及末尾复核使用相同范围。局部清单不宣称全仓完整性或总数,只绑定所选索引和正文,未选索引变动不使其失效。公开工具的显式列表仍最多 200 项。不设默认总模型调用次数或执行秒表,初次捕获/复核均遵循宿主与用户取消。SVN 仍枚举本地完整已观察清单,项目模式拒绝不支持节点属性,不隐式继承固定历史审查的网络权限。
20
20
 
21
+ ## 预览与当前人类授权
22
+
23
+ - 明确要求“审核 r7364”等执行请求,在范围、模型和发送目的地明确且一致时就是当前审核授权;Agent 仍须先内部 preview,固定并核对 scope、route、budget 与权限,再使用真实 `previewId` 和 `confirmed:true` 执行,不重复询问确认。只要求预览或明确要求等待确认时,不得执行;范围、模型或发送目的地超出授权,或边界不明确时,先询问。SVN 远端读取等独立权限不由审核请求隐式授予。
24
+ - `confirmed:true` 是 Agent 对当前人类明确授权且本次预览匹配的声明,不是取消确认参数或无授权绕过。保留 preview→execute 两阶段与 service 确认参数,不新增一键执行 API;宿主/会话工具权限、模型发送许可、快照新鲜度、会话/目录/路由身份及预算校验均不变。
25
+ - 源变化、预览过期或身份变化须生成新 preview 并重新核对当前授权,不能将旧预览批准用于新快照。保存的后台记录、`resumeFromJobId`、自动续跑及 host 重启本身都不授予外发权限;显式续审须有当前人类授权和新预览,匹配时无需重复询问。私有模型工件的源码提示与独立明确同意要求保持不变。
26
+
21
27
  ## 变更审查调度与离线对照
22
28
 
23
29
  生产 changes 服务默认 `changesConcurrency=2`,配置范围 1~4;底层 `reviewSnapshot` 未传该选项时保持串行兼容。所有文件共用原批准快照 reader,不能按 worker 重置额度。业务分组先完成,交互 owner 等待此前成员链结束;窗口、轮次与复核仍串行依赖,固定索引归并结果。取消或 checkpoint 失败必须先排空所有在途链再释放锁。
@@ -80,6 +86,12 @@ JSON合同新增280字节传输提示防止原始换行/反斜线转义错误
80
86
 
81
87
  Git 路径和索引、SVN XML、规则文件与 SVN 链接记录仍按各自 UTF-8 协议校验,不因源码兼容而放宽;固定宽度关键字映射仍须独立核验,不支持的混杂编码或映射会明确阻断。
82
88
 
89
+ ## 默认报告呈现
90
+
91
+ 变更与当前源码模式统一输出一份人可读 Markdown:业务首屏结论、按“本次新增(复核支持)/当前源码问题(复核支持)/待核实/原有问题(复核支持)/已反驳”分区的问题卡、聚合未完成原因和下一步。完整证据与判定、逐项覆盖、风险计划、执行诊断和按原文聚合的限制放在同一报告的 `<details>` 技术附录;不改成需要用户拼接的批报告,JSON 仍完整保留原始记录。
92
+
93
+ 这是本地呈现层调整,不改变取证算法、来源权限、独立复核或新旧对照门槛,不额外调用 LLM,也不由通俗主文代做源码复审。复核支持和引用校验不等于事实或因果证明;变更模式中缺少有效新旧对照的 supported 候选仍待核实;当前源码模式不要求历史对照,也不冒充本次新增。未完成原因区分检索轮数、共享读取调用数、读取字节、阶段输入预算和前置阶段未完成;未知原因不推测成输入超限,限制记录数不当作缺陷数或待审文件数。主文有界聚合不隐藏限制,全部范围与原始记录仍可在附录追溯。历史审查标签按实际模式显示 Git/SVN,不能把 SVN 历史误标为 Git。
94
+
83
95
  ## 性能与等待诊断
84
96
 
85
97
  风险计划只产生待证问题和后续来源需求,不再自行展开检索对话;计划不合法则明确回退,主审、跨文件分析和独立证据复核保持各自严格校验,不把回退当作完成。规划原始调用与 JSON 纠正仍使用同一共享调用预算。批准目录附各来源物理 `lineCount`(空源为 0,不计尾换行的虚拟空行),旧/新侧分别取界,不将越界读取钳位成成功或伪造证据。
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DSH Code Review
2
2
 
3
- 在 **DeepSeek Harness(DSH)的 Agent 对话里**审查 Git / SVN 代码变更。插件会先列出审查范围,得到确认后,再用当前 Agent 选择的模型生成中文问题报告。
3
+ 在 **DeepSeek Harness(DSH)的 Agent 对话里**审查 Git / SVN 代码变更。插件先预览并核对审查范围,再在当前人类明确授权内,用当前 Agent 选择的模型生成中文问题报告;已明确要求审核且预览匹配时,不重复询问确认。
4
4
 
5
5
  **只读:不修改代码,不自动修复,不提交、回滚或更新你的业务工作副本。**
6
6
 
@@ -53,7 +53,9 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
53
53
  确认刚才展示的预览范围和模型,使用该 previewId 执行只读审查。
54
54
  ```
55
55
 
56
- 插件使用预览返回的 ID 执行。默认预览有效期约 **5 分钟**,返回 `expiresAt` 与 `validForMs`;到期、文件、模型或会话目录变化后,需要重新预览和确认。计划最少调用只是初始下限,不是总调用数或耗时上限,后续取证、复核与综合会增加工作量。宿主权限不允许时,确认也不能绕过限制。
56
+ 以上是“只预览,等我确认”的流程。若你已明确要求“审核 r7364”(SVN 远端访问仍须单独授权),Agent 应先内部预览,固定并核对范围、模型路由、预算与权限;预览符合当前明确授权时,直接执行,不再要求你重复确认。只要求预览或明确要求等待确认时不得执行;范围、模型或发送目的地超出授权,或授权边界不明确时,先询问。
57
+
58
+ 插件使用预览返回的 ID 执行。默认预览有效期约 **5 分钟**,返回 `expiresAt` 与 `validForMs`;到期、文件、模型或会话目录变化后,需要新预览并重新核对当前授权,不能复用旧预览批准。`confirmed:true` 表示当前人类已明确授权且本次预览与授权一致,不是无授权绕过。计划最少调用只是初始下限,不是总调用数或耗时上限,后续取证、复核与综合会增加工作量。会话工具权限与宿主安全校验照旧,确认不能绕过限制。
57
59
 
58
60
  ### 后台执行如何等待
59
61
 
@@ -61,13 +63,15 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
61
63
 
62
64
  ### 5. 查看结果
63
65
 
64
- 执行工具会在对话中返回 **中文 Markdown 报告**,同时返回结构化 JSON。“执行诊断”列出实际加载的插件版本、阶段调用和墙钟统计;累计调用耗时可能并发重叠,不代表纯提供方耗时或可相加的总耗时。先看前置的“结论摘要”和问题列表,再核对覆盖与限制;摘要区分已确认新增、未决、已反驳和既有问题,不把仅 supported 或缺少旧新对照的候选当作确认新增。
66
+ 执行工具默认在对话中返回 **一份人可读的中文 Markdown 报告**,同时保留完整结构化 JSON。首屏先回答所选范围是否查完、记录了哪些候选和哪些结论尚不能下;问题卡按“本次新增(复核支持)”“当前源码问题(复核支持)”“待核实”“原有问题(复核支持)”“已反驳”分区,说明位置、触发条件、影响、依据及已有建议。分区仍遵循严格证据判定:仅 supported 或缺少有效旧新对照不能算本次新增,当前源码模式不作历史新增归因;**复核支持和引用校验不等于事实、运行复现或因果证明。**
67
+
68
+ 主文聚合未完成原因并给出下一步;完整证据与判定、逐项覆盖、风险计划、执行诊断及聚合限制保留在**同一报告的 `<details>` 技术附录**,展开即可核对,不需要拼接批报告。通俗主文只是已有结果的呈现,不代做源码复审、不隐藏限制、不放宽证据门槛,也不会为改写报告额外调用模型或改变取证算法。执行诊断列出实际加载的插件版本、阶段调用和墙钟统计;累计调用耗时可能并发重叠,不代表纯提供方耗时或可相加的总耗时。
65
69
 
66
70
  如需保存报告,可以另行告诉 Agent:“将本次报告保存到我指定的位置”。插件本身不会自动在业务仓库创建报告文件。
67
71
 
68
72
  ## 按场景直接使用
69
73
 
70
- 下面的自然语言可以直接发给 Agent。每个场景都应先预览、展示范围,再确认执行。
74
+ 下面的自然语言可以直接发给 Agent。其中只要求预览或明确要求等待确认的示例,不得直接执行;若直接明确要求审核,则按上面的授权规则内部核对预览后执行,不重复询问。
71
75
 
72
76
  ### 只审查几个文件(Git / SVN 工作副本)
73
77
 
@@ -224,16 +228,16 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
224
228
 
225
229
  ## 长任务:后台预览、审查与取回结果
226
230
 
227
- 全项目或慢仓库建议使用后台工具,不让一次外层调用等待整个项目。仍先预览,再由你确认外发;启动返回jobId不等于审核完成。
231
+ 全项目或慢仓库建议使用后台工具,不让一次外层调用等待整个项目。仍先预览并核对当前明确授权,匹配时直接执行;仅要求预览则停在预览,超授权则询问。启动返回jobId不等于审核完成。
228
232
 
229
233
  1. 调用code_review_start,phase设为preview,previewArgs填原预览参数(例如reviewMode=project)。
230
234
  2. 用code_review_status和jobId查询;完成后读取resultSummary中的预览范围、模型和previewId。
231
- 3. 展示预览,确认后调用code_review_start,phase设为execute,传previewId与confirmed=true。
235
+ 3. 内部检查预览的范围、模型、发送目的地、预算和权限;当前人类已明确要求匹配的审核时,不重复询问,调用code_review_start,phase设为execute,传previewId与confirmed=true。仅要求预览或等待确认时先展示并等待;超授权时先询问。
232
236
  4. 持续调用code_review_wait等待进度变化或终态,不因启动返回任务ID就结束用户请求。可展示阶段与“处理数/总数”,不分批汇报问题;抓取阶段分母未知则不显示虚假百分比。等待返回仍running就继续等待,不重新启动。任务结束后取回并统一交付一份最终报告。查询审查任务;offset/limit分页取回检查点、文件状态和发现;单个检查点内的大清单用checkpointIndex指定其零基索引,再用offset/limit取回内部全部条目。可调用code_review_cancel取消。
233
237
 
234
238
  报告保存在宿主profile的data/dsh-code-review/jobs目录;没有profile环境时使用用户主目录.dsh下的数据目录,不写入业务仓库。内容可能含源码片段,应按源码敏感级别保护。本次启动调用结束不自动取消后台任务;明确取消、停用插件或宿主退出会停止它。
235
239
 
236
- 重启后可按原jobId读回保存记录,原运行标记interrupted,不自动继续外发。recovery给出已观察文件/窗口与未完成路径;它不是完整分母或新授权,补审必须重新预览确认。新预览的runtime包含实际加载版本和模块位置,排查不同工作区时分别核对,不根据另一台机器推断。
240
+ 重启后可按原jobId读回保存记录,原运行标记interrupted,不自动继续外发。recovery给出已观察文件/窗口与未完成路径;它不是完整分母或新授权;宿主重启及自动恢复本身不授权外发,补审必须有当前明确授权并重新预览核对。新预览的runtime包含实际加载版本和模块位置,排查不同工作区时分别核对,不根据另一台机器推断。
237
241
 
238
242
  变更审查的独立文件链默认最多 2 条并行;配置 `changesConcurrency` 为 1~4(1 回退串行),跨文件综合等待相关前置主审结束。配置 `maxRetrievalRounds` 为 0~50,默认为 3,限制每阶段检索请求回合数(不是成功读取次数;非法请求也会消耗回合)(0 只允许最终答复)。配置不是 preview 参数,不改变模型或来源授权;并发仍共享同一份读取/调用预算,额度不足时完成顺序可能影响哪些任务未完成,不能保证与串行逐项结果相同。提供方限流时应降低并发。复核可预取批准快照中候选锚点附近的同侧原文,仍允许追加反证读取并逐字校验;不复用主审结论或其他会话凭据。完整变更计划可容纳时仅生成一次再分发到窗口,否则保留原分窗口计划并在预览估算中体现。
239
243
 
@@ -241,7 +245,7 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
241
245
 
242
246
  ### 显式续审与私有模型工件
243
247
 
244
- 需要补审时,在**新预览**中传入 `resumeFromJobId`,例如 `{"resumeFromJobId":"原执行任务的真实jobId"}`;仍须展示新范围、重新确认并通过当前宿主权限。只复用同一会话、工作目录、模型路由/思考设置下,完整快照、配置和运行时身份均匹配的已完成且符合复用条件的文件链;项目模式还绑定对应批次。来源或配置变化不做猜测复用。旧发现标记 `priorRun`,不伪造本轮读取凭据、模型调用或覆盖。变更模式开启阶段续审时,schema2 检查点还可保存已接受但所在文件链未完成的 primary 生成;它只省去匹配的主审生成,综合、交互和复核在新安全边界执行,复核重新取得独立回执。预览分别显示可复用文件、生成及仍需的新鲜复核;不能把保存了正文或候选当作阶段完成。
248
+ 需要补审时,在**新预览**中传入 `resumeFromJobId`,例如 `{"resumeFromJobId":"原执行任务的真实jobId"}`;仍须核对新范围与当前明确续审授权并通过当前宿主权限;不能复用旧批准,明确要求匹配的补审时无需再重复询问。只复用同一会话、工作目录、模型路由/思考设置下,完整快照、配置和运行时身份均匹配的已完成且符合复用条件的文件链;项目模式还绑定对应批次。来源或配置变化不做猜测复用。旧发现标记 `priorRun`,不伪造本轮读取凭据、模型调用或覆盖。变更模式开启阶段续审时,schema2 检查点还可保存已接受但所在文件链未完成的 primary 生成;它只省去匹配的主审生成,综合、交互和复核在新安全边界执行,复核重新取得独立回执。预览分别显示可复用文件、生成及仍需的新鲜复核;不能把保存了正文或候选当作阶段完成。
245
249
 
246
250
  后台审查会在上述宿主任务目录的 `artifacts` 子目录留存有界模型工件,包括失败/取消时已捕获的文本或工具参数;不含 reasoning。它们可能含源码,应按源码保护,不会自动加入提示词、当作已确认发现或在普通状态报告公开。默认单工件 1 MiB、每任务原文总量 8 MiB、最多 4096 条记录;截断由工件标记,存储超限明确返回遗漏标记和无源码诊断。读取绑定原会话、目录、提供方、模型和思考设置。需要查看时,Agent 必须先提示“可能把源码/模型原文带入当前对话”,取得你的明确同意,再调用 `code_review_artifacts({jobId,confirmed:true})`;可用 `offset`/`limit` 分页,默认一次 1 条、最多 100 条。返回内容标为 `sourceBearing:true`、`untrusted:true`,不是批准证据;普通 `status` 不返回工件原文。目录/文件使用 0700/0600 权限模式及原子写入;Windows 模式位不等于额外 ACL 保证。
247
251
 
@@ -277,8 +281,8 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
277
281
  普通使用者只需上面的对话请求,不必手工构造 ID。需要精确控制时,让 Agent 按下列顺序调用:
278
282
 
279
283
  1. 调用 `code_review_preview`,参数可以是 `{}` 或上面的 JSON 示例。
280
- 2. 展示返回的仓库、模型、文件与限制,保留返回的 `previewId`。**此时还没有完成审查。**
281
- 3. 用户确认后,调用 `code_review_execute`:
284
+ 2. 内部核对返回的仓库、模型路由、发送目的地、文件、预算与权限,保留返回的 `previewId`;只预览或需确认时向用户展示。**此时还没有完成审查。**
285
+ 3. 当前人类已明确要求审核且预览与授权一致时,直接调用 `code_review_execute`,不重复询问;只预览、等待确认或超授权时不得直接执行:
282
286
 
283
287
  ```json
284
288
  {"previewId":"替换为本次预览真实返回的ID","confirmed":true}
@@ -312,7 +316,7 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
312
316
  | Agent 没有调用代码审查工具,只给了普通文字分析 | 明确要求调用 `code_review_preview`;若工具不可用,检查插件是否启用并已加载到当前会话 |
313
317
  | 找不到仓库 / 选错仓库 | 检查会话工作目录;多仓库时用 `repositoryPath` 指定子仓库根目录 |
314
318
  | 新文件没有出现 | 用 `selectedPaths` 显式选择未跟踪文件;路径相对于仓库 |
315
- | 预览过期、文件已变化、模型已变化 | 重新预览,核对新范围后再确认,不复用旧 ID |
319
+ | 预览过期、文件已变化、模型已变化 | 新预览并重新核对当前授权,不复用旧 ID 或旧批准;超出授权时询问 |
316
320
  | SVN 历史提示需要网络许可 | 确认可以访问仓库后,预览参数增加 `svnAllowNetwork:true` |
317
321
  | SVN 登录、权限或版本不存在 | 检查 SVN 本身的访问权限和版本号;不要把密码写进对话或仓库 URL |
318
322
  | 文件被阻断或报告 partial | 阅读具体原因;缩小变更批次、补充允许的上下文,或处理真实冲突后重新预览。不能把排除文件视为通过 |
package/RELEASE-NOTES.md CHANGED
@@ -1,23 +1,17 @@
1
- # v0.87.2 发布说明
1
+ # v0.87.4 发布说明
2
2
 
3
- 使用方式见 [README](README.md),历史变化见 [变更记录](CHANGELOG.md),发布门禁见 [开发说明](DEVELOPMENT.md)。
3
+ ## 默认报告先给人能读懂的结论
4
4
 
5
- ## 差异交付与预算恢复
5
+ 默认输出不再把执行台账平铺为报告。仍只交付一份统一 Markdown:先说明是否查完、形成了什么问题记录,再按复核支持本次新增/当前源码问题、待核实、原有问题、已反驳分区,先讲触发条件、影响和建议。没有问题记录不等于没有缺陷,主审完成不等于可以放行。
6
6
 
7
- 大文件稀疏、远距离编辑增加有界最短编辑回退,避免差异表超限后不必要地扩大为近全文包络;耗尽预算仍明确受限。文件/窗口主审保留同次检索链,等待其他阶段释放局部额度后有限续跑,不重置总账、检索轮数、纠正许可或超时。
7
+ 未完成范围按原因和受影响条目聚合,区分单阶段检索轮数、读取分配或共享额度、输入预算和前置依赖。完整证据、逐项覆盖、计划、执行诊断和保留归属的限制放在同一报告的折叠技术附录;重复限制只集中显示一次原文,保留次数、差异字段及可重建的原记录。
8
8
 
9
- 正常组主审仍合并调用;仅真实局部分配失败且尚未接受组结果时明确回退成员主审,保留已消费预算、不回放已接受生成。组风险规划明确要求各成员提供 risks(允许空数组);缺失或畸形仍报告限制,合法发现保留,不隐藏补发规划。
9
+ ## 保留真实性与证据边界
10
10
 
11
- ## 迁移对照与安全边界
11
+ 此修复仅改变本地呈现,不额外调用模型、不重新审核业务源码、不改变读取和执行预算、不放宽证据或新增归因校验。原始报告 JSON 保持完整。分页不足、取消、失败、过期、复用、窗口/复核/跨文件未完成仍明确披露;项目报告保留结论冲突的候选,SVN 历史不再误标为 Git。
12
12
 
13
- 迁移导航保留旧公开入口、旧委托实现及新入口。删除导出、单例调用和不同名新入口仅提供有限词法候选导航,强制 uncertain,不证明类型、宏语义、共享状态身份或回归因果。不支持或歧义的旧入口明确报告限制。
13
+ ## 验证
14
14
 
15
- 全部宿主迁移合同从批准快照独立重建并匹配完整身份,防止删除种类、基线或来源字段降级绕过限制;拒绝不可信访问器、Proxy 和自定义序列化钩子。独立复核仍使用本代新证据回执。
15
+ 新增报告端到端、问题卡、限制聚合与安全转义测试,并使用保留归档中74条目、290限制、34跨文件未完成这一计数模式的合成呈现夹具回归。公开夹具不含原始路径、源码或限制正文;另在本地验证真实归档呈现,不是新的源码审核结论。发布以全量测试及四矩阵标签 CI 为门禁。
16
16
 
17
- 预览返回有效截止时间并区分失效原因;后台分别显示阶段限制与全局余量,调用下限不是成本或耗时上限。批准等待中的取消或模型变化不能发送源码。
18
-
19
- ## 验证与交付
20
-
21
- 修复冻结本地完整回归1309项:1305通过、0失败、4跳过;最终独立复核定向65项通过。真实DSH ToolRuntime加载/工具注册、包内容与差异检查通过。这些不是业务项目真实模型重跑,也不保证召回率或实际耗时。
22
-
23
- 正式发布须通过冻结标签 Ubuntu/Windows × Node.js22/24 四矩阵,并核验 Actions、npm 与 GitHub Release 的同一原始安装包及校验清单。发布不自动安装、升级或重启当前宿主。
17
+ 安装与使用见 [README](README.md),历史变化见 [变更记录](CHANGELOG.md),发布门禁见 [开发说明](DEVELOPMENT.md)。发布不自动安装或重启当前宿主。
package/index.mjs CHANGED
@@ -6,10 +6,10 @@ import { reviewToolOutput } from './src/tool-view.mjs';
6
6
  export const name = 'dsh-code-review';
7
7
  export const inject = ['tools', 'llm'];
8
8
 
9
- const PREVIEW_DESCRIPTION = 'Preview Git/SVN working-copy or Git commit/range review scope and model destination without sending source to a model. repositoryPath selects an explicit child repository relative to session workspace; file paths are repository-relative. Each session reviews one selected repository independently; reviewMode=project selects tracked current source instead of changes; changeScope=staged or unstaged narrows a Git working-copy review to the index or to unstaged edits. Show the preview before execution.';
9
+ const PREVIEW_DESCRIPTION = 'Preview Git/SVN working-copy or Git commit/range review scope and model destination without sending source to a model. repositoryPath selects an explicit child repository relative to session workspace; file paths are repository-relative. Each session reviews one selected repository independently; reviewMode=project selects tracked current source instead of changes; changeScope=staged or unstaged narrows a Git working-copy review to the index or to unstaged edits. Always capture and inspect the preview internally before execution. An explicit user request to review the selected code already authorizes execution within that request: proceed without asking the user to confirm the same review again. Ask only if the preview exceeds the requested scope or changes the authorized model/destination, or authorization is unclear. Never execute if the user requested preview only or explicitly asked to wait for confirmation. Preserve host tool permissions and all snapshot/identity checks.';
10
10
  const PREVIEW_PARAMETERS = { type: 'object', additionalProperties: false, properties: { resumeFromJobId: { type: 'string', description: 'Explicit terminal job to reuse identity-matching completed chains. Requires a fresh captured preview, confirmation and authorization; does not restore old approval.' }, reviewMode: {type:'string',enum:['changes','project'],description:'project reviews all tracked current files, including unchanged files, in bounded batches with shared session budget. Does not include ignored/untracked/external contents. Each DSH session owns independent preview, lock and model budget.'}, changeScope: {type:'string',enum:['worktree','staged','unstaged'],description:'Git working-copy comparison scope. worktree (default) compares HEAD to the working tree (net change); staged compares HEAD to the index so only staged content is reviewed; unstaged compares the index to the working tree so only unstaged edits are reviewed and staged content is the old side. Requires a Git working copy; mutually exclusive with Git history, SVN parameters and project mode.'}, svnReplacementBaseRevision: {type:'string',description:'Working-copy file replacement only: explicitly compare to this fixed repository revision at the target URL. Not local BASE and not proof of the removed-node revision. Requires separate remote opt-in when applicable; identity and properties frozen in preview.'}, svnReplacementAllowNetwork: {type:'boolean',description:'Explicit permission to read the selected working-copy target URL at svnReplacementBaseRevision. Does not permit arbitrary URLs, updates, merges or writes.'}, svnBasePath: {type:'string',description:'Explicit old endpoint path relative to the current SVN repository root; requires svnTargetPath and fixed revision range. Not an arbitrary URL. Same repository UUID required.'}, svnTargetPath: {type:'string',description:'Explicit new endpoint repository-relative path paired with svnBasePath. Each side content/context stays at its URL/revision; shared capture budgets and remote opt-in apply.'}, svnEffectiveMergeinfo: { type: 'boolean', description: 'Explicit SVN opt-in to bounded BASE/working or fixed-history ancestor mergeinfo property reads on each selected side. Offline for working copies; history uses existing remote permission and budgets. Mixed BASE revisions, unsafe ancestors or incomplete URL boundaries remain unknown. No branch source reads or merge attribution.' }, svnRevision: { type: 'string', description: 'SVN fixed numeric revision reviewed against revision minus one. Requires SVN working-copy root; no HEAD/date aliases.' }, svnBaseRevision: { type: 'string', description: 'SVN fixed numeric old endpoint; requires svnTargetRevision, mutually exclusive with svnRevision and Git revision parameters.' }, svnTargetRevision: { type: 'string', description: 'SVN fixed numeric new endpoint, greater than old endpoint. Fixed repository objects, not working-copy text. Supports explicitly selected target context with verified baseline provenance and target rules; explicit old-only context is also supported with fixed SVN provenance; autoContext follows at most three independent fixed-side dependency layers with bounded parent listings and shared capture budgets; callerScopePaths supports bounded fixed-side SVN historical literal discovery; only matched sides captured, never complete caller coverage.' }, svnAllowNetwork: { type: 'boolean', description: 'Explicit opt-in to read the working-copy repository URL at fixed SVN revisions during preview/execute; preview sends no source to a model. Required for non-file repository URLs; default denies remote access. Does not allow checkout/update/merge or arbitrary URLs.' }, reviewRounds: { type: 'integer', minimum: 1, maximum: 3, description: 'Explicit independent review passes, default 1. Preview discloses extra sends; later passes omit the first risk plan. Shares existing timeout, call and retrieval budgets, not provider reasoning effort. Bound to this preview; execute cannot override.' }, oldContextPaths: { type: 'array', maxItems: 20, items: { type: 'string' }, description: 'Explicit Git/SVN historical baseline-only context paths. Git uses fixed blobs; SVN uses UUID/URL/peg/operative revision/hash. Shares 20 context slots. No target text is inferred; selected old changes reuse original identity, excluded changes cannot reenter. Unsupported on working reviews.' }, callerScopePaths: { type: 'array', minItems: 1, maxItems: 4, items: { type: 'string' }, description: 'Explicit non-root repository-relative directories for bounded tracked literal reverse import/include discovery. Git/SVN working review or fixed Git/SVN historical review. SVN uses offline normal nodes, rejects unsafe ancestor states and special/keyword properties. Historical sides share scan budgets and use object blobs only. Preview reads candidate bodies, including nonmatches; only captured matching context is sent. Navigation only, not complete caller coverage.' }, businessRequirement: { type: 'string', maxLength: 16384, description: 'Optional business constraint text up to 16KiB UTF8; untrusted data, not evidence or permission. Bound to preview and sent in review phases.' }, commit: { type: 'string', description: 'Git commit versus sole parent; root empty, merge requires range.' }, baseRevision: { type: 'string', description: 'Git old endpoint; requires targetRevision, mutually exclusive with commit.' }, targetRevision: { type: 'string', description: 'Git new endpoint, exact tree endpoints not merge-base. Explicit context uses target tree with available baseline provenance; oldContextPaths is baseline-only. Historical caller discovery scans fixed sides; explicit rules use target-tree blobs.' }, repositoryPath: { type: 'string', description: 'Workspace-relative explicit child Git/SVN repository root, forward slashes; omit for current repository.' }, selectedPaths: { type: 'array', items: { type: 'string' }, maxItems: 200 }, autoContext: { type: 'boolean', description: 'Opt in to tracked literal import/include context shown in preview. Working Git follows changed and explicitly selected context sources at most 3 dependency layers under shared 512 exact probes, 20 candidates/context slots, 1MiB source and 30s verification budget. Git history follows at most 3 baseline/target dependency layers with shared limits, object reads only; SVN follows at most 3 safe offline current-side layers with exact path probes and node/ancestor identity verification, sharing limits; no SVN historical review. Not semantic binding or complete definitions.' }, contextPaths: { type: 'array', items: { type: 'string' }, maxItems: 20 }, rulePaths: { type: 'array', items: { type: 'string' }, maxItems: 4 } } };
11
- const EXECUTE_DESCRIPTION = 'Execute a previewed read-only code review using the current DSH model. Sends the approved snapshot to the current Agent model under DSH tool execution policy; no separate plugin approval prompt. Never modifies the reviewed repository.';
12
- const EXECUTE_PARAMETERS = { type: 'object', additionalProperties: false, properties: { previewId: { type: 'string' }, confirmed: { type: 'boolean' } }, required: ['previewId', 'confirmed'] };
11
+ const EXECUTE_DESCRIPTION = 'Execute a previewed read-only code review using the current DSH model. Sends the approved snapshot to the current Agent model under DSH tool execution policy; no separate plugin approval prompt. confirmed=true attests that the current user explicitly requested this review and the inspected preview matches that authorization; it does not require a second confirmation message. Never execute a preview-only or explicit wait-for-confirmation request, or exceed authorized scope/model/destination. Never modifies the reviewed repository.';
12
+ const EXECUTE_PARAMETERS = { type: 'object', additionalProperties: false, properties: { previewId: { type: 'string' }, confirmed: { type: 'boolean', description: 'True only when the current user explicitly authorized this review and the inspected preview matches the requested scope/model/destination. The original explicit review request is sufficient; do not ask again for the same review. Preview-only or explicit wait-for-confirmation requests do not authorize execution.' } }, required: ['previewId', 'confirmed'] };
13
13
 
14
14
  export function apply(ctx, config = {}) {
15
15
  // This entry runs only when the host invokes the registered tool.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.87.2",
3
+ "version": "0.87.4",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -3,8 +3,8 @@ import os from 'node:os';
3
3
  import { createReviewJobs } from './review-jobs.mjs';
4
4
  import { reviewToolOutput } from './tool-view.mjs';
5
5
 
6
- const START_DESCRIPTION = 'Start a durable background preview or confirmed review. Returns a job ID immediately, NOT task completion. Keep the user request active using code_review_wait until terminal status; never final-answer only that a task started. Preview phase never sends source. For execute supply the approved previewId and confirmed=true. Use code_review_status to collect progress/results; code_review_cancel to stop. Does not automatically resume after host restart or grant new source approval.';
7
- const START_PARAMETERS = { type: 'object', additionalProperties: false, properties: { phase: { type: 'string', enum: ['preview', 'execute'] }, previewArgs: { type: 'object', description: 'Same parameters as code_review_preview; do not include execute fields.' }, previewId: { type: 'string' }, confirmed: { type: 'boolean' } }, required: ['phase'] };
6
+ const START_DESCRIPTION = 'Start a durable background preview or confirmed review. Returns a job ID immediately, NOT task completion. Keep the user request active using code_review_wait until terminal status; never final-answer only that a task started. Preview phase never sends source. Always inspect the captured preview internally. An explicit current user request to review the selected code authorizes execution when the preview matches its scope/model/destination: proceed with phase=execute, previewId and confirmed=true without asking for duplicate confirmation. If the user requested preview only or explicitly asked to wait for confirmation, do not execute. Ask for scope expansion, a changed authorized model/destination, or unclear authorization. Host permissions and snapshot/identity checks still apply. Use code_review_status to collect progress/results; code_review_cancel to stop. Does not automatically resume after host restart or grant new source approval.';
7
+ const START_PARAMETERS = { type: 'object', additionalProperties: false, properties: { phase: { type: 'string', enum: ['preview', 'execute'] }, previewArgs: { type: 'object', description: 'Same parameters as code_review_preview; do not include execute fields.' }, previewId: { type: 'string' }, confirmed: { type: 'boolean', description: 'Attest current explicit review authorization after inspecting the preview. A matching explicit review request is sufficient; do not ask again. Preview-only or explicit wait-for-confirmation is not execution authorization.' } }, required: ['phase'] };
8
8
 
9
9
  const STATUS_DESCRIPTION = 'Read a background review task owned by this session, directory and model. Returns saved progress or final preview/report. completed task means the tool returned; inspect nested report coverage/status. Interrupted tasks retain checkpoints but require new preview and approval to resend.';
10
10
  const STATUS_PARAMETERS = { type: 'object', additionalProperties: false, properties: { jobId: { type: 'string' }, offset: { type: 'integer', minimum: 0 }, checkpointIndex: { type: 'integer', minimum: 0, description: 'Read one checkpoint nested files/findings page; offset/limit apply within it.' }, limit: { type: 'integer', minimum: 1, maximum: 100 } }, required: ['jobId'] };
@@ -12,7 +12,7 @@ const STATUS_PARAMETERS = { type: 'object', additionalProperties: false, propert
12
12
  const CANCEL_DESCRIPTION = 'Cancel a background review task owned by this session. Does not discard saved checkpoints. No source is sent by cancellation.';
13
13
  const CANCEL_PARAMETERS = { type: 'object', additionalProperties: false, properties: { jobId: { type: 'string' } }, required: ['jobId'] };
14
14
 
15
- const WAIT_DESCRIPTION = 'Wait for review progress or terminal result without ending the user request. Prefer waitFor=terminal to ignore intermediate checkpoints; progress preserves checkpoint-by-checkpoint compatibility. Reuse the exact returned waitRevision as after; never guess increments or call status after every event. Timeout defaults to 60000ms and is bounded to 60000ms; changed=false means timeout, not completion or cancellation. Call again while queued/running. Preview completion requires confirmation; execute completion returns one final consolidated report.';
15
+ const WAIT_DESCRIPTION = 'Wait for review progress or terminal result without ending the user request. Prefer waitFor=terminal to ignore intermediate checkpoints; progress preserves checkpoint-by-checkpoint compatibility. Reuse the exact returned waitRevision as after; never guess increments or call status after every event. Timeout defaults to 60000ms and is bounded to 60000ms; changed=false means timeout, not completion or cancellation. Call again while queued/running. Preview completion is not review completion. If the current user explicitly requested the matching review, proceed to execute with confirmed=true without asking again; preview-only, explicit wait-for-confirmation or out-of-authorization scope/model/destination requires user authorization; execute completion returns one final consolidated report.';
16
16
  const WAIT_PARAMETERS = { type: 'object', additionalProperties: false, properties: { jobId: { type: 'string' }, after: { type: 'integer', minimum: 0, description: 'Exact waitRevision returned by start/status/wait; do not increment it yourself.' }, waitFor: { type: 'string', enum: ['progress', 'terminal'], default: 'progress', description: 'Use terminal to wait through intermediate saves; progress wakes on each durable checkpoint.' }, timeoutMs: { type: 'integer', minimum: 1, maximum: 60000, default: 60000 } }, required: ['jobId'] };
17
17
 
18
18
  const TERMINAL_STATUSES = ['completed', 'failed', 'cancelled', 'interrupted'];
@@ -45,10 +45,10 @@ function terminalSummary(value) {
45
45
  if (value.resultPage) lines.push('当前页:文件 ' + (value.resultPage.files?.length ?? 0) + '/' + count(value.resultPage.totalFiles) + ',发现 ' + (value.resultPage.findings?.length ?? 0) + '/' + count(value.resultPage.totalFindings) + ';code_review_status 的 offset/limit 仅切换结果页。');
46
46
  lines.push('完整报告:调用一次 code_review_wait(jobId 同上,waitFor=terminal)立即取得最终报告;无需继续轮询。');
47
47
  } else if (report?.previewId) {
48
- lines.push('预览已完成,尚未执行审查;请展示批准范围并取得用户确认后再执行。');
48
+ lines.push('预览已完成,尚未执行审查;请先内部核对范围、模型与发送目的地。用户已明确要求审核且预览符合该授权时,直接以 confirmed=true 继续执行,不要重复询问确认。仅要求预览时不得执行;明确要求等待确认时也不得执行。超出原授权范围、改变授权模型/发送目的地或授权边界不明确时才询问。宿主权限及快照/身份校验仍须通过。');
49
49
  lines.push('预览文件总量:' + count(report.filePage?.total ?? report.files?.length) + ';计划最少调用:' + count(report.plan?.minimumCalls));
50
50
  lines.push('最少调用仅为初始计划下限,不是总调用数或耗时上限;后续取证、复核及综合会增加工作量。');
51
- if (typeof report.expiresAt === 'string' && Number.isFinite(Date.parse(report.expiresAt))) lines.push('预览有效至:' + new Date(report.expiresAt).toISOString() + ';过期后须重新预览并确认。');
51
+ if (typeof report.expiresAt === 'string' && Number.isFinite(Date.parse(report.expiresAt))) lines.push('预览有效至:' + new Date(report.expiresAt).toISOString() + ';过期后须重新预览并核对当前授权,不得复用旧预览或恢复旧批准。');
52
52
  } else {
53
53
  lines.push('未提供完整报告摘要;终态不会自动恢复或重新发送源码。请检查已保存检查点和未完成范围。');
54
54
  }
@@ -1,6 +1,6 @@
1
1
  // Preview validity is diagnostic only: it never restores approval or bypasses recapture.
2
2
  export function previewValidity(preview, current, now) {
3
- const fail = (code, message) => { const error = new Error(message + '; preview again and obtain fresh confirmation'); error.code = code; throw error; };
3
+ const fail = (code, message) => { const error = new Error(message + '; preview again and verify current explicit user authorization; do not reuse old approval. A matching explicit review request needs no duplicate confirmation; preview-only or explicit wait-for-confirmation does not authorize execution, and scope/model/destination changes require authorization'); error.code = code; throw error; };
4
4
  if (!preview) fail('PREVIEW_MISSING', 'Preview missing or already consumed');
5
5
  if (preview.expires <= now) fail('PREVIEW_EXPIRED', 'Preview expired');
6
6
  if (preview.agent !== current.agent) fail('PREVIEW_OWNER_CHANGED', 'Preview owner changed');
@@ -1,23 +1,12 @@
1
1
  import { markdownReport } from './review.mjs';
2
2
 
3
- function dedupeFindings(findings) {
4
- const seen = new Set();
5
- const result = [];
6
- for (const f of findings ?? []) {
7
- const key = JSON.stringify([f.path, f.anchor, f.title, f.evidence]);
8
- if (!seen.has(key)) { seen.add(key); result.push(f); }
9
- }
10
- return result;
11
- }
12
-
13
3
  export function renderProjectReport(report) {
14
4
  const merged = {
15
5
  ...report,
16
- findings: dedupeFindings(report.findings),
6
+ reviewMode: 'project',
7
+ findings: report.findings ?? [],
17
8
  interactions: (report.batches ?? []).flatMap(b => b.interactions ?? []),
18
- limitations: (report.batches ?? []).flatMap(b => b.limitations ?? []),
9
+ limitations: [...(report.limitations ?? []), ...(report.batches ?? []).flatMap(b => b.limitations ?? [])],
19
10
  };
20
- const scope = '总体范围:' + report.coverage.completed + '/' + report.coverage.total + ' 个文件完成主审;排除 ' + report.coverage.excluded + ',阻断 ' + report.coverage.blocked + ',未完成 ' + report.coverage.pending + '。综合未完成项单独列出;主审完成不等于审核通过。';
21
- const outdated = report.outdated ? '\n注意:来源已变化或无法复核,仅对应原批准快照。' : '';
22
- return markdownReport(merged).replace('# Code Review 报告', '# 全项目代码审查报告') + '\n\n' + scope + outdated;
11
+ return markdownReport(merged).replace('# Code Review 报告', '# 全项目代码审查报告');
23
12
  }
@@ -13,7 +13,7 @@ import { reviewProjectBatches, projectConcurrency as validateProjectConcurrency,
13
13
  import { createDshModel } from './dsh-model.mjs';
14
14
  import { retrievalRounds } from './review-execution-policy.mjs';
15
15
 
16
- const PREVIEW_NOTICE_SUFFIX = ' Full current source audit, not regression-only diff. Shared session execution budget: no internal elapsed-time or model-call cutoff, 16MiB capture memory protection. Blocked/excluded/pending files are not reviewed successfully. No untracked/ignored/external bodies; no complete semantic call graph. Independent approved batches run with bounded concurrency; per-batch evidence and global model-call budgets remain enforced. Show scope and model before confirmed execute.';
16
+ const PREVIEW_NOTICE_SUFFIX = ' Full current source audit, not regression-only diff. Shared session execution budget: no internal elapsed-time or model-call cutoff, 16MiB capture memory protection. Blocked/excluded/pending files are not reviewed successfully. No untracked/ignored/external bodies; no complete semantic call graph. Independent approved batches run with bounded concurrency; per-batch evidence and global model-call budgets remain enforced. Inspect scope and model against current explicit user authorization before execute. A matching explicit review request needs no duplicate confirmation; do not execute preview-only or explicit wait-for-confirmation requests, and ask if scope/model/destination exceeds authorization or is unclear. Host permissions and snapshot/identity checks remain required.';
17
17
  const REPORT_NOTICE_SUFFIX = ' Coverage counts processing of selected tracked files only, not proof of correctness. Excluded and blocked are never completed. No cross-batch whole-program semantic coverage is claimed.';
18
18
 
19
19
  function owner(exec) {
@@ -1,5 +1,5 @@
1
1
  // Saved evidence is a ledger, not renewed source authority or automatic resume.
2
- const NOTICE = 'Saved approved manifest (when present) plus checkpoints; before that, only observed scope, not a complete denominator. Completed windows are not a completed file. New source sends require fresh preview and confirmation; no approval is restored from this ledger.';
2
+ const NOTICE = 'Saved approved manifest (when present) plus checkpoints; before that, only observed scope, not a complete denominator. Completed windows are not a completed file. New source sends require fresh preview and current explicit user authorization; no approval is restored from this ledger. A matching explicit resume request needs no duplicate confirmation; preview-only or explicit wait-for-confirmation does not authorize execution, and scope/model/destination changes require authorization.';
3
3
 
4
4
  export function recoveryCoverage(record) {
5
5
  const states = new Map(), windows = new Map(), stages = new Map();
@@ -0,0 +1,57 @@
1
+ import { groupDuplicateFindings } from './finding-groups.mjs';
2
+ import { comparisonValidated } from './report-summary.mjs';
3
+
4
+ const safe = value => Array.from(String(value ?? '')).map(c => {
5
+ if (c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127) return ' ';
6
+ if (c === '&') return '&amp;';
7
+ if (c === '<') return '&lt;';
8
+ if (c === '>') return '&gt;';
9
+ return '*_[]#|!()\\`'.includes(c) ? '\\' + c : c;
10
+ }).join('');
11
+
12
+ export function findingDisposition(finding, report = {}) {
13
+ const v = finding.verification;
14
+ const evidence = Array.isArray(v?.evidence) && v.evidence.length > 0 && v.evidence.every(e => e?.status === 'references-validated');
15
+ if (v?.status !== 'completed' || !evidence) return '待核实';
16
+ if (v.verdict === 'refuted') return '已反驳';
17
+ if (v.verdict !== 'supported') return '待核实';
18
+ if (report.mode === 'project' || report.reviewMode === 'project' || report.projectScope || finding.attribution?.causality === 'current-source-only') return '当前源码问题(复核支持)';
19
+ const r = v.regression;
20
+ if (!comparisonValidated(finding)) return '待核实';
21
+ return r.classification === 'introduced' ? '本次新增(复核支持)' : r.classification === 'preexisting' ? '原有问题(复核支持)' : '待核实';
22
+ }
23
+
24
+ function location(finding) {
25
+ const a = finding.anchor ?? {};
26
+ return safe(finding.path ?? '未标明文件') + (a.kind === 'line' ? ' · ' + (a.side === 'old' ? '修改前' : a.side === 'new' ? '修改后' : safe(a.side)) + '第 ' + safe(a.start) + '–' + safe(a.end) + ' 行' : a.kind === 'property' ? ' · 属性 ' + safe(a.name) : ' · 文件级');
27
+ }
28
+
29
+ export function findingsMarkdown(report) {
30
+ const findings = Array.isArray(report.findings) ? report.findings : [];
31
+ const duplicate = new Map();
32
+ for (const group of groupDuplicateFindings(findings)) for (const i of group.findingIndices.slice(1)) duplicate.set(i, group.findingIndices[0]);
33
+ const sections = new Map([['本次新增(复核支持)',[]],['当前源码问题(复核支持)',[]],['待核实',[]],['原有问题(复核支持)',[]],['已反驳',[]]]);
34
+ findings.forEach((f,i)=>{ if (!duplicate.has(i)) sections.get(findingDisposition(f,report)).push({f,i}); });
35
+ const output = ['', '## 审查发现'];
36
+ if (!findings.length) return [...output, '本轮没有形成问题记录;这不等于已经排除未完成范围内的问题。'];
37
+ output.push('以下区分复核支持、待核实和已反驳的记录;证据引用通过校验不等于运行复现或因果证明。');
38
+ for (const [label, items] of sections) {
39
+ if (!items.length) continue;
40
+ output.push('', '### ' + label);
41
+ for (const {f,i} of items) {
42
+ const severity = ({critical:'严重',high:'高',medium:'中',low:'低'})[f.severity] ?? safe(f.severity ?? '未分级');
43
+ output.push('', '#### ' + (i+1) + '. ' + safe(f.title) + '(严重程度:' + severity + ')', '- 位置:' + location(f));
44
+ for (const [key,title] of [['trigger','什么情况下发生'],['impact','可能造成的影响'],['evidence','已看到的依据']]) if (f[key]) output.push('- ' + title + ':' + safe(f[key]));
45
+ if (label === '待核实') output.push('- 尚未确定:' + safe(f.verification?.reason ?? '候选尚未完成有效的独立复核或修改前后对照,不能作为已确认缺陷。'));
46
+ if (label === '已反驳') output.push('- 反证结论:' + safe(f.verification?.reason ?? '复核反驳该候选;不计为待修缺陷。'));
47
+ if (f.attribution?.beforeBehavior) output.push('- 修改前:' + safe(f.attribution.beforeBehavior));
48
+ if (f.attribution?.afterBehavior) output.push('- 修改后:' + safe(f.attribution.afterBehavior));
49
+ if (f.suggestion) output.push('- 建议:' + safe(f.suggestion));
50
+ const related = [...duplicate].filter(([,first])=>first===i).map(([index])=>location(findings[index]));
51
+ if (related.length) output.push('- 相同证据与解释的其他位置:' + related.join(';') + '。正文合并展示,原始候选仍保留。');
52
+ if (f.reused || f.priorRun) output.push('- 来源:包含既往审核结果,不表示本轮重新取证。');
53
+ output.push('- 完整证据与判定过程:见技术附录中的候选 ' + (i+1) + '。');
54
+ }
55
+ }
56
+ return output;
57
+ }
@@ -0,0 +1,147 @@
1
+ // Pure presentation: original report records remain authoritative and unchanged.
2
+ const rows = value => Array.isArray(value) ? value : [];
3
+ const escape = value => String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/[\x00-\x1f\x7f]/g, ' ').replace(/[\\\x60*_{}\[\]()#+.!|~=-]/g, '\\$&');
4
+ const brief = value => { const text = String(value ?? ''); return escape(text.length > 180 ? text.slice(0, 180) + '…(完整值见附录)' : text); };
5
+ const html = value => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
6
+ const stages = { primary: '主审', verification: '独立复核', synthesis: '综合', 'window-synthesis': '跨窗口综合', interaction: '跨文件综合' };
7
+ const stage = value => Object.hasOwn(stages, value) ? stages[value] : String(value ?? '未标明阶段');
8
+
9
+ function reason(record = {}) {
10
+ const value = typeof record === 'string' ? record : record?.failureCode ?? record?.blockCode ?? record?.code ?? record?.reason;
11
+ const text = String(value ?? '');
12
+ // Dependency state must not be relabelled as an input-budget failure.
13
+ if (/dependenc|primary.*incomplete|member.*incomplete|prerequisite|依赖.*未完成|主审.*未完成/i.test(text)) return '依赖的主审或前置阶段未完成';
14
+ if (/RETRIEVAL_ROUND_BUDGET_EXCEEDED|检索轮数|retrieval.*round.*(budget|limit)/i.test(text)) return '单阶段检索轮数达到上限(读取额度可能仍有余量)';
15
+ if (/READER_CALL_BUDGET_EXCEEDED|reader.*call.*(budget|limit)|读取调用.*(不足|耗尽|上限)/i.test(text)) return '阶段分配或共享读取调用次数不足';
16
+ if (/READER_OUTPUT_BUDGET_EXCEEDED|reader.*output.*(budget|limit)|读取.*字节|输出字节/i.test(text)) return '阶段分配或共享读取输出字节预算不足';
17
+ if (/input[-_ ]budget|input.*(budget|limit).*exceed|输入.*(预算|上限|超限)/i.test(text)) return '阶段输入预算不足';
18
+ if (/PROJECT_MODEL_CALL_LIMIT|model call budget|模型调用.*(额度不足|上限)/i.test(text)) return '模型调用额度不足';
19
+ if (/unsupported|not[-_ ]supported|不支持/i.test(text)) return '当前不支持此来源或操作';
20
+ if (text === 'exact-edit-analysis-unavailable') return '精确编辑分析不可用';
21
+ if (/CANCELLED|cancelled|canceled/i.test(text)) return '执行已取消';
22
+ if (/MODEL_TIMEOUT|timed? ?out/i.test(text)) return '阶段执行超时';
23
+ return text ? '未知/其他原因(完整原文见附录)' : '原因未记录(未知;不推测为预算不足)';
24
+ }
25
+
26
+ function fileReasons(file, findings) {
27
+ const result = [];
28
+ const limits = [...rows(file?.retrievalLimits), ...rows(file?.groupRetrievalLimits)];
29
+ const failures = rows(file?.stageFailures);
30
+ const hasReason = record => record != null && ['failureCode', 'blockCode', 'code', 'reason'].some(key => record[key] != null && record[key] !== '');
31
+ if (['pending', 'blocked', 'failed', 'cancelled', 'running'].includes(file?.status)) {
32
+ // A missing top-level code does not erase concrete phase/group evidence.
33
+ const evidence = hasReason(file) ? [file] : [...failures, ...limits].filter(hasReason);
34
+ result.push(...(evidence.length ? evidence : [file]).map(reason));
35
+ }
36
+ for (const failure of failures) result.push(stage(failure?.phase ?? failure?.stage) + '未完成;' + reason(failure));
37
+ if (file?.retrievalLimited || limits.length) {
38
+ result.push(...(limits.length ? limits.map(reason) : [reason(file)]));
39
+ }
40
+ if (rows(file?.windowCoverage?.pending).length) result.push('变更窗口尚未完成');
41
+ if (file?.continuationPending) result.push('阶段续审尚未完成');
42
+ for (const [key, label] of [['synthesisStatus', '跨窗口综合'], ['verificationStatus', '独立复核']]) if (file?.[key] && file[key] !== 'completed') result.push(label + '未完成');
43
+ if (rows(file?.reviewPasses).some(pass => pass?.status !== 'completed')) result.push('独立审查轮次尚未完成');
44
+ if (file?.fileId != null && findings.some(finding => finding?.fileId === file.fileId && finding?.verification?.status !== 'completed')) result.push('候选的独立复核尚未完成');
45
+ return [...new Set(result)];
46
+ }
47
+
48
+ function collect(report) {
49
+ const files = rows(report?.files), findings = rows(report?.findings);
50
+ const names = new Map(files.filter(file => file?.fileId != null).map(file => [file.fileId, file.path]));
51
+ const identity = id => names.has(id) && names.get(id) != null ? String(names.get(id)) : '未知文件身份:' + (typeof id === 'object' ? JSON.stringify(id) : String(id));
52
+ const paths = record => {
53
+ if (record == null || typeof record !== 'object') return [];
54
+ return [...new Set([...(record.path != null ? [String(record.path)] : []), ...(record.fileId != null ? [identity(record.fileId)] : []), ...rows(record.fileIds).map(identity)])];
55
+ };
56
+ const unfinished = files.map(record => ({ record, reasons: fileReasons(record, findings), paths: record?.path != null ? [String(record.path)] : paths(record) })).filter(item => item.reasons.length);
57
+ const interactions = rows(report?.interactions).filter(record => record?.status !== 'completed').map(record => ({ record, reasons: [reason(record)], paths: paths(record) }));
58
+ return { unfinished, interactions, limitations: rows(report?.limitations), paths };
59
+ }
60
+
61
+ function grouped(items) {
62
+ const groups = new Map();
63
+ for (const item of items) for (const label of item.reasons) {
64
+ if (!groups.has(label)) groups.set(label, []);
65
+ groups.get(label).push(item);
66
+ }
67
+ return [...groups];
68
+ }
69
+
70
+ function summary(output, label, items) {
71
+ if (!items.length) return;
72
+ output.push('- ' + label + ':' + items.length + ' 条记录。');
73
+ const groups = grouped(items);
74
+ for (const [why, members] of groups.slice(0, 6)) {
75
+ const paths = [...new Set(members.flatMap(member => member.paths))];
76
+ output.push(' - ' + brief(why) + ':' + members.length + ' 条;' + (paths.length ? paths.slice(0, 4).map(brief).join('、') + (paths.length > 4 ? ';另有 ' + (paths.length - 4) + ' 个路径/身份' : '') : '未记录文件路径/身份') + '。');
77
+ }
78
+ if (groups.length > 6) output.push(' - 另有 ' + (groups.length - 6) + ' 类原因;完整范围与原始记录见本报告限制附录。');
79
+ }
80
+
81
+ export function readabilityMarkdown(report = {}) {
82
+ const data = collect(report);
83
+ const output = ['', '## 未完成范围与原因', '以下为处理状态与限制记录数,非缺陷数;同一文件可能涉及多个阶段,限制不自动表示待审或缺陷。'];
84
+ summary(output, '未完成条目', data.unfinished);
85
+ summary(output, '跨文件综合未完成', data.interactions);
86
+ if (!data.unfinished.length && !data.interactions.length) output.push(report?.partial || data.limitations.length ? '- 报告仍有部分完成标记或限制;未逐项记录待续审范围,不表示全部查完。' : '- 未记录未完成条目或跨文件综合;这不是正确性证明。');
87
+ if (data.limitations.length) output.push('- 另有 ' + data.limitations.length + ' 条限制记录,按原文聚合保留在本报告限制附录;不将这些限制全部计入未完成范围。');
88
+ if (data.unfinished.length || data.interactions.length) output.push('- 以上仅列有界摘要;全部路径、身份、阶段及原始记录见本报告限制附录。');
89
+ output.push('', '## 下一步');
90
+ if (data.unfinished.length || data.interactions.length) output.push('- 先处理对应阻断/失败原因,再选择未完成条目、窗口、综合或复核阶段继续;已完成主审不能替代后续综合和独立复核。', '- 检索轮数、读取调用数、字节与输入预算是不同限制,应按实际原因调整;依赖未完成时先完成前置阶段,不据此推断输入超限。', '- 本报告不会自动续审或调用模型;再次执行需新预览并核对当前人类授权,范围或发送目的地变化时重新确认。');
91
+ else output.push('- 如需扩大或继续审核,请显式选择范围并核对新预览与当前人类授权;不要仅凭没有未完成记录判定通过。');
92
+ return output;
93
+ }
94
+
95
+ function originalDetails(output, record) {
96
+ output.push('<details><summary>原始记录 JSON(完整保留)</summary>', '', '<pre>' + html(JSON.stringify(record, null, 2) ?? 'null') + '</pre>', '', '</details>');
97
+ }
98
+
99
+ // Deduplicate identical records without losing their original positions or multiplicity.
100
+ function packed(records) {
101
+ const entries = new Map();
102
+ records.forEach((record, index) => {
103
+ const key = JSON.stringify(record);
104
+ if (!entries.has(key)) entries.set(key, { record, positions: [] });
105
+ entries.get(key).positions.push(index);
106
+ });
107
+ return [...entries.values()];
108
+ }
109
+
110
+ export function limitationDetailsMarkdown(report = {}) {
111
+ const data = collect(report);
112
+ const output = ['', '## 限制附录', '以下按原文聚合;计数为记录数,非缺陷数,也不等于待审条目数。相同原文只展示一次,归属与重复次数聚合;折叠 JSON 保留差异字段及原始顺序。完整源报告 JSON 仍为权威记录。'];
113
+ const groups = new Map();
114
+ for (const record of data.limitations) {
115
+ const text = typeof record === 'string' ? record : record?.text;
116
+ // Prefixes distinguish a text-bearing record from an opaque legacy record.
117
+ const key = text == null ? 'record:' + JSON.stringify(record) : 'text:' + JSON.stringify(text);
118
+ if (!groups.has(key)) groups.set(key, { text, records: [] });
119
+ groups.get(key).records.push(record);
120
+ }
121
+ if (!groups.size) output.push('- 未记录限制原文。');
122
+ for (const { text, records } of groups.values()) {
123
+ output.push('', '### 限制(' + records.length + ' 条记录)', text == null ? '未提供限制原文' : '<pre>' + html(JSON.stringify(text)) + '</pre>');
124
+ const scopes = new Map();
125
+ for (const record of records) {
126
+ const paths = data.paths(record);
127
+ const scope = '- 范围:' + (paths.length ? paths.map(escape).join('、') : '未标明文件身份') + (record?.stage != null || record?.phase != null ? ';阶段:' + escape(stage(record.stage ?? record.phase)) : '');
128
+ scopes.set(scope, (scopes.get(scope) ?? 0) + 1);
129
+ }
130
+ for (const [scope, count] of scopes) output.push(scope + ';重复次数:' + count);
131
+ // Shared text is represented by the visible original above, not repeated in every JSON row.
132
+ const compact = records.map(record => typeof record === 'string' ? { type: 'string' } : record && typeof record === 'object' && record.text != null && Object.hasOwn(record, 'text') ? { type: 'object-with-shared-text', fields: Object.fromEntries(Object.entries(record).filter(([key]) => key !== 'text')) } : { type: 'original', value: record });
133
+ output.push('折叠记录中 object-with-shared-text 的 text、string 的原值均为本组上方完整原文;positions 为组内原始索引。');
134
+ originalDetails(output, packed(compact));
135
+ }
136
+ for (const [label, items] of [['未完成条目明细', data.unfinished], ['未完成跨文件综合明细', data.interactions]]) {
137
+ if (!items.length) continue;
138
+ output.push('', '### ' + label + '(' + items.length + ' 条记录)');
139
+ for (const item of items) output.push('- ' + (item.paths.length ? item.paths.map(escape).join('、') : '未标明文件身份') + ':' + item.reasons.map(escape).join(';'));
140
+ // State/reason metadata includes window IDs and pass statuses; full source and candidates already
141
+ // exist in the complete report JSON and must not be duplicated into this appendix.
142
+ const keys = ['fileId', 'path', 'groupId', 'fileIds', 'status', 'failureCode', 'blockCode', 'code', 'reason', 'stage', 'phase', 'stageFailures', 'retrievalLimited', 'retrievalLimits', 'groupRetrievalLimits', 'continuationPending', 'synthesisStatus', 'verificationStatus', 'windowCoverage', 'reviewPasses'];
143
+ output.push('以下仅为状态与原因元数据;源码、候选及其他完整字段见完整源报告 JSON。');
144
+ originalDetails(output, packed(items.map(item => Object.fromEntries(keys.filter(key => Object.hasOwn(item.record ?? {}, key)).map(key => [key, item.record[key]])))));
145
+ }
146
+ return output;
147
+ }
@@ -1,20 +1,12 @@
1
- const FILE_STATES = ['completed', 'excluded', 'blocked', 'failed', 'cancelled', 'pending'];
1
+ const FILE_STATES = ['completed', 'excluded', 'blocked', 'failed', 'cancelled', 'pending', 'running', 'interrupted'];
2
2
  const STATUS_LABELS = { completed: '已完成', partial: '部分完成', failed: '执行失败', cancelled: '已取消', interrupted: '已中断', pending: '待执行', running: '执行中' };
3
- const safe = value => Array.from(String(value ?? '').slice(0, 240)).map(c => {
4
- if (c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127) return ' ';
5
- if (c === '&') return '&amp;';
6
- if (c === '<') return '&lt;';
7
- if (c === '>') return '&gt;';
8
- if ('*_[]#|!()\\'.includes(c) || c.charCodeAt(0) === 96) return '\\' + c;
9
- return c;
10
- }).join('');
11
3
  const rows = value => Array.isArray(value) ? value : [];
12
4
  const validCount = n => Number.isSafeInteger(n) && n >= 0;
13
5
  const supported = finding => finding?.verification?.status === 'completed' && finding.verification.verdict === 'supported' && rows(finding.verification.evidence).length > 0 && finding.verification.evidence.every(ref => ref?.status === 'references-validated');
14
6
 
15
7
  // Presentation consumes host-validated results; it neither re-reads sources nor
16
8
  // upgrades model verdicts, reference validation, or reuse into causal proof.
17
- function comparisonValidated(finding) {
9
+ export function comparisonValidated(finding) {
18
10
  const { evidence, regression } = finding.verification ?? {};
19
11
  if (regression?.status !== 'comparison-references-validated' || !Array.isArray(evidence)) return false;
20
12
  const old = regression.oldEvidence, fresh = regression.newEvidence;
@@ -25,35 +17,89 @@ function comparisonValidated(finding) {
25
17
  return old.every(i => typeof evidence[i].sourceId === 'string' && evidence[i].sourceId) && fresh.every(i => typeof evidence[i].sourceId === 'string' && evidence[i].sourceId && old.every(j => evidence[j].sourceId !== evidence[i].sourceId));
26
18
  }
27
19
 
28
- export function reportSummaryMarkdown(report = {}) {
20
+ /**
21
+ * Pure presentation data for Markdown/Lead renderers. Counts are candidate records,
22
+ * not proven defects. coverage counts main-review entries (directories included in
23
+ * change mode); unfinished preserves every loaded entry with incomplete stages, while
24
+ * crossFile is separate. complete means processing finished, never release approval.
25
+ * Returned objects/arrays are newly allocated and contain no raw model/source text.
26
+ */
27
+ export function reportSummaryData(report = {}) {
29
28
  const findings = rows(report.findings), files = rows(report.files);
29
+ const project = report.mode === 'project' || report.reviewMode === 'project' || Boolean(report.projectScope);
30
30
  const counts = { introduced: 0, unresolved: 0, refuted: 0, preexisting: 0, current: 0 };
31
31
  for (const finding of findings) {
32
32
  const verification = finding?.verification;
33
33
  if (verification?.status === 'completed' && verification.verdict === 'refuted' && rows(verification.evidence).length && verification.evidence.every(ref => ref?.status === 'references-validated')) counts.refuted++;
34
34
  else if (!supported(finding)) counts.unresolved++;
35
- else if (report.reviewMode === 'project' || report.projectScope || finding.attribution?.causality === 'current-source-only') counts.current++;
35
+ else if (project || finding.attribution?.causality === 'current-source-only') counts.current++;
36
36
  else if (comparisonValidated(finding) && verification.regression.classification === 'introduced') counts.introduced++;
37
37
  else if (comparisonValidated(finding) && verification.regression.classification === 'preexisting') counts.preexisting++;
38
38
  else counts.unresolved++;
39
39
  }
40
40
  const coverage = Object.fromEntries(FILE_STATES.map(status => [status, validCount(report.coverage?.[status]) ? report.coverage[status] : files.filter(f => f?.status === status).length]));
41
- const total = validCount(report.coverage?.total) ? report.coverage.total : files.length;
42
- const unfinished = files.filter(file => !['completed', 'excluded'].includes(file?.status) || rows(file.stageFailures).length || file.retrievalLimited || rows(file.windowCoverage?.pending).length || file.synthesisStatus && file.synthesisStatus !== 'completed' || file.verificationStatus && file.verificationStatus !== 'completed' || rows(file.reviewPasses).some(pass => pass?.status !== 'completed') || findings.some(finding => finding?.fileId === file.fileId && finding?.verification?.status !== 'completed'));
43
- const incompleteInteractions = rows(report.interactions).filter(item => item?.status !== 'completed');
44
- const output = ['', '## 结论摘要', '- 整体状态:' + (STATUS_LABELS[report.status] ?? '未知') + ';完成状态只表示处理进度,不表示代码正确。',
45
- '- 已确认新增(独立复核支持、旧新对照引用已校验):' + counts.introduced + ';未决/证据不足:' + counts.unresolved + ';已反驳候选:' + counts.refuted + ';既有问题(复核支持且旧新对照已校验):' + counts.preexisting + '。'];
46
- if (counts.current) output.push('- 当前源码复核支持:' + counts.current + ';当前源码模式不评估历史新增归因,不计为已确认新增。');
47
- output.push('- 以上按候选记录计数,不是去重根因数;“已确认新增”仅指模型复核支持及引用校验成立,因果解释仍未经验证,不是事实或因果证明。');
48
- if (findings.some(finding => finding?.reused || finding?.priorRun)) output.push('- 含既往轮次复用发现;保留原复核来源,不表示本轮重新取证。');
49
- if (report.outdated) output.push('- 注意:来源已变化或无法复核;这些结论仅对应原批准快照,不能当作当前源码结论。');
50
- output.push('- 范围:' + total + ' 个文件;完成 ' + coverage.completed + ',排除 ' + coverage.excluded + ',阻断 ' + coverage.blocked + ',失败 ' + coverage.failed + ',取消 ' + coverage.cancelled + ',待审 ' + coverage.pending + '。排除/阻断不是通过。');
51
- if (unfinished.length) {
52
- output.push('- 关键未完成范围:' + unfinished.slice(0, 5).map(file => safe(file.path ?? '未命名文件')).join('、') + (unfinished.length > 5 ? ';另有 ' + (unfinished.length - 5) + ' 个文件,见覆盖明细。' : ';阶段和限制见覆盖明细。'));
41
+ const accounted = FILE_STATES.reduce((sum, status) => sum + coverage[status], 0);
42
+ const total = validCount(report.coverage?.total) ? report.coverage.total :
43
+ validCount(report.totalFiles) ? report.totalFiles : Math.max(files.length, accounted);
44
+ const detailsComplete = files.length >= total;
45
+ const unfinished = files.flatMap(file => {
46
+ if (file?.status === 'excluded') return [];
47
+ const reasons = [];
48
+ if (file?.status !== 'completed') reasons.push(STATUS_LABELS[file?.status] ?? '主审未完成');
49
+ if (rows(file?.stageFailures).length) reasons.push('阶段失败');
50
+ if (file?.retrievalLimited) reasons.push('取证受限');
51
+ if (file?.continuationPending) reasons.push('后续阶段待执行');
52
+ if (rows(file?.windowCoverage?.pending).length) reasons.push('窗口待审 ' + file.windowCoverage.pending.length);
53
+ if (file?.synthesisStatus && file.synthesisStatus !== 'completed') reasons.push('综合未完成');
54
+ if (file?.verificationStatus && file.verificationStatus !== 'completed' || findings.some(finding => file?.fileId != null && finding?.fileId === file.fileId && finding?.verification?.status !== 'completed')) reasons.push('复核未完成');
55
+ if (rows(file?.reviewPasses).some(pass => pass?.status !== 'completed')) reasons.push('审查轮次未完成');
56
+ return reasons.length ? [{ path: String(file?.path ?? '未命名条目'), reasons }] : [];
57
+ });
58
+ const interactions = rows(report.interactions);
59
+ const crossFile = { total: interactions.length, completed: interactions.filter(item => item?.status === 'completed').length, pending: interactions.filter(item => item?.status !== 'completed').length };
60
+ const stages = Object.fromEntries(['pending', 'running', 'stopped'].map(status => [status, validCount(report.stageContinuation?.[status]) ? report.stageContinuation[status] : 0]));
61
+ const unverifiedCandidates = findings.filter(finding => finding?.verification?.status !== 'completed').length;
62
+ coverage.unaccounted = Math.max(0, total - accounted);
63
+ const complete = report.status === 'completed' && detailsComplete && total > 0 && coverage.completed > 0 &&
64
+ !unfinished.length && !crossFile.pending && !Object.values(stages).some(Boolean) && !unverifiedCandidates && !coverage.unaccounted &&
65
+ !FILE_STATES.some(status => !['completed', 'excluded'].includes(status) && coverage[status] > 0);
66
+ return {
67
+ status: typeof report.status === 'string' ? report.status : 'unknown',
68
+ complete, outdated: Boolean(report.outdated), emptySelection: total === 0,
69
+ counts: { total: findings.length, ...counts },
70
+ coverage: { total, loaded: files.length, detailsComplete, unit: project ? 'files' : 'change-entries', ...coverage },
71
+ unfinished, crossFile, stages, unverifiedCandidates,
72
+ limitations: rows(report.limitations).length,
73
+ reused: findings.some(finding => finding?.reused || finding?.priorRun),
74
+ currentSourceOnly: Boolean(project || findings.some(finding => finding?.attribution?.causality === 'current-source-only')),
75
+ };
76
+ }
77
+
78
+ export function reportSummaryMarkdown(report = {}) {
79
+ const data = reportSummaryData(report), { counts, coverage } = data;
80
+ const state = data.emptySelection ? '未选择条目,没有执行代码审核(' + (STATUS_LABELS[data.status] ?? '状态未知') + ')' :
81
+ data.complete ? '所选范围已查完' : '尚未查完(' + (STATUS_LABELS[data.status] ?? '状态未知') + ')';
82
+ const headline = state + (data.outdated ? ';报告已过期' : '') + ';' +
83
+ (counts.total ? '记录 ' + counts.total + ' 个候选问题' : '未记录候选问题') + ';不得据此放行。';
84
+ const output = ['', '## 结论摘要', '- ' + headline];
85
+ if (counts.total) {
86
+ const labels = { introduced: '复核支持本次新增', unresolved: '待核实', preexisting: '复核支持原有', refuted: '复核反驳', current: '当前源码复核支持' };
87
+ output.push('- 候选复核:' + Object.entries(labels).filter(([key]) => counts[key] > 0).map(([key, label]) => label + ' ' + counts[key]).join(';') + '。');
53
88
  }
54
- if (incompleteInteractions.length) output.push('- 跨文件综合未完成:' + incompleteInteractions.length + ' 项;主文件完成不能替代交互覆盖。');
55
- if (rows(report.limitations).length) output.push('- 已记录限制:' + report.limitations.length + ' 项;完整内容保留在限制明细,不能据空发现判定通过。');
56
- if (total === 0) output.push('- 未选择文件,没有执行代码审核;不能解释为零缺陷结论。');
57
- if (!findings.length) output.push(total > 0 && coverage.completed > 0 && report.status === 'completed' && !unfinished.length && !incompleteInteractions.length && !rows(report.limitations).length && !report.outdated ? '- 已审范围内没有候选问题记录;不证明不存在缺陷。' : '- 当前没有候选问题记录;执行状态、未完成范围或限制不支持“审核通过”。');
89
+ const unit = coverage.unit === 'files' ? '个文件' : '个变更条目(含目录)';
90
+ const labels = { completed: '主审完成', excluded: '排除', blocked: '阻断', failed: '失败', cancelled: '取消', pending: '待审', running: '执行中', interrupted: '中断', unaccounted: '状态未记账' };
91
+ output.push('- 覆盖:' + coverage.total + ' ' + unit + ';' + Object.entries(labels).filter(([key]) => coverage[key] > 0).map(([key, label]) => label + ' ' + coverage[key]).join(',') + (coverage.total ? '。' : '未执行。'));
92
+ if (!coverage.detailsComplete) output.push('- 明细仅 ' + coverage.loaded + '/' + coverage.total + ' 项;未加载条目的路径与阶段状态未在本摘要核实。');
93
+ if (data.unfinished.length) output.push('- 未完成条目 ' + data.unfinished.length + '(完整范围见后文及附录)' + (coverage.detailsComplete ? '。' : ';仅计已加载明细。'));
94
+ if (Object.values(data.stages).some(Boolean)) output.push('- 后续阶段:待执行 ' + data.stages.pending + ',执行中 ' + data.stages.running + ',已停止 ' + data.stages.stopped + '。');
95
+ if (data.unverifiedCandidates) output.push('- 候选复核未完成:' + data.unverifiedCandidates + ' 项。');
96
+ if (data.crossFile.total) output.push('- 跨文件综合:完成 ' + data.crossFile.completed + '/' + data.crossFile.total + ' 项;未完成 ' + data.crossFile.pending + ' 项。');
97
+ if (data.limitations) output.push('- 已记录限制:' + data.limitations + ' 项,见限制明细。');
98
+ const qualifications = ['完成仅指处理进度,空发现不证明无缺陷'];
99
+ if (counts.total) qualifications.push('按候选记录而非去重根因计数,复核支持及引用校验不是事实或因果证明');
100
+ if (data.currentSourceOnly) qualifications.push('当前源码模式不评估历史新增归因');
101
+ if (data.reused) qualifications.push('含既往轮次复用发现,不表示本轮重新取证');
102
+ if (data.outdated) qualifications.push('仅对应原批准快照,不能作为当前源码结论');
103
+ output.push('- 技术限定:' + qualifications.join(';') + '。');
58
104
  return output;
59
105
  }
@@ -78,7 +78,7 @@ export function createReviewJobs({directory}={}){
78
78
  },
79
79
  async resume(id,exec){const r=await owned(id,exec);if(jobs.get(id)&&!jobs.get(id).settled||['queued','running'].includes(r.status)||!r.resumeState)throw new Error('No terminal completed-chain resume state available');return structuredClone(r.resumeState);},
80
80
  async artifacts(id,exec,page){const r=await owned(id,exec);return artifacts.read(id,r.owner,page);},
81
- async status(id,exec,page){const r=structuredClone(await owned(id,exec));delete r.resumeState;const active=jobs.get(id);if(active&&!active.settled&&!['queued','running'].includes(r.status)){r.status='running';r.stage='finalizing';delete r.result;delete r.finishedAt;}r.recovery=recoveryCoverage(r);r.progress=jobProgress(r);r.waitRevision=jobs.get(id)?.savedRevision??r.checkpoints.length;r.nextAction=['queued','running'].includes(r.status)?'Call code_review_wait with waitFor=terminal; reuse the exact returned waitRevision as after, never guess increments. No code_review_status call is needed after each event. Do not end the user request while task runs.':r.result?.previewId?'Show preview and request user confirmation before execute.':'Collect final report and unfinished scope, then provide one final answer.';if(!page)return r;const {offset=0,limit=20,checkpointIndex}=page;if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(limit)||limit<1||limit>100)throw new Error('Invalid result page');if(checkpointIndex!==undefined){if(!Number.isSafeInteger(checkpointIndex)||checkpointIndex<0||checkpointIndex>=r.checkpoints.length)throw new Error('Invalid checkpoint index');return {jobId:r.jobId,status:r.status,waitRevision:r.waitRevision,progress:r.progress,nextAction:r.nextAction,checkpointIndex,checkpoint:pageCheckpoint(r.checkpoints[checkpointIndex],offset,limit),page:{offset,limit}};}const result=r.result;const report=result?.report;return {...r,result:undefined,recovery:{...r.recovery,unfinishedStages:r.recovery.unfinishedStages.slice(offset,offset+limit),files:r.recovery.files.slice(offset,offset+limit),windows:r.recovery.windows.slice(offset,offset+limit),suggestedPaths:r.recovery.suggestedPaths.slice(offset,offset+limit),totalObservedFiles:r.recovery.files.length},checkpoints:r.checkpoints.slice(offset,offset+limit).map(c=>pageCheckpoint(c,0,limit)),checkpointPage:{offset,limit,total:r.checkpoints.length},...(result?{resultSummary:report?{status:report.status,coverage:report.coverage,model:report.model,snapshotId:report.snapshotId,diagnostics:report.diagnostics,execution:report.execution,modelCalls:report.modelCalls,totalFiles:report.files?.length,totalFindings:report.findings?.length,totalLimitations:report.limitations?.length}:({...result,files:result.files?.slice(offset,offset+limit),filePage:result.files?{offset,limit,total:result.files.length}:undefined}),resultPage:report?{files:report.files?.slice(offset,offset+limit),findings:report.findings?.slice(offset,offset+limit),totalFiles:report.files?.length,totalFindings:report.findings?.length}:undefined}: {})};},
81
+ async status(id,exec,page){const r=structuredClone(await owned(id,exec));delete r.resumeState;const active=jobs.get(id);if(active&&!active.settled&&!['queued','running'].includes(r.status)){r.status='running';r.stage='finalizing';delete r.result;delete r.finishedAt;}r.recovery=recoveryCoverage(r);r.progress=jobProgress(r);r.waitRevision=jobs.get(id)?.savedRevision??r.checkpoints.length;r.nextAction=['queued','running'].includes(r.status)?'Call code_review_wait with waitFor=terminal; reuse the exact returned waitRevision as after, never guess increments. No code_review_status call is needed after each event. Do not end the user request while task runs.':r.result?.previewId?'Inspect preview against current explicit user authorization. For a matching explicit review request, execute with confirmed=true without asking again. Do not execute preview-only or explicit wait-for-confirmation requests; ask for authorization if scope/model/destination exceeds authorization or is unclear. Preserve host permissions and snapshot/identity checks.':'Collect final report and unfinished scope, then provide one final answer.';if(!page)return r;const {offset=0,limit=20,checkpointIndex}=page;if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(limit)||limit<1||limit>100)throw new Error('Invalid result page');if(checkpointIndex!==undefined){if(!Number.isSafeInteger(checkpointIndex)||checkpointIndex<0||checkpointIndex>=r.checkpoints.length)throw new Error('Invalid checkpoint index');return {jobId:r.jobId,status:r.status,waitRevision:r.waitRevision,progress:r.progress,nextAction:r.nextAction,checkpointIndex,checkpoint:pageCheckpoint(r.checkpoints[checkpointIndex],offset,limit),page:{offset,limit}};}const result=r.result;const report=result?.report;return {...r,result:undefined,recovery:{...r.recovery,unfinishedStages:r.recovery.unfinishedStages.slice(offset,offset+limit),files:r.recovery.files.slice(offset,offset+limit),windows:r.recovery.windows.slice(offset,offset+limit),suggestedPaths:r.recovery.suggestedPaths.slice(offset,offset+limit),totalObservedFiles:r.recovery.files.length},checkpoints:r.checkpoints.slice(offset,offset+limit).map(c=>pageCheckpoint(c,0,limit)),checkpointPage:{offset,limit,total:r.checkpoints.length},...(result?{resultSummary:report?{status:report.status,coverage:report.coverage,model:report.model,snapshotId:report.snapshotId,diagnostics:report.diagnostics,execution:report.execution,modelCalls:report.modelCalls,totalFiles:report.files?.length,totalFindings:report.findings?.length,totalLimitations:report.limitations?.length}:({...result,files:result.files?.slice(offset,offset+limit),filePage:result.files?{offset,limit,total:result.files.length}:undefined}),resultPage:report?{files:report.files?.slice(offset,offset+limit),findings:report.findings?.slice(offset,offset+limit),totalFiles:report.files?.length,totalFindings:report.findings?.length}:undefined}: {})};},
82
82
  async wait(id,exec,{after,waitFor='progress',timeoutMs=60000,signal}={}){
83
83
  const r=await owned(id,exec);
84
84
  if(!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>60000)throw new Error('Invalid wait duration');
@@ -199,6 +199,6 @@ export function validateResumeState(value, snapshot, options) {
199
199
  }
200
200
 
201
201
  export function resumePreview(state, sourceJobId) {
202
- if (state.schemaVersion === 2) return { sourceJobId, reusableFiles: state.completed.length, fileIds: state.completed.map(item => item.fileId), reusableGenerations: state.stageGenerations.length, stageFileIds: [...new Set(state.stageGenerations.map(item => item.fileId))], notice: 'Exact-identity accepted primary generations may skip generation only. Pending chains are not completed reviews. Verification always runs again with fresh independent receipts; secondary stages restart at a fresh provenance boundary. Saved continuation is historical bookkeeping, not fresh completion or a new attempt allowance. Fresh preview confirmation and model authorization remain required.' };
203
- return { sourceJobId, reusableFiles: state.completed.length, fileIds: state.completed.map(item => item.fileId), notice: 'Only exact-identity completed file chains are reusable. Pending/interrupted stages run again; old findings retain prior-run provenance, not fresh model evidence. Fresh preview confirmation and model authorization are still required.' };
202
+ if (state.schemaVersion === 2) return { sourceJobId, reusableFiles: state.completed.length, fileIds: state.completed.map(item => item.fileId), reusableGenerations: state.stageGenerations.length, stageFileIds: [...new Set(state.stageGenerations.map(item => item.fileId))], notice: 'Exact-identity accepted primary generations may skip generation only. Pending chains are not completed reviews. Verification always runs again with fresh independent receipts; secondary stages restart at a fresh provenance boundary. Saved continuation is historical bookkeeping, not fresh completion or a new attempt allowance. Fresh preview and current explicit user/model authorization remain required; a matching explicit resume request needs no duplicate confirmation. Preview-only or explicit wait-for-confirmation does not authorize execution; ask for scope/model/destination changes.' };
203
+ return { sourceJobId, reusableFiles: state.completed.length, fileIds: state.completed.map(item => item.fileId), notice: 'Only exact-identity completed file chains are reusable. Pending/interrupted stages run again; old findings retain prior-run provenance, not fresh model evidence. Fresh preview and current explicit user/model authorization are still required; a matching explicit resume request needs no duplicate confirmation. Preview-only or explicit wait-for-confirmation does not authorize execution; ask for scope/model/destination changes.' };
204
204
  }
package/src/review.mjs CHANGED
@@ -35,6 +35,8 @@ import { configureWireBudget, modelWireBudget } from './model-wire-budget.mjs';
35
35
  import { createEvidenceContext } from './evidence-context.mjs';
36
36
  import { prepareInteractionRisk, consumeInteractionRisks } from './interaction-risk.mjs';
37
37
  import { reportSummaryMarkdown } from './report-summary.mjs';
38
+ import { findingsMarkdown } from './report-findings.mjs';
39
+ import { readabilityMarkdown, limitationDetailsMarkdown } from './report-readability.mjs';
38
40
  import { validateGroupInteraction } from './group-interaction.mjs';
39
41
  import { stageKey } from './review-stage-state.mjs';
40
42
  import { createStageGenerationCache } from './stage-generation-cache.mjs';
@@ -852,7 +854,7 @@ function renderFinding(output, finding, index, duplicates) {
852
854
  }
853
855
 
854
856
  export function markdownReport(report) {
855
- const output = ['# Code Review 报告', '', '状态:' + report.status, '快照:' + report.snapshotId, ...reportSummaryMarkdown(report), '', '## 审查发现'];
857
+ const output = ['# Code Review 报告', ...reportSummaryMarkdown(report), ...findingsMarkdown(report), ...readabilityMarkdown(report), '', '## 技术附录', '', '<details>', '<summary>展开完整证据、逐项覆盖与执行诊断(不影响上面的结论)</summary>', '', '状态:' + safe(report.status), '快照:' + safe(report.snapshotId), '', '## 候选原始证据与判定'];
856
858
  if (!report.findings.length) output.push(report.status === 'completed' ? '在已审查范围内未发现具体问题;不代表代码已被证明正确。' : '当前没有有效问题记录,但审查存在未完成项或限制,不能视为通过。');
857
859
  const duplicates = new Map();
858
860
  for (const group of groupDuplicateFindings(report.findings)) for (const index of group.findingIndices.slice(1)) duplicates.set(index, group.findingIndices[0]);
@@ -861,7 +863,7 @@ export function markdownReport(report) {
861
863
  if (report.callerDiscovery) output.push('- 引用方发现:扫描 '+report.callerDiscovery.scanned.length+' 个文件,捕获 '+report.callerDiscovery.capturedPaths.length+' 个上下文;仅批准目录字面引用导航,不证明函数调用或完整覆盖。');
862
864
  if(report.businessRequirement)output.push('- 业务需求:SHA256 '+safe(report.businessRequirement.hash)+' · '+report.businessRequirement.bytes+' bytes;不可信约束,不是证据或权限,全文不在报告重复。');
863
865
  for (const file of report.files) for (const correction of file.anchorCorrections ?? []) output.push('- 定位纠正:' + safe(correction.originalCandidate.fileId) + ' · ' + safe(correction.status) + ' · ' + safe(correction.reason) + ';原完整候选保留JSON,仅定位不证明因果');
864
- if (report.history) output.push('- Git历史审查:' + safe(report.history.mode) + ' · ' + safe(report.history.base ?? 'empty') + ' → ' + safe(report.history.target) + ';精确端点树差异,重命名按增删,不含工作树');
866
+ if (report.history) output.push('- ' + (String(report.history.mode).startsWith('svn') ? 'SVN' : 'Git') + '历史审查:' + safe(report.history.mode) + ' · ' + safe(report.history.base ?? 'empty') + ' → ' + safe(report.history.target) + ';精确端点树差异,重命名按增删,不含工作树');
865
867
  if (report.changeScope) output.push('- Git审查范围:' + safe(changeScopeLabel(report.changeScope)) + ';其他范围未审查,未跟踪文件仍默认排除。');
866
868
  if (report.businessGrouping) output.push('- 业务分组:' + safe(report.businessGrouping.status) + (report.businessGrouping.reason ? ' — ' + safe(report.businessGrouping.reason) : '') + ';仅调度假设,不证明依赖或覆盖');
867
869
  for (const file of report.files) for (const plan of file.riskPlans ?? []) output.push('- 风险计划:' + safe(file.path) + ' · ' + safe(plan.status) + (plan.method === 'inline' ? '(合并主审,非独立规划)' : '') + ' · ' + plan.risks.length + '项待证假设;不表示缺陷成立或覆盖完成');
@@ -880,6 +882,6 @@ export function markdownReport(report) {
880
882
  if (report.rules?.length) output.push('', '## 项目规则', '规则仅为已批准约束数据,不扩大权限或证明缺陷。', ...report.rules.map(rule => '- ' + safe(rule.path) + ' · SHA256 ' + safe(rule.hash)));
881
883
  if (report.followup?.suggestedPaths.length) output.push('', '## 后续选择建议', '仅为原快照未完成项;不会自动续审。再次执行需新预览、新审批,并显式选择规则和上下文。阻断项需先处理原因;关联上下文不等于主文件已审查。', ...report.followup.items.filter(item => item.followup).map(item => '- ' + safe(item.path) + ':' + item.status + (item.incompleteVerification ? '(候选复核未完成)' : '')));
882
884
  output.push(...diagnosticsMarkdown(report.diagnostics));
883
- if (report.limitations.length) output.push('', '## 限制', ...report.limitations.map(l => '- ' + safe(l.text)));
885
+ output.push(...limitationDetailsMarkdown(report), '', '</details>');
884
886
  return output.join('\n') + '\n';
885
887
  }
package/src/service.mjs CHANGED
@@ -53,7 +53,7 @@ function describeFile(file, snapshot) {
53
53
  /** Compose the user-facing preview notice; text is verbatim from the original rules. */
54
54
  function buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcurrency, maxRetrievalRounds) {
55
55
  const scopeNotice = options.changeScope !== undefined && options.changeScope !== 'worktree' ? changeScopePreviewNotice(options.changeScope) : '';
56
- return scopeNotice + (snapshot.history?.vcs==='svn'&&options.autoContext===true?'SVN历史autoContext为精确依赖定位读取固定版本父目录有界清单,包含同目录非匹配名字元数据;仅匹配捕获正文发送模型,不扫描全仓,不用工作树替代历史版本。':'') + (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + `执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多${maxRetrievalRounds}轮检索;独立文件审查链最多${changesConcurrency}条并发,不设插件内部默认时间或总模型调用次数截止,遵循宿主/用户取消;输入字节和检索轮次保护仍保留;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。`;
56
+ return scopeNotice + (snapshot.history?.vcs==='svn'&&options.autoContext===true?'SVN历史autoContext为精确依赖定位读取固定版本父目录有界清单,包含同目录非匹配名字元数据;仅匹配捕获正文发送模型,不扫描全仓,不用工作树替代历史版本。':'') + (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + `执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多${maxRetrievalRounds}轮检索;独立文件审查链最多${changesConcurrency}条并发,不设插件内部默认时间或总模型调用次数截止,遵循宿主/用户取消;输入字节和检索轮次保护仍保留;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先内部核对预览与当前人类授权。用户已明确要求审核且范围、模型和发送目的地一致时直接执行,不重复询问确认;只要求预览或明确要求等待确认时不得执行,超出授权或边界不明确时才询问。宿主权限和快照/身份校验仍须通过。`;
57
57
  }
58
58
 
59
59
  function createSessionReviewService(llm, { capture = captureSnapshot, now = Date.now, ttlMs = 300000, maxPreviews = 8, allowModelSending = false, enableRiskPlanning = false, skipSmallRiskPlans = true, reviewRounds = 1, enableBusinessGrouping = false, enableAnchorCorrection = false, changesConcurrency = 2, maxRetrievalRounds = 3, riskPlanningMode = 'separate', readerBudgetPolicy = 'fixed', optimizeReviewCalls = false, enablePrimarySeeds = false, focusPrimaryContext = false, enableSearchSeeds = false, nativeOutputTools = false, enableGroupPrimary = false, enableEvidenceContext = false, inlineInteractionRisks = false, enableMigrationContracts = false, enableMigrationRegression = false, enableGroupInteractions = false, enableStageContinuation = false, authorize = async () => false } = {}) {