robmclarty/checkride
GitHub: robmclarty/checkride
checkride 是一个为 AI 编程智能体设计的验证 pipeline 工具,通过单一命令定义「工作完成」标准并强制模块边界,让智能体知道何时停止且不越界。
Stars: 1 | Forks: 1

# checkride
**一个命令就能告诉编程智能体——以及你——工作到底什么时候才算真正完成。**
checkride 是一个 npm 包,它将你的整个验证 pipeline 作为一个单一命令 (`pnpm check`) 来运行,其退出码即为最终判定,并在机制上强制执行 module 边界,从而使并行的更改互不干扰。
这两点共同为 LLM 智能体提供了它原本缺乏的两样东西:完成的定义,以及必须遵守的边界。
它首先是为 TypeScript 构建的,但并非仅限 TypeScript:当 repo 中不存在内置检查所需的工具时,它们会自动退出;而自定义检查可以运行任何命令——因此,同样的关卡也适用于纯 JavaScript 包、多语言 monorepo,甚至完全不同的生态系统(不过目前两者都是通过 npm 分发的)。
## 核心理念
智能体擅长编写代码,却不擅长知道何时该停止。Checkride 解决了这两个问题。
1. **完成的定义。** 一个命令即可运行整个验证 pipeline —— 类型、lint、结构、死代码、测试、文档、链接、拼写。**Exit 0 意味着工作已完成。** 智能体不再盲目猜测;人类不再需要复查半成品。
2. **结构化的边界。** 代码被组织成具有狭窄公开表面的 module。一个 module 最初只是一个单文件;当它有了值得隐藏的内部实现时,它就会变成一个文件夹,其唯一的公开表面是它的 `index.ts`,而同级的 module 只导入该 index——绝不直接导入内部实现。(这些是 **deep module** —— Ousterhout 在《*A Philosophy of Software Design*》一书中提出的术语,指的是隐藏了大量实现的小型接口。)checkride 在机制上强制执行这些规则,这使智能体保持在各自的轨道内,并让人类和智能体能够并行工作,同时将 merge 冲突降到最低。
首先需要了解的一个设计选择:由于输出的消费者是 LLM,checkride 永远不会将诊断信息标准化为通用格式。每个工具都会将其原始 JSON 写入 `.check/`,然后由智能体去读取工具输出的任何内容。跳过标准化就去除了那一层适配器维护代码,而这正是让传统的“运行我所有工具”的封装器变得难以扩展的原因。
## 安装
对于**现有的代码库**,安装 checkride(使用精确锁定版本——参见 [锁定策略](./docs/contract.md)),并让 `init` 采纳你已有的工具:
```
pnpm add -D -E checkride
pnpm exec checkride init
```
对于**新项目**,在一个空目录中运行 `init` —— 无需先安装;`dlx` 会获取 checkride,并且生成的 `package.json` 会将其锁定:
```
pnpm dlx checkride init --shape flat --name my-app
pnpm install
pnpm check
```
这两种方式最终的结果是一致的 —— `init` 会自动检测当前属于哪种情况。
它会写入一个 `"check": "checkride"` 的 script alias,因此无论该工具的名称是什么,日常使用都是 `pnpm check`。它还会写入智能体契约:一段说明“exit 0 = 完成”规则的 AGENTS.md 片段,以及位于 `.claude/settings.json` 中的 Claude Code **Stop hook**,用于在 pipeline 报错(变红)时阻止智能体结束任务。该 hook 会使用检测到的包管理器(`pnpm`/`npm`/`yarn`/`bun run check`);可以使用 `--no-hook` 跳过它。如果要将所有这些内容——alias、片段和 hook——添加到你已经设置好的 repo 中,请运行 `checkride agent-setup`。
## 文档
以任务为导向的指南位于 [docs/](./docs/README.md) 中:
- [为什么使用 checkride](./docs/why.md) —— 采用它的理由:它的卖点是什么,与 ad-hoc 脚本或任务运行器相比的 ROI 是多少,以及对常见反对意见的回答。
- [入门指南](./docs/getting-started.md) —— 前置条件,如何将 checkride 添加到项目中(新建或现有的),你的首次运行,以及日常工作流。
- [备忘单](./docs/cheatsheet.md) —— 一屏展示的命令、标志、npm-script 别名以及 `.check/` 输出文件参考。
- [工具与安装](./docs/tools.md) —— 每个 pipeline 检查运行的内容,以及当 `doctor` 报告缺少工具时如何安装它。
- [在 CI 中运行](./docs/ci.md) —— 可直接复制粘贴的 GitHub Actions 配方(以及 npm/yarn/bun 变体),以及为什么 gate 应该通过 `--strict`。
- [契约](./docs/contract.md) —— 消费者可以依赖的接口表面:退出码、`summary.json` 的 schema 规范、标志、导出和锁定策略。
- [可靠性](./docs/reliability.md) —— 为什么 checkride 足够安全,可以作为 gate 的基础。
本 README 的其余部分是参考资料:命令表面、pipeline 模型、`.check/` 输出契约、配置以及 baseline。
## 命令
pipeline 是一系列 **slot** —— 例如 `types`、`lint`、`test` 等角色 —— 每个 slot 由一个 **adapter**(即运行它的具体工具)填充(完整目录在[下方](#the-pipeline-slots-and-adapters))。命令同时引用了这两者:
```
checkride Run the default checks. Exit 0 pass / 1 fail / 2 error.
--only --skip --bail --json --changed --all --include
--digest --strict (zero checks running = exit 2 — use wherever checkride gates)
checkride init Set up a project (new or existing — auto-detected).
--shape flat|monorepo|hybrid --name --scope <@s> --license --author
--add (existing mode) scaffold blessed configs for the named empty slots
--baseline (existing mode) grandfather current debt instead of disabling slots
--force (new mode) overwrite existing files instead of refusing
--no-hook skip writing the Claude Code Stop hook
--dry-run plan only; write nothing
checkride doctor Verify environment + every slot's status (read-only, exit 0/1).
checkride fix Run every active adapter's fix command (oxlint --fix, ...).
checkride baseline Record current diagnostics as a committed baseline.
checkride agent-setup Add the "check" alias, AGENTS.md stanza + Stop hook to a repo.
--no-hook skip the Stop hook (write only the stanza)
```
在迭代过程中,缩小循环范围:`checkride --bail`, `checkride --only types,lint`, `checkride --changed`。
输出流:人类可读的进度信息发送到 stderr;stdout 仅承载机器输出 —— 即 `--json` 下的摘要 JSON,它镜像了 `.check/summary.json`。因此,`checkride --json` 会在 stdout 上生成纯净的 JSON,可以安全地通过管道传输;而默认运行时 stdout 保持为空。
## Pipeline:slot 与 adapter
**Slot** 是 pipeline 中的一个角色(顺序很重要 —— 从成本最低的开始)。**Adapter** 是填充该 slot 的具体工具。每个 slot 都有一个官方指定的默认项;备选项也已连接好以便 checkride 运行它们,但 `init` 只为官方默认项生成配置。
| Slot | 角色 | 官方默认项 | 备选项 |
| ---------- | -------------------------------------- | ------------------- | ---------------- |
| `types` | 类型检查 | `tsc --build` | — |
| `format` | 格式化 (opt-in) | `prettier` | `biome` |
| `lint` | Linting | `oxlint` | `biome`, `eslint`|
| `struct` | 结构规则 (deep modules) | `ast-grep` | — |
| `dead` | 死代码、依赖、循环、边界 | `fallow` (死代码)| `knip` |
| `dupes` | 代码重复 (opt-in) | `fallow` (重复)| — |
| `health` | 复杂度 / 可维护性 (opt-in) | `fallow` (健康)| — |
| `test` | 测试 + 覆盖率 | `vitest` | `jest` |
| `docs` | Markdown lint | `markdownlint-cli2` | — |
| `links` | 相对 markdown 链接解析 | 内置 | — |
| `spell` | 拼写检查 | `cspell` | — |
| `mutation` | 变异测试 (opt-in) | `stryker` | — |
| `security` | 依赖审计 (opt-in) | `pnpm audit` | — |
| `publint` | 包发布 lint (opt-in) | `publint` | — |
| `attw` | 跨模块系统的类型解析 (opt-in) | `attw --pack`| — |
这些工具中大部分你已经了解了。唯一可能陌生的名字是 `fallow`,这是一个基于 Rust 的原生代码库分析工具,涵盖死代码、代码重复和复杂度分析;[工具指南](./docs/tools.md) 对它以及其他所有 adapter 都有详细介绍。
零配置:对于每个 slot,checkride 会运行其配置文件存在的第一个 adapter,并跳过没有检测到工具的 slot。核心部分对任何被检查的工具**没有运行时依赖** —— 它只是 spawn ` exec `;由项目自己控制锁定的工具版本。
**Opt-in slot** (`format`, `mutation`, `security`, `publint`, `attw`) 默认不会在运行中启用,因此采用 checkride —— 或升级其版本 —— 永远不会因为你未曾要求的检查而让 repo 变红。使用 `--include ` (或 `--all`) 或**在 `checks` 中命名它**来开启某项检查:像 `"format": "prettier"` 这样的显式条目会将该 slot 纳入每次运行。随后,`checkride fix` 将与其他修复程序一起运行其写入形式 (例如 `prettier --write`)。`format` 位于 `lint` 之前,以便在 linter 检查之前代码树已经是整洁的。
`publint` 和 `attw` 是**库发布**的黄金搭档 —— 为你发布到 npm 的包启用它们,让“发布的产物是正确的”成为你完成定义的一部分。`publint` 对 `package.json` 的发布表面(exports、files、types)进行 lint;`attw` 运行 `attw --pack` 来检查你的类型在每个模块系统下是否都能正确解析(`--format json`,捕获到 `.check/attw.json`)。两者均为 opt-in,因此从不发布的普通应用不会运行它们。
### 包管理器
checkride 与包管理器无关。它通过 `packageManager` 字段或 lockfile (`pnpm-lock.yaml`, `package-lock.json`, `yarn.lock`, `bun.lock`) 检测 repo 的包管理器,默认为 **pnpm**,并将每个 adapter 规范的 `pnpm exec ` 转换为该管理器的形式 (`npx`, `yarn`, 或 `bunx`)。默认的 pnpm 运行方式保持不变;`doctor` 会报告检测到的管理器。一个例外是:opt-in 的 `security` slot 是 `pnpm audit`,其标志和 JSON 结构为 pnpm 专属,因此在出现针对每个管理器的审计 adapter 之前,它**在非 pnpm 管理器上不可用**。
## `.check/` 契约
每次运行都会写入 `.check/`。这是为智能体提供的公开 API —— 承诺的接口表面(退出码、摘要结构、标志、导出)在 [docs/contract.md](./docs/contract.md) 中有详细说明,并由 `test/contract/` 测试套件锁定。
摘要结构作为 JSON Schema 发布在 [`schema/checkride.summary.schema.json`](./schema/checkride.summary.schema.json) 中;在特定的 `schema_version` 下,字段的添加是受限的。
- `summary.json` — 汇总报告:
{
"schema_version": 1,
"timestamp": "…",
"ok": true,
"checks_run": 8, // 实际执行的检查
"total_duration_ms": 4200,
"checks": [
{ "name": "lint", "adapter": "oxlint", "description": "…",
"ok": true, "exit_code": 0, "duration_ms": 470, "output_file": "lint.json" }
]
}
当 `ok: true` 且 `checks_run: 0` 时,意味着**什么都没有验证** —— 每个 slot 都退出了(未检测到工具、被禁用或被跳过)。这种状态是**空洞的绿色(vacuous green)**:一个什么都没验证的通过。运行发生这种情况时会在 stderr 上发出响亮的警告,而 `--strict` 会将其变为 exit 2,这样 gate 就永远不会把“什么都没运行”误认为“全部通过”。任何基于 checkride 的 gate(CI、commit hook)都应该通过 `--strict`。
- `.json` — 当 stdout 解析为 JSON 时的原始工具 JSON;否则为 `.stdout.txt` / `.stderr.txt`。写入自己文件的工具(vitest `--outputFile`, stryker)继续保持原样。
当 [baseline](#baseline) 掩盖了某个 slot 的发现时,该检查将获得一个附加的 `"baselined": ` 字段,用于统计被存量豁免的诊断信息;在没有 baseline 的运行中该字段不存在,因此 `schema_version` 保持不变。
- `digest.md` — 仅在 `--digest` 下写入:一份具有 **token 限制** 的 Markdown 摘录,仅包含*失败*的 slot,这样当智能体在一个充斥着大量错误的 repo 中工作时,读取的是一个有上限的索引,而不是每一个原始文件。每个部分列出了最初的几个发现(复用 baseline 指纹提取器,或者对于没有提取器的 slot 使用原始文本的尾部),并链接到权威的 `.check/.json`,该文件永远不会被修改。限制为:每个 slot 十个发现,总共 8 kB** —— 大约两千个 token;超过上限后,剩余失败的 slot 只会列出名称而不会渲染具体内容。它**只会截断,永远不会标准化**;绿色的运行不会留下任何 digest(任何过期的 digest 都会被删除),因此它的存在永远意味着“此次运行失败了”。它是一个文件,永远不会出现在 stdout 中 —— 机器输出的划分得以保持。
要调试失败:读取 `summary.json` 找到失败的 slot,然后读取该 slot 的原始输出以获取结构化的诊断信息。在大型 repo 上,`--digest` 会写入 `digest.md` 作为有上限的起点。
## 配置
`checkride.config.json` 是可选的 —— 仅在需要偏离默认值时添加:
```
{
"$schema": "https://raw.githubusercontent.com/robmclarty/checkride/v0.4.0/schema/checkride.config.schema.json",
"extends": "@acme/checkride-preset", // inherit a shared preset, then override below
"timeout": 1200, // global per-check timeout in seconds (default: 600)
"checks": {
"format": "prettier", // enable the opt-in format slot (blessed: prettier)
"lint": "biome", // pick an alternate adapter
"spell": false, // disable a slot
"test": { "use": "vitest", "timeout": 0, "changedArgs": ["--changed", "origin/main"] },
"tidy": { // a bespoke custom check that runs FIRST, ahead of the built-ins
"command": "pnpm",
"args": ["exec", "some-formatter", "--write"],
"order": "first"
},
"licenses": { // a custom check (runs last by default)
"command": "node",
"args": ["scripts/check-licenses.mjs"]
}
}
}
```
### `$schema` 指针
`"$schema"` 指针是可选的,但推荐使用:它能在支持 JSON Schema 的编辑器(如 VS Code 等)中为 `checkride.config.json` 开启验证和自动补全。`checkride init` 会在生成的配置中写入一个锁定版本的指针;schema 本身随包一起发布在 [`schema/checkride.config.schema.json`](./schema/checkride.config.schema.json) 中。
### 自定义检查
自定义检查(以非内置 slot 名称作为键的检查)默认在内置目录*之后*运行。设置 `"order": "first"` 可使其在所有内置检查之前运行 —— 这对于在 linter 和测试检查代码树之前对其进行标准化的**定制**格式化工具非常方便。`"order": "last"` 是默认设置的显式形式。在每个组中,自定义检查按照它们在配置中出现的顺序运行。
对于格式化,官方指定的 `format` slot(prettier 或 biome)是铺设好的平坦大道(paved road)—— 使用 `"format": "prettier"` 启用它,`checkride fix` 就会为你写入格式化。`order: "first"` 自定义检查的逃生舱口与它并存,用于 slot 未覆盖的一次性格式化工具;slot 并没有淘汰它。
### 使用 `detect` 为自定义检查设置门控
在自定义检查中添加 `"detect": ["", …]` 可以基于标记文件为其设置门控:只有当 repo 中至少存在一个列出的文件时,它才会运行,否则就会被跳过 —— 是跳过,而不是失败。这使得共享配置在不都使用相同工具的各个 repo 中保持安全:对于某个 repo 缺少的工具的检查会悄悄退出,而不是亮起红灯。`detect` 仅适用于与目录一起运行的自定义检查;填充内置 slot 的自定义检查始终运行。
### 使用 `extends` 共享预设
使用 `"extends"` 来继承共享的预设 —— 可以是文件路径 (`"./base.json"`) 或已安装的包 (`"@acme/checkride-preset"`),或者使用它们的数组来分层叠加多个预设。基础配置从左到右合并,而你的本地配置优先级高于它们所有:对象进行深度合并(因此覆盖某个检查的一个字段会保留其余字段),而数组和标量直接替换 —— 数组**不会**连接。将其与上面的 `detect` 结合使用,可以发布一个全组织范围的预设,并在不都使用相同工具的各个 repo 中保持安全。如果找不到 `extends`,或者配置在循环中继承了自身,会快速失败并报错 `invalid checkride.config.json: `。
### 超时
每个检查的超时机制可以防止工具挂起,并且**默认开启**:一个可能会永远挂起的“完成定义”门控,在最糟糕的一天就辜负了它唯一的使命。默认上限是一个宽裕的**每个检查 600 秒**;达到上限的检查会收到 SIGTERM(在短暂的宽限期后收到 SIGKILL),并记录为失败,附带 `timed out after 600s` 的说明 —— 变红,绝不会是空洞的绿色。通过全局的 `timeout`(秒)进行调整,针对每个检查进行覆盖 (`"timeout": `),或者设置 `"timeout": 0`(全局或针对每个检查)以禁用该上限。在确实需要长时间运行的大型 repo 上,给 `dead`、`test` 和 `mutation` 设置更高的上限 —— 或者设为 `0`。
## Baseline
在现有 repo 上采用 checkride 不应该成为一个清理项目。**Baseline** 会将 repo *今天* 存在的诊断信息豁免,这样第一天的运行就能通过,同时任何*新*的诊断仍然会导致失败 —— 对于遗留代码,将“不要让情况变得更糟”作为完成的定义。
```
checkride baseline # record current diagnostics into checkride.baseline.json
```
`checkride.baseline.json` 位于 repo 根目录下,紧挨着 `checkride.config.json`,并且**必须被提交** —— 它必须在版本控制中才能起作用。它记录了每个 slot 的一组稳定的*指纹*(一个能在行号变动后依然存活的 `file:rule:message` 键),而不是原始输出:
```
{
"schema_version": 1,
"slots": {
"lint": ["src/legacy.ts:no-explicit-any:Unexpected any"],
"spell": ["docs/old.md::teh"]
}
}
```
一旦它存在,每一次正常的运行都是**具备 baseline 意识的**:
- 每个 slot 的当前发现都会减去被豁免的部分。当一个 slot **只剩下被 baseline 豁免的发现时,它就是绿色的**,并且**在失败时只列出新的发现** —— 原始的 `.check/.json` 仍然包含所有内容,而失败的检查会增加一个 `"baselined": ` 计数。
- Baseline 是一个**棘轮(ratchet)**:修复一个被豁免的发现会将其从文件中剔除(它只会缩小),因此技术债务不会悄无声息地卷土重来。部分运行(`--only`、`--skip`、`--changed`,或者提前停止的 `--bail`)永远不会触发剔除 —— 它无法区分未观察到的发现和已修复的发现,所以它会保持 baseline 不变。
- 绝不要为了通过检查而向 baseline 中添加内容;去修复发现的问题,或者刻意重新运行 `checkride baseline` 来重新豁免。
两点操作说明。**Merge 冲突:** 该文件是规范化的(slot 和键已排序),因此并行分支通常能顺利合并;当它们发生冲突时,通过保留双方条目并运行完整的 `checkride` 来解决 —— 棘轮机制会剔除任何已修复的问题,因此过于大度的合并会自我修复,而仍然失败的丢失条目只会作为红色检查重新浮出水面。**刻意的重新 baseline:** `checkride baseline` 会重新记录当前所有失败的项 —— 包括你可能更希望修复的全新债务 —— 因此请将重新运行它视为经过审查的更改:有意识地去执行(例如,在采用了更严格的规则集之后),并在 PR 中像审查代码一样阅读 `checkride.baseline.json` 的 diff。
只有其工具具备指纹提取器的 slot 才会参与(目前包括通过 oxlint 的 `lint`、通过 ast-grep 的 `struct`、通过 cspell 的 `spell`,以及 fallow slot `dead`/`dupes`/`health`);其他 slot(`types`、`test` 等)永远不会出现在 baseline 中。崩溃或空输出永远不会被掩盖 —— 只有当存在发现并且所有发现都被豁免时,slot 才会变绿。
要在现有的 repo 上采用,`checkride init --baseline` 会将今天失败的(可提取指纹的)slot 豁免到 baseline 中并保持启用状态,而不是将它们直接置为 `false`;没有提取器的失败 slot 仍然会回退到禁用状态。
## 项目形态
`init` 搭建了三种形态。除了 `tsconfig.json`、`fallow.toml` 和 `pnpm-workspace.yaml` 之外,它们共享一切:
- **flat** — 在 `src/` 下使用 deep-modules 布局的单一包。
- **monorepo** — 一个由 `apps/*`(可部署的叶子节点)和 `libs/*`(可复用的内部模块)组成的 pnpm workspace;libs 不能从 apps 导入。
- **hybrid** — `src/` 中的根应用加上 `packages/*` 下的内部包。
每种生成的形态开箱即用都是绿色的 —— 有一项端到端测试在强制执行这一点。在运行时,checkride 本身是与 workspace 无关的:无论是什么形态,它都**从 repo 根目录运行一次**每个工具 —— 一个 pipeline,一个 `.check/`。Workspace 意识来自于工具自身的配置,而这正是 monorepo 脚手架所设置的:`tsc --build` 遵循根目录 tsconfig 的 project references,而 vitest、oxlint、ast-grep 和其余工具会遍历整个代码树。这里没有针对每个包的编排,也无法检查单一包 —— 请使用 `--only`/`--changed` 来缩小运行范围,而不是针对目录。
## 约定
由 `ast-grep` 和 `fallow` 强制执行的 Module 边界:
- Module 是一个封装单元。单个文件就是一个 module;当它有了值得隐藏的内部实现时,将其提升为一个带有 barrel `index.ts` 的文件夹 —— 只有一个文件的文件夹纯属形式主义。
- 文件夹 module 的 `index.ts` 是其唯一的公开表面:它只负责 re-export,不包含任何逻辑。同级模块通过 `'..//index.js'` 导入它,绝不导入其内部实现。
- 仅限命名导出;没有类;相对导入要带 `.js` 扩展名(NodeNext);测试与它们覆盖的代码放在一起。
## 经过测试的边界
每次推送都会运行完整的测试套件 —— 单元测试、契约测试和端到端测试(生成的项目,已安装并进行了真实的检查) —— 在 **macOS 和 Linux** 上,在 **Node 22.18.0(确切支持的最低版本)和 Node 24** 上运行,并且 e2e 测试套件涵盖了全部四种包管理器:**pnpm、npm、yarn 和 bun**。Windows 未经过测试,也不提供相关保证;它在等待真正需要它的消费者。
承诺的接口表面(退出码、`summary.json` 结构、标志、导出)由专门的[契约套件](./test/contract/)锁定 —— 参见 [docs/contract.md](./docs/contract.md)。除了行覆盖率(强制要求 70%)之外,测试套件本身也经过了测试:Stryker 变异测试运行时的硬性下限为 55;目前的变异分数为 **69%**(运行 `pnpm mutation` 复现)。
有关智能体遵循的契约,请参见 [AGENTS.md](./AGENTS.md);有关发布流程,请参见 [CONTRIBUTING.md](./CONTRIBUTING.md);有关发布说明,请参见 [CHANGELOG.md](./CHANGELOG.md)。
## 编程 API
CLI 是主要接口,但每个命令也是一个函数。从包的根目录导入,以便在你自己的工具内部运行 pipeline;结果带有与写入 `.check/summary.json` 相同的摘要,以及 CLI 将返回的退出码:
```
import { runChecks, type RunResult } from 'checkride';
const result: RunResult = await runChecks({ cwd: process.cwd(), strict: true });
process.exitCode = result.exitCode;
```
`runChecks` 与 `runInit`、`runDoctor`、`runFix` 以及 `loadConfig`/`resolveChecks` 配对并存;adapter 注册表(`SLOTS`、`ADAPTERS`)和所有公共类型也被重新导出。完整的接口表面是该包的 `exports`,它是[契约](./docs/contract.md)的一部分。
## 许可证
[MIT](./LICENSE)
标签:AI编程助手, SOC Prime, TypeScript, 可视化界面, 安全插件, 工程化规范, 开发工具, 暗色界面, 自动化攻击