# Changes

## 1.1.3 - 2026-10-08

- **修复 `normalize` 的归并缺陷（自装 raku-pm 在 OpenSSL 3.0 系统上仍误报缺失 `libcrypto.so.1.1`）**：
  1.1.2 引入的 `NativeLib.normalize` 有两个 bug，导致 `libcrypto.so` / `.so.3` /
  `.so.1.1` 等**同库候选未能归并到逻辑名 `crypto`**，自装时依旧把不存在的
  `libcrypto.so.1.1` 判成硬缺失而回滚（用户实测：`raku-pm.raku install . --force`
  在只有 OpenSSL 3.0 的机器上必然失败）：
    - `%rev` 反向表用 `$v<files>.values.flat` 构建，平台文件数组被「条目化
      （itemized）」而未被展平，实际只生成了「每平台一整个数组当成一个键」的错误映射；
    - 版本号剥离正则 `^(.+?)\.(\d[\w.]*)$` 只能处理**尾随**版本（如 `libcrypto.so.1.1`），
      无法处理**中缀**版本（如 `libcrypto.3.dylib`），且对 Windows Git 的绝对路径候选
      无能为力。
  现改为两层遍历构建 `%rev`，并新增 `core-name` 同时覆盖尾随 / 中缀两种版本位置、
  对绝对路径取 basename 再归并。`t/native-normalize.t` 已覆盖三平台 + 未知库 +
  绝对路径场景（并修正原 `plan 10` 与实际 8 条断言不符导致该测试永远不过的问题）。

## 1.1.2 - 2026-10-08

- **修复运行时本地库检查的跨平台误报（自装 raku-pm 不再因此失败）**：
  `runtime-native-libs` 会把源码里每个 `is native('...')` 当成独立硬依赖，而
  raku-pm 自己的 `MD5` 为跨平台 + 纯 Raku 回退写了**一组多平台候选**
  （`libcrypto` / `.so` / `.so.3` / `.so.1.1` / `.dylib` / `.3.dylib` / `-3-x64.dll`）。
  旧逻辑用「全部都要存在」的 AND 语义，于是 Linux 上把 macOS / Windows 候选以及
  旧的 `libcrypto.so.1.1`（OpenSSL 1.1 ABI）全判成「缺失」而放弃安装，导致
  `raku-pm.raku install .` 在 OpenSSL 3.0 系统上必然失败。新增 `NativeLib.normalize`
  归并：丢弃非本平台的候选名、把同库多平台候选反向归并到逻辑名（如
  `libcrypto.so.1.1 → crypto`），从而只算逻辑库、走本平台候选列表「任一存在即满足」
  的 OR 语义。该修复同时惠及任何声明多平台 native 候选的包。

## 1.1.1 - 2026-10-08

- **自装 raku-pm 时自动跳过自身回归测试**：`raku-pm.raku install .`（用源码脚本自装 /
  自升级）此前会走通用安装流程，在「自己正在安装的事务里」跑 raku-pm 自己的回归套件，
  而该套件会重入写文件锁 / 真实 HTTP / 拉起 raku-pm CLI 做真实安装，导致死锁或超时、
  整链回滚、install 被判失败。现对名为 `RakuPM` 的发行版（`Client.rakumod` 的
  `!install-chain-inner`）强制跳过自身测试，依赖仍照常测试；装完用 `raku-pm test .`
  （tools/run-suite.raku）即可独立、安全地验证。

## 1.1.0 - 2026-10-08

- **help 横幅（title）旁显示当前程序版本号**：`raku-pm help` / `help <命令>` / `help all` /
  `help <组名>` 在终端（富文本）模式下，横幅提示行 `raku-pm help` 之后追加 `vX.Y.Z`
  （取自 raku-pm 自身的 META6.json / 已装发行版元数据，取不到时静默不显示）。
  改动集中在 `UI.rakumod` 的 `ui-banner` 增加可选 `:$version` 参数，由 `CLI.rakumod` 的
  `print-help` 一次性取 `raku-pm-version()` 后传给四处横幅调用；非富文本（管道/重定向）
  横幅本就为空串，故对纯文本输出与测试零影响。

## 1.0.4 - 2026-09-29

- **修复 Rakudo 2026.09 下 `unhandled Failure detected in DESTROY` 警告**（`install`/`repos`/`list` 等命令）。
  Rakudo 2026.09 起，`.d`/`.e` 等文件测试符对【不存在】的路径不再稳定返回 `Bool::False`
  （本机 WSL 2026.09 实测 `.d` 对缺失路径返回未处理 `Failure`），该 Failure 若不被显式消费
  会在对象销毁时报警。根因在 `CLI.rakumod` 的 `looks-like-path`，旧写法
  `return True if try { $s.IO.d }` 的 `if` 修饰符未能可靠消费缺失路径产生的 Failure。
  现改为「先 `.e` 再 `.d`、且都用 `.so` 显式消费」的短路写法：缺失路径时 `.e` 已为 False，
  短路后根本不会调用 `.d`，从源头杜绝未处理 Failure。
  同步审查并加固其余文件测试符用法（`Repositories.add/remove`、`CLI` 的 `raku-pm-version`/
  `raku-pm-git-root`/`list --paths`/`install` 的 git-URL 判断、`Author.logout`、`Store.has`、
  `Ledger.exists`、`Lock.exists`），凡可能落在缺失路径上的 `.d` 一律加 `.e` 短路或 `.so` 消费。

## 1.0.3 - 2026-09-24

- 修复 `ui-wrap` 无断点文本（URL 等）折行退化为逐字符竖排的 bug（1.0.2 引入）：
  回退到「最近可断点」时，若整段剥到 1 字符仍没找到断点，旧判断条件照样成立，
  导致每行只推 1 个字符（实测 `raku-pm info DB::SQLite` 的源码地址竖排成 6 行）。
  现在只有真的找到断点才回退，否则整段硬断——每段占满列宽、绝不丢字。
  回归测试 3 条钉住（含「第一段不得短于 60 列」的竖排探针），变异验证通过。

## 1.0.2 - 2026-09-24

- **修复 `raku-pm info` 长描述「错行」**（UI 展示层）。`ui-table` 此前把整段长描述当成一列、
  不按终端宽度折行，终端被迫在任意位置硬折，把表格边框 `│` 打断，看着很别扭。
  现在按终端宽度（`RAKUPM_COLUMNS`/`COLUMNS` 可覆盖，默认 80）截断列宽，长文本列承担压缩，
  并按【显示宽度】折行（CJK 友好：优先在 `、` `：` 等标点后断开，无词边界时按字符硬断），
  续行各列空白对齐、边框完整。配套 `ui-term-width` / `ui-wrap` 两个辅助子，
  及 `t/ui.t` 的折行回归测试。

## 1.0.1 - 2026-09-24

**修复：部分 Windows 机器上 `zef install` 因随包测试误报而中止。**

- 现象：`t/author-remote.t` 场景 1b「当前用户 X 拥有显式完全控制」在个别 Windows 机器上失败
  （13 项仅挂这 1 项），zef 跑测试即中止安装。根因有二：
  ① 生产侧按 `whoami` 字面串（`主机\用户`）做 icacls 授权，而 icacls 显示的账户名
     与该串可能**大小写不同或主机前缀不同**（NetBIOS 名显示形式随机器而异）；
  ② 测试侧 `contains` 大小写敏感地匹配整串，且 ACE 集为空时「无继承」断言**空真**。
- 生产侧（`Author.save-credentials`）：授权主体改用 **SID**（icacls 的 `*S-1-...:F` 语法，
  机器无关、不受账户名显示形式影响；`whoami /user` 的表头本地化但 SID 恒为 ASCII），
  SID 拿不到再退回账户名。
- 测试侧（`t/author-remote.t`）：账户匹配改为**用户名段 + 大小写不敏感**（icacls 显示
  可能不带 `主机\` 前缀），并新增「ACE 行非空」断言堵住空真；失败时 `diag` 打印
  icacls 原始输出便于定位。
- WSL/Linux 不受影响（走 POSIX chmod 600 分支）。
- 已做变异验证：跳过 ACL 加固后「去掉继承权限」断言变红，防线真实。

## 1.0.0 - 2026-09-24

**首个稳定版。** 0.88.x 这一路是「1.0 前审查与加固」专场：消费侧与生产侧功能在更早的版本已完整，
1.0 的重心是把**质量门禁、路径安全、凭据安全**做到经得起第三方审计。

- **门禁提速与可信度**（0.88.2–0.88.5）：套件加每用例墙钟 + 最慢排行小结；`t/` 与 `xt/`
  合成单池、按历史耗时降序做 LPT 分桶、各条道**独立连续跑（去掉层屏障）**，
  全量墙钟从 **699.8s 压到 494.4s（加速比 7.12x）**；并加**防漏跑断言**（计划集 vs 执行集比对，
  漏跑用例不再假绿）。
- **路径安全收口（第三方审计 AUDIT.md 的 S-1 / M-1，0.88.6）**：修 `Client::Git.!git-cache-key`
  的整条路径直拼穿越（含恶意 `git-deps` 入口，用户无感）；把「含 `..` 成员的 tar 包不会写出
  目标目录」钉进 `xt/tar-member-escape.t`（变异验证：加 `-P` 立刻红）。
- **凭据安全（审计 L-1，0.88.7）**：Windows 上 `Author.save-credentials` 改用 `icacls` 把
  `credentials.json` 的 ACL 收敛到**仅当前用户**（去掉继承 + 显式完全控制），不再因跳过
  `chmod` 而在多用户机器上暴露 api-key。
- **第三方审计全条目核实**（0.88.6–0.88.7）：M-2（`RAKUPM_CURL` 不存在）/ M-3（curl 默认校验证书）/
  L-3（`%!last-conflict` 已自带重置）/ L-4（`!tarball-url` 失败路径不存在）经核实不成立或不可利用，
  均记录在案、未做无效改动。
- 配套不变式修正与文档同步：`assert-under` 对 `..` 穿越**天然失效**，净化须在生成路径时做
  （`docs/architecture{,.en}.md`、`nav-map.md` 的净化点清单已从 6 处更新到 7 处）；
  双语 `README` 与 `Changes` 随版本同步。

语义上 1.0.0 遵循本项目约定：**major = 磁盘布局或行为的不兼容变更**——本版本未引入不兼容变更，
跳到 1.0 是**成熟度信号**（完整功能 + 已通过安全审计 + 门禁可信），API 与 0.88.x 保持兼容。

## 0.88.7 - 2026-09-24
- **凭据文件在 Windows 上的权限收紧（第三方审计报告 AUDIT.md 的 L-1，防御性加固）。**
  `Author.save-credentials` 原先在 Windows 上直接跳过 POSIX `chmod`（Windows 无此语义），
  凭据文件 `credentials.json`（含 api-key，类 ssh key）在多用户机器上存在被别的账号读到的风险。
  修法：`is-windows` 时改用 `icacls` 把 ACL 收敛到**仅当前用户**——`/inheritance:r` 去掉继承权限、
  再 `/grant:r <当前用户>:F` 显式只授当前用户完全控制（best-effort，失败不致命、不喷噪声）。
  当前用户取自已 `whoami`（按字节读，避开中文 Windows 的 GBK 编码坑）。
  守卫 `t/author-remote.t` 场景 1b：Windows 断言 ACE 无 `(I)` 继承标记且当前用户持 `(F)`；
  POSIX 断言权限位为 `0600`。
- **核实并关闭审计 AUDIT.md 的 L-3 / L-4（无需改代码，仅作记录）：**
  - L-3：`Resolver.explain-all` 与 `resolve-all` 开头都已 `%!last-conflict = %()` 重置，
    审计担心的「共享状态跨命令串味」在当前调用模式下不会发生，已自带防御。
  - L-4：审计称 `!derive-base` 对非 http URL 返回 Nil 会在 `!tarball-url` 里字符串化拼错 URL，
    但实测 `!tarball-url` 拼相对路径用的是 `$!index-url.subst(...)` 而非 `base-url`，
    该失败路径不存在；`base-url` 全项目无消费者，故不加显式 die（避免误伤 file:// 本地索引测试）。

## 0.88.6 - 2026-09-23
- **修 git 缓存目录的路径穿越（第三方审计报告 AUDIT.md 的 S-1，1.0 前最后一处漏网）。**
  `Client::Git.!git-cache-key` 是全项目**唯一"整条路径直接拼"**的注入点：它把 git 地址
  规整成缓存目录名后直接 `$cache-root.add($key)`，**没过 `path-component` / `assert-under`**。
  于是 `https://host/x/../../../tmp/evil.git` 能让 clone 落到 `target/git-cache` 之外 ——
  实测：`git-cache/github.com/foo/../../../escaped-dir` 最终建在 git-cache 的**上一层**。
  三条入口都汇聚到它：CLI 直接输入、`self-upgrade --from`、**以及恶意发行版 META6.json
  里的 `git-deps`**（这条用户完全无感）。

  修法：对 key 的每一段做**栈式规范化**（`..` 上溯一层、已出界则丢弃；`.` 跳过；其余段
  过 `path-component`）。刻意不用"见到 `..` 就 die" —— 远程 URL 里 `..` 非法，但
  `self-upgrade --from ../repo` 这类**本地路径**里它完全合法，一刀切会把正常用法打死。

  **★ 顺带写明一条对不变式的修正**：`assert-under` 对 `..` 穿越**天然失效** —— 它比的是
  **字面前缀**（`$child.absolute` 以 `$root.absolute ~ sep` 开头），而含 `..` 的路径字面上
  确实"在下面"；再加上 **Raku 的 `IO::Path` 不折叠 `..`**（`.absolute` 与 `.cleanup` 都原样
  保留，真正解析 `..` 的是内核），这类检查**看起来完全正常**。
  ⇒ **净化必须在生成路径时做，事后断言补不上。**（`docs/architecture{,.en}.md` 与
  `nav-map.md` 的净化点清单已从 6 处更新到 7 处。）

  回归测试 `t/git-cache-key-safety.t`（12 条）刻意**不靠字符串断言**做核心判定 —— 它真的
  让文件系统去建目录、再看落点（因为字符串层面的"在不在根下"对 `..` 恒为真）。变异验证：
  把净化换成"原样返回" → 8 条红；第一版把逃逸落点写成 `$base/tmp`（实际上溯三层落在
  `$target/tmp`），变异时暴露为假阴性，已改成与层数无关的兜底判据。

- **把"含穿越成员的 tar 包不会写出目标目录"钉进门禁**（同一份审计的 M-1）。
  审计指出 `Ecosystem.source-dir-for` 解压源码包时是裸调 `tar -xzf`、没有安全标志。
  **实测结论：当前不可利用** —— GNU tar / bsdtar 默认就拒绝含 `..` 的成员（打印
  "Removing leading `../'" 与 "Member name contains '..'"，非零退出，一个文件都不解出来）。
  **没有改解压命令本身**：加安全标志在 GNU tar / bsdtar 之间语法不一致，而当前行为已经
  安全，为它引入平台差异不划算。改为新增 `xt/tar-member-escape.t` 钉住这条不变量 ——
  变异验证：给解压加上 `-P` 之后文件立刻落到目标目录之外、测试当场变红。

- **审计报告其余条目的核实结论（部分与源码事实不符，故未改动）**：
  · **M-2「`RAKUPM_CURL` 可被替换成任意可执行文件」不成立**：源码里**没有** `RAKUPM_CURL`
    这个变量（HTTP 层走 `HTTP::Tinyish`，没有开放 exe 替换）。`RAKUPM_TAR` 确实存在
    （Author 打包 / Ecosystem 解包都认），但它是**用户自己设的环境变量**，与审计自己
    "不是设计错误"的判断一致 —— 加 `--version` 校验反而会给合法的替代实现添堵。
  · **M-3「TLS 未显式校验」**：curl 默认即校验证书，且没有明文降级路径，未改。
  · **L-2 / L-5** 审计自己判定"不是 bug / 不是安全边界" → 未动。
  · **L-1 / L-3 / L-4**（Windows 凭据 ACL / Resolver 状态重置 / 非 http(s) 索引基址显式报错）
    属防御性加固，当前代码路径都不会触发，留作后续。

## 0.88.5 - 2026-09-23
- **门禁再降一截：去掉"按层同步"的屏障（各条道独立连续跑）。**
  0.88.3 的合并调度把总墙钟从 699.8s 压到 544.8s，但离理论下界仍差得远 ——
  下界 = `max(最长用例 ≈195s, 用例合计/8 ≈320s) ≈ 371s`。差距出在执行粒度上：当时是
  「第 i 层 = 每条道的第 i 个元素，一起跑完再进下一层」，**每层末尾都是一道屏障** ——
  某条道上排着 160s 的用例时，别的道跑完自己那层（可能只要 5s）就得干等 155s。

  现在每条道**独立连续推进**：谁先跑空谁立刻接下一个，屏障消失。并发上限仍是 `$jobs`
  （每条道同时只跑一个子进程），不会把机器压爆。线程安全上刻意保守：worker 只做两件事
  —— ① 实时打印（经一把 `Lock` 串行化，保证每行完整不交错）② 把结果 push 进**自己的**
  数组并作为返回值交回；共享容器（`@results` / `@timings` / `%est`）一律由主线程在
  `await` 之后统一更新，因为多线程并发写 Array/Hash 在 Raku 里没有保证 —— 宁可多一轮
  汇总，也不赌。

  防漏跑断言（计划集 vs 实际执行集）**照旧生效**：变异验证把 `for @lane` 改成
  `for @lane.head(*-1)`（每条道少跑最后一个）→ 立即中止并点名 3 个未执行用例。

## 0.88.4 - 2026-09-23
- **修 `raku-pm reinstall <没装过的包>`（不带 `--version`）直接报"找不到模块"。**
  用户现场：`raku-pm reinstall CSV::Table` → 「在所有已配置的仓库里都找不到模块
  'CSV::Table' (约束 = )」。对照实验：同一个包 `raku-pm install CSV::Table` 能正常
  解析出 9 个发行版，说明只是 reinstall 这条路坏了。

  **根因是三处叠加的"空串 vs 未定义"语义错位**：
  ① `Client.install` 的签名是 `Str :$version`（**没有默认值**），内部用
     `$version.defined ?? "= $version" !! $constraint` 判断"有没有指定版本"；
  ② CLI 在没给 `--version` 时把兜底值写成**空串** `''` —— 空串是【已定义】的，
     于是 `$effective` 变成 `"= "`（等于空版本）→ 解析器谁都不匹配；
  ③ `reinstall` 的"未安装 → 退化为 install"分支又把这个空串原样传给了 `install`，
     于是"本该装最新版"变成"找不到模块"。

  修法：CLI 与 `install` 对齐，兜底值传**未定义的 `Str`**；`Client.reinstall` 再加一道
  —— 只在真的给了版本时才透传 `:version`，空串一律丢弃。两处都改，因为它们各自都
  可能被单独回退（回归测试里各配了一条断言）。

  **为什么既有测试没抓住它**：`t/reinstall.t` 里早就有「包未装过 → 退化为 install」的
  用例，但走的是 **API 直调**（`$client.reinstall('FreshPkg', '')`，version 是未定义
  值）—— 与 CLI 的形态不同，而这个洞**只**在 CLI 传空串时出现。新用例**照 CLI 修复前
  的形态**传 `:version('')`，所以抓得住；另加一条静态守卫扫 CLI 里传给 `install` /
  `reinstall` 的 `:version` 兜底值，并按**被调方语义**限定范围：`upgrade` /
  `uninstall` / `fetch` / `look` 的签名是 `Str :$version = ''`，它们**就是**用空串表示
  "没指定"，那几处 `!! ''` 是对的，一刀切禁掉反而会误报。

  变异验证两组：去掉 `Client` 侧的加固 → 3 条红；把 CLI 兜底值改回空串 → 静态守卫红。

- 顺带记录一处 Raku 陷阱（本次自己踩到）：`my Bool $x = $a.defined && $a.chars;` 会
  `Type check failed … expected Bool but got Int (0)` —— **`&&` 返回的是操作数本身，
  不是布尔值**。要写 `so (...)`。

## 0.88.3 - 2026-09-23
- **门禁把 700s 往下压：`t/` 与 `xt/` 合并调度 + LPT 分桶（首次用实测数据做优化）。**
  0.88.2 的计时数据指出瓶颈不在进程启动（只占 2%），而在**分批方式**：旧实现按目录分开
  跑、各自按文件名分批，长任务于是随机落在批次尾部 —— 批尾常只剩一两个还在跑、其余并发槽
  空转（`xt/self-upgrade-bootstrap` 单个 158.7s，足以独占一条尾巴），8 并发实测只有 2.83x。

  现在：① `t/` 与 `xt/` **合成一个池**，`t/` 里那些 1–8s 的短用例正好填进 `xt/` 长用例留出
  的空槽；② 按**历史耗时降序**调度（LPT：最长处理时间优先 + 依次放进当前最空的桶），耗时
  缓存在 `~/.raku-pm/suite-timings.json`（`RAKUPM_SUITE_TIMINGS` 可改路径；首次无历史时用
  粗启发式，第二次起用实测）；③ 预热从"每段各一次"变为**只预热一次**。

  **刻意没选"谁先空谁抢下一个"的事件驱动调度**：LPT 只是排序 + 一次划分，没有异步状态机，
  且每个用例恰好属于一个桶 —— 配合新增的**防漏跑断言**（计划里有用例没被执行到 →
  立即中止门禁并点名），"调度自己出 bug、少测了几个文件却报绿"这种最危险的假绿在结构上就
  不可能悄悄发生。宁可慢一点，也不要一个看起来很干净的绿灯。

  判定口径不变：`t/` 强制安静、`xt/` 只判成败、失败用例仍串行复验一次。合并调度后日志里
  的用例是**混着出现**的（这是必然），故末尾新增**按目录的分组小结**（`t/ 97 个：全过`）。

- **`t/message.t` 不再对"环境级 rakudo 提示"假红。** 该测试断言子进程 stderr **逐字节**
  等于 `E\nW\nH\n`；可 rakudo 每个进程都会扫 RAKULIB 链上的 FileSystem 仓库，只要有
  `.pm` 扩展名的模块就往 stderr 喷一段 deprecation 提示 —— 于是**在"用户自己维护 RAKULIB
  模块目录"的机器上必然假红**（而这恰恰是 raku-pm 的目标场景：见 `Installer/ShellTemplates`
  里那条注释），原因与 raku-pm 毫无关系。实测：用户 `~/raku-modules` 里 pull 出一个
  `B-Raku/lib/B/Raku.pm` 之后，本文件立刻变红。

  修法是**精确剥离**那块提示本身（首行 `Saw N occurrence … of deprecated code.` 到尾行
  `… this message will disappear!`，含其中分隔线），其余一个字节都不动；并配**负面断言**
  钉住边界（产品自己多写一行不许被当成噪音丢掉）。两处变异验证：完全不过滤 → 3 条红；
  过度过滤（只留 E/W/H 行）→ 负面断言红。放宽类改动必须配负面断言，否则"过滤"会悄悄
  退化成"什么都放过"。

## 0.88.2 - 2026-09-23
- **门禁首次能回答"为什么慢"：`tools/run-suite.raku` 默认打印耗时小结。**
  全量 `t/`+`xt/` 要十几分钟，此前只有一个总数 —— "慢在进程启动、还是慢在某几个
  测试"只能靠猜。现在每个用例输出一行墙钟，结尾给出最慢 15 个排行与统计：

  ```
  ===== 用例耗时（单文件墙钟，含 raku 启动；最慢 15 个） =====
      <秒>s  xt/<最慢的那个>.t
      …
    用例耗时合计（= 若串行跑一遍的下限）: <秒>s（<N> 个用例，平均 <秒>s）
    t/  实际墙钟: <秒>s
    并行加速比: <x.xx>x（用例合计 ÷ 墙钟；墙钟含预热/串行复验/自检）
    ↳ 其中 t/ 预热预编译: <秒>s（已含在墙钟内）
    raku 启动基线（min of 3 × `raku -e ""`）: <秒>s
    → 光启动就占用例合计的 <N>%（<N> × <秒>s = <秒>s）；用例内容本身的净耗时约 <秒>s
  ```

  关键是**启动基线及其占比**：它把"起了个进程"与"测试真在算"分开，否则任何优化都
  无从判断收益。判据 —— 若各用例耗时都贴近基线，瓶颈就是**进程数**（合并用例文件 /
  复用进程才有用）；只有少数明显突出（网络、大量子进程）才值得去抠测试逻辑。
  单文件耗时刻意覆盖"起进程 → 退出"整段（含 raku 启动与 precomp 检查），那才是门禁
  真正付出的代价；只计时测试体会低估到失去意义。`--no-timings` 可关掉（CI 日志求干净）。
  预热、串行复验的耗时单独列出且注明"已含在墙钟内"，避免重复计数误判。

## 0.88.1 - 2026-09-23
- **外部数据的畸形形态：一律"人话报错 / 归一 / 滤除"，不再甩裸 Raku 异常**
  （1.0 审查 P2 批最后一项，外加全量数据复核实测出的两处）。共同点是：用户拿到的
  要么是看不懂的内部异常，要么是莫名噪音 —— 而原因往往只是"少了一对引号"。

  **① `name` / `version` / `description` / `builder` 类型错或缺失 → 明确报错。**
  修复前直接把这些值塞进构造器，类型不对就抛：

  ```
  Type check failed in assignment to $!version; expected Str but got Rat (1.0)
  ```

  没有文件名、没有字段上下文、也没有"该怎么改"。现在：

  ```
  META 文件 .../META6.json 的「version」字段类型不对：期望字符串，实际是 Rat（1）。
    JSON 里不加引号就是数值类型，请给整个值加一对引号让它成为字符串。
    注意不能靠程序自动转：数值解析成 Rat 时已丢失书写形式……
  ```

  **为什么不"归一"而是报错**：`name` / `version` 是发行版的**身份**（要进 store 路径
  与账本），而且没法无损转 —— `"version": 1.0` 到 Raku 是 Rat，`~1.0` 得到 `"1"`，
  悄悄转会把用户装的东西记成另一个版本，比失败糟得多。（`description` / `builder`
  同理按严格处理；`auth` 早就单独归一了。）

  **★ 一处自我纠正**：初版提示会回显"改好的字面量" `"version": "1"` —— 而 Rat 把
  `1.0` 归一成了 `1`，用户照抄当场把版本从 1.0 改成 1。这是"照解析结果反推原文"
  的经典错误，已改为只给方法（加引号）、不回显值，并加了**防回退断言**钉住它。

  **② 索引行 `auth` 是数组 → 归一（既有 bug，本轮实测发现）。**
  `from-meta-file` 那条路早就在用 `!normalize-auth` 处理数组形态，`from-index-row`
  这条路只判 `.defined && .chars` —— 数组**能通过判断却被原样返回**，撞上
  `auth-of-row(--> Str)` 的返回约束：

  ```
  Type check failed for return value; expected Str but got Array (["github:titsuki"])
  ```

  cpan 索引里的 **`Text::Sift4`**（5 行）就这样带着裸异常倒下。两条路现已共用同一个
  归一实现。全量索引实测：63 行 `auth` 非字符串（58 行是 `"auth": null`，那类不崩）。

  **③ `resources` 数组里的 `null` → 滤除（既有 bug）。** 真实索引里 `Config`
  1.3.0 / 1.3.1（cpan 与 rea 共 4 行）是 `"resources": [null]`，旧实现
  `.map(*.Str)` 对 null 调 `~Nil`：

  ```
  Use of uninitialized value @result of type Any in string context
  ```

  除了噪音，还会把**空串当成一个资源名**收进列表。现在先滤掉未定义/空元素。

