---
name: safe-path-migration
description: 根目录改名/移动后，安全批量更新全树绝对路径引用。只替换「路径形式」的旧根前缀，保留品牌语义名、GitHub URL、历史旧根引用；规避备份目录递归陷阱；用 robocopy /MIR 强制清空长路径深层残留。适用于任何「D:\OldRoot → D:\NewRoot」或「嵌套到新父目录」的目录重构。
---

# safe-path-migration — 安全路径引用迁移

当需要重命名或移动一个被大量文件硬编码引用的根目录时，盲目全局替换 `OldRoot → NewRoot` 会破坏品牌语义、仓库 URL、历史文档。本技能给出经过实战验证的安全流程。

## 何时使用
- 根目录改名（如 `D:\OldRoot → D:\NewRoot`）。
- 目录整体迁移/嵌套（如把 `A`、`B` 两个顶层目录移入 `D:\Parent\`，变成 `D:\Parent\A\…`）。
- 任何「大量文件含旧绝对路径、需重定向」的场景。

## 核心原则：区分三类字符串，绝不盲替
对扫描到的每个 `OldRoot` 出现，先判定类别：
1. **路径形式** `D:\OldRoot\…` 或 `D:/OldRoot/…` → 按映射表替换（这是要改的）。
2. **品牌语义裸名** `OldRoot`（不跟路径分隔符，如「OldRoot 平台」）→ **保留不动**。
3. **仓库/站点 URL** `github.com/user/OldRoot`、`username.github.io/OldRoot` → **保留不动**（由仓库名决定，不随本地文件夹变）。
4. **历史旧根** `D:\OldRoot_OldSuffix\…`（不同字符串，如下划线旧命名）→ **保留不动**（属历史记录）。

判定靠正则：只匹配 `D:[\\/]OldRoot[\\/]`（要求紧接路径分隔符），天然排除上述 2/3/4。

## 步骤
1. **枚举首段，建立证据化映射**（不要猜）：
   `grep -rhoE 'D:[\\/]OldRoot[\\/][^\\/[:space:]"'"'"'()<>]*' ROOT | sed 's#...##' | sort | uniq -c`
   看 `OldRoot/` 之后第一层目录是什么，据此决定每个首段重定向到哪：
   - 仍在原仓库内的 → `NewRoot\同段`
   - 已迁出的子目录（如 `Assets`、`Workspace` 等已移到别处的）→ 正确新位置
   - 已改名的子目录 → 新名称
   映射必须**最长优先**（先匹配 `Projects\SubProject` 再匹配 `Projects`），避免短路。
2. **写替换脚本**（Python），要点：
   - 扫描多个 root（`os.walk`），`EXCLUDE_DIRS = {'.git','.workbuddy','node_modules','__pycache__',...}`。
   - 备份根目录必须放在**所有扫描 root 之外**（如 `D:\_pathfix_backup\`），且在 walk 中对含 `_pathfix_backup` 的子串做 `dirnames[:] = [d for d in dirnames if '_pathfix_backup' not in d]` 排除——**否则备份副本会被自身递归，路径无限嵌套、崩溃**。
   - 替换逻辑：对每行依次应用映射（路径分隔符 `\` 与 `/` 都兼容）；只改路径形式，品牌/URL/历史不动。
   - 默认 dry-run 打印报告；`--apply` 才写文件（写前 `shutil.copy2` 备份到树外）。
3. **先 dry-run**，人工核对报告：确认每条 `+` 新路径合理、无 `+` 行残留 `OldRoot`（残留必为历史 `OldRoot_Suffix` 之类，确认 benign）。
4. **`--apply`**，然后复验：
   `grep -rnoE 'D:[\\/]OldRoot[\\/]' ROOT | grep -v '/.git/' | grep -v '/.workbuddy/' | grep -v '_pathfix_backup'` → 应为 **0**。
5. **清理临时产物**：删脚本、报告、树外备份、`D:\_empty_del`（若有）。

## 致命坑：长路径深层残留删不掉
`rm -rf`、Python `shutil.rmtree`、甚至 `\\?\` 前缀在某些沙箱/虚拟化文件系统上会因 260 字符路径限制**静默失败或报 WinError 123**，导致损坏的嵌套备份目录删不净。
**解法（已验证）**：用 `robocopy /MIR` 镜像一个空目录来清空——
```
python -c "import os; os.makedirs(r'D:\_empty_del', exist_ok=True)"
robocopy "D:/_empty_del" "<目标深层目录>" /MIR /R:0 /W:0 /NFL /NDL /NP
rm -rf "<目标深层目录>"
rm -rf "D:/_empty_del"
```
robocopy 是 LongPathAware，能删穿深层嵌套。空目录源用 Python 在目标盘建（避免 Git Bash `/tmp` 映射问题）。

## 第二轮迁移（嵌套到新父目录）
当把 `A` 和 `B` 移入 `Parent`（`Parent\A\…`、`Parent\B\…`）时，映射变为：
- `D:\A\…` → `D:\Parent\A\…`
- `D:\B\…` → `D:\Parent\B\…`
注意是**二级路径** `Parent\A\…`，不是旧根 `Parent\…` 直接裸用。复用本技能的脚本时仅需改映射表与 root 列表（root 改成 `D:\Parent` 或分别扫 `D:\A`、`D:\B`）。

## 不适用 / 注意
- 工具自动生成的运行产物（`runs/latest/*.json` 等）内的旧路径，属重算产物，不手改。
- 私有仓库内的历史/结构文档若把旧根作为「历史记录」叙述，按类别 4 保留。
- 改完后务必 `git status` 确认改动面符合预期。
