Commit Graph
577 Commits
Author SHA1 Message Date
AskaEth 413553d3ea Merge branch 'feat/m207-uasweep-unify-user-agent-contact-in'
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-09 13:15:58 +08:00
AskaEth e3ae71ac37 M207: UA 收成一份带联系方式 + 两处字符串副本(gh_proxy 键、用例注释)
1) UA 只有一份定义:core 的 `download::http::USER_AGENT`
   (`Unlucky-Launcher/<CARGO_PKG_VERSION> (+仓库 URL)`,不用邮箱)。
   - 引擎侧:`EngineConfig::default().user_agent` 直接取它(config.rs 等量替换,仍是 400 行)
   - 带 header 侧:presenter 不再持有 UA 字面量,`get_with_headers` 发 `engine.config().user_agent`
   M205 复审核实过的缺口:Modrinth 收到的是引擎那份 `UnluckyLauncher/<ver>`(无连字符、无联系方式),
   带联系方式的那份只发给 CurseForge 搜索 —— Modrinth 的 UA 政策在真正会被限流的路径上没被满足。

   断言:core 一条钉形状(版本 == CARGO_PKG_VERSION、联系方式段 == 仓库 URL、不含 `@`);
   presenter 一条在回环上抓**发出去的字节**(两条路径的 UA 逐字节相同且 == EngineConfig::default())。

2) `app-iced/.../java/downloads/engine.rs`:`"download.gh_proxy"` 字面量改成 `KEY_GH_PROXY`。

3) `presenter/src/host/addon/http.rs` 的用例注释改成事实(Modrinth 走 core 的 EngineTransport,
   `get` 的无 header 分支生产零调用者),模块说明表格同步。

4) `docs/download-settings.md`:§6.1 / §6.1.1 / §7 按 M201(iced 真控件 + 快照通路)之后的现状重写。
2026-11-09 13:13:20 +08:00
AskaEth a853986283 Merge branch 'feat/m206-badgesel-current-item-marker-contra'
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-09 12:58:50 +08:00
AskaEth b7a1abb18a fix(m206): 「当前默认」改用选中的语汇,不再套禁用配色
Java 组用 `Chip::badge` 表示「哪个 JDK 是默认」,而 badge 走的是「无消息 ⇒ 禁用」
那条路(`OnSurface`@38% 叠 surface):M204 实测字/底只有 1.84:1(浅)/ 2.28:1(深),
成了整页最不可读的一段,而它正是「能不能改默认 Java」这个问题的答案本身。
同一张设置页里相邻的「下载」组表达「选中」用的是 `SecondaryContainer` +
`OnSecondaryContainer`(7.21:1)——两个相邻页面对同一个语义用了两套语汇。

改法(不动禁用语义):

1. 新增 `Chip::current(label)` + `paint::current_item`:**与选中 chip 同一套颜色**,
   但没有消息、不吃五态(没有可点性就没有反馈)。它不等于「未选中」,也不等于
   「禁用」——第三档,判据表写在 `components/selection/chip.rs` 的模块文档里。
2. `Chip::paint(&self, ui)` 成为「哪个档位 → 什么颜色」的唯一入口,`chip()` 用它求
   静息色,断言也用它——档位的判据只有一处,「改回旧配色」的变异会被直接抓住。
3. Java 页的动作位抽成 `view::action_chip`,当前默认走 `Chip::current`,其余仍是可点
   的 `Chip::filter(.., false, ..)`。
4. 下载组**不改**:它本来就是参照语汇(M201 已验收),改完两页逐字段同色。

断言(`pages/settings/java/tests/marker.rs`,浅深两档都跑):
对比度 ≥4.5:1(实测 7.21/7.21)、与下载组选中 chip 逐字段相等、以及反向断言
「禁用态仍走 38% 字 + 12% 容器且与当前项肉眼可分」。两条变异都在报告里复现过:
`action_chip` 换回 `Chip::badge` ⇒ 对比度断言红(只有 1.84:1);`Chip::current` 里
`marker: true` 改 `false` ⇒ 三条全红。

举证图(离屏 `--screenshot`,浅/深 + 改前/改后 + 下载组对照)与实测色值表见
`DevDocs/artifacts/m206/`(gitignore,不入库)。
2026-11-09 12:44:18 +08:00
AskaEth 46559910c9 Merge branch 'feat/m204-javagroup-java-runtime-settings-gro'
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-09 12:11:26 +08:00
AskaEth 8a050c4402 fix(m204): 补「扫描中/还没扫描过/正在取消」三格种子 + try_lock 说明对得上代码
视觉复审 r1 判 p2-1items:六张举证图里「取消」全程是禁用态,「正在扫描… 3/7」
一格都没拍,而 README 却写成「两态」——证据覆盖不到结论(与 M201 同一个坑)。
本提交补种子与拆文件,不出产品行为改动。

- `pages/settings/java/seed.rs`:新增 `scanning` / `unscanned` / `cancelling` 三档。
  三档都把快照写进 `ScanSlot`(新增 `ScanSlot::seed_report`)而不只改 `State.scanning`:
  `clock.advance` 每拍用槽里的快照重算 `scanning`,只改 State 的话第一拍就被推回「没在扫」。
- `pages/settings/java/scan.rs`:`ScanReport` / `Job` / `ScanSlot` / `run_staged` / `rows`
  从 `store.rs` 拆出(加 `seed_report` 之后那个文件 408 行,越过 400 行硬线);
  `store.rs` 用 `pub use` 再导出 `ScanReport` / `ScanSlot`,既有路径一个都没改。
- `app/view.rs`:`note_screenshot_group` 的锁从 `lock()` 换成 `try_lock()`——代码复审 r1 指出
  注释写着「拿不到锁就不打、不能把界面帧卡在锁上」,而实现用的是阻塞 `lock()`,只兜住中毒
  不兜住等待。选 try_lock 而不是改注释:注释承诺的保证应由代码提供,不能靠「今天只有一个
  调用点、锁里只有一次比较」这种会随改动失效的前提。
- `pages/settings/java/tests/cmdline.rs`:新增 `the_scan_seeds_cover_the_running_states`,
  逐档钉 `State.scanning` 与 `scan.report().running`(后者才是「心跳下一拍会不会把它推开」)。
2026-11-09 12:09:02 +08:00
AskaEth 92ab3d4ddf fix(m204): Java 组写盘成功后请 host 重发设置快照
`pages/settings/java/store.rs` 的 `save()` / `set_selected()` / `prefs_store::save()`
是页面自己的一次同步原子落盘(`SettingStore::set` 多个键 + 一次 `save()`),
不经过 `AppCommand::SetSetting` 的写盘分支;而 `CoreEvent::SettingsChanged`
全仓只有 `dispatch.rs` 的 `SetSetting` 分支与 `host/mod.rs::refresh_all` 两处发出。
于是三键写成功之后,`AppState::settings_groups` 里 java 组那几行会停在旧值,
直到下一次任何别的写入顺手带一份新快照回来。

修法用一条**动作键**(`dto::JAVA_REFRESH_ACTION` = `java.action.refresh`,
与 `download.action.detect_region` 同一族:不写盘、见到就返回 Ok):
写盘成功后派发它,`SetSetting` 分支照常重发快照。

不用 `RefreshAll`:那个除重发快照之外还会 `versions::spawn_list` 与
`start_update_check`——每点一次「保存内存」打两个网络请求(实测:一次真 host 出图
就在数据根里落了 `cache/meta/version_manifest_v2.json`),代价与动作不相称。

