dsh-tool-mining
把一堆仓库数字变成一句能站住的判断 —— 先看谁在写代码、多久合一次, 再看这份工作有多依赖一个人,最后看 backlog 是不是在烂。
给 DeepSeek Harness 用的自建工具插件:读公开的 GitHub REST API,
不装任何 SDK,只用 fetch。
Compatibility: built and tested against dsh
0.2.0-rc.2(preview). Theapply(ctx)plugin spec is stable; verify against your own dsh version if newer.
一行安装
dsh plugin --profile desktop add github:yuehancn/dsh-tool-mining
支持的 profile:desktop(桌面版)/ web(Web 版)。装完重启 dsh 即可用。
为什么需要它
GitHub 自己就把这些数字摆在那儿了 —— 贡献者页有排名,PR 列表有日期。 问题不是拿不到数字,而是这些数字很容易被读错:
- 均值是有毒的。 一个 40 天没人理的 PR,能把一批 1~3 天合并的 PR 的均值 从 3 天拖到 10.8 天。而「这个仓库合 PR 很快」这句话,恰恰是由均值说出来的。 中位数顶着这一个离群值,说 3 天 —— 这才是真相。
- 「贡献者 300 人」可能是废话。 里面可能有一半是
[bot],剩下的人里 一个人写了 60%。300 这个数看着很热闹,但对「这人要是休假了怎么办」这个问题, 答案是 1。 - Issues API 会把 PR 混进来。 你用
/issues数 backlog,数到的其实是 「issue + PR」。而 draft PR 只存在于/pulls端点 —— 想排掉它,得再问一次。 少问这一次,backlog 就会虚高。 - 分页会悄悄截断。 不跟
Link: rel="next",你拿到的是第一页, 但你以为拿到了全部。这是最坏的一种错:答案看起来很正常,就是只有一半。
这个插件把「看清楚」和「下判断」分开:
mining_status(owner, repo) → API 通不通 / 还剩多少配额
mining_contributors(owner, repo) → 70% 集中在一人 / bus factor 1
mining_velocity(owner, repo) → 中位 3 天,均值被拖到 10.8 天(两个都报)
mining_busfactor(owner, repo) → 4 个文件只有一个人碰过
mining_issues(owner, repo) → 一半 issue 没人回,中位年龄 10 天
关键设计:mining_velocity 同时报中位数和均值。只报一个数就是替调用方
做了判断,而这两个数的分歧本身就是最有用的信息。
五个工具
mining_status
报 API 能不能通、token 配没配、这一天还剩多少请求配额、重置时间, 以及可用操作和分页上限。多页分析前先问一句 —— 匿名会话一小时只有 60 次, 读两次大仓库就见底了。
mining_contributors(owner, repo, limit?)
按提交数排名,并说明工作集中到什么程度:第一名占多少、前五名占多少、
需要几个人才能覆盖一半提交(halfOfWorkNeeds)。
| 参数 | 说明 |
|---|---|
limit |
单独列出多少人,默认全列 |
机器人和人是分开的。 份额算在人类提交里,不算 bot 的提交 ——
否则一个每天提 5 次的 dependabot 会把一个人的真实占比冲淡。
所以你会看到 topContributorShare 比「70/100」略高(是 70/98):
这是故意的,bot 不该稀释维护信号。
mining_velocity(owner, repo, weeks?, includeBots?)
从 PR 被开到被合,量出中位数和 p90,再加每周合并数的时间线 —— 让「变慢了」表现为趋势,而不是一个孤零零的数字。
| 返回值 | 说明 |
|---|---|
medianMergeDays |
主指标,扛得住离群值 |
meanMergeDays |
一起报,专门为了让两者的差距可见 |
p90MergeDays |
线性插值,不是取最近的样本 |
trend |
按周分桶,新的一周在最后 |
p90 用插值算:5 个样本 [1,2,3,8,40] 的 p90 位置是 0.9 × 4 = 3.6,
落在第 4 个和第 5 个之间 → 8 + (40−8) × 0.6 = 27.2。
如果按「取最近的样本」实现,这里会报 40 —— 那样 p90 就退化成最大值了。
mining_busfactor(owner, repo, commits?, sampleFiles?)
用一份 commit 列表同时回答两个问题:每个作者的提交数, 以及哪些文件只被一个人碰过。
比逐个文件问「谁改的」便宜得多 —— 后者是一个文件一次请求。 commit 列表本身带作者和文件列表,一遍扫完。
| 参数 | 说明 |
|---|---|
commits |
分析多少个近期提交,默认 300,上限 maxPages × perPage |
sampleFiles |
最多列几个单作者文件,默认 10 |
判据是严格大于一半:一个人占正好 50% 不算够 —— 需要第二个作者。
risk 按 bus factor 给出 high / moderate / low,话术里写明判据
(「一个账号握着超过一半的近期工作」),而不是只给一个词。
文件列表可能缺席。 commit 列表端点经常不回 files 数组。
这时候不是「没有单作者文件」,而是「这次查不出来」—— 两种结论天差地别,
所以 notes 里会明说,而不是让空数组自己暗示。
mining_issues(owner, repo, weeks?)
数 backlog:开了多少、中间那个等了多久、多少条一个回复都没有、 标签分布、以及每周新增的时间线。
做了三件过滤,每一件都写进 notes:
- Issues API 会把 PR 混进来 → 按有没有
pull_request字段排掉。 - draft PR 只存在于
/pulls→ 额外查一次开的 PR,把 draft 号收集起来排掉。 - 仓库的
open_issues_count是 issue + PR 合在一起的 → 明说这个数是 过滤后的 issue 数,和 GitHub 页面上显示的不是同一个数。
配置
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: tool-mining
config:
token: ghp_xxxxxxxxxxxxxxxxxxxx
maxPages: 5
perPage: 100
| 字段 | 默认 | 说明 |
|---|---|---|
token |
"" |
GitHub PAT。强烈建议配:匿名一小时 60 次,多页分析几秒就耗尽。公开仓库不配也能用 |
apiBase |
https://api.github.com |
API 基地址,可指向企业版 |
maxPages |
5 |
每份列表最多翻几页 |
perPage |
100 |
每页多少条 |
timeoutMs |
600000 |
单次调用预算(10 分钟) |
status/contributors/velocity/busfactor/issues |
true |
按需关掉某个工具 |
安全说明
- token 只进请求头,不进 URL、不进日志、不进错误信息。 401 的报错 只说「GitHub 拒绝了 token」,不会把 token 回显出来。
- 只用
fetch,不落任何文件,不写磁盘。 owner/repo一律encodeURIComponent——a/b c会变成a%2Fb%20c, 不会把路径拼歪(有专门的断言盯这一点)。- 错误按类型分辨,不糊成一句「请求失败」:401(token 无效)、 403 且配额为 0(配额耗尽,附重置时间)、404(仓库不存在, 或 token 看不见私有仓库)、非 JSON 响应(比如被网关挡了)。
- 分页有上限,截断了就明说(
truncated: true+ 一句 note), 不给一个「看起来完整」的半份答案。
实现说明:四个值得说的地方
1. 分页的总数要读「广告里的」per_page,不是「我要的」。
Link 头的 rel="last" 只给页号,总数得自己乘。最初写的是
最后一页 × 我请求的 perPage —— 错。GitHub 会在头里把它实际用的
per_page 写出来,两个数不一致时(比如我请求 100、它给 2),
总数就会算成 300 而真相是 6。正确做法是从 Link 头里把 per_page 抠出来
再乘。这个 bug 是逻辑测试里断言「多页取满」时抓出来的。
2. 中位数和均值要一起报,因为分歧本身是信息。
测试数据 [1,2,3,8,40]:中位 3 天,均值 10.8 天。断言里专门写了一条
「一个巨大的离群值几乎不动中位数」,和一条「一个离群值把均值拖到 202」
(另一组数据),把这两种行为钉在测试里。哪天有人把中位数换成均值,
测试会红。
3. 严格大于一半。
estimateBusFactor 的循环条件是 covered / total > 0.5。
正好 50% 不算够 —— 一个人占一半,意味着另一半在别人手里,
这个人休假仓库不会停。测试里有一条断言就叫
「exactly 50% is not enough」,专门防这个边界被改宽。
4. 份额的分母排除 bot。
mining_contributors 的 topContributorShare 分母是人类提交总数,
不是全部提交。所以同一个 70/100 的仓库,配 2 次 bot 提交后报的是 0.7143
而不是 0.70。这是刻意选择:bot 提交量不反映「知识集中度」,
把它算进分母等于让机器人稀释风险信号。
测试方式:HTTP 全部走一个 githubRequest 助手,fetchImpl 可注入。
集成与 e2e 套件装上脚本化的假 fetch,请求 URL 全部被记录、被断言 ——
整个测试套件一次网络都不碰。而且每个 fixture 装完假 fetch 会立刻调
assertNoNetwork:如果请求没打到假实现(说明打到了真网络),直接抛错。
这条守卫不是装饰 —— 它抓到了一个真实的 bug:测试往一个对象上写 fetchImpl,
而插件读的是另一个对象。
跑测试
mkdir -p node_modules/@deepseek-ai
cp -r "$HOME/.dsh/profiles/desktop/node_modules/@deepseek-ai/." node_modules/@deepseek-ai/
node _test/run-all.mjs
三个套件,346 条断言全绿(对着真实 @deepseek-ai/dsh-tools 跑,不 mock):
| 套件 | 断言 | 内容 |
|---|---|---|
test-logic.mjs |
108 | Config 默认值与覆盖、注册开关、schema 归一化、isBot(8 种写法)、中位数(含「离群值几乎不动它」)、百分位插值、均值(含「离群值把它拖到 202」)、daysBetween、bus factor(7 种分布,含「正好 50% 不够」)、周分桶(含周日锚点)、错误映射(401 / 404 / 500 / 配额耗尽 / 网络 / 非 JSON / 无 fetch)、分页(多页、触顶截断、非列表响应) |
test-integration.mjs |
99 | 五个工具各自对着脚本化 API 跑通:4 人 60/25/10/5 → bus factor 1;5 个 PR 在 1/2/3/8/40 天合并 → 中位 3、均值 10.8、p90 插值 27.2;单作者文件识别与共享文件排除;空 backlog 不给 0 而是 undefined;owner/repo 真的进了 URL 且被编码(a%2Fb%20c) |
test-e2e.mjs |
46 | 一条完整链路:status → contributors → velocity → busfactor → issues,全对着同一个自洽的仓库故事。断言「一个账号占 70%」这个结论是从原始计数推出来的,而不是被印出来的;并做两次读同一份 fixture 结果逐字节一致的确定性检查 |
三个套件都是独立进程(一个崩了不会盖住其他两个的结果),退出码汇总。
许可
MIT
No comments yet. Be the first to write one.