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,550 @@
# M4 开工基线证据审计(07 号报告)
> 任务:M4「AI 创作」开工前对全部声称基线**逐条亲自取证**。每条结论附实际命令输出或 `文件:行号`。
> 执行人:Evidence Collector 取证日期:**2026-09-14**(工作机本地检出,三仓均停在 `v0.4.0`)
> 纪律:只读取证 + 只写本报告。未修改任何生产代码、`mkdocs.yml`、服务器配置;未执行 `git commit` / `push`。
> **结论先行**:10 条基线中 **6 条完全对上**、**3 条对不上**、**1 条无法静态复核**。
> 三个专项均发现实质问题:真机挂起项**实际登记 10 项而非 8 项**(差的 2 项是 M3.5 的
> caregiver 改宠物头像与获赞数对账,登记在常设清单里但未进功能清单跟踪);
> 发布 checklist **仍写着「M2+M3 两份」E2E,从未改成四份**;文档站待评估项只以
> 表格单元格内的一句 `⚠️` 存在,无归属人、无时限、无 ADR。
> 埋点白名单实为 **41 条**(非 42),且**没有任何测试锁住这个数**。
> api 测试实为 **381**(非 379)——`v0.4.0` 发布门禁表里的数字是旧的。
---
## 0. 取证环境事实
| 项 | 值 | 证据 |
| --- | --- | --- |
| 取证时刻 | `2026-09-14 14:09:28 +0800` | `date '+%Y-%m-%d %H:%M:%S'` |
| 默认 JDK | `openjdk 26.0.2.1 (2026-08-18)` | `java -version` |
| 可用 JVM | `java-11-openjdk` / `java-17-openjdk` / `java-26-openjdk` | `ls /usr/lib/jvm/` |
| `JAVA_HOME` | **未设置** | `echo $JAVA_HOME``(unset)` |
| 后端容器 | **全部未运行** | `docker compose ps` 输出为空 |
> 说明:`git-workflow.md` 与 `device-verification.md` 都写 `JAVA_HOME=/usr/lib/jvm/java-17-openjdk`
> 该路径**确实存在**(`ls -d` 成功),故文档前置有效;但本次基线测试在**默认 JDK 26** 下跑,
> 仍 381/381 全绿(surefire 日志内 `using Java 26.0.2.1`)。两条通路都可用,非缺陷,仅记录口径差异。
>
> **后端容器未运行**是本次唯一的取证硬约束——它导致 E2E 断言数无法运行时复核(见基线 9)。
---
## 1. 基线逐条对账表
| # | 基线 | 声称值 | 实测值 | 证据(命令 / 文件:行号) | 裁决 |
| --- | --- | --- | --- | --- | --- |
| 1a | 三仓工作区干净 | 全部干净 | 全部干净 | `git -C <仓> status --porcelain` 三仓均**空输出** | ✅ 对上 |
| 1b | 三仓停在 `v0.4.0` | 是 | 是 | `git tag --points-at HEAD` 三仓均返回 `v0.4.0` | ✅ 对上 |
| 1c | api HEAD | `dev@3cd8005` | `dev@3cd8005` | `3cd80055779db2d52cf8cc1af425d06131f7e41d``HEAD -> dev, tag: v0.4.0, origin/main, origin/dev` | ✅ 对上 |
| 1d | flutter HEAD | `dev@fbcd734` | `dev@fbcd734` | `fbcd73468805e95b8055395654ca1014e0d6c13c`,同点位含 `origin/main` | ✅ 对上 |
| 1e | doc HEAD | `main@5cc6361` | `main@5cc6361` | `5cc63615345fc30cb342a336292d7decdffea98f` | ✅ 对上 |
| 2a | api 测试数 | **379** | **381** | `./mvnw -B test` exit 0surefire XML 聚合:auth 40 + common 3 + community 107 + pet 100 + user 131 = **381**failures=0 errors=0 skipped=0 | ❌ **对不上(+2** |
| 2b | flutter 测试数 | **597** | **597**+2 skipped | `flutter test``01:03 +597 ~2: All tests passed!` exit 0 | ⚠️ 数字对上,**但 2 项被跳过未披露** |
| 3a | 契约版本 | `v1.4.0` | `1.4.0` | `openapi.yaml:4``version: 1.4.0` | ✅ 对上 |
| 3b | 契约规模 | 32 路径 / 45 操作 / 75 schema | **32 / 45 / 75** | Python + PyYAML 机械计数(逐路径列出,见 §2.1) | ✅ 对上 |
| 3c | 四模块字节级快照锁 | 四模块一致 | **5 份全同**md5 `a7081fb84f1207eef579ab94025f5801` | `md5sum` 于 doc 正典 + auth/community/pet/user 四份 `openapi-v1.4.0.yaml` | ✅ 对上(与 04 号报告的 `a7081f…5801` 吻合) |
| 4 | 契约矩阵 181 格零漂移 | 181 格 | 表列合计 **24+83+66+8 = 181**4 测试类 40 个 `@Test` 全绿 | `iteration-3.5/04-contract-freeze-v140.md:96` 合计行;4 个 `*ContractConformanceTest.java` 全在 381 内 0 失败 | ⚠️ **口径自洽,但「181」不可机械计数**(见 §2.2 |
| 5a | Flyway V1~V5 | V1~V5 | 恰好 V1~V5**无 V6** | `find -name "V*.sql"` → 5 个源文件;测试日志 `Successfully applied 5 migrations ... now at version v5` | ✅ 对上 |
| 5b | `posts.generation_job_id` 裸列无 FK | 裸列、无外键 | **列存在且确无 FK** | `V5__community_baseline.sql:48``generation_job_id uuid,`(无 `REFERENCES`);`:47` 注释明写 `FK ... stripped (M4 补回)`;全文件 15 处 `REFERENCES` 无一涉及该列 | ✅ 对上 |
| 6 | 埋点白名单 42 条 | **42** | **41** | `EventDictionary.java` 单一 `WHITELIST` Map`Map.entry(` 计数 = **41**,去重事件名 = **41** | ❌ **对不上(−1** |
| 7 | ADR 001~022 齐全无缺号 | 001~022 | **22 条,001~022 连续无缺号** | `decisions.md` 标题计数 = 22`grep -o "ADR-[0-9]\{3\}" \| sort -u` 输出 001…022 连续 | ✅ 对上 |
| 8 | 部署六容器(含自托管 MinIO) | 6 | **6** | `docker-compose.yml` services = `postgres`(postgres:18)、`minio`(minio/minio:RELEASE.2025-04-22)、`user``auth``pet``community`;另有 2 个 volume`pgdata`/`minio-data`,非容器) | ✅ 对上(MinIO 自托管确认,ADR-016 |
| 9a | E2E 脚本 4 份 | 仓库根 4 份「脚本」 | 4 份,位于 **patbond-flutter 仓库根**,为 **`.dart``.sh`** | `patbond-flutter/test_e2e_{,m2_,m3_,m35_}manual.dart`;工作区根 `ls *.sh` → 无此文件 | ✅ 对上(措辞校正见 §2.3) |
| 9b | E2E 42 场景 | 42 | **42**7+11+14+10) | 各脚本自述与收尾断言:`test_e2e_m2_manual.dart:767` `$_passed/11``test_e2e_m3_manual.dart:1345` `$_passed/14``test_e2e_m35_manual.dart:1139` `$_passed/10`M1 脚本 `[N/7]` 分段 | ✅ 对上 |
| 9c | E2E 234 断言 | 234 | **静态调用点仅 207**M2 41 / M3 83 / M3.5 83+ M1 另一体例 | `grep -c "check("` 去定义行;脚本内**无断言计数器**(`_passed` 只数场景) | ❌ **未取证 / 不可静态复核**(见 §2.3 |
| 9d | v0.4.0 发布时零失败 | 零失败 | **本次未复跑**(后端六容器未运行) | `docker compose ps` 空 | ⚠️ **未取证**(沿用 `iteration-3.5/08` 记录,非本次实证) |
| 10a | mkdocs 可构建 | 可构建 | **exit 0,零 warning** | `mkdocs build --strict -d /tmp/mkdocs-audit-site``Documentation built in 1.66 seconds``MKDOCS_EXIT=0` | ✅ 对上 |
| 10b | 导航无死链 | 无死链 | **0 条** nav 指向缺失文件 | 脚本比对:nav 引用 102 个 `.md`,全部存在 | ✅ 对上 |
| 10c | 无孤立文档 | 无孤立 | **0 个孤立** | `docs/` 下 102 个 `.md`,nav 引用 102 个,差集为空 | ✅ 对上 |
---
## 2. 关键条目的取证细节
### 2.1 契约规模(基线 3b)——三个数字全部机械复核
用 PyYAML 解析后按 OpenAPI 方法名白名单统计操作数:
```
info.version = 1.4.0
paths = 32
operations = 45
schemas = 75
```
32 条路径的操作分布(逐条列出以便 M4 增量时对照):`/auth/{login,logout,refresh,register}` 各 1、
`/breeds` 1、`/care-reminders/{reminderId}` 1、`/comments/{commentId}` 1、`/events` 1、`/feed` 1、
`/health-events/{eventId}` 1、`/me` **2**`/me/{bookmarks,community-stats,posts}` 各 1、
`/media/uploads` 1、`/media/uploads/{assetId}/complete` 1、`/pets` **2**`/pets/{petId}` **2**
`/pets/{petId}/{care-reminders,health-events,vaccinations,weights}`**2**`/pets/{petId}/summary` 1、
`/posts` 1、`/posts/{postId}` **3**`/posts/{postId}/{bookmark,comments,like}`**2**
`/users/{userId}/follow` **2**`/users/{userId}/follow-stats` 1、`/vaccinations/{vaccinationId}` 1、
`/vaccine-catalog` 1。
### 2.2 契约矩阵 181 格(基线 4)——为什么标「不可机械计数」
`iteration-3.5/04-contract-freeze-v140.md:88-96` 的表格自身算术正确:
| 模块 | 测试类 | 操作 | 单元格 |
| --- | --- | --- | --- |
| patbond-auth | `AuthContractConformanceTest` | 7 | 24 |
| patbond-pet | `ContractConformanceTest` | 18 | 83 |
| patbond-community | `CommunityContractConformanceTest` | 18 | 66 |
| patbond-user | `MediaContractConformanceTest` | 2 | 8 |
| **合计** | 4 类 | **45** | **181** |
四个类都存在且全绿,`@Test` 方法数为 **10 / 12 / 13 / 5 = 40**
但「单元格」是**报告里人工定义的「操作 × 响应类」概念单位**,与 `@Test` 方法数不是一对一
(一个 `@Test` 内常覆盖多格,如 §3.1 里 `404` 一格含 `40400`/`40405` 两码)。
代码侧**没有任何断言锁住「181」这个总数**。
故本条裁决为:**「零漂移」已由 40 个绿测试实证;「181 格」只能信报告口径,无法独立机械复核。**
### 2.3 E2E 场景与断言(基线 9)——场景可证,断言不可
**场景数 42 = 已机械证实。** 每份脚本收尾都打印 `$_passed/N`,且 `_passed++` 出现次数与 N 相符
(如 `test_e2e_m35_manual.dart``_passed++` 恰 10 次,行 343/391/431/493/618/690/745/880/943/1136)。
**断言数 234 = 无法复核。** 三点证据:
1. **脚本里没有断言计数器**`_passed` 只在场景末自增,数的是场景不是断言
`test_e2e_m35_manual.dart:78` `int _passed = 0;`,无 `_asserts` 之类)。
报告 `iteration-3.5/08:18-24` 的「断言数」列(M1 7 / M2 44 / M3 88 / M3.5 95)是**人工清点值**
非程序输出。
2. **静态调用点少于声称值**
| 脚本 | 报告声称断言 | `check()` 静态调用点 | 差 |
| --- | --- | --- | --- |
| `test_e2e_manual.dart`M1 | 7 | **无 `check()` 体例**16 处 `✗` 失败守卫) | 体例不同 |
| `test_e2e_m2_manual.dart` | 44 | **41** | 3 |
| `test_e2e_m3_manual.dart` | 88 | **83** | 5 |
| `test_e2e_m35_manual.dart` | 95 | **83** | 12 |
差值**可由循环内断言解释**M3.5 的 710/717/899 行 `check()` 位于 `for` 循环内,运行时会执行多次),
但报告未说明「断言数」是运行时计数,读者无从判断。
3. **口径不统一**:M1 记 7(≈ 每场景 1 条),而 M2/M3/M3.5 按单条 `check()` 记。
把两种口径相加得到的 **234 不是一个同质量纲的数**
**结论**`42 场景`可直接引用;`234 断言`建议**停止作为门禁数字引用**,或改为脚本自打印
(在 `check()` 内加一个 `_asserts++` 并在收尾打印,一行改动即可让此数自证)。
后端六容器未运行,本次**未复跑**任何 E2E 脚本,「零失败」沿用旧记录、非本次实证。
### 2.4 埋点白名单 41 vs 42(基线 6)——差在哪里
唯一权威源是 `patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/analytics/EventDictionary.java`
(单一 `WHITELIST``Map.ofEntries(...)``isKnownEvent()` 直接查它,无第二个注册表)。
- `grep -c "Map.entry("`**41**
- 去重事件名 → **41**(逐条已列,此处略)
分域核算:auth 11 + `page_viewed` 1 + pet 3 + health_record 7 = **22**= v2 基线,与文档一致);
post 域 **8**(发布漏斗 5 + 草稿/删除 + 媒体三段中的 started/succeeded/failed)、feed **2**、互动 **8**
= community 增量 **18**;再加 `experiment_exposed` **1****22 + 18 + 1 = 41**
**文档口径错在把 community 增量记为 19。** 三处文档同错(转述扩散):
- `feature-checklist.md:218` → 「community 域 **19** + experiment_exposed」「EventDictionary 22→**42**」
- `iteration-3/29-m3-summary.md:20` → 「**42 事件**(+19 community 域 + experiment_exposed)」
- `iteration-3/27-wave3-closure.md:17,27``iteration-3/index.md:47` → 「22→**42**」
`EventDictionary.java` 的**类 Javadoc 自身是对的**——它写 `post domain (8...)``feed domain (2...)`
`interactions (8...)`,合计 18,另记 `experiment_exposed`。即:**代码注释 = 41,四处文档 = 42**。
**放大这个问题的根因**`EventDictionaryTest.java`155 行、13 个 `@Test`)逐事件断言 props 键集,
但**没有任何一条断言锁住白名单的总条数**(无 `hasSize` / `size()` 断言)。
所以「文档 42 / 实现 41」这类漂移不会被任何门禁拦住。
---
## 3. 专项 A:真机验证挂起项逐项清点
**声称**:共 8 项(M2 两项 + M3 四项 + M3.5 两项)。
**实测**`docs/development/device-verification.md` 实际登记 **10 项**
| 迭代 | # | 项目 | 步骤完整性 | 前置齐备 | 执行记录 |
| --- | --- | --- | --- | --- | --- |
| **M2** | 1 | 验证一:Android 事件真实落库(~10 min) | ✅ 4 步 + 3 条通过标准 + psql 命令 | ✅ | ⬜ 待填 |
| **M2** | 2 | 验证二:SessionTracker 30 分钟后台换会话(~45 min) | ✅ 4 步 + 2 条通过标准 + 巡检 SQL 兜底 | ✅(接验证一同一登录) | ⬜ 待填 |
| **M3** | 1 | 媒体上传弱网表现 | ✅ (a)~(e) 五档 | ✅ 明写 `PATBOND_MINIO_PUBLIC_ENDPOINT` 必配 | ⬜ 待补 |
| **M3** | 2 | 乐观更新真机手感 | ✅ (a)~(e) | ✅ 含造数指引 | ⬜ 待补 |
| **M3** | 3 | Feed 图片加载 | ✅ (a)~(e) | ✅ 含「先发 ≥26 帖」造数 | ⬜ 待补 |
| **M3** | 4 | 社区事件落库 | ✅ 步骤 + (a)~(f) 六条标准 + psql | ✅ | ⬜ 待补 |
| **M3.5** | 1 | 头像上传弱网表现 | ✅ (a)~(f) 六档 | ✅ | ⬜ 待补 |
| **M3.5** | 2 | 头像缓存表现 | ✅ (a)~(d) | ✅ | ⬜ 待补 |
| **M3.5** | 3 | **caregiver 账号改宠物头像** | ✅ (a)~(c) | ❌ **前置不可执行**(见下) | ⬜ 待补 |
| **M3.5** | 4 | **获赞数与帖子点赞数对账** | ✅ (a)~(d) | ⚠️ **无「前置」小节**1/2/3 都有) | ⬜ 待补 |
### A.1 「8 项」是怎么来的——两份文档对不上
「8」出自 **功能完成清单**,不是真机清单本身:
- `feature-checklist.md:221` → 「Android 真机验证(**M2 两项 + M3 四项**)」= 6
- `feature-checklist.md:244` → 「真机项(**头像上传弱网、头像缓存**)」= 2
- 合计 8。`releases.md` 的 v0.4.0「已知遗留」同样只写「M3.5 新增(头像上传弱网、头像缓存)」。
`device-verification.md` 的 M3.5 节开篇明写「下列**四项**是桌面替代不了的部分」,
并编号登记了 4 项。
> **⚠️ 实质风险**M3.5 的第 3 项(caregiver 改宠物头像)与第 4 项(获赞数对账)
> **只存在于常设真机清单,没有进入功能完成清单的跟踪行**。
> 功能清单是收官时的销账依据——这两项当前**处于「已登记但无人跟踪」状态**,
> 按现有流程走完 M4 收官也不会有人发现漏了它们。
### A.2 M3.5 第 3 项前置不可执行(唯一的硬阻塞)
原文:
> **前置**:两个账号 Aowner/ Bcaregiver),B 对 A 的宠物有 caregiver 角色
> (关系授予入口尚未开放,按 `pet_health` 的协作表直接造数据,**收口时补 SQL**)。
「收口时补 SQL」**从未补**——文件里没有任何造数 SQL。执行人拿到这一项无法开工。
**取证:所需信息其实都已就位,写出这段 SQL 没有障碍**
- 表与角色枚举存在:`V3__pet_health_baseline.sql:97` `CREATE TABLE pet_health.pet_owners (`
`:104` `CONSTRAINT ck_pet_owners_role CHECK (role IN ('owner', 'caregiver', 'viewer'))`
- 后端授予路径已被测试覆盖:`PetAvatarIntegrationTest.java:214`
`void caregiverMayChangeTheAvatarButNotTheProfile()``:218` `grantRole(petId, caregiver, "caregiver")`
**建议**:把 `grantRole` 的等价 `INSERT INTO pet_health.pet_owners (...)` 写进该项前置,此项即可执行。
### A.3 有没有「其实已做过但没销账」的项?
**没有任何一项已完整完成**——四处执行记录(M2 节、M3 节、M3.5 节)全部为「待填 / 待补」。
但**有两项的后端部分已被证实,真机清单里没有反映**:
1. **M3.5 第 4 项(获赞数对账)的后端口径已证**
`iteration-3.5/08-release-e2e-regression.md:302`
「其中『获赞数对账』的**后端口径**已在场景 10 证明(详情 `likeCount` == `receivedLikeCount`),
真机侧待验的只剩 UI 呈现。」
→ 该项 4 个勾选框在 `device-verification.md` 里仍全空,未标注「(a)(b) 后端已证、真机只验 UI」。
2. **M3.5 第 3 项(caregiver)的后端行为已被单测覆盖**
`iteration-3.5/08` §未覆盖范围 → 「后端已有 `PetAvatarIntegrationTest` 三段断言覆盖」,
本次已直接在源码核实(见 A.2)。
→ 真机清单同样未标注,真机侧实际只需验 **UI 角标可见性分档**WRITE vs MANAGE)。
**判断**:不是「做完没销账」,而是**「已缩小的范围没有回写」**——真机项的实际剩余工作量
比清单表面看起来小,但清单不写,下一个执行人会重复评估。
### A.4 M2 两项的时限已进入最后一周
`device-writer.md` 内的时限提醒(引自 `iteration-3/06`):
> **时限提醒**:建议在 **2026-09-21(北极星首次出数日)前完成**,否则首批读数只能标「未验收」。
今天 **2026-09-14**,剩 **7 天**。两项合计耗时自述 ~55 分钟(10 min + 45 min,其中 40 min 是等待),
**唯一前置是一台 Android 真机或模拟器**。步骤、psql、通过标准全部齐备,可立即执行。
> **注**:M2 验证二的巡检 SQL 兜底(`count(DISTINCT session_id)/count(*)` 应远小于 0.9
> 是防「每事件一个 sessionId」缺陷复发的,与北极星读数直接相关——这也是时限挂在 09-21 的原因。
### A.5 其它文档债(不阻塞,记录备查)
1. **通用前置的 `flutter run` 只传 3 个 base URL**,靠一句注解补 community
「M3 起若新增服务端口(如 community :8084),相应补 `PATBOND_COMMUNITY_API_BASE_URL`」。
结果是 M3/M3.5 的每一项都要重复叮嘱「四个 base URL 全传」。建议把通用前置的命令块直接补全到四个。
2. **勾选框体例不一致**M3 的第 1/2/3 项用 `- a` 普通列表,M3 第 4 项与 M3.5 全部用 `- [ ]` 勾选框。
真机执行时无法在前三项上打勾销账。
---
## 4. 专项 B:发布流程文档一致性
### B.1 「发布后生效的纪律」现状——存在、基本可执行,但有一处已过期
该节位于 `docs/development/releases.md`**v0.3.0** 小节末尾(不在 v0.4.0 小节)。内容核实:
| 条目 | 内容 | 裁决 |
| --- | --- | --- |
| `main` 禁直推 | 一切影响 main 的变更走 PR + CI 状态检查 | ✅ 已生效并已被 v0.4.0 实证 |
| 五步 PR 流程 | ①完成 checklist 1~2 步 → ②建 `dev → main` PR → ③等状态检查绿 → ④Gitea 合并(应为快进)→ ⑤续 checklist 5、7 步 | ⚠️ **第 ① 步文字过期**(见下) |
| checklist 第 4 步作废 | 本地 `merge --ff-only && push` 仅适用首次发布 | ✅ 已明确记录 |
| checklist 第 3、6 步一次性 | 不再重复 | ✅ 已明确记录 |
| 分叉即排查 | PR 显示无法快进 → 先查明原因,不用合并提交掩盖 | ✅ 清晰 |
**过期点**:五步流程第 ① 步写「完成 checklist 第 1~2 步(三仓 CI 绿 + **E2E 双份回归 PASS**)」。
而同一文件 v0.4.0 小节的「checklist 变更(下次发布适用)」已宣布
「**回归清单由两份改为四份**M1/M2/M3/M3.5」。
**同一份 `releases.md` 内自相矛盾**v0.3.0 节的纪律说双份,v0.4.0 节说四份。
### B.2 checklist 里**没有**写成四份(核心发现)
用户问「核实 checklist 里是否真的写成了四份」——**答案:没有,仍写着两份。**
`releases.md` 顶部「维护约定」指向的权威 checklist 是
`iterations/iteration-3/08-git-workflow-plan.md` §3.3。原文第 2 步(**该文件 84 行**):
> 2. **实测**compose 全栈起,跑 **M2+M3 两份** E2E 烟囱脚本,全场景 PASS,证据入波次报告;
**这份 checklist 自 M3 起从未被修改。** 八步中有 **4 步已与现实脱节**
| 步 | 原文要点 | 现状 | 修正记录在哪 |
| --- | --- | --- | --- |
| 2 | 跑 **M2+M3 两份** E2E | 应为 **四份**M1/M2/M3/M3.5 | 只在 `releases.md` v0.4.0「checklist 变更」段 |
| 3 | 命名统一 + api main 重建 | **一次性项,已完成** | 只在 `releases.md` v0.3.0 纪律段 |
| 4 | 本地 `checkout main && merge --ff-only && push` | **已失效**main 受保护会被拒) | 只在 `releases.md` v0.3.0 纪律段 |
| 6 | Gitea 开启分支保护 | **一次性项,已完成** | 只在 `releases.md` v0.3.0 纪律段 |
**即:checklist 本体是 M3 时代的原稿,全部修正只散落在发布记录页的散文里。**
下次发布若有人照 §3.3 逐步执行,会跑错 E2E 份数、并撞上一条已被分支保护拒绝的 git 命令。
### B.3 checklist 至今仍是「草案」,且从未固化进常设文档
`iteration-3/08-git-workflow-plan.md` §3.3 的标题原文:
> ### 3.3 发布 checklist 草案(**首次发布用,验证后固化进 `git-workflow.md`**
这个固化动作**从未发生**。核实 `docs/development/git-workflow.md`64 行,常设文档):
```
grep -n "checklist\|发布\|E2E\|四份\|两份\|PR" docs/development/git-workflow.md
→ (无任何输出)
```
**该常设文档里完全没有发布流程、没有 checklist、没有 PR 纪律、没有「main 受保护」的说法。**
它的「禁止事项」只说「不 force push 共享分支(`dev` / `main`)」,仍是保护启用前的口径。
> **⚠️ 实质风险**:唯一的权威发布 checklist 是一份**标着「草案」的 iteration-3 迭代报告**
> 内容已 4 步过期;而常设的 `git-workflow.md` 对发布流程**完全沉默**。
> 两次发布的经验只以散文形式沉在 `releases.md` 的两个版本小节里。
### B.4 附带核实:doc 仓 main 未受保护
`releases.md` v0.3.0 的分支保护表(经 Gitea API 核实的记录):
| 仓库 | protected | 状态检查上下文 |
| --- | --- | --- |
| patbond-api | `true` | `CI / backend-test (push)` |
| patbond-flutter | `true` | `CI / flutter-gates (push)` |
| **patbond-doc** | **未启用** | —— |
即「main 受保护禁直推」**只适用 api 与 flutter 两仓**doc 仓 `main` 仍是直推流
`git-workflow.md` 亦写「patbond-doc:直接提交 `main`」)。本报告即直接写在 doc 仓 main 工作区。
这是**按规划的有意安排**(doc main 即日常分支、不承担发布分支语义),非缺陷,仅澄清口径
——避免「下次发布必须走 PR」被误读为三仓皆然。
---
## 5. 专项 C:文档站公开可访问 / 访问控制待评估项
> 纪律声明:本节**纯取证 + 建议**。未连接服务器、未读取或修改任何 nginx / 防火墙配置。
> 下述服务器侧事实均引自仓库内的 `server-exposure.md` 登记(该文件本身即为核对产物),
> 并明确标注哪些是「文档记载」而非「本次实测」。
### C.1 待评估项的登记现状——只有一句话,在表格单元格里
文件确认存在:`docs/development/server-exposure.md`(常设文档,2026-09-11 因安全事件新建)。
待评估项的**全部原文**,位于 §2「常驻服务」表格的「状态」列单元格内:
> **文档站(patbond-doc** 经 nginx 443 mkdocs 构建产物,含架构/部署/迭代全部文档 | —— |
> ✅ 运行;⚠️ **公开可访问,待评估是否加 basic auth 或 IP 白名单**(无凭证内容,但暴露内部架构细节)
`releases.md` v0.4.0 末尾有一行呼应:
> **待评估**:文档站公开可访问是否加访问控制(见服务器暴露面清单)。
**登记质量问题**(三项俱缺):
1. **无归属决策**:该行的「归属决策」列是 `——`(空)。同表其它服务都指向 ADR 或 CI 手册
gitea → CI Runner 手册、dockerd → ADR-006)。文档站**没有任何 ADR 覆盖它的暴露决策**。
2. **无时限、无归属人**:不像真机 M2 两项挂着明确的 `2026-09-21`
3. **不在处置记录里**:§5「处置与核对记录」只有 2026-09-11 一行(安全事件处置),
待评估项**未登记为待办事项**,只以表格内 `⚠️` 存在——检索性差,收官核对时极易滑过。
### C.2 当前暴露面(据 `server-exposure.md` 登记,2026-09-11 核对)
对公网开放端口 **4 项**22SSH)、80(跳 443)、443HTTPS)、ICMP。
其中 443 由 nginx 按域名分流,`sites-enabled/` 内两个站点:`git.patbond.cn``patbond-doc`
即:**文档站与 Gitea 共用 443,靠域名分流,无任何认证层。**
已于 2026-09-11 关闭并记录防重开:3000(Gitea 直连,本次事件入口)、8848/9848NacosADR-002 已移除)、
2222(无服务监听的空规则)。
**「最后确认」列全部为 `2026-09-11`。** 而 `server-exposure.md` 自身的维护约定 ② 要求
「**每次迭代收官核对一遍**,更新『最后确认』」——M3.5 于 09-11 收官、`v0.4.0`**09-14** 发布,
发布当日未再核对。属轻微逾期(3 天),M4 开工正是补这次核对的时机。
### C.3 暴露内容评估(本次实测部分)
我可以实测的是**文档站会发布什么内容**(构建产物侧),这部分不需要碰服务器:
- 构建产物含 **102 个页面**`mkdocs build --strict` 全量成功,nav 102 项零孤立)。
- 内容面包括:完整 OpenAPI 契约(`docs/api/openapi.yaml`32 路径全部端点与错误码)、
数据库全量 DDL`docs/database/patbond_postgresql.sql`)、22 条 ADR、
**`server-exposure.md` 本身(服务器端口与常驻服务清单)**、
`ci-runner-setup.md`(CI runner 部署细节)、以及含安全事件复盘的
`iteration-3.5/07-security-incident-20260911.md`
- 凭证面:三仓 `check-secrets.sh --all` 在 v0.4.0 门禁 exit 0(引自 `releases.md`,本次未复跑)。
故「无凭证内容」的判断有依据。
> **⚠️ 值得指出的悖论**:因安全事件而建立的 `server-exposure.md`——那份逐条列出
> 「哪些端口开着、哪些服务在跑、内部 API 走哪条路径、凭证轮换触发条件」的清单——
> **本身正随文档站公开发布**。同理还有 `ci-runner-setup.md` 与安全事件复盘全文
> (后者详述了注入路径与处置手法)。
> 这不是凭证泄漏,但**恰好是攻击者做侦察最想读的三份文档**,且描述的是刚被攻击过的这台主机。
> 这一点在待评估项的括注「无凭证内容,但暴露内部架构细节」里被显著低估了。
### C.4 建议(不改配置,仅供拍板)
优先级排序:
1. **先把待评估项从表格单元格提成一条正式待办**(§5 处置记录里加一行,或建 M4 工单),
补齐归属人 + 时限。当前形态检索不到,等于没登记。
2. **推荐方案:IP 白名单 优先于 basic auth。** 理由:读者集合当前就是维护者本人;
nginx 侧 `allow/deny` 比 basic auth 少一套凭证要管(而按纪律 6,新增凭证还要走「不回显」流程)。
若将来需给外部评审看,再叠加 basic auth。
3. **若维持公开,则做内容分层**:把 `server-exposure.md``ci-runner-setup.md`
安全事件复盘三份移出公开构建(mkdocs `exclude_docs` 或独立私有站),
契约与 ADR 保持公开。**这三份的敏感度与其余 99 份不在一个档位。**
4. **无论选哪条,补一条 ADR**,让文档站的暴露决策像其它服务一样有归属决策可查
——这正是 `server-exposure.md` 缘起段所说「代码侧有 ADR 防『决策变了实现没跟上』,
服务器侧此前无任何对应机制」要补的那一环。
5. 顺手更新 §1/§2 的「最后确认」到本次核对日期。
---
## 6. 所有对不上的差异(按严重度排序)
| # | 严重度 | 差异 | 声称 | 实测 | 证据 |
| --- | --- | --- | --- | --- | --- |
| D1 | **高** | 发布 checklist 未更新为四份 E2E,且 4/8 步已脱节 | 「已由两份改为四份」 | checklist 本体仍写「M2+M3 两份」 | `iteration-3/08-git-workflow-plan.md:84` |
| D2 | **高** | 真机挂起项数量 | 8 项 | **10 项**M2 2 + M3 4 + **M3.5 4** | `device-verification.md` M3.5 节「下列四项」+ 4 条编号项;对比 `feature-checklist.md:221,244` |
| D3 | **中** | 埋点白名单条数 | 42 条 | **41 条** | `EventDictionary.java` `Map.entry(` = 41;错误口径见 `feature-checklist.md:218``iteration-3/29:20``iteration-3/27:17,27``iteration-3/index.md:47` |
| D4 | **中** | api 测试基线 | 379 | **381** | surefire 聚合 40+3+107+100+131`iteration-3.5/04` 自身已写「379→381」,但 `releases.md` v0.4.0 门禁表与 tag 表仍写 379 |
| D5 | **中** | `releases.md` 内部自相矛盾 | —— | v0.3.0 纪律段写「E2E **双份**回归」,v0.4.0 段写「改为**四份**」 | `releases.md` 两节并存 |
| D6 | **低** | flutter「597 全绿」未披露跳过项 | 597 双绿 | 597 passed **+ 2 skipped** | `flutter test``+597 ~2`;跳过项为 `test/smoke/media_upload_smoke_test.dart:133``test/smoke/detail_interactions_smoke_test.dart:173`(需 compose 后端 + 环境变量) |
| D7 | **低** | E2E 脚本形态措辞 | 「仓库根 4 份脚本」 | 在 **patbond-flutter** 仓库根,为 `.dart``dart run`)非 `.sh`;工作区根无 `*.sh` | `ls /home/lx/workspace/patbond/*.sh` → 无此文件 |
---
## 7. 所有未取证项(诚实清单)
| # | 项 | 为何未取证 | 补证方式 |
| --- | --- | --- | --- |
| U1 | **E2E 234 断言** | 脚本无断言计数器,静态调用点 207 与声称值不符;差值疑为循环内执行但报告未说明口径 | 在 `check()` 内加 `_asserts++` 并于收尾打印;或明确标注为运行时计数 |
| U2 | **E2E「v0.4.0 零失败」** | 后端六容器未运行(`docker compose ps` 空),本次未复跑四份脚本 | `docker compose up -d` 后串行跑四份(须串行,M3 场景 10 与 M3.5 场景 10 的全量翻页比对会被并发发帖污染) |
| U3 | **契约矩阵「181 格」** | 「单元格」是报告人工定义单位,代码侧无断言锁总数(4 类共 40 个 `@Test`,非一对一) | 若要可复核,在契约测试里加一条总数断言;否则明确标注为文档口径 |
| U4 | **CI 三仓绿** | 未查 Gitea commit status API(本次为本地取证,且不宜在 8 agent 并发时打服务器) | `GET /repos/{owner}/{repo}/commits/{sha}/status` 三仓各一次 |
| U5 | **服务器侧实际暴露面** | 按任务纪律不碰服务器配置;C.2 全部引自 `server-exposure.md` 登记(2026-09-11),非本次实测 | 按该文件 §3「核对方法」在服务器上执行 `ss -tlnp` 等四条命令 |
| U6 | **`check-secrets.sh --all` 三仓 exit 0** | 未复跑(v0.4.0 门禁记录为绿,本次未验) | 三仓各 `sh scripts/check-secrets.sh --all` |
| U7 | **零迁移在存量库上实证** | 需 compose 起于既有 `pgdata` volume;容器未运行 | 与 U2 同一轮 compose 启动时看 Flyway 日志 |
---
## 8. 找到的真实问题清单(按严重度排序)
### P1(高)发布 checklist 是 M3 时代原稿,4/8 步已脱节,且从未固化进常设文档
- **证据**`iteration-3/08-git-workflow-plan.md:84` 仍写「M2+M3 **两份** E2E」;
§3.3 标题仍标「**草案**(首次发布用,验证后固化进 `git-workflow.md`)」;
`grep "checklist\|发布\|E2E\|PR" docs/development/git-workflow.md`**零输出**
- **影响**:下次发布照单执行会 ①漏跑 M1/M3.5 两份回归、②撞上第 4 步已被分支保护拒绝的
`push origin main`。修正只以散文存在于 `releases.md` 两个版本小节。
- **建议**:把 checklist 固化进 `git-workflow.md` 并就地改正 4 步(2 改四份、3/6 标一次性已完成、
4 换 PR 流程),iteration-3/08 §3.3 加一行「已被 `git-workflow.md` 取代」。M4 开工前即可完成。
### P2(高)真机挂起项漏跟踪 2 项,且其中 1 项前置不可执行
- **证据**`device-verification.md` 登记 **10** 项,`feature-checklist.md:221,244` 只跟踪 **8**
——M3.5 的 caregiver 改宠物头像、获赞数对账**未进功能清单**。
且 caregiver 项前置写「**收口时补 SQL**」,SQL 从未补(造数所需的
`pet_health.pet_owners` 表与 `caregiver` 角色枚举早已存在:`V3:97,104`
授予写法可照抄 `PetAvatarIntegrationTest.java:218``grantRole`)。
- **影响**:两项在现行销账流程外,M4 收官不会被发现;caregiver 项即便有人接手也无法开工。
- **建议**:功能清单补齐 4 项;把 `INSERT INTO pet_health.pet_owners` 造数 SQL 写进前置。
### P3(中)埋点白名单实为 41 条,四处文档写 42,且无测试锁总数
- **证据**`EventDictionary.java` `Map.entry(` = **41**(类 Javadoc 自身的分域说明 8+2+8 也等于 18,
与 41 自洽);文档四处写 42(`feature-checklist.md:218``iteration-3/29:20`
`iteration-3/27:17,27``iteration-3/index.md:47`),错在把 community 增量记为 19(实为 18)。
`EventDictionaryTest.java`13 个 `@Test`**无任何总数断言**。
- **影响**:M4 要新增 AI 创作域事件,增量必然基于错误基数计算
(「42 + N」会与实现的「41 + N」持续差 1)。这正是 M3.5 教训里「转述即污染」的同型问题。
- **建议**:**M4 开工前先修基数**——四处文档改 41,并在 `EventDictionaryTest` 加一条
`assertThat(WHITELIST).hasSize(41)` 级别的断言(需暴露 size 或用
`isKnownEvent` 遍历),让此数此后自证。
### P4(中)`v0.4.0` 发布门禁表引用的 api 测试数(379)低于实际(381)
- **证据**surefire 聚合 **381**、0 失败(在 tag `v0.4.0` = `3cd8005` 工作区实测);
`releases.md` v0.4.0 的 tag 表与门禁表均写 379;
`iteration-3.5/04-contract-freeze-v140.md:8` 自己写的是「379→**381**」。
- **影响**:门禁证据数字与被 tag 的代码不符。数字虽小,但发布记录是**跨迭代对账基准**
——M4 收官时「381 → N」的增量核算会从错的起点算。
- **建议**`releases.md` 两处 379 改 381(历史记录订正加脚注说明,不静默改)。
### P5(中)4 份 `integration_test/` 活体测试完全在自动化门禁之外
- **证据**`flutter test` 的 597 项**不含** `integration_test/`(日志内 `integration_test/` 出现 **0** 次);
该目录下 4 个文件(`profile_avatar_live_test.dart``feed_live_test.dart`
`client_ux_live_test.dart``publish_live_test.dart`)均 `skip: !enabled` 且需真后端。
- **影响**`iteration-3.5/05` 号报告用 `integration_test/profile_avatar_live_test.dart`
作为「桌面全链路已通」的证据,而这份证据**不被任何门禁重跑**,回归时不会报警。
另有 2 个 `test/smoke/*` 在 597 里被静默跳过(D6)。
- **建议**:要么在发布 checklist 里把这几份列为手动必跑项(与 E2E 同级),
要么明确标注为「一次性取证、非回归资产」,避免后续报告继续把它当活证据引用。
### P6(中)doc 仓无 `.gitignore``site/` 不入库的纪律仅靠开发者机器的全局配置兜着
- **证据**`patbond-doc/` 根**无 `.gitignore`**`cat .gitignore` → 无此文件);
`git check-ignore -v site/``/home/lx/.gitignore_global:183:/site`
——即 `site/` 只被**用户级全局 gitignore** 忽略。
`git-workflow.md` 的门禁表明确要求「不把 `site/` 落进仓库」。
取证期间 `site/` 目录 mtime 为 `14:10`(本次会话期间被并发 agent 重建,本报告未触碰它)。
- **影响**:换一台机器、或 CI 里执行 `mkdocs build`(默认输出 `site/`)后跑 `git add -A`
102 个页面的构建产物会直接入库。纪律写在文档里,仓库侧零强制。
- **建议**`patbond-doc/` 加一份最小 `.gitignore`(至少 `site/`)。一行改动消除整类风险。
(本次未代改——按纪律只写本报告。)
### P7(低)安全事件催生的三份服务器文档,正随公开文档站发布
- **证据**`server-exposure.md` §2 登记文档站「公开可访问」且「归属决策」列为空;
该文件本身、`ci-runner-setup.md``iteration-3.5/07-security-incident-20260911.md`
均在 `mkdocs.yml` nav 的 102 项之内(零孤立,即全部发布)。
- **影响**:端口清单 / 常驻服务 / 内部 API 路径 / 凭证轮换触发条件 / 刚发生的注入路径与处置手法,
对这台刚被攻击过的主机构成现成侦察材料。待评估项的括注
「无凭证内容,但暴露内部架构细节」低估了这一点。
- **建议**:见 §5.C.4——优先把待评估项提成正式待办(当前只是表格单元格里的一句 `⚠️`,检索不到),
倾向 IP 白名单;若维持公开则把这三份移出公开构建。**不要在 M4 里继续挂着不决。**
---
## 9. 给 M4 开工的直接结论
**可以放心引用的基线**(本次机械复核通过):
- 三仓 `v0.4.0` 干净同点位:api `3cd8005` / flutter `fbcd734` / doc `5cc6361`
- flutter **597** 测试(记得注明另有 2 项跳过)
- 契约 **v1.4.0****32 路径 / 45 操作 / 75 schema**,五份副本字节级同一(md5 `a7081fb8…5801`
- Flyway **V1~V5**`community.posts.generation_job_id` **裸列已就位、确无 FK**
`V5:48`)——**M4 补 FK 时新增 `V6`,不得改 V5**`git-workflow.md` 禁改已推送迁移)
- ADR **001~022** 连续无缺号 → M4 首个新决策为 **ADR-023**
- 部署 **六容器**postgres:18 + MinIO + auth/user/pet/community
- E2E **42 场景**M1 7 / M2 11 / M3 14 / M3.5 10
- 文档站:`mkdocs build --strict` exit 0 零 warning102 页 nav 全覆盖、零孤立、零死链
**开工前建议先修的三个基数/流程问题**(都是小改动,但会污染 M4 全程估算):
1. **埋点白名单 41 而非 42**(P3)——M4 必然新增 AI 创作域事件,基数错则增量全错
2. **发布 checklist 改四份 E2E 并固化进 `git-workflow.md`**P1)——M4 收官要发版
3. **api 测试基线 381 而非 379**P4)——M4 增量核算的起点
**时限压力**:真机 M2 两项挂着 **2026-09-21**(北极星首次出数),今天 **09-14**,剩 7 天;
两项合计 ~55 分钟且步骤齐备,**唯一缺一台 Android 设备**。逾期则首批北极星读数只能标「未验收」。
**本报告未做的事**:未 commit / push;未改 `mkdocs.yml`
(本文件需由 `mkdocs.yml` 负责人挂进 iteration-4 导航节,否则不进文档站);
未改任何生产代码、未碰服务器配置。
---
**取证人**Evidence Collector
**取证日期**2026-09-14
**基线点位**api `3cd8005` / flutter `fbcd734` / doc `5cc6361`(三仓均 `v0.4.0`
**裁决汇总**:基线 10 条 → 对上 6 / 对不上 3 / 不可静态复核 1;专项 A/B/C 各有实质发现;
真实问题 **7 个**(高 2 / 中 4 / 低 1);未取证项 **7 个**