@feng0531/dsh-code-review 0.59.0 → 0.60.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 +5 -1
- package/CORE-ALIGNMENT-TASKS.md +2 -1
- package/README.md +1 -1
- package/RELEASE-NOTES.md +8 -8
- package/SVN-SUPPORT-MATRIX.md +2 -1
- package/package.json +1 -1
- package/src/svn.mjs +43 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
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.60.0 — SVN 普通目录新增与删除后代覆盖
|
|
6
|
+
|
|
7
|
+
离线展开默认 status 隐藏的删除目录后代,纳入原文件数与共享元数据预算并在采集末尾复核。普通新增和删除目录只在后代显式选择且可审查时开放目录节点,后代仍逐项覆盖;凭据或阻断子项不会被目录成功掩盖。不扩展到目录复制、替换或历史模式。
|
|
8
|
+
|
|
5
9
|
## 0.59.0 — SVN 复制新增文件与移动两侧覆盖
|
|
6
10
|
|
|
7
|
-
支持同仓库、固定 copyfrom revision 的复制新增文件,旧侧明确为空目标,完整审查工作侧正文与属性;预览披露来源 URL/revision/UUID,采集末尾复核来源身份。移动作为目标新增与原路径删除分别覆盖,不冒充已验证重命名。不读复制来源正文,也不增加网络访问。凭据来源拒绝;复制替换与目录复制继续阻断,避免把来源 BASE 冒充目标旧节点。
|
|
11
|
+
支持同仓库、固定 copyfrom revision 的复制新增文件,旧侧明确为空目标,完整审查工作侧正文与属性;预览披露来源 URL/revision/UUID,采集末尾复核来源身份。移动作为目标新增与原路径删除分别覆盖,不冒充已验证重命名。不读复制来源正文,也不增加网络访问。凭据来源拒绝;复制替换与目录复制继续阻断,避免把来源 BASE 冒充目标旧节点。459 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 117,098 字节逐字节一致,双哈希、清单与冻结发布说明通过。
|
|
8
12
|
|
|
9
13
|
## 0.58.1 — SVN 根目录属性预览修复
|
|
10
14
|
|
package/CORE-ALIGNMENT-TASKS.md
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
|
|
33
33
|
## 当前验证结论
|
|
34
34
|
|
|
35
|
-
- 最近完整回归:
|
|
35
|
+
- 最近完整回归:459 项通过。
|
|
36
|
+
- v0.59.0 已交付 SVN 同仓库复制新增文件和文件移动的新增/删除独立覆盖。459 项串行回归、实际宿主、隔离安装和四组 CI 通过;原产物/npm/Release 117,098 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明通过。复制替换、目录后代和历史审查等仍未闭环。
|
|
36
37
|
- v0.58.1 已交付 SVN 根目录 mergeinfo 属性预览修复及全面支持矩阵。456 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 116,254 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明核验通过。SVN 全面扩展目标仍在进行,历史与特殊状态尚未全部支持。
|
|
37
38
|
- v0.58.0 已正式交付实验性本地评测入口及可靠性整理。454 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 113,697 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明核验通过。正常 DSH 审查行为与预算不变;尚无真实模型基准评分。
|
|
38
39
|
- v0.57.0 已正式交付对齐结论与边界收尾。449 项回归与四组 CI 通过;npm、Release 与原包 108,451 字节一致,SHA-256、SHA-512、清单与发布说明核验通过;不改变审查行为或参数。
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ commit 与 baseRevision/targetRevision 不能同时使用。oldContextPaths 只
|
|
|
89
89
|
- 不执行项目代码、构建脚本或模型生成的 shell 命令。模型只能在批准的快照中查找、搜索和读取。
|
|
90
90
|
- 常见凭据路径会被排除,但普通源码也可能含秘密。路径过滤不能代替人工检查。
|
|
91
91
|
- 二进制、不能解码的文件、链接和超限内容会被阻断,不会自动截断后假装完成。
|
|
92
|
-
- SVN 同仓库复制新增文件以空目标旧侧审查,copyfrom URL/revision/UUID
|
|
92
|
+
- SVN 同仓库复制新增文件以空目标旧侧审查,copyfrom URL/revision/UUID 在预览中披露,仅作来源信息,不自动读取来源正文,也不认定为已验证重命名。文件移动的新增/删除两侧分别覆盖。普通目录新增/删除会逐项列出后代;选择目录不会自动批准后代,未全选或子项阻断会标明覆盖不足。复制目录、替换、切换目录、外部工作副本、冲突及部分属性状态仍未支持;含 svn:keywords 的文件仍阻断。全面扩展进度见 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md)。
|
|
93
93
|
|
|
94
94
|
## 默认规模限制
|
|
95
95
|
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
# v0.
|
|
1
|
+
# v0.60.0 发布说明
|
|
2
2
|
|
|
3
|
-
## SVN
|
|
3
|
+
## SVN 普通目录新增与删除后代覆盖
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
SVN 默认 status 可能只显示删除目录父节点,隐藏其后代。本版本在离线、原文件数上限和共享元数据预算内展开这些后代,并在采集末尾复核清单。
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
7
|
+
- 普通新增和删除目录的属性、子目录、子文件分别列入快照覆盖。
|
|
8
|
+
- 指定父目录不会自动批准后代;未全选后代时明确覆盖不足。
|
|
9
|
+
- 凭据子文件继续排除,阻断或排除后代不会被父目录的成功状态掩盖。
|
|
10
|
+
- 独立测试验证仓库离线、删除子文件 BASE 正文、稳定快照、预览零发送和文件数超限。
|
|
11
11
|
|
|
12
12
|
## 当前限制
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
本版不支持目录复制或替换,未跟踪目录也不能自动读取其子文件。关键字、固定 revision 历史审查等继续按 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md) 推进;全面 SVN 目标尚未完成。
|
|
15
15
|
|
|
16
16
|
日常使用见 [README](README.md)。
|
package/SVN-SUPPORT-MATRIX.md
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
| 常规文件修改、普通新增、删除 | 已有 BASE/工作字节与属性捕获测试 | 维持并扩充组合验收 |
|
|
18
18
|
| 子目录属性变更 | 已有独立属性审查测试 | 第一批 |
|
|
19
19
|
| 根目录属性/mergeinfo | v0.58.1 已发布;独立复现后修复,指定三文件和默认预览均通过、0 模型发送、状态不变;456 项串行回归、宿主、隔离安装和四矩阵 CI 通过,原产物/npm/Release 116,254 字节逐字节一致及双哈希/清单/发布说明通过 | 第一批先发布此修复 |
|
|
20
|
-
| 复制/移动、copyfrom 及目录结构变更 |
|
|
20
|
+
| 复制/移动、copyfrom 及目录结构变更 | v0.59.0 已发布同仓库复制新增文件及文件移动的新增/删除独立覆盖:新增目标旧侧为空,copyfrom 只作来源信息,预览披露并冻结身份;离线与凭据来源拒绝测试通过。459 项串行回归、宿主、隔离安装、四矩阵 CI 通过;原产物/npm/Release 117,098 字节逐字节一致,双哈希/清单/冻结发布说明通过。复制目录及后代覆盖仍未实现 | 第二批:明确旧侧来源和后代覆盖 |
|
|
21
|
+
| 普通新增/删除目录及后代 | 本地开发补齐默认 status 隐藏的删除后代,离线递归枚举与末尾清单复核;普通结构目录只在后代全选且可审查时开放,凭据/阻断子项导致目录覆盖不足。尚待本批发布核验 | 第二批普通目录子批 |
|
|
21
22
|
| 替换、删除再新增、文件/目录类型变化 | 当前 replaced 被拒绝 | 第二批:区分 BASE 旧节点与目标节点 |
|
|
22
23
|
| 缺失、文本/属性/树冲突、阻塞 | 当前多数仅统一 blocked | 第三批:明确状态诊断;不能合成不存在的工作侧或声称审核通过 |
|
|
23
24
|
| svn:keywords、svn:eol-style | keywords 当前阻断;换行保留原文但无规范化映射 | 第三批:双侧可验证映射,保留原字节和定位 |
|
package/package.json
CHANGED
package/src/svn.mjs
CHANGED
|
@@ -22,6 +22,31 @@ async function properties(root, name, baseline, options) {
|
|
|
22
22
|
}
|
|
23
23
|
return Object.fromEntries(Object.entries(result).sort(([a], [b]) => a.localeCompare(b)));
|
|
24
24
|
}
|
|
25
|
+
// Default status hides deleted directory descendants. Expand only explicit deleted
|
|
26
|
+
// versioned roots, sharing one metadata cap and the original change-count limit.
|
|
27
|
+
async function deletedEntries(root, entries, options, limit) {
|
|
28
|
+
const byPath = new Map(entries.map(entry => [entry['@_path'].replaceAll('\\', '/'), entry]));
|
|
29
|
+
const roots = entries.filter(entry => entry['wc-status']?.['@_item'] === 'deleted').map(entry => entry['@_path'].replaceAll('\\', '/')).sort();
|
|
30
|
+
const visited = [];
|
|
31
|
+
let bytes = 0;
|
|
32
|
+
for (const name of roots) {
|
|
33
|
+
relativePath(name);
|
|
34
|
+
if (visited.some(parent => name.startsWith(parent + '/'))) continue;
|
|
35
|
+
visited.push(name);
|
|
36
|
+
const data = await svn(root, ['status', '--xml', '--verbose', '--ignore-externals', '--depth', 'infinity', '--', target(name)], { ...options, maxBytes: Math.min(options.maxBytes ?? 1024 * 1024, 1024 * 1024) });
|
|
37
|
+
bytes += data.length;
|
|
38
|
+
if (bytes > 1024 * 1024) throw new Error('SVN deleted descendant metadata limit exceeded');
|
|
39
|
+
for (const entry of xml(data).status?.target?.flatMap(t => t.entry ?? []) ?? []) {
|
|
40
|
+
const child = entry['@_path'].replaceAll('\\', '/');
|
|
41
|
+
relativePath(child);
|
|
42
|
+
if (child !== name && !child.startsWith(name + '/')) throw new Error('SVN deleted descendant escaped scope');
|
|
43
|
+
if (entry['wc-status']?.['@_item'] !== 'deleted') throw new Error('Unexpected SVN deleted descendant state');
|
|
44
|
+
if (!byPath.has(child)) byPath.set(child, entry);
|
|
45
|
+
if (byPath.size > limit) throw new Error('Change count limit exceeded');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...byPath.values()].sort((a,b) => a['@_path'] < b['@_path'] ? -1 : a['@_path'] > b['@_path'] ? 1 : 0);
|
|
49
|
+
}
|
|
25
50
|
export async function captureSvn(cwd, options = {}) {
|
|
26
51
|
const { signal, selectedPaths, maxFiles = 200, maxFileBytes = 256 * 1024 } = options;
|
|
27
52
|
if (selectedPaths) for (const name of selectedPaths) if (name !== '.') relativePath(name);
|
|
@@ -29,8 +54,9 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
29
54
|
const root = await realpath(info['wc-info']['wcroot-abspath']);
|
|
30
55
|
const statusArgs = ['status', '--xml', '--ignore-externals', '--', '.@'];
|
|
31
56
|
const before = await svn(root, statusArgs, options);
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
57
|
+
const originalEntries = xml(before).status.target.flatMap(t => t.entry ?? []);
|
|
58
|
+
if (originalEntries.length > maxFiles) throw new Error('Change count limit exceeded');
|
|
59
|
+
const entries = await deletedEntries(root, originalEntries, options, maxFiles);
|
|
34
60
|
const known = new Set(entries.map(e => e['@_path'].replaceAll('\\', '/')));
|
|
35
61
|
if (selectedPaths?.some(p => !known.has(p))) throw new Error('Selected path is not a current change');
|
|
36
62
|
const files = [];
|
|
@@ -76,7 +102,13 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
76
102
|
}
|
|
77
103
|
item.propertyIdentity = hash(JSON.stringify([leftProps, rightProps]));
|
|
78
104
|
if (kind === 'dir') {
|
|
79
|
-
if (status === 'unversioned'
|
|
105
|
+
if (status === 'unversioned') throw new Error('Unversioned directory requires explicit descendant tracking');
|
|
106
|
+
if (status === 'deleted' || status === 'added') {
|
|
107
|
+
const descendants = entries.filter(entry => entry['@_path'].replaceAll('\\', '/').startsWith(name + '/')).map(entry => entry['@_path'].replaceAll('\\', '/'));
|
|
108
|
+
if (selectedPaths && descendants.some(child => !selectedPaths.includes(child))) throw new Error('Directory descendants are not all explicitly selected');
|
|
109
|
+
item.structuralDescendants = descendants;
|
|
110
|
+
item.structuralNotice = 'Directory node coverage is separate from each descendant; no implicit descendant approval.';
|
|
111
|
+
}
|
|
80
112
|
item.left = decode(Buffer.alloc(0)); item.right = decode(Buffer.alloc(0));
|
|
81
113
|
} else {
|
|
82
114
|
item.left = decode(hasBase ? await svn(root, ['cat', '-r', 'BASE', '--', target(name)], { ...options, maxBytes: maxFileBytes }) : Buffer.alloc(0));
|
|
@@ -89,6 +121,14 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
89
121
|
}
|
|
90
122
|
const after = await svn(root, statusArgs, options);
|
|
91
123
|
if (!isDeepStrictEqual(xml(after), xml(before))) throw new Error('Working copy changed during capture');
|
|
124
|
+
if (!isDeepStrictEqual(await deletedEntries(root, xml(after).status.target.flatMap(t => t.entry ?? []), options, maxFiles), entries)) throw new Error('Deleted descendants changed during capture');
|
|
125
|
+
// A directory must not look complete when any descendant was blocked/excluded.
|
|
126
|
+
for (const item of files.filter(f => f.structuralDescendants).sort((a,b) => b.path.length - a.path.length)) {
|
|
127
|
+
if (item.structuralDescendants.some(name => files.find(f => f.path === name)?.eligibility !== 'reviewable')) {
|
|
128
|
+
item.eligibility = 'blocked'; item.reason = 'Directory descendant coverage incomplete';
|
|
129
|
+
delete item.left; delete item.right;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
92
132
|
for (const item of files.filter(f => f.eligibility === 'reviewable')) {
|
|
93
133
|
if (item.copySource) {
|
|
94
134
|
const node = xml(await svn(root, ['info', '--xml', '--', target(item.path)], options)).info.entry[0];
|