@feng0531/dsh-code-review 0.42.0 → 0.43.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 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.43.1 — 已批准正文中的唯一定义位置
6
+
7
+ 命名导入可标出已批准目标正文里唯一的 function、const、let 或 class 位置。没有调用也会进入审查输入;重复、注释、重导出和跨侧正文不猜测。默认导出与命名导入不混用,同一行竞争及续行字符串拒绝提示。不新增读取权限,也不表示完整语义绑定。v0.43.0 候选在发布前复核被替代,未作为正式交付。
8
+
5
9
  ## 0.42.0 — SVN 安全依赖链
6
10
 
7
- SVN 工作副本的自动上下文支持最多三层依赖及手动入口。精确路径探测替代全上下文索引;读取前检查节点与祖先身份,捕获后重新核验。歧义、未跟踪依赖及不安全属性不取得正文权限,各层共享数量、字节与时间上限。
11
+ SVN 工作副本的自动上下文支持最多三层依赖及手动入口。精确路径探测替代全上下文索引;读取前检查节点与祖先身份,捕获后重新核验。歧义、未跟踪依赖及不安全属性不取得正文权限,各层共享数量、字节与时间上限。已正式发布;400 项回归、实际宿主、独立安装及四组 CI 通过,公开安装包与原包一致。
8
12
 
9
13
  ## 0.41.0 — Git 历史两侧依赖链
10
14
 
@@ -23,22 +23,23 @@
23
23
 
24
24
  | 优先级 | 未完成项 | 验收重点 |
25
25
  |---|---|---|
26
- | 1 | 必要语义调用者、定义、别名及实例上下文 | 歧义不能猜测;不扩大到全仓无界扫描 |
27
- | 2 | SVN 的有界依赖链及剩余历史来源场景 | Git 历史三层链已交付;仍需更完整来源组合验收 |
26
+ | 1 | 必要语义调用者、别名及实例上下文 | 唯一定义位置已接入已批准正文;完整调用者与实例仍待验收 |
27
+ | 2 | 剩余历史来源与旧侧组合 | Git 历史与 SVN 三层链已交付;其他旧侧来源仍待验收 |
28
28
  | 3 | 其他旧侧独有来源 | 来源、版本、权限与删除场景保持一致 |
29
29
  | 4 | 剩余语言与路径专用指导 | 只提示具体缺陷和反证;不照搬风格禁令 |
30
30
  | 5 | 剩余预算组合与端到端矩阵 | 分组、窗口、检索、多次审查的终态与覆盖一致 |
31
31
  | 后置 | 真实模型质量评测 | 保存真实输出,人工裁决,分开统计失败和未完成 |
32
32
 
33
- Git 工作区三层字面依赖链已实现并通过测试。这解决了“导入转发文件后,未取得后续实现正文”的部分问题,但仍不是完整定义绑定。本批还补齐了手动上下文作为链入口,真实 Git、服务与独立安装四项测试通过,388 项全量回归和实际宿主检查通过,已随 v0.40.0 正式交付。Git 历史三层链本批已实现,5 项独立安装验收及 393 项回归、实际宿主检查通过,已随 v0.41.0 正式交付;SVN 安全三层链本批已实现,6 项独立安装验收及节点属性变化专项通过,400 项完整回归与实际宿主检查通过,待正式发布。
33
+ Git 工作区三层字面依赖链已实现并通过测试。这解决了“导入转发文件后,未取得后续实现正文”的部分问题,但仍不是完整定义绑定。本批还补齐了手动上下文作为链入口,真实 Git、服务与独立安装四项测试通过,388 项全量回归和实际宿主检查通过,已随 v0.40.0 正式交付。Git 历史三层链本批已实现,5 项独立安装验收及 393 项回归、实际宿主检查通过,已随 v0.41.0 正式交付;SVN 安全三层链本批已实现,6 项独立安装验收及节点属性变化专项通过,400 项完整回归与实际宿主检查通过,已随 v0.42.0 正式交付。唯一定义位置本批已接入审查输入,原候选 8 项独立安装验收及 403 项回归通过;发布前复核发现默认导出误匹配,已修正并增加反例,v0.43.1 待完整复验和正式发布。
34
34
 
35
35
  ## 当前验证结论
36
36
 
37
- - 最近完整回归:400 项通过。
37
+ - 最近完整回归:403 项通过。
38
38
  - 实际 DSH 工具运行时检查通过;模型响应受控,不是质量成绩。
39
39
  - v0.39.0 独立安装包的真实 Git、服务与预算测试:5 项通过。
40
40
  - v0.39.1 已正式交付,包含 Git 工作区三层依赖能力。四组 CI 通过;npm、Release 与保存原包的 91,502 字节完全一致,SHA-256、SHA-512 和校验清单通过。v0.39.0 不单独计作已核验交付。
