@feng0531/dsh-code-review 0.67.0 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/CORE-ALIGNMENT-TASKS.md +3 -1
- package/README.md +2 -2
- package/RELEASE-NOTES.md +10 -9
- package/SVN-SUPPORT-MATRIX.md +2 -2
- package/index.mjs +1 -1
- package/package.json +1 -1
- package/src/service.mjs +1 -1
- package/src/svn-history-callers.mjs +60 -0
- package/src/svn-history-dependencies.mjs +83 -0
- package/src/svn-history-lookup.mjs +31 -0
- package/src/svn-history.mjs +57 -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.69.0 — SVN 固定历史限定调用方发现
|
|
6
|
+
|
|
7
|
+
callerScopePaths 支持固定 SVN 历史显式目录双侧扫描与同版本字面引用导航。只有匹配侧正文进入上下文,非匹配候选正文不发送模型;命名调用行/声明只绑定同侧已批准正文。可与autoContext组合且共享采集/精确探测/来源/上下文预算,规则与old-only限制保持。报告保留字面发现不完整状态,不冒充完整调用覆盖。
|
|
8
|
+
|
|
9
|
+
## 0.68.0 — SVN 固定历史三层自动依赖
|
|
10
|
+
|
|
11
|
+
autoContext 在 SVN 固定历史模式沿两侧 revision 独立捕获最多三层字面 import/include 依赖;用成功的有界父目录清单证明缺失,访问/正文失败直接传播,不以 stderr 猜测不存在。目录/多扩展名保留歧义,排除变更、规则、显式旧侧限制不回流。共享原上下文/读取预算并明确三层截断;预览披露有界清单元数据读取,不扫描全仓或读取工作树。历史调用方仍待实现。496 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 134,056 字节逐字节一致,双哈希、清单与冻结发布说明通过。
|
|
12
|
+
|
|
5
13
|
## 0.67.0 — SVN 工作副本显式 mergeinfo 导航
|
|
6
14
|
|
|
7
|
-
将有界 mergeinfo 来源路径/区间导航用于本地 BASE/工作侧属性变化,预览与结构化报告披露两侧元数据;复用历史解析,不扩大读取或网络范围。排除/阻断节点不回流,无效或超限保持明确状态、不输出部分导航。不是有效继承、归属或合并提交完整审查证明;全面 SVN 缺口继续保留。
|
|
15
|
+
将有界 mergeinfo 来源路径/区间导航用于本地 BASE/工作侧属性变化,预览与结构化报告披露两侧元数据;复用历史解析,不扩大读取或网络范围。排除/阻断节点不回流,无效或超限保持明确状态、不输出部分导航。不是有效继承、归属或合并提交完整审查证明;全面 SVN 缺口继续保留。489 项串行回归、宿主、隔离安装与四矩阵 CI 通过;原产物/npm/Release 130,866 字节逐字节一致,双哈希、清单与冻结发布说明通过。
|
|
8
16
|
|
|
9
17
|
## 0.66.1 — SVN 合并记录变化下的工作副本导航修复
|
|
10
18
|
|
package/CORE-ALIGNMENT-TASKS.md
CHANGED
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
|
|
33
33
|
## 当前验证结论
|
|
34
34
|
|
|
35
|
-
- 最近完整回归:
|
|
35
|
+
- 最近完整回归:496 项通过。
|
|
36
|
+
- v0.68.0 已交付固定 SVN 历史三层 autoContext,逐侧正文/来源固定、父目录有界清单确认精确节点,不吞访问失败,不读工作树。496 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 134,056 字节逐字节一致,双哈希、清单与冻结发布说明通过。历史调用方和全面 SVN 其他缺口继续。
|
|
37
|
+
- v0.67.0 已交付 SVN 工作副本 BASE/工作侧 mergeinfo 的有界显式导航、预览与报告披露,不扩大来源读取或将导航视为审查证明。489 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 130,866 字节逐字节一致,双哈希、清单与冻结发布说明通过。全面 SVN 目标仍未完成。
|
|
36
38
|
- v0.66.1 已修复根目录 mergeinfo 条目在工作副本依赖/调用方候选的非法路径失败,祖先仅合并记录属性变化经双侧比较后允许且仍入指纹。488 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 130,642 字节逐字节一致,双哈希、清单与冻结发布说明通过。全面 SVN 目标仍未完成。
|
|
37
39
|
- v0.66.0 已交付批准历史 mergeinfo 属性的有界来源/区间导航,不展开 revision、不读取来源,也不证明有效继承/归属/审查覆盖。485 项串行回归、宿主、隔离安装及四矩阵 CI 通过;原产物/npm/Release 129,866 字节逐字节一致,双哈希、清单与冻结发布说明通过。跨 URL 分支比较、自动历史链/调用方等仍未完成。
|
|
38
40
|
- v0.65.0 已交付固定 SVN 历史文件/目录类型替换,各侧节点类型/正文/属性与来源独立绑定,预览披露且后代覆盖保护保留。482 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 128,792 字节逐字节一致,双哈希、清单与冻结发布说明通过。未提交替换、自动历史链/调用方和其他全面 SVN 缺口仍待实现。
|
package/README.md
CHANGED
|
@@ -68,9 +68,9 @@ DSH 的只读代码审查插件。它比较 Git 或 SVN 中的代码变更,使
|
|
|
68
68
|
|
|
69
69
|
commit 与 baseRevision/targetRevision 不能同时使用。oldContextPaths 只能用于 Git/SVN 固定历史审查,不适用于工作副本模式。
|
|
70
70
|
|
|
71
|
-
SVN 历史模式使用独立参数,不可与 Git 参数混用。示例:`{"svnRevision":"7315","svnAllowNetwork":true}`;或 `{"svnBaseRevision":"7314","svnTargetRevision":"7315","svnAllowNetwork":true}`。只接受固定十进制 revision,不接受 HEAD、日期或相对别名。仅选择工作副本根当前仓库 URL,不支持任意远端 URL;预览会读取明确端点的仓库元数据与源码(远端需显式许可),但不发送模型。历史模式支持显式 contextPaths 与 rulePaths:上下文按目标版和已核验的旧版正文分别保存,规则只读取目标版;共享 20 上下文名额与原采集预算。oldContextPaths 可显式选择仅旧版正文,没有目标占位,选中旧变更复用原身份;不能恢复已排除/阻断/无旧侧的变更。autoContext
|
|
71
|
+
SVN 历史模式使用独立参数,不可与 Git 参数混用。示例:`{"svnRevision":"7315","svnAllowNetwork":true}`;或 `{"svnBaseRevision":"7314","svnTargetRevision":"7315","svnAllowNetwork":true}`。只接受固定十进制 revision,不接受 HEAD、日期或相对别名。仅选择工作副本根当前仓库 URL,不支持任意远端 URL;预览会读取明确端点的仓库元数据与源码(远端需显式许可),但不发送模型。历史模式支持显式 contextPaths 与 rulePaths:上下文按目标版和已核验的旧版正文分别保存,规则只读取目标版;共享 20 上下文名额与原采集预算。oldContextPaths 可显式选择仅旧版正文,没有目标占位,选中旧变更复用原身份;不能恢复已排除/阻断/无旧侧的变更。autoContext 可沿旧/新固定 revision 独立捕获最多三层字面依赖;精确子节点通过固定版父目录 immediates 清单确认,可能读取有界同目录元数据但不读取无关正文。共享 512 精确探测、512 清单条目、20 候选/上下文名额、1MiB依赖来源及原历史命令/字节/时间预算,缺失与访问失败不混淆。历史 callerScopePaths 支持限定目录的旧/新固定版字面引用发现:预览扫描两侧候选正文,包括非匹配文件,只有命中侧上下文发送模型;共享256索引条目、128扫描文件、1MiB导航来源和512精确探测及原采集预算,可与autoContext组合。不能回退到当前工作副本;声明导航只用同版本目标正文,不证明完整调用覆盖。目录新增/删除按后代逐项覆盖,凭据、链接/特殊节点与超限继续拒绝。
|
|
72
72
|
|
|
73
|
-
自动上下文和引用方发现是保守的文本导航,不是编译器分析。动态导入、宏、重载、继承和实例方法可能无法解析;找到引用不等于证明函数实际被调用。历史调用方的命名导入线索逐侧对应相同版本声明,标明 side/revision;缺少该版本的批准正文不回退另一侧。Git 历史可沿修改前后两侧分别捕获最多三层依赖;SVN
|
|
73
|
+
自动上下文和引用方发现是保守的文本导航,不是编译器分析。动态导入、宏、重载、继承和实例方法可能无法解析;找到引用不等于证明函数实际被调用。历史调用方的命名导入线索逐侧对应相同版本声明,标明 side/revision;缺少该版本的批准正文不回退另一侧。Git 历史可沿修改前后两侧分别捕获最多三层依赖;SVN 工作副本模式可沿安全的当前节点取得最多三层依赖;固定 revision 历史模式沿旧/新侧分别最多三层依赖,不把一侧正文或工作树替代另一侧。已批准正文中的唯一命名声明和唯一 default 导出会标出位置;同名、重导出或跨侧内容不猜测。命名空间导入后的独立成员调用可导航至唯一命名声明;写入、逃逸或间接调用不推定绑定,不是实例类型分析。
|
|
74
74
|
|
|
75
75
|
## 如何读报告
|
|
76
76
|
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
# v0.
|
|
1
|
+
# v0.69.0 发布说明
|
|
2
2
|
|
|
3
|
-
## SVN
|
|
3
|
+
## SVN 固定历史限定调用方发现
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`callerScopePaths` 支持固定 SVN revision 审查的显式目录字面引用发现,不读取当前工作树或任意来源分支。
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
7
|
+
- 旧/新 revision 分别扫描批准目录内候选正文,包括不匹配文件;预览零模型发送,执行仅发送命中侧上下文。
|
|
8
|
+
- 同侧命名调用行与声明导航只使用该版本已批准正文,不回退另一侧。
|
|
9
|
+
- 规则、已排除变更、显式旧侧限定与上下文名额继续约束捕获;目录竞争项保持歧义。
|
|
10
|
+
- 与 autoContext 组合,调用方上下文可继续沿各侧依赖链读取;共享 256 索引记录、128 扫描文件、512 精确探测、1MiB 导航来源和原采集命令/元数据/字节/时间预算。
|
|
11
|
+
- 报告保留字面引用发现不完整状态。主窗口完成不等于完整调用覆盖;不改为虚假的 completed。
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
跨 URL 分支比较、未提交工作副本替换、关键字映射及其他全面 SVN 项仍在 [支持矩阵](SVN-SUPPORT-MATRIX.md),目标尚未完成。
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
使用见 [README](README.md)。
|
package/SVN-SUPPORT-MATRIX.md
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
| switched、externals、稀疏/混合 revision | 多数拒绝;调用方/依赖链要求祖先 normal | 第四批:明确仓库边界与独立来源,禁止隐式跨仓读取 |
|
|
27
27
|
| 固定 revision 单提交 | v0.62.0 已发布 svnRevision:r-1/r 固定对象、UUID/URL/peg/operative revision 逐侧绑定,正文不读工作树;远端显式 svnAllowNetwork。472 项串行回归、宿主、隔离安装、四矩阵 CI 及原包/npm/Release 125,486 字节、双哈希/清单/发布说明通过。历史上下文/规则/调用方尚未支持 | 第五批:新增明确 SVN 参数与冻结身份 |
|
|
28
28
|
| 固定 revision 区间 | v0.62.0 已发布 svnBaseRevision/svnTargetRevision,固定树端点 summary+逐侧 cat/proplist,目录后代与属性纳入预算,不建临时工作树;同上述完整发布验证。分支间不同 URL 和历史上下文尚未支持 | 第五批:固定树端点与逐路径差异 |
|
|
29
|
-
| 分支复制、分支间比较、合并来源 | v0.66.0 已发布批准历史 mergeinfo 属性的来源路径/区间导航,保留非继承标记、有界不展开 revision、不读取分支源码;invalid/limited 明确且不裁剪。485 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 129,866
|
|
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
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
|
|
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项通过;502项串行全量回归通过;尚需合并autoContext组合验收、工具文档、宿主/隔离安装及发布门禁,未发布,不算该批验收完成 | 第六批:逐侧固定版本,不能回退至工作副本 |
|
|
32
32
|
| 真正 r7314→r7315 样本 | 用户未提供隔离副本路径;本地类似复现不是该样本验收 | 获得路径后只读生成预览,不自动发送模型 |
|
|
33
33
|
|
|
34
34
|
## 每批发布门禁
|
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;
|
|
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 } } },
|
|
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
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.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), ...(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: (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, eligibility: f.eligibility, reason: f.reason, ...(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), ...(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) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {validateCallerScopes} from './caller-index.mjs';
|
|
2
|
+
import {callerCandidates} from './caller-candidates.mjs';
|
|
3
|
+
import {relativePath,hash} from './content.mjs';
|
|
4
|
+
import {isSecretPath} from './secret-path.mjs';
|
|
5
|
+
|
|
6
|
+
// Fixed revision capabilities only. All failures propagate; no worktree fallback.
|
|
7
|
+
export async function svnHistoryCallers(files,history,scopes,context,{index,lookup,read,signal,maxContextFiles=20,rulePaths=[]}={}) {
|
|
8
|
+
const approved=validateCallerScopes(scopes);
|
|
9
|
+
if(!Array.isArray(files)||files.length>200||!Array.isArray(context)||context.length>maxContextFiles||!Number.isSafeInteger(maxContextFiles)||maxContextFiles<1||maxContextFiles>20||![index,lookup,read].every(fn=>typeof fn==='function')||!Array.isArray(rulePaths)||!history||!['base','target'].every(k=>typeof history[k]==='string'&&/^(0|[1-9][0-9]*)$/.test(history[k])&&Number.isSafeInteger(Number(history[k]))))throw new Error('Invalid SVN historical caller capability or budget');
|
|
10
|
+
const changed=new Set(files.map(f=>f.path)),rules=new Set(rulePaths),candidates=[],scanned=[],allProbes=new Set();
|
|
11
|
+
let records=0,scanBytes=0;
|
|
12
|
+
for(const [side,revision]of [['old',history.base],['new',history.target]]){
|
|
13
|
+
signal?.throwIfAborted();const entries=await index(approved,revision);
|
|
14
|
+
if(!Array.isArray(entries))throw new Error('Invalid SVN historical caller index');
|
|
15
|
+
const seen=new Set(),sources=[];
|
|
16
|
+
for(const entry of entries){
|
|
17
|
+
if(++records>256)throw new Error('SVN historical caller index limit exceeded');
|
|
18
|
+
const name=relativePath(entry?.path);
|
|
19
|
+
if(!approved.some(scope=>name.startsWith(scope+'/'))||seen.has(name)||!['file','dir'].includes(entry.kind))throw new Error('Unsafe SVN historical caller index');
|
|
20
|
+
seen.add(name);
|
|
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
|
+
if(scanned.length>=128)throw new Error('SVN historical caller scan count exceeded');
|
|
23
|
+
const value=await read(name,revision);signal?.throwIfAborted();
|
|
24
|
+
if(typeof value?.content?.text!=='string'||value.kind!=='file'||value.source?.operativeRevision!==revision)throw new Error('Invalid fixed SVN caller body');
|
|
25
|
+
const bodyBytes=value.content.bytes??Buffer.byteLength(value.content.text);
|
|
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');
|
|
27
|
+
scanBytes+=bodyBytes;
|
|
28
|
+
if(scanBytes>1024*1024)throw new Error('SVN historical caller scan source limit exceeded');
|
|
29
|
+
const source={path:name,...value.content,svnSource:value.source,side,revision};sources.push(source);scanned.push(source);
|
|
30
|
+
}
|
|
31
|
+
const targets=files.map(file=>{
|
|
32
|
+
const kind=side==='old'?file.oldKind:file.newKind,body=side==='old'?file.left:file.right;
|
|
33
|
+
return {...file,right:kind==='file'?body:undefined,rightExists:kind==='file'&&typeof body?.text==='string'};
|
|
34
|
+
});
|
|
35
|
+
const requested=callerCandidates(targets,sources,[]).probePaths,present=[];
|
|
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);
|
|
39
|
+
if(node!==null&&(!node||!['file','dir'].includes(node.kind)))throw new Error('Invalid fixed SVN caller probe');
|
|
40
|
+
if(node)present.push(name); // Directories remain resolution competitors.
|
|
41
|
+
}
|
|
42
|
+
const plan=callerCandidates(targets,sources,present);
|
|
43
|
+
for(const candidate of plan.candidates){
|
|
44
|
+
const receipt={...candidate,side,revision,status:'pending'};candidates.push(receipt);
|
|
45
|
+
const source=sources.find(value=>value.path===candidate.fromPath);
|
|
46
|
+
let item=context.find(value=>value.path===source.path);
|
|
47
|
+
if(item?.oldOnly&&side==='new'){receipt.status='blocked';receipt.reason='Explicit old-only context cannot approve target caller';continue;}
|
|
48
|
+
if(!item&&context.length>=maxContextFiles){receipt.status='blocked';receipt.reason='Context file limit exceeded';continue;}
|
|
49
|
+
const existing=side==='old'?item?.oldText:item?.text;
|
|
50
|
+
if(existing!==undefined&&existing!==source.text)throw new Error('Fixed SVN caller context content mismatch');
|
|
51
|
+
if(!item){item={path:source.path};context.push(item);}
|
|
52
|
+
if(side==='new')Object.assign(item,{text:source.text,hash:source.hash,bytes:source.bytes,encoding:source.encoding,revision,svnSource:source.svnSource});
|
|
53
|
+
else Object.assign(item,{oldText:source.text,oldHash:hash(source.text),oldRevision:revision,oldSvnSource:source.svnSource});
|
|
54
|
+
receipt.status='captured';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Only matched sides are retained. A new-side match never approves an old body.
|
|
58
|
+
for(const item of context)if(item.text===undefined&&item.oldText!==undefined)item.oldOnly=true;
|
|
59
|
+
return {scopes:approved,scanned:scanned.map(({path,hash,bytes,revision,side,svnSource})=>({path,hash,bytes,revision,side,svnSource})),scannedBytes:scanBytes,records,probeCount:allProbes.size,candidates,capturedPaths:[...new Set(candidates.filter(c=>c.status==='captured').map(c=>c.fromPath))].sort(),incomplete:true,notice:'Fixed-side literal references in explicit scopes only; nonmatches scanned but not captured. Not function calls, semantic binding or complete caller coverage.'};
|
|
60
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {contextCandidates} from './context-candidates.mjs';
|
|
2
|
+
import {relativePath,hash} from './content.mjs';
|
|
3
|
+
import {isSecretPath} from './secret-path.mjs';
|
|
4
|
+
|
|
5
|
+
// Injected fixed-revision capabilities must verify UUID/URL/revisions themselves.
|
|
6
|
+
// A null lookup means proven absence, never a swallowed read/authentication error.
|
|
7
|
+
export async function svnHistoryDependencies(files, history, context, {lookup, read, signal, maxContextFiles=20, rulePaths=[]}={}) {
|
|
8
|
+
if(typeof lookup!=='function'||typeof read!=='function'||!Array.isArray(files)||files.length>200||!Array.isArray(context)||!Number.isSafeInteger(maxContextFiles)||maxContextFiles<1||maxContextFiles>20||context.length>maxContextFiles||!Array.isArray(rulePaths))throw new Error('Invalid SVN dependency capabilities or budget');
|
|
9
|
+
const reserved=new Set(rulePaths);for(const name of reserved)relativePath(name);
|
|
10
|
+
if(!history||!['base','target'].every(key=>typeof history[key]==='string'&&/^(0|[1-9][0-9]*)$/.test(history[key])&&Number.isSafeInteger(Number(history[key]))))throw new Error('Invalid fixed SVN dependency revisions');
|
|
11
|
+
const cache={old:new Map(),new:new Map()},visited={old:new Set(),new:new Set()},candidates=new Map();
|
|
12
|
+
let probes=0,bytes=0,truncated=false;
|
|
13
|
+
const probe=async(side,name)=>{
|
|
14
|
+
relativePath(name);signal?.throwIfAborted();
|
|
15
|
+
if(!cache[side].has(name)){
|
|
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);
|
|
18
|
+
if(node!==null&&(!node||!['file','dir'].includes(node.kind)))throw new Error('Invalid SVN historical dependency probe result');
|
|
19
|
+
cache[side].set(name,node);
|
|
20
|
+
}
|
|
21
|
+
return cache[side].get(name);
|
|
22
|
+
};
|
|
23
|
+
const charge=text=>{bytes+=Buffer.byteLength(text);if(bytes>1024*1024)throw new Error('SVN historical dependency source limit exceeded');};
|
|
24
|
+
const nextSource=(name,text)=>({path:name,eligibility:'reviewable',right:{text}});
|
|
25
|
+
let frontier={old:[],new:[]};
|
|
26
|
+
for(const file of files){
|
|
27
|
+
if(file.eligibility!=='reviewable'||file.kind==='dir'&&file.path==='.')continue;
|
|
28
|
+
for(const side of ['old','new']){
|
|
29
|
+
const kind=side==='old'?file.oldKind:file.newKind,text=(side==='old'?file.left:file.right)?.text;
|
|
30
|
+
if(kind==='file'&&typeof text==='string'){visited[side].add(file.path);frontier[side].push(nextSource(file.path,text));}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for(const item of context)for(const side of ['old','new']){
|
|
34
|
+
const text=side==='old'?item.oldText:item.text;if(typeof text==='string'){visited[side].add(item.path);frontier[side].push(nextSource(item.path,text));charge(text);}
|
|
35
|
+
}
|
|
36
|
+
for(let depth=1;depth<=3;depth++){
|
|
37
|
+
const next={old:[],new:[]};
|
|
38
|
+
for(const side of ['old','new']){
|
|
39
|
+
signal?.throwIfAborted();
|
|
40
|
+
const requested=new Set();
|
|
41
|
+
for(let i=0;i<frontier[side].length;i+=200)contextCandidates(frontier[side].slice(i,i+200),[],{onProbe:names=>names.forEach(name=>requested.add(name))});
|
|
42
|
+
for(const name of requested)await probe(side,name);
|
|
43
|
+
const present=[...cache[side]].filter(([,node])=>node!==null).map(([name])=>name);
|
|
44
|
+
for(let i=0;i<frontier[side].length;i+=200){
|
|
45
|
+
const plan=contextCandidates(frontier[side].slice(i,i+200),present,{includeChanged:true});truncated ||= plan.truncated;
|
|
46
|
+
for(const entry of plan.candidates){
|
|
47
|
+
if(visited[side].has(entry.path))continue;visited[side].add(entry.path);
|
|
48
|
+
let candidate=candidates.get(entry.path);
|
|
49
|
+
if(!candidate){if(candidates.size>=20)throw new Error('SVN historical dependency candidate limit exceeded');candidate={...entry,depth,sides:[]};candidates.set(entry.path,candidate);}
|
|
50
|
+
const receipt={side,revision:side==='old'?history.base:history.target,status:'pending'};candidate.sides.push(receipt);
|
|
51
|
+
if(reserved.has(entry.path)){receipt.status='blocked';receipt.reason='Rule path cannot become dependency context';continue;}
|
|
52
|
+
const changed=files.find(file=>file.path===entry.path);
|
|
53
|
+
if(changed){
|
|
54
|
+
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
|
+
receipt.status='reused-change';next[side].push(nextSource(entry.path,text));continue;
|
|
57
|
+
}
|
|
58
|
+
if(isSecretPath(entry.path)){receipt.status='blocked';receipt.reason='Credential dependency excluded';continue;}
|
|
59
|
+
if(cache[side].get(entry.path)?.kind!=='file'){receipt.status='blocked';receipt.reason='Dependency is not a file';continue;}
|
|
60
|
+
let item=context.find(value=>value.path===entry.path);
|
|
61
|
+
// Explicit old-only context never grants target text, even if navigation sees it.
|
|
62
|
+
if(item?.oldOnly&&side==='new'){receipt.status='blocked';receipt.reason='Explicit old-only context cannot approve target side';continue;}
|
|
63
|
+
if(!item&&context.length>=maxContextFiles){receipt.status='blocked';receipt.reason='Context file limit exceeded';continue;}
|
|
64
|
+
const presentText=side==='old'?item?.oldText:item?.text;
|
|
65
|
+
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
|
+
if(typeof value?.content?.text!=='string'||!value.source||value.source.operativeRevision!==receipt.revision)throw new Error('Invalid fixed SVN dependency body');
|
|
68
|
+
charge(value.content.text);
|
|
69
|
+
if(!item){item={path:entry.path};context.push(item);}
|
|
70
|
+
if(side==='new')Object.assign(item,value.content,{revision:history.target,svnSource:value.source});
|
|
71
|
+
else Object.assign(item,{oldText:value.content.text,oldHash:hash(value.content.text),oldRevision:history.base,oldSvnSource:value.source});
|
|
72
|
+
receipt.status='captured';next[side].push(nextSource(entry.path,value.content.text));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
frontier=next;
|
|
77
|
+
if(!next.old.length&&!next.new.length)break;
|
|
78
|
+
if(depth===3)truncated=true;
|
|
79
|
+
}
|
|
80
|
+
for(const candidate of candidates.values())candidate.status=candidate.sides.some(side=>side.status==='blocked')?'blocked':candidate.sides.some(side=>side.status==='captured')?'captured':'reused-context';
|
|
81
|
+
for(const item of context)if(item.text===undefined&&item.oldText!==undefined)item.oldOnly=true;
|
|
82
|
+
return {candidates:[...candidates.values()].sort((a,b)=>a.path.localeCompare(b.path)),capturedPaths:[...candidates].filter(([,c])=>c.sides.some(s=>s.status==='captured')).map(([name])=>name).sort(),probeCount:probes,sourceBytes:bytes,maxDepth:3,truncated,notice:'Fixed side/revision literal navigation only; no current worktree, semantic binding or complete dependency coverage.'};
|
|
83
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {relativePath} from './content.mjs';
|
|
3
|
+
|
|
4
|
+
// A successful immediate-parent listing proves absence within the visible scope.
|
|
5
|
+
// Access errors are propagated, not interpreted using stderr or localized text.
|
|
6
|
+
export function fixedSvnLookup({list,signal,maxEntries=512}={}) {
|
|
7
|
+
if(typeof list!=='function'||!Number.isSafeInteger(maxEntries)||maxEntries<1||maxEntries>512)throw new Error('Invalid SVN historical lookup capability');
|
|
8
|
+
const directories=new Map();let entries=0;
|
|
9
|
+
const lookup=async(name,revision)=>{
|
|
10
|
+
relativePath(name);signal?.throwIfAborted();
|
|
11
|
+
if(typeof revision!=='string'||!/^(0|[1-9][0-9]*)$/.test(revision)||!Number.isSafeInteger(Number(revision)))throw new Error('Invalid SVN lookup revision');
|
|
12
|
+
const parent=path.posix.dirname(name),key=JSON.stringify([revision,parent]);
|
|
13
|
+
if(!directories.has(key)){
|
|
14
|
+
if(parent!=='.'){
|
|
15
|
+
const node=await lookup(parent,revision);
|
|
16
|
+
if(!node||node.kind!=='dir')return null;
|
|
17
|
+
}
|
|
18
|
+
const records=await list(parent,revision);signal?.throwIfAborted();
|
|
19
|
+
if(!Array.isArray(records))throw new Error('Invalid fixed SVN directory listing');
|
|
20
|
+
const children=new Map();
|
|
21
|
+
for(const record of records){
|
|
22
|
+
if(++entries>maxEntries)throw new Error('SVN historical directory entry budget exceeded');
|
|
23
|
+
if(typeof record?.name!=='string'||record.name.includes('/')||relativePath(record.name)!==record.name||!['file','dir'].includes(record.kind)||children.has(record.name))throw new Error('Invalid fixed SVN directory entry');
|
|
24
|
+
children.set(record.name,{kind:record.kind});
|
|
25
|
+
}
|
|
26
|
+
directories.set(key,children);
|
|
27
|
+
}
|
|
28
|
+
return directories.get(key).get(path.posix.basename(name))??null;
|
|
29
|
+
};
|
|
30
|
+
return {lookup,usage:()=>({directories:directories.size,entries})};
|
|
31
|
+
}
|
package/src/svn-history.mjs
CHANGED
|
@@ -5,6 +5,10 @@ import {hash,decode,relativePath} from './content.mjs';
|
|
|
5
5
|
import {isSecretPath,assertNonSecretPath} from './secret-path.mjs';
|
|
6
6
|
import {captureProjectRules} from './project-rules.mjs';
|
|
7
7
|
import {snapshotMergeSources} from './svn-mergeinfo.mjs';
|
|
8
|
+
import {fixedSvnLookup} from './svn-history-lookup.mjs';
|
|
9
|
+
import {svnHistoryDependencies} from './svn-history-dependencies.mjs';
|
|
10
|
+
import {svnHistoryCallers} from './svn-history-callers.mjs';
|
|
11
|
+
import {validateCallerScopes} from './caller-index.mjs';
|
|
8
12
|
|
|
9
13
|
const revision = value => {
|
|
10
14
|
if (typeof value !== 'string' || !/^(0|[1-9][0-9]*)$/.test(value) || !Number.isSafeInteger(Number(value))) throw new Error('SVN revision must be a fixed nonnegative decimal string');
|
|
@@ -43,7 +47,7 @@ export function mergeSvnHistoryRecord(previous, item) {
|
|
|
43
47
|
export async function captureSvnHistory(cwd,options={}) {
|
|
44
48
|
const range=svnHistoryRange(options);
|
|
45
49
|
if (options.svnAllowNetwork!==undefined && typeof options.svnAllowNetwork!=='boolean') throw new Error('Invalid SVN network opt-in');
|
|
46
|
-
if (options.
|
|
50
|
+
if (options.callerScopePaths!==undefined) validateCallerScopes(options.callerScopePaths);
|
|
47
51
|
const contextPaths=options.contextPaths??[],oldContextPaths=options.oldContextPaths??[],rulePaths=options.rulePaths??[],maxContextFiles=options.maxContextFiles??20;
|
|
48
52
|
if(!Number.isSafeInteger(maxContextFiles)||maxContextFiles<1||maxContextFiles>20||!Array.isArray(contextPaths)||contextPaths.length>maxContextFiles||new Set(contextPaths).size!==contextPaths.length||!Array.isArray(rulePaths)||rulePaths.length>4||new Set(rulePaths).size!==rulePaths.length||rulePaths.some(name=>contextPaths.includes(name)))throw new Error('Invalid SVN historical context or rule selection');
|
|
49
53
|
if(!Array.isArray(oldContextPaths)||oldContextPaths.length>20||new Set(oldContextPaths).size!==oldContextPaths.length||contextPaths.length+oldContextPaths.length>maxContextFiles||oldContextPaths.some(name=>contextPaths.includes(name)||rulePaths.includes(name)))throw new Error('Invalid SVN old context selection');
|
|
@@ -167,11 +171,62 @@ export async function captureSvnHistory(cwd,options={}) {
|
|
|
167
171
|
const rules=await captureProjectRules(root,rulePaths,{signal,files,read:async(_root,name)=>{
|
|
168
172
|
const value=await targetContent(name);if(value.content.bytes>16*1024)throw new Error('Rule file size limit exceeded');return value.content;
|
|
169
173
|
}});
|
|
174
|
+
let autoContext,callerDiscovery;
|
|
175
|
+
if(options.autoContext===true||options.callerScopePaths!==undefined){
|
|
176
|
+
const probes=fixedSvnLookup({signal,list:async(name,rev)=>{
|
|
177
|
+
const directory=await identity(name,rev);if(directory['@_kind']!=='dir')throw new Error('Historical lookup parent is not directory');
|
|
178
|
+
const data=xml(await run(['list','--xml','--depth','immediates','-r',rev,'--',at(name,rev)]));
|
|
179
|
+
const lists=data.lists?.list;
|
|
180
|
+
const one=Array.isArray(lists)?lists:[lists];
|
|
181
|
+
if(one.length!==1||!one[0]||one[0]['@_path']!==at(name,rev).slice(0,-rev.length-1))throw new Error('Historical directory listing identity mismatch');
|
|
182
|
+
return (one[0].entry??[]).map(entry=>({name:entry.name,kind:entry['@_kind']}));
|
|
183
|
+
}});
|
|
184
|
+
const exactProbes=new Map();
|
|
185
|
+
const lookup=async(name,rev)=>{
|
|
186
|
+
const key=JSON.stringify([rev,name]);
|
|
187
|
+
if(!exactProbes.has(key)){
|
|
188
|
+
if(exactProbes.size>=512)throw new Error('SVN historical shared navigation probe limit exceeded');
|
|
189
|
+
exactProbes.set(key,await probes.lookup(name,rev));
|
|
190
|
+
}
|
|
191
|
+
return exactProbes.get(key);
|
|
192
|
+
};
|
|
193
|
+
let navigationBytes=context.reduce((sum,item)=>sum+Buffer.byteLength(item.text??'')+Buffer.byteLength(item.oldText??''),0);
|
|
194
|
+
const navigationRead=async(name,rev)=>{
|
|
195
|
+
const value=await readSource(name,rev);navigationBytes+=value.content.bytes;
|
|
196
|
+
if(navigationBytes>1024*1024)throw new Error('SVN historical shared navigation source limit exceeded');
|
|
197
|
+
return value;
|
|
198
|
+
};
|
|
199
|
+
if(navigationBytes>1024*1024)throw new Error('SVN historical shared navigation source limit exceeded');
|
|
200
|
+
if(options.callerScopePaths!==undefined){
|
|
201
|
+
let indexRecords=0;
|
|
202
|
+
const index=async(scopes,rev)=>{
|
|
203
|
+
const entries=[];
|
|
204
|
+
for(const scope of scopes){
|
|
205
|
+
const node=await lookup(scope,rev);if(!node)continue;if(node.kind!=='dir')throw new Error('Historical caller scope is not directory');
|
|
206
|
+
await identity(scope,rev);
|
|
207
|
+
const data=xml(await run(['list','--xml','--depth','infinity','-r',rev,'--',at(scope,rev)]));
|
|
208
|
+
const lists=data.lists?.list,one=Array.isArray(lists)?lists:[lists];
|
|
209
|
+
if(one.length!==1||!one[0]||one[0]['@_path']!==at(scope,rev).slice(0,-rev.length-1))throw new Error('Historical caller listing identity mismatch');
|
|
210
|
+
for(const entry of one[0].entry??[]){
|
|
211
|
+
if(++indexRecords>256)throw new Error('Historical caller shared index limit exceeded');
|
|
212
|
+
const name=relativePath(entry.name);entries.push({path:scope+'/'+name,kind:entry['@_kind']});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return entries;
|
|
216
|
+
};
|
|
217
|
+
callerDiscovery=await svnHistoryCallers(files,range,options.callerScopePaths,context,{index,lookup,read:navigationRead,signal,maxContextFiles,rulePaths});
|
|
218
|
+
}
|
|
219
|
+
if(options.autoContext===true){
|
|
220
|
+
autoContext=await svnHistoryDependencies(files,range,context,{lookup,read:navigationRead,signal,maxContextFiles,rulePaths});
|
|
221
|
+
autoContext.directoryEntries=probes.usage().entries;
|
|
222
|
+
}
|
|
223
|
+
if(callerDiscovery)callerDiscovery.navigationSourceBytes=navigationBytes;
|
|
224
|
+
}
|
|
170
225
|
// Read the same fixed summary and repository identity again; never use HEAD.
|
|
171
226
|
const after=await run(['diff','--summarize','--xml','--notice-ancestry','-r',range.base+':'+range.target,'--',at('.',range.target)]);
|
|
172
227
|
if(!isDeepSummary(summary,after))throw new Error('SVN historical summary changed during capture');
|
|
173
228
|
await identity('.',range.base);await identity('.',range.target);
|
|
174
|
-
const snapshot={schemaVersion:1,vcs:'svn',root,repositoryUuid:local.repository.uuid,context,...(rules.length?{rules}:{}),history:{...range,vcs:'svn',url,repositoryUuid:local.repository.uuid,pegRevision:range.target,networkAccess,...(mergeSources.length?{mergeSources}:{}),semantics:'fixed revision tree endpoints; no merge attribution or working text'},files};
|
|
229
|
+
const snapshot={schemaVersion:1,vcs:'svn',root,repositoryUuid:local.repository.uuid,context,...(rules.length?{rules}:{}),...(autoContext?{autoContext}:{}),...(callerDiscovery?{callerDiscovery}:{}),history:{...range,vcs:'svn',url,repositoryUuid:local.repository.uuid,pegRevision:range.target,networkAccess,...(mergeSources.length?{mergeSources}:{}),semantics:'fixed revision tree endpoints; no merge attribution or working text'},files};
|
|
175
230
|
const serialized=JSON.stringify(snapshot);if(Buffer.byteLength(serialized)>maxSnapshotBytes)throw new Error('SVN historical snapshot limit exceeded');
|
|
176
231
|
return {...snapshot,id:hash(serialized)};
|
|
177
232
|
}
|