# Changelog

## 0.2.12

- 修复读取版本竞态：包装官方 `read`，直接对实际读取的 Buffer 计算 hash，不在 `tool_result` 阶段重新读取磁盘。大文件不再仅依赖 size/mtime/inode；同大小、同 mtime 的内容替换也会被拒绝。
- `write` 的已知版本绑定输入 `content`，不把其他进程随后写入的字节当成模型已知内容。补齐新建普通/空文件后立即按行编辑的回归测试；明确 shell 创建、上下文重置和 `write + then_run` 整体失败仍需 `read`（即使文件已被创建）。
- 编辑增删行后，可以直接使用最新成功报告实际展示的新文件行号；省略改动块、截断区域、删除旧行及跨未展示区域的范围仍需 `read` 或 `oldText`。外部变化仍使视图失效，`dryRun` 不授权新坐标。
- 修复混合 LF/CRLF 文件中，行号定位配合 CRLF `newText` 生成双 CR（`\r\r\n`）的问题；保留混合文件中精确 `oldText` 的字面换行语义。
- 修复末行无换行符时，相邻行区间删除共享分隔符被误判重叠；真正的重叠和 `oldText` 冲突仍拒绝。

## 0.2.11

修 0.2.10 里我自己引入的计数错误。

### 合并告警的换行数说错了

0.2.10 的 note 按「合并次数」计数，却写成了「少几个换行」。四行合并成一行时丢 3 个换行，
提示却说 `1 fewer line break(s)`。现在改为累计实际丢失的换行数，那个例子会说 `3`。

这是发布后用真实工具测试发现的：`a\nb\nc\nd` → `abcd` 时提示数字对不上。

## 0.2.10

新增一条提示，行为不变。

### 两行被粘成一行时会明确告警

从真实会话里发现的失败模式：`oldText` 以 `\n` 结尾、`newText` 不以 `\n` 结尾，于是**下一行被粘到这一行末尾**。
编辑“成功”了，diff 也写了 `+1 -2 lines`，但没有一句话说明原因；模型随后就把粘好的行当成基准写了下一处，
结果删掉了一个函数签名。

现在这种情况会多一条 note：

```
note: edit #1: newText has 1 fewer line break(s) than the text it replaces, so lines were joined.
      If that was not intended, end newText with a newline or keep the original line structure.
```

**不会误报删除整行**。删行也会减少换行数，但它前后两行本来就是分开的、删除后依然分开，不算合并。
判据：替换后换行数减少、匹配点后面还有内容、且 `newText` 不以换行结尾且非空。

## 0.2.9

只改一处提示文案，行为不变。

### 行号剥离的 note 不再笼统说 `oldText/newText`

`oldText` 带行号被剥离时，提示原本固定写：

```
stripped accidental line-number prefixes ("12│") from oldText/newText.
```

但 `newText` 现在只在行号与 `oldText` 连续时才被剥，所以“newText 也被剥”并不总成立。
例如 `newText` 根本不含行号时，这个提示会误报。现在按实际情况区分：

- `newText` 确实被剥 → `from oldText and newText.`
- `newText` 未被动 → `from oldText.`

顺带把两处旧测试的断言从「包含 `stripped accidental line-number prefixes`」收紧到具体断言哪一侧，
否则改文案后它们仍会通过，失去守护力。

## 0.2.8

修 code review 里三项经实证确认的问题。前两项会真实影响写入结果。

### 1. newText 的行号前缀不再吃掉合法代码

`oldText` 带行号（如从错误提示里抄的 `   2│const b = 2;`）时，`newText` 也会被剥离行号。
旧规则把 `数字 + 空白 + |` 一律当行号，于是这种新写的合法代码会被破坏：

```ts
// 文件: "flags = (A |\n        B)"
oldText: "   1│flags = (A |\n   2│        B)"
newText: "   1│flags = (A |\n        1 | B)"   // 位运算续行
→ 旧结果: "flags = (A |\n B)"                  // "        1 |" 被当行号吃了
→ 新结果: "flags = (A |\n        1 | B)"       // 保留
```

新规则：`newText` 的行号只有在**与 oldText 实际匹配到的行号连续递进**时才被剥离。
真正的行号块照旧剥离（包括模型把 `│` 打成 ASCII `|` 的情况），而代码里孤立出现的
`1 | B)` 不会与那个序列对上。`oldText` 一侧维持宽松（它要求每行都有行号，不会有歧义）。

