Commit Graph
100 Commits
Author SHA1 Message Date
AskaEth edd6402fd5 fix(core): 规则结论改在脱敏前算,会话路上 fabric.loader_0_12 能命中(M241)
会话 JSONL 是写前脱敏的,而脱敏的「≥64 无点号不透明串」是有损的:
fabric.loader_0_12 唯一的证据行(Sponge mixin 类路径 66 字符)被抹成
<redacted:opaque>,规则在会话路上永不命中。

修法不动脱敏策略、也不往盘上多写一份原文:
1. SessionWriter 在内存里留一个 2000 行的原始尾窗(不落盘);
2. 收尾时把这些**原始**事实交给新的 diagnose::session::conclude 跑规则,
   产出契约形状的 diagnosis payload —— 它落盘前仍过 Redactor::scrub_json;
3. 日志页优先读这条落盘结论,老会话才回落到「在脱敏文本上重跑规则」。

为什么不是「原文与脱敏文本并存」:会话 JSONL 里存原文会踩
core/tests/launch/pipeline_cases.rs 早已钉死的「事件流不得含凭据明文」。

顺带把 EventKind 纯搬移到 launch/session/event_kind.rs 守 400 行上限。
2026-11-11 09:19:42 +08:00
AskaEth 74be510114 Merge branch 'feat/fix-narrow-window-chips-squeeze-and-menu'
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-11 08:41:04 +08:00
AskaEth df0944894f fix(app-iced): 面板预算按内容推导 + 版本菜单不画 L2 阴影(M238)
两条都在「视口不终止内容」这一族里,修法各一半:

① 三栏档面板预算从死的 `PANEL_BUDGET = 300` 改成 `panel_budget(menu_items)`
   (内边距 24 + 首屏 76 + 间距 8 + 钉死区 88 + 菜单高 + 列表↔面板 21)。
   旧常数不含列表↔面板那 21、也不随版本菜单行数变,于是 760/900 档面板实得
   286/294px,滚动视口被挤到 54/62px——六枚 loader chip 只露出 10px(736 与 1180
   同样严重,纵向预算与窗宽无关)。修后 760 档视口 98px(chips 31px 整枚在屏)、
   900 档 106px;列表按整行让出:760 档 4→3 整行、900 档 7→6 整行;只有一条
   加载器版本时菜单只占 64px,列表仍是 4 整行。

② 版本菜单改用 `Menu::flat()`:不是「把阴影裁掉」,而是**不在滚动容器里画阴影**。
   `iced_tiny_skia` 0.14 画 quad 阴影走 `draw_pixmap(.., mask: None)`
   (engine.rs:136-143),绕过裁切掩码;`iced_wgpu` 那边是整层 scissor
   (quad.rs:96-101)⇒ 同一棵树两个后端画出来不一样。修前菜单底下 54px 一条被压暗
   (浅色 249→231,−7.2%,含 8px 缝与实例名框内上半部),修后逐像素等于画布色。
   菜单的 L2 令牌不动,真实浮层照旧带阴影。

判据(两条,抓得住的/抓不住的都写进文档注释):
- 结构:`rows/tail.rs` 断言三栏档 760/900 的滚动视口 ≥ 首屏 76px(对生产常量做算术);
- 像素:`options_shot/compact.rs` 断言 chip 描边上下两行都在、且实例名框上方那条缝
  与框内都是纯画布色。
变异自证:预算退回 300 ⇒ 结构断言红("滚动视口只有 54px < 首屏 76px");阴影无视
`flat()` ⇒ 像素断言红("y=660 x=304 是 [231,231,232] 而不是画布色")。

没验:滚动中/底部三态拍不出来(harness 无 `scroll_to` 派发通道,已另开 finding)。
2026-11-11 08:28:06 +08:00
AskaEth dbc5274965 Merge branch 'feat/wire-disabled-control-base-into-product'
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-11 07:56:14 +08:00
AskaEth ba441351dd fix(app-iced): 复审 r2 三条收口(M237)
- P1-1 判据会让 CI 一亮红:`java_shot.rs` 原来写「没喂图时 `assert!(CI 未设置)`」,
  而 `.github/workflows/ci.yml` 的 test 作业在 ubuntu + windows 上都跑
  `cargo test --workspace --all-targets`,出图步骤只给 `options_shot` 备了 `ULL_DL_*_SHOT`
  (本判据的图不在出图清单里)⇒ `CI=true` 实测 exit 101,一合即红。改成:没喂图**一律响亮跳过**
  (`not_fed()` + CI 时多一句「本仓 CI 不出这张图,跳过是预期」),模块文档写清「要让它守门需要
  跨 scope 加出图步骤 + 注入 `ULL_M237_JAVA_SHOT`,以及那时该怎么改回硬失败」。
  自证:`CI=true` 不喂图 → exit 0(改前 101);无 CI 不喂图 → exit 0;喂图(浅/深)→ 真跑,exit 0。
- P2-1 同类错漏一处:实例列表卡画的是 `MaterialLayer::Card`(`instances.rs:263`),
  但 `list_row` 的 `ListItem` 没传底 ⇒ 行 hover/按下按 `Base::CANVAS` 合成。
  改成 `item.base(frame::BASE)`(同一层,不另立常量),并加进接线守卫。
  像素证据(`--force-state=hover`,1180×760 列表卡矩形 `(112,160)-(448,735)` 内 before/after 差):
  浅色 57923 px `(232,232,233)→(226,226,227)`,深色 57923 px `(32,38,31)→(35,40,35)`
  —— 即 hover 8% 从画布底换到卡底。
- 守卫自身升硬:`base_wiring.rs` 的计数原来按整段文本数,**把接线注释掉仍算数**(r2 实测绿)。
  改成先剔掉整行 `//` 注释再数;变异自证:注释掉 `item.base(frame::BASE)` ⇒ 红
  (`instances.rs: .base(..) 出现 0 次,期望 1 次`,退出码 101),还原后绿。
- 门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-237):fmt 0 / check --all-targets 0 /
  clippy --workspace --all-targets -D warnings 0 / test -p unlucky-app-iced --lib 907 通过 0 失败 /
  test --workspace 3344 通过 0 失败。
2026-11-11 07:56:11 +08:00
AskaEth 3dcd5fd117 feat(app-iced): 页面层把卡片底接进禁用/交互控件(M237)
M234 把 `interaction::Base` 与组件侧的 `.base(..)` 接好了,但页面层仍是默认的画布底:
卡片(`Role::SurfaceContainerLow`)里的禁用按钮 / chip / 开关把 10% / 12% / 38% 叠在画布色上。
M223 记的「卡片里禁用控件 CR 1.05、形丢失」因此只在 gallery 里修好,产品页没修。

- 导出容器底:`pages::frame::BASE = Base::of(MaterialLayer::Card.surface_role())`
  —— 取的是 `card()` **真正画出来的那一层**(不是「反正是 surface」);`logs::panel::BASE`
  是同一个常量的别名(两张卡画的是同一个材质层)。
- 接线(改前一律 `Base::CANVAS`,改后 `frame::BASE`):设置·Java(扫描 / 取消 / 设为默认 chip /
  三个输入框 / 添加 / 保存 / 预置项 chip / 下载徽标);设置·通用(语言三档、主题三档、日志行
  两个手写按钮 → 状态层叠 `BASE.role()`);实例详情(MODS 开关、SAVES 的重试与打开文件夹、
  启动参数区的两个输入框 + 后端 chip + 保存 / 恢复);日志页(诊断卡页脚 `Copy summary`)。
  详情头的启动 / 停止 / 动作行**不在卡里**,保持画布底并加注释。
- 判据:`pages/frame/tests.rs` 逐值断言「`BASE` == 卡片画出来的颜色」+ 源码结构盯 `card()`
  不许改画别的材质层;`pages/settings/tests/java_shot.rs` 是**页面级像素判据**(读 1180×760
  真截图、逐位匹配:卡区里「叠卡底」3136 px vs「叠画布」21 px,深色 3128 / 1);
  `pages/tests/base_wiring.rs` 数接线点,守像素够不到的页面(实例详情 MODS / SAVES)。
- 变异自证:撤掉「立即扫描」的 `.base(BASE)` ⇒ 像素判据红(73 px vs 3083 px,退出码 101);
  撤掉诊断卡页脚的 `.base(..)` ⇒ 接线判据红(0 vs 1,退出码 101)。均已还原。
- 门禁:fmt / check -p unlucky-app-iced --all-targets / test --workspace(3344 通过,0 失败)/
  clippy --workspace --all-targets -D warnings / 文件 ≤400(最大 389),全部本地过。
- 已知未尽:面板 α<1 的半透明卡(`Base` 只装一个 `Role`)、GPU 档、实例详情与日志页在
  1180×760 出图里没有可量测的禁用控件(夹具缺 running,已报 finding);app_bar / adopt pick /
  设置页分组列仍未导出 `BASE`;设置·下载 / 更新 / 助手、账户页、内容库、下载页仍未接线。
2026-11-11 07:56:11 +08:00
AskaEth cf8a951e29 Merge branch 'feat/fix-accounts-aba-false-red-and-probe-wal'
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-11 06:53:54 +08:00
AskaEth 4952ee2706 fix(accounts,probe): 测试缝换稳定身份(ABA)+ 超时墙钟对齐真实预算(M236)
两条 M230 台账判定「真开着」的测试债:

1. accounts 测试缝的 key 是 Arc::as_ptr(&job.cancelled)。job 一 drop,控制块
   立刻可能分给下一个 LoginJob,于是新 job 读到旧 job 的记录(ABA),并行跑的
   「设备码入口不许置位 browser_first」因此偶发假红。改成 LoginJob 自带的
   稳定身份 JobId(cfg(test) 字段,进程内单调递增、永不复用);ENTRIES 从
   Vec<(usize,bool)> 改成带 Weak 的 Entry:record 时 retain 收走已 drop 的 job,
   同一 job 覆写成「最后一次」(消掉 mode_for 取第一条的歧义)。
   反向证据:身份退回堆地址 ⇒ the_test_seam_does_not_reuse_a_heap_address_as_identity
   红(mode_for(&younger) 读到 Some(true))。

2. probe 的 silent_server_times_out 断 `elapsed < 3s`,跟真实预算(400ms)无关。
   改成 预算 + SLACK(400ms + 1s):抓「预算没生效」(那要等假服务器睡满 5s),
   放开毫秒级精度;并新增两条判据——detail 里的时限就是本次预算(证明走的是
   预算分支)、假服务器真的收到 1 次请求(不是没连上就早退)。
   反向证据:摘掉 tokio::time::timeout ⇒ 5.08s / unreachable 红;把计时值硬编码
   3s ⇒ 3.006s > 1.4s 红。
2026-11-11 06:53:52 +08:00
AskaEth 30d4163816 refactor(accounts): 登录两条流拆到 accounts/login.rs(纯搬移,M236 前置)
accounts.rs 正好 400 行零余量,本轮要给测试缝换稳定身份,改前必须先拆。
搬移口径:start_login / start_browser_login / msa_auth / fail / finish /
store_session 的每一行(含签名与 doc)与搬移前逐行相同(130 行非空行
diff 为空,只有空行分隔差异);公开路径由 `pub use login::{...}` 保住
(host::accounts::start_login / start_browser_login 不变)。
列表/增删/LoginJob/MsaEntry/测试缝留在父文件:那是测试要用的私有件,
搬走它们就得放宽可见性,纯搬移口径会随之破掉。
2026-11-11 06:53:51 +08:00
AskaEth 32951df3d6 Merge branch 'feat/fix-disabled-control-compositing-base-at'
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-11 06:34:31 +08:00
AskaEth 9bb3c33b8c fix(app-iced): 复审 r1 四条 P2 收口(M234)
- P2-1 判据恒绿:`gallery/probe/bases.rs` 的「反向检查」把字段档计数(`:172` 遮蔽后的
  `hits`)拿去比按钮档 stray ⇒ 恒绿。改成 `button_hits` / `field_hits` 两个计数,各比各的
  stray,失败信息的数值与单位对齐量测对象。变异自证:卡列里留一颗忘了 `.base(..)` 的按钮
  ⇒ 该断言单独红(2137 px vs 3178 px,退出码 101)。
- P2-2 FAB 的底错了:禁用态背景仍是满档 `PrimaryContainer`(`Disabled.layer() == None`),
  38% 内容却合在画布上。改成 `Base::of(PrimaryContainer)` + `on_container = false`
  (那一层 10% 在 FAB 身上不存在),两处调用收进一个 `content_color()`;新增值判据
  `disabled_fab_ink_composites_over_its_own_container`(修前 #8CA0AA / 修后 #548FBC 量级)。
- P2-3 漏改:`selection::paint::state_layer()`(hover/focus/pressed)与 `list_item` 的
  状态层叠底仍写死 `Role::Surface`,改成跟 `base`;同类两处(`assemble::transparent_state_color`
  的 Outlined/Text hover、drawer 项药丸)一并跟上。
- P2-4 文档失真:`feedback/{action,mod}.rs` 删掉「本地 α 合成 / 规则只认两档」的旧描述
  与失效链接;`list_item` 的旧签名;三处 `probe/base.rs` → `probe/bases.rs`;
  新增 `surfaces::elevated::tests::the_card_base_constant_is_the_role_the_card_paints`
  真断言(原来只在文档里声称那条测试守 `BASE`),并把这句描述改准。
- nit:`disabledbase.rs` 字符串里塌成一行的续行空格、probe 汇总行把列左缘横坐标说成
  「像素计数」的措辞。

门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-234):fmt / check --all-targets /
test -p unlucky-app-iced(896 + 6 通过,0 失败)/ clippy --workspace --all-targets -D warnings /
文件 ≤400 全绿;probe 浅色与深色都过。
2026-11-11 06:34:24 +08:00
AskaEth 35dbefb08d feat(app-iced): 禁用态合成底改成容器给的 Base(M234)
`interaction::disabled` 的四个入口一直收一个「底」,但 8 个调用点全传死
`Role::Surface`:卡片(SurfaceContainer)/ 面板(SurfaceContainerLow)里的禁用控件
把 10%(按钮族)/ 12%(面状控件族)叠在画布色上 —— M223「CR 1.05、形丢失」的根因。