41
- - v0.40.0 已正式交付手动上下文依赖导航。四组 CI 与唯一发布通过,npm、Release 与保存原包的 91,901 字节完全一致,双哈希及校验清单通过。准确语义绑定、SVN 依赖链和剩余旧侧来源等差距仍保留。
41
+ - v0.40.0 已正式交付手动上下文依赖导航。四组 CI 与唯一发布通过,npm、Release 与保存原包的 91,901 字节完全一致,双哈希及校验清单通过。准确语义绑定和剩余旧侧来源等差距仍保留。
42
+ - v0.42.0 已正式交付 SVN 安全三层链。400 项回归、实际宿主、独立安装与四组 CI 通过;npm、Release 与原包的 94,404 字节一致,双哈希及校验清单通过。
42
43
  - v0.41.0 已正式交付 Git 历史两侧三层链。393 项回归、实际宿主、独立安装与四组 CI 通过;npm、Release 与原包的 93,202 字节一致,双哈希及校验清单通过。
43
44
 
44
45
  ## 不在范围内
package/README.md CHANGED
@@ -67,7 +67,7 @@ DSH 的只读代码审查插件。它比较 Git 或 SVN 中的代码变更,使
67
67
 
68
68
  commit 与 baseRevision/targetRevision 不能同时使用。oldContextPaths 只能用于 Git 历史审查。
69
69
 
70
- 自动上下文和引用方发现是保守的文本导航,不是编译器分析。动态导入、宏、重载、继承和实例方法可能无法解析;找到引用不等于证明函数实际被调用。Git 历史可沿修改前后两侧分别捕获最多三层依赖;SVN 可沿安全的当前工作副本节点取得最多三层依赖,不读取 SVN 历史。
70
+ 自动上下文和引用方发现是保守的文本导航,不是编译器分析。动态导入、宏、重载、继承和实例方法可能无法解析;找到引用不等于证明函数实际被调用。Git 历史可沿修改前后两侧分别捕获最多三层依赖;SVN 可沿安全的当前工作副本节点取得最多三层依赖,不读取 SVN 历史。已批准正文中的唯一命名声明会标出位置;同名或跨侧内容不猜测。
71
71
 
72
72
  ## 如何读报告
73
73
 
package/RELEASE-NOTES.md CHANGED
@@ -1,22 +1,19 @@
1
- # v0.42.0 发布说明
1
+ # v0.43.1 发布说明
2
2
 
3
- ## 新增:SVN 有界依赖链
3
+ ## 新增:已批准正文中的唯一定义位置
4
4
 
5
- SVN 工作副本启用 autoContext 后,可以从变更文件及手动上下文沿静态导入/包含关系读取最多三层必要实现。无需新参数,不启用时不扩大上下文。
5
+ 命名导入现在可以标出目标文件里唯一的直接声明位置,包括 function、const、let 和 class。没有实际调用时也会保留该位置,供审查输入使用。
6
6
 
7
- 依赖发现改为精确本地路径探测,不枚举全工作副本上下文。目录、链接和未跟踪路径也会参与歧义判断,但不会因此获得正文读取权限。
7
+ 只检查已经批准的变更或上下文正文,不新增文件读取,也不扫描整个仓库。
8
8
 
9
- ## 安全与限制
9
+ ## 限制
10
10
 
11
- - 每个自动依赖读取前检查正常 SVN 节点、祖先目录、工作副本根和仓库身份。
12
- - 切换目录、外部工作副本、复制状态和特殊/关键字属性被拒绝。
13
- - 节点身份、竞争路径及正文在捕获结束重新核验;预览过期必须重新生成。
14
- - 所有层共享 512 个路径、20 个候选和上下文名额、1 MiB 来源正文、256 KiB 单文件及 30 秒捕获与核验时间。
15
-
16
- 仅支持当前工作副本,未新增 SVN 历史审查。依赖导航不等于编译器解析、完整语义绑定或完整调用图。
11
+ - 默认导出不能匹配命名导入;同一行重复名称、别名冲突、注释、模板、续行字符串、重导出或未导入名称不生成定义位置。
12
+ - 旧侧只使用旧正文,不会借用目标侧声明。
13
+ - 这些位置仍是语法线索,不证明语义绑定、调用可达或因果。
17
14
 
18
15
  ## 验证
19
16
 
20
- 真实 SVN 测试覆盖三层深度、循环、手动入口、数量与路径预算、未跟踪歧义、关键字属性及批准发送前过期拒绝。全量回归、实际宿主、独立安装和四组 CI 是正式发布条件,不是模型效果成绩。
17
+ 测试覆盖无调用的唯一定义、旧侧隔离、重复声明和审查输入过滤。全量回归、实际宿主、独立安装和四组 CI 是正式发布条件,不是模型效果成绩。
21
18
 
