@feng0531/dsh-code-review 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,9 +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.59.0 — SVN 复制新增文件与移动两侧覆盖
6
+
7
+ 支持同仓库、固定 copyfrom revision 的复制新增文件,旧侧明确为空目标,完整审查工作侧正文与属性;预览披露来源 URL/revision/UUID,采集末尾复核来源身份。移动作为目标新增与原路径删除分别覆盖,不冒充已验证重命名。不读复制来源正文,也不增加网络访问。凭据来源拒绝;复制替换与目录复制继续阻断,避免把来源 BASE 冒充目标旧节点。
8
+
9
+ ## 0.58.1 — SVN 根目录属性预览修复
10
+
11
+ 修复 SVN 合并产生根目录 mergeinfo 属性时,默认预览和指定文件预览均报 Invalid relation file 的问题。保留根目录属性变更和覆盖统计,仅允许有目录身份的根节点 `.`,普通路径穿越仍拒绝;上下文竞争条目保留目录身份。独立 SVN 三文件变更与根 mergeinfo 场景验证预览零模型发送和工作副本状态不变。全面 SVN 扩展另按支持矩阵分批推进,尚未宣称历史区间或所有特殊状态已支持。456 项串行回归、宿主、隔离安装和四组 CI 通过;原产物/npm/Release 116,254 字节逐字节一致,双哈希、清单与冻结发布说明通过。
12
+
5
13
  ## 0.58.0 — 本地评测入口可靠性整理
6
14
 
7
- 新增无需 Docker 的实验性 ReviewBench 本地入口与使用说明。要求显式本地仓库、模型与端点;复用严格 JSON 校验,拒绝截断和夹杂文本。未完成审查、不可映射的旧侧发现及超限结果明确失败并保存原报告,不伪装为空发现;同一行的不同问题不再误合并。mock 输出与可评分候选隔离,运行结果不覆盖。正常 DSH 工具行为、权限与预算不变;不代表真实模型质量已得到验证。
15
+ 新增无需 Docker 的实验性 ReviewBench 本地入口与使用说明。要求显式本地仓库、模型与端点;复用严格 JSON 校验,拒绝截断和夹杂文本。未完成审查、不可映射的旧侧发现及超限结果明确失败并保存原报告,不伪装为空发现;同一行的不同问题不再误合并。mock 输出与可评分候选隔离,运行结果不覆盖。正常 DSH 工具行为、权限与预算不变;不代表真实模型质量已得到验证。454 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原产物 113,697 字节逐字节一致,双哈希、清单与冻结发布说明核验通过。
8
16
 
9
17
  ## 0.57.0 — 对齐结论与边界收尾
10
18
 
@@ -25,14 +25,16 @@
25
25
 
26
26
  - 继承、原型链与动态分派:需要语言解析器或类型检查器做语义绑定,超出「只读批准快照、不放宽预算」的保守边界。
27
27
  - 完整语义调用方、别名与跨目录实例上下文:需要语义解析器;保守的命名/默认/命名空间成员/类实例方法导航已交付,其余不猜测。
28
- - SVN 历史区间等旧侧独有来源:历史/旧侧三层链、oldContextPaths、删除旧正文复用已交付;SVN 历史区间按设计不支持。
28
+ - SVN 全面支持扩展:用户已要求将工作副本特殊状态与固定 revision 历史审查纳入新目标。当前历史/旧侧三层链、oldContextPaths 和删除旧正文复用仅是 Git 已交付能力,不能算作 SVN 支持。SVN 当前缺口、批次与验收见 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md)。
29
29
  - 真实模型质量评测:目标明确后置,保存真实输出、人工裁决、分开统计,不作为对齐前提。
30
30
 
31
31
  各版交付结论见「当前验证结论」与 [版本变化](CHANGELOG.md)。
32
32
 
33
33
  ## 当前验证结论
34
34
 