- 新增 `interaction::Base`(画布 / 容器底的一等参数,`From<Role>` 兼容历史调用点);
  四个入口改成 `(ui, base, …)`,内部不再出现任何写死的 `Role::Surface`。
- 容器导出自己的 `BASE`:`surfaces::elevated::BASE`(卡)、`inputs::menu::PANEL_BASE`
  (菜单面板,项的 38% 与状态层都改叠它)、`navigation::drawer::BASE`。
- 控件收 `.base(..)`:Button / icon_on / Field / SearchBar / Slider / Checkbox / Radio /
  Switch / Chip / ListItem / Tabs;Fab 与 icon() 是画布默认值。
- `feedback/action.rs` 的本地 α 合成删掉,改走 `disabled_content_over_role` 并给真实容器底
  (Snackbar `InverseSurface` / Dialog `SurfaceContainerHigh`)。
- 判据:`interaction::disabled::tests`(卡片/面板/画布三档逐值)、
  `buttons::tests::base`(Button::base → StyleSpec → style_closure 接线)、
  `gallery` 新增 `disabledbase` 段 + `gallery::probe::bases` 精确像素断言
  (±10 容差区分不了 1/255 的差,所以自己数逐位相等的像素)。
- 变异自证:assemble 的调用点改回画布底后,上面两条都红(见 artifacts/m234/README.md)。

已知未尽:`crates/app-iced/src/pages/**` 不在 scope 内,页面侧「把控件放进卡片/面板」的
调用点尚未接 `.base(..)`(行为与本 mission 之前一致);`pages/…/java/tests/marker.rs`
有一行被迫更新(入口多了一个实参,语义不变 = 画布底)。
2026-11-11 06:34:23 +08:00
AskaEth a9d1cb4c44 Merge branch 'feat/instance-saves-read-only-list-mvp-storem'
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-11 06:00:29 +08:00
AskaEth f217d83680 feat(saves): 实例存档 / 截图的只读列表与打开文件夹(M235 STOREMVP)
用户诉求是 PCL 差距四项里的「存档截图 / 导入」,M230 审计确认三层全缺。塔裁决 U3
把这一版钉成「只读列表 + 打开文件夹」,不做缩略图墙(缩略图要引图片解码依赖,而
AGENTS §2 只允许 palette-extract 引 image 类依赖;截图/导入产物形态也没定型)。

core
- `game::saves`(新):只读列 `<run_dir>/saves`(世界目录)与 `<run_dir>/screenshots`
  (图片文件)。只调 symlink_metadata / read_dir / metadata,不建目录、不写文件、不读
  文件内容(`icon.png` 只判存在)。目录不存在 ⇒ 空列表不是错误;存在但不可用(符号
  链接 / 不是目录 / 读不了)⇒ 具名错误。条目级异常逐条登记原因,不静默丢。
- `platform::opener`(新):`open_dir` 是**目录**打开器的唯一收敛点(explorer / open /
  xdg-open),内部复用 `child::quiet`,cfg 分支只此一处。塔明确要求按 AGENTS §6 放
  platform 而不是业务模块,并要一条可执行判据防退化
  (`the_launcher_literals_live_here_only` 扫 `crates/core/src`:除本文件外不许出现
  `"explorer"` / `"xdg-open"` 字面量)。

presenter
- `dto::saves`:SavesCommand(List / OpenFolder)、SavesListView、SavesFailureView
  (结构 + OS 原文,不拼中文)、`SavesListView` 的计数助手。
- `host::saves`:走 `job::spawn`,UI 只收快照;运行目录仍是
  `isolation::derive_game_dir` 的同一个答案(与启动 / mod 列表同源,**不写盘**)。
  「打开文件夹」打开哪个目录由 host 定:`saves/` → `screenshots/` → 运行目录。
  失败分三类(InstanceUnavailable / DirUnusable / OpenFailed),可重试。
- `state::saves`:快照整体替换、迟到结果丢弃、失败保留绑定(重试才有意义)。
- 接线:CoreEvent::Saves / AppCommand::Saves / HostTask::Saves。

app-iced
- `pages::instances::saves`:详情页 SAVES 卡片(MODS 之后、启动参数之前;**不套第二层
  滚动条**,整列那一条是唯一入口)。世界行带 UTC 时间与「有图标」,截图行带大小与时间;
  第一行固定画「这一版只读」(U3 边界让用户看得见)。读盘失败画一条红字 + 「重试」;
  打开文件夹失败**不画**重试(那个动作的重试就是再点一次按钮)。
- 接线:Message::{ReloadSaves, OpenSavesFolder}、`sync_saves`(与 `sync_mods` 同一判据)、
  Model::saves、`saves_shot` 出图夹具(ULL_SAVES_FIXTURE=1)。

i18n
- `InstancesSaves*` 20 条中英成对,catalog 与金表在**同一条锚点**后追加、逐字节一致。

证据
- core 只读判据:`game::saves::tests` 在真盘建世界/截图,调用前后各拍一次
  (相对路径, 是否目录, 字节数, mtime)指纹并逐条比对;另有用例钉「目录缺失不建目录」。
- presenter:`host::saves::tests` 真盘端到端(custom 档位派生运行目录 → 快照)。
- GUI:`--software-renderer` 1180×760 浅/深各一张 + 1180×1280 两张「整块卡片可见」
  (1180×760 下 SAVES 卡片位于折叠线附近,只能看到卡头;第二对用 ULL_WINDOW_SIZE
  把窗口拉高,口径与 M98 的矩阵一致),落 `DevDocs/artifacts/M235/`。

没做的(U3 边界内,见 handoff):缩略图墙、导入、删除/重命名、`level.dat` 的
LevelName 解析(世界名暂时就是目录名)。
2026-11-11 06:00:27 +08:00
AskaEth 3789cb7e61 Merge branch 'feat/fix-launch-dry-run-disk-side-effect-dryr'
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-11 05:27:43 +08:00
AskaEth c917bfa18f fix(core,cli): M231 复审三条 P2 —— 恢复误删用例 / 收窄 --dry-run 文案 / 判定链去重
P2-1:`natives_cases.rs` 里 `missing_native_jar_becomes_missing_component` 是上一轮
批量截断时**误伤**掉的(我把「tree_snapshot 之后的所有内容」整段砍掉,连带这条与
dry-run 无关的用例),不是有意删。现已从 main 原样贴回,`MissingNativeJar` /
natives 组件缺失这条用户可见失败路径恢复覆盖。

P2-2:help 的「也不写盘」是过度承诺 —— `commands/launch.rs` 的 `launcher_log::set_dir`
/`record_start`/`record` 都在 `if args.dry_run` 之前,dry-run 仍会 append
`<数据根>/logs/launcher.log`(连父目录 create_dir_all)。文案收窄成「不碰版本树
(不解压 natives、不建 natives 目录);启动器自身的运行日志照记」。core 的 `DryRun`
文档同步加了「本层承诺 / 调用方日志另算」的边界。

P2-3:`extract` 与 `preview` 的「无 jar / 戳记命中 / 重解」判定链合成 `inspect()` 一份,
返回 `DirStatus`(NoJars / Ready(files) / Stale)+ 源 jar 指纹;两个入口只在这里分叉。

另:`tree_snapshot_notices_a_transient_directory` 在全量并行跑时偶发红(本轮门禁抓到 1 次,
单跑 3 次全绿)——「建了立刻删」对父目录 mtime 的影响会被 NTFS 合并。换成确定性自检
`tree_snapshot_sees_new_nodes_and_mtime_changes`(新节点 + 同内容重写的 mtime 变化),
并把这条能力边界写进 `tree_snapshot` 的「抓不住」。
2026-11-11 05:18:56 +08:00
AskaEth 1fa30c4885 fix(core): launch --dry-run 不再落盘(natives 只预览,零建目录/零写文件)
`ull launch --dry-run` 在「环境准备」段走的是真解压:即使只打印命令行,也会在
`versions/<id>/natives/` 下建目录、解 dll、写戳记,把别人的版本树改脏(父目录
mtime 也变)。失败路径(缺 jar 报 missing_components)照样留下这些副作用。

- natives 新增 `preview()`:与 `extract()` 共用条目筛选与戳记口径,但零落盘
  (无 create_dir_all / clear_natives / write_stamp),只清点「将写下几个原生库」;
  jar 打不开照样报错,预览不藏坏包。
- `NativesReport` 增 `planned` / `dir_exists`:预览报告自报家门,人读行与 JSON
  说清「未落盘」「目录尚不存在」。
- `Launcher::dry_run` 改走 `preview_environment`,`spawn` 仍走 `prepare_environment`。
- 报告类型拆到 `natives/report.rs`(mod.rs 曾破 400 行红线)。
- 回归测试(natives_dry_run_cases.rs):有 jar / 无 jar / 空 jar / 已解压过四条
  零落盘判据,用「节点类型 + 纳秒 mtime」快照逐字比较版本树,并带一条判据自检
  (瞬时建删目录必须被快照看见,防假绿)。
- help:`--dry-run` 的承诺补成「不写盘、不解压 natives、不建目录」。

证据(外部版本树 versions/1.16.5 + 真 natives jar):
修复前 dry-run 后 versions/1.16.5 mtime 1794342830.6861426 → 1794342832.4544542,
新增 natives/ + lwjgl.dll + OpenAL.dll + .ull-natives.json;修复后同一场景快照逐字
不变(mtime 1794343730.6618167),JSON natives.planned=true / dir_exists=false。
2026-11-11 05:18:54 +08:00
AskaEth 774a547aa0 Merge branch 'feat/clean-stale-comments-and-cli-loader-text'
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-11 04:52:10 +08:00
AskaEth 38d1f3c20f docs: DOCFIX-2 收口三处旧口径注释(M232)
- cli/cli/model.rs:147 `InstallArgs::loader` 文档补 `liteloader`(第六个可装取值),
  并点明 `optifine` 是「认得出但装不了」→ `loader_unsupported`;
- commands/instances/tests.rs:103 删掉「只在认得出但装不了的那两家会红」的旧口径,
  改成真实理由(第三方前缀形状、没有 `<mc>-<kind>-<ver>` 段);:231 同句副本改成
  「optifine 装不了 / liteloader 自 M222 起能装」;
- addon/loader/liteloader.rs:44 `DEFAULT_REPO` 文档由「BMCLAPI 有 bmclapi-liteloader
  改写规则」改为「直连官方」:该规则自 M225 起 mirrors.json 里是 enabled=false
  (镜像侧实测 404),并按 M225 finding 的方向补上「库走 CENTRAL_REPO」。

纯文档,零行为变化。
2026-11-11 04:38:53 +08:00
AskaEth 654e12c100 Merge branch 'feat/m229-ink-cat-skin-rollout-app-iced-inksk'
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-11 01:49:19 +08:00
AskaEth 36bac74822 docs(app-iced): probe 模块文档的复现命令补 SECTION(M229 复审 r3 P2-1)
r5 报告说「模块文档复现命令已补 `ULL_GALLERY_SECTION`」,但 diff 只碰了 `shot_path()` 与
测试体——模块文档 9-15 行一个字没改,照抄那条命令仍 exit 101(panic 报只支持 buttons/divider)。

这轮补齐:
- 模块文档复现命令改成两组(`ULL_GALLERY_SECTION=buttons` / `=divider`),产物路径用
  `${CARGO_TARGET_DIR:-target}`(本仓一树一 target,旧命令的 `./target/debug` 在 worktree 里根本
  没有 exe),并写明 `all` 不支持及其理由;
- `shot_path()` 的文档改真:它只读 `ULL_GALLERY_SHOT`,「这张图属于哪一段」由测试体开头那条
  assert 管(assert 早已搬走,且现在也认 divider)——原文还写着「此处不满足就失败」;
- 收掉 assert 文案里的九连空格接缝;
- 矩阵带过滤的注释从「`all` 默认会画全段」改成「buttons 段没有后续段,过滤是防御性的」。

自证:按文档两组命令实跑,四条命令全部 exit 0;`grep -n ULL_GALLERY_SECTION probe.rs` 命中
8/15/17/21/23(模块文档)与 144/150/357(实现)。
2026-11-11 01:49:16 +08:00
AskaEth f5a2610e4d docs(app-iced): 值字级注释改真 + probe 只认 buttons/divider + 截图主题钉死回归测试(M229 复审 r2)
1) P1-2 的注释值与量测对齐(四处):`elevated.rs` 的 `PAD`/`Texts::title`/`texts()` 与
   `pages/instances/detail.rs` 还写着「值 14/400」,而实现是 12/400;`PAD` 文档更把 12px 的
   118px 量测挂在「14px 下」。全部改成真值,并把 14px=138px 与 12px=118px 两个量测并列写清。

2) probe 门禁只解决一半:`SECTION=buttons` 绿、`SECTION=all` 仍红(sizes 段的实心块撞「按钮
   40 高」判据),断言文案却写「all 或 buttons」。现在:
   - 只支持 `buttons`(按钮矩阵,实心块判据**只量矩阵区**,后续段不再参与)与 `divider`
     (分隔线两条判据,独立出口);
   - `all` / 其它 / 未给 → 明确失败并给理由(整页比 1180×760 高,分隔线段与按钮矩阵不可能同屏;
     整页图的 chip/卡片会被当按钮量);
   - 模块文档的复现命令补上 `ULL_GALLERY_SECTION`(以前照着跑会 panic);
   - `gallery/mod.rs` 的「五段」→ 七段;`gallery/buttons.rs` 的「8 行 + icon·standard」→ 9 行 + 四式。

3) 截图通道的主题档钉死此前没有回归测试(复审 r2 P2-a)。新增
   `the_screenshot_channel_pins_the_theme_mode_instead_of_reading_the_file`:两份不同的机器配置 +
   同一组截图参数 ⇒ 同一个档位且等于参数;反证非截图通道下两份配置确实分出两档。
   变异自证:把分支改回 `bridge.load().1` → 该测试红(left: Dark, right: Light)。

