@feng0531/dsh-code-review 0.14.0 → 0.16.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.16.0
4
+
5
+ - 批准范围C++头/实现命名与quoted include关系接入现有分组综合,显式context原include位置及方向/当前版本注释。
6
+ - C++零参独立调用原行导航,每侧20条,未解析宏/重载/动态,不冒充完整调用覆盖或因果。
7
+ - 保留宿主审批闭集拒绝诊断,只有literal allowed-once放行;服务级拒绝0发送回归,不变更宿主策略。
8
+ - 线程退出、调试器关闭、分配器生命周期缺陷/safe合成样本及跨文件精确凭据复核;230项全量与宿主只读离线冒烟通过,真实效果未测。
9
+
10
+ ## 0.15.0
11
+
12
+ - C#显式批准上下文补窄范围静态类型/零参方法位置线索,原行号进入主输入及输入预算,无新增读取/权限。
13
+ - 不支持目标仍保留为歧义竞争者,拒绝重载、同名遮蔽及畸形/类型外/嵌套声明;current context不冒充old历史。
14
+ - 219项全量与宿主只读审批离线冒烟通过;仅语法提示,不是通用语义绑定或C++适配,不宣称真实模型质量。
15
+
3
16
  ## 0.14.0
4
17
 
5
18
  - 同一批准仓库关联组追加有界跨文件综合,2–4文件,目录精确编辑与本阶段原文read覆盖,不静默截断规则/context。
package/README.md CHANGED
@@ -4,12 +4,24 @@
4
4
 
5
5
  ## 当前状态
6
6
 
7
- 当前代码版本为 0.14.0,包含有界覆盖预检、显式项目规则及候选证据与反证复核;发布状态以 npm 和 GitHub Release 为准。已通过真实临时 Git/SVN 仓库测试,以及隔离进程中的真实 DSH ToolRuntime 注册验证。当前开发会话的实际预览已返回0.6.0的plan/rules字段,空变更预览与默认关闭外发已验证;已进行经宿主审批的六次小规模真实试跑,累计12次模型调用;四组合成正负例与历史修复对照符合预期,历史引入方向检出候选但引用复核未完成(partial)。不是独立盲测或整体质量证明。详情见 [试跑结果](evaluation/PILOT-RESULTS.md)。
7
+ 当前代码版本为 0.16.0,包含有界覆盖预检、显式项目规则及候选证据与反证复核;发布状态以 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
+ ## C++关联与审批诊断(0.16.0)
14
+
15
+ 批准文件中的同目录唯一头/实现命名与字面量引号include进入现有关联分组,最多4文件的综合仍共享原读取/调用/生命周期预算。显式context注释保留from/to与include原行,current内容不冒充old历史。无需扫描新文件或执行编译器;系统include、构建搜索路径、条件宏和不支持文本不解析,同目录命名不证明绑定。
16
+
17
+ 主输入提供每侧最多20条零参独立调用原位置,含 `worker.join()`、`debugger->Close()`、`Allocator::Shutdown()` 等语法导航;标记未解析,不证明宏展开、重载选择、动态分派或执行顺序,也不表示全部调用覆盖。其他源码须批准范围内read获取。线程退出/调试器关闭/分配器生命周期合成缺陷与safe反例仅验证明确抽象契约及离线证据协议,不是编译或真实模型质量成绩。
18
+
19
+ 审批只有宿主字面量allowed-once放行;rejected/cancelled/unavailable分别诊断,缺失服务及异常值拒绝。不绕过用户策略,confirmed=true不代替批准;拒绝表示没有发送代码/没有审查,不是通过。检查执行会话的审批策略与UI可用性并重新预览,不自动修改宿主配置、安装或重启。本版本没有解决用户另会话具体拒绝配置,也没有完成其实际C++改动审查。
20
+
21
+ ## C#显式上下文位置线索(0.15.0)
22
+
23
+ 仅注释已批准.cs context,不增加读取或覆盖。支持file-scoped同namespace、静态class、public static零参void/int/bool/string方法与独立Type.Method()语法;保留调用/类型/方法原始行号。目标括号须平衡、方法直属类型;重载、同名竞争者(包括不支持语法)、局部遮蔽、using/partial/继承或动态等不推定绑定。有字符串/注释/预处理等不支持文本保守不生成线索,不影响普通源码审查。当前context明确不是old历史内容。不执行编译器、工程或用户源码,不冒充C++适配、语义绑定或可达性证明,线索不足仍须模型说明证据限制。
24
+
13
25
  ## 批准快照内跨文件综合(0.14.0)
14
26
 
