@feng0531/dsh-code-review 0.88.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +15 -1
- package/RELEASE-NOTES.md +10 -12
- package/evaluation/README.md +6 -0
- package/index.mjs +9 -2
- package/package.json +2 -2
- package/src/approved-retrieval.mjs +13 -0
- package/src/background-review.mjs +1 -0
- package/src/child-output-schema.mjs +22 -0
- package/src/counter-verification.mjs +48 -0
- package/src/finding-ledger.mjs +169 -0
- package/src/host-review-children.mjs +100 -0
- package/src/incremental-review.mjs +95 -0
- package/src/project-execution.mjs +6 -1
- package/src/project-review-service.mjs +12 -6
- package/src/report-findings.mjs +5 -0
- package/src/report-summary.mjs +43 -4
- package/src/review-child-broker.mjs +65 -0
- package/src/review-child-plugin.mjs +32 -0
- package/src/review-effort.mjs +104 -0
- package/src/review-history.mjs +29 -0
- package/src/review-jobs.mjs +1 -0
- package/src/review.mjs +10 -6
- package/src/service.mjs +37 -10
- package/src/verification-loop.mjs +3 -0
- package/src/whole-scope-review.mjs +51 -18
- package/src/whole-scope-workers.mjs +88 -0
package/src/review.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { approvedRetrievalSnapshot } from './approved-retrieval.mjs';
|
|
1
2
|
import { REVIEW_INSTRUCTIONS, PROJECT_REVIEW_INSTRUCTIONS } from './prompts.mjs';
|
|
2
3
|
import { reviewWholeScope } from './whole-scope-review.mjs';
|
|
3
4
|
import { reviewRounds } from './review-rounds.mjs';
|
|
@@ -16,6 +17,7 @@ import { validateSynthesisReads } from './synthesis-evidence.mjs';
|
|
|
16
17
|
import { hash } from './content.mjs';
|
|
17
18
|
import { coverageFollowup } from './coverage-followup.mjs';
|
|
18
19
|
import { verificationLoop } from './verification-loop.mjs';
|
|
20
|
+
import { runCounterVerification } from './counter-verification.mjs';
|
|
19
21
|
import { inferFileRelations } from './file-relations.mjs';
|
|
20
22
|
import { buildReviewGroups } from './review-groups.mjs';
|
|
21
23
|
import { changeMap } from './change-map.mjs';
|
|
@@ -47,7 +49,7 @@ import { buildMigrationContracts } from './migration-contracts.mjs';
|
|
|
47
49
|
|
|
48
50
|
export { REVIEW_INSTRUCTIONS };
|
|
49
51
|
export function createReviewReader(snapshot, options = {}) {
|
|
50
|
-
return configureWireBudget(createRetrievalScope(snapshot, { ...reviewReaderBudget(snapshot, options.readerBudgetPolicy ?? 'fixed'), signal: options.signal, ...(options.optimizeReviewCalls ? { reuseResults: true } : {}) }), { nativeOutputTools: options.nativeOutputTools === true });
|
|
52
|
+
return configureWireBudget(createRetrievalScope(approvedRetrievalSnapshot(snapshot, options.approvedRetrievalSnapshot), { ...reviewReaderBudget(snapshot, options.readerBudgetPolicy ?? 'fixed'), signal: options.signal, ...(options.optimizeReviewCalls ? { reuseResults: true } : {}) }), { nativeOutputTools: options.nativeOutputTools === true });
|
|
51
53
|
}
|
|
52
54
|
export { PROJECT_REVIEW_INSTRUCTIONS };
|
|
53
55
|
const nonempty = value => typeof value === 'string' && value.trim().length > 0 && value.length <= 8000;
|
|
@@ -107,7 +109,7 @@ async function invoke(model, request, timeoutMs, parentSignal) {
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
110
|
-
if (options.executionStrategy === 'whole-scope') return reviewWholeScope(snapshot, model, { ...options, instructions: REVIEW_INSTRUCTIONS, validateFindingResponse: validateResponse });
|
|
112
|
+
if (options.executionStrategy === 'whole-scope') return reviewWholeScope(snapshot, model, { ...options, instructions: options.instructions ?? REVIEW_INSTRUCTIONS, validateFindingResponse: validateResponse });
|
|
111
113
|
if (options.executionStrategy !== undefined && options.executionStrategy !== 'staged') throw new Error('Invalid review execution strategy');
|
|
112
114
|
const startedAt = performance.now();
|
|
113
115
|
const rounds = reviewRounds(options.reviewRounds);
|
|
@@ -131,7 +133,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
131
133
|
if(!Number.isSafeInteger(timeoutMs)||timeoutMs<0)throw new Error('Invalid review limit');
|
|
132
134
|
// Clone once: callers cannot change the input while a model call is pending.
|
|
133
135
|
const input = structuredClone(snapshot);
|
|
134
|
-
const instructions=input.projectScope?PROJECT_REVIEW_INSTRUCTIONS:REVIEW_INSTRUCTIONS;
|
|
136
|
+
const instructions=options.instructions ?? (input.projectScope?PROJECT_REVIEW_INSTRUCTIONS:REVIEW_INSTRUCTIONS);
|
|
135
137
|
if (!input || !nonempty(input.id) || !['git', 'svn'].includes(input.vcs) || !Array.isArray(input.files) || input.files.length > 200) throw new Error('Invalid review snapshot');
|
|
136
138
|
const identities = new Set(), paths = new Set();
|
|
137
139
|
for (const file of input.files) {
|
|
@@ -144,7 +146,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
144
146
|
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');
|
|
145
147
|
const evidenceContext = options.enableEvidenceContext === true ? createEvidenceContext(input.id) : null;
|
|
146
148
|
const readerLimits = reviewReaderBudget(input, options.readerBudgetPolicy ?? 'fixed');
|
|
147
|
-
const scope = options.enableRetrieval === true ? createRetrievalScope({ ...input, context: input.context ?? [] }, { ...readerLimits, signal, ...(options.optimizeReviewCalls === true ? { reuseResults: true } : {}) }) : null;
|
|
149
|
+
const scope = options.enableRetrieval === true ? createRetrievalScope({ ...approvedRetrievalSnapshot(input, options.approvedRetrievalSnapshot), context: input.context ?? [] }, { ...readerLimits, signal, ...(options.optimizeReviewCalls === true ? { reuseResults: true } : {}) }) : null;
|
|
148
150
|
const report = { schemaVersion: 1, snapshotId: input.id, vcs: input.vcs, status: 'completed', files: [], findings: [], limitations: [], modelCalls: 0 };
|
|
149
151
|
validateBusinessRequirement(input.businessRequirement);
|
|
150
152
|
if(input.businessRequirement!==undefined)report.businessRequirement={hash:hash(input.businessRequirement),bytes:Buffer.byteLength(input.businessRequirement),status:'untrusted-constraint'};
|
|
@@ -612,7 +614,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
612
614
|
for (const finding of generated.findings) finding.verification = { status: 'pending', causality: 'unverified' };
|
|
613
615
|
try {
|
|
614
616
|
activeStage = 'verification';
|
|
615
|
-
|
|
617
|
+
let verdicts = await verificationLoop(trackedModel, { ...request, onProtocolEvent, reviewPhase: 'verification' }, generated.findings, stageScope('verification:' + file.id), {
|
|
616
618
|
correctProtocol:options.correctProtocol===true,maxRounds:maxRetrievalRounds,maxInputBytes, beforeCall, onBudgetLimited, yieldOnBudget: options.enableStageContinuation === true,
|
|
617
619
|
enableMigrationRegression: options.enableMigrationRegression === true, migrationContracts: report.migrationContracts?.contracts ?? [], migrationSnapshot: input,
|
|
618
620
|
onMigrationSeeds: event => {
|
|
@@ -626,7 +628,9 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
626
628
|
onTruncated: () => { result.limitations.push({ fileId: file.id, text: '复核检索结果达到数量上限' }); },
|
|
627
629
|
});
|
|
628
630
|
request.signal.throwIfAborted();
|
|
629
|
-
generated.findings
|
|
631
|
+
verdicts = await runCounterVerification(trackedModel, { ...request, onProtocolEvent, reviewPhase:'verification' }, generated.findings, verdicts, stageScope('verification:' + file.id), { enabled:options.enableCounterVerification === true, maxRounds:maxRetrievalRounds,maxInputBytes,beforeCall,onBudgetLimited, isFatalError:error=>checkpointFailed||error?.code==='MODEL_ARTIFACT_FAILED' });
|
|
632
|
+
request.signal.throwIfAborted();
|
|
633
|
+
generated.findings.forEach((finding, i) => { finding.verification = { status: verdicts[i].counterVerification?.status === 'incomplete' ? 'incomplete' : 'completed', ...verdicts[i] }; });
|
|
630
634
|
if (verdicts.some(v => v.verdict === 'supported' && v.regression?.classification === 'uncertain')) result.limitations.push({ fileId: file.id, text: '部分模型支持候选缺少充分前后对照,不能确认由本次变更引入' });
|
|
631
635
|
if (verdicts.some(v => v.verdict === 'uncertain')) result.limitations.push({ fileId: file.id, text: '部分候选复核仍不确定,不能视为已确认或已排除' });
|
|
632
636
|
} catch (error) {
|
package/src/service.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { previewValidity } from './preview-validity.mjs';
|
|
2
|
+
import { reviewEffortPolicy } from './review-effort.mjs';
|
|
3
|
+
import { prepareReviewHistory, finishReviewHistory } from './review-history.mjs';
|
|
2
4
|
import { configureWireBudget } from './model-wire-budget.mjs';
|
|
3
5
|
import {REVIEW_RUNTIME} from './runtime-identity.mjs';
|
|
4
6
|
import { buildResumeState, validateResumeState, resumePreview } from './review-resume.mjs';
|
|
@@ -56,10 +58,12 @@ function buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcur
|
|
|
56
58
|
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不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先内部核对预览与当前人类授权。用户已明确要求审核且范围、模型和发送目的地一致时直接执行,不重复询问确认;只要求预览或明确要求等待确认时不得执行,超出授权或边界不明确时才询问。宿主权限和快照/身份校验仍须通过。`;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
function createSessionReviewService(llm, { capture = captureSnapshot, executionStrategy = 'staged', maxInputBytes = 96 * 1024, 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, enableEvidenceContext = false, inlineInteractionRisks = false, enableMigrationContracts = false, enableMigrationRegression = false, enableGroupInteractions = false, enableStageContinuation = false, authorize = async () => false } = {}) {
|
|
61
|
+
function createSessionReviewService(llm, { capture = captureSnapshot, reviewEffort = 'auto', enableCounterVerification = false, hostChildren, wholeScopeConcurrency = 2, executionStrategy = 'staged', maxInputBytes = 96 * 1024, 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, enableEvidenceContext = false, inlineInteractionRisks = false, enableMigrationContracts = false, enableMigrationRegression = false, enableGroupInteractions = false, enableStageContinuation = false, authorize = async () => false } = {}) {
|
|
60
62
|
for (const value of [ttlMs, maxPreviews]) if (!Number.isSafeInteger(value) || value < 1) throw new Error('Invalid preview limit');
|
|
61
63
|
if (!Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1 || maxInputBytes > 4 * 1024 * 1024) throw new Error('Invalid configured input capacity');
|
|
62
64
|
if (!['staged', 'whole-scope'].includes(executionStrategy)) throw new Error('Invalid review execution strategy');
|
|
65
|
+
if (!['auto','quick','standard','deep'].includes(reviewEffort) || typeof enableCounterVerification !== 'boolean') throw new Error('Invalid review effort policy');
|
|
66
|
+
wholeScopeConcurrency = reviewConcurrency(wholeScopeConcurrency);
|
|
63
67
|
changesConcurrency = reviewConcurrency(changesConcurrency);
|
|
64
68
|
maxRetrievalRounds = reviewRetrievalRounds(maxRetrievalRounds);
|
|
65
69
|
riskPlanningMode = validateRiskPlanningMode(riskPlanningMode);
|
|
@@ -67,7 +71,7 @@ function createSessionReviewService(llm, { capture = captureSnapshot, executionS
|
|
|
67
71
|
for (const value of [optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds, nativeOutputTools, enableGroupPrimary, enableEvidenceContext, inlineInteractionRisks, enableMigrationContracts, enableMigrationRegression, enableGroupInteractions, enableStageContinuation]) if (typeof value !== 'boolean') throw new Error('Invalid review optimization option');
|
|
68
72
|
const lifetime = new AbortController();
|
|
69
73
|
const bind = exec => ({ ...exec, signal: exec.signal ? AbortSignal.any([exec.signal, lifetime.signal]) : lifetime.signal });
|
|
70
|
-
const resumeOptions = (options, route, rounds) => ({ captureOptions: options, route, runtime: REVIEW_RUNTIME, instructionsHash: hash(
|
|
74
|
+
const resumeOptions = (options, route, rounds, effortMode, instructions) => ({ captureOptions: options, route, runtime: REVIEW_RUNTIME, instructionsHash: hash(instructions), execution: { reviewEffort:effortMode, enableCounterVerification, executionStrategy, maxInputBytes, wholeScopeConcurrency, rounds, enableRiskPlanning, skipSmallRiskPlans, enableBusinessGrouping, enableAnchorCorrection, changesConcurrency, maxRetrievalRounds, riskPlanningMode, readerBudgetPolicy, optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds, nativeOutputTools, enableGroupPrimary, enableEvidenceContext, inlineInteractionRisks, enableMigrationContracts, enableMigrationRegression, enableGroupInteractions, enableStageContinuation } });
|
|
71
75
|
const previews = new Map();
|
|
72
76
|
let busy = false;
|
|
73
77
|
function prune() { for (const [id, p] of previews) if (p.expires <= now()) previews.delete(id); }
|
|
@@ -76,7 +80,7 @@ function createSessionReviewService(llm, { capture = captureSnapshot, executionS
|
|
|
76
80
|
exec = bind(exec);
|
|
77
81
|
exec.signal.throwIfAborted();
|
|
78
82
|
if (busy) throw new Error('Review service busy');
|
|
79
|
-
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');
|
|
83
|
+
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','rereviewFromJobId','findingOutcomes','reviewEffort','rereviewScope'].includes(k))) throw new Error('Unknown preview argument');
|
|
80
84
|
if (args.resumeFromJobId !== undefined && (typeof args.resumeFromJobId !== 'string' || !/^[a-f0-9-]{36}$/.test(args.resumeFromJobId))) throw new Error('Invalid resume job id');
|
|
81
85
|
if (args.autoContext !== undefined && typeof args.autoContext !== 'boolean') throw new Error('Invalid autoContext option');
|
|
82
86
|
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');
|
|
@@ -84,10 +88,22 @@ function createSessionReviewService(llm, { capture = captureSnapshot, executionS
|
|
|
84
88
|
if (executionStrategy === 'whole-scope' && (rounds !== 1 || args.resumeFromJobId !== undefined)) throw new Error('Whole-scope requires fresh single-round changes review');
|
|
85
89
|
validateBusinessRequirement(args.businessRequirement);
|
|
86
90
|
if (args.callerScopePaths !== undefined) validateCallerScopes(args.callerScopePaths);
|
|
91
|
+
const effortMode = args.reviewEffort ?? reviewEffort;
|
|
92
|
+
if (!['auto','quick','standard','deep'].includes(effortMode)) throw new Error('Invalid review effort mode');
|
|
93
|
+
if (args.rereviewFromJobId !== undefined && (typeof args.rereviewFromJobId !== 'string' || !/^[a-f0-9-]{36}$/.test(args.rereviewFromJobId) || args.resumeFromJobId !== undefined || rounds !== 1)) throw new Error('Invalid or conflicting re-review source');
|
|
94
|
+
if (args.findingOutcomes !== undefined && (!Array.isArray(args.findingOutcomes) || args.findingOutcomes.length > 200 || args.rereviewFromJobId === undefined)) throw new Error('Finding outcomes require bounded explicit re-review');
|
|
95
|
+
if(args.rereviewScope!==undefined&&(!['full','diff-only'].includes(args.rereviewScope)||args.rereviewFromJobId===undefined))throw new Error('Explicit re-review scope requires owned prior review');
|
|
87
96
|
const current = owner(exec);
|
|
88
97
|
busy = true;
|
|
89
98
|
try {
|
|
90
99
|
const options = structuredClone(args);
|
|
100
|
+
const rereviewJobId = options.rereviewFromJobId, findingOutcomes = options.findingOutcomes, explicitDiffOnly = options.rereviewScope === 'diff-only';
|
|
101
|
+
delete options.rereviewFromJobId; delete options.findingOutcomes; delete options.reviewEffort; delete options.rereviewScope;
|
|
102
|
+
let previousHistory;
|
|
103
|
+
if (rereviewJobId !== undefined) {
|
|
104
|
+
if (typeof exec.resolvePriorReview !== 'function') throw new Error('Owned prior review requires a new background preview');
|
|
105
|
+
previousHistory = await exec.resolvePriorReview(rereviewJobId);
|
|
106
|
+
}
|
|
91
107
|
const sourceJobId = options.resumeFromJobId;
|
|
92
108
|
delete options.resumeFromJobId;
|
|
93
109
|
let savedResume;
|
|
@@ -99,14 +115,19 @@ function createSessionReviewService(llm, { capture = captureSnapshot, executionS
|
|
|
99
115
|
const snapshot = await capture(target, { ...options, signal: exec.signal });
|
|
100
116
|
exec.signal?.throwIfAborted();
|
|
101
117
|
if (owner(exec).cwd !== current.cwd) throw new Error('Agent directory changed; preview again');
|
|
102
|
-
const
|
|
118
|
+
const effort = reviewEffortPolicy(snapshot,{mode:effortMode});
|
|
119
|
+
const history = prepareReviewHistory(snapshot,current.route,{...options,reviewEffort:effortMode,executionStrategy,enableCounterVerification},{previous:previousHistory,outcomes:findingOutcomes,explicitDiffOnly});
|
|
120
|
+
const instructions = REVIEW_INSTRUCTIONS + '\nReview focus (not proof, no additional authority): ' + JSON.stringify(effort.checks);
|
|
121
|
+
const resumeState = savedResume === undefined ? undefined : validateResumeState(savedResume,snapshot,resumeOptions(options,current.route,rounds,effortMode,instructions));
|
|
103
122
|
const readerBudget = reviewReaderBudget(snapshot, readerBudgetPolicy);
|
|
104
|
-
const plan = buildCoveragePlan(snapshot, { executionStrategy, enableEvidenceContext, inlineInteractionRisks, enableMigrationContracts, enableMigrationRegression, enableGroupInteractions, enableStageContinuation, enableGroupPrimary, resumeState, focusPrimaryContext, riskPlanningMode, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:rounds, enableBusinessGrouping, instructions
|
|
123
|
+
const plan = buildCoveragePlan(history.selection?.reviewSnapshot ?? snapshot, { executionStrategy, enableEvidenceContext, inlineInteractionRisks, enableMigrationContracts, enableMigrationRegression, enableGroupInteractions, enableStageContinuation, enableGroupPrimary, resumeState, focusPrimaryContext, riskPlanningMode, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:rounds, enableBusinessGrouping, instructions, scope: configureWireBudget(createRetrievalScope(snapshot, { ...readerBudget, signal: exec.signal, ...(optimizeReviewCalls ? { reuseResults: true } : {}) }), { nativeOutputTools }), grouping: buildReviewGroups(snapshot.files, inferFileRelations(snapshot.files)), maxInputBytes, signal: exec.signal });
|
|
124
|
+
const useHostChildren = executionStrategy === 'whole-scope' && plan.primaryGroups.length > 1 && !!hostChildren;
|
|
125
|
+
if (useHostChildren) await hostChildren.prepare();
|
|
105
126
|
prune();
|
|
106
127
|
while (previews.size >= maxPreviews) previews.delete(previews.keys().next().value);
|
|
107
128
|
const previewId = randomUUID();
|
|
108
129
|
const expires = now() + ttlMs;
|
|
109
|
-
previews.set(previewId, { ...current, target, options, rounds, snapshotId: snapshot.id, resumeState, sourceJobId, expires });
|
|
130
|
+
previews.set(previewId, { ...current, target, options, rounds, snapshotId: snapshot.id, resumeState, sourceJobId, useHostChildren, effort, instructions, history, rereviewJobId, expires });
|
|
110
131
|
return {
|
|
111
132
|
...(options.businessRequirement !== undefined
|
|
112
133
|
? { businessRequirement: { text: options.businessRequirement, hash: hash(options.businessRequirement), bytes: Buffer.byteLength(options.businessRequirement) } }
|
|
@@ -116,9 +137,11 @@ function createSessionReviewService(llm, { capture = captureSnapshot, executionS
|
|
|
116
137
|
validForMs: ttlMs,
|
|
117
138
|
...(resumeState ? { resume: resumePreview(resumeState, sourceJobId) } : {}),
|
|
118
139
|
plan,
|
|
140
|
+
reviewEffort: effort,
|
|
141
|
+
...(rereviewJobId ? { rereview: { sourceJobId:rereviewJobId,cycle:history.previous.cycle+1,maxCycles:3,incremental:history.incremental,notice:'Historical metadata only; no prior approval, conclusions or receipts are reused. Navigation paths are not coverage.' } } : {}),
|
|
119
142
|
snapshotId: snapshot.id,
|
|
120
143
|
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.' },
|
|
121
|
-
execution: { executionStrategy, ...(executionStrategy === 'whole-scope' ? { resumeSupported: false, maxInputBytes } : {}), nativeOutputTools, enableGroupPrimary, enableEvidenceContext, inlineInteractionRisks, enableMigrationContracts, enableMigrationRegression, enableGroupInteractions, enableStageContinuation, fileConcurrency: changesConcurrency, maxRetrievalRounds, ...(optimizeReviewCalls ? { riskPlanningMode, readerBudget: { policy: readerBudgetPolicy, ...readerBudget }, missingContextSeeds: enablePrimarySeeds, focusPrimaryContext, searchContextSeeds: enableSearchSeeds } : {}) },
|
|
144
|
+
execution: { executionStrategy, counterVerification: enableCounterVerification && effort.counterVerification.eligible, ...(executionStrategy === 'whole-scope' ? { resumeSupported: false, maxInputBytes, batchExecution: useHostChildren ? 'host-subagents' : 'direct', batchConcurrency: useHostChildren ? Math.min(wholeScopeConcurrency, plan.primaryGroups.length) : 1 } : {}), nativeOutputTools, enableGroupPrimary, enableEvidenceContext, inlineInteractionRisks, enableMigrationContracts, enableMigrationRegression, enableGroupInteractions, enableStageContinuation, fileConcurrency: changesConcurrency, maxRetrievalRounds, ...(optimizeReviewCalls ? { riskPlanningMode, readerBudget: { policy: readerBudgetPolicy, ...readerBudget }, missingContextSeeds: enablePrimarySeeds, focusPrimaryContext, searchContextSeeds: enableSearchSeeds } : {}) },
|
|
122
145
|
repositoryRoot: snapshot.root,
|
|
123
146
|
vcs: snapshot.vcs,
|
|
124
147
|
...(snapshot.changeScope ? { changeScope: snapshot.changeScope } : options.changeScope !== undefined ? { changeScope: options.changeScope } : {}),
|
|
@@ -148,7 +171,7 @@ function createSessionReviewService(llm, { capture = captureSnapshot, executionS
|
|
|
148
171
|
...(snapshot.callerDiscovery ? { callerDiscovery: snapshot.callerDiscovery } : {}),
|
|
149
172
|
...(snapshot.autoContext ? { autoContext: snapshot.autoContext } : {}),
|
|
150
173
|
rules: (snapshot.rules ?? []).map(({ path, hash }) => ({ path, hash })),
|
|
151
|
-
notice: executionStrategy === 'whole-scope' ? '整体优先主审:批准完整双侧按实际输入字节合批;不执行逐文件风险规划、业务分组或旧专门综合。候选集中独立复核;补读可增加调用,共享读取账本不重置。超大文件明确阻断,多批跨边界未核查保持部分完成;不承诺一次请求或审查通过。不支持旧阶段恢复或多轮。' : buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcurrency, maxRetrievalRounds) + (enableMigrationContracts ? ' 迁移契约仅由批准快照的明确旧/新入口与调用绑定产生,无法解析的语言结构保持未解决,不以相似名称推断迁移。' : '') + (enableMigrationRegression ? ' 显式跨文件旧新比较仅在host契约限定来源和本次独立回执齐备时成立,因果仍未证明。' : '') + (enableGroupInteractions ? ' 组主审可附带交互结果,只有当前代全组编辑实读且host校验通过时替代匹配专门阶段;缺失/非法回退,候选仍独立复核。' : '') + (enableStageContinuation ? ' 未完成阶段可在其他阶段释放额度后有限继续,不重置总账或原超时;已验收主审生成可精确续用,但复核重新读取、新回执,不继承旧证据。' : '') + (enableEvidenceContext ? ' 可将本次实际交付过的批准原行按相关性经当前阶段reader有限重新投递;缓存仍计交付字节,不继承历史读取覆盖或复核回执。' : '') + (inlineInteractionRisks ? ' 预算适配的精确跨文件单元可在交互主审同次返回风险假设,省去独立规划;缺失或无效风险不补发、不代表已完成风险核查。' : '') + (optimizeReviewCalls ? ` 完整正文或覆盖全部精确编辑的单批窗口可将风险问题合并入主审(非独立规划);仅对批准上下文的确定坐标预取小片段,不代表完整核查。${focusPrimaryContext ? " 主审优先保留主文件全文,仅附带有关联的有界完整上下文;其余批准来源保留目录按需读,不表示不存在或已核查。" : ""}${enableSearchSeeds ? " 可将模型搜索命中的批准原行小片段与搜索结果同轮提供,每阶段最多2次额外读取,共用原预算;不猜测另侧坐标,不增加检索轮次。" : ""}共享检索上限按本次范围计算:${readerBudget.maxCalls}次唯一操作结果生成/${readerBudget.maxOutputBytes}交付字节,所有阶段共用、不逐文件重置;同快照相同请求可复用原始结果,每次交付仍计字节并重新签发复核回执;主审、综合及复核按权重预留最低额度,未用额度释放,额度不足仍如实标记未完成。` : ''),
|
|
174
|
+
notice: executionStrategy === 'whole-scope' ? '整体优先主审:批准完整双侧按实际输入字节合批;不执行逐文件风险规划、业务分组或旧专门综合。候选集中独立复核;补读可增加调用,共享读取账本不重置。超大文件明确阻断,多批跨边界未核查保持部分完成;不承诺一次请求或审查通过。不支持旧阶段恢复或多轮。' + (useHostChildren ? ` 超限多组使用真实spawn子智能体,最多${wholeScopeConcurrency}组并行,每组保留独立持续上下文,仅批准快照读取工具;逐模型生成尝试共用总账,逻辑LLM输入预算不代表最终HTTP字节。` : '') : buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcurrency, maxRetrievalRounds) + (enableMigrationContracts ? ' 迁移契约仅由批准快照的明确旧/新入口与调用绑定产生,无法解析的语言结构保持未解决,不以相似名称推断迁移。' : '') + (enableMigrationRegression ? ' 显式跨文件旧新比较仅在host契约限定来源和本次独立回执齐备时成立,因果仍未证明。' : '') + (enableGroupInteractions ? ' 组主审可附带交互结果,只有当前代全组编辑实读且host校验通过时替代匹配专门阶段;缺失/非法回退,候选仍独立复核。' : '') + (enableStageContinuation ? ' 未完成阶段可在其他阶段释放额度后有限继续,不重置总账或原超时;已验收主审生成可精确续用,但复核重新读取、新回执,不继承旧证据。' : '') + (enableEvidenceContext ? ' 可将本次实际交付过的批准原行按相关性经当前阶段reader有限重新投递;缓存仍计交付字节,不继承历史读取覆盖或复核回执。' : '') + (inlineInteractionRisks ? ' 预算适配的精确跨文件单元可在交互主审同次返回风险假设,省去独立规划;缺失或无效风险不补发、不代表已完成风险核查。' : '') + (optimizeReviewCalls ? ` 完整正文或覆盖全部精确编辑的单批窗口可将风险问题合并入主审(非独立规划);仅对批准上下文的确定坐标预取小片段,不代表完整核查。${focusPrimaryContext ? " 主审优先保留主文件全文,仅附带有关联的有界完整上下文;其余批准来源保留目录按需读,不表示不存在或已核查。" : ""}${enableSearchSeeds ? " 可将模型搜索命中的批准原行小片段与搜索结果同轮提供,每阶段最多2次额外读取,共用原预算;不猜测另侧坐标,不增加检索轮次。" : ""}共享检索上限按本次范围计算:${readerBudget.maxCalls}次唯一操作结果生成/${readerBudget.maxOutputBytes}交付字节,所有阶段共用、不逐文件重置;同快照相同请求可复用原始结果,每次交付仍计字节并重新签发复核回执;主审、综合及复核按权重预留最低额度,未用额度释放,额度不足仍如实标记未完成。` : ''),
|
|
152
175
|
};
|
|
153
176
|
} finally { busy = false; }
|
|
154
177
|
},
|
|
@@ -170,14 +193,18 @@ function createSessionReviewService(llm, { capture = captureSnapshot, executionS
|
|
|
170
193
|
const snapshot = await capture(target, { ...preview.options, signal: exec.signal });
|
|
171
194
|
exec.signal?.throwIfAborted();
|
|
172
195
|
if (snapshot.id !== preview.snapshotId || owner(exec).cwd !== current.cwd) throw new Error('Files changed since preview; preview again');
|
|
173
|
-
|
|
196
|
+
if (preview.useHostChildren && !hostChildren?.capability()) throw new Error('Approved child execution capability changed; preview again');
|
|
197
|
+
const resumeIdentity = resumeOptions(preview.options, preview.route, preview.rounds, preview.effort.mode, preview.instructions);
|
|
174
198
|
const resumeState = preview.resumeState ? validateResumeState(preview.resumeState, snapshot, resumeIdentity) : undefined;
|
|
175
199
|
if (await authorize({ snapshot, route: preview.route, exec }) !== true) throw new Error('Model sending approval denied or unavailable');
|
|
176
200
|
exec.signal?.throwIfAborted();
|
|
177
201
|
if (owner(exec).cwd !== current.cwd || JSON.stringify(owner(exec).route) !== JSON.stringify(preview.route)) throw new Error('Agent changed during approval');
|
|
178
202
|
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}))});
|
|
179
|
-
const report = await reviewSnapshot(snapshot, createDshModel(llm, preview.route,{correctJson:true,useNativeTools:nativeOutputTools}), { executionStrategy, maxInputBytes, signal: exec.signal, enableRetrieval: true, enableGrouping: true, enableGroupPrimary, enableEvidenceContext, inlineInteractionRisks, enableMigrationContracts, enableMigrationRegression, enableGroupInteractions, enableStageContinuation, 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 });
|
|
203
|
+
const report = await reviewSnapshot(preview.history.selection?.reviewSnapshot ?? snapshot, createDshModel(llm, preview.route,{correctJson:true,useNativeTools:nativeOutputTools}), { approvedRetrievalSnapshot: snapshot, instructions: preview.instructions, reviewEffort: preview.effort, enableCounterVerification: enableCounterVerification && preview.effort.counterVerification.eligible, executionStrategy, maxInputBytes, wholeScopeConcurrency, childExecution: preview.useHostChildren ? 'host-subagents' : undefined, createBatchModel: preview.useHostChildren ? args => hostChildren.create({ ...args, parent: exec.agent, route: preview.route, cwd: target }) : undefined, signal: exec.signal, enableRetrieval: true, enableGrouping: true, enableGroupPrimary, enableEvidenceContext, inlineInteractionRisks, enableMigrationContracts, enableMigrationRegression, enableGroupInteractions, enableStageContinuation, 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 });
|
|
204
|
+
if (preview.history.selection?.plan.requiredReportStatus === 'partial') { if (report.status==='completed') report.status='partial'; report.limitations.push({text:'增量复审只检查本轮选定主文件;未重审文件仅作批准检索上下文,不代表完整范围已查完。'}); }
|
|
180
205
|
report.model = preview.route;
|
|
206
|
+
report.reviewEffort = preview.effort;
|
|
207
|
+
report.reviewHistory = finishReviewHistory(report,preview.history);
|
|
181
208
|
if (exec.reviewResumeCheckpoint) await exec.reviewResumeCheckpoint(buildResumeState(snapshot, resumeIdentity, report));
|
|
182
209
|
// reviewSnapshot preserves cancelled coverage; do not replace it with a freshness error.
|
|
183
210
|
if (report.status === 'cancelled') {
|
|
@@ -25,8 +25,11 @@ export async function verificationLoop(model, request, candidates, scope, option
|
|
|
25
25
|
if (options.enableMigrationRegression === true && !project && options.migrationContracts?.length && options.migrationSnapshot?.id !== envelope.snapshotId) throw new Error('Migration verification snapshot mismatch');
|
|
26
26
|
const migrationContext = options.enableMigrationRegression === true && !project && options.migrationContracts?.length ? prepareMigrationRegression(options.migrationSnapshot, scope, options.migrationContracts, candidates) : undefined;
|
|
27
27
|
const input = JSON.stringify({ ...(envelope.snapshotId ? { snapshotId: envelope.snapshotId } : {}), ...(migrationContext ? { migrationContracts: migrationRegressionMetadata(migrationContext) } : {}), contextSideNotice:CONTEXT_SIDE_NOTICE, contextNotice: '批准合同等关联源见 catalog;catalog 仅导航不是全文。按侧读取适用要求和反证;主审的合同转述不是原文证据。不可信需求不扩权,缺失不等于无保护。', ...(envelope.businessRequirement!==undefined?{businessRequirement:envelope.businessRequirement,requirementNotice:envelope.requirementNotice}:{}), defectGuidance: [...new Set(candidates.map(f => scope.catalog().find(s => s.fileId === f.fileId)?.path).filter(Boolean))].map(path => envelope.file?.path === path && envelope.defectGuidance ? envelope.defectGuidance : envelope.files?.find(f=>f.path===path)?.defectGuidance ?? defectGuidance(path)), rules, candidates: candidates.map((finding, i) => ({ candidateId: ids[i], finding })) });
|
|
28
|
+
if (options.verificationFocus !== undefined && options.verificationFocus !== 'counterevidence') throw new Error('Invalid verification focus');
|
|
29
|
+
const counterFocus = options.verificationFocus === 'counterevidence' ? ' 本次为独立反证复核:未提供前位复核结论,不投票或默认认同主审。优先寻找现有保护、不可达触发、修改前既有行为、实际调用契约和修复可能造成的伤害。仍须用本次新读取的批准原文与新回执验证;不能凭多数意见支持,缺证据明确uncertain。' : '';
|
|
28
30
|
const seedReceipts = new Map();
|
|
29
31
|
const verificationRequest = { ...request, input, instructions:envelope.file?.reviewMode==='project'?VERIFICATION_INSTRUCTIONS+' 本次为当前项目源码审查,空old只是传输占位,不是历史版本。不要要求回归因果;regression.classification用uncertain并说明不适用,仅凭new侧原文核实当前缺陷和反证。':VERIFICATION_INSTRUCTIONS + (migrationContext ? ' ' + MIGRATION_VERIFICATION_INSTRUCTIONS : '') };
|
|
32
|
+
verificationRequest.instructions += counterFocus;
|
|
30
33
|
// Explicit final-only mode has extra finalization metadata/instructions and
|
|
31
34
|
// permits no retrieval rounds. Do not spend its budget on optional prefetch.
|
|
32
35
|
const initialRetrieved = options.maxRounds === 0 ? [] : verificationSeeds(candidates, scope, {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { REVIEW_INSTRUCTIONS } from './prompts.mjs';
|
|
2
|
+
import { runWholeScopeWorkers, createCheckpointQueue } from './whole-scope-workers.mjs';
|
|
3
|
+
import { changesConcurrency } from './review-execution-policy.mjs';
|
|
2
4
|
import { createReviewReader } from './review.mjs';
|
|
3
5
|
import { prepareWholeScope } from './whole-scope-plan.mjs';
|
|
4
6
|
import { retrievalLoop } from './retrieval-loop.mjs';
|
|
5
7
|
import { verificationLoop } from './verification-loop.mjs';
|
|
8
|
+
import { runCounterVerification } from './counter-verification.mjs';
|
|
6
9
|
import { validateGroupPrimary } from './group-primary.mjs';
|
|
7
10
|
import { modelWireBudget } from './model-wire-budget.mjs';
|
|
8
11
|
import { retrievalRounds } from './review-execution-policy.mjs';
|
|
@@ -21,6 +24,8 @@ export async function reviewWholeScope(snapshot, model, options = {}) {
|
|
|
21
24
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0) throw new Error('Invalid whole-scope timeout');
|
|
22
25
|
if (options.enableAnchorCorrection !== undefined && typeof options.enableAnchorCorrection !== 'boolean') throw new Error('Invalid anchor correction option');
|
|
23
26
|
const maxRounds = retrievalRounds(options.maxRetrievalRounds);
|
|
27
|
+
const concurrency = changesConcurrency(options.wholeScopeConcurrency ?? 2);
|
|
28
|
+
if (options.createBatchModel !== undefined && typeof options.createBatchModel !== 'function') throw new Error('Invalid child agent factory');
|
|
24
29
|
const input = structuredClone(snapshot);
|
|
25
30
|
if (!input || !['git', 'svn'].includes(input.vcs) || !Array.isArray(input.files) || input.files.length > 200 || input.projectScope || input.files.some(file => file.reviewMode === 'project')) throw new Error('Whole-scope review requires a changes snapshot');
|
|
26
31
|
const identities = new Set(), paths = new Set();
|
|
@@ -41,21 +46,23 @@ export async function reviewWholeScope(snapshot, model, options = {}) {
|
|
|
41
46
|
interactions: [], rules: (input.rules ?? []).map(({ path, hash }) => ({ path, hash })),
|
|
42
47
|
};
|
|
43
48
|
const stateById = new Map();
|
|
44
|
-
let reader, checkpointFailed = false;
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
let reader, checkpointFailed = false, fatalError;
|
|
50
|
+
const checkpoints = createCheckpointQueue(options.onResumeCheckpoint, error => { checkpointFailed = true; fatalError ??= error; controller.abort(error); });
|
|
51
|
+
const reportOrder = new Map(input.files.map((file, index) => [file.id, index]));
|
|
52
|
+
const checkpoint = () => {
|
|
53
|
+
report.findings.sort((a, b) => reportOrder.get(a.fileId) - reportOrder.get(b.fileId));
|
|
54
|
+
return checkpoints.enqueue(report);
|
|
48
55
|
};
|
|
49
56
|
const gate = () => {
|
|
50
57
|
signal.throwIfAborted();
|
|
51
58
|
if (report.modelCalls >= maxCalls) throw failure('WHOLE_SCOPE_MODEL_CALL_LIMIT', 'Model call budget exceeded');
|
|
52
59
|
if (options.remainingCalls?.() <= 0) throw failure('PROJECT_MODEL_CALL_LIMIT', 'Shared project model call limit exceeded');
|
|
53
60
|
};
|
|
54
|
-
const
|
|
61
|
+
const trackModel = delegate => async request => {
|
|
55
62
|
gate();
|
|
56
63
|
if (!modelWireBudget(request, maxInputBytes).fits) throw failure('WHOLE_SCOPE_INPUT_BUDGET', 'Actual model wire input budget exceeded');
|
|
57
64
|
report.modelCalls++;
|
|
58
|
-
const result = await
|
|
65
|
+
const result = await delegate({ ...request, maxInputBytes,
|
|
59
66
|
onModelArtifact: options.onModelArtifact ? async artifact => {
|
|
60
67
|
try {
|
|
61
68
|
const reference = await options.onModelArtifact({ ...artifact, snapshotId: input.id, phase: request.reviewPhase });
|
|
@@ -83,6 +90,7 @@ export async function reviewWholeScope(snapshot, model, options = {}) {
|
|
|
83
90
|
signal.throwIfAborted();
|
|
84
91
|
return result;
|
|
85
92
|
};
|
|
93
|
+
const trackedModel = trackModel(model);
|
|
86
94
|
const requestFor = batch => ({ input: batch.payload, instructions: batch.instructions, signal,
|
|
87
95
|
reviewPhase: 'group-primary', nativeOutputTools: options.nativeOutputTools === true,
|
|
88
96
|
onProtocolEvent: options.onProtocolEvent });
|
|
@@ -112,12 +120,27 @@ export async function reviewWholeScope(snapshot, model, options = {}) {
|
|
|
112
120
|
if (plan.batches.length > 1) report.limitations.push({ text: '整体范围按字节分为多个批次;跨 batch 边界关系未核查,不声明整体范围完整覆盖。' });
|
|
113
121
|
report.scopeNotices = [{ text: '整体主审结合当前 batch 完整文件;批准关联源须按需检索,不声明专门 interaction 阶段或全部上下文覆盖。' }];
|
|
114
122
|
const generatedBatches = [];
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
const parallelChildren = plan.batches.length > 1 && typeof options.createBatchModel === 'function';
|
|
124
|
+
report.orchestration.batchExecution = parallelChildren ? (options.childExecution ?? 'child-factory') : 'direct';
|
|
125
|
+
report.orchestration.batchConcurrency = parallelChildren ? Math.min(concurrency, plan.batches.length) : 1;
|
|
126
|
+
const accepted = new Array(plan.batches.length);
|
|
127
|
+
report.orchestration.batches = plan.batches.map(batch => ({ id: batch.id, fileIds: [...batch.fileIds], status: 'pending' }));
|
|
128
|
+
const runBatch = async (batch, index) => {
|
|
129
|
+
const stage = report.orchestration.batches[index];
|
|
130
|
+
let child;
|
|
118
131
|
try {
|
|
119
132
|
gate();
|
|
120
|
-
|
|
133
|
+
if (parallelChildren) {
|
|
134
|
+
child = await options.createBatchModel({ batchId: batch.id, fileIds: [...batch.fileIds], signal });
|
|
135
|
+
if (!child || (typeof child.invoke !== 'function' && typeof child.runPrimary !== 'function') || typeof child.dispose !== 'function') throw new Error('Invalid host child model');
|
|
136
|
+
signal.throwIfAborted();
|
|
137
|
+
}
|
|
138
|
+
const batchModel = child ? trackModel(request => child.invoke(request)) : trackedModel;
|
|
139
|
+
const response = child?.runPrimary ? await child.runPrimary({ request: requestFor(batch), scope: reader, maxRounds, maxInputBytes,
|
|
140
|
+
reserveModelCall: () => { gate(); report.modelCalls++; },
|
|
141
|
+
onRetrieved: loopOptions(stage).onRetrieved, onBudgetLimited: loopOptions(stage).onBudgetLimited, onTruncated: loopOptions(stage).onTruncated,
|
|
142
|
+
onProgress: options.onProgress }) : await retrievalLoop(batchModel, requestFor(batch), reader, loopOptions(stage));
|
|
143
|
+
if (child?.transport === 'host-subagent') { stage.childId = child.childId; stage.execution = 'host-subagent'; }
|
|
121
144
|
const members = validateGroupPrimary(response, batch, input, options.validateFindingResponse, reader.catalog(), { enableAnchorCorrection: options.enableAnchorCorrection === true, signal });
|
|
122
145
|
signal.throwIfAborted();
|
|
123
146
|
// Validate the entire member set before publishing any member result.
|
|
@@ -130,18 +153,23 @@ export async function reviewWholeScope(snapshot, model, options = {}) {
|
|
|
130
153
|
report.limitations.push(...generated.limitations.map(text => ({ fileId: id, text })));
|
|
131
154
|
}
|
|
132
155
|
stage.status = stage.retrievalLimited ? 'pending' : 'completed';
|
|
133
|
-
|
|
156
|
+
accepted[index] = batch;
|
|
134
157
|
await checkpoint();
|
|
135
158
|
} catch (error) {
|
|
136
159
|
signal.throwIfAborted();
|
|
137
|
-
if (checkpointFailed || error.code === 'MODEL_ARTIFACT_FAILED') throw error;
|
|
160
|
+
if (checkpointFailed || error.code === 'MODEL_ARTIFACT_FAILED') { fatalError ??= error; controller.abort(error); throw error; }
|
|
138
161
|
stage.status = isBudget(error) ? 'pending' : 'failed';
|
|
139
162
|
stage.failureCode = error.code ?? 'WHOLE_SCOPE_PRIMARY_FAILED'; stage.reason = error.message;
|
|
140
163
|
for (const id of batch.fileIds) Object.assign(stateById.get(id), { status: stage.status, primaryStatus: stage.status, failureCode: stage.failureCode, reason: error.message });
|
|
141
164
|
report.limitations.push({ text: '整体主审未完成:' + error.message });
|
|
142
|
-
|
|
165
|
+
} finally {
|
|
166
|
+
if (child) { try { await child.dispose(); } catch (error) { fatalError ??= error; controller.abort(error); throw error; } }
|
|
143
167
|
}
|
|
144
|
-
}
|
|
168
|
+
};
|
|
169
|
+
await runWholeScopeWorkers(plan.batches, runBatch, { concurrency: report.orchestration.batchConcurrency, signal, onFatal: error => { if (!signal.aborted) { fatalError ??= error; controller.abort(error); } } });
|
|
170
|
+
generatedBatches.push(...accepted.filter(Boolean));
|
|
171
|
+
const order = new Map(input.files.map((file, index) => [file.id, index]));
|
|
172
|
+
report.findings.sort((a, b) => order.get(a.fileId) - order.get(b.fileId));
|
|
145
173
|
// One global verification queue, not a per-file chain. Receipt ownership and
|
|
146
174
|
// primary-file regression checks remain entirely in verificationLoop.
|
|
147
175
|
for (const finding of report.findings) finding.verification = { status: 'pending', causality: 'unverified' };
|
|
@@ -151,10 +179,12 @@ export async function reviewWholeScope(snapshot, model, options = {}) {
|
|
|
151
179
|
(report.orchestration.verifications ??= []).push(stage);
|
|
152
180
|
try {
|
|
153
181
|
gate(); // Do not prefetch receipt evidence for a vetoed model call.
|
|
154
|
-
|
|
182
|
+
let verdicts = await verificationLoop(trackedModel, { ...requestFor(generatedBatches[0]), reviewPhase: 'verification' }, candidates, reader, loopOptions(stage));
|
|
155
183
|
signal.throwIfAborted();
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
verdicts = await runCounterVerification(trackedModel, { ...requestFor(generatedBatches[0]),reviewPhase:'verification' }, candidates, verdicts, reader, { ...loopOptions(stage),enabled:options.enableCounterVerification===true,isFatalError:error=>checkpointFailed||error?.code==='MODEL_ARTIFACT_FAILED' });
|
|
185
|
+
signal.throwIfAborted();
|
|
186
|
+
candidates.forEach((finding, i) => { finding.verification = { status: verdicts[i].counterVerification?.status === 'incomplete' ? 'incomplete' : 'completed', ...verdicts[i] }; });
|
|
187
|
+
stage.status = stage.retrievalLimited || verdicts.some(v => v.counterVerification?.status === 'incomplete') ? 'pending' : 'completed';
|
|
158
188
|
if (verdicts.some(v => v.verdict === 'uncertain' || v.verdict === 'supported' && !['introduced', 'preexisting'].includes(v.regression?.classification))) report.limitations.push({ text: '部分候选缺少充分复核证据或前后对照,保留不确定,不强行确认支持或本次引入。' });
|
|
159
189
|
} catch (error) {
|
|
160
190
|
signal.throwIfAborted();
|
|
@@ -181,9 +211,12 @@ export async function reviewWholeScope(snapshot, model, options = {}) {
|
|
|
181
211
|
await options.onProgress?.({ stage: 'batch-complete', snapshotId: input.id, files: report.files, findings: report.findings, limitations: report.limitations });
|
|
182
212
|
return report;
|
|
183
213
|
} catch (error) {
|
|
214
|
+
if (fatalError) throw fatalError;
|
|
184
215
|
if (!signal.aborted || checkpointFailed || error.code === 'MODEL_ARTIFACT_FAILED') throw error;
|
|
185
216
|
if (!report.files.length) report.files = input.files.map(file => ({ fileId: file.id, path: file.path, status: file.eligibility === 'reviewable' ? 'cancelled' : file.eligibility }));
|
|
186
217
|
for (const state of report.files) if (state.status === 'pending') state.status = 'cancelled';
|
|
218
|
+
const cancelledOrder = new Map(input.files.map((file, index) => [file.id, index]));
|
|
219
|
+
report.findings.sort((a, b) => cancelledOrder.get(a.fileId) - cancelledOrder.get(b.fileId));
|
|
187
220
|
for (const finding of report.findings) if (finding.verification?.status !== 'completed') finding.verification = { status: 'incomplete', causality: 'unverified' };
|
|
188
221
|
report.status = 'cancelled';
|
|
189
222
|
report.limitations.push({ text: '整体审查已取消或超时;已验收成果保留,其他范围未完成。' });
|
|
@@ -193,6 +226,6 @@ export async function reviewWholeScope(snapshot, model, options = {}) {
|
|
|
193
226
|
await checkpoint();
|
|
194
227
|
return report;
|
|
195
228
|
} finally {
|
|
196
|
-
clearTimeout(timer);
|
|
229
|
+
try { await checkpoints.drain(); } catch (error) { throw fatalError ?? error; } finally { clearTimeout(timer); }
|
|
197
230
|
}
|
|
198
231
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Run a bounded set of workers, always joining admitted work before rejecting.
|
|
2
|
+
* Cancellation rejects with signal.reason after the join; a worker failure takes
|
|
3
|
+
* precedence, including when onFatal synchronously aborts the outer controller.
|
|
4
|
+
*/
|
|
5
|
+
export async function runWholeScopeWorkers(items, run, { concurrency = 2, signal, onFatal } = {}) {
|
|
6
|
+
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 4) {
|
|
7
|
+
throw new RangeError('concurrency must be an integer from 1 to 4');
|
|
8
|
+
}
|
|
9
|
+
const results = new Array(items.length);
|
|
10
|
+
let next = 0, failed = false, failure;
|
|
11
|
+
async function worker() {
|
|
12
|
+
while (!failed && !signal?.aborted && next < items.length) {
|
|
13
|
+
const index = next++;
|
|
14
|
+
try {
|
|
15
|
+
results[index] = await run(items[index], index);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (!failed) {
|
|
18
|
+
failed = true;
|
|
19
|
+
failure = error;
|
|
20
|
+
// Invoke before yielding so outer abort reaches other active work.
|
|
21
|
+
// A throwing notification cannot replace the original worker failure.
|
|
22
|
+
onFatal?.(error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
await Promise.allSettled(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
28
|
+
if (failed) throw failure;
|
|
29
|
+
if (signal?.aborted) throw signal.reason;
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Serialize immutable checkpoint values. The first failure is permanent.
|
|
34
|
+
* Internal rejection handlers prevent fire-and-forget enqueue calls from causing
|
|
35
|
+
* unhandled rejections; callers still receive the original rejecting promise.
|
|
36
|
+
*/
|
|
37
|
+
export function createCheckpointQueue(save, onFatal) {
|
|
38
|
+
let tail = Promise.resolve(), failed = false, failure;
|
|
39
|
+
function rejectFirst(error) {
|
|
40
|
+
if (!failed) {
|
|
41
|
+
failed = true;
|
|
42
|
+
failure = error;
|
|
43
|
+
onFatal?.(error);
|
|
44
|
+
}
|
|
45
|
+
throw failure;
|
|
46
|
+
}
|
|
47
|
+
function observe(promise) {
|
|
48
|
+
// Normalize notification exceptions back to the first persistence failure.
|
|
49
|
+
tail = promise.catch(error => {
|
|
50
|
+
if (failed) throw failure;
|
|
51
|
+
return rejectFirst(error);
|
|
52
|
+
});
|
|
53
|
+
tail.catch(() => {});
|
|
54
|
+
return tail;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
enqueue(value) {
|
|
58
|
+
if (save == null) return Promise.resolve();
|
|
59
|
+
if (failed) return observe(Promise.reject(failure));
|
|
60
|
+
let checkpoint;
|
|
61
|
+
try {
|
|
62
|
+
checkpoint = structuredClone(value);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Retain FIFO even if cloning an enqueued value fails.
|
|
65
|
+
return observe(tail.then(() => rejectFirst(error)));
|
|
66
|
+
}
|
|
67
|
+
return observe(tail.then(async () => {
|
|
68
|
+
if (failed) throw failure;
|
|
69
|
+
try {
|
|
70
|
+
await save(checkpoint);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return rejectFirst(error);
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
},
|
|
76
|
+
async drain() {
|
|
77
|
+
// Also join values enqueued while a save is in progress.
|
|
78
|
+
for (;;) {
|
|
79
|
+
const pending = tail;
|
|
80
|
+
await pending.catch(() => {});
|
|
81
|
+
if (pending === tail) {
|
|
82
|
+
if (failed) throw failure;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|