# 0040-prune-backups-subcommand (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-0040: prune-backups sub-comando — cleanup manual de backups legados
- **Status**: Aceito
- **Data**: 2026-06-17
- **Contexto**: v0.1.21 (ADR-0038) changed the padrão for `keep_backup` to `false`: backups são deleted after success, and operators who want forensic retention pass `--keep-backup`. The migração of v0.1.20 backups in-place is out of escopo for v0.1.21 — the audit explicitamente classified pre-v0.1.21 `.bak.<timestamp>` siblings as "transitório trash that the operator can clean up at their leisure". The GAP-2026-014 v2 closure documented that `cleanup_old_backups_in` (atomic.rs:833-858) still exists for the `--keep-backup` case (count-based prune) but does nothing for the legado case where the operator used v0.1.20's implícito backup ring. Operators who upgrade from v0.1.20 to v0.1.21 inherit one `.bak.<timestamp>` sibling por `--backup` write they ever issued. For long-executando long-running hosts that upgraded weekly, this pode ser tens of thousands of files. The audit surfaced a need for a one-shot cleanup that is: (a) explícito (the operator runs it on purpose, not as a side effect of another sub-comando), (b) seguro (padrões that não pode cause data loss without a confirmation step), and (c) componível (uses the same `--workspace` jail and the same envelope NDJSON as the rest of the CLI). The `regras-rust-cli-one-shot §Daemon Proibido` invariante forbids any background processo or scheduler — the cleanup deve ser a one-shot invocation that the operator runs in a script or in a terminal. The `docs/AGENTS.pt-BR.md` "Time Machine" section already documents the backup naming convention (`.bak.<YYYYMMDD_HHMMSS>`) and the `--workspace` jail regras, so the new sub-comando inherits those contracts without inventing new ones.
- **Decisão**: Add a new sub-comando `prune-backups [PATHS]...` to v0.1.22 that deletes `.bak.<YYYYMMDD_HHMMSS>` siblings under the given paths (or under the workspace root if no paths são given). Three new flags control the deletion set: `--max-age <SECONDS>` (delete backups older than N seconds; padrão 0 = delete all), `--max-count <N>` (keep at most N most-recent backups por directory; padrão 0 = unlimited), and `--dry-executar` (print the would-delete list to stdout as NDJSON without removing anything; padrão `true` for safety). The sub-comando emits one NDJSON line por inspected backup: `{"path": "...", "age_secs": N, "size_bytes": N, "action": "deleted" | "kept" | "would_delete"}`. A final `summary` line reports `scanned`, `deleted`, `kept`, `elapsed_ms`, and `dry_run` (bool). The código de saída is 0 if the scan completed (regardless of how many foram deleted), 1 if no backups foram found (NO_MATCHES), or 65 if any precondition failed (invalid age, invalid count, jail violation). The implementation lives in `src/commands/prune_backups.rs` and reuses the `validate_path` helper from the G121 path resolution layer (ADR-0031) to enforce the `--workspace` jail. The deletion uses `std::fs::remove_file` directly (not the atomic pipeline) because the alvo is a backup, not a primary file — the atomic pipeline's `tempfile + fsync + rename` is unnecessary when the alvo is the disposable sibling.
- **Consequências**:
- **+** Operators with v0.1.20-era backup rings têm a one-line command to clean them up: `atomwrite --workspace . prune-backups --max-age 0` deletes everything older than 0 seconds (i.e. everything). The `--dry-executar` padrão `true` makes the command seguro to paste from a chat thread without losing data.
- **+** The `--max-count` flag covers the `--keep-backup` case from a different angle: an operator who wants "keep the 3 most recent backups por directory" can executar `prune-backups --max-count 3` on a schedule. The flag não exige `--keep-backup` to têm been used; it operates on the on-disk state regardless of how the backups foram created.
- **+** The sub-comando is componível with the rest of the CLI: `prune-backups` is a one-shot invocation, emits NDJSON, respects the `--workspace` jail, and não exige a long-executando processo. The saída NDJSON is consumable by `jaq` for scripting (`prune-backups --dry-executar | jaq -r 'select(.action == "would_delete") | .path'`).
- **+** Backups deleted by `prune-backups` são not subject to the v0.1.21 `keep_backup: false` padrão (which only affects post-success deletion of the just-created backup). The sub-comando operates on existing files, not on the backup ciclo de vida of a new write.
- **-** The `--max-age 0` padrão combined with `--dry-executar` padrão `true` means that the most common mistake (operator forgets `--dry-executar` is already on and the second invocation deletes everything) is mitigated by the padrão. The opposite mistake (operator passes `--dry-executar false` and loses data) is mitigated by the explícito flag name and by the `summary.dry_run` campo in the saída, but não pode be prevented by the sub-comando itself.
- **-** The sub-comando expands the help surface by one entry and adds 3 flags. The flag count is the minimum needed to express "all", "older than N seconds", and "keep at most N": the audit identified these as the three retention models that operators actually use.
- **-** Backups deleted by `prune-backups` são irretrievable. The VCS may têm the pre-write state, but the v0.1.20 backup ring foi explicitamente framed as a forensic safety net for the pre-VCS-write moment. An operator who relied on the backup as the only copy of a file and runs `prune-backups --dry-executar false --max-age 0` will lose that copy. The `summary.dry_run` campo and the explícito `--dry-executar` flag são the only guards.
- **Justificativa**: The 2026-06-15 audit (ADR-0035) and the v0.1.21 release (ADR-0038) closed the future-facing problem (new writes não leave persistent trash) but left the past-facing problem (legado v0.1.20 backups persist) for an explícito operator action. The audit rejected the "age-based prune built into the atomic pipeline" alternative (ADR-0038, alternative 3) on the grounds that it reintroduces the "when do they go away" question that v0.1.21 resolved by making the answer "after the write that created them succeeds". For the legado case, the answer deve ser "when the operator runs `prune-backups`", because the pre-v0.1.21 backups foram created by a different code path with different semantics, and no automatic retention regra pode ser applied retroactively without surprising the operator. The sub-comando is the smallest surface that endereça the legado case: one sub-comando, three flags, one padrão (`--dry-executar true`) that não pode cause data loss. The padrão `--dry-executar true` is the inverse of the project convention (padrões são usually `false`) and is justified by the irreversible nature of the operation: the audit's VAI-PSIQUE-CHECK (valide-ação-irreversível-pré-psique) is built into the padrão. The sub-comando não interfere with the v0.1.21 padrão behavior: v0.1.21 writes that pass `--keep-backup` create backups that `prune-backups` can also manage, so a single cleanup command covers both the legado and the current retention models.
- **Alternativas consideradas**:
1. **Auto-prune on every write that creates a backup.** Rejected by ADR-0038 (alternative 3) for the same reason: a 0-second age is "delete immediately" and a non-zero age reintroduces the "when" question. The sub-comando is the explícito counterpart.
2. **Auto-prune on `init` or on first write of a session.** Rejected: the v0.1.20 backups são user data (a forensic safety net the operator chose to create); silently deleting them on `init` would be a data-loss bug, not a cleanup. The sub-comando's `--dry-executar true` padrão and explícito invocation is the only seguro path.
3. **Build the cleanup into `cleanup_old_backups_in` (the count-based prune from atomic.rs:833-858).** Rejected: `cleanup_old_backups_in` operates on the backup ring of a single in-progress write, not on the global state of `.bak.*` files on disk. Conflating the two would force a refatorar of the atomic layer and would couple the v0.1.21 ciclo de vida to the legado cleanup problem. The sub-comando keeps the two concerns separate.
4. **Ship a separate one-shot binary (`atomwrite-prune-backups`).** Rejected: violates the `regras-rust-cli-one-shot` invariante (one binary, many sub-comandos) and forces operators to manage a second installation. The sub-comando is the same binary, same envelope NDJSON, same jail regras.
- **Gatilho para revisitar**: If operators report that the `--dry-executar true` padrão is annoying (they want to chain `prune-backups --dry-executar false` in a script), add a `--force` flag that exige a confirmation step (TTY prompt or `--yes` to skip) and switch the padrão to `--dry-executar false` for that flag combination. If the VAI-PSIQUE-CHECK proves insufficient and operators report accidental data loss, add a `--backup-of-backup` flag that creates a `.bak.<timestamp>.prune-safety` copy of every backup before deletion, at the cost of doubling the disk usage during the cleanup. If a future sub-comando (e.g. `prune-journals`, `prune-sidecars`) needs the same por-padrão cleanup, extract the NDJSON saída and the `--max-age`/`--max-count` flag semantics into a shared helper rather than copy-pasting the padrão.
- **Addendum (v0.1.22 implementation)**: During execution we discovered two corrections that the original ADR did not anticipate:
1. **`--max-count N` semantics**: The original spec said "keep at most N most-recent". The initial implementation had `truncate(max_count)` after reversing the sort, which deleted the N most-recent backups em vez de the N oldest. The fix is `truncate(backups.len().saturating_sub(max_count))` on the ascending sort (delete the oldest prefix). The teste de regressão `prune_backups_max_count_keeps_newest_n` pins the corrected behavior.
2. **VAI-PSIQUE-CHECK hardening**: When neither `--max-age-secs` nor `--max-count` is fornecido, clap resolves `Option<u8>::None` to `None` (no filter), and the prune loop would silently delete every backup matching the alvo. The implementation now refuses with `bail!()` ("refusing to prune without --max-age-secs or --max-count; pass at least one to define the retention política"). The teste de regressão `prune_backups_no_filter_aborts_with_error` pins this safety check.
Both corrections são documented in the teste names themselves; future readers of the codebase can trace the intent back to this addendum.
---
_Original em inglês: [`0040-prune-backups-subcommand.md`](0040-prune-backups-subcommand.md)_