From ed9ded21109ec36373ed0f13fa42d1ebf55a5bbf Mon Sep 17 00:00:00 2001 From: ila Date: Fri, 17 Jul 2026 10:49:58 +0800 Subject: [PATCH] Define visible snapshot lifecycle task (T-609) --- docs/00-ai-start-here.md | 4 +- docs/06-tasks.md | 3 +- docs/current-state.md | 8 ++-- docs/review/phase2-review.md | 3 +- docs/tasks/T-609.md | 82 ++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 docs/tasks/T-609.md diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index 6c63c55..2a787fe 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -45,7 +45,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, ## 当前阶段 -当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-608`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线与双适配器交互契约均已关闭;下一步按 Phase 2 交叉审核顺序落成 `VisibleItems` 快照生命周期任务,其余审核整改与 Phase 1 中央目录预扫描继续串行处理,T-302 暂后置。 +当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-608`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线与双适配器交互契约均已关闭;T-609 已落成待领取,下一步修正 `VisibleItems` 快照生命周期,其余审核整改与 Phase 1 中央目录预扫描继续串行处理,T-302 暂后置。 优先路径: @@ -53,7 +53,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, 2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。 3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。 4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。 -5. 已完成 T-606/T-607/T-608:图标缓存资源边界、Load/Decode→application event→有界 relay/Invalidate→UI ApplyEvent 线程接线,以及双 Gio 适配器的 Editor/Clickable、AppID、viewport、详情上下文与控件生命周期契约;下一步落成 `VisibleItems` 快照任务,再串行处理其余整改与 T-302/T-303、Phase 4-6。 +5. 已完成 T-606/T-607/T-608:图标缓存资源边界、Load/Decode→application event→有界 relay/Invalidate→UI ApplyEvent 线程接线,以及双 Gio 适配器的 Editor/Clickable、AppID、viewport、详情上下文与控件生命周期契约;下一步实现 T-609 的 `VisibleItems` 快照代际修复,再串行处理其余整改与 T-302/T-303、Phase 4-6。 ## 领取任务规则 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 560d91f..6175dcf 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -59,13 +59,14 @@ Phase 1 安全整改按 `docs/review/phase1-security-review.md` 的交叉复核 #### Phase 2 交叉审核加固 -Phase 2 整改按 `docs/review/phase2-review.md` 的交叉复核定稿顺序串行落成。T-606 已关闭正式图标接入前的并发、读取和内存边界;T-607 建立后台图标结果经 application event 回到 Gio UI goroutine 的线程契约;T-608 为两个隔离 Gio 适配器建立交互契约。VisibleItems 快照与 shell 拆分在前一整改完成并提交后再正式编号。 +Phase 2 整改按 `docs/review/phase2-review.md` 的交叉复核定稿顺序串行落成。T-606 已关闭正式图标接入前的并发、读取和内存边界;T-607 建立后台图标结果经 application event 回到 Gio UI goroutine 的线程契约;T-608 为两个隔离 Gio 适配器建立交互契约;T-609 修正 `VisibleItems` 快照生命周期。shell 拆分在前一整改完成并提交后再正式编号。 | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | | T-606 | 收紧图标缓存并发与内存边界 | T-204, T-605 | 按 key in-flight 去重且不同 key 并行;Fetcher 流式有界读取;memory LRU 同时限制字节/条目;modern/win7 删除 app 时剪枝内存 ImageOp | | T-607 | 建立图标后台结果的 UI 线程事件投递 | T-606 | 后台 Load/Decode 只发布 application event;有界 relay 请求重绘;UI goroutine drain 后 ApplyIcon;过期结果不回写已删除或已换图标的 app | | T-608 | 建立双 Gio 适配器交互契约 | T-607 | 双端同场景验证 Editor/Clickable 接线、AppID 行身份、详情上下文、500 项 viewport 与控件释放;不重复 ViewModel 纯逻辑 | +| T-609 | 修正 VisibleItems 快照生命周期 | T-608 | refilter 构造新 backing array 后替换;旧快照跨 model 更新保持稳定;每帧读取不复制;明确单 owner 与只读约定 | ### Phase 3 · 下载与安装 diff --git a/docs/current-state.md b/docs/current-state.md index d4d8793..cdee4f1 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -13,7 +13,7 @@ ## 当前快照 - 日期:2026-07-17 -- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-608 已完成,T-302 继续暂后置 +- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-608 已完成,T-609 已落成待领取,T-302 继续暂后置 - 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析 - 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略、安全 ZIP 解压/回滚原型、无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存、图标 Load/Decode 事件发布用例、有界 application event relay,以及默认并发 2 的持久可恢复下载队列;modern/win7 主循环已接 relay/Invalidate,AppShell 已实现搜索/分类/视图、惰性列表、详情右栏、图标请求身份与 UI-only ApplyEvent/ApplyIcon - 测试:core 覆盖 Catalog、SemVer/12 状态、本地安装记录、Windows dot-space/设备名/Unicode 折叠路径攻击、ZIP destination 包含性、列表/图标并发/取消/读取边界/LRU、图标事件身份/失败分类/relay 背压与关闭、下载并发/暂停/取消/重试/Range/断连/恢复/事件失败与文件身份替换;两个 app 覆盖 Editor/视图/分类/行/恢复/关闭接线、500 项 viewport、AppID 控件与分类控件生命周期、详情上下文、空状态语义、UI drain 前后、最新/取消/换引用图标结果与平台 stub;安装恢复矩阵保持通过 @@ -21,14 +21,14 @@ - 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、执行完整 Phase 0 闸门、打印双目标构建命令) - 标准验证路径:`bash scripts/verify_phase0.sh` / `./scripts/verify_phase0.ps1` - 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交 -- 当前 blocker:无;下一步按 `docs/review/phase2-review.md` 最终顺序落成 `VisibleItems` 快照生命周期任务;Phase 1 中央目录预扫描等继续串行,T-302 继续后置 +- 当前 blocker:无;下一步领取 T-609,修正 `VisibleItems` 快照生命周期;Phase 1 中央目录预扫描等继续串行,T-302 继续后置 ## 当前目录要点 | 路径 | 状态 | 说明 | | --- | --- | --- | | `docs/` | 已有 | harness coding 文档集(本次初始化完成) | -| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-608 已完成;其余审核整改尚未编号,T-302 暂后置 | +| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-608 已完成;T-609 已落成待领取,其余审核整改尚未编号,T-302 暂后置 | | `scripts/` | 已有 | harness 治理、core 边界、Go 版本检查与 Phase 0 双平台验证入口 | | `core/` | 已建 | Go 1.20 兼容;已有正式 Catalog、本地状态/存储、共享 Windows safepath、列表模型、有界并发图标缓存、图标事件/relay、可恢复下载队列与 Phase 1 安装安全原型 | | `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;Modern AppShell 已接入虚拟列表、详情、图标事件 drain/过期拒绝和内存 ImageOp | @@ -42,7 +42,7 @@ - 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204`;Phase 3 的 `T-301`;审核整改 `T-604`~`T-608`。 - 正在进行:无。 -- 下一个可领取任务:无;先按 `docs/review/phase2-review.md` 最终顺序落成 `VisibleItems` 快照生命周期任务。 +- 下一个可领取任务:`T-609`(修正 `VisibleItems` 快照生命周期),依赖 `T-608` 已完成。 ## 当前可运行内容 diff --git a/docs/review/phase2-review.md b/docs/review/phase2-review.md index 25ae576..bd21283 100644 --- a/docs/review/phase2-review.md +++ b/docs/review/phase2-review.md @@ -286,4 +286,5 @@ modern/win7 的 `ApplyIcon` 都直接写 `shell.icons` map,Layout 同时读取 - `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 定向测试及完整闸门通过。 - `T-608` 已完成最终处理顺序第 3 项:modern/win7 使用除 edition/窗口尺寸外一致的场景矩阵,验证 Editor/Clickable 经 Layout 更新共享 model、重排后 AppID 行身份、详情关闭上下文、500 项 viewport、app/category controls 释放及两类空状态语义;双端定向重复测试与完整闸门通过,生产 `shell.go` 无需修正。 -- `VisibleItems` 快照、`shell.go` 拆分和 unsafe cache 诊断尚未编号;下一任务从 `VisibleItems` 快照生命周期开始,继续按顺序串行落成。 +- `T-609` 已按最终处理顺序第 4 项落成待领取:`refilter` 在 model 变化时构造新外层快照后替换,旧 generation 保持稳定,`VisibleItems()` 每帧读取不复制;不扩展为并发安全或防御性深拷贝。 +- `shell.go` 拆分和 unsafe cache 诊断尚未编号;必须等待 T-609 完成并提交后再按顺序落成。 diff --git a/docs/tasks/T-609.md b/docs/tasks/T-609.md new file mode 100644 index 0000000..fb251ab --- /dev/null +++ b/docs/tasks/T-609.md @@ -0,0 +1,82 @@ +--- +id: T-609 +title: 修正 VisibleItems 快照生命周期 +phase: 2 +deps: [T-608] +status: TODO +created: 2026-07-17 +issue: null +context_ref: null +claim_branch: null +work_branch: null +write_paths: + - docs/tasks/T-609.md + - core/application/ + - docs/routes.md + - docs/04-architecture.md + - docs/05-coding-rules.md + - docs/review/phase2-review.md + - docs/00-ai-start-here.md + - docs/06-tasks.md + - docs/current-state.md +--- + +## 问题 / 背景 + +`core/application.CatalogListModel.VisibleItems()` 当前直接返回 `model.visible`,`refilter()` 则从 `model.visible[:0]` 开始 append。Gio Layout 每帧只读当前切片时功能正常,但任何调用者只要跨一次 `SetQuery`、`SetCategory`、`SetView`、`ResetFilters` 或 `SetItems` 保留旧切片,旧切片的 backing array 就可能被新结果覆盖。导出的“immutable-by-convention snapshot”因此没有稳定生命周期,调用方也容易误以为旧快照可以安全保留到下一帧。 + +直接让 `VisibleItems()` 每次返回完整副本可以隐藏该问题,但 modern/win7 Layout 每帧都会调用它;对数百项列表逐帧复制会引入持续分配和复制成本,与虚拟列表性能目标冲突。Phase 2 审核定稿已选择在筛选状态实际变化时构造新 backing array、完成后替换当前快照,让读取继续保持 O(1)。 + +本任务只收紧共享 ViewModel 的快照代际语义:旧快照在后续 model 更新后内容保持不变,当前快照仍由调用方按只读约定使用。它不把 `CatalogListModel` 改成跨 goroutine 并发容器,也不为恶意调用方提供逐字段防御性复制。 + +## 方案 + +1. 修改 `core/application.CatalogListModel.refilter` 的快照构造方式: + - 先在局部新切片中按现有 category/view/query 规则构造完整结果,不得从 `model.visible[:0]` 复用上一代 backing array。 + - 构造完成后再以一次普通字段赋值替换 `model.visible`;“原子替换”描述的是单 owner goroutine 内不暴露半成品,不引入 `sync/atomic`、mutex 或并发读写承诺。 + - 保持现有筛选顺序、稳定项顺序、空结果语义和 `CatalogListItem` 值不变。 +2. 保持 `VisibleItems()` 为无复制读取: + - 不在每次调用时 clone 当前切片,不新增逐帧 O(n) 分配。 + - 更新注释明确返回值属于当前 snapshot generation,调用方可在后续 model 变更后继续读取旧 generation,但不得修改切片元素、嵌套 `Tags` 或容量范围。 + - 同一 generation 内重复调用应观察同一 backing array;只有实际执行 refilter 才发布新 generation。setter 因值未变化而提前返回时不强制创建无意义快照。 +3. 在 `core/application/catalog_list_test.go` 增加生命周期回归矩阵: + - 分别跨 `SetQuery`、`SetCategory`、`SetView`、`ResetFilters` 和 `SetItems` 保留旧快照,验证旧快照的长度、ID、字段及 tags 不被新结果覆盖。 + - 对非空结果验证 refilter 前后 backing array 不同;同一 generation 的重复 `VisibleItems()` 不复制且仍指向同一 backing array。 + - 覆盖空 Catalog、过滤为零项、恢复全部和 stable order,确保修复不改变现有 ViewModel 语义。 + - 测试只通过公开 model 操作触发代际变化,不直接调用 `refilter` 或依赖未导出容量细节。 +4. 保持两个 Gio 适配器调用方式不变,通过双 workspace 测试/构建证明 modern 与 Win7 每帧读取 `VisibleItems()` 不需要迁移到新 API。 +5. 同步架构、路由和编码规则,冻结“变更时分配、读取时只读 O(1)、UI owner goroutine”契约。 + +## 验收要点 + +- `refilter` 不再通过 `model.visible[:0]` 复用上一代 backing array;新结果完整构造后才替换当前快照。 +- 调用者在任一公开 refilter 入口前保存的非空旧快照,在后续 query/category/view/reset/items 变化后长度、顺序、字段和 tags 保持原值。 +- 非空新 generation 与仍被持有的旧 generation 不共享外层 backing array;同一 generation 内重复调用 `VisibleItems()` 共享当前只读快照且不逐次复制。 +- `VisibleItems()` 文档明确只读约定和生命周期,不声称线程安全;调用方修改返回切片或嵌套数据仍属于违反契约。 +- 现有搜索、分类、all/installed/updates、selection、空状态和 stable order 测试保持通过,不改变 UI 可见业务语义。 +- 不增加 `VisibleLen`/`VisibleItem` 第二套 API,不修改 modern/win7 shell 调用代码,不引入锁、`sync/atomic` 或新依赖。 +- Go 1.20.14 + `GOWORK=off` 下 `go vet ./application`、`go test -count=10 ./application` 通过。 +- modern Go 1.25.0 与 win7 Go 1.20.14 的 UI 测试和 Windows amd64 构建通过;`./scripts/verify_phase0.ps1` 全绿。 +- `python scripts/validate_agent_context.py`、`python scripts/validate_harness_governance.py` 与提交前差异检查通过。 + +## 边界(不改什么) + +- 不把 `CatalogListModel` 改成线程安全对象;所有 setter、selection 与 snapshot 发布继续由同一 application/UI owner goroutine 串行调用。 +- 不在 `VisibleItems()` 每次读取时复制,不深拷贝每个 item/tag,不承诺防御违反只读约定的调用方写入。 +- 不增加 `VisibleLen()`/`VisibleItem(index)`、iterator、泛型集合或其他并行列表 API,不修改公开业务字段和筛选规则。 +- 不修改 modern/win7 的 `shell.go`、Gio 依赖或 T-608 适配器契约;双端只作为不迁移调用方的回归验证。 +- 不拆分 `shell.go`,该维护项按审核顺序另立后续任务。 +- 不处理 `ErrIconCacheUnsafe` quarantine/诊断策略、ZIP 中央目录预扫描、断电耐久、Catalog 签名向量或 T-302 安装整合。 +- 不修改协议/Schema,不升级 Go/Gio,不合并 workspace,不修改、提交或删除用户的 `soft_quay.code-workspace`。 + +## 协作约束 + +- 按仓库当前规则由单 Agent 串行执行,不启动子 Agent。 +- 本任务只允许修改 frontmatter 中的 `write_paths`;若修复需要改变 Gio 调用方式、公共协议或线程模型,必须停止并记录,不得在 T-609 内扩大边界。 +- T-609 完成、完整验证并提交前,不落成或领取 Phase 2 审核顺序中的后续任务。 + +## 执行记录 + +- 2026-07-17:根据 `docs/review/phase2-review.md` 交叉复核定稿的第四优先级整改落成任务;现有全局最大任务为 T-608,因此取 T-609,依赖已完成的 T-608。 +- 2026-07-17:代码图确认 `VisibleItems` 有 core 测试与 modern/win7 Layout/适配器契约等 8 个调用点,当前为直接返回;五个公开 mutation 入口最终调用 `refilter`,而 `refilter` 明确以 `model.visible[:0]` 重用上一代 backing array。 +- 2026-07-17:任务采用审核定稿方案 1:状态变化时构造并替换新外层快照,每帧读取不复制;只读约定覆盖 slice 元素与嵌套 tags,但不把本任务扩展为并发安全或防御性深拷贝。