@feng0531/dsh-code-review 0.87.6 → 0.87.7

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
4
4
 
5
+ ## 0.87.7 — 清晰结论与有界复核纠正
6
+
7
+ - 报告区分已复核不确定与复核未完成;尚未成立的候选使用待证条件、影响假说与待验证方案,不再以确定影响和必改建议呈现。
8
+ - 开启协议纠正时,不完整复核批次可在原调用额度内纠正一次,复用本次已读原文及回执,不重启取证;完整响应仍须全部严格校验。最终答复模式不追加纠正。
9
+ - 保留原预算、截止时间和来源授权,记录纠正成本;本版不宣称真实审核提速,组取证续跑涉及分配安全边界,未贸然上线。
10
+
5
11
  ## 0.87.6 — 迁移复核成套取证
6
12
 
7
13
  - 复用宿主已校验的迁移契约,为匹配候选有界预取旧入口、旧委托实现和新入口;同套证据原子接纳,容量、预算或输入不满足时回退普通检索,减少模型重新寻找已知路径的往返。
package/RELEASE-NOTES.md CHANGED
@@ -1,17 +1,17 @@
1
- # v0.87.6 发布说明
1
+ # v0.87.7 发布说明
2
2
 
3
- ## 让已知迁移路径更早成为本次复核证据
3
+ ## 先看清楚结论,再减少复核返工
4
4
 
5
- 复核已能接收宿主校验的迁移契约,但此前默认种子只覆盖候选锚点附近,旧公开入口、旧委托实现与新入口仍需模型自行检索。本版为匹配候选增加有界成套取证,减少寻找已有导航坐标的往返;完整角色无法容纳时回退普通检索,不宣称已完成比较。
5
+ 报告区分复核支持、已复核不确定、复核未完成与已反驳。候选未成立时,以待证条件、影响假说与待验证方案表达,避免把主审描述误读为独立确认的缺陷;完整证据仍保留在技术附录。
6
6
 
7
- 种子沿用同一批准快照和读取总账;保留最多 8 次读取、16 KiB 种子输入、每次最多 200 行及最终证据复验余量。同套读取采用事务,失败或取消不发布部分回执。每次复核重新签发回执,不能借用既往主审/复核证据身份。
7
+ ## 不完整批次的有界纠正
8
8
 
9
- ## 诊断与安全边界
9
+ 启用协议纠正时,复核返回不完整批次可再请求一次完整判定。使用同次调用最后已发送的证据与新鲜回执,不重启检索、不重新签发整套证据;仍计入相同模型调用额度、输入限制和截止时间。仅接纳少项且已有成员均为合法身份、无证据无归因的 uncertain 批次进行纠正;含支持、回执或归因的部分响应仍直接失败,避免漏项掩盖非法证据。身份、回执、原文及修改前后归因继续严格校验;再次不完整即失败。检索最终答复模式不追加纠正。
10
10
 
11
- 执行诊断记录迁移种子接纳/回退计数,不含源码正文;预取接纳不证明模型已调用,更不证明复核支持或新增因果成立。普通比较仍要求正确主文件与来源侧;跨文件仅限已校验契约,删除导出路由仍仅允许不确定归因。
11
+ 执行诊断单列复核批次纠正的调用与原因,不将纠正当成缺陷成立。正常完整批次不增加调用;纠正可能增加一次模型往返,不承诺每次审核更快。
12
12
 
13
- 不增加模型调用额度、检索轮数、读取总额或并发;不降低复核证据门槛。未来同任务对照同时检查端到端与分段耗时、触发链、最小修复建议、覆盖及可读性,不能以减少调用或缩短报告代替质量提升。本版没有重新执行 r7364 真实模型审核,不承诺固定提速或确认两项既往候选。
13
+ ## 验证与边界
14
14
 