35
- - 最近完整回归:449 项通过。
35
+ - 最近完整回归:456 项通过。
36
+ - v0.58.1 已交付 SVN 根目录 mergeinfo 属性预览修复及全面支持矩阵。456 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 116,254 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明核验通过。SVN 全面扩展目标仍在进行,历史与特殊状态尚未全部支持。
37
+ - v0.58.0 已正式交付实验性本地评测入口及可靠性整理。454 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 113,697 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明核验通过。正常 DSH 审查行为与预算不变;尚无真实模型基准评分。
36
38
  - v0.57.0 已正式交付对齐结论与边界收尾。449 项回归与四组 CI 通过;npm、Release 与原包 108,451 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
37
39
  - v0.56.0 已正式交付依赖清单与配置文件专用指导。449 项回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 108,081 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
38
40
  - v0.55.0 已正式交付模板引擎与基础设施即代码专用指导。446 项回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 106,969 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
package/README.md CHANGED
@@ -89,7 +89,7 @@ commit 与 baseRevision/targetRevision 不能同时使用。oldContextPaths 只
89
89
  - 不执行项目代码、构建脚本或模型生成的 shell 命令。模型只能在批准的快照中查找、搜索和读取。
90
90
  - 常见凭据路径会被排除,但普通源码也可能含秘密。路径过滤不能代替人工检查。
91
91
  - 二进制、不能解码的文件、链接和超限内容会被阻断,不会自动截断后假装完成。
92
- - SVN 的复制、替换、切换目录、外部工作副本、冲突及部分属性状态不受支持。含 svn:keywords 的文件也会被阻断,以免把关键字展开当作代码变化。
92
+ - SVN 同仓库复制新增文件以空目标旧侧审查,copyfrom URL/revision/UUID 在预览中披露,仅作来源信息,不自动读取来源正文,也不认定为已验证重命名。文件移动的新增/删除两侧分别覆盖。复制目录、替换、切换目录、外部工作副本、冲突及部分属性状态仍未支持;含 svn:keywords 的文件仍阻断。全面扩展进度见 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md)。
93
93
 
94
94
  ## 默认规模限制
95
95
 
package/RELEASE-NOTES.md CHANGED
@@ -1,16 +1,16 @@
1
- # v0.58.0 发布说明
1
+ # v0.59.0 发布说明
2
2
 
3
- ## 本地评测入口可靠性整理
3
+ ## SVN 复制新增文件与移动两侧覆盖
4
4
 
5
- 新增无需 Docker 的实验性 ReviewBench 本地评测入口和说明,复用现有审查核心。配置、模型调用、结果映射分开测试;模型和端点显式指定,不改变 DSH 宿主配置。
5
+ 支持同仓库复制新增文件,目标旧侧明确为空;完整审查工作侧正文和属性,不将复制来源的 BASE 当作目标旧版本。
6
6
 
7
- - 复用严格 JSON 校验,拒绝截断响应和夹杂文字;HTTP 错误不输出正文。
8
- - 未完成审查、无法表达为 head 行号的发现以及超限结果明确失败,保存完整原报告,不静默丢弃或假装成功。
9
- - 同一行的不同问题保留,只去除完全相同的导出记录;旧结果不会被覆盖。
10
- - mock 只生成冒烟文件,不生成可评分候选。
7
+ - 预览披露 copyfrom URL、revision、仓库 UUID;采集末尾复核复制身份。
8
+ - 文件移动的原路径删除与目标新增分别计入覆盖,不推定已验证重命名。
9
+ - 不读取复制来源正文,不新增网络访问;凭据路径来源继续拒绝。
10
+ - 复制后修改、来源缺失和仓库离线均有独立测试。
11
11
 
12
- ## 使用与边界
12
+ ## 当前限制
13
13
 
