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