diff --git a/docs/architecture.md b/docs/architecture.md index 4e9d90e..80baab6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/ecosystems.md b/docs/ecosystems.md index b28dcce..b11a2dd 100644 --- a/docs/ecosystems.md +++ b/docs/ecosystems.md @@ -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///X/.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///X/.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 diff --git a/internal/classifier/git.go b/internal/classifier/git.go index 9977456..30ff0bf 100644 --- a/internal/classifier/git.go +++ b/internal/classifier/git.go @@ -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 diff --git a/internal/cli/scan.go b/internal/cli/scan.go index 7166456..e31e0bb 100644 --- a/internal/cli/scan.go +++ b/internal/cli/scan.go @@ -47,7 +47,6 @@ func newScanCmd() *cobra.Command { return err } - // Sort sortResults(results, sortBy, reverse) if jsonOutput { diff --git a/internal/output/output_test.go b/internal/output/output_test.go index 602f421..00571ec 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -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",