Build me a complete, native macOS disk-space analyzer app called **DiskDude**, in one go. It must compile and run on the first try. Follow this spec exactly.

# 1. What the app is

DiskDude scans a folder, your home folder, or the whole Mac. It builds an in-memory tree of every file and folder, then lets you explore where the space went through 9 visualizations. It also includes a duplicate finder, an app uninstaller that finds leftovers, a live system monitor, before/after snapshots, and a "Quick Wins" junk finder.

Core safety promise: **nothing is ever deleted directly.** Everything goes into a Cleanup Queue first. Committing the queue moves items to the **Trash** (never a hard delete), and only after an explicit confirmation dialog.

# 2. Tech stack & project layout

- Swift Package Manager executable, `swift-tools-version: 5.10`, Swift 5 language mode. `platforms: [.macOS(.v14)]`. Single `.executableTarget(name: "DiskDude", path: "Sources/DiskDude")`. No third-party dependencies.
- SwiftUI for UI, AppKit where needed (NSOpenPanel, NSWorkspace, Quick Look, NSRunningApplication), Darwin/Mach APIs for scanning and monitoring, CryptoKit for hashing.
- `swift build && swift run` must work. When run as a bare executable (no bundle id), call `NSApplication.shared.setActivationPolicy(.regular)` and activate it so a window appears.
- Folders: `Sources/DiskDude/{DiskDudeApp.swift, Models/, Scanning/, Views/, Theme/, Utils/}`, plus `Packaging/`, `scripts/`, `docs/`.
- Not sandboxed, and no entitlements are needed. A disk analyzer must read arbitrary folders.

# 3. Window & layout

- `WindowGroup` with `.windowStyle(.hiddenTitleBar)`, default size 1680×1050, min 1280×760.
- **Top bar** (56pt tall):
  - 64pt of empty space on the left for the traffic lights.
  - Tab capsules: Explore, Duplicates, Applications, Monitor, Snapshots. The active tab is a filled accent capsule that slides between tabs with `matchedGeometryEffect`. Use `ViewThatFits` so inactive tabs hide their labels when the window is narrow.
  - A divider, then a breadcrumb for Explore: a back chevron plus the last 4 ancestors, each clickable.
  - On the right:
    - A "Filter by name..." search capsule showing a ⌘K hint; ⌘K focuses it, and a clear button appears when it has text.
    - The Cleanup Queue button, with a red count badge.
    - A Light/Dark theme toggle (sun/moon icon).
    - A toggle that shows or hides the inspector.
- **Body**: sidebar (340pt), then the main area, then the inspector (382pt; only on Explore, slides in from the right). Separate them with 1pt dividers.

# 4. Scanner (performance-critical)

Build `DiskScanner` (a `final class`, `@unchecked Sendable`):

- Read directories with **`getattrlistbulk(2)`** (256 KB buffer, `FSOPT_NOFOLLOW`). Request name, error, object type, creation time, modification time, file allocated size and file data length. Parse the packed attribute records correctly: honor the returned-attributes bitmap, and note that `ATTR_CMN_ERROR` comes first.
- Parallel traversal: one worker `Thread` per active CPU core. Workers pull jobs from a shared LIFO job stack guarded by `NSCondition`. The scan ends when the stack is empty and no worker is active.
- Never follow symlinks. Skip `/System, /Volumes, /dev, /private/var/vm, /.Spotlight-V100, /.fseventsd, /cores, /net, /home`.
- Any file inside a `.app` bundle is categorized as Apps.
- Count folders that fail with `EACCES`/`EPERM`, and keep up to 8 sample paths.
- Progress (files scanned, bytes, current path) lives in a lock-protected snapshot. The UI polls it every 150 ms; do not publish per file.
- Support cancellation. Starting a new scan cancels the old one.
- **Roll-up** after the scan, parallelized for the top 2 levels with `DispatchQueue.concurrentPerform`: every folder gets logical size, size on disk, file count, folder count, bytes per file category (`typeSizes: [Int64]`), and the most recent descendant modification time. Children are sorted largest-first by size on disk.
- `static func allocatedSize(atPath:)` measures a whole tree the same way; the app uninstaller uses it.
- Target: about 1.2M files in about 5–6 seconds.

