Concurrency Architecture
RawCull actor isolation, structured concurrency, backpressure, cancellation, and request coalescing.
RawCull is a macOS app for culling RAW photos: scanning a folder of camera RAW files (Sony ARW today), showing fast previews, letting the photographer rate/reject/flag images, grouping near-duplicate “burst” shots with on-device AI, and finally exporting the keepers to a destination folder.
This document is the entry point and index for the whole Docs/
catalog. Read it first, then follow the links below — in the “Reading
order” section, and repeated here for quick reference — for deep dives into
each subsystem. All docs assume you already know Swift and SwiftUI, but
nothing about this codebase.
| # | Document | Covers |
|---|---|---|
| 00 | Overview (this file) | App summary, source map, composition root, glossary |
| 01 | Concurrency-Architecture.md | Actors, TaskGroup backpressure, Sendable rules, coalescing |
| 02 | Image-Pipeline-and-Caching.md | Scan → decode → thumbnail → cache pipeline |
| 03 | Culling-and-Persistence.md | Ratings model, debounced save, catalog switching |
| 04 | Export-Copy-Pipeline.md | The rsync-backed export/copy step |
| 05 | Intelligence-AI-Subsystem.md | On-device AI: similarity, semantic search, deep review |
| — | Intelligence Runtime | Deep dive: how the Intelligence runtime is built, reconfigured, and extended |
| 06 | SwiftUI-View-Layer.md | Navigation, state-management idioms, the grid view |
| 07 | Settings-and-Configuration.md | User preferences, AI settings, memory monitor |
| — | Features and Roadmap | RawCull vs. other culling apps, and the post-3.2.0 roadmap |
| — | Known Issues | Code-review findings, with severities |
Naming note: the project (and this worktree’s branch) carries the name “rsyncosx” because it evolved from the author’s earlier RsyncOSX rsync GUI. RawCull is not a two-way sync tool — it’s a one-way RAW photo culling/export app that happens to reuse rsync (and several helper packages) from that project for its final copy step. See Export and Copy Pipeline for the full story and a table of legacy names you’ll bump into while reading the code.
RawCull/
├── Main/ RawCullApp (the @main entry point), RawCullMainView, FileItem typealias
├── Actors/ The file-scanning / thumbnail / disk-cache pipeline (all `actor`s)
├── Model/
│ ├── ViewModels/ RawCullViewModel (central state) + its feature extensions,
│ │ CullingModel, SettingsViewModel, MemoryViewModel, GridThumbnailViewModel
│ ├── Cache/ Cache config/keys shared by the Actors layer
│ ├── Handlers/ Small callback-bundle structs passed into background workers
│ ├── ParametersRsync/ Copy/export configuration + rsync argument building
│ └── JSON/ Codable read/write helpers for on-disk persistence
├── Intelligence/ The on-device AI subsystem (similarity, semantic search,
│ burst analysis, deep review, model management) — see its own doc
├── Views/ SwiftUI view layer, one folder per feature area
└── Extensions/ Small Foundation/Thread extensions
RawCullCore (types like RawCullFileItem/FileItem, RawCullSourceCatalog,
ExifMetadata, burst grouping/ranking algorithms) and the AI-facing
PhotoAIContracts/PhotoAnalysisKit packages, plus RawParserKit (RAW
decoding) and the rsync helper packages (RsyncArguments,
RsyncProcessStreaming, ParseRsyncOutput, DecodeEncodeGeneric), are all
separate Swift packages pulled in via SPM from the rsyncOSX GitHub org.
They are out of scope for this documentation — treat them as RawCull’s
external boundary and consult their own repos if you need their internals.
The app has exactly one place where its two long-lived object graphs are
built: RawCullApplicationState.live() in
RawCull/Intelligence/Composition/RawCullIntelligenceRuntime.swift.
@MainActor
struct RawCullApplicationState {
let intelligenceRuntime: RawCullIntelligenceRuntime
let viewModel: RawCullViewModel
static func live() -> RawCullApplicationState { make(integration: RawCullAIIntegration()) }
static func make(integration:, similarityArtifactStore:, userDefaults:, ...) -> RawCullApplicationState { ... }
}
RawCullApp.init() calls RawCullApplicationState.live() once and stores
viewModel and intelligenceRuntime in @State. Everything downstream
(views, feature controllers, background workers) either receives these by
parameter or reads them via .environment(...). Nothing else in the app
constructs a second RawCullViewModel or a second AI runtime — this matters
because several assertions in make(...) (assert(viewModel.similarityFeature === intelligenceRuntime.similarityFeature), etc.) exist specifically to catch
an accidental second instance during refactors.
RawCullApp (RawCull/Main/RawCullApp.swift) declares three Scenes:
"main-window" — hosts RawCullMainView, the whole photo-browsing UI.Settings — hosts SettingsView, bound to intelligenceRuntime.settingsModel."about-window" — a simple about box.An NSApplicationDelegateAdaptor-backed AppDelegate intercepts app
termination to flush pending culling-state writes
(viewModel.cullingModel.flushPersistence()) before actually quitting, and to
release any active security-scoped folder access.
RawCullViewModel (Model/ViewModels/RawCullViewModel.swift) is a
@MainActor @Observable final class — the single source of truth for almost
all UI-visible state: the current file list, selection, view mode (loupe /
grid / similarity grid / rated grid / comparison grid), zoom-overlay state,
rating filter, and references to the “stable feature boundaries” it doesn’t
own outright: similarityFeature, semanticSearchFeature,
deepAIReviewController, burstAnalysisCoordinator, plus its own
cullingModel and sharpnessModel.
It’s split across one main file and several RawCullViewModel+*.swift
extensions by concern (+Culling, +Catalog, +Sharpness, +Similarity,
+Thumbnails, +BurstGrouping) — a common Swift pattern for keeping a large
@Observable model’s storage in one place while spreading its behavior
across topic-focused files.
TaskGroup, cancellation, Sendable rules. Read this early; almost every
other subsystem leans on these patterns.| Term | Meaning |
|---|---|
Catalog / ARWSourceCatalog | A user-selected source folder of RAW files, plus its bookmark/metadata. Culling decisions are keyed per catalog. |
| Culling | The act of rating (-1 reject, 0 keeper, 2–5 stars) or flagging RAW files so a subset can be exported. |
| Burst | A group of near-duplicate frames (e.g. continuous shooting) detected via similarity scoring, presented together so the user picks the best one. |
| Deep Review | An optional, heavier AI pass over a burst group (subject segmentation + focus scoring) that recommends a winner. |
| Loupe | The single-photo detail view mode (as opposed to a grid of thumbnails). |
| FileItem | typealias for RawCullCore.RawCullFileItem — the value type representing one scanned RAW file (name, URL, metadata). |
RawCull actor isolation, structured concurrency, backpressure, cancellation, and request coalescing.
The RawCull scan, decode, thumbnail, preview, memory-cache, and disk-cache pipeline.
RawCull ratings, filters, catalog switching, and debounced persistent storage.
The one-way rsync-backed pipeline RawCull uses to export selected photographs.
RawCull’s on-device similarity, semantic search, burst analysis, and deep-review architecture.
How RawCull constructs, refreshes, and safely extends its long-lived intelligence runtime.
RawCull windows, navigation, state flow, grid composition, and view-layer conventions.
RawCull preferences, AI configuration, settings persistence, and memory monitoring.
RawCull’s product position, design principles, and post-3.2.0 roadmap.
Prioritized RawCull code-review findings and recommended remediations.
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.