# xlings 2026.7.28.1 — 发布说明

**日期**: 2026-07-28
**类型**: 发布记录 (release notes)
**关联**: `2026-07-28-seamless-upgrade-design.md`、`2026-07-28-self-repair-design.md`

> **版本号**：日常发布 `N` 从 `.1` 起，`.0` 保留给正式/里程碑版本。规范见
> `.agents/skills/xlings-contributing/SKILL.md`。

---

## 一句话

**`xlings self doctor --fix` 现在会把老客户端留下的注册记录迁移过来，而不是打印一堆让你自己粘贴的命令。**

---

## 为什么需要它

在真机上跑了一遍 0.4.69 → 最新的升级（PR #434），三个平台，装了 10 个常用包。
结论分两半：

**没坏的部分**（实测，不是假设）：老客户端装的包升级后照常可用，`gcc 15.1.0 ↔ 16.1.0`
双向切换正常（用 shim 自己的 `--version` 验的）；`self update` 本身 runner 上 3.8s、
家用网络 16.8s。

**坏的部分**：升级完的 home 上 `self doctor` 直接 exit 1，而且报的是这种东西：

```
✗ broken payload [active]  linux-headers@5.11.1 executable 'linux-headers'
                           not found in …/xim-x-linux-headers/5.11.1
  → run  xlings install linux-headers@5.11.1
```

`linux-headers` 是纯头文件包，本来就没有可执行文件。0.4.69 把它记成**一个**
program 类型的条目，带 payload 路径、里面没有可执行文件 —— 新客户端分辨不出这和
「程序的二进制丢了」有什么区别。同一台机器上五个包就报了 **56 条**。

关键在于：**它打印的那条命令其实是对的**。重跑 install 会重跑 `config()`，把包按
当前格式重新注册（1 个目标 → 12 个目标，带 bindingGroup 和成员表），条目于是被认出
是 release anchor，报错消失。但 doctor 拒绝替你执行，于是你要么手动粘 56 条命令，
要么一直看着 `self doctor` 红着。

---

## 用户可见的变化

### `xlings self doctor --fix` 会修了

| 发现 | 之前 | 现在 |
|---|---|---|
| 旧格式注册 / payload 损坏 | 打印命令，不执行 | 走修复阶梯 |
| 缺失 shim | 重建 | 重建（不变）|
| 孤儿 shim | 删除 | 删除（不变）|
| alias 无法解析 | 不动 | 不动（可能是系统命令）|
| 损坏的 binding 元数据 | `--reset-metadata` | 不变（会丢信息，必须显式要求）|

修复阶梯，逐级尝试，成功即停：

```
R2  重新注册    xlings install <pkg>@<ver> -y
                payload 还在时只重跑 config()，不下载
R3  卸载重装    xlings remove … 然后 xlings install …
                给注册层拒绝原地覆盖的记录用（xvm-legacy-payload-mismatch）
停   报告        带上失败的命令和退出码
```

R3 是兜底：**取出来再放回去永远可用**。它也是唯一可能让你更糟的一级，所以有三条约束：
索引里查不到的包不会被删（删了装不回来就不是修复）、一次只处理一个包、
删成功但装失败会单独且醒目地报出来。

### `--dry-run`

```
xlings self doctor --fix --dry-run
```

先看它打算干什么。原来的 `--fix` 承诺「绝不联网、无副作用」，修复阶梯打破了这个承诺，
所以用一个更窄的承诺替换它，而不是直接丢掉。

### 会有人告诉你该跑它

升级完之后，`install` / `use` / `list` 会打一行：

```
this home was set up by 0.4.69; packages installed then may still be
registered in its format
  run  xlings self doctor --fix
```

只在终端打、每个进程最多一次、`--fix` 成功盖章后彻底消失。不联网 —— 只是两个字符串比较。

**为什么不是 `self update` 自己修**：`self update` 跑的是**旧**二进制，修复逻辑在新的
里面。它换掉自己然后退出，之后做任何事要么还是旧代码，要么是一个它无法妥善处理失败的
子进程 —— 而此时 home 正处于半迁移状态。所以修复是用户手动触发的，新二进制只负责在
你下一次用它的时候提醒你。

---

## 修复

| 变化 | 之前 |
|---|---|
| `xlings remove <pkg>` 不再崩溃 | 目标已注册但无 active 版本时 SIGSEGV（读了已释放的 VersionDB）|
| 同形状的 use-after-free 编译不过 | `get_vinfo(Config::versions(), …)` 能编译，发过三个版本 |
| doctor 汇总不再把 binding 问题算成 payload | 「broken payloads 1」但列表里一条 payload 都没有 |
| 纯库包不再被当成「二进制丢了」 | `gcc-runtime@15.1.0` 每次都报坏、每次都修不好 |

最后一条是发布前跑 10 包模拟拦下来的，值得单独说：`gcc-runtime` 没有可执行文件，
xvm 默认把它类型化成 `program`，而原来的豁免判据是「是不是 binding root」——
`is_binding_root` 会跳过条目自身，而 `gcc-runtime` 是它自己那个 release 的唯一成员，
于是返回假。

放着不管不只是多一行噪声：`--fix` 修不掉它 → 修复轮次报告失败 → 迁移标记是按修复轮次
盖的 → 标记永远落不下 → **那句让你去跑 `self doctor --fix` 的提示，在你跑过之后还会一直出现**，
在任何装了 gcc 的机器上。这就是这次发布的核心承诺在它最常见的输入上失效。

判据改成看 payload 而不是看名字：payload 里一个可执行文件都没有 → 这个包本来就不提供程序；
有别的可执行文件、唯独少了它 → 才是真坏。后半句由 S8d 钉住（删掉二进制、在旁边留一个
别的可执行文件，必须仍然报错），并用变异验证过：把豁免判据改成无条件成立，S8d 会失败。

`xlings remove glibc` 的 SIGSEGV 是现场报的。根因是 `Config::versions()` 按值返回，
把它直接传给返回指针的 `get_vinfo()` 就得到一个指向临时量的指针。点修之外还删掉了
右值重载，同样的写法现在在所有调用点都是编译错误。

---

## 验收

不是「命令退出 0」—— 那正是要防的失败形态。实测于全新的 0.4.69 home：

两次测量，各自证明一半，合起来才是完整的：

**修复确实发生**（全新 0.4.69 home，装 gcc + mcpp，升级后用候选构建）：

```
payload findings  1 → 0 (pass 1) → 0 (pass 2)     幂等
recorded version  v0.4.69 → 运行版本               盖章
```

**规模下不留残留**（10 个常用包：llvm / node 各两版本，Linux 另加 gcc 两版本）。
这一轮先暴露了 `gcc-runtime` 那个修不掉的条目并因此**未能盖章**；修好判据后重跑，
四条断言全过、标记落下。这轮的 `before` 已经是 0，因为 home 在前一轮就被迁移过了 ——
它证明的是「不留残留、幂等、可盖章」，不是「从零修复」，那一半由上面那组数据承担。

修复结果是**重新检测**出来的，不是相信子进程的退出码：状态文件从磁盘重新加载
（修复跑在子进程里，本进程手上的是启动时读的旧副本），逐条复查。实测数据说明了
为什么必须这样：真实 0.4.69 home 上 55 条治好、1 条假失败，那一条的治法纯粹是元数据
层面的，不看磁盘就判不出来。
