Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 13 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,19 @@ type Scanner interface {
}
```

Scanners report progress via context-attached callbacks for real-time UI updates; the walk batch reports under a single "projects" label, stat scanners under their own names. Sizing collects two figures per artifact via an in-process walk (`scanner.Measure`): **disk** (allocated blocks, `st_blocks×512` — sparse-accurate and matching `du`) and **apparent** (sum of logical file sizes). Directories contribute their own blocks to disk (real on ext4, ~0 on APFS); symlinks are never followed. Hard-linked inodes (`Nlink>1`) are counted once per artifact and recorded (keyed by `(dev, ino)`) so shared blocks can be netted out across artifacts. The walk engine does not size inline — it collects every matched artifact during the single pass, then sizes them across a bounded worker pool (`min(NumCPU, 8)`) so the traversals and their I/O overlap. The stat scanners (global/xcode/llm) size the same way: they discover artifacts via `stat` first, then defer to that same `sizePending` pool instead of measuring each inline. Disk is the primary figure (sorting, `--min-size`, totals); apparent surfaces only when a file is materially sparse.
Scanners report progress via context-attached callbacks: the walk batch reports under a single "projects" label, stat scanners under their own names.

**Sizing** collects two figures per artifact through an in-process walk (`scanner.Measure`):

- **disk** — allocated blocks (`st_blocks×512`), sparse-accurate and matching `du`. The primary figure: sorting, `--min-size`, and totals all use it. Directories contribute their own blocks (real on ext4, ~0 on APFS).
- **apparent** — sum of logical file sizes. Surfaces only when a file is materially sparse.

Two invariants keep the figures honest:

- **Hard links** (`Nlink>1`) are counted once per artifact, keyed by `(dev, ino)`, so shared blocks net out across artifacts.
- **Symlinks** are never followed.

Neither scanner family sizes inline. Each collects its artifacts first — the walk during its single pass, stat scanners via `stat` — then sizes them through one shared bounded worker pool (`sizePending`, `min(NumCPU, 8)`) so the tree-walk I/O overlaps.

### Walk engine

Expand Down
20 changes: 18 additions & 2 deletions docs/ecosystems.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,25 @@ Each entry that is `caution` carries a consequence-of-deletion note in `recommen
| `~/Library/Application Support/Cursor/{Cache,CachedData,Code Cache}` | cache | safe (cache subdirs only — settings live alongside) |
| `/private/var/folders/*/*/X/*.code_sign_clone` | cache | safe / caution (macOS only — Browser Temp, see below) |

**Config roots and user state are deliberately excluded.** A home dotfile is treated as config unless it is unambiguously a package/build cache (like `~/.cache/uv`). Excluded: the whole `~/.claude` tree (session transcripts, project memory, agents, skills, plugins, todos), `~/.codex`, `~/.gemini`, Claude Code's `~/Library/Caches/claude-cli-nodejs`, `~/.cursor` (extensions & settings), `~/.gem` (holds the RubyGems credential + installed gems), and `~/.android/avd` (emulator user data). Deleting any of it is unrecoverable loss or credential loss, and the "caches" inside only reappear as install-time scaffolding — so reclaiming them is worthless against that risk. devclean never offers them for deletion. Only genuine caches under those trees (e.g. `~/.cargo/registry`, `~/Library/Application Support/Cursor/Cache`) or dedicated cache dirs remain eligible.
**Config roots and user state are deliberately excluded.** A home dotfile is treated as config unless it is unambiguously a package/build cache (like `~/.cache/uv`). Deleting an excluded tree is unrecoverable or credential loss, and the "caches" inside only reappear as install-time scaffolding — worthless against that risk, so devclean never offers them for deletion.

**Browser Temp (macOS)**: Chromium-family browsers (Chrome, Brave, Edge, Arc, Vivaldi, …) copy their own bundle to `/private/var/folders/<xx>/<yyy>/X/<bundle-id>.code_sign_clone/` on launch to verify their code signature and remove the copy on normal exit. Force-killed processes — typically headless automation like lighthouse or puppeteer — leave zombie copies that accumulate (observed: 92 copies / 156 GB). Matching uses a single `*.code_sign_clone` glob rather than a per-browser catalog; the label carries the browser name (derived from the bundle ID) and the copy count. Safety follows run state: `safe` when the browser is not running (true zombies), `caution` while it runs (checked via `pgrep`, once per browser — the newest copy may be in use) or when the bundle ID is unrecognized (run state unknowable). Because the path lies outside home, it is reported only when the scan root covers the home directory — a `--path` scan of a home subdirectory never surfaces system temp. Reported size may overstate real usage when the copies are APFS clones of the installed app.
| Excluded path | Why |
|---|---|
| `~/.claude` (whole tree) | session transcripts, project memory, agents, skills, plugins, todos |
| `~/.codex`, `~/.gemini` | agent CLI state |
| `~/Library/Caches/claude-cli-nodejs` | Claude Code state |
| `~/.cursor` | extensions & settings |
| `~/.gem` | RubyGems credential + installed gems |
| `~/.android/avd` | emulator user data |

Only genuine caches under those trees (e.g. `~/.cargo/registry`, `~/Library/Application Support/Cursor/Cache`) or dedicated cache dirs remain eligible.

**Browser Temp (macOS)**: Chromium-family browsers (Chrome, Brave, Edge, Arc, Vivaldi, …) copy their own bundle to `/private/var/folders/<xx>/<yyy>/X/<bundle-id>.code_sign_clone/` on launch to verify their code signature, removing it on normal exit. Force-killed processes — typically headless automation like lighthouse or puppeteer — leave zombie copies that accumulate (observed: 92 copies / 156 GB).

- **Matching**: a single `*.code_sign_clone` glob, not a per-browser catalog; the label carries the browser name (from the bundle ID) and the copy count.
- **Safety follows run state**: `safe` when the browser is not running (true zombies); `caution` while it runs (checked via `pgrep`, once per browser — the newest copy may be in use) or when the bundle ID is unrecognized (run state unknowable).
- **Scope**: the path lies outside home, so it is reported only when the scan root covers the home directory — a `--path` scan of a home subdirectory never surfaces system temp.
- **Size caveat**: reported size may overstate real usage when the copies are APFS clones of the installed app.

## LLM Model Stores

Expand Down
1 change: 0 additions & 1 deletion internal/classifier/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,6 @@ func ApplyGitInfo(results []model.ScanResult) {

info := cache[gitRoot]

// Set project root to git root
results[i].ProjectRoot = gitRoot

// Use the most recent of: artifact mtime, git commit time, project dir mtime
Expand Down
1 change: 0 additions & 1 deletion internal/cli/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ func newScanCmd() *cobra.Command {
return err
}

// Sort
sortResults(results, sortBy, reverse)

if jsonOutput {
Expand Down
4 changes: 2 additions & 2 deletions internal/output/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ func sampleResults() []model.ScanResult {
}

// TestWriteTableSparseAnnotation verifies a materially sparse artifact renders
// its apparent size alongside disk — the point of A1. Full render-path test:
// it pins that the artifact size cell actually routes through sizeCell.
// its apparent size alongside disk. Full render-path test: it pins that the
// artifact size cell actually routes through sizeCell.
func TestWriteTableSparseAnnotation(t *testing.T) {
sparse := model.ScanResult{
Path: "/Users/dev/proj/node_modules",
Expand Down