Commit Graph
470 Commits
Author SHA1 Message Date
AskaEth b9009c0e61 fix(m168): 复审 r2 三项 —— 夹具不再空转、启动不再替英文用户下载、时限不再判慢线下不完
复审 r2(p2-3 / fix-then-merge)三项:

1. **P2-1 爬行服务端是空转的**(`font/tests/support.rs`,从 `tests.rs` 拆出守 400 行):
   `seen.windows(4).any(|w| w == b"\n\n")` 拿 4 字节窗口比 2 字节切片 ⇒ `PartialEq` 先比长度 ⇒
   恒 false ⇒ 请求头永远读不完、一个字节不发,用例证明的其实只是「连接挂着被掐断」。上一版
   这里是 Python 生成时把 `\r\n` 落成了真实换行字节。现在两处都是转义写法
   (`b"\r\n\r\n"` + 完整 CRLF 响应头),并且**把「夹具真的在说话」变成断言**:
   `serve_trickle()` 返回 `TrickleProof { header, dripped }`,用例断言「真的发过头」「真的挤过
   字节」——否则夹具再坏一次也会绿。拆成两条用例:`a_trickling_candidate_is_cut_by_the_attempt_deadline`
   ([爬行, 正常],300 ms 切断第一个后第二个成功,0.38 s)与
   `the_total_deadline_cuts_a_single_crawling_candidate`(单候选 + 400 ms 总预算)。
   变异证明:把服务端改成「一个字节都不发」⇒ 两条用例红在「爬行服务端必须真的发出响应头」。

2. **P2-2 启动替英文用户下 17 MiB(真行为 bug)**:
   - `fonts::resolve_with` **不再触发下载**(启动那一拍只报告判据)。唯一的触发点收在
     `fonts::cjk_for`(`ui/lang.rs::log_cjk`),那一拍的 `setting` 与 `LangChoice` 同源
     (ULL_LANG > 配置 > 系统 locale)= 「界面实际用哪种语言」。
   - `plan` 把 `is_downloading()` / `Ready` 的闸提到**所有策略之前**:上一版只在 `NeedsDownload`
     分支里,"下载中 + Unprobeable" 会把状态改回「无法探测」而线程还在飞,下完又翻回「已就绪」。
     当时那条理由(「否则界面停在正在下载」)只对僵死成立 ⇒ worker 现在用 `catch_unwind`
     兜底「任何 panic 都写终态」,闸才敢前移。
   - 出图通道:新增 `install::set_offline(true)`(`App::with_presenter` 在
     `options.screenshot.is_some()` 时调),`plan_with_offline` 在 offline 下永不 `Start`。
     于是 `pages/harness/app.rs` 与 `--screenshot` 都不会在 zh-locale 无字体机器上真下 17 MiB。
   - 被钉住的旧判据(`Unprobeable + Downloading ⇒ Apply`)改成 `Skip` 并写明旧行为为什么是错的;
     新增 `every_strategy_skips_while_a_download_is_in_flight`(四策略打表)与
     `the_screenshot_channel_never_starts_a_download`。

3. **P2-3 90 s 单次上限把「慢但能下完」变永久失败**:改 `ATTEMPT_DEADLINE=120 s` /
   `TOTAL_DEADLINE=360 s`,并且**最后一个候选拿整个剩余总预算**(前 n-1 个仍 `min(单次, 剩余)`)。
   算账写在常量文档里:17 772 300 B / 360 s ≈ 49 KB/s,比 r1 实测最慢那条 IPv6(67~77 KB/s)
   还低一档 ⇒ 慢而能下完的线路仍有一次成功机会;健康线路(1.1~16.6 s)在第一/第二个候选就结束。
   代价(最坏 6 分钟)也是刻意取舍,写在同一处。

顺手:`i18n::CjkFont::source` 的「尚未钉死」改成指向 `Source::Pinned`;`clock.rs` 与
`general_section.rs` 注释里的 17.0 MB → 16.9 MB。

验证(`CARGO_TARGET_DIR=C:/Temp/ull-target/wt-168`,全量重编;取退出码,不截断):
- `cargo test -p unlucky-core --all-targets` → **16 个 target / 1189 passed / 0 failed / exit 0**
  (lib 625 + setting 115 + 其余 14 个)
- `cargo test -p unlucky-i18n` → 31 passed / 0 failed / exit 0
- `cargo test -p unlucky-app-iced`(含 `no_hardcoded_tokens` integration)→ **693 passed / 0 failed / exit 0**
- `cargo clippy -p unlucky-i18n -p unlucky-core -p unlucky-app-iced --all-targets -- -D warnings` → exit 0
- `cargo fmt --all -- --check` 干净;全仓受检文件 ≤400(本分支最大 361)
2026-11-08 14:38:25 +08:00
AskaEth 875b792206 fix(m168): 复审 r1 五项 —— 修红的 integration target、独立失败域、整体时限
复审 r1(p1-1/fix-then-merge)五项全改:

1. **p1-1 CI 会红的 4 条 integration test**(`crates/core/tests/setting/`,我上一轮只跑了 `--lib`,
   scope 漏掉了覆盖 schema 版本号的目录式 target):
   - `java_install_keys` / `isolation_keys` / `appearance_keys`:版本断言 1.6.0 → 1.7.0(M168 的
     `ui.font_cdn`),并写明「每加一批键推一次 minor 是纪律」。
   - `update_keys::a_read_only_launcher_config_never_retires_the_legacy_file`:夹具从**写死
     1.7.0** 改成 `launcher_version().minor + 1` 经 `SchemaRef::url()` 生成。写死数字的版本一推
     这条守卫就变成「同版本」⇒ 文件可写 ⇒ 迁移真的执行 ⇒ 旧文件被退役,而断言还绿着(用户数据
     守卫静默失效)。相对推导不会过期。
2. **p2-1 备用镜像不备用**:`cdn` 与 `fastly` 同属 Fastly(都 CNAME 到 jsdelivr.map.fastly.net),
   换掉。现在首选 `gcore.jsdelivr.net`(Cloudflare,独立失败域),二选 `cdn.jsdelivr.net`
   (Fastly)。注释里放了实测表(本机 + 复审机两份数据、字节数、sha256、IPv6 慢路径),
   并写清排序依据(Fastly 侧 IPv6 实测 67 KB/s ⇒ 丢掉 4 分钟;Cloudflare 侧两次都完整下完)。
3. **p2-2 整体时限**:`core::download::font` 新增 `ATTEMPT_DEADLINE = 90 s` /
   `TOTAL_DEADLINE = 180 s`,落在本模块自己的候选循环里(不动引擎全体语义):每次尝试
   `min(单次上限, 剩余总预算)`,超时归 `Network` 档。新增守卫
   `a_trickling_connection_is_cut_by_the_deadline`(回环服务端每 100 ms 挤 1 字节、预算 300 ms)。
   顺带修掉一个真 bug:`tokio::time::timeout` 在运行时外构造会 panic(5 条既有用例当场抓到),
   现在整段包进 `block_on(async { … })`。UI 文案改成「网络不通或太慢(超时)」,不校验失败混。
4. **p2-3 说 `Source::PendingPin` 的两处**:`fonts/cjk.rs` 的模块文档已改;`ui/lang.rs::log_cjk`
   那句「D28 的下载通道还是 PendingPin」(在下载已发起之后打印,等于当面否定本功能)改成
   「已按 D28 发起下载,见 [font] cjk-download … 与设置页那一行」。全仓再扫,只剩注释里的历史说明。
5. **p2-4 假守卫**:判据抽成纯函数 `install::plan(strategy, current) -> Step`(`Skip` / `Apply` /
   `Start`),`after_probe` 只执行。旧的空转用例换成打真判据的打表用例;变异证明:删掉
   `Skip` 那一臂 ⇒ `a_started_or_finished_install_is_not_restarted` 红(已实跑)。

次要项也一并处理:`from_cache` 改成**本次是否真的命中缓存**(`CdnFetcher::was_cached()`,
`has_cached()` 降级为诊断字段 `cache_present=`);`FontSpec` 的 URL 口径收紧为「非回环必须
https」(`is_acceptable_url` + 守卫);`ui.font_cdn` 写进状态行 hint;文档里的 17.0 MB → 16.9 MB。
`download/font.rs` 拆出 `font/spec.rs` 守 400 行。

验证(这台机器上单个 800 MB 测试 exe 链接要 10 分钟,链接期间文件被 Defender 扫描,故耗时很长):
- `cargo test -p unlucky-core --all-targets` → **1188 passed / 0 failed**(16 个 target,含
  `setting` 那个 115 条、上一轮红 4 条的 integration target)
- `cargo test -p unlucky-i18n` → 31 passed / 0 failed
- `cargo test -p unlucky-app-iced --lib` → 685 passed / 0 failed
- `cargo clippy -p unlucky-i18n -p unlucky-core -p unlucky-app-iced --all-targets -- -D warnings` → 干净
- `cargo fmt --all -- --check` → 干净;全仓受检文件行数最大 388(<400)
2026-11-08 14:38:23 +08:00
AskaEth 449ded2cb7 feat(m168): 中文字体 CDN 下载真接线(D28 后半)
M167 调研的结论是「中文字体 CDN 下载至今未实装」:`i18n::font` 只有 `Fetcher` trait 与
假实现,`needs-download` 分支什么也不做。这条把它接成一条真链路。

判据 → 下载 → 注册(一条链,四个 crate 各管一段):

* `unlucky_i18n::font`(零依赖):`Source::Pinned { urls, sha256, bytes }` 把「下什么」
  钉死——google/fonts 的 `NotoSansSC[wght].ttf` 钉到 commit `2894aab…`(不是 `@main`),
  17 772 300 B / sha256 `a3041811…`。可变字体一份覆盖 wght 100–900,省掉三份静态子集的 25 MiB。
  `FetchError` 补第三档 `Disk`(网络 / 校验 / 磁盘,UI 按类别选词)。
* `unlucky_core::download::font`(新):缓存命中 → 共享引擎 `download_bytes` → 复验(sha256 +
  sfnt 魔数 + 32 MiB 上限)→ 原子落盘。**复验在写盘之前**,所以「哈希不符」时盘上什么都没有;
  缓存按 `<sha256 前 16 位>` 命名,pin 一换自动换名。
* `unlucky_app_iced::fonts::{cdn,install}`(新):`CdnFetcher` 把 pin 接进 core(`ui.font_cdn`
  可覆盖 URL,哈希不换);`install` 是状态机 + 后台线程,只有 `NeedsDownload` 才起线程
  (英文 / 系统已有 / 探测不出一律零网络)。注册走 `iced_graphics::text::FontSystem::load_font`,
  族名从字体库读回来核对。失败只降级(继续用系统字体),不阻塞、不崩。
* 接入两条路径:`fonts::resolve_with`(启动)与 `fonts::cjk_for`(设置页切语言)都调
  `install::after_probe`;设置页 General 组多一行状态(下载中 / 完成 / 失败 / 无需下载 + 三类失败
  原因),文案进 catalog 金表(链尾追加,既有顺序不动);`App::advance` 每秒比对状态快照后重建
  视图模型,所以那一行会自己刷新。

配置:新增 `ui.font_cdn`(`keys/font.rs`,schema 1.7.0),空 = 内置 jsDelivr 镜像(含备用边缘)。

守卫(三条都能变红,变异证明见报告):core 侧 8 条(缓存不联网 / 哈希不符拒落盘 / HTML 不当字体
/ 坏缓存自愈 / 上限 / pin 校验),app 侧 6 条(三种闲置策略零调用、失败不注册、类别不串、幂等、
截图钉子、单行日志),页面侧 1 条(四态 × 两语言无缺译、不写假百分比)。

