@feng0531/dsh-code-review 0.15.0 → 0.16.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
@@ -1,5 +1,17 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.16.1
4
+
5
+ - 宿主effectivePolicy为never时明确解释自动拒绝与完整文件权限不等于外发批准;保留宿主请求与唯一allowed-once授权,不修改策略。
6
+ - 补never诊断回归,全量231项;每批改动完成均更新版本并按标签CI/Actions发布核验,不只提交代码。
7
+
8
+ ## 0.16.0
9
+
10
+ - 批准范围C++头/实现命名与quoted include关系接入现有分组综合,显式context原include位置及方向/当前版本注释。
11
+ - C++零参独立调用原行导航,每侧20条,未解析宏/重载/动态,不冒充完整调用覆盖或因果。
12
+ - 保留宿主审批闭集拒绝诊断,只有literal allowed-once放行;服务级拒绝0发送回归,不变更宿主策略。
13
+ - 线程退出、调试器关闭、分配器生命周期缺陷/safe合成样本及跨文件精确凭据复核;230项全量与宿主只读离线冒烟通过,真实效果未测。
14
+
3
15
  ## 0.15.0
4
16
 
5
17
  - C#显式批准上下文补窄范围静态类型/零参方法位置线索,原行号进入主输入及输入预算,无新增读取/权限。
package/README.md CHANGED
@@ -4,12 +4,24 @@
4
4
 
5
5
  ## 当前状态
6
6
 
7
- 当前代码版本为 0.15.0,包含有界覆盖预检、显式项目规则及候选证据与反证复核;发布状态以 npm 和 GitHub Release 为准。已通过真实临时 Git/SVN 仓库测试,以及隔离进程中的真实 DSH ToolRuntime 注册验证。当前开发会话的实际预览已返回0.6.0的plan/rules字段,空变更预览与默认关闭外发已验证;已进行经宿主审批的六次小规模真实试跑,累计12次模型调用;四组合成正负例与历史修复对照符合预期,历史引入方向检出候选但引用复核未完成(partial)。不是独立盲测或整体质量证明。详情见 [试跑结果](evaluation/PILOT-RESULTS.md)。
7
+ 当前代码版本为 0.16.1,包含有界覆盖预检、显式项目规则及候选证据与反证复核;发布状态以 npm 和 GitHub Release 为准。已通过真实临时 Git/SVN 仓库测试,以及隔离进程中的真实 DSH ToolRuntime 注册验证。当前开发会话的实际预览已返回0.6.0的plan/rules字段,空变更预览与默认关闭外发已验证;已进行经宿主审批的六次小规模真实试跑,累计12次模型调用;四组合成正负例与历史修复对照符合预期,历史引入方向检出候选但引用复核未完成(partial)。不是独立盲测或整体质量证明。详情见 [试跑结果](evaluation/PILOT-RESULTS.md)。
8
8
 
9
9
  ## 工作区下多个子仓库(0.11.0)
10
10
 
11
11
  当会话目录不是仓库而其子目录含Git/SVN仓库时,预览可指定 `repositoryPath`,例如会话位于工作区根时传 `{ "repositoryPath": "c3animation" }`。该参数必须是工作区相对路径(正斜杠),指向明确仓库根;不能使用绝对路径、上级路径或链接目录。文件、上下文、规则路径仍相对此次选定仓库。多个子仓库分别预览和审批执行,不混用快照;execute只接收previewId,不能另改目标。省略参数保持原行为。使用此参数需宿主安装0.11.0或更新版本;仓库发布不会自动更新当前宿主。
12
12
 
