Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
170 commits
Select commit Hold shift + click to select a range
50a897e
feat: expose physical cache reclaim
seonghobae Aug 26, 2026
ad10a5b
docs: record physical reclaim loop evidence
seonghobae Aug 26, 2026
080274f
fix: preserve cache purge results on journal errors
seonghobae Aug 26, 2026
baaa99f
docs: describe journal failure outcomes
seonghobae Aug 26, 2026
3c65760
docs: bind gap baseline to purge fix
seonghobae Aug 26, 2026
9a2a7fc
fix: bind cache purge to approved candidate set
seonghobae Aug 26, 2026
1979cb8
docs: track cache purge approval gate
seonghobae Aug 26, 2026
40588f4
test: cover pending cache journal failure
seonghobae Aug 26, 2026
7108266
docs: record customer cache guidance
seonghobae Aug 26, 2026
7624516
test: bind cache purge to reviewed snapshot
seonghobae Aug 26, 2026
49443f0
test: expose cache purge audit failures
seonghobae Aug 26, 2026
31b291a
fix: keep cache purge audit failures visible
seonghobae Aug 26, 2026
9760a17
fix: surface cache purge audit gaps
seonghobae Aug 26, 2026
4f6d7ef
fix: bind cache purge to reviewed snapshot
seonghobae Aug 26, 2026
36b2335
fix: route cache purge through reviewed snapshot
seonghobae Aug 26, 2026
3cd5d29
feat: make disk reclaim and customer copy actionable
seonghobae Aug 26, 2026
28131ff
test: verify reviewed cache snapshot purge
seonghobae Aug 26, 2026
967d040
fix: consume atomic cache trash review
seonghobae Aug 26, 2026
ed81ce3
fix: bind cache purge UI to one review snapshot
seonghobae Aug 26, 2026
987613c
Merge remote cache audit fixes
seonghobae Aug 26, 2026
96e6835
test: align cache UI contract with audit summary
seonghobae Aug 26, 2026
fefefb4
test: bind cache purge invocation to reviewed candidates
seonghobae Aug 26, 2026
5ee20ab
test: bind cache trash approval to root identity
seonghobae Aug 26, 2026
e5493d3
fix: bind permanent cache purge to root identity
seonghobae Aug 26, 2026
9ab1907
Merge reviewed cache snapshot implementation
seonghobae Aug 26, 2026
ae840d4
test: fail closed without object-bound cache purge
seonghobae Aug 26, 2026
d587976
fix: fail closed without final object-bound cache delete
seonghobae Aug 26, 2026
a235b82
test: preserve cache without object-bound permanent delete
seonghobae Aug 26, 2026
0e49a05
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
24e0785
test: disable cache purge affordance without object-bound delete
seonghobae Aug 26, 2026
21c9a4f
test: hide unavailable permanent cache purge action
seonghobae Aug 26, 2026
b6aef3d
feat: project fail-closed cache purge availability
seonghobae Aug 26, 2026
13f079f
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
d6c18c9
fix: suppress cache purge authority without object-bound delete
seonghobae Aug 26, 2026
9daabeb
fix: hide unsafe cache purge affordance
seonghobae Aug 26, 2026
8442df0
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
dffedc3
test: require fail-closed cache purge UI wiring
seonghobae Aug 26, 2026
a051d5f
docs: describe cache Trash purge fail-closed boundary
seonghobae Aug 26, 2026
1cf2f4d
Keep unavailable cache purge guidance actionable
seonghobae Aug 26, 2026
68c0859
docs: record fail-closed cache Trash review behavior
seonghobae Aug 26, 2026
22b2f27
docs: record object-bound cache purge requirement
seonghobae Aug 26, 2026
0102db7
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
d0ff1c0
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
d9cdf22
test: reject misleading cache-trash platform copy
seonghobae Aug 26, 2026
1028fc9
fix: enforce actionable customer-facing copy
seonghobae Aug 26, 2026
fe11990
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
7663bd7
test: reject misleading non-mac cache-trash support
seonghobae Aug 26, 2026
b8c3a26
fix: describe cache-trash support without overstating purge
seonghobae Aug 26, 2026
927cee9
fix: clarify cache trash deletion boundary
seonghobae Aug 26, 2026
9a22460
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
702440f
fix: distinguish macOS cache review from purge support
seonghobae Aug 26, 2026
8f0dc5f
test: bind cache-trash scope copy to availability contract
seonghobae Aug 26, 2026
26d34b1
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
9027c0f
fix: remove misleading cache-trash platform claim
seonghobae Aug 26, 2026
9045627
test: keep cache copy contract synchronized
seonghobae Aug 26, 2026
f0c3be2
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
cc423cd
refactor: remove dead cache-trash support state
seonghobae Aug 26, 2026
9dd965e
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
60085ba
chore: format cache trash guard
seonghobae Aug 26, 2026
d36efa9
test: refuse unsafe cache-trash CLI permanent deletion
seonghobae Aug 26, 2026
49ced5b
fix: fail closed on cache-trash CLI permanent delete
seonghobae Aug 26, 2026
bd54641
fix: keep cloud errors customer actionable
seonghobae Aug 26, 2026
8f22826
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
a2c0f47
fix: remove pathname-recursive cache-trash purge authority
seonghobae Aug 26, 2026
1e7b594
test: distinguish cache purge operation and audit failures
seonghobae Aug 26, 2026
c0a9cce
chore: format cache cleanup cli
seonghobae Aug 26, 2026
b2397f0
fix: distinguish cache purge operation and audit failures
seonghobae Aug 26, 2026
2b2185c
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
a219d09
fix: distinguish cache purge and journal failures
seonghobae Aug 26, 2026
cbb6ded
test: enforce bounded cache purge feedback
seonghobae Aug 26, 2026
4b90d3a
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
e5f2055
test: distinguish production date confidence labels
seonghobae Aug 26, 2026
57f8bf2
fix: label production date confidence accurately
seonghobae Aug 26, 2026
76a0005
test: gate platform-specific cache cleanup imports
seonghobae Aug 26, 2026
2970222
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
c6ddd73
test: bind purge error copy to its helper
seonghobae Aug 26, 2026
8189f17
fix: render production date confidence accurately
seonghobae Aug 26, 2026
a05a443
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
5f7a7e1
docs: align cache reclaim baseline with fail-closed behavior
seonghobae Aug 26, 2026
67786cb
fix: preserve cache cleanup CLI evidence shape
seonghobae Aug 26, 2026
83d9022
test: create nested trash paths portably
seonghobae Aug 26, 2026
df2a743
test: fail closed when Organize markup markers are absent
seonghobae Aug 26, 2026
674ac53
test: fail closed when Cleanup markup markers are absent
seonghobae Aug 26, 2026
69e6f4e
test: require cache cleanup handlers in coverage builds
seonghobae Aug 26, 2026
5c28b7c
test: validate customer markup boundaries safely
seonghobae Aug 26, 2026
6a9eb21
fix: keep cache cleanup handlers in coverage builds
seonghobae Aug 26, 2026
7d43bd0
Merge remote-tracking branch 'origin/feat/physical-cache-reclaim-v1' …
seonghobae Aug 26, 2026
869f86c
docs: record current customer-copy and PR evidence
seonghobae Aug 26, 2026
b85a31a
fix: keep cache review actionable when purge is blocked
seonghobae Aug 26, 2026
c036d7c
docs: record follow-up customer-copy fixes
seonghobae Aug 26, 2026
7cd2e62
docs: bind gap baseline to latest exact head
seonghobae Aug 26, 2026
68d4a9f
docs: record current disk and worktree evidence
seonghobae Aug 26, 2026
a7a5241
fix: align cache trash CLI guidance
seonghobae Aug 26, 2026
c3abfe2
docs: record cache CLI guidance fix
seonghobae Aug 26, 2026
b4acaaf
fix: enforce actionable customer guidance
seonghobae Aug 26, 2026
d570d37
docs: record customer copy and cache boundary
seonghobae Aug 26, 2026
c360185
copy: guide empty reconciliation state
seonghobae Aug 26, 2026
7787c06
fix: separate cache purge retry outcomes
seonghobae Aug 26, 2026
6b2be59
docs: record cache purge outcome states
seonghobae Aug 26, 2026
09e1fd7
test: remove unused customer-copy fixture read
seonghobae Aug 26, 2026
92df2ed
fix(release): include artifact compatibility verifier
seonghobae Aug 26, 2026
dacedc0
copy: explain when Trash space is reclaimed
seonghobae Aug 26, 2026
2f3d130
docs: record release verifier and Trash guidance
seonghobae Aug 26, 2026
841b157
docs: keep physical reclaim out of canonical baseline owner
seonghobae Aug 26, 2026
e635aae
feat: suggest repository retention reference
seonghobae Aug 26, 2026
38a7668
test: cover retention reference fallback
seonghobae Aug 26, 2026
e31243b
docs: record reclaim review boundaries
seonghobae Aug 26, 2026
0d07044
merge main into physical reclaim lane
seonghobae Aug 26, 2026
9d1b349
fix: align release artifact verifier with Windows runner
seonghobae Aug 26, 2026
70623ca
test: require visible iCloud eviction approval value
seonghobae Aug 26, 2026
df5d6ab
fix: show iCloud eviction approval value
seonghobae Aug 26, 2026
2133381
chore: keep release verifier repair in canonical owner
seonghobae Aug 26, 2026
66fc7d0
security: converge cache-trash fail-closed repair on current main
seonghobae Aug 26, 2026
e0e75bc
refactor: remove unreachable cache-trash purge result
seonghobae Aug 26, 2026
ccdb035
Merge remote-tracking branch 'origin/fix/release-artifact-windows-nam…
seonghobae Aug 26, 2026
87b4781
chore: keep cache-trash PR within owner boundary
seonghobae Aug 26, 2026
6deb5ea
chore: restore release verifier ownership boundary
seonghobae Aug 26, 2026
299c94f
fix: verify Windows release artifact namespace
seonghobae Aug 26, 2026
6355c57
fix: bind release artifacts to platform directories
seonghobae Aug 26, 2026
34f6e22
fix: verify tag artifacts before sbom
seonghobae Aug 26, 2026
d4e7f82
chore: restore cache-trash owner boundary
seonghobae Aug 26, 2026
f797100
chore: preserve release verifier ownership
seonghobae Aug 26, 2026
766e45f
fix: keep cache journal helpers in coverage builds
seonghobae Aug 26, 2026
a4bdafa
fix: honor XDG trash location for current home
seonghobae Aug 26, 2026
1716fed
test: bind Linux cache-trash evidence to XDG data home
seonghobae Aug 26, 2026
e850498
test: cover XDG trash routing
seonghobae Aug 26, 2026
672bede
Revert "chore: preserve release verifier ownership"
seonghobae Aug 26, 2026
a40429e
Revert "chore: restore cache-trash owner boundary"
seonghobae Aug 26, 2026
e70f3d8
chore: keep release verifier owned by release PR
seonghobae Aug 26, 2026
ab648af
chore: remove release verifier mode drift
seonghobae Aug 26, 2026
62f10ad
fix: match Windows release artifact runner
seonghobae Aug 26, 2026
fe4fa72
fix(ui): keep customer copy actionable
seonghobae Aug 26, 2026
567a945
test(ui): guard customer copy markup bounds
seonghobae Aug 26, 2026
bf61404
fix(ui): remove duplicate cloud guidance
seonghobae Aug 26, 2026
695f861
fix(ui): keep all customer guidance actionable
seonghobae Aug 26, 2026
6ecfb5d
chore: restore cache-trash owner boundary
seonghobae Aug 27, 2026
c90ed5a
refactor: remove unreachable cache purge success payload
seonghobae Aug 27, 2026
95df094
test: reject duplicate cache cleanup authority options
seonghobae Aug 27, 2026
b6aedc1
fix: reject duplicate cache cleanup authority options
seonghobae Aug 27, 2026
3e8af4d
fix(ui): enforce actionable customer copy boundary
seonghobae Aug 28, 2026
0603583
feat(cache): include Playwright browser cache
seonghobae Aug 28, 2026
137680e
chore: reconverge cache-trash repair scope
seonghobae Aug 28, 2026
f156f04
Merge remote-tracking branch 'origin/main' into feat/physical-cache-r…
seonghobae Aug 29, 2026
0476e43
fix(cache): bound invalid argument diagnostics
seonghobae Aug 29, 2026
adf22e9
fix: exclude provider-managed cache roots
seonghobae Aug 29, 2026
b96a9e8
test: hide managed provider cache candidates
seonghobae Aug 29, 2026
93e5d56
fix: hide managed provider cache roots
seonghobae Aug 29, 2026
a27aefa
test: reject provider cache aliases
seonghobae Aug 30, 2026
2ef4243
merge: converge release verifier into cache safety owner
seonghobae Aug 30, 2026
ef34a90
test: reject cache aliases into managed provider storage
seonghobae Aug 31, 2026
74e2ff1
test: compile provider alias regression against production rules
seonghobae Aug 31, 2026
03f9042
fix: bind cache roots to resolved provider boundary
seonghobae Aug 31, 2026
56ede53
test: provide production-rules identity stub
seonghobae Aug 31, 2026
2350e81
chore: converge cache-trash safety onto current main
seonghobae Sep 3, 2026
5bdbadf
fix: restore release verifier ownership boundary
seonghobae Sep 3, 2026
d6be416
fix: preserve protected verifier file mode
seonghobae Sep 3, 2026
78bae57
fix(stack): bind cache safety repair to release verifier owner
seonghobae Sep 3, 2026
96b6eaf
fix(stack): preserve complete release verifier foundation
seonghobae Sep 3, 2026
abc7d85
test: lock fail-closed cache-trash operator guidance
seonghobae Sep 3, 2026
c860ab0
docs: make cache-trash operator guidance fail closed
seonghobae Sep 3, 2026
80b5a5c
test: require superseding cache-trash safety ADR
seonghobae Sep 3, 2026
2e02269
docs: supersede unsafe cache-trash deletion decision
seonghobae Sep 3, 2026
1242d3c
docs: propose fail-closed cache-trash deletion ADR
seonghobae Sep 3, 2026
3cd1988
docs: index proposed cache-trash safety decision
seonghobae Sep 3, 2026
96c38af
merge(stack): inherit release binary isolation contracts
seonghobae Sep 3, 2026
c52c649
chore(stack): adopt current release foundation
seonghobae Sep 4, 2026
0401a4f
chore(stack): adopt current release foundation
seonghobae Sep 4, 2026
b3b52fe
chore(stack): adopt exact release contract repair
seonghobae Sep 4, 2026
f25f51a
chore: restack cache-trash security on current release owner
seonghobae Sep 4, 2026
d9b7fcf
chore(stack): restack cache-trash security on current release owner
seonghobae Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and