14
- 正常 DSH 插件工具、权限和预算保持不变。实验入口采用单次直审,不启用检索、复核或业务分组;其结果不能代表完整插件配置。尚未完成真实模型、真实基准 PR 或 judge 评分验证。
14
+ 复制替换、目录复制及目录后代覆盖尚未实现;特别是复制替换的 `svn cat -r BASE` 可能返回来源内容,不能当作目标旧版本。关键字、冲突、固定 revision 历史审查等继续按 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md) 推进,本版不宣称全面 SVN 支持已经完成。
15
15
 
16
- 本地入口说明见 [本地评测指南](evaluation/REVIEWBENCH-LOCAL.md),插件日常使用见 [README](README.md)。
16
+ 日常使用见 [README](README.md)。
@@ -0,0 +1,43 @@
1
+ # SVN 审查支持矩阵与实施顺序
2
+
3
+ 本矩阵按用户新的「全面支持 SVN」要求扩展目标。此前把 SVN 历史审查列为设计不支持,不再作为此任务的完成理由。支持状态须由实际测试确认,不以规划、发布数量或版本号代替验收。
4
+
5
+ ## 不变的约束
6
+
7
+ - 不对用户工作副本执行 checkout/update/switch/merge/revert/cleanup/commit;集成测试在独立本地仓库和临时工作副本运行。
8
+ - 预览不发送源码,执行必须绑定同一预览、模型、工作目录与快照身份。
9
+ - 本地工作副本模式默认离线;新增远端历史读取必须显式选择并在预览中披露网络访问和来源。
10
+ - 历史来源固定仓库 UUID、规范 URL、peg revision、operative revision、节点路径与内容哈希;禁止把较新工作树当作历史正文。
11
+ - 文件/字节/上下文/精确探测/调用/超时预算继续共享。超限、不可解码、不可定位和权限失败明确保留未完成状态。
12
+
13
+ ## 当前盘点(不是全部支持)
14
+
15
+ | 场景 | 当前证据/限制 | 实施批次 |
16
+ |---|---|---|
17
+ | 常规文件修改、普通新增、删除 | 已有 BASE/工作字节与属性捕获测试 | 维持并扩充组合验收 |
18
+ | 子目录属性变更 | 已有独立属性审查测试 | 第一批 |
19
+ | 根目录属性/mergeinfo | v0.58.1 已发布;独立复现后修复,指定三文件和默认预览均通过、0 模型发送、状态不变;456 项串行回归、宿主、隔离安装和四矩阵 CI 通过,原产物/npm/Release 116,254 字节逐字节一致及双哈希/清单/发布说明通过 | 第一批先发布此修复 |
20
+ | 复制/移动、copyfrom 及目录结构变更 | 本地开发已支持同仓库复制新增文件及文件移动的新增/删除独立覆盖:新增目标旧侧为空,copyfrom 只作来源信息,预览披露并冻结身份;离线与凭据来源拒绝测试通过。复制目录及后代覆盖仍未实现,待本批发布核验 | 第二批:明确旧侧来源和后代覆盖 |
21
+ | 替换、删除再新增、文件/目录类型变化 | 当前 replaced 被拒绝 | 第二批:区分 BASE 旧节点与目标节点 |
22
+ | 缺失、文本/属性/树冲突、阻塞 | 当前多数仅统一 blocked | 第三批:明确状态诊断;不能合成不存在的工作侧或声称审核通过 |
23
+ | svn:keywords、svn:eol-style | keywords 当前阻断;换行保留原文但无规范化映射 | 第三批:双侧可验证映射,保留原字节和定位 |
24
+ | svn:special/链接 | 当前阻断,不跟随链接取正文 | 第三批:如支持只审链接目标数据;不沿链接读取文件 |
25
+ | switched、externals、稀疏/混合 revision | 多数拒绝;调用方/依赖链要求祖先 normal | 第四批:明确仓库边界与独立来源,禁止隐式跨仓读取 |
26
+ | 固定 revision 单提交 | 未实现;Git commit 参数不能冒充 SVN revision | 第五批:新增明确 SVN 参数与冻结身份 |
27
+ | 固定 revision 区间 | 未实现;不能通过临时重建工作树冒充原生历史能力 | 第五批:固定树端点与逐路径差异 |
28
+ | 分支复制、分支间比较、合并来源 | mergeinfo 当前仅作为属性文本;不证明源码归属或被合并提交均已审查 | 第六批:URL/peg/operative revision 与 mergeinfo 导航披露 |
29
+ | 当前三层依赖与限定目录调用方 | 已有安全 normal 节点的离线有界测试;根属性修改可能使祖先检查拒绝,需单独验证 | 第四批补祖先/混合状态组合 |
30
+ | 历史两侧上下文与调用方 | 未实现 | 第六批:逐侧固定版本,不能回退至工作副本 |
31
+ | 真正 r7314→r7315 样本 | 用户未提供隔离副本路径;本地类似复现不是该样本验收 | 获得路径后只读生成预览,不自动发送模型 |
32
+
33
+ ## 每批发布门禁
34
+
35
+ 1. 独立测试副本复现,确认 capture→关系推断→预览→执行的覆盖和拒绝路径。
36
+ 2. 专项、全量串行回归、实际宿主、隔离安装通过。
37
+ 3. 提交/推送/冻结版本标签;四矩阵 CI 通过后唯一 Actions 发布。
38
+ 4. 保存原始 Actions 产物,核验 npm/Release 逐字节、SHA-256、SHA-512、清单与冻结发布说明。
39
+ 5. 写入该批验证结论;更新本矩阵,剩余项继续保留未完成。
40
+
41
+ ## 收尾标准
42
+
43
+ 每行均有明确支持模式、失败处理、源码位置和测试证据。诊断未完成不等于场景已经可审查;任何保守拒绝须单列原因,不能把拒绝范围自动从目标中删除。真实模型质量另行评测,不由采集/发布测试推断。
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
7
7
  "main": "./index.mjs",