22
19
  使用方法见 [README](README.md),维护步骤见 [开发指南](DEVELOPMENT.md)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.42.0",
3
+ "version": "0.43.1",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -26,7 +26,7 @@ export function inferContextRelations(input, file) {
26
26
  const all = [primary, ...entries, ...competitors];
27
27
  if (all.length > 200) return [];
28
28
  const byId = new Map(all.map(f => [f.id, f.path]));
29
- const imports = inferImportRelations(all).filter(e => e.specifier && (e.from === 'primary' || e.to === 'primary')).map(e => ({ fromPath: byId.get(e.from), toPath: byId.get(e.to), side: e.from === 'primary' ? e.side : 'context', line: e.line, specifier: e.specifier, callSites: e.callSites, bindingHints: e.bindingHints, reason: 'relative-import:approved-context' }));
29
+ const imports = inferImportRelations(all).filter(e => e.specifier && (e.from === 'primary' || e.to === 'primary')).map(e => ({ fromPath: byId.get(e.from), toPath: byId.get(e.to), side: e.from === 'primary' ? e.side : 'context', line: e.line, specifier: e.specifier, callSites: e.callSites, bindingHints: e.bindingHints, definitionHints: e.definitionHints, reason: 'relative-import:approved-context' }));
30
30
  const cpp = inferCppRelations(all).filter(e => e.from === 'primary' || e.to === 'primary').map(e => ({ fromPath: byId.get(e.from), toPath: byId.get(e.to), side: e.from === 'primary' ? e.side : 'context', line: e.line, specifier: e.specifier, reason: e.reason, contextVersion: 'approved-current-not-historical' }));
31
31
  return [...imports, ...cpp].slice(0, 2000);
32
32
  }