### Security

- Return a stable cache-cleanup argument error without reflecting an untrusted option payload into
terminal or automation logs; permanent cache-Trash deletion remains fail closed.
- Default personal cloud-provider OAuth consent to read-only; upload scope and API write
authority now require an explicit user opt-in.
- Catalog the Cargo registry source tree as an explicit, identity-bound regenerable-cache target;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0002: Cache cleanup is per-item active-use evidence bound

**Status:** Accepted
**Status:** Superseded by ADR-0012
**Date:** 2026-08-20

## Context
Expand Down Expand Up @@ -99,5 +99,6 @@ applies to user files or cloud-provider placeholders.
## References

- [ADR-0001: Provider evidence drives the cloud-offload Goal](0001-cloud-offload-goal-state.md)
- [ADR-0012: Cache Trash permanent deletion fails closed](0012-cache-trash-permanent-delete-fails-closed.md)
- `src-tauri/src/cache_cleanup.rs`
- `src-tauri/src/rules.rs`
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# ADR-0012: Cache Trash permanent deletion fails closed

**Status:** Proposed
**Date:** 2026-09-03
**Supersedes:** ADR-0002 only for permanent deletion of cache entries already in OS Trash

## Context

ADR-0002 allowed a separate `--execute --purge-proven-cache-trash` path to permanently remove
structurally recognized cache directories from operating-system Trash after pathname-based
revalidation. Subsequent implementation review showed that the final irreversible deletion syscall
was not bound to the exact reviewed filesystem object. A pathname can be replaced after review and
before recursive removal, so the earlier policy could not satisfy DiskSage's deletion-safety
boundary even when the candidate name, structure, size, and symlink checks were repeated.

