# xlings 2026.7.28.2 — 发布说明

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

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

---

## 一句话

**`self doctor --fix` 现在知道自己站在哪个 subos 里：只修这个空间的东西，说清楚别的
空间该去哪修，并且不再把「没删成」报告成「删了」。**

---

## 为什么需要它

2026.7.28.1 把 `--fix` 从「打印命令」变成了「执行修复」。那次的修复阶梯是按**单空间**
写的，而它检测的对象——versions DB 和 payload——是**全 home 共享**的。单 subos 的机器
上看不出区别；多 subos 的机器上，这个错配有四个后果，其中两个是谎报。

```
共享：  ~/.xlings.json:versions        data/xpkgs/<pkg>/<ver>/     :version 迁移标记
独占：  subos/<name>/.xlings.json      subos/<name>/{bin,lib,usr}
```

---

## 用户可见的变化

### 1. 属于别的空间的问题，不再在这里修

之前：`default` 里跑 `--fix`，一个只有 `other` 用的包 payload 坏了，也会被
`xlings install` 修好——而 `install` 的 `config()` 会把这个包注册进**当前**空间。
用户想修自己的环境，结果多了一个别人的包、一个 shim、一份 sysroot 条目。

现在：

```
✗ broken payload [subos: other]  ms-only-b@1.0.0 path …/xim-x-ms-only-b/1.0.0 missing
  → run                          xlings subos use other && xlings self doctor --fix
```

报告、归属、给出命令，不越界修，也**不计入本空间的退出码**——这个空间没引用它，
把它算成红色会让 `default` 永远失败而没有任何命令能从这里清掉。

归属判据有一条重要的例外：**没有任何空间认领的条目仍然在这里修**。0.4.69 的 home 是
旧 schema，没有 `installed[]`，所以升级用户的大多数条目都"无人认领"——写成"必须认领
才修"会把 2026.7.28.1 的迁移能力整个关掉。

### 2. 别的空间指向了不存在的版本，现在看得见

```
⚠ other subos  subos 'other' has 'ms-only-b' active at 9.9.9, which is not
               registered — xvm-subos-active-missing — run `xlings subos use other`
               then `xlings install ms-only-b@9.9.9` to restore it
```

之前这种状态只有站在那个空间里才看得到，而没有任何地方会告诉你该站过去。同样只报告
不计入退出码：修它意味着替那个空间决定该指向什么，那是 `xlings use` 的语义。

`installed[]` 里的悬挂项报为 `ⓘ`（Notice）——它不影响使用，但会让 `remove` 的引用计数
把一个已经不存在的版本算成"还有人用"。

### 3. 迁移标记不再按第一个修完的空间盖章

标记写在 home 配置里，修复发生在一个 subos 里。之前在 `default` 修完就盖章，`other`
从此再也收不到那句 `run xlings self doctor --fix` 的提示——尽管它一次都没被修过。

现在只有当**没有任何一个别的空间还有待修的 payload** 时才盖章。只算这一类，不算
`xvm-subos-active-missing`：后者 `--fix` 修不了，计进去会让标记永远落不下，重演
gcc-runtime 那次"提示永不消失"。

### 4. R3 不再报告没发生的删除

修复阶梯的 R3 是 `remove` 然后 `install`。`xlings remove` 退出 0 有四种含义，其中三种
**记录还在**：

- 版本被别的 subos 引用 → 只 detach 当前空间；
- 包不在当前空间 → 幂等 no-op；
- payload 已经不在了 → 拒绝"删除"一个装都没装的东西（而这正是 broken payload 修复的
  起点）；
- 真的删掉了。

R3 把 0 一律当成第四种，于是输出 `REMOVED but could not reinstall` —— 一句关于用户磁盘
的、不成立的断言，出现在他们最可能照做的那条消息里。

现在删完会**回读状态文件复核**这次修复涉及的条目是否真的消失，没消失就说实话：

```
✗ repair failed  ms-shared@1.0.0 — re-register failed, and `remove` exited 0 without
                 dropping ms-shared@1.0.0 — it is still registered. …
                 Take it out where it lives (`xlings subos use <name>` then
                 `xlings remove ms-shared@1.0.0`) and rerun
```

复核问的是**这次修复覆盖的发现**，不是它执行命令时用的包名：一条发现叫 `ms-shared`
（xvm 目标），阶梯装的是 `xim:ms-shared`（包），而包名不是 versions DB 的键。拿包名去
查什么都查不到，会被读成"删掉了"——第一版复核就是这么写的，它自己重现了它要防的 bug。

## 修复

| 变化 | 之前 |
|---|---|
| payload 引用计数取 project ∪ home 的并集 | 按 scope 二选一，两边不相交：两个 project subos 共用的全局包，从其中一个删会**真删掉** payload，另一个空间随即拿到一个未注册的 active 版本 |
| `~/.xlings/subos/*/.xlings.json` 只有一处遍历 | 三处，各有各的"什么算可读"，所以一个空间可以同时被 GC 算作引用、被引用计数漏掉 |

---

## 验证

- 单元 13 例：归属三分支（含"无人认领"的兼容路径）、跨空间引用的两个 code、快照读取
  （跳过损坏 JSON / `current` 符号链接）、R3 复核的三种走向。
- e2e `self_doctor_multi_subos_test.sh`（E2E-42）：7 个场景，覆盖上述四条变化 + 引用
  计数并集。
- **变异验证**：逐条把实现改回旧行为，确认对应断言失败——归属判据 → S2、迁移标记门槛
  → S3、R3 复核 → S6、引用计数并集 → S7。四个变异，四条不同的断言，没有一条是恒真的。
- 回归：`self_doctor_test.sh`（单空间行为的闸门，一个字节没改）、
  `subos_payload_refcount_test.sh`、`subos_install_remove_isolation_test.sh`、
  `remove_multi_version_test.sh`、`subos_workspace_c2_schema_test.sh`、
  project 三件套，全绿；`mcpp test` 23/23。