测试:`cargo test -p unlucky-i18n --lib` 31 绿;`-p unlucky-core --lib` 622 绿;
`-p unlucky-app-iced --lib` 684 绿;`cargo fmt --check` 干净。截图三张见报告。
2026-11-08 14:38:23 +08:00
AskaEth 2a004a5754 ci(m171): 两个 windows 作业显式豁免跨卷守卫
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
M171 合并后,跨卷守卫在只有一块系统卷的 runner 上会判红(这是设计:
跳过必须是响的)。runner 是否有第二块可写卷没有核实手段,因此不赌,
显式给 ULL_XDEV_TEST_WAIVE=1;该豁免是惰性的,探到第二块卷时不参与
判断。补丁位置属 M171 scope 之外,由 tower 在合并窗口内一并落地。
2026-11-08 13:52:35 +08:00
AskaEth 9700de1bd4 Merge branch 'feat/m171-xdevreplace-fix-cross-volume-update' 2026-11-08 13:51:03 +08:00
AskaEth ef77c67d3c fix(m171): r2 —— 跨卷守卫不许静默跳过,崩溃现场不删唯一旧副本
按首轮复审 p2-1items 的三条就地修。

P2-1(唯一阻塞项)跨卷守卫在只有一块卷的机器上静默变 ok:探不到第二块卷时
是 `eprintln! + return`,libtest 默认捕获 stdout/stderr,于是默认输出只有
`... ok`,跳过的记录只有 `--nocapture` 才看得见(复审 M3 变异实测)。
按复审给的方案 (b) 把裁决收成一条常跑用例(本仓口径同
`options_shot/feed.rs::not_fed`:跳过必须是响的):

  探到第二块卷            → 绿(真跨卷用例真跑)
  探不到 + ULL_XDEV_TEST_WAIVE=1 → 绿(显式、记录在案的降级,打印一行)
  探不到 + 没豁免         → 红(默认判红,失败正文给出三条出路)
  另有 ULL_XDEV_TEST_REQUIRE=1:声明「必须有」,与豁免同给时以它为准(不许自相矛盾)

裁决是纯函数 `xdev_guard(found, waiver, require)`:逐条用例走同一个入口
(`cross_volume_scene`),没人能靠 `return` 静静跳过;core 与 CLI e2e 两处政策一致
(源头在 `install_xdev_cases`,CLI 侧 `support::cross_volume_waived` 照抄并注明)。

CI 选择:**windows 作业显式声明豁免**(不挂 VHD)。理由:VHD 要多一步 diskpart、
要管盘符、易碎;豁免是一行 env、写在 ci.yml 里可见,且只在探测失败时被咨询——
将来 runner 真有第二块盘,真跨卷用例照样真跑。ubuntu 侧不豁免:`/tmp` 与
`/dev/shm` 是不同设备(本机 docker debian:bookworm-slim 实测设备号 119 / 158),
真跨卷守卫在 Linux 上应当是真跑的。ci.yml 不在本 mission 的改动范围,已用
TowerFinding 登记那一行补丁。

P3-1 `update_e2e_test.rs` 里 `install::prune_stale_backups` → `prune_stale_artifacts`。

P3-2 `prune_stale_artifacts(None, dest)` 会删掉崩溃现场唯一幸存的旧 exe:上一次装到
「dest → .ullold-*」之后被杀时,dest 空着、旧字节只剩备份那一份;下次成功安装时
`symlink_metadata(dest)` 失败 ⇒ keep=None ⇒ 连它一起删,用户失去手工恢复的可能
(修前不会删)。改成 keep=None 时**只清 `.ullnew-*`、一个 `.ullold-*` 都不动**,
模块文档补上这条例外与其来由。

验证(本机,CARGO_TARGET_DIR=C:/Temp/ull-target/wt-171):
* `cargo test -p unlucky-core --lib update::tests` → 90 passed / 0 failed
* `cargo test -p unlucky-core` → 623 passed(+ 各集成目标全绿),exit 0
* `cargo clippy -p unlucky-core --all-targets` / `-p unlucky-cli --all-targets
  --features update-test-source` → exit 0
* `cargo test -p unlucky-cli --features update-test-source --test update_e2e_test
  --test update_e2e_tamper_test` → 3 + 4 passed,含 `installs_across_volumes_from_the_cli`
* fmt clean
变异实测(都验完再还原):
  M3 让 probe 恒 None ⇒ 默认 `cargo test` 输出 2 failed(无须 --nocapture),
     正文「没有第二块可写卷,也没有显式豁免:跨卷守卫在这台机器上是空跑。」
  M3 + WAIVE=1 ⇒ 3 passed;M3 + REQUIRE=1 ⇒ 2 failed
  去掉 keep.is_some() 那一半 ⇒ `a_crashed_install_keeps_the_only_surviving_old_copy` 红
2026-11-08 13:34:29 +08:00
AskaEth 36ff193609 fix(m171): 跨卷替换改成「目标同目录中转名 + 同卷原子改名」
已发布版本上的真 bug:暂存包在 <数据根>/cache/update/staging,目标 exe 在安装目录,
两者不同卷(便携版放 U 盘 / 数据根在 D 盘而程序在 C 盘 / 部分 macOS 配置)时
`rename(staged → dest)` 直接报 EXDEV / ERROR_NOT_SAME_DEVICE(io::ErrorKind::CrossesDevices),
更新必然失败,而 is_transient 不含 CrossesDevices,也没有任何降级路径。