### 2. `replaceAll` 与 `nearLine` 同时给出改为报错

二者语义互斥：`replaceAll` 要改全部，`nearLine` 要挑一处。旧行为是 `replaceAll` 静默胜出，
于是只想改一处的调用会把所有匹配都改掉。现在直接报错并说明该保留哪个。
`replaceAll: false` + `nearLine` 仍然合法。

### 3. 大文件视图提前升级为精确哈希

`execute()` 本来就要读文件并算 sha1；视图仍有效时现在立即记录该哈希（`index.ts`），
而不是只在写入成功后。`View.hash` 的注释也改为与实际行为一致。

这一项没有可观测的行为差异（指纹包含 mtime/ino，外部改动会先被指纹拦住），
属于收紧内部状态与修正失实注释，因此没有为它编造测试。

## 0.2.7

两处错误文案修正，行为不变（重叠仍被拒绝）。

### `replaceAll` 自重叠时说清楚真正原因

`oldText` 在文中自我重叠时（如 `aa` 出现在 `aaaa` 里），报错原本是：

```
Edit #1 (line 1) and edit #1 (line 1) overlap.
```

同一个编辑跟自己比较“重叠”读起来像工具 bug，也掩盖了真实原因。现在改为：

```
Edit #1: oldText itself overlaps inside the file (lines 1 and 1).
Use a longer oldText that cannot match twice in a row, or drop replaceAll and target one occurrence.
```

两个**不同**编辑真互相重叠时仍用原文案，那是它该说的话。

### 不再给出一定会失败的 `replaceAll` 建议

候选自重叠时，歧义提示原本仍写 `- replaceAll: true to change all N occurrences.`。但 `findExact`
故意保留重叠候选（`engine.ts:452`），应用阶段必然拒绝，所以这条建议会把模型直接送进**第二次同样的失败**。
现在仅在候选互不重叠时才提供该建议。

非重叠的普通歧义（两次 `dup` 在不同行）照旧给出建议，`replaceAll` 正常工作。

## 0.2.6

仅补元数据，代码与 0.2.5 完全相同（`engine.ts`/`index.ts`/`writer.ts` 未改动）。

### 加入 Pi 包画廊预览图

`package.json` 的 `pi.image` 指向 `assets/preview.png`（1200x630，经 96 色量化，48KB）。Pi 的包画廊
（https://pi.dev/packages）会把它渲染成卡片预览。

图片通过 jsDelivr 从 npm 包内取，而不是 GitHub raw：源仓库 `bjstan1979/pi-extensions` 是私有的，
`raw.githubusercontent.com` 对匿名访问返回 404，画廊拿不到图。jsDelivr 直接服务 npm tarball 内的文件，
因此 `assets/preview.png` 必须同时列入 `files`，否则不会被打进包。

URL 钉在版本号上（`pi-agent-edit@0.2.6`）而非 `@latest`：后者会被缓存且可能在版本切换时短暂 404。

## 0.2.5

第二轮：修 0.2.4 报告里明确留下的四项。引擎输出与上一版一致（差分 224 + 720 + 36 组 0 差异）。

### 中断（abort）检查改为在每个 await 之后

0.2.4 只在写入前检查一次 `signal.aborted`。后果有两个：

- 写入过程中被中断**不会被发现**，工具照常返回成功——但调用方已经认为它失败了。
- 读取阶段就被中断时，这次编辑要白跑完全程（48MB 文件上是数秒）。

现在按 Pi 内置 edit 的写法，在每个 await 之后检查（`resolveExisting` / `stat` / `readFile` / 写入前）。**写入成功后刻意不再检查**：此时文件已经改了，若报 abort 会让模型以为没改而对着旧内容重试。新增两个用例分别覆盖“写前中断什么都不写”与“写中中断不误报失败”。

### 读取/写入后的视图追踪不再全量读文件

`tool_result` 处理器原先对每次 read/write 结果都 `fs.readFile` + sha1，只为记下“模型看到的是这一版”。48MB 文件上每次 45–78ms，且完全阻塞事件循环。

