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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

app-iced: 实例详情页「启动参数」区(JVM/游戏参数、内存、Java、三态后端 chips + 灰化提示 + 保存/恢复),表单状态归 App、i18n 中英文案同步。
2026-11-07 10:36:35 +08:00
AskaEth d68523b02a Merge branch 'feat/m156-termxmerge-merge-termux-glibc-into'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
2026-11-07 09:38:20 +08:00
AskaEth 3b3430a93e docs(termux): 对外口径升级——CPU 软渲染 + 自编 Mesa GPU 两条路径各真机验证一次(用户拍板 A,四处同步) 2026-11-07 09:33:24 +08:00
AskaEth cb3ae8556b docs(core): install.rs 模块文档补上 Termux 分支与平台参数不早退
拆分后模块文档只描述归档链,读起来像「Termux 也走这条」;补一行说明 Termux 的系统包
分支在链之前,以及 `AdoptiumTarget::host()` 失败已改成每家一条降级原因(不再整体早退)。
2026-11-07 09:33:24 +08:00
AskaEth 88ac9d5aca docs(termux): 适配文档头部与合并指南对齐实际合并(15 提交 / --no-ff / 两处 PC 侧修复)
- 头部:`14 个提交,HEAD 38d3024` 是写文档那一刻的自我指涉,永远不可能对——改成
  `15 个提交,分叉点 87ab878,合并前 HEAD e987e02`。
- 合并指南第 1 步原来写 `git rebase origin/dev`,实际按用户令走 `--no-ff` 合并
  (原 15 个提交一字未改地留在历史里,便于按组 revert),改为记录既成事实与
  零文本冲突的交集(Cargo.lock / 两个 README / app-iced 的 Cargo.toml)。
- 新增第 6 条:PC 门禁发现、已就地修掉的两处——cfg(unix) 测试导入未 gate 导致
  Windows clippy 判错;两个文件越 400 行红线后的拆分。这两条在 Termux 上跑门禁看不出来,
  写进合并指南,免得下一个人以为「本机绿了就等于 PC 绿」。
2026-11-07 09:33:24 +08:00
AskaEth 4dbeb07220 refactor(core): 拆分越 400 行红线的 install.rs 与 install/libraries.rs
合并 termux-glibc 后两个文件顶破了「单文件 ≤400 行」(guards.yml 的 file-size):
`game/java/install.rs` 379 → 456、`game/install/libraries.rs` 258 → 409。

按步骤拆,纯搬运、零行为变化:

- `install/termux_install.rs`(新):Termux 系统包那条路。它与归档链不共用任何一步
  (不查版本元数据、不下载、不解压、不校验 sha256),原来只能是一段嵌在 install() 中间的
  if。返回 `Outcome::Done`/`Skipped` 并用 Box 装 `InstalledJdk`(否则 large_enum_variant:
  392 vs 24 字节)。
- `install/one.rs`(新):回退链里「一家」的机械步骤——下载 → 解压 → 探测 + 幂等复用,
  连同 `Prepared` 与 `note()`。
- `install/libraries/arm64_natives.rs`(新):Linux arm64 的 natives 改写与 Maven 兜底
  (两个常量 + 改写/拼址两个函数)。用例仍留在 libraries.rs 的 tests 里,它们从 `plan()`
  的出口断言,不碰私有步骤。