8
8
  "exports": { ".": "./index.mjs", "./package.json": "./package.json", "./locale/*.json": "./locale/*.json" },
9
- "files": ["scripts/review-bench-adapter.mjs", "evaluation/REVIEWBENCH-LOCAL.md", "index.mjs", "cordis.patch.yml", "src/", "locale/", "README.md", "CHANGELOG.md", "RELEASE-NOTES.md", "DEVELOPMENT.md", "CORE-ALIGNMENT-TASKS.md", "CORE-SOURCE-INVENTORY.md", "LICENSE", "LICENSES/"],
9
+ "files": ["scripts/review-bench-adapter.mjs", "evaluation/REVIEWBENCH-LOCAL.md", "index.mjs", "cordis.patch.yml", "src/", "locale/", "README.md", "CHANGELOG.md", "RELEASE-NOTES.md", "DEVELOPMENT.md", "CORE-ALIGNMENT-TASKS.md", "CORE-SOURCE-INVENTORY.md", "SVN-SUPPORT-MATRIX.md", "LICENSE", "LICENSES/"],
10
10
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
11
11
  "repository": { "type": "git", "url": "https://github.com/f-e-n-g-0531/dsh-code-review.git" },
12
12
  "type": "module",
@@ -22,7 +22,7 @@ export function inferContextRelations(input, file) {
22
22
  const primary = { ...file, id: 'primary' };
23
23
  const entries = contexts.map((c, i) => ({ id: 'context-' + i, path: c.path, eligibility: 'reviewable', left: { text: '' }, right: { text: c.text } }));
24
24
  // Other selected paths participate as opaque competitors, not context.
25
- const competitors = input.files.filter(f => f.id !== file.id).map((f, i) => ({ id: 'selected-' + i, path: f.path, eligibility: 'blocked' }));
25
+ const competitors = input.files.filter(f => f.id !== file.id).map((f, i) => ({ id: 'selected-' + i, path: f.path, ...(f.kind === 'dir' ? {kind: 'dir'} : {}), eligibility: 'blocked' }));
26
26
  const all = [primary, ...entries, ...competitors];
27
27
  if (all.length > 200) return [];
28
28
  const byId = new Map(all.map(f => [f.id, f.path]));
@@ -5,7 +5,7 @@ export function inferCppRelations(files) {
5
5
  if (!Array.isArray(files) || files.length > 200) throw new Error('Invalid C++ relation input');
6
6
  const byPath = new Map(), ids = new Set();
7
7
  for (const file of files) {
8
- if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.includes(':') || file.path.startsWith('/') || file.path.split('/').some(s => !s || s === '.' || s === '..') || byPath.has(file.path) || !['reviewable','excluded','blocked'].includes(file.eligibility)) throw new Error('Invalid C++ relation file');
8
+ if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.includes(':') || file.path.startsWith('/') || (!(file.path === '.' && file.kind === 'dir') && file.path.split('/').some(s => !s || s === '.' || s === '..')) || byPath.has(file.path) || !['reviewable','excluded','blocked'].includes(file.eligibility)) throw new Error('Invalid C++ relation file');
9
9
  ids.add(file.id); byPath.set(file.path,file);
10
10
  }