另外给 D30 §2.1 补一行自证身份的日志:`--screenshot` 且身份变化时打
`[shot] settings route=… group=… theme=…`(`view` 每帧都跑,所以只在变化时落一行),
免得再出现「拿外观页当下载组证据」那种图与结论对不上的事故。

守卫(`tests/java_refresh.rs`,4 条):
- 真 `CoreHost` + 临时数据根跑完整链:写盘 ⇒ 快照里 `java.extra_args` 真的变了;
- 被拒的保存不许请求刷新;
- 成功之后确实派发了那条动作键(键名写错就红);
- 「立即扫描」在 App 层真的接线(此前没有任何用例驱动 `Message::Scan`)。
2026-11-09 12:09:01 +08:00
AskaEth a48d055118 Merge branch 'feat/m205-debtsweep2-gh-proxy-assertion-user'
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-09 11:36:57 +08:00
tower-worker 67a03f9402 M205 r1: 修 config.rs 的假话与「最坏 9」的措辞,UA 说明改成事实
- download/config.rs: KEY_GH_PROXY 的「本文件没有断言把它与 schema 登记绑起来」是 M205 自己造出来的假话,改成指向 proxy.rs 的新用例(纯文字替换,400 行不变)
- addon/online/download.rs: 断言消息与边界段改说「出厂档 3x3 = 9;retry_limit 调到 N 时是 3xN(上限 10 => 30)」,别再让人把出厂值读成事实上限
- presenter/host/addon/http.rs: 「Modrinth 的请求走 get_via_engine」是错的(实际走 core::addon::online::EngineTransport -> download_bytes,带的是引擎 UA;get_via_engine 今天只有单测在用),改成事实
2026-11-09 11:20:27 +08:00
tower-worker d4aa3b23f4 M205: gh-proxy 键绑定断言、外层重试预算判据、UA 版本号来源、下载设置文档更正
- setting/keys/proxy: 新增断言把 KEY_GH_PROXY 常量与 schema 登记绑起来(键名 / Text{max_len:256} / 默认空串)
- addon/online/download: RETRY_ATTEMPTS 与引擎每源次数是两件事(层级相乘、管的失败类型不同),写清判据并用两条编译期断言钉住 3 与 3x3=9 的边界
- presenter/host/addon/http: UA 版本号改为从 CARGO_PKG_VERSION 拼出,补断言钉住版本 == CARGO_PKG_VERSION 且联系方式段不丢
- docs/download-settings: §3.1 四条 CLI 命令都吃设置(update 是自更新路径),§6 补 M11-M13 三条 CLI 变异,§1/§7 里 gh_proxy 已合入的事实
2026-11-09 11:12:59 +08:00
AskaEth 4c29cbac3e Merge branch 'feat/m202-settingsflow-iced-settings-data-pat'
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-09 10:53:32 +08:00
AskaEth 5dda9ac31d M201 r2: 修视觉 r1 的两条 P2 + 代码复审的坏值提示 p2
视觉 r1(fix-then-merge):
- P2-1 分隔线贴字:downloaded 组的行间距从 0 改成 `Space::X3`(12dp)。实测
  说明末行→分隔线 1–3px → **12–13px**、分隔线→下一标题 4–5px → **12–17px**
  (同族页 9–18px;同一把尺子量演示回退卡 16–19px)。不动 `frame::divider` /
  `demo_card`;`spacing` 只作用于子节点之间,卡片首行顶距与末行底距仍是 20。
- P2-2 孤字「效」:`with_note` 把「原值 X 已按出厂 N 生效」从说明段落里挪到
  **独立一行**(`\n` 分隔,渲染器按行断)。浅/深/窄三张复验:无 <20px 窄行。
  两种措辞(出厂 8 / 已按 1)一字未改,presenter 侧新增 `ends_with("\n原值 …")`
  断言把「独占一行」钉住。

代码复审 p2(非整数坏值被静默丢掉):
- `raw_int`(`Value::as_i64`)→ `raw_text`:文件里显式写过的原值转**上屏文本**
  (字符串取原文不带 serde 引号、数字/布尔 `to_string`、`null` 当没写)。
  于是 `"auto"` / `8.5` / `true` / `-4` 都能进那句提示,不再「画着 8 却说不出
  用户写的 auto 被忽略」。
- 新用例 `every_bad_value_shape_says_what_the_raw_value_was`:负整数 / 字符串 /
  浮点 / 布尔 / null 五种形态。

验证:fmt --all --check / check -p unlucky-app-iced --all-targets /
test -p unlucky-app-iced(845+6) / test -p unlucky-presenter(316) /
test -p unlucky-i18n(36),全部 EXIT=0。
2026-11-09 10:51:16 +08:00
AskaEth ab573838f3 M201: 接通 iced 设置页的数据通路(下载组:真值 + 真控件 + 写回闭环)
M193 的用户投诉「设置页里改了不生效」的结构性根因是 iced 侧没有设置数据通路:
`CoreEvent::SettingsChanged` 在 `crates/app-iced/src` 零命中、`pages::Model` 没有设置快照、
`download::State` 是空 struct。presenter 那套设置组早就是真值来源(`host::settings::groups`
由 `refresh_all` 与 `dispatch` 的 `SetSetting` 分支发成 `SettingsChanged`),只是没有接到 iced。

这条线现在通了:

    host::settings::groups → SettingsChanged → AppState::settings_groups
      → pages::Model.settings → settings::States.settings → 下载组控件
      → AppCommand::SetSetting → 写盘 + 重新发 SettingsChanged(闭环)

主要改动
- presenter `host/settings/download.rs`:并发/重试两行改成画**引擎生效值**(`DownloadTuning`),
  文件原值与生效值不一致时写明「原值 X 已按出厂 N 生效」(P3-5,两个数都现读);
  gh-proxy 行补「当前生效:跟随内置默认/已关闭/覆盖为 X」。
- app-iced `pages/settings/download.rs`:`GROUP.body = Some(body)`,按 `SettingControl`
  画 choice(filter chip)/ text(输入框 + 应用)/ action(按钮)/ toggle / readonly;写回走
  `app/update/settings/download.rs` → `AppCommand::SetSetting`(不做乐观回显)。
- `pages::Model.settings` + `settings::States.settings`:快照进页面的唯一通道。
- i18n:新增 `SettingsDownloadsApply`(金表锚点追加,`split_keeps_every_translation` 绿)。
- 取证:`ULL_SCREENSHOT_REAL_HOST=1` 让 `--screenshot` 接真 host(截图通道默认强制 mock,
  而 mock 不发 `SettingsChanged`,真值屏拍不出来)。

测试(新增 11 条)
- presenter `download_tests.rs`:P3-5 的越界回落 / 0→1 / 无差异不出提示 + P3-3 的
  gh-proxy 文件→三态端到端链。
- app-iced `app/update/settings/download_tests.rs`:假后端闭环 ×4 + **真 `CoreHost` 端到端**
  (点 16 ⇒ 磁盘上 `launcher.json` 真的有 16)。
- `pages/settings/tests/download_section.rs`:`body.is_none()` 绊线翻成 `is_some()` +
  控件形状 / 空快照不编值。
2026-11-09 10:51:16 +08:00
AskaEth 0cc1a35420 Merge branch 'feat/m203-clituning-cli-download-tuning-from'
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-09 10:24:51 +08:00
AskaEth 7995f62908 fix(m203): 给第 4 条吃设置的路径(ull update)补打点,并清掉 KEY_GH_PROXY 的腐化 doc
复审 r1 P2-1(p2-1items / fix-then-merge):`ull update` 复用 `install::build_engine` 后成了第 4 条
吃设置的路径,但没有任何用例钉它——把 update 换回裸引擎,四条 download::tests 全绿。按 pack 的完全
同形接缝补上:

