@feng0531/dsh-code-review 0.86.0 → 0.87.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 +16 -0
- package/CORE-ALIGNMENT-TASKS.md +19 -4
- package/CORE-SOURCE-INVENTORY.md +6 -6
- package/DEVELOPMENT.md +15 -1
- package/README.md +22 -5
- package/RELEASE-NOTES.md +14 -14
- package/index.mjs +1 -1
- package/package.json +1 -1
- package/src/background-review.mjs +36 -0
- package/src/candidate-verification.mjs +1 -1
- package/src/coverage-plan.mjs +20 -6
- package/src/cpp-migration-bindings.mjs +149 -0
- package/src/evidence-context.mjs +96 -0
- package/src/group-interaction.mjs +48 -0
- package/src/group-primary.mjs +14 -3
- package/src/interaction-risk.mjs +71 -0
- package/src/migration-contracts.mjs +160 -0
- package/src/migration-regression.mjs +103 -0
- package/src/model-contract.mjs +3 -3
- package/src/model-tools.mjs +6 -4
- package/src/primary-seeds.mjs +6 -2
- package/src/project-review-service.mjs +2 -2
- package/src/prompts.mjs +1 -0
- package/src/regression-assessment.mjs +8 -2
- package/src/report-summary.mjs +59 -0
- package/src/retrieval-loop.mjs +17 -4
- package/src/review-artifacts.mjs +1 -1
- package/src/review-diagnostics.mjs +2 -1
- package/src/review-jobs.mjs +1 -1
- package/src/review-reader-allocation.mjs +117 -0
- package/src/review-reader-policy.mjs +1 -1
- package/src/review-resume.mjs +67 -2
- package/src/review-stage-state.mjs +143 -0
- package/src/review.mjs +221 -52
- package/src/service.mjs +8 -8
- package/src/snapshot-reader.mjs +62 -22
- package/src/stage-continuation.mjs +74 -0
- package/src/verification-loop.mjs +9 -4
- package/src/verification-seeds.mjs +18 -1
package/src/review.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { inferFileRelations } from './file-relations.mjs';
|
|
|
19
19
|
import { buildReviewGroups } from './review-groups.mjs';
|
|
20
20
|
import { changeMap } from './change-map.mjs';
|
|
21
21
|
import { createRetrievalScope } from './retrieval-scope.mjs';
|
|
22
|
-
import { retrievalLoop } from './retrieval-loop.mjs';
|
|
22
|
+
import { retrievalLoop, retrievalRequest } from './retrieval-loop.mjs';
|
|
23
23
|
import { reviewDiagnostics, diagnosticsMarkdown } from './review-diagnostics.mjs';
|
|
24
24
|
import { preparePrimaryInput } from './primary-input.mjs';
|
|
25
25
|
import { validateAttribution } from './attribution.mjs';
|
|
@@ -29,8 +29,16 @@ 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';
|
|
32
33
|
import { prepareGroupPrimary, validateGroupPrimary } from './group-primary.mjs';
|
|
33
34
|
import { configureWireBudget, modelWireBudget } from './model-wire-budget.mjs';
|
|
35
|
+
import { createEvidenceContext } from './evidence-context.mjs';
|
|
36
|
+
import { prepareInteractionRisk, consumeInteractionRisks } from './interaction-risk.mjs';
|
|
37
|
+
import { reportSummaryMarkdown } from './report-summary.mjs';
|
|
38
|
+
import { validateGroupInteraction } from './group-interaction.mjs';
|
|
39
|
+
import { stageKey, prepareStageGenerations } from './review-stage-state.mjs';
|
|
40
|
+
import { createStageContinuation } from './stage-continuation.mjs';
|
|
41
|
+
import { buildMigrationContracts } from './migration-contracts.mjs';
|
|
34
42
|
|
|
35
43
|
export { REVIEW_INSTRUCTIONS };
|
|
36
44
|
export { PROJECT_REVIEW_INSTRUCTIONS };
|
|
@@ -123,8 +131,10 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
123
131
|
}
|
|
124
132
|
if (options.enableBusinessGrouping === true && options.enableRetrieval !== true) throw new Error('Business grouping requires approved retrieval scope');
|
|
125
133
|
if (options.enableVerification === true && options.enableRetrieval !== true) throw new Error('Verification requires approved retrieval scope');
|
|
134
|
+
for (const key of ['enableEvidenceContext', 'inlineInteractionRisks', 'enableMigrationContracts', 'enableMigrationRegression', 'enableGroupInteractions', 'enableStageContinuation']) if (options[key] !== undefined && typeof options[key] !== 'boolean') throw new Error('Invalid review optimization option');
|
|
135
|
+
const evidenceContext = options.enableEvidenceContext === true ? createEvidenceContext(input.id) : null;
|
|
126
136
|
const readerLimits = reviewReaderBudget(input, options.readerBudgetPolicy ?? 'fixed');
|
|
127
|
-
const scope = options.enableRetrieval === true ? createRetrievalScope({ ...input, context: input.context ?? [] }, { ...readerLimits, signal }) : null;
|
|
137
|
+
const scope = options.enableRetrieval === true ? createRetrievalScope({ ...input, context: input.context ?? [] }, { ...readerLimits, signal, ...(options.optimizeReviewCalls === true ? { reuseResults: true } : {}) }) : null;
|
|
128
138
|
const report = { schemaVersion: 1, snapshotId: input.id, vcs: input.vcs, status: 'completed', files: [], findings: [], limitations: [], modelCalls: 0 };
|
|
129
139
|
validateBusinessRequirement(input.businessRequirement);
|
|
130
140
|
if(input.businessRequirement!==undefined)report.businessRequirement={hash:hash(input.businessRequirement),bytes:Buffer.byteLength(input.businessRequirement),status:'untrusted-constraint'};
|
|
@@ -145,9 +155,14 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
145
155
|
if(input.effectiveMergeSources)report.effectiveMergeSources=structuredClone(input.effectiveMergeSources);
|
|
146
156
|
report.rules = (input.rules ?? []).map(({ path, hash }) => ({ path, hash }));
|
|
147
157
|
if (scope) configureWireBudget(scope, { nativeOutputTools: options.nativeOutputTools === true });
|
|
148
|
-
if (options.
|
|
158
|
+
if (options.enableMigrationContracts === true && scope && !input.projectScope) {
|
|
159
|
+
report.migrationContracts = buildMigrationContracts(input);
|
|
160
|
+
}
|
|
161
|
+
if (options.enableGrouping === true || report.migrationContracts?.contracts.length) report.grouping = buildReviewGroups(input.files, [...inferFileRelations(input.files), ...(report.migrationContracts?.links ?? []).map(link => ({ from: link.from, to: link.to, reason: link.reasons?.join(',') ?? 'migration-contract' }))]);
|
|
149
162
|
report.interactions = [];
|
|
150
|
-
const
|
|
163
|
+
const selectedGroups = (report.grouping?.groups ?? []).filter(group => group.fileIds.length > 1);
|
|
164
|
+
if (report.migrationContracts) report.migrationContracts.groups = selectedGroups.filter(group => report.migrationContracts.groups.some(other => [...other.fileIds].sort().join(',') === [...group.fileIds].sort().join(',')));
|
|
165
|
+
const interactionPlans = scope ? selectedGroups.map(group => ({ group, prepared: prepareInteractionInput(input, group, payload => initialInputBudget({ instructions, input: payload }, scope, maxInputBytes)) })) : [];
|
|
151
166
|
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' });
|
|
152
167
|
const fileResults = new Array(input.files.length);
|
|
153
168
|
const sharedState = report.orchestration = { modelMetrics: {}, modelCallPurposes: {} };
|
|
@@ -173,21 +188,47 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
173
188
|
await options.onProgress?.({ stage: info.action, snapshotId: input.id, phase: request.reviewPhase, ...info });
|
|
174
189
|
} });
|
|
175
190
|
};
|
|
176
|
-
const groupPrepared = new Map(), groupResults = new Map(), groupStarted = new Map();
|
|
191
|
+
const groupPrepared = new Map(), groupResults = new Map(), groupStarted = new Map(), groupInteractions = new Map();
|
|
192
|
+
const acceptedStages = new Map(prepareStageGenerations(input, options.resumeState?.stageGenerations ?? []).filter(record => record.phase === 'primary').map(record => [record.key, { generation: record.generation, record, restored: true }]));
|
|
193
|
+
const deferredStages = [], yieldedUnits = new Set(), fileDeadlines = new Map(), primaryStageKeys = new Map();
|
|
194
|
+
const checkpointStages = () => {
|
|
195
|
+
if (!options.enableStageContinuation) return;
|
|
196
|
+
report.stageGenerations = prepareStageGenerations(input, [...acceptedStages.values()].map(entry => entry.record).filter(record => record?.phase === 'primary'));
|
|
197
|
+
};
|
|
198
|
+
let readerAllocation;
|
|
199
|
+
const stageScope = id => readerAllocation
|
|
200
|
+
? configureWireBudget(readerAllocation.scope(id), { nativeOutputTools: options.nativeOutputTools === true }) : scope;
|
|
201
|
+
const releaseStage = id => { if (!yieldedUnits.has(id)) readerAllocation?.release(id); };
|
|
202
|
+
const contextSeeds = (reader, request, existing = [], buildRequest = records => retrievalRequest(request, reader, records)) => {
|
|
203
|
+
if (!evidenceContext || !reader || maxRetrievalRounds === 0) return [];
|
|
204
|
+
const payload = JSON.parse(request.input), catalog = reader.catalog();
|
|
205
|
+
const members = [payload, ...(payload.reviews ?? [])];
|
|
206
|
+
const fileIds = new Set(members.flatMap(member => [member.file?.id, ...(member.files ?? []).map(item => item.fileId), ...(member.relatedFiles ?? []).map(item => item.id)]).filter(Boolean));
|
|
207
|
+
const paths = new Set(members.flatMap(member => (member.contextRelations ?? []).flatMap(item => [item.fromPath, item.toPath])).filter(Boolean));
|
|
208
|
+
for (const caller of members.flatMap(member => member.callerBindings ?? [])) if (caller.path) paths.add(caller.path);
|
|
209
|
+
const riskIds = new Set(members.flatMap(member => (member.riskPlan ?? []).flatMap(item => item.sourceIds ?? [])));
|
|
210
|
+
const sourceIds = catalog.filter(source => fileIds.has(source.fileId) || paths.has(source.path) || riskIds.has(source.id)).map(source => source.id).slice(0, 200);
|
|
211
|
+
return evidenceContext.seed(reader, request, { sourceIds, existing, maxInputBytes, buildRequest });
|
|
212
|
+
};
|
|
177
213
|
const groupResult = async group => {
|
|
178
214
|
if (!groupResults.has(group.id)) {
|
|
179
215
|
groupStarted.set(group.id, performance.now());
|
|
180
216
|
groupResults.set(group.id, invoke(async request => {
|
|
181
|
-
const prepared = groupPrepared.get(group.id)
|
|
182
|
-
const
|
|
217
|
+
const prepared = groupPrepared.get(group.id), groupScope = stageScope('group:' + group.id); const limits = [], reads = [];
|
|
218
|
+
const groupRequest = { ...request, nativeOutputTools: options.nativeOutputTools === true, input: prepared.payload, instructions: prepared.instructions, reviewPhase: 'group-primary' };
|
|
219
|
+
const response = await retrievalLoop(sharedModel, groupRequest, groupScope, {
|
|
220
|
+
initialContext: (existing, build) => contextSeeds(groupScope, groupRequest, existing, build),
|
|
183
221
|
maxRounds: maxRetrievalRounds, maxInputBytes, beforeCall: reserveShared, correctProtocol: options.correctProtocol === true,
|
|
184
222
|
enableSearchSeeds: options.enableSearchSeeds === true,
|
|
185
|
-
onRetrieved: records => { reads.push(...records); (sharedState.retrievalAudit ??= []).push(...records); },
|
|
186
|
-
onBudgetLimited: info => {
|
|
223
|
+
onRetrieved: records => { reads.push(...records); (sharedState.retrievalAudit ??= []).push(...records); evidenceContext?.remember(records); },
|
|
224
|
+
onBudgetLimited: info => { limits.push(info); (sharedState.retrievalLimits ??= []).push(info); },
|
|
187
225
|
});
|
|
188
226
|
const results = validateGroupPrimary(response, prepared, input, validateResponse, scope.catalog());
|
|
227
|
+
const interaction = validateGroupInteraction(response, prepared, input, reads, scope.catalog());
|
|
228
|
+
groupInteractions.set(group.id, interaction);
|
|
229
|
+
if (interaction.status !== 'disabled') (report.groupInteractionChannels ??= []).push({ groupId: group.id, status: interaction.status, reason: interaction.reason });
|
|
189
230
|
for (const generated of results.values()) {
|
|
190
|
-
generated.
|
|
231
|
+
generated.groupLimits = structuredClone(limits);
|
|
191
232
|
if (generated.groupRisk) {
|
|
192
233
|
generated.groupRisk.sourceReadCoverage = riskReadCoverage(generated.groupRisk.risks, reads, scope.catalog(), input.id);
|
|
193
234
|
if (generated.groupRisk.status !== 'completed') generated.limitations.push('组主审的合并风险计划缺失或无效,未补发规划调用;不表示已完成风险条件核查');
|
|
@@ -197,7 +238,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
197
238
|
return results;
|
|
198
239
|
}, { input: groupPrepared.get(group.id).payload, instructions: groupPrepared.get(group.id).instructions }, timeoutMs, signal));
|
|
199
240
|
}
|
|
200
|
-
return groupResults.get(group.id);
|
|
241
|
+
try { return await groupResults.get(group.id); } finally { releaseStage('group:' + group.id); }
|
|
201
242
|
};
|
|
202
243
|
const runFile = async index => {
|
|
203
244
|
const file = input.files[index];
|
|
@@ -229,14 +270,16 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
229
270
|
if(changes.precision==='envelope')result.limitations.push({fileId:file.id,text:'精确差异不可得,按保守包络分段检查;包络包含可能未变的行,不证明精确编辑或回归因果。'});
|
|
230
271
|
if (changes.status === 'limited') result.limitations.push({ fileId: file.id, text: '精确变更分析受限:' + changes.reason + ';仍按完整两侧内容审查' });
|
|
231
272
|
if (state.groupFallback) result.limitations.push({ fileId: file.id, text: '关联组上下文超过输入预算,回退单文件审查' });
|
|
232
|
-
if (state.initialInputBytes > maxInputBytes) { state.status = 'blocked'; state.reason = '输入超过预算,未截断或提交模型'; return; }
|
|
273
|
+
if (state.initialInputBytes > maxInputBytes) { state.status = 'blocked'; state.reason = '输入超过预算,未截断或提交模型'; releaseStage('primary:' + file.id); releaseStage('synthesis:' + file.id); releaseStage('verification:' + file.id); return; }
|
|
233
274
|
try {
|
|
234
275
|
const beforeCall = ({ reserved = false } = {}) => {
|
|
235
276
|
if (!reserved && options.remainingCalls?.() <= 0) throw Object.assign(new Error('Shared project model call limit exceeded'), { code: 'PROJECT_MODEL_CALL_LIMIT' });
|
|
236
277
|
if (report.modelCalls >= maxCalls) throw new Error('Model call budget exceeded');
|
|
237
278
|
report.modelCalls++;
|
|
238
279
|
};
|
|
280
|
+
let activeReader = scope, activeStage = 'primary';
|
|
239
281
|
const onBudgetLimited = info => {
|
|
282
|
+
if (activeStage !== 'primary') (state.secondaryRetrievalLimits ??= []).push({ phase: activeStage, ...info });
|
|
240
283
|
state.retrievalLimited = true;
|
|
241
284
|
(state.retrievalLimits ??= []).push(info);
|
|
242
285
|
result.limitations.push({ fileId: file.id, text: '检索预算受限,已保留现有结果但不能视为完整审查:' + info.reason });
|
|
@@ -277,8 +320,9 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
277
320
|
});
|
|
278
321
|
};
|
|
279
322
|
const executor = scope
|
|
280
|
-
? (request, onLimited = () => {}) => retrievalLoop(trackedModel, { ...request, nativeOutputTools: options.nativeOutputTools === true, onProtocolEvent },
|
|
323
|
+
? (request, onLimited = () => {}) => retrievalLoop(trackedModel, { ...request, nativeOutputTools: options.nativeOutputTools === true, onProtocolEvent }, activeReader, { correctProtocol:options.correctProtocol===true,maxRounds:maxRetrievalRounds,maxInputBytes, beforeCall, initialRetrieved: request.initialRetrieved ?? [], yieldOnBudget: options.enableStageContinuation === true && activeStage !== 'primary', initialContext: (existing, build) => contextSeeds(activeReader, request, existing, build), allowExhaustedInitial: request.allowExhaustedInitial === true, enableSearchSeeds: options.enableSearchSeeds === true && !request.reviewPhase?.includes('verification'), onBudgetLimited: info => { onBudgetLimited(info); onLimited(info); }, onRetrieved: records => {
|
|
281
324
|
(state.retrievalAudit ??= []).push(...records);
|
|
325
|
+
evidenceContext?.remember(records);
|
|
282
326
|
}, onTruncated: () => {
|
|
283
327
|
if (!state.retrievalTruncated) result.limitations.push({ fileId: file.id, text: '检索结果达到数量上限,部分匹配未提供给模型' });
|
|
284
328
|
state.retrievalTruncated = true;
|
|
@@ -288,42 +332,83 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
288
332
|
const groupedGeneration = grouped ? (await groupResult({ id: grouped[0] })).get(file.id) : null;
|
|
289
333
|
if (grouped) {
|
|
290
334
|
state.primaryGroupId = grouped[0]; state.primaryMethod = 'group-primary';
|
|
291
|
-
if (groupedGeneration.
|
|
335
|
+
if (groupedGeneration.groupLimits?.length) {
|
|
336
|
+
state.retrievalLimited = true;
|
|
337
|
+
state.groupRetrievalLimits = structuredClone(groupedGeneration.groupLimits);
|
|
338
|
+
result.limitations.push(...state.groupRetrievalLimits.map(limit => ({ fileId: file.id, text: '组主审检索受限:' + limit.code + ';已接受的本成员成果保留,不表示完整审查' })));
|
|
339
|
+
}
|
|
292
340
|
if (groupedGeneration.groupRisk) (state.riskPlans ??= []).push({ ...groupedGeneration.groupRisk, method: 'inline', scope: 'group-all-edits', groupId: grouped[0], decision: { required: true, reason: 'group-inline-primary' } });
|
|
293
341
|
}
|
|
294
342
|
const chainTimeout = grouped && timeoutMs > 0 ? Math.max(0, timeoutMs - (performance.now() - groupStarted.get(grouped[0]))) : timeoutMs;
|
|
295
343
|
if (grouped && timeoutMs > 0 && chainTimeout <= 0) throw Object.assign(new Error('Model timeout'), { code: 'MODEL_TIMEOUT' });
|
|
344
|
+
if (!fileDeadlines.has(file.id)) fileDeadlines.set(file.id, timeoutMs > 0 ? performance.now() + chainTimeout : Infinity);
|
|
296
345
|
await invoke(async enclosingRequest => {
|
|
297
346
|
const batches = [...(prepared.batches ?? [{ payload, windowIds: state.windowIds ?? [] }])];
|
|
298
347
|
if (prepared.synthesis?.status === 'ready') batches.push({ payload: prepared.synthesis.payload, windowIds: [], synthesis: true });
|
|
299
348
|
const matchingInteractionPlans = interactionPlans.filter(p => input.files.filter(f => p.group.fileIds.includes(f.id)).at(-1)?.id === file.id);
|
|
300
349
|
for (const interactionPlan of matchingInteractionPlans.filter(p => p.prepared.status === 'ready')) batches.push({ payload: interactionPlan.prepared.payload, windowIds: [], interaction: interactionPlan.group });
|
|
301
350
|
const scheduled = Array.from({length:rounds},(_,i)=>batches.map(b=>({...b,round:i+1}))).flat();
|
|
351
|
+
primaryStageKeys.set(file.id, scheduled.filter(batch => !batch.synthesis && !batch.interaction).map(batch => stageKey(file.id, { phase: 'primary', round: batch.round, windowIds: batch.windowIds ?? [] })));
|
|
302
352
|
if(rounds>1) state.reviewPasses=scheduled.map(b=>({round:b.round,windowIds:b.windowIds,...(b.interaction?{groupId:b.interaction.id}:{}),...(b.synthesis?{synthesis:true}:{}),status:'pending'}));
|
|
303
353
|
const riskInput = canonicalRiskInput(input, file, prepared, { instructions: RISK_PLAN_INSTRUCTIONS, scope, maxInputBytes });
|
|
304
354
|
let primaryRiskPlan;
|
|
305
|
-
|
|
355
|
+
const executeStage = async (batch, batchIndex, enclosingRequest, retry = false) => {
|
|
306
356
|
enclosingRequest.signal.throwIfAborted();
|
|
357
|
+
const key = stageKey(file.id, { phase: batch.interaction ? 'file-interaction' : batch.synthesis ? 'window-synthesis' : 'primary', round: batch.round, windowIds: batch.windowIds ?? [], ...(batch.interaction ? { groupId: batch.interaction.id } : {}) });
|
|
358
|
+
const priorStage = acceptedStages.get(key);
|
|
359
|
+
if (retry && priorStage?.completed) return;
|
|
360
|
+
const allocationId = batch.interaction ? 'interaction:' + batch.interaction.id : batch.synthesis ? 'synthesis:' + file.id : 'primary:' + file.id;
|
|
361
|
+
const defer = (error, unitId = allocationId) => {
|
|
362
|
+
if (!options.enableStageContinuation || !readerAllocation || !['READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED'].includes(error?.code)) return false;
|
|
363
|
+
if (!yieldedUnits.has(unitId)) { readerAllocation.yield(unitId, { reason: error.code }); yieldedUnits.add(unitId); }
|
|
364
|
+
let pending = deferredStages.find(item => item.key === key);
|
|
365
|
+
if (pending && pending.allocationId !== unitId) { const previousUnit = pending.allocationId; pending.allocationId = unitId; if (!deferredStages.some(other => other !== pending && !other.done && other.allocationId === previousUnit)) { yieldedUnits.delete(previousUnit); readerAllocation.release(previousUnit); } }
|
|
366
|
+
if (!pending) {
|
|
367
|
+
pending = { key, allocationId: unitId, fileId: file.id, phase: unitId.startsWith('verification:') ? 'verification' : phase === 'window-synthesis' ? 'synthesis' : 'interaction', fileIds: batch.interaction?.fileIds ?? [file.id], attempts: 0, run: () => invoke(next => executeStage(batch, batchIndex, next, true), { instructions, input: payload }, timeoutMs > 0 ? Math.max(1, fileDeadlines.get(file.id) - performance.now()) : 0, signal) };
|
|
368
|
+
deferredStages.push(pending);
|
|
369
|
+
}
|
|
370
|
+
pending.requests = error.pendingRequests ?? []; pending.reason = error.code; pending.done = false;
|
|
371
|
+
state.continuationPending = true;
|
|
372
|
+
if (batch.interaction) Object.assign(report.interactions.find(item => item.groupId === batch.interaction.id), { status: 'pending', reason: error.code });
|
|
373
|
+
if (batch.synthesis) state.synthesisStatus = 'pending';
|
|
374
|
+
return true;
|
|
375
|
+
};
|
|
307
376
|
const phase=batch.interaction?'file-interaction':batch.synthesis?'window-synthesis':'primary';
|
|
308
377
|
try{
|
|
309
|
-
|
|
378
|
+
activeStage = phase;
|
|
379
|
+
if (!priorStage && yieldedUnits.has(allocationId)) { defer(Object.assign(new Error('Stage is waiting for released reader capacity'), { code: 'READER_CALL_BUDGET_EXCEEDED' })); return; }
|
|
380
|
+
activeReader = priorStage || groupedGeneration && !batch.synthesis && !batch.interaction ? scope : stageScope(allocationId);
|
|
381
|
+
// Consuming an already validated group member performs no new source/model work.
|
|
382
|
+
const combinedInteraction = batch.interaction && batch.round === 1 ? [...groupInteractions].find(([id, channel]) => channel.status === 'accepted' && JSON.stringify([...groupPrepared.get(id).fileIds].sort()) === JSON.stringify([...batch.interaction.fileIds].sort()))?.[1] : null;
|
|
383
|
+
const cachedPrimary = !!priorStage || groupedGeneration && !batch.synthesis && !batch.interaction;
|
|
384
|
+
const admission = !cachedPrimary && !combinedInteraction && activeReader && options.optimizeReviewCalls === true
|
|
385
|
+
? readerAdmission({ synthesis: batch.synthesis === true, interaction: !!batch.interaction, metadata: prepared.metadata }, activeReader.budget()) : { permitted: true };
|
|
310
386
|
if (!admission.permitted) {
|
|
311
|
-
|
|
387
|
+
if ((batch.synthesis || batch.interaction) && defer(Object.assign(new Error('Stage reader allocation unavailable'), { code: admission.code }))) return;
|
|
388
|
+
state.readerAdmission = { code: admission.code, remaining: activeReader.budget() };
|
|
312
389
|
if (batch.synthesis || batch.interaction) throw Object.assign(new Error('Required evidence budget unavailable before stage admission'), { code: admission.code });
|
|
313
390
|
state.reason = '检索预算不足以取得本阶段所需证据,未启动模型;需重新预览补审';
|
|
314
391
|
state.failureCode = admission.code;
|
|
315
392
|
state.retrievalLimited = true;
|
|
316
|
-
|
|
393
|
+
return;
|
|
317
394
|
}
|
|
318
395
|
let batchPayload = batch.payload;
|
|
319
|
-
const
|
|
320
|
-
|
|
396
|
+
const relevantContracts = report.migrationContracts?.contracts.filter(contract => contract.candidateFileIds.includes(file.id) || batch.interaction?.fileIds.some(id => contract.candidateFileIds.includes(id))) ?? [];
|
|
397
|
+
if (relevantContracts.length) {
|
|
398
|
+
const enriched = JSON.stringify({ ...JSON.parse(batchPayload), migrationContracts: relevantContracts, migrationNotice: 'Host navigation hypotheses only; old/new entries and callers are side-qualified. Follow the complete old public gate → new entry → caller chain under the SAME trigger. Contracts do not prove semantic migration, grant sources, or replace current independent verification.' });
|
|
399
|
+
if (initialInputBudget({ input: enriched, instructions }, activeReader, maxInputBytes).fits) batchPayload = enriched;
|
|
400
|
+
else (state.migrationContextLimitations ??= []).push('Migration navigation did not fit this stage; no coverage claim.');
|
|
401
|
+
}
|
|
402
|
+
const interactionStrategy = batch.interaction && !combinedInteraction ? prepareInteractionRisk(input, batch.interaction, matchingInteractionPlans.find(plan => plan.group.id === batch.interaction.id).prepared, { ...options, instructions, scope: activeReader, maxInputBytes, round: batch.round }) : null;
|
|
403
|
+
const inlineInteraction = !priorStage && interactionStrategy?.mode === 'inline';
|
|
404
|
+
const inlineRisk = !priorStage && (inlineInteraction || (!groupedGeneration && prepared.inlineRisk === true && batch.round === 1 && !batch.synthesis && !batch.interaction));
|
|
405
|
+
const batchInstructions = inlineInteraction ? interactionStrategy.instructions : inlineRisk ? prepared.instructions : instructions;
|
|
321
406
|
let riskEntry, activeRisks = [];
|
|
322
407
|
if (inlineRisk) {
|
|
323
|
-
riskEntry = { status: 'pending', method: 'inline', windowIds: batch.windowIds, scope: state.sourceMode === 'change-windows' ? 'all-edits' : 'full-file', risks: [], decision: { required: true, reason: 'inline-primary' } };
|
|
408
|
+
riskEntry = { status: 'pending', method: 'inline', windowIds: batch.windowIds, scope: inlineInteraction ? 'group-all-edits' : state.sourceMode === 'change-windows' ? 'all-edits' : 'full-file', ...(inlineInteraction ? { groupId: batch.interaction.id, sourceMode: 'file-interaction' } : {}), risks: [], decision: { required: true, reason: inlineInteraction ? 'inline-interaction' : 'inline-primary' } };
|
|
324
409
|
(state.riskPlans ??= []).push(riskEntry);
|
|
325
410
|
}
|
|
326
|
-
if (options.enableRiskPlanning === true && (!groupedGeneration || batch.interaction) && !inlineRisk && !batch.synthesis && batch.round === 1) {
|
|
411
|
+
if (options.enableRiskPlanning === true && (!groupedGeneration || batch.interaction) && !inlineRisk && !combinedInteraction && !priorStage && !batch.synthesis && batch.round === 1) {
|
|
327
412
|
const reusable = !batch.interaction && riskInput.cacheable;
|
|
328
413
|
riskEntry = reusable ? primaryRiskPlan : undefined;
|
|
329
414
|
if (!riskEntry) {
|
|
@@ -364,7 +449,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
364
449
|
}
|
|
365
450
|
}
|
|
366
451
|
}
|
|
367
|
-
if(report.modelCalls>=maxCalls && !(groupedGeneration && !batch.synthesis && !batch.interaction))throw new Error('Model call budget exceeded before next review pass');
|
|
452
|
+
if(report.modelCalls>=maxCalls && !priorStage && !combinedInteraction && !(groupedGeneration && !batch.synthesis && !batch.interaction))throw new Error('Model call budget exceeded before next review pass');
|
|
368
453
|
if(rounds>1) state.reviewPasses[batchIndex].status='running';
|
|
369
454
|
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' };
|
|
370
455
|
let auditStart = state.retrievalAudit?.length ?? 0;
|
|
@@ -372,7 +457,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
372
457
|
const generate = async attempt => {
|
|
373
458
|
if (!initialInputBudget(attempt, scope, maxInputBytes).fits) throw new Error('Review correction input budget exceeded');
|
|
374
459
|
const initialRetrieved = scope && options.enablePrimarySeeds === true && maxRetrievalRounds > 0 && !batch.synthesis && !batch.interaction
|
|
375
|
-
? primarySeeds(
|
|
460
|
+
? primarySeeds(activeReader, attempt, { maxInputBytes, signal: attempt.signal }) : [];
|
|
376
461
|
return executor({ ...attempt, initialRetrieved }, () => { finalResponseOnly = true; });
|
|
377
462
|
};
|
|
378
463
|
const validate = response => {
|
|
@@ -390,13 +475,15 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
390
475
|
response = decoded;
|
|
391
476
|
}
|
|
392
477
|
}
|
|
393
|
-
const
|
|
394
|
-
|
|
478
|
+
const interactionResult = inlineInteraction ? consumeInteractionRisks(response, input, batch.interaction, scope.catalog()) : null;
|
|
479
|
+
const generated = batch.interaction ? validateInteractionFindings(interactionResult?.response ?? response, input, batch.interaction, (state.retrievalAudit ?? []).slice(auditStart), scope.catalog()) : validateResponse(response, file, changes);
|
|
480
|
+
if (inlineInteraction) generated.interactionRisk = interactionResult.risk;
|
|
481
|
+
else if (inlineRisk) { const value = typeof response === 'string' ? JSON.parse(response) : response; if (Object.hasOwn(value, 'risks')) generated.inlineRisks = value.risks; }
|
|
395
482
|
if (batch.synthesis) validateSynthesisReads(generated.findings, changes, (state.retrievalAudit ?? []).slice(auditStart), scope.catalog(), file.id, input.id);
|
|
396
483
|
if (!batch.interaction && state.sourceMode === 'change-windows') validateWindowFindings(generated.findings, batch.synthesis ? prepared.synthesis.windows : JSON.parse(batch.payload).windows, batch.synthesis === true);
|
|
397
484
|
return generated;
|
|
398
485
|
};
|
|
399
|
-
const generated = groupedGeneration && !batch.synthesis && !batch.interaction ? structuredClone(groupedGeneration) : await correctedResponse(generate, validate, request, {
|
|
486
|
+
const generated = priorStage ? priorStage.generation : combinedInteraction ? { findings: structuredClone(combinedInteraction.findings), limitations: structuredClone(combinedInteraction.limitations) } : groupedGeneration && !batch.synthesis && !batch.interaction ? structuredClone(groupedGeneration) : await correctedResponse(generate, validate, request, {
|
|
400
487
|
enabled: options.correctProtocol === true,
|
|
401
488
|
canCorrect: () => !finalResponseOnly,
|
|
402
489
|
onCorrection: async info => {
|
|
@@ -408,7 +495,12 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
408
495
|
await options.onProgress?.({ stage: 'protocol-correction', fileId: file.id, path: file.path, ...info });
|
|
409
496
|
},
|
|
410
497
|
});
|
|
411
|
-
if (
|
|
498
|
+
if (inlineInteraction) {
|
|
499
|
+
Object.assign(riskEntry, generated.interactionRisk);
|
|
500
|
+
activeRisks = riskEntry.risks;
|
|
501
|
+
if (riskEntry.limitation) result.limitations.push({ fileId: file.id, text: riskEntry.limitation });
|
|
502
|
+
delete generated.interactionRisk;
|
|
503
|
+
} else if (inlineRisk) {
|
|
412
504
|
const response = generated.inlineRisks;
|
|
413
505
|
if (response === undefined) { riskEntry.status = 'absent'; result.limitations.push({ fileId: file.id, text: '合并主审未返回风险问题,保留有效发现但不声明独立风险规划完成' }); }
|
|
414
506
|
else {
|
|
@@ -417,7 +509,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
417
509
|
}
|
|
418
510
|
delete generated.inlineRisks;
|
|
419
511
|
}
|
|
420
|
-
if (options.enableRiskPlanning === true && (!groupedGeneration || batch.interaction) && !batch.synthesis && batch.round === 1) {
|
|
512
|
+
if (options.enableRiskPlanning === true && !priorStage && !combinedInteraction && (!groupedGeneration || batch.interaction) && !batch.synthesis && batch.round === 1) {
|
|
421
513
|
const coverage = riskReadCoverage(activeRisks,(state.retrievalAudit??[]).slice(auditStart),scope?.catalog()??[],input.id);
|
|
422
514
|
(riskEntry.generations ??= []).push({ round: batch.round, windowIds: batch.windowIds, coverage });
|
|
423
515
|
// Preserve the single-generation field without unioning evidence across windows.
|
|
@@ -427,14 +519,24 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
427
519
|
}
|
|
428
520
|
request.signal.throwIfAborted();
|
|
429
521
|
if(rounds>1)generated.findings.forEach(f=>{f.reviewRound=batch.round;});
|
|
430
|
-
|
|
522
|
+
if (options.enableStageContinuation && !priorStage && !state.retrievalLimited) {
|
|
523
|
+
const record = { key, fileId: file.id, phase, round: batch.round, windowIds: batch.windowIds ?? [], ...(batch.interaction ? { groupId: batch.interaction.id } : {}), generation: { findings: structuredClone(generated.findings), limitations: structuredClone(generated.limitations) } };
|
|
524
|
+
acceptedStages.set(key, { generation: generated, record, completed: false });
|
|
525
|
+
checkpointStages();
|
|
526
|
+
}
|
|
527
|
+
for (const finding of generated.findings) { const index = priorStage ? result.findings.findIndex(existing => existing.id === finding.id) : -1; if (index < 0) result.findings.push(finding); else result.findings[index] = finding; }
|
|
431
528
|
if (generated.findings.some(f => f.attribution.status === 'missing')) result.limitations.push({ fileId: file.id, text: '部分发现缺少变更归因,仅校验了定位,尚不能确认属于本次回归' });
|
|
432
529
|
result.limitations.push(...generated.limitations.map(text => ({ fileId: file.id, text })));
|
|
433
|
-
if (
|
|
530
|
+
if (!groupedGeneration && !scheduled.slice(batchIndex + 1).some(next => !next.synthesis && !next.interaction)) releaseStage('primary:' + file.id);
|
|
531
|
+
if (options.enableStageContinuation && !batch.interaction && !batch.synthesis && state.windowCoverage && batch.round === 1) { state.windowCoverage.completed = [...new Set([...state.windowCoverage.completed, ...batch.windowIds])]; state.windowCoverage.pending = state.windowCoverage.pending.filter(id => !batch.windowIds.includes(id)); }
|
|
532
|
+
if (options.enableVerification === true && generated.findings.length && !priorStage?.completed) {
|
|
533
|
+
if (yieldedUnits.has('verification:' + file.id)) { defer(Object.assign(new Error('Verification allocation is waiting for released capacity'), { code: 'READER_CALL_BUDGET_EXCEEDED' }), 'verification:' + file.id); return; }
|
|
434
534
|
for (const finding of generated.findings) finding.verification = { status: 'pending', causality: 'unverified' };
|
|
435
535
|
try {
|
|
436
|
-
|
|
437
|
-
|
|
536
|
+
activeStage = 'verification';
|
|
537
|
+
const verdicts = await verificationLoop(trackedModel, { ...request, onProtocolEvent, reviewPhase: 'verification' }, generated.findings, stageScope('verification:' + file.id), {
|
|
538
|
+
correctProtocol:options.correctProtocol===true,maxRounds:maxRetrievalRounds,maxInputBytes, beforeCall, onBudgetLimited, yieldOnBudget: options.enableStageContinuation === true,
|
|
539
|
+
enableMigrationRegression: options.enableMigrationRegression === true, migrationContracts: report.migrationContracts?.contracts ?? [], migrationSnapshot: input,
|
|
438
540
|
onRetrieved: records => { (state.retrievalAudit ??= []).push(...records); },
|
|
439
541
|
onTruncated: () => { result.limitations.push({ fileId: file.id, text: '复核检索结果达到数量上限' }); },
|
|
440
542
|
});
|
|
@@ -447,11 +549,14 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
447
549
|
for (const finding of generated.findings) finding.verification = { status: 'incomplete', causality: 'unverified' };
|
|
448
550
|
result.limitations.push({ fileId: file.id, text: '候选复核未完成:' + (error?.message ?? String(error)) });
|
|
449
551
|
request.signal.throwIfAborted();
|
|
552
|
+
if (defer(error, 'verification:' + file.id)) return;
|
|
450
553
|
}
|
|
451
554
|
}
|
|
555
|
+
if (acceptedStages.has(key)) acceptedStages.get(key).completed = !options.enableVerification || generated.findings.every(finding => finding.verification?.status === 'completed');
|
|
452
556
|
if (batch.synthesis) state.synthesisStatus = 'completed';
|
|
453
557
|
if (batch.interaction) {
|
|
454
558
|
const interaction=report.interactions.find(p => p.groupId === batch.interaction.id);
|
|
559
|
+
if (combinedInteraction) Object.assign(interaction, { method: 'group-primary-validated-channel', channelGroupId: combinedInteraction.groupId });
|
|
455
560
|
if(rounds>1){interaction.completedRounds??=[];interaction.completedRounds.push(batch.round);interaction.reviewRounds=rounds;}
|
|
456
561
|
if(batch.round===rounds)interaction.status='completed';
|
|
457
562
|
}
|
|
@@ -460,10 +565,16 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
460
565
|
state.windowCoverage.pending = state.windowCoverage.pending.filter(id => !batch.windowIds.includes(id));
|
|
461
566
|
}
|
|
462
567
|
if(rounds>1)state.reviewPasses[batchIndex].status='completed';
|
|
568
|
+
if (retry) { const pending = deferredStages.find(stage => stage.key === key); if (pending) pending.succeeded = !options.enableVerification || generated.findings.every(finding => finding.verification?.status === 'completed'); }
|
|
463
569
|
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});
|
|
464
570
|
}catch(error){
|
|
465
571
|
enclosingRequest.signal.throwIfAborted();
|
|
466
|
-
if(
|
|
572
|
+
if (defer(error)) return;
|
|
573
|
+
if(phase==='primary'||error.code==='MODEL_ARTIFACT_FAILED')throw error;
|
|
574
|
+
if (['Model call budget exceeded', 'Model call budget exceeded before next review pass'].includes(error.message) || error.code === 'PROJECT_MODEL_CALL_LIMIT') {
|
|
575
|
+
result.limitations.push({ fileId: file.id, text: phase + '模型调用额度不足,尚未启动的阶段保留 pending;已完成主审成果保留' });
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
467
578
|
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}:{})};
|
|
468
579
|
(state.stageFailures??=[]).push(failure);
|
|
469
580
|
if(batch.synthesis)state.synthesisStatus='failed';
|
|
@@ -472,16 +583,17 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
472
583
|
result.limitations.push({fileId:file.id,text:phase+'未完成:'+error.message+';已完成主文件/窗口成果保留,不表示综合已通过'});
|
|
473
584
|
await options.onProgress?.({stage:'synthesis-failed',snapshotId:input.id,fileId:file.id,path:file.path,...failure,windowCoverage:state.windowCoverage});
|
|
474
585
|
}
|
|
475
|
-
}
|
|
586
|
+
};
|
|
587
|
+
for (const [batchIndex, batch] of scheduled.entries()) if (await executeStage(batch, batchIndex, enclosingRequest) === false) break;
|
|
476
588
|
}, { instructions, input: payload }, chainTimeout, signal);
|
|
477
589
|
if (signal?.aborted) throw signal.reason;
|
|
478
590
|
|
|
479
|
-
state.status = state.retrievalLimited ? 'pending' : 'completed';
|
|
591
|
+
state.status = state.retrievalLimited || state.continuationPending ? 'pending' : 'completed';
|
|
480
592
|
if (state.retrievalLimited) {
|
|
481
|
-
const codes = new Set((state.retrievalLimits ?? []).map(limit => limit.code));
|
|
593
|
+
const codes = new Set([...(state.retrievalLimits ?? []), ...(state.groupRetrievalLimits ?? [])].map(limit => limit.code));
|
|
482
594
|
state.reason = codes.has('RETRIEVAL_ROUND_BUDGET_EXCEEDED')
|
|
483
|
-
? '
|
|
484
|
-
: '共享读取调用或输出预算受限,未完成完整审查;已有发现保留,不表示通过';
|
|
595
|
+
? '单阶段检索轮数上限达到,未完成完整审查;读取额度可能仍有余量,已有发现保留,不表示通过'
|
|
596
|
+
: (readerAllocation ? '阶段取证分配或共享读取预算受限,未完成完整审查;其他阶段保留额度不表示本阶段已完成,已有发现保留' : '共享读取调用或输出预算受限,未完成完整审查;已有发现保留,不表示通过');
|
|
485
597
|
if (codes.size === 1) state.failureCode = [...codes][0];
|
|
486
598
|
}
|
|
487
599
|
|
|
@@ -496,8 +608,15 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
496
608
|
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';
|
|
497
609
|
}
|
|
498
610
|
} finally {
|
|
611
|
+
if (readerAllocation && file.eligibility === 'reviewable' && !options.resumeState?.completed?.some(row => row.fileId === file.id)) {
|
|
612
|
+
if (![...groupPrepared.values()].some(group => group.fileIds.includes(file.id))) releaseStage('primary:' + file.id);
|
|
613
|
+
releaseStage('verification:' + file.id);
|
|
614
|
+
releaseStage('synthesis:' + file.id);
|
|
615
|
+
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);
|
|
616
|
+
}
|
|
499
617
|
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');
|
|
500
618
|
await options.onProgress?.({stage:'file-complete',snapshotId:input.id,file:state,findings:result.findings});
|
|
619
|
+
checkpointStages();
|
|
501
620
|
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)] });
|
|
502
621
|
}
|
|
503
622
|
};
|
|
@@ -530,16 +649,29 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
530
649
|
}
|
|
531
650
|
if (options.enableGroupPrimary === true && scope && rounds === 1) {
|
|
532
651
|
const seen = new Set();
|
|
533
|
-
const groups = report.businessGrouping?.groups ?? report.grouping?.groups ?? [];
|
|
652
|
+
const groups = [...(report.migrationContracts?.groups ?? []), ...(report.businessGrouping?.groups ?? report.grouping?.groups ?? [])];
|
|
534
653
|
for (const group of groups) {
|
|
535
|
-
if (group.fileIds.some(id => seen.has(id) || options.resumeState?.completed?.some(row => row.fileId === id))) continue;
|
|
536
|
-
const prepared = prepareGroupPrimary(input, group, { instructions, scope, maxInputBytes, focusPrimaryContext: options.focusPrimaryContext === true, enableRiskPlanning: options.enableRiskPlanning === true });
|
|
654
|
+
if (group.fileIds.some(id => seen.has(id) || options.resumeState?.completed?.some(row => row.fileId === id) || options.resumeState?.stageGenerations?.some(row => row.fileId === id))) continue;
|
|
655
|
+
const prepared = prepareGroupPrimary(input, group, { instructions, scope, maxInputBytes, focusPrimaryContext: options.focusPrimaryContext === true, enableRiskPlanning: options.enableRiskPlanning === true, enableGroupInteractions: options.enableGroupInteractions === true, migrationContracts: report.migrationContracts?.contracts ?? [] });
|
|
537
656
|
if (!prepared) continue;
|
|
538
657
|
groupPrepared.set(group.id, prepared);
|
|
539
658
|
for (const id of group.fileIds) seen.add(id);
|
|
540
659
|
}
|
|
541
660
|
report.primaryGroups = [...groupPrepared].map(([groupId, p]) => ({ groupId, fileIds: p.fileIds, mode: 'group-primary' }));
|
|
542
661
|
}
|
|
662
|
+
if (scope && (options.optimizeReviewCalls === true || options.enableStageContinuation === true)) {
|
|
663
|
+
const units = [];
|
|
664
|
+
const reusable = new Set((options.resumeState?.completed ?? []).map(row => row.fileId));
|
|
665
|
+
for (const [id, prepared] of groupPrepared) units.push({ id: 'group:' + id, weight: prepared.fileIds.length * 4 });
|
|
666
|
+
for (const file of input.files.filter(file => file.eligibility === 'reviewable' && !reusable.has(file.id))) {
|
|
667
|
+
const prepared = preparePrimaryInput(input, file, { instructions, scope, grouping: report.grouping, maxInputBytes, riskPlanningMode: options.riskPlanningMode, focusPrimaryContext: options.focusPrimaryContext === true, enableRiskPlanning: options.enableRiskPlanning, skipSmallRiskPlans: options.skipSmallRiskPlans });
|
|
668
|
+
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) });
|
|
669
|
+
if (prepared.synthesis?.status === 'ready') units.push({ id: 'synthesis:' + file.id, weight: 2 });
|
|
670
|
+
if (options.enableVerification === true) units.push({ id: 'verification:' + file.id, weight: 2 });
|
|
671
|
+
}
|
|
672
|
+
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 });
|
|
673
|
+
if (units.length) readerAllocation = createReviewReaderAllocation(scope, units);
|
|
674
|
+
}
|
|
543
675
|
const dependencies = new Map();
|
|
544
676
|
for (const plan of interactionPlans.filter(p => p.prepared.status === 'ready')) {
|
|
545
677
|
const members = input.files.map((file, index) => plan.group.fileIds.includes(file.id) ? index : -1).filter(index => index >= 0);
|
|
@@ -550,7 +682,46 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
550
682
|
try { await runFile(index); }
|
|
551
683
|
catch (error) { controller.abort(error); throw error; }
|
|
552
684
|
}, dependencies, fileConcurrency);
|
|
685
|
+
if (options.enableStageContinuation === true && deferredStages.length && !signal.aborted) {
|
|
686
|
+
const primaryDone = input.files.filter(file => file.eligibility === 'reviewable' && (options.resumeState?.completed?.some(row => row.fileId === file.id) || primaryStageKeys.get(file.id)?.length && primaryStageKeys.get(file.id).every(key => acceptedStages.has(key)))).map(file => file.id);
|
|
687
|
+
const units = deferredStages.map(stage => ({ id: hash(stage.key), kind: stage.phase, fileIds: stage.fileIds, dependencies: stage.fileIds.map(id => 'primary:' + id) }));
|
|
688
|
+
const continuationOptions = { rounds, maxInputBytes, enableMigrationContracts: options.enableMigrationContracts === true, enableMigrationRegression: options.enableMigrationRegression === true };
|
|
689
|
+
let continuation = createStageContinuation(input, continuationOptions, { completedPrimary: primaryDone, units, maxAttempts: 2 });
|
|
690
|
+
if (options.resumeState?.continuation?.state) {
|
|
691
|
+
const fresh = continuation.checkpoint(), prior = options.resumeState.continuation.state;
|
|
692
|
+
for (const unit of fresh.units) { const old = prior.units.find(item => item.id === unit.id && item.kind === unit.kind && JSON.stringify(item.fileIds) === JSON.stringify(unit.fileIds)); if (old && old.status !== 'completed') { unit.attempts = Math.min(fresh.maxAttempts, old.attempts); unit.stopped = unit.attempts >= fresh.maxAttempts; unit.pendingReason = unit.stopped ? 'STAGE_ATTEMPTS_EXHAUSTED' : 'STAGE_INTERRUPTED'; } }
|
|
693
|
+
continuation = createStageContinuation(input, continuationOptions, { completedPrimary: primaryDone, units, checkpoint: fresh, maxAttempts: 2 });
|
|
694
|
+
}
|
|
695
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
696
|
+
let progressed = false;
|
|
697
|
+
for (const stage of deferredStages) {
|
|
698
|
+
if (stage.done || !continuation.ready().some(unit => unit.id === hash(stage.key))) continue;
|
|
699
|
+
if (performance.now() >= fileDeadlines.get(stage.fileId) || signal.aborted || report.modelCalls >= maxCalls) { continuation.stop(hash(stage.key), 'STAGE_NO_PROGRESS'); continue; }
|
|
700
|
+
const admitted = !yieldedUnits.has(stage.allocationId) ? { resumed: true } : stage.requests.length ? readerAllocation.resume(stage.allocationId, stage.requests) : readerAllocation.resumeBudget(stage.allocationId);
|
|
701
|
+
if (!admitted.resumed) continue;
|
|
702
|
+
yieldedUnits.delete(stage.allocationId);
|
|
703
|
+
const token = continuation.start(hash(stage.key), { signal });
|
|
704
|
+
stage.attempts++; await stage.run();
|
|
705
|
+
progressed = true;
|
|
706
|
+
if (stage.succeeded && !yieldedUnits.has(stage.allocationId)) { stage.done = true; continuation.complete(hash(stage.key), token.attempt, { signal }); if (!deferredStages.some(other => other !== stage && !other.done && other.allocationId === stage.allocationId)) readerAllocation.release(stage.allocationId); }
|
|
707
|
+
else { continuation.yield(hash(stage.key), stage.reason); if (!yieldedUnits.has(stage.allocationId)) continuation.stop(hash(stage.key), 'STAGE_FAILED'); }
|
|
708
|
+
}
|
|
709
|
+
if (!progressed) break;
|
|
710
|
+
}
|
|
711
|
+
report.stageContinuation = continuation.summary();
|
|
712
|
+
report.continuation = continuation.checkpoint();
|
|
713
|
+
report.continuationOptions = continuationOptions;
|
|
714
|
+
for (const file of input.files) {
|
|
715
|
+
const result = fileResults[input.files.indexOf(file)];
|
|
716
|
+
const pending = deferredStages.filter(stage => stage.fileId === file.id && !stage.done);
|
|
717
|
+
if (result?.state.continuationPending && !pending.length) { result.state.continuationPending = false; if (result.state.status === 'pending' && primaryDone.includes(file.id) && !result.state.stageFailures?.length && !result.state.reviewPasses?.some(pass => pass.status !== 'completed')) result.state.status = result.state.retrievalLimited ? 'pending' : 'completed'; }
|
|
718
|
+
}
|
|
719
|
+
checkpointStages();
|
|
720
|
+
await options.onResumeCheckpoint?.({ ...report, files: fileResults.map(result => result.state), findings: fileResults.flatMap(result => result.findings), limitations: [...report.limitations, ...fileResults.flatMap(result => result.limitations)] });
|
|
721
|
+
}
|
|
722
|
+
for (const id of yieldedUnits) readerAllocation?.release(id);
|
|
553
723
|
if (checkpointFailed) throw checkpointFailure;
|
|
724
|
+
for (const result of fileResults) if (result?.state.continuationPending === false) { const state = result.state; state.resumeEligible = state.status === 'completed' && !state.stageFailures?.length && !state.reviewPasses?.some(pass => pass.status !== 'completed') && !state.windowCoverage?.pending?.length && !state.retrievalLimited && result.findings.every(finding => finding.verification?.status === 'completed') && report.interactions.filter(item => item.fileIds?.includes(state.fileId)).every(item => item.status === 'completed'); }
|
|
554
725
|
report.files = fileResults.map(result => result.state);
|
|
555
726
|
report.findings = fileResults.flatMap(result => result.findings);
|
|
556
727
|
report.limitations.push(...fileResults.flatMap(result => result.limitations));
|
|
@@ -576,6 +747,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
576
747
|
await options.onResumeCheckpoint?.(report);
|
|
577
748
|
report.followup = coverageFollowup(report);
|
|
578
749
|
if (scope) report.readerBudget = { ...scope.usage(), ...scope.budget(), ...readerLimits };
|
|
750
|
+
if (readerAllocation) report.readerAllocation = readerAllocation.summary();
|
|
579
751
|
report.diagnostics = reviewDiagnostics(report, { reviewWallMs: performance.now() - startedAt });
|
|
580
752
|
return report;
|
|
581
753
|
}
|
|
@@ -621,7 +793,12 @@ function renderFinding(output, finding, index, duplicates) {
|
|
|
621
793
|
}
|
|
622
794
|
|
|
623
795
|
export function markdownReport(report) {
|
|
624
|
-
const output = ['# Code Review 报告', '', '状态:' + report.status, '快照:' + report.snapshotId, '', '##
|
|
796
|
+
const output = ['# Code Review 报告', '', '状态:' + report.status, '快照:' + report.snapshotId, ...reportSummaryMarkdown(report), '', '## 审查发现'];
|
|
797
|
+
if (!report.findings.length) output.push(report.status === 'completed' ? '在已审查范围内未发现具体问题;不代表代码已被证明正确。' : '当前没有有效问题记录,但审查存在未完成项或限制,不能视为通过。');
|
|
798
|
+
const duplicates = new Map();
|
|
799
|
+
for (const group of groupDuplicateFindings(report.findings)) for (const index of group.findingIndices.slice(1)) duplicates.set(index, group.findingIndices[0]);
|
|
800
|
+
for (const [index, finding] of report.findings.entries()) renderFinding(output, finding, index, duplicates);
|
|
801
|
+
output.push('', '## 覆盖情况');
|
|
625
802
|
if (report.callerDiscovery) output.push('- 引用方发现:扫描 '+report.callerDiscovery.scanned.length+' 个文件,捕获 '+report.callerDiscovery.capturedPaths.length+' 个上下文;仅批准目录字面引用导航,不证明函数调用或完整覆盖。');
|
|
626
803
|
if(report.businessRequirement)output.push('- 业务需求:SHA256 '+safe(report.businessRequirement.hash)+' · '+report.businessRequirement.bytes+' bytes;不可信约束,不是证据或权限,全文不在报告重复。');
|
|
627
804
|
for (const file of report.files) for (const correction of file.anchorCorrections ?? []) output.push('- 定位纠正:' + safe(correction.originalCandidate.fileId) + ' · ' + safe(correction.status) + ' · ' + safe(correction.reason) + ';原完整候选保留JSON,仅定位不证明因果');
|
|
@@ -640,18 +817,10 @@ export function markdownReport(report) {
|
|
|
640
817
|
}
|
|
641
818
|
for (const file of report.files) if (file.windowCoverage) output.push('- ' + safe(file.path) + ':变更窗口已完成 ' + file.windowCoverage.completed.length + ',待审 ' + file.windowCoverage.pending.length + ';仅表示窗口分析完成,不证明全文正确');
|
|
642
819
|
for (const file of report.files) if (file.synthesisStatus) output.push('- ' + safe(file.path) + ':跨窗口综合 ' + safe(file.synthesisStatus) + ';不等于事实正确性证明');
|
|
643
|
-
for (const interaction of report.interactions ?? []) output.push('- 跨文件综合 ' + safe(interaction.groupId) + ':' + safe(interaction.status) + '
|
|
820
|
+
for (const interaction of report.interactions ?? []) output.push('- 跨文件综合 ' + safe(interaction.groupId) + ':' + safe(interaction.status) + (interaction.failureCode ? ' · ' + safe(interaction.failureCode) : '') + (interaction.reason ? ' — ' + safe(interaction.reason) : '') + ';与组末主文件共享生命周期,不证明完整交互覆盖');
|
|
644
821
|
if (report.rules?.length) output.push('', '## 项目规则', '规则仅为已批准约束数据,不扩大权限或证明缺陷。', ...report.rules.map(rule => '- ' + safe(rule.path) + ' · SHA256 ' + safe(rule.hash)));
|
|
645
822
|
if (report.followup?.suggestedPaths.length) output.push('', '## 后续选择建议', '仅为原快照未完成项;不会自动续审。再次执行需新预览、新审批,并显式选择规则和上下文。阻断项需先处理原因;关联上下文不等于主文件已审查。', ...report.followup.items.filter(item => item.followup).map(item => '- ' + safe(item.path) + ':' + item.status + (item.incompleteVerification ? '(候选复核未完成)' : '')));
|
|
646
|
-
output.push('', '## 审查发现');
|
|
647
|
-
if (!report.findings.length) output.push(report.status === 'completed' ? '在已审查范围内未发现具体问题;不代表代码已被证明正确。' : '当前没有有效问题记录,但审查存在未完成项或限制,不能视为通过。');
|
|
648
823
|
output.push(...diagnosticsMarkdown(report.diagnostics));
|
|
649
|
-
// Recompute from exact data rather than trusting potentially stale group metadata.
|
|
650
|
-
const duplicates = new Map();
|
|
651
|
-
for (const group of groupDuplicateFindings(report.findings)) {
|
|
652
|
-
for (const index of group.findingIndices.slice(1)) duplicates.set(index, group.findingIndices[0]);
|
|
653
|
-
}
|
|
654
|
-
for (const [index, finding] of report.findings.entries()) renderFinding(output, finding, index, duplicates);
|
|
655
824
|
if (report.limitations.length) output.push('', '## 限制', ...report.limitations.map(l => '- ' + safe(l.text)));
|
|
656
825
|
return output.join('\n') + '\n';
|
|
657
826
|
}
|