@feng0531/dsh-code-review 0.83.1 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/CORE-ALIGNMENT-TASKS.md +1 -1
- package/DEVELOPMENT.md +7 -1
- package/README.md +24 -2
- package/RELEASE-NOTES.md +16 -8
- package/index.mjs +3 -3
- package/package.json +1 -1
- package/src/anchor-correction.mjs +6 -4
- package/src/dsh-model.mjs +78 -72
- package/src/git-change-scope.mjs +39 -0
- package/src/git.mjs +138 -26
- package/src/model-contract.mjs +10 -5
- package/src/model-json.mjs +16 -10
- package/src/primary-input.mjs +2 -0
- package/src/project-execution.mjs +40 -0
- package/src/project-review-service.mjs +12 -13
- package/src/response-correction.mjs +28 -0
- package/src/retrieval-loop.mjs +57 -12
- package/src/review.mjs +67 -34
- package/src/service.mjs +5 -2
- package/src/snapshot-reader.mjs +95 -48
- package/src/snapshot.mjs +6 -0
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.85.0 — 审查失败恢复与全项目提速
|
|
6
|
+
|
|
7
|
+
- 全项目独立批次默认 3 路并行,插件配置 projectConcurrency 支持 1~8;预览/报告显示并发数,结果仍按原批次顺序合并,保持共享调用上限和取消后等待全部清理。批内主审与复核不跳过,预算竞争时未完成文件仍待处理。
|
|
8
|
+
- 无效 JSON 最多两次显式纠正,记录所有尝试的脱敏诊断与实际调用次数;不修补响应,不重试取消、提供方失败或不完整输出。纠正结果不再插入可能破坏阶段 schema 的元数据。
|
|
9
|
+
- 主审身份、严重性、字段或行证据校验失败可请求一次新响应,仍严格验证,不自动改身份、映射严重性或伪造证据。全项目模式支持同文件当前侧唯一精确文本的行号修正,保留原候选并继续窗口校验。
|
|
10
|
+
- 向模型提供剩余检索预算;预检整批动态读取范围及预算,避免无效批次部分消耗。预算不足仅给一次受限最终答复,文件仍未完成,不把审查失败当缺陷或通过。
|
|
11
|
+
|
|
12
|
+
## 0.84.0 — 暂存区与未暂存审查范围
|
|
13
|
+
|
|
14
|
+
新增 Git `changeScope`:`staged` 只审查 HEAD 到索引的已暂存内容,工作区里尚未暂存的修改不作为变更正文参与或发送(上下文与规则文件仍按工作区读取);`unstaged` 只审查索引到工作区的未暂存改动,已暂存内容仅作旧侧基线。省略时仍是 HEAD 到工作区的净变化。两个范围只用于 Git 工作副本,不能与历史版本、SVN 参数或全项目模式混用;未跟踪文件仍默认排除。预览与报告会标明实际比较范围,执行前重新核验对应内容。
|
|
15
|
+
|
|
5
16
|
## 0.83.1 — 代码结构整理
|
|
6
17
|
|
|
7
18
|
整理内部结构,不改变行为:模型指令集中到 src/prompts.mjs 并由原模块继续重导出;重复的工具输出 schema 抽到 src/tool-view.mjs;缺陷/协议指引的扩展名判定改为优先级查找表;超长单行与巨型函数拆成具名小函数,覆盖 service、dsh-model、git、git-history、snapshot、review 渲染、项目模式、会话/后台任务与 SVN 纯辅助模块。工具参数、协议字段和报告语义不变,升级前后输出一致。
|
package/CORE-ALIGNMENT-TASKS.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| 能力 | 用途 | 不能据此推断 |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| Git/SVN变更采集 |
|
|
9
|
+
| Git/SVN变更采集 | Git工作副本净变化/暂存区/未暂存范围、固定历史端点、同仓库SVN分支比较 | 任意SVN状态均可审查、完整提交逐一覆盖 |
|
|
10
10
|
| 范围与来源保护 | 先预览后确认,固定来源与哈希,执行前重新采集 | 普通源码一定没有秘密、模型提供方不保存数据 |
|
|
11
11
|
| 上下文 | 显式文件、旧侧文件、最多三层字面依赖、限定目录引用方 | 完整语义依赖图、调用图或动态执行可达性 |
|
|
12
12
|
| 输入与调度 | 差异窗口、分组和失败回退,共享预算 | 大仓库一定能在预算内完成 |
|
package/DEVELOPMENT.md
CHANGED
|
@@ -14,10 +14,16 @@
|
|
|
14
14
|
|
|
15
15
|
## 会话与模式
|
|
16
16
|
|
|
17
|
-
服务按DSH session对象隔离缓存和锁,同一会话changes/project
|
|
17
|
+
服务按DSH session对象隔离缓存和锁,同一会话changes/project两种模式共用互斥锁;不同会话可并行,模型调用与取消由各请求生命周期约束。Git工作区默认比较HEAD到工作区;changeScope=staged/unstaged分别只比较HEAD→index和index→工作区,执行前复核对应内容。宿主dispose终止全部会话服务。全项目是跟踪文件的当前源码分批,不是多仓库汇总或跨批次语义分析。
|
|
18
18
|
|
|
19
19
|
项目捕获最多10000清单节点、1000选择节点、16MiB,不设默认总模型调用次数或执行秒表,初次捕获/复核均遵循宿主与用户取消。SVN项目模式拒绝不支持节点属性,不隐式继承固定历史审查的网络权限。
|
|
20
20
|
|
|
21
|
+
## 全项目并发与离线速度对照
|
|
22
|
+
|
|
23
|
+
插件配置 `projectConcurrency` 默认为 3,范围 1~8;1 为串行回退。仅独立批准批次并行,各批仍有独立检索来源和账本,所有调用与 JSON 纠正共享原执行计数。该上限按单次执行生效,不是全宿主 RPM/TPM 限流器;提供方不支持并发或遇到 429 时应调低。取消/致命检查点错误必须等待全部在途模型清理后释放会话锁,后台 checkpoint 继续使用原串行持久化队列。
|
|
24
|
+
|
|
25
|
+
运行 `node scripts/review-speed-benchmark.mjs` 比较相同 12 个 mock 批次在串行与 3 路并发下的耗时;脚本模拟每次模型调用 40ms 延迟,不访问网络、不证明真实模型速度或质量。核对工作量、调用数、完成数和输出顺序一致,不把降低覆盖作为加速。
|
|
26
|
+
|
|
21
27
|
## 审查预算
|
|
22
28
|
|
|
23
29
|
除 README 中的文件和输入上限外:
|
package/README.md
CHANGED
|
@@ -104,6 +104,20 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
|
|
|
104
104
|
|
|
105
105
|
比较两个版本的文件树,不自动计算合并基点,也不会把工作区中的未提交正文当作历史正文。`commit` 不能与这两个参数混用。
|
|
106
106
|
|
|
107
|
+
### 只审查 Git 暂存区或未暂存改动
|
|
108
|
+
|
|
109
|
+
提交前只看已暂存内容,或只想确认工作区里还没暂存的修改:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{"changeScope":"staged"}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`staged` 比较 HEAD 与索引(相当于 `git diff --cached`):右侧是已暂存内容,工作区里尚未 `git add` 的修改不作为变更内容,也不会作为变更正文发送给模型。把值换成 `"unstaged"` 则比较索引与工作区(相当于 `git diff`):左侧是已暂存内容,仅作为旧侧基线,右侧是工作区文件。省略 `changeScope` 时默认仍是 HEAD 到工作区的净变化。
|
|
116
|
+
|
|
117
|
+
这两个范围只用于 Git 工作副本,不能与 `commit`、`baseRevision`、`targetRevision`、SVN 参数或 `reviewMode:"project"` 同时使用。未跟踪文件仍默认排除;`worktree` / `unstaged` 下用 `selectedPaths` 显式选择后才会纳入。预览和报告都会标明实际比较的是哪两个内容。
|
|
118
|
+
|
|
119
|
+
`staged` 下显式上下文、自动依赖、调用方候选和规则文件仍按工作区当前内容读取,可能包含尚未暂存的修改;预览会列出这些文件。
|
|
120
|
+
|
|
107
121
|
### 审查 SVN 单个 revision
|
|
108
122
|
|
|
109
123
|
例如审查 r7315 引入的净变化,即 **r7314 → r7315**:
|
|
@@ -242,6 +256,8 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
242
256
|
|
|
243
257
|
清单上限10000节点,单次选择最多1000节点/16MiB,12节点一批,单文件256KiB;超限明确拒绝,缩小选择后必须重新预览。单次采集共享8MiB元数据内存保护,不按命令条数、插件秒表或模型调用次数中断;耗时受宿主/模型提供方限制,用户可随时取消。SVN为本地已观察清单,稀疏/外部/链接及不支持属性显示限制;不证明仓库远端完整覆盖。
|
|
244
258
|
|
|
259
|
+
全项目审查默认最多 **3 个独立批次并行**,每批内主审、检索、复核仍按依赖顺序执行;预览和报告的 `execution.batchConcurrency` 显示实际配置。插件配置 `projectConcurrency` 可设为 1~8,设 1 回退串行;遇到模型提供方并发限制或 429,应降低并发。该设置不是 preview 参数,不扩大来源或共享模型调用预算,也不减少模型调用量或承诺 token 成本下降。仅一个批次时不会获得批间加速;实际提速受提供方吞吐、批次均衡及检查点写入影响。
|
|
260
|
+
|
|
245
261
|
对外只交付一份统一最终报告,给出总数、completed、excluded、blocked、pending、去重后的发现及未完成综合阶段;内部批次快照仅供证据追溯,不需要用户拼接报告。completed只表示所选捕获文件已处理,不代表代码正确;排除与阻断从不算完成。代码或清单变化会拒绝旧预览,运行后无法复核则标记outdated。
|
|
246
262
|
|
|
247
263
|
## 工具调用流程(给需要精确指定参数的人)
|
|
@@ -269,6 +285,12 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
269
285
|
|
|
270
286
|
每个候选问题会保留位置、触发条件、影响和复核结果。复核可能支持、反驳、无法确定或未完成;引用原文匹配也不等于缺陷一定成立。**没有发现问题不证明没有缺陷,读取过上下文也不等于审查了整个项目。**
|
|
271
287
|
|
|
288
|
+
### 模型协议错误与读取预算恢复
|
|
289
|
+
|
|
290
|
+
已启用的模型桥接对完整结束但 JSON 无效的响应最多请求两次明确纠正(共最多三次调用),不会修补损坏 JSON;取消、提供方错误、不完整结束或输出超限不作此类重试。主审响应中的文件身份、严重性、必填字段或行证据校验失败时,最多请求一次完整响应纠正,仍按原快照、输入及调用预算严格验证。每次额外调用和纠正事件都会计数记录,恢复失败仍标为失败,不代表发现了缺陷。
|
|
291
|
+
|
|
292
|
+
全项目模式可将行号错位的证据重新定位到同文件当前侧的唯一精确文本;不跨文件、跨版本或模糊匹配,原候选保留供审计,窗口范围仍须通过校验。模型会收到共享检索预算余量;预算耗尽或请求超过余量时仅给一次不再检索的最终答复机会,不扩大读取权限。预算受限的文件仍列为未完成,已有有效候选保留,不会因返回空 findings 就算审查通过。
|
|
293
|
+
|
|
272
294
|
## 常见问题
|
|
273
295
|
|
|
274
296
|
| 遇到的问题 | 处理方法 |
|
|
@@ -280,13 +302,13 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
280
302
|
| SVN 历史提示需要网络许可 | 确认可以访问仓库后,预览参数增加 `svnAllowNetwork:true` |
|
|
281
303
|
| SVN 登录、权限或版本不存在 | 检查 SVN 本身的访问权限和版本号;不要把密码写进对话或仓库 URL |
|
|
282
304
|
| 文件被阻断或报告 partial | 阅读具体原因;缩小变更批次、补充允许的上下文,或处理真实冲突后重新预览。不能把排除文件视为通过 |
|
|
283
|
-
|
|
|
305
|
+
| 想只审查 Git 暂存区或只审查未暂存改动 | 预览参数设置 `changeScope:"staged"`(HEAD → 索引)或 `changeScope:"unstaged"`(索引 → 工作区);默认仍是 HEAD 到工作区的净变化 |
|
|
284
306
|
|
|
285
307
|
## 支持范围与安全边界
|
|
286
308
|
|
|
287
309
|
- 执行会把批准范围内的代码、上下文、所选规则及业务要求发送给当前模型提供方;敏感项目请先检查预览和模型。常见凭据路径过滤不能保证源码没有秘密。
|
|
288
310
|
- 插件遵循 DSH 工具权限;确认预览不等于绕过宿主权限。不执行项目代码、构建脚本或模型生成的 shell 命令。
|
|
289
|
-
- Git
|
|
311
|
+
- Git 支持工作区净变化、暂存区/未暂存范围、单提交、两版本比较。SVN 支持工作副本和固定历史端点;**尚未完整支持所有 SVN 状态**。
|
|
290
312
|
- SVN 根目录属性可预览;同仓库复制新增以空目标旧侧审查,复制来源只作信息;移动的新增/删除分别覆盖。目录后代须逐项纳入,选择父目录不自动批准所有后代。
|
|
291
313
|
- SVN 标准、固定宽度和受支持的复合关键字展开,以及工作副本变更/自动依赖/命中调用方、固定历史变更及上下文的换行元数据保留原文证据;不以规范化结果证明行为等价。自定义/固定宽度关键字、未提交替换、冲突、特殊节点、切换/外部工作副本及跨仓库比较等仍有限制。同仓库跨分支与独立基线替换需要下文列出的显式参数。已提交固定历史替换与未提交替换的支持不同,详见 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md)。
|
|
292
314
|
- 二进制、不能解码的文件、普通源码路径中的链接和超限内容会明确阻断;已跟踪的 SVN 链接只审查 `link 目标` 记录,不读取目标正文,不静默截断后声称完成。文本导航不证明语义绑定、执行可达或完整调用覆盖。
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
# v0.
|
|
1
|
+
# v0.85.0 发布说明
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 全项目审核提速
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
全项目模式默认最多 3 个独立批准批次并行;批内主审、检索、复核仍按依赖顺序执行,不跳过文件或降低证据校验要求。插件配置 `projectConcurrency` 支持 1~8,设为 1 回退串行;预览与结构化报告展示实际配置。最终报告仍按原批次、文件顺序合并。共享模型调用上限覆盖检索、复核和纠正;取消或致命检查点失败后等待在途调用清理再释放会话锁,未发送文件保持待处理。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- 重复的工具输出 schema 抽到 `src/tool-view.mjs`。
|
|
9
|
-
- 缺陷/协议指引的扩展名判定改为优先级查找表;超长单行与巨型函数拆成具名小函数,覆盖 service、dsh-model、git、git-history、snapshot、review 渲染、项目模式与会话/后台任务。
|
|
10
|
-
- SVN 纯辅助模块(URL、EOL、关键字、mergeinfo、预算等)按同一风格整理。
|
|
7
|
+
离线延迟模拟显示并行调度收益,但不是生产模型速度承诺,也不证明质量提升。实际速度取决于提供方并发、限流、批次均衡及检查点持久化;并行不减少模型调用量或保证 token 成本下降。并发上限按单次执行生效,不是跨会话 RPM/TPM 限流器;遇到 429 应降低配置。
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
## 审查失败的有界恢复
|
|
10
|
+
|
|
11
|
+
- 模型正常结束但 JSON 无效时,最多两次显式重新生成;不正则修补损坏响应,不回灌无效输出,不重试取消、提供方错误、截断结束或超限输出。记录每次尝试的脱敏诊断和预算计数,不往阶段响应注入可能破坏 schema 的元数据。
|
|
12
|
+
- 主审文件身份、严重性、必填字段或行证据校验失败时,最多请求一次完整响应纠正,仍执行严格校验,不自动更改身份、映射严重性或编造证据。
|
|
13
|
+
- 全项目行号错位仅在同文件当前侧存在唯一精确文本时修正,保留原候选供审计;不跨文件、跨版本或模糊匹配,窗口限制继续生效。
|
|
14
|
+
- 模型可见共享检索预算余量;整批动态范围与预算先预检,实际输出超限不部分发布结果或审计。预算耗尽后只允许一次最终答复,不扩大读取权限;已有有效候选保留,文件仍未完成,不以空 findings 视为通过。
|
|
15
|
+
|
|
16
|
+
## 兼容性与验证边界
|
|
17
|
+
|
|
18
|
+
默认审查范围和 Git/SVN 权限边界不变;未跟踪、忽略和外部来源不会因并行自动纳入。`completed` 表示相应处理完成,不是代码正确或缺陷不存在的保证;失败文件也不代表发现了缺陷。
|
|
19
|
+
|
|
20
|
+
用法见 [README](README.md),发布与离线验证方法见 [DEVELOPMENT](DEVELOPMENT.md)。本版本不宣称已完成当前工作区的真实模型串行/并行质量与速度对照。发布不自动安装、升级或重启 DSH,需用户更新并重新加载插件。
|
package/index.mjs
CHANGED
|
@@ -5,15 +5,15 @@ import { reviewToolOutput } from './src/tool-view.mjs';
|
|
|
5
5
|
export const name = 'dsh-code-review';
|
|
6
6
|
export const inject = ['tools', 'llm'];
|
|
7
7
|
|
|
8
|
-
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. Show the preview before execution.';
|
|
9
|
-
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.'}, 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 } } };
|
|
8
|
+
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_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
10
|
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.';
|
|
11
11
|
const EXECUTE_PARAMETERS = { type: 'object', additionalProperties: false, properties: { previewId: { type: 'string' }, confirmed: { type: 'boolean' } }, required: ['previewId', 'confirmed'] };
|
|
12
12
|
|
|
13
13
|
export function apply(ctx, config = {}) {
|
|
14
14
|
// This entry runs only when the host invokes the registered tool.
|
|
15
15
|
// Host tool policy owns permission; do not request a second approval here.
|
|
16
|
-
const service = createReviewService(ctx.llm, { allowModelSending: true, enableRiskPlanning: true, enableBusinessGrouping: true, enableAnchorCorrection: true, authorize: async () => true });
|
|
16
|
+
const service = createReviewService(ctx.llm, { allowModelSending: true, enableRiskPlanning: true, enableBusinessGrouping: true, enableAnchorCorrection: true, projectConcurrency: config.projectConcurrency ?? 3, authorize: async () => true });
|
|
17
17
|
ctx.tools.register({
|
|
18
18
|
name: 'code_review_preview', description: PREVIEW_DESCRIPTION, parameters: PREVIEW_PARAMETERS, output: reviewToolOutput,
|
|
19
19
|
async execute(args, exec) { const preview = await service.preview(args, exec); return { json: JSON.stringify(preview), markdown: '' }; },
|
package/package.json
CHANGED
|
@@ -6,13 +6,15 @@ export function correctAnchor(raw, file, changes, {signal}={}) {
|
|
|
6
6
|
const text=(a.side==='old'?file.left:file.right)?.text;if(typeof text!=='string'||Buffer.byteLength(text)>256*1024)return result('not-applicable','source-limit');
|
|
7
7
|
const source=text.replaceAll('\r\n','\n').split('\n'),snippet=a.snippet.replaceAll('\r\n','\n').split('\n');
|
|
8
8
|
if(source.slice(a.start-1,a.end).join('\n')===snippet.join('\n'))return result('unchanged','already-exact');
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const currentSource = file.reviewMode === 'project' && a.side === 'new';
|
|
10
|
+
if(file.reviewMode === 'project' && !currentSource)return result('rejected','project-requires-new-side');
|
|
11
|
+
if(!currentSource&&(changes?.status!=='changed'||changes.precision!=='exact'||!Array.isArray(raw.attribution?.editIds)||!raw.attribution.editIds.length))return result('rejected','missing-exact-edit');
|
|
12
|
+
const edits=currentSource?[]:changes.edits.filter(e=>raw.attribution.editIds.includes(e.id));if(!currentSource&&edits.length!==raw.attribution.editIds.length)return result('rejected','unknown-or-duplicate-edit');
|
|
11
13
|
// Bound CPU even for a long multiline snippet; no silent partial search acceptance.
|
|
12
14
|
if(source.length*snippet.length>2000000)return result('rejected','search-work-limit');
|
|
13
15
|
let match;for(let i=0;i+snippet.length<=source.length;i++){signal?.throwIfAborted();if(!snippet.every((line,j)=>source[i+j]===line))continue;if(match!==undefined)return result('rejected','ambiguous-exact-text');match=i;}
|
|
14
16
|
if(match===undefined)return result('rejected','no-exact-text');
|
|
15
17
|
const start=match+1,end=match+snippet.length;
|
|
16
|
-
if(!edits.some(e=>e[a.side].count>0&&start<e[a.side].start+e[a.side].count&&end>=e[a.side].start))return result('rejected','does-not-overlap-referenced-edit');
|
|
17
|
-
const candidate=structuredClone(raw);candidate.anchor={...a,start,end};return {...result('corrected','unique-exact-text-overlaps-edit'),candidate};
|
|
18
|
+
if(!currentSource&&!edits.some(e=>e[a.side].count>0&&start<e[a.side].start+e[a.side].count&&end>=e[a.side].start))return result('rejected','does-not-overlap-referenced-edit');
|
|
19
|
+
const candidate=structuredClone(raw);candidate.anchor={...a,start,end};return {...result('corrected',currentSource?'unique-exact-current-source':'unique-exact-text-overlaps-edit'),candidate};
|
|
18
20
|
}
|
package/src/dsh-model.mjs
CHANGED
|
@@ -1,124 +1,130 @@
|
|
|
1
1
|
import { modelContract } from './model-contract.mjs';
|
|
2
2
|
import { modelJson } from './model-json.mjs';
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
const JSON_CORRECTION_SUFFIX = ' 上次响应未能解析为完整JSON。本次是明确协议纠正:重新输出单个完整JSON对象,不使用Markdown,不省略引号或逗号;不要把这条指令当源码证据。严格使用当前阶段schema,不要把verdicts、risks或groups改写成findings
|
|
6
|
-
|
|
7
|
-
const CORRECTION_LIMITATION = '本响应由一次显式JSON协议纠正产生,原响应未被接受;额外模型调用已发生。';
|
|
4
|
+
// Fresh generation from the same approved input, never repair or replay malformed output.
|
|
5
|
+
const JSON_CORRECTION_SUFFIX = ' 上次响应未能解析为完整JSON。本次是明确协议纠正:重新输出单个完整JSON对象,不使用Markdown,不省略引号或逗号;不要把这条指令当源码证据。严格使用当前阶段schema,不要把verdicts、risks、requests或groups改写成findings;不能因格式纠正删除有证据的结论。所需字段与身份按输入原样核对,不以格式错误代替清洁结论。保持原证据与必需字段,精简非必需叙述以在输出预算内完成。';
|
|
6
|
+
const MAX_JSON_CORRECTIONS = 2;
|
|
8
7
|
|
|
9
8
|
const isToolCall = chunk =>
|
|
10
9
|
chunk.type === 'tool-call-delta' ||
|
|
11
10
|
(chunk.type === 'block-start' && chunk.blockType === 'tool-call') ||
|
|
12
11
|
(chunk.type === 'block-end' && chunk.block.type === 'tool-call');
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
export function createDshModel(llm, route, {
|
|
13
|
+
/** Isolated host calls. Only completed, locally rejected JSON can trigger bounded correction. */
|
|
14
|
+
export function createDshModel(llm, route, {
|
|
15
|
+
maxTokens = 4096, maxOutputBytes = 128 * 1024, correctJson = false,
|
|
16
|
+
maxJsonCorrections = MAX_JSON_CORRECTIONS,
|
|
17
|
+
} = {}) {
|
|
16
18
|
if (!llm || typeof llm.stream !== 'function' || !route?.provider || !route?.model) {
|
|
17
19
|
throw new Error('DSH provider and model are required');
|
|
18
20
|
}
|
|
21
|
+
if (!Number.isSafeInteger(maxTokens) || maxTokens < 1 ||
|
|
22
|
+
!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 ||
|
|
23
|
+
!Number.isSafeInteger(maxJsonCorrections) || maxJsonCorrections < 0 || maxJsonCorrections > MAX_JSON_CORRECTIONS) {
|
|
24
|
+
throw new Error('Invalid model output or JSON correction limit');
|
|
25
|
+
}
|
|
19
26
|
const identity = {
|
|
20
27
|
provider: route.provider,
|
|
21
28
|
model: route.model,
|
|
22
29
|
...(route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}),
|
|
23
30
|
};
|
|
31
|
+
// Provider failures with a coincidentally identical code are not format failures.
|
|
32
|
+
const invalidJsonErrors = new WeakSet();
|
|
24
33
|
|
|
25
|
-
async function once({ instructions, input, signal }) {
|
|
34
|
+
async function once({ instructions, input, signal }, contract) {
|
|
26
35
|
signal?.throwIfAborted();
|
|
27
36
|
let output = '';
|
|
28
37
|
let size = 0;
|
|
29
38
|
let finished = false;
|
|
30
39
|
let finishKind = 'missing';
|
|
31
|
-
|
|
32
40
|
const diagnostic = () => ({
|
|
33
|
-
visibleBytes: Buffer.byteLength(output),
|
|
34
|
-
|
|
35
|
-
finishKind,
|
|
36
|
-
inputBytes: Buffer.byteLength(input),
|
|
37
|
-
requestedMaxTokens: maxTokens,
|
|
41
|
+
visibleBytes: Buffer.byteLength(output), totalOutputBytes: size, finishKind,
|
|
42
|
+
inputBytes: Buffer.byteLength(input), requestedMaxTokens: maxTokens, maxOutputBytes,
|
|
38
43
|
});
|
|
39
|
-
|
|
40
|
-
const providerCode = reason =>
|
|
41
|
-
typeof reason.error?.code === 'string' ? reason.error.code
|
|
42
|
-
: typeof reason.code === 'string' ? reason.code
|
|
43
|
-
: undefined;
|
|
44
|
-
|
|
44
|
+
const failure = (message, code) => Object.assign(new Error(message), { code, diagnostic: diagnostic() });
|
|
45
45
|
const stream = llm.stream({
|
|
46
46
|
...identity,
|
|
47
|
-
system: instructions + '\n' +
|
|
47
|
+
system: instructions + '\n' + contract,
|
|
48
48
|
messages: [{ role: 'user', content: [{ type: 'text', text: input }] }],
|
|
49
|
-
tools: [],
|
|
50
|
-
maxTokens,
|
|
51
|
-
signal,
|
|
49
|
+
tools: [], maxTokens, signal,
|
|
52
50
|
});
|
|
53
|
-
|
|
54
51
|
for await (const chunk of stream) {
|
|
55
52
|
signal?.throwIfAborted();
|
|
56
|
-
if (finished) throw
|
|
57
|
-
if (isToolCall(chunk)) throw
|
|
58
|
-
|
|
53
|
+
if (finished) throw failure('Model emitted data after finish', 'MODEL_PROTOCOL_ERROR');
|
|
54
|
+
if (isToolCall(chunk)) throw failure('Review model attempted a tool call', 'MODEL_PROTOCOL_ERROR');
|
|
59
55
|
if (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta') {
|
|
60
56
|
size += Buffer.byteLength(chunk.text);
|
|
61
|
-
if (size > maxOutputBytes) throw
|
|
57
|
+
if (size > maxOutputBytes) throw failure('Model output limit exceeded', 'MODEL_OUTPUT_LIMIT');
|
|
62
58
|
if (chunk.type === 'text-delta') output += chunk.text;
|
|
63
59
|
}
|
|
64
|
-
|
|
65
60
|
if (chunk.type === 'finish') {
|
|
66
61
|
finishKind = chunk.reason.kind;
|
|
67
62
|
if (finishKind !== 'stop') {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
const error = failure('Model did not complete: ' + finishKind, 'MODEL_INCOMPLETE_OUTPUT');
|
|
64
|
+
const providerCode = chunk.reason.error?.code ?? chunk.reason.code;
|
|
65
|
+
if (typeof providerCode === 'string') error.diagnostic.providerCode = providerCode;
|
|
66
|
+
throw error;
|
|
72
67
|
}
|
|
73
68
|
finished = true;
|
|
74
69
|
}
|
|
75
70
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
code: 'MODEL_INCOMPLETE_OUTPUT',
|
|
80
|
-
diagnostic: diagnostic(),
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
71
|
+
// Cancellation during iterator cleanup/end must not become an invalid-JSON correction.
|
|
72
|
+
signal?.throwIfAborted();
|
|
73
|
+
if (!finished) throw failure('Model stream ended without finish', 'MODEL_INCOMPLETE_OUTPUT');
|
|
84
74
|
try {
|
|
85
|
-
return modelJson(output);
|
|
75
|
+
return { text: modelJson(output), diagnostic: diagnostic() };
|
|
86
76
|
} catch (error) {
|
|
87
|
-
error.diagnostic = diagnostic();
|
|
77
|
+
error.diagnostic = { ...diagnostic(), ...error.diagnostic };
|
|
78
|
+
invalidJsonErrors.add(error);
|
|
88
79
|
throw error;
|
|
89
80
|
}
|
|
90
81
|
}
|
|
91
82
|
|
|
92
|
-
async function retryOnceCorrected(request, firstError) {
|
|
93
|
-
await request.onProtocolCorrection?.({
|
|
94
|
-
code: firstError.code,
|
|
95
|
-
diagnostic: firstError.diagnostic,
|
|
96
|
-
action: 'explicit-json-correction',
|
|
97
|
-
notice: 'Second model response requested; original output not repaired or accepted.',
|
|
98
|
-
});
|
|
99
|
-
let value;
|
|
100
|
-
try {
|
|
101
|
-
value = JSON.parse(await once({ ...request, instructions: request.instructions + JSON_CORRECTION_SUFFIX }));
|
|
102
|
-
} catch (second) {
|
|
103
|
-
second.attempts = [
|
|
104
|
-
{ attempt: 1, code: firstError.code, diagnostic: firstError.diagnostic },
|
|
105
|
-
{ attempt: 2, code: second.code, diagnostic: second.diagnostic },
|
|
106
|
-
];
|
|
107
|
-
throw second;
|
|
108
|
-
}
|
|
109
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
110
|
-
if (Array.isArray(value.limitations)) value.limitations.push(CORRECTION_LIMITATION);
|
|
111
|
-
}
|
|
112
|
-
return JSON.stringify(value);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
83
|
return async function invoke(request) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
84
|
+
// Snapshot these before an async budget/progress hook can mutate the request.
|
|
85
|
+
const original = { instructions: request.instructions, input: request.input, signal: request.signal };
|
|
86
|
+
const contract = modelContract(original.input, original.instructions);
|
|
87
|
+
const correctionLimit = correctJson ? maxJsonCorrections : 0;
|
|
88
|
+
const attempts = [];
|
|
89
|
+
for (let attempt = 1; ; attempt++) {
|
|
90
|
+
let result;
|
|
91
|
+
try {
|
|
92
|
+
result = await once({ ...original, instructions: original.instructions +
|
|
93
|
+
(attempt > 1 ? JSON_CORRECTION_SUFFIX + ' 纠正次数:' + (attempt - 1) + '/' + correctionLimit + '。' : '') }, contract);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
attempts.push({ attempt, code: error?.code, diagnostic: error?.diagnostic });
|
|
96
|
+
const invalidJson = invalidJsonErrors.delete(error);
|
|
97
|
+
if (!invalidJson || attempt > correctionLimit) {
|
|
98
|
+
if (attempts.length > 1 && error && typeof error === 'object' && Object.isExtensible(error)) {
|
|
99
|
+
error.attempts = attempts;
|
|
100
|
+
if (invalidJson) {
|
|
101
|
+
error.message = 'Model response is not complete JSON after ' + (attempt - 1) +
|
|
102
|
+
' explicit JSON correction(s) (' + attempt + ' model calls); correction budget exhausted; no malformed output accepted';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
original.signal?.throwIfAborted();
|
|
108
|
+
// Exactly one reservation per extra call; a thrown budget/cancellation veto is final.
|
|
109
|
+
await request.onProtocolCorrection?.({
|
|
110
|
+
code: error.code, diagnostic: error.diagnostic, attempts: [...attempts],
|
|
111
|
+
action: 'explicit-json-correction', attempt: attempt + 1,
|
|
112
|
+
correction: attempt, maxJsonCorrections: correctionLimit,
|
|
113
|
+
notice: 'Additional model response requested; original output not repaired or accepted.',
|
|
114
|
+
});
|
|
115
|
+
original.signal?.throwIfAborted();
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (attempt > 1) {
|
|
119
|
+
// Out-of-band audit only: never inject metadata into stage-specific model schemas.
|
|
120
|
+
await request.onProtocolEvent?.({
|
|
121
|
+
action: 'json-correction-complete', attempt, corrections: attempt - 1,
|
|
122
|
+
attempts: [...attempts, { attempt, diagnostic: result.diagnostic }],
|
|
123
|
+
notice: 'Fresh complete JSON accepted; malformed outputs discarded; extra model calls occurred.',
|
|
124
|
+
});
|
|
125
|
+
original.signal?.throwIfAborted();
|
|
126
|
+
}
|
|
127
|
+
return result.text;
|
|
122
128
|
}
|
|
123
129
|
};
|
|
124
130
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Git working-copy comparison scopes: whole working tree (default), staged index or unstaged edits.
|
|
2
|
+
export const CHANGE_SCOPES = ['worktree', 'staged', 'unstaged'];
|
|
3
|
+
|
|
4
|
+
/** Normalize the requested scope; only the three documented values are accepted. */
|
|
5
|
+
export function changeScope(value) {
|
|
6
|
+
if (value === undefined) return 'worktree';
|
|
7
|
+
if (typeof value !== 'string' || !CHANGE_SCOPES.includes(value)) throw new Error('Invalid changeScope; expected worktree, staged or unstaged');
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Model-facing note: what the two captured sides actually are for this scope. */
|
|
12
|
+
export const CHANGE_SCOPE_NOTICE = {
|
|
13
|
+
staged: '本次比较为 Git 暂存区(HEAD → index):变更文件左侧为 HEAD 内容,右侧为已暂存内容;工作区未暂存修改不作为变更正文发送。显式上下文、自动依赖、调用方候选与规则文件按工作区当前内容读取,可能包含未暂存修改。',
|
|
14
|
+
unstaged: '本次比较为 Git 未暂存改动(index → 工作区):左侧为已暂存内容,仅作旧侧基线;右侧为工作区文件。已暂存内容不代表由本次未暂存改动引入。',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Preview/report label: disclose the exact comparison and its exclusions. */
|
|
18
|
+
export const CHANGE_SCOPE_LABEL = {
|
|
19
|
+
staged: 'Git 暂存区(HEAD → index),不包含工作区未暂存修改;上下文与规则文件按工作区读取',
|
|
20
|
+
unstaged: 'Git 未暂存改动(index → 工作区),已暂存内容仅作旧侧基线',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Preview notice: tell the user what will be sent before confirmation. */
|
|
24
|
+
export const CHANGE_SCOPE_PREVIEW_NOTICE = {
|
|
25
|
+
staged: '本次审查范围是 Git 暂存区(HEAD → index):只发送已暂存变更内容,工作区未暂存修改不作为变更正文;如选择上下文、自动依赖、调用方或规则文件,它们按工作区当前内容读取。',
|
|
26
|
+
unstaged: '本次审查范围是 Git 未暂存改动(index → 工作区):已暂存内容只作为旧侧基线。',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function changeScopeNotice(scope) {
|
|
30
|
+
return CHANGE_SCOPE_NOTICE[scope];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function changeScopeLabel(scope) {
|
|
34
|
+
return CHANGE_SCOPE_LABEL[scope];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function changeScopePreviewNotice(scope) {
|
|
38
|
+
return CHANGE_SCOPE_PREVIEW_NOTICE[scope];
|
|
39
|
+
}
|