Repository navigation
gate: change-risk 闸判百分位——分母换人就变红,失败率被写死成 10% #201
Description
症状
PR #195 连续两轮 CI,代码没变,判决翻了:
| 第一轮 | 第二轮 | |
|---|---|---|
| score | 67 | 68(+1) |
| percentile | 82th (vs last 49 commits) | 92th (vs last 48 commits) |
| 结论 | 🟡 medium,放行 | 🔴 high,拦下 |
+1 分换来跨过闸门。变红的不是这个变更,是它的邻居。
机制
compute_percentile(scoring.rs:28)算的是「样本里多少 commit 分数 ≤ 我」,样本来自 mod.rs:408 的 sample_history(repo, tip, N)——从当前 tip 往回数 N 个 commit。main 每合一次,分母就换一批人。
三个性质
1. 失败率被写死成 ~10%,与仓库健康无关
百分位是排名。按定义,任何时刻都恰好有约 10% 的 commit 落在 90 分位及以上。percentile >= 90 这道闸因此永远拦下约 10% 的 PR,仓库变得多干净都一样。没人能靠「变好」通过它,只能靠「比邻居差得少」。这是配额,不是阈值。
2. 判据自己已经认过机械性膨胀不算数
mod.rs:414 的注释原话:
mechanically inflates
risk_percentileregardless of whether the change is actually riskier
那里专门排除了「被评范围内的 commit 混进样本」这一个膨胀源。分母换人是同一个 bug 的另一个源,只是当时只堵了一个口。
3. 移动靶这个病,本仓判过一次死刑
#172 把巨石棘轮从「数量 vs 会漂的 baseline」改成「身份 vs 记录在案的 baseline」,理由就是移动靶给假绿灯。change-risk 留着移动靶,方向反过来——给假红灯。
实测:分布里有个空档
对 origin/main 最近 60 个非 merge commit 逐个跑 change risk <sha>^..<sha>:
n=60 min 11 | p50 57 | p75 66 | p80 67 | p90 68 | p95 69 | max 93 mean 51.08
前 8 名:93 90 69 68 68 68 68 67
66–69 是一个稠密簇,69 与 90 之间空无一物。
后果有两层:
- 稠密簇里排名由并列和噪声决定。feat(serve): agent 原生查询面——管线写码中途终于能被问一句话 (#54 #58) #195 的 68 分跟另外 4 个已合并 commit 并列——82th 还是 92th,取决于分母那批人里恰好有几个跟它同分。
- 阈值落在 70–89 任意位置,拦下的是同样两个真离群值(93、90),放行整个正常簇。这个区间里阈值取多少,结果完全一样。
| 阈值 | 拦下 |
|---|---|
| score ≥ 65 | 18/60 (30%) |
| score ≥ 68 | 7/60 (12%) |
| score ≥ 70…85 | 2/60 (3%) |
提案:闸判绝对 score,百分位只留叙述
score 已经是 0–100 语料校准值、报告里本来就有。pr-gate.yml 把 RISK_PERCENTILE_BLOCK: "90" 换成 RISK_SCORE_BLOCK: "80"——落在实测空档正中,两侧都有余量。
- 稳定:分数不随别人的提交大小变。
- 单调:变更变小/变安全,分数下降,能通过。
- 可通过:不是配额,理论上全绿是可达状态。
- 百分位继续出现在 PR 评论里回答「这次比平时如何」——排名适合做这个,不适合当闸。
诚实的代价
拦截率从 ~10% 降到 ~3%,闸变钝了。接受这个交换的理由是:被放掉的那 7%(score 66–69)本来就是靠并列噪声决定生死的,拦得不准;真离群值(90/93)照拦。百分位仍在评论里可见,人要在意排名尖峰仍然看得到,只是不再自动挡路。
剩下的漂
绝对分数也会漂:churn 90 天窗、bug-magnet 180 天窗都是时间相对的。但那种漂跟踪的是被测文件本身(它真的又挨了几次修),是该漂的。百分位漂跟踪的是别人提交的大小,与被评变更无关。
出口
-
pr-gate.yml判score不判risk_percentile,阈值 80 - PR 评论仍同时给出 score 与 percentile,footer 写清判据换成 score
- 阈值来源(上面这份 60-commit 实测)写进 workflow 注释,可复算
- 阈值不得为了放行任何在飞 PR 而调整——本票的 80 取自空档,feat(serve): agent 原生查询面——管线写码中途终于能被问一句话 (#54 #58) #195 的 68 落在正常簇内,不是压线放行
相关
- Quality judgements have no negative judgement: freeze a corpus with false-positive traps #185(质量判据缺负样本):没有假阳性陷阱,调灵敏度就能刷指标。本票动的正是灵敏度,Quality judgements have no negative judgement: freeze a corpus with false-positive traps #185 的负样本语料应覆盖它。
- gate: get_change_risk 接入每张 PR 的 check + 写时 advisory 触发面 #95(change risk 接入 PR 闸)原票。
Activity
fix(gate): change-risk 闸判绝对分数,不判会漂的排名 (#201) (#202)