@feng0531/dsh-code-review 0.78.0 → 0.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/CORE-ALIGNMENT-TASKS.md +3 -1
- package/DEVELOPMENT.md +2 -2
- package/README.md +27 -1
- package/RELEASE-NOTES.md +15 -9
- package/SVN-SUPPORT-MATRIX.md +33 -2
- package/index.mjs +1 -1
- package/package.json +1 -1
- 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 +19 -0
- package/src/svn-keyword-mapping.mjs +34 -12
- package/src/svn-replacement-source.mjs +25 -0
- package/src/svn-replacement-tree.mjs +49 -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 +55 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
|
|
4
4
|
|
|
5
|
+
## 0.80.0 — SVN集中能力更新
|
|
6
|
+
|
|
7
|
+
集中加入固定跨分支、祖先mergeinfo、独立历史基线替换树、链接记录、固定宽度/复合关键字与全采集预算。明确unknown和不可恢复状态,不隐式跟随链接/externals。原生%_空格格式通过复现,引号配置拒绝。详情见本版发布说明,统一验收完成前不称正式交付。
|
|
8
|
+
|
|
9
|
+
### 同批发布等待优化
|
|
10
|
+
|
|
11
|
+
正式发布严格核验并复用相同冻结标签/提交的成功四矩阵运行,不重复执行四组测试;错仓库/工作流/提交/标签/attempt、缺组或失败明确拒绝。保留原包单次构建、安装与npm/Release核验,不新增SVN支持状态。
|
|
12
|
+
|
|
13
|
+
## 0.79.0 — 简单自定义 revision 关键字
|
|
14
|
+
|
|
15
|
+
支持ASCII名称加单个%r格式的自定义关键字,复用BASE收缩核验及原始展开证据。预览标出名称/格式,来源预算不扩张;标准名覆盖、重复自定义名、复杂格式和固定宽度仍拒绝,不能视为全部格式支持。521项串行回归、6项关键字组合、宿主/隔离安装及四矩阵CI通过;原始Actions/npm/Release包152,877字节逐字节一致,双哈希/清单/冻结说明通过。
|
|
16
|
+
|
|
5
17
|
## 0.78.0 — SVN 标准关键字限定调用方
|
|
6
18
|
|
|
7
|
-
限定调用方复用已验证的标准关键字来源捕获,只有命中原文进入上下文,未命中扫描及BASE
|
|
19
|
+
限定调用方复用已验证的标准关键字来源捕获,只有命中原文进入上下文,未命中扫描及BASE核验仍计入共享预算。正文/属性/祖先复核保留,不支持配置或读取失败明确失败,不冒充完整调用覆盖。首次全量Git历史测试超时保留记录,保持原限制独立5项及重新全量519/519通过;宿主/隔离安装及四矩阵CI通过,原始Actions/npm/Release包151,916字节逐字节一致,双哈希/清单/冻结正文通过。
|
|
8
20
|
|
|
9
21
|
## 0.77.0 — SVN 标准关键字自动依赖
|
|
10
22
|
|
package/CORE-ALIGNMENT-TASKS.md
CHANGED
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
|
|
33
33
|
## 当前验证结论
|
|
34
34
|
|
|
35
|
-
- 最近完整回归:
|
|
35
|
+
- 最近完整回归:521项通过;v0.79.0 关键字组合6项通过。
|
|
36
|
+
- v0.79.0 已发布简单Name=%r自定义revision关键字,原文/BASE核验与预算保持,其他自定义格式/固定宽度不冒充支持;宿主/隔离安装及四矩阵CI通过,原始Actions/npm/Release包152,877字节逐字节一致,双哈希/清单/冻结正文通过。后续按完整能力组织批次,减少细碎重复全量发布;全面SVN仍未闭环。
|
|
37
|
+
- v0.78.0 已发布标准关键字限定调用方,原展开命中正文捕获、非匹配隔离,扫描和BASE核验共享来源预算/截止;宿主/隔离安装及四矩阵CI通过,原始Actions/npm/Release包151,916字节逐字节一致,双哈希/清单/冻结正文通过。特殊状态、有效继承mergeinfo与跨URL等继续待闭环。
|
|
36
38
|
- v0.77.0 已发布自动标准关键字依赖及显式入口,复用BASE核验/身份复核,全部正文共享来源预算,原展开证据保留,不支持配置明确阻断;宿主/隔离安装和四矩阵CI通过,原始Actions/npm/Release包151,247字节逐字节一致,双哈希/清单/冻结说明通过。调用方关键字与其余全面SVN缺口继续。
|
|
37
39
|
- v0.76.3 已发布本地导航共享1MiB来源/30秒截止BUG修复,显式/依赖/调用方扫描及关键字BASE核验共同扣费,共享耗尽传播失败;宿主/隔离安装和四矩阵CI通过,原始Actions/npm/Release包150,607字节逐字节一致,双哈希/清单/冻结正文通过。全面SVN其余缺口继续。
|
|
38
40
|
- v0.76.2 已发布普通SVN变更来源URL/UUID/revision冻结和切换祖先阻断,预览execute来源变化拒绝且零发送;v0.76.1测试编码断言失败未发布、原标签保留,修正后四矩阵CI通过。原始Actions/npm/Release包149,274字节逐字节一致,双哈希/清单/冻结正文通过。共享预算超限独立BUG仍待修复。
|
package/DEVELOPMENT.md
CHANGED
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
|
|
28
28
|
## 正式发布
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
按最新要求,先集中完成SVN支持矩阵全部剩余功能,再统一执行复现验收、全量测试、提交推送及正式发布;开发中不逐小功能测试、打标签或发布。未执行的测试用例和未验收实现必须明确标注。版本格式为 `vX.Y.Z`:X是大版本(重大或不兼容升级),Y是小版本/新增功能,Z是BUG修复;纯修复递增Z,不一律递增Y。已发布标签不移动,发布不自动安装、升级或重启宿主。
|
|
31
31
|
|
|
32
32
|
1. 修改版本号与锁文件;更新 [变更记录](CHANGELOG.md) 和 [发布说明](RELEASE-NOTES.md)。
|
|
33
33
|
2. 完成测试、差异检查和独立安装验证后,提交、推送,并创建对应 vX.Y.Z 标签。标签一旦进入发布流程就不再移动。
|
|
34
34
|
3. 等待标签 CI 的 Ubuntu/Windows × Node.js 22/24 四组测试全部成功。
|
|
35
|
-
4. 在 Actions 的 CI 工作流选择该标签,设置 publish: true
|
|
35
|
+
4. 在 Actions 的 CI 工作流选择该标签,设置 publish: true,并将 ci_run_id 填为第3步成功的push标签CI运行ID,仅触发一次正式发布。发布不再重复四矩阵;先核验同仓库/工作流/标签/提交/当前attempt及四组npm test成功证据,缺失或变化直接失败。
|
|
36
36
|
5. 发布任务打包一次、验证安装、保存安装包,再发布 npm。随后下载 npm 实际安装包,检查它与保存的原包是否一致。
|
|
37
37
|
6. 核验通过后,把同一安装包与 SHA256SUMS.txt 附到 GitHub Release,并使用发布说明作为正文。
|
|
38
38
|
7. 下载 Release 的实际附件,与原包逐字节比较,核验 SHA-256、SHA-512 及校验清单。
|
package/README.md
CHANGED
|
@@ -136,6 +136,32 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
|
|
|
136
136
|
|
|
137
137
|
`svnAllowNetwork:true` 只允许读取本次工作副本对应仓库的固定版本,不授权更新、合并或任意远端 URL。本地 `file:` 仓库通常无需此参数。
|
|
138
138
|
|
|
139
|
+
### 比较 SVN 两个分支(v0.80.0)
|
|
140
|
+
|
|
141
|
+
以下为v0.80.0集中更新的用法;安装旧版本时这些参数不可用,先确认当前加载版本。
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{"svnBaseRevision":"7315","svnTargetRevision":"7315","svnBasePath":"branches/release","svnTargetPath":"trunk","svnAllowNetwork":true,"autoContext":true}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
分支路径相对于当前SVN仓库根,不是本地目录或任意URL。旧、新两侧分别固定URL、peg与operative revision;同revision可以比较不同分支。只能在同仓库UUID内比较;嵌套重叠根和不明确来源拒绝,不能推断merge提交归属。
|
|
148
|
+
|
|
149
|
+
### 查看有效继承 mergeinfo(v0.80.0)
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{"svnRevision":"7315","svnAllowNetwork":true,"svnEffectiveMergeinfo":true}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
显式允许读取选中节点及最近祖先的合并属性,额外读取受共享预算限制;工作副本模式也可使用该开关并保持离线。空属性屏蔽继承,祖先证据不完整、混合BASE或不安全来源显示unknown;结果仅为属性导航,不证明相关revision已经审核。
|
|
156
|
+
|
|
157
|
+
### 审查未提交替换(v0.80.0)
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{"selectedPaths":["src/module","src/module/old.cpp","src/module/new.cpp"],"svnReplacementBaseRevision":"7314","svnReplacementAllowNetwork":true}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
替换比较的旧侧是你显式选择的目标URL固定历史节点,不是本地BASE,也不证明它就是被删除节点的原始revision。目录后代必须独立选入;只选父目录不能算后代均已批准。远端读取使用独立许可,默认不访问。没有可确认旧来源时明确拒绝,不读取复制源冒充旧节点。
|
|
164
|
+
|
|
139
165
|
### 加入业务要求、规则和调用方
|
|
140
166
|
|
|
141
167
|
```text
|
|
@@ -156,7 +182,7 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
|
|
|
156
182
|
|
|
157
183
|
这些文件和目录需要实际存在,路径相对于所选仓库:
|
|
158
184
|
|
|
159
|
-
- `contextPaths`:手动加入辅助文件,不代表该文件本身完成了主审。SVN工作副本的未变更文件须通过离线正常节点/属性及来源检查;标准关键字在显式上下文和autoContext依赖中经过BASE
|
|
185
|
+
- `contextPaths`:手动加入辅助文件,不代表该文件本身完成了主审。SVN工作副本的未变更文件须通过离线正常节点/属性及来源检查;标准关键字在显式上下文和autoContext依赖中经过BASE核验后支持;简单自定义Name=%r也经同一BASE核验支持;复杂自定义/固定宽度关键字、特殊或不可确认来源仍明确拒绝,已选择的可审查变更复用快照。
|
|
160
186
|
- `autoContext:true`:自动补充最多三层文本依赖,不是全仓扫描或编译器分析。
|
|
161
187
|
- `callerScopePaths`:在明确指定的非根目录中寻找字面引用。预览可能扫描该目录内不匹配的候选正文;只有捕获的匹配上下文随执行发送模型。SVN标准关键字候选经BASE核验后支持,扫描及核验正文共同计费;不支持配置明确失败,不保证找到全部调用方。
|
|
162
188
|
- `rulePaths`:最多四个 UTF-8 Markdown 或文本规则文件,不能同时列为上下文。
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
# v0.
|
|
1
|
+
# v0.80.1 发布说明
|
|
2
2
|
|
|
3
|
-
## SVN
|
|
3
|
+
## SVN 集中能力更新
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- 同仓库跨分支固定URL比较:svnBasePath/svnTargetPath指定仓库根相对路径,支持相同revision不同分支,各侧正文、上下文、依赖和调用方独立来源。
|
|
6
|
+
- svnEffectiveMergeinfo显式读取BASE/工作或固定历史的祖先合并属性,保留来源证据;不完整边界和混合BASE返回unknown,不推断合并提交已审查。
|
|
7
|
+
- svnReplacementBaseRevision显式指定目标URL历史基线,支持文件/目录替换树及类型变化,每个后代单独批准;不使用copy-source BASE,不声称恢复被删除节点。远端需svnReplacementAllowNetwork。
|
|
8
|
+
- SVN链接只审link目标记录,不跟随目标,不把链接数据用于源码导航。稀疏/混合revision明确披露已观察范围,不伪造全树覆盖。
|
|
9
|
+
- 关键字支持固定宽度、截断标记及复合自定义格式,原正文/hash/行号保留。空格用SVN原生%_;引号格式明确拒绝,不模拟shell语义。
|
|
10
|
+
- 工作副本统一命令/元数据/截止账本,含导航、mergeinfo及末尾验证,超限明确失败。
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
- 工作及BASE展开/未展开正文全部进入共享1MiB来源/30秒预算;扫描正文与总来源分别报告,核验不另开额度。
|
|
9
|
-
- 所有已扫描文件的节点/祖先指纹及正文哈希均复核,包含未命中候选。
|
|
10
|
-
- 自定义、固定宽度、不完整展开或读取失败明确失败,不把失败当作无引用;不保证完整调用覆盖。
|
|
12
|
+
## 发布验证等待优化
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
发布核验同一冻结标签/提交/当前attempt的成功四矩阵CI证据,不再重复运行四组测试。错仓库、错提交、缺组、失败或重跑中的记录拒绝;原包一次构建、npm/Release字节及双哈希核验保持。
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
## 必须了解的限制
|
|
17
|
+
|
|
18
|
+
冲突、缺失、阻塞、incomplete没有可确认目标时只诊断,不标记审查成功,也不自动解决。外部/切换来源必须独立选择仓库根或明确固定分支端点,不隐式跟随。不完整权限、不可解码、稀疏缺项和预算耗尽均不得视为通过。跨仓库/嵌套重叠分支根不支持。
|
|
19
|
+
|
|
20
|
+
使用步骤与JSON示例见[README](README.md),逐项证据与限制见[支持矩阵](SVN-SUPPORT-MATRIX.md)。
|
package/SVN-SUPPORT-MATRIX.md
CHANGED
|
@@ -10,6 +10,27 @@
|
|
|
10
10
|
- 历史来源固定仓库 UUID、规范 URL、peg revision、operative revision、节点路径与内容哈希;禁止把较新工作树当作历史正文。
|
|
11
11
|
- 文件/字节/上下文/精确探测/调用/超时预算继续共享。超限、不可解码、不可定位和权限失败明确保留未完成状态。
|
|
12
12
|
|
|
13
|
+
## 当前集中验收状态(尚未正式发布)
|
|
14
|
+
|
|
15
|
+
最新执行要求:全部剩余功能集中改完后统一测试和正式发布,覆盖下文历史“每批发布”节奏;历史发布证据不改写。当前已进入统一验收,初组25项与真实跨分支/关键字组6项通过;既有SVN组96/97通过,发现summary记录顺序导致误报后修复,历史修复组11/11通过。所有失败日志保留;统一全项目串行545/545通过、0跳过;最终原生关键字语义修正另做4项专项及实际宿主通过。545项运行开始后曾修正原生引号假设,因此最终冻结提交仍需四矩阵完整重验;未打标签、未发布,统一版本拟为v0.80.0。
|
|
16
|
+
|
|
17
|
+
| 能力 | 本地集中验收证据 | 边界与未证明事项 |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| CI证据复用 | 拒绝错仓库/提交/标签/attempt、缺组/重复/失败/缺npm test步骤;真实旧运行API校验通过 | 首次新流程端到端发布尚待执行 |
|
|
20
|
+
| 固定宽度/复合关键字 | 真实SVN固定字段及复合格式BASE收缩核验通过,原文/行号保留 | 原生%_表示空格;引号属性未按预期展开,明确拒绝,不模拟shell。未知格式/不完整展开拒绝 |
|
|
21
|
+
| 历史链接记录 | 真实special→普通文件转换、仓库原记录、目标内容隔离通过 | 仅link记录,不跟随目标;特殊侧不能当源码上下文 |
|
|
22
|
+
| 本地链接记录 | 原生符号链接readlink、普通link记录、目标文件不读取通过 | 当前主机通过;Windows/Ubuntu最终四矩阵仍须验证。链接权限失败不能跳过算支持 |
|
|
23
|
+
| 有效继承mergeinfo | 最近显式祖先、空属性、不可继承*、历史逐侧及工作BASE/工作侧复现通过 | 边界外未知、混合BASE、不安全祖先返回unknown;无合并归属/提交覆盖推断 |
|
|
24
|
+
| 跨URL固定历史 | 同revision不同分支真实复现、上下文来源与侧缓存隔离通过;summary顺序误报已修复,历史组合11/11通过 | 同仓库根相对端点;跨仓库/重叠嵌套根拒绝,分支来源不构成merge归属 |
|
|
25
|
+
| 全采集预算 | 命令/元数据/取消边界及导航超限组合通过;统一全项目545/545通过 | 额外采集与末尾复核共享原上限,超限直接失败,不保证任意规模可审查 |
|
|
26
|
+
| 稀疏/混合revision | depth身份与观察到revision集合披露用例通过;原结构目录回归通过 | 不声称稀疏全树覆盖,非完整结构目录明确诊断,缺项不等于删除 |
|
|
27
|
+
| 独立历史基线替换树 | 文件/目录类型、复制替换目标旧正文、old-only/new-only后代及父项独选阻断真实复现通过 | 用户显式基线不证明等于原丢失BASE;需独立远端授权;每个后代独立批准 |
|
|
28
|
+
| URL来源身份 | 编码分隔符/凭据/控制字符拒绝与合法转义等价用例、普通复制/中文路径回归通过 | 最终四矩阵待验;不允许任意URL或隐式跨仓库 |
|
|
29
|
+
| 不可恢复或未独立授权状态 | missing/冲突/阻塞/incomplete/switched/externals分类、拒绝来源回流已有回归 | 仅诊断不是可审查支持;不自动修复、更新、跟随externals或选择替代来源 |
|
|
30
|
+
|
|
31
|
+
16份延期用例已转为正式用例,集中新增25项、跨分支/关键字6项及历史修复组11项通过;初次summary顺序失败和原生引号假设失败保留记录。统一串行545/545、最终语义修正4项和实际宿主通过;冻结提交四矩阵/隔离安装/正式工件仍待完成,不提前宣称正式交付。
|
|
32
|
+
|
|
33
|
+
|
|
13
34
|
## 当前盘点(不是全部支持)
|
|
14
35
|
|
|
15
36
|
| 场景 | 当前证据/限制 | 实施批次 |
|
|
@@ -31,10 +52,20 @@
|
|
|
31
52
|
| 历史两侧上下文与调用方 | 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
53
|
| 真正 r7314→r7315 样本 | 用户未提供隔离副本路径;本地类似复现不是该样本验收 | 获得路径后只读生成预览,不自动发送模型 |
|
|
33
54
|
|
|
34
|
-
##
|
|
55
|
+
## 简单自定义revision关键字(v0.79.0 已发布)
|
|
56
|
+
|
|
57
|
+
冻结提交 `9c3582dd4b2cd0724587c23e00d2bfa92ccdd8ac`;标签CI `38740586071`,唯一发布运行 `38741741913`,原工件 `11975739387`。原始Actions/npm/Release包152,877字节逐字节一致;SHA-256 `33138b593242ca7495baa5696a109f9548fd5901ead8ffff27300c81ff81d34b`,SHA-512/清单/冻结发布说明核验通过。
|
|
58
|
+
|
|
59
|
+
- 只支持ASCII字母起始字母数字名称=单个%r,不覆盖标准名称,不接收重复名/复杂格式/其他占位符;原行保持收缩逻辑及BASE核验保留,customKeywords元数据披露。
|
|
60
|
+
- `test/svn-custom-keywords.test.mjs` 真实离线Build=%r展开、BASE核验、显式来源与变更原文通过;配置拒绝边界与既有标准/显式/依赖组合6项通过。521项串行全量回归、6项关键字组合、宿主/隔离安装、四矩阵标签及发布CI通过,正式发布工件核验完成。
|
|
61
|
+
- 其他自定义格式、固定宽度、特殊状态与分支来源等仍未完成,不把窄子集当作该矩阵行全闭环。
|
|
62
|
+
|
|
63
|
+
## 标准关键字限定调用方(v0.78.0 已发布)
|
|
64
|
+
|
|
65
|
+
冻结提交 `deb0c91f6216a0637d43b44fd3dfc8194d306940`;标签CI `38735681006`,唯一发布运行 `38736546649`,原工件 `11974502757`。原始Actions/npm/Release包151,916字节逐字节一致;SHA-256 `efe0ef2a6b23ed5d63e8a92252f139cced0d8f4d59ddc8bc3bcb3ba1d39cf04c`,SHA-512/清单/冻结发布说明核验通过。
|
|
35
66
|
|
|
36
67
|
- 调用方扫描复用captureSvnExplicitContext标准关键字核验,捕获原展开上下文,只保留命中正文;所有扫描身份/哈希复核。BASE两份核验进入原共享来源账本,sourceBytes与scannedBytes分别披露。
|
|
37
|
-
- `test/svn-caller-keywords.test.mjs` 离线真实复现:命中展开原文、非命中正文不进入快照/模型、来源计费、revision保留、预览0发送与稳定ID;既有调用方/预算/EOL组合4项通过,首次全量518通过/1失败:未改动Git历史链测试超时;保持原限制独立复跑5/5通过,重新全量519/519
|
|
68
|
+
- `test/svn-caller-keywords.test.mjs` 离线真实复现:命中展开原文、非命中正文不进入快照/模型、来源计费、revision保留、预览0发送与稳定ID;既有调用方/预算/EOL组合4项通过,首次全量518通过/1失败:未改动Git历史链测试超时;保持原限制独立复跑5/5通过,重新全量519/519通过,原失败记录保留;宿主/隔离安装及四矩阵标签/发布CI通过,正式发布工件核验完成。
|
|
38
69
|
- 自定义/固定宽度/不完整展开或读取失败仍不能当作负搜索证据。特殊状态、有效继承mergeinfo与跨URL缺口继续。
|
|
39
70
|
|
|
40
71
|
## 自动依赖标准关键字(v0.77.0 已发布)
|
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
|
@@ -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
|
+
}
|
|
@@ -10,7 +10,7 @@ export async function svnHistoryCallers(files,history,scopes,context,{index,look
|
|
|
10
10
|
const changed=new Set(files.map(f=>f.path)),rules=new Set(rulePaths),candidates=[],scanned=[],allProbes=new Set();
|
|
11
11
|
let records=0,scanBytes=0;
|
|
12
12
|
for(const [side,revision]of [['old',history.base],['new',history.target]]){
|
|
13
|
-
signal?.throwIfAborted();const entries=await index(approved,revision);
|
|
13
|
+
signal?.throwIfAborted();const entries=await index(approved,revision,side);
|
|
14
14
|
if(!Array.isArray(entries))throw new Error('Invalid SVN historical caller index');
|
|
15
15
|
const seen=new Set(),sources=[];
|
|
16
16
|
for(const entry of entries){
|
|
@@ -20,7 +20,7 @@ export async function svnHistoryCallers(files,history,scopes,context,{index,look
|
|
|
20
20
|
seen.add(name);
|
|
21
21
|
if(entry.kind!=='file'||changed.has(name)||rules.has(name)||isSecretPath(name)||!/\.(mjs|cjs|js|jsx|ts|tsx|c|cc|cpp|cxx|h|hh|hpp|hxx|shader|hlsl|glsl|cginc|hlsli|compute)$/i.test(name))continue;
|
|
22
22
|
if(scanned.length>=128)throw new Error('SVN historical caller scan count exceeded');
|
|
23
|
-
const value=await read(name,revision);signal?.throwIfAborted();
|
|
23
|
+
const value=await read(name,revision,side);signal?.throwIfAborted();
|
|
24
24
|
if(typeof value?.content?.text!=='string'||value.kind!=='file'||value.source?.operativeRevision!==revision)throw new Error('Invalid fixed SVN caller body');
|
|
25
25
|
const bodyBytes=value.content.bytes??Buffer.byteLength(value.content.text);
|
|
26
26
|
if(!Number.isSafeInteger(bodyBytes)||bodyBytes<0||bodyBytes>256*1024||Buffer.byteLength(value.content.text)>256*1024)throw new Error('Invalid fixed SVN caller size');
|
|
@@ -29,13 +29,13 @@ export async function svnHistoryCallers(files,history,scopes,context,{index,look
|
|
|
29
29
|
const source={path:name,...value.content,svnSource:value.source,side,revision};sources.push(source);scanned.push(source);
|
|
30
30
|
}
|
|
31
31
|
const targets=files.map(file=>{
|
|
32
|
-
const kind=side==='old'?file.oldKind:file.newKind,body=side==='old'?file.left:file.right;
|
|
32
|
+
const kind=file.specialData?.[side]?'special':side==='old'?file.oldKind:file.newKind,body=side==='old'?file.left:file.right;
|
|
33
33
|
return {...file,right:kind==='file'?body:undefined,rightExists:kind==='file'&&typeof body?.text==='string'};
|
|
34
34
|
});
|
|
35
35
|
const requested=callerCandidates(targets,sources,[]).probePaths,present=[];
|
|
36
36
|
for(const name of requested){
|
|
37
|
-
signal?.throwIfAborted();allProbes.add(JSON.stringify([revision,name]));if(allProbes.size>512)throw new Error('SVN historical caller probe limit exceeded');
|
|
38
|
-
const node=await lookup(name,revision);
|
|
37
|
+
signal?.throwIfAborted();allProbes.add(JSON.stringify([side,revision,name]));if(allProbes.size>512)throw new Error('SVN historical caller probe limit exceeded');
|
|
38
|
+
const node=await lookup(name,revision,side);
|
|
39
39
|
if(node!==null&&(!node||!['file','dir'].includes(node.kind)))throw new Error('Invalid fixed SVN caller probe');
|
|
40
40
|
if(node)present.push(name); // Directories remain resolution competitors.
|
|
41
41
|
}
|
|
@@ -14,7 +14,7 @@ export async function svnHistoryDependencies(files, history, context, {lookup, r
|
|
|
14
14
|
relativePath(name);signal?.throwIfAborted();
|
|
15
15
|
if(!cache[side].has(name)){
|
|
16
16
|
if(++probes>512)throw new Error('SVN historical dependency probe limit exceeded');
|
|
17
|
-
const node=await lookup(name,side==='old'?history.base:history.target);
|
|
17
|
+
const node=await lookup(name,side==='old'?history.base:history.target,side);
|
|
18
18
|
if(node!==null&&(!node||!['file','dir'].includes(node.kind)))throw new Error('Invalid SVN historical dependency probe result');
|
|
19
19
|
cache[side].set(name,node);
|
|
20
20
|
}
|
|
@@ -26,6 +26,7 @@ export async function svnHistoryDependencies(files, history, context, {lookup, r
|
|
|
26
26
|
for(const file of files){
|
|
27
27
|
if(file.eligibility!=='reviewable'||file.kind==='dir'&&file.path==='.')continue;
|
|
28
28
|
for(const side of ['old','new']){
|
|
29
|
+
if(file.specialData?.[side])continue;
|
|
29
30
|
const kind=side==='old'?file.oldKind:file.newKind,text=(side==='old'?file.left:file.right)?.text;
|
|
30
31
|
if(kind==='file'&&typeof text==='string'){visited[side].add(file.path);frontier[side].push(nextSource(file.path,text));}
|
|
31
32
|
}
|
|
@@ -52,7 +53,7 @@ export async function svnHistoryDependencies(files, history, context, {lookup, r
|
|
|
52
53
|
const changed=files.find(file=>file.path===entry.path);
|
|
53
54
|
if(changed){
|
|
54
55
|
const kind=side==='old'?changed.oldKind:changed.newKind,text=(side==='old'?changed.left:changed.right)?.text;
|
|
55
|
-
if(changed.eligibility!=='reviewable'||kind!=='file'||typeof text!=='string'){receipt.status='blocked';receipt.reason='Dependency conflicts with excluded blocked or unavailable change';continue;}
|
|
56
|
+
if(changed.eligibility!=='reviewable'||changed.specialData?.[side]||kind!=='file'||typeof text!=='string'){receipt.status='blocked';receipt.reason='Dependency conflicts with excluded blocked or unavailable change';continue;}
|
|
56
57
|
receipt.status='reused-change';next[side].push(nextSource(entry.path,text));continue;
|
|
57
58
|
}
|
|
58
59
|
if(isSecretPath(entry.path)){receipt.status='blocked';receipt.reason='Credential dependency excluded';continue;}
|
|
@@ -63,7 +64,7 @@ export async function svnHistoryDependencies(files, history, context, {lookup, r
|
|
|
63
64
|
if(!item&&context.length>=maxContextFiles){receipt.status='blocked';receipt.reason='Context file limit exceeded';continue;}
|
|
64
65
|
const presentText=side==='old'?item?.oldText:item?.text;
|
|
65
66
|
if(typeof presentText==='string'){receipt.status='reused-context';next[side].push(nextSource(entry.path,presentText));continue;}
|
|
66
|
-
const value=await read(entry.path,receipt.revision);signal?.throwIfAborted();
|
|
67
|
+
const value=await read(entry.path,receipt.revision,side);signal?.throwIfAborted();
|
|
67
68
|
if(typeof value?.content?.text!=='string'||!value.source||value.source.operativeRevision!==receipt.revision)throw new Error('Invalid fixed SVN dependency body');
|
|
68
69
|
charge(value.content.text);
|
|
69
70
|
if(!item){item={path:entry.path};context.push(item);}
|