# ws_mod：文本编辑统一工作流（prepare-first + single apply）

你拥有 rtws（运行时工作区）读写能力，但**所有增量文本编辑必须先 prepare，再 apply**：先生成可复核的 diff/evidence + `hunk_id`，再显式确认写入。

## 总原则

- 增量编辑：通过 `prepare_*` 生成可 apply 的 hunk；然后用 `apply_file_modification` 写入。
- LLM 顺序硬约束：`prepare_*` 只生成内存中的预览，不会写盘；在 `apply_file_modification` 之前再次读取文件仍只能读到旧内容。若要基于本次改动继续修改，必须先 apply 当前 hunk，再重新 read/prepare 新改动。
- 旧工具已移除（无兼容层）：`append_file` / `insert_after` / `insert_before` / `replace_block` / `apply_block_replace`。
- 约束：`*.tsk/` 下的路径属于封装差遣牒，文件工具不可访问。
- 并行约束：同一轮对话中的多个工具调用可能并行执行；**prepare → apply 必须分两轮**（除非未来有顺序编排器）。
- 输出以 YAML + unified diff 为主：低注意力可复核（`summary` + `evidence`/`apply_evidence`）。
- 规范化：所有写入遵循"每行以 `\n` 结尾（含最后一行）"；EOF 换行会被补齐并通过 `normalized.*` 字段呈现。
- 例外：`overwrite_entire_file` 是"整文件覆盖写入"的函数工具（会直接写盘，不走 prepare/apply）。它要求提供 `known_old_total_lines/known_old_total_bytes` 作为对账护栏（建议从 `read_file` 的 YAML header 读取 `total_lines/size_bytes`）；`content_format` 可填写任意非空文本标签（例如 `yaml`），但若正文疑似 diff/patch，仍只有显式声明 `content_format=diff|patch` 才会放行。仅用于"新内容很小（例如 <100 行）"或"明确为重置/生成物"的场景；其他情况优先 prepare/apply。
  - 复制参数建议：对账参数请直接用 `read_file` 的 `total_lines/size_bytes`。
- 例外：`create_new_file` 只负责"创建新文件"（允许空内容），不做增量编辑、不走 prepare/apply；若文件已存在会拒绝（避免误用覆盖写入语义）。
- 二进制图片工具：用 `read_picture({ path })` 把 PNG/JPEG/WebP/GIF 图片作为真实图片上下文读入；用 `write_picture({ path, data_base64, mime_type, overwrite })` 从 base64 写图片。它们是二进制图片操作，不走 prepare/apply。

## 该用哪个 `prepare_*`

- 精确范围改动（行号范围）：`prepare_file_range_edit({ path, range, content, existing_hunk_id })`
- 末尾追加：`prepare_file_append({ path, content, create, existing_hunk_id })`
- 锚点插入：`prepare_file_insert_after|prepare_file_insert_before({ path, anchor, content, occurrence, match, existing_hunk_id })`
- 双锚点块替换：`prepare_file_block_replace({ path, start_anchor, end_anchor, content, existing_hunk_id, occurrence, include_anchors, match, require_unique, strict })`
  - `include_anchors: true`（默认）：**保留 anchor 行**，仅替换两者之间的内容（start/end 行不被删除）。
  - `include_anchors: false`：替换范围**包含** start/end anchor 行（会删除并以新内容替换）。
- 创建新文件（允许空内容）：`create_new_file({ path, content })`

> 可选字段默认可省略。
> 若你想显式传入“未指定/默认”，支持以下哨兵值写法：
>
> - `existing_hunk_id: ""` 表示不覆写旧规划（生成新 hunk）。
> - `occurrence: ""` 或 `0` 表示不指定 occurrence（当候选不唯一时会被要求显式指定）。
> - `match: ""` 表示默认 `contains`（注意：`match` 是匹配模式，不是要匹配的文本/正则）。

## hunk id 规则（重要）

- `prepare_*` 会生成 `hunk_id`（TTL=1 小时）；apply 只能用仍然存在的 hunk。
- 过期/未使用的 hunk **不会产生任何副作用**，会在运行时自动清理；只需关注"最后一次准备的那个 `hunk_id`"。
- 部分 prepare 工具支持 `existing_hunk_id` 作为"覆写同一 prepare"的方式；**不支持自定义新 id**。
- 若只是修订同一个未落盘预览，可用同一 prepare 工具配合 `existing_hunk_id` 覆写；若想基于这次改动继续做下一笔修改，必须先 apply 当前 hunk，再重新 prepare。

## apply 语义（context_match）

- `exact`：文件内容与 prepare 时一致。
- `fuzz`：文件已被修改，但仍能安全应用；此时输出会给出 `file_changed_since_preview` 与（planned/current）digest 便于复核。
- `rejected`：无法唯一定位或不安全；必须重新 prepare。

## 两步模板（复制即用）

1. Prepare（返回 `hunk_id` + unified diff）：

```text
按以下参数调用函数工具 `prepare_file_insert_after`：
{ "path": "docs/spec.md", "anchor": "## Configuration", "content": "### Defaults\\n- provider: codex\\n" }
```

2. Apply（必须单独一轮/单独一步）：

```text
按以下参数调用函数工具 `apply_file_modification`：
{ "hunk_id": "<hunk_id>" }
```

在这一步之前，prepare 结果还没有落盘；如果此时再次 `read_file`，读到的仍是旧内容。

## 示例

- 末尾追加：

```text
按以下参数调用函数工具 `prepare_file_append`：
{ "path": "notes/prompt.md", "content": "## Tools\\n- Use prepare_* + apply_file_modification for incremental edits.\\n" }
```

- 行号范围替换（`content` 可为空字符串表示删除）：

```text
按以下参数调用函数工具 `prepare_file_range_edit`：
{ "path": "README.md", "range": "10~12", "content": "New line 10\\nNew line 11\\n" }
```

- 双锚点块替换：

```text
按以下参数调用函数工具 `prepare_file_block_replace`：
{ "path": "docs/spec.md", "start_anchor": "## Start", "end_anchor": "## End", "content": "NEW BLOCK LINE 1\\nNEW BLOCK LINE 2\\n" }
```

## 常见失败与下一步

- `ANCHOR_AMBIGUOUS`：锚点多次出现且未指定 occurrence；请指定 occurrence 或改用行号范围（`prepare_file_range_edit`）。
- `ANCHOR_NOT_FOUND`：锚点未找到；必要时先 `read_file` 或用 `ripgrep_snippets` 定位。
- apply `context_match: rejected`：文件已被修改导致无法唯一定位/不安全；请重新 prepare（缩小范围或增加上下文）。
- apply 失败：输出会包含失败原因与关键诊断信息；按提示重新 prepare（必要时缩小范围、增加上下文或指定 `occurrence`）。