- `commands/update.rs` 新增 `pub(crate) fn engine(ctx)`(`job()` 改调它),doc 写明这条路径上并发
  不可观测(自更新只下一个 artifact),随设置变的是每源重试次数与 gh-proxy 前缀;
- `download/tests.rs` 新增 `update_engine_takes_the_concurrency_from_the_setting_store`:
  写 concurrency=4 ⇒ 断言 `config().max_concurrency` 与 `concurrency_probe()` 都是 4。

顺带收掉复审的三条观察:
1. 新增 `the_gh_proxy_setting_reaches_the_candidate_chain`:镜像优先地区档案下,出厂(空串 = 跟随
   内置)确实生成 gh-proxy 候选;手写 `gh_proxy: "off"` 后候选里没有 gh-proxy(读的是 `plan()` 的
   候选链,不是配置值;两段式,避免「本来就没有候选」的白绿)。
2. 测试写侧改用 `ctx.setting_path_for(KEY_CONCURRENCY)`(`ull config set` 用的那个表达式),不再与
   实现读侧的 `launcher_config()` 共用同一个表达式——实现改读别的文件时用例会红而不是一起漂移。
3. `core/download/config.rs` 的 `KEY_GH_PROXY` doc 已腐化(写「暂未登记、M197 才登记、别删这条弱
   断言」,而该键在 main 上就已登记、本文件根本没有针对该常量的断言):改成事实,并保留「弱在哪」
   ——常量值与 schema 登记没被断言绑在一起,改名只会让宿主侧静默退化成 Builtin。

变异验证(只改实现侧,逐次还原):
- update::engine 换回裸 `EngineConfig::default()` ⇒ 只有 update 那条红(left 8 / right 4),另五条绿;
- `download::tuning` 不读 `KEY_GH_PROXY`(传 None)⇒ 只有 gh_proxy 那条红(off 档仍有
  `https://gh-proxy.com/...` 候选),另五条绿;
- `gh_proxy_override("")` 返回 Off ⇒ gh_proxy 那条的「出厂该套内置前缀」与回落用例的
  `tuning == default` 同时红(两处都该红)。
2026-11-09 10:07:41 +08:00
AskaEth b31ca636d1 feat(m203): CLI 三条下载命令改吃 download.* 设置(并发/重试/gh-proxy)
`ull install` / `ull java` / `ull pack` 此前各自 `DownloadEngine::new(EngineConfig::default(), …)`:
用户在设置里配的 `download.concurrency` / `retry_limit` / `gh_proxy` 一概不生效;M193 把出厂并发
从 64 改成 8 之后,它们还静默跟着降了。现在三处都经 `crates/cli/src/download.rs::engine(ctx)`:

- core:`shared/tuned.rs` 新增 `engine_with_tuning(cache_root, tune)` = `set_tuning` + `engine_for`,
  由 `shared.rs` 重导出;`engine_for` 的签名与行为一字未改(presenter 的两步语义原样保留)。
  放子模块是为了不动 `engine.rs`(那是 400 行零余量),`shared.rs` 仍 398 行。
- CLI:`download::tuning(ctx)` 用 `SettingStore::load(ctx.launcher_config(), launcher_schema())` 读
  三个键(与 `ull config set` 同一份文件),默认值/夹取/回落在 `DownloadTuning::from_settings`;
  键缺失或坏值静默回落出厂值,`engine()` 没有错误通道,安装不会因此中断。
- 顺带:`ull update` 复用 `install::build_engine`,也随之吃到设置。
- 测试:`crates/cli/src/download/tests.rs` 三条命令各一条(读 `config().max_concurrency` 与
  `concurrency_probe()` 的真实值)+ 一条「缺失/越界/负数/类型不对 ⇒ 静默回落出厂值」;
  `shared/tuned.rs` 两条守住「灌调优与取引擎是一步」。变异实测:pack 换回裸 `EngineConfig::default()`
  ⇒ 只有 pack 那条红(left 8 / right 2)。
2026-11-09 10:07:41 +08:00
AskaEth c97630d59e Merge branch 'feat/m204-cleanupdoc-debt-sweep-four-small-it'
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-09 09:58:56 +08:00
AskaEth 8e37bb9cf9 docs(packaging): §12 文件清单里本文的节号范围补到 §11–§16
复审观察 1:那张表还写「索引 + §11 / §12 / §13」,而本文现在也有 §14/§15/§16(含本次新编的
§16)。是改动前就存在的陈旧,顺手补全。
2026-11-09 09:49:44 +08:00
AskaEth 3b5f95375d docs(presenter): 「抓不住」那段别再点名 signature_cases.rs 管 manifest_sha256(复审 P2)
信封字段与钥表确实在 core/src/update/tests/signature_cases.rs;但 manifest_sha256 那条冗余校验
(signature.rs:161-171,要求「钥已钉住 + 方程验过 + 声明 sha 不同」)全仓没有任何用例覆盖,
现有篡改用例都先死在方程上。原文把这一项也推给那个文件,等于把责任推给了不存在的守卫。

改成如实写「全仓无用例,已立 finding」。只动 doc,用例逻辑与断言未改(394 → 395 行)。
2026-11-09 09:49:41 +08:00
AskaEth ba96a4fc94 docs(presenter): 更正 ATTEMPTS 注释里「拿不到宿主设置」的假话(复审 P1)
原文把「没读设置」写成了物理限制。事实:get_with_headers 手里就有 env,第一句 engine(env)
已经走过 apply_tuning(host/settings/download.rs → core/download/shared.rs),用户当前值就在
engine.config().max_attempts_per_source。这条路径之所以恒为出厂默认,只是因为它没读那个字段。

只改注释,行为不变(仍是编译期 const)。同时把不对称写清:另一条无 header 的路径走引擎,
本来就跟随 download.retry_limit(源冷却中时引擎给 1 次,那是恢复探测)。
2026-11-09 09:49:36 +08:00
AskaEth 133f14ba0b test(presenter): 坏清单用例按错误分类区分「JSON 坏」与「签名被拒」
台账 §10 第 7 条 / M199 复审 P3-2:原来只断言「结果是 Failed」,而它喂的每一份清单都签对了
——验签一旦 fail-open 或签名错误被误分类,这条用例照样绿。

现在两组各钉自己的分类:
- 内容坏(5 个变体,签名都对):kind 必须是 invalid_manifest / invalid_version;
- 字节好、只有签名对不上(放行就会报「有更新 0.9.0」):kind 必须是 signature_invalid 且不可重试。

变异证明(只改实现侧,跑完已还原):
- A core::update::signature::verify_manifest fail-open → ② 分支红,实际 Available(0.9.0);
- B core::update::manifest::from_json 把解析失败误分类成 SignatureInvalid → ① 分支红,
  「not json:坏内容分类是 signature_invalid」。
两个变异各只打红对应分支,说明两类坏输入确实各走各的分类。

文件仍在 400 行红线内(394 行)。
2026-11-09 09:32:58 +08:00
AskaEth 06fcbb8a51 docs(packaging): 修掉两个 §14 撞号(M190 让位到 §16)
台账 §10 第 5 条:packaging/README.md 里 §14 被 M169(更新链路测试通道)与 M190(产物署名与
清单签名)各用了一次。

