# Changes

## 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`（版本编排）命令。
