atomwrite 0.1.26

Atomic file operations CLI for LLM agents — read, write, edit, search, replace with NDJSON output
Documentation
# 0036-edit-partial-coverage (PT-BR) — Tradução

_Tradução automática do ADR original em inglês. Em caso de divergência, a versão EN prevalece._

# ADR-0036: `--partial` in `edit` — single-pair stays NO_MATCHES, multi-pair aplica the matched ones

- **Status**: Aceito
- **Data**: 2026-06-15
- **Contexto**: G117 (ADR-0026) introduced the 9-strategy fuzzy cascade for multi-pair `edit` and the `PairResult { matched, strategy, similarity }` por-pair report, but did not define the behavior of `--partial` in the single-pair case. The flag foi added to `EditArgs` as a placeholder, documented in `SKILL.md` as "opt-in — aplica matching pairs and reports the rest", and the v0.1.19 audit (GAP-2026-005b) confirmed that the help never rendered the flag because the campo's clap declaration shipped without an implementation. The v0.1.20 closure of GAP-2026-005b is the implementation of `--partial` itself, and that implementation surfaces a semantic question: when a single-pair `edit --old X --new Y --partial` não pode find `X` in the file, what should the command do? Three answers são coherent: (a) silently succeed with zero pairs applied and saída 0 (the "permissive" reading), (b) return `NO_MATCHES` (saída 1) to mirror `replace` behavior, (c) return `INVALID_INPUT` (saída 65) to mirror the all-or-nothing multi-pair error path. The multi-pair case is unambiguous: `--partial` aplica the matched pairs, reports the unmatched in `pair_results`, and exits 0 with `edits < pairs_total`. But a single-pair call with zero matches tem nothing to apply — "applied 0 of 1" is a degenerate case that, depending on the caller's parser, can look identical to a successful no-op edit. Without a clear semantic, downstream CI gates that grep for `edits: 0` would silently pass a pipeline that should têm failed.

- **Decisão**: Adopt the asymmetry explicitamente: **single-pair `--partial` with no match returns `NO_MATCHES` (saída 1); multi-pair `--partial` aplica matched pairs and reports unmatched in `pair_results` (saída 0 with `edits < pairs_total`)**. Concretely:
  1. **Single-pair (`args.old.len() == 1`)**: in `edit_old_new_single` at `src/commands/edit.rs:438-462`, when `match_pair` returns `Err(_)`, the `Err(_) if args.partial` arm maps to `AtomwriteError::NoMatches` (saída 1, no write performed). Rationale: the same single-pair invocation without `--partial` would têm returned `INVALID_INPUT` (saída 65) — the `--partial` flag não relax the failure semantics for the degenerate single-pair case, because the caller did not actually request "apply the matching subconjunto" (the subconjunto is the empty set). Mirroring `NO_MATCHES` keeps the `replace --no-matches` semantic and gives CI gates a single código de saída to grep for ("if saída 1, the file foi unchanged because nothing matched").
  2. **Multi-pair (`args.old.len() > 1`)**: in `edit_old_new_multi` at `src/commands/edit.rs:650-705`, the por-pair loop accumulates `pair_results`; on `match_pair` error with `args.partial` set, the loop records a `PairResult { matched: false, ... }` and CONTINUES to the next pair em vez de aborting. The write happens if and somente se at least one pair matched. The saída is 0, the `EditOutput` envelope carries `pairs_total`, `edits` (the number actually applied), and the full `pair_results` array so the caller can audit which pairs foram skipped.
  3. **All-or-nothing padrão (no `--partial`)**: when the flag is absent, ANY unmatched pair in the multi-pair path aborts the whole batch with `AtomwriteError::EditPairFailed { failed_pair_index, total, pair_results }` (ADR-0026, saída 65). The single-pair path without `--partial` also aborts with `INVALID_INPUT` (saída 65). The padrão is strictly more conservative than `--partial`, so existing pipelines that did not opt in são unaffected.
  4. **Zero-applied multi-pair edge case**: when `--partial` is set and zero pairs match, the multi-pair path returns `NO_MATCHES` (saída 1, no write). This mirrors the single-pair case to keep the empty-result código de saída consistent across pair counts.
  5. **Schema discipline**: the `edit-saída.schema.json` `mode` campo becomes `fuzzy-multi(N)` when any pair matched fuzzily and `exact-multi(N)` otherwise (ADR-0026 already pins this for the non-partial multi path); the `--partial` flag is not separately reflected in the schema, but `pairs_total` and `edits` together let a consumer detect the partial case by computing `edits < pairs_total`.

