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 |
|
tower-worker
|
a0f61d8bb5
|
feat(mods): 本地 Mod 列表与启停(M224 MODTOGGLE)
三层一次做完:core 读写目录 → presenter DTO/host → 实例详情页 MODS 区。
r1 复审后 rebase 到 main 71144df(M223 已合入),并修掉四条文档疵。
core(crates/core/src/addon/mods/)
- suffix:`.disabled` / `.old` 的**唯一判据**(四态枚举 + 展示名 + 双向改名目标名),
收敛原先两份同构实现——`export/scan.rs::is_disabled_path` 与 `import/plan/paths.rs::suffix_variant`
改为委派/再导出,签名与行为不变。
- list:单根语义(入口是 derive_game_dir 派生出的运行目录)列 `<run>/mods/`,不按扩展名过滤,
符号链接/目录/怪名字逐条登记原因;`mods/` 本身是符号链接时拒(不跟随)。
- toggle:启停 = 在 `mods/` 里改名。五条守卫(裸文件名 / 目录可用 / 普通文件 / 不覆盖 / 不做
copy+delete 兜底)+ 幂等;`.disabled` 只插在 `.old` 之前。
presenter
- dto/mods:ModsCommand / ModListView / ModFailureView(不拼文案,只给结构 + OS 原文)。
- host/mods:后台线程列目录与启停,运行目录走与启动同一份 D20 派生、不写任何设置;
游戏在跑时 host 侧直接挡。
- state/mods:快照整体替换、迟到结果丢弃、在飞标记;`AppState::mods`。
app-iced(实例详情页)
- MODS 区(`pages/instances/mods.rs`):名字 + 大小 + `.old` 徽标 + Switch;运行中/在飞只读。
- 进入详情页即后台列一次(不走 3 s 观察窗,避免打红 M192 的「空闲不烧帧」守卫)。
- MODS 卡排在启动参数**之前**(详情列是滚动区,排在后面 1180×1000 下永远看不见)。
- i18n:19 条中英文案(`catalog/instances.rs` + 金表)。
rebase 冲突解法(三个文件,全是"两侧同段新增",都保留):
- `i18n/catalog/instances.rs` + `i18n/catalog/golden.rs`:M223 的 Adopt* 段在前、M224 的
InstancesMods* 段在后(= `Key::ALL` 顺序 = 金表顺序)。
- `presenter/src/state.rs`:`adopt` 与 `mods` 字段/初始化/apply_core 分支都留;`tiles()` 归
`state/tiles.rs`(本分支搬的),`apply_agent`/`agent_rail_visible` 归 `state/agent.rs`
(M223 搬的)——两处搬移不重叠,state.rs 里都不再出现。
r1 四条文档疵:
1. `pages/instances/mods.rs` 模块文档:挂载位置写反("启动参数卡之后"→实际之前),并补上为什么;
2. `state/mods.rs`:引用不存在的 `CoreEvent::ModsChanged/ModsFailed` → 改成 `CoreEvent::Mods`
+ 文字说明 Ok 落快照 / Err 落具名失败;
3. `app/state/mods_shot.rs`:文档说"目录串取真实派生结果"但代码留空 → 改成"故意留空";
4. `app/state/refresh.rs::note_mods_failure`:文档说"连点两次看见两次红字"与去重机制不符 →
改成按 `(实例, 文件, 大类)` 三元组去重(防刷屏),换文件/换实例/换大类仍会再弹。
顺带(r1 已确认的 main 红):`pages/download/options/view.rs:88-89` 两条
`clippy::doc_lazy_continuation` 只缩进修掉,文字未动。
|
2026-11-10 21:09:37 +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
|
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
|
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
|
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
|
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
|
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
|
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
|
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 |
|
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
|
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
|
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
|
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
|
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 |
|
worker-logplace
|
8c5bebf3e3
|
fix(m188): r1 复审 P2-1——后端启动失败的具名原因进 launcher.log
复审结论 p2-1/1 item + fix-then-merge:主线成立,但「启动器自己因 GPU / 渲染后端起不来」的
事故原因仍只进 stderr —— 双击启动没有 stderr,那条路径上等于没有日志。
- renderer/mod.rs::report_failure 的两行(`[gpu] <backend> 初始化失败(具名)` /
`[gpu] 失败详情(具名)`)改走 diag!;
- renderer/log_bridge.rs 转出的 wgpu 日志(含 `…Selected: AdapterInfo…`,GPU 真生效的唯一证据
行)改走 diag!;
- renderer/preflight.rs 的 `[gpu-preflight]`(ULL_PREFLIGHT=1)改走 diag!;
- probe.rs::apply_with_preflight 的三行 `[probe]` 改走 diag!。
- app/diag.rs 模块文档 + docs/logs-location.md §3 的判据改准:
「逐帧度量不接;显式取证开关与一次性事故行可接」,并修掉 `[corner]` 两处自相矛盾
(唯一打印点在 app/update/appearance.rs,走 diag! ⇒ 进日志;corner.rs 只 format_line)。
- 新守门 renderer::tests::a_backend_startup_failure_lands_in_the_launcher_log:
断言那两行真的落进 launcher.log;变异(改回纯 eprintln)→ 红。
- README 两语的 launcher.log 行补上「渲染后端起不来的具名原因」。
|
2026-11-09 01:05:40 +08:00 |
|
worker-logplace
|
5fd5cd47dc
|
feat(m188): launcher.log 早落盘 + 会话日志搬到 logs/game(旧家只读兼容)
用户投诉原话:「我们的启动日志放在哪?…他找半天没找到崩溃日志」「只有一个安装日志」。
启动器运行日志(.ULL/logs/launcher.log)
- presenter 构造 HostEnv 时就登记日志目录:启动早期缓冲的 200 行([probe]/[font]/[theme])
当场刷盘,不再是「只有开过一局游戏才有日志」。
- app/** 里所有 eprintln! 诊断改走 app::diag::diag!(stderr + 文件双写,行首 [tag] 即 scope),
含 boot/boot::fallback/state/update 各线。
- boot 那一拍补一行后端事实(request/effective/fallback)。
- 滚动上限(1 MiB → launcher.1.log,只留一份)写进 README 与 docs/logs-location.md。
会话日志搬家:data/agent/sessions → logs
- LauncherLayout: sessions_root()/game_logs() 新增,agent_sessions() 降级为只读兼容。
- host/logs 与 cli/commands/sessions 两个根合并(同 id 新根优先),event_path 回落旧家。
- 不搬不删;日志目录写一份 session-log-location.json 记录(moved=0, policy=read-only-compat)。
入口与文案
- 设置页「通用」新增「日志文件夹」行:路径 + 打开目录 + 复制路径(i18n 中英)。
- 日志页页头的「打开目录」不再是占位:走 M187 的 open_dir,失败文本带路径并送进剪贴板。
- README.md / README.zh-CN.md 新增「日志在哪」章节(含 crash-reports 只在游戏走到那一步才有),
docs/logs-location.md 给出上限、双写判据、迁移策略与没验的部分。
守门(每条都有变异证明,见报告)
- 旧家会话仍列得出/打不开 ⇒ 只读新根变红;新会话写 logs/game ⇒ 只读旧根、写回旧家都变红。
- launcher.log 滚动 ⇒ 干掉 rename 变红。
- 打开日志文件夹指向真实目录 ⇒ 换成 cache/ 或去掉 create_dir_all 都变红。
- 日志根随三模式变化 ⇒ 把 HostEnv::logs_dir 写死成便携落点变红。
|
2026-11-09 00:43:45 +08:00 |
|
AskaEth
|
2b6dcbed40
|
fix(m187): r2 复审 p2——截断过的文件也必须脱敏(截断 改到文本流里做)
r2 复审挖到的隐私旁路:上一版在**字节**上切两刀,切在多字节字符中间时
String::from_utf8 失败 ⇒ 整份 entry 走「原样字节」分支,家目录 / 用户名 / 令牌原文
按字节进包,而清单什么也不说。命中的正是最容易超过 4 MiB 的 latest.log 与会话 JSONL;
中文 Windows 用户名(C:\Users\张三)让家目录串本身就是 CJK,切点落在附近必然命中。
修法(比复审给的三条更严一点:先脱敏再截断,避免切点留下半截 secret):
解码(lossy,任何字节都能进脱敏器)→ 脱敏整份 → 按**字符/行边界**截断 → 插截断标记。
新增 crash_bundle/clip.rs(切点逻辑 + 4 条用例),write_bundle 里只剩一条通路,
「二进制原样」这条分支彻底删除。
守卫:truncated_entries_are_still_redacted —— 造一个 '中' 正好跨在 HEAD_BYTES 上的
4 MiB+ 文件,内含注册过的会话 id 与家目录;断言 ①包里找不到原文 ②<home> 在
③尾部 exit 事件仍在。变异(改回字节级截断 + 仅解码成功才脱敏)⇒ 红在「仍带着家目录原文」。
顺带:
- P3-1 启动日志写两遍(WindowHandle(None) 分支重复调 record_boot_log)→ 删掉重复调用。
- P3-2 清单里的截断字节数改成**实际保留量**(原来报的是常量 HEAD_BYTES)。
- P3-4 truncated>0 时界面那句话补用例(拆出 dialog/tests/export.rs,两个文件都 < 400 行)。
- P3-6 BundleSpec::manifest() 自己也过脱敏,去掉「谁调谁泄漏」的口子。
- P3-5 卡片宽度取官方上限 560 的语义在代码里写清(不改行为,登记为台账)。
|
2026-11-08 23:57:49 +08:00 |
|