# ADR-0039: edit-loop helper — N edições em 1 invocação
- **Status**: Accepted
- **Date**: 2026-06-17
- **Context**: Operator scripts that apply a batch of textual transformations to one file (rename an identifier in 7 places, swap import paths across a refactor, sweep obsolete type aliases) currently invoke `edit` N times. Each invocation pays the full subprocess startup cost (clap parse, BLAKE3 read, `--expect-checksum` re-validation, write pipeline, NDJSON envelope emission) and re-enters the workspace jail. For 5 edits on a 50 KB file, the cumulative overhead is dominated by process spawn (~15-30 ms × 5 = 75-150 ms) rather than the actual string matching (<5 ms total). The 2026-06-15 audit (ADR-0035) and the GAP-2026-012 closure that introduced `--partial` for multi-pair `edit` (ADR-0036) established that the multi-pair path inside one `edit` invocation is a verified, fuzz-tested code path with per-pair `pair_results` and the 9-strategy cascade. Extending that path to N pairs in a single invocation is mechanically trivial; the operator-visible benefit is N→1 process boundary crossings. The G118 path resolution helper (ADR-0031) means the target file is resolved once against the workspace jail and reused; the G119 WAL policy (ADR-0028) means the sidecar is either absent (default) or written once for the whole loop. The new subcommand does not change semantics: it is a thin stdin-driven wrapper around the existing per-pair `edit` engine, with the loop driven by NDJSON on stdin and the same `pair_results` envelope emitted on stdout (one NDJSON line per pair, plus a final summary line). The default `edit` behavior (all-or-nothing abort on first failed pair) is preserved unless `--partial` is passed, matching the contract ADR-0036 documented.
- **Decision**: Add a new subcommand `edit-loop` to v0.1.22 that accepts NDJSON on stdin in the form `{"old": "...", "new": "..."}` per line and applies the pairs sequentially to a single target file, reusing the per-pair fuzzy cascade from `edit`. The subcommand accepts the same target-file positional as `edit`, plus the same global flags (`--workspace`, `--expect-checksum`, `--preserve-timestamps`, `--line-ending`, `--partial`, `--fuzzy`). A new flag `--re-capture-checksum` (default `false`) controls whether the checksum is re-read after each pair (slower, safer for concurrent scripts) or only at the end (default; matches the single-`edit` behavior). The output is NDJSON: one `pair_result` object per input line (in order), plus a final `summary` line with `pairs_total`, `pairs_matched`, `pairs_unmatched`, `bytes_before`, `bytes_after`, `checksum_before`, `checksum_after`, and `mtime_preserved`. The exit code is 0 if all pairs matched (or `--partial` was passed and at least one matched), 1 if zero pairs matched (NO_MATCHES), or 65 if any precondition failed (missing target, invalid NDJSON, `--expect-checksum` mismatch). The implementation lives in `src/commands/edit_loop.rs` and delegates to `commands::edit::run_pair` for each pair, sharing the same in-memory buffer for the target content to avoid re-reading the file from disk N times.
- **Consequences**:
- **+** N edits collapse to 1 subprocess invocation. For a 5-pair batch on a small file, wall-clock drops from ~150 ms to ~35 ms. The actual saving scales with N and with process spawn cost on the host (heavier on Windows due to slower CreateProcess).
- **+** Single read of the target file from disk. The file content is loaded once at subcommand start, mutated in memory, and written once at subcommand end via the existing atomic pipeline. No mid-loop race with another writer unless `--re-capture-checksum` is passed.
- **+** Pair ordering is deterministic: pairs are applied in stdin order, and the per-pair fuzzy cascade uses the post-previous-pair content as the search space. This matches the G117 contract (ADR-0026) and avoids the "non-overlapping pair pre-validation" problem that would require a separate pass over the file.
- **+** The `--partial` flag propagates the same semantics as multi-pair `edit` (ADR-0036): matched pairs are applied, unmatched are reported with `matched: false`, exit 0 if at least one pair matched, exit 1 if zero matched. Operators scripting a best-effort sweep can pass `--partial` once and consume the NDJSON stream.
- **+** The `--expect-checksum` flag is read once at subcommand start, not per pair, matching the single-`edit` behavior. A concurrent writer that mutates the target between subcommand start and atomic_write will trigger the standard state-drift envelope (exit 82) on the final write, not on intermediate states.
- **-** The entire target file is held in memory. The same `--max-filesize` global cap that protects `read` and `edit` protects `edit-loop`; a file that exceeds the cap is rejected with exit 65 before any pair is applied. This is the documented contract and matches the in-memory pattern of `edit --multi` (v0.1.15).
- **-** Pair interaction is not analyzed before the loop. If pair 3's `old` text was already consumed by pair 1's `new` text, the cascade may match pair 1's output as pair 3's input, producing a "successful" pair that the operator did not intend. The fix is operator discipline (order pairs so `old` texts are disjoint) or pre-validation via a separate pass; the subcommand does not pre-validate.
- **-** Adding a subcommand expands the help surface by one entry. The `edit` help text gains a cross-reference to `edit-loop` ("for N edits in one invocation, see `edit-loop`") and vice versa. The cross-reference is a 1-line addition to each help, not a new flag.
- **Rationale**: The 2026-06-15 audit (ADR-0035) and the GAP-2026-012 closure (ADR-0036) established that the multi-pair `edit` path is the canonical answer to "I have several text changes for one file" inside a single invocation. The remaining gap is the operator who scripts the same 5 `edit` calls in a `for` loop in a shell script: the per-invocation overhead is wasteful and the script is harder to read than a single NDJSON stream. `edit-loop` is the smallest command surface (one subcommand, no new flags beyond `--re-capture-checksum`) that addresses the loop case without disturbing the single-pair or multi-pair paths. The default behavior matches the existing `edit --multi` semantics: read once, mutate in memory, write once. The `--partial` propagation is required by symmetry: an operator who passes `--partial` to multi-pair `edit` should get the same semantics in `edit-loop`, and the absence of `--partial` should preserve the all-or-nothing abort that ADR-0026 documented as the safer default. The `--re-capture-checksum` flag is the only new opt-in; it is off by default to match the existing `edit` behavior and to avoid the I/O cost on the hot path. The in-memory buffer is the natural data structure (the per-pair cascade already operates on a `String` content), and the `--max-filesize` cap already protects against pathological inputs.
- **Alternatives considered**:
1. **Add a flag `--allow-sequential-drift` to `edit` that re-reads the file between pairs.** Rejected: mixes the loop case with the single-pair case, expands the `edit` help surface by a non-obvious flag, and forces every `edit` caller to reason about a mode they will never use. The subcommand is the cleaner factoring.
2. **Add a flag `--loop <COUNT>` to `edit` that re-invokes the same pair N times.** Rejected: does not match the operator mental model (N different pairs, not the same pair N times), and the syntax for N different pairs via repeated `--old`/`--new` already exists inside a single `edit` call.
3. **Generalize `edit --multi` to accept NDJSON from a file via `--multi-file <PATH>`.** Rejected: `--multi` already accepts NDJSON from stdin. The subcommand is the right factoring because it changes the input source model (one file, many pairs over time) and the output envelope model (per-pair result line plus summary) — both warrant a named subcommand for discoverability.
4. **Defer to a separate `bulk-edit` tool that supports multiple target files.** Out of scope for v0.1.22: the audit identified the same-file N-pairs case as the immediate need. Multi-file batching is a v0.1.23+ concern that can layer on top of `edit-loop` (one subprocess per file, NDJSON manifest of `target + pairs`) without changing the single-file contract.
- **Trigger to revisit**: If operators report that the in-memory buffer is a bottleneck (files near the 1 GiB cap), revisit with a streaming variant that writes a tempfile after each pair (loses atomicity within the loop but allows larger files). If `--re-capture-checksum` proves necessary as the default rather than the opt-in (i.e. concurrent-write patterns dominate over single-script use), flip the default and document the I/O cost. If a future subcommand (e.g. `replace-loop`, `transform-loop`) needs the same loop pattern, extract the NDJSON-stream-to-`run_pair` adapter into a shared helper rather than copy-pasting the stdin parser.