@@ -17,11 +17,50 @@ export function directImportBindings(lines, importLine, calls, targetText) {
17
17
  if (positions.some(call => [...lines[call.line - 1].matchAll(occurrences)].length !== 1)) continue;
18
18
  // Every other occurrence may be declaration, parameter, mutation or escape.
19
19
  if (lines.some((line, i) => i + 1 !== importLine && !positions.some(c => c.line === i + 1) && token.test(line))) continue;
20
- const declarations = targets.map((line, i) => ({ line, number: i + 1 })).filter(item => /^\s*export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/.exec(item.line)?.[1] === imported);
20
+ const declarations = uniqueApprovedDeclarations(targets, imported);
21
21
  if (declarations.length !== 1) continue;
22
22
  const exportedToken = new RegExp('(^|[^A-Za-z0-9_$])' + imported.replaceAll('$', String.fromCharCode(92) + '$') + '([^A-Za-z0-9_$]|$)');
23
23
  if (targets.some((line, i) => i + 1 !== declarations[0].number && exportedToken.test(line))) continue;
24
- result.push(...positions.map(call => ({ imported, local, importLine, callLine: call.line, declarationLine: declarations[0].number, confidence: 'conservative-syntax-only' })));
24
+ result.push(...positions.map(call => ({ imported, local, importLine, callLine: call.line, declarationLine: declarations[0].number, declarationKind: declarations[0].kind, confidence: 'conservative-syntax-only' })));
25
+ }
26
+ return result.slice(0, 20);
27
+ }
28
+
29
+ // Only one direct declaration inside already approved text. No extra file reads.
30
+ export function uniqueApprovedDeclarations(lines, imported) {
31
+ if (!Array.isArray(lines) || lines.length > 20000 || typeof imported !== 'string' || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(imported)) return [];
32
+ const text=lines.join(String.fromCharCode(10));
33
+ if (lines.some(line=>typeof line!=='string') || Buffer.byteLength(text)>256*1024 || text.includes('/*') || text.includes(String.fromCharCode(96)) || text.includes(String.fromCharCode(92,10)) || text.includes(String.fromCharCode(92,13,10))) return [];
34
+ const forms = [
35
+ ['function', /^\s*export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/],
36
+ ['const', /^\s*export\s+(?:const|let)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=/],
37
+ ['class', /^\s*export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)(?=\s|\{)/]
38
+ ];
39
+ const found = [];
40
+ for (let i = 0; i < lines.length; i++) {
41
+ for (const [kind, pattern] of forms) {
42
+ if (pattern.exec(lines[i])?.[1] === imported) found.push({ number: i + 1, kind });
43
+ }
44
+ }
45
+ if (found.length !== 1) return [];
46
+ const occurrences=new RegExp('(^|[^A-Za-z0-9_$])'+imported.replaceAll('$','\\$')+'(?=[^A-Za-z0-9_$]|$)','g');
47
+ if ([...lines[found[0].number-1].matchAll(occurrences)].length!==1) return [];
48
+ const token = new RegExp('(^|[^A-Za-z0-9_$])' + imported.replaceAll('$', '\\$') + '([^A-Za-z0-9_$]|$)');
49
+ if (lines.some((line, i) => i + 1 !== found[0].number && token.test(line))) return [];
50
+ return found;
51
+ }
52
+
53
+ // Named imports only. Missing, duplicate, comment, or reexport names stay unresolved.
54
+ export function namedImportDefinitions(importLine, targetText) {
55
+ if (typeof targetText !== 'string' || Buffer.byteLength(targetText) > 256 * 1024 || /[\x60]|\/\*/.test(targetText)) return [];
56
+ const match = /^\s*import\s+\{([^}]+)\}\s+from\s+(['"])[^'"]+\2\s*;?\s*(?:\/\/.*)?$/.exec(importLine);
57
+ if (!match) return [];
58
+ const names = match[1].split(',').map(part => /^\s*([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*))?\s*$/.exec(part));
59
+ if (!names.length || names.some(item => !item) || new Set(names.map(item => item[1])).size !== names.length || new Set(names.map(item => item[2] ?? item[1])).size !== names.length) return [];
60
+ const lines = targetText.split(String.fromCharCode(10)), result = [];
61
+ for (const name of names) {
62
+ const found = uniqueApprovedDeclarations(lines, name[1]);
63
+ if (found.length === 1) result.push({ imported: name[1], declarationLine: found[0].number, declarationKind: found[0].kind, confidence: 'approved-unique-syntax-only' });
25
64
  }
26
65
  return result.slice(0, 20);
27
66
  }
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { directImportBindings } from './import-bindings.mjs';
2
+ import { directImportBindings, namedImportDefinitions } from './import-bindings.mjs';
3
3
  import { importedCallSites, indexStandaloneCalls } from './import-call-sites.mjs';
4
4
 
5
5
  const extensions = ['.mjs', '.cjs', '.js', '.jsx', '.ts', '.tsx'];
@@ -39,7 +39,8 @@ export function inferImportRelations(files) {
39
39
  const callSites = importedCallSites(lines, i + 1, callIndex);
40
40
  const targetText = (side === 'old' ? matches[0].left : matches[0].right)?.text;
41
41
  const bindingHints = directImportBindings(lines, i + 1, callSites, targetText);
42
- edges.push({ from: file.id, to: matches[0].id, reason: 'relative-import:' + side + ':L' + (i + 1), side, line: i + 1, specifier: match[2], callSites, bindingHints });
42
+ const definitionHints = namedImportDefinitions(lines[i], targetText);
43
+ edges.push({ from: file.id, to: matches[0].id, reason: 'relative-import:' + side + ':L' + (i + 1), side, line: i + 1, specifier: match[2], callSites, bindingHints, definitionHints });
43
44
  if (callSites.length && edges.length < 2000) edges.push({ from: file.id, to: matches[0].id, reason: 'imported-call-syntax:' + side + ':L' + callSites.map(c => c.line).join(','), side, callSites });
44
45
  if (edges.length >= 2000) return edges;
45
46
  }
@@ -36,7 +36,7 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
36
36
  const relatedFiles = input.files.filter(f => f.id !== file.id && f.eligibility === 'reviewable' && group.fileIds.includes(f.id));
37
37
  if (relatedFiles.length) {
38
38
  const relations = grouping.links.filter(link => !link.split && group.fileIds.includes(link.from) && group.fileIds.includes(link.to));
39
- const bindingRelations = inferImportRelations(input.files).filter(edge => edge.bindingHints?.length && (edge.from === file.id || edge.to === file.id) && group.fileIds.includes(edge.from) && group.fileIds.includes(edge.to)).map(({ from, to, side, bindingHints }) => ({ from, to, side, bindingHints }));
39
+ const bindingRelations = inferImportRelations(input.files).filter(edge => (edge.bindingHints?.length || edge.definitionHints?.length) && (edge.from === file.id || edge.to === file.id) && group.fileIds.includes(edge.from) && group.fileIds.includes(edge.to)).map(({ from, to, side, bindingHints, definitionHints }) => ({ from, to, side, bindingHints, ...(definitionHints?.length ? { definitionHints } : {}) }));
40
40
  const grouped = JSON.stringify({ ...base, relatedFiles, relations, bindingRelations, relationNotice: '命名和相对导入关系仅为源码线索,可能被同名局部变量遮蔽,不证明绑定、调用可达或因果;只报告主file的问题,不为relatedFiles重复生成发现。' });
41
41
  if (measure(grouped).fits) { payload = grouped; metadata.relatedFileIds = relatedFiles.map(f => f.id); }
42
42
  else metadata.groupFallback = 'input-budget';