amql-mutate 0.0.0-alpha.0

Pure source code mutation operations
Documentation
# `amql-mutate`

- Pure source mutations — ∅ I/O, ∅ file system
- All fns: source text + node refs in → modified source + updated node refs out
- Same inputs → same outputs; no hidden state
- Newtypes defined here, re-exported by `amql-engine`:
  - `NodeKind` — tree-sitter AST node kind
  - `RelativePath` — path relative to project root

## `NodeRef`

- `NodeRef { file: RelativePath, start_byte: usize, end_byte: usize, kind: NodeKind, line: usize, column: usize, end_line: usize, end_column: usize }`
- Byte offsets are into the source text at time of creation
- `NodeRef` is stale after any mutation — byte offsets shift; ∄ reuse after any op
- Obtain `NodeRef` from nav/select results — do not construct manually

## Operations

- `remove_node(source: &str, node: &NodeRef) → Result<RemoveResult, String>`
  - does NOT take `file` arg
  - `RemoveResult { result: MutationResult, detached: String }``detached` = removed text
- `insert_source(source: &str, file: &RelativePath, target: &NodeRef, position: InsertPosition, new_source: &str) → Result<MutationResult, String>`
  - `InsertPosition`: `Before`, `After`, `Into`
    - `Into` → before the closing delimiter (`}`, `]`, or `)`), indented +1 level (auto-detected from target line)
- `replace_node(source: &str, file: &RelativePath, node: &NodeRef, new_source: &str) → Result<MutationResult, String>`
- `move_node(source: &str, file: &RelativePath, node: &NodeRef, target: &NodeRef, position: InsertPosition) → Result<MutationResult, String>`
- `MutationResult { source: String, affected_nodes: Vec<NodeRef> }`
  - `source` = full updated text
  - `affected_nodes` = valid refs into updated source; may be empty if no stable refs remain

## Chaining ops on the same file

```
CHAIN(source₀: String, ops: Vec<Op>) → String:
  s ← source₀
  ∀ op ∈ ops:
    r ← op(s, ...)     // use a ref valid for current s
    s ← r.source
    // prior refs invalid; re-select from s if needed ref absent from affected_nodes
  return s
```

- ∀ op: feed `result.source` as next `source`
- `affected_nodes` may not contain ∀ nodes — re-select from `result.source` when needed ref is absent
- `amql-engine` transaction engine buffers ops per file in `FxHashMap<RelativePath, String>`

## `RelativePath` type inference

- `RelativePath` implements both `AsRef<str>` and `AsRef<Path>`
- At call sites with ambiguous inference → use `AsRef::<str>::as_ref()` to disambiguate

## Gotchas

- `remove_node` takes no `file` arg; `insert_source`, `replace_node`, `move_node` do
- `InsertPosition::Into` → last child, ≠ first child
- `Before`/`After` → sibling of target, ∉ inside target
- `move_node`: op order is position-dependent — if node precedes target, removes first then inserts (target shifts left); otherwise inserts first then removes (source range adjusted); caller sees only the final `MutationResult`
- Pre-mutation refs are invalid after any op — including refs to untouched nodes
- `affected_nodes` can be empty — always be prepared to re-select