15
27
  关联组最多4个批准主文件、至少2个文本变化时追加跨文件综合。目录保留完整精确编辑原位置,不作为源码证据;每条发现须引用至少2文件的 `interaction.editRefs:[{fileId,editId}]`,主attribution与主引用一致,原锚重叠主编辑,每个引用编辑至少一非空侧须本综合生成read完整覆盖。共享批准reader/全局调用额度,嵌入组最后主文件原超时,不重置生命周期;预检包含额外初始调用,检索复核另计。interactions独立记录pending/completed/blocked,跨组和未完成明确限制。不合并多文件候选,不自动扩文件/仓库。源码/规则/context仍不可信。真实模型样本未跑,安全反例空输出模拟不代表模型查准率;拒绝审批表示未审查不是通过。
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.14.0",
3
+ "version": "0.16.0",
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
+ }
@@ -0,0 +1,46 @@
1
+ // Approved text only, no compiler/module lookup. Intentionally narrow syntax hints.
2
+ const safe = text => typeof text === 'string' && Buffer.byteLength(text) <= 256 * 1024 && !/["'\x60#]|\/\/|\/\*/.test(text);
3
+ const namespaceOf = text => { const matches = [...text.matchAll(/^\s*namespace\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;\s*$/gm)]; return matches.length === 1 ? matches[0][1] : null; };
4
+ export function inferCsharpContext(input, primary) {
5
+ if (!primary.path.endsWith('.cs')) return [];
6
+ const contexts = (input.context ?? []).filter(c => c.path.endsWith('.cs') && !input.files.some(f => f.path === c.path));
7
+ const result = [];
8
+ for (const side of ['old', 'new']) {
9
+ const source = (side === 'old' ? primary.left : primary.right)?.text;
10
+ if (!safe(source) || /\busing\b|\bdynamic\b/.test(source)) continue;
11
+ const ns = namespaceOf(source); if (!ns) continue;
12
+ const lines = source.split(String.fromCharCode(10));
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const call = /^\s*(?:await\s+)?([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s*;\s*$/.exec(lines[i]);
15
+ if (!call) continue;
16
+ const [, type, method] = call;
17
+ const token = new RegExp('\\b' + type + '\\b');
18
+ // Other occurrences may introduce a variable/type that shadows the qualifier.
19
+ if (lines.some((line,j) => j !== i && token.test(line))) continue;
20
+ // Unsupported declarations still compete; filtering them out must not create uniqueness.
21
+ const competitors = contexts.filter(c => typeof c.text !== 'string' || token.test(c.text));
22
+ if (competitors.length !== 1 || input.files.some(f => f !== primary && f.path.endsWith('.cs') && f.path.split('/').at(-1) === type + '.cs')) continue;
23
+ const candidates = competitors.filter(c => safe(c.text) && namespaceOf(c.text) === ns && !/\busing\b|\bpartial\b|\bvirtual\b|\boverride\b|\binterface\b/.test(c.text));
24
+ const declarations = [];
25
+ for (const context of candidates) {
26
+ const targetLines = context.text.split(String.fromCharCode(10));
27
+ const classes = targetLines.map((line,j) => ({ line, number:j+1 })).filter(item => /^\s*(?:public|internal)\s+static\s+class\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{?\s*$/.exec(item.line)?.[1] === type);
28
+ // Only one type per file; no nesting, inheritance, generics or partial resolution.
29
+ if (classes.length !== 1 || (context.text.match(/\bclass\b|\bstruct\b|\brecord\b/g) ?? []).length !== 1) continue;
30
+ const occurrences = targetLines.filter(line => new RegExp('\\b' + method + '\\s*\\(').test(line));
31
+ const methods = targetLines.map((line,j) => ({ line, number:j+1 })).filter(item => new RegExp('^\\s*public\\s+static\\s+(?:void|int|bool|string)\\s+' + method + '\\s*\\(\\s*\\)\\s*(?:\\{|$)').test(item.line));
32
+ let depth = 0, typeDepth, methodDepth, malformed = false;
33
+ for (let j = 0; j < targetLines.length; j++) {
34
+ if (j + 1 === classes[0].number) typeDepth = depth;
35
+ if (j + 1 === methods[0]?.number) methodDepth = depth;
36
+ for (const character of targetLines[j]) { if (character === '{') depth++; if (character === '}') depth--; if (depth < 0) malformed = true; }
37
+ }
38
+ if (malformed || depth !== 0 || typeDepth !== 0 || methodDepth !== 1 || methods[0]?.number <= classes[0].number) continue;
39
+ if (occurrences.length === 1 && methods.length === 1) declarations.push({ contextPath:context.path, typeLine:classes[0].number, declarationLine:methods[0].number });
40
+ }
41
+ if (declarations.length === 1) result.push({ primaryPath:primary.path, side, contextVersion:'approved-current-not-historical', callLine:i+1, namespace:ns, type, method, ...declarations[0], confidence:'conservative-syntax-only' });
42
+ if (result.length >= 20) return result;
43
+ }
44
+ }
45
+ return result;
46
+ }
@@ -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,8 @@
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
+ throw new Error('Model sending approval denied or unavailable (' + status + '): 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.');
8
+ }
@@ -1,3 +1,5 @@
1
+ import { cppCallSites } from './cpp-call-sites.mjs';
2
+ import { inferCsharpContext } from './csharp-context.mjs';
1
3
  import { inferImportRelations } from './import-relations.mjs';
2
4
  import { inferContextRelations } from './context-relations.mjs';
3
5
  import { prepareWindowSynthesis } from './window-synthesis.mjs';
@@ -13,8 +15,14 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
13
15
  const contextRelations = inferContextRelations(input, file);
14
16
  if (contextRelations.length) {
15
17
  base.contextRelations = contextRelations;
16
- base.contextRelationNotice = '导入与调用位置仅为语法线索;可能被同名局部变量遮蔽,不证明绑定、可达性或因果。';
18
+ base.contextRelationNotice = '导入/include/命名与调用位置仅为语法线索;可能被宏、构建搜索路径或同名局部变量影响,不证明绑定、可达性或因果。context为批准当前文本,不是old历史内容。';
17
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条零参语法位置/侧,仅导航子集;不表示全部调用覆盖,不证明宏展开、重载选择、动态分派或执行顺序。其他源码须在批准范围内读取。'; }
23
+ }
24
+ const csharpHints = inferCsharpContext(input, file);
25
+ if (csharpHints.length) { base.csharpContextHints = csharpHints; base.csharpContextNotice = '仅批准当前context的C#静态类型/方法语法位置;不证明语义绑定或执行可达,也不是旧侧历史上下文。歧义不推定,线索不增加审查覆盖。'; }
18
26
  let payload = JSON.stringify(base);
19
27
  const measure = value => initialInputBudget({ instructions, input: value }, scope, maxInputBytes);
20
28
  const metadata = {};