79b33dba31
八角色并行开工分析,合计 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>
688 lines
52 KiB
Markdown
688 lines
52 KiB
Markdown
# 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.0:32 路径 / 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 stripped(M4 补回)」 | ✅ 对 |
|
||
| 六容器部署(含自托管 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 0,1.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 WORK(M4 前必须定)** | 定「版本号随字典版本」还是「随单事件 schema」;同步改契约描述(现称「字典 v1 全部为 1」);补服务端校验 |
|
||
| 429 限流 | 🟠 **NEEDS WORK(M4 强相关)** | 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>
|