DiskSage already has a reversible, identity-bound cleanup path that moves inactive regenerable cache
children into OS Trash. Permanent removal is different: once Trash is bypassed there is no product
undo boundary, so evidence that is sufficient for staging is not sufficient for irreversible
deletion.

## Decision

DiskSage does not perform in-app permanent deletion of reviewed cache-Trash entries until the final
irreversible filesystem operation can be bound to the exact object that was reviewed and approved.

- `--purge-proven-cache-trash` remains a read-only evidence operation.
- `--execute --purge-proven-cache-trash` returns
`cache-trash-identity-bound-permanent-delete-unavailable` before journal or filesystem mutation.
- The library boundary also fails closed and does not call pathname-recursive permanent-deletion
primitives.
- Candidate names, signatures, byte counts, and approval phrases are review evidence only; they do
not create irreversible mutation authority.
- Operators who intend permanent reclaim must inspect the candidate evidence and empty the native
Trash manually through the operating system. DiskSage does not claim those bytes as physically
reclaimed until the operating system reports the resulting availability change.
- User files, cloud-provider placeholders, and arbitrary Trash entries remain outside this cache
evidence path.

This decision leaves ADR-0002's per-item active-use checks and reversible OS-Trash staging intact.
Only its separate permanent-delete authorization is superseded.