门禁:fmt --check 0 / check -p unlucky-app-iced --all-targets 0 / test -p unlucky-app-iced 0
(887 passed + 6 integration)/ clippy -p unlucky-app-iced --all-targets -D warnings 0 / 400 行扫描干净。
probe 亲跑:buttons 浅+深两张 sheet OK,divider 一张 OK,`all` 按预期被拒。
2026-11-11 01:49:16 +08:00
AskaEth 8958b7f67d fix(app-iced): IconButton Outlined 真的画环 + ELEVATED CARD 值串单行(M229 复审 P1)
P1-1:`icon()` 把 `StyleSpec.stroke` 写死 `0.0`,`IconVariant::Outlined` 的描边角色只到
`roles()` 为止、没画出来 —— Outlined 与 Plain 逐像素相同。改成「有描边角色就是 1px」,
并把「角色 → 填充/内容/描边」抽成 `icon_style_spec()`(`icon()` 直接吃它),单测因此能断言
**接线**而不只是断言 `icon_stroke()` 这个纯函数(第一版正是这样漏掉调用点的)。

回归门两条:纯函数/接线(`buttons/tests/icon.rs`,变异回 0.0 时红)+ 渲染层
(`gallery/probe/rows.rs`:图标按钮左缘 4px 带里 Outlined 有环墨、Plain 必须 0 px;
变异后重出图再跑会红)。环在软渲染里是 Outline 叠画布的抗锯齿混色(实测 110/124/110,
本体 123/138/123),所以判据是「这一带有墨/没墨」而不是颜色匹配。

P1-2:ELEVATED CARD 内距 24 + 值 16px 让「2026-10-29 19:22 UTC」折行,同排卡底不齐;
旧测试还断言 20/14(不变量 118+2×padding ≤ 160 变成 166 > 160,假绿)。
按塔裁决「收内距/字号、不破卡片宽度契约」:`PAD` 24 → 20(= card_padding)、值字级 → BodySmall 12。
量测:时戳 14px 墨水宽 138px(160 外框内宽 120 放不下)、12px 为 118px(放得下,余 2px)。
测试改成四条绑一起(字级 12 / 内距 20 / 160−2×PAD ≥ 118 / 2 列实际宽度也 ≥ 118),
并反证「14px 确实放不下」防登记被当噪音删掉;变异回 14px 时红。

P2(顺带):
- 截图通道的主题档由 `--screenshot-theme` 钉死(此前读机器 launcher.json,设置页主题行会漂)
- `gallery::probe` 只认 gallery sheet(必须给 `ULL_GALLERY_SECTION`,否则整页图的 chip 会被当按钮量)、
  行表 8 → 9 行(四式图标按钮)、分隔线判据只在渲染了 divider 段时执行、逐行阈值 40 → 30 并加
  「满档色像素必须少于 38% 像素」的反向判据、probe 拆出 `probe/rows.rs` 守 400 行
- FAB extended 的重复分支合成一处,右内距提取 `EXTENDED_PAD_END`;iced 0.14 无 `Length::Min`,
  「最小宽随标签增长」退化为固定宽已登记
- 五处注释谎言改真(theme/tests、general/theme、elevated、launch_args、app/tests/theme)

`buttons/tests.rs` 加用例后 402 行 ⇒ 图标按钮那组拆到 `buttons/tests/icon.rs`。
2026-11-11 01:49:15 +08:00
AskaEth 6ed26626db docs(app-iced): 登记禁用滑块激活轨 38% 为 Pixel 级偏离 #9(M229 复审 r3)
复审核对出 artifacts README 的归因错误:`page-settings-g1-dark.png` 里禁用滑块轨是
`on-surface@38% over surface`(轨带 6030 px),不是面状控件族的 12%;上一版引用的 7 px
12% 小簇在 Image path 字段标签附近,与滑轨无关。

按塔裁决**不改实现**:规格卡只定义了面状控件族(chip 描边 / switch 轨 / 字段底 / 滑轨未激活段)的
12%,而禁用**激活轨**沿用 M3 `SliderTokens.DisabledActiveTrack` 的 38% —— 两者是两个族。
登记写在本文件参考值表下方(`DevDocs/` 被 gitignore,README 里的同一段无法入库,故以代码为准)。
2026-11-11 01:49:15 +08:00
AskaEth 4ce24d6fbd fix(app-iced): 禁用态与遮罩改从品牌板合成(M229 复审 F1)+ 选择类图注回填(F2)
F1(核心):三处 `Scheme::baseline(ui.flavor())` 自己重算了一份方案,绕开 `Ui` 里注入的品牌板,
于是 Ink 板上的禁用控件与对话框遮罩仍按 tone 派生的天蓝中性合成(复审量到滑块轨道 #58646C、
填充框 #222E36、遮罩 #060E13,全是蓝系)。修法:`Ui::scheme()` 公开已注入的那份,
`interaction/disabled.rs`(两族合成 + 内容色)、`feedback/dialog.rs`(scrim)、
`feedback/action.rs`(禁用动作文字)改为从它取。

修完 Ink 下实测:禁用容器(12%)#292F28、按钮容器(10%)#252B24、遮罩 #0A0E0A(绿系中性,
R = B);Silver 档遮罩 #A9A9AA。旧三个蓝系值在 7 张深色图里出现次数均为 0。

回归门:`disabled_colors_follow_the_brand_board_not_the_tone_baseline`(按板合成 + 与 baseline
不等 + 深色档 G ≥ B)、`scrim_is_scrimalpha_precomposited_onto_the_canvas` 增加「不许退回 baseline」。

F2:`selection/{matrix,mod,checkbox,radio,geometry}.rs` 里「20×20 / rx2 / 外圈 20」回填成实际渲染的
「18×18 / rx4 / 外圈 18」(勾形比例不变 ⇒ 10.8dp),并按塔裁决把搜索药丸容器从
`SurfaceContainerHigh` 抬到 **`SurfaceContainerHighest`**(贴规格卡,与 filled 字段同底);
`search.rs` / `probe/search.rs` / gallery `inputs` 图注同步。展开的搜索面板与菜单是浮层,
仍留 High 档(已在文档里写明不是漏改)。

`interaction/disabled.rs` 加测试后 405 行 ⇒ 单测纯搬移到 `interaction/disabled/tests.rs`。
2026-11-11 01:49:14 +08:00
AskaEth f9877a89d8 fix(app-iced): 下载页页头「内容库」选中态反转(M229 复审 F3)
`pages/content.rs::section_switcher` 把 `on_content` 写死成 `true`,而下载页也调它,
于是版本管理页的页头把「内容库」画成选中、「版本管理」画成未选中。

改成由调用方给位置:下载页传 false、内容库页自己传 true;并加一条源码层面的调用点断言
(iced 的 Element 不可内省,判据只有一行,写错必红)。

不是 M229 引入的缺陷(M109 就有),按复审要求单独提交,不与收官换肤混在一起。
2026-11-11 01:49:14 +08:00
AskaEth db541c820e feat(app-iced): Ink Cat / Silver Cat 换肤 —— 双色板默认 + 规格卡组件几何(M229)
产品出厂外观从 tone 派生的 `Scheme::baseline` 切到品牌色板:深色 = Ink(淡绿)、
浅色 = Silver(钢蓝),D38 取色链(背景图 / 壁纸)仍然优先,取色失败回落到色板。

主题三档接进设置页「通用 → 主题」:Ink / Silver / 跟随系统(落盘仍是
`appearance.theme_mode` 的 dark / light / auto,不新增配置键)。

组件几何按规格卡对齐(M228 的 `inkcat` 登记项逐条落地):
- 卡片圆角 12 → 16;实例元信息卡改用新的 ELEVATED CARD(无描边、SurfaceContainer
  底、内距 24、上标注 11/600)
- Snackbar 48/4 → 52/16(双行 72)
- Checkbox 20/rx2 → 18/rx4;Radio 20 → 18 Ø(内点仍 10)
- 按钮加 XS~XL 五档(产品仍只用 S=40),内距改走 ControlSize::pad_x
- 新增 FAB 四态(40/56/80/160×56)与图标按钮第四式 outlined
- 语言与主题两行 chips 统一成规格卡 chip(32 高 / rx8)

`theme.rs` 原本正好 400 行,先把单测纯搬移到 `theme/tests.rs` 再加色板字段;
`app/tests.rs`、`settings/general.rs` 同样按纯搬移拆分守 400 行硬线。

验证:cargo fmt --all --check、cargo check -p unlucky-app-iced --all-targets、
cargo test --workspace(除 clippy 前的一轮 flake,见报告)、cargo clippy --workspace
--all-targets -- -D warnings、全仓 400 行扫描。视觉矩阵 42 张在
DevDocs/artifacts/m229/(gitignored,不进提交)。
2026-11-11 01:49:13 +08:00
AskaEth a06b7ff4ec Merge branch 'feat/m227-narrow-window-layout-budget-narrowf'
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-10 23:07:23 +08:00
AskaEth 4f5a76f238 docs(download): r2 复审返工——改名后的旧注释三处、视口高度口径如实分档、fade 单测改成对生产值断言(M227)
P2-1(三处改名后旧注释 + 一处口径,纯文档/注释,无像素变化,未重出图):
- `install.rs` 段标题「两个 Shared 分支」→「两支「面板先保预算」的分支(Shared / Narrow)」,
  两条子项分别标注 `(Shared)` / `(Narrow)`(改名后只剩一个 Shared,另一支已是 Narrow)。
- `options/view.rs` 的 `Shared` 条目删掉「(chips 档还可能有错误行)」——chips 档被 arm 1 接走
  给 `Narrow`,`Shared` 走不到错误行;`Narrow` 条目本来就写对。
- `tests/options_shot/chips.rs` 断言③ 的注释:实测这条断言在窗口 518–610 里命中的是
  y=550、宽 94px 的**实心「安装」药丸**,版本菜单容器上沿在 636–639(窗口外)——删掉
  「菜单容器的满宽底边」这个错误替代项,并把实测值写进注释。
- 滚动区高度口径:八审原文恢复为「1180×760 ≈96–104px」(署名真实性),另起一句写 M227 在
  tip 上的复量:1180×760 = 101px、1180×900 = 109px、窄档 = 120px,并注明 r2 量的 116px
  与 109px 只差「钉死区从哪一行算起」。`DevDocs/artifacts/m227/README.md` 同句一并改。

P2-2(fade 单测空转,r2 实测变异 STEPS=6 仍绿):
- 步数与 α 曲线提成 `pub(super) const VIEWPORT_FADE_STEPS` + `pub(super) fn step_alpha(i)`,
  `viewport_fade` 用它们;单测改成对**生产值**断言四条:步数 = 高度像素数(1px/条)、
  α 严格递增且除末条外 <1、相邻增量非递减(曲线要凸)、末条 = 1.0。

变异验证(自己跑,红过再还原):
- `VIEWPORT_FADE_STEPS` 12 → 6:`panicked … 步数必须等于高度像素数:12px 要 1px 一条,
  现在 6 条 ⇒ 2px/条`,MUTATION_EXIT=101;还原 12 后 GREEN_EXIT=0。
- `t.powf(1.6)` → `t.powf(1.0)`:`panicked … 第 2 条比前一条浓得更少…曲线要凸(t^1.6)`,
  MUTATION2_EXIT=101;还原后绿。

门禁(真实退出码,CARGO_TARGET_DIR=C:/Temp/ull-target/wt-227,-j 4):
fmt 0 / check -p unlucky-app-iced --all-targets 0 / test --workspace 0(67 个 ok,
app-iced lib 875 passed,四条像素守门 + fade 单测全绿)/ clippy --workspace --all-targets
-D warnings 0 / .rs 扫描 >400 行 0 个、<2 字节 0 个。
2026-11-10 23:07:20 +08:00
AskaEth 122accdd02 fix(download): 窄档滚动视口底部加 12px 淡出遮罩,去掉被硬裁的「空壳蓝条」(M227 返工)
D30 视觉审 hold 的必改项:滚动视口在 y=660 硬裁,把版本菜单的容器上沿切成 639–659 = 21px、
**零字形墨迹**的一条色块(深色下 (22,33,41) vs 画布 (9,21,28),CR 1.99),读起来像渲染错
而不是「下面还有内容可滚」。

改法(reviewer 推荐方案①,保留发现性):
- 新增 `options/fade.rs::viewport_fade` + `VIEWPORT_FADE_PX = 12`:12 条 1px、α 单调上升的
  **画布色**实色块拼成的斜坡(iced 0.14 没有渐变原语;1px 步进的差别在 1/255 量级);
- `options::section` 的窄档分支把滚动区包一层 `stack![滚动区, 底部对齐的遮罩]`,遮罩贴着
  视口内沿(≤660),压不到钉死区的实例名(≥668);
- 颜色取 `ui.layer(MaterialLayer::Canvas)`:浅色淡出到纸面、**深色淡出到墨底**(不是白雾),
  与它下方那块背景同色,所以两档都不新增颜色;
- 三栏档不铺这条遮罩:那里滚动区内容恒 76px < 视口 ≈118px,不会出现半截内容,铺了只是
  「画布盖画布」,反而会动 M222 已验的 1180 像素;
- `options/view.rs` 已 422 行、撞 400 行硬线,遮罩拆成 `options/fade.rs`(含一条几何自检单测)。

实测(720×760):
- 浅色 x=600 竖列 648 (211,229,245) → 659 (246,250,255) 单调升到画布色;深色 648 (55,72,85)
  → 659 (9,21,28),无白雾;中文与浅色逐值同串;
- 可见带 21px → 墨迹口径 18px + 底 12px 连续渐变(硬裁边消失,右侧滚动条仍在);
- 逐像素零回退:`77d675b`(遮罩前)vs 本轮 720 浅/深/中 + 700 守门,差异 bbox 只有
  (112,648)–(696,660) 这 12 行,其余像素一个都没动;1180×760 浅/深/中 与 1180×900 对
  M222 基线仍 **IDENTICAL**。

