@feng0531/dsh-code-review 0.87.3 → 0.87.5
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 +14 -0
- package/README.md +5 -3
- package/RELEASE-NOTES.md +11 -9
- package/package.json +1 -1
- package/src/project-report.mjs +4 -15
- package/src/report-findings.mjs +57 -0
- package/src/report-readability.mjs +191 -0
- package/src/report-summary.mjs +74 -28
- package/src/retrieval-loop.mjs +30 -13
- package/src/review-reader-allocation.mjs +28 -13
- package/src/review.mjs +54 -22
- package/src/stage-continuation.mjs +2 -2
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.5 — 主审优先与依赖感知续审
|
|
6
|
+
|
|
7
|
+
- 先完成所有文件的主审扫描,再执行跨窗口/跨文件综合,避免后续分析在尚有文件未主审时抢先消耗模型调用;继续保留有限续跑、原截止时间和完整覆盖分母。
|
|
8
|
+
- 读取恢复按当前真实就绪阶段排队,跳过依赖未满足的队头;首次精确请求可利用自身保留额度或缓存恢复,不释放其他阶段保留额、不重置总账或检索轮数。
|
|
9
|
+
- 跟进取证输入超限时只移除重复初始源码正文,保留精确编辑、风险/调用线索与已交付证据回执;避免大文件重复正文挤掉新读证据。
|
|
10
|
+
- 未完成原因从阶段关联回文件和报告,保留依赖、分配等待、超时、调用上限与尝试状态;一个阶段受限不再阻止其他健康阶段保留生成。仍受限时明确 partial,不把评论、候选或报告收尾当作审核完成。
|
|
11
|
+
|
|
12
|
+
## 0.87.4 — 默认交付人能读懂的审核报告
|
|
13
|
+
|
|
14
|
+
- 默认主文先给是否查完、问题卡、未完成原因和下一步;完整证据与执行台账移到同一报告折叠附录,不要求使用者手动拼接批次。
|
|
15
|
+
- 按原文聚合限制并保留归属和记录;区分轮数、读取分配/共享额度、输入预算及依赖,空发现不暗示通过。
|
|
16
|
+
- 严格区分复核支持、待核实、原有和已反驳;保留项目冲突候选及分页/过期/复用限制,修正 SVN 历史标签。仅本地呈现,不改变 JSON、取证或证据门槛。
|
|
17
|
+
|
|
5
18
|
## 0.87.3 — 明确审核指令不再重复确认
|
|
6
19
|
|
|
7
20
|
- 明确要求审核且内部预览与当前授权范围、模型及发送目的地一致时,工具指导直接继续执行,不再要求第二次人工确认;仅预览或超出授权时仍暂停。
|
package/DEVELOPMENT.md
CHANGED
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
|
|
33
33
|
运行 `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
34
|
|
|
35
|
+
### 主审优先与依赖感知恢复
|
|
36
|
+
|
|
37
|
+
启用 `enableStageContinuation` 时,跨窗口/跨文件综合先登记为依赖阶段,待全体文件主审扫描后由 continuation 运行;独立复核可在主审链内进行,受限后进入同一有界队列。读取恢复传入完整当前 ready allocation IDs,跳过依赖未就绪队头但不释放其 floor;首次精确请求可无 release 尝试自身可负担成本,实际 admission 计次,同 revision 无进展不忙重试。未知成本的 scalar admission 仍要求容量释放,不声称它足以支付任意 verifier 预取。
|
|
38
|
+
|
|
39
|
+
跟进输入超限时只瘦身重复的原始 source text,保留完整编辑/元数据和已读取证据;既有窗口/综合的元数据也不再为压缩而删除。原始输入、当前接收的证据、独立复核与历史复用保持不同信任层。阶段限制按当前生成判定,不用文件曾经受限的 sticky 标记阻止健康阶段缓存;文件总体受限和不完整窗口仍阻止完成。
|
|
40
|
+
|
|
41
|
+
`continuation.units` 保存真实 pendingReason、依赖与尝试;报告按 fileIds 关联并在附录保留完整身份。模型额度、超时、依赖阻断和读取分配等待不互相冒充。预算仍有余额不保证所有保护中的阶段可运行,全体大请求超过各自 floor 时不偷取其他阶段保留额,仍保留 partial。
|
|
42
|
+
|
|
35
43
|
### 大上下文回归与纠正记账
|
|
36
44
|
|
|
37
45
|
正式变更入口默认 `focusPrimaryContext:true`,纯准备阶段只附带直接关系、C#和调用方线索对应的完整批准上下文对象,序列化最多16KiB/输入四分之一,稳定原顺序保留新旧版本对象。其余来源保留批准目录按需读;不会因缺少检索能力隐藏正文。底层API默认false,设置false可回退。主文件、规则与需求不删减;即使旧输入能容纳整批上下文,也应用该附带上限。
|
|
@@ -86,6 +94,12 @@ JSON合同新增280字节传输提示防止原始换行/反斜线转义错误
|
|
|
86
94
|
|
|
87
95
|
Git 路径和索引、SVN XML、规则文件与 SVN 链接记录仍按各自 UTF-8 协议校验,不因源码兼容而放宽;固定宽度关键字映射仍须独立核验,不支持的混杂编码或映射会明确阻断。
|
|
88
96
|
|
|
97
|
+
## 默认报告呈现
|
|
98
|
+
|
|
99
|
+
变更与当前源码模式统一输出一份人可读 Markdown:业务首屏结论、按“本次新增(复核支持)/当前源码问题(复核支持)/待核实/原有问题(复核支持)/已反驳”分区的问题卡、聚合未完成原因和下一步。完整证据与判定、逐项覆盖、风险计划、执行诊断和按原文聚合的限制放在同一报告的 `<details>` 技术附录;不改成需要用户拼接的批报告,JSON 仍完整保留原始记录。
|
|
100
|
+
|
|
101
|
+
这是本地呈现层调整,不改变取证算法、来源权限、独立复核或新旧对照门槛,不额外调用 LLM,也不由通俗主文代做源码复审。复核支持和引用校验不等于事实或因果证明;变更模式中缺少有效新旧对照的 supported 候选仍待核实;当前源码模式不要求历史对照,也不冒充本次新增。未完成原因区分检索轮数、共享读取调用数、读取字节、阶段输入预算和前置阶段未完成;未知原因不推测成输入超限,限制记录数不当作缺陷数或待审文件数。主文有界聚合不隐藏限制,全部范围与原始记录仍可在附录追溯。历史审查标签按实际模式显示 Git/SVN,不能把 SVN 历史误标为 Git。
|
|
102
|
+
|
|
89
103
|
## 性能与等待诊断
|
|
90
104
|
|
|
91
105
|
风险计划只产生待证问题和后续来源需求,不再自行展开检索对话;计划不合法则明确回退,主审、跨文件分析和独立证据复核保持各自严格校验,不把回退当作完成。规划原始调用与 JSON 纠正仍使用同一共享调用预算。批准目录附各来源物理 `lineCount`(空源为 0,不计尾换行的虚拟空行),旧/新侧分别取界,不将越界读取钳位成成功或伪造证据。
|
package/README.md
CHANGED
|
@@ -63,7 +63,9 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
|
|
|
63
63
|
|
|
64
64
|
### 5. 查看结果
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
执行工具默认在对话中返回 **一份人可读的中文 Markdown 报告**,同时保留完整结构化 JSON。首屏先回答所选范围是否查完、记录了哪些候选和哪些结论尚不能下;问题卡按“本次新增(复核支持)”“当前源码问题(复核支持)”“待核实”“原有问题(复核支持)”“已反驳”分区,说明位置、触发条件、影响、依据及已有建议。分区仍遵循严格证据判定:仅 supported 或缺少有效旧新对照不能算本次新增,当前源码模式不作历史新增归因;**复核支持和引用校验不等于事实、运行复现或因果证明。**
|
|
67
|
+
|
|
68
|
+
主文聚合未完成原因并给出下一步;完整证据与判定、逐项覆盖、风险计划、执行诊断及聚合限制保留在**同一报告的 `<details>` 技术附录**,展开即可核对,不需要拼接批报告。通俗主文只是已有结果的呈现,不代做源码复审、不隐藏限制、不放宽证据门槛,也不会为改写报告额外调用模型或改变取证算法。执行诊断列出实际加载的插件版本、阶段调用和墙钟统计;累计调用耗时可能并发重叠,不代表纯提供方耗时或可相加的总耗时。
|
|
67
69
|
|
|
68
70
|
如需保存报告,可以另行告诉 Agent:“将本次报告保存到我指定的位置”。插件本身不会自动在业务仓库创建报告文件。
|
|
69
71
|
|
|
@@ -237,9 +239,9 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
237
239
|
|
|
238
240
|
重启后可按原jobId读回保存记录,原运行标记interrupted,不自动继续外发。recovery给出已观察文件/窗口与未完成路径;它不是完整分母或新授权;宿主重启及自动恢复本身不授权外发,补审必须有当前明确授权并重新预览核对。新预览的runtime包含实际加载版本和模块位置,排查不同工作区时分别核对,不根据另一台机器推断。
|
|
239
241
|
|
|
240
|
-
变更审查的独立文件链默认最多 2 条并行;配置 `changesConcurrency` 为 1~4(1
|
|
242
|
+
变更审查的独立文件链默认最多 2 条并行;配置 `changesConcurrency` 为 1~4(1 回退串行),启用阶段续审时先扫描全体文件主审,再调度跨窗口/跨文件综合;恢复队列按真实依赖就绪情况排队,保留其他阶段的读取最低份额。配置 `maxRetrievalRounds` 为 0~50,默认为 3,限制每阶段检索请求回合数(不是成功读取次数;非法请求也会消耗回合)(0 只允许最终答复)。配置不是 preview 参数,不改变模型或来源授权;并发仍共享同一份读取/调用预算,额度不足时完成顺序可能影响哪些任务未完成,不能保证与串行逐项结果相同。提供方限流时应降低并发。复核可预取批准快照中候选锚点附近的同侧原文,仍允许追加反证读取并逐字校验;不复用主审结论或其他会话凭据。完整变更计划可容纳时仅生成一次再分发到窗口,否则保留原分窗口计划并在预览估算中体现。
|
|
241
243
|
|
|
242
|
-
大文件超出单次输入时按原行号分段。精确差异算不出时使用明确标注的包络,可能包含未变行,不声称精确归因。完整结束但 JSON 语法无效时会按当前阶段 schema
|
|
244
|
+
大文件超出单次输入时按原行号分段。精确差异算不出时使用明确标注的包络,可能包含未变行,不声称精确归因。完整结束但 JSON 语法无效时会按当前阶段 schema 最多请求两次明确纠正,记录额外调用;检索请求非法时整批不执行并明确纠正,回填采用可追溯的滚动工作集、必要时延后重复初始源码正文,保留精确编辑、风险/调用线索和已读证据回执,不虚构证据。主审与复核默认各最多 3 轮检索;到限后只允许最终答复,保留未完成范围,不把已用完预算当成审核通过。不会给错误输出补括号、猜字段或当成无缺陷。
|
|
243
245
|
|
|
244
246
|
### 显式续审与私有模型工件
|
|
245
247
|
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
# v0.87.
|
|
1
|
+
# v0.87.5 发布说明
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 修复审核执行链路,而不只是整理报告
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
先完成全体文件的主审扫描,再运行跨窗口、跨文件综合,减少后续分析挤占尚未主审文件的模型调用。续审队列按真实依赖就绪集合排队,不再让依赖未满足的 verifier 队头阻挡可运行的 primary;精确已知请求可使用自身保留容量或缓存首次恢复,不扩大共享预算。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
跟进取证超出输入上限时,移除重复的初始源码正文,保留身份、精确编辑、调用/风险线索、已读取证据正文及回执;初始输入和可容纳的小请求不变。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
阶段停止原因按 fileIds 关联回报告,明确区分前置依赖、读取分配等待、调用预算、超时和尝试耗尽。一个阶段的检索限制不再阻止其他健康阶段缓存已接受生成;原文件仍如实保留受限状态。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## 参考与边界
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
参考用户指定的 [Open Code Review](https://github.com/alibaba/open-code-review) 的冻结覆盖清单、结构化停止和预算耗尽后保全发现机制;核查本地 a758d9cb 与远端 5c039e815023a05a169b1240ae6b4b24c28270b6 的相关变更。采用独立实现,未复制其源码或模板。不照搬“已有评论即完成”的覆盖推断,也不将 Git merge-base 流程移植为 SVN revision 语义。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
本次不增加读取总额、不重置检索轮数或重试次数、不放宽证据与新增归因校验。Git/SVN 固定批准范围和独立复核回执仍有效;预算、超时或证据不足时仍可能 partial/uncertain。离线受控回归不是原业务 r7364 的重跑,不承诺真实模型必定查完或固定提速比例。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## 验证与升级
|
|
18
|
+
|
|
19
|
+
新增 Git/SVN 主审优先、依赖队头阻塞、精确恢复公平性、完整阶段取证输入与停止原因关联回归,并执行全量测试和冻结标签四矩阵 CI。发布不自动安装或重启当前宿主。安装见 [README](README.md),门禁见 [开发说明](DEVELOPMENT.md)。
|
package/package.json
CHANGED
package/src/project-report.mjs
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
import { markdownReport } from './review.mjs';
|
|
2
2
|
|
|
3
|
-
function dedupeFindings(findings) {
|
|
4
|
-
const seen = new Set();
|
|
5
|
-
const result = [];
|
|
6
|
-
for (const f of findings ?? []) {
|
|
7
|
-
const key = JSON.stringify([f.path, f.anchor, f.title, f.evidence]);
|
|
8
|
-
if (!seen.has(key)) { seen.add(key); result.push(f); }
|
|
9
|
-
}
|
|
10
|
-
return result;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
3
|
export function renderProjectReport(report) {
|
|
14
4
|
const merged = {
|
|
15
5
|
...report,
|
|
16
|
-
|
|
6
|
+
reviewMode: 'project',
|
|
7
|
+
findings: report.findings ?? [],
|
|
17
8
|
interactions: (report.batches ?? []).flatMap(b => b.interactions ?? []),
|
|
18
|
-
limitations: (report.batches ?? []).flatMap(b => b.limitations ?? []),
|
|
9
|
+
limitations: [...(report.limitations ?? []), ...(report.batches ?? []).flatMap(b => b.limitations ?? [])],
|
|
19
10
|
};
|
|
20
|
-
|
|
21
|
-
const outdated = report.outdated ? '\n注意:来源已变化或无法复核,仅对应原批准快照。' : '';
|
|
22
|
-
return markdownReport(merged).replace('# Code Review 报告', '# 全项目代码审查报告') + '\n\n' + scope + outdated;
|
|
11
|
+
return markdownReport(merged).replace('# Code Review 报告', '# 全项目代码审查报告');
|
|
23
12
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { groupDuplicateFindings } from './finding-groups.mjs';
|
|
2
|
+
import { comparisonValidated } from './report-summary.mjs';
|
|
3
|
+
|
|
4
|
+
const safe = value => Array.from(String(value ?? '')).map(c => {
|
|
5
|
+
if (c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127) return ' ';
|
|
6
|
+
if (c === '&') return '&';
|
|
7
|
+
if (c === '<') return '<';
|
|
8
|
+
if (c === '>') return '>';
|
|
9
|
+
return '*_[]#|!()\\`'.includes(c) ? '\\' + c : c;
|
|
10
|
+
}).join('');
|
|
11
|
+
|
|
12
|
+
export function findingDisposition(finding, report = {}) {
|
|
13
|
+
const v = finding.verification;
|
|
14
|
+
const evidence = Array.isArray(v?.evidence) && v.evidence.length > 0 && v.evidence.every(e => e?.status === 'references-validated');
|
|
15
|
+
if (v?.status !== 'completed' || !evidence) return '待核实';
|
|
16
|
+
if (v.verdict === 'refuted') return '已反驳';
|
|
17
|
+
if (v.verdict !== 'supported') return '待核实';
|
|
18
|
+
if (report.mode === 'project' || report.reviewMode === 'project' || report.projectScope || finding.attribution?.causality === 'current-source-only') return '当前源码问题(复核支持)';
|
|
19
|
+
const r = v.regression;
|
|
20
|
+
if (!comparisonValidated(finding)) return '待核实';
|
|
21
|
+
return r.classification === 'introduced' ? '本次新增(复核支持)' : r.classification === 'preexisting' ? '原有问题(复核支持)' : '待核实';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function location(finding) {
|
|
25
|
+
const a = finding.anchor ?? {};
|
|
26
|
+
return safe(finding.path ?? '未标明文件') + (a.kind === 'line' ? ' · ' + (a.side === 'old' ? '修改前' : a.side === 'new' ? '修改后' : safe(a.side)) + '第 ' + safe(a.start) + '–' + safe(a.end) + ' 行' : a.kind === 'property' ? ' · 属性 ' + safe(a.name) : ' · 文件级');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function findingsMarkdown(report) {
|
|
30
|
+
const findings = Array.isArray(report.findings) ? report.findings : [];
|
|
31
|
+
const duplicate = new Map();
|
|
32
|
+
for (const group of groupDuplicateFindings(findings)) for (const i of group.findingIndices.slice(1)) duplicate.set(i, group.findingIndices[0]);
|
|
33
|
+
const sections = new Map([['本次新增(复核支持)',[]],['当前源码问题(复核支持)',[]],['待核实',[]],['原有问题(复核支持)',[]],['已反驳',[]]]);
|
|
34
|
+
findings.forEach((f,i)=>{ if (!duplicate.has(i)) sections.get(findingDisposition(f,report)).push({f,i}); });
|
|
35
|
+
const output = ['', '## 审查发现'];
|
|
36
|
+
if (!findings.length) return [...output, '本轮没有形成问题记录;这不等于已经排除未完成范围内的问题。'];
|
|
37
|
+
output.push('以下区分复核支持、待核实和已反驳的记录;证据引用通过校验不等于运行复现或因果证明。');
|
|
38
|
+
for (const [label, items] of sections) {
|
|
39
|
+
if (!items.length) continue;
|
|
40
|
+
output.push('', '### ' + label);
|
|
41
|
+
for (const {f,i} of items) {
|
|
42
|
+
const severity = ({critical:'严重',high:'高',medium:'中',low:'低'})[f.severity] ?? safe(f.severity ?? '未分级');
|
|
43
|
+
output.push('', '#### ' + (i+1) + '. ' + safe(f.title) + '(严重程度:' + severity + ')', '- 位置:' + location(f));
|
|
44
|
+
for (const [key,title] of [['trigger','什么情况下发生'],['impact','可能造成的影响'],['evidence','已看到的依据']]) if (f[key]) output.push('- ' + title + ':' + safe(f[key]));
|
|
45
|
+
if (label === '待核实') output.push('- 尚未确定:' + safe(f.verification?.reason ?? '候选尚未完成有效的独立复核或修改前后对照,不能作为已确认缺陷。'));
|
|
46
|
+
if (label === '已反驳') output.push('- 反证结论:' + safe(f.verification?.reason ?? '复核反驳该候选;不计为待修缺陷。'));
|
|
47
|
+
if (f.attribution?.beforeBehavior) output.push('- 修改前:' + safe(f.attribution.beforeBehavior));
|
|
48
|
+
if (f.attribution?.afterBehavior) output.push('- 修改后:' + safe(f.attribution.afterBehavior));
|
|
49
|
+
if (f.suggestion) output.push('- 建议:' + safe(f.suggestion));
|
|
50
|
+
const related = [...duplicate].filter(([,first])=>first===i).map(([index])=>location(findings[index]));
|
|
51
|
+
if (related.length) output.push('- 相同证据与解释的其他位置:' + related.join(';') + '。正文合并展示,原始候选仍保留。');
|
|
52
|
+
if (f.reused || f.priorRun) output.push('- 来源:包含既往审核结果,不表示本轮重新取证。');
|
|
53
|
+
output.push('- 完整证据与判定过程:见技术附录中的候选 ' + (i+1) + '。');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Pure presentation: original report records remain authoritative and unchanged.
|
|
2
|
+
const rows = value => Array.isArray(value) ? value : [];
|
|
3
|
+
const escape = value => String(value ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/[\x00-\x1f\x7f]/g, ' ').replace(/[\\\x60*_{}\[\]()#+.!|~=-]/g, '\\$&');
|
|
4
|
+
const brief = value => { const text = String(value ?? ''); return escape(text.length > 180 ? text.slice(0, 180) + '…(完整值见附录)' : text); };
|
|
5
|
+
const html = value => String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6
|
+
const stages = { primary: '主审', verification: '独立复核', synthesis: '综合', 'window-synthesis': '跨窗口综合', interaction: '跨文件综合' };
|
|
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]]));
|
|
13
|
+
|
|
14
|
+
function reason(record = {}) {
|
|
15
|
+
const value = typeof record === 'string' ? record : record?.failureCode ?? record?.blockCode ?? record?.code ?? record?.reason ?? record?.pendingReason;
|
|
16
|
+
const text = String(value ?? '');
|
|
17
|
+
// Dependency state must not be relabelled as an input-budget failure.
|
|
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 '阶段执行超时';
|
|
25
|
+
if (/RETRIEVAL_ROUND_BUDGET_EXCEEDED|检索轮数|retrieval.*round.*(budget|limit)/i.test(text)) return '单阶段检索轮数达到上限(读取额度可能仍有余量)';
|
|
26
|
+
if (/READER_CALL_BUDGET_EXCEEDED|reader.*call.*(budget|limit)|读取调用.*(不足|耗尽|上限)/i.test(text)) return '阶段分配或共享读取调用次数不足';
|
|
27
|
+
if (/READER_OUTPUT_BUDGET_EXCEEDED|reader.*output.*(budget|limit)|读取.*字节|输出字节/i.test(text)) return '阶段分配或共享读取输出字节预算不足';
|
|
28
|
+
if (/input[-_ ]budget|input.*(budget|limit).*exceed|输入.*(预算|上限|超限)/i.test(text)) return '阶段输入预算不足';
|
|
29
|
+
if (/PROJECT_MODEL_CALL_LIMIT|model call budget|模型调用.*(额度不足|上限)/i.test(text)) return '模型调用额度不足';
|
|
30
|
+
if (/unsupported|not[-_ ]supported|不支持/i.test(text)) return '当前不支持此来源或操作';
|
|
31
|
+
if (text === 'exact-edit-analysis-unavailable') return '精确编辑分析不可用';
|
|
32
|
+
if (/CANCELLED|cancelled|canceled/i.test(text)) return '执行已取消';
|
|
33
|
+
if (/MODEL_TIMEOUT|timed? ?out/i.test(text)) return '阶段执行超时';
|
|
34
|
+
return text ? '未知/其他原因(完整原文见附录)' : '原因未记录(未知;不推测为预算不足)';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fileReasons(file, findings, units = []) {
|
|
38
|
+
const result = [];
|
|
39
|
+
const limits = [...rows(file?.retrievalLimits), ...rows(file?.groupRetrievalLimits)];
|
|
40
|
+
const failures = rows(file?.stageFailures);
|
|
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)));
|
|
46
|
+
if (['pending', 'blocked', 'failed', 'cancelled', 'running'].includes(file?.status)) {
|
|
47
|
+
// A missing top-level code does not erase concrete phase/group evidence.
|
|
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));
|
|
51
|
+
}
|
|
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));
|
|
54
|
+
if (file?.retrievalLimited || limits.length) {
|
|
55
|
+
result.push(...(limits.length ? limits.filter(record => !explained(record)).map(reason) : units.length ? [] : [reason(file)]));
|
|
56
|
+
}
|
|
57
|
+
if (rows(file?.windowCoverage?.pending).length) result.push('变更窗口尚未完成');
|
|
58
|
+
if (file?.continuationPending) result.push('阶段续审尚未完成');
|
|
59
|
+
for (const [key, label] of [['synthesisStatus', '跨窗口综合'], ['verificationStatus', '独立复核']]) if (file?.[key] && file[key] !== 'completed') result.push(label + '未完成');
|
|
60
|
+
if (rows(file?.reviewPasses).some(pass => pass?.status !== 'completed')) result.push('独立审查轮次尚未完成');
|
|
61
|
+
if (file?.fileId != null && findings.some(finding => finding?.fileId === file.fileId && finding?.verification?.status !== 'completed')) result.push('候选的独立复核尚未完成');
|
|
62
|
+
return [...new Set(result)];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collect(report) {
|
|
66
|
+
const files = rows(report?.files), findings = rows(report?.findings);
|
|
67
|
+
const names = new Map(files.filter(file => file?.fileId != null).map(file => [file.fileId, file.path]));
|
|
68
|
+
const identity = id => names.has(id) && names.get(id) != null ? String(names.get(id)) : '未知文件身份:' + (typeof id === 'object' ? JSON.stringify(id) : String(id));
|
|
69
|
+
const paths = record => {
|
|
70
|
+
if (record == null || typeof record !== 'object') return [];
|
|
71
|
+
return [...new Set([...(record.path != null ? [String(record.path)] : []), ...(record.fileId != null ? [identity(record.fileId)] : []), ...rows(record.fileIds).map(identity)])];
|
|
72
|
+
};
|
|
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);
|
|
97
|
+
const interactions = rows(report?.interactions).filter(record => record?.status !== 'completed').map(record => ({ record, reasons: [reason(record)], paths: paths(record) }));
|
|
98
|
+
return { unfinished, interactions, units, limitations: rows(report?.limitations), paths };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function grouped(items) {
|
|
102
|
+
const groups = new Map();
|
|
103
|
+
for (const item of items) for (const label of item.reasons) {
|
|
104
|
+
if (!groups.has(label)) groups.set(label, []);
|
|
105
|
+
groups.get(label).push(item);
|
|
106
|
+
}
|
|
107
|
+
return [...groups];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function summary(output, label, items) {
|
|
111
|
+
if (!items.length) return;
|
|
112
|
+
output.push('- ' + label + ':' + items.length + ' 条记录。');
|
|
113
|
+
const groups = grouped(items);
|
|
114
|
+
for (const [why, members] of groups.slice(0, 6)) {
|
|
115
|
+
const paths = [...new Set(members.flatMap(member => member.paths))];
|
|
116
|
+
output.push(' - ' + brief(why) + ':' + members.length + ' 条;' + (paths.length ? paths.slice(0, 4).map(brief).join('、') + (paths.length > 4 ? ';另有 ' + (paths.length - 4) + ' 个路径/身份' : '') : '未记录文件路径/身份') + '。');
|
|
117
|
+
}
|
|
118
|
+
if (groups.length > 6) output.push(' - 另有 ' + (groups.length - 6) + ' 类原因;完整范围与原始记录见本报告限制附录。');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function readabilityMarkdown(report = {}) {
|
|
122
|
+
const data = collect(report);
|
|
123
|
+
const output = ['', '## 未完成范围与原因', '以下为处理状态与限制记录数,非缺陷数;同一文件可能涉及多个阶段,限制不自动表示待审或缺陷。'];
|
|
124
|
+
summary(output, '未完成条目', data.unfinished);
|
|
125
|
+
summary(output, '跨文件综合未完成', data.interactions);
|
|
126
|
+
if (!data.unfinished.length && !data.interactions.length) output.push(report?.status === 'partial' || report?.partial || data.limitations.length ? '- 报告仍有部分完成标记或限制;未逐项记录待续审范围,不表示全部查完。' : '- 未记录未完成条目或跨文件综合;这不是正确性证明。');
|
|
127
|
+
if (data.limitations.length) output.push('- 另有 ' + data.limitations.length + ' 条限制记录,按原文聚合保留在本报告限制附录;不将这些限制全部计入未完成范围。');
|
|
128
|
+
if (data.unfinished.length || data.interactions.length) output.push('- 以上仅列有界摘要;全部路径、身份、阶段及原始记录见本报告限制附录。');
|
|
129
|
+
output.push('', '## 下一步');
|
|
130
|
+
if (data.unfinished.length || data.interactions.length) output.push('- 先处理对应阻断/失败原因,再选择未完成条目、窗口、综合或复核阶段继续;已完成主审不能替代后续综合和独立复核。', '- 检索轮数、读取调用数、字节与输入预算是不同限制,应按实际原因调整;依赖未完成时先完成前置阶段,不据此推断输入超限。', '- 本报告不会自动续审或调用模型;再次执行需新预览并核对当前人类授权,范围或发送目的地变化时重新确认。');
|
|
131
|
+
else output.push('- 如需扩大或继续审核,请显式选择范围并核对新预览与当前人类授权;不要仅凭没有未完成记录判定通过。');
|
|
132
|
+
return output;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function originalDetails(output, record) {
|
|
136
|
+
output.push('<details><summary>原始记录 JSON(完整保留)</summary>', '', '<pre>' + html(JSON.stringify(record, null, 2) ?? 'null') + '</pre>', '', '</details>');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Deduplicate identical records without losing their original positions or multiplicity.
|
|
140
|
+
function packed(records) {
|
|
141
|
+
const entries = new Map();
|
|
142
|
+
records.forEach((record, index) => {
|
|
143
|
+
const key = JSON.stringify(record);
|
|
144
|
+
if (!entries.has(key)) entries.set(key, { record, positions: [] });
|
|
145
|
+
entries.get(key).positions.push(index);
|
|
146
|
+
});
|
|
147
|
+
return [...entries.values()];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function limitationDetailsMarkdown(report = {}) {
|
|
151
|
+
const data = collect(report);
|
|
152
|
+
const output = ['', '## 限制附录', '以下按原文聚合;计数为记录数,非缺陷数,也不等于待审条目数。相同原文只展示一次,归属与重复次数聚合;折叠 JSON 保留差异字段及原始顺序。完整源报告 JSON 仍为权威记录。'];
|
|
153
|
+
const groups = new Map();
|
|
154
|
+
for (const record of data.limitations) {
|
|
155
|
+
const text = typeof record === 'string' ? record : record?.text;
|
|
156
|
+
// Prefixes distinguish a text-bearing record from an opaque legacy record.
|
|
157
|
+
const key = text == null ? 'record:' + JSON.stringify(record) : 'text:' + JSON.stringify(text);
|
|
158
|
+
if (!groups.has(key)) groups.set(key, { text, records: [] });
|
|
159
|
+
groups.get(key).records.push(record);
|
|
160
|
+
}
|
|
161
|
+
if (!groups.size) output.push('- 未记录限制原文。');
|
|
162
|
+
for (const { text, records } of groups.values()) {
|
|
163
|
+
output.push('', '### 限制(' + records.length + ' 条记录)', text == null ? '未提供限制原文' : '<pre>' + html(JSON.stringify(text)) + '</pre>');
|
|
164
|
+
const scopes = new Map();
|
|
165
|
+
for (const record of records) {
|
|
166
|
+
const paths = data.paths(record);
|
|
167
|
+
const scope = '- 范围:' + (paths.length ? paths.map(escape).join('、') : '未标明文件身份') + (record?.stage != null || record?.phase != null ? ';阶段:' + escape(stage(record.stage ?? record.phase)) : '');
|
|
168
|
+
scopes.set(scope, (scopes.get(scope) ?? 0) + 1);
|
|
169
|
+
}
|
|
170
|
+
for (const [scope, count] of scopes) output.push(scope + ';重复次数:' + count);
|
|
171
|
+
// Shared text is represented by the visible original above, not repeated in every JSON row.
|
|
172
|
+
const compact = records.map(record => typeof record === 'string' ? { type: 'string' } : record && typeof record === 'object' && record.text != null && Object.hasOwn(record, 'text') ? { type: 'object-with-shared-text', fields: Object.fromEntries(Object.entries(record).filter(([key]) => key !== 'text')) } : { type: 'original', value: record });
|
|
173
|
+
output.push('折叠记录中 object-with-shared-text 的 text、string 的原值均为本组上方完整原文;positions 为组内原始索引。');
|
|
174
|
+
originalDetails(output, packed(compact));
|
|
175
|
+
}
|
|
176
|
+
for (const [label, items] of [['未完成条目明细', data.unfinished], ['未完成跨文件综合明细', data.interactions]]) {
|
|
177
|
+
if (!items.length) continue;
|
|
178
|
+
output.push('', '### ' + label + '(' + items.length + ' 条记录)');
|
|
179
|
+
for (const item of items) output.push('- ' + (item.paths.length ? item.paths.map(escape).join('、') : '未标明文件身份') + ':' + item.reasons.map(escape).join(';'));
|
|
180
|
+
// State/reason metadata includes window IDs and pass statuses; full source and candidates already
|
|
181
|
+
// exist in the complete report JSON and must not be duplicated into this appendix.
|
|
182
|
+
const keys = ['fileId', 'path', 'groupId', 'fileIds', 'status', 'failureCode', 'blockCode', 'code', 'reason', 'pendingReason', 'stage', 'phase', 'stageFailures', 'retrievalLimited', 'retrievalLimits', 'groupRetrievalLimits', 'continuationPending', 'synthesisStatus', 'verificationStatus', 'windowCoverage', 'reviewPasses'];
|
|
183
|
+
output.push('以下仅为状态与原因元数据;源码、候选及其他完整字段见完整源报告 JSON。');
|
|
184
|
+
originalDetails(output, packed(items.map(item => Object.fromEntries(keys.filter(key => Object.hasOwn(item.record ?? {}, key)).map(key => [key, item.record[key]])))));
|
|
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
|
+
}
|
|
190
|
+
return output;
|
|
191
|
+
}
|
package/src/report-summary.mjs
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
const FILE_STATES = ['completed', 'excluded', 'blocked', 'failed', 'cancelled', 'pending'];
|
|
1
|
+
const FILE_STATES = ['completed', 'excluded', 'blocked', 'failed', 'cancelled', 'pending', 'running', 'interrupted'];
|
|
2
2
|
const STATUS_LABELS = { completed: '已完成', partial: '部分完成', failed: '执行失败', cancelled: '已取消', interrupted: '已中断', pending: '待执行', running: '执行中' };
|
|
3
|
-
const safe = value => Array.from(String(value ?? '').slice(0, 240)).map(c => {
|
|
4
|
-
if (c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127) return ' ';
|
|
5
|
-
if (c === '&') return '&';
|
|
6
|
-
if (c === '<') return '<';
|
|
7
|
-
if (c === '>') return '>';
|
|
8
|
-
if ('*_[]#|!()\\'.includes(c) || c.charCodeAt(0) === 96) return '\\' + c;
|
|
9
|
-
return c;
|
|
10
|
-
}).join('');
|
|
11
3
|
const rows = value => Array.isArray(value) ? value : [];
|
|
12
4
|
const validCount = n => Number.isSafeInteger(n) && n >= 0;
|
|
13
5
|
const supported = finding => finding?.verification?.status === 'completed' && finding.verification.verdict === 'supported' && rows(finding.verification.evidence).length > 0 && finding.verification.evidence.every(ref => ref?.status === 'references-validated');
|
|
14
6
|
|
|
15
7
|
// Presentation consumes host-validated results; it neither re-reads sources nor
|
|
16
8
|
// upgrades model verdicts, reference validation, or reuse into causal proof.
|
|
17
|
-
function comparisonValidated(finding) {
|
|
9
|
+
export function comparisonValidated(finding) {
|
|
18
10
|
const { evidence, regression } = finding.verification ?? {};
|
|
19
11
|
if (regression?.status !== 'comparison-references-validated' || !Array.isArray(evidence)) return false;
|
|
20
12
|
const old = regression.oldEvidence, fresh = regression.newEvidence;
|
|
@@ -25,35 +17,89 @@ function comparisonValidated(finding) {
|
|
|
25
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));
|
|
26
18
|
}
|
|
27
19
|
|
|
28
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Pure presentation data for Markdown/Lead renderers. Counts are candidate records,
|
|
22
|
+
* not proven defects. coverage counts main-review entries (directories included in
|
|
23
|
+
* change mode); unfinished preserves every loaded entry with incomplete stages, while
|
|
24
|
+
* crossFile is separate. complete means processing finished, never release approval.
|
|
25
|
+
* Returned objects/arrays are newly allocated and contain no raw model/source text.
|
|
26
|
+
*/
|
|
27
|
+
export function reportSummaryData(report = {}) {
|
|
29
28
|
const findings = rows(report.findings), files = rows(report.files);
|
|
29
|
+
const project = report.mode === 'project' || report.reviewMode === 'project' || Boolean(report.projectScope);
|
|
30
30
|
const counts = { introduced: 0, unresolved: 0, refuted: 0, preexisting: 0, current: 0 };
|
|
31
31
|
for (const finding of findings) {
|
|
32
32
|
const verification = finding?.verification;
|
|
33
33
|
if (verification?.status === 'completed' && verification.verdict === 'refuted' && rows(verification.evidence).length && verification.evidence.every(ref => ref?.status === 'references-validated')) counts.refuted++;
|
|
34
34
|
else if (!supported(finding)) counts.unresolved++;
|
|
35
|
-
else if (
|
|
35
|
+
else if (project || finding.attribution?.causality === 'current-source-only') counts.current++;
|
|
36
36
|
else if (comparisonValidated(finding) && verification.regression.classification === 'introduced') counts.introduced++;
|
|
37
37
|
else if (comparisonValidated(finding) && verification.regression.classification === 'preexisting') counts.preexisting++;
|
|
38
38
|
else counts.unresolved++;
|
|
39
39
|
}
|
|
40
40
|
const coverage = Object.fromEntries(FILE_STATES.map(status => [status, validCount(report.coverage?.[status]) ? report.coverage[status] : files.filter(f => f?.status === status).length]));
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
const accounted = FILE_STATES.reduce((sum, status) => sum + coverage[status], 0);
|
|
42
|
+
const total = validCount(report.coverage?.total) ? report.coverage.total :
|
|
43
|
+
validCount(report.totalFiles) ? report.totalFiles : Math.max(files.length, accounted);
|
|
44
|
+
const detailsComplete = files.length >= total;
|
|
45
|
+
const unfinished = files.flatMap(file => {
|
|
46
|
+
if (file?.status === 'excluded') return [];
|
|
47
|
+
const reasons = [];
|
|
48
|
+
if (file?.status !== 'completed') reasons.push(STATUS_LABELS[file?.status] ?? '主审未完成');
|
|
49
|
+
if (rows(file?.stageFailures).length) reasons.push('阶段失败');
|
|
50
|
+
if (file?.retrievalLimited) reasons.push('取证受限');
|
|
51
|
+
if (file?.continuationPending) reasons.push('后续阶段待执行');
|
|
52
|
+
if (rows(file?.windowCoverage?.pending).length) reasons.push('窗口待审 ' + file.windowCoverage.pending.length);
|
|
53
|
+
if (file?.synthesisStatus && file.synthesisStatus !== 'completed') reasons.push('综合未完成');
|
|
54
|
+
if (file?.verificationStatus && file.verificationStatus !== 'completed' || findings.some(finding => file?.fileId != null && finding?.fileId === file.fileId && finding?.verification?.status !== 'completed')) reasons.push('复核未完成');
|
|
55
|
+
if (rows(file?.reviewPasses).some(pass => pass?.status !== 'completed')) reasons.push('审查轮次未完成');
|
|
56
|
+
return reasons.length ? [{ path: String(file?.path ?? '未命名条目'), reasons }] : [];
|
|
57
|
+
});
|
|
58
|
+
const interactions = rows(report.interactions);
|
|
59
|
+
const crossFile = { total: interactions.length, completed: interactions.filter(item => item?.status === 'completed').length, pending: interactions.filter(item => item?.status !== 'completed').length };
|
|
60
|
+
const stages = Object.fromEntries(['pending', 'running', 'stopped'].map(status => [status, validCount(report.stageContinuation?.[status]) ? report.stageContinuation[status] : 0]));
|
|
61
|
+
const unverifiedCandidates = findings.filter(finding => finding?.verification?.status !== 'completed').length;
|
|
62
|
+
coverage.unaccounted = Math.max(0, total - accounted);
|
|
63
|
+
const complete = report.status === 'completed' && detailsComplete && total > 0 && coverage.completed > 0 &&
|
|
64
|
+
!unfinished.length && !crossFile.pending && !Object.values(stages).some(Boolean) && !unverifiedCandidates && !coverage.unaccounted &&
|
|
65
|
+
!FILE_STATES.some(status => !['completed', 'excluded'].includes(status) && coverage[status] > 0);
|
|
66
|
+
return {
|
|
67
|
+
status: typeof report.status === 'string' ? report.status : 'unknown',
|
|
68
|
+
complete, outdated: Boolean(report.outdated), emptySelection: total === 0,
|
|
69
|
+
counts: { total: findings.length, ...counts },
|
|
70
|
+
coverage: { total, loaded: files.length, detailsComplete, unit: project ? 'files' : 'change-entries', ...coverage },
|
|
71
|
+
unfinished, crossFile, stages, unverifiedCandidates,
|
|
72
|
+
limitations: rows(report.limitations).length,
|
|
73
|
+
reused: findings.some(finding => finding?.reused || finding?.priorRun),
|
|
74
|
+
currentSourceOnly: Boolean(project || findings.some(finding => finding?.attribution?.causality === 'current-source-only')),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function reportSummaryMarkdown(report = {}) {
|
|
79
|
+
const data = reportSummaryData(report), { counts, coverage } = data;
|
|
80
|
+
const state = data.emptySelection ? '未选择条目,没有执行代码审核(' + (STATUS_LABELS[data.status] ?? '状态未知') + ')' :
|
|
81
|
+
data.complete ? '所选范围已查完' : '尚未查完(' + (STATUS_LABELS[data.status] ?? '状态未知') + ')';
|
|
82
|
+
const headline = state + (data.outdated ? ';报告已过期' : '') + ';' +
|
|
83
|
+
(counts.total ? '记录 ' + counts.total + ' 个候选问题' : '未记录候选问题') + ';不得据此放行。';
|
|
84
|
+
const output = ['', '## 结论摘要', '- ' + headline];
|
|
85
|
+
if (counts.total) {
|
|
86
|
+
const labels = { introduced: '复核支持本次新增', unresolved: '待核实', preexisting: '复核支持原有', refuted: '复核反驳', current: '当前源码复核支持' };
|
|
87
|
+
output.push('- 候选复核:' + Object.entries(labels).filter(([key]) => counts[key] > 0).map(([key, label]) => label + ' ' + counts[key]).join(';') + '。');
|
|
53
88
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!
|
|
89
|
+
const unit = coverage.unit === 'files' ? '个文件' : '个变更条目(含目录)';
|
|
90
|
+
const labels = { completed: '主审完成', excluded: '排除', blocked: '阻断', failed: '失败', cancelled: '取消', pending: '待审', running: '执行中', interrupted: '中断', unaccounted: '状态未记账' };
|
|
91
|
+
output.push('- 覆盖:' + coverage.total + ' ' + unit + ';' + Object.entries(labels).filter(([key]) => coverage[key] > 0).map(([key, label]) => label + ' ' + coverage[key]).join(',') + (coverage.total ? '。' : '未执行。'));
|
|
92
|
+
if (!coverage.detailsComplete) output.push('- 明细仅 ' + coverage.loaded + '/' + coverage.total + ' 项;未加载条目的路径与阶段状态未在本摘要核实。');
|
|
93
|
+
if (data.unfinished.length) output.push('- 未完成条目 ' + data.unfinished.length + '(完整范围见后文及附录)' + (coverage.detailsComplete ? '。' : ';仅计已加载明细。'));
|
|
94
|
+
if (Object.values(data.stages).some(Boolean)) output.push('- 后续阶段:待执行 ' + data.stages.pending + ',执行中 ' + data.stages.running + ',已停止 ' + data.stages.stopped + '。');
|
|
95
|
+
if (data.unverifiedCandidates) output.push('- 候选复核未完成:' + data.unverifiedCandidates + ' 项。');
|
|
96
|
+
if (data.crossFile.total) output.push('- 跨文件综合:完成 ' + data.crossFile.completed + '/' + data.crossFile.total + ' 项;未完成 ' + data.crossFile.pending + ' 项。');
|
|
97
|
+
if (data.limitations) output.push('- 已记录限制:' + data.limitations + ' 项,见限制明细。');
|
|
98
|
+
const qualifications = ['完成仅指处理进度,空发现不证明无缺陷'];
|
|
99
|
+
if (counts.total) qualifications.push('按候选记录而非去重根因计数,复核支持及引用校验不是事实或因果证明');
|
|
100
|
+
if (data.currentSourceOnly) qualifications.push('当前源码模式不评估历史新增归因');
|
|
101
|
+
if (data.reused) qualifications.push('含既往轮次复用发现,不表示本轮重新取证');
|
|
102
|
+
if (data.outdated) qualifications.push('仅对应原批准快照,不能作为当前源码结论');
|
|
103
|
+
output.push('- 技术限定:' + qualifications.join(';') + '。');
|
|
58
104
|
return output;
|
|
59
105
|
}
|
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];
|
|
@@ -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
|
@@ -35,6 +35,8 @@ import { configureWireBudget, modelWireBudget } from './model-wire-budget.mjs';
|
|
|
35
35
|
import { createEvidenceContext } from './evidence-context.mjs';
|
|
36
36
|
import { prepareInteractionRisk, consumeInteractionRisks } from './interaction-risk.mjs';
|
|
37
37
|
import { reportSummaryMarkdown } from './report-summary.mjs';
|
|
38
|
+
import { findingsMarkdown } from './report-findings.mjs';
|
|
39
|
+
import { readabilityMarkdown, limitationDetailsMarkdown } from './report-readability.mjs';
|
|
38
40
|
import { validateGroupInteraction } from './group-interaction.mjs';
|
|
39
41
|
import { stageKey } from './review-stage-state.mjs';
|
|
40
42
|
import { createStageGenerationCache } from './stage-generation-cache.mjs';
|
|
@@ -194,7 +196,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
194
196
|
const generationCache = createStageGenerationCache(input, options.resumeState?.stageGenerations ?? []);
|
|
195
197
|
const acceptedStages = new Map(generationCache.records().filter(record => record.phase === 'primary').map(record => [record.key, { generation: record.generation, record, restored: true }]));
|
|
196
198
|
const primarySuspensions = new Map(), correctedStages = new Set();
|
|
197
|
-
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();
|
|
198
200
|
const checkpointStages = () => {
|
|
199
201
|
if (!options.enableStageContinuation) return;
|
|
200
202
|
report.stageGenerations = generationCache.records().filter(record => record.phase === 'primary');
|
|
@@ -202,7 +204,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
202
204
|
let readerAllocation;
|
|
203
205
|
const stageScope = id => readerAllocation
|
|
204
206
|
? configureWireBudget(readerAllocation.scope(id), { nativeOutputTools: options.nativeOutputTools === true }) : scope;
|
|
205
|
-
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); };
|
|
206
208
|
const contextSeeds = (reader, request, existing = [], buildRequest = records => retrievalRequest(request, reader, records)) => {
|
|
207
209
|
if (!evidenceContext || !reader || maxRetrievalRounds === 0) return [];
|
|
208
210
|
const payload = JSON.parse(request.input), catalog = reader.catalog();
|
|
@@ -280,7 +282,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
280
282
|
}
|
|
281
283
|
if (signal?.aborted) { state.status = 'cancelled'; state.reason = '用户取消'; return; }
|
|
282
284
|
const assignedGroup = [...groupPrepared].find(([, value]) => value.fileIds.includes(file.id));
|
|
283
|
-
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; }
|
|
284
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 });
|
|
285
287
|
const { changes, payload } = prepared;
|
|
286
288
|
Object.assign(state, prepared.metadata);
|
|
@@ -298,8 +300,9 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
298
300
|
if (report.modelCalls >= maxCalls) throw new Error('Model call budget exceeded');
|
|
299
301
|
report.modelCalls++;
|
|
300
302
|
};
|
|
301
|
-
let activeReader = scope, activeStage = 'primary';
|
|
303
|
+
let activeReader = scope, activeStage = 'primary', activeStageLimited = false;
|
|
302
304
|
const onBudgetLimited = info => {
|
|
305
|
+
activeStageLimited = true;
|
|
303
306
|
if (activeStage !== 'primary') (state.secondaryRetrievalLimits ??= []).push({ phase: activeStage, ...info });
|
|
304
307
|
state.retrievalLimited = true;
|
|
305
308
|
(state.retrievalLimits ??= []).push(info);
|
|
@@ -402,6 +405,22 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
402
405
|
const phase=batch.interaction?'file-interaction':batch.synthesis?'window-synthesis':'primary';
|
|
403
406
|
try{
|
|
404
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
|
+
}
|
|
405
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)))) {
|
|
406
425
|
defer(Object.assign(new Error('Stage is waiting for accepted primary generations'), { code: 'READER_CALL_BUDGET_EXCEEDED' })); return;
|
|
407
426
|
}
|
|
@@ -558,7 +577,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
558
577
|
}
|
|
559
578
|
request.signal.throwIfAborted();
|
|
560
579
|
if(rounds>1)generated.findings.forEach(f=>{f.reviewRound=batch.round;});
|
|
561
|
-
if (options.enableStageContinuation && !priorStage && !
|
|
580
|
+
if (options.enableStageContinuation && !priorStage && !activeStageLimited) {
|
|
562
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) } };
|
|
563
582
|
const admission = generationCache.admit(record);
|
|
564
583
|
if (admission.accepted) {
|
|
@@ -605,19 +624,19 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
605
624
|
}
|
|
606
625
|
}
|
|
607
626
|
if (acceptedStages.has(key)) acceptedStages.get(key).completed = !options.enableVerification || generated.findings.every(finding => finding.verification?.status === 'completed');
|
|
608
|
-
if (batch.synthesis) state.synthesisStatus = 'completed';
|
|
627
|
+
if (batch.synthesis) state.synthesisStatus = activeStageLimited ? 'pending' : 'completed';
|
|
609
628
|
if (batch.interaction) {
|
|
610
629
|
const interaction=report.interactions.find(p => p.groupId === batch.interaction.id);
|
|
611
630
|
if (combinedInteraction) Object.assign(interaction, { method: 'group-primary-validated-channel', channelGroupId: combinedInteraction.groupId });
|
|
612
|
-
if(rounds>1){interaction.completedRounds??=[];interaction.completedRounds.push(batch.round);interaction.reviewRounds=rounds;}
|
|
613
|
-
if(batch.round===rounds)interaction.status='completed';
|
|
631
|
+
if(rounds>1){interaction.completedRounds??=[];if(!activeStageLimited)interaction.completedRounds.push(batch.round);interaction.reviewRounds=rounds;}
|
|
632
|
+
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'; }
|
|
614
633
|
}
|
|
615
634
|
if (state.windowCoverage && batch.round === 1) {
|
|
616
635
|
state.windowCoverage.completed.push(...batch.windowIds);
|
|
617
636
|
state.windowCoverage.pending = state.windowCoverage.pending.filter(id => !batch.windowIds.includes(id));
|
|
618
637
|
}
|
|
619
638
|
if(rounds>1)state.reviewPasses[batchIndex].status='completed';
|
|
620
|
-
if (retry) { const pending = deferredStages.find(stage => stage.key === key); if (pending) pending.succeeded = !options.enableVerification || generated.findings.every(finding => finding.verification?.status === 'completed'); }
|
|
639
|
+
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'; } }
|
|
621
640
|
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});
|
|
622
641
|
}catch(error){
|
|
623
642
|
enclosingRequest.signal.throwIfAborted();
|
|
@@ -738,7 +757,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
738
757
|
}, dependencies, fileConcurrency);
|
|
739
758
|
if (options.enableStageContinuation === true && deferredStages.length && !signal.aborted) {
|
|
740
759
|
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);
|
|
741
|
-
const units = deferredStages.map((stage, index) => ({ id: hash(stage.key), kind: stage.phase, fileIds: stage.fileIds,
|
|
760
|
+
const units = deferredStages.map((stage, index) => ({ id: hash(stage.key), kind: stage.phase, fileIds: stage.fileIds, pendingReason: stage.reason,
|
|
742
761
|
dependencies: stage.phase === 'primary'
|
|
743
762
|
? deferredStages.slice(0, index).filter(other => other.phase === 'primary' && other.fileId === stage.fileId).map(other => hash(other.key))
|
|
744
763
|
: 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]; }) }));
|
|
@@ -751,17 +770,26 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
751
770
|
}
|
|
752
771
|
for (let pass = 0; pass < 2; pass++) {
|
|
753
772
|
let progressed = false;
|
|
754
|
-
for (const stage of deferredStages) {
|
|
773
|
+
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))) {
|
|
755
774
|
if (stage.done || !continuation.ready().some(unit => unit.id === hash(stage.key))) continue;
|
|
756
|
-
if (performance.now() >= fileDeadlines.get(stage.fileId) || signal.aborted || report.modelCalls >= maxCalls
|
|
757
|
-
|
|
758
|
-
|
|
775
|
+
if (performance.now() >= fileDeadlines.get(stage.fileId) || signal.aborted || report.modelCalls >= maxCalls || options.remainingCalls?.() <= 0) {
|
|
776
|
+
stage.reason = signal.aborted ? 'STAGE_CANCELLED' : performance.now() >= fileDeadlines.get(stage.fileId) ? 'STAGE_TIMEOUT' : 'PROJECT_MODEL_CALL_LIMIT';
|
|
777
|
+
continuation.stop(hash(stage.key), stage.reason); continue;
|
|
778
|
+
}
|
|
779
|
+
const eligibleIds = [...new Set(deferredStages.filter(candidate => !candidate.done && continuation.ready().some(unit => unit.id === hash(candidate.key))).map(candidate => candidate.allocationId))];
|
|
780
|
+
const admitted = !yieldedUnits.has(stage.allocationId) ? { resumed: true } : stage.requests.length ? readerAllocation.resume(stage.allocationId, stage.requests, { eligibleIds }) : readerAllocation.resumeBudget(stage.allocationId, { eligibleIds });
|
|
781
|
+
if (!admitted.resumed) { stage.reason = ['READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED'].includes(admitted.reason) ? admitted.reason : 'STAGE_ALLOCATION_WAITING'; continue; }
|
|
759
782
|
yieldedUnits.delete(stage.allocationId);
|
|
783
|
+
if (!stage.allocationId.startsWith('verification:') || !deferredStages.some(other => other !== stage && !other.done && other.fileId === stage.fileId)) queuedStageUnits.delete(stage.allocationId);
|
|
760
784
|
const token = continuation.start(hash(stage.key), { signal });
|
|
761
785
|
stage.attempts++; await stage.run();
|
|
762
786
|
progressed = true;
|
|
763
|
-
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
|
|
764
|
-
else { continuation.yield(hash(stage.key),
|
|
787
|
+
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); }
|
|
788
|
+
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); }
|
|
789
|
+
if (!deferredStages.some(other => !other.done && other.fileId === stage.fileId)) {
|
|
790
|
+
queuedStageUnits.delete('verification:' + stage.fileId);
|
|
791
|
+
releaseStage('verification:' + stage.fileId);
|
|
792
|
+
}
|
|
765
793
|
}
|
|
766
794
|
if (!progressed) break;
|
|
767
795
|
}
|
|
@@ -769,6 +797,10 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
769
797
|
for (const [id, keys] of primaryStageKeys) if (!primaryDone.includes(id) && keys.length && keys.every(key => acceptedStages.has(key))) primaryDone.push(id);
|
|
770
798
|
report.stageContinuation = continuation.summary();
|
|
771
799
|
report.continuation = continuation.checkpoint();
|
|
800
|
+
for (const unit of report.continuation.units) if (unit.status !== 'completed') {
|
|
801
|
+
const stage = deferredStages.find(item => hash(item.key) === unit.id);
|
|
802
|
+
if (stage?.reason && !unit.stopped) unit.pendingReason = stage.reason;
|
|
803
|
+
}
|
|
772
804
|
report.continuationOptions = continuationOptions;
|
|
773
805
|
for (const file of input.files) {
|
|
774
806
|
const result = fileResults[input.files.indexOf(file)];
|
|
@@ -778,7 +810,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
778
810
|
checkpointStages();
|
|
779
811
|
await options.onResumeCheckpoint?.({ ...report, files: fileResults.map(result => result.state), findings: fileResults.flatMap(result => result.findings), limitations: [...report.limitations, ...fileResults.flatMap(result => result.limitations)] });
|
|
780
812
|
}
|
|
781
|
-
for (const id of yieldedUnits) readerAllocation?.release(id);
|
|
813
|
+
for (const id of new Set([...yieldedUnits, ...queuedStageUnits])) readerAllocation?.release(id);
|
|
782
814
|
if (checkpointFailed) throw checkpointFailure;
|
|
783
815
|
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'); }
|
|
784
816
|
report.files = fileResults.map(result => result.state);
|
|
@@ -793,7 +825,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
793
825
|
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 };
|
|
794
826
|
if (!reviewableCount) report.limitations.push({ text: input.files.length ? '没有可审查文件;排除或阻塞不表示代码审核通过。' : '未选择变更文件,没有执行代码审核。' });
|
|
795
827
|
const unfinished = report.files.some(f => !['completed', 'excluded'].includes(f.status));
|
|
796
|
-
report.status = signal?.aborted ? 'cancelled' : unfinished || report.limitations.length ? 'partial' : 'completed';
|
|
828
|
+
report.status = signal?.aborted ? 'cancelled' : unfinished || report.limitations.length || report.stageContinuation?.pending || report.files.some(file => file.synthesisStatus && file.synthesisStatus !== 'completed') ? 'partial' : 'completed';
|
|
797
829
|
if (report.files.some(f => f.status === 'failed') && !report.files.some(f => f.status === 'completed')) report.status = signal?.aborted ? 'cancelled' : 'failed';
|
|
798
830
|
report.coverage = Object.fromEntries(['completed', 'excluded', 'blocked', 'failed', 'cancelled', 'pending'].map(s => [s, report.files.filter(f => f.status === s).length]));
|
|
799
831
|
if (scope) {
|
|
@@ -802,7 +834,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
802
834
|
}
|
|
803
835
|
report.regressionCoverage = Object.fromEntries(['introduced', 'preexisting', 'uncertain', 'unassessed'].map(classification => [classification, report.findings.filter(f => (f.verification?.regression?.classification ?? 'unassessed') === classification).length]));
|
|
804
836
|
report.findingGroups = groupDuplicateFindings(report.findings);
|
|
805
|
-
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');
|
|
837
|
+
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');
|
|
806
838
|
await options.onResumeCheckpoint?.(report);
|
|
807
839
|
report.followup = coverageFollowup(report);
|
|
808
840
|
if (scope) report.readerBudget = { ...scope.usage(), ...scope.budget(), ...readerLimits };
|
|
@@ -852,7 +884,7 @@ function renderFinding(output, finding, index, duplicates) {
|
|
|
852
884
|
}
|
|
853
885
|
|
|
854
886
|
export function markdownReport(report) {
|
|
855
|
-
const output = ['# Code Review 报告', '', '状态:' + report.status, '快照:' + report.snapshotId
|
|
887
|
+
const output = ['# Code Review 报告', ...reportSummaryMarkdown(report), ...findingsMarkdown(report), ...readabilityMarkdown(report), '', '## 技术附录', '', '<details>', '<summary>展开完整证据、逐项覆盖与执行诊断(不影响上面的结论)</summary>', '', '状态:' + safe(report.status), '快照:' + safe(report.snapshotId), '', '## 候选原始证据与判定'];
|
|
856
888
|
if (!report.findings.length) output.push(report.status === 'completed' ? '在已审查范围内未发现具体问题;不代表代码已被证明正确。' : '当前没有有效问题记录,但审查存在未完成项或限制,不能视为通过。');
|
|
857
889
|
const duplicates = new Map();
|
|
858
890
|
for (const group of groupDuplicateFindings(report.findings)) for (const index of group.findingIndices.slice(1)) duplicates.set(index, group.findingIndices[0]);
|
|
@@ -861,7 +893,7 @@ export function markdownReport(report) {
|
|
|
861
893
|
if (report.callerDiscovery) output.push('- 引用方发现:扫描 '+report.callerDiscovery.scanned.length+' 个文件,捕获 '+report.callerDiscovery.capturedPaths.length+' 个上下文;仅批准目录字面引用导航,不证明函数调用或完整覆盖。');
|
|
862
894
|
if(report.businessRequirement)output.push('- 业务需求:SHA256 '+safe(report.businessRequirement.hash)+' · '+report.businessRequirement.bytes+' bytes;不可信约束,不是证据或权限,全文不在报告重复。');
|
|
863
895
|
for (const file of report.files) for (const correction of file.anchorCorrections ?? []) output.push('- 定位纠正:' + safe(correction.originalCandidate.fileId) + ' · ' + safe(correction.status) + ' · ' + safe(correction.reason) + ';原完整候选保留JSON,仅定位不证明因果');
|
|
864
|
-
if (report.history) output.push('- Git历史审查:' + safe(report.history.mode) + ' · ' + safe(report.history.base ?? 'empty') + ' → ' + safe(report.history.target) + ';精确端点树差异,重命名按增删,不含工作树');
|
|
896
|
+
if (report.history) output.push('- ' + (String(report.history.mode).startsWith('svn') ? 'SVN' : 'Git') + '历史审查:' + safe(report.history.mode) + ' · ' + safe(report.history.base ?? 'empty') + ' → ' + safe(report.history.target) + ';精确端点树差异,重命名按增删,不含工作树');
|
|
865
897
|
if (report.changeScope) output.push('- Git审查范围:' + safe(changeScopeLabel(report.changeScope)) + ';其他范围未审查,未跟踪文件仍默认排除。');
|
|
866
898
|
if (report.businessGrouping) output.push('- 业务分组:' + safe(report.businessGrouping.status) + (report.businessGrouping.reason ? ' — ' + safe(report.businessGrouping.reason) : '') + ';仅调度假设,不证明依赖或覆盖');
|
|
867
899
|
for (const file of report.files) for (const plan of file.riskPlans ?? []) output.push('- 风险计划:' + safe(file.path) + ' · ' + safe(plan.status) + (plan.method === 'inline' ? '(合并主审,非独立规划)' : '') + ' · ' + plan.risks.length + '项待证假设;不表示缺陷成立或覆盖完成');
|
|
@@ -880,6 +912,6 @@ export function markdownReport(report) {
|
|
|
880
912
|
if (report.rules?.length) output.push('', '## 项目规则', '规则仅为已批准约束数据,不扩大权限或证明缺陷。', ...report.rules.map(rule => '- ' + safe(rule.path) + ' · SHA256 ' + safe(rule.hash)));
|
|
881
913
|
if (report.followup?.suggestedPaths.length) output.push('', '## 后续选择建议', '仅为原快照未完成项;不会自动续审。再次执行需新预览、新审批,并显式选择规则和上下文。阻断项需先处理原因;关联上下文不等于主文件已审查。', ...report.followup.items.filter(item => item.followup).map(item => '- ' + safe(item.path) + ':' + item.status + (item.incompleteVerification ? '(候选复核未完成)' : '')));
|
|
882
914
|
output.push(...diagnosticsMarkdown(report.diagnostics));
|
|
883
|
-
|
|
915
|
+
output.push(...limitationDetailsMarkdown(report), '', '</details>');
|
|
884
916
|
return output.join('\n') + '\n';
|
|
885
917
|
}
|
|
@@ -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;
|