# env-coverage 检查与 ratchet（棘轮）模式

**日期**: 2026-04-29
**触发**: 一次 worktree 出现 SHARED_CHECKIN_SECRET_MISSING 后，决定加 CI 守门员防漂移
**适用**: 任何在 brownfield 代码库引入新质量门禁的场景

## 核心问题

发现 v1.5 加的 `SHARED_CHECKIN_SECRET` 漏更新到 `.env.example`，于是写 CI 检查 `process.env.X` vs `.env.example` 字段对齐。一跑：**49 项历史遗留 missing**，40 项僵尸 env。

如果直接硬开门禁：下一个 PR 立刻挂，需要 PR 作者修与本次改动无关的 49 个历史遗留 → 反而劝退、变成"先 disable 再说"。

## 解法：ratchet 模式

把当前所有违规 baseline 化（`scripts/ops/env-coverage-baseline.txt`），CI 仅阻断**新增**违规。

```
代码 process.env.X 集合 = A
.env.example 声明集合 = B
baseline 缓刑集合 = G

missing = (A - B) - G    ← 硬阻断
zombie = B - A           ← 警告（example 列了代码不用的）
stale_baseline = G - A   ← 警告（baseline 里代码已不引用，鼓励删）
```

每次清理一项遗留 = 在 example 加 key + 从 baseline 删行，**棘轮只能往紧的方向走**，不会松动。

## 设计要点

1. **baseline 要进 git**（不像 .env），被 PR 审视。新人不能偷偷往 baseline 加 key 来"绕过"门禁，因为 reviewer 会看到。
2. **stale_baseline 警告**也要打出来 —— 提醒"代码已不再引用这个 key，从 baseline 删一行就能收紧门禁"，让清理有正反馈。
3. **检查目标是代码 vs 模板，不是代码 vs `.env`**：本地 `.env` 是 gitignored，CI 看不见也不该看见；模板 `.env.example` 才是事实源。
4. **AMBIENT 名单要小**：只列 Linux/Node 真正自动注入的（NODE_ENV、PATH、HOME、CI 等）；项目业务 env 一律走 example。

## 适用范围（不止 env）

棘轮模式可移植到任何"想要立即强制、但当下违规太多"的场景：

| 场景 | baseline 文件 |
|------|--------------|
| ESLint 新规则上线 | `.eslintrc-baseline.json`（用 eslint-baseline 包） |
| TypeScript strict 渐进开启 | `tsc-strict-baseline.txt`（错误数对比） |
| Knip / depcheck 找未用代码 | `unused-allow.txt` |
| 新 CI 门禁本身（如此次） | `env-coverage-baseline.txt` |

通用判据：**新违规阻断、旧违规缓刑、清理路径明确（在 baseline 删一行）**。

## bash 坑

`set -u` + `declare -A x` + `${#x[@]}`（空数组）= unbound variable。改用 `set -eo pipefail`（不开 -u）就好。本脚本头里写了注释解释。

## 关联文件

- `scripts/ops/check-env-coverage.sh` —— 实现
- `scripts/ops/env-coverage-baseline.txt` —— 当前 49 项缓刑
- `.gitea/workflows/quality-gates.yml` —— `env-coverage-check` job
- `CLAUDE.md` / `AGENTS.md` —— 「环境变量」段