现在 2MB 以下仍记精确 sha1（本来就不慢），更大的文件只记 `size:mtimeMs:ino` 指纹（48MB 上 **0–1ms**）。`execute()` 本来就要读文件，因此它比对的仍是精确 sha1；两者结合，跨“模型读取 → 本次编辑读取”这个窗口仍然抓得住同尺寸改写（mtime 保留亚毫秒精度）。新增用例覆盖：同尺寸外部改写被拒绝、大文件不再被额外读。

### `replaceAll` 字符串值不再被当成 true

Pi 的 schema 转换把任意非空字符串转成 `true`，于是 `replaceAll: "false"` 实际是**全量替换**——模型明确要求只改一处，结果全改了。现在 `"false"/"no"/"0"` → `false`，`"true"/"yes"/"1"` → `true`，其他字符串留给 schema 报错。

### `decode()` 快路径与计数重写

decode() 每次编辑跑两遍。纯 LF 文件（绝大多数源码）原先要走完整的逐字符 CRLF 计数，结果必然是不需要归一化。现在：

- 没有任何 `\r` 时直接返回（`indexOf` 原生扫描），25MB 从 **48ms → 1ms**；
- 有 CRLF 时改用 `indexOf("\r\n")` 计数，25MB CRLF 从 **134ms → 95ms**。

### 仍然存在的限制

- 大文件（>2MB）的视图指纹依赖 mtime。文件系统 mtime 粒度低于枚举精度、或有人刻意用 `utimes` 把时间改回原值时，指纹会失效；这是为每次 read/write 省 45–78ms 付出的代价，且有哈希的场景不受影响。
- **Node 20 不再被 CI 覆盖**：Pi 的 peer 依赖 `@earendil-works/pi-coding-agent` 自己声明 `engines.node >= 22.19.0`，并从 `node:fs` 具名导入 `globSync`（Node 22 新增）。`package.json` 的 `engines` 因此从 `>=20.0.0` 改为 `>=22.19.0`。纯引擎（`engine.ts`/`writer.ts`）本身仍能在 Node 20 上跑。

## 0.2.4

针对一次代码审查提出的崩溃、性能与工程化问题。语义未变：新旧引擎在 981 组差分用例上输出、报错文本、`notes` 完全一致。

### 崩溃修复（大文件上原本直接抛 `RangeError: Maximum call stack size exceeded`）

两处把大数组展开成函数参数，匹配数一多就把栈撑爆：

- `applyEdits` 里 `ranges.push(...resolveEdit(...))`：`replaceAll` 命中超过约 10 万处即崩溃。200k 行文件上 `oldText: "const "` 原为 `RangeError`，现为 289ms 正常完成（25 万处命中）。
- `nearLine` 消歧用 `Math.min(...d)`：候选行超过约 12.5 万行即崩溃。改为 `reduce`。

这两处原先抛出的是裸 `RangeError`，不经过 `EditError` 包装，模型看到的是「Maximum call stack size exceeded」而不是编辑错误说明。

### 性能

- **`byEdit` 汇总从 O(n²) 改为 O(n)**：`[...(byEdit.get(k) || []), r]` 每命中一次就整个数组拷贝一次。80k 处命中由 25921ms 降到原来的若干十分之一（同规模下 P0 修复后 `replaceAll` 为 235ms）。
- **失败诊断的 bigram 扫描复用单个 `Map`**：原先为文件每一行分配一个短命 `Map`，48MB/70k 行文件上 GC 占 48%。改为一个 `Map` 逐行 `clear()` 重填，评分逻辑与结果完全不变。
- **`LineIndex.normLines(level)` 归一化结果按层级缓存**：与已有 `indentInfo` 惰性缓存同风格。失败路径不再每次重算全文件归一化。
- **诊断路径的 `findExact`/`locate` 增加可选 `cap`**：只在「newText 是否已存在于文件」这一处诊断用 `cap = 2`（只需区分 0/1/多处）。编辑路径保持 `Infinity`——给 `replaceAll` 加封顶会静默少改，这一点被明确拒绝。

实测（同一进程内对照 HEAD 版本，`oldText` 不存在即走完整失败诊断）：

| 文件 | 场景 | 0.2.3 | 0.2.4 |
| --- | --- | --- | --- |
| 13.6MB / 200k 行 | 1 次失败编辑 | 2516ms | **1555ms** |
| 13.6MB / 200k 行 | 4 次失败编辑 | 10111ms | **3742ms** |
| 48MB / 700k 行 | 1 次失败编辑 | 11051ms | **6208ms** |
| 48MB / 700k 行 | 4 次失败编辑 | 38402ms | **12238ms** |