判据(谁后插进来谁让位):§14 与 §15 都被仓库里的引用钉着——packaging/check_test_source_off.py
引 §14(M169),.github/workflows/release-build.yml 与 crates/app-iced/src/platform/win32/console.rs
引 §15(M196);这两处引用都在本 mission 的 scope 外。所以 M169 保持 §14、M196 保持 §15,
M190 那一节整段后移到文末编 §16,内容一字未改,只改编号与位置(新增一段「编号来历」说明为什么
它让位)。全文 §11/§12/§13/§14/§15/§16 各一次,无重复。
2026-11-09 09:32:54 +08:00
AskaEth 9163054d46 refactor(presenter): 带 header 的请求重试次数改引 core 的单一来源
台账 §10 第 4 条:http.rs 的 const ATTEMPTS: u32 = 3 是「数字 3 的第二处副本」。
改成引用 unlucky_core::download::config::DEFAULT_MAX_ATTEMPTS_PER_SOURCE(download.retry_limit
的 schema 默认值同源),core 侧一个字未改。

这条带 header 的路径(只有 CurseForge 用)拿不到宿主设置,所以只吃出厂默认——这一点写进了
doc 注释,免得下一个人以为它会跟随用户的 download.retry_limit。
2026-11-09 09:32:51 +08:00
AskaEth 56402d97a1 Merge branch 'feat/m201-cfgkeyring-compile-time-strip-of-th'
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-09 08:37:18 +08:00
AskaEth 50d13d26ab fix(m201): 测试钥改由 #[cfg] 编译期剔除,无测试档产物里不再留那段字节
`keyring()` 原先用 `cfg!(...)` 判断要不要并入测试钥 —— 那是「运行期值为假的分支」,
`TEST_ONLY` 的字节照编进产物(实测:无 feature 的 dev / release 两档 core rlib 里,
测试公钥十六进制与 key_id 都能 grep 到)。

* `TEST_ONLY` 拆成两条同名 const:`#[cfg(any(test, feature = "update-test-source",
  feature = "update-test-key"))]` 的真条目 + `#[cfg(not(any(...)))]` 的空表;
  `keyring()` 删掉 `cfg!` 判断,改成无条件 `extend_from_slice(TEST_ONLY)`(空表那档等于
  什么都没并,`mut` 两档都有用武之地,不需要 `unused_mut` 口子)。
* 文档把 M199 降级掉的那句强断言改回真话,并贴出六行实测读数(dev / release 均 0 命中,
  带 `--features update-test-key` 的对照臂 2 命中)。
* 明确划界:读数只到 rlib 为止;最终 exe 经 `lto = "fat"` + `strip = "symbols"` 之后是否
  仍有那段字节**未验**。

验收:cargo fmt --all --check → 0;cargo clippy --workspace --all-targets -- -D warnings → 0;
cargo test -p unlucky-presenter → 311 passed / 0 failed(1 ignored);
cargo test --workspace --all-targets → 0。
2026-11-09 08:19:04 +08:00
AskaEth de41e4c3b1 Merge branch 'feat/m200-presenterfix-sign-manifest-fixtures'
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-09 07:56:36 +08:00
AskaEth 540b82e197 fix(m200-r1): 文档降到实测能撑的程度 + 守门收紧 core 声明内容(复审 P2-1 / P3-1)
复审 reviewer-presenterfix-r1 的 fix-then-merge 两条,逐条落地。

P2-1(a)「两档」措辞被本次提交自己作废 ⇒ 三处全改成三档:
* `crates/core/src/update/trust.rs` 模块文档(读者最先看到的那句,上次漏了)
* `crates/core/src/update/tests/support_sign.rs` 模块文档 + `TEST_SIGNING_SEED` 的文档

P2-1(b)「三者都进不了发布产物」被复证实测证伪(`cfg!()` 是运行时假分支,不是 `#[cfg]`
编译期剔除;无 feature 的 dev 与 --release 两档 core rlib 里公钥十六进制与 `test-only-key` 都在)
⇒ 降级到事实能撑的断言:「不会被 `keyring()` 并进信任表,正式构建永远不认这把测试钥」,
并明写「字节仍编在产物里」「字节级不在产物里未证、也未声称」「换 `#[cfg]` 是另一条结构性改动」。
没有换成 `#[cfg]`(那是复审建议另开的 mission)。

P3-1 守门对 core「自己的声明」只认形状 ⇒ 收紧为只许空数组:
`DECLARATION` 从 `^\s*update-test-key\s*=\s*\[` 改成 `^\s*update-test-key\s*=\s*\[\s*\]\s*$`,
并加一条自检向量(core 声明里偷偷带子 feature ⇒ 必须红)。`--self-test` 从 6 条变 7 条
(5 红 / 2 绿)+ 基线,`packaging/README.md` 的计数同步。

⚠ 一个自己踩出来的坑,已被既有守门抓住:收紧时我先在 `check_test_key_off.py` 里把那条更宽的
测试通道 feature 写成**字面量**(自检向量 + 文档表格),`check_test_source_off.py` 立刻红
(`packaging/**` 按字面扫,这次提交前它一直是绿的)。改法照该文件自己的声明办——用
`OTHER_FEATURE = "-".join(("update", "test", "source"))` 拼出来,源码里字面量为 0,
自检向量仍然构造出完整字符串并必须判红。

验证(均在最终树上):`cargo test --workspace --all-targets` 0、`cargo fmt --all --check` 0、
`check_test_key_off.py --self-test` 0(7 向量)/ 正式扫描 0、`check_test_source_off.py` 0、
`check_workflows_yaml.py` 0。
2026-11-09 07:56:33 +08:00
AskaEth 528f5706a0 docs(m200): 补 update-test-key 的两处文档一致性(cherry-pick 之外)
`80b291c` 原样落盘(与本提交前一个 commit 逐字节相同),这里只补两处**文档一致性**,
不改任何判据、不改任何 feature 行为:

1. `crates/core/src/update/trust.rs`:`TEST_ONLY` 的文档注释还写着「**只**在 `cfg(test)` 与
   `update-test-source` 构建里存在」——同一文件里 `keyring()` 新增的表已经把三档(`cfg(test)` /
   `update-test-source` / `update-test-key`)讲清了,上面这句变成了自相矛盾的旧口径。改成三档,
   指向 `keyring()` 的说明。
2. `packaging/README.md` §12「文件清单」:新守门 `packaging/check_test_key_off.py` 已接进
   `release-guard.yml`,但清单表里没有它(同类守门 `check_test_source_off.py` 在表里),补一行。

验证:`cargo test --workspace --all-targets` exit 0(311 passed)、`cargo clippy --workspace
--all-targets -- -D warnings` exit 0、`cargo fmt --all --check` exit 0、
`packaging/check_test_key_off.py`(自检 + 正式扫描)exit 0、`packaging/check_workflows_yaml.py`
exit 0。
2026-11-09 07:56:33 +08:00
AskaEth 19cc5c7950 fix(m190): presenter 更新链路用例补上清单签名(6 条既有测试回绿)+ 测试公钥守门
背景:M190 把「验签」做成了强制且唯一收口(`source::verified_manifest`,先验签再解析),而
`crates/presenter` 的更新链路用例只写 `manifest.json`、不写 `.sig` —— 它们写在验签之前,所以在
dev 上红成 6 条 `signature_missing`。真发布本来就必须带 `.sig`,所以修的是**夹具**,不是判据:
没有放宽任何断言、没有在测试里关掉验签、没有把 `available=true` 改成「接受 Failed」。

夹具(`crates/presenter/src/host/update_tests.rs`):
* `write_manifest()` 把清单**连签名**成对落盘(`dist/stable.json` + `dist/stable.json.sig`),
  `dir_source()` 改走它;三处直接写清单的用例(坏清单 / 空清单 / 缺本平台产物)也改走它。
