@feng0531/dsh-code-review 0.87.5 → 0.87.6

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.6 — 迁移复核成套取证
6
+
7
+ - 复用宿主已校验的迁移契约,为匹配候选有界预取旧入口、旧委托实现和新入口;同套证据原子接纳,容量、预算或输入不满足时回退普通检索,减少模型重新寻找已知路径的往返。
8
+ - 保留共享总账、独立新回执、最终原文复验与迁移分类限制;成套预取成功不等于模型已调用或候选成立。诊断区分预取接纳和回退,不记录源码。
9
+ - 优化按同任务直接审核对照耗时、具体触发链、最小修复建议、覆盖与可读性;本版离线回归不代表真实业务审核已经提速或确认既往候选。
10
+
5
11
  ## 0.87.5 — 主审优先与依赖感知续审
6
12
 
7
13
  - 先完成所有文件的主审扫描,再执行跨窗口/跨文件综合,避免后续分析在尚有文件未主审时抢先消耗模型调用;继续保留有限续跑、原截止时间和完整覆盖分母。
package/DEVELOPMENT.md CHANGED
@@ -24,11 +24,15 @@
24
24
  - `confirmed:true` 是 Agent 对当前人类明确授权且本次预览匹配的声明,不是取消确认参数或无授权绕过。保留 preview→execute 两阶段与 service 确认参数,不新增一键执行 API;宿主/会话工具权限、模型发送许可、快照新鲜度、会话/目录/路由身份及预算校验均不变。
25
25
  - 源变化、预览过期或身份变化须生成新 preview 并重新核对当前授权,不能将旧预览批准用于新快照。保存的后台记录、`resumeFromJobId`、自动续跑及 host 重启本身都不授予外发权限;显式续审须有当前人类授权和新预览,匹配时无需重复询问。私有模型工件的源码提示与独立明确同意要求保持不变。
26
26
 
27
+ ## 同任务质量与效率对照
28
+
29
+ 每轮优化以相同固定范围、模型和尽量相同运行条件下的直接 Agent 审核作参照,分开记录用户请求到交付、工具执行和审查阶段墙钟,不将并发累计调用时间当作墙钟。比较具体触发条件、新旧入口行为、反证、最小可行动建议、主审/跨文件覆盖和默认报告可读性。直接审核结论也需证据核验,不作为绝对真值;合成响应只验证协议与接线,不证明模型召回或真实提速。测试不复制私有业务源码或模型工件。
30
+
27
31
  ## 变更审查调度与离线对照
28
32
 
29
33
  生产 changes 服务默认 `changesConcurrency=2`,配置范围 1~4;底层 `reviewSnapshot` 未传该选项时保持串行兼容。所有文件共用原批准快照 reader,不能按 worker 重置额度。业务分组先完成,交互 owner 等待此前成员链结束;窗口、轮次与复核仍串行依赖,固定索引归并结果。取消或 checkpoint 失败必须先排空所有在途链再释放锁。
30
34
 
31
- `maxRetrievalRounds` 默认 3、范围 0~50;主审/综合/复核分别使用该轮数上限,独立风险规划为一次待证问题生成而非检索对话,保留最终答复回合与 partial/uncertain。复核预取仅来自本次 reader 的候选同侧锚点范围,受输入/输出和最终校验预留约束;缺少事务、超预算或非行锚点回退普通检索,不信任模型自报证据。全范围风险计划仅限本次不可变文件调用内复用,规划元数据超过输入预算回退原窗口;预览与执行估算使用相同规划输入。
35
+ `maxRetrievalRounds` 默认 3、范围 0~50;主审/综合/复核分别使用该轮数上限,独立风险规划为一次待证问题生成而非检索对话,保留最终答复回合与 partial/uncertain。复核预取来自本次 reader 的候选同侧锚点;对于匹配宿主已校验迁移契约的候选,优先原子预取完整旧入口、旧委托实现和新入口。全局仍限 8 次种子读取、16 KiB 和每次 200 行,受输入/输出及最终原文校验预留约束;角色不完整、缺事务或超预算回退普通检索,不信任模型自报契约或证据。预取接纳仅为宿主步骤,不代表已外发、已复核或已确认新增回归;删除导出路由仍仅允许 uncertain。全范围风险计划仅限本次不可变文件调用内复用,规划元数据超过输入预算回退原窗口;预览与执行估算使用相同规划输入。
32
36
 
