@feng0531/dsh-code-review 0.85.11 → 0.86.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/src/service.mjs CHANGED
@@ -1,4 +1,6 @@
1
+ import { configureWireBudget } from './model-wire-budget.mjs';
1
2
  import {REVIEW_RUNTIME} from './runtime-identity.mjs';
3
+ import { buildResumeState, validateResumeState, resumePreview } from './review-resume.mjs';
2
4
  import { reviewDiagnostics } from './review-diagnostics.mjs';
3
5
  import {createProjectReviewService} from './project-review-service.mjs';
4
6
  import {svnDiagnostic} from './svn-diagnostics.mjs';
@@ -53,15 +55,16 @@ function buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcur
53
55
  return scopeNotice + (snapshot.history?.vcs==='svn'&&options.autoContext===true?'SVN历史autoContext为精确依赖定位读取固定版本父目录有界清单,包含同目录非匹配名字元数据;仅匹配捕获正文发送模型,不扫描全仓,不用工作树替代历史版本。':'') + (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + `执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多${maxRetrievalRounds}轮检索;独立文件审查链最多${changesConcurrency}条并发,不设插件内部默认时间或总模型调用次数截止,遵循宿主/用户取消;输入字节和检索轮次保护仍保留;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。`;
54
56
  }
55
57
 
