# 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/` 之类),
语义不变、不再出现该字符串。
- 复核:两类痕迹在全仓残留均为 **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/`、`/Home/`、` 的 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` 读取,位置参数仍优先。
- 版本纪律: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..requires 嵌套结构都原样),缺源时退回
`module:ver` 至少保版本。新增回归 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`)。后果:① 读 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`」至少保版本。新增回归测试 `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 顺带同步文档里的身份版本示例(`: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 判断是否需要升级,再改用
【取到的新版代码】执行安装(子进程跑 `/bin/raku-pm.raku` 并 `-I /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 同款语义),包装在 ``(全局前缀)——两者
**有意分离**(锁记录「项目的依赖闭包」,包全局共享)。`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〕作者侧凭据小节**。检查 `/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=`),可直接 `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/-` 建一个独立 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=@" -H "Authorization: Zef " ` 形态
(与 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= --password=`:真实联网 `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 "`(含空格)与 `-F "dist=@"`,本环境
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 .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 `,multipart 字段 `dist`),把「生产侧发布到
真实生态」这条链路从「完全空缺」推进到「凭据 + 请求结构本地可验证」:
- 新增 `raku-pm login [--api-key=] [--username= --password=
]`:
把生态 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 /upload`、`Authorization: Zef `、字段 `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=` 或
`RAKUPM_PUBLISH_REPO`),使其立即可被 `raku-pm install` 解析。
- 复用 / 重打:`--from=` 吃现成包;默认复用 `dist` 现打(先跑 check 门禁)。
- 碰撞检查:`/-/` 已存在则拦(先 `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` 当成相对路径变成 `/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/.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]` 会**抛异常**而不是干净
失败,于是文件在第一条断言就中断、后面几条根本不跑(报告只剩半截,看不出别处有没有问题)。
加 `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 的 `/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 写的那份
(`` / `.bat` / `.ps1`),把它改名备份为 `.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#/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)` 的核心 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 // ...) if $d.meta eq 'RakuPM';`。
`eq` 是**字符串比较**,会对左操作数调 `.Str`;某些已装发行版的 meta 里没有 `name`
字段 → `Nil` → 警告。Rakudo 报的是这条语句的**起始行**,所以指到 76。
- 改法:`next unless (($d.meta // '') 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)` 不建
父目录**,`//` 这类深层路径会直接失败 —— 属于「只在
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`:`/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
- 并发测试失败串行复验时,复验轮日志写 `.flake.log` 而非覆盖首跑
`.log`,保留首跑(可能是确定性编译错误)全文用于诊断;复验通过提示改为
“并发首跑的失败判定为假失败”,不再臆断用例本身不稳定。
## 0.41.0
- 作者侧新增 `raku-pm dist`(打包 sdist,内联可选 Build.pm)与 `bump`(版本编排)命令。