33
37
  运行 `node scripts/changes-speed-benchmark.mjs [baseline-directory]` 比较真实 review 入口上的 8 文件 SVN 受控候选/反驳夹具,包含 receipt 复核与最终精确原文校验;每次模型响应模拟 20ms。可先将冻结版本的 src 解包到独立目录作为 baseline。比较调用数、峰值并发和原文证据,不把离线计时或 mock 结论一致当作真实质量、token 成本或 45→12 分钟保证。`model-start/model-end` 检查点只记录计时、字节、状态及阶段,不写入源码或推理;各文件 `modelMetrics` 是提供方实际调用(含纠错)累计时间,并行时不可相加当作墙钟耗时。
34
38
 
package/RELEASE-NOTES.md CHANGED
@@ -1,19 +1,17 @@
1
- # v0.87.5 发布说明
1
+ # v0.87.6 发布说明
2
2
 
3
- ## 修复审核执行链路,而不只是整理报告
3
+ ## 让已知迁移路径更早成为本次复核证据
4
4
 
5
- 先完成全体文件的主审扫描,再运行跨窗口、跨文件综合,减少后续分析挤占尚未主审文件的模型调用。续审队列按真实依赖就绪集合排队,不再让依赖未满足的 verifier 队头阻挡可运行的 primary;精确已知请求可使用自身保留容量或缓存首次恢复,不扩大共享预算。
5
+ 复核已能接收宿主校验的迁移契约,但此前默认种子只覆盖候选锚点附近,旧公开入口、旧委托实现与新入口仍需模型自行检索。本版为匹配候选增加有界成套取证,减少寻找已有导航坐标的往返;完整角色无法容纳时回退普通检索,不宣称已完成比较。
6
6
 
7
- 跟进取证超出输入上限时,移除重复的初始源码正文,保留身份、精确编辑、调用/风险线索、已读取证据正文及回执;初始输入和可容纳的小请求不变。
7
+ 种子沿用同一批准快照和读取总账;保留最多 8 次读取、16 KiB 种子输入、每次最多 200 行及最终证据复验余量。同套读取采用事务,失败或取消不发布部分回执。每次复核重新签发回执,不能借用既往主审/复核证据身份。
8
8
 
9
- 阶段停止原因按 fileIds 关联回报告,明确区分前置依赖、读取分配等待、调用预算、超时和尝试耗尽。一个阶段的检索限制不再阻止其他健康阶段缓存已接受生成;原文件仍如实保留受限状态。
9
+ ## 诊断与安全边界
10
10
 
11
- ## 参考与边界
11
+ 执行诊断记录迁移种子接纳/回退计数,不含源码正文;预取接纳不证明模型已调用,更不证明复核支持或新增因果成立。普通比较仍要求正确主文件与来源侧;跨文件仅限已校验契约,删除导出路由仍仅允许不确定归因。
12
12
 