## Consequences

- The CLI and desktop remain conservative under disk pressure: they can identify regenerable cache
material but cannot silently turn that evidence into an irreversible delete.
- Automation receives a stable refusal code rather than a partial journal or ambiguous success
receipt.
- Physical space recovery may require an explicit operating-system Trash action after DiskSage has
completed its reversible cleanup.
- A future permanent-delete capability requires a new or superseding ADR, a real object-bound
deletion primitive for each supported platform, race/alias/mount/hardlink tests, recovery and
audit semantics, and current-head release evidence before it can become Accepted.

## Alternatives rejected

- **Keep pathname revalidation plus recursive deletion.** Rejected because repeated pathname checks
do not bind the final syscall to the reviewed object and leave a check/use race at an irreversible
boundary.
- **Treat a candidate-set approval phrase as delete authority.** Rejected because a phrase proves
what the user reviewed, not that the pathname still names the same filesystem object at mutation
time.
- **Delete first and rely on the journal for recovery.** Rejected because a journal cannot restore an
object after a genuinely permanent delete and journal failure can itself occur after mutation.
- **Broaden automatic cleanup instead of using Trash.** Rejected because reversible OS-Trash staging
is the product's established safety and recovery boundary for regenerable cache content.

## Evidence and acceptance

The production CLI regression creates a real cache-shaped directory under a temporary Trash,
invokes `--execute --purge-proven-cache-trash`, and requires the refusal code while proving both the
cache object and journal remain untouched. Documentation contract coverage requires the runbook and
ADR index to describe the same fail-closed behavior.

