Files
patbond-doc/docs/development/iterations/iteration-3.5/04-contract-freeze-v140.md
T
lixi f9b1358b37
CI / docs-build (push) Successful in 1m59s
docs: 2026-09-11 安全事件复盘 + 新建服务器暴露面清单 + M3.5 报告 03/04/05 入档
安全事件(已闭环,服务恢复):
- 根因链:Gitea 3000 对公网开放 → 外部调用 /api/internal/manager/add-logger
  注入 gitconfig 的 uploadpack.packObjectsHook → 指向不存在的脚本 →
  upload-pack 发 NAK 后无法产出 pack → 全仓 HTTPS clone 失败(CI 全挂)
- 攻击未达成代码执行(hook 目标脚本不存在);三仓 ref 与本地逐一核对未被篡改;
  无系统层入侵(无陌生 key/crontab/挖矿进程/陌生登录)
- 新建常设「服务器暴露面清单」:补上服务器侧「决策变了环境没跟上」的核对机制
  (Nacos 在 ADR-002 移除后仍暴露公网近两个月)
- CI Runner 手册排障表增三条:CI 秒失败先在本机复现 checkout、跨仓比 CI
  须核对时间戳、clone 坏而 push 正常时查 packObjectsHook 注入

M3.5 交付报告:03 后端资料与头像(api 334→379)、04 契约冻结 v1.4.0
(31→32 路径、矩阵 173→181 格、11 格红转绿)、05 资料页与头像 UI(flutter 526→597)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-11 17:45:35 +08:00

