@feng0531/dsh-code-review 0.85.13 → 0.86.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 +15 -0
- package/CORE-ALIGNMENT-TASKS.md +8 -2
- package/CORE-SOURCE-INVENTORY.md +4 -2
- package/README.md +17 -7
- package/RELEASE-NOTES.md +12 -17
- package/index.mjs +2 -2
- package/package.json +1 -1
- package/src/background-review.mjs +51 -1
- package/src/coverage-plan.mjs +16 -2
- package/src/dsh-model.mjs +80 -15
- package/src/group-primary.mjs +40 -0
- package/src/input-budget.mjs +2 -0
- package/src/model-contract.mjs +1 -0
- package/src/model-json.mjs +23 -1
- package/src/model-tools.mjs +58 -0
- package/src/model-wire-budget.mjs +32 -0
- package/src/primary-seeds.mjs +7 -2
- package/src/project-execution.mjs +3 -2
- package/src/project-review-service.mjs +47 -10
- package/src/retrieval-loop.mjs +12 -10
- package/src/review-artifacts.mjs +117 -0
- package/src/review-diagnostics.mjs +21 -9
- package/src/review-jobs.mjs +6 -2
- package/src/review-reader-allocation.mjs +75 -0
- package/src/review-reader-policy.mjs +1 -1
- package/src/review-resume.mjs +53 -0
- package/src/review.mjs +185 -54
- package/src/search-seeds.mjs +2 -1
- package/src/service.mjs +25 -8
- package/src/snapshot-reader.mjs +62 -22
- package/src/verification-loop.mjs +2 -1
- package/src/verification-seeds.mjs +18 -1
package/src/review.mjs
CHANGED
|
@@ -29,6 +29,9 @@ import { changesConcurrency, retrievalRounds } from './review-execution-policy.m
|
|
|
29
29
|
import { scheduleFileChains } from './file-chain-scheduler.mjs';
|
|
30
30
|
import { reviewReaderBudget, readerAdmission } from './review-reader-policy.mjs';
|
|
31
31
|
import { primarySeeds } from './primary-seeds.mjs';
|
|
32
|
+
import { createReviewReaderAllocation } from './review-reader-allocation.mjs';
|
|
33
|
+
import { prepareGroupPrimary, validateGroupPrimary } from './group-primary.mjs';
|
|
34
|
+
import { configureWireBudget, modelWireBudget } from './model-wire-budget.mjs';
|
|
32
35
|
|
|
33
36
|
export { REVIEW_INSTRUCTIONS };
|
|
34
37
|
export { PROJECT_REVIEW_INSTRUCTIONS };
|
|
@@ -99,6 +102,8 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
99
102
|
const maxRetrievalRounds = retrievalRounds(options.maxRetrievalRounds);
|
|
100
103
|
let checkpointFailed = false, checkpointFailure;
|
|
101
104
|
const progress = options.onProgress;
|
|
105
|
+
const resumeCheckpoint = options.onResumeCheckpoint;
|
|
106
|
+
if (resumeCheckpoint) options = { ...options, onResumeCheckpoint: async report => { try { await resumeCheckpoint(report); } catch (error) { checkpointFailed = true; checkpointFailure = error; controller.abort(error); throw error; } } };
|
|
102
107
|
options = { ...options, onProgress: progress ? async event => {
|
|
103
108
|
if (checkpointFailed) throw checkpointFailure;
|
|
104
109
|
try { await progress(structuredClone(event)); }
|
|
@@ -120,7 +125,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
120
125
|
if (options.enableBusinessGrouping === true && options.enableRetrieval !== true) throw new Error('Business grouping requires approved retrieval scope');
|
|
121
126
|
if (options.enableVerification === true && options.enableRetrieval !== true) throw new Error('Verification requires approved retrieval scope');
|
|
122
127
|
const readerLimits = reviewReaderBudget(input, options.readerBudgetPolicy ?? 'fixed');
|
|
123
|
-
const scope = options.enableRetrieval === true ? createRetrievalScope({ ...input, context: input.context ?? [] }, { ...readerLimits, signal }) : null;
|
|
128
|
+
const scope = options.enableRetrieval === true ? createRetrievalScope({ ...input, context: input.context ?? [] }, { ...readerLimits, signal, ...(options.optimizeReviewCalls === true ? { reuseResults: true } : {}) }) : null;
|
|
124
129
|
const report = { schemaVersion: 1, snapshotId: input.id, vcs: input.vcs, status: 'completed', files: [], findings: [], limitations: [], modelCalls: 0 };
|
|
125
130
|
validateBusinessRequirement(input.businessRequirement);
|
|
126
131
|
if(input.businessRequirement!==undefined)report.businessRequirement={hash:hash(input.businessRequirement),bytes:Buffer.byteLength(input.businessRequirement),status:'untrusted-constraint'};
|
|
@@ -140,21 +145,85 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
140
145
|
if(input.workingScope)report.workingScope=structuredClone(input.workingScope);
|
|
141
146
|
if(input.effectiveMergeSources)report.effectiveMergeSources=structuredClone(input.effectiveMergeSources);
|
|
142
147
|
report.rules = (input.rules ?? []).map(({ path, hash }) => ({ path, hash }));
|
|
148
|
+
if (scope) configureWireBudget(scope, { nativeOutputTools: options.nativeOutputTools === true });
|
|
143
149
|
if (options.enableGrouping === true) report.grouping = buildReviewGroups(input.files, inferFileRelations(input.files));
|
|
144
150
|
report.interactions = [];
|
|
145
151
|
const interactionPlans = scope && report.grouping ? report.grouping.groups.filter(g => g.fileIds.length > 1).map(group => ({ group, prepared: prepareInteractionInput(input, group, payload => initialInputBudget({ instructions, input: payload }, scope, maxInputBytes)) })) : [];
|
|
146
152
|
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' });
|
|
147
153
|
const fileResults = new Array(input.files.length);
|
|
154
|
+
const sharedState = report.orchestration = { modelMetrics: {}, modelCallPurposes: {} };
|
|
155
|
+
const retainArtifact = options.onModelArtifact ? async artifact => { const reference = await options.onModelArtifact(artifact); if (reference) (report.artifacts ??= []).push(reference); } : undefined;
|
|
156
|
+
report.resume = { reusedFiles: options.resumeState?.completed?.length ?? 0 };
|
|
157
|
+
const reserveShared = ({ correction = false } = {}) => {
|
|
158
|
+
if (!correction && options.remainingCalls?.() <= 0) throw Object.assign(new Error('Shared project model call limit exceeded'), { code: 'PROJECT_MODEL_CALL_LIMIT' });
|
|
159
|
+
if (report.modelCalls >= maxCalls) throw new Error('Model call budget exceeded');
|
|
160
|
+
report.modelCalls++;
|
|
161
|
+
};
|
|
162
|
+
const sharedModel = async request => {
|
|
163
|
+
let correction;
|
|
164
|
+
return model({ ...request, nativeOutputTools: options.nativeOutputTools === true, maxInputBytes: options.nativeOutputTools === true ? maxInputBytes : request.maxInputBytes, onModelArtifact: retainArtifact ? artifact => retainArtifact({ ...artifact, snapshotId: input.id, phase: request.reviewPhase }) : undefined,
|
|
165
|
+
onProtocolEvent: async info => { if (info.action === 'json-transport-recovery') (sharedState.modelRecoveries ??= []).push(info); await request.onProtocolEvent?.(info); },
|
|
166
|
+
onProtocolCorrection: async info => { reserveShared({ correction: true }); correction = info; await request.onProtocolCorrection?.(info); },
|
|
167
|
+
onModelEvent: async info => {
|
|
168
|
+
if (info.action === 'model-end') {
|
|
169
|
+
const phase = request.reviewPhase, metrics = sharedState.modelMetrics[phase] ??= { calls: 0, durationMs: 0, inputBytes: 0, totalOutputBytes: 0 };
|
|
170
|
+
metrics.calls++; metrics.durationMs += info.durationMs; metrics.inputBytes += info.inputBytes; metrics.totalOutputBytes += info.totalOutputBytes;
|
|
171
|
+
if (info.callPurpose) sharedState.modelCallPurposes[info.callPurpose] = (sharedState.modelCallPurposes[info.callPurpose] ?? 0) + 1;
|
|
172
|
+
if (info.callPurpose === 'json-correction' && correction) { (sharedState.modelCorrections ??= []).push({ action: 'explicit-json-correction', code: 'MODEL_INVALID_JSON', jsonIssue: correction.jsonIssue }); correction = undefined; }
|
|
173
|
+
}
|
|
174
|
+
await options.onProgress?.({ stage: info.action, snapshotId: input.id, phase: request.reviewPhase, ...info });
|
|
175
|
+
} });
|
|
176
|
+
};
|
|
177
|
+
const groupPrepared = new Map(), groupResults = new Map(), groupStarted = new Map();
|
|
178
|
+
let readerAllocation;
|
|
179
|
+
const stageScope = id => readerAllocation
|
|
180
|
+
? configureWireBudget(readerAllocation.scope(id), { nativeOutputTools: options.nativeOutputTools === true }) : scope;
|
|
181
|
+
const releaseStage = id => readerAllocation?.release(id);
|
|
182
|
+
const groupResult = async group => {
|
|
183
|
+
if (!groupResults.has(group.id)) {
|
|
184
|
+
groupStarted.set(group.id, performance.now());
|
|
185
|
+
groupResults.set(group.id, invoke(async request => {
|
|
186
|
+
const prepared = groupPrepared.get(group.id), groupScope = stageScope('group:' + group.id); const limits = [], reads = [];
|
|
187
|
+
const response = await retrievalLoop(sharedModel, { ...request, nativeOutputTools: options.nativeOutputTools === true, input: prepared.payload, instructions: prepared.instructions, reviewPhase: 'group-primary' }, groupScope, {
|
|
188
|
+
maxRounds: maxRetrievalRounds, maxInputBytes, beforeCall: reserveShared, correctProtocol: options.correctProtocol === true,
|
|
189
|
+
enableSearchSeeds: options.enableSearchSeeds === true,
|
|
190
|
+
onRetrieved: records => { reads.push(...records); (sharedState.retrievalAudit ??= []).push(...records); },
|
|
191
|
+
onBudgetLimited: info => { limits.push(info); (sharedState.retrievalLimits ??= []).push(info); },
|
|
192
|
+
});
|
|
193
|
+
const results = validateGroupPrimary(response, prepared, input, validateResponse, scope.catalog());
|
|
194
|
+
for (const generated of results.values()) {
|
|
195
|
+
generated.groupLimits = structuredClone(limits);
|
|
196
|
+
if (generated.groupRisk) {
|
|
197
|
+
generated.groupRisk.sourceReadCoverage = riskReadCoverage(generated.groupRisk.risks, reads, scope.catalog(), input.id);
|
|
198
|
+
if (generated.groupRisk.status !== 'completed') generated.limitations.push('组主审的合并风险计划缺失或无效,未补发规划调用;不表示已完成风险条件核查');
|
|
199
|
+
if (generated.groupRisk.sourceReadCoverage.some(item => item.unreadSourceIds.length)) generated.limitations.push('组风险计划请求的部分来源未在本次组生成读取;不表示已核查或排除风险条件');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return results;
|
|
203
|
+
}, { input: groupPrepared.get(group.id).payload, instructions: groupPrepared.get(group.id).instructions }, timeoutMs, signal));
|
|
204
|
+
}
|
|
205
|
+
try { return await groupResults.get(group.id); } finally { releaseStage('group:' + group.id); }
|
|
206
|
+
};
|
|
148
207
|
const runFile = async index => {
|
|
149
208
|
const file = input.files[index];
|
|
150
209
|
const state = { fileId: file.id, path: file.path, status: file.eligibility === 'reviewable' ? 'pending' : file.eligibility, reason: file.reason, ...(file.blockCode ? {blockCode:file.blockCode} : {}) };
|
|
151
|
-
|
|
210
|
+
let result = { state, findings: [], limitations: [] };
|
|
152
211
|
fileResults[index] = result;
|
|
153
212
|
try {
|
|
154
213
|
await options.onProgress?.({stage:'file-started',snapshotId:input.id,fileId:file.id,path:file.path,status:state.status});
|
|
155
214
|
if (state.status !== 'pending') return;
|
|
215
|
+
const reusable = options.resumeState?.completed?.find(row => row.fileId === file.id && row.path === file.path);
|
|
216
|
+
if (reusable) {
|
|
217
|
+
const prior = structuredClone(reusable.state);
|
|
218
|
+
for (const key of ['modelMetrics', 'modelCallPurposes', 'modelCorrections', 'modelRecoveries', 'retrievalAudit', 'protocolAttempts', 'protocolDiagnostic', 'responseAttempts', 'responseCorrections', 'anchorCorrections', 'retrievalLimits']) delete prior[key];
|
|
219
|
+
Object.assign(state, prior, { reused: true, reuseNotice: 'Prior completed evidence retained for an identical approved snapshot/configuration; no new model or evidence receipt claimed.' });
|
|
220
|
+
result.findings = structuredClone(reusable.findings).map(finding => ({ ...finding, reused: true, priorRun: { snapshotId: options.resumeState.snapshotId, evidenceStatus: 'historical-verified-evidence-not-current-run-receipts' } }));
|
|
221
|
+
result.limitations = structuredClone(reusable.limitations ?? []);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
156
224
|
if (signal?.aborted) { state.status = 'cancelled'; state.reason = '用户取消'; return; }
|
|
157
|
-
|
|
225
|
+
const assignedGroup = [...groupPrepared].find(([, value]) => value.fileIds.includes(file.id));
|
|
226
|
+
if (!assignedGroup && (report.modelCalls >= maxCalls || options.remainingCalls?.() <= 0)) { state.reason = '模型调用预算耗尽'; return; }
|
|
158
227
|
const prepared = preparePrimaryInput(input, file, { instructions, scope, grouping: report.grouping, maxInputBytes, riskPlanningMode: options.riskPlanningMode, focusPrimaryContext: options.focusPrimaryContext === true, enableRiskPlanning: options.enableRiskPlanning, skipSmallRiskPlans: options.skipSmallRiskPlans });
|
|
159
228
|
const { changes, payload } = prepared;
|
|
160
229
|
Object.assign(state, prepared.metadata);
|
|
@@ -165,20 +234,23 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
165
234
|
if(changes.precision==='envelope')result.limitations.push({fileId:file.id,text:'精确差异不可得,按保守包络分段检查;包络包含可能未变的行,不证明精确编辑或回归因果。'});
|
|
166
235
|
if (changes.status === 'limited') result.limitations.push({ fileId: file.id, text: '精确变更分析受限:' + changes.reason + ';仍按完整两侧内容审查' });
|
|
167
236
|
if (state.groupFallback) result.limitations.push({ fileId: file.id, text: '关联组上下文超过输入预算,回退单文件审查' });
|
|
168
|
-
if (state.initialInputBytes > maxInputBytes) { state.status = 'blocked'; state.reason = '输入超过预算,未截断或提交模型'; return; }
|
|
237
|
+
if (state.initialInputBytes > maxInputBytes) { state.status = 'blocked'; state.reason = '输入超过预算,未截断或提交模型'; releaseStage('primary:' + file.id); releaseStage('synthesis:' + file.id); releaseStage('verification:' + file.id); return; }
|
|
169
238
|
try {
|
|
170
239
|
const beforeCall = ({ reserved = false } = {}) => {
|
|
171
240
|
if (!reserved && options.remainingCalls?.() <= 0) throw Object.assign(new Error('Shared project model call limit exceeded'), { code: 'PROJECT_MODEL_CALL_LIMIT' });
|
|
172
241
|
if (report.modelCalls >= maxCalls) throw new Error('Model call budget exceeded');
|
|
173
242
|
report.modelCalls++;
|
|
174
243
|
};
|
|
244
|
+
let activeReader = scope, activeStage = 'primary';
|
|
175
245
|
const onBudgetLimited = info => {
|
|
246
|
+
if (activeStage !== 'primary') (state.secondaryRetrievalLimits ??= []).push({ phase: activeStage, ...info });
|
|
176
247
|
state.retrievalLimited = true;
|
|
177
248
|
(state.retrievalLimits ??= []).push(info);
|
|
178
249
|
result.limitations.push({ fileId: file.id, text: '检索预算受限,已保留现有结果但不能视为完整审查:' + info.reason });
|
|
179
250
|
};
|
|
180
251
|
let pendingStageCorrection;
|
|
181
252
|
const onProtocolEvent = async info => {
|
|
253
|
+
if (info.action === 'json-transport-recovery') (state.modelRecoveries ??= []).push(info);
|
|
182
254
|
if (info.action === 'retrieval-protocol-correction') pendingStageCorrection = { action: info.action, code: 'RETRIEVAL_PROTOCOL_ERROR' };
|
|
183
255
|
await options.onProgress?.({ stage: 'protocol-correction', fileId: file.id, path: file.path, ...info });
|
|
184
256
|
};
|
|
@@ -187,7 +259,8 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
187
259
|
// The executor has admitted this call; budget-vetoed retries never reach here.
|
|
188
260
|
if (pendingStageCorrection) { (state.modelCorrections ??= []).push(pendingStageCorrection); pendingStageCorrection = undefined; }
|
|
189
261
|
let pendingJsonCorrection;
|
|
190
|
-
return model({ ...request, callPurpose: request.callPurpose ?? request.reviewPhase ?? 'primary-initial', onProtocolEvent,
|
|
262
|
+
return model({ ...request, nativeOutputTools: options.nativeOutputTools === true, maxInputBytes: options.nativeOutputTools === true ? maxInputBytes : request.maxInputBytes, callPurpose: request.callPurpose ?? request.reviewPhase ?? 'primary-initial', onProtocolEvent,
|
|
263
|
+
onModelArtifact: retainArtifact ? artifact => retainArtifact({ ...artifact, snapshotId: input.id, fileId: file.id, path: file.path, phase: request.reviewPhase ?? 'primary' }) : undefined,
|
|
191
264
|
onModelEvent: async info => {
|
|
192
265
|
if (info.action === 'model-end') {
|
|
193
266
|
const phase = request.reviewPhase ?? 'primary';
|
|
@@ -211,40 +284,27 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
211
284
|
});
|
|
212
285
|
};
|
|
213
286
|
const executor = scope
|
|
214
|
-
? (request, onLimited = () => {}) => retrievalLoop(trackedModel, { ...request, onProtocolEvent },
|
|
287
|
+
? (request, onLimited = () => {}) => retrievalLoop(trackedModel, { ...request, nativeOutputTools: options.nativeOutputTools === true, onProtocolEvent }, activeReader, { correctProtocol:options.correctProtocol===true,maxRounds:maxRetrievalRounds,maxInputBytes, beforeCall, initialRetrieved: request.initialRetrieved ?? [], allowExhaustedInitial: request.allowExhaustedInitial === true, enableSearchSeeds: options.enableSearchSeeds === true && !request.reviewPhase?.includes('verification'), onBudgetLimited: info => { onBudgetLimited(info); onLimited(info); }, onRetrieved: records => {
|
|
215
288
|
(state.retrievalAudit ??= []).push(...records);
|
|
216
289
|
}, onTruncated: () => {
|
|
217
290
|
if (!state.retrievalTruncated) result.limitations.push({ fileId: file.id, text: '检索结果达到数量上限,部分匹配未提供给模型' });
|
|
218
291
|
state.retrievalTruncated = true;
|
|
219
292
|
} })
|
|
220
293
|
: request => { beforeCall(); return trackedModel(request); };
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (Buffer.byteLength(groupInput) + Buffer.byteLength(BUSINESS_GROUP_INSTRUCTIONS) > maxInputBytes) throw new Error('Business grouping input budget exceeded');
|
|
230
|
-
beforeCall();
|
|
231
|
-
const response = await trackedModel({ ...enclosingRequest, instructions: BUSINESS_GROUP_INSTRUCTIONS, input: groupInput, reviewPhase: 'business-grouping' });
|
|
232
|
-
enclosingRequest.signal.throwIfAborted();
|
|
233
|
-
grouped = validateBusinessGroups(response, input.files);
|
|
234
|
-
}
|
|
235
|
-
report.businessGrouping = { status: 'completed', ...grouped };
|
|
236
|
-
for (const group of grouped.groups.filter(g => g.fileIds.length > 1)) {
|
|
237
|
-
if (interactionPlans.some(p => [...p.group.fileIds].sort().join(',') === [...group.fileIds].sort().join(','))) continue;
|
|
238
|
-
const prepared = prepareInteractionInput(input, group, payload => initialInputBudget({ instructions, input: payload }, scope, maxInputBytes));
|
|
239
|
-
interactionPlans.push({ group, prepared });
|
|
240
|
-
if (prepared.status !== 'not-applicable') report.interactions.push({ groupId: group.id, fileIds: prepared.fileIds, status: prepared.status === 'ready' ? 'pending' : 'blocked', reason: prepared.reason, lifecycle: 'shares-last-primary-timeout' });
|
|
241
|
-
}
|
|
242
|
-
} catch (error) {
|
|
243
|
-
report.businessGrouping = { status: 'fallback', reason: error.message };
|
|
244
|
-
result.limitations.push({ text: '业务分组未完成,保留原分组:' + error.message });
|
|
245
|
-
enclosingRequest.signal.throwIfAborted();
|
|
246
|
-
}
|
|
294
|
+
const grouped = assignedGroup;
|
|
295
|
+
const groupedGeneration = grouped ? (await groupResult({ id: grouped[0] })).get(file.id) : null;
|
|
296
|
+
if (grouped) {
|
|
297
|
+
state.primaryGroupId = grouped[0]; state.primaryMethod = 'group-primary';
|
|
298
|
+
if (groupedGeneration.groupLimits?.length) {
|
|
299
|
+
state.retrievalLimited = true;
|
|
300
|
+
state.groupRetrievalLimits = structuredClone(groupedGeneration.groupLimits);
|
|
301
|
+
result.limitations.push(...state.groupRetrievalLimits.map(limit => ({ fileId: file.id, text: '组主审检索受限:' + limit.code + ';已接受的本成员成果保留,不表示完整审查' })));
|
|
247
302
|
}
|
|
303
|
+
if (groupedGeneration.groupRisk) (state.riskPlans ??= []).push({ ...groupedGeneration.groupRisk, method: 'inline', scope: 'group-all-edits', groupId: grouped[0], decision: { required: true, reason: 'group-inline-primary' } });
|
|
304
|
+
}
|
|
305
|
+
const chainTimeout = grouped && timeoutMs > 0 ? Math.max(0, timeoutMs - (performance.now() - groupStarted.get(grouped[0]))) : timeoutMs;
|
|
306
|
+
if (grouped && timeoutMs > 0 && chainTimeout <= 0) throw Object.assign(new Error('Model timeout'), { code: 'MODEL_TIMEOUT' });
|
|
307
|
+
await invoke(async enclosingRequest => {
|
|
248
308
|
const batches = [...(prepared.batches ?? [{ payload, windowIds: state.windowIds ?? [] }])];
|
|
249
309
|
if (prepared.synthesis?.status === 'ready') batches.push({ payload: prepared.synthesis.payload, windowIds: [], synthesis: true });
|
|
250
310
|
const matchingInteractionPlans = interactionPlans.filter(p => input.files.filter(f => p.group.fileIds.includes(f.id)).at(-1)?.id === file.id);
|
|
@@ -257,9 +317,14 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
257
317
|
enclosingRequest.signal.throwIfAborted();
|
|
258
318
|
const phase=batch.interaction?'file-interaction':batch.synthesis?'window-synthesis':'primary';
|
|
259
319
|
try{
|
|
260
|
-
|
|
320
|
+
activeStage = phase;
|
|
321
|
+
activeReader = groupedGeneration && !batch.synthesis && !batch.interaction ? scope : stageScope(batch.interaction ? 'interaction:' + batch.interaction.id : batch.synthesis ? 'synthesis:' + file.id : 'primary:' + file.id);
|
|
322
|
+
// Consuming an already validated group member performs no new source/model work.
|
|
323
|
+
const cachedPrimary = groupedGeneration && !batch.synthesis && !batch.interaction;
|
|
324
|
+
const admission = !cachedPrimary && activeReader && options.optimizeReviewCalls === true
|
|
325
|
+
? readerAdmission({ synthesis: batch.synthesis === true, interaction: !!batch.interaction, metadata: prepared.metadata }, activeReader.budget()) : { permitted: true };
|
|
261
326
|
if (!admission.permitted) {
|
|
262
|
-
state.readerAdmission = { code: admission.code, remaining:
|
|
327
|
+
state.readerAdmission = { code: admission.code, remaining: activeReader.budget() };
|
|
263
328
|
if (batch.synthesis || batch.interaction) throw Object.assign(new Error('Required evidence budget unavailable before stage admission'), { code: admission.code });
|
|
264
329
|
state.reason = '检索预算不足以取得本阶段所需证据,未启动模型;需重新预览补审';
|
|
265
330
|
state.failureCode = admission.code;
|
|
@@ -267,14 +332,14 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
267
332
|
continue;
|
|
268
333
|
}
|
|
269
334
|
let batchPayload = batch.payload;
|
|
270
|
-
const inlineRisk = prepared.inlineRisk === true && batch.round === 1 && !batch.synthesis && !batch.interaction;
|
|
335
|
+
const inlineRisk = !groupedGeneration && prepared.inlineRisk === true && batch.round === 1 && !batch.synthesis && !batch.interaction;
|
|
271
336
|
const batchInstructions = inlineRisk ? prepared.instructions : instructions;
|
|
272
337
|
let riskEntry, activeRisks = [];
|
|
273
338
|
if (inlineRisk) {
|
|
274
339
|
riskEntry = { status: 'pending', method: 'inline', windowIds: batch.windowIds, scope: state.sourceMode === 'change-windows' ? 'all-edits' : 'full-file', risks: [], decision: { required: true, reason: 'inline-primary' } };
|
|
275
340
|
(state.riskPlans ??= []).push(riskEntry);
|
|
276
341
|
}
|
|
277
|
-
if (options.enableRiskPlanning === true && !inlineRisk && !batch.synthesis && batch.round === 1) {
|
|
342
|
+
if (options.enableRiskPlanning === true && (!groupedGeneration || batch.interaction) && !inlineRisk && !batch.synthesis && batch.round === 1) {
|
|
278
343
|
const reusable = !batch.interaction && riskInput.cacheable;
|
|
279
344
|
riskEntry = reusable ? primaryRiskPlan : undefined;
|
|
280
345
|
if (!riskEntry) {
|
|
@@ -315,15 +380,15 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
315
380
|
}
|
|
316
381
|
}
|
|
317
382
|
}
|
|
318
|
-
if(report.modelCalls>=maxCalls)throw new Error('Model call budget exceeded before next review pass');
|
|
383
|
+
if(report.modelCalls>=maxCalls && !(groupedGeneration && !batch.synthesis && !batch.interaction))throw new Error('Model call budget exceeded before next review pass');
|
|
319
384
|
if(rounds>1) state.reviewPasses[batchIndex].status='running';
|
|
320
|
-
const request = { ...enclosingRequest, instructions: batchInstructions, input: batchPayload, reviewPhase: phase, allowExhaustedInitial: options.optimizeReviewCalls === true && !batch.synthesis && !batch.interaction && prepared.metadata.contextMode !== 'approved-retrieval' };
|
|
385
|
+
const request = { ...enclosingRequest, nativeOutputTools: options.nativeOutputTools === true, instructions: batchInstructions, input: batchPayload, reviewPhase: phase, allowExhaustedInitial: options.optimizeReviewCalls === true && !batch.synthesis && !batch.interaction && prepared.metadata.contextMode !== 'approved-retrieval' };
|
|
321
386
|
let auditStart = state.retrievalAudit?.length ?? 0;
|
|
322
387
|
let finalResponseOnly = false;
|
|
323
388
|
const generate = async attempt => {
|
|
324
389
|
if (!initialInputBudget(attempt, scope, maxInputBytes).fits) throw new Error('Review correction input budget exceeded');
|
|
325
390
|
const initialRetrieved = scope && options.enablePrimarySeeds === true && maxRetrievalRounds > 0 && !batch.synthesis && !batch.interaction
|
|
326
|
-
? primarySeeds(
|
|
391
|
+
? primarySeeds(activeReader, attempt, { maxInputBytes, signal: attempt.signal }) : [];
|
|
327
392
|
return executor({ ...attempt, initialRetrieved }, () => { finalResponseOnly = true; });
|
|
328
393
|
};
|
|
329
394
|
const validate = response => {
|
|
@@ -347,7 +412,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
347
412
|
if (!batch.interaction && state.sourceMode === 'change-windows') validateWindowFindings(generated.findings, batch.synthesis ? prepared.synthesis.windows : JSON.parse(batch.payload).windows, batch.synthesis === true);
|
|
348
413
|
return generated;
|
|
349
414
|
};
|
|
350
|
-
const generated = await correctedResponse(generate, validate, request, {
|
|
415
|
+
const generated = groupedGeneration && !batch.synthesis && !batch.interaction ? structuredClone(groupedGeneration) : await correctedResponse(generate, validate, request, {
|
|
351
416
|
enabled: options.correctProtocol === true,
|
|
352
417
|
canCorrect: () => !finalResponseOnly,
|
|
353
418
|
onCorrection: async info => {
|
|
@@ -368,7 +433,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
368
433
|
}
|
|
369
434
|
delete generated.inlineRisks;
|
|
370
435
|
}
|
|
371
|
-
if (options.enableRiskPlanning === true && !batch.synthesis && batch.round === 1) {
|
|
436
|
+
if (options.enableRiskPlanning === true && (!groupedGeneration || batch.interaction) && !batch.synthesis && batch.round === 1) {
|
|
372
437
|
const coverage = riskReadCoverage(activeRisks,(state.retrievalAudit??[]).slice(auditStart),scope?.catalog()??[],input.id);
|
|
373
438
|
(riskEntry.generations ??= []).push({ round: batch.round, windowIds: batch.windowIds, coverage });
|
|
374
439
|
// Preserve the single-generation field without unioning evidence across windows.
|
|
@@ -381,10 +446,12 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
381
446
|
result.findings.push(...generated.findings);
|
|
382
447
|
if (generated.findings.some(f => f.attribution.status === 'missing')) result.limitations.push({ fileId: file.id, text: '部分发现缺少变更归因,仅校验了定位,尚不能确认属于本次回归' });
|
|
383
448
|
result.limitations.push(...generated.limitations.map(text => ({ fileId: file.id, text })));
|
|
449
|
+
if (!groupedGeneration && !scheduled.slice(batchIndex + 1).some(next => !next.synthesis && !next.interaction)) releaseStage('primary:' + file.id);
|
|
384
450
|
if (options.enableVerification === true && generated.findings.length) {
|
|
385
451
|
for (const finding of generated.findings) finding.verification = { status: 'pending', causality: 'unverified' };
|
|
386
452
|
try {
|
|
387
|
-
|
|
453
|
+
activeStage = 'verification';
|
|
454
|
+
const verdicts = await verificationLoop(trackedModel, { ...request, onProtocolEvent, reviewPhase: 'verification' }, generated.findings, stageScope('verification:' + file.id), {
|
|
388
455
|
correctProtocol:options.correctProtocol===true,maxRounds:maxRetrievalRounds,maxInputBytes, beforeCall, onBudgetLimited,
|
|
389
456
|
onRetrieved: records => { (state.retrievalAudit ??= []).push(...records); },
|
|
390
457
|
onTruncated: () => { result.limitations.push({ fileId: file.id, text: '复核检索结果达到数量上限' }); },
|
|
@@ -394,6 +461,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
394
461
|
if (verdicts.some(v => v.verdict === 'supported' && v.regression?.classification === 'uncertain')) result.limitations.push({ fileId: file.id, text: '部分模型支持候选缺少充分前后对照,不能确认由本次变更引入' });
|
|
395
462
|
if (verdicts.some(v => v.verdict === 'uncertain')) result.limitations.push({ fileId: file.id, text: '部分候选复核仍不确定,不能视为已确认或已排除' });
|
|
396
463
|
} catch (error) {
|
|
464
|
+
if (error?.code === 'MODEL_ARTIFACT_FAILED') throw error;
|
|
397
465
|
for (const finding of generated.findings) finding.verification = { status: 'incomplete', causality: 'unverified' };
|
|
398
466
|
result.limitations.push({ fileId: file.id, text: '候选复核未完成:' + (error?.message ?? String(error)) });
|
|
399
467
|
request.signal.throwIfAborted();
|
|
@@ -413,7 +481,11 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
413
481
|
await options.onProgress?.({stage:'window-complete',snapshotId:input.id,fileId:file.id,path:file.path,windowIds:batch.windowIds,round:batch.round,findings:generated.findings,windowCoverage:state.windowCoverage});
|
|
414
482
|
}catch(error){
|
|
415
483
|
enclosingRequest.signal.throwIfAborted();
|
|
416
|
-
if(phase==='primary'
|
|
484
|
+
if(phase==='primary'||error.code==='MODEL_ARTIFACT_FAILED')throw error;
|
|
485
|
+
if (['Model call budget exceeded', 'Model call budget exceeded before next review pass'].includes(error.message) || error.code === 'PROJECT_MODEL_CALL_LIMIT') {
|
|
486
|
+
result.limitations.push({ fileId: file.id, text: phase + '模型调用额度不足,尚未启动的阶段保留 pending;已完成主审成果保留' });
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
417
489
|
const failure={phase,status:'failed',code:error.code??'REVIEW_EXECUTION_FAILED',reason:error.message,round:batch.round,...(error.attempts?{attempts:error.attempts}:{}),...(error.diagnostic?{diagnostic:error.diagnostic}:{})};
|
|
418
490
|
(state.stageFailures??=[]).push(failure);
|
|
419
491
|
if(batch.synthesis)state.synthesisStatus='failed';
|
|
@@ -423,19 +495,20 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
423
495
|
await options.onProgress?.({stage:'synthesis-failed',snapshotId:input.id,fileId:file.id,path:file.path,...failure,windowCoverage:state.windowCoverage});
|
|
424
496
|
}
|
|
425
497
|
}
|
|
426
|
-
}, { instructions, input: payload },
|
|
498
|
+
}, { instructions, input: payload }, chainTimeout, signal);
|
|
427
499
|
if (signal?.aborted) throw signal.reason;
|
|
428
500
|
|
|
429
501
|
state.status = state.retrievalLimited ? 'pending' : 'completed';
|
|
430
502
|
if (state.retrievalLimited) {
|
|
431
|
-
const codes = new Set((state.retrievalLimits ?? []).map(limit => limit.code));
|
|
503
|
+
const codes = new Set([...(state.retrievalLimits ?? []), ...(state.groupRetrievalLimits ?? [])].map(limit => limit.code));
|
|
432
504
|
state.reason = codes.has('RETRIEVAL_ROUND_BUDGET_EXCEEDED')
|
|
433
|
-
? '
|
|
434
|
-
: '共享读取调用或输出预算受限,未完成完整审查;已有发现保留,不表示通过';
|
|
505
|
+
? '单阶段检索轮数上限达到,未完成完整审查;读取额度可能仍有余量,已有发现保留,不表示通过'
|
|
506
|
+
: (readerAllocation ? '阶段取证分配或共享读取预算受限,未完成完整审查;其他阶段保留额度不表示本阶段已完成,已有发现保留' : '共享读取调用或输出预算受限,未完成完整审查;已有发现保留,不表示通过');
|
|
435
507
|
if (codes.size === 1) state.failureCode = [...codes][0];
|
|
436
508
|
}
|
|
437
509
|
|
|
438
510
|
} catch (error) {
|
|
511
|
+
if (error?.code === 'MODEL_ARTIFACT_FAILED') { checkpointFailed = true; checkpointFailure = error; controller.abort(error); throw error; }
|
|
439
512
|
state.status = signal?.aborted ? 'cancelled' : error?.code === 'PROJECT_MODEL_CALL_LIMIT' ? 'pending' : 'failed';
|
|
440
513
|
state.reason = error?.message ?? String(error);
|
|
441
514
|
if(error?.attempts)state.protocolAttempts=error.attempts;
|
|
@@ -445,13 +518,68 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
445
518
|
state.failureCode = signal?.aborted ? 'CANCELLED' : ['MODEL_INVALID_JSON','MODEL_INCOMPLETE_OUTPUT','MODEL_TIMEOUT','MODEL_INVALID_READ_REQUEST','MODEL_OUTPUT_LIMIT','MODEL_PROTOCOL_ERROR','MODEL_INVALID_REVIEW_RESPONSE','MODEL_INVALID_FINDING_IDENTITY','MODEL_INVALID_FINDING_SEVERITY','MODEL_MISSING_FINDING_FIELD','MODEL_INVALID_ANCHOR','MODEL_ANCHOR_MISMATCH','READER_CALL_BUDGET_EXCEEDED','READER_OUTPUT_BUDGET_EXCEEDED','RETRIEVAL_ROUND_BUDGET_EXCEEDED','RETRIEVAL_FINAL_RESPONSE_REQUIRED','PROJECT_MODEL_CALL_LIMIT'].includes(error?.code) ? error.code : 'REVIEW_EXECUTION_FAILED';
|
|
446
519
|
}
|
|
447
520
|
} finally {
|
|
521
|
+
if (readerAllocation && file.eligibility === 'reviewable' && !options.resumeState?.completed?.some(row => row.fileId === file.id)) {
|
|
522
|
+
if (![...groupPrepared.values()].some(group => group.fileIds.includes(file.id))) releaseStage('primary:' + file.id);
|
|
523
|
+
releaseStage('verification:' + file.id);
|
|
524
|
+
releaseStage('synthesis:' + file.id);
|
|
525
|
+
for (const plan of interactionPlans.filter(plan => input.files.filter(member => plan.group.fileIds.includes(member.id)).at(-1)?.id === file.id)) releaseStage('interaction:' + plan.group.id);
|
|
526
|
+
}
|
|
527
|
+
state.resumeEligible = state.status === 'completed' && !state.stageFailures?.length && !state.reviewPasses?.some(pass => pass.status !== 'completed') && !state.windowCoverage?.pending?.length && !state.retrievalLimited && result.findings.every(f => f.verification?.status === 'completed') && report.interactions.filter(item => item.fileIds?.includes(file.id)).every(item => item.status === 'completed');
|
|
448
528
|
await options.onProgress?.({stage:'file-complete',snapshotId:input.id,file:state,findings:result.findings});
|
|
529
|
+
await options.onResumeCheckpoint?.({ ...report, files: fileResults.filter(Boolean).map(item => item.state), findings: fileResults.filter(Boolean).flatMap(item => item.findings), limitations: [...report.limitations, ...fileResults.filter(Boolean).flatMap(item => item.limitations)] });
|
|
449
530
|
}
|
|
450
531
|
};
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
532
|
+
if (options.resumeState?.businessGrouping) report.businessGrouping = structuredClone(options.resumeState.businessGrouping);
|
|
533
|
+
for (const interaction of report.interactions) {
|
|
534
|
+
if (interaction.fileIds.every(id => options.resumeState?.completed?.some(row => row.fileId === id))) Object.assign(interaction, { status: 'completed', reused: true });
|
|
535
|
+
}
|
|
536
|
+
// Freeze grouping before admitting file chains, without serializing a whole first file.
|
|
537
|
+
if (options.enableBusinessGrouping === true && prepareBusinessGroups(input.files) && !signal.aborted) {
|
|
538
|
+
try {
|
|
539
|
+
const local = report.businessGrouping?.groups ? report.businessGrouping : localBusinessGroups(input.files);
|
|
540
|
+
const grouped = local ?? await invoke(async request => {
|
|
541
|
+
const groupInput = prepareBusinessGroups(input.files);
|
|
542
|
+
if ((options.nativeOutputTools === true ? !modelWireBudget({ input: groupInput, instructions: BUSINESS_GROUP_INSTRUCTIONS, nativeOutputTools: true }, maxInputBytes).fits : Buffer.byteLength(groupInput) + Buffer.byteLength(BUSINESS_GROUP_INSTRUCTIONS) > maxInputBytes)) throw new Error('Business grouping input budget exceeded');
|
|
543
|
+
reserveShared();
|
|
544
|
+
return validateBusinessGroups(await sharedModel({ ...request, reviewPhase: 'business-grouping', callPurpose: 'business-grouping' }), input.files);
|
|
545
|
+
}, { input: prepareBusinessGroups(input.files), instructions: BUSINESS_GROUP_INSTRUCTIONS }, timeoutMs, signal);
|
|
546
|
+
report.businessGrouping = { status: 'completed', ...grouped };
|
|
547
|
+
for (const group of grouped.groups.filter(g => g.fileIds.length > 1)) {
|
|
548
|
+
if (interactionPlans.some(p => [...p.group.fileIds].sort().join(',') === [...group.fileIds].sort().join(','))) continue;
|
|
549
|
+
const prepared = prepareInteractionInput(input, group, payload => initialInputBudget({ instructions, input: payload }, scope, maxInputBytes));
|
|
550
|
+
interactionPlans.push({ group, prepared });
|
|
551
|
+
if (prepared.status !== 'not-applicable') report.interactions.push({ groupId: group.id, fileIds: prepared.fileIds, status: group.fileIds.every(id => options.resumeState?.completed?.some(row => row.fileId === id)) ? 'completed' : prepared.status === 'ready' ? 'pending' : 'blocked', reason: prepared.reason, lifecycle: 'shares-last-primary-timeout' });
|
|
552
|
+
}
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (signal.aborted || checkpointFailed || error.code === 'MODEL_ARTIFACT_FAILED') throw error;
|
|
555
|
+
report.businessGrouping = { status: 'fallback', reason: error.message };
|
|
556
|
+
report.limitations.push({ text: '业务分组未完成,保留原分组:' + error.message });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (options.enableGroupPrimary === true && scope && rounds === 1) {
|
|
560
|
+
const seen = new Set();
|
|
561
|
+
const groups = report.businessGrouping?.groups ?? report.grouping?.groups ?? [];
|
|
562
|
+
for (const group of groups) {
|
|
563
|
+
if (group.fileIds.some(id => seen.has(id) || options.resumeState?.completed?.some(row => row.fileId === id))) continue;
|
|
564
|
+
const prepared = prepareGroupPrimary(input, group, { instructions, scope, maxInputBytes, focusPrimaryContext: options.focusPrimaryContext === true, enableRiskPlanning: options.enableRiskPlanning === true });
|
|
565
|
+
if (!prepared) continue;
|
|
566
|
+
groupPrepared.set(group.id, prepared);
|
|
567
|
+
for (const id of group.fileIds) seen.add(id);
|
|
568
|
+
}
|
|
569
|
+
report.primaryGroups = [...groupPrepared].map(([groupId, p]) => ({ groupId, fileIds: p.fileIds, mode: 'group-primary' }));
|
|
570
|
+
}
|
|
571
|
+
if (scope && options.optimizeReviewCalls === true) {
|
|
572
|
+
const units = [];
|
|
573
|
+
const reusable = new Set((options.resumeState?.completed ?? []).map(row => row.fileId));
|
|
574
|
+
for (const [id, prepared] of groupPrepared) units.push({ id: 'group:' + id, weight: prepared.fileIds.length * 4 });
|
|
575
|
+
for (const file of input.files.filter(file => file.eligibility === 'reviewable' && !reusable.has(file.id))) {
|
|
576
|
+
const prepared = preparePrimaryInput(input, file, { instructions, scope, grouping: report.grouping, maxInputBytes, riskPlanningMode: options.riskPlanningMode, focusPrimaryContext: options.focusPrimaryContext === true, enableRiskPlanning: options.enableRiskPlanning, skipSmallRiskPlans: options.skipSmallRiskPlans });
|
|
577
|
+
if (![...groupPrepared.values()].some(group => group.fileIds.includes(file.id))) units.push({ id: 'primary:' + file.id, weight: 4 * Math.max(1, prepared.batches?.length ?? 1) });
|
|
578
|
+
if (prepared.synthesis?.status === 'ready') units.push({ id: 'synthesis:' + file.id, weight: 2 });
|
|
579
|
+
if (options.enableVerification === true) units.push({ id: 'verification:' + file.id, weight: 2 });
|
|
580
|
+
}
|
|
581
|
+
for (const plan of interactionPlans.filter(plan => plan.prepared.status === 'ready' && !plan.group.fileIds.every(id => reusable.has(id)))) units.push({ id: 'interaction:' + plan.group.id, weight: 2 });
|
|
582
|
+
if (units.length) readerAllocation = createReviewReaderAllocation(scope, units);
|
|
455
583
|
}
|
|
456
584
|
const dependencies = new Map();
|
|
457
585
|
for (const plan of interactionPlans.filter(p => p.prepared.status === 'ready')) {
|
|
@@ -459,7 +587,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
459
587
|
const owner = members.at(-1);
|
|
460
588
|
if (owner !== undefined) dependencies.set(owner, [...new Set([...(dependencies.get(owner) ?? []), ...members.slice(0, -1)])]);
|
|
461
589
|
}
|
|
462
|
-
await scheduleFileChains(input.files.map((_, index) => index)
|
|
590
|
+
await scheduleFileChains(input.files.map((_, index) => index), async index => {
|
|
463
591
|
try { await runFile(index); }
|
|
464
592
|
catch (error) { controller.abort(error); throw error; }
|
|
465
593
|
}, dependencies, fileConcurrency);
|
|
@@ -485,8 +613,11 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
485
613
|
}
|
|
486
614
|
report.regressionCoverage = Object.fromEntries(['introduced', 'preexisting', 'uncertain', 'unassessed'].map(classification => [classification, report.findings.filter(f => (f.verification?.regression?.classification ?? 'unassessed') === classification).length]));
|
|
487
615
|
report.findingGroups = groupDuplicateFindings(report.findings);
|
|
616
|
+
for (const state of report.files) state.resumeEligible = state.status === 'completed' && !state.stageFailures?.length && !state.reviewPasses?.some(pass => pass.status !== 'completed') && !state.windowCoverage?.pending?.length && !state.retrievalLimited && report.findings.filter(f => f.fileId === state.fileId).every(f => f.verification?.status === 'completed') && report.interactions.filter(item => item.fileIds?.includes(state.fileId)).every(item => item.status === 'completed');
|
|
617
|
+
await options.onResumeCheckpoint?.(report);
|
|
488
618
|
report.followup = coverageFollowup(report);
|
|
489
619
|
if (scope) report.readerBudget = { ...scope.usage(), ...scope.budget(), ...readerLimits };
|
|
620
|
+
if (readerAllocation) report.readerAllocation = readerAllocation.summary();
|
|
490
621
|
report.diagnostics = reviewDiagnostics(report, { reviewWallMs: performance.now() - startedAt });
|
|
491
622
|
return report;
|
|
492
623
|
}
|
|
@@ -518,7 +649,7 @@ function renderFinding(output, finding, index, duplicates) {
|
|
|
518
649
|
const verification = finding.verification;
|
|
519
650
|
if (verification) {
|
|
520
651
|
const label = verification.status !== 'completed' ? '未完成' : ({ supported: '模型复核支持', refuted: '模型复核反驳(保留候选供追溯)', uncertain: '不确定' }[verification.verdict] ?? '未知');
|
|
521
|
-
output.push('- 复核状态:' + label + ';不是事实或因果证明');
|
|
652
|
+
output.push('- 复核状态:' + label + (finding.reused ? '(相同快照的既往轮次结果,本次未重新取证)' : '') + ';不是事实或因果证明');
|
|
522
653
|
if (verification.reason) output.push('- 复核理由:' + safe(verification.reason));
|
|
523
654
|
for (const ref of verification.evidence ?? []) output.push('- 复核证据:' + safe(ref.sourceId) + ':' + ref.start + '(' + ref.count + '行) · SHA256 ' + safe(ref.hash) + ' · ' + safe(ref.text));
|
|
524
655
|
}
|
|
@@ -551,7 +682,7 @@ export function markdownReport(report) {
|
|
|
551
682
|
}
|
|
552
683
|
for (const file of report.files) if (file.windowCoverage) output.push('- ' + safe(file.path) + ':变更窗口已完成 ' + file.windowCoverage.completed.length + ',待审 ' + file.windowCoverage.pending.length + ';仅表示窗口分析完成,不证明全文正确');
|
|
553
684
|
for (const file of report.files) if (file.synthesisStatus) output.push('- ' + safe(file.path) + ':跨窗口综合 ' + safe(file.synthesisStatus) + ';不等于事实正确性证明');
|
|
554
|
-
for (const interaction of report.interactions ?? []) output.push('- 跨文件综合 ' + safe(interaction.groupId) + ':' + safe(interaction.status) + '
|
|
685
|
+
for (const interaction of report.interactions ?? []) output.push('- 跨文件综合 ' + safe(interaction.groupId) + ':' + safe(interaction.status) + (interaction.failureCode ? ' · ' + safe(interaction.failureCode) : '') + (interaction.reason ? ' — ' + safe(interaction.reason) : '') + ';与组末主文件共享生命周期,不证明完整交互覆盖');
|
|
555
686
|
if (report.rules?.length) output.push('', '## 项目规则', '规则仅为已批准约束数据,不扩大权限或证明缺陷。', ...report.rules.map(rule => '- ' + safe(rule.path) + ' · SHA256 ' + safe(rule.hash)));
|
|
556
687
|
if (report.followup?.suggestedPaths.length) output.push('', '## 后续选择建议', '仅为原快照未完成项;不会自动续审。再次执行需新预览、新审批,并显式选择规则和上下文。阻断项需先处理原因;关联上下文不等于主文件已审查。', ...report.followup.items.filter(item => item.followup).map(item => '- ' + safe(item.path) + ':' + item.status + (item.incompleteVerification ? '(候选复核未完成)' : '')));
|
|
557
688
|
output.push('', '## 审查发现');
|
package/src/search-seeds.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { retrievalRequest } from './retrieval-loop.mjs';
|
|
2
|
+
import { modelWireBudget } from './model-wire-budget.mjs';
|
|
2
3
|
|
|
3
4
|
const validLine = value => Number.isSafeInteger(value) && value > 0;
|
|
4
5
|
const capacity = () => Object.assign(new Error('Optional search seed unavailable'), { code: 'SEARCH_SEED_CAPACITY' });
|
|
@@ -59,7 +60,7 @@ export function searchSeeds(scope, request, records, { existing = [], maxReads =
|
|
|
59
60
|
check();
|
|
60
61
|
const remaining = scope.budget();
|
|
61
62
|
if (typeof next?.input !== 'string' || typeof next.instructions !== 'string') throw new Error('Invalid search seed request builder');
|
|
62
|
-
if (Buffer.byteLength(next.input) + Buffer.byteLength(next.instructions) > maxInputBytes || remaining.remainingCalls < 4 || remaining.remainingOutputBytes < 16 * 1024) throw capacity();
|
|
63
|
+
if ((request.nativeOutputTools === true ? !modelWireBudget(next, maxInputBytes).fits : Buffer.byteLength(next.input) + Buffer.byteLength(next.instructions) > maxInputBytes) || remaining.remainingCalls < 4 || remaining.remainingOutputBytes < 16 * 1024) throw capacity();
|
|
63
64
|
return { request: operation, result };
|
|
64
65
|
});
|
|
65
66
|
seeds.push(item);
|
package/src/service.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { configureWireBudget } from './model-wire-budget.mjs';
|
|
1
2
|
import {REVIEW_RUNTIME} from './runtime-identity.mjs';
|
|
3
|
+
import { buildResumeState, validateResumeState, resumePreview } from './review-resume.mjs';
|
|
2
4
|
import { reviewDiagnostics } from './review-diagnostics.mjs';
|
|
3
5
|
import {createProjectReviewService} from './project-review-service.mjs';
|
|
4
6
|
import {svnDiagnostic} from './svn-diagnostics.mjs';
|
|
@@ -53,15 +55,16 @@ function buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcur
|
|
|
53
55
|
return scopeNotice + (snapshot.history?.vcs==='svn'&&options.autoContext===true?'SVN历史autoContext为精确依赖定位读取固定版本父目录有界清单,包含同目录非匹配名字元数据;仅匹配捕获正文发送模型,不扫描全仓,不用工作树替代历史版本。':'') + (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + `执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多${maxRetrievalRounds}轮检索;独立文件审查链最多${changesConcurrency}条并发,不设插件内部默认时间或总模型调用次数截止,遵循宿主/用户取消;输入字节和检索轮次保护仍保留;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。`;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
function createSessionReviewService(llm, { capture = captureSnapshot, now = Date.now, ttlMs = 300000, maxPreviews = 8, allowModelSending = false, enableRiskPlanning = false, skipSmallRiskPlans = true, reviewRounds = 1, enableBusinessGrouping = false, enableAnchorCorrection = false, changesConcurrency = 2, maxRetrievalRounds = 3, riskPlanningMode = 'separate', readerBudgetPolicy = 'fixed', optimizeReviewCalls = false, enablePrimarySeeds = false, focusPrimaryContext = false, enableSearchSeeds = false, authorize = async () => false } = {}) {
|
|
58
|
+
function createSessionReviewService(llm, { capture = captureSnapshot, now = Date.now, ttlMs = 300000, maxPreviews = 8, allowModelSending = false, enableRiskPlanning = false, skipSmallRiskPlans = true, reviewRounds = 1, enableBusinessGrouping = false, enableAnchorCorrection = false, changesConcurrency = 2, maxRetrievalRounds = 3, riskPlanningMode = 'separate', readerBudgetPolicy = 'fixed', optimizeReviewCalls = false, enablePrimarySeeds = false, focusPrimaryContext = false, enableSearchSeeds = false, nativeOutputTools = false, enableGroupPrimary = false, authorize = async () => false } = {}) {
|
|
57
59
|
for (const value of [ttlMs, maxPreviews]) if (!Number.isSafeInteger(value) || value < 1) throw new Error('Invalid preview limit');
|
|
58
60
|
changesConcurrency = reviewConcurrency(changesConcurrency);
|
|
59
61
|
maxRetrievalRounds = reviewRetrievalRounds(maxRetrievalRounds);
|
|
60
62
|
riskPlanningMode = validateRiskPlanningMode(riskPlanningMode);
|
|
61
63
|
reviewReaderBudget({ files: [] }, readerBudgetPolicy);
|
|
62
|
-
for (const value of [optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds]) if (typeof value !== 'boolean') throw new Error('Invalid review optimization option');
|
|
64
|
+
for (const value of [optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds, nativeOutputTools, enableGroupPrimary]) if (typeof value !== 'boolean') throw new Error('Invalid review optimization option');
|
|
63
65
|
const lifetime = new AbortController();
|
|
64
66
|
const bind = exec => ({ ...exec, signal: exec.signal ? AbortSignal.any([exec.signal, lifetime.signal]) : lifetime.signal });
|
|
67
|
+
const resumeOptions = (options, route, rounds) => ({ captureOptions: options, route, runtime: REVIEW_RUNTIME, instructionsHash: hash(REVIEW_INSTRUCTIONS), execution: { rounds, enableRiskPlanning, skipSmallRiskPlans, enableBusinessGrouping, enableAnchorCorrection, changesConcurrency, maxRetrievalRounds, riskPlanningMode, readerBudgetPolicy, optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds, nativeOutputTools, enableGroupPrimary } });
|
|
65
68
|
const previews = new Map();
|
|
66
69
|
let busy = false;
|
|
67
70
|
function prune() { for (const [id, p] of previews) if (p.expires <= now()) previews.delete(id); }
|
|
@@ -70,7 +73,8 @@ function createSessionReviewService(llm, { capture = captureSnapshot, now = Date
|
|
|
70
73
|
exec = bind(exec);
|
|
71
74
|
exec.signal.throwIfAborted();
|
|
72
75
|
if (busy) throw new Error('Review service busy');
|
|
73
|
-
if (!args || Object.keys(args).some(k => !['repositoryPath', 'selectedPaths', 'contextPaths', 'rulePaths', 'autoContext', 'commit', 'baseRevision', 'targetRevision','businessRequirement','callerScopePaths','oldContextPaths','reviewRounds','svnRevision','svnBaseRevision','svnTargetRevision','svnAllowNetwork','svnEffectiveMergeinfo','svnBasePath','svnTargetPath','svnReplacementBaseRevision','svnReplacementAllowNetwork','changeScope'].includes(k))) throw new Error('Unknown preview argument');
|
|
76
|
+
if (!args || Object.keys(args).some(k => !['repositoryPath', 'selectedPaths', 'contextPaths', 'rulePaths', 'autoContext', 'commit', 'baseRevision', 'targetRevision','businessRequirement','callerScopePaths','oldContextPaths','reviewRounds','svnRevision','svnBaseRevision','svnTargetRevision','svnAllowNetwork','svnEffectiveMergeinfo','svnBasePath','svnTargetPath','svnReplacementBaseRevision','svnReplacementAllowNetwork','changeScope','resumeFromJobId'].includes(k))) throw new Error('Unknown preview argument');
|
|
77
|
+
if (args.resumeFromJobId !== undefined && (typeof args.resumeFromJobId !== 'string' || !/^[a-f0-9-]{36}$/.test(args.resumeFromJobId))) throw new Error('Invalid resume job id');
|
|
74
78
|
if (args.autoContext !== undefined && typeof args.autoContext !== 'boolean') throw new Error('Invalid autoContext option');
|
|
75
79
|
for (const key of ['selectedPaths', 'contextPaths', 'rulePaths', 'oldContextPaths']) if (args[key] !== undefined && (!Array.isArray(args[key]) || args[key].length > (key === 'rulePaths' ? 4 : key === 'oldContextPaths' ? 20 : 200) || args[key].some(p => typeof p !== 'string'))) throw new Error('Invalid path list');
|
|
76
80
|
const rounds=validateReviewRounds(args.reviewRounds === undefined ? reviewRounds : args.reviewRounds);
|
|
@@ -80,24 +84,34 @@ function createSessionReviewService(llm, { capture = captureSnapshot, now = Date
|
|
|
80
84
|
busy = true;
|
|
81
85
|
try {
|
|
82
86
|
const options = structuredClone(args);
|
|
87
|
+
const sourceJobId = options.resumeFromJobId;
|
|
88
|
+
delete options.resumeFromJobId;
|
|
89
|
+
let savedResume;
|
|
90
|
+
if (sourceJobId !== undefined) {
|
|
91
|
+
if (typeof exec.resolveReviewResume !== 'function') throw new Error('Durable job resume is unavailable; use a new background preview');
|
|
92
|
+
savedResume = await exec.resolveReviewResume(sourceJobId);
|
|
93
|
+
}
|
|
83
94
|
const target = await resolveWorkspaceRepository(current.cwd, options.repositoryPath);
|
|
84
95
|
const snapshot = await capture(target, { ...options, signal: exec.signal });
|
|
85
96
|
exec.signal?.throwIfAborted();
|
|
86
97
|
if (owner(exec).cwd !== current.cwd) throw new Error('Agent directory changed; preview again');
|
|
98
|
+
const resumeState = savedResume === undefined ? undefined : validateResumeState(savedResume, snapshot, resumeOptions(options, current.route, rounds));
|
|
87
99
|
const readerBudget = reviewReaderBudget(snapshot, readerBudgetPolicy);
|
|
88
|
-
const plan = buildCoveragePlan(snapshot, { focusPrimaryContext, riskPlanningMode, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:rounds, enableBusinessGrouping, instructions: REVIEW_INSTRUCTIONS, scope: createRetrievalScope(snapshot, { ...readerBudget, signal: exec.signal }), grouping: buildReviewGroups(snapshot.files, inferFileRelations(snapshot.files)), maxInputBytes: 96 * 1024, signal: exec.signal });
|
|
100
|
+
const plan = buildCoveragePlan(snapshot, { enableGroupPrimary, resumeState, focusPrimaryContext, riskPlanningMode, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:rounds, enableBusinessGrouping, instructions: REVIEW_INSTRUCTIONS, scope: configureWireBudget(createRetrievalScope(snapshot, { ...readerBudget, signal: exec.signal, ...(optimizeReviewCalls ? { reuseResults: true } : {}) }), { nativeOutputTools }), grouping: buildReviewGroups(snapshot.files, inferFileRelations(snapshot.files)), maxInputBytes: 96 * 1024, signal: exec.signal });
|
|
89
101
|
prune();
|
|
90
102
|
while (previews.size >= maxPreviews) previews.delete(previews.keys().next().value);
|
|
91
103
|
const previewId = randomUUID();
|
|
92
|
-
previews.set(previewId, { ...current, target, options, rounds, snapshotId: snapshot.id, expires: now() + ttlMs });
|
|
104
|
+
previews.set(previewId, { ...current, target, options, rounds, snapshotId: snapshot.id, resumeState, sourceJobId, expires: now() + ttlMs });
|
|
93
105
|
return {
|
|
94
106
|
...(options.businessRequirement !== undefined
|
|
95
107
|
? { businessRequirement: { text: options.businessRequirement, hash: hash(options.businessRequirement), bytes: Buffer.byteLength(options.businessRequirement) } }
|
|
96
108
|
: {}),
|
|
97
109
|
previewId,
|
|
110
|
+
...(resumeState ? { resume: resumePreview(resumeState, sourceJobId) } : {}),
|
|
98
111
|
plan,
|
|
99
112
|
snapshotId: snapshot.id,
|
|
100
|
-
|
|
113
|
+
privateArtifacts: { enabled: typeof exec.onModelArtifact === 'function', sourceBearing: true, maxArtifactBytes: 1048576, maxJobBytes: 8388608, maxArtifacts: 4096, notice: 'Background jobs retain bounded untrusted model output outside the reviewed workspace; omissions/truncation explicit. Never included in ordinary status or reused as evidence; explicit consent required to read.' },
|
|
114
|
+
execution: { nativeOutputTools, enableGroupPrimary, fileConcurrency: changesConcurrency, maxRetrievalRounds, ...(optimizeReviewCalls ? { riskPlanningMode, readerBudget: { policy: readerBudgetPolicy, ...readerBudget }, missingContextSeeds: enablePrimarySeeds, focusPrimaryContext, searchContextSeeds: enableSearchSeeds } : {}) },
|
|
101
115
|
repositoryRoot: snapshot.root,
|
|
102
116
|
vcs: snapshot.vcs,
|
|
103
117
|
...(snapshot.changeScope ? { changeScope: snapshot.changeScope } : options.changeScope !== undefined ? { changeScope: options.changeScope } : {}),
|
|
@@ -127,7 +141,7 @@ function createSessionReviewService(llm, { capture = captureSnapshot, now = Date
|
|
|
127
141
|
...(snapshot.callerDiscovery ? { callerDiscovery: snapshot.callerDiscovery } : {}),
|
|
128
142
|
...(snapshot.autoContext ? { autoContext: snapshot.autoContext } : {}),
|
|
129
143
|
rules: (snapshot.rules ?? []).map(({ path, hash }) => ({ path, hash })),
|
|
130
|
-
notice: buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcurrency, maxRetrievalRounds) + (optimizeReviewCalls ? ` 完整正文或覆盖全部精确编辑的单批窗口可将风险问题合并入主审(非独立规划);仅对批准上下文的确定坐标预取小片段,不代表完整核查。${focusPrimaryContext ? " 主审优先保留主文件全文,仅附带有关联的有界完整上下文;其余批准来源保留目录按需读,不表示不存在或已核查。" : ""}${enableSearchSeeds ? " 可将模型搜索命中的批准原行小片段与搜索结果同轮提供,每阶段最多2次额外读取,共用原预算;不猜测另侧坐标,不增加检索轮次。" : ""}共享检索上限按本次范围计算:${readerBudget.maxCalls}
|
|
144
|
+
notice: buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcurrency, maxRetrievalRounds) + (optimizeReviewCalls ? ` 完整正文或覆盖全部精确编辑的单批窗口可将风险问题合并入主审(非独立规划);仅对批准上下文的确定坐标预取小片段,不代表完整核查。${focusPrimaryContext ? " 主审优先保留主文件全文,仅附带有关联的有界完整上下文;其余批准来源保留目录按需读,不表示不存在或已核查。" : ""}${enableSearchSeeds ? " 可将模型搜索命中的批准原行小片段与搜索结果同轮提供,每阶段最多2次额外读取,共用原预算;不猜测另侧坐标,不增加检索轮次。" : ""}共享检索上限按本次范围计算:${readerBudget.maxCalls}次唯一操作结果生成/${readerBudget.maxOutputBytes}交付字节,所有阶段共用、不逐文件重置;同快照相同请求可复用原始结果,每次交付仍计字节并重新签发复核回执;主审、综合及复核按权重预留最低额度,未用额度释放,额度不足仍如实标记未完成。` : ''),
|
|
131
145
|
};
|
|
132
146
|
} finally { busy = false; }
|
|
133
147
|
},
|
|
@@ -148,12 +162,15 @@ function createSessionReviewService(llm, { capture = captureSnapshot, now = Date
|
|
|
148
162
|
const snapshot = await capture(target, { ...preview.options, signal: exec.signal });
|
|
149
163
|
exec.signal?.throwIfAborted();
|
|
150
164
|
if (snapshot.id !== preview.snapshotId || owner(exec).cwd !== current.cwd) throw new Error('Files changed since preview; preview again');
|
|
165
|
+
const resumeIdentity = resumeOptions(preview.options, preview.route, preview.rounds);
|
|
166
|
+
const resumeState = preview.resumeState ? validateResumeState(preview.resumeState, snapshot, resumeIdentity) : undefined;
|
|
151
167
|
if (await authorize({ snapshot, route: preview.route, exec }) !== true) throw new Error('Model sending approval denied or unavailable');
|
|
152
168
|
exec.signal?.throwIfAborted();
|
|
153
169
|
if (owner(exec).cwd !== current.cwd || JSON.stringify(owner(exec).route) !== JSON.stringify(preview.route)) throw new Error('Agent changed during approval');
|
|
154
170
|
await exec.reviewCheckpoint?.({stage:'approved-manifest',snapshotId:snapshot.id,files:snapshot.files.map(f=>({fileId:f.id,path:f.path,status:f.eligibility==='reviewable'?'pending':f.eligibility}))});
|
|
155
|
-
const report = await reviewSnapshot(snapshot, createDshModel(llm, preview.route,{correctJson:true}), { signal: exec.signal, enableRetrieval: true, enableGrouping: true, enableVerification: true, riskPlanningMode, readerBudgetPolicy, optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:preview.rounds, enableBusinessGrouping, enableAnchorCorrection,changesConcurrency,maxRetrievalRounds,correctProtocol:true, onProgress:exec.reviewCheckpoint });
|
|
171
|
+
const report = await reviewSnapshot(snapshot, createDshModel(llm, preview.route,{correctJson:true,useNativeTools:nativeOutputTools}), { signal: exec.signal, enableRetrieval: true, enableGrouping: true, enableGroupPrimary, nativeOutputTools, enableVerification: true, riskPlanningMode, readerBudgetPolicy, optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:preview.rounds, enableBusinessGrouping, enableAnchorCorrection,changesConcurrency,maxRetrievalRounds,correctProtocol:true, onProgress:exec.reviewCheckpoint, resumeState, onModelArtifact: exec.onModelArtifact, onResumeCheckpoint: exec.reviewResumeCheckpoint ? async checkpoint => { await exec.reviewResumeCheckpoint(buildResumeState(snapshot, resumeIdentity, checkpoint)); } : undefined });
|
|
156
172
|
report.model = preview.route;
|
|
173
|
+
if (exec.reviewResumeCheckpoint) await exec.reviewResumeCheckpoint(buildResumeState(snapshot, resumeIdentity, report));
|
|
157
174
|
// reviewSnapshot preserves cancelled coverage; do not replace it with a freshness error.
|
|
158
175
|
if (report.status === 'cancelled') {
|
|
159
176
|
report.outdated = true;
|