把今天写下的每一行代码,兑换成一顿刚刚好的麦当劳。
努力了一天,应该好好放松。 你今天的努力没有被记在 OKR 里,也没有出现在周报上——但它确实发生了。 我们把它从 Git 里挖出来,折算成一顿刚好配得上今天的麦当劳。
读取今天的 Git 提交 → 评估这一天的分量(L1~L4)→ 算出热量上限 → 从真实门店菜单与优惠里挑出一份「性价比最高 / 折扣力度最大」的套餐 → 打印一张《赛博治愈对冲账单》。
零依赖,node index.js --demo all 即可跑通全流程。
给只想看结论的读者(评审、面试官、新贡献者):
- 问题:开发者的日投入缺乏客观量尺,导致「过度补偿」与「不敢犒赏」两种坏结局。
- 方法:用 Git 增删行数与提交次数构造工作量分数,映射到四个等级与固定热量预算,再在预算约束下对真实商品做多目标排序。
- 模型:
Score = 新增×1.0 + 删除×1.5 + 提交×30;等级阈值 100 / 500 / 1200;匹配权重 折扣 0.40 / 性价比 0.35 / 利用率 0.25。 - 工程:零第三方依赖,纯函数内核 + 副作用隔离在接口层,24 个单元测试,
src层行覆盖率 92.3%,CI 覆盖 Node 18/20/22。 - 可复现:结算读仓库内快照而非实时请求,无凭据、无网络也能得到完全一致的结论;快照带
capturedAt与traceId,可逐条对账。
程序员的一天是高度非线性的。写了 13 个 commit 的那天和写了 2 个的那天,身体感受完全不同,但日历上看起来一模一样。
于是出现两种坏结局:
- 过度补偿:今天只改了三行文档,晚上却点了全家桶,负罪感到凌晨两点。
- 不敢犒赏:今天线上着火救了一整天,最后只吃了碗泡面,觉得"也没干什么"。
这两种情况是同一个问题:没有一个客观的量尺,去衡量"今天值多少放松"。
工时是自报的,会撒谎;Git 是副产品,不会。这个项目就用 Git 里的增删行数,给今天的努力定个价。
需要说明的是,行数只是可自动获取的最优代理指标,它区分量级而不区分难度。这既是这个模型的价值,也是它最大的局限——见第十一节。
不需要 npm install,没有任何第三方依赖。
# 1. 看看今天(当前 Git 仓库,最近 24 小时)
node index.js
# 2. 没有提交也能看效果:跑一遍内置的四档演示
node index.js --demo all
# 3. 单独看某一档
node index.js --demo l4
# 4. 把 git 统计贴进来算(不用是 Git 仓库)
git log --shortstat -3 | node index.js --input -
# 5. 导出一份 Markdown 账单 / MCP 调用记录
node index.js --demo l3 --out report.md
node index.js --demo all --record workbuddy.md想先确认环境没问题:
npm run verify # 自检 + 24 个单元测试
npm run test:coverageflowchart LR
A["Git 采集<br/>增 / 删 / 提交数"] --> B["工作量评分<br/>权重 1.0 / 1.5 / 30"]
B --> C["L1–L4 定档<br/>阈值 100 / 500 / 1200"]
C --> D["套餐匹配<br/>硬约束 + 三目标排序"]
D --> E["赛博治愈对冲账单"]
| 等级 | 名称 | English | 判定分数 | 热量上限 | 判词 |
|---|---|---|---|---|---|
| L1 | 微光轻步 | Chill | Score < 100 |
400 kcal | 今天轻松搞定,维持充沛状态。 |
| L2 | 踏实前行 | Normal | 100 ≤ Score < 500 |
700 kcal | 踏实推进业务,脑力平稳燃烧。 |
| L3 | 破浪攻坚 | Hardcore | 500 ≤ Score < 1200 |
1100 kcal | 攻克核心难点,身体急需碳水补充! |
| L4 | 拯救世界 | Legendary | Score ≥ 1200 |
2000 kcal(可用 --unlimited 切到不限) |
拯救屎山代码,宇宙级功德!今晚热量彻底解禁! |
区间左闭右开,额度是固定档位值——不做档内插值。判定只看分数落在哪一档,越简单越不容易解释不清。
Score = 新增行数 × 1.0 + 删除行数 × 1.5 + 提交次数 × 30
删除权重 1.5 高于新增 1.0:重构和消灭屎山比堆新代码更费心血。删掉 500 行祖传代码的人,值得比加了 500 行脚手架的人多吃一点。
这是本项目对业务规范唯一一处数值调整,理由是档位可达性。按 50 标定,提交次数的得分会碾压行数:
| 提交次数 | ×50 | 落入档位 | ×30 | 落入档位 |
|---|---|---|---|---|
| 1 | 50 | L1 | 30 | L1 |
| 2 | 100 | L2 | 60 | L1 |
| 3 | 150 | L2 | 90 | L1 |
| 4 | 200 | L2 | 120 | L2 |
×50 时,2 次提交(哪怕每行都是空行)就直接进 L2,L1 实际只剩 0~1 次提交,几乎不可达。 而「今天只改了个 typo,正常下班」正是 L1 这个档位存在的唯一理由。
改量的临界点可以精确求出:L1 演示场景为 18 增 / 4 删 / 2 提交,s = 24 + 2·cw,s ≥ 100 ⟺ cw ≥ 38。取 30 保留了 8 分余量。权重常量在 src/effort-scorer.js 的 WEIGHTS 里,一改就生效,测试会立刻告诉你档位漂了没有。
完整的标定表与敏感性实验见 ALGORITHM.md §2。
初版曾把「深夜提交 +12 / 周末 +10 / 热修 +25」算进分数。改版后全部移出评分,只在账单的补充说明里出现一句:
另:深夜提交 2 次、周末提交 13 次、热修/回滚 3 次——这些不计入得分,但今天确实更累。
原因很简单:计分会造成双重惩罚。热修那天通常行数本来就多,再叠一个 25 分/次的加成,会把 L4 推到几千分去,让 1200 这个门槛失去意义;而「深夜」取决于你的时区与作息习惯,把它算进客观分里是不公平的。把它降级为一句人话提示,信息没丢,标定反而干净了。
拿到热量上限后,程序读 data/live/ 里的 mcd-mcp 真实快照(菜单价格、营养热量、套餐成分、可领优惠券),拼成一份可核算的商品目录,然后逐个打分:
综合评分 = 0.40 × 折扣力度 ← (原价 − 现价) / 原价,真的省钱
+ 0.35 × 性价比 ← kcal / 元,按候选集内最大值归一
+ 0.25 × 额度利用率 ← kcal / 热量上限
硬约束:总热量 ≤ 今日上限。 超了直接排除,并明确告诉你差多少(账单里的「差一点点」)。
- 为什么留 25% 给额度利用率:纯按折扣排序会退化成「买最便宜的打折品」——L4 拿着 2000 kcal 的额度,被推荐一杯 9.9 元的冰美式,这是明显的产品事故。
- 为什么要有「点两份」:门店最顶的套餐是
全明星双人分享餐八件套(2142 kcal),超出 L4 的 2000 上限;次一档只有 949 kcal,L4 的额度永远用不到一半。于是匹配器会给每个打折套餐额外生成一个 ×2 候选(热量、价格、优惠同倍放大,折扣率不变),只有放得进额度的才会进入候选集。实测 L4 从「949 kcal / 47%」变成「1842 kcal / 92%」,而 L3 装不下双份,结论稳定停在单份。 - 热量是算出来的,不是估的:真实接口不给套餐热量,只给成分。
query-meal-detail取默认选配 → 逐项用商品名回查list-nutrition-foods的energyKcal→ 乘以份数累加。名字对不上就排除该商品,绝不猜一个数字。本轮有 20 个商品因此被排除,账单里会写明「已排除 20 个无法核算热量的商品,不做估算」。
四层,依赖单向向下,领域层不碰 I/O:
| 层 | 模块 | 职责 |
|---|---|---|
| 接口层 | index.js |
参数解析、编排、导出 |
| 数据接入层 | src/git-analyzer.js、src/catalog.js |
Git 子进程、快照读取与归一化 |
| 领域层 | src/effort-scorer.js、src/levels.js、src/meal-matcher.js |
评分、分档、约束下的选择(纯函数) |
| 表现层 | src/report.js、src/mcp-trace.js |
账单与调用记录渲染(纯函数) |
这个边界带来的直接收益:领域层可以用普通对象测试,不需要 mock 文件系统或 Git;「换数据源」只是实现一个返回 GitCommit[] 的函数,下游完全无感——项目现有的三个数据源(真实 Git / 演示场景 / 粘贴文本)就是同一契约的三次实现。
完整的模块清单、数据契约、系统不变式与 8 条设计决策记录见 docs/ARCHITECTURE.md。
| 项 | 状态 |
|---|---|
| 第三方依赖 | 0(dependencies: {}) |
| 单元测试 | 24 个,覆盖评分 / 分级 / 数据 / 匹配 / 输出五层 |
src 层覆盖率 |
行 92.3% · 分支 70.5% · 函数 87.1% |
| CI | GitHub Actions,Node 18.20 / 20 / 22 矩阵 + 端到端 smoke test |
| 自检 | npm run check:JS 语法、JSON 快照、Markdown 相对链接、包元数据 |
| 可复现 | 快照离线结算,同一份代码在任何机器上给出同一份结论 |
npm run verify # 自检 + 测试
npm run test:coverage # 覆盖率测试按「风险」而不是「行数」分配,重点盯四个最容易出错的地方:
| 层 | 覆盖内容 |
|---|---|
| 评分 | 公式正确性、删除权重高于新增、两次提交不越过 L2、深夜/周末/热修不影响得分、空列表不崩 |
| 等级 | 100/500/1200 三个断点左闭右开、固定额度 400/700/1100/2000、--unlimited 只影响 L4 |
| 数据 | 真实快照能拼成目录、套餐热量 = 成分之和、别名匹配(新地 → 朱古力新地)、查不到返回 null 不估算、--numstat 解析、统计文本解析不丢行数 |
| 匹配 | 任意额度下推荐永不超支、额度极小返回空方案而非崩溃、双份只在放得下时出现且折扣率不变、四档推荐互不重复 |
| 输出 | 账单字段与顺序符合规范、多份展示写成 (A + B) × 2、L4 双份方案点明"为什么是两份" |
| 取舍 | 选择 | 理由 |
|---|---|---|
| 数据源 | Git 而非工时 | 工时自报会失真;Git 是副产品,不可伪造、有粒度、零侵入 |
| 评分口径 | 删除权重 > 新增 | 重构的认知负荷高于堆码;等权会奖励堆砌 |
| 额度 | 固定档位值 | 账单要说人话:一句「你落在 L3,上限 1100」胜过一条分段函数 |
| 主观信号 | 深夜/周末/热修不计分 | 避免双重惩罚;「深夜」依赖时区,缺乏跨用户可比性 |
| 热量缺失 | 排除而非估算 | 估一个「看起来合理」的数字,会让整张账单的可信度归零 |
| 数据时效 | 快照而非实时 | 可复现性优先,代价是价格可能过期(账单标注 capturedAt) |
| 匹配策略 | 贪心排序而非背包 | 候选仅 34 个,组合优化可行,但多品组合无法用一句话解释 |
| L4 上限 | 默认 2000 | 有硬约束才有可计算的匹配故事;--unlimited 保留另一种口径 |
完整决策记录(ADR-001 ~ ADR-008)见 ARCHITECTURE.md §7。
### 🍔 赛博治愈对冲账单 (Cyber Healing Invoice)
* **今日工作量评估**:L4 拯救世界(Legendary)
* **代码变动详情**:新增 700 行 | 删除 320 行 | 共 13 次提交
* **工作量得分**:1570 分
* **建议热量上限**:2000 kcal
#### 💖 麦当劳 MCP 推荐治愈套餐:
- **套餐名称**:麦辣鸡腿汉堡三件套 ×2
- **包含单品**:(麦辣鸡腿汉堡 + 中薯条 + 可乐中杯)× 2
- **预估总热量**:1842 kcal(额度占用 92%)
- **优惠活动/价格**:¥67.0(原价 ¥92.0,省 ¥25.0,约 27.2% off)
- **治愈寄语**:屎山是你拦下的,功德是你赚的,这顿是你应得的。
账单之后跟一段补充说明:为什么是它 / 备选方案 / 差一点点 / 距下一档还差多少 / 哪些信号不计分 / 数据源与排除数。加 --no-extras 只输出账单主体。完整样例见 docs/sample-report.md。
| 场景 | 描述 | 新增/删除/提交 | 得分 | 等级 | 上限 | 推荐套餐 | 价格 | 热量 |
|---|---|---|---|---|---|---|---|---|
l1 |
周四 · 正常下班 | 18 / 4 / 2 | 84 | L1 | 400 | 那么大鸡排(椒盐风味) | ¥14.0 | 385(96%) |
l2 |
周四 · 稳步推进 | 180 / 40 / 4 | 360 | L2 | 700 | 麦乐鸡经典中套餐 | ¥25.0 | 649(93%) |
l3 |
周五 · 攻坚 + 加班 | 380 / 180 / 7 | 860 | L3 | 1100 | 麦辣鸡腿汉堡三件套 | ¥33.5 | 921(84%) |
l4 |
周六 · 线上救火通宵 | 700 / 320 / 13 | 1570 | L4 | 2000 | 麦辣鸡腿汉堡三件套 ×2 | ¥67.0 | 1842(92%) |
四档推荐互不重复,额度利用率都在 84% 以上——不是"努力了一天推荐你吃个甜筒"。
加 --unlimited 后 L4 会切到真正的无上限,此时答案是 全明星双人分享餐八件套(2142 kcal,56.4% off,¥49.9/原价 ¥114.5)——那份超出上限 142 kcal 的双人餐,正好是"宇宙级功德"该有的样子。
本项目调用的是真实的 mcd-mcp,工具以宿主原生形态注入。用到的 5 个接口:
| 工具 | 作用 | 本项目怎么用 |
|---|---|---|
query-nearby-stores |
查附近门店 | 选定 storeCode=1450750(上海徐汇宜山路长兴园餐厅,444 m) |
query-meals |
查门店菜单 | 49 个商品的现价 / 划线价 → 折扣来源 |
query-meal-detail |
查套餐成分 | 8 个套餐的默认选配 → 热量核算的输入 |
list-nutrition-foods |
餐品营养表 | 160 项,取 41 项与菜单相关 → energyKcal |
available-coupons |
可领优惠券 | 4 张(不含 SKU 与价格,故不参与匹配,仅作展示) |
调用过程逐条记录在 workbuddy.md(13 条真实调用,带服务端 traceId);协议细节、名称对齐策略与踩坑记录见 MCP_INTEGRATION.md。
为什么结算读快照而不是实时请求? 让任何人在没有 MCP 凭据的环境里 node index.js --demo all 也能跑出完全一样的结果。每份快照都带抓取时间与 traceId,可逐条对账。代价是价格可能过期,这一点在账单里明确标注,不做隐瞒。
隐私:全程本地执行 git log --numstat,只把「提交次数、行数」这类聚合数字用于计算,代码内容不出本机,运行时零网络请求。详见 SECURITY.md。
| 参数 | 说明 |
|---|---|
--demo [l1|l2|l3|l4|all] |
使用内置演示场景(默认 l3) |
--input <file|-> |
解析贴进来的 git log --shortstat / git diff --stat 文本 |
--days <n> |
统计最近 n 天,默认 1 |
--store <code> |
门店编码(快照默认 1450750,不一致会明确报错而不是静默回退) |
--unlimited |
L4 视为不限热量(默认 2000 kcal) |
--no-double |
关闭「同款点两份」候选 |
--out <file.md> |
导出 Markdown 账单 |
--record <file.md> |
导出 MCP 调用记录 |
--json [file] |
输出机器可读 JSON(- 表示 stdout) |
--no-extras |
只输出账单主体 |
-v, --version |
显示版本 |
-h, --help |
帮助 |
写在这里,是为了让评价者不必自己去发现:
- 行数是难度的代理指标,不是难度本身。 100 行 YAML 与 100 行无锁并发代码同分。这是本模型最大的效度威胁。
- 匹配是贪心排序,不是全局最优。 候选仅 34 个,0/1 背包可解,但多品组合的解释成本过高,当前选择解释性优先。
- 同款叠加只到 ×2。
k的上界是经验值,不是从数据推出的。 - 不建模用户偏好。 过敏原、口味、是否吃过都不在目标函数里。
- 快照有时效。 价格与供应状态以
capturedAt为准,过期快照不会自动失效。 - 单人单仓库。 没有跨仓库聚合,也不处理多人协作的贡献切分。
进一步的分析与改进方向见 ALGORITHM.md §8。
| 文档 | 内容 | 适合谁 |
|---|---|---|
| docs/ARCHITECTURE.md | 系统上下文、分层、数据契约、设计决策、失败模式 | 想读代码的人 |
| docs/ALGORITHM.md | 评分与匹配的形式化定义、参数标定、敏感性实验、局限 | 关心方法的人 |
| MCP_INTEGRATION.md | mcd-mcp 协议细节、名称对齐策略、踩坑记录 | 想复用 MCP 的人 |
| workbuddy.md | 13 条真实调用记录(含 traceId) |
想核对数据的人 |
| docs/sample-report.md | 四档完整账单样例 | 想先看效果的人 |
| CONTRIBUTING.md | 开发约定、参数变更流程、快照更新流程 | 想提 PR 的人 |
| CHANGELOG.md | 版本变更记录 | 所有人 |
| SECURITY.md | 威胁模型、隐私边界、漏洞报告方式 | 关心安全的人 |
本项目为麦当劳程序员节创意开发大赛参赛作品,由参赛者独立开发,非麦当劳官方产品。项目输出仅供参考,不构成医疗、营养或其他专业建议;餐品信息、价格及供应状态以麦当劳官方渠道的实时结果为准。详见 CONTEST_DECLARATION.md。