diff --git a/evals/baseline.md b/evals/baseline.md new file mode 100644 index 0000000..be469cd --- /dev/null +++ b/evals/baseline.md @@ -0,0 +1,202 @@ +# 评测基线(首轮 · 12 条口径) + +首次跑通 `evals/runbook.md` 的三种问法,记录结果作为后续改动的对照点。 + +> **口径提示**:本文件记录的是**首轮 12 条 ground truth 口径**下的分数。随后 `ground-truth.md` 已扩充到 25 条(补进首轮实测发现的 13 条真缺陷,见 #14 / #15),`rubric.md` 的分母同步改为 25。 +> **本文件的分数与新口径不可直接比较**——要比较,必须按新口径重跑一轮。 +> 新口径下的重跑结果尚未产生,本文件暂按首轮事实保留。 + +**结论先行** + +1. **两档模式分流成功**:问「有没有回归、能不能合」的那一轮输出了紧凑列表 + 一句结论,没有套六段骨架;另两轮问「按七维审」「给模块做体检」的输出了完整的七维覆盖表 + 拍板项 + 已查过没问题。 +2. **三轮共同漏掉的只有 2 条,而这两条恰好是标准答案站不住的那两条**(见「口径缺陷」一节)——漏报不是本 skill 的系统性短板,是标准答案的问题。 +3. **归类质量三轮接近**(66.7% / 68.8% / 68.8%,按已报条目算),**差距主要在命中覆盖**:快模式那轮命中 54%,两轮全量分别 71% / 79%。快模式少的是设计面条目,符合它「只报本次引入、能落到触发路径」的定位,不是缺陷。 +4. **2/3 轮把「两个包都叫 `package shop`」报成了问题**——这正是标准答案「不该报」清单里点名的口味问题。边界没守住,需要在 skill 里补具体例(已开票)。 +5. **标准答案漏了 13 条真缺陷**,三路合计发现,全部可复核。这意味着当前的 12 分制只覆盖了样本真实缺陷的一部分,基线偏乐观。 + +--- + +## 运行条件 + +| 项 | 值 | +|---|---| +| 日期 | 2026-10-21 | +| skill 版本 | `main` @ `594bee1`(v1.0.0 tag 之后,尚未打新 tag) | +| 被测上下文 | 三个全新子 Agent 上下文,互不污染,轮间不共享结论 | +| 样本 | `evals/fixtures/tiny-shop`,复制到仓外隔离目录后审(`scratchpads\dcr-eval\tiny-shop`) | +| 隔离 | `ground-truth.md` / `rubric.md` 不在样本目录内;样本 README 改成中性描述,不提示「有缺陷」 | +| 原始输出 | 仓外 `scratchpads\dcr-eval\runs\round-{A,B,C}.md`(不入版本控制) | + +三轮问法与期望(来自 `evals/runbook.md`): + +| 轮次 | 问法 | 期望 | 实际形态 | 判定 | +|---|---|---|---|---| +| A | 审一下这个 PR 有没有回归,能不能合 | 快模式:紧凑列表 + 结论,不套六段骨架 | 9 条紧凑条目 + 一句结论 + 三行说明 | 通过 | +| B | 按七维审 | 全量:七维 + 类型 playbook + 六段式 + 覆盖表 | 六段式 + 13 行覆盖表 + 5 条拍板 + 8 条已查过没问题 | 通过 | +| C | 给这个模块做个体检 | 全量,且应命中设计类条目 | 六段式 + 14 行覆盖表 + 2 条拍板 + 5 条已查过没问题 | 通过 | + +--- + +## 口径说明(评分怎么算的) + +按 `evals/rubric.md`: + +- **命中**:对照 `ground-truth.md` 的 G1–G12,满分 12。命中且触发路径正确记 1;触发路径含糊记 0.5;漏报记 0。**只在「证据/边界说明」里点到、没有作为一条结论提出**的,记 0.5(C1 只算表达加分,不占命中)。 +- **误报**:编造不存在的缺陷每条 −2;口味问题当问题报每条 −1;泛泛建议每条 −1;「不该报」清单里的候选每条 −1。 +- **归类正确性**:逐条取平均,满分 12。级别与标准答案一致记 1;偏差一级记 0.5;偏差两级以上、或该拍板判成缺陷,记 0。**未报出的条目记 0**,所以这一项对漏报是二次惩罚——因此同时给出「按已报条目计」的辅助值。 +- **证据强度**:每条 finding 定级(5 跑了测试/构造了最小复现;4 读了测试并确认覆盖或未覆盖;3 grep 了全部调用方;2 读了完整函数 + 相邻实现;1 只读了 diff),取该轮全部上报条目(含拍板项)的均值。声称强度高于实际做过的每条 −1;「已查过没问题」里没有验证方式的每条 −1。 +- **表达质量**:逐条,满分 12。用大白话讲清机制且配了最小对照例子记 1;讲清机制没例子记 0.6;只有结论记 0.2;术语堆砌/行号当论据记 0。拍板类条目(G12)另加一条要求:给了「两条路 + 建议 + 要你定什么」才记 1。主动说清「哪块查过没问题」并给依据,对应 C1,额外 +0.5。 +- **raw** = 命中 + 归类 + 证据均值 × 12 + 表达 − 误报扣分。 + +原始分数受标准答案缺陷影响(下一节),所以 raw 只在同口径内可比。 + +--- + +## 汇总 + +| 维度 | A(快模式) | B(按七维审) | C(模块体检) | +|---|---|---|---| +| 命中 | 6.5 / 12(54%) | 8.5 / 12(71%) | 9.5 / 12(79%) | +| 误报 | 0 条 | 1 条(−1:包名/目录名口味) | 1 条(−1:包名/目录名口味) | +| 归类(按 12 项,未命中计 0) | 4.0 / 12(33%) | 5.5 / 12(46%) | 5.5 / 12(46%) | +| 归类(按已报条目,辅助看) | 4.0 / 6(66.7%) | 5.5 / 8(68.8%) | 5.5 / 8(68.8%) | +| 证据强度均值 | 2.8 / 5 | 3.3 / 5 | 3.3 / 5 | +| 表达质量 | 6.6 / 12 | 8.2 / 12 | 8.6 / 12 | +| raw | 50.7 | 60.8 | 62.2 | +| 命中条目 | G1 G2 G3 G5 G6 G12 + G10(0.5) | G1 G2 G5 G6 G7 G9 G11 G12 + G10(0.5) | G1 G2 G5 G6 G7 G9 G11 G12 + G10(0.5) | + +三轮的证据强度都在 5 级满分之下是合理的:三轮都没有在被审目录里新建复现用例(只读约束),最高只到 4(读测试确认覆盖缺口)。 + +--- + +## 逐条对照 + +`G×` 表示命中且触发路径正确;`G×(0.5)` 表示只在证据/边界说明里点到、未作为结论提出;`—` 表示未报出。 + +| GT | A | B | C | 说明 | +|---|---|---|---|---| +| G1 Stats 指针无构造函数 → panic | G× | G× | G× | 三轮都给了具体触发(`&Checkout{}` → nil 解引用),级别都对 | +| G2 timeout 形同虚设 | G× | G× | G× | 三轮都指出了零引用 | +| G3 Code 字段从未被读取 | G× | — | — | B、C 与 G9 同根因合并成一条拍板,按标准答案的「同一根因只算一次」条款记在 G9 | +| G4 godoc 只读承诺 vs 实际写计数器 | — | — | — | **三条都没报,且 A 明确判定该承诺成立**——见「口径缺陷」 | +| G5 批次中途失败不回滚计数 | G× | G× | G× | 触发路径都对(第二单负金额 → 计数已加);A、B、C 都报成 P2,标准答案是 P1 → 归类 0.5 | +| G6 两份 moneyToCents 舍入不同 | G× | G× | G× | A 报 P1、B/C 报 P0,标准答案是 P2 → 归类 0.5 / 0 | +| G7 两侧 E_INVALID 非同一值 | — | G× | G× | A 未报;B、C 都给了 `errors.Is` 恒假的论证 | +| G8 refundWindow 硬编码 | — | — | — | **三条都没报,且 B、C 明确按「已命名、已注释、单一使用者」判为不算**——见「口径缺陷」 | +| G9 DiscountStrategy 单一实现零调用 | — | G× | G× | B、C 都归入过度设计/拍板,标准答案 P2 → 归类 0.5 | +| G10 测试盲区 | G×(0.5) | G×(0.5) | G×(0.5) | 三轮都只在证据里点了「每次都显式传 `Stats: &Stats{}`」,没有作为结论提出 | +| G11 测试用字符串比较断言 | — | G× | G× | B、C 都在 E_INVALID 那条的修法里写了「测试改用 `errors.Is`」 | +| G12 Percent 无范围校验 | G× | G× | G× | **B、C 放进了「需要你拍板」=正确处理**;A 报成 P2 缺陷 → 归类 0、表达 0.6 | +| C1 并发查过没问题 | +0.5 | +0.5 | +0.5 | 三轮都主动写明并给了依据(行号 + 锁范围推理),没有编造竞态 | + +### 每条 GT 的命中归类明细 + +| GT | 命中 A / 归类 / 表达 | 命中 B / 归类 / 表达 | 命中 C / 归类 / 表达 | +|---|---|---|---| +| G1 | 1 / 1 / 1 | 1 / 1 / 1 | 1 / 1 / 1 | +| G2 | 1 / 1 / 1 | 1 / 1 / 1 | 1 / 1 / 1 | +| G3 | 1 / 1 / 1 | 0 / 0 / 0 | 0 / 0 / 0 | +| G4 | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 | +| G5 | 1 / 0.5 / 1 | 1 / 0.5 / 1 | 1 / 0.5 / 1 | +| G6 | 1 / 0.5 / 1 | 1 / 0 / 1 | 1 / 0 / 1 | +| G7 | 0 / 0 / 0 | 1 / 1 / 1 | 1 / 1 / 1 | +| G8 | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 | +| G9 | 0 / 0 / 0 | 1 / 0.5 / 1 | 1 / 0.5 / 1 | +| G10 | 0.5 / 0 / 0.5 | 0.5 / 0 / 0.5 | 0.5 / 0 / 0.5 | +| G11 | 0 / 0 / 0 | 1 / 0.5 / 0.6 | 1 / 0.5 / 0.6 | +| G12 | 1 / 0 / 0.6 | 1 / 1 / 0.6 | 1 / 1 / 1 | +| 合计 | 6.5 / 4.0 / 6.6 | 8.5 / 5.5 / 8.2 | 9.5 / 5.5 / 8.6 | + +--- + +## 口径缺陷(本次评测暴露的标准答案问题) + +这两条不是模型漏报,是标准答案自己站不住。修法已开票,**本文件的分数按修前的标准答案(v1)记录**,修后复算见下一节。 + +### G4:godoc 的「只读」承诺没有被兑现(判定有误) + +标准答案写的是「文档承诺只读,实际写了 `c.Processed`(`:77`)与 `c.Stats.CheckedOut`(`:71`)」。但源码里那句承诺是有限定的: + +``` +// Checkout is read-only with respect to the order store: it never mutates +// orders and never writes back to the database. +``` + +限定语是「read-only **with respect to the order store**」——它承诺的是不动订单、不回写数据库。`c.Processed` 和 `c.Stats` 是 `Checkout` 自己的字段,不是订单存储,写它们不违反这句承诺。三轮里有两轮明确判定「该承诺成立」并给了推理(A:`checkout.go:67-78` 只读 `orders` 副本;B:逐行读 `Run` 确认无写操作)。把「有副作用」等同于「违反 read-only 承诺」是我当初写标准答案时的混淆。 + +B 轮提出的另一个角度更接近真实问题:承诺里提到的「订单存储 / 数据库」在这个模块里根本不存在,所以这条承诺没有对应对象——那是一条该进拍板的文档问题,不是「文档与实现矛盾」的 P1。 + +### G8:硬编码判定与 skill 自己的第 5 维细则冲突 + +标准答案要求把 `refundWindow = 24 * time.Hour` 当作硬编码报出。但 `references/dimensions.md` 第 5 维的「不算」条款写着: + +> **不算**:真不变量(协议固定值、7 天),已命名、已注释、只有一个使用者。 + +同一维度的「算」条款又写着「它必须随环境/租户/调用方/**数据规模/版本**变化」——退款窗口会随业务口径变化,也就是随版本变化,按「算」条款应该报。同一节的两句话互相打架,B、C 两轮都明确按「不算」条款判了「硬编码:无发现」,并且把依据写出来了(已命名、已注释、单一使用者)。**模型没做错,是细则自相矛盾。** + +需要人工拍板的是:退款窗口这类「业务政策值」到底算不算硬编码。我倾向要算——协议固定值(单位换算 100、HTTP 状态码)和业务政策值(退款窗口、账期)不是一回事,细则的括号里那个「7 天」例子恰好把业务政策值归进了「真不变量」,是根源。 + +--- + +## 标准答案漏掉的 13 条真缺陷 + +三路合计发现,全部可复核(每条都带具体触发路径)。这些是标准答案该补而没补的——当前 12 分制只覆盖了样本真实缺陷的一部分,所以基线偏乐观。 + +| # | 缺陷 | A | B | C | +|---|---|---|---|---| +| 1 | `ExpiresAt` 用**查询时刻** +24h,签发时刻根本没存(`issued` 只存金额)→ 退款窗口永远关不上 | P1 缺陷 | 拍板 | P1 缺陷 | +| 2 | 退款侧 `E_INVALID` 每次现造,与 `checkout.ErrInvalid` 非同一值 → `errors.Is` 恒假 | — | P2 | P2 | +| 3 | `Issue` 注释说「第二个返回值」表示是否新签发,实际是**第一个** | P2 | — | — | +| 4 | NaN / Inf 绕过 `< 0` 校验 → 静默返回荒谬总额 | P2 | 拍板 | P2 | +| 5 | 低于阈值的订单也被计入 `Stats.CheckedOut`,与 `Processed` 口径冲突 | P2 | P2 | P2 | +| 6 | `Checkout` 共享指针 + 非原子自增 → 并发丢计数 / data race | — | P1 | P2 | +| 7 | `issued` map 只增不减,无淘汰路径 → 内存单调增长 | — | P2 | P2 | +| 8 | 0 元退款被记成「已退款」→ 后续真退款静默变 no-op | — | — | P2 | +| 9 | `Refunder{}` 零值不可用(往 nil map 写 → panic) | — | — | P2 | +| 10 | `Order.ID` 全模块零使用 | — | — | P3 | +| 11 | `Issue` 收「元」、`IssuedCents` 返「分」→ 传结算总额进去是 100 倍退款 | — | — | P1 | +| 12 | 退款幂等只在进程内,重启/多实例会重复退款 | — | 拍板 | — | +| 13 | `PercentageDiscount.Apply` 先截断再减(total=105、10% → 95,少减半分) | — | 拍板 | 拍板 | + +第 1 条最值得注意:三轮里有两轮把它当成**缺陷**报出,一轮当成**待拍板**。分歧点在「这个模块到底该不该有退款窗口」——在没有任何调用方的模块里,这确实更接近设计问题。标准答案对此没有任何条目,所以目前无法评分。 + +--- + +## v2 口径复算(若 G4、G8 按上面的结论修正) + +把 G4、G8 移出「应当报出」(G4 移到「不该报」并附理由,G8 改为待人工确认的争议项),有效条目变 10 条: + +| 维度 | A | B | C | +|---|---|---|---| +| 命中 | 6.5 / 10(65.0%) | 8.5 / 10(85.0%) | 9.5 / 10(95.0%) | +| 归类(按 10 项) | 4.0 / 10(40%) | 5.5 / 10(55%) | 5.5 / 10(55%) | +| 归类(按已报条目) | 66.7% | 68.8% | 68.8% | + +命中差距拉开是正常的:G4、G8 两条谁都没报,删掉它们对三轮是同量抬升,但相对差距不变(4.6 个百分点 → 30 个百分点的绝对差)。 + +**注意**:这是**只动 G4、G8 两条**的假设复算。实际修复后 GT 扩到了 25 条(补进 13 条真缺陷),分母变成 25,**上面这张表也不能代表新口径**——新口径必须重跑。 + +**后续比较用哪个口径**:v1 口径留给本次记录(`evals/baseline.md` 现状),后续改动要与本次对比就用 v1;等标准答案扩充到 v2(补上那 13 条)之后,需要**重跑一轮**才能得到与 v2 同口径的基线,否则 v1 与 v2 的分数不可比。 + +--- + +## 方法论局限 + +- **样本与标准答案同一人写**:基线偏乐观,真实水平要打折看。这是本次评测最大的系统性偏差。 +- **每种问法只跑 1 轮**:没有方差估计。同一 skill 两轮之间有波动是已知的(README 里写了),所以单轮差异小于 2 个百分点的结论不要当结论。 +- **快模式轮的 GT 覆盖天然不全**:G8 / G9 / G10 / G11 属设计面与测试面,快模式按契约本就不报。拿 A 轮的命中分去比全量轮是不公平的,上面的对比只应看「相对各自口径」。 +- **数值复算用的是 IEEE-754 等价推演**(.NET double 模拟 Go 的 `int64()` 截断与 `math.Round`),没有在被审目录里落 Go 探针(C 轮自己声明了这点)。Go 探针会给出同样的结论,但严格性差一点。 +- **A 轮有一条依赖「该目录无 git 历史」这个前提**:五个文件全算「本次引入」。换一个真实的 PR,A 轮的行为会不同(届时要区分既有与新引入)。 +- **没有做变异对照**:没有改坏代码去验证每条 finding 是否真能让测试变红,所以「finding 是真的」靠的是读码 + 数值复算,不是测试红。 + +--- + +## 相关文件 + +| 文件 | 作用 | +|---|---| +| `evals/ground-truth.md` | 标准答案(v1)。**本文件的分数按它计算** | +| `evals/rubric.md` | 评分表与五维口径 | +| `evals/runbook.md` | 怎么跑一次评测 | +| `evals/fixtures/tiny-shop/` | 样本(约 150 行 Go) | +| 仓外 `scratchpads\dcr-eval\runs\round-{A,B,C}.md` | 三轮原始输出,不入版本控制 | diff --git a/evals/ground-truth.md b/evals/ground-truth.md index f6abe50..df9a06d 100644 --- a/evals/ground-truth.md +++ b/evals/ground-truth.md @@ -11,7 +11,7 @@ | G1 | `checkout.go:71` `c.Stats.CheckedOut++` | 实现 | P1 | `Stats` 是指针且没有构造函数,零值 `Checkout{}` 直接空指针 panic | 调用方写 `&Checkout{}` 或忘了初始化 `Stats` → 第一单就崩 | 指出「`Stats` 应为值类型 / 应提供构造函数 / 应判空并报明确错误」任一 | | G2 | `checkout.go:62` `timeout` 参数 | 方法契约 | P1 | `timeout` 形同虚设:函数体从未使用它,也没有 `ctx`,调用方的超时保护完全不起作用 | 下游读取订单卡住 → `Run` 一直等下去 | 指出「`timeout` 应生效 / 应改用 `ctx` / 应删掉这个参数」任一 | | G3 | `checkout.go:18` `Code` 字段 | 方法契约 | P1 | `DiscountRequest.Code` 从未被读取,折扣码被静默忽略 → 按原价结算 | 传 `DiscountRequest{Code: "SAVE20"}` → 折扣不生效,且没有任何报错 | 指出「`Code` 应被校验并生效 / 该字段应删除」任一 | -| G4 | `checkout.go:60-61` godoc | 文档 | P1 | 文档承诺只读("never mutates orders and never writes back to the database"),实际写了 `c.Processed`(:77)与 `c.Stats.CheckedOut`(:71) | 任何一次 `Run` | 指出「文档与实现对齐」即可(改文档或改实现都算对) | +| G4 | `checkout.go:60-61` godoc | 文档 | P2 | 承诺提到的「订单存储 / 数据库」在这个模块里**根本不存在**——没有 DB、没有 repository、没有接口。承诺本身没有对应对象 | 读者按承诺去找「订单存储」找不到,会以为这函数已经接到 DB 上 | 指出「承诺与现状对齐 / 补一个存储接缝 / 改承诺措辞」任一;**放进「需要你拍板」也算对** | | G5 | `checkout.go:68-69` + `:77` | 错误路径 | P1 | 批次中途失败时,前面的订单已经 `c.Processed++`,但函数返回 `(0, err)`,计数被丢弃 → 重跑整批 → 已处理订单被重复处理 | 批次第 2 单金额为负、第 1 单正常 | 指出「失败时保留计数 / 不推进游标 / 返回部分结果并同时带错误」任一 | | G6 | `checkout.go:83` vs `refund.go:57` | 重复 | P2 | 两个包各写一份「元→分」换算:`checkout` 用截断、`refund` 用 `math.Round` → 同一笔金额两条路径结果可能差 1 分 | 金额带小数(如 10.005 元) | 指出「合并到一处 / 对齐舍入规则」任一;并要能说出「金额一律以分为单位」这条共同需求 | | G7 | `refund.go:29` vs `checkout.go:13` | 兼容性 / 错误语义 | P2 | 同一个错误码 `E_INVALID` 有两个不同的 error 值 → `errors.Is(err, checkout.ErrInvalid)` 恒为 false,调用方只能退回字符串比较 | 退款侧的错误传到结算侧做统一判断 | 指出「统一用 sentinel / 抽共享错误码」任一 | @@ -26,6 +26,28 @@ |---|---|---|---| | G12 | `checkout.go:35-37` | `Percent` 没有校验,允许 >100 或为负;负折扣会让 total **变大** | 这是业务口径问题(允不允许负折扣)。正确处理是放进「需要你拍板」,给两条路 + 建议 + 要你定什么。**直接报成 P0/P1 算误判** | +## 第二批:首轮评测实测补进来的(G13–G25) + +首轮 A/B/C 三轮评测合计发现 13 条真缺陷,原 12 条没覆盖到。补进来后本表共 25 条。 + +| # | 位置 | 维度 | 级别 | 问题 | 触发路径 | 可接受的变体 | +|---|---|---|---|---|---|---| +| G13 | `refund.go:49-54` + `:36` | 实现 / 时间语义 | P1 | 退款窗口从**查询时刻**起算:`issued` map 只存金额不存签发时间,`ExpiresAt` 返回 `now.Add(refundWindow)` → 窗口永远关不上 | T0 签发,T0+23h 查 `ExpiresAt` → 返回 T0+47h 而不是 T0+24h | 指出「要存签发时间 / 窗口起点错了 / 该用注入时钟」任一 | +| G14 | `refund.go:27` + `:41` | 方法契约 | P1 | `Issue` 收「元」、`IssuedCents` 返「分」:同一类型两套单位 → 把结算总额直接传进去就是 100 倍退款 | 结算返回 5000 分,调用方 `r.Issue(id, float64(total))` → 存成 500000 分 | 指出「`Issue` 应收分 / 单位要统一 / 入口唯一换算」任一 | +| G15 | `refund.go:28` + `:33` | 实现 | P2 | 0 元退款被记成「已退款」:`amount < 0` 只挡负数,0.004 放行后存成 0 分并返回 `true` → 之后任何真退款都命中重复分支静默 no-op | `r.Issue("o1", 0.004)` 之后 `r.Issue("o1", 5.00)` → 返回 `(false, nil)`,一分不退 | 指出「要挡 <= 0 / 已退与金额要分开表达」任一 | +| G16 | `refund.go:20` + `:36` | 实现 | P2 | `Refunder{}` 零值不可用:忘了 `NewRefunder` 就在写 nil map 时 panic | `var r Refunder; r.Issue("o1", 1.00)` → assignment to entry in nil map | 指出「零值应可用 / 要懒初始化 / godoc 要写明必须用构造函数」任一 | +| G17 | `checkout.go:71` + `:77` | 实现 / 并发 | P1 | `Stats` 是共享指针且计数是非原子自增,两个 goroutine 共用一个 `*Stats` → 丢计数 + data race | 两个并发请求各建 `*Checkout` 但共用同一个 `*Stats` → `c.Stats.CheckedOut++` 交错 | 指出「要用 atomic / 改成值字段 / 要声明非并发安全」任一 | +| G18 | `refund.go:16` + `:36` | 并发与生命周期 | P2 | `issued` map 只增不减,无淘汰路径;`refundWindow` 只被用来算时间点,从没用于丢数据 → 常驻进程内存随退款笔数单调增长 | 每笔退款写一条 map,全模块无 `delete` | 指出「要淘汰 / 要按窗口清理 / 显式清扫入口」任一 | +| G19 | `refund.go:24-26` | 文档 | P2 | `Issue` 注释说「第二个返回值」表示是否新签发,实际 `bool` 是**第一个**返回值 | 照注释写 `err, ok := r.Issue(...)` → 编译期类型不匹配 | 指出「注释与签名对齐 / 写明 `(issued bool, err error)`」任一 | +| G20 | `checkout.go:68` + `:84` | 实现 | P2 | NaN / Inf 绕过 `< 0` 校验(NaN 的所有比较都是 false)→ 静默返回荒谬总额且 `err` 为 nil | `Order{Amount: math.NaN()}` → `int64(NaN*100)` 是实现相关的乱数 → 返回巨额负数、无错 | 指出「要校验非有限值 / 要拒绝 NaN Inf」任一 | +| G21 | `checkout.go:71` vs `:73` | 实现 | P2 | `Stats.CheckedOut` 在阈值判断**之前**自增,被跳过的订单也计成「已结算」,与 `Processed` 口径冲突 | `Run([{Amount:5.00}], {Threshold:1000})` → 合计 0 但 `CheckedOut` 为 1、`Processed` 为 0 | 指出「自增要挪到判断之后 / 两个计数要统一口径 / 要改名」任一 | +| G22 | `refund.go:24-26` + `:33` | 兼容性 | P2 | 退款幂等只存在于进程内存的 map,无持久化、无存储接缝 → 重启/多实例后同一单会退两次 | 进程重启后 `r.Issue("o1", ...)` → 判重失效,重复退款 | 指出「要抽存储接口 / 幂等要跨进程 / 至少在 godoc 写明仅限本进程」任一 | +| G23 | `checkout.go:42` | 过度设计 | P3 | `Order.ID` 全模块零使用(grep `\.ID` 零命中)→ 字段叫 ID,读代码的人会以为结算链路用它做去重或记账 | 同一订单在批次里出现两次会被结算两次 | 指出「要么用起来 / 要么删掉」任一 | +| G24 | `checkout.go:36` | 实现 | P3 | `PercentageDiscount.Apply` 先截断再减(`total - total*Percent/100`),total=105、Percent=10 → 95,比标称 10% 少减半分 | `Apply(105)` with `Percent: 10` → 95 而非 94.5 | 指出「取整方向要明确 / 要先乘后除 / 换算要与金额换算同一套」任一 | +| G25 | `checkout.go:24-37` | 设计 | P2 | `DiscountStrategy` 接口只有 1 个实现、0 个调用者,且 `internal/` 下不构成对外契约 | —— | 指出「删掉接口直接用具体类型 / 给出第二个真实使用者 / 把折扣接进 `Run`」任一 | + +**G25 与 G3 / G9 的关系**:G3(`Code` 字段从未被读取)、G9(`DiscountStrategy` 过度设计)、G25 是同一根因「折扣能力整体没接线」在三个维度上的投影。计分时按「同一根因只算一次」处理。 + ## 应当明确说「查过没问题」 | # | 位置 | 事实 | 判定标准 | @@ -36,13 +58,16 @@ | 候选 | 为什么不报 | |---|---| -| 两个包都叫 `package shop`、一个文件里放多个类型 | 命名与分包口味,`SKILL.md` 的不报清单第一条 | +| 两个包都叫 `package shop`、一个文件里放多个类型 | 命名与分包口味,`SKILL.md` 的不报清单第一条。**import 别名是 Go 的正常机制、编译器不报错** → 不算缺陷;要提只能进「需要你拍板」 | +| `checkout.go:60-61` 承诺「never mutates orders and never writes back to the database」但写了 `c.Processed` / `c.Stats` | **该承诺有限定语**「read-only *with respect to the order store*」,写 `Checkout` 自己的计数器不违反它;模块里也没有数据库可供违反。**不算缺陷**(G4 改后指的是另一个问题:承诺本身没有对应对象) | | `refundWindow` 没导出 | 未导出常量不构成公开契约问题;过度设计维度里「公开契约」是除外项 | | 泛泛的「应该补更多测试」 | 没有具体高风险分支就不算测试盲区;G10 才是合法的那条 | | `Order.Amount` 用 float64 | 属于设计讨论,不是一条带触发路径的缺陷;**若要提,必须给具体的偏差路径,不能只说「浮点不精确」** | ## 计分要点 -- **同一根因只算一次**:例如「折扣能力整体没接线」如果被拆成 G3 和 G9 两条报出来,G9 单独计分,G3 不重复加分。 +- **同一根因只算一次**:例如「折扣能力整体没接线」如果被拆成 G3 / G9 / G25 三条报出来,按出现顺序取靠前的一条计分,其余不重复加分。 - **归类正确性单独计分**:把 G12 报成 P0/P1 的,即使结论方向对也要扣分——它测的是「先判设计使然还是缺陷」这条纪律。 - **C1 的正向确认单独计分**:主动说清「这块没问题」比不报错更有价值;只在覆盖表里写「并发:无」也算,但带依据的更好。 +- **G13 可以判成 bug,也可以判成拍板**:这个模块没有任何调用方,「该不该有退款窗口」本身就是业务问题。判成拍板并给出两条路 + 建议的,**同样算命中**;判成 P1 缺陷的也算命中。但把它说成「已经确认的 bug」而不谈业务前提的,归类扣分。 +- **G8 修过口径**:现在 `refundWindow` 属「业务政策值」,按 `references/dimensions.md` 第 5 维的新细则**应该报**。仍然按「真不变量」判为不报的,不算漏报——那一版细则确实有歧义,但复评时以新细则为准。 diff --git a/evals/rubric.md b/evals/rubric.md index 88f545c..b81c8e7 100644 --- a/evals/rubric.md +++ b/evals/rubric.md @@ -6,7 +6,7 @@ ### 1. 命中(recall) -对照 `ground-truth.md` 的 G1–G12。 +对照 `ground-truth.md` 的 G1–G25。 | 项 | 分数 | |---|---| @@ -14,7 +14,9 @@ | 命中但触发路径含糊(只说「可能有问题」) | 0.5 | | 漏报 | 0 | -满分 12(按 G1–G12 计;C1 不算命中,算第 5 项)。 +满分 25(按 G1–G25 计;C1 不算命中,算第 5 项)。 + +**首轮基线用的是旧版 12 条口径**(见 `baseline.md`),与本表的 25 条**不可直接比较**。改 GT 之后必须重跑一轮评测,才能得到同口径的基线。 ### 2. 误报(precision) @@ -37,7 +39,7 @@ | 级别偏差一级 | 0.5 | | 级别偏差两级以上,或该拍板判成缺陷 | 0 | -逐条计,取平均。 +逐条计,取平均。**未报出的条目记 0**,所以这一项对漏报是二次惩罚——同时给出「按已报条目计」的辅助值,那个值更能反映判级能力本身。 ### 4. 证据强度 @@ -77,19 +79,19 @@ raw = 命中 + 归类正确性 + 证据强度 + 表达质量 - 误报扣分 | 4 ~ 7.9 | 噪声偏多 | | < 4 | 不建议使用这个输出 | -命中项满分 12,加上归类(12 项×1)、证据(12 项×~2)、表达(12 项×~0.7), -满 raw 约 50+。**换算时请按维度占比看,不要只看绝对值**——不同轮次之间本来就有波动。 +命中项满分 25,加上归类(25 项×1)、证据(25 项×~2)、表达(25 项×~0.7), +满 raw 约 100+。**换算时请按维度占比看,不要只看绝对值**——不同轮次之间本来就有波动。 ## 记录格式 ```markdown ### <日期> · · <问法> -- 命中:__/12(G__ 命中) +- 命中:__/25(G__ 命中) - 误报:__ 条(__) -- 归类正确:__/12 +- 归类正确:__/25(按已报条目计:__) - 证据强度:均值 __ -- 表达质量:__/12 +- 表达质量:__/25 - raw:__ - 备注:漏了 __(为什么没抓到);多报了 __(误判在哪) ``` diff --git a/evals/runbook.md b/evals/runbook.md index 30a9594..d11c7a0 100644 --- a/evals/runbook.md +++ b/evals/runbook.md @@ -53,4 +53,13 @@ A 轮验的是**快模式本身**:合并判定必须由这个 skill 直接给 ## 归档 -把评分表贴到对应 issue 的评论里。这样每个 PR 都能看到「这个改动让审查变好还是变坏」。 +把每轮的输出原样存下来(不要摘要),再对照 `ground-truth.md` 打分, +按 `rubric.md` 的格式写出评分表。 + +**存哪**: + +- 原始输出(每轮一份全文)**不入版本控制**,放仓外(例如 `scratchpads\dcr-eval\runs\round-{A,B,C}.md`)。 +- 评分表入版本控制,追加到 `evals/baseline.md`;跑完一版就追加一节,**不要覆盖旧记录**——旧记录是「当时按当时的标准答案跑出来的结果」,属历史事实。 +- 改了 `ground-truth.md` 或 `rubric.md` 之后,旧分数与新分数**不可比**,必须在记录里写明,并重跑一轮拿新口径的基线。 + +**贴到 issue**:如果这次评测是为了验证某个改动,把评分表贴到那个改动的 issue 或 PR 评论里。这样每个 PR 都能看到「这个改动让审查变好还是变坏」。 diff --git a/references/dimensions.md b/references/dimensions.md index 5b19cba..2bc213e 100644 --- a/references/dimensions.md +++ b/references/dimensions.md @@ -92,6 +92,14 @@ **不算**:只是啰嗦、错别字、纯排版。 +**命名 / 分包这类口味问题,也不要报成缺陷。** 判据三条,全过才谈得上「值得提」: + +1. **编译器和工具链会不会因此出错?** `import` 别名是 Go 的正常机制,编译器不报错、`go doc` 也能正常生成 → 属口味,不报。 +2. **同项目里有没有相反的惯例?** 若项目别处包名都跟目录名对齐,才可能说「这里不一致」;项目内部本来就五花八门,那就不是这一条。 +3. **能不能不改?** 改包名要动所有 import,代价与收益不成比例 → 更该不改,不报。 + +真要提,只能作为「需要你拍板」里的风格统一建议(一条路:统一改包名;另一条:保持现状并写进 README),**不能进「必须修 / 建议修」**。反过来,别因为这条就把真正的文档缺陷放过去——godoc 与实现行为不符、注释把返回值顺序说反了,这些要照报。 + ## 5. 硬编码 **核心问题**:哪个值本该可变,却被写死了? @@ -108,7 +116,19 @@ **判断分两档** - **算**:它必须随环境/租户/调用方/数据规模/版本变化;或它会随外部世界变化而静默过期(证书、URL、表结构)。 -- **不算**:真不变量(协议固定值、7 天),已命名、已注释、只有一个使用者。 +- **不算**:**协议固定值**(单位换算的 100、HTTP 状态码、分页默认大小),已命名、已注释、只有一个使用者。 + +**先分清是哪一类**(这是最容易判错的一步): + +| | 协议固定值 | 业务政策值 | +|---|---|---| +| 例子 | 单位换算 100、HTTP 状态码、分页默认大小 | 退款窗口、账期、免费额度、保留时长、超时上限 | +| 改了会怎样 | **改了就是 bug**(协议说 100 就是 100) | 只是业务口径变了,代码本身没错 | +| 算不算硬编码 | 不算 | **算** | + +判据一句话:**改了会出错 → 不算;改了只是口径变了 → 算。** + +业务政策值即使已命名、已注释、只有一个使用者,**也要报**——「写死了」不是豁免的理由,「它是业务政策」才是。报的时候按「需要变化的边界」选最小一档:命名常量 < 函数参数 < 配置项 < 运行时发现。不要一律上配置,也不要一律上 P2。 **修法按「需要变化的边界」选最小一档**:命名常量 < 函数参数 < 配置项 < 运行时发现。不要一律上配置。