@feng0531/dsh-code-review 0.87.5 → 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 +12 -0
- package/DEVELOPMENT.md +5 -1
- package/RELEASE-NOTES.md +9 -11
- package/package.json +1 -1
- package/src/candidate-verification.mjs +6 -2
- package/src/migration-regression.mjs +30 -0
- package/src/migration-verification-seeds.mjs +90 -0
- package/src/report-findings.mjs +19 -10
- package/src/report-summary.mjs +18 -0
- package/src/review-diagnostics.mjs +25 -5
- package/src/review.mjs +8 -0
- package/src/verification-loop.mjs +32 -3
- package/src/verification-seeds.mjs +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.87.6 — 迁移复核成套取证
|
|
12
|
+
|
|
13
|
+
- 复用宿主已校验的迁移契约,为匹配候选有界预取旧入口、旧委托实现和新入口;同套证据原子接纳,容量、预算或输入不满足时回退普通检索,减少模型重新寻找已知路径的往返。
|
|
14
|
+
- 保留共享总账、独立新回执、最终原文复验与迁移分类限制;成套预取成功不等于模型已调用或候选成立。诊断区分预取接纳和回退,不记录源码。
|
|
15
|
+
- 优化按同任务直接审核对照耗时、具体触发链、最小修复建议、覆盖与可读性;本版离线回归不代表真实业务审核已经提速或确认既往候选。
|
|
16
|
+
|
|
5
17
|
## 0.87.5 — 主审优先与依赖感知续审
|
|
6
18
|
|
|
7
19
|
- 先完成所有文件的主审扫描,再执行跨窗口/跨文件综合,避免后续分析在尚有文件未主审时抢先消耗模型调用;继续保留有限续跑、原截止时间和完整覆盖分母。
|
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
|
|
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.
|
|
1
|
+
# v0.87.7 发布说明
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 先看清楚结论,再减少复核返工
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
报告区分复核支持、已复核不确定、复核未完成与已反驳。候选未成立时,以待证条件、影响假说与待验证方案表达,避免把主审描述误读为独立确认的缺陷;完整证据仍保留在技术附录。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 不完整批次的有界纠正
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
启用协议纠正时,复核返回不完整批次可再请求一次完整判定。使用同次调用最后已发送的证据与新鲜回执,不重启检索、不重新签发整套证据;仍计入相同模型调用额度、输入限制和截止时间。仅接纳少项且已有成员均为合法身份、无证据无归因的 uncertain 批次进行纠正;含支持、回执或归因的部分响应仍直接失败,避免漏项掩盖非法证据。身份、回执、原文及修改前后归因继续严格校验;再次不完整即失败。检索最终答复模式不追加纠正。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
执行诊断单列复核批次纠正的调用与原因,不将纠正当成缺陷成立。正常完整批次不增加调用;纠正可能增加一次模型往返,不承诺每次审核更快。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## 验证与边界
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
本版采用离线正反回归、全量测试、真实宿主离线兼容检查及冻结标签四矩阵门禁。没有重新运行真实业务审核,也没有将直接审核的历史结论当作真值。分组取证续跑需要可回滚的额度转移,尚未改动该失败回退语义。
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
新增 Git/SVN 主审优先、依赖队头阻塞、精确恢复公平性、完整阶段取证输入与停止原因关联回归,并执行全量测试和冻结标签四矩阵 CI。发布不自动安装或重启当前宿主。安装见 [README](README.md),门禁见 [开发说明](DEVELOPMENT.md)。
|
|
17
|
+
发布不自动安装或重启当前宿主。安装见 [README](README.md),门禁见 [开发说明](DEVELOPMENT.md)。
|
package/package.json
CHANGED
|
@@ -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
|
|
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 });
|
|
@@ -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
|
+
}
|
package/src/report-findings.mjs
CHANGED
|
@@ -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)=>{
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
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('-
|
|
48
|
-
if (f.attribution?.afterBehavior) output.push('-
|
|
49
|
-
if (
|
|
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('- 来源:包含既往审核结果,不表示本轮重新取证。');
|
package/src/report-summary.mjs
CHANGED
|
@@ -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);
|
|
@@ -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.' };
|
|
@@ -60,7 +70,7 @@ export function diagnosticsMarkdown(d) {
|
|
|
60
70
|
output.push('', '累计调用墙钟可能并发重叠,且包含生命周期观察/初始检查点等待,不是纯提供方耗时,也不能从总耗时中相减当作本地开销。');
|
|
61
71
|
} else output.push('- 逐阶段模型观测不可用(未产生或旧报告未记录),不按零耗时解释。');
|
|
62
72
|
if (d.modelCallPurposes?.length) {
|
|
63
|
-
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': '复核批次纠正' };
|
|
64
74
|
output.push('- 调用目的:' + d.modelCallPurposes.filter(row => PURPOSES.includes(row.purpose)).map(row => labels[row.purpose] + ' ' + (number(row.calls) ?? '不可用')).join(';') + '。');
|
|
65
75
|
}
|
|
66
76
|
if (d.readerBudget && READER_FIELDS.some(field => number(d.readerBudget[field]) !== null)) {
|
|
@@ -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
|
@@ -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 => {
|
|
@@ -608,6 +609,13 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
608
609
|
const verdicts = await verificationLoop(trackedModel, { ...request, onProtocolEvent, reviewPhase: 'verification' }, generated.findings, stageScope('verification:' + file.id), {
|
|
609
610
|
correctProtocol:options.correctProtocol===true,maxRounds:maxRetrievalRounds,maxInputBytes, beforeCall, onBudgetLimited, yieldOnBudget: options.enableStageContinuation === true,
|
|
610
611
|
enableMigrationRegression: options.enableMigrationRegression === true, migrationContracts: report.migrationContracts?.contracts ?? [], migrationSnapshot: input,
|
|
612
|
+
onMigrationSeeds: event => {
|
|
613
|
+
// Source-free host events only; never persist model text or reader bodies.
|
|
614
|
+
if (!['accepted', 'fallback'].includes(event?.status)) return;
|
|
615
|
+
const kind = ['old-delegation', 'deleted-export-route'].includes(event.kind) ? event.kind : 'entry-migration';
|
|
616
|
+
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';
|
|
617
|
+
(state.migrationSeedEvents ??= []).push({ status: event.status, kind, reason });
|
|
618
|
+
},
|
|
611
619
|
onRetrieved: records => { (state.retrievalAudit ??= []).push(...records); },
|
|
612
620
|
onTruncated: () => { result.limitations.push({ fileId: file.id, text: '复核检索结果达到数量上限' }); },
|
|
613
621
|
});
|
|
@@ -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
|
|
|
@@ -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);
|
|
@@ -42,7 +44,34 @@ export async function verificationLoop(model, request, candidates, scope, option
|
|
|
42
44
|
const resolveEvidence = value => (seedReceipts.get(value?.receiptId) ?? receipts.resolve)(value);
|
|
43
45
|
// No caller/model-supplied seed inputs: every initial result above was freshly
|
|
44
46
|
// read under this invocation's receipt table and the shared approved budget.
|
|
45
|
-
|
|
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
|
+
}
|
|
46
75
|
request.signal?.throwIfAborted();
|
|
47
|
-
return validateCandidateVerification(
|
|
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])) });
|
|
48
77
|
}
|
|
@@ -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 [];
|