Skip to content

feat: decide and execute cleanup interactively - #102

Open
FeathBow wants to merge 18 commits into
mainfrom
feat/interactive-cleanup
Open

FeathBow wants to merge 18 commits into
mainfrom
feat/interactive-cleanup

Conversation

@FeathBow

Copy link
Copy Markdown
Owner

Summary

Closes #101.

degu already knows which caches are safe to delete and which it refuses to decide. What it had no place for was the decision itself. A judgement about one location — this model is coming back next week, that one is not — could only be recorded by transcribing paths out of a report that had already scrolled past, and the command that took them, degu clean, frees no quota: it stages, and space comes back only from a purge that until now took everything or nothing.

This adds the place to make those decisions, and the command-line selector the interface needs in order to wrap one. degu tui runs the same scan, shows its findings beside the current staging trash, and hands what the reader decided to degu trash purge and degu clean. degu trash purge --path narrows a purge to entries staged from at or under a path: a new selector over plan_expired_trash's existing predicate shape, matching the origin the operation log recorded, not a new way to destroy data.

User-visible behavior. degu tui is a new subcommand taking scan's own arguments. Space puts the selected finding in the clean plan or takes it out; Ready to clean findings start in it and Needs review findings start out, so including one is still an explicit act for one location at a time. t shows the staging trash, where space chooses entries for permanent deletion and nothing is chosen by default. The masthead carries the current plan, and the staged screen counts what was chosen, what the seven-day expiry removes whether or not it was chosen, and what the trash holds once both have run. p previews the clean half through clean --dry-run; c runs both halves, each printing its own plan and taking its own confirmation. The equivalent command line is printed before each — not to be typed, but so that the next time the decision is a rule rather than a judgement, the reader already knows how to say it. degu trash purge gains a repeatable --path; without it the plan is still every entry.

Compatibility. No WAL, sidecar, oplog, or JSON schema changes, and no change to what any existing command does with the arguments it already accepted. The standalone degu-tui crate added in #99 is removed and its modules live under crates/degu/src/tui/; it was publish = false and absent from the release archive and the installer, so nothing shipped depended on it. The workspace publishes four crates as before. degu gains ratatui, which resolves against the workspace's existing crossterm; everything else the interface needs it already had. tools/release-contract.sh and tools/package-release.sh gain the new subcommand's man page and completion position.

Moving in deleted a parallel model rather than relocating it: a Finding struct deserialized from JSON, mirrors of Disposition and Recovery, a Coverage enum, and string matching on "eligible" / "opt_in" all gave way to the real types the compiler checks. Reading a saved report from another machine is no longer possible and is not part of this PR; the interface now scans in process, because deciding requires being where the files are.

Validation

Passed locally on this branch:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace
./tools/verify-release-contract.sh
./tools/test-release-records.sh
cargo publish --workspace --dry-run --locked
git diff --check

The workspace run reports 1565 passed, 0 failed. Nineteen of those are new unit tests over the two decision models, built against findings that pass through finalize_findings so the tier assertions are degu's real classification rather than a fixture's claim about it: what may be decided and what may not, that deciding twice returns to where it started, that the arguments name every chosen path and carry no flag the interface did not set, and on the trash side the two kinds of entry a path selector cannot name, the expiry split, and the cursor bounds. Four new integration tests drive the real binary against the purge selector, covering one origin among several, a parent path, a selector matching nothing, and that expired-claim housekeeping still runs.

A/B. Behaviour was measured end to end by driving the real binary through a pty in an isolated HOME. Fixture: ccache and go-build already staged, go-build recreated on disk at 6 MiB, Mozilla.sccache left on disk as an unchosen Needs review finding. Choosing the staged ccache entry and pressing c:

degu trash purge --path …/Library/Caches/ccache
Type 'purge' to permanently delete this plan: purge
Purged 1 trash entry
degu clean
Ready to clean · 1 location · 6.0 MiB
Proceed? [y/N] y
Staged 1 location · 6.0 MiB · 2 inodes into the trash in 9ms