11
11
  const edges = [];
@@ -11,7 +11,7 @@ export function inferTestRelations(files) {
11
11
  if (!Array.isArray(files) || files.length > 200) throw new Error('Invalid relation input');
12
12
  const paths = new Set(), ids = new Set(), implementations = new Map(), tests = [];
13
13
  for (const file of files) {
14
- if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.split('/').some(p => !p || p === '.' || p === '..') || paths.has(file.path) || !['reviewable', 'blocked', 'excluded'].includes(file.eligibility)) throw new Error('Invalid relation file');
14
+ if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || (!(file.path === '.' && file.kind === 'dir') && file.path.split('/').some(p => !p || p === '.' || p === '..')) || paths.has(file.path) || !['reviewable', 'blocked', 'excluded'].includes(file.eligibility)) throw new Error('Invalid relation file');
15
15
  ids.add(file.id); paths.add(file.path);
16
16
  if (file.eligibility !== 'reviewable') continue;
17
17
  const match = /^(.*)\.(mjs|cjs|js|jsx|ts|tsx)$/.exec(file.path);
@@ -12,7 +12,7 @@ export function inferImportRelations(files) {
12
12
  if (!Array.isArray(files) || files.length > 200) throw new Error('Invalid import relation input');
13
13
  const byPath = new Map(), ids = new Set();
14
14
  for (const file of files) {
15
- if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.startsWith('/') || file.path.includes(':') || file.path.split('/').some(p => !p || p === '.' || p === '..') || byPath.has(file.path) || !['reviewable', 'blocked', 'excluded'].includes(file.eligibility)) throw new Error('Invalid import relation file');
15
+ if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.startsWith('/') || file.path.includes(':') || (!(file.path === '.' && file.kind === 'dir') && file.path.split('/').some(p => !p || p === '.' || p === '..')) || byPath.has(file.path) || !['reviewable', 'blocked', 'excluded'].includes(file.eligibility)) throw new Error('Invalid import relation file');
16
16
  ids.add(file.id); byPath.set(file.path, file);
17
17
  }
18
18
  const edges = [];
