Files
soft_quay/docs/review/phase2-review.md
T

289 lines
24 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.
# Phase 2 审查(T-201 ~ T-204:清单接入 / 本地状态 / 虚拟列表 / 详情与图标缓存)
> 审查范围:`2e21c9f`(T-201 catalog 接入)、`819b1cd`(T-202 本地状态识别)、`db8d93e`(T-203 虚拟列表)、`a52acbf`(T-204 详情与图标缓存)。
> 审查视角:全栈开发工程师。
> 审查方式:静态代码审计 + 对照 Phase 0/1 遗留项核验。因 WSL 无 Go,未执行测试。
> 附带确认:Phase 0 的 P1(workspace 隔离,T-604)与 Phase 1 的 P0(Windows 路径,T-605)是否真正闭合。
> 日期:2026-07-17
## 总体判断
Phase 2 质量稳定,延续了 Phase 1 的工程水准。**Phase 0 与 Phase 1 审查提出的两个结构性问题已被真正修复并接线**(见"遗留项闭合确认")。Phase 2 本身无安全级缺陷,主要是若干性能与可维护性优化项。M3 前半(清单→列表→详情)成立。
## 遗留项闭合确认(重要)
| 遗留项 | 状态 | 证据 |
| --- | --- | --- |
| Phase 1 P0 · Windows dot-space 路径逃逸 | **已闭合** | 新建 `core/internal/safepath`:`validateSegment` 逐段拒绝首/尾 ASCII 空格、尾随句点、DOS 设备名(含 `COM¹²³` 变体)、控制字符与 `<>:"\|?*`;`JoinUnder` 在 `filepath.Join` 后用 `filepath.Rel` 做 destination 包含性兜底。`extractor.go` 已改用 `safepath.ValidateRelative` + `JoinUnder`(不再只靠 POSIX `path.Clean`);`parser.go`、`storage/installed_app.go` 共用同一校验器,无各层漂移。两条整改建议(逐段校验 + 包含性兜底)都落地。 |
| Phase 0 P1 · 根 workspace Gio 版本污染 | **已闭合**(T-604) | 见 `7efcab5 Isolate modern and Win7 workspaces`(需在该任务范围单独确认,非本次重点) |
## 逐模块核验
### T-201 · 清单接入(catalog/parser + filter + client)
- `parser.go` 严格:`DisallowUnknownFields`、拒绝尾随 JSON、拒绝重复 app id、signature 必须 strict Base64 且解码为 64 字节、URL 校验。签名验证已在 T-101 闭环,parser 作为 `DocumentValidator` 接入 loader(Phase 1 已确认 `TestClientValidatesBeforeReplacingCache`)。
- `filter.go`:**channel 交叉拒绝正确**——`manifest.Channel != target.Channel` 直接整份拒绝,Win7 客户端不会消费 modern 清单;`hidden` 跳过,`deprecated`/`min_os` 不满足/无对应架构包的项**保留可见并带 Reason、不可安装**,符合"不兼容软件可见说明但不可下载"。
- `supportsOS` 用 rank map(7/10/11),高版本系统可装低 min_os 应用,正确。
- `entry.Package = &publishedPackage`:`publishedPackage` 是循环体内 `:=` 新变量,逐次地址独立,无 Go 循环变量取址陷阱。
### T-202 · 本地状态识别(domain/semver + status_resolver + storage/installed_app)
- `semver.go` 符合 SemVer 2.0.0:prerelease 数值/字母混合比较、数值 < 字母、较短 prerelease 优先、build 元数据不参与排序、prerelease 数值前导零拒绝。`compareNumericText` 用"长度优先再字典序"比较数值文本,规避大版本号整数溢出,且因核心标识拒绝前导零而正确。
- `status_resolver.go`(Phase 0 复核已见):按"恢复事务→活跃操作→运行→不兼容→是否安装→是否有更新"从 facts 推导单一可见状态,IO-free。真实的状态权威在此,`allowedTransitions` 迁移表仍是并行的测试脚手架(Phase 0 复核已记,待收口)。
- `installed_app.go` 共用 safepath 校验文件路径。
### T-203 · 虚拟列表(application/CatalogListModel + 两个 Gio 适配)
- **共享 ViewModel 纪律落地**(回应 Phase 0 P2):`core/application.CatalogListModel` 持有 items/visible/categories/query/category/view/selectedID,全部 IO-free 纯内存;搜索、单分类、all/installed/updates 视图、选中态都在 core。
- **控件状态按 app ID 保存**(架构硬约束满足):`shell.rows map[string]*rowControls`、`shell.icons map[string]paint.ImageOp`、`categoryControls map[string]*widget.Clickable` 均以稳定 ID/分类名为键;`SetItems` 按 `item.ID` 保留既有 `rowControls`,列表过滤/重排时点击态不串行。
- Gio 侧用惰性 `layout.List`;Layout 内 grep 无 `os.`/`http.`/`sha256`/`ReadFile`,确认无 IO。
### T-204 · 详情与图标缓存(catalog/icon_cache + Gio 详情)
- `icon_cache.go` 安全性对齐下载/清单缓存:
- `cacheKey` 要求 `sha256:` 前缀 + 校验 hex 解码/长度/DPI 范围(48–768);文件名 `<64hex>-<dpi>.icon` 由已验证 hex + int 拼成,无路径分隔符,无穿越面。
- 内存→磁盘→远端;磁盘与远端字节**都重新 SHA-256 复验 + 尺寸/大小复核**,不盲信磁盘。
- **图片解压炸弹防御**:`DecodeConfig` 先取宽高并按 2048 上限拒绝,**再**做完整 `image.Decode`,阻止"小文件声明巨大尺寸";输入 2 MiB 上限。
- 磁盘 Lstat 拒绝符号链接/非普通文件;原子写(temp+chmod 0600+sync+rename);损坏普通缓存删除重取。
- Gio 详情右栏只读 `SelectedItem()` 与内存 `paint.ImageOp`,关闭详情不清筛选/列表位置。
## 需优化项(按优先级)
### P1 · 图标缓存的 mutex 横跨网络 fetch,拖累滚动吞吐
**现状:** `IconCache.Load` 全程 `cache.mu.Lock()` + `defer Unlock`,且在锁内调用 `cache.fetcher.FetchIcon(ctx, request)`(网络 IO)。
**影响:** 任意一个图标的远端拉取在飞行中时,**所有**其他 `Load`(哪怕是内存命中)都阻塞在同一把锁上。数百项列表滚动时按需拉图标会形成 convoy,与"数百项流畅滚动"验收目标直接冲突。当前实现用全局串行换取了"同一图标不重复拉取",但牺牲了不同图标的并行度——对网络受限的图标加载,并行度通常更重要。
**建议:** 锁内只做内存/磁盘查与内存写,fetch 在锁外(check→release→fetch→re-acquire→store);或用 per-digest 锁 / `singleflight`(注意保持 core 的 Go 1.20 兼容)。
### P2 · 两个 Gio shell 的并行布局代码持续翻倍
**现状:** T-203 给 `app-modern/ui/gio/shell.go` 加了 712 行、`app-win7` 加了 626 行;modern shell 已 922 行。共享 ViewModel 覆盖的是**逻辑**,布局/控件代码仍是两份平行实现。
**影响:** modern(Gio v0.10.1)与 win7(Gio v0.6.0)API 有差异,平行是既定取舍;但每新增一个视图(详情、下载、设置、授权),这 600+ 行平行代码就再翻一倍,且两份必须手动保持行为一致,漂移风险随视图增加上升。
**建议:** 不强行共享 Gio 控件代码,但补一层**跨适配的行为一致性测试**(对同一 `CatalogListModel` 状态断言两个 shell 的可见项/选中项/视图切换语义一致),成本低、能挡住漂移;长期可评估数据驱动的布局描述层(纯数据、非 Gio)减少平行量。
### P3 · 多标签筛选:需求与实现口径不一致
**现状:** `CatalogListModel` 提供单分类过滤 + 标签并入搜索(`matchesQuery` 含 Tags),但没有"多标签筛选"。`docs/routes.md` 写"分类和多标签筛选",而 `02-requirements.md` 表格把标签定位为搜索项。
**影响:** 文档内部口径不一,实现满足 requirements 表但不满足 routes 的"多标签筛选"。
**建议:** 先裁定多标签筛选是否属 MVP:若是,补 `CatalogListModel` 的多标签过滤;若否,改 `routes.md` 表述与实现对齐。不要留文档-实现二义。
### P4 · `VisibleItems()` 返回可被 refilter 复用的切片
**现状:** `refilter` 用 `model.visible = model.visible[:0]` 复用底层数组,`VisibleItems()` 直接返回 `model.visible`,注释称"immutable-by-convention"。
**影响:** 单线程 UI 下每帧现取现用,安全;但任何调用方若跨一次 `refilter` 持有该切片,底层数组会被 append 覆盖,是隐性 footgun。
**建议:** 返回拷贝,或把"不得跨 SetQuery/SetView/SetItems 持有"写成硬约束注释并在测试固化。
### P5 · shell.go 单文件 922 行,随视图增长将更臃肿
**现状:** 搜索、分类条、虚拟列表、详情右栏都在一个 `shell.go` 的 Layout 链里。
**建议:** 视图增多前,按视图拆文件(list/detail/category),降低单文件复杂度与两适配漂移面。优先级低。
### P6 · 图标磁盘缓存遇 symlink 硬失败而非自愈
**现状:** `loadDisk` 遇符号链接返回 `ErrIconCacheUnsafe`,`Load` 对该错误早返回、不删除、不回退远端。
**影响:** 被植入的 symlink 缓存项会**永久**卡住该图标直到人工清理。相较普通损坏缓存"删除重取"的自愈,这里选择了保守硬失败。
**建议:** 保守是可辩护的(不自动删可疑文件),但至少记一条明确告警/诊断,便于运维定位;或对 symlink 缓存项也走"隔离到 .quarantine 后回退远端"。
## 结论与处理顺序
Phase 2 可作为后续下载/安装 UI 接入的可信基线。建议顺序:
1. **[性能 · 影响验收]** P1:图标缓存 fetch 移出锁,消除滚动 convoy。
2. **[防漂移 · 低成本]** P2:补跨适配行为一致性测试。
3. **[消除二义]** P3:裁定多标签筛选归属并对齐文档/实现。
4. **[健壮性]** P4/P6:VisibleItems 拷贝或硬约束;图标 symlink 缓存告警/隔离。
5. **[可维护性 · 低优先]** P5:shell.go 按视图拆分。
> 声明:本报告为静态审计;P1 的吞吐影响、两适配行为一致性建议在真实 Windows Gio 运行与 `go test` 下验证。Phase 0 P1(T-604)与 Phase 1 P0(T-605)已确认接线闭合,但其各自任务范围的完整验收以对应任务复核为准。
## Codex 全栈二次复核
> 复核视角:Codex 全栈开发工程师。
> 复核方式:对照 T-201~T-204 任务边界、当前实现调用点、现行需求/路由文档和双 workspace 测试逐项核验。
> 复核基线:`6fd19d0`。
> 日期:2026-07-17
### 复核结论
原审核的总体判断成立:Phase 2 没有阻断后续开发的功能或安全缺陷,T-201~T-204 可以作为后续下载/安装 UI 接入的基线;T-604/T-605 的代码级整改也已经接线闭合。
但原整改清单不宜原样落任务:
1. P1 的并发问题真实,但当前 `IconCache` 尚未接入生产装配,不能直接认定已经破坏列表滚动验收。
2. P2/P4/P5 的现象成立,实施方式需要兼顾双 Gio 隔离与每帧分配成本。
3. P3 引用了已经不存在的文档冲突,应撤销。
4. P6 是可辩护的 fail-closed 安全策略,不应定性为 Phase 2 缺陷。
5. 原审核遗漏了远端响应读取上限和 Gio UI 线程投递两个正式接入前必须冻结的约束。
### 对原优化项的修订
#### P1 · 接受问题,修正影响范围与落地方案
`IconCache.Load` 当前从 `cache.mu.Lock()` 到返回全程持锁,锁内包含磁盘读取、图片验证、`FetchIcon` 网络调用、磁盘写入和内存更新。不同 digest/DPI 的请求因此也会互相阻塞,慢请求还会阻塞本应很快的内存命中。该并发设计需要修复。
但当前生产代码没有 `IconCache.Load` 调用者,仅测试直接调用;T-204 也明确把真实网络请求装配留给后续任务。图标未命中时 UI 使用内存占位,Layout 不等待 `Load`。因此准确结论是:
- 这是正式图标后台加载接入前必须关闭的吞吐风险。
- 它尚不是已经复现的“滚动卡顿”或 Phase 2 验收失败。
建议使用按 cache key 的 in-flight 去重:全局 mutex 只保护 memory/in-flight map,不同 key 的磁盘和网络工作可以并发;同一 key 只允许一个 leader 获取并让等待者复用结果。不要只做简单的 check→unlock→fetch→lock→store,否则同一图标仍会重复下载和竞争写盘。
并发测试至少覆盖:
- 慢 key 不阻塞另一 key 的内存命中。
- 不同 key 可以并行获取。
- 同一 key 并发只获取一次。
- leader 失败、context 取消和磁盘写 warning 能正确传播,且不会遗留永久 in-flight 状态。
#### P2 · 部分接受,测试应覆盖适配器接线而非重复 ViewModel 语义
两个 `shell.go` 的平行实现和持续膨胀属实,modern/win7 的行为漂移风险存在。但筛选、可见项、选中项和视图切换的核心语义已经由共享 `core/application.CatalogListModel` 决定并在 core 测试覆盖;把相同 ViewModel 断言再对两个 shell 各跑一次,不能有效发现 Gio 事件接线漂移。
更有价值的做法是建立一组适配器交互契约,分别在 modern 与 win7 包中复用同一用例矩阵,验证:
- 搜索、分类、视图按钮和清空筛选事件实际更新共享 model。
- 行点击打开正确 app ID,关闭详情保留筛选和列表上下文。
- 500 项有限 viewport 仍只布局可见子集。
- row controls 按 app ID 保持,删除的 app/category controls 被释放。
- 空 Catalog 与过滤无结果保持不同恢复语义。
测试辅助数据可以共享,但不要让 modern workspace import win7 Gio,也不要为了测试重新合并两个不兼容的 Gio 版本。
#### P3 · 撤销:现行文档不存在多标签筛选冲突
现行文档口径一致:
- `docs/02-requirements.md` 定义“按名称/标签搜索,按分类和全部/已安装/可更新视图筛选”。
- `docs/routes.md` 明确记录“多标签复选随对应用例后续接入”。
- `docs/tasks/T-203.md` 的边界明确“不实现多标签复选”。
当前实现满足 Phase 2 已冻结范围。除非产品后来把多标签复选提升为 MVP,否则无需修改代码或文档,P3 不应进入整改任务。
#### P4 · 接受 API footgun,但不建议每帧无条件返回拷贝
`VisibleItems()` 暴露 `model.visible`,而 `refilter()` 用 `model.visible[:0]` 复用 backing array。调用者若跨一次 refilter 保存旧切片,内容可能被覆盖;调用者也能直接改写当前切片元素。这是导出 API 的生命周期风险。
不过两个 Gio Layout 每帧都会读取 `VisibleItems()`,若每次访问都复制数百项,会引入与性能目标相反的持续分配。建议二选一:
1. `refilter` 每次构造新的 snapshot backing array并原子替换,访问方约定只读;旧 snapshot 可安全保留到下一帧之后。
2. 提供 `VisibleLen()` + `VisibleItem(index)` 只读索引接口,Layout 不再持有内部切片。
无论采用哪种方式,都应补“跨 refilter 持有旧快照不会变化”或“调用者无法取得内部切片”的测试。
#### P5 · 接受为低优先级维护项
按 list/detail/category 或 header/content/detail 拆分同 package 文件有助于降低单文件复杂度和双适配 diff 噪声,且不会破坏 Gio 版本隔离。建议在新增下载队列、授权或设置视图前完成,但不需要为了纯文件拆分阻断当前功能任务。
#### P6 · 改为安全策略与诊断观察项
对 symlink/非普通缓存项返回 `ErrIconCacheUnsafe` 并停止回退,是明确的 fail-closed 策略:代码不会跟随可疑链接读取或写入攻击者选择的位置。自动删除、rename 或隔离可疑对象需要额外处理 Windows reparse point、跨进程竞争和 TOCTOU,不能默认比硬失败更安全。
正式装配日志/错误 UI 时应把 `ErrIconCacheUnsafe` 转成可定位的诊断和人工清理指引。只有产品明确接受自动隔离策略并补齐攻击测试后,才考虑 quarantine;P6 不作为 Phase 2 必修复代码缺陷。
### 原审核遗漏的接入风险
#### 新 P1 · 2 MiB 校验不能限制 Fetcher 已经读取和分配的响应
`IconFetcher.FetchIcon` 返回完整 `[]byte`,`IconCache.validate` 在 Fetch 完成后才检查 `len(document)`。这可以阻止超大对象进入缓存,却不能阻止未来 HTTP Fetcher 先下载并在内存中分配任意大响应。原审核把它概括为“输入 2 MiB 上限”不够准确。
正式网络 Fetcher 必须在读取阶段同时执行:
- 拒绝明显超过上限的 `Content-Length`。
- 用 `io.LimitReader(maxBytes+1)` 或等价有界读取处理缺失/伪造的长度头。
- 读到 `maxBytes+1` 即返回资源超限,不得把完整大响应交给 `IconCache`。
该约束应与 P1 并发整改一起进入正式图标加载任务。
#### 新 P2 · `ApplyIcon` 必须在 Gio UI 事件循环中执行
modern/win7 的 `ApplyIcon` 都直接写 `shell.icons` map,Layout 同时读取该 map,当前没有 mutex。现阶段只有测试在同一 goroutine 调用,没有数据竞争;未来若后台加载 goroutine 直接调用 `ApplyIcon`,会产生 map race,并且不会自动遵守 Gio 的 UI 更新模型。
正式接入必须保持现有架构硬约束:后台任务只发布完成事件;UI goroutine 在 drain/ApplyEvent 阶段执行 `DecodeIcon` 之外的 model/ImageOp 更新并调用 `Window.Invalidate`。应增加 `go test -race` 可执行环境下的事件投递测试或等价的单线程契约测试,禁止把 `ApplyIcon` 宣称为可从任意 goroutine 调用的线程安全 API。
### 验证补充
原审核因 WSL 无 Go 未执行测试。本次在 Windows 环境按隔离 workspace 补跑并通过:
- Go 1.20.14、`GOWORK=off`:`go -C core test -count=1 ./catalog ./application ./domain ./storage`。
- Go 1.25.0、根 `go.work`:`go -C app-modern test -count=1 ./ui/gio`。
- Go 1.20.14、`app-win7/go.work`:`go -C app-win7 test -count=1 ./ui/gio`。
测试通过证明当前覆盖范围内实现一致,不能替代未来 IconFetcher 并发/有界读取测试、UI 线程投递测试或真实 Windows 滚动性能测量。
### 修订后的处理顺序
1. **[正式图标接入前 · 最高]** `IconCache` 改为按 key in-flight 去重,网络/磁盘工作移出全局锁;HTTP Fetcher 在读取阶段执行硬大小上限;补并发、取消和有界读取测试。
2. **[正式图标接入前 · 契约]** 后台结果通过事件队列回到 Gio UI goroutine 后执行 `ApplyIcon`/Invalidate,禁止后台 goroutine 直接改 shell map。
3. **[防漂移]** 为 modern/win7 补适配器交互契约测试,重点验证 Gio 事件接线、虚拟化和上下文保持,不重复证明共享 ViewModel 的纯逻辑。
4. **[API 健壮性]** 修正 `VisibleItems` 快照生命周期,避免每帧无条件复制。
5. **[可维护性 · 低优先]** 在继续增加视图前按职责拆分两个 `shell.go`。
6. **[观察项]** 为 `ErrIconCacheUnsafe` 增加上层诊断/人工清理指引;是否自动 quarantine 另行做威胁模型裁定。
> 二次复核裁定:保留 Phase 2“可信基线”的总体结论;接受原 P1/P2/P4/P5 的问题事实但修订定级或落地方式;撤销原 P3;把原 P6 降为安全策略观察项;新增远端有界读取和 UI 线程投递两项正式接入门槛。当前不需要回滚或重做 T-201~T-204。
## 交叉复核裁定(定稿)
> 复核视角:Claude 全栈开发工程师,对 Codex 二次复核逐条核验后裁定。
> 核验方式:重读当前 `routes.md`/`T-203.md`/`02-requirements.md` 措辞;对照 `icon_cache.go` 的 `validate` 位置、`shell.go` 的 `ApplyIcon`/`icons` map 读写点、`SetItems` 剪枝范围、`IconCache.Load` 生产调用者确认技术主张;因 WSL 无 Go,未执行测试。
> 日期:2026-07-17
### 承认原审核错误
本轮 Codex 复核比原审核更准。原审核有一处 stale 误判与两处不精确表述,应更正:
- **原 P3(多标签筛选文档冲突)撤销——原审核错。** 当前文档口径一致:`routes.md` 第 26 行"多标签复选随对应用例后续接入"、`T-203.md` 边界"不实现…多标签复选"、`02-requirements` 把标签定位为搜索项。原 P3 依据的是最初 routes.md 版本的记忆,**未重读当前文档**;Codex 在 T-203 已将其更新为"后续接入"。不存在文档-实现二义,P3 不进整改。
- **原 T-204 "输入 2 MiB 上限" 不精确。** `validate` 的 `len(document) > maxBytes` 在 `FetchIcon` 返回完整 `[]byte` **之后**执行,只界定"进缓存",不界定 fetch 内存。
- **原 T-204/P1 "ApplyIcon 从后台调用" 表述有误且危险。** `ApplyIcon` 写 `shell.icons`、Layout 读同 map、无 mutex;当前仅测试同 goroutine 调用,无生产调用者。正确契约是后台发事件、UI goroutine 应用,不得从任意 goroutine 调 `ApplyIcon`。
- **原 P1 "已直接冲突流畅滚动验收" overstate。** `IconCache.Load` 无生产调用者(仅测试),应定级为"正式图标接入前必须关闭",非"已破坏验收"。
### 已确认的事实(可作为后续任务前提)
| 项 | 结论 | 证据 |
| --- | --- | --- |
| 多标签筛选文档一致 | 属实(P3 撤销) | `routes.md:26` / `T-203.md:50` / `02-requirements.md:28` 三者一致,多标签复选明确后续接入 |
| 图标大小上限只界定 cache 入口 | 属实(NEW P1) | `validate` 在 `FetchIcon` 返回完整 `[]byte` 后才查 `len`;正式 HTTP Fetcher 须 Content-Length 拒绝 + `io.LimitReader(maxBytes+1)` |
| ApplyIcon/icons map 无锁 | 属实(NEW P2) | `shell.go:98` 写、`shell.go:514` 读,无 mutex;仅测试调用,无生产调用者 |
| IconCache 未接生产 | 属实 | `IconCache.Load` 仅测试调用,T-204 把网络装配留后续 |
| 内存缓存无界(双方原漏) | 属实(新增) | `IconCache.memory` 无 delete/LRU/上限;`SetItems` 重建 rows/categories 但**不剪枝** `shell.icons`,移除的 app 图标永久滞留 |
### 采纳 Codex 的修订与 sharpen
- **接受** 原 P3 撤销、NEW P1(远端有界读取)、NEW P2(UI 线程投递契约)。
- **接受** P1 落地方案:按 cache key 的 in-flight 去重(全局锁只护 memory/in-flight map,不同 key 磁盘/网络并发,同 key 单 leader),优于 naive check-unlock-fetch-lock-store(后者同图标会重复拉取)。
- **接受** P2 sharpen:适配器**交互契约测试**(验证 Gio 事件真更新 model、行点击开对 app ID、viewport 虚拟化、controls 按 app ID 保持),而非把共享 ViewModel 语义再断言一遍。
- **接受** P4 sharpen:`refilter` 构造新快照原子替换(筛选变化时分配),而非每帧返回拷贝。
- **接受** P6 收紧:fail-closed + 诊断为默认;自动 quarantine 需先做威胁模型裁定(reparse point / TOCTOU / 跨进程竞争),不默认更安全。
### 新增(双方原漏):无界内存缓存
- `IconCache.memory` 无淘汰;`shell.icons` 在 `SetItems` 不按当前 items 剪枝。数百款 × 每图标最大 2 MiB,内存只增不减(典型图标数十 MB,极端可更高)。
- 建议:内存层加容量/LRU 上限;`SetItems` 按当前 items 剪枝 `shell.icons`。与 P1 图标接入整改合并处理。
### 最终处理顺序(定稿)
1. **[正式图标接入前 · 最高]** `IconCache` 按 key in-flight 去重 + 网络/磁盘移出全局锁;HTTP Fetcher 读取阶段 Content-Length 拒绝 + `io.LimitReader` 硬上限;**内存层加容量/LRU 上限,`SetItems` 剪枝 `shell.icons`**;补并发/取消/有界读取/内存上限测试。
2. **[正式图标接入前 · 契约]** 后台结果经事件队列回 UI goroutine 后再 `ApplyIcon`/`Invalidate`,禁止后台直接改 shell map;补 `go test -race` 或等价单线程契约测试。
3. **[防漂移]** modern/win7 适配器交互契约测试,验证 Gio 事件接线/虚拟化/上下文保持,不重复证明共享 ViewModel。
4. **[API 健壮性]** `VisibleItems` 快照生命周期:refilter 原子替换新快照,避免每帧复制。
5. **[可维护性 · 低优先]** 增视图前按职责拆两个 `shell.go`。
6. **[观察项]** `ErrIconCacheUnsafe` 上层诊断/人工清理指引;自动 quarantine 另做威胁模型裁定。
> 裁定:本轮 Codex 复核成立且优于原审核——撤销原审核 stale 的 P3、纠正两处不精确表述、sharpen 三处落地方案。保留 Phase 2"可信基线"结论,T-201~T-204 不需回滚;新增"内存层无界"一项并入处理顺序第 1 步。
## 任务落地追踪
- `T-606` 已完成最终处理顺序第 1 项:按 key in-flight 去重、Fetcher 流式有界读取、32 MiB/256-key memory LRU 与 modern/win7 `shell.icons` 剪枝已实现并通过完整双 workspace 闸门。
- `T-607` 已完成最终处理顺序第 2 项:后台图标 Load/Decode 只发布强类型 application event,有界 FIFO relay 无损背压并请求重绘,由 Gio Frame/UI goroutine drain 后执行 `ApplyEvent`/`ApplyIcon`;最新请求、删除 app、IconRef/DPI 变化和取消都阻断迟到结果回写。core 与双 workspace 定向测试及完整闸门通过。
- 适配器交互契约、`VisibleItems` 快照、`shell.go` 拆分和 unsafe cache 诊断尚未编号;下一任务从双适配器交互契约开始,继续按顺序串行落成。