This ADR remains Proposed while the implementing PR is unmerged. Acceptance requires an unchanged
exact head with the repository's required tests, security gates, coverage, review, and release
verification all passing under live protection rules.

## References

- [ADR-0002: Cache cleanup is per-item active-use evidence bound](0002-cache-cleanup-is-per-item-evidence-bound.md)
- `src-tauri/src/bin/disksage-cache-cleanup.rs`
- `src-tauri/src/cache_cleanup.rs`
- `src-tauri/tests/cache_cleanup_cli_purge_fail_closed.rs`
- [Cache cleanup operator runbook](../../development/cache-cleanup-operator-runbook.md)
3 changes: 2 additions & 1 deletion docs/architecture/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ new numbered record rather than rewriting history.
| ADR | Decision | Status |
| --- | --- | --- |
| [0001](0001-cloud-offload-goal-state.md) | Provider evidence drives the cloud-offload Goal | Accepted |
| [0002](0002-cache-cleanup-is-per-item-evidence-bound.md) | Cache cleanup is per-item evidence-bound | Accepted |
| [0002](0002-cache-cleanup-is-per-item-evidence-bound.md) | Cache cleanup is per-item evidence-bound | Superseded by 0012 for permanent cache-Trash deletion |
| [0003](0003-zotero-local-api-metadata-handoff.md) | Zotero Local API metadata handoff | Accepted |
| [0004](0004-bounded-maintenance-command-execution.md) | Bounded maintenance command execution | Accepted |
| [0005](0005-hourly-agent-loop-is-advisory.md) | Hourly agent loop is advisory | Superseded by 0008 |
Expand All @@ -17,6 +17,7 @@ new numbered record rather than rewriting history.
| [0009](0009-path-free-lineage-relation-graph.md) | Export a path-free lineage relation graph | Accepted |
| [0010](0010-rooted-organize-destinations.md) | Require rooted, process-independent organize destinations | Accepted |
| [0011](0011-cloud-transfer-failure-and-materialization.md) | Durable failed-copy evidence and placeholder-safe adoption | Accepted |
| [0012](0012-cache-trash-permanent-delete-fails-closed.md) | Cache Trash permanent deletion fails closed | Proposed |