`FileNode` (a final class; memory matters because there can be over a million of them):

- `name`, `isDirectory`, `category`, `children`, `weak parent`, and `rootPath` (set on the root only).
- The path is rebuilt from the parent chain on demand. Do not store a URL per node.
- `logicalSize`, `sizeOnDisk`, `modifiedStamp`/`createdStamp` stored as `Double` (0 means unknown), `typeSizes`, `fileCount`, `folderCount`.
- Helpers: `shareOfParent`, `compressionSavings` (logical − on disk), `dominantCategory`, `topCategories(n)`, `isDescendant(of:)`, `walk`, `allFiles`.
- `NodeBox: @unchecked Sendable` wraps a node so it can be passed into detached tasks.
- When a scan is replaced, release the old tree on a background queue so the main thread never stalls freeing a million nodes.

`FileCategory` (an Int enum with a color and an SF Symbol each). Classify by lowercased extension from the raw UTF-8 name bytes, without creating a URL:

| Category | Color | Extensions |
| --- | --- | --- |
| Video | #FDA4AF | mp4 mov m4v mkv avi wmv flv webm mpg mpeg 3gp mts m2ts vob |
| Audio | #C4B5FD | mp3 wav aac m4a flac ogg aiff aif alac wma opus mid midi caf m4b |
| Image | #7DD3FC | jpg jpeg png gif heic heif tiff tif bmp webp svg raw cr2 cr3 nef arw dng psd ai sketch fig ico icns exr |
| Document | #FCD34D | pdf doc docx xls xlsx ppt pptx key pages numbers txt rtf md csv epub odt ods odp html htm tex |
| Developer | #86EFAC | swift m mm h c cc cpp hpp js mjs cjs ts tsx jsx json py pyc rb go rs java kt class jar o a dylib so node wasm map lock yml yaml toml xml sh zsh css scss less gradle pbxproj storyboard xib sql php vue svelte dart lua r ipynb tsbuildinfo d |
| Archive | #FDBA74 | zip gz tgz tar bz2 xz 7z rar dmg iso pkg xip zst lz4 cab |
| Apps | #93C5FD | anything inside a .app |
| System | #5EEAD4 | plist log db sqlite sqlite3 sqlite-wal sqlite-shm db-wal db-shm wal shm cache dat ldb idx pack vmdk img sparseimage sparsebundle asar |
| Other | #CBD5E1 | everything else |

# 5. Sidebar

1. **"Scan Full Mac"**: a big filled accent button.
2. Two outline buttons: **Home** and **Folder...** (`NSOpenPanel`, directories only, prompt "Scan").
3. **Recent**: the last 5 scanned paths, stored in `UserDefaults`. Clicking one rescans it.
4. **Disk Storage**: the volume name, a circular usage ring (accent angular gradient, "xx.x% USED" in the center), and Total / Used (red) / Free (green). Use `volumeAvailableCapacityForImportantUsage`.
5. **Current View**:
   - The folder name, and the scan duration (for example "5.5s scan").
   - The full path in monospaced text, truncated in the middle.
   - **Reveal** and **Copy Path** buttons. Copy Path briefly changes to "Copied ✓" for 1 second.
6. **Quick Wins**:
   - Show the total. Each row has an icon, title, item count and size. Clicking a row navigates to the biggest match.
   - Right-click menu: "Add all N to Cleanup" and "Reveal in Finder".
   - The categories are computed from the already-built tree (no extra disk pass) and sorted by size:
     - Downloads: `~/Downloads`
     - Caches & logs: `Library/Caches`, `Library/Logs`
     - iOS Simulators: `Developer/CoreSimulator`
     - Large media: video/audio/image files ≥ 10 MB
     - node_modules
     - Build artifacts: `build .build dist .next target .turbo .parcel-cache`
     - Xcode DerivedData
   - A category never counts something nested inside its own match. Cap each category at 400 items.
