qwen-code 的 report_findings 类型化契约:让代码评审发现以结构化数据直达所有客户端
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
在 qwen-code(一个运行于终端中的开源 AI 编码代理)的/review评审流程中,评审产出的 findings(发现项)此前只有两种存在形态:写入磁盘的 JSON 工件(.qwen/tmp/下的qwen review findings产物)与最终渲染出来的 Markdown 复述。本篇文章以设计文档 report-findings-typed-contract.md 为骨架,结合核心实现 report-findings.ts、CLI 侧工件命令 findings.ts 与 TUI 渲染组件,深入讲解report_findings这个带内(in-band)类型化契约工具:它如何把 findings 以结构化数据直接送达终端 UI、Web Shell 与 ACP 宿主,如何在--fix之后回执每条 finding 的结果(outcome),以及“后一次调用替换整个列表”这一契约如何在转录层被严格执行。读完你将掌握该契约的完整字段规范、两次调用模型、身份校验门与渲染/压缩规则,并能在自己的宿主或客户端上正确消费findings_list结构。
背景:评审发现的两次“转录”问题
设计文档开篇指出现状:/review其实已经两次把 findings 规范化成数据——qwen review findings把类型化工件写到.qwen/tmp/,Step 8 的save-artifact+record_artifact再把一份持久副本发布给 Web Shell 渲染(CodeReviewArtifactDetail)。但这两者都是事后注册的文件。
问题在于:所有实时渲染会话的客户端——终端 UI、Web Shell 转录、ACP 宿主、daemon TUI——收到的只是同一份列表的Markdown 复述。而在--fix(或之后的fix these issues)执行完毕后,没有任何带内(in-band)信号告诉客户端哪些 finding 已被关闭。客户端只能看到两段文字,无法可靠地知道清单中每一项的处置状态。
report_findings就是这条缺失的带内半边:一次工具调用,{level, findings[]},由宿主 UI 按逐条 finding 渲染。核心实现文件头部注释点明了设计动机(report-findings.ts):
评审的 findings 已经作为数据存在过一次——
qwen review findings工件——但那个文件在磁盘上,事后才通过record_artifact注册。每个实时渲染会话的客户端看到的只是散文式复述,而这正是工件想要消除的转录面。
工具契约:字段与枚举
report_findings是一次性交付型工具,不持久化任何内容,也不裁决结论——它的返回值是一个结构化returnDisplay,类型为findings_list。执行失败只是 UI 交付失败:披露它,继续流程,绝不改动评审工件或裁决。
顶层参数
| 参数 | 类型 | 说明 |
|---|---|---|
level | 'low' \| 'medium' \| 'high' | 本次评审投入档位(低投入低努力度)。仅在客户端渲染时用于标注,见 REPORT_FINDINGS_LEVELS |
findings | ReportFindingsFindingParams[] | 完整 findings 列表,最多50条(REPORT_FINDINGS_MAX);空数组是合法的“无发现”报告 |
单条 finding 字段(与工件逐字对齐)
设计文档明确:字段名与枚举拼写必须与 findings 工件完全一致,让模型从工件中“复制”值而不是“翻译”值。实现中的 schema 定义于 FINDING_ITEM_SCHEMA:
| 字段 | 约束 | 语义 |
|---|---|---|
id | 字符串,≤64 字符,全列表唯一 | 工件 id(如"R1-2"),outcome 回执按它归位 |
severity | 'Critical' \| 'Suggestion' \| 'Nice to have' | 必填;该数组顺序即排序顺序(最严重在前) |
confidence | 'high' \| 'low' | 未验证的低投入轮次可省略 |
source | 'review' \| 'build' \| 'test' \| 'probe' \| 'lint' | 发现来源,默认review |
file | ≤4096 字符(REPORT_FINDINGS_FILE_MAX) | 仓库相对路径,或评审的"(body)"占位符;必填。上限对齐文件系统 PATH_MAX,避免截断路径导致定位错位 |
line | 整数 ≥1,须在 JS 安全整数范围内 | 可选 |
summary | 不限长度 | 一句话陈述缺陷;必填,trim 后不得为空 |
shortSummary | ≤60 字符 | 供紧凑列表 UI 使用的压缩标签;缺省时由summary推导 |
failureScenario | ≤4000 字符 | 具体触发条件与错误结果(finding 的证据);必填 |
category | ≤64 字符 | 自由格式 kebab-case 标签(correctness、security、test-coverage…) |
direction | 'certifies-falsely' \| 'fails-closed' | Critical 的两条决策轴之一(#10291):伪造正确结果 vs. 拒绝/卡死/降级。工件没有则不填 |
baseline | 'regression' \| 'new-surface' | Critical 的另一决策轴:合并基线本可正确处理(回归)vs. 失败路径在基线不存在(新面) |
outcome | 'fixed' \| 'skipped' \| 'no_change_needed' | 仅在修复后的回执调用中携带 |
outcomeNote | ≤1000 字符 | 修复者理由;skipped时必填——读者有权知道未完成工作的原因 |
必填字段只有四个:severity、file、summary、failureScenario。注意summary在工具 schema 中刻意不设 maxLength(测试用例钉死了这一点),因为工件不限制它,收紧上限会拒绝工件本身能容纳的列表。
排序规则:严重度 → 置信度 → 位置
实现中sortReportedFindings(report-findings.ts)与工件自身的sortFindings排序逻辑对齐:
- 严重度:按
FINDING_SEVERITIES数组下标(Critical 最前); - 置信度:
high排在缺省(未验证)之前,缺省排在low之前——这是对工件排序的唯一扩展:工件要求confidence必填,而本契约允许低投入轮次省略; - 位置:
file按代码单元(code-unit)比较,行号缺失的排在行锚定之前(?? 0),最后用id打破同文件同行平局,使排序完全确定。
测试用“缺失行优先 + id 按代码单元”以及“混合大小写路径”两组用例锁死了这些细节(report-findings.test.ts),并特意避免localeCompare——ICU 排序与代码单元顺序在a与B这类大小写混合场景下不一致。
shortSummary 压缩
compressFindingSummary(report-findings.ts)把超长标签压到 60 字符以内:
- 先把换行等空白折叠为单空格(避免源码散文里的换行进入单行列表单元格);
- 截断点尽量落在词边界(在接近上限的合理位置找空格),使标签读起来像从句而非被切断的单词;
- 省略号用单个字符
…(U+2026)而非三个点; - 硬截断落在代理对(surrogate pair)中间时回退一个单元——未配对的 high surrogate 不是字符,终端消毒与显示压缩等其它截断路径同样按码点边界切割。
优先级为:调用方提供的shortSummary(若合法)→ 压缩后的summary;两者都会再走压缩(normalizeFinding)。
校验拒绝规则
validateToolParamValues(report-findings.ts)在构建期拒绝:
- 控制字符:
id、file、summary、shortSummary、failureScenario、category、outcomeNote均不得含控制字符;其中三个散文字段(summary/failureScenario/outcomeNote)放行换行(行内空白合法),而file、shortSummary等单行字段连换行都拒绝; - 空必填字段:
file/summary/failureScenariotrim 后为空即报错,并带出索引; - 非安全整数行号:
line必须是 JS 安全整数(Number.MAX_SAFE_INTEGER + 1这类 JSON 可表示的舍入值也会被拦下); - 重复 id:
id全局唯一,否则报duplicate id "…"; - 部分 outcome:
withOutcome > 0 && withOutcome < length时报错——outcome 要么全有,要么全无。这正是设计文档强调的“部分覆盖是失败模式”:修复了 9 条中的 6 条只回报 6 条,并没有对任何一条撒谎,但它悄悄缩短了清单,读者无法得知消失的 3 条。
全部校验逻辑都有对应测试用例(report-findings.test.ts)。
枚举的“三副本”结构与职责边界
一个值得注意的实现约束:这些枚举拼写在仓库里存在三处刻意保留的副本(见 report-findings.ts 的注释):
- 核心定义:packages/core/src/tools/report-findings.ts 中的
FINDING_SEVERITIES、FINDING_CONFIDENCES、FINDING_OUTCOMES、FINDING_SOURCES、FINDING_DIRECTIONS、FINDING_BASELINES——这是权威源; - CLI 历史名称重导出:findings.ts 以历史名称
SEVERITIES、CONFIDENCES、OUTCOMES、SOURCES、DIRECTIONS、BASELINES重导出核心常量,供工件命令使用; - Web Shell 渲染器:
CodeReviewArtifactDetail.tsx是浏览器 bundle,不能导入 Node 侧包,保留自己的副本,并且对未知值失败关闭(fail closed)——在核心新增一个枚举值必须同步更新渲染器副本,否则已保存工件中携带该值会导致渲染中断。
direction/baseline两条轴只有核心与 CLI 两份副本——渲染器刻意忽略这两个字段,不参与词汇快照。
两次调用模型:报告 + 结果回执
设计文档把工具的使用模式定义为两次调用,而“第二次调用是第一次值得信任的原因”:
- 首次调用(Step 6):在写入 findings 工件之后立即调用一次,
level取本次评审投入档位,每条 finding 的字段逐字复制自刚写出的工件(不要重新推导或改述——工件是 oracle)。低投入(low effort)轮次没有工件,用level: "low"上报未验证清单,且confidence: "low"只打在保留Confidence: low标记的候选上,其余省略——因为low档位本身已给整份列表打上“未验证”标签(见 SKILL.md 与 SKILL.md 的 Step 6 规则)。 - 结果回执(Step 6B 及之后):
--fix运行后,重新调用,每条 finding 携带outcome(skipped的还要带outcomeNote)。这条规则比 Step 6B 更长寿:会话中任何时刻某条已上报 finding 的处置发生变化(用户说fix these issues、某 finding 被证明有误、修复在对话中途落地),都要把 outcome 记回工件并重新发起调用——SKILL.md 明令“客户端逐条状态只信任携带 outcome 的调用”,否则“树已关闭的 finding 在所有客户端上仍渲染为打开”。
两次调用的共性:后一次调用整体替换清单,绝不追加。失败时披露并继续,不影响工件与裁决。
身份门:activeReportIds
为避免 outcome 回执悄悄改写清单,工具实例维护一个活进程契约——activeReportIds(report-findings.ts):
- 当且仅当最近一次已交付的报告所有 finding 都带 id 时,才记录该 id 集合;
- 带 outcome 的替换调用必须完整匹配该身份:不得丢任何一条(
drops N finding(s) from the active report),也不得引入清单外的新 id(carries finding(s) the active report does not have)——因为 outcome 调用替换整个清单,必须把每条活跃 finding 连同 outcome 一起重报; - 身份在交付成功时提交,而非构建时(report-findings.ts):调度器会预先构建一批调用并可能在执行前丢弃(预校验取消、中止信号),一个构建了但从未交付的报告绝不能顶替客户端实际收到的身份。测试 report-findings.test.ts 专门钉死了“未交付不阻塞”这一行为;
- 冷会话恢复没有身份:
--resume或进程重启会构造全新实例,其 outcome 调用按自身条件(全有或全无)校验,而不是对照重启前的报告。跨重启持久化身份被刻意排除在范围之外——转录层的替换机制不依赖它(测试 report-findings.test.ts 验证了同样的子集在旧实例被拒、在新实例被接受)。
与 CLI 工件命令的衔接
qwen review findings命令(findings.ts)是工件的权威写入方,参数包括--input、--out、--outcomes(JSON 数组{id, outcome, note?},必须覆盖每条 finding)、--test-delta、--to-anchors、--print。设计文档特别指出:--input也接受已保存的评审工件或先前的--out报告(任何以findings键携带数组的对象),因为 Step 9 清理会删除findings-in.json侧文件,而后置会话的 outcome 路径需要能从不被删除的幸存文件中恢复(实现见 validateFindings 的包装解包逻辑)。validateOutcomes与applyOutcomes(findings.ts)在命令侧执行同一“全有或全无”纪律:未覆盖的 finding 与未知 id 都是硬错误——前者是修复者悄悄缩短清单,后者意味着台账建立在别的清单上,合并会挂错行。
渲染端:TUI、Web Shell 与转录压缩
TUI:FindingsDisplay
终端 UI 用 ink 组件 FindingsDisplay.tsx 渲染findings_list:
- 顶部:
level: "low"时先渲染灰色横幅(low-effort pass — findings are unverified),且优先于空态分支——空列表依然是该轮次的产物,不带标记渲染会被误读为“已核验的干净报告”; - 每行:严重度着色(Critical 红、Suggestion 黄、Nice to have 灰)、id、
file:line(青色)、short summary、低置信度标记(low confidence)、outcome 徽标;fixed/no_change_needed视为已解决(绿色勾 + 删除线),skipped保持未解决并内联outcomeNote理由; - 所有插值都过
terminalSafe(剥离 C0/C1 控制字符)——outcomeNote合法携带行内空白,任何漏网的控制字符都可能伪造或覆盖被当作可信 findings 的行; - 压缩截断:历史/录制压缩对自由文本字段截断,并对整份列表应用聚合保留显示预算,保留最严重前缀、统计被逐出的尾部为
omittedFindings,渲染为(+N more findings removed by history compaction)。
转录层的“最后一次胜出”
“后一次调用替换整个列表”不止是校验,还要被渲染。模块 findings-coalescing.ts 实现了转录侧的同一规则:
- 每个转录面——实时历史、恢复的历史、录制/续播、daemon 投影——只保留最后交付的
findings_list,更早的每一条都被折叠为一行替换标记(findings replaced by a later report_findings call); - 被折叠的显示仍保留在工具上(
supersededFindingsDisplay),以便回卷(rewind)越过替换调用时恢复; - 截断/回卷修复时会先恢复被替换的显示、再对幸存者重新合并(
recoalesceFindingsHistoryItems),保证“只保留最后一份清单”的不变式在截断后的转录上依然成立。
这样,初次报告与其 outcome 回执永远不会并排出现两份清单。daemon TUI 适配器则直接透传findings_list,由 Web Shell 侧(i18n.tsx、toolFormatting.ts 等处引用)与 ACP 宿主按同一结构消费。
验证体系
设计文档列出的验证面在仓库中均有落点:
- 核心工具单测:report-findings.test.ts 覆盖排序、shortSummary 推导/压缩、空列表、outcome 计数、部分 outcome 拒绝、重复 id、控制字符、schema 违规、字段 trim 与安全整数行号;
- 压缩单测:自由文本字段截断、类型化字段幸存(
omittedFindings计数); - FindingsDisplay 渲染测试:FindingsDisplay.test.tsx 覆盖行渲染、带跳过理由的 outcome、空态;
- 回归保障:
findings.ts、save-artifact、ToolMessage、daemon 适配器、config 注册、SKILL 一致性(SKILL.test.ts)与 review-digest 测试套件保持绿色。
小结
report_findings解决的是一个具体而普遍的问题:结构化数据已经存在于磁盘工件中,却在客户端渲染面上退化成散文,进而在修复后失去处置状态。它的答案是把契约搬进带内:一次调用、逐字复制工件、findings_list结构化返回、全有或全无的 outcome 回执、以 id 集合为身份的活动进程校验门,以及“最后一次胜出”的转录替换。无论你是在终端里跑/review、在 Web Shell 里回看评审,还是以 ACP 宿主身份消费会话,这份契约都是客户端理解“哪些 finding 仍待处理、哪些已被关闭”的唯一可靠来源。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考