@feng0531/dsh-code-review 0.11.1 → 0.13.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 +1 -1
- package/src/import-bindings.mjs +23 -0
- package/src/import-relations.mjs +4 -1
- package/src/primary-input.mjs +11 -3
- package/src/review.mjs +10 -2
- package/src/synthesis-evidence.mjs +17 -0
- package/src/window-findings.mjs +2 -1
- package/src/window-synthesis.mjs +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
- 增加批准范围命名相对import别名→简单调用→同侧直接export function的保守位置线索,组输入及显式context保留原始行号。
|
|
6
|
+
- 调用名其他出现、目标同名变更、重复导出/重导出及不支持语法不生成绑定线索,不冒充通用语义图或因果证明。
|
|
7
|
+
- 不增加读取权限、组成员、模型调用或审查覆盖;实际主输入传输、歧义拒绝、全量及宿主只读审批离线验收通过。
|
|
8
|
+
- 本轮质量增强到此冻结,最终版本发布工件核验后停止自主轮次,不自动升级或重启宿主。
|
|
9
|
+
|
|
10
|
+
## 0.12.0
|
|
11
|
+
|
|
12
|
+
- 多批主文件增加有界跨窗口综合,使用完整原始窗口定位目录及批准快照读取,不把目录当作源码证据。
|
|
13
|
+
- 综合发现必须引用至少两个不同窗口编辑,且每个引用编辑的非空旧/新侧至少一侧在本阶段read完整覆盖;保持原始定位和精确证据复核。
|
|
14
|
+
- 综合与局部/复核共享每文件超时、全局调用及读取额度;预检包含综合初始请求,局部覆盖和综合状态分别记录,超限/耗尽/取消不冒完成。
|
|
15
|
+
- 离线集成覆盖综合检索、旧新证据凭据复核、取消和预算边界;不扩权限、不证明模型结论质量,不自动升级宿主。
|
|
16
|
+
|
|
3
17
|
## 0.11.1
|
|
4
18
|
|
|
5
19
|
- 0.11.0候选标签Windows CI发现临时目录8.3短路径与canonical长路径的测试比较差异,未触发正式发布。夹具根统一realpath,保留生产路径安全校验不放宽;原标签不移动,使用新候选。
|
package/README.md
CHANGED
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
## 当前状态
|
|
6
6
|
|
|
7
|
-
当前代码版本为 0.
|
|
7
|
+
当前代码版本为 0.13.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.13.0)
|
|
14
|
+
|
|
15
|
+
批准JS/TS源码中的命名相对import及别名、简单独立调用,可补充到同侧直接 `export function` 声明的原始行号线索。组输入 `bindingRelations` 及显式上下文注释保留导入/调用/声明位置;不新增读取、组成员或审查覆盖。局部名其他出现(参数/声明/赋值/逃逸)、目标名额外出现、重复导出、重导出或不支持语法保守不生成绑定线索;绑定不确定不阻止普通源码审查。不是通用模块解析、语义绑定或可达性/因果证明。输入预算不足沿用原有关联回退,不截断规则或上下文。
|
|
16
|
+
|
|
17
|
+
## 跨窗口综合分析(0.12.0)
|
|
18
|
+
|
|
19
|
+
主文件需要多批精确窗口时,在局部分析后追加一次有界综合检查。完整原始窗口目录不充当源码;综合发现必须引用至少两个不同窗口的编辑,每个引用编辑至少一个非空旧/新侧必须在本综合生成阶段被批准read完整覆盖(不能借用前批/复核审计或search)。使用原定位及证据复核,不截断规则/context。共享原每文件超时和全局调用/读取额度;预检minimumCalls包含综合初始请求,后续检索复核另计。synthesisStatus独立记录pending/completed/blocked,局部窗口完成不表示综合完成;输入超限或预算耗尽不冒算覆盖。读取覆盖及模型分析仍不证明跨路径正确性或因果事实,真实模型质量未新增验证。
|
|
20
|
+
|
|
13
21
|
## 修改前后对照复核(0.11.0)
|
|
14
22
|
|
|
15
23
|
复核可返回 `regression`:`introduced`(模型判断本次引入)、`preexisting`(模型判断原有问题)、`uncertain`(对照不足)。前两者必须引用候选主文件旧新两侧的已精确校验证据,索引指向本候选复核 `evidence` 数组;空源或缺少任一侧时不能虚构支持。旧协议缺省标为 `unassessed`,关闭复核同样明确未评估。报告保留全部候选并展示分类、理由及 `regressionCoverage`;模型支持但对照不足进入限制。分类及引用有效不等于因果事实成立,也不宣称真实模型质量提升已获验证。去重隔离分类/理由/证据索引冲突,uncertain及unassessed不折叠。
|
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
|
@@ -14,7 +14,7 @@ export function buildCoveragePlan(input, options) {
|
|
|
14
14
|
const item = { fileId: file.id, path: file.path, status: file.eligibility, minimumCalls: 0 };
|
|
15
15
|
if (file.eligibility !== 'reviewable') return item;
|
|
16
16
|
const prepared = preparePrimaryInput(input, file, options);
|
|
17
|
-
return { ...item, ...prepared.metadata, status: prepared.budget.fits ? 'ready' : 'input-blocked', minimumCalls: prepared.budget.fits ? (prepared.batches?.length ?? 1) : 0 };
|
|
17
|
+
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
18
|
});
|
|
19
19
|
return { snapshotId: input.id, maxInputBytes: options.maxInputBytes, items, minimumCalls: items.reduce((sum,item) => sum + item.minimumCalls, 0), initialRequestsOnly: true };
|
|
20
20
|
}
|
|
@@ -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
|
}
|
package/src/primary-input.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { inferImportRelations } from './import-relations.mjs';
|
|
1
2
|
import { inferContextRelations } from './context-relations.mjs';
|
|
3
|
+
import { prepareWindowSynthesis } from './window-synthesis.mjs';
|
|
2
4
|
import { planWindowBatches } from './window-batches.mjs';
|
|
3
5
|
import { changeWindows } from './change-windows.mjs';
|
|
4
6
|
import { changeMap } from './change-map.mjs';
|
|
@@ -16,14 +18,15 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
|
|
|
16
18
|
let payload = JSON.stringify(base);
|
|
17
19
|
const measure = value => initialInputBudget({ instructions, input: value }, scope, maxInputBytes);
|
|
18
20
|
const metadata = {};
|
|
19
|
-
let batches;
|
|
21
|
+
let batches, synthesis;
|
|
20
22
|
const group = grouping?.groups.find(g => g.fileIds.includes(file.id));
|
|
21
23
|
if (group) {
|
|
22
24
|
metadata.groupId = group.id;
|
|
23
25
|
const relatedFiles = input.files.filter(f => f.id !== file.id && f.eligibility === 'reviewable' && group.fileIds.includes(f.id));
|
|
24
26
|
if (relatedFiles.length) {
|
|
25
27
|
const relations = grouping.links.filter(link => !link.split && group.fileIds.includes(link.from) && group.fileIds.includes(link.to));
|
|
26
|
-
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重复生成发现。' });
|
|
27
30
|
if (measure(grouped).fits) { payload = grouped; metadata.relatedFileIds = relatedFiles.map(f => f.id); }
|
|
28
31
|
else metadata.groupFallback = 'input-budget';
|
|
29
32
|
}
|
|
@@ -53,6 +56,11 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
}
|
|
59
|
+
if (batches?.length > 1) {
|
|
60
|
+
synthesis = prepareWindowSynthesis(batches.map(batch => batch.payload), measure);
|
|
61
|
+
metadata.synthesisStatus = synthesis.budget.fits ? 'ready' : 'input-blocked';
|
|
62
|
+
metadata.synthesisInputBytes = synthesis.budget.bytes;
|
|
63
|
+
}
|
|
56
64
|
const budget = measure(payload);
|
|
57
|
-
return { changes, payload, batches, budget, metadata: { ...metadata, initialInputBytes: budget.bytes } };
|
|
65
|
+
return { changes, payload, batches, synthesis, budget, metadata: { ...metadata, initialInputBytes: budget.bytes } };
|
|
58
66
|
}
|
package/src/review.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateWindowFindings } from './window-findings.mjs';
|
|
2
2
|
import { groupDuplicateFindings } from './finding-groups.mjs';
|
|
3
|
+
import { validateSynthesisReads } from './synthesis-evidence.mjs';
|
|
3
4
|
import { hash } from './content.mjs';
|
|
4
5
|
import { coverageFollowup } from './coverage-followup.mjs';
|
|
5
6
|
import { verificationLoop } from './verification-loop.mjs';
|
|
@@ -92,6 +93,8 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
92
93
|
const prepared = preparePrimaryInput(input, file, { instructions: REVIEW_INSTRUCTIONS, scope, grouping: report.grouping, maxInputBytes });
|
|
93
94
|
const { changes, payload } = prepared;
|
|
94
95
|
Object.assign(state, prepared.metadata);
|
|
96
|
+
if (prepared.synthesis) state.synthesisStatus = prepared.synthesis.budget.fits ? 'pending' : 'blocked';
|
|
97
|
+
if (prepared.synthesis && !prepared.synthesis.budget.fits) report.limitations.push({ fileId: file.id, text: '跨窗口综合输入超限,未发送;单窗口完成不表示交互影响已检查' });
|
|
95
98
|
if (state.sourceMode === 'change-windows') state.windowCoverage = { completed: [], pending: [...state.windowIds] };
|
|
96
99
|
if (changes.status === 'limited') report.limitations.push({ fileId: file.id, text: '精确变更分析受限:' + changes.reason + ';仍按完整两侧内容审查' });
|
|
97
100
|
if (state.groupFallback) report.limitations.push({ fileId: file.id, text: '关联组上下文超过输入预算,回退单文件审查' });
|
|
@@ -110,12 +113,15 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
110
113
|
} })
|
|
111
114
|
: request => { beforeCall(); return model(request); };
|
|
112
115
|
await invoke(async enclosingRequest => {
|
|
113
|
-
const batches = prepared.batches ?? [{ payload, windowIds: state.windowIds ?? [] }];
|
|
116
|
+
const batches = [...(prepared.batches ?? [{ payload, windowIds: state.windowIds ?? [] }])];
|
|
117
|
+
if (prepared.synthesis?.budget.fits) batches.push({ payload: prepared.synthesis.payload, windowIds: [], synthesis: true });
|
|
114
118
|
for (const batch of batches) {
|
|
115
119
|
enclosingRequest.signal.throwIfAborted();
|
|
116
120
|
const request = { ...enclosingRequest, input: batch.payload };
|
|
121
|
+
const auditStart = state.retrievalAudit?.length ?? 0;
|
|
117
122
|
const generated = validateResponse(await executor(request), file, changes);
|
|
118
|
-
if (
|
|
123
|
+
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);
|
|
119
125
|
request.signal.throwIfAborted();
|
|
120
126
|
report.findings.push(...generated.findings);
|
|
121
127
|
if (generated.findings.some(f => f.attribution.status === 'missing')) report.limitations.push({ fileId: file.id, text: '部分发现缺少变更归因,仅校验了定位,尚不能确认属于本次回归' });
|
|
@@ -138,6 +144,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
138
144
|
request.signal.throwIfAborted();
|
|
139
145
|
}
|
|
140
146
|
}
|
|
147
|
+
if (batch.synthesis) state.synthesisStatus = 'completed';
|
|
141
148
|
if (state.windowCoverage) {
|
|
142
149
|
state.windowCoverage.completed.push(...batch.windowIds);
|
|
143
150
|
state.windowCoverage.pending = state.windowCoverage.pending.filter(id => !batch.windowIds.includes(id));
|
|
@@ -184,6 +191,7 @@ export function markdownReport(report) {
|
|
|
184
191
|
for (const file of report.files) if (file.relatedFileIds?.length) output.push('- ' + safe(file.path) + ':输入包含 ' + file.relatedFileIds.length + ' 个关联文件');
|
|
185
192
|
}
|
|
186
193
|
for (const file of report.files) if (file.windowCoverage) output.push('- ' + safe(file.path) + ':变更窗口已完成 ' + file.windowCoverage.completed.length + ',待审 ' + file.windowCoverage.pending.length + ';仅表示窗口分析完成,不证明全文正确');
|
|
194
|
+
for (const file of report.files) if (file.synthesisStatus) output.push('- ' + safe(file.path) + ':跨窗口综合 ' + safe(file.synthesisStatus) + ';不等于事实正确性证明');
|
|
187
195
|
if (report.rules?.length) output.push('', '## 项目规则', '规则仅为已批准约束数据,不扩大权限或证明缺陷。', ...report.rules.map(rule => '- ' + safe(rule.path) + ' · SHA256 ' + safe(rule.hash)));
|
|
188
196
|
if (report.followup?.suggestedPaths.length) output.push('', '## 后续选择建议', '仅为原快照未完成项;不会自动续审。再次执行需新预览、新审批,并显式选择规则和上下文。阻断项需先处理原因;关联上下文不等于主文件已审查。', ...report.followup.items.filter(item => item.followup).map(item => '- ' + safe(item.path) + ':' + item.status + (item.incompleteVerification ? '(候选复核未完成)' : '')));
|
|
189
197
|
output.push('', '## 审查发现');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Audit only successful reads from this synthesis generation, never prior batches/searches.
|
|
2
|
+
export function validateSynthesisReads(findings, changes, reads, catalog, fileId, snapshotId) {
|
|
3
|
+
const sources = new Map(catalog.map(s => [s.id, s]));
|
|
4
|
+
for (const finding of findings) {
|
|
5
|
+
for (const id of finding.attribution.editIds) {
|
|
6
|
+
const edit = changes.edits.find(e => e.id === id);
|
|
7
|
+
const covered = ['old', 'new'].some(side => {
|
|
8
|
+
const range = edit?.[side];
|
|
9
|
+
return range?.count > 0 && reads.some(r => {
|
|
10
|
+
const source = sources.get(r.sourceId);
|
|
11
|
+
return r.kind === 'read' && r.snapshotId === snapshotId && source?.fileId === fileId && source.side === side && r.start <= range.start && r.start + r.count >= range.start + range.count;
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
if (!covered) throw new Error('Synthesis referenced edit was not read in this generation');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/window-findings.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Called after ordinary snapshot/attribution validation, before accepting batch findings.
|
|
2
|
-
export function validateWindowFindings(findings, windows) {
|
|
2
|
+
export function validateWindowFindings(findings, windows, synthesis = false) {
|
|
3
3
|
const edits = new Set(windows.flatMap(w => w.editIds));
|
|
4
4
|
for (const finding of findings) {
|
|
5
5
|
const attribution = finding.attribution;
|
|
6
6
|
if (attribution.status !== 'references-validated' || !attribution.editIds?.length || attribution.editIds.some(id => !edits.has(id))) throw new Error('Finding outside current window edits');
|
|
7
|
+
if (synthesis && windows.filter(w => w.editIds.some(id => attribution.editIds.includes(id))).length < 2) throw new Error('Synthesis finding must reference multiple windows');
|
|
7
8
|
const anchor = finding.anchor;
|
|
8
9
|
if (anchor.kind === 'line' && !windows.some(w => {
|
|
9
10
|
const range = w[anchor.side];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Pure approved-data preparation, no reads or sends. No source text is silently clipped.
|
|
2
|
+
export function prepareWindowSynthesis(batchPayloads, measure) {
|
|
3
|
+
if (!Array.isArray(batchPayloads) || batchPayloads.length < 2 || batchPayloads.length > 20) throw new Error('Invalid synthesis batches');
|
|
4
|
+
const batches = batchPayloads.map(payload => JSON.parse(payload));
|
|
5
|
+
const base = batches[0];
|
|
6
|
+
const windows = batches.flatMap(batch => batch.windows);
|
|
7
|
+
if (batches.some(batch => batch.snapshotId !== base.snapshotId || batch.file.id !== base.file.id || batch.sourceMode !== 'change-windows') || windows.length > 200 || new Set(windows.map(w => w.id)).size !== windows.length) throw new Error('Inconsistent synthesis windows');
|
|
8
|
+
const windowIndex = windows.map(w => ({ id: w.id, editIds: w.editIds, old: { start: w.old.start, count: w.old.count }, new: { start: w.new.start, count: w.new.count } }));
|
|
9
|
+
const payload = JSON.stringify({ ...base, windows: undefined, windowIndex, sourceMode: 'window-synthesis', windowNotice: '本请求综合检查所有窗口的共同影响,不重报单窗口问题;windowIndex仅为原始定位目录,不含源码,必须在批准catalog中读取相关旧新片段,不能仅凭目录认定缺陷。输出仍仅主file的发现,归因至少关联两个不同窗口的编辑;证据不足明确限制。' });
|
|
10
|
+
const budget = measure(payload);
|
|
11
|
+
return { payload, windows, windowIds: windows.map(w => w.id), budget };
|
|
12
|
+
}
|