7. **File Types**: a thin stacked bar across categories, plus a legend with a color dot, name and size for each.

Sidebar actions always switch to the Explore tab.

# 6. Explore tab

- **Idle state**: "Map every byte on your Mac", with Scan Home and Choose Folder… buttons.
- **Scanning state**: a spinner, then "Scanning…", then a live "N files · X GB" counter (numeric text transition), then the current path.
- **Done state**:
  - **Header**: the big folder name, then its size, then "· N files · N folders", plus a Reveal in Finder icon button.
  - **Toolbar**:
    - Mode picker: an icon-only capsule segmented control; the active mode also shows its label and slides with `matchedGeometryEffect`.
    - Caption text for the active mode.
    - **Color mode** segmented control: By type / By folder / By age. Only for Treemap, Sunburst, Flame, Bubbles and Mind Map.
    - **Depth** slider from 2 to 10, default 7. Only for Treemap, Sunburst and Flame.
    - **"A" abbreviate-labels toggle**, which turns labels into initials ("Application Support" → "AS", "node_modules" → "NM"). Only for canvas-style modes.
    - Use `ViewThatFits` to drop the caption first, then the color labels, then wrap onto a second row.
  - **Unreadable folders notice**, shown when access was denied:
    - A lock icon and the text "N folders couldn't be read — sizes may be incomplete", with an explanation. Sample paths appear in a tooltip.
    - An **Open Privacy Settings** button (`x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles`) and a dismiss ✕.
    - When the app becomes active again (`NSApplication.didBecomeActiveNotification`) while this notice is showing, rescan silently so a newly granted Full Disk Access takes effect.

**Interactions everywhere**: single click selects; double click opens a folder (files are just selected). Canvas views hit-test their own geometry. A hover chip in the bottom-left shows icon, name, size, % of scan and a file-count pill. Right-click on any item offers: Open (folders), Quick Look, Reveal in Finder, Copy Path, Add to Cleanup.

**The 9 visualizations**, all rendering the same tree:

1. **Treemap**: "Every file as a rectangle, sized by bytes". Nested **squarified** layout (Bruls et al.). Folders are labelled groups with a header strip, down to the chosen depth. Skip cells smaller than about 6pt², and leave the remainder blank so proportions stay honest.
2. **Folders** (default mode): "Browse folder by folder, sized as you go".
   - A responsive grid (about 290pt columns) of **folder-shaped cards**: a custom `Shape` with a tab on the top-left, pastel tint, a subtle white gradient sheen, and a lift-and-shadow on hover.
   - Each card shows the name, a divider, up to 3 overlapping category color dots, "N items", and size.
   - Below the folders, a "Files" grid of tiles: category icon, name, "Category · relative age", and size. Double-clicking a file opens Quick Look. Cap files at 600.
3. **Sunburst**: "Rings radiating out from the scan root". One ring per depth level; arc length is proportional to size. The center disc shows the folder. Labels run radially on narrow arcs and along the ring on wide ones, and are never upside down.
4. **Flame**: "Depth top to bottom, size left to right". Each row is a depth level, and each block sits beneath its parent's span. Skip blocks under 1pt wide.
5. **Bubbles**: "Nested bubbles, one per folder". Hierarchical circle packing: folders are translucent circles containing their children; area is proportional to bytes.
6. **Mind Map**: "Branches from the root, sized by weight". A radial tree with curved links; dot size reflects share of the folder. Labels are placed biggest-first and skipped if they would collide.
7. **Top Sizes**: "The biggest items, ranked".
   - Scope picker: In this folder / Biggest files anywhere / Biggest folders anywhere. The "anywhere" rankings are the top 200, computed in a detached task and cached per folder and scan generation.
   - Each row has a rank, an icon, a pastel horizontal bar proportional to the largest item with the name (plus the parent path for the "anywhere" scopes) inside it, "N files" or the kind, % of folder, and size.