- 回归测试：新增 `t/meta-type-errors.t`（**51 条**）—— 4 个字符串字段的类型错矩阵
  （断言报错点名文件 + 字段 + 期望/实际类型）、缺失与 `null`、**防回退**（不回显
  有损字面量）、`from-index-row` 的可读报错、`resources` 含 null 不喷警告且不留空串、
  `auth` 数组/null 两种形态归一，以及端到端（CLI rc=1 且 stderr 里能找到文件名与
  字段名、不再出现 `Type check failed`）。
- 变异验证：不校验类型 → **15 红**；回显有损字面量 → **2 红**（正好那两条防回退
  断言）；缺失静默默认 → **9 红**；`resources` 的 null 不过滤 → **2 红**；
  `auth` 退回只认字符串 → **2 红**（含 `expected Str but got Array` 原文）。
- 全量真实数据复核（这是本轮几个结论的来源）：缓存里 **28 个已解包发行版**全部
  解析成功；**全量索引 25117 行**全部通过，**0 误拒**；扫描过程中的
  `Use of uninitialized value` 警告由 4 条降到 **0 条**。
- 文档：双语 `docs/architecture{,.en}.md` 的「发行版 = 元数据文件」一节补一张
  「畸形外部数据按**能不能无损修复**分三类」的对照表（报错 / 归一 / 滤除）与两条
  纪律（索引与 META 两条路共用同一归一实现；报错要给可执行的改法但不回显字面量）；
  `docs/nav-map.md` 同步 `Distribution` 行数。

- Bump to 0.88.1.