### 参数归一化

- `prepareArguments` 不再静默丢弃顶层 `oldText`/`newText`：当调用同时带 `edits` 和顶层 `oldText`/`newText` 时，顶层那对按「追加一个 edit」处理（与 Pi 内置 edit 的 `prepareEditArguments` 一致）。0.2.3 会把这部分内容无提示丢掉，模型的改动就此消失。`edits: []` 加顶层对同样不再变成空操作。
- `edits` 是无法解析的字符串且又带顶层 `oldText`/`newText` 时改为明确报错，不再猜。

### 路径解析

- `@` 之外，被 `"…"`、`'…'`、`` `…` `` 包裹的路径也会去壳重试（`"@src/app.ts"` 这种组合也覆盖）。字面名仍优先，所以名字里真带引号的文件照旧可用。

### 工程化

- `package.json` 补 `repository`、`engines`（当前为 `node >= 22.19.0`，跟随 Pi peer 依赖）、`typecheck` 脚本；`prepublishOnly` 现在跑 `npm test && npm run typecheck`。
- 新增本地 `typescript` devDependency（^5.9.3）。此前 `tsc` 只来自全局安装，CI 与本地结果不可复现。
- 新增 CI：`.github/workflows/pi-agent-edit.yml`，对 `pi-agent-edit/**` 的 push/PR 在 Node 22/24 上跑 `npm ci` + `typecheck` + `test`。
- README / README.zh-CN 的开发命令改为 `npm run typecheck` 并说明 CI。

### 仍然存在的限制

- 上述 diff 基线是 0.2.3 已发布的内容；可复现的差分脚本未随包发布。
- `findExact` 的 `cap` 是内部参数，不是公开 API 的一部分。

## 0.2.3

### oldText 多处匹配时不必重新读取

0.2.2 及之前只报 `matches 3 places (lines 2, 6, 10)`，模型看不到这几处各自的内容，通常只能重新 read。现在报错包含：

- **每处出现的上下文**：前后各一行，带 `   12│` 行号（与本工具其他输出格式一致，误带进 oldText 时会被自动剥离）；超长的多行匹配中间折叠。最多展示 5 处上下文，匹配行号最多列出 12 个。
- **每处的“唯一 oldText”**：从文件原样截取，逐步向上、向下扩展整行（最多 6 行、400 字符），直到在全文中恰好出现一次；以 JSON 字符串给出，可直接复制。提示模型把扩展进来的上下文行原样放进 newText。实在无法唯一时改为提示用 `nearLine`；同一行多个匹配会说明行号无法区分，需在唯一上下文中只替换目标出现位置。某个扩展窗口超长时仍尝试其他方向的窗口。
- **报错中的行号可以立即作为 `nearLine` 使用**：这些行号是对当前文件内容算出来的，所以即使已读视图过期（从未 read、外部修改后、行数变化后），只要文件哈希与报错时一致、且 `nearLine` 恰好等于报错列出的某个匹配行，就接受；文件一旦变化即失效，猜测的近似行号仍会被拒绝。会话切换等事件同样会清空。

用例在 `test/hardening.engine.test.ts`、`test/hardening.extension.test.ts`。

### 行为变化

- `EditError` 新增可选字段 `nearLineOffers`，`ApplyOptions` 新增 `offeredNearLines`。
- 多处匹配的报错文本变长、格式变化；按旧文本做精确匹配的调用方需要调整（`matches N places (lines …)` 这一句保持不变）。

## 0.2.2

针对 0.2.1 审查后保留的四项风险。新用例在 `test/writer.test.ts`、`test/hardening.engine.test.ts`、`test/hardening.extension.test.ts`。

### 写入（新模块 `writer.ts`，不依赖 Pi）