New records must state context, decision, consequences, rejected alternatives, and the evidence or
standard that led to the decision. A record never grants cloud-write or source-eviction authority;
Expand Down
13 changes: 9 additions & 4 deletions docs/development/cache-cleanup-operator-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@ If the Trash is consuming space, inspect only structurally proven cache entries

`cargo run --locked --manifest-path src-tauri/Cargo.toml --bin disksage-cache-cleanup -- --purge-proven-cache-trash --journal-path /ABSOLUTE/journal.jsonl`

The command is read-only until both `--execute` and `--purge-proven-cache-trash` are supplied.
That explicit path permanently removes only the known cache signatures already in OS Trash; it
does not empty Trash generally and never targets cloud placeholders or user files. Review its JSON
result and journal before treating the reported bytes as reclaimed.
This command is read-only evidence. Supplying `--execute` together with
`--purge-proven-cache-trash` is intentionally refused with
`cache-trash-identity-bound-permanent-delete-unavailable` before journal or filesystem mutation,
because DiskSage does not yet have an object-bound primitive for the final irreversible deletion
syscall. Review the JSON candidate evidence, then empty the native Trash manually through the
operating system when permanent reclaim is intended. Do not treat candidate logical bytes as
physically reclaimed until the operating system reports the resulting availability change.
DiskSage never treats this review path as authority to empty Trash generally or to mutate cloud
placeholders or user files.

