awesome-copilot Bug Receipt 凭证契约:为缺陷修复构建可机器校验的证据结构
导读
本文围绕 awesome-copilot 仓库中 bug-receipt 技能 的证据契约文档 receipt-contract.md 展开,讲解如何把一次缺陷诊断、修复与验证过程,固化为一份结构化的 JSON「修复凭证(Bug Receipt)」。读者将掌握凭证的全部必填字段与取值约束、三种状态(verified / partial / blocked)各自必须满足的不变量,以及如何通过 JSON Schema 与校验脚本在本地或 CI 中自动验证凭证的合法性,让 AI 助手产生的修复结论可被机器可靠消费。
为什么需要一份机器可读的修复凭证
GitHub Copilot 在修复缺陷或处置事故时,自然语言回复天然存在歧义:哪些结论是"当场执行验证过的",哪些只是"用户提供的既有证据";哪些检查真的通过了,哪些只是"没跑过但看起来应该没问题"。当人类阅读时这些差异尚可容忍,但当用户、CI 流水线或其他工具需要消费修复结果时,就必须有一份结构明确、字段固定、状态可自动判定的工件。
这就是 Bug Receipt 契约的定位。契约文档明确划定了它的适用边界:
Use JSON only when the user, CI, or another tool needs a structured artifact. Keep the normal final answer human-readable.
也就是说,日常对话场景仍然输出人类可读的文本收据(见 SKILL.md 中定义的 BUG RECEIPT · VERIFIED | PARTIAL | BLOCKED 文本模板),JSON 凭证只在需要结构化产物时才出场。契约的全部强制要求,最终都被落成了一份可执行的 JSON Schema(receipt.schema.json)和一个 Node 校验脚本(validate-receipt.mjs),从而把"证据规范"从散文变成了可编程的约束。
必填字段与取值约束
契约规定一份凭证必须包含以下字段,缺一不可:
| 字段 | 类型与取值 | 含义 |
|---|---|---|
version | 整数 1 或 2 | 2 用于新凭证;1 保留兼容 |
status | verified / partial / blocked | 修复状态判定 |
evidenceSource | executed-now / supplied / mixed | 证据来源(version: 2 必填) |
problem | 非空字符串 | 简洁描述缺陷与预期行为 |
baseline | 对象 { command, result, evidence } | 复现基线 |
rootCause | 对象 { summary, evidence[] } | 根因结论与证据 |
changes | 数组 { file, summary }[] | 变更清单 |
verification | 数组 { command, result, evidence }[] | 验证检查清单 |
gaps | 非空字符串数组 | 缺失的证据陈述 |
其中两类结果枚举值是独立的,不可混用:
- 基线结果(
baseline.result):failed(失败复现)、observed(观察到问题)、not-run(未运行); - 验证结果(
verification[].result):passed、failed、not-run。
在 receipt.schema.json 中,additionalProperties: false 被全局开启,且所有字符串字段均通过 nonEmptyString 定义(minLength: 1 且 pattern: "\\S")——这意味着空字符串、纯空白、未知字段一律非法。校验脚本 validate-receipt.mjs 对每个字段逐项落地了这些检查:例如 baseline 必须是对象且仅允许 command / result / evidence 三个键(第 57 行),rootCause.evidence 数组中的每一项都必须是包含 location 与 observation 的对象(第 71-77 行)。
字段详解:从证据边界到验证闭环
problem:先描述"观察到什么"与"应该是什么"
problem 要求一句话同时给出缺陷现象与预期行为,这是整个凭证的证据起点。SKILL.md 中的证据边界原则(Evidence Boundary)与本字段呼应:在动手编辑代码前,就必须先记录观察到的问题、预期行为、最强的直接检查手段以及证据来源。凭证只记录事实,不隐含推断。
baseline:复现基线的三重信息
"baseline": {
"command": "npm test -- discount.test.ts",
"result": "failed",
"evidence": "Expected 90, received 100."
}
command:可精确复现缺陷的命令或交互;result:只能是failed/observed/not-run三者之一;evidence:决定性的观察结果,或说明为何无法取得。
not-run 是一个被明确允许但携带强烈信号的取值——它意味着"没有复现证据",凭证的状态因此不可能达到 verified。
rootCause:根因必须带"位置 + 观察"
"rootCause": {
"summary": "The subtotal was rounded before the percentage discount was applied.",
"evidence": [
{ "location": "src/pricing.ts:42", "observation": "roundCurrency(subtotal) was passed into applyDiscount()." }
]
}
契约要求根因对象由 summary(机制结论)与 evidence(证据数组)组成,且每条证据必须同时包含 location 与 observation。这正是 SKILL.md 中"在命名根因之前,必须有一个具体的位置或运行时转变"(Require a concrete location or runtime transition before naming root cause)的机器化表达——没有定位到文件与行号、没有可观察事实支撑的"根因",只是假设。
changes 与 verification:最小变更 + 决定性检查
changes:每个条目记录被修改的文件(file)与变更摘要(summary),强调"最小负责变更",避免无关清理、静默回退等动作混入;verification:每个条目是{ command, result, evidence }三元组,其中result枚举为passed/failed/not-run。契约特别规定"不要把未运行的检查伪装成通过"(Never convert an unrun check intopassed),这正是not-run枚举存在的意义。
SKILL.md 给出了按受影响界面选择"决定性证明"的指引:逻辑层要求原始失败输入或聚焦测试转为通过,UI 层要求真实交互加控制台与网络观察,持久化层要求经过真实所有者路径的写/读或重载往返,并发或生命周期问题要求重复并发触发与事务证据。verification 数组应只包含受影响契约要求的检查,而不是一场漫无目的的测试狂欢。
gaps:诚实地声明缺失的证据层
gaps 是字符串数组,逐条列出"缺失的证明"。契约的立场非常鲜明:对于 partial 或 blocked 状态,必须指明能弥合决定性缺口的那一个最小实验或证据包,绝不虚构命令、观察、计数、位置或结果。
状态不变量:三种状态的硬性判定规则
契约为每种状态定义了不可违反的不变量,这些规则同时被写进了 receipt.schema.json 的 allOf 条件约束(第 74-98 行)与 validate-receipt.mjs 的运行时校验中。
verified:证据闭环的完整拼图
要达到 verified,以下条件全部必须满足:
- 基线必须被观察到:
baseline.result只能是failed或observed,绝不能是not-run; - 至少一条具体根因证据:
rootCause.evidence非空,且每条都有location与observation; - 至少一处变更:
changes数组至少包含一个文件或工件; - 至少一项验证:
verification非空; - 所有验证结果均为
passed:存在任何failed或not-run项即失败; - 无缺口:
gaps必须为空数组(Schema 中体现为maxItems: 0,校验脚本第 111 行对应报错 "Verified status cannot contain proof gaps.")。
partial:保留证据,标注缺口
partial 表示"有可用证据,但某个必需证明层缺失或不具决定性"。其不变量是:
- 保留已获得的所有证据,不因状态降级而删除;
- 把每一个缺失或不确定的证明层写入
gaps(因此gaps至少要有 1 项,见校验脚本第 114 行); - 绝不把未运行的检查改写为
passed。
blocked:外部条件阻断,拒绝推测
blocked 表示"某个具体的外部条件阻止了复现、修复或证明"。其不变量是:
- 至少一个
gaps条目明确点名外部阻断条件(校验脚本第 115 行对应报错 "Blocked status must name the external blocking condition."); - 未执行的工作保持为空或标记
not-run,不推测其结果。
三种状态的分工与 SKILL.md 的状态判定口径完全一致:VERIFIED 要求"已观察基线 + 具体根因 + 负责变更 + 声明检查全部通过 + 无实质缺口";PARTIAL 承认证据存在但不闭环;BLOCKED 承认外部条件卡住了进程。对 partial / blocked,还必须给出弥合缺口的最小下一步实验。
版本兼容:evidenceSource 的引入
契约明确了两点版本规则:
- 新凭证一律使用整数
version: 2; version: 1仍被接受,用于兼容历史工件。
二者的关键差异在于 evidenceSource 字段:它在版本 2 中是必填的(取值 executed-now / supplied / mixed),用于回答一个尖锐的问题——"这些证据是本次运行当场执行出来的,还是用户/上游提供的,还是两者混合?"。SKILL.md 强调"绝不明示或暗示所提供的证据是在当前运行中执行的"。校验脚本第 50-51 行的检查顺序体现了这一点:evidenceSource 若存在则必须落在合法枚举内;而当 version === 2 时它必须存在。
这一点对 CI 场景意义重大:一个标注 supplied 的凭证与一个标注 executed-now 的凭证,在可信度上完全不同,而机器可以通过该字段直接区分,无需猜测。
三明治式的三层验证体系
契约给出了三条互相补充的验证路径,可从三个不同位置触发:
- Schema 静态校验:凭证必须通过 receipt.schema.json 的结构校验;
- 本地脚本校验:在技能目录下运行
node scripts/validate-receipt.mjs <file> - 标准输入管道 + JSON 输出(适合 CI):
cat receipt.json | node scripts/validate-receipt.mjs - --json脚本从标准输入读取 JSON(
-参数),并以 JSON 形式输出{ valid, issues }结果,便于流水线程序化消费; - CLI 命令:当安装该技能配套的 npm 包 CLI 时,可直接使用
bug-receipt check <file>。
这些命令的行为可以在 validate-receipt.mjs 的 main() 函数(第 120-144 行)中看到:校验通过时向标准输出打印 ✓ <file> is a valid <STATUS> bug receipt. 并以退出码 0 结束;校验失败时向标准错误逐条打印 字段路径: 错误信息 并以退出码 1 结束——这一退出码约定让校验可以直接挂在 CI 的门禁检查上。
一个完整的 verified 凭证示例
校验脚本中导出的 sampleReceipt(validate-receipt.mjs)是一个可直接照抄的最小合法示例,完整展示了 verified 状态的每个必需元素:
{
"version": 2,
"status": "verified",
"evidenceSource": "executed-now",
"problem": "A 10% checkout discount returns 100 instead of 90 after currency rounding.",
"baseline": {
"command": "npm test -- discount.test.ts",
"result": "failed",
"evidence": "Expected 90, received 100."
},
"rootCause": {
"summary": "The subtotal was rounded before the percentage discount was applied.",
"evidence": [
{ "location": "src/pricing.ts:42", "observation": "roundCurrency(subtotal) was passed into applyDiscount()." }
]
},
"changes": [
{ "file": "src/pricing.ts", "summary": "Apply the discount to the subtotal before currency rounding." }
],
"verification": [
{ "command": "npm test -- discount.test.ts", "result": "passed", "evidence": "1 test passed." },
{ "command": "npm test", "result": "passed", "evidence": "42 tests passed." }
],
"gaps": []
}
对照前面的不变量逐项核验:baseline.result 为 failed(已观察)、rootCause.evidence 含 location 与 observation、changes 与 verification 均非空、全部验证 passed、gaps 为空——六条 verified 约束全部命中。
从模板开始:partial 凭证的起点
对于尚未闭环的场景,技能提供了官方起点模板 assets/receipt.template.json。它预设了一个诚实的最低信息骨架:
{
"version": 2,
"status": "partial",
"evidenceSource": "supplied",
"problem": "Describe the observed defect and intended behavior.",
"baseline": {
"command": "Record the exact reproduction command or interaction.",
"result": "not-run",
"evidence": "State the decisive observation, or why it could not be obtained."
},
"rootCause": { "summary": "State the evidence-backed mechanism, or mark it unresolved.", "evidence": [] },
"changes": [],
"verification": [],
"gaps": ["Replace this with the exact missing proof layer."]
}
注意模板的默认设计本身就演示了契约精神:基线显式标记 not-run、根因证据为空、changes 与 verification 为空、gaps 保留一条占位——这是一份合法但明确未闭环的凭证,与"留空假装完整"形成了鲜明对比。SKILL.md 建议:需要 JSON 工件时从该模板出发,写入任务专属路径,再用 node scripts/validate-receipt.mjs <receipt.json> 校验;除非用户明确要求,生成的凭证不提交到仓库。
实战建议
- 只有结构化消费者才使用 JSON:向人类汇报仍以 SKILL.md 的文本收据为准,JSON 凭证是给用户、CI 和其他工具准备的;
- 先定证据边界,再写字段:
evidenceSource与baseline.result应在动工前就确定,避免事后把"推测"升级为"事实"; not-run是诚实的盟友:未执行的检查写not-run而非passed,这既是契约义务,也是保护凭证可信度的底线;- 把校验挂进门禁:借助
node scripts/validate-receipt.mjs - --json的标准输入/JSON 输出能力和非零退出码,可在 CI 中自动拦截"伪造完整"的凭证; - 追溯实现细节:字段约束与状态不变量的最终解释权在 receipt.schema.json 与 validate-receipt.mjs 两份文件中,任何对契约的疑问都应回到这里核验。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