## 0.88.0 - 2026-09-23
- **Windows 保留字符改为「百分号编码」，不再放进路径**（1.0 审查 P2 批第三项）。
  动手前我对它的定性**错过一次**，先更正：`* ? " < > |` **不含路径分隔符、也不含
  `.`/`..`，拼不出目标之外的路径**，所以它**不是安全漏洞**（与 0.87.0 修的
  `/` `\` `..` `:` 不是一类）。它真实的性质是**健壮性缺陷**，而且后果可复现：

  | 平台 | 旧行为 |
  | --- | --- |
  | Windows | 缓存路径变成 `.../<name>/*.tar.gz` → 打开失败报 **`Invalid argument`**，重试 3 次后回滚 |
  | 其它 | 不报错，但造出一个名字就是 `*` 的文件/目录（还会被 shell 的 glob 展开） |

  两边报错都看不出真因 —— 用户只会觉得"这包有毒"。

- **影响面（全量索引 25117 条实测，逐分量扫 50234 个 name/version）**：
  `*` 出现在 **109 条**（全是 `version` 占位符：`Add`、`Algorithm::Viterbi`、
  `Apache::LogFormat`…）；`?` `"` `<` `>` `|` `%` **各 0 条**。
  另核实：这 109 条的 `source-url` 本身是**好的**（URL 里是 `%2A` 编码），
  坏的只有本地路径 —— 所以修好它就能让它们真的装上。

- 修法：`Fs.path-component` 内部把输入分成**两类，处理方式刻意不同**
  （这条线已写进函数注释与 `docs/architecture{,.en}.md`，别混起来）：
  · **穿越类**（`/` `\`、恰为 `.`/`..`、控制字符、结尾点）→ **继续 die**（有安全含义）；
  · **Windows 保留类**（`* ? " < > |`）→ **百分号编码**：`*`→`%2A`、`?`→`%3F`、
    `"`→`%22`、`<`→`%3C`、`>`→`%3E`、`|`→`%7C`，且 **`%` 自身编成 `%25`**。

  【为什么是编码，而不是"替换成 `_` 之类"】编码是**单射**的 —— 不同输入永远落到
  不同输出，所以不会出现"两个分量撞进同一目录"（那正是 0.87.0 选择"拒绝而非静默
  清理"的理由）。`%` 一起编码就是为单射服务的：字面量 `%2A` 必须与"编码后的 `*`"
  落在不同目录。测试里专门放了 `_` / `a_` / `A_B` 样本，让"替换成 `_`"那类非单射
  写法当场撞车。

- **端到端实证（本项最硬的凭据）**：造一个 `version = "*"` 的发行版 ——

  ```
  装完 store/WinStar/%2A/            ← 落在编码目录，不是字面量 *
  账本记 version=*                   ← 用户可见面仍是原始串
  外部 raku 进程 use StarPkg → USE_OK 42   ← 真的能用
  ```

  CUR::Installation 那层**接受** `*`，不会二次失败 —— 这点动手前专门验过，
  否则"修了路径还是装不上"就白改了。

- 兼容性（全量真实数据复核）：**0 个分量被误拒**（历史 store 条目照常可达）；
  映射发生变化的分量**恰好 109 个**，且只有 `*` → `%2A` 一种。
- 回归测试：`t/path-safety.t` 由 69 → **85 条**，新增编码矩阵（7 个字符）、
  混合串、**单射性**（12 个输入两两不撞车）与 `version="*"` 的端到端安装。
- 变异验证：去掉编码 → **13 红**；把 `*` 换成 `_`（非单射）→ **6 红**，且正好含
  那两条单射性断言；改成"拒绝"（方案 B）→ 测试硬中止、门禁判失败。
- 顺带修掉一处文档漂移：`xt/http-no-hang.t` 的头部注释仍称网络层是「纯 Raku 的
  HTTP::Tiny（无外部进程）」—— 那是 0.82.0 之前的说法。现状是**单后端
  `HTTP::Tinyish`，底层就是系统 curl，仍然是外部进程**（区别在"谁来管子进程"）。
- 文档：双语 `docs/architecture{,.en}.md` 的路径安全不变式补"两类字符"的划分与
  109 条的实测数据；`docs/nav-map.md` 同步 `Fs` 行数。

- Bump to 0.88.0.

## 0.87.3 - 2026-09-23
- **修掉两处「静默错答」**（1.0 发布前审查 P2 批里的前两项）。共同点是：不报错、
  不崩，看起来一切正常，但给出的答案是错的。

  **① 预发布版的比较** —— `Version.key` 原先把整个预发布段折成一个布尔
  （`$!prerelease eq '' ?? 1 !! 0`），于是 `1.0.0-alpha` / `-beta` / `-rc1` 的 key
  **完全相同**、`cmp` 一律判 `Same`。而 `cmp` 是 `pick-best` / `sort-versions` /
  `installed` 星号标注共用的底座，于是答案**取决于输入顺序**：

  | 调用 | 修复前 | 修复后 |
  | --- | --- | --- |
  | `pick-best("*", <1.0.0-alpha 1.0.0-beta 1.0.0-rc1>)` | `1.0.0-alpha` ❌ | `1.0.0-rc1` |
  | `pick-best("*", <1.0.0-rc1 1.0.0-beta 1.0.0-alpha>)` | `1.0.0-rc1` ✅（纯属运气） | `1.0.0-rc1` |

  修法：新增 `Version.!pre-key`，按 semver 2.0.0 §11 展开成可比较结构 ——
  正式版 > 预发布；数字标识符 < 字母数字标识符；同位数字比**数值**（不是字典序）；
  前缀相同则字段少的更低。顺带在 `from-string` 里剥掉 build metadata
  （`+build` 在 semver 里**不参与**优先级比较，不剥会把 `1.0.0-alpha+1` 与
  `1.0.0-alpha` 排成两个不同版本）。

  影响面（本机全量索引 25117 条实测）：带预发布标记的只有 **16 条 / 3 个发行版
  （0.06%）**；其中 1 个发行版的最佳版本会变 —— 生态里的 `Hey` 从
  `1.0.0-beta.2`（错）变成 `1.0.0-beta.9`（对：它是 8 个 beta 里最高的）。

  **② 卸载的「虚假成功」** —— `Client.uninstall` 此前不看 `Installer.uninstall`
  的返回值，无条件打印「已卸载 Foo」。四类「其实什么都没发生」的情形全部报成功 +
  **rc=0**：包根本没装 / 指定版本不存在 / 范围没匹配到任何版本 / 因反向依赖被拒绝
  （最后这条走 `msg-warn` + `return`，同样 rc=0）。脚本靠退出码判断成败
  （`set -e`、`uninstall X && …`）会被全部骗过；而且报的是「已卸载」——
  比不报更糟，用户以为卸干净了。

  修法：`Installer.uninstall` 改为返回「本次真的从 CUR 摘掉了哪些版本」
  （以 CUR 为准 —— 它才是「装没装」的事实来源）；`Client.uninstall` 据此判定，
  一个都没摘掉就 `fail-die`（rc=1）并给出可执行的措辞（列出实际已装版本；
  被依赖而拒绝时给出 `--recursive` / `--force` 两条出路）。反向依赖拒绝同样改成
  `fail-die`。`--force` / `--recursive` / 正常卸载的路径行为不变。
- 回归测试：
  · `t/version.t` 新增预发布版语义块 —— 10 组 `cmp` 三向断言、semver §11 示例序列
    整条严格升序、`pick-best` / `sort-versions` **颠倒输入结果不变**、`satisfies`
    侧一致性（`1.0.0-alpha` 不满足 `>= 1.0.0`）。**顺序无关性是关键**：
    旧实现在同一集合上换个输入顺序就换答案，只断言「选出来了」根本抓不到。
  · 新增 `t/uninstall-missing.t`（29 条）：四类情形进程内断言「抛错」且
    **断言没有打印「已卸载」**（那正是 bug 的症状）、断言失败后包仍在（无副作用）、
    端到端子进程断言**真实退出码**、以及 `Installer.uninstall` 的返回值契约。
- 变异验证（门禁不会变红就等于没有门禁）：
  `key` 退回布尔 → `t/version.t` **9 红**（`t/uninstall-missing.t` 保持全绿，隔离正确）；
  Client 不看返回值 → **12 红**；拒绝路径退回静默 `return` → **16 红**。
- `xt/rdepends.t` 同步：拒绝卸载现在以 `die` 表达（rc=1），测试改用裸块 + `CATCH`
  接住并断言「确实被拒绝」（`Mid` 仍在）。
- 文档：双语 `docs/architecture{,.en}.md` 的「版本管理」补两条不变式 ——
  预发布定序**只有一处实现**（`Version.key`）且别退回布尔；卸载成没成是**判定的**
  不是宣告的。`docs/MANUAL{,.en}.md` 的 `uninstall` 段补退出码说明与预发布定序速查。
  `docs/nav-map.md` 行数表顺带校准（8 处漂移）。

- Bump to 0.87.3.

## 0.87.2 - 2026-09-23
- **修掉 semver 糖语法 `^` / `~` 的上界算错（1.0 发布前审查的 F4）—— 方向是「约束过宽」**。
  根因很短但很典型：`Version` 里数「用户写了几段版本」用的是
  `.comb('.').elems`，而 **`comb` 返回的是匹配到的分隔符本身**，不是被分隔的段：
  `'1.2'.comb('.')` → `(.)` 只有 1 个元素，段数却是 2 —— 整整差一位，
  于是「按最后指定的位 +1」那类分支全部错位：

  | 约束 | 修复前上界 | 正确上界 |
  | --- | --- | --- |
  | `~1.2`（最常见的两段写法） | `<2.0.0` | `<1.3.0` |
  | `~0.1` | `<1.0.0` | `<0.2.0` |
  | `~0.0` | `<1.0.0` | `<0.1.0` |
  | `^0.0` | `<1.0.0` | `<0.1.0` |
  | `^0.0.0` | `<0.1.0` | `<0.0.1` |

  影响面：`uninstall <包> --version='~1.2'` 会**连 1.3.0 / 1.9.0 一起卸掉**；
  依赖解析里同类的 `^0.x` 约束会放进整个 1.x。比写严了危险得多 —— 用户以为锁住了
  minor，实际放进来整个大版本。
- 修法：新增 `Version.!spec-parts`（`split('.')` 数段数，语义不歧义），
  `!caret-upper` / `!tilde-upper` 共用它。**`~1.2.3` 与 `^1` 当年「看起来是对的」纯属
  歪打正着**（差一位后恰好落进正确分支）—— 这也是这个 bug 能活这么久的原因。
- 回归测试：
  · `t/version.t` 的糖语法块改成 **12 行矩阵**，每行三条断言 —— **精确展开串**（逐字）
    + **应含** + **应不含**（`t/version.t` 对 `~1.2` 原先只有正面断言「含 1.2.5」，
    上界放宽到 2.0.0 后照样为真 → 整个 bug 在套件里隐形；**放宽类 bug 只能靠负面断言
    与逐字展开串抓**）。共 80 条用例。
  · `t/uninstall-range.t` 补 `~1.2` 的**用户可见面**端到端（这个文件的注释一直声称覆盖
    `~1.2`，实际只测了 `^1.0`）：版本集 `1.2.0 / 1.2.9 / 1.3.0 / 1.9.0` 正是为区分
    「锁 minor（正确）」与「放到 2.0.0（错误）」而挑的。
- 变异验证：`!spec-parts` 复原成 `comb` 计数 → `t/version.t` 10 红、**`t/uninstall-range.t`
  2 红**（正是新加的 `~1.2` 块，证明它对老 bug 有区分力）；`tilde-upper` 忽略 minor → 8 红；
  `caret` 全零分支不看段数 → 4 红。
- 文档：`MANUAL{,.en}.md` 里一条**过时的说法**顺带修掉 —— 它写「`--version` 仅接受精确
  版本号，不支持范围」，而范围（`<1.0` / `>=1.0,<2.0` / `*` / `^1.0` / `~1.2`）早已支持，
  CLI 的 help 文案里本来就写着；现在两份 MANUAL 都改成准确描述 + 糖语法上界速查。
- 顺带修一处**测试自身的环境脆弱点**（`t/ghost-provides.t` 的子进程校验）：它用
  `run(..., :in($空文件路径))` 给子进程喂 EOF，而实测在本机环境下**传路径**会让子进程
  直接被杀（`exitcode -16`、零输出），只有传**已打开的 IO::Handle** 才正常。
  改成传句柄（注释里写明原因与三种失败的写法，免得日后被"简化"回去）。
  ⇒ 教训：`run` 的 `:in` 用句柄比用路径稳。

- Bump to 0.87.2.

- Bump to 0.87.2.

## 0.87.1 - 2026-09-23
- **修掉「幽灵安装」：装完报成功、`use` 永远找不到（1.0 发布前审查的 F2 + F3）**。
  症状最恶劣的一条链：`install` 返回 rc=0 → 账本记着已装 → `installed` 列得出来
  → 而 `use` 报 "Could not find"，且 `verify` 还会说「✓ 没有不一致」，用户毫无线索。
  两个独立成因，必须一起修：
  ① **`provides` 缺失**（META6 里它是可选字段）。旧代码照样写出一份 provides 为**空**
     的标准 META6.json → CUR::Installation 一个模块都不挂。全量索引实测：zef 索引
     8060 条**一条不缺**，缺的全在 **rea（138 条 / 53 个发行版）与 cpan（8 条，
     真正的 Perl 5 包）** —— 正好是 `.pm` 时代的老包。provides 被写成字符串
     （`"provides": "lib/Foo.rakumod"`）也会被归一成空，同一个坑。
  ② **`.pm` 不被认作模块源**。`provides-with-paths` / `scan-provides` 只试
     `.rakumod` / `.pm6`；而 `.pm` 是 Rakudo 正式支持的模块扩展名（会打 deprecation
     提示，`inst#` 下同样能加载），早期 Perl 6 时代是主流写法。实证 `Cache::Memcached`
     的模块就是 `lib/Cache/Memcached.pm`，旧代码完全认不出。
- 修法：
  · 新增 `RakuPM::Distribution.provides-from-disk` —— **有声明就用声明（且只保留磁盘上
    真有文件的项，与旧行为逐字节一致）；声明为空、或声明的模块一个都对不上磁盘，
    就扫 `lib/` 反推**。`lib/` 是发行版自己的内容，比一份缺失/写坏的元数据字段更可信。
  · 模块源扩展名收敛成**唯一一份** `@MODULE-EXTS`（`.rakumod` / `.pm6` / `.pm`），
    跨模块经 `module-extensions` 取（`which` 也改成走它）——原先同一份清单在两处各写了
    一遍、都漏了 `.pm`，正是这次漏的根。
  · **幽灵安装护栏**：反推仍拿不到任何模块、而 `lib/` 里确有文件时，`Store.put`
    **拒绝入库**（宁可不装，也不写一个 `use` 不到东西的空条目）。判定放在落盘**之前**，
    所以拒绝时连目录都不会建。「只发 bin/资源、不含模块」的发行版仍按合法处理。
  · `Store` 的 `META.json`（`Store.provides-of` / `show-store` 靠它按模块名反查发行版）
    与账本条目都改记**生效的** provides，而不是索引行那份空声明——否则 `installed`
    按模块名查、`autoremove` 的反向依赖图对这批包全部失灵。
  · `verify` / `doctor` 新增第 ⑥ 段检查（issue kind = `ghost-provides`）：揪出**已经装在
    机器上**的历史幽灵条目（store 的标准 META6.json 零声明、而 `lib/` 里有模块源），
    并给出可执行的修复建议。`--fix` 修不了这类，只能重装（重装会走上面的反推）。
- 回归守卫 `t/ghost-provides.t`（37 条）：反推判定矩阵 + `.pm` 识别 + 三类畸形元数据的
  **真装 + 外部 raku 进程 `use`** 端到端 + 负控（拒绝入库、零残留）+ 事务性（失败不毁
  已有账本）+ verify 能查出历史幽灵。变异验证：去掉反推 → 20 红；去掉 `.pm` → 9 红；
  在 `Query` 里重新内联扩展名清单 → 静态守卫 1 红。
- 实测（真实生态，均为索引里缺 `provides` 的 REA 老包）：`Cache::Memcached`（模块是
  `.pm`）、`Compress::Snappy`、`C::Parser`、`Automata::Cellular`、`Clifford`、`Chess`、
  `App::P6Dx` 现在都装得上且 provides 正确，前四者经外部 `raku` 进程 `use` 验证通过；
  修复前它们全是幽灵安装。

- **修掉门禁自身的一个死锁**（本轮顺带发现；不修就跑不完 `xt/` 那一半）：
  `tools/run-suite.raku` 的 `run-one` 在 `run(..., :out, :err)` 之后是**先 slurp
  stdout、再 slurp stderr**。子进程若把某一条流写满管道缓冲（Windows 上约 64KB），
  它会阻塞在 write 上，而父进程永远等不到另一条流的 EOF → **双向死锁**。表现极具
  欺骗性：**CPU 0%、无子进程、无限等待**，看起来只是「套件跑得慢」。实测 `xt/http.t`
  的 stderr 有 258KB（stdout 仅 1.4KB），`--only=xt` 因此卡在它上面 23 分钟一动不动，
  而单独跑该测试只要 **1 秒**、18/18 全绿。修法：新增 `capture()` helper **并发**抽干
  两条流，5 处捕获点全部改走它（预热/编译/静态检查那几处同款隐患一并消除）。

- Bump to 0.87.1.

## 0.87.0 - 2026-09-23
- **安全修复：发行版名 / 版本号未消毒导致任意路径穿越写文件（1.0 发布前审查的 P0）**。
  `name` 与 `version` 来自**包自己的 META6.json / 生态索引行**（发布者可控），却被
  直接当作路径分量拼进 `store/<name>/<version>`、下载缓存、日志目录。修复前
  `Store` 里只做 `subst('::','-')`（旧私有方法 `!safe-name` —— 名字叫 safe，其实
  **只处理模块分隔符、不处理路径结构字符**），而同一条路径的 `$version` 连这层都没有。
  实测载荷 `"name": "../../../../../../tmp/PWNED"` 会把整个 store 条目写到 target
  **之外**（逃到 `C:\Users\<me>\tmp\PWNED`）——即「发布一个包就能往用户机器任意位置
  写文件」。`version` 同样可穿越。
- 修法：新增**唯一的净化入口** `RakuPM::Fs.path-component`，并在**全部 6 处**拼路径的
  地方接入（不只 Store）：
  ① `Store.!safe-name` / `path-for`（name **与** version）；
  ② `Client::Tester` 的测试日志目录 `log/<name>/<version>`；
  ③ `Repository::Ecosystem` 的**下载缓存**路径（索引行可控，发生在 store 之前）；
  ④ `Installer` 的临时文件名 `rakupm-adopt-<pid>-<name>-<v>.json`；
  ⑤ `Author` 的分发归档名与发布落点目录；
  ⑥ `Client::Git.!fetch-source` 的 `fetch --to` 输出目录。
  另在 `Author.new` 加一处**冗余**防御（那里的模块名已被正则校验，留着防日后放宽正则）。
  另加**第二道防线** `RakuPM::Fs.assert-under`（落盘前断言路径在根之内；入口会随代码
  演进增多，漏做一处就立刻变成明确报错，而不是一次静默的越界写入）。
  净化规则：拒 `/` 与 `\`、恰为 `.` / `..`、控制字符、结尾点；模块分隔符 `::` 与单个
  冒号 `:` 一律归一到 `-`。**选择 die 而不是静默清理**：静默把 `../x` 改成 `x` 会让不同
  发行版撞进同一目录（互相覆盖），比拒绝更危险。
- **向后兼容**：对一切合法输入，`path-component` 与旧 `subst('::','-')` **输出逐字节相同**
  （已用真实索引全量验证：25117 条条目 0 误拒；本机 33 个已装 store 条目 0 映射差异）
  —— 历史 store 条目仍然可达，不会被判成"未入库"。唯一例外是名字含**单个冒号**的
  5 个发行版（`App:Ralc`、`Slang:Date` 等），0.87.0 起也归一到 `-`：它们在 Windows 上
  本来就无法作为目录名（`mkdir App:Ralc` 会写成文件 `App` 的备用数据流 ADS），归一等于修好。
- 新增 `t/path-safety.t`（68 条）：净化函数接受/拒绝矩阵、`assert-under`（含"同前缀兄弟
  目录"这个经典假绿点）、以及**端到端**——真装恶意发行版，断言被拒 + **按旧实现推算的
  逃逸落点确实不存在**。另含**正控**（合法名必须确实装成），避免"安装链根本没走到拼路径
  那一步"造成否定断言假绿。已做变异验证：去掉 version 净化 → 11 条红；让 `path-component`
  变成空操作 → 45 条红。

- Bump to 0.87.0.

## 0.86.2 - 2026-09-23
- **Windows 上给原生 MD5 增加 Git 候选**：与 macOS / Linux 不同，**Windows 不自带
  `libcrypto`**，全新装机常常一个候选都命中不了（那时就走纯 Raku + 清单兜底）。但
  **Git for Windows 几乎人人都有**，且自带一份 `libcrypto-3-x64.dll` —— 故补三个候选：
  默认安装目录的绝对路径（`C:\Program Files\Git\mingw64\bin\…` 与 `(x86)` 那一支）
  与裸名 `libcrypto-3-x64.dll`（Git 的 `mingw64\bin` 在 PATH 上时命中）。
  实测本机两条都能加载（`abc` 向量正确）。探测失败照旧跳过，对其它平台零影响。
- `.files.json` 清单缓存**保留**（按评估结论）：它是「原生库不可用」时的兜底 ——
  在那种机器上把 `verify` 从约 10.9s 压到约 1.9s。
- 文档：`MANUAL{,.en}.md` 的 verify 说明补充「Windows 上还会尝试 Git 自带的那份」。

## 0.86.1 - 2026-09-23
- 文档补充（无功能变更）：说明**原生 MD5 不需要用户额外安装 OpenSSL**。
  `libcrypto` 是**系统库**、不是 raku-pm 的新依赖（META6 没加依赖、zef 也不会装它）；
  设计是「有就用、没有就自动回退纯 Raku」，两边指纹逐位一致，只是快慢不同。
  各平台实际情况写进 `MANUAL{,.en}.md` 与 `RakuPM::MD5` 头注释：
  macOS 系统自带 `libcrypto.dylib`（LibreSSL）、Linux 多数发行版随 curl/git 依赖装好
  `libcrypto.so.3`、Windows 不保证（开发机上那份实测是 LibreSSL 3.8.2，随某软件带进来）。

## 0.86.0 - 2026-09-23
- **MD5 改用原生实现（NativeCall 绑系统 libcrypto），并把「系统命令」那一层删掉**。
  起因：用户提出「CPU 密集还是要系统程序来做」。实测证实：
  - NativeCall 绑 `libcrypto` 的 `MD5()`：store 里 296 个文件（2.1 MB）**0.246s**
    —— 比纯 Raku 的 10.97s 快 **44 倍**（RFC 向量与纯 Raku 逐位一致）。
  - `verify` 强制全量哈希 **11.7s → 2.86s**（此时剩下的主要是 ~1s 的 CLI/Client 启动
    与读文件，哈希已不再是瓶颈）。
  - 这条路比「起系统命令」更快，而且**不需要**为每个文件起一个进程、也不需要处理
    certutil / md5 -q / md5sum 三种输出格式与平台命令名差异。
- 实现：`RakuPM::MD5` 里写多个候选 sub（`libcrypto` / `libcrypto.so.3` / `.so.1.1` /
  `libcrypto.so` / `libcrypto.3.dylib` / `libcrypto.dylib` —— 库名在 NativeCall 里必须是
  编译期字面量），**首次调用时逐个试并用 RFC 1321 的 `abc` 向量自校验**，结果缓存；
  全部不可用才落到纯 Raku。刻意不在 import 期探测（否则每条命令的启动都要付一次库加载）。
  新增 `native-md5-available()` 供诊断 / 测试。
- **删除**：`RakuPM::MD5` 的 `system-md5` 与 `parse-md5-bytes`、`RakuPM::Platform` 的
  `md5-command` 与 `parse-md5-output`、`md5-file` 的 64KB 阈值分支，以及
  `t/platform.t` 的第 4 节与 `t/md5.t` 的两个系统命令场景。
  连带消失的还有：中文 Windows 上 certutil 输出 GBK 导致整条路径静默失效那类坑。
- `md5-file` 现在只剩两条路：**原生 → 纯 Raku**，任何环境都算得出来，且没有平台分支。
  `RAKUPM_MD5=raku` 仍可强制纯 Raku（调试 / 对照）。
- `t/md5.t` 改为钉住新不变量：`native-md5-available()` 返回 Bool 且不抛错；
  **原生可用时其结果必须与纯 Raku 逐位一致**（6 组样本含空串 / 中文）；`RAKUPM_MD5=raku`
  能强制回纯 Raku。刻意**不**断言「必须有原生」—— 那会让没装库的机器假红。
- 一个 Raku 语法坑（记档）：sub 名不能以 `-数字` 结尾（`md5-libcrypto-3` 会被解析成
  减法）；另外 `List.fmt('%02x')` 的**默认分隔符是 `", "`**，拼 hex 必须写
  `.fmt('%02x', '')`，否则自校验会全部误判失败（这个坑让原生探测一开始全灭）。
- 文档：`MANUAL{,.en}` 的 verify 说明、`architecture{,.en}` 的「verify 内容校验」一节
  都重写为三步迭代史（0.84.3 修静默失效 → 0.85.0 少算 → 0.86.0 换实现）。
- 说明：`.files.json` 清单缓存**保留**（原生库不是每台机器都有，它是那种情况下的兜底）。

## 0.85.0 - 2026-09-23
- **`verify` 的内容校验加「清单旁车」，少算而不是算得快**（首次 ~13s → 之后 **1.8s**）。
  背景：内容校验要逐文件算 MD5，纯 Raku MD5 只有约 0.16 MB/s（store 296 个文件 10.7s），
  而**并行 / 多进程实测都救不了**（8 线程仅 1.18x；多进程被每个 ~1s 的启动成本吃掉）。
  做法：每个 store 条目在根目录存一份 `.files.json`，记下每个文件的 (size, mtime, md5)。
  下次校验时 **size + mtime 都没变的文件直接复用上次算好的 md5**，只重算变过 / 新增的文件。
  - 它是与 `.digest` / `build.json` 同类的「缓存旁车」：`!fingerprint` 跳过它（否则指纹自指），
    Installer 布置 site 时只处理 lib/resources/bin 等子目录、不会把它带进安装点。
  - **取舍明说**：「改了内容、又把 size 与 mtime 都保持原样」的改动查不出来。
    要无条件全量哈希：`RAKUPM_VERIFY_NOCACHE=1`，或删掉条目里的 `.files.json`。
  - 实测：`raku-pm verify` 首次 20.4s（建清单）→ 命中 **1.8s**；逃生门强制全量 11.7s。
- `t/store-verify.t` 新增 5 条场景（4 → 9）：清单落盘且可解析、命中时确实复用旧 md5
  （连同取舍一起断言）、`RAKUPM_VERIFY_NOCACHE=1` 强制全量后能正确判出「已修改」。
  已做变异验证（让清单永不命中 → 第 8 条必红）。
- 文档：`MANUAL{,.en}.md` 的 `verify` 条目补清单缓存与逃生门说明；
  `architecture{,.en}.md` 性能节补 0.85.0 的对策与实测数据。

## 0.84.3 - 2026-09-23
- **性能修复：中文 Windows 上「大文件走系统 md5」这条加速路径静默失效**（`verify` 19.3s → 13.5s）。
  `certutil -hashfile x MD5` 在中文 Windows 上输出的是 **GBK**（实测原始字节：
  `MD5 \xb5\xc4 C:\…\licenses.json \xb9\xfe\xcf\xa3:`），而 `$proc.out.slurp(:close)`
  默认按 UTF-8 解码 → 抛 "Malformed UTF-8" → 被外层 `try` 吞掉 → 返回空 →
  **静默回退纯 Raku**。后果：每个 ≥64KB 的文件都白起一次 certutil（0.25s）再跑一遍
  慢速纯 Raku（331KB 实测 2.07s），合计 2.5s —— 比直接用纯 Raku 还慢。
  修法：按字节读（`slurp(:close, :bin)`）+ latin-1 解码（字节↔字符一一对应，
  非 UTF-8 字节不会抛错），抽成可单测的 `RakuPM::MD5.parse-md5-bytes(Blob, $os)`；
  `system-md5` 外层仍包 `try` 兜底（解析出意外只意味着「这次系统命令不可用」，
  回退纯 Raku，绝不把异常抛给 Store 的指纹计算）。
  实测：331KB 文件 `md5-file` 2.5s → **0.25s**；store 全量 md5 17.6s → **10.7s**
  （平均 59.5 → 36.1ms/文件，>300ms 的慢文件清零）；`raku-pm verify` 19.3s → **13.5s**；
  两条路径（系统命令 / 纯 Raku）哈希值一致。
- `t/md5.t` 新增场景：用 latin-1 造出真实的 GBK 字节（`chr(0xB5)` 等）断言
  `parse-md5-bytes` 仍能解析出摘要。**原有的用例抓不到这个坑**——它只断言「不抛异常 +
  结果一致」，而静默回退恰好两条都满足。已做变异验证（把 latin-1 改回 utf-8 → 必红）。
- **记档（实测否定的方向）**：**并行算 store 指纹没有收益**——8 线程只有 1.1x
  （19.8s → 17.5s，结果与串行逐位一致）。纯 Raku 的哈希计算在 MoarVM 上不能真并行
  （对照：并行解析索引 JSON 有效，4.32s → 3.31s，因为那条含 IO）。剩下的 10.7s 是
  纯 Raku MD5 约 0.16 MB/s 的固有成本，加并发救不了。
- `architecture{,.en}.md` 性能章节新增「又一处：`verify` 的内容校验」小节。

## 0.84.2 - 2026-09-23
- **性能修复：索引缓存被重复解析一遍**（`search` / `info` 等查询命令快约 4.3s，约 −36%）。
  `Ecosystem.!read-fetched-at` 原先 `slurp + from-json` **整个索引缓存**，只为读一个
  `_rakupm_t` 时间戳；而紧接着 `!load-rows` 又会把同一份文件完整解析一遍 —— 每次 refresh
  白花一倍解析时间。索引缓存实测最大 24MB（rea 23.6 / zef 12.6 / cpan 2.6 MB），三份
  合计约 4.2s。
  改为**只扫文件头尾各 4KB**（时间戳总落在最前或最后几个字节：rea / cpan 在偏移 5，
  zef 在距末尾 24 字节 —— 两种键序都在用，故头尾都扫），解码用 latin-1
  （截断多字节字符也不会抛错）。
  实测（同机 min-of-3~5）：`refresh(rea)` 5.86s → **3.19s**；
  `raku-pm search sqlite --offline` 12.03s → **7.71s**；
  `raku-pm info SQLite --offline` 11.12s → **6.64s**。
  `version` / `list` / `installed`（不读索引）不受影响。
- 顺带记档：真正的**匹配**只占 0.36s（零命中与满命中的关键词耗时相同）—— 这类命令的
  大头从来是「把 40MB 索引读进来解析」，不是搜索算法。
- `xt/http.t` 新增场景：把 `_rakupm_t` 写在缓存文件**末尾**（zef 那种键序）、且构造
  > 8KB 的缓存（小了就没有区分力 —— 头部一次 read 就读完了），断言仍能识别新鲜度。
  已做变异验证：临时禁用尾部扫描 → 该条必红。
- 架构文档（`architecture{,.en}.md`）新增「慢命令的真正大头：索引加载」小节，含
  实测数据、修复方式，以及「查询用精简索引」方案**评估后不做**的理由。

## 0.84.1 - 2026-09-23
- 文档修订（无代码变更）：`docs/nav-map.md` 的「哪些可砍」清单里有两条判断错了，已改正。
- `NativeLib`：原写「非 native 包用不到，可降级为按需」—— **错**。它不只是 `raku-pm native`
  这个只读查询命令的后端，**安装链每次都调它**查待装发行版声明的 `:from<native>` 本地库与
  `:from<bin>` 外部命令依赖（`Client/Git.rakumod` 的 `!check-native-libs` /
  `!check-external-deps`，除非 `--no-native-check`）。而且实测：
  热 precomp 缓存下 `require RakuPM::NativeLib`（含 Platform + Fs）约 **276ms**，
  `require` 任意轻量模块（`Fs`）约 **275ms**，空 raku 启动约 **4ms** ——
  那 ~270ms 是「Raku 加载任意模块」的固定开销，NativeLib 并不额外付费；
  故「改成按需 require」收益≈0。**撤销该建议**。
- `Author`：原写「建议 `raku-pm author ...` 独立命名空间」—— 模块面其实**早已独立**
  （独立文件 + 独立命名空间 + CLI 懒加载 `(require RakuPM::Author)`），扎眼的只是命令面；
  0.84.0 已按「导航档」处理（只加 `help <组名>` 分组总览，命令面一个没动）。

## 0.84.0 - 2026-09-23
- **help 增加「分组总览」这一层（纯导航；命令面零变化）**：新增 `raku-pm help <组名>`
  （如 `raku-pm help author`，中文标题同样认：`raku-pm help 作者侧`），列出该组的定位
  一句话 + 组内命令与各自用法行。此前 `help <命令>` 只能看单个命令、`help` 摘要只给
  名字，中间缺「这组是干嘛的、我要不要看」——只装别人包的用户看到索引里 `作者侧` 那 8 个
  发布相关命令平铺着，容易以为必须搞懂它们才能用。
- `raku-pm help` 摘要给 `作者侧` / `开发门禁` 两个重型组各加一句注记
  （「发布/打包用，只装别人包不需要」），并在末尾提示 `raku-pm help <组名>`。
  索引完备性不变：命令与别名照旧一个不漏（`t/help.t` + `xt/cli-output-stability.t` 守着）。
- 未知参数报错细化：「没有名为 'x' 的命令」→「没有名为 'x' 的命令或分组」，并多给
  一行分组入口提示。
- 命令面**未动**：`raku-pm publish` / `bump` / `check` … 仍是一等公民，CliSpec 未改。
  本次只在 Help 新增 `help-group` / `%GROUP-HELP` / `%GROUP-NOTE`，以及 CLI 的
  `print-help` 里一条「不是命令就试分组名」的分支（命令优先）。
- 文档：README×2 / MANUAL×2 的 help 分级说明补上 `<组名>` 一层；nav-map 行数更新。

## 0.83.0 - 2026-09-23
- **self-upgrade 从三条路径收敛到两条（不兼容变更）**：删掉「当前这份在 git 仓库里 →
  就地 `git pull` 你的工作树」那条（删 `!upgrade-from-git` 与 `self-upgrade` 的 `:$source`
  形参）。它与远端路径本就是同一件事的两种取法，且是唯一会改动用户工作树的分支；删后
  在源码仓库里跑也走默认远端（gitee），行为更可预期。现在只剩：① `--from-dir <目录>`
  （本地源码目录，也是两阶段自举第二阶段的入口）；② git 远端（`--from <url>` 或默认上游）。
  注：原先同在这条路上的 `!in-git-repo` **没删**，只是搬到 `Client::Git` ——
  `install-from-dir` 用它判断本地目录是不是 git 工作树以记录 HEAD 提交号（与 self-upgrade 无关）。
- 收尾消息去重：把原先在三个方法里各抄一遍的「跑 `raku-pm version` 确认 / Windows
  PowerShell 的 PATHEXT 一次性提示」抽成 `!after-upgrade-hint(Str $how)`，供
  `!install-self-from` 与远端回退分支共用。Windows PS1 提示原先只挂在被删的那条路上，
  现在所有升级路径的 Windows 用户都能看到。
- CLI：`self-upgrade` 分支去掉 `my $src = raku-pm-git-root()` 与 `source => $src`
  （`raku-pm-git-root()` 仍被 `preflight` 使用，保留）；`Help` 的 self-upgrade 文案去掉
  「在 git 仓库里跑则 pull 当前仓库」。
- 测试：`xt/self-mgmt.t` 删「--from 覆盖 source」与「checkout 走 git 分支」两个场景，
  并去掉随之为死的 `capture` 助手与已不再使用的 `use RakuPM::Distribution`；
  `t/named-arg-wiring.t` 删掉「② self-upgrade 的 source 接线」文本级断言（形参已不存在）。
- 文档：`MANUAL{,.en}.md` 的 self-upgrade 来源优先级改为两条；`nav-map.md` 更新行数与
  「已简化」标注，并把 0.82.0 漏改的「双 HTTP 后端」一并更正为单后端；`architecture{,.en}.md`
  的用法行补 `--from-dir`，并修正 role 列表（`Reporter` → `Query`、4 → 5 个 role，含 `Flusher`）；
  `Repository/Matching.rakumod` 与 `Client.rakumod` 里残留的 `Reporter` 注释改为 `Query`。
- 不兼容提示：`self-upgrade` 不再有 `source` 语义（该形参已删）；在源码仓库里跑不再自动
  `git pull` 你的工作树。

- Bump to 0.83.0.

## 0.82.0 - 2026-09-23
- **HTTP 层改成单后端，只保留 curl —— 移除可插拔后端机制（不兼容变更）。** 删掉纯 Raku 的
  `HTTP::Tiny` 兜底后端（`lib/RakuPM/HTTP/HTTPTiny.rakumod`）与整套插件化：`%REGISTRY` /
  `register-backend` / `backend-names` / 后端顺序 `@.order` / `RAKUPM_HTTP_BACKEND` 环境变量 /
  「加载失败自动回退下一个」。`RakuPM::HTTP` 现在只持**一个**后端实例（默认首次用到时懒建
  `RakuPM::HTTP::Tinyish`，底层系统 curl），外加指数退避重试 —— 与 `post-form` / `post-json`
  （本就直调 curl）统一到 curl 一条路径。
  - 测试替身改经构造器 `:backend` 注入（原先靠 `register-backend` + `RAKUPM_HTTP_BACKEND` 选后端）。
- 相应改测试：`t/http-wire.t` 删 HTTPTiny 用例与「回退」用例（只留 Tinyish 的条件 GET + 超时，
  plan 10→4；并修掉会让进程**永久挂住**的假服务器 accept 线程 —— 收尾加哨兵连接唤醒 accept，
  实测从挂死 10 分钟+ 降到 ~5s 退出）；`t/http-debug.t` 改用 `:backend` 注入、
  删后端顺序/覆盖来源/回退类断言（plan 13→9）；`xt/http.t` 删 `backend-names` / `order` 断言、
  改注入替身。
- 依赖与声明：`META6.json` 去掉 `HTTP::Tiny` 依赖与 `RakuPM::HTTP::HTTPTiny` provides，
  description 的「HTTP 客户端插件化」改为「单后端 HTTP 客户端（curl）」。
- 具名实参白名单：`tools/NamedArgCheck.rakumod` 补 `:host` / `:port`
  （`IO::Socket::INET.new(:host,:port)` 连客户端 —— `t/http-wire.t` 的哨兵连接用）。
- 文档：`README{,.en}.md` 依赖列表、`MANUAL{,.en}.md` §16 与依赖/环境变量表、
  `architecture{,.en}.md` 的 HTTP 章节（删「双后端 / 回退 / HTTP::Tiny 调用侧加闸」段）、
  `nav-map.md`、`Repository/Ecosystem.rakumod` 注释、内部 `CODEBUDDY.md` 同步。
- 不兼容提示：`RAKUPM_HTTP_BACKEND` 不再被识别；`RakuPM::HTTP.register-backend` 已移除。

- Bump to 0.82.0.

## 0.81.0 - 2026-09-23
- **测试进度改为「按当前文件的用例」显示（配合 0.80.0 的串行测试）。** 串行下同一时刻只跑
  一个文件，故进度行从「第几个文件 / 共几个」改为当前文件的
  「`测试 <文件号>/<文件数>  <文件名>  <用例完成数>/<用例总数> 用例  pct%`」——通过实时解析
  该文件正在输出的 TAP（顶层 `ok`/`not ok` 计数 + `1..N` plan）持续刷新；plan 尚未出现时
  （如 `done-testing` 在结尾才打印）只显示已完成用例数、不显示百分比。
  - `Client::Tester`：`!progress-text`（文件级）→ `!progress-file-line`（用例级），新增
    `!tap-counters`（只数顶层 TAP，缩进的子测试不计入）；`!run-one-test-async` 增
    `:$progress`/`:file-index`/`:file-total`/`:$on-case`，在 stdout tap 里按新 TAP 增量回调重画；
    `!run-tests-serial` 经 `:on-case` 把「行状态 + 文件序号」传下去，跑完统一擦除。
- `xt/tester-progress.t`：夹具改用前置 `plan 1`，新增「`1/1 用例`」断言，注释改用例级口径。
- 文档：`architecture{,.en}.md` 的 ⑤「实时进度」描述改为用例级格式。

## 0.80.0 - 2026-09-23
- **测试改为串行；移除测试并发机制 —— `--test-jobs` 开关随之取消（CLI 不兼容变更）。**
  此前默认按 CPU 核数并行跑 `t/`，而为了让并发不误伤安装，还得额外配「串行预热 precomp」+
  「失败用例串行复验」两道安全网。这份复杂度对单机教学场景不划算（且 `self-upgrade` 早已
  单独强制串行），故整体移除：`Client::Tester` 删掉 worker 池（Channel 信号量 + Lock）、
  `!warmup-precomp` 与 `RAKUPM_TEST_JOBS`，退化为纯串行循环 `!run-tests-serial`；运行行由
  「N 个文件，并发 J」改为「N 个文件，串行」。
  **保留**「失败用例串行复验」（防上游 flaky，如 `Concurrent::Stack 1.3` stress 连跑 10 挂 3）、
  超时看门狗与实时进度。
- 整条 CLI 链同步移除 `test-jobs`：`CliSpec`（install / reinstall / smoke / test 四处旗标集）、
  `InstallOptions`（去掉 `$.test-jobs` 字段与 describe 行）、`CLI` / `Client` / `Git` /
  `SelfManager`（去掉 `:test-jobs` 形参与透传）、`Help`（去掉 `[--test-jobs=N]`）。
- 测试调整：删 `xt/tester-warmup.t`（专测已移除的预热）；`xt/tester-async.t` 删并发场景
  （保留失败首行 + 日志 + 超时）；`xt/tester-defaults.t` 场景1 改断言「3 个文件，串行」；
  `t/install-options.t` 去掉 test-jobs 断言（plan 15→11）。
- 文档：`MANUAL{,.en}.md` §15 的「测试并发」小节改写为串行版、环境变量表去掉
  `RAKUPM_TEST_JOBS` 与 `RAKUPM_NO_PRECOMP_WARMUP`；`architecture{,.en}.md` 的「测试并发」
  章节改写并更正因果；`nav-map.md` 同步（并纠正其中「套件并集会死锁」的错误说法 —— 套件
  现为默认并行 + 预热 + 复验）。
- 不兼容提示：`raku-pm install X --test-jobs=N` 现在报「未知选项」（原先被接受）。

## 0.79.4 - 2026-09-22

- **重命名 `Client::Reporter` → `Client::Query` 并拆分写命令（可读性重构）**：
  原 `Reporter` 名字误导（里面其实装着 `uninstall` / `rollback` / `autoremove`
  等写命令）。现在这三个写命令挪回 `Client`（编排器），`Query` role 从此只承载
  只读查询 + 诊断（`list` / `which` / `why` / `depends` / `doctor` / `verify` 等）
  + native 库检查。所有 `use`/`does` 引用、测试、文档同步更新，`provides` 登记
  同步改名。零功能损失（方法逐字搬移，已通过加载 + 三个相关测试 + CLI 冒烟）。
- **新增 `docs/nav-map.md`（贡献者速查）**：「改动 X → 去哪个文件」排障地图 +
  端到端流程 + 模块分层与「哪些可砍」清单，并登记进发布包公开白名单。

## 0.79.3 - 2026-09-22

- **内部笔记迁移 + 脱敏**：把根目录的 `CODEBUDDY.md` / `REFACTOR-ISSUES.md` /
  `COMMANDS-WIKI.md` 三份开发笔记搬进 `docs/internal/`，与公开文档（`MANUAL` /
  `architecture` / `quickstart` …）分开；`.distignore` 相应改为排除 `docs/internal/`
  整棵子树（继续不进发布包）。笔记里的 gitee 个人账号 `skyter10086` 统一替换为
  `<发布者>`（包身份 `auth<zef:skyter10086>` 属已发布元数据，保持不变）。
  CLI 行为与发布内容均不变。
- 版本：0.79.2 → 0.79.3（patch：内部文档 relocation + 脱敏；无功能变更）。

## 0.79.2 - 2026-09-22

- **修：发布包（sdist）里混进了内部文档** —— `docs/refactor-roadmap.md`（内部重构路线图）
  与 `docs/github-issue-templates.md` 会随 `dist` / `publish --remote` 一起发到公开生态。
  `.distignore` 的注释早就写明「内部开发文档不宜发到公开生态」，但它的匹配是**按路径**的：
  根目录的 `REFACTOR-ISSUES.md` 一直被排除着，而它的 **docs/ 版本是后来新增/搬家**的 ——
  没人补 `.distignore`，于是静默进包（实测 `tar -tzf RakuPM-0.79.1.tar.gz` 清单里确有其名）。
  现已把两条补进 `.distignore`，并实测重打包后的 tar 清单只剩 8 份公开文档。
- **新增门禁：`check-shipped-docs`（挂进 `check-docs`，故 `--docs-only` / `--precommit` /
  全量三种口径都跑）** —— 这类泄漏**不会让任何测试变红**（它只在发布那一刻生效），
  所以必须前移到门禁：
  - 「**哪些 .md 允许进发布包**」改成**显式白名单**：新增一个 .md 时二者必居其一 ——
    登记进 `tools/run-suite.raku` 的 `@public`（＝决定公开），或写进 `.distignore`
    （＝内部文件）；否则门禁红。
  - 白名单条目必须存在（防清单过期）。
  - **刻意不复刻「`.distignore` 条目是否指向存在的文件」**：那条规则 `Author` 打包时
    已经在对「写了却匹配不到任何文件」的条目喷 ⚠（见 `xt/dist-ignore.t` 场景 3），
    同一规则实现两遍＝日后改一处忘一处。
- **变异验证**（不会变红的门禁等于没有）：① 去掉新排除项＝复现原 bug → 门禁如期变红并
  点名 `docs/refactor-roadmap.md`；② 新增一个未登记的 `docs/*.md` → 变红；
  ③ 删掉/注释掉门禁接线 → `t/run-suite-docs.t` 变红。为此在 `t/run-suite-docs.t`
  加了一条**接线断言**（plan 3 → 4）—— 且断言写成「有【一整行】恰好是这个调用」而非
  `.contains`：后者对**被注释掉**的接线同样成立，变异时实测漏判过。
- **文档纠错：`--help` 在 PowerShell 下是否可用**。此前 4 处措辞称「`raku-pm --help`
  会被 zef wrapper 吞掉」，实测**已不准确**：本机 wrapper 是 raku-pm 自己生成的
  `site/bin/raku-pm.ps1`，它把 `@args` 原样转发，`raku-pm --help` / `-h` 都正常进帮助
  （退出码 0）。现改为明确区分**遗留 zef 包装器**（`raku-pm.exe`，其 `*%` 会吃掉
  `--help`，且会在安装/升级时被改名 `<name>.zef-old`）与 **raku-pm 自生成的包装器**，
  并保留「推荐一律用 `raku-pm help`」这条跨形态都成立的建议。
- 版本：0.79.1 → 0.79.2（patch：修发布泄漏 + 加门禁 + 文档纠错；CLI 行为不变）。

## 0.79.1 - 2026-09-22

- **文档：把「安装时的测试并发会不会影响安装成败」写清**（`MANUAL{,.en}.md` §15）。
  新增要问的三层：① **并发边界** —— 只有「跑测试」这一段并发，解析/下载/构建/入
  store/进暂存/晋升**全串行**（`Client.!install-chain-inner` 是 `for @order -> $dist`），
  跨进程靠文件锁；② **两道安全网** —— 串行预热 precomp（消掉并发首编竞态 → 假的
  `===SORRY!===`）、失败串行复验一次（过滤假失败与上游 flaky），并写明复验日志
  `<名字>.flake.log` 不覆盖首跑日志；③ **残余风险表**（如实列出）—— 复验也挂会
  **整链回滚**、上游 flaky 与并发无关、**测试自己用共享资源时预热防不了**（这类包
  `--test-jobs=1` 能过），以及超时误判；附四个旋钮与三个环境变量。
  `architecture{,.en}.md` 的「测试并发」节同步补上这层因果（面向设计）。
- **文档：新增「启动耗时构成」与**「评估后**不做**」**的优化记录**
  （`architecture{,.en}.md`）。实测（7 轮取最小值）：raku 启动 **0.148s**、
  加载**第一个** precomp 模块 **+0.20s（固定成本）**、之后**每多**一个仅
  **+0.011s**；`RakuPM::CLI` 全量 +0.231s，`RakuPM::Client` 整条重模块图**只多 ~80ms**。
  结论：**precomp 模块的加载成本与模块行数无关**（`RakuPM::Installer` 1205 行与零依赖的
  `RakuPM::Net` 单独加载耗时几乎相同），贵的始终是**加载机制本身**。
  - 据此**评估后决定不做**「拆开 / 惰性化 `RakuPM::Client` 重模块图」：收益上限仅
    ~80ms，且只对纯查询命令有意义（对 install 是 0.1% 噪声）；而要真做到「读命令不加载
    Installer/Builder/Tester」，必须**去掉 `Client` 的属性类型约束 + 改惰性访问器** ——
    等于拿掉项目里最安全攸关那个类（整树事务化安装）的编译期保证；且它**本来就是惰性
    `require` + 已 precomp**，与 0.79.0 那次「bin 从不可缓存变可缓存」的架构性收益不同。
  - 顺带排除一个可疑项：CLI 顶层 `use JSON::Fast`（只为 `version` 读一次 META6）看似可省，
    但 `RakuPM::Spec` 已 `use JSON::Fast`，**传递引入不可避免**，边际成本仅 **0.020s**。
- 无代码/行为变更（纯文档），版本 0.79.0 → 0.79.1（patch）。

## 0.79.0 - 2026-09-22

- **CLI 主体从 bin 搬进 `RakuPM::CLI`、bin 退化成薄壳 —— 启动实测量快 0.6~0.8s**。
  根本原因：**bin 脚本不享受 precomp**，raku 每次运行都要把它整体编译一遍；实测
  `raku -c bin/raku-pm.raku` 要 1.267s（空脚本基线才 0.516s），即光编译 bin 就 ~0.75s。
  把逻辑搬进普通模块后，每次只编译那十几行薄壳，其余走模块的 precomp 缓存。
  实测（同机同命令，Git Bash）：`version` 1.72s → **1.14s**、`help` 1.91s → **1.10s**；
  PowerShell 端重构后约 **1.00s**。raku 解释器自身启动那 ~0.55s 无法消除。
- **验收三条**：① 全量门禁 `--no-xt` 全绿；② **xt/ 全部 39 个文件全绿**；
  ③ **新旧 CLI 输出逐字节相同** —— 用 `git archive` 取 0.78.1 旧树，对 12 个纯输出场景
  （version / help / help all / help <命令> / 4 种 shell 补全 / 裸调用 / --help / 未知命令）
  比对 stdout+stderr+退出码的 md5：**12/12 完全相同**。
- 附带修复：入口刻意叫 `run-cli` 而**不是** `run` —— 叫 `run` 会把 Raku 内建的 `run()`
  （本模块内多处用它起子进程）遮蔽掉，令 `preflight` 转调 run-suite 变成
  `raku-pm raku tools/run-suite.raku …`。这个坑由 `xt/preflight.t` 抓到（`--no-xt` 口径
  覆盖不到，说明大重构必须额外跑 xt/）。
- `t/named-arg-wiring.t` 的文本级断言改指 `lib/RakuPM/CLI.rakumod`（代码搬了家）；
  provides 新增 `RakuPM::CLI`；架构文档（中英）的层级图补上该模块与「薄壳」说明。
- 版本：0.78.1 → 0.79.0（minor：新增模块 / 磁盘布局变化；CLI 行为不变）。

## 0.78.1 - 2026-09-22

- **测试失败的病因不再被外壳藏住**：Raku 编译失败时 stderr 是嵌套的 ——
  `===SORRY!=== Error while compiling <测试文件>` / `<模块.rakumod>` 两行外壳之后，
  才是真正的病因（如 `Could not find Foo:ver<…>` / `Version must be specified`）。
  旧实现前台**只印首行**，等于把病因完全藏起来；而日志目录在安装失败【回滚】时又会
  被清空，用户事后无从查起。现在 `RakuPM::Client::Tester.!failure-lines` 会跳过开头
  连续的 SORRY 外壳行，把最内层病因印最多 3 行；**普通失败仍只印首行**（契约不变）。
  （起因：实测某个上游包安装失败时，前台只剩一句无信息量的
  `Error while compiling t/…`，真正原因被两层外壳挡在后面。）
- 测试：`xt/tester-defaults.t` 新增场景 4（2 断言）锁住新行为，并做变异验证
  （短路新逻辑后场景 4 如期变红）；既有「只印首行」契约由场景 2 继续钉住。

## 0.78.0 - 2026-09-22

- **help 分级显示**：`raku-pm help` 默认只给紧凑命令索引（一屏内：命令按分组 +
  别名速查），`raku-pm help <命令>` 看单个命令的用法，`raku-pm help all` 给完整手册。
  原先 help 正文 358 行一次性刷屏、前面的内容被顶走，想看还得往回翻。
- **help 正文抽到新模块 `RakuPM::Help`**：命令清单 / 分组 / 别名一律从 CliSpec 现取
  并自动排版 —— 新增命令时索引自动跟上，不会「加了命令忘了写 help」。模块可 precomp，
  bin 从 1304 行瘦到 ~970 行；bin 侧惰性 require（只有 help 才加载）。
- **bash wrapper 启动优化**：入口路径改为安装时写死（与 .bat/.ps1 同款），不再每次
  调用 `ls | sort -V | tail` 挑最新版本 —— 那一句在 Windows/Git Bash 下要 ~0.7s。
  写死路径不在时（store 被 clean 清掉）才退回探测。
- 测试：新增 `t/help.t`（18 断言）；`xt/cli-output-stability.t` 的 help 完备性断言
  （首行含「用法」+ 全命令 + 全别名 + 幂等）继续成立；`t/installer-shell-templates.t`
  补「入口写死 + fallback」断言。

## 0.77.0 - 2026-09-22

- 新增中文/英文用户手册 `docs/MANUAL.md` 与 `docs/MANUAL.en.md`：覆盖安装、命令参考、依赖解析、原子安装与世代回滚、HTTP 层、作者工作流与发布等。
- 修正手册中的两处不准确：`RAKUPM_AUTHOR_AUTH` 默认值应为 `zef:skyter10086`（非 `gitee:`）；`RAKUPM_ECOSYSTEM` 实为「仅兜底覆盖、非默认单一索引」，默认内置 zef+cpan+rea+本地仓库。

## 0.76.0 - 2026-09-22

- **发布准备：auth 命名空间 `gitee:` → `zef:`，bump 0.75.2 → 0.76.0（minor）**：
  为发布到 fez / 42.zef.pm 生态，META6 的 `auth` 须为 `zef:<用户名>`（官方要求），
  原 `gitee:skyter10086` 会在上传时被拒或归属错误。故：
  - `META6.json` auth → `zef:skyter10086`，version → 0.76.0；
  - 两份 README 的 Identity 示例、help 文本、`Author.rakumod` 的 `RAKUPM_AUTHOR_AUTH`
    默认值、相关 xt 测试 fixture 一并同步为 `zef:`；
  - `check-preflight.t` 的 env auth 同步，门禁 auth 一致性检查恢复 0 警告。
  （fez 用户名即 `skyter10086`；若注册时该名被占用需改用他名，发布前再改一次 auth 即可。）

## 0.75.2 - 2026-09-22

- **把「个人痕迹」钉成门禁**（`tools/run-suite.raku` 新增 `check-personal-traces`）：
  0.75.1 清掉痕迹后，我在写 0.75.1 的 Changes 时**又把那两个字符串原样写了回来**
  （一次是 0.75.0 的条目说「已清理某用户名」、一次是 0.75.1 的条目写「本机用户名 →
  中性占位」）—— 语义上是报告修复，效果上是换个地方把它发到公开生态。同类错误
  连续复发两次，说明「发版前记得 grep」不可靠，故改为机器守：
  - 三条规则：① 本机工作区绝对路径；② 历史遗留的短用户名；③ `Users\<名字>` 且名字
    不在中性占位白名单（`user`/`foo`/`x`/`tester`/`example` …）里（疑似真实用户名）。
  - **扫描范围 = 会进发布包的内容**：跳过 dot 目录与 `blib`，并跳过 `.distignore`
    列出的条目 —— 否则内部文档里的路径会把门禁误报成红（不会误伤不发布的东西）。
  - 在 `--docs-only` / `--precommit` / 全量门禁**三种口径下都会跑**：起初写成了
    `xt/` 测试，但 `--precommit` 与 `--no-xt` 都不跑 xt/，等于日常提交路径上根本没生效，
    故挪进门禁脚本本体。
  - 门禁脚本里的黑名单**必须是拼接构造**（`'z' ~ 'by'`）：写字面量的话，这个检查
    文件自己就成了泄漏源（`tools/` 同样进 sdist）。
  - 变异验证 4 项全通过：三条规则各注入一次都变红（命中哪条也精确对上），
    另验证「被 `.distignore` 排除的文件不参与扫描」（不误报），之后全部还原。
- 修正 0.75.1 条目里对「门禁落在哪」的描述（当时写的是新增 xt 测试；实际落在门禁脚本）。
- 文档：`docs/architecture.md`（与 `.en.md`）在「跑测试只用 run-suite」那条补上它带的
  三道静态闸门（具名实参 / 文档纪律 / 个人痕迹）；门禁脚本自身的 `--help` 同步。
- 顺带记一个 Raku 坑（本版本首次踩到）：**`Array.append` 不展平 `Seq`** —— `.map` 的
  返回值会被当成**单个元素**塞进去（stringify 成内容拼接，空 Seq 即空串），
  症状是「明明命中 3 处、打印出来却是 3 行空白」。必须显式写 `.append(@x.map({…}).List)`。
- 版本纪律：0.75.1 → **0.75.2**（patch：清理 + 门禁加固，无用户可见行为变更）。

## 0.75.1 - 2026-09-22

- **发布前审计补漏：清掉发布包里最后的个人痕迹**（0.75.0 的 sdist 内容级扫描发现）：
  - `t/builder-quote.t` / `t/known-test-failure.t` / `t/known-failure-nojson.t` /
    `lib/RakuPM/Builder.rakumod` 里的本机 Windows 用户名 → 中性占位 `user`（共 10 处）。
    **只换用户名、反斜杠数量一字不动** —— 那几个夹具是 `is-known-windows-test-failure`
    的测试数据，其「单/双反斜杠形态」正是 0.36.16 线上漏匹配事故的回归锚点，
    动它必须原样保留转义形态（改完 `t/known-test-failure.t` 等三个测试全绿即证）。
  - `Changes` 里 0.74.1 那条「清理用户名痕迹」的说明本身又把待清理的原始用户名写了
    出来 —— 等于换个方式把它发到公开生态。改写为中性描述（`/home/<user>` 之类），
    语义不变、不再出现该字符串。
  - 复核：两类痕迹在全仓残留均为 **0 处**（0.75.2 起由门禁脚本自动守着，见下条）。
- 版本纪律：0.75.0 → **0.75.1**（patch：清理 / 无行为变更）。

## 0.75.0 - 2026-09-22

- **新增 `.distignore` 自定义排除清单**（`RakuPM::Author`）：0.74.1 的隐藏项过滤只挡
  「以 `.` 开头」的东西，挡不住【名字正常的内部文件】（AI 指令 / 内部路线图 / 内部手册
  这类：文件本身没毛病，只是不该发到公开生态）。现在在发行版根放一个 `.distignore`
  即可逐条排除 —— **文件保留在 git 仓库、只是不进 sdist**。
  - 语法刻意做小、可预测：一行一条相对路径（`/` 分隔）；空行与 `#` 开头的行忽略；
    **行尾注释也认**（`#` 前须有空白，与 `.gitignore` 同款语义 —— 不剥的话
    「`FOO.md   # 内部文档`」会被当成含空格的长文件名而**静默失配**）；行首 `./`、
    行尾多余 `/` 剥掉；匹配 = 相对路径恰好相等或以 `<条目>/` 开头（目录条目覆盖整棵
    子树）；**不支持通配符**（避免「以为排除了、其实没有」的静默事故）。
  - **安全网**：列了却匹配不到任何文件的条目会 `⚠` 提示到 stderr（不阻断打包）——
    拼错文件名会立刻暴露，而不是静默把文件发出去。
  - `.distignore` 自身以 `.` 开头，按既有规则天然不进包。未提供该文件时零行为变化。
  - 本仓库据此排除 `CODEBUDDY.md` / `REFACTOR-ISSUES.md` / `COMMANDS-WIKI.md`
    （**仍留在仓库**，仅不进发布包）；`repo/` 是 `t/store-lock.t` 的夹具，明确保留。
  - 测试：`t/author-dist.t` 新增场景 7（4 断言，plan 15→19：文件条目 / 行尾注释与空行 /
    目录条目覆盖子树 / 常规文件照常进包且 `.distignore` 自身不进包）；新增
    `xt/dist-ignore.t`（6 断言，真实 CLI 子进程：排除生效、未列出的照常进包、
    `.distignore` 不进包、**拼错条目会提示到 stderr**）。两条排除分支均做过变异验证
    （摘掉过滤器 / 摘掉行尾注释剥离 → 测试变红）。
  - 实测真实仓库：sdist 197 个成员，三份内部文档全部排除、无任何 `.` 开头条目，
    LICENSE / repo / META6 / README 均在。
- 文档：`README.md` / `README.en.md` 的「打包内容」各补 `.distignore` 说明；
  `dist` 的 `--help` 注明「排除 `.` 开头的隐藏项 + 可选 `.distignore` 清单」。
- 版本纪律：0.74.1 → **0.75.0**（minor：新能力）。

## 0.74.1 - 2026-09-22

- **修 `dist` 打包泄漏：不再把隐藏项（`.` 开头）打进 sdist。** `dist` 是按【目录】扫描
  （走 `RakuPM::Fs.walk-files`，只跳 `.git/.hg/.svn/.precomp/blib`），**不读 `.gitignore`**
  —— 于是仓库里被 gitignore 的私有目录（如 `.workbuddy/` 工作记忆、`.workflow/` CI 配置）
  会被打进发布包发到公开生态（发布前审计实测到）。现于打包这一层额外排除「相对发行版根、
  路径中含隐藏分量」的一切条目（`.workbuddy` / `.workflow` / `.vscode` / `.idea` /
  `.gitignore` / `.gitattributes` …）。
  【为什么在 dist 层过滤、而不改 `walk-files` 的默认跳过集】后者还用于内容指纹（`Store`）
  与 provides 扫描；改默认集会让既有包指纹漂移，只在打包层收紧影响面最小。
  另把 `raku-pm.lock` 也一并排除（作者在发行版目录里跑过 `raku-pm install` 时，cwd 会留下
  这个本地锁文件，属于本地状态、不该进发布包）。
- **新增 `LICENSE`（Apache License 2.0 全文）**：`META6.json` 的 `license` 由 `Artistic-2.0`
  改为 `Apache-2.0`（此前声明了 license 却一直没有 LICENSE 文件）。
- **清理个人用户名痕迹**：历史文档与注释里遗留的个人路径 / 用户名一律换成中性占位
  （`/home/<user>`、`/Home/<User>`、`<user> 的 X 现场` 之类），涉及
  `Changes` / `docs/architecture.md`(与 `.en.md`) / `CODEBUDDY.md` /
  `lib/RakuPM/{Builder,Installer}.rakumod` / `t/builder-quote.t` / `xt/rollback-restore.t`。
- 测试：`t/author-dist.t` 增场景 6（4 断言）——断言 `.workbuddy/` / `.workflow/` /
  根级 dot 文件都不进包、而常规文件照常进包（plan 10 → 14）。
- 文档：两份 README 的「打包内容」补一句「排除任何 `.` 开头的隐藏项」。
- 版本纪律：0.74.0 → **0.74.1**（patch：修 bug）；双语文档版本示例同步。

## 0.74.0 - 2026-09-22

- **测试阶段实时进度**（治「跑测试时长时间黑屏、像卡死」）：`RakuPM::Client::Tester`
  现在在**终端**上刷实时进度 —— 串行预热阶段逐文件 `预热 i/N … <文件>`；并发跑测试
  阶段逐文件 `测试 i/N  pct%  ✓/✗ <文件>`。都用 `\r` 原地重画（不刷屏、跑完擦掉）。
  - 那两段「长静默」正源于此：预热是逐文件串行 `-c` 编译（全程无输出），并发执行又是
    「结果先收集、跑完才汇总打印」。现在终端始终有动静，一眼看出没卡死。
  - **只碰 stderr，且仅当 stderr 是终端时才显示** —— 管道 / 重定向 / 测试捕获自动
    静默，`stdout` 与日志文件**零影响**（既有断言不受影响）。`RAKUPM_TEST_PROGRESS`
    设 `0` 强制关、设 `1` 强制开。
  - 顺带修：`· 运行测试` / `· 预热预编译` 两行打印后补 `$*OUT.flush`，否则块缓冲会把
    标题压到进度行之后（看着像「先跑后说」）。
- 新增 `xt/tester-progress.t`（9 断言）：起子进程分别以 `RAKUPM_TEST_PROGRESS=1/0` 跑
  一次真实安装，断言 =1 时 stderr 出现「预热 i/N」「测试 i/N pct%」且能到 100%、=0 时
  完全静默；两种情况下 `stdout` 的 `✓` 汇总都不变。
- 文档：bin help 的测试阶段说明、`docs/architecture.md`（与 `.en.md`）测试要点各补一条
  实时进度说明（含 `RAKUPM_TEST_PROGRESS`）。
- **修真 bug：发行版 wrapper 下敲裸 `raku-pm` 不显示帮助。** 三份 wrapper 模板都会隐式
  注入 `--target=<前缀>`，而「无位置参数」分支把所有非 help 旗标一律当「未知选项」→
  裸 `raku-pm` 实际得到的是 `未知选项：--target` 并退出 1（真人在终端里敲 `raku-pm`
  就是这个形态）。现改为 **公共旗标（`target` / `no-lock` / `promote-bin` /
  `no-promote-bin` / `offline` / `no-color`）一律放行** —— 它们都不蕴含某个命令；只有
  真·拼错的旗标才报错（`--dry` 这类每命令旗标无命令时仍报错）。
  `t/cli-flags.t` 补 5 条断言做回归（公共旗标无命令 → 帮助；`--dry` 无命令 → 仍报错）。
- 版本纪律：0.73.0 → **0.74.0**（minor：新能力）；双语文档版本示例同步。

## 0.73.0 - 2026-09-22

- **UI 美化**（`RakuPM::UI` 新增三个能力，全部 **rich-gated**：非 TTY / 管道 / 测试捕获下
  一律退化为原样文本，故 help 的纯文本输出与改动前**逐字节一致**，既有断言零影响）：
  - `ui-banner()`：`raku-pm help` / 无参数时在终端顶部打印 ANSI Shadow 艺术字标题
    （`raku-pm`，6 行、每行 60 列等宽）+ 一行副标题；非 TTY 返回空串。
  - `ui-colorize-help()`：help 正文**命令名染粗体绿、`--旗标` 染黄、`<占位符>` 染品红**；
    并把 `--no-color` 的判定提前到 help 分支**之前**（`--no-color help` 与 `help --no-color` 都生效）。
  - `ui-tree-branch()` / `ui-tree-child()`：树形连接符（rich=Unicode `├─ └─ │`；非 rich=ASCII `` |- `- | ``）。
- **依赖树显示改造**：`rdepends` 从扁平列表改为**反向依赖树**（自目标包向上长：谁依赖我 →
  谁又依赖它们 …，每层标出「经 <模块> + 约束」，`%seen` 防环）；`depends` 已有的树改用同一套
  UI helper 并上色（已装=绿、待装=黄★、找不到=红、重复=暗）。树连接符随之 rich / 非 rich 自适配。
- **修 `bump --to=X.Y.Z` 不生效的真缺陷**：bin 的 bump 派发此前只认【位置参数】里的版本号，
  `--to=` 被静默忽略、退化成 patch（而 help 明确写了 `--to=`；`Author.bump` 本就支持 `:to`）。
  现从 `%opts<to>` 读取，位置参数仍优先。
- 版本纪律：0.72.7 → **0.73.0**（minor：新能力）；双语文档版本示例同步。

## 0.72.7 - 2026-09-21

- **Issue 1（文档拆分与快速上手）**：README 瘦身，深度设计内容迁到 `docs/`。
  - `docs/architecture.md`（中英 `docs/architecture.en.md`）：接收架构总览 / HTTP 双后端 /
    安装五阶段 / 原子安装与世代回滚 / 反向依赖与回收 / 校验 / 仓库数组 / 本地目录布局 /
    版本管理与多版本 / 锁文件 / 跨进程文件锁 / 测试并发 / 缓存回收 / flush / 与 zef 对应 /
    核心设计思想——**仅位置迁移，内容未删减**。
  - `docs/quickstart.md`（中英 `docs/quickstart.en.md`）：新增 5 分钟最小上手
    （install 一个包 + `use` 加载，复用真实输出）。
  - README 只留介绍 / 安装 / 最小用法（快速开始）/ 生产侧 / 测试 / 版本约定 /
    已实现 vs 待实现；顶部加文档导航，章节空缺处加单行回指 `docs/architecture.md`。
  - 中英文两份 README 同步，顶部互链保持。
- **文档纪律门禁同步扩展**（`tools/run-suite.raku` 的 `check-docs`）：版本示例
  `RakuPM:ver<>` 的校验范围从 `README.md`/`README.en.md` 扩展到递归扫描 `docs/*.md`；
  README 两文件维持「必须有版本示例且一致」，docs 下改为「若有则必须一致」（不强制必须有）。
  **变异验证**：故意在 `docs/architecture.md` 注入 `RakuPM:ver<0.0.0>`，门禁变红并定位到该文件，
  还原后恢复绿——符合 Issue 1「门禁仍能拦版本漂移」的验收。

## 0.72.6 - 2026-09-21

- **Issue 2（CLI 元数据独立模块验收）收口**：0.67.0 已把命令元数据抽成
  `RakuPM::CliSpec`（旗标/别名/分组/写锁/联网维度一处定义，bin 只做解析与派发）。
  本次补 `xt/cli-output-stability.t` 锁 `raku-pm help` 与
  `raku-pm completions`（bash/zsh/fish/pwsh）的**稳定性不变量**：
  · 幂等性——连跑两次字节一致（防 hash 顺序 / 日期 / 路径等非确定因素漂出），
  · 完备性——CliSpec 的每一个规范命令、别名、旗标都出现在产物里（真正的回归风险
    是「CliSpec 改了但补全脚本没跟上」，而非某次措辞变化），
  · 结构片段——四种 shell 各自的关键引导串都在。
  不冻结 84KB 易碎快照，改守不变量；以后动 CliSpec 或补全文案不会误红、动错才会红。

- **Issue 3（用户面消息层收口）收口**：0.68.0 已建 `RakuPM::Message`（warn/error/
  hint/fail-die + 退出码）与 `RakuPM::UI`（排版/色彩/TTY 降级）分工、Message 不自带
  ANSI。本次把 `lib/RakuPM/Client/Reporter.rakumod` 里**绕开** Message 层的 stderr
  端提示收口：doctor 外部命令缺失、native 放弃安装、`uninstall` 反向依赖守卫、
  verify 外部命令依赖、`browse` 打开失败等处，从裸 `note "⚠ …"` / `die "⚠ …"`
  改为统一走 `msg-warn` / `msg-hint` / `fail-die`，与 bin 的 CLI 校验报错共用同一套
  「措辞 + 退出码」出口。报告体里的 `⚠` 表格注释行属排版、保持走 stdout 不变。

## 0.72.5 - 2026-09-21

- 延续 0.72.4：installed META6.json 的 `build-depends` / `test-depends` 也做保真写回。
  原先这两字段同样没写（raku-pm 自己的构建/测试依赖图读不到准确约束）。
  现抽出 `!faithful-depends($dist-dep, $source, $field)` 助手，`depends` /
  `build-depends` / `test-depends` 三档复用：优先照搬源 META6 对应字段
  （数组 / 平铺 Hash / depends.<phase>.requires 嵌套结构都原样），缺源时退回
  `module:ver<constraint>` 至少保版本。新增回归 t/store-depends-fidelity.t（12 例）。

## 0.72.4 - 2026-09-21

- 修 installed META6.json 的 `depends` 失真（md：Store.rakumod `!meta6-for`）：
  原先写 `depends => $dist.depends.keys.sort.List`，只写模块名、把版本/作者
  约束全丢了，已安装包的 `depends` 退化成裸名（`String::Utils` 而非
  `String::Utils:ver<0.0.35+>:auth<zef:lizmat>`）。后果：① 读 installed 元数据的
  包/测试拿到错误依赖——真实案例 Identity::Utils 0.0.29 的
  "can we get dependencies from identity" 测试（test 176）因此 `not ok`；
  ② 污染 raku-pm 自己的反向依赖图（Installer.!depends-of 把约束读成 `*`）。
  改为直接照搬源 META6 的 `depends`（zef 同款原样保留），`$source` 缺失时
  退回「`module:ver<constraint>`」至少保版本。新增回归测试 `t/store-depends-fidelity.t`。

## 0.72.3 - 2026-09-21

- 修 `upgrade` 的潜在崩溃（Issue 8 的 golden e2e 才暴露）：`upgrade` 方法本身带有
  `Str :$version` 形参，其内部写 `self.install($name, :$version($target), :$dry)`
  时，`:$version($target)` 被 Raku 解析成「短配对 `(version => $version)`（取 upgrade
  自己的空 `$version`）再拿 `($target)` 去调用它」，于是抛
  `No such method 'CALL-ME' for invocant of type 'Pair'`，`upgrade` 整条链路直接死。
  之前没有任何测试走过「带版本约束的升级」，故该隐患一直潜伏。改为显式 fat-comma
  `self.install($name, version => $target, :$dry)` 即修。

- 新增两条跨阶段 golden e2e（`xt/`，Issue 8）：
  · `xt/golden-install-upgrade-rollback.t`：走 Client 命令层把
    `install V1 → upgrade → rollback` 串起来，并在每个阶段用子进程真实 `use` 该包、
    读出其版本常量，证明「安装状态」与「实际加载到的版本」一致（generations.t 没做的
    硬验证）；顺带抓出上面的 upgrade 崩溃。
  · `xt/golden-verify-fix.t`：走 Client 命令层的 `verify-all(:fix)` / `verify-all()`，
    制造「CUR 有、账本被清空」的半截状态，证明 `--fix` 真的把账本自动补记回来、而无
    `--fix` 只报不动（引擎级 t/verify-consistency.t 不覆盖命令层这三段分支）。

## 0.72.2 - 2026-09-21

- 修 self-upgrade 自装时会跑本项目 `t/`，并行首跑偶发触发 lib 预编译缓存竞态，
  前台先打出一个吓人但良性的 `✗ <某测试>.t`（Tester 的串行复验会吸收、升级仍成功）。
  self-upgrade 很低频，故在 `!install-self-from` / `!upgrade-from-git` / 回退
  `install-from-git` 三处给安装链传 `:test-jobs(1)`，让自装测试串行跑，从根上
  消除该竞态、不再出现假失败噪音。正常安装行为不变。

## 0.72.1 - 2026-09-21

- 修 uninstall 反向依赖守卫的判定反转：`Reporter.uninstall` 里拦截「被依赖的包」的
  逻辑原本写在 `unless $whole`（非整包卸载时才拦），但 `$whole=True` 才是「会删光
  全部版本」该拦的情形，导致 `uninstall` 一个仍被其它包依赖的整包时不会拒绝、留
  下坏掉的依赖。改为 `if $whole`，与注释语义一致；部分版本卸载（还有其它版本在）
  仍跳过检查，行为不变。顺带让 `xt/rdepends.t`（孤儿回收）通过。
- 对齐 `xt/search-spec.t` 的 `installed` 断言到表格渲染格式（`| Dual | 0.9.2 |` 用
  竖线分隔包名与版本，不再空格相连），与门禁内 `t/query-installed-store.t` 同款；
  这是实现表格化后测试漂移，非实现 bug。

## 0.72.0 - 2026-09-21

- （Issue 5）新增 `why` 依赖解析诊断命令：把「为什么选了这个版本 / 冲突的双方约束」
  讲清楚，不再黑盒。正常安装行为不受影响（仍走 `resolve-all`，会 die）。
  - 引擎（`RakuPM::Resolver.explain-all`）：**不 die** 的诊断入口 —— 跑解析 +
    拓扑排序，冲突时把结构化冲突存进 `%!last-conflict` 并返回 `ok=False`，与
    fatal 路径共用同一份数据，避免两套实现漂移。返回每模块的 chosen / 有效约束 /
    全部候选（含来源仓库、是否满足约束、淘汰原因）/ 完整请求链（谁因什么要求过它）。
  - 展示（`RakuPM::Client::Reporter.show-why`）：冲突时先友好展开「双方约束 +
    最后选定的版本」，再列冲突模块的候选细节（帮用户决定 pin 哪边）；非冲突失败
    （如找不到模块）转述 fatal 路径原始报错；普通解析逐模块打印选中版本、来源仓库、
    有效约束、候选表与请求链。
  - 协议面：CliSpec 登记 `why`（命令组 `packages`、别名 `wh`、支持 `--pin`、
    需要网络）；`bin/raku-pm.raku` 新增派发与 help 文本。
  - 来源仓库可追溯：`%!chosen-repo` 与 `chosen-detail` 让「这个版本从哪个仓库
    解析出来」可回答（Distribution 模型本身不带仓库信息）。
  - 修复：`!find-best-satisfying-all` 返回类型声明漏改 —— `!materialize-first`
    改为返回 `{dist, repo}` 哈希后，其声明仍是 `RakuPM::Distribution`，冲突时求
    交集改选会触发类型检查失败（`Type check failed for return value`）。已改为
    `Hash`，`t/conflict-msg.t` 的「'*' 与 '0.0.24' 交集非空不报冲突」场景恢复。

## 0.71.0 - 2026-09-21

- （Issue 6）HTTP 层可观察性增强：排障时不再黑盒。
  - `RakuPM::HTTP` 新增 `:$debug` 构造器开关 + 环境变量 `RAKUPM_HTTP_DEBUG`
    （1/true/yes/on 算开）双入口；**默认关**，关时本层零新增输出（不刷屏，
    且排障行前缀 `  · http:` 刻意不含 ⚠ / [RakuPM]，不会触发 zef install 的
    噪音判定）。开时把排障信息打到 stderr。
  - 可观测维度：① 后端顺序及来源（默认 `tinyish→httptiny` / 显式指定 /
    `RAKUPM_HTTP_BACKEND` 覆盖 —— 覆盖来源一并报出，解决「为什么这次用的是
    httptiny」查不清）；② 实际使用的后端、落到哪个后端（加载失败 / 不可用 /
    请求失败都报）；③ 全部后端不可用时报「所有后端都失败：[…]」；④ 条件 GET
    命中缓存统一为「304 命中本地缓存（Not Modified，不重新下载）」；⑤ 每个出口
    入口报出 `离线=ON/OFF`，与 `RakuPM::Net.offline()` 口径对齐（HTTP / 生态索引
    都经本门面被 `assert-online` 硬拦，git 走的是另一条子进程出口、不在本层）。
  - curl 上传路径（post-form / post-json）同样报出离线状态与 `RAKUPM_CURL`
    覆盖来源。
  - 新增 `t/http-debug.t`（纯假后端、零网络）覆盖：默认不刷屏、各维度可观测、
    离线仍被硬拦（行为不变）。
  - 修一个隐藏事故：`RAKUPM_HTTP_DEBUG` 的「开」判定原写成 `eq any(<1 true yes on>)`
    返回 Junction，赋给 `Bool` 属性会触发 Type check 失败 —— 会让**设了该环境变量的
    每次 `RakuPM::HTTP.new` 都抛异常**；改为 `.first + eq` 返回普通 Bool。

## 0.70.0 - 2026-09-21

- （Issue 9）扩展 `RakuPM::InstallOptions` 收敛安装参数：把尚未纳入该类的零散策略
  统一收入安装链单一载体，清除残余的逐层布尔/Int 抄写。
  - 新增字段 `pins`（依赖钉版 Map）、`test-jobs`（并发测试数）、`test-timeout`（单测
    看门狗秒数）、`recursive`（卸载递归）、`keep-store`（卸载保留 store 快照）。
  - `test-jobs` / `test-timeout` 改由 `InstallOptions` 承载，经 `$opts` 进入
    `Tester`（env `RAKUPM_TEST_JOBS` / `RAKUPM_TEST_TIMEOUT` 仍作默认值回退）；
    移除了 `Client` 上的 `has Int $.test-jobs` / `$.test-timeout` 实例属性及
    `set-test-jobs` / `set-test-timeout` 访问器。
  - 卸载侧 `keep-store` 经 `$opts` 进入 `Installer.uninstall`（`Reporter.uninstall`
    与 `reinstall` 内部回滚均改用 `:opts(...)`）；保留 `:$keep-store` 兼容形参以
    兼容直调内部方法与 `bin` 入口。
  - 新增 `t/install-options.t` 覆盖默认值 / `with()` 不可变派生 / `describe()` 仅列
    非默认字段。验收：现有安装命令行为不变（`t/install-opts.t`、`xt/install-opts.t`、
    `t/uninstall-*.t`、`xt/tester-async.t` 全绿），新增策略只改 `InstallOptions` 一处。

## 0.69.0 - 2026-09-21

- bump 顺带同步文档里的身份版本示例（`<name>:ver<旧版>` → 新版）：此前 bump 只改
  META6 + Changes，README 里的示例会与 META6 漂移，而文档纪律门禁要求两者一致，
  于是每次 bump 后都要手工补两份 README，漏了就一提交就红。现在扫描发行版目录下
  所有 .md（递归，跳过隐藏目录），只替换【恰好等于旧版本】的实例 —— 历史引用
  （如「0.60.0 起支持 X」）原样保留，改掉反而成了错误信息。新增 --no-sync-docs
  可关闭该步（发布第三方包、其文档里的身份示例不属于本包时用）。
- 修 Changes 新条目的插入位置：文件以 `# 标题` 开头时插到标题【之后】。此前是
  无条件前插，会把文件标题挤到新条目下面，形成颠倒结构（既有测试只断言
  `contains('## X.Y.Z')`、不看位置，故长期未暴露）。
- 新增 `raku-pm preflight`：一条命令给出「能不能提交」的明确结论。内部转调
  `tools/run-suite.raku --no-xt`（文档纪律 + 具名实参 + t/），与预提交钩子
  【共用同一份门禁实现】，不另起一套以免漂移；`--filter` / `--jobs` 可透传。
- bin 帮助文案补 `preflight` 与本条新增旗标。
- 测试：新增 `t/author-bump-docs.t`（14 断言：同步 / 不动历史引用 / --no-sync-docs /
  --dry 一个字节不改）；`t/author-bump.t` 补 2 条钉住 Changes 插入位置；
  新增 `xt/preflight.t`（4 断言：转调链路 + 退出码透传）。

## 0.68.0 - 2026-09-21

- **用户面消息层收口（新增 `RakuPM::Message`）**：把散在 `bin/raku-pm.raku` 里的
  **24 处裸 `die`**、以及无参数分支 / 顶层 `CATCH` / 注销失败等处的裸 `note`，统一到
  `usage-die`（用法错）/ `fail-die`（业务失败）/ `msg-error` / `msg-warn` / `msg-hint`
  五个出口；裸数字退出码（9 处 `exit 0` + 4 处三目）改用 `EXIT-OK` / `EXIT-FAIL` 常量。
- **与 `RakuPM::UI` 划清边界**：Message 只管「说什么、以什么身份说、用什么退出码」，
  排版与上色归 UI；**Message 不得自己拼 ANSI 转义** —— `t/message.t` 有断言钉住这一点，
  否则「非 TTY / NO_COLOR 自动降级」会从这里漏出一个口子。
- **退出码语义显式化并写进文档**：`0` 成功 / `1` 失败（业务失败与用法错共用）。
  历史上两者都是 1（`die` 的默认退出码），外部脚本已在依赖，**本轮刻意不拆成 2**；
  将来真要区分，只改 `EXIT-*` 与 `usage-die` / `fail-die` 两处 —— 这正是把它们从
  20 多处裸写法里显式化的意义。失败信息一律走 stderr，正常数据才走 stdout。
- **零行为变更（本轮是收口，不是改文案）**：用固定输入探针对重构前后比对，
  24 个探针 × 3 类产物（stdout / stderr / 退出码）= **72 个文件全部 IDENTICAL**。
- **变异验证**：破坏三条契约（退出码改成 2 / 给消息加前缀 / 改写 die 的文本），
  确认 `t/message.t` 每次都恰好在【预期的那条断言】上变红，再整份原文写回复原。
- 新增 `t/message.t`（8 断言）：退出码常量、die 类消息逐字原样、msg-* 只写 stderr
  且不加上前缀、输出不含 ANSI。

## 0.67.0 - 2026-09-21

- **CLI 命令元数据抽成 `RakuPM::CliSpec`**（重构：纯搬家，不改行为）：原先埋在
  `bin/raku-pm.raku` 里的 `%CMD_FLAGS` / `%CMD_ALIASES` / 公共旗标清单 /
  `allowed-flags` / `completion-script` 全部迁入新模块，bin 只保留解析 / 校验 / 派发。
  收益：新增一个旗标从「改三处」变成「改一处」，且这张表可以直接单测
  （新增 `t/cli-spec.t`），不必再起子进程跑 CLI。
- **补上原先散落的三类声明**：`writes-prefix`（会写前缀的命令，原 bin 的
  `%prefix-writer`）、`needs-network`（可能联网的命令，原 `%net-cmd`）、
  `command-group`（命令分组，新增；`t/cli-spec.t` 断言每个命令都有分组，防止新增
  命令时漏登）。
- **等价性验证（逐字节）**：用 `git archive` 导出重构前的代码树，对同一组固定输入
  （help 族 / 四种 shell 补全 / 各类校验报错，17 个探针 × 3 类产物 = 51 个文件）
  逐字节比对，**全部 IDENTICAL**。
- **变异验证**：故意破坏 `CliSpec` 三处（漏登命令分组 / 别名指向不存在的命令 /
  白名单漏掉公共旗标），确认 `t/cli-spec.t` 每次都恰好在【预期的那条断言】上变红，再复原。
- 坑：`canonical` 这个名字与 `RakuPM::Prefix` 已导出的同名 sub 冲突 —— bin 同时
  `use` 两者会直接编译失败（`Cannot import symbol '&canonical'`）→ 改名 `canonical-command`。

## 0.66.0 - 2026-09-21

- **self-upgrade 改为两阶段自举**：先把源码取到 git-cache 判断是否需要升级，再改用
  【取到的新版代码】执行安装（子进程跑 `<clone>/bin/raku-pm.raku` 并 `-I <clone>/lib`），
  旧版只负责「取源码 + 决定要不要升」。这样即便新版改了安装链行为（测试门禁、依赖解析、
  wrapper 写法…），也不会出现「新版自己能装自己、旧版装它必失败」的升不上去死循环。
  自举子进程凭父进程下发的锁凭证免锁放行（`RAKUPM_LOCK_TOKEN` + 锁旁 `.token`），
  因此必须透传 `--target`（写锁按 target 定位）。自举不可用时（老源码没有 `bin/`、
  或子进程失败）自动回退到原路径，不会把升级卡死。
- **新增 `self-upgrade --from-dir <目录>`**：直接用本地源码目录升级（不 clone、不联网）；
  它也是自举第二阶段的入口。
- **修 Windows 上 `--from <本地仓库路径>` 直接失败**：`git-cache-key` 过去只处理
  `git@` / `https` / `http` / `git` 四种协议，本地路径原样当作目录名 —— Windows 盘符里的
  冒号在文件名中非法，`git clone` 报 `Invalid argument`。现在本地路径统一落到 `local/`
  前缀下并去掉冒号。
- 新增 `xt/self-upgrade-bootstrap.t`：覆盖自举命令行拼装（尤其 `--target` 透传）、
  `--from-dir` 端到端、真自举端到端（本地 git 仓库 + 在新源码 bin 里埋标记，验证
  「执行安装的确实是新版代码」，并验证父进程持锁时子进程不会自锁）。

## 0.65.0 - 2026-09-21

- **终端展示层 `RakuPM::UI`（零依赖）**：新增色彩与表格渲染，`installed` / `list` / `info`
  三个命令改用表格 + 色彩输出。中文按【显示宽度】对齐（`uniprop('East_Asian_Width')`，
  宽 / 全角 = 2 列），表格不再因中文而歪列。
- **自动降级**：非 TTY（管道 / 重定向 / 测试捕获）或设了 `NO_COLOR` / `RAKUPM_NO_COLOR`
  时一律输出纯文本（无 ANSI 转义、边框退化为 ASCII）—— 测试断言不受影响；并新增
  `--no-color` 公共旗标，供在终端里临时关闭。
- **刻意不引入第三方依赖**：评估过 `Terminal::Table`（功能最强，但带
  ANSIcolor / WCWidth / Text::Tabs 依赖链）后仍选择自写 —— raku-pm 是包管理器，
  界面美化不该变成它自己的安装负担，也不能让门禁依赖「本机 site 恰好装了某几个包」。
  `RakuPM::UI` 只用 Raku 内置能力。
- 新增 `t/ui.t`（显示宽度 / 对齐 / 降级 / 富文本）；`t/query-installed-store.t`
  的断言随表格化更新。

## 0.64.6 - 2026-09-21

- **`rakulib-shadowing` 自检不再误报单仓库多版本**：`CliCheck::check-rakulib-shadowing`
  改为只在「跨仓库出现不同 RakuPM 版本」（链序遮蔽风险）时报警；同一 `site` 仓库内的
  CUR 多版本共存（Rakudo 永远按 semver 取最高，无害）现在静默。自装/升级后 site/ 残留
  旧版不再触发噪音提示。`t/rakulib-shadow.t` 新增单仓库多版本静默用例。

## 0.64.5 - 2026-09-21

- **uninstall 支持 semver 版本范围**：`--version` 的版本筛选从「精确匹配」升级为「semver 约束匹配」，
  除精确版本外，现支持区间（`<1.0`、`>=1.0, <2.0`）、通配（`*`）与 zef 风格 `0.2+`；并新增
  npm/cargo 风格的 `^` / `~` 糖语法（`^1.0` → `>=1.0.0, <2.0.0`、`~1.2` → `>=1.2.0, <1.3.0`），
  加进 `RakuPM::Version` 后依赖解析同样可用。承载点：`Installer.uninstall` 的 CUR/store 筛选与
  `Ledger.remove` 的版本过滤统一走 `RakuPM::Version.satisfies`；`Reporter.uninstall` 的反向依赖守卫
  改为「范围删光全部已装版本时」才拦截（单版本/部分范围跳过，与原整包语义一致）。向后兼容：
  精确版本串行为不变。

### 针对外部审计文档（HARDWARE_FIX_SUGGESTIONS，H1–H12）的修复总览

某份第三方审计文档基于 **v0.52.1**（commit `55a3f83`）扫描 raku-pm，列了 H1–H12 共 12
条「硬伤 / 缺陷 / 建议」。我们在 0.52.2 → 0.54.3 之间逐条对照**当前源码**核对，并修复了其中
5 条（H3 / H6 / H10 / H11 为代码级新增；H7 为现有行为已满足）。下表是总览，
方便读者一眼看清「针对那份文档改了哪些、哪些是有意不修」。

| 文档 ID | 主题 | 状态 | 落点版本 / 说明 |
|---------|------|------|----------------|
| **H3** | `clean` / `gc` 会回收被保留世代引用的版本 | ✅ 已修 | 0.52.2（`Generations.protected-versions` + `Cleaner` 来源 4） |
| **H6** | Windows 已知测试失败豁免写死 `Log::Async` | ✅ 已修 | 0.54.2（规则外置 `known-windows-test-failures.json`） |
| **H7** | `uninstall` 无反向依赖检查 | ✅ 已缓解 | 现有行为：`Reporter.uninstall` 已算 `reverse-dependencies` 并报告下游伤害（非本轮新增） |
| **H10** | `install` 期不主动读 `build.json` | ✅ 已修 | 0.54.3（装完读构建痕迹，提前告警） |
| **H11** | 无 `RAKULIB` 遮蔽自检 | ✅ 已修 | 0.54.1（`RakuPM::CliCheck` 冷启动告警） |
| H1 | CUR 预编译不含 core 链 | ⬜ 不修（语言级） | Rakudo 限制，需上游配合 |
| H2 | HTTP 超时悬挂线程杀不掉 | ⬜ 不修（语言级） | Raku 无线程取消原语；超时闸已加，仅无法回收线程 |
| H4 | native 一行两 heredoc 只跟踪第一个 | ⬜ 不修（刻意边界） | 宁可漏报不误报，误报会让安装自锁死 |
| H5 | 版本排序非数字 → 0 | ⬜ 不修（刻意边界） | `pick-best` 返回原始串，选择时不混为同版本 |
| H8 | 缺显式 `--pin` | ✅ 已修 | 0.58.0（`install`/`reinstall` 支持 `--pin=Mod@ver[,Mod2@ver2]`，解析器按精确匹配钉版，优先级高于依赖方约束） |
| H9 | 无真实上传到远端索引 | ✅ 已修 | 0.57.0（`publish --remote` multipart PUT 到 `42.zef.pm/upload`；`login --username/--password` 联网换 key） |
| H12 | `.ps1` 优先于 `.bat` | ⬜ 部分缓解 | cmd 行为改不动；`.bat` 仍生成、`.ps1` 优先 |

> 注：该文档基于 0.52.1 扫描，其「当前源码实锤」对 0.52.2 之后的改动已过期——
> H3（0.52.2 已堵）、H7（下游报告现有代码已有）在我们这轮之前就已解决，并非漏看。

## 0.64.4 - 2026-09-20
- 性能：测试默认改为【按 CPU 核数并行】（上限 8），不再为躲预编译竞态而永远串行。
  - 门禁 `tools/run-suite.raku`：`--jobs` 默认值从 1 改为 `min(CPU 核数, 8)`；并修复原并行分支的语义错误（旧逻辑把 `--jobs=N` 当成「批数」、实际并发 = 文件数/N）；新增「跑前预热 lib 预编译缓存（-c 写热）+ 跑后失败用例串行复验」两道安全网吸收并发竞态假失败。
  - 安装测试 `RakuPM::Client::Tester`：`--test-jobs=0`（auto）从「串行」改为「按 CPU 核数并行（上限 8）」；复用已有的预热 precomp + 串行复验兜底，故默认并行也稳定。
  - 帮助文案（bin + COMMANDS-WIKI）同步：明确「默认并行、设 1 则串行」。
  - 并发度读取收进 `RakuPM::Platform.cpu-cores`（新增导出函数）：消除 `Tester` 里直接读 `$*KERNEL` 对「平台判断只许在 Platform」铁律的违反（原 `t/platform-centralized.t` 会因 `$*KERNEL.cpu-cores` 直接出现而红）。
  - 并行默认改动引发的回归修复（同一轮）：`uninstall` 默认清 store 后，`rollback-transaction` 与 `rollback-to` 内部「摘 CUR」的卸载必须 `:keep-store`——store 是世代回滚的还原源，不能随之删掉，否则 `xt/generations.t` / `xt/rollback-restore.t` 回滚时无源可装；`xt/rollback-restore.t` / `xt/skip-installed.t` 的 setup 卸载同步加 `:keep-store`（它们专门验证「store 还在时能恢复/重新入库」）。

## 0.64.3 - 2026-09-20
- 修复 `self-upgrade --force` 在原生 Windows 上因 `known-test-failure.t` 失败而回滚的事故：
  - 根因：`is-known-windows-test-failure` 的规则只从 `lib/RakuPM/Client/known-windows-test-failures.json` 加载，而该 `.json` 不在 `META6` 的 `provides`（只列 `.rakumod`），安装到 `site/` 时不会随包拷过去；`self-upgrade` 跑测试时 `Tester` 可能从已安装的 `site/` 加载，`$?FILE` 兄弟文件缺失 → 规则集被掏空 → Windows 上本应软通过的已知失败误返 `False` → 安装被无辜回滚。
  - 修复：在 `Tester` 内嵌一份【内置兜底规则】（Log::Async 12-context），文件缺失/解析失败时不再退化为「无规则」，文件存在时再叠加社区规则。新增回归测试 `t/known-failure-nojson.t`（强制 JSON 缺失 + 模拟 win32）锁死该路径。
- `uninstall` 默认把被卸版本的 **`store` 源码快照一并清掉**（CUR + 账本 + store 三处一致），普通用户「卸了就该没了」，不用卸完再跑 `clean`。新增 `--keep-store` 旗标保留快照（供秒级离线重装；`reinstall` 自动带此旗标）。`Installer.uninstall` / `Reporter.uninstall` / `repos uninstall` 分发同步：`keep-store => $keep-store`。新增回归测试 `t/uninstall-store.t` 锁死三态（整包清 / `--keep-store` 保留 / `--version` 只清那一版）。撤销了「新增 `remove`/`trim`/`prune` 命令做版本修剪」的提案——`uninstall Foo --version=X` 已能「保留包、只甩某旧版」，无需独立命令（`remove` 现只指 `repos remove`）。

## 0.64.2 - 2026-09-20
- 作者侧 `check --remote`（联网查同版本是否已发布）真实端到端验证 + 回归补全：
  - **真实端到端跑通**：对真实生态索引（默认 zef + cpan + rea；验证时约束到 zef）
    实测三种场景均正确——已发布（自测探针包 `RakuPMVerifyTmp:ver<0.1.0>` 与第三方
    `JSON::Fast:ver<0.21.0>`）返回 `ok=False` 并提示「版本已发布…无法重传」；
    未发布（随机名）返回 `ok=True` 放行。顺带证实 0.64.1 的 `publish --remote`
    确实落进了 zef 索引（publish→索引→check 链路闭合）。
  - **回归**：`t/author-check.t` 补「联网未能确认（缝=Nil）→ 警告但不阻断」分支
    （plan 15→17），至此 `check --remote` 的「已发布 / 未发布 / 未能确认」三态分支
    全部离线锁死（真网络路径仍靠手动验证，因依赖外网、不宜进门禁）。
  - 纯测试/验证增强，**无源码行为变更**（patch）。
- 测试加固（同一批门禁修复，仍 patch）：
  - `t/known-test-failure.t`：判据从 `$*DISTRO.is-win` 改为 `is-windows()`。前者在
    `$*DISTRO.name='win32'` 的 Windows 上返回 **False**（Rakudo 只认 `mswin32`），而识别函数
    内部用项目统一的 `is-windows()`（兼容 `win32`）返回 True → 两端判据不一致致测试在
    `win32` 命名 Windows 上误红，进而让 `self-upgrade` 在安装期跑 `t/` 时回滚。
  - `xt/tester-async.t`：场景 1 把「并发证明」从脆弱的墙钟耗时阈值改为**进程起止标记重叠证明**
    （每个测试起止各写标记文件，最晚启动 ≤ 最早结束 ⇒ 真并发；与计时无关，满负载不虚高）；
    另修正 `(now).posix` 误用（`Instant` 无此方法 → 生成的测试体编译失败），改 `(now).Num`，
    并去掉并发下框架日志 flush 的 benign 竞态断言。场景 3 超时返回阈值放宽到 15s。

## 0.64.1 - 2026-09-20
- 作者侧真实端到端验证抓出的 wire 契约 bug 修复（patch）：
  - `login --username/--password` 此前用 multipart/form-data 打 42.zef.pm/login，
    真实服务端 from-body 只认 application/json，导致服务端 500（「Odd number of
    elements found where hash initializer expected」）。新增 `RakuPM::HTTP.post-json`
    （发 JSON 体 + Authorization 头），`Author.login-remote` 改用它。
  - 真实登录 / 发布已对 42.zef.pm 跑通：login 用裸用户名（如 `skyter10086`，不含
    `zef:`/`gitee:` 前缀——前缀是包 identity 的 auth 命名空间，不是登录 handle）成功换得
    真实 api-key；publish --remote 用该 key 真实上传返回 `{"success": true}`。
  - `RakuPM::HTTP.post-form` / `post-json` 的状态码解析顺带加固：容忍 Windows CRLF
    （子进程 stdout 的 \r），改用「提取数字串」而非尾随锚定正则。
- 回归：t/http-post-json.t（新增，验证 post-json 发 JSON 体、非 multipart、带 auth 头）；
  t/author-upload.t 场景 B 断言改为 JSON 体；t/mock-curl.raku 增加 -d JSON 体捕获。

## 0.64.0 - 2026-09-20
- A2：upgrade 不带包名默认升级全部前，新增一行醒目提示（非阻塞、不挡 CI；
  --dry 试算时不提示），引导用户显式指定包名或改用 install --locked 做可复现升级。
- A5：移除 %net-cmd 中 refresh/check/publish 三个死配置（作者侧命令在更早的
  `if $cmd eq` 分支已 exit 0，永不到达离线软提示块），离线 UX 逻辑收敛到单一来源。
- A7：help 别名速查表补列 rdeps（= rdepends）/ gens（= generations），与实际可用拼写一致。
- 回归：t/upgrade-mass-warning.t（A2）、t/p1-readonly-cmds.t 增 A7 断言。
  （A6 已在 0.62.0 随别名补全一并修复，本次无改动。）

## 0.63.0 - 2026-09-20

UX 别扭点修复（A1 / A3 / A8 / A9，minor：提示增强 + 一处启发式误判修复）

- **A3：install 顶层参数的 git URL 探测误判本地路径**。旧逻辑：任何以 `.git`
  结尾的字符串都被当成 git URL 去 `git clone`，于是本地目录 `./my.repo.git`
  会被误判、clone 失败。`bin` 的分流判断从 `$spec ~~ /'.git' $/` 改为
  `($spec ~~ /'.git' $/ && !$spec.IO.e)`——磁盘上确实存在的本地路径优先当
  本地目录装（`install-from-dir`），不再误走 git。`t/install-git-url-heuristic.t`
  端到端钉死（构造本地 `dist.git` 目录，断言不被拿去 clone、走 install-from-dir）。
- **A1：锁文件位置透明化 + 修正过时注释**。锁默认写在【当前目录】`raku-pm.lock`
  （项目级、可提交，Cargo.lock 同款语义），包装在 `<target>`（全局前缀）——两者
  **有意分离**（锁记录「项目的依赖闭包」，包全局共享）。`Client.rakumod` 里那条
  「锁文件放 target」的注释是过时的（bin 总是显式传 `:lock-path` 覆盖），已更正。
  `doctor` 新增〔6/7〕锁文件小节，明确揭示锁的绝对路径、与包分离的意图、
  以及「upgrade 不读锁」的提示，消除「取决于你在哪」的困惑。
- **A8：upgrade help 强调可复现**。升级提示加一句：upgrade 取【最新版】、不读锁文件
  （`--locked` 才会钉死）；要可复现请用 `install --locked`。`t/p1-readonly-cmds.t`
  钉死 help 含该提示。
- **A9：doctor 新增〔7/7〕作者侧凭据小节**。检查 `<target>/credentials.json`
  是否存在，提示 `publish --remote` / `check --remote` 需先 `raku-pm login`，
  仅本地发布不受影响。
- 文档纪律：META6 / README.md / README.en.md 的 `RakuPM:ver<>` 与 Changes 顶部
  同步到 0.63.0。

## 0.62.0 - 2026-09-20

命令简写别名（minor：新增批量短别名能力）

- 新增 `%CMD_ALIASES` 单一事实来源，覆盖几乎所有命令的双/三字母简写（如 `i`→install、
  `un`/`rm`→uninstall、`up`→upgrade、`ri`→reinstall、`ls`→list、`li`→installed、
  `inf`→info、`dep`→depends、`rdep`→rdepends、`s`→search、`doc`→doctor、
  `out`→outdated、`gen`→generations、`rb`→rollback、`ar`→autoremove 等 30+ 条）。
- 派发时一次性把别名归一化成规范命令名，`help` / `version` / 旗标校验 / `given` 派发
  全部只看规范名，别名不会让任何分支重复或漂移。
- 补全脚本（`completions`）同步输出别名，`help` 文本加一行别名说明。
- 回归测试：`t/p1-readonly-cmds.t` 新增端到端别名断言（`e`→`env`、`doc`→`doctor`、
  无参 `un` 走 uninstall 报错分支、bash 补全清单含 `i`/`rm`）。

## 0.61.1 - 2026-09-20

两个一致性 bug 修复（patch）

- **B1：`update` 命令漏进 `%CMD_FLAGS` 单一事实来源**。`raku-pm update` 已实现、
  help 也写了，但 `%CMD_FLAGS` 里没有 `'update'` 键，导致 `completions` 生成的补全脚本
  漏了 `update`（以及它的别名 `repos update`/`gens`/`rdeps` 也取不到），且以后给 `update`
  加旗标会被「未知选项」静默拒掉。补上后补全脚本与命令清单自动同步。
- **B2：`reinstall Foo:ver<1.0>` 静默丢掉 `:ver<>` 约束**。原实现「已装」分支只用
  `--version` 旗标筛版本，完全忽略规格串里的 `:ver<>`，于是 `reinstall Foo:ver<1.0>` 会把
  Foo 的**所有已装版本**全重装一遍，与 `install`/`upgrade` 都认规格串约束的行为不一致。
  现改为与 `version`/`installed` 同义：用 `RakuPM::Version.satisfies` 解释约束
  （`'1.0'` 精确、`'1.0+'` 范围）。`--version` 仍优先。
- 回归测试：`t/reinstall.t` 增 B2 用例（同包双版本，断言只重装匹配版）；
  `t/p1-readonly-cmds.t` 增 B1 用例（bash 补全必须含 `update`）。

## 0.61.0 - 2026-09-20

self-upgrade 覆盖安装语义（minor：self-upgrade 新增「自动清理旧版本」能力）

- **`self-upgrade` 升级完自动清掉 RakuPM 的旧版本**（CUR / store / 账本三处），
  等价于「覆盖安装」——升级后无需再手动 `uninstall` 旧版本。
  - 根因：`install()` 只摘「同版本」、`!record` 把每个版本都累加进账本 `versions[]`，
    于是升级后旧版本仍残留在 CUR / store / 账本里（wrapper 永远跑 store 里最新的那份，
    故功能上新版在跑，但旧版占着空间、还出现在 `list` / `outdated` 里）。
  - 实现：新增 `Installer.prune-other-versions($name, $keep)`，摘掉「除 keep 外的所有版本」；
    `self-upgrade` 的两条路径（git 远端 / 本地仓库）安装成功后调用，只保留刚装的这一版。
  - **刻意只用于 RakuPM 自身**：普通用户包仍然支持多版本共存，绝不调用本方法。
  - `--dry` 时只报告、不实际删除（与 install 的 --dry 一致）。

## 0.60.2 - 2026-09-18

新增三个只读命令（均不进写锁、不改账本、不改 CUR；patch：纯能力扩展，对外事务化核心不变）

- **`outdated`**：列出 raku-pm 管理的、生态里有更高版本的已装包（`当前 → 最新`），
  并标注「源码安装 / 生态里找不到」的跳过项。刻意**绕过** `reconcile-installed`
  直接读账本，避免只读命令顺手改写账本。
- **`doctor`**：综合体检 5 小节——RAKULIB 指向、PATH 中 raku-pm 是否被遮蔽、
  `target/stage` 残留暂存、账本↔CUR 一致性（`consistency-issues`）、运行环境
  （Rakudo/VM/平台），最后给总评。契合「教学型」定位，排障友好。
- **`completions`**：生成 bash / zsh / fish / PowerShell 的补全脚本并打到 stdout
  （`raku-pm completions <shell>`，或 `--shell=<shell>`），可直接 `eval` 或落地到
  shell 的 completion 目录。

**命令清单 / 旗标单一事实来源重构**：把原来散落在 `allowed-flags` 内的局部
`%per` 提升为文件级 `my %CMD_FLAGS`（命令→旗标白名单的唯一来源）；`allowed-flags`
改为从它读取；新增命令/选项只需改 `%CMD_FLAGS` 一处，`completions` 自动同步。
顺手修掉 bash 补全公共旗标重复追加的 bug。

## 0.60.1 - 2026-09-20

收敛旧的每包独立暂存进整树事务化路径（patch：内部清理，对外行为不变）

- **删除 per-package 暂存方法** `Installer.stage` / `cleanup-stage` / `purge-cur-entry`
  （各自在 `target/stage/<name>-<ver>` 建一个独立 CUR）。它们的职责已被
  `install-pending`（写共享整树暂存 CUR `target/stage/pending`）+ `pending-cur-spec`
  + `raku-lib-spec`/`pending-lib-spec`（注入子进程 `-I`）完整覆盖；清理由
  `cleanup-stage-all`（成功晋升后）与 `discard-pending`（失败路径）统一负责。
- 同版本重装的「去旧」逻辑改由 `install-pending` 内部对暂存 CUR 做 `uninstall`、
  以及 `install()` 对真实 CUR 做 `uninstall` 承担——`purge-cur-entry`（针对真实
  site 的整份摘除）已无调用方。
- 同步更新 `lib/RakuPM/Fs.rakumod` 与 `CODEBUDDY.md` 里对 `Installer.stage` 的引用。

## 0.60.0 - 2026-09-18

彻底事务化安装 + 修复整树暂存 CUR 未被注册为包含路径（minor：改变安装期间的落盘时序，对外行为等价）

- **整树事务化安装落地**：安装链期间所有包只装进共享的「整树暂存 CUR」（`target/stage/pending`），
  测试也在那里跑；整条链全部通过后才由 `promote-pending` 一次性原子晋升到真实 CUR + 账本 + bin。
  失败 / 进程中断时真实 CUR 与 `installed.json` 自始未被触碰，只需丢弃暂存（`discard-pending`），
  无需逐个回滚——原子「全有或全无」语义真正落地（与 0.59.0 的逐包即时激活对比）。
- **修复 P0：`cleanup-stage-all` 从未定义**：0.59.0 的失败路径会因「找不到方法」先崩溃、且从不回滚。
  0.60.0 补齐 `cleanup-stage-all` 等整树暂存方法（`pending-cur-spec` / `install-pending` /
  `promote-pending` / `discard-pending` / `!reset-pending`），保证暂存目录不论成败都被清干净。
- **修复构建/测试子进程找不到暂存依赖**：原先把暂存 CUR 与真实 CUR 用空格拼成一个串塞进
  `raku-lib-spec`，但 `Builder` 与 `Tester` 把它当【单条】`-I` 传入，Rakudo 不会拆分空格串，
  导致暂存 CUR 从未生效——构建期 `use` 不到刚暂存的 `build-depends`（`t/build.t` 必现回归）。
  现改为 `raku-lib-spec` 恒返回真实 CUR，新增独立的 `pending-lib-spec`，由 Builder / Tester
  注入为【独立 `-I`】（排在真实 site 之前）。`t/build.t` 由此恢复全绿。

## 0.59.0 - 2026-09-18

安装健壮性与缓存回收（minor：改变清理/回收语义，无破坏性接口变更）

- **多版本清理根因修复**：账本 `installed.json` 的 `versions[]` 不再累加「安装历史」，
  改为只镜像 CUR 里该发行版的当前激活集。此前每次 `self-upgrade` 都 append 一条版本，
  导致 `clean` 的保护集合锁死、`store` 里 30 个旧版本永远回收不掉——现在只要旧版本
  不在任何保留世代里就能被 `clean` 正常回收。
- **CUR 修枝**：`install` 激活新版后，顺手把 CUR（site/）里该发行版的其它版本摘掉，
  避免升级后旧版本残留在 site/（多版本膨胀在 CUR 一侧的表现）。
- **安装失败必清 Mock**：测试失败/进程中断时，安装链在失败路径调用新增的
  `cleanup-stage-all`，确保 `target/stage/` 暂存 CUR 不论成败都被清干净
  （此前只有成功路径才清，失败的残留会累积）。
- 保留 `rollback-to` + `generations` + `clean`；`rollback-transaction` 的失败回滚仍保留
  （依赖在构建/测试期就会被写进 site，链中段失败时它负责把现场还原）。

## 0.58.0 - 2026-09-17

H8 收尾 + 凭据管理闭环（minor：新增 `--pin` / `logout` 两个能力）

- **`--pin` 依赖钉版**（H8 正式修复）：`install` / `reinstall` 支持
  `--pin=Mod@ver[,Mod2@ver2]`，把指定模块（含传递依赖）精确钉到某个版本。
  解析器在 `RakuPM::Resolver` 里新增 `%!pins`，`!resolve-one` 命中钉版时用
  精确约束 `= ver` 覆盖任何来路约束（依赖方提出的、CLI 位置参数给的），优先级最高；
  若钉版与某依赖约束无法同时满足，冲突报错仍会把钉版列进请求链，便于定位。
  钉版沿 `install → !resolve → Resolver` 以及 git/本地目录路径的 `resolve-deps-of`、
  构建/测试相依赖、`!ensure-git-deps` 全链路透传（`%pins` 命名参数）。
- **`logout` 命令**：与 `login` 配对，删除本地凭据文件
  `~/.raku-pm/credentials.json`（或 `RAKUPM_TARGET` 下的）。凭据文件本来不存在时
  只提示「没有已登录的会话」、不报错。返回结构 `(ok, existed, deleted, path, error?)`。
- 文档：两份 README 的 `RakuPM:ver<>` 示例同步到 0.58.0；Changes 总览表 H8 由
  「已用 --only 缓解」改为「✅ 已修」。

## 0.57.0 - 2026-09-17

真实生态上传（**H9 修复的收尾里程碑 M3 → minor**）

把 M2 已构造好的上传请求真正发出去，并把账号登录换 key 也补上，H9 至此闭合：

- 新增 `RakuPM::HTTP.upload-multipart` / `post-form`：raku-pm 唯一的上行路径。
  用系统 curl 的 `curl -F "dist=@<tar>" -H "Authorization: Zef <key>" <url>` 形态
  （与 fez/zef 服务端 `42.zef.pm/upload` 期望完全一致；HTTP::Tinyish 不原生支持
  multipart 文件上传，故上行走 curl，下载仍走 HTTP::Tinyish 后端）。run 后
  `slurp(:close)` 关掉两条管道（防 Windows 句柄泄漏），用 `-o`/`-w '%{http_code}'`
  精确拿 HTTP 状态与响应体。首行 `assert-online` 离线硬拦。
- `login --username=<u> --password=<p>`：真实联网 `POST 42.zef.pm/login` 换 api-key
  并存入 `~/.raku-pm/credentials.json`（密码不落盘，与 fez 一致）；响应里健壮抽取
  key（key / api-key / api_key / token，或裸串）。`--api-key` 仍是直接导入已有 key。
- `publish --remote`（非 `--dry`）：先校验 api-key（环境变量 `RAKUPM_API_KEY` >
  凭据文件），再 `upload-multipart` 真实上传；成功打印确认，失败给出含「同版本已
  存在 / auth 不匹配」提示的友好报错，由服务端强制校验。`--dry` 仍只预览不发网。
- 版本纪律：META6 / 两份 README 示例 / Changes 顶部一致到 0.57.0；README 生产侧
  「发布到生态」由 🟡 改为 ✅；H1–H12 总览表 H9 行由「不修」改为「已修」。
- `post-form` 韧性加固：子进程（curl）的 stdout / stderr 按二进制读回，先试 utf-8
  解码、失败退回 latin-1。修复 Windows 上子进程 stderr 可能是系统代码页（GBK）文本、
  严格 utf-8 解码直接抛「Malformed UTF-8」炸掉整个上传链路的真实缺陷（见 `t/author-upload.t`
  的 mock curl 测试：本环境 `IO::Socket::INET :listen` 抛 "Invalid port (Int)"、
  `IO::Socket::Async` 第 3 连接数据 Supply 不触发，本地 mock 服务器起不来，故用替换
  curl 二进制的方式离线验证 wire 契约）。

### 测试夹具 `t/mock-curl.raku` 的最终收口（本环境 Rakudo/Windows 专属坑）

上传路径带 `-H "Authorization: Zef <key>"`（含空格）与 `-F "dist=@<path>"`，本环境
rakudo 的 `run()` 会把 `.bat` 参数包进 `cmd /c` 并**错引含空格的参数** → `.bat` 起不来 →
mock 根本没跑 → `post-form` 收到 exit 1（Windows `ERROR_INVALID_NAME`）。login 那条
只传 `-F username=… -F password=…`（无 `-H`、无 `@` 字段）所以能过，正好佐证是
空格参数引述问题。最终收口方式（向后兼容）：`post-form` 检测 `RAKUPM_CURL` 以
`.raku` / `.rakumod` 结尾时，直接用 `$*EXECUTABLE`（真实 `.exe`，不经 `cmd /c`）
跑 `$*EXECUTABLE <mock>.raku …`；其它 curl 仍按原样直接调用。mock 自身三处加固：
① 日志目录保持 `IO::Path`（本环境 `IO::Path.absolute` 返回 **Str**，在其上 `.add`
会抛 "No such method 'add' for string"、让 mock 在启动期就崩）；② 文件内容按二进制
读回（`slurp(:bin)` + `try .decode('utf-8') // .decode('latin-1')`）—— sdist 是真实
二进制 tar.gz，默认 UTF-8 `slurp` 会抛 "Malformed UTF-8 near byte …"；③ 用 `.raku`
取代已弃用的 `.perl`，避免弃用告警污染 stderr 被 `post-form` 当成错误。

## 0.56.0 - 2026-09-17

作者侧凭据管理 + 真实上传请求构造（**H9 修复的第一里程碑 M2 → minor**）

基于 M1 对 fez/zef 生态上传协议的调研（`https://42.zef.pm` 是规范上传端点，
认证头 `Authorization: Zef <api-key>`，multipart 字段 `dist`），把「生产侧发布到
真实生态」这条链路从「完全空缺」推进到「凭据 + 请求结构本地可验证」：

- 新增 `raku-pm login [--api-key=<key>] [--username=<u> --password=<p>]`：
  把生态 api-key 存到 `~/.raku-pm/credentials.json`（权限收紧到仅属主可读，类 ssh key）；
  `--api-key` 直接导入一把已有 key（与 fez 共用同一把），`--username/--password`
  实时换 key 的登录在 M3 实现（仅记 username、明确告知延迟）。
- `publish --remote` 重写：先校验 api-key（环境变量 `RAKUPM_API_KEY` > 凭据文件），
  构造真实上传请求（`POST <host>/upload`、`Authorization: Zef <key>`、字段 `dist`）；
  `--dry` 只打印请求、不发网络、不本地部署；**非 dry 的真实 multipart PUT 在 M3
  实现**（RakuPM::Net 目前只覆盖 GET 类请求，没有上传能力），M2 明确报错延后、不静默发网。
- `publish --remote` 不再要求配置本地目录仓库（`--to`/`RAKUPM_PUBLISH_REPO`）——
  真实上传路径在本地部署之前短路。
- 凭据读写全部走 `RakuPM::Platform.is-windows` 单一平台入口（chmod 仅非 Windows 执行），
  不破坏平台集中化门禁。
- 新增 `t/author-remote.t`（login 凭据读写 + publish --remote dry 预览 + 缺凭据/非 dry 报错）。

## 0.55.0 - 2026-09-17

新增 `shell` 命令：只打印可 eval 的环境赋值，把 raku-pm 环境灌进当前交互式 shell
（**缺口补齐：以前只有 `env` 打印、`look --shell` 看源码，没有真正注入环境的命令**）

- `raku-pm shell` → 打印 `export RAKULIB=…` / `export PATH=…`（bash/zsh/sh/Git Bash 默认）
- 自动识别 shell 类型，也支持 `--shell=fish|pwsh|cmd` 强制格式
- 与 `env`（带说明、给人看）不同，`shell` 只输出赋值、无注释，可直接 `eval` 安全生效
- `env` 输出里加了一句提示：`eval "$(raku-pm shell)"`

## 0.54.3 - 2026-09-17

install 期主动读 build.json 提示构建问题（**硬伤/H10 修复 → patch**）

`Installer.install` 装完会把 store 的 build.json 旁车读一遍，遇到「构建没跑成功 /
被 `--no-build` 跳过 / 声明了 resources 却没生成」这类【事后才暴露、运行期才崩】
的问题，立即在 stderr 打 `⚠ 构建痕迹：…` 提示（纯警告、不中断安装）。
原本这类只在 `raku-pm verify` 才查得到，现在安装时就前置暴露。
抽取 `!build-issues-for($name,$v)` 供 `consistency-issues` 与 `install` 共用，
行为不变、单一事实来源。

## 0.54.2 - 2026-09-17

Windows 已知测试失败豁免名单外置为 JSON（**硬伤/H6 修复 → patch**）

`Tester.is-known-windows-test-failure` 里写死的 `Log::Async` 专属逻辑改为查表：
规则数据放在 `lib/RakuPM/Client/known-windows-test-failures.json`（JSON 数组），
社区无需改代码即可贡献新规则。每条规则字段：`name`（包名精确匹配）、
`test-starts-with` / `test-contains`（测试文件过滤，可选）、`contains`（失败输出
子串列表，任一命中即匹配）。判定仍够窄（具体包+测试文件+失败特征+Windows），
真 bug 不会被掩盖；JSON 缺失/解析失败退化为「无已知规则」，不写死任何包名。
可用 `RAKUPM_KNOWN_FAILURES_JSON` 覆盖数据文件路径（测试/高级用户用）。

## 0.54.1 - 2026-09-17

CLI 冷启动自检 RAKULIB 遮蔽（**硬伤/H11 修复 → patch**）

加载链（`$*REPO.repo-chain`）上若出现【多个不同版本】的 RakuPM，raku-pm 启动时
会在 stderr 打印警告，提示用户「实际加载的未必是以为的那份」—— 典型坑：全局
RAKULIB / 多个 site（cur/home/vendor）指向旧版 raku-pm，导致跑着旧版却以为新版。

- 新增零依赖模块 `RakuPM::CliCheck`（承载 `check-rakulib-shadowing`）；
- 只在「响应 `.installed` 的安装型仓库」上查，开发期 `use lib` 前置的 FileSystem
  仓库自动排除，**开发期不误报**；单一版本（哪怕散落多仓库）静默，只有多版本才打扰；
- 纯警告、零代价，置于 MAIN 入口、`require RakuPM::Client` 之前，不拖 Client 模块图。

## 0.54.0 - 2026-09-16

可提交锁文件（**新能力 → minor**）：把锁文件从「全局前缀内部」挪到「项目可提交」。

之前 `raku-pm.lock` 写在 `target/raku-pm.lock`（默认全局前缀 `~/.raku-pm`），是整机
共享的一份、没法按项目提交、也不能复现单个项目的依赖闭包。0.54.0 起：

- 默认位置改为**当前工作目录**的 `raku-pm.lock`（仿 Cargo.lock），可随项目提交，
  实现可复现安装：`git add -f raku-pm.lock` → 队友 `raku-pm install --locked` 装出
  完全一致的版本（`--locked` 下锁缺失 / 与解析结果不符直接报错）。
- 新增 `--lock-file=<路径>` 旗标（install / reinstall / self-upgrade / lock 都认），
  可把锁改到任意位置（CI 常用）。
- 锁描述的是「项目的依赖闭包」，与安装位置（`--target`）无关：即使包装进独立前缀
  `.raku-env`，锁仍写在项目根，提交的是锁不是前缀。
- 库层 `RakuPM::Lock` / `Client` 不变，仍是 `$.lock-path` 注入；CLI 负责推导默认路径
  （无 `--lock-file` 时取 `$*CWD/raku-pm.lock`）。测试直接构造 `Client` 仍走 target
  内锁，不污染仓库根。
- 默认 `.gitignore` 忽略 `raku-pm.lock`（避免开发残留），要提交可复现锁用 `git add -f`
  或项目内 `!raku-pm.lock` 取消忽略。
- 文档：`README.md` / `README.en.md` 各加「可提交锁文件」小节，help 文本补
  `--lock-file` 与锁文件说明。

## 0.53.0 - 2026-09-16

新增 `reinstall` 与 `update` 两条命令（**新能力 → minor**）。

- `reinstall <模块名>`：把当前已装版本卸掉再按**完全相同版本**装回（对应 zef
  `reinstall`）。复用 `uninstall` 的反向依赖保护（被依赖时同样会拒绝，除非
  `--force`/`--recursive`），`install` 传 `:version` 精确钉回避免被解析成升级；
  完全没装过退化为一次普通 `install`；`--dry` 只预览（uninstall 无 --dry，故 dry
  时跳过卸载只调 install :dry）。放在 `RakuPM::Client.reinstall`。
- `update [仓库名|URL]`：顶层别名，等价于 `repos update`，刷新仓库索引缓存
  （不加名 = 全部）。进入 `%net-cmd` 离线提示集。注意它与 `upgrade`（升级已装包）
  语义不同。
- 同步：META6 / README / README.en / Changes 版本升 0.52.2 → 0.53.0。

## 0.52.2 - 2026-09-16

修复 `clean` / `gc` 会回收「被保留世代引用的版本」的真实缺口（AI 审查提的 H3）。**修 bug → patch。**

- `RakuPM::Installer::Generations` 新增 `protected-versions`：扫所有保留世代的 `manifest.json`，
  返回其中出现过的 `name\0version` 集合（空目录 / 解析失败都安全返回空）。
- `RakuPM::Cleaner.protected-versions` 新增来源 4：合并世代 manifest 引用的版本。
  `rollback-to` 把账本恢复到某世代的 `manifest` 后，缺失版本从 `store` 装回；若被 clean
  回收就装不回了（只能留空世代 + 警告）。现在这些版本纳入受保护集，clean 不会打断回滚。
- `t/clean.t` 增加隔离回归块（6 断言）：账本/锁/site 都不认的版本，只要被某世代 manifest
  引用就受保护、不进回收列表、试算不落盘。

## 0.52.1 - 2026-09-16

修复 `info` / `browse` 拿不到许可证与主页地址的真·生态联调问题（对 360.zef.pm 实测暴露）。**修 bug → patch。**

- `RakuPM::Repository::Ecosystem` / `RakuPM::Repository::Local` 新增 `raw-row($name, $version = '')`：
  返回完整索引行 / META6（含 `source-url` / `license` / `support`），不再被 `search-rows`
  的精简显示裁掉。找不到返回安全空 Hash（非 Nil）。
- `RakuPM::Client::Reporter.!raw-row-for` 改用 `raw-row` 取行（老仓库类型无 `raw-row` 时
  退回 `search-rows` 精简行兜底）。
- 实测：`raku-pm info JSON::Fast` 现在显示「许可证：Artistic-2.0」「项目主页：git://github.com/timo/json_fast.git」；
  `browse JSON::Fast` 打印并在默认浏览器打开该地址；`smoke JSON::Fast` 拉源码跑完 14 个测试文件全过。
- 新增 `t/query-commands.t` 中 MetaPkg 用例，离线锁定「info 把 license / source-url / support 打出来」。

## 0.52.0 - 2026-09-16

补齐与 zef 对齐的元信息查询四条命令（`info` / `browse` / `locate` / `smoke`）。**新能力 → minor。**

### 发行版元信息查询（对齐 zef）
- `info <包>` —— 显示发行版详情：身份（name:ver:auth）、描述、作者、提供的模块、
  运行时/构建/测试依赖、本地库依赖、外部命令依赖、git 依赖、资源文件，以及
  生态索引里的源码地址 / 许可证 / 项目主页。仓库里查不到的本地包（如 raku-pm 自身）
  退回到已装账本也能看到。
- `browse <包>` —— 在默认浏览器打开主页或源码地址（地址优先级：META6 的
  `support.source` > 索引行的 `source-url`；都没有则明确说无法打开）。平台分支集中在
  新增的 `RakuPM::Platform.open-url`（Windows `start` / macOS `open` / Linux `xdg-open`）。
- `locate <模块>` —— 走仓库链解析模块，打印实际加载的那份 `.rakumod`/`.pm6` 文件路径；
  多份共存时指出被遮蔽的那些（与 `which` 同源口径）。
- `smoke [包…]` —— 取源码并跑测试（复用 `test-dir`），统计通过/失败/跳过；
  不给规格则测全部已装（best-effort，离线或取不到源码的会跳过，可能很慢）。

### 其它
- `RakuPM::Platform` 新增 `open-url`（集中浏览器打开的平台分支，可被测试遮蔽）。
- 版本纪律：META6 0.51.0→0.52.0；两份 README 示例同步；此条目已加（门禁 --docs-only 全过）。

### 离线模式（`--offline` / `RAKUPM_OFFLINE=1`）
- 唯一判定处 `RakuPM::Net`：三处网络出口（HTTP 门面 / 生态索引 / git 子进程）都读它，
  于是「这一轮能不能联网」是结构性保证，不是靠每个调用点自觉。
- 索引无缓存、git 无副本时**明确报错**，绝不静默改成联网。
- 用途：网络受限环境、CI 要确定性、离线复现。

### 查询 / 单阶段命令
- `depends <模块>` —— 显示完整正向依赖树（已装 / 待装 / 找不到一眼看清，含环检测）。
- `test [路径]` —— 只跑测试、不安装（复用 Tester，默认当前目录）。
- `build [路径]` —— 只跑构建、不安装（默认当前目录）。
- `look <模块> [--shell]` —— 取源码进 `cache/look/` 目录看；默认只打印路径，
  `--shell` 才进交互 shell。

### 生产侧：`raku-pm publish`（本地目录仓库的发布）
- 把 `dist` 打出的 sdist **解包部署**到本地目录仓库（`--to=<repo-dir>` 或
  `RAKUPM_PUBLISH_REPO`），使其立即可被 `raku-pm install` 解析。
- 复用 / 重打：`--from=<tar.gz>` 吃现成包；默认复用 `dist` 现打（先跑 check 门禁）。
- 碰撞检查：`<repo>/<name>-<version>/` 已存在则拦（先 `bump` 或 `--force`）；
  `--remote` 额外联网查远端生态索引同名同版（离线硬拦）。
- 作者侧工作流至此闭合：`new → refresh → check → dist → publish`。
- 真实生态的上传（`login`）仍待做。

### 其它
- 新增模块 `RakuPM::Net`（零依赖，冷启动无负担）。
- 作者侧命令（`new`/`refresh`/`check`/`dist`/`bump`/`publish`）在构造 `Client` 之前
  分发，不进写锁、不碰安装目标。

## 0.50.0 - 2026-09-16

新增命令 **`raku-pm flush`**：一键清空当前前缀（含 raku-pm 自己的入口），回到
「可以重新安装」的状态。**新能力 → minor。**

### 为什么单开一个命令，而不是加强 `clean`

三个命令的意图不同，`clean` 的每条规则都是「什么不能删」：

| 命令 | 意图 |
|------|------|
| `clean` / `gc` | **有保护**地回收（只动没被引用的版本，默认还留着索引与克隆缓存） |
| `self-remove` | 只卸 raku-pm 自己 |
| `flush` | **无保护**地全清，只为从零重来 |

想要「从零重来」，原先得 `self-remove` → `clean --all --yes` → 再手工收拾，还清不干净
（世代目录、日志、账本、前缀外的入口副本都留着）。合成一个命令迟早会有人把 `flush`
当成 `clean` 的加强版顺手敲出去，所以刻意分开。

### 做了什么

- 新增 role `RakuPM::Client::Flusher` + 命令 `flush [--yes|--force]`。
  **默认只试算**（删文件不可撤销，与 `clean` 同一约定）。
- 清：前缀内的 `store/` `site/` `git-cache/` `cache/` `log/` `generations/` `stage/`
  `bin/` `installed.json` `raku-pm.lock`，以及**前缀外**那几份自装入口
  （全局 `site/bin` 下的 `raku-pm` / `.bat` / `.ps1`）。
- **两道护栏**（全删命令的护栏比功能重要）：
  1. 目录必须**看起来就是一份 raku-pm 前缀**（`store/`、`site/`、`installed.json`
     至少一个），且不是 `$*HOME` 本身、不是文件系统根 —— 否则直接拒绝。
     `--target` / `RAKUPM_TARGET` 打错一个字符时靠它兜住。
  2. 只删**认得的条目名**，别的一律不动（并列出来）→ 前缀与别的文件共处一个目录
     也不会误伤。
- **入口按内容判、不按名字判**：同一个目录里可能躺着别人放的入口，名字还就叫
  `raku-pm`（Unix 上 zef 生成的就是无扩展名的 `<名>`）。按名字删会删掉别人的东西；
  按内容只删自己写的那三份（它们都设 `RAKUPM_RAKU` 作启动兜底 —— 结构标记，改文案改不动它）。
- **不动别人的东西**：zef 装的 RakuPM 不在本前缀内（只提示 `zef uninstall RakuPM`）；
  `<名>.exe.zef-old`（zef 入口的唯一备份）只报告不删。
- 结尾提示如实说明「入口已一并清掉，所以此刻没有 `raku-pm` 命令了」，并给出两种
  装回来的办法（写「raku-pm self-upgrade」是错的 —— 那时已经没有这个命令）。
- `flush` 也进了「会写前缀」的命令集，于是自定义前缀下会先提示前缀路径再动手。
- 新增 `RakuPM::Prefix.is-fs-root`：把护栏里「是不是文件系统根」抽成**可测谓词**，
  否则那个分支没法在测试里触发（临时目录的 canonical 形式不可能是根）。
  顺带修掉它的一处平台陷阱：`C:` 必须在 `normalize` **之前**判 —— 否则会被
  `IO::Path.absolute` 当成相对路径变成 `<cwd>/C:`，永远匹配不到根。

### 测试

`xt/flush.t`（19 例，**放 xt/**：会触发「发现外来入口」分支并打印 ⚠，而 t/ 有洁净度硬约束）。
三条纪律写在文件头部：
① **永远注入 `:entry-dir`** —— 否则测试会删掉开发机上真正的 `raku-pm` 命令；
② 每条断言都问两遍「该删的删了没 / 不该删的还在不在」（只验前者的话，
   一个「把前缀整个 rm -rf」的实现也能全绿，而那正是最该挡住的实现）；
③ 护栏必须有测试。

**变异验证 3 个全红**：拿掉条目白名单（→「不认得的条目仍在」红）、拿掉入口内容标记
（→「外来的同名入口保留」红）、拿掉护栏（→「像前缀/家目录拒绝」3 条红）。
`t/prefix.t` 补 `is-fs-root` 5 例（≥16→21）。

### 自己踩的坑（记下来）

- 我用临时前缀跑 CLI 冒烟测试时，**真的把本机全局 `site/bin` 里的 `raku-pm` 入口删掉了**
  —— 生产行为是对的（flush 本就该清入口），但说明**冒烟测试也必须注入入口目录**。
  已按原样复制恢复（`~/.raku-pm/bin/` 里那三份 → 全局 `site/bin`），`raku-pm version` 复验可用。
- 新增的 Raku 陷阱已进 CODEBUDDY：`try {…} CATCH {…}` 的 CATCH 是死代码（try 吞异常）、
  `||` 链里夹 `~~` 会返回 Match（撞 `--> Bool`）、`Seq.sort({…})` 把 Seq 喂给比较块、
  `<[a..Z]>` 是倒序区间。

## 0.49.5 - 2026-09-16

修两件事：让 `xt/fetch.t` 真正离线（它是门禁里最后那个红），并补全索引拉取的韧性
—— 后者是**真实产品缺陷**，用户在国内网络下必然撞上。

### ① `xt/fetch.t` 声称离线，其实会去拉三个远程索引

它自己的注释写着「离线、确定性：源用本地目录仓库模拟 … 不去网络」，但构造 Client 时
只传了 `:repo-root`、**没传 `:repos`** → Client 照常读 `repositories.json` / 默认值，
于是挂上 zef + cpan + rea 三个**远程**索引。解析 `Lib::A`（明明在本地仓库里）之前
先要拉三个网络索引，任一无缓存且不可达就整条崩：

```
# You planned 8 tests, but ran 0
生态索引拉取失败且本地无缓存（https://raw.githubusercontent.com/Raku/REA/main/META.json）
```

现在显式 `:repos([Local…])`，注释与行为一致，门禁也不再随网络抖动变红。

### ② 索引拉不动的韧性：无缓存时也不再让整条命令失败

`Ecosystem.!refetch-index` 的失败分支原本是：

| 情形 | 旧行为 |
|------|--------|
| 拉取失败 + 有旧缓存 | 用旧缓存继续 + 告警 ✅ |
| 拉取失败 + **无缓存** | **`die`** ❌ |

而本文件上方早就写着原则「**单点网络抖动不应升级成安装失败**」—— 只在「有缓存」那半
兑现了。解析器会把每个仓库都问一遍，所以任何一个索引不可达都能让**整条命令**失败，
哪怕答案本来就在可达的仓库里（本地目录仓库、或 zef 索引）。**用户现场（国内网络）**：
`raw.githubusercontent.com` 上的 cpan/rea 不可达且从未缓存过 →
`raku-pm fetch / install / search` 全都直接崩。

- 无缓存失败改为：**按空索引处理 + 一行 `⚠`（点名索引与 URL）**，不再 `die`。
- 新增公开属性 `Ecosystem.degraded`：两种降级（用旧缓存 / 按空索引）都置位。
  照着 `Client.did-self-shadow-warn` 的既有做法 —— **警告要能被测试断言**，
  而不是让测试去 grep stderr（那种写法一改文案就假绿）。
- README（中/英）补「某个索引拉不动时怎么办」：两种情形一张表 + 怎么读那几行 ⚠
  （「找不到模块」紧挨着索引 ⚠ = 不是没有、是这次看不到）。

**测试**：`xt/http.t` 新增 7d（无缓存失败 → 不抛错 + `degraded` 置位 + 空索引，
并先断言「cache-root 里确实没有任何缓存」）。
**变异验证**：把 `die` 放回去 → 7d 两条断言变红；恢复后绿。
**回归**：`t/` 66/66、`xt/` **30/30**（`xt/fetch.t` 首次在离线环境下通过）。

## 0.49.4 - 2026-09-16

文档补充（无代码改动）：补上「**全局 `RAKULIB` 对「开发 raku-pm 本身」是个陷阱**」的警告
—— README 的 PATH 模式一节本来就建议把 `RAKULIB` 写进 shell 配置，却没说清副作用。

**触发它的真实数据**（用户 WSL，裸 `raku` 的仓库链）：

| 链序 | 仓库 | `RakuPM::Version` |
|------|------|-------------------|
| [0] | `/home/user/.raku-pm/site` | **0.36.14** ← 过期副本，且排在最前 |
| [1] | `/home/user/.raku` | 0.46.1 |
| [2] | `/home/user/rakudo/share/perl6/site` | 0.46.1 |

后果是**逐模块的版本混用**，比整份旧版更难认：`RakuPM::Author` 在 0.36.14 里还不存在
（作者侧是 0.41.0 才加的），于是 `use RakuPM::Author` 落到 [1] 的 0.46.1
（它第 57 行会调 `scan-provides`），而 `RakuPM::Distribution` 从 [0] 的 0.36.14 拿到
（那时还没这个方法）→
`No such method 'scan-provides' for invocant of type 'RakuPM::Distribution'`。
这与 0.49.3 修的那个假绿是同一件事的两面：**测的都不是工作树**。

- README（中/英）在「安装前缀」下新增小节：说清副作用、给三条自保办法
  （只用 `raku tools/run-suite.raku`；单跑用 `raku -Ilib t/<x>.t`；用
  `raku-pm which RakuPM::Version` 看此刻裸 `raku` 会加载哪一份），
  以及清理过期副本的**命令**步骤（`installed` → `self-upgrade` →
  `uninstall --version=`，**别手删**）。
- CODEBUDDY 把这条症状写进排查清单：**「旧版的 bug 被当成当前代码的失败」先查链首**。

## 0.49.3 - 2026-09-16

修两个**测试基建**缺陷（产品逻辑无改动）。用户在 WSL 上跑 `raku tools/run-suite.raku`
一次报了 6 个红：其中 5 个是同一个原因，另 1 个是 0.49.2 同类问题的漏网。

### ① 门禁跑测试时漏传 `-Ilib` —— 5 个红的总因

`tools/run-suite.raku` 的 `run-one` 是

```raku
run($raku, $rel, :out, :err, :cwd($root));      # ← 没有 -Ilib
```

（同文件的「编译暂存文件」那处倒是传了 `-Ilib`，所以一直没被注意到。）
于是**任何没写 `use lib` 的测试都在测「系统里已安装的 RakuPM」，而不是工作树** ——
`t/author-check.t`、`t/store-lock.t`、`t/store-provides-ext.t`、`xt/repos.t`、
`xt/http.t` 正好都属于这一类。

**本机实测对照**（`raku -e 'say $*REPO.repo-chain.head.Str'`）：

| 启动方式 | 仓库链首 | 结果 |
|----------|----------|------|
| `raku -Ilib …` | `<仓库>/lib` | ✅ 测的是工作树 |
| `raku …`（无 `-I`） | 用户 `RAKULIB` 目录 | ❌ 本仓库 `lib/` **根本不在链上** |

所以开发机上一直全绿，只因那份已安装副本恰好自洽；用户机器上它与当前代码版本不一致，
于是**旧版的 bug 被当成当前代码的失败报了出来**。最典型的一条：`xt/repos.t` 报
`123.foo.example.com → foo`，而 `foo` 正是**修复前**的返回值（同一组里
`360.zef.pm → zef` 旧版也能过，所以只有 4c 一条红 —— 与旧代码行为完全吻合）。

- `run-one` 改为带 `-I<绝对路径>/lib`；启动参数收敛成**唯一**的 `test-args`
  （自检与真实运行共用它，否则自检只是自说自话：删掉 `run-one` 里的 `-I` 它照样绿）。
- 新增**门禁自检** `tests-see-worktree`：跑测试之前，用同一个启动方式验证
  `$*REPO.repo-chain.head` 就是本仓库的 `lib/`；不是就**直接中止**并说明「再跑下去
  只会得到测了已安装副本的假绿」。这类「门禁看着绿、其实测的不是这份代码」的假绿，
  比没有门禁更危险。
- **变异验证**：把 `-I` 从 `test-args` 去掉 → 自检立刻中止（exit=1，并打印排查命令）；恢复后正常。

**审计（范围扩到 `xt/`）**：0.49.2 只扫了 `t/`，所以漏掉了 `xt/entry-check.t`。这次两边一起扫，
**共 14 个测试文件没有 `use lib`**（`t/` 9 个：author-check / clean / known-test-failure /
resolver-core-modules / store-lock / store-precomp / store-provides-ext / test-depends / version；
`xt/` 5 个：check-preflight / http / new-scaffold / repos / self-upgrade）。
它们**完全依赖**运行器传 `-Ilib` —— 这正是自检要守的东西。

### ② `xt/entry-check.t` 也是「宿主依赖」（第 6 个红）

与 0.49.2 修的 `t/bin-path-order.t` 是同一次事故的第二个现场：场景 1-5 写的是
**Windows 语义**（靠 `.exe`/`.bat` 参与命令解析）却没注入平台 —— Linux 上
`pick-command-file` 只看无扩展名，一个都认不出来 → 不接管 → 断言全红。
（`t/` 那个会挡住 `install .`；`xt/` 这个只挡自己跑门禁，所以上一轮没暴露。）

- `Installer.ensure-bin-entry` 新增 `Str :$os` 注入口（与 `:@dirs` 同一惯例），
  透传给 `pick-command-file`；生产路径不传，**行为不变**。
- `xt/entry-check.t` 的 Windows 场景统一写 `:os('win32')`，并新增场景 6 验 Unix 语义：
  **Linux 下 `.exe` 根本不参与命令解析**，所以那种外来 `.exe` 不该被改名
  （改名在 Linux 上毫无意义，还白改别人的文件）。
- **变异验证**：去掉 `$os` 透传 → 场景 6 两条断言变红（exit=2）；恢复后 10/10 全绿。

## 0.49.2 - 2026-09-16

修「raku-pm 在 Linux 上装不起来」：`t/bin-path-order.t` 是**只在 Windows 上能过**的测试，
它在 t/ 里，于是 Linux 上 `bin/raku-pm.raku install .` 跑到它必红、整次安装被回滚。

**用户现场**（WSL）：
```
✓ …（其余 65 个用例全过）
✗ bin-path-order.t
↻ 串行复验 1 个失败用例一次（并行编译竞态 / 上游 flaky 都可能）
⚠ 安装未全部成功，正在回滚到本次操作前的状态…
```
复验仍红 ⇒ **稳定失败，不是并发竞态**，是测试本身的问题。

**根因**：该文件 0.48.0 进来时把 **Windows 语义当成了宿主事实** —— 断言里放的是
`.bat` / `.exe`，而 Linux 上 `Platform.exec-suffixes` 只认无扩展名那一种（`: ('',)`），
一个候选都找不到。**被测逻辑本身没问题**：唯一判定处 `Platform.pick-command-file` 是对的，
`raku-pm env` 在 Linux 上工作正常；错在测试没有注入平台。

- **给 `Reporter.detect-bin-on-path` 加 `Str :$os` 注入口**（与既有的 `:@dirs` 同一惯例），
  并透传给 `pick-command-file` / `exec-suffixes`。生产路径不传 `$os`，行为**完全不变**。
- **`t/bin-path-order.t` 改为两套语义都注入**：`:os('win32')` 验 Windows 那一节、
  `:os('linux')` 验 Unix 那一节。于是任何宿主上都能验两个方向，Windows 分支不再只能靠
  「恰好在这台机器上」验证。新增 Unix 侧 4 条断言，其中最关键的一条是
  「只有 `.bat`/`.exe` 时零候选」—— 这正是宿主混淆时会红的那条。
- **顺带修掉测试报告的一个缺陷**：候选数组为空时 `@got[0]<path>` 会**抛异常**而不是干净
  失败，于是文件在第一条断言就中断、后面几条根本不跑（报告只剩半截，看不出别处有没有问题）。
  加 `base-at(@got, $i)` 安全取值（空则返回 `'<缺>'`），失败时如实列出全部红项。

**审计**：全库扫 `t/` 里依赖宿主平台的地方，**只此一处** offender
（`t/build.t` / `t/build-trace.t` / `t/by-distro-depends.t` / `t/md5.t` /
`t/known-test-failure.t` / `t/native-search-path.t` 都走了 `$*DISTRO` 分支或遮蔽）。

**变异验证**：把 `detect-bin-on-path` 的 `$os` 透传去掉（= 修前状态），Unix 那一节
**四条断言全部变红**（7/8/9/10，exit=4）；恢复后 11/11 全绿。这也从**代码侧**证明了
「宿主依赖」这一类问题现在会被测试抓到 —— 用户那次 Linux 运行是同一形态的实例。

**顺带记录一个 Raku 陷阱**（写上面那个安全取值助手时踩的）：`A && B` 短路求值成
**`False`，而 `False` 是 defined 的** → `(... ) // '<缺>'` 兜不住 → 返回值是 Bool →
`--> Str` 当场类型检查失败。`//` 只兜未定义，与 `try` 那条约同一类坑。

## 0.49.1 - 2026-09-16

文档补充（无代码改动）：把 **PATH 模式**写成「安装前缀」一节的推荐做法 ——
即完全不往前缀外写文件的那条路。

0.49.0 的参数已经够用（`--no-promote-bin` + 把 `<前缀>/bin` 放 PATH 最前），
但 README 只说了「有这个旗标」，没说清**怎么整体切换、切换后要注意什么**：

- 三步切换：① 先配 PATH 与 RAKULIB（顺序必须在前）→ ② `self-upgrade --no-promote-bin`
  → ③ 可选清掉此前写进全局 `site/bin` 的三份副本。
  **顺序不能反**：先配好 PATH 再删副本，否则会短暂没有入口。
- 讲清三个好处（前缀外零文件 / 不动别人的文件 / 撤销只要改 PATH）与代价
  （自己配一次那两行，`raku-pm env` 会打印）。
- 补一节「多版本 / 多项目共存」：一份前缀就是一份独立环境，
  `--target=./projA/.raku-env`，各项目的同一个包可以有不同版本。
- 提醒 0.48.0 的「入口体检」改名只是补救手段，PATH 模式才是干净解。

中英两份 README 同步。

## 0.49.0 - 2026-09-16

把 `--target` 从「一个能传但会漏的开关」变成**真正的安装前缀**（插座），并让
`promote-bin` 的默认值随前缀走。**行为变更**：自定义前缀下不再默认往全局
`site/bin` 写 wrapper（要保留旧行为就加 `--promote-bin`）。

**为什么值得单独一版**：`--target` 与 `RAKUPM_TARGET` 一直都在，但插座是**断的** ——

1. 三份 wrapper（bash / `.bat` / `.ps1`）都**没有把前缀传给 CLI**，只设了 `RAKULIB`。
   于是 `<前缀>/bin/raku-pm` 设的 RAKULIB 指向自己的 site，而 CLI 回落到默认前缀
   `~/.raku-pm` —— **命令装进 A、模块却从 B 找**。实测：直接跑 store 里的 CLI，
   `raku-pm env` 一律打印 `C:\Users\...\.raku-pm`，与 wrapper 所在前缀无关。
2. `promote-bin` 在 CLI 里**无条件为真** → 自定义前缀也会往全局 `site/bin`
   （zef 用的那个、天然在 PATH 上的目录）写 wrapper。0.47.0 开发时用
   `--target=_h9repro/target` 装了一次，全局 `raku-pm` 就被指向了那个临时目录。

- **新增 `RakuPM::Prefix`**：「默认前缀」的唯一出处。CLI 要在 `require RakuPM::Client`
  **之前**就知道前缀（`version` / `help` 要零模块图冷启动），而 Client 又要按前缀决定
  promote —— 两边各写一遍 `~/.raku-pm` 就是两个事实来源。零依赖，含 `default-target` /
  `resolve` / `canonical` / `is-default-target` / `default-promote-bin`。
- **顺手修掉一个真实缺陷：`~` 不展开。** 实测 Rakudo **不会**展开 `IO::Path` 里的前导
  `~` —— `'~/.raku-pm'.IO.absolute` 得到的是 `<当前目录>/~/.raku-pm`。所以修复前
  `raku-pm --target=~/myenv install Foo` 会真的在当前目录下建一个字面叫 `~` 的目录。
  现在在入口统一展开（认 `~/x` 与 `~\x`；不认 `~user`，跨平台语义不一致不猜）。
- **前缀自洽**：三份 wrapper 都把 `--target=<自己的前缀>` 传给 CLI —— 敲哪个前缀的
  wrapper 就在哪个前缀里干活，与 venv 的 `<venv>/bin/python` 同一语义。
- **`promote-bin` 的默认值按前缀算**（`Prefix.default-promote-bin`）：
  默认前缀 → 写全局 site/bin（保持 zef 同款「装完即用」）；
  自定义前缀 → 不写（局部安装不该污染整机环境，多个前缀还会互相覆盖）。
  新增 `--promote-bin` / `--no-promote-bin` 显式覆盖（互斥，同时给会报错）。
  `Client.promote-bin` 改为**不写默认值**，用「未定义 = 未指定」区分「没传」与
  「显式要求关」，于是自动策略只有一处实现。
- **自定义前缀下如实告知**：会写前缀的命令打印说明（前缀路径 + 该把哪个 bin 放进 PATH
  + 想回 zef 行为加什么旗标）。显式给了 `--no-promote-bin` / `--promote-bin` 时不再重复。
- **`--target` 不带值不再静默回落默认前缀**（以前会被当成「没给」，用户以为指定了）。
- **「是不是默认前缀」按路径值判**（不是按「有没有写 --target」）：把
  `RAKUPM_TARGET=~/.raku-pm` 写进 shell 配置的人，意图就是默认前缀；Windows 上折大小写。

**测试**：新增 `t/prefix.t`（16 例，前缀解析/判定的纯函数）、`t/prefix-isolation.t`
（20 例 = CLI 策略 14 + Client 策略 3 + **真装一次**并断言前缀外零写入 2）。
`t/installer-shell-templates.t` / `t/wrapper-rakulib.t` 补「显式传前缀」断言（3 处）；
`xt/wrapper-rakulib-run.t` 真跑 Git Bash，断言 `--target` 确实落到了子进程。

**变异验证 4 个全红**：拿掉 bash 的 `--target`（3 处断言红）、拿掉 `.bat`/`.ps1` 的
（2 处红）、Client 改回无条件 promote（4 处红）、CLI 改回无条件 promote（3 处提示断言红）。
其中「Client 改回无条件 promote」那一次让 `t/prefix-isolation.t` 的泄漏断言真的变红 ——
它确实往全局 `site/bin` 写了探针文件，事后已清理并核对目录回到基线（也正因此，
「自定义前缀不污染全局」这条不是纸面承诺而是被测试守住的）。

**层3 的测试刻意带正控**：先断言「bin 确实落进了自己的前缀」，再断言「全局 site/bin 里
没有」—— 否则 `!install-bin` 根本没跑也会绿（假绿高发区，本项目反复踩过）。

**顺带记录两个 Raku 事实**（都实测确认）：
`IO::Path.absolute` 返回的是 **Str** 而不是 IO::Path（`--> IO::Path` 会当场断言失败）；
`Bool` 是原生类型 —— 属性**省略**是「未指定」，但显式传一个未定义的 `Any` 会直接类型
检查失败（`has Bool $.x;` + `:$x` 透传时要注意）。

## 0.48.0 - 2026-09-16

修「self-upgrade 成功了、敲 `raku-pm` 却还是旧版」，并补上「zef 与 raku-pm 都装了
同一个包时怎么查」的可观测性。**无破坏性变更**（新增 `raku-pm which`；`env` 输出多两节）。

**用户现场**：本机跑 `raku-pm self-upgrade`（0.46.1 → 0.47.0），流程全通 —— 入 store、
62 个测试文件全绿、三份 wrapper 重写、lock 更新 —— 但升级后敲 `raku-pm version`
仍显示 **0.46.1**。

根因（已实证）：Windows 用 `PATHEXT` 解析命令名，**`.EXE` 优先于 `.BAT`**，也优先于
没有扩展名的文件。而 `PATH` 上 rakudo 的全局 `site/bin` 排在自装目录之前，那里留着
**zef 时代生成的 `raku-pm.exe`** —— 它会一直遮蔽 raku-pm 自己写的 bash/`.bat` wrapper。
self-upgrade 只维护自写的那三类 wrapper，从不处理这个 `.exe`。
（把该 `.exe` 临时移开 → 立刻变成 0.47.0；移回 → 0.46.1。）

- **入口体检 + 自动接管**（`Installer.ensure-bin-entry`）：安装/升级结束时按平台解析
  规则算出用户敲该命令名**实际会执行哪个文件**；若不是 raku-pm 写的那份
  （`<name>` / `<name>.bat` / `<name>.ps1`），把它改名备份为 `<name>.zef-old`
  （可逆、不删数据）并打印说明与还原方法。幂等；没有自写 wrapper 时不接管
  （否则会制造「连命令都没有」，比遮蔽更糟）。判定与 `raku-pm env` 共用
  `Platform.pick-command-file`，不再两处各写一套。
- **`exec-suffixes` 改为 PATHEXT 感知**：旧值是 `('', '.exe', '.bat', '.cmd', '.ps1')`
  —— 把无扩展名排第一（Unix 语义），于是自查工具指错「实际执行的那份」。现在 Windows
  读真实 `PATHEXT`，且**不把无扩展名列进去**（cmd/PowerShell 根本不会执行无扩展名
  文件）；Unix 仍是 `('',)`。`find-executable` / `has-executable` 一并受益。
- **`raku-pm env` 的候选排序改为「模拟系统解析」**：不再依赖 `which`/`where` 的输出
  顺序（Windows 的 `where` 实测按「无扩展名 → .bat → .exe」列，与系统解析不符）。
  现在按 PATH 目录顺序 × 目录内 PATHEXT 顺序算 —— 第一个即实际执行的那份；后续标注
  「被前面那份遮蔽，不会被执行」，并点名「自装 wrapper 被遮蔽了」。
  顺带：目录顺序**优先于**扩展名优先级（同一个目录内才轮到扩展名说话）。
- **新增 `raku-pm which [模块名]`**（回答「两个包管理器都有同一个包，程序从哪加载」）：
  - 不给参数：跨仓库冲突总览 —— 同一发行版存在于多个仓库的全部情况，按仓库链序号
    标优先级。本机实测 **24 个**（其中 `HTTP::Tiny`/`JSON::Fast`/`MIME::Base64`
    版本还不一样）。
  - 给模块名：该模块实际从哪个仓库解析、版本多少；多份时全部列出并标出被遮蔽的。
    要写**模块名**（如 `JSON::Fast`），不是发行版名。
- **wrapper 保留用户已有的 `RAKULIB`**（三个平台模板）：旧 wrapper 直接
  `export RAKULIB="inst#<target>/site"` —— **整条替换**，把用户自己设的仓库
  （用户机上 `RAKULIB` 指向 `~/raku-modules` 这个 FileSystem 仓库）挤掉，raku-pm
  会话里那些模块突然看不见。现在：先存原值 → 设 raku-pm 的 site（排最前，保住
  「raku-pm 装的包优先」）→ 原值非空则**追加在后面**。
- **promote 目标改为「全局 site/bin」**，并把 `.ps1` 一并推上去：`site-bin-dir` 取的是
  「链上第一个 basename 为 `site` 的仓库」，而 raku-pm 自己的 `~/.raku-pm/site` 也叫
  `site` —— 于是通过 raku-pm 的 wrapper（`RAKULIB` 已设）启动时，promote 会把 wrapper
  写进那个**不在 PATH 上**的目录，用户永远敲不到。新增 `global-site-bin-dir`
  （显式排除自己的 site），promote 与入口体检都用它。另外 `.ps1` 以前只生成不推广，
  现在三份一致（输出也不再声称写了三个却只放上去两个）。
- 回归：新增 `t/wrapper-rakulib.t`(9)、`t/bin-path-order.t`(7)、`xt/entry-check.t`(8)、
  `xt/wrapper-rakulib-run.t`(5，真跑 Git Bash 验证 RAKULIB 拼接语义)、`t/platform.t`
  第 8 节（PATHEXT 顺序 + `pick-command-file` 三份并存场景）。
  **变异验证**：4 个变异（bash 模板退回整条替换 / `exec-suffixes` 退回旧表 / 入口体检
  去掉 own 判断 / env 目录顺序反转）全部按预期变红，源码逐份复原。
- 顺带：`tools/NamedArgCheck` 的 `%ALLOWED` 补 `short-name`
  （`CompUnit::DependencySpecification.new(:short-name<Foo>)` 的核心 API 参数）。

## 0.47.0 - 2026-09-16

落地审查报告遗留项 **H9（平台分支不可达）**，顺带修掉用户报的 `raku-pm version`
在 Linux 上喷 `Use of Nil in string context`。**无破坏性变更**；`--os=` 现在会校验取值。

### 新能力：`RakuPM::Platform` —— 平台差异的唯一出处

- 要解决的问题：`if $*DISTRO.is-win { ... } else { ... }` 在当前平台上是**单向短路**的，
  非当前平台那一半代码在开发机上**永远跑不到**，错误也就永远测不出来。项目真实栽过：
  `MD5.system-md5` 里写了 `$*DISTRO.is-macosx`（`Distro` 根本没这个方法），Windows 上
  `is-win` 为真、`elsif` 直接短路，全套测试全绿；一到 Linux 就
  「No such method 'is-macosx'」崩在 Store 的内容指纹计算上。
- 做法：把「这台机器是什么平台」收敛成一个模块。`os-key` / `is-windows` / `is-macos` /
  `path-list-sep` / `env-dirs` / `exec-suffixes` / `find-executable` / `native-path`（MSYS
  路径转换）/ `which-command` / `kill-tree`（taskkill vs SIGKILL）/ `library-file-names` /
  `library-search-paths` / `linux-family` / `md5-command` / `parse-md5-output` 全在这里，
  一律**调用时**读 `$*DISTRO`。
- 因此测试能用 `my $*DISTRO = Distro.new(:name('linux'))` 词法遮蔽，在任意一台机器上把
  三个平台的分支**真跑一遍**（Raku 的动态变量查找是运行期的，能穿透进被调模块）。
- 消费方全部改为调用它（`MD5` / `NativeLib` / `Client` / `Client::Tester` /
  `Client::Reporter` / `Client::SelfManager` / `Installer` / `Distribution`）。
  `NativeLib` 的公开方法（`os-key` / `os-label` / `package-key` / `candidates` /
  `search-paths`）签名不变，只把实现委托过去。
- 途中修掉两个真实缺陷：① `os-key` 若只信 `$*DISTRO.is-win`，遮蔽成 `win32` 会被误判成
  linux（Rakudo 只把 name `mswin32` 判成 win）→ 补 name 判据；② `native-path` 原先**无
  条件**做 MSYS 转换，在 Linux 上会把合法的绝对路径 `/c/foo` 篡改成 `C:\foo` → 改为按平台门控。
- `--os=` 现在**校验取值**：只认 win32/windows、darwin/macos/osx、linux/unix；拼错直接报错。
  （`resolve-os` 对认不出的值会回落到本机 —— 静默判错平台比报错更糟。）
- 回归：新增 `t/platform.t`（94 例，三平台 × 每个函数 + 消费方跟随遮蔽 + 真实 md5 输出
  样本解析 + kill-tree 契约与真杀 + 遮蔽不泄漏）、`t/platform-centralized.t`（12 例，
  禁止 `lib/` 除 Platform 外再直接用 `$*DISTRO`/`$*KERNEL`，并反向要求 Platform 自己确实在读）。

### 修复：`raku-pm version` 在 Linux 上的 `Use of Nil in string context`

- 现场：0.46.1 在 Linux 上每次 `raku-pm version` 都在 stderr 多一行警告，报的行号指向
  `bin/raku-pm.raku` 第 76 行 —— 而那行看起来毫无字符串插值。
- 根因：76-77 行是**一条跨行语句**
  `return ($d.meta<version> // ...) if $d.meta<name> eq 'RakuPM';`。
  `eq` 是**字符串比较**，会对左操作数调 `.Str`；某些已装发行版的 meta 里没有 `name`
  字段 → `Nil` → 警告。Rakudo 报的是这条语句的**起始行**，所以指到 76。
- 改法：`next unless (($d.meta<name> // '') eq 'RakuPM')`。项目里 20+ 处读 CUR 元数据的
  地方本来就都写了 `// ''`，只有 CLI 这一处漏了。
- 新增 `t/meta-definedness.t`（8 例）：同一份探针只差一行守卫，证明那行守卫是承重的；
  外加全库静态检查（禁止 `meta<...>` 直接 `eq`/`ne`），并修掉 `xt/skip-installed.t`
  里最后一处同类写法。

### 修复：Linux 上的测试超时看门狗从未生效（做 H9 时挖出来的）

- 现场：把 Tester 的超时杀进程抽成 `Platform.kill-tree` 后，`xt/tester-async.t` 场景 3
  （`sleep 30` 的测试应被看门狗杀掉、安装应在远小于 30s 内返回）**跑了 60.95s**。
- 查下去发现两件事：① 我抽出来时把签名写成 `Proc $p`，而调用点传的是
  `$proc.start()` 返回的 **Promise** → 类型检查失败 → 异常在 Promise 的 `.then`
  回调里被静默吞掉 → 根本没杀；② 更要紧的是**回看基线**：0.46.1 的 Unix 分支写的是
  `try $p.kill(9)`，而那里的 `$p` **本来就是 Promise**（`Promise` 没有 `.kill`）——
  也就是说**超时看门狗在 Linux/macOS 上从来没有生效过**，只是被 `try` 吞得无声无息。
  Windows 分支只用 `$pid`（走 taskkill）、碰都不碰这个参数，所以开发机上永远发现不了。
- 改法：`kill-tree` 的第一个参数改成 `$proc where *.can('kill')`（既在**平台分支之前**
  拒绝传错对象，又允许测试传替身对象），调用点改传 `$proc`。
- 这条也顺手说明了 H9 的价值：它与 `is-macosx` 那次是同一类 —— **非当前平台的分支
  没有任何验证**，错误就长期潜伏在用户那边。
- 回归：`t/platform.t` 新增三个断言（误传 Promise 立刻抛；Unix 分支调 `kill(9)`；
  Windows 分支不调 `.kill`），并**真起一个睡 3600s 的子进程真杀一次**（0.2s 见效）。
  变异验证：把 Unix 分支改成 `kill(0)` → 变红；把 Tester 调用点退回传 Promise →
  `xt/tester-async.t` 场景 3 变红（即本次那个回归本身）。

## 0.46.1 - 2026-09-16

修 0.46.0 引入的检查器健壮性隐患：**heredoc 遮蔽改为两遍扫描**。

- 问题：`mask-heredocs` 原本是单遍扫描 —— 只要起始判据误把某行当成 heredoc 起始
  （典型情形：**被扫描项目的文档注释里恰好列了 `q:to/END/` 这类写法**），就会一路
  遮蔽到文件末尾、或下一次偶然出现该标记的行，**静默吞掉大片真实代码**，而检查器
  照样输出「通过」。这正是作者第一版翻车的原因（NativeLib 的注释里列了各种 heredoc
  写法 → 273 行的 `has Str $.os` 整行变空格，属性名一个都收不到）。
- 改法：**先扫描确认「结束标记真的存在」，再遮蔽**。找不到结束标记就**不当作 heredoc**
  —— 「起始判据误判」于是自然退化为「没找到」，不再有吞掉整个文件的后果。另加 500 行
  上限作防御。
- 回归：`t/named-arg-check.t` 新增第 ⑫ 例「有起始行、却无结束标记时不能吞掉后面的
  代码」（单遍实现会在这一例变红），共 13 例。

## 0.46.0 - 2026-09-16

落地审查报告遗留项里优先级最高的一条：**具名实参名的静态检查**。做它的过程中又抓
出一类真实的跨平台缺陷（`mkdir` 的 `:parents`/`:p` 是无效参数）。**无破坏性变更**。

### 新能力：具名实参静态检查（`tools/check-named-args.raku`）

- 要抓的问题：Raku 的 **method 会静默忽略未知具名实参**（而 sub 会报错）——
  `$o.m(:zzz(9))` 不报错、不警告，参数凭空失效。项目已因此栽过两次：
  `uninstall($name, :$ver)`（形参其实叫 `:version`，整包版本被删）、
  `self-upgrade(:$src)`（形参叫 `:source`，那条分支永不走到）。
- 做法：全项目收集「已声明的具名名字」（routine 形参 + `has` 属性名），再扫所有
  colonpair，名字不在集合里就报。**全程字符级扫描** —— 不用正则配命名捕获，
  那是 0.44.0 那次失败的死因（命名捕获配字符类会「匹配上了但捕获是 Nil」→
  全体误判为通过 → 检查器变假绿）。
- 已接入回归门禁（`run-suite.raku` 的静态检查一节；`--docs-only` / `--precommit`
  也会跑）；新增 `t/named-arg-check.t`（12 例，每个场景都是「喂人造源码 →
  断言报 / 不报」）与 `tools/check-named-args.raku --verbose`。
- 白名单 `%ALLOWED` 每条都写了理由；新增前先问「这个名字能不能在项目源码里找到
  声明」，能就别加（说明是扫描器漏收）。

### 顺带修掉的两类「静默失效」（检查器第一次运行就报出来的）

- **`mkdir(:parents)` / `mkdir(:p)` 是无效参数**：Rakudo 2026.08 的
  `IO::Path.mkdir` 实测签名是 `:(IO::Path:D $: Int(Any) $mode = 511, *%_)` ——
  只有 `$mode`，`:parents` / `:p` 全被 `*%_` 吞掉（不报错、也不生效）。Windows 上
  因 `CreateDirectoryW` 会自动建父目录而「看起来正常」，**Linux 上 `mkdir(2)` 不建
  父目录**，`<root>/<name>/<version>` 这类深层路径会直接失败 —— 属于「只在
  Linux/CI 上炸、本机 Windows 测不出来」的一类。
- **新增 `RakuPM::Fs.mkdir-p`**：逐级创建目录（等价 `mkdir -p`），返回 `$p` 以便
  链式调用。lib/ 里 25 处 `.mkdir` 全部改用它（逐个判断父目录是否已存在不现实，
  统一走逐级创建最稳）。

### 修掉一个既有测试的偶发失败（与本次改动无关，顺手治好）

`xt/new-scaffold.t` 的工作目录只用 `$*PID` 命名且**从不清理**：TMPDIR 里攒了十几个
`raku-pm-new-xt-*` 残骸后，一旦 PID 重用就会撞上上次的 `Foo-Bar`，`new` 因「目标目录
已存在」而退出 1（单独跑却总是绿的）。改为「PID + 时间戳」并把工作目录纳入 `LEAVE` 清理。

## 0.45.0 - 2026-09-16

审查报告里的 **H6**（`with-write-lock` 用环境变量广播绕过子进程锁 —— 设计脆弱）
落地。**无破坏性变更**（`--no-lock` / `RAKUPM_NO_LOCK` 的用法与语义不变）。

### 变更：锁的「继承」从广播开关改为校验凭证

- 问题：持锁进程原先设 `RAKUPM_NO_LOCK=1` 给子进程「开绿灯」（`Build.pm` 里再调
  `raku-pm` 是常态，不这样做会自锁到 600s 超时）。那是个**设了就开门**的全局布尔
  开关 —— 任何继承了该变量的进程都自动拿到免锁权限：用户随手 `export` 一下就绕过
  了锁；而且**装 raku-pm 自己时会跑它自带的 `t/`**，子进程继承该变量让
  `t/client-write-lock.t` 的真锁从未持起、3 条断言恒真（假绿）。
- 现在：持锁时下发一张随机**凭证**（锁旁 `.token` 文件 + `RAKUPM_LOCK_TOKEN`
  环境变量）。子进程走 `FileLock.should-bypass`，要求三件事同时成立才免锁放行：
  ① 带着 token；② 盘上 token 与它一致；③ **锁此刻确实被占**。
  伪造不了，且凭证随锁作废 —— 父进程一放锁，晚到的子进程照样排队。
- `RAKUPM_NO_LOCK` 保留但**只作**用户逃生舱；程序内部不再设置它。
  同进程嵌套也不再依赖环境变量，交回 `FileLock` 的引用计数
  （原先内层会完全绕过 `acquire`，连计数都不加）。
- 回归：`t/file-lock-token.t`（20 例）覆盖凭证生命周期、同进程重入，以及跨进程的
  「正确凭证放行 / 伪造凭证被挡」；`t/client-write-lock.t` 场景 4 改为断言新机制。
  两者都做过**变异验证**（弱化判定 / 退回广播 / 不校验锁被占 / 不删凭证文件，
  各自令对应断言变红）。

## 0.44.0 - 2026-09-16

审查报告里的 **H5**（`verify` 补记构建痕迹）落地，并在过程中**顺带查出并修掉
两个真实的静默失效缺陷**。**无破坏性变更**（store 只多一个旁车文件，旧条目不受影响）。

### 新能力：构建痕迹 `build.json` 旁车（H5）

- 背景（此前 README 里被诚实标注为「查不出来」的一类）：`Store` 只把**磁盘上真实
  存在**的资源写进 store 的 `META6.json`（必须如此 —— 声明了却找不到会让
  `CUR::Installation` 装不上）。于是「构建没跑成功 → 声明过的资源压根没生成」
  在元数据里被**静默剔除**，事后看那份 `META6.json` 干干净净，`verify` 无从判断。
- 现在 `Store.put` 落一个 `build.json` 旁车，记下：构建阶段做了什么
  （`mode` = `built` / `skipped-no-build` / `not-needed` / `not-recorded`、
  成败、builder 名 / Build 文件名、用的 raku 与**编译器发行版号**），以及
  `resources-declared` / `resources-present` / `resources-missing` ——
  **被剔掉的是哪些，原样留证**。
- 它**不计入内容指纹**（`!fingerprint` 里与 `.digest` 同级跳过）：是「条目怎么
  产出的」的记录，不是发行版内容；带时间戳，计入指纹会让每次重算都漂移而误报
  「内容已被修改」。已用测试钉死「改写 build.json 不影响 verify」「重新入库指纹不变」。
- `Store.!meta6-for` 补写 `builder` 字段：store 作为快照不该丢掉「这个包需要构建」
  这个事实，否则 `verify` 判断不了「一个没有痕迹的条目本不该有痕迹」。
- `Installer.consistency-issues` 新增 4 类检查（全部 `fixable=False`，需重装）：
  `missing-build-resource`（构建后资源仍没生成）、`no-build-trace`（声明了 builder
  却没有痕迹）、`build-skipped`（用 `--no-build` 装进去的）、
  `build-failed`（构建失败却仍入库）。
- `Client.!install-chain-inner` 里**两处**重复的构建调用（readmit 分支与新装分支）
  收敛成唯一的 `!build-phase`：加一个字段不再要改两处 —— 正是本项目最高产的漏传
  模式。`ensure-git-deps` 的调用时机按原样保留不动。

### 顺带修掉的两个静默失效缺陷（都源于下面那条 Rakudo 行为）

- **① 按版本卸载会变成整包卸载**：`Reporter.uninstall` 写的是
  `self.installer.uninstall($name, :$ver)`，而 `Installer.uninstall` 的形参叫
  `:version` —— 具名参数被静默丢弃，版本约束凭空消失，
  `raku-pm uninstall Foo --version=1.0.0` 会把 Foo 的**所有版本**都删掉，
  却照样打印「已卸载 Foo 1.0.0」。既有测试没抓到是因为 `t/multi-version.t` 走的是
  `$inst.uninstall(:version(...))`，**绕过了出问题的那一层**。
- **② self-upgrade 的「在仓库里跑则 pull 当前仓库」永远走不到**：CLI 传
  `:$src`，而 `SelfManager.self-upgrade` 的形参叫 `:source` —— 同样被静默丢弃，
  开发者的本地提交被无视、直接去拉上游（与帮助文案承诺不符）。
- 新增 `t/named-arg-wiring.t`：① 走 **Client 公开 API** 做行为断言（变异验证过：
  退回 `:$ver` 时它确实变红，且复现了「两个版本全没」）；② 对 self-upgrade 的
  调用形式做文本级钉（真跑要联网 + 改本机安装，不适合放进 `t/`）。

### 那条 Rakudo 行为（本轮最重要的发现）

本机 Rakudo（v2026.08）实测：`sub` 传未知具名参数会报
`Unexpected named argument`，但 **`method` / 私有方法 / role 方法一律静默忽略**
未知具名参数 —— 不报错、不警告、直接取默认值。也就是说**方法调用点上的具名实参
名字拼错，编译器与运行时都不会告诉你**。上面两个缺陷都落在同一个盲区里。

- 已把这条写进 `CODEBUDDY.md` 踩坑清单，并定下写法约定：
  方法调用一律用具名 `name => value`（而不是 `:%x` 简写），除非形参确实同名。
- 曾尝试写一个「全域静态检查」测试把整类风险挡住，**但没能做成可靠的实现**
  （手工解析签名时形参表收集始终为空，成了「假绿」—— 比没有检查更糟），
  已撤下、不发布半成品。**遗留项**：把「具名实参名必须存在」做成可靠的
  lint（思路与踩到的坑记在 `CODEBUDDY.md`）。

### 测试

- 新增 `t/build-trace.t`（27 例）：构建成功 / 资源缺失 / `--no-build` / 无需构建 /
  旧条目无痕迹 / 旁车不参与指纹，六种情形全覆盖。
- 新增 `t/named-arg-wiring.t`（6 例，含变异验证）。
- 两条新测试都按要求做了**修复前必然变红**的验证：`t/build-trace.t` 在未改动的
  0.43.0 代码树上跑，报 `No such method 'facts'` 退出码 1。


## 0.43.0 - 2026-09-16

健壮性加固第二轮（承接 0.42.0 的重构）：把「回归门禁」真正建起来，并拆掉 Installer
最后两块越界职责。**无破坏性变更** —— 公开 API、命令行用法、磁盘布局、以及生成出来的
wrapper 文本全部与 0.42.0 逐字节兼容。

### 新增：回归门禁（本轮最重要的东西）

- `tools/run-suite.raku`：**唯一**的测试判定口径。t/（逐个取真实退出码 + 强制洁净 ——
  任何 `⚠` / `[RakuPM]` 生产级警告即判失败，因为 zef install 会跑这一批）、xt/（只判
  成败）、文档纪律校验。默认**串行**（对齐 zef；`--jobs=N` 才并行，理由见 0.41.3）。
  `prove6` 不进这条链路：它会把「Subtest N doesn't have a plan」这类无害解析告警当失败。
- **文档纪律纳入门禁**：META6 版本 ↔ README.md / README.en.md 的 `RakuPM:ver<>` 示例 ↔
  Changes 顶部条目三者必须一致；`provides` 与 `lib/` 下 `.rakumod` 必须互为全集；
  `bin` 清单文件必须存在；`depends.runtime.requires` 不得有空项。这些此前全靠人肉自觉，
  已漂移过多次。
- `tools/git-hooks/pre-commit` + `tools/install-hooks.raku`：预提交跑「文档纪律 + 编译本次
  暂存的文件」，秒级（不跑测试 —— 钩子一慢就会被 `--no-verify` 绕过）。
- `.workflow/master-pipeline.yml` 改为直接调同一个脚本：本地跑什么，CI 就跑什么，
  不再出现「本地绿、CI 红」而让人怀疑门禁本身。
- 门禁本身做了**变异验证**：故意改坏版本号 / 多加一个未声明的 lib 文件 / 让 provides 指向
  不存在的文件 / 让用例失败 / 让 t/ 喷警告 —— 五种情况都如实变红之后才恢复。不会变红的
  门禁等于没有门禁（这是本项目 5 起「假绿测试」的直接对策）。

### 架构：拆掉 Installer 最后两块越界职责（1029 → 923 行）

- 新增 `RakuPM::Installer::ShellTemplates`：bin wrapper 的三段平台 shell 模板
  （bash / cmd `.bat` / PowerShell `.ps1`）。原先它们是混在安装逻辑里的约 150 行字符串
  拼接，平台细节（cmd 的二次解析、PATHEXT、GNU vs BSD `sort -V`、`RAKUPM_RAKU` 兜底）
  与「把发行版交给 CUR」纠缠在一起。提成**无状态函数**后可脱离文件系统与 CUR 单独单测。
- 新增 `RakuPM::Installer::Generations`：`<target>/generations/` 的格式与生命周期
  （id 分配 / `before.json` / `meta.json` / `manifest.json` / `current` / 裁剪），原先散在
  Installer 的 6 个私有方法里。提成**值对象**（只吃一个 `$.dir`）。
- 为什么不用 role：Raku 的 role 只能访问自己在 role 内声明的属性，访问宿主的 `$!target`
  会直接编译报错（`Attribute $!target not declared in role`）。拆共享私有状态的代码，
  应当把状态提炼成值对象、把纯逻辑提炼成无状态函数。
- **验收标准是「输出逐字节相同」**：用 `git archive HEAD` 取 0.42.0 的代码树，在新旧两棵树
  上用**固定的 target 路径**各生成一次 wrapper，比 md5 —— bash / `.bat` / `.ps1` 三轮全部
  `IDENTICAL`。拆这种「生成用户可见文本」的代码，只看测试是否绿是不够的。
- `Generations.finish` 加 `// ''` 守卫：调用方若误把 Hash 展平后传进来，不再喷一屏
  `Use of Nil in string context`，只写空。

### 测试

- 新增 `t/installer-generations.t`（23 例）：id 分配、discard 后编号复用（刻意不留空洞）、
  prune 只保留最新且永不裁掉当前世代。
- 新增 `t/installer-shell-templates.t`（19 例）：钉住三个模板的关键结构（LF vs CRLF、
  `inst#` 前缀、fallback 分支、`@args`、GNU/BSD sort 两条路）。
- 新增 `t/run-suite-docs.t`：门禁自身的守护 —— 文档纪律一旦漂移，`zef install` 的测试阶段
  就会红，而不是等用户发现。
- 门禁接入后全量：t/ 55 个用例全绿，xt/ 全绿。

### 文档

- `CODEBUDDY.md`：新增「回归门禁」章节；把「新增/修改 if-else 或 fallback 分支，必须有人为
  触发该分支的测试」写成规则（本项目 5 起假绿全部命中同一判据：bug 存在时那条测试也是绿的）；
  补 role 属性访问限制、逐字节等价验收、「`[%(...)]` 会摊平」等踩坑。
- 两份 README 的模块清单同步新增的两个模块。
- 模块数量与用例数量在文档里改为「以 `META6.json` / 目录为准」，不再硬编码，避免再次漂移。

## 0.42.0 - 2026-09-16

以全项目《架构与缺陷审查报告》为依据的重构 + 健壮性加固：新增 3 个模块、修 5 个
既有/潜在缺陷、补 5 个守护测试。**无破坏性变更** —— 公开 API、命令行用法与磁盘
布局都保持兼容。

### 架构：消除三类「根因级」重复

- 新增 `RakuPM::Fs`：递归删除 / 复制 / 遍历 / 体积的**单一实现处**。
  原先 `rm-tree` 有 4 份（Installer / Store / Cleaner / SelfManager）、
  `copy-tree` 2 份、目录遍历 5 份，各自的跳过规则与异常兜底互不一致；其中 3 份用
  惰性 `for $d.dir`，会在整个递归期间持住目录句柄，Windows 上导致调用方随后
  `rmdir` 失败（曾因此残留空目录）。现在一律**急切收集** + 统一跳过
  `.git/.hg/.svn/.precomp/blib` + 按绝对路径稳定排序。
  `copy-tree` 另加一道防呆：拒绝「目标目录在源目录内部」——那会自我复制到路径
  长度爆炸（实测 "Failed to mkdir: name too long" 并留下几百层目录）。
- 新增 `RakuPM::InstallOptions`：安装链的选项载体。7+ 个布尔开关原先要在
  install → !install-local-dist → !install-chain → !install-chain-inner →
  !ensure-phase-deps / !ensure-git-deps 共 6 层签名里各抄一遍，
  加一个开关要改 6 处、漏一处就出现「开关传了但不起作用」。
- 新增 `RakuPM::Ledger`：账本（installed.json）的**唯一**读写入口。原先同一套
  「解析 → 守卫 → 改 → 序列化」样板在 Installer 里复制了 6 处，账本格式一变就要
  同步 6 处。`Installer` 上原有公开方法保留为薄转发，外部调用方不受影响。
- `Distribution` 新增**单一构造入口** `clone-with` / `from-index-row`，取消
  `Ecosystem.fetch-distribution` 与 `Git.!distribution-from-git`（两分支）的手工
  挑字段重建 —— 那是本项目最高产的缺陷模式（resources → git-deps → builder →
  build-depends → test-depends 依次漏传，每次都造成真实故障）。

### 修复

- **native 库扫描漏报（潜在缺陷，已实证）**：`NativeLib.runtime-native-libs` 的
  逐行 POD 启发式，在 heredoc / 多行字符串正文含 `=` 行首内容时（内嵌 Markdown /
  配置片段很常见）会进入「缩写 POD」模式并吞到下一个空行，把紧随其后的真实
  `is native(...)` 声明一起漏掉 → 不报缺库 → 安装照跑 → 运行期才以
  `Slip (Empty)` / `Cannot locate native library` 崩（正是这个检查要消灭的形态）。
  修法：① POD 指令必须**顶格**（Raku 规定指令是行首第一个字符）；② 识别 heredoc
  起始（`q:to/…/`、`:to<…>`、裸 `to/…/`）并整段跳过正文。
- **两处开关漏传**：`!ensure-git-deps` 漏 `no-native-check`；
  `!ensure-phase-deps` 漏 `no-native-check` 与 `allow-test-failure`。用户给 git /
  构建 / 测试依赖加这些开关时子链不认，仍会跑本机库检查并可因此 die、把整条链
  回滚（用户明明关了开关却照样被拦）。收成 InstallOptions 后结构性消灭。
- **串行模式下复验提示语错**：默认已改为串行，提示却仍写「并发首跑的失败判定为
  假失败」，自相矛盾。改为与 jobs 无关的准确措辞。
  （复验本身不能因默认串行而删：它还要对付**上游自己 flaky** 的测试 ——
  实测 Concurrent::Stack 的 stress.rakutest 连跑 10 次挂 3 次，串行复验照样能翻盘；
  xt/flaky-retry.t 会红。）
- **CLI 未知旗标静默忽略**：选项原先是开放式接收（任何 `--key` 都进 `%opts`），
  于是 `raku-pm install Foo --no-tets` 会照常跑测试（用户以为跳过了）、
  `--verison=1.2` 被静默丢弃。现在按命令白名单校验，未知旗标报错并列出可用选项。
  顺带修掉：旗标写在**命令位之前**时，Raku MAIN 会抛一句莫名的内部错误
  （"rindex search target requires a concrete string, but got null"）——
  用 `*%early` 接住并并入 `%opts`，两个位置从此等价（原先只有命令之后才有效）。
- **既有 xt 测试失败**：`xt/dist-bump-cli.t` 硬编码 MSYS 的 `/c/…` 路径转换，但
  Windows 自带 tar 是 bsdtar（认原生 `C:/…`）→ tar 报错、成员列表为空、断言假失败。
  改用项目自己的 `Author.tar-archive-list`（自动适配 GNU / BSD 两种 tar）。

### 新增测试（守护）

- `t/distribution-fields.t`：**反射式**遍历 `Distribution` 的全部属性，断言
  `clone-with` 逐个保留；并断言 `from-index-row` 对「每个字段都有值」的索引行全部
  解析出来。以后新增字段却忘了同步这两个入口，测试立刻红。
- `t/ledger.t`（30 例）：账本主键由账本自己保证、`modify`（upsert）与
  `modify-existing` 的区别、版本 semver 排序（0.9.2 < 0.13.0）、老格式兼容、
  按版本 / 整包删除、损坏账本的可读报错。
- `xt/install-opts.t`（9 例）：用「构建依赖的源码声明了缺失的 native 库」
  「测试依赖自带的测试必挂」两个真实场景，断言用户的开关**确实传到依赖子链**
  （该文件在修复前是红的）。
- `t/cli-flags.t`（24 例）：未知旗标两种位置都报错、合法旗标两种位置都生效、
  5 个帮助入口都可用、未知命令报「未知命令」。
- `t/native-lib-scan.t` 扩到 12 例：新增 heredoc 夹具（修复前必红）。

### 文档

- 两份 README 的架构图补上 3 个新模块；版本示例同步 0.42.0。
- `CODEBUDDY.md` 补入本轮的新踩坑（heredoc/POD 扫描、InstallOptions 漏传、
  账本主键、tar 形态差异）。

## 0.41.4 - 2026-09-15
- 修复 self-upgrade 的「永远升不上去」死循环：旧版 self-upgrade 跑【旧版】扫描器
  去检查【全新】源码，而 raku-pm 自己的文档注释 / POD 里有 `is native('foo')` /
  `is native('...')` 这样的【样例】，被误当成真实依赖、报「缺少 foo/... 」而放弃
  自装，导致 0.41.3 永远装不上去。两处修正：
  - `self-upgrade` 现在【默认跳过】运行时本地库检查（raku-pm 是纯 Raku、无
    native 依赖，检查对它无意义且自我阻碍）。这是打破鸡生蛋死循环的关键。
  - `NativeLib.runtime-native-libs` 进一步跳过 **Raku POD 文档块**（`=begin/=end`
    定界块、`=for`/`=head` 等缩写块到空行结束），不再只剥 `#` 行注释；扫描器
    对任意包的文档样例都免疫（t/native-lib-scan.t 新增 POD 用例）。

## 0.41.3 - 2026-09-15
- 修复 native 库扫描器误报：逐行匹配 `is native('...')` 前先剥离 `#` 行注释，
  否则模块文档注释里的样板 `is native('foo')` / `is native('...')` 会被当成真实
  依赖，导致 `self-upgrade` 在自身（及无 native 依赖的纯 Raku 包）上误报缺少
  duckdb / foo / ... 而放弃安装。同时递归收集目录条目改为急切收集，避免 Windows
  上目录句柄泄漏导致清理时 rmdir 失败。
- 测试执行默认改为串行，对齐 zef：zef 逐个跑测试文件、不并行；此前默认并发会让
  多个 raku 子进程同时首次编译同一份 lib/*.rakumod，偶发触发 rakudo 预编译缓存
  竞态（表现为 `===SORRY!=== Error while compiling` 假失败），这正是「zef 能装、
  raku-pm 频频报错」的主因。需要速度的用户用 `--test-jobs=N`（N>1）显式并行。

## 0.41.2 - 2026-09-15
- 防御性预编译预热：并发测试前先串行 `raku -c` 写热 precomp 缓存，规避并发首跑
  多进程同时首次编译同一份 lib/*.rakumod 的竞态（可用 RAKUPM_NO_PRECOMP_WARMUP=1 跳过）。
- 面向原生 Windows 重写 tar 路径处理：`!wsl` 废弃，改 `!tar-is-gnu` / `!tar-path`
  探测原生 BSD libarchive tar 与 GNU tar 的差异（盘符路径转换），不再假设 Git Bash。

## 0.41.1
- 并发测试失败串行复验时，复验轮日志写 `<name>.flake.log` 而非覆盖首跑
  `<name>.log`，保留首跑（可能是确定性编译错误）全文用于诊断；复验通过提示改为
  “并发首跑的失败判定为假失败”，不再臆断用例本身不稳定。

## 0.41.0
- 作者侧新增 `raku-pm dist`（打包 sdist，内联可选 Build.pm）与 `bump`（版本编排）命令。
