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,206 @@
# M4「AI 创作」开工汇总
**日期**2026-09-14 **状态**:开工分析完成,待拍板 **上一迭代**M3.5 收官、v0.4.0 已发布
---
## 0. 本页的使用纪律
本页是 8 份开工报告(合计 7448 行)的**转述层**,存在的目的是让拍板不必翻全文。
但本批分析最大的发现恰恰是**「转述即污染」**:十余处文档与注释所写与代码实际所做相反,且多数已被下游报告采信,直接造成过规模误判(见 §6)。因此:
- 本页每条结论都标注**来源报告编号**(如 `[02]`)。要动手实现或据此拍板时,**回读该报告的取证行号,不要止步于本页**。
- 本页**不新增任何结论**。凡本页出现而来源报告中没有的判断,视为本页的错误。
- 各报告末尾均有「取证边界 / 未取证项」附录,汇总见 §7。**未取证项不得当事实使用**。
---
## 1. 报告索引
| # | 报告 | 行数 | 一句话 |
|---|---|---|---|
| 01 | [任务分解](01-pm-task-breakdown.md) | 742 | 28 工单 / 4 波 / 14 决策;推翻 8 处既有结论 |
| 02 | [后端技术评估](02-backend-technical-assessment.md) | 1114 | 队列方案、新模块、V6+V7、契约 +4/+5/+810 决策 |
| 03 | [Flutter 技术评估](03-flutter-technical-assessment.md) | 1670 | 自适应轮询、13 处 AI 模拟定位、测试 +30011 决策 |
| 04 | [现实核查](04-reality-check.md) | 687 | 总体 **NEEDS WORK**AI provider **BLOCKED** |
| 05 | [AI 创作 UI 规格](05-ai-create-ui-spec.md) | 1117 | 8 界面单元、等待态形态、新建 8 复用 16;18 决策 |
| 06 | [埋点与实验规划](06-experiment-tracking-plan.md) | 762 | 字典 41→49、eventVersion 定型、A/A 空跑;10 决策 |
| 07 | [基线证据审计](07-evidence-baseline-audit.md) | 550 | 基线逐条对账 6 对 3 错、真机项清点、发布流程一致性 |
| 08 | [Git 流程规划](08-git-workflow-plan.md) | 806 | 门禁漏洞、分支策略、契约升版顺序、v0.5.0 门禁;10 决策 |
决策总计约 **73 项**,本页只提炼需要跨角色协调或阻塞开工的部分(§3、§4);其余为各报告内的细节决策,随工单执行时就地拍板。
---
## 2. 基线实测(开工基准)
三仓工作区洁净、均停 `v0.4.0`HEADapi `dev@3cd8005` / flutter `dev@fbcd734` / doc `main@5cc6361``[07][04]`
| 项 | 文档声称 | 实测 | 裁决 |
|---|---|---|---|
| api 测试 | 379 | **381** | 文档错(成因待仲裁,见 §4-2)`[07][04]` |
| flutter 测试 | 597 | 597 通过 **+ 2 skipped** | 数字对,skip 从未披露 `[07]` |
| 埋点白名单 | 42 | **41** | 文档错,根因 `experiment_exposed` 重复计数 `[07][06]` |
| 真机验证挂起 | 8(或 4/6) | **10**M2 2 + M3 4 + M3.5 4 | 文档错,转述链每跳丢项 `[07][04]` |
| E2E 场景 | 42 | **42** | 对 `[07][04]` |
| E2E 断言 | 234 | 机械可数 **226** | 差异待仲裁,见 §4-6 `[04]` |
| 契约 v1.4.0 | 32/45/75 | 逐格相符 | 对 `[07][04]` |
| 四模块快照 | 字节一致 | md5 `a7081fb8…5801` 五处一致 | **副本一致为真,但无 CI 保证**(§6-2`[07][04]` |
| 契约矩阵 | 181 格 | 表格自洽,**不可机械复核** | 只存在于报告散文;代码唯一规模断言是 `operations()==45` `[04]` |
| Flyway | V1~V5 | V1~V5,无 V6`V5:48` `generation_job_id uuid,``REFERENCES` | 对 `[07][04]` |
| ADR | 001~022 | 22 条无缺号 | 对 `[07]` |
| 容器 | 六容器 | postgres/minio/auth/user/pet/community | 对;**无 Redis、无 MQ** `[07][02]` |
| 文档站 | 可构建无死链 | `mkdocs --strict` exit 0102 页零孤立 | 对 `[07]` |
| CI | 「仍红」(`iteration-3.5/04 §7.3` | 三仓五上下文 09-14 全 `success` | **过期快照,勿当风险继承** `[04][08]` |
| v0.4.0 发布时间 | 09-11 | **09-14 11:17** | 文档错 `[08]` |
---
## 3. 多方独立收敛的结论(证据已定,无需拍板)
| 结论 | 独立证实方 |
|---|---|
| **队列用 Postgres `FOR UPDATE SKIP LOCKED` + 租约列,不引入 Redis/MQ**。目标模型已备好六列租约字段与两条与出队/回收语句逐列对齐的部分索引(`patbond_postgresql.sql:605-716`,现成领取 SQL 在 `:1896-1917`);换中间件等于让这批列作废、推翻已评审模型。缺的只有租约超时回收器。 | `[01][02][04]` |
| **服务端零对象写能力**`ObjectStorage` 仅 4 方法,**零 `putObject`、零 `getObject`**。故「媒体链路可直接复用」只对读侧与客户端上传侧成立,Worker 落产物与喂输入均为净新增。**这是 M4 立足点缺失的一块地基。** | `[01][02][04]` |
| **「四模块字节级快照锁 CI」不存在**:CI 只有 `check-secrets.sh` + `mvnw test`,无任何 diff/校验和;实际门禁是结构断言(版本串 + 计数 + tag 操作集)。后果:只改 description 的契约漂移,四模块全都不会红。 | `[01][02][04][08]` |
| **定稿模型是图生图,不支持文生图**`input_asset_id uuid NOT NULL`。 | `[01][02][05]` |
| **「等真机设备」是伪阻塞**:本机 `Pixel_7` AVD 可启动、`/dev/kvm``crw-rw-rw-`、android-36 镜像在位,而 `device-verification.md:9` 明文接受「Android 真机(推荐)**或 Android 模拟器**」。 | `[04][06]` |
| **契约完全没有 429 / `Retry-After` / 任何响应头机制**`components.headers: []`,全契约 `headers:` 零命中);后端 429 亦零命中。M4 是首次引入。 | `[03][04][05]` |
| **跨 schema 外键的裁剪理由已不成立,补回是 V5 自己下的指令**`V5:15-19`/`V3:14-19` 原话为「FKs into schemas **not yet migrated** are STRIPPED」——理由是目标 schema 当时不存在;反证有 `V1:262-264``V5:45-46``V5:107` 三条在案的跨 schema 外键。 | `[02]` |
---
## 4. 必须开工前拍板
### 阻塞 V6 定稿与开工
| # | 决策 | 现状与推荐 |
|---|---|---|
| **A** | **AI provider 选型** | **BLOCKED**。零 provider SDK、零 endpoint、零额度;密钥面仅 4 项(DB/internal/MinIO×2);本机 AMD Vega 集显无 CUDA,自托管不可行。**最硬的反证:正典种子自己写的就是 `provider_code='fixture'`**,且 `generation_jobs` 三个快照列 `NOT NULL`,逼迫显式选边。推荐 **fixture provider 起步 + 可插拔适配层**(M4 四条验收标准全部与图好不好看无关,fixture 还能注入真实供应商难复现的故障)。**缺失的外部输入:生产服务器 GPU 情况、云 API 报价** —— 这是唯一真正卡住的外部依赖。`[01][02][04]` |
| **B** | **参考图必填(图生图)还是可选(文生图)** | 三方独立指出与工单描述「可选参考图」冲突;定稿模型 `input_asset_id NOT NULL`,正典原文亦为「上传一张照片…」。推荐**沿用 NOT NULL、图生图先行**。此项同时决定 V6 能否照抄目标模型、create 页整个交互、A2 的 gating 形态。`[01][02][05]` |
| **C** | **上游调用形态** | 推荐可插拔 `GenerationProvider` **两阶段 submit/poll**。两阶段是硬要求:否则「不重复扣费」的恢复路径永不被测试覆盖。stub 用 JDK 内置 ImageIO/Java2D 出真实字节,零新依赖。`[02]` |
| **D** | **新模块归属与命名** | 推荐新建模块、端口 `:8085`、前缀 `/api/v1/creation/**`。**命名存在冲突**`patbond-creation``[01]` vs `patbond-ai``[02]`。ADR-009 原文记载「后端评估建议 user 内包,**用户裁定新建模块**」。代价:`BearerAuthFilter` 将成第 4 份(community 版 Javadoc 自称 "Third copy")、存储/配置第 4 份、`UuidV7` 第 3 份。`[01][02]` |
| **E** | **4 件共享物是否上提 common** | 推荐同波次上提,退路是各抄一份。**本迭代最大回归面**,与 D 强耦合。`[02]` |
### 流程与安全(建议第一波,先于相关代码落地)
| # | 决策 | 现状与推荐 |
|---|---|---|
| **F** | **门禁必需上下文改为 `(pull_request)`** | 现状必需上下文为 `CI / backend-test (push)`,而 `ci.yml:16-17` 把 push 触发器限定在 `branches: [dev]`。**双重后果**:① 门禁实际由「推 dev」满足,**PR 侧检查红也能合并**;② **任何非 dev 分支 → main 的 PR 永久无法合并**head 永不产生 `(push)` 状态),使 `git-workflow.md:9` 的 hotfix 短命分支纪律实际是死的。正确上下文真名已从 CI 实跑记录取得(非猜测):`CI / backend-test (pull_request)` / `CI / flutter-gates (pull_request)`。**推荐替换而非追加**(追加不解除 hotfix 死锁),改完做一次**空 PR 验证**,勿拿正式发布当试验场。`[04][08]` |
| **G** | **`check-secrets.sh` 补 AI 凭证规则** | 现有 7 条规则对 `sk-`/`sk-ant-` **零覆盖**`KEY-ASSIGN` 只认 `access_key`/`secret_key` 不认 `api_key`,故 `ANTHROPIC_API_KEY=sk-ant-…` 全漏网。**必须先于任何 AI key 落地。** 另:三仓 `core.hooksPath` 全部 unset,防泄漏第一层完全未生效。`[08]` |
| **H** | **北极星 09-21 时限处置** | **窗口已于昨日关闭**,不是「来不及」:09-21 是 W37 队列(首记 09-07~09-13+8 天的成熟日,而 09-13 = 周日 = W37 最后一天,今日 09-14 已是 W38 第 1 天,此后产生的首记永远进不了 W37。**补救无从下手。** 且 W37 分母极可能为 0(真机执行记录全空、无分发、API 未对外、桌面 100% 丢弃)。推荐:档 A 把首次出数由**日期承诺改为事件驱动触发条件**(改写 `device-verification.md:40` 等两处)+ 档 B 09-21 仍出一次「基建就绪读数」(只报 SQL 可执行 + 实测分母 + 链路证据)。**明确否决**用桌面 override 或 curl 造数凑读数。另:北极星出数**至今无可执行 SQL 载体**,`iteration-2/06 §2.1` 的 SQL 只存在于报告正文。`[06]` |
| **I** | **发布流程固化去处** | 正典是一份标题至今写着「草案」的迭代报告,8 步里 4 步过期(第 2/3/4/6 步);`git-workflow.md` 全文 65 行无任何发布/PR/E2E 内容。推荐固化进 `git-workflow.md` 新章节,**E2E 份数只在一处写死**,其余位置表述为「全份」并链接过去(防复发)。`[07][08]` |
| **J** | **文档站访问控制** | 登记项仅为 `server-exposure.md` §2 表格里一个内联 `⚠️`,无责任人、无期限、「归属决策」列为空、未进 §5 行动记录。**102 页全部公开,含 `server-exposure.md` 自身、`ci-runner-setup.md`、以及 09-11 安全事件复盘**(端口清单、常驻服务、内部 API 路径、刚被攻击那台主机的注入路径与处置手法)。与 Gitea 同机同 nginx、共用 443、无鉴权层。**防护方式存在分歧**,见 §4-3。`[07][04]` |
| **K** | **进度反馈机制** | 推荐**自适应轮询**(1s×3 → 2s×5 → 3s,上限 5s,硬超时 5min),不做 SSE/WebSocket。决定性理由:现有网络层全部围绕「信封解包 + 401/40101 单飞刷新重放一次」构建(`api_client.dart:97-131,163-176`),SSE 两条都用不上,**等于旁路整条已建好的鉴权链、形成第二套鉴权路径——而 M3.5 端口事故的根因正是「跨模块调用绕开既有接线纪律」**;且两侧流式基础设施均为零(后端 `SseEmitter` 0 命中、客户端 `ResponseType` 0 命中);**A 是 B 的真子集**(任何推送方案都必须有「查状态」GET 兜底,有了它轮询已免费到手)。`[03][05]` |
| **L** | **中断恢复的事实来源** | 推荐服务端「我的进行中任务」端点,**不做本地持久化**。理由:与「服务端是唯一事实来源」纪律一致;全仓刻意没有任何业务任务态被持久化(M3 防泄漏成果),开这个口子须在 `app.dart:242-249` 再加一处清理,漏则是安全缺口;换设备场景下本地方案完全失效。`[03]` |
| **M** | **eventVersion 定型** | 定为「**每个事件自身的 props schema 版本**」,否决「字典世代」读法。决定性理由是**可修复性不对称**:此读法下客户端现行硬编码 `1` **本就正确**(零改动零回填);「字典世代」读法下全部历史行皆错且不可修复(`server_ts` 无法反推事件当初属于哪代字典)。落地:白名单 key 改 `(name, version)` 二元组 + 新增逐条拒绝原因 `unknown_event_version`——**必须放逐条路径,不可放 DTO 校验,否则重犯 platform 的整批连坐错误**(见 §6-1)。`[06]` |
---
## 5. 待仲裁:agent 之间的分歧(6 项)
| # | 分歧 | 两方主张 |
|---|---|---|
| 1 | **新模块命名** | `patbond-creation``[01]`vs `patbond-ai``[02]`)。端口 `:8085` 与前缀一致,仅名称不同。 |
| 2 | **api 测试 381 的成因** | `[04]``3cd8005` 就是那次「379→381」的契约冻结提交,`releases.md:17/:35``feature-checklist.md:5` 照抄了冻结前的数(给出 3+131+40+100+107 分解)。`[01]`379 是 surefire 报告数、381 是注解数,属参数化展开差异、非回归。**两解释互斥**;`[04]` 给了可核的分解,倾向 `[04]`,但两方均未联合复跑。 |
| 3 | **文档站防护方式** | `[07]`:IP 白名单(单读者、纪律 6 下不必新增凭证),或内容分层把三份敏感文件剔出公开构建。`[04]`basic auth。 |
| 4 | **A/B 前置状态口径** | `[01]`:**5 绿 3 半**(分流哈希、Flutter 曝光封装、feature flag 三者代码零实现)。`[06]`:**放行条件 0/6 满足**,故 M4 首个「实验」应为 **A/A 基建验证空跑**(零产品风险,≥200 曝光即可暴露分流失衡与曝光漏报)。两者可能在数不同的东西(前置组件 vs 放行条件),需统一口径后再排期。**注意:原文档「6 绿 1 半」两方均否定** —— 按它排期会在实验启动日才发现要先写三个组件。 |
| 5 | **429 错误码个数** | `[02]`:单码 `42900``[05]`:**必须拆两个码** —— 配额耗尽等的是明天(弹 sheet 给出口),频率过快等的是几秒(弹 SnackBar),共用一码必然有一半场景的文案和出口是错的。 |
| 6 | **E2E 断言数 226 vs 234** | `[04]`:机械可数 226210 `check(` + M1 16 `✗`)。`[07]`:静态调用点 207,缺口**可能**由循环执行产生但原报告从未说明。声称值 234 无来源可复核。 |
**两处属细化而非冲突,一并记录**
- **SegmentedButton 债的优先级**`[01]` 改判搭车 M4(create 页分段器正是改造对象,一次修 6 处,边际 S);`[05]` 实测 `secondaryContainer #FFDAD2` + `onSecondaryContainer #5D4038` **对比度 7.19:1 达标**,故是 **P2 品牌一致性债、非 P1 无障碍债**,不应为它拖 M4 范围。`[03]` 补根因:主题层无 `segmentedButtonTheme` 亦无 `chipTheme`0 命中),与 M3.5-01 修 DatePicker **完全同构**`app_theme.dart:215-296` 是现成样板。→ 结论:搭车但降级为 P2。
- **`widthPx/heightPx` 的归因**`[01][04]` 记为服务端缺尺寸探测;`[03]` 拆成**三段事实** —— 详情页客户端**已正确消费**(`post_detail_page.dart:515-521` 有 clamp,服务端一给即生效)、Feed 卡片是**客户端硬编码缺陷**(`post_card.dart``post_media_grid.dart:326-327` 硬编码 4:3,维度可用却被忽略)、**客户端根本无法提供维度**(`CreateMediaUploadRequest` 契约无该字段)。→ 必须两侧同批改,只修一侧用户零感知。`[05]` 补严重度判断:不是「更严重了」而是**「从看不见变成看得见」**——A4 用 job 自己的 width/height 画出完整构图、发到 Feed 后被裁成 4:3,是**同一次会话内的直接前后对照**,9:16 竖图裁掉约 58%,用户结论会是「发布把我的图裁坏了」。修复成本极低(Worker 写 output asset 时 job 的 width/height 就在手里)。
---
## 6. 「文档与代码相反」清单(本批核心教训)
M3.5 曾因一句转述(报告写「无 nickname 字段」实指「契约未暴露」)把规模预估**高估一整档**。本批发现该模式**远非孤例**:
| # | 说谎处 | 实际情况 | 已污染范围 |
|---|---|---|---|
| 1 | `analytics_service.dart:99-102` 注释称「逐条 rejected、不影响客户端」 | **与实现相反**`TrackEventsRequest.java:16``@Valid` 级联到列表元素 → 任一元素失败即整请求 400 → 客户端 `:273-281` 视为永久拒绝并**整批删段**。**这是一个与平台无关的「毒丸批次放大器」**:任何一条事件的任何一个字段失败都会毒死整批 50 条。桌面采集能力为零。 | 转述进 **5 处文档** `[03][06]` |
| 2 | CI 自称 "byte-identical"、文档称「四模块字节级快照锁 CI」 | CI 零 diff、零校验和;md5 比对是人工步骤 | 4 份报告 `[01][02][04][08]` |
| 3 | `releases.md` 同一文件 `:36/:56`「四份」vs `:131`「双份」 | 前瞻侧(给 M4 用的那条)写错,照它执行漏跑 **17** 个场景 | `[07][04][08]` |
| 4 | 发布 checklist `iteration-3/08:84`「跑 M2+M3 **两份**」 | 应为全份;且该 checklist 标题至今是「草案(验证后固化进 `git-workflow.md`)」,**固化从未发生**grep `git-workflow.md``checklist\|发布\|E2E\|PR` 零命中) | `[07][08]` |
| 5 | `openapi.yaml:1228`「purpose 仅 `post_image`」 | 与 `:3460` 三值枚举矛盾(M3.5 加值时漏改端点描述)。M4 再加值必须一并修,否则第三次漂移 | `[03]` |
| 6 | `PostMediaAttachIntegrationTest:44-45` 断言 640/480 **是绿的** | 靠 `CommunityTestData:55` 夹具直插。**测试绿 ≠ 生产有值**,生产恒 null | `[04]` |
| 7 | `feature-checklist.md:218`「community 域 19 条」 | 算术错误,代码自身注释拆分为 8+2+8=**18**`experiment_exposed` 被重复计数;`EventDictionaryTest` **零条数断言**故漂移不可见 | 4 份文档 `[07][06]` |
| 8 | `releases.md:17/:35``feature-checklist.md:5` 的 379 | 照抄了契约冻结前的数,实测 381 | `[04][07]` |
| 9 | `releases.md:121` 整段漏掉 M3.5 | 真机项转述链 **4→6→8→10 每跳都在丢项** | `[04][07]` |
| 10 | `releases.md:47`「分支保护确实要求该 (pull_request) 检查」 | 表象对、机制归因错:真正解锁合并的是 09:44:05 转绿的 `(push)` | `[08]` |
| 11 | `iteration-3.5/04 §7.3`「CI 仍红」 | 过期快照;同一 run 73 已于 09-14 09:38 重跑、09:44 转绿 | `[04][08]` |
| 12 | `iteration-3.5/05``integration_test/` 证「桌面链路可用」 | 那 4 份真机测试**完全在 `flutter test` 之外**,无任何门禁会跑。凡「桌面/真机实测已验证」一律降级为「有脚本,无门禁」 | `[07][03]` |
| 13 | M3 建议「话题随 `ai_creation` 一起做」 | `ai_creation` 只是 category 枚举值,与 topics 表**零字段关联**,搭车理由不成立 | `[01]` |
**由此造成的既有误判(本批已纠正)**
- `widthPx/heightPx` 规模 **M→S**(列早已存在于 `V1:217-218`,缺的只是尺寸探测)`[01]`
- 「M4 需设计数据模型」→ **完整设计早已定稿**(3 表 40 列 11 索引),且**零跨 schema FK 需裁剪**T4-01 由 **L→M** `[01]`
- 「社区侧有 purpose 白名单校验」→ **不存在**`PostService.validateAssets:343-358` 只校验归属 + ready`MediaAssetRef.java:9-16``MediaAssetGateway` 的 SQL 均不含 `purpose` 列(对比 pet 侧 `PetService.java:196` 确有校验)。**后果:不补强制校验,「AI 创作」标签可被任意图片伪造。** `[02]`
- 「需评估 DB / Redis / MQ 三选一」→ 前提本身多余,目标模型已把租约队列设计完(§3)`[02][04]`
- 「A/B 前置 6 绿 1 半」→ 两方均否定(§5-4)`[01][06]`
---
## 7. 未取证项汇总(不得当事实使用)
| 项 | 缺什么 | 阻塞谁 |
|---|---|---|
| 生产服务器 GPU 情况、云 API 报价 | 外部输入 | **决策 A(阻塞开工)** `[01]` |
| E2E 断言精确值(226 / 234) | 脚本无计数器;循环执行未证 | 门禁数字口径 `[04][07]` |
| 契约矩阵 181 格 | 只存在于报告散文,无机械可数载体 | 契约漂移检测可信度 `[04]` |
| 分支保护「禁直推」的具体机制 | `/branch_protections` 返 401`/branches/main` 匿名可读故主结论仍成立) | 决策 F 的复核 `[08]` |
| E2E 零失败重跑、零迁移实证 | compose 未起 | 发布门禁复现 `[04][07]` |
| `check-secrets.sh` 实际效力 | 未实跑 | 决策 G `[04][07]` |
| 服务器侧实际暴露面 | 只做了文档取证,未探测服务器 | 决策 J `[04][07]` |
| 服务端是否探测图片尺寸、`page_viewed` props 取值集合 等 6 项 | 见 `[03]` 附录 A | 相关工单 `[03]` |
| 实验设计模板文档实体、北极星出数 SQL 载体 | 均无实体 | 决策 H `[01][06]` |
| 厂商侧参数配比、配额默认值、单机资源余量 | 需产品/运维确认 | 后端工单 `[02]` |
---
## 8. 规模概览(各报告独立估算,未经交叉校准)
| 维度 | M4 预期 | 来源 |
|---|---|---|
| 工单 | 28(含 3 条件单),S×5 / M×16 / L×44 波 | `[01]` |
| 关键路径 | 4 个 L 单:后端队列 2 + 客户端状态机 2(与 M3「媒体双端占其二」同型) | `[01]` |
| Flyway | **2 个**V6creation schema 三表 + 13 索引 + 3 触发器 + 补回 `posts.generation_job_id` 外键)、V7(目录种子)。creation schema 零结构偏离,逐列抄目标模型 | `[02]` |
| 契约 | v1.4.0 → **v1.5.0 纯增量**+4 路径 / +5 操作 / +8 schema32/45/75 → **36/50/83**)、+3 错误码、改 4 个既有 schema;零删除零重命名零必填收紧。「一键建草稿」复用既有 `POST /api/v1/posts`**0 新增路径** | `[02]` |
| 客户端页面 | 6 页(4 AI + 2 搭车:我的收藏 / 我的草稿) | `[03]` |
| UI 界面单元 | 86 页 + 1 sheet + 1 全局层),新建组件 8 / 复用 16(6 需小改) | `[05]` |
| 客户端测试 | 597 → **约 897**(建议 +300 | `[03]` |
| E2E | 42 → **54 场景**;新增第五份 `test_e2e_m4_manual.dart` | `[03][08]` |
| 埋点字典 | **41 → 49**(新增 8 条 `creation` 域 + 2 处既有事件加属性 + 启用 `experiment_exposed`,废弃 0 | `[06]` |
| demo 消亡 | `create_page.dart` **563 行 → 约 40 行**13 处模拟) | `[03]` |
---
## 9. 今天即可执行的两件事
1. **M2 两项真机验证** —— 约 **55 分钟**(其中 35 分钟纯等待),模拟器合规、`Pixel_7` AVD 已就位,只差导出 `ANDROID_HOME`/`PATH`。**已白拖 6 天**;虽已错过 W37 窗口(决策 H),但它把最早可得读数从「无限期」拉到 **2026-09-28**`[04][06]`
2. **`check-secrets.sh` 补 AI 凭证规则** —— 决策 G,须先于任何 AI key 落地。`[08]`
---
## 10. 必须与写侧同批修掉的两处(否则 M4 制造新缺陷)
1. **数据损坏路径**`post_compose_page.dart:209-211` 把恢复的 AI 草稿类目静默降级为 `general`。现在无害(写侧被封),**M4 放开写侧后会把类目改错并写回服务端** —— 必须与放开写侧同一个提交改掉。`[03]`
2. **`CommunityMigrationIntegrationTest.java:81-88` 断言 `fkCount == 0`** —— V6 补外键**必然让它变红**,需拆成 generation=1 / region=0。提前知道可省一轮排查。`[02]`
---
## 11. 两条会影响 M4 观感的客户端纪律
1. **绝不能承诺推送**:客户端**无任何推送能力**(无 WS/SSE/FCM/相关端点)。等待页写「完成后通知你」会让用户锁屏等推送然后什么都收不到。正确说法是「回到 App 就能看到结果」。`[05]`
2. **不要押注百分比进度**`generation_jobs` 的 CHECK 规定 `queued` 时 progress 恒 0、`running` 时 progress 无下限约束,押注「会有百分比」的后果是**最常见观感变成「0% 卡住」**。故主形态为阶段文字 + 不确定进度环,`progress>0` 才叠确定型数字;**不展示 ETA,改展示已用时**(已用时是事实,ETA 是承诺,而 M4 给不出可信 ETA)。另需定义服务端 5 个 status 之外的**第六个 UI 态:僵死/超时**(`running` 超阈值),且它不是错误态。`[05]`
---
## 12. M4 核心指标当前算不出来
两个 post 事件(`post_publish_succeeded``post_draft_saved`)的现行白名单**无任何标识帖子来源的属性**,因此「AI 生成 → 发帖」这条 **M4 最核心的转化在现行字典下根本不可计算**。处置:各加 `creationTaskId`。另建议补一条**断言白名单条数**的测试——本批 41/42 漂移之所以四份文档不可见,正因 `EventDictionaryTest` 零条数断言。`[06]`
@@ -0,0 +1,742 @@
# Patbond 第四迭代任务分解(M4 AI 创作)
> 作者:Senior Project Manager
> 日期:2026-09-14
> 依据:`docs/development/development-plan.md`(第 7 节 M4、第 4/6 节规范、第 9/10 节质量门禁与 DoD)、`iterations/iteration-3/29-m3-summary.md` §4、`iterations/iteration-3.5/06-wave2-closure.md` §7、`docs/architecture/backend-modules.md`、`docs/architecture/decisions.md`ADR-001~022)、`docs/database/patbond_postgresql.sql``creation` schema 3 表,已评审定稿)、`docs/api/openapi.yaml` v1.4.032 路径 / 45 操作 / 75 schema,冻结中)
> 编号约定:本迭代工单以 `T4-` 前缀编号,决策以 `D4-` 前缀编号。
> 范围声明:严格限定为 M4 AI 创作。本地服务与预约(M5)、通知推送与交付加固(M6)不在本迭代范围;`posts.region_id` 仍仅作预留。范围外需求一律记 backlog。
**结论摘要**:工单 **28** 个(其中条件单 3 个)分 **4 波**;待用户拍板决策 **14** 项,头号三项为 AI 提供方选型、模块归属与 Worker 形态、生成输入图是否必填。开工审计**推翻或修正了 8 处既有文档结论**,其中两处直接改变规模预估:`creation` schema 的完整设计早已评审定稿(V6 是提取而非设计,且零跨 schema 外键需裁剪),而「输出写入媒体表」一句话背后隐藏着「服务端目前根本不能写对象存储」这一真实工作量。
---
## 1. 范围界定与依据
### 1.1 开发计划 M4 原文(正典依据)
开发计划第 7 节 M4`development-plan.md:246-255`)逐字引用:
- 目标:「替换上传和生成延时模拟。」(:248)
- 「实现模型/风格目录、生成任务创建、查询和取消。」(:250)
- 「Worker 通过队列消费任务,使用租约、重试和幂等键防止重复生成。」(:251)
- 「输出写入媒体表,成功后可一键创建社区草稿。」(:252)
- 「Flutter 展示排队、生成进度、失败重试和取消状态。」(:253)
- 验收标准:「任务状态流转合法;Worker 重启不丢任务;相同幂等请求不重复扣费或生成;失败原因可追踪。」(:255)
四条验收标准与工单的映射:
| 验收标准 | 主责工单 | 取证方式 |
| --- | --- | --- |
| 任务状态流转合法 | T4-06 / T4-07 / T4-08 | `ck_generation_jobs_state` 数据库层强制五态字段组合(目标模型 `patbond_postgresql.sql:667-692`),非法迁移**在库层即被拒**;应用层再补六类路径测试 + T4-25 E2E |
| Worker 重启不丢任务 | T4-08 | 租约过期回收(`lease_expires_at` + `ix_generation_jobs_running` partial 索引);T4-25 E2E 在 running 态重启容器后断言任务被重新领取并完成 |
| 相同幂等请求不重复扣费或生成 | T4-06 | `UNIQUE (user_id, idempotency_key)` + `uq_generation_jobs_provider_request`;配额按 `generation_jobs` 行数计(见 D4-7),幂等命中不新建行即天然不重复扣费 |
| 失败原因可追踪 | T4-07 / T4-08 | `error_code` / `error_message` / `attempt_count` 三列经查询端点外露;provider 原始失败经日志(不含密钥)留痕 |
### 1.2 其他正典约束(M4 必须遵守)
- **业务域→schema 映射已定**`development-plan.md:67`「AI 创作 = 模型、风格、异步任务、重试、生成结果 → `creation` schema」;:64「媒体域含 AI 输入输出」。
- **写接口幂等强制**`development-plan.md:173`「创建帖子、**生成任务**和预约等写接口必须支持 `Idempotency-Key`」。
- **可观测性硬要求**`development-plan.md:329`「技术指标须含**队列积压和 Worker 失败率**」;:330 产品漏斗须含「**AI 任务成功**」。
- **`patbond-common` 纪律**`development-plan.md:79`「不应让所有服务被动引入 RabbitMQ、Feign 等依赖」——这条对 D4-3 队列载体选型有直接约束力。
- **Flutter feature 拆分已预留 `creation`**`development-plan.md:96` 明列 `auth/pets/community/creation/marketplace`
- **单迁移链**`backend-modules.md:41`「Flyway 迁移链唯一持有者……**其他模块不得携带 Flyway**」——V6 必须进 `patbond-user`,与模块归属决策(D4-2)解耦。
- **结构性变更须经 ADR**`backend-modules.md:3-5`「新增/拆分模块须经 ADR 决策并同步更新本页」。M4 是 **ADR-023 起**的落点(现存最高 ADR-022)。
- **AI 提供方在正典中仍为未决项**:`development-plan.md:358`「AI 提供方、对象存储、天气/地图供应商和通知渠道尚未确定」、:365 待确认事项同列。对象存储已由 ADR-016 解决,**AI 提供方是 M4 唯一从头拍板的供应商项**(D4-1)。
## 2. 开工前的关键事实(PM 逐项核实原始 SQL / 契约 / 源码)
> **纪律说明**M3.5 的教训(`iteration-3.5/06-wave2-closure.md:42`「转述不可采信」——一份报告写「无 nickname 字段」实指「契约未暴露」,被当成缺列,规模高估一整档)在本节被严格执行。下列每一条「存在 / 不存在」的判断都注明了核实的**原始文件与行号**,未经原文核实的一律标注「未取证」。
### 2.1 `creation` schema 的完整设计早已评审定稿——V6 是**提取**而非设计(推翻「M4 需从零设计数据模型」的隐含预期)
`docs/database/patbond_postgresql.sql`(评审定稿的目标模型)已含 `creation` schema 全部三张表:
| 对象 | 行号 | 关键内容 |
| --- | --- | --- |
| `CREATE SCHEMA creation` | :60 | 七个 schema 之一(platform/identity/media/pet_health/community/**creation**/marketplace |
| `creation.generation_models` | :556-573 | `code`/`display_name`/`provider_code`/`provider_model_name`/`media_kind`/`enabled`/`sort_order``UNIQUE (code, media_kind)` + `UNIQUE (id, media_kind)`;索引 :575-577partial `WHERE enabled` |
| `creation.generation_styles` | :580-598 | `code`/`title`/`subtitle`/`media_kind`/`preview_asset_id`(FK→media.assets ON DELETE SET NULL)/`enabled`/`sort_order`;索引 :600-602 |
| `creation.generation_jobs` | :605-696 | **40 列**,含完整队列语义(见 2.2 |
| 队列索引 | :711-715 | `ix_generation_jobs_queue (priority DESC, next_attempt_at, created_at, id) WHERE status='queued'``ix_generation_jobs_running (lease_expires_at, id) WHERE status='running'` |
| provider 幂等唯一索引 | :699-700 | `uq_generation_jobs_provider_request (provider_code_snapshot, provider_request_id) WHERE provider_request_id IS NOT NULL` |
| `updated_at` 触发器 | :1245-1249 | 三张表均复用 `platform.set_updated_at()`V1 已建) |
| 开发种子 | :1495-1523 | 2 模型(image+video/ 5 风格 / 1 已完成任务;`provider_code = 'fixture'` |
**规模含义**T4-01 的性质与 M3 的 T3-01 完全同构(「从 bootstrap SQL 提取」,`iteration-3/01:52`),是**抄录 + 裁剪判断**,不是建模。据此定为 **M** 而非 L。
### 2.2 M4 的迁移**零跨 schema 外键需要裁剪**——与 V3/V5 的先例相反
V3 剪了 4 条 marketplace 外键、V5 剪了 2 条(`V5__community_baseline.sql:15-25` 原文)。M4 的 V6 **一条都不用剪**
- `generation_jobs` 的三条跨 schema 外键目标全部已存在:`identity.users`V1:60)、`pet_health.pets`V3:35)、`media.assets`V1:205)。`generation_styles.preview_asset_id → media.assets` 同理。
- `generation_jobs` 内部两条复合外键(→`generation_models(id, media_kind)`、→`generation_styles(id, media_kind)`,:644-647)同一迁移内建表即可满足。
- **反向的一条要补回**`community.posts.generation_job_id` 目前是裸列无外键——`V5__community_baseline.sql:47` 行内注释原文 `-- generation_job_id: bare nullable uuid, FK to creation.generation_jobs stripped (M4 补回)`,列定义 :48 `generation_job_id uuid`nullable),索引 `ix_posts_generation_job` 已在 :95 建好。V5 文件头 :17-19 亦言明「the M4 migration that creates the creation schema re-adds this constraint」。
### 2.3 队列语义在库层已完备——**不需要引入任何新中间件**
`generation_jobs` 已含全套 DB-as-queue 列(`patbond_postgresql.sql` 行号):`status`(:625)、`progress`(:626)、`priority`(:627)、`attempt_count`/`max_attempts`(:628-629)、`provider_request_id`(:630)、`error_code`/`error_message`(:631-632)、`idempotency_key`/`request_hash`(:633-634 均 NOT NULL)、`next_attempt_at`(:635)、`lease_owner`/`lease_expires_at`(:636-637)、`started_at`/`completed_at`(:640-641)、`version`(:642)。
配合 :711-715 的两条 partial 索引,`SELECT … FOR UPDATE SKIP LOCKED` 即构成带优先级、退避与租约回收的完整队列。
**与开发计划的偏离提示**`development-plan.md:205` 写「RabbitMQ 在异步任务阶段启用」。核实基础设施现状后(见 2.4),PM 建议本迭代**不引入 RabbitMQ**,改用 DB 队列——这属于对正典的建议性偏离,须经 D4-3 拍板并落 ADR。
### 2.4 现有异步基础设施:几乎为零(这是 T4-08 的真实起点)
| 事实 | 核实位置 |
| --- | --- |
| 无 Redis / RabbitMQ / Kafka(依赖与容器均无) | `docker-compose.yml` 六服务:postgres:13 / minio:34 / user:49 / auth:79 / pet:97 / community:125;各模块 pom 依赖清单(如 `patbond-user/pom.xml:28-104`)无任何 MQ/Redis 坐标 |
| `@EnableScheduling` 仅一处 | `patbond-user/.../UserApplication.java:7` |
| `@Scheduled` 唯一使用点 | `patbond-user/.../session/SessionCleanupJob.java:32-41``fixedDelayString = "${patbond.session.cleanup-interval:PT6H}"`),配置 `patbond-user/src/main/resources/application.yml:24-26` |
| `@Async` / `@EnableAsync` / `TaskExecutor` / `ThreadPoolTaskScheduler` | **全仓零命中**(主代码与测试) |
| 队列消费循环 / MQ 监听器 / outbox 发布器 | **不存在**`platform` schema 注释(V1:24)承诺了 "reliable outbox",但表与消费代码均未落地 |
| 无限流 | 全仓无 `RateLimit`/`bucket4j`/`429``patbond-common/.../error/ErrorCode.java` 无 429 段(最接近的 `LOGIN_LOCKED(42300, 423, …)`:36 是账号锁定不是限流) |
| 无 feature flag 设施 | 全仓零实现(详见 2.9) |
结论:M4 的 Worker 只有**一个先例可循**`SessionCleanupJob` 式的 `@Scheduled` + DB 幂等操作,Spring 默认单线程 scheduler、多实例并跑无锁)。这既是 D4-2/D4-3 的现实约束,也说明 T4-08 必须自带线程池与并发度配置。
### 2.5 「输出写入媒体表」一句话背后:**服务端目前根本不能写对象存储**(本迭代最易被低估的工作量)
`ObjectStorage` 接口只有四个方法(`patbond-user/.../media/ObjectStorage.java:15-37`):`ensureBucket()`:18、`presignPut(...)`:25、`stat(...)`:28、`presignGet(...)`:31。实现 `S3ObjectStorage.java``S3Client` 实际只用于 `headBucket`/`createBucket`(:66-79) 与 `headObject`(:102)`putObject` 仅出现在**预签名构造**里(:83-86 `presigner.presignPutObject(...)`)。
即:现有媒体链路是「客户端直传,服务端只签名与校验」。AI 输出的字节由 provider 返回、必须**由服务端落盘**,因此 M4 必须:
1.`ObjectStorage` 上新增写方法 + `S3ObjectStorage` 实现 + `MediaStorageConfig.java:33-59``UnconfiguredObjectStorage` 补桩;
2. 解决「谁来写 `media.assets` 行」——ADR-017`decisions.md:156`)定死「media 上传流程实现在 `patbond-user`」,且 `media` 写入侧代码全在 `patbond-user/.../media/``MediaAssetRepository.insertUploading`:32-41、`markReady`:81-86);`pet`/`community` 侧只有 `MediaUrlSigner`(本地 SigV4 签名,不持 `S3Client`)与只读 `MediaAssetGateway`
这直接决定 D4-2 的子问题:新模块如何把生成结果落进 media 域。已单列为工单 **T4-03**
### 2.6 `media.assets` 的尺寸列早已存在——`widthPx/heightPx` 恒 null 不是缺列(M3.5 同型教训的复现)
- `width_px integer` / `height_px integer` 均在 `V1__identity_media_baseline.sql:217-218`,约束仅 `ck_media_dimensions`:241-245,正数校验),**nullable**。
- 契约侧唯一关于回填的原文在 `openapi.yaml:3536``description: complete 后回填,可空`;但 `POST /api/v1/media/uploads/{assetId}/complete`:1256-1299**没有 requestBody**:1274 直接进 parameters),也没有任何字段承载尺寸;`MediaAsset.required` = `[id, kind, purpose, mimeType, status, createdAt]`(:3516),两个尺寸字段不在其中。
- 故此项的真实缺口是「**服务端无尺寸探测**」+「契约声明了来源不存在的回填」,规模为 **S**(配合 T4-03 新增的对象读能力),不是「加列迁移」。处置方案见 D4-9。
### 2.7 `ai_creation` 分类在写侧被**双重封死**(代码 + 契约)
- 数据库允许:`V5__community_baseline.sql:68` `CONSTRAINT ck_posts_category CHECK (category IN ('general', 'help', 'ai_creation'))`
- 后端写侧拒绝:`patbond-community/.../dto/CreatePostRequest.java:26` `@Pattern(regexp = "general|help", message = "category 仅支持 general/help")`
- 契约写侧拒绝:`openapi.yaml:3657` `CreatePostRequest.category` enum `[general, help]`:3659 注明「`ai_creation` 为 M4 预留值,M3 不开放写入(提交 400/40000)」;读侧 `Post.category`(:3747-3748) 与 `FeedCard.category`(:3826) 已含三值。
- `generationJob` **在契约里没有字段本体**——`openapi.yaml:136` 与 :3717 只是说明文字,`PostResponse.java:12` 亦注明该字段「do not appear at all」。
- 落库通路也缺:`patbond-community/.../repository/PostRepository.java:56-58``insertPost(...)` 签名与 :60-64 的 INSERT 列清单**均无 `generation_job_id`**。
即 T4-10 需同时改:Java 校验、Repository INSERT、契约 enum、契约新增 `generationJob` 字段。
### 2.8 目标模型内部存在一处矛盾,必须在 V6 前裁决
`generation_jobs.model_version_snapshot varchar(64) **NOT NULL**``patbond_postgresql.sql:622` 区块,:620-623 三个 snapshot 列)——但 `creation.generation_models`:556-573**没有任何 version 列**,种子数据把它硬编码为 `'1.0'`(:1514)。这是评审定稿模型里的一处自相矛盾,V6 必须选边(D4-6)。
另一处需注意的约束后果(可满足但不直观):`ck_generation_jobs_state`:667-692)要求 `queued` 态必须 `started_at IS NULL AND progress = 0 AND lease_* IS NULL`。因此**租约过期回收/重试时必须把 `started_at` 清回 NULL**,首次尝试的开始时间会丢失。T4-08 需在实现说明中显式记录这一点。
### 2.9 A/B 前置「6 绿 1 半」不成立——实测为 **5 绿 3 半**
`iteration-3/06-experiment-tracking-plan.md:336-351` 的八项前置表逐项对代码核实后:
| # | 前置 | 文档判定 | 代码核实 |
| --- | --- | --- | --- |
| 4 | 稳定分流组件 | 绿(:345) | **部分绿**`anonymousId` 持久化已落地(`analytics_service.dart:51`、:177-190),但 `hash(userId, salt) % buckets` 分流组件**全仓零实现** |
| 5 | 曝光事件 | 绿(:346 | **部分绿**:后端字典已有(`EventDictionary.java:103`props `{experimentKey, variant}`),Flutter 侧 `grep -rn experiment lib test integration_test` **零命中** |
| 7 | 护栏监控与回滚 | 部分绿(:348,「feature flag 随社区功能发布开关顺带落地」) | **回滚也不绿**feature flag / 社区发布开关在两仓中**完全不存在**,`feature-checklist.md` 连该条目都未登记 |
另:#2「指标基线」判绿,但北极星出数 SQL 仅以 Markdown 代码块存在于 `iteration-2/06-experiment-tracking-plan.md:200-229`,三仓 `scripts/` 下**无任何可执行巡检/看板脚本**(只有 `check-secrets.sh``hooks/`)。
含义:「M4 可启首个实验」这一结论的前提比文档描述的弱。若要在 M4 启动实验,需先补分流 + 曝光 + 开关三件(工单 T4-23,条件单,随 D4-11)。
### 2.10 埋点白名单实测 **41** 条,文档三处写 42`experiment_exposed` 被重复计数)
- 权威定义在**代码常量**`patbond-user/.../analytics/EventDictionary.java:42-104``grep -c 'Map.entry("'` = **41**。无字典表(V4 只 seed `breeds``vaccine_catalog`),DB 侧仅正则约束 `V2:21`
- 文档写 42 的三处:`iteration-3/22-event-whitelist-v3.md:4,14``iteration-3/29-m3-summary.md:20``feature-checklist.md:218`
- 根因:设计源文档自己写的是 19 个新事件(`iteration-3/06-experiment-tracking-plan.md:12,149`),22v2+ 19 = 4122 号报告把 `experiment_exposed` 既算进 19 又单列一次。
- **无门禁能拦**`EventDictionaryTest.java` 无任何总量断言(无 `hasSize`/`size()`)。T4-22 顺手补一条总量断言即可永久钉住。
### 2.11 真机验证挂起实为 **10** 项(不是 8 项),M2 两项带 2026-09-21 硬时限
`device-verification.md` 逐项清点:M2 **2** 项(`:42` 验证一 Android 事件落库、`:63` 验证二 SessionTracker 30 分钟换会话)+ M3 **4** 项(`:114`/`:124`/`:134`/`:144`+ M3.5 **4** 项(`:176`/`:190`/`:200`/`:209`= **10 项**,三个执行记录(`:106`/`:168`/`:221`)全为「_(待补)_」,即 10 项全部未执行。
M2 两项的时限来自 `iteration-3/29-m3-summary.md:52`(埋点角色建议 2026-09-21 北极星首次出数日前完成,否则首批读数只能标未验收)与 `iteration-2/06-experiment-tracking-plan.md:308-312`(出数日历:2026-09-21 周一首次出数)。**今日 09-14,仅剩 7 天**——已按硬时限单独排为 T4-24 并置于第一波。
### 2.12 发布流程与回归门禁的实况(含一处文档自相矛盾)
- `main` 已受分支保护禁直推,发布必须走 Gitea PR:`releases.md:126-138` 五步流程;状态检查上下文 `CI / backend-test (push)`api)与 `CI / flutter-gates (push)`flutter),见 `releases.md:110-114`。api 的 `ci.yml:15-18` 确认 `pull_request` 触发器在位、job 名 `backend-test`
- **回归门禁已由两份改为四份**`releases.md:56` 原文「回归清单由两份改为四份……原则是『**每个引入对外端点的迭代都应有对应的 E2E 脚本,并在此后每次发布回归**』」;v0.4.0 实测 42/42 场景、234 断言(`releases.md:36`)。
- **文档自相矛盾(须在 M4 收官修正)**:同一文件 `releases.md:131` 的「发布后生效的纪律」第 1 步仍写「E2E **双份**回归 PASS」,与 :36/:56 的四份口径冲突;且该原则**从未固化进 `git-workflow.md`**(该文件 :56-64 的门禁表只列三仓的 format/analyze/test/mkdocs 四命令,无 E2E 条目)。M4 引入生成域对外端点,按 :56 原则须新增**第五份** E2E 脚本(T4-25),并把「双份」改为「五份」+ 把该原则写进 `git-workflow.md`T4-27)。
- 现有四份脚本实为纯 Dart CLI(`dart run`,非 `flutter test`,不计入 597):`test_e2e_manual.dart`M17 场景)、`test_e2e_m2_manual.dart`11)、`test_e2e_m3_manual.dart`14)、`test_e2e_m35_manual.dart`10);四份同构骨架(`fail()`/`check()`/`_passed` 计数/`Resp` 包装/通用 `call()`/脚本内现注册账号取 token/token 与预签名 URL 脱敏),T4-25 照抄即可。
### 2.13 契约快照锁的实际强度**低于文档口径**(「四模块字节级快照锁 CI」不成立)
- 正典 `docs/api/openapi.yaml` 与四份快照(`patbond-{auth,user,pet,community}/src/test/resources/contract/openapi-v1.4.0.yaml`)实测 md5 五处一致(`a7081fb84f1207eef579ab94025f5801`),字节级同步这一**事实**成立。
- 但**CI 里没有任何 md5/checksum 校验**`patbond-api/.gitea/workflows/ci.yml` 只有三步(secret scan :41-42、装 JDK17 :46、`./mvnw -B clean test` :55);`scripts/` 下只有 `check-secrets.sh`。md5 比对只出现在人工冻结记录里(`iteration-3.5/04-contract-freeze-v140.md:77``03-backend-profile-avatar.md:287`「字节级复制正典 + md5 逐一比对」= 人工步骤)。
- CI 实际锁住的是**每模块一条守卫测试**的「版本 + 三个计数 + tag 分组」:`AuthContractConformanceTest.java:399-407``MediaContractConformanceTest.java:246-251`、pet `ContractConformanceTest.java:755-760``CommunityContractConformanceTest.java:522-527`,断言值 `1.4.0 / 32 / 45 / 75`
- **缺口**:任何「计数守恒的字节修改」(改 description、改 enum 取值、改 min/max、同时增删各一字段)都能悄悄通过。M4 契约面显著扩大(新增生成域 + 可能第五个模块快照),建议顺手补一条真校验和断言(T4-14,S)。
- 第二道门禁在位且有效:`@Order(99) everyDeclaredResponseCellIsExercised()`auth :414-425 / pet :771 / community :538 / user :261),v1.4.0 共 181 格、豁免 1 格。
### 2.14 Flutter create 页现状:563 行、零测试、全假
`lib/features/create/create_page.dart`563 行,`lib/features/create/` 下仅此一个文件);`grep -rn "CreatePage" test integration_test` **零命中**——这页没有任何 widget 测试。页首注释 :10-15 自述「AI 生成模拟(700/650/500ms 假延时、风格/模型/分辨率设置、结果卡)属 M4 范围,T3-17 原样保留」。
| 模拟点 | 位置 | 实况 |
| --- | --- | --- |
| 假上传 | `:55-64 simulateUpload()` | `Future.delayed(700ms)`;展示的图直接取 demo 宠物头像 `appState.pet.avatarUrl`:162 |
| 假生成 | `:66-92 generate()` | 4 步进度,`650ms × 3` + `500ms` |
| 结果图 | `:86` | `selectedStyle.image`,即 `lib/data/demo_data.dart:206-234` 的 4 个 unsplash 硬编码 URL |
| 结果文案 | `:87-90` | 硬编码「豆豆的{风格}冒险」等 |
| 发布 | `:122-129 publish()` | 纯占位 SnackBar「AI 作品发布随 AI 创作能力上线(M4)」 |
| 模型下拉 | `:179` | 硬编码 `['Patbond-V1','Pet-Art Pro','Cute Motion']` |
| 风格选择器 | `:218-292` | 横向 150×125 卡 + 渐变遮罩 + 选中描边,数据源 `creationStyles`demo |
| 进度条 | `:294`,定义 `:532-563` | `LinearProgressIndicator(value: step/4)` + 4 条硬编码 label |
| 结果区 | `:312-386` | 16:10 图 + 标题/正文 TextField + 话题 Chip + 硬编码位置「北京市 · 朝阳区」 |
| 真实入口(保留) | `:136`,定义 `:394-419` | `_ComposeEntryCard` → push 真实 `PostComposePage` |
**可复用的 UI 骨架**:模式分段器、风格横滑卡、进度条、结果编辑区四块布局可整体保留、只换数据源与状态源——这降低了 T4-17/T4-18 的视觉返工风险(M3 的 R10 风险在此不复现)。
**顺带命中一项主题债**:该页 `:138``SegmentedButton` 选中态粉底根因是主题里**没有 `segmentedButtonTheme`**`lib/core/theme/app_theme.dart:94-196` 只定制了 card/filledButton/inputDecoration/snackBar/datePicker/navigationBar),因此吃了 `ColorScheme.fromSeed`(:88-92)的派生色。已审计色对表在 `app_theme.dart:199-214`,落地范本是 `_datePickerTheme`:215-296)。M4 既然要重做该控件,此债搭车成本为 S(详见 §8)。
### 2.15 Flutter 架构与可复用资产
- **分层**feature 内平铺 `page/controller/repository/models/display/analytics/exceptions`,约定原文 `lib/features/community/community_controller.dart:14-20``Page/Widget → Controller → Repository → ApiClient`)。
- **状态管理**:无 Riverpod/Bloc/provider——原生 `ChangeNotifier` + `ListenableBuilder`,手写 DI 全在 `lib/app/app.dart:80-147`
- **路由**:无 go_router、无路由表;`MaterialApp` 只给 `home:``app.dart:308-318`),跳转靠散落的 `Navigator.push`;路由名仅服务埋点(`lib/analytics/analytics_page_name.dart:7-25`)。**新增 AI 页面需自建 `AnalyticsPageName` 枚举值**,否则 `fromRouteName` 返回 null 不上报。
- **API Client**dio;四条 baseUrl 由 `--dart-define` 注入(`lib/core/network/api_client.dart:8-34`)。**若 D4-2 新建模块,须新增第五条 `PATBOND_CREATION_API_BASE_URL`**,并同步四份 E2E 脚本的硬编码常量与真机清单的 `flutter run` 参数说明。
- **429 已有粗分支、无 Retry-After**`api_client.dart:164-167``status == 429 → ApiRateLimitException`);`lib/analytics/analytics_service.dart:268-272` 注释明写「Retry-After 分支待其落地后一并做」。T4-11 落地后此处可一并收口。
- **`CursorPage` 已有**`lib/core/models/cursor_page.dart:3-27`。**但没有可复用的分页列表容器**——四态骨架各页各写一份;可照抄的两个范本:Feed(`community_controller.dart:9,12,146-200` + `home_page.dart:456-523,536-580`)与页面级自持游标(`lib/features/pets/health_events_page.dart:19,71-127`,同款 `_ListPhase` 已复制 4 份)。
- **`MediaUploader` 可直接复用于 AI 输入图**`lib/features/community/media_uploader.dart:127-144``purpose` 由调用方注入(:154-161),已被发布页(9 图并发 2)与头像 sheet(1 图并发 1)两处复用。
### 2.16 收藏与草稿两页:后端与仓库层全就绪,**只缺页面**
- `listMyBookmarks()``GET /api/v1/me/bookmarks``community_repository.dart:52`、实现 :274-283**全仓无调用点**。
- `listMyPosts()``GET /api/v1/me/posts?status=``community_repository.dart:26-30`、:179-193;唯一调用点是发布页恢复最新一条草稿(`post_compose_page.dart:196-199``limit:1`)。
- 契约侧路径名须注意:**不存在** `/me/drafts``/me/favorites`;正确路径是 `openapi.yaml:1462``/api/v1/me/posts`,含 inline `status` enum `[draft, published]` :1476-1482)与 :1749`/api/v1/me/bookmarks`,项形态为 `FeedCard`)。全域术语是 **bookmark**,无 favorite。
- 入口现为 demo SnackBar`lib/features/profile/profile_page.dart:49` 菜单项 + :235 `showDemoMessage`,注释 :43-46 已自认「后端能力已就位,列表页本单未做」。
- 故此项规模确为 **M**(两页 + 导航 + 测试,照抄 `_ListPhase` 范本),与 M4 契约零耦合,可在第一波并行消化。
### 2.17 基线数字的口径澄清
| 指标 | 数字 | 口径说明 |
| --- | --- | --- |
| patbond-api 测试 | **379**surefire 运行数) / **381**`@Test`+`@ParameterizedTest`+`@RepeatedTest` 注解静态计数,62 个测试类) | 文档登记的 379 来自 `releases.md:17,35` 的 surefire 汇总;两者差异属参数化用例展开口径,**不是回归**。M4 报告统一以 `./mvnw -B clean test` 输出为准并注明口径 |
| patbond-flutter 测试 | **597 通过 + 2 skip** | `flutter test` 输出原文 `00:30 +597 ~2: All tests passed!``iteration-3.5/08-release-e2e-regression.md:277`);2 skip 是环境门控冒烟(`test/smoke/detail_interactions_smoke_test.dart:173``media_upload_smoke_test.dart:133`),不含 `integration_test/`(4 份桌面实测)与仓库根四份 E2E |
| 埋点事件白名单 | **41**(非 42 | 见 2.10 |
| 真机挂起项 | **10**(非 8 | 见 2.11 |
| 契约 | v1.4.0 / 32 路径 / 45 操作 / 75 schema / 181 格矩阵 | 自行计数复核一致(`openapi.yaml:4` 版本,路径与操作按缩进计数,schemas 自 :2082 起) |
| Flyway | V1~V5,最高 V5`flyway_schema_history` 单一,归属 `patbond-user` | `patbond-user/src/main/resources/application.yml:11-12`pet/community 仅 test 作用域带 Flyway`patbond-pet/pom.xml:79-100``patbond-community/pom.xml:91-113`);auth 无库 |
| ADR | 最高 **ADR-022**M4 新决策自 **ADR-023** 起 | `docs/architecture/decisions.md:184-195` |
| 错误码 | 27 个,最大段位见 `ErrorCode.java` | 下一可用:404 段 `40407`、403 段 `40302`、422 段 `42206`、**429 段全新** |
### 2.18 未取证事项(明确声明)
1. **AI 提供方的可用性与价格**:本机为 AMD Radeon Vega 集显、无 `nvidia-smi`(即无 CUDA),自托管 SD/ComfyUI 在当前工作机不可行;生产侧腾讯云轻量服务器是否有 GPU **未取证**(需用户确认)。云 API 的具体额度与单价**未取证**(需用户提供账号或授权调研)。
2. **实验设计模板与样本量规则文档**A/B 前置 #3/#6,文档判绿):两仓中**未定位到该文档实体**,仅在 `iteration-3/06` 表格里被声明为已交付。
3. **`patbond-doc` 无仓库级 `.gitignore`**`site/` 目前仅靠用户全局 `~/.gitignore_global:183` 排除(实测未被 tracked)。换机器或 CI 检出时 `mkdocs build` 产物可被误提交,与 `git-workflow.md:62` 的纪律冲突。已并入 T4-27(一行修复)。
## 3. 本迭代 MVP 范围
PM 建议口径,待 §6 拍板确认。
**纳入**
- `creation` schema 三表落 V6 + 补回 `posts.generation_job_id` 外键(T4-01
- 模型/风格只读目录 + 种子(T4-04)
- provider 适配层 + 至少一个可跑通全链路的 provider 实现(T4-05,形态随 D4-1
- 生成任务创建(`Idempotency-Key` 强制)/ 查询 / 列表 / 取消(T4-06、T4-07
- Worker 队列消费:DB 队列 + 租约 + 指数退避重试 + provider 幂等(T4-08
- 输出落 `media.assets`(含服务端对象写入能力)与真实尺寸回填(T4-03、T4-09)
- 一键建社区草稿:开放 `category='ai_creation'` 写侧 + `generationJobId` 落库与外露(T4-10
- 生成域配额与限流(429 + `Retry-After`,契约首个响应头)(T4-11
- Flutter`creation` feature 分层、create 页真实化、排队/进度/失败重试/取消四态、结果页与一键建草稿(T4-16~T4-19
- 契约 v1.5.0 扩展与冻结(T4-13+ 快照锁补强(T4-14
- 埋点字典 v4AI 创作漏斗)+ `eventVersion` 口径定型 + 白名单总量断言(T4-22)
- 历史遗留搭车五项:`widthPx/heightPx`、429 限流、uploading 超时清理、收藏与草稿两页、真机 M2 两项(详见 §8)
- 第五份 E2E 脚本 + 五份回归 + v0.5.0 走 PR 发布(T4-25、T4-26
**待拍板裁剪项**(默认建议见 §6):
- 视频生成(D4-5,建议剪出,仅保留 `media_kind` 列与枚举)
- 文生图(无输入图,D4-4,建议剪出,图生图先行)
- 我的生成记录列表页(T4-20,条件单,建议纳入——排查失败任务的唯一入口)
- A/B 首个实验(T4-23,条件单,随 D4-11
- 文档站访问控制(T4-28,条件单,随 D4-12
**默认剪出**:真实计费与支付(无计费表,以配额替代)、生成结果的审核与水印、生成完成推送通知(M6)、多模型并行对比、逐帖曝光与服务端排序实验日志(ADR-020 backlog)、完整可观测性栈(M6)——逐条理由见 §7。
## 4. 工单列表
预估规模口径沿用前三迭代:**S ≈ 半天内,M ≈ 1-2 天,L ≈ 3-5 天**(含测试与文档)。
### A 组:数据、地基与工程基础(后端)
#### T4-01 Flyway V6creation schema 提取与 `posts.generation_job_id` 外键补回
- **仓库**patbond-api(迁移进 `patbond-user/src/main/resources/db/migration/`,单迁移链纪律 `backend-modules.md:41`),patbond-doc(迁移说明)
- **描述**:从目标模型 `docs/database/patbond_postgresql.sql:556-716` 提取 `creation` schema 三表(`generation_models` / `generation_styles` / `generation_jobs`)为 `V6__creation_baseline.sql`,含全部 CHECK、复合外键、11 条索引(两条队列 partial 索引 :711-715 必须逐字照抄)与三个 `updated_at` 触发器(:1245-1249,复用 V1 的 `platform.set_updated_at()`);**补回 `community.posts.generation_job_id → creation.generation_jobs(id) ON DELETE SET NULL`**(V5:47 承诺的 M4 义务,索引 `ix_posts_generation_job` 已存在无需重建)。按 D4-4 决定 `input_asset_id` 是否保持 `NOT NULL`、按 D4-6 处置 `model_version_snapshot` 的来源矛盾——**两处偏离目标模型的地方必须在迁移文件头注释里逐条写明理由**(沿 V5:1-33 的写法)。种子数据独立为不进生产链的 dev 脚本(沿 `db/dev/afterMigrate__dev_seed.sql` 先例)。
- **验收标准**
- 全新 postgres:18Testcontainers)上 V1→V6 全量迁移一次成功;表结构与目标模型逐列比对一致(偏离项除外,差异入迁移说明)。
- **存量库实证**:在已迁至 V5 的库上单独跑 V6 成功(沿 v0.4.0 的零迁移实证惯例 `releases.md:36`),`flyway_schema_history` 只增一行。
- 新增迁移测试断言:`creation` schema 存在、三表列数与关键 CHECK 名在位、`posts` 上出现指向 `creation.generation_jobs` 的外键约束(与 `CommunityMigrationIntegrationTest.java:79-85` 现有的「不存在该外键」断言**互为镜像,必须同步反转**,否则该测试必红)。
- `./mvnw -B clean test` 全绿(既有 379/381 测试不回归)。
- **依赖**:D4-4、D4-6 拍板(第一波首项,两项决策不定则 V6 无法定稿)。
- **规模**M
#### T4-02 生成域模块落位与鉴权接入(形态随 D4-2)
- **仓库**patbond-apipatbond-doc`architecture/backend-modules.md` 与 ADR-023 随收口)
- **描述**:按 D4-2 拍板结果落位生成域。**PM 建议方案**:沿 ADR-009/ADR-017 先例新建 Maven 模块 `patbond-creation`(:8085),复用 JWT 资源侧校验与当前用户解析(照抄 `patbond-community``security/``config/SecurityConfig.java` 结构),只读写 `creation` schema**test 作用域引入 Flyway**(照抄 `patbond-community/pom.xml:91-113` 的注释与依赖,生产侧不携带 Flyway);`application.yml``.sample` 模式(pet/community 的主 yml 被 `.gitignore:10-11` 排除,compose 直接挂载 `.sample`);compose 编排纳入第七个容器(`depends_on: postgres service_healthy` + `user service_started`)。若 D4-2 选择并入 `patbond-user`,本单退化为「新增 `creation` 包 + 端点前缀 + 属性类」,规模降为 S。
- **验收标准**
- 模块编译入构建链,`./mvnw -B clean test` 全绿;无 token / 过期 token 返回 401 + 既有 `40100` 系错误码逐字节一致。
- compose 起七容器(postgres + minio + auth + user + pet + community + creation)全部 healthy**跨服务接线以 compose 实测验证**M3 的 T3-13 曾抓出「media 端点挂错服务,单测无法覆盖」,`iteration-3/29:45`)。
- `backend-modules.md` 模块表与端口表同步更新;ADR-023 记录模块归属与 Worker 形态决策。
- **依赖**:D4-2 拍板(骨架期变更成本最低,可先按建议方案搭);T4-01。
- **规模**M(若并入 user 则 S
#### T4-03 对象存储服务端写入能力与媒体内部登记端点
- **仓库**patbond-api`patbond-user/.../media/`),patbond-doc(媒体链路说明补章)
- **描述**:**本迭代最易被低估的一单,见 §2.5**。现有 `ObjectStorage``patbond-user/.../media/ObjectStorage.java:15-37`)只有 `ensureBucket`/`presignPut`/`stat`/`presignGet`,**服务端无法写对象**。本单:① 在 `ObjectStorage` 上新增写方法(建议 `void put(String objectKey, byte[] bytes, String contentType)` 或流式重载)与读方法(`Optional<byte[]> get(...)` 或仅取图片头部字节,供 T4-09 探测尺寸),在 `S3ObjectStorage` 实现并给 `MediaStorageConfig.java:33-59``UnconfiguredObjectStorage` 补桩(保持未配置即 500 的既有语义);② 新增 `/internal` 媒体登记端点(建议 `POST /internal/media/assets`),一次调用完成「写对象 + 插入 `media.assets` 行并直接置 `ready`」,供生成域落盘输出——**这样 ADR-017「media 上传流程实现在 patbond-user」的边界不被打破**;③ `MediaProperties.allowedPurposes``MediaProperties.java:66`,当前 `post_image,user_avatar,pet_avatar`YAML `application.yml:45`)与 mime 白名单按 D4-14 扩充新用途(**纯配置变更,`media.assets.purpose` 无 CHECK 约束,无需迁移**——ADR-022 已验证同一路径)。若 D4-2 选择并入 user,②可退化为内部服务方法调用。
- **验收标准**
- MinIO Testcontainer 全链路:服务端 put → `media.assets``status='ready'``ready_at` 非空 → 预签名 GET 可取回同一字节(sha256 比对)。
- `/internal/media/assets``X-Internal-Token` / 错误 token 返回 401 + `TOKEN_INVALID`(复用 `InternalAuthFilter.java:28,54-56` 的常量时间比较,**未配置密钥时 fail closed** 语义不变)。
- `ck_media_location` / `ck_media_ready` / `uq_media_object (bucket, object_key)` 四条数据库约束与应用层校验一致,各有测试。
- 未配置对象存储时行为与既有一致(500,不静默成功)。
- **依赖**:T4-01(不强依赖,可与之并行);D4-2 决定②的形态;D4-14 决定新 purpose 命名。
- **规模**M
#### T4-04 模型/风格只读目录端点与种子数据
- **仓库**patbond-apipatbond-doc(契约草案)
- **描述**:实现模型与风格的只读目录(建议 `GET /api/v1/creation/models``GET /api/v1/creation/styles`,均支持 `mediaKind` 过滤,`enabled=true` 过滤走 partial 索引 `ix_generation_models_kind_order` / `ix_generation_styles_kind_order`,按 `sort_order, id` 排序,**不分页**——沿 care-reminders/breeds/vaccine-catalog 的既有例外先例 `openapi.yaml:98`)。风格响应含 `previewUrl``preview_asset_id` 现签预签名 GET,与 `Pet.avatarUrl`/`AuthorSummary.avatarUrl` 同口径:三种情况为 null、键恒在)。种子按 D4-5 只入 image 模型与风格(目标模型 :1495-1508 的 5 条风格里 4 条为 image、1 条为 video);`provider_code` 取值随 D4-1。**风格预览图本身需要真实资产**:种子引用的 `preview_asset_id` 在目标模型里指向 fixture asset,本单需明确预览图来源(建议随种子上传 4 张占位图并记录 assetId,或首版允许 `previewUrl` 为 null 由客户端回落纯色卡)。
- **验收标准**:目录端点返回按 `sort_order` 稳定排序;`enabled=false` 的行不出现;`mediaKind` 非法值 400/40000`previewUrl` 为 null 时键仍在;六类测试路径中适用的四类(成功/参数错/无权限/不存在)覆盖。
- **依赖**T4-01T4-02。
- **规模**S
#### T4-05 provider 适配层与首个 provider 实现
- **仓库**patbond-apipatbond-docprovider 接入说明 + ADR-023
- **描述**:**依赖 D4-1 拍板,是关键路径起点**。定义 provider 抽象接口(提交生成请求 → 返回 `providerRequestId`;轮询或回调获取状态与结果字节/URL;显式区分**可重试失败**与**永久失败**,后者不再消耗 `max_attempts`);把供应商差异全部收敛在适配层(沿 ADR-016 对象存储适配层的同一手法:「代码经存储适配层隔离供应商」)。按 D4-1 实现首个 provider。**PM 建议先实现 fixture provider**:可配置延时与进度、返回内置图片字节、可注入失败/超时/慢响应用于测试——它同时是 CI 与 E2E 的唯一可用 provider(无外部依赖、无密钥、无成本)。若 D4-1 同时批准接入云 API,则云 provider 作为第二实现另计(见 T4-05b 说明:不单列工单,按 D4-1 结果并入本单并把规模提为 L)。
- **验收标准**
- 适配层接口下至少一个实现全链路可跑;provider 密钥一律经 `.env` 注入、`.sample` 占位,`scripts/check-secrets.sh --all` 零命中(凭证防泄漏两层门禁在位,`git-workflow.md:36-54`)。
- fixture provider 可注入四类故障:可重试失败、永久失败、超时、返回损坏字节;各有测试。
- 日志不含 provider 密钥与完整 prompt 明文(`development-plan.md` 第 6.1 节日志纪律)。
- **依赖**:**D4-1 拍板**(未拍板期间可先写接口与 fixture 实现,明确止损线:适配层以上不写任何供应商特定代码)。
- **规模**:M(若同时接云 API 则 L)
### B 组:后端生成纵切
#### T4-06 生成任务创建:幂等键、快照与配额
- **仓库**patbond-api
- **描述**`POST /api/v1/creation/jobs`**`Idempotency-Key` 必带**`development-plan.md:173` 强制;沿 `POST /api/v1/posts` 的既有形态 `openapi.yaml:1320``IdempotencyKeyRequiredHeader` :1928-1944,同键异 payload → 409/40905)。落 `UNIQUE (user_id, idempotency_key)` + `request_hash`(规范化请求体的 sha256,32 字节,`ck_generation_jobs_idempotency` 校验)。校验链:模型/风格存在且 `enabled``media_kind` 一致(复合外键 `(model_id, media_kind)` 天然兜底)、输入 asset 属本人且 `status='ready'`(复用 community 的 `MediaAssetGateway` 只读模式;非 ready → 422/42203 沿用既有 `MEDIA_NOT_READY`)、宠物归属校验(若带 `petId`)、尺寸在 `ck_generation_jobs_dimensions` 的 64~8192 内。**四个 snapshot 列在创建时一次性冻结**(`provider_code_snapshot` / `provider_model_snapshot` / `model_version_snapshot` / `style_code_snapshot`)——这是「失败原因可追踪」的基础:目录改了不影响历史任务的可复现性。配额检查按 D4-7(PM 建议直接 `COUNT` 本人窗口内的 `generation_jobs` 行,**不新建计数表**:幂等命中不新建行,故天然满足「相同幂等请求不重复扣费」)。任务落库即 `status='queued'`(满足 `ck_generation_jobs_state` 的 queued 分支:`progress=0``started_at`/`completed_at`/`output_asset_id`/`error_code`/`lease_*` 全 NULL)。
- **验收标准**
- 六类测试路径全覆盖(成功 / 参数错 / 资源不存在 / 无权限 / 并发冲突 / 幂等重试),走真实 PostgreSQL。
- **相同 `Idempotency-Key` 并发 N 次只产生一行**,且响应为同一 `jobId`(多线程真并发测试,沿 `LikeBookmarkIntegrationTest.java:141,167` 的既有手法);同键异 payload 返回 409/40905。
- 引用他人 / 非 ready asset 被拒且错误码稳定;`enabled=false` 的模型被拒。
- 超配额返回 429 + `Retry-After`(依赖 T4-11);幂等重试**不**消耗配额,有专项测试。
- snapshot 四列非空且与创建时刻的目录值一致;事后改目录不改历史行,有测试。
- **依赖**T4-01、T4-02、T4-04、T4-05(接口即可,不需 provider 实现完成);D4-7 拍板配额口径。
- **规模**L
#### T4-07 生成任务查询、列表与取消
- **仓库**patbond-api
- **描述**`GET /api/v1/creation/jobs/{jobId}`(详情,含 `status`/`progress`/`errorCode`/`errorMessage`/`attemptCount`/`outputAsset` 现签 URL)、`GET /api/v1/creation/jobs`(我的生成记录,cursor 分页走 `ix_generation_jobs_user_created (user_id, created_at DESC, id DESC)`**禁 OFFSET**,支持 `status` 白名单过滤)、`DELETE``POST .../cancel`(取消,语义随 D4-8 子项)。取消的状态机后果必须逐条对齐 `ck_generation_jobs_state`:667-692):`cancelled` 要求 `completed_at IS NOT NULL``lease_*` 全 NULL——因此**取消 `queued` 任务需同时写 `completed_at`**;取消 `running` 任务需清租约(此时 provider 侧可能仍在跑,须明确「取消是尽力而为,已产生的 provider 消耗不退配额」并写入契约描述);`succeeded`/`failed`/`cancelled` 的重复取消为幂等 no-op 还是 422,随 D4-8 定型。他人任务一律 404(防枚举,沿 `POST_NOT_FOUND` 的既有做法 `openapi.yaml:45`)。新增错误码建议:`GENERATION_JOB_NOT_FOUND(40407, 404)``GENERATION_STATE_INVALID(42206, 422)`
- **验收标准**
- 五个状态各自的详情响应形态定型并入契约;`errorCode`/`errorMessage` 在 failed 态必非空、在其他态必为 null(与库层 CHECK 互证)。
- 分页不丢不重:以 `limit=7` 多页与 `limit=100` 单页两次全量翻页,**有序 id 列表逐位相等**(沿 M3 T3-05 的取证手法 `iteration-3/29:39`)。
- 取消四种源状态(queued / running / 已终态 / 他人任务)各有测试;取消后库层 CHECK 不被违反。
- 六类测试路径覆盖。
- **依赖**T4-06。可与 T4-08 并行。
- **规模**M
#### T4-08 Worker 队列消费:租约、退避重试与幂等
- **仓库**patbond-apipatbond-docWorker 运行手册 + ADR-023
- **描述**:**M4 的技术核心,两条验收标准(状态流转合法、Worker 重启不丢任务)的主责单**。按 D4-3 实现 DB 队列消费(PM 建议):
- **领取**`SELECT … FROM creation.generation_jobs WHERE status='queued' AND next_attempt_at <= now() ORDER BY priority DESC, next_attempt_at, created_at, id LIMIT :n FOR UPDATE SKIP LOCKED`(正对 `ix_generation_jobs_queue` :711-713),随即写 `status='running'` + `started_at` + `lease_owner`(实例标识)+ `lease_expires_at`
- **续租**:长任务周期性延长 `lease_expires_at` 并更新 `progress`
- **回收**:独立扫描 `status='running' AND lease_expires_at < now()`(正对 `ix_generation_jobs_running` :714-715),重置为 `queued`。**⚠️ 实现陷阱(§2.8**`ck_generation_jobs_state` 的 queued 分支要求 `started_at IS NULL AND progress = 0`,故回收时必须把 `started_at``progress` 清零——首次尝试的开始时间会丢失,须在代码注释与迁移说明中记录该取舍。
- **重试**`attempt_count + 1`;未达 `max_attempts` 且属可重试失败 → 回 `queued` 并按指数退避写 `next_attempt_at`;达上限或永久失败 → `failed` + `error_code`/`error_message` + `completed_at`
- **provider 幂等**`provider_request_id` 落库并受 `uq_generation_jobs_provider_request` 保护(:699-700),重试时优先**查询已有 provider 请求状态**而非重新提交——这是「相同幂等请求不重复扣费或生成」在 provider 侧的落点。
- **调度基座**`@EnableScheduling` + 显式 `TaskExecutor`(全仓当前无任何线程池配置,§2.4),并发度、租约时长、退避参数、轮询间隔全部配置化。Worker 形态(同进程 vs 独立容器)随 D4-2。
- **验收标准**
- **Worker 重启不丢任务**:集成测试在 `running` 态强杀 Worker(或直接令租约过期)后,任务被重新领取并最终 `succeeded`T4-25 E2E 用 `docker compose restart` 做真容器取证。
- 多 Worker 并发领取同一批任务,**同一任务不被两个实例同时持有**(`SKIP LOCKED` + 租约双重保证),有并发测试。
- 四类 provider 故障(可重试 / 永久 / 超时 / 损坏输出,由 T4-05 fixture 注入)各自的终态与 `attempt_count` 正确;退避间隔递增有测试。
- 全部状态迁移穷举测试:每条合法迁移成功、每条非法迁移被应用层拒绝**且**被库层 CHECK 兜底(两层各有断言)。
- 队列积压与 Worker 失败率**可查**`development-plan.md:329` 的最小兑现:SQL 可查 + 结构化日志留痕;完整指标栈 M6,见 §7)。
- **依赖**T4-06、T4-05T4-03(输出落盘经 T4-09);D4-2、D4-3 拍板。
- **规模**L
#### T4-09 生成输出落媒体表与真实尺寸回填(含 `widthPx/heightPx` 遗留清偿)
- **仓库**patbond-apipatbond-doc(契约描述修订)
- **描述**Worker 成功后把 provider 输出经 T4-03 的写能力落 `media.assets``purpose` 随 D4-14、`owner_user_id` = 请求用户、`storage_type='object'``status='ready'` + `ready_at`),回填 `generation_jobs.output_asset_id` 并置 `succeeded``ck_generation_jobs_state` 要求 succeeded 时 `progress=100``started_at`/`completed_at`/`output_asset_id` 非空、`error_code`/`lease_*` 为 NULL——五个条件同事务写齐)。**AI 输出的 `width_px`/`height_px` 天然已知**(请求参数 + provider 返回),直接写入。同时按 D4-9 处置 M3 遗留:PM 建议在 `POST /api/v1/media/uploads/{assetId}/complete` 里补服务端尺寸探测(读对象头部字节解析 JPEG/PNG/WebP 尺寸),使契约 `openapi.yaml:3536` 的「complete 后回填」名副其实;同时把 `PostMediaItem.widthPx/heightPx`:3605-3610,当前无 description)补上口径说明。**本单不做**:视频时长探测(D4-5 剪出视频)。
- **验收标准**
- 生成成功后 `media.assets``status='ready'``width_px`/`height_px` 与请求尺寸一致;预签名 GET 可取回字节。
- 五个 succeeded 字段条件同事务写齐;中途失败不留「有 output 但非 succeeded」的中间态,有测试。
- 用户上传路径的尺寸回填:jpeg/png/webp 三种 mime 各有测试,探测失败时回落 null 且不影响 `ready`(向后兼容,`MediaAsset.required` 不含尺寸字段)。
- 单图帖不再一律回落 4:3(M3 观察项 2 闭环,`iteration-3/29:56`)——客户端侧验收在 T4-19/T4-21。
- **依赖**T4-03、T4-08D4-9 拍板。
- **规模**M
#### T4-10 一键建社区草稿:`ai_creation` 写侧开放与 `generationJobId` 打通
- **仓库**patbond-api`patbond-community`),patbond-doc(契约)
- **描述**:打通「生成成功 → 社区草稿」。改动面(§2.7 已逐处定位):① `CreatePostRequest.java:26``@Pattern(regexp = "general|help")` 放开为含 `ai_creation`;② `CreatePostRequest` 新增 `generationJobId`;③ `PostRepository.insertPost`:56-58 签名、:60-64 INSERT 列清单)与 `PostService`:88 category 默认值、:98/:102 canonicalize 与插入、:151/:178 更新路径)补该列;④ `PostResponse.java``FeedCardResponse` 按 D4-8 决定是否外露 `generationJob`(当前 `PostResponse.java:12` 注明该字段完全不出现)。**归属与防伪校验**:`generationJobId` 必须属于当前用户且 `status='succeeded'`,否则 404/40407;按 D4-8 定型「`category='ai_creation'` 是否强制要求带 `generationJobId`」(PM 建议强制,防止用户伪造 AI 分类)。草稿创建沿用既有两步语义(`status='draft'` 创建 → `PATCH status='published'` 发布,`openapi.yaml:3660-3664`、:3701-3707),本单**不新增发布路径**。
- **验收标准**
- 生成任务 → 建草稿 → 发布 → Feed 可见全链路走真实 PostgreSQL`posts.generation_job_id` 落库正确且外键约束生效(引用不存在的 jobId 被库层拒绝)。
- 他人 jobId / 未成功 jobId / 无 jobId 但 `category='ai_creation'` 三种非法组合各有测试与稳定错误码。
- `ai_creation` 帖在 Feed 与详情的读侧形态与 `general` 一致(`FeedCard.category` 枚举早已含三值 `openapi.yaml:3826`,无需改读侧枚举)。
- 既有 107 个 community 测试不回归;契约矩阵新增格全部被真实触发(`everyDeclaredResponseCellIsExercised` 门禁)。
- **依赖**T4-01(外键)、T4-06/T4-09succeeded 任务);D4-8 拍板。
- **规模**M
#### T4-11 生成域配额与限流:429 + `Retry-After`(清偿 M3 遗留)
- **仓库**patbond-api`patbond-common` 加错误码 + 生成域实现),patbond-doc(契约首个响应头)
- **描述**:**契约史上第一个响应头声明**——`openapi.yaml` 全文当前**零处 `headers:` 键**(§2.13 核实)。新增 `RATE_LIMITED(42900, 429, …)``ErrorCode.java`(该文件当前无 429 段),按 D4-7 实现配额与限流:建议 per-user 并发上限(`COUNT` 本人 `queued|running` 行)+ 每日次数上限(`COUNT` 本人当日 `created_at` 行),**均直接查 `generation_jobs` 不新建表**;超限返回 429/42900 + `Retry-After`(秒数,指向下一次可提交时间)。**无 Redis 的现实约束**(§2.4)决定了计数只能落 PG 或进程内——建议落 PG(多实例正确,代价是每次创建多一次 COUNT,走 `ix_generation_jobs_user_created`)。同时清偿客户端侧遗留:`lib/core/network/api_client.dart:164-167` 已有 429 粗分支但无 `Retry-After` 解析,`lib/analytics/analytics_service.dart:268-272` 注释明写等待后端落地——本单交付后由 T4-16 收口客户端分支。**本单不做**:全站通用限流(属 M6「限流、审计、结构化日志」范围),只做生成域。
- **验收标准**
- 超并发 / 超日限两种超限各返回 429 + `Retry-After`,头值可解析为正整数秒;有集成测试。
- **幂等重试不计入配额**(同 `Idempotency-Key` 重试在超限后仍返回原任务而非 429),有专项测试——这是「相同幂等请求不重复扣费」的一半取证。
- 契约新增 429 响应与 `Retry-After` 头声明;契约测试覆盖该格(新增矩阵格必须被真实触发)。
- 客户端 429 分支与 `Retry-After` 尊重在 T4-16 有测试。
- **依赖**T4-06D4-7 拍板具体数值。
- **规模**M
#### T4-12 uploading 超时未确认 asset 清理定时任务(清偿 M3 遗留)
- **仓库**patbond-api`patbond-user`),patbond-docfeature-checklist 转绿)
- **描述**:M3 T3-13 已有方案未实现(`iteration-3/29:58``feature-checklist.md:193`)。本单实现 `@Scheduled` 清理:把超过阈值仍为 `status='uploading'` 的 asset 置 `failed`(或按方案置 `deleted` + `deleted_at`,须满足 `ck_media_deleted`),并按需删除孤儿对象。**搭车理由**:M4 本就要建 Worker 调度基座(T4-08),且 `@Scheduled` 已有先例可照抄(`SessionCleanupJob.java:32-41` 的幂等、可多实例并跑写法 + `application.yml:24-26` 的配置形态);同时 AI 输入图会放大孤儿资产量(每次生成都要先传一张图),此项从「可选清理」升为「成本控制项」。索引已就绪:`ix_media_uploading_created`V1:255 区域)。
- **验收标准**:阈值内的 uploading 不被误清;超阈值被置终态且 `identity.users.avatar_asset_id` / `pet_health.pets.avatar_asset_id` / `post_media` / `generation_jobs.input_asset_id` 等引用方**不出现悬空引用**(外键为 RESTRICT 的路径须验证清理不会失败);任务幂等、多实例并跑无害,有测试。
- **依赖**T4-02(或直接在 user 模块内);与主线解耦,可任意波次插入。
- **规模**S
### C 组:契约与测试
#### T4-13 OpenAPI v1.5.0 扩展与冻结(**本波闸门**)
- **仓库**patbond-doc`docs/api/openapi.yaml`),patbond-api(四份或五份快照字节级同步 + 守卫测试期望值更新)
- **描述**:沿用三个迭代验证过的**迭代式契约冻结**:第一波按 §1.1 与目标模型出草案(放在 `iterations/iteration-4/openapi-creation-draft.yaml`,沿 M2/M3 的 `openapi-pets-draft.yaml`/`openapi-community-draft.yaml` 先例),以 `TODO-FREEZE` 标注**四处待定型点**——① 生成任务响应的五态字段形态、② `Retry-After` 头与 429 的声明形态(契约首个响应头)、③ `generationJob``Post`/`FeedCard` 的外露程度、④ 新 `purpose` 枚举值命名;随 T4-06/07/09/10/11 实现定型回填 → 拍板 → 冻结合入 + api 侧快照同步升版。
变更清单(**对 v1.4.0 应为纯增量**,延续 v1.4.0 写入 `info.description` 的承诺):新增生成域路径与 schema;`CreateMediaUploadRequest.purpose` enum 增值(:3458);`CreatePostRequest.category` enum 增 `ai_creation`:3657,并删去 :3659 那句「M3 不开放写入」);`CreatePostRequest`/`Post` 新增 `generationJobId`/`generationJob``MediaAsset.widthPx` description 修订(:3536);新增错误码 `40407`/`42206`/`42900``info.description` 错误码表(:35-62,现 27 个码);新增 429 响应与 `Retry-After` 头。
**同步义务(缺一 CI 必红)**:四份守卫测试的期望值 `1.4.0 / 32 / 45 / 75` 必须整体更新(`AuthContractConformanceTest.java:399-407``MediaContractConformanceTest.java:246-251`、pet `ContractConformanceTest.java:755-760``CommunityContractConformanceTest.java:522-527`),四份 `OpenApiContract.java``RESOURCE` 常量(pet:35 / auth:39 / user:39 / community:39)须指向 `openapi-v1.5.0.yaml`;若 D4-2 新建模块则为**五份**。
- **验收标准**:契约评审通过;四/五份快照 md5 与正典完全一致;守卫测试与 `everyDeclaredResponseCellIsExercised` 双门禁全绿、矩阵零漂移;新增格全部被真实触发(不得豁免);`mkdocs build --strict` 零 warning;冻结后任何变更须显著上报两端同步。
- **依赖**:草案仅依赖目标模型;冻结须 T4-06/07 状态形态 + T4-09 输出形态 + T4-10 权限语义 + T4-11 限流形态定型。**不冻结不放行第三波两端联调。**
- **规模**M
#### T4-14 契约快照锁补强:从「计数守恒」到真校验和
- **仓库**patbond-api(四/五模块测试),patbond-doc(冻结纪律补章)
- **描述**:§2.13 核实的缺口——CI 只锁 `info.version` + 3 个计数 + tag 分组,md5 比对是**人工冻结步骤**,任何「计数守恒的字节修改」(改 description、改 enum 取值、改 min/max、同时增删各一字段)都能悄悄通过。本单在每模块守卫测试中补一条**快照文件校验和断言**(对 `src/test/resources/contract/openapi-v1.x.y.yaml` 算 sha256 与硬编码期望值比对),把「字节级快照锁」从人工纪律变成 CI 门禁。**注意**:期望值需在 T4-13 冻结后填入,故本单分两步——先在 v1.4.0 上落机制并验证能抓出注毒(可先行于第一波),再随 T4-13 更新期望值。
- **验收标准**:定向注毒自证——对快照做一处「计数守恒」的字节修改(如改一句 description),断言必红;恢复后转绿。四/五模块各有该断言。文档纪律更新为「升版须同步快照 + 更新校验和」。
- **依赖**:无(机制可先落);期望值随 T4-13。
- **规模**S
#### T4-15 后端集成测试滚动补齐与 CI(横切单)
- **仓库**patbond-api
- **描述**:随 B 组滚动补齐 Testcontainers 集成测试与契约一致性测试(机制复用既有三层结构:纯单测 / 契约一致性 / Testcontainers postgres:18 + 一处 MinIO 容器)。**本迭代专项矩阵**:① 状态机穷举(五态两两迁移的合法与非法各一格);② 队列并发(多 Worker 领取不重叠、`SKIP LOCKED` 有效性);③ 租约过期回收(含 `started_at` 清零后仍满足 CHECK);④ 幂等三层(应用层键、DB 唯一约束、provider 请求 id);⑤ 配额边界(超限 / 幂等重试不计数 / 跨日窗口翻转)。MinIO 容器使用面因 T4-03/T4-09 扩大,记录 CI 时长;若超阈值按模块分层执行,但**不降低「提交前全绿」标准**。
- **验收标准**:每个新业务接口覆盖六类路径;Gitea Actions 全绿(以 commit status API 实查为准,沿 M2/M3 惯例——`iteration-3/29:47` 记录过「一个 agent 识破 Monitor 的假 success 事件」的教训);CI 时长记录在案。
- **依赖**:随 T4-04~T4-12 滚动。
- **规模**M(分摊在各单内)
### D 组:Flutter 客户端
#### T4-16 `creation` feature 分层与 API Client
- **仓库**patbond-flutter
- **描述**:按 `development-plan.md:96` 已预留的 `creation` feature 拆分(`lib/features/creation/`),对齐 community/pets 的既有结构(`Page → Controller → Repository → ApiClient`,约定原文 `community_controller.dart:14-20`;状态用原生 `ChangeNotifier`DI 手写进 `lib/app/app.dart:80-147`)。依 T4-13 冻结契约实现 DTO 与 Client(模型/风格目录、任务创建/详情/列表/取消)。**若 D4-2 新建模块,须新增第五条 baseUrl**:`patbondCreationApiBaseUrl` + `PATBOND_CREATION_API_BASE_URL`(照抄 `api_client.dart:8-34` 的四条现有写法),并同步 `lib/app/app.dart` 装配、四份 E2E 脚本的地址常量、`device-verification.md` 通用前置的 `flutter run` 参数说明(当前写「四个 base URL 全传」,须改为五个)。新增 `AnalyticsPageName` 枚举值(`lib/analytics/analytics_page_name.dart:7-25`;不在枚举内 `fromRouteName` 返回 null 即不上报)。**顺带收口 429**:`api_client.dart:164-167` 现有的 `ApiRateLimitException``Retry-After` 解析与承载(T4-11 交付后),并按注释 `analytics_service.dart:268-272` 的承诺一并处理埋点上传侧的 `Retry-After` 分支。
- **验收标准**DTO 映射(含五种 status、null 尺寸、null previewUrl)有单元测试;错误映射类型化(新增 `40407`/`42206`/`42900` 各有分支);`Retry-After` 解析(有头 / 无头 / 非法值)有测试;UI 无关骨架可先行。
- **依赖**:T4-13 冻结(骨架部分可提前与后端并行)。
- **规模**M
#### T4-17 create 页替换:真实目录、输入与提交
- **仓库**patbond-flutter
- **描述**:替换 `lib/features/create/create_page.dart`(563 行、零测试、全假,逐处清单见 §2.14)。保留可用的四块视觉骨架(模式分段器 / 风格横滑卡 / 进度条 / 结果编辑区),只换数据源与状态源:模型下拉与风格卡改读 T4-04 目录(删掉 `:179` 硬编码模型数组与 `demo_data.dart:206-234` 的 unsplash URL 依赖);假上传(`:55-64`)改接 `MediaUploader``purpose` 用 T4-03 新增的 AI 输入用途,单图单并发,照抄头像 sheet 的 `maxImages:1, maxConcurrentUploads:1` 用法);prompt 输入、尺寸与高清增强开关映射到真实请求字段(`prompt`/`negativePrompt`/`widthPx`/`heightPx`/`upscale`/`parameters`);提交携带客户端生成并在重试间保持的 `Idempotency-Key`(照抄发布页 `post_compose_page.dart:267``_idempotencyKey ??= _uuid.v4()` 手法)。**视频模式按 D4-5 处理**(建议:分段器保留但 video 项禁用并给「即将上线」提示,而非删除——避免 UI 大改,也与 `media_kind` 保留枚举一致)。**搭车主题债**:给主题补 `segmentedButtonTheme`,选中态改用 `app_theme.dart:199-214` 已审计色对表中的色对(`_datePickerTheme` :215-296 是落地范本),一次修复全部 6 处 `SegmentedButton` 使用点(create/home/services/pet_form×2/vaccination_form)。
- **验收标准**
- 页面不再读任何 demo 常量(`demo_data.dart``AppState.pet` 依赖清零);`grep` 断言无 `Future.delayed` 假延时残留。
- 目录加载四态(loading/empty/error/retry)齐备并有 widget 测试——**该页当前零测试,本单是从 0 建测试基线**。
- 提交校验与错误提示:非法尺寸、未选风格、未传图、超配额 429(含 `Retry-After` 文案)各有测试。
- `SegmentedButton` 选中态对比度达 WCAG AA 并在已审计色对表中登记新增行;6 处使用点视觉一致。
- **依赖**T4-16T4-04/T4-06 联调。
- **规模**L
#### T4-18 生成状态机 UI:排队、进度、失败重试与取消
- **仓库**patbond-flutter
- **描述**`development-plan.md:253` 的直接落点(「Flutter 展示排队、生成进度、失败重试和取消状态」)。实现任务状态轮询与展示:`queued`(排队中 + 队列位置或预计时间,视 T4-07 是否外露)、`running`(真实 `progress` 驱动进度条,替换 `:532-563` 现有的 `step/4` 假进度与 4 条硬编码 label)、`succeeded`(转 T4-19)、`failed``errorCode`/`errorMessage` 可读化 + 重试按钮,重试语义随 D4-8:**是新建任务还是复用同一任务**须定型)、`cancelled`。轮询策略需明确:间隔、退避、页面不可见时暂停、离页后是否继续(建议离页即停 + 回页重取,避免后台耗电);**尊重 `Retry-After`**。断网与超时的四态齐备。
- **验收标准**
- 五种状态各有 widget 测试;状态迁移驱动的 UI 变化(queued→running→succeeded / →failed)有测试。
- 轮询在页面不可见时暂停、回前台恢复,有测试(M3 曾用 widget 测试抓出「回前台不开新曝光段」的真 bug,`iteration-3/29:46`——同类风险在此复现,须专项覆盖)。
- 失败重试与取消的乐观/悲观策略明确且不假成功(离线操作提示明确,沿 M3 T3-17 的既有纪律)。
- `progress` 为真实值(非一跳 100%),有测试。
- **依赖**T4-16、T4-17T4-07/T4-08 联调。
- **规模**L
#### T4-19 结果页与一键建社区草稿
- **仓库**patbond-flutter
- **描述**:生成成功后的结果展示与去向。复用现有结果区布局(`create_page.dart:312-386`)但换真实数据:输出图用 `outputAsset.url`(预签名 GET,**缓存 key 须剥 `X-Amz-*` 签名参数**——沿用已有的 `presignedImageCacheKey` 机制,否则每次刷新重下,该机制的回归判据已写进 `device-verification.md:190` 第 2 项);按真实 `widthPx/heightPx` 渲染宽高比(T4-09 交付后不再一律 4:3);删掉硬编码位置「北京市 · 朝阳区」(`:369-374`)与硬编码文案(`:87-90`)。「发布到社区」从占位 SnackBar(`:122-129`)改为真实调用 T4-10:建草稿并跳转 `PostComposePage` 预填(图与 `generationJobId` 已挂),由用户在既有发布页完成文案与发布——**不在本页做完整发布流程**(避免与发布页的草稿/幂等/媒体逻辑重复实现)。
- **验收标准**:结果图真实渲染且宽高比正确;预签名 URL 不被持久化(纪律 R2)、过期可重取;一键建草稿 → 发布页预填 → 发布 → Feed 出现全链路真实后端;失败四态齐备;有 widget 测试。
- **依赖**T4-16、T4-18T4-09/T4-10 联调。
- **规模**M
#### T4-20 我的生成记录列表(条件单,随 D4-11 之外的独立取舍)
- **仓库**patbond-flutter
- **描述**:若纳入:接 T4-07 的列表端点做「我的作品/生成记录」页(cursor 分页四态,照抄 `lib/features/pets/health_events_page.dart:19,71-127``_ListPhase` 范本),支持按 status 过滤、点击进详情、失败任务可重试、可删除记录(随 D4-14)。**PM 倾向纳入**:它是用户排查失败任务与找回历史输出的唯一入口,缺它则一次生成失败后用户无处可查(「失败原因可追踪」在客户端侧没有落点)。若周期紧张可后置,但须在收官总结记范围缺口。
- **验收标准**:分页不丢不重(widget 测试模拟游标);四态齐备;状态与详情页同源一致。
- **依赖**T4-16、T4-18。
- **规模**S
#### T4-21 我的收藏与草稿两页(M3.5 遗留搭车)
- **仓库**patbond-flutter
- **描述**:清偿 `iteration-3.5/06:71` 遗留(「后端与仓库层均已就位,缺两个页面 + 导航」)。两页均照抄 `_ListPhase` + cursor 分页范本:**我的收藏**接 `listMyBookmarks()``community_repository.dart:52,274-283`**当前全仓零调用点**,项形态为 `FeedCard`);**我的草稿**接 `listMyPosts(status: draft)`:26-30,179-193,当前唯一调用点是发布页取 `limit:1` 恢复最新草稿)。入口替换 `lib/features/profile/profile_page.dart:49` 菜单项的 demo SnackBar:235 `showDemoMessage`);顺带把「我的作品」统计数字(`:307-310`,当前不可点)接成可点入草稿/已发布列表。**本单不做**:草稿自动保存(见 §7)。
- **验收标准**:两页分页不丢不重、四态齐备、有 widget 测试;收藏页的取消收藏与 Feed/详情状态同源一致(复用既有 `toggle_sync.dart` 乐观更新机制);profile 页不再有该项 demo SnackBar(有反向断言)。
- **依赖**:无(与 M4 契约零耦合,第一波即可并行消化)。
- **规模**M
### E 组:埋点、实验、遗留与收口
#### T4-22 埋点字典 v4AI 创作漏斗 + `eventVersion` 口径定型 + 白名单计数纠偏
- **仓库**patbond-flutter(挂接)、patbond-api(白名单扩充)、patbond-doc(字典)
- **描述**:事件定义以 Experiment Tracker 的 M4 埋点方案为准(**本单不自造字典**;沿用「结果编码进事件名」惯例与 ADR-013 纪律)。预期覆盖 `development-plan.md:330` 要求的「AI 任务成功」漏斗:创作页进入 → 输入图上传(复用既有 `post_media_upload_*` 三段还是新增独立事件,由埋点角色定)→ 任务提交成功/失败 → 生成成功/失败/取消 → 建草稿 → 发布。**隐私红线**:props 不含 prompt 明文、不含 assetId/jobId、不含精确字节数(沿 M3 红线,服务端 `EventDictionary.java:38-39,114-116` + 客户端 `analytics_service.dart:288-295` 双层过滤在位)。
搭车三项修正:① **`eventVersion` 口径定型**M3 观察项 2`iteration-3/29:57``feature-checklist.md:222`)——契约描述 `openapi.yaml:2341-2344` 可两读、服务端零取值校验(`TrackEventsRequest.java:38-39``@NotNull``@Min`,唯一取值校验在 DB 的 `V2:22`)、客户端硬编码 1`analytics_service.dart:139`);定型为「事件 schema 版本」、写入字典纪律、并决定是否补 `@Min(1)` 与契约 `minimum`。② **白名单计数纠偏**:实测 41 条而文档三处写 42(§2.10),修正 `iteration-3/22-event-whitelist-v3.md:4,14``iteration-3/29-m3-summary.md:20``feature-checklist.md:218`,并在 `EventDictionaryTest.java` 补一条**总量断言**(当前无任何 `hasSize`)永久钉住。③ 新增事件同步进 v4 字典文档与 `EventDictionary.java`,并对刻意不做的事件名(如逐帖曝光类)维持「不在白名单即 unknown」的锁死机制。
- **验收标准**:关键动作事件端到端落库(compose 实测或 curl 造真实 payload,沿 M3 的 §5c 取证惯例);白名单与字典文档条数一致且有总量断言;`eventVersion` 定型结论写入字典纪律与契约描述;三处文档计数修正;不回归既有 597 前端测试。
- **依赖**Experiment Tracker 方案;随 T4-17~T4-19 滚动挂接。
- **规模**M
#### T4-23 A/B 最小分流设施与首个实验(条件单,随 D4-11)
- **仓库**patbond-flutter、patbond-api、patbond-doc
- **描述**:若 D4-11 拍板启动首个实验,须先补 §2.9 核实的三处零实现:① **稳定分流组件**(前置 #4 的缺失半边)——`hash(userId 或 anonymousId, experimentSalt) % buckets`,建议**纯客户端确定性哈希**`anonymousId` 持久化已在 `analytics_service.dart:51,177-190` 就位),避免为一个实验搭服务端下发设施;② **Flutter 曝光封装**(前置 #5 的缺失半边)——`experiment_exposed` 事件的强类型封装(服务端字典 `EventDictionary.java:103` 已就位、props `{experimentKey, variant}`,客户端零命中),触发时机按 `iteration-3/06:145` 的既定口径「用户**实际到达**实验触点时(渲染了变体 UI),非分配时」;③ **开关与回滚**(前置 #7 的「回滚绿」实无依据)——最小实现为客户端可远程或配置关闭实验、回落对照组。首个实验对象建议取**低风险、纯展示层**的项(如 create 页风格卡的默认排序,或结果页「发布到社区」的引导文案),**不实验后端生成逻辑**(成本与状态机风险)。
- **验收标准**:同一用户多次进入分到同一变体(确定性有测试);`experiment_exposed` 落库且 props 键集与字典一致;关闭开关后全量回落对照组,有测试;实验设计与样本量按既有模板评审(**注意**:该模板文档在两仓中未定位到实体,见 §2.18,须先补或现场约定)。
- **依赖**D4-11 拍板;T4-17(触点所在页面);T4-22(字典)。
- **规模**M
#### T4-24 真机验证 M2 两项(**硬时限 2026-09-21**
- **仓库**patbond-doc(执行记录回填)
- **描述**:**本迭代唯一带外部硬时限的工单**。执行 `device-verification.md:42`(验证一:Android 事件真实落库,~10 分钟)与 `:63`(验证二:SessionTracker 30 分钟后台换会话,~45 分钟含等待),回填 `:104` 的「M2 项执行记录」。时限来自北极星首次出数日 2026-09-21(`iteration-2/06-experiment-tracking-plan.md:308-312`)与埋点角色的提醒(`iteration-3/29:52`:未完成则首批读数只能标未验收)。**今日 09-14,仅剩 7 天,本单必须置于第一波首日。**
**降级方案(做不到时)**:① 首批北极星读数(W37 队列)在数据报告中显式标注「埋点落库未经 Android 真机验收,读数仅作趋势参考」;② 把复评点推到下一个成熟窗口(约 H14,即再迟 7 天);③ 用桌面/模拟器覆盖能覆盖的部分并明确记录**不可替代的缺口**——注意 `device-verification.md:144` 已言明 Linux 桌面的 `platform=linux` 不在契约枚举内、整批 400 被拒,故**事件落库这一项桌面根本无法替代**,降级只能降到「标注未验收」,不能降到「换环境验证」。
- **验收标准**:两项的通过标准(`:59-61``:82-83`)逐条勾选并回填执行记录(日期、机型/Android 版本、psql 输出脱敏摘录、执行人);若未执行,则降级方案的三项动作全部落地并在收官总结显式记录。
- **依赖**Android 真机到位(**外部依赖,PM 无法消除**,故列 D4-13 请用户拍板)。
- **规模**S
#### T4-25 第五份 E2E 脚本:`test_e2e_m4_manual.dart`
- **仓库**patbond-flutter(脚本)、patbond-apicompose 环境)、patbond-doc(证据归档)
- **描述**:按 `releases.md:56` 的既定原则(「每个引入对外端点的迭代都应有对应的 E2E 脚本,并在此后每次发布回归」),M4 引入生成域对外端点,须新增第五份。照抄现有四份的同构骨架(纯 Dart CLI `dart run``fail()`/`check()``_passed` 计数、`Resp` 包装、通用 `call()`、脚本内现注册账号取 token、token 截断与预签名 URL 签名 `<SIGNATURE_REDACTED>` 脱敏、内联小图 fixture——**注意 `iteration-3.5/06:56` 的教训:1×1 极小 PNG 能传能下但 Flutter 解码器拒绝,夹具须 ≥16×16**)。场景对齐 M4 四条验收标准:
1. 目录读取(模型/风格,含 `enabled` 过滤与排序)
2. 传图 → 提交生成任务 → 轮询至 `succeeded` → 输出 asset 可预签名取回且 `widthPx/heightPx` 非空
3. **相同 `Idempotency-Key` 重提 → 同一 jobId、不新增行、不消耗配额**
4. **Worker 重启不丢任务**:任务处 `running``docker compose restart` 生成域容器 → 任务被重新领取并最终成功
5. 非法状态迁移被拒(对 succeeded 任务取消、对 queued 任务重复取消等)
6. provider 失败 → 重试 → 达上限 → `failed``errorCode`/`errorMessage`/`attemptCount` 可查
7. 取消 queued 与取消 running 各一
8. 超配额 → 429 + `Retry-After`
9. 一键建草稿 → `posts.generation_job_id` 落库 → 发布 → 另一账号 Feed 可见(`category='ai_creation'`
10. 他人 jobId / 未成功 jobId 建草稿被拒
另需在脚本头补上第五条 baseUrl 常量(若 D4-2 新建模块)。
- **验收标准**:全场景绿;契约偏差 0;M4 四条验收标准逐条有证据(HTTP transcript 脱敏 + psql 库层交叉核对,沿 M3 T3-21 的双源取证强度);连跑 3 轮零 flake。
- **依赖**T4-06~T4-11、T4-19。
- **规模**M
#### T4-26 五份 E2E 回归与 v0.5.0 发布(PR 流程)
- **仓库**:三仓
- **描述**:按 `releases.md:126-138` 的**新流程**发布(v0.4.0 已实测过一轮):① 三仓 CI 绿 + **五份 E2E 同环境串行回归 PASS**M1 7 + M2 11 + M3 14 + M3.5 10 + M4 约 10 = 约 52 场景);② Gitea 建 PR `dev → main`(标题写版本号,正文贴门禁证据链接);③ 等状态检查转绿(`CI / backend-test``CI / flutter-gates``releases.md:110-114`);④ 合并(**须为快进**,若显示分叉先查明原因不得用合并提交掩盖);⑤ 打 tag + 登记发布记录。版本号建议 **v0.5.0**(功能性里程碑,随 M4 收官)。
- **验收标准**:五份回归全绿并出回归报告(沿 `iteration-3.5/08-release-e2e-regression.md` 格式);PR 快进合并、历史线性、零合并提交;三仓 tag 一致;`releases.md` 追加记录含三仓 tag/哈希、门禁证据、已知遗留。
- **依赖**:T4-25 及全部交付单。
- **规模**M
#### T4-27 文档与迭代收口
- **仓库**patbond-doc
- **描述**OpenAPI v1.5.0 归档;`architecture/backend-modules.md` 更新(模块表/端口表/生成域归属与 media 写入边界);`architecture/decisions.md` 新增 **ADR-023 起**(AI 提供方、模块与 Worker 形态、队列载体、输入图必填与视频剪出、配额与限流口径——每条拍板决策一条 ADR 或合并为一条复合 ADR);`feature-checklist.md` 新增 M4 章节(当前 AI 创作**零条目**,仅 :183/:208 两处提及)并把清偿项转绿(:193 清理任务、:194 429 限流、:211 widthPx/heightPx、:222 eventVersion、:243 收藏与草稿页)、把已过期条目纠正(**:145「auth 域契约测试补齐 ⬜」实际 M3 T3-19 已交付**);`device-verification.md` 新增 M4 节(真机专属项预登记)并回填 M2 执行记录;迭代报告归档与收官总结。
**三项文档纪律修正**:① `releases.md:131` 的「E2E **双份**回归」与 :36/:56 的四份口径自相矛盾,须改为**五份**;② 把「每个引入对外端点的迭代都应有对应 E2E 脚本并在每次发布回归」这条原则**固化进 `git-workflow.md`**(该文件 :56-64 的门禁表当前无 E2E 条目,原则只存在于 `releases.md:56` 的一句叙述里);③ **`patbond-doc` 补仓库级 `.gitignore`**(当前无该文件,`site/` 仅靠用户全局 `~/.gitignore_global:183` 排除,换机器或 CI 检出即可被误提交,与 `git-workflow.md:62` 冲突)。
**`iteration-4` 目录的 mkdocs.yml 导航由文档维护者收口提交统一添加(本拆解报告不改 mkdocs.yml**——参考 iteration-3.5 的挂法(`mkdocs.yml:100-108`,节点名脱离「第 N 迭代」序列、无 `index.md` 进展看板行),插入位置在 :108 之后、:109 `- API:` 之前。
- **验收标准**`mkdocs build --strict` 零 warning;报告索引完整;三项文档纪律修正各有对应 diff;ADR 编号连续无冲突。
- **依赖**:各波交付。
- **规模**S
#### T4-28 文档站访问控制(条件单,随 D4-12)
- **仓库**:服务器侧配置 + patbond-doc`server-exposure.md` 状态更新)
- **描述**:若 D4-12 拍板纳入:给文档站加 basic auth 或 IP 白名单。登记原文 `server-exposure.md:32`:「✅ 运行;⚠️ **公开可访问,待评估是否加 basic auth 或 IP 白名单**(无凭证内容,但暴露内部架构细节)」。实现面为 nginx 配置(`sites-enabled/` 已按域名分流,:31+ 凭据经 `.env`/密码文件不入库(沿 2026-09-11 安全事件后固化的服务器侧纪律,`iteration-3.5/07`)。**本迭代的新论据**:M4 报告将暴露 AI 提供方选型、密钥管理方式、配额策略与队列实现细节,公开可读的价值收益低于信息暴露成本。
- **验收标准**:未授权访问返回 401/403;授权后文档站功能不变;`server-exposure.md:32` 状态位更新为已处置并记录核对命令;凭据不入库(`check-secrets.sh --all` 零命中)。
- **依赖**:D4-12 拍板。不占关键路径(纯运维项)。
- **规模**S
## 5. 波次划分与关键路径
沿用已验证模式:波次并行 + 迭代式契约冻结 + 同仓串行跨仓并行 + 每波 compose 实测。
### 第一波(并行开工)
| 并行线 | 工单 | 说明 |
| --- | --- | --- |
| **硬时限** | **T4-24 真机 M2 两项** | **首日执行,2026-09-21 前必须闭环或启动降级**;不占技术关键路径但占人 |
| 数据与骨架 | T4-01 → T4-02 | V6 + 生成域落位,一人连续负责;**需 D4-2/D4-4/D4-6 开工前拍板** |
| 存储写能力 | T4-03 | 与 T4-01 并行;是 T4-09 的前置,**不要等到第二波才开** |
| provider 适配 | T4-05 | **需 D4-1 拍板**;未拍板时先写接口 + fixture,止损线:适配层以上不写供应商代码 |
| 目录 | T4-04 | T4-01 后即可 |
| 契约草案 | T4-13(起草态) | 四处 TODO-FREEZE 标注 |
| 门禁补强 | T4-14(机制部分) | 先在 v1.4.0 上落校验和断言并注毒自证 |
| 前端遗留 | T4-21 我的收藏与草稿 | 与 M4 契约零耦合,第一波并行消化 |
| 清理任务 | T4-12 | 与主线解耦,可插任意空档 |
| UI 设计 | 生成状态五态与结果页设计稿 | 供 T4-17~T4-19,不占关键路径 |
### 第二波(后端纵切,契约收敛)
| 并行线 | 工单 | 说明 |
| --- | --- | --- |
| 后端主线 | T4-06 → T4-08 / T4-0706 后两线并行) | T4-06 是全部生成单的前置 |
| 输出落地 | T4-09 | 依 T4-03 + T4-08 |
| 草稿打通 | T4-10 | 依 T4-01 外键 + succeeded 任务 |
| 限流 | T4-11 | 依 T4-06;**契约首个响应头,须早于冻结定型** |
| 前端骨架 | T4-16(不依赖契约部分) | Repository/DTO 骨架先行 |
| 测试滚动 | T4-15 | 即测即绿即提交 |
**波末闸门:T4-13 契约冻结 v1.5.0**(条件:T4-06/07 状态形态 + T4-09 输出形态 + T4-10 权限语义 + T4-11 限流与 `Retry-After` 形态四处全部定型;四/五份快照同步升版 + 守卫测试期望值整体更新 + T4-14 校验和期望值填入)。**不冻结不放行第三波两端联调。**
### 第三波(冻结契约下两端并行)
| 并行线 | 工单 | 说明 |
| --- | --- | --- |
| 前端主线 | T4-16(完成)→ T4-17 → T4-18 → T4-19;条件单 T4-20 | 创作页先通,状态机与结果页才有意义 |
| 埋点/实验 | T4-22;条件单 T4-23 | 随页面落地滚动挂接 |
| 后端旁路 | 契约测试补齐、队列并发压测、provider 故障注入矩阵 | 不占关键路径 |
### 第四波(收官)
T4-25 第五份 E2E → T4-26 五份回归与 v0.5.0 发布 → T4-27 文档收口;条件单 T4-28 可并行。
### 关键路径
```text
[D4-1/D4-2/D4-3/D4-4 拍板]
→ T4-01(M) → T4-03(M) → T4-05(M) → T4-06(L) → T4-08(L) → T4-09(M)
→ [T4-13 契约冻结闸门]
→ T4-16(M) → T4-17(L) → T4-18(L) → T4-19(M) → T4-25(M) → T4-26(M)
```
四个 L 工单串在关键路径上(T4-06 / T4-08 / T4-17 / T4-18),**后端队列侧与客户端状态机侧各占其二**——与 M3 的「媒体双端占其二」结构同型。
**压缩手段**
1. **D4-1~D4-4 置顶开工前裁决**(四项都在关键路径起点,其中 D4-1 阻塞 T4-05、D4-4 阻塞 V6 定稿)。
2. **T4-03 前移到第一波**:它不依赖任何生成域代码,却是 T4-09 的硬前置;若拖到第二波会把 T4-09 挤到冻结闸门之后。
3. **T4-05 fixture 优先**fixture provider 让 T4-06/T4-08 不必等真实供应商接通即可开发与测试。
4. **T4-13 草案与 T4-16 骨架前移**M2/M3 两度验证有效)。
5. **旁路化**T4-07、T4-10、T4-11、T4-12、T4-20、T4-21、T4-22 均不在关键路径上,可灵活填空档。
6. **T4-24 首日执行**:它只占 ~1 小时机时但有 7 天外部时限,越早越好。
## 6. 需要用户拍板的决策清单
以下决策 PM 只给建议,**不替用户拍板**。**D4-1 是头号**(阻塞关键路径起点且正典自认未决:`development-plan.md:358,365`);**D4-1~D4-4 建议开工前裁决**(四项都影响 V6 定稿或关键路径起点)。
| # | 决策事项 | 影响 | PM 建议(仅供参考) |
| --- | --- | --- | --- |
| **D4-1** | **AI 提供方选型**(正典三度登记为未决:`development-plan.md:358`「AI 提供方……尚未确定」、:365 待确认事项)。候选:**A. fixture / 本地 stub provider**(可配置延时与进度、返回内置图、可注入四类故障;零密钥零成本零外部依赖,CI 与 E2E 唯一可用);**B. 云 AI API**(通义万相 / 即梦 / Replicate / OpenAI 等:能出真图,但引入账号、密钥、按次成本、网络不可达风险与 CI 外部依赖);**C. 自托管 SD / ComfyUI****已核实不可行**:本工作机为 AMD Radeon Vega 集显、无 `nvidia-smi` 即无 CUDA;生产侧腾讯云轻量服务器是否有 GPU 未取证,按 ADR-016 的同类背景推断为无) | 阻塞 T4-05(关键路径起点)、T4-06/T4-08 的开发与测试;决定 provider 抽象的形态(同步返回 vs 轮询 vs 回调)、密钥管理、CI 可跑性、以及验收标准「不重复扣费」中「扣费」是真金钱还是配额 | **A 起步 + 适配层,B 作为末期可选加挂**:M4 的四条验收标准(状态流转合法 / Worker 重启不丢 / 幂等不重复 / 失败可追踪)**全部与图好不好看无关**,fixture 能 100% 取证且能注入真实供应商难以复现的故障;同时把云 API 的接入点留在适配层,若用户愿意提供密钥与预算,第三波末加挂一个云 provider 做一次「真图」验证并入报告。**不建议 C**(无 GPU) |
| **D4-2** | **模块归属与 Worker 形态**:① 生成域——**新建 `patbond-creation`:8085,七容器)** vs **并入 `patbond-user`**(后者已持 Flyway、已持唯一的 `ObjectStorage` 写入方、已有唯一的 `@EnableScheduling` 先例);② Worker 形态——**与 API 同进程 `@Scheduled`**(省容器)vs **同一 jar 不同 profile 起独立容器**(八容器,「重启不丢任务」的取证更干净、长任务不与在线请求抢线程) | 决定 T4-02/T4-03 骨架、compose 容器数(6/7/8)、CI 时长、客户端是否需要第五条 baseUrl(连带四份 E2E 脚本与真机清单的参数说明)、以及 ADR-023 的内容 | **①新建 `patbond-creation`:8085**:沿 ADR-009/ADR-017 两次先例(数据所有权独立、模块边界即未来服务边界,`backend-modules.md:68-72`);且 Worker 是长任务,与在线 API 同模块会互相影响。**媒体写入不下放**——生成输出经 T4-03 新增的 `POST /internal/media/assets``patbond-user` 落盘,**ADR-017「media 上传流程实现在 patbond-user」的边界不破**。**②同进程 `@Scheduled` + 配置开关**`patbond.creation.worker.enabled`):DB 队列 + 租约天然支持多实例,需要拆时改配置即可拆;「重启不丢任务」用重启该容器取证同样成立。七容器已是双人团队的运维上限,不建议直接上八 |
| **D4-3** | **队列载体****A. DB 队列**`FOR UPDATE SKIP LOCKED` + 目标模型已设计好的 `lease_owner`/`lease_expires_at`/`next_attempt_at`/`priority` 四列 + 两条 partial 索引)vs **B. 引入 RabbitMQ**`development-plan.md:205` 原文「RabbitMQ 在异步任务阶段启用」,选 A 即是对正典的建议性偏离)vs **C. 引入 Redis** | 决定是否新增第 7/8 个容器与新依赖;决定 T4-08 的实现复杂度;影响 `patbond-common` 纪律(`development-plan.md:79`「不应让所有服务被动引入 RabbitMQ、Feign 等依赖」) | **ADB 队列)**,理由三条:① 目标模型的 `generation_jobs` 已把租约、退避、优先级、幂等、provider 请求去重**全部设计在表里**,两条 partial 索引(`patbond_postgresql.sql:711-715`)就是为 `SKIP LOCKED` 队列写的——选 B 等于把已定稿的设计弃用一半;② 零新增基础设施(现状无 MQ 无 Redis,§2.4),运维面不增;③ 单库事务内「领任务 + 改状态」天然原子,比「MQ 消息 + DB 状态」的双写一致性问题简单一个数量级。**偏离正典须落 ADR-023 并注明**:M6「事务 Outbox 发布器」阶段若真需要 MQ,届时再引入,届时 DB 队列可作为 outbox 的对照实现 |
| **D4-4** | **生成输入是否必须有图**(决定 V6 能否照抄目标模型):目标模型 `generation_jobs.input_asset_id uuid **NOT NULL**``patbond_postgresql.sql:610`)——**即评审定稿的模型不支持纯文生图**。选项:**A. 沿用 NOT NULL,图生图先行****B. 改为 nullable 以支持文生图**(V6 偏离目标模型,须写入迁移说明) | 决定 V6 是否偏离目标模型;决定 create 页整个交互(A 则「先传宠物照片」是硬门槛,现有假上传卡直接转真实;B 则需要「有图/无图」两条分支 UI);决定配额与滥用面(A 天然收窄) | **A(沿用 NOT NULL,图生图先行)**:① 产品定位是宠物 App,核心价值是「用**我的宠物**照片生成」,纯文生图与定位弱相关且与通用 AI 画图工具直接竞争;② 现有 create 页的上传卡(`create_page.dart:159-168`)本就是必经步骤,改造成本最低;③ NOT NULL 让每次生成都有一张已归属校验的输入资产,滥用面与成本可控。若产品坚持文生图,改 nullable 的成本本身很小(V6 一处),但连带 UI 分支与滥用防护为 +1M |
| **D4-5** | **媒体形态****A. 仅 image**`media_kind` 列与枚举保留,种子只入 image)vs **B. image + video 同期** | 视频涉及转码、封面帧、时长、存储量级台阶;`duration_ms` 列已在模型里;现有 create 页已有「AI 视频」分段项(`create_page.dart:138-157`)与 video 时长 ChoiceChip:191-196);媒体上传契约的 `kind` enum 当前是 `[image]``openapi.yaml:3454`,注明 video/document 为向后新增预留) | 决定 T4-04 种子、T4-09 是否需时长探测、T4-17 分段器处置、存储成本量级 | **A(仅 image**:视频是台阶式复杂度(转码 + 封面帧 + 时长 + 带宽——注意 ADR-016 已把「单机公网带宽」列为接受的限制,视频会立刻击穿它)。**分段器建议保留但 video 项禁用 + 「即将上线」提示**,而非删除:既与保留的 `media_kind` 枚举一致,也避免 UI 大改。视频随 M5/M6 的带宽与存储决策一起重评 |
| **D4-6** | **`model_version_snapshot` 的来源矛盾**(目标模型内部自相矛盾,V6 前必须裁决):`generation_jobs.model_version_snapshot varchar(64) **NOT NULL**`,但 `generation_models`:556-573**无任何 version 列**,种子把它硬编码为 `'1.0'`:1514)。选项:**A. V6 给 `generation_models``version varchar(64) NOT NULL DEFAULT '1.0'`**(最小增列偏离);**B. 把 snapshot 列改为 nullable**(弱化可追踪性);**C. 让 snapshot 存 `provider_model_name`**(语义重复,两列同值) | 决定 V6 的一处偏离;影响「失败原因可追踪」与「历史任务可复现」的强度 | **A(给目录加 version 列)**:snapshot 四列的全部价值就是「目录改了不影响历史任务的可复现性」,B 会让这条价值在最关键的模型版本维度上失效;C 造成两列恒同值的冗余。A 的成本是 V6 里一行加列 + 一行迁移说明。**偏离须写入迁移文件头注释**(沿 V5:1-33 写法) |
| **D4-7** | **配额与限流口径**(决定「不重复扣费」中「扣费」的定义):目标模型**没有任何计费/额度表**,故 M4 的「扣费」只能理解为「消耗配额」。待定:① 每用户并发上限(建议 1);② 每用户每日次数上限(建议 20);③ 计数落在哪(**建议直接 `COUNT` `generation_jobs` 行,不新建表**);④ 429 的 `Retry-After` 取值口径(下一次可提交的秒数) | 决定 T4-06/T4-11 实现;决定 T4-25 的第 3、8 场景;直接决定成本上限(若 D4-1 选云 API,日限 × 单价 = 每日成本天花板) | **并发 1 + 日限 20 + 直接 COUNT 不建表**:① 并发 1 让排队语义对用户可解释、也让 Worker 并发度调优不受用户维度干扰;② 20 次/日对个人用户足够宽松、对成本足够安全;③ 不建表的关键理由——**幂等键已保证重复请求不新建行,所以「按行数计配额」天然满足『相同幂等请求不重复扣费』**,这是最省事又最正确的实现;④ `Retry-After` 取「最早一个 running 任务的预计完成时间」或「次日零点」的秒数,取小者。**四个数字均请用户确认,尤其若 D4-1 选 B(云 API),日限直接等于每日成本上限** |
| **D4-8** | **生成任务与草稿的语义定型**(四个子项):① 「一键建草稿」是**用户点按钮建**还是**成功后自动建**;② `category='ai_creation'` 是否**强制要求带 `generationJobId`**;③ `generationJob``Post`/`FeedCard` 读侧**外露到什么程度**(完全不露 / 只露 id / 露模型与风格名);④ 失败任务的「重试」是**新建任务**还是**复用同一任务**;⑤ 对已终态任务重复取消是**幂等 200** 还是 **422** | 决定 T4-07/T4-10/T4-18/T4-19 的实现与契约形态;③ 直接决定契约新增字段的规模;④ 影响幂等键语义(新建任务须换 key,否则命中旧任务) | ①**用户点按钮建**(自动建会产生草稿垃圾,且用户可能只想看看不想发);②**强制带 `generationJobId`**(否则用户可伪造 AI 分类,读侧 `ai_creation` 就失去可信度);③**只露 id + 模型与风格的展示名**(够做「由 Patbond-V1 · 治愈动画 生成」的角标,不露 prompt——prompt 可能含隐私);④**新建任务**(复用同一行会破坏 `attempt_count <= max_attempts` 与 snapshot 语义;客户端须生成新 `Idempotency-Key`,并在 UI 明确「重试会消耗一次配额」);⑤**幂等 200 no-op**(沿 `UpdatePostRequest.status` 对已发布帖重复提交为幂等 no-op 的既有先例 `openapi.yaml:3701-3707` |
| **D4-9** | **`widthPx`/`heightPx` 恒 null 的处置**(M3 观察项 2,跨两个迭代未决):**A. 服务端在 complete 时探测尺寸**(需 T4-03 顺带加对象读能力,读图片头部字节解析 jpeg/png/webp);**B. 只改契约描述**(把 `openapi.yaml:3536` 的「complete 后回填」改为「仅 AI 输出回填」,用户上传路径承认恒 null);**C. 契约新增 complete 请求体由客户端申报尺寸**(客户端已解码过图,成本最低,但客户端申报值不可信) | 决定单图帖能否按真实宽高比渲染(现一律回落 4:3);决定 T4-09 的规模;A 会让 T4-03 多一个方法 | **A(服务端探测)**:① M4 本就要给 `ObjectStorage` 加写能力,顺带加读只是一个方法,边际成本 S;② AI 输出路径无论如何都要写真实尺寸(尺寸是生成请求参数,服务端已知),若用户上传路径仍恒 null,就会出现「AI 图有宽高比、用户图没有」的分裂体验;③ C 的客户端申报值不可信(可被篡改,且与 `ck_media_dimensions` 的库层校验形成两个事实源)。**探测失败时回落 null 且不阻塞 `ready`**`MediaAsset.required` 不含尺寸字段,向后兼容) |
| **D4-10** | **契约版本与快照锁补强**:① 版本号 **v1.5.0**(若确认对 v1.4.0 为纯增量);② 是否顺手把快照锁从「计数守恒」补强为**真校验和断言**(§2.13CI 当前只锁 version + 3 计数 + tag 分组,md5 比对是人工步骤,任何计数守恒的字节改动都能溜过) | ① 决定四/五份快照文件名与守卫测试期望值;② T4-14 是否成立 | ①**v1.5.0,且维持「对既有集成方纯增量」的承诺**:核对全部变更(新增生成域路径、两处 enum 增值、新增字段、一处 description 修订、新增错误码与 429 响应)——**无字段删改、无类型变更、无必填收紧**,纯增量成立,可继续写入 `info.description`。②**纳入(T4-14,S)**:M4 契约面显著扩大(新增域 + 可能第五份快照),且已有一次真实教训——契约测试累计抓修 3 处真问题(`iteration-3/29:44`),其中「校验器对 `nullable + allOf` 静默跳过」正是这类「看不见的漂移」 |
| **D4-11** | **A/B 首个实验是否本迭代启动**(ADR-012 原定「M4 首实验」,`decisions.md:130`):**A. 启动,但极小面**(补分流哈希 + Flutter 曝光封装 + 最小开关三件,实验对象取纯展示层项);**B. 后置到 M5/M6**(等监控设施选型落地一并做) | 决定 T4-23 是否成立(M);决定 ADR-012 的承诺是否兑现 | **A,但请用户知情前提被高估了**:§2.9 核实后前置实为 **5 绿 3 半**#4 分流组件、#5 Flutter 曝光封装、#7 feature flag 三者**代码零实现**),文档写的「6 绿 1 半」偏乐观。启动首个实验需先补这三件(约 1M),建议用**纯客户端确定性哈希**(`anonymousId` 持久化已就位)避免为一个实验搭服务端下发设施;实验对象取 create 页风格卡默认排序或结果页引导文案这类**纯展示层**项,**不实验后端生成逻辑**。若用户认为 M4 周期已满,选 B 亦合理,但须在收官总结显式记录 ADR-012 承诺顺延,并把「6 绿 1 半」的口径更正为「5 绿 3 半」 |
| **D4-12** | **文档站是否加访问控制**`server-exposure.md:32` 登记的待评估项):**A. 本迭代纳入(S 独立工单 T4-28)**;**B. 继续挂起** | 纯运维项,不占关键路径;影响内部架构信息的暴露面 | **A(纳入,S**:本迭代的新论据是 M4 报告将写入 AI 提供方选型、密钥管理方式、配额策略与队列实现细节——公开可读的收益低于暴露成本;且 2026-09-11 安全事件后服务器侧纪律已固化,趁热做成本最低。实现为 nginx basic auth 或 IP 白名单,凭据不入库。若用户偏好继续挂起亦无技术风险,但建议至少把「M4 迭代报告」这一目录先行限制 |
| **D4-13** | **真机 M2 两项的 09-21 硬时限如何应对**(外部依赖,PM 无法消除):**A. 7 天内投入 Android 真机**(借用或采购)并首日执行 T4-24;**B. 接受降级**——首批北极星读数标注「未经真机验收」+ 复评点顺延至下一成熟窗口 | 决定 2026-09-21 首批北极星读数(W37 队列)是否可作为正式基线;连带影响 A/B 前置 #1「数据质量验收」的成色(该项判绿但拦路项正是真机) | **A(投入真机)**:① 两项合计仅约 1 小时机时(`device-verification.md:42` ~10 分钟 + `:63` ~45 分钟含等待),投入产出比极高;② **桌面无法替代**——`device-verification.md:144` 已言明 Linux 桌面 `platform=linux` 不在契约枚举内、整批 400 被拒,故「事件落库」这一项只能在 Android 上验;③ 这两项是 10 项挂起中**唯一带外部时限**的,其余 8 项无时限可继续挂起。若确实无设备,B 的三项降级动作(标注 / 顺延 / 记录不可替代缺口)须全部落地 |
| **D4-14** | **AI 资产的用途命名、归属与生命周期**:① 新 `purpose` 枚举值命名(建议 `ai_input` / `ai_output`vs 复用 `post_image`);② 输出 asset 的 `owner_user_id` 是否为请求用户(建议是);③ 失败/取消任务的**输入图**是否清理、生成记录是否可删、删除记录是否连带删对象;④ 生成图被建成帖子后,删帖是否影响该资产 | 决定 T4-03 的配置扩充、T4-09 的写入字段、T4-12 的清理范围、T4-20 的删除能力;直接影响长期存储成本(每次生成 ≥1 输入 + 1 输出) | ①**新增 `ai_input` / `ai_output` 两个值**(不复用 `post_image`:用途是审计与清理策略的依据,混用后无法区分「用户主动发的图」与「生成中间产物」)——纯配置变更无需迁移(`MediaProperties.java:66` + `application.yml:45`,ADR-022 已验证同一路径);②**是**(便于按用户清理与配额审计);③**输入图随任务终态保留、不主动清理**(重试与申诉都需要它;靠 T4-12 只清理未确认的 uploading 孤儿);**生成记录允许软删但首版不删对象**(对象清理属 M6 存储治理);④**删帖不影响资产**(`posts.generation_job_id``ON DELETE SET NULL``post_media` 与 assets 的引用关系独立,沿既有语义不动) |
**决策依赖关系提示**D4-1 → T4-05D4-2 → T4-02/T4-03②/T4-16 第五条 baseUrlD4-3 → T4-08**D4-4 + D4-6 → V6 定稿(T4-01 的开工前提)**D4-5 → T4-04 种子 + T4-17 分段器;D4-7 → T4-06/T4-11D4-8 → T4-07/T4-10/T4-18/T4-19 + 契约;D4-9 → T4-03/T4-09D4-10 → T4-13/T4-14D4-11 → T4-23 成立与否;D4-12 → T4-28 成立与否;D4-13 → T4-24 的执行 vs 降级;D4-14 → T4-03 配置 + T4-09 + T4-12 + T4-20。
## 7. 刻意不做的事项及理由
以下均为**主动裁剪**,不是遗漏。逐条给出理由,供收官总结引用与产品知情。
| # | 不做的事 | 理由 |
| --- | --- | --- |
| 1 | **视频生成**(随 D4-5) | 台阶式复杂度:转码 + 封面帧 + 时长探测 + 存储与带宽量级。**ADR-016 已把「单机公网带宽」列为接受的限制**(自托管 MinIO 在腾讯云轻量服务器),视频会立刻击穿它。`media_kind` 列与枚举保留,UI 分段项保留但禁用 |
| 2 | **纯文生图**(随 D4-4) | 评审定稿的目标模型 `input_asset_id NOT NULL``patbond_postgresql.sql:610`)本就不支持;且与「用我的宠物照片生成」的产品定位弱相关。改 nullable 的技术成本很小,连带的 UI 分支与滥用防护不小 |
| 3 | **真实计费与支付** | 目标模型**零计费/额度表**。「不重复扣费」以「不重复消耗配额」取证(D4-7),配额直接 `COUNT` 任务行。支付属独立产品决策,不在 M0~M6 任何里程碑原文中 |
| 4 | **引入 RabbitMQ / Redis**(随 D4-3 | `generation_jobs` 已把租约、退避、优先级、幂等、provider 去重全部设计在表里 + 两条为 `SKIP LOCKED` 而建的 partial 索引;引入 MQ 反而带来「消息 + DB 状态」双写一致性问题。`development-plan.md:205` 的 RabbitMQ 承诺顺延到 M6「事务 Outbox」阶段,须落 ADR 记录偏离 |
| 5 | **生成内容审核、敏感词、举报、水印** | 无运营后台(M3 D3-7 已定「`hidden`/`archived` 保留字段不开放端点」)。**但须显式声明敞口**:AI 生图的 UGC 风险高于普通图文(可生成不宜内容且难以事后归因),数据库侧可手工 `hidden` 应急;审核与水印入 backlog,随 M6 或运营后台一起设计 |
| 6 | **生成完成的推送通知** | M6 原文「通知、事务 Outbox 发布器和失败重试」(`development-plan.md:271`)。M4 内客户端靠轮询(T4-18),离页即停、回页重取 |
| 7 | **完整可观测性栈**(指标、Trace、告警、准实时护栏监控) | M6 原文「限流、审计、结构化日志、指标、Trace 和告警」(:272);A/B 前置 #7 的监控半边也已明确「留 M4」实指「依赖监控设施选型」。M4 只做 `development-plan.md:329` 的**最小兑现**:队列积压与 Worker 失败率 **SQL 可查 + 结构化日志留痕**,不建看板不接告警 |
| 8 | **全站通用限流** | T4-11 只做生成域限流(成本刚需)。全站限流属 M6 :272 范围。**注意这意味着 M3 遗留的 429 只被部分清偿**——埋点上报端点(`POST /api/v1/events`)仍无限流,须在 feature-checklist 里如实标注为部分完成 |
| 9 | **草稿自动保存** | T4-21 只做「我的草稿」列表页。自动保存(防抖 Timer + 冲突处理)是独立交互设计问题,且现有埋点枚举已明确「自动保存不埋」(`post_analytics.dart:30-41` 只有 `manual`/`on_exit`)。继续挂起 |
| 10 | **多模型并行生成 / 一次出多图 / 生图质量 A-B 对比** | 目标模型一个任务对一个 `output_asset_id`(单值列),多图需改模型(新增输出表或数组列)。属产品功能扩展,不在 M4 原文 |
| 11 | **逐帖曝光与服务端排序实验日志** | ADR-020 已否决逐卡曝光并把它记为「M4+ backlog 待服务端下发日志」(`decisions.md:169`)。服务端日志下发依赖 M6 的日志设施,本迭代不启 |
| 12 | **话题、关注列表、作者主页** | ADR-018 裁剪项,与 AI 创作零耦合。继续挂起(详见 §8) |
| 13 | **access token 黑名单、`/internal` 改 mTLS** | 跨迭代技术债。**但须记录权重上升**:T4-03 新增 `POST /internal/media/assets`(且承载对象写入),`/internal` 面在本迭代扩大,mTLS 的优先级应在 M6 加固时上调 |
| 14 | **月份网格选择器、大图下滑关闭手势、`SegmentedButton` 之外的主题债** | 与 M4 零功能耦合。大图下滑关闭需引入 `photo_view` 新依赖,不为一个手势引入依赖;月份网格的实测痛点已被年份网格 + 手输 + 「今天」覆盖(`iteration-3.5/06:72` |
| 15 | **真机 M3 四项与 M3.5 四项** | 无设备且无时限(只有 M2 两项带 09-21 硬时限)。M4 会再新增真机专属登记项,届时一并执行更经济 |
## 8. 历史遗留处置:搭车 vs 挂起
清点范围为 M3`iteration-3/29-m3-summary.md:52-62`)与 M3.5`iteration-3.5/06-wave2-closure.md:69-76`)的全部遗留,加上本次审计新发现的文档/门禁缺口。
### 8.1 搭车进 M411 项)
| 遗留项 | 判定理由 | 插入位置 |
| --- | --- | --- |
| **`widthPx`/`heightPx` 恒 null**M3 观察项 2) | AI 输出的尺寸是**生成请求参数,服务端必然已知**,不写就自相矛盾;若只写 AI 路径会造成「AI 图有宽高比、用户图没有」的分裂。且 T4-03 本就要给 `ObjectStorage` 加方法,顺带加读只是边际 S。**核实纠正:不是缺列**(V1:217-218 早已存在),规模 S 非 M | **T4-09**(随 D4-9 |
| **429 限流 + 客户端 `Retry-After` 分支** | AI 生成有真实成本,配额与限流是**刚需而非可选**;客户端 `api_client.dart:164-167` 已有 429 粗分支、`analytics_service.dart:268-272` 的注释明写在等后端落地 | **T4-11**(后端)+ **T4-16**(客户端收口)。**注意只清偿生成域**,埋点端点限流仍挂起 |
| **uploading 超时未确认 asset 清理任务** | M4 本就要建 `@Scheduled` 调度基座;且**每次生成都要先传一张输入图**,孤儿资产量被放大,此项从「可选清理」升为「成本控制项」 | **T4-12** |
| **`eventVersion` 口径未定型** | 字典 v4 本就要动 `EventDictionary` 与契约描述,顺手定型边际成本近零 | **T4-22** |
| **「我的收藏与草稿」两页** | 后端与仓库层**全就绪、零调用点**(`listMyBookmarks` 全仓无调用),与 M4 契约零耦合,可在第一波并行消化,不占关键路径 | **T4-21** |
| **真机 M2 两项** | **唯一带外部硬时限的遗留**2026-09-21),仅约 1 小时机时 | **T4-24**(第一波首日,随 D4-13 |
| **`SegmentedButton` 粉底(M2 主题债)** | **本次审计改判为搭车**:该控件的使用点之一正是 M4 要重做的 create 页模式分段器(`create_page.dart:138`),根因已定位(主题缺 `segmentedButtonTheme``app_theme.dart:94-196`)、已审计色对表与落地范本俱在(:199-214、:215-296),一次修复 6 处使用点,边际成本 S | **T4-17** |
| **A/B 前置 #4/#5/#7 的缺失半边** | ADR-012 承诺「M4 首实验」;且文档「6 绿 1 半」经核实实为「5 绿 3 半」,若不补则实验无法启动 | **T4-23**(条件单,随 D4-11 |
| **契约快照锁强度不足**(本次新发现) | CI 只锁计数不锁字节;M4 契约面显著扩大(新增域 + 可能第五份快照),此时补机制成本最低 | **T4-14** |
| **埋点白名单计数 42→41 纠偏 + 缺总量断言**(本次新发现) | 字典 v4 本就要动白名单;补一条总量断言即永久钉住,边际成本近零 | **T4-22** |
| **三项文档纪律缺口**(本次新发现):`releases.md:131` 仍写「E2E 双份」与 :36/:56 的四份矛盾;E2E 回归原则从未固化进 `git-workflow.md``patbond-doc` 无仓库级 `.gitignore``site/` 仅靠全局 gitignore 排除) | M4 本就要把回归清单改为五份并新增第五份脚本,三项一并修正;`.gitignore` 是一行 | **T4-27** |
### 8.2 继续挂起(10 项)
| 遗留项 | 挂起理由 | 建议落点 |
| --- | --- | --- |
| **真机 M3 四项 + M3.5 四项**(共 8 项) | 无设备且**无外部时限**;M4 会再新增真机专属登记项,届时一并执行更经济。步骤已在 `device-verification.md:110-168`、:172-217 备齐 | 设备到位即插入,不阻塞任何工单(约 1 天) |
| **完整草稿列表的自动保存** | 独立交互设计问题;现有埋点枚举已明确「自动保存不埋」。T4-21 只交付列表页 | M5 或独立客户端体验单 |
| **月份网格选择器** | 实测痛点已被年份网格 + 手输 + 「今天」覆盖(`iteration-3.5/06:72` | 独立客户端体验单 |
| **大图「下滑关闭」手势** | 需引入 `photo_view` 新依赖,不为一个手势引入依赖 | 独立客户端体验单(与依赖评估一并做) |
| **话题功能**ADR-018 剪出) | 与 AI 创作零耦合。**注意一处历史推测已被本次审计削弱**:M3 曾建议「topics 端点随 AI 创作分类需求一起做(`ai_creation` category 天然关联)」(`iteration-3/01:285`),但实际 `ai_creation` 只是 `category` 的第三个枚举值、与 `topics` 表无任何字段关联,**该关联不成立**,不构成搭车理由 | M5+ 或独立社区增量单 |
| **关注列表 / 作者主页**ADR-018 裁剪) | 与 AI 创作零耦合 | M5+ |
| **逐帖 Feed 曝光** | ADR-020 已否决,明确「待 M4+ 服务端下发日志」,而日志设施属 M6 | M6 后重评 |
| **access token 黑名单** | 跨迭代技术债,无 M4 耦合 | M6 加固 |
| **`/internal` 改 mTLS** | 跨迭代技术债。**权重上升须记录**:T4-03 新增 `/internal/media/assets` 且承载对象写入,`/internal` 面在本迭代扩大 | M6 加固(优先级上调) |
| **全站通用限流(生成域之外)** | T4-11 只做生成域;埋点上报端点仍无限流 | M6 :272 |
## 9. 风险清单
| # | 风险 | 影响 | 缓解措施 |
| --- | --- | --- | --- |
| R1 | **AI 提供方未决且正典自认未决**`development-plan.md:358,365`):若拖延,T4-05 空转,连带 T4-06/T4-08 无法测试;若选云 API,还叠加密钥、成本、网络不可达与 CI 外部依赖四重不确定 | 关键路径起点空转 | D4-1 置顶开工前裁决;**未拍板期间先写适配层接口 + fixture 实现**,止损线明确:适配层以上不写任何供应商特定代码;fixture 无论如何都要做(CI 与 E2E 唯一可用 provider |
| R2 | **异步基础设施从零起步**(§2.4:无 MQ、无 Redis、无线程池、`@Scheduled` 唯一先例是删会话行):租约、退避、并发领取、多实例安全全是新代码,且 `ck_generation_jobs_state` 的五态字段组合约束严格(回收时必须清 `started_at``progress`,否则库层直接拒绝) | T4-08(L)估算失准直接拖垮迭代;状态机 bug 密集区 | 选 DB 队列(D4-3)让实现收敛在一条 SQL 模式内;**T4-15 的状态机穷举矩阵作为 DoD 硬项**(每条合法迁移成功 + 每条非法迁移被应用层拒且被库层 CHECK 兜底,两层各有断言);`started_at` 清零的取舍写进代码注释与迁移说明 |
| R3 | **「输出写入媒体表」被一句话低估**(§2.5):服务端目前根本不能写对象存储,且 ADR-017 把 media 写入边界钉在 `patbond-user` | T4-03 若拖到第二波会把 T4-09 挤到契约冻结之后,连锁推迟第三波 | T4-03 前移到第一波(它不依赖任何生成域代码);用 `/internal/media/assets` 保住 ADR-017 边界不破;MinIO Testcontainer 覆盖服务端 put 全链路 |
| R4 | **契约冻结面比 M3 更宽**:四处待定型点(五态响应形态、429 + `Retry-After` 这一契约史上首个响应头、`generationJob` 外露程度、新 purpose 命名),且若 D4-2 新建模块则快照从四份变五份、守卫测试期望值需五处同步更新 | 冻结延迟连锁推迟第三波两端联调 | 四处待定型点第一波即在草案中显式 `TODO-FREEZE` 并限期第二波中期收敛;闸门纪律不放松;T4-14 的校验和断言让「漏同步一份快照」在 CI 立即暴露 |
| R5 | **AI 生成 UGC 的内容风险高于普通图文**:可生成不宜内容,且无审核、无水印、无举报、无运营后台 | 内容风险敞口(虽 MVP 用户面小) | §7 第 5 条已显式声明敞口并要求产品知情;`hidden` 字段可手工应急;prompt 不入埋点明文(隐私);审核与水印入 backlog 并在收官总结列明 |
| R6 | **成本失控**(仅在 D4-1 选云 API 时成立):无配额则单用户可无限提交 | 真金钱损失 | D4-7 的日限即每日成本天花板,**必须与 D4-1 同时拍板**T4-11 的配额是 T4-06 的准入条件而非可选后置项;fixture provider 让开发与 CI 阶段零成本 |
| R7 | **create 页从零建测试基线**(§2.14563 行、`grep "CreatePage" test` 零命中) | T4-17/T4-18 是「改代码 + 建测试」双份工作量;假延时改真轮询是状态 bug 高发区 | 保留四块视觉骨架只换数据源(降低视觉返工,M3 的 R10 不复现);轮询的「页面不可见暂停 / 回前台恢复」列为 DoD 硬项——M3 曾用 widget 测试抓出「回前台不开新曝光段」的同类真 bug(`iteration-3/29:46` |
| R8 | **09-21 硬时限只剩 7 天**且依赖外部设备(PM 无法消除) | 首批北极星读数(W37)只能标未验收,连带 A/B 前置 #1 的成色 | D4-13 请用户即刻拍板;T4-24 排在第一波首日;降级方案三项动作已写入工单,且明确**桌面无法替代**`platform=linux` 不在契约枚举、整批 400 |
| R9 | **容器数与 CI 时长继续增长**:六 → 七(或八)容器;MinIO 容器使用面因 T4-03/T4-09 扩大;测试数从 379/597 继续上量 | 门禁反馈变慢被绕过 | D4-2 建议同进程 Worker 以省一个容器;T4-15 记录每波 CI 时长,超阈值按模块分层执行,**不降低「提交前全绿」标准** |
| R10 | **文档结论与代码实况的系统性偏差**(本次审计一次性发现 8 处,见 §11):白名单计数、快照锁强度、A/B 前置成色、真机项数、E2E 份数口径…… 说明「转述被当成事实」不是 M3.5 的偶发事故 | 规模预估失准、承诺无法兑现、门禁虚假安全感 | 本报告全部结论标注核实文件与行号;**把「可机验断言」作为纠偏机制**(T4-22 补白名单总量断言、T4-14 补契约校验和断言——两者都是把人工纪律变成 CI 门禁);收官总结须逐条回填修正后的口径 |
| R11 | **未提交/未推送风险**(历次惯例项) | 工作量全损 | 每波每单交付即提交即推送(ADR-011/ADR-021:只推 devmain 走 PR);PM 每波核对三仓 `git status` 与远端同步 |
## 10. 质量要求(对全部工单生效)
- 遵守开发计划第 10 节 DoD 八条:契约/迁移/代码一致;不依赖 demo 常量;权限、校验、幂等、并发已处理;四态齐备;日志可定位且不泄敏;干净环境可复现。
- 契约规范沿用:`camelCase`、UUID 字符串、ISO 8601 + `timestamptz`、统一信封 `{code, message, data}`、稳定错误码(生成域新码段定死:建议 `40407`/`42206`/`42900`,**永不复用或改号**)、cursor 分页(禁 OFFSET)、**生成任务创建强制 `Idempotency-Key`**`development-plan.md:173`)、`version` 乐观锁。
- **契约冻结后 api 侧快照同步升版**,四/五份 md5 与正典一致;守卫测试期望值(version + 3 计数 + tag 分组)与 T4-14 的校验和断言同步更新;`everyDeclaredResponseCellIsExercised` 新增格全部真实触发,不得豁免。
- **迁移纪律**V6 进 `patbond-user`(单迁移链);不修改已推送的 V1~V5;任何偏离目标模型之处在迁移文件头注释逐条写明理由(沿 V5:1-33 写法);存量库实证 + 全新库全量迁移双向验证。
- 集成测试一律 Testcontainers `postgres:18`ADR-006/008),媒体与生成输出测试用 MinIO 容器;每单交付 `JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test` 全绿、`dart format --set-exit-if-changed` + `flutter analyze` + `flutter test` 全绿、`mkdocs build --strict` 零 warning(门禁表 `git-workflow.md:56-64`**门禁不绿不提交**)。
- 每个业务接口覆盖六类路径:成功 / 参数错 / 资源不存在 / 无权限 / 并发冲突 / 幂等重试。
- 所有网络页面四态(loading/empty/error/retry)齐备 + 离线提示;图片位另加占位/失败态;预签名 URL **不得持久化**、缓存 key 须剥 `X-Amz-*`
- **凭证纪律**:不提交任何密码、token、对象存储密钥、**AI provider 密钥**`.env` / `.sample` 模式);两层 `check-secrets.sh` + CI 兜底;埋点不含 prompt 明文、不含 assetId/jobId、不含精确字节数。
- **本迭代不实现**预约、通知推送、运营后台的任何接口或页面;`posts.region_id` 仅预留;范围外需求记 backlog。
## 11. 工单统计
- **工单总数:28**(A 数据与地基 5 + B 后端生成纵切 7 + C 契约与测试 3 + D Flutter 6 + E 埋点遗留收口 7),其中 **3 个条件单**T4-20 我的生成记录、T4-23 A/B 首实验、T4-28 文档站访问控制)
- **规模分布**(核心 25 单):S × 5T4-04 / T4-12 / T4-14 / T4-24 / T4-27)、M × 16、L × 4T4-06 / T4-08 / T4-17 / T4-18);条件单另计 S × 2、M × 1
- **波次**:4 波(第一波并行开工 10 线 / 第二波后端纵切 6 线 + 契约冻结闸门 / 第三波两端并行 3 线 / 第四波收官 3 单)
- **关键路径**D4-1~D4-4 拍板 → T4-01 → T4-03 → T4-05 → T4-06 → T4-08 → T4-09 → **T4-13 冻结闸门** → T4-16 → T4-17 → T4-18 → T4-19 → T4-25 → T4-26;L × 4 在链上,后端队列与客户端状态机各占其二
- **待拍板决策:14 项**D4-1~D4-14);**D4-1 头号**(阻塞关键路径起点),**D4-1~D4-4 建议开工前裁决**D4-4 与 D4-6 是 V6 定稿的前提)
- **遗留处置**:搭车 11 项、继续挂起 10 项
- **刻意不做**:15 项,逐条附理由
### 11.1 本报告推翻或修正的既有文档结论(8 处)
| # | 既有文档结论 | 核实后的实况 | 影响 |
| --- | --- | --- | --- |
| 1 | 「事件白名单 42 条」(`iteration-3/22:4,14``iteration-3/29:20``feature-checklist.md:218` | **41 条**`EventDictionary.java:42-104` 实测 `Map.entry` 41 个;根因是 `experiment_exposed` 被重复计数(设计源文档 `iteration-3/06:12,149` 自己写的是 19 个新事件,22 + 19 = 41)。**无任何门禁能拦**(`EventDictionaryTest` 无总量断言) | 三处文档纠正 + 补总量断言(T4-22) |
| 2 | 「四模块**字节级快照锁 CI**」 | **CI 里没有任何 md5/checksum 校验**`ci.yml` 只有三步(secret scan / 装 JDK / `mvnw clean test`);md5 比对只是人工冻结步骤。CI 实际锁的是「version + 3 计数 + tag 分组」,任何**计数守恒的字节修改**都能溜过 | 新增 T4-14(S)把纪律变门禁 |
| 3 | 「A/B 前置 **6 绿 1 半**M4 可启首个实验」(`iteration-3/29:68` | **实为 5 绿 3 半**#4 分流哈希组件、#5 Flutter 曝光封装、#7 feature flag / 社区发布开关**三者代码零实现**(后两者 grep 全仓零命中,#7 的「回滚绿」无任何依据,连 feature-checklist 都未登记) | D4-11 需在知情前提下拍板;T4-23 规模上调 |
| 4 | 「真机挂起**四项**」/ 主会话口径「共 8 项(M3.5 两项)」 | **共 10 项**M2 2 + M3 4 + **M3.5 4**`device-verification.md:176/:190/:200/:209`M3.5 收口报告 `06:75` 自己写的也是「4 项 + 1 备注」)。三个执行记录全为「待补」 | §8.2 按 10 项登记;T4-24 只抢 M2 两项 |
| 5 | 「`widthPx/heightPx` 恒 null」被隐含理解为能力缺口 | **列早已存在**(V1:217-218);真实缺口是「服务端无尺寸探测」+「契约声明了不存在的回填入口」(complete 端点 `openapi.yaml:1256-1299` **无 requestBody**)。**这是 M3.5「转述不可采信」教训的同型复现** | 规模由 M 降为 S,搭车 T4-09 |
| 6 | 「M4 需设计 AI 创作数据模型」(隐含预期) | **完整设计早已评审定稿**`docs/database/patbond_postgresql.sql:556-716` 含三表 40 列、全套租约/退避/优先级/幂等列、11 条索引(两条为 `SKIP LOCKED` 队列而建)、三个触发器、开发种子。且**零跨 schema 外键需裁剪**(V3 剪 4 条、V5 剪 2 条的先例在 M4 不重演,因 identity/pet_health/media 三个 schema 均已存在) | T4-01 由 L 降为 M;D4-3 有了「不引入 MQ」的硬证据 |
| 7 | 「输出写入媒体表」(`development-plan.md:252` 一句话) | **服务端目前根本不能写对象存储**`ObjectStorage.java:15-37` 只有 `ensureBucket`/`presignPut`/`stat`/`presignGet``putObject` 仅用于构造预签名。需新增写方法 + 实现 + 未配置桩 + 跨模块落库通路(受 ADR-017 边界约束) | 新增独立工单 T4-03(M),并前移到第一波 |
| 8 | 「回归清单已改为四份」与「发布流程」 | 口径**自相矛盾且未固化**:`releases.md:36/:56` 写四份,同一文件 :131 的纪律条目仍写「E2E **双份**回归」;且「每个引入对外端点的迭代都应有 E2E 脚本」这条原则**从未写进 `git-workflow.md`**(该文件门禁表无 E2E 条目)。另 `feature-checklist.md:145`「auth 域契约测试补齐 ⬜」已过期(M3 T3-19 已交付) | 三项文档修正并入 T4-27;第五份脚本单列 T4-25 |
**附带修正两处口径(非推翻)**:① patbond-api 测试数 **379**surefire 运行数,`releases.md:17,35`)与 **381**(注解静态计数,62 个测试类)的差异属参数化展开口径,不是回归;② patbond-flutter 是 **597 通过 + 2 skip**(环境门控冒烟),且不含 `integration_test/` 四份桌面实测与仓库根四份 E2E 脚本。
### 11.2 尚未取证、需用户或后续角色补齐的三项
1. **AI 提供方的可用性与价格**:已核实本工作机无 CUDA(AMD Vega 集显、无 `nvidia-smi`)故自托管不可行;**生产服务器是否有 GPU、云 API 的额度与单价均未取证**,需用户确认或授权调研(阻塞 D4-1)。
2. **A/B 实验设计模板与样本量规则文档**(前置 #3/#6,文档判绿):两仓中**未定位到文档实体**,仅在 `iteration-3/06` 表格里被声明为已交付(影响 T4-23 的验收标准可执行性)。
3. **北极星出数与对账 SQL 无可执行载体**SQL 仅以 Markdown 代码块存在(`iteration-2/06:200-229`、:364-470),三仓 `scripts/` 下只有 `check-secrets.sh``hooks/`。2026-09-21 首次出数须人工执行,建议后续沉淀为脚本(不在 M4 范围,记 backlog)。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,687 @@
# 04 M4 开工现实核查(Reality Check
> 作者:Reality Checker
> 日期:2026-09-14
> 输入:三仓工作区实测(api `3cd8005` / flutter `fbcd734` / doc `5cc6361`,均 tag `v0.4.0`);
> `docs/database/patbond_postgresql.sql` 正典模型;`device-verification.md``releases.md`
> `server-exposure.md`Gitea API 实时提交状态。
> 同批 07 号(Evidence Collector)已独立清点基线数字,本报告复用其结论并**不重复清点**;
> 本报告的增值面是 **M4 规划的未证前提狙击**与**跨文档一致性**
>
> **总体裁决:NEEDS WORK。** 基线代码质量与契约纪律确实扎实(32/45/75 契约、四模块字节级快照、
> 381 + 597 双绿、CI 三仓已恢复全绿),但 **M4「AI 创作」的三个核心前提全部无证据支撑**
> 无任何 AI 服务商 endpoint / 凭证 / 额度(`.env``init-secrets.sh` 共 4 个密钥,无一条与 AI 相关);
> 服务端**完全不具备写对象存储的能力**(全代码库 S3 调用仅 `createBucket`/`headBucket`/`headObject`/`close`
> 零 `putObject`、零 `getObject`),故「媒体链路可直接复用」只对读侧与客户端上传侧成立、
> Worker 产出物落盘是**净新增**;且挂起的真机项已达 **10 项**(非 8 项),M4 若沿现有模式将再批量产出一批。
> 唯一的好消息是 Worker 队列**不需要** Redis/MQ——正典模型已给出 Postgres `FOR UPDATE SKIP LOCKED`
> 租约队列设计,现有 compose 六容器无需扩容。
---
## 1. 基线数字逐条核对
同批 07 号报告已逐格清点,此处只列**核对结果**与**我侧独立取证的差异项**。
| 声称 | 实测 | 裁决 |
| --- | --- | --- |
| 三仓工作区干净、全部停在 `v0.4.0` | `git status --porcelain` 三仓均空输出;`git tag --points-at HEAD` 三仓均返回 `v0.4.0` | ✅ 对 |
| api `dev@3cd8005` | `3cd80055779db2d52cf8cc1af425d06131f7e41d` | ✅ 对 |
| **api 379 测试全绿** | **381**`3+131+40+100+107`),`BUILD SUCCESS`Failures 0 / Errors 0 / **Skipped 0** | ❌ **不对,见问题 P4** |
| flutter `dev@fbcd734` | `fbcd73468805e95b8055395654ca1014e0d6c13c` | ✅ 对 |
| flutter 597 测试全绿 | `00:39 +597 ~2: All tests passed!`exit 0 | ✅ 对(**但含 2 个默认跳过项,见问题 P6**) |
| doc `main@5cc6361` | `5cc63615345fc30cb342a336292d7decdffea98f` | ✅ 对 |
| 契约 v1.4.032 路径 / 45 操作 / 75 schema | `info.version=1.4.0`paths 32、operations 45、schemas 75**逐格相符** | ✅ 对 |
| 四模块字节级快照锁 | `md5 = a7081fb84f1207eef579ab94025f5801`doc 正典 + auth/user/pet/community 四份快照**五处完全相同** | ✅ 对 |
| **契约矩阵 181 格零漂移** | 181 这个数字**只存在于报告散文**(`iteration-3.5/04:96``releases.md:39``feature-checklist.md:242`);代码里唯一被钉住的规模断言是 `MediaContractConformanceTest.java:250``assertThat(CONTRACT.operations()).hasSize(45)`。无任何测试断言「181」 | ⚠️ **不可机械复核,见问题 P5** |
| Flyway V1~V5 | `patbond-user/src/main/resources/db/migration/` 恰好 V1~V5,无 V6 | ✅ 对 |
| V5 已为 `posts.generation_job_id` 留裸列(无外键) | `V5__community_baseline.sql:48` = `generation_job_id uuid,`(零 `REFERENCES`);`:95``ix_posts_generation_job``:47` 注释明写「FK to creation.generation_jobs strippedM4 补回)」 | ✅ 对 |
| 六容器部署(含自托管 MinIO) | `docker-compose.yml` services = `postgres, minio, user, auth, pet, community`,恰 6 | ✅ 对 |
| **埋点事件白名单 42 条** | **41 条**`EventDictionary.java``Map.ofEntries``Map.entry` 41 个,去重后仍 41 | ❌ **不对,见问题 P3** |
| ADR-001~022 | `decisions.md` 22 个 `## ADR-0xx` 标题,无缺号无重号 | ✅ 对 |
| E2E 四份脚本 **42 场景** | 脚本自报计数相加**恰好 42**:M1 `[1/7]`~`[7/7]` = 7、M2 `$_passed/11`、M3 `$_passed/14`、M3.5 `$_passed/10` | ✅ 对 |
| E2E **234 断言** | 机械可数:`check(` 计数 M2 42 + M3 84 + M3.5 84 = **210**M1 无 `check(`、用 16 处 `✗` 卫语句 → 合计 **226**。差 8 处无法机械复现(疑为 `assertMeShape` 等辅助函数内部断言另计) | ⚠️ 数量级可信、精确值未取证 |
| mkdocs 可构建、导航无死链 | `mkdocs build --strict` **exit 0**`Documentation built in 1.71 seconds` | ✅ 对 |
| main 分支保护禁直推 + 两个状态检查上下文 | 仅有 `releases.md:110-114` 表格作为证据(称经 Gitea API 核实);我侧 `GET /branch_protections` 返回 **401**(无 token,未取证)。**且该上下文的选型本身有洞,见问题 P2** | ⚠️ 配置未复核 / 设计有洞 |
### 1.1 我侧独立取证:CI 状态比声称的**更好**(唯一正向偏差)
`iteration-3.5/04:8``§7.3` 记录「⚠️ Gitea CI 仍红——runner 级故障,最后一次 CI 绿是 M3 末的 `8089c06`」,
`08-release-e2e-regression.md` 全文**零处**提及 CI/runner/Gitea。若照抄这条结论,会以为 M4 开工时 CI 仍死。
实测(Gitea API `GET /repos/{owner}/{repo}/commits/{sha}/status`2026-09-14):
| 仓 | state | 上下文 | 结果 | 上报时间 |
| --- | --- | --- | --- | --- |
| patbond-api | `success` | `CI / backend-test (push)` | Successful in 5m31s | 2026-09-14T09:44:05+08:00 |
| patbond-api | `success` | `CI / backend-test (pull_request)` | Successful in 6m42s | 2026-09-14T09:54:37+08:00 |
| patbond-flutter | `success` | `CI / flutter-gates (push)` | Successful in 3m46s | 2026-09-14T09:58:23+08:00 |
| patbond-flutter | `success` | `CI / flutter-gates (pull_request)` | Successful in 3m44s | 2026-09-14T10:02:08+08:00 |
| patbond-doc | `success` | `CI / docs-build (push)` | Successful in 1m22s | 2026-09-14T11:20:44+08:00 |
**结论:runner 已修复,三仓 HEAD 全绿,`(push)``(pull_request)` 双上下文均真实跑过。**
`iteration-3.5/04 §7.3` 的「CI 仍红」是**已过期的历史状态**,M4 开工不必把它当风险。
(此项 07 号列为「未取证」,本报告补齐。)
## 2. 遗留与挂起项的当前真实状态
**纪律**:本节每一项都从源码/迁移文件直接取证,**不采信任何报告结论**。
### 2.1 真机挂起项:实际 10 项,且「转述污染」链条已可完整还原
`device-truth`:打开 `docs/development/device-verification.md` 逐节清点顶层验证项:
| 迭代 | 节标题 | 顶层项数 | 明细 | 执行记录 |
| --- | --- | --- | --- | --- |
| M2 | 「M2 挂起项(2026-09-08 登记,待执行)」 | **2** | 验证一 Android 事件真实落库;验证二 SessionTracker 30 分钟后台换会话 | `_(待真机到位后填写…)_`**未做** |
| M3 | 「M3 预登记(社区)」 | **4** | 媒体上传弱网;乐观更新真机手感;Feed 图片加载;社区事件落库 | `_(待补)_`**未做** |
| M3.5 | 「M3.5 预登记(用户资料与头像)」 | **4** | 头像上传弱网;头像缓存;caregiver 改宠物头像;获赞数对账 | `_(待补)_`**未做** |
| | **合计** | **10** | | **0 项已执行** |
M3.5 节的正文自证是四项:「下列**四项**是桌面替代不了的部分」。
**污染链条**(这正是 M3.5 教训的同型复发,值得单独记录):
1. **源头**`releases.md:121` 写「**真机验证四项挂起**(媒体上传弱网、乐观更新手感、Feed 图片加载、社区事件落库);另有 M2 两项」= 登记 **6 项****整段漏掉 M3.5 的四项**。
2. 我的开工任务书据此写「真机**四项**验证」(只剩 M3 的 4)。
3. 协调者更正为「**8 项** = M2 两项 + M3 四项 + M3.5 **两项**」——方向对了,但 M3.5 又少计 2。
4. **实测 = 10 项**
即:同一事实经过三次转述,出现 4 → 6 → 8 → 10 四个不同数字,**每一次转述都在丢项**。
`device-verification.md` 是唯一正确的原始文件,任何规划都必须直接读它。
### 2.2 M2 两项的 2026-09-21 时限:可行,且「等真机」是个伪阻塞
`device-verification.md` 的 M2 节挂着时限提醒「建议在 **2026-09-21**(北极星首次出数日)前完成」。今天 09-14,剩 7 天。
关键取证——**该清单明写「Android 真机(推荐)或 Android 模拟器」,而本机模拟器链路完整可用**:
```
$ ls ~/.android/avd/ → Pixel_7.avd Pixel_7.ini
$ ~/Android/Sdk/emulator/emulator -list-avds → Pixel_7
$ ls -la /dev/kvm → crw-rw-rw- 1 root kvm (全员可读写,无需加组)
$ ls ~/Android/Sdk/system-images → android-36
$ ls ~/Android/Sdk → build-tools cmake cmdline-tools emulator ndk platforms platform-tools skins …
```
`flutter devices` 当前只看到 Linux/Chrome,原因是 `ANDROID_HOME`/`ANDROID_SDK_ROOT` **未设置**且
`adb`/`emulator` 不在 `PATH`(实测 `which adb` → not found),**不是缺硬件**。
**裁决:时限可行(CERTIFIED-可行)。** M2 两项的净工作量是「验证一 ~10 分钟 + 验证二 ~45 分钟(其中 35 分钟是纯等待)」,
合计约 1 小时挂钟时间,其中人工操作不足 15 分钟。前置只差三条命令(导出 `ANDROID_HOME`
`PATH``platform-tools`/`emulator`、起 `Pixel_7`)。**7 天时限绰绰有余;把它记作「待真机到位」已经拖了 6 天,属登记口径错误而非资源不足。**
**解除条件**:起 `Pixel_7` 模拟器 + compose 六容器,跑完 M2 两项,填 `device-verification.md` 的「M2 项执行记录」,
并把 `feature-checklist.md` 第 9 节由 🟡 改 ✅。
**但需同时纠正一处过度乐观**:10 项里只有一部分模拟器可替代。按各项通过标准的物理依赖分类:
| 可用模拟器完成(6 项) | 必须真实硬件(4 项) |
| --- | --- |
| M2 两项(`platform=android` 落库、SessionTracker 30min | M3-1 媒体上传弱网(需蜂窝/飞行模式掐断、中端机压缩耗时 ≤2s) |
| M3-4 社区事件落库(只要 platform 枚举命中即可) | M3-2 乐观更新手感(需中低端机真实帧率判「无可见掉帧」) |
| M3.5-3 caregiver 改宠物头像(纯权限档位) | M3-3 Feed 图片加载(含「蜂窝 vs Wi-Fi 可达性差异」) |
| M3.5-4 获赞数对账(纯数字口径) | M3.5-1 头像上传弱网(含 **iPhone HEIC** —— 模拟器与 Android 都造不出) |
| M3.5-2 头像缓存(跨页命中可在模拟器观察) | |
**另注(07 号已取证,此处只标注影响)**M3.5-3 caregiver 项的前置写着「按 `pet_health` 的协作表直接造数据,**收口时补 SQL**」,
该 SQL 从未补 → **此项当前不可执行**,即便有设备也跑不了。解除条件是先补造数 SQL。
### 2.3 `widthPx` / `heightPx` 仍恒 null —— 结构性,且 M4 会被它直接绊到
**取证(不看文档,看写路径)**
- 声明侧存在:`V1__identity_media_baseline.sql:217-218``width_px integer, height_px integer`
`MediaAssetResponse.java:18-19``PostMediaItemResponse.java:15-16` 都外露这两个字段。
- **写路径不存在**`MediaAssetRepository.insertUploading()` 的 INSERT 列清单是
`(id, owner_user_id, kind, purpose, storage_type, bucket, object_key, mime_type, byte_size, sha256, status)`
—— **不含 width_px / height_px**
- **更新路径也不存在**:全库 `grep width_px` 命中的 `UPDATE` 语句 **0 条**`media.assets` 上仅两条 UPDATE
`markReady``SET status='ready', ready_at=now(), updated_at=now()`)与 `markFailed``SET status='failed'`),
**都不碰尺寸列**
**结论:生产链路上这两列永久为 NULL,没有任何代码能写入。** 裁决 **NEEDS WORK(确认遗留)**
⚠️ **陷阱提示**`PostMediaAttachIntegrationTest.java:44-45` 断言 `widthPx==640 / heightPx==480` **是绿的**
因为 `CommunityTestData.java:55` 在测试夹具里直接 INSERT 了这两列。
**测试绿 ≠ 生产有值** —— 这与 M3.5「无 nickname 字段」同型:断言测的是夹具,不是产品。任何规划不得据此认为该字段可用。
### 2.4 `eventVersion` 口径仍未定型 —— 且服务端根本不校验
- 契约要求必填:`openapi.yaml` `TrackedEvent.required``eventVersion`
- 契约描述**已过期**`openapi.yaml:2341-2343``description: 事件 schema 版本(**字典 v1 全部为 1**)`
`EventDictionary.java` 类注释开头即 `Event dictionary **v3**`
- **服务端零校验**`grep -rn "eventVersion" --include=*.java``EventDictionary.java`
`AnalyticsService.java` 中命中 **0 次**。唯一处理是 `TrackEventsRequest.java:38``@NotNull`
`AnalyticsRepository.java:26/48` 的原样落库。
- 库侧只有宽约束:`V2__create_platform_product_events.sql:11` `event_version smallint NOT NULL DEFAULT 1`
`:22` `CHECK (event_version > 0)`
**结论**:客户端可对任意事件上报任意正整数版本号并被静默接受;字典已到 v3 而契约仍宣称「全部为 1」。
裁决 **NEEDS WORK(确认遗留)**。M4 若新增事件,必须先定这个口径,否则新事件版本号写什么都「对」。
### 2.5 429 限流仍缺 —— 后端与契约**双零命中**
```
$ grep -rn "429|RateLimit|rateLimit|TOO_MANY" patbond-api --include=*.java --include=*.yml → 0 行
$ grep -n "429" patbond-doc/docs/api/openapi.yaml → 0 行
```
**结论**:不仅未实现,**契约里连 429 这个响应码都没声明**,即系统在任何路径上都不可能返回 429。
裁决 **NEEDS WORK(确认遗留)**
连带影响:`iteration-3/03:151` 规划过「429 按 `Retry-After` 退避……Retry-After 留接线点」。
客户端那条分支**永远走不到**,属当前**无法被任何测试覆盖的死分支**。
M4 上 AI 生成必然要限流(见 §3.5),届时这条分支才第一次有意义。
### 2.6 「我的收藏与草稿」页仍缺 —— 后端就绪、数据层就绪、**UI 零**
| 层 | 状态 | 证据 |
| --- | --- | --- |
| 契约 | ✅ 有 | `openapi.yaml``/api/v1/me/bookmarks [GET]``/api/v1/me/posts [GET]` |
| Flutter 数据层 | ✅ 有 | `community_repository.dart:185` `listMyPosts`(带 `status` 过滤)、`:279` `listMyBookmarks` |
| Flutter UI | ❌ **无** | `find lib -iname "*bookmark*" -o -iname "*draft*" -o -iname "*favorite*"`**零结果**`grep -rn "bookmark|draft|favorite" lib/app/`**零结果**(无路由) |
| 入口 | 假入口 | `profile_page.dart:49` 菜单项「我的收藏与草稿」仍走演示提示;`:45-46` 注释自承「后端能力已就位(`/me/bookmarks``/me/posts`),但列表页本单未做」 |
**死代码取证**`listMyBookmarks` 的生产调用方 **0 个**`grep` 仅命中 `community_repository.dart` 自身与 3 个测试文件)。
`listMyPosts` 有 1 个生产调用方:`post_compose_page.dart:196``_restoreLatestDraft()`
其注释自承「**草稿恢复(最小实现:最新一条)**」,`limit: 1, status: PostStatus.draft`
**结论**:裁决 **NEEDS WORK(确认遗留)**。收藏列表完全无 UI 且数据层是死代码;草稿只有「恢复最新一条」,无列表、无自动保存。
这一条对 M4 直接相关 —— 见 §3.4。
## 3. M4 规划未证前提狙击
本节是本报告的重心。每条前提给出**它需要什么证据**与**实测到的证据**。
### 3.1 【最危险】真实 AI 模型服务:endpoint / 凭证 / 额度**三者全无**
需要的证据:一个可调用的模型服务地址、一份可用凭证、一个已确认的额度或计费口径。
实测:
```
$ grep -rniE "openai|anthropic|stability|replicate|dashscope|volcengine|comfyui|sdxl|api_key|apiKey" \
patbond-api --include=*.java --include=*.yml --include=*.yaml --include=*.sample
→ 零命中(排除 gen_random / generated / generation_job_id 等同形词后)
$ grep -oE "^[A-Z_]+" patbond-api/.env
→ PATBOND_DB_PASSWORD / PATBOND_INTERNAL_TOKEN / PATBOND_MINIO_ROOT_USER / PATBOND_MINIO_ROOT_PASSWORD
$ grep -oE "PATBOND_[A-Z_]+" patbond-api/deploy/init-secrets.sh | sort -u
→ 同上 4 个,无第五个
```
**即:整个项目的密钥面共 4 项(DB 口令、服务间 token、MinIO 用户/口令),无一条与任何 AI 服务商相关;
没有任何 HTTP 客户端、SDK 依赖、配置占位符指向任何模型服务。**
更关键的一条反证 —— **正典模型自己就只设想了 fixture 提供方**
`patbond_postgresql.sql:1495-1499` 的种子数据:
```sql
INSERT INTO creation.generation_models
(id, code, display_name, provider_code, provider_model_name, media_kind, sort_order)
VALUES
(…, 'patbond-v1', 'Patbond-V1', 'fixture', 'patbond-image-v1', 'image', 10),
(…, 'patbond-v1', 'Patbond-V1', 'fixture', 'patbond-video-v1', 'video', 10);
```
`provider_code = 'fixture'``:1510` 的样例 job 亦为 `provider_code_snapshot='fixture'`
`provider_request_id='fixture-provider-job-1'`。**设计者从未假设 M4 接真模型。**
同时注意 `generation_jobs` 有三个 **NOT NULL** 的快照列:
`provider_code_snapshot``provider_model_snapshot``model_version_snapshot``:620-622`)。
任何一次入队都必须填出这三个值 —— 接 fixture 也要填,这是**契约级强制**,不能含糊。
**裁决:BLOCKED。**
**这直接击穿「本迭代端到端可验证」的说法。** 两条路,必须现在拍板选一条,不能含混:
- **路 A(推荐,可 CERTIFIED**M4 明确只做 **fixture/stub provider**,与正典种子一致。
「端到端可验证」重新定义为「入队 → 租约领取 → 状态机流转 → 产物落 MinIO → 挂帖」全链路可验,
**产物是 stub 图**(例如把输入图做一次确定性变换)。这条路的每一环都能被自动化测试覆盖,无外部依赖、无额度风险、CI 可跑。
- **路 B(需先解阻塞)**:接真模型。**开工前必须先有**:服务商选定 + endpoint + 凭证注入方案
`init-secrets.sh` 增第 5 项 + compose 环境变量 + ADR)+ 额度/计费上限 + 失败与超时口径 +
ADR 记录「凭证不入库」如何保证。这些**一件都还没有**。
⚠️ 若规划文本同时写「fixture 兜底」又写「端到端接通真实模型」,那是自相矛盾,
按本报告纪律判 **NEEDS WORK**,必须二选一并写进 ADR。
### 3.2 【前提是伪命题】Worker 队列**不需要** Redis / MQ
这条前提我给出的是**否证**:任务书假设「Worker 队列需要 Redis 或 MQ」,实测该假设本身不成立。
- 现有 compose 六服务 = `postgres, minio, user, auth, pet, community`。**无 Redis、无 RabbitMQ/Kafka、无任何 broker。**
- 但正典模型**已经给出了完整的 Postgres 租约队列设计**,不需要 broker:
- `generation_jobs` 具备队列所需全部列:`status``queued/running/succeeded/failed/cancelled`)、
`priority``attempt_count`/`max_attempts``next_attempt_at``lease_owner``lease_expires_at``progress``version`
- 两个专用偏索引:`ix_generation_jobs_queue ON (priority DESC, next_attempt_at, created_at, id) WHERE status='queued'`
`ix_generation_jobs_running ON (lease_expires_at, id) WHERE status='running'``:712-715`)。
- `patbond_postgresql.sql:1896-1917` 直接给出了**多 Worker 并发领取的标准写法**,注释即
「AI worker claim pattern for multiple concurrent workers」:
```sql
WITH picked AS (
SELECT id FROM creation.generation_jobs
WHERE status='queued' AND attempt_count < max_attempts AND next_attempt_at <= now()
ORDER BY priority DESC, next_attempt_at, created_at, id
FOR UPDATE SKIP LOCKED LIMIT 1
)
UPDATE creation.generation_jobs j
SET status='running', started_at=now(), progress=1, attempt_count=attempt_count+1,
lease_owner=:worker_id, lease_expires_at=now()+interval '2 minutes', version=version+1
FROM picked WHERE j.id=picked.id RETURNING j.*;
```
- `ck_generation_jobs_state` 是一条**五分支状态机 CHECK**,把每个 status 允许的字段组合钉死
(如 `running` 必须 `lease_owner IS NOT NULL``succeeded` 必须 `progress=100 AND output_asset_id IS NOT NULL`)。
这是很强的资产:**状态机由数据库强制,Worker 写错就报错**。
- 调度侧也有现成先例:`UserApplication.java:7` 已有 `@EnableScheduling`
`SessionCleanupJob.java:32` 已有一个 `@Scheduled` 任务在生产运行。轮询式 Worker 与它同构。
**裁决:CERTIFIED(基础设施无需扩容)。** 这是 M4 少有的**真·已就位**项。
**明确建议不要引入 Redis/MQ** —— 会凭空增加一个容器、一套运维面、一份 ADR,而正典设计已否决其必要性。
⚠️ 但两个**未证的衍生点**必须写进规划:
1. **`lease_expires_at` 的回收者不存在。** 设计给了 `ix_generation_jobs_running` 索引,
却没有任何代码回收超租约的僵尸 job。这与已登记的「`uploading` 超时清理任务」是**同型缺口**
而那一项至今未做(`releases.md:123` 已登记)。M4 若不写回收器,`running` 的 job 崩了就永久卡死。
2. **Worker 放哪个模块未定。** `@EnableScheduling` 只在 `patbond-user`。新建 `patbond-creation`
模块意味着第 7 个容器(compose 从 6 → 7),需 ADR。若塞进现有模块,则 AI 长任务会与在线请求争线程池。
### 3.3 【半真半假,最易误判】「媒体链路 M3 已就位可直接复用」
这句话必须**按方向拆开**验,因为读侧成立、客户端写侧成立、**服务端写侧完全不存在**。
**取证 —— 全代码库 S3 调用面**:
```
$ grep -rhoE "\b(s3|client|s3Client)\.[a-zA-Z]+\(" patbond-user/src/main/java/com/patbond/patbond/user/
→ client.close( client.createBucket( client.headBucket( client.headObject(
```
即整个后端对对象存储的能力只有:建桶、探桶、探对象元数据、关闭客户端。
**零 `putObject`、零 `getObject`。** `patbond-pet``patbond-community` 侧则只有 `S3Presigner`
做本地 SigV4 签名(`MediaUrlSigner.java`),连网络调用都没有。
| 复用面 | 结论 | 证据 |
| --- | --- | --- |
| 预签名 GET 读取(帖图/头像展示) | ✅ **真可复用** | `MediaUrlSigner.java` 在 pet/community 两处已复制运行 |
| 客户端上传三段(createUpload → 直传 → complete | ✅ **真可复用** | `MediaService.java` + `/media/uploads` 两端点已在契约内 |
| 桶初始化 | ✅ 可复用 | `S3ObjectStorage.ensureBucket():66` |
| **服务端写对象(Worker 产出物落盘)** | ❌ **净新增,零基础** | 无 `putObject` |
| **服务端读对象字节(把输入图喂给模型)** | ❌ **净新增,零基础** | 无 `getObject``headObject` 只取元数据) |
| `purpose` 白名单 | ⚠️ 需扩展 | `application.yml.sample:59` = `post_image,user_avatar,pet_avatar``MediaProperties.java:66` 同值。无 AI 输入/输出用途 |
| 产物尺寸写入 `media.assets` | ❌ 不可复用 | 见 §2.3`insertUploading` 无尺寸列、无 UPDATE 路径 |
**裁决:NEEDS WORK(该表述必须在规划里改写)。**
准确表述应为:「媒体的**读链路与客户端上传链路**可直接复用;**服务端读写对象字节的能力为零,是 M4 的净新增工作**。」
好消息是 `purpose` 扩展很便宜 —— `MediaProperties.java:61` 注释明写该白名单是
「configuration + contract-enum change, **never a migration**」,即改配置 + 改契约枚举即可,不需要迁移。
⚠️ 一个**具体的下游矛盾**`generation_jobs` 自己存了 `width_px`/`height_px``:616-617`
`CHECK … BETWEEN 64 AND 8192`),也就是 AI 产物的尺寸在 `creation` schema 里**是已知的**
`media.assets` 的同名列永远为 NULL(§2.3)。若 Worker 不顺手把尺寸写进 `media.assets`
客户端渲染 AI 产物会**继续回落 4:3 占位**(这正是已登记的「单图帖回落 4:3」遗留)。
M4 有一次几乎零成本修掉它的机会(Worker 本来就知道尺寸),**建议顺带修掉,不要再滚一轮**。
### 3.4 「社区草稿已就位可复用」—— 只有一半
- ✅ 库侧就位:`V5__community_baseline.sql:53` `status varchar(16) NOT NULL DEFAULT 'draft'`
- ✅ 契约就位:`/api/v1/me/posts` 支持 `status` 过滤;`Post.category` 枚举已含 `ai_creation`
- ✅ **M4 的读侧钩子已预留**:契约明写 `ai_creation 为 M4 预留值,M3 不开放写入(提交 400/40000`
且有测试钉住 —— `PostLifecycleIntegrationTest.java:102-104` 断言 M3 提交 `category: ai_creation` 被拒。
**M4 要做的是把这个拒绝改成放行**,需同步改契约、快照、该测试。
- ❌ **草稿 UI 只有「恢复最新一条」**:见 §2.6。无草稿列表、无自动保存(`releases.md:123` 已登记为遗留)。
**裁决:NEEDS WORK。** 「AI 创作产物存草稿再发布」这条产品路径依赖草稿列表,而列表不存在。
规划若假设「用户可以把多个 AI 产物存成草稿再挑一个发」,那是**未证前提** —— 当前只能恢复最新一条,多草稿会互相覆盖。
### 3.5 【高危】长耗时异步任务在桌面端**基本无法验证**,M4 极可能再产出一批挂起项
这是我对 M4 最强的风险判断,有三条硬证据。
**证据一:埋点在桌面端结构性不可用。**
`openapi.yaml:2373-2375`
```yaml
platform:
type: string
enum: [android, ios]
```
枚举只有两个值。Linux 桌面上报 `platform=linux` → 整批 400。这不是缺陷而是契约内行为,
`device-verification.md` 通用前置已写明「桌面/Web 不可用……platform 值不在契约枚举内会被服务端整批拒绝」。
**推论:M4 新增的任何创作漏斗事件(生成发起/成功/失败/耗时分桶),其「落库」验证只能在 Android 上做 → 又是一批真机挂起项。**
**证据二:选图与压缩在 Linux 桌面无原生实现,桌面实测走的是替身。**
`image_picker``flutter_image_compress` **确实已实现**`media_picking.dart:28` `SystemMediaImagePicker`
`media_compression.dart:42` `FlutterImageCompress.compressWithList`)——
`media_uploader.dart:570-571``app.dart:53` 都注明「Linux 桌面既无 `image_picker` 也无
`flutter_image_compress` 的原生实现,桌面真链路**只替换选图与压缩两层**」。
(顺带纠正我方任务书的一处错误表述:并非「无 image_picker/compress 实现」,
而是**实现有、Linux 平台支持无**。这是原始文件与转述的又一处偏差。)
**推论:AI 创作必然以「选一张宠物照」开头 → 该入口在桌面永远是替身 → 首步就无法真实验证。**
**证据三:现有 E2E 与 integration_test 都不在门禁里。**
四份 `test_e2e_*_manual.dart` 是手工脚本,需 compose 全栈在位,**不在 `flutter test` 内、CI 不会跑**
07 号亦独立取证 `integration_test/` 下 4 个真机测试完全在 `flutter test` 之外)。
`flutter test` 的 2 个 skip 也正是需要后端的 smoke(`PATBOND_MEDIA_SMOKE=1` / `PATBOND_DETAIL_SMOKE=1`)。
**推论:长耗时异步任务(入队→轮询→完成,正典租约 2 分钟)如果照现有模式验证,
只会再产出一份「手工脚本 + 真机清单」,自动化门禁覆盖率为零。**
**裁决:NEEDS WORK,且这是 M4 最可能失控的一面。**
**可解除的具体做法**(这些都能在桌面/CI 内做,不必等真机):
1. **Worker 状态机做纯后端集成测试**Testcontainers + fixture provider)——
入队、SKIP LOCKED 并发领取、租约过期回收、重试退避、`max_attempts` 耗尽转 failed、幂等键去重。
这些**全部不需要客户端、不需要真模型、不需要真机**,可 100% 进 `mvnw test` 门禁。这是 M4 最该先建的护栏。
2. **`platform` 枚举扩容拍板**:若想让桌面参与埋点验证,就在契约 `enum``linux`(或加通用 `desktop`)。
这是一行契约改动 + 快照同步,能一次性解掉 M2/M3/M3.5 遗留下来的「事件落库只能真机验」死结,
**收益跨三个迭代**。若决定不加,则必须承认 M4 的埋点验证同样挂起,并写进清单。
3. **入队/轮询/取消的契约面进 E2E 脚本**(第五份),并同步更新回归清单(见 §4.1)。
### 3.6 其余未证前提(一次列清)
| 前提 | 实测 | 裁决 |
| --- | --- | --- |
| 「V5 已留裸列,M4 补外键很轻」 | 裸列确实在(`V5:48`)。但 V6 需 `CREATE SCHEMA creation` + 3 张表 + ~12 索引 + 3 触发器 + 补 1 个外键,**不是轻量迁移** | NEEDS WORK(工作量被低估) |
| 「跨 schema 外键照正典补回即可」 | `V5:20-30` 注释确立的纪律是「未迁移 schema 的跨库外键一律裁剪」。`generation_jobs` 自身引用 `identity.users`/`pet_health.pets`/`media.assets`(均已存在,可保留),但它还被 `marketplace` 之外的 `platform.regions` 牵连口径 —— **需逐条确认哪些保留哪些裁剪**,无现成结论 | 未取证,需 API/DBA 角色出定型表 |
| 「A/B 实验前置已就位」 | 服务端字典**有** `experiment_exposed``EventDictionary.java:103`,注释自承「dictionary ahead of its M4 first use」)。但 **Flutter 侧零引用**`grep -rn "experiment_exposed\|experimentKey" lib/ test/`**零命中**。即无任何客户端能发这个事件 | NEEDS WORK(仅服务端半就位) |
| 「北极星 2026-09-21 首次出数」 | 依赖 M2 两项真机验证(§2.2)。技术上可行,但**至今 0 项执行**,且 `platform=linux` 死结未解 | NEEDS WORK(可行但已拖期) |
| 「零迁移」惯例可延续 | M3.5 做到零迁移是因为所需列 V1/V3/V5 已存在。**M4 必然需要 V6**(`creation` schema 完全不存在),零迁移惯例**在 M4 必然中断** | 需明确写进规划,别延续错误预期 |
## 4. 发布流程与服务器侧登记项(协调者追加三项)
按分工,此处**不重复** Evidence Collector 对 checklist 的逐步清点,只判「规划是否站得住」。
### 4.1 发布流程:文档内部自相矛盾,且状态检查上下文的选型有洞
**a)同一份 `releases.md` 自己打自己(回归清单份数)**
| 位置 | 表述 |
| --- | --- |
| `releases.md:36` | 「**E2E 回归(四份,同环境串行)**……M1 7/7 + M2 11/11 + M3 14/14 + M3.5 10/10 = 42/42 场景」 |
| `releases.md:56` | 「**回归清单由两份改为四份**M1/M2/M3/M3.5」 |
| **`releases.md:131`** | 「1. 完成 checklist 第 1~2 步(三仓 CI 绿 + **E2E 双份回归 PASS**)」 |
`:131` 位于「**发布后生效的纪律**」一节 —— 也就是**给下一次(即 M4)发布看的那段前瞻指令,写的是「双份」**。
`:36`/`:56` 是本次回顾记录,写的是「四份」。**前瞻指令与本次结论矛盾,且错在前瞻侧。**
`:131` 执行 M4 发布,会只跑 2 份、漏掉 M3/M3.5 两份共 24 个场景。
叠加 Evidence Collector 独立取证的「发布 checklist 正文(`iteration-3/08:84`)仍写着 M2+M3 两份、
8 步里 4 步过期、标题至今是『草案』从未固化进 `git-workflow.md`」——
**即『四份』这个正确结论只活在回顾表格里,两处可执行入口(checklist 正文 + 纪律段)都还是旧的。**
裁决 **NEEDS WORK**。解除条件:把 `releases.md:131` 的「双份」改「四份」,
同步 `iteration-3/08` checklist 正文,并把 checklist 从「草案」固化进 `git-workflow.md`
**b)状态检查上下文选了 `(push)`,与「等 PR 检查转绿」的指令不是一回事**
`releases.md:110-114` 登记的必需上下文是:
| 仓库 | protected | 状态检查上下文 |
| --- | --- | --- |
| patbond-api | `true` | `CI / backend-test **(push)**` |
| patbond-flutter | `true` | `CI / flutter-gates **(push)**` |
| patbond-doc | 未启用 | —— |
`releases.md:133` 的指令是「3. 等 **PR 的** CI 状态检查转绿(即上表的 `status_check_contexts`)」。
**这两者不等价。** 我实测(§1.1)同一个 commit 上 `(push)``(pull_request)` 是**两个独立上下文**
```
CI / backend-test (push) success 5m31s 09:44:05
CI / backend-test (pull_request) success 6m42s 09:54:37
```
api 工作流是 `on: push: branches: [dev]`。因此 `dev → main` 的 PR 场景下,
`(push)` 状态是**推 dev 时就已经写好的**,PR 开出来之前它就是绿的。
**结论:这个门禁实际由「推 dev」满足,而不是由 PR 满足。**
若某次 PR 的 `(pull_request)` 检查失败、而先前推 dev 的 `(push)` 是绿的,**合并仍会被放行** —— 门禁形同虚设。
`releases.md:116` 自承选择显式写死上下文是为了消除「留空 = 空集为真反而放行」的歧义,
方向正确,但**挑错了上下文**:要真正在 PR 时把关,必需上下文应是 `(pull_request)`(或两者都要求)。
裁决 **NEEDS WORK(真实的门禁漏洞)**。解除条件:把必需上下文改为
`CI / backend-test (pull_request)` / `CI / flutter-gates (pull_request)`,或两个上下文都列为必需,
并重新用 Gitea API 核实后更新 `releases.md` 表格。
(注:我侧 `GET /branch_protections` 返回 **401**,无 token 故无法复核当前实际配置,
上述判断基于文档登记值 + 我实测到的上下文命名事实。**修正前必须先用有权限的 token 核一遍实际配置。**)
**(c)doc 仓的口径需要说清楚,避免误读**
`releases.md:128` 的「`main` 已禁止直接推送——影响 `main` 的一切变更一律走 PR」是**全局语气**,
`:114`/`:116` 明确 doc 仓 main **未启用保护**、「即日常分支、不参与发布分支语义,按规划不设保护」。
两处并存容易被后续 agent 误读成「doc 也要走 PR」。且 doc 工作流是 `on: push: branches: [main]`
**若哪天真给 doc main 加上保护并要求 `(push)` 上下文,会直接死锁**(推 main 被禁 → `(push)` 永不产生 → PR 永不可合)。
建议在 `:128` 加一句限定「(api 与 flutter 两仓;doc 仓 main 保持直推)」。裁决 **NEEDS WORK(表述风险)**
### 4.2 服务器侧:文档站公开可访问,登记项仍悬空
`server-exposure.md` §2 常驻服务表中该行原文:
> **文档站(patbond-doc** | 经 nginx 443 | mkdocs 构建产物,含架构/部署/迭代全部文档 | —— |
> ✅ 运行;⚠️ **公开可访问,待评估是否加 basic auth 或 IP 白名单**(无凭证内容,但暴露内部架构细节)
**现状核实**
- 该项**仍是「待评估」,无结论、无归属决策**(「归属决策」列为空 `——`)、无 ADR 记录。
- `https://git.patbond.cn/` 实测返回 **200**nginx 在服;文档站按 `sites-enabled` 另一域名分流,我未探测其域名,
**未取证**:文档站实际 URL 与是否真的无鉴权)。
- §1 端口表「最后确认」全部停在 **2026-09-11**。而 `server-exposure.md` 自订纪律 ②
是「**每次迭代收官核对一遍,更新『最后确认』**」,M3.5 收官与 v0.4.0 发布均发生在 **09-14**
该列**未更新**。按同文档纪律 ⑤「本页与实际不符即为缺陷」,这本身是一处待补。
**我的评估与建议(只取证与建议,不动服务器)**:
风险等级判**中**,倾向「应当加访问控制」,理由是三条**已发生**的事实叠加:
1. 文档站内容包含 `server-exposure.md` 本身 —— 即**一份完整的对外端口清单与常驻服务清单**,
还包含 `ci-runner-setup.md`CI 拓扑)、`decisions.md`(全部 22 条架构决策)、
数据库正典 SQL(全表结构与约束)。这是一份**给攻击者的现成侦察报告**。
2. 本项目**刚刚发生过**一次真实入侵尝试(`iteration-3.5/07`Gitea gitconfig 注入),
`server-exposure.md` 的缘起就是「Nacos 在 ADR-002 移除后仍暴露公网近两个月」——
**说明本环境的暴露面治理确实曾经失守过**,不是理论风险。
3 该站与 Gitea **同一台机器、同一个 nginx**`sites-enabled/` 同级分流)。文档站的任何 nginx 配置失误
与代码托管共享爆炸半径。
**建议**:加 IP 白名单或 basic auth(二者皆可,basic auth 更省事),并把结论写成 ADR 或在
`server-exposure.md` 的「归属决策」列填上,把状态从「待评估」改成终态。
成本约十分钟 nginx 配置,**不应再挂第四个迭代**。
⚠️ 但需注意一条**执行顺序约束**:若加 basic auth`mkdocs build` 的产物是静态站,不受影响;
但若有任何自动化在拉取文档站 URL 做校验,会被 401 打断 —— 我**未取证**是否存在这类消费方,
落地前应先 grep 三仓有无对文档站 URL 的自动访问。
裁决 **NEEDS WORK(登记项悬空,建议本迭代内闭环)**
## 5. 真实问题清单(按严重度排序)
> 编号 P1~P12。「新发现」= 本报告首次登记;「已登记」= 文档已知但状态需纠正。
### P1 —— 阻断级:M4 无任何 AI 服务商 endpoint / 凭证 / 额度(新发现)
密钥面共 4 项,无一与 AI 相关;代码库零 provider SDK;正典种子 `provider_code='fixture'`
**影响**:「AI 创作端到端可验证」当前是无证据的口号。**必须在开工前二选一**(fixture 路 / 真模型路,见 §3.1)。
`generation_jobs` 三个 NOT NULL 快照列迫使这个选择必须显式。
### P2 —— 阻断级:服务端零对象写能力,「媒体链路可复用」被高估(新发现)
全库 S3 调用仅 `createBucket`/`headBucket`/`headObject`/`close`。Worker 落产物需 `putObject`
喂输入需 `getObject`**两者皆为净新增**。
**影响**:M4 媒体侧工作量被系统性低估;规划中「直接复用」的措辞必须改写(§3.3)。
### P3 —— 高:埋点白名单实为 41 条,四份文档写成 42,且无测试锁定(已登记数字错误)
实测 `Map.entry` 41 个。差异来源已定位到 **`feature-checklist.md:218` 的算术错误**
它写「community 域 **19** + experiment_exposed」= +20 → 22+20=42
`EventDictionary.java` 类注释自己的拆分是 post **8** + feed **2** + interactions **8** = **18**
18 + `experiment_exposed` 1 = 19 增量,22+19 = **41**。即「community 域 19」把 `experiment_exposed` 重复计了一次。
**影响**:中等(数字失真,不影响运行),但**无任何测试断言该数量**(唯一规模断言是 `operations()==45`),
所以这个数字会继续漂。**建议加一条 `WHITELIST.size()` 断言把它钉住**,成本一行。
### P4 —— 高:v0.4.0 发布记录把 381 测试写成 379(已登记数字错误)
实测 `3cd8005` = **381**。差异来源已定位:`iteration-3.5/04:150-159` 明确记录契约冻结
「测试数 **379 → 381+2**」,而 `3cd8005` **正是那次契约冻结的提交**
`releases.md:17``:35``feature-checklist.md:5` 三处写 379,都是照抄了冻结**前**的数字。
**影响**`releases.md` 把 hash `3cd8005` 与 379 绑在一行,是内部不自洽的发布记录;
后续任何「测试数应为 379」的回归判断都会误判。
### P5 —— 高:门禁漏洞——必需状态检查选了 `(push)` 而非 `(pull_request)`(新发现)
见 §4.1(b)。`(push)` 在 PR 开出前就已绿,门禁实际由推 dev 满足;PR 检查红也能合并。
**影响**`main` 分支保护的实际强度**低于文档宣称**。这是本次发现的唯一安全/流程类真实漏洞。
### P6 —— 中高:M4 极可能再批量产出「只能真机验」的挂起项(新发现)
三条硬约束叠加:`platform` enum 仅 `[android, ios]`(桌面埋点整批 400);
Linux 桌面无选图/压缩原生实现(走替身);四份 E2E + `integration_test/` 4 个测试**全在 CI 之外**
`flutter test` 的 2 个 skip 也是需后端的 smoke。
**影响**:若不先建后端侧 Worker 状态机自动化测试并拍板 `platform` 枚举,M4 收官时挂起项会从 10 项继续往上加。
### P7 —— 中高:真机挂起项实为 10 项,`releases.md` 只登记 6 项(已登记但漏项)
`releases.md:121` 漏掉整个 M3.5 四项。转述链 4→6→8→10 每一跳都在丢项(§2.1)。
其中 **M3.5-3 caregiver 项因前置造数 SQL 从未补,当前不可执行**
**影响**M4 规划若照 `releases.md` 估算收尾工作量,会低估 4 项。
### P8 —— 中:发布纪律段仍写「E2E 双份回归」,前瞻指令错误(新发现)
`releases.md:131` 与同文件 `:36`/`:56` 矛盾,且错在给 M4 用的前瞻侧;
叠加 checklist 正文(`iteration-3/08:84`)仍写两份、8 步中 4 步过期、至今是「草案」未固化。
**影响**M4 发布会漏跑 24 个 E2E 场景。
### P9 —— 中:`429` 限流在后端与契约**双零命中**,客户端退避分支是死代码(已登记)
契约里连 429 响应码都未声明。M4 上 AI 生成必然需要配额限流,届时这条分支才第一次有意义(§2.5)。
### P10 —— 中:`widthPx`/`heightPx` 永久 NULL,且有一条「测试绿但生产空」的陷阱(已登记 + 新发现陷阱)
写路径与更新路径均不存在;`PostMediaAttachIntegrationTest:44-45` 断言 640/480 靠的是
`CommunityTestData:55` 的夹具直插。**这是 M3.5「nickname」教训的同型复发。**
M4 的 Worker 天然知道产物尺寸,**有一次近乎零成本修掉的机会**(§3.3 末)。
### P11 —— 中:`eventVersion` 服务端零校验,契约描述已过期(已登记)
契约称「字典 v1 全部为 1」,实际字典 v3;服务端仅 `@NotNull`,任意正整数均被接受(§2.4)。
M4 新增事件前必须定口径。
### P12 —— 低:`patbond-doc``.gitignore``site/` 仅靠**全局** gitignore 屏蔽(Evidence Collector 取证,我侧补充成因)
我侧补充证据:`git check-ignore -v site` 返回 `/home/lx/.gitignore_global:183:/site`
即屏蔽规则来自**本机用户级全局配置**,不在仓库内。
**影响**:任何新机器/新克隆/CI 容器里跑 `mkdocs build` 都会让 `site/`(数百文件)变成未跟踪,
极易被误 commit。修复成本:仓库根加一行 `site/``.gitignore`
### 另记:一处**正向**偏差(不是问题,但必须纠正认知)
CI 并非「仍红」。三仓 HEAD 全绿、`(push)``(pull_request)` 双上下文均已真实跑过(§1.1)。
`iteration-3.5/04 §7.3` 的「CI 仍红」是 09-11 的历史快照,**M4 开工不应把它当风险项继承**。
## 6. 逐项裁决与解除条件
### 6.1 基线资产(M4 可以放心站上去的部分)
| 项 | 裁决 | 依据 / 解除条件 |
| --- | --- | --- |
| 契约 v1.4.0 规模与四模块字节级快照锁 | ✅ **CERTIFIED** | 32/45/75 逐格相符;md5 五处一致。无条件可用 |
| Flyway V1~V5 链 + `generation_job_id` 裸列 | ✅ **CERTIFIED** | `V5:48``REFERENCES``V5:95` 索引在,无 V6 |
| 六容器编排 | ✅ **CERTIFIED** | 6 services 实测;**且 M4 无需扩容**(§3.2) |
| Worker 队列基础设施 | ✅ **CERTIFIED(无需 Redis/MQ** | 正典 SKIP LOCKED 租约设计 + 五分支状态机 CHECK + 两个偏索引 + 已有 `@EnableScheduling` 先例 |
| ADR-001~022 连续无缺号 | ✅ **CERTIFIED** | 22 个标题实测 |
| mkdocs `--strict` 可构建 | ✅ **CERTIFIED** | exit 01.71s |
| CI 三仓门禁 | ✅ **CERTIFIED** | 五个上下文全 `success`(09-14)。**注:门禁强度另见 P5** |
| flutter 597 测试 | ✅ **CERTIFIED(带注)** | 597 passed;注:2 个 env-gated smoke 默认跳过 |
| api 测试全绿 | ✅ **CERTIFIED(数字须改 381** | BUILD SUCCESS、0 失败 0 跳过;**解除条件**:把三处 379 改 381 |
### 6.2 M4 开工前必须拍板/解阻塞(BLOCKED 与高危 NEEDS WORK
| 项 | 裁决 | 解除条件 |
| --- | --- | --- |
| AI 模型服务来源 | 🔴 **BLOCKED** | 二选一并写进 ADR**路 A** 只做 fixture provider(推荐,全链路可自动化验证);**路 B** 接真模型,则须先备齐 endpoint + 凭证注入方案(`init-secrets.sh` 第 5 项 + compose 环境变量)+ 额度上限 + 超时/失败口径。**在此之前不得声称「端到端可验证」** |
| 「端到端可验证」的定义 | 🔴 **BLOCKED** | 随上一条同时定义。若走路 A,须显式写明「产物为 stub,不验证生成质量」 |
| 服务端对象读写能力 | 🟠 **NEEDS WORK** | 承认为净新增工作项并排期:`putObject`(产物落盘)+ `getObject`/预签名读(输入喂模型)+ `purpose` 白名单扩 AI 用途(改配置 + 契约枚举,无需迁移) |
| 「媒体链路可直接复用」表述 | 🟠 **NEEDS WORK** | 规划文本改写为「读链路与客户端上传链路可复用;服务端读写对象为净新增」 |
| Worker 归属模块与容器数 | 🟠 **NEEDS WORK** | 出 ADR:新建 `patbond-creation`(compose 6→7)还是并入现有模块(需评估线程池隔离)。当前无结论 |
| 租约超时回收器 | 🟠 **NEEDS WORK** | 正典给了 `ix_generation_jobs_running` 索引但无回收代码。M4 必须实现,否则 `running` job 崩溃即永久卡死。与既有「`uploading` 超时清理」同型缺口,建议一并做 |
| M4 是否再产出真机挂起项 | 🟠 **NEEDS WORK** | 三条并行解除:① 后端 Worker 状态机进 `mvnw test` 门禁(不需真机/真模型,**M4 第一优先**);② 拍板 `platform` enum 是否加 `linux`/`desktop`;③ 新增第五份 E2E 脚本覆盖入队/轮询/取消契约面 |
| 「零迁移」预期 | 🟠 **NEEDS WORK** | 明确写进规划:**M4 必然需要 V6**(`creation` schema 完全不存在),零迁移惯例在此中断 |
| V6 工作量 | 🟠 **NEEDS WORK** | 按 `CREATE SCHEMA` + 3 表 + ~12 索引 + 3 触发器 + 补 1 外键估,不是轻量迁移。需 DBA/API 角色出跨 schema 外键保留/裁剪定型表 |
### 6.3 遗留项裁决(M4 需明确「修」还是「继续挂」)
| 项 | 裁决 | 解除条件 / 建议 |
| --- | --- | --- |
| 真机挂起 10 项 | 🟠 **NEEDS WORK** | 先修**登记口径**`releases.md:121` 补 M3.5 四项、`feature-checklist.md` 补跟踪 2 项);再按 §2.2 分类推进 |
| M2 两项 @ 09-21 时限 | 🟢 **可行(判 CERTIFIED-可行)** | 本机 `Pixel_7` AVD + `/dev/kvm` 齐备,约 1 小时挂钟即可完成。只差导出 `ANDROID_HOME` / `PATH`。**「等真机」是伪阻塞** |
| M3.5-3 caregiver 项 | 🔴 **BLOCKED** | 前置造数 SQL 从未补,有设备也跑不了。解除条件:先补 `pet_health` 协作表造数 SQL |
| `widthPx`/`heightPx` 恒 NULL | 🟠 **NEEDS WORK(建议 M4 顺带修)** | Worker 已知产物尺寸,写入近乎零成本;不修则 AI 产物继续回落 4:3 |
| `eventVersion` 口径 | 🟠 **NEEDS WORKM4 前必须定)** | 定「版本号随字典版本」还是「随单事件 schema」;同步改契约描述(现称「字典 v1 全部为 1」);补服务端校验 |
| 429 限流 | 🟠 **NEEDS WORKM4 强相关)** | AI 生成需配额限流。落地时须同时在契约声明 429 + `Retry-After`,客户端死分支才能激活并被测到 |
| 「我的收藏与草稿」页 | 🟠 **NEEDS WORK** | 若 M4 产品路径含「多个 AI 产物存草稿再挑发」,则草稿列表是**前置依赖**,当前只能恢复最新一条、多草稿互相覆盖 |
| `experiment_exposed` 客户端缺失 | 🟠 **NEEDS WORK** | 服务端字典已有,Flutter 零引用。若 M4 要做 A/B,需补客户端发射点 |
| 白名单数量无测试锁定 | 🟠 **NEEDS WORK** | 加一行 `WHITELIST.size()` 断言,防数字继续漂 |
### 6.4 流程与服务器侧
| 项 | 裁决 | 解除条件 |
| --- | --- | --- |
| 必需状态检查上下文选型 | 🟠 **NEEDS WORK(真实漏洞)** | 改为 `(pull_request)` 或双上下文皆必需;用有权限 token 复核实际配置后更新 `releases.md` 表格。**我侧 401 未能复核当前配置** |
| E2E 回归份数(前瞻指令) | 🟠 **NEEDS WORK** | `releases.md:131` 双份→四份;同步 checklist 正文;从「草案」固化进 `git-workflow.md` |
| doc 仓 main 口径表述 | 🟠 **NEEDS WORK(低)** | `releases.md:128` 加限定「api 与 flutter 两仓;doc 保持直推」。**警告**:若日后给 doc main 加保护并要求 `(push)`,因工作流是 `on: push: branches:[main]` 会**死锁** |
| 文档站访问控制 | 🟠 **NEEDS WORK(建议本迭代闭环)** | 加 basic auth 或 IP 白名单;把 `server-exposure.md` 该行状态从「待评估」改终态并填「归属决策」列 |
| `server-exposure.md` 最后确认日期 | 🟠 **NEEDS WORK(低)** | 全表停在 09-11,但 v0.4.0 发布在 09-14。按该文档纪律 ②/⑤ 应更新 |
| `patbond-doc``.gitignore` | 🟠 **NEEDS WORK(低)** | 仓库根加 `.gitignore``site/`;当前仅靠 `~/.gitignore_global:183` 屏蔽 |
## 7. 给 M4 的最小可信化建议
不改变 M4 的产品目标,只让它**可被证明**。按优先级:
1. **先拍 AI provider 的板(路 A / 路 B),并写进 ADR。** 这是唯一的阻断项,其他一切排期都依赖它。
若一周内拿不到真模型凭证,就走路 A —— **fixture 路完全可以交付一个诚实的、全自动验证的 M4**
2. **第一波先建后端 Worker 的自动化护栏,不碰客户端。** 用 Testcontainers 覆盖:入队幂等、
SKIP LOCKED 并发领取、租约过期回收、重试退避、`max_attempts` 耗尽、五状态机流转。
这些**零外部依赖、零真机、可进 CI**,是 M4 唯一能靠自动化门禁守住的部分,应当先做厚。
3. **顺手修两个近乎零成本的历史遗留**`media.assets` 尺寸写入(Worker 本来就知道),
以及白名单数量断言。都是一次改动换一个永久防漂。
4. **拍板 `platform` 枚举。**`linux`/`desktop` 能一次解掉横跨 M2/M3/M3.5/M4 的
「事件落库只能真机验」死结,收益跨四个迭代,成本是一行契约 + 快照同步。
5. **本迭代内清掉 M2 两项真机验证**(09-21 时限,模拟器 1 小时即可),别让北极星首批读数标「未验收」。
6. **修掉发布流程的两处(P5 门禁上下文、P8 双份/四份)**,否则 M4 发布会重复本次的漏洞。
7. **不要引入 Redis/MQ。** 正典已否决其必要性;引入等于凭空多一个容器、一套运维面、一份 ADR。
---
## 附:本报告的取证边界
**已亲自取证**(命令 + 输出 / 文件 + 行号,均在正文内):三仓 HEAD 与 tag 与洁净度、
`mvnw clean test` 全量实跑(381)、`flutter test` 全量实跑(597 + 2 skip)、
openapi 32/45/75 与五处 md5、Flyway V1~V5 与 `V5:48`、compose 6 服务、
`EventDictionary` 41 条、ADR 22 条、`mkdocs build --strict`
**Gitea 五个 CI 上下文实时状态**、`platform` enum、`eventVersion` 全链路、
429 双零命中、`widthPx` 写路径缺失、S3 调用面全集、`.env`/`init-secrets.sh` 密钥面、
`purpose` 白名单、收藏/草稿 UI 缺失与死代码、E2E 场景自报计数、
Android SDK/AVD/KVM 可用性、`device-verification.md` 10 项、`releases.md` 内部矛盾、
`server-exposure.md` 登记项、`patbond-doc``.gitignore` 的成因。
**未取证(明确列出缺什么)**
| 项 | 缺什么 |
| --- | --- |
| E2E **234 断言**精确值 | 机械可数 226(210 `check(` + 16 `✗`)。差 8 处需人工核对 `assertMeShape` 类辅助函数的内部断言计法 |
| E2E 四份**零失败重跑** | 需 compose 六容器起栈 + 串行跑约数十分钟。本次**未重跑**,仅采信 `08` 号报告 |
| **契约矩阵 181 格** | 无机械可数来源;代码内唯一规模断言是 `operations()==45`。需人工按 `iteration-3.5/04 §3` 表逐格复核 |
| **零迁移在存量库上实证** | 需起 compose 于既有 `pgdata` volume 观察 Flyway 日志。未执行 |
| `check-secrets.sh --all` | 未执行(三仓) |
| **`main` 分支保护实际配置** | `GET /branch_protections` 返回 **401**,无 token。文档登记值未能复核 —— **P5 修正前必须先用有权限 token 核实** |
| 服务器侧实际暴露面 | 未登录服务器执行 `ss -tlnp`;文档站实际域名与是否真无鉴权未探测(仅确认 `git.patbond.cn` 返回 200 |
| 跨 schema 外键保留/裁剪定型 | 需 API/DBA 角色出定型表,非本报告职责 |
**未修改任何生产代码**;未改 `mkdocs.yml`;未执行任何 `git commit` / `push`
`mkdocs build --strict` 重建了被全局 gitignore 屏蔽的 `patbond-doc/site/`,三仓 `git status` 仍为空。
</content>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,762 @@
# 第四迭代埋点与实验规划(AI 创作)
> 角色:Experiment Tracker
> 日期:2026-09-14
> 前序:`iteration-2/06`(字典 v2、北极星定义式、H1~H4、A/B 八项前置)、`iteration-2/24`(白名单 v2)、`iteration-3/06`(字典 v3、聚合曝光裁定、A/B 前置推进)、`iteration-3/22`(白名单 v3)、`iteration-3/10`(队列硬化)、`iteration-3/28` §7 观察项 2eventVersion 歧义登记)
> 依据:`patbond-api` dev@3cd8005 `EventDictionary.java` / `TrackEventsRequest.java` / `AnalyticsService.java` / `V2__create_platform_product_events.sql` 现行实现;`patbond-flutter` dev@fbcd734 `lib/analytics/``lib/features/*/*_analytics.dart` 现行挂接;契约 v1.4.0`development-plan.md` §M4`device-verification.md`
> 范围:M4 AI 创作纵切(模型/风格目录、生成任务创建/查询/取消、Worker 队列执行、产物入媒体表、一键建社区草稿)的埋点与实验;本地服务(M5)不在本轮定义
> 性质:纯规划文档,供 M4 开发工单直接引用;本报告未改动任何生产代码或配置
**速览(七个核心结论)**
1. **北极星 09-21 首次出数不可行,且不是「来不及」而是「窗口已关闭」**。09-21 是 W37 队列(首记 09-07~09-13)+8 天的成熟日,入队窗口已于 **09-13 结束**——今日起做任何事都无法为 W37 补进一个用户。给出三档降级方案,推荐「触发条件替代日期承诺」+「基建就绪读数」,见 §0。
2. **decisive finding:阻塞 M2 两项真机验证的「待设备到位」已经不成立**。本机 `~/Android/Sdk` 已装 android-36 x86_64 系统镜像与 `platform-tools/adb``flutter emulators` 列出一个可直接启动的 **Pixel_7** AVD;而 `device-verification.md:9` 明文接受「Android 真机(推荐)或 Android 模拟器」。两项合计约 55 分钟,**今天即可执行**。这把最早可得的北极星读数从「无限期」拉到 **2026-09-28W38 队列)**,见 §0.3。
3. **白名单实际 41 条,不是 42**,根因是 `experiment_exposed` 被重复计数(community 域实为 18 条,加 platform 域 1 条共 19 条新增;文档记作「19 + experiment_exposed」= 20)。**无任何测试断言字典条数**,故漂移四份文档一路不可见。M4 前置项:补一个条数断言,见 §1.1、§7。
4. **eventVersion 定型为「每个事件自身的 props schema 版本」**(否决「字典世代」读法):客户端现行硬编码 1 在此读法下**本就正确**,零客户端改动、零历史回填;反之则全部历史行皆错且不可修复。配套给出 bump 规则与三层校验对齐方案,见 §2。
5. **`platform=linux` 整批 400 是契约明文行为,枚举排除 linux 是三层锁死的设计意图;缺陷在客户端**——`analytics_service.dart:99-100` 注释称「逐条 rejected(不影响客户端)」与实现相反:实际整批 400 且被 `uploadBatch` 判为永久拒绝**整批丢弃**,桌面端 100% 静默丢事件。处置为客户端本地短路 + 调试覆盖开关,不动枚举,见 §3。
6. **服务端生成侧不建第二条埋点管道**`platform.product_events``anonymous_id`/`session_id` NOT NULL 与 `platform IN ('android','ios')` CHECK 使 Worker 事件无法入表;裁定以 `creation.generation_tasks` 事实表为服务端指标权威,经 `creationTaskId` 与客户端事件拼接,见 §4.2。
7. **M4 首个「实验」应是 A/A 基建验证空跑,真实 A/B 冻结设计待流量**。A/B 前置为 **5 绿 3 半**——分流哈希、Flutter 曝光封装、feature flag **三者代码零实现**(本角色逐项 grep 取证),必须作为前置工单而非既有资产,见 §6。
**事件增量**:新增 **8** 条(全部客户端侧,`creation` 域),既有事件补属性 **2** 处,废弃 **0** 条;字典 **41 → 49**
---
## 0. 北极星 09-21 时限:风险评估与行动建议
> 本节按任务要求置于全文最前(惯例的 §0「基线现状」顺延为 §1)。
### 0.1 北极星是否已定型:**是,无待办**
定型载体 **ADR-012**`architecture/decisions.md:128`,原文):
> 北极星 = **7 日回访记录率**(分母:当 ISO 周产生生命周期首条 `health_record_create_succeeded` 的去重用户;分子:其中在首记日之后第 1–7 个 UTC 自然日内再次创建成功者;不含首记当日;首记日 +8 天出数)。
ADR-020`decisions.md:172`)确认 M3 保持不变,复评点 = M3 收官 + H7 读数。完整定义式、七项口径与出数 SQL 在 `iteration-2/06` §2.1。**指标定义侧零缺口**
- 唯一数据源 `health_record_create_succeeded` 在白名单内(`EventDictionary.java:67`),且客户端**确实在发**`lib/features/pets/health_record_analytics.dart` 挂接,本角色 grep 取证于 §1.2 的 33 条实发清单)。
- 口径刻意不依赖 `sessionId`(只用 `userId` + `server_ts`),故队列硬化与会话缺陷都不污染读数。
- 统计纪律已定:Wilson 95% CI 发布、<50 人周合并或 4 周滚动、未成熟队列不发布。
**但出数缺一个可执行载体**`iteration-2/06` §2.1 的 SQL 只存在于报告正文,仓库内无脚本、无视图、无定时任务,没有任何一处「跑一下就出数」的入口。这与 PM 报告的判断一致,列为 §8 拍板项与 §附 工单。
### 0.2 09-21 为何不可行:窗口已关闭(算术判定,非进度判断)
09-21 这个日期的来源是 `iteration-3/06:312`(原文):
> | 北极星首个成熟周队列 | W37 队列(09-07~09-13 首记)+8 天成熟 | **2026-09-21(周一)首次出数**,此后每周一滚动 | 数据侧 | 本角色发布(Wilson 95% CI<50 人周合并) |
对齐 ISO 周实测(`date -d`):09-07 = 周一 / ISO 周 37 第 1 天,09-13 = 周日 / W37 第 7 天,09-14 = 周一 / **W38 第 1 天**。因此:
| 事实 | 结论 |
| --- | --- |
| W37 的**入队窗口** = 首记日落在 09-07~09-13 | 该窗口已于 **2026-09-13(昨天)结束** |
| 分母 = 首记落在 W37 的去重 userId | 今日(09-14)起产生的首记一律归入 **W38**,永远进不了 W37 |
| 09-21 = W37 最后一名成员(09-13 首记)的 +8 天成熟日 | 09-21 只能出 W37 这一个队列的读数 |
**判定:不可行,且无任何补救能改变。** 这不是「一周内做不完」的问题——即使今天就把 10 项真机验证全部做完、立刻拉来一批真实用户,他们全部落在 W38,W37 的分母不会增加一个人。
**W37 分母的现实取值:极可能为 0**,证据链:
1. `device-verification.md` 三处执行记录(L106 / L168 / L221**全部空白**——10 项真机验证一项未执行。
2. 无 android/ios 包分发:`releases.md` v0.4.009-14 发布)只登记三仓 tag 与 CI 门禁,无商店/内测分发;`server-exposure.md` 对公网只开 22/80/443Gitea + 文档站),**无 API 端点对外**,因此不存在真实用户可达的后端。
3. 桌面端 100% 丢弃(§3 取证):唯一实际跑过的客户端形态(Linux 桌面)产生的事件全被整批 400 后永久丢弃。
4. 库中可能存在的 `product_events` 行只有 curl 人工注入的合成 payload`iteration-3/25` §5c、`iteration-3/26` §5c,均以 `platform=android` 伪值造),且位于可被 `docker compose down -v` 清掉的本地卷 `patbond_pgdata`
未取证项:本角色**未查询数据库**(compose 未运行,且 8 个 agent 并发期间不宜起容器占端口)。W37 分母的实测值需执行:
```bash
cd <你的工作区>/patbond-api && docker compose up -d postgres
docker exec patbond-postgres-1 psql -U patbond -d patbond -c \
"SELECT platform, count(*) FILTER (WHERE event_name='health_record_create_succeeded') AS first_rec_src,
count(*) AS all_events, min(server_ts), max(server_ts)
FROM platform.product_events GROUP BY platform;"
```
### 0.3 一周内真正可行的事(decisive finding
**「待设备到位」这个阻塞理由已经不成立。** 本角色实测本机环境:
| 取证 | 结果 |
| --- | --- |
| `ls ~/Android/Sdk/system-images/*/*/*` | `android-36/google_apis_ps16k/x86_64``android-36/google_apis/x86_64` |
| `ls ~/.android/avd/` | `Pixel_7.avd``Pixel_7.ini` |
| `flutter emulators` | `1 available emulator: Pixel_7 • Pixel 7 • Google • android` |
| `ls ~/Android/Sdk/platform-tools/adb` | 存在 |
| `flutter devices` | 当前仅 linux + chrome(模拟器未启动,**不是不存在**) |
`device-verification.md:9` 明文把模拟器列为一等路径:
> **设备**Android 真机(推荐)或 Android 模拟器。桌面/Web 不可用——没有真实的移动端后台生命周期(`paused` 不触发),且 platform 值不在契约枚举内会被服务端整批拒绝。
M2 两项的步骤完整、通过标准明确、合计约 **55 分钟**(验证一 ~10 分钟:注册→切 Tab/建宠物/记体重→退后台 5 秒→psql 查 `platform='android'`;验证二 ~45 分钟:同一次登录不杀进程,5 分钟不换会话 / 35 分钟换会话,期望恰好 2 个 `session_id`)。唯一需要补的是 `ANDROID_HOME`/PATH 与 `flutter emulators --launch Pixel_7`,以及模拟器场景下宿主机地址用 `10.0.2.2`(文档 L20-31 已给命令)。
**这把最早可得的北极星读数从「无限期」拉到 2026-09-28**W38 = 09-14~09-20,最晚首记 09-20 → +8 天 = **09-28(周一)**。前提是本周内(09-14~09-20)确实产生首记,且**同一 userId 在首记之后的第 1~7 个 UTC 自然日中的另一天**再创建一条成功记录(分子不含首记当日,故单次模拟器会话无法产生分子,需跨日两次操作)。
诚实标注:这样得到的 W38 读数**是测试账号自播种的,不是产品读数**。它的价值是**管道自证**(证明 SQL、口径、落库、去重全链路可跑通并能出一个非零数),必须与真实产品读数分账登记,绝不可用于任何产品结论或 A/B 基线。
### 0.4 降级方案(三档,推荐 A+B)
| 档 | 方案 | 代价 | 本角色意见 |
| --- | --- | --- | --- |
| **A** | **把首次出数从「日期承诺」改为「事件驱动触发条件」**:定义 T0 = 首个满足「≥1 名真实移动端用户产生生命周期首条 `health_record_create_succeeded`」的 ISO 周;首次出数 = T0 + 8 天后的首个周一,此后每周一滚动。同时把 `device-verification.md:40``iteration-3/29:52` 的「09-21 时限提醒」改写为「已失效 + 指向本节」 | 常设文档两处改写 | **推荐**。真机与真实流量都不在项目控制范围内,继续挂日期只会持续生产假红线——本次就是第一例 |
| **B** | **09-21 仍按期出一次「基建就绪读数」**:不报北极星数值,只报「北极星 SQL 已可执行 + W37 分母 = N(实测)+ 链路证据」,作为管道验收而非指标读数 | 一条 SQL + 一段登记 | **推荐并行**。保住「每周一滚动」的节奏纪律不断线,且顺带把 §0.1 的「无 SQL 载体」缺口一并补上 |
| **C** | 用桌面 override(§3)或 curl 造数凑出「首批读数」 | —— | **明确否决**。数据造假,且会污染 W37 之后所有队列基线与 A/B 的历史对照 |
### 0.5 本周行动建议(按优先级,含责任侧)
| # | 行动 | 责任侧 | 工时 | 产出 |
| --- | --- | --- | --- | --- |
| 1 | 启动 `Pixel_7` 模拟器,执行 M2 两项真机验证,填 `device-verification.md` L106 执行记录 | 真机执行人(PM 已排为第一波首日工单) | 1 小时 | `platform='android'` 事件实证;A/B 前置 #1 的拦路项解除 |
| 2 | 把 `iteration-2/06` §2.1 的北极星 SQL 落成仓库内可执行载体(脚本或数据库视图),跑出 W37 实测分母 | 数据侧 | 0.5 天 | §0.4-B 的基建就绪读数;消除「无 SQL 载体」缺口 |
| 3 | 拍板 §0.4 的 A + B,改写两处常设文档的时限表述 | 用户 / PM | 0.5 小时 | 假红线清除 |
| 4 | 本周内跨两天用模拟器自播种 W38 队列(首记 + 隔日回访记录各一条),09-28 出管道自证读数 | 真机执行人 | 2 × 15 分钟 | 首个非零读数(标「自播种,非产品数据」) |
| 5 | 按 §3 修客户端桌面短路 + 调试覆盖开关,让桌面 E2E 也能验端上完整链路 | Flutter | 0.5 天 | 「埋点落库」类验证从「只能真机」降级为「桌面可验 + 真机只验移动生命周期」 |
**关于其余 8 项真机挂起**(真机挂起实为 **10 项** = M2 两项 + M3 四项 + M3.5 四项;`feature-checklist.md` 只跟踪了 M3.5 的两项,漏登记 caregiver 改宠物头像与获赞数对账两项):与北极星出数**无关**,不进本周关键路径。其中 **M3.5-3caregiver 改宠物头像)当前不可执行**——`device-verification.md:202` 承诺的造数 SQL「收口时补」至今未补,且关系授予入口未开放,需先补 SQL 才能排期。
## 1. 基线现状取证(含三处文档口径纠正)
本节所有判断均以打开原始源码/配置为准,逐条注明文件与行号。**不采信任何文档转述**(M3.5 教训:一份报告写「无 nickname 字段」实指「契约未暴露」,被误读后规模预估高估一整档)。
### 1.1 纠正一:白名单实际 **41 条**,不是 42
| 项 | 实测 | 证据 |
| --- | --- | --- |
| 白名单定义位置 | **唯一一处** `WHITELIST` map | `patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/analytics/EventDictionary.java:42-104` |
| `Map.entry(` 条数 | **41** | `grep -c 'Map.entry(' EventDictionary.java` → 41;逐条枚举见下 |
| 是否存在第二处白名单 | **否** | grep `experiment_exposed` / `user_unfollowed` 全仓:命中仅该文件、其测试、文档与客户端发送点,无第二份配置 |
| 是否有测试断言条数 | **否** | `grep -n "hasSize\|size()\|42\|41\|40\|count" EventDictionaryTest.java`**零命中** |
分域实测计数:auth **11**`:43-57`+ `page_viewed` **1**`:59`+ pet **3**`:61-64`+ health_record **7**`:66-74`+ post **8**`:76-86`+ feed **2**`:88-91`+ 互动 **8**`:93-101`+ `experiment_exposed` **1**`:103`= **41**
**根因(已定位)**`experiment_exposed` 被重复计数。community 域实为 **18** 条(post 8 + feed 2 + 互动 8),`experiment_exposed` 属 platform 域,v3 新增合计 **19** 条,22 + 19 = **41**。但 `iteration-3/22:13` 写作「新增事件 **20**06 号 §1.5 的 19 个 + experiment_exposed 已含其中,编号 22~40)」——括号内「已含其中」自我否证了「20」这个数,而下游文档取了 20:`iteration-3/29-m3-summary.md:20`「22 事件 → **42 事件**(+19 community 域 + experiment_exposed)」、`feature-checklist.md:218`「EventDictionary 22→**42**」。该类自身 Javadoc`:11-16` 的 8+2+8 + experiment_exposed)反而是对的,等于 41。
**为何漂移四份文档一路不可见**`EventDictionaryTest.java` 逐事件断言 props 集合,但**从不断言字典总条数**——加一条、少一条、数错一条,测试全绿。这正是「无门禁的口径」必然漂移的样本。
**M4 前置项(本报告列为硬要求)**:在 `EventDictionaryTest` 增一个条数断言 + 一份全事件名快照断言,把「字典条数」变成受测契约。见 §7、§附 工单 1。
### 1.2 字典 41 条 vs 客户端实发 33 条:**8 条字典空转**
客户端实发事件名(`grep -rhoP "(trackEvent|_track)\(\s*'\K[a-z_]+" patbond-flutter/lib/ | sort -u`**33** 条)与字典逐条比对,以下 **8** 条在 `patbond-flutter/lib/` **零引用**(逐条 raw grep 复核,非按差集推断):
| # | 事件名 | 字典位置 | 空转原因(取证) | M4 处置建议 |
| --- | --- | --- | --- | --- |
| 1 | `auth_register_started` | `:43` | 未挂接。注意字典**也没有** `auth_login_started` | **建议 M4 顺带补挂**:注册漏斗起点缺失 ⇒ 注册转化率无分母 |
| 2 | `auth_token_refresh_succeeded` | `:50` | 未挂接 | 保留,M6 可观测性迭代补 |
| 3 | `auth_token_refresh_failed` | `:51` | 未挂接 | 同上 |
| 4 | `auth_session_restore_started` | `:54` | 未挂接 | 同上 |
| 5 | `auth_session_restore_succeeded` | `:55` | 未挂接 | 同上 |
| 6 | `auth_session_restore_failed` | `:56` | 未挂接 | 同上 |
| 7 | `health_record_deleted` | `:74` | `health_record_analytics.dart:8` 注释:「因 M2 契约无删除端点暂无挂接点,留待删除交互落地」 | 保留(等删除 UI) |
| 8 | `experiment_exposed` | `:103` | **`lib/` 零引用**——A/B 前置 #5 只交付了字典半边,Flutter 强类型封装未落地 | **M4 必做**,见 §6.2 |
**废弃建议:0 条。** 上述 8 条均有明确的将来挂接点,按 ADR-013 的判例(「废弃是零成本的」以客户端零引用为前提)反向适用:这些是「字典先行」而非「设计废弃」,删掉只会在挂接时再加回来。改为**登记为「字典空转清单」纳入离线巡检**,防止其无声长期存在。
### 1.3 纠正二:真机验证挂起实为 **10 项**,不是 8 项
`device-verification.md` 全文实登记 **10** 项 = M2 **2** + M3 **4** + M3.5 **4**。汇总文档口径分叉:`feature-checklist.md:221``releases.md:120``iteration-3/29:52` 只统计「M2 两项 + M3 四项」= 6;`feature-checklist.md:244` 只登记 M3.5 的两项(头像上传弱网、头像缓存),**漏登记 M3.5-3(caregiver 改宠物头像)与 M3.5-4(获赞数与帖子点赞数对账)**。
与埋点相关的只有两项:**M2-验证一**(Android 事件真实落库)与 **M3-4**(社区事件落库)。三处执行记录(L106/L168/L221)全空。
### 1.4 纠正三:A/B 前置为 **5 绿 3 半**,不是「6 绿 1 半」
`iteration-3/06:351` 原文结论:「**结论:M3 末 6 项全绿 + #7 部分绿,M4 初补齐监控即可启动首实验。**」本角色对代码侧逐项 grep 取证,与 PM 报告结论一致:**分流哈希、Flutter 曝光封装、feature flag 三者代码零实现**。
| # | 前置条件 | M3 宣称 | 代码/文档取证 | 对齐后 |
| --- | --- | --- | --- | --- |
| 1 | 数据质量验收 | 绿(注「拦路项是真机」) | 真机 0/10 执行;巡检无数据可跑 | **绿(条件性)**——§0.5 行动 1 执行后成立 |
| 2 | 指标基线 | 绿(「09-21 起自然达成」) | 前瞻式绿;无数据,且 §0 已判定 09-21 不成立 | **绿(条件性)**——依赖 §0.4 重定 T0 |
| 3 | 样本量规则成文 | 绿(M3 中交付) | `docs/` 无独立文档;方法学示例存在于 `iteration-2/06:317`(基线率/MDE/α/功效/每组样本量/8 周止损线) | **半**:方法已示范,规则未成文、未按实测基线重算(无基线可代入) |
| 4 | 稳定分流组件 | 绿 | `anonymousId` 持久化**已落**`analytics_service.dart:177-190`key `pb.analytics.anonymousId`);`hash(userId, experimentSalt) % buckets` **零实现**——grep `experimentSalt\|assignVariant\|分流` 全 api/flutter 主源码,`bucket` 命中全是 MinIO 对象存储桶 | **半** |
| 5 | 曝光事件 | 绿(「Flutter 强类型封装同批出」) | 字典半边已落(`EventDictionary.java:103`);Flutter 封装**零实现**(§1.2 第 8 条) | **半** |
| 6 | 实验设计模板与评审流程 | 绿(与 #3 同文档) | 同 #3,无独立文档 | 并入 #3 计 |
| 7 | 护栏监控与回滚 | **部分绿**(「回滚随社区发布开关顺带落地」) | feature flag **零实现**grep `featureflag/toggle/ConditionalOnProperty` 于 api 主源码与 `application*.yml``docker-compose*.yml``.env*`、flutter `lib/` **全部零命中**。即「回滚绿」这半也不成立 | **半(偏红)** |
| 8 | 隐私合规复核 | 常态 | 每实验一次 | **常态**M4 首实验时执行 |
**结论:三项零代码前置(#4 分流哈希、#5 Flutter 曝光封装、#7 feature flag)必须作为 M4 前置工单排期,不得假定已就绪。** 这直接决定 §6 的实验形态选择。
### 1.5 接收链路现状(M4 设计的硬约束)
| 项 | 现状 | 证据 |
| --- | --- | --- |
| 端点 | `POST /api/v1/events`,匿名可报(唯一允许匿名的写端点),单批 1–50 | `AnalyticsController.java:31-37`;契约 `openapi-v1.4.0.yaml:440-495` |
| 逐条拒绝原因 | `unknown_event_name` / `identity_mismatch` / `forbidden_field` / `schema_invalid` | `AnalyticsService.java:46-88` |
| 整批 400 的触发面 | JSON 不可解析、条数越界、**任一条 DTO 字段校验失败** | 契约 `:481``TrackEventsRequest``@NotNull`/`@Pattern`/`@Size` 全是请求级校验 |
| props 处置 | 白名单外的键**剥离后入库**(事件保留,计 warning) | `AnalyticsService.java:90-108` |
| 幂等 | `event_id` PRIMARY KEY + `ON CONFLICT DO NOTHING` | `AnalyticsRepository.java:44``V2:9` |
| 统计权威时间 | `server_ts timestamptz NOT NULL DEFAULT now()`,客户端不发 | `V2:16` |
| 事实表 | `platform.product_events`props `jsonb``user_id` **刻意不设外键** | `V2:8-31` |
| 高频事件索引 | `(event_name, server_ts)``(user_id, server_ts) WHERE NOT NULL``(anonymous_id, server_ts)` | `V2:37-45` |
| 未实现项 | 64KB 请求体上限、429 限流 | 契约据实未写(`iteration-3/06` §0 |
**对 M4 最关键的三条硬约束**(决定 §4.2 的裁定):
1. `anonymous_id uuid NOT NULL``session_id uuid NOT NULL``V2:12,14`)——服务端 Worker 二者皆无。
2. `CONSTRAINT ck_product_events_platform CHECK (platform IN ('android','ios'))``V2:23`)——**平台枚举在 DB 层也锁死**,加值需 Flyway 迁移。
3. `CONSTRAINT ck_product_events_client_ts CHECK (client_ts >= server_ts - interval '30 days' AND client_ts <= server_ts + interval '1 day')``V2:27-30`)——离线积压超 30 天的事件**落库即失败**,被吞成 `schema_invalid`
### 1.6 命名与公共属性口径(M4 沿用,不改)
- 事件名:`<域>_<动作>_<结果>` snake_case,结果后缀仅 `_succeeded`/`_failed`**单义事件不设结果后缀**`health_record_viewed`/`post_deleted` 先例)。DTO 正则 `^[a-z][a-z0-9_]{1,63}$``TrackEventsRequest.java:35`),DB CHECK 同式(`V2:21`)。
- 属性名 camelCase;公共属性十项全带(`iteration-1/13` §4.0),`userId` 是唯一可空项,`serverTs` 服务端补写、客户端不发。
- **禁止多路复用事件**ADR-013 判例):不设 `creation_action(actionType)` 式事件,任何一个动作的枚举扩充不得污染其他指标口径的分母。
- 去重:dedupe key = `eventId`(客户端 UUIDv7,实测 `analytics_service.dart:137` 已为 `Uuid().v7()`,v2 登记的 v4 偏差已修);指标层主体去重一律 `userId`;漏斗归因窗 24 小时。
- 质量阈值:丢失率 <5%、对账偏差 <5%、去重命中 <10%、`serverTs` 覆盖 100%、无红线泄漏。
## 2. 历史包袱一:eventVersion 口径定型
### 2.1 歧义取证(三层各说各话)
登记于 `iteration-3/28` §7 观察项 2,本角色逐层复核确认:
| 层 | 现状 | 证据 |
| --- | --- | --- |
| 契约 | 描述「事件 schema 版本(**字典 v1 全部为 1**)」——可读作「每个事件自身的 schema 版本」,也可读作「事件字典版本」;`type: integer`**无 `minimum`** | `openapi-v1.4.0.yaml:2341-2344` |
| 服务端 DTO | `@NotNull Integer eventVersion`**无范围/枚举校验** | `TrackEventsRequest.java:38-39` |
| 服务端逻辑 | 原样传给落库,**全程不校验、不参与任何判断** | `AnalyticsService.java:77``AnalyticsRepository.java:26,48` |
| 数据库 | `event_version smallint NOT NULL DEFAULT 1` + `CHECK (event_version > 0)` | `V2:11,22` |
| 客户端 | 对**所有**事件硬编码 `'eventVersion': 1` | `analytics_service.dart:139` |
| E2E 脚本 | M2 版发 **2**、M3 版发 **3**(按「字典世代」读法) | `iteration-3/28` §7 |
**两处附带缺陷(本报告新增取证)**:
1. **三层校验不一致导致错误被降级**`eventVersion=0` 或负数可通过 DTO 校验(只有 `@NotNull`),到落库时被 DB CHECK 拒绝抛异常,在 `AnalyticsService.java:81-84` 被 catch 后返回 `schema_invalid`。即客户端的版本号 bug 不会得到清晰的 400,而是伪装成「落库失败」逐条静默拒绝。契约「据实不写 minimum」(`iteration-2/09` §出入 4)的选择反过来固化了这个不一致。
2. **契约描述本身已过期**:「字典 v1 全部为 1」写于字典 v1 时期,字典已走到 v3 仍未更新——这句话的存在本身就是该字段长期无人管理的证据。
### 2.2 定型裁定:**每个事件自身的 props schema 版本**(否决「字典世代」读法)
四条理由,按决定性排序:
1. **历史数据的可修复性是不对称的**。按「每事件 schema 版本」读,客户端现行硬编码 1 **本就是正确的**——41 条事件中无一条曾变更过 props 语义,全部理应为 1。零客户端改动、零回填、零历史重解释。按「字典世代」读,则**所有历史行全错**:既有行该是 1/2/3 三种值却全是 1,而 `server_ts` 无法反推事件当初属于哪一代字典(同一事件名跨代持续上报),**不可修复**。
2. **「字典世代」读法承载零信息量**。字典世代可由 `event_name` 唯一确定(每个事件名只在一代中入册,`iteration-3/22` §1 的编号表即映射)。一个可被现有列完全推导的版本列是死重量,还会诱导下游写出 `WHERE event_version = 3` 这种在客户端口径下永远为空的查询——`iteration-3/28` §7 记录的「后果」正是此。
3. **「每事件 schema 版本」才是该字段的设计意图**。`iteration-1/13` §4 原文「schema 变更须递增事件版本并在本字典追加条目,禁止原地改语义」——「**追加条目**」只有在「同名事件的不同版本并列为两条字典条目」时才讲得通,这是每事件版本的语义。规划文档的历次用法也一律如此:`iteration-2/06` §1.4「若编辑放弃率成为问题再以 eventVersion=2 增补」、`iteration-3/06` §1.4「届时以 eventVersion=2 增补 `_failed`」。
4. **错的是脚本,不是客户端**。两版 E2E 脚本发 2/3 是沿 M2 先例的误读,`iteration-3/28` §3.13 落库的 `event_version=3` 已明确「**不是客户端真实取值**」。修脚本是两行改动。
### 2.3 bump 规则(定型的核心,此前完全缺失)
歧义的真正代价不是取值错,而是**没人知道什么时候该 +1**。定型如下:
| 变更类型 | 是否 bump | 理由 |
| --- | --- | --- |
| 新增**可选**属性,且不改变既有属性语义 | **不 bump** | 下游按键取值,多一个键不破坏任何既有解析;这是最高频的变更,若 bump 则版本号会因无害增补而通胀 |
| 删除属性 | **必须 bump** | 下游解析会 KeyError / 静默变 NULL |
| 改属性语义、单位或口径(如 `durationMs` 的起点变了) | **必须 bump** | 最危险的一类:不 bump 则新旧数据混在同一列,指标断层不可见 |
| 改既有枚举值的含义(新增枚举值**不**算) | **必须 bump** | 同上 |
| 改触发时机(如 `_succeeded` 从「收到响应」改为「UI 渲染完成」) | **必须 bump** | 时间序列会出现无解释的漂移 |
| 改事件名 | 不 bump,**按新事件处理** | 新名即新条目,旧名进「故意不进字典」锁死清单 |
配套纪律:
- bump 后**新旧版本在字典中并列为两条条目**(同名、不同 version、各自的 props 白名单),旧版本标注冻结日期;下游一律按 `(event_name, event_version)` 二元组取口径,**禁止只按 event_name 聚合跨版本数据**。
- 字典条目一旦发布,其 `(name, version)` 的 props 白名单**只可增不可减**(减即 bump)。
- 客户端不得硬编码字面量 `1`:改为按事件查表取版本(见 §2.4)。
### 2.4 落地方案(M4 内完成,两档)
**推荐档(严校验,把口径变成受测契约)**:
1. `EventDictionary` 的 WHITELIST key 从 `String eventName` 改为 `(eventName, version)` 二元组(Java 可用 `record EventKey(String name, int version)`),`isKnownEvent`/`allowedProps` 同步改签名;41 条现有条目全部登记为 version **1**
2. `AnalyticsService.processEvent` 在「未知事件名」之后增一道校验:`(name, version)` 不在字典 ⇒ 逐条 `rejected`,新增 reason **`unknown_event_version`**。
- 关键:**放在逐条拒绝路径,不放 DTO 校验**。若做成 DTO 的 `@Min/@Max`,一条脏事件会整批 400 连坐——这正是 §3 里 `platform` 犯过的错,不重犯。
3. 契约同步:`eventVersion` 描述改为无歧义表述 + 补 `minimum: 1`(与 DB CHECK 对齐,消除 §2.1 缺陷 1);`EventResult.reason` 枚举追加 `unknown_event_version`。**属纯增量**(新增枚举值 + 新增校验说明),v1.4.0 客户端无需改动。
4. 客户端 `analytics_service.dart:139` 的字面量 `1` 改为从事件定义查表取值(与强类型封装同层,编译期锁死)。
5. 两份 E2E 脚本的 `eventVersion` 由 2/3 改 **1**
契约描述建议措辞(可直接抄):
> `eventVersion`**该事件自身 props schema 的版本**,起始 1。新增可选属性不递增;删除属性、改属性语义/单位、改既有枚举值含义、改触发时机必须递增,并在事件字典中与旧版本并列成两条条目。**不是**事件字典的世代号——字典世代由 `eventName` 唯一确定。当前全部 41 条事件均为版本 1。
**最小档(若不愿动校验逻辑)**:只做上述第 3 步的 `minimum: 1` + DTO `@Min(1)` + 第 4、5 步。代价:口径靠文档纪律维持,无门禁——鉴于 §1.1 刚证明「无门禁的口径必然漂移」,本角色**不推荐**最小档。
**为何必须在 M4 新增事件之前定型**:M4 一次进 8 条新事件,若口径未定,这 8 条会各自带上一个含义不明的版本号,债务从 41 条规模翻到 49 条规模,且新条目还会成为「按字典世代读」的新证据(有人会想给它们标 4)。定型的边际成本此刻最低。
## 3. 历史包袱二:platform=linux 桌面端埋点整批 400 的处置
### 3.1 是设计意图还是缺陷:**两者都有,但缺陷不在服务端**
**枚举排除 linux = 设计意图,且是三层锁死的**(改动成本远高于文档暗示):
| 层 | 约束 | 位置 |
| --- | --- | --- |
| 契约 | `platform: {type: string, enum: [android, ios]}` | `openapi-v1.4.0.yaml:2373-2376`(× 5 份副本) |
| 服务端 DTO | `@Pattern(regexp = "^(android|ios)$", message = "platform 必须为 android 或 ios")` | `TrackEventsRequest.java:56-58` |
| 数据库 | `CONSTRAINT ck_product_events_platform CHECK (platform IN ('android','ios'))` | `V2:23` |
**整批 400 = 契约明文行为,不是缺陷**。契约 `:481` 原文:「**整批拒绝**——JSON 不可解析、events 为空或超过 50 条、**单条事件字段校验失败**(code 40000)」。`platform` 是 DTO 字段级 `@Pattern`,属请求级校验,故一条 linux 事件否掉整批,与设计一致。
**缺陷在客户端**,两处:
1. **注释与实现相反**`analytics_service.dart:99-100` 原文:
> `/// 平台标识。契约枚举为 android/ios;Web/桌面为开发调试形态,`
> `/// 上报值不在枚举内会被服务端逐条 rejected(不影响客户端),属预期。`
实际不是「逐条 rejected」而是**整批 400**。逐条 rejected 只发生在 `AnalyticsService.processEvent` 的四种原因里(`unknown_event_name`/`identity_mismatch`/`forbidden_field`/`schema_invalid`),`platform` 根本到不了那一层。
2. **「不影响客户端」也是错的——影响是 100% 静默丢弃**。`_platformName()``:101-112`)在桌面走 `return Platform.operatingSystem``"linux"`;随后 `uploadBatch``:273-281`)把 4xx 判为**永久拒绝**
> `// 4xx 为永久性拒绝(校验失败/批量超限等),重试不可能成功;`
> `// 丢弃并打日志,避免毒丸批次无限重回队列阻塞后续事件。`
`_flush()``:229`)据此 `removeSegments(..., countAsDropped: true)`。**结果:桌面端每一批事件都被丢弃,端上采集/队列/冲刷全部正常运转但落库恒为零**,只在 debug 日志留一行 `Analytics batch permanently rejected`
这个错误注释已被转述进至少 5 处文档(`releases.md:120``device-verification.md:144``feature-checklist.md:221``iteration-3/27:34``iteration-3/28:56`),措辞多为「属契约内行为」「既有预期」——**「契约内」是对的,「预期」掩盖了「桌面端埋点能力为零」这个事实**。这与 §1.1 的 41/42 是同一类问题:一句不准确的表述被当作结论反复引用。
### 3.2 处置建议:客户端本地短路 + 显式调试覆盖开关(不动枚举)
**否决「把 linux/server 加进枚举」**:需 Flyway V6 改 CHECKM4 本就要建 `creation` schema,但混进埋点平台枚举会让这次迁移横跨两个无关关注点)+ 5 份契约副本 + DTO 正则 + 4 个模块的契约一致性矩阵重跑;且会让 `platform` 维度混入非产品流量,污染所有按平台切分的指标。收益仅为「桌面调试方便」,不成比例。
**建议做三件事(均在 Flutter 侧,零契约、零迁移)**:
1. **本地短路**`_platformName()` 返回值不在 `{android, ios}` 内时,`trackEvent` **直接不入队**并 `debugPrint` 一条明确的「桌面端埋点已本地禁用(platform=$p 不在契约枚举内)」。
- 收益不只是省流量:当前桌面上**所有**事件被同一个 400 连坐丢弃,若将来同一队列里混入合法事件(例如覆盖开关只对部分事件生效),毒丸批次会把它们一起带走。短路把这个风险从「隐性」变为「不存在」。
2. **显式调试覆盖开关**:新增 `--dart-define=PATBOND_ANALYTICS_PLATFORM_OVERRIDE=android`**默认关**。开启后桌面上报 `platform=android`,使桌面 E2E 能真验端上完整链路(采集 → 分段持久化队列 → 冲刷 → 202 → 逐条 `accepted``rejected=0`)。
- **这是解 §0 困局的技术杠杆**:可把「埋点落库」这一类验证从「只能真机」降级为「桌面可验链路 + 真机只验移动生命周期」。真机仍不可替代的部分是 `paused` 生命周期与 30 分钟后台换会话(`device-verification.md:9` 明确「桌面/Web 不可用——没有真实的移动端后台生命周期(`paused` 不触发)」),即 M2-验证二的核心。
- **数据完整性护栏(必须一并写入纪律)**:覆盖开关只允许对**本地 compose 后端**使用,产生的行 `platform` 维度是伪值,禁止用于任何产品读数、北极星队列或 A/B 基线;建议同时把 `appVersion` 打上可识别后缀(如 `0.4.0+desktop-e2e`)使这类行在 SQL 层可被一条 `WHERE app_version NOT LIKE '%desktop-e2e%'` 干净剔除。**这是本建议能否被采纳的前提条件**——没有这条护栏,覆盖开关就是 §0.4 档 C(造数据)的后门。
3. **修注释与文档**:把 `analytics_service.dart:99-100` 改为陈述实际行为(整批 400 + 永久丢弃 + 桌面采集能力为零),并在 §7 列出的 5 处文档转述点同步纠正。
### 3.3 顺带发现(不属本节范围,登记备查)
`ck_product_events_client_ts``V2:27-30`)要求 `client_ts >= server_ts - interval '30 days'`。客户端持久化队列容量 500 条、宣称「离线积压约两周容量」(`analytics_service.dart:159-161`),但若设备离线超 30 天后回连,积压事件**落库即触发 CHECK 失败**,被 `AnalyticsService.java:81-84` 吞成 `schema_invalid` 逐条拒绝,客户端收 202 后删段——数据静默丢失且原因不可辨(与真正的 schema 问题同一个 reason)。建议 M4 顺手给这类拒绝一个独立 reason(如 `client_ts_out_of_range`),或在客户端冲刷前按 30 天窗口本地淘汰。**未取证**:无线上数据可证明该路径是否真实发生过。
## 4. 事件字典 v4 增量(AI 创作)
### 4.1 沿用原则与域划分
命名 `<域>_<动作>_<结果>` snake_case、结果编码进事件名(`_succeeded`/`_failed`)、单义事件不设结果后缀、`eventVersion` 起始 1(口径按 §2 定型)、公共属性十项全带、属性 camelCase。
**新增域前缀:`creation`**(与 `development-plan.md` §3 的 schema 名 `creation` 对齐,域按实体归属划——实体是「生成任务」)。服务端 Worker 侧的执行事实**不进事件字典**,理由见 §4.2。
**设计纪律(沿 ADR-013 判例)**:不设 `creation_action(actionType)` 式多路复用事件。提交/结果/失败/取消/建草稿是五个语义独立的动作,各自独立成名;任何一个的枚举扩充不污染其他指标口径的分母。
### 4.2 核心裁定:服务端生成侧**不建第二条埋点管道**
M4 的生成执行发生在 Worker(队列消费、租约、重试、超时),这些事实客户端观测不到。三个候选方案:
| 方案 | 做法 | 判定 |
| --- | --- | --- |
| A | Worker 走 `POST /api/v1/events``platform`/`anonymousId`/`sessionId` 填伪值 | **否决**`platform` 只能填 android/ios`V2:23`),会把服务端流量混进平台维度,**污染所有既有按平台切分的指标**;`session_id NOT NULL` 也只能造假 |
| B | 扩 `product_events`platform 加 `server` 值、`anonymous_id`/`session_id` 改可空 | **否决**。需 Flyway 迁移改 CHECK + 放宽两个 NOT NULL + 5 份契约;放宽 NOT NULL 是**收紧不可逆**的反向操作,且此后每个下游查询都要处理 NULL 会话 |
| C | 新建 `platform.server_events` 表,Worker 进程内直写(不走 HTTP) | 可行但**多余**,见下 |
| **D** | **以 `creation.generation_tasks` 事实表为服务端指标权威**,经 `creationTaskId` 与客户端事件拼接 | **采纳** |
**采纳 D 的理由**
1. **所需事实 100% 已在任务表里**。M4 的验收标准(`development-plan.md:252-256`)本就要求「任务状态流转合法、Worker 重启不丢任务、相同幂等请求不重复生成、失败原因可追踪」——这逼出的表结构天然包含时间戳、状态、尝试次数、失败原因。§5 的服务端侧指标(成功率、P50/P95 生成耗时、排队时长、重试率、超时率)**全部可由该表直接聚合**,事件层加不了任何信息。
2. **避免双写不一致**。若同一事实既有任务表行又有事件行,两者必然在某些边界(Worker 崩溃、事务回滚)分叉,届时「哪个是真的」无解。任务表是状态机的 system of record,事件只能是它的影子。
3. **零迁移增量**(相对方案 B/C):M4 本就要为 `creation` schema 建 V6 迁移,指标所需列并入即可,不额外建表、不改埋点管道。
4. 客户端事件继续只承载**用户可见行为**(看到了什么、点了什么、等了多久),这是它的比较优势且不可被服务端替代(用户是否真的看到结果,服务端不知道)。
**因此这是对后端工单的硬要求**(本报告的指标口径依赖它,请在契约冻结时一并确认):`creation.generation_tasks` 须持久化以下列,且**终态行保留 ≥90 天不得物理删除**(软终态):
`id`= `creationTaskId`)、`user_id``model_key``style_key``status``created_at``queued_at``first_attempt_at``last_attempt_at``finished_at``attempt_count``failure_reason``input_media_count``output_asset_count``idempotency_key`/`request_hash`
若上述任一列缺失,对应指标不可计算——`first_attempt_at` 缺失 ⇒ 排队时长与纯执行耗时无法分离(只能得到端到端耗时);`attempt_count` 缺失 ⇒ 重试率不可得。
### 4.3 隐私红线增量与一处修订申请
沿用现行红线:props 键命中 `password|token|secret|phone|mobile|email|credential|idfa|gaid` ⇒ 整条 rejected`EventDictionary.java:38-39`;客户端同款本地拦截 `analytics_service.dart:288-295`)。社区红线(`EventDictionary.java:22-25`):无自由文本、无内容或对方主体 ID、无话题名、无文件名/路径/URL,只有行为计数与分桶。
**AI 创作域的红线增量(新增三条)**:
1. **生成 prompt 文本绝不入 props**,任何长度、任何截断、任何哈希都不行——哈希可被字典攻击还原短 prompt。只允许 `promptLengthBucket`(分桶)。
2. **模型/风格标识只许上报目录的稳定 key**`modelKey`/`styleKey`),不上报展示名、不上报版本串、不上报供应商名。展示名可能被产品运营改成含营销文案的自由文本。
3. **产物不入 props**:无 assetId、无 objectKey、无签名 URL、无缩略图,只有 `resultCount`/`outputAssetCount` 计数(沿 M3 媒体域先例)。
**修订申请(§8 拍板项 6)**:现行红线写作「无内容或对方主体 IDpostId/commentId/topicId/target userId)」,字面上会连带禁止 `creationTaskId`。但 §5 的漏斗**必须**有一个客户端与服务端共享的拼接键,否则「提交 → 排队 → 完成 → 建草稿 → 发帖」跨越两个数据源无法连成一条。
建议把红线措辞**收敛为其原本意图**:
> 禁止**他人主体与内容主体** IDpostId / commentId / topicId / 对方 userId);**允许上报调用者自有资源的 ID 作为漏斗拼接键**,当前仅 `creationTaskId` 一例,新增需本报告同级评审。
理由:原红线的隐私意图是「防止从事件流重建「谁对谁的内容做了什么」的社交图谱」(`iteration-3/06` §1.3)。`creationTaskId` 指向的是**上报者自己**的任务,不涉及第二主体;它不泄露 prompt 或产物(那些在 `creation` 表里,与 props 同一数据库同一信任域,不构成跨边界泄漏);且它是 UUIDv7,除时间戳外无内在信息。
**否决的替代方案**:把拼接键做成第 11 个**公共属性**(顶层字段而非 props)。虽然架构上更干净(props 红线完全不动),但要改 5 份契约副本的 `TrackedEvent`、改 DTO、加 DB 列(Flyway 迁移)、且要为一个只有 `creation` 域用得上的字段污染全部 41 条既有事件的公共属性集——**十项公共属性是跨迭代冻结面,为单域需求撬动它不成比例**。走 props 白名单则是零迁移(props 是 jsonb)。
### 4.4 新事件清单(`creation` 域,8 条,全部客户端侧)
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `creation_started` | 进入 AI 创作流程并产生**首次有效交互**(选定模型或风格、或首次输入 prompt),每次进入记一次;仅浏览不交互不记 | `entryPoint``create_tab` / `home` / `pet_detail` / `post_form`,待 UI 定稿收敛) |
| `creation_model_selected` | 模型或风格**选定动作完成**时(每次变更各记一次,不去重——变更次数本身是选择摩擦的度量) | `modelKey``styleKey``selectSeq`(本次创作内第几次选择,从 1 起) |
| `creation_submit_succeeded` | 生成任务创建接口成功响应后(拿到 taskId,**非生成完成**)(**漏斗事件**,M4 全部转化率指标的分母源) | `creationTaskId``durationMs`started→submit)、`modelKey``styleKey``inputMediaCount``promptLengthBucket``regenerateFrom` |
| `creation_submit_failed` | 生成任务创建接口失败(含配额拦截、内容审核前置拒绝) | `modelKey``failureReason``errorCode``httpStatus``attemptSeq` |
| `creation_result_viewed` | 成功生成的结果**首次渲染可见**(不是任务完成,是用户真的看到了) | `creationTaskId``waitedMs`(submit→首次可见,用户感知等待)、`resultCount` |
| `creation_failure_viewed` | 失败态**首次渲染**给用户(与 `creation_submit_failed` 区分:后者是提交就没成,此者是排队/执行后才失败) | `creationTaskId``failureReason``waitedMs` |
| `creation_cancelled` | 用户主动取消且取消接口成功响应后 | `creationTaskId``waitedMs``stage``queued` / `running` |
| `creation_draft_created` | 「一键创建社区草稿」成功响应后 | `creationTaskId``selectedCount`(选入草稿的产物数) |
**枚举值定义**(客户端编译期锁死,离线巡检;ingest 只校验键不校验值——`EventDictionary.java:26-33` 的既有设计):
| 属性 | 枚举 / 口径 |
| --- | --- |
| `regenerateFrom` | `none`(首次生成)/ `failure`(对失败任务重试)/ `dissatisfaction`(对已成功结果不满意再生成)——三值必须分开,否则「重新生成率」会把「系统不可靠」与「产品不满意」两个完全不同的问题混成一个数 |
| `failureReason` | 沿用既有族 + 新增 `quota_exceeded``model_unavailable``content_rejected`(内容安全拒绝)、`generation_timeout``cancelled` |
| `promptLengthBucket` | 沿 `textLengthBucket` 先例分桶(如 `0` / `1-20` / `21-60` / `61-200` / `200+`),**绝不上报原文与长度精确值** |
| `stage` | `queued`(尚未被 Worker 取走)/ `running`(已租约执行中) |
| `modelKey` / `styleKey` | 服务端目录接口的稳定 key,与目录契约同批冻结;**非展示名** |
| `inputMediaCount` / `resultCount` / `selectedCount` | int 计数,无输入为 0 |
**去重口径**
- `creation_started`:按「进入一次创作流程」记一次,同一 `sessionId` 内反复切页不重复记(客户端持有本次流程状态,离开流程即重置)。
- `creation_result_viewed` / `creation_failure_viewed`**每个 `creationTaskId` 至多一条**(「首次可见」语义)。客户端需按 taskId 记忆已上报集合,避免用户来回切页刷出多条——否则「结果触达率」分子会 >1。
- `creation_model_selected`**刻意不去重**(见上表 `selectSeq`)。
- 其余事件:一次成功业务动作一条。
- 服务端幂等仍以 `eventId` 为唯一保证点(客户端 at-least-once 投递)。
### 4.5 v4 增量总览(编号沿跨迭代连续编号)
字典现有 41 条(§1.1 取证),编号沿 `iteration-3/22` 的 22~40 续接。**注意:既有编号最大为 40 而实际条数为 41**(`page_viewed` 在 v2 转正时记作 `—` 未占号),本报告不回改历史编号,新增从 **41** 起编,并在 §7 要求把「编号」与「条数」两个口径在测试里各自锁死。
| # | 事件名 | 版本 | 性质 |
| --- | --- | --- | --- |
| 41 | `creation_started` | 1 | 新增 |
| 42 | `creation_model_selected` | 1 | 新增 |
| 43 | `creation_submit_succeeded` | 1 | 新增(漏斗事件) |
| 44 | `creation_submit_failed` | 1 | 新增 |
| 45 | `creation_result_viewed` | 1 | 新增(漏斗事件,结果触达) |
| 46 | `creation_failure_viewed` | 1 | 新增 |
| 47 | `creation_cancelled` | 1 | 新增 |
| 48 | `creation_draft_created` | 1 | 新增(漏斗事件,转化前置) |
| — | `post_publish_succeeded` | 1 | **属性增量**+`creationTaskId`,不 bump——§2.3 规则:新增可选属性不递增) |
| — | `post_draft_saved` | 1 | **属性增量**+`creationTaskId`,不 bump |
| — | `experiment_exposed` | 1 | **启用**(字典 M3 先行,M4 首次实际上报;无属性变更) |
**字典条数:41 → 49。废弃 0 条。**
**后端 `EventDictionary` 白名单增量(工单可直接抄)**
```java
// v4 增量 creation 域(iteration-4 报告 06 §4.4
Map.entry("creation_started", Set.of("entryPoint")),
Map.entry("creation_model_selected", Set.of("modelKey", "styleKey", "selectSeq")),
Map.entry("creation_submit_succeeded",
Set.of("creationTaskId", "durationMs", "modelKey", "styleKey",
"inputMediaCount", "promptLengthBucket", "regenerateFrom")),
Map.entry("creation_submit_failed",
Set.of("modelKey", "failureReason", "errorCode", "httpStatus", "attemptSeq")),
Map.entry("creation_result_viewed", Set.of("creationTaskId", "waitedMs", "resultCount")),
Map.entry("creation_failure_viewed", Set.of("creationTaskId", "failureReason", "waitedMs")),
Map.entry("creation_cancelled", Set.of("creationTaskId", "waitedMs", "stage")),
Map.entry("creation_draft_created", Set.of("creationTaskId", "selectedCount")),
```
**既有条目改写两处**(在原位加属性,不新增条目):
```java
// 原:Set.of("durationMs", "mediaCount", "topicCount", "textLengthBucket", "fromDraft")
Map.entry("post_publish_succeeded",
Set.of("durationMs", "mediaCount", "topicCount", "textLengthBucket",
"fromDraft", "creationTaskId")),
// 原:Set.of("trigger", "mediaCount")
Map.entry("post_draft_saved", Set.of("trigger", "mediaCount", "creationTaskId")),
```
**类注释红线措辞同步修订**`EventDictionary.java:22-25`):按 §4.3 把「no content or counterpart IDs」改为「no **counterpart-subject or content** IDs; the caller's own `creationTaskId` is permitted as the funnel join key」。
### 4.6 漏斗定义、闭环与缺口复核
**AI 创作全漏斗(六段,跨两个数据源,单键 `creationTaskId` 拼接)**
| 段 | 事件 / 事实 | 数据源 | 主体去重 |
| --- | --- | --- | --- |
| 1 进入创作 | `creation_started` | 客户端 | userId |
| 2 选定模型 | `creation_model_selected`(末次) | 客户端 | userId |
| 3 提交生成 | `creation_submit_succeeded` / `creation_submit_failed` | 客户端 | userId(任务量用 creationTaskId |
| 4 排队与执行 | `generation_tasks``queued_at``first_attempt_at``finished_at``status``attempt_count` | **服务端事实表** | creationTaskId |
| 5 结果触达 | `creation_result_viewed` / `creation_failure_viewed` / `creation_cancelled` | 客户端 | creationTaskId |
| 6 建草稿 → 发帖 | `creation_draft_created``post_publish_succeeded``creationTaskId` 非空) | 客户端 | userId |
**join key 定型**`creationTaskId`(UUIDv7,由服务端在创建任务时生成并在响应中返回,客户端原样上报)。
- 客户端事件侧位置:`props->>'creationTaskId'`
- 服务端侧:`creation.generation_tasks.id`
- 拼接 SQL 惯例(漏斗第 3~5 段):
```sql
SELECT t.id, t.status, t.attempt_count,
t.first_attempt_at - t.queued_at AS queue_wait,
t.finished_at - t.first_attempt_at AS exec_time,
v.props->>'waitedMs' AS perceived_wait_ms
FROM creation.generation_tasks t
LEFT JOIN platform.product_events v
ON v.event_name = 'creation_result_viewed'
AND (v.props->>'creationTaskId')::uuid = t.id
WHERE t.created_at >= :from;
```
- **为何不用 (userId, 时间窗) 拼接**:用户可并发提交多个任务(重试 + 新建),时间窗拼接在并发场景下会把 A 任务的耗时配给 B 任务的结果,且无法察觉——P95 耗时这类尾部指标对错配极其敏感。此外未登录不可创作(生成消耗配额),故 userId 恒非空,但仍不足以消歧。
**闭环成立**:六段无断点,第 4 段的服务端事实与前后客户端事件均可经 `creationTaskId` 连上。
**缺口 1(接受不埋):客户端 `creation_queued`。** 排队进入/结束的时点以服务端为准(`queued_at`/`first_attempt_at`),客户端只能靠轮询观测,粒度取决于轮询周期,会系统性高估排队时长。客户端侧的「等待」由 `waitedMs` 一个用户感知量表达即可,不再补一个精度更差的重复事件。
**缺口 2(接受不埋):`creation_retried` 独立事件。** 重试即一次新提交,已由 `creation_submit_succeeded.regenerateFrom ∈ {failure, dissatisfaction}` 表达。独立事件会与提交事件双计,且让「提交总数」这个分母出现两种口径。
**缺口 3(接受不埋):`creation_quota_blocked` 独立事件。** 已由 `creation_submit_failed(failureReason='quota_exceeded')` 覆盖;独立成名违反「结果编码进事件名」惯例,且分裂提交失败的分母。
**缺口 4(登记待拍板,缺口 4 = §8 拍板 9):结果内单产物的选择/滑动行为**(多产物时用户看了第几张、选了第几张)。首版不埋——UI 未定稿,且 `resultIndex` 类属性容易被误当作「用户偏好某模型输出」的产品结论。若成为问题,届时按 §2.3 以新增可选属性(不 bump)补 `selectedIndexBucket`
**修订 1(实质缺口,本版已修)**:`post_publish_succeeded``post_draft_saved` 的现行白名单**无任何标识帖子来源的属性**(分别为 `durationMs`/`mediaCount`/`topicCount`/`textLengthBucket`/`fromDraft``trigger`/`mediaCount`,见 `EventDictionary.java:78-79,77`),因此「AI 生成 → 发帖」这条 M4 最核心的转化**在现行字典下根本不可计算**。这是本报告发现的最实质字典缺口,靠 §4.5 的两处属性增量修复。
**维度够用性**`modelKey`/`styleKey` 支持按模型切分全部指标;`regenerateFrom` 区分系统不可靠与产品不满意;`promptLengthBucket`/`inputMediaCount` 支持输入复杂度 × 耗时/成功率相关分析;`entryPoint` 支持入口效率对比;平台与版本维度由公共属性提供。**一处不够用但无需加属性**:「是否首次使用 AI 创作」可由用户级 `min(server_ts)` of `creation_submit_succeeded` 推导。
### 4.7 pageName 枚举增量
`page_viewed.pageName` 由客户端编译期枚举锁死,**ingest 只校验键不校验值**(`EventDictionary.java:30-33` 明确「pageName growth needs no code change here」),故后端零改动。
现行客户端枚举实测 **14** 个(`analytics_page_name.dart:9-25`):`login`/`register`/`home`/`profile`/`pet_list`/`pet_detail`/`pet_form`/`record_form`/`record_detail`/`create`/`pet_archive`/`services`/`post_detail`/`post_form`
**M4 增量:新增 1 个** —— `creation_result`(生成结果页)。AI 创作 Tab 复用既有 `create`;模型/风格选择若为同页 sheet 则不单独计页。
**顺带登记的偏差(无功能影响)**`EventDictionary.java:28-31` 的注释声称 v3 pageName 家族含 `topic_list`/`topic_detail`/`user_profile`/`follower_list`/`following_list`/`favorite_list`/`draft_list`,但客户端枚举中**这 7 个都不存在**(对应页面属 ADR-018 剪出或尚未落地)。因 ingest 不校验值,无功能影响;但这是「文档先于实现」的又一例,建议随本次修订据实收敛。
## 5. M4 指标口径定义
统计口径沿用 v1/v2`server_ts` 划 UTC 日界;主体去重一律 `userId`;漏斗归因窗 24 小时。**任务级指标按 `creationTaskId` 去重,用户级指标按 `userId` 去重**——两者不可混用(一个用户可提交 N 个任务,混用会让重度用户支配读数)。
| 指标 | 分子 | 分母 | 数据源 | 口径要点 |
| --- | --- | --- | --- | --- |
| **生成成功率** | `status='succeeded'` 的任务数 | `status IN ('succeeded','failed')` 的任务数 | 服务端事实表 | **排除 `cancelled`**(用户主动取消不是系统失败,计入会让「用户不耐烦」伪装成「系统不可靠」);**排除未终态**(`queued`/`running`);按 task 去重非按 user |
| **P50 / P95 生成耗时** | —— | —— | 服务端事实表 | `finished_at - first_attempt_at`(**纯执行耗时,不含排队**);只取 `status='succeeded'`(失败任务的耗时是超时值,混入会污染尾部);P95 用 `percentile_cont(0.95)`;按模型(`model_key`)分组发布,**不发布跨模型合并的 P95**(不同模型耗时分布差一个量级,合并值无意义) |
| **排队时长** | —— | —— | 服务端事实表 | `first_attempt_at - queued_at`;P50/P95 同上;**重试任务只取首次尝试**,否则重试等待会被计成排队 |
| **端到端感知等待**(辅助) | —— | —— | 客户端 | `creation_result_viewed.waitedMs` 的 P50/P95。与「排队 + 执行」之差即客户端轮询/渲染开销——**这个差值是客户端性能债的直接读数** |
| **结果触达率** | `creation_result_viewed` 的去重 `creationTaskId` | `status='succeeded'` 的任务数 | 双源 | 衡量「生成完了但用户没看到」。**这是 AI 创作特有的浪费指标**:算力已花但价值未交付。触达率低 ⇒ 需要完成通知 |
| **生成 → 发帖转化率**M4 核心) | 24h 内 `post_publish_succeeded``props->>'creationTaskId'` 非空 的去重 `userId` | `creation_result_viewed` 的去重 `userId` | 客户端 | 分母用**结果触达**而非提交或成功——没看到结果的人不可能发帖,用提交做分母会把系统故障算进产品转化的账 |
| **草稿 → 发帖转化率**(辅助) | 同上分子 | `creation_draft_created` 去重 `userId` | 客户端 | 拆解上一指标:区分「不想发」与「建了草稿卡在发布环节」 |
| **重新生成率** | `creation_submit_succeeded``regenerateFrom <> 'none'` | 全部 `creation_submit_succeeded` | 客户端 | **必须按 `regenerateFrom` 值分开发布两个数**`failure` 档升高 = 系统不可靠(应归因到生成成功率);`dissatisfaction` 档升高 = 产品质量问题。合并成一个数会让两种截然不同的病症互相掩盖 |
| **配额触顶率** | `creation_submit_failed``failureReason='quota_exceeded'` 的去重 `userId` | `creation_started` 的去重 `userId` | 客户端 | 分母用「进入创作」而非「提交」——被配额拦住的用户其提交是失败的,用提交做分母会自我循环。**同时须按用户分层看**(触顶用户占比 vs 触顶次数分布),均值会掩盖「少数重度用户天天触顶」 |
| **创作漏斗整体转化** | `creation_submit_succeeded` 去重 `userId` | `creation_started` 去重 `userId` | 客户端 | 提交前流失(选模型放弃、prompt 写不出来) |
**护栏指标(M4 全程周报,不只 A/B 期间)**:
| 护栏 | 口径 | 告警线 |
| --- | --- | --- |
| 生成成功率 | 见上 | 周环比相对下降 >5% ⇒ P1 |
| P95 生成耗时 | 见上,按模型 | 周环比 +50% ⇒ P1 |
| 内容审核拒绝率 | `failureReason='content_rejected'` / 提交总数 | 绝对值 >5% ⇒ 复核审核阈值是否过严 |
| 埋点质量四项 | 丢失率 <5%、对账偏差 <5%、去重命中 <10%、`serverTs` 覆盖 100% | 任一破线 ⇒ 当周读数标「数据未验收」 |
| **北极星不回退** | 7 日回访记录率(ADR-012) | AI 创作是新场景,**必须确认它没有把用户从「记录」这个核心场景吸走**——这是 M4 最重要的护栏 |
| 帖子删除率 | `post_deleted` / `post_publish_succeeded`,按 `creationTaskId` 有无分两群 | AI 帖删除率显著高于自发帖 ⇒ 一键发布在诱导用户发不想发的内容 |
**对账 SQL 增量(入周一巡检,沿 `iteration-2/06` §6 惯例)**
```sql
-- 对账 1:客户端提交事件数 vs 服务端任务行数(期望偏差 <5%)
SELECT
(SELECT count(DISTINCT props->>'creationTaskId') FROM platform.product_events
WHERE event_name = 'creation_submit_succeeded' AND server_ts >= :from) AS client_submits,
(SELECT count(*) FROM creation.generation_tasks WHERE created_at >= :from) AS server_tasks;
-- 对账 2:孤儿 taskId——客户端上报了但服务端无此任务(期望恒为空集,命中即 P1)
SELECT DISTINCT e.props->>'creationTaskId' AS orphan_task
FROM platform.product_events e
LEFT JOIN creation.generation_tasks t ON (e.props->>'creationTaskId')::uuid = t.id
WHERE e.event_name LIKE 'creation_%' AND e.props ? 'creationTaskId' AND t.id IS NULL;
-- 对账 3AI 来源帖的 creationTaskId 必须指向本人任务(期望恒为空集,命中即越权或口径错)
SELECT e.event_id
FROM platform.product_events e
JOIN creation.generation_tasks t ON (e.props->>'creationTaskId')::uuid = t.id
WHERE e.event_name = 'post_publish_succeeded' AND e.user_id IS NOT NULL
AND t.user_id <> e.user_id;
-- 对账 4:红线巡检——creation 域事件不得出现 prompt/URL 形态的值(期望恒为空集)
SELECT event_id, event_name, props
FROM platform.product_events
WHERE event_name LIKE 'creation_%'
AND (props::text ~* '(https?://|/[a-z0-9_-]+\.(jpg|jpeg|png|webp|heic))'
OR length(props::text) > 512); -- 512 字节以上说明混入了自由文本
-- 对账 5:字典空转巡检(§1.2)——列出白名单内但本周零上报的事件
-- (期望只剩已知的 8 条空转项;出现新成员即挂接回归)
SELECT e.name FROM (VALUES ('creation_started'),('creation_submit_succeeded'),
('creation_result_viewed'),('creation_draft_created'),('experiment_exposed')) AS e(name)
WHERE NOT EXISTS (SELECT 1 FROM platform.product_events p
WHERE p.event_name = e.name AND p.server_ts >= :from);
```
**未取证**:以上 SQL 未在真实数据上执行过(无数据,§0.2),且 `creation.generation_tasks` 的列名以 §4.2 的要求为准、尚未由后端契约确认;后端定稿后需回改列名并试跑一次。
## 6. A/B 实验设计
### 6.1 形态裁定:M4 首个「实验」应是 A/A 基建验证,真实 A/B 冻结设计待流量
`iteration-3/29:68` 的路线是「M4 可启动首个实验」。本角色按 §1.4 取证后**修订这个判断**,两条理由:
1. **前置三项代码零实现**(分流哈希、Flutter 曝光封装、feature flag)。这不是「补监控即可」的距离,是从零写三个组件。
2. **样本量不可达**。§6.3 的真实 A/B 需约 **1,560** 名到达生成结果的用户;当前真实用户数为 **0**(§0.2 取证)。按 M2 §4.3 的止损纪律「若按届时 DAU 换算实验需运行超过 8 周,判定该实验不可行」,该实验现在就判不可行。
**因此 M4 的实验交付物拆成两件**:
- **6.2 A/A 基建验证空跑**——M4 内可执行、零产品风险、极小样本即有价值。
- **6.3 首个真实 A/B 设计书**——设计冻结、判定线预登记(防 HARKing),**放行条件挂在流量上**,不挂日期。
### 6.2 A/A 基建验证实验(M4 交付,非产品实验)
**目的**:在没有产品风险的前提下验证实验基建本身。A/A 的全部价值在于「**当两组体验完全相同时,指标差异应当不显著**」——若显著,说明基建有缺陷(分流不均、曝光漏报、指标管道错配),而这类缺陷在真实 A/B 中会被误读成产品结论。
| 项 | 设计 |
| --- | --- |
| 假设 | 无产品假设。原假设即「两组无差异」,期望**不拒绝** |
| 变体 | Control 与 Variant **完全相同的现有体验**(代码路径一致,只走一遍分流与曝光上报) |
| 触点 | AI 创作结果页(`creation_result` 页曝光时上报 `experiment_exposed` |
| 分流单位 | `userId`(AI 创作需登录消耗配额,故无需 anonymousId 分流——**这顺带让前置 #4 的「登录前分流归并规则」不成为本实验的前置**) |
| 分流实现 | `hash(userId + experimentSalt) % 100``experimentKey='creation_result_cta_aa'`50/50 |
| 检查项 | ① **SRM(样本比例失配)**:两组曝光用户数的卡方检验,α=0.001,超出即分流有偏;② 曝光事件与分配的一致性:`experiment_exposed` 去重用户数 ≈ 实际到达结果页用户数(漏报率 <2%);③ 每个 userId 的 `variant` **跨会话、跨设备恒定**(同一 userId 出现两个 variant ⇒ P1);④ §5 全部指标在两组间的差异均不显著(p>0.05);⑤ 停止规则与回滚开关各演练一次 |
| 样本量 | 检测粗差不需要功效计算:**≥200 曝光用户合计**即可暴露 >60/40 的分流失衡与曝光漏报;恒定性检查(③)在几十个用户上就能发现 bug。**测试账号可参与**(无产品结论,不存在污染产品读数的问题,但须与产品读数分账) |
| 运行周期 | ≥3 天(跨一次冷启动与一次 token 刷新,验证 variant 在会话重建后不漂移) |
| 停止规则 | 检查项 ①③ 任一失败 ⇒ 立即停、修基建、重跑;不设「提前成功」——A/A 没有成功可提前 |
| 产品风险 | **零**(两组体验相同) |
**这一步的产出是「基建可信」这个前提本身**。跳过它直接跑真实 A/B,等于把三个从未运行过的新组件与一个产品结论绑在一起,出问题时无法归因。
### 6.3 首个真实 A/B 设计书(设计冻结,放行条件挂流量)
**候选选择说明**`iteration-3/06:351` 的候选池按 H3/H4/H5/H7 判定结果动态排序,但 H1~H8 **全部无读数**(无数据),数据驱动的排序此刻不可用。故按「M4 新建面 + 效应量可期 + 与 M4 核心指标直接挂钩」选定下述实验,并明确它**不占用**候选池中那四个记录域实验的席位。
```markdown
# 实验:生成结果页「发布到社区」引导强度
## 假设
**问题陈述**:AI 生成的产物默认停留在创作页,用户需自行找到入口才能发到社区;
「生成 → 发帖」这条 M4 最核心的价值链路很可能在最后一步大量流失。
**假设**:把生成结果页的「发布到社区」从次级入口提升为主按钮,并预填一段可编辑的
默认文案,将提升「生成 → 发帖转化率」。
**主指标**:生成 → 发帖转化率(§5 定义:24h 内 post_publish_succeeded 且
creationTaskId 非空的去重 userId / creation_result_viewed 去重 userId
**成功阈值**:绝对提升 ≥ +6pp 且 95% CI 下界 > 0
**次要指标**:草稿 → 发帖转化率、创作漏斗整体转化、重新生成率(dissatisfaction 档)
**护栏指标**:生成成功率、P95 生成耗时(按模型)、**帖子删除率(AI 帖 vs 自发帖)**、
次日回访、**北极星 7 日回访记录率不得下降**
## 实验设计
**类型**:双臂 A/Bfeature flag 控制,服务端下发变体)
**总体**:全部到达生成结果页的登录用户;无地域/机型限制;新老用户均入组(并预登记
按「是否首次使用 AI 创作」的分层分析,**该分层是预登记的、非事后挖掘**)
**分流单位**userId`hash(userId + experimentSalt) % 100`),50/50
**样本量**:每组 ~780、合计 ~1,560 名**曝光**(= 到达结果页)用户
推导:p₁=0.20(基线假设,**无实测基线,见下方风险**)、p₂=0.26(MDE +6pp)、
双侧 α=0.05、功效 80%
n = (1.96·√(2·0.23·0.77) + 0.8416·√(0.20·0.80 + 0.26·0.74))² / 0.06² ≈ 772 → 取 780
**最短运行周期**:**≥14 天**且不早于样本量达标。14 天的三个来源:① 覆盖两个完整周内
节律(周末创作行为与工作日不同);② 主指标含 24h 归因窗;③ 北极星护栏需 +8 天成熟期
**变体**
- Control:现状——结果页主操作为「保存」,发布社区为次级入口
- Variant A:结果页主按钮改为「发布到社区」,预填默认文案(可编辑),「保存」降为次级
## 风险评估
**潜在风险**
1. 诱导发布低质内容 → 社区 Feed 质量下降、用户后悔删帖
2. 预填文案若千篇一律 → Feed 同质化
3. 「保存」降级可能损害只想留存产物的用户
**缓解**:帖子删除率为一级护栏(AI 帖 vs 自发帖分群对比);预填文案不得含固定营销语;
「保存」保持一次点击可达;feature flag 支持 5 分钟内全量回滚
**成功/失败判定**
- 主指标达阈值 且 全部护栏未破 → **Go**,全量
- 主指标达阈值 但 帖子删除率显著升高 → **不全量**,改做「发布前预览确认」再测
- 主指标未达阈值(CI 含 0)→ **No-Go**,保持现状,转而排查结果触达率
## 统计纪律(预登记,不得事后调整)
- **固定视界,不许偷看**:样本量达标且满 14 天前不做任何显著性判定。中途看板只
允许查看护栏与数据质量,**不得查看主指标的 p 值**
- **单一主指标**,故无需多重比较校正;次要指标一律标注为探索性,**不可用于翻盘
No-Go 结论**
- 护栏采用单侧监控(α=0.005),其触发只用于**提前停止**,不用于宣告成功
- **提前停止仅在护栏破线时**:生成成功率相对下降 >5%,或 P95 生成耗时 +50%
或帖子删除率相对升高 >30%
- 判定线先于数据存在(防 HARKing);分层分析只有上面预登记的一项
- 隐私合规复核(A/B 前置 #8)在 T0 前执行一次
## 放行条件(全部满足才能启动,**不挂日期**)
1. §6.2 的 A/A 验证通过(含 SRM 与 variant 恒定性)
2. A/B 前置 #4 分流哈希、#5 Flutter 曝光封装、#7 feature flag **代码落地并有测试**
3. #3/#6 样本量规则与实验设计模板成文
4. 埋点质量四项达标,且 §5 对账 1~4 连续两周无破线
5. **实测基线**`creation_result_viewed → 发帖` 转化率连续 ≥2 周稳定产出(当前用的
p₁=0.20 是**假设值**,达标后必须用实测值重算样本量——若实测基线是 5% 而非 20%,
所需样本量会变成数千每组,实验可行性结论随之翻转)
6. 按实测 DAU 换算,预计运行 **≤8 周**(>8 周即判不可行,退回观察性分析,止损线预登记)
```
**当前放行条件状态:0/6 满足。** 条件 5 与 6 依赖真实流量,不在工程可控范围内——这是本设计书**必须挂条件而非挂日期**的原因,也是 §0.4 给北极星提的同一条纪律(用触发条件替代日期承诺)。
### 6.4 `experiment_exposed` 的上报纪律(M4 首次启用)
字典条目 M3 已先行(`EventDictionary.java:103`props = `experimentKey``variant`),M4 首次实际上报。三条纪律:
1. **在用户实际到达实验触点、变体 UI 已渲染时上报**,不在分配时上报。分配时上报会把「被分到但从未看到」的用户算进分母,系统性稀释效应量(`iteration-3/06:145` 已定此口径)。
2. **每个 (userId, experimentKey) 每会话至多一条**,跨会话可重复(用于验证 variant 恒定性)。分析时按 userId 去重取首次曝光。
3. **`experimentKey` 取自实验注册表枚举**,不接受自由字符串;`variant``control` / `variant_a` 等固定枚举。注册表是 §附 工单的交付物之一。
## 7. 白名单变更清单与同步点
### 7.1 变更汇总
| 类别 | 数量 | 明细 |
| --- | --- | --- |
| **新增事件** | **8** | `creation_started``creation_model_selected``creation_submit_succeeded``creation_submit_failed``creation_result_viewed``creation_failure_viewed``creation_cancelled``creation_draft_created` |
| **既有事件加属性** | **2** | `post_publish_succeeded` +`creationTaskId``post_draft_saved` +`creationTaskId` |
| **启用既有事件** | **1** | `experiment_exposed`(字典已在,客户端封装 M4 补) |
| **废弃事件** | **0** | 见 §1.2:8 条空转事件均有将来挂接点,改为纳入巡检而非废弃 |
| **eventVersion bump** | **0** | 新增可选属性按 §2.3 规则不递增 |
| **pageName 新增** | **1** | `creation_result`(后端零改动) |
| **字典条数** | **41 → 49** | 基数 41 已取证(§1.1),非文档记载的 42 |
| **Flyway 迁移** | 埋点侧 **0** | `creation` schema 的 V6 迁移属后端范围;`product_events` 不动(props 是 jsonb,加属性零迁移) |
### 7.2 同步点(契约/配置/测试,逐项可勾)
| # | 同步点 | 文件 | 改动 |
| --- | --- | --- | --- |
| 1 | 后端白名单 | `patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/analytics/EventDictionary.java` | +8 条目(§4.5 代码块可直抄);改写 2 条既有条目;类注释红线措辞按 §4.3 修订;若采纳 §2.4 推荐档则 WHITELIST key 改 `(name, version)` 二元组 |
| 2 | 后端服务 | `.../analytics/AnalyticsService.java` | 仅 §2.4 推荐档需改:增 `unknown_event_version` 逐条拒绝分支(**放逐条路径,不放 DTO**) |
| 3 | 后端 DTO | `.../analytics/TrackEventsRequest.java` | `eventVersion``@Min(1)`(与 DB CHECK 对齐,消除 §2.1 缺陷 1)。**`platform` 正则不动** |
| 4 | **字典条数门禁** | `.../analytics/EventDictionaryTest.java` | **本次必做**:① 断言白名单条数 = 49;② 断言全事件名快照集合;③ 新增 8 条各自的 props 断言;④ 2 条改写事件的 props 断言更新。**理由见 §1.1——没有这个断言,41/42 这类漂移永久不可见** |
| 5 | 契约(5 份副本,md5 现为同一值) | `patbond-api/patbond-{auth,user,pet,community}/src/test/resources/contract/openapi-v1.4.0.yaml` + `patbond-doc/docs/api/openapi.yaml` | `eventVersion` 描述改无歧义表述 + 补 `minimum: 1``EventResult.reason` 枚举 +`unknown_event_version`(推荐档)。**均为纯增量**,v1.4.0 客户端无需改动。(`patbond-doc/site/` 下同名文件是 mkdocs 构建产物,不手改) |
| 6 | 客户端埋点核心 | `patbond-flutter/lib/analytics/analytics_service.dart` | ① `:139``'eventVersion': 1` 字面量改查表取值;② `:99-100` 注释按 §3.1 据实改写;③ `_platformName()` 非枚举值本地短路(§3.2);④ 新增 `PATBOND_ANALYTICS_PLATFORM_OVERRIDE` 调试覆盖 + `appVersion` 后缀护栏 |
| 7 | 客户端强类型封装(新建) | `patbond-flutter/lib/features/creation/creation_analytics.dart` | 8 条事件的编译期封装 + 枚举(沿 `pet_analytics.dart`/`post_analytics.dart`/`community_interaction_analytics.dart` 先例:枚举锁死,业务代码禁止手拼事件名与属性) |
| 8 | 客户端曝光封装(新建) | `patbond-flutter/lib/analytics/experiment_exposure.dart`(建议路径) | `experiment_exposed` 强类型封装 + 实验注册表枚举(A/B 前置 #5 的缺失半边,§1.4 |
| 9 | pageName 枚举 | `patbond-flutter/lib/analytics/analytics_page_name.dart` | +`creationResult('creation_result')`;顺带据实收敛 §4.7 登记的 7 个不存在项的注释 |
| 10 | 既有帖子埋点 | `patbond-flutter/lib/features/community/post_analytics.dart` | 发布/存草稿两处透传 `creationTaskId`(仅 AI 来源时携带) |
| 11 | E2E 脚本 | `patbond-flutter/test_e2e_m2_manual.dart``test_e2e_m3_manual.dart` | `eventVersion` 由 2/3 改 **1**(§2.2 理由 4 |
| 12 | 常设文档条数纠正 | `patbond-doc/docs/development/feature-checklist.md:218` | 「EventDictionary 22→42」改为据实的 41,并记 M4 后 49 |
| 13 | 常设文档 platform 表述 | `releases.md:120``device-verification.md:144``feature-checklist.md:221` | 把「逐条 rejected / 不影响客户端」改为「整批 400 + 永久丢弃,桌面采集能力为零」(§3.1) |
| 14 | 常设文档北极星时限 | `device-verification.md:40``iteration-3/29:52` | 按 §0.4 档 A 改为触发条件表述(迭代报告存档按「迭代报告豁免」惯例不回改,只改常设页与被引用的时限提醒) |
| 15 | ADR | `patbond-doc/docs/architecture/decisions.md` | 新增「ADR-023 M4 埋点与实验决策」:eventVersion 定型 + bump 规则、服务端不建第二管道、红线为 `creationTaskId` 开例外、北极星时限改触发条件、A/B 前置状态按取证重置为 5 绿 3 半、M4 首个实验为 A/A |
### 7.3 巡检增量
- §5 的对账 SQL 1~5 入周一巡检。
- 「字典空转清单」(§1.2 的 8 条 + M4 后新增项)纳入月度巡检,防止字典先行条目无声长期空转。
- SRM 检查(§6.2 检查项 ①)在任何实验运行期间每日跑一次。
## 8. 待拍板清单(汇总)
| # | 事项 | 选项 | 本角色裁定 / 建议 |
| --- | --- | --- | --- |
| **1** | **北极星 09-21 首次出数时限的处置** | ① 维持日期承诺 ② 改为事件驱动触发条件(档 A)+ 09-21 出基建就绪读数(档 B) ③ 造数凑读数(档 C) | **建议 ② = A+B**。09-21 已被算术判定为不可能(W37 入队窗口 09-13 已关闭,§0.2),维持日期只会持续生产假红线;档 C **明确否决**(数据造假且污染后续所有队列基线)。同时把 `iteration-2/06` §2.1 的北极星 SQL 落成仓库内可执行载体——它目前只存在于报告正文 |
| **2** | **eventVersion 口径定型** | ① 每个事件自身的 props schema 版本 ② 事件字典世代号 | **建议 ①**(§2.2 四条理由)。决定性理由是可修复性不对称:读法 ① 下客户端现行硬编码 1 **本就正确**,零改动零回填;读法 ② 下全部历史行皆错且**不可修复**(`server_ts` 无法反推事件当初属于哪代字典)。必须在 M4 新增 8 条事件**之前**定型,否则债务从 41 条规模翻到 49 条 |
| **3** | **eventVersion 是否上服务端校验** | ① 推荐档:字典 key 改 `(name, version)`,未知版本逐条 `unknown_event_version` ② 最小档:仅补契约 `minimum: 1` + DTO `@Min(1)` | **建议 ①**。§1.1 刚证明「无门禁的口径必然漂移四份文档而不可见」,纯文档纪律不足。注意实现须放**逐条拒绝路径**,不可放 DTO 校验——否则重犯 `platform` 的整批连坐错误 |
| **4** | **platform=linux 处置** | ① 把 linux/server 加进枚举 ② 客户端本地短路 + 默认关闭的调试覆盖开关 | **建议 ②**。枚举是三层锁死(契约 × 5 份 + DTO 正则 + DB CHECK `V2:23`),加值需 Flyway 迁移且会让非产品流量污染 platform 维度;收益仅「桌面调试方便」,不成比例。**采纳 ② 的前提条件**:覆盖开关必须配 `appVersion` 可识别后缀护栏(§3.2),否则它就是档 C 造数据的后门 |
| **5** | **服务端生成侧指标来源** | ① 新建 `server_events` 埋点管道 ② 扩 `product_events` 容纳服务端事件 ③ 以 `creation.generation_tasks` 事实表为权威 | **建议 ③**(§4.2)。所需事实 100% 已在任务表(M4 验收标准本就逼出这些列),事件层加不了信息还会引入双写不一致;② 需放宽两个 NOT NULL(不可逆的反向操作)+ 改 DB CHECK。**代价**:后端须承诺 §4.2 列出的列齐备且终态行保留 ≥90 天,否则对应指标不可算 |
| **6** | **隐私红线是否为 `creationTaskId` 开例外** | ① 维持「无内容或对方主体 ID」的字面禁止,漏斗改用 (userId, 时间窗) 拼接 ② 收敛措辞为「禁他人/内容主体 ID」,允许上报者自有资源 ID 作拼接键 | **建议 ②**。原红线意图是防止重建「谁对谁的内容做了什么」的社交图谱,`creationTaskId` 不涉第二主体、不泄露 prompt 与产物;① 的时间窗拼接在并发提交下会把 A 任务耗时配给 B 任务结果且不可察觉,对 P95 这类尾部指标是致命的。**否决**「做成第 11 个公共属性」的替代方案——为单域需求撬动跨迭代冻结的十项公共属性面不成比例 |
| **7** | **M4 首个「实验」的形态** | ① 直接跑真实 A/B ② A/A 基建验证先行,真实 A/B 冻结设计待流量 | **建议 ②**(§6.1)。真实 A/B 需 ~1,560 名到达结果页用户,当前真实用户 0,按 M2 §4.3 止损纪律现在就判不可行;且三项前置为零代码。A/A 零产品风险、极小样本即可暴露分流失衡与曝光漏报——**跳过它等于把三个从未运行过的组件与一个产品结论绑在一起** |
| **8** | **A/B 前置状态是否按取证结果重置** | ① 维持 `iteration-3/29` 的「6 绿 1 半」 ② 重置为 **5 绿 3 半**并写入 ADR | **建议 ②**#4 分流哈希、#5 Flutter 曝光封装、#7 feature flag 三者代码零实现(逐项 grep 取证,§1.4),且 #1/#2 的绿是**前瞻式**的(挂在「09-21 起自然达成」上,该前提已随 §0 失效)。这直接决定 M4 排期——**若按「6 绿 1 半」排,会在实验启动日才发现要先写三个组件** |
| **9** | 结果内单产物选择/滑动行为是否首版就埋(§4.6 缺口 4) | ① 首版补 `selectedIndexBucket` ② 不埋,成为问题时按 §2.3 新增可选属性 | **建议 ②**。UI 未定稿;且 `resultIndex` 类数据容易被误读成「用户偏好某模型输出」的产品结论。复活条件已写明 |
| **10** | 白名单条数口径纠正(42 → 41,M4 后 49)的回改范围 | ① 全量回改含历史迭代报告 ② 只改常设文档(`feature-checklist.md`),迭代报告存档不动 | **建议 ②**,沿既有「迭代报告豁免」惯例:迭代报告是时点存档,回改会破坏其与当时代码状态的对应关系。但**必须**同时落地 §7.2 同步点 4 的条数断言,否则纠正一次仍会漂移第二次 |
**最关键三项****#1**(北极星时限——唯一有外部日期承诺、且已失效)、**#8**(A/B 前置重置——直接改变 M4 排期与工单量)、**#5**(服务端指标来源——决定 M4 后端表结构,定晚了要返工迁移)。
## 附:M4 埋点工单拆分建议(按依赖排序)
1. **真机执行人(首日,1 小时,无依赖,最高优先)**`export ANDROID_HOME=~/Android/Sdk` + `flutter emulators --launch Pixel_7`,执行 M2 两项真机验证(步骤 `device-verification.md:42-92`,模拟器场景宿主机地址用 `10.0.2.2`),填 L106 执行记录。产出 `platform='android'` 事件实证,解除 A/B 前置 #1 的拦路项。**这是 §0.3 的 decisive finding 的直接落地,且不依赖任何其他工单**。
2. **用户 / PM(首日)**:拍板 §8 的 #1#2#5#8(前两项决定后续工单形态,#5 决定后端表结构,#8 决定排期)。
3. **数据侧(0.5 天,依赖 1**:把 `iteration-2/06` §2.1 北极星 SQL 落成仓库内可执行载体,跑出 W37 实测分母,按 §0.4 档 B 出「基建就绪读数」。
4. **后端(依赖 2 的 #5**`creation` schema 的 V6 迁移中确保 §4.2 要求的列齐备(`queued_at`/`first_attempt_at`/`attempt_count`/`failure_reason` 等)+ 终态行保留 ≥90 天纪律;与生成任务契约同批冻结 `modelKey`/`styleKey` 枚举。
5. **后端(依赖 2 的 #2#3**`EventDictionary` v4 增量 8 条 + 2 条改写(§4.5 代码块可直抄);eventVersion 校验按推荐档;**同批补字典条数与全名快照断言**(§7.2 同步点 4)。可与 4 并行。
6. **后端(依赖 5**:契约 5 份副本同步 `eventVersion` 描述 + `minimum: 1` + `reason` 枚举增量;跑契约一致性矩阵。
7. **Flutter(依赖 2 的 #4,可与 4/5 并行)**`analytics_service.dart` 四项改动(eventVersion 查表、注释据实、桌面短路、调试覆盖 + appVersion 护栏);两份 E2E 脚本 `eventVersion` 改 1。
8. **Flutter(依赖 5 的字典)**:新建 `creation_analytics.dart` 强类型封装 8 事件 + 枚举;`analytics_page_name.dart` +`creationResult``post_analytics.dart` 透传 `creationTaskId`
9. **后端 + FlutterA/B 前置补齐,依赖 2 的 #8)**:① 分流组件 `hash(userId + experimentSalt) % 100` + 实验注册表;② `experiment_exposure.dart` 曝光强类型封装;③ feature flag 开关机制(含 5 分钟内回滚能力)。**三者均为零代码起步,不得按「已就绪」排期**。
10. **本角色(依赖 9**:样本量规则 + 实验设计模板成文(A/B 前置 #3/#6,此前两迭代标绿但无文档);A/A 基建验证实验的检查项清单与 SRM 巡检脚本。
11. **本角色(M4 全程)**:每周一北极星/护栏读数复核;§5 对账 SQL 1~5 入巡检;字典空转清单月度巡检。
12. **文档(收官前)**ADR-023 落地;§7.2 同步点 12~14 的三处常设文档纠正。
@@ -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 个**
@@ -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(正典)→ api(4 份快照+守卫) | **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,由主会话统一处置。
+10
View File
@@ -106,6 +106,16 @@ nav:
- 06 M3.5 收口: development/iterations/iteration-3.5/06-wave2-closure.md
- 07 安全事件复盘: development/iterations/iteration-3.5/07-security-incident-20260911.md
- 08 发布 E2E 回归: development/iterations/iteration-3.5/08-release-e2e-regression.md
- 第四迭代 AI 创作:
- 00 开工汇总与拍板清单: development/iterations/iteration-4/00-kickoff-index.md
- 01 任务分解: development/iterations/iteration-4/01-pm-task-breakdown.md
- 02 后端技术评估: development/iterations/iteration-4/02-backend-technical-assessment.md
- 03 Flutter 技术评估: development/iterations/iteration-4/03-flutter-technical-assessment.md
- 04 现实核查: development/iterations/iteration-4/04-reality-check.md
- 05 AI 创作 UI 规格: development/iterations/iteration-4/05-ai-create-ui-spec.md
- 06 埋点与实验规划: development/iterations/iteration-4/06-experiment-tracking-plan.md
- 07 基线证据审计: development/iterations/iteration-4/07-evidence-baseline-audit.md
- 08 Git 流程规划: development/iterations/iteration-4/08-git-workflow-plan.md
- API:
- 契约说明: api/index.md
- 架构: