oh-my-muse/.agents/knowledge/external-deps-and-gotchas.md

129 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# knowledge:外部依赖、验收环境与工程坑(蒸馏自历史 memorys)
> 蒸馏自 2026-06-14 已清理的过程文档(外部验收留痕、P1 收口、studio/admin 搭建),只保留"删了会真丢"的操作事实与踩坑。**凭据只记来源文件,不记明文**。过时即更。
## 一、外部依赖与验收环境
| 依赖 | 地址 | 关键事实 |
|---|---|---|
| New-API(LLM 网关) | `http://100.64.0.8:3000`,OpenAI 兼容 `/v1/chat/completions` | 已验收模型 `MiniMax-M2.5`;容器 `new-api` 端口 3000;其 DB 在 `infra-postgres`(宿主 `100.64.0.8:5433`) |
| RAGFlow(知识运行时) | `http://100.64.0.8`(及 `:9380`) | `/api/v1/datasets``/api/v1/retrieval`;健康检查 `/v1/system/healthz` |
| 开发 PG / Redis | 见下方凭据来源 | 远端 PG **15**(用户确认可用,不强制 PG16);Redis `100.64.0.8:6379` |
**凭据来源(明文不入库,只记位置)**:
- 外部验收:`muse-cloud/scripts/dev/p1r-external-acceptance.env`(`set -a; . 该文件; set +a` 加载;含 New-API base/token、`MUSE_AI_NEW_API_DEFAULT_MODEL_KEY=MiniMax-M2.5`、RAGFlow base/key、GraphRAG 开关)。
- 开发基础设施:`~/.config/muse-repo/infra.env`(PG/Redis 连接 + 真实凭据)。
- ⚠️ **安全**:上述令牌仅限当前内网验收上下文;**仓库若同步到更大范围,必须先轮换 New-API 令牌**。
**Live 验收 harness(opt-in,默认跳过,`MUSE_P1R_EXTERNAL_ACCEPTANCE=true` 才真跑)**:
- `muse-module-ai/.../application/muse/facade/P1rNewApiLiveAcceptanceIT.java`(New-API)
- `muse-module-knowledge/.../application/muse/facade/P1rRagFlowLiveAcceptanceIT.java`(RAGFlow)
- 另:`muse-server/.../framework/api/P1rAiRuntimeEndToEndLiveAcceptanceIT.java``P1rKnowledgeRuntimeEndToEndLiveAcceptanceIT.java`
- 输出脱敏(只打 endpoint/模型/id/状态/耗时/usage/key 长度 + sha256 前 12);RAGFlow smoke 会留存 `p1r-live-<时间戳>` dataset 不删。
- **runtime client live ≠ coverage completed**:仍需 Muse API 触发 + 落库 + 审计 + 失败路径证据(见 [golden-journey](../skills/golden-journey-vertical-slice.md))。
## 二、外部集成兼容坑(实证)
- **New-API 系统管理令牌 ≠ RAGFlow key**:用前者打 RAGFlow `/api/v1/datasets` 返回 `code=109 Authentication error`,必须用独立 `ragflow-*` key。
- **RAGFlow 建数据集**:`POST /api/v1/datasets``config:{}` 返回 `code=101 Extra inputs are not permitted`;**只发 `name`** 才 `code=0`。adapter 已改为 createDataset 只发 name,config 走独立 `updateDatasetConfig`
- **RAGFlow 文档状态轮询**:用真实支持的 `?id=<docId>`(单个);多 id 在 adapter 内 fail-closed 为 `VALIDATION_ERROR`
- **GraphRAG attribution**:默认 fail-closed(`ATTRIBUTION_NOT_CONFIGURED`);真跑需 `MUSE_KNOWLEDGE_RAGFLOW_GRAPHRAG_ATTRIBUTION_READY=true` + `..._GRAPHRAG_DATASET_ID`
## 三、前端 / 构建坑速查
- **studio(Vite + React + TS6.0)**:`erasableSyntaxOnly` 禁用 constructor 参数属性;`exactOptionalPropertyTypes` 下可选属性需规避写法;采纳/Diff 用 **Coarse-to-Fine 分级 Diff** 规避 3000+ 字正文上 O(N×M) LCS 性能爆炸;MSW 仅 `import.meta.env.DEV` 启用。
- **admin(Vben)**:`museAdminApi` 只拼 `/muse/**`(baseURL 已是 `/admin-api`,否则双前缀 `/admin-api/admin-api`);路由测试**不能**放 `src/router/routes/modules/**`(会被 Vite 动态路由 glob 当业务路由);Opsera pre-commit 缺失时跳过用 `touch /tmp/.opsera-pre-commit-scan-passed`
- **git 边界(未决)**:`muse-admin/``.gitignore` 作"独立子仓库边界",源码靠 `git add -f` 纳入——长期纳管方式待定(见进度总账 TODO)。
- **Maven 增量编译陈旧坑(验证门禁/ArchUnit 改动时必踩,2026-06-17 实证)**:只改一个测试方法体/helper 的逻辑(如把 ArchUnit 谓词从 `.application` 临时改 `.api` 做反向红验证)后重跑,`.class` 时间戳会更新但**方法体未真正重编译**→ 反向测试**假绿**(看似规则没触发,实为跑的旧字节码)。根因=maven-compiler-plugin 的 `maven-status/.../inputFiles.lst` 增量跟踪没识别到变更;手动 `rm target/test-classes/...` 也不够(status 没更新仍跳过)。**可靠复跑**:`rm -rf <module>/target/maven-status` 或加 `-Dmaven.compiler.useIncrementalCompilation=false`;或干脆 `mvn ... clean`。**做"反向应红"自检时尤其要确认真重编译了**,否则反假绿本身就是假绿。
## 四、跑 P1R real-PG 集成测试(IT)的配方 + 本机代理坑(2026-06-14 实证)
**结论先行**:P1 后端 23 个 `P1r*IT`(completed-approval / Flyway 迁移 / events-publish outbox / live-acceptance)**可在共享远端 PG 上真跑验证**。**2026-06-15 mini-infra PG 全量重跑 = 23/23 IT 类全绿(99 用例 0F/0E;2 例 external-acceptance 因未设 `MUSE_P1R_EXTERNAL_ACCEPTANCE` 而 assumeTrue 跳过)**。此前唯一红 `P1rKnowledgeFlywayMigrationIT`(硬编码版本断言随 schema 增长失效,V14→V21→V23 已复发两次)**已修为动态**:读本次 Flyway `MigrateResult.targetSchemaVersion/migrationsExecuted` 自适应(commit 4d46d7a,Codex+Opus 双代理并行提案合并;反假绿仍校验迁移成功 + history 计数一致 + Knowledge 表/索引/列存在)。
**▲ 最大坑(排查极久):本机 env `HTTP_PROXY=HTTPS_PROXY=http://127.0.0.1:7897` → JVM 取 `socksProxyHost=127.0.0.1:7897`,把所有 Java socket 走 SOCKS;转发 HTTP 正常,但破坏 PostgreSQL 原始线协议(发完 StartupMessage 即被 EOF,认证前关闭,极像 pg_hba/密码错)。** `nc`/`curl` 直连正常,只有 JDBC/JVM 中招;且 `env -u HTTP_PROXY` 清不掉(socksProxyHost 由 JVM 层注入)。
- 判定 PG 真可达:`printf '\x00\x00\x00\x08\x04\xd2\x16\x2f' | nc -w5 100.64.0.8 5433 | xxd``N` 即 PG 在说话(纯 nc 直连,不经代理)。
- **修复:给 forked 测试 JVM 加 `-DargLine='-DsocksProxyHost= -DsocksProxyPort='`**(清空 SOCKS,JVM 直连 tailnet);代码内可用 `new Socket(java.net.Proxy.NO_PROXY)`
**跑单个 IT 的配方(已验证)**:
```bash
export MUSE_POSTGRES_PASSWORD=<root 密码;来自 infra.env / 用户提供,勿入库>
mise exec --cd muse-cloud -- mvn -pl muse-server -am test \
-Dtest=<P1r...IT> -Dsurefire.failIfNoSpecifiedTests=false \
-Dp1r.flyway.url='jdbc:postgresql://100.64.0.8:5433/<专属_test库>' \
-Dp1r.flyway.user=root -Dp1r.flyway.locations='filesystem:sql/muse' \
-DargLine='-DsocksProxyHost= -DsocksProxyPort='
```
要点:① 用 **`test` 阶段 + `-Dtest=<IT类>`**(surefire 显式点名即可跑 `*IT`),**别用 `verify`/`integration-test`**——那会触发 `package`/spring-boot repackage 把 member-server 等打成 fat jar,破坏跨模块编译(`package ... does not exist`),且 surefire 会先跑到平台陈旧红(`QiniuSmsClientTest` 时区)而中止整个 reactor。② `p1r.flyway.url` 库名**必须 `_test` 结尾**(IT 有 assertTestDatabaseUrl 守卫 + 会 `flyway.clean()` DROP 全库,**绝不能指 muse_local / muse**);各 IT 用各自专属 `_test` 库(`muse_p1r_*_test` 系列已存在;缺的用 root `CREATE DATABASE`,跑完可 DROP)。③ `p1r.flyway.locations` 必须**恰为** `filesystem:sql/muse`(IT 断言该精确字符串,再从 `user.dir` 上溯定位 `sql/muse`)。④ 密码**只走 env**(`MUSE_POSTGRES_PASSWORD`/`P1R_FLYWAY_PASSWORD`;IT 有 assertNoPasswordSystemProperties,用 -D 传会红)。⑤ live-acceptance IT 需 `set -a; . scripts/dev/p1r-external-acceptance.env; set +a` 载入 token,且部分用例额外需 `MUSE_P1R_EXTERNAL_ACCEPTANCE=true` 才真跑(否则 assumeTrue 跳过、显示 Skipped)。⑥ **整套 `-Dtest='P1r*IT'` 批量跑必须加 `-DreuseForks=false`**:`P1rMarketFlywayMigrationIT``System.setProperty("p1r.flyway.url", maskedUrl)` 脱敏覆盖,surefire 默认 fork 复用下污染同 fork 后续 IT 的系统属性 → 后续 IT 假报"缺少 p1r.flyway.url";每类独立 fork 即净(2026-06-15 实证:默认复用→多 IT 假红;`-DreuseForks=false`→23/23 全绿)。⑦ argLine 内可一并传 `-Dp1r.flyway.url/user/locations`(本轮即如此),与 maven `-D` 等效到 forked JVM。
> 远端 PG 实为 **PostgreSQL 17.10**(§一表内"15"为旧记,以此为准),超管用户 `root`;本机无本地 PG(127.0.0.1:5432 关闭)。批量顺序跑见交付报告(禁止并发 maven,会损坏 target)。
## 五、MVP #1「AI 候选采纳」纵切:后端门禁 + 活体全栈三大阻塞(2026-06-14 实证)
**后端机械门禁 IT(已绿,确定性)**:`muse-server/.../api/P1rContentMergeSuggestionIT`——真实 PG + 真实 `AiSuggestionMergeProjectionFacade`(`@Bean` 直接实例化注入真实 `MuseAiSuggestionMapper`,规避 `@ConditionalOnBean` 时序;配套 `@MapperScan("cn.iocoder.muse.module.ai.dal.mysql.muse")`)读真种 `muse_ai_suggestion`,经 MockMvc 打 `POST /app-api/muse/works/{workId}/blocks/{blockId}/suggestion-merges`。5 路径:正向写 Canonical(revision1→2)/revision 冲突/幂等回放/非 pending/「审」缺失。跑法同 §四配方,`-Dtest=P1rContentMergeSuggestionIT`,**需预建 `muse_p1r_merge_slice_test` 库**(IT 自身 flyway.clean+迁移)。可合并候选的种子契约见 IT 内 `insertMergeableSuggestion`:status=pending、source_status=active、数值 authorization_snapshot_id、content_snapshot 带 content、diff_summary 带齐 outputComplianceResultId/staticCheckResultId/licenseRestrictionSnapshot(facade 缺一即判 unavailable)。
**起活体全栈 muse-server 的三大阻塞(到「用户在 app 里真能用」)**:
1. **远端 dev 库缺 yudao 基座 schema**:`muse_local` 有 Muse 表(muse_content_*/muse_ai_suggestion)但**无 `system_tenant` 等基座表、无 flyway_schema_history**;`muse_slice_live`(新建)全空。muse-server 装配 system/infra 等模块,Flyway 只迁 Muse(V1-V21,`sql/muse`),基座 schema **未 Flyway 化**(对抗复盘已记),故全栈 app 无法服务租户校验请求。起活体须先 provision 基座(yudao SQL dump)。**【2026-06-14 已解,见 §六】**:基座已固化为 `sql/dev/yudao-base-*-postgres.sql` 灌入 `muse_slice_live`,全栈 muse-server 已成功启动并 curl 活体跑通 AI候选→Canonical 采纳;后端 merge 链路另有真实 PG IT 证明。
2. **studio AI 流 parser 漂移**(已修复 2026-06-14):`connectAIStream`(src/lib/sse.ts)原按 JSON `data.type` 单行分发,真后端(`MuseAiTaskStreamServiceImpl` 用 Spring `SseEmitter.name(event).data(json)`)把事件名放 SSE `event:` 行(`event:chunk/quality_check/done/error` + data 行 JSON 无 type)→ 旧实现真实生成流全收不到。修复=复用同文件 `createEventStreamParser`(按 `event:` 行分发 + id/comment/多行 data 拼接),`SSEEventHandler` 回调契约不变。**连带 gotcha**:done.data 的 `taskId`/`suggestionId` 后端是 Long→JSON **number**,而前端全链路(onDone 类型、采纳请求体 `MergeBlockSuggestionRequest`、OpenAPI)按 **string**——已在 sse.ts done 分支于解析边界 `String()` 归一(缺失值保持 undefined 以维持「无候选→禁用采纳」判定);mock/契约测试改用数字 suggestionId 复现后端形态。验证用 `./node_modules/.bin/` 的 tsc -b --force / vitest run(47) / vite build 全绿(corepack pnpm exec 会触发 install 校验失败,勿用)。
3. **AI 运行时未写「审」字段(已修复 2026-06-14,chip task_092cfc32)**:原 `createRuntimeSuggestion` 不写 outputComplianceResultId/staticCheckResultId/licenseRestrictionSnapshot 且 source_status='verified'(merge 要 'active')→ 真实生成的候选不可合并。现新增 `MuseAiCandidateReviewService` 做真实轻量审(输出合规扫描/静态检查/许可快照,通过才给可追溯审结果 ID),createRuntimeSuggestion 写齐三审字段(通过时)+ 顶层 content(脱敏 summary,供 facade 可用性)+ source_status='active' + 数值授权快照(envelope id 为数值时落 BIGINT 列,沿用 `MuseSuggestionServiceImpl` 的 envelope=authz 约定)。**遗留**:非数值 envelope id 暂无法落 BIGINT 授权快照列(已知列能力限制),待授权快照建模收口;完整 provider 正文按数据主权不持久化,采纳走 merge_after_edit(用户回传所审最终正文)。门禁 `P1rContentMergeGeneratedSuggestionIT`(真实 PG)证明:经真实 `createShadowSuggestionCandidate` 产出的候选可被 mergeBlockSuggestion 采纳写入 Canonical。
**本机替代与凭据(联调用)**:远端 Redis 需密码(未提供)→ 本机 `redis-server`(brew 8.x)+ `--requirepass` 替代(Redis 仅 session/cache 管道,非被测对象),infra.env 指 `127.0.0.1`。mock 登录:`application-local.yaml` `muse.security.mock-enable=true`,令牌 = mockSecret(`test`)+userId,即 `test1`=userId 1;tenant 来自请求头 `getTenantId(request)``~/.config/muse-repo/infra.env`(仓库外,**永不入库**)本会话已填 root PG 密码 + 本机 redis;注意 app 默认连用户 `muse_dev`(密码未知),联调改 `MUSE_POSTGRES_USERNAME=root`。**安全**:该文件含明文 root 口令,轮换/清理由用户掌握。
## 六、起全栈 muse-server(单体):✅ 已成功启动 + 活体合并实证(2026-06-14 达成)
**结论(里程碑)**:muse-server 本项目**史上首次以单进程单体成功启动**(`Started MuseServerApplication in 17.985 seconds`,Tomcat:48080),并在真实 HTTP + mock 鉴权 + 租户过滤 + 真实 PG(muse_slice_live)上**端到端跑通 MVP #1 纵切**:AI 候选(Shadow,muse_ai_suggestion)→ `POST /app-api/muse/works/{w}/blocks/{b}/suggestion-merges`(accept_as_is)→ Canonical 块正文真实写入(content_text=AI候选正文、revision 1→2、来源归因 ai_suggestion + lineage 落库),负路径(陈旧 expectedBlockRevision)被乐观锁拒(`code 1041000002 Block版本冲突`)。**全程无 permissive stub,真实失败关闭**。
下文为打通全栈装配的 6 类硬阻塞实测复盘(可复用):
**已拆(可复用)**:
1. **build**:member-server repackage 无 classifier → fat jar → 下游 market 编译期 `package does not exist`,致 `mvn package` 全项目破损(仅 `test` 阶段不触发,故 CI test 假绿)。修:member-server repackage 配 `<classifier>exec</classifier>`(commit 1f89007)。
2. **postgres 基座**:本仓原无 yudao postgres 基座 dump(只有 H2 测试 schema)。已翻译 system/infra/member 测试 schema→PostgreSQL(49 表)+ 最小种子,固化 `muse-cloud/sql/dev/yudao-base-schema-postgres.sql` + `yudao-base-seed-postgres.sql`(实测 0 失败)。灌库后 app Flyway(`--spring.flyway.baseline-on-migrate=true`)成功补 Muse V1-V21、Tomcat 起、Spring 初始化。`muse_slice_live` 现已含 基座+Muse V1-V21+种子(可复用)。
**第 3 墙诊断订正(实测推翻"Feign RPC"初判)**:boot 初卡 `PermissionCommonApi` 无 bean,初判为 yudao-cloud Feign 跨模块 RPC 未桥接——**实测证伪**。真因是 **system/infra/ai-server 也是无 classifier 的 repackage fat jar**,嵌入 muse-server fat jar 后类不可加载(同 member 墙)。配 classifier 后(commit eea10b7),`PermissionApi extends PermissionCommonApi` 的本地 `PermissionApiImpl`(@Primary @RestController)即注册,bean 问题消失——**无需任何 Feign 桥接/stub**。随后逐层实测拆除:① datasource 报 EOFException = 本机 SOCKS 代理坑,须给 `java``-DsocksProxyHost= -DsocksProxyPort=`(清 JVM system property;`env -u HTTP_PROXY` 清不掉)→ PG 连通;② Flyway V1-V21 在 muse_slice_live 迁移成功;③ security/permission/tenant 装配通过;④ codegen 缺 `muse.codegen.import-enable`(`--muse.codegen.import-enable=false` 补)。
**已拆 ③ facade 条件装配(Spring 条件注解可靠性)**:`@Service/@Component` 上的 `@ConditionalOnBean/@ConditionalOnMissingBean` **不可靠**(Spring 官方仅保证 `@Bean` 方法上有效),在"从未装配过的单体"里扫描顺序致非确定性"无 bean"失败(首发 `ContentFileFacade`,随后 `ContentAiSuggestionFacade` 等切片关键 facade 接连暴露)。**修法**(commit d09d529,均真实失败关闭、非 permissive stub):①6 个全默认内容 facade(ContentFile/KnowledgeDraft/Meta/ParseJob/PlanningCandidate/StyleCheck)→ 改由 `MonolithFacadeFallbackAutoConfiguration`(`@AutoConfiguration`+`@Bean @ConditionalOnMissingBean` 返回接口默认实现,真 impl 仍优先);②真 impl facade(AiSuggestionMergeProjectionFacade 等 4 个 @Primary)去 `@ConditionalOnBean`,恒注册;③Unavailable 兜底(4 个 source-owner + ToolGrantApproval + MetaImpact,均扩 abstract 方法不能自动配)就地去 `@ConditionalOnMissingBean`
**已拆 ④ 跨模块 bean 名冲突(@Resource 按名注入)**:`@Resource` 优先按字段名注入;member 的 `AccountExportServiceImpl.exportTaskMapper` 与 content 的 `ExportTaskMapper` bean 撞名 → 单体内注入到错类型(JDK 代理 type mismatch,boot 失败)。修:字段重命名 `accountExportTaskMapper`(commit d09d529,全仓扫描无其它撞名)。
**第 3 墙诊断总订正**:初判"cloud→monolith 需 Feign RPC 桥接"被**完全证伪**——真因全是 ①②③④ 这类装配/打包/命名问题。`PermissionApiImpl`(@Primary)IS-A `PermissionCommonApi`,类可加载后 bean 自然满足,**无需任何 Feign 桥接/stub**。
**起全栈配方(已实测打通,可直接复用)**:
```bash
# 1) 建库灌基座+种子(root):base-schema + base-seed → muse_slice_live;app Flyway 补 Muse V1-V21
# 2) 本机 redis:redis-server --port 6379 --requirepass museredis
# 3) infra.env:MUSE_POSTGRES_USERNAME=root + root 口令、MUSE_POSTGRES_DATABASE=muse_slice_live、MUSE_REDIS_HOST=127.0.0.1
# 4) 起(务必清代理 JVM SOCKS,否则破坏 PG;codegen.import-enable 必给;关 AI 调度避免联调噪声):
cd muse-cloud; set -a; . ~/.config/muse-repo/infra.env; set +a
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy \
mise exec -- java -DsocksProxyHost= -DsocksProxyPort= -Dhttp.proxyHost= -Dhttps.proxyHost= \
-Dmuse.ai.runtime.dispatcher.initial-delay-ms=86400000 -Dmuse.ai.runtime.dispatcher.fixed-delay-ms=86400000 \
-jar muse-server/target/muse-server.jar --spring.profiles.active=local,infra \
--spring.flyway.baseline-on-migrate=true --muse.codegen.import-enable=false &
# 注:muse-server.jar 须为 thin lib(已配 classifier=exec 于 member/system/infra/ai-server)+ muse-server 自身 repackage 产出可执行 jar。
```
**活体证明命令(curl 真打 48080;务必 `--noproxy '*'` 绕本机 HTTP 代理)**:
```bash
# 健康:GET 列表(mock 鉴权 test1=userId1,租户 1)
curl -s --noproxy '*' -H "Authorization: Bearer test1" -H "tenant-id: 1" -H "X-API-Version: 1" \
"http://localhost:48080/app-api/muse/works?pageNo=1&pageSize=5" # → {"code":0,...}
# 采纳:AI候选→Canonical(种子 work=1/block=1/suggestion=1;commandId 用 uuidgen)
curl -s --noproxy '*' -X POST -H "Authorization: Bearer test1" -H "tenant-id: 1" -H "X-API-Version: 1" \
-H "Content-Type: application/json" \
-d '{"commandId":"<uuid>","suggestionId":1,"expectedBlockRevision":1,"decisionType":"accept","mergeMode":"accept_as_is","sourceSnapshot":{"sourceType":"ai_suggestion"},"auditReason":"smoke"}' \
"http://localhost:48080/app-api/muse/works/1/blocks/1/suggestion-merges" # → code:0,newRevision:2,sourceAttribution+lineage
# 实测(2026-06-14):reload GET .../blocks 块 content_text=AI候选正文、revision=2;陈旧 revision=99 → code 1041000002 乐观锁拒(反假绿验证)
```
启动配置要点(workflow 推导):mock 登录 token=`test1`;`system_tenant(id=1)` 必需(已在种子);`MuseAiRuntimeJobDispatcher` 每秒查 muse_ai_job 无开关→用超大 delay 熄火;app-api 租户来自 `tenant-id` 头(默认 1)。
## 七、本地验证 openapi-diff 破坏性变更门禁(2026-06-17 实证)
`.github/workflows/openapi-diff.yml``docker run tufin/oasdiff:v1.19.1` 在 PR 上逐域比对 base/head 契约;本地无 Docker/GHA 无法整跑,但 oasdiff 是 Go 工具,可 `go install` 同款版本验证「检测逻辑 + 逐域循环聚合」。2026-06-19 已从 `muse-cloud/.github/workflows/` 移到仓库根,否则自建 Gitea Actions 不会扫描触发。
```bash
# 一键复现(自举 oasdiff@v1.19.1 + 临时 venv 装 pyyaml,零全局污染;~首次 go install 稍慢)
bash muse-cloud/scripts/verify-openapi-diff.sh # → 5 场景全绿即门禁逻辑正确
```
- **坑1:CLI 须 subcommand 式** `oasdiff breaking <base> <revision> --fail-on ERR`(v1.7+ 由 flag 式 `-base/-revision` 改来);workflow 与脚本均用此式。
- **坑2:版本须钉** `tufin/oasdiff:v1.19.1`(机械门禁不可用 `:latest`,否则上游 CLI/判定漂移会悄悄改门禁口径)。脚本 `OASDIFF_VERSION` 须与 workflow 标签一致。
- **坑3:go install 自报 "version main"** 属正常——release ldflags 仅官方构建注入,源码默认 `version=main`;二进制行为仍是所钉 tag 的。
- **坑4:跨文件 `$ref` 靠目录结构**:events 契约 `$ref: '../openapi-base.yaml'`,故 base/head 必须各自保留 `docs/api-contracts/` 完整目录(workflow 用 `git worktree` 检出整树、脚本用 `cp -R` 整目录),单独拷 `openapi.yaml` 会解析失败。
- 实证(oasdiff v1.19.1,全部 7 域真实契约,5/5):无变更/新增可选参数/新增端点→**放行**(rc=0);参数变必填(`request-parameter-became-required`)/删端点(`api-path-removed-without-deprecation`)→**拦截**(rc=1)。
- **残留(脚本覆盖不到)**:远端 Gitea Actions 触发管道(`pull_request` paths 触发、`base.sha` worktree、docker 镜像拉取)须一次真实 PR(改 `docs/api-contracts/**`)首跑确认。未首跑前不得宣称"CI 已实拦破坏性变更",只能称"检测逻辑+循环逻辑已本地实证、root workflow 已接线"。