* 签名用 `TEST_SIGNING_SEED` + `key_id = test-only-key`,信封形状与 core 的
  `tests/support_sign.rs` **同源**(文件头写明「改一处要改两处」;presenter 不依赖 core 的
  `#[cfg(test)]` 资产,也没把测试资产导出成生产 API)。

为了让这把**公开**测试公钥在 presenter 的测试构建里被认得:
* core 新增 feature `update-test-key`(**只**把 `trust::TEST_ONLY` 并进信任表,不开任何数据源开关);
* 只有 `crates/presenter` 的 `[dev-dependencies]` 打开它(库/发布构建拿不到);锁里只多
  `ed25519-dalek` 这一行(presenter 的 dev-dep,包已在锁里,无新包、无版本变动);
* 新守门 `packaging/check_test_key_off.py`(238 行,带 6 条变异自检)接进 `release-guard.yml`:
  core 的 `Cargo.toml` 只许自己的声明、presenter 只许 `[dev-dependencies]` 段内,其余(workflow /
  packaging 脚本 / 根 Cargo.toml / cargo 配置)出现即红 —— 这把钥进发布构建就等于「谁都能签出官方更新」。
* 没有用 `debug_assertions`/`cfg(test)` 旁路:那会让「测试构建的信任表和发布构建不同」变成一句注释,
  而不是一条门禁。
2026-11-09 07:56:32 +08:00
AskaEth 1f2d89c23e Merge branch 'feat/m199-javaguard-auto-scan-and-proc-kill-g'
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-09 07:20:18 +08:00
AskaEth aae9a233ff fix(m198): JAVAGUARD——首屏自动扫 / 子进程回收 / 参数长度三条守卫 + 设置页正文结构断言
- 首屏自动扫:删掉 `cfg!(test)` 分叉,生产入口在测试里真跑;新增
  `the_first_hydrate_starts_a_real_scan_by_itself`(删掉那一行、或把实参翻成 `true`
  都红)。顺手删掉恒为假的 `seeded && !live`:截图种子档在上方就 return 了,编译器也
  报 "assigned but never used"。
- proc:`kill()` + `wait()` 收进 `terminate()`(三个分支共用);用例先用同一条命令
  真跑一次证明子进程起得来(`spawn().ok()?` 的空跑假绿),并新增 Linux/Android 的
  僵尸回收用例(只 kill 不 wait ⇒ `/proc/<pid>` 留成僵尸)。
- java.extra_args:`MAX_JVM_ARGS = 1024` 镜像 schema,`Refusal::JvmTooLong` + 中英
  文案 + JVM 输入框变红;App 级用例断言回执是具名的「太长」而不是通用的「写不进去」。
- 设置页正文:判据①从「数 `stack = stack.push(` 的出现次数」升级成按角色断言
  (唯一来源 section_columns、`push(` 恰一次且实参是循环变量、尾表达式、禁
  extend/chain/once/concat/append),单行折叠变异体现在会红。
2026-11-09 06:46:29 +08:00
AskaEth 7f697f5762 Merge branch 'feat/m198-javadown-jdk-version-list-and-mirro'
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-09 06:28:22 +08:00
AskaEth 6e80fd3563 merge: origin/dev(1623f68)合入 M197 分支——解 golden/tail.rs 冲突
基线:合的是 **origin/dev**(`1623f68`)。`origin/main` 是 D41 的孤儿发布快照
(`d616d65` parents 为空),不参与合并。用 merge 而不是 rebase,保住三轮复审 clean 的 tip。

冲突只有一个:`crates/i18n/src/catalog/golden/tail.rs`(热点文件,多条 mission 都往这里加 key)。

解法(不重排别人的行、不改别人的译文):
1. 以 **dev 侧 398 行**为底稿(含其它 mission 后加的 key);
2. 把 M197 的 68 行(`SettingsJavaTooNew` … `SettingsJavaReasonInstallRootTooLong`)
   整块插在 `SettingsJavaReasonWriteFailed` 之后、M174 助手域(`SettingsGroupAssistant`)之前
   —— 即 `settings_java_entries` 那段之后,装配链的正确位置;
3. 两侧合起来 463 行会越过 400 行硬线,于是按既有先例把**M174 助手域起的那半截**
   (dev 的 260 行)整段纯搬移到 `golden/tail_ai.rs`:`tail.rs` 211 行 / `tail_ai.rs` 274 行,
   链序 `golden → tail → tail_ai → tail2` 不变,逐字节不删不改;
4. 解冲突时发现 dev 的 golden 行还是 M197 之前的旧文案(`SettingsJavaEmptyBody` /
   `SettingsJavaAddHint` 的反引号、`SettingsJavaNeverScannedBody` 的旧说法),已把这三条的
   金表行同步回域文件里的现行文案(`split_keeps_every_translation` 逐位置比对当场抓出来的)。

顺带(合并导致的越线):`crates/core/src/download/engine.rs` 两侧各 400 行、合并后 401
(我 +1 行 `kind: err.kind`,dev +1 行),把模块文档里那三行散文重排成两行(一字未删)→ 400 行。
同一次还把这文件里对 `MirrorTable::set_gh_proxy` 的旧描述改成 M193 的现实(那两个函数已被删)。

验证:fmt 0 · check(app-iced+core+i18n all-targets)0 · i18n 36 · engine_mock 8 · app-iced java 45 ·
core lib java 71 · setting 116 · app-iced lib 全量 832;全仓 file-size 守门脚本复制执行 → 超限 0。
2026-11-09 06:17:54 +08:00
AskaEth 1623f68483 Merge branch 'feat/m197-shellfix-gui-subsystem-and-cli-stdo'
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-09 06:01:42 +08:00
tower-worker 1764301cdf merge: 解 origin/dev 三处冲突(M182 / M184+M190 / README §14),两边都留
冲突(都来自别人后来改同一批文件):
- crates/app-iced/src/platform/mod.rs:保留 M182 的 HostOs / host·linux·macos 分册表 /
  plan_corner_for,同时保留 M196 的 `[`win32::console`]` 落点说明;把 M182 措辞里已经不
  成立的「唯一 unsafe 落点 / 只此一处」改成「两处落点(transparency + console)」
  (unsafe-scope 门禁自 M196 起期望集合就是两处,不改这句文档就是错的)。
  注意:`pub mod console;` 属于 `platform/win32/mod.rs`,本次解冲突时一度被误加到
  `platform/mod.rs`(`cargo check` 立刻报 E0583 文件不存在),已修回。
- .github/workflows/release-build.yml:M190 的「PE 版本资源读回」步、M184 的
  linux-x64 xvfb 冒烟步与首帧上传步**全部逐字保留**;M196 的 PE 子系统门禁作为**兄弟步**
  插在导入表门禁之后(两道 PE 门禁互不替代)。按块比对:dev 的 10 个步骤逐字未变,
  合并后 11 个步骤。
- packaging/README.md:保留 M184 的 §11 补充与 §12 行、M190 的 §14 签名段;§12 里
  check_publish_slots 用 dev 的 11 条版本、另加 M196 的 check_pe_subsystem 行;§15 归位在
  §14 之后。

行数:release-build.yml 合并后 413 行会撞 400 行红线(dev 侧本来 393),故把 M196 那一步
压到 6 行 + 1 空行(说明改由步骤名指路 packaging/README.md §15 与脚本 docstring 承载),
最终正好 400 行。