13
- 参考用户指定的 [Open Code Review](https://github.com/alibaba/open-code-review) 的冻结覆盖清单、结构化停止和预算耗尽后保全发现机制;核查本地 a758d9cb 与远端 5c039e815023a05a169b1240ae6b4b24c28270b6 的相关变更。采用独立实现,未复制其源码或模板。不照搬“已有评论即完成”的覆盖推断,也不将 Git merge-base 流程移植为 SVN revision 语义。
14
-
15
- 本次不增加读取总额、不重置检索轮数或重试次数、不放宽证据与新增归因校验。Git/SVN 固定批准范围和独立复核回执仍有效;预算、超时或证据不足时仍可能 partial/uncertain。离线受控回归不是原业务 r7364 的重跑,不承诺真实模型必定查完或固定提速比例。
13
+ 不增加模型调用额度、检索轮数、读取总额或并发;不降低复核证据门槛。未来同任务对照同时检查端到端与分段耗时、触发链、最小修复建议、覆盖及可读性,不能以减少调用或缩短报告代替质量提升。本版没有重新执行 r7364 真实模型审核,不承诺固定提速或确认两项既往候选。
16
14
 
17
15
  ## 验证与升级
18
16
 
19
- 新增 Git/SVN 主审优先、依赖队头阻塞、精确恢复公平性、完整阶段取证输入与停止原因关联回归,并执行全量测试和冻结标签四矩阵 CI。发布不自动安装或重启当前宿主。安装见 [README](README.md),门禁见 [开发说明](DEVELOPMENT.md)。
17
+ 新增迁移成套种子、事务回退、新鲜回执及预算边界回归,并通过全量测试与冻结标签四矩阵 CI 后发布。发布不自动安装或重启当前宿主。安装见 [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.5",
3
+ "version": "0.87.6",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -128,6 +128,36 @@ export function migrationRegressionMetadata(context) {
128
128
  return context.contracts.map(contract => ({ ...structuredClone(contract), old: [structuredClone(contract.old)], new: [structuredClone(contract.new)], status: 'navigation-only', causality: 'unverified' }));
129
129
  }
130
130
 
131
+ // Host-only read plans. The capability, candidate attribution and coordinates all
132
+ // come from this compiler invocation, never from a later candidate/contract copy.
133
+ export function migrationVerificationPlans(context) {
134
+ if (!contexts.has(context)) fail();
135
+ return context.candidates.map((candidate, index) => {
136
+ const candidateId = 'c' + (index + 1);
137
+ const matches = context.contracts.filter(contract => contract.candidateFileIds.includes(candidate.fileId) &&
138
+ candidate.attribution?.status === 'references-validated' && Array.isArray(candidate.attribution.editIds) &&
139
+ [contract.old, contract.new].some(role => role.fileId === candidate.fileId && role.edits.some(edit =>
140
+ candidate.attribution.editIds.includes(edit.editId) && candidate.anchor?.kind === 'line' &&
141
+ candidate.anchor.side === (role.side === 'old' ? 'old' : 'new') &&
142
+ positive(candidate.anchor.start) && positive(candidate.anchor.end) && candidate.anchor.end >= candidate.anchor.start &&
143
+ overlap({ start: candidate.anchor.start, count: candidate.anchor.end - candidate.anchor.start + 1 }, edit.range))));
144
+ const contract = matches[0];
145
+ if (!contract) return { candidateId, candidate, reason: 'NO_MATCHING_CONTRACT' };
146
+ const roles = [contract.old, contract.new, ...(contract.baseline ?? [])];
147
+ const identity = { candidateId, candidate, contractId: contract.id, kind: contract.kind ?? 'migration', rolesCount: roles.length };
148
+ if (roles.some(role => !positive(role.startLine) || !positive(role.endLine) || role.endLine < role.startLine)) return { ...identity, reason: 'INCOMPLETE_ROLE_RANGE' };
149
+ const requests = new Map();
150
+ for (const role of roles) {
151
+ for (let start = role.startLine; start <= role.endLine; start += 200) {
152
+ const request = { kind: 'read', id: role.sourceId, start, count: Math.min(200, role.endLine - start + 1) };
153
+ requests.set(JSON.stringify(request), request);
154
+ if (requests.size > 8) return { ...identity, reason: 'EVIDENCE_LIMIT' };
155
+ }
156
+ }
157
+ return { ...identity, requests: [...requests.values()] };
158
+ });
159
+ }
160
+
131
161
  export function validateMigrationRegression(value, evidence, fileId, context, candidateId) {
132
162
  if (!contexts.has(context) || typeof value.migrationContractId !== 'string') fail();
133
163
  const contract = context.contracts.find(contract => contract.id === value.migrationContractId && contract.candidateFileIds.includes(fileId));
@@ -0,0 +1,90 @@
1
+ import { createEvidenceReceipts } from './evidence-receipts.mjs';
2
+ import { migrationVerificationPlans } from './migration-regression.mjs';
3
+
4
+ const size = value => Buffer.byteLength(JSON.stringify(value));
5
+ const key = request => JSON.stringify(request);
6
+ const capacity = reason => Object.assign(new Error('Optional migration seed capacity exceeded'), { code: 'VERIFICATION_SEED_CAPACITY', reason });
7
+ const optional = error => ['VERIFICATION_SEED_CAPACITY', 'READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED'].includes(error?.code) || error?.message === 'Line range outside source';
8
+
9
+ // No external plans are accepted: only the compiled context selects candidates.
10
+ export function migrationVerificationSeeds(context, scope, { signal, fits, onReceipt = () => {}, onMigrationSeeds = () => {} }) {
11
+ signal?.throwIfAborted();
12
+ const plans = migrationVerificationPlans(context), catalog = scope.catalog();
13
+ const seeds = [], selected = new Map(), assignments = new Map();
14
+ const ordinary = candidate => {
15
+ const anchor = candidate.anchor;
16
+ if (anchor?.kind !== 'line' || !['old', 'new'].includes(anchor.side) || !Number.isSafeInteger(anchor.start) || !Number.isSafeInteger(anchor.end) || anchor.start < 1 || anchor.end < anchor.start) return [];
17
+ const sources = catalog.filter(source => source.fileId === candidate.fileId && source.side === anchor.side);
18
+ if (sources.length !== 1) return [];
19
+ const start = Math.max(1, anchor.start - 4);
20
+ return [{ kind: 'read', id: sources[0].id, start, count: Math.min(200, anchor.end - start + 5) }];
21
+ };
22
+ const notify = (plan, status, reason) => {
23
+ // Deliberately reconstruct a source-free allowlist, never spread a plan.
24
+ onMigrationSeeds({ candidateId: plan.candidateId, ...(plan.contractId ? { contractId: plan.contractId } : {}), kind: plan.kind ?? 'migration', status, reason, rolesCount: plan.rolesCount ?? 0 });
25
+ signal?.throwIfAborted();
26
+ };
27
+ const admit = (plan, requests, complete) => {
28
+ if (!requests.length || typeof scope.transaction !== 'function' || typeof scope.preflight !== 'function') throw capacity('TRANSACTION_UNAVAILABLE');
29
+ const fresh = requests.filter(request => !selected.has(key(request)));
30
+ if (seeds.length + fresh.length > 8) throw capacity('SEED_LIMIT');
31
+ const receipts = createEvidenceReceipts(scope.read, { limit: Math.max(1, fresh.length) });
32
+ const items = scope.transaction(() => {
33
+ signal?.throwIfAborted();
34
+ if (fresh.length) scope.preflight(fresh);
35
+ const items = fresh.map(request => {
36
+ const result = receipts.read(request);
37
+ signal?.throwIfAborted();
38
+ if (result.sourceId !== request.id || result.start !== request.start || !result.text || (complete ? result.count !== request.count : result.start + result.count <= plan.candidate.anchor.start)) throw capacity('INCOMPLETE_READ');
39
+ return { request, result };
40
+ });
41
+ const proposed = [...seeds, ...items], lookup = new Map(proposed.map(item => [key(item.request), item]));
42
+ const nextAssignments = new Map(assignments).set(plan.candidateId, requests.map(key));
43
+ // Reserve repeated candidates now, not after the first set has consumed
44
+ // headroom. Exact shared plans need no additional materialization call.
45
+ for (const other of plans) {
46
+ // A prior fallback does not limit what this candidate may reference from
47
+ // the globally visible role set admitted by a later candidate. Upgrade its
48
+ // reserve whenever its complete plan becomes available.
49
+ const completeReferences = other.requests?.map(key);
50
+ const references = completeReferences?.every(reference => lookup.has(reference)) ? completeReferences : nextAssignments.get(other.candidateId) ?? ordinary(other.candidate).map(key);
51
+ if (references.length && references.every(reference => lookup.has(reference))) nextAssignments.set(other.candidateId, references);
52
+ }
53
+ let calls = plans.length - nextAssignments.size, bytes = 0;
54
+ // Every candidate reference is a final delivery, even for shared cached
55
+ // results. EOF-clamped validation counts have their own exact cache key.
56
+ for (const references of nextAssignments.values()) for (const reference of references) {
57
+ const result = lookup.get(reference).result;
58
+ const cost = scope.preflight([{ kind: 'read', id: result.sourceId, start: result.start, count: result.count }]);
59
+ if (!Number.isSafeInteger(cost?.calls) || cost.calls < 0) throw capacity('FINAL_RESERVE');
60
+ calls += cost.calls;
61
+ bytes += size(result);
62
+ }
63
+ const budget = scope.budget?.();
64
+ if (budget && (budget.remainingCalls < calls || budget.remainingOutputBytes < bytes)) throw capacity('FINAL_RESERVE');
65
+ if (size(proposed) > 16 * 1024 || !fits(proposed)) throw capacity('INPUT_CAPACITY');
66
+ signal?.throwIfAborted();
67
+ return items;
68
+ });
69
+ for (const item of items) {
70
+ selected.set(key(item.request), item);
71
+ seeds.push(item);
72
+ onReceipt(item.result.receiptId, receipts.resolve);
73
+ }
74
+ assignments.set(plan.candidateId, requests.map(key));
75
+ };
76
+ for (const plan of plans) {
77
+ signal?.throwIfAborted();
78
+ let reason = plan.reason;
79
+ if (plan.requests) {
80
+ let accepted = false;
81
+ try { admit(plan, plan.requests, true); accepted = true; }
82
+ catch (error) { signal?.throwIfAborted(); if (!optional(error)) throw error; reason = error.reason ?? error.code ?? 'INCOMPLETE_READ'; }
83
+ if (accepted) { notify(plan, 'accepted', 'COMPLETE_ROLE_SET'); continue; }
84
+ }
85
+ notify(plan, 'fallback', reason ?? 'NO_MATCHING_CONTRACT');
86
+ try { admit(plan, ordinary(plan.candidate), false); }
87
+ catch (error) { signal?.throwIfAborted(); if (!optional(error)) throw error; }
88
+ }
89
+ return seeds;
90
+ }
@@ -41,11 +41,21 @@ export function reviewDiagnostics(report, { reviewWallMs = null, executionWallMs
41
41
  const riskPlanningFallbackReasons = FALLBACK_REASONS.flatMap(reason => { const calls = (report.files ?? []).filter(f => f.riskPlanningFallbackReason === reason).length; return calls ? [{ reason, calls }] : []; });
42
42
  const recoveryEvents = sources.flatMap(source => source.modelRecoveries ?? []);
43
43
  const transportRecoveries = RECOVERY_KINDS.flatMap(kind => { const calls = recoveryEvents.filter(event => event?.action === 'json-transport-recovery' && event.kind === kind).length; return calls ? [{ kind, calls }] : []; });
44
+ const migrationEvents = sources.flatMap(source => Array.isArray(source.migrationSeedEvents) ? source.migrationSeedEvents : []);
45
+ const migrationSeeds = ['entry-migration', 'old-delegation', 'deleted-export-route'].flatMap(kind =>
46
+ ['accepted', 'fallback'].flatMap(status => {
47
+ const count = migrationEvents.filter(event => event?.kind === kind && event.status === status).length;
48
+ return count ? [{ kind, status, count }] : [];
49
+ }));
50
+ const migrationSeedReasons = ['COMPLETE_ROLE_SET', 'NO_MATCHING_CONTRACT', 'INCOMPLETE_ROLE_RANGE', 'EVIDENCE_LIMIT', 'TRANSACTION_UNAVAILABLE', 'SEED_LIMIT', 'INCOMPLETE_READ', 'FINAL_RESERVE', 'INPUT_CAPACITY', 'READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED', 'OTHER'].flatMap(reason => {
51
+ const count = migrationEvents.filter(event => ['accepted', 'fallback'].includes(event?.status) && ['entry-migration', 'old-delegation', 'deleted-export-route'].includes(event.kind) && event.reason === reason).length;
52
+ return count ? [{ reason, count }] : [];
53
+ });
44
54
  const resume = { reusedFiles: number(report.resume?.reusedFiles) };
45
55
  const artifactRecords = Array.isArray(report.artifacts) ? report.artifacts : null;
46
56
  const artifacts = { retained: artifactRecords ? artifactRecords.filter(record => record?.omitted === false && record.recorded === true).length : null, omitted: artifactRecords ? artifactRecords.filter(record => record?.omitted === true).length : null };
47
57
  return { schemaVersion: 2, resume, artifacts, transportRecoveries, pluginVersion: REVIEW_RUNTIME.version, reviewWallMs: number(reviewWallMs), executionWallMs: number(executionWallMs),
48
- modelCalls: number(report.modelCalls), observedModelCalls, phases, modelCallPurposes, readerBudget, correctionReasons, readerLimitReasons, readerLimits, riskPlanningFallbackReasons,
58
+ modelCalls: number(report.modelCalls), observedModelCalls, phases, modelCallPurposes, readerBudget, correctionReasons, readerLimitReasons, readerLimits, riskPlanningFallbackReasons, migrationSeeds, migrationSeedReasons,
49
59
  planning: { completed: plans.filter(p => p.status === 'completed' && p.method !== 'inline').length, inline: plans.filter(p => p.method === 'inline' && p.status === 'completed').length, inlineAbsent: plans.filter(p => p.method === 'inline' && p.status === 'absent').length, inlineInvalid: plans.filter(p => p.method === 'inline' && p.status === 'invalid').length, fallback: plans.filter(p => p.status === 'fallback').length, skipped: plans.filter(p => p.status === 'skipped').length },
50
60
  callPurposeNotice: 'primary-initial is the first call of a retrieval-enabled stage, not only primary review; phase identifies the stage. Correction reason counts include actual correction invocation records only, not both failures and completion notices.',
51
61
  timingNotice: 'Observed call wall sums overlap under concurrency and include lifecycle observation; not pure provider latency. Missing values are unavailable, not zero.' };
@@ -73,6 +83,16 @@ export function diagnosticsMarkdown(d) {
73
83
  if (number(d.readerLimits?.maxRounds) !== null) output.push('- 每阶段最大检索轮次:' + number(d.readerLimits.maxRounds) + ';调用数/输出字节耗尽与轮次耗尽分别记录,不能混同。');
74
84
  if (d.riskPlanningFallbackReasons?.length) output.push('- 风险策略回退原因:' + d.riskPlanningFallbackReasons.filter(r => FALLBACK_REASONS.includes(r.reason)).map(r => r.reason + ' ' + (number(r.calls) ?? '不可用')).join(';') + '。');
75
85
  if (d.transportRecoveries?.length) output.push('- 本地结果传输:' + d.transportRecoveries.filter(row => RECOVERY_KINDS.includes(row.kind)).map(row => row.kind + ' ' + (number(row.calls) ?? '不可用')).join(';') + ';仅计成功解析的传输事件,不表示额外模型调用或缺陷确认。');
86
+ if (d.migrationSeeds?.length) {
87
+ const labels = { 'entry-migration': '入口迁移', 'old-delegation': '旧入口委托', 'deleted-export-route': '删除导出导航' };
88
+ const rows = d.migrationSeeds.filter(row => Object.hasOwn(labels, row.kind) && ['accepted', 'fallback'].includes(row.status) && Number.isSafeInteger(row.count) && row.count >= 0);
89
+ if (d.migrationSeedReasons?.length) {
90
+ const reasons = { COMPLETE_ROLE_SET: '完整角色接纳', NO_MATCHING_CONTRACT: '无匹配契约', INCOMPLETE_ROLE_RANGE: '角色范围不完整', EVIDENCE_LIMIT: '证据条数上限', TRANSACTION_UNAVAILABLE: '无事务能力', SEED_LIMIT: '种子数量上限', INCOMPLETE_READ: '读取不完整', FINAL_RESERVE: '最终复验预留不足', INPUT_CAPACITY: '输入容量不足', READER_CALL_BUDGET_EXCEEDED: '读取调用不足', READER_OUTPUT_BUDGET_EXCEEDED: '读取字节不足', OTHER: '其他回退' };
91
+ const details = d.migrationSeedReasons.filter(row => Object.hasOwn(reasons, row.reason) && Number.isSafeInteger(row.count) && row.count >= 0);
92
+ if (details.length) output.push('- 迁移种子原因:' + details.map(row => reasons[row.reason] + ' ' + row.count).join(';') + '。');
93
+ }
94
+ if (rows.length) output.push('- 迁移证据成套预取:' + rows.map(row => labels[row.kind] + ' ' + (row.status === 'accepted' ? '预取接纳' : '回退普通检索') + ' ' + row.count).join(';') + ';计预取尝试,不表示模型已调用、复核通过或新增缺陷成立,删除导出导航仍仅允许不确定归因。');
95
+ }
76
96
  if (number(d.resume?.reusedFiles) !== null) output.push('- 续审复用已完成文件:' + number(d.resume.reusedFiles) + ';复用成果不计为本次模型调用或新读取证据。');
77
97
  if (number(d.artifacts?.retained) !== null || number(d.artifacts?.omitted) !== null) output.push('- 私有模型工件:已留存 ' + (number(d.artifacts.retained) ?? '不可用') + ',未留存 ' + (number(d.artifacts.omitted) ?? '不可用') + ';诊断仅含计数,不包含正文或访问标识。');
78
98
  output.push('- primary-initial 表示启用检索的某阶段初始调用,不仅是主审;具体阶段以阶段统计为准。');
package/src/review.mjs CHANGED
@@ -608,6 +608,13 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
608
608
  const verdicts = await verificationLoop(trackedModel, { ...request, onProtocolEvent, reviewPhase: 'verification' }, generated.findings, stageScope('verification:' + file.id), {
609
609
  correctProtocol:options.correctProtocol===true,maxRounds:maxRetrievalRounds,maxInputBytes, beforeCall, onBudgetLimited, yieldOnBudget: options.enableStageContinuation === true,
610
610
  enableMigrationRegression: options.enableMigrationRegression === true, migrationContracts: report.migrationContracts?.contracts ?? [], migrationSnapshot: input,
611
+ onMigrationSeeds: event => {
612
+ // Source-free host events only; never persist model text or reader bodies.
613
+ if (!['accepted', 'fallback'].includes(event?.status)) return;
614
+ const kind = ['old-delegation', 'deleted-export-route'].includes(event.kind) ? event.kind : 'entry-migration';
615
+ const reason = ['COMPLETE_ROLE_SET', 'NO_MATCHING_CONTRACT', 'INCOMPLETE_ROLE_RANGE', 'EVIDENCE_LIMIT', 'TRANSACTION_UNAVAILABLE', 'SEED_LIMIT', 'INCOMPLETE_READ', 'FINAL_RESERVE', 'INPUT_CAPACITY', 'READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED'].includes(event.reason) ? event.reason : 'OTHER';
616
+ (state.migrationSeedEvents ??= []).push({ status: event.status, kind, reason });
617
+ },
611
618
  onRetrieved: records => { (state.retrievalAudit ??= []).push(...records); },
612
619
  onTruncated: () => { result.limitations.push({ fileId: file.id, text: '复核检索结果达到数量上限' }); },
613
620
  });
@@ -31,6 +31,8 @@ export async function verificationLoop(model, request, candidates, scope, option
31
31
  // permits no retrieval rounds. Do not spend its budget on optional prefetch.
32
32
  const initialRetrieved = options.maxRounds === 0 ? [] : verificationSeeds(candidates, scope, {
33
33
  signal: request.signal,
34
+ migrationContext,
35
+ onMigrationSeeds: options.onMigrationSeeds,
34
36
  onReceipt: (id, resolve) => seedReceipts.set(id, resolve),
35
37
  fits: seeds => {
36
38
  const next = retrievalRequest(verificationRequest, scope, seeds);
@@ -1,4 +1,5 @@
1
1
  import { createEvidenceReceipts } from './evidence-receipts.mjs';
2
+ import { migrationVerificationSeeds } from './migration-verification-seeds.mjs';
2
3
 
3
4
  // Host-only optional prefetch. Only approved catalog identities and actual line
4
5
  // coordinates select reads; candidate prose/snippets/receipts never become evidence.
@@ -7,8 +8,9 @@ const MAX_SEED_BYTES = 16 * 1024;
7
8
  const optionalFailure = error => ['VERIFICATION_SEED_CAPACITY', 'READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED'].includes(error?.code);
8
9
  const capacityError = () => Object.assign(new Error('Optional verification seed capacity exceeded'), { code: 'VERIFICATION_SEED_CAPACITY' });
9
10
 
10
- export function verificationSeeds(candidates, scope, { signal, fits, onReceipt = () => {} }) {
11
+ export function verificationSeeds(candidates, scope, { signal, fits, onReceipt = () => {}, migrationContext, onMigrationSeeds }) {
11
12
  signal?.throwIfAborted();
13
+ if (migrationContext !== undefined) return migrationVerificationSeeds(migrationContext, scope, { signal, fits, onReceipt, onMigrationSeeds });
12
14
  // Without atomic rollback an optional seed could consume the budget and then
13
15
  // fail its model-input fit check. Leave those readers on ordinary retrieval.
14
16
  if (typeof scope.transaction !== 'function') return [];