Files
patbond-doc/docs/development/iterations/iteration-4/04-reality-check.md
T
lixi 79b33dba31 docs: M4「AI 创作」开工分析八份报告 + 汇总拍板页入档挂导航
八角色并行开工分析,合计 7448 行;另出 00 汇总页(跨角色收敛结论、
13 项待拍板、6 项待仲裁分歧、未取证项汇总),挂第四迭代导航最前。
mkdocs build --strict 通过。

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 15:48:56 +08:00

688 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 断言**精确值 | 机械可数 226210 `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>