@feng0531/dsh-code-review 0.87.4 → 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 +13 -0
- package/DEVELOPMENT.md +13 -1
- package/README.md +2 -2
- package/RELEASE-NOTES.md +9 -9
- package/package.json +1 -1
- package/src/migration-regression.mjs +30 -0
- package/src/migration-verification-seeds.mjs +90 -0
- package/src/report-readability.mjs +55 -11
- package/src/retrieval-loop.mjs +30 -13
- package/src/review-diagnostics.mjs +21 -1
- package/src/review-reader-allocation.mjs +28 -13
- package/src/review.mjs +56 -19
- package/src/stage-continuation.mjs +2 -2
- package/src/verification-loop.mjs +2 -0
- package/src/verification-seeds.mjs +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.87.5 — 主审优先与依赖感知续审
|
|
12
|
+
|
|
13
|
+
- 先完成所有文件的主审扫描,再执行跨窗口/跨文件综合,避免后续分析在尚有文件未主审时抢先消耗模型调用;继续保留有限续跑、原截止时间和完整覆盖分母。
|
|
14
|
+
- 读取恢复按当前真实就绪阶段排队,跳过依赖未满足的队头;首次精确请求可利用自身保留额度或缓存恢复,不释放其他阶段保留额、不重置总账或检索轮数。
|
|
15
|
+
- 跟进取证输入超限时只移除重复初始源码正文,保留精确编辑、风险/调用线索与已交付证据回执;避免大文件重复正文挤掉新读证据。
|
|
16
|
+
- 未完成原因从阶段关联回文件和报告,保留依赖、分配等待、超时、调用上限与尝试状态;一个阶段受限不再阻止其他健康阶段保留生成。仍受限时明确 partial,不把评论、候选或报告收尾当作审核完成。
|
|
17
|
+
|
|
5
18
|
## 0.87.4 — 默认交付人能读懂的审核报告
|
|
6
19
|
|
|
7
20
|
- 默认主文先给是否查完、问题卡、未完成原因和下一步;完整证据与执行台账移到同一报告折叠附录,不要求使用者手动拼接批次。
|
package/DEVELOPMENT.md
CHANGED
|
@@ -24,14 +24,26 @@
|
|
|
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
|
|
|
39
|
+
### 主审优先与依赖感知恢复
|
|
40
|
+
|
|
41
|
+
启用 `enableStageContinuation` 时,跨窗口/跨文件综合先登记为依赖阶段,待全体文件主审扫描后由 continuation 运行;独立复核可在主审链内进行,受限后进入同一有界队列。读取恢复传入完整当前 ready allocation IDs,跳过依赖未就绪队头但不释放其 floor;首次精确请求可无 release 尝试自身可负担成本,实际 admission 计次,同 revision 无进展不忙重试。未知成本的 scalar admission 仍要求容量释放,不声称它足以支付任意 verifier 预取。
|
|
42
|
+
|
|
43
|
+
跟进输入超限时只瘦身重复的原始 source text,保留完整编辑/元数据和已读取证据;既有窗口/综合的元数据也不再为压缩而删除。原始输入、当前接收的证据、独立复核与历史复用保持不同信任层。阶段限制按当前生成判定,不用文件曾经受限的 sticky 标记阻止健康阶段缓存;文件总体受限和不完整窗口仍阻止完成。
|
|
44
|
+
|
|
45
|
+
`continuation.units` 保存真实 pendingReason、依赖与尝试;报告按 fileIds 关联并在附录保留完整身份。模型额度、超时、依赖阻断和读取分配等待不互相冒充。预算仍有余额不保证所有保护中的阶段可运行,全体大请求超过各自 floor 时不偷取其他阶段保留额,仍保留 partial。
|
|
46
|
+
|
|
35
47
|
### 大上下文回归与纠正记账
|
|
36
48
|
|
|
37
49
|
正式变更入口默认 `focusPrimaryContext:true`,纯准备阶段只附带直接关系、C#和调用方线索对应的完整批准上下文对象,序列化最多16KiB/输入四分之一,稳定原顺序保留新旧版本对象。其余来源保留批准目录按需读;不会因缺少检索能力隐藏正文。底层API默认false,设置false可回退。主文件、规则与需求不删减;即使旧输入能容纳整批上下文,也应用该附带上限。
|
package/README.md
CHANGED
|
@@ -239,9 +239,9 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
239
239
|
|
|
240
240
|
重启后可按原jobId读回保存记录,原运行标记interrupted,不自动继续外发。recovery给出已观察文件/窗口与未完成路径;它不是完整分母或新授权;宿主重启及自动恢复本身不授权外发,补审必须有当前明确授权并重新预览核对。新预览的runtime包含实际加载版本和模块位置,排查不同工作区时分别核对,不根据另一台机器推断。
|
|
241
241
|
|
|
242
|
-
变更审查的独立文件链默认最多 2 条并行;配置 `changesConcurrency` 为 1~4(1
|
|
242
|
+
变更审查的独立文件链默认最多 2 条并行;配置 `changesConcurrency` 为 1~4(1 回退串行),启用阶段续审时先扫描全体文件主审,再调度跨窗口/跨文件综合;恢复队列按真实依赖就绪情况排队,保留其他阶段的读取最低份额。配置 `maxRetrievalRounds` 为 0~50,默认为 3,限制每阶段检索请求回合数(不是成功读取次数;非法请求也会消耗回合)(0 只允许最终答复)。配置不是 preview 参数,不改变模型或来源授权;并发仍共享同一份读取/调用预算,额度不足时完成顺序可能影响哪些任务未完成,不能保证与串行逐项结果相同。提供方限流时应降低并发。复核可预取批准快照中候选锚点附近的同侧原文,仍允许追加反证读取并逐字校验;不复用主审结论或其他会话凭据。完整变更计划可容纳时仅生成一次再分发到窗口,否则保留原分窗口计划并在预览估算中体现。
|
|
243
243
|
|
|
244
|
-
大文件超出单次输入时按原行号分段。精确差异算不出时使用明确标注的包络,可能包含未变行,不声称精确归因。完整结束但 JSON 语法无效时会按当前阶段 schema
|
|
244
|
+
大文件超出单次输入时按原行号分段。精确差异算不出时使用明确标注的包络,可能包含未变行,不声称精确归因。完整结束但 JSON 语法无效时会按当前阶段 schema 最多请求两次明确纠正,记录额外调用;检索请求非法时整批不执行并明确纠正,回填采用可追溯的滚动工作集、必要时延后重复初始源码正文,保留精确编辑、风险/调用线索和已读证据回执,不虚构证据。主审与复核默认各最多 3 轮检索;到限后只允许最终答复,保留未完成范围,不把已用完预算当成审核通过。不会给错误输出补括号、猜字段或当成无缺陷。
|
|
245
245
|
|
|
246
246
|
### 显式续审与私有模型工件
|
|
247
247
|
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
# v0.87.
|
|
1
|
+
# v0.87.6 发布说明
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 让已知迁移路径更早成为本次复核证据
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
复核已能接收宿主校验的迁移契约,但此前默认种子只覆盖候选锚点附近,旧公开入口、旧委托实现与新入口仍需模型自行检索。本版为匹配候选增加有界成套取证,减少寻找已有导航坐标的往返;完整角色无法容纳时回退普通检索,不宣称已完成比较。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
种子沿用同一批准快照和读取总账;保留最多 8 次读取、16 KiB 种子输入、每次最多 200 行及最终证据复验余量。同套读取采用事务,失败或取消不发布部分回执。每次复核重新签发回执,不能借用既往主审/复核证据身份。
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## 诊断与安全边界
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
执行诊断记录迁移种子接纳/回退计数,不含源码正文;预取接纳不证明模型已调用,更不证明复核支持或新增因果成立。普通比较仍要求正确主文件与来源侧;跨文件仅限已校验契约,删除导出路由仍仅允许不确定归因。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
不增加模型调用额度、检索轮数、读取总额或并发;不降低复核证据门槛。未来同任务对照同时检查端到端与分段耗时、触发链、最小修复建议、覆盖及可读性,不能以减少调用或缩短报告代替质量提升。本版没有重新执行 r7364 真实模型审核,不承诺固定提速或确认两项既往候选。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## 验证与升级
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
新增迁移成套种子、事务回退、新鲜回执及预算边界回归,并通过全量测试与冻结标签四矩阵 CI 后发布。发布不自动安装或重启当前宿主。安装见 [README](README.md),门禁见 [开发说明](DEVELOPMENT.md)。
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
|
@@ -5,12 +5,23 @@ const brief = value => { const text = String(value ?? ''); return escape(text.le
|
|
|
5
5
|
const html = value => String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6
6
|
const stages = { primary: '主审', verification: '独立复核', synthesis: '综合', 'window-synthesis': '跨窗口综合', interaction: '跨文件综合' };
|
|
7
7
|
const stage = value => Object.hasOwn(stages, value) ? stages[value] : String(value ?? '未标明阶段');
|
|
8
|
+
const reasonKeys = ['failureCode', 'blockCode', 'code', 'reason', 'pendingReason'];
|
|
9
|
+
const hasReason = record => typeof record === 'string' ? record !== '' : reasonKeys.some(key => record?.[key] != null && record[key] !== '');
|
|
10
|
+
const unknownReason = label => /未知|原因未记录/.test(label);
|
|
11
|
+
const unitKeys = ['id', 'kind', 'fileIds', 'dependencies', 'status', 'attempts', 'pendingReason', 'stopped'];
|
|
12
|
+
const unitMetadata = unit => Object.fromEntries(unitKeys.filter(key => Object.hasOwn(unit ?? {}, key)).map(key => [key, unit[key]]));
|
|
8
13
|
|
|
9
14
|
function reason(record = {}) {
|
|
10
|
-
const value = typeof record === 'string' ? record : record?.failureCode ?? record?.blockCode ?? record?.code ?? record?.reason;
|
|
15
|
+
const value = typeof record === 'string' ? record : record?.failureCode ?? record?.blockCode ?? record?.code ?? record?.reason ?? record?.pendingReason;
|
|
11
16
|
const text = String(value ?? '');
|
|
12
17
|
// Dependency state must not be relabelled as an input-budget failure.
|
|
13
18
|
if (/dependenc|primary.*incomplete|member.*incomplete|prerequisite|依赖.*未完成|主审.*未完成/i.test(text)) return '依赖的主审或前置阶段未完成';
|
|
19
|
+
if (/STAGE_ATTEMPTS_EXHAUSTED/i.test(text)) return '阶段尝试次数达到上限(不等同于模型调用或读取额度耗尽)';
|
|
20
|
+
if (/STAGE_ALLOCATION_WAITING/i.test(text)) return '阶段等待读取分配(不代表总额度已耗尽)';
|
|
21
|
+
if (/STAGE_NO_PROGRESS/i.test(text)) return '阶段未取得进展而停止(未记录具体预算耗尽原因)';
|
|
22
|
+
if (/STAGE_INTERRUPTED/i.test(text)) return '阶段执行中断,尚待续审';
|
|
23
|
+
if (/STAGE_FAILED/i.test(text)) return '阶段执行失败';
|
|
24
|
+
if (/STAGE_TIMEOUT/i.test(text)) return '阶段执行超时';
|
|
14
25
|
if (/RETRIEVAL_ROUND_BUDGET_EXCEEDED|检索轮数|retrieval.*round.*(budget|limit)/i.test(text)) return '单阶段检索轮数达到上限(读取额度可能仍有余量)';
|
|
15
26
|
if (/READER_CALL_BUDGET_EXCEEDED|reader.*call.*(budget|limit)|读取调用.*(不足|耗尽|上限)/i.test(text)) return '阶段分配或共享读取调用次数不足';
|
|
16
27
|
if (/READER_OUTPUT_BUDGET_EXCEEDED|reader.*output.*(budget|limit)|读取.*字节|输出字节/i.test(text)) return '阶段分配或共享读取输出字节预算不足';
|
|
@@ -23,19 +34,25 @@ function reason(record = {}) {
|
|
|
23
34
|
return text ? '未知/其他原因(完整原文见附录)' : '原因未记录(未知;不推测为预算不足)';
|
|
24
35
|
}
|
|
25
36
|
|
|
26
|
-
function fileReasons(file, findings) {
|
|
37
|
+
function fileReasons(file, findings, units = []) {
|
|
27
38
|
const result = [];
|
|
28
39
|
const limits = [...rows(file?.retrievalLimits), ...rows(file?.groupRetrievalLimits)];
|
|
29
40
|
const failures = rows(file?.stageFailures);
|
|
30
|
-
|
|
41
|
+
// Compare phase-local evidence before rendering: a legacy unknown must not
|
|
42
|
+
// obscure a concrete continuation reason for that same stage. Raw values stay in the appendix.
|
|
43
|
+
const evidence = [...failures, ...limits, ...units.map(item => item.evidence)];
|
|
44
|
+
const phase = record => record?.phase ?? record?.stage ?? record?.kind;
|
|
45
|
+
const explained = record => unknownReason(reason(record)) && evidence.some(other => phase(record) != null && phase(other) === phase(record) && !unknownReason(reason(other)));
|
|
31
46
|
if (['pending', 'blocked', 'failed', 'cancelled', 'running'].includes(file?.status)) {
|
|
32
47
|
// A missing top-level code does not erase concrete phase/group evidence.
|
|
33
|
-
const
|
|
34
|
-
result.push(...
|
|
48
|
+
const causes = hasReason(file) && !explained(file) ? [file] : [...failures, ...limits].filter(record => hasReason(record) && !explained(record));
|
|
49
|
+
if (causes.length) result.push(...causes.map(reason));
|
|
50
|
+
else if (!units.length) result.push(reason(file));
|
|
35
51
|
}
|
|
36
|
-
for (const failure of failures) result.push(stage(
|
|
52
|
+
for (const failure of failures.filter(record => !explained(record))) result.push(stage(phase(failure)) + '未完成;' + reason(failure));
|
|
53
|
+
for (const item of units.filter(item => !explained(item.evidence))) result.push(stage(item.record.kind) + '未完成;' + reason(item.evidence));
|
|
37
54
|
if (file?.retrievalLimited || limits.length) {
|
|
38
|
-
result.push(...(limits.length ? limits.map(reason) : [reason(file)]));
|
|
55
|
+
result.push(...(limits.length ? limits.filter(record => !explained(record)).map(reason) : units.length ? [] : [reason(file)]));
|
|
39
56
|
}
|
|
40
57
|
if (rows(file?.windowCoverage?.pending).length) result.push('变更窗口尚未完成');
|
|
41
58
|
if (file?.continuationPending) result.push('阶段续审尚未完成');
|
|
@@ -53,9 +70,32 @@ function collect(report) {
|
|
|
53
70
|
if (record == null || typeof record !== 'object') return [];
|
|
54
71
|
return [...new Set([...(record.path != null ? [String(record.path)] : []), ...(record.fileId != null ? [identity(record.fileId)] : []), ...rows(record.fileIds).map(identity)])];
|
|
55
72
|
};
|
|
56
|
-
const
|
|
73
|
+
const allUnits = rows(report?.continuation?.units);
|
|
74
|
+
const byId = new Map(allUnits.filter(unit => unit?.id != null).map(unit => [unit.id, unit]));
|
|
75
|
+
const completedPrimary = rows(report?.continuation?.completedPrimary);
|
|
76
|
+
const dependency = id => {
|
|
77
|
+
const unit = byId.get(id);
|
|
78
|
+
if (unit) return { ...unitMetadata(unit), paths: paths(unit) };
|
|
79
|
+
if (typeof id === 'string' && id.startsWith('primary:')) {
|
|
80
|
+
const fileId = id.slice(8);
|
|
81
|
+
return { id, kind: 'primary', fileIds: [fileId], paths: [identity(fileId)], status: completedPrimary.includes(fileId) ? 'completed' : 'pending' };
|
|
82
|
+
}
|
|
83
|
+
return { id, status: 'unknown' };
|
|
84
|
+
};
|
|
85
|
+
const units = allUnits.flatMap((record, position) => {
|
|
86
|
+
if (!record || record.status === 'completed') return [];
|
|
87
|
+
const dependencies = rows(record.dependencies).map(dependency);
|
|
88
|
+
const blocked = dependencies.some(dep => dep.status !== 'completed');
|
|
89
|
+
// An explicit reason always wins. Dependency readiness is not a call-budget signal.
|
|
90
|
+
const evidence = !hasReason(record) && blocked ? { ...record, pendingReason: 'DEPENDENCY' } : record;
|
|
91
|
+
return [{ record, position, dependencies, evidence, paths: paths(record) }];
|
|
92
|
+
});
|
|
93
|
+
const unfinished = files.map(record => {
|
|
94
|
+
const associated = record?.fileId == null ? [] : units.filter(item => rows(item.record.fileIds).includes(record.fileId));
|
|
95
|
+
return { record, reasons: fileReasons(record, findings, associated), paths: record?.path != null ? [String(record.path)] : paths(record) };
|
|
96
|
+
}).filter(item => item.reasons.length);
|
|
57
97
|
const interactions = rows(report?.interactions).filter(record => record?.status !== 'completed').map(record => ({ record, reasons: [reason(record)], paths: paths(record) }));
|
|
58
|
-
return { unfinished, interactions, limitations: rows(report?.limitations), paths };
|
|
98
|
+
return { unfinished, interactions, units, limitations: rows(report?.limitations), paths };
|
|
59
99
|
}
|
|
60
100
|
|
|
61
101
|
function grouped(items) {
|
|
@@ -83,7 +123,7 @@ export function readabilityMarkdown(report = {}) {
|
|
|
83
123
|
const output = ['', '## 未完成范围与原因', '以下为处理状态与限制记录数,非缺陷数;同一文件可能涉及多个阶段,限制不自动表示待审或缺陷。'];
|
|
84
124
|
summary(output, '未完成条目', data.unfinished);
|
|
85
125
|
summary(output, '跨文件综合未完成', data.interactions);
|
|
86
|
-
if (!data.unfinished.length && !data.interactions.length) output.push(report?.partial || data.limitations.length ? '- 报告仍有部分完成标记或限制;未逐项记录待续审范围,不表示全部查完。' : '- 未记录未完成条目或跨文件综合;这不是正确性证明。');
|
|
126
|
+
if (!data.unfinished.length && !data.interactions.length) output.push(report?.status === 'partial' || report?.partial || data.limitations.length ? '- 报告仍有部分完成标记或限制;未逐项记录待续审范围,不表示全部查完。' : '- 未记录未完成条目或跨文件综合;这不是正确性证明。');
|
|
87
127
|
if (data.limitations.length) output.push('- 另有 ' + data.limitations.length + ' 条限制记录,按原文聚合保留在本报告限制附录;不将这些限制全部计入未完成范围。');
|
|
88
128
|
if (data.unfinished.length || data.interactions.length) output.push('- 以上仅列有界摘要;全部路径、身份、阶段及原始记录见本报告限制附录。');
|
|
89
129
|
output.push('', '## 下一步');
|
|
@@ -139,9 +179,13 @@ export function limitationDetailsMarkdown(report = {}) {
|
|
|
139
179
|
for (const item of items) output.push('- ' + (item.paths.length ? item.paths.map(escape).join('、') : '未标明文件身份') + ':' + item.reasons.map(escape).join(';'));
|
|
140
180
|
// State/reason metadata includes window IDs and pass statuses; full source and candidates already
|
|
141
181
|
// exist in the complete report JSON and must not be duplicated into this appendix.
|
|
142
|
-
const keys = ['fileId', 'path', 'groupId', 'fileIds', 'status', 'failureCode', 'blockCode', 'code', 'reason', 'stage', 'phase', 'stageFailures', 'retrievalLimited', 'retrievalLimits', 'groupRetrievalLimits', 'continuationPending', 'synthesisStatus', 'verificationStatus', 'windowCoverage', 'reviewPasses'];
|
|
182
|
+
const keys = ['fileId', 'path', 'groupId', 'fileIds', 'status', 'failureCode', 'blockCode', 'code', 'reason', 'pendingReason', 'stage', 'phase', 'stageFailures', 'retrievalLimited', 'retrievalLimits', 'groupRetrievalLimits', 'continuationPending', 'synthesisStatus', 'verificationStatus', 'windowCoverage', 'reviewPasses'];
|
|
143
183
|
output.push('以下仅为状态与原因元数据;源码、候选及其他完整字段见完整源报告 JSON。');
|
|
144
184
|
originalDetails(output, packed(items.map(item => Object.fromEntries(keys.filter(key => Object.hasOwn(item.record ?? {}, key)).map(key => [key, item.record[key]])))));
|
|
145
185
|
}
|
|
186
|
+
if (data.units.length) {
|
|
187
|
+
output.push('', '### 未完成阶段续审元数据', '按 continuation.units 原始索引保留对应阶段及依赖身份;record 为原始状态元数据,resolvedReason 仅为展示解释,不覆盖原始 pendingReason。');
|
|
188
|
+
originalDetails(output, data.units.map(item => ({ position: item.position, record: unitMetadata(item.record), paths: item.paths, resolvedReason: reason(item.evidence), dependencies: item.dependencies })));
|
|
189
|
+
}
|
|
146
190
|
return output;
|
|
147
191
|
}
|
package/src/retrieval-loop.mjs
CHANGED
|
@@ -7,16 +7,33 @@ export { RETRIEVAL_INSTRUCTIONS };
|
|
|
7
7
|
|
|
8
8
|
function compactFollowup(input) {
|
|
9
9
|
const p = JSON.parse(input);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
let removed = false;
|
|
11
|
+
const strip = (source, keys = ['text']) => {
|
|
12
|
+
if (!source || typeof source !== 'object') return;
|
|
13
|
+
for (const key of keys) if (typeof source[key] === 'string' && source[key].length) {
|
|
14
|
+
delete source[key];
|
|
15
|
+
removed = true;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const file = value => {
|
|
19
|
+
if (!value || typeof value !== 'object') return;
|
|
20
|
+
// Full snapshot files use left/right; windows use old/new. Never traverse
|
|
21
|
+
// arbitrary claims, exact edits, or retrieved results looking for text.
|
|
22
|
+
for (const side of ['left', 'right', 'old', 'new']) strip(value[side]);
|
|
23
|
+
};
|
|
24
|
+
const envelope = value => {
|
|
25
|
+
file(value.file);
|
|
26
|
+
for (const key of ['relatedFiles', 'files', 'windows']) {
|
|
27
|
+
if (Array.isArray(value[key])) value[key].forEach(file);
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value.context)) value.context.forEach(item => strip(item, ['text', 'oldText']));
|
|
30
|
+
};
|
|
31
|
+
envelope(p);
|
|
32
|
+
if (Array.isArray(p.reviews)) p.reviews.forEach(envelope);
|
|
33
|
+
if (!removed) return input;
|
|
34
|
+
p.followupSourceNotice = 'Repeated initial source bodies deferred; identity, sides and original coordinates remain. Use approved catalog reader operations for source evidence. Previously supplied bodies are not independent reads or complete coverage; deferred/omitted bodies are not evidence. Retrieved results and receipts are unchanged.';
|
|
35
|
+
const compact = JSON.stringify(p);
|
|
36
|
+
return Buffer.byteLength(compact) < Buffer.byteLength(input) ? compact : input;
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
export function retrievalRequest(request, scope, retrieved = []) {
|
|
@@ -105,7 +122,7 @@ export async function retrievalLoop(model, request, scope, { correctProtocol = f
|
|
|
105
122
|
let parsed = pendingResponse;
|
|
106
123
|
if (!parsed) {
|
|
107
124
|
let { input, instructions } = nextRequest(retrieved);
|
|
108
|
-
if (correctProtocol &&
|
|
125
|
+
if (correctProtocol && round > 0 && !fits({ ...request, input, instructions })) input = compactFollowup(input);
|
|
109
126
|
if (!fits({ ...request, input, instructions })) throw new Error('Retrieval input budget exceeded');
|
|
110
127
|
request.signal?.throwIfAborted();
|
|
111
128
|
beforeCall();
|
|
@@ -174,7 +191,7 @@ export async function retrievalLoop(model, request, scope, { correctProtocol = f
|
|
|
174
191
|
next.instructions = request.instructions + '\n' + RETRIEVAL_INSTRUCTIONS + FINAL_SUFFIX;
|
|
175
192
|
}
|
|
176
193
|
next.input = JSON.stringify(value);
|
|
177
|
-
if (correctProtocol) next.input = compactFollowup(next.input);
|
|
194
|
+
if (correctProtocol && !fits(next)) next.input = compactFollowup(next.input);
|
|
178
195
|
return next;
|
|
179
196
|
}, signal: request.signal,
|
|
180
197
|
});
|
|
@@ -186,7 +203,7 @@ export async function retrievalLoop(model, request, scope, { correctProtocol = f
|
|
|
186
203
|
let candidate = [...retrieved, item];
|
|
187
204
|
let next = nextRequest(candidate);
|
|
188
205
|
if (correctProtocol) {
|
|
189
|
-
while (retrieved.length && !fits({ ...next, input: compactFollowup(next.input) })) {
|
|
206
|
+
while (retrieved.length && !fits(next) && !fits({ ...next, input: compactFollowup(next.input) })) {
|
|
190
207
|
retrieved.shift();
|
|
191
208
|
await request.onProtocolEvent?.({ action: 'prior-retrieval-deferred', notice: 'Prior returned fragment no longer in current prompt; reread approved source for evidence.' });
|
|
192
209
|
candidate = [...retrieved, item];
|
|
@@ -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 表示启用检索的某阶段初始调用,不仅是主审;具体阶段以阶段统计为准。');
|
|
@@ -30,24 +30,39 @@ export function createReviewReaderAllocation(root, units, { maxResumeAttempts =
|
|
|
30
30
|
return { ...remaining, remainingCalls: Math.max(0, remaining.remainingCalls - protectedCalls), remainingOutputBytes: Math.max(0, remaining.remainingOutputBytes - protectedBytes) };
|
|
31
31
|
};
|
|
32
32
|
const check = (state, cost) => { const remaining = available(state); if (cost.calls > remaining.remainingCalls) fail('call'); if (cost.outputBytes > remaining.remainingOutputBytes) fail('output'); };
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Only the host knows dependencies. An explicit COMPLETE current ready-ID set
|
|
34
|
+
// filters FIFO opportunities, never the protected floors in available().
|
|
35
|
+
const readyIds = eligibleIds => {
|
|
36
|
+
if (eligibleIds === undefined) return null;
|
|
37
|
+
if (!(Array.isArray(eligibleIds) || eligibleIds instanceof Set) || eligibleIds.size > states.size || eligibleIds.length > states.size) throw new Error('Invalid reader resume eligibility');
|
|
38
|
+
const ready = new Set(eligibleIds);
|
|
39
|
+
if ([...ready].some(id => !states.has(id))) throw new Error('Invalid reader resume eligibility');
|
|
40
|
+
return ready;
|
|
41
|
+
};
|
|
42
|
+
const resumeWithCost = (id, costForAttempt, { eligibleIds, exact = false } = {}) => {
|
|
43
|
+
const state = states.get(id), ready = readyIds(eligibleIds);
|
|
35
44
|
if (!state || state.released || !state.yielded) throw new Error('Reader allocation unit unavailable');
|
|
36
45
|
if (state.resumeAttempts >= maxResumeAttempts) return { resumed: false, reason: 'READER_RESUME_ATTEMPTS_EXHAUSTED', attempts: state.resumeAttempts };
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
const waiting = () => ({ resumed: false, reason: 'READER_RESUME_WAITING', attempts: state.resumeAttempts });
|
|
47
|
+
// One first exact admission may use existing own-floor/cache capacity. Later
|
|
48
|
+
// opportunities require a release; readiness changes cannot reset this gate.
|
|
49
|
+
const opportunity = row => row.yieldRevision < releaseRevision || (exact && row.resumeAttempts === 0);
|
|
50
|
+
const head = yielded.find(key => { const row = states.get(key); return (!ready || ready.has(key)) && row && !row.released && row.yielded && row.resumeAttempts < maxResumeAttempts && opportunity(row); });
|
|
51
|
+
if (head !== id || !opportunity(state)) return waiting();
|
|
52
|
+
let failure = null;
|
|
41
53
|
try {
|
|
42
54
|
const cost = costForAttempt();
|
|
43
55
|
if (!cost || !Number.isSafeInteger(cost.calls) || cost.calls < 0 || !Number.isSafeInteger(cost.outputBytes) || cost.outputBytes < 0) throw new Error('Reader allocation preflight unavailable');
|
|
44
56
|
check(state, cost);
|
|
45
|
-
state.yielded = false; state.pendingReason = null; yielded.splice(yielded.indexOf(id), 1);
|
|
46
|
-
return { resumed: true, reason: null, attempts: state.resumeAttempts };
|
|
47
57
|
} catch (error) {
|
|
48
58
|
if (!['READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED'].includes(error?.code)) throw error;
|
|
49
|
-
|
|
59
|
+
failure = error.code;
|
|
50
60
|
}
|
|
61
|
+
state.resumeAttempts++; state.yieldRevision = releaseRevision;
|
|
62
|
+
yielded.splice(yielded.indexOf(id), 1);
|
|
63
|
+
if (failure) { yielded.push(id); return { resumed: false, reason: failure, attempts: state.resumeAttempts }; }
|
|
64
|
+
state.yielded = false; state.pendingReason = null;
|
|
65
|
+
return { resumed: true, reason: null, attempts: state.resumeAttempts };
|
|
51
66
|
};
|
|
52
67
|
const scopes = new Map();
|
|
53
68
|
return Object.freeze({
|
|
@@ -99,17 +114,17 @@ export function createReviewReaderAllocation(root, units, { maxResumeAttempts =
|
|
|
99
114
|
state.yielded = true; state.yieldRevision = releaseRevision; state.pendingReason = reason; yielded.push(id);
|
|
100
115
|
return { yielded: true, attempts: state.resumeAttempts };
|
|
101
116
|
},
|
|
102
|
-
resume(id, requests = []) {
|
|
117
|
+
resume(id, requests = [], { eligibleIds } = {}) {
|
|
103
118
|
const state = states.get(id);
|
|
104
119
|
if (!state || state.released || !state.yielded) throw new Error('Reader allocation unit unavailable');
|
|
105
120
|
if (!Array.isArray(requests) || !requests.length) return { resumed: false, reason: 'READER_RESUME_REQUESTS_REQUIRED', attempts: state.resumeAttempts };
|
|
106
|
-
return resumeWithCost(id, () => root.preflight(requests));
|
|
121
|
+
return resumeWithCost(id, () => root.preflight(requests), { eligibleIds, exact: true });
|
|
107
122
|
},
|
|
108
123
|
// Scalar admission is only a retry opportunity, never authority to read.
|
|
109
124
|
// Every actual read still goes through exact preflight and root accounting.
|
|
110
|
-
resumeBudget(id, { requiredCalls = 1, requiredOutputBytes = 1 } = {}) {
|
|
125
|
+
resumeBudget(id, { requiredCalls = 1, requiredOutputBytes = 1, eligibleIds } = {}) {
|
|
111
126
|
if (![requiredCalls, requiredOutputBytes].every(value => Number.isSafeInteger(value) && value >= 0) || requiredCalls + requiredOutputBytes === 0) throw new Error('Invalid reader resume budget');
|
|
112
|
-
return resumeWithCost(id, () => ({ calls: requiredCalls, outputBytes: requiredOutputBytes }));
|
|
127
|
+
return resumeWithCost(id, () => ({ calls: requiredCalls, outputBytes: requiredOutputBytes }), { eligibleIds });
|
|
113
128
|
},
|
|
114
129
|
release(id) { const state = states.get(id); if (!state) return; if (state.inTransaction) throw new Error('Reader allocation transaction active'); if (!state.released) { state.released = true; releaseRevision++; } },
|
|
115
130
|
summary() { const usage = root.usage(); return { units: states.size, releasedUnits: [...states.values()].filter(state => state.released).length, calls: usage.calls, outputBytes: usage.outputBytes, ...(usage.cacheHits !== undefined ? { cacheHits: usage.cacheHits } : {}) }; },
|
package/src/review.mjs
CHANGED
|
@@ -196,7 +196,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
196
196
|
const generationCache = createStageGenerationCache(input, options.resumeState?.stageGenerations ?? []);
|
|
197
197
|
const acceptedStages = new Map(generationCache.records().filter(record => record.phase === 'primary').map(record => [record.key, { generation: record.generation, record, restored: true }]));
|
|
198
198
|
const primarySuspensions = new Map(), correctedStages = new Set();
|
|
199
|
-
const deferredStages = [], yieldedUnits = new Set(), fileDeadlines = new Map(), primaryStageKeys = new Map();
|
|
199
|
+
const deferredStages = [], yieldedUnits = new Set(), queuedStageUnits = new Set(), fileDeadlines = new Map(), primaryStageKeys = new Map();
|
|
200
200
|
const checkpointStages = () => {
|
|
201
201
|
if (!options.enableStageContinuation) return;
|
|
202
202
|
report.stageGenerations = generationCache.records().filter(record => record.phase === 'primary');
|
|
@@ -204,7 +204,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
204
204
|
let readerAllocation;
|
|
205
205
|
const stageScope = id => readerAllocation
|
|
206
206
|
? configureWireBudget(readerAllocation.scope(id), { nativeOutputTools: options.nativeOutputTools === true }) : scope;
|
|
207
|
-
const releaseStage = id => { if (!yieldedUnits.has(id)) readerAllocation?.release(id); };
|
|
207
|
+
const releaseStage = id => { if (!yieldedUnits.has(id) && !queuedStageUnits.has(id) && !deferredStages.some(stage => !stage.done && stage.allocationId === id)) readerAllocation?.release(id); };
|
|
208
208
|
const contextSeeds = (reader, request, existing = [], buildRequest = records => retrievalRequest(request, reader, records)) => {
|
|
209
209
|
if (!evidenceContext || !reader || maxRetrievalRounds === 0) return [];
|
|
210
210
|
const payload = JSON.parse(request.input), catalog = reader.catalog();
|
|
@@ -282,7 +282,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
282
282
|
}
|
|
283
283
|
if (signal?.aborted) { state.status = 'cancelled'; state.reason = '用户取消'; return; }
|
|
284
284
|
const assignedGroup = [...groupPrepared].find(([, value]) => value.fileIds.includes(file.id));
|
|
285
|
-
if (!assignedGroup && (report.modelCalls >= maxCalls || options.remainingCalls?.() <= 0)) { state.reason = '模型调用预算耗尽'; return; }
|
|
285
|
+
if (!assignedGroup && (report.modelCalls >= maxCalls || options.remainingCalls?.() <= 0)) { state.reason = '模型调用预算耗尽'; state.failureCode = 'PROJECT_MODEL_CALL_LIMIT'; return; }
|
|
286
286
|
const prepared = preparePrimaryInput(input, file, { instructions, scope, grouping: report.grouping, maxInputBytes, riskPlanningMode: options.riskPlanningMode, focusPrimaryContext: options.focusPrimaryContext === true, enableRiskPlanning: options.enableRiskPlanning, skipSmallRiskPlans: options.skipSmallRiskPlans });
|
|
287
287
|
const { changes, payload } = prepared;
|
|
288
288
|
Object.assign(state, prepared.metadata);
|
|
@@ -300,8 +300,9 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
300
300
|
if (report.modelCalls >= maxCalls) throw new Error('Model call budget exceeded');
|
|
301
301
|
report.modelCalls++;
|
|
302
302
|
};
|
|
303
|
-
let activeReader = scope, activeStage = 'primary';
|
|
303
|
+
let activeReader = scope, activeStage = 'primary', activeStageLimited = false;
|
|
304
304
|
const onBudgetLimited = info => {
|
|
305
|
+
activeStageLimited = true;
|
|
305
306
|
if (activeStage !== 'primary') (state.secondaryRetrievalLimits ??= []).push({ phase: activeStage, ...info });
|
|
306
307
|
state.retrievalLimited = true;
|
|
307
308
|
(state.retrievalLimits ??= []).push(info);
|
|
@@ -404,6 +405,22 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
404
405
|
const phase=batch.interaction?'file-interaction':batch.synthesis?'window-synthesis':'primary';
|
|
405
406
|
try{
|
|
406
407
|
activeStage = phase;
|
|
408
|
+
activeStageLimited = phase === 'primary' && !!groupedGeneration?.groupLimits?.length;
|
|
409
|
+
// Seal the complete primary sweep before spending reads on secondary synthesis.
|
|
410
|
+
// A scheduled dependency is not a reader failure and does not consume a retry.
|
|
411
|
+
if (options.enableStageContinuation && !retry && (batch.synthesis || batch.interaction)) {
|
|
412
|
+
if (!deferredStages.some(stage => stage.key === key)) deferredStages.push({
|
|
413
|
+
key, allocationId, fileId: file.id, phase: batch.synthesis ? 'synthesis' : 'interaction',
|
|
414
|
+
fileIds: batch.interaction?.fileIds ?? [file.id], attempts: 0, requests: [], reason: 'STAGE_DEPENDENCY_PENDING',
|
|
415
|
+
run: () => invoke(next => executeStage(batch, batchIndex, next, true), { instructions, input: payload }, timeoutMs > 0 ? Math.max(1, fileDeadlines.get(file.id) - performance.now()) : 0, signal),
|
|
416
|
+
});
|
|
417
|
+
queuedStageUnits.add(allocationId);
|
|
418
|
+
if (options.enableVerification) queuedStageUnits.add('verification:' + file.id);
|
|
419
|
+
if (batch.synthesis) state.continuationPending = true;
|
|
420
|
+
if (batch.synthesis) state.synthesisStatus = 'pending';
|
|
421
|
+
if (batch.interaction) Object.assign(report.interactions.find(item => item.groupId === batch.interaction.id), { status: 'pending', reason: 'STAGE_DEPENDENCY_PENDING' });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
407
424
|
if (options.enableStageContinuation && (batch.synthesis || batch.interaction) && (batch.interaction?.fileIds ?? [file.id]).some(id => deferredStages.some(stage => stage.fileId === id && stage.phase === 'primary' && !acceptedStages.has(stage.key)))) {
|
|
408
425
|
defer(Object.assign(new Error('Stage is waiting for accepted primary generations'), { code: 'READER_CALL_BUDGET_EXCEEDED' })); return;
|
|
409
426
|
}
|
|
@@ -560,7 +577,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
560
577
|
}
|
|
561
578
|
request.signal.throwIfAborted();
|
|
562
579
|
if(rounds>1)generated.findings.forEach(f=>{f.reviewRound=batch.round;});
|
|
563
|
-
if (options.enableStageContinuation && !priorStage && !
|
|
580
|
+
if (options.enableStageContinuation && !priorStage && !activeStageLimited) {
|
|
564
581
|
const record = { key, fileId: file.id, phase, round: batch.round, windowIds: batch.windowIds ?? [], ...(batch.interaction ? { groupId: batch.interaction.id } : {}), generation: { findings: structuredClone(generated.findings), limitations: structuredClone(generated.limitations) } };
|
|
565
582
|
const admission = generationCache.admit(record);
|
|
566
583
|
if (admission.accepted) {
|
|
@@ -591,6 +608,13 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
591
608
|
const verdicts = await verificationLoop(trackedModel, { ...request, onProtocolEvent, reviewPhase: 'verification' }, generated.findings, stageScope('verification:' + file.id), {
|
|
592
609
|
correctProtocol:options.correctProtocol===true,maxRounds:maxRetrievalRounds,maxInputBytes, beforeCall, onBudgetLimited, yieldOnBudget: options.enableStageContinuation === true,
|
|
593
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
|
+
},
|
|
594
618
|
onRetrieved: records => { (state.retrievalAudit ??= []).push(...records); },
|
|
595
619
|
onTruncated: () => { result.limitations.push({ fileId: file.id, text: '复核检索结果达到数量上限' }); },
|
|
596
620
|
});
|
|
@@ -607,19 +631,19 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
607
631
|
}
|
|
608
632
|
}
|
|
609
633
|
if (acceptedStages.has(key)) acceptedStages.get(key).completed = !options.enableVerification || generated.findings.every(finding => finding.verification?.status === 'completed');
|
|
610
|
-
if (batch.synthesis) state.synthesisStatus = 'completed';
|
|
634
|
+
if (batch.synthesis) state.synthesisStatus = activeStageLimited ? 'pending' : 'completed';
|
|
611
635
|
if (batch.interaction) {
|
|
612
636
|
const interaction=report.interactions.find(p => p.groupId === batch.interaction.id);
|
|
613
637
|
if (combinedInteraction) Object.assign(interaction, { method: 'group-primary-validated-channel', channelGroupId: combinedInteraction.groupId });
|
|
614
|
-
if(rounds>1){interaction.completedRounds??=[];interaction.completedRounds.push(batch.round);interaction.reviewRounds=rounds;}
|
|
615
|
-
if(batch.round===rounds)interaction.status='completed';
|
|
638
|
+
if(rounds>1){interaction.completedRounds??=[];if(!activeStageLimited)interaction.completedRounds.push(batch.round);interaction.reviewRounds=rounds;}
|
|
639
|
+
if(batch.round===rounds) { interaction.status = activeStageLimited || rounds > 1 && new Set(interaction.completedRounds).size !== rounds ? 'pending' : 'completed'; if (activeStageLimited) interaction.reason = state.secondaryRetrievalLimits?.at(-1)?.code ?? 'STAGE_FAILED'; }
|
|
616
640
|
}
|
|
617
641
|
if (state.windowCoverage && batch.round === 1) {
|
|
618
642
|
state.windowCoverage.completed.push(...batch.windowIds);
|
|
619
643
|
state.windowCoverage.pending = state.windowCoverage.pending.filter(id => !batch.windowIds.includes(id));
|
|
620
644
|
}
|
|
621
645
|
if(rounds>1)state.reviewPasses[batchIndex].status='completed';
|
|
622
|
-
if (retry) { const pending = deferredStages.find(stage => stage.key === key); if (pending) pending.succeeded = !options.enableVerification || generated.findings.every(finding => finding.verification?.status === 'completed'); }
|
|
646
|
+
if (retry) { const pending = deferredStages.find(stage => stage.key === key); if (pending) { pending.succeeded = !activeStageLimited && (!options.enableVerification || generated.findings.every(finding => finding.verification?.status === 'completed')); if (activeStageLimited) pending.reason = state.retrievalLimits?.at(-1)?.code ?? 'STAGE_FAILED'; } }
|
|
623
647
|
await options.onProgress?.({stage:'window-complete',snapshotId:input.id,fileId:file.id,path:file.path,windowIds:batch.windowIds,round:batch.round,findings:generated.findings,windowCoverage:state.windowCoverage});
|
|
624
648
|
}catch(error){
|
|
625
649
|
enclosingRequest.signal.throwIfAborted();
|
|
@@ -740,7 +764,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
740
764
|
}, dependencies, fileConcurrency);
|
|
741
765
|
if (options.enableStageContinuation === true && deferredStages.length && !signal.aborted) {
|
|
742
766
|
const primaryDone = input.files.filter(file => file.eligibility === 'reviewable' && (options.resumeState?.completed?.some(row => row.fileId === file.id) || primaryStageKeys.get(file.id)?.length && primaryStageKeys.get(file.id).every(key => acceptedStages.has(key)))).map(file => file.id);
|
|
743
|
-
const units = deferredStages.map((stage, index) => ({ id: hash(stage.key), kind: stage.phase, fileIds: stage.fileIds,
|
|
767
|
+
const units = deferredStages.map((stage, index) => ({ id: hash(stage.key), kind: stage.phase, fileIds: stage.fileIds, pendingReason: stage.reason,
|
|
744
768
|
dependencies: stage.phase === 'primary'
|
|
745
769
|
? deferredStages.slice(0, index).filter(other => other.phase === 'primary' && other.fileId === stage.fileId).map(other => hash(other.key))
|
|
746
770
|
: stage.fileIds.flatMap(id => { const pending = deferredStages.filter(other => other.phase === 'primary' && other.fileId === id); return pending.length ? pending.map(other => hash(other.key)) : ['primary:' + id]; }) }));
|
|
@@ -753,17 +777,26 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
753
777
|
}
|
|
754
778
|
for (let pass = 0; pass < 2; pass++) {
|
|
755
779
|
let progressed = false;
|
|
756
|
-
for (const stage of deferredStages) {
|
|
780
|
+
for (const stage of [...deferredStages].sort((a, b) => (a.phase === 'primary' ? 0 : a.phase === 'verification' ? 1 : 2) - (b.phase === 'primary' ? 0 : b.phase === 'verification' ? 1 : 2))) {
|
|
757
781
|
if (stage.done || !continuation.ready().some(unit => unit.id === hash(stage.key))) continue;
|
|
758
|
-
if (performance.now() >= fileDeadlines.get(stage.fileId) || signal.aborted || report.modelCalls >= maxCalls
|
|
759
|
-
|
|
760
|
-
|
|
782
|
+
if (performance.now() >= fileDeadlines.get(stage.fileId) || signal.aborted || report.modelCalls >= maxCalls || options.remainingCalls?.() <= 0) {
|
|
783
|
+
stage.reason = signal.aborted ? 'STAGE_CANCELLED' : performance.now() >= fileDeadlines.get(stage.fileId) ? 'STAGE_TIMEOUT' : 'PROJECT_MODEL_CALL_LIMIT';
|
|
784
|
+
continuation.stop(hash(stage.key), stage.reason); continue;
|
|
785
|
+
}
|
|
786
|
+
const eligibleIds = [...new Set(deferredStages.filter(candidate => !candidate.done && continuation.ready().some(unit => unit.id === hash(candidate.key))).map(candidate => candidate.allocationId))];
|
|
787
|
+
const admitted = !yieldedUnits.has(stage.allocationId) ? { resumed: true } : stage.requests.length ? readerAllocation.resume(stage.allocationId, stage.requests, { eligibleIds }) : readerAllocation.resumeBudget(stage.allocationId, { eligibleIds });
|
|
788
|
+
if (!admitted.resumed) { stage.reason = ['READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED'].includes(admitted.reason) ? admitted.reason : 'STAGE_ALLOCATION_WAITING'; continue; }
|
|
761
789
|
yieldedUnits.delete(stage.allocationId);
|
|
790
|
+
if (!stage.allocationId.startsWith('verification:') || !deferredStages.some(other => other !== stage && !other.done && other.fileId === stage.fileId)) queuedStageUnits.delete(stage.allocationId);
|
|
762
791
|
const token = continuation.start(hash(stage.key), { signal });
|
|
763
792
|
stage.attempts++; await stage.run();
|
|
764
793
|
progressed = true;
|
|
765
|
-
if (stage.succeeded && !yieldedUnits.has(stage.allocationId)) { stage.done = true; continuation.complete(hash(stage.key), token.attempt, { signal }); if (!deferredStages.some(other => other !== stage && !other.done && other.allocationId === stage.allocationId)) readerAllocation
|
|
766
|
-
else { continuation.yield(hash(stage.key),
|
|
794
|
+
if (stage.succeeded && !yieldedUnits.has(stage.allocationId)) { stage.done = true; continuation.complete(hash(stage.key), token.attempt, { signal }); if (!deferredStages.some(other => other !== stage && !other.done && other.allocationId === stage.allocationId) && !queuedStageUnits.has(stage.allocationId)) readerAllocation?.release(stage.allocationId); }
|
|
795
|
+
else { const cause = stage.reason === 'STAGE_DEPENDENCY_PENDING' ? 'STAGE_FAILED' : stage.reason ?? 'STAGE_FAILED'; continuation.yield(hash(stage.key), cause); if (!yieldedUnits.has(stage.allocationId)) continuation.stop(hash(stage.key), cause); }
|
|
796
|
+
if (!deferredStages.some(other => !other.done && other.fileId === stage.fileId)) {
|
|
797
|
+
queuedStageUnits.delete('verification:' + stage.fileId);
|
|
798
|
+
releaseStage('verification:' + stage.fileId);
|
|
799
|
+
}
|
|
767
800
|
}
|
|
768
801
|
if (!progressed) break;
|
|
769
802
|
}
|
|
@@ -771,6 +804,10 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
771
804
|
for (const [id, keys] of primaryStageKeys) if (!primaryDone.includes(id) && keys.length && keys.every(key => acceptedStages.has(key))) primaryDone.push(id);
|
|
772
805
|
report.stageContinuation = continuation.summary();
|
|
773
806
|
report.continuation = continuation.checkpoint();
|
|
807
|
+
for (const unit of report.continuation.units) if (unit.status !== 'completed') {
|
|
808
|
+
const stage = deferredStages.find(item => hash(item.key) === unit.id);
|
|
809
|
+
if (stage?.reason && !unit.stopped) unit.pendingReason = stage.reason;
|
|
810
|
+
}
|
|
774
811
|
report.continuationOptions = continuationOptions;
|
|
775
812
|
for (const file of input.files) {
|
|
776
813
|
const result = fileResults[input.files.indexOf(file)];
|
|
@@ -780,7 +817,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
780
817
|
checkpointStages();
|
|
781
818
|
await options.onResumeCheckpoint?.({ ...report, files: fileResults.map(result => result.state), findings: fileResults.flatMap(result => result.findings), limitations: [...report.limitations, ...fileResults.flatMap(result => result.limitations)] });
|
|
782
819
|
}
|
|
783
|
-
for (const id of yieldedUnits) readerAllocation?.release(id);
|
|
820
|
+
for (const id of new Set([...yieldedUnits, ...queuedStageUnits])) readerAllocation?.release(id);
|
|
784
821
|
if (checkpointFailed) throw checkpointFailure;
|
|
785
822
|
for (const result of fileResults) if (result?.state.continuationPending === false) { const state = result.state; state.resumeEligible = state.status === 'completed' && !state.stageFailures?.length && !state.reviewPasses?.some(pass => pass.status !== 'completed') && !state.windowCoverage?.pending?.length && !state.retrievalLimited && result.findings.every(finding => finding.verification?.status === 'completed') && report.interactions.filter(item => item.fileIds?.includes(state.fileId)).every(item => item.status === 'completed'); }
|
|
786
823
|
report.files = fileResults.map(result => result.state);
|
|
@@ -795,7 +832,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
795
832
|
report.selection = { total: input.files.length, reviewable: reviewableCount, excluded: input.files.filter(f => f.eligibility === 'excluded').length, blocked: input.files.filter(f => f.eligibility === 'blocked').length };
|
|
796
833
|
if (!reviewableCount) report.limitations.push({ text: input.files.length ? '没有可审查文件;排除或阻塞不表示代码审核通过。' : '未选择变更文件,没有执行代码审核。' });
|
|
797
834
|
const unfinished = report.files.some(f => !['completed', 'excluded'].includes(f.status));
|
|
798
|
-
report.status = signal?.aborted ? 'cancelled' : unfinished || report.limitations.length ? 'partial' : 'completed';
|
|
835
|
+
report.status = signal?.aborted ? 'cancelled' : unfinished || report.limitations.length || report.stageContinuation?.pending || report.files.some(file => file.synthesisStatus && file.synthesisStatus !== 'completed') ? 'partial' : 'completed';
|
|
799
836
|
if (report.files.some(f => f.status === 'failed') && !report.files.some(f => f.status === 'completed')) report.status = signal?.aborted ? 'cancelled' : 'failed';
|
|
800
837
|
report.coverage = Object.fromEntries(['completed', 'excluded', 'blocked', 'failed', 'cancelled', 'pending'].map(s => [s, report.files.filter(f => f.status === s).length]));
|
|
801
838
|
if (scope) {
|
|
@@ -804,7 +841,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
804
841
|
}
|
|
805
842
|
report.regressionCoverage = Object.fromEntries(['introduced', 'preexisting', 'uncertain', 'unassessed'].map(classification => [classification, report.findings.filter(f => (f.verification?.regression?.classification ?? 'unassessed') === classification).length]));
|
|
806
843
|
report.findingGroups = groupDuplicateFindings(report.findings);
|
|
807
|
-
for (const state of report.files) state.resumeEligible = state.status === 'completed' && !state.stageFailures?.length && !state.reviewPasses?.some(pass => pass.status !== 'completed') && !state.windowCoverage?.pending?.length && !state.retrievalLimited && report.findings.filter(f => f.fileId === state.fileId).every(f => f.verification?.status === 'completed') && report.interactions.filter(item => item.fileIds?.includes(state.fileId)).every(item => item.status === 'completed');
|
|
844
|
+
for (const state of report.files) state.resumeEligible = state.status === 'completed' && !state.continuationPending && (!state.synthesisStatus || state.synthesisStatus === 'completed') && !state.stageFailures?.length && !state.reviewPasses?.some(pass => pass.status !== 'completed') && !state.windowCoverage?.pending?.length && !state.retrievalLimited && report.findings.filter(f => f.fileId === state.fileId).every(f => f.verification?.status === 'completed') && report.interactions.filter(item => item.fileIds?.includes(state.fileId)).every(item => item.status === 'completed');
|
|
808
845
|
await options.onResumeCheckpoint?.(report);
|
|
809
846
|
report.followup = coverageFollowup(report);
|
|
810
847
|
if (scope) report.readerBudget = { ...scope.usage(), ...scope.budget(), ...readerLimits };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
const KINDS = new Set(['primary', 'verification', 'synthesis', 'interaction']);
|
|
3
|
-
const REASONS = new Set(['READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED', 'STAGE_INTERRUPTED', 'STAGE_CANCELLED', 'STAGE_TIMEOUT', 'STAGE_NO_PROGRESS', 'STAGE_ATTEMPTS_EXHAUSTED', 'STAGE_FAILED']);
|
|
3
|
+
const REASONS = new Set(['READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED', 'STAGE_INTERRUPTED', 'STAGE_CANCELLED', 'STAGE_TIMEOUT', 'STAGE_NO_PROGRESS', 'STAGE_ATTEMPTS_EXHAUSTED', 'STAGE_FAILED', 'STAGE_DEPENDENCY_PENDING', 'STAGE_ALLOCATION_WAITING', 'PROJECT_MODEL_CALL_LIMIT', 'RETRIEVAL_ROUND_BUDGET_EXCEEDED', 'RETRIEVAL_FINAL_RESPONSE_REQUIRED']);
|
|
4
4
|
const fail = code => Object.assign(new Error(code), { code });
|
|
5
5
|
const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().filter(key => value[key] !== undefined).map(key => [key, canonical(value[key])])) : value;
|
|
6
6
|
const digest = value => createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex');
|
|
@@ -15,7 +15,7 @@ export function createStageContinuation(snapshot, options, { completedPrimary =
|
|
|
15
15
|
const primary = [...completedPrimary].sort(), states = new Map();
|
|
16
16
|
for (const unit of units) {
|
|
17
17
|
if (!unit || typeof unit.id !== 'string' || !unit.id || unit.id.length > 500 || states.has(unit.id) || unit.id.startsWith('primary:') || !KINDS.has(unit.kind) || !Array.isArray(unit.fileIds) || !unit.fileIds.length || new Set(unit.fileIds).size !== unit.fileIds.length || unit.fileIds.some(id => !fileIds.has(id)) || !Array.isArray(unit.dependencies ?? []) || new Set(unit.dependencies ?? []).size !== (unit.dependencies ?? []).length) throw fail('INVALID_STAGE_UNIT');
|
|
18
|
-
states.set(unit.id, { id: unit.id, kind: unit.kind, fileIds: [...unit.fileIds], dependencies: [...(unit.dependencies ?? [])], status: 'pending', attempts: 0, pendingReason: null, stopped: false });
|
|
18
|
+
states.set(unit.id, { id: unit.id, kind: unit.kind, fileIds: [...unit.fileIds], dependencies: [...(unit.dependencies ?? [])], status: 'pending', attempts: 0, pendingReason: REASONS.has(unit.pendingReason) ? unit.pendingReason : null, stopped: false });
|
|
19
19
|
}
|
|
20
20
|
const visit = (id, visiting = new Set(), visited = new Set()) => {
|
|
21
21
|
if (visited.has(id)) return;
|
|
@@ -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 [];
|