Files
patbond-doc/docs/development/iterations/iteration-4/07-evidence-baseline-audit.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

551 lines
39 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 开工基线证据审计(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 个**