- **并发修改不再被静默覆盖**：`atomicWrite(target, content, { expectedHash })` 在临时文件写好并 fsync 之后、rename 之前，重新读取目标并核对哈希；原地写路径则在同一个文件描述符上读出、核对、写入。不一致时抛出 `ConcurrentModificationError`，什么都不写，`edit` 同时清除该文件的已读视图，迫使模型重新读取。0.2.1 中，读取后到写入前被其他进程修改的内容会被本次编辑覆盖。
- **原地写改为可恢复**：0.2.1 用 `fs.writeFile`（O_TRUNC）原地写，写到一半失败会留下被截断或半写的文件。现在：不截断地覆盖写 → `ftruncate` 到新长度 → fsync；原始字节保留在内存，并以 0600 权限备份到系统临时目录。任何一步失败都会写回原文；回滚也失败时，错误信息给出备份路径。成功后删除备份。
- **临时文件在 rename 前 fsync，rename 后对目录做尽力 fsync**；临时文件 fsync 失败按存储错误处理，原文件不动。
- 硬链接、无法 chown、rename 被拒（EXDEV/EBUSY 等）这几条原地写路径全部改用上述可恢复写法。

### 容错匹配

- **非精确匹配时，未改动的行原样保留**：`newText` 中与 `oldText` 某行完全相同的行（按 LCS 对齐，插入或移动其他行不影响配对），写回文件中原来那一行，而不是模型输入的版本。0.2.1 中，宽松匹配会把未改动行里的连续空白（包括字符串字面量内部）、弯引号、全角标点改成模型输入的形式；trailing-ws 匹配会去掉未改动行的行尾空白；Makefile 中未改动的 recipe 行也会经过重缩进。混合换行文件中被保留的行同时保留自己的 CRLF。
- **空白敏感文件给出警告**（`whitespaceWarnings`，结果开头的 `WARNING:` 行，dryRun 也会显示）：Python/YAML/Makefile 中走了 indent/loose 匹配；Python 新增行 tab/空格混用或与文件风格不一致；YAML 新增行用 tab 缩进；Makefile 中紧跟规则头或 recipe 行、却以空格开头的新增行。只检查本次新增的行，不报告文件里原有的问题。

### 行为变化（可能影响调用方）

- `atomicWrite` 移到 `writer.ts`，`index.ts` 仍然重新导出；新增可选第三参数 `{ expectedHash }`，不传时行为与之前一致（但原地写已改为可恢复写法）。
- `writer.ts` 导出 `sha`、`writeInPlace`、`ConcurrentModificationError`；`engine.ts` 导出 `whitespaceWarnings`。
- 非精确匹配下，未改动行的空白不再被规范化（见上），依赖旧行为“顺手清理空白”的调用方会看到差异。

### 验收补充

- 大块容错编辑超过 LCS 预算时，先剥离相同首尾行；剩余对齐仍超过预算则拒写，要求拆分编辑或提供精确原文，不再按位置回退而静默改变字符串空白。
- 原地写在备份完成后再次核对同一文件描述符的内容，拒绝覆盖备份 I/O 期间发生的修改。
- lockfile 版本同步至 0.2.2；旧 ToolCall 测试补齐类型字段，完整类型检查通过。

### 已知限制

- 核对与 rename 之间仍有竞态窗口（POSIX 没有比较后 rename 的原子操作）；这不是文件锁，也不阻止别的程序之后覆盖。
- 原地写可恢复但不是原子的：进程被强杀或断电时来不及回滚；最终复核之后及写入/回滚期间仍可能与其他写入者竞争，不提供文件锁。
- NFS/SMB 等网络文件系统上的 rename/fsync 语义无法由本地测试证明。
- 空白检查是文本启发式，不理解语法；关键修改仍需看 diff、跑编译或测试。
- 原子写入产生新 inode，ACL 和 xattr 不会被复制（同 0.2.1）。

## 0.2.1

每条修复在 `test/regression.engine.test.ts` 或 `test/regression.extension.test.ts` 里都有对应用例，注释写明了 0.2.0 的错误行为。

### 会静默写错或损坏文件的问题

- **非 UTF-8 文件被整体损坏**：0.2.0 以宽松 UTF-8 读取，GBK、Latin-1 等文件的每个非 ASCII 字节都会变成 U+FFFD 并写回，连未编辑的行也不例外。现在用严格解码，失败时拒绝编辑并提示先转码。二进制检测改为在原始字节上进行。
- **行号陈旧保护只比较总行数**：一次编辑在顶部 +1 行、底部 −1 行时总行数不变，保护被绕过，后续 `startLine` 改错行。现在要求每个改动块的行数都不变（`ApplyResult.linesShifted`）。
- **"已应用，勿重复"误报**：删除型编辑（newText 是 oldText 的一部分）或 newText 很常见（如 `}`）时，会误报"已应用"并且不再给出候选区。现在只在 newText 唯一存在、且不包含于 oldText 时才提示，并且无论如何都会给出最接近的候选区。
- **newText 部分行带行号前缀时，前缀被写进文件**：oldText 带前缀而 newText 混有手写新行时，0.2.0 会把 `    12│` 原样写入。现在逐行剥离带前缀的行。

