diff --git a/Cargo.lock b/Cargo.lock index 968bde2f..0b25dad0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "ime-cli" -version = "1.2.0" +version = "1.2.1" dependencies = [ "ime-config", "ime-core", @@ -35,7 +35,7 @@ dependencies = [ [[package]] name = "ime-config" -version = "1.2.0" +version = "1.2.1" dependencies = [ "arc-swap", "toml_edit", @@ -43,15 +43,15 @@ dependencies = [ [[package]] name = "ime-core" -version = "1.2.0" +version = "1.2.1" [[package]] name = "ime-dict" -version = "1.2.0" +version = "1.2.1" [[package]] name = "ime-ffi" -version = "1.2.0" +version = "1.2.1" dependencies = [ "ime-config", "ime-core", @@ -62,14 +62,14 @@ dependencies = [ [[package]] name = "ime-pinyin" -version = "1.2.0" +version = "1.2.1" dependencies = [ "ime-core", ] [[package]] name = "ime-table" -version = "1.2.0" +version = "1.2.1" dependencies = [ "ime-core", ] @@ -131,4 +131,4 @@ dependencies = [ [[package]] name = "wubi-encoder" -version = "1.2.0" +version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index c391d7d5..170b24cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package] -version = "1.2.0" +version = "1.2.1" edition = "2021" license = "GPL-3.0-or-later" rust-version = "1.98" diff --git a/HANDOFF.md b/HANDOFF.md index 419dd4fe..5952c8c7 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -119,7 +119,7 @@ make macos-offline-tests - 2026-11-06 / pi(air):**B3 学习区实现(报告 253)**。`WubiLearningView` 改两行布局(完整码+简码 / 字根格+识别码两行),候选窗零改动;新增 `LearningHints`(读 `B0-runtime.tsv`,缺失退回 D14);词组按输入码位置映射到字(KWWL → 中K 华W 人W 国L)。`wubi-learning.swift` 扩到可注入资源并生成亮/暗实拍;离线回归全绿。 - 2026-11-06 / pi(air):**B2 数据地基(报告 252 / ADR-311)**。B0 扩到 3,000 字并预渲染成 `B0-runtime.tsv`(287,310 B ≈ 96 B/字,预算 512 KiB 反而有余量;B0.json 同规模是 1.62 MiB),登记 derived `b0-runtime`,新增 `test-b0-runtime.py` 守体积/格式/无 PUA/消费契约。ADR-311 定装载方式(bundle 内只读文件、宿主查表、Rust 不参与)。 - 2026-11-06 / pi(air):**1.2.0 B1 批完成(报告 251)**。D1 签名 tag 改 SSH key(白名单 `packaging/git-allowed-signers`,本地已验签);D2 CI 取 D2 拆字数据 + D17 简码表入库,B0 独立重算不再 skip;D5 perf/soak 绝对硬顶改 vmmap footprint;D6 站点改占位符 + `tools/stamp-site.py` + Pages 盖章;D4 延迟统计加 long-code/paging 两段;E2 soak 30 分钟档。未换包、未改输入行为;离线 61 文件 / 323 例 PASS。 -- 2026-11-06 / pi(air):**1.2.0 迭代计划(`docs/迭代计划-1.2.0.md`)**。按"除平台扩张外全部收口"排成 6 批:B1 工程收口 → B2 数据地基 → B3 L3 提示 → B4 L4 练习 → B5 质量项(有证据才做)→ B6 数据扩张;只换一次包。原则:低风险先行、依赖链不打断、无证据的项明确关闭并写触发条件。可立即开始 B1 与 C1(互不阻塞)。 +- 2026-11-06 / pi(air):**1.2.0 迭代计划(`docs/archive/迭代计划-1.2.0.md`)**。按"除平台扩张外全部收口"排成 6 批:B1 工程收口 → B2 数据地基 → B3 L3 提示 → B4 L4 练习 → B5 质量项(有证据才做)→ B6 数据扩张;只换一次包。原则:低风险先行、依赖链不打断、无证据的项明确关闭并写触发条件。可立即开始 B1 与 C1(互不阻塞)。 - 2026-11-06 / pi(air):**下一版积压梳理(`docs/待办与积压.md`)**。把 ROADMAP 未勾项、ADR 待定项、报告遗留与已知限制收成 A–G 七类,每项给证据/成本/依赖/授权;给出 1.2.0 三套套餐(A 工程收口+教学地基 / B 质量与门禁最小版 / C 平台扩张),推荐 A。需要用户拍三件事:套餐、签名 tag 是否启用、Windows spike 是否启动。 - 2026-11-06 / pi(air):**1.1.0 闭环 + air 输入源故障收尾**。用户在 pro 确认候选/切源正常;air 报「萤火虫不能用」经查是**被移出已启用输入源**(包、宿主、签名均健康),用户重新添加后恢复。把排查命令与「TIS 后台启用不持久化」写进 `docs/故障恢复.md` §4.1,HANDOFF 同步。 - 2026-11-06 / pi(air):**1.1.0 发布收口**。HANDOFF 发布列车改为「1.1.0 已发布 → 下一站 1.2.0」;ROADMAP 三条发布任务全部勾掉(自用豁免、tag/Release、站点、pro 升级),新增「1.2.0 候选」与「pro 本机实键入确认」。当前:main `730d894`、最新 tag `v1.1.0`、站点 `v1.1.0 · 构建 38`、air/pro 同包。 diff --git a/ROADMAP.md b/ROADMAP.md index ddf34a2e..09ee3f69 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5,7 +5,7 @@ > 2026-11-06 对账过一轮:把已完成但未勾的行补齐(IT-15 / IT-22 / IT-23 / IT-25 / UI04-05 等), > 仍未勾的只剩「确实没做」或「待裁定」两类。发布流程里的勾选对账见 §发布对账。 -## 当前发布列车:1.2.0 候选已验收(2026-11-09)→ 待 tag/Release/站点 +## 当前发布列车:1.2.1(补丁候选)← 1.2.0 已发布(2026-11-09) - [x] 1.1.0 **自用 3 天**(用户 2026-11-06 显式豁免,记录见 `docs/releases/1.1.0.md` §3) - [x] 1.1.0 **tag → Release**(tag `v1.1.0` → `393318c`;Release run `42225134493` success;ZIP `448d3170…` 与候选逐字节一致、DMG 公证通过、appcast `sparkle:version=38`;报告 248) @@ -17,6 +17,9 @@ - [x] **1.2.0 装机反馈修正(报告 255)**:⌥L 在无候选窗时不再漏出 `¬`(组合键前移到客户端判定之前);拆字键位按 `positionMismatch` 截断降级(「我」→ `丿T 扌R ?N`);`trnt` 不自动上屏确认是「同码不唯一」的预期行为(我/特性/选择性)。 - [x] **1.2.0 B4 练习(ADR-313,报告 257)**:`Practice.swift`(只出 full 题、判分、识别码表、进度 TSV 有界原子写)+ `PracticeWindow.swift`(独立窗口,题型 A 看字打码 / C 识别码判断);新增配置 `hotkey.open_practice`(默认 ⌥K)。 - [x] **1.2.0 B5 质量收口(ADR-314,报告 258)**:A4 冷首键实测 ~28 ms(与学习区无关,用户不可感知);A1/A2/A3/A5 全部关闭并写触发条件;内存口径按 ADR-312 双阈值。 +- [ ] **1.2.1 补丁候选**(Cargo 1.2.1 / build 40):安装体验(旧宿主重启询问、输入源缺失时打开设置页)、 + 练习页无障碍(VoiceOver 标签与播报)、回归并发默认 4;另含死代码/死资源清理与文档状态标注。 + 发布稿 `docs/releases/1.2.1.md`;tag 需授权。 - [x] **1.2.0 候选**(Cargo 1.2.0 / build 39,候选 `31fdbcd`、执行 `ef25e92b…`):B1 工程收口 → B2 数据地基 → B3 L3 提示 → B4 L4 练习 → B5 质量项 全部完成; 练习页四题型 + 键位图/口诀(ADR-316–323)、学习区改版(ADR-311)、签名 tag/站点盖章等工程项(报告 251–262)。 发版材料见 [`docs/releases/1.2.0.md`](docs/releases/1.2.0.md);tag 待授权。 diff --git a/assets/icons/yinghuo-icon-v4-menu-template.png b/assets/icons/yinghuo-icon-v4-menu-template.png deleted file mode 100644 index 463e9d19..00000000 Binary files a/assets/icons/yinghuo-icon-v4-menu-template.png and /dev/null differ diff --git a/assets/icons/yinghuo-official-icon-navy-v1.png b/assets/icons/yinghuo-official-icon-navy-v1.png deleted file mode 100644 index 518e35d1..00000000 Binary files a/assets/icons/yinghuo-official-icon-navy-v1.png and /dev/null differ diff --git a/docs/README.md b/docs/README.md index 57c92034..944b8896 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@ # 萤火虫输入法 · 文档索引 -> 当前状态只看 [`../HANDOFF.md`](../HANDOFF.md)|任务清单看 [`../ROADMAP.md`](../ROADMAP.md)|门禁看 [macOS 发布清单](macos-release-checklist.md) +> 当前状态只看 [`../HANDOFF.md`](../HANDOFF.md)|下一步看 [`待办与积压.md`](待办与积压.md)|历史任务清单看 [`../ROADMAP.md`](../ROADMAP.md) +> **文档状态**:本表为**当前有效**;`archive/`、`history/`、`design/`、`reviews/` 是历史证据,只读不改(需要时加"历史文档"标注)。 > **AI 代理**:先读 [`../AGENTS.md`](../AGENTS.md)(铁律、工作流、验证命令),再按本索引展开。 ## 权威(当前有效) @@ -23,9 +24,10 @@ |---|---| | [MVP产品验收标准.md](MVP产品验收标准.md) | MVP 定义与门禁(IT-14/15/22/23) | | [MVP单人验收清单.md](MVP单人验收清单.md) | 用户手动走查、五工作日试用、独立核验材料 | -| [macos-release-checklist.md](macos-release-checklist.md) | 发布清单:已关闭/未关闭门禁 | +| [macos-release-checklist.md](macos-release-checklist.md) | **历史**(0.1.0 首版清单);当前发版走 [更新发布流程](更新发布流程.md) | | [更新发布流程.md](更新发布流程.md) · [更新源与镜像.md](更新源与镜像.md) | 发版流程与站点规则 | -| [releases/](releases/) | 每个版本的发布说明与实测结论:**1.2.0(候选)**、1.1.0、1.0.2、1.0.1、1.0.0、0.1.15、0.1.13 | +| [releases/](releases/) | 每个版本的发布说明与实测结论:**1.2.1(候选)**、1.2.0、1.1.0、1.0.2、1.0.1、1.0.0、0.1.15、0.1.13 | +| [待办与积压.md](待办与积压.md) | **剩余待办**(P0 补丁 / P1 教学与数据 / P2 有证据才做 / 明确不做) | | [人工验收与多机复现.md](人工验收与多机复现.md) | 历史验收操作与双机复现步骤 | | [验收协作清单.md](验收协作清单.md) | 换包验收时"谁做什么"、15 分钟窗口顺序 | @@ -54,7 +56,7 @@ | [MVP后执行计划.md](MVP后执行计划.md) | **批次划分与执行顺序**(v1.0.0 之后先做什么) | | [结构库第二阶段需求.md](结构库第二阶段需求.md) | corpus 需求清单:缺什么数据、准入规格、验收门槛(ADR-309/310) | | [待办与积压.md](待办与积压.md) | **下一版范围来源**:A–G 七类积压项、成本/依赖、1.2.0 三套套餐建议 | -| [迭代计划-1.2.0.md](迭代计划-1.2.0.md) | **1.2.0 执行顺序**:B1–B6 六个批次、依赖链、完成标志与风险 | +| [迭代计划-1.2.0.md](archive/迭代计划-1.2.0.md) | **1.2.0 执行顺序**:B1–B6 六个批次、依赖链、完成标志与风险 | ## 设计稿 diff --git a/docs/archive/README.md b/docs/archive/README.md index acfed495..a96f4572 100644 --- a/docs/archive/README.md +++ b/docs/archive/README.md @@ -9,6 +9,7 @@ | [发布说明-0.1.15.md](发布说明-0.1.15.md) | 0.1.15 的用户版发布说明底稿。**自 1.0.0 起改为 `docs/releases/<版本>.md`** | | [技术方案-v0.2-Rust.md](技术方案-v0.2-Rust.md) · [v0.3](技术方案-v0.3.md) · [v0.4](技术方案-v0.4.md) · [v0.5](技术方案-v0.5.md) | 技术方案历史稿,结论已并入 [`../技术方案.md`](../技术方案.md) | | [技术架构方案.md](技术架构方案.md) · [中文输入法调研.md](中文输入法调研.md) | 早期架构讨论与竞品调研,仅作背景 | +| [迭代计划-1.2.0.md](迭代计划-1.2.0.md) | 1.2.0 的批次划分与验收口径,B1–B5 已完成并随 `v1.2.0` 发布 | **归档约定** diff --git "a/docs/\350\277\255\344\273\243\350\256\241\345\210\222-1.2.0.md" "b/docs/archive/\350\277\255\344\273\243\350\256\241\345\210\222-1.2.0.md" similarity index 96% rename from "docs/\350\277\255\344\273\243\350\256\241\345\210\222-1.2.0.md" rename to "docs/archive/\350\277\255\344\273\243\350\256\241\345\210\222-1.2.0.md" index 110a9573..5a139235 100644 --- "a/docs/\350\277\255\344\273\243\350\256\241\345\210\222-1.2.0.md" +++ "b/docs/archive/\350\277\255\344\273\243\350\256\241\345\210\222-1.2.0.md" @@ -1,3 +1,6 @@ +> **历史文档(2026-11-09 归档)**:1.2.0 已发布(tag `v1.2.0`),本计划的 B1–B5 全部完成, +> 保留作为批次划分与验收口径的记录。当前待办见 [docs/待办与积压.md](../待办与积压.md)。 + # 1.2.0 迭代计划(顺序与批次) > 2026-11-06。目标:**除平台扩张(F)外,把 `docs/待办与积压.md` 里的积压项在 1.2.0 周期内"做完或明确关闭"**, diff --git a/docs/macos-release-checklist.md b/docs/macos-release-checklist.md index 5de3ba29..f697a728 100644 --- a/docs/macos-release-checklist.md +++ b/docs/macos-release-checklist.md @@ -1,3 +1,7 @@ +> **历史文档(2026-11-09 标注)**:这是**首版 0.1.0** 的发布前清单,已由 +> [更新发布流程](更新发布流程.md) 与 [AGENTS.md §6](../AGENTS.md) 的发布步骤取代; +> 文中包身份、日期与"进行中"状态都属历史,不能作为当前任务或通过证据。 + # macOS 首版发布前清单 > 2026-10-18:0.1.0 正式包已由 `macos-acceptance` 流程产出并通过签名/公证核验(`ba6d796`,执行 SHA `a234ef31…`),B 已装并实测菜单/循环/快捷键;A 已装并以新源方式注册(TIS 选中成功、系统日志有 `Activate Server`/`Menu`),菜单外观待肉眼确认。正式报告仍是旧包 `abdb83f` 的 1 PASS / 22 BLOCKED,需对 `a234ef31` 重跑。复盘与改进项见 `docs/reviews/069-iteration-retrospective.md`。 diff --git a/docs/releases/1.2.1.md b/docs/releases/1.2.1.md new file mode 100644 index 00000000..6764a792 --- /dev/null +++ b/docs/releases/1.2.1.md @@ -0,0 +1,32 @@ +# 1.2.1(build 40) + +> **状态:候选开发中(2026-11-09)**——P0 补丁档:安装体验与练习页无障碍;不改候选顺序、不改引擎行为。 +> 发布材料在打 tag 后回填资产与回读结果。 + +## 1. 内容(相对 1.2.0) + +| 项 | 变化 | +|---|---| +| **覆盖安装后旧宿主** | 安装器检测到仍有宿主跑在被移走的备份 inode 时:交互式安装会询问是否重启(`LAMPYRIS_RESTART_HOST=1` 可自动),不再只打印"重新登录" | +| **输入源缺失** | 安装尾部(`LampyrisTIS` 回读未通过)时给出恢复步骤,并在交互式/`LAMPYRIS_OPEN_SETTINGS=1` 时直接打开「键盘 → 文字输入」设置页 | +| **练习页无障碍** | 作答格有 `textField` 角色与实时值、进度条有数值、判分结果用 VoiceOver 播报;键位图有说明性 help 文本 | +| **回归稳定性** | 离线回归默认并发 4(Swift 编译不再互相抢资源导致假超时);个别测试的超时上限提到 180s | +| **文档** | `docs/故障恢复.md` 新增 §4.4(Terminal 里 ⌥L 被终端吞掉);完成态计划归档、索引与待办对齐;删除死代码与死资源(见下) | + +## 2. 清理(不改变行为) + +- 删除 `PracticeRootRowView`(练习改版后已无引用)。 +- 删除未使用成员:`PracticeWindow.iconButton/toolbarButtons/answerHeight`、`PracticeQuestion.rootHint`、 + `KeyboardMapView.accessibilitySummary`、`PracticeAnswerView.setTextForTesting`。 +- 删除零引用资源:`assets/icons/yinghuo-icon-v4-menu-template.png`、`assets/icons/yinghuo-official-icon-navy-v1.png`。 +- 删除零引用脚本:`platforms/macos/tests/run-selection-probe.py`。 +- `docs/迭代计划-1.2.0.md` → `docs/archive/`;`docs/macos-release-checklist.md` 标注为历史文档。 + +## 3. 验收计划 + +- `make verify` + 离线回归全绿(无行为改动,回归是主要防线)。 +- 装机复验:覆盖安装后宿主是否自动重启、输入源缺失提示是否出现、练习页 VoiceOver 是否读出题面与结果。 + +## 4. 已知限制 + +与 1.2.0 相同(三级简码未提供、冷首键 ~28 ms、学习区 +~3 MiB 等),见 [1.2.0](1.2.0.md) §5。 diff --git a/docs/reviews/253-b3-learning-area.md b/docs/reviews/253-b3-learning-area.md index 433aef75..6a389e50 100644 --- a/docs/reviews/253-b3-learning-area.md +++ b/docs/reviews/253-b3-learning-area.md @@ -51,4 +51,4 @@ ## 6. 关联 -ADR-311、`docs/design/l3-hint.md`、`docs/迭代计划-1.2.0.md`、报告 252。 +ADR-311、`docs/design/l3-hint.md`、`docs/archive/迭代计划-1.2.0.md`、报告 252。 diff --git a/docs/reviews/263-1.2.1-audit-cleanup.md b/docs/reviews/263-1.2.1-audit-cleanup.md new file mode 100644 index 00000000..8ffc7218 --- /dev/null +++ b/docs/reviews/263-1.2.1-audit-cleanup.md @@ -0,0 +1,47 @@ +# 263 · 1.2.1 补丁与代码/文档审计清理 + +- 日期:2026-11-09 +- 范围:P0 补丁档(安装体验、练习页无障碍、回归稳定性)+ 全仓库死代码/死资源/死文档审计 +- 结论:**行为面只动安装与无障碍;清理全部为删除零引用物**。`make verify` 与离线回归在全过程每一步后复跑。 + +## 1. P0 修复 + +| 项 | 改动 | 证据 | +|---|---|---| +| 覆盖安装后旧宿主仍在跑备份 inode | `install.sh` 检测到后:交互式询问是否重启;`LAMPYRIS_RESTART_HOST=1` 自动重启;非交互只提示命令 | 本轮复现两次(进程执行路径落在 `install-backup-…`) | +| 输入源被移出启用列表 | 安装尾部 `LampyrisTIS` 回读失败时给出恢复步骤;交互式或 `LAMPYRIS_OPEN_SETTINGS=1` 时打开「键盘 → 文字输入」设置页。**不写系统偏好**(TIS 后台启用不持久化) | `docs/故障恢复.md` §4.1/§4.3 | +| 练习页无障碍 | 作答格:`textField` 角色 + 实时值 + 帮助文本;进度条:数值;判分结果用 `announcementRequested` 播报;键位图补 help | 代码内 `setAccessibility*` | +| 回归假超时 | 离线回归默认并发 `min(4, cpu)`;受影响测试的编译上限 45/90s → 180s | 6 并发实测曾连续 5 个文件超时 | +| Terminal 里 ⌥L 被吞 | 新增 `docs/故障恢复.md` §4.4(现象/原因/处理) | 报告 255 §7 | + +## 2. 代码清理(零引用) + +| 删除 | 依据 | +|---|---| +| `platforms/macos/Sources/PracticeRootRowView.swift` | 练习改为 `PracticeExplanationView` 后无任何引用(7 个测试清单 + dev-preview 同步移除) | +| `PracticeWindow.iconButton/toolbarButtons/answerHeight` | 动作迁到 `NSToolbar` 后不再调用 | +| `PracticeQuestion.rootHint` | 讲解区改结构化展示后无调用 | +| `KeyboardMapView.accessibilitySummary` | 面板已提供标签,旧摘要无人调用 | +| `PracticeAnswerView.setTextForTesting` | 测试改用 `keyEvent` 注入 | +| `assets/icons/yinghuo-icon-v4-menu-template.png`、`yinghuo-official-icon-navy-v1.png` | 全仓库零引用(构建用 `yinghuo-official-app.icns`) | +| `platforms/macos/tests/run-selection-probe.py` | 全仓库零引用(同类能力由 `selsrc.swift` / `isolated-imk.py` 覆盖) | + +## 3. 文档整理 + +| 动作 | 说明 | +|---|---| +| `docs/迭代计划-1.2.0.md` → `docs/archive/` | 1.2.0 已发布,B1–B5 全部完成;归档并在 `archive/README.md` 登记、引用处改路径 | +| `docs/macos-release-checklist.md` 加“历史文档”标注 | 0.1.0 首版清单,已被 `更新发布流程.md` 取代;保留以维持历史链接 | +| `docs/README.md` 重写状态说明 | 明确“当前有效 vs 历史证据(archive/history/design/reviews)”;索引补 `待办与积压.md`、`releases/` 状态 | +| `docs/reviews/README.md` | 补齐 225–263 的阶段划分与“现在还差什么”的正确入口 | +| `docs/待办与积压.md` | 重排为 P0/P1/P2/明确不做,并归档 1.2.0 完成范围 | +| `docs/故障恢复.md` | 新增 §4.3(输入源未选中)/§4.4(Terminal 吞 ⌥L) | + +**刻意保留**:`docs/design/`(21 MB 历史截图)、`docs/reviews/`(过程证据)、`docs/history/`、 +`docs/archive/`。它们是发布/验收的可追溯证据,删除会让历史报告失去引用对象;如需瘦身应单独决策(不在本次范围)。 + +## 4. 复验 + +- 每一步删除后跑 `make verify` 与离线回归;最终 `make macos-offline-tests`:**62 文件 / 328 例 PASS**(4 并发)。 +- `platforms/macos/dev-preview.sh --build-only` 通过(AppKit 面编译验证)。 +- 版本推进到 **1.2.1 / build 40**,发布稿 `docs/releases/1.2.1.md`;tag 需用户授权。 diff --git a/docs/reviews/README.md b/docs/reviews/README.md index 1a6e3c5c..89e65108 100644 --- a/docs/reviews/README.md +++ b/docs/reviews/README.md @@ -36,9 +36,13 @@ | 学习区与教学 | 071–110 | 学习区上下对应、长词、紧凑学习区、拆字说明 | | 安装/更新/卸载 | 111–200 | 安装器、更新程序、自卸载、配置恢复 | | 1.0.0 收尾 | 201–224 | 见上表 | +| 1.1.0 发布线 | 225–250 | 简码/排序、验收成本、延迟归因、1.1.0 发布与站点 | +| 1.2.0 候选与发布 | 251–262 | B1 门禁、B0 运行时、学习区、练习四题型与键位图、1.2.0 发布 | +| 1.2.1 清理 | 263 | 安装/无障碍补丁、死代码与文档审计 | ## 4. 常见查找 - **"这个行为当初为什么这样定"** → 先查 [决策记录](../决策记录.md)(ADR 是冻结决策,报告是过程证据)。 - **"某版包到底是什么"** → 查该版 `docs/releases/*.md` 与对应报告里的 ZIP/执行 SHA。 -- **"现在还差什么"** → `ROADMAP.md` + [macos-release-checklist](../macos-release-checklist.md) + `HANDOFF.md`,**不要**从报告里推断当前状态。 +- **"现在还差什么"** → [`docs/待办与积压.md`](../待办与积压.md) + `ROADMAP.md` + `HANDOFF.md`,**不要**从报告里推断当前状态。 + (`macos-release-checklist.md` 是 0.1.0 时代的历史清单,已标注为历史文档。) diff --git "a/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" "b/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" index 73b9a0d5..c7ff1c9d 100644 --- "a/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" +++ "b/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" @@ -2943,7 +2943,7 @@ - **验证**:体积(净增量 ≤512 KiB)、查询延迟(p95 ≤5 ms)、装机后内存增量、离线回归与体积断言; 规则一致性用"同一字在 `b0_query.py` 与运行时 TSV 的文案相同"抽样校验。 - **状态**:已采纳(2026-11-06,1.2.0 B2 批)。 -- **关联**:ADR-189/308/309、报告 240–251、`docs/结构库第二阶段需求.md`、`docs/迭代计划-1.2.0.md`。 +- **关联**:ADR-189/308/309、报告 240–251、`docs/结构库第二阶段需求.md`、`docs/archive/迭代计划-1.2.0.md`。 ### ADR-312:内存门槛按"学习区开/关"分别设定 @@ -2988,7 +2988,7 @@ 5. 打开练习窗口前后,稳态 p95 与常驻内存无劣化(沿用 ADR-307/312 口径)。 - **非目标**:排行榜、社交、云同步、AI 生成题目、语音、打字统计上报、把练习成绩用于候选排序。 - **风险**:① 题型设计不当会变成"背题库"——用随机抽样与错题复现控制;② 进度文件是新增落盘文件——已按"数据非配置"归类并设上限。 -- **关联**:ADR-189/309/311/312、报告 240–256、`docs/结构库第二阶段需求.md`、`docs/迭代计划-1.2.0.md`(B4)。 +- **关联**:ADR-189/309/311/312、报告 240–256、`docs/结构库第二阶段需求.md`、`docs/archive/迭代计划-1.2.0.md`(B4)。 ### ADR-314:1.2.0 质量项收口(A1–A5 关闭与触发条件) @@ -3028,7 +3028,7 @@ - **验收**:自动化断言 26 个键(25 键名 + 万能键 Z)、键名字根与区归属、敲键点亮、答完描边、 TSV 与生成器输出一致(幂等)、体积 ≤32 KiB;换 layout 不改视图代码即可渲染另一套字根表。 - **状态**:已采纳(2026-11-08)。 -- **关联**:ADR-313、报告 262、`docs/迭代计划-1.2.0.md`(B4)。 +- **关联**:ADR-313、报告 262、`docs/archive/迭代计划-1.2.0.md`(B4)。 ### ADR-316:练习题型扩展、详情区默认隐藏、字根表以标准表为真源 diff --git "a/docs/\346\225\205\351\232\234\346\201\242\345\244\215.md" "b/docs/\346\225\205\351\232\234\346\201\242\345\244\215.md" index 43c7350e..9dede44c 100644 --- "a/docs/\346\225\205\351\232\234\346\201\242\345\244\215.md" +++ "b/docs/\346\225\205\351\232\234\346\201\242\345\244\215.md" @@ -163,6 +163,18 @@ open "$HOME/Library/Input Methods/Lampyris.app" --- +## 4.4 Terminal 里 ⌥L 没反应、屏幕上出现 `¬` + +**症状**:在 Terminal(或 Ghostty 等把 Option 当 Meta 的终端)里按 ⌥L,学习区不出现,光标处出现 `¬`。 + +**原因**:终端把 Option 组合键自己解释成 Meta 字符,事件根本没到输入法(报告 255 §7 已实测:TextEdit / 系统 +「终端」正常,Ghostty 会吞)。 + +**处理**: +- 换到 TextEdit / Safari 等原生应用里用 ⌥L;或 +- 在设置页把「学习区」快捷键改成不冲突的组合(如 `alt+shift+l`,改完重启宿主生效)。 +- 这不是缺陷,输入法无法拦截被终端先行消费的按键。 + ## 5. 通用原则(勿违反) 1. 输入法**只读**你的文件;出错时**保留上一份有效数据**。 diff --git a/modules/keymap/Sources/KeyboardMapView.swift b/modules/keymap/Sources/KeyboardMapView.swift index f303b3c8..71c41950 100644 --- a/modules/keymap/Sources/KeyboardMapView.swift +++ b/modules/keymap/Sources/KeyboardMapView.swift @@ -29,6 +29,7 @@ final class KeyboardMapView: NSView { setAccessibilityElement(true) setAccessibilityRole(.group) setAccessibilityLabel("\(layout.displayName) 字根键位图") + setAccessibilityHelp("每题答案用到的键位会描边,位次以圈号标在键位下方") } @available(*, unavailable) @@ -192,8 +193,4 @@ final class KeyboardMapView: NSView { } } - func accessibilitySummary() -> String { - "\(layout.displayName) 字根键位图:" + layout.entries.sorted { $0.key < $1.key } - .map { "\($0.key)=\($0.value.keyName)" }.joined(separator: ",") - } } diff --git a/packaging/build-number b/packaging/build-number index a2720097..425151f3 100644 --- a/packaging/build-number +++ b/packaging/build-number @@ -1 +1 @@ -39 +40 diff --git a/packaging/macos/tests/test-update-public-key.py b/packaging/macos/tests/test-update-public-key.py index 062897c3..817313a4 100644 --- a/packaging/macos/tests/test-update-public-key.py +++ b/packaging/macos/tests/test-update-public-key.py @@ -55,6 +55,9 @@ def test_private_key_is_not_committed(self): if not raw: continue path = ROOT / raw.decode() + # 已从工作树删除但尚未提交的文件仍会出现在 ls-files 里,跳过即可。 + if not path.exists(): + continue if path == Path(__file__).resolve() or path.stat().st_size > 4096: continue try: diff --git a/platforms/macos/Sources/Practice.swift b/platforms/macos/Sources/Practice.swift index bac8558d..f617415c 100644 --- a/platforms/macos/Sources/Practice.swift +++ b/platforms/macos/Sources/Practice.swift @@ -54,10 +54,6 @@ struct PracticeQuestion { let short1: String? let short2: String? - /// 拆解提示:字根(带键位),未知字根显示 `?`。 - var rootHint: String { - roots.map { "\($0.glyph)\($0.key)" }.joined(separator: " ") - } } enum Practice { diff --git a/platforms/macos/Sources/PracticeAnswerView.swift b/platforms/macos/Sources/PracticeAnswerView.swift index 9a435ede..805e335c 100644 --- a/platforms/macos/Sources/PracticeAnswerView.swift +++ b/platforms/macos/Sources/PracticeAnswerView.swift @@ -26,6 +26,18 @@ final class PracticeAnswerView: NSView { private(set) var isActive = true override var acceptsFirstResponder: Bool { true } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + setAccessibilityElement(true) + setAccessibilityRole(.textField) + setAccessibilityLabel("编码输入") + setAccessibilityHelp("输入字母,回车提交;Esc 或 ⌘⌫ 清空") + } + + override func accessibilityValue() -> Any? { + text.isEmpty ? "空(\(capacity) 位)" : text.uppercased() + } override var isFlipped: Bool { true } override var focusRingMaskBounds: NSRect { bounds } @@ -63,7 +75,6 @@ final class PracticeAnswerView: NSView { isActive = active needsDisplay = true } - func setTextForTesting(_ value: String) { text = String(value.prefix(capacity)) } override func draw(_ dirtyRect: NSRect) { let count = CGFloat(capacity) diff --git a/platforms/macos/Sources/PracticeRootRowView.swift b/platforms/macos/Sources/PracticeRootRowView.swift deleted file mode 100644 index a4139b98..00000000 --- a/platforms/macos/Sources/PracticeRootRowView.swift +++ /dev/null @@ -1,38 +0,0 @@ -import AppKit - -/// 练习里的字根行:与学习区同一套样式——上面字形、下面大写键位,一目了然。 -final class PracticeRootRowView: NSView { - static let cellWidth: CGFloat = 46 - static let rowHeight: CGFloat = 36 - private(set) var roots: [(glyph: String, key: String)] = [] - override var isFlipped: Bool { true } - - func show(_ roots: [(glyph: String, key: String)]) { - self.roots = Array(roots.prefix(6)) - setAccessibilityLabel(self.roots.map { "\($0.glyph),键 \($0.key)" }.joined(separator: ";")) - needsDisplay = true - } - - override func draw(_ dirtyRect: NSRect) { - var x: CGFloat = 0 - for root in roots { - let glyphRect = NSRect(x: x, y: 0, width: Self.cellWidth, height: 21) - let keyRect = NSRect(x: x, y: 21, width: Self.cellWidth, height: 15) - let glyph = NSMutableAttributedString(string: root.glyph, attributes: [ - .font: NSFont.systemFont(ofSize: 15), - .foregroundColor: NSColor.labelColor]) - var paragraph = NSMutableParagraphStyle() - paragraph.alignment = .center - glyph.addAttribute(.paragraphStyle, value: paragraph, range: NSRange(location: 0, length: glyph.length)) - glyph.draw(in: glyphRect) - let key = NSMutableAttributedString(string: root.key.uppercased(), attributes: [ - .font: NSFont.monospacedSystemFont(ofSize: 10.5, weight: .regular), - .foregroundColor: NSColor.secondaryLabelColor]) - paragraph = NSMutableParagraphStyle() - paragraph.alignment = .center - key.addAttribute(.paragraphStyle, value: paragraph, range: NSRange(location: 0, length: key.length)) - key.draw(in: keyRect) - x += Self.cellWidth - } - } -} diff --git a/platforms/macos/Sources/PracticeWindow.swift b/platforms/macos/Sources/PracticeWindow.swift index 9c8111aa..f0eaccf6 100644 --- a/platforms/macos/Sources/PracticeWindow.swift +++ b/platforms/macos/Sources/PracticeWindow.swift @@ -62,7 +62,6 @@ final class PracticeWindow: NSObject, NSWindowDelegate { private let progressBar = NSProgressIndicator() private let card = NSBox() private let scrollView = NSScrollView() - private var toolbarButtons: [NSButton] = [] private var answered = false private let kindControl = NSSegmentedControl(labels: PracticeKind.allCases.map(\.title), trackingMode: .selectOne, target: nil, action: nil) @@ -207,6 +206,8 @@ final class PracticeWindow: NSObject, NSWindowDelegate { progressBar.minValue = 0 progressBar.maxValue = 1 progressBar.controlSize = .small + progressBar.setAccessibilityLabel("练习进度") + progressBar.setAccessibilityHelp("已练字数占题库的比例") content.addSubview(progressBar) // 键位图面板(含方案名与口诀开关)自包含;是否显示由顶部工具栏决定。 @@ -317,40 +318,6 @@ final class PracticeWindow: NSObject, NSWindowDelegate { scrollView.frame = NSRect(origin: .zero, size: NSSize(width: Self.contentWidth, height: windowHeight)) } - /// 图标按钮:悬浮提示代替文字标题。 - private func iconButton(_ symbol: String, tooltip: String, action: Selector, - prominent: Bool = false) -> NSButton { - let image = NSImage(systemSymbolName: symbol, accessibilityDescription: tooltip) ?? NSImage() - let button = NSButton(image: image, target: self, action: action) - if prominent { - button.isBordered = false - button.wantsLayer = true - button.layer?.backgroundColor = NSColor.controlAccentColor.cgColor - button.layer?.cornerRadius = 9 - button.contentTintColor = .white - button.image = image.withSymbolConfiguration( - NSImage.SymbolConfiguration(pointSize: 14, weight: .semibold)) ?? image - } else { - button.isBordered = false - button.wantsLayer = true - button.layer?.backgroundColor = NSColor.labelColor.withAlphaComponent(0.08).cgColor - button.layer?.cornerRadius = 9 - button.contentTintColor = .labelColor - button.image = image.withSymbolConfiguration( - NSImage.SymbolConfiguration(pointSize: 14, weight: .medium)) ?? image - } - button.imagePosition = .imageOnly - button.toolTip = tooltip - button.setAccessibilityLabel(tooltip) - button.refusesFirstResponder = true - content?.addSubview(button) - return button - } - - private func answerHeight() -> CGFloat { - kind.isTyping ? Gap.codeAnswerHeight : Gap.recognitionAnswerHeight - } - private func showExplanation(_ question: PracticeQuestion, revealsAnswer: Bool) { // 本题目要求的那一项排在最前,完整码与两个简码随后——一块讲完,不再另起一行重复。 let target: String? = { @@ -412,6 +379,7 @@ final class PracticeWindow: NSObject, NSWindowDelegate { progressLabel.stringValue = "题库 \(session.bank.count) 字 · 已练 \(stats.practiced) 字 · " + "\(stats.attempts) 次 · 正确率 \(rate)% · 进度 \(percent)%" progressBar.doubleValue = Double(stats.practiced) / Double(bankSize) + progressBar.setAccessibilityValue("\(percent)%") } private func showCurrent() { @@ -503,8 +471,12 @@ final class PracticeWindow: NSObject, NSWindowDelegate { if let question = current, let expected = kind.answer(for: question) { keymapPanel.map.markAnswer(expected) } - feedbackLabel.stringValue = (result.correct ? "✓ 正确" : "✗ 再想想") - + (result.explanation.isEmpty ? "" : " · " + result.explanation) + let headline = result.correct ? "✓ 正确" : "✗ 再想想" + feedbackLabel.stringValue = headline + (result.explanation.isEmpty ? "" : " · " + result.explanation) + // VoiceOver:判分结果直接播报,不必移动光标。 + NSAccessibility.post(element: NSApp as Any, notification: .announcementRequested, + userInfo: [.announcement: feedbackLabel.stringValue, + .priority: NSAccessibilityPriorityLevel.medium.rawValue]) feedbackLabel.textColor = result.correct ? .systemGreen : .systemOrange updateStats() } diff --git a/platforms/macos/dev-preview.sh b/platforms/macos/dev-preview.sh index cabc2b77..5e8edacd 100755 --- a/platforms/macos/dev-preview.sh +++ b/platforms/macos/dev-preview.sh @@ -56,7 +56,6 @@ swiftc -g -Onone -warnings-as-errors -module-name LampyrisPreview \ "${SCRIPT_DIR}/Sources/Practice.swift" \ "${SCRIPT_DIR}/Sources/PracticeWindow.swift" \ "${SCRIPT_DIR}/Sources/PracticeAnswerView.swift" \ - "${SCRIPT_DIR}/Sources/PracticeRootRowView.swift" \ "${SCRIPT_DIR}/Sources/PracticeExplanationView.swift" \ "${ROOT}/modules/keymap/Sources/KeyboardMapView.swift" \ "${ROOT}/modules/keymap/Sources/KeymapPanelView.swift" \ diff --git a/platforms/macos/install.sh b/platforms/macos/install.sh index f5af80fc..3a00598b 100755 --- a/platforms/macos/install.sh +++ b/platforms/macos/install.sh @@ -78,12 +78,28 @@ fi # 覆盖安装后让 LaunchServices 重新登记:否则系统可能仍按旧记录启动(表现为"装了但样式/版本没变")。 LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister" [ -x "${LSREGISTER}" ] && "${LSREGISTER}" -f "${DEST}" >/dev/null 2>&1 || true -# 旧宿主若还跑在被移走的备份 inode 上,界面会是旧代码;这里只提示,不替用户杀掉进程。 +# 旧宿主若还跑在被移走的备份 inode 上,界面会是旧代码:默认只提示,交互式安装会问一次要不要重启。 STALE_HOSTS="$(pgrep -f "${HOME}/Library/Application Support/Lampyris/updates/install-backup-.*/Lampyris.app/Contents/MacOS/Lampyris" 2>/dev/null || true)" +restart_host() { + local pid + for pid in $(pgrep -f "MacOS/Lampyris" 2>/dev/null || true); do kill "${pid}" 2>/dev/null || true; done + sleep 1 + open "${DEST}" 2>/dev/null || true + echo "已重启宿主(新包)" +} if [ -n "${STALE_HOSTS}" ]; then - echo "⚠️ 仍有旧宿主在运行(pid $(echo "${STALE_HOSTS}" | tr '\n' ' ')),它加载的是备份里的旧程序;" >&2 - echo " 重新登录或重启后才会用上新安装的版本。" >&2 + echo "⚠️ 仍有旧宿主在运行(pid $(echo "${STALE_HOSTS}" | tr '\n' ' ')),它加载的是备份里的旧程序。" >&2 + if [ "${LAMPYRIS_RESTART_HOST:-0}" = "1" ]; then + restart_host + elif [ -t 0 ] && [ -t 1 ]; then + printf "现在重启宿主以使用新版本?[y/N] " >&2 + read -r answer || answer="" + case "${answer}" in [yY]*) restart_host ;; *) echo " 稍后可重新登录,或手动:pkill -f 'MacOS/Lampyris'; open '${DEST}'" >&2 ;; esac + else + echo " 非交互安装:设 LAMPYRIS_RESTART_HOST=1 可自动重启,或稍后重新登录。" >&2 + fi fi + # ADR-295:包内自带更新程序时一并安装(与 DMG 安装器、更新器同一套校验与备份语义)。 install_updater() { local source="${SCRIPT_DIR}/../LampyrisUpdateCheck.app" @@ -147,6 +163,19 @@ HELPER="${DEST}/Contents/Helpers/LampyrisTIS" # 不能只看 `enable` 的返回值:macOS 13 上 `TISEnableInputSource` 会返回 0 但源并未进入启用列表 # (报告201/207 两次实测),所以回读一次再决定措辞。 READBACK="${SCRIPT_DIR}/tools/input-source-enabled.sh" +open_keyboard_settings() { + # 只把用户送到设置页,不写系统偏好(TIS 后台启用不持久化,见 docs/故障恢复.md §4.1)。 + if [ "${LAMPYRIS_OPEN_SETTINGS:-0}" = "1" ] || { [ -t 0 ] && [ -t 1 ]; }; then + open "x-apple.systempreferences:com.apple.Keyboard-Settings.extension" 2>/dev/null || true + fi +} +add_source_help() { + cat >&2 </dev/null 2>&1 || true "${HELPER}" enable "${IDENTIFIER}" >/dev/null 2>&1 || true @@ -154,11 +183,12 @@ if [ -x "${HELPER}" ]; then echo "安装完成。请用 ⌃Space 或输入法菜单确认「萤火虫」;若菜单里还没有,重新登录一次或手动添加。" elif "${READBACK}" "${HELPER}" "${IDENTIFIER}"; then echo "安装完成,已注册到输入源。" - echo "现在用 ⌃Space 或输入法菜单选中「萤火虫」即可;若菜单里还没出现,重启登录一次。" + echo "现在用 ⌃Space 或输入法菜单选中「萤火虫」即可;若菜单里还没出现,重新登录一次。" else - echo "安装完成,但系统没有接受自动注册(macOS 13 上的已知行为)。" - echo "请在系统设置 → 键盘 → 文字输入 → + → 中文(简体)添加「萤火虫」。" + echo "安装完成,但系统没有接受自动注册(macOS 13 上的已知行为)。" >&2 + add_source_help fi else echo "安装完成。请注销/重新登录,再在系统设置 → 键盘 → 文字输入添加萤火虫。" + add_source_help fi diff --git a/platforms/macos/tests/run-offline-tests.py b/platforms/macos/tests/run-offline-tests.py index 988b9cad..2798a7b5 100644 --- a/platforms/macos/tests/run-offline-tests.py +++ b/platforms/macos/tests/run-offline-tests.py @@ -32,11 +32,12 @@ def main(): # 报告230:这 56 个文件互不共享状态(各自用临时目录/假 HOME),串行跑纯属浪费—— # 改成并行后总时长由最慢的一个文件决定。JOBS=1 可退回串行做归因。 try: - jobs = int(os.environ.get('JOBS', '0')) + # 默认并发不超过 4:再高时 Swift 编译会互相抢资源导致假超时(2026-11-09 实测)。 + jobs = int(os.environ.get('JOBS', '0')) or min(4, os.cpu_count() or 4) except ValueError: jobs = 0 if jobs <= 0: - jobs = min(6, os.cpu_count() or 4) + jobs = min(4, os.cpu_count() or 4) jobs = max(1, min(jobs, len(files))) def run_one(index, file): diff --git a/platforms/macos/tests/run-selection-probe.py b/platforms/macos/tests/run-selection-probe.py deleted file mode 100644 index 5953091e..00000000 --- a/platforms/macos/tests/run-selection-probe.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Select only an owned IMK source and verify exact restoration; never send keys.""" -import importlib.util -import json -import os -from pathlib import Path -import plistlib -import selectors -import shutil -import signal -import subprocess -import sys -import time - -spec = importlib.util.spec_from_file_location('isolated', Path(__file__).with_name('isolated-imk.py')) -isolated = importlib.util.module_from_spec(spec) -spec.loader.exec_module(isolated) -LS = '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister' - - -def main(): - if len(sys.argv) != 2: - raise ValueError('必须提供本次独有根目录') - root = Path(sys.argv[1]).resolve() - isolated.require_owned(root) - app = root/'LampyrisIMKTest.app' - isolated.validate_identity(plistlib.loads((app/'Contents/Info.plist').read_bytes()), - isolated.IDENTITY+root.name.removeprefix('lampyris-imk-')) - binding = app/'Contents/Resources/IMKTestRoot' - if (binding.is_symlink() or not binding.is_file() or binding.stat().st_uid != os.geteuid() - or binding.stat().st_mode & 0o777 != 0o600 or Path(binding.read_text()).resolve() != root): - raise ValueError('实体包私有根声明不符') - helper = root/'source-state' - subprocess.run(['swiftc','-O','-warnings-as-errors','-module-cache-path',str(root/'module-cache'), - str(isolated.ROOT/'platforms/macos/tests/isolated-imk/source-state.swift'),'-o',str(helper)],check=True) - evidence = root/'selection-evidence' - evidence.mkdir(mode=0o700) - number = 0 - - def state(action): - nonlocal number - number += 1 - if action in ['select-owned', 'restore-original'] and alias.exists(): - command = [str(alias/'Contents/MacOS/LampyrisIMKTest'), '--source-action', 'select' if action == 'select-owned' else 'restore'] - else: - command = [str(helper),action,str(root)] - result = subprocess.run(command,capture_output=True,text=True,timeout=15) - (evidence/(str(number)+'-'+action+'.json')).write_text(json.dumps({'exit':result.returncode,'stdout':result.stdout,'stderr':result.stderr})) - if result.returncode: - raise RuntimeError(result.stderr.strip()) - return {} if result.stdout.startswith("SOURCE ") else json.loads(result.stdout) - - before = state('state') - journal = root/'selection-state.json' - with journal.open('x') as stream: - json.dump(dict(before, identity=isolated.IDENTITY+root.name.removeprefix('lampyris-imk-')),stream) - journal.chmod(0o600) - protected = Path.home()/'Library/Application Support/Lampyris' - def real(): - return {name:(protected/name).read_bytes() if (protected/name).exists() else None for name in ['config.toml','user-dictionary.tsv']} - real_before = real() - alias = Path.home()/('Library/Input Methods/LampyrisIMKTest-'+root.name.removeprefix('lampyris-imk-')+'.app') - process = None - server_error = None - created = False - reader = None - completed = False - old_int = signal.getsignal(signal.SIGINT) - old_term = signal.getsignal(signal.SIGTERM) - def interrupted(_number,_frame): - raise KeyboardInterrupt('输入源恢复探针捕获中断') - signal.signal(signal.SIGINT,interrupted) - signal.signal(signal.SIGTERM,interrupted) - try: - if os.path.lexists(alias): - raise RuntimeError('已有同名路径,不覆盖') - shutil.copytree(app,alias) - created = True - subprocess.run([LS,'-f',str(alias)],capture_output=True,timeout=15,check=True) - state('register') - server_error = (evidence/'server-error.txt').open('w') - process = subprocess.Popen([str(alias/'Contents/MacOS/LampyrisIMKTest')],stdout=subprocess.PIPE, - stderr=server_error,text=True) - reader = selectors.DefaultSelector() - reader.register(process.stdout,selectors.EVENT_READ) - deadline = time.monotonic()+10 - ready = False - while time.monotonic()