docs: M4「AI 创作」开工分析八份报告 + 汇总拍板页入档挂导航

八角色并行开工分析,合计 7448 行;另出 00 汇总页(跨角色收敛结论、
13 项待拍板、6 项待仲裁分歧、未取证项汇总),挂第四迭代导航最前。
mkdocs build --strict 通过。

基线实测修正(文档与实况不符):
- api 测试 381(releases.md 记 379,成因待仲裁)
- 埋点白名单 41(四份文档记 42,experiment_exposed 重复计数)
- 真机验证挂起 10 项(转述链 4→6→8→10 每跳丢项)
- E2E 断言机械可数 226(声称 234 无可复核来源)
- v0.4.0 实际发布 09-14 11:17;CI 非红,三仓五上下文全绿

多方独立收敛(无需拍板):
- 队列用 Postgres SKIP LOCKED + 租约列,不引入 Redis/MQ
- 服务端零对象写能力(ObjectStorage 无 put/get),M4 立足点缺地基
- 「四模块字节级快照锁 CI」不存在,实际门禁仅结构断言
- 定稿模型 input_asset_id NOT NULL,即图生图不支持文生图
- 跨 schema 外键补回是 V5 自身指令,裁剪理由已不成立

阻塞项与安全缺口:
- AI provider BLOCKED:零 SDK/endpoint/额度,正典种子即 fixture
- 分支保护必需上下文选错触发器:(push) 限定 branches:[dev],
  致「推 dev 即满足门禁」且「非 dev 分支 PR 永久无法合并」
- check-secrets.sh 对 sk-/sk-ant- 零覆盖,须先于任何 AI key 落地
- 北极星 09-21 窗口已于 09-13 关闭,补救无从下手,建议改事件驱动