门禁:fmt ✓ / check ✓ / test --workspace(喂四张守门图)exit 0(app-iced lib 860 passed、
四条像素守门 + 400 行门全绿)/ clippy --workspace --all-targets -D warnings exit 0 /
.rs 全仓扫描 >400 行 0 个、<2 字节 0 个。

已知残留(本轮不动,按债务台账口径登记):menu 的 L2 阴影不随滚动视口裁切(压在实例名输入框上
约 6.9%,三栏档 fabric 同样存在)——根因在 `components::inputs::menu`,不在 M227 scope,
待专门 mission;finding `.tower/comms/findings/20261110-worker-m227-bug-l2.md`。
2026-11-10 23:07:20 +08:00
AskaEth a70d5f8010 fix(download): 窄档面板加专属预算,首屏露出 head+六枚 chip+说明、实例名回到 56px(M227)
720×760(chips 档)原先走 `TailShare::Shared`:面板与列表各吃 Fill 的一半,面板钉死区
(说明 + 菜单 + 实例名 ≈180px)把它吃穿 ⇒ 滚动区视口 0,head(版本 id + 安装按钮)、
六枚加载器 chip、说明一个像素都不在屏,实例名被挤成 35px 且降部被自身边框裁掉
(M222 D30 五审 `artifacts/m222/visual-verdict-r5-hold.md`)。

改法(把 M222 在 1180 档验证过的「面板先保预算、列表吃整行」落到窄档):

- `TailShare` 拆出第三支 `Narrow`(chips 档 + 面板占高度),`of()` 的三条 arm 与
  `Shared` 的机制注释按 r8 finding 重写(旧文档只写 M125 那一条条件);
- 新增 `CHIPS_STACK` 121 / `CHIPS_LIST_CHROME` 119 / `CHIPS_PANEL_BUDGET` 192 与
  `narrow_rows()`:720/760 下面板先拿预算,列表 = **132px = 3 整行**(M125 像素守门的下限);
- `options::section` 三分支:窄档钉死区**只留实例名**,head / 六枚 chip / 说明 / 版本菜单
  进滚动区(滚动区 120px ≥ 首屏 104,菜单露容器上沿 + 滚动条可见);
- `TailShare::list_height()` 把「列表吃多少」收到一处,两个装配点共用(三栏档行为不变)。

取舍:窄档列表 4 整行 → 3 整行;菜单从钉死改为可滚。理由(459px 里「钉住菜单」+「3 整行
列表」互斥)写在 `rows/tail.rs::CHIPS_PANEL_BUDGET`。

实测(tiny-skia 出图 + 自写 measure.py,DevDocs/artifacts/m227/):
720×760 分隔线 559→514、滚动区 0→120、head 539–578、chips 583–614(32px)、说明 623–634
(8.98:1 浅 / 10.83:1 深)、实例名 668–723 = 56px(inset 12/9/17);
700×760(options-fabric)实例名从「完全不在屏」回到 56px;四条像素守门全绿;
1180×760 浅/深/中 与 1180×900 **逐像素零变化**。
2026-11-10 23:07:19 +08:00
AskaEth 6c060a09e2 Merge branch 'feat/m228-ink-cat-dual-theme-design-tokens-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-10 22:34:08 +08:00
AskaEth 15309394ac docs(design): M228 复审 r2 两处文档口径(Ink 矩阵上限、测试指针)
P2-1 统一全矩阵口径:palette/visibility.rs 实测表 Ink 行第二列「1.54 ~ 12.13」是旧的「对
surface-container-highest 单一层」量法,改成 4×5 全矩阵(4 个状态容器 × 5 个容器层)的上限
**16.47**(primary-container 压 surface-container-lowest),下限 1.54(secondary-container 压
highest)不变——Silver 行本来就是全矩阵(1.02 ~ 1.80),现在两行同口径。表头补「4×5 全矩阵」
说明,避免下次又被按单层读。

P2-2 修正失效指针:visibility.rs 模块文档的「实测」小节把测试指向
`tests/ink_cat_boards.rs`,但容器层可视性那几条在 r1 返工时已搬到
`tests/ink_cat_visibility.rs` —— 改指后者。boards/mod.rs:42 的「26 组文字/图标对
(tests/ink_cat_boards.rs)」仍正确,未动。

