AskaEth
|
96f15c79fb
|
fix(coexist): 多启动器共存——存储模式写回同源 + 整合包缺省落点对齐 GUI + 版本清单不再静默覆盖(M216)
三条 M215 判定的共存债,一次修完:
① 存储模式切不回来(high)
- 写侧:`host::settings::set` 老实现一律写 `env.store()`(= 解析后数据根那份),而启动解析
只读 `<exe>/.ULL/config/launcher.json` ⇒ Standard 下把模式改成 portable 写进了 `%APPDATA%`,
下次启动看不见,切不回来。
- 现在 `core.*` 四个数据根键一律落便携那份(判据收在 `settings_bridge::setting_path` 一处,
与 CLI `Ctx::setting_path_for` 同一契约),其余键照旧。
- 读侧:`settings_bridge` 删掉自己那份 `request_from_portable`/`DATA_ROOT_KEYS` 拷贝,改用
`host::env` 的那份(presenter 侧两份逐字拷贝合成一处,不新增真相源)。
- 设置页 `core.data_custom_root` 的显示值也读便携那份,否则改完输入看起来凭空消失。
② `ull pack import` 缺省落点与 GUI 不一致(high)
- CLI 老缺省 = `ctx.game().root()`(共享 `.minecraft` 根);GUI 导入页落在
`<data>/instances/<实例目录名>` 并登记。
- 新增 `core::game::install::pack_dir`(`pack_stem` / `instance_dir_name` / `slug` /
`instance_dir` / `import_target`):命名与落点只有一份实现;CLI 调它,presenter 的
`convert::{dir_name,pack_stem,slug}` 改成一行委托 core(对外签名不变)。
- CLI 缺省现在与 GUI 同源,并复用 GUI 那条「非空目录不复用」预检。
③ 装同版本静默覆盖 `versions/<id>/<id>.json`(medium)
- `write_version_json` 三档:不存在 → `Fresh`;逐字节相同 → `Identical`(一个字节都不写,
重装同版本照旧正常);不同 → 先 copy 到 `<dest>.ullbak-<pid>` 再原子写 → `Replaced{backup}`。
- `InstallReport` 新增结构化字段 `manifest: Option<ManifestWrite>`,并把「已替换版本清单 +
备份路径」推进 `report.warnings`(进 `--json` 的 `result.warnings`)。
用例(全部零网络,走真的 `execute`):三档各一条,各写抓得住/抓不住。
三个变异各跑一次(只改实现侧)都红:写回落点改回旧的、缺省改回 `.minecraft` 根、去掉备份只覆盖。
|
2026-11-09 18:42:41 +08:00 |
|
AskaEth
|
ff78fe2cae
|
fix(core): 老版本 Java 按版本距离选 + 启动看门狗有界等待 + 结束归属(M214 D4/D6/D10)
D4:`pick` 的排序键从「来源序」改成「版本距离 → 来源序」,全不满足时选最接近要求的那个
(托管 25 与 JAVA_HOME/扫盘 17 之间选 17,而不是按 rank 选托管)。新增 `select::decide`:
不符且**没有人显式指定 Java** 时 `ull launch` 直接报 `java_mismatch` 不 spawn(`--java` /
实例 `javaPath` 是豁免,只告警)——1.12.2 配 Java 17/25 必然起不来,警告没有信息量。
`JavaMismatch` + `check` 拆到 `mismatch.rs` 并加 `advice()`(mapping.rs 408→282 行)。
D6:`LaunchSession::finish` 的等进程改成**有界**。判据在 `startup_watch.rs`(纯逻辑):
①窗口从未出现且**连续 N 秒没有任何日志**(还在打日志就说明还在加载,慢加载不被判死);
②日志出现关闭标记后 N 秒仍未退出(实机「关窗后 java.exe 仍存活」的形状)。
到点只**停止等待、绝不杀进程**:写 `session_end(still_running)` + 索引 + `launcher.log`,
返回 `LaunchError::Watchdog`,CLI 翻成 `launch_timeout`。两个时长可配
(默认 300s/60s;`ULL_LAUNCH_STARTUP_TIMEOUT_SECS` / `ULL_LAUNCH_SHUTDOWN_GRACE_SECS`,
生效值每次启动写进 launcher.log 与人读输出)。
D10:把「被终止」与「应用错误」分开。①我们自己杀的:`tree_kill` **送达即登记**
`kill_registry`(pid 登记簿,取走语义、上限 64),覆盖 GUI 从别的线程 `kill_tree` 的路径,
`finish` 据此判 `killed`;②外部强杀:Windows 上「退出码 1 + 零错误输出 + 无崩溃/JVM 关键字」
判 `killed` 并标 `terminated_externally`(任务管理器与 `taskkill /F` 都是
`TerminateProcess(h, 1)`,应用真崩给的是 NTSTATUS 异常码)。
顺带:`SessionHandle` 拆 `session_handle.rs`(launch_session.rs 422→343 行)、
`NativesReport::to_json` 落 core(cli/launch.rs 腾出空间,仍 399 行)。
用例:select 距离/取舍、startup_watch 状态机、watchdog_cases(4 条真进程)、
exit_cases(外部终止与「我们杀的」区分)、pipeline 跨线程 kill。
|
2026-11-09 17:07:22 +08:00 |
|
AskaEth
|
5959a27add
|
fix(core): child_quiet.rs 446 行越 400 红线,按职责拆成两个测试文件
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
M213 r2 把守卫的剥注释+两条新用例都塞进同一个文件,涨到 446 行。tower 的合并门里
400 行扫描算出了 OVER=1,但 push 写在扫描之后没有用它拦住自己,所以 dev 上先红了一次。
- child_quiet.rs(85):quiet 的**行为**测试(管道/退出码/env/错误语义)
- child_quiet_guard.rs(370):源码扫描型守卫(角色表 + 4 条变异对应的用例 + 解析 helper)
拆分后复验:两文件 5+4=9 条全绿;变异 D(discover.rs:156 改裸构造)仍 exit 101 并点名该行
——守卫的牙没在拆分中丢掉。
|
2026-11-09 15:14:23 +08:00 |
|
AskaEth
|
6dd88a27a3
|
fix(core): 守卫的剥注释不再吞文件 + 变异 D 变红(M213 复审 P1-1 / P2-1)
复审 P1-1:`code_lines()` 对每一行找 `/*`,找着就当块注释开头。本仓库的文档注释里到处是
glob(`/usr/lib/jvm/*`、`rules.d/*.yaml`、`crash-reports/*.txt`、`.github/workflows/**`),
字符串里也有(`"mods/*.jar"`),这些 `/*` 没有配对的 `*/`,于是 in_block 一旦为真就再也
不会变假 —— 该文件从那一行到 EOF 全部被吞掉。实测 crates/core/src 里 8/371 个文件中招,
其中一个是 game/java/discover.rs(它自己有一处 reg query),复审判的变异 D 因此全绿。
- code_lines 只在「去掉前导空白后以 /* 开头」的行上进入块注释态,行中/字符串里的 /* 一律
当普通代码。本仓库零处行首块注释,所以结构上不可能再吞代码。
- 新增 code_lines_is_not_fooled_by_glob_slashes_in_strings_or_docs:字符串里的 /* 不许
吞掉后一行、文档 glob 不许吞掉真代码、真块注释仍要被剥。
- 新增 the_scan_reaches_the_end_of_every_file:逐文件核对**最后一行代码的行号**必须出现在
扫面结果里(比数总行数尖锐 —— 文件普遍以 } 结尾,光比内容会被别处的 } 蒙混),外加
全仓可见行占比 ≥60% 的兜底(本机实测 81%)。
- 变异 D(discover.rs:156 改回裸 Command::new)现在 exit 101 并点名该行;A/B/C 复验仍红。
复审 P2-1:原注释说「两个 flag 不能叠加」,按 MSDN 这是错的。改成:两个 flag 在 Win32 层
可任意组合,真正的坑是 std 的 creation_flags 是 setter(后一次静默覆盖前一次)。结论不变
(launch/process.rs 仍零改动),顺带修正 DETACHED_PROCESS 的说明。
|
2026-11-09 15:07:56 +08:00 |
|
AskaEth
|
40a296471d
|
fix(core): 所有子进程统一走 platform::child::quiet + 角色守卫(M213)
GUI 父进程(PE Subsystem = 2)spawn 控制台子进程时,Windows 会为子进程新建一块
控制台——桌面上就是「黑框闪一下」。改前 crates/core/src 有 21 处 Command::new,
只有 tree_kill 那一处带了 CREATE_NO_WINDOW,于是探 N 个 JDK 就闪 N 次 java -version。
- 新增 crates/core/src/platform/child.rs:唯一构造器 quiet(program),Windows 上统一
.creation_flags(CREATE_NO_WINDOW),其它平台原样返回。tree_kill 里那份就地合并进来,
不再保留第二份常量。
- 20 处 spawn 全部改走它(region_system / java{discover,manual,probe,proc,termux} /
addon exec / platform{linux_caps,macos_version,theme_probe,vk_probe,windows_version,
wallpaper/io})。
- launch/process.rs 零改动:游戏进程要 CREATE_NEW_PROCESS_GROUP 撑 taskkill /T,
带不带控制台窗口是产品决定。
- crates/core/tests/child_quiet.rs:按角色判「有没有绕开构造器」的守卫,外加构造器
本身的 4 条行为用例。三条变异(裸 Command::new / 别名 Spawn::new / 删掉 creation_flags)
都实测变红并已还原。
|
2026-11-09 15:07:56 +08:00 |
|
AskaEth
|
fbb4a79955
|
fix(agent): MCP 读日志/诊断崩溃改走 session_index::event_file(D2 第三处读取点)
M212(M211 复审观察 7 确认的真 bug):
- `agent/ports/events.rs::event_path` 仍在 `root.join("game").join(&entry.log_path)`,
而 `log_path` 已经是**相对会话根**的 `game/<id>.jsonl` ⇒ 对每个真实会话解析成
`game/game/<id>.jsonl`。后果是 `ull agent` / MCP 的 read_game_log 与 diagnose_crash
两个工具对真实会话全报「读会话事件流失败」——内置 AI Agent 的看家本领是坏的。
现在解析只走 M211 建立的唯一真相源 `session_index::event_file`(新旧两个根都走它)。
- 夹具:`ports/session.rs`、`ports/diagnose.rs` 的索引夹具此前写裸文件名
`20261029T124501-9f3c2a.jsonl`,与生产差一层 `game/`,所以这个 bug 在测试里永远看不见。
改成 `session_index::log_path_for(id)` + `session_index::event_file` 落盘。
- 用例:`ports/events.rs` 新增三条(解析结果 = 写入侧落盘的那一个 / 文件缺失时指向的仍是
生产形状 / 旧根回落走同一套解析);`cli/tests/agent_mcp_stdio_test.rs` 新增一条真机
MCP 用例(起 `ull agent serve --stdio`,对一份生产形状的会话调 list_game_sessions →
read_game_log → diagnose_crash)。旧实现下两条路径都红,红的是
`...\logs\game\game\20261030T101500-abcdef.jsonl`。
|
2026-11-09 14:28:42 +08:00 |
|
AskaEth
|
a91930e021
|
test(core): crash_scan 用例改用 slice::from_ref(clippy::cloned_ref_to_slice_refs)
|
2026-11-09 14:12:23 +08:00 |
|
AskaEth
|
4b30bf07f0
|
fix(launch): 会话事件流路径只留一个真相源 + crash_found 只认真实证据 + ull launch 也写 launcher.log
M211(来自 M208 实机 e2e 的 D2/D3/D5):
- D2「game/ 双写」:log_path 的契约原来有两种说法(字段文档写「相对 game/」,写入侧一直
写「相对会话根」的 game/<sid>.jsonl),于是两处读取点各自又多 join 一层 game/,
ull logs / ull diag / GUI 日志页对每个真实会话都报「找不到事件流」。现在解析只走 core 的
session_index::event_file(CLI 与 presenter 共用),写入侧只走
session_index::log_path_for。方向选的是「读取侧不再 join」:存量索引里的 log_path 从第一版
起就是 game/<id>.jsonl,改读取侧能把用户盘上已有的旧会话一起救回来(零迁移、零改写)。
- D2 夹具:cli/tests/support 与 presenter/host/logs_tests 的索引夹具改成生产形状
(game/<id>.jsonl)。夹具此前自己说了第三种形状,所以这个 bug 在测试里永远看不见——
修夹具比修路径更重要。
- D3「crash_found 恒为 true」:logs/latest.log 每局都写,不再被当现场文件(只在已经找到
权威现场时作为补充证据)。崩溃结论改由 crash_scan::has_crash_evidence 判定(权威现场文件
/ 非 0 退出 + 错误行),索引旗标由 SessionWriter::mark_crash 写入;crash_found 事件恢复
成纯事实记录。
- D5「ull launch 从不写 launcher.log」:现在与 GUI 共用
launcher_log::record_start / record_end(字段名与顺序只在一个地方定义),写的是同一份
<data>/logs/launcher.log。
测试:core 新增 session_path_tests(写入形状 = 读取解析结果 = 真落点的往返)、crash_scan 的
干净退出用例、launcher_log 的行格式锁;CLI 新增 a_launch_attempt_writes_the_launcher_log。
|
2026-11-09 14:12:23 +08:00 |
|
AskaEth
|
1748e046f8
|
fix(cli): progress.rs 测试 helper 的返回类型抽成 Recorder 别名
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
M210 引入 type_complexity(clippy 1.99 -D warnings)。M210 的 brief 里没有 clippy
这一项(五步门禁是它 spawn 之后才补上的),所以第一轮没抓到;新门禁在合并态立刻咬住。
|
2026-11-09 14:05:52 +08:00 |
|
AskaEth
|
d8ca411117
|
fix(core): bmclapi 库镜像补 /maven;install 进度行改说实话
D1(high):mirrors.json 的 bmclapi-libraries 少了 inject,官方
libraries.minecraft.net/<path> 被重写成镜像根 + <path>(BMCLAPI 的 maven 仓库挂在
/maven 下),实测 70/70 个库全 404、全部回落官方源(单请求 EMA 6.3 s)。本轮实测:
guava:32.1.2-jre 无 /maven → 404 / 9 B;带 /maven → 200(2 次 302)/ 3041591 B,
sha1 与官方逐字节一致。补法与 bmclapi-assets-objects 的 /assets 同一机制(inject),
不把 /maven 拼进 to(to 是镜像根)。
守卫两道,抓不同的东西:
- 形态:凡 from 指向 maven 仓库的规则,to+inject 必须以 /maven 结尾(并点名覆盖的
5 条规则 id,删/改名都会红);
- 行为:4 个真实官方 maven 根展开后必须逐字等于 /maven/<子路径>。
原有 prefix_rule_preserves_sub_path 的期望值曾把 D1 的 bug 钉成期望,一并改正。
变异(删掉 inject)实测三条同时红,恢复后 mirrors 14 passed。
D11(low):进度回调只累加「处理到第几个文件」,还打「下载 {done}/{total}」,
重装同版本(全部已就绪)时会连打 3961 行「下载」而一个字节都没下。拆出
install/progress.rs:计数全部来自 FileOutcome.status(Downloaded/Skipped/Failed),
下载数为 0 时人读行不出现「下载」二字(改说「已就绪 d/t(跳过 N 个)」),--json 的
progress 事件补 downloaded/skipped/failed。用例构造 96 个全 Skipped ⇒ 断言每行都不含
「下载」且末行为「已就绪 96/96(跳过 96 个)」;变异(退回旧文本)实测红。
其它镜像表(assets / jar / version_manifest / 索引 / fabric / forge)的 inject 形状
实测一致,另有 liteloader 目标路径 404、quilt maven 无规则两条【不同类】观察,
不在本轮范围,报告里交回 tower。
|
2026-11-09 13:57:32 +08:00 |
|
AskaEth
|
391ebf88b7
|
fix(ci): 修两处 clippy -D warnings 红(needless_lifetimes / drain_collect)
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
两处都来自 M201(5dda9ac),非本轮 mission 引入。tower 合并门此前只跑
fmt+check+test,从未跑 clippy,所以 dev 上 CI 的 clippy job 一直是红的没人发现。
- presenter/host/settings/download_tests.rs: chosen 的显式生命周期可省略
- app-iced/.../download_tests.rs: drain(..).collect() -> mem::take
语义等价,纯 lint。门禁已补上 clippy(见 00-恢复指引 §6)。
|
2026-11-09 13:56:09 +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
|
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
|
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 |
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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 |
|
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 |
|
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
|
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
|
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
|
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 |
|
AskaEth
|
5a52f38f35
|
feat(content): 安装失败的两档分组与「只重试这些」(M191)
M186 把「可只重试失败项」算到了 core/presenter(DependencyPlan::retry_request()),
但 DTO 没字段、界面没按钮,用户拿不到。这一轮补最后一公里:
- DTO:ContentFailureKind 加 Incompatible 一档(不再借 Unsupported,与「缺密钥」分开);
InstallProgressView 加 retry: Vec<InstallFailureView>(project + 档位 + core 原话),
档位是专门的 InstallFailureKind(Incompatible / Unreachable,穷尽映射 core 的 FailureKind);
ContentInstallRequest 加 retry: bool(重试路径唯一读的字段是 instance)。
- presenter:plan.rs 把上一轮失败的计划按 host+实例记住(与 addon/pack.rs 同一手法,
host/mod.rs 不在 scope);retry_request()/retry_plan() 就是「只重试这些」——
调 core 的 retry_request()、只请求失败的那几个项目,present 重新扫目录。
install.rs 的重试入口没有记录时回一条失败,不退化成整单重装;成功则 forget + 清名单。
- 界面:内容库页插入按档分组的失败面板(两段文案、各自条目)+「只重试这 N 个」实心按钮;
判据只有一条且在模型里(stage==Failed && 名单非空),全成功时连同按钮一起不渲染。
- i18n:新增 6 条中英文案(不兼容 / 取不到 / 只重试这 N 个 / 标题 / 提示 / 关闭),金表同步。
守门(每条都做过变异验证,见报告):
① 把 retry_request() 换成根项目请求 ⇒ retrying_asks_for_the_failed_projects_only 红(roots 变 ["sodium"]);
② 两档合并 ⇒ the_retry_list_keeps_the_two_tiers_apart / the_two_tiers_are_two_groups_with_two_labels 红;
③ 恒渲染 ⇒ a_fully_successful_install_has_no_retry_surface_at_all 红。
数据包(datapack)本轮仍未做:ContentKind 没有这一项,界面上也没有半成品入口。
|
2026-11-09 05:06:04 +08:00 |
|
AskaEth
|
93bd704786
|
fix(m192): r1 复审 P1-1——动效起跑搬进 update 侧 + 帧基准兜底 + 按运行时顺序的守卫
复审的机制三步全部复核成立(iced_winit-0.14.1 `src/lib.rs:1222` 调 update、`1338` 在
`update()` 内部求值订阅表、`1233` 之后才 `build_user_interfaces` 调 view)。第一版把
「标识变了 → play」写在 `pages::render`(= view)里,于是:同一拍的订阅表看不见轨道 ⇒
`frames()` 要等下一条消息才打开;而那一帧的 dt 是整段空闲时长 ⇒ 轨道被当场判到期删掉。
用户看到的是「一帧 12/8px 偏移 + 闪跳」,比硬切更差;截图通道因为 `has_screenshot()` 恒真,
结构上不可能发现它。
修法(三件都做)
- **起跑搬进 update**:新增 `app/update/motion_sync.rs::App::sync_motion`,在 `App::update`
收尾(`route_message` 之后、`subscription()` 之前)按**路由**算内容标识并 `motion::observe`。
`pages::render` 只留 `motion::sample()`(纯采样)。
顺带:`screen == None`(账户页 / AI 独立页)只记标识不起跑,于是「从它们回到某一屏」也跑换页。
- **帧基准兜底**:`observe` 起跑成功时把 `LAST_FRAME` 重置到「现在」;`Pump::advance` 再加一道
`MAX_STEP_MS`(4 帧 = 64 ms)单次步进上限——掉帧/挂起时宁可把动画放慢,也不许一帧抹掉。
- **没有窗口就不起跑**:`window.is_none()` 且不是截图通道时 `sync_motion` 直接返回(也**不记**
标识)⇒「首屏入场」等窗口到了才算第一眼,启动期那棵空树吃不掉它(原先被 1 Hz 心跳 /
既有 `an_idle_app_...` 用例照出来的就是这个边界)。
- `on_heartbeat` 也推进动效时间轴:窗口被遮挡/最小化/合成器不送绘制时,帧一个都不来,而
「有轨道」会让 1 Hz 推进整段停摆 ⇒ 轨道靠心跳按单次上限收尾(最坏 5 拍内自然退订)。
守卫(新增 5 条,全部按运行时顺序写)
- `a_state_change_opens_the_frame_subscription_in_the_same_tick`:**先** `assert!(!needs_frames)`
→**一条真消息**(`Message::Page(Navigate(..))`)→ `assert!(needs_frames)`,并断言
`watch_until` 仍是 `None`(排除「观察窗顺手开帧」这条假绿)。r1 那条顺序是反的,已删。
- `opening_a_detail_also_opens_the_frames_in_the_same_tick`、`pages_that_render_elsewhere_do_not_start_a_track`、
`without_a_window_nothing_animates`、`a_stale_baseline_cannot_kill_the_track_in_one_sample`。
r1 两条 P2
- P2-1:`pump::FRAME_MS` 与 `mod.rs` 契约表补写「16 ms 只卡采样/布局推进,**整窗重绘跟随渲染器**
(软件渲染 ≈970 帧/秒、一次 300 ms 换页 ≈290 次整窗重绘 ≈0.30 核·秒;GPU 档 AutoVsync 40~60)」。
- P2-2:删掉 `app/subscription/tests.rs`,测试并回 `app/subscription.rs` 的 `mod tests` ⇒ 本轮
动过的文件全部落在声明 scope 内(新文件只有 `app/update/motion_sync.rs`,在 `app/update/**` 下)。
真机证据(GPU 档,`ULL_TICK_TRACE=1`,空闲 14 s 后由心跳触发一次**真实**导航,
`DevDocs/artifacts/m192/idle-click-evidence-gpu-final.log`):
idle: frames=96 clock=idle-1hz motion=14(连续 7 拍不动)
fire: clock=frames motion=16 → 17(同一拍订阅表就开了)
end : frames=102 clock=idle-1hz motion=20(自己退订)
说明:本机会话没有前台窗口(`GetForegroundWindow()==0`),合成点击(`mouse_event` 与
`PostMessage(WM_LBUTTONDOWN)`)递不进 iced 的输入层,故用「心跳第 N 秒真导航」这条同构路径;
样本次数由**绘制送达**决定(该次动效只收到 ~5 次绘制 ⇒ +6 次采样),正常可见窗口下 60 Hz 呈现
对应 ~19 次。
变异证明 7/7 红(`DevDocs/artifacts/m192/mutate.sh`,含新增 M6「删掉 sync_motion」与
M7「去掉单次步进上限」),还原后自检全绿。
编译:cargo test -p unlucky-app-iced → 0(797 passed / 2 ignored,集成 6 passed);
clippy --all-targets -D warnings → 0;fmt --check → 0;build → 0。
出图基线复验:三张 rest 基线与九张序列帧的 sha256 与 r1 **逐字节相同**(常驻的那层 container 零内距)。
|
2026-11-09 04:52:40 +08:00 |
|
AskaEth
|
a1876a08c0
|
feat(m192): 动效帧泵——design motion 令牌接进 iced 的有界帧订阅 + 首批 3 条入场动效
令牌早就在 `crates/design/src/motion/` 里齐着(16 档时长 / 可采样缓动 / 弹簧二维表 /
MotionScale 降级),但适配层从没消费过,所以一切状态变化都是硬切。本次补的正是那条中间层:
新增 `components/motion/`(时间轴,不是控件家族)
- `pump.rs`:`Slot`(boot/screen/detail 三个互斥触发点)× `Spec`(时长档 × 缓动档,
**唯一一张表**,页面拿不到「给我 250ms」这种口子,D31)→ 进度采样;时间轴按 16ms 结算
(≈60 次/秒),到期轨道**当场删除**。
- `mod.rs`:进程内单时间轴(thread-local;iced 在事件循环线程上串行调 update/view,
与 `App::update(&mut self)` 同一条纪律,测试也因此天然隔离)+ `enter`/`shifted` 两个页面入口。
- **默认 NONE**:只有产品 `on_boot` 按档位打开;`pages_demo` 这类截图 harness 从不打开。
接线(都接在既有那条链上,不另开循环)
- `App::needs_frames` 多一条「有轨道」分支 → 与截图 / Busy / toast / watch_until 共享**同一个布尔**,
`window::frames()` 在源码里仍只有一处(守卫:双订阅就红)。
- `on_frame` 每帧推进时间轴;`on_heartbeat` 不变(有帧时不推进)。
- 定档策略集中在 `update/boot_env.rs::motion_scale`:截图档 NONE(D30 出图可复现)、
CPU 渲染档 HALF、GPU 档 STANDARD,`ULL_MOTION` 可强制。
首批 3 条动效(`pages::render` 一个漏斗,静息恒为 (0,0) ⇒ 既有截图基线一张都不用重出)
- 首屏进入 16px / 换页 12px(纵向) / 列表↔详情 8px(横向);
时长 400/300/350ms 全部取自 `DurationToken`,曲线 `Easing::Standard`
(`EmphasizedDecelerate` 把 75% 位移压在 51ms 内走完,12px 的整页平移会像跳一格)。
守卫与取证
- 11 条帧泵算术守卫 + 4 条接线守卫(含「NONE 档帧采样次数必须为 0」与「帧订阅源码只出现一处」);
- 5 条变异证明全部复现(删退订 / 删 NONE 短路 / 删 needs_frames 分支 / 删 16ms 闸门 /
加第二条帧订阅 ⇒ 对应用例红);
- 多帧序列图 12 张在 `DevDocs/artifacts/m192/`(t=0 / 半程 / 末 + 静息基线,
`--diff --diff-scan` 实测换页 12px/6px、详情 8px);
- 空闲实证:`ULL_TICK_TRACE=1` 跑真窗口,帧订阅窗口一过 `clock=idle-1hz` 且 `frames=` 冻结。
|
2026-11-09 03:56:19 +08:00 |
|
AskaEth
|
a219a21db8
|
fix(m190): r2 复审——dalek strict 口径照实测写正、补小阶 R 用例、锁里 toml_datetime 退回 1.1.2
三条 P2 都是「文档/注释与事实不符 + 一条上报事实错了」,代码行为没变。
P2-1 dalek 的 strict vs 非 strict 口径改成正测试出来的样子:
* `VerifyingKey::from_bytes` **不**校验编码规范性(`y = p` 的非规范写法返回 Ok,`is_weak() == true`),
先前写的「要求规范编码」是错的;
* 非 strict 的 `verify` **也**查 `S` 规范性 —— 先前把它写成「不做可锻检查」是半错;
* 于是这批拒绝类用例里,strict 与非 strict 唯一的分歧是**小阶点**:承重的是
`small_order_public_key_is_rejected`,其余几条(malleable / non_canonical_r / empty)换掉 strict 后
**仍然绿**,文件头的表与用例注释都改成这个口径,别让人误以为它们也是守卫。
* 新补 `small_order_r_is_rejected_only_by_strict`:R = 单位元 + S = k·a mod L 的伪造,**两边都断言**
—— dalek 的非 strict `verify` 接受它、我们的 `verify` 拒绝。这是第二条「只由 strict 拒」的判据。
* `non_canonical_r_encoding_is_rejected` 里那条「非规范公钥编码」原来对两种实现都绿(等于没测),
现在断言 dalek 的**真实**行为(`from_bytes(...).is_ok()` + `is_weak()`),dalek 升级改口径会红。
P2-2 `packaging/sign_manifest.py` 里指向已删除 `ed25519/{field,group}.rs` 的旧注释删掉(那段还写着
「same formulas」,而两套实现现在是**互相独立**才构成交叉验证)。
P2-3 `Cargo.lock`:`toml_datetime` 从 1.1.3 **退回 1.1.2**(`cargo update -p toml_datetime --precise
1.1.2` 实测可降、`cargo metadata --locked` 自洽)。先前报告里「被 `toml` 拒绝所以无法避免」的理由
**不成立**,已在报告里更正。现在相对 `44a0c87` 的锁变更是**纯新增**:16 个包、零既有版本变动。
变异证明(本轮按要求重跑):把 `verify_strict` 换成 `verify`(并补 `Verifier` trait 使其可编译)
⇒ `test result: FAILED. 110 passed; 2 failed`,红的两条正是
`small_order_public_key_is_rejected` 与 `small_order_r_is_rejected_only_by_strict`;其余拒绝类用例全绿
(与修正后的口径一致)。恢复后 `112 passed; 0 failed`。
|
2026-11-09 02:33:08 +08:00 |
|
AskaEth
|
918d826645
|
fix(m190): r1 复审——0x01 修成 \1 + YAML 门禁、被调方声明 secret、拆两个超限测试文件、验签换 ed25519-dalek
四条 P1 + 四条 P2 一并处置(不变的是判据,变的是「谁在承重」)。
P1-1 `.github/workflows/release-build.yml` 的 sed 替换串是裸 0x01(本意 `\1`):GitHub 解析该文件会
失败 ⇒ 整个 run 起不来;即便容忍,版本号也会被求值成单字节,PE 判据报「与 Cargo.toml 不同源?」把
排障引向错方向。修成字面 `\1`,并新增 `packaging/check_workflows_yaml.py`(真 YAML 解析器 parse
全部 workflow + 裸 C0 控制字符字节判据;带 --self-test 注入 0x01/坏结构断言必红),接进
`release-guard.yml` 两步。文本扫描型守卫看不见这类字节,这道门禁专门挡它。
P1-2 `release-publish.yml` 没在 `on.workflow_call.secrets` 里声明 `ULL_MANIFEST_SIGNING_KEY`,调用方
却传了:可复用工作流只拿得到自己声明过的 secret ⇒ 用户配好钥也会在签名步红,而错误文案反过来指控
Secret 没配。补声明(required: false,fail-closed 由步内 ::error:: + exit 1 承担,同
ULL_MSA_CLIENT_ID 的模式)。
P1-3 两个测试支撑文件越过 400 行守卫:签名夹具拆到 `tests/support_sign.rs`(support.rs 458→392),
CLI e2e 的签名部分拆到 `support/update_e2e_sign.rs`(update_e2e.rs 406→348)。纯搬家。
P2-2 「唯一收口点」改成结构事实:从 `UpdateSource` 删掉 `fetch_manifest` 默认实现,调用方只能拿
`verified_manifest(&source, channel)`(源只交原文,想绕过验签就得自己重写解析与渠道判据)。
P2-3 文档写的第 ④ 条命令补进流水线:`sign_manifest.py --verify-pinned` 用 trust.rs 里钉住的公钥
回验刚落盘的 .sig(不碰私钥),发布侧因此验证的是「客户端的判据」而不是「签名脚本的自我声明」。
P2-4 随自写实现一起删除(`Fe` 的错误 PartialEq 不再存在)。
P2-5 app-iced/build.rs 注释里那个 windows_subsystem 属性本分支不存在(M196 才有),改为说明
「本文件只写版本资源,子系统由 rustc 属性决定,check_pe_version.py 会把 subsystem 打出来」。
P2-6 坏清单不再带 error.path 是有意降级(来源 id 在 message 里),保持。
裁定项:自写 Ed25519 换成 `ed25519-dalek::verify_strict`(信任锚不许自写密码学;此刻信任表还是空表,
零兼容负担)。删除 `update/ed25519/{field,group,mod}.rs`(含只服务它们的单测),新 `update/ed25519.rs`
只剩一个薄封装;拒绝类用例(可锻 S+L / 小阶公钥 / 非规范 R / 空签名 / 未知 key)**一条没删**,
改成钉 dalek 的 strict 语义——实测 dalek 对全部五类的结论与旧实现一致(非规范 R 的拒绝理由不同:
dalek 走「小阶点」而非「编码非规范」,用例注释里写明)。签名侧留在 `packaging/ed25519_ref.py`
(纯 Python、零 pip 依赖),`--self-test` 用 RFC 8032 同一组向量钉两边。
|
2026-11-09 02:14:32 +08:00 |
|
AskaEth
|
b7d9b99e12
|
feat(m190): 产物署名(PE VERSIONINFO)+ 清单 Ed25519 多签名与更新前验签
两件事:让「这是谁发的」在产物与清单上都留下可验证的痕迹。
① PE 版本资源(两个 bin):`packaging/pe_version.rs` 是唯一口径(CompanyName
「Unlucky Launcher Project」/ ProductName、FileDescription「Unlucky Launcher」/
LegalCopyright「© 2026 … · Apache-2.0」/ 四段式版本号与 Cargo.toml 同源),被
`crates/{cli,app-iced}/build.rs` 各 include 一次;非 windows 目标整段不执行。
判据是读产物字节的 `packaging/check_pe_version.py`(CI 的 Windows 槽位)。
② 清单签名:`manifest.json` 旁边多一份 `manifest.json.sig`(分离签名,签的是清单**原始
字节**),信封是 `{format, signatures:[{alg,key_id,sig,manifest_sha256}]}` —— 多签名数组,
任意一条命中钉住的钥即放行(换钥窗口)。信任锚是 `crates/core/src/update/trust.rs`
里的编译期公钥表(正式表为空 = fail-closed,测试钥只在 cfg(test)/update-test-source 里)。
管线顺序改成「先验签、再解析」:`UpdateSource` 只交原文 + `.sig`,`verified_manifest`
是唯一收口点,`Job::apply` 因此在取包**之前**就拒。新增五类错误分类
(signature_missing / malformed / unknown_key / keyring_empty / invalid ),全部不可重试,
CLI 的 hint 单独说人话。
③ Ed25519 验签自带实现(`update/ed25519/*`,零新依赖):scope 不含 core 的 Cargo.toml,
用 RFC 8032 §7.1 向量 + 可锻/小阶/非规范编码的拒绝用例钉住;签名侧是
`packaging/sign_manifest.py`(纯 Python 参考实现,零 pip 依赖,`--self-test` 同向量)。
④ 守门:`release-publish.yml` 加签名步(`set +x`,只回显公钥;没 Secret / 钥没钉 / 签不上
一律红 ⇒ 不出 Release);`release-build.yml` 加 PE 资源读回;`release-guard.yml` 跑两个
脚本的 `--self-test`。密钥纪律与轮换(旧钥签新钥)写在 `packaging/docs/signing.md`。
Cargo.lock:新增 winresource(build-dependency)与 toml 系;toml_datetime 1.1.2→1.1.3 是
toml 1.2.1 的硬要求(`cargo update --precise 1.1.2` 已实测被拒)。
|
2026-11-09 01:59:05 +08:00 |
|
AskaEth
|
44a0c87a11
|
style: cargo fmt --all(合并期整合修复)
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
M187/M188 并入后全仓 fmt --check 为红(CI 的 fmt job 会红)。
本次仅 cargo fmt --all 的换行/缩进变化,无语义改动;
cargo fmt --check 与 cargo check 均已复验为绿。
|
2026-11-09 01:43:31 +08:00 |
|