Skip to content

Releases: RasputinKaiser/StorageScope

v0.7.1 — Window Polish and Keyboard Navigation

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 02 Jul 05:03
4099025

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

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 01 Jul 22:39
5055559

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) — FilterStore changes never propagated into ScanStore.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

  • autoreleasepool around the parallel enumeration hot loop cuts peak memory on broad scans; SHA-256 hex encoding uses a lookup table instead of per-byte String(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); StorageItemRow regained Equatable diff-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 hitting FileManager every sidebar render (#112).
  • Size bars in Tree Explorer, Type Breakdown, and the Overview storage map are drawn with a shared Canvas instead of a per-row GeometryReader, 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

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 22 Jun 20:53

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

  • StorageItemRow and DuplicateItemRow adopted Equatable so SwiftUI can skip diff/recompute for identity-equal rows on large scans.
  • CleanupCandidateRow adopted Equatable and hoisted per-row state, reducing redundant diff work on the cleanup-review lane during scans and selection churn.
  • DuplicateItemList render-path refactor extracted rows to DuplicateFileRow to enable per-row @State hover 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, simplifying ContentView to pure split-view chrome.
  • Removed ensureAncestorsExpanded from find-next navigation; the auto-expand-on-Cmd+G behaviour was reverted.

StorageScope v0.5.3 — Performance measurement slice

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 22 Jun 05:33

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-fixture prints 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 — passes
  • bash ./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, SHA256 080e092b6222dd152d1958410dd27001071279acb6fc08fcf70121f46b8e3edfhdiutil verify confirmed

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

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 22 Jun 05:03

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 to scan.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 returns nil so 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.Category now 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 — passes
  • bash ./script/build_and_run.sh --verify — bundle launches cleanly
  • DMG: exports/StorageScope-0.5.2.dmg, SHA256 cebdff8c78245365866d33d6328d4e5d70642567b68ae226fd2892fde836dfaahdiutil verify confirmed

Deferred (carried from v0.5.0)

  • Tier 3 SelectionState / Tier 4 TrashReviewState extractions — still pending; same lazy-var + closure-injection template.
  • Tier 5 @Observable macro migration — still blocked by didSet observers on tracked properties.
  • Sparkle 2.x auto-update — still gated on notarization.

StorageScope v0.5.1 — Post-release UX polish

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 22 Jun 04:56

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 OldLargeFilesThresholdHint inline 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 — passes
  • bash ./script/build_and_run.sh --verify — bundle launches cleanly
  • DMG: exports/StorageScope-0.5.1.dmg, SHA256 0d6feb5b62f3c4444dbccb3c796a1baf425b7809017a22940ea570907a58b807hdiutil verify confirmed

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 @Observable macro migration — still blocked by didSet observers on tracked properties.
  • Sparkle 2.x auto-update — still gated on notarization.

StorageScope v0.5.0 — Search + Performance Upgrade

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 22 Jun 03:37

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, so Documents report matches both Documents as a path segment AND report as a substring of name. Matched substrings render in accent color via a new HighlightedText SwiftUI view struct (file list + Folder Tree).
  • Search-result count badge (#86) — pill-shaped N matches badge in StorageItemTable header when a search is active. Distinguishes nil (no search) from zero matches so the empty-state view can tell them apart.
  • Search-empty vs filter-empty distinction (#87) — FilterRecoveryView now distinguishes noScan, noMatches (search active, zero hits), and filteredEmpty (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 selectedItemID so the matched row takes focus. Menu items gate on search-active.
  • Search recents persistence (#89) — SearchRecentsStore mirrors the RecentsStore ring-buffer pattern; recent terms surface as .searchSuggestions on the search field with Clear Recents. Persists across launches via UserDefaults.

Performance

  • os_signpost instrumentation on scan phases (#81) — enumerate, verify, persist, and index spans 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 cleanupCandidates cache 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 .utility priority; @Published entries still updates synchronously so SwiftUI sees immediate change. Matches the v0.3.0 hashCache.persist() pattern.
  • Extend ScanBenchmarkReport with per-phase durations (#83) — enumerateDuration, verifyDuration, persistDuration, and totalDuration computed property. duplicateVerificationDuration kept 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 + filterBinding bridge + derived cache invalidation, with an ASCII store-hierarchy diagram.
  • Cross-page version bump to v0.4.5 (#77, #78) — index landing page What's new ticker + 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 pass
  • plutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy — clean
  • bash -n across all scripts — clean
  • ./script/public_upload_audit.sh — passes
  • DMG: exports/StorageScope-0.5.0.dmg, SHA256 09691b50ca0c60318a5f3544fcb8fa1a3f44007843188a8e348f2080ac9fb871hdiutil verify confirmed

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 @Observable macro migration — still blocked by didSet observers on tracked properties (deferred to v1.0.0).
  • Sparkle 2.x auto-update — still gated on notarization (PR #19's script/notarize_dmg.sh scaffolding from v0.4.0 waits on Apple Developer ID credentials).

v0.4.5 — Sidebar UX arc, ScanStore split, accessibility

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 21 Jun 17:07
6fee1a9

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.ActiveFilter struct + clearActiveFilter(_:). The single "Clear" button became "Clear All"; each chip is now a Button with an xmark.
  • 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 ToolbarItemGroup button missed from #40.
  • Auto-clear stale file-type focus on view changes (#47) — new SmartView.supportsFileTypeFocus. Navigating from .largestFiles (where the Type: mp4 chip 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 renders Type: mp4 rather than mislabeling as Search: mp4. ItemsCacheKey folds fileTypeFocus in 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.didSet cancels the debounce task and propagates query = "" 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 RecentScanRow subtitle now uses TimelineView(.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)) on StorageItemRow, DuplicateItemList, and TreeNodeRow. Preserves single-tap selection; containers open in Finder, files in their handler.
  • Clickable sortable column headers (#65) — StorageItemTable headers (Name / Size / Kind / Modified) now sort on click with chevron.up/chevron.down indicator. 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 underlying Date.

Trash review sheet

  • Reveal All (#50) — per-section revealAllTrashReviewItems(_:) opens every URL in one Finder window via NSWorkspace.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) — ScanNoticeView gains optional dismissAction. scanNoticeIsDismissible is 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 off tag. Active item marked .on so 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.searchFieldFocusRequest counter + requestSearchFieldFocus() helper. ContentView observes via .onChange and applies .searchFocused() through an if #available(macOS 15.0, *)-guarded SearchFocusModifier (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 GitHub and Report an Issue items. Registered as NSApp.helpMenu so 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. session bumped from private to private(set) so the projected publisher is observable.

Overview / Welcome / Inspector

  • Tappable Overview insight cards (#58) — InsightCard gains onTap closure + chevron.right affordance 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) — canOpenSelectedItem checks selectedItem.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.shortTitle stays "Review"; .reviewCleanupSuggestions.shortTitle becomes "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) — mountedVolumes fetch adds volumeAvailableCapacityKey + volumeTotalCapacityKey; volumeCapacityDescription(for:) formats the pair ("247 GB free of 1 TB"). Reuses SidebarPathButton.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. Defensive objectWillChange.send() since @AppStorage inside an ObservableObject-derived class doesn't reliably fire for @ObservedObject subscribers.

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 @ObservedObject wrapper can't traverse ObservableObject sub-stores via dynamic member lookup — $store.filters.X produces no valid Binding<T>. ScanStore.filterBinding<T>(_:) bridges via explicit Binding<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 pass
  • plutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy — clean
  • bash -n across all 5 scripts — clean
  • ./script/public_upload_audit.sh — passes
  • DMG: exports/StorageScope-0.4.5.dmg, SHA256 913ca5618ea5162dafaab50f15cec84286c0088c97b9184718766b50a4f79fb1hdiutil verify confirmed

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 @Observable macro migration — still blocked by didSet observers on tracked properties (deferred to v1.0.0).
  • Sparkle 2.x auto-update — still gated on notarization (PR #19's script/notarize_dmg.sh scaffolding 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...
Read more

StorageScope v0.4.0 — Polish, notarize scaffolding, audit hardening, contributor docs

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 21 Jun 04:13
0868167

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. Default radius: 8 matches the shipping pattern.
  • selectionBackground(isSelected:tint:radius:) (#31) — replaces 4 hard-coded Color.accentColor.opacity(0.X) : Color.clear highlight patterns across CleanupReviewView, TreeExplorerView, SidebarView, StorageItemTable. Preserves per-site tint (0.18 default, 0.24 sidebar) and radius (nil for list rows, 6 sidebar, 8 cards).

Bundle polish (#20) tidied three smaller things in one PR:

  • TrashReviewActions struct — collapses TrashConfirmationSheet's four-closure init into a single value type
  • String.nonEmpty consolidation — removed three duplicate private extensions, single shared internal extension String lives in StorageFormat.swift alongside the existing formatter helpers
  • Cached relative-date rendering in recent-scan sidebar rows — RecentScanRow view caches the .formatted(.relative(presentation: .named)) string via @State populated 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:

  1. 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 || true was 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. Replaced rg -I -n -i with POSIX grep -E -H -n -i (universally available, same extended regex syntax). Real hardcoded secrets (sk-proj-..., ghp_...) now trip the audit as intended; the || true correctly covers only the "no matches" exit.

  2. sk- regex tightened. sk-[A-Za-z0-9]+sk-[A-Za-z0-9]{20,}. The old pattern false-positived on docs/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 after sk-) while dropping the false positive.

  3. 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_FILEPATH in script/notarize_dmg.sh and CONTRIBUTING.md. These match the api[_-]?key regex but carry no checked-in secret value — they're declarations and references to credentials the maintainer supplies in the local shell. Added safe_env_var_ref_patterns and 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 mentioning APP_STORE_CONNECT_API_KEY_ID as 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.
  • ScanStore split into focused @Observable types — the v0.4.0 plan originally called for splitting ScanStore into ScanState / FilterState / SelectionState and migrating to the @Observable macro. Deferred because the migration has real SwiftUI-binding surface risks (the @Published didSet observers 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-version to 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 pass
  • plutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy — clean
  • bash -n across 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

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 21 Jun 01:23
123fa25

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 Task with an isMovingToTrash progress 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 → Complete instead 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.FormatStyle replaces the shared DateFormatter that 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.sh locally and gh 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.0 tag was intentionally skipped — its work is folded into this release. The sidebar will go v0.1.1 → v0.3.0.

Verification

  • swift test — 52 tests across 5 suites pass
  • plutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy — clean
  • bash ./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.