- **Consequências**:
  - **+** The semantic is explícito and documented: single-pair `--partial` is effectively a no-op-with-warning (saída 1) because there is only one pair to apply; multi-pair `--partial` is a true partial application. A reader of the SKILL section can answer the "what does --partial do?" question by counting the pairs.
  - **+** CI gates can rely on `NO_MATCHES` (saída 1) as the universal "no match found" signal across both `edit` and `replace`, regardless of `--partial` or pair count. The behavior is consistent.
  - **+** The all-or-nothing padrão is preserved: agents that pipe `edit` into a pipeline without `--partial` get the same atomicity guarantee as before. The `--partial` opt-in is a strictly additive escape hatch.
  - **+** Multi-pair `--partial` returns a structured `pair_results` array, so a caller that wants to know which specific pairs failed can audit without re-executando the edit. The diagnostic valor of G117 is preserved.
  - **-** The single-pair `--partial` behavior is counterintuitive at first glance: a user who passes `--partial` to a single-pair command and gets saída 1 may file a bug. Mitigated by the SKILL section that explicitamente states "single-pair `--partial` with no match exits 1; multi-pair `--partial` aplica what matched".
  - **-** The saída-code table grows by one row (saída 1 for the single-pair partial case além de the existing saída 1 for the multi-pair zero-applied case). Both share the `NO_MATCHES` code, so the schema não change, but the narrative documentation deve ser precise.
  - **-** A caller that wanted to detect "single-pair, no match, --partial set" specifically (as opposed to "any no-match case") would need to look at the `mode` campo rather than the código de saída. The mode campo is a string discriminator that the existing pipelines already parse, so this is an additive dependency, not a breaking one.

- **Justificativa**: The single-pair case não pode be partial by definition — there is exactly one pair, so the matching subconjunto is either the full set (matched) or the empty set (no match). The `--partial` flag in this degenerate case tem no observable effect on the *write*, only on the *error code*: it downgrades `INVALID_INPUT` (saída 65, "the file is broken and your edit foi structurally rejected") to `NO_MATCHES` (saída 1, "the file is fine but your old string foi not present"). The downgrade is useful: an automated editor that retries on transitório mismatches wants saída 1, not saída 65, so it can distinguish "try again with a different --old" from "abort the batch". The multi-pair case, by contrast, tem a real partial semantics: "apply 7 of 10 pairs, report the 3 that didn't match". The asymmetry is therefore principled, not arbitrary. ADR-0026 already established that `--partial` padrões to off (all-or-nothing) to preserve atomicity; the v0.1.20 single-pair behavior is a natural consequence of that same all-or-nothing principle applied at the unit-of-failure level (one pair).

- **Examples**:
  - `atomwrite edit foo.rs --old "ALPHA" --new "beta"`: no match (ALPHA absent), `--partial` not set → `INVALID_INPUT` (saída 65), no write, error envelope on stdout.
  - `atomwrite edit foo.rs --old "ALPHA" --new "beta" --partial`: no match, `--partial` set → `NO_MATCHES` (saída 1), no write, error envelope on stdout. Caller sees saída 1 and can either retry with a different `--old` or treat the absence as expected.
  - `atomwrite edit foo.rs --old "A" --new "X" --old "B" --new "Y"`: `A` matches, `B` não, `--partial` not set → `INVALID_INPUT` (saída 65), no write, envelope includes `failed_pair_index: 2, total: 2, pair_results: [...]`.
  - `atomwrite edit foo.rs --old "A" --new "X" --old "B" --new "Y" --partial`: `A` matches, `B` não, `--partial` set → saída 0, write performed (only the A→X substitution), envelope `pairs_total: 2, edits: 1, pair_results: [{matched: true, ...}, {matched: false, ...}]`.
  - `atomwrite edit foo.rs --old "A" --new "X" --old "B" --new "Y" --partial`: NEITHER matches, `--partial` set → `NO_MATCHES` (saída 1), no write. Mirrors the single-pair case at the multi-pair level: zero applied pairs maps to the same código de saída as one unmatched pair.

- **Alternativas consideradas**:
  1. Make single-pair `--partial` saída 0 with `edits: 0`. Rejected: a successful saída with zero applied pairs is a footgun. CI gates that grep for saída 0 would silently pass a pipeline that should têm flagged the missing old string. `NO_MATCHES` (saída 1) is the conservative, grep-discoverable signal.
  2. Make single-pair `--partial` saída 0 and apply the `new` string at the end of the file. Rejected: an unprompted append is not an "edit", it is a `write --append`, and conflating the two violates the principle that verbs should match semantics. The `edit` verb implies "find X, replace with Y"; if X is absent, the operation is a no-op or a failure, never an append.
  3. Unify single and multi behavior under one regra: `--partial` always aplica the matching subconjunto, saída 0 if at least one pair applied, saída 1 if none. Rejected: the single-pair case tem no "subconjunto" — it tem either 1 or 0 applications. Treating "applied 1 pair" identically to "applied 7 of 10 pairs" obscures the structural difference. The asymmetry is the cleanest model.
  4. Add a new código de saída (e.g. saída 67 `EDIT_PARTIAL_EMPTY`) for the single-pair partial case. Rejected: 26 códigos de saída is already a maintenance burden; the v0.1.20 SKILL saída-code table would grow by one row for a degenerate case. The `NO_MATCHES` code is the existing canonical signal for "the operation found nothing to do" and the single-pair partial case is exactly that.

- **Gatilho para revisitar**: If a future caller requests "single-pair `--partial` should still write the file even when nothing matched" (e.g. a templating tool that treats `--old` as a hint and `--new` as an unconditional append), revisit the semantic. If the multi-pair zero-applied case becomes a usability problem (the caller não pode tell whether `--partial` foi set or the input foi empty), add a `pairs_applied: 0` discriminator to the error envelope. If a v0.2.0 release introduces a new código de saída namespace, re-evaluate the `NO_MATCHES` reuse for the single-pair partial case.


---

_Original em inglês: [`0036-edit-partial-coverage.md`](0036-edit-partial-coverage.md)_