Releases: RasputinKaiser/StorageScope
Release list
v0.7.1 — Window Polish and Keyboard Navigation
StorageScope v0.7.1
Changes
- Polished the first-run window sizing and launch layout so cold launch opens cleanly.
- Added keyboard-driven cleanup navigation and focused reveal behavior in the folder tree.
- Refreshed README and GitHub Pages screenshots for both cold launch and post-scan states.
- Updated the public docs, changelog, sitemap, and download links for v0.7.1.
- Fixed the DMG checksum sidecar so it records the release filename instead of a local absolute path.
Verification
- Swift test suite passed locally: 200 tests in 20 suites.
- Public upload audit passed.
- DMG verified with hdiutil and mounted app reports CFBundleShortVersionString 0.7.1.
- Mounted app code signature verified with codesign --verify --deep --strict.
- Exact public path/name/key scan was clean for the mounted app, DMG, and checksum sidecar.
Distribution note
This DMG is ad-hoc signed, matching the existing local release flow. Notarization is still required before broad public distribution.
SHA-256:
232639a4fd56c77a667d135ab4523e7c66762211ad4e9989dca17678855f11fd StorageScope-0.7.1.dmg
v0.7.0 — Scan Control, Privacy, and a Faster Interface
Everything merged since v0.6.0 (PRs #91–#117).
Scan control (#114)
- Folder exclusion list — Settings gains an on/off toggle plus an editable list of excluded folders (seeded with
node_modules,.git,~/Library/Caches,~/Library/Application Support). Excluded folders are skipped entirely during traversal, not just hidden from results. A right-click "Exclude This Folder" action is available in Largest Files/Folders, Tree Explorer, Cleanup Review, and Duplicate Review (where it targets the file's containing folder). - Live streaming results — the scanner emits throttled in-progress snapshots, so ranked views populate and re-sort while a scan is still running instead of showing "No Items" until it finishes.
- Cancel keeps partial results — cancelling leaves the last streamed snapshot browsable (marked partial) instead of resetting to an empty window.
- Pause/resume — long scans pause and resume from the toolbar (in-memory only; does not survive quitting).
- Rate/elapsed readout — the scan header shows an EMA-smoothed items/sec estimate and elapsed time.
Privacy (#113)
- Redaction toggle — a new Privacy setting masks file/folder names and paths across the entire UI (inspector, tables, tree, duplicates, cleanup, sheets, tooltips, accessibility labels) with stable per-session placeholders like "File 3.mp4" / "Folder 1". Sizes, dates, and counts stay real, and Trash/move/reveal/copy-path always operate on real paths — masking is display-layer only. Built for screen-sharing and screenshots.
Fixes
- Settings controls appeared stuck (#116) —
FilterStorechanges never propagated intoScanStore.objectWillChange, so toggles/steppers didn't visually update until unrelated navigation forced a re-render. Pre-existing bug (reproduces on v0.6.0), caught during redaction verification. - Tree / Storage Map rendered empty on large scans (#112) — depth-first retention exhausted the shared budget before the scan root's own children were retained. Root-level children are now always kept.
- Duplicate detection found almost nothing in typical folders (#112) — default candidate threshold lowered 100 MB → 10 MB, now user-tunable via a Settings stepper; empty states name the active threshold instead of misdirecting.
- Verify Now polish — re-verify taps on an already-completed group are a guaranteed no-op (#105); verification that finds no duplicate groups no longer invalidates caches (#106); cancelling mid-verify clears the spinner correctly and persist failures surface instead of dying silently (#103, #109).
- "Clear Filters" no longer renders as a dead button when no filters are active (#112).
- Search debounce is cancellable and recents persistence is serialized — rapid typing can't resurrect stale queries or interleave writes (#93).
Error surfacing & reliability
- Scan and bookmark failures are categorized (permission denied / missing folder / stale bookmark) and alerts name the affected path instead of a generic message (#96, #102, #108).
- Reveal/Open/Copy Path failures now surface through the standard error alert instead of no-op'ing (#100), including in the keeper comparison sheet (#109).
- Duplicate hash cache I/O errors are reported instead of swallowed (#98); the scanner's error propagation, cancellation checks, and hashing budget policy were tightened (#97).
- Transactional Trash moves preflight in a single pass and capture partial-rollback state so a failed batch reports exactly what moved back (#95); missing-on-disk selections are reported before the move starts (#94).
scan(_:)runs in a structured TaskGroup with a re-entry guard so rapid rescans can't race two scans against each other (#99).
Performance
Scan engine
autoreleasepoolaround the parallel enumeration hot loop cuts peak memory on broad scans; SHA-256 hex encoding uses a lookup table instead of per-byteString(format:)(#112).- Cleanup selection planning collapsed from O(N²) ancestor scans to a hash-join pass (#94).
Interface
- Per-row store subscriptions eliminated; the inspector uses native styling (#107);
StorageItemRowregainedEquatablediff-skipping (#92). - Derived-cache keys normalize irrelevant filter dimensions, so no-op filter changes stop wiping still-valid caches (#101).
- Type Breakdown's up-to-4000-row lists use
LazyVStack; mounted-volume enumeration is cached instead of hittingFileManagerevery sidebar render (#112). - Size bars in Tree Explorer, Type Breakdown, and the Overview storage map are drawn with a shared
Canvasinstead of a per-rowGeometryReader, removing a layout pass per visible row; type/category breakdowns cache per scan + query; "Expand All" enablement no longer re-walks the retained tree; sidebar volume capacity strings no longer do synchronous disk I/O per render (#117).
Website
- The project site was redesigned: dark, data-forward landing page with an animated scan-window hero, and every page (guides, FAQ, privacy, changelog, shortcuts, architecture) unified on one shared design system and nav (#117).
Install
Download StorageScope-0.7.0.dmg, open it, and drag StorageScope to Applications. Requires macOS 14+.
Verify the download:
shasum -a 256 -c StorageScope-0.7.0.dmg.sha256
v0.6.0 — Visual Refresh
What's new in 0.6.0
Visual refresh across the entire app
Welcome screen — opening StorageScope with no scan now shows a full-width hero spanning both columns, with a spring transition when a scan starts or completes. The welcome view was extracted into its own module (WelcomeView.swift, PR #90) and polished into a hero layout. Empty-state ownership also moved out of ContentView into DetailView, so DetailView now branches between WelcomeView (overview), a new NoScanPlaceholder for other sidebar selections, and the normal scan-related layout.
Inspector redesign — the inspector panel now leads with a tinted icon header (coloured rounded-rect background matching the file type), shows attributes in a two-column grid, and uses a clear button hierarchy: a prominent primary action for Reveal in Finder, compact secondary buttons for Open and Copy Path, and a role-appropriate Trash button. The empty state shows cleaner copy.
Card radius 12 — all card backgrounds bumped from 8 → 12 pt, keeping inner bars and elements concentric.
Storage Map — the Overview's storage map is now a single unified card with dividers between rows instead of ten individually-wrapped cards. Each row highlights on hover.
Hover states — all interactive list rows (Storage Map, Largest Files/Folders, Old Large Files, Folder Tree, Cleanup Review, Duplicate Review) now show a subtle hover highlight.
Metric cards — the four header cards (Footprint, Items, Large Files, Reviewable) use a more opaque material so their boundaries are visible, and numeric values use tabular figures throughout.
View transitions — switching between smart views fades smoothly instead of snapping.
Sidebar Grant Access buttons — slimmed down from heavy full-width blocks to compact icon + label style, matching the visual weight of the rest of the sidebar.
Settings window — the settings panel was too short and clipped the Duplicate Hash Cache section (Last updated label, Clear Cache button, and footnote). Fixed with a taller default size and a scroll view.
Performance
StorageItemRowandDuplicateItemRowadoptedEquatableso SwiftUI can skip diff/recompute for identity-equal rows on large scans.CleanupCandidateRowadoptedEquatableand hoisted per-row state, reducing redundant diff work on the cleanup-review lane during scans and selection churn.DuplicateItemListrender-path refactor extracted rows toDuplicateFileRowto enable per-row@Statehover tracking.
Documentation
- Refreshed README overview screenshot to the current app UI.
Internal
- WelcomeView lifted into its own file (PR #90); welcome and empty-state branch now owned by
DetailView, simplifyingContentViewto pure split-view chrome. - Removed
ensureAncestorsExpandedfrom find-next navigation; the auto-expand-on-Cmd+G behaviour was reverted.
StorageScope v0.5.3 — Performance measurement slice
v0.5.3 — Performance measurement slice
Measurement-only release. No behavior changes, no scan-pipeline optimizations. This slice exists to validate (or invalidate) the bottleneck hypotheses in the new PLAN.md before any optimization commits. Premise: most proposed optimizations are waste unless the underlying bottleneck is real, so we measure first.
Signpost coverage added
Six new os_signpost spans wrap the gaps the v0.5.0 instrument (#81) left open. They show up under Instruments → Points of Interest → subsystem com.rasputinkaiser.StorageScope:
| Signpost | Wraps | What it measures |
|---|---|---|
verify_on_demand |
FileSystemScanner.verifySizeGroup(_:) |
Per-Verify-Now-call hash time, with items=N metadata |
persist_on_demand |
OnDemandVerificationStore's background hashCache.persist() |
The JSON encode + disk write cost the main actor skips |
cache_invalidate_derived |
ScanStore.invalidateDerivedCaches() |
Event density during cleanup-review interactions (annotated with active view) |
cache_invalidate_items |
ScanStore.invalidateItemsCache() |
Same pattern, items-only cache (annotated with active view) |
lookup_build |
StorageScan.buildItemLookup |
Eager [String: StorageItem] build cost at scan-complete; reports counts per contributing array |
cleanup_candidates_build |
ScanAccumulator.cleanupCandidates(...) |
Sort + filter pass; closes the gap ScanBenchmarkReport.text already admits between totalDuration and duration |
ScanBenchmark fixture scaling
StorageScopeBenchmark now accepts scaled-fixture flags so maintainers can capture baselines at 10k / 100k / 500k items:
# Still works (v0.5.0 curated 7-file fixture):
swift run StorageScopeBenchmark --synthetic
# New: scaled synthetic tree:
swift run StorageScopeBenchmark --synthetic --items 100000 --depth 8 --duplicates 0.05--items <n>emits N files distributed across a directory tree of--depth <d>levels (~120 files per leaf directory).--duplicates <0..1>is the fraction of items emitted as content-identical pairs/triples sharing a SHA-256, so the verify path actually fires.--keep-fixtureprints the fixture path so Instruments can be attached for a manual re-scan at scale.
Backward compat: --synthetic with no --items still builds the v0.5.0-era curated 7-file fixture, so build_and_run.sh --fixture-scan workflow is unchanged.
v0.5.2 baselines captured
Three baselines committed under docs/perf-baselines/v0.5.2/:
| Fixture | Items | Total size | Duration (wall) | Phase total | Peak memory |
|---|---|---|---|---|---|
curated.txt |
24 | 1.11 GB | 0.01s | 0.01s | 16.5 MB |
10k.txt |
10,353 | 687.3 MB | 6.90s | 6.90s | 52.8 MB |
100k.txt |
105,277 | 7.16 GB | 185.51s | 185.51s | 271.8 MB |
500k skipped on the dev machine: would need ~36 GB disk vs ~33 GB free. See docs/perf-baselines/v0.5.2/README.md for regeneration steps and the validation thresholds under PLAN.md §6.3 that gate which optimizations actually ship.
Validation
swift test— 90 tests across 10 suites pass./script/public_upload_audit.sh— passesbash ./script/build_and_run.sh --verify— bundle launches cleanly- Scaled fixtures regenerate and benchmark cleanly at 10k and 100k items locally.
- DMG:
exports/StorageScope-0.5.3.dmg, SHA256080e092b6222dd152d1958410dd27001071279acb6fc08fcf70121f46b8e3edf—hdiutil verifyconfirmed
What ships next
Per PLAN.md's decision rules (§6.3): each subsequent proposal's signpost must show ≥20% of totalDuration on the 100k fixture (at least 37s out of v0.5.3's 185.51s measured baseline) to be promoted to v0.5.4. If the suspected hotspots don't reach that threshold, the plan pauses and the next slice will be feature-driven, not perf-driven.
The roll forward to v0.5.4 (scan-pipeline wins) is queued but gated on the next profiling pass — likely candidates per §3.1 are the autoreleasepool in tight enumeration and the file-attribute-fetch batching, but neither ships until the new signposts confirm the cost.
StorageScope v0.5.2 — Inspector, File Types colors, Duplicate Review copy
v0.5.2 — Inspector, File Types colors, Duplicate Review copy
Three more tour-driven fixes after v0.5.1. No API or scan-pipeline changes; safe drop-in upgrade over v0.5.1.
Inspector
- Removed the root-scan fallback in
ScanStore.selectedItem. Previously, when no item was explicitly selected, the inspector fell back toscan.rootItem(the entire scan root folder) — making the inspector look like it was "stuck on the root" on every view, and masking the fact that selection is explicit. It now returnsnilso the existing "No Selection — Select an item to inspect actions and metadata" empty state appears, which is what should have surfaced in the first place.
File Types
- Per-category bar tint. Both the Category Mix bar chart and the per-extension bar chart used a single color (teal for categories, blue for extensions), which made the visual length difference hard to register when bars were short (the Archives@16 GB vs Installers@572 MB bars looked near-identical proportionally). Each
FileTypeStat.Categorynow maps to a deterministic system color (Archives → orange, Audio → pink, Developer → purple, Documents → blue, Databases → red, Fonts → indigo, Images → green, Installers → teal, Video → yellow, Virtual Machines → brown, Other → gray). Bar lengths were already linear with an 8pt floor — no scaling change, just better perceptual differentiation.
Duplicate Review
- Subtitle de-jargoned. "Verified duplicates are content-hashed. Same-size candidates remain separated as review leads." → "SHA-256 confirmed copies are safe to remove. Size-matched candidates need manual review." Same meaning; fewer formal-CS terms.
Validation
swift test— 90 tests across 10 suites pass./script/public_upload_audit.sh— passesbash ./script/build_and_run.sh --verify— bundle launches cleanly- DMG:
exports/StorageScope-0.5.2.dmg, SHA256cebdff8c78245365866d33d6328d4e5d70642567b68ae226fd2892fde836dfaa—hdiutil verifyconfirmed
Deferred (carried from v0.5.0)
- Tier 3 SelectionState / Tier 4 TrashReviewState extractions — still pending; same lazy-var + closure-injection template.
- Tier 5
@Observablemacro migration — still blocked bydidSetobservers on tracked properties. - Sparkle 2.x auto-update — still gated on notarization.
StorageScope v0.5.1 — Post-release UX polish
v0.5.1 — Post-release UX polish
Targeted fixes for issues raised during the v0.5.0 UI tour: Folder Tree expand behavior, persistent-header context-sensitivity, sparse-result view layout, and Keeper badge explainability. No API or scan-pipeline changes; safe drop-in upgrade over v0.5.0.
Folder Tree
- Disclosure triangle is its own toggle. Previously the whole row mutated both selection and expand state on click, and double-clicking a folder opened it in Finder instead of drilling in-app. The chevron is now a dedicated
Button; the row body only selects. Deprecated the double-click-to-Finder shortcut on tree rows (Reveal/Open remain in the row's context menu). - Empty-tree accessibility hint clarified: rows that can expand say "Click the disclosure triangle to expand or collapse"; leaf rows still say "Selects this item."
Context-sensitive header
- FilterBar removed from Overview / File Types / Cleanup Review. The Display + Scan Options strip was ~120pt of dead chrome on views that don't use it. The bar now appears only when the active view actually consumes the size or sort picker.
- Size picker gated per-view. Hidden on Overview / File Types / Cleanup Review where the threshold aggregates nothing.
- Sort picker gated per-view. Tree is hierarchical; Overview / File Types / Cleanup Review have no column sort the user can flip from the bar.
- Removed Scan Options group from the filter bar. "Old after N days" and "Hidden" were already covered by Settings → Scan Options; their presence in the bar was redundant. The Old-after stepper was re-added only on Old Large Files, since the threshold directly drives that view.
Old Large Files
- Threshold hint card when results are sparse. A new
OldLargeFilesThresholdHintinline card surfaces when 1–5 files qualify: copy acknowledges the small set, secondary line guides the user to lower the size filter or "Old after N days" then Rescan, and a Rescan button is wired up for one-tap action. - List minHeight scales with content. The scroll area no longer reserves 420pt when only a handful of rows exist — drops to ~220pt for ≤5 items so the empty canvas below sparse results is gone.
Duplicate Review
- Keeper badge tooltip. The green "Keeper" capsule now carries a help-tag explaining what keeper means and how to reassign it (right-click another copy → "Set as Keeper"). Row accessibility now appends ", keeper" to the row label and the hint explains the swap path.
Validation
swift test— 90 tests across 10 suites pass./script/public_upload_audit.sh— passesbash ./script/build_and_run.sh --verify— bundle launches cleanly- DMG:
exports/StorageScope-0.5.1.dmg, SHA2560d6feb5b62f3c4444dbccb3c796a1baf425b7809017a22940ea570907a58b807—hdiutil verifyconfirmed
Why v0.5.1 (not v0.5.0.1)
UX-only fixes plus docs version bump; the v0.5.x series is already a feature-bearing minor track. Patch version under v0.5 is appropriate since no scan pipeline / public API changed.
Deferred (carried from v0.5.0)
- Tier 3 SelectionState / Tier 4 TrashReviewState extractions — still pending; same lazy-var + closure-injection template.
- Tier 5
@Observablemacro migration — still blocked bydidSetobservers on tracked properties. - Sparkle 2.x auto-update — still gated on notarization.
StorageScope v0.5.0 — Search + Performance Upgrade
v0.5.0 — Search + Performance Upgrade
15 PRs since v0.4.5 (#75–#89). All backwards-compatible additions; no breaking API changes. The cumulative story is the search bar becoming a real Mac-app search surface (multi-word AND, find-next, recents, badge, empty states) plus the first round of scan-phase performance work (signpost instrumentation, derived-cache reuse, background-persisted disk writes).
Search
- Multi-word AND search with path-segment matching + substring highlight (#84) — queries split on whitespace and require every term to match. Terms also match against
/-split path segments, soDocuments reportmatches bothDocumentsas a path segment ANDreportas a substring of name. Matched substrings render in accent color via a newHighlightedTextSwiftUI view struct (file list + Folder Tree). - Search-result count badge (#86) — pill-shaped
N matchesbadge inStorageItemTableheader when a search is active. Distinguishesnil(no search) from zero matches so the empty-state view can tell them apart. - Search-empty vs filter-empty distinction (#87) —
FilterRecoveryViewnow distinguishesnoScan,noMatches(search active, zero hits), andfilteredEmpty(filter chips, zero hits) with state-appropriate iconography and copy. - Cmd+G find-next navigation (#88) — Mail/Finder convention. Cmd+G advances through search-result rows and wraps around; Cmd+Shift+G reverses. Sets
selectedItemIDso the matched row takes focus. Menu items gate on search-active. - Search recents persistence (#89) —
SearchRecentsStoremirrors theRecentsStorering-buffer pattern; recent terms surface as.searchSuggestionson the search field with Clear Recents. Persists across launches via UserDefaults.
Performance
- os_signpost instrumentation on scan phases (#81) —
enumerate,verify,persist, andindexspans wrap the hot paths so Instruments → Points of Interest can profile. Zero behavioral change — no-ops when no profiler is attached. - Cache duplicateGroups + verifiedDuplicateGroups per scanFinishedAt (#79) — mirrors the existing
cleanupCandidatescache pattern. Avoids re-walking size-group / verified-group aggregation on every Duplicate Review view transition. - Background-persist RecentsStore writes off the main actor (#80) — heavy JSON encode + UserDefaults write detaches to
.utilitypriority;@Published entriesstill updates synchronously so SwiftUI sees immediate change. Matches the v0.3.0hashCache.persist()pattern. - Extend ScanBenchmarkReport with per-phase durations (#83) —
enumerateDuration,verifyDuration,persistDuration, andtotalDurationcomputed property.duplicateVerificationDurationkept as deprecated alias for backward compat.
UI / chrome
- File → Open Recent submenu (#82, bonus) — mirrors macOS convention for non-document apps: lists recently-scanned folder paths so users can re-launch a scan without reaching for the sidebar. Dynamic rebuild on each menu open via
NSMenuDelegate; gates on!store.isScanning. - keyboard-shortcuts.html docs page (#76) — documents every v0.4.5 + v0.5.0 hotkey (Cmd+G/Cmd+Shift+G originally listed as "Coming in v0.5.0"; now shipping).
- architecture.html docs page (#75) — documents the ScanStore tier-2 sub-store extraction pattern +
filterBindingbridge + derived cache invalidation, with an ASCII store-hierarchy diagram. - Cross-page version bump to v0.4.5 (#77, #78) — index landing page
What's newticker + cross-page nav links bumped to v0.4.5 (carried forward to v0.5.0 in the prep PR). - Expand changelog with v0.4.5 subsections + v0.4.0 release (#85) — backfilled the full v0.4.5 section structure (Sidebar / Item rows / Trash review / Mac menu / Overview-Inspector / Refactors / Accessibility) and the v0.4.0 release section so the changelog reads as a contiguous history.
Validation
swift test— 90 tests across 10 suites passplutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy— cleanbash -nacross all scripts — clean./script/public_upload_audit.sh— passes- DMG:
exports/StorageScope-0.5.0.dmg, SHA25609691b50ca0c60318a5f3544fcb8fa1a3f44007843188a8e348f2080ac9fb871—hdiutil verifyconfirmed
Why v0.5.0 (not v0.4.6)
Per semver: pre-1.0 minor is the standard "feature-bearing release" bump. v0.5.0 ships the second major UX arc (search) — a meaningful feature surface, not a patch on v0.4.5's polish work. The 15 PRs are all backwards-compatible additions; no breaking API changes to FileSystemScanner.scan(...) or ScanStore's public surface.
Deferred
- Tier 3 SelectionState / Tier 4 TrashReviewState extractions — still pending; same lazy-var + closure-injection template from v0.4.5 Tiers 1-2.
- Tier 5
@Observablemacro migration — still blocked bydidSetobservers on tracked properties (deferred to v1.0.0). - Sparkle 2.x auto-update — still gated on notarization (PR #19's
script/notarize_dmg.shscaffolding from v0.4.0 waits on Apple Developer ID credentials).
v0.4.5 — Sidebar UX arc, ScanStore split, accessibility
v0.4.5 — Sidebar UX arc, ScanStore split, accessibility
39 PRs since v0.4.0 (#33–#72). All backwards-compatible additions; no breaking API changes. The cumulative story is the sidebar becoming a real Mac-app surface (tappable cards, hotkeys, dismissible chips) plus tier-1/tier-2 of the ScanStore split for v0.5.0.
Sidebar / Filter Bar
- Filter chips individually dismissible (#39) — new
FilterStore.ActiveFilterstruct +clearActiveFilter(_:). The single "Clear" button became "Clear All"; each chip is now a Button with anxmark. - Sidebar scan buttons disabled mid-scan (#40) — Grant Access / Recent Scans / Volumes buttons gate on
store.isScanning. Recent scan context menu stays interactive so "Forget" still works. - Toolbar Choose Folder gated mid-scan (#49) — same gate applied to the SwiftUI
ToolbarItemGroupbutton missed from #40. - Auto-clear stale file-type focus on view changes (#47) — new
SmartView.supportsFileTypeFocus. Navigating from.largestFiles(where theType: mp4chip applies) to.largestFolders(where folders lack extensions) auto-drops the chip instead of silently emptying the view. - File-type filter separated from search query (#46) — new
FilterStore.fileTypeFocus: String?. Chip now rendersType: mp4rather than mislabeling asSearch: mp4.ItemsCacheKeyfoldsfileTypeFocusin so the cache invalidates on focus change. - File-type row highlight in Type Breakdown (#62) — focused extension row shows
checkmark.circle.fill+selectionBackground. - Instant search-clear on empty field (#45) —
searchText.didSetcancels the debounce task and propagatesquery = ""synchronously when the field empties, so the chip drops immediately instead of waiting 200ms. - Context-aware search prompt (#64) — Tree → "Search folders and paths", File Types → "Search extensions", Cleanup Review → "Search cleanup candidates", Largest Folders → "Search largest folders".
- Highlight the focused
Type: <ext>row in Type Breakdown (#62) — per-row checkmark + selection tint shows which extension is currently set. - Auto-refreshing recent scan timestamps (#56) — recent-scan
RecentScanRowsubtitle now usesTimelineView(.periodic)so "5 minutes ago" stays current without manual state.
Item rows & tables
- Double-click-to-open across item rows (#43) —
simultaneousGesture(TapGesture(count: 2))onStorageItemRow,DuplicateItemList, andTreeNodeRow. Preserves single-tap selection; containers open in Finder, files in their handler. - Clickable sortable column headers (#65) —
StorageItemTableheaders (Name / Size / Kind / Modified) now sort on click withchevron.up/chevron.downindicator. Modified cycles Newest ↔ Oldest per click. - Voice sort direction in column header a11y values (#68) — accessibilityValue reports "Active sort, descending/ascending" instead of just "Active sort".
- Relative time in Modified column (#66) —
StorageFormat.relativeOrAbsoluteDate(_:relativeTo:)shows "2 hours ago" for files <7 days old, falls back to the absolute date beyond. Sort still uses the underlyingDate.
Trash review sheet
- Reveal All (#50) — per-section
revealAllTrashReviewItems(_:)opens every URL in one Finder window viaNSWorkspace.activateFileViewerSelecting. - Open button on rows (#53) — new
openTrashReviewItem(_:)+ icon button so users preview files in their default app before confirming Trash. - Remove All (#67) — paired with Reveal All; drops an entire section (e.g. all review-suggested items leaving verified-only) in one tap via
Set.subtract. - Dismissible scan-canceled acknowledgment (#55) —
ScanNoticeViewgains optionaldismissAction.scanNoticeIsDismissibleis true only for transient cancel notices; stale-options and results-need-refresh notices still require Rescan.
Mac menu / window structure
- View menu + Cmd+1–8 hotkeys (#41) — Smart View entries with
selectSidebarView(_:)sharing a single selector keyed offtag. Active item marked.onso the menu doubles as a where-am-I indicator. - Window menu Close (#52) —
NSWindow.performClose(_:)with Cmd+W. Was missing entirely; standard Mac Window menu convention. - Edit menu Find… (Cmd+F) (#69) —
ScanStore.searchFieldFocusRequestcounter +requestSearchFieldFocus()helper.ContentViewobserves via.onChangeand applies.searchFocused()through anif #available(macOS 15.0, *)-guardedSearchFocusModifier(the modifier API lands in macOS 15; project target is macOS 14, so older builds track state but field doesn't auto-focus — graceful fallback). - View menu Enter Full Screen (#70) — Cmd+Ctrl+F wired to
NSWindow.toggleFullScreen(_:). System auto-toggles the title between "Enter Full Screen" / "Exit Full Screen". - Help menu (#59) —
StorageScope on GitHubandReport an Issueitems. Registered asNSApp.helpMenuso macOS's standard Help-search integration will work later when docs exist. - Dynamic window title with scan root (#48) — title re-renders to "StorageScope — " when a scan completes, via Combine subscription on
store.$session.sessionbumped fromprivatetoprivate(set)so the projected publisher is observable.
Overview / Welcome / Inspector
- Tappable Overview insight cards (#58) — InsightCard gains
onTapclosure +chevron.rightaffordance when an item is present. Tap selects the item in the Inspector (Reveal/Open/Copy Path/Move to Trash already wired). - Recent Scans quick-launch on Welcome (#57) — chip row below capability cards surfaces the last 8 scan paths as tap-to-scan chips via horizontal ScrollView. Lazy rendering for overflow; disabled while scanning; row hidden on fresh installs.
- Gate Open on inaccessible items (#60) —
canOpenSelectedItemchecksselectedItem.isReadable(not just "selected && not scanning"). Inspector Open button help-tag explains "File is not readable; try Reveal in Finder instead" when disabled. Reveal/Copy Path keep the broader gate (they're pure URL/path operations). - Disambiguate Reclaim Plan "Suggestions" label (#42) —
.reviewVerifiedDuplicates.shortTitlestays"Review";.reviewCleanupSuggestions.shortTitlebecomes"Suggestions". Two side-by-side action buttons no longer both say "Review". - Seconds precision in scan duration (#54) —
StorageFormat.duration(from:to:)shows "1 min 30 sec" for sub-10-min scans (where seconds still matter); rounded-minute ("12 min") beyond. - Volume free-space in sidebar (#51) —
mountedVolumesfetch addsvolumeAvailableCapacityKey+volumeTotalCapacityKey;volumeCapacityDescription(for:)formats the pair ("247 GB free of 1 TB"). ReusesSidebarPathButton.subtitle. - Persist selected sidebar view across launches (#63) — selected SmartView persists via
@AppStorage("StorageScope.selectedView"). Returning users reopen where they left them, not always Overview. DefensiveobjectWillChange.send()since@AppStorageinside an ObservableObject-derived class doesn't reliably fire for@ObservedObjectsubscribers.
Refactors (v0.5.0 preliminaries)
- Tier 1a: RecentsStore (#34) — extracted
RecentsStore(entries, UserDefaults JSON, one-time migration from legacy string-array). - Tier 1b: OnDemandVerificationStore (#35) — extracted verified-groups and per-group verify.
- Tier 2: FilterStore (#38) — extracted all filter state into
FilterStore. Same lazy-var + closure-injection pattern. Critical discovery: SwiftUI's@ObservedObjectwrapper can't traverse ObservableObject sub-stores via dynamic member lookup —$store.filters.Xproduces no validBinding<T>.ScanStore.filterBinding<T>(_:)bridges via explicitBinding<T>for Picker/Stepper/Toggle sites. - First-run permission card body to .footnote (#33).
- Inline StorageFormatless.kindLabel (#36) — dead indirection removed.
- Remove dead import AppKit from ScanStore (#37).
Accessibility
- File Type Category Mix rows labeled for VoiceOver (#71) —
accessibilityLabel("<category> category, <total bytes>")+accessibilityValue("<ext count>, <file count label>"). Percent-of-bar geometry stays inaccessible (visual summaries duplicated as digits would mislead).
Validation
swift test— 54 tests across 5 suites passplutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy— cleanbash -nacross all 5 scripts — clean./script/public_upload_audit.sh— passes- DMG:
exports/StorageScope-0.4.5.dmg, SHA256913ca5618ea5162dafaab50f15cec84286c0088c97b9184718766b50a4f79fb1—hdiutil verifyconfirmed
Why v0.4.5 (not v0.5.0 or v0.4.0)
Per semver: pre-1.0 minor is the standard "feature-bearing release" bump. v0.4.x patch feels right versus v0.5.0 because the ScanStore split is half-done — Tiers 3 (SelectionState), 4 (TrashReviewState), and 5 (@Observable macro migration) remain pending. The 39 shipped UX improvements are all backwards-compatible additions; no breaking API changes to FileSystemScanner.scan(...) or ScanStore's public surface.
Deferred
- Tier 3 SelectionState / Tier 4 TrashReviewState extractions — pending; same lazy-var + closure-injection template.
- Tier 5
@Observablemacro migration — still blocked bydidSetobservers on tracked properties (deferred to v1.0.0). - Sparkle 2.x auto-update — still gated on notarization (PR #19's
script/notarize_dmg.shscaffolding from v0.4.0 waits on Apple Developer ID credentials).
How to upgrade
Existing v0.4.0 users can move directly to v0.4.5. No settings migrations; the hash cache, recent scans, bookmarks, and @AppStorage keys (StorageScope.didDismissFirstRunCard) persist unchanged. New StorageScope.selectedView is the only added UserDefaults key.
Building from source
bash ./script/build_and_run.sh # build a...StorageScope v0.4.0 — Polish, notarize scaffolding, audit hardening, contributor docs
v0.4.0 — Polish, cache maintenance, contributor docs, notarize scaffolding
Version-worth of polish + reliability work since v0.3.0 (~14 PRs). All changes are backwards-compatible additions and refactors; no breaking changes.
Polish (SwiftUI helper extensions)
Two new View extensions in Sources/StorageScope/Support/CardBackground.swift standardize repeated background patterns that had drifted across the view layer.
cardBackground(_:radius:)(#26) — replaces 22 hard-coded.background(.regularMaterial/.thinMaterial, in: RoundedRectangle(cornerRadius: 8))call sites across 9 view files. Defaultradius: 8matches the shipping pattern.selectionBackground(isSelected:tint:radius:)(#31) — replaces 4 hard-codedColor.accentColor.opacity(0.X) : Color.clearhighlight patterns acrossCleanupReviewView,TreeExplorerView,SidebarView,StorageItemTable. Preserves per-site tint (0.18 default, 0.24 sidebar) and radius (nilfor list rows, 6 sidebar, 8 cards).
Bundle polish (#20) tidied three smaller things in one PR:
TrashReviewActionsstruct — collapsesTrashConfirmationSheet's four-closure init into a single value typeString.nonEmptyconsolidation — removed three duplicate private extensions, single sharedinternal extension Stringlives inStorageFormat.swiftalongside the existing formatter helpers- Cached relative-date rendering in recent-scan sidebar rows —
RecentScanRowview caches the.formatted(.relative(presentation: .named))string via@Statepopulated from.onAppear, instead of computing on every body evaluation
Duplicate-hash cache maintenance (#21)
DuplicateHashCache.purgeStale(except:) drops entries whose underlying files no longer exist (or whose paths become unreadable) after every scan. Wired into ScanStore's existing detached Task.detached(priority: .utility) persist step so the purge runs alongside hashCache.persist() without blocking the main actor. Returns the dropped count.
Fixes the slow leak from prior versions where deleted/moved files' hash entries lingered in ~/Library/Caches/StorageScopeDuplicateHashCache.json until the 10,000-entry cap evicted the oldest-mtime ones, with no way for the cache to self-cleanse. The Clear Cache button in Settings (added in v0.3.0) remains the heavy hammer; purgeStale is now the automatic background counterpart.
Public-upload audit hardening (#22, #23)
script/public_upload_audit.sh gained two real fixes:
-
POSIX grep portability. The script's secret-detection loop previously called
rg(ripgrep) with|| true. macOS GitHub Actions runners don't ship ripgrep by default, and|| truewas silently swallowing the resulting "command not found" exit code alongside the "no matches" exit code — so the entire secret-detection step had been no-oping in CI since the script was introduced. Replacedrg -I -n -iwith POSIXgrep -E -H -n -i(universally available, same extended regex syntax). Real hardcoded secrets (sk-proj-...,ghp_...) now trip the audit as intended; the|| truecorrectly covers only the "no matches" exit. -
sk-regex tightened.sk-[A-Za-z0-9]+→sk-[A-Za-z0-9]{20,}. The old pattern false-positived ondocs/macos-disk-space-analyzer.html(where "sk-s" matched with no word boundary) and similar pages. The{20,}minimum matches real OpenAI keys (40+ chars aftersk-) while dropping the false positive. -
Env var reference allowlist. PR #19 added references to
APP_STORE_CONNECT_API_KEY_ID,APP_STORE_CONNECT_API_ISSUER_ID,APP_STORE_CONNECT_API_KEY_FILEPATHinscript/notarize_dmg.shandCONTRIBUTING.md. These match theapi[_-]?keyregex but carry no checked-in secret value — they're declarations and references to credentials the maintainer supplies in the local shell. Addedsafe_env_var_ref_patternsand a post-loop filter step that drops lines matching those patterns before the audit fails. Word-boundary anchors on the variable names preserve the precision: any line mentioningAPP_STORE_CONNECT_API_KEY_IDas a word is safe; hardcoded secrets still trip.
Notarize script scaffolding (#19)
script/notarize_dmg.sh — new maintainer-only script submitting a Developer ID-signed DMG to Apple's notary service via xcrun notarytool submit, stapling the result via xcrun stapler staple, and validating the final DMG. Reads App Store Connect credentials only from environment variables (APP_STORE_CONNECT_API_KEY_ID, APP_STORE_CONNECT_API_ISSUER_ID, APP_STORE_CONNECT_API_KEY_FILEPATH) — never from CLI flags or repo files — and redacts the key id and issuer id when echoing. Detects whether the input DMG is ad-hoc signed (the current default from script/export_dmg.sh) and exits with guidance to re-export with STORAGESCOPE_SIGN_IDENTITY="Developer ID Application: ..." set.
CONTRIBUTING.md gains a ## Notarization (for maintainers) section covering one-time ASC API key setup, required env vars, Developer ID-signed DMG production, script invocation, and credential rotation guidance.
.gitignore adds *.p8 and AuthKey_*.p8 so App Store Connect API keys cannot be committed.
This unblocks the actual notarization pipeline for when an Apple Developer ID certificate is in hand — it does not perform notarization itself, which still requires Developer ID credentials.
Contributor docs (#28)
CONTRIBUTING.md gains a ## Developer Fixtures section documenting ./script/build_and_run.sh --fixture-scan and the eight optional flags that customize the launched app's starting state (--duplicates, --mark-stale, --view, --select-verified-cleanup, --query, --size-filter, --sort, --cleanup-lane). Documents the fixture root layout, four example workflows, and the SmartView raw values usable with --view.
The fixture-scan mode landed in PRs #1, #5, #6, and #12 across v0.1.0-v0.3.0; contributor docs never caught up until now.
Test coverage depth (#18)
verifySizeGroupSurfacesExactMatchesAcrossMultiDirFixture — new stress-test for the v0.3.0-shipped FileSystemScanner.verifySizeGroup(_:cancellation:) API. Five-sibling-directory fixture with mixed same-content patterns: common-content group (5 files, all 5 dirs verified), partial-content group (3 files, only dir0/dir1/dir2 verified; dir3/dir4 stay unverified at different content), unique-content per dir (never verified). Asserts exactly 2 verified groups returned, matching sanity checks on group membership and byte size, distinct checksums, no verified group touches the unique-content file.
Documentation site
The static documentation site under docs/ gained several maintainer contributions during the v0.4.0 cycle:
- Shared site header/footer + landing page content expansion (#25)
- Privacy and changelog pages, linked from the site footer (#27)
- Article and Breadcrumb JSON-LD structured data + walkthrough cards on guides (#29)
- FAQ page with FAQPage JSON-LD, linked from every page footer (#30)
- v0.3.0 DMG download surface surfacing (#24)
Why this release is minor (not major, not patch)
Per semver: pre-1.0 minor is the right call versus patch because the changes add new functionality (notarize scaffolding, hash cache purge, parallel-scan property test) rather than only fixing bugs. Versus major because there are no breaking API changes to FileSystemScanner.scan(...) or ScanStore's public surface — every change behaves the same way for existing callers.
Deferred from v0.4.0
- Sparkle 2.x auto-update integration — investigation only, no code shipped. Technically compatible with StorageScope's SwiftPM-only build and the local-first privacy posture (set
SUSendSystemProfile=false, appcast over HTTPS, the framework makes no calls beyond the feed fetch + archive download). But it provides zero user-trustworthy value while DMGs are ad-hoc signed — Sparkle auto-update only delivers trustworthy updates when archives are Developer-ID-signed AND notarized. Deferred to v0.5.0 / v0.6.0, gated on the notarization pipeline (PR #19's scaffolding) being live first. Full integration shape documented in maintainer notes for that spike. ScanStoresplit into focused@Observabletypes — the v0.4.0 plan originally called for splittingScanStoreintoScanState/FilterState/SelectionStateand migrating to the@Observablemacro. Deferred because the migration has real SwiftUI-binding surface risks (the@PublisheddidSetobservers don't translate cleanly) that I shouldn't ship solo against no UI test net. Candidate for v0.5.0 as an isolated research-session refactor.- Swift 6 strict concurrency spike — bumped
swift-tools-versionto 6.0 and fixing the resulting strict-concurrency errors (including the implicit-self-in-closure warning the v0.3.0 parallel scanner introduced) deferred until after the ScanStore split lands, since both touch the same files.
How to upgrade
Existing users on v0.3.0 or earlier can move directly to v0.4.0. No settings migrations, no backup steps required. The hash cache persists unchanged.
Building from source
bash ./script/build_and_run.sh # build app bundle, sign ad-hoc, launch
./script/export_dmg.sh # export a local DMG (ad-hoc signed)Verification
swift test— 54 tests across 5 suites passplutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy— cleanbash -nacross all 5 scripts — clean./script/public_upload_audit.sh— passes (and now actually runs in CI for the first time)bash ./script/build_and_run.sh— builds, signs, launches the app
StorageScope v0.3.0 — Scan performance and duplicate review completion
v0.3.0 — Scan performance and duplicate review completion
Two version-worth of work since v0.1.1 was released — the v0.2.0 work (PR #12) was version-bumped but never tagged, so this release covers everything since the last shipped version.
What's new
Scan performance (v0.3.0, PR #13)
- Parallel sibling-directory enumeration. The scanner now recurses into folder children in parallel via
DispatchQueue.concurrentPerform. Wide directory layouts (home folder, mounted volumes) benefit from SSD parallelism. Child item order is preserved deterministically. - Adaptive hash concurrency. Duplicate-verification hashing now scales with available cores up to 8 (was fixed at 4), so SSD hosts with more cores get proportionally more parallel hashing without saturating the disk queue.
Duplicate review completion (v0.3.0, PRs #14, #16)
- Keeper comparison sheet (#14). Verified duplicate groups gain a side-by-side sheet (launched via a new "Compare" button) showing the current keeper at the top and each alternative copy below, with size, modification date, and path. A per-row "Set as Keeper" button instantly reassigns.
- On-demand "Verify Now" for same-size candidate groups (#16). Same-size candidate groups beyond the auto-verification budget now have a "Verify Now" button that hashes the group out-of-band and surfaces real SHA-256-verified duplicate matches, moving them from the speculative same-size list to the verified-duplicate review lane. Uses the persisted hash cache, so cached hits are near-instant.
Cache management (v0.3.0, PR #15)
- Hash cache management in Settings. The Settings window now includes a "Duplicate Hash Cache" section showing stored-entry count and last-write timestamp, plus a destructive "Clear Cache" button guarded by an alert. Useful for forcing a full re-hash after moving many large files.
Trash flow and review UX (v0.2.0, PR #12)
- Async batch trash with progress. Batch trash operations now run on a background
Taskwith anisMovingToTrashprogress state, so the UI never freezes during large folder moves. - Debounced search input. Search input is now debounced ~200ms, so filtering and sorting don't fire per keystroke.
- Unified trash confirmation. Single-item "Move to Trash" actions from table/tree/inspector context menus no longer use a native
NSAlert; they route through the same review sheet batched actions do. - Per-item removal in trash sheet. The trash confirmation sheet gains a "Remove from batch" button per item, so you can prune without dismissing.
- Keeper selection for verified duplicates. Verified duplicate groups now designate a keeper copy (default: first), reassignable via context menu or the new comparison sheet. Keepers are excluded at trash time, and confirming a batch containing only keepers surfaces an informative error.
- Un-ignore affordance. Cleanup candidates can now be ignored and restored without rescanning via a new "Ignored" section in Cleanup Review.
- First-run permission card. First launch shows a dismissible card explaining folder access and Full Disk Access, with a button that deep-links to System Settings → Privacy & Security.
- Settings toolbar entry. Settings now has a visible gear icon in the toolbar — no longer reachable only from the menu bar.
- Stage-based scan progress. The sidebar footer shows
Enumerating → Verifying duplicates → Completeinstead of a generic "Scanning" line. - Recent-scan metadata. Recent scans now display when they ran and how big they were, instead of just the path.
- Persisted SHA-256 hash cache. Verified-duplicate hashing now caches to disk, so rescans skip unchanged files entirely.
- Thread-safe date formatter. Shared
Date.FormatStylereplaces the sharedDateFormatterthat could race on background format paths.
How to upgrade
Existing users on v0.1.1 or v0.2.0 can move directly to v0.3.0. No settings migrations required.
Building from source
bash ./script/build_and_run.sh # build app bundle, sign ad-hoc, launch
./script/export_dmg.sh # export a local DMG (ad-hoc signed)Notes
- This release ships as source — the tag does not include a downloadable DMG. To produce a DMG, run
./script/export_dmg.shlocally andgh release upload v0.3.0 <dmg-path>to attach it. Pre-built DMGs with Developer ID signing + notarization are planned for the 1.0.0 release. - The
v0.2.0tag was intentionally skipped — its work is folded into this release. The sidebar will gov0.1.1 → v0.3.0.
Verification
swift test— 52 tests across 5 suites passplutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy— cleanbash ./script/build_and_run.sh— builds, signs, launches the app
Contributing
Thanks to everyone who reported issues and tested the v0.2.0/v0.3.0 work. Issues and PRs welcome at https://github.com/RasputinKaiser/StorageScope.