---
title: 副本漂移清理 —— 「改了不生效」的根治
tags: [静默失败, 副本漂移, 唯一真源]
created: 2026-09-12
status: final
---

# 副本漂移清理（2026-09-12，老板批准「移走」）

## 一、问题形态

同一程序在 `~/.hermes/scripts/` 与 `~/stock_workspace/` **各有一份，内容已分叉**。
危险不在于报错，而在于**改到哪份都不确定，且表面毫无异常** ——
本次实测的两对，方向还正好相反：

| 程序 | 真正在跑的 | 死的 | 判据 |
|---|---|---|---|
| `dragon_preselect.py` | `~/stock_workspace/`（492 行，新） | `~/.hermes/scripts/`（396 行，旧） | `dragon_preselect_wrapper.sh:9` 先 `cd "${HOME}/stock_workspace"` 再 `python3 dragon_preselect.py`；`engine_verify_audit.py:53` 显式用 `WORKSPACE / "..."` |
| `system_self_heal.py` | `~/.hermes/scripts/`（199 行） | `~/stock_workspace/`（188 行） | cron job 的 `script` 字段是**裸名**；`cron/scheduler.py:2658` 把相对路径解析到 `~/.hermes/scripts/` |

## 二、我犯的错（记录以免重犯）

第一轮我只看 `wrapper` 里的 `python3 X.py` 就断言「跑的是 scripts 那份」→ **错**。
漏看了同一文件第 9 行的 `cd "${HOME}/stock_workspace"`。
⇒ **判断「哪份在跑」必须：①看 cd/workdir ②看 cron 的 script 字段是裸名还是绝对路径 ③看是否有显式 WORKSPACE 拼接。**

## 三、为什么不能用 symlink 合并（源码事实）

`cron/scheduler.py:2658-2667`：解析脚本路径后强制
```python
path.relative_to(scripts_dir_resolved)   # 越界即 Blocked
```
⇒ **指向 scripts 目录外**的 symlink 会被拒绝执行。
方向只能是「真文件放 `~/.hermes/scripts/`、另一个目录放 symlink」（如 `data_hub.py` 现状）。

## 四、处置（不删，可回滚）

```
mv ~/.hermes/scripts/dragon_preselect.py     → ~/.hermes/_dead_copies/20260912/dragon_preselect.py.scripts-dead
mv ~/stock_workspace/system_self_heal.py     → ~/.hermes/_dead_copies/20260912/system_self_heal.py.workspace-dead
+ README.md（原委 / 判据 / 取回方法 / 为什么不能 symlink）
+ 两个活文件顶部加「唯一真源声明」注释（禁止再放同名副本）
```

**验证**：
- 移走前：全量同名 .py 对比 → **只有这 2 对**不同（其余 100+ 相同或 symlink）
- 移走后：两个活文件 `py_compile` OK；`residue_audit` → 「scripts/*.py 与 stock_workspace 全部同步」✅
- 台账 14/14 无新增漂移

## 五、可复用结论

1. **同名副本 = 定时炸弹**：它不报错，只让「改动静默失效」。审计必须按 **内容哈希** 比对，不能只比存在性。
2. **判断"哪份在跑"是三个证据的合取**（cd/workdir + cron 字段形态 + 显式路径拼接），少一个就会判错（本次实证）。
3. **清理死副本的正确动作是"移走 + 备份 + 声明"**，不是删除：留取回路径，并在活文件上写死「唯一真源」，防止后人复制出第二份。


## 六、清理后的连锁反应（台账正确捕捉到，值得记）

移走死副本后，**A1 存在性台账立刻报 1 条漂移**：
```
• skill[dragon-preselect] 系统脚本不存在: ~/.hermes/scripts/dragon_preselect.py
```
两处原因、两种修法：

1. **技能里的「双路径」声明失效** → 改成唯一真源（`fugui/dragon-preselect/SKILL.md:78`）
   —— 这正是「声明即校验」的价值：**移走文件后，所有指向它的声明会自己浮出来**。
2. **我自己写的说明文字被误当活声明** → 因为我在解释里也用了规范记号
   `~/.hermes/scripts/dragon_preselect.py`。
   ⇒ **约定：规范记号 = 活声明**；历史引用/已移走的路径必须用**非规范记号**（去掉
   `~/.hermes/scripts/` 前缀，写成「scripts 目录下那份」），否则校验器会一直报缺失。

修后：A1 校验项 399 / 命中 0 ✅