15
- ## 验证与升级
15
+ 本版采用离线正反回归、全量测试、真实宿主离线兼容检查及冻结标签四矩阵门禁。没有重新运行真实业务审核,也没有将直接审核的历史结论当作真值。分组取证续跑需要可回滚的额度转移,尚未改动该失败回退语义。
16
16
 
17
- 新增迁移成套种子、事务回退、新鲜回执及预算边界回归,并通过全量测试与冻结标签四矩阵 CI 后发布。发布不自动安装或重启当前宿主。安装见 [README](README.md),门禁见 [开发说明](DEVELOPMENT.md)。
17
+ 发布不自动安装或重启当前宿主。安装见 [README](README.md),门禁见 [开发说明](DEVELOPMENT.md)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.87.6",
3
+ "version": "0.87.7",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -1,9 +1,9 @@
1
1
  import { validateRegressionAssessment } from './regression-assessment.mjs';
2
2
  import { validateEvidenceReference } from './evidence-reference.mjs';
3
3
  // Offline only. Candidate ids come from the host, not the model.
4
- export function validateCandidateVerification(value, candidateIds, read, resolveEvidence = value => value, comparison = {}) {
4
+ export function validateCandidateVerificationEnvelope(value, candidateIds) {
5
5
  if (!Array.isArray(candidateIds) || candidateIds.length > 50 || candidateIds.some(id => typeof id !== 'string' || !id || id.length > 200) || new Set(candidateIds).size !== candidateIds.length) throw new Error('Invalid candidate identities');
6
- if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== 1 || !Array.isArray(value.verdicts) || value.verdicts.length !== candidateIds.length) throw new Error('Incomplete verification response');
6
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== 1 || !Array.isArray(value.verdicts) || value.verdicts.length !== candidateIds.length) throw Object.assign(new Error('Incomplete verification response'), { code: 'MODEL_INCOMPLETE_VERIFICATION' });
7
7
  const seen = new Set();
8
8
  // Validate the entire envelope before any budgeted evidence read.
