From 16629b96aff55dd806f0c5662ce633a59d5a80d9 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Wed, 28 Oct 2026 02:55:51 +0800 Subject: [PATCH 1/6] docs: freeze 1.0.0 build 29 as the MVP candidate Records the identity of the locked candidate (commit, run, ZIP, host executable), the behaviour that ships for the first time, and the six screenshots that verify the new caret placement: below the line, mirrored at the right edge, flipped above near the bottom, with no overlap. Also scopes the formal IT-22 run honestly: only the rows this candidate actually changed have to be re-run, the rest cite the 0.1.16 rehearsal and say so. Signed-off-by: wangzhizhou <824219521@qq.com> --- docs/reviews/217-1.0.0-freeze.md | 59 ++++++++++++++++++++++++++++++++ docs/tasks/T-ITER-22.md | 2 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 docs/reviews/217-1.0.0-freeze.md diff --git a/docs/reviews/217-1.0.0-freeze.md b/docs/reviews/217-1.0.0-freeze.md new file mode 100644 index 00000000..317b4e09 --- /dev/null +++ b/docs/reviews/217-1.0.0-freeze.md @@ -0,0 +1,59 @@ +# 217 锁定候选 1.0.0 / build 29 + +> ADR-298 阶段 C 的起点:从这一刻起,**1.0.0 / build 29 就是唯一候选**, +> 门禁(IT-22 / IT-15 / IT-23)都必须在它上面做;任何换包都让 IT-23 当天作废重来。 + +## 1. 身份 + +| 项 | 值 | +|---|---| +| 版本 / 构建 | **1.0.0 / 29** | +| 源码提交 | `11abece3a348213429441950af667235486a482d`(PR #209 merge) | +| 候选构建 | `macOS Acceptance Package` run `40559560484`(success) | +| 候选 ZIP | `aca4ed5c8db8ed5320055a3f6bfd2f089efee7b8cd45e81ff49e779b5fc34f6e` | +| 宿主执行 SHA-256 | `4ad949e6c3608c689c07e8a3f092a53babc788b290e424b4784bebc1d3aae31c` | +| 签名 | Developer ID + 公证(`spctl` = Notarized Developer ID) | +| air 已装 | 宿主 1.0.0/29(执行 `4ad949e6…`),更新程序 1.0.0/29 | + +## 2. 这一版包含的、上一发布版没有的行为 + +- 更新链路:装包自带更新程序、更新器自更新与「重新打开更新程序」、窗口内卸载(ADR-294/295/296)、`--purge-data` 数据根修复、更新程序定位修复(报告213)。 +- 输入质量:拼音**多词分段拼接**(ADR-299)——`woaini→我爱你`、`zhongguoren→中国人`、`nihaoma→你好吗`、`jintiantianqihenhao→今天天气很好`。 +- 光标跟随:`CaretPlacement` 统一几何(ADR-301)——行框归一化、候选窗**先下后上**、右边缘**镜像**、三条不变量。 +- 可访问性:组合开始播报输入编码(ADR-300)。 +- 卸载语义:`keepData` 只保留 `config.toml` 与个人词库(ADR-297)。 + +## 3. 装机后的定向复验(build 29,用户执行) + +| 项 | 结果 | 证据 | +|---|---|---| +| 2.1 多词分段拼接 | PASS | 用户实测 `woaini` 等出正确整串 | +| 9.1 编码播报(VO) | PASS | 组合开始即念「编码 wq」 | +| 11.1 候选框跟随光标 | **PASS(6 张截图)** | `platforms/macos/build/acceptance-air-1.0.0-b29/shots/shot-1…6` | + +六张截图对应的位置与判定: + +| 截图 | 场景 | 观察 | 判定 | +|---|---|---|---| +| 1 | 首行、屏幕顶部 | 面板在行**下方**,有留白 | PASS | +| 2 | 超长行行末、贴右边缘 | 面板在**下方**且**镜像到插入点左侧**,不越界 | PASS | +| 3 | 接近屏幕底部 | 面板**翻到行上方** | PASS | +| 4 | 首行右侧 | 下方 + 镜像;间距偏紧但未压字 | PASS(间距待观察) | +| 5 | 中部行行末右侧 | 下方 + 镜像;间距偏紧但未压字 | PASS(间距待观察) | +| 6 | 中部行 | 面板在下方,留白正常 | PASS | + +## 4. 门禁分工 + +| 门禁 | 谁做 | 状态 | +|---|---|---| +| IT-22 正式走查 | 用户(代理整理清单/证据) | 待做(见 §5) | +| IT-15 第三方独立重建 | **用户本人**(ADR-125);本机重建不算独立结果 | 待做 | +| IT-23 五工作日试用 | 用户 | 待开始(第 1 天起算前先记一次 `walkthrough-evidence.sh`) | + +## 5. IT-22 正式走查范围(不必 60 项全重跑) + +build 29 相对 0.1.16(演练基线)改动的只有「候选几何 + 拼音拼接 + VO 播报 + 版本号」,所以正式走查按**受影响 + 抽查**执行,并在报告里如实写明范围,不假装全量重跑: + +- **必查(受影响)**:1.3/1.7(拼音候选)、4.1–4.6(候选/长词/多页/贴边)、9.1(VO 播报)、11.1(长文本跟随)。 +- **抽查(未受影响但属核心)**:1.1/1.2(五笔)、2.1–2.4(长组合与撤销)、3.1(TextEdit)、5.2(设置即时生效)、8.2(菜单开更新窗口)。 +- 其余项引用报告 216 在 0.1.16 上的结果,并注明基线与差异。 diff --git a/docs/tasks/T-ITER-22.md b/docs/tasks/T-ITER-22.md index 383bdae6..f3fbf532 100644 --- a/docs/tasks/T-ITER-22.md +++ b/docs/tasks/T-ITER-22.md @@ -155,7 +155,7 @@ tools/macos-probe/walkthrough-evidence.sh --label it22-end # 收尾 | # | 步骤 | 预期 | 结果 | 现象 / 证据 | |---|---|---|---|---| -| 11.1 | 正常打一段 50 字左右的中英混排 | 无卡顿、无候选闪烁 | FAIL| 无卡顿、无候选闪烁, 但是输入较长文本时,候选框无法准确跟随焦点。| +| 11.1 | 正常打一段 50 字左右的中英混排 | 无卡顿、无候选闪烁 | PASS| build29 六张截图:下方/镜像/底部翻上均正常(报告217 §3)| | 11.2 | 放置 10 秒后再输入 | 首键不卡 | PASS| | | 11.3 | 活动监视器看宿主内存 | 长时间使用后不明显增长(自动化已在报告 199/207 覆盖稳态) | PASS | | From 9b5151648276755c501434bf1ddb8f3b6788e39f Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Wed, 28 Oct 2026 03:04:38 +0800 Subject: [PATCH 2/6] docs: add the locked-candidate IT-22 rows and the IT-15 rebuild recipe The affected rows are the eleven the candidate actually changed, plus eight spot checks on core paths that did not change; everything else cites the 0.1.16 rehearsal with its baseline named. IT-15 can run before the release because the acceptance artifact ships its matching source tarball; the recipe records the two digests and the expected outcome for each comparison (data tables must match byte for byte, executable is expected to differ across SDK 27 vs 26.5). Signed-off-by: wangzhizhou <824219521@qq.com> --- docs/reviews/217-1.0.0-freeze.md | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/reviews/217-1.0.0-freeze.md b/docs/reviews/217-1.0.0-freeze.md index 317b4e09..4bb75ea3 100644 --- a/docs/reviews/217-1.0.0-freeze.md +++ b/docs/reviews/217-1.0.0-freeze.md @@ -57,3 +57,63 @@ build 29 相对 0.1.16(演练基线)改动的只有「候选几何 + 拼音 - **必查(受影响)**:1.3/1.7(拼音候选)、4.1–4.6(候选/长词/多页/贴边)、9.1(VO 播报)、11.1(长文本跟随)。 - **抽查(未受影响但属核心)**:1.1/1.2(五笔)、2.1–2.4(长组合与撤销)、3.1(TextEdit)、5.2(设置即时生效)、8.2(菜单开更新窗口)。 - 其余项引用报告 216 在 0.1.16 上的结果,并注明基线与差异。 + +## 6. IT-22 受查项清单(build 29,用户执行,约 15 分钟) + +**必查(本版改动直接影响)** + +| # | 操作 | 预期 | 结果 | +|---|---|---|---| +| 1.3 | 打 `nihao` | 候选含「你好」 | | +| 1.7 | `nihao` 后空格 | 首选上屏(不误上屏) | | +| 1.3+ | 打 `woaini`、`zhongguoren`、`nihaoma` | 分别出「我爱你」「中国人」「你好吗」(ADR-299) | | +| 4.1 | 亮色外观 ⇢ 输入 | 候选与状态芯片对比清晰、无白底白字 | | +| 4.2 | 暗色外观 ⇢ 输入 | 同上 | | +| 4.3 | 长候选(`kwwl`、`zhonghuarenmingongheguo`) | 不截断、序号与来源可辨 | | +| 4.4 | 鼠标悬停某个候选 | 悬停态与首选态可区分 | | +| 4.5 | 多页候选翻到第 2 页 | 页码/箭头正确、头部不跳 | | +| 4.6 | 插入点贴屏幕右/下边缘输入 | 面板**镜像/翻上**、不越界、不压字(ADR-301) | | +| 9.1 | 开 VoiceOver 打 `wq`,再按 ← | 第一声「编码 wq」;← 念「第N项,文字,来源」(ADR-300) | | +| 11.1 | 长文档行末输入 | 面板贴光标、留白 ≥8pt、不压当前行 | | + +**抽查(本版未改,属核心路径)** + +| # | 操作 | 预期 | 结果 | +|---|---|---|---| +| 1.1 | `wq` | 首选「你」 | | +| 1.2 | `kwwl` | 候选含「中华人民共和国」 | | +| 2.1 | 连续 8–10 个字母不选字 | 不丢字;能 Esc 取消(现在拼音会给拼接候选,五笔超过 4 码仍无候选) | | +| 2.3 | 组合中 Esc | 组合取消,文档不变 | | +| 2.4 | 上屏后 `⌘Z` | 撤销掉刚上屏的文本 | | +| 3.1 | TextEdit 空白文档输入 | 输入/翻页/撤销正常 | | +| 5.2 | 设置里改一项后立即输入 | 立即生效,无需重启宿主 | | +| 8.2 | 菜单「检查更新…」 | 直接开更新窗口,标题显示「0.1.16 · 构建 26」→ 现在应是 **1.0.0 · 构建 29** | | + +其余项引用报告 216 在 0.1.16 上的结果(同清单、同判定口径),本次不重复执行,并在结论里注明基线差异。 + +## 7. IT-15 独立重建(用户本人执行,约 30–40 分钟) + +候选已自带**对应源码包**,无需先发布: + +| 项 | 值 | +|---|---| +| 候选 ZIP | `aca4ed5c8db8ed5320055a3f6bfd2f089efee7b8cd45e81ff49e779b5fc34f6e` | +| 源码包 | `40d88f366f725eeb5ccd344dd3cc29ac544a8b5a005e2d1280c92ef12e27d696` | +| 位置 | `platforms/macos/build/acceptance-air-1.0.0-b29/candidate-mwFXhy/`(不入库) | + +一条命令跑完全部比对(校验和 → 离线重建 → 载荷/数据表比对): + +```sh +cd ~/Documents/EasyInputMethod +platforms/macos/tests/run-repro.sh \ + platforms/macos/build/acceptance-air-1.0.0-b29/candidate-mwFXhy/Lampyris-1.0.0-universal.zip \ + | tee ~/Desktop/it15-run-repro.log +``` + +判定口径(ADR-033,报告 208 §4 已实证): + +- **身份链**:`.sha256` 校验通过、`SOURCE-COMMIT` = `11abece3…`、`Cargo.toml` 1.0.0 / `build-number` 29 与 `BUILD-INFO.txt` 一致 → 必须一致。 +- **数据表**:`wubi86-core.tsv` / `pinyin-chars.tsv` / `pinyin-core.tsv` / `variant-s2t.tsv` → 必须 **4/4 逐字节一致**。 +- **可执行文件**:本机 SDK 27.0 / Swift 6.4 vs 构建机 SDK 26.5 / Swift 6.3.3 → `cmp` **预期不同**,记录 `otool -l` 的 `sdk`/链接器差异即可,不据此判失败。 +- **静态零外联**:`./scripts/verify-no-network.sh`(或 `make verify-no-network`)通过。 +- **运行时零外联**:一边打字一边 `lsof -i -a -p "$(pgrep -x Lampyris)"`,预期**无输出**。 From 1efbd04cb8250bbb5f3895a35e6a2d03aa258423 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Wed, 28 Oct 2026 03:25:02 +0800 Subject: [PATCH 3/6] docs: record the IT-22 result on the locked candidate All eleven affected rows and eight spot checks pass on 1.0.0 build 29; the remaining rows keep the 0.1.16 rehearsal result with its baseline named, so the gate is closed with an explicit scope rather than a claim that all sixty rows ran again. Signed-off-by: wangzhizhou <824219521@qq.com> --- ROADMAP.md | 1 + docs/macos-release-checklist.md | 2 +- docs/reviews/217-1.0.0-freeze.md | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index b47b506b..853298b3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -243,6 +243,7 @@ ADR-147:Terminal正常实际提交“你”及wq前缀确认后的failure/INT/ - [~] I2/IT-10/11、16…22:工程部分与机器可判部分已完成(报告197 对比度 26 图、T-ITER-16…21);**综合产品走查(IT-22)仍待 0.1.15 定稿包**。 - [x] 2026-10-27 IT-22 执行清单(`docs/tasks/T-ITER-22.md`):11 节 60 项,含开始前取证命令、`PASS/FAIL/BLOCKED/SKIP` 判定口径、UX-1 与搜狗日志噪音两条「已知接受项」,以及报告215 的 VoiceOver 反例条件(VO 开 → 宿主重启 → 立即打字)。**清单本身不是通过证据**,需在锁定候选上人工走一遍。 - [ ] **2026-10-27 MVP 正式发布(目标 `1.0.0`,ADR-298)**:阶段 A 在 0.1.16/26 上演练 IT-22(`docs/tasks/T-ITER-22.md`)→ 修复入库 → 阶段 B 递增 1.0.0(build 27)并双机装同一包(同时闭环「更新器自更新」)→ 阶段 C 只在 1.0.0 上做正式 IT-22、IT-15 独立重建、IT-23 五工作日 → 阶段 D `tag v1.0.0` 打在验收候选提交 → Release → Pages。 +- [x] 2026-10-28 **锁定候选 1.0.0 / build 29**(报告217):提交 `11abece3`、ZIP `aca4ed5c…`、宿主执行 `4ad949e6…`,air 已装同包。装机定向复验:2.1 多词拼接 PASS、9.1 编码播报 PASS、11.1 候选跟随(6 张截图:下方/镜像/底部翻上,无压字)PASS。**IT-22 限定范围通过**(必查 11 + 抽查 8 全 PASS,其余沿用报告216 基线)。 - [~] I3/IT-12/23:IT-12 性能/内存/久用已 PASS(报告191/192/199/207);**IT-23 五工作日试用尚未开始**(模板报告211)。 - [~] I4/IT-13…15:IT-13 附件绑定已在两次 Release 对账(报告194/208);**IT-14 最终候选锁定、IT-15 第三方核验待 0.1.15**。 diff --git a/docs/macos-release-checklist.md b/docs/macos-release-checklist.md index 5866245c..2dd15288 100644 --- a/docs/macos-release-checklist.md +++ b/docs/macos-release-checklist.md @@ -41,7 +41,7 @@ ADR-084:必须先满足 [MVP产品验收标准](MVP产品验收标准.md)。 | 门禁 | 缺什么 | 阻断/依赖 | |---|---|---| | **IT-14** 最终候选锁定 | **锁定 `1.0.0`**(ADR-298):0.1.16 上先演练 IT-22 → 修复入库 → 递增 1.0.0 → 双机装同一个包 → 只在它上面做 IT-22/IT-15/IT-23 | 演练与修复完成后再冻结 | -| **IT-22** 最终包产品走查 | 执行清单已就绪:`docs/tasks/T-ITER-22.md`(11 节 60 项 + 取证命令 + 判定口径 + VO 反例条件);**先在 0.1.16/26 演练,正式结果必须在 1.0.0 上重跑** | 演练可立即开始 | +| **IT-22** 最终包产品走查 | **已通过(限定范围,报告217 §6)**:锁定候选 1.0.0/build 29 上必查 11 项 + 抽查 8 项全 PASS;其余项沿用 0.1.16 演练结果(报告216)并注明基线,不宣称全量重跑 | — | | **IT-23** 五工作日试用 | 模板已备(报告211),一天未记;必须在 **1.0.0 候选**上连续 5 个工作日(换包当天作废) | 需先冻结 1.0.0 | | **IT-15 / P1-M5** 第三方独立重建 + 零外联 | 预演通过(报告208),正式结果必须用户本人执行;逐字节一致需与构建同 SDK 的机器 | 用户执行 | | **更新器自更新 / 真实跨版本升级** | 报告213 第 4 项 BLOCKED:`isolated-update-drill.yml` 只有 `build` 作业(ADR-288 所述 publish 不存在);旧版无内置更新程序,0.1.14 → 0.1.15 升级只能用旧脚本或外部更新程序 | 需授权补 publish 作业;自更新需再出一个更高构建号 | diff --git a/docs/reviews/217-1.0.0-freeze.md b/docs/reviews/217-1.0.0-freeze.md index 4bb75ea3..d3d581f1 100644 --- a/docs/reviews/217-1.0.0-freeze.md +++ b/docs/reviews/217-1.0.0-freeze.md @@ -89,8 +89,13 @@ build 29 相对 0.1.16(演练基线)改动的只有「候选几何 + 拼音 | 5.2 | 设置里改一项后立即输入 | 立即生效,无需重启宿主 | | | 8.2 | 菜单「检查更新…」 | 直接开更新窗口,标题显示「0.1.16 · 构建 26」→ 现在应是 **1.0.0 · 构建 29** | | +**执行结果(2026-10-28,用户操作,build 29):必查 11 项 + 抽查 8 项全部 PASS。** + 其余项引用报告 216 在 0.1.16 上的结果(同清单、同判定口径),本次不重复执行,并在结论里注明基线差异。 +因此 IT-22 门禁的结论是:**受影响项在锁定候选上通过,未受影响项沿用 0.1.16 演练结果并已注明基线**; +不宣称 60 项在 build 29 上全量重跑。 + ## 7. IT-15 独立重建(用户本人执行,约 30–40 分钟) 候选已自带**对应源码包**,无需先发布: From 085b0670936101e450e5810cac0cf33dacb6eba1 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Wed, 28 Oct 2026 03:32:34 +0800 Subject: [PATCH 4/6] fix: compare data tables before the executable in run-repro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user's IT-15 run stopped at "expected ... differ: char 22" because set -e aborted on the executable comparison, so the three data tables were never compared at all — and variant-s2t.tsv was missing from the list. Report 208 hit the same abort without noticing. Data tables now compare first (all four, hard failure on any mismatch), the executable comparison explains the SDK mismatch per ADR-033 and only fails when STRICT_BYTES=1, and both digests are printed. Records the IT-15 result (report 218): identity chain, offline rebuild, per-architecture import check, 4/4 identical data tables, zero egress at idle; executable differs because the build used SDK 26.5 and this machine has 27.0. Signed-off-by: wangzhizhou <824219521@qq.com> --- ROADMAP.md | 1 + docs/macos-release-checklist.md | 2 +- docs/reviews/218-it15-independent-rebuild.md | 57 ++++++++++++++++++++ platforms/macos/tests/run-repro.sh | 28 +++++++--- 4 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 docs/reviews/218-it15-independent-rebuild.md diff --git a/ROADMAP.md b/ROADMAP.md index 853298b3..d8ee2e51 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -244,6 +244,7 @@ ADR-147:Terminal正常实际提交“你”及wq前缀确认后的failure/INT/ - [x] 2026-10-27 IT-22 执行清单(`docs/tasks/T-ITER-22.md`):11 节 60 项,含开始前取证命令、`PASS/FAIL/BLOCKED/SKIP` 判定口径、UX-1 与搜狗日志噪音两条「已知接受项」,以及报告215 的 VoiceOver 反例条件(VO 开 → 宿主重启 → 立即打字)。**清单本身不是通过证据**,需在锁定候选上人工走一遍。 - [ ] **2026-10-27 MVP 正式发布(目标 `1.0.0`,ADR-298)**:阶段 A 在 0.1.16/26 上演练 IT-22(`docs/tasks/T-ITER-22.md`)→ 修复入库 → 阶段 B 递增 1.0.0(build 27)并双机装同一包(同时闭环「更新器自更新」)→ 阶段 C 只在 1.0.0 上做正式 IT-22、IT-15 独立重建、IT-23 五工作日 → 阶段 D `tag v1.0.0` 打在验收候选提交 → Release → Pages。 - [x] 2026-10-28 **锁定候选 1.0.0 / build 29**(报告217):提交 `11abece3`、ZIP `aca4ed5c…`、宿主执行 `4ad949e6…`,air 已装同包。装机定向复验:2.1 多词拼接 PASS、9.1 编码播报 PASS、11.1 候选跟随(6 张截图:下方/镜像/底部翻上,无压字)PASS。**IT-22 限定范围通过**(必查 11 + 抽查 8 全 PASS,其余沿用报告216 基线)。 +- [x] 2026-10-28 **IT-15 独立离线重建通过(报告218)**:用户在 air 上从候选自带源码包离线重建 1.0.0/build 29,身份链与逐架构网络导入 PASS、**默认数据 4/4 逐字节一致**、空闲 0 外联;可执行文件因本机 SDK 27.0 vs 发布 26.5 不同(ADR-033 边界)。顺带修掉 `run-repro.sh` 的缺陷:原来 `cmp` 可执行文件失败会中断,导致数据表比对根本没跑,且漏比 `variant-s2t.tsv`。 - [~] I3/IT-12/23:IT-12 性能/内存/久用已 PASS(报告191/192/199/207);**IT-23 五工作日试用尚未开始**(模板报告211)。 - [~] I4/IT-13…15:IT-13 附件绑定已在两次 Release 对账(报告194/208);**IT-14 最终候选锁定、IT-15 第三方核验待 0.1.15**。 diff --git a/docs/macos-release-checklist.md b/docs/macos-release-checklist.md index 2dd15288..57cccfad 100644 --- a/docs/macos-release-checklist.md +++ b/docs/macos-release-checklist.md @@ -43,7 +43,7 @@ ADR-084:必须先满足 [MVP产品验收标准](MVP产品验收标准.md)。 | **IT-14** 最终候选锁定 | **锁定 `1.0.0`**(ADR-298):0.1.16 上先演练 IT-22 → 修复入库 → 递增 1.0.0 → 双机装同一个包 → 只在它上面做 IT-22/IT-15/IT-23 | 演练与修复完成后再冻结 | | **IT-22** 最终包产品走查 | **已通过(限定范围,报告217 §6)**:锁定候选 1.0.0/build 29 上必查 11 项 + 抽查 8 项全 PASS;其余项沿用 0.1.16 演练结果(报告216)并注明基线,不宣称全量重跑 | — | | **IT-23** 五工作日试用 | 模板已备(报告211),一天未记;必须在 **1.0.0 候选**上连续 5 个工作日(换包当天作废) | 需先冻结 1.0.0 | -| **IT-15 / P1-M5** 第三方独立重建 + 零外联 | 预演通过(报告208),正式结果必须用户本人执行;逐字节一致需与构建同 SDK 的机器 | 用户执行 | +| **IT-15 / P1-M5** 第三方独立重建 + 零外联 | **已通过(报告218)**:用户本人在 air 上离线重建 1.0.0/build 29 —— 身份链 PASS、逐架构网络导入 PASS、**默认数据 4/4 逐字节一致**、空闲态 0 外联;可执行文件因 SDK 27.0 vs 26.5 不一致(ADR-033 边界,不判失败) | — | | **更新器自更新 / 真实跨版本升级** | 报告213 第 4 项 BLOCKED:`isolated-update-drill.yml` 只有 `build` 作业(ADR-288 所述 publish 不存在);旧版无内置更新程序,0.1.14 → 0.1.15 升级只能用旧脚本或外部更新程序 | 需授权补 publish 作业;自更新需再出一个更高构建号 | 门禁全绿后才进入 tag/Release 操作;二进制与对应源码须同页长期提供(ADR-064/065)。 diff --git a/docs/reviews/218-it15-independent-rebuild.md b/docs/reviews/218-it15-independent-rebuild.md new file mode 100644 index 00000000..b9005c46 --- /dev/null +++ b/docs/reviews/218-it15-independent-rebuild.md @@ -0,0 +1,57 @@ +# 218 IT-15 独立离线重建(1.0.0 / build 29) + +> ADR-125:独立核验必须由用户本人执行,代理在本机跑的重建不算独立结果。 +> 本次重建由用户在 air 上执行(2026-10-28 03:30),代理只负责核对日志、修工具缺陷与写报告。 + +## 1. 输入 + +| 项 | 值 | +|---|---| +| 版本 / 构建 | **1.0.0 / 29**(= 锁定候选,报告217) | +| 源码提交 | `11abece3a348213429441950af667235486a482d` | +| 候选 ZIP | `aca4ed5c8db8ed5320055a3f6bfd2f089efee7b8cd45e81ff49e779b5fc34f6e` | +| 源码包 | `40d88f366f725eeb5ccd344dd3cc29ac544a8b5a005e2d1280c92ef12e27d696` | +| 用户原始日志 | `~/Desktop/it15-run-repro.log`(23 行,2026-10-28 03:30) | +| 复跑日志(修好脚本后) | `platforms/macos/build/acceptance-air-1.0.0-b29/ev-90-it15-repro-fixed.log` | + +## 2. 结果 + +| 环节 | 结果 | 证据 | +|---|---|---| +| 源码包 `.sha256` 校验 | PASS | 原始日志 `Lampyris-1.0.0-source.tar.gz: OK` | +| 候选包内文件校验(`SHA256SUMS`) | PASS | 原始日志「候选文件校验通过」 | +| 离线重建(`CARGO_NET_OFFLINE=true`,`cargo build --locked --offline`) | PASS | 两架构 staticlib + Swift 宿主均构建成功 | +| 逐架构网络导入检查 | PASS | 日志「✅ 宿主逐架构网络导入检查通过」 | +| **默认数据逐字节一致** | **4/4 PASS** | 复跑日志「✅ 默认数据 4/4 逐字节一致」(`wubi86-core.tsv`、`pinyin-chars.tsv`、`pinyin-core.tsv`、`variant-s2t.tsv`) | +| 可执行文件逐字节一致 | **不一致(预期)** | 发布构建 SDK **26.5**,本机 SDK **27.0**;按 ADR-033/报告208 §4,跨 SDK/链接器不承诺载荷字节一致 | +| 运行时零外联 | PASS | `lsof -i -a -p <宿主>` 三次采样均 0 条连接(`ev-91-egress.log`) | + +去签名可执行文件摘要(仅供记录,不代表不一致原因之外的结论): + +| 产物 | SHA-256 | +|---|---| +| 候选(expected) | `df245ac7eb5965b06edd99ed867b950ff34a2f3b8577ff9b42bed90a6f818fdf` | +| 独立重建(rebuilt) | `5156bf5bd3824a035cabfbdd0e44c4ba897d044563ea81d562068358bc61cd24` | + +## 3. 过程中发现并修掉的工具缺陷 + +用户首次运行 `platforms/macos/tests/run-repro.sh` 时日志止于: + +``` +.../expected .../rebuilt differ: char 22, line 1 +``` + +原因:脚本在 `set -euo pipefail` 下**先** `cmp` 可执行文件,跨 SDK 必然失败 → 直接中断, +**后面 3 张数据表的比对从未执行**(报告 208 的预演也踩过同一处,只是当时没意识到)。 + +修法(同一批提交): + +1. 默认数据先比,且补齐为 **4 张表**(原来只比 3 张,漏了 `variant-s2t.tsv`);不一致直接失败。 +2. 可执行文件后比,不一致时打印双方 SDK 并说明 ADR-033 边界,默认**不判失败**;需要严格时用 `STRICT_BYTES=1`。 +3. 两个产物摘要都打印,便于写进报告。 + +## 4. 结论 + +- **IT-15 通过(按 ADR-033 的边界口径)**:身份链 + 离线重建 + 逐架构网络导入 + **默认数据 4/4 逐字节一致** + 运行时零外联。 +- **不宣称**「可执行文件逐字节重现」:跨 SDK 27.0 vs 26.5 不具备可比性;同工具链下复跑才适用(ADR-033)。 +- 遗留提示:IT-23 五工作日试用期间,把「键入期零外联」一并作为日常证据(本次只采样了空闲态)。 diff --git a/platforms/macos/tests/run-repro.sh b/platforms/macos/tests/run-repro.sh index 2190ce62..7b7fde12 100755 --- a/platforms/macos/tests/run-repro.sh +++ b/platforms/macos/tests/run-repro.sh @@ -39,13 +39,29 @@ PY (cd "${SOURCE}" && CARGO_NET_OFFLINE=true \ CLANG_MODULE_CACHE_PATH="${WORK}/module-cache" ./platforms/macos/build.sh --strict --universal) BUILT="${SOURCE}/platforms/macos/build/Lampyris.app" +# 1) 默认数据先比:这是「源码对应」的硬证据,不受工具链影响(ADR-033)。 +for name in wubi86-core.tsv pinyin-chars.tsv pinyin-core.tsv variant-s2t.tsv; do + cmp "${PACKAGE}/Lampyris.app/Contents/Resources/${name}" "${BUILT}/Contents/Resources/${name}" \ + || { echo "数据表不一致:${name}" >&2; exit 1; } +done +echo "✅ 默认数据 4/4 逐字节一致" + +# 2) 可执行文件:只有同工具链/SDK 才能逐字节一致(ADR-033、报告208 §4)。 cp "${PACKAGE}/Lampyris.app/Contents/MacOS/Lampyris" "${WORK}/expected" cp "${BUILT}/Contents/MacOS/Lampyris" "${WORK}/rebuilt" codesign --remove-signature "${WORK}/expected" codesign --remove-signature "${WORK}/rebuilt" -cmp "${WORK}/expected" "${WORK}/rebuilt" -for name in wubi86-core.tsv pinyin-chars.tsv pinyin-core.tsv; do - cmp "${PACKAGE}/Lampyris.app/Contents/Resources/${name}" "${BUILT}/Contents/Resources/${name}" -done -shasum -a 256 "${WORK}/rebuilt" -echo "离线源码重建成功;去签名可执行文件与默认数据逐字节一致。" +shasum -a 256 "${WORK}/expected" "${WORK}/rebuilt" +built_sdk="$(otool -l "${WORK}/expected" | awk '/ sdk /{print $2; exit}')" +local_sdk="$(xcrun --show-sdk-version 2>/dev/null || echo unknown)" +if cmp -s "${WORK}/expected" "${WORK}/rebuilt"; then + echo "✅ 去签名可执行文件逐字节一致(SDK ${built_sdk} = 本机 ${local_sdk})" +else + echo "ℹ️ 去签名可执行文件不同:发布构建 SDK ${built_sdk},本机 SDK ${local_sdk}。" + echo " 按 ADR-033,跨 SDK/链接器不承诺载荷逐字节一致;默认数据一致即视为源码对应。" + if [ "${STRICT_BYTES:-0}" = "1" ]; then + echo "STRICT_BYTES=1:要求逐字节一致,判定失败。" >&2 + exit 1 + fi +fi +echo "离线源码重建成功。" From 4fbb5416c5ce96e3488ce2358f869edf91370b67 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Wed, 28 Oct 2026 03:36:14 +0800 Subject: [PATCH 5/6] docs: install 1.0.0 build 29 on both machines and open the IT-23 baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the trial baseline: air and pro now run the same binary (4ad949e6…), pro's config and 593-line dictionary are unchanged and its previous bundle is backed up under uninstalled/bundle.aPZKev. The installer refused the first attempt because IMK relaunched the host within three seconds of the stop; stopping, confirming the process is gone, then installing worked. Notes that as correct protection rather than a defect. Signed-off-by: wangzhizhou <824219521@qq.com> --- ROADMAP.md | 1 + docs/reviews/211-it23-five-day-template.md | 12 ++++++------ docs/reviews/217-1.0.0-freeze.md | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d8ee2e51..01d85bd1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -245,6 +245,7 @@ ADR-147:Terminal正常实际提交“你”及wq前缀确认后的failure/INT/ - [ ] **2026-10-27 MVP 正式发布(目标 `1.0.0`,ADR-298)**:阶段 A 在 0.1.16/26 上演练 IT-22(`docs/tasks/T-ITER-22.md`)→ 修复入库 → 阶段 B 递增 1.0.0(build 27)并双机装同一包(同时闭环「更新器自更新」)→ 阶段 C 只在 1.0.0 上做正式 IT-22、IT-15 独立重建、IT-23 五工作日 → 阶段 D `tag v1.0.0` 打在验收候选提交 → Release → Pages。 - [x] 2026-10-28 **锁定候选 1.0.0 / build 29**(报告217):提交 `11abece3`、ZIP `aca4ed5c…`、宿主执行 `4ad949e6…`,air 已装同包。装机定向复验:2.1 多词拼接 PASS、9.1 编码播报 PASS、11.1 候选跟随(6 张截图:下方/镜像/底部翻上,无压字)PASS。**IT-22 限定范围通过**(必查 11 + 抽查 8 全 PASS,其余沿用报告216 基线)。 - [x] 2026-10-28 **IT-15 独立离线重建通过(报告218)**:用户在 air 上从候选自带源码包离线重建 1.0.0/build 29,身份链与逐架构网络导入 PASS、**默认数据 4/4 逐字节一致**、空闲 0 外联;可执行文件因本机 SDK 27.0 vs 发布 26.5 不同(ADR-033 边界)。顺带修掉 `run-repro.sh` 的缺陷:原来 `cmp` 可执行文件失败会中断,导致数据表比对根本没跑,且漏比 `variant-s2t.tsv`。 +- [ ] 2026-10-28 IT-23 五工作日试用(**1.0.0 / build 29**,air 与 pro 执行 SHA 均为 `4ad949e6…`):两台设备已装同一包(报告217 §8),pro 装前数据与备份已核对;从下一个工作日开始逐日记录,换包即作废。 - [~] I3/IT-12/23:IT-12 性能/内存/久用已 PASS(报告191/192/199/207);**IT-23 五工作日试用尚未开始**(模板报告211)。 - [~] I4/IT-13…15:IT-13 附件绑定已在两次 Release 对账(报告194/208);**IT-14 最终候选锁定、IT-15 第三方核验待 0.1.15**。 diff --git a/docs/reviews/211-it23-five-day-template.md b/docs/reviews/211-it23-five-day-template.md index fa29f285..d5d97d47 100644 --- a/docs/reviews/211-it23-five-day-template.md +++ b/docs/reviews/211-it23-five-day-template.md @@ -1,6 +1,6 @@ # 211 IT-23 五工作日试用记录(v0.1.14) -> 用途:给**用户本人**(ADR-125:一位试用者即满足人数)在 `v0.1.14` 上做累计 5 个工作日的日常试用,逐日留证。 +> 用途:给**用户本人**(ADR-125:一位试用者即满足人数)在 **1.0.0 / build 29**(锁定候选,报告217)上做累计 5 个工作日的日常试用,逐日留证。 > 这不是工程自测,代理签字的记录不算;本文件由用户填写,代理只负责整理与最后补进 schema 2 报告。 > 门禁要求见 `docs/MVP产品验收标准.md` 与 `docs/MVP单人验收清单.md` §「五工作日观察记录」。 @@ -8,12 +8,12 @@ | 项 | 值 | |---|---| -| 版本 / 构建 | **0.1.14 / build 23** | -| 源码提交 | `0b2aa244f536fe9db9f4c76221a4d294fbe0e627`(tag `v0.1.14`) | -| 执行文件 SHA-256 | `a165f05c0b704796380a39f5666da5e2cfa1a8cb2d23868dfd3d1532d429e4dc` | -| 发布资产 | ZIP `7741cdde…`、DMG `c247f626…`、源码 `964f32f1…` | +| 版本 / 构建 | **1.0.0 / build 29** | +| 源码提交 | `11abece3a348213429441950af667235486a482d`(锁定候选,报告217) | +| 执行文件 SHA-256 | `4ad949e6c3608c689c07e8a3f092a53babc788b290e424b4784bebc1d3aae31c` | +| 候选资产 | ZIP `aca4ed5c…`(= 候选,逐字节)、源码 `40d88f36…`;Release 资产待发布后对账 | | 签名 | Developer ID + 公证(`spctl` = `accepted source=Notarized Developer ID`) | -| 设备 | pro(Intel / macOS 13.7.8,执行 `a165f05c…`)、air(arm64 / macOS 27.0.1,执行 `a165f05c…`) | +| 设备 | air(arm64 / macOS 27.0.1)与 pro(Intel / macOS 13.7.8)**执行 SHA 相同**,均为 `4ad949e6…` | > 每天开始前花 5 秒确认:`shasum -a 256 "$HOME/Library/Input Methods/Lampyris.app/Contents/MacOS/Lampyris"` 应为上面那个 SHA。 > 如果哪天换了包(哪怕只是 build 变了),**当天记录作废重来**,否则 5 天不在同一个候选上。 diff --git a/docs/reviews/217-1.0.0-freeze.md b/docs/reviews/217-1.0.0-freeze.md index d3d581f1..714ca655 100644 --- a/docs/reviews/217-1.0.0-freeze.md +++ b/docs/reviews/217-1.0.0-freeze.md @@ -122,3 +122,21 @@ platforms/macos/tests/run-repro.sh \ - **可执行文件**:本机 SDK 27.0 / Swift 6.4 vs 构建机 SDK 26.5 / Swift 6.3.3 → `cmp` **预期不同**,记录 `otool -l` 的 `sdk`/链接器差异即可,不据此判失败。 - **静态零外联**:`./scripts/verify-no-network.sh`(或 `make verify-no-network`)通过。 - **运行时零外联**:一边打字一边 `lsof -i -a -p "$(pgrep -x Lampyris)"`,预期**无输出**。 + +## 8. IT-23 试用基线:两台设备装同一个包(2026-10-28) + +| 项 | air(本机,arm64 / macOS 27.0.1) | pro(x86_64 / macOS 13.7.8) | +|---|---|---| +| 版本 / 构建 | **1.0.0 / 29** | **1.0.0 / 29** | +| 宿主执行 SHA-256 | `4ad949e6c3608c689c07e8a3f092a53babc788b290e424b4784bebc1d3aae31c` | **同值**(同一个二进制) | +| 更新程序构建号 | 29 | 29 | +| 签名 / 公证 | Developer ID + `spctl` Notarized | 同(`codesign --strict`、`stapler validate` 均 PASS) | +| 用户数据 | config `cfc86cff…`、词库 800+ 行 | config `2bd3fde6…`、词库 **593 行**(装前装后一致) | +| 旧版本备份 | `updates/`、`uninstalled/` 历史项 | `uninstalled/bundle.aPZKev`(0.1.16) | +| 输入源 | `cn.jokerhub.inputmethod.Lampyris.default` 已启用 | 已启用;当前停在 ABC,使用时按 ⌃Space 切 | + +pro 的安装记录:`ev-95-pro-stage.log`(ZIP 校验 → 解包 → 停宿主 → `scripts/install.sh --app` → 回读)。 +第一次尝试被安装器的「宿主仍在运行」安全检查挡下(IMK 在 `pkill` 后 3 秒内重新拉起宿主), +改为「停宿主 → 确认 `pgrep -x Lampyris` 为空 → 立即安装」后成功;这是安装器**正确的**保护行为,不是缺陷。 + +**IT-23 从下一个工作日开始**:两台设备都固定在 1.0.0/29,期间任何换包都会让当天记录作废。 From d07a8dc58e4d5ae13d0e78e4ff28fd4cfb14732a Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Thu, 29 Oct 2026 19:27:18 +0800 Subject: [PATCH 6/6] fix: keep the candidate window on the focused caret, not the mouse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing into an empty field placed the panel at the mouse. caretRect passed allowsEmptyCaret: false to the resolver, whose first line returns nil when length() is zero, so the controller fell straight through to its last resort — the mouse position. The HUD path has always passed true, which is why only the candidate window drifted. The resolver now gets allowsEmptyCaret: true, and the fallback chain is explicit and testable: this round's caret, then the last caret that did resolve, and only then the mouse. Each fallback logs its reason under LAMPYRIS_HUD_DIAG. Build 30. Signed-off-by: wangzhizhou <824219521@qq.com> --- docs/reviews/219-caret-follows-focus.md | 42 +++++++++++++++++++ packaging/build-number | 2 +- platforms/macos/Sources/CaretPlacement.swift | 17 ++++++++ platforms/macos/Sources/InputController.swift | 26 +++++++----- platforms/macos/tests/caret-placement.swift | 15 ++++++- 5 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 docs/reviews/219-caret-follows-focus.md diff --git a/docs/reviews/219-caret-follows-focus.md b/docs/reviews/219-caret-follows-focus.md new file mode 100644 index 00000000..ecacc33e --- /dev/null +++ b/docs/reviews/219-caret-follows-focus.md @@ -0,0 +1,42 @@ +# 219 候选窗跟着鼠标而不是输入焦点(build 29 → 30) + +> 用户复验 1.0.0/build 29 时报告:**输入框已经获得焦点,但鼠标离得很远时,候选窗出现在鼠标位置**。 +> 这是报告 217 §3 定向复验没有覆盖到的场景(那六张截图鼠标都在输入点附近)。 + +## 1. 根因 + +`InputController.caretRect()` 调 `StateHUDCaretResolver.resolve(...)` 时传的是: + +```swift +startIndex: start, hasText: length > 0, allowsEmptyCaret: false +``` + +而解析器的第一行是 `guard hasText || allowsEmptyCaret else { return nil }`。 +于是只要 **`length() == 0`**(空输入框,或客户端的 `length()` 不把预编辑文本算进去), +解析器立刻返回 `nil`,`caretRect` 直接落到最后一档回退:**鼠标位置**(1×1 点)。 + +HUD 那条路径一直传的是 `allowsEmptyCaret: true`,所以同样的场景下 HUD 位置是对的、只有候选窗跑偏。 +这个差异是 ADR-301 重构前就存在的(旧代码只调一次 `attributes` 也有同类问题),重构时把 +`allowsEmptyCaret: false` 原样带了过来。 + +## 2. 修法 + +1. **允许空插入点**:候选窗与 HUD 一样传 `allowsEmptyCaret: true`——空输入框的插入点仍然有效。 +2. **回退链收敛**:新增 `CaretPlacement.fallback(current:previous:mouse:)`,顺序为 + **本轮解析结果 → 上一次成功的位置 → 鼠标**。鼠标几乎从不是用户想要的位置,只在「本轮失败且 + 没有任何历史位置」时使用;每条回退都写 `LAMPYRIS_HUD_DIAG` 原因(`fallback=previous/mouse`)。 +3. `lastCaretRect` 只在解析成功时更新,避免把回退值当成历史。 + +## 3. 验证 + +| 层 | 内容 | +|---|---| +| 单元 | `caret-placement.swift` 新增回退链三例:本轮结果优先 / 无本轮时用上次位置 / 两者都没有才用鼠标 | +| 解析器 | 既有断言已覆盖「空文档 + `allowsEmptyCaret: true` 取到行框」,本次让候选窗走同一条 | +| 回归 | `make verify`、`make macos-offline-tests`(56 文件 / 303 例)全绿 | +| 实机 | 待 build 30 装机复验:**聚焦输入框、把鼠标移到屏幕另一端、再打字**,候选窗应贴着插入点 | + +## 4. 影响 + +- 版本仍 1.0.0,构建号 29 → **30**;锁定候选随之更新(报告 217 §8 的身份表在复验后补记)。 +- IT-23 尚未开始记第 1 天,因此**不影响**试用基线;两台设备都升级到 30 后再开始计时。 diff --git a/packaging/build-number b/packaging/build-number index f04c001f..64bb6b74 100644 --- a/packaging/build-number +++ b/packaging/build-number @@ -1 +1 @@ -29 +30 diff --git a/platforms/macos/Sources/CaretPlacement.swift b/platforms/macos/Sources/CaretPlacement.swift index 83cb5c19..7ca66b48 100644 --- a/platforms/macos/Sources/CaretPlacement.swift +++ b/platforms/macos/Sources/CaretPlacement.swift @@ -100,4 +100,21 @@ enum CaretPlacement { let spaceAbove = visible.maxY - line.maxY return spaceBelow >= spaceAbove ? below : above } + + /// 取不到光标时的回退顺序(报告219):上一次成功的位置 → 鼠标。 + /// + /// 鼠标几乎从来不是用户想要的位置:输入框有焦点、鼠标在别处时,跟着鼠标会把候选窗 + /// 放到屏幕上完全无关的角落。因此只在「本轮解析失败且没有任何历史位置」时才用鼠标。 + enum Fallback: String, Equatable { + case resolved + case previous + case mouse + } + + static func fallback(current: NSRect?, previous: NSRect?, + mouse: NSPoint) -> (rect: NSRect, source: Fallback) { + if let current { return (current, .resolved) } + if let previous { return (previous, .previous) } + return (NSRect(x: mouse.x, y: mouse.y, width: 1, height: 1), .mouse) + } } diff --git a/platforms/macos/Sources/InputController.swift b/platforms/macos/Sources/InputController.swift index e78d632c..9eee0b54 100644 --- a/platforms/macos/Sources/InputController.swift +++ b/platforms/macos/Sources/InputController.swift @@ -719,6 +719,9 @@ final class LampyrisInputController: IMKInputController { return min(max(0, location), length - 1) } + /// 上一次成功解析到的插入点矩形:解析失败时优先复用它,而不是跳到鼠标(报告219)。 + private var lastCaretRect: NSRect? + /// 光标矩形(候选窗/HUD 定位)。 /// /// 走与 HUD 同一套两趟解析(报告094/095):`attributes` 在文末与长文本里常返回零矩形, @@ -738,18 +741,19 @@ final class LampyrisInputController: IMKInputController { actualRange: nil) } let screens = NSScreen.screens.map(\.frame) + // `allowsEmptyCaret: true`:空输入框(或 length 不含预编辑文本的客户端)里 + // 插入点仍然有效,退成 false 会让解析器直接返回 nil、候选窗跟着鼠标跑(报告219)。 let resolved = StateHUDCaretResolver.resolve( - startIndex: start, hasText: length > 0, allowsEmptyCaret: false, + startIndex: start, hasText: length > 0, allowsEmptyCaret: true, lineRect: lineRect, firstRect: firstRect) - if let resolved, StateHUDLayout.isOnScreen(caret: resolved, screens: screens) { - return resolved - } - let mouse = NSEvent.mouseLocation - if Self.hudDiagEnabled { - let reason = resolved == nil ? "两趟解析都失败" : "解析结果不在任何屏幕内" - Self.hudDiag("caret fallback to mouse: \(reason) selection=\(selection.location)/" - + "\(length) rect=\(String(describing: resolved))") - } - return NSRect(x: mouse.x, y: mouse.y, width: 1, height: 1) + let valid = resolved.flatMap { StateHUDLayout.isOnScreen(caret: $0, screens: screens) ? $0 : nil } + let choice = CaretPlacement.fallback(current: valid, previous: lastCaretRect, + mouse: NSEvent.mouseLocation) + if choice.source == .resolved { lastCaretRect = choice.rect } + if Self.hudDiagEnabled, choice.source != .resolved { + Self.hudDiag("caret fallback=\(choice.source.rawValue) selection=\(selection.location)/" + + "\(length) resolved=\(String(describing: resolved))") + } + return choice.rect } } diff --git a/platforms/macos/tests/caret-placement.swift b/platforms/macos/tests/caret-placement.swift index 43d6b591..43bb45d8 100644 --- a/platforms/macos/tests/caret-placement.swift +++ b/platforms/macos/tests/caret-placement.swift @@ -81,4 +81,17 @@ let onSecondary = CaretPlacement.frame(request(NSRect(x: 1500, y: 400, width: 1, require(onSecondary.minX >= secondary.minX && onSecondary.maxX <= secondary.maxX, "uses the visible frame of the target screen: \(onSecondary)") -print("PASS caret-placement: below-first, flip, glyph normalization, right-edge mirror, HUD centered-above, clamping, secondary screen") +// 9) 回退链(报告219):本轮解析 → 上次位置 → 鼠标,鼠标只在最后一档。 +let caretLine = NSRect(x: 100, y: 420, width: 2, height: 16) +let previousLine = NSRect(x: 300, y: 300, width: 2, height: 16) +let mouse = NSPoint(x: 900, y: 90) +let first = CaretPlacement.fallback(current: caretLine, previous: previousLine, mouse: mouse) +require(first.source == .resolved && first.rect == caretLine, "resolved caret wins: \(first)") +let second = CaretPlacement.fallback(current: nil, previous: previousLine, mouse: mouse) +require(second.source == .previous && second.rect == previousLine, + "falls back to the last known caret, not the mouse: \(second)") +let third = CaretPlacement.fallback(current: nil, previous: nil, mouse: mouse) +require(third.source == .mouse && third.rect.minX == mouse.x && third.rect.minY == mouse.y, + "uses the mouse only when nothing else is known: \(third)") + +print("PASS caret-placement: below-first, flip, glyph normalization, right-edge mirror, HUD centered-above, clamping, secondary screen, fallback order")