本批核心教训:13 处文档/注释与代码相反且多已被下游采信,其中
5 处造成实际规模误判(widthPx M→S、数据模型早已定稿 L→M、
社区侧 purpose 校验实际不存在等)。汇总页 §0 立转述纪律。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-14 15:48:56 +08:00
parent 5cc6361534
commit 79b33dba31
10 changed files with 7664 additions and 0 deletions
@@ -0,0 +1,806 @@
# M4「AI 创作」Git 工作流与发布流程规划
> **角色**Git Workflow Master **日期**2026-09-14 **性质**:只读调研 + 规划,本报告未执行任何 commit / push / tag / 分支创建 / 远端配置变更。
> **前置**v0.4.0 已发布(三仓 tag 齐)。本报告是 [iteration-3/08](../iteration-3/08-git-workflow-plan.md) 的 M4 续篇,并接管其 §3.3「发布 checklist 草案」的固化职责。
---
## 结论摘要
1. **用户交付的 6 条分支保护描述,逐条核实全部为真**Gitea API 原样贴在 §1.2):api/flutter 的 `main``protected=true``enable_status_check=true`、上下文显式填写、`required_approvals=0`;两仓 `dev` 均未保护;doc 仓 `main` 未保护。
2. **但填的是错的触发器——这是本报告最重要的发现(§1.4)**。必需上下文是 `CI / backend-test (push)`,而 `push` 触发器被限定在 `branches: [dev]`。推论有两个后果:门禁实际由「推 dev」满足而非 PR 自身;且**任何非 dev 分支 → main 的 PR 将永久无法合并**,与「hotfix 走短命分支 + PR」的既定纪律直接冲突。修复方向与真实上下文字符串见 §1.4 / 拍板 D1。
3. **发布 checklist 从未固化**`releases.md:4` 指向的正典是 iteration-3/08 §3.3,标题至今仍是「草案(验证后固化进 git-workflow.md)」,而 `git-workflow.md` 全文 65 行无任何发布/PR/E2E 章节。所有修正只以散文形式活在 `releases.md`,且该文件**自我矛盾**。处置见 §4.0。
4. **M4 分支策略推荐:继续 dev 直推**,不引入常态 feature 分支——理由之一是 CI 的 `push` 触发器不覆盖 `feat/**`,开分支等于自断快速反馈(§2)。
5. **契约 v1.4.0 → v1.5.0 需同步 9 处**doc 2 处 + api 4 份快照 + 4 个守卫常量 + 4×4 计数断言),且**字节级一致性无 CI 保障**、全靠人工 md5 —— 操作顺序见 §3。
6. **M4 必须新增第五份 E2E 脚本** `test_e2e_m4_manual.dart`,发布回归门禁由四份变五份(§4)。
7. M4 引入 AI provider 凭证,而 `check-secrets.sh` 的 7 条规则对 `sk-` / `sk-ant-` / 通用 `api_key` 形态**零覆盖**(§5)。
---
## 1. 核实结果(原始配置取证)
### 1.1 三仓 HEAD / tag / 远端一致性 —— 全部符合
`git ls-remote origin` 与本地 `for-each-ref` 双向比对(2026-09-14):
| 仓库 | 本地当前分支 | origin/dev | origin/main | tag v0.4.0 → commit | 工作区 |
| --- | --- | --- | --- | --- | --- |
| patbond-api | `dev` @ `3cd8005` | `3cd8005` | `3cd8005` | `46afe8a`annotated)→ `3cd8005` | clean |
| patbond-flutter | `dev` @ `fbcd734` | `fbcd734` | `fbcd734` | `c963a0a`annotated)→ `fbcd734` | clean |
| patbond-doc | `main` @ `5cc6361` | —(无 dev | `5cc6361` | `08fda68`annotated)→ `5cc6361` | clean |
三仓 `dev == main == tag`,历史线性,**符合描述**。三个 v0.4.0 均为 **annotated tag**`git cat-file -t` = `tag`),带完整发布说明正文——延续此惯例。
补充事实(用户未提及,非差异但需知悉):
- **v0.4.0 的 tag 实际打于 2026-09-14 11:17**`taggerdate`api/flutter `11:17:35~36`、doc `11:19:17`),tagger `Lixi20`。v0.3.0 打于 `2026-09-10 15:47`。即**发布动作发生在今天上午**,而非 M3.5 收官日 09-11——安全事件导致的 CI 中断修复与发布挤在同一天。
- api 仓存在本地私有 ref `refs/backup/old-main-ff876bc``ff876bc`v0.3.0 重建 main 时保留的孤儿提交备份)。`ls-remote` 无此 ref ⇒ **仅存于本机**。若换机器或本仓重克隆,该备份即消失。
- **分支拓扑不对称**:api 本地只有 `dev` 一个分支;flutter 本地另有 `main` @ `030b11f`**落后 origin/main`fbcd734`)**。这是个陷阱:flutter 仓若有人 `git checkout main` 会拿到陈旧的 main。M4 期间建议直接删掉 flutter 本地 main(发布走 PR,本地根本不需要 main)。
- doc 仓远端**确实只有 main**`GET /branches/dev``not found`),不参与发布分支语义 —— 符合描述。
### 1.2 分支保护实配 —— 6 条描述全部为真
取证方式:`GET /api/v1/repos/zhaoyuxi/{repo}/branches/{branch}`Gitea 1.26.4`https://132.232.242.77`,自签证书需 `-k`)。该端点**匿名可读**并直接返回生效的保护字段。原始响应关键字段:
```text
patbond-api main : protected=true enable_status_check=true
status_check_contexts=["CI / backend-test (push)"]
required_approvals=0
patbond-api dev : protected=false enable_status_check=false status_check_contexts=[]
patbond-flutter main : protected=true enable_status_check=true
status_check_contexts=["CI / flutter-gates (push)"]
required_approvals=0
patbond-flutter dev : protected=false enable_status_check=false status_check_contexts=[]
patbond-doc main : protected=false enable_status_check=false status_check_contexts=[]
patbond-doc dev : {"message":"not found"}
```
`releases.md:112-113` 的表格**逐格一致**,与用户描述**逐条一致**。
**取证边界(必须声明)**`GET /repos/{owner}/{repo}/branch_protections`(完整保护规则对象)**返回 401 `{"message":"token is required"}`**——本机无 Gitea token(已查:无 `~/.gitea*`、无 `~/.netrc`、无 `~/.config/tea`、无 `tea` CLI、环境变量无 token)。因此:
-**已证实**`protected` / `enable_status_check` / `status_check_contexts` / `required_approvals` 四项(上表,来自 `/branches/{branch}`)。
- ⚠️ **未直接证实**:「**禁直接推送**」的具体机制。响应里的 `user_can_push=false` 是**匿名身份**的结果,不能作为「已认证的 Lixi20 也被禁推」的证据。该结论目前的支撑是间接的:`protected=true` + v0.4.0 **确实被迫走了 PR**(§1.5,两仓各有一个 merged PR,而 v0.3.0 是直推)。
- **复核命令**(拿到 token 后执行,建议 M4 开工时补做并把响应贴入本节):
```bash
# 只需 read:repository 权限的 tokenGitea → Settings → Applications → Generate Token
for r in patbond-api patbond-flutter patbond-doc; do
echo "=== $r ==="
curl -sk -H "Authorization: token $GITEA_TOKEN" \
"https://132.232.242.77/api/v1/repos/zhaoyuxi/$r/branch_protections" |
python3 -m json.tool
done
```
重点看 `enable_push`false = 禁一切直推)、`enable_push_whitelist` / `push_whitelist_usernames`、`enable_merge_whitelist`、`block_on_official_review_requests`、`required_approvals`、`status_check_contexts`。
### 1.3 CI 工作流触发器 —— 符合,但覆盖面是 §1.4 问题的根源
三仓均只有 `.gitea/workflows/ci.yml`**无 `.github/workflows/`**。触发器原文:
| 仓库 | 文件:行号 | 触发器 | job 名 |
| --- | --- | --- | --- |
| patbond-api | `.gitea/workflows/ci.yml:15-18` | `on: push: branches: [dev]` + `pull_request:`(无分支过滤) | `backend-test`:26 |
| patbond-flutter | `.gitea/workflows/ci.yml:9-12` | `on: push: branches: [dev]` + `pull_request:`(无分支过滤) | `flutter-gates`:19 |
| patbond-doc | `.gitea/workflows/ci.yml:5-8` | `on: push: branches: [main]` + `pull_request:`(无分支过滤) | `docs-build`:16 |
api `ci.yml:15-18` 原文:
```yaml
on:
push:
branches: [dev]
pull_request:
```
**关键点:`push` 触发器被白名单限定在单一分支**(api/flutter 是 `dev`doc 是 `main`);`pull_request` 无分支过滤,任何 PR 都会跑。
门禁命令与本地门禁同源(`git-workflow.md:58-62`):api = `./mvnw -B clean test``ci.yml:55`);flutter = `dart format --output=none --set-exit-if-changed lib test` / `flutter analyze` / `flutter test``ci.yml:56-58`);doc = `mkdocs build --strict -d /tmp/site`。三仓首个 step 均为 `sh scripts/check-secrets.sh --all`api `ci.yml:41-42`、flutter `:31-32`、doc `:22-24`)。
三仓 `concurrency: group: ci-${{ github.ref }}` + `cancel-in-progress: true`push 与 PR 的 `github.ref` 不同(`refs/heads/dev` vs `refs/pull/N/...`)⇒ **两条流水线并行互不取消**,与 §1.5 实测的「同一提交同时挂两个上下文」吻合。
### 1.4 ⚠️ 必需状态检查选错触发器 —— **确认成立,且比预想更严重**
Reality Checker 提出、本节独立复核**确认成立**。取证:`GET /repos/zhaoyuxi/{repo}/commits/{sha}/statuses?limit=50`(匿名可读)。api `3cd8005`= dev tip = main = v0.4.0)的**全部 9 条** commit status,按时间倒序:
```text
[success ] ctx='CI / backend-test (pull_request)' 2026-09-14T09:54:37+08:00 runs/77
[pending ] ctx='CI / backend-test (pull_request)' 2026-09-14T09:47:54+08:00 runs/77
[success ] ctx='CI / backend-test (push)' 2026-09-14T09:44:05+08:00 runs/73 ← 唯一被要求的上下文
[pending ] ctx='CI / backend-test (pull_request)' 2026-09-14T09:39:50+08:00 runs/77
[pending ] ctx='CI / backend-test (push)' 2026-09-14T09:38:34+08:00 runs/73
[pending ] ctx='CI / backend-test (push)' 2026-09-14T09:38:30+08:00 runs/73
[failure ] ctx='CI / backend-test (push)' 2026-09-11T10:33:31+08:00 runs/73
[pending ] ctx='CI / backend-test (push)' 2026-09-11T10:33:29+08:00 runs/73
[pending ] ctx='CI / backend-test (push)' 2026-09-11T10:33:28+08:00 runs/73
```
flutter `fbcd734` 同构(6 条,`CI / flutter-gates (pull_request)` success `10:02:08` / `CI / flutter-gates (push)` success `09:58:23`)。doc `5cc6361` 只有 `CI / docs-build (push)` success `11:20:44`doc 无 PR)。
**合并后的 combined status**`/commits/{sha}/status`):`state: success`,含两个上下文 —— 即**每个 dev 提交同时携带 `(push)` 与 `(pull_request)` 两条独立状态**。
#### 三条事实推出的结论
| # | 事实 | 出处 |
| --- | --- | --- |
| A | 必需上下文 = `CI / backend-test (push)`**仅此一个** | §1.2 API 响应 |
| B | `push` 触发器限定 `branches: [dev]` | `api/.gitea/workflows/ci.yml:16-17` |
| C | Gitea 的上下文名格式为 `<workflow> / <job> (<event>)` | §1.4 实测两种 event 各自成名 |
**推论一(门禁语义错位,已成立)**:`(push)` 状态只可能由「推送到 dev」产生。因此 `dev → main` PR 的合并门禁**实际由 dev 的 push 流水线满足**,而非 PR 自身的流水线。`(pull_request)` 上下文**不在必需列表中 ⇒ 它红也不阻塞合并**。M3 当初显式填写上下文名是为堵「留空 = 空集为真 = 放行」的漏洞(`releases.md:116` 记载该理由),但填成了 `(push)`——把「空集漏洞」换成了「错触发器漏洞」。
⚠️ 对 v0.4.0 的判断需要修正:`releases.md:47` 写「等 `pull_request` 状态检查转绿后合并」「**分支保护确实要求该检查通过**」——**后半句不成立**。当时真正解除阻塞的是 09:44:05 转绿的 `(push)``(pull_request)` 在 09:54:37 才绿,两者在 11:14:55 合并前都已绿,所以**表象正确、机制归因错误**。这正是「文档转述被全链路采信」的又一例。
**推论二(更严重,尚未被触发)**:**任何非 `dev` 分支 → `main` 的 PR 将永久无法合并。** 因为其 head 提交从未被推送到 `dev` ⇒ 永远不会产生 `(push)` 上下文 ⇒ 必需检查永远处于「缺失」⇒ 无法合并(除非管理员临时改保护配置)。这与两条既定纪律**直接冲突**:
- `git-workflow.md:9`:「改动跨多天、有破坏性风险…从最新 dev 拉出 `feat/<主题>` / `fix/<主题>` 分支」
- iteration-3/08 §3.3 第 8 步:「影响 main 的 hotfix 一律走短命分支 + PR」
即**当前配置下 hotfix 路径是死的**。M4 若出现需要绕过 dev 直修 main 的线上问题,会在最紧急的时刻撞上这个死锁。
#### 修复方向(真实上下文字符串,非猜测)
正确的必需上下文字符串已从 §1.4 的 CI 实跑记录中取到真名:
| 仓库 | 应填的上下文(实测真名) |
| --- | --- |
| patbond-api | `CI / backend-test (pull_request)` |
| patbond-flutter | `CI / flutter-gates (pull_request)` |
**推荐(拍板 D1)**:把必需上下文**替换**为 `(pull_request)` 单值,而非追加。理由:
1. **语义正确**:PR 的合并门禁应由 PR 自己的流水线把关。
2. **解锁 hotfix 路径**`pull_request` 触发器无分支过滤(`ci.yml:18`),任何分支的 PR 都会跑 ⇒ `feat/*` / `hotfix/*` → main 的 PR 可正常合并。
3. **不重新引入空集漏洞**:仍是显式命名,只是名字改对。
4. **不损失保证**:有人会担心失去「dev tip 自身 push 绿」的冗余。但 ff-only 合并下 PR head ≡ dev tip,两条流水线跑的是同一棵树、同一套命令 ⇒ 该保证是**重复的**,不是额外的。
5. **若改为「两者都要」**:严格性更高,但 hotfix 死锁**依然存在**`(push)` 仍缺失)⇒ 不解决推论二,不推荐。
**变更后必做的一次性验证**(否则等于换一个未验证的配置):改配置后在任一仓开一个**空改动的试验 PR**(或就用 v0.5.0 的正式 PR),确认 Gitea 的 PR 页面把 `(pull_request)` 显示为 required 且它红时合并按钮确实禁用。M3 的教训是「配置改了但语义没验」——这次要在 v0.5.0 发布**之前**验完,不要拿正式发布当试验场。
**注意上下文名的脆弱性**:字符串由 `name: CI`workflow 名)+ job key`backend-test`)+ event 三部分拼成。**改任何一个都会使必需上下文永久缺失、PR 永久不可合并**。见 §5 的预案。
### 1.5 v0.4.0 PR 流程实证 —— 符合描述
`GET /repos/zhaoyuxi/{repo}/pulls?state=all`
| 仓库 | PR | 标题 | base ← head | merged | merge_commit_sha | merged_at |
| --- | --- | --- | --- | --- | --- | --- |
| patbond-api | #1 | `v0.4.0 M3.5 体验补齐` | `main` ← `dev` | true | `3cd8005` | 2026-09-14T11:14:55+08:00 |
| patbond-flutter | #1 | `v0.4.0 M3.5 体验补齐` | `main` ← `dev` | true | `fbcd734` | 2026-09-14T11:15:05+08:00 |
**`merge_commit_sha` 恰等于 head sha** ⇒ **Fast-forward、零合并提交**,与描述一致。`ls-remote` 存在 `refs/pull/1/head`(无 `refs/pull/1/merge`)。两仓各仅 1 个 PR ⇒ 历史上从未有过其他 PR,**「非 dev 分支 → main」这条路径从未被走过**,故 §1.4 推论二至今未暴露。
「PR 会自动跟随 dev 新提交重跑」——**间接证实**:`3cd8005` 上有 **3 条** `(pull_request)` 状态(09:39:50 pending → 09:47:54 pending → 09:54:37 success),同一 run 77 被重复上报,与 `releases.md:48` 描述的「发布途中 E2E 脚本提交推入 dev,PR 随即重跑」吻合。
**CI 当前并非红**:三仓五个上下文在 09-14 全部 `success`api 两个、flutter 两个、doc 一个)。iteration-3.5/04 §7.3 记载的 `3cd8005` **`failure`"Failing after 2s"runner 装配级故障)** 确实存在于 `2026-09-11T10:33:31`,但**同一 run 73 于 2026-09-14 09:38 重跑并于 09:44:05 转绿** ⇒ 该 runner 故障已闭环,不是 M4 的开工障碍。
### 1.6 契约快照锁实配 —— 符合,但**字节一致性无 CI 保障**
正典:`patbond-doc/docs/api/openapi.yaml``info.version: 1.4.0`,见该文件 `:4`)。api 侧四份快照 + 正典**五路 md5 全等**:
```text
a7081fb84f1207eef579ab94025f5801 patbond-doc/docs/api/openapi.yaml
a7081fb84f1207eef579ab94025f5801 patbond-api/patbond-auth/src/test/resources/contract/openapi-v1.4.0.yaml
a7081fb84f1207eef579ab94025f5801 patbond-api/patbond-pet/.../openapi-v1.4.0.yaml
a7081fb84f1207eef579ab94025f5801 patbond-api/patbond-user/.../openapi-v1.4.0.yaml
a7081fb84f1207eef579ab94025f5801 patbond-api/patbond-community/.../openapi-v1.4.0.yaml
```
注意**文件名不同**:正典是无版本号的 `openapi.yaml`,快照是 `openapi-v1.4.0.yaml`(内容字节相同)。
四个守卫常量 + 四组计数断言(升版必改的 8 个文件):
| 模块 | `RESOURCE` 常量 | 计数断言(version / paths / operations / schemas |
| --- | --- | --- |
| patbond-auth | `OpenApiContract.java:39` | `AuthContractConformanceTest.java:401-404` → `1.4.0` / 32 / 45 / 75 |
| patbond-pet | `OpenApiContract.java:35` | `ContractConformanceTest.java:757-760` → 同上 |
| patbond-user | `OpenApiContract.java:39` | `MediaContractConformanceTest.java:248-251` → 同上 |
| patbond-community | `OpenApiContract.java:39` | `CommunityContractConformanceTest.java:524-527` → 同上 |
**关键缺口**`./mvnw clean test` 只读 api 仓内文件 ⇒ CI 能发现「api 内部快照与守卫不自洽」,**但完全无法发现「api 快照与 doc 正典不一致」**。跨仓字节一致性**纯靠人工 md5 比对**,无自动门禁。这是 M4 升版最容易静默漂移的一环(§3 给出强制校验命令,§6 拍板 D5 提议补自动化)。
### 1.7 E2E 脚本清单与命名 —— 四份成立,但**命名不齐**
`patbond-flutter` 仓根目录(`ls` 实测):
| 迭代 | 文件名 | 大小 | 场景数 |
| --- | --- | --- | --- |
| M1 | `test_e2e_manual.dart` ⚠️ **无版本号** | 11015 | 7 |
| M2 | `test_e2e_m2_manual.dart` | 27886 | 11 |
| M3 | `test_e2e_m3_manual.dart` | 51212 | 14 |
| M3.5 | `test_e2e_m35_manual.dart` | 45758 | 10 |
**差异**M1 那份叫 `test_e2e_manual.dart`**不是** `test_e2e_m1_manual.dart` —— 用户描述的「延续 `test_e2e_m4_manual.dart` 口径」对 M2/M3/M3.5 成立,M1 是例外。回归清单变五份后,一个不带版本号的名字最容易被误读成「总入口脚本」。见拍板 D7。
**运行方式**`test_e2e_m35_manual.dart:9` 头注释):`dart run test_e2e_m35_manual.dart`;前置为 api 仓 `docker compose up -d --build`(六容器 postgres + minio + auth:8081 + user:8082 + pet:8083 + community:8084)。
**这些脚本与 CI 门禁的关系(三条,M4 新脚本必须遵守)**:
1. **不被 `flutter test` 执行**:脚本在仓根而非 `test/` ⇒ `flutter test``ci.yml:58`)不会收集它们。这是有意的(需要活的后端),保持现状。
2. **被 `flutter analyze` 检查**`analysis_options.yaml` 只 `include: package:flutter_lints/flutter.yaml`**无任何 exclude** ⇒ 分析器覆盖整包含仓根 `.dart` 文件。故新脚本**必须过 analyze**,且需照既有惯例在文件头加 `// ignore_for_file: avoid_print``test_e2e_m35_manual.dart:3` 原文即如此)。
3. **不被格式门禁检查**`dart format --output=none --set-exit-if-changed lib test``ci.yml:56`**只覆盖 `lib` 与 `test`**,仓根脚本不在范围内。⇒ 新脚本格式跑偏不会红 CI,但建议仍手动 `dart format` 保持一致。
另有 4 个 `integration_test/*.dart``client_ux_live_test.dart` / `feed_live_test.dart` / `profile_avatar_live_test.dart` / `publish_live_test.dart`),是真机/活后端集成测试,与发布回归的五份 E2E 是**不同体系**,不计入门禁计数。
### 1.8 其他差异与既存缺口
以下均为**本报告独立实测发现**,不在用户描述范围内,但属 Git 工作流职责域:
#### (1) ⚠️ flutter 仓提交前缀大面积违反规范
`git-workflow.md:13` 原文:「格式:`<前缀>: <中文主题>`,前缀取 `feat` / `fix` / `refactor` / `docs` / `test` / `chore`」。
flutter 仓全部 43 个提交的前缀普查(`git log --format='%s' | sed 's/[:].*//' | sort | uniq -c`):
```text
20 新增 ← 违反
5 fix
4 修复 ← 违反
3 feat
2 test
2 chore
1 重构 ← 违反
1 style ← 前缀不在白名单
1 新增静态演示界面 / 1 完善README / 1 Update project README / 1 update README.md / 1 Initial ...
```
即 **25/43 用中文前缀**(新增 20 + 修复 4 + 重构 1),**规范与实况反向**——实况才是主流。api 仓相反,全用英文(`feat` / `test` / `chore` / `refactor`);doc 仓全用 `docs`。
另有**规范未覆盖的 scope 形态**已在实用:api `test(contract): …`、doc `docs(api): …` / `docs(architecture): …`。`git-workflow.md:13` 的格式串里没有 scope。
⇒ 规范文档与三仓实况三方不一致,M4 需收口(拍板 D6)。
#### (2) 凭证防泄漏第一层(pre-commit)**三仓全部未启用**
`git-workflow.md:40-47` 要求「每人每仓启用一次」`git config core.hooksPath scripts/hooks`。实测:
```text
patbond-api core.hooksPath: (unset) scripts/hooks/pre-commit: 存在
patbond-flutter core.hooksPath: (unset) scripts/hooks/pre-commit: 存在
patbond-doc core.hooksPath: (unset) scripts/hooks/pre-commit: 存在
```
⇒ **钩子脚本入库了但一处都没挂上**,第一层完全未生效,当前只有 CI 兜底(第二层)在防。规范写的是「推荐」不是「强制」,故非违规,但 M4 要落 AI provider 凭证(§5),第一层的价值从「锦上添花」变成「拦在 push 之前」。三条 `git config` 命令即可修复(拍板 D9)。
三仓 `scripts/check-secrets.sh` **md5 全等**`0d4deed251e9161e33a4f1126def4927`)⇒ 副本同步纪律执行到位。
#### (3) ⚠️ patbond-doc 仓**完全没有 `.gitignore`**
实测:`cat .gitignore` → 「没有那个文件或目录」。而本地存在 `site/`mkdocs 默认输出目录)。
`git -c core.excludesFile=/dev/null check-ignore -v site/` → **未被仓库规则忽略**;仅 `git check-ignore -v site/` 命中 **`/home/lx/.gitignore_global:183`** —— 即**只靠本机用户级全局忽略文件屏蔽**。
- 当前 `git ls-files site/ | wc -l` = **0**(尚未误提交,工作区 clean)。
- 风险:换机器、新 clone、或任何未配置全局忽略的环境下,一次 `git add -A` 就会把整个 `site/` 构建产物提交进去(约百余文件)。
- 对比:api 仓有 `.gitignore``:1` `target/`、`:10-11` 本地 `application.yml` + `.sample` 例外、`:14` `.env`),flutter 仓有 `.gitignore``build` / `.dart_tool` 均 repo-ignored)。**只有 doc 仓是裸的。**
- 这与 `git-workflow.md:62` 的「不把 `site/` 落进仓库」是同一条纪律 —— 但该纪律**只写在文档里,没有机器保障**。
⇒ 一行修复(doc 仓新建 `.gitignore` 写入 `site/`),列为 M4 第一波待办(拍板 D8)。本次**只规划不改**。
#### (4) 数字口径矛盾(三处,需在 v0.5.0 发布记录中统一)
本报告**未运行测试**,故下列数字**非本报告实测**,仅记录来源冲突,供 PM/RC 收口:
| 项 | `releases.md` / `feature-checklist.md` 记载 | 其他来源 | 判断 |
| --- | --- | --- | --- |
| api 测试数 | **379**`releases.md:17` / `:35`、`feature-checklist.md:5` | **381**iteration-3.5/04 §7.2 原文「BUILD SUCCESS,381 测试全绿」;该报告同时说明测试 379→381 因新增 2 个矩阵方法) | **381 应为准**;379 是冻结前口径,被下游照抄 |
| E2E 断言数 | **234**`releases.md:36` | **226**Evidence Collector 机械计数:210 个 `check(` + M1 的 16 个 ``) | 倾向 **226**;本报告未复核计数方法 |
| 真机挂起项 | v0.4.0 段落列「M2 两项 + M3 四项 + M3.5 新增两项」= 8 | **10 项**Evidence Collector | 未复核,以 EC/RC 口径为准 |
| 埋点白名单 | 42 | **41** | 未复核,以 EC/RC 口径为准 |
E2E **场景数 42 正确**7+11+14+10),本报告独立核算一致。
⇒ 纯文档口径问题,不影响 Git 流程,但 v0.5.0 发布记录**不要再照抄**上游数字,一律以当次实跑输出为准(写进 §4 checklist)。
## 2. M4 分支与提交规划
### 2.1 分支策略:**继续 dev 直推**(不引入常态 feature 分支)
推荐 **dev 直推为默认**,仅在两种情形开短命分支。理由按证据强度排序:
1. **CI 的 `push` 触发器不覆盖 feature 分支**`ci.yml:16-17` `branches: [dev]`)⇒ 推到 `feat/xxx` **不会跑任何 CI**。开分支等于自断快速反馈,除非每个分支都立刻开 PR 换 `pull_request` 触发(`ci.yml:18` 无分支过滤,这条是通的)。这是本项目特有的、比通用最佳实践更强的约束。
2. **既有纪律本就如此**`git-workflow.md:9` 把 feature 分支限定为「跨多天 / 有破坏性风险 / 多人并行同仓」三种情形,M1–M3.5 四个迭代全程 dev 直推,43flutter)/ 数十(api)提交零合并提交、历史线性。没有出现需要分支的问题。
3. **单人开发**ADR-011 的 PR 强制条款已在 iteration-3/08 §6 #1 降级为「两人并行同仓」与「影响 main 的变更」两种情形。M4 若仍是单人推进,feature 分支的核心收益(隔离并行)不存在。
**例外——建议开短命分支的两种 M4 情形**:
| 情形 | 分支名 | 处置 |
| --- | --- | --- |
| **AI provider 接入探针(spike**:第三方 SDK/HTTP 选型、prompt 迭代、成本与延迟实测,大概率产生大量废弃代码 | `spike/ai-provider` | 探完**不合并**,结论写报告,实现另起干净提交直推 dev。分支删除。 |
| **AI 创作可能触发的跨模块重构**(如 media 域被复用为 AI 产物存储) | `refactor/<主题>` | 完成后 `git rebase dev` 整理为原子提交序列,`--ff-only` 合回 dev 并删分支(`git-workflow.md:9` 的「短命分支,不留长期分叉」) |
两种情形都**不要**从 feature 分支直接开 PR 到 `main` —— §1.4 推论二下那种 PR 永久不可合。
**dev 必须随时可构建**`git-workflow.md:7`):M4 的契约升版、新模块接入等「一改就全红」的动作必须**攒成单个原子提交**再推,不要分两次推让 dev 中途红(§3 详述)。
### 2.2 提交粒度与信息规范
**粒度**(沿 `git-workflow.md:17`「一次提交做一件事,可独立回退」)。M4 具体切法建议:
| M4 工作项 | 建议提交粒度 |
| --- | --- |
| Flyway 迁移(若有) | **迁移 + 其验证集成测试 = 一个提交**(沿 T3-01 先例:`feat: Flyway V5 community schema 基线 + 迁移验证集成测试`)。迁移一旦推入 dev 即不可变(`git-workflow.md:33`),故必须与验证同时落地 |
| 新模块骨架(若有) | 单独一个提交(沿 T3-02 `feat: 新建 patbond-community 模块骨架`),含根 pom `<module>` 一行 |
| 每个对外端点 | 一个 `feat:` 提交,含实现 + 单测 + 集成测试 |
| **契约升版 v1.5.0** | **doc 侧一个提交、api 侧一个原子提交**,两侧都不与业务代码混(§3) |
| 客户端每页 | 一个提交(沿 flutter 惯例,一页一提交) |
| 第五份 E2E 脚本 | 单独一个提交(沿 v0.4.0 先例 `新增:M3.5 E2E 烟囱脚本…(v0.4.0 发布门禁)` |
| 报告入档 | doc 仓按波次批量一个 `docs:` 提交(沿既有惯例) |
**信息规范**(M4 起统一,见拍板 D6):
```text
<前缀>[(<scope>)]: <中文主题><工单号>[ADR-0xx]
- 关键改动列表
- 门禁:<命令输出结论>(测试数量与结果)
```
- 前缀白名单:`feat` / `fix` / `refactor` / `docs` / `test` / `chore` / `style``style` 已在 flutter 实用,补进白名单)。
- **scope 可选**,正式承认 `test(contract):` / `docs(api):` 形态。
- 主题用中文一句话;引用 M4 工单号(`T4-xx`)与 ADR 编号。
- 正文必须带**验收证据**`git-workflow.md:16`)——测试数、门禁命令结论。这是 M4 数字口径不再漂移的第一道防线(§1.8(4))。
- **不改写已推送历史**`git-workflow.md:34`);**不 force push `dev`/`main`**`:32`)。
### 2.3 三仓同步点(契约不漂移的机制)
M4 有 **4 个强同步点**,其中契约升版是唯一「必须同一时间窗内完成」的:
| 同步点 | 涉及仓 | 顺序约束 | 漂移检测 |
| --- | --- | --- | --- |
| **S1 契约升版 v1.5.0** | doc(正典)→ api4 份快照+守卫) | **doc 先,api 后**,同一工作时段完成 | 五路 `md5sum` 人工比对(**无 CI 保障**,§1.6)+ api 侧守卫计数断言 |
| **S2 客户端消费新契约** | api → flutter | api 端点落地并契约冻结后,flutter 才接线 | flutter 数据层测试对齐契约操作数(沿 T3-12「契约 v1.3.0 十九操作全覆盖」惯例) |
| **S3 防泄漏规则增补** | api + flutter + doc **三仓同时** | 无先后,但必须**同批提交** | `md5sum` 三仓 `scripts/check-secrets.sh` 全等(`git-workflow.md:38` 明文要求;当前全等 ✅) |
| **S4 发布** | api + flutterPR)→ doc(记录+tag | 两仓 PR 合并并打 tag 后,doc 记录+打 tag | 三仓 tag 同名 v0.5.0,互为对照 |
**S1 防漂移的硬机制**(因 CI 不管跨仓,必须靠人工纪律 + 一条命令):
```bash
WS=<你的工作区> # 例:~/workspace/patbond
V=1.5.0
md5sum "$WS/patbond-doc/docs/api/openapi.yaml" \
"$WS"/patbond-api/patbond-{auth,pet,user,community}/src/test/resources/contract/openapi-v$V.yaml |
awk '{print $1}' | sort -u | wc -l
# 必须输出 1(五路字节全等);输出 ≥2 即已漂移,禁止提交
```
把这条命令写进 M4 契约升版工单的验收条件,并在 api 侧升版提交的正文里贴出 md5 值(沿 T3.5-07 先例:其提交正文原文含 `md5 与正典逐一比对一致 a7081fb84f1207eef579ab94025f5801`)。
**S1 的一个易漏点**(M3.5 踩过,已写入代码注释):`/api/v1/me` 的契约守卫在 **patbond-auth** 模块,而实现在 patbond-user。M4 若再扩 `/me` 面,先看 auth 模块。
## 3. 契约 v1.5.0 升版的 CI 联动操作顺序
### 3.1 升版实际要动 9 处(清单)
| # | 位置 | 改什么 |
| --- | --- | --- |
| 1 | `patbond-doc/docs/api/openapi.yaml:4` | `version: 1.4.0` → `1.5.0` |
| 2 | 同文件 `:5+` `info.description` | 追加 **1.5.0 M4 冻结**段落 + 重申「对 v1.4.0 纯增量」承诺 |
| 3 | `patbond-doc/docs/api/index.md:3` | 「(OpenAPI 3v1.4.0),当前 32 路径 / 45 操作」→ 新版本与新计数 |
| 4 | 同文件 `:36` | 冻结纪律行追加「AI 创作域已冻结(1.5.0)」 |
| 5 | api ×4 快照文件 | 新增 `openapi-v1.5.0.yaml`(正典字节副本)、**`git rm` 旧 `openapi-v1.4.0.yaml`** |
| 6 | api ×4 `OpenApiContract.java` | `RESOURCE` 常量:auth`:39` / pet`:35` / user`:39` / community`:39`,值改 `/contract/openapi-v1.5.0.yaml`;顺带改类 javadoc 里的版本字样 |
| 7 | api ×4 conformance test | 计数断言 4 行 × 4 文件:auth`:401-404` / pet`:757-760` / user`:248-251` / community`:524-527` |
| 8 | api ×N conformance test | 新增操作的响应矩阵入场(新格数) |
| 9 | flutter 数据层测试 | 对齐新操作数(S2,可延后到客户端波次) |
**旧快照删除是既定纪律**(T3-19 先例、T3.5-07 复述):守卫只认一份,保留旧快照是死重,历史版本由 git 与 doc 仓承载。
### 3.2 操作顺序(避免 CI 红的关键:**api 侧必须是单个原子提交**)
理解为什么能避免红,先记住一条已核实的事实(§1.6):**api 的 CI 只读 api 仓内文件**。它检测的是「快照 ↔ 守卫 ↔ 矩阵」三者**内部自洽**,不检测与 doc 正典的一致性。所以:
- doc 与 api 之间**没有 CI 层面的先后依赖**,doc 先推不会让 api 红,api 后推也不会让 doc 红。
- 但 **api 仓内部**只要三者中任一处未同步,`./mvnw clean test` 立即红(守卫的 `info.version` 断言就是为此设计的)。⇒ **红与不红只取决于 api 侧是否原子提交。**
推荐顺序(延续 T3.5-07 实证路径:doc `5f02909` 先,api `3cd8005` 后):
**第 0 步 — 前置(契约先行纪律)**
`docs/api/index.md:36` 明文要求「契约变更须先改本文件目录下的 OpenAPI,再改实现」。故 M4 的端点实现落地前,先在 doc 侧定型契约(可以是草案报告,正式升版在实现定型后)。
**第 1 步 — doc 仓:升正典,单独提交**
```bash
cd $WS/patbond-doc
# 改 openapi.yaml 的 version + description、index.md 的版本与计数、冻结纪律行
python3 -c "import yaml;yaml.safe_load(open('docs/api/openapi.yaml'))" # 解析必须通过
mkdocs build --strict -d /tmp/site # exit 0 零 warning
sh scripts/check-secrets.sh --all # exit 0
git add docs/api/openapi.yaml docs/api/index.md
git commit -m "docs(api): M4 契约冻结 v1.5.0——AI 创作域(T4-xx"
git push origin main # doc CI 只跑 mkdocs strict + secret scan,与 api 无关
```
补充校验(沿 T3.5-07 §7.2 惯例,建议保留):全部 `$ref` 可解析、`operationId` 无重复无缺失、零未引用 schema、`tags` 声明与使用双向闭合。
**第 2 步 — 记录正典 md5(后续所有比对的基准)**
```bash
md5sum $WS/patbond-doc/docs/api/openapi.yaml
```
**第 3 步 — api 仓:一次性改完 5~8 项,本地全绿后才提交(⚠️ 不要中途 commit)**
```bash
cd $WS/patbond-api
CANON=$WS/patbond-doc/docs/api/openapi.yaml
for m in auth pet user community; do
cp "$CANON" patbond-$m/src/test/resources/contract/openapi-v1.5.0.yaml
git rm -q patbond-$m/src/test/resources/contract/openapi-v1.4.0.yaml
done
# 改 4 个 RESOURCE 常量、4×4 计数断言、新增操作矩阵(见 §3.1 的 6/7/8)
```
提交**前**必须过的三道校验:
```bash
# (a) 五路字节全等 —— 唯一能防跨仓漂移的检查,无 CI 兜底
md5sum "$CANON" patbond-{auth,pet,user,community}/src/test/resources/contract/openapi-v1.5.0.yaml |
awk '{print $1}' | sort -u | wc -l # 必须为 1
# (b) 全量测试(守卫 + 矩阵)
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test # BUILD SUCCESS0 失败
# (c) 防泄漏
sh scripts/check-secrets.sh --all # exit 0
# (d) 确认没有残留旧快照
git ls-files '*openapi-v*' | sort # 只应出现 4 个 v1.5.0
```
三道全过再一次性提交:
```bash
git commit -m "test(contract): v1.5.0 快照四模块同步 + AI 创作矩阵入场(T4-xx)
- 四模块快照 openapi-v1.4.0.yaml → openapi-v1.5.0.yaml,删旧文件(守卫只认一份)
- 四份守卫期望升版:1.4.0/32/45/75 → 1.5.0/<路径>/<操作>/<schema>
- 矩阵 181 → <新格数> 格
- md5 与正典五路逐一比对一致 <md5 值>
- 门禁:mvnw clean test BUILD SUCCESS<N> 测试全绿;check-secrets --all exit 0"
git push origin dev
```
**第 4 步 — 等 dev 的 `(push)` CI 转绿**,确认 `CI / backend-test (push)` 为 success 再继续后续工单:
```bash
SHA=$(git rev-parse HEAD)
curl -sk "https://132.232.242.77/api/v1/repos/zhaoyuxi/patbond-api/commits/$SHA/status" |
python3 -m json.tool
```
**第 5 步 — flutter 侧(S2,可延后)**:客户端数据层测试对齐新操作数,单独提交。
### 3.3 反模式(会红 CI,明确禁止)
| 反模式 | 后果 |
| --- | --- |
| 只 `cp` 新快照就 commit,守卫常量下一提交再改 | 守卫仍指向已被 `git rm` 的 v1.4.0 → `契约快照缺失` 抛错,**dev 全红** |
| 只改守卫版本断言,没换快照文件 | `info.version` 断言 `1.5.0` vs 快照 `1.4.0` → 四模块全红 |
| 只同步 2~3 个模块 | 未同步模块红。**四个模块必须同批**auth / pet / user / community |
| 手改 api 侧快照而非从正典 `cp` | 字节漂移,md5 不等;CI **不会发现**(§1.6),漂移静默进库 —— 最危险的一种 |
| 新增操作只升计数不进矩阵 | 计数断言绿,但「未声明字段即漂移」类断言红(M3.5 曾一次性红 11 格) |
| 保留旧快照「以防万一」 | 违反 T3-19 纪律;且 `git ls-files '*openapi-v*'` 会暴露死重 |
### 3.4 M4 若新增 api 模块:契约成本会从 4 处变 5 处
已核实 iteration-3/08 §5.1 的「新模块 CI 零成本」结论**至今仍准确**:根 pom `<modules>` 现为 common/user/auth/pet/community 五个,`ci.yml:55` 是根反应堆 `./mvnw -B clean test`,新增模块只需根 pom 加一行、**`ci.yml` 零改动**。
但**契约侧不是零成本**:当前四个模块各自持有一份 `OpenApiContract.java` + 快照(同构副本纪律,与 `BearerAuthFilter` 同)。若 M4 新建模块**也承载契约一致性测试**,升版就要同步 **5 份快照 + 5 个常量 + 5 组断言**,每次升版成本 +25%。
⇒ 建议:**新模块默认不复制契约框架**,仅当它确实需要对冻结契约做一致性断言时才复制;其端点的契约矩阵可挂在既有模块(`/me` 系挂 auth 就是先例)。见拍板 D3。
**Flyway**:现有 V1~V5 **全部位于 `patbond-user/src/main/resources/db/migration/`**(含 pet_health 与 community 的 schema),即**迁移集中由 user 模块承载**。M4 若需新表,下一个版本号是 **`V6__*.sql`**,仍放该目录(沿 5/5 先例),**不要**在新模块另起迁移目录(会导致两个 Flyway 位置,历史表版本冲突)。已推送的 V1~V5 不可修改(`git-workflow.md:33`)。
## 4. v0.5.0 发布 checklist
### 4.0 先解决「流程该固化到哪」与双处矛盾
**问题(Evidence Collector 提出,本报告独立复核确认)**:发布流程目前**没有一份可信的单一来源**。
已核实的三条事实:
1. **`releases.md:4` 指向的正典是一份「草案」**:原文「按 `[发布 checklist](iterations/iteration-3/08-git-workflow-plan.md)`(§3.3)执行」,而该节标题至今是「**§3.3 发布 checklist 草案(首次发布用,验证后固化进 git-workflow.md**」。
2. **固化从未发生**:本报告通读 `git-workflow.md` 全文 **65 行**,章节仅 5 个——分支模型(:5) / 提交信息约定(:11) / 禁止事项(:28) / 凭证防泄漏(:36) / 提交前本地门禁(:56)。**无任何发布、PR、E2E、checklist 内容**。
3. **iteration-3/08 §3.3 的 8 步里 4 步已过期**:
- 第 2 步原文「跑 **M2+M3 两份** E2E 烟囱脚本」→ 实际已是**四份**M1/M2/M3/M3.5),M4 后为五份;
- 第 3 步(命名统一 + api main 重建)→ 一次性项,已完成,不再重复;
- 第 4 步「`git checkout main && git merge --ff-only dev && git push origin main`」→ **保护启用后该命令必被拒**;
- 第 6 步(启用分支保护)→ 一次性项,已完成。
4. **`releases.md` 自我矛盾**`:36` 与 `:56` 写「**四份**」E2E,但 `:131`(「发布后生效的纪律」的前瞻步骤 1)仍写「完成 checklist 第 1~2 步(三仓 CI 绿 + **E2E 双份回归** PASS)」。**错在前瞻侧**——照 `:131` 执行会只跑 M2+M3**漏跑 M1 的 7 场景 + M3.5 的 10 场景,共 17 个场景**(M4 后漏跑 17 + M4 新增份数)。
⇒ 一份「草案」被当正典、修正散落在发布记录的散文里、且该散文自我矛盾。**这正是 M3.5「转述被全链路采信」教训的同类结构。**
#### 推荐处置(拍板 D2
**把发布流程固化为 `git-workflow.md` 的新增章节「## 发布流程(dev → main)」**,理由:
- `git-workflow.md` 是**跨迭代常设规范**,本就是 iteration-3/08 §3.3 自己指定的固化目标(「验证后固化进 git-workflow.md」),只是没执行;
- 迭代报告(iteration-3/08)是**历史快照**,不应承担常设正典职责——它写的是「M3 时的规划」,随时间必然过期;
- `releases.md` 定位是「每次发布**追加一条记录**」(`:3`),不是流程正典。
配套三个动作(本报告不执行,建议由主会话统一处置):
| 动作 | 目标文件 | 内容 |
| --- | --- | --- |
| A | `git-workflow.md` | **新增**「## 发布流程(dev → main)」章节,正文为 §4.1 的 checklist |
| B | `iteration-3/08-git-workflow-plan.md` §3.3 | **标题加历史标注**:「(**已于 M4 固化进 git-workflow.md,本节仅存历史,勿照此执行**)」。不删改正文(历史报告不改写) |
| C | `releases.md:4` + `:131` | `:4` 的链接改指 `git-workflow.md` 的新章节;`:131` 的「E2E 双份回归」**改为「E2E 全份回归(份数以 git-workflow.md 为准)」**——不写死份数,避免再次出现「数字散落多处」 |
**防复发设计**:份数**只在一处写死**(`git-workflow.md` 的发布流程章节),其他所有位置一律表述为「全份 E2E 回归」并链接过去。M4 新增第五份时只改那一处。
### 4.1 v0.5.0 发布 checklist(可直接执行)
> **适用前提**`main` 已启用保护、禁直推(§1.2);必需状态检查上下文**已按拍板 D1 修正为 `(pull_request)` 并完成一次性验证**(§1.4)。若 D1 未采纳,第 4 步的门禁语义仍是错位的,见 §5.1。
> **变量**`WS=<你的工作区>``GITEA=https://132.232.242.77`(自签证书,`curl` 需 `-k`)。
#### 第 1 步 — 冻结与本地门禁(三仓)
```bash
# api
cd $WS/patbond-api && git status --porcelain # 必须为空
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test # BUILD SUCCESS0 失败
sh scripts/check-secrets.sh --all # exit 0
# flutter
cd $WS/patbond-flutter
dart format --output=none --set-exit-if-changed lib test # 0 changed
flutter analyze # No issues
flutter test # All tests passed
sh scripts/check-secrets.sh --all # exit 0
# doc
cd $WS/patbond-doc && mkdocs build --strict -d /tmp/site # exit 0 零 warning
sh scripts/check-secrets.sh --all # exit 0
```
**记录实跑输出的测试数**(api / flutter 各一个数字),后续发布记录**一律用这两个数**,不得照抄上游文档(§1.8(4) 的 379/381 就是照抄事故)。
#### 第 2 步 — 三仓 CI 绿(查 API,不看网页印象)
```bash
for r in patbond-api patbond-flutter patbond-doc; do
cd $WS/$r; SHA=$(git rev-parse HEAD)
echo "=== $r $SHA ==="
curl -sk "$GITEA/api/v1/repos/zhaoyuxi/$r/commits/$SHA/status" |
python3 -c "import sys,json;d=json.load(sys.stdin);print(' state:',d['state']);[print(' ',s['status'],repr(s['context'])) for s in d['statuses']]"
done
```
放行标准:三仓 `state: success`。**必须是当前 HEAD 的状态**——若 dev 在此期间又有提交推入,重跑本步(`releases.md:57` 的教训:不要用旧的绿色状态放行)。
#### 第 3 步 — E2E **五份**全份回归(发布门禁核心)
```bash
cd $WS/patbond-api && docker compose up -d --build # 六容器
docker compose logs postgres | grep -i flyway # 记录 Current version(零迁移/新迁移均需取证)
cd $WS/patbond-flutter
dart run test_e2e_manual.dart # M1 7 场景
dart run test_e2e_m2_manual.dart # M2 11 场景
dart run test_e2e_m3_manual.dart # M3 14 场景
dart run test_e2e_m35_manual.dart # M3.5 10 场景
dart run test_e2e_m4_manual.dart # M4 <N> 场景 ← 本迭代新增,见 §4.2
```
放行标准:**五份全 PASS,场景数 42 + M4 的 N,零失败**;契约偏差 **0**(逐场景对照 v1.5.0)。同环境串行跑,证据入迭代报告。
⚠️ **门禁是五份,不是两份也不是四份**。`releases.md:131` 的「双份」表述**已过期,勿照执行**(§4.0)。
#### 第 4 步 — 建 PRapi + flutter 各一个)
在 Gitea 网页建 `dev → main` PR
- 标题:`v0.5.0 M4 AI 创作`
- 正文:贴门禁证据链接(测试数、五份 E2E 报告、契约 v1.5.0 纯增量比对结论)
- 等**必需状态检查**转绿。按 D1 修正后应为 `CI / backend-test (pull_request)` / `CI / flutter-gates (pull_request)`。
- **合并方式必须选 Fast-forward only**v0.4.0 先例:`merge_commit_sha == head sha`,零合并提交、历史线性)。
- 合并后核验:
```bash
for r in patbond-api patbond-flutter; do
cd $WS/$r && git fetch -q origin
echo "$r main=$(git rev-parse origin/main) dev=$(git rev-parse origin/dev)"
done # 两个 sha 必须相等
```
#### 第 5 步 — 打 tag(三仓,annotated
```bash
cd $WS/patbond-api && git tag -a v0.5.0 -m "M4 AI 创作" && git push origin v0.5.0
cd $WS/patbond-flutter && git tag -a v0.5.0 -m "M4 AI 创作" && git push origin v0.5.0
cd $WS/patbond-doc && git tag -a v0.5.0 -m "M4 AI 创作" && git push origin v0.5.0
```
延续惯例:**annotated tag**,正文写完整发布说明(api 的 v0.4.0 tag 正文即模板:版本内容分条 + 契约版本 + 测试数 + 门禁结论)。doc 仓在**发布记录提交之后**打,使 tag 指向含本次记录的提交。
#### 第 6 步 — 发布记录入 doc
在 `releases.md` **顶部**追加 v0.5.0 段落(最新在最上,`:4` 约定):三仓 tag 与哈希、版本内容增量、门禁证据表(**测试数用第 1 步实跑值**、E2E 写「五份 / 场景数 / 断言数」)、已知遗留、checklist 变更。
#### 第 7 步 — 发布后核验(三仓一致性)
```bash
for r in patbond-api patbond-flutter patbond-doc; do
echo "=== $r ==="; cd $WS/$r
git ls-remote origin | grep -E 'refs/heads/(dev|main)$|refs/tags/v0.5.0'
done
```
放行:api/flutter 的 `dev == main == v0.5.0^{}`doc 的 `main == v0.5.0^{}`。
**已废止的步骤**(勿执行):iteration-3/08 §3.3 的第 3 步与第 6 步(一次性项,已完成);第 4 步的本地 `checkout main && merge --ff-only && push`**保护启用后必被拒**)。
### 4.2 第五份 E2E 脚本的落位与命名约定
| 项 | 约定 |
| --- | --- |
| **路径** | `patbond-flutter/` **仓根**(与既有四份并列,**不进 `test/`**——`flutter test` 不应收集它,它需要活的后端) |
| **文件名** | `test_e2e_m4_manual.dart`(延续 m2/m3/m35 口径) |
| **运行** | `dart run test_e2e_m4_manual.dart`;前置 api 仓 `docker compose up -d --build` |
| **头注释** | 照 `test_e2e_m35_manual.dart:1-40` 模板:shebang、`// ignore_for_file: avoid_print`、前置条件、运行方式、**与前四份的分工声明**、冻结契约版本(v1.5.0 + 路径/操作/schema 计数)、场景清单编号、脱敏说明、`library;` |
| **覆盖范围** | **只覆盖 M4 增量对外面**,回归由前四份承担(M3.5 脚本明文如此声明,延续) |
| **必过门禁** | `flutter analyze`(分析器覆盖仓根,`analysis_options.yaml` 无 exclude)⇒ 必须加 `// ignore_for_file: avoid_print`,否则 CI 红 |
| **不受门禁** | `dart format` 只覆盖 `lib test``ci.yml:56`)⇒ 仓根脚本不被格式门禁拦,但仍建议手动 `dart format` |
| **脱敏纪律** | token 截断、预签名 URL 签名 query 抹为 `<SIGNATURE_REDACTED>`、Idempotency-Key 占位(M3.5 脚本既有纪律)。**M4 新增:AI provider 的 API key 与 prompt/响应中的用户内容同样必须脱敏**,且脚本本身会被 `check-secrets.sh --all` 扫(`ALLOW` 白名单含 `redacted`,用 `<...REDACTED>` 形态即可放行) |
| **提交** | 单独一个提交,信息延续 `新增:M4 E2E 烟囱脚本——<主题>N 场景(v0.5.0 发布门禁)` |
| **登记** | 场景数与断言数写进迭代报告;**份数写进 `git-workflow.md` 的发布流程章节(唯一写死处,§4.0 防复发设计)** |
## 5. 风险与预案
### 5.1 PR 出现非快进(无法 ff)
**成因判别先于动作**。`dev` 与 `main` 分叉只有三种可能,处置完全不同:
```bash
cd $WS/<仓> && git fetch origin
git rev-list --left-right --count origin/main...origin/dev # 输出「A<TAB>B」
# A=0 → main 无独有提交,可 ff(正常)
# A>0 → main 上有 dev 没有的提交 ⇒ 分叉,先查明
git log --oneline origin/dev..origin/main # 列出 main 独有的提交,逐个看作者与时间
```
| 成因 | 判别特征 | 处置 |
| --- | --- | --- |
| **(a) 有人绕过 dev 直接改了 main** | main 独有提交的作者/时间可查;理论上被保护阻止,但管理员可临时关保护 | **不要用合并提交掩盖**(`releases.md:138` 明文)。查明该提交内容,把它 **cherry-pick 到 dev**,然后 main 重新 ff。同时查为什么保护被绕过 |
| **(b) 上次发布用了非 ff 的合并方式** | main 独有提交是一个 merge commit | 历史已污染但可接受,本次仍可 ffmerge commit 是 dev 的后代?若否则同 (a))。**下次严格选 Fast-forward only** |
| **(c) 误在 main 上打了 hotfix** | 见 (a) | 同 (a),且暴露 §1.4 推论二:hotfix 想走 PR 会死锁,所以有人图省事直推了 main |
**明确禁止**`git push --force origin dev:main`(破「不 force push 共享分支」戒律,`git-workflow.md:32`);`merge --allow-unrelated-histories`iteration-3/08 §3.1 选项 C 已判定不推荐)。
**若确实必须做非 ff 合并**(例如 main 上有必须保留的独有提交且无法 cherry-pick):在 Gitea PR 里选 `Create merge commit`,并**在发布记录中显式登记为例外 + 写清原因**。历史线性性让位于可追溯性,但必须留痕。
### 5.2 CI 状态检查上下文名变化 —— **最高杀伤力的低概率风险**
上下文字符串 = `<workflow name> / <job key> (<event>)`,三段任一改动即使必需上下文**永久缺失**,PR **永久不可合并**(不是变红,是「检查从未上报」)。触发改名的动作:
| 动作 | 后果 |
| --- | --- |
| 改 `ci.yml` 的 `name: CI` | 全部上下文改名 |
| 改 job key`backend-test` / `flutter-gates` | 该仓上下文改名 |
| 增删 job(如把 `backend-test` 拆成 `unit` + `integration` | 旧上下文消失 |
| 改 `on:` 触发器(如给 `pull_request` 加分支过滤) | 对应 event 的上下文可能不再产生 |
**预案(顺序执行)**
1. **改名前先改保护配置**,不要反过来。顺序:Gitea 保护里**先追加新上下文名**(此时新旧并存,required 是「都要绿」)→ 推 `ci.yml` 改名 → 确认新上下文上报成功 → **再从保护里删掉旧上下文名**。这样任一时刻都不会出现「required 上下文无人上报」。
2. **M4 期间尽量不动 `ci.yml` 的 name/job key**。若 M4 新增 api 模块,根反应堆 `./mvnw -B clean test``ci.yml:55`)自动覆盖,**无需新增 job** ⇒ 天然规避(§3.4)。
3. **卡死时的解锁手段**(记录下来,免得临场慌):仓库 admin 在 Gitea → Settings → Branches → main → 暂时取消勾选「Enable Status Check」或删掉失效上下文 → 合并 → **立即改回正确上下文名**。每次这样操作都要在发布记录里留痕。
4. **改名后必须做一次空 PR 验证**(同 §1.4 的一次性验证要求),不要拿正式发布当试验场。
**取真名的唯一可靠方法**(不要猜、不要凭记忆):
```bash
SHA=$(cd $WS/patbond-api && git rev-parse origin/dev)
curl -sk "$GITEA/api/v1/repos/zhaoyuxi/patbond-api/commits/$SHA/statuses?limit=50" |
python3 -c "import sys,json;[print(repr(s['context']),s['status']) for s in json.load(sys.stdin)]"
```
### 5.3 doc 仓不受保护带来的风险
doc 仓 `main` **未启用保护、可直推、且是唯一分支**(§1.2)。四条实际风险与处置:
| 风险 | 现状评估 | 处置 |
| --- | --- | --- |
| **契约正典可被无门禁直推** | ⚠️ **这是最实质的风险**:`docs/api/openapi.yaml` 是 api 四份快照的**唯一上游**(§1.6),改它没有任何 CI 门禁能验证下游一致性。一次误推正典 → api 侧 md5 静默不等 → 漂移进库 | **不靠保护,靠流程**:契约升版严格按 §3.2 顺序,api 侧提交正文**必须贴 md5**(可事后审计)。中期方案见拍板 D5(在 api CI 里加跨仓 md5 校验 step |
| **构建产物误入库** | ⚠️ **`.gitignore` 完全不存在**(§1.8(3)),`site/` 仅靠 `~/.gitignore_global:183` 屏蔽 | 一行修复:doc 仓新建 `.gitignore` 含 `site/`(拍板 D8 |
| **误推可直接改写发布记录** | 中等:`releases.md` 是发布事实的唯一记录,无审核 | 依赖 tag 作为交叉对照(三仓同名 tag 互证,`releases.md:5` 既有设计)。可接受 |
| **doc CI 红也能推上去** | 低:`push` 触发器覆盖 main`ci.yml:7`),红了看得见,但不阻塞 | 保持现状;提交前跑 `mkdocs build --strict` 是既有纪律(`git-workflow.md:62` |
**是否给 doc 仓 main 启用保护?推荐不启用**(拍板 D10):doc 是日常直推分支,启用保护意味着每条文档改动都要开 PR,成本远超收益;且 doc 无 dev 分支,启用后连日常提交都要建分支。**替代方案**是把关键风险(契约正典)用 CI 校验兜住(D5),而不是用分支保护。
### 5.4 tag 打错的回退方式
三种错法,处置不同。**前提**:v0.5.0 tag 一旦被他人 fetch,改动就是历史改写——但本项目是单人 + 三仓自托管,回退窗口实际很宽。
**(a) tag 打在错误的提交上**(最常见)
```bash
cd $WS/<仓>
git tag -d v0.5.0 # 删本地
git push origin :refs/tags/v0.5.0 # 删远端(冒号前缀 = 删除 ref)
git tag -a v0.5.0 <正确的sha> -m "M4 AI 创作"
git push origin v0.5.0
```
⚠️ 删远端 tag 后**务必立即重打并推**,不要留「三仓 tag 不齐」的中间态。**三仓必须同批处理**——若只有 api 的 tag 错了,也只需改 api,但要重新核验 §4.1 第 7 步的三仓一致性。
**(b) tag 名写错**(如打成 `v0.5` / `V0.5.0`
```bash
git tag v0.5.0 v0.5 # 从旧 tag 建正确名(annotated 会退化为 lightweight
# 更稳:直接按 (a) 用 -a 重建,保留完整发布说明正文
git tag -d v0.5 && git push origin :refs/tags/v0.5
```
本项目三仓全部 v0.3.0/v0.4.0 均为 **annotated**,重建时**必须用 `-a` 并补回说明正文**,否则 tag 类型漂移(`git cat-file -t` 会从 `tag` 变 `commit`)。
**(c) tag 说明写错但指向正确**
```bash
git tag -a v0.5.0 -f -m "<修正后的说明>" # -f 覆盖本地
git push origin -f v0.5.0 # 强推该 tag
```
这是**本项目唯一被接受的 force push 形态**——`git-workflow.md:32` 禁的是共享**分支**,tag 不在其列。但仍建议在发布记录里留一句「tag 说明于 <时间> 修正」。
**预防**:打 tag 前先核对指向(`git rev-parse origin/main` 与将要打 tag 的 sha 一致),并按 §4.1 第 7 步一次性核验三仓。
### 5.5 M4 特有风险:AI provider 凭证扫描规则**零覆盖**
**已核实**`scripts/check-secrets.sh:28-37` 的规则表共 **7 条**
```text
AK-AWS AKIA[0-9A-Z]{16}
AK-QCLOUD AKID[0-9A-Za-z]{16,}
AK-ALIYUN LTAI[0-9A-Za-z]{12,}
MINIO-DEFAULT minio[-_.]?admin
PRIVATE-KEY ^\s*-----BEGIN [A-Z ]*PRIVATE KEY-----\s*$
KEY-ASSIGN (access[-_]?key(_?id)?|secret[-_]?(access[-_]?)?key)["']?\s*[:=]\s*["']?[A-Za-z0-9+/=_-]{8,}
JWT-SECRET (jwt[-_.]?secret|signing[-_]?key|token[-_]?secret|hmac[-_]?(key|secret))["']?\s*[:=]\s*["']?[A-Za-z0-9+/=_-]{8,}
DB-PASSWORD (password|passwd|pwd)... (仅限 .ya?ml|properties|toml|conf|ini 文件)
```
**缺口分析**
- **无任何规则匹配 `sk-` 前缀族**OpenAI `sk-` / `sk-proj-`、Anthropic `sk-ant-`、DeepSeek / Moonshot / 阿里百炼 DashScope 均为 `sk-` 形态)。
- **`KEY-ASSIGN` 不覆盖 `api_key`**:其模式只认 `access_key` / `secret_key`**不含通用 `api[-_]?key`**。⇒ `ANTHROPIC_API_KEY=sk-ant-xxx`、`openai_api_key: sk-xxx`、`dashscope_api_key: sk-xxx` **全部漏网**。
- 文件名黑名单(`:24` `DENY_NAME`)覆盖 `.env` / `credentials*` / `*AccessKey*.csv` / `rootkey.csv`,对 AI provider 尚可(凭证通常进 `.env`),但内容层无兜底。
**这与 M3 的处境完全同构**iteration-3/08 §4.2 当年把「对象存储凭证进入任何开发机之前必须上线 CI 兜底」定为 M3 第一波必做。**M4 的 AI provider key 风险更高**——它是第三方控制台签发的长期凭证、直接绑计费、泄漏即可被外部调用刷额度,且 AI 辅助开发下 key 从配置流向「示例代码 / 测试 / 报告 / prompt 样例」的路径比对象存储更多。
**处置(拍板 D4,M4 第一波、先于任何 AI 凭证落地)**:向规则表增补,三仓**同批提交**(`git-workflow.md:38` 要求副本同步,当前三仓 md5 全等需保持):
```text
AK-LLM-SK - - \bsk-(ant-)?[A-Za-z0-9_-]{20,}
API-KEY-ASSIGN i - (api[-_]?key|apikey)["']?[[:space:]]*[:=][[:space:]]*["']?[A-Za-z0-9+/=_-]{16,}
```
注意两点:(1) 现有 `ALLOW` 白名单(`:18`)含 `example|sample|dummy|fake|redacted|placeholder|your[-_]…|\$\{…\}`,文档里写 `sk-ant-your-key-here` 或 `sk-xxx-example` 会被放行 ⇒ **文档与 E2E 脚本里的示例 key 一律用这些占位形态**。(2) 增补后必须跑 `sh scripts/check-secrets.sh --all` 三仓验证**无误报**(历史文档里可能有形似字符串),有误报则收紧模式而非放宽白名单。
**配套**:同时启用第一层 pre-commit(§1.8(2),三仓 `core.hooksPath` 全未设置)——AI 凭证场景下「拦在 commit 之前」比「push 后 CI 才红」价值大得多,因为后者意味着凭证已经在远端历史里了。
**若真泄漏**:第一动作是**去 AI provider 控制台吊销/轮换该 key**,之后才是清理历史(`git-workflow.md:54` 与 `check-secrets.sh:12` 双处明文)。M4 需在 `server-exposure.md` 增登记 AI provider 凭证条目。
## 6. 待拍板决策清单
共 **10 项**。标 ⭐ 的三项最关键(前两项建议在 M4 开工第一波就定,不要拖到发布前)。
| # | 事项 | 推荐 | 理由摘要 | 见 |
| --- | --- | --- | --- | --- |
| ⭐**D1** | **修正 main 的必需状态检查上下文**:`CI / backend-test (push)` → `CI / backend-test (pull_request)`flutter 同理 `flutter-gates`。**替换而非追加**。改完做一次空 PR 验证 | **采纳** | 当前门禁由「推 dev」满足而非 PR 自身,`(pull_request)` 红也能合;且非 dev 分支 → main 的 PR **永久不可合**,hotfix 路径是死的。上下文真名已从 CI 实跑记录取到,非猜测 | §1.4 |
| ⭐**D2** | **发布流程固化到 `git-workflow.md` 新增章节**「## 发布流程(dev → main)」;iteration-3/08 §3.3 标题加「已固化,仅存历史,勿照执行」标注;`releases.md:4` 改指新章节、`:131` 的「E2E 双份回归」改为「全份回归」并链接过去。E2E 份数**只在一处写死** | **采纳** | 现正典是一份「草案」,固化从未发生(`git-workflow.md` 65 行无发布内容),8 步里 4 步过期,且 `releases.md` 自我矛盾(`:36/:56` 四份 vs `:131` 双份)。照 `:131` 执行会漏跑 17 个场景 | §4.0 |
| ⭐**D4** | **`check-secrets.sh` 增补 AI provider 凭证规则**`sk-` / `sk-ant-` 前缀族 + 通用 `api_key` 赋值),三仓同批提交;**M4 第一波、先于任何 AI 凭证落地** | **采纳** | 现有 7 条规则对 `sk-` 零覆盖,`KEY-ASSIGN` 只认 `access_key`/`secret_key` 不认 `api_key` ⇒ `ANTHROPIC_API_KEY=sk-ant-…` 全漏网。风险高于 M3 的对象存储凭证(第三方签发、绑计费、泄漏即可外部刷额度)。沿 iteration-3/08 §4.2 先例 | §5.5 |
| D3 | **M4 若新增 api 模块,默认不复制契约框架**(`OpenApiContract.java` + 快照);新端点的契约矩阵挂既有模块 | **不复制** | 复制会使契约升版成本从 4 处变 5 处(+25%/次)。`/me` 守卫挂 auth 而实现在 user 已是先例。模块本身接 CI 是零成本(根 pom 加一行,`ci.yml` 零改) | §3.4 |
| D5 | **在 api CI 增加跨仓 md5 校验 step**(拉 doc 仓正典比对四份快照) | **本迭代不做,登记为技术债** | 缺口真实存在(api CI 完全不校验与 doc 正典的字节一致性,漂移会静默进库)。但实现要在 api CI 里 clone doc 仓,触发归属含糊(沿 iteration-3/08 §5.2 对 E2E 进 CI 的同类判断)。M4 先用「提交正文贴 md5」做可审计留痕,验证纪律有效性后再自动化 | §1.6 / §5.3 |
| D6 | **提交前缀规范收口**:`git-workflow.md:13` 增补 `style` 前缀与可选 `(scope)` 形态;**M4 起新提交统一用英文前缀**;历史不改写 | **采纳(改规范 + 改新实践,不改历史)** | 三方不一致:规范只认 6 个英文前缀;flutter 仓 **25/43 用中文前缀**(新增/修复/重构);api/doc 已在用规范未覆盖的 `test(contract):`/`docs(api):`。反向方案(规范承认中文前缀)会让 api 与 flutter 永久分裂 | §1.8(1) |
| D7 | **M1 的 E2E 脚本改名** `test_e2e_manual.dart` → `test_e2e_m1_manual.dart``git mv`,同步 4 处文档引用) | **采纳** | 回归清单变五份后,一个不带版本号的名字最易被误读成「总入口脚本」,进而漏跑。纯 rename,零逻辑风险。若嫌动文档,可推迟到 M4 收官波 | §1.7 |
| D8 | **doc 仓新建 `.gitignore`**(至少含 `site/` | **采纳,M4 第一波** | doc 仓**完全没有 `.gitignore`**`site/` 仅靠本机 `~/.gitignore_global:183` 屏蔽。换机器或新 clone 时一次 `git add -A` 即提交上百个构建产物。一行修复 | §1.8(3) |
| D9 | **三仓启用 pre-commit 第一层**:各执行一次 `git config core.hooksPath scripts/hooks` | **采纳,与 D4 同批** | 钩子脚本已入库但三仓 `core.hooksPath` **全部 unset** ⇒ 第一层完全未生效,当前只有 CI 兜底。AI 凭证场景下「拦在 commit 前」远优于「push 后 CI 才红」(后者凭证已进远端历史) | §1.8(2) |
| D10 | **doc 仓 main 不启用分支保护**(延续现状) | **不启用** | doc 是日常直推分支且无 dev,启用后每条文档改动都要建分支+PR,成本远超收益。关键风险(契约正典无门禁)用 D5 的 CI 校验兜,而非用分支保护兜 | §5.3 |
### 6.1 采纳后需落实的改动(本报告均未执行)
| 归属 | 动作 |
| --- | --- |
| **Gitea 平台**(需 admin | D1 改两仓必需上下文 + 一次性空 PR 验证;补做 §1.2 的 `branch_protections` 带 token 复核并把响应贴回本报告 |
| **patbond-doc** | D2 三处文档改动(`git-workflow.md` 新增章节、iteration-3/08 §3.3 加标注、`releases.md:4`/`:131` 修正);D6 规范增补;D8 新建 `.gitignore`;本报告挂入 `mkdocs.yml` 导航(**本次未动 mkdocs.yml**8 agent 并发中,由主会话统一处置) |
| **三仓同批** | D4 规则表增补 + `md5sum` 三仓校验 + `--all` 无误报验证;D9 各执行一次 `git config` |
| **patbond-flutter** | D7 `git mv` 改名 + 4 处文档引用同步 |
| **PM / RC** | §1.8(4) 的数字口径统一(api 测试 **381** 非 379、断言 **226** 非 234、真机挂起 **10** 项、埋点白名单 **41** 非 42),v0.5.0 发布记录一律用当次实跑值 |
### 6.2 本报告的取证边界(明确未证实项)
诚实登记,避免本报告本身成为「被转述采信」的下一个源头:
| 项 | 状态 | 缺什么 |
| --- | --- | --- |
| main「**禁直接推送**」的具体机制 | ⚠️ **未直接证实** | `GET /branch_protections` 返 401(本机无 token)。已证实的是 `protected=true` + v0.4.0 确实被迫走 PR。复核命令见 §1.2 |
| api 测试数 381 / flutter 597 | ⚠️ **本报告未运行测试** | 仅记录来源冲突(iteration-3.5/04 §7.2 = 381 vs `releases.md:17` = 379),未自行跑 `mvnw clean test` |
| E2E 断言数 226 | ⚠️ 未复核 | 采信 Evidence Collector 的机械计数,本报告未独立数 |
| 真机挂起 10 项 / 埋点白名单 41 | ⚠️ 未复核 | 非本角色职责域,采信 EC/RC 口径 |
| `(pull_request)` 作为必需上下文能否正常 gate | ⚠️ **未验证**(配置尚未改) | 必须按 D1 做一次性空 PR 验证。**不要拿 v0.5.0 正式发布当试验场** |
| M4 契约的路径/操作/schema 计数 | 未知 | M4 契约尚未定型,§3.1 的计数留占位 |
---
**已证实的核心结论一句话**:分支保护配置本身与描述**完全一致**,但**必需状态检查填的是 `(push)` 而非 `(pull_request)`**——门禁语义错位、hotfix 路径死锁;同时发布流程的「正典」是一份从未固化的草案且自我矛盾。这两条是 M4 开工前应优先修掉的。
---
> 本报告只写不提交(随波末统一入档);未修改 `mkdocs.yml`、`releases.md`、`git-workflow.md` 及任何 checklist —— 改动建议见 §6.1,由主会话统一处置。