4.6 KiB
规则:契约先行 —— API/DB 契约为 SSOT(机械门禁)
类型:硬约束(rules) · 状态:生效 2026-06-14 · 门禁:JUnit/surefire(本地+CI 机械阻断) 上位:
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与本规则索引指向,不复制内容。
二、规则
- 契约先行:任何对外 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,P0 已打开测试门 + JDK21)在 PR/push 阻断。两条:- Flyway 迁移卫生:版本号唯一、从 1 连续无缺口、命名
V<版本>__<描述>.sql。重复/缺口/乱名 → 红。 - OpenAPI 契约存在性+结构:必备业务域(account/ai/content/events/knowledge/market/meta)各有可解析的
openapi.yaml,含openapi/info/paths顶层键且为 OAS3。被删/写坏 → 红。
- Flyway 迁移卫生:版本号唯一、从 1 连续无缺口、命名
- 已验证:2026-06-14 JDK21 全 reactor
clean test,ContractFirstGateTestTests run: 2, Failures: 0(证据见交付报告)。
四、加固:openapi-diff 破坏性变更检测(CI 级,仓库根 workflow 已接线 / 检测逻辑已本地实证 / 远端首跑待确认)
存在性/结构门禁挡不住“悄悄改坏既有契约语义”(删字段、改必填、改类型)。破坏性变更检测用 oasdiff 在 PR 上比对 base 分支与 PR 的 OpenAPI,发现破坏性差异即红。
已落地为独立 workflow .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 忠实复刻 workflow 循环,对全部 7 域真实契约跑 5 场景全绿:无变更/新增可选参数/新增端点→放行;参数变必填(request-parameter-became-required)/删端点(api-path-removed-without-deprecation)→拦截。坑与配方见 knowledge §七。
⚠️ 残留:远端 Gitea Actions 触发管道(
pull_requestpaths 触发、base.shaworktree、docker 镜像拉取)须一次真实 PR(改docs/api-contracts/**)首跑确认。首跑前:可宣称"破坏性变更检测逻辑已本地实证(对真实契约拦真破坏、放行安全演化),root workflow 已接线",但不得宣称"CI 已在线上实拦"。
五、扩展方式
- 新增业务域:在
docs/api-contracts/<域>/openapi.yaml建契约,并把<域>加入ContractFirstGateTest.REQUIRED_API_DOMAINS(机械要求该域契约长存)。 - DB 结构演进:新增
V<下一版本>__<描述>.sql,切勿改历史迁移;门禁自动校验连续与唯一。 - 需要“契约↔实现不漂移”的更强门禁(OpenAPI operationId 与控制器/服务交叉核对)时,与 P0 覆盖台账(
P1rApiCoverageReportTest)合并设计,避免重复台账。