@feng0531/dsh-code-review 0.76.0 → 0.76.3
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 +9 -1
- package/CORE-ALIGNMENT-TASKS.md +3 -1
- package/DEVELOPMENT.md +1 -1
- package/RELEASE-NOTES.md +8 -9
- package/SVN-SUPPORT-MATRIX.md +21 -2
- package/package.json +1 -1
- package/src/service.mjs +1 -1
- package/src/snapshot.mjs +6 -3
- package/src/svn-caller-capture.mjs +1 -1
- package/src/svn-dependency-chain.mjs +3 -3
- package/src/svn-explicit-context.mjs +2 -2
- package/src/svn-navigation-budget.mjs +5 -0
- package/src/svn-working-identity.mjs +9 -0
- package/src/svn.mjs +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
|
|
4
4
|
|
|
5
|
+
## 0.76.3 — 修复 SVN 本地导航组合共享预算
|
|
6
|
+
|
|
7
|
+
显式上下文/自动依赖/调用方扫描/关键字BASE核验共同使用1MiB来源账本和30秒截止,不再分阶段重新计费。非命中扫描仍计费,共享耗尽明确失败,不变成成功或普通候选阻断。原有更细限制保持,纯BUG修复不增加读取权限。
|
|
8
|
+
|
|
9
|
+
## 0.76.2 — 修复 SVN 普通变更来源身份遗漏
|
|
10
|
+
|
|
11
|
+
普通节点和根URL进入快照并末尾复核,切换祖先下的子文件明确阻断,正常预览后切换目录会拒绝执行;不以相同正文/revision代替来源身份。复制语义及中文转义兼容保持。共享来源预算的独立审查发现仍待修复。515项串行回归和16项采集专项通过;v0.76.1因Windows测试URL等价断言失败未发布、标签未移动,修正后v0.76.2四矩阵CI通过。原始Actions/npm/Release包149,274字节逐字节一致,双哈希/清单/冻结说明通过。
|
|
12
|
+
|
|
5
13
|
## 0.76.0 — 独立显式 SVN 标准关键字上下文
|
|
6
14
|
|
|
7
|
-
未启用autoContext时可显式选择标准关键字正常文件;原始展开正文/hash/行号保持,BASE收缩与未展开正文独立核验,全部核验正文计入原显式来源预算。预览只披露映射元数据,自定义/固定宽度和不完整展开拒绝,自动依赖和调用方关键字仍未开放。
|
|
15
|
+
未启用autoContext时可显式选择标准关键字正常文件;原始展开正文/hash/行号保持,BASE收缩与未展开正文独立核验,全部核验正文计入原显式来源预算。预览只披露映射元数据,自定义/固定宽度和不完整展开拒绝,自动依赖和调用方关键字仍未开放。514项串行回归、9项显式/依赖专项、宿主/隔离安装和四矩阵CI通过;原始Actions/npm/Release包147,614字节逐字节一致,双哈希/清单/冻结说明通过。
|
|
8
16
|
|
|
9
17
|
## 0.75.1 — 修复 SVN 根级上下文祖先校验遗漏
|
|
10
18
|
|
package/CORE-ALIGNMENT-TASKS.md
CHANGED
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
|
|
33
33
|
## 当前验证结论
|
|
34
34
|
|
|
35
|
-
- 最近完整回归:
|
|
35
|
+
- 最近完整回归:515项通过;v0.76.2 采集专项16项通过。
|
|
36
|
+
- v0.76.2 已发布普通SVN变更来源URL/UUID/revision冻结和切换祖先阻断,预览execute来源变化拒绝且零发送;v0.76.1测试编码断言失败未发布、原标签保留,修正后四矩阵CI通过。原始Actions/npm/Release包149,274字节逐字节一致,双哈希/清单/冻结正文通过。共享预算超限独立BUG仍待修复。
|
|
37
|
+
- v0.76.0 已发布独立显式标准关键字上下文,BASE展开/未展开核验计入原来源预算,原始展开正文为证据,预览元数据披露;宿主/隔离安装及四矩阵CI通过,原始Actions/npm/Release包147,614字节逐字节一致,双哈希/清单/冻结说明通过。自动依赖/调用方关键字与全面SVN其余缺口继续。
|
|
36
38
|
- v0.75.1 已发布根级上下文祖先检查BUG修复,所有节点指纹绑定祖先属性,mergeinfo变化使旧身份过期,不安全根属性拒绝;宿主/隔离安装和四矩阵CI通过,原始Actions/npm/Release包146,408字节逐字节一致,双哈希/清单/冻结正文通过。纯BUG递增Z;全面SVN缺口继续。
|
|
37
39
|
- v0.75.0 已发布独立显式SVN工作副本上下文节点/属性/URL身份核验与EOL元数据,末尾复核且离线受限,拒绝不安全来源;宿主/隔离安装与四矩阵CI通过,原始Actions/npm/Release包145,650字节逐字节一致,双哈希/清单/冻结说明通过。关键字、特殊状态与分支来源等缺口继续。
|
|
38
40
|
- v0.74.0 已发布离线自动依赖/显式导航入口/命中调用方EOL元数据与normal属性候选漏选修复,关键字/special/非正常状态仍检查;宿主/隔离安装与四矩阵CI通过,原始Actions/npm/Release包144,494字节逐字节一致,双哈希/清单/冻结正文通过。独立无autoContext显式本地属性映射、关键字与其余SVN缺口继续。
|
package/DEVELOPMENT.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
- 每个主文件的模型与检索阶段共享默认 120 秒;关联综合沿用相关主文件的生命周期,不因切换阶段重新计时。
|
|
20
20
|
- 每阶段最多三轮检索;全局最多 50 次读取请求,返回内容累计 256 KiB,单次最多 64 KiB,读取最多 200 行。
|
|
21
21
|
- Git 工作区依赖链最多三层、512 个精确路径查询、20 个候选、手动入口与自动依赖共享 1 MiB 正文;文件读取与最终复核共享 30 秒。
|
|
22
|
-
- SVN 依赖链最多三层,以精确本地路径和安全正常节点捕获;512 路径、20
|
|
22
|
+
- SVN 依赖链最多三层,以精确本地路径和安全正常节点捕获;512 路径、20 候选和上下文。工作副本显式上下文、自动依赖、调用方扫描(含非命中)及关键字BASE核验共享1 MiB来源账本与30秒含核验截止,不能切换阶段重新获得额度;切换、外部与不支持属性节点拒绝。
|
|
23
23
|
- Git 历史依赖链同样最多三层,两侧共享 512 个路径、20 个候选和上下文、1 MiB 正文及 30 秒;只读取固定对象。
|
|
24
24
|
- 引用方发现最多四个不重叠的非根目录;128 个候选正文共 1 MiB,目录索引最多 256 条/64 KiB,竞争路径最多 512 个。发现与复核共享 30 秒。
|
|
25
25
|
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
# v0.76.
|
|
1
|
+
# v0.76.3 发布说明
|
|
2
2
|
|
|
3
|
-
## SVN
|
|
3
|
+
## 修复 SVN 本地导航组合绕过共享预算
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
工作副本审查现在为本次导航创建同一份1MiB来源账本和30秒截止时间。显式上下文、自动依赖、限定调用方候选扫描,以及显式关键字BASE核验共同扣费,不再各阶段重新获得一份额度。
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- 自定义配置、固定宽度和未闭合展开仍拒绝。自动依赖与限定调用方的关键字读取没有放开;不支持的组合会明确报错。
|
|
7
|
+
- 即使每组单独不足1MiB,组合超过上限也明确失败。未命中调用方扫描正文同样计费,不以最终捕获文件大小代替实际来源读取量。
|
|
8
|
+
- 依赖内部不能把共享预算耗尽吞成普通候选阻断,必须传播失败;取消和截止时间贯穿后续阶段及核验。
|
|
9
|
+
- 原有单阶段计数、文件数、字节、输入与核验限制仍保留,不增加网络读取、不修改工作副本。
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
以前可以通过组合参数读取超额正文的请求,本版会被拒绝。请缩小contextPaths或callerScopePaths范围后重新预览,而不是把失败当作审查完成。
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
本版是BUG补丁,不新增SVN状态支持。用法见 [README](README.md),验收及剩余状态见 [支持矩阵](SVN-SUPPORT-MATRIX.md)。
|
package/SVN-SUPPORT-MATRIX.md
CHANGED
|
@@ -31,11 +31,30 @@
|
|
|
31
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
32
|
| 真正 r7314→r7315 样本 | 用户未提供隔离副本路径;本地类似复现不是该样本验收 | 获得路径后只读生成预览,不自动发送模型 |
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## 审查发现修复:本地组合共享预算(v0.76.3 待发布)
|
|
35
|
+
|
|
36
|
+
- 修复前真实组合contextPaths+callerScopePaths可读取1,140,102字节,超过1MiB但成功。修复后snapshot入口创建一个source账本/30秒signal,各本地导航阶段共同扣费,关键字BASE两份核验正文亦计费。
|
|
37
|
+
- `test/svn-navigation-budget.test.mjs` 覆盖精确1MiB边界/越界/取消和真实显式+非命中扫描组合、autoContext入口+扫描组合越界拒绝,小请求正常;与关键字/EOL组合4项通过。517项串行全量回归、4项预算/关键字/EOL组合、宿主/隔离安装通过,尚未发布。
|
|
38
|
+
- 共享预算错误在依赖链直接传播,不伪装成成功;保留原有单阶段核验/文件/字节限制。本批未新增特殊SVN状态或自动关键字支持。
|
|
39
|
+
|
|
40
|
+
## 审查发现修复:普通变更来源身份(v0.76.2 已发布)
|
|
41
|
+
|
|
42
|
+
冻结提交 `34ded1840536406fd454ab120b977ec6aa97abf7`;标签CI `38719902365`,唯一发布运行 `38720759455`,原工件 `11967005142`。原始Actions/npm/Release包149,274字节逐字节一致;SHA-256 `e7702aa23e01146c440d4e3bdf4820332d5a7e6eb5f1747ab66d854a01f33cbf`,SHA-512/清单/冻结发布说明核验通过。
|
|
43
|
+
|
|
44
|
+
v0.76.1 标签CI `38718993310` 的Windows两组因新测试比较 `~` 与 `%7E` 原始URL字符串失败,Ubuntu两组通过;未触发正式发布,标签保留不移动。修正测试按等价URL比较后使用新补丁v0.76.2重新验收。
|
|
45
|
+
|
|
46
|
+
- 已复现切换祖先的修改子文件误收及不同分支同正文/revision快照碰撞。修复为普通节点冻结URL/UUID/仓库根/revision/kind/schedule,末尾及根身份复核,预览披露workingSource。
|
|
47
|
+
- `test/svn-working-identity.test.mjs` 真实仓库证明正常来源绑定、切换子节点SVN_SWITCHED_ANCESTOR阻断、无正文授权、旧预览execute拒绝且0模型发送;与普通采集组合16项通过。中文/空格/URL转义兼容修正,515项串行全量回归、16项采集专项、宿主/隔离安装、修正测试后的四矩阵标签与发布CI通过,正式发布工件核验完成。
|
|
48
|
+
- switched状态并未因此可审查;阻断项可同ID但无正文授权,不把其视为已审核成功。
|
|
49
|
+
- 独立待修BUG:本地contextPaths+callerScopePaths来源分别计费,实测1,140,102字节超过共享1MiB仍成功;本批不声称已解决。
|
|
50
|
+
|
|
51
|
+
## 独立显式标准关键字上下文(v0.76.0 已发布)
|
|
52
|
+
|
|
53
|
+
冻结提交 `ad439af93fd48a805cd85206e2138851078f05cb`;标签CI `38714424406`,唯一发布运行 `38715124806`,原工件 `11962002956`。原始Actions/npm/Release包147,614字节逐字节一致;SHA-256 `918c3b115329986f257917a2278152b0f75968f5cb6d1fa62d112d91ed046202`,SHA-512/清单/冻结正文核验通过。
|
|
35
54
|
|
|
36
55
|
- 仅 `captureSvnExplicitContext` 显式开启节点标准关键字能力,默认调用方/依赖安全检查仍拒绝keywords;正常状态、URL/UUID/祖先指纹核验保留。
|
|
37
56
|
- 原始工作正文保留;BASE展开收缩与cat --ignore-keywords核验,两个核验正文计入同一1MiB来源/30秒预算。预览contextKeywordMappings不含正文,模型使用原展开原行号。
|
|
38
|
-
- `test/svn-explicit-keywords.test.mjs` 离线真实复现通过:原文/映射、BASE来源计费、预览0发送、模型原文、status不变和稳定快照;自定义/固定宽度拒绝,默认节点与autoContext组合继续拒绝。与显式/依赖组合9项通过,514项串行全量回归、9
|
|
57
|
+
- `test/svn-explicit-keywords.test.mjs` 离线真实复现通过:原文/映射、BASE来源计费、预览0发送、模型原文、status不变和稳定快照;自定义/固定宽度拒绝,默认节点与autoContext组合继续拒绝。与显式/依赖组合9项通过,514项串行全量回归、9项显式/依赖专项、宿主/隔离安装、四矩阵标签和发布CI通过,正式发布工件核验完成。
|
|
39
58
|
- 自动关键字依赖/调用方、特殊状态和分支来源仍未闭环。
|
|
40
59
|
|
|
41
60
|
## BUG修复:根级上下文祖先检查(v0.75.1 已发布)
|
package/package.json
CHANGED
package/src/service.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export function createReviewService(llm, { capture = captureSnapshot, now = Date
|
|
|
49
49
|
while (previews.size >= maxPreviews) previews.delete(previews.keys().next().value);
|
|
50
50
|
const previewId = randomUUID();
|
|
51
51
|
previews.set(previewId, { ...current, target, options, rounds, snapshotId: snapshot.id, expires: now() + ttlMs });
|
|
52
|
-
return { ...(options.businessRequirement!==undefined?{businessRequirement:{text:options.businessRequirement,hash:hash(options.businessRequirement),bytes:Buffer.byteLength(options.businessRequirement)}}:{}), previewId, plan, snapshotId: snapshot.id, repositoryRoot: snapshot.root, vcs: snapshot.vcs, ...(snapshot.history ? { history: snapshot.history } : {}), ...(snapshot.mergeSources ? {mergeSources:snapshot.mergeSources} : {}), model: current.route, modelSendingEnabled: allowModelSending, files: snapshot.files.map(f => ({ path: f.path, 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不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。' };
|
|
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
53
|
} finally { busy = false; }
|
|
54
54
|
},
|
|
55
55
|
async execute(args, exec) {
|
package/src/snapshot.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {captureSvnDependencyChain} from './svn-dependency-chain.mjs';
|
|
2
|
+
import {createSvnNavigationBudget} from './svn-navigation-budget.mjs';
|
|
2
3
|
import {captureSvnExplicitContext} from './svn-explicit-context.mjs';
|
|
3
4
|
import {captureDependencyChain} from './dependency-chain.mjs';
|
|
4
5
|
import {captureSvnCallers} from './svn-caller-capture.mjs';
|
|
@@ -54,18 +55,20 @@ export async function captureSnapshot(cwd, options = {}) {
|
|
|
54
55
|
const snapshot = await capture(cwd, options);
|
|
55
56
|
if (!Array.isArray(rulePaths) || rulePaths.some(name => contextPaths.includes(name))) throw new Error('Rule paths must be separate from context paths');
|
|
56
57
|
const rules = await captureProjectRules(snapshot.root, rulePaths, { signal: options.signal, files: snapshot.files });
|
|
58
|
+
const navigationBudget=type==='svn'?createSvnNavigationBudget(options.signal):undefined;
|
|
59
|
+
const navigationOptions=navigationBudget?{...options,signal:navigationBudget.signal,svnNavigationBudget:navigationBudget}:options;
|
|
57
60
|
const context = [];
|
|
58
61
|
let autoContext, index, verifyDependencies, explicitSeeds = [], indexOptions = options;
|
|
59
62
|
if (options.autoContext === true && type==='git') {
|
|
60
63
|
const chain=await captureDependencyChain(snapshot.root,snapshot.files,options);
|
|
61
64
|
explicitSeeds=chain.explicitSeeds;context.push(...chain.context);autoContext=chain.autoContext;index=chain.index;indexOptions=chain.indexOptions;
|
|
62
65
|
} else if (options.autoContext === true) {
|
|
63
|
-
const chain=await captureSvnDependencyChain(snapshot.root,snapshot.files,
|
|
66
|
+
const chain=await captureSvnDependencyChain(snapshot.root,snapshot.files,navigationOptions);
|
|
64
67
|
explicitSeeds=chain.explicitSeeds;context.push(...chain.context);autoContext=chain.autoContext;indexOptions=chain.indexOptions;verifyDependencies=chain.verify;
|
|
65
68
|
}
|
|
66
69
|
let verifyExplicit;
|
|
67
70
|
if(type==='svn'&&options.autoContext!==true&&contextPaths.length){
|
|
68
|
-
const explicit=await captureSvnExplicitContext(snapshot.root,[...new Set(contextPaths)].filter(name=>!snapshot.files.some(f=>f.path===name)).sort(),
|
|
71
|
+
const explicit=await captureSvnExplicitContext(snapshot.root,[...new Set(contextPaths)].filter(name=>!snapshot.files.some(f=>f.path===name)).sort(),navigationOptions);
|
|
69
72
|
explicitSeeds=explicit.context;verifyExplicit=explicit.verify;indexOptions=explicit.options;
|
|
70
73
|
}
|
|
71
74
|
for (const name of [...new Set(contextPaths)].sort()) {
|
|
@@ -84,7 +87,7 @@ export async function captureSnapshot(cwd, options = {}) {
|
|
|
84
87
|
}
|
|
85
88
|
let callerDiscovery, verifyCallers;
|
|
86
89
|
if (options.callerScopePaths !== undefined) {
|
|
87
|
-
const callers = await (type==='git'?captureCallers:captureSvnCallers)(snapshot.root, snapshot.files, options.callerScopePaths, options);
|
|
90
|
+
const callers = await (type==='git'?captureCallers:captureSvnCallers)(snapshot.root, snapshot.files, options.callerScopePaths, type==='svn'?navigationOptions:options);
|
|
88
91
|
verifyCallers = callers.verify;
|
|
89
92
|
callerDiscovery = { ...callers.discovery, capturedPaths: [] };
|
|
90
93
|
for (const item of callers.context) {
|
|
@@ -23,7 +23,7 @@ export async function captureSvnCallers(root, files, scopes, options={}) {
|
|
|
23
23
|
// Read failures are fatal: unread bytes are not negative search evidence.
|
|
24
24
|
const identity=await svnCallerNode(root,name,bounded);
|
|
25
25
|
const content=await readLocal(root,name,bounded);
|
|
26
|
-
scannedBytes+=content.bytes;
|
|
26
|
+
bounded.svnNavigationBudget?.charge(content.bytes);scannedBytes+=content.bytes;
|
|
27
27
|
if(scannedBytes>1024*1024)throw new Error('Caller scan byte limit exceeded');
|
|
28
28
|
scanned.push({path:name,...content,...(identity.eolStyle?{eolMapping:svnEolMapping(content.text,identity.eolStyle)}:{}),nodeFingerprint:identity.fingerprint,revision:identity.revision});
|
|
29
29
|
}
|
|
@@ -10,7 +10,7 @@ export async function captureSvnDependencyChain(root,files,options={}){
|
|
|
10
10
|
// Explicit context remains separately selected; safe normal nodes can seed navigation.
|
|
11
11
|
const explicitSeeds=[];for(const name of [...new Set(options.contextPaths??[])].sort()){
|
|
12
12
|
const changed=files.find(f=>f.path===name);if(changed){if(changed.eligibility!=='reviewable'||changed.rightExists===false)throw new Error('Context path is an excluded blocked or deleted change');continue;}
|
|
13
|
-
const node=await svnCallerNode(root,name,bounded),item={path:name,...await readLocal(root,name,bounded)};if(node.eolStyle)item.eolMapping=svnEolMapping(item.text,node.eolStyle);bytes+=item.bytes;if(bytes>1024*1024)throw new Error('SVN dependency source byte limit exceeded');nodes.push({path:name,fingerprint:node.fingerprint});explicitSeeds.push(item);visited.add(name);frontier.push({path:name,eligibility:'reviewable',right:item});
|
|
13
|
+
const node=await svnCallerNode(root,name,bounded),item={path:name,...await readLocal(root,name,bounded)};if(node.eolStyle)item.eolMapping=svnEolMapping(item.text,node.eolStyle);bounded.svnNavigationBudget?.charge(item.bytes);bytes+=item.bytes;if(bytes>1024*1024)throw new Error('SVN dependency source byte limit exceeded');nodes.push({path:name,fingerprint:node.fingerprint});explicitSeeds.push(item);visited.add(name);frontier.push({path:name,eligibility:'reviewable',right:item});
|
|
14
14
|
}
|
|
15
15
|
const onProbe=paths=>{for(const p of paths)probes.add(p);if(probes.size>512)throw new Error('SVN dependency lookup limit exceeded');};
|
|
16
16
|
for(let depth=1;depth<=3&&frontier.length;depth++){
|
|
@@ -18,8 +18,8 @@ export async function captureSvnDependencyChain(root,files,options={}){
|
|
|
18
18
|
const plans=[];for(let i=0;i<frontier.length;i+=200)plans.push(contextCandidates(frontier.slice(i,i+200),index.paths));const next=[];
|
|
19
19
|
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');
|
|
20
20
|
if(context.length+new Set(options.contextPaths??[]).size>=limit){c.status='blocked';c.reason='Context file limit exceeded';continue;}
|
|
21
|
-
try{const node=await svnCallerNode(root,entry.path,bounded),item={path:entry.path,...await readLocal(root,entry.path,bounded)};if(node.eolStyle)item.eolMapping=svnEolMapping(item.text,node.eolStyle);bytes+=item.bytes;if(bytes>1024*1024)throw new Error('SVN dependency source byte limit exceeded');context.push(item);nodes.push({path:item.path,fingerprint:node.fingerprint});c.status='captured';next.push({path:item.path,eligibility:'reviewable',right:item});}
|
|
22
|
-
catch(error){signal.throwIfAborted();if(error.message==='SVN dependency source byte limit exceeded')throw error;c.status='blocked';c.reason=error.message;}
|
|
21
|
+
try{const node=await svnCallerNode(root,entry.path,bounded),item={path:entry.path,...await readLocal(root,entry.path,bounded)};if(node.eolStyle)item.eolMapping=svnEolMapping(item.text,node.eolStyle);bounded.svnNavigationBudget?.charge(item.bytes);bytes+=item.bytes;if(bytes>1024*1024)throw new Error('SVN dependency source byte limit exceeded');context.push(item);nodes.push({path:item.path,fingerprint:node.fingerprint});c.status='captured';next.push({path:item.path,eligibility:'reviewable',right:item});}
|
|
22
|
+
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;}
|
|
23
23
|
}}frontier=next;
|
|
24
24
|
}if(frontier.length)truncated=true;
|
|
25
25
|
const verify=async()=>{for(const node of nodes)if((await svnCallerNode(root,node.path,bounded)).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();};
|
|
@@ -8,11 +8,11 @@ export async function captureSvnExplicitContext(root,names,options={}){
|
|
|
8
8
|
if(!Array.isArray(names)||names.length>20||new Set(names).size!==names.length)throw new Error('Invalid SVN explicit context selection');
|
|
9
9
|
const context=[],identities=[];let bytes=0;
|
|
10
10
|
for(const name of names){
|
|
11
|
-
const node=await svnCallerNode(root,name,bounded),content=await readLocal(root,name,bounded);bytes+=content.bytes;if(bytes>1024*1024)throw new Error('SVN explicit context source byte limit exceeded');
|
|
11
|
+
const node=await svnCallerNode(root,name,bounded),content=await readLocal(root,name,bounded);bounded.svnNavigationBudget?.charge(content.bytes);bytes+=content.bytes;if(bytes>1024*1024)throw new Error('SVN explicit context source byte limit exceeded');
|
|
12
12
|
let keywordMapping;
|
|
13
13
|
if(node.keywords){
|
|
14
14
|
const current=svnKeywordMapping(content.text,node.keywords);
|
|
15
|
-
const readBase=async(ignore)=>{const data=await checked('svn',['--non-interactive','cat',...(ignore?['--ignore-keywords']:[]),'-r','BASE','--',name+'@'],{...bounded,cwd:root,maxBytes:bounded.maxFileBytes});bytes+=data.length;if(bytes>1024*1024)throw new Error('SVN explicit context source byte limit exceeded');return decode(data);};
|
|
15
|
+
const readBase=async(ignore)=>{const data=await checked('svn',['--non-interactive','cat',...(ignore?['--ignore-keywords']:[]),'-r','BASE','--',name+'@'],{...bounded,cwd:root,maxBytes:bounded.maxFileBytes});bounded.svnNavigationBudget?.charge(data.length);bytes+=data.length;if(bytes>1024*1024)throw new Error('SVN explicit context source byte limit exceeded');return decode(data);};
|
|
16
16
|
const base=svnKeywordMapping((await readBase(false)).text,node.keywords),repository=await readBase(true);
|
|
17
17
|
if(base.text!==repository.text)throw new Error('SVN explicit BASE keyword mapping differs from repository text');
|
|
18
18
|
const {text,...metadata}=current;keywordMapping={...metadata,status:'verified-line-view',notice:'Raw working text remains evidence; BASE contraction independently verified. Keyword view is not behavioral equivalence.'};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// One source ledger/deadline per working-copy navigation capture, never per stage.
|
|
2
|
+
export function createSvnNavigationBudget(signal){
|
|
3
|
+
const sharedSignal=AbortSignal.any([AbortSignal.timeout(30000),...(signal?[signal]:[])]);let bytes=0;
|
|
4
|
+
return Object.freeze({signal:sharedSignal,charge(size){sharedSignal.throwIfAborted();if(!Number.isSafeInteger(size)||size<0)throw new Error('Invalid SVN navigation source size');bytes+=size;if(bytes>1024*1024)throw Object.assign(new Error('SVN shared navigation source byte limit exceeded'),{code:'SVN_NAVIGATION_BUDGET'});},usage(){return bytes;}});
|
|
5
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {hash} from './content.mjs';
|
|
2
|
+
export function svnWorkingIdentity(node,rootInfo,name){
|
|
3
|
+
const expected=name==='.'?rootInfo.url:rootInfo.url.replace(/\/$/,'')+'/'+name.split('/').map(encodeURIComponent).join('/');
|
|
4
|
+
const sameUrl=(a,b)=>{try{const x=new URL(a),y=new URL(b);return x.protocol===y.protocol&&x.host===y.host&&x.username===y.username&&x.password===y.password&&decodeURIComponent(x.pathname)===decodeURIComponent(y.pathname)&&x.search===y.search&&x.hash===y.hash;}catch{return false;}};
|
|
5
|
+
if(!sameUrl(node?.url,expected))throw Object.assign(new Error('SVN node URL differs from selected working-copy path; switched ancestors are unsupported'),{code:'SVN_SWITCHED_ANCESTOR'});
|
|
6
|
+
if(!rootInfo.repository?.uuid||node.repository?.uuid!==rootInfo.repository.uuid||node.repository?.root!==rootInfo.repository.root||node['wc-info']?.['wcroot-abspath']!==rootInfo['wc-info']?.['wcroot-abspath'])throw Object.assign(new Error('SVN node repository or working-copy identity mismatch'),{code:'SVN_NODE_IDENTITY'});
|
|
7
|
+
const source={url:node.url,repositoryUuid:node.repository.uuid,repositoryRoot:node.repository.root,revision:String(node['@_revision']),kind:node['@_kind'],schedule:node['wc-info']?.schedule,copyFromUrl:node['wc-info']?.['copy-from-url']??null,copyFromRevision:node['wc-info']?.['copy-from-rev']??null};
|
|
8
|
+
return {source,fingerprint:hash(JSON.stringify(source))};
|
|
9
|
+
}
|
package/src/svn.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {svnWorkingIdentity} from './svn-working-identity.mjs';
|
|
1
2
|
import { isSecretPath } from './secret-path.mjs';
|
|
2
3
|
import { svnStateIssue } from './svn-state.mjs';
|
|
3
4
|
import { snapshotMergeSources } from './svn-mergeinfo.mjs';
|
|
@@ -59,6 +60,7 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
59
60
|
if (selectedPaths) for (const name of selectedPaths) if (name !== '.') relativePath(name);
|
|
60
61
|
const info = xml(await svn(cwd, ['info', '--xml', '--', '.@'], options)).info.entry[0];
|
|
61
62
|
const root = await realpath(info['wc-info']['wcroot-abspath']);
|
|
63
|
+
const rootInfo=xml(await svn(root,['info','--xml','--','.@'],options)).info.entry[0];
|
|
62
64
|
const statusArgs = ['status', '--xml', '--ignore-externals', '--', '.@'];
|
|
63
65
|
const before = await svn(root, statusArgs, options);
|
|
64
66
|
const originalEntries = xml(before).status.target.flatMap(t => t.entry ?? []);
|
|
@@ -87,6 +89,8 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
87
89
|
if (status === 'unversioned') kind = (await lstat(path.join(root, name))).isDirectory() ? 'dir' : 'file';
|
|
88
90
|
else {
|
|
89
91
|
const node = xml(await svn(root, ['info', '--xml', '--', target(name)], options)).info.entry[0];
|
|
92
|
+
const identity=svnWorkingIdentity(node,rootInfo,name);
|
|
93
|
+
item.workingSource=identity.source;item.workingIdentity=identity.fingerprint;
|
|
90
94
|
kind = node['@_kind'];
|
|
91
95
|
if (state['@_copied'] === 'true' || node['wc-info']?.['copy-from-url']) {
|
|
92
96
|
let source = node['wc-info']?.['copy-from-url'], revision = node['wc-info']?.['copy-from-rev'];
|
|
@@ -145,7 +149,7 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
} catch (error) {
|
|
148
|
-
signal?.throwIfAborted(); item.eligibility = 'blocked'; item.reason = error.message;
|
|
152
|
+
signal?.throwIfAborted(); item.eligibility = 'blocked'; item.reason = error.message;if(error.code)item.blockCode=error.code;
|
|
149
153
|
delete item.left; delete item.right;
|
|
150
154
|
}
|
|
151
155
|
}
|
|
@@ -160,6 +164,7 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
160
164
|
}
|
|
161
165
|
}
|
|
162
166
|
for (const item of files.filter(f => f.eligibility === 'reviewable')) {
|
|
167
|
+
if(item.workingIdentity){const node=xml(await svn(root,['info','--xml','--',target(item.path)],options)).info.entry[0];if(svnWorkingIdentity(node,rootInfo,item.path).fingerprint!==item.workingIdentity)throw new Error('SVN node identity changed during capture');}
|
|
163
168
|
if (item.copySource) {
|
|
164
169
|
const node = xml(await svn(root, ['info', '--xml', '--', target(item.path)], options)).info.entry[0];
|
|
165
170
|
const identity = hash(JSON.stringify([node.url, node['@_kind'], node['@_revision'], node['wc-info']?.schedule, node['wc-info']?.['wcroot-abspath'], node['wc-info']?.['copy-from-url'] ?? null, node['wc-info']?.['copy-from-rev'] ?? null, node.repository]));
|
|
@@ -170,7 +175,9 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
170
175
|
const right = ['deleted', 'unversioned'].includes(item.rawStatus) ? {} : await properties(root, item.path, false, options);
|
|
171
176
|
if (hash(JSON.stringify([left, right])) !== item.propertyIdentity) throw new Error('Properties changed during capture');
|
|
172
177
|
}
|
|
178
|
+
const finalRootInfo=xml(await svn(root,['info','--xml','--','.@'],options)).info.entry[0];
|
|
179
|
+
if(svnWorkingIdentity(finalRootInfo,rootInfo,'.').fingerprint!==svnWorkingIdentity(rootInfo,rootInfo,'.').fingerprint)throw new Error('SVN root identity changed during capture');
|
|
173
180
|
const mergeSources = snapshotMergeSources(files);
|
|
174
|
-
const snapshot = { schemaVersion: 1, vcs: 'svn', root, repositoryUuid:
|
|
181
|
+
const snapshot = { schemaVersion: 1, vcs: 'svn', root, repositoryUuid: rootInfo.repository.uuid, repositoryUrl:rootInfo.url, ...(mergeSources.length ? {mergeSources} : {}), files };
|
|
175
182
|
return { ...snapshot, id: hash(JSON.stringify(snapshot)) };
|
|
176
183
|
}
|