56
- function createSessionReviewService(llm, { capture = captureSnapshot, now = Date.now, ttlMs = 300000, maxPreviews = 8, allowModelSending = false, enableRiskPlanning = false, skipSmallRiskPlans = true, reviewRounds = 1, enableBusinessGrouping = false, enableAnchorCorrection = false, changesConcurrency = 2, maxRetrievalRounds = 3, riskPlanningMode = 'separate', readerBudgetPolicy = 'fixed', optimizeReviewCalls = false, enablePrimarySeeds = false, authorize = async () => false } = {}) {
58
+ function createSessionReviewService(llm, { capture = captureSnapshot, now = Date.now, ttlMs = 300000, maxPreviews = 8, allowModelSending = false, enableRiskPlanning = false, skipSmallRiskPlans = true, reviewRounds = 1, enableBusinessGrouping = false, enableAnchorCorrection = false, changesConcurrency = 2, maxRetrievalRounds = 3, riskPlanningMode = 'separate', readerBudgetPolicy = 'fixed', optimizeReviewCalls = false, enablePrimarySeeds = false, focusPrimaryContext = false, enableSearchSeeds = false, nativeOutputTools = false, enableGroupPrimary = false, authorize = async () => false } = {}) {
57
59
  for (const value of [ttlMs, maxPreviews]) if (!Number.isSafeInteger(value) || value < 1) throw new Error('Invalid preview limit');
58
60
  changesConcurrency = reviewConcurrency(changesConcurrency);
59
61
  maxRetrievalRounds = reviewRetrievalRounds(maxRetrievalRounds);
60
62
  riskPlanningMode = validateRiskPlanningMode(riskPlanningMode);
61
63
  reviewReaderBudget({ files: [] }, readerBudgetPolicy);
62
- for (const value of [optimizeReviewCalls, enablePrimarySeeds]) if (typeof value !== 'boolean') throw new Error('Invalid review optimization option');
64
+ for (const value of [optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds, nativeOutputTools, enableGroupPrimary]) if (typeof value !== 'boolean') throw new Error('Invalid review optimization option');
63
65
  const lifetime = new AbortController();
64
66
  const bind = exec => ({ ...exec, signal: exec.signal ? AbortSignal.any([exec.signal, lifetime.signal]) : lifetime.signal });
67
+ const resumeOptions = (options, route, rounds) => ({ captureOptions: options, route, runtime: REVIEW_RUNTIME, instructionsHash: hash(REVIEW_INSTRUCTIONS), execution: { rounds, enableRiskPlanning, skipSmallRiskPlans, enableBusinessGrouping, enableAnchorCorrection, changesConcurrency, maxRetrievalRounds, riskPlanningMode, readerBudgetPolicy, optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds, nativeOutputTools, enableGroupPrimary } });
65
68
  const previews = new Map();
66
69
  let busy = false;
67
70
  function prune() { for (const [id, p] of previews) if (p.expires <= now()) previews.delete(id); }
@@ -70,7 +73,8 @@ function createSessionReviewService(llm, { capture = captureSnapshot, now = Date
70
73
  exec = bind(exec);
71
74
  exec.signal.throwIfAborted();
72
75
  if (busy) throw new Error('Review service busy');
73
- if (!args || Object.keys(args).some(k => !['repositoryPath', 'selectedPaths', 'contextPaths', 'rulePaths', 'autoContext', 'commit', 'baseRevision', 'targetRevision','businessRequirement','callerScopePaths','oldContextPaths','reviewRounds','svnRevision','svnBaseRevision','svnTargetRevision','svnAllowNetwork','svnEffectiveMergeinfo','svnBasePath','svnTargetPath','svnReplacementBaseRevision','svnReplacementAllowNetwork','changeScope'].includes(k))) throw new Error('Unknown preview argument');
76
+ if (!args || Object.keys(args).some(k => !['repositoryPath', 'selectedPaths', 'contextPaths', 'rulePaths', 'autoContext', 'commit', 'baseRevision', 'targetRevision','businessRequirement','callerScopePaths','oldContextPaths','reviewRounds','svnRevision','svnBaseRevision','svnTargetRevision','svnAllowNetwork','svnEffectiveMergeinfo','svnBasePath','svnTargetPath','svnReplacementBaseRevision','svnReplacementAllowNetwork','changeScope','resumeFromJobId'].includes(k))) throw new Error('Unknown preview argument');
77
+ if (args.resumeFromJobId !== undefined && (typeof args.resumeFromJobId !== 'string' || !/^[a-f0-9-]{36}$/.test(args.resumeFromJobId))) throw new Error('Invalid resume job id');
74
78
  if (args.autoContext !== undefined && typeof args.autoContext !== 'boolean') throw new Error('Invalid autoContext option');
75
79
  for (const key of ['selectedPaths', 'contextPaths', 'rulePaths', 'oldContextPaths']) if (args[key] !== undefined && (!Array.isArray(args[key]) || args[key].length > (key === 'rulePaths' ? 4 : key === 'oldContextPaths' ? 20 : 200) || args[key].some(p => typeof p !== 'string'))) throw new Error('Invalid path list');
76
80
  const rounds=validateReviewRounds(args.reviewRounds === undefined ? reviewRounds : args.reviewRounds);
@@ -80,24 +84,34 @@ function createSessionReviewService(llm, { capture = captureSnapshot, now = Date
80
84
  busy = true;
81
85
  try {
82
86
  const options = structuredClone(args);
87
+ const sourceJobId = options.resumeFromJobId;
88
+ delete options.resumeFromJobId;
89
+ let savedResume;
90
+ if (sourceJobId !== undefined) {
91
+ if (typeof exec.resolveReviewResume !== 'function') throw new Error('Durable job resume is unavailable; use a new background preview');
92
+ savedResume = await exec.resolveReviewResume(sourceJobId);
93
+ }
83
94
  const target = await resolveWorkspaceRepository(current.cwd, options.repositoryPath);
84
95
  const snapshot = await capture(target, { ...options, signal: exec.signal });
85
96
  exec.signal?.throwIfAborted();
86
97
  if (owner(exec).cwd !== current.cwd) throw new Error('Agent directory changed; preview again');
98
+ const resumeState = savedResume === undefined ? undefined : validateResumeState(savedResume, snapshot, resumeOptions(options, current.route, rounds));
87
99
  const readerBudget = reviewReaderBudget(snapshot, readerBudgetPolicy);
88
- const plan = buildCoveragePlan(snapshot, { riskPlanningMode, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:rounds, enableBusinessGrouping, instructions: REVIEW_INSTRUCTIONS, scope: createRetrievalScope(snapshot, { ...readerBudget, signal: exec.signal }), grouping: buildReviewGroups(snapshot.files, inferFileRelations(snapshot.files)), maxInputBytes: 96 * 1024, signal: exec.signal });
100
+ const plan = buildCoveragePlan(snapshot, { enableGroupPrimary, resumeState, focusPrimaryContext, riskPlanningMode, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:rounds, enableBusinessGrouping, instructions: REVIEW_INSTRUCTIONS, scope: configureWireBudget(createRetrievalScope(snapshot, { ...readerBudget, signal: exec.signal }), { nativeOutputTools }), grouping: buildReviewGroups(snapshot.files, inferFileRelations(snapshot.files)), maxInputBytes: 96 * 1024, signal: exec.signal });
89
101
  prune();
90
102
  while (previews.size >= maxPreviews) previews.delete(previews.keys().next().value);
91
103
  const previewId = randomUUID();
92
- previews.set(previewId, { ...current, target, options, rounds, snapshotId: snapshot.id, expires: now() + ttlMs });
104
+ previews.set(previewId, { ...current, target, options, rounds, snapshotId: snapshot.id, resumeState, sourceJobId, expires: now() + ttlMs });
93
105
  return {
94
106
  ...(options.businessRequirement !== undefined
95
107
  ? { businessRequirement: { text: options.businessRequirement, hash: hash(options.businessRequirement), bytes: Buffer.byteLength(options.businessRequirement) } }
96
108
  : {}),
97
109
  previewId,
110
+ ...(resumeState ? { resume: resumePreview(resumeState, sourceJobId) } : {}),
98
111
  plan,
99
112
  snapshotId: snapshot.id,
100
- execution: { fileConcurrency: changesConcurrency, maxRetrievalRounds, ...(optimizeReviewCalls ? { riskPlanningMode, readerBudget: { policy: readerBudgetPolicy, ...readerBudget }, missingContextSeeds: enablePrimarySeeds } : {}) },
113
+ privateArtifacts: { enabled: typeof exec.onModelArtifact === 'function', sourceBearing: true, maxArtifactBytes: 1048576, maxJobBytes: 8388608, maxArtifacts: 64, notice: 'Background jobs retain bounded untrusted model output outside the reviewed workspace; omissions/truncation explicit. Never included in ordinary status or reused as evidence; explicit consent required to read.' },
114
+ execution: { nativeOutputTools, enableGroupPrimary, fileConcurrency: changesConcurrency, maxRetrievalRounds, ...(optimizeReviewCalls ? { riskPlanningMode, readerBudget: { policy: readerBudgetPolicy, ...readerBudget }, missingContextSeeds: enablePrimarySeeds, focusPrimaryContext, searchContextSeeds: enableSearchSeeds } : {}) },
101
115
  repositoryRoot: snapshot.root,
102
116
  vcs: snapshot.vcs,
103
117
  ...(snapshot.changeScope ? { changeScope: snapshot.changeScope } : options.changeScope !== undefined ? { changeScope: options.changeScope } : {}),
@@ -127,7 +141,7 @@ function createSessionReviewService(llm, { capture = captureSnapshot, now = Date
127
141
  ...(snapshot.callerDiscovery ? { callerDiscovery: snapshot.callerDiscovery } : {}),
128
142
  ...(snapshot.autoContext ? { autoContext: snapshot.autoContext } : {}),
129
143
  rules: (snapshot.rules ?? []).map(({ path, hash }) => ({ path, hash })),
130
- notice: buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcurrency, maxRetrievalRounds) + (optimizeReviewCalls ? ` 完整单批文件可将风险问题合并入主审(非独立规划);仅对批准上下文的确定坐标预取小片段,不代表完整核查。共享检索上限按本次范围计算:${readerBudget.maxCalls}次操作/${readerBudget.maxOutputBytes}字节,所有阶段共用、不逐文件重置;无法取得必要证据的阶段不再无效启动。` : ''),
144
+ notice: buildPreviewNotice(snapshot, options, enableRiskPlanning, changesConcurrency, maxRetrievalRounds) + (optimizeReviewCalls ? ` 完整正文或覆盖全部精确编辑的单批窗口可将风险问题合并入主审(非独立规划);仅对批准上下文的确定坐标预取小片段,不代表完整核查。${focusPrimaryContext ? " 主审优先保留主文件全文,仅附带有关联的有界完整上下文;其余批准来源保留目录按需读,不表示不存在或已核查。" : ""}${enableSearchSeeds ? " 可将模型搜索命中的批准原行小片段与搜索结果同轮提供,每阶段最多2次额外读取,共用原预算;不猜测另侧坐标,不增加检索轮次。" : ""}共享检索上限按本次范围计算:${readerBudget.maxCalls}次操作/${readerBudget.maxOutputBytes}字节,所有阶段共用、不逐文件重置;无法取得必要证据的阶段不再无效启动。` : ''),
131
145
  };
132
146
  } finally { busy = false; }
133
147
  },
@@ -148,12 +162,15 @@ function createSessionReviewService(llm, { capture = captureSnapshot, now = Date
148
162
  const snapshot = await capture(target, { ...preview.options, signal: exec.signal });
149
163
  exec.signal?.throwIfAborted();
150
164
  if (snapshot.id !== preview.snapshotId || owner(exec).cwd !== current.cwd) throw new Error('Files changed since preview; preview again');
165
+ const resumeIdentity = resumeOptions(preview.options, preview.route, preview.rounds);
166
+ const resumeState = preview.resumeState ? validateResumeState(preview.resumeState, snapshot, resumeIdentity) : undefined;
151
167
  if (await authorize({ snapshot, route: preview.route, exec }) !== true) throw new Error('Model sending approval denied or unavailable');
152
168
  exec.signal?.throwIfAborted();
153
169
  if (owner(exec).cwd !== current.cwd || JSON.stringify(owner(exec).route) !== JSON.stringify(preview.route)) throw new Error('Agent changed during approval');
154
170
  await exec.reviewCheckpoint?.({stage:'approved-manifest',snapshotId:snapshot.id,files:snapshot.files.map(f=>({fileId:f.id,path:f.path,status:f.eligibility==='reviewable'?'pending':f.eligibility}))});
155
- const report = await reviewSnapshot(snapshot, createDshModel(llm, preview.route,{correctJson:true}), { signal: exec.signal, enableRetrieval: true, enableGrouping: true, enableVerification: true, riskPlanningMode, readerBudgetPolicy, optimizeReviewCalls, enablePrimarySeeds, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:preview.rounds, enableBusinessGrouping, enableAnchorCorrection,changesConcurrency,maxRetrievalRounds,correctProtocol:true, onProgress:exec.reviewCheckpoint });
171
+ const report = await reviewSnapshot(snapshot, createDshModel(llm, preview.route,{correctJson:true,useNativeTools:nativeOutputTools}), { signal: exec.signal, enableRetrieval: true, enableGrouping: true, enableGroupPrimary, nativeOutputTools, enableVerification: true, riskPlanningMode, readerBudgetPolicy, optimizeReviewCalls, enablePrimarySeeds, focusPrimaryContext, enableSearchSeeds, enableRiskPlanning, skipSmallRiskPlans, reviewRounds:preview.rounds, enableBusinessGrouping, enableAnchorCorrection,changesConcurrency,maxRetrievalRounds,correctProtocol:true, onProgress:exec.reviewCheckpoint, resumeState, onModelArtifact: exec.onModelArtifact, onResumeCheckpoint: exec.reviewResumeCheckpoint ? async checkpoint => { await exec.reviewResumeCheckpoint(buildResumeState(snapshot, resumeIdentity, checkpoint)); } : undefined });
156
172
  report.model = preview.route;
173
+ if (exec.reviewResumeCheckpoint) await exec.reviewResumeCheckpoint(buildResumeState(snapshot, resumeIdentity, report));
157
174
  // reviewSnapshot preserves cancelled coverage; do not replace it with a freshness error.
158
175
  if (report.status === 'cancelled') {
159
176
  report.outdated = true;
@@ -1,6 +1,7 @@
1
1
  import { VERIFICATION_INSTRUCTIONS } from './prompts.mjs';
2
2
  import { CONTEXT_SIDE_NOTICE } from './context-identity.mjs';
3
3
  import { defectGuidance } from './defect-guidance.mjs';
4
+ import { modelWireBudget } from './model-wire-budget.mjs';
4
5
  import { createEvidenceReceipts } from './evidence-receipts.mjs';
5
6
  import { retrievalLoop, retrievalRequest } from './retrieval-loop.mjs';
6
7
  import { verificationSeeds } from './verification-seeds.mjs';
@@ -28,7 +29,7 @@ export async function verificationLoop(model, request, candidates, scope, option
28
29
  onReceipt: (id, resolve) => seedReceipts.set(id, resolve),
29
30
  fits: seeds => {
30
31
  const next = retrievalRequest(verificationRequest, scope, seeds);
31
- return Buffer.byteLength(next.input) + Buffer.byteLength(next.instructions) <= (options.maxInputBytes ?? 96 * 1024);
32
+ return request.nativeOutputTools === true ? modelWireBudget(next, options.maxInputBytes ?? 96 * 1024).fits : Buffer.byteLength(next.input) + Buffer.byteLength(next.instructions) <= (options.maxInputBytes ?? 96 * 1024);
32
33
  },
33
34
  });
34
35
  const receipts = createEvidenceReceipts(scope.read, { limit: 50 - seedReceipts.size });