纯文档两行,无数值/代码变更。fmt --all --check=0;test -p unlucky-design --all-targets=0
(267 passed = 186 lib + 81 集成;ink_cat_boards 7 / ink_cat_visibility 4 / ink_cat_geometry 3)。
2026-11-10 22:34:05 +08:00
AskaEth b8003177d7 fix(design): M228 复审 r1 三条(字段底角色对齐、四处小疵、产品默认口径)
P2-1 字段底角色不对称(塔裁决方案 a):Silver 的 #dcddde 从 surface-container-high 改判到
surface-container-highest,与 Ink 的 #262b25 同位——component_map 的 TextField 口径就是最高
容器层,单一角色接法才不会一边掉层。Silver 的 high 改取推导值 #e4e4e5(tone 90),阶梯
94.12 → 90.61 → 88.07 保持单调、步长 ≥1.74 L*。实测:Error on 新 highest = 4.686(原按单一
角色接到 high 时的 4.379 失守已消除)、OnSurfaceVariant = 6.383、OnSurface = 12.576。
测试同步:Silver 三条字段用法 pin 从 high 改 highest(与 Ink 对称),并新增
`field_backdrop_is_the_same_role_on_both_boards` 把这个不对称钉住(顺带断言 high 比 highest
更亮、highest 就是规格卡的 #dcddde)。

P2-2 四处小疵:
① inkcat/size.rs 扩展 FAB 起始内距 16 → **28**(规格卡画的数):框 x=292 → 图标盒 320(`+`
   font-size 24 居中于 x=332),28 才是图上的距离;16 是 M3 官方口径,已在 doc 里登记为偏离,
  并说明「要回 M3 的 16 得同时把图标中心挪回 316,那是要出截图的视觉改动」。测试补一条自洽
   断言:292 + 28 + 24/2 = 332。
② inkcat/metrics.rs 的 SWITCH_THUMB_INSET 注释重写(原句断成碎片且读起来像算错):写明
   轨道 y=1060..1092 / 拇指 y=1064..1088 ⇒ 上下各 4、右端 116−112=4,以及行程
   52−24−2×4 = 20 的推导。
③ Silver「状态容器 / 容器层」数字统一成实测口径:boards/mod.rs 与 palette/visibility.rs 都写
   「最低 1.02」(Ink 1.54),并点名最差的一组是 tertiary-container 压 surface-container-high
   = 1.016;visibility 表的 Silver 阶梯步长改写 −1.7 ~ −4.1。
④ inkcat/type_scale.rs 正文清单只列 11 个**文字**字号(去掉 FAB 字形 20/26/34),并注明那三个
   归 FabSize::icon_px、SpecText 没有 20 这一档。

P2-3 产品默认口径统一:palettes.rs 的「两个入口」表与 Palettes::baseline 文档、color/mod.rs
的「出厂默认色」小节、Scheme::baseline 文档统一写明「M228 起出厂外观入口是
DesignTokens::brand,本入口是 D38 取色链中性锚点」,与 lib.rs 同口径;顺带把 BASELINE_SEED /
DEFAULT_SEED / DEFAULT_NEUTRAL_CHROMA 的「产品默认」措辞改成取色链口径。

另:tests/ink_cat_boards.rs 已 400 行贴线,把 4 条容器层可视性测试拆到新文件
tests/ink_cat_visibility.rs(纯搬移 + 补 import);palettes.rs 因新增口径说明到 403 行,按
「合并相邻段落、不删信息」压回 399 行;size.rs 现在 323 行。

五步(design 口径):fmt --all --check=0;check -p unlucky-design --all-targets=0(零 warning);
test -p unlucky-design --all-targets=0(267 passed);clippy -p unlucky-design --all-targets
-- -D warnings=0;400 行扫描只剩 README.md(豁免)、无 <2 字节 .rs。
2026-11-10 22:34:05 +08:00
AskaEth 0169d4c8fb feat(design): 加 Ink/Silver 双主题色板 + 规格卡几何与字号令牌(M228)
规格卡 DevDocs/artifacts/components-ink.svg 的令牌化,只动 crates/design,零依赖不变。

- color::boards:BrandBoard(Ink 深 / Silver 浅)→ 49 角色 × 2 套固化色板。
  spec 直写值进 ANCHORS(测试逐条比 hex,规格卡改了代码不改会红);规格卡没给的
  角色按「同色相 + M3 tone 关系」推导后固化,规则写在 boards/ink.rs 的行注释里。
  Scheme::from_board / DesignTokens::brand 是两个新入口;Scheme::baseline 一字未改
  (它是 D38 取色链的中性锚点,改它等于同时改 app-iced 观感 + 取色语义)。
- palette::visibility:容器层可视性裁决,即债务台账「SecondaryContainer 对 Surface
  仅 1.06」的令牌层结论。状态容器(*-container)对 surface ≥1.20:1 是唯一数字门禁;
  容器层之间不设亮度门禁,改为「选中态 = 容器底 + 文字对比 + 形状/图标」三件套的构成
  要求(浅色板实测 1.05~1.24,任何有意义的门禁都过不了,详情见模块文档实测表)。
- inkcat:规格卡的尺寸与形状数据。ControlSize(按钮 XS~XL = 32/40/56/96/136,圆角
  一律半高)、FabSize(40/56/80/160×56,圆角 12/16/20/16)、控件方框(卡片 16、
  对话框 28、snackbar 52@16、列表行 60 药丸、chip 32@8、字段 4/28 + 内距 16/32、
  复选 18@4、单选 18、开关 52×32 + 拇指 24、进度 4/6)、SpecText(规格卡 11 个字号
  → 命中 / 落档 / 低于 11px 下限三类结论)。
  component_map 的表值一处未动:app-iced 的 pages/frame/tests.rs:42 与 shell/tests.rs:30
  按 Medium(12) pin 住卡片,改表等于连带改观感且破坏既有断言;差异在 inkcat 模块文档
  逐条登记,换肤时由 M229 一次切换 + 截图验收。
- 测试:186 lib(+13 条新单测)+ tests/ink_cat_boards.rs(10 条:完整性/锚点/双主题
  切换/26 对 WCAG AA/25 组规格卡用法/状态容器 1.2 门禁/阶梯单调)+ tests/ink_cat_geometry.rs
  (3 条:按钮 FAB 几何、控件圆角、字号校核)。
2026-11-10 22:34:05 +08:00
AskaEth 7fe38fb240 Merge branch 'feat/m224-local-mods-enable-disable-modtoggle'
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-10 22:07:09 +08:00
AskaEth 71144dfca8 Merge branch 'feat/m223-gui-adopt-import-wiring-guiadopt'
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-10 20:18:14 +08:00
AskaEth 44c00b4873 fix(app-iced): 测试助手改用 matches!,消掉 deny 级 clippy(复审 r2 P1-1)
`dispatched_adopt` 内层 `match (&**command, want) { … _ => false }` 被判
`match expression looks like matches! macro`(deny 级 ⟹ CI 的 `--workspace --all-targets
-D warnings` 必红;558fbf2 原树 `cargo clippy -p unlucky-app-iced --all-targets` 实测 exit 101)。

改成 `let AppCommand::Adopt(..) = command else { return false }` + 一条 `matches!`,
判据与原来逐字等价(四种命令种类 × 对应 slug)。同一处的注释补一句「为什么不能再写 match」,
免得下一个人改回去。

教训记账:r2 自验只跑了 fmt/check/test,漏了 clippy(T23「五步少一步等于那步不存在」);
本轮五步齐跑,退出码见完成报告。
2026-11-10 19:37:45 +08:00
AskaEth fa3fab9086 fix(app-iced): 复审 r1 三条(在飞守卫补 Back/Target、预检按版本过期、shell 文档如实)
P2-1(行为):`Busy::Importing` 的早退只盖了 Close。「返回」会派 Scan、「换目标档」会派
零写盘 Preview,两条都把 `adopt_epoch` +1,于是在飞的 Confirm 交卷时 Done 被代次丢掉——
实例写进去了,界面什么都没说(正是 Close 守卫要防的形状)。两处补同款早退,且守卫写在
改本地状态**之前**:挡下来的换档不该把界面改成 B 档而盘上写的是 A 档。

P2-2(显示陈旧):`Form::fresh_plan` 原来只按目标档过期。先选 1.20.1 + 版本目录档,再改选
1.19.4,目标档没变,旧预检的 notes(说的是 `versions/1.20.1`)会继续当新版本的后果原文画。
现在连 `plan.version_id == selected` 一起判;没选中版本时也返回 None。同处那句
「版本一致性由 Self::candidate 那一路保证(选中即重扫)」与代码不符(Select 只改本地字段、
不发命令),改成如实说明过期判定只能在这里做。

P2-3(文档):`app/adopt/shell.rs` 的「纯搬移:函数体一行未改」不成立——同一提交里卡壳几何、
页脚去重、预览摘要都改过。改成逐条列出改了什么与为什么。

用例(两条都是先写红再修,红时 panic 信息就是错在哪):
- back_and_target_are_ignored_while_the_registration_is_in_flight(记账后端断言「不许派
  Scan / 不许派 Preview」);红:`登记在飞时 Back 不许派 Scan(会把 Done 的代次顶掉)`;
- fresh_plan_expires_when_the_selected_version_changes;红:`换了版本之后旧预检必须过期
  (否则画的是 1.20.1 的后果原文)`。

顺带补图:`ULL_ADOPT_STATE` 加 `pick-selected` 一档(选中行底色 + 预检启用态),
出图 `DevDocs/artifacts/m223/adopt-pick-selected-light.png`(r1 复审说这格只有代码没截图)。
2026-11-10 19:37:45 +08:00
AskaEth 6ffbd51f60 feat(app-iced): 向导的出图种子 + 卡壳几何修正(M223 视觉自检)
D30 的截图通道在本机可用(`--software-renderer` 走 tiny-skia,1180×760 落盘),所以不再把视觉
证据留空:新增 `ULL_ADOPT_STATE`(与 `ULL_JAVA_STATE` / `ULL_AI_STATE` 同一族,**只在
`--screenshot` 下生效**),可把向导摆到七种状态之一(pick / empty / preview / shared / failed /
done + 深色),于是出图不靠「赌异步时序」。

顺带修掉三处只有出图才看得见的问题:

1. **卡壳把页脚裁掉了**:内容高于 `max_height` 时 iced 直接裁,第一版 PICK 段的「实例名」与
   按钮整排不可见。现在列表段(Pick/Failed)定高 + 中间区可滚、页脚固定;短内容段
   (Preview/Done)按内容收高,不留一大片空白;
2. **两枚一模一样的「关闭」**:Failed/Done 的主按钮就是关闭,文字关闭不该再出现;
3. Preview 段少了实例名一行,且标题下的摘要重复了「共用目录 · 共用游戏目录」。

夹具只造候选/问题/运行目录这些**版面数据**,后果原文仍由 core 的 `target_notes()` 现算,
图证明版面与文案、不证明 core 判据(见 `DevDocs/artifacts/m223/README.md` 的真/假表)。

为守 400 行(`components::gate` 实测 412 行红),卡壳/标题/页脚纯搬移到 `app/adopt/shell.rs`。
2026-11-10 19:37:44 +08:00
AskaEth a36e17a5d3 fix(app-iced): 点选共享档立刻显示后果原文,登记在飞时不吃关闭(M223 自检)
两处自己复审抓到的问题:

1. 契约 §5.2 要求 Shared/VersionDir 被**点选**就展示 target_notes() 原文,而上一版要等
   用户再点一次「预检」才出现。现在点选这两档会顺手发一次**零写盘**的预检,结果落在
   Pick 段当提示读(不把用户甩到确认页:确认仍由显式的「预检」按钮触发,Form::checked)。
   改档之后旧 plan 因目标档不一致而不再显示(Form::fresh_plan),避免画上一档的后果原文。
2. 「关闭」在登记在飞时被忽略:core 那条线程已经开跑,关面板会让 Done 事件被代次丢掉,
   用户看到的就是「实例写进去了,界面什么都没说」。

用例:a_foreign_target_shows_its_notes_without_jumping_to_confirm /
choosing_a_foreign_target_previews_but_waits_for_the_user /
closing_is_ignored_while_the_registration_is_in_flight。
2026-11-10 19:37:43 +08:00
AskaEth 9ca5ee8ae7 docs(core): adopt 的两条文档小疵(r4 复审留的,M223 顺手修)
- plan.rs 第 11-12 行:一个悬空半句(「…必须落在」后面断掉)补完,并把 fspaths 的引用接上;
- run_dir.rs 第 16 行:删掉「配置写面看 <.ULL>、并额外排除游戏根」这条**已移交**
  fspaths::ensure_surfaces_clear_of_game_tree 的职责描述,明说本模块只负责数据写面这一条。

纯文档,零行为变化。
2026-11-10 19:37:43 +08:00
AskaEth 41caae3571 feat(app-iced): 实例页「导入已有版本」向导(M223)
页头「导入」按钮(以前是占位动作,点了只打日志)现在打开四段向导:

| 段 | 画什么 |
|---|---|
| Pick | 候选版本(可导入的才可点)+ 目标三档 chip + 实例名(缺省 InstanceDir) |
| Preview | 生效运行目录(回程给的)+ 隔离档 + target_notes() 原文逐条 |
| Failed | 稳定码 + problems 逐条(不折叠成一句「导入失败」) |
| Done | 实例名 + 运行目录 + 提示原文 |

- 向导是叠在页面之上的一层(遮罩 + 居中卡),不搅乱 list-detail 的版式判据;
- 表单态在 App(app/adopt.rs 的 Form),事件真相在 presenter 的 AdoptState;
- 在等回程时主按钮禁用(连点「确认」不会写两条实例);
- i18n 中英目录 + 金表补 27 条向导文案(实例域末尾追加)。
2026-11-10 19:37:42 +08:00
AskaEth 5a59cb33b2 feat(presenter): adopt 的指令/事件/任务接线 + install 那句假话变真话(M223)
- AppCommand::Adopt(Box<AdoptCommand>)、CoreEvent::Adopt(Box<AdoptEvent>)、
  HostTask::Adopt { event, epoch }:三条穷尽枚举各加一条变体(代次独立于安装与 pack);
- dispatch.rs 一条分支、handle.rs 一条分支、state.rs 的 adopt 字段与一条 apply 分支;
- host/install.rs 的「还没有接入安装管线…请先在实例页导入已有版本」改成指向真入口:
  那里以前是一句假话(实例页根本没有导入动作),现在向导落地了,它才成立。
2026-11-10 19:37:41 +08:00
AskaEth 26e4fbb91d feat(presenter): 导入已有版本的 DTO 与三条后台线程(M223)
core 的能力(M221)接进 presenter 的 IPC 面:

- dto/adopt.rs:AdoptCommand / AdoptEvent / 六种视图 / AdoptState + AdoptStage。
  全部 Serialize+Deserialize,**不 import 任何 core 类型**(契约 §5.4);
- host/adopt.rs:spawn_scan(adopt::scan)/ spawn_adopt(plan 或 adopt),结果经
  HostTask::Adopt 回流;core 的候选/计划/结果/失败逐字段搬进 DTO,problems[] 一条不少;
- CoreHost 加 adopt_epoch(每条命令 +1、关面板也 +1),连点与迟到的回程不会点亮面板;
- 登记成功后补一次 refresh_instances(新实例这一拍就出现在列表里)。

运行目录只从 plan()/adopt() 的返回值取(缺省档可能带 -2 后缀,只有 core 知道用了哪个);
target_notes() 的原文随 AdoptPlanView::notes 上屏,界面不另写一份后果话术。
2026-11-10 19:37:23 +08:00
AskaEth 9ab909d01c refactor(presenter): 两处纯搬移腾出 400 行余量(events 用例 / AI 栏事件投递)
events.rs 399 行、state.rs 400 行(AGENTS §3.1 与 §3.9 的零余量贴线文件)。M223 要往
AppCommand/CoreEvent 各加一条「导入已有版本」的变体、往 AppState 加一份事件态,都得先腾地方。

纯搬移、一行未改:
- events.rs 尾部 #[cfg(test)] mod tests(80 行)→ dto/events/tests.rs(399 → 321);
- state.rs 的 apply_agent + agent_rail_visible(28 行)→ state/agent.rs(400 → 376)。

方法路径不变(impl 写在子模块、pub use 无需新增),断言一条未动,搬移 diff 可逐行对。
2026-11-10 19:37:23 +08:00
AskaEth 13a317d35c Merge branch 'feat/m226-fix-instances-module-doc-stale-clai' 2026-11-10 19:36:47 +08:00
AskaEth 938ce9866b docs(cli): instances 模块文档的约定二限定到 optifine,注明 liteloader 自 M222 可装
同一句假话(optifine / liteloader 都装不了、--loader 只有五个取值)在 cli/help.rs 的那份
已由 55e2f23 修好,这里是第二份副本。现在把限定收窄到 optifine(要跑外部 Patcher,报
loader_unsupported,只能继承进来),并写明 liteloader 自 M222 起可以 `install --loader
liteloader`,取值表指向 `ull help install`(原来指向 crate::cli::model——那份字段文档自己
也是旧口径)。认得出仍是七个,装得了是六个。

纯模块文档改动,不动任何断言。
2026-11-10 19:36:38 +08:00
AskaEth 77f73a307f Merge branch 'feat/m222-liteloader-install-route-and-suppor'
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-10 19:36:09 +08:00
AskaEth a16aadd467 docs(app-iced): 按 TailShare::of 的真实分支清扫下载页面板注释(r7 三条 + 全量扫)
(i) 上轮新引入的错误声明 rescope:错误行「恒为 None / 滚动区只有 head+chips」只在**三栏档 +
    带版本列表**这一支成立(has_loader_versions() 要求 problem.is_none())。**chips 档**(可用宽
    < 600,TailShare::of 对 Options/Install 无条件 Shared)里 problem 可以是 Some(静态不兼容 /
    列表取不到 / 空表三种),错误行会进滚动区且可能被滚走;show_versions() 为假时钉死区只剩
    实例名。install.rs 与 options/view.rs 都按这两支分别写清,并注明该行为域由 M227 重设计。
(ii) install.rs 的「显式给对半分档标志」「对半分:面板吃满自己那一半」改成预算优先口径
    (与同文件十行上方「不是早期的对半分」不再矛盾)。
(iii) rows.rs 三处:三栏档「照旧钉死」限定为「**不带版本列表**的选项区」;`content` 的 share
     说明改成引用 TailShare::of 的真实两支,不再复述「三栏钉死 / chips 各吃一半」;
     「只有对半分那一档才按整行取高」改成「只有 Shared(面板先保预算)那一档」。

全量扫附带修正:tail.rs 的 ROW_PITCH / PANEL_BUDGET 账(760 例改成 176/437、900 改成 569;
预算拆成滚动区 76 + 钉死区 153);options/model.rs 的 has_loader_versions 文档改成预算口径。

零代码零测试变更。
2026-11-10 19:34:15 +08:00
AskaEth 409c742aa8 docs(app-iced): 扫掉 r6 的三处文档残留(B 结构合入前最后一改)
(a) install.rs 的 panel/panel_with 文档按当前结构重写:`shared` 不再是「对半分」(现为
    面板先保预算 + 列表按整行,见 rows/tail.rs::whole_rows);滚动区实际只有 head + 六枚 chips
    (菜单 9358668 进钉死区、说明 a6f7974 进钉死区),钉死区 = 说明或标签 + 菜单 + 实例名;
    并写明错误行在 Shared 档恒为 None(静态不兼容时不画版本菜单、走不滚动那条路)。
(b) options/view.rs 里「标签与说明都搬到钉死区」自相矛盾:改成「**说明**搬到钉死区;
    『安装选项』标签在 Shared 档**删掉**(八审裁决),Pinned 档保留」。
(c) 同文件 section() 文档里「滚动区溢出时被滚走的是错误行」描述的是不可达情形:改成
    「滚动区只有 head + chips,用户可见内容不会被滚走(错误行在 Shared 档恒为 None)」。

零代码零测试变更。
2026-11-10 19:34:15 +08:00
AskaEth 33d05689e8 fix(app-iced): 说明只画一份(删滚动区重复)+ 口径三处改真
八审唯一阻断:中性说明画了两份(滚动区 546–557 与钉死区 576–587 像素级相同、重影)。
删掉滚动区那份,钉死区保留;01/02/05 与 900 的说明墨迹带现在只有一条(576–587 / 716–727)。

口径三处:
1. `section` 文档的滚动区内容改成实际(head + 六枚 chips + 错误行);
2. 旧的「≈150px / 无需滚动」改成八审实测(可见 96–104px、内容 76px、head 墨迹 462–501、
   chips 506–537 四角闭合);
3. 钉死区注释「标签与说明都搬到钉死区」改成事实:Shared 档的「安装选项」标签是**删掉**的
   (设计决定,八审裁决),只有说明是搬下去的;Pinned 档仍保留标签。
2026-11-10 19:34:15 +08:00
AskaEth e2e3c39f61 docs(app-iced): 扫掉 r5 的五处文档漂移(返工链留下的旧描述)
1. install.rs 模块文档:删掉「钉页脚本轮没做」(6f87c8b 已做:钉死区现在是菜单 + 实例名),
   三层滚动的第 3 层改引 `version_menu_height`(VERSION_ROWS 已改),并写明「选项区自己滚」
   这层是 M222 之后才有的、`Options` 态不再套外层 scrollable(只有 `Install` 态用)。
2. rows.rs:五个 loader chip → 六枚;「实例名那一格滚一下就到」→ 已钉死;分法描述改成
   「面板先保预算 + 列表按整行」(PANEL_BUDGET / whole_rows)。
3. rows/tail.rs 的 TailShare 表:两半各 Fill → 面板先吃预算;补上「带加载器版本列表」这一例外行;
   判据注释改成「像素由 window_height 算」(view_sized 已接)。
4. rows/tail.rs 的 M222 分支注释:删「仍欠一项 S1b」(107d619 已修、26ec3f4 已换预算公式),
   写清现在 1180×760 = 176px/4 行、900 = 308px/7 行。
5. options/view.rs 控件树示意:改成当前真实顺序(滚动区 = head + chips;钉死区 = 说明/标签 +
   菜单 + 实例名),并补上钉死档另走一条路的说明。
2026-11-10 19:34:15 +08:00
AskaEth 7ea3dbf8f0 fix(app-iced): 方案 B——说明与「安装选项」标签撤出滚动区,滚动区只装控件
六审算术:滚动视口 103px < 内容 130px,说明 0 像素 + chip 底边框被裁 2 行。方案 C(标签并排)
不可行——六枚 chip 已占 404px、正文列 424px,并排必横向溢出裁掉第六枚,等于用一个已完成项
换另一个。按塔授权走 B:

- **滚动区**(仅 Shared 档):head + 六枚 chips,间距 X2→X1;
- **钉死区**:说明行(有说明时占「加载器版本」标签那一行,否则显示标签)+ 版本菜单 + 实例名。
  钉死区高度不变(两者都是一行),列表仍 4 行;
- **钉死档保持旧结构**:把标签撤出会让原版面板矮 26px、列表涨到 302px(6.86 行、切出半行),
  实测把 `04` 从 531 挪到 563 —— 第一版就是这个坑,已按此回退。

实测:01/02/05 分隔线 437、列表 **176px = 4.00 整行**、说明墨迹带 y 576–587(真像素);
04 **531 未变**;03/06 268 未变;900 档 569 / 308px = 7.00 整行。
2026-11-10 19:34:14 +08:00
AskaEth cb2cffb3ca fix(app-iced,i18n): 分高改「面板先保预算」+ 说明压成单行(D30 五审一次性重分配)
五审:菜单文字/720 列表/44-97 标定/04 都过,但滚动区被压到 82px(宽)/24px(窄)——宽档切
「安装选项」字身、六枚 chip 0 像素。塔裁决停止逐轮调顺序,一次性重分配:

1. **分高从「对半分」改成「面板先保预算」**:`PANEL_BUDGET = 300`(滚动区 128 + 钉死区 136),
   `whole_rows = floor((column − LIST_CHROME − PANEL_BUDGET)/44) × 44`。
   1180×760:580−97−300 = 183 ⇒ **176px = 4 整行**(原 5 行),面板 307px ⇒ 滚动区 ≈150px;
   1180×900:323 ⇒ **308px = 7 整行**(未变)。720 档走 `None`(Fill),未动。
2. **说明压成单行**(`DownloadOptionsLiteLoaderVanillaOnly`):两行 28px 是滚动区最大的可压项。
   en `Vanilla parent only — Forge goes through the CLI` / zh `只叠原版;叠 Forge 请走命令行`;
   完整命令行说法留在 `ull help install`。单测断言随之改成「含该句 + 只有一行」并写明原因。

文档按六审要求改口:滚走的是说明/错误行(非控件);说明变单行后 760 档滚动区已能装下
head+标签+chips+单行说明,**无需滚动**。
2026-11-10 19:34:14 +08:00
AskaEth 6eb7132ab0 fix(app-iced): 加载器版本菜单搬进钉死区(裁切位置由钉死区高度决定,不是首屏分配)
四审点破根因:面板的裁切 y 由**钉死区高度**决定、与窗宽无关(1180 与 720 都切在同一处),
所以上一轮「把说明挪到菜单之后」只是换了被切的对象——菜单容器 642–706 只露 18px、条目文字
一个都没画,说明整块不可见。

按 tower 裁决改成结构方案:
- **滚动区**:head(版本 id + 安装按钮)+ 加载器 chips + 中性说明 + 错误行;
- **钉死区**:加载器版本菜单(标签 + 菜单,按条目数占高)+ 实例名输入框。

菜单不再依赖首屏剩余高度分配,可达性由结构保证;溢出时被滚走的是说明/错误行(非控件)。

实测(未变的部分也一并核对):01/02/05 分隔线 **481**、列表 **220px = 5 整行**;04 对照 **531 未变**;
03/06 窄窗 **268 未变**(本轮结构改动没有挤动列表);900 高加测 569 ⇒ **308px = 7 整行**。

文档按四审改正:`section` 的文档不再写「首屏能装下菜单/切点不再切字」,改成「裁切位置由钉死区
高度决定」;面板结构写清 head 在滚动区、菜单与实例名在钉死区。
2026-11-10 19:34:14 +08:00
AskaEth 0ca28d24ff fix(app-iced): 版本行节距/列表 chrome 标定重算(44/97)+ 中性说明挪到版本菜单之后
三审给了独立量测,两处都按它的数字收敛:

1. `ROW_PITCH` 278/6=46.33 → **44.0**(278 里含列表与面板之间的 8px `Space::X2`);
   `LIST_CHROME` 93 → **97**(列顶 156 → 列表顶 253)。1180×760 分隔线 492 → **481**、
   列表视口 **220px = 5 整行**;1180×900 加测 = 308px = 7 整行,三审报的 900 高残影不复发。
2. S7 钉页脚引入的新问题:面板上半块的滚动切点压在字形上(宽窗切「加载器版本」标签上沿、
   窄窗把 chips 切成 4px 条)。把中性说明从菜单**之前**移到**之后**——它两行高,排前面正是把
   唯一的加载器专属控件顶出首屏的元凶。

顺带纠正一处文档:面板结构不是「头部 + 可滚上半 + 钉页脚」,head(版本 id + 安装按钮)是上半块
的第一个子项、**安装按钮也在滚动区**,只有实例名是钉死的。
2026-11-10 19:34:14 +08:00
AskaEth 954281d71a fix(app-iced): 版本列表在三栏对半分档按整行取高(S1b/S9)
列表视口高度不再是「Fill 剩下的」:正文列高度由仓里已有的 `frame::body_height` 算出,
经 `view_sized` → `body` → `rails::three_column` → `rows::content` 传下来,Shared 档据此
`floor(((h − LIST_CHROME)/2) / ROW_PITCH) * ROW_PITCH` 取整行。

实测(1180×760):列表 245px → **239px**(分隔线 498 → 492,腾出的 6px 归面板),
`04` 对照**未变**(531 / 278px),720 窄窗**未变**(本来就是 4.0 整行、底缘 0 墨迹)。

两处修正写在提交里:
1. 第一版给所有三栏场合取整,把 `04` 也缩到 239px ⇒ 收窄为只在 `TailShare::Shared` 取整;
2. `rows.rs` 涨到 419 行撞 400 行门禁 ⇒ `TailShare` 与三个标定量纯搬移到 `rows/tail.rs`,
   `rows.rs` 回到 355 行(门禁 `components::gate` 与全仓扫描都绿)。

`view` 保留 4 参数(`#[cfg(test)]`,只有单测用),产品路径走 `view_sized`。
2026-11-10 19:34:13 +08:00
AskaEth f29ca609ec fix(app-iced,i18n): 实例名钉成面板页脚(S7)+ 中文占位符还原(S8)+ 注释实测值(S6)
D30 r2 视觉二审四条:

1. **S7(新回归)**:S1 的对半分让面板变矮之后实例名被挤出 1180×760 首屏。按 `install.rs`
   早已登记的方案改成**可滚上半块 + 钉死页脚**:`install::panel_with(.., shared)` →
   `options::section(.., scroll)`;`rows::content` 对 `PanelState::Options` 不再整块套
   scrollable(那层既把实例名卷进滚动区,也在窄窗产生第二条并列滚动条)。页脚不参与滚动,
   可达性由结构保证,S2 随之关闭。
2. **S8**:`catalog/download.rs` 与金表同一行的中文命令占位符 `<Forge 版本 id>` 照抄不可执行,
   还原成 `<forge id>`(两处一起改,金表顺序未动)。
3. **S6**:`rows.rs` 里「两半各 ≈215px / 列表 4.6 行」的估算换成实测(列表 245px = 5.29 行、
   面板 237px、分隔线 y=498),并写明 S1b 仍欠一项与其正确落点。

**S1b/S9(列表底缘整行)本轮没修成**:试过 `responsive` 拿「分给列表的高度」再 floor,
实测无效(`Fill` 子项上拿到非确定高度,量测显示列表区最后墨迹行仍在分隔线前一行),已回退,
不留无效代码。正确落点是把列表可用高度从 `pages/download.rs` / `rails.rs`(都在 scope 外)
传进 `rows::content`。

测试:`-p unlucky-app-iced` 862 passed、`-p unlucky-i18n` 36 passed(金表改动后仍绿)。
2026-11-10 19:34:13 +08:00
AskaEth cd7813f8eb fix(app-iced): 选项区变高时与版本列表对半分(S1)+ 中英出图注入 + 注释同步
D30 r1 视觉驳回的三条里,代码能修的两条:

1. **S1 版面**:带加载器版本列表的选项区在三栏档把版本列表从 6.0 行压到 2.4 行(1180×760
   实测 278px→110px,底缘切在第三行图标上)。`rows::TailShare::of` 现在让这类面板在**任何档位**
   都与列表对半分(`scrollable` 分支早已存在),另把加载器版本菜单改成**按条目数**占高
   (1 条 64px / 2 条 112px,原先只有 1 条也固定撑两行)。实测:列表回到 **245px = 5.3 行**。
2. **注释同步(S6)**:`options/view.rs` 模块头原先写「加载器分成两行」+ 两行示意,实际是
   六枚一行(末枚右缘约 404px / 正文列 424);那条「本轮无看图通道,未出截图」也换成实测值。

顺带(使能中文出图):`pages/mod.rs::demo_model` 读 `ULL_DL_LANG=zh` 把 `model.lang` 与下载页
目录一起切中文。之前 `LC_ALL=zh_CN.UTF-8` 对出图**完全无效**——页面文案来自 `model.lang`,
而 `Model::new` 写死 `En`、`demo_model` 不碰它(`i18n::detect_env` 不在这条链上)。

测试:`options/s1_tests.rs` 新增两条(菜单只按整行占高;带版本列表的选项区在每个档位都 Shared,
原版/不兼容仍 Pinned),`-p unlucky-app-iced` 862 passed。

未完成:S2 窄窗「滚到底」图——iced 0.14 没有自由函数 `scroll_to`,程序化滚动要手写
`widget::Operation`(约 40 行),本轮不拿半成品充数;面板 `scrollable` 的稳定 Id 已就位。
2026-11-10 19:34:13 +08:00
AskaEth 841c17152a test(app-iced): 下载页加 options-liteloader 出图取样点(D30 视觉取证)
M222 的视觉门禁一直缺图:本机没有看图通道,但**出图通道可用**(`--software-renderer` 走
tiny-skia CPU 后端,无桌面也能落盘)。下载页早有专属夹具 `ULL_DL_DEMO`(`demo.rs`,产品路径
不读),M105 就用它出过安装选项区三态,所以这里加第四个取样点,而不是再造一个平行 env:

  ULL_DL_DEMO=options-liteloader pages_demo --screen=download --width=1180 --height=760 \
    --screenshot=out.png --screenshot-theme=light --software-renderer

取样点是「选中 1.12.2 + LiteLoader」(官方版本表只到 1.12.2),走**产品那条真转换**
`Download::from_snapshot`:chip 行、中性说明「只叠原版」、确认键状态都是产品行为。
版本列表**只在本次状态里**补一条 1.12.2——不动 `demo_versions()`,因为那是别处基线图的行集
(`tests/view.rs` 钉着 8 行)。

四张图与真假表落在 `DevDocs/artifacts/m222/`(git 忽略区,不入库):浅色 / 深色 / 720 窄窗 /
原版对照,README 里写明哪张证明什么、哪些是夹具、哪些是产品真转换。图的内容判定未做——
出图者无图像能力,交给视觉 reviewer。
2026-11-10 19:34:13 +08:00
AskaEth 7d43f1d686 fix(cli): help instances 里把「install 不接受 liteloader」改成只限 optifine
M222 让 `install --loader liteloader` 可用后,`help instances` 那句「optifine / liteloader 只能由
别的启动器装好后继承进来」的后半句就成了假话,而且与同文件 `help install` 的记载
(`--loader` 取值含 liteloader、optifine 报 loader_unsupported)自相矛盾。

现在这条限定只落在 optifine,并注明 liteloader 自 M222 起可装。`kind` 仍是「认得出」的口径——
这句话的主体(kind=vanilla 只表示 id 里没痕迹、权威结论要读 libraries[])没动。

无测试断言过旧措辞:`help_vocabulary_tests::help_lists_every_stable_id` 只按 `LoaderKind::ALL`
遍历「id 出现在文案里」,新文案两个 id 都在,因此不需要同步改期望。
2026-11-10 19:34:12 +08:00
AskaEth 04a123deab fix(app-iced,presenter): 修 clippy -D warnings 抓到的三处(文档注释位置、manual_range_contains、单元素 for)
1. view.rs:我删掉手写 `LOADERS` 常量后,那段文档注释落到了 `VERSION_ROWS` 头上并留了空行
   (clippy::empty_line_after_doc_comments)。说明本身是给「chip 顺序为什么委派」用的,
   改成普通 `//` 注释,不冒充某个条目的文档。
2. model.rs:LiteLoader 的版本区间判定改成 `((1, 5, 2)..=(1, 12, 2)).contains(&version)`
   (clippy::manual_range_contains),语义一字未变。
3. install_tests.rs:只剩 OptiFine 一家后,单元素 `for` 改成直调(clippy::single_element_loop)。
2026-11-10 19:34:12 +08:00
AskaEth 3114fa2d4f fix(core): 首轮编译/测试抓到的四个问题(BTreeMap、测试模块布局、display_loader_version、未用 import)
1. liteloader.rs 拆文件后漏了 `use std::collections::BTreeMap`(`loader_library` 里用)。
2. `tests/liteloader.rs` 是 **crate root**,`mod guards;` 会被找去 `tests/guards.rs`;
   改成 `tests/liteloader/main.rs`(与 `tests/game/main.rs` 同形),target 名仍是 liteloader。
3. `display_loader_version` 去通配时丢了 NeoForge@1.20.1(forge 布局)去前缀的语义:
   既有用例 `neoforge_1201_uses_forge_layout` 当场抓住(1.20.1-47.1.106 → 47.1.106)。
   现在按「加载器 + 是否 forge 布局」显式决定,四家原有行为逐字不变。
4. `dto/instances.rs` 的 `pub use super::LaunchArgsView` 在非 test 构建里没人用(-D warnings 会红),
   改由 `dto/instances/tests.rs` 直接 `use crate::dto::LaunchArgsView`。

顺带修两条**我自己写错的测试期望**(不是产品 bug):
- 取消用例里那条 `resolve` 断言用错了父版本表(父版本在内存表里,当然解析成功);
- 新式父链用例误以为 `minecraftArguments` 会是 None,实际会沿链继承祖父层那份
  —— 断言改成「继承来的 legacy 串里不许出现 LiteLoaderTweaker」。
2026-11-10 19:34:12 +08:00
AskaEth 51ea32284d docs(core): inherit::merge 里写清 minecraftArguments(子覆盖父)与 arguments.game(追加)的区别
M222 的准入条件就是这条;不给结论留档,下一个人还会以为「叠一层」随便往哪个字段写都行。
2026-11-10 19:34:12 +08:00
AskaEth c311032080 chore(cli): report_loader 测试里省一行,给 400 行硬线留出余量 2026-11-10 19:34:11 +08:00
AskaEth 0eaefebf3b fix(core): 同一 loader 版本 id 撞两条父链时具名报错,不覆盖盘上清单(旧实例会静默换父链) 2026-11-10 19:34:11 +08:00
AskaEth 9c6b5eaa9b chore(core): 去掉 liteloader 集成测试与 versions.rs 里未使用的 import(-D warnings 会红) 2026-11-10 19:34:10 +08:00
AskaEth c8bf536c37 feat(app-iced): 下载页在选 LiteLoader 时给中性说明(GUI 只叠原版、Forge 组合走 CLI)
新增 Key::DownloadOptionsLiteLoaderVanillaOnly(中英各一句,不含花括号占位符);
金表按 Key::ALL 的真实位置插入(DownloadOptionsNeedsAtLeast 之后、DownloadCancelled 之前),
只加新键不重排旧键。说明走 OptionsView 的新字段 note,不塞 problem(那是错误色 + 禁用确认)。
2026-11-10 19:34:10 +08:00
AskaEth 7ed954f7fa fix(core): LiteLoader 快照仓固定为 repo.mumfrey.com/snapshots(1.10.2 的 repo.url 是 /repo/ 形态,照它会 404) 2026-11-10 19:34:10 +08:00
AskaEth 944b1610c8 feat(core): LiteLoader 安装路线(官方版本表 + 从零合成 launchwrapper 清单)
按 M218 SURVEY-p1 §4.1 的清单接上 LiteLoader 的安装路线,不动 OptiFine。

- core: 新增 addon::loader::liteloader(官方 versions.json 解析 + 合成清单)与
  addon::loader::parent(父版本链读盘定位原版版本);LoaderKind 加 LiteLoader;
  LoaderRelease 加 parent_version_id、LoaderPlanInput::vanilla_installed → parent_installed;
  LoaderInstallPlan::build 改返回 Result(父版本缺失具名报错);LoaderSource::resolve_on
  支持指定父版本(仅 LiteLoader,其它组合具名 unsupported)。
- 继承边界(本 mission 第一步核实):arguments.game 父在前子在后追加,minecraftArguments
  子覆盖父,启动顺序 legacy(24) → arguments.game(25)。所以 tweakClass 只写 arguments.game,
  绝不写 minecraftArguments(会顶掉 Forge 1.12.2 的 FMLTweaker)。
- maven/fabric 的 `_ =>` 通配改成穷尽分支:非 maven 族不再静默回落 Forge 的仓库;
  maven::{metadata_url,installer_url} 改返回 Option<String>。
- presenter: supported() 收 LiteLoader、wizard_choices 六选、dto::instances 按关注点拆成
  instances/{loader,runtime,create,tests}.rs(原文件 395 行,动手前先拆)。
- cli: --loader 区分「认不出」(loader_unknown) 与「认得但装不了」(loader_unsupported);
  <version> 位写加载器版本 id 时按父版本处理(只有 liteloader 接受);help 只改 install 段。
- app-iced: 下载页加载器 chip 加 LiteLoader(1.5.2~1.12.2,两头按官方没有构建拒),
  chip 清单改为委派 Loader::ALL。
- 测试: 真实响应的裁剪夹具(dl.liteloader.com 与 repo.mumfrey.com 的 maven-metadata)
  驱动「解析→计划→落盘→继承→启动参数顺序」全链;Forge 父链有回归(FMLTweaker 必须还在
  且排在 LiteLoader 之前);父版本缺失/组合不支持/取消/坏表各自具名错误。
2026-11-10 19:34:09 +08:00
AskaEth 307d29239a Merge branch 'feat/m225-fix-liteloader-mirror-404-and-repo'
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-10 14:32:13 +08:00
AskaEth 146eb3d8db fix(download): LiteLoader 不再套 bmclapi 改写,Central 库接上 bmclapi/maven
bmclapi-liteloader 改成 enabled=false(规则保留作形状留档):2026-11-10 实探
`apply()` 的真实产物 https://bmclapi2.bangbang93.com/maven/versions.json 404 / 9 B、
/maven/com/mumfrey/liteloader/1.7.10/liteloader-1.7.10_04.jar 404 / 9 B、
/maven/com/mumfrey/liteloader/{maven-metadata.xml,1.12.2-SNAPSHOT/maven-metadata.xml}
全 404 / 9 B;官方 dl.liteloader.com 的 versions.json 200 / 49129 B、artifact
200 / 533903 B。引擎遇 404 只会白丢一次请求再前进(NotFound 不重试),官方不可达时
更是直接失败,所以镜像优先用户不该再为每个 LiteLoader 文件试一次镜像。

新增 bmclapi-maven-central(repo1.maven.org/maven2 → bmclapi/maven):bmclapi 的
/maven 不只是 libraries.minecraft.net 的镜像,Central 的路径逐字节一致(asm-all 5.2
247787 B、gson 2.10.1 283367 B、log4j-core 2.17.1 1790452 B,sha1 逐一相同),不存在
的坐标两侧都是 404(镜像回 9 B 纯文本,没有「200 + HTML」假阳性)。

快照仓 repo.mumfrey.com 不加规则:bmclapi /maven、maven.aliyun.com/repository/central、
repo.huaweicloud.com/repository/maven 对 com/mumfrey/liteloader/1.12.2-SNAPSHOT/
maven-metadata.xml 全是 404,只有官方 200 / 358 B,所以只登记探测结果、不铺未验证的规则。

测试:新增 liteloader_urls_stay_official_only(镜像优先模式也只出官方一条,把 enabled
翻回 true 的变异实测会红);library_urls_expand_to_the_maven_subpath_on_bmclapi 补
Central 用例;形态判据的 EXPECTED 表加 bmclapi-maven-central。不探外网。

version 保持 3 不升 4:download/jdk_sources.rs 与 tests/game/java_vendor_chain.rs 两处
pin 了 version == 3,后者不在本 mission 的 scope(第一轮 workspace test 就是被这条 pin
抓红的),升版留给专门一轮;规则改动与探测记录写进 mirrors.json 的 notes。
2026-11-10 13:56:58 +08:00
AskaEth 54995a4af8 Merge branch 'feat/m221-adoptcore-existing-versions-read-on'
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-10 12:57:03 +08:00
AskaEth 347a3cc90d fix(game): 复审 r2 三项——Win32 尾点/尾空格别名 + 写面与游戏树重叠 + 更高祖先断链
r1 五项里 R4/R5 与 slug/大小写等直接案例已由 r2 复审独立通过;这三条是它补证出的**剩余边界**,
只做限定修复,不推翻重写:

r2.1(未存在认领路径的 Win32 尾点/尾空格别名):比较键新增 `fspaths::comparison_real()`——
已存在前缀用 `canonicalize`,**尚不存在的尾段按平台语义归一**:Windows 普通路径折叠尾 `.`/空格
(`Foo.` 与 `Foo ` 都是 `Foo`,Win32 建目录时就这么做),**verbatim 路径(`\?\…`)不折叠**
(那是另一个物理目录,合法)。原先 `dir_key` 只统一分隔符与大小写,于是一个尚未创建的
`custom` 认领 `Foo.` 与缺省派生 `Foo` 键不相等却被判定「不撞」,两个实例共用一个运行目录。

r2.2(`data`/`.ULL` 整体映射到受保护游戏树时仍静默外写):新增
`fspaths::ensure_surfaces_clear_of_game_tree()`——默认写盘面(`.ULL`、`config/`、
`config/instances/`、`profiles.json`、`data/`,以及缺省档的运行目录)**解析后不得落进游戏根**;
`plan`(dry-run 也报)与 `adopt`(写盘前)各核一次,重叠即 `adopt_write_surface_in_game_tree`
具名拒绝并提示把数据目录挪到游戏树之外。整体迁移到游戏树**之外**照旧允许(判据是落点不是链接)。
配置写面判据同时收窄职责:`ensure_config_write_surface` 只管「在 `.ULL` 内」,
不再带「`.ULL` 在游戏根里」的豁免特例——重叠由上面那条统一负责。

r2.3(游戏根更高祖先断链仍当空列表):`scan` 的 `NotFound` 分支改为沿祖先找**最近的已存在入口**,
它能 `lstat` 到但 `canonicalize` 不出来(断链的符号链接/目录联接、目标被搬走)⇒
`adopt_versions_unreadable`;一路到盘根都不存在才是「真正的新数据根」,空结果仍允许。
原先只查 `versions` 与游戏根两层,迁移联接在更高层断开时会误报空库。

新错误码:`adopt_write_surface_in_game_tree`(CLI hint 与 help 拒绝清单同步)。

验证(持久日志在 `target/m221-r2/`,git 忽略、不入库;上一轮 `target/m221-r1/` 证据未覆盖):
- `cargo fmt --all --check` 0;`cargo check -p unlucky-core -p unlucky-cli --all-targets` 0;
- `cargo test -p unlucky-core --test adopt` **43 passed / 0 failed**(另跑一次
  `ULL_ADOPT_TEST_SYMLINK_REQUIRE=1` 同样 43 passed);
- `cargo test -p unlucky-cli --test adopt_test` **13 passed / 0 failed**;
- `cargo clippy -p unlucky-core -p unlucky-cli --all-targets -- -D warnings` 0;
- 额外:`cargo test -p unlucky-core --lib` **778 passed**、`cargo test -p unlucky-cli` 全部目标通过;
- 三次**实现-only 变异**(不折叠 Win32 尾点尾空格 / 不核游戏树重叠 / NotFound 直接当空)
  全部以**断言红**收场(1 / 4 / 2 条失败,非编译错误、非环境错误),每次还原后与基线逐文件一致,
  收尾再跑 core+cli 双绿。

界限:只覆盖 core+cli 的定向目标(不等于 workspace 全量);没有启动真实 Minecraft;GUI 未接;
判据是检查那一刻的快照(TOCTOU 看不到);Win32 8.3 短名与非 junction 重解析点未覆盖。
2026-11-09 23:56:02 +08:00
AskaEth 8110cde3f5 fix(game): 复审 r1 五项——运行目录不许被两实例共用 + 配置写面越界 + versions 断链 + 链接守卫反转 + 快照漏目录
复审 p2-5items 的五项,全部按「先能红的用例,再改实现」:

R1(默认目录可被不同实例隐式共用,产品缺陷):新增 `adopt/run_dir.rs`——登记前读**每个已有实例**
的 `gameDir`(现役设置 + M27 前档案的遗留字段),用「真实路径 + 拼回不存在的尾段 + 大小写归一」
(Windows/macOS)比较;撞上就顺延 `<base>-2/-3…`(上限 32,撞满 `adopt_no_free_run_dir`)并在
`notes` 点名撞的是哪个实例;**非空且无人认领**仍拒绝(那是用户数据,不偷偷换目录)。这样
`Better MC` 与 `Better-MC`(slug 同名)、Windows 上 `Foo` 与 `foo`(同目录)都不会共用一个运行目录。
大小写归一在 macOS 上是**保守**选择:大小写敏感卷上会多一次顺延,换来的是「绝不共用」在任何卷上成立。

R2(配置写面 junction 写穿到外部游戏根,产品缺陷):`run_dir::ensure_config_write_surface()`
在 `plan`(dry-run 也报)与 `adopt`(写盘前)各核一次——`config/`、`config/instances/`、
`profiles.json` 的真实落点必须在 `<.ULL>` 内,且**不得经链接落进游戏根**(错误按真实归属分类:
「逃出 .ULL」vs「落进游戏根」)。`.ULL` 本身在游戏根内是布局事实,附加判据在该布局下零判别力,
主判据仍然拦下一切跑出 `.ULL` 的落点——这不是给链接开后门。新码 `adopt_config_surface_unsafe`。

R3(dangling versions 被当不存在,产品缺陷):`scan` 的 `read_dir` `NotFound` 再三分——
`lstat(versions)` 看见入口(断链符号链接/目录联接/目标被移走)或游戏根解析失败(祖先断链)⇒
`adopt_versions_unreadable`;两者都不是才算「真没有」。`inspect` 也补了同族判据(条目存在但
不是可读目录 ⇒ `directory_unreadable`,不再混成「没装」)。

R4(强制链接测试逻辑反转,假绿):`fixture::link_gate(created, required) -> Run|Skip|Fail` 是纯
真值表,`link_guard_with()` 显式传参(`link_guard()` 只读环境变量);新增
`link_guard_executes_all_three_branches` 用 `catch_unwind` **执行** Fail/Skip 两个分支。
四处调用点统一走 guard;建链接/联接的夹具收进 `fixture.rs`。

R5(快照遗漏目录节点,假绿):新增 `tests/adopt/snapshot.rs`——全节点快照(根/目录/文件/链接
各记类型 + 适用 mtime;目录记 mtime、链接只记目标不跟随、Windows 用重解析点属性认 junction、
读盘失败直接 panic)。**CLI 测试用 `#[path]` 复用同一份**,判据只一处;新增 `fixture_cases`
五条工具自测,`plan`/dry-run 的零写盘判据改用 `snapshot_opt`(连空目录都不许出现)。

验证(本轮持久日志,`target/m221-r1/`,git 忽略、不入库):
- `cargo fmt --all --check` 0;`cargo check -p unlucky-core -p unlucky-cli --all-targets` 0;
- `cargo test -p unlucky-core --test adopt` **37 passed / 0 failed**(另跑一次
  `ULL_ADOPT_TEST_SYMLINK_REQUIRE=1` 同样 37 passed);
- `cargo test -p unlucky-cli --test adopt_test` **11 passed / 0 failed**;
- `cargo clippy -p unlucky-core -p unlucky-cli --all-targets -- -D warnings` 0;
- 四次**实现-only 变异**(缺省不查认领 / 配置写面不核 / versions 的 NotFound 当空 / 快照不记目录)
  全部以**断言红**收场(不是编译错误、不是环境错误),每次还原后与基线清单逐文件一致,收尾再跑绿。

界限:只覆盖 core+cli 的定向目标(不等于 workspace 全量);没有启动真实 Minecraft;GUI 未接;
判据是检查那一刻的快照,TOCTOU 与非 junction 重解析点仍列边界。
2026-11-09 23:13:57 +08:00
AskaEth 9f5c001387 feat(game): 从其他启动器只读登记已有版本(ull instances scan|adopt)+ 登记可一并定隔离档(M221)
盘上已经铺好的版本(多半是 PCL/HMCL 装的)今天在 ULL 里看不见,用户只能重装一遍。这个
commit 把它们登记成实例,**只读外部、只写自己的配置**:不搬文件、不补清单、不改任何外部
文件的 mtime(versions/ libraries/ assets/ 一个字节都不写)。

core(`game::adopt`,新模块):
- `scan` / `inspect`:只读扫描 + 逐项检查,坏 JSON(带行列号)/ 缺 id / id 与目录名不一致 /
  缺父 / 成环 / 读不出来 / 越界 / 同名冲突一律**结构化**报错(稳定 `code`),不吞成空列表;
  `versions/` 不存在才是空结果,读不了就是错误;
- 越界判据用 canonicalize 后的前缀比较,同时覆盖**符号链接、Windows 目录联接(junction)与
  祖先链接**(junction 不需要管理员权限,是最常见的「把别人版本目录挂进来」);越界条目在
  边界检查就停手,一个字节都不去读;
- `plan`(零写盘,喂 dry-run 与后续 GUI 预检)/ `adopt`(plan + 只写自己的配置):
  **缺省运行目录落 ULL 数据区 `<data>/instances/<实例目录名>`(custom 档),绝不沿用全局
  `isolated`**——那条缺省的运行目录正是 `versions/<versionId>/`,会在他人的版本目录里长出
  mods/saves/config;`shared` / `isolated` 必须显式选,且 `target_notes()` 给出后果话术;
- 缺省运行目录已存在且非空 ⇒ 拒绝(不覆盖、不猜「合并进已有实例」);数据区里已有的祖先
  若解析到数据根之外(符号链接/联接)⇒ 拒绝,不沿它写盘;
- 登记与隔离档一次写清:`install/instance` 拆出 `register` 子模块,新增
  `register_with(InstanceSettings)`;写盘顺序改成「先实例设置、后 profiles」(中断只留孤立
  设置文件,不会留下按全局档启动的实例),profiles 写失败会撤掉刚写的设置。`register`
  签名与行为不变(`register` = `register_with(inherit)`;同版本同名的幂等复用照旧)。

CLI:
- `ull instances scan`(只读列出候选与原因)、`ull instances adopt <version-id> [--name N]
  [--isolation shared|isolated|custom] [--game-dir DIR] [--dry-run]`;`--game-dir` 单独给即
  custom,相对路径在解析期就报用法错误;
- 人读/`--json`/help 三条出口齐备(`error.detail.problems` 逐条可分流),已有命令未改。

测试(真磁盘夹具,`cargo test -p unlucky-core --test adopt` 22 passed /
`cargo test -p unlucky-cli --test adopt_test` 10 passed):
- 一份「别的启动器铺好的」`.minecraft`(versions/libraries/assets/mods/saves/config +
  launcher_profiles.json),扫描与登记前后做「路径集合 + 字节数 + 内容哈希 + mtime」全等断言;
- 正常 / 缺清单 / 坏 JSON / 缺父 / 成环 / id 不匹配 / 非法 id / 符号链接越界 / 根内符号链接
  放行 / **junction 越界** / 数据区 junction 写盘边界 / 缺省非 isolated / shared 与 isolated
  的话术 / 重名 / 重复 / 非空目录 / 空目录复用 / dry-run 零写盘;
- CLI 侧验 `launch --dry-run` 的 `--gameDir` 指向选定运行目录(argv 已脱敏,比尾段;
  `data.game_dir` 全等);
- 变异核对(只改实现、跑完即回滚):缺省档改 `VersionDir` ⇒ 红;边界判据摘掉 ⇒ 符号链接与
  junction 两条红;写盘边界摘掉 ⇒ 数据区 junction 那条红;缺父当链终点 ⇒ 两条红。

界限:没有启动真实 Minecraft;GUI(presenter/app-iced)接线另派;D20 单游戏根模型未改
(不扫第二个 `.minecraft`)。
2026-11-09 22:21:37 +08:00
AskaEth 0577ee87a3 Merge branch 'feat/m220-javapinnit-m214-java-pin-and-nit-sw'
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 21:31:23 +08:00
AskaEth 75aa5ee82c fix(cli): 启动用的 Java 钉死 D4 校验过的那个 + M214 复审 nit1/nit2/nit3
- override_path 五来源统一钉住 D4 选中的运行时:以前只有 managed 钉路径,
  JAVA_HOME/PATH/system 三种会被 snapshot::prepare 的 selection() 按环境再挑一遍,
  D4 判过的 Java 根本不是真启动的那个。现在 `plan_java` 只做「选中 → 钉路径」,
  人读行写出「版本(来源 · 具体可执行文件)」,--json 的 java 块并排给出
  binary(校验过的)/ launch_binary(装配落地的)/ source,两者不一致时当场告警。
  实例设置/全局设置里的 javaPath 也并入「谁指定了 Java」,不会被自动挑的结果顶掉。
- nit3:D4 的 Java 不符拦截挪到 identity::resolve 之前(微软轨解析身份会联网刷新
  令牌、还会回写凭据),拦下的启动不再先触发一次刷新;不符提示补上「挑中的是哪个
  java」,文案只加不减。
- nit1:error.rs / launcher.rs 的人读串里残留的 ** 标记去掉,并补两条回归断言。
- nit2:java_launch.rs 里「只提示不阻断」等旧注释对齐 D4 现状。
- launch.rs(399 行零余量):D20 的隔离档位派生与切换检测整体搬到新文件
  launch_isolation.rs,纯挪函数体(逐行 diff 只多 6 处 pub(super)),
  用 #[path] 挂载以免改 commands/mod.rs。

用例:钉路径纯函数单测 3 条 + 端到端 2 条(JAVA_HOME 置空证明非 managed 来源也钉住;
数据根放两个账户证明 D4 拦在身份解析之前)+ 人读串无 ** 的 2 条回归。
两条变异(①退回只钉 Managed ②退回身份解析之后)各自让新用例变红,已实测。
2026-11-09 21:30:55 +08:00
AskaEth 1f6e15127a Merge branch 'feat/m217-guardrange-child-spawn-guard-across'
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 21:12:24 +08:00
AskaEth f8acb50156 fix(app-iced): 子进程统一走 platform::child::quiet(消灭启动闪窗)+ 守卫射程扩到整棵 crates/(M217)
用户报「CI 构建版启动时底下闪一个控制台」:M213 统一了 crates/core/src 的 20 处
Command::new,但守卫射程只有 core/src,app-iced 的 reg.exe(每次 GUI 启动都跑)漏网。

- crates/core/tests/child_quiet_guard.rs:扫描根 crates/core/src → crates/(1219 个 .rs),
  角色清单改成相对 crates/ 的路径(文件角色钉精确处数,目录角色至少一处);
  「扫不到文件」兜底断言保留,另加角色文件锚点兜底;顺带修掉全树扫描会撞上的
  「纯 //! 结论页没有代码行」误判
- app-iced:platform/locale.rs 的 reg.exe、app/update/update_action.rs、
  app/boot/fallback.rs、app/boot/termux_window.rs 的 xdpyinfo、
  renderer/acceptance/locale.rs 的 defaults 全部改走 quiet(后三处是同一 exe /
  POSIX 上的 pass-through,为的是规则整体可被守卫钉住)
- app/dialog/export.rs 的 explorer / open / xdg-open 保持裸构造并写清「窗口就是产品」,
  presenter/src/host/opener.rs 同理登记成角色(不在本 mission 改动范围)

门禁:fmt/check --workspace --all-targets/test --workspace -j 4/clippy --workspace
--all-targets -D warnings/全仓 400 行 + <2 字节 .rs 扫描,六步退出码全 0,CHAIN_EXIT=0。
2026-11-09 21:12:02 +08:00
AskaEth 7521c47944 Merge branch 'feat/m216-coexistcore-shared-minecraft-no-sil'
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 21:10:48 +08:00
AskaEth aa61565fe7 fix(coexist): r2 返工——帮助写真实例路径 + 加载器安装也说出「原版清单被替换」+ --json 给出绑定成败(M216)
r2 复审两条 P2 必修 + 一条「能一行接上就接」的 P3。

P2-1(`ull help pack` 的落点少一层 `.ULL/data`)
- 「数据根」在这份帮助自己的定义里是「含 `.ULL/` 与 `.minecraft/` 的父目录」(`--data-dir` 那行),
  所以真路径是 `<数据根>/.ULL/data/instances/<实例目录名>`。r1 写成 `<数据根>/instances/<实例名>`,
  照读会去一个不存在的位置找包内容。
- help.rs 与 model.rs 的字段文档改成真路径,并点明目录名是归一化后的结果(`Better-MC`)。
- 用例改成 `help_describes_the_new_default_target_with_the_real_path`:先断言真路径整串在、
  再断言 `<数据根>/instances/` **不在**(这一条就是抓「少写 .ULL/data」那种错法)。

P2-2(装加载器时原版那一步的「替换了清单」没人说)
- `loader.rs`:`plan.vanilla_install_needed` 那条路以前只拿 `vanilla_report` 判 `is_complete()`
  就丢掉;现在把它留在 `vanilla_report` 里传给 `report_loader::build`。
- `report_loader.rs`:`build` / `human_lines` 各多一个 `vanilla: Option<&InstallReport>`;
  新增 `vanilla_lines`(人读:`告警(原版): …` + `原版版本清单已替换(旧的那份备份在 …)`)与
  `vanilla_json`(`--json` 的 `vanilla` 段:warnings / version_json / manifest{kind,backup})。
- 用例 `a_replaced_vanilla_manifest_is_said_out_loud_in_the_loader_output`:用真
  `LoaderInstallPlan`(最小 Fabric,零网络)调 `human_lines`,断言告警与备份路径都在;
  未先装原版时不多说话;`vanilla_json` 的 `kind`/`backup` 结构正确、`None` 时没有该段。

P3(复审「能一行接上就接」):`--json` 给出导入绑定的**结构化成败**
- core `pack_dir::bind_created_instance` 返回 `Option<Result<String, String>>`(None=不需要绑 /
  Ok=已绑 / Err=绑失败),一句话就能分出成败,不必猜文案。
- CLI:`outcome` 多写一个 `"instance_bound": true|false`(人读行照旧给原因);
  GUI:`import.rs` 按 Ok/Err 分别记 `instance_bound` / `instance_binding_failed` 注记。
- 端到端用例里补断言 `data.instance_bound == true`。
2026-11-09 20:43:46 +08:00
AskaEth 4e1323212b fix(coexist): 复审 r1 返工——帮助文案跟上新落点 + 「替换了清单」进人读通道 + 导入建实例绑到内容目录(M216 P2/P3/§5)
P2-1 ② 改了缺省落点,CLI 自己的文档还在写旧行为
- `cli/help.rs`「目标目录缺省 = 数据根的共享游戏目录(.minecraft/)」→ 新实例目录
  `<数据根>/instances/<实例名>`(并登记实例);
- `cli/model.rs` 的 `PackImportArgs::target` 字段文档同步。
- 用例 `help_describes_the_new_default_target`(断言帮助含新落点、不含旧落点)。

P2-2 ③ 的「显式说明」进不了任何人读通道
- CLI:`human_lines` 原只遍历 `plan.warnings`,而运行期告警(含「已替换版本清单 …旧的那份
  备份在 …」)在 `report.warnings` 里 ⇒ `ull install` 人读输出从没说过这句话。现在有 report 就
  报 report.warnings(它开头就是 plan 告警的副本),`manifest=Replaced` 再单独打一行备份路径。
- GUI:`install.rs` 新增 `Job::journal_report`(把 `report.warnings` 与 `manifest` 的备份路径写进
  安装日志),`install/routes.rs` 两处 `ensure_complete` 之前各加一行调用(tower 批的两行)。
  此前 `report.manifest` 全仓零消费者。
- 用例 `a_replaced_manifest_is_said_out_loud_in_the_human_output`(走真 `InstallPlan` 排版)。

P3-1 缺省目录被占的 hint 删掉不成立的 `--overwrite` 那半句(它绕过不了这条预检)。
P3-2 GUI 落点改成委托 core 的 `pack_dir::instance_dir`(此前 presenter 自己拼 `data()/instances/…`);
`convert::dir_name` 缩成 `#[cfg(test)]` 的委托入口(生产侧已无调用点,留着继续钉归一化判据)。
P3-3 补 `the_default_target_is_a_new_instance_dir_and_the_instance_is_bound_to_it`:不带 `--target`
跑通整条 CLI,断言落点、内容、登记、以及实例设置里的 `isolation`/`gameDir`。

§5 导入建出来的实例原先「导入成功但内容永远不会被启动用到」
- 新增 `pack_dir::bind_instance_dir`(`InstanceConfig.isolation = Custom` + `game_dir = 导入目录`)
  与 `pack_dir::bind_created_instance`(判据+文案,CLI 与 GUI 同一份);
- CLI 在缺省建实例后、GUI 在 `import.rs::run` 里各调一次;只在「我们新建了实例目录且档案是新建」
  时绑(显式 `--target` / `--no-register` / 复用已有档案都不动);
- 运行目录 = 导入目录(saves/mods/logs/config),**versions/libraries/assets 仍按 D20 恒共享**;
- 未改 `derive_game_dir`。
- 用例:正向 `binding_an_imported_instance_points_it_at_the_import_dir`、反向
  `a_freshly_registered_instance_still_inherits_the_global_mode`。
2026-11-09 19:46:41 +08:00
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 ab5b593708 Merge branch 'feat/m214-javacompat-java-version-distance-la'
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 17:27:23 +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 aa8c8f506a Merge branch 'feat/m213-consolesilence2-quiet-child-process'
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 15:08:49 +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 bb8be87c8f Merge branch 'feat/m212-agentlog-agent-and-mcp-log-diag-too'
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
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
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-09 14:42:10 +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 994ffda626 Merge branch 'feat/m211-logreach-session-jsonl-path-double'
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 14:14:14 +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 96753c5b38 Merge branch 'feat/m210-mirrorfix-bmclapi-libraries-missing' 2026-11-09 13:59:58 +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