The cache catalog includes the macOS `uv`, Hugging Face, Codex runtime, Gradle, npm, pip, and Cargo
registry cache/source roots when present. The Cargo registry source root is catalogued for explicit
Expand Down
120 changes: 89 additions & 31 deletions src-tauri/src/bin/disksage-cache-cleanup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
//! Without `--execute` this command is read-only. With it, the library path moves only inactive,
//! identity-bound children of the npm, pnpm, Adobe, Edge, uv, and Trivy cache roots to OS Trash.

use disksage_lib::cache_cleanup::{
clean_regenerable_caches_headless, proven_cache_trash_candidates, purge_proven_cache_trash,
};
use disksage_lib::cache_cleanup::{clean_regenerable_caches_headless, proven_cache_trash_snapshot};
use std::ffi::OsString;
use std::path::PathBuf;

const PERMANENT_CACHE_TRASH_DELETE_UNAVAILABLE: &str =
"cache-trash-identity-bound-permanent-delete-unavailable";
const USAGE: &str = "Usage: disksage-cache-cleanup [--execute] [--purge-proven-cache-trash] [--journal-path PATH]\n\
Without --execute it reports the command is a no-op. With --execute it moves only observed,\n\
inactive regenerable cache children to OS Trash. --purge-proven-cache-trash permanently removes\n\
only structurally proven cache directories already in OS Trash.";
inactive regenerable cache children to OS Trash. --purge-proven-cache-trash is read-only evidence;\n\
permanent in-app deletion remains unavailable until the final syscall is object-bound.";

fn read_only_notice(purge_proven_cache_trash: bool) -> &'static str {
if purge_proven_cache_trash {
"proven cache-Trash review is read-only; empty the native Trash manually to reclaim space; --execute cannot enable permanent deletion"
} else {
"pass --execute to move guarded cache children to OS Trash"
}
}