验证见 mission 报告:cargo check/test/fmt 全绿,两个 PE 门禁在同一批产物上都 exit 0
(子系统 GUI=2 / CLI=3,版本资源的 subsystem 读数同为 2 / 3)。
2026-11-09 05:49:38 +08:00
AskaEth 9bb9855392 Merge branch 'feat/m194-dlmsettings-wire-download-concurren'
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-09 05:36:38 +08:00
worker-dlmsettings 54dde7f494 fix(m193): 复审 r2——台账三处失效引用、M39 注释逐字还原、schema doc 数字补守卫
- P2-1:docs 的契约表把 retry_limit 范围从 (1,10) 改成 0–10(0 与 1 都 = 只试一次),
  与 §4 和 keys.rs 对齐;§2 与 §6 的守卫名统一成 the_schema_contract_is_single_sourced
  (原两条已合并),并把变异 M9(去掉 .max(1) ⇒ 红)补进 §6 台账。
- P3-4:keys.rs 的 M39(MCP server)既有注释按 origin/dev 原文逐字还原(含「同一安全策略、
  同一审计日志」);腾出的 1 行用「去掉 alias use、两处常量改全路径」换,没有压任何既有论证。
  比对:版本史 / D20 / M39 三段与 origin/dev 逐字节相同。
- P3-2:SettingSpec.doc 是 &'static str(concat! 插不进常量),描述里的数字是第三处第二来源,
  现在有守卫 the_key_docs_quote_the_real_limits(改 1~64 → 1~99 ⇒ 红,变异 M10 已跑)。
  §6.1.1 用表登记三处「数字的第二来源」现状。
- P3-1:docs 里那个游离反引号删掉。
2026-11-09 05:24:12 +08:00
worker-dlmsettings 94f56f9d9c fix(m193): 复审 r1 三条 P2——版本史还原、retry_limit 0 有诚实语义、装配点措辞更正
- P2-2:keys.rs 的 schema 版本史按原文逐字还原(1.4.0/1.5.0 的键名、1.2.0 的迁移口径、
  旧读新的完整结论),不再用版本文档换行数;腾行改从我自己新加的注释里出。
- P2-1:download.retry_limit 范围恢复 0~10,并把 0 的语义定成「只试一次」(attempts = max(1, 0)):
  老配置合法、零迁移、ull config get 读到的 0 与引擎真做的事一致(不再回落出厂 3)。
  schema 描述与 presenter/iced 两处 UI 都写清「0 与 1 都 = 只试一次,不代表不下载」。
- P2-3:shared.rs 的「唯一一处装配」更正为「presenter 侧唯一一处」(CLI 还有三处自建),
  docs 加「哪些路径吃这套设置」表,并立 finding(CLI install/java/pack 不读设置、并发 64→8)。
- P3-3:mirrors.rs 的 gh_proxies 注释改成「无生产读者,仅解析留档」。
- P3-4(presenter 侧):并发/重试描述的出厂值与上限改用常量 format!,不再是第二处字面量。
- 出图重拍(含 0/1 = 只试一次)。
2026-11-09 05:24:12 +08:00
worker-dlmsettings e8544b19a7 feat(m193): 下载并发/重试/gh-proxy 真接进引擎(schema 与引擎同源常量)
- download.concurrency / download.retry_limit 以前引擎侧零消费者(恒用
  EngineConfig::default() 的 64,而 schema 说 8)。现在默认值与硬上限收成
  download/config.rs 的常量,schema、EngineConfig::default()、Semaphore 容量
  三处同源;宿主把三个键翻译成 DownloadTuning 后经 shared::set_tuning(按数据根)
  灌进装配点,改了设置下一次取引擎就生效。
- download.gh_proxy 三态(留空=跟随内置默认 / off=关 / 前缀=覆盖)下沉到
  shared::build,所有走共享引擎的下载路径(内容库/版本清单/字体/安装)一致。
- 删掉 MirrorTable::set_gh_proxy 与 default_gh_proxy 两个改了没用的入口。
- 下载设置页把三条假演示值换成说语义的中文行(含三态说明与 1~64 硬上限)。
- 守卫:探针读 Semaphore 真实许可数(不是读配置),每条配变异证明。
- docs/download-settings.md 记下取证表(另四个键仍零消费,已立 finding)。
2026-11-09 05:24:11 +08:00
AskaEth 98262d3835 Merge branch 'feat/m196-straycard-settings-page-renders-for'
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-09 05:23:49 +08:00
AskaEth fb0b1a5311 merge(dev): 解 M196 与 dev(M179/M193)在 settings/mod.rs 的冲突
`origin/main` 是 D41 的孤儿快照(单提交、与开发史无共同祖先),M179/M193 这些提交在
`origin/dev` 上;按「跟上主线」的意图合的是 `origin/dev`。用 merge 不用 rebase:复审 clean 的
tip `e5a4b9b` 原样保留,本提交只负责解冲突。

唯一冲突:`settings/mod.rs` 里 `form` 的文档注释(M179 与 M195 重写过同一段)。两边都留:
- **M179/M193**:`type Body` 带 `Lang`、四条按下标分发(General / Update / Assistant / Java)、
  M193 的 download / logs 接线及其文档更新——只有一处措辞随之调整:「四组在下面各有 `else if`」
  → 「在 [`section_columns`] 里各有 `else if`」,因为 M195 把那层循环抽成了独立函数;
- **M195**:循环之后不再 push 那张「没有逐实例覆盖」的空态卡、`overrides_empty` 不存在、
  `section_columns` 与其行为断言(`frame.rs` 两条用例)。
合并后的注释同时说明两件事:`GROUP.body` 从形参拿生效语言(M179)+ 尾部不许再挂东西、
逐实例覆盖的真控件在实例详情页(M195)。

为守住 400 行硬线(冲突态 424 → 399),只压自己的重复叙述:M196 段压到 4 行、
`section_columns` 的文档改为指向 `form`(它原来重复了一遍分发点,且在 M179 之后已不准确)、
模块文档里一段被 M179 实现取代的历史「r3 订正」压成一句可执行的结论。

代码零语义改动(证据见汇报):`section_columns` 的循环体与 `origin/dev` 归一化后逐字节相同(46 行),
`GROUPS` / `GROUP_LABELS` 三方一致;相对 `origin/dev` 的差异只剩本 mission 的 4 个文件。
2026-11-09 05:18:29 +08:00
AskaEth 0e6d21ba89 Merge branch 'feat/m195-importcancel-import-pipeline-cancel'
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-09 05:07:04 +08:00
AskaEth 59011b2d56 fix(m195): 复审 r3 —— 失败不再被说成「取消太晚」,并给三档补推导用例
P2-1(界面撒谎):`cancel_view` 的 `_ if cancel_requested => TooLate` 把 `Failed` 也吞进去了。
「点取消的同时摘要不符」这类失败(取消置位之后、下一个检查点之前的非容忍失败)会得到
`status=Failed cancel=TooLate rolled_back=true`,于是同一屏上红卡说「已回滚 —— 目标目录零残留」,
下面还有一句「取消来得太晚:这次导入已经跑完,实例会保留,同名文件可能已被覆盖」——后半句是假的,
而且与红卡直接对立。改成**只有** `Success | Partial + cancel_requested` 才判 `TooLate`;
`Failed` 一律 `NotRequested`(失败卡自己用 `failure_stage` 分「没写过 / 回滚干净 / 回滚没做完」,
真相已经说全)。
另外 app-iced 的 `cancel_note` 加一道双保险:只有 `status` 是 `Success/Partial` 时才出那句话,
推导万一回归也不会贴到红卡上。

