@feng0531/dsh-code-review 0.12.0 → 0.14.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 +14 -0
- package/README.md +9 -1
- package/package.json +1 -1
- package/src/context-relations.mjs +1 -1
- package/src/coverage-plan.mjs +4 -1
- package/src/finding-groups.mjs +2 -0
- package/src/import-bindings.mjs +23 -0
- package/src/import-relations.mjs +4 -1
- package/src/interaction-findings.mjs +36 -0
- package/src/interaction-input.mjs +18 -0
- package/src/primary-input.mjs +3 -1
- package/src/review.mjs +16 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
- 同一批准仓库关联组追加有界跨文件综合,2–4文件,目录精确编辑与本阶段原文read覆盖,不静默截断规则/context。
|
|
6
|
+
- 文件限定编辑引用、主归因一致与原锚校验,保留原候选,不参与模糊去重。
|
|
7
|
+
- 综合共享批准reader、全局调用及组末主文件生命周期;预检一致,超时取消/预算不足和跨组未覆盖明确限制。
|
|
8
|
+
- 215项回归及宿主只读审批离线冒烟通过;增加缺陷/safe样本判读协议,不宣称真实模型质量。
|
|
9
|
+
|
|
10
|
+
## 0.13.0
|
|
11
|
+
|
|
12
|
+
- 增加批准范围命名相对import别名→简单调用→同侧直接export function的保守位置线索,组输入及显式context保留原始行号。
|
|
13
|
+
- 调用名其他出现、目标同名变更、重复导出/重导出及不支持语法不生成绑定线索,不冒充通用语义图或因果证明。
|
|
14
|
+
- 不增加读取权限、组成员、模型调用或审查覆盖;实际主输入传输、歧义拒绝、全量及宿主只读审批离线验收通过。
|
|
15
|
+
- 本轮质量增强到此冻结,最终版本发布工件核验后停止自主轮次,不自动升级或重启宿主。
|
|
16
|
+
|
|
3
17
|
## 0.12.0
|
|
4
18
|
|
|
5
19
|
- 多批主文件增加有界跨窗口综合,使用完整原始窗口定位目录及批准快照读取,不把目录当作源码证据。
|
package/README.md
CHANGED
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
## 当前状态
|
|
6
6
|
|
|
7
|
-
当前代码版本为 0.
|
|
7
|
+
当前代码版本为 0.14.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
|
+
## 批准快照内跨文件综合(0.14.0)
|
|
14
|
+
|
|
15
|
+
关联组最多4个批准主文件、至少2个文本变化时追加跨文件综合。目录保留完整精确编辑原位置,不作为源码证据;每条发现须引用至少2文件的 `interaction.editRefs:[{fileId,editId}]`,主attribution与主引用一致,原锚重叠主编辑,每个引用编辑至少一非空侧须本综合生成read完整覆盖。共享批准reader/全局调用额度,嵌入组最后主文件原超时,不重置生命周期;预检包含额外初始调用,检索复核另计。interactions独立记录pending/completed/blocked,跨组和未完成明确限制。不合并多文件候选,不自动扩文件/仓库。源码/规则/context仍不可信。真实模型样本未跑,安全反例空输出模拟不代表模型查准率;拒绝审批表示未审查不是通过。
|
|
16
|
+
|
|
17
|
+
## 静态调用绑定线索(0.13.0)
|
|
18
|
+
|
|
19
|
+
批准JS/TS源码中的命名相对import及别名、简单独立调用,可补充到同侧直接 `export function` 声明的原始行号线索。组输入 `bindingRelations` 及显式上下文注释保留导入/调用/声明位置;不新增读取、组成员或审查覆盖。局部名其他出现(参数/声明/赋值/逃逸)、目标名额外出现、重复导出、重导出或不支持语法保守不生成绑定线索;绑定不确定不阻止普通源码审查。不是通用模块解析、语义绑定或可达性/因果证明。输入预算不足沿用原有关联回退,不截断规则或上下文。
|
|
20
|
+
|
|
13
21
|
## 跨窗口综合分析(0.12.0)
|
|
14
22
|
|
|
15
23
|
主文件需要多批精确窗口时,在局部分析后追加一次有界综合检查。完整原始窗口目录不充当源码;综合发现必须引用至少两个不同窗口的编辑,每个引用编辑至少一个非空旧/新侧必须在本综合生成阶段被批准read完整覆盖(不能借用前批/复核审计或search)。使用原定位及证据复核,不截断规则/context。共享原每文件超时和全局调用/读取额度;预检minimumCalls包含综合初始请求,后续检索复核另计。synthesisStatus独立记录pending/completed/blocked,局部窗口完成不表示综合完成;输入超限或预算耗尽不冒算覆盖。读取覆盖及模型分析仍不证明跨路径正确性或因果事实,真实模型质量未新增验证。
|
package/package.json
CHANGED
|
@@ -12,5 +12,5 @@ export function inferContextRelations(input, file) {
|
|
|
12
12
|
const all = [primary, ...entries, ...competitors];
|
|
13
13
|
if (all.length > 200) return [];
|
|
14
14
|
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, reason: 'relative-import:approved-context' }));
|
|
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
16
|
}
|
package/src/coverage-plan.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { prepareInteractionInput } from './interaction-input.mjs';
|
|
2
|
+
import { initialInputBudget } from './input-budget.mjs';
|
|
1
3
|
import { preparePrimaryInput } from './primary-input.mjs';
|
|
2
4
|
|
|
3
5
|
// Metadata only. No retained payload, file reads, model calls, or approval grants.
|
|
@@ -16,5 +18,6 @@ export function buildCoveragePlan(input, options) {
|
|
|
16
18
|
const prepared = preparePrimaryInput(input, file, options);
|
|
17
19
|
return { ...item, ...prepared.metadata, status: prepared.budget.fits ? 'ready' : 'input-blocked', minimumCalls: prepared.budget.fits ? ((prepared.batches?.length ?? 1) + (prepared.synthesis?.budget.fits ? 1 : 0)) : 0 };
|
|
18
20
|
});
|
|
19
|
-
|
|
21
|
+
const interactions = options.scope && options.grouping ? options.grouping.groups.filter(g => g.fileIds.length > 1).map(group => { const prepared = prepareInteractionInput(input, group, payload => initialInputBudget({ instructions: options.instructions, input: payload }, options.scope, options.maxInputBytes)); return { groupId: group.id, fileIds: prepared.fileIds, status: prepared.status, reason: prepared.reason, minimumCalls: prepared.status === 'ready' ? 1 : 0 }; }) : [];
|
|
22
|
+
return { interactions, snapshotId: input.id, maxInputBytes: options.maxInputBytes, items, minimumCalls: items.reduce((sum,item) => sum + item.minimumCalls, 0) + interactions.reduce((sum,item) => sum + item.minimumCalls, 0), initialRequestsOnly: true };
|
|
20
23
|
}
|
package/src/finding-groups.mjs
CHANGED
|
@@ -3,6 +3,8 @@ export function groupDuplicateFindings(findings) {
|
|
|
3
3
|
const buckets = new Map();
|
|
4
4
|
for (let index = 0; index < findings.length; index++) {
|
|
5
5
|
const f = findings[index], a = f.attribution, v = f.verification;
|
|
6
|
+
// Cross-file candidates carry distinct qualified causal references; never collapse them.
|
|
7
|
+
if (f.interaction) continue;
|
|
6
8
|
if (a?.status !== 'references-validated' || v?.status !== 'completed' || v.verdict !== 'supported' || !v.evidence?.length || v.evidence.some(e => e.status !== 'references-validated')) continue;
|
|
7
9
|
// Require the entire exact evidence set, not mere overlap/transitive similarity.
|
|
8
10
|
const evidence = v.evidence.map(e => JSON.stringify([e.snapshotId, e.sourceId, e.hash, e.start, e.count, e.text])).sort();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Conservative direct exported-function hints, never execution/reachability proof.
|
|
2
|
+
export function directImportBindings(lines, importLine, calls, targetText) {
|
|
3
|
+
if (typeof targetText !== 'string' || Buffer.byteLength(targetText) > 256 * 1024 || /[\x60]|\/\*/.test(targetText)) return [];
|
|
4
|
+
const match = /^\s*import\s+\{([^}]+)\}\s+from\s+(['"])[^'"]+\2\s*;?\s*(?:\/\/.*)?$/.exec(lines[importLine - 1]);
|
|
5
|
+
if (!match) return [];
|
|
6
|
+
const bindings = 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));
|
|
7
|
+
if (bindings.some(b => !b)) return [];
|
|
8
|
+
const targets = targetText.split(String.fromCharCode(10)), result = [];
|
|
9
|
+
for (const binding of bindings) {
|
|
10
|
+
const imported = binding[1], local = binding[2] ?? imported;
|
|
11
|
+
const token = new RegExp('(^|[^A-Za-z0-9_$])' + local.replaceAll('$', '\\$') + '([^A-Za-z0-9_$]|$)');
|
|
12
|
+
const positions = calls.filter(c => c.local === local);
|
|
13
|
+
if (!positions.length) continue;
|
|
14
|
+
// Every other occurrence may be declaration, parameter, mutation or escape.
|
|
15
|
+
if (lines.some((line, i) => i + 1 !== importLine && !positions.some(c => c.line === i + 1) && token.test(line))) continue;
|
|
16
|
+
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);
|
|
17
|
+
if (declarations.length !== 1) continue;
|
|
18
|
+
const exportedToken = new RegExp('(^|[^A-Za-z0-9_$])' + imported.replaceAll('$', String.fromCharCode(92) + '$') + '([^A-Za-z0-9_$]|$)');
|
|
19
|
+
if (targets.some((line, i) => i + 1 !== declarations[0].number && exportedToken.test(line))) continue;
|
|
20
|
+
result.push(...positions.map(call => ({ imported, local, importLine, callLine: call.line, declarationLine: declarations[0].number, confidence: 'conservative-syntax-only' })));
|
|
21
|
+
}
|
|
22
|
+
return result.slice(0, 20);
|
|
23
|
+
}
|
package/src/import-relations.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { directImportBindings } from './import-bindings.mjs';
|
|
2
3
|
import { importedCallSites, indexStandaloneCalls } from './import-call-sites.mjs';
|
|
3
4
|
|
|
4
5
|
const extensions = ['.mjs', '.cjs', '.js', '.jsx', '.ts', '.tsx'];
|
|
@@ -36,7 +37,9 @@ export function inferImportRelations(files) {
|
|
|
36
37
|
const matches = [...candidates].map(p => byPath.get(p)).filter(Boolean);
|
|
37
38
|
if (matches.length !== 1 || matches[0].eligibility !== 'reviewable' || matches[0].id === file.id) continue;
|
|
38
39
|
const callSites = importedCallSites(lines, i + 1, callIndex);
|
|
39
|
-
|
|
40
|
+
const targetText = (side === 'old' ? matches[0].left : matches[0].right)?.text;
|
|
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 });
|
|
40
43
|
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 });
|
|
41
44
|
if (edges.length >= 2000) return edges;
|
|
42
45
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { changeMap } from './change-map.mjs';
|
|
2
|
+
import { validateFindings } from './review.mjs';
|
|
3
|
+
import { validateSynthesisReads } from './synthesis-evidence.mjs';
|
|
4
|
+
|
|
5
|
+
// File-qualified references prevent identical e1 IDs in different files being confused.
|
|
6
|
+
export function validateInteractionFindings(response, snapshot, group, reads, catalog) {
|
|
7
|
+
if (typeof response === 'string') { if (Buffer.byteLength(response) > 128 * 1024) throw new Error('Interaction response too large'); response = JSON.parse(response); }
|
|
8
|
+
if (!response || Buffer.byteLength(JSON.stringify(response)) > 128 * 1024 || !Array.isArray(response.findings) || response.findings.length > 50 || !Array.isArray(response.limitations) || response.limitations.length > 50 || response.limitations.some(s => typeof s !== 'string' || !s.trim() || s.length > 8000)) throw new Error('Invalid interaction response');
|
|
9
|
+
if (!group || !Array.isArray(group.fileIds) || group.fileIds.length < 2 || group.fileIds.length > 4 || new Set(group.fileIds).size !== group.fileIds.length) throw new Error('Invalid interaction scope');
|
|
10
|
+
const files = new Map();
|
|
11
|
+
for (const id of group.fileIds) {
|
|
12
|
+
const matches = snapshot.files.filter(f => f.id === id);
|
|
13
|
+
if (matches.length !== 1 || matches[0].eligibility !== 'reviewable') throw new Error('Interaction file outside approved scope');
|
|
14
|
+
files.set(id, matches[0]);
|
|
15
|
+
}
|
|
16
|
+
const findings = response.findings.map(raw => {
|
|
17
|
+
const file = files.get(raw?.fileId);
|
|
18
|
+
if (!file) throw new Error('Interaction primary outside group');
|
|
19
|
+
const refs = raw.interaction?.editRefs;
|
|
20
|
+
if (!Array.isArray(refs) || refs.length < 2 || refs.length > 50 || refs.some(r => !r || Object.keys(r).sort().join(',') !== 'editId,fileId' || typeof r.editId !== 'string' || !files.has(r.fileId))) throw new Error('Invalid file-qualified interaction references');
|
|
21
|
+
if (new Set(refs.map(r => JSON.stringify([r.fileId, r.editId]))).size !== refs.length || new Set(refs.map(r => r.fileId)).size < 2) throw new Error('Interaction requires distinct edits across multiple files');
|
|
22
|
+
const finding = validateFindings({ findings: [raw], limitations: [] }, file).findings[0];
|
|
23
|
+
const primary = refs.filter(r => r.fileId === file.id).map(r => r.editId);
|
|
24
|
+
if (finding.attribution.status !== 'references-validated' || !primary.length || primary.some(id => !finding.attribution.editIds.includes(id)) || finding.attribution.editIds.some(id => !primary.includes(id))) throw new Error('Interaction primary attribution mismatch');
|
|
25
|
+
const primaryChanges = changeMap(file.left.text, file.right.text);
|
|
26
|
+
if (finding.anchor.kind !== 'line' || !primaryChanges.edits.some(e => { const range = e[finding.anchor.side]; return primary.includes(e.id) && range.count > 0 && finding.anchor.start < range.start + range.count && finding.anchor.end >= range.start; })) throw new Error('Interaction anchor must overlap a referenced primary edit');
|
|
27
|
+
for (const id of new Set(refs.map(r => r.fileId))) {
|
|
28
|
+
const target = files.get(id), changes = changeMap(target.left.text, target.right.text);
|
|
29
|
+
const editIds = refs.filter(r => r.fileId === id).map(r => r.editId);
|
|
30
|
+
if (changes.status !== 'changed' || changes.precision !== 'exact' || editIds.some(editId => !changes.edits.some(e => e.id === editId))) throw new Error('Unknown or inexact interaction edit');
|
|
31
|
+
validateSynthesisReads([{ attribution: { editIds } }], changes, reads, catalog, id, snapshot.id);
|
|
32
|
+
}
|
|
33
|
+
return { ...finding, interaction: { status: 'references-and-reads-validated', causality: 'unverified', editRefs: refs.map(r => ({ ...r })) } };
|
|
34
|
+
});
|
|
35
|
+
return { findings, limitations: [...response.limitations] };
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { changeMap } from './change-map.mjs';
|
|
2
|
+
|
|
3
|
+
// Pure navigation over one already approved snapshot. No source reads or model sends.
|
|
4
|
+
export function prepareInteractionInput(snapshot, group, measure) {
|
|
5
|
+
if (!snapshot || typeof snapshot.id !== 'string' || !Array.isArray(snapshot.files) || !group || !Array.isArray(group.fileIds) || group.fileIds.length < 2 || group.fileIds.length > 4 || new Set(group.fileIds).size !== group.fileIds.length || typeof measure !== 'function') throw new Error('Invalid interaction group');
|
|
6
|
+
const files = group.fileIds.map(id => {
|
|
7
|
+
const matches = snapshot.files.filter(f => f.id === id);
|
|
8
|
+
if (matches.length !== 1 || matches[0].eligibility !== 'reviewable' || typeof matches[0].left?.text !== 'string' || typeof matches[0].right?.text !== 'string') throw new Error('Interaction file outside approved reviewable scope');
|
|
9
|
+
const file = matches[0], changes = changeMap(file.left.text, file.right.text);
|
|
10
|
+
if (changes.status !== 'changed' || changes.precision !== 'exact') return { fileId: file.id, path: file.path, unavailable: true };
|
|
11
|
+
return { fileId: file.id, path: file.path, edits: changes.edits.map(({ id, old, new: after }) => ({ id, old, new: after })) };
|
|
12
|
+
});
|
|
13
|
+
if (group.fileIds.filter(id => { const file = snapshot.files.find(f => f.id === id); return file.left.text !== file.right.text; }).length < 2) return { status: 'not-applicable', fileIds: [...group.fileIds] };
|
|
14
|
+
if (files.some(f => f.unavailable)) return { status: 'blocked', reason: 'exact-edit-analysis-unavailable', fileIds: [...group.fileIds] };
|
|
15
|
+
const payload = JSON.stringify({ snapshotId: snapshot.id, sourceMode: 'file-interaction', groupId: group.id, files, context: snapshot.context ?? [], rules: snapshot.rules ?? [], notice: 'Only combined defects across at least two listed files; directories are navigation not evidence. Read referenced edit source sides from approved catalog in this generation. Return ordinary findings with fileId of one listed primary and exact original line anchor; attribution.editIds references only that primary. Add interaction:{editRefs:[{fileId,editId}]} including all involved edits across at least two files. This sourceMode overrides single-primary input instructions. Retain original primary anchor and file-qualified edit references. Unsupported conclusions become limitations; no repeated single-file issues. Relations and read coverage are not causal proof.' });
|
|
16
|
+
const budget = measure(payload);
|
|
17
|
+
return { status: budget.fits ? 'ready' : 'blocked', reason: budget.fits ? undefined : 'input-budget', fileIds: [...group.fileIds], payload, budget };
|
|
18
|
+
}
|
package/src/primary-input.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inferImportRelations } from './import-relations.mjs';
|
|
1
2
|
import { inferContextRelations } from './context-relations.mjs';
|
|
2
3
|
import { prepareWindowSynthesis } from './window-synthesis.mjs';
|
|
3
4
|
import { planWindowBatches } from './window-batches.mjs';
|
|
@@ -24,7 +25,8 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
|
|
|
24
25
|
const relatedFiles = input.files.filter(f => f.id !== file.id && f.eligibility === 'reviewable' && group.fileIds.includes(f.id));
|
|
25
26
|
if (relatedFiles.length) {
|
|
26
27
|
const relations = grouping.links.filter(link => !link.split && group.fileIds.includes(link.from) && group.fileIds.includes(link.to));
|
|
27
|
-
const
|
|
28
|
+
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 }));
|
|
29
|
+
const grouped = JSON.stringify({ ...base, relatedFiles, relations, bindingRelations, relationNotice: '命名和相对导入关系仅为源码线索,可能被同名局部变量遮蔽,不证明绑定、调用可达或因果;只报告主file的问题,不为relatedFiles重复生成发现。' });
|
|
28
30
|
if (measure(grouped).fits) { payload = grouped; metadata.relatedFileIds = relatedFiles.map(f => f.id); }
|
|
29
31
|
else metadata.groupFallback = 'input-budget';
|
|
30
32
|
}
|
package/src/review.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { prepareInteractionInput } from './interaction-input.mjs';
|
|
2
|
+
import { validateInteractionFindings } from './interaction-findings.mjs';
|
|
3
|
+
import { initialInputBudget } from './input-budget.mjs';
|
|
1
4
|
import { validateWindowFindings } from './window-findings.mjs';
|
|
2
5
|
import { groupDuplicateFindings } from './finding-groups.mjs';
|
|
3
6
|
import { validateSynthesisReads } from './synthesis-evidence.mjs';
|
|
@@ -84,6 +87,9 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
84
87
|
const report = { schemaVersion: 1, snapshotId: input.id, vcs: input.vcs, status: 'completed', files: [], findings: [], limitations: [], modelCalls: 0 };
|
|
85
88
|
report.rules = (input.rules ?? []).map(({ path, hash }) => ({ path, hash }));
|
|
86
89
|
if (options.enableGrouping === true) report.grouping = buildReviewGroups(input.files, inferFileRelations(input.files));
|
|
90
|
+
report.interactions = [];
|
|
91
|
+
const interactionPlans = scope && report.grouping ? report.grouping.groups.filter(g => g.fileIds.length > 1).map(group => ({ group, prepared: prepareInteractionInput(input, group, payload => initialInputBudget({ instructions: REVIEW_INSTRUCTIONS, input: payload }, scope, maxInputBytes)) })) : [];
|
|
92
|
+
for (const plan of interactionPlans.filter(p => p.prepared.status !== 'not-applicable')) report.interactions.push({ groupId: plan.group.id, fileIds: plan.prepared.fileIds, status: plan.prepared.status === 'ready' ? 'pending' : 'blocked', reason: plan.prepared.reason, lifecycle: 'shares-last-primary-timeout' });
|
|
87
93
|
for (const file of input.files) {
|
|
88
94
|
const state = { fileId: file.id, path: file.path, status: file.eligibility === 'reviewable' ? 'pending' : file.eligibility, reason: file.reason };
|
|
89
95
|
report.files.push(state);
|
|
@@ -115,13 +121,16 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
115
121
|
await invoke(async enclosingRequest => {
|
|
116
122
|
const batches = [...(prepared.batches ?? [{ payload, windowIds: state.windowIds ?? [] }])];
|
|
117
123
|
if (prepared.synthesis?.budget.fits) batches.push({ payload: prepared.synthesis.payload, windowIds: [], synthesis: true });
|
|
124
|
+
const interactionPlan = interactionPlans.find(p => input.files.filter(f => p.group.fileIds.includes(f.id)).at(-1)?.id === file.id);
|
|
125
|
+
if (interactionPlan?.prepared.status === 'ready') batches.push({ payload: interactionPlan.prepared.payload, windowIds: [], interaction: interactionPlan.group });
|
|
118
126
|
for (const batch of batches) {
|
|
119
127
|
enclosingRequest.signal.throwIfAborted();
|
|
120
128
|
const request = { ...enclosingRequest, input: batch.payload };
|
|
121
129
|
const auditStart = state.retrievalAudit?.length ?? 0;
|
|
122
|
-
const
|
|
130
|
+
const response = await executor(request);
|
|
131
|
+
const generated = batch.interaction ? validateInteractionFindings(response, input, batch.interaction, (state.retrievalAudit ?? []).slice(auditStart), scope.catalog()) : validateResponse(response, file, changes);
|
|
123
132
|
if (batch.synthesis) validateSynthesisReads(generated.findings, changes, (state.retrievalAudit ?? []).slice(auditStart), scope.catalog(), file.id, input.id);
|
|
124
|
-
if (state.sourceMode === 'change-windows') validateWindowFindings(generated.findings, batch.synthesis ? prepared.synthesis.windows : JSON.parse(batch.payload).windows, batch.synthesis === true);
|
|
133
|
+
if (!batch.interaction && state.sourceMode === 'change-windows') validateWindowFindings(generated.findings, batch.synthesis ? prepared.synthesis.windows : JSON.parse(batch.payload).windows, batch.synthesis === true);
|
|
125
134
|
request.signal.throwIfAborted();
|
|
126
135
|
report.findings.push(...generated.findings);
|
|
127
136
|
if (generated.findings.some(f => f.attribution.status === 'missing')) report.limitations.push({ fileId: file.id, text: '部分发现缺少变更归因,仅校验了定位,尚不能确认属于本次回归' });
|
|
@@ -145,6 +154,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
156
|
if (batch.synthesis) state.synthesisStatus = 'completed';
|
|
157
|
+
if (batch.interaction) report.interactions.find(p => p.groupId === batch.interaction.id).status = 'completed';
|
|
148
158
|
if (state.windowCoverage) {
|
|
149
159
|
state.windowCoverage.completed.push(...batch.windowIds);
|
|
150
160
|
state.windowCoverage.pending = state.windowCoverage.pending.filter(id => !batch.windowIds.includes(id));
|
|
@@ -160,6 +170,8 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
160
170
|
state.reason = error?.message ?? String(error);
|
|
161
171
|
}
|
|
162
172
|
}
|
|
173
|
+
for (const interaction of report.interactions) if (interaction.status !== 'completed') report.limitations.push({ text: '跨文件综合未完成:' + interaction.groupId + ' (' + interaction.status + ')' });
|
|
174
|
+
if (report.grouping?.links.some(l => l.split)) report.limitations.push({ text: '跨组关系未综合覆盖;关联分组不代表全部跨文件交互覆盖' });
|
|
163
175
|
const unfinished = report.files.some(f => !['completed', 'excluded'].includes(f.status));
|
|
164
176
|
report.status = signal?.aborted ? 'cancelled' : unfinished || report.limitations.length ? 'partial' : 'completed';
|
|
165
177
|
if (report.files.some(f => f.status === 'failed') && !report.files.some(f => f.status === 'completed')) report.status = signal?.aborted ? 'cancelled' : 'failed';
|
|
@@ -192,6 +204,7 @@ export function markdownReport(report) {
|
|
|
192
204
|
}
|
|
193
205
|
for (const file of report.files) if (file.windowCoverage) output.push('- ' + safe(file.path) + ':变更窗口已完成 ' + file.windowCoverage.completed.length + ',待审 ' + file.windowCoverage.pending.length + ';仅表示窗口分析完成,不证明全文正确');
|
|
194
206
|
for (const file of report.files) if (file.synthesisStatus) output.push('- ' + safe(file.path) + ':跨窗口综合 ' + safe(file.synthesisStatus) + ';不等于事实正确性证明');
|
|
207
|
+
for (const interaction of report.interactions ?? []) output.push('- 跨文件综合 ' + safe(interaction.groupId) + ':' + safe(interaction.status) + ';共享组末主文件超时,不证明完整交互覆盖');
|
|
195
208
|
if (report.rules?.length) output.push('', '## 项目规则', '规则仅为已批准约束数据,不扩大权限或证明缺陷。', ...report.rules.map(rule => '- ' + safe(rule.path) + ' · SHA256 ' + safe(rule.hash)));
|
|
196
209
|
if (report.followup?.suggestedPaths.length) output.push('', '## 后续选择建议', '仅为原快照未完成项;不会自动续审。再次执行需新预览、新审批,并显式选择规则和上下文。阻断项需先处理原因;关联上下文不等于主文件已审查。', ...report.followup.items.filter(item => item.followup).map(item => '- ' + safe(item.path) + ':' + item.status + (item.incompleteVerification ? '(候选复核未完成)' : '')));
|
|
197
210
|
output.push('', '## 审查发现');
|
|
@@ -205,6 +218,7 @@ export function markdownReport(report) {
|
|
|
205
218
|
const a = finding.anchor;
|
|
206
219
|
const location = a.kind === 'line' ? a.side + ':' + a.start + '-' + a.end : a.kind === 'property' ? '属性 ' + a.name : '文件级';
|
|
207
220
|
output.push('', '### [' + finding.severity + '] ' + safe(finding.title), safe(finding.path) + ' · ' + safe(location));
|
|
221
|
+
if (finding.interaction) output.push('- 跨文件编辑引用:' + safe(finding.interaction.editRefs.map(r => r.fileId + ':' + r.editId).join(', ')) + ';引用及读取已校验,因果未经证实');
|
|
208
222
|
const regression = finding.verification?.regression;
|
|
209
223
|
const regressionLabel = ({ introduced: '模型判断本次引入', preexisting: '模型判断原有问题', uncertain: '前后对照证据不足', unassessed: '尚未评估修改前后' }[regression?.classification ?? 'unassessed']);
|
|
210
224
|
output.push('- 回归分类:' + regressionLabel + ';不等于因果事实证明');
|