diff --git a/ROADMAP.md b/ROADMAP.md index 0e6bce0..d4a43c7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -303,6 +303,7 @@ ADR-147:Terminal正常实际提交“你”及wq前缀确认后的failure/INT/ - [x] `TBD-3` ADR-029:用户词库TSV v1 - [x] `TBD-4` 英文/包名最终形(ADR-056:Lampyris) +- [x] **更新器自更新验收(2026-11-05 闭环,报告 226 / ADR-306)**:1.0.0→1.0.1 演练暴露 `stopHost` 把正在自更新的进程 SIGTERM 掉;1.0.2 修复后真机复验:回执 `payloadSHA256=732756e5…`、更新程序 bundle 被替换且旧版备份、staging 清理、进程未自杀。 - [ ] IT-25:ADR-159五笔主动后项学习工程已通过隔离回归、make verify/strict universal/Swift FFI;待新签名原生验收与用户例子确认。见docs/tasks/T-ITER-25.md。 2026-10-14报告011:21个隔离默认配置FFI样例与Apple/Rime/Fcitx官方对照,新增待决IT-29混输提交歧义、IT-26兜底首屏、IT-27隔音符/读音质量与IT-28编辑/隐私补验;无产品代码修改。IT-25本地提交先保存,整体UI/公开推送继续暂缓。 diff --git "a/docs/MVP\345\220\216\346\211\247\350\241\214\350\256\241\345\210\222.md" "b/docs/MVP\345\220\216\346\211\247\350\241\214\350\256\241\345\210\222.md" index cf59dbd..909599d 100644 --- "a/docs/MVP\345\220\216\346\211\247\350\241\214\350\256\241\345\210\222.md" +++ "b/docs/MVP\345\220\216\346\211\247\350\241\214\350\256\241\345\210\222.md" @@ -19,6 +19,7 @@ | **B 基线采集** | 无发布 | 150–200 条个人高频样本集(单字/两字词/混排长句)+评测脚本;空闲功耗、冷加载基线 | **不需要换包**,可在 1.0.0 上直接做,且必须在改排序**之前**取得 | 无(可与 A 并行) | 脚本入库、基线 JSON 入库、能在任意包上一键复跑 | | **B2 设置一致性** | **1.0.1** | ADR-305:`input.full_width` Bool→枚举、⌥F 改为持久化(与 ⌥P/⌥T 对齐)、旧布尔配置迁移 | 单一行为面(设置项一致性),改动小、可直接出补丁;顺带成为 1.0.0→1.0.1 的升级练手,闭环更新器自更新 | A | 面板按 ⌥F 立即跟随、四项控件同构、迁移单测通过、升级路径 PASS | | **C 排序质量包** | **1.1.0** | ① D17 二级简码表 + 一级 25 条入表(ADR-304 P0)② `personalize` 改有界位次上移(P1)③ IT-25 原生验收 ④ IT-22 受影响行复跑 ⑤ **更新器自更新闭环**(1.0.0 → 1.1.0 由 build 34 更新程序执行) | 全部落在"候选顺序"这一个行为面上,一次换包、一次回归;③④⑤ 本来就要求"新签名包装机复验" | B 的基线与脚本;D17 数据许可登记 | 简码 25/625 校验通过、6 条新单测、样本集四项指标不劣化且关键项改善、两台设备同包、升级路径 PASS、IT-25/IT-22 受影响行 PASS | +| **B3 记账小修** | 随下一版 | 回执补 `updaterBundle`(报告 226 §6)、ADR-304 相关文档 | 已实现待发,随 1.1.0 一起出 | — | 回执字段可见、离线回归通过 | | **D 排序观察期** | 1.1.x 补丁 或 1.2.0 | ADR-304 P2:误选不学习、时间衰减、MRU;P3:位置记忆/「固定顺序」开关 | 只有 C 之后样本集仍显示明显非首选率才做;**一次一项**,便于归因 | C 的样本集对比数据 | 单变量对比显示改善,且无"越用越乱"回归 | | **E 拼音深化** | 独立立项 | 拼音后端质量评估 → 是否自研 FSA 的 ADR → 整句/长词(很可能不做) | 风险面与五笔排序无关,不能搭车 | 质量评估报告 | 有证据支持才立 ADR | | **F 平台扩展** | 独立项目 | R3 Windows(TSF spike 限 1–2 周,失败退 C++ 薄壳)→ R4 Linux → R5 移动端 | 各平台宿主互不影响,但会分散维护精力,**串行** | macOS 维护有余力 | 各自平台真实应用验收 | diff --git a/docs/reviews/226-self-update-kills-itself.md b/docs/reviews/226-self-update-kills-itself.md index 04ce8ae..9c81e0e 100644 --- a/docs/reviews/226-self-update-kills-itself.md +++ b/docs/reviews/226-self-update-kills-itself.md @@ -53,3 +53,31 @@ receiptsExists = true - `install-core.swift` 新增两条断言(生产停、自更新不停) - 真机复验:见本报告后续追加(将在 1.0.2 发布后执行:更新程序 ≥ 宿主版本的一次安装, 检查回执写入、staging 清理、更新程序被替换且进程不再自杀) + +## 5. 真机复验(2026-11-05,air,1.0.2 / build 37) + +做法:装 1.0.2(宿主与更新程序都是 37,含修复),用仓库里既有的**演练专用**比较基准 +`LAMPYRIS_UPDATE_CURRENT_BUILD=30` 启动更新程序,使它认为"有新版本 37",从而走完整安装 + 自更新分支 +(`WebsiteReleaseConfiguration.currentBuild` 注释即为此用途)。 + +在更新程序窗口里点「安装更新」后核对: + +| 项 | 结果 | +|---|---| +| 回执写入 | `updates/receipts/2026-11-05T10-11-57Z.json` ✅ | +| `payloadSHA256` | `732756e5…` = **已发布的 1.0.2 ZIP** ✅ | +| 宿主执行 | `b0b3bf5c…`(1.0.2/37)✅ | +| 旧宿主备份 | `upgrade-2026-11-05T10-11-50Z-…/backup/Lampyris.app` ✅ | +| **更新程序被替换** | `~/Applications/LampyrisUpdateCheck.app` mtime = 负载内同名 bundle 的 mtime(17:35:44),旧版在 `.lampyris-updater-backup-2026-11-05T10-11-54Z`(4.3 MB,build 37)✅ | +| **进程未被自己杀掉** | 安装持续到写完回执(10:11:54 替换 → 10:11:57 回执)✅ | +| staging 清理 | `upgrade-…/` 只剩 `backup/`;`~/Applications/.lampyris-install-*` 无残留 ✅ | +| 用户数据 | config `5d4271fb…`、词库 1124 行,与升级前一致 ✅ | + +**结论:ADR-306 的修复经真机验证通过;长期挂着的「更新器自更新」验收项闭环。** + +## 6. 同批发现的两处记账/门禁问题(已修或已排期) + +1. **回执漏记自更新结果**:`ReceiptPayload` 没有 `updaterBundle` 字段,导致本次只能靠 mtime 与备份目录反推"更新程序真被替换了"。 + 已在源码补上该字段并加断言(`install-core.swift` 7b 段),随下个版本发布。 +2. **发布工作流取产物失败**:`gh run list --commit X --status success` 对 1.0.2 的提交返回空,而两个条件单独都正常; + 改为只按提交过滤、在 jq 里判 `completed && success`(PR #220)。 diff --git a/platforms/macos/InstallCore/Sources/LampyrisInstallCore/UpdateInstaller.swift b/platforms/macos/InstallCore/Sources/LampyrisInstallCore/UpdateInstaller.swift index 3f3340e..803f6c2 100644 --- a/platforms/macos/InstallCore/Sources/LampyrisInstallCore/UpdateInstaller.swift +++ b/platforms/macos/InstallCore/Sources/LampyrisInstallCore/UpdateInstaller.swift @@ -307,12 +307,17 @@ public enum UpdateInstaller { let previous: String? let candidateExecutableSHA256: String let installedExecutableSHA256: String + /// 自更新把更新程序换到哪、旧版备份在哪;没做自更新时为 nil。 + /// + /// 之前漏了这一列,导致真机演练里"更新程序确实被替换了"只能靠 mtime 与备份目录反推(报告 226 §5)。 + let updaterBundle: String? init(receipt: InstallReceipt) { payloadSHA256 = receipt.payloadSHA256 signature = receipt.signature installed = receipt.installed previous = receipt.previous candidateExecutableSHA256 = receipt.candidateExecutableSHA256 + updaterBundle = receipt.updaterBundle installedExecutableSHA256 = receipt.installedExecutableSHA256 } } diff --git a/platforms/macos/tests/install-core.swift b/platforms/macos/tests/install-core.swift index c6a428d..25f1045 100644 --- a/platforms/macos/tests/install-core.swift +++ b/platforms/macos/tests/install-core.swift @@ -192,6 +192,22 @@ let signature = try! signingKey.signature(for: payloadBytes).base64EncodedString let publicKey = signingKey.publicKey.rawRepresentation.base64EncodedString() let receiptSupport = root.appendingPathComponent("support-with-file-receipts") +// 7b) 回执必须记录自更新结果(报告 226 §5:漏了这列,真机只能靠 mtime 反推) +let receiptOK = root.appendingPathComponent("receipt-ok") +try! FileManager.default.createDirectory(at: receiptOK, withIntermediateDirectories: true) +_ = try! UpdateInstaller.install( + payload: payload, signature: signature, publicKeyBase64: publicKey, + domain: receiptOK, support: root.appendingPathComponent("receipt-ok-support"), + environment: environment(expect: "payload-host", executable: "Lampyris"), + selfUpdate: UpdateInstaller.SelfUpdate( + directory: applications, + environment: environment(expect: "payload-updater", executable: "LampyrisUpdateCheck"))) +let receiptFiles = try! FileManager.default.contentsOfDirectory( + at: root.appendingPathComponent("receipt-ok-support/receipts"), includingPropertiesForKeys: nil) +require(receiptFiles.count == 1, "one receipt per install") +let receiptJSON = String(decoding: try! Data(contentsOf: receiptFiles[0]), as: UTF8.self) +require(receiptJSON.contains("\"updaterBundle\""), "receipt must record the self-updated updater path") + try! FileManager.default.createDirectory(at: receiptSupport, withIntermediateDirectories: true) try! Data().write(to: receiptSupport.appendingPathComponent("receipts")) // 目录位置被文件占用