复现(本机真跨卷:C:/Temp NVMe ↔ D:/Temp 机械盘,原实现):
  InstallFailure { error: UpdateError { kind: Io, message: "系统无法将文件移到不同的磁盘驱动器。
  (os error 17)", path: Some("D:/Temp\ull-xdev-…\app\ull.exe") }, rolled_back: true }
失败点就是 `rename(暂存包 → dest)`;唯一调用方是 core::update::job::Job::apply(CLI/GUI 共用)。

修法(不把原子替换退化成可能留半截的复制):
  0. 新包 → <目标同目录>/.<文件名>.ullnew-<pid>-<seq>  同卷 rename;跨卷才 copy 到该中转名,
     并核对写出字节数(少了直接判失败)
  1. dest → <同目录 .ullold-…>                         旧版本先安全落地
  2. rename(中转名 → dest)                             同卷,原子
  3a. 成功 → InstallReceipt;3b. 失败 → 回滚 + 清中转名
目标路径上的新字节只来自一次原子命名,任何时刻都不是半截文件;同卷路径一个字节都不多搬
(注入用例断言 copies == 0,rename 恰好 3 次)。

备份清理策略一并定死:成功后只留最近一份 .ullold-*(更早的与残留的 .ullnew-* 删掉,
别的程序名的备份不碰)。旧行为是每次 apply 留一份、全仓无人调 remove_backup ⇒ 无限堆积。

顺手把文件动作抽成 replace_io::ReplaceFs(真实现 = std::fs),让「跨卷被拒」「复制到一半
失败」「改名失败」成为确定性输入;判据与顺序仍在 install 里,注入改不了。

守卫(都做过变异实测,均能变红):
  * install_xdev_cases:真跨卷安装成功且目标字节完整(去掉降级 → 红,报 os error 17)
  * install_io_cases:跨卷降级只复制一次 / 半截复制后目标仍是旧字节且无 .ullnew- 残留 /
    短复制被字节数判据拒掉 / 最后一步改名失败回滚并清中转名 / 同卷 copies == 0
  * cli update_e2e_test::installs_across_volumes_from_the_cli:数据根一卷、安装目录另一卷,
    整环从 CLI 入口跑通(去掉降级 → 红,输出 update_apply_failed + os error 17)
2026-11-08 13:06:57 +08:00
AskaEth ab79a28b28 Merge branch 'feat/m169-updatee2e-update-channel-end-to-end'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-08 11:31:48 +08:00
worker-updatee2e d7adbd1172 fix(m169): r3 四条就地修完 —— 只认同级 name:、纳入 .cargo/config.toml、登记 env 交接边界、校准两处数字
合并门禁只认 clean,r3 的 p2-4items 就地修掉(无行为改动,判据更严,不放松任何一条)。

P2-1(一行 + 三向量)`_name_of` 现在只认**同级** `name:`(`item.end() == key_indent`):更深缩进的
`- name:`(`env:`/`with:` 子列表、`run: |` 块标量里的行)不再被当成项名;同一项里出现两个**不同**
的 `name:` 返回 `None`(无法判定 ⇒ 默认拒绝,不去猜 YAML 重复键语义)。新向量:name 藏在 env 子项 /
藏在 `run: |` 里 / 两个不同 name ⇒ 必须红。

P2-2 `.cargo/config*.toml` 进扫描面(`CONFIG_GLOBS`:根与 `crates/*` 下的 `config.toml`/`config`),
新增 `scan_cargo_config`:feature 字面量(覆盖 `--cfg feature="…"`、`[env] RUSTFLAGS`)走原有判据,
`rustc-wrapper` / `rustc-workspace-wrapper` **一律判红**(wrapper 注入什么由程序决定,文本扫描看不见;
合法用途如 sccache 需要 review 明确同意,不能顺手加)。新向量:config.toml 用 `--cfg` 开该 feature
⇒ 红;设 rustc-wrapper ⇒ 红;普通 config.toml ⇒ 绿(防误杀)。

P2-3 运行期 env 交接写进守门自己的「抓不住」表(脚本头 + README §14):第一步把 feature 写进
`$GITHUB_ENV`、第二步只用 `--features "$FEAT"`——第二步没有字面量,**任何静态扫描都看不见**,这是本
守门的固有边界(引 YAML 解析器也修不了,因为这是数据流不是结构),不是漏判。

P2-4 两处过期数字:`release-guard.yml` 的「注入 14 条」→ 26 条注入 + 6 条防误杀/放行(另加基线,
条数以 `--self-test` 输出为准);README 的「26 条:21 红 + 6 绿」→「32 条向量:26 红 + 6 绿 + 基线」。

守门脚本 399 行(≤400,仍零第三方依赖);自测数据 32 条向量在 `packaging/tests/guard_vectors.json`。

验证(本机,未跑全量 cargo —— tower 已在合并态跑过 workspace 全量 EXIT=0/2648 passed):
- `--self-test`:26 条注入全红 + 7 条基线/防误杀绿,exit 0。
- **真仓注入 8 组**(改真 ci.yml / 新建真 .cargo/config.toml,改完即还原):env 子项里的 name ⇒ 红;
  `run: |` 里的 name ⇒ 红;两个不同 name ⇒ 红;`--cfg feature=` ⇒ 红;`rustc-wrapper` ⇒ 红;
  普通 config.toml ⇒ 绿;r2 的无名步骤 / flow 步骤回归 ⇒ 仍红;还原后基线绿。
- 正式扫描(真仓)exit 0;全仓 400 行扫描无超限;改动文件全 LF。
2026-11-08 11:27:21 +08:00
worker-updatee2e 68a1f90828 fix(m169): 复审 r2 —— 守门归属改成「列表项结构判定 + 默认拒绝」,堵掉无名步骤/无名 job/flow 步骤的继承放行
r2 复审(reviewer-updatee2e-r2,p2-4items;被抓出一个真洞,mission 翻回 active)逐条落地。

P2-1(本轮核心,真洞)allowlist 的归属原来按「最近一行 `- name:`」切块,于是任何不写成
`- name: <x>` 的步骤都继承上一块的名字;被放行的 `update-e2e` 恰好是 ci.yml 里最后一个
`- name:`,所以追加到文件末尾的一切都落在它的块里。复审三条真实注入(无名 `- run:`、无名步骤的
整个 job、flow 风格步骤)当时全绿。现在:
- `split_items()` 按 YAML **列表项**切块(每个 `- ` 起一项;比当前项深的行归当前项;同级或更浅的
  非列表行**结束**当前项)。项名 = 该项内**同级**的 `name:` 键(键序无关;`run: |` 块里的
  `name: dummy` 不算;`run: echo "name: x"` 也不算)。
- **默认拒绝**:落不到列表项上的行(`jobs:` 之前的孤儿行、job 级 `run:` 这类项外键)带 feature 即红;
  没有 `name:` 的项带 feature 即红。不再有「继承上一块」这回事。
- 三条复审注入 + 本轮新增三条(`jobs:` 之前的孤儿行、`run: |` 里夹带并伪造 name、shell 变量拼接)
  都进了自测向量;在**真仓库文件**上重跑也是全红。

P2-2(同族误杀):
- 键序无关:允许的那一步把 `run:` 写在 `name:` 之前仍绿(旧实现会报到上一块的名字上)。
- `cargo hack check --all-targets` 不再被误判成 `--all`(正则收紧到 `--all(?![-\w])`)。
- Cargo 的 `update-test-source = [ … ]` **跨行声明**不再被误判成「写进了别的 feature 列表」
  (`scan_manifest` 增加 `in_declaration` 状态)。

P2-3 `Fixture::run` 的第二条断言比错对象:原来比夹具字段 `self.dest`,手写
`fx.run(&["apply","--dest","<测试二进制>","--json"])` 两条都过。现在从 `args` 里解析真实 `--dest`
值(含 `--dest=` 形式)再 canonicalize 比较;夹具字段那份作为额外一道保留。

P2-4 `release-guard.yml` 的步骤级注释还停在 r1 的扫描面:改成「含复合 action / packaging 递归 /
Makefile 等 + 归属按列表项判 + 唯一允许 ci.yml 的 update-e2e 一步」。

自测数据搬到 `packaging/tests/guard_vectors.json`(基线 8 个文件 + 26 条向量:21 红 / 6 绿),
脚本本身从 379 行降到 373 行(守门仍 ≤400);README §14 补上归属规则与「抓不住」清单。

验证(本机,全部 exit 0):
- 守门正式扫描绿;`--self-test` 21 条注入全红 + 6 条基线/放行绿;向量文件缺失时 exit 1(不静默)。
- **真仓注入**(在真 ci.yml / release-build.yml / cli/Cargo.toml 上改完即还原):复审三条(无名步骤 /
  无名 job / flow 步骤)全红;本轮新增三条(孤儿行 / `run: |` 夹带 / shell 拼接)全红;
  三条合法写法(键序、`--all-targets`、跨行声明)全绿;还原后基线绿。
- E2E:debug 6 绿、release 6 绿(`running 3 tests` ×2,0.26s / 0.23s,非 0 measured)。
- `cargo clippy -p unlucky-cli --all-targets --features update-test-source -- -D warnings` 干净;
  `cargo fmt --all -- --check` 干净;全仓 400 行扫描无超限(守门 373 / 向量 194 / ci.yml 317)。

约束遵守:未 push;`PUBLISH_SLOTS` 只列 `windows-x64`(未动);release 侧仍无该 feature;跨卷 bug 未碰。
⚠ 仍未验:CI 里 E2E 真跑起来本机验不了(只证明 workflow 写对 + 守门放行,首跑要等推送后看 Actions)。
2026-11-08 11:12:11 +08:00
worker-updatee2e 28bf104041 fix(m169): 复审 r1 五条 p2 —— 守门开一个极窄 allowlist 并把 E2E 真接进 CI
r1 复审(reviewer-updatee2e-r1,p2-5items,均不挡合并)逐条落地:

P2-3(本轮主产出)守门把「E2E 进 CI」这条路堵死了:`scan_enabling_text` 对 workflow 里任何该
feature 字面量判红,于是 `cargo test --release … --features update-test-source --test
update_e2e_test` 这种**合法调用**也红,自己登记的 follow-up 无法实现。现在:
- `ALLOWED_STEP = {workflow: .github/workflows/ci.yml, name: update-e2e,
  must_contain: --test update_e2e_test}`;按 `- name:` 把 workflow 切成步骤块再判,文件名 / 步骤名
  / 命令行**三者任一不符即红**(job 名不参与判据:改 job 名不该解锁新能力)。
- `ci.yml` **新增一个独立 job** `update-e2e`(windows-latest、timeout 45m、release 档;现有 job
  一行未动,纯追加 27 行)。只在 Windows:夹具的「真形状」是裸 `.exe` + `portable-exe`,Linux/macOS
  的真实产物是 `.tar.gz` + `portable-zip`(会被 `ensure_self_replaceable` 直接拒),在 ubuntu 上跑绿
  是假绿。不降 debug:`debug_assertions`/优化/panic 行为都不同。
- self-test 覆盖 allowlist 六向量(允许 / 换文件 / 换步骤名 / 缺 `--test` / 发布工作流 / ci 内另一步)。

P2-2 守门 8 条绕过:注释剥离改**引号感知**(`echo "#"; cargo …` 不再吃掉真命令);扫描面扩到
`.github/actions/**`、`packaging/**` 递归子目录、`Makefile|justfile|Taskfile` 第二构建入口;
`--all-features` 之外补 `cargo hack --feature-powerset|--each-feature|--all`。大小写/下划线变体按
复审结论写进「抓不住」表(cargo feature 名敏感,写错编不过,不算漏检)。

P2-1 GUI 人工验收命令根本跑不起来:`-p unlucky-launcher-iced` 是 bin 名,改成 package 名
`-p unlucky-app-iced`(app-iced / presenter 两处,并写明 `-p` 只认 package)。

P2-5 夹具缺「不给 --dest 就 panic」的结构性保险:`Fixture::run` 加两条断言——`apply` 必带
`--dest`、`--dest` 不许等于本进程 exe 的 canonical 路径。抓得住「以后有人手写 fx.run(&["apply"])」,
抓不住「绕开 Fixture 直接调 unlucky_cli::run」。

P2-4 夹具「真形状」只在 Windows 成立:写进 `tests/support/update_e2e.rs` 文件头(说明 Linux 上跑绿
是假绿、CI 因此只在 Windows),**不改断言**。另按复审 ⑤ 补一行:失败路径三例抓不住回滚/备份
(摘要失败发生在 install_staged 之前),回滚靠正路备份断言 + install_cases。

跨卷替换 bug 按 tower 指令**未动**(另开 mission)。

验证:守门正式扫描 + self-test 绿(14 红 / 3 绿);真仓注入 6 组(基线绿、步骤改名红、缺 --test 红、
搬进 release-build 红、改 --all-features 红、还原绿);E2E debug 6 绿、**release 6 绿**;
`cargo clippy -p unlucky-cli --all-targets --features update-test-source -- -D warnings` 干净;
`cargo fmt --all -- --check` 干净;全仓 400 行扫描无超限(守门脚本 379 行、ci.yml 317 行)。

⚠ 未验:CI 里 E2E 真的跑起来这件事本机验不了——只证明 workflow 写对 + 守门放行,首次真实运行要等
推送后看 Actions。
2026-11-08 11:12:11 +08:00
worker-updatee2e 24826c1a7c feat(m169): 更新通道端到端验收 —— 本地源改成编译期 feature,补一条可一键跑的 E2E + CI 守门
§6 欠账:更新链路的 GUI 端从来没验过,而 M167 发现它根本没法在本机验——`ULL_UPDATE_DIR` 只在
`debug_assertions` 下生效,发布产物却是 release;`apply` 又会替换本进程 exe,于是没人敢跑。

判据换档(tower 定案 B):`cfg!(debug_assertions)` → 编译期 cargo feature `update-test-source`
(core 定义,cli/presenter/app-iced 链式转发,**默认关**)。带 feature 的构建里还要两档环境变量
(`ULL_UPDATE_DIR` + `ULL_UPDATE_TEST_SOURCE=1`),只给路径什么都不会发生。官方发布二进制里那段
分支根本编不进来——README「release builds do not contain that path at all」这句因此继续为真。

- core: `job::default_source` 的本地源分支整段 `#[cfg(feature = "update-test-source")]`;判据抽成
  纯函数 `test_source_root_from` 并逐条单测(半开一律关、空路径、承认值只认 `1`);
  `the_test_channel_is_a_compile_time_opt_in` 在**两种构建下各断各的**(带 feature ⇒ 换源;
  不带 ⇒ 环境变量设满也走 GitHub)。
- cli: `update check/apply` 的 `--json` 增加 `test_source` 字段(判据派生自实际用了哪个源,不是回读
  环境变量),人读输出多一行点名;`help.rs` 的 `ULL_UPDATE_DIR` 说明按新判据重写。
- 验收:`crates/cli/tests/update_e2e_test.rs` + `update_e2e_tamper_test.rs` + 共用夹具
  `tests/support/update_e2e.rs`。临时数据根 + 本地源 + 真形状清单(与 `manifest.golden.json` 同
  字段)+ 真 PE 产物(现场构建的 ull.exe 前 1 MiB,理由写在 `PAYLOAD_LIMIT`)。断言覆盖:发现
  新版本(source=file:… / test_source=true / size / notes)、下载 → 双检 → 就位(dest 字节、旧字节
  进备份、暂存无残留)、重复 apply 的幂等终态、篡改 sha256(清单撒谎 / 包体被换)与截断都必须
  拒绝且不动现有安装、修复后能重装。**永远带 `--dest`**:不带会真的改名换掉测试二进制。
- 守门:`packaging/check_test_source_off.py`(stdlib-only)扫 `.github/workflows/**`、
  `packaging/*.py|*.sh` 与各 `Cargo.toml` 的 `default`,任何一处打开 feature(或 `--all-features`)
  就红;`--self-test` 注入 5 条真实回归证明每条都会红。CI 挂在 release-guard.yml 两步。

验证(本机):core update 单测 80 绿 / 带 feature 6 绿;E2E debug 6 绿;
`cargo test --release --features update-test-source` 6 绿(tower 要求 release 档验收);
`cargo test -p unlucky-cli -p unlucky-presenter` 全绿;守门 self-test + 正式扫描绿(真注入后红)。

已知没验:真 GitHub Release 下载、GUI 点击与重启替换(人工);跨卷替换(另开 finding)。
2026-11-08 11:12:10 +08:00
AskaEth a14b9e17c1 Merge branch 'feat/m170-zip64gate-make-gate-order-test-real'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-08 10:55:57 +08:00
AskaEth add7f2e672 test(m170): 改对复审点出的两处证据陈述 + 收紧用例前提(零行为改动)
r1 复审 p2-2items(守卫本身判 clean,四条变异自跑确认有牙)。这一轮**只动注释与测试
前提,产品行为零变化**:`format/write.rs` 一字未改,`probe.rs` 保持上一提交的收窄结果。

p2-1 复审核正的事实:`one_file_over_the_threshold_...` 的 `kind()` **不区分闸的位置**。
它的源是**可读的** 8 KiB 真文件,闸被挪到 `File::open` 之后时写循环第二道闸
`Written::spend`(每块之后 `guard_with`)照样返回 `Limits`——所以它在「闸挪位置」的
变异下仍然绿(我这轮重跑变异 A 也复现:其余 4 条全绿)。原来那句「源文件能读时这里
拿到的是 pack_io」是错的,改成「可读源命中第二道闸」并把顺序的举证责任明确交给
`the_gate_runs_before_any_content_is_opened`。

p2-2 目录长度的实测数字之前钉错了:我早先那条探针用的是**短名**(`f0`..`f39`,7 条
才到 4096、40 条仍 4096),而用例里填的是 `filler-NN`。这轮按用例真实形状重测三组
(`std::fs::metadata`,同一台机器同一 `%TEMP%`):
  短名 `f0`…`f39`:空 0 | 3 条 0 | 7 条 4096 | 20/32/39/40 条 4096
  `filler-NN`:    空 0 | 3 条 4096 | 7 条 4096 | 20 条 8192 | 32 条 8192 | 39 条 12288 | 40 条 16384
两次独立跑同形状结果一致。所以「第几个条目起非零」没有平台无关答案(NTFS 目录索引按
4 KiB 一档分配,跟条目名长度与条数都有关),三处注释改成区间 + 一张实测表,并注明
「只钉 32 个 `filler-NN` 这个形状(本机 8192)」,不再硬凑一个好看的数字。

顺带做掉复审那条「不计入 p2 但建议收紧」:前提从 `declared > 0` 提到
**`limit > 清单 JSON 长度`**(`limit = declared.saturating_sub(1)`,`json_len` 由
`CURSEFORGE.render(&manifest)` 现算,本用例实测 220 字节)。否则某个对 32 个条目报得很
小的文件系统会让用例因「清单自己越线」而假绿。同时把 `declared - 1` 换成
`saturating_sub`,让长度为 0 时走到清楚的 assert 文案而不是整数下溢 panic。
把 `DIR_LENGTH_FILLERS` 临时改成 0 验证过这条 assert 是活的:红在这句上
(`声明长度 0 − 1 = 0 … 必须大于清单 JSON 自身长度(220)`)。

变异复跑(本机 Windows/NTFS,跑完已还原 write.rs):
- A:`preflight` 挪到 `File::open(&source)` 之后 → 新用例**红**(left Io / right Limits),
  其余 4 条仍绿(含上面那条可读源用例,正好印证 p2-1 的更正);
- C:A 之上只留 `expect_err` / `!target.exists()` / `leftovers` → **5 条全绿**,
  结构断言确属同义反复。

门禁:`cargo test -p unlucky-core --lib` 613 passed / 0 failed / 0 ignored;
`cargo fmt --all --check` 干净;`cargo clippy -p unlucky-core --all-targets -- -D warnings`
干净;zip64.rs 345 行(<400)、probe.rs 45 行。
2026-11-08 10:39:28 +08:00
AskaEth 72767067f3 test(m170): 闸顺序用例在 Windows 上也真跑(声明长度自己造 + 反证)
M166 r2 复审的两条「合并后收口」观察:

① `zip64::the_gate_runs_before_any_content_is_opened` 原来量夹具目录的
   `metadata().len()`,量到 0 就 `return`——而本机 NTFS 上空目录就是 0(实测:空 0、
   第 7 个条目起 4096),于是这条「行为证明」在 Windows 上一条断言都没执行。
   现在:先把目录填到长度非零(32 个 filler),拿「声明长度减一字节」当总量上限,
   前提不满足就 `assert!`;单条目上限给 `u64::MAX`,不让 `guard_entry` 当替身。
   另加一条**反证**:同一份夹具把限额放到 `u64::MAX`,必须死在 `File::open`/`read`
   上拿到 `pack_io`——于是上面拿到 `pack_limits` 只能解释为「闸在碰内容之前」。
   同时更正 `one_file_over_the_threshold_...` 里那句过头的注释:`leftovers` /
   `!target.exists()` 在任何失败路径上都成立,属结构侧检,不是「没读一个字节」的证明。

② `probe.rs` 的模块级 `#![allow(clippy::missing_const_for_thread_local)]` 收窄到
   `thread_local!` 内那个 `static` 上(A/B 实测:没带的那组只报一条 lint,带了就干净,
   且不判 unused),模块级 allow 不再替未来的真漏 const 打掩护。

变异验证(本机 Windows/NTFS,跑完已还原):
- 预检挪到 `File::open` 之后 → 本用例红(`left: Io` / `right: Limits`),改前那版全绿;
- 预检改成用 `std::fs::read` 量尺寸 → 同样红;
- 只留结构断言(拆掉 kind/文案/反证)→ 同一变异下**全绿**,证明它们确实区分不了顺序。
2026-11-08 09:54:31 +08:00
AskaEth 5965575210 Merge branch 'feat/m166-zip64fix-shrink-zip64-test-fixture'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-08 09:30:30 +08:00
AskaEth 9b23221a62 test(m166): 钉住生产 write() 的限额绑定(复审 r1 p2-1)
复审 r1 的变异:把 `format.rs` 里 `write()` 传给 `write_with_limits` 的
`WriteLimits::production()` 换成 `WriteLimits::new(u64::MAX, u64::MAX, entries)`,
**606 条全绿**。原因是夹具缩到 KiB 之后,"生产路径 + 真超限内容"那种顺带守住生产
限额的证法没了,而 `the_production_limits_are_the_shared_constants` 只钉了
`production()` 的**返回值**、没钉 `write()` 的**调用点**。

补法(不动生产签名、不进发布构建):
- 新增 `format/probe.rs`(`#[cfg(test)]`):线程局部的限额记录钩子;
  `write_with_limits` 进门时 `#[cfg(test)] probe::record(limits);` 记一笔。
- `tests/limits.rs::the_production_write_path_hands_the_shared_constants_to_the_preflight`
  走**生产**的 `CURSEFORGE.write`(KiB 级 payload)后断言记到的就是
  `WriteLimits::production()` 的逐字段值,并再钉一遍统一常量。
- 反向验:上述变异现在恰好 1 条红(`总量限额:生产路径传错了`,2026-11-08 实测
  18446744073709551615 vs 536870912);只把 entry 换成 u64::MAX 也红(`单条目限额`)。

附带(复审非阻断观察):
- 观察 2 采纳:新增 `zip64::the_gate_runs_before_any_content_is_opened`,把"闸在读之前"
  从结构证明升级成行为证明——源做成**目录**(`metadata()` 有长度、`File::open` 拒绝访问,
  本机实测 C:\Windows 20480 字节 / crates/core/src 4096 字节),闸若挪到读之后会拿到
  `pack_io` 而不是 `pack_limits`。
- 观察 1 采纳:`tests/limits.rs` 里"改坏常量会在 cargo check 就红"的旧注释改成准确说法
  (E0080 只在测试构建出现,裸 `cargo check` 是绿的)。
- `probe.rs` 里 `#![allow(clippy::missing_const_for_thread_local)]`:clippy 1.99 对已经
  写成 `const { Cell::new(None) }` 的 `thread_local!` 仍报错(最小复现同样红),是误报;
  外挂在宏上的 `#[allow]` 会被判 unused attribute,故用模块级 inner attribute。

cargo test -p unlucky-core --lib:608 passed / 0 failed / 0 ignored(+2 条);
clippy --all-targets -D warnings、fmt --check 干净;全仓 0 个 >400 行文件。
2026-11-08 09:30:26 +08:00
AskaEth 49a2fafe51 test(m166): zip64 夹具改用小 limits,去掉 8.5 GiB 真落盘
`set_len` 造的所谓稀疏文件在 NTFS 上并不稀疏:实测 `SetLength(1 GiB)` 之后
C 盘可用空间正好掉 1024 MiB。于是 `zip64.rs` 那两条用例的声明尺寸
(`u32::MAX + 1` ≈ 4 GiB 加 `3 × 1.5 GiB` = 4.5 GiB)就是 8.5 GiB **真占用**,
再加上 `write/tests.rs` 的 3 GiB 与 `limits.rs` 的三条 64 MiB 级用例,
单次跑的落盘峰值到过 ~12 GiB。

判据本身跟夹具大小无关(吃的是 `metadata().len()`),所以把 limits 做成参数:
`format::write_with_limits` 是 `PackFormat::write` 的实体,生产路径仍只传
`WriteLimits::production()`,测试传 KiB 级小值走**同一条** `preflight` 分支。
`WriteLimits` 的可见性顺势开到 `pub(in crate::addon::pack)`。

GiB 级的**数值判定**搬进 `format/write/tests.rs` 的纯算术用例:>4 GiB(撞 ZIP64
阈值即拒、差一字节放行)与 >2 GiB(M152 那个 2.5 GB 样本现在按统一上限拒),
以及第二道闸 `Written` 在同一批数值上的行为——两道闸吃的都是 u64,不需要落盘。

断言一条没删、用例一条没新加 `#[ignore]`(反而把旧的那条 `#[ignore]` 的
2.5 GB 用例换成会跑的 KiB 级版本,补上 `guard_entry` 分支的端到端覆盖)。

扫描:`grep -rn 稀疏 crates/ packaging/ docs/` 里凡写着"稀疏文件不占磁盘"的
注释一并改成实测口径(含 `addon/temp.rs`),避免下一个人照着再踩一次。
2026-11-08 09:30:26 +08:00
AskaEth 5e1f0868f1 Merge branch 'feat/m165-dlengine-reuse-download-engine-acro'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-08 09:30:10 +08:00
AskaEth afe169751f fix(m165): forget 之后让自更新也换引擎(复审 P2-1)+ 卡口计数与端到端守卫
复审 P2-1(采「修法 1」,不是只改措辞):`shared::forget` 只让登记处不再持有
那一份,换不掉自更新 `Factory` 里常驻的 Arc —— 于是「重新检测地区」对更新路径
不生效,直到重启。现在设置动作会把 Factory 一起重建。

- presenter/update_state: `Runtime::rebuild_factory` + `CoreHost::refresh_update_factory`;
  只换 factory,不动 view/inflight(在飞的 Job 派发时已克隆走装配,重建不打断它)
- presenter/dispatch: `SetSetting` 成功且键是「重新检测地区」时调 refresh
- presenter/engine: `DETECT_REGION_ACTION` 常量(设置页行 id / set 分派 / dispatch
  判断共用);放这里而不是 settings.rs 是因为后者已到 400 行红线
- core/shared: 文档改准 —— forget 的范围是「登记处不再持有」,长命持有者必须自己重建;
  记账从 `engine_for` 移到 `DownloadEngine::new` 这个卡口(分两把锁,避免自锁),
  于是 `constructions` 连绕过登记处另造引擎的调用点也统得住

守卫(都做了变异实证):
- update_tests::detect_region_rebuilds_the_update_factory_engine:走真实命令路径
  `AppCommand::SetSetting` → 断言 Factory 换了引擎且指向登记处当前实例;变异(删掉
  dispatch 里那句 refresh)→ 红在 Arc::ptr_eq
- update_tests::rebuilding_the_factory_does_not_touch_a_running_job:重建不清
  inflight、不改快照,在飞 Job 仍能跑完
- addon/http::tests::three_host_request_paths_share_one_engine:连走三条真实宿主路径
  (Modrinth 元数据 GET / CF 带 header GET / Java 页渲染)后 constructions == 1;
  变异(让 get_via_engine 自己造引擎)→ 红,left:2 right:1

顺带修:settings.rs 在上一提交被推到 403 行、engine.rs 401 行,两者都超
guards.yml 的 file-size 红线(>400 直接失败)。现在 settings.rs 400 / engine.rs 399,
全仓 `*.rs` 复跑「>400」扫描无输出。

验证:core lib 608 过、5 个 download 集成 target 42 过、presenter lib 270 过、
clippy --all-targets 干净、fmt 干净。真站点仍未打(见报告「没验的部分」)。
2026-11-08 09:30:05 +08:00
AskaEth 46389d61fd feat(m165): 下载引擎按数据根跨请求复用(运行时常驻 + 构造次数守卫)
宿主侧以前每次请求都现建一个 DownloadEngine:连接池、信号量限流、
健康度记忆、内容寻址缓存索引全部清零。现在改成按数据根共享一个实例。

- core: 新增 download::shared(进程级登记处,键 = 缓存根)——
  engine_for / forget / constructions;region/health/cache/mirrors 的装配
  收在这一个 build() 里(持锁构造只做本地 I/O,不联网)
- core: download::blocking 改成进程内唯一常驻运行时。reqwest 的连接池绑在
  驱动连接的运行时上,引擎复用必须配运行时常驻,否则复用的是个装满死连接的池
- presenter: host::engine::engine 返回 Arc<DownloadEngine>(共享实例),
  addon/update/versions/java/authlib 等全部调用点随之改;新增 forget 供
  「重新检测地区」失效(否则进程级引擎下那句话不成立)
- presenter: addon 详情页把「项目 + 版本列表」两次 block_on 收成一次

失败分类 / 镜像回退 / Retry-After 语义未改动(error.rs、http.rs、engine.rs 的
候选循环一字未动)。

守卫(都带变异实证):
- shared::tests::one_engine_per_cache_root + host::engine::tests::
  host_requests_share_one_engine:断言 constructions(root) == 1(不是只看结果)
- shared::tests::pooled_connection_survives_across_block_on_calls:127.0.0.1
  回环服务统计 accept 次数,两次 block_on 必须只拨号一次

验证:cargo test -p unlucky-core --lib(608 过)、5 个 download 集成 target(42 过)、
cargo test -p unlucky-presenter --lib(267 过)、cargo clippy --all-targets、
cargo fmt --check 全绿。真站点(Modrinth/CurseForge/BMCLAPI)未打,见报告。
2026-11-08 09:30:05 +08:00
AskaEth b4e6fbf8c1 Merge branch 'feat/m164-windowsarm64-fix-arm64-build-and-pu'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-08 08:50:21 +08:00
AskaEth 5ff6cdb38b fix(m164): 复审三条 P2 —— vcvarsall 用 wrapper .bat 真抓到 LIB/INCLUDE、ls 进 arm64 分支、两处数字与一句顺序承诺
P2-1(核心:上一轮的 ② 在 git-bash 下是死代码):`cmd //c "\"${vcvars//\//\}\" arm64 …"`
不行——MSYS 把参数里的 `"` 改写成 `\"` 交给 cmd.exe,cmd 的 `/c` 引号剥离接不住,LIB/INCLUDE
全空,只剩 `set` 的空输出;于是 ③「显式 link.exe」在**没有 LIB** 的状态下生效,正是本步注释里
写的那种会造出 `LNK1181: cannot open input file 'kernel32.lib'` 的组合。改法(复审验过的路线):
往 `$RUNNER_TEMP` 写一个 **CRLF** 的 wrapper `.bat`(`call "<vcvarsall>" arm64 >/dev/null 2>&1` + `set`),
`cmd //c` 只收**不含引号**的相对文件名(先 cd 进去)——引号留在 .bat 内部由 cmd 解析,路径带空格
也不受影响。同源的 ③ 也补齐:`vswhere -find` 的 `vcvarsall.bat` 与两个 `link.exe` 候选都改成
「反斜杠形态 + 正斜杠形态各试一次」(和同文件 dumpbin 步同规矩),不再赌一种形态。

P2-2:cargo build 步末尾那条 `ls -l` 移到 `if [ "$SLOT" = "windows-arm64" ]` 门内(并加 `|| true`):
x64 是当前唯一在发的槽位,不给它加一个失败面。

P2-3:① `release-publish.yml` 步数改对——主线 7 步、本分支 8 步(`release.yml` 与
`packaging/docs/build-and-ci.md` 两处都改,并列出 8 步是哪几步);② `check_publish_slots.py`
的 report() 全部改走 **stdout**:注解(`::error::`/`::warning::`)在 stdout 上同样被 Actions 识别,
而分两个流写会让「表格 → 注解」的顺序不可控(原注释承诺的顺序当时并不成立)。顺带在
`build-and-ci.md` 的 arm64 工具链段补一句「`cmd //c` 别收带引号的路径、要 wrapper .bat」,
免得下一个 agent 再把 ② 写回死代码。

本地证据(同一 worktree 复跑):
- P2-1 新证据:把本步 `run:` 块原样抽出,用 `bash --noprofile --norc -eo pipefail`(与 runner 同
  语义)、假 vswhere + **路径带空格**的假 `vcvarsall.bat`(真 cmd)跑:新写法 LIB/INCLUDE/linker
  三行全部写进 `$GITHUB_ENV`、rc=0;同一套假件跑 e858664 的老写法 ⇒ LIB/INCLUDE 全空、只有 linker
  (与复审结果一致)。降级路径也复测:vswhere 不存在 ⇒ 只 1 条 warning、rc=0;vswhere 不给
  vcvarsall ⇒ ② warning、③ 仍拿到 Hostx64 兜底、rc=0。
- 体检步 5 场景(无 VS / vswhere 全成功 / 报错退出 / 垃圾+非零 / 路径不存在)全部 rc=0、0 条
  `::error::`。
- `check_publish_slots.py --self-test` 8 条 + 警告断言全绿;`--dist` 六场景复测(x64-only 绿 /
  多一份 arm64 绿+warning / 缺 CLI rc=1 / 空 dist rc=1 / `--slots` 漂移 rc=1 / `--dist` 非目录
  rc=1),并且缺产物时输出顺序是「表格 → warning → error → 汇总」,stderr 为空。
- 7 个 workflow `yaml.safe_load` OK;全部 `run:` 块 `bash -n` OK;全仓 400 行扫描 0 个超限
  (release-build.yml 316、check_publish_slots.py 345)。
- `check_release_semantics.py`(+`--self-mutation`)、`release_marker.py --self-test`、
  `make_manifest.py --self-test`、`cargo metadata --no-deps` 全绿。

边界不变:arm64 产物仍**从未被构建过、也从未在真机跑过**;`PUBLISH_SLOTS` 仍是 `"windows-x64"`,
发布语义与 x64 静态 CRT 门禁未动。下一次 tag 的 arm64 仍可能红,红点大概率换到 C 依赖编译
(aws-lc-sys 的 CMake + aarch64 cl.exe)或链接器(LNK1181)——**不是「arm64 修好了」**。
2026-11-08 08:45:02 +08:00
AskaEth e858664fca ci(release): M164 arm64 判定权交回编译器 + 发布名单守卫(不翻牌)
根因(拿到的真实证据,不是猜的):v0.2.2(run 42322062879 / job 128248713926)与 v0.2.3
(run 42429017002 / job 128573547829)两份 check-run 注解逐字一致 —— 失败步骤是第 6 步
`Windows ARM64 工具链体检`,第 7 步 `cargo build` 状态 skipped。也就是说 v0.2.1~v0.2.3 五轮
全红都红在一个**编译前的启发式探针**上,真正的编译器/链接器一次都没说话。该探针的结论还与它
自己打印的快照矛盾:`SDK Lib 版本→um 架构: 10.0.22621.0→um:[arm,arm64,x64,x86,]` 明明在,却
同时报 `kernel32=缺失`(同一个 `find -path` 模式在本地合成目录树上命中)。runner 实况是
Windows 11 Arm64 + Visual Studio 2026 镜像,`VC.Tools.ARM64` 与 `Windows11SDK.22621/26100`
都装好了,M142 的 `setup.exe modify` 反倒返回非 0。

改:
- release-build.yml:探针降级为「只取证」——vswhere 组件查询 + 各目录 `.lib` 计数 +
  arm64 `link.exe` 候选,一条 `::error::` 都不写、永不 exit 1;不再自动补装 VS。红绿只由
  cargo/链接/静态 CRT 导入表门禁决定。cargo build 步给 arm64 显式备齐工具链:
  `rustup target add aarch64-pc-windows-msvc`、`vcvarsall.bat arm64` 取 `LIB`/`INCLUDE`、
  vswhere 定位 arm64 `link.exe` 后导出 `CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER`
  (顺序不能反:只给链接器不给 LIB ⇒ LNK1181 kernel32.lib)。
- 新增 packaging/check_publish_slots.py(344 行,零依赖):名单 ⇄ 产物**两个方向**。
  声明了缺 GUI/CLI ⇒ `::error::` + exit 1;有产物没声明 ⇒ `::warning::` 明说未发布(M142 之后
  这个方向是静默的,裁剪步只 echo 一行 drop:)。另有源码级断言(名单每项必须存在且 mode=ci、
  运行期名单必须等于文件里那行)与 8 条 `--self-test` 注入回归。
- release-publish.yml:裁剪前插一步跑那个守卫,原来内联的齐全性循环搬进脚本(一套判据一处实现);
  重算 SHA256SUMS 的步骤只负责重算。
- release-guard.yml:PR 阶段跑守卫自测 + 源码级断言。
- 文档同步:release.yml / release-publish.yml / release-build.yml 头注释、packaging/README §12、
  §1 矩阵、§3 构建与发布、§9 支持矩阵、§7.5 arm64 取证、release-notes.md、README/README.zh-CN
  的产物矩阵与说明。口径统一为「arm64 构建取证中、未发布」,不写成已支持。

没做的(按 tower 裁决「方案乙」):`PUBLISH_SLOTS` 本轮**不加回** arm64。arm64 从未产出过 PE、
本机也证不了,写进名单就等于拿 x64 的发布去赌它(M142 移出名单正是为避免这个事故)。下一 tag 的
CI 真出包后翻一行即可,守卫与 SHA256SUMS 自动跟着扩。

验证:workflow YAML 全部可被 PyYAML 解析;所有 `run:` 块过 `bash -n`;探针步在本机(无 VS/SDK)
实跑 exit 0 并打出实况注解;守卫 `--self-test` 8 条全绿、正反两个方向都在合成 dist 上实测过;
check_release_semantics.py(含 --self-mutation)/ release_marker / make_manifest 自检全绿;
cargo metadata 正常。**arm64 产物从未被构建过、也从未在真机跑过**——本机 `rustc --target
aarch64-pc-windows-msvc` 直接撞上没有 MSVC 链接器(git-bash 的 GNU `link` 被当成 link.exe),
`cargo check --workspace --target aarch64-pc-windows-msvc` 也死在 aws-lc-sys 的 cc-rs 阶段
(GNU host 上没有该 target 的 C 编译器)。真实结论只能等下一 tag 的 CI。
2026-11-08 08:33:44 +08:00
AskaEth 15a19bb375 Merge branch 'feat/m163-releasesem-stable-by-default-publis'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-07 19:11:10 +08:00
AskaEth 1d2e84ed61 fix(m163): push tag 默认建稳定版(发布标记语义 + 0.2.3 bump)
发布标记:`release.yml` 的 dispatch `prerelease` 默认值 true → false;`release-publish.yml`
的 create 路径不再塞 `--prerelease`(空串 = push tag ⇒ 稳定版),edit 路径行为不变(空串不碰
标记)。三种触发各创建什么改由 `packaging/release_marker.py` 一张表定义,流水线
`--emit-bash` 出来两个数组直接用,自己不再写 case——v0.2.2 至今 `releases/latest` 404 的根因
就是「push 默认 prerelease」这层叠上「dispatch 默认 true」。

守门:新增 `packaging/check_release_semantics.py`(CI 在 `.github/workflows/release-guard.yml`,
guards.yml 已 398 行不能再挂 job):①标记表逐条对(push tag 的 create 不含 --prerelease);
②把 `--emit-bash` 真灌进 bash 读回两个数组;③publish 的可执行内容里没有 --prerelease 字面量、
必须调用 emit-bash、两个数组各喂 create/edit;④release.yml 的 dispatch 默认值 == false(读
YAML 结构)。`--self-mutation` 注入三项真实回归(默认值改回 true / 塞回字面量 / 表里 push
改成预发布),断言每项都红、基线绿。

版本:0.2.2 → 0.2.3(workspace `[workspace.package]` + 五处 path 依赖 pin + Cargo.lock)。

文档:README 中英两版版本行改 0.2.3,并把本阶段能力补进现有章节(Modrinth 匿名在线内容源、
自更新与稳定通道、整合包导入逐文件决策、实例级启动参数),删掉「没有自动更新通道」「导入只有
两份全局策略」两条过期说法;`release-notes.md` 写 0.2.3 的真实更新摘要(保留 {{VERSION}} /
{{SUMS}});`packaging/README.md` 与 `docs/build-and-ci.md` 里「push 默认仍 prerelease」的旧口径
全部改掉。
2026-11-07 18:56:01 +08:00
AskaEth 5d5fe8aa73 Merge branch 'feat/m162-onlinecontent-modrinth-online-conte'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-07 17:54:42 +08:00
AskaEth c319f9dbfd fix(m162-rebase): M161 合入后三处越线文件回到 400 行内
rebase 到 0241f79(M161)之后,两侧各加了几行,这三个文件同时越过 400 行硬线
(guards.yml::file-size 会红)。处理原则:**不删语义、只搬位置**。

- `app-iced/src/pages/content/parts.rs` 410 → **240**:把状态层(loading / empty / failed /
  offline / centered / status_body)搬到新文件 `pages/content/parts/status.rs`(200 行)。
  状态层本来就是「一屏只有一个」的独立东西,拆开后 parts.rs 只剩筛选条、列表、图标与排序文案。
- `presenter/src/dto/events.rs` 402 → **399**:`CoreEvent::ContentDetail` 的文档由 4 行压成 1 行
  (字段说明一个不少,`project` 是请求时选中的 id 这句保留)。
- `presenter/src/state.rs` 404 → **400**:把 `ContentFailed` / `ContentDetail` 两个事件的落地
  搬进 `state/content.rs`(`apply_content_failure` / `apply_content_detail`),state.rs 只剩
  一行投递——与既有的 `apply_content` 同一个形状。

`state/content.rs` 58 → 64(两个新方法),仍在 400 内。语义零改动:只是投递点换文件。
2026-11-07 17:37:13 +08:00
AskaEth 47ed270293 fix(m162-r3): 金表条数与内容同源(堵住「少挂一段仍绿」的假绿)+ 补两条覆盖缺口
P2-1(r3 抓到的假绿):`golden::len()` 从「各段 len() 相加」改成 **`rows().count()`**。
r3 实测的变异是:删掉 `rows()` 里的 `chain(tail2::ROWS)` 后 `split_keeps_every_translation`
仍绿——因为条数来自一处手工聚合(仍含 tail2 ⇒ 459 == `Key::ALL.len()`)而内容只遍历
`rows()`(少了 213 行)。现在条数与内容同源,漏挂一段当场对不上。
- 变异复现(删 chain):`rows().count()=246 len()=246 Key::ALL.len()=459` ⇒ 用例
  **FAILED**(`left: 459 / right: 246`,「key 条数与拆分前不一致」);
- 恢复后:`rows().count()=459 len()=459 Key::ALL.len()=459` ⇒ **ok**;
- 断言一字未改(没有放宽成「只比条数」或「只比前缀」)。

顺带三条:
1. `golden.rs` 的 `rows()` 文档补上 `tail2`(并写明「加段必须也 chain 上,条数与内容只能从
   这一条序列导出」),与上一条同族。
2. `online/mod.rs` 模块表里 `[complete]` 的职责改成「按摘要认文件(`identity_of`)」,
   与「补全循环已删、生产在 `import::complete`」一致;批量 POST 那段也点明循环位置。
3. 补两条 pack 用例走不到的覆盖(`import/complete.rs` 的 `#[cfg(test)] mod tests`):
   - `an_entry_without_sha512_is_counted_not_guessed`:有 path、有 downloads、无 sha512 ⇒
     `unfilled += 1` **且一次请求都不发**(mrpack 读者强制 sha512,这条是防御分支);
   - `an_existing_download_list_is_never_extended`:已有直链时 `downloads` **逐字相等**
     (长度 + 内容),不再用「包含」这种测不出重复追加的断言。
   两条都验过能红:把「缺 sha512 不计数」与「总是追加直链」同时变异 ⇒ 两条 **FAILED**;
   恢复后 **ok**。

登记项(r3 指出的 scan 注记)也顺手收了:`Completion::note` 在 `filled == 0` 时改说
「按 sha512 从 Modrinth 补全 sha1:一条也没补到(补不出 N 条)」,并且不再带
「其余条目照旧按『缺省保留』处置」(那是导入场景的尾缀,预检里没有这回事)。用例
`offline_with_nothing_filled_does_not_say_zero_items_were_filled` 与
`import_online::only_a_real_offline_stops_the_completion_round` 都钉住「真断网 + 一条没补到」
既不说「补全了 0 条」也不说「缺省保留」。

全仓行数(guards.yml::file-size 同口径):**0 个超限**。
2026-11-07 17:37:13 +08:00
AskaEth ecd24f0608 fix(m162-r2): 金表沿用主线的拆分边界 + phase/pipeline 回到 400 行内
金表(rebase 到 M161 后重写这一段):**采用 main 的拆分边界**——`golden/tail.rs`
(`InstancesLaunchReset` 之后、`PackTargetNew` 之前)+ `golden/tail2.rs`
(`PackInstanceName` 之后),不再保留本分支原来那套 205/221 的切法。
本分支新增的 6 条 key(`ContentOffline{Title,Body}`、`ContentDetail{Fetching,FetchFailed,
VersionCount,VersionLine}`)按 `Key::ALL` 的顺序并入 main 的 `tail.rs`;
`golden.rs` 的 `rows()` 保持 `GOLDEN + tail + tail2` 三段串联。(`len()` 与内容同源的修正
在下一个提交里。)

其余两处(本来就是我推过线的,现在回到红线内):
- `import/phase.rs` 397 → **400**:只动注释(`Resolved` 的两个摘要字段共用一句、
  `resolve`/`settle` 的文档各并一行),代码语义零改动。
- `import/pipeline.rs` 399 → **400**:把补全注记(14 行)搬到 `import/complete.rs` 的
  `Completion::note(lead)` 旁边,管线只剩一行 `complete_and_note(...)`;
  `scan_pack_online` 也改用同一个 `note("预检前")`——注记文案由两处变一处。

同时删除与 `import/complete.rs` 重复的 `online::complete::{complete_files, CompletionSummary}`
(生产零调用方),只留生产在用的 `identity_of`。
2026-11-07 17:37:13 +08:00
AskaEth b394316df4 fix(m162-r1): 叶子成因穿出引擎 + 安装路径切 core 在线源 + 详情迟到结果闸门
P1-1(p1-1items 的根因):
- download/error.rs:`DownloadError` 增 `leaf: Option<DownloadErrorKind>` 与
  `effective_kind()`;`leaf_of()` 汇总规则只此一处:全 404 ⇒ NotFound、有
  Network/Timeout ⇒ Network、全 5xx ⇒ ServerError、混合按「网络错 > 5xx > 404」
  取最可诊断的那个(**不从 suppressed 文本猜状态码**)。
- download/bytes.rs:候选源循环收集逐源 kind,收尾时把叶子挂到 AllSourcesFailed 上。
- online/transport.rs:`From<DownloadError>` 改用 `effective_kind()`,于是
  NotFound/Server/Integrity 在生产路径可达(项目被删/id 拼错/限速不再画成「未联网」)。
- import/fetch.rs:新增 `FetchErrorKind::Offline`(真正的「没联网」)并与 Network 分开;
  `From<DownloadError>`/`From<OnlineError>` 都按叶子/大类映射。
- import/complete.rs:补全只在 `Offline` 时早停;404(真实 mrpack 大多托管在 CF CDN,
  Modrinth 必然 404)与 5xx 只影响那一条,不再掐掉整轮;注记「因未联网提前停止」因此
  只会在真断网时出现。

P2-1:presenter 安装路径不再自己拼 Modrinth URL / 自己解析 JSON——改用
`core::addon::online::Modrinth::versions` + `primary_file()` + `VersionFile::sha1/sha512`;
`HostTask::InstallDone.result` 改成 `Result<String, ContentFailure>`,
`InstallProgressView` 增 `failure_kind`(离线 ≠ 查无此项)。

P2-2:`app/update/content.rs` 把被详情方法抢走的「装入闸门」注释还回
`content_install_request`,详情方法只留自己那一行。

P2-3:`ContentState::apply_detail/fail_detail` 带上 project(事件与 HostTask 都携带请求时的
项目 id),迟到结果一律丢弃;`request_detail` 记下在飞项目,`detail_pending` 不再被迟到
结果清掉。

顺手项:`usable_hash` 让形状非法的那一档摘要被**丢弃**(sha512 仍校验,不再整条跳过;
两档都不可用才拒绝);`digest_file` 的 IO 失败先删落盘文件再返回。i18n 的
ContentOfflineBody 改成不暗示「有缓存」的措辞(golden 同步)。

用例:download/error.rs 叶子四档 + effective_kind;online/tests/classify.rs 5 条
(AllSourcesFailed+leaf 404 ⇒ NotFound、无叶子 ⇒ NotConnected 保守、叶子一一映射、
单源自身 kind、导入侧 404≠Offline);import_online 增「404 不停」「真离线才停」
(共 7 条);download 增「sha1 形状非法仍用 sha512 校验成功」;presenter 增详情迟到闸门。
2026-11-07 17:28:36 +08:00
AskaEth 81278f55af feat(m162): Modrinth 在线内容源(匿名)+ 远端条目按 sha512 补全 + 离线态
core:新增 addon::online —— 按 Modrinth v2 真实形状建模(search facets / project /
project versions / version_file by hash),网络出口只有 Transport 一个 trait,生产实现
EngineTransport 走既有 DownloadEngine::download_bytes(零新依赖);错误分大类,网络类
失败映射成 NotConnected(= 未联网),不再用空列表冒充「没有结果」。

core:online::download::download_verified —— sha1 交给引擎流式校验、sha512 落盘后复核
(download::HashAlgo 没有 Sha512,改它超出 scope,已登记 finding),不符/失败删文件重试,
最多 3 次尝试且不留半截文件。

core:远端条目补全 —— PackFetcher::modrinth_file_by_sha512(默认 Unsupported = 补不出),
import 管线与新增的 scan_pack_online 都会在「有 path 但缺 hashes.sha1」时按 sha512 反查;
补出的 sha1 走 M160 同一套 judge 与合并判定,补不出仍进 unknown_remote 计数(口径未改)。
DownloadEngineFetcher 改用 download_verified,FetchRequest 增 sha512。

presenter:Modrinth 检索/详情改由 core 的 online 客户端提供(不再自己拼 URL、不再读错
date_modified);ContentFailure{kind} 取代裸字符串,无网 = Offline;新增
ContentDetailRequest/ContentDetail + LoadContentDetail 命令与 CoreEvent::ContentDetail 通道;
预检改走 scan_pack_online;内容库安装也走 download_verified(sha1+sha512)。

app-iced:Status::Offline 独立一态(未联网 ≠ 空列表 ≠ 普通失败),详情面板消费新 DTO
显示可用版本;选中项目即拉详情。i18n 新增 6 个键并同步金表。

测试:online 模块 24 个离线用例(录制 JSON + 假字节:搜索/详情/版本/按摘要反查/校验失败
即删/重试/离线分类/补全),import 侧 6 个(离线计数不变、补全后判冲突、补错摘要 ⇒ Failed),
presenter/app-iced 各补失败分类与详情状态机用例。
2026-11-07 17:28:36 +08:00
AskaEth 0241f793b2 Merge branch 'feat/m161-uipolish-debts-sweep-update-keys-bu'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-07 17:27:39 +08:00
AskaEth 79b796fd58 fix(m161-r1): 崩溃/静默失败不再冒充「启动进程失败」;补三处假绿的守卫
复审 P1-1(用户可见假文案):`LaunchEnd` 在 `failure == None`(跑起来之后崩的)时
临时造了 `Stage::Spawn + format!("exit {:?}")`,于是磁贴写「启动失败 · 启动游戏进程」、
详情页漏出 `exit Some(1)`、静默失败丢掉「exit 0 却没起来」,而同一时刻 Toast 说「崩溃」。
- 新增 `LaunchFailureCause{Stage|Crashed|SilentFailure}`(dto/launch_failure.rs),
  `LaunchFailureCause::of(outcome, failure, exit_code)` 是唯一组装点;`InstanceRuntime::Failed{cause}`。
  `LaunchFailureStage` 保持「只装装配阶段」(文档那句「崩溃是结局不是阶段」现在成立)。
- 界面:`InstancesFailed`(框架)+ `InstancesCrashed` + `InstancesSilentFailed`;
  删掉 `InstancesFailedStage`。退出码按数字出词,不再有 Debug 文本。
- 用例:presenter 断言**枚举变体**(三种形状逐条)、app 断言**具体句子**
  (「不许出现 Some(」「进程起来了不许说启动游戏进程」「静默失败要点出没起来」)。
  mock 夹具与 launch_state_tests 的期望值同步;launch-error 三张图重拍。

复审 P2-1(`convert::failure_stage` 零覆盖):新增 `convert/stage_tests.rs` 5 条(三档 +
「既没回滚也没宣称干净」+ 非致命无阶段 + `failed_report_with` ⇒ NotStarted)。
变异实测:改成恒 `RolledBack` ⇒ 3 条红。

复审 P2-2(矮窗口用例的算式三个数全错、`body_height` 零测试):用例改成直接调
`frame::body_height`,chrome 从 `Ui::metrics` + `WidthClass::gutter` 现取,并把门槛与真实
chrome 绑死(chrome+MIN ⇒ Split、-1px ⇒ ScrollWhole);新增 `frame` 自己的 `body_height` 用例。
变异实测:`2.0 * gutter` → `gutter` ⇒ 2 条红。

复审 P2-3(源指纹闸门 / `Refused` 零覆盖):新增两条 core 用例——闸门(原地留着 prefs.json
+ 预先写好的同源收据 ⇒ Nothing 且收据未被重写,反证:源变了允许重放)、`Refused`
(`$schema` 1.7.0 ⇒ ReadOnly ⇒ 不动旧文件、不写收据、理由含「不可写」)。
变异实测:删闸门 ⇒ 1 条红;`can_save` 守卫改 `if false` ⇒ 1 条红。

顺手收:
- CLI `pack import` 那句「已整包回滚」也按三档出词(`RollbackStage` + `rollback_line`),
  加 2 条单测。CLI 不依赖 presenter,判定暂与 presenter 各一份 → 已 file finding 建议下沉 core。
- 设置页 `status_text()` 抽出 applying 档,用例断言「正在安装…」且非错误色。

金表:新 key 加在 `golden.rs`(81 行),未动 `golden/tail.rs`(211 行),避免与 M162 的拆分冲突。
2026-11-07 17:01:13 +08:00
AskaEth 6804b0ad1d docs(m161): 内容库筛选条「会自动换行」的注释改成如实描述(只改注释,不动行为) 2026-11-07 15:05:31 +08:00
AskaEth 03741ecd2d fix(m161): 收更新键/按钮/矮窗口/启动错误/导入结果五笔债
① update.auto_check / update.channel 登记进 core setting schema(1.6.0)
   - 新增 setting/keys/update.rs(两键 + UPDATE_CHANNELS,只允许 stable);
     SettingSchema 加 with_all 供域键表批量登记。
   - 新增 setting/update_prefs.rs:cache/update/prefs.json → launcher.json 的一次性迁移。
     四条硬约束:源指纹闸门(幂等)、不丢 false、新值胜出、旧文件改名 .migrated 后不再被读;
     主配置不可写时不动旧文件。收据 config/update-prefs-migration.json。
   - presenter Runtime 建会话时迁一次并把结局留在 migration();prefs 字段换成
     auto_check + 每次派发从主配置读 channel(不再硬编码 Channel::Stable)。
     保留旧字段 auto_check 的真实来源(原有测试 cases 会断言)。
   - CLI config get/set/dump 先跑同一次迁移(幂等),输出里给 migration 段与收据路径。

② presenter/dto/events.rs 的「本次会话不再弹提示条」注释改成真实语义
   (记的是版本号:同版本压住、更高版本照弹、手动检查清掉)。

③ 检查/下载进行中按钮禁用:pages/update/busy.rs 的 busy/can_check/can_apply;
   设置页两颗按钮改画禁用(不是消失),app 层 on_update_action 再挡一道
   (忙碌时 CheckNow/Apply 不派发);新增两条用例(含「忙完恢回可点」)。

④ 内容库低高度:content_layout::MIN_BODY_HEIGHT=460 + BodyMode(按固定件+三整行算出来,
   用例守着算式);frame::body_height 从 Ui::metrics 算正文可用高,pages::Model 带 window_height。
   ≥460 列表自己滚;<460 主列整列一个滚动条、列表按自然高度铺开。

⑤ 启动错误文案本地化:presenter 删掉 launch_summary / 「启动失败:」前缀 / 五条中文阶段前缀,
   改发 LaunchFailureView{stage,detail};LaunchFinished 改带 exit_code/uptime_ms/failure,
   Toast 由 app 层按 i18n key 现求(与 install_cancelled 同构)。InstancesFailed 拆成
   InstancesFailedStage(列表单行)+ LaunchStageFailed(详情/Toast 全文)。

⑥ 导入结果块:ImportReportView 加 failure_stage(NotStarted/RolledBack/RollbackIncomplete,
   由 core 的 rollback 机器位推出);标题、图标、底色三处分档,预检失败不再说「已回滚」。

顺带:六处注释/文档漂移(update.rs 的分工表与偏好落点、pack 的「已整包回滚」、
build_request 的 Errors、settings/update 的按钮禁用说明等),
以及为守 400 行把 update/cli-config/core-keys/presenter-dto/presenter-launch/i18n-golden
里贴线或越线的文件按模块拆开(纯搬移,行为不变)。
2026-11-07 15:02:13 +08:00
AskaEth b2d9f3927f Merge branch 'feat/m160-modpackpick-per-file-overwrite-deci'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-07 13:57:08 +08:00
worker-modpackpick d568cc6f37 fix(m160-r1): 预检失败不再静默空转 + 写侧条目数闸 + 判据归一 + 冲突页对齐
复审 reviewer-modpackpick-r1(p2-1items / fix-then-merge)的返工。

P2-1 预检失败被吞掉(错误呈现回归)
- host::import::spawn_scan 在 `scan.error.is_some()` 时**再发一条 PackEvent::Report**
  (`convert::failed_report_of` 把 core 的 ImportError 含 hint 逐字段搬过去),于是页面回到
  既有的「致命 —— 已回滚」结果块,不再落回入口态。
- convert 里把失败报告的组装收成一处(`failed_report` / `failed_report_of` → `failed_report_with`)。
- 证据:`a_failed_scan_still_reaches_the_screen_as_a_report` 断言**事件序列**
  `["scan-with-error", "report-failed"]` + 报告里 code/message 与预检同源(MultiMC
  `formatVersion: 2`,detect 认得、read 才失败);`a_clean_scan_does_not_emit_a_report` 是对照。
  变异验证:删掉那条 Report 发送 ⇒ 该用例红。

P3-1 写侧补条目数闸
- `WriteLimits` 增加 `entries`(生产口径 = `PACK_MAX_ENTRIES`);`preflight` 在**读盘之前**
  按「清单 1 条 + 覆盖文件 N 条」判 `count > limit`。
- 证据:`the_write_side_refuses_a_pack_over_the_unified_entry_count`(20001 条声明只放 1 个
  真文件 ⇒ 报 `pack_limits` 而不是 `pack_io`,即闸在读盘前)、
  `the_entry_count_gate_is_the_same_number_as_the_read_side`(两侧同一个数)。
  变异验证:注释掉 `guard_entries` 调用 ⇒ 前一条红。

P3-2 「内容相同」判据归一
- 删掉 `scan::judge` 里与 `merge::decide` 重复的 `disk.sha1 == pack_sha1` 早退,只留
  `decide().reason?`;两处文档互相指路(`merge.rs` 注明这是唯一判据)。
- 变异验证:把 `decide` 的相同分支改成"冲突" ⇒ **8 条**红(含 4 条 scan 用例与 merge 单测)。

P3-3/P3-4 口径与注释
- `TogglePick` 注释改成与代码一致(保留项留在表里,靠 `decisions()` 过滤)。
- `unknown_remote` 的计数与文案统一:判不了的两种来路(补全前没路径 / 有路径但缺 sha1)都算,
  注记载明两者;新增 `a_remote_entry_with_a_path_but_no_sha1_is_counted_as_unjudgeable`。

视觉(tower 看图给的三条)
- 冲突行两种取值都改成**同几何的 pill**(保留=描边、覆盖=tonal),各自右对齐进同宽度的槽 ⇒
  右边缘同一条线;路径列吃 `Fill` 把槽推到行右缘。
- 列表与动作行之间加 8dp 呼吸;分页行的 Previous/Next 用 `Space::Fill` 顶到右端,
  与内容列对齐(不再是右侧一大片留白)。
- 重新出图:C:/Temp/m160-shots/pick-light.png / pick-dark.png。

顺手
- `format/write.rs` 的单测拆到 `format/write/tests.rs`(纯搬移,守 400 行),并把里面两个
  自己拼 `%TEMP%` 路径的用例改用 `TempRoot`(断言红了也不留垃圾)。
- `import_decisions.rs` 的夹具拆到 `tests/decisions_support.rs`(纯搬移,守 400 行)。

证据:fmt 干净、clippy -D warnings 干净、`cargo test --workspace --all-targets -j 4`
= 2512 passed / 0 failed / **4 ignored**(51 个 test binary);跑前/跑后 `%TEMP%` 的
`ull-pack-*` 计数 1922→1922、`ull-pack-*-zip64-*` 0→0、`ull-import-*` 1360→1360(增量 0),
df 106G→106G 余量不变。
2026-11-07 13:52:44 +08:00
worker-modpackpick 4defc41e8d feat(m160): 整合包逐文件覆盖/保留决策 + 导入前冲突清单 + 限额口径统一
主任务(逐文件覆盖/保留)
- core: 新增 import/scan.rs —— 只读预检产出「冲突清单」(目标已存在且 SHA-1 不同才进),
  逐条给路径/来源/两侧体积与哈希;用户删过的、内容相同的、被叠层遮蔽的一律不打扰。
- core: 新增 import/decision.rs —— ConflictDecisions(整批档 + 逐项表,逐项优先),
  缺省档恒为 KeepUser;ImportRequest.conflict_policy 改由它承载,CLI 调用点不变。
- core: phase.rs 的 settle 按 path 取决策;非冲突路径不受决策影响。
- presenter: ImportScanView / ImportConflictView / ConflictDecisionsView(全部保留 /
  全部覆盖 / 逐项{default,items})+ PackCommand::Scan / PackEvent::Scan;host 新增只读
  spawn_scan,且新实例路径不再提前建目录(target_of 只算路径,prepare_target 才建)。
- app-iced: 导入前冲突清单页(分页 40 条/页、三入口、计数摘要、确认/取消、两行式行布局);
  预检干净时直接往下导,不打扰用户。出图见 C:/Temp/m160-shots/{pick-light,pick-dark}.png。
- i18n: 新增 18 条 en/zh 文案 + 更新「两者都留」那条口径说明。

限额口径统一
- pack/archive/limits.rs 成为唯一来源:PACK_MAX_ENTRIES/UNCOMPRESSED_BYTES/ENTRY_BYTES
  (20k / 512 MiB / 64 MiB);读侧 PackLimits::default 与写侧 WriteLimits::production 同源。
- 写侧不再直接用 zip::ZIP64_BYTES_THR;新增单条目闸,于是「能导出的包一定能导入」。
  不等式在 pack/tests/limits.rs 里用 const 断言钉死。

测试临时物泄漏(塔台追加项)
- addon/temp.rs:TempRoot —— Drop 即删(panic 展开同样删),Deref/AsRef 到落点路径,
  under()/at() 支持"子树落点、删父根"。形状照 core::update::tests::support::TempDir。
- addon::pack 与 addon::tests 两族全部改用 TempRoot(含 fixtures::Fixture 持守卫、
  multimc/fabric_instance 等返回守卫);4 GiB 稀疏文件的语义一字未改,只改"谁负责删"。
- 自证:pack/tests/mod.rs 三条用例(drop 删 / panic 也删 / 子树删父根)。

测试
- import_decisions.rs(11 例):预检清单、不打扰、逐项决策后的最终文件集、基线记包内哈希。
- roundtrip_three.rs(2 例):叠层 + 冲突夹具 import→export→import 三轮文件集逐字节一致,
  以及「选保留就按设计分叉」的反向证据。
- pack/tests/limits.rs(5 例):单条/总量边界、读侧用同一批常量、写出即可读回。

证据:cargo fmt --check 干净;cargo clippy --workspace --all-targets -D warnings 干净;
cargo test --workspace --all-targets -j 4 → 2507 passed / 0 failed(51 个 test binary)。
2026-11-07 13:52:43 +08:00
AskaEth a4f096eea3 Merge branch 'feat/m159-updategui-auto-update-client-wiring'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-07 13:35:22 +08:00
AskaEth 04ac90911e fix(m159): r1 复审返工 —— parse.rs 拆出子模块 + 五条 P3 真问题
按独立复审 `reviewer-updategui-r1` 的 p1-1 / fix-then-merge 返工。

P1-1(挡门)
- `crates/cli/src/cli/parse.rs` 406 → **382** 行:`fn parse_update` 搬到
  `crates/cli/src/cli/parse/update.rs`(与 `parse/{accounts,agent,instances}.rs` 同形状)。
  全仓 `crates/**/*.rs` 最大仍是 400(`design/tests/card_neutral.rs` 等三处,未越线)。
  上一轮报告里「所有改动文件都在 400 行内」那句是错的,已在 r2 报告里更正。

P3-6 `Job::apply` 不查包形态
- 新增 `self_replaceable(format)`(只有 `PortableExe` 允许)+ `ensure_self_replaceable`,
  **挡在取包之前**;错误分类 `InvalidManifest`(发布侧挂错,换源/重试无用)。
  比复审建议的 `needs_installer()` 更严:`portable-zip`/`appimage` 同样不能原地当 exe。
- 用例:`crates/core/src/update/tests/job_cases.rs`(msi/zip 被拒 + 目标零改动 + 暂存目录
  没被建 + 便携 exe 整环成功 + Nothing 不碰目标 + 闸门的源码锚点)。

P3-7 `ULL_UPDATE_DIR` 生产可用
- `default_source` 改走 `local_dir_override()`:`if !cfg!(debug_assertions) { return None }`,
  发布构建里整段被常量折叠掉(这条路径在 release 二进制里不存在)。判据与三档取舍写在
  `job.rs` 的文档表里(为什么不用 CLI 参数、为什么不用运行期开关)。

P3-1「忽略这个版本」语义
- `UpdateView.dismissed: bool` → `skipped: Option<String>`(记**版本号**):忽略 0.2.3 之后
  同一次运行里冒出 0.2.4 **照常提示**;手动检查清空(`begin_check(manual)`),静默检查不清。
  文案保持「忽略这个版本」。presenter / app 侧视图模型 / 两处用例同步。

P3-3 CLI `--json` 失败信封缺结构化分类
- `CliError` 加 `detail`(`Option<Box<Value>>`,装箱避免 `result_large_err`)+ `with_detail`,
  `to_json` 输出 `error.detail`;`commands/update.rs` 的 failure 填
  `{"kind": core 分类 id, "retryable": 可重试位}`。
- 上一轮报告里引的 `{"kind":"digest_mismatch"}` 形状确实不存在(那是 CLI 的 user/internal),
  r2 报告换成了实机 dump。

P3-4 update 命令没有解析测试
- `crates/cli/tests/parse_test.rs`:`update_subcommands`(check / apply / `--dest` 两种写法)
  + `update_usage_errors`(无子命令 / 未知子命令 / 多余位置参数 / 缺少取值 / 未知选项);
  `help_text_covers_every_command` 的 topic 列表补 `"update"`。

P3-5 `install.rs` 模块文档与实测矛盾
- 改成「可以改名、不能覆盖/删除」,写明 M159 在 Windows 真机验过运行中的 exe 自替换,
  并提醒别再照旧文档去设计「退出自身 → 另一个进程搬运」的双进程方案。

顺手(复审 P3-2/P3-8 里低成本的两条)
- 设置页补「下载中」态(`SettingsUpdateApplying` 不再是死 key,下载期间不画「下载并安装」);
  渠道显示改读 `update.channel`(`stable` 走 key,其它原样);删掉从未使用的
  `UpdateBannerRestartLater`(catalog + 金表同步,29 条 key 全部有引用)。
- `adopt` 只让**检查**刷新 `checked_at`(应用回报不再冒充「上次检查」);
  `UpdateView::default()` 的 `current_version` 取本进程版本号(mock 下不再空白)。

门禁(`CARGO_TARGET_DIR=C:/Temp/ull-target/wt-159`):fmt 绿、clippy -D warnings 绿、
`cargo test --workspace --all-targets --no-fail-fast -j 4` **exit 0**(51 个测试二进制全 ok,
`core::addon::pack` 未被 skip)。该族用例复现了复审记的泄漏:一次全量在 `/tmp` 留 72 个
`ull-pack-*`(`/tmp` 149M → 8853M),跑完已 `rm -rf` 清回 150M。
2026-11-07 13:03:18 +08:00
AskaEth 1c8f6aa1b3 feat(m159): 自更新 GUI 客户端接线 + CLI update check/apply 同一内核
D50 的「检查 → 提示 → 可拒绝 → 下载校验 → 下一次启动生效」整环接线,GUI 与
CLI 共用 core 的一条内核(`update::Job`),并顺带修掉 M157 最后一轮复审的三处 P3。

内核(core::update)
- 新增 job.rs:`Job { source, target, staging, dest }` + `check` / `apply`(复查 →
  取包 → 长度/SHA256 双检 → 原子替换),`default_source`(GitHub,`ULL_UPDATE_DIR`
  可切本地目录夹具)、`process_dest`、`staging_dir`。
- `mod.rs` 导出 job 一族;`source.rs` 为 `Box<dyn UpdateSource>` 补 trait 转发
  (壳层要运行期选源,`UpdateChecker` 是泛型的)。

presenter(宿主通道)
- `dto/update.rs`:`UpdateView` / `UpdateOutcome`(Idle/Checking/UpToDate/Available/
  MissingArtifact/Failed 六态,**失败与最新不折叠**)/ `UpdateBanner` 优先级判据。
- `host/update.rs`:Factory(生产 GitHub / 单测本地目录)+ core 结论 → DTO 的唯一映射
  + 更新域偏好(`cache/update/prefs.json`)。
- `host/update_state.rs`:Runtime(会话状态机:begin_check/adopt/begin_apply/dismiss/
  set_auto_check)+ `CoreHost` 派发与后台线程。
- `AppCommand::{CheckUpdate, ApplyUpdate, DismissUpdate, SetUpdateAutoCheck}`、
  `CoreEvent::Update`、`AppState.update`;`refresh_all` 按开关派发静默检查。

app-iced
- 设置页新增第 5 组「更新」(当前版本 / 渠道 / 自动检查开关 / 立即检查 / 上次检查与
  结果 / 下载并安装 / 就位后立即重启),走 `settings::view` 的 `update` 形参 + 按下标
  分发(与 M106 语言行同一取舍)。
- 顶部提示条(每页可见):有新版本(版本号 + 体积 + 说明摘要,可安装/可忽略)、下载中、
  已就位(立即重启);静默检查失败**不**弹条(原因进设置页)。
- `pages::Message::Update(UpdateAction)` → `app/update/update_action.rs`;「立即重启」
  拉同参数的新进程后退出(截图通道不重启)。

CLI
- `ull update check` / `ull update apply [--dest <path>]`:复用同一 `Job`;四种结论分开
  报(`--json` 的 `status`),失败退出码 1(`update_check_failed` / `update_apply_failed`)。

i18n
- 新增 `catalog/update.rs`(30 条,链尾追加)+ 金表对应 30 行。

M157 复审 P3 三件
- `app/update/instances.rs` 模块文档改成「清脏的唯一时机是回程快照等于表单」。
- `pages/instances/demo.rs` 的 `demo_form` 注释改成现状(两个参数框早已是单行输入)。
- `pages/instances/launch_args.rs`:门限不满足时只灰 `opengl`/`vulkan`,`default` 仍可点
  (它是「不下发」的安全档);判据抽成 `backend_chip_enabled` 并补 3 条用例。浅色 + 灰化
  两张图:`C:/Temp/m159-shots/instances-launch-args-{light,gated}.png`。

顺带:harness 支持 `ULL_SETTINGS_GROUP=<下标>`(拍第 5 组的验收图需要它)。

验证:cargo fmt --check 绿;cargo clippy --workspace --all-targets -D warnings 绿;
cargo test --workspace --all-targets 51 个测试二进制全绿(唯一例外:core::addon::pack
一族要造 4 GiB 稀疏文件,本机磁盘 99% 满 ⇒ StorageFull,与本次改动无关);CLI 用本地
目录夹具跑通「发现 9.9.9 → 下载 → 校验 → 替换运行中的 exe → 备份留旧文件」,并验证
摘要不符时拒绝应用、目标文件零改动、暂存目录零残留。
2026-11-07 12:09:42 +08:00
AskaEth a80362915a Merge branch 'feat/m157-instargs-per-instance-launch-args-a'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-07 11:28:53 +08:00
AskaEth ee67965b9b fix(m157): r4 修复未收集的周快照用例 + 两处 clippy 红,并补整环回归(复审 p1-2)
P1-1 crates/core/src/launch/overrides/tests.rs:modded_weekly_snapshot_needs_the_root_version_for_the_gate 原先写在 backend_allowed_combines_capability_and_threshold 函数体内(unnameable_test_items,永不执行)。移到模块层;移动后首次真正执行暴露出用例自身的 EmptyJavaPath(AdapterOptions 默认 java 为空),补 JavaSelection 后绿。--list 现在 10 条 launch::overrides::tests::(含该名字)。

P1-2 clippy -D warnings 两处:cli/commands/instance_launch.rs needless_borrows_for_generic_args(去 &);presenter/mock_data.rs items_after_test_module(mock_launch 移到 mod tests 之前)。

P3-a 新增整环回归 app/tests/instances_save_loop.rs:真 Message → dispatch → 后端回错误 Toast → pump → refresh_view → sync_launch_form,断言 4g 草稿与脏标记都还在。
P3-b LaunchForm::matches 内存改按数值比(解析失败退文本),补 memory_is_compared_by_value_not_by_text(04096 / +4096 / 清空三档)。
P3-c 门槛文案不再在 catalog 硬编码 26.2:InstancesLaunchBackendGated 改用 {min} 占位符,页面从 LaunchFacts::min_version(← core GRAPHICS_BACKEND_MIN_VERSION)代入;金表同步;灰化态截图重拍。
2026-11-07 11:06:18 +08:00
AskaEth 211e07b470 fix(launch): M157 r3 周快照门限按继承根版本 + 保存失败保留草稿(复审 P2-1/P2-2)
P2-1 周快照 + 加载器的 --graphicsBackend 静默丢失:presenter host::launch::build_request 与 cli snapshot::prepare 给 SnapshotInput.game_version 传 None,装配按带加载器后缀的 version_id(26w14a-fabric-0.16.5)判门限,GameVersion::is_weekly_snapshot 的 6 字符形态认不出来。两个调用点改为传继承链的 MC 根版本(25 行注释说明「门槛与拼装必须同一口径」)。回归:core launch::overrides 的周快照实际装配用例(正/负两档)、presenter host::launch::tests、cli snapshot::tests 各一条根版本断言。

P2-2 保存失败丢草稿:app/update/instances.rs 的 Save 分支派发前就把 launch_form.dirty 清掉,失败事件后 pump 用旧快照重建视图并覆盖表单。改为派发不清脏;LaunchForm::sync 只在「回程快照已等于表单」(= 落盘确认)时清脏并重播种,否则保留草稿。回归:failed_save_keeps_the_draft / confirmed_save_clears_dirty_and_reseeds / save_dispatch_keeps_the_dirty_flag。

顺带:launch::overrides 测试拆到 overrides/tests.rs(守 400 行)。
2026-11-07 10:36:37 +08:00
AskaEth c76a1437e6 fix(app-iced): M157 r2 详情列加滚动区 + 启动参数卡对齐信息卡(D30 复审)
- 详情列(含卡片与动作行)套 scrollable:1180×1000 下被裁的内容可达、滚动条与左列同一套几何;右侧 X2 内缩,卡片描边不再被滚动条压住
- 启动参数卡改用 frame::card(card_padding 21 + OutlinedCard 描边),标题 LabelMedium 不加粗——与相邻信息卡同款
- 段间距 X3→X2,卡片不再比内容长
- 演示夹具补 Game arguments(--fullscreen)
- presenter host 单测临时目录加序号(同一进程并行用例互删导致 flake)
2026-11-07 10:36:35 +08:00
AskaEth e2021645d7 feat(launch): M157 实例级启动参数(五项回落 + graphicsBackend 三态)
core: 新增 game/launch_overrides(LaunchOverrides 五项 All-Option + 三级回落 + 文本补丁校验)与 launch/overrides(拼装落位:全局 java.extra_args 进 user.jvm、实例 jvmArgs 进 override_jvm、gameArgs 进 user.game、memory 进 -Xmx、backend 进 --graphicsBackend 并复用既有版本门槛告警);实例 schema 登记 launch.* 五键。

presenter: dto/LaunchArgsView+LaunchArgsPatch,主机侧读/写与门槛判定(backend_allowed),AppCommand::SetInstanceLaunchArgs。

cli: 新命令 instances launch-args(读/写/--clear),launch 装配链消费回落值。

app-iced: 实例详情页「启动参数」区(JVM/游戏参数、内存、Java、三态后端 chips + 灰化提示 + 保存/恢复),表单状态归 App、i18n 中英文案同步。
2026-11-07 10:36:35 +08:00
AskaEth d68523b02a Merge branch 'feat/m156-termxmerge-merge-termux-glibc-into'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-07 09:38:20 +08:00
AskaEth 3b3430a93e docs(termux): 对外口径升级——CPU 软渲染 + 自编 Mesa GPU 两条路径各真机验证一次(用户拍板 A,四处同步) 2026-11-07 09:33:24 +08:00
AskaEth cb3ae8556b docs(core): install.rs 模块文档补上 Termux 分支与平台参数不早退
拆分后模块文档只描述归档链,读起来像「Termux 也走这条」;补一行说明 Termux 的系统包
分支在链之前,以及 `AdoptiumTarget::host()` 失败已改成每家一条降级原因(不再整体早退)。
2026-11-07 09:33:24 +08:00