P2-2(推导零覆盖):此前只有「给定这一档、文案怎么画」的用例,把 `RollbackIncomplete` 的推导
删掉也全绿。现在 `convert/cancel.rs` 带三条推导用例:`Cancelled + !rolled_back ⇒ 回滚没做完`
(含对照 `rolled_back ⇒ 已回滚`)、`Failed + 取消 ⇒ NotRequested`(含对照 `Success/Partial ⇒ TooLate`)、
没点过取消 ⇒ `NotRequested`。

变异实测:
- P2-1:把 `Failed` 也判 `TooLate` ⇒ `a_failed_run_is_never_dressed_up_as_a_late_cancel` 红
  (`left: TooLate / right: NotRequested`,"失败的导入没有「跑完」这一说")。
- P2-2:删掉「回滚没做完」的推导(`Cancelled` 一律按已回滚)⇒ `an_incomplete_rollback_is_its_own_bucket` 红
  (`left: RolledBack / right: RollbackIncomplete`)。

验证:`cargo test -p unlucky-core -p unlucky-presenter -p unlucky-app-iced` = 2188 passed / 0 failed;
`-p unlucky-i18n` = 31 / 0;clippy 四 crate = 0 warning / 0 error。
2026-11-09 05:06:56 +08:00
AskaEth b1524635ba fix(m195): 复审 r2 —— 取消的真实结局必须到达界面(三档 + 终态穿透代次)
复审 r2 P2-1:界面无条件断言「已停手并回滚:没有留下半包,也没有覆盖已有文件」,而两条路径下
这句话是假的——①取消来得太晚(落在最后一个检查点之后)时管线已经跑完,用户的同名文件被覆盖、
实例已登记;②回滚本身没做完(`rolled_back=false`)。core 一直是诚实的,是 presenter 把报告丢了:
`cancel.load()` 早退 + `cancel_pack` bump 代次后 `handle` 按代次丢弃。

按"接通真相"做(不是改软话):

- **终态一定发出**:`import::run` 去掉 `cancel` 的无条件早退,收尾抽成 `finish`(清自建空目录 +
  无条件 `send(PackEvent::Report)`);取消标记在管线返回后**再读一次**,作为"太晚"的唯一证据。
- **终态穿透代次**:`HostPackState::terminal_epochs` 记下 `cancel_pack` 作废的那个代次,
  `handle` 对它**只放行一条 `PackEvent::Report`**(进度事件照旧按代次丢,`fresh_cancel` 起新
  一轮时清空)。这是路径 A 的成因与修法。
- **DTO 三档**(tower 批准扩 scope):`ImportStatusView` 加 `Cancelled`;新增
  `ImportCancelView { NotRequested, RolledBack, RollbackIncomplete, TooLate }` 挂在
  `ImportReportView::cancel` 上——已回滚 / 回滚没做完 / 取消太晚,三档各自可判。
  `PackEvent::Report` 到达时清掉 `ImportState.cancelled`(它现在只是"取消后还没有结果"的占位)。
- **界面按结局分支**:标题 `PackStatusCancelled`(已取消 — 已回滚)/ `PackCancelIncomplete`
  (已取消 — 回滚没做完)/ 取消太晚时保留真实状态标题并另起一句 `PackCancelTooLate`
  (实例会保留、同名文件可能已被覆盖);占位的 `PackCancelled` 改成"正在停手,真实结果随后给出"。
  图标:`Close`(描边,页面对填充字形有守门)/ 回滚不完整用 `Warn`,一档都不用 `Role::Error`。

守门与变异:
- 新增路径 A 用例 `a_late_cancel_still_delivers_the_real_outcome_to_the_ui`:真管线把取消置在
  **最后一条 Commit 进度**上(之后没有检查点)⇒ core 报 `Success`/未回滚、用户的 `mods/a.jar`
  确实被覆盖;再 `cancel_pack`(代次 +1 并记终态)→ `finish` → `handle`,断言界面收到
  `status == Success` + `rolled_back == false` + `cancel == TooLate`,且同代次的进度事件仍被丢。
- 变异①:`handle` 恢复"代次不符一律丢" ⇒ 该用例红("取消太晚时,真实结果必须到达界面")。
- 变异②:`finish` 恢复"取消就早退" ⇒ 该用例红(另带 P2-2 那条也红)。
- app-iced 新增 `model/tests/cancel.rs`:三档标题互不相同 + 「太晚」必须有那句补充说明 +
  已回滚不许显示成「部分成功」。

验证(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-194):
`cargo test -p unlucky-core -p unlucky-presenter -p unlucky-app-iced` = 2185 passed / 0 failed;
`-p unlucky-i18n` = 31 / 0;clippy 四 crate = 0 warning / 0 error。
2026-11-09 05:06:55 +08:00
AskaEth db5b08b9ab fix(m195): 复审 r1 三项 —— 解压检查点真守卫、presenter 接线测试、取消文案改回真话
P2-1(解压循环检查点没有真守卫):那条用例原来的断言(status/target/files/bytes/无残留暂存)
在删掉 `extract_overrides` 的检查点后**依然成立**——提交前检查点⑤会替它停一次,于是几 GB 的包
会一直解到结束而测试全绿。改成数 `ImportStage::Extract` 事件:有检查点 = 1 条、删掉 = 3 条
(两面实测)。模块文档同时如实登记"还没有真守卫"的检查点:②③④⑤、全新导入 commit 的取消
分支、phase.rs 远端下载逐条、complete.rs 补全循环的 break。

P2-2(presenter 的真令牌接线零测试):新增 `pack/tests/cancel.rs`,直接调 `import::run` 并把
传入的 `Arc<AtomicBool>` **预先置 true**,判据只能是盘上事实(目标目录不存在 + profiles.json
零实例)——"没收到 Report"那种判据两条路都成立,`run` 早退条件里的 `cancel.load()` 会把
"没接线"完全遮住。

P2-3(用户可见文案说反了话):`PackCancelled` 中英双语从「导入会在后台继续跑完 / Import keeps
running in the background…已经写下的文件会留下」改为「导入已取消 —— 后台已停手并回滚:没有留下
半包,也没有覆盖已有文件」;golden 金表同步;`model/tests.rs` 那条把旧文案钉死的用例改成钉新
语义(必须说清停手+回滚,且**禁止**出现 background);`model.rs` 的 `cancelled` 字段文档与
`dispatch.rs` 的取消注释同族修正。

验证:`cargo test -p unlucky-core -p unlucky-presenter` 1480 passed / 0 failed;
i18n 31 / 0;app-iced packui 28 / 0;三 crate clippy 零 warning。
2026-11-09 05:06:53 +08:00
AskaEth 1441a240fe feat(m195): 导入管线取消令牌与原子回滚(取消不是失败)
M190 复核 §3 A-2:导入管线没有取消令牌,「取消导入」只改界面状态,后台仍跑完并写盘
(半包 / 覆盖风险,丢数据档)。本轮把取消做成管线级:

- core 新增 `import/cancel.rs`:`ImportCancel` 令牌(可 `from_flag` 接管既有
  `Arc<AtomicBool>`)+ 内部 `Stopped::{Cancelled, Failed}` 两条停止路线;
- 结果类型单列 `ImportStatus::Cancelled`(`error` 恒为 None、退出码 130、文案 key 独立),
  与 `Failed` 是两条路——UI 不弹红、脚本不会把「改主意」记成「装坏了」;
- 七个检查点:入口 → 解析后 → 补全后 → 开暂存目录前 → 提交前 → 合并态逐文件 → 登记前;
  解压 / 远端下载各逐条检查;单条下载在飞不可中断(fetcher 无取消参数)已在文档里说准;
