oh-my-muse/.agents/rules/contract-first.md

55 lines
4.6 KiB
Markdown

# 规则:契约先行 —— API/DB 契约为 SSOT(机械门禁)
> **类型**:硬约束(rules) · **状态**:生效 2026-06-14 · **门禁**:JUnit/surefire(本地+CI 机械阻断)
> **上位**:[`verification-and-anti-false-green.md`](verification-and-anti-false-green.md) §二
> **由来**:契约与 DB schema 此前靠自觉维护、无机械校验,易漂移、易被误删/写坏而不被发现——又一处“假绿”温床。
---
## 一、SSOT 原地认定(不另起 contracts/ 副本)
本项目已有两套契约源,**原地**认定为 SSOT,**禁止复制到新目录**形成双源漂移(否则即犯“孤儿/拼接设计”):
| 契约类别 | SSOT 位置 | 运行时关系 |
|---|---|---|
| API 契约 | `docs/api-contracts/<域>/openapi.yaml` + `docs/api-contracts/openapi-base.yaml` | 前后端/跨 BC 协作契约;`docs/api-contracts/generated/` 为产物,非 SSOT |
| DB schema 契约 | `muse-cloud/sql/muse/V<版本>__<描述>.sql` | Flyway `spring.flyway.locations``filesystem:sql/muse`,**运行时真实执行** |
> 参考项目 `games-development-ai` 把契约集中到根 `contracts/`;本项目因 Flyway 已绑定 `sql/muse`、OpenAPI 已成体系,**原地认定**比物理搬迁更小风险、零重复。需要统一入口时由 [`../../AGENTS.md`](../../AGENTS.md) 与本规则**索引**指向,不复制内容。
---
## 二、规则
- **契约先行**:任何对外 API / DB 结构变更,先改契约(OpenAPI / 新增 Flyway 迁移),再改实现;实现反向对齐契约。
- **DB 迁移只增不改**:已合入的 `V*.sql` 不得编辑(改其内容会破坏 Flyway 校验和与他人已应用的历史);变更一律**新增**更高版本号迁移。
- **跨 BC / 前后端**只认契约,不靠口头约定;契约缺失即视为接口未定义。
---
## 三、机械门禁(不是自觉)
- 测试:`muse-server/src/test/java/cn/iocoder/muse/server/framework/contract/ContractFirstGateTest.java`,随 CI([`maven.yml`](../../muse-cloud/.github/workflows/maven.yml),P0 已打开测试门 + JDK21)在 PR/push 阻断。两条:
1. **Flyway 迁移卫生**:版本号唯一、从 1 连续无缺口、命名 `V<版本>__<描述>.sql`。重复/缺口/乱名 → 红。
2. **OpenAPI 契约存在性+结构**:必备业务域(account/ai/content/events/knowledge/market/meta)各有可解析的 `openapi.yaml`,含 `openapi/info/paths` 顶层键且为 OAS3。被删/写坏 → 红。
- 已验证:2026-06-14 JDK21 全 reactor `clean test`,`ContractFirstGateTest` Tests run: 2, Failures: 0(证据见交付报告)。
---
## 四、加固:openapi-diff 破坏性变更检测(CI 级,仓库根 workflow 已接线 / 检测逻辑已本地实证 / 远端首跑待确认)
存在性/结构门禁挡不住“悄悄改坏既有契约语义”(删字段、改必填、改类型)。**破坏性变更检测**用 `oasdiff` 在 PR 上比对 base 分支与 PR 的 OpenAPI,发现破坏性差异即红。
**已落地为独立 workflow** [`.github/workflows/openapi-diff.yml`](../../.github/workflows/openapi-diff.yml)(PR 改 `docs/api-contracts/**` 时触发,逐域 `oasdiff breaking ... --fail-on ERR`,镜像钉 `tufin/oasdiff:v1.19.1`)。workflow 即 SSOT,此处**不复制 YAML**(避免双源漂移)。2026-06-19 已从 `muse-cloud/.github/workflows/` 移到仓库根,因为自建 Gitea Actions 只扫描仓库根 `.github/workflows/`
**检测逻辑 + 逐域循环聚合:已本地实证**(2026-06-17,oasdiff v1.19.1)。复现脚本 [`muse-cloud/scripts/verify-openapi-diff.sh`](../../muse-cloud/scripts/verify-openapi-diff.sh) 忠实复刻 workflow 循环,对全部 7 域真实契约跑 5 场景全绿:无变更/新增可选参数/新增端点→放行;参数变必填(`request-parameter-became-required`)/删端点(`api-path-removed-without-deprecation`)→拦截。坑与配方见 [knowledge §七](../knowledge/external-deps-and-gotchas.md)。
> ⚠️ **残留**:远端 Gitea Actions 触发管道(`pull_request` paths 触发、`base.sha` worktree、docker 镜像拉取)须一次真实 PR(改 `docs/api-contracts/**`)首跑确认。**首跑前**:可宣称"破坏性变更检测逻辑已本地实证(对真实契约拦真破坏、放行安全演化),root workflow 已接线",但**不得**宣称"CI 已在线上实拦"。
---
## 五、扩展方式
- 新增业务域:在 `docs/api-contracts/<域>/openapi.yaml` 建契约,并把 `<域>` 加入 `ContractFirstGateTest.REQUIRED_API_DOMAINS`(机械要求该域契约长存)。
- DB 结构演进:新增 `V<下一版本>__<描述>.sql`,切勿改历史迁移;门禁自动校验连续与唯一。
- 需要“契约↔实现不漂移”的更强门禁(OpenAPI operationId 与控制器/服务交叉核对)时,与 P0 覆盖台账(`P1rApiCoverageReportTest`)合并设计,避免重复台账。