package/src/service.mjs CHANGED
@@ -49,7 +49,7 @@ export function createReviewService(llm, { capture = captureSnapshot, now = Date
49
49
  while (previews.size >= maxPreviews) previews.delete(previews.keys().next().value);
50
50
  const previewId = randomUUID();
51
51
  previews.set(previewId, { ...current, target, options, rounds, snapshotId: snapshot.id, expires: now() + ttlMs });
52
- return { ...(options.businessRequirement!==undefined?{businessRequirement:{text:options.businessRequirement,hash:hash(options.businessRequirement),bytes:Buffer.byteLength(options.businessRequirement)}}:{}), previewId, plan, snapshotId: snapshot.id, repositoryRoot: snapshot.root, vcs: snapshot.vcs, ...(snapshot.history ? { history: snapshot.history } : {}), model: current.route, modelSendingEnabled: allowModelSending, files: snapshot.files.map(f => ({ path: f.path, eligibility: f.eligibility, reason: f.reason })), contextPaths: snapshot.context.map(c => c.path), ...(options.oldContextPaths !== undefined ? {oldContextPaths:options.oldContextPaths,oldContextSources:snapshot.context.filter(c=>c.oldOnly).map(c=>({path:c.path,side:'context-old',revision:c.oldRevision,blobOid:c.oldBlobOid,hash:c.oldHash}))} : {}), ...(snapshot.callerDiscovery ? {callerDiscovery:snapshot.callerDiscovery} : {}), ...(snapshot.autoContext ? { autoContext: snapshot.autoContext } : {}), rules: (snapshot.rules ?? []).map(({ path, hash }) => ({ path, hash })), notice: (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + '执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多3轮检索,共享每文件120秒和总模型调用100次预算;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。' };
52
+ return { ...(options.businessRequirement!==undefined?{businessRequirement:{text:options.businessRequirement,hash:hash(options.businessRequirement),bytes:Buffer.byteLength(options.businessRequirement)}}:{}), previewId, plan, snapshotId: snapshot.id, repositoryRoot: snapshot.root, vcs: snapshot.vcs, ...(snapshot.history ? { history: snapshot.history } : {}), model: current.route, modelSendingEnabled: allowModelSending, files: snapshot.files.map(f => ({ path: f.path, eligibility: f.eligibility, reason: f.reason, ...(f.copySource ? {copySource:f.copySource,copyNotice:f.copyNotice} : {}) })), contextPaths: snapshot.context.map(c => c.path), ...(options.oldContextPaths !== undefined ? {oldContextPaths:options.oldContextPaths,oldContextSources:snapshot.context.filter(c=>c.oldOnly).map(c=>({path:c.path,side:'context-old',revision:c.oldRevision,blobOid:c.oldBlobOid,hash:c.oldHash}))} : {}), ...(snapshot.callerDiscovery ? {callerDiscovery:snapshot.callerDiscovery} : {}), ...(snapshot.autoContext ? { autoContext: snapshot.autoContext } : {}), rules: (snapshot.rules ?? []).map(({ path, hash }) => ({ path, hash })), notice: (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + '执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多3轮检索,共享每文件120秒和总模型调用100次预算;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。' };
53
53
  } finally { busy = false; }
54
54
  },
55
55
  async execute(args, exec) {
package/src/svn.mjs CHANGED
@@ -38,7 +38,7 @@ export async function captureSvn(cwd, options = {}) {
38
38
  signal?.throwIfAborted();
39
39
  const name = entry['@_path'].replaceAll('\\', '/');
40
40
  const state = entry['wc-status'], status = state['@_item'];
41
- const item = { id: hash(name), path: name, rawStatus: status, baseRevision: state['@_revision'], eligibility: 'reviewable', properties: [] };
41
+ const item = { id: hash(name), path: name, ...(name === '.' ? {kind: 'dir'} : {}), rawStatus: status, baseRevision: state['@_revision'], eligibility: 'reviewable', properties: [] };
42
42
  files.push(item);
43
43
  if (isSecretPath(name)) { item.eligibility='excluded'; item.reason='Credential path excluded'; continue; }
44
44
  if ((selectedPaths && !selectedPaths.includes(name)) || (status === 'unversioned' && !selectedPaths?.includes(name))) {
@@ -47,14 +47,22 @@ export async function captureSvn(cwd, options = {}) {
47
47
  try {
48
48
  if (name !== '.') relativePath(name);
49
49
  if (!['modified', 'added', 'deleted', 'normal', 'unversioned'].includes(status) || state['@_props'] === 'conflicted' || state['@_tree-conflicted'] === 'true') throw new Error('Unsupported or conflicted SVN state: ' + status);
50
- if (state['@_copied'] === 'true' || state['@_switched'] === 'true' || state['@_file-external'] === 'true') throw new Error('Copy, switched or external node requires explicit historical support');
50
+ if (state['@_switched'] === 'true' || state['@_file-external'] === 'true') throw new Error('Switched or external node requires explicit historical support');
51
+ if (state['@_copied'] === 'true' && status !== 'added') throw new Error('Copied replacement requires verified target baseline');
51
52
  const hasBase = !['added', 'unversioned'].includes(status);
52
53
  let kind;
53
54
  if (status === 'unversioned') kind = (await lstat(path.join(root, name))).isDirectory() ? 'dir' : 'file';
54
55
  else {
55
56
  const node = xml(await svn(root, ['info', '--xml', '--', target(name)], options)).info.entry[0];
56
57
  kind = node['@_kind'];
57
- if (node['wc-info']?.['copy-from-url']) throw new Error('Copied node baseline is not supported');
58
+ if (state['@_copied'] === 'true' || node['wc-info']?.['copy-from-url']) {
59
+ const source = node['wc-info']?.['copy-from-url'], revision = node['wc-info']?.['copy-from-rev'];
60
+ if (status !== 'added' || kind !== 'file' || typeof source !== 'string' || !source || !/^[0-9]+$/.test(String(revision)) || node.repository?.uuid !== info.repository.uuid || node.repository?.root !== info.repository.root) throw new Error('Copied node requires a same-repository added file with fixed copy source');
61
+ if (isSecretPath(decodeURIComponent(new URL(source).pathname))) throw new Error('Credential copy source excluded');
62
+ item.copySource = { url: source, revision: String(revision), repositoryUuid: node.repository.uuid };
63
+ item.copyNotice = 'Target addition is reviewed against an empty target baseline; copy source is provenance only, not an approved source body or verified rename.';
64
+ item.copyNodeIdentity = hash(JSON.stringify([node.url, node['@_kind'], node['@_revision'], node['wc-info']?.schedule, node['wc-info']?.['wcroot-abspath'], source, String(revision), node.repository]));
65
+ }
58
66
  }
59
67
  item.kind = kind;
60
68
  item.rightExists = status !== 'deleted';
@@ -82,6 +90,11 @@ export async function captureSvn(cwd, options = {}) {
82
90
  const after = await svn(root, statusArgs, options);
83
91
  if (!isDeepStrictEqual(xml(after), xml(before))) throw new Error('Working copy changed during capture');
84
92
  for (const item of files.filter(f => f.eligibility === 'reviewable')) {
93
+ if (item.copySource) {
94
+ const node = xml(await svn(root, ['info', '--xml', '--', target(item.path)], options)).info.entry[0];
95
+ const identity = hash(JSON.stringify([node.url, node['@_kind'], node['@_revision'], node['wc-info']?.schedule, node['wc-info']?.['wcroot-abspath'], node['wc-info']?.['copy-from-url'], String(node['wc-info']?.['copy-from-rev']), node.repository]));
96
+ if (identity !== item.copyNodeIdentity) throw new Error('Copy source identity changed during capture');
97
+ }
85
98
  if (item.kind === 'file' && item.rawStatus !== 'deleted' && (await readLocal(root, item.path, options)).hash !== item.right.hash) throw new Error('Content changed during capture');
86
99
  const left = !['added', 'unversioned'].includes(item.rawStatus) ? await properties(root, item.path, true, options) : {};
87
100
  const right = ['deleted', 'unversioned'].includes(item.rawStatus) ? {} : await properties(root, item.path, false, options);