# 原生目录扫描与普通文件恢复性能验证

本轮实现目录项缓存与 scoped restore 的可见路径检查，保留恢复前后的拓扑校验。目录扫描仍会逐个打开、核验工作区内的目录；缓存减少 `readdir`，没有把整次检查变成只访问 changedPaths 的增量操作。

## 实现与边界

- `scan-directories-v2` 在 macOS/Linux 上缓存每个目录的 `dev/ino/mtime/ctime`、Git 标记和直接子目录。每次检查仍核验全部目录的身份与时间戳，匹配时才复用目录项。检查点之间发生变化的目录重新枚举；扫描期间发生变化则显式失败，不接受部分结果。
- 缓存把 racy 标记固化到记录中，以开始枚举目录前的观察时刻计算两秒窗口。长子树扫描或之后的时间流逝不会让旧的 racy 记录自动变成可信记录。时间戳纳秒部分为零、无效或处于未来时采取保守重扫。
- 扫描继续进入 ignored 目录，以发现其中的嵌套仓库；不进入名称恰为 `.git` 的元数据目录，不跟随 symlink。目录句柄、父子绑定和工作区身份检查保留。超过 128 层时丢弃原生部分结果，由 TypeScript 从根重扫。
- v2 cache 绑定工作区路径与身份，结构解析失败或不匹配时不复用。它位于工作区外的私有临时目录，位置检查会解析 `TMPDIR` 的符号链接；无法安全放置请求时使用 TypeScript。cache 创建失败时使用本次请求目录，cache 写入失败不使成功扫描失败。正常进程退出时尽力清理，强制终止仍可能留下系统临时文件。
- 能力探测一次取得 capability 集合；旧 `scan-directories-v1` helper 继续完整扫描，缺少 helper 时使用 TypeScript。扫描仍受操作剩余时间和 `PI_UNDO_OPERATION_TIMEOUT_MS` 限制，取消或未确认退出保持原有处理契约。
- `listVisibleLeafPaths` 支持 `includePaths`。带 scope 的 complete restore 只枚举 scope 内的可见叶子；scope 外新增文件保留，scope 内新增可见路径仍会在修改前阻止恢复。未指定 scope 的完整恢复继续枚举整个工作区。嵌套仓库边界、内容、mode、symlink、mutation journal 和 quarantine 检查保留。
- 本轮没有引入 FSEvents/inotify 变更索引，也没有消除 settled 阶段完整 baseline 的枚举成本。因此，目录很多或 baseline 捕获很慢的工作区仍可能有明显等待，不能据此保证用户的 50GB 项目达到秒级响应。

缓存信任应用进程创建的私有文件及操作系统提供的目录元数据，不提供跨整个工作区的原子快照，也不提供抵抗同用户篡改 cache 的完整性保证。

## 本轮前后对比

环境：本机 macOS arm64、Node 23.11.1、Pi 0.86.1 SDK、离线 faux provider。基线为本轮改动前的 `bbe5c3f8`；基线和最终实现分别在隔离 worktree 中顺序运行相同用例，均使用原生 helper。每组执行三次 undo，中间 redo 并等待后台持久化清理，校验文件内容、历史状态和扫描次数。

| 场景 | 本轮改动前 | 本轮改动后 | 耗时降低 |
| --- | ---: | ---: | ---: |
| 10,000 个依赖包，修改 1 文件 | 2,792 ms | 2,504 ms | 10.3% |

基线三次耗时为 `[2970, 2792, 2763]` ms，最终实现为 `[2497, 2504, 2529]` ms；两组扫描次数都为 `[5, 5, 5]`。上述百分比只比较本轮前后，没有把更早版本的收益算入本轮。强制 TypeScript 回退也通过同一大型工作区用例，扫描次数为 `[6, 6, 6]`；额外一次来自无法复用原生 durable source 时重新捕获安全快照。

此前开发中一次 v2 采样为 `[2443, 2423, 2419]` ms；它与最终对比属于不同批次，不合并计算。三次本机采样只能说明这一基准的表现，没有墙钟性能断言，也没有对用户实际 50GB 项目、其他平台或其他磁盘作出速度保证。

## 上一轮原生优化的历史数据

下表保留上一轮以 `d7d80230` 为起点的结果，便于追踪原生扫描及普通文件恢复的收益；这些数据不是本轮缓存改动的前后对比。单位为毫秒。

| 场景 | 上一轮优化前 | 上一轮优化后 | 耗时降低 |
| --- | ---: | ---: | ---: |
| 无依赖目录，修改 1 文件 | 146 | 148 | 约持平 |
| 3,000 包，修改 1 文件 | 1,976 | 924 | 53% |
| 10,000 包，修改 1 文件 | 5,857 | 2,755 | 53% |
| 100 文件全部覆盖 | 179 | 182 | 约持平 |
| 100 文件在根目录新增后撤销 | 177 | 174 | 约持平 |
| 100 文件在已有子目录新增后撤销 | 517 | 180 | 65% |
| 50 文件覆盖、50 文件新增后混合撤销 | 711 | 186 | 74% |

上一轮分阶段计时中，3,000 包的五次扫描合计从 1,759 ms 降至 716 ms；10,000 包从 5,470 ms 降至 2,398 ms。另一批四组大型工作区测量覆盖 3,000／10,000 包 × 1／100 文件，中位数依次为 878、923、2,582、2,673 ms；这些独立批次不混算。

已有 `restore-files-v2` 支持父目录已存在的普通文件覆盖、创建、删除和混合计划，最多保留 128 个非根父目录句柄。目录创建/删除、类型替换、过多父目录和不支持的平台继续使用 TypeScript。durable pack 格式与恢复协议未改变：修改前持久化 pack，逐项校验内容和 mode，失败由现有 recovery 回滚并保留外来冲突文件。历史新增文件测试保留父目录中的 `keep.txt`，不包含目录删除成本。

## 复现与验证

本轮最终默认 Vitest：623 项通过、4 项按需基准跳过；Rust：12 项通过；原生相关 TypeScript：76 项通过。强制 TypeScript 回退完整测试和大型工作区用例、类型检查、npm 打包内容检查分别运行，不把跳过项算作通过。

```bash
npm run build:native
cp native/pi-undo-fs/target/release/pi-undo-fs native/bin/pi-undo-fs-darwin-arm64
npm run test:native
npm test
PI_UNDO_DISABLE_NATIVE=1 npm test
npm run typecheck
npm run pack:dry-run
PI_UNDO_LARGE_WORKSPACE=1 npx vitest run test/large-workspace.test.ts --testNamePattern='10000 个依赖包，修改 1 个文件'
PI_UNDO_DISABLE_NATIVE=1 PI_UNDO_LARGE_WORKSPACE=1 npx vitest run test/large-workspace.test.ts --testNamePattern='10000 个依赖包，修改 1 个文件'
```

二进制复制路径适用于本次 macOS arm64 环境；其他平台需要对应构建产物。本轮更新了仓库现有的 macOS arm64 二进制，未在本机验证 Linux、其他架构或 Windows 的构建与运行；发布仍依赖现有六平台构建流程。

测试覆盖目录缓存命中、深层 Git 标记新增、目录删除后的失效、racy 记录重扫、缓存身份与重复子项校验、TMPDIR 符号链接、v1/v2 协议、取消和超时，以及 scoped restore 保留范围外文件、拒绝范围内新增可见文件。独立只读审查已完成，审查提出的时间戳窗口、缓存清理和复用测试问题已修正；最终边界修正由主代理复核并重新验证。旧 helper 的混合计划仍可能先准备 pack 再回退，保留额外准备开销。
