# Development

## Files

| File | Purpose |
|---|---|
| `index.ts` | Extension entry: flags, `/edit-mode`, footer status, tool override. |
| `libtool.ts` | Pure matching + atomic-write logic (no pi imports). |
| `libtool.test.ts` | Unit tests for the pure logic. |
| `smoke.test.ts` | Loads `index.ts` via jiti and exercises input shapes, flags, command, env. |
| `compat.test.ts` | Differential test: snapshot matching and argument coercion vs pi's built-in. |
| `bench.ts` | Benchmark: pi's built-in `edit` vs this extension, optional git-ref baseline. |
| `package.json` | Pi package manifest (`pi.extensions`, peers, npm metadata). |
| `assets/preview.svg` | Gallery preview source; `preview.png` is rendered from it. |
| `LICENSE` | MIT (includes attribution for code derived from pi). |

## Testing

```bash
# pure logic (no pi needed)
npm test

# entry + flags + /edit-mode (needs pi packages for import resolution)
cd ~/.pi/agent/extensions/atomic-edit
ln -sfn "$HOME/.pi/agent/install/releases/$(cat "$HOME/.pi/agent/install/current-version")/node_modules" node_modules
npm run test:smoke
npm run test:compat
rm -f node_modules
```

`test:compat` compares snapshot matching against pi's own `edit-diff` helper on
curated edge cases plus 500 deterministic fuzz cases. It asserts that both
implementations produce identical content, that this extension never rejects a
call the built-in accepts, and that the only extra acceptances are the
documented exact-space uniqueness difference. It skips (exit 0) when the helper
is unavailable.

## Benchmarking

`bench.ts` compares pi's built-in `edit` with this extension, optionally against
`libtool.ts` from an older commit (useful as a regression baseline):

```bash
cd ~/.pi/agent/extensions/atomic-edit
ln -sfn "$HOME/.pi/agent/install/releases/$(cat "$HOME/.pi/agent/install/current-version")/node_modules" node_modules
npm run bench
BENCH_BASELINE=d1ea63b npm run bench
rm -f node_modules
```

It writes its temporary files under `.bench-*` in the extension directory (so
`fsync` costs are real) and removes them afterwards. Matching numbers are
CPU-only; tool numbers include read, diff generation, and the write.

Methodology:

- Every variant is warmed up once, then measured round-robin (one sample per
  variant per round) so JIT and GC state do not systematically favor whichever
  variant runs first.
- Both matching modes are covered. pi's built-in `edit` only implements snapshot
  semantics, so the sequential table has no built-in column.
- The full tool path is measured in six configurations: pi built-in, this
  extension with snapshot/sequential matching crossed with atomic/fsync,
  atomic/no-fsync, plus snapshot + direct write.
- `BENCH_BASELINE=<git-ref>` loads `libtool.ts` from that commit and adds
  baseline columns; it is off by default.

### Reference results

Development machine: Linux, ext4 SSD, Node 22, pi 1.4.0. Medians from
`bench.ts`; absolute numbers vary by machine and by roughly 10% between runs,
the ratios are the point.

Pure matching on a 3.80 MB / 80k-line file (CPU only, no I/O):

| Workload | pi built-in (snapshot) | snapshot | sequential (default) |
|---|---:|---:|---:|
| 25 edits | 432 ms | **43 ms** | **68 ms** |
| 100 edits | 1.74 s | **153 ms** | **258 ms** |
| 25 fuzzy edits | 613 ms | **274 ms** | **369 ms** |
| 100 fuzzy edits | 2.35 s | **1.05 s** | **1.47 s** |

Full tool path (read + match + diff + write):

| Workload | pi built-in | snapshot atomic | snapshot no-fsync | sequential atomic | sequential no-fsync | snapshot direct |
|---|---:|---:|---:|---:|---:|---:|
| 3.80 MB / 100 edits | 1.97 s | 289 ms | 275 ms | 411 ms | 395 ms | 275 ms |
| 4 KB / 1 edit | 0.45 ms | 9.19 ms | 0.55 ms | 9.19 ms | 0.60 ms | 0.37 ms |

Exact-match editing is ~10–12× faster than the built-in on large files;
fuzzy-heavy workloads are ~2.2× faster in snapshot mode and ~1.6× faster in the
default sequential mode. The full path is ~6.8× faster in snapshot mode and
~4.8× faster in sequential mode. On small files the atomic path's `fsync`
dominates (9.2 ms); `PI_EDIT_FSYNC=0` restores parity while keeping the atomic
rename.

Against the pre-optimization `libtool.ts` (`BENCH_BASELINE=d1ea63b`), on the
same file: 100 snapshot edits 1.74 s → 153 ms, 100 sequential edits 1.63 s →
258 ms, 100 sequential fuzzy edits 4.08 s → 1.47 s, and 100 snapshot fuzzy
edits 2.43 s → 1.05 s.

## Where the speed comes from

- **One scan per edit, match reuse.** Snapshot matching remembers the first
  pass and reuses it when no edit needed the fuzzy fallback, instead of scanning
  the file twice per edit.
- **Uniqueness stop.** Occurrence checks stop as soon as a second occurrence is
  found; the count is only needed for the error message.
- **Line offsets, not line strings.** Fuzzy replacements map offsets to lines
  with binary search over a start-offset array, avoiding per-line string and
  span-object allocations for untouched lines.
- **One forward pass (snapshot).** Replacements are applied in a single pass
  after sorting, so many edits do not multiply buffer copies.
- **Read once, write once.** The file is read once and written once regardless
  of edit count. The atomic write adds a temp-file create + rename (~0.2 ms for
  small files); `fsync` (not the rename) dominates the added latency.
- **Sequential is inherently heavier.** Chained semantics require scanning and
  copying the evolving buffer per edit, and `~9 ms` of fuzzy normalization per
  3.8 MB per fuzzy edit. It is still faster than the built-in for typical
  workloads, but snapshot is the better choice for large disjoint edit batches.

Diff and patch generation still uses pi's own helper, runs after the write, and
is the remaining CPU cost on very large files. It is kept so the tool result
shape matches the built-in `edit`.
