# 规则:契约先行 —— 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`](../../.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。被删/写坏 → 红。 3. **OpenAPI diff workflow 位置**:必须在仓库根 `.github/workflows/openapi-diff.yml`,不得回流到子目录。 4. **Maven CI workflow 位置与纯层口径**:必须在仓库根 `.github/workflows/maven.yml`,不得跳测试,不得在默认 CI 接 real-PG/live 凭据。 - 已验证:2026-06-14 JDK21 全 reactor `clean test` 曾通过;2026-06-27 E0 后该门禁扩为 4 类,以当前 CI / 本地复跑结果为准。 --- ## 四、加固: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`)合并设计,避免重复台账。