- 取消后不留半包 / 不覆盖已有实例:暂存目录整个删、合并态 journal 逆序回滚(备份改回原名)、
  落盘后取消连内容一起回滚;`files / conflicts / missing / bytes_written` 清零,不留假账;
- presenter:同一个取消标记现在真交给 core(`with_cancel`),取消后不再「后台跑完并登记」;
  CLI 增 `cancelled → 130` 一档与三档人读名。

守门三条各带变异证明(见 mission 报告):删回滚 → 红、删登记前检查 → 红、把取消并进
`fail()` → 红。

没验:超大整合包的真实 IO 下取消响应延迟(最坏 = 当前一条文件读完/下完 + 一次回滚)、
单条下载在飞不可中断、macOS / Linux 未跑。
2026-11-09 05:06:52 +08:00
AskaEth 3116f43c9f Merge branch 'feat/m192-retryui-surface-dependency-failures'
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-09 05:06:09 +08:00
AskaEth 476bf92f97 test(addon): 给「失败路径取名字」的生产接线与降级补守卫 (M191 r3)
复审 r3 p2:那两处都只靠消费层用例撑着,生产接线上删掉/改坏也全绿。补两条能真红的守卫。

**接线层**:`install.rs` 里那次 `names_for_incompatible(..)` 原来不可驱动。现在下载器与
取名字器从 `Ports` 参数进来(生产 = 引擎 + `plan::fetch_title`,用例 = 假实现),
于是用例能驱动**生产那个 `collect`**:一份「一个文件下成功 + 一个不兼容前置 + 一条 404」的
计划跑完,断言不兼容那条拿到了名字、且**只**给不兼容档问过一次。
变异(把那一行短路成空名单)⇒ `the_failed_install_path_really_fetches_names_for_the_incompatible_items`
红:`left: "P7dR8mSH"` / `right: "Entity Culling (P7dR8mSH)"`。

**降级层**:把「一次查询 → 有名字/没名字」的折叠抽成 `title_from(query, id)`
(`fetch_title` 只剩「建客户端 + 调它」)。参数化之后三种「没名字」都能离线钉住:
查询失败 / 空标题 / 全空白 ⇒ `None`,不 panic。
变异 ①`ok` 换 `expect` ⇒ `a_failed_or_blank_lookup_yields_no_name` 红
(panic:`带断网`);②去掉空串过滤 ⇒ 同一条红(空白档)。

**顺手**:`fetch_title` 真发一次 HTTP 的那一跳没进用例(本轮实测它确实取到了真数据
`P7dR8mSH → "Fabric API"`,但把网络调用放进 `cargo test` 会变脆,所以只把可坏的那一步——折叠——
参数化测掉,报告里如实写「HTTP 这一跳仍无用例」)。

**行数**:`install.rs` 加到 415、`tests/retry.rs` 到 436,都越过 400 硬线 ⇒ 两步纯搬移:
`collect` + `Ports` + `Placement` → `install/collect.rs`(117 行,`install.rs` 321);
r3 用例 → `tests/retry/wire.rs`(115 行,父文件 332)。
2026-11-09 05:06:06 +08:00
AskaEth 26e7b7333e fix(content): 不兼容档在失败路径上取项目名 + 重试轮不再贴错根的标题 (M191 r2)
复审 r2 两条 p2,都修了,并按复审给的口径验过红。

**P2-1(续):不兼容档不再只有裸 id**
`FailureKind::Incompatible` 的项目**确实存在**(站点只是没有匹配目标版本/加载器的条目),
所以失败路径上值得现取一次名字——`plan::names_for_incompatible` 按失败项去重、一项一次 GET
(`Modrinth::project`),`fetch_title` 把任何失败都折成 `None`:
**拿名字失败不许影响安装结果**,取不到 / 拿到空标题就退回 id。只有这一档发请求;
「取不到」档 404 那部分项目根本不存在,再发一次只会再吃 404。

**P2-2(新):重试轮把根的标题贴到了前置头上**
根因是判「这条是不是根项目自己失败」用了 `plan.request.roots.first()`,而重试那一轮的 plan
来自 `retry_request()`,它的 `roots` 就是上一轮的失败名单——第一个是前置。
现在判据改成 **id 比对**(`Root{id,title}`,id 由 `remember` 一起记下来):
`label_of` 只在这条失败的 id 真的等于原始根 id 时才用根的标题。复审给的那个形状
(roots=["P7dR8mSH","1eAoo2KR"] + root_title="Sodium")现在画 `P7dR8mSH`,不再是
`Sodium (P7dR8mSH)`。

**守门(每条都真跑红过,且在拆完模块的最终代码上复跑)**
- 去掉不兼容档的名字来源 ⇒ `an_incompatible_item_shows_the_project_name_once_it_is_known` 红
  (`left: "P7dR8mSH"` / `right: "Entity Culling (P7dR8mSH)"`)。
- `label_of` 换回 `roots.first()` ⇒ `the_retry_round_does_not_paste_the_root_title_onto_a_dependency` 红
  (`left: ["Sodium (P7dR8mSH)","1eAoo2KR"]`,正是复审实测的那一行)。
- 取名字不降级(`expect`)⇒ `a_failed_name_lookup_falls_back_to_the_id_without_breaking_anything` 红。

**行数**:`plan.rs` 加完这套到 414 行、`install.rs` 到 403 行,都越过 400 硬线 ⇒
把「名字从哪来」整块(`Root`/`Names`/`names_for_incompatible`/`label_of`/`fetch_title`)
纯搬移到 `plan/labels.rs`(111 行):`plan.rs` 336、`install.rs` 388。
2026-11-09 05:06:06 +08:00
AskaEth 961ff453a0 fix(content): 重试面板画可读名,不再只有站点 id (M191 r1 P2-1)
复审 P2-1:面板原来只画 `P7dR8mSH` 这种项目键,用户拿到的是一串哈希。
可读名其实都在手边,只是没进 DTO——现在补上,按复审给的**最小修法**做,没扩散:

- `InstallFailureView` 加 `label: String`(**语言中立**:站点标题或文件名),
  配 `line()` 统一决定「名字 (id)」还是「只有 id」(名字为空时不编造)。
  `detail`(core 中文散文)**依旧不进界面**——那会踩 i18n R2。
- `plan::label_of` 三个来源,零额外请求:
  根项目 → 请求里的 `title`;计划里有节点(下载失败)→ `node.file.filename`;
  都没有(解析 404 / 不兼容的前置)→ 空,界面退回只画 id(复审认这条兜底)。
- 计划被记住时连显示名一起记(`plan::remember` 多一个 `title`),于是重试那一轮
  面板头与失败名单沿用的是同一个名字(顺手收掉复审 N5 的「重试时面板头显示 id」)。
- N2:删掉新加的 `ContentInstallDismiss`,复用既有的 `DownloadDismiss`(同一句话不立两条 key)。
- 行数/clippy:`collect` 的参数捆成 `Placement{dir,title}`(9 个参数超限);
  `plan.rs` 加完 `label_of` 到 429 行,把「记住的计划」表纯搬移到 `plan/remembered.rs`(363 + 82)。

守门(真跑红再还原):
- 把 `label_of` 置空 ⇒ `the_retry_list_shows_a_readable_name_not_just_the_project_id` 红
  (三行全变成 `["AANobbMI","P7dR8mSH","1eAoo2KR"]`);
- 审阅指出的另一半判据也补上了:`Done` + 残留名单 ⇒ 必须不画;现在**只去掉**
  `stage != Failed` 那一半就红(之前只去掉它仍是全绿,报告的表述不准,已改)。
2026-11-09 05:06:05 +08:00