拆分后:install.rs 319、one.rs 100、termux_install.rs 104、libraries.rs 348、
arm64_natives.rs 72;全仓受检文件(*.rs/*.md/*.toml/*.yml/*.json/*.py/*.sh)复算
最大 400 行,红线内。
2026-11-07 09:33:24 +08:00
AskaEth c82e39d0eb fix(core): Windows 上 clippy 报 discover.rs 两个 cfg(unix) 测试导入未使用
这两个 Termux 扫描用例挂着 `#[cfg(unix)]`,但它们的 `use`(`standard_roots_in`、
`platform::paths::MapEnv`)写在 mod tests 顶部没跟着 gate:

- Termux/Linux 上 cfg(unix) 成立 → 有用例 → 导入有使用者 → clippy 干净(作者本机 475 测试
  通过是真的);
- Windows 上那两个用例不编译 → 导入无使用者 → `-D warnings` 直接判错
  (`unused import` ×2,`could not compile unlucky-core (lib test)`)。

把两个 `use` 同样 gate 成 `#[cfg(unix)]`。代价是这两个用例在 Windows 上不编译不执行,
覆盖由 Linux CI 承担(它们断言的本来就是 Unix 路径)。
2026-11-07 09:33:23 +08:00
AskaEth 161aec52f4 fix(app-iced): 新增的 unlucky-core 依赖版本对齐 0.2.2(合并后语义冲突)
termux-glibc 分支写这条依赖时 workspace 版本还是 0.2.1;M154 之后其余内部依赖
都已被抬到 0.2.2。caret 约束下 0.2.1 仍能解析到 0.2.2,所以不会构建失败,
但版本对齐是 M154 的既定契约,留一条 0.2.1 会让下一个读到该文件的人以为有例外。
2026-11-07 09:33:23 +08:00
AskaEth 41019d9cae docs(termux): 根目录新增适配方案与合并指南
TERMUX-ADAPTATION.md:方案(平台识别/JDK/字体/横屏/GPU/MC)、逐文件改动表、
兼容性说明(Termux gate、x86_64 不变)、合并步骤与 CI 预期、验证矩阵、
后续路线与按组回滚策略。README.termux 顶部链过去。
2026-11-07 09:33:23 +08:00
AskaEth fe472e36a5 docs(termux): README 校正——GPU 默认、横屏截图尺寸、数据根说明
- 构建描述从 CPU-only 改为 GPU 构建(wgpu + CPU 兜底)
- 窗口证据改为横屏 1657x1067、首帧 ~60 ms
- 快速开始不再强制 --software-renderer:默认 auto,驱动缺失自动降级
- 补数据根说明(Portable 按 exe 目录,--data-dir 暂忽略)
2026-11-07 09:33:23 +08:00
AskaEth 394faad611 docs(termux): MC 1.20.4 真机跑通结论与剩余项
- README:MC 标 ✅(glibc JRE + arm64 natives + softpipe);后续收敛为
  zink/DRI3、剪贴板/IME、narrator/音频
- 构建文档 §4.2 补真机启动命令、窗口尺寸与日志位置、非致命项
2026-11-07 09:33:23 +08:00
AskaEth 23c2abebc2 feat(termux): launch 侧同样改写 arm64 natives;真机跑通 MC 1.20.4
- Library::linux_arm64_native_name 收敛改写规则,install 与 launch 共用
- presenter/CLI 的 classpath 组装:1.19+ 的独立 natives 条目按 arm64 坐标
  找文件并进 classpath;≤1.18 的 natives 表条目仍归 natives::collect
- 真机验证:ull install 1.20.4(740MB)→ glibc Temurin 21 + arm64 natives +
  softpipe GL,MC 主菜单在 termux-x11 中渲染(窗口 854x480)
- 已知非致命:narrator flite 缺失、LWJGL JNI 版本告警、oshi cpuVendor NPE
2026-11-07 09:33:23 +08:00
AskaEth 0e7ae44923 fix(termux): 现代清单的 LWJGL natives 条目也改写为 arm64
1.20.x 把 natives 作为独立库条目(org.lwjgl:lwjgl:3.3.2:natives-linux),
不走 natives 表。Linux/aarch64 上按坐标改写成 natives-linux-arm64,
URL 兜底到阿里云 Maven(Mojang 对 arm64 一律 404)。
真机 1.20.4 的 dry-run:7 个 natives 全部变成 arm64;x86_64 不变。
2026-11-07 09:33:22 +08:00
AskaEth 7eed6c6261 feat(termux): MC 启动接线——Linux arm64 natives + softpipe 子进程环境
- Library::native_classifier:Linux/aarch64 上把 LWJGL3 的 natives-linux
  改写成 natives-linux-arm64(Mojang 清单只有 x86_64)
- install::libraries:arm64 分类器在清单里没有下载信息时,按 Maven 相对路径
  兜底到阿里云镜像(实测 Mojang libraries 404、Aliyun 200);
  单测覆盖 arm64 兜底与 x86_64 不变
- launch::defaults:Termux glibc 构建启动 MC 时默认带
  MESA_LOADER_DRIVER_OVERRIDE=swrast / LIBGL_ALWAYS_SOFTWARE=1(用户可覆盖)
- 文档:JRE patchelf 一次性步骤与接线现状;README 同步
- 验证:patched Temurin java 直接执行;LWJGL arm64+GLFW 探针 gl-ok
2026-11-07 09:33:22 +08:00
AskaEth 02ed09ae49 docs(termux): 补 MC 运行时结论(glibc JRE + LWJGL arm64 + softpipe GL)
- §4.2:Temurin 21 JRE 在 glibc 下跑通;AWT/X11 正常;
  LWJGL 3.3.3 arm64 + GLFW + GLX/softpipe 端到端探针通过(GL 3.3)
- zink 仍卡 DRI3;softpipe 可用(软件 GL)
- README 同步:MC 标 🟡(栈已验证,启动器接线待做),后续改为接线 + DRI3
2026-11-07 09:33:22 +08:00
AskaEth 173a18b842 feat(termux): glibc 构建自动启用自编 Mesa 26 turnip ICD(GPU 打通)
- app/boot/termux_gpu:glibc 构建 + Termux 且随附 ICD 存在时,在探测前设
  VK_ICD_FILENAMES/VK_DRIVER_FILES;用户显式设置不覆盖
- 文档:Mesa 26.2.6 + KGSL 的完整构建/安装/验证步骤(含 shebang、
  --wrap-mode=nofallback、C++/链接参数三处坑)
- 实测:wgpu 选中 Adreno 840(turnip Mesa 26.2.6),截图首帧 61 ms
- README.termux:GPU 标 ✅,MC 标 🟡(glibc JRE+AWT 已验证,GLX 待 GL 构建)
2026-11-07 09:33:21 +08:00