@feng0531/dsh-code-review 0.13.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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.14.0
4
+
5
+ - 同一批准仓库关联组追加有界跨文件综合,2–4文件,目录精确编辑与本阶段原文read覆盖,不静默截断规则/context。
6
+ - 文件限定编辑引用、主归因一致与原锚校验,保留原候选,不参与模糊去重。
7
+ - 综合共享批准reader、全局调用及组末主文件生命周期;预检一致,超时取消/预算不足和跨组未覆盖明确限制。
8
+ - 215项回归及宿主只读审批离线冒烟通过;增加缺陷/safe样本判读协议,不宣称真实模型质量。
9
+
3
10
  ## 0.13.0
4
11
 
5
12
  - 增加批准范围命名相对import别名→简单调用→同侧直接export function的保守位置线索,组输入及显式context保留原始行号。
package/README.md CHANGED
@@ -4,12 +4,16 @@
4
4
 
5
5
  ## 当前状态
6
6
 
7
- 当前代码版本为 0.13.0,包含有界覆盖预检、显式项目规则及候选证据与反证复核;发布状态以 npm 和 GitHub Release 为准。已通过真实临时 Git/SVN 仓库测试,以及隔离进程中的真实 DSH ToolRuntime 注册验证。当前开发会话的实际预览已返回0.6.0的plan/rules字段,空变更预览与默认关闭外发已验证;已进行经宿主审批的六次小规模真实试跑,累计12次模型调用;四组合成正负例与历史修复对照符合预期,历史引入方向检出候选但引用复核未完成(partial)。不是独立盲测或整体质量证明。详情见 [试跑结果](evaluation/PILOT-RESULTS.md)。
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
+
13
17
  ## 静态调用绑定线索(0.13.0)
14
18
 
15
19
  批准JS/TS源码中的命名相对import及别名、简单独立调用,可补充到同侧直接 `export function` 声明的原始行号线索。组输入 `bindingRelations` 及显式上下文注释保留导入/调用/声明位置;不新增读取、组成员或审查覆盖。局部名其他出现(参数/声明/赋值/逃逸)、目标名额外出现、重复导出、重导出或不支持语法保守不生成绑定线索;绑定不确定不阻止普通源码审查。不是通用模块解析、语义绑定或可达性/因果证明。输入预算不足沿用原有关联回退,不截断规则或上下文。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.13.0",
3
+ "version": "0.14.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,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
- return { snapshotId: input.id, maxInputBytes: options.maxInputBytes, items, minimumCalls: items.reduce((sum,item) => sum + item.minimumCalls, 0), initialRequestsOnly: true };
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
  }
@@ -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,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/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 generated = validateResponse(await executor(request), file, changes);
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 + ';不等于因果事实证明');