# API Contract Validation
本文档说明如何验证 OpenLark 的 typed API 实现是否和飞书开放平台官方接口契约一致。
这套校验补充 `tools/validate_apis.py` 的覆盖率口径。覆盖率只回答“接口文件是否存在”,contract validation 进一步回答“实现里的 HTTP endpoint、request 字段和 response 字段是否和官方文档一致”。
## 1. 校验层级
### 1.1 Endpoint 离线校验
默认推荐入口:
```bash
just api-contracts
```
等价命令:
```bash
python3 tools/validate_api_contracts.py --all-crates --strict endpoint
```
该模式不访问网络,使用仓库根目录的 `api_list_export.csv` 作为官方快照,检查:
- Rust 实现文件是否存在;
- `ApiRequest::get/post/put/patch/delete(...)` 方法是否匹配 CSV 中的 HTTP method;
- Rust endpoint 常量或简单 `format!` 路径是否匹配 CSV 中的 `/open-apis/...` path。
报告输出:
- `reports/api_contracts/summary.md`
- `reports/api_contracts/summary.json`
- `reports/api_contracts/crates/<crate>.md`
- `reports/api_contracts/crates/<crate>.json`
CI 启用的 strict gate(`api-contracts` job):
- 全仓离线 endpoint(`--all-crates --strict endpoint`)
- token 层:`openlark-security` + `openlark-auth`(见 §1.4)
- field 层:`openlark-hr --biz-tag attendance`(#526/#533)与
`openlark-docs` 全 crate(#569,ccm/base/baike/minutes;0.20 首个域扩展)
### 1.2 Endpoint live 校验
需要确认当前官网详情页是否和 checked-in CSV 快照一致时,使用:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-ai \
--live-endpoints \
--strict endpoint \
--report-dir /tmp/openlark-api-contracts-live-endpoints
```
该模式会访问飞书文档详情接口,读取 `schema.apiSchema.httpMethod` 和 `schema.apiSchema.path`,再和 Rust 实现比较。它适合人工抽样或 API 变动排查,不适合作为默认本地快检。
### 1.3 Field live 校验
字段级校验必须显式打开 live 模式:
```bash
just api-contract-fields openlark-ai 5
```
等价命令:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-ai \
--fields \
--live-fields \
--max-field-apis 5 \
--report-dir reports/api_contract_fields
```
该模式会读取当前官网详情页中的结构化 schema,并和 Rust 结构体的可序列化字段比较。当前覆盖:
- request body 顶层字段:`schema.apiSchema.requestBody.content.*.schema.properties`
- response data 顶层字段:`schema.apiSchema.responses.200.content.application/json.schema.properties.data.properties`
request 字段解析支持:
- `#[serde(rename = "...")]`
- `#[serde(rename_all = "camelCase")]`
- `Option<T>` optionality
- required request field missing
如果需要让字段漂移直接返回非零退出码,使用 strict 入口:
```bash
just api-contract-fields-strict openlark-ai 5
```
或:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-ai \
--fields \
--live-fields \
--max-field-apis 5 \
--strict fields
```
### 1.4 Token 类型 live 校验
核对 Rust 实现里 `.with_supported_access_token_types(...)` 声明的 token 类型,是否被
飞书官方文档「请求头 → Authorization」接受。这是 [#511](https://github.com/foxzool/openlark/issues/511)
acs / security_and_compliance 批量误配(误设 `App`/`app_access_token`)的防回归手段。
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-security \
--tokens \
--strict tokens \
--report-dir /tmp/openlark-api-contracts-tokens
```
oracle 取值(优先级递减):
1. 官网详情 payload 的 `schema.apiSchema.security.supportedAccessToken`(结构化,多数页面)。
2. 部分 `server-docs` 风格页(如 acs `user/get`、`user/list`)的 detail payload 不含该字段,
回退到抓取文档 `.md` 源的「请求头 Authorization」行(SPA 页正文来源,见
`docs/api-spec-accuracy-audit.md` 的核对方法节)。注意:该回退对每个 detail payload 缺标注
的接口会多一次 `.md` HTTP 抓取,全量核对时网络调用量会相应放大。
判定规则(被测对象 = Rust 声明的有效 token 集合,未显式声明时取默认 `[User, Tenant]`):
- Rust 集合与官方集合**不相交** → `ERROR`(运行时注入的 token 必被飞书拒绝)。
- 官方未标注 → `UNVERIFIED`(无法核对,不阻塞)。
- 实现文件缺失 → `WARN`。
- 存在交集(SDK 至少能选出一种官方接受的 token)→ 无 finding。
- 声明 `None`(自行管理鉴权、bypass token cache)但源码手动注入
`Authorization: Bearer <self.token_field>` 的端点(如 OIDC `authen/v1/user_info/get`),
按实际注入的 token 类型核对,而非 `none_access_token`——避免把「手动注入 user token」
误判为 disjoint `ERROR`。真正无鉴权(声明 `None` 且无手动注入)对要求 token 的文档仍报 `ERROR`。
与 live 校验一致,该维度访问网络。CI 对 `openlark-security` 与 `openlark-auth` 启用
`--strict tokens`,作为 [#511](https://github.com/foxzool/openlark/issues/511) acs /
security_and_compliance 误配的回归 gate(与 endpoint strict gate 同级;auth 覆盖 OIDC token
端点)。全仓 live 核对因每个接口都要抓取详情 payload、调用量过大,未纳入 CI;其他 crate 用
`just api-contract-tokens <crate>` 人工抽样。
### 1.5 Field strict 域扩展(attendance → docs)
字段 strict 按域推进,**禁止**一次 PR 把 monorepo 全量 `--strict fields` 打开。先验
基线(0 `ERROR`),再 flip CI。
| attendance | `openlark-hr --biz-tag attendance`(~39 API) | CI `--strict fields` | #526 / #533 / #534 / #540 |
| docs | `openlark-docs`(ccm/base/baike/minutes,~214 API) | CI `--strict fields` | #569(0.20 首批) |
| *(next field-strict domain slot)* | 待选(**一域一 PR**) | 未 flip | 见 §1.6 admission |
本地复现 docs 门禁:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-docs \
--fields \
--live-fields \
--strict fields \
--report-dir /tmp/openlark-api-contracts-docs-fields
```
基线证据(#569 落地时 live 全量):`0 error` / 仅 `WARN`(主要为
`W_*_FIELDS_UNRESOLVED` 与 1 条 `W_REQUIRED_REQUEST_FIELD_OPTIONAL`,不阻塞 strict)。
字段扫描器对 docs multipart(`UploadMeta` / `json!` 字面量)的识别见 #223 / #224。
### 1.6 Trust gate inventory + next field-strict domain admission(#586)
本节把 0.20 已落地的 contract / coverage trust gate **制度化**:CI green 必须继续
表示「可调用准确性」,而不是「阈值被下调」或「域 gate 被静默删掉」。
#### 1.6.1 Gate inventory(当前 pinned 清单)
下列门禁由 `.github/workflows/ci.yml` 的 `api-contracts` job 执行,并由
`tools/tests/test_validate_api_contracts_ci_gates.py`(gate inventory test)钉死。
删除 / 放宽任一 pin 会让该 unittest 失败。
| Endpoint | monorepo 全仓离线 | `--all-crates --strict endpoint` | `test_endpoint_strict_covers_all_crates` |
| Token | `openlark-security` + `openlark-auth` | `--strict tokens`(各一 step) | `test_token_strict_covers_security_and_auth` |
| Field | attendance(`openlark-hr --biz-tag attendance`) | `--live-fields --strict fields` | `test_attendance_field_strict_gate` |
| Field | docs(`openlark-docs`) | `--live-fields --strict fields` | `test_docs_field_strict_gate` |
| Field inventory | **恰好** attendance + docs 两域 | 无 monorepo-wide field strict | `test_field_strict_inventory_is_exactly_attendance_and_docs` + `test_no_monorepo_wide_field_strict` |
与 coverage 侧硬门禁的关系(**不在本 job 内执行,但同属 0.20 trust 程序**):
| Typed-coverage hard gates | `tools/typed_coverage_release.toml` + `docs/typed-coverage-release-criteria.md` | 阈值不得下调(见 #586 非目标) |
| path_noise vs true_gap 分类 | `tools/validate_apis.py` 报告 + denoise 回归测试 | 分类保留;不得把噪音当「实现完成」删掉真相 |
| Core-business P0 missing = 0 | release gate / `core_business` dashboard | 不得靠降低门槛伪装 PASS |
| Platform P1 clear-or-disprove | `tools/tests/test_p1_platform_clear_or_disprove.py` | 锁仍绿 |
| Selective P2 path_noise | `tools/tests/test_p2_selective_slice.py` | 锁仍绿 |
本地复核 inventory(离线、秒级;不跑 live fields/tokens):
```bash
python3 -m unittest tools.tests.test_validate_api_contracts_ci_gates -v
python3 -m unittest tools.tests.test_typed_coverage_release_policy -v
```
#### 1.6.2 Next field-strict domain admission(下一域准入)
**Slot**:上表「next field-strict domain slot」——每次只接纳 **一个** 新域进入
CI `--strict fields`。本 ticket(#586)只制度化 slot + 规则 + 绿 inventory,
**不**在本变更中 flip 第三个域。
Admission 准入条件(全部满足才允许 flip):
1. **一域一 PR**:候选域必须单独成 PR(或明确 scoped 子单元),不得与无关重构混装。
2. **Live baseline 必须 0 ERROR**:对候选域先跑 live field 校验,报告中
`ERROR` 计数为 0 后,才允许把该域 step 以 `--strict fields` 写入 CI。
`WARN` / `UNVERIFIED` 可带入(与 attendance/docs 先例一致),但不得靠
关掉 strict 或缩小扫描范围「假绿」。
3. **Dual-edit(双改)规则**:同一变更必须同时更新:
- `.github/workflows/ci.yml` 的 `api-contracts` job(新增 domain-scoped step);
- `tools/tests/test_validate_api_contracts_ci_gates.py` 的
`FIELD_STRICT_DOMAINS` inventory 与对应断言;
- 本节域表(§1.5)把 slot 行改成新域的「已 flip」状态。
只改 workflow 或只改测试 = 审查拒绝。
4. **域范围显式**:step 必须带 `--crate …`(及必要时 `--biz-tag …`),
report-dir 使用 `api_contract_fields/<domain>` 风格片段,便于 inventory 钉死。
推荐本地基线命令(以假设候选 `openlark-communication` 为例,**非**已 flip 域):
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-communication \
--fields \
--live-fields \
--report-dir /tmp/openlark-api-contracts-candidate-fields
# 仅当 0 ERROR 后,再在 PR 中加 --strict fields 并 dual-edit inventory
```
#### 1.6.3 非目标(Explicit non-goals)
- **禁止 monorepo-wide field strict**:不得用 `--all-crates --strict fields`
(或等价「一次打开全仓 fields」)替代域批推进。
- **禁止 hard-gate 阈值下调**:不得为了让 typed-coverage / contract CI 变绿而降低
`tools/typed_coverage_release.toml` 中的 hard gate 阈值;阈值变更必须是独立
policy PR,且只能升高或维持,不得降低。
- **本制度化单元不 march 新域**:#586 交付 slot + rules + green inventory,
不实现第三个域的 full field-strict flip(除非作为文档示例命令,且不写入 CI)。
## 2. 单 crate 使用
只验证一个 crate 的 endpoint:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-docs \
--strict endpoint
```
输出默认写到 `reports/api_contracts/`。如需避免污染本地报告目录:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-docs \
--strict endpoint \
--report-dir /tmp/openlark-api-contracts-docs
```
## 3. 结果解读
报告中的 severity:
| `ERROR` | 已确认 contract drift;strict 模式会失败 |
| `WARN` | 实现缺失、解析不到或低噪声风险;endpoint strict 当前不因 warning 失败 |
| `UNVERIFIED` | 官方详情或实现形态无法机器确认,需要人工判断 |
常见 finding code:
| `E_ENDPOINT_METHOD_MISMATCH` | Rust `ApiRequest::*` 方法和官方 method 不一致 |
| `E_ENDPOINT_PATH_MISMATCH` | Rust endpoint path 和官方 path 不一致 |
| `W_ENDPOINT_UNRESOLVED` | validator 暂时无法解析实现里的 endpoint 表达式 |
| `W_IMPLEMENTATION_FILE_MISSING` | CSV 期望的 API 文件不存在 |
| `E_REQUIRED_REQUEST_FIELD_MISSING` | 官网 required request field 在 Rust 请求结构体中缺失 |
| `W_OPTIONAL_REQUEST_FIELD_MISSING` | 官网 optional request field 在 Rust 请求结构体中缺失 |
| `W_REQUIRED_REQUEST_FIELD_OPTIONAL` | 官网 required 字段在 Rust 中建模为 `Option<T>` |
| `W_RESPONSE_FIELD_MISSING` | 官网 response `data` 字段在 Rust 响应模型中缺失 |
| `E_ACCESS_TOKEN_TYPE_MISMATCH` | Rust 声明的 token 类型全被官方文档拒绝(不相交,鉴权必失败) |
| `U_ACCESS_TOKEN_UNANNOTATED` | 官方文档未标注 `supportedAccessToken`/Authorization,token 类型无法核对 |
| `U_OFFICIAL_DETAIL_FETCH_FAILED` | live 模式无法获取官网详情 payload |
## 4. 当前已知验证证据
`openlark-ai` 的字段 live 烟测能发现真实漂移:
```bash
just api-contract-fields openlark-ai 1
```
当前报告会指出两个真实漂移:
- 官网 request body 要求 `multipart/form-data` 字段 `file`,而 Rust 实现是 `file_token, is_async`。
- 官网 response `data` 下有 `bank_card`,而 Rust 响应模型中是 `parsing_result` 及其派生字段。
这说明字段级 validator 已经能用官网当前结构化 schema 验出 request 和 response 的实现不一致。
## 5. 与覆盖率校验的关系
- `just api-coverage`:验证 API 文件覆盖率和缺失 API backlog。
- `just api-contracts`:验证已实现 API 的 endpoint contract。
- `just api-contract-fields`:抽样验证 request/response 字段是否和当前官网一致。
推荐日常顺序:
1. `just api-coverage`
2. `just api-contracts`
3. 对可疑 crate 跑 `just api-contract-fields <crate> <N>`
## 6. 当前限制
- 字段级校验目前只覆盖 request body 顶层字段和 response `data` 顶层字段。
- 嵌套字段、query/path 参数还未纳入 strict 比较。
- endpoint 解析对**无法识别**的复杂动态拼接仍会给出 `W_ENDPOINT_UNRESOLVED`,不会在 endpoint strict 模式下失败。
- live 模式依赖飞书官网详情接口,适合抽样和排查,不应替代离线快检。
### 6.1 Docs CatalogEndpoint 解析(#568)
`openlark-docs` 的 typed API 几乎全部通过 `CatalogEndpoint::to_request()` 构造请求(而不是
`ApiRequest::get/post(...)` 字面量)。contract validator 现已支持:
- `api_endpoints.rs` **以及** `api_endpoints/**/*.rs` 子模块中的 enum `to_url` / `method`
- `Enum::Variant(...).to_request()` / `.to_request::<T>()` / `var.to_request()`
- `pub use Target as Alias` 与独立 baike/lingo catalog(路径前缀不可混用)
因此 docs 域的 path/method drift 会以 `E_ENDPOINT_*` **ERROR** 出现在报告中(不再被
`W_ENDPOINT_UNRESOLVED` 掩盖)。`just api-contracts` / CI `--strict endpoint` 对
`openlark-docs` 与其他 crate 使用同一 strict 规则。