#[derive(Debug, PartialEq, Eq)]
struct Args {
Expand Down Expand Up @@ -69,12 +77,31 @@ fn parse_args(raw_args: impl IntoIterator<Item = OsString>) -> Result<Option<Arg
let mut execute = false;
let mut purge_proven_cache_trash = false;
let mut journal_path = default_journal_path()?;
let mut seen_execute = false;
let mut seen_purge_proven_cache_trash = false;
let mut seen_journal_path = false;
let mut args = first_arg.into_iter().chain(args);
while let Some(arg) = args.next() {
match arg.to_str() {
Some("--execute") => execute = true,
Some("--purge-proven-cache-trash") => purge_proven_cache_trash = true,
Some("--execute") => {
if seen_execute {
return Err("--execute may be supplied once".into());
}
seen_execute = true;
execute = true;
}
Some("--purge-proven-cache-trash") => {
if seen_purge_proven_cache_trash {
return Err("--purge-proven-cache-trash may be supplied once".into());
}
seen_purge_proven_cache_trash = true;
purge_proven_cache_trash = true;
}
Some("--journal-path") => {
if seen_journal_path {
return Err("--journal-path may be supplied once".into());
}
seen_journal_path = true;
journal_path = PathBuf::from(
args.next()
.ok_or_else(|| "--journal-path requires PATH".to_string())?,
Expand All @@ -84,7 +111,7 @@ fn parse_args(raw_args: impl IntoIterator<Item = OsString>) -> Result<Option<Arg
}
}
Some("-h" | "--help") => return Err(format!("--help must be used alone\n{USAGE}")),
Some(value) => return Err(format!("unknown option: {value}\n{USAGE}")),
Some(_) => return Err(format!("cache-cleanup-invalid-argument\n{USAGE}")),
None => return Err(format!("invalid UTF-8 option\n{USAGE}")),
}
}
Expand All @@ -108,11 +135,18 @@ fn run_with_args(raw_args: impl IntoIterator<Item = OsString>) -> Result<(), Str
return Ok(());
};
if !args.execute {
let cache_trash = if args.purge_proven_cache_trash {
serde_json::to_value(proven_cache_trash_candidates(&home_directory()?))
.map_err(|error| error.to_string())?
let notice = read_only_notice(args.purge_proven_cache_trash);
let (cache_trash, cache_trash_snapshot) = if args.purge_proven_cache_trash {
let snapshot = proven_cache_trash_snapshot(&home_directory()?);
let candidates =
serde_json::to_value(&snapshot.candidates).map_err(|error| error.to_string())?;
let snapshot = serde_json::to_value(snapshot).map_err(|error| error.to_string())?;
(candidates, snapshot)
} else {
serde_json::Value::Array(Vec::new())
(
serde_json::Value::Array(Vec::new()),
serde_json::Value::Null,
)
};
println!(
"{}",
Expand All @@ -121,27 +155,18 @@ fn run_with_args(raw_args: impl IntoIterator<Item = OsString>) -> Result<(), Str
"journal_path": args.journal_path,
"purge_proven_cache_trash": args.purge_proven_cache_trash,
"proven_cache_trash": cache_trash,
"notice": "pass --execute to perform the guarded OS-Trash operation"
"proven_cache_trash_snapshot": cache_trash_snapshot,
"notice": notice
})
);
return Ok(());
}
if args.purge_proven_cache_trash {
return Err(PERMANENT_CACHE_TRASH_DELETE_UNAVAILABLE.into());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
seonghobae marked this conversation as resolved.
if let Some(parent) = args.journal_path.parent() {
std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
}
if args.purge_proven_cache_trash {
let results = purge_proven_cache_trash(&home_directory()?, &args.journal_path, now_ms())?;
println!(
"{}",
serde_json::json!({
"executed": true,
"purge_proven_cache_trash": true,
"journal_path": args.journal_path,
"results": results
})
);
return Ok(());
}
let evidence = clean_regenerable_caches_headless(&args.journal_path, now_ms())?;
println!(
"{}",
Expand Down Expand Up @@ -172,11 +197,8 @@ mod tests {

#[test]
fn help_must_be_used_alone() {
let error = parse_args([
OsString::from("--help"),
OsString::from("--execute"),
])
.unwrap_err();
let error =
parse_args([OsString::from("--help"), OsString::from("--execute")]).unwrap_err();
assert!(error.starts_with("--help must be used alone"));
}

Expand All @@ -190,6 +212,34 @@ mod tests {
assert_eq!(error, "--journal-path must be absolute");
}

#[test]
fn duplicate_authority_singletons_are_rejected() {
let duplicate_execute = parse_args([
OsString::from("--execute"),
OsString::from("--execute"),
])
.unwrap_err();
assert_eq!(duplicate_execute, "--execute may be supplied once");

let duplicate_purge = parse_args([
OsString::from("--purge-proven-cache-trash"),
OsString::from("--purge-proven-cache-trash"),
])
.unwrap_err();
assert_eq!(
duplicate_purge,
"--purge-proven-cache-trash may be supplied once"
);
}

#[test]
fn unknown_argument_is_not_reflected() {
let payload = "--unknown-with-sensitive-value";
let error = parse_args([OsString::from(payload)]).unwrap_err();
assert!(error.contains("cache-cleanup-invalid-argument"));
assert!(!error.contains(payload));
}

#[test]
fn purge_cache_trash_flag_is_explicit() {
let args = parse_args([OsString::from("--purge-proven-cache-trash")])
Expand All @@ -198,4 +248,12 @@ mod tests {
assert!(!args.execute);
assert!(args.purge_proven_cache_trash);
}

#[test]
fn read_only_notice_matches_the_requested_action() {
assert!(read_only_notice(false).contains("pass --execute"));
assert!(read_only_notice(true).contains("read-only"));
assert!(read_only_notice(true).contains("empty the native Trash"));
assert!(!read_only_notice(true).contains("pass --execute to move"));
}
}
Loading
Loading