# dsh-checkpoint-diff 契约（Contract）

> 本文档记录 dsh-checkpoint-diff 0.4.x 对外承诺的实际行为，供其他插件、工具与 AI 参考——它描述的是事实，不是对生态的要求。语义变更会进入 [CHANGELOG](../CHANGELOG.md) 并在插件版本号中体现。
>
> 上游 [dsh-checkpoint-rewind](https://github.com/PerryLink/dsh-checkpoint-rewind) 是快照**生产者**；本文档是**消费侧契约**——描述我们如何读它的检查点，以及本插件在"从检查点恢复工作区"这一动作上给自己划的安全底线。它不取代、也不修改 rewind 的规范（域 spec 未从 rewind 包导出，本插件在 `lib/domain.mjs` 中同构重声明）。

## 1. 检查点消费契约（Checkpoint consumption contract）

### 1.1 记录模型（`checkpoints` 域）

域 `checkpoints`，单表 `checkpoints`，键为检查点 `id`。**双版本消费**：rewind
0.4.0 使用域 version 1，0.5.0 使用 version 2——本插件按 v2 打开（介质不存在时
创建 v2），介质为 v1 时回退 v1 打开；rewind 在场时复用其已打开的域（任意版本）。
记录 schema（zod，**容错超集**：严格性属于生产者 rewind，本插件是只读消费者，
v1/v2 记录都接受）：

| 字段 | 类型 | 含义 |
|---|---|---|
| `id` | string | 检查点 id（git provider 为对象 sha，copy provider 为 UUID） |
| `sessionId` | string | 归属会话 |
| `cwd` | string | 归属工作区（绝对路径） |
| `seq` | int ≥ 0 | 会话内序号 |
| `time` | int ≥ 0 | epoch 毫秒 |
| `provider` | `'git' \| 'copy'` | 快照载体 |
| `triggerTool` | string | 触发该检查点的工具名 |
| `turn` / `step` | int > 0 | 触发位置（会话轮次/步） |
| `files` / `bytes` | int ≥ 0 | 快照规模（文件数/字节） |
| `ref` | string | provider 引用（git: 40/64 hex；copy: UUID） |
| `stepEndSeq?` | int ≥ 0 | v1/v2：步结束序号 |
| `forkSeq?` | int ≥ 0 | **仅 v1**：fork 血缘序号（v2 移除，血缘改用时间锚定，见 §1.4） |
| `kind?` | `'manual'\|'auto'\|'guard'\|'mutation'` | **v2**（rewind 必填，我们容错）：快照来源分类；`guard` 与 `triggerTool==='rewind'` 一样标记保护检查点 |
| `config?` / `tree?` / `note?` / `sessionBoundary?` | — | **v2**：rewind 的配置快照/树 sha/备注/重放边界，本插件不消费（只容忍） |

归属键 = `(sessionId, cwd)`，工作区按 `workspaceKeyOf(cwd)` 归一化（跨会话合并的基础）。

### 1.2 快照语义

- 快照是**变更前**状态：`/diff <from> <to>` 呈现 from 快照 → to 快照的差异，`to` 不含 `to` 之后的变更。
- `git` provider：未引用对象（`git stash create`/`commit-tree`），只经只读原语访问（`diff-tree`/`show`/`ls-tree`/`cat-file -e`）；ref 入参前按 `^[0-9a-f]{40,64}$` 校验。
- `copy` provider：Harness home（`$DSH_HOME`，缺失时 `~/.dsh`）下 `dsh-checkpoint-rewind/<workspaceKeyHash16>/<uuid>/` 快照目录 + manifest；ref 按 UUID 校验。
- 混合 provider 两端点配对拒绝（响亮报错），不做隐式转换。

### 1.3 寻址（Addressing）

- 节点地址：id 前缀（任意长度，最短 1 字符）或字面量 `latest`（最新节点）。
- 前缀歧义 → 报错（`is ambiguous (N matches)`），绝不静默取首条。
- 项目范围下歧义时**偏好本会话记录**。
- 节点显示名：`#短id`（8 字符）+ 相对/时钟时间 + 意图标签（如 `#a1b2c3d4 14:02 · edit README.md`）。

### 1.4 作用域（Scope）

- `scope=session`（默认）：当前会话 + 当前工作区键。
- `scope=project`：按工作区键合并全部会话，沿 `/rewind` fork 血缘（可选服务 `sessionQuery.traceSession`）组织分支；服务缺席时**退化为扁平合并**（不报错）。
- fork 标记锚点：v1 记录取父会话中带 `forkSeq` 的最后一条记录（fork 发生于其 turn/end）；v2 记录（无 `forkSeq`）取父会话中不晚于子会话 `createdAt` 的最后一条记录；父侧无记录时回退子会话首条。

### 1.5 降级矩阵（Degradation matrix）

| 情形 | 行为 |
|---|---|
| 记录被配额剪枝 / 缺失 | 时间线直接不含该节点；diff/回滚报明确错误 |
| git 快照对象被 `git gc` 回收或重克隆丢失 | 节点标记 `⚠ degraded`（只读 `cat-file -e` 探测），默认选择跳过；时间线显示 "N checkpoint(s) degraded"；diff/回滚报错**点名死节点**（如 `checkpoint #9312717a (to side) is missing … (bad object …)`） |
| 域介质为 v1 / v2（rewind 0.4.0 / 0.5.0） | 双版本打开：v2 优先，`version-mismatch` 回退 v1；rewind 在场时直接复用其已打开的域（任意版本） |
| 混合 provider 配对 | 响亮拒绝（400） |
| `sessionQuery` 缺席 | 项目范围退化为扁平合并，无分支标记 |
| 会话日志缺失 | 意图标签回退 `triggerTool` 原文（不报错） |

降级原则：**绝不删除任何记录或数据**，永远给出可行动的报错。

## 2. 回滚安全契约（Rollback safety contract）

> 以下六条是 dsh-checkpoint-diff 对"从检查点恢复工作区"这一动作**自身的承诺**，全部有测试与集成测试背书。我们认为这是同类操作应有的底线，供其他插件参考；是否采纳由各项目自行判断。

### 2.1 定位

回滚是唯一写路径，其余一律只读。回滚 = 把节点快照的文件内容写回会话工作区（整节点或单文件）。

### 2.2 不变量（Invariants）

1. **只覆盖写，绝不删除**——节点之后新建的文件保留并报告（`leftovers`），回滚不删除任何东西。唯一例外见 §2.4（撤销删除恢复自己刚创建的文件）。
2. **不越界**——不写出工作区根；拒绝穿越（`..`）与绝对路径；路径上任何一环是符号链接即拒绝；`/\.git|\.dsh/` 段在任何深度都拒绝。
3. **不碰别的存储**——绝不写快照存储、git（索引/工作树/历史）、会话；git provider 只用只读原语。
4. **git provider 前置条件**——会话 cwd 必须是仓库根（快照树路径是根相对），否则响亮拒绝。
5. **预览先行**——应用前必须能产出 dry-run 计划（将恢复/不变/跳过 + 遗留文件清单）；HTTP 端点 `dryRun: true` 与 `/rollback --dry-run` 均不写盘。
6. **每工作区串行**——同一工作区的恢复操作串行执行，不做并发交错。

### 2.3 预览与计划（Preview）

计划字段：`files[{rel, action: 'restore'|'unchanged'|'skip', reason?}]`、`restored/unchanged/skipped` 计数、`leftovers`（节点之后新建、被保留的文件）。预览状态可附带**当前工作区 → 目标快照**的逐行 diff（只读），应用前可逐文件核对。

### 2.4 单次撤销（Undo）

- 撤销最近一次恢复：被覆盖文件写回恢复前内容；**恢复新建的文件可以被删除**（"绝不删除"的唯一例外，且仅限恢复自己创建的）。
- 恢复后被改动的文件**跳过不动**（全部跳过 → 409，不做事）。
- 进程内存状态，重启失效；无 redo；删除前经过与恢复相同的路径校验。

### 2.5 为什么（理念）

| 不变量 | 对应理念 |
|---|---|
| 绝不删除 | "退得回"的底线：任何时刻都有完整现场可追究 |
| 不越界 / 不碰别的存储 | 信任边界：只承诺改变你明确授权的一个区域 |
| 预览先行 | "应用前可验证"：先看清再动手，不是事后解释 |
| 单次撤销 | 误操作有退路；撤销本身也遵守同样的安全线 |

## 3. API 表面（Stable API surface）

前缀 `/checkpoint-diff/api`（harness `webServer` 同源 JSON）：

- 读端点（GET，只读）：`/api/timeline`、`/api/summary`、`/api/file-diff`、`/api/preview-diff`（可选 `scope=project`）。
- 写端点（POST，唯二）：`/api/rollback`（dryRun 规划 / 应用）、`/api/rollback-undo`（撤销）。请求体 ≤ 64 KiB、JSON 校验。
- 错误形状：`{ok:false, error}`，状态码语义见 [README](../README.md) 的 HTTP API 表。

**稳定承诺**：寻址语义（§1.3）、A/M/D 语义、降级语义（§1.5）与回滚不变量（§2.2）不做破坏性变更；确需变更时先进 CHANGELOG 并随 minor 版本发布。

## 4. 版本与修订

- 本文档与插件版本同轨（当前 0.4.x）；契约修订记录在 [CHANGELOG](../CHANGELOG.md) 的 [Unreleased] 中累积。
- 对契约的争议请走 [Issues](https://github.com/tmpdot/dsh-checkpoint-diff/issues)；安全相关走 [SECURITY.md](../SECURITY.md) 的私有上报。