Afterwards the ccache entry is gone from the trash directory, so its 3 MiB is off the quota; the unchosen go-build entry is still staged and still reachable by degu undo; Mozilla.sccache is untouched. The staged screen had predicted Afterwards it holds 2 locations · 8.00 MiB, and degu trash list reports 8.0 MiB across 2 entries. Typing anything but purge gives error: the clean was not run either: Purge cancelled; no trash entries were deleted., with the trash intact and the eligible cache still on disk.

before after
including a review finding transcribe the path into degu clean --include-review --path … move to the row, press space
dropping a Ready to clean finding no way to say it short of listing every other path press space on it
reclaiming one origin's space impossible; trash purge took everything choose the entry, or degu trash purge --path <origin>
what the interface could execute nothing the two commands its marks describe

A defect the A/B caught. The first version of the decision model marked Ready to clean findings as already in the plan and passed only the chosen review paths as --path. The run showed such a finding marked included and then still sitting on disk afterwards, because --path keeps only findings at or under a path — a filter. Choosing one review finding silently emptied the default plan of everything else, and the printed plan said one location while the screen the reader had just left said two. There is no CLI spelling for "the default plan, plus these two judgements", which is the issue's own problem statement; so the mark column was narrowed to answer one question, is this row in the plan, and every row degu will act on became decidable in both directions. When nothing has been changed the arguments carry no --path at all, so an unchanged plan stays the default plan rather than a filter that happens to match it today.

Two smaller things the same runs exposed: the masthead still said Read-only snapshot and sat beside a source field that had been dead since the browser stopped reading a saved JSON file, and both were replaced by the live plan; and p ran a dry run and then asked a bare Proceed? [y/N] directly under a plan, which reads as consent to run it, so the prompt now says what it is asking.

Safety

  • The interface carries no deletion capability. crates/degu/src/tui/ contains no filesystem write, no traversal, no DetectCtx, no Lifecycle, and no lock; its only imports from the mutation side are the seven-day retention constant and TrashEntry, the read-only row degu trash list already prints. What it produces is a CleanArgs and a TrashCommand::Purge, handed to the existing command implementations, so admission, sealing, the mutation lock, execution-time re-verification, and both confirmations stay exactly where they were and there is no second deletion path to grow.
  • Eligibility does not change. Not managed findings and node-runtime findings admit no decision from any surface and no keystroke reaches them. Needs review findings start outside the plan and are added one location per keystroke; there is no bulk toggle and no select-all. A Ready to clean finding can be taken out of the plan, which only ever narrows one.
  • Nothing executes while the alternate screen is up. Both commands run after the screen is restored, so their plans and confirmations land in the scrollback exactly as they would had the commands been typed.
  • Permanent removal keeps its friction: confirm_permanent_delete runs unchanged, including typing the word purge, and refusing it cancels the clean with it rather than proceeding to stage more.
  • Permanent deletion runs before the clean. A clean stages new entries under the same origins, so cleaning first would let a selector the reader aimed at an older staged copy reach the one they had just made.
  • The purge selector matches the origin recorded in the operation log. An entry with no recorded origin, and an interrupted-purge claim, are not selectable in the interface because --path could never name them; ambiguous entries stay selectable and carry the CLI's own warning, because the CLI warns about them rather than refusing them and the interface must refuse only where the CLI refuses.
  • Narrowing a purge changes which entries are destroyed, not the housekeeping every purge performs. Expired claims are still collected, because degu runs no background timer and that work rides on the next mutating command; a reader who only ever purges selectively would otherwise accumulate them forever.
  • An explicit selection stays atomic. The interface passes --path, so clean's existing rule applies: if one selected item fails sealed-staging preflight the whole batch is refused before any source, trash, or WAL mutation.
  • The interface refuses where the CLI refuses. A non-terminal on either stdin or stdout is rejected before the alternate screen is entered, because with only stdout checked a redirected stdin takes the screen and then waits for a key that can never arrive. A held mutation lock and an incomplete scan are refused by the commands themselves rather than by a second gate in the interface that could disagree with them.

@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4102dbdd-1a08-43e6-a55a-c597d80e526b


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@FeathBow
FeathBow force-pushed the feat/interactive-cleanup branch from 48fd85a to 1525b75 Compare September 16, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: decide and execute cleanup interactively

1 participant