13
+ ## 完整文件权限与审批策略
14
+
15
+ 宿主文件权限和approval policy为独立设置。已核对安装宿主实现:`approval.effectivePolicy(session) === "never"` 时,无论文件权限是否 `danger-full-access`,主动审批请求直接返回rejected且不弹卡片;never不是自动批准。插件保留独立外发批准,不以文件权限代替。若想实际审核,需要用户选择approval policy为ask的宿主权限预设并一次性批准;宿主 `/permission` 命令(不带参数)可列出当前/可用预设,具体映射依宿主配置,不假定哪个名字为ask。本插件不修改该策略。
16
+
17
+ ## C++关联与审批诊断(0.16.0)
18
+
19
+ 批准文件中的同目录唯一头/实现命名与字面量引号include进入现有关联分组,最多4文件的综合仍共享原读取/调用/生命周期预算。显式context注释保留from/to与include原行,current内容不冒充old历史。无需扫描新文件或执行编译器;系统include、构建搜索路径、条件宏和不支持文本不解析,同目录命名不证明绑定。
20
+
21
+ 主输入提供每侧最多20条零参独立调用原位置,含 `worker.join()`、`debugger->Close()`、`Allocator::Shutdown()` 等语法导航;标记未解析,不证明宏展开、重载选择、动态分派或执行顺序,也不表示全部调用覆盖。其他源码须批准范围内read获取。线程退出/调试器关闭/分配器生命周期合成缺陷与safe反例仅验证明确抽象契约及离线证据协议,不是编译或真实模型质量成绩。
22
+
23
+ 审批只有宿主字面量allowed-once放行;rejected/cancelled/unavailable分别诊断,缺失服务及异常值拒绝。不绕过用户策略,confirmed=true不代替批准;拒绝表示没有发送代码/没有审查,不是通过。检查执行会话的审批策略与UI可用性并重新预览,不自动修改宿主配置、安装或重启。本版本没有解决用户另会话具体拒绝配置,也没有完成其实际C++改动审查。
24
+
13
25
  ## C#显式上下文位置线索(0.15.0)
14
26
 
15
27
  仅注释已批准.cs context,不增加读取或覆盖。支持file-scoped同namespace、静态class、public static零参void/int/bool/string方法与独立Type.Method()语法;保留调用/类型/方法原始行号。目标括号须平衡、方法直属类型;重载、同名竞争者(包括不支持语法)、局部遮蔽、using/partial/继承或动态等不推定绑定。有字符串/注释/预处理等不支持文本保守不生成线索,不影响普通源码审查。当前context明确不是old历史内容。不执行编译器、工程或用户源码,不冒充C++适配、语义绑定或可达性证明,线索不足仍须模型说明证据限制。
package/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { requestModelApproval } from './src/model-approval.mjs';
1
2
  import { createReviewService } from './src/service.mjs';
2
3
  export const name = 'dsh-code-review';
3
4
  export const inject = ['tools', 'llm', 'approval'];