### 中等

- **重缩进产生 tab/空格混排**：0.2.0 只替换参考行的前缀，其余空白原样保留，例如 `    \treturn`，在 Python 中会触发 TabError。现在根据 oldText 与文件逐行的缩进对应关系计算映射，整段重建前导空白并统一使用文件自己的风格。当 oldText 体现出缩进比例时（例如 2 空格对 4 空格），newText 中更深的层级会按同样比例换算。
- **`nearLine` 不受陈旧保护**：行号偏移后，`nearLine` 会静默选中另一处匹配。现在行号不可信时，若需要用 nearLine 在多处匹配中做选择，会拒绝并说明原因；唯一匹配不受影响。
- **原子写入的兜底逻辑可能丢数据**：0.2.0 遇到任何临时文件错误都会退回到会截断原文件的原地写，磁盘满时原文件会丢失。现在只对 EACCES、EPERM、EROFS、EXDEV、EBUSY、ENOTSUP、EINVAL、ENAMETOOLONG 这类"无法在此创建或替换文件"的错误回退，其他错误直接失败，原文件保持不变。
- **rename 后属主/属组丢失**：现在会把临时文件 chown 回原属主；没有权限时改为原地写，保留原 inode。
- **CRLF 文件中未触及的 `\r\r\n` 被改成 `\r\n`**：`encode` 改为只把 LF 映射回 CRLF。

### 较轻 / 体验

- `@src/app.ts` 形式的路径现在可以解析（字面以 `@` 开头的文件优先）。read 时记录的已读视图使用同一套解析。
- 从本工具成功 diff 中复制的 `+    12│...` 行现在能被识别为行号前缀。
- 行匹配路径下 newText 末尾恰好去掉一个换行：`X\n` 不产生空行，`X\n\n` 产生一个空行（0.2.0 下是 0 个或 2 个）。
- `prepareArguments` 接受 `"12"` 这样的整数字符串；`null` 视为未设置（0.2.0 下会报错或残留到 schema 校验）。
- 混合换行文件中，被行级操作触及的 CRLF 行保持 CRLF；插入行跟随相邻行的换行风格。
- `oldText` 与 `endLine`、行号定位与 `replaceAll`/`nearLine` 混用时，现在会报错，而不是被静默忽略。
- 除 `session_start` 外，还会在 `session_switch`、`session_fork`、`session_branch`、`session_tree`、`session_compact` 时清空已读视图。不同 Pi 版本的事件名可能不同，注册不存在的事件名不会产生影响，但建议对照所用版本确认。

### 行为变化（可能影响调用方）

- `applyEdits(decoded, edits, opts?)` 新增可选参数 `{ lineNumbersTrusted, untrustedReason }`，`ApplyResult` 新增 `linesShifted`。
- `index.ts` 新增命名导出 `atomicWrite`。
- `test/extension.test.ts` 中原先断言 `"1"` 会被拒绝的用例，已改为断言 `" 1.0"` 会被拒绝，因为整数字符串现在会被接受。

### 部署验收补充修复

- 行号可信度按原始编辑区间的换行差计算（含 EOF 无末尾换行边界），不再依赖合并后的展示 hunk，避免相邻的 +1/−1 编辑抵消后绕过保护。
- chown 只有 EPERM/EACCES 才允许原地写回退；EIO/ENOSPC/EDQUOT 直接拒绝并保留原文件。恢复顺序改为先 chown 再 chmod，保留 setuid/setgid 位。
- 仅匹配到一个缩进层级时，使用文件的缩进步长推断新层级，不再把绝对嵌套深度当作缩进单位比例。

### 已知限制

- 原子写入会产生新 inode，ACL 和扩展属性（xattr）不会被复制。属主不同且无法 chown 时会走原地写，从而保留这些属性。
- 原地写本身不是原子的。
