@feng0531/dsh-code-review 0.79.0 → 0.80.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -1
- package/CORE-ALIGNMENT-TASKS.md +19 -81
- package/CORE-SOURCE-INVENTORY.md +1 -1
- package/DEVELOPMENT.md +3 -2
- package/README.md +31 -5
- package/RELEASE-NOTES.md +9 -8
- package/SVN-RELEASE-EVIDENCE.md +114 -0
- package/SVN-SUPPORT-MATRIX.md +38 -119
- package/evaluation/README.md +1 -1
- package/index.mjs +1 -1
- package/package.json +2 -2
- package/src/caller-candidates.mjs +1 -1
- package/src/context-candidates.mjs +1 -1
- package/src/context-identity.mjs +8 -6
- package/src/file-relations.mjs +2 -1
- package/src/process.mjs +5 -1
- package/src/review.mjs +2 -0
- package/src/service.mjs +3 -2
- package/src/snapshot.mjs +10 -1
- package/src/svn-capture-budget.mjs +6 -0
- package/src/svn-dependency-chain.mjs +1 -1
- package/src/svn-diagnostics.mjs +16 -0
- package/src/svn-effective-mergeinfo.mjs +46 -0
- package/src/svn-history-callers.mjs +5 -5
- package/src/svn-history-dependencies.mjs +4 -3
- package/src/svn-history-endpoints.mjs +15 -0
- package/src/svn-history-lookup.mjs +4 -4
- package/src/svn-history.mjs +85 -46
- package/src/svn-keyword-config.mjs +10 -0
- package/src/svn-keyword-mapping.mjs +32 -15
- package/src/svn-replacement-source.mjs +25 -0
- package/src/svn-replacement-tree.mjs +66 -0
- package/src/svn-special.mjs +10 -0
- package/src/svn-url.mjs +8 -0
- package/src/svn-working-identity.mjs +4 -3
- package/src/svn-working-mergeinfo.mjs +31 -0
- package/src/svn-working-scope.mjs +7 -0
- package/src/svn-working-special.mjs +24 -0
- package/src/svn.mjs +56 -19
package/SVN-SUPPORT-MATRIX.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# SVN
|
|
1
|
+
# SVN 支持范围与验收依据
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本页区分可以审查的模式、需要额外授权的读取,以及只能诊断的状态。预览成功不等于审查完成;测试存在、版本数量或发布成功均不证明任意SVN状态可审查。日常操作先看 [README](README.md)。
|
|
4
4
|
|
|
5
5
|
## 不变的约束
|
|
6
6
|
|
|
@@ -10,122 +10,52 @@
|
|
|
10
10
|
- 历史来源固定仓库 UUID、规范 URL、peg revision、operative revision、节点路径与内容哈希;禁止把较新工作树当作历史正文。
|
|
11
11
|
- 文件/字节/上下文/精确探测/调用/超时预算继续共享。超限、不可解码、不可定位和权限失败明确保留未完成状态。
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 版本与验收状态
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| 常规文件修改、普通新增、删除 | 已有 BASE/工作字节与属性捕获测试 | 维持并扩充组合验收 |
|
|
18
|
-
| 子目录属性变更 | 已有独立属性审查测试 | 第一批 |
|
|
19
|
-
| 根目录属性/mergeinfo | v0.58.1 已发布;独立复现后修复,指定三文件和默认预览均通过、0 模型发送、状态不变;456 项串行回归、宿主、隔离安装和四矩阵 CI 通过,原产物/npm/Release 116,254 字节逐字节一致及双哈希/清单/发布说明通过 | 第一批先发布此修复 |
|
|
20
|
-
| 复制/移动、copyfrom 及目录结构变更 | v0.59.0 已发布同仓库复制新增文件及文件移动的新增/删除独立覆盖:新增目标旧侧为空,copyfrom 只作来源信息,预览披露并冻结身份;离线与凭据来源拒绝测试通过。459 项串行回归、宿主、隔离安装、四矩阵 CI 通过;原产物/npm/Release 117,098 字节逐字节一致,双哈希/清单/冻结发布说明通过。v0.61.0 已发布同仓库目录复制后代展开与继承来源身份,目标旧侧为空且预览披露。465 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 120,159 字节、双哈希/清单/冻结发布说明通过。复制替换仍未支持 | 第二批:明确旧侧来源和后代覆盖 |
|
|
21
|
-
| 普通新增/删除目录及后代 | v0.60.0 已发布:补齐默认 status 隐藏的删除后代,离线递归枚举与末尾清单复核;普通结构目录只在后代全选且可审查时开放,凭据/阻断子项导致目录覆盖不足。461 项串行回归、宿主、隔离安装与四矩阵 CI 通过;原产物/npm/Release 118,284 字节逐字节一致,双哈希/清单/冻结发布说明通过 | 第二批普通目录子批 |
|
|
22
|
-
| 替换、删除再新增、文件/目录类型变化 | 普通与复制替换分别用 SVN_REPLACEMENT/SVN_COPIED_REPLACEMENT 诊断;离线探测确认前者 BASE 无 pristine,后者可能返回复制来源,禁止伪造旧侧。已新增未接入采集的 candidate-only 完整 diff 重建组件及真实离线测试:匹配完整工作侧,拒绝片段、错侧、截断与超限。diff 仍不能独立证明旧节点/编码/旧属性,因此未提交工作副本真实双侧替换仍未支持,不能把辅助组件算作支持完成。v0.64.1 已发布已提交固定历史版本的同路径 deleted/added 合并,真实目标旧/新正文与属性通过测试;481 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 128,279 字节、双哈希/清单/发布说明通过。v0.65.0 已发布已提交固定历史文件/目录类型替换:分侧 kind/正文/属性、预览披露和目录后代覆盖;482 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 128,792 字节、双哈希/清单/发布说明通过。未提交替换仍拒绝 | 第二批:区分 BASE 旧节点与目标节点 |
|
|
23
|
-
| 缺失、文本/属性/树冲突、阻塞 | v0.60.1 已发布稳定状态诊断:SVN_MISSING_NODE/OBSTRUCTED_NODE/INCOMPLETE_NODE/TEXT_CONFLICT/PROPERTY_CONFLICT/TREE_CONFLICT 等,预览与报告透传 blockCode。463 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 119,462 字节、双哈希/清单/发布说明通过。诊断不算真实双侧支持,仍需后续实现 | 第三批:明确状态诊断;不能合成不存在的工作侧或声称审核通过 |
|
|
24
|
-
| svn:keywords、svn:eol-style | v0.70.0 已发布标准关键字行保持映射,BASE收缩与cat --ignore-keywords核验、原正文及hash保留、预览披露与模型原行号证据通过;不以规范化正文代替证据或证明行为等价。自定义/固定宽度/未闭合展开仍阻断;504项串行全量回归、宿主/隔离安装、四矩阵CI通过;原包/npm/Release 137,678字节、双哈希/清单/发布说明核验通过。本地新增 EOL 元数据映射:保留原文/原hash与LF视图hash、CRLF/LF/CR计数,CRLF/LF行号identity,单CR明确不等同LF证据行号;预览与模型原文集成测试通过,已补充 UTF-16 字节/文本哈希区分、关键字组合、属性移除、非法样式明确失败与状态不变验证;专项3项、根目录预览/关键字/EOL组合7项、实际ASAR宿主及隔离安装通过,v0.71.0 已发布:507项串行全量回归与冻结版本65项SVN专项通过(0失败/跳过),四矩阵标签CI及发布CI通过,原始Actions/npm/Release 139,804字节逐字节一致,双哈希/清单/冻结发布说明核验通过。不以LF视图替换证据或证明行为等价 | 第三批:双侧可验证映射,保留原字节和定位 |
|
|
25
|
-
| svn:special/链接 | 当前阻断,不跟随链接取正文 | 第三批:如支持只审链接目标数据;不沿链接读取文件 |
|
|
26
|
-
| switched、externals、稀疏/混合 revision | 多数拒绝;调用方/依赖链要求祖先 normal | 第四批:明确仓库边界与独立来源,禁止隐式跨仓读取 |
|
|
27
|
-
| 固定 revision 单提交 | v0.62.0 已发布 svnRevision:r-1/r 固定对象、UUID/URL/peg/operative revision 逐侧绑定,正文不读工作树;远端显式 svnAllowNetwork。472 项串行回归、宿主、隔离安装、四矩阵 CI 及原包/npm/Release 125,486 字节、双哈希/清单/发布说明通过。历史上下文/规则/调用方已由下方 v0.63–0.69 行跟踪,不再沿用 v0.62 当时缺口 | 第五批:新增明确 SVN 参数与冻结身份 |
|
|
28
|
-
| 固定 revision 区间 | v0.62.0 已发布 svnBaseRevision/svnTargetRevision,固定树端点 summary+逐侧 cat/proplist,目录后代与属性纳入预算,不建临时工作树;同上述完整发布验证。历史上下文见下方 v0.63–0.69;分支间不同 URL 仍未支持 | 第五批:固定树端点与逐路径差异 |
|
|
29
|
-
| 分支复制、分支间比较、合并来源 | v0.66.0 已发布批准历史 mergeinfo 属性的来源路径/区间导航,保留非继承标记、有界不展开 revision、不读取分支源码;invalid/limited 明确且不裁剪。485 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 129,866 字节、双哈希/清单/发布说明通过;v0.67.0 已发布工作副本 BASE/工作侧属性的同一有界导航及预览/报告披露;489 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 130,866 字节、双哈希/清单/发布说明通过。有效继承 mergeinfo、跨 URL 比较、源码归属/合并提交覆盖仍未支持 | 第六批:URL/peg/operative revision 与 mergeinfo 导航披露 |
|
|
30
|
-
| 当前三层依赖与限定目录调用方 | v0.66.1 已发布根目录仅 mergeinfo 变化下的自动依赖/限定调用方修复:根目录不作源码候选,祖先双侧属性仅 mergeinfo 差异可通过且入指纹;其他属性/unsafe 状态仍拒绝。488 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 130,642 字节、双哈希/清单/发布说明通过。其他祖先/混合状态继续待验证 | 第四批补祖先/混合状态组合 |
|
|
31
|
-
| 历史两侧上下文与调用方 | v0.63.0 已发布显式 contextPaths 固定双侧来源及目标 rulePaths,UUID/URL/peg/operative revision/原字节与正文 hash 验证,禁止脏工作树与排除项回流。477 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 127,355 字节、双哈希/清单/发布说明通过。v0.64.0 已发布 oldContextPaths 旧侧独有正文、SVN 来源专用校验、预览披露与选中删除身份复用;479 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 127,879 字节、双哈希/清单/发布说明通过;本地开发了注入固定版本 lookup/read 能力的三层历史依赖导航核心,逐侧共享512探测/20候选/1MiB来源与上下文名额;排除项、旧侧限定和规则隔离测试通过。v0.68.0 已发布三层逐侧历史 autoContext,固定父目录有界清单 lookup,清单成功证明缺失、访问失败直接传播,真实旧/新版本链和脏工作树隔离通过。496 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 134,056 字节、双哈希/清单/发布说明通过;本地新增逐侧固定 revision 调用方核心能力接口:共享256索引/128扫描/1MiB正文/512侧探测,命中侧才捕获正文,规则/变更排除与old-only限制保留,同侧命名调用声明导航测试通过;本地已接入固定 SVN 显式目录递归清单/同侧正文、统一30秒/512命令/元数据/正文预算,caller 与依赖共享精确探测及来源上限,真实集成专项13项通过;v0.69.0 已发布历史限定调用方和autoContext组合;502项串行全量回归、组合模型输入隔离、宿主/隔离安装、四矩阵 CI 通过。原包/npm/Release 136,030 字节、双哈希/清单/发布说明通过;字面发现仍不证明完整调用覆盖 | 第六批:逐侧固定版本,不能回退至工作副本 |
|
|
32
|
-
| 真正 r7314→r7315 样本 | 用户未提供隔离副本路径;本地类似复现不是该样本验收 | 获得路径后只读生成预览,不自动发送模型 |
|
|
33
|
-
|
|
34
|
-
## 下一批:简单自定义revision关键字(v0.79.0 待发布)
|
|
35
|
-
|
|
36
|
-
- 只支持ASCII字母起始字母数字名称=单个%r,不覆盖标准名称,不接收重复名/复杂格式/其他占位符;原行保持收缩逻辑及BASE核验保留,customKeywords元数据披露。
|
|
37
|
-
- `test/svn-custom-keywords.test.mjs` 真实离线Build=%r展开、BASE核验、显式来源与变更原文通过;配置拒绝边界与既有标准/显式/依赖组合6项通过。521项串行全量回归、6项关键字组合、宿主/隔离安装通过,尚未发布。
|
|
38
|
-
- 其他自定义格式、固定宽度、特殊状态与分支来源等仍未完成,不把窄子集当作该矩阵行全闭环。
|
|
39
|
-
|
|
40
|
-
## 标准关键字限定调用方(v0.78.0 已发布)
|
|
41
|
-
|
|
42
|
-
冻结提交 `deb0c91f6216a0637d43b44fd3dfc8194d306940`;标签CI `38735681006`,唯一发布运行 `38736546649`,原工件 `11974502757`。原始Actions/npm/Release包151,916字节逐字节一致;SHA-256 `efe0ef2a6b23ed5d63e8a92252f139cced0d8f4d59ddc8bc3bcb3ba1d39cf04c`,SHA-512/清单/冻结发布说明核验通过。
|
|
43
|
-
|
|
44
|
-
- 调用方扫描复用captureSvnExplicitContext标准关键字核验,捕获原展开上下文,只保留命中正文;所有扫描身份/哈希复核。BASE两份核验进入原共享来源账本,sourceBytes与scannedBytes分别披露。
|
|
45
|
-
- `test/svn-caller-keywords.test.mjs` 离线真实复现:命中展开原文、非命中正文不进入快照/模型、来源计费、revision保留、预览0发送与稳定ID;既有调用方/预算/EOL组合4项通过,首次全量518通过/1失败:未改动Git历史链测试超时;保持原限制独立复跑5/5通过,重新全量519/519通过,原失败记录保留;宿主/隔离安装及四矩阵标签/发布CI通过,正式发布工件核验完成。
|
|
46
|
-
- 自定义/固定宽度/不完整展开或读取失败仍不能当作负搜索证据。特殊状态、有效继承mergeinfo与跨URL缺口继续。
|
|
47
|
-
|
|
48
|
-
## 自动依赖标准关键字(v0.77.0 已发布)
|
|
49
|
-
|
|
50
|
-
冻结提交 `fc482b55f81f44b354eb91f829f12a37fb9c89b0`;标签CI `38728355698`,唯一发布运行 `38729326841`,原工件 `11969183899`。原始Actions/npm/Release包151,247字节逐字节一致;SHA-256 `1a654d83860f74cf0709169e0a348e72a79c9ae46969e0639678f870008cb2cb`,SHA-512/清单/冻结发布说明核验通过。
|
|
51
|
-
|
|
52
|
-
- 本地依赖链复用已验证的显式来源捕获,工作/BASE展开/未展开全部计费,共享1MiB/30秒;原始展开为唯一正文证据,三层/20名额/512探测保持。
|
|
53
|
-
- 捕获后按同一允许关键字策略复核身份;显式入口元数据复用,不把节点允许策略泄漏给调用方扫描。
|
|
54
|
-
- `test/svn-auto-keywords.test.mjs` 真实离线链复现:展开原文、BASE计费、后续依赖、unsupported候选阻断、预览0发送与模型原文/稳定ID;既有显式/依赖/预算10项通过。518项串行全量回归、10项显式/依赖/预算组合、离线链专项及宿主/隔离安装、四矩阵标签及发布CI通过,正式发布工件核验完成。
|
|
55
|
-
- 限定调用方标准关键字、自定义/固定宽度与其余SVN缺口仍未完成。
|
|
56
|
-
|
|
57
|
-
## 审查发现修复:本地组合共享预算(v0.76.3 已发布)
|
|
58
|
-
|
|
59
|
-
冻结提交 `7794dcd0b2b9eaee66c3a2d71ee3e416b803e00c`;标签CI `38724071757`,唯一发布运行 `38724938443`,原工件 `11968546653`。原始Actions/npm/Release包150,607字节逐字节一致;SHA-256 `aa25450df36c068567b1da3984a09d2af6a7f86cebb75cb9a3318eaba87150d4`,SHA-512/清单/冻结发布正文核验通过。
|
|
60
|
-
|
|
61
|
-
- 修复前真实组合contextPaths+callerScopePaths可读取1,140,102字节,超过1MiB但成功。修复后snapshot入口创建一个source账本/30秒signal,各本地导航阶段共同扣费,关键字BASE两份核验正文亦计费。
|
|
62
|
-
- `test/svn-navigation-budget.test.mjs` 覆盖精确1MiB边界/越界/取消和真实显式+非命中扫描组合、autoContext入口+扫描组合越界拒绝,小请求正常;与关键字/EOL组合4项通过。517项串行全量回归、4项预算/关键字/EOL组合、宿主/隔离安装、四矩阵标签和发布CI通过,正式发布工件核验完成。
|
|
63
|
-
- 共享预算错误在依赖链直接传播,不伪装成成功;保留原有单阶段核验/文件/字节限制。本批未新增特殊SVN状态或自动关键字支持。
|
|
64
|
-
|
|
65
|
-
## 审查发现修复:普通变更来源身份(v0.76.2 已发布)
|
|
66
|
-
|
|
67
|
-
冻结提交 `34ded1840536406fd454ab120b977ec6aa97abf7`;标签CI `38719902365`,唯一发布运行 `38720759455`,原工件 `11967005142`。原始Actions/npm/Release包149,274字节逐字节一致;SHA-256 `e7702aa23e01146c440d4e3bdf4820332d5a7e6eb5f1747ab66d854a01f33cbf`,SHA-512/清单/冻结发布说明核验通过。
|
|
15
|
+
冻结提交 `eb4b4ee10c9b4264d561287af7de9e0671945919`;最终四矩阵标签CI `38756170907` 全部成功,唯一发布运行 `38757421980`,原工件 `11984565729`。Actions原包/npm/Release包167,327字节逐字节一致;SHA-256 `12e43c2d562d0c811e4f98ae25829a17f52715e662885a45fef46fd3eec3e3a3`,SHA-512、清单、冻结发布正文核验通过。发布任务实际只运行release,复用前述同提交四矩阵证据;release耗时2分39秒。v0.80.0因Ubuntu原生链接测试夹具commit失败未发布,Windows两组通过,原标签保留未移动;修正原生Unix符号链接夹具后v0.80.1四矩阵通过。
|
|
68
16
|
|
|
69
|
-
v0.
|
|
17
|
+
历史v0.80.1的冻结提交已通过四矩阵及工件核验,但随后代码审查发现替换树祖先授权和多行关键字两项缺陷。当前v0.80.2集中修复这两项;相关回归已通过,完整回归与正式发布尚在验收中。下表描述支持模式和边界,不把历史测试数用作安全承诺。
|
|
70
18
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
## BUG修复:根级上下文祖先检查(v0.75.1 已发布)
|
|
86
|
-
|
|
87
|
-
冻结提交 `55b796432d1847e337f08f66f1b1be356dac3eff`;标签CI `38710236827`,唯一发布运行 `38711005747`,原工件 `11960911950`。原始Actions/npm/Release包146,408字节逐字节一致;SHA-256 `77ff46e66cc61191a8df4a829b9f540139fde3557199812b2086f0865433c00c`,SHA-512/清单/冻结发布说明通过。
|
|
88
|
-
|
|
89
|
-
- 新真实离线复现证明:根目录直属文件在根mergeinfo变化后节点指纹不变,`svnCallerNode` 跳过了父目录为根的检查。修复前测试明确失败。
|
|
90
|
-
- `svnCallerAncestors` 支持严格根目录scope,避免重复根节点;`svnCallerNode` 始终核验父目录并把祖先指纹纳入节点身份。普通相对路径限制不放宽。
|
|
91
|
-
- `test/svn-root-context-safety.test.mjs` 修复后通过:根mergeinfo改变指纹、已捕获上下文verify拒绝、其他根属性与externals明确阻断;与优先根预览/mergeinfo祖先/显式上下文组合7项通过。513项串行全量回归、7项根级安全/优先预览组合与宿主/隔离安装、四矩阵标签及发布CI通过,正式发布工件核验完成。
|
|
92
|
-
- 本批纯BUG修复使用v0.75.1,不当作新增能力;其余SVN缺口保留。
|
|
93
|
-
|
|
94
|
-
## 独立显式本地上下文(v0.75.0 已发布)
|
|
95
|
-
|
|
96
|
-
冻结提交 `d08cb354709a2fb2051a7769b853cb98ae1619cd`;标签CI `38706343061`,唯一发布运行 `38707025423`,原工件 `11958951726`。原始Actions/npm/Release包145,650字节逐字节一致;SHA-256 `7d251aa209a7430c8b72a018472cc6d5d2e1e24ea9282623f19b5fd58896917e`,SHA-512/清单/冻结发布正文核验通过。
|
|
97
|
-
|
|
98
|
-
- `src/svn-explicit-context.mjs` 在无autoContext模式核验未变更显式SVN节点与属性/URL/祖先身份,末尾复核;20名额/1MiB正文/30秒含核验,离线只读。
|
|
99
|
-
- `snapshot.mjs` 复用显式捕获正文,已选可审查变更复用工作侧EOL;排除/删除/关键字/special等不回流。预览沿用contextEolMappings披露,不显示原文。
|
|
100
|
-
- `test/svn-explicit-context.test.mjs` 真实离线复现:CRLF元数据、稳定身份、0预览发送、status不变、关键字拒绝、未跟踪排除、属性变化及取消拒绝;与既有SVN采集组合16项通过。512项串行全量回归、16项既有采集组合及最终3项安全组合、实际宿主/隔离安装、四矩阵标签及发布CI通过,正式发布工件核验完成。
|
|
101
|
-
- 工作副本关键字、特殊状态、跨URL与有效继承mergeinfo仍未完成。
|
|
19
|
+
| 能力 | 本地集中验收证据 | 边界与未证明事项 |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| CI证据复用 | 拒绝错仓库/提交/标签/attempt、缺组/重复/失败/缺npm test步骤;真实旧运行API校验通过 | 首次新流程已端到端通过;同冻结提交四矩阵证据校验后仅release运行,原包核验保持 |
|
|
22
|
+
| 固定宽度/复合关键字 | 真实SVN固定字段及复合格式BASE收缩核验通过,原文/行号保留 | 原生%_表示空格;引号属性未按预期展开,明确拒绝,不模拟shell。未知格式/不完整展开拒绝 |
|
|
23
|
+
| 历史链接记录 | 真实special→普通文件转换、仓库原记录、目标内容隔离通过 | 仅link记录,不跟随目标;特殊侧不能当源码上下文 |
|
|
24
|
+
| 本地链接记录 | 原生符号链接readlink、普通link记录、目标文件不读取通过 | 当前主机及最终Windows/Ubuntu四矩阵均通过。链接权限失败不能跳过算支持 |
|
|
25
|
+
| 有效继承mergeinfo | 最近显式祖先、空属性、不可继承*、历史逐侧及工作BASE/工作侧复现通过 | 边界外未知、混合BASE、不安全祖先返回unknown;无合并归属/提交覆盖推断 |
|
|
26
|
+
| 跨URL固定历史 | 同revision不同分支真实复现、上下文来源与侧缓存隔离通过;summary顺序误报已修复,历史组合11/11通过 | 同仓库根相对端点;跨仓库/重叠嵌套根拒绝,分支来源不构成merge归属 |
|
|
27
|
+
| 全采集预算 | 命令/元数据/取消边界及导航超限组合通过;统一全项目545/545通过 | 额外采集与末尾复核共享原上限,超限直接失败,不保证任意规模可审查 |
|
|
28
|
+
| 稀疏/混合revision | depth身份披露、真实--depth files checkout及混合BASE不同旧正文集成用例通过;原结构目录回归通过 | 不声称稀疏全树覆盖,非完整结构目录明确诊断,缺项不等于删除 |
|
|
29
|
+
| 独立历史基线替换树(祖先修复需v0.80.2) | 文件/目录类型、复制替换目标旧正文、old-only/new-only后代及父项独选阻断真实复现通过 | 用户显式基线不证明等于原丢失BASE;需独立远端授权;每个后代独立批准 |
|
|
30
|
+
| URL来源身份 | 编码分隔符/凭据/控制字符拒绝与合法转义等价用例、普通复制/中文路径回归通过 | 最终四矩阵通过;不允许任意URL或隐式跨仓库 |
|
|
31
|
+
| 不可恢复或未独立授权状态 | missing/冲突/阻塞/incomplete/switched/externals分类、拒绝来源回流已有回归 | 仅诊断不是可审查支持;不自动修复、更新、跟随externals或选择替代来源 |
|
|
102
32
|
|
|
103
|
-
|
|
33
|
+
新增能力的复现测试和实际运行记录属于机制验收,不代表已覆盖所有属性、平台和状态组合。v0.80.2回归特别检查:不安全祖先下的子项不得读取正文;安全但未选择的父项不影响独立子项;祖先属性变化进入快照身份;LF/CRLF关键字和XML数字引用正确处理。
|
|
104
34
|
|
|
105
|
-
冻结提交 `09241e046eaeff4f9c917102bc9dad97d91e81d9`;标签CI `38702380795`,唯一发布运行 `38703089522`,原工件 `11957397597`。原始Actions/npm/Release包144,494字节逐字节一致;SHA-256 `35946d5d536482cb8fd7061a9a291c030c49a564e9eeffacac82ecf206e37552`,SHA-512、清单及冻结正文核验通过。
|
|
106
35
|
|
|
107
|
-
|
|
108
|
-
- 真实复现发现带已提交属性的调用方被索引 none-only 条件漏选;改为 none/normal,正文仍经既有关键字/special/externals/URL/祖先安全检查。modified/conflicted 等不放开。
|
|
109
|
-
- `test/svn-working-context-eol.test.mjs` 离线真实仓库组合通过:入口→依赖和限定调用方映射、非匹配正文不捕获、预览0发送、模型原文、status不变、快照稳定及属性变化拒绝。既有关键字拒绝7项依赖专项通过;511项串行全量回归与11项本地导航安全专项、实际宿主/隔离安装、四矩阵标签及发布CI通过,发布工件核验完成。
|
|
110
|
-
- 独立无autoContext的显式本地上下文未因本批新增属性映射;工作副本关键字上下文与其余矩阵缺口继续保留。
|
|
36
|
+
## 使用时的拒绝条件
|
|
111
37
|
|
|
112
|
-
|
|
38
|
+
上表是当前版本结论;下文旧版验收仅为历史记录,不能作为当前缺口重复计数。
|
|
113
39
|
|
|
114
|
-
|
|
40
|
+
| 请求项 | 当前模式 | 结论 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| 工作副本状态/目录属性 | 正常变更、属性、结构后代;冲突/缺失/阻塞/incomplete明确诊断 | 正常模式可审查;不可确认目标绝不视为通过 |
|
|
43
|
+
| 复制/移动 | 空目标旧侧+copyfrom来源披露,新增/删除独立覆盖 | 不把来源信息当旧目标正文,不宣称语义重命名 |
|
|
44
|
+
| 未提交替换/类型变化 | 显式固定目标URL基线及逐后代树比较 | 需用户批准;无法证明原丢失BASE,默认本地不可恢复状态拒绝 |
|
|
45
|
+
| 关键字/换行 | 标准/固定宽度/原生复合格式,BASE核验、原文/hash/行号 | 引号/未知格式明确拒绝;不把收缩当行为等价 |
|
|
46
|
+
| 链接 | 本地readlink/普通记录/固定历史记录 | 仅审目标字符串,不跟随,不转为源码导航 |
|
|
47
|
+
| 固定revision单提交/区间 | 逐侧UUID/URL/peg/operative revision固定 | 不读取脏工作树替代历史 |
|
|
48
|
+
| 分支与合并来源 | 同仓库不同URL固定侧、有效继承mergeinfo | 不推断merge归属或完整提交覆盖;未知边界明确unknown |
|
|
49
|
+
| 同版本上下文/依赖/调用方 | 显式/old-only/三层依赖/限定目录字面发现 | 同侧来源,不回退另一版本;非完整语义调用图 |
|
|
50
|
+
| 稀疏/混合revision | 正常可用节点分别BASE,明确稀疏及非完整清单 | 真实稀疏checkout/混合BASE复现通过;不推断缺项删除或全树覆盖 |
|
|
51
|
+
| switched/externals | 独立仓库根或固定分支端点 | 不自动跟随,不隐式跨仓库;未授权仍阻断 |
|
|
52
|
+
| 用户真实r7314→r7315副本 | 未提供隔离路径 | 无法进行该私有样本验收;通用本地复现不冒充该样本成功 |
|
|
115
53
|
|
|
116
|
-
- 固定历史 readSource 把EOL元数据绑定各侧source,显式/旧侧独有/自动依赖/命中调用方均保留;不新增读取命令或放大预算。
|
|
117
|
-
- 预览 contextSources 列出实际捕获来源,不显示正文;context-identity 校验映射与该侧原文一致,拒绝篡改。
|
|
118
|
-
- `test/svn-history-context-eol.test.mjs` 真实仓库复现通过:四类上下文组合、旧侧不回流、未匹配正文不捕获、脏工作树隔离、0预览发送、模型原文、status不变与快照稳定。510项串行全量回归、最终66项上下文/检索兼容与9项历史组合、实际宿主与隔离安装、四矩阵标签和发布CI通过,正式发布及工件核验完成。
|
|
119
|
-
- 工作副本关键字上下文、未提交替换、跨URL与有效继承mergeinfo等缺口仍未完成。
|
|
120
54
|
|
|
121
|
-
##
|
|
55
|
+
## 历史证据
|
|
122
56
|
|
|
123
|
-
|
|
57
|
+
旧版本哈希与当时的限制单独保存在 [历史发布证据](SVN-RELEASE-EVIDENCE.md)。已发布事实不改写;发现的新问题以当前补丁说明为准,不把过去测试通过说成不存在缺陷。
|
|
124
58
|
|
|
125
|
-
- `src/svn-history.mjs` 对已有固定逐侧正文和属性计算 EOL 元数据,不新增 SVN 命令,不改变 UUID/URL/peg/operative revision 与原始字节哈希。
|
|
126
|
-
- 缺失侧与目录侧明确 null;真实 SVN 测试覆盖 CRLF→LF、CR、新增/删除、文件→目录替换、单revision/区间一致、脏工作树隔离、预览0发送、模型原文及status不变。
|
|
127
|
-
- `test/svn-history-eol.test.mjs` 与 EOL/历史替换组合7项通过;完整串行回归509项通过,历史导航/EOL/替换组合12项通过,实际宿主及隔离安装、四矩阵标签CI与发布CI通过,正式发布及工件核验完成。历史上下文 EOL 元数据仍未新增。
|
|
128
|
-
- README 重写为实际使用指南;`test/readme-usage.test.mjs` 验证 JSON 示例参数与真实工具定义一致,文档链接测试通过。
|
|
129
59
|
|
|
130
60
|
## 实现与复现入口索引
|
|
131
61
|
|
|
@@ -145,20 +75,9 @@ v0.76.1 标签CI `38718993310` 的Windows两组因新测试比较 `~` 与 `%7E`
|
|
|
145
75
|
| 显式 mergeinfo 来源导航 | `src/svn-mergeinfo.mjs` | `test/svn-mergeinfo.test.mjs` |
|
|
146
76
|
| 本地依赖/调用方及 mergeinfo 祖先 | `src/svn-dependency-chain.mjs`、`src/svn-caller-capture.mjs` | `test/svn-dependency-chain.test.mjs`、`test/svn-caller-capture.test.mjs`、`test/svn-mergeinfo-ancestor.test.mjs` |
|
|
147
77
|
| 历史两侧上下文/依赖/调用方 | `src/svn-history.mjs`、`src/svn-history-dependencies.mjs`、`src/svn-history-callers.mjs` | `test/svn-history.test.mjs`、`test/svn-history-dependencies.test.mjs`、`test/svn-history-callers-integration.test.mjs` |
|
|
148
|
-
|
|
|
78
|
+
| 链接记录/稀疏混合/有效继承/跨分支 | `src/svn-special.mjs`、`src/svn-working-scope.mjs`、`src/svn-effective-mergeinfo.mjs`、`src/svn-history.mjs` | `test/svn-special-history.test.mjs`、`test/svn-real-working-scope.test.mjs`、`test/svn-working-effective-mergeinfo.test.mjs`、`test/svn-cross-branches.test.mjs`;边界见上表 |
|
|
79
|
+
| 替换祖先安全与多行关键字修复 | `src/svn-replacement-tree.mjs`、`src/svn-keyword-config.mjs`、`src/svn.mjs` | `test/svn-review-fixes.test.mjs` |
|
|
149
80
|
|
|
150
|
-
## EOL 子批验收索引(v0.71.0 已发布)
|
|
151
|
-
|
|
152
|
-
冻结提交 `9e9ffc8db9c9467a54366c17b6d6a6449524e795`;标签CI运行 `38690842208`,唯一发布运行 `38691445215`,原始工件 `11952831232`。包139,804字节;SHA-256 `2e99d5c73ef194be0dc542d19b5a69bfc850e548a1347f6d5ef6aec124ec0f96`。原始Actions包、npm、Release逐字节一致,SHA-512、SHA256SUMS及冻结发布正文通过。此验证不代表自动升级当前宿主或完整SVN支持。
|
|
153
|
-
|
|
154
|
-
| 项目 | 实现/复现证据 | 当前结论 |
|
|
155
|
-
|---|---|---|
|
|
156
|
-
| CRLF/LF/CR 与混合计数、视图哈希 | `src/svn-eol-mapping.mjs`;`test/svn-eol-mapping.test.mjs` 前两项 | 专项通过;原文/原字节哈希不替换;单 CR 不保证 LF 行映射 |
|
|
157
|
-
| 编码身份 | 同上 UTF-16 BOM 案例 | 文本 hash 与 source bytes hash 明确区分,不能互换 |
|
|
158
|
-
| 预览/模型输入及只读 | `src/svn.mjs`、`src/service.mjs`;真实 SVN EOL 集成案例 | 预览不发送;受控模型读取原文;文件与解析后 status 不变 |
|
|
159
|
-
| 组合与失败 | 同一集成案例中标准关键字+CRLF、移除属性、强制非法样式 | 组合通过;非法样式被 SVN 在 status 阶段拒绝,采集明确失败 |
|
|
160
|
-
| 根目录优先修复持续回归 | `test/svn-root-preview.test.mjs` 默认/指定三文件预览 | 本批重新实跑通过,0模型发送,状态不变;原 v0.58.1 交付不重发 |
|
|
161
|
-
| 历史、上下文、特殊状态 | 不在本子批新增范围 | 仍按上表未完成项推进,不由元数据功能推断支持 |
|
|
162
81
|
|
|
163
82
|
## 每批发布门禁
|
|
164
83
|
|
package/evaluation/README.md
CHANGED
package/index.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export function apply(ctx, config = {}) {
|
|
|
12
12
|
};
|
|
13
13
|
ctx.tools.register({
|
|
14
14
|
name: 'code_review_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. Preview each repository separately. Show the preview before execution.',
|
|
15
|
-
parameters: { type: 'object', additionalProperties: false, properties: { 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 } } },
|
|
15
|
+
parameters: { type: 'object', additionalProperties: false, properties: { 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 } } },
|
|
16
16
|
output,
|
|
17
17
|
async execute(args, exec) { const preview = await service.preview(args, exec); return { json: JSON.stringify(preview), markdown: '' }; },
|
|
18
18
|
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feng0531/dsh-code-review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.2",
|
|
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", "SVN-SUPPORT-MATRIX.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", "SVN-RELEASE-EVIDENCE.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",
|
|
@@ -34,7 +34,7 @@ export function callerCandidates(files, scanned, resolutionPaths, {includeBindin
|
|
|
34
34
|
if(file.path==='.'&&file.kind==='dir'){changed.add(file.path);continue;}
|
|
35
35
|
relativePath(file.path); changed.add(file.path);
|
|
36
36
|
if (file.oldPath) { relativePath(file.oldPath); changed.add(file.oldPath); }
|
|
37
|
-
if (file.eligibility === 'reviewable' && file.rightExists !== false && !isSecretPath(file.path)) targets.add(file.path);
|
|
37
|
+
if (file.eligibility === 'reviewable' && !file.specialData && file.rightExists !== false && !isSecretPath(file.path)) targets.add(file.path);
|
|
38
38
|
}
|
|
39
39
|
const candidates = [], probes = new Set(), seen = new Set();
|
|
40
40
|
for (const source of scanned) {
|
|
@@ -8,7 +8,7 @@ export function contextCandidates(files, trackedPaths, {maxCandidates=20,onProbe
|
|
|
8
8
|
const found=new Map();const add=(p,from,reason)=>{if(!tracked.has(p)||(!includeChanged&&changed.has(p)))return;if(!found.has(p))found.set(p,{path:p,fromPaths:[],reasons:[],confidence:'navigation-only'});const v=found.get(p);if(!v.fromPaths.includes(from))v.fromPaths.push(from);if(!v.reasons.includes(reason))v.reasons.push(reason);};
|
|
9
9
|
for(const f of files){
|
|
10
10
|
if(f.path==='.'&&f.kind==='dir')continue; // Root properties have no source dependency syntax.
|
|
11
|
-
relativePath(f.path);if(f.eligibility!=='reviewable'||f.rightExists===false)continue;
|
|
11
|
+
relativePath(f.path);if(f.eligibility!=='reviewable'||f.specialData||f.rightExists===false)continue;
|
|
12
12
|
const text=f.right?.text;if(typeof text!=='string'||Buffer.byteLength(text)>256*1024)continue;
|
|
13
13
|
if(text.includes('/*')||text.includes(String.fromCharCode(96))||text.includes('R"')||/\\\r?\n/.test(text))continue;
|
|
14
14
|
if(f.path.endsWith('.cs')&&!/["'\x60#]|\/\/|\busing\b|\bdynamic\b/.test(text)){
|
package/src/context-identity.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {sameSvnUrl} from './svn-url.mjs';
|
|
1
2
|
import { hash, relativePath } from './content.mjs';
|
|
2
3
|
import { assertNonSecretPath } from './secret-path.mjs';
|
|
3
4
|
import {svnEolMapping} from './svn-eol-mapping.mjs';
|
|
@@ -5,9 +6,10 @@ import {isDeepStrictEqual} from 'node:util';
|
|
|
5
6
|
|
|
6
7
|
export const CONTEXT_SIDE_NOTICE = 'context.text属于批准当前或target侧;oldText/oldRevision与context-old属于baseline旧侧。oldOnly:true表示只捕获旧侧,没有target正文,不证明目标路径不存在。不得互换版本或用旧侧上下文证明新侧行为;没有证据须保留uncertain。';
|
|
7
8
|
|
|
8
|
-
function verifySvnSource(source, history, name, revision, text) {
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
function verifySvnSource(source, history, name, revision, text, side) {
|
|
10
|
+
const root=side==='old'?history.baseUrl??history.url:history.targetUrl??history.url;
|
|
11
|
+
const expected=root?.replace(/\/$/,'')+'/'+name.split('/').map(encodeURIComponent).join('/');
|
|
12
|
+
if(!source||source.repositoryUuid!==history.repositoryUuid||!sameSvnUrl(source.url,expected)||source.pegRevision!==revision||source.operativeRevision!==revision||!/^[0-9a-f]{64}$/.test(source.hash??'')||source.textHash!==hash(text))throw new Error('Invalid SVN historical context provenance');
|
|
11
13
|
if(source.eolMapping!==undefined&&!isDeepStrictEqual(source.eolMapping,svnEolMapping(text,source.eolMapping?.style)))throw new Error('Invalid SVN historical context EOL metadata');
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -18,7 +20,7 @@ export function contextSources(item, history) {
|
|
|
18
20
|
if (item.oldOnly !== undefined && item.oldOnly !== true) throw new Error('Invalid old-only context marker');
|
|
19
21
|
if (item.oldOnly === true && history?.vcs === 'svn') {
|
|
20
22
|
if(typeof item.oldText!=='string'||item.oldRevision!==history.base||item.oldHash!==hash(item.oldText)||['text','revision','hash','svnSource','oldBlobOid'].some(key=>item[key]!==undefined))throw new Error('Invalid SVN old-only historical context provenance');
|
|
21
|
-
verifySvnSource(item.oldSvnSource,history,item.path,history.base,item.oldText);
|
|
23
|
+
verifySvnSource(item.oldSvnSource,history,item.path,history.base,item.oldText,'old');
|
|
22
24
|
return [{side:'context-old',text:item.oldText,revision:item.oldRevision,hash:item.oldHash,svnSource:structuredClone(item.oldSvnSource)}];
|
|
23
25
|
}
|
|
24
26
|
if (item.oldOnly === true) {
|
|
@@ -26,12 +28,12 @@ export function contextSources(item, history) {
|
|
|
26
28
|
return [{side:'context-old',text:item.oldText,revision:item.oldRevision,blobOid:item.oldBlobOid,hash:item.oldHash}];
|
|
27
29
|
}
|
|
28
30
|
if (history?.vcs === 'svn') {
|
|
29
|
-
const verify = (source, revision, text) => verifySvnSource(source,history,item.path,revision,text);
|
|
31
|
+
const verify = (source, revision, text, side) => verifySvnSource(source,history,item.path,revision,text,side);
|
|
30
32
|
if (item.revision !== history.target || typeof item.text !== 'string') throw new Error('Invalid SVN historical context provenance');
|
|
31
33
|
verify(item.svnSource, history.target, item.text);
|
|
32
34
|
if (item.oldText !== undefined) {
|
|
33
35
|
if (item.oldRevision !== history.base || typeof item.oldText !== 'string') throw new Error('Invalid SVN historical context provenance');
|
|
34
|
-
verify(item.oldSvnSource, history.base, item.oldText);
|
|
36
|
+
verify(item.oldSvnSource, history.base, item.oldText,'old');
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
if (typeof item.text !== 'string') throw new Error('Missing current context text');
|
package/src/file-relations.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { inferImportRelations } from './import-relations.mjs';
|
|
|
3
3
|
|
|
4
4
|
export function inferFileRelations(files) {
|
|
5
5
|
// Keep the existing group edge budget; naming hints have priority at the cap.
|
|
6
|
-
|
|
6
|
+
const sources=files.filter(file=>!file.specialData);
|
|
7
|
+
return [...inferTestRelations(sources), ...inferImportRelations(sources), ...inferCppRelations(sources)].slice(0, 2000);
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
// Conservative naming hints only. No filesystem access or dependency claims.
|
package/src/process.mjs
CHANGED
|
@@ -38,7 +38,11 @@ export function run(command, args, { cwd, signal, timeoutMs = 30_000, maxBytes =
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export async function checked(command, args, options) {
|
|
41
|
-
const
|
|
41
|
+
const budget=command==='svn'?options?.svnCaptureBudget:undefined;
|
|
42
|
+
budget?.reserve();
|
|
43
|
+
const signal=budget?(options?.signal?AbortSignal.any([options.signal,budget.signal]):budget.signal):options?.signal;
|
|
44
|
+
const result = await run(command, args, budget?{...options,signal}:options);
|
|
45
|
+
budget?.charge(result.stdout.length+result.stderr.length,args.includes('cat'));
|
|
42
46
|
if (result.code !== 0) throw new Error(command + ' failed (' + result.code + '): ' + result.stderr.toString('utf8').slice(0, 2000));
|
|
43
47
|
return result.stdout;
|
|
44
48
|
}
|
package/src/review.mjs
CHANGED
|
@@ -106,6 +106,8 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
106
106
|
}
|
|
107
107
|
if (input.history) report.history = structuredClone(input.history);
|
|
108
108
|
if (input.mergeSources) report.mergeSources = structuredClone(input.mergeSources);
|
|
109
|
+
if(input.workingScope)report.workingScope=structuredClone(input.workingScope);
|
|
110
|
+
if(input.effectiveMergeSources)report.effectiveMergeSources=structuredClone(input.effectiveMergeSources);
|
|
109
111
|
report.rules = (input.rules ?? []).map(({ path, hash }) => ({ path, hash }));
|
|
110
112
|
if (options.enableGrouping === true) report.grouping = buildReviewGroups(input.files, inferFileRelations(input.files));
|
|
111
113
|
report.interactions = [];
|
package/src/service.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {svnDiagnostic} from './svn-diagnostics.mjs';
|
|
1
2
|
import { reviewRounds as validateReviewRounds } from './review-rounds.mjs';
|
|
2
3
|
import { validateCallerScopes } from './caller-index.mjs';
|
|
3
4
|
import { validateBusinessRequirement } from './business-requirement.mjs';
|
|
@@ -30,7 +31,7 @@ export function createReviewService(llm, { capture = captureSnapshot, now = Date
|
|
|
30
31
|
exec = bind(exec);
|
|
31
32
|
exec.signal.throwIfAborted();
|
|
32
33
|
if (busy) throw new Error('Review service busy');
|
|
33
|
-
if (!args || Object.keys(args).some(k => !['repositoryPath', 'selectedPaths', 'contextPaths', 'rulePaths', 'autoContext', 'commit', 'baseRevision', 'targetRevision','businessRequirement','callerScopePaths','oldContextPaths','reviewRounds','svnRevision','svnBaseRevision','svnTargetRevision','svnAllowNetwork'].includes(k))) throw new Error('Unknown preview argument');
|
|
34
|
+
if (!args || Object.keys(args).some(k => !['repositoryPath', 'selectedPaths', 'contextPaths', 'rulePaths', 'autoContext', 'commit', 'baseRevision', 'targetRevision','businessRequirement','callerScopePaths','oldContextPaths','reviewRounds','svnRevision','svnBaseRevision','svnTargetRevision','svnAllowNetwork','svnEffectiveMergeinfo','svnBasePath','svnTargetPath','svnReplacementBaseRevision','svnReplacementAllowNetwork'].includes(k))) throw new Error('Unknown preview argument');
|
|
34
35
|
if (args.autoContext !== undefined && typeof args.autoContext !== 'boolean') throw new Error('Invalid autoContext option');
|
|
35
36
|
for (const key of ['selectedPaths', 'contextPaths', 'rulePaths', 'oldContextPaths']) if (args[key] !== undefined && (!Array.isArray(args[key]) || args[key].length > (key === 'rulePaths' ? 4 : key === 'oldContextPaths' ? 20 : 200) || args[key].some(p => typeof p !== 'string'))) throw new Error('Invalid path list');
|
|
36
37
|
const rounds=validateReviewRounds(args.reviewRounds === undefined ? reviewRounds : args.reviewRounds);
|
|
@@ -49,7 +50,7 @@ export function createReviewService(llm, { capture = captureSnapshot, now = Date
|
|
|
49
50
|
while (previews.size >= maxPreviews) previews.delete(previews.keys().next().value);
|
|
50
51
|
const previewId = randomUUID();
|
|
51
52
|
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 } : {}), ...(snapshot.mergeSources ? {mergeSources:snapshot.mergeSources} : {}), model: current.route, modelSendingEnabled: allowModelSending, files: snapshot.files.map(f => ({ path: f.path,...(f.workingSource?{workingSource:f.workingSource}:{}), eligibility: f.eligibility, reason: f.reason, ...(f.eolMapping?{eolMapping:f.eolMapping,textNotice:f.textNotice}:{}), ...(f.keywordMapping?{keywordMapping:f.keywordMapping,textNotice:f.textNotice}:{}), ...(f.rawStatus==='replaced'&&snapshot.history?.vcs==='svn'?{oldKind:f.oldKind,newKind:f.newKind,nodeNotice:f.nodeNotice}:{}), ...(f.blockCode ? {blockCode:f.blockCode} : {}), ...(f.copySource ? {copySource:f.copySource,copyNotice:f.copyNotice} : {}) })), contextPaths: snapshot.context.map(c => c.path), ...(snapshot.context.some(c=>c.keywordMapping)?{contextKeywordMappings:snapshot.context.filter(c=>c.keywordMapping).map(c=>({path:c.path,keywordMapping:c.keywordMapping}))}:{}), ...(snapshot.context.some(c=>c.eolMapping)?{contextEolMappings:snapshot.context.filter(c=>c.eolMapping).map(c=>({path:c.path,eolMapping:c.eolMapping}))}:{}), ...(snapshot.history?.vcs==='svn'?{contextSources:snapshot.context.map(c=>({path:c.path,...(c.svnSource?{new:c.svnSource}:{}),...(c.oldSvnSource?{old:c.oldSvnSource}:{})}))}:{}), ...(options.oldContextPaths !== undefined ? {oldContextPaths:options.oldContextPaths,oldContextSources:snapshot.context.filter(c=>c.oldOnly).map(c=>({path:c.path,side:'context-old',revision:c.oldRevision,...(c.oldSvnSource?{svnSource:c.oldSvnSource}:{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: (snapshot.history?.vcs==='svn'&&options.autoContext===true?'SVN历史autoContext为精确依赖定位读取固定版本父目录有界清单,包含同目录非匹配名字元数据;仅匹配捕获正文发送模型,不扫描全仓,不用工作树替代历史版本。':'') + (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + '执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多3轮检索,共享每文件120秒和总模型调用100次预算;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。' };
|
|
53
|
+
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.workingScope?{workingScope:snapshot.workingScope}:{}), ...(snapshot.history ? { history: snapshot.history } : {}), ...(snapshot.mergeSources ? {mergeSources:snapshot.mergeSources} : {}),...(snapshot.effectiveMergeSources?{effectiveMergeSources:snapshot.effectiveMergeSources}:{}), model: current.route, modelSendingEnabled: allowModelSending, files: snapshot.files.map(f => ({ path: f.path,...(f.workingSource?{workingSource:f.workingSource}:{}),...(f.replacementSource?{replacementSource:f.replacementSource,contentNotice:f.contentNotice}:{}), eligibility: f.eligibility, reason: f.reason,...(f.specialData?{specialData:f.specialData,contentNotice:f.contentNotice}:{}), ...(f.eolMapping?{eolMapping:f.eolMapping,textNotice:f.textNotice}:{}), ...(f.keywordMapping?{keywordMapping:f.keywordMapping,textNotice:f.textNotice}:{}), ...(f.rawStatus==='replaced'&&(snapshot.history?.vcs==='svn'||f.replacementSource)?{oldKind:f.oldKind,newKind:f.newKind,nodeNotice:f.nodeNotice}:{}), ...(f.blockCode ? {blockCode:f.blockCode,...(svnDiagnostic(f.blockCode)?{diagnostic:svnDiagnostic(f.blockCode)}:{})} : {}), ...(f.copySource ? {copySource:f.copySource,copyNotice:f.copyNotice} : {}) })), contextPaths: snapshot.context.map(c => c.path), ...(snapshot.context.some(c=>c.keywordMapping)?{contextKeywordMappings:snapshot.context.filter(c=>c.keywordMapping).map(c=>({path:c.path,keywordMapping:c.keywordMapping}))}:{}), ...(snapshot.context.some(c=>c.eolMapping)?{contextEolMappings:snapshot.context.filter(c=>c.eolMapping).map(c=>({path:c.path,eolMapping:c.eolMapping}))}:{}), ...(snapshot.history?.vcs==='svn'?{contextSources:snapshot.context.map(c=>({path:c.path,...(c.svnSource?{new:c.svnSource}:{}),...(c.oldSvnSource?{old:c.oldSvnSource}:{})}))}:{}), ...(options.oldContextPaths !== undefined ? {oldContextPaths:options.oldContextPaths,oldContextSources:snapshot.context.filter(c=>c.oldOnly).map(c=>({path:c.path,side:'context-old',revision:c.oldRevision,...(c.oldSvnSource?{svnSource:c.oldSvnSource}:{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: (snapshot.history?.vcs==='svn'&&options.autoContext===true?'SVN历史autoContext为精确依赖定位读取固定版本父目录有界清单,包含同目录非匹配名字元数据;仅匹配捕获正文发送模型,不扫描全仓,不用工作树替代历史版本。':'') + (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + '执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多3轮检索,共享每文件120秒和总模型调用100次预算;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。' };
|
|
53
54
|
} finally { busy = false; }
|
|
54
55
|
},
|
|
55
56
|
async execute(args, exec) {
|
package/src/snapshot.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {createSvnCaptureBudget} from './svn-capture-budget.mjs';
|
|
1
2
|
import {captureSvnDependencyChain} from './svn-dependency-chain.mjs';
|
|
2
3
|
import {createSvnNavigationBudget} from './svn-navigation-budget.mjs';
|
|
3
4
|
import {captureSvnExplicitContext} from './svn-explicit-context.mjs';
|
|
@@ -42,7 +43,13 @@ export async function captureSnapshot(cwd, options = {}) {
|
|
|
42
43
|
if (options.oldContextPaths !== undefined && (!Array.isArray(options.oldContextPaths) || options.oldContextPaths.length > 20 || new Set(options.oldContextPaths).size !== options.oldContextPaths.length)) throw new Error('Invalid old context paths');
|
|
43
44
|
for (const name of options.oldContextPaths ?? []) relativePath(name);
|
|
44
45
|
const type = await detectVcs(cwd);
|
|
45
|
-
if
|
|
46
|
+
if(options.svnReplacementBaseRevision!==undefined||options.svnReplacementAllowNetwork!==undefined){
|
|
47
|
+
if(type!=='svn'||['svnRevision','svnBaseRevision','svnTargetRevision','svnBasePath','svnTargetPath','commit','baseRevision','targetRevision'].some(key=>options[key]!==undefined))throw new Error('Replacement repository baseline requires SVN working-copy mode');
|
|
48
|
+
if(typeof options.svnReplacementBaseRevision!=='string'||!/^(0|[1-9][0-9]*)$/.test(options.svnReplacementBaseRevision)||!Number.isSafeInteger(Number(options.svnReplacementBaseRevision)))throw new Error('Replacement baseline requires fixed numeric revision');
|
|
49
|
+
if(options.svnReplacementAllowNetwork!==undefined&&typeof options.svnReplacementAllowNetwork!=='boolean')throw new Error('Invalid replacement network permission');
|
|
50
|
+
}
|
|
51
|
+
if(options.svnEffectiveMergeinfo!==undefined&&type!=='svn')throw new Error('Effective SVN mergeinfo requires SVN repository');
|
|
52
|
+
if (['svnRevision','svnBaseRevision','svnTargetRevision','svnAllowNetwork','svnBasePath','svnTargetPath'].some(key=>options[key]!==undefined)) {
|
|
46
53
|
if (type!=='svn') throw new Error('SVN revision parameters require SVN working copy');
|
|
47
54
|
return bindBusinessRequirement(await captureSvnHistory(cwd,options),options.businessRequirement);
|
|
48
55
|
}
|
|
@@ -51,6 +58,7 @@ export async function captureSnapshot(cwd, options = {}) {
|
|
|
51
58
|
if (type !== 'git') throw new Error('Git revision review is unsupported on SVN');
|
|
52
59
|
return bindBusinessRequirement(await captureGitHistory(cwd, options),options.businessRequirement);
|
|
53
60
|
}
|
|
61
|
+
if(type==='svn'){const budget=createSvnCaptureBudget(options.signal);options={...options,signal:budget.signal,svnCaptureBudget:budget};}
|
|
54
62
|
const capture = type === 'git' ? captureGit : captureSvn;
|
|
55
63
|
const snapshot = await capture(cwd, options);
|
|
56
64
|
if (!Array.isArray(rulePaths) || rulePaths.some(name => contextPaths.includes(name))) throw new Error('Rule paths must be separate from context paths');
|
|
@@ -76,6 +84,7 @@ export async function captureSnapshot(cwd, options = {}) {
|
|
|
76
84
|
if (changed) {
|
|
77
85
|
if (changed.eligibility !== 'reviewable') throw new Error('Context path is an excluded or blocked change');
|
|
78
86
|
if (changed.rightExists === false) throw new Error('Deleted path cannot be working context');
|
|
87
|
+
if(changed.specialData)throw new Error('SVN link records cannot be working source context');
|
|
79
88
|
context.push({ path: name, ...changed.right,...(changed.eolMapping?.new?{eolMapping:structuredClone(changed.eolMapping.new)}:{}) });
|
|
80
89
|
} else {
|
|
81
90
|
const seed=explicitSeeds.find(c=>c.path===name);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Shared across initial capture, navigation, mergeinfo and final verification.
|
|
2
|
+
export function createSvnCaptureBudget(signal){
|
|
3
|
+
const deadline=AbortSignal.any([AbortSignal.timeout(30000),...(signal?[signal]:[])]);let commands=0,metadataBytes=0,bodyBytes=0;
|
|
4
|
+
const error=message=>Object.assign(new Error(message),{code:'SVN_CAPTURE_BUDGET'});
|
|
5
|
+
return Object.freeze({signal:deadline,reserve(){deadline.throwIfAborted();if(++commands>512)throw error('SVN capture shared command budget exceeded');},charge(size,body=false){deadline.throwIfAborted();if(!Number.isSafeInteger(size)||size<0)throw error('Invalid SVN capture byte charge');if(body){bodyBytes+=size;if(bodyBytes>16*1024*1024)throw error('SVN capture shared verification body budget exceeded');}else{metadataBytes+=size;if(metadataBytes>1024*1024)throw error('SVN capture shared metadata budget exceeded');}},usage(){return {commands,metadataBytes,bodyBytes};}});
|
|
6
|
+
}
|
|
@@ -21,7 +21,7 @@ export async function captureSvnDependencyChain(root,files,options={}){
|
|
|
21
21
|
for(const plan of plans){truncated ||= plan.truncated;for(const entry of plan.candidates){if(visited.has(entry.path)||reserved.has(entry.path))continue;visited.add(entry.path);const c={...entry,depth};candidates.push(c);if(candidates.length>20)throw new Error('SVN dependency candidate limit exceeded');
|
|
22
22
|
if(context.length+new Set(options.contextPaths??[]).size>=limit){c.status='blocked';c.reason='Context file limit exceeded';continue;}
|
|
23
23
|
try{const {node,item}=await capture(entry.path);context.push(item);nodes.push({path:item.path,fingerprint:node.fingerprint});c.status='captured';next.push({path:item.path,eligibility:'reviewable',right:item});}
|
|
24
|
-
catch(error){signal.throwIfAborted();if(error.code==='SVN_NAVIGATION_BUDGET'||error.message==='SVN dependency source byte limit exceeded')throw error;c.status='blocked';c.reason=error.message;}
|
|
24
|
+
catch(error){signal.throwIfAborted();if(error.code==='SVN_CAPTURE_BUDGET'||error.code==='SVN_NAVIGATION_BUDGET'||error.message==='SVN dependency source byte limit exceeded')throw error;c.status='blocked';c.reason=error.message;}
|
|
25
25
|
}}frontier=next;
|
|
26
26
|
}if(frontier.length)truncated=true;
|
|
27
27
|
const verify=async()=>{for(const node of nodes)if((await svnCallerNode(root,node.path,{...bounded,allowExplicitKeywords:true})).fingerprint!==node.fingerprint)throw new Error('SVN dependency node changed during capture');if(index&&(await svnCallerProbes(root,[...probes],bounded)).fingerprint!==index.fingerprint)throw new Error('SVN dependency resolution changed during capture');signal.throwIfAborted();};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Stable distinctions between review completion and unavailable coherent sources.
|
|
2
|
+
export const SVN_STATE_GUIDANCE=Object.freeze({
|
|
3
|
+
SVN_MISSING_NODE:{recoverable:false,action:'Restore the intended file or explicitly schedule deletion outside this read-only review. Missing is not deletion.'},
|
|
4
|
+
SVN_TEXT_CONFLICT:{recoverable:false,action:'Resolve the conflict outside review; conflict markers do not define an approved target.'},
|
|
5
|
+
SVN_PROPERTY_CONFLICT:{recoverable:false,action:'Resolve property conflicts outside review before capturing target semantics.'},
|
|
6
|
+
SVN_TREE_CONFLICT:{recoverable:false,action:'Resolve tree history outside review; do not infer replacement or copy provenance.'},
|
|
7
|
+
SVN_OBSTRUCTED_NODE:{recoverable:false,action:'Choose the intended tracked node outside review; obstruction does not authorize arbitrary local data.'},
|
|
8
|
+
SVN_INCOMPLETE_NODE:{recoverable:false,action:'Complete the working copy outside review; incomplete inventory is not a successful audit.'},
|
|
9
|
+
SVN_SWITCHED_NODE:{recoverable:false,action:'Select an independently rooted working copy for the switched source or use explicit fixed branch endpoints.'},
|
|
10
|
+
SVN_SWITCHED_ANCESTOR:{recoverable:false,action:'Use explicit fixed branch endpoints or an independently selected source root; do not reuse parent URL identity.'},
|
|
11
|
+
SVN_EXTERNAL_NODE:{recoverable:false,action:'Review an external through its own explicit repository scope. Never follow externals implicitly.'},
|
|
12
|
+
SVN_REPLACEMENT:{recoverable:true,action:'An explicit fixed repository baseline can define a separate comparison, not recover the removed local BASE.'},
|
|
13
|
+
SVN_COPIED_REPLACEMENT:{recoverable:true,action:'Use explicit target-URL baseline; copy-source pristine is not target old data.'},
|
|
14
|
+
SVN_SPARSE_STRUCTURE:{recoverable:false,action:'Select individual available nodes or a fixed repository tree; sparse descendants cannot imply deletion or complete coverage.'}
|
|
15
|
+
});
|
|
16
|
+
export function svnDiagnostic(code){return SVN_STATE_GUIDANCE[code]?{code,...SVN_STATE_GUIDANCE[code],readOnly:true,reviewCompleted:false}:null;}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {hash,relativePath} from './content.mjs';
|
|
2
|
+
import {mergeinfoNavigation} from './svn-mergeinfo.mjs';
|
|
3
|
+
const notice='Effective property navigation within the evidenced ancestor boundary only; not merge attribution, reviewed revision coverage, permission to read branch bodies, or proof of merge execution.';
|
|
4
|
+
|
|
5
|
+
// records are nearest-first and complete from target to the declared boundary.
|
|
6
|
+
// present:false is proven absence; missing records must never mean absent.
|
|
7
|
+
export function effectiveMergeinfo(target,records,{boundary='.',complete=false}={}){
|
|
8
|
+
if(target!=='.')relativePath(target);if(boundary!=='.')relativePath(boundary);
|
|
9
|
+
if(boundary!=='.'&&target!==boundary&&!target.startsWith(boundary+'/'))throw new Error('Mergeinfo boundary is not an ancestor');
|
|
10
|
+
const unknown=reason=>({status:'unknown',reason,sources:[],notice});
|
|
11
|
+
if(!Array.isArray(records)||records.length>33)return unknown('Ancestor evidence missing or depth limit exceeded');
|
|
12
|
+
let cursor=target;const evidence=[];
|
|
13
|
+
for(const record of records){
|
|
14
|
+
if(!record||record.path!==cursor||typeof record.present!=='boolean'||typeof record.identity!=='string'||!record.identity)return unknown('Incomplete or unordered ancestor provenance');
|
|
15
|
+
if(record.present&&typeof record.value!=='string')return unknown('Mergeinfo value is not captured text');
|
|
16
|
+
evidence.push({path:cursor,identity:record.identity,present:record.present,...(record.present?{propertyHash:hash(record.value)}:{})});
|
|
17
|
+
if(record.present){
|
|
18
|
+
const parsed=mergeinfoNavigation(record.value);if(parsed.status!=='parsed')return {...parsed,evidence,notice};
|
|
19
|
+
const inherited=cursor!==target,suffix=cursor==='.'?(target==='.'?'':target):target.slice(cursor.length+1);
|
|
20
|
+
const sources=parsed.sources.map(source=>({sourcePath:inherited?(source.sourcePath==='/'?'':source.sourcePath)+'/'+suffix:source.sourcePath,ranges:source.ranges.filter(range=>!inherited||range.inheritable)})).filter(source=>source.ranges.length);
|
|
21
|
+
if(sources.some(s=>s.sourcePath.length>1024))return {status:'limited',reason:'Inherited mergeinfo source path limit exceeded',sources:[],evidence,notice};
|
|
22
|
+
return {status:'parsed',origin:cursor,inherited,explicitEmpty:record.value==='',sources,evidence,hash:hash(JSON.stringify(evidence)),notice};
|
|
23
|
+
}
|
|
24
|
+
if(cursor===boundary){if(!complete)return {...unknown('Ancestor boundary does not prove repository-wide absence'),evidence,hash:hash(JSON.stringify(evidence))};return {status:'parsed',origin:null,inherited:false,sources:[],evidence,hash:hash(JSON.stringify(evidence)),notice};}
|
|
25
|
+
if(cursor==='.')return unknown('Declared ancestor boundary was not reached');
|
|
26
|
+
cursor=cursor.includes('/')?cursor.slice(0,cursor.lastIndexOf('/')):'.';
|
|
27
|
+
}
|
|
28
|
+
return unknown('Ancestor evidence ended before nearest explicit property or boundary');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Injected reader must freeze UUID/URL/peg/operative revision and charge its
|
|
32
|
+
// existing command/metadata budget. This helper never reads branch bodies.
|
|
33
|
+
export async function captureEffectiveMergeinfo(target,{read,signal,boundary='.',complete=false}={}){
|
|
34
|
+
if(typeof read!=='function')throw new Error('Fixed ancestor property reader required');
|
|
35
|
+
if(target!=='.')relativePath(target);if(boundary!=='.')relativePath(boundary);
|
|
36
|
+
if(boundary!=='.'&&target!==boundary&&!target.startsWith(boundary+'/'))throw new Error('Mergeinfo boundary is not an ancestor');
|
|
37
|
+
const records=[];let cursor=target;
|
|
38
|
+
for(let depth=0;depth<33;depth++){
|
|
39
|
+
signal?.throwIfAborted();const record=await read(cursor);signal?.throwIfAborted();
|
|
40
|
+
if(record?.path!==cursor)throw new Error('Mergeinfo ancestor reader identity mismatch');records.push(record);
|
|
41
|
+
if(record.present||cursor===boundary)return effectiveMergeinfo(target,records,{boundary,complete});
|
|
42
|
+
if(cursor==='.')throw new Error('Mergeinfo ancestor boundary not reached');
|
|
43
|
+
cursor=cursor.includes('/')?cursor.slice(0,cursor.lastIndexOf('/')):'.';
|
|
44
|
+
}
|
|
45
|
+
return {status:'limited',reason:'Mergeinfo ancestor depth limit exceeded',sources:[],notice};
|
|
46
|
+
}
|