8. **Age Map**: "Where your bytes sit on a timeline". Computed off the main thread.
   - "How old are these bytes?" shows bytes in 6 buckets: Last 7 days, 8–30 days, 1–3 months, 3–12 months, 1–2 years, Over 2 years.
   - "Bytes by last-modified month" is a year × month heatmap with a less/more legend and the busiest month.
   - **"Big & Untouched"** lists files ≥ 40 MB not modified in over a year. Each row has a +/✓ toggle to stage or unstage it, and a "Stage top N for cleanup" button stages them all.
9. **List**: "Every item as an expandable outline". An `OutlineGroup` where each row has an icon, name, a mini bar of share-of-parent, %, and size. Only non-empty folders get disclosure triangles; cap at 500 children per level.

**Color modes**:
- **By folder**: each top-level branch gets its own pastel family (blue, teal, violet, amber, rose, green, sky, slate), with shades varying by depth and index.
- **By type**: color by dominant category, and **regroup** the view so every file under the folder is bucketed into one synthetic group per category.
- **By age**: color by age bucket (#34D399, #60A5FA, #818CF8, #A78BFA, #F472B6, #F87171), and regroup into "Last 7 days … Older" groups.

Regrouping builds a throwaway wrapper tree and never mutates the real tree.

**Rendering performance**:
- Cache each layout keyed by (scan generation, node, size, depth, color mode).
- Draw the heavy static layer in a `Canvas` wrapped in an `Equatable` view that only repaints when its key changes.
- Draw the hover highlight in a separate tiny `Canvas`, so moving the mouse never redraws the whole visualization.
- The name filter applies to Folders, Top Sizes and List.

# 7. Inspector (right panel)

For the selected node, or the current folder if nothing is selected:
- An icon and name, then "Folder" or the category name.
- The full path in selectable monospaced text.
- Big size text, plus "x.x% of scan".
- **Details**: Size on disk, Logical size, Compressed by (in green; the APFS savings), Files / Folders (or Kind for a file), Of parent %, Modified (relative), Created.
- **Largest Inside**: a ranked list of children.
- Buttons: Reveal, Quick Look, Focus (opens the folder, or the folder that contains the file), Copy Path.
- A big accent button, **"Add to Cleanup"**, that turns into **"✓ Staged for Cleanup"** (tap again to unstage).

Quick Look: a small `QuickLookController.shared.preview(url)` wrapper around `QLPreviewPanel`.

# 8. Cleanup Queue

`CleanupQueue: ObservableObject` (`@MainActor`):
- `@Published private(set) var items: [Item]`, where each Item has id, url, size, and reason ("Duplicate", "Big & Untouched", "App bundle: X", "X — Caches", and so on).
- Methods: `stage`, `stage(urls:)`, `unstage`, `remove`, `clear`, `contains`, `totalSize`, and `commitToTrash()`.

`Trash.move(urls)`:
- Sort paths shortest-first. Items nested inside an already-trashed folder count as trashed, and missing files count as already gone.
- Try `FileManager.trashItem` first. Retry failures with `NSWorkspace.shared.recycle`, which asks for an admin password for root-owned apps.
- Return `(trashed, failed)`.

After trashing, **remove those nodes from the in-memory tree without a rescan**:
- Subtract sizes, counts and typeSizes from every ancestor, and re-sort children.
- Fix `currentFolder` and `selectedNode` if they were removed.
- Prune duplicate groups and installed-app leftovers.
- Clear cached reports, bump `generation`, and recompute Quick Wins.

**Cleanup Queue popover**, opened from the top-bar button (460×520):
- Header: "Cleanup Queue", then "X GB staged", then a **Clear** button, then a **red** "Move to Trash…" button (`.borderedProminent` + `.tint(.red)`). Both buttons are disabled when the queue is empty.
- Each row: name, reason caption, size, and an ✕ button to remove it.
- Empty state: a tray icon and "Nothing staged yet…".
- Confirmation alert: "Move N items to Trash?", with "This frees X. Items go to the Trash…". On success, dismiss; on failure, show "Couldn't trash N item(s)".

**IMPORTANT SwiftUI pitfalls (these caused real bugs; avoid them):**
- `CleanupQueue` and `SnapshotStore` are ObservableObjects **nested inside** `ScanViewModel`. Their changes do **not** propagate through the parent. Every view that displays their data must receive them explicitly as `@ObservedObject`, for example `CleanupQueueView(queue: viewModel.cleanupQueue)`. Never read `viewModel.cleanupQueue.items` from a view that only observes the view model, or Clear/✕ won't update until the popover is reopened.
- Give the popover an opaque background: `.presentationBackground(theme.background)`, plus `.scrollContentBackground(.hidden)` on its `List`. The default translucent popover material picks up color from the window behind it.

# 9. Duplicates tab

- A "Scan for Duplicates" button, which requires a finished scan.
- Three-pass detection:
  1. Group files by logical size, ignoring files with 0 allocated blocks (iCloud placeholders).
  2. SHA-256 the first 64 KB of each candidate.
  3. SHA-256 the full contents only for files larger than 64 KB that still match.
- Hash in parallel with `concurrentPerform`, streaming 1 MB chunks through `open`/`read`, with `F_NOCACHE` on for full hashes.
- Sort groups by wasted space: size × (count − 1).
- UI: "X could be reclaimed across N groups". Each group lists its files, and each file has **"Keep this, stage the rest"**, which stages the others with reason "Duplicate".

# 10. Applications tab (App Uninstaller)

- List every `.app` in `/Applications`, with bundle size measured in parallel.
  - Each entry has: Finder icon (cached), display name, bundle id, version, and "Last used" (Spotlight `kMDItemLastUsedDate`).
  - Show the total footprint, plus "+X leftovers" when there are any.
- Leftovers are matched case-insensitively by bundle id **or** name with spaces removed, inside `~/Library/`:
  - Caches
  - Preferences
  - Application Support
  - Logs
  - HTTP Storage
  - Saved Application State
  - Containers
- List each Library folder only once and share the listing across all apps.
- Load everything in a detached task and keep the result across tab switches. Header: "N installed · total", with a Refresh button.
- Sort apps by total footprint. Rows expand to show "App bundle" and each leftover group with sizes.
- **Uninstall** alert:
  - "Move to Trash" quits the app first: `terminate()`, wait up to 5 seconds, then `forceTerminate()`. Then it trashes the bundle and all leftovers.
  - "Add to Cleanup Queue" stages the bundle ("App bundle: X") and each group ("X — Caches", and so on).
  - "Cancel".
- If anything fails, show an alert listing the paths, telling the user to allow the app under Privacy & Security → App Management / Full Disk Access, with an "Open Privacy Settings" button.

# 11. Monitor tab (live, every 2 seconds, no shelling out)

`SystemMonitor.sample()` uses Mach/BSD APIs, and computes deltas against the previous sample:
- **CPU**: `host_statistics(HOST_CPU_LOAD_INFO)` for user (including nice), system and idle %, plus `getloadavg`.
- **Memory**: `hw.memsize`, and `host_statistics64(HOST_VM_INFO64)` for wired, compressed, active, inactive and free.
- **Network**: `getifaddrs` over up, non-loopback `en*` interfaces using `if_data`. Report down/up bytes per second and session totals.
- **Processes**: `proc_listpids`, `proc_pid_rusage` (CPU time delta → %; resident memory), and `proc_name`. Keep the top 60 by CPU and the top 60 by memory.

Layout: a "Monitor" title, then a 2×2 grid of cards:
- **CPU**: big %, a 120-sample sparkline, and USER / SYSTEM / LOAD.
- **Memory**: used bytes; a segmented bar of wired (red), compressed (gold), other (accent) and free; WIRED / COMPRESSED / TOTAL; and a "Pressure over time" sparkline.
- **Network**: "X/s ↓", with DOWN / UP / SESSION IN.
- **Storage**: every mounted browsable volume with a used/total bar ("Show N more volumes" after 3), and a "Free space over time" sparkline.

Below the grid, a **Top Processes** table:
- A CPU/Memory sort toggle and a "Filter processes" field.
- Columns: PROCESS / PID / CPU / MEMORY.
- A "Show all N" expander.

# 12. Snapshots tab

- **"Take Snapshot Now"** is disabled when there's no scan, while scanning, or while saving. It saves folder path → logical size **only for folders ≥ 1 MB** (skip entire smaller subtrees), off the main thread.
- Storage: one JSON file per snapshot in `~/Library/Application Support/DiskDude/Snapshots/`, plus a small `index.json` of summaries (id, root path, date, total, folder count) so the list loads instantly.
- Each row has the path, "size · date", **Before** and **After** toggle buttons, and a delete button.
- Picking Before and After loads both snapshots in the background and diffs them. A folder missing on one side counts as 0.
  - Show the top 200 changes sorted by absolute change: "+X" in red for growth, green for shrinkage.
  - If the two snapshots have different roots, say so.

# 13. Theme & visual design

- A `Theme` struct is injected through a custom `EnvironmentValues.theme`. Light and Dark palettes are based on Apple system colors:
  - **Light**: background #FFFFFF, panel #F5F5F7, card #FFFFFF, border #D8D8DD, divider #E5E5EA, chip #F2F2F7, track #E5E5EA, text #1D1D1F, textSecondary #6E6E73, textTertiary #8E8E93, **accent (ink) #FF6B4A**, onInk white, used #FF3B30, free #34C759.
  - **Dark**: background #1E1E20, panel #28282B, card #2C2C2E, border #3F3F43, divider #38383A, chip #3A3A3C, track #3A3A3C, text #F5F5F7, textSecondary #AEAEB2, textTertiary #8E8E93, ink #FF7A5C, used #FF453A, free #30D158.
- The theme toggle sets `NSApplication.shared.appearance` (aqua or darkAqua) and saves the choice to `UserDefaults`. The first launch follows the system appearance.
- Data colors (pastels) are the same in both themes. Text drawn on top of pastel fills uses #0F172A and #475569.
- Shared components:
  - `InkButton`: filled accent.
  - `OutlineButton`: card fill with border, and optional transient confirm text.
  - `SquareIconButton`, `SectionCaption` (uppercase, tracked, tertiary color), and `PressableStyle` (scale 0.985 and opacity 0.75 on press).
- Spacing: 8–14pt continuous corner radii, and snappy spring animations (0.25s).
- `Format.bytes`: **decimal** units with 3 significant digits, like Finder ("118 GB", "5.12 GB", "33.7 MB"). Also provide percent helpers ("<0.1%"), relative ages ("Just now", "3 days ago"), and duration ("5.5s").

# 14. Dev hooks

Two environment variables skip the clicking:
- `DISKDUDE_SCAN=/path` starts a scan on launch.
- `DISKDUDE_MODE=Sunburst` picks the visualization (Treemap, Folders, Sunburst, Flame, Bubbles, Mind Map, Top Sizes, Age Map, List).

# 15. Packaging (.app + .dmg)

- **`Packaging/AppConfig.env`** is the single source of truth: `APP_NAME="DiskDude"`, `EXECUTABLE_NAME="DiskDude"`, `BUNDLE_ID="com.<you>.diskdude"`, `VERSION="1.0.1"`, `BUILD_NUMBER="2"`, `MIN_MACOS="14.0"`, `COPYRIGHT`, `ARCHS="${ARCHS:-arm64 x86_64}"`, `SIGN_IDENTITY="${SIGN_IDENTITY:-}"`, `NOTARY_PROFILE`.
- **`Packaging/Info.plist.template`** uses `__TOKENS__`. It sets the category to utilities, `NSHighResolutionCapable`, and `CFBundleIconFile AppIcon`, plus purpose strings for Desktop, Documents, Downloads, removable volumes and network volumes ("…measures the size of files… Nothing is moved or deleted unless you confirm it.").
- **`Packaging/App.entitlements`**: an empty dict, with a comment explaining there is no sandbox and Hardened Runtime is enabled via codesign.
- **`Packaging/Icon/make_icon.swift`** plus **`scripts/make_icon.sh`**: generate an original placeholder icon (a dark navy rounded tile with concentric colored storage-ring segments) into an `.iconset`, then run `iconutil` to produce `AppIcon.icns`.
- **`scripts/build_app.sh`**:
  1. Validate the config values contain no `& < > | \`.
  2. If `xcode-select` points at the Command Line Tools, set `DEVELOPER_DIR` to `/Applications/Xcode.app`. If Xcode is missing, fall back to the host architecture only.
  3. Run `swift build -c release --arch arm64 --arch x86_64` (universal).
  4. Assemble `dist/DiskDude.app`, and `strip -rSTx` the binary.
  5. Fill in the Info.plist with `sed`, and check it with `plutil -lint`.
  6. Write PkgInfo and copy the icon.
  7. Run `xattr -cr`.
  8. Run `codesign --force --options runtime` with the entitlements: Developer ID with `--timestamp` if `SIGN_IDENTITY` is set, otherwise ad-hoc (`--sign -`).
  9. Verify with `codesign --verify --strict` and print the architectures with `lipo -archs`.
- **`scripts/package_app.sh`** (`--no-build` to reuse the existing app):
  - Create `dist/DiskDude-<VERSION>.zip` with `ditto -c -k --norsrc --noextattr --keepParent`.
  - Create `dist/DiskDude-<VERSION>.dmg` by staging the app plus an `/Applications` symlink, then `hdiutil create -volname DiskDude -fs HFS+ -format UDZO`.
  - Sign the dmg if an identity is set, and print SHA-256 checksums.
- **`scripts/notarize.sh <file>`**:
  - Refuse unless the app is signed with Developer ID, Hardened Runtime and a timestamp.
  - Run `xcrun notarytool submit --keychain-profile … --wait`.
  - Staple the ticket and check it with `spctl --assess`. For a zip, staple the app and re-zip it.
- `.gitignore`: `.DS_Store .build/ .swiftpm/ dist/ *.iconset/`.
- **`docs/DISTRIBUTION.md`** and **`README.md`** should cover build/run, packaging, permissions (Full Disk Access is granted manually and can't be requested), and Gatekeeper.
  - Without a paid Developer ID, other Macs show "Apple could not verify…".
  - Users can fix it with System Settings → Privacy & Security → **Open Anyway**, or `xattr -dr com.apple.quarantine /Applications/DiskDude.app`.

# 16. Deliverable & acceptance checklist

Write every file in full: no placeholders, no TODO stubs, and no "rest of implementation here". Then verify:
- [ ] `swift build` succeeds with no errors, and `swift run` opens the window.
- [ ] Scanning Home finishes quickly and shows sizes, counts, Quick Wins and File Types.
- [ ] All 9 visualizations render; hover chip, click-select, double-click-open, depth, color modes and abbreviation all work.
- [ ] Staging from the inspector, Age Map, Quick Wins, Duplicates and Uninstaller updates the badge. **Clear and ✕ in the popover update instantly.**
- [ ] Move to Trash is red, confirms first, trashes items, and removes them from every view without a rescan.
- [ ] Monitor updates every 2 seconds; snapshots save, list, compare and delete.
- [ ] Light/Dark toggle persists across launches.
- [ ] `scripts/package_app.sh` produces `dist/DiskDude-1.0.1.dmg` containing a universal app.

This is original code. Don't copy any other product's code, assets or branding.
