Files
patbond-doc/docs/development/iterations/iteration-4/08-git-workflow-plan.md
T
lixi 79b33dba31 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>
2026-09-14 15:48:56 +08:00

807 lines
64 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.
# 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,由主会话统一处置。