245 lines
21 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 M3.5 契约冻结 v1.3.0 → v1.4.0doc 正典 + api 四模块快照与矩阵,一单连贯)
> 作者:API Platform Engineer(契约)
> 日期:2026-09-11
> 工单:T3.5-07 契约冻结 + api 侧快照/矩阵同步(M3 分两单,本次规模小故连贯执行,避免 api 侧 CI 长时间红)
> 输入:iteration-3.5/03 号报告 §1 定型表与 §6 机械化清单(**实现定型表 > 推断**);ADR-022;正典 v1.3.0doc main@6e1ab8e);patbond-api dev@d98a400379 测试,其中契约守卫 11 格红)
> 提交:doc `5f02909`origin/main)→ api `3cd8005`origin/dev
> 结论先行:**v1.4.0 已冻结并两侧同步。规模 31→32 路径 / 43→45 操作 / 72→**75** schemas(比 03 号预估的 74 多一个,理由见 §1.4)。四模块快照字节级一致(md5 `a7081f…5801` 五处相同)。矩阵 173→181 格(+8),豁免仍 1 格。T3.5-04/05/06 遗留的 11 格守卫红**全部转绿**,本地根反应堆 `clean test` 全绿(379→381),mutation 三处定向注毒均红、还原即绿,`check-secrets.sh --all` exit 0。**与定型表零矛盾**(一处可选口径与两处措辞修正见 §6,均已在此列明)。⚠️ Gitea CI 仍红——但是**先于本单存在的 runner 级故障**job 无任何 step、1~2 秒即失败),最后一次 CI 绿是 M3 末的 `8089c06`,处置见 §7。**
---
## 1. 冻结总表(逐项:新增 / 变更)
信封、命名(camelCase)、时间格式、分页正典、错误信封均沿 v1.3.0 不变。**v1.4.0 相对 v1.3.0 纯增量**——无字段删改、无类型变更、无必填收紧,v1.3.0 客户端无需改动即可继续工作(这句已写进 `info.description`,作为对既有集成方的显式承诺)。
### 1.1 路径与操作
| 变更类 | 操作 | 说明 |
| --- | --- | --- |
| **新增操作** | `PATCH /api/v1/me` | 挂在既有路径上(故路径只 +1 不 +2);tag `user` → 守卫归 **patbond-auth** |
| **新增路径 + 操作** | `GET /api/v1/me/community-stats` | tag `posts` → 守卫归 patbond-communitytag 选择理由见 §6.2 |
| 变更(扩响应字段) | `GET /api/v1/me` 200 | 补 `nickname``avatarUrl` |
| 变更(扩响应字段) | `GET /api/v1/pets``GET/POST/PATCH` 的 Pet 四处 | `Pet` schema 补 `avatarUrl`,一处改动波及四个操作的响应 |
| 变更(扩请求字段 + 新响应格) | `PATCH /api/v1/pets/{petId}` | 请求体补三态 `avatarAssetId`**新增 422/42203 单元格**404 单元格补第二种业务码 40405 |
| 变更(枚举追加) | `POST /api/v1/media/uploads` 请求 | `purpose` 枚举 `[post_image]``[post_image, user_avatar, pet_avatar]` |
规模:**路径 31 → 32;操作 43 → 45**(与 03 号 §6 预期一致)。
### 1.2 Schema 变更
| Schema | 动作 | 内容 |
| --- | --- | --- |
| `Me` | 变更 | 补 `nickname`(string, nullable, 1~32 码点) 与 `avatarUrl`(string, nullable),两者**进 required**(键恒在、值可空);description 从「恰好这 4 个字段」改为 6 个字段,并写明**不含 `avatarAssetId`** |
| `UpdateMeRequest` | **新增** | 两字段皆 nullable、皆非必填;三态语义(键缺省/显式 null/给值)逐条进 description |
| `Pet` | 变更 | 补 `avatarUrl`(string, nullable) 进 properties 与 required(位置在 `status` 后、`myRole` 前,与实现的键序一致);description 里「`avatarAssetId` 不出现在 M2 契约(ADR-010)」**改写**为「头像以 `avatarUrl` 现签形式返回,`avatarAssetId` 不外露(只写不读)」 |
| `UpdatePetRequest` | 变更 | 补 `avatarAssetId`(uuid, nullable)description 声明它是**本 schema 唯一的三态字段**,其余字段保持 M2 两态语义 |
| `CommunityStats` | **新增** | `receivedLikeCount` / `publishedPostCount`int64required,非 nullable),聚合口径逐条进 description |
| `CommunityStatsEnvelope` | **新增** | 见 §1.4 |
| `CreateMediaUploadRequest` | 变更 | `purpose` 枚举三值 + 「用途即引用侧类型检查」的说明 |
规模:**schemas 72 → 75**。
### 1.3 描述与错误码表(零新增错误码)
本单**未新增任何业务码**——复用 40000/40101/40300/40400/40401/40405/40902/42203。错误码表因此只补语义、不加号:
| 行 | 修改 |
| --- | --- |
| 40300 | 补「viewer 写记录**或改头像**、caregiver 改宠物档案——**头像除外**,见 M3.5 分档」 |
| 40405 | 补「或**用途与引用场景不符**(帖图当头像、user_avatar 当宠物头像等)」,并注明用途不符分支只对调用者自己的 asset 可达,故 message 可以具体 |
| 42203 | 补「用途相符但非 ready」与「帖图与用户/宠物头像同构」 |
新增 `info.description` 的**「用户资料与头像域约定(M3.5 冻结)」**整段(与既有 Pets 域、Community/Media 域约定同格式),把 10 条跨端点约定写在一处:三态语义的适用范围、空 patch/纯空白的 400、昵称按码点计与不做 username 回退、avatarUrl 的过期纪律与三重降级、`avatarAssetId` 只写不读、三种用途互不通用与四态校验、宠物头像的按字段分档与混合取更严、`/me` 无乐观锁但靠列级 UPDATE 防丢失更新、community-stats 的独立主体与「永不 404」。
同步修正的既有条目(不属新增,但不改会自相矛盾):Pets 域约定的三档权限表补「M3.5 起 WRITE 档还含 `avatarAssetId`」与「按本次请求触及字段定档」;`responses.PetWriteDenied``responses.MediaNotFound` 的 description 随之更新。
### 1.4 与 03 号 §6 预估的唯一偏差:schemas 74 → 75
03 号预估 `72 + UpdateMeRequest + CommunityStats = 74`,并注明「最终以正典实际计数为准」。实际为 **75**,多出的一个是 **`CommunityStatsEnvelope`**。
理由是一致性而非必要性:全 API 的每一个 200 响应都 `$ref` 一个 `XxxEnvelope` schema`MeEnvelope`/`PetEnvelope`/`FollowStatsEnvelope`…),没有任何一处内联信封。为 community-stats 内联一个信封会成为全契约唯一的例外——对生成 SDK 的工具与阅读契约的人都是一处无理由的不规则。**多一个 schema 比多一处例外便宜。**(守卫期望按实际计数写 75,四处一致。)
---
## 2. 四模块快照同步(字节级)
| 位置 | 旧 | 新 | 处置 |
| --- | --- | --- | --- |
| `patbond-auth/src/test/resources/contract/` | openapi-v1.3.0.yaml | openapi-v1.4.0.yaml | 替换(**删旧** |
| `patbond-user/src/test/resources/contract/` | openapi-v1.3.0.yaml | openapi-v1.4.0.yaml | 替换(删旧) |
| `patbond-pet/src/test/resources/contract/` | openapi-v1.3.0.yaml | openapi-v1.4.0.yaml | 替换(删旧) |
| `patbond-community/src/test/resources/contract/` | openapi-v1.3.0.yaml | openapi-v1.4.0.yaml | 替换(删旧) |
一致性校验(正典 = doc 仓 `main@5f02909``docs/api/openapi.yaml`):
```text
md5 a7081fb84f1207eef579ab94025f5801 ← 正典与四份快照,五处完全相同
sha256 0ba7bd53f4937d33dfbbf0c6d70aff79000fabeb5178eaea700e845332fcab5b ← 正典
```
- **旧 v1.3.0 快照删除而非保留**:沿 T3-19 先例——每个模块的 `OpenApiContract.RESOURCE` 只认一份快照,守卫锁 `info.version`,保留旧文件只是死重;历史版本由 git 历史与 doc 仓承载。
- 四份守卫期望同步升版:`1.3.0 / 31 路径 / 43 操作 / 72 schemas`**`1.4.0 / 32 / 45 / 75`**。
- 各域 `operationsTagged` 断言随操作面更新:auth 域 6→**7**+`PATCH /api/v1/me`)、community 域 17→**18**+`GET /api/v1/me/community-stats`)、pets 域 18 与 media 域 2 不变(= v1.2.0/v1.3.0 的冻结面未被 v1.4.0 触碰的实证)。
- `ContractValidator``allOf` 展平注释里那句「v1.3.0 引入的 `nullable + allOf: [$ref]` 模式」**刻意保留 v1.3.0 字样**:那是历史事实(该模式的引入版本),不是当前快照版本。
---
## 3. 矩阵扩展(173 → 181 格,+8;豁免仍 1 格)
| 域 | 模块 | 测试类 | 操作 | 单元格 | 增量 | 豁免 |
| --- | --- | --- | --- | --- | --- | --- |
| auth/user/analytics | patbond-auth | `AuthContractConformanceTest` | 6 → **7** | 19 → **24** | **+5** | 0 |
| pets/dictionaries/health-records | patbond-pet | `ContractConformanceTest` | 18 | 82 → **83** | **+1** | 1(沿用) |
| community | patbond-community | `CommunityContractConformanceTest` | 17 → **18** | 64 → **66** | **+2** | 0 |
| media | patbond-user | `MediaContractConformanceTest` | 2 | 8 | — | 0 |
| **合计(v1.4.0 全部 45 操作)** | 4 模块 | 4 类 | **45** | **181** | **+8** | **1** |
### 3.1 `PATCH /api/v1/me` 全响应矩阵(auth 模块,5 格)
**易漏点复核**`/api/v1/me` 的守卫与矩阵都在 **patbond-auth**(实现在 patbond-user,但契约测试在 auth 模块内启同 JVM 的真实 user 服务、跨服务发真实 HTTP)。03 号 §6 特别提示过这点,本单在类 javadoc 里把它写成了常设备注,避免下一次扩契约再踩。
| 单元格 | 触发方式 |
| --- | --- |
| 200 | 三态「给值」设昵称(并断言 `avatarUrl` 为 null——存储未配置时的降级实证);三态「显式 null」清空昵称(断言回 null) |
| 400/40000 | 空 patch `{}`(证明不静默 200 |
| 401/40101 | 无 token |
| 404 | **同格两码**:合法签名但用户不存在 → 40400;幽灵 `avatarAssetId` → 40405 |
| 422/42203 | 本人的 `user_avatar` asset 仍在 `uploading` |
附带把 `GET /api/v1/me` 200 在 **nickname 非空分支**上再走了一遍严格校验(v1.3.0 时代该字段不存在,此前只覆盖过全空形态)。
- 422 需要一枚可控状态的 asset。本上下文未配置对象存储(走 `POST /media/uploads` 会 500),故按 `MeProfileIntegrationTest` 的先例直接写 `media.assets` 行——**测试数据造法,不触碰实现**。
### 3.2 `GET /api/v1/me/community-stats`community 模块,2 格)
| 单元格 | 触发方式 |
| --- | --- |
| 200 | ① 全新用户:断言两数为 **0 而非 null 且不是 404**;② 两篇已发布帖 + 他人赞一次:断言 `1 赞 / 2 作品` |
| 401/40101 | 由既有「全操作 401 循环」自动覆盖(新操作入 `COMMUNITY_OPERATIONS` 即被纳入) |
该操作**只有两格**——契约上没有 404,正是「永不 404」这条定型的机器可读表达:矩阵门禁不会去找一个不存在的 404 格。
### 3.3 `PATCH /api/v1/pets/{petId}` 的新格(pet 模块,1 格 + 1 码)
| 单元格 | 触发方式 |
| --- | --- |
| **422/42203(新增状态码)** | 本人的 `pet_avatar` asset 仍在 `uploading` |
| 404 的第二种业务码 40405 | 幽灵 `avatarAssetId`;以及**用途不符**(本人的 `post_image` asset)——两者合并同答,防枚举语义实证 |
| 200(补一格路径) | 挂上 ready 的 `pet_avatar` |
- pet 模块的测试数不变(100):新单元格加在既有测试方法 `validationConflictAndRuleErrorsMatchContract` 内,不新建方法。
- `avatarUrl` 的**非 null 分支不在契约矩阵内**:pet/auth 两个契约上下文都未配置对象存储,`avatarUrl` 恒 null(nullable 声明因此得到实证)。真实签名 URL 的全链路由 `MeAvatarSigningIntegrationTest`(真实 MinIO、真实下载比对)与 `PetAvatarIntegrationTest` 覆盖——它们是业务测试而非契约测试,分工不变。
### 3.4 豁免格清单
**本单新增矩阵零豁免。** 全仓唯一豁免格仍是 pet 侧沿用的 `PATCH /api/v1/care-reminders/{reminderId} 409`(并发条件更新守卫落空,单线程无法确定性构造,行为语义由并发一致性设计文档背书)。
---
## 4. 11 格红转绿对照
| 模块 | 测试类 | 原红格 | 根因 | 本单处置 | 现状 |
| --- | --- | --- | --- | --- | --- |
| patbond-auth | `AuthContractConformanceTest` | 2`GET /api/v1/me 200``$.data.nickname``$.data.avatarUrl` 契约未声明;连带 `everyDeclaredResponseCellIsExercised` | v1.3.0 的 `Me` 冻结面不含两字段 | `Me` 补两字段进 properties + required,快照升版 | ✅ 绿 |
| patbond-pet | `ContractConformanceTest` | 9`POST /api/v1/pets 201``$.data.avatarUrl` 契约未声明,经 `newCat()` 连锁到 6 个用例;连带矩阵门禁) | v1.3.0 的 `Pet` 冻结面不含 `avatarUrl` | `Pet``avatarUrl` 进 properties + required,快照升版 | ✅ 绿 |
| **合计** | | **11** | 单一根因:「未声明字段即漂移」× 尚未冻结 | | **11/11 转绿** |
9 个 pet 红格确如 03 号判断,是**一个字段**经建宠前置步骤放大的连锁,而非 9 个独立问题——一处 schema 改动即全部消解。
测试数:**379 → 381+2**
| 模块 | 基线 | 现在 | 增量 |
| --- | --- | --- | --- |
| patbond-common | 3 | 3 | — |
| patbond-user | 131 | 131 | —(守卫升版,数量不变) |
| patbond-auth | 39 | **40** | +1`meProfileWriteShapes` |
| patbond-pet | 100 | 100 | —(新格并入既有方法) |
| patbond-community | 106 | **107** | +1`myCommunityStatsSuccessShapes` |
| **合计** | **379** | **381** | **+2** |
---
## 5. mutation 自证(注毒应红、还原应绿)
三处注毒**定向打本单新增的冻结面**,而不是随便找一处已有字段——目的是证明新增的声明真的在校验路径上,不是写在契约里没人读的死字。
| 轮次 | 注毒点(快照) | 预期 | 实测 |
| --- | --- | --- | --- |
| 1 | auth`Me.required` 追加 `fakeMeField` | 红 | 3 失败:`GET /api/v1/me 200`**`PATCH /api/v1/me 200`** 均报 `$.data.fakeMeField: 契约必填字段缺失`,矩阵门禁连带红 |
| 2 | pet`Pet.required` 追加 `fakePetAvatarField`(紧邻新加的 `avatarUrl` | 红 | 9 失败:`POST /api/v1/pets 201` 等报 `$.data.fakePetAvatarField: 契约必填字段缺失`(与 §4 的 9 格连锁同形,反向印证根因判断) |
| 3 | community`CommunityStats.required` 追加 `fakeStatsField` | 红 | 2 失败:**`GET /api/v1/me/community-stats 200`** 报 `$.data.fakeStatsField: 契约必填字段缺失`,矩阵门禁连带红 |
| 还原 | 四快照 `cp` 回正典 + md5 复核 | 绿 | 五处 md5 一致;根反应堆 `clean test` **BUILD SUCCESS**381 测试全绿 |
第 1 与第 3 轮的失败点分别落在 `PATCH /api/v1/me 200``GET /api/v1/me/community-stats 200` 上,正是本单**新入矩阵**的两个操作——这两条即「新格真的在跑」的证据。
> 排障备注(沿 03 号 §5):本轮第一次用 `./mvnw test -pl patbond-auth,patbond-pet,patbond-community`(未加 `-am`)跑 mutation,得到 9 个 **假红**——`NoClassDefFoundError: JdbcClient`,根因是 patbond-user 从本地仓库的旧 jar 解析而非反应堆。**mutation 与门禁验证一律走根反应堆**`./mvnw clean test` 或加 `-am`),本报告的红/绿结论均来自根反应堆运行。
---
## 6. 冻结中的判断记录(含三处偏离与修正,逐条列明不自行仲裁的部分)
### 6.1 与 03 号定型表的一致性:零矛盾
逐项对照实现代码复核(`MeResponse` 6 字段、`UpdateMeRequest` 两个 presence 标志与 `isEmptyPatch``PetResponse``avatarUrl``status` 之后、`CommunityStatsResponse` 两个 `long``MeProfileService` 的错误码序列、`PetService.requiredLevel` 的按字段分档、`ErrorCode.USER_NOT_FOUND=40400``MediaProperties.allowedPurposes` 三值)——**定型表与实现一致,与 ADR-022 一致,内部无矛盾**。冻结按定型表照单全收,未作任何自行裁量的语义改动。
### 6.2 需要拍板者知晓的三处判断(均不改变定型语义)
1. **`CommunityStatsEnvelope`schemas 75 而非 74)**——见 §1.4。定型表授权「以正典实际计数为准」,此处按一致性优先。
2. **`GET /api/v1/me/community-stats` 的 tag 选 `posts`,而非新开一个 `stats` tag**。约束前提:四个守卫用 **tag 集合划分模块归属**auth 模块认 `{auth,user,analytics}`、community 模块认 `{posts,feed,comments,interactions,follows}`),所以该操作**必须**带一个 community 侧的 tag,不能用 `user`。在 `posts` 与新 tag 之间选了 `posts`:两个数字都由帖子派生(`SUM(posts.like_count)` 与帖数),端点也住在 `PostController``/me/posts` 旁边;而新开一个 `stats` tag 会让门户上出现两个 stats 分区(`follow-stats` 按主体归在 `follows`),对翻文档的人是无来由的意外。`posts` 的 tag description 已相应补一句「含由帖子派生的『我的社区数字』聚合」。若拍板者更倾向独立 tag,改动面是 1 行 tag + community 守卫的 tag 集合 + 本行说明。
3. **40405 的 example message 从「媒体不存在」改为「媒体资源不存在」(3 处)**——服务端 `ErrorCode.MEDIA_NOT_FOUND` 的实际 message 是「媒体资源不存在」,v1.3.0 的三处 example 与之不符。example 不在守卫校验范围内,但同一业务码在契约里出现两种 message 正是集成方会踩的那类小意外,故一并对齐到实现。**未改任何业务码与 HTTP 语义。**
### 6.3 刻意不动的一处既有不规则(留档,不在本单裁量)
`Me.phone` 的键与 `nickname`/`avatarUrl` 一样恒存在(record 序列化),但 v1.3.0 只把它放在 properties、未进 `required`。本单把新增的两字段**放进了 required**(03 号定型表明确「键恒在」),于是同一 schema 内出现「同样恒在的三个可空字段,两个 required 一个不 required」的不规则。**未顺手把 `phone` 补进 required**——那会改动一个既有字段的声明(虽然对消费方是安全的强化),超出本单「冻结第一波新增面」的范围。建议留作一次独立的、显式的契约整理,不夹带在冻结里。
---
## 7. 提交、门禁与 CI 状态
### 7.1 提交
| 仓 | 分支 | hash | 内容 |
| --- | --- | --- | --- |
| patbond-doc | main(未受保护,直推) | **`5f02909`** | `docs/api/openapi.yaml` 升 v1.4.0 + `docs/api/index.md` 同步(32 路径/45 操作、M3.5 段落、冻结纪律行) |
| patbond-api | **dev**main 受保护,禁直推) | **`3cd8005`** | 四模块快照替换 + 四份守卫升版 + 矩阵扩展;12 文件,**零 `src/main` 改动** |
### 7.2 本地门禁(两侧全通)
```bash
# doc 侧
cd <你的工作区>/patbond-doc
python3 -c "import yaml;yaml.safe_load(open('docs/api/openapi.yaml'))" # 解析通过
# 另校验:96 处 $ref 全解析、45 个 operationId 无重复无缺失、零未引用 schema、
# tags 声明与使用双向闭合
mkdocs build --strict # 通过
bash scripts/check-secrets.sh --all # exit 0
# api 侧
cd <你的工作区>/patbond-api
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test # BUILD SUCCESS381 测试全绿
bash scripts/check-secrets.sh --all # exit 0
```
### 7.3 ⚠️ Gitea CI:红,但先于本单存在(runner 级故障,非测试失败)
`3cd8005` 的 commit status`failure``CI / backend-test (push)`**"Failing after 2s"**。
判定为**基础设施故障而非本单引入**,三条证据:
1. **前一个提交同症**`d98a400`T3.5-06)同样 `failure`"Failing after **1s**"。最后一次 CI 绿是 **M3 末的 `8089c06`**"Successful in 5m26s")——即 M3.5 第一波开始后 CI 就没再绿过。
2. **job 无任何 step 执行**Gitea 的 run 详情返回 `currentJob.steps: null``logs.stepsLog: null``duration: 2s`。工作流第一步是手动 checkout,连它都没跑起来,说明失败发生在 job 装配阶段(`runs-on: ubuntu-latest` 无匹配 runner,或 runner 拉不起容器镜像)。真实的 `./mvnw -B clean test` 需要数分钟,1~2 秒不可能是测试红。
3. **同一条命令本地绿**CI 跑的是 `./mvnw -B clean test``sh scripts/check-secrets.sh --all`,两者本地在 `3cd8005` 的树上均通过(§7.2)。
处置:**不在本单范围内自行修 runner**(需服务器侧凭证与 Actions 配置,属 Git/CI 工程角色)。请波末收口时一并处理,并按 iteration-3/08 的 CI 规划复核 runner 在线状态与镜像可用性。在 CI 恢复前,api 侧的放行证据以**根反应堆本地门禁 + 本报告的 mutation 自证**为准。
---
## 8. 冻结后的纪律与交接
- **契约同步纪律自此仍是「五处」**:doc 正典升版 → 四模块字节级复制新快照 + 四份守卫期望(`info.version` / 路径 / 操作 / schemas / `operationsTagged`)更新。任一处忘记同步,CI(与本地门禁)立即红。
- **`/api/v1/me` 的守卫在 patbond-auth** ——已写进该类 javadoc 的常设备注。下次扩 `/me` 面时先看 auth 模块。
- **第二波(客户端 T3.5-08/09/10)可依赖的接口面即 v1.4.0 正典**,无需再读 03 号定型表推断。三条客户端纪律再强调:`avatarUrl` **会过期、禁止入本地存储**(沿 M3 纪律 R2`SignedNetworkImage` 的缓存 key 已剥签名参数);「是否有头像」判 `avatarUrl != null`(响应里没有 `avatarAssetId`,别去找);本人昵称展示做 `nickname ?? username``/me` 不回退是定型,不是遗漏)。
- **v1.4.0 之后的新增仍走纯增量**:新增可选字段/新增端点/枚举追加不需要新主版本;任何字段删改、类型变更、必填收紧都需要 v2 + 迁移指南 + 日落期,不得在 v1 内静默进行(`info.description` 的纯增量承诺已把这条写给集成方看)。
- 本报告只写不提交(随波末统一入档);`mkdocs.yml` 本次未动;03 号报告仍未 commit(波末一并入档)。