9
9
  for (const item of value.verdicts) {
@@ -13,6 +13,10 @@ export function validateCandidateVerification(value, candidateIds, read, resolve
13
13
  if (!['supported', 'refuted', 'uncertain'].includes(item.verdict) || typeof item.reason !== 'string' || !item.reason.trim() || item.reason.length > 8000) throw new Error('Invalid verdict explanation');
14
14
  if (!Array.isArray(item.evidence) || item.evidence.length > 8 || (item.verdict !== 'uncertain' && !item.evidence.length)) throw new Error('Missing or excessive evidence');
15
15
  }
16
+ }
17
+
18
+ export function validateCandidateVerification(value, candidateIds, read, resolveEvidence = value => value, comparison = {}) {
19
+ validateCandidateVerificationEnvelope(value, candidateIds);
16
20
  const results = new Map(value.verdicts.map(item => {
17
21
  const evidence = item.evidence.map(ref => validateEvidenceReference(resolveEvidence(ref), read));
18
22
  const regression = validateRegressionAssessment(item.regression, evidence, comparison.catalog ?? [], comparison.fileIds?.[item.candidateId], { ...comparison, candidateId: item.candidateId });
@@ -1,5 +1,5 @@
1
1
  import { groupDuplicateFindings } from './finding-groups.mjs';
2
- import { comparisonValidated } from './report-summary.mjs';
2
+ import { comparisonValidated, verificationPresentation } from './report-summary.mjs';
3
3
 
4
4
  const safe = value => Array.from(String(value ?? '')).map(c => {
5
5
  if (c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127) return ' ';
@@ -30,23 +30,32 @@ export function findingsMarkdown(report) {
30
30
  const findings = Array.isArray(report.findings) ? report.findings : [];
31
31
  const duplicate = new Map();
32
32
  for (const group of groupDuplicateFindings(findings)) for (const i of group.findingIndices.slice(1)) duplicate.set(i, group.findingIndices[0]);
33
- const sections = new Map([['本次新增(复核支持)',[]],['当前源码问题(复核支持)',[]],['待核实',[]],['原有问题(复核支持)',[]],['已反驳',[]]]);
34
- findings.forEach((f,i)=>{ if (!duplicate.has(i)) sections.get(findingDisposition(f,report)).push({f,i}); });
33
+ const sections = new Map([['本次新增(复核支持)',[]],['当前源码问题(复核支持)',[]],['复核后不确定',[]],['复核未完成',[]],['原有问题(复核支持)',[]],['已反驳',[]]]);
34
+ findings.forEach((f,i)=>{
35
+ const state = verificationPresentation(f, report);
36
+ const label = state === 'uncertain' ? '复核后不确定' : state === 'incomplete' ? '复核未完成' : findingDisposition(f,report);
37
+ if (!duplicate.has(i)) sections.get(label).push({f,i});
38
+ });
35
39
  const output = ['', '## 审查发现'];
36
40
  if (!findings.length) return [...output, '本轮没有形成问题记录;这不等于已经排除未完成范围内的问题。'];
37
- output.push('以下区分复核支持、待核实和已反驳的记录;证据引用通过校验不等于运行复现或因果证明。');
41
+ output.push('以下分开列示复核支持、复核后不确定、复核未完成和已反驳的记录;证据引用通过校验不等于运行复现或因果证明。');
38
42
  for (const [label, items] of sections) {
39
43
  if (!items.length) continue;
40
44
  output.push('', '### ' + label);
41
45
  for (const {f,i} of items) {
42
46
  const severity = ({critical:'严重',high:'高',medium:'中',low:'低'})[f.severity] ?? safe(f.severity ?? '未分级');
43
- output.push('', '#### ' + (i+1) + '. ' + safe(f.title) + '(严重程度:' + severity + ')', '- 位置:' + location(f));
44
- for (const [key,title] of [['trigger','什么情况下发生'],['impact','可能造成的影响'],['evidence','已看到的依据']]) if (f[key]) output.push('- ' + title + ':' + safe(f[key]));
45
- if (label === '待核实') output.push('- 尚未确定:' + safe(f.verification?.reason ?? '候选尚未完成有效的独立复核或修改前后对照,不能作为已确认缺陷。'));
47
+ const hypothesis = label === '复核后不确定' || label === '复核未完成' || label === '已反驳';
48
+ output.push('', '#### ' + (i+1) + '. ' + (hypothesis ? '主审假说:' : '') + safe(f.title) + '(' + (hypothesis ? '主审拟议严重程度' : '严重程度') + ':' + severity + ')', '- 位置:' + location(f));
49
+ if (hypothesis) output.push('- 阅读方式:以下为主审记录的线索和假说,不是确定事实或必改建议;原始字段及完整证据保留在技术附录。');
50
+ const fields = hypothesis ? [['evidence','已知差异/线索(主审记录,非缺陷证明)'],['trigger','待证触发条件'],['impact','影响假说']] : [['trigger','什么情况下发生'],['impact','可能造成的影响'],['evidence','已看到的依据']];
51
+ for (const [key,title] of fields) if (f[key]) output.push('- ' + title + ':' + safe(f[key]));
52
+ if (label === '复核后不确定') output.push('- 复核结论:本候选已完成复核记录,但缺陷或新增归因仍未确定。' + safe(f.verification?.reason ?? '现有证据不足以支持或排除该假说。'));
53
+ if (label === '复核未完成') output.push('- 复核进度:尚未完成有效复核,不能当作已复核不确定或已确认缺陷。' + safe(f.verification?.reason ?? '尚无有效复核结论。'));
46
54
  if (label === '已反驳') output.push('- 反证结论:' + safe(f.verification?.reason ?? '复核反驳该候选;不计为待修缺陷。'));
47
- if (f.attribution?.beforeBehavior) output.push('- 修改前:' + safe(f.attribution.beforeBehavior));
48
- if (f.attribution?.afterBehavior) output.push('- 修改后:' + safe(f.attribution.afterBehavior));
49
- if (f.suggestion) output.push('- 建议:' + safe(f.suggestion));
55
+ if (f.attribution?.beforeBehavior) output.push('- 修改前' + (hypothesis ? '(主审描述,待核实)' : '') + ':' + safe(f.attribution.beforeBehavior));
56
+ if (f.attribution?.afterBehavior) output.push('- 修改后' + (hypothesis ? '(主审描述,待核实)' : '') + ':' + safe(f.attribution.afterBehavior));
57
+ if (hypothesis) output.push('- 待验证方案:先核对触发条件、实际调用路径及修改前后行为,再决定是否需要修复。' + (f.suggestion ? '原主审建议(待验证,不是必改项):' + safe(f.suggestion) : '原记录未提供建议。'));
58
+ else if (f.suggestion) output.push('- 建议:' + safe(f.suggestion));
50
59
  const related = [...duplicate].filter(([,first])=>first===i).map(([index])=>location(findings[index]));
51
60
  if (related.length) output.push('- 相同证据与解释的其他位置:' + related.join(';') + '。正文合并展示,原始候选仍保留。');
52
61
  if (f.reused || f.priorRun) output.push('- 来源:包含既往审核结果,不表示本轮重新取证。');
@@ -17,6 +17,19 @@ export function comparisonValidated(finding) {
17
17
  return old.every(i => typeof evidence[i].sourceId === 'string' && evidence[i].sourceId) && fresh.every(i => typeof evidence[i].sourceId === 'string' && evidence[i].sourceId && old.every(j => evidence[j].sourceId !== evidence[i].sourceId));
18
18
  }
19
19
 
20
+ // Rendering-only states. A completed uncertain verdict may legitimately have no
21
+ // evidence (the verifier contract allows this); it is not an unfinished run.
22
+ export function verificationPresentation(finding, report = {}) {
23
+ const v = finding?.verification;
24
+ if (v?.status !== 'completed' || !['supported', 'uncertain', 'refuted'].includes(v.verdict)) return 'incomplete';
25
+ const evidenceValid = rows(v.evidence).length > 0 && v.evidence.every(ref => ref?.status === 'references-validated');
26
+ if (v.verdict === 'refuted' && evidenceValid) return 'refuted';
27
+ if (v.verdict !== 'supported' || !evidenceValid) return 'uncertain';
28
+ if (report.mode === 'project' || report.reviewMode === 'project' || report.projectScope || finding.attribution?.causality === 'current-source-only') return 'current';
29
+ if (comparisonValidated(finding) && ['introduced', 'preexisting'].includes(v.regression.classification)) return v.regression.classification;
30
+ return 'uncertain';
31
+ }
32
+
20
33
  /**
21
34
  * Pure presentation data for Markdown/Lead renderers. Counts are candidate records,
22
35
  * not proven defects. coverage counts main-review entries (directories included in
@@ -86,6 +99,11 @@ export function reportSummaryMarkdown(report = {}) {
86
99
  const labels = { introduced: '复核支持本次新增', unresolved: '待核实', preexisting: '复核支持原有', refuted: '复核反驳', current: '当前源码复核支持' };
87
100
  output.push('- 候选复核:' + Object.entries(labels).filter(([key]) => counts[key] > 0).map(([key, label]) => label + ' ' + counts[key]).join(';') + '。');
88
101
  }
102
+ const states = rows(report.findings).map(finding => verificationPresentation(finding, report));
103
+ const uncertain = states.filter(state => state === 'uncertain').length;
104
+ const incomplete = states.filter(state => state === 'incomplete').length;
105
+ output.push('- 复核状态分列:复核支持 ' + (counts.introduced + counts.preexisting + counts.current) + ';已复核不确定 ' + uncertain + ';复核未完成(未复核或未完成有效复核) ' + incomplete + (counts.refuted ? ';已反驳 ' + counts.refuted : '') + '。');
106
+ if (counts.unresolved) output.push('- “待核实”为合计:已复核不确定与复核未完成分开列示,前者不表示复核未执行;均不能直接视为缺陷或必改项。');
89
107
  const unit = coverage.unit === 'files' ? '个文件' : '个变更条目(含目录)';
90
108
  const labels = { completed: '主审完成', excluded: '排除', blocked: '阻断', failed: '失败', cancelled: '取消', pending: '待审', running: '执行中', interrupted: '中断', unaccounted: '状态未记账' };
91
109
  output.push('- 覆盖:' + coverage.total + ' ' + unit + ';' + Object.entries(labels).filter(([key]) => coverage[key] > 0).map(([key, label]) => label + ' ' + coverage[key]).join(',') + (coverage.total ? '。' : '未执行。'));
@@ -1,9 +1,9 @@
1
1
  import { REVIEW_RUNTIME } from './runtime-identity.mjs';
2
2
  const PHASES = ['business-grouping', 'risk-planning', 'primary', 'group-primary', 'file-interaction', 'window-synthesis', 'verification'];
3
- const PURPOSES = ['primary-initial', 'retrieval-followup', 'budget-finalization', 'json-correction', 'business-grouping', 'risk-planning', 'group-primary', 'window-synthesis', 'file-interaction', 'verification'];
3
+ const PURPOSES = ['primary-initial', 'retrieval-followup', 'budget-finalization', 'json-correction', 'business-grouping', 'risk-planning', 'group-primary', 'window-synthesis', 'file-interaction', 'verification', 'verification-correction'];
4
4
  const READER_FIELDS = ['calls', 'outputBytes', 'maxCalls', 'maxOutputBytes', 'remainingCalls', 'remainingOutputBytes', 'maxResultBytes', 'cacheHits', 'deliveries', 'cacheEntries', 'cacheBytes'];
5
5
  const JSON_ISSUES = ['property-name', 'colon', 'separator', 'invalid-escape', 'control-character', 'unterminated-string', 'unexpected-end', 'trailing-content', 'syntax'];
6
- const CORRECTION_CODES = ['MODEL_INVALID_JSON', 'MODEL_INVALID_REVIEW_RESPONSE', 'MODEL_INVALID_FINDING_IDENTITY', 'MODEL_INVALID_FINDING_SEVERITY', 'MODEL_MISSING_FINDING_FIELD', 'MODEL_INVALID_ANCHOR', 'MODEL_ANCHOR_MISMATCH', 'RETRIEVAL_PROTOCOL_ERROR'];
6
+ const CORRECTION_CODES = ['MODEL_INVALID_JSON', 'MODEL_INVALID_REVIEW_RESPONSE', 'MODEL_INVALID_FINDING_IDENTITY', 'MODEL_INVALID_FINDING_SEVERITY', 'MODEL_MISSING_FINDING_FIELD', 'MODEL_INVALID_ANCHOR', 'MODEL_ANCHOR_MISMATCH', 'RETRIEVAL_PROTOCOL_ERROR', 'MODEL_INCOMPLETE_VERIFICATION'];
7
7
  const LIMIT_CODES = ['READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED', 'RETRIEVAL_ROUND_BUDGET_EXCEEDED'];
8
8
  const RECOVERY_KINDS = ['native-tool', 'text-compatible', 'trailing-prose'];
9
9
  const FALLBACK_REASONS = ['mode-notauto', 'smallchange', 'disabled', 'project', 'laterround', 'multi-batch', 'incomplete-edit-scope', 'input-budget'];
@@ -29,7 +29,7 @@ export function reviewDiagnostics(report, { reviewWallMs = null, executionWallMs
29
29
  const readerBudget = Object.fromEntries(READER_FIELDS.map(field => [field, number(report.readerBudget?.[field])]));
30
30
  const correctionCounts = new Map();
31
31
  for (const event of sources.flatMap(f => f.modelCorrections ?? [])) {
32
- if (!['explicit-json-correction', 'retrieval-protocol-correction', 'review-format-correction'].includes(event?.action) || !CORRECTION_CODES.includes(event.code)) continue;
32
+ if (!['explicit-json-correction', 'retrieval-protocol-correction', 'review-format-correction', 'verification-response-correction'].includes(event?.action) || !CORRECTION_CODES.includes(event.code)) continue;
33
33
  const jsonIssue = JSON_ISSUES.includes(event.jsonIssue) ? event.jsonIssue : null;
34
34
  const key = JSON.stringify([event.code, jsonIssue]);
35
35
  correctionCounts.set(key, (correctionCounts.get(key) ?? 0) + 1);
@@ -70,7 +70,7 @@ export function diagnosticsMarkdown(d) {
70
70
  output.push('', '累计调用墙钟可能并发重叠,且包含生命周期观察/初始检查点等待,不是纯提供方耗时,也不能从总耗时中相减当作本地开销。');
71
71
  } else output.push('- 逐阶段模型观测不可用(未产生或旧报告未记录),不按零耗时解释。');
72
72
  if (d.modelCallPurposes?.length) {
73
- const labels = { 'primary-initial': '阶段初始', 'retrieval-followup': '检索后续', 'budget-finalization': '预算受限最终答复', 'json-correction': 'JSON纠正', 'business-grouping': '业务分组', 'risk-planning': '风险计划', 'group-primary': '分组主审', 'window-synthesis': '窗口综合', 'file-interaction': '跨文件综合', verification: '复核' };
73
+ const labels = { 'primary-initial': '阶段初始', 'retrieval-followup': '检索后续', 'budget-finalization': '预算受限最终答复', 'json-correction': 'JSON纠正', 'business-grouping': '业务分组', 'risk-planning': '风险计划', 'group-primary': '分组主审', 'window-synthesis': '窗口综合', 'file-interaction': '跨文件综合', verification: '复核', 'verification-correction': '复核批次纠正' };
74
74
  output.push('- 调用目的:' + d.modelCallPurposes.filter(row => PURPOSES.includes(row.purpose)).map(row => labels[row.purpose] + ' ' + (number(row.calls) ?? '不可用')).join(';') + '。');
75
75
  }
76
76
  if (d.readerBudget && READER_FIELDS.some(field => number(d.readerBudget[field]) !== null)) {
package/src/review.mjs CHANGED
@@ -312,6 +312,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
312
312
  const onProtocolEvent = async info => {
313
313
  if (info.action === 'json-transport-recovery') (state.modelRecoveries ??= []).push(info);
314
314
  if (info.action === 'retrieval-protocol-correction') pendingStageCorrection = { action: info.action, code: 'RETRIEVAL_PROTOCOL_ERROR' };
315
+ if (info.action === 'verification-response-correction' && info.code === 'MODEL_INCOMPLETE_VERIFICATION') (state.modelCorrections ??= []).push({ action: info.action, code: info.code });
315
316
  await options.onProgress?.({ stage: 'protocol-correction', fileId: file.id, path: file.path, ...info });
316
317
  };
317
318
  const trackedModel = request => {
@@ -6,7 +6,7 @@ import { modelWireBudget } from './model-wire-budget.mjs';
6
6
  import { createEvidenceReceipts } from './evidence-receipts.mjs';
7
7
  import { retrievalLoop, retrievalRequest } from './retrieval-loop.mjs';
8
8
  import { verificationSeeds } from './verification-seeds.mjs';
9
- import { validateCandidateVerification } from './candidate-verification.mjs';
9
+ import { validateCandidateVerification, validateCandidateVerificationEnvelope } from './candidate-verification.mjs';
10
10
 
11
11
  export { VERIFICATION_INSTRUCTIONS };
12
12
 
@@ -44,7 +44,34 @@ export async function verificationLoop(model, request, candidates, scope, option
44
44
  const resolveEvidence = value => (seedReceipts.get(value?.receiptId) ?? receipts.resolve)(value);
45
45
  // No caller/model-supplied seed inputs: every initial result above was freshly
46
46
  // read under this invocation's receipt table and the shared approved budget.
47
- const response = await retrievalLoop(model, verificationRequest, retrievalScope, { ...options, initialRetrieved });
47
+ let lastRequest;
48
+ const response = await retrievalLoop(value => { lastRequest = value; return model(value); }, verificationRequest, retrievalScope, { ...options, initialRetrieved });
49
+ let accepted = response;
50
+ try { validateCandidateVerificationEnvelope(response, ids); } catch (error) {
51
+ request.signal?.throwIfAborted();
52
+ // Correct only a structurally incomplete batch, before any final evidence
53
+ // reads. Never retry bad identities, invented receipts or regression failures.
54
+ if (options.correctProtocol !== true || error.code !== 'MODEL_INCOMPLETE_VERIFICATION' || JSON.parse(lastRequest.input).retrievalFinalization) throw error;
55
+ // Admit only an otherwise valid subset of evidence-free uncertain entries.
56
+ // Do not let a missing member hide an invalid identity, receipt or regression.
57
+ if (!response || Object.keys(response).length !== 1 || !Array.isArray(response.verdicts) || response.verdicts.length >= ids.length) throw error;
58
+ const subset = response.verdicts.map(item => item?.candidateId);
59
+ validateCandidateVerificationEnvelope(response, subset);
60
+ if (subset.some(id => !ids.includes(id)) || response.verdicts.some(item => item.verdict !== 'uncertain' || item.evidence.length || Object.hasOwn(item, 'regression'))) throw error;
61
+ const correction = { ...lastRequest, callPurpose: 'verification-correction', instructions: lastRequest.instructions + '\n本次仅纠正不完整的候选复核批次。必须为本次 candidates 每个 candidateId 恰好返回一个 verdict;不可漏项、重复或添加身份。仅返回完整 verdicts,不得 requests,不得再次检索。沿用已提供的原文回执;缺证据的候选明确 uncertain 并说明缺失条件,不得编造支持或删除候选。全部结果仍须通过原校验。' };
62
+ const max = options.maxInputBytes ?? 96 * 1024;
63
+ if (!(request.nativeOutputTools === true ? modelWireBudget(correction, max).fits : Buffer.byteLength(correction.input) + Buffer.byteLength(correction.instructions) <= max)) throw error;
64
+ options.beforeCall?.();
65
+ request.signal?.throwIfAborted();
66
+ await request.onProtocolEvent?.({ action: 'verification-response-correction', code: error.code });
67
+ request.signal?.throwIfAborted();
68
+ const next = await model(correction);
69
+ request.signal?.throwIfAborted();
70
+ const serialized = typeof next === 'string' ? next : JSON.stringify(next);
71
+ if (typeof serialized !== 'string' || Buffer.byteLength(serialized) > 128 * 1024) throw new Error('Verification response too large');
72
+ accepted = JSON.parse(serialized);
73
+ validateCandidateVerificationEnvelope(accepted, ids);
74
+ }
48
75
  request.signal?.throwIfAborted();
49
- return validateCandidateVerification(response, ids, scope.read, resolveEvidence, { enableMigrationRegression: options.enableMigrationRegression === true && !project, migrationContext, catalog: scope.catalog(), fileIds: Object.fromEntries(ids.map((id, i) => [id, candidates[i].fileId])) });
76
+ return validateCandidateVerification(accepted, ids, scope.read, resolveEvidence, { enableMigrationRegression: options.enableMigrationRegression === true && !project, migrationContext, catalog: scope.catalog(), fileIds: Object.fromEntries(ids.map((id, i) => [id, candidates[i].fileId])) });
50
77
  }