@@ -5,10 +6,10 @@ export const inject = ['tools', 'llm', 'approval'];
5
6
  export function apply(ctx, config = {}) {
6
7
  const service = createReviewService(ctx.llm, {
7
8
  allowModelSending: config.allowModelSending === true,
8
- authorize: async ({ snapshot, route, exec }) => (await ctx.approval.request({
9
+ authorize: async ({ snapshot, route, exec }) => requestModelApproval(ctx.approval, {
9
10
  agent: exec.agent, toolName: 'code_review_execute', signal: exec.signal,
10
11
  reason: '只读代码审查将发送代码给 ' + route.provider + '/' + route.model + '。仓库:' + snapshot.root + ';快照:' + snapshot.id + ';文件:' + JSON.stringify(snapshot.files.filter(f => f.eligibility === 'reviewable').map(f => f.path)) + ';上下文:' + JSON.stringify(snapshot.context.map(c => c.path)) + ';项目规则(全文将发送):' + JSON.stringify((snapshot.rules ?? []).map(({ path, hash }) => ({ path, hash }))),
11
- })) === 'allowed-once',
12
+ }),
12
13
  });
13
14
  const output = {
14
15
  schema: { type: 'object', additionalProperties: false, properties: { json: { type: 'string' }, markdown: { type: 'string' } }, required: ['json', 'markdown'] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -1,3 +1,4 @@
1
+ import { inferCppRelations } from './cpp-relations.mjs';
1
2
  import { inferImportRelations } from './import-relations.mjs';
2
3
 
3
4
  // Context entries are already sent with every primary. These annotations add
@@ -12,5 +13,7 @@ export function inferContextRelations(input, file) {
12
13
  const all = [primary, ...entries, ...competitors];
13
14
  if (all.length > 200) return [];
14
15
  const byId = new Map(all.map(f => [f.id, f.path]));
15
- return 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' }));
16
+ 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' }));
17
+ 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' }));
18
+ return [...imports, ...cpp].slice(0, 2000);
16
19
  }
@@ -0,0 +1,14 @@
1
+ // Syntactic zero-argument lifecycle call navigation only, not resolved bindings.
2
+ export function cppCallSites(text) {
3
+ if (typeof text !== 'string' || Buffer.byteLength(text) > 256*1024 || /\/\*|R"|\\\r?\n/.test(text)) return [];
4
+ const lines = text.split(String.fromCharCode(10));
5
+ if (lines.some(line => /^\s*#\s*(?:define|undef|if|ifdef|ifndef|elif|else|endif)\b/.test(line))) return [];
6
+ const sites = [];
7
+ for (let i=0;i<lines.length;i++) {
8
+ const match = /^\s*((?:[A-Za-z_][A-Za-z0-9_]*(?:::|->|\.))*[A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s*;\s*(?:\/\/.*)?$/.exec(lines[i]);
9
+ if (!match) continue;
10
+ sites.push({ line:i+1, expression:match[1], confidence:'syntax-only-unresolved', notice:'May be macro, overload or dynamic dispatch; no binding or order guarantee.' });
11
+ if (sites.length === 20) break;
12
+ }
13
+ return sites;
14
+ }
@@ -0,0 +1,38 @@
1
+ import path from 'node:path';
2
+ const cpp = p => /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/.test(p);
3
+ // Same-directory names and literal quoted includes are navigation hints, not builds.
4
+ export function inferCppRelations(files) {
5
+ if (!Array.isArray(files) || files.length > 200) throw new Error('Invalid C++ relation input');
6
+ const byPath = new Map(), ids = new Set();
7
+ for (const file of files) {
8
+ if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || file.path.includes('\\') || file.path.includes(':') || file.path.startsWith('/') || file.path.split('/').some(s => !s || s === '.' || s === '..') || byPath.has(file.path) || !['reviewable','excluded','blocked'].includes(file.eligibility)) throw new Error('Invalid C++ relation file');
9
+ ids.add(file.id); byPath.set(file.path,file);
10
+ }
11
+ const edges = [];
12
+ for (const file of files.filter(f => f.eligibility === 'reviewable' && cpp(f.path))) {
13
+ const stem = file.path.replace(/\.[^.]+$/, '');
14
+ const headers = files.filter(f => f.path.replace(/\.[^.]+$/, '') === stem && /\.(?:h|hh|hpp|hxx)$/.test(f.path));
15
+ const implementations = files.filter(f => f.path.replace(/\.[^.]+$/, '') === stem && /\.(?:c|cc|cpp|cxx)$/.test(f.path));
16
+ if (headers.length === 1 && implementations.length === 1 && headers[0].eligibility === 'reviewable' && implementations[0].eligibility === 'reviewable' && file.id === implementations[0].id) edges.push({from:file.id,to:headers[0].id,reason:'cpp-header-implementation-name'});
17
+ for (const side of ['old','new']) {
18
+ if (side === 'old' && file.oldPath && file.oldPath !== file.path) continue;
19
+ const text = (side === 'old' ? file.left : file.right)?.text;
20
+ if (typeof text !== 'string' || Buffer.byteLength(text) > 256*1024 || /\/\*|\\\r?\n|R"/.test(text)) continue;
21
+ const lines = text.split(String.fromCharCode(10));
22
+ // Conditional/macro includes are not resolved without a build configuration.
23
+ if (lines.some(line => /^\s*#\s*(?:if|ifdef|ifndef|elif|else|endif|define|undef)\b/.test(line))) continue;
24
+ for (let i=0;i<lines.length;i++) {
25
+ const match = /^\s*#\s*include\s+"([^"\\]+)"\s*(?:\/\/.*)?$/.exec(lines[i]);
26
+ if (!match || match[1].startsWith('/') || match[1].includes(':')) continue;
27
+ const targetPath = path.posix.normalize(path.posix.join(path.posix.dirname(file.path),match[1]));
28
+ if (targetPath.startsWith('../') || targetPath === '..') continue;
29
+ if (side === 'old' && files.some(f => f.oldPath && f.oldPath !== f.path && (f.oldPath === targetPath || f.path === targetPath))) continue;
30
+ const target = byPath.get(targetPath);
31
+ if (!target || target.id === file.id || target.eligibility !== 'reviewable' || !cpp(target.path)) continue;
32
+ edges.push({from:file.id,to:target.id,side,line:i+1,specifier:match[1],reason:'cpp-quoted-include:'+side+':L'+(i+1)});
33
+ if (edges.length >= 2000) return edges;
34
+ }
35
+ }
36
+ }
37
+ return edges.slice(0,2000);
38
+ }
@@ -1,8 +1,9 @@
1
+ import { inferCppRelations } from './cpp-relations.mjs';
1
2
  import { inferImportRelations } from './import-relations.mjs';
2
3
 
3
4
  export function inferFileRelations(files) {
4
5
  // Keep the existing group edge budget; naming hints have priority at the cap.
5
- return [...inferTestRelations(files), ...inferImportRelations(files)].slice(0, 2000);
6
+ return [...inferTestRelations(files), ...inferImportRelations(files), ...inferCppRelations(files)].slice(0, 2000);
6
7
  }
7
8
 
8
9
  // Conservative naming hints only. No filesystem access or dependency claims.
@@ -0,0 +1,12 @@
1
+ // Host is the only authority. Never reinterpret truthy values as a grant.
2
+ export async function requestModelApproval(approval, request) {
3
+ if (typeof approval?.request !== 'function') throw new Error('Model sending approval unavailable: host approval service missing; no code sent. Enable host approval before a new preview; confirmed=true is not a grant.');
4
+ const outcome = await approval.request(request);
5
+ if (outcome === 'allowed-once') return true;
6
+ const status = ['rejected', 'cancelled', 'unavailable'].includes(outcome) ? outcome : 'invalid-outcome';
7
+ let policy;
8
+ // Diagnostic only: policy never changes the grant decision or host audit request.
9
+ try { if (typeof approval.effectivePolicy === 'function') policy = approval.effectivePolicy(request.agent?.session); } catch {}
10
+ const policyHint = policy === 'never' ? ' Host approval policy=never: approval requests are automatically rejected; full filesystem access does not grant model sending. Select a host permission preset with approval policy=ask, then approve once.' : '';
11
+ throw new Error('Model sending approval denied or unavailable (' + status + '):' + policyHint + ' no code sent, review not performed. Check host approval policy/UI availability; obtain allowed-once after a new preview. confirmed=true does not replace host approval.');
12
+ }
@@ -1,3 +1,4 @@
1
+ import { cppCallSites } from './cpp-call-sites.mjs';
1
2
  import { inferCsharpContext } from './csharp-context.mjs';
2
3
  import { inferImportRelations } from './import-relations.mjs';
3
4
  import { inferContextRelations } from './context-relations.mjs';
@@ -14,7 +15,11 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
14
15
  const contextRelations = inferContextRelations(input, file);
15
16
  if (contextRelations.length) {
16
17
  base.contextRelations = contextRelations;
17
- base.contextRelationNotice = '导入与调用位置仅为语法线索;可能被同名局部变量遮蔽,不证明绑定、可达性或因果。';
18
+ base.contextRelationNotice = '导入/include/命名与调用位置仅为语法线索;可能被宏、构建搜索路径或同名局部变量影响,不证明绑定、可达性或因果。context为批准当前文本,不是old历史内容。';
19
+ }
20
+ if (/\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/.test(file.path)) {
21
+ const calls = ['old','new'].flatMap(side => cppCallSites((side === 'old' ? file.left : file.right).text).map(site => ({ ...site, side })));
22
+ if (calls.length) { base.cppCallSites = calls; base.cppCallNotice = '最多20条零参语法位置/侧,仅导航子集;不表示全部调用覆盖,不证明宏展开、重载选择、动态分派或执行顺序。其他源码须在批准范围内读取。'; }
18
23
  }
19
24
  const csharpHints = inferCsharpContext(input, file);
20
25
  if (csharpHints.length) { base.csharpContextHints = csharpHints; base.csharpContextNotice = '仅批准当前context的C#静态类型/方法语法位置;不证明语义绑定或执行可达,也不是旧侧历史上下文。歧义不推定,线索不增加审查覆盖。'; }