career-ops 的 PR 审查手册:数据契约、测试门禁与安全边界的完整执行指南
本篇技术文章基于 career-ops 仓库的审查者单页文档 REVIEWING.md,完整拆解该项目 PR 审查的三条核心规则与五项按序执行的检查清单,并结合 MAINTAINERS.md、DATA_CONTRACT.md、update-system.mjs 与 test-all.mjs 等仓库真实文件,讲解每一项检查背后的源码级实现依据。读完后,你将掌握一个 local-first、human-in-the-loop 项目在"用户数据永不被触碰"这一硬性约束下,如何让 PR 既快又安全地合并。
审查者从哪里来:contributor ladder 中的 Reviewer
REVIEWING.md 开篇明确了目标读者:贡献者阶梯(contributor ladder)上的 Reviewer,合并(merge)最终仍由 maintainer 执行,而 reviewer 的审查正是让合并"既快又安全"的关键环节。
这一角色定位可以在 MAINTAINERS.md 中找到完整定义。career-ops 的治理结构有三个层级:
| 层级 | 权限 |
|---|---|
| Contributor | 开 PR / issue,无需任何权限 |
| Reviewer | 分诊、打标签、初审 PR;无合并权 |
| Maintainer | 合并权 + 架构/评分/数据契约决策权 |
Reviewer 的晋升条件是"在多个领域有多个高质量已合并 PR,且对他人 PR 有持续有建设性的审查意见"。此外,MAINTAINERS.md 中还存在介于 Reviewer 与 Maintainer 之间的 Area owner 角色(如负责 tests/、dashboard/、providers/ 的领域负责人)——该领域内的 PR 在 owner 批准前无法合并。一个关键细节:分支保护要求每个 PR、每个人(包括 lead maintainer 本人)都必须获得一次批准,这直接呼应了 REVIEWING.md 第三条规则"每件作品都要有两个人"。
三条核心规则
规则一:Assigned means owned(被指派即拥有)
如果 PR 或 issue 指派给了你,你就是驱动它走向决定的人——审查它、打标签,或显式地交还。"沉默是唯一错误的动作"。
这一条与 MAINTAINERS.md 中关于门禁的设计哲学一致:一个 owner 缺席的阻断性门禁比没有门禁更糟,而且它会"无声地失败"——PR 只是静静地躺在那里,看起来一切正常。因此对领域负责人的期望不是"随时在线",而是"说出来":忙碌一周就声明该领域当周放开。
规则二:书面教义胜过个人口味(Written doctrine beats personal taste)
审查必须依据仓库的书面约定——CONTRIBUTING.md、DATA_CONTRACT.md、AGENTS.md 以及 modes/ 下的模式文件——而不是"我会怎么写"。如果教义缺失或错误,正确动作是开一个 issue,而不是在 review 评论中强制执行个人意见。
这里的"教义"最典型的就是数据契约。DATA_CONTRACT.md 将仓库文件划分为两层,并给出唯一不可妥协的总规则:
If a file is in the User Layer, no update process may read, modify, or delete it. (如果一个文件属于用户层,任何更新流程都不得读取、修改或删除它。)
规则三:Every piece gets two people(每件作品都要有两个人)
没有任何东西可以只被其作者审查后合并。你的批准就是那"第二双眼睛",它使得 --admin 强合并变得没有必要。这条规则与 MAINTAINERS.md 中"分支保护要求每个 PR 都有批准"的机制互为表里:流程上保证至少两人参与,规则上禁止自我审查。
检查清单:五项按序检查
REVIEWING.md 给出的检查顺序不是随意的——数据契约排在最前,被称为"the one non-negotiable(唯一不可谈判项)"。下面逐项展开,并给出仓库中的实现证据。
1. 数据契约:diff 是否触碰了用户文件
检查问题:diff 是否触碰了用户文件(cv.md、config/profile.yml、data/、reports/)?用户文件永远不在没有显式 opt-in 的情况下被写入。
这一条为什么是"唯一不可谈判项"?因为 career-ops 处理的是求职者的真实职业数据。DATA_CONTRACT.md 的用户层清单极长——除文档点名的四个路径外,还包括 config/cv-facts.json、interview-prep/sessions/*.md(含真实姓名/公司的敏感面试记录)、data/applications.md(申请追踪的 source of truth)、data/scan-history.tsv、portals.yml、reports/*、output/* 等约 40 项。
从源码结构看,这条契约由 update-system.mjs 中的 USER_PATHS 数组以代码形式固化,自动更新器在 apply 前会用它做安全校验:
// update-system.mjs L503
export const USER_PATHS = [
'cv.md',
'config/profile.yml',
'modes/_profile.md',
'modes/_custom.md',
'modes/_brief.md',
'voice-dna.md',
'portals.yml',
'article-digest.md',
'interview-prep/',
'documents/',
'data/',
'reports/',
'output/',
'jds/',
'writing-samples/',
'config/plugins.yml',
'plugins.local/',
'plugins.lock',
'opencode.json',
...
];
值得注意的是,仓库还解决了一个"契约自我指涉"难题:USER_PATHS 住在 update-system.mjs 里,而该文件本身会被上游更新覆盖、被 git 每次同步 re-merge——把"这个文件是我的"声明写在会不断覆盖它的位置上,声明会被它要约束的过程本身抹掉。因此 fork 的私有文件通过 gitignored 的 config/local-paths.txt 在运行时声明(见 update-system.mjs 中 LOCAL_PATHS_FILE 的定义),且有三类声明会被"响亮地拒绝":绝对路径或含 .. 的路径、系统层已发布的文件、以及 config/local-paths.txt 自身。审查 PR 时,如果某个改动想"顺手"把某个用户文件挪进系统层,这就是红旗。
2. 测试:test-all.mjs 是否通过,新顶层文件是否注册
检查问题:node test-all.mjs 是否通过?新行为是否附带了检查?在顶层新增的文件必须注册进 SYSTEM_PATHS(update-system.mjs)。
这里有两个容易踩坑的细节:
(a)新顶层 .mjs 文件必须注册进 SYSTEM_PATHS。 SYSTEM_PATHS 数组定义了自动更新器允许覆盖的系统层文件白名单。一个未注册的新顶层脚本不会出现在任何已安装用户的下一次更新中——代码合并了,但存量用户永远收不到它。审查时对照 SYSTEM_PATHS 检查 diff 是否同步更新了这个数组,是必做动作。
(b)--only 是开发便利,不是 PR 门禁。 从 test-all.mjs 的头部注释可以确认:
node test-all.mjs --quick # 跳过 dashboard 构建(更快)
node test-all.mjs --only <substring> # 只运行 tests/**/*.test.mjs 中匹配的文件
# LOUD WARNING: `--only` 跳过所有内联核心检查节
# (syntax、scripts、dashboard、data contract、paths 等)。
# 绿色的 --only 运行 不是 绿色的完整套件。
(c)新测试必须独立成文件。 test-all.mjs 的注释(L21-L31)记录了一次真实教训:2026 年 8 月六位贡献者同时往 test-all.mjs 末尾添加编号测试节,全部选中了 60a,导致约 15 次 rebase 和 6 次串行 CI 运行,只为 6 行测试代码。因此仓库约定:匹配 tests/**/*.test.mjs 的文件自动发现、无需注册,新测试一律独立成文件,绝不往 test-all.mjs 里加编号节。审查者看到 PR 在 test-all.mjs 中新增编号节时,应当要求改为独立文件。
3. 范围(Scope):diff 是否与 issue 匹配
检查问题:diff 是否匹配所链接 issue 要求的内容?没人要求的特性,或者超过 200 行且没有 issue 的改动,先要一场对话,再谈审查。
这一条与 CONTRIBUTING.md 的流程要求呼应:新特性、新模式或命令、架构变更应先开 issue;而 bug 修复、新的零认证扫描器 provider、文档与翻译则欢迎直接 PR,"不要让流程拖慢这些贡献"。200 行阈值是审查侧的量化护栏:它不是拒绝理由,而是"先对话"的触发器。
4. 行为变更:modes/ 是 agent 行为代码
检查问题:modes/ 下的任何变更都会改变 agent 的行为。该仓库的"house style(风格惯例)"是把带防护的描述性信号写进文本;而任何改变评分(scoring)或层级(tiers)的变更,必须上交给 maintainer。
从源码结构看,modes/ 目录(见 DATA_CONTRACT.md 系统层清单)是纯 Markdown 的 agent 指令集:modes/_shared.md 是 eval-core(评分系统、全局规则、工具),modes/oferta.md、modes/scan.md、modes/apply.md 等各自定义一个模式的指令。这意味着模式文件不是给人读的文档,而是"提示词即代码"——一行措辞的改动会直接改变数百台机器上 agent 的行为。因此:
- 描述性信号 + 文本内防护(guard)是安全的变更形态,reviewer 可以独立批准;
- 触及评分规则或 tier 划分的变更,reviewer 只能标注并转交 maintainer——MAINTAINERS.md 明确"架构、评分规则和数据契约由 lead maintainer 有最终决定权"。
5. 安全:hostname 验证、依赖与自动提交
检查问题:新 fetch 是否有 hostname 验证(解析 URL,绝不做子串匹配)?没有讨论就不得新增依赖?有没有任何东西替候选人自动提交?
这三条在仓库中都有对应的实现或拒绝清单可查:
hostname 验证的正面范例是 providers/_trust-validator.mjs。它以 URL 解析出的 hostname 做域名列表匹配,而非对原始字符串做子串比较:
// providers/_trust-validator.mjs
export function matchesDomainList(hostname, domainList) {
for (const domain of domainList) {
if (hostname === domain || hostname.endsWith('.' + domain)) {
return true;
}
}
...
}
调用侧先 new URL(url).hostname.toLowerCase() 再进入验证逻辑(见 L226-L245),用于可疑域名黑名单与 ATS 白名单的匹配,以及"公司名与 hostname 是否对得上"的启发式检查。审查新 provider 时,可以把它当作 hostname 处理的参照实现;子串匹配 URL 的方案应直接打回。
依赖与自动提交对应 CONTRIBUTING.md 的 "What we do NOT accept" 清单,其中与该检查项直接相关的有:
- 不做子串匹配的 fetch 规则之外,"启用未经人工审查的自动提交申请的 PR"被明确拒绝——"career-ops 是决策支持工具,不是垃圾机器人";
- 没有事先在 issue 中讨论的外部 API 依赖不接受;
- 抓取禁止自动化访问的平台的 PR(如 LinkedIn)被主动拒绝;
- 包含个人数据的 PR(真实简历、邮箱、电话)不接受,应使用
examples/中的虚构数据。
语气规范:温暖、merge-then-refine、留一条出路
REVIEWING.md 最后一段规定了审查语气,这也是该项目文化的显式承诺:
- 每位贡献者都值得被温暖对待,尤其是第一次贡献者。 merge-then-refine(先合并再打磨)胜过在第一个 PR 上吹毛求疵(bikeshedding);
- 当某事无法落地时,说清楚为什么,并留一条前行之路——一个不留出路的关闭(close without a path)就是摔门(a door slam)。
这条语气规则与 CONTRIBUTING.md 中对新人体验的整体设计(/assign 认领机制、7 天自动释放、abandoned PR 的公开领养阶梯)构成同一套哲学:让第一次开源贡献"是赢,而不是迷宫"。
审查者速查表
| 顺序 | 检查项 | 通过标准 | 依据文件 |
|---|---|---|---|
| 1 | 数据契约 | diff 不触碰用户层文件,或触碰有显式 opt-in | DATA_CONTRACT.md、update-system.mjs |
| 2 | 测试 | node test-all.mjs 全绿;新行为有检查;新顶层文件已入 SYSTEM_PATHS |
test-all.mjs、update-system.mjs |
| 3 | 范围 | diff 匹配链接的 issue;>200 行无 issue 先对话 | CONTRIBUTING.md |
| 4 | 行为变更 | modes/ 变更符合"描述性信号 + 文本内防护"风格;评分/tier 变更转 maintainer |
MAINTAINERS.md |
| 5 | 安全 | 新 fetch 解析 URL 做 hostname 验证;无未讨论依赖;无自动提交 | providers/_trust-validator.mjs、CONTRIBUTING.md |
| 6 | 语气 | 温暖、留出路、拒绝时给出前行路径 | docs/REVIEWING.md |
这套审查体系的设计意图可以概括为一句话:流程保证两个人参与(规则三),书面教义保证判断一致(规则二),而数据契约保证无论谁批准,用户的职业数据永远不会被代码流程触碰。 对 reviewer 而言,五项检查的顺序本身就是优先级排序——先问"它动没动用户数据",再问"测试过没过",最后才是"范围合不合适"。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00