diff --git a/core/dr-catalog/src/recovery.rs b/core/dr-catalog/src/recovery.rs index cb1b185..5d7be11 100644 --- a/core/dr-catalog/src/recovery.rs +++ b/core/dr-catalog/src/recovery.rs @@ -198,6 +198,54 @@ pub fn backup_before_migration(conn: &Connection, catalog: &Path) -> Result<(), Ok(()) } +/// How long a catalog may go without a backup before the next opportunity +/// takes one. +/// +/// A day. The catalog is an index, so what a backup protects is the day's +/// worth of collection and people edits the sidecars do not hold — and a +/// second copy of a 130 MB file per launch would be a cost with nothing to +/// show for it when the user launches four times in an afternoon. +pub const BACKUP_EVERY: i64 = 24 * 60 * 60; + +/// Whether [`BACKUP_EVERY`] has passed since the newest backup, or there is +/// none. +/// +/// Read from the filenames, like [`backups`], so a restored or copied backup +/// directory answers the same way it did on the machine it came from. +pub fn backup_due(catalog: &Path) -> bool { + match backups(catalog).first() { + Some(newest) => now() - newest.taken_at >= BACKUP_EVERY, + None => true, + } +} + +/// TRACES: NFR-R2 +/// Take the scheduled backup, if one is due. Returns the file written, or +/// `None` when the newest is recent enough. +/// +/// The scheduled half of NFR-R2 — the migration half is +/// [`backup_before_migration`]. "On a schedule" for an application that runs +/// when the user opens it means "at the next chance after a day has passed", +/// and the chance the caller picks is the end of a library sweep: the +/// catalog is quiet, the work is already off the UI thread, and it is the +/// moment a day's edits have just been consolidated. +/// +/// A brand-new catalog with no images is not backed up: there is nothing in +/// it yet that a rescan would not rebuild, and the first backup would only be +/// a copy of an empty schema. +pub fn backup_if_due(conn: &Connection, catalog: &Path) -> Result, CatalogError> { + if !backup_due(catalog) { + return Ok(None); + } + let images: i64 = conn.query_row("SELECT count(*) FROM images", [], |r| r.get(0))?; + if images == 0 { + return Ok(None); + } + let path = backup(conn, catalog)?; + log::info!("scheduled backup of the catalog to {}", path.display()); + Ok(Some(path)) +} + /// The backups available for `catalog`, newest first. /// /// Never fails: an unreadable or absent backup directory means there are no @@ -386,6 +434,49 @@ mod tests { base } + #[test] + fn a_scheduled_backup_is_taken_once_a_day_and_not_more() { + let dir = tempdir("scheduled"); + let path = dir.join("catalog.sqlite"); + fixture(&path, 3); + let cat = Catalog::open(&path).unwrap(); + + // Nothing yet: due. + assert!(backup_due(&path)); + let first = backup_if_due(cat.connection(), &path).unwrap(); + assert!(first.is_some(), "the first opportunity takes one"); + + // Taken just now: not due, and a second call does nothing. + assert!(!backup_due(&path)); + assert_eq!(backup_if_due(cat.connection(), &path).unwrap(), None); + assert_eq!(backups(&path).len(), 1); + + // Age the one backup past the interval by renaming it, since the + // timestamp is read from the name. Now it is due again. + let old = first.unwrap(); + let aged = old + .parent() + .unwrap() + .join(format!("catalog-{}.sqlite", now() - BACKUP_EVERY - 1)); + std::fs::rename(&old, &aged).unwrap(); + assert!(backup_due(&path)); + assert!(backup_if_due(cat.connection(), &path).unwrap().is_some()); + assert_eq!(backups(&path).len(), 2); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn an_empty_catalog_is_not_worth_backing_up() { + let dir = tempdir("empty"); + let path = dir.join("catalog.sqlite"); + let cat = Catalog::open(&path).unwrap(); + assert!(backup_due(&path), "due in principle"); + assert_eq!(backup_if_due(cat.connection(), &path).unwrap(), None); + assert!(backups(&path).is_empty()); + let _ = std::fs::remove_dir_all(&dir); + } + /// A catalog on disk with enough rows to span several pages, closed. /// /// Closed matters: WAL means the rows are in `catalog.sqlite-wal` until diff --git a/core/dr-catalog/src/sync.rs b/core/dr-catalog/src/sync.rs index 67022b6..6b5efea 100644 --- a/core/dr-catalog/src/sync.rs +++ b/core/dr-catalog/src/sync.rs @@ -54,9 +54,31 @@ pub fn checkpoint(conn: &Connection) -> Result<(), CatalogError> { pub fn snapshot_for_upload(conn: &Connection, dest: &Path) -> Result<(), CatalogError> { let out = copy_to(conn, dest)?; strip_face_crops(&out)?; + verify_snapshot(&out)?; Ok(()) } +/// TRACES: NFR-R2 +/// Refuse to hand over a snapshot that will not pass `quick_check`. +/// +/// The upload is the copy every other device merges from, and a damaged one +/// costs far more than the check: each device downloads it, fails, and — for +/// a week, once — declines to push over it. `quick_check` reads every page +/// but skips index verification, which is the affordable version of "is this +/// a database" on a 40 MB file that has just been written and is still in the +/// page cache. A failure here is [`CatalogError::Corrupt`], the same thing a +/// receiving device would have said, so the sync reports it the same way. +fn verify_snapshot(snapshot: &Connection) -> Result<(), CatalogError> { + let verdict: String = snapshot.query_row("PRAGMA quick_check", [], |r| r.get(0))?; + if verdict == "ok" { + Ok(()) + } else { + Err(CatalogError::Corrupt { + detail: format!("the snapshot for upload failed quick_check: {verdict}"), + }) + } +} + /// Checkpoint, then copy the whole database to `dest`, and hand back the /// connection to the copy. /// diff --git a/core/dr-sync-nextcloud/src/lib.rs b/core/dr-sync-nextcloud/src/lib.rs index ad93db0..bac7816 100644 --- a/core/dr-sync-nextcloud/src/lib.rs +++ b/core/dr-sync-nextcloud/src/lib.rs @@ -121,16 +121,24 @@ impl NextcloudBackend { body: Vec, ) -> Result { let total = body.len() as u64; - // Named from the destination so a resumed or abandoned upload is - // identifiable, and so two uploads cannot collide in one directory. - let token: String = path - .as_str() - .bytes() - .map(|b| match b { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' => (b as char).to_string(), - _ => "-".to_string(), - }) - .collect(); + // TRACES: FR-NC-9 + // Named from the destination, so an abandoned upload is identifiable, + // **and from a nonce, so no two uploads ever share a directory.** + // + // The name used to be the destination alone, on the reasoning that two + // *files* could then not collide. Two *devices* uploading the same file + // could, and did: both wrote `00001`…`00009` into one directory, and + // whichever `MOVE`d first assembled a mix of the two — a catalog of + // exactly the right size whose pages came from two different + // databases. SQLite called it malformed, every client then declined to + // overwrite it, and collections stopped syncing on all of them for a + // week. An upload that died on a phone's link left its chunks there + // for the next device to assemble in, by the same mechanism. + let token = format!( + "{}-{}", + sanitise_for_upload_dir(path.as_str()), + upload_nonce() + ); let dir = format!( "{}/remote.php/dav/uploads/{}/{token}", self.server, self.login @@ -138,6 +146,27 @@ impl NextcloudBackend { self.mkcol_url(&dir).await?; + // Whatever happens below, the directory does not outlive the attempt. + // With a unique name a leftover is only quota rather than corruption, + // but a phone that abandons uploads all day would still leave dozens + // of 5 MB chunks behind, and the server only sweeps them eventually. + let result = self.put_chunks_and_assemble(path, &dir, body, total).await; + if result.is_err() { + self.discard_upload_dir(&dir).await; + } + result + } + + /// The transfer half of [`put_chunked`](Self::put_chunked): the chunks, + /// then the `MOVE` that assembles them. Split out so that a failure at any + /// point returns to one place that cleans up. + async fn put_chunks_and_assemble( + &self, + path: &RemotePath, + dir: &str, + body: Vec, + total: u64, + ) -> Result { // Chunks are numbered from 1 and must sort correctly as strings, which // is why they are zero-padded rather than bare integers. let chunk_size = CHUNKS.min_chunk as usize; @@ -191,6 +220,27 @@ impl NextcloudBackend { self.dir_validator(path).await } + /// Best-effort `DELETE` of an upload directory whose transfer failed. + /// + /// Errors are logged and dropped: this runs on the way out of a failure, + /// and the failure is what the caller needs to hear about. A connection + /// that has just died will refuse this too, and that is fine — the + /// server sweeps abandoned upload directories on its own; this only + /// spares it the wait when the link is still up. + async fn discard_upload_dir(&self, dir: &str) { + let attempt = self + .client + .delete(dir) + .basic_auth(&self.login, Some(&self.password)) + .send() + .await; + match attempt { + Ok(resp) if resp.status().is_success() || resp.status() == 404 => {} + Ok(resp) => log::debug!("leaving abandoned upload {dir}: {}", resp.status()), + Err(e) => log::debug!("leaving abandoned upload {dir}: {e}"), + } + } + /// `MKCOL` at an absolute URL, treating "already there" as success. async fn mkcol_url(&self, url: &str) -> Result<(), RemoteError> { let resp = self @@ -725,6 +775,37 @@ fn map_send_error(e: reqwest::Error) -> RemoteError { } } +/// The destination path reduced to characters every WebDAV server accepts in +/// an upload directory name. +fn sanitise_for_upload_dir(path: &str) -> String { + path.bytes() + .map(|b| match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' => (b as char).to_string(), + _ => "-".to_string(), + }) + .collect() +} + +/// A token no other upload — on this device or any other — will produce. +/// +/// Nanoseconds since the epoch, the process id, and a counter, mixed rather +/// than concatenated so the name stays short. Two devices would have to start +/// an upload in the same nanosecond from the same pid to collide, and the +/// counter separates two uploads this process starts in one tick. No +/// randomness crate is pulled in for this: unique is the requirement, not +/// unguessable. +fn upload_nonce() -> String { + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(0); + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0); + let pid = u64::from(std::process::id()); + let n = COUNTER.fetch_add(1, Ordering::Relaxed); + format!("{:016x}", nanos ^ (pid << 40) ^ n.rotate_left(20)) +} + /// Translate an HTTP status into a typed error. fn map_status(status: reqwest::StatusCode, what: &str) -> Result<(), RemoteError> { match status.as_u16() { @@ -765,6 +846,33 @@ fn encode_path(path: &str) -> String { #[cfg(test)] mod tests { + #[test] + fn two_uploads_of_one_destination_never_share_a_directory() { + // The collision that assembled two devices' chunks into one file. + // Same path, back to back, same process: still two names. + let a = format!( + "{}-{}", + super::sanitise_for_upload_dir("PhotosRaw/.darkroom-derived/catalog.sqlite"), + super::upload_nonce() + ); + let b = format!( + "{}-{}", + super::sanitise_for_upload_dir("PhotosRaw/.darkroom-derived/catalog.sqlite"), + super::upload_nonce() + ); + assert_ne!(a, b); + assert!(a.starts_with("PhotosRaw--darkroom-derived-catalog-sqlite-")); + } + + #[test] + fn upload_directory_names_are_plain() { + assert_eq!( + super::sanitise_for_upload_dir("Photos Raw/été/x.sqlite"), + "Photos-Raw---t---x-sqlite" + ); + assert!(super::upload_nonce().bytes().all(|b| b.is_ascii_hexdigit())); + } + use super::*; fn backend() -> NextcloudBackend { diff --git a/docs/traceability.md b/docs/traceability.md index 12ec673..260a736 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| | Source files scanned | 354 | -| TRACES tags found | 1469 | +| TRACES tags found | 1479 | | Requirements defined | 191 | | Requirements covered | 140 | | **Coverage** | **73.3%** (140/191) | @@ -37,17 +37,17 @@ _None._ | FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:309`](../core/dr-types/src/settings.rs#L309), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:586`](../platform/dr-plat/src/storage.rs#L586), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1508`](../ui/dr-ui/src/lib.rs#L1508), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1033`](../ui/dr-ui/ui/library.slint#L1033), [`ui/dr-ui/ui/library.slint:1254`](../ui/dr-ui/ui/library.slint#L1254), [`ui/dr-ui/ui/library.slint:979`](../ui/dr-ui/ui/library.slint#L979) | | FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1508`](../ui/dr-ui/src/lib.rs#L1508), [`ui/dr-ui/src/library.rs:183`](../ui/dr-ui/src/library.rs#L183), [`ui/dr-ui/src/library.rs:3047`](../ui/dr-ui/src/library.rs#L3047), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:122`](../core/dr-pipeline/src/sidecar.rs#L122) | -| FR-CAT-13 | [`core/dr-catalog/src/rating.rs:278`](../core/dr-catalog/src/rating.rs#L278), [`core/dr-catalog/src/schema.rs:653`](../core/dr-catalog/src/schema.rs#L653), [`core/dr-sync/src/scan.rs:40`](../core/dr-sync/src/scan.rs#L40), [`core/dr-sync/src/scan.rs:944`](../core/dr-sync/src/scan.rs#L944), [`core/dr-types/src/settings.rs:92`](../core/dr-types/src/settings.rs#L92), [`core/dr-xmp/src/lib.rs:1`](../core/dr-xmp/src/lib.rs#L1), [`core/dr-xmp/src/lib.rs:331`](../core/dr-xmp/src/lib.rs#L331), [`core/dr-xmp/src/lib.rs:442`](../core/dr-xmp/src/lib.rs#L442), [`core/dr-xmp/src/lib.rs:535`](../core/dr-xmp/src/lib.rs#L535), [`core/dr-xmp/src/lib.rs:733`](../core/dr-xmp/src/lib.rs#L733), [`core/dr-xmp/src/lib.rs:746`](../core/dr-xmp/src/lib.rs#L746), [`core/dr-xmp/src/read.rs:1`](../core/dr-xmp/src/read.rs#L1), [`core/dr-xmp/src/write.rs:1`](../core/dr-xmp/src/write.rs#L1), [`core/dr-xmp/tests/round_trip.rs:1`](../core/dr-xmp/tests/round_trip.rs#L1), [`ui/dr-ui/src/library.rs:1500`](../ui/dr-ui/src/library.rs#L1500), [`ui/dr-ui/src/library.rs:1563`](../ui/dr-ui/src/library.rs#L1563), [`ui/dr-ui/src/library.rs:1579`](../ui/dr-ui/src/library.rs#L1579), [`ui/dr-ui/src/library.rs:1686`](../ui/dr-ui/src/library.rs#L1686), [`ui/dr-ui/src/library_ui.rs:1285`](../ui/dr-ui/src/library_ui.rs#L1285), [`ui/dr-ui/src/library_ui.rs:301`](../ui/dr-ui/src/library_ui.rs#L301), [`ui/dr-ui/src/library_ui.rs:3411`](../ui/dr-ui/src/library_ui.rs#L3411), [`ui/dr-ui/src/library_ui.rs:3467`](../ui/dr-ui/src/library_ui.rs#L3467), [`ui/dr-ui/src/library_ui.rs:3565`](../ui/dr-ui/src/library_ui.rs#L3565), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:519`](../ui/dr-ui/src/library_ui.rs#L519), [`ui/dr-ui/src/library_ui.rs:5709`](../ui/dr-ui/src/library_ui.rs#L5709), [`ui/dr-ui/src/settings_ui.rs:506`](../ui/dr-ui/src/settings_ui.rs#L506), [`ui/dr-ui/src/xmp_sync.rs:1`](../ui/dr-ui/src/xmp_sync.rs#L1), [`ui/dr-ui/src/xmp_sync.rs:250`](../ui/dr-ui/src/xmp_sync.rs#L250), [`ui/dr-ui/src/xmp_sync.rs:287`](../ui/dr-ui/src/xmp_sync.rs#L287), [`ui/dr-ui/src/xmp_sync.rs:324`](../ui/dr-ui/src/xmp_sync.rs#L324), [`ui/dr-ui/ui/app.slint:849`](../ui/dr-ui/ui/app.slint#L849), [`ui/dr-ui/ui/settings.slint:1001`](../ui/dr-ui/ui/settings.slint#L1001), [`ui/dr-ui/ui/settings.slint:184`](../ui/dr-ui/ui/settings.slint#L184) | -| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:923`](../core/dr-catalog/src/schema.rs#L923), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:457`](../core/dr-sync-nextcloud/src/lib.rs#L457), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:618`](../core/dr-sync/src/scan.rs#L618), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`core/dr-types/src/place.rs:91`](../core/dr-types/src/place.rs#L91), [`ui/dr-ui/src/collections_ui.rs:1435`](../ui/dr-ui/src/collections_ui.rs#L1435), [`ui/dr-ui/src/collections_ui.rs:2341`](../ui/dr-ui/src/collections_ui.rs#L2341), [`ui/dr-ui/src/library.rs:183`](../ui/dr-ui/src/library.rs#L183), [`ui/dr-ui/src/library.rs:200`](../ui/dr-ui/src/library.rs#L200), [`ui/dr-ui/src/library.rs:249`](../ui/dr-ui/src/library.rs#L249), [`ui/dr-ui/src/library.rs:4845`](../ui/dr-ui/src/library.rs#L4845), [`ui/dr-ui/src/library.rs:4879`](../ui/dr-ui/src/library.rs#L4879), [`ui/dr-ui/src/library_ui.rs:238`](../ui/dr-ui/src/library_ui.rs#L238), [`ui/dr-ui/src/library_ui.rs:870`](../ui/dr-ui/src/library_ui.rs#L870), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:745`](../ui/dr-ui/ui/collections.slint#L745) | +| FR-CAT-13 | [`core/dr-catalog/src/rating.rs:278`](../core/dr-catalog/src/rating.rs#L278), [`core/dr-catalog/src/schema.rs:653`](../core/dr-catalog/src/schema.rs#L653), [`core/dr-sync/src/scan.rs:40`](../core/dr-sync/src/scan.rs#L40), [`core/dr-sync/src/scan.rs:944`](../core/dr-sync/src/scan.rs#L944), [`core/dr-types/src/settings.rs:92`](../core/dr-types/src/settings.rs#L92), [`core/dr-xmp/src/lib.rs:1`](../core/dr-xmp/src/lib.rs#L1), [`core/dr-xmp/src/lib.rs:331`](../core/dr-xmp/src/lib.rs#L331), [`core/dr-xmp/src/lib.rs:442`](../core/dr-xmp/src/lib.rs#L442), [`core/dr-xmp/src/lib.rs:535`](../core/dr-xmp/src/lib.rs#L535), [`core/dr-xmp/src/lib.rs:733`](../core/dr-xmp/src/lib.rs#L733), [`core/dr-xmp/src/lib.rs:746`](../core/dr-xmp/src/lib.rs#L746), [`core/dr-xmp/src/read.rs:1`](../core/dr-xmp/src/read.rs#L1), [`core/dr-xmp/src/write.rs:1`](../core/dr-xmp/src/write.rs#L1), [`core/dr-xmp/tests/round_trip.rs:1`](../core/dr-xmp/tests/round_trip.rs#L1), [`ui/dr-ui/src/library.rs:1500`](../ui/dr-ui/src/library.rs#L1500), [`ui/dr-ui/src/library.rs:1563`](../ui/dr-ui/src/library.rs#L1563), [`ui/dr-ui/src/library.rs:1579`](../ui/dr-ui/src/library.rs#L1579), [`ui/dr-ui/src/library.rs:1686`](../ui/dr-ui/src/library.rs#L1686), [`ui/dr-ui/src/library_ui.rs:1285`](../ui/dr-ui/src/library_ui.rs#L1285), [`ui/dr-ui/src/library_ui.rs:301`](../ui/dr-ui/src/library_ui.rs#L301), [`ui/dr-ui/src/library_ui.rs:3411`](../ui/dr-ui/src/library_ui.rs#L3411), [`ui/dr-ui/src/library_ui.rs:3467`](../ui/dr-ui/src/library_ui.rs#L3467), [`ui/dr-ui/src/library_ui.rs:3565`](../ui/dr-ui/src/library_ui.rs#L3565), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:519`](../ui/dr-ui/src/library_ui.rs#L519), [`ui/dr-ui/src/library_ui.rs:5749`](../ui/dr-ui/src/library_ui.rs#L5749), [`ui/dr-ui/src/settings_ui.rs:506`](../ui/dr-ui/src/settings_ui.rs#L506), [`ui/dr-ui/src/xmp_sync.rs:1`](../ui/dr-ui/src/xmp_sync.rs#L1), [`ui/dr-ui/src/xmp_sync.rs:250`](../ui/dr-ui/src/xmp_sync.rs#L250), [`ui/dr-ui/src/xmp_sync.rs:287`](../ui/dr-ui/src/xmp_sync.rs#L287), [`ui/dr-ui/src/xmp_sync.rs:324`](../ui/dr-ui/src/xmp_sync.rs#L324), [`ui/dr-ui/ui/app.slint:849`](../ui/dr-ui/ui/app.slint#L849), [`ui/dr-ui/ui/settings.slint:1001`](../ui/dr-ui/ui/settings.slint#L1001), [`ui/dr-ui/ui/settings.slint:184`](../ui/dr-ui/ui/settings.slint#L184) | +| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:923`](../core/dr-catalog/src/schema.rs#L923), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:507`](../core/dr-sync-nextcloud/src/lib.rs#L507), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:618`](../core/dr-sync/src/scan.rs#L618), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`core/dr-types/src/place.rs:91`](../core/dr-types/src/place.rs#L91), [`ui/dr-ui/src/collections_ui.rs:1435`](../ui/dr-ui/src/collections_ui.rs#L1435), [`ui/dr-ui/src/collections_ui.rs:2341`](../ui/dr-ui/src/collections_ui.rs#L2341), [`ui/dr-ui/src/library.rs:183`](../ui/dr-ui/src/library.rs#L183), [`ui/dr-ui/src/library.rs:200`](../ui/dr-ui/src/library.rs#L200), [`ui/dr-ui/src/library.rs:249`](../ui/dr-ui/src/library.rs#L249), [`ui/dr-ui/src/library.rs:4845`](../ui/dr-ui/src/library.rs#L4845), [`ui/dr-ui/src/library.rs:4879`](../ui/dr-ui/src/library.rs#L4879), [`ui/dr-ui/src/library_ui.rs:238`](../ui/dr-ui/src/library_ui.rs#L238), [`ui/dr-ui/src/library_ui.rs:870`](../ui/dr-ui/src/library_ui.rs#L870), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:745`](../ui/dr-ui/ui/collections.slint#L745) | | FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:56`](../core/dr-types/src/lib.rs#L56), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:46`](../platform/dr-plat/src/storage.rs#L46) | | FR-CAT-2 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1) | -| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:105`](../core/dr-sync/src/scan.rs#L105), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488), [`ui/dr-ui/src/library.rs:3520`](../ui/dr-ui/src/library.rs#L3520), [`ui/dr-ui/src/library.rs:4278`](../ui/dr-ui/src/library.rs#L4278), [`ui/dr-ui/src/library_ui.rs:177`](../ui/dr-ui/src/library_ui.rs#L177), [`ui/dr-ui/src/library_ui.rs:4459`](../ui/dr-ui/src/library_ui.rs#L4459), [`ui/dr-ui/src/library_ui.rs:5970`](../ui/dr-ui/src/library_ui.rs#L5970), [`ui/dr-ui/ui/app.slint:383`](../ui/dr-ui/ui/app.slint#L383), [`ui/dr-ui/ui/settings.slint:420`](../ui/dr-ui/ui/settings.slint#L420), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | +| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:105`](../core/dr-sync/src/scan.rs#L105), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488), [`ui/dr-ui/src/library.rs:3520`](../ui/dr-ui/src/library.rs#L3520), [`ui/dr-ui/src/library.rs:4278`](../ui/dr-ui/src/library.rs#L4278), [`ui/dr-ui/src/library_ui.rs:177`](../ui/dr-ui/src/library_ui.rs#L177), [`ui/dr-ui/src/library_ui.rs:4499`](../ui/dr-ui/src/library_ui.rs#L4499), [`ui/dr-ui/src/library_ui.rs:6010`](../ui/dr-ui/src/library_ui.rs#L6010), [`ui/dr-ui/ui/app.slint:383`](../ui/dr-ui/ui/app.slint#L383), [`ui/dr-ui/ui/settings.slint:420`](../ui/dr-ui/ui/settings.slint#L420), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | | FR-CAT-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/schema.rs:381`](../core/dr-catalog/src/schema.rs#L381), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:230`](../ui/dr-ui/src/library.rs#L230), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) | -| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1341`](../core/dr-catalog/src/schema.rs#L1341), [`core/dr-catalog/src/schema.rs:838`](../core/dr-catalog/src/schema.rs#L838), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`ui/dr-ui/src/collections_ui.rs:1731`](../ui/dr-ui/src/collections_ui.rs#L1731), [`ui/dr-ui/src/collections_ui.rs:1837`](../ui/dr-ui/src/collections_ui.rs#L1837), [`ui/dr-ui/src/collections_ui.rs:1886`](../ui/dr-ui/src/collections_ui.rs#L1886), [`ui/dr-ui/src/collections_ui.rs:275`](../ui/dr-ui/src/collections_ui.rs#L275), [`ui/dr-ui/src/collections_ui.rs:484`](../ui/dr-ui/src/collections_ui.rs#L484), [`ui/dr-ui/src/collections_ui.rs:87`](../ui/dr-ui/src/collections_ui.rs#L87), [`ui/dr-ui/src/library.rs:4893`](../ui/dr-ui/src/library.rs#L4893), [`ui/dr-ui/src/library_ui.rs:745`](../ui/dr-ui/src/library_ui.rs#L745), [`ui/dr-ui/src/library_ui.rs:7929`](../ui/dr-ui/src/library_ui.rs#L7929), [`ui/dr-ui/src/library_ui.rs:7940`](../ui/dr-ui/src/library_ui.rs#L7940), [`ui/dr-ui/src/library_ui.rs:7953`](../ui/dr-ui/src/library_ui.rs#L7953), [`ui/dr-ui/src/library_ui.rs:7968`](../ui/dr-ui/src/library_ui.rs#L7968), [`ui/dr-ui/src/library_ui.rs:7977`](../ui/dr-ui/src/library_ui.rs#L7977), [`ui/dr-ui/ui/app.slint:304`](../ui/dr-ui/ui/app.slint#L304), [`ui/dr-ui/ui/app.slint:569`](../ui/dr-ui/ui/app.slint#L569), [`ui/dr-ui/ui/library.slint:1360`](../ui/dr-ui/ui/library.slint#L1360), [`ui/dr-ui/ui/library.slint:1384`](../ui/dr-ui/ui/library.slint#L1384), [`ui/dr-ui/ui/library.slint:1437`](../ui/dr-ui/ui/library.slint#L1437), [`ui/dr-ui/ui/library.slint:19`](../ui/dr-ui/ui/library.slint#L19), [`ui/dr-ui/ui/library.slint:4201`](../ui/dr-ui/ui/library.slint#L4201) | -| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:838`](../core/dr-catalog/src/schema.rs#L838), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:64`](../core/dr-types/src/settings.rs#L64), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:254`](../ui/dr-ui/src/library.rs#L254), [`ui/dr-ui/src/library.rs:5369`](../ui/dr-ui/src/library.rs#L5369), [`ui/dr-ui/src/library_ui.rs:391`](../ui/dr-ui/src/library_ui.rs#L391), [`ui/dr-ui/src/library_ui.rs:497`](../ui/dr-ui/src/library_ui.rs#L497), [`ui/dr-ui/src/library_ui.rs:6692`](../ui/dr-ui/src/library_ui.rs#L6692), [`ui/dr-ui/src/library_ui.rs:6745`](../ui/dr-ui/src/library_ui.rs#L6745), [`ui/dr-ui/src/library_ui.rs:8069`](../ui/dr-ui/src/library_ui.rs#L8069), [`ui/dr-ui/ui/app.slint:316`](../ui/dr-ui/ui/app.slint#L316), [`ui/dr-ui/ui/app.slint:569`](../ui/dr-ui/ui/app.slint#L569), [`ui/dr-ui/ui/app.slint:845`](../ui/dr-ui/ui/app.slint#L845), [`ui/dr-ui/ui/library.slint:111`](../ui/dr-ui/ui/library.slint#L111), [`ui/dr-ui/ui/library.slint:1547`](../ui/dr-ui/ui/library.slint#L1547), [`ui/dr-ui/ui/library.slint:4201`](../ui/dr-ui/ui/library.slint#L4201), [`ui/dr-ui/ui/settings.slint:176`](../ui/dr-ui/ui/settings.slint#L176) | -| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1117`](../ui/dr-ui/src/collections_ui.rs#L1117), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:1225`](../ui/dr-ui/src/collections_ui.rs#L1225), [`ui/dr-ui/src/collections_ui.rs:1704`](../ui/dr-ui/src/collections_ui.rs#L1704), [`ui/dr-ui/src/collections_ui.rs:1750`](../ui/dr-ui/src/collections_ui.rs#L1750), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2000`](../ui/dr-ui/src/collections_ui.rs#L2000), [`ui/dr-ui/src/collections_ui.rs:2065`](../ui/dr-ui/src/collections_ui.rs#L2065), [`ui/dr-ui/src/collections_ui.rs:2125`](../ui/dr-ui/src/collections_ui.rs#L2125), [`ui/dr-ui/src/collections_ui.rs:2701`](../ui/dr-ui/src/collections_ui.rs#L2701), [`ui/dr-ui/src/collections_ui.rs:2755`](../ui/dr-ui/src/collections_ui.rs#L2755), [`ui/dr-ui/src/collections_ui.rs:2801`](../ui/dr-ui/src/collections_ui.rs#L2801), [`ui/dr-ui/src/collections_ui.rs:2903`](../ui/dr-ui/src/collections_ui.rs#L2903), [`ui/dr-ui/src/collections_ui.rs:2959`](../ui/dr-ui/src/collections_ui.rs#L2959), [`ui/dr-ui/src/collections_ui.rs:3120`](../ui/dr-ui/src/collections_ui.rs#L3120), [`ui/dr-ui/src/collections_ui.rs:3142`](../ui/dr-ui/src/collections_ui.rs#L3142), [`ui/dr-ui/src/collections_ui.rs:3208`](../ui/dr-ui/src/collections_ui.rs#L3208), [`ui/dr-ui/src/collections_ui.rs:329`](../ui/dr-ui/src/collections_ui.rs#L329), [`ui/dr-ui/src/collections_ui.rs:3328`](../ui/dr-ui/src/collections_ui.rs#L3328), [`ui/dr-ui/src/collections_ui.rs:339`](../ui/dr-ui/src/collections_ui.rs#L339), [`ui/dr-ui/src/collections_ui.rs:362`](../ui/dr-ui/src/collections_ui.rs#L362), [`ui/dr-ui/src/collections_ui.rs:3633`](../ui/dr-ui/src/collections_ui.rs#L3633), [`ui/dr-ui/src/collections_ui.rs:383`](../ui/dr-ui/src/collections_ui.rs#L383), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:718`](../ui/dr-ui/src/collections_ui.rs#L718), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:5174`](../ui/dr-ui/src/library.rs#L5174), [`ui/dr-ui/src/library.rs:5200`](../ui/dr-ui/src/library.rs#L5200), [`ui/dr-ui/src/library.rs:5255`](../ui/dr-ui/src/library.rs#L5255), [`ui/dr-ui/src/library_ui.rs:2737`](../ui/dr-ui/src/library_ui.rs#L2737), [`ui/dr-ui/src/library_ui.rs:4321`](../ui/dr-ui/src/library_ui.rs#L4321), [`ui/dr-ui/src/library_ui.rs:5072`](../ui/dr-ui/src/library_ui.rs#L5072), [`ui/dr-ui/ui/app.slint:307`](../ui/dr-ui/ui/app.slint#L307), [`ui/dr-ui/ui/app.slint:3117`](../ui/dr-ui/ui/app.slint#L3117), [`ui/dr-ui/ui/app.slint:564`](../ui/dr-ui/ui/app.slint#L564), [`ui/dr-ui/ui/app.slint:593`](../ui/dr-ui/ui/app.slint#L593), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:587`](../ui/dr-ui/ui/collections.slint#L587), [`ui/dr-ui/ui/library.slint:1327`](../ui/dr-ui/ui/library.slint#L1327), [`ui/dr-ui/ui/library.slint:2902`](../ui/dr-ui/ui/library.slint#L2902), [`ui/dr-ui/ui/library.slint:4179`](../ui/dr-ui/ui/library.slint#L4179), [`ui/dr-ui/ui/library.slint:4257`](../ui/dr-ui/ui/library.slint#L4257), [`ui/dr-ui/ui/library.slint:998`](../ui/dr-ui/ui/library.slint#L998) | -| FR-CAT-8 | [`core/dr-catalog/src/schema.rs:557`](../core/dr-catalog/src/schema.rs#L557), [`core/dr-pipeline/src/coverage.rs:1`](../core/dr-pipeline/src/coverage.rs#L1), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/graph.rs:640`](../core/dr-pipeline/src/graph.rs#L640), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1373`](../core/dr-pipeline/src/sidecar.rs#L1373), [`core/dr-pipeline/src/sidecar.rs:2205`](../core/dr-pipeline/src/sidecar.rs#L2205), [`core/dr-pipeline/src/sidecar.rs:96`](../core/dr-pipeline/src/sidecar.rs#L96), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:56`](../core/dr-sync/src/scan.rs#L56), [`core/dr-sync/src/scan.rs:926`](../core/dr-sync/src/scan.rs#L926), [`ui/dr-ui/src/develop.rs:2155`](../ui/dr-ui/src/develop.rs#L2155), [`ui/dr-ui/src/develop.rs:5421`](../ui/dr-ui/src/develop.rs#L5421), [`ui/dr-ui/src/develop.rs:5450`](../ui/dr-ui/src/develop.rs#L5450), [`ui/dr-ui/src/develop.rs:6824`](../ui/dr-ui/src/develop.rs#L6824), [`ui/dr-ui/src/develop.rs:6878`](../ui/dr-ui/src/develop.rs#L6878), [`ui/dr-ui/src/develop.rs:6980`](../ui/dr-ui/src/develop.rs#L6980), [`ui/dr-ui/src/export.rs:799`](../ui/dr-ui/src/export.rs#L799), [`ui/dr-ui/src/lib.rs:1285`](../ui/dr-ui/src/lib.rs#L1285), [`ui/dr-ui/src/lib.rs:1806`](../ui/dr-ui/src/lib.rs#L1806), [`ui/dr-ui/src/lib.rs:2374`](../ui/dr-ui/src/lib.rs#L2374), [`ui/dr-ui/src/lib.rs:2512`](../ui/dr-ui/src/lib.rs#L2512), [`ui/dr-ui/src/lib.rs:751`](../ui/dr-ui/src/lib.rs#L751), [`ui/dr-ui/src/library.rs:1391`](../ui/dr-ui/src/library.rs#L1391), [`ui/dr-ui/src/library.rs:1426`](../ui/dr-ui/src/library.rs#L1426), [`ui/dr-ui/src/library.rs:2447`](../ui/dr-ui/src/library.rs#L2447), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:552`](../ui/dr-ui/src/library.rs#L552), [`ui/dr-ui/src/library.rs:589`](../ui/dr-ui/src/library.rs#L589), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:7599`](../ui/dr-ui/src/library.rs#L7599), [`ui/dr-ui/src/library.rs:862`](../ui/dr-ui/src/library.rs#L862), [`ui/dr-ui/src/library_ui.rs:1302`](../ui/dr-ui/src/library_ui.rs#L1302), [`ui/dr-ui/src/library_ui.rs:6293`](../ui/dr-ui/src/library_ui.rs#L6293), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | -| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:895`](../core/dr-catalog/src/schema.rs#L895), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:35`](../core/dr-sync-nextcloud/src/desktop_client.rs#L35), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:211`](../core/dr-sync/src/scan.rs#L211), [`core/dr-sync/src/scan.rs:519`](../core/dr-sync/src/scan.rs#L519), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`ui/dr-ui/src/develop.rs:4710`](../ui/dr-ui/src/develop.rs#L4710), [`ui/dr-ui/src/develop.rs:4753`](../ui/dr-ui/src/develop.rs#L4753), [`ui/dr-ui/src/library.rs:1291`](../ui/dr-ui/src/library.rs#L1291), [`ui/dr-ui/src/library.rs:1344`](../ui/dr-ui/src/library.rs#L1344), [`ui/dr-ui/src/library.rs:1375`](../ui/dr-ui/src/library.rs#L1375), [`ui/dr-ui/src/library.rs:1426`](../ui/dr-ui/src/library.rs#L1426), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:1955`](../ui/dr-ui/src/library.rs#L1955), [`ui/dr-ui/src/library.rs:2407`](../ui/dr-ui/src/library.rs#L2407), [`ui/dr-ui/src/library.rs:2483`](../ui/dr-ui/src/library.rs#L2483), [`ui/dr-ui/src/library.rs:274`](../ui/dr-ui/src/library.rs#L274), [`ui/dr-ui/src/library.rs:5477`](../ui/dr-ui/src/library.rs#L5477), [`ui/dr-ui/src/library.rs:589`](../ui/dr-ui/src/library.rs#L589), [`ui/dr-ui/src/library.rs:846`](../ui/dr-ui/src/library.rs#L846), [`ui/dr-ui/src/library.rs:862`](../ui/dr-ui/src/library.rs#L862), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:1369`](../ui/dr-ui/src/library_ui.rs#L1369), [`ui/dr-ui/src/library_ui.rs:1960`](../ui/dr-ui/src/library_ui.rs#L1960), [`ui/dr-ui/src/library_ui.rs:2004`](../ui/dr-ui/src/library_ui.rs#L2004), [`ui/dr-ui/src/library_ui.rs:2020`](../ui/dr-ui/src/library_ui.rs#L2020), [`ui/dr-ui/src/library_ui.rs:2114`](../ui/dr-ui/src/library_ui.rs#L2114), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:2977`](../ui/dr-ui/src/library_ui.rs#L2977), [`ui/dr-ui/src/library_ui.rs:320`](../ui/dr-ui/src/library_ui.rs#L320), [`ui/dr-ui/src/library_ui.rs:328`](../ui/dr-ui/src/library_ui.rs#L328), [`ui/dr-ui/src/library_ui.rs:3600`](../ui/dr-ui/src/library_ui.rs#L3600), [`ui/dr-ui/src/library_ui.rs:3822`](../ui/dr-ui/src/library_ui.rs#L3822), [`ui/dr-ui/src/library_ui.rs:4100`](../ui/dr-ui/src/library_ui.rs#L4100), [`ui/dr-ui/src/library_ui.rs:4188`](../ui/dr-ui/src/library_ui.rs#L4188), [`ui/dr-ui/src/library_ui.rs:4372`](../ui/dr-ui/src/library_ui.rs#L4372), [`ui/dr-ui/src/library_ui.rs:4486`](../ui/dr-ui/src/library_ui.rs#L4486), [`ui/dr-ui/src/library_ui.rs:555`](../ui/dr-ui/src/library_ui.rs#L555), [`ui/dr-ui/src/library_ui.rs:618`](../ui/dr-ui/src/library_ui.rs#L618), [`ui/dr-ui/src/library_ui.rs:6790`](../ui/dr-ui/src/library_ui.rs#L6790), [`ui/dr-ui/src/library_ui.rs:6872`](../ui/dr-ui/src/library_ui.rs#L6872), [`ui/dr-ui/src/presets.rs:486`](../ui/dr-ui/src/presets.rs#L486), [`ui/dr-ui/src/presets.rs:498`](../ui/dr-ui/src/presets.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | +| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1341`](../core/dr-catalog/src/schema.rs#L1341), [`core/dr-catalog/src/schema.rs:838`](../core/dr-catalog/src/schema.rs#L838), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`ui/dr-ui/src/collections_ui.rs:1731`](../ui/dr-ui/src/collections_ui.rs#L1731), [`ui/dr-ui/src/collections_ui.rs:1837`](../ui/dr-ui/src/collections_ui.rs#L1837), [`ui/dr-ui/src/collections_ui.rs:1886`](../ui/dr-ui/src/collections_ui.rs#L1886), [`ui/dr-ui/src/collections_ui.rs:275`](../ui/dr-ui/src/collections_ui.rs#L275), [`ui/dr-ui/src/collections_ui.rs:484`](../ui/dr-ui/src/collections_ui.rs#L484), [`ui/dr-ui/src/collections_ui.rs:87`](../ui/dr-ui/src/collections_ui.rs#L87), [`ui/dr-ui/src/library.rs:4893`](../ui/dr-ui/src/library.rs#L4893), [`ui/dr-ui/src/library_ui.rs:745`](../ui/dr-ui/src/library_ui.rs#L745), [`ui/dr-ui/src/library_ui.rs:7969`](../ui/dr-ui/src/library_ui.rs#L7969), [`ui/dr-ui/src/library_ui.rs:7980`](../ui/dr-ui/src/library_ui.rs#L7980), [`ui/dr-ui/src/library_ui.rs:7993`](../ui/dr-ui/src/library_ui.rs#L7993), [`ui/dr-ui/src/library_ui.rs:8008`](../ui/dr-ui/src/library_ui.rs#L8008), [`ui/dr-ui/src/library_ui.rs:8017`](../ui/dr-ui/src/library_ui.rs#L8017), [`ui/dr-ui/ui/app.slint:304`](../ui/dr-ui/ui/app.slint#L304), [`ui/dr-ui/ui/app.slint:569`](../ui/dr-ui/ui/app.slint#L569), [`ui/dr-ui/ui/library.slint:1360`](../ui/dr-ui/ui/library.slint#L1360), [`ui/dr-ui/ui/library.slint:1384`](../ui/dr-ui/ui/library.slint#L1384), [`ui/dr-ui/ui/library.slint:1437`](../ui/dr-ui/ui/library.slint#L1437), [`ui/dr-ui/ui/library.slint:19`](../ui/dr-ui/ui/library.slint#L19), [`ui/dr-ui/ui/library.slint:4201`](../ui/dr-ui/ui/library.slint#L4201) | +| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:838`](../core/dr-catalog/src/schema.rs#L838), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:64`](../core/dr-types/src/settings.rs#L64), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:254`](../ui/dr-ui/src/library.rs#L254), [`ui/dr-ui/src/library.rs:5369`](../ui/dr-ui/src/library.rs#L5369), [`ui/dr-ui/src/library_ui.rs:391`](../ui/dr-ui/src/library_ui.rs#L391), [`ui/dr-ui/src/library_ui.rs:497`](../ui/dr-ui/src/library_ui.rs#L497), [`ui/dr-ui/src/library_ui.rs:6732`](../ui/dr-ui/src/library_ui.rs#L6732), [`ui/dr-ui/src/library_ui.rs:6785`](../ui/dr-ui/src/library_ui.rs#L6785), [`ui/dr-ui/src/library_ui.rs:8109`](../ui/dr-ui/src/library_ui.rs#L8109), [`ui/dr-ui/ui/app.slint:316`](../ui/dr-ui/ui/app.slint#L316), [`ui/dr-ui/ui/app.slint:569`](../ui/dr-ui/ui/app.slint#L569), [`ui/dr-ui/ui/app.slint:845`](../ui/dr-ui/ui/app.slint#L845), [`ui/dr-ui/ui/library.slint:111`](../ui/dr-ui/ui/library.slint#L111), [`ui/dr-ui/ui/library.slint:1547`](../ui/dr-ui/ui/library.slint#L1547), [`ui/dr-ui/ui/library.slint:4201`](../ui/dr-ui/ui/library.slint#L4201), [`ui/dr-ui/ui/settings.slint:176`](../ui/dr-ui/ui/settings.slint#L176) | +| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1117`](../ui/dr-ui/src/collections_ui.rs#L1117), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:1225`](../ui/dr-ui/src/collections_ui.rs#L1225), [`ui/dr-ui/src/collections_ui.rs:1704`](../ui/dr-ui/src/collections_ui.rs#L1704), [`ui/dr-ui/src/collections_ui.rs:1750`](../ui/dr-ui/src/collections_ui.rs#L1750), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2000`](../ui/dr-ui/src/collections_ui.rs#L2000), [`ui/dr-ui/src/collections_ui.rs:2065`](../ui/dr-ui/src/collections_ui.rs#L2065), [`ui/dr-ui/src/collections_ui.rs:2125`](../ui/dr-ui/src/collections_ui.rs#L2125), [`ui/dr-ui/src/collections_ui.rs:2701`](../ui/dr-ui/src/collections_ui.rs#L2701), [`ui/dr-ui/src/collections_ui.rs:2755`](../ui/dr-ui/src/collections_ui.rs#L2755), [`ui/dr-ui/src/collections_ui.rs:2801`](../ui/dr-ui/src/collections_ui.rs#L2801), [`ui/dr-ui/src/collections_ui.rs:2903`](../ui/dr-ui/src/collections_ui.rs#L2903), [`ui/dr-ui/src/collections_ui.rs:2959`](../ui/dr-ui/src/collections_ui.rs#L2959), [`ui/dr-ui/src/collections_ui.rs:3120`](../ui/dr-ui/src/collections_ui.rs#L3120), [`ui/dr-ui/src/collections_ui.rs:3142`](../ui/dr-ui/src/collections_ui.rs#L3142), [`ui/dr-ui/src/collections_ui.rs:3208`](../ui/dr-ui/src/collections_ui.rs#L3208), [`ui/dr-ui/src/collections_ui.rs:329`](../ui/dr-ui/src/collections_ui.rs#L329), [`ui/dr-ui/src/collections_ui.rs:3328`](../ui/dr-ui/src/collections_ui.rs#L3328), [`ui/dr-ui/src/collections_ui.rs:339`](../ui/dr-ui/src/collections_ui.rs#L339), [`ui/dr-ui/src/collections_ui.rs:362`](../ui/dr-ui/src/collections_ui.rs#L362), [`ui/dr-ui/src/collections_ui.rs:3633`](../ui/dr-ui/src/collections_ui.rs#L3633), [`ui/dr-ui/src/collections_ui.rs:383`](../ui/dr-ui/src/collections_ui.rs#L383), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:718`](../ui/dr-ui/src/collections_ui.rs#L718), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:5174`](../ui/dr-ui/src/library.rs#L5174), [`ui/dr-ui/src/library.rs:5200`](../ui/dr-ui/src/library.rs#L5200), [`ui/dr-ui/src/library.rs:5255`](../ui/dr-ui/src/library.rs#L5255), [`ui/dr-ui/src/library_ui.rs:2737`](../ui/dr-ui/src/library_ui.rs#L2737), [`ui/dr-ui/src/library_ui.rs:4353`](../ui/dr-ui/src/library_ui.rs#L4353), [`ui/dr-ui/src/library_ui.rs:5112`](../ui/dr-ui/src/library_ui.rs#L5112), [`ui/dr-ui/ui/app.slint:307`](../ui/dr-ui/ui/app.slint#L307), [`ui/dr-ui/ui/app.slint:3117`](../ui/dr-ui/ui/app.slint#L3117), [`ui/dr-ui/ui/app.slint:564`](../ui/dr-ui/ui/app.slint#L564), [`ui/dr-ui/ui/app.slint:593`](../ui/dr-ui/ui/app.slint#L593), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:587`](../ui/dr-ui/ui/collections.slint#L587), [`ui/dr-ui/ui/library.slint:1327`](../ui/dr-ui/ui/library.slint#L1327), [`ui/dr-ui/ui/library.slint:2902`](../ui/dr-ui/ui/library.slint#L2902), [`ui/dr-ui/ui/library.slint:4179`](../ui/dr-ui/ui/library.slint#L4179), [`ui/dr-ui/ui/library.slint:4257`](../ui/dr-ui/ui/library.slint#L4257), [`ui/dr-ui/ui/library.slint:998`](../ui/dr-ui/ui/library.slint#L998) | +| FR-CAT-8 | [`core/dr-catalog/src/schema.rs:557`](../core/dr-catalog/src/schema.rs#L557), [`core/dr-pipeline/src/coverage.rs:1`](../core/dr-pipeline/src/coverage.rs#L1), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/graph.rs:640`](../core/dr-pipeline/src/graph.rs#L640), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1373`](../core/dr-pipeline/src/sidecar.rs#L1373), [`core/dr-pipeline/src/sidecar.rs:2205`](../core/dr-pipeline/src/sidecar.rs#L2205), [`core/dr-pipeline/src/sidecar.rs:96`](../core/dr-pipeline/src/sidecar.rs#L96), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:56`](../core/dr-sync/src/scan.rs#L56), [`core/dr-sync/src/scan.rs:926`](../core/dr-sync/src/scan.rs#L926), [`ui/dr-ui/src/develop.rs:2155`](../ui/dr-ui/src/develop.rs#L2155), [`ui/dr-ui/src/develop.rs:5421`](../ui/dr-ui/src/develop.rs#L5421), [`ui/dr-ui/src/develop.rs:5450`](../ui/dr-ui/src/develop.rs#L5450), [`ui/dr-ui/src/develop.rs:6824`](../ui/dr-ui/src/develop.rs#L6824), [`ui/dr-ui/src/develop.rs:6878`](../ui/dr-ui/src/develop.rs#L6878), [`ui/dr-ui/src/develop.rs:6980`](../ui/dr-ui/src/develop.rs#L6980), [`ui/dr-ui/src/export.rs:799`](../ui/dr-ui/src/export.rs#L799), [`ui/dr-ui/src/lib.rs:1285`](../ui/dr-ui/src/lib.rs#L1285), [`ui/dr-ui/src/lib.rs:1806`](../ui/dr-ui/src/lib.rs#L1806), [`ui/dr-ui/src/lib.rs:2374`](../ui/dr-ui/src/lib.rs#L2374), [`ui/dr-ui/src/lib.rs:2512`](../ui/dr-ui/src/lib.rs#L2512), [`ui/dr-ui/src/lib.rs:751`](../ui/dr-ui/src/lib.rs#L751), [`ui/dr-ui/src/library.rs:1391`](../ui/dr-ui/src/library.rs#L1391), [`ui/dr-ui/src/library.rs:1426`](../ui/dr-ui/src/library.rs#L1426), [`ui/dr-ui/src/library.rs:2447`](../ui/dr-ui/src/library.rs#L2447), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:552`](../ui/dr-ui/src/library.rs#L552), [`ui/dr-ui/src/library.rs:589`](../ui/dr-ui/src/library.rs#L589), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:7599`](../ui/dr-ui/src/library.rs#L7599), [`ui/dr-ui/src/library.rs:862`](../ui/dr-ui/src/library.rs#L862), [`ui/dr-ui/src/library_ui.rs:1302`](../ui/dr-ui/src/library_ui.rs#L1302), [`ui/dr-ui/src/library_ui.rs:6333`](../ui/dr-ui/src/library_ui.rs#L6333), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:895`](../core/dr-catalog/src/schema.rs#L895), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:35`](../core/dr-sync-nextcloud/src/desktop_client.rs#L35), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:211`](../core/dr-sync/src/scan.rs#L211), [`core/dr-sync/src/scan.rs:519`](../core/dr-sync/src/scan.rs#L519), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`ui/dr-ui/src/develop.rs:4710`](../ui/dr-ui/src/develop.rs#L4710), [`ui/dr-ui/src/develop.rs:4753`](../ui/dr-ui/src/develop.rs#L4753), [`ui/dr-ui/src/library.rs:1291`](../ui/dr-ui/src/library.rs#L1291), [`ui/dr-ui/src/library.rs:1344`](../ui/dr-ui/src/library.rs#L1344), [`ui/dr-ui/src/library.rs:1375`](../ui/dr-ui/src/library.rs#L1375), [`ui/dr-ui/src/library.rs:1426`](../ui/dr-ui/src/library.rs#L1426), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:1955`](../ui/dr-ui/src/library.rs#L1955), [`ui/dr-ui/src/library.rs:2407`](../ui/dr-ui/src/library.rs#L2407), [`ui/dr-ui/src/library.rs:2483`](../ui/dr-ui/src/library.rs#L2483), [`ui/dr-ui/src/library.rs:274`](../ui/dr-ui/src/library.rs#L274), [`ui/dr-ui/src/library.rs:5477`](../ui/dr-ui/src/library.rs#L5477), [`ui/dr-ui/src/library.rs:589`](../ui/dr-ui/src/library.rs#L589), [`ui/dr-ui/src/library.rs:846`](../ui/dr-ui/src/library.rs#L846), [`ui/dr-ui/src/library.rs:862`](../ui/dr-ui/src/library.rs#L862), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:1369`](../ui/dr-ui/src/library_ui.rs#L1369), [`ui/dr-ui/src/library_ui.rs:1960`](../ui/dr-ui/src/library_ui.rs#L1960), [`ui/dr-ui/src/library_ui.rs:2004`](../ui/dr-ui/src/library_ui.rs#L2004), [`ui/dr-ui/src/library_ui.rs:2020`](../ui/dr-ui/src/library_ui.rs#L2020), [`ui/dr-ui/src/library_ui.rs:2114`](../ui/dr-ui/src/library_ui.rs#L2114), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:2977`](../ui/dr-ui/src/library_ui.rs#L2977), [`ui/dr-ui/src/library_ui.rs:320`](../ui/dr-ui/src/library_ui.rs#L320), [`ui/dr-ui/src/library_ui.rs:328`](../ui/dr-ui/src/library_ui.rs#L328), [`ui/dr-ui/src/library_ui.rs:3600`](../ui/dr-ui/src/library_ui.rs#L3600), [`ui/dr-ui/src/library_ui.rs:3822`](../ui/dr-ui/src/library_ui.rs#L3822), [`ui/dr-ui/src/library_ui.rs:4100`](../ui/dr-ui/src/library_ui.rs#L4100), [`ui/dr-ui/src/library_ui.rs:4216`](../ui/dr-ui/src/library_ui.rs#L4216), [`ui/dr-ui/src/library_ui.rs:4404`](../ui/dr-ui/src/library_ui.rs#L4404), [`ui/dr-ui/src/library_ui.rs:4526`](../ui/dr-ui/src/library_ui.rs#L4526), [`ui/dr-ui/src/library_ui.rs:555`](../ui/dr-ui/src/library_ui.rs#L555), [`ui/dr-ui/src/library_ui.rs:618`](../ui/dr-ui/src/library_ui.rs#L618), [`ui/dr-ui/src/library_ui.rs:6830`](../ui/dr-ui/src/library_ui.rs#L6830), [`ui/dr-ui/src/library_ui.rs:6912`](../ui/dr-ui/src/library_ui.rs#L6912), [`ui/dr-ui/src/presets.rs:486`](../ui/dr-ui/src/presets.rs#L486), [`ui/dr-ui/src/presets.rs:498`](../ui/dr-ui/src/presets.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | | FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | | FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:420`](../core/dr-catalog/src/schema.rs#L420), [`core/dr-catalog/src/schema.rs:598`](../core/dr-catalog/src/schema.rs#L598), [`core/dr-catalog/src/schema.rs:724`](../core/dr-catalog/src/schema.rs#L724), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`core/dr-types/src/settings.rs:129`](../core/dr-types/src/settings.rs#L129), [`ui/dr-ui/src/develop.rs:121`](../ui/dr-ui/src/develop.rs#L121), [`ui/dr-ui/src/develop.rs:130`](../ui/dr-ui/src/develop.rs#L130), [`ui/dr-ui/src/develop.rs:196`](../ui/dr-ui/src/develop.rs#L196), [`ui/dr-ui/src/develop.rs:2416`](../ui/dr-ui/src/develop.rs#L2416), [`ui/dr-ui/src/develop.rs:715`](../ui/dr-ui/src/develop.rs#L715), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2484`](../ui/dr-ui/src/lib.rs#L2484), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:724`](../core/dr-catalog/src/schema.rs#L724), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/library.rs:295`](../ui/dr-ui/src/library.rs#L295), [`ui/dr-ui/src/library.rs:325`](../ui/dr-ui/src/library.rs#L325), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | @@ -56,7 +56,7 @@ _None._ | FR-CULL-3 | [`core/dr-gpu/src/focus.rs:154`](../core/dr-gpu/src/focus.rs#L154), [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/raw_histogram.rs:129`](../core/dr-gpu/src/raw_histogram.rs#L129), [`core/dr-gpu/src/raw_histogram.rs:1`](../core/dr-gpu/src/raw_histogram.rs#L1), [`core/dr-gpu/src/raw_histogram.rs:272`](../core/dr-gpu/src/raw_histogram.rs#L272), [`core/dr-gpu/src/raw_histogram.rs:407`](../core/dr-gpu/src/raw_histogram.rs#L407), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`core/dr-gpu/src/shaders/raw_histogram.wgsl:1`](../core/dr-gpu/src/shaders/raw_histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:4137`](../ui/dr-ui/src/develop.rs#L4137), [`ui/dr-ui/src/develop.rs:4150`](../ui/dr-ui/src/develop.rs#L4150), [`ui/dr-ui/src/develop.rs:4195`](../ui/dr-ui/src/develop.rs#L4195), [`ui/dr-ui/src/develop.rs:4206`](../ui/dr-ui/src/develop.rs#L4206), [`ui/dr-ui/src/develop.rs:4212`](../ui/dr-ui/src/develop.rs#L4212), [`ui/dr-ui/src/develop.rs:4229`](../ui/dr-ui/src/develop.rs#L4229), [`ui/dr-ui/src/develop.rs:810`](../ui/dr-ui/src/develop.rs#L810), [`ui/dr-ui/src/develop.rs:830`](../ui/dr-ui/src/develop.rs#L830), [`ui/dr-ui/src/develop.rs:836`](../ui/dr-ui/src/develop.rs#L836), [`ui/dr-ui/src/develop.rs:8734`](../ui/dr-ui/src/develop.rs#L8734), [`ui/dr-ui/src/develop.rs:8798`](../ui/dr-ui/src/develop.rs#L8798), [`ui/dr-ui/src/develop.rs:8821`](../ui/dr-ui/src/develop.rs#L8821), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/histogram.rs:208`](../ui/dr-ui/src/histogram.rs#L208), [`ui/dr-ui/src/histogram.rs:228`](../ui/dr-ui/src/histogram.rs#L228), [`ui/dr-ui/src/histogram.rs:272`](../ui/dr-ui/src/histogram.rs#L272), [`ui/dr-ui/src/histogram.rs:544`](../ui/dr-ui/src/histogram.rs#L544), [`ui/dr-ui/src/histogram.rs:565`](../ui/dr-ui/src/histogram.rs#L565), [`ui/dr-ui/src/histogram.rs:593`](../ui/dr-ui/src/histogram.rs#L593), [`ui/dr-ui/src/histogram.rs:621`](../ui/dr-ui/src/histogram.rs#L621), [`ui/dr-ui/src/histogram.rs:658`](../ui/dr-ui/src/histogram.rs#L658), [`ui/dr-ui/src/lib.rs:1850`](../ui/dr-ui/src/lib.rs#L1850), [`ui/dr-ui/src/lib.rs:1958`](../ui/dr-ui/src/lib.rs#L1958), [`ui/dr-ui/src/lib.rs:2071`](../ui/dr-ui/src/lib.rs#L2071), [`ui/dr-ui/src/lib.rs:2112`](../ui/dr-ui/src/lib.rs#L2112), [`ui/dr-ui/src/lib.rs:3595`](../ui/dr-ui/src/lib.rs#L3595), [`ui/dr-ui/src/lib.rs:436`](../ui/dr-ui/src/lib.rs#L436), [`ui/dr-ui/src/peaking.rs:1`](../ui/dr-ui/src/peaking.rs#L1), [`ui/dr-ui/ui/app.slint:1932`](../ui/dr-ui/ui/app.slint#L1932), [`ui/dr-ui/ui/app.slint:2939`](../ui/dr-ui/ui/app.slint#L2939), [`ui/dr-ui/ui/app.slint:87`](../ui/dr-ui/ui/app.slint#L87), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1), [`ui/dr-ui/ui/peaking.slint:25`](../ui/dr-ui/ui/peaking.slint#L25), [`ui/dr-ui/ui/peaking.slint:56`](../ui/dr-ui/ui/peaking.slint#L56), [`ui/dr-ui/ui/peaking.slint:91`](../ui/dr-ui/ui/peaking.slint#L91) | | FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-xmp/src/lib.rs:442`](../core/dr-xmp/src/lib.rs#L442), [`ui/dr-ui/src/library.rs:254`](../ui/dr-ui/src/library.rs#L254), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500) | | FR-CULL-5 | [`core/dr-catalog/src/bursts.rs:1`](../core/dr-catalog/src/bursts.rs#L1), [`core/dr-catalog/src/schema.rs:463`](../core/dr-catalog/src/schema.rs#L463), [`ui/dr-ui/src/bursts.rs:1`](../ui/dr-ui/src/bursts.rs#L1), [`ui/dr-ui/src/library.rs:210`](../ui/dr-ui/src/library.rs#L210), [`ui/dr-ui/src/library.rs:6258`](../ui/dr-ui/src/library.rs#L6258) | -| FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:531`](../core/dr-catalog/src/schema.rs#L531), [`core/dr-catalog/src/schema.rs:683`](../core/dr-catalog/src/schema.rs#L683), [`core/dr-catalog/src/schema.rs:724`](../core/dr-catalog/src/schema.rs#L724), [`core/dr-face/src/align.rs:291`](../core/dr-face/src/align.rs#L291), [`core/dr-types/src/settings.rs:176`](../core/dr-types/src/settings.rs#L176), [`ui/dr-ui/examples/face_detectors.rs:1`](../ui/dr-ui/examples/face_detectors.rs#L1), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/derived_sync.rs:116`](../ui/dr-ui/src/derived_sync.rs#L116), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/faces.rs:331`](../ui/dr-ui/src/faces.rs#L331), [`ui/dr-ui/src/faces.rs:347`](../ui/dr-ui/src/faces.rs#L347), [`ui/dr-ui/src/faces.rs:454`](../ui/dr-ui/src/faces.rs#L454), [`ui/dr-ui/src/identity_ui.rs:548`](../ui/dr-ui/src/identity_ui.rs#L548), [`ui/dr-ui/src/lib.rs:1460`](../ui/dr-ui/src/lib.rs#L1460), [`ui/dr-ui/src/lib.rs:3770`](../ui/dr-ui/src/lib.rs#L3770), [`ui/dr-ui/src/library.rs:3521`](../ui/dr-ui/src/library.rs#L3521), [`ui/dr-ui/src/library.rs:3679`](../ui/dr-ui/src/library.rs#L3679), [`ui/dr-ui/src/library.rs:3926`](../ui/dr-ui/src/library.rs#L3926), [`ui/dr-ui/src/library.rs:4092`](../ui/dr-ui/src/library.rs#L4092), [`ui/dr-ui/src/library.rs:4128`](../ui/dr-ui/src/library.rs#L4128), [`ui/dr-ui/src/library.rs:4208`](../ui/dr-ui/src/library.rs#L4208), [`ui/dr-ui/src/library_ui.rs:401`](../ui/dr-ui/src/library_ui.rs#L401), [`ui/dr-ui/src/library_ui.rs:485`](../ui/dr-ui/src/library_ui.rs#L485), [`ui/dr-ui/src/settings_ui.rs:192`](../ui/dr-ui/src/settings_ui.rs#L192), [`ui/dr-ui/src/settings_ui.rs:518`](../ui/dr-ui/src/settings_ui.rs#L518), [`ui/dr-ui/ui/app.slint:895`](../ui/dr-ui/ui/app.slint#L895), [`ui/dr-ui/ui/settings.slint:452`](../ui/dr-ui/ui/settings.slint#L452), [`ui/dr-ui/ui/settings.slint:82`](../ui/dr-ui/ui/settings.slint#L82), [`ui/dr-ui/ui/settings.slint:94`](../ui/dr-ui/ui/settings.slint#L94) | +| FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:531`](../core/dr-catalog/src/schema.rs#L531), [`core/dr-catalog/src/schema.rs:683`](../core/dr-catalog/src/schema.rs#L683), [`core/dr-catalog/src/schema.rs:724`](../core/dr-catalog/src/schema.rs#L724), [`core/dr-face/src/align.rs:291`](../core/dr-face/src/align.rs#L291), [`core/dr-types/src/settings.rs:176`](../core/dr-types/src/settings.rs#L176), [`ui/dr-ui/examples/face_detectors.rs:1`](../ui/dr-ui/examples/face_detectors.rs#L1), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/derived_sync.rs:124`](../ui/dr-ui/src/derived_sync.rs#L124), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/faces.rs:331`](../ui/dr-ui/src/faces.rs#L331), [`ui/dr-ui/src/faces.rs:347`](../ui/dr-ui/src/faces.rs#L347), [`ui/dr-ui/src/faces.rs:454`](../ui/dr-ui/src/faces.rs#L454), [`ui/dr-ui/src/identity_ui.rs:548`](../ui/dr-ui/src/identity_ui.rs#L548), [`ui/dr-ui/src/lib.rs:1460`](../ui/dr-ui/src/lib.rs#L1460), [`ui/dr-ui/src/lib.rs:3770`](../ui/dr-ui/src/lib.rs#L3770), [`ui/dr-ui/src/library.rs:3521`](../ui/dr-ui/src/library.rs#L3521), [`ui/dr-ui/src/library.rs:3679`](../ui/dr-ui/src/library.rs#L3679), [`ui/dr-ui/src/library.rs:3926`](../ui/dr-ui/src/library.rs#L3926), [`ui/dr-ui/src/library.rs:4092`](../ui/dr-ui/src/library.rs#L4092), [`ui/dr-ui/src/library.rs:4128`](../ui/dr-ui/src/library.rs#L4128), [`ui/dr-ui/src/library.rs:4208`](../ui/dr-ui/src/library.rs#L4208), [`ui/dr-ui/src/library_ui.rs:401`](../ui/dr-ui/src/library_ui.rs#L401), [`ui/dr-ui/src/library_ui.rs:485`](../ui/dr-ui/src/library_ui.rs#L485), [`ui/dr-ui/src/settings_ui.rs:192`](../ui/dr-ui/src/settings_ui.rs#L192), [`ui/dr-ui/src/settings_ui.rs:518`](../ui/dr-ui/src/settings_ui.rs#L518), [`ui/dr-ui/ui/app.slint:895`](../ui/dr-ui/ui/app.slint#L895), [`ui/dr-ui/ui/settings.slint:452`](../ui/dr-ui/ui/settings.slint#L452), [`ui/dr-ui/ui/settings.slint:82`](../ui/dr-ui/ui/settings.slint#L82), [`ui/dr-ui/ui/settings.slint:94`](../ui/dr-ui/ui/settings.slint#L94) | | FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:598`](../core/dr-catalog/src/schema.rs#L598), [`core/dr-catalog/src/schema.rs:724`](../core/dr-catalog/src/schema.rs#L724), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`core/dr-types/src/settings.rs:129`](../core/dr-types/src/settings.rs#L129), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/faces.rs:454`](../ui/dr-ui/src/faces.rs#L454), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/library.rs:3625`](../ui/dr-ui/src/library.rs#L3625), [`ui/dr-ui/src/library.rs:4208`](../ui/dr-ui/src/library.rs#L4208), [`ui/dr-ui/ui/identity.slint:314`](../ui/dr-ui/ui/identity.slint#L314) | | FR-DEV-1 | [`core/dr-pipeline/src/graph.rs:1`](../core/dr-pipeline/src/graph.rs#L1), [`core/dr-pipeline/src/sidecar.rs:1`](../core/dr-pipeline/src/sidecar.rs#L1) | | FR-DEV-10 | [`core/dr-gpu/src/mask.rs:1027`](../core/dr-gpu/src/mask.rs#L1027), [`core/dr-gpu/src/mask.rs:1251`](../core/dr-gpu/src/mask.rs#L1251), [`core/dr-gpu/src/mask.rs:1509`](../core/dr-gpu/src/mask.rs#L1509), [`core/dr-gpu/src/mask.rs:421`](../core/dr-gpu/src/mask.rs#L421), [`core/dr-gpu/src/mask.rs:463`](../core/dr-gpu/src/mask.rs#L463), [`core/dr-gpu/src/mask.rs:58`](../core/dr-gpu/src/mask.rs#L58), [`core/dr-gpu/src/mask.rs:60`](../core/dr-gpu/src/mask.rs#L60), [`core/dr-gpu/src/mask.rs:834`](../core/dr-gpu/src/mask.rs#L834), [`core/dr-gpu/src/mask.rs:85`](../core/dr-gpu/src/mask.rs#L85), [`core/dr-gpu/src/mask.rs:913`](../core/dr-gpu/src/mask.rs#L913), [`core/dr-gpu/src/shaders/mask.wgsl:117`](../core/dr-gpu/src/shaders/mask.wgsl#L117), [`core/dr-gpu/src/shaders/mask.wgsl:59`](../core/dr-gpu/src/shaders/mask.wgsl#L59), [`core/dr-gpu/tests/range_masks.rs:119`](../core/dr-gpu/tests/range_masks.rs#L119), [`core/dr-gpu/tests/range_masks.rs:147`](../core/dr-gpu/tests/range_masks.rs#L147), [`core/dr-gpu/tests/range_masks.rs:175`](../core/dr-gpu/tests/range_masks.rs#L175), [`core/dr-gpu/tests/range_masks.rs:1`](../core/dr-gpu/tests/range_masks.rs#L1), [`core/dr-gpu/tests/range_masks.rs:94`](../core/dr-gpu/tests/range_masks.rs#L94), [`core/dr-pipeline/src/mask.rs:125`](../core/dr-pipeline/src/mask.rs#L125), [`core/dr-pipeline/src/mask.rs:2839`](../core/dr-pipeline/src/mask.rs#L2839), [`core/dr-pipeline/src/mask.rs:2852`](../core/dr-pipeline/src/mask.rs#L2852), [`core/dr-pipeline/src/mask.rs:2871`](../core/dr-pipeline/src/mask.rs#L2871), [`core/dr-pipeline/src/mask.rs:2885`](../core/dr-pipeline/src/mask.rs#L2885), [`core/dr-pipeline/src/mask.rs:2895`](../core/dr-pipeline/src/mask.rs#L2895), [`core/dr-pipeline/src/mask.rs:709`](../core/dr-pipeline/src/mask.rs#L709), [`core/dr-pipeline/src/mask.rs:748`](../core/dr-pipeline/src/mask.rs#L748), [`core/dr-pipeline/src/mask.rs:808`](../core/dr-pipeline/src/mask.rs#L808), [`core/dr-pipeline/src/mask.rs:827`](../core/dr-pipeline/src/mask.rs#L827), [`core/dr-pipeline/src/mask.rs:851`](../core/dr-pipeline/src/mask.rs#L851), [`core/dr-pipeline/src/mask.rs:863`](../core/dr-pipeline/src/mask.rs#L863), [`core/dr-pipeline/src/mask.rs:875`](../core/dr-pipeline/src/mask.rs#L875), [`core/dr-pipeline/src/sidecar.rs:1309`](../core/dr-pipeline/src/sidecar.rs#L1309), [`core/dr-pipeline/src/sidecar.rs:1488`](../core/dr-pipeline/src/sidecar.rs#L1488), [`core/dr-pipeline/src/sidecar.rs:1577`](../core/dr-pipeline/src/sidecar.rs#L1577), [`core/dr-pipeline/src/sidecar.rs:1681`](../core/dr-pipeline/src/sidecar.rs#L1681), [`core/dr-pipeline/src/sidecar.rs:1889`](../core/dr-pipeline/src/sidecar.rs#L1889), [`core/dr-pipeline/tests/mask_sidecar.rs:1157`](../core/dr-pipeline/tests/mask_sidecar.rs#L1157), [`core/dr-pipeline/tests/mask_sidecar.rs:1186`](../core/dr-pipeline/tests/mask_sidecar.rs#L1186), [`core/dr-pipeline/tests/mask_sidecar.rs:1204`](../core/dr-pipeline/tests/mask_sidecar.rs#L1204), [`core/dr-pipeline/tests/mask_sidecar.rs:1234`](../core/dr-pipeline/tests/mask_sidecar.rs#L1234), [`ui/dr-ui/src/develop.rs:2318`](../ui/dr-ui/src/develop.rs#L2318), [`ui/dr-ui/src/develop.rs:3657`](../ui/dr-ui/src/develop.rs#L3657), [`ui/dr-ui/src/develop.rs:3684`](../ui/dr-ui/src/develop.rs#L3684), [`ui/dr-ui/src/develop.rs:3716`](../ui/dr-ui/src/develop.rs#L3716), [`ui/dr-ui/src/develop.rs:3740`](../ui/dr-ui/src/develop.rs#L3740), [`ui/dr-ui/src/develop.rs:3746`](../ui/dr-ui/src/develop.rs#L3746), [`ui/dr-ui/src/develop.rs:3753`](../ui/dr-ui/src/develop.rs#L3753), [`ui/dr-ui/src/develop.rs:3773`](../ui/dr-ui/src/develop.rs#L3773), [`ui/dr-ui/src/labels.rs:63`](../ui/dr-ui/src/labels.rs#L63), [`ui/dr-ui/src/masks_ui.rs:1150`](../ui/dr-ui/src/masks_ui.rs#L1150), [`ui/dr-ui/src/masks_ui.rs:748`](../ui/dr-ui/src/masks_ui.rs#L748) | @@ -92,27 +92,27 @@ _None._ | FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:601`](../ui/dr-ui/src/lib.rs#L601), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:781`](../ui/dr-ui/src/settings_ui.rs#L781) | -| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:992`](../ui/dr-ui/src/export.rs#L992), [`ui/dr-ui/src/lib.rs:262`](../ui/dr-ui/src/lib.rs#L262), [`ui/dr-ui/src/lib.rs:2721`](../ui/dr-ui/src/lib.rs#L2721), [`ui/dr-ui/src/lib.rs:601`](../ui/dr-ui/src/lib.rs#L601), [`ui/dr-ui/src/lib.rs:647`](../ui/dr-ui/src/lib.rs#L647), [`ui/dr-ui/src/lib.rs:674`](../ui/dr-ui/src/lib.rs#L674), [`ui/dr-ui/src/library_ui.rs:4205`](../ui/dr-ui/src/library_ui.rs#L4205), [`ui/dr-ui/src/library_ui.rs:677`](../ui/dr-ui/src/library_ui.rs#L677), [`ui/dr-ui/src/library_ui.rs:777`](../ui/dr-ui/src/library_ui.rs#L777), [`ui/dr-ui/src/library_ui.rs:7911`](../ui/dr-ui/src/library_ui.rs#L7911), [`ui/dr-ui/src/library_ui.rs:7988`](../ui/dr-ui/src/library_ui.rs#L7988), [`ui/dr-ui/src/library_ui.rs:8000`](../ui/dr-ui/src/library_ui.rs#L8000), [`ui/dr-ui/src/library_ui.rs:834`](../ui/dr-ui/src/library_ui.rs#L834), [`ui/dr-ui/ui/app.slint:1088`](../ui/dr-ui/ui/app.slint#L1088), [`ui/dr-ui/ui/app.slint:1624`](../ui/dr-ui/ui/app.slint#L1624), [`ui/dr-ui/ui/library.slint:1583`](../ui/dr-ui/ui/library.slint#L1583), [`ui/dr-ui/ui/library.slint:4278`](../ui/dr-ui/ui/library.slint#L4278) | +| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:992`](../ui/dr-ui/src/export.rs#L992), [`ui/dr-ui/src/lib.rs:262`](../ui/dr-ui/src/lib.rs#L262), [`ui/dr-ui/src/lib.rs:2721`](../ui/dr-ui/src/lib.rs#L2721), [`ui/dr-ui/src/lib.rs:601`](../ui/dr-ui/src/lib.rs#L601), [`ui/dr-ui/src/lib.rs:647`](../ui/dr-ui/src/lib.rs#L647), [`ui/dr-ui/src/lib.rs:674`](../ui/dr-ui/src/lib.rs#L674), [`ui/dr-ui/src/library_ui.rs:4233`](../ui/dr-ui/src/library_ui.rs#L4233), [`ui/dr-ui/src/library_ui.rs:677`](../ui/dr-ui/src/library_ui.rs#L677), [`ui/dr-ui/src/library_ui.rs:777`](../ui/dr-ui/src/library_ui.rs#L777), [`ui/dr-ui/src/library_ui.rs:7951`](../ui/dr-ui/src/library_ui.rs#L7951), [`ui/dr-ui/src/library_ui.rs:8028`](../ui/dr-ui/src/library_ui.rs#L8028), [`ui/dr-ui/src/library_ui.rs:8040`](../ui/dr-ui/src/library_ui.rs#L8040), [`ui/dr-ui/src/library_ui.rs:834`](../ui/dr-ui/src/library_ui.rs#L834), [`ui/dr-ui/ui/app.slint:1088`](../ui/dr-ui/ui/app.slint#L1088), [`ui/dr-ui/ui/app.slint:1624`](../ui/dr-ui/ui/app.slint#L1624), [`ui/dr-ui/ui/library.slint:1583`](../ui/dr-ui/ui/library.slint#L1583), [`ui/dr-ui/ui/library.slint:4278`](../ui/dr-ui/ui/library.slint#L4278) | | FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:655`](../core/dr-types/src/lib.rs#L655), [`core/dr-types/src/settings.rs:577`](../core/dr-types/src/settings.rs#L577), [`ui/dr-ui/src/develop.rs:4539`](../ui/dr-ui/src/develop.rs#L4539), [`ui/dr-ui/src/develop.rs:4651`](../ui/dr-ui/src/develop.rs#L4651), [`ui/dr-ui/src/develop.rs:729`](../ui/dr-ui/src/develop.rs#L729), [`ui/dr-ui/src/export.rs:467`](../ui/dr-ui/src/export.rs#L467), [`ui/dr-ui/src/export.rs:634`](../ui/dr-ui/src/export.rs#L634), [`ui/dr-ui/src/export.rs:680`](../ui/dr-ui/src/export.rs#L680), [`ui/dr-ui/src/export.rs:702`](../ui/dr-ui/src/export.rs#L702), [`ui/dr-ui/src/export.rs:826`](../ui/dr-ui/src/export.rs#L826), [`ui/dr-ui/src/export.rs:844`](../ui/dr-ui/src/export.rs#L844), [`ui/dr-ui/src/lib.rs:262`](../ui/dr-ui/src/lib.rs#L262), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`ui/dr-ui/src/lib.rs:633`](../ui/dr-ui/src/lib.rs#L633), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:4672`](../ui/dr-ui/src/develop.rs#L4672), [`ui/dr-ui/src/develop.rs:4710`](../ui/dr-ui/src/develop.rs#L4710), [`ui/dr-ui/src/develop.rs:6878`](../ui/dr-ui/src/develop.rs#L6878), [`ui/dr-ui/src/lib.rs:601`](../ui/dr-ui/src/lib.rs#L601), [`ui/dr-ui/src/library.rs:3679`](../ui/dr-ui/src/library.rs#L3679), [`ui/dr-ui/src/library.rs:4092`](../ui/dr-ui/src/library.rs#L4092), [`ui/dr-ui/src/library.rs:4128`](../ui/dr-ui/src/library.rs#L4128), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | | FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:361`](../core/dr-sync/src/account.rs#L361), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) | -| FR-NC-10 | [`core/dr-sync/src/account.rs:226`](../core/dr-sync/src/account.rs#L226), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:674`](../ui/dr-ui/src/lib.rs#L674), [`ui/dr-ui/src/library.rs:1170`](../ui/dr-ui/src/library.rs#L1170), [`ui/dr-ui/src/library.rs:2483`](../ui/dr-ui/src/library.rs#L2483), [`ui/dr-ui/src/library.rs:589`](../ui/dr-ui/src/library.rs#L589), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:2020`](../ui/dr-ui/src/library_ui.rs#L2020), [`ui/dr-ui/src/library_ui.rs:4205`](../ui/dr-ui/src/library_ui.rs#L4205), [`ui/dr-ui/src/library_ui.rs:618`](../ui/dr-ui/src/library_ui.rs#L618), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | -| FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:40`](../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/lib.rs:914`](../core/dr-sync-nextcloud/src/lib.rs#L914), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:87`](../core/dr-sync/src/account.rs#L87), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | +| FR-NC-10 | [`core/dr-sync/src/account.rs:226`](../core/dr-sync/src/account.rs#L226), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:674`](../ui/dr-ui/src/lib.rs#L674), [`ui/dr-ui/src/library.rs:1170`](../ui/dr-ui/src/library.rs#L1170), [`ui/dr-ui/src/library.rs:2483`](../ui/dr-ui/src/library.rs#L2483), [`ui/dr-ui/src/library.rs:589`](../ui/dr-ui/src/library.rs#L589), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:2020`](../ui/dr-ui/src/library_ui.rs#L2020), [`ui/dr-ui/src/library_ui.rs:4233`](../ui/dr-ui/src/library_ui.rs#L4233), [`ui/dr-ui/src/library_ui.rs:618`](../ui/dr-ui/src/library_ui.rs#L618), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:1022`](../core/dr-sync-nextcloud/src/lib.rs#L1022), [`core/dr-sync-nextcloud/src/lib.rs:40`](../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:87`](../core/dr-sync/src/account.rs#L87), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | | FR-NC-13 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`ui/dr-ui/src/launch_ui.rs:316`](../ui/dr-ui/src/launch_ui.rs#L316), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | | FR-NC-2 | [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:304`](../core/dr-sync/src/account.rs#L304), [`core/dr-sync/src/account.rs:361`](../core/dr-sync/src/account.rs#L361), [`core/dr-sync/src/account.rs:59`](../core/dr-sync/src/account.rs#L59), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) | -| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:85`](../core/dr-sync/src/capability.rs#L85), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:3520`](../ui/dr-ui/src/library.rs#L3520), [`ui/dr-ui/src/library.rs:4278`](../ui/dr-ui/src/library.rs#L4278), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:4459`](../ui/dr-ui/src/library_ui.rs#L4459), [`ui/dr-ui/src/library_ui.rs:5970`](../ui/dr-ui/src/library_ui.rs#L5970), [`ui/dr-ui/ui/app.slint:383`](../ui/dr-ui/ui/app.slint#L383), [`ui/dr-ui/ui/settings.slint:420`](../ui/dr-ui/ui/settings.slint#L420), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | +| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:85`](../core/dr-sync/src/capability.rs#L85), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:3520`](../ui/dr-ui/src/library.rs#L3520), [`ui/dr-ui/src/library.rs:4278`](../ui/dr-ui/src/library.rs#L4278), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:4499`](../ui/dr-ui/src/library_ui.rs#L4499), [`ui/dr-ui/src/library_ui.rs:6010`](../ui/dr-ui/src/library_ui.rs#L6010), [`ui/dr-ui/ui/app.slint:383`](../ui/dr-ui/ui/app.slint#L383), [`ui/dr-ui/ui/settings.slint:420`](../ui/dr-ui/ui/settings.slint#L420), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | | FR-NC-4 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-nextcloud/src/propfind.rs:103`](../core/dr-sync-nextcloud/src/propfind.rs#L103), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/scan.rs:129`](../core/dr-sync/src/scan.rs#L129), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61) | | FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488) | | FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) | -| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1296`](../core/dr-catalog/src/schema.rs#L1296), [`core/dr-catalog/src/schema.rs:895`](../core/dr-catalog/src/schema.rs#L895), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2940`](../ui/dr-ui/src/collections_ui.rs#L2940), [`ui/dr-ui/src/collections_ui.rs:3182`](../ui/dr-ui/src/collections_ui.rs#L3182), [`ui/dr-ui/src/collections_ui.rs:4083`](../ui/dr-ui/src/collections_ui.rs#L4083), [`ui/dr-ui/src/collections_ui.rs:785`](../ui/dr-ui/src/collections_ui.rs#L785), [`ui/dr-ui/src/collections_ui.rs:856`](../ui/dr-ui/src/collections_ui.rs#L856), [`ui/dr-ui/src/lib.rs:2404`](../ui/dr-ui/src/lib.rs#L2404), [`ui/dr-ui/src/lib.rs:3762`](../ui/dr-ui/src/lib.rs#L3762), [`ui/dr-ui/src/library.rs:2109`](../ui/dr-ui/src/library.rs#L2109), [`ui/dr-ui/src/library.rs:2132`](../ui/dr-ui/src/library.rs#L2132), [`ui/dr-ui/src/library.rs:2407`](../ui/dr-ui/src/library.rs#L2407), [`ui/dr-ui/src/library_ui.rs:1445`](../ui/dr-ui/src/library_ui.rs#L1445), [`ui/dr-ui/src/library_ui.rs:1518`](../ui/dr-ui/src/library_ui.rs#L1518), [`ui/dr-ui/src/library_ui.rs:1619`](../ui/dr-ui/src/library_ui.rs#L1619), [`ui/dr-ui/src/library_ui.rs:1674`](../ui/dr-ui/src/library_ui.rs#L1674), [`ui/dr-ui/src/library_ui.rs:1809`](../ui/dr-ui/src/library_ui.rs#L1809), [`ui/dr-ui/src/library_ui.rs:1927`](../ui/dr-ui/src/library_ui.rs#L1927), [`ui/dr-ui/src/library_ui.rs:2694`](../ui/dr-ui/src/library_ui.rs#L2694), [`ui/dr-ui/src/library_ui.rs:344`](../ui/dr-ui/src/library_ui.rs#L344), [`ui/dr-ui/src/library_ui.rs:355`](../ui/dr-ui/src/library_ui.rs#L355), [`ui/dr-ui/src/library_ui.rs:370`](../ui/dr-ui/src/library_ui.rs#L370), [`ui/dr-ui/src/library_ui.rs:379`](../ui/dr-ui/src/library_ui.rs#L379), [`ui/dr-ui/src/library_ui.rs:509`](../ui/dr-ui/src/library_ui.rs#L509), [`ui/dr-ui/src/library_ui.rs:524`](../ui/dr-ui/src/library_ui.rs#L524), [`ui/dr-ui/src/library_ui.rs:571`](../ui/dr-ui/src/library_ui.rs#L571), [`ui/dr-ui/src/library_ui.rs:634`](../ui/dr-ui/src/library_ui.rs#L634), [`ui/dr-ui/src/library_ui.rs:665`](../ui/dr-ui/src/library_ui.rs#L665), [`ui/dr-ui/src/library_ui.rs:677`](../ui/dr-ui/src/library_ui.rs#L677), [`ui/dr-ui/src/library_ui.rs:6807`](../ui/dr-ui/src/library_ui.rs#L6807), [`ui/dr-ui/src/library_ui.rs:6825`](../ui/dr-ui/src/library_ui.rs#L6825), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:1518`](../ui/dr-ui/ui/app.slint#L1518), [`ui/dr-ui/ui/app.slint:3067`](../ui/dr-ui/ui/app.slint#L3067), [`ui/dr-ui/ui/app.slint:535`](../ui/dr-ui/ui/app.slint#L535), [`ui/dr-ui/ui/app.slint:543`](../ui/dr-ui/ui/app.slint#L543), [`ui/dr-ui/ui/app.slint:549`](../ui/dr-ui/ui/app.slint#L549), [`ui/dr-ui/ui/collections.slint:1037`](../ui/dr-ui/ui/collections.slint#L1037), [`ui/dr-ui/ui/collections.slint:1048`](../ui/dr-ui/ui/collections.slint#L1048), [`ui/dr-ui/ui/collections.slint:1127`](../ui/dr-ui/ui/collections.slint#L1127), [`ui/dr-ui/ui/collections.slint:272`](../ui/dr-ui/ui/collections.slint#L272), [`ui/dr-ui/ui/collections.slint:424`](../ui/dr-ui/ui/collections.slint#L424), [`ui/dr-ui/ui/collections.slint:433`](../ui/dr-ui/ui/collections.slint#L433), [`ui/dr-ui/ui/collections.slint:436`](../ui/dr-ui/ui/collections.slint#L436), [`ui/dr-ui/ui/collections.slint:482`](../ui/dr-ui/ui/collections.slint#L482), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:720`](../ui/dr-ui/ui/collections.slint#L720), [`ui/dr-ui/ui/collections.slint:877`](../ui/dr-ui/ui/collections.slint#L877), [`ui/dr-ui/ui/collections.slint:91`](../ui/dr-ui/ui/collections.slint#L91), [`ui/dr-ui/ui/icons.slint:262`](../ui/dr-ui/ui/icons.slint#L262) | +| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1296`](../core/dr-catalog/src/schema.rs#L1296), [`core/dr-catalog/src/schema.rs:895`](../core/dr-catalog/src/schema.rs#L895), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2940`](../ui/dr-ui/src/collections_ui.rs#L2940), [`ui/dr-ui/src/collections_ui.rs:3182`](../ui/dr-ui/src/collections_ui.rs#L3182), [`ui/dr-ui/src/collections_ui.rs:4083`](../ui/dr-ui/src/collections_ui.rs#L4083), [`ui/dr-ui/src/collections_ui.rs:785`](../ui/dr-ui/src/collections_ui.rs#L785), [`ui/dr-ui/src/collections_ui.rs:856`](../ui/dr-ui/src/collections_ui.rs#L856), [`ui/dr-ui/src/lib.rs:2404`](../ui/dr-ui/src/lib.rs#L2404), [`ui/dr-ui/src/lib.rs:3762`](../ui/dr-ui/src/lib.rs#L3762), [`ui/dr-ui/src/library.rs:2109`](../ui/dr-ui/src/library.rs#L2109), [`ui/dr-ui/src/library.rs:2132`](../ui/dr-ui/src/library.rs#L2132), [`ui/dr-ui/src/library.rs:2407`](../ui/dr-ui/src/library.rs#L2407), [`ui/dr-ui/src/library_ui.rs:1445`](../ui/dr-ui/src/library_ui.rs#L1445), [`ui/dr-ui/src/library_ui.rs:1518`](../ui/dr-ui/src/library_ui.rs#L1518), [`ui/dr-ui/src/library_ui.rs:1619`](../ui/dr-ui/src/library_ui.rs#L1619), [`ui/dr-ui/src/library_ui.rs:1674`](../ui/dr-ui/src/library_ui.rs#L1674), [`ui/dr-ui/src/library_ui.rs:1809`](../ui/dr-ui/src/library_ui.rs#L1809), [`ui/dr-ui/src/library_ui.rs:1927`](../ui/dr-ui/src/library_ui.rs#L1927), [`ui/dr-ui/src/library_ui.rs:2694`](../ui/dr-ui/src/library_ui.rs#L2694), [`ui/dr-ui/src/library_ui.rs:344`](../ui/dr-ui/src/library_ui.rs#L344), [`ui/dr-ui/src/library_ui.rs:355`](../ui/dr-ui/src/library_ui.rs#L355), [`ui/dr-ui/src/library_ui.rs:370`](../ui/dr-ui/src/library_ui.rs#L370), [`ui/dr-ui/src/library_ui.rs:379`](../ui/dr-ui/src/library_ui.rs#L379), [`ui/dr-ui/src/library_ui.rs:509`](../ui/dr-ui/src/library_ui.rs#L509), [`ui/dr-ui/src/library_ui.rs:524`](../ui/dr-ui/src/library_ui.rs#L524), [`ui/dr-ui/src/library_ui.rs:571`](../ui/dr-ui/src/library_ui.rs#L571), [`ui/dr-ui/src/library_ui.rs:634`](../ui/dr-ui/src/library_ui.rs#L634), [`ui/dr-ui/src/library_ui.rs:665`](../ui/dr-ui/src/library_ui.rs#L665), [`ui/dr-ui/src/library_ui.rs:677`](../ui/dr-ui/src/library_ui.rs#L677), [`ui/dr-ui/src/library_ui.rs:6847`](../ui/dr-ui/src/library_ui.rs#L6847), [`ui/dr-ui/src/library_ui.rs:6865`](../ui/dr-ui/src/library_ui.rs#L6865), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:1518`](../ui/dr-ui/ui/app.slint#L1518), [`ui/dr-ui/ui/app.slint:3067`](../ui/dr-ui/ui/app.slint#L3067), [`ui/dr-ui/ui/app.slint:535`](../ui/dr-ui/ui/app.slint#L535), [`ui/dr-ui/ui/app.slint:543`](../ui/dr-ui/ui/app.slint#L543), [`ui/dr-ui/ui/app.slint:549`](../ui/dr-ui/ui/app.slint#L549), [`ui/dr-ui/ui/collections.slint:1037`](../ui/dr-ui/ui/collections.slint#L1037), [`ui/dr-ui/ui/collections.slint:1048`](../ui/dr-ui/ui/collections.slint#L1048), [`ui/dr-ui/ui/collections.slint:1127`](../ui/dr-ui/ui/collections.slint#L1127), [`ui/dr-ui/ui/collections.slint:272`](../ui/dr-ui/ui/collections.slint#L272), [`ui/dr-ui/ui/collections.slint:424`](../ui/dr-ui/ui/collections.slint#L424), [`ui/dr-ui/ui/collections.slint:433`](../ui/dr-ui/ui/collections.slint#L433), [`ui/dr-ui/ui/collections.slint:436`](../ui/dr-ui/ui/collections.slint#L436), [`ui/dr-ui/ui/collections.slint:482`](../ui/dr-ui/ui/collections.slint#L482), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:720`](../ui/dr-ui/ui/collections.slint#L720), [`ui/dr-ui/ui/collections.slint:877`](../ui/dr-ui/ui/collections.slint#L877), [`ui/dr-ui/ui/collections.slint:91`](../ui/dr-ui/ui/collections.slint#L91), [`ui/dr-ui/ui/icons.slint:262`](../ui/dr-ui/ui/icons.slint#L262) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1674`](../ui/dr-ui/src/library_ui.rs#L1674), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | -| FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:818`](../core/dr-sync-folder/src/lib.rs#L818), [`core/dr-sync-folder/src/lib.rs:857`](../core/dr-sync-folder/src/lib.rs#L857), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:172`](../core/dr-sync-nextcloud/src/desktop_client.rs#L172), [`core/dr-sync-nextcloud/src/desktop_client.rs:35`](../core/dr-sync-nextcloud/src/desktop_client.rs#L35), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`core/dr-types/src/lib.rs:204`](../core/dr-types/src/lib.rs#L204), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:4083`](../ui/dr-ui/src/collections_ui.rs#L4083), [`ui/dr-ui/src/collections_ui.rs:785`](../ui/dr-ui/src/collections_ui.rs#L785), [`ui/dr-ui/src/collections_ui.rs:856`](../ui/dr-ui/src/collections_ui.rs#L856), [`ui/dr-ui/src/derived_sync.rs:606`](../ui/dr-ui/src/derived_sync.rs#L606), [`ui/dr-ui/src/derived_sync.rs:826`](../ui/dr-ui/src/derived_sync.rs#L826), [`ui/dr-ui/src/library.rs:1020`](../ui/dr-ui/src/library.rs#L1020), [`ui/dr-ui/src/library.rs:2229`](../ui/dr-ui/src/library.rs#L2229), [`ui/dr-ui/src/library.rs:2319`](../ui/dr-ui/src/library.rs#L2319), [`ui/dr-ui/src/library.rs:3889`](../ui/dr-ui/src/library.rs#L3889), [`ui/dr-ui/src/library.rs:4398`](../ui/dr-ui/src/library.rs#L4398), [`ui/dr-ui/src/library_ui.rs:1445`](../ui/dr-ui/src/library_ui.rs#L1445), [`ui/dr-ui/src/library_ui.rs:1518`](../ui/dr-ui/src/library_ui.rs#L1518), [`ui/dr-ui/src/library_ui.rs:1717`](../ui/dr-ui/src/library_ui.rs#L1717), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/ui/collections.slint:272`](../ui/dr-ui/ui/collections.slint#L272), [`ui/dr-ui/ui/collections.slint:877`](../ui/dr-ui/ui/collections.slint#L877), [`ui/dr-ui/ui/icons.slint:262`](../ui/dr-ui/ui/icons.slint#L262) | +| FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:818`](../core/dr-sync-folder/src/lib.rs#L818), [`core/dr-sync-folder/src/lib.rs:857`](../core/dr-sync-folder/src/lib.rs#L857), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:172`](../core/dr-sync-nextcloud/src/desktop_client.rs#L172), [`core/dr-sync-nextcloud/src/desktop_client.rs:35`](../core/dr-sync-nextcloud/src/desktop_client.rs#L35), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`core/dr-types/src/lib.rs:204`](../core/dr-types/src/lib.rs#L204), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:4083`](../ui/dr-ui/src/collections_ui.rs#L4083), [`ui/dr-ui/src/collections_ui.rs:785`](../ui/dr-ui/src/collections_ui.rs#L785), [`ui/dr-ui/src/collections_ui.rs:856`](../ui/dr-ui/src/collections_ui.rs#L856), [`ui/dr-ui/src/derived_sync.rs:1031`](../ui/dr-ui/src/derived_sync.rs#L1031), [`ui/dr-ui/src/derived_sync.rs:631`](../ui/dr-ui/src/derived_sync.rs#L631), [`ui/dr-ui/src/library.rs:1020`](../ui/dr-ui/src/library.rs#L1020), [`ui/dr-ui/src/library.rs:2229`](../ui/dr-ui/src/library.rs#L2229), [`ui/dr-ui/src/library.rs:2319`](../ui/dr-ui/src/library.rs#L2319), [`ui/dr-ui/src/library.rs:3889`](../ui/dr-ui/src/library.rs#L3889), [`ui/dr-ui/src/library.rs:4398`](../ui/dr-ui/src/library.rs#L4398), [`ui/dr-ui/src/library_ui.rs:1445`](../ui/dr-ui/src/library_ui.rs#L1445), [`ui/dr-ui/src/library_ui.rs:1518`](../ui/dr-ui/src/library_ui.rs#L1518), [`ui/dr-ui/src/library_ui.rs:1717`](../ui/dr-ui/src/library_ui.rs#L1717), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/ui/collections.slint:272`](../ui/dr-ui/ui/collections.slint#L272), [`ui/dr-ui/ui/collections.slint:877`](../ui/dr-ui/ui/collections.slint#L877), [`ui/dr-ui/ui/icons.slint:262`](../ui/dr-ui/ui/icons.slint#L262) | | FR-NC-6d | [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1) | -| FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:105`](../core/dr-sync-nextcloud/src/lib.rs#L105), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:4278`](../ui/dr-ui/src/library.rs#L4278), [`ui/dr-ui/src/library_ui.rs:4459`](../ui/dr-ui/src/library_ui.rs#L4459), [`ui/dr-ui/ui/settings.slint:420`](../ui/dr-ui/ui/settings.slint#L420) | +| FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:105`](../core/dr-sync-nextcloud/src/lib.rs#L105), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:4278`](../ui/dr-ui/src/library.rs#L4278), [`ui/dr-ui/src/library_ui.rs:4499`](../ui/dr-ui/src/library_ui.rs#L4499), [`ui/dr-ui/ui/settings.slint:420`](../ui/dr-ui/ui/settings.slint#L420) | | FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:309`](../core/dr-types/src/settings.rs#L309), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1508`](../ui/dr-ui/src/lib.rs#L1508), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-NC-7b | [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`ui/dr-ui/src/import.rs:122`](../ui/dr-ui/src/import.rs#L122), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import.rs:584`](../ui/dr-ui/src/import.rs#L584), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1508`](../ui/dr-ui/src/lib.rs#L1508) | | FR-NC-8 | [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:929`](../core/dr-catalog/src/rating.rs#L929), [`core/dr-catalog/src/schema.rs:179`](../core/dr-catalog/src/schema.rs#L179), [`core/dr-pipeline/src/sidecar.rs:122`](../core/dr-pipeline/src/sidecar.rs#L122), [`core/dr-pipeline/src/sidecar.rs:2944`](../core/dr-pipeline/src/sidecar.rs#L2944), [`core/dr-pipeline/src/sidecar.rs:645`](../core/dr-pipeline/src/sidecar.rs#L645), [`core/dr-pipeline/src/sidecar.rs:720`](../core/dr-pipeline/src/sidecar.rs#L720), [`core/dr-pipeline/src/sidecar.rs:96`](../core/dr-pipeline/src/sidecar.rs#L96), [`ui/dr-ui/src/lib.rs:2374`](../ui/dr-ui/src/lib.rs#L2374), [`ui/dr-ui/src/library.rs:1062`](../ui/dr-ui/src/library.rs#L1062), [`ui/dr-ui/src/library.rs:2538`](../ui/dr-ui/src/library.rs#L2538), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:760`](../ui/dr-ui/src/library.rs#L760), [`ui/dr-ui/src/library.rs:7767`](../ui/dr-ui/src/library.rs#L7767), [`ui/dr-ui/src/library_ui.rs:586`](../ui/dr-ui/src/library_ui.rs#L586), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:336`](../ui/dr-ui/src/presets.rs#L336) | -| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:929`](../core/dr-catalog/src/rating.rs#L929), [`core/dr-catalog/src/schema.rs:179`](../core/dr-catalog/src/schema.rs#L179), [`core/dr-catalog/src/schema.rs:557`](../core/dr-catalog/src/schema.rs#L557), [`core/dr-catalog/src/schema.rs:838`](../core/dr-catalog/src/schema.rs#L838), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:177`](../core/dr-pipeline/src/sidecar.rs#L177), [`core/dr-pipeline/src/sidecar.rs:204`](../core/dr-pipeline/src/sidecar.rs#L204), [`core/dr-pipeline/src/sidecar.rs:2473`](../core/dr-pipeline/src/sidecar.rs#L2473), [`core/dr-pipeline/src/sidecar.rs:2710`](../core/dr-pipeline/src/sidecar.rs#L2710), [`core/dr-pipeline/src/sidecar.rs:2944`](../core/dr-pipeline/src/sidecar.rs#L2944), [`core/dr-pipeline/src/sidecar.rs:380`](../core/dr-pipeline/src/sidecar.rs#L380), [`core/dr-pipeline/src/sidecar.rs:478`](../core/dr-pipeline/src/sidecar.rs#L478), [`core/dr-pipeline/src/sidecar.rs:645`](../core/dr-pipeline/src/sidecar.rs#L645), [`core/dr-pipeline/src/sidecar.rs:695`](../core/dr-pipeline/src/sidecar.rs#L695), [`core/dr-pipeline/src/sidecar.rs:720`](../core/dr-pipeline/src/sidecar.rs#L720), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/mask_sidecar.rs:1075`](../core/dr-pipeline/tests/mask_sidecar.rs#L1075), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:56`](../core/dr-sync/src/scan.rs#L56), [`core/dr-sync/src/scan.rs:926`](../core/dr-sync/src/scan.rs#L926), [`core/dr-xmp/src/lib.rs:733`](../core/dr-xmp/src/lib.rs#L733), [`core/dr-xmp/src/lib.rs:746`](../core/dr-xmp/src/lib.rs#L746), [`ui/dr-ui/src/derived_sync.rs:606`](../ui/dr-ui/src/derived_sync.rs#L606), [`ui/dr-ui/src/library.rs:1062`](../ui/dr-ui/src/library.rs#L1062), [`ui/dr-ui/src/library.rs:1082`](../ui/dr-ui/src/library.rs#L1082), [`ui/dr-ui/src/library.rs:1391`](../ui/dr-ui/src/library.rs#L1391), [`ui/dr-ui/src/library.rs:1426`](../ui/dr-ui/src/library.rs#L1426), [`ui/dr-ui/src/library.rs:2538`](../ui/dr-ui/src/library.rs#L2538), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:7599`](../ui/dr-ui/src/library.rs#L7599), [`ui/dr-ui/src/library.rs:760`](../ui/dr-ui/src/library.rs#L760), [`ui/dr-ui/src/library.rs:7767`](../ui/dr-ui/src/library.rs#L7767), [`ui/dr-ui/src/library.rs:832`](../ui/dr-ui/src/library.rs#L832), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:1302`](../ui/dr-ui/src/library_ui.rs#L1302), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:336`](../ui/dr-ui/src/presets.rs#L336), [`ui/dr-ui/src/xmp_sync.rs:1`](../ui/dr-ui/src/xmp_sync.rs#L1), [`ui/dr-ui/src/xmp_sync.rs:287`](../ui/dr-ui/src/xmp_sync.rs#L287) | +| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:929`](../core/dr-catalog/src/rating.rs#L929), [`core/dr-catalog/src/schema.rs:179`](../core/dr-catalog/src/schema.rs#L179), [`core/dr-catalog/src/schema.rs:557`](../core/dr-catalog/src/schema.rs#L557), [`core/dr-catalog/src/schema.rs:838`](../core/dr-catalog/src/schema.rs#L838), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:177`](../core/dr-pipeline/src/sidecar.rs#L177), [`core/dr-pipeline/src/sidecar.rs:204`](../core/dr-pipeline/src/sidecar.rs#L204), [`core/dr-pipeline/src/sidecar.rs:2473`](../core/dr-pipeline/src/sidecar.rs#L2473), [`core/dr-pipeline/src/sidecar.rs:2710`](../core/dr-pipeline/src/sidecar.rs#L2710), [`core/dr-pipeline/src/sidecar.rs:2944`](../core/dr-pipeline/src/sidecar.rs#L2944), [`core/dr-pipeline/src/sidecar.rs:380`](../core/dr-pipeline/src/sidecar.rs#L380), [`core/dr-pipeline/src/sidecar.rs:478`](../core/dr-pipeline/src/sidecar.rs#L478), [`core/dr-pipeline/src/sidecar.rs:645`](../core/dr-pipeline/src/sidecar.rs#L645), [`core/dr-pipeline/src/sidecar.rs:695`](../core/dr-pipeline/src/sidecar.rs#L695), [`core/dr-pipeline/src/sidecar.rs:720`](../core/dr-pipeline/src/sidecar.rs#L720), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/mask_sidecar.rs:1075`](../core/dr-pipeline/tests/mask_sidecar.rs#L1075), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:124`](../core/dr-sync-nextcloud/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:56`](../core/dr-sync/src/scan.rs#L56), [`core/dr-sync/src/scan.rs:926`](../core/dr-sync/src/scan.rs#L926), [`core/dr-xmp/src/lib.rs:733`](../core/dr-xmp/src/lib.rs#L733), [`core/dr-xmp/src/lib.rs:746`](../core/dr-xmp/src/lib.rs#L746), [`ui/dr-ui/src/derived_sync.rs:1059`](../ui/dr-ui/src/derived_sync.rs#L1059), [`ui/dr-ui/src/derived_sync.rs:602`](../ui/dr-ui/src/derived_sync.rs#L602), [`ui/dr-ui/src/derived_sync.rs:631`](../ui/dr-ui/src/derived_sync.rs#L631), [`ui/dr-ui/src/derived_sync.rs:664`](../ui/dr-ui/src/derived_sync.rs#L664), [`ui/dr-ui/src/derived_sync.rs:810`](../ui/dr-ui/src/derived_sync.rs#L810), [`ui/dr-ui/src/library.rs:1062`](../ui/dr-ui/src/library.rs#L1062), [`ui/dr-ui/src/library.rs:1082`](../ui/dr-ui/src/library.rs#L1082), [`ui/dr-ui/src/library.rs:1391`](../ui/dr-ui/src/library.rs#L1391), [`ui/dr-ui/src/library.rs:1426`](../ui/dr-ui/src/library.rs#L1426), [`ui/dr-ui/src/library.rs:2538`](../ui/dr-ui/src/library.rs#L2538), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:7599`](../ui/dr-ui/src/library.rs#L7599), [`ui/dr-ui/src/library.rs:760`](../ui/dr-ui/src/library.rs#L760), [`ui/dr-ui/src/library.rs:7767`](../ui/dr-ui/src/library.rs#L7767), [`ui/dr-ui/src/library.rs:832`](../ui/dr-ui/src/library.rs#L832), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:1302`](../ui/dr-ui/src/library_ui.rs#L1302), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:336`](../ui/dr-ui/src/presets.rs#L336), [`ui/dr-ui/src/xmp_sync.rs:1`](../ui/dr-ui/src/xmp_sync.rs#L1), [`ui/dr-ui/src/xmp_sync.rs:287`](../ui/dr-ui/src/xmp_sync.rs#L287) | | FR-PLAT-AND-2 | [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-sync-folder/src/lib.rs:242`](../core/dr-sync-folder/src/lib.rs#L242), [`core/dr-sync-folder/src/tests.rs:51`](../core/dr-sync-folder/src/tests.rs#L51), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/error.rs:195`](../core/dr-sync/src/error.rs#L195), [`core/dr-sync/src/scan.rs:211`](../core/dr-sync/src/scan.rs#L211), [`core/dr-sync/src/scan.rs:519`](../core/dr-sync/src/scan.rs#L519), [`core/dr-sync/src/scan.rs:545`](../core/dr-sync/src/scan.rs#L545), [`core/dr-sync/src/scan.rs:574`](../core/dr-sync/src/scan.rs#L574), [`ui/dr-ui/src/library.rs:1291`](../ui/dr-ui/src/library.rs#L1291), [`ui/dr-ui/src/library.rs:1344`](../ui/dr-ui/src/library.rs#L1344), [`ui/dr-ui/src/library.rs:1375`](../ui/dr-ui/src/library.rs#L1375), [`ui/dr-ui/src/library.rs:1955`](../ui/dr-ui/src/library.rs#L1955), [`ui/dr-ui/src/library_ui.rs:1273`](../ui/dr-ui/src/library_ui.rs#L1273), [`ui/dr-ui/src/library_ui.rs:1369`](../ui/dr-ui/src/library_ui.rs#L1369), [`ui/dr-ui/src/library_ui.rs:1968`](../ui/dr-ui/src/library_ui.rs#L1968), [`ui/dr-ui/src/library_ui.rs:328`](../ui/dr-ui/src/library_ui.rs#L328), [`ui/dr-ui/src/library_ui.rs:555`](../ui/dr-ui/src/library_ui.rs#L555) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | | FR-PLAT-AND-4 | [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | @@ -131,13 +131,13 @@ _None._ | FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:262`](../ui/dr-ui/src/lib.rs#L262) | | FR-RAW-5 | [`core/dr-decode/src/lib.rs:167`](../core/dr-decode/src/lib.rs#L167), [`core/dr-gpu/src/demosaic.rs:34`](../core/dr-gpu/src/demosaic.rs#L34), [`core/dr-gpu/src/demosaic.rs:602`](../core/dr-gpu/src/demosaic.rs#L602), [`core/dr-gpu/src/demosaic.rs:681`](../core/dr-gpu/src/demosaic.rs#L681), [`core/dr-gpu/src/demosaic.rs:805`](../core/dr-gpu/src/demosaic.rs#L805) | | FR-UI-1 | [`core/dr-types/src/settings.rs:1269`](../core/dr-types/src/settings.rs#L1269), [`core/dr-types/src/settings.rs:424`](../core/dr-types/src/settings.rs#L424), [`core/dr-types/src/settings.rs:444`](../core/dr-types/src/settings.rs#L444), [`ui/dr-ui/src/lib.rs:102`](../ui/dr-ui/src/lib.rs#L102), [`ui/dr-ui/src/lib.rs:117`](../ui/dr-ui/src/lib.rs#L117), [`ui/dr-ui/src/lib.rs:139`](../ui/dr-ui/src/lib.rs#L139), [`ui/dr-ui/src/lib.rs:2619`](../ui/dr-ui/src/lib.rs#L2619), [`ui/dr-ui/src/lib.rs:3874`](../ui/dr-ui/src/lib.rs#L3874), [`ui/dr-ui/src/lib.rs:3893`](../ui/dr-ui/src/lib.rs#L3893), [`ui/dr-ui/src/lib.rs:3920`](../ui/dr-ui/src/lib.rs#L3920), [`ui/dr-ui/src/lib.rs:3971`](../ui/dr-ui/src/lib.rs#L3971), [`ui/dr-ui/src/lib.rs:4004`](../ui/dr-ui/src/lib.rs#L4004), [`ui/dr-ui/src/masks_ui.rs:1471`](../ui/dr-ui/src/masks_ui.rs#L1471), [`ui/dr-ui/src/settings_ui.rs:161`](../ui/dr-ui/src/settings_ui.rs#L161), [`ui/dr-ui/ui/adjust.slint:974`](../ui/dr-ui/ui/adjust.slint#L974), [`ui/dr-ui/ui/app.slint:2746`](../ui/dr-ui/ui/app.slint#L2746), [`ui/dr-ui/ui/app.slint:855`](../ui/dr-ui/ui/app.slint#L855), [`ui/dr-ui/ui/app.slint:959`](../ui/dr-ui/ui/app.slint#L959), [`ui/dr-ui/ui/identity.slint:219`](../ui/dr-ui/ui/identity.slint#L219), [`ui/dr-ui/ui/library.slint:1076`](../ui/dr-ui/ui/library.slint#L1076), [`ui/dr-ui/ui/settings.slint:111`](../ui/dr-ui/ui/settings.slint#L111), [`ui/dr-ui/ui/settings.slint:609`](../ui/dr-ui/ui/settings.slint#L609), [`ui/dr-ui/ui/toolrail.slint:156`](../ui/dr-ui/ui/toolrail.slint#L156), [`ui/dr-ui/ui/toolrail.slint:89`](../ui/dr-ui/ui/toolrail.slint#L89) | -| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:1197`](../ui/dr-ui/src/collections_ui.rs#L1197), [`ui/dr-ui/src/collections_ui.rs:1207`](../ui/dr-ui/src/collections_ui.rs#L1207), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1704`](../ui/dr-ui/src/collections_ui.rs#L1704), [`ui/dr-ui/src/collections_ui.rs:1721`](../ui/dr-ui/src/collections_ui.rs#L1721), [`ui/dr-ui/src/collections_ui.rs:1973`](../ui/dr-ui/src/collections_ui.rs#L1973), [`ui/dr-ui/src/collections_ui.rs:2701`](../ui/dr-ui/src/collections_ui.rs#L2701), [`ui/dr-ui/src/collections_ui.rs:3596`](../ui/dr-ui/src/collections_ui.rs#L3596), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:102`](../ui/dr-ui/src/lib.rs#L102), [`ui/dr-ui/src/library_ui.rs:6692`](../ui/dr-ui/src/library_ui.rs#L6692), [`ui/dr-ui/src/library_ui.rs:8019`](../ui/dr-ui/src/library_ui.rs#L8019), [`ui/dr-ui/ui/app.slint:588`](../ui/dr-ui/ui/app.slint#L588), [`ui/dr-ui/ui/app.slint:602`](../ui/dr-ui/ui/app.slint#L602), [`ui/dr-ui/ui/app.slint:68`](../ui/dr-ui/ui/app.slint#L68), [`ui/dr-ui/ui/app.slint:943`](../ui/dr-ui/ui/app.slint#L943), [`ui/dr-ui/ui/controls.slint:626`](../ui/dr-ui/ui/controls.slint#L626), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1287`](../ui/dr-ui/ui/library.slint#L1287), [`ui/dr-ui/ui/library.slint:1294`](../ui/dr-ui/ui/library.slint#L1294), [`ui/dr-ui/ui/library.slint:1391`](../ui/dr-ui/ui/library.slint#L1391), [`ui/dr-ui/ui/library.slint:2581`](../ui/dr-ui/ui/library.slint#L2581), [`ui/dr-ui/ui/library.slint:4316`](../ui/dr-ui/ui/library.slint#L4316), [`ui/dr-ui/ui/library.slint:965`](../ui/dr-ui/ui/library.slint#L965), [`ui/dr-ui/ui/library.slint:998`](../ui/dr-ui/ui/library.slint#L998), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:125`](../ui/dr-ui/ui/settings.slint#L125) | -| FR-UI-3 | [`ui/dr-ui/src/collections_ui.rs:209`](../ui/dr-ui/src/collections_ui.rs#L209), [`ui/dr-ui/src/collections_ui.rs:2701`](../ui/dr-ui/src/collections_ui.rs#L2701), [`ui/dr-ui/src/collections_ui.rs:414`](../ui/dr-ui/src/collections_ui.rs#L414), [`ui/dr-ui/src/collections_ui.rs:427`](../ui/dr-ui/src/collections_ui.rs#L427), [`ui/dr-ui/src/develop.rs:2850`](../ui/dr-ui/src/develop.rs#L2850), [`ui/dr-ui/src/develop.rs:3238`](../ui/dr-ui/src/develop.rs#L3238), [`ui/dr-ui/src/library_ui.rs:5721`](../ui/dr-ui/src/library_ui.rs#L5721), [`ui/dr-ui/src/masks_ui.rs:1563`](../ui/dr-ui/src/masks_ui.rs#L1563), [`ui/dr-ui/src/masks_ui.rs:1585`](../ui/dr-ui/src/masks_ui.rs#L1585), [`ui/dr-ui/src/masks_ui.rs:339`](../ui/dr-ui/src/masks_ui.rs#L339), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2289`](../ui/dr-ui/ui/app.slint#L2289), [`ui/dr-ui/ui/collections.slint:1127`](../ui/dr-ui/ui/collections.slint#L1127), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:75`](../ui/dr-ui/ui/collections.slint#L75), [`ui/dr-ui/ui/collections.slint:877`](../ui/dr-ui/ui/collections.slint#L877), [`ui/dr-ui/ui/masks.slint:1208`](../ui/dr-ui/ui/masks.slint#L1208), [`ui/dr-ui/ui/widgets.slint:71`](../ui/dr-ui/ui/widgets.slint#L71) | -| FR-UI-4 | [`tools/traceability/src/gestures.rs:1`](../tools/traceability/src/gestures.rs#L1), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:1197`](../ui/dr-ui/src/collections_ui.rs#L1197), [`ui/dr-ui/src/collections_ui.rs:1207`](../ui/dr-ui/src/collections_ui.rs#L1207), [`ui/dr-ui/src/collections_ui.rs:126`](../ui/dr-ui/src/collections_ui.rs#L126), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1704`](../ui/dr-ui/src/collections_ui.rs#L1704), [`ui/dr-ui/src/collections_ui.rs:1721`](../ui/dr-ui/src/collections_ui.rs#L1721), [`ui/dr-ui/src/collections_ui.rs:1837`](../ui/dr-ui/src/collections_ui.rs#L1837), [`ui/dr-ui/src/collections_ui.rs:1973`](../ui/dr-ui/src/collections_ui.rs#L1973), [`ui/dr-ui/src/collections_ui.rs:2000`](../ui/dr-ui/src/collections_ui.rs#L2000), [`ui/dr-ui/src/collections_ui.rs:209`](../ui/dr-ui/src/collections_ui.rs#L209), [`ui/dr-ui/src/collections_ui.rs:2116`](../ui/dr-ui/src/collections_ui.rs#L2116), [`ui/dr-ui/src/collections_ui.rs:2701`](../ui/dr-ui/src/collections_ui.rs#L2701), [`ui/dr-ui/src/collections_ui.rs:2755`](../ui/dr-ui/src/collections_ui.rs#L2755), [`ui/dr-ui/src/collections_ui.rs:3208`](../ui/dr-ui/src/collections_ui.rs#L3208), [`ui/dr-ui/src/collections_ui.rs:3596`](../ui/dr-ui/src/collections_ui.rs#L3596), [`ui/dr-ui/src/collections_ui.rs:3633`](../ui/dr-ui/src/collections_ui.rs#L3633), [`ui/dr-ui/src/collections_ui.rs:414`](../ui/dr-ui/src/collections_ui.rs#L414), [`ui/dr-ui/src/collections_ui.rs:427`](../ui/dr-ui/src/collections_ui.rs#L427), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:718`](../ui/dr-ui/src/collections_ui.rs#L718), [`ui/dr-ui/src/collections_ui.rs:738`](../ui/dr-ui/src/collections_ui.rs#L738), [`ui/dr-ui/src/develop.rs:5283`](../ui/dr-ui/src/develop.rs#L5283), [`ui/dr-ui/src/develop.rs:5308`](../ui/dr-ui/src/develop.rs#L5308), [`ui/dr-ui/src/develop.rs:5319`](../ui/dr-ui/src/develop.rs#L5319), [`ui/dr-ui/src/develop.rs:5333`](../ui/dr-ui/src/develop.rs#L5333), [`ui/dr-ui/src/develop.rs:6561`](../ui/dr-ui/src/develop.rs#L6561), [`ui/dr-ui/src/develop.rs:6592`](../ui/dr-ui/src/develop.rs#L6592), [`ui/dr-ui/src/develop.rs:6613`](../ui/dr-ui/src/develop.rs#L6613), [`ui/dr-ui/src/gesture_book.rs:3`](../ui/dr-ui/src/gesture_book.rs#L3), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:1862`](../ui/dr-ui/src/lib.rs#L1862), [`ui/dr-ui/src/lib.rs:2286`](../ui/dr-ui/src/lib.rs#L2286), [`ui/dr-ui/src/lib.rs:2526`](../ui/dr-ui/src/lib.rs#L2526), [`ui/dr-ui/src/lib.rs:3194`](../ui/dr-ui/src/lib.rs#L3194), [`ui/dr-ui/src/lib.rs:4108`](../ui/dr-ui/src/lib.rs#L4108), [`ui/dr-ui/src/lib.rs:454`](../ui/dr-ui/src/lib.rs#L454), [`ui/dr-ui/src/lib.rs:483`](../ui/dr-ui/src/lib.rs#L483), [`ui/dr-ui/src/library_ui.rs:5721`](../ui/dr-ui/src/library_ui.rs#L5721), [`ui/dr-ui/src/library_ui.rs:5733`](../ui/dr-ui/src/library_ui.rs#L5733), [`ui/dr-ui/src/library_ui.rs:5803`](../ui/dr-ui/src/library_ui.rs#L5803), [`ui/dr-ui/src/library_ui.rs:5913`](../ui/dr-ui/src/library_ui.rs#L5913), [`ui/dr-ui/src/library_ui.rs:5941`](../ui/dr-ui/src/library_ui.rs#L5941), [`ui/dr-ui/src/library_ui.rs:6825`](../ui/dr-ui/src/library_ui.rs#L6825), [`ui/dr-ui/ui/app.slint:168`](../ui/dr-ui/ui/app.slint#L168), [`ui/dr-ui/ui/app.slint:1984`](../ui/dr-ui/ui/app.slint#L1984), [`ui/dr-ui/ui/app.slint:2106`](../ui/dr-ui/ui/app.slint#L2106), [`ui/dr-ui/ui/app.slint:2513`](../ui/dr-ui/ui/app.slint#L2513), [`ui/dr-ui/ui/app.slint:2646`](../ui/dr-ui/ui/app.slint#L2646), [`ui/dr-ui/ui/app.slint:3117`](../ui/dr-ui/ui/app.slint#L3117), [`ui/dr-ui/ui/app.slint:564`](../ui/dr-ui/ui/app.slint#L564), [`ui/dr-ui/ui/app.slint:593`](../ui/dr-ui/ui/app.slint#L593), [`ui/dr-ui/ui/app.slint:602`](../ui/dr-ui/ui/app.slint#L602), [`ui/dr-ui/ui/app.slint:702`](../ui/dr-ui/ui/app.slint#L702), [`ui/dr-ui/ui/collections.slint:75`](../ui/dr-ui/ui/collections.slint#L75), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1053`](../ui/dr-ui/ui/library.slint#L1053), [`ui/dr-ui/ui/library.slint:1262`](../ui/dr-ui/ui/library.slint#L1262), [`ui/dr-ui/ui/library.slint:1287`](../ui/dr-ui/ui/library.slint#L1287), [`ui/dr-ui/ui/library.slint:1294`](../ui/dr-ui/ui/library.slint#L1294), [`ui/dr-ui/ui/library.slint:1327`](../ui/dr-ui/ui/library.slint#L1327), [`ui/dr-ui/ui/library.slint:1384`](../ui/dr-ui/ui/library.slint#L1384), [`ui/dr-ui/ui/library.slint:1391`](../ui/dr-ui/ui/library.slint#L1391), [`ui/dr-ui/ui/library.slint:2581`](../ui/dr-ui/ui/library.slint#L2581), [`ui/dr-ui/ui/library.slint:4179`](../ui/dr-ui/ui/library.slint#L4179), [`ui/dr-ui/ui/library.slint:4257`](../ui/dr-ui/ui/library.slint#L4257), [`ui/dr-ui/ui/library.slint:4316`](../ui/dr-ui/ui/library.slint#L4316), [`ui/dr-ui/ui/library.slint:965`](../ui/dr-ui/ui/library.slint#L965), [`ui/dr-ui/ui/library.slint:992`](../ui/dr-ui/ui/library.slint#L992), [`ui/dr-ui/ui/library.slint:998`](../ui/dr-ui/ui/library.slint#L998) | +| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:1197`](../ui/dr-ui/src/collections_ui.rs#L1197), [`ui/dr-ui/src/collections_ui.rs:1207`](../ui/dr-ui/src/collections_ui.rs#L1207), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1704`](../ui/dr-ui/src/collections_ui.rs#L1704), [`ui/dr-ui/src/collections_ui.rs:1721`](../ui/dr-ui/src/collections_ui.rs#L1721), [`ui/dr-ui/src/collections_ui.rs:1973`](../ui/dr-ui/src/collections_ui.rs#L1973), [`ui/dr-ui/src/collections_ui.rs:2701`](../ui/dr-ui/src/collections_ui.rs#L2701), [`ui/dr-ui/src/collections_ui.rs:3596`](../ui/dr-ui/src/collections_ui.rs#L3596), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:102`](../ui/dr-ui/src/lib.rs#L102), [`ui/dr-ui/src/library_ui.rs:6732`](../ui/dr-ui/src/library_ui.rs#L6732), [`ui/dr-ui/src/library_ui.rs:8059`](../ui/dr-ui/src/library_ui.rs#L8059), [`ui/dr-ui/ui/app.slint:588`](../ui/dr-ui/ui/app.slint#L588), [`ui/dr-ui/ui/app.slint:602`](../ui/dr-ui/ui/app.slint#L602), [`ui/dr-ui/ui/app.slint:68`](../ui/dr-ui/ui/app.slint#L68), [`ui/dr-ui/ui/app.slint:943`](../ui/dr-ui/ui/app.slint#L943), [`ui/dr-ui/ui/controls.slint:626`](../ui/dr-ui/ui/controls.slint#L626), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1287`](../ui/dr-ui/ui/library.slint#L1287), [`ui/dr-ui/ui/library.slint:1294`](../ui/dr-ui/ui/library.slint#L1294), [`ui/dr-ui/ui/library.slint:1391`](../ui/dr-ui/ui/library.slint#L1391), [`ui/dr-ui/ui/library.slint:2581`](../ui/dr-ui/ui/library.slint#L2581), [`ui/dr-ui/ui/library.slint:4316`](../ui/dr-ui/ui/library.slint#L4316), [`ui/dr-ui/ui/library.slint:965`](../ui/dr-ui/ui/library.slint#L965), [`ui/dr-ui/ui/library.slint:998`](../ui/dr-ui/ui/library.slint#L998), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:125`](../ui/dr-ui/ui/settings.slint#L125) | +| FR-UI-3 | [`ui/dr-ui/src/collections_ui.rs:209`](../ui/dr-ui/src/collections_ui.rs#L209), [`ui/dr-ui/src/collections_ui.rs:2701`](../ui/dr-ui/src/collections_ui.rs#L2701), [`ui/dr-ui/src/collections_ui.rs:414`](../ui/dr-ui/src/collections_ui.rs#L414), [`ui/dr-ui/src/collections_ui.rs:427`](../ui/dr-ui/src/collections_ui.rs#L427), [`ui/dr-ui/src/develop.rs:2850`](../ui/dr-ui/src/develop.rs#L2850), [`ui/dr-ui/src/develop.rs:3238`](../ui/dr-ui/src/develop.rs#L3238), [`ui/dr-ui/src/library_ui.rs:5761`](../ui/dr-ui/src/library_ui.rs#L5761), [`ui/dr-ui/src/masks_ui.rs:1563`](../ui/dr-ui/src/masks_ui.rs#L1563), [`ui/dr-ui/src/masks_ui.rs:1585`](../ui/dr-ui/src/masks_ui.rs#L1585), [`ui/dr-ui/src/masks_ui.rs:339`](../ui/dr-ui/src/masks_ui.rs#L339), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2289`](../ui/dr-ui/ui/app.slint#L2289), [`ui/dr-ui/ui/collections.slint:1127`](../ui/dr-ui/ui/collections.slint#L1127), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:75`](../ui/dr-ui/ui/collections.slint#L75), [`ui/dr-ui/ui/collections.slint:877`](../ui/dr-ui/ui/collections.slint#L877), [`ui/dr-ui/ui/masks.slint:1208`](../ui/dr-ui/ui/masks.slint#L1208), [`ui/dr-ui/ui/widgets.slint:71`](../ui/dr-ui/ui/widgets.slint#L71) | +| FR-UI-4 | [`tools/traceability/src/gestures.rs:1`](../tools/traceability/src/gestures.rs#L1), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:1197`](../ui/dr-ui/src/collections_ui.rs#L1197), [`ui/dr-ui/src/collections_ui.rs:1207`](../ui/dr-ui/src/collections_ui.rs#L1207), [`ui/dr-ui/src/collections_ui.rs:126`](../ui/dr-ui/src/collections_ui.rs#L126), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1704`](../ui/dr-ui/src/collections_ui.rs#L1704), [`ui/dr-ui/src/collections_ui.rs:1721`](../ui/dr-ui/src/collections_ui.rs#L1721), [`ui/dr-ui/src/collections_ui.rs:1837`](../ui/dr-ui/src/collections_ui.rs#L1837), [`ui/dr-ui/src/collections_ui.rs:1973`](../ui/dr-ui/src/collections_ui.rs#L1973), [`ui/dr-ui/src/collections_ui.rs:2000`](../ui/dr-ui/src/collections_ui.rs#L2000), [`ui/dr-ui/src/collections_ui.rs:209`](../ui/dr-ui/src/collections_ui.rs#L209), [`ui/dr-ui/src/collections_ui.rs:2116`](../ui/dr-ui/src/collections_ui.rs#L2116), [`ui/dr-ui/src/collections_ui.rs:2701`](../ui/dr-ui/src/collections_ui.rs#L2701), [`ui/dr-ui/src/collections_ui.rs:2755`](../ui/dr-ui/src/collections_ui.rs#L2755), [`ui/dr-ui/src/collections_ui.rs:3208`](../ui/dr-ui/src/collections_ui.rs#L3208), [`ui/dr-ui/src/collections_ui.rs:3596`](../ui/dr-ui/src/collections_ui.rs#L3596), [`ui/dr-ui/src/collections_ui.rs:3633`](../ui/dr-ui/src/collections_ui.rs#L3633), [`ui/dr-ui/src/collections_ui.rs:414`](../ui/dr-ui/src/collections_ui.rs#L414), [`ui/dr-ui/src/collections_ui.rs:427`](../ui/dr-ui/src/collections_ui.rs#L427), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:718`](../ui/dr-ui/src/collections_ui.rs#L718), [`ui/dr-ui/src/collections_ui.rs:738`](../ui/dr-ui/src/collections_ui.rs#L738), [`ui/dr-ui/src/develop.rs:5283`](../ui/dr-ui/src/develop.rs#L5283), [`ui/dr-ui/src/develop.rs:5308`](../ui/dr-ui/src/develop.rs#L5308), [`ui/dr-ui/src/develop.rs:5319`](../ui/dr-ui/src/develop.rs#L5319), [`ui/dr-ui/src/develop.rs:5333`](../ui/dr-ui/src/develop.rs#L5333), [`ui/dr-ui/src/develop.rs:6561`](../ui/dr-ui/src/develop.rs#L6561), [`ui/dr-ui/src/develop.rs:6592`](../ui/dr-ui/src/develop.rs#L6592), [`ui/dr-ui/src/develop.rs:6613`](../ui/dr-ui/src/develop.rs#L6613), [`ui/dr-ui/src/gesture_book.rs:3`](../ui/dr-ui/src/gesture_book.rs#L3), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:1862`](../ui/dr-ui/src/lib.rs#L1862), [`ui/dr-ui/src/lib.rs:2286`](../ui/dr-ui/src/lib.rs#L2286), [`ui/dr-ui/src/lib.rs:2526`](../ui/dr-ui/src/lib.rs#L2526), [`ui/dr-ui/src/lib.rs:3194`](../ui/dr-ui/src/lib.rs#L3194), [`ui/dr-ui/src/lib.rs:4108`](../ui/dr-ui/src/lib.rs#L4108), [`ui/dr-ui/src/lib.rs:454`](../ui/dr-ui/src/lib.rs#L454), [`ui/dr-ui/src/lib.rs:483`](../ui/dr-ui/src/lib.rs#L483), [`ui/dr-ui/src/library_ui.rs:5761`](../ui/dr-ui/src/library_ui.rs#L5761), [`ui/dr-ui/src/library_ui.rs:5773`](../ui/dr-ui/src/library_ui.rs#L5773), [`ui/dr-ui/src/library_ui.rs:5843`](../ui/dr-ui/src/library_ui.rs#L5843), [`ui/dr-ui/src/library_ui.rs:5953`](../ui/dr-ui/src/library_ui.rs#L5953), [`ui/dr-ui/src/library_ui.rs:5981`](../ui/dr-ui/src/library_ui.rs#L5981), [`ui/dr-ui/src/library_ui.rs:6865`](../ui/dr-ui/src/library_ui.rs#L6865), [`ui/dr-ui/ui/app.slint:168`](../ui/dr-ui/ui/app.slint#L168), [`ui/dr-ui/ui/app.slint:1984`](../ui/dr-ui/ui/app.slint#L1984), [`ui/dr-ui/ui/app.slint:2106`](../ui/dr-ui/ui/app.slint#L2106), [`ui/dr-ui/ui/app.slint:2513`](../ui/dr-ui/ui/app.slint#L2513), [`ui/dr-ui/ui/app.slint:2646`](../ui/dr-ui/ui/app.slint#L2646), [`ui/dr-ui/ui/app.slint:3117`](../ui/dr-ui/ui/app.slint#L3117), [`ui/dr-ui/ui/app.slint:564`](../ui/dr-ui/ui/app.slint#L564), [`ui/dr-ui/ui/app.slint:593`](../ui/dr-ui/ui/app.slint#L593), [`ui/dr-ui/ui/app.slint:602`](../ui/dr-ui/ui/app.slint#L602), [`ui/dr-ui/ui/app.slint:702`](../ui/dr-ui/ui/app.slint#L702), [`ui/dr-ui/ui/collections.slint:75`](../ui/dr-ui/ui/collections.slint#L75), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1053`](../ui/dr-ui/ui/library.slint#L1053), [`ui/dr-ui/ui/library.slint:1262`](../ui/dr-ui/ui/library.slint#L1262), [`ui/dr-ui/ui/library.slint:1287`](../ui/dr-ui/ui/library.slint#L1287), [`ui/dr-ui/ui/library.slint:1294`](../ui/dr-ui/ui/library.slint#L1294), [`ui/dr-ui/ui/library.slint:1327`](../ui/dr-ui/ui/library.slint#L1327), [`ui/dr-ui/ui/library.slint:1384`](../ui/dr-ui/ui/library.slint#L1384), [`ui/dr-ui/ui/library.slint:1391`](../ui/dr-ui/ui/library.slint#L1391), [`ui/dr-ui/ui/library.slint:2581`](../ui/dr-ui/ui/library.slint#L2581), [`ui/dr-ui/ui/library.slint:4179`](../ui/dr-ui/ui/library.slint#L4179), [`ui/dr-ui/ui/library.slint:4257`](../ui/dr-ui/ui/library.slint#L4257), [`ui/dr-ui/ui/library.slint:4316`](../ui/dr-ui/ui/library.slint#L4316), [`ui/dr-ui/ui/library.slint:965`](../ui/dr-ui/ui/library.slint#L965), [`ui/dr-ui/ui/library.slint:992`](../ui/dr-ui/ui/library.slint#L992), [`ui/dr-ui/ui/library.slint:998`](../ui/dr-ui/ui/library.slint#L998) | | FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:3239`](../ui/dr-ui/src/lib.rs#L3239), [`ui/dr-ui/src/lib.rs:4068`](../ui/dr-ui/src/lib.rs#L4068), [`ui/dr-ui/src/lib.rs:4259`](../ui/dr-ui/src/lib.rs#L4259), [`ui/dr-ui/src/masks_ui.rs:1518`](../ui/dr-ui/src/masks_ui.rs#L1518), [`ui/dr-ui/ui/app.slint:2473`](../ui/dr-ui/ui/app.slint#L2473), [`ui/dr-ui/ui/app.slint:2513`](../ui/dr-ui/ui/app.slint#L2513), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/session.slint:75`](../ui/dr-ui/ui/session.slint#L75) | | FR-UI-6 | [`ui/dr-ui/ui/widgets.slint:1`](../ui/dr-ui/ui/widgets.slint#L1) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:195`](../core/dr-pipeline/src/descriptor.rs#L195), [`core/dr-pipeline/src/framing.rs:395`](../core/dr-pipeline/src/framing.rs#L395), [`core/dr-types/src/settings.rs:424`](../core/dr-types/src/settings.rs#L424), [`ui/dr-ui/src/develop.rs:1330`](../ui/dr-ui/src/develop.rs#L1330), [`ui/dr-ui/src/lib.rs:3893`](../ui/dr-ui/src/lib.rs#L3893), [`ui/dr-ui/ui/adjust.slint:974`](../ui/dr-ui/ui/adjust.slint#L974), [`ui/dr-ui/ui/app.slint:2229`](../ui/dr-ui/ui/app.slint#L2229), [`ui/dr-ui/ui/settings.slint:609`](../ui/dr-ui/ui/settings.slint#L609), [`ui/dr-ui/ui/toolrail.slint:89`](../ui/dr-ui/ui/toolrail.slint#L89) | -| FR-UI-8 | [`core/dr-catalog/src/collections.rs:654`](../core/dr-catalog/src/collections.rs#L654), [`core/dr-catalog/src/collections.rs:675`](../core/dr-catalog/src/collections.rs#L675), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/derived_sync.rs:208`](../ui/dr-ui/src/derived_sync.rs#L208), [`ui/dr-ui/src/derived_sync.rs:678`](../ui/dr-ui/src/derived_sync.rs#L678), [`ui/dr-ui/src/derived_sync.rs:688`](../ui/dr-ui/src/derived_sync.rs#L688), [`ui/dr-ui/src/derived_sync.rs:74`](../ui/dr-ui/src/derived_sync.rs#L74), [`ui/dr-ui/src/derived_sync.rs:770`](../ui/dr-ui/src/derived_sync.rs#L770), [`ui/dr-ui/src/library.rs:1123`](../ui/dr-ui/src/library.rs#L1123), [`ui/dr-ui/src/library.rs:4961`](../ui/dr-ui/src/library.rs#L4961), [`ui/dr-ui/src/library.rs:5679`](../ui/dr-ui/src/library.rs#L5679), [`ui/dr-ui/src/library.rs:5745`](../ui/dr-ui/src/library.rs#L5745), [`ui/dr-ui/src/library_ui.rs:195`](../ui/dr-ui/src/library_ui.rs#L195), [`ui/dr-ui/src/library_ui.rs:2155`](../ui/dr-ui/src/library_ui.rs#L2155), [`ui/dr-ui/src/library_ui.rs:2344`](../ui/dr-ui/src/library_ui.rs#L2344), [`ui/dr-ui/src/library_ui.rs:2361`](../ui/dr-ui/src/library_ui.rs#L2361), [`ui/dr-ui/src/library_ui.rs:2459`](../ui/dr-ui/src/library_ui.rs#L2459), [`ui/dr-ui/src/library_ui.rs:5754`](../ui/dr-ui/src/library_ui.rs#L5754), [`ui/dr-ui/src/library_ui.rs:5794`](../ui/dr-ui/src/library_ui.rs#L5794), [`ui/dr-ui/src/library_ui.rs:5823`](../ui/dr-ui/src/library_ui.rs#L5823), [`ui/dr-ui/src/library_ui.rs:6086`](../ui/dr-ui/src/library_ui.rs#L6086), [`ui/dr-ui/src/library_ui.rs:6309`](../ui/dr-ui/src/library_ui.rs#L6309), [`ui/dr-ui/src/library_ui.rs:7033`](../ui/dr-ui/src/library_ui.rs#L7033), [`ui/dr-ui/src/library_ui.rs:891`](../ui/dr-ui/src/library_ui.rs#L891), [`ui/dr-ui/src/library_ui.rs:924`](../ui/dr-ui/src/library_ui.rs#L924), [`ui/dr-ui/src/library_ui.rs:970`](../ui/dr-ui/src/library_ui.rs#L970), [`ui/dr-ui/src/place.rs:1`](../ui/dr-ui/src/place.rs#L1) | +| FR-UI-8 | [`core/dr-catalog/src/collections.rs:654`](../core/dr-catalog/src/collections.rs#L654), [`core/dr-catalog/src/collections.rs:675`](../core/dr-catalog/src/collections.rs#L675), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/derived_sync.rs:216`](../ui/dr-ui/src/derived_sync.rs#L216), [`ui/dr-ui/src/derived_sync.rs:81`](../ui/dr-ui/src/derived_sync.rs#L81), [`ui/dr-ui/src/derived_sync.rs:883`](../ui/dr-ui/src/derived_sync.rs#L883), [`ui/dr-ui/src/derived_sync.rs:893`](../ui/dr-ui/src/derived_sync.rs#L893), [`ui/dr-ui/src/derived_sync.rs:975`](../ui/dr-ui/src/derived_sync.rs#L975), [`ui/dr-ui/src/library.rs:1123`](../ui/dr-ui/src/library.rs#L1123), [`ui/dr-ui/src/library.rs:4961`](../ui/dr-ui/src/library.rs#L4961), [`ui/dr-ui/src/library.rs:5679`](../ui/dr-ui/src/library.rs#L5679), [`ui/dr-ui/src/library.rs:5745`](../ui/dr-ui/src/library.rs#L5745), [`ui/dr-ui/src/library_ui.rs:195`](../ui/dr-ui/src/library_ui.rs#L195), [`ui/dr-ui/src/library_ui.rs:2155`](../ui/dr-ui/src/library_ui.rs#L2155), [`ui/dr-ui/src/library_ui.rs:2344`](../ui/dr-ui/src/library_ui.rs#L2344), [`ui/dr-ui/src/library_ui.rs:2361`](../ui/dr-ui/src/library_ui.rs#L2361), [`ui/dr-ui/src/library_ui.rs:2459`](../ui/dr-ui/src/library_ui.rs#L2459), [`ui/dr-ui/src/library_ui.rs:5794`](../ui/dr-ui/src/library_ui.rs#L5794), [`ui/dr-ui/src/library_ui.rs:5834`](../ui/dr-ui/src/library_ui.rs#L5834), [`ui/dr-ui/src/library_ui.rs:5863`](../ui/dr-ui/src/library_ui.rs#L5863), [`ui/dr-ui/src/library_ui.rs:6126`](../ui/dr-ui/src/library_ui.rs#L6126), [`ui/dr-ui/src/library_ui.rs:6349`](../ui/dr-ui/src/library_ui.rs#L6349), [`ui/dr-ui/src/library_ui.rs:7073`](../ui/dr-ui/src/library_ui.rs#L7073), [`ui/dr-ui/src/library_ui.rs:891`](../ui/dr-ui/src/library_ui.rs#L891), [`ui/dr-ui/src/library_ui.rs:924`](../ui/dr-ui/src/library_ui.rs#L924), [`ui/dr-ui/src/library_ui.rs:970`](../ui/dr-ui/src/library_ui.rs#L970), [`ui/dr-ui/src/place.rs:1`](../ui/dr-ui/src/place.rs#L1) | | NFR-A11Y-2 | [`ui/dr-ui/tests/ui_controls_are_accessible.rs:1`](../ui/dr-ui/tests/ui_controls_are_accessible.rs#L1), [`ui/dr-ui/ui/adjust.slint:171`](../ui/dr-ui/ui/adjust.slint#L171), [`ui/dr-ui/ui/app.slint:2646`](../ui/dr-ui/ui/app.slint#L2646), [`ui/dr-ui/ui/app.slint:2684`](../ui/dr-ui/ui/app.slint#L2684) | | NFR-A11Y-3 | [`ui/dr-ui/src/histogram.rs:356`](../ui/dr-ui/src/histogram.rs#L356), [`ui/dr-ui/ui/histogram.slint:137`](../ui/dr-ui/ui/histogram.slint#L137), [`ui/dr-ui/ui/library.slint:761`](../ui/dr-ui/ui/library.slint#L761), [`ui/dr-ui/ui/library.slint:909`](../ui/dr-ui/ui/library.slint#L909), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:3679`](../ui/dr-ui/src/library.rs#L3679) | @@ -150,13 +150,13 @@ _None._ | NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | | NFR-P14 | [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`ui/dr-ui/src/develop.rs:4229`](../ui/dr-ui/src/develop.rs#L4229), [`ui/dr-ui/src/lib.rs:2071`](../ui/dr-ui/src/lib.rs#L2071) | | NFR-P3 | [`tools/bench/src/thumbnails.rs:1`](../tools/bench/src/thumbnails.rs#L1) | -| NFR-P5 | [`core/dr-catalog/src/schema.rs:381`](../core/dr-catalog/src/schema.rs#L381), [`ui/dr-ui/src/library.rs:5068`](../ui/dr-ui/src/library.rs#L5068), [`ui/dr-ui/src/library.rs:6181`](../ui/dr-ui/src/library.rs#L6181), [`ui/dr-ui/src/library_ui.rs:4949`](../ui/dr-ui/src/library_ui.rs#L4949), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) | +| NFR-P5 | [`core/dr-catalog/src/schema.rs:381`](../core/dr-catalog/src/schema.rs#L381), [`ui/dr-ui/src/library.rs:5068`](../ui/dr-ui/src/library.rs#L5068), [`ui/dr-ui/src/library.rs:6181`](../ui/dr-ui/src/library.rs#L6181), [`ui/dr-ui/src/library_ui.rs:4989`](../ui/dr-ui/src/library_ui.rs#L4989), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) | | NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:992`](../ui/dr-ui/src/export.rs#L992), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) | | NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:272`](../core/dr-types/src/lib.rs#L272), [`core/dr-types/src/lib.rs:305`](../core/dr-types/src/lib.rs#L305), [`platform/dr-plat/src/dirs.rs:1`](../platform/dr-plat/src/dirs.rs#L1), [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) | | NFR-PORT-2 | [`core/dr-gpu/src/lib.rs:1`](../core/dr-gpu/src/lib.rs#L1) | | NFR-PORT-3 | [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1) | | NFR-R1 | [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-gpu/src/lib.rs:192`](../core/dr-gpu/src/lib.rs#L192), [`core/dr-sync/src/account.rs:226`](../core/dr-sync/src/account.rs#L226), [`ui/dr-ui/src/library.rs:1170`](../ui/dr-ui/src/library.rs#L1170) | -| NFR-R2 | [`core/dr-catalog/src/recovery.rs:1`](../core/dr-catalog/src/recovery.rs#L1), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1) | +| NFR-R2 | [`core/dr-catalog/src/recovery.rs:1`](../core/dr-catalog/src/recovery.rs#L1), [`core/dr-catalog/src/recovery.rs:222`](../core/dr-catalog/src/recovery.rs#L222), [`core/dr-catalog/src/sync.rs:61`](../core/dr-catalog/src/sync.rs#L61), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`ui/dr-ui/src/derived_sync.rs:602`](../ui/dr-ui/src/derived_sync.rs#L602), [`ui/dr-ui/src/derived_sync.rs:725`](../ui/dr-ui/src/derived_sync.rs#L725), [`ui/dr-ui/src/library_ui.rs:4158`](../ui/dr-ui/src/library_ui.rs#L4158), [`ui/dr-ui/src/library_ui.rs:4476`](../ui/dr-ui/src/library_ui.rs#L4476) | | NFR-R4 | [`core/dr-types/src/settings.rs:92`](../core/dr-types/src/settings.rs#L92), [`ui/dr-ui/src/library.rs:1563`](../ui/dr-ui/src/library.rs#L1563), [`ui/dr-ui/src/library.rs:1579`](../ui/dr-ui/src/library.rs#L1579), [`ui/dr-ui/src/library_ui.rs:3411`](../ui/dr-ui/src/library_ui.rs#L3411), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:519`](../ui/dr-ui/src/library_ui.rs#L519), [`ui/dr-ui/src/settings_ui.rs:506`](../ui/dr-ui/src/settings_ui.rs#L506), [`ui/dr-ui/src/xmp_sync.rs:1`](../ui/dr-ui/src/xmp_sync.rs#L1), [`ui/dr-ui/ui/settings.slint:1001`](../ui/dr-ui/ui/settings.slint#L1001), [`ui/dr-ui/ui/settings.slint:184`](../ui/dr-ui/ui/settings.slint#L184) | | NFR-R5 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1) | | NFR-R6 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/lib.rs:255`](../core/dr-catalog/src/lib.rs#L255), [`core/dr-catalog/src/recovery.rs:1`](../core/dr-catalog/src/recovery.rs#L1) | diff --git a/ui/dr-ui/src/derived_sync.rs b/ui/dr-ui/src/derived_sync.rs index 2efd6b4..9bcd91c 100644 --- a/ui/dr-ui/src/derived_sync.rs +++ b/ui/dr-ui/src/derived_sync.rs @@ -52,6 +52,13 @@ pub struct SyncReport { pub thumbnails_adopted: usize, pub catalog_uploaded: bool, pub catalog_merged: bool, + /// The copy on the server was damaged beyond reading, and ours replaced it. + /// + /// Counted because it is the one outcome here that *destroys* something. A + /// sync that silently overwrote a peer's collections would be the bug this + /// whole path is written to avoid, so when it does happen the report has to + /// be able to say so rather than looking like an ordinary push. + pub catalog_replaced: bool, pub collections_gained: usize, /// Membership rows the merge brought in (FR-CAT-7). /// @@ -88,6 +95,7 @@ impl SyncReport { || self.shards_downloaded > 0 || self.catalog_uploaded || self.catalog_merged + || self.catalog_replaced || self.face_shards_uploaded > 0 || self.face_shards_downloaded > 0 || self.place_adopted @@ -583,11 +591,28 @@ fn legacy_upload_of_ours(store: &ThumbStore, id: u32, remote_size: u64) -> bool .unwrap_or(false) } -/// Exchange the catalog, for its collections. +/// Exchange the catalog, for its collections and its people. /// -/// Only collections merge — see [`dr_catalog::sync`]. The rest of a catalog -/// describes local state (folder ETags, cache paths, job rows) and importing -/// another device's version would be actively wrong. +/// Only collections, keywords and people merge — see [`dr_catalog::sync`]. The +/// rest of a catalog describes local state (folder ETags, cache paths, job +/// rows) and importing another device's version would be actively wrong. +/// +/// # Generations +/// +/// TRACES: FR-NC-9 | NFR-R2 +/// The server keeps the current copy and the [`GENERATIONS`] before it: +/// `catalog.sqlite`, then `catalog.1.sqlite` (the one it replaced), `.2`, +/// `.3`. A push uploads to a temporary name, rotates, and moves the upload into +/// place — so at no moment is there no current copy, and a copy that turns out +/// damaged has the one before it to fall back on. Rotation is server-side +/// renames; the only transfer is the upload itself. +/// +/// This is what a damaged copy used to lack. With one copy and nothing behind +/// it, "the current file will not open" left two answers, both bad: refuse for +/// ever, or overwrite with ours and lose whatever another device had added +/// since. Now it has a third — merge from the newest readable generation, which +/// loses nothing — and the damaged file itself is kept as `.1` by the ordinary +/// rotation rather than by a separate upload. async fn sync_catalog( backend: &dyn RemoteBackend, base: &RemotePath, @@ -595,7 +620,7 @@ async fn sync_catalog( scratch: &Path, report: &mut SyncReport, ) -> Result<(), String> { - let remote_name = "catalog.sqlite"; + let remote_name = CATALOG_NAME; let target = RemotePath::new(format!("{}/{remote_name}", base.as_str())); // ---- take theirs first ----------------------------------------------- @@ -627,30 +652,48 @@ async fn sync_catalog( }; if let Some(bytes) = theirs { - let downloaded = scratch.join("catalog-remote.sqlite"); - if std::fs::write(&downloaded, &bytes).is_ok() { - match dr_catalog::Catalog::open(catalog_path) { - Ok(catalog) => match catalog.merge_remote_catalog(&downloaded) { - Ok(merge) => { - report.catalog_merged = true; - report.collections_gained = merge.inserted + merge.updated; - report.members_gained = merge.members_added; - } - // Unreadable is not the same as absent: it may be a newer - // format, or a torn upload. Ours must not go over it. - Err(e) => { - log::warn!("not pushing the catalog: merging the server's copy: {e}"); - let _ = std::fs::remove_file(&downloaded); - return Ok(()); - } - }, - Err(e) => { - log::warn!("not pushing the catalog: opening ours to merge: {e}"); - let _ = std::fs::remove_file(&downloaded); + let catalog = match dr_catalog::Catalog::open(catalog_path) { + Ok(c) => c, + Err(e) => { + log::warn!("not pushing the catalog: opening ours to merge: {e}"); + return Ok(()); + } + }; + match merge_downloaded(&catalog, scratch, &bytes, report) { + Ok(()) => {} + // TRACES: FR-NC-9 + // Damaged beyond reading, and the whole file arrived — so it is + // not a short download, and no device will read it either. Fall + // back to the generation before it; only with none readable is + // ours the whole truth. See the header for why refusing here for + // ever was the wrong answer. + Err(dr_catalog::CatalogError::Corrupt { detail }) => { + if !arrived_whole(backend, base, remote_name, bytes.len(), &detail).await { return Ok(()); } + match merge_from_generations(backend, base, &catalog, scratch, report).await { + Some(n) => log::warn!( + "the catalog on the server is damaged ({detail}) and all {} bytes of \ + it arrived, so no device can read it; merged from the generation \ + before it ({}) instead, and replacing it", + bytes.len(), + generation_name(n) + ), + None => log::warn!( + "the catalog on the server is damaged ({detail}) and all {} bytes of \ + it arrived, so no device can read it, and no earlier generation is \ + readable either; replacing it with this device's copy", + bytes.len() + ), + } + report.catalog_replaced = true; + } + // Unreadable is not the same as absent: it may be a newer + // format. Ours must not go over it. + Err(e) => { + log::warn!("not pushing the catalog: merging the server's copy: {e}"); + return Ok(()); } - let _ = std::fs::remove_file(&downloaded); } } @@ -666,15 +709,177 @@ async fn sync_catalog( .map_err(|e| e.to_string())?; let bytes = std::fs::read(&snapshot).map_err(|e| e.to_string())?; - match backend.put(&target, bytes, None).await { - Ok(_) => report.catalog_uploaded = true, - Err(e) => log::warn!("uploading catalog: {e}"), - } let _ = std::fs::remove_file(&snapshot); + // To a temporary name first. The upload is the only step that can fail + // half-way, and a half-uploaded *current* copy is exactly the damaged file + // this whole scheme exists to survive. Under its own name a failure leaves + // the current copy untouched and costs one stray file, retried next pass. + let staging = RemotePath::new(format!("{}/{UPLOAD_NAME}", base.as_str())); + let sent = bytes.len() as u64; + if let Err(e) = backend.put(&staging, bytes, None).await { + log::warn!("uploading catalog: {e}"); + return Ok(()); + } + + // TRACES: NFR-R2 + // Confirm the server holds what was sent before it becomes the copy every + // other device reads. A chunked upload is assembled server-side, and an + // assembly that went wrong is a file of plausible size that no device can + // open — the one failure the generations exist to survive, and cheaper + // to catch here, on the device that caused it, than on every other one + // after. One listing; the size is what the server can vouch for without + // reading the file back. + match remote_size(backend, base, UPLOAD_NAME).await { + Some(held) if held == sent => {} + Some(held) => { + log::warn!( + "not replacing the catalog: sent {sent} bytes but the server holds {held}; \ + the upload is discarded and retried next pass" + ); + let _ = backend.delete(&RemoteId::Path(staging), None).await; + return Ok(()); + } + None => { + log::warn!( + "not replacing the catalog: the server would not confirm the upload's size; \ + it is discarded and retried next pass" + ); + let _ = backend.delete(&RemoteId::Path(staging), None).await; + return Ok(()); + } + } + + // Rotate, then move the upload into place. Every step here is a rename on + // the server, and every destination is empty by the time it is written to + // — a `move_to` will not overwrite, by design — so a failure at any point + // leaves a gap in the generations and never a missing current copy. + if let Err(e) = rotate_generations(backend, base).await { + log::warn!("not replacing the catalog: rotating the earlier copies: {e}"); + return Ok(()); + } + match backend.move_to(&RemoteId::Path(staging), &target).await { + Ok(()) => report.catalog_uploaded = true, + Err(e) => log::warn!("moving the uploaded catalog into place: {e}"), + } + Ok(()) } +/// The current copy's name on the server. +const CATALOG_NAME: &str = "catalog.sqlite"; + +/// Where a push lands before it is rotated into place. +const UPLOAD_NAME: &str = "catalog.upload.sqlite"; + +/// How many earlier copies the server keeps behind the current one. +/// +/// Three, because what they are for is surviving one damaged push and the one +/// or two syncs it may take for a device to notice. More would cost nothing in +/// transfer — rotation is renames — but each is a 40 MB file on the account's +/// quota, and the local backups (NFR-R2) are the long-term store. +const GENERATIONS: usize = 3; + +/// `catalog.N.sqlite` for `1 <= N <= GENERATIONS`; `.1` is the newest. +fn generation_name(n: usize) -> String { + format!("catalog.{n}.sqlite") +} + +/// Merge a downloaded catalog into ours, through a file in scratch. +/// +/// Attaching needs a path, and the download is bytes. Written and removed here +/// so the callers — the current copy, and each generation tried after it — +/// cannot disagree about cleanup. +fn merge_downloaded( + catalog: &dr_catalog::Catalog, + scratch: &Path, + bytes: &[u8], + report: &mut SyncReport, +) -> Result<(), dr_catalog::CatalogError> { + let downloaded = scratch.join("catalog-remote.sqlite"); + std::fs::write(&downloaded, bytes).map_err(|e| dr_catalog::CatalogError::Io(e.to_string()))?; + let result = catalog.merge_remote_catalog(&downloaded); + let _ = std::fs::remove_file(&downloaded); + let merge = result?; + report.catalog_merged = true; + report.collections_gained += merge.inserted + merge.updated; + report.members_gained += merge.members_added; + Ok(()) +} + +/// TRACES: FR-NC-9 +/// Merge from the newest generation that reads, when the current copy will +/// not. Returns which one, or `None` when none of them does. +/// +/// Newest first, and the first readable one wins: a generation is a complete +/// snapshot, so an older one adds nothing a newer one lacks. A generation that +/// is absent, damaged, or from a newer schema is skipped the same way — none of +/// those is a reason to stop looking further back. +async fn merge_from_generations( + backend: &dyn RemoteBackend, + base: &RemotePath, + catalog: &dr_catalog::Catalog, + scratch: &Path, + report: &mut SyncReport, +) -> Option { + for n in 1..=GENERATIONS { + let path = RemotePath::new(format!("{}/{}", base.as_str(), generation_name(n))); + let bytes = match read_derived(backend, &path).await { + Ok(b) => b, + Err(RemoteError::NotFound(_)) => continue, + Err(e) => { + log::debug!("skipping {}: {e}", generation_name(n)); + continue; + } + }; + match merge_downloaded(catalog, scratch, &bytes, report) { + Ok(()) => return Some(n), + Err(e) => log::debug!("skipping {}: {e}", generation_name(n)), + } + } + None +} + +/// Make room for a new current copy: drop the oldest generation and shift the +/// rest back by one, ending with the current copy as `.1`. +/// +/// Oldest first, so that each destination is empty when it is moved into — +/// `move_to` refuses to overwrite, and rightly. A name that is not there is +/// not an error at any step: a library that has synced twice has no `.3` yet. +async fn rotate_generations(backend: &dyn RemoteBackend, base: &RemotePath) -> Result<(), String> { + let at = |name: String| RemotePath::new(format!("{}/{name}", base.as_str())); + + match backend + .delete(&RemoteId::Path(at(generation_name(GENERATIONS))), None) + .await + { + Ok(()) | Err(RemoteError::NotFound(_)) => {} + Err(e) => return Err(format!("dropping {}: {e}", generation_name(GENERATIONS))), + } + for n in (1..GENERATIONS).rev() { + match backend + .move_to( + &RemoteId::Path(at(generation_name(n))), + &at(generation_name(n + 1)), + ) + .await + { + Ok(()) | Err(RemoteError::NotFound(_)) => {} + Err(e) => return Err(format!("moving {} back: {e}", generation_name(n))), + } + } + match backend + .move_to( + &RemoteId::Path(at(CATALOG_NAME.into())), + &at(generation_name(1)), + ) + .await + { + Ok(()) | Err(RemoteError::NotFound(_)) => Ok(()), + Err(e) => Err(format!("setting the current copy back: {e}")), + } +} + /// TRACES: FR-UI-8 /// The place's name inside the derived folder. /// @@ -851,6 +1056,64 @@ async fn read_derived( } } +/// TRACES: FR-NC-9 +/// Whether a download that will not open is the server's whole file. +/// +/// A truncated download is also unreadable, and on a phone it is the far +/// likelier story — this library's logs are full of aborted bodies and DNS +/// failures. Treating that as damage would let one bad connection discard a +/// catalog the server was holding perfectly well. So the size the server +/// advertises is compared against what actually arrived, and anything short, +/// or any size the listing cannot confirm, is the transport failure it is: +/// the caller must leave the server's copy alone. +async fn arrived_whole( + backend: &dyn RemoteBackend, + base: &RemotePath, + name: &str, + received: usize, + detail: &str, +) -> bool { + let Some(advertised) = remote_size(backend, base, name).await else { + log::warn!( + "not pushing the catalog: the copy on the server will not open ({detail}), \ + but its size could not be confirmed, so it may simply have arrived short" + ); + return false; + }; + if advertised != received as u64 { + log::warn!( + "not pushing the catalog: the copy on the server will not open ({detail}), \ + but only {received} of {advertised} bytes arrived — that is a truncated \ + download, not a damaged file, so the server's copy is left alone" + ); + return false; + } + true +} + +/// The size the server says an entry in the derived folder has. +/// +/// One listing of a folder that holds a handful of files, rather than a HEAD +/// the backend trait does not offer. `None` covers "the listing failed", "it is +/// not there", and "the size it reports means nothing" alike, and the caller +/// treats all of them as not knowing — which is the answer that declines to +/// overwrite. +/// +/// A placeholder is excluded rather than trusted: `RemoteEntry::size` is +/// explicitly not meaningful when `materialised` is false — a suffix-mode stub +/// is one byte and carries no record of what it stands for — so comparing a +/// download against it would be comparing against nothing. +async fn remote_size(backend: &dyn RemoteBackend, base: &RemotePath, name: &str) -> Option { + backend + .list(base, None) + .await + .ok()? + .into_iter() + .find(|e| e.kind == dr_sync::EntryKind::File && e.path.name() == name) + .filter(|e| e.materialised) + .map(|e| e.size) +} + fn shard_name(client: &str, id: u32) -> String { format!("shard-{client}-{id:04}.sqlite") } @@ -979,6 +1242,19 @@ mod derived_guard_tests { /// record went up rather than only that one did. last_put: Arc>>, caps: dr_sync::Capabilities, + /// The size `list` claims `catalog.sqlite` has, if it lists it at all. + /// This is what says whether a body that will not open is a damaged + /// file or merely a short download. + advertise: Option, + /// Bodies by file name, for tests that need the current copy and a + /// generation to differ. When empty every read serves `body`; when + /// not, a name absent here reads as `NotFound`. + bodies: std::collections::HashMap>, + /// Every `move_to`, as (from, to) names, in order. + moves: Arc>>, + /// What `list` claims the staged upload's size is, when a test wants + /// the server to have assembled it wrongly. `None` reports the truth. + staged_size: Option, } impl Fussy { @@ -1000,6 +1276,10 @@ mod derived_guard_tests { puts: puts.clone(), last_put: last_put.clone(), caps: dr_sync::Capabilities::minimal(), + advertise: None, + bodies: Default::default(), + moves: Default::default(), + staged_size: None, }, puts, last_put, @@ -1017,10 +1297,36 @@ mod derived_guard_tests { } async fn list( &self, - _dir: &RemotePath, + dir: &RemotePath, _since: Option<&dr_sync::Validator>, ) -> Result, RemoteError> { - Ok(Vec::new()) + let entry = |name: &str, size: u64| { + let path = RemotePath::new(format!("{}/{name}", dir.as_str())); + dr_sync::RemoteEntry { + id: RemoteId::Path(path.clone()), + path, + kind: dr_sync::EntryKind::File, + validator: dr_sync::Validator::new("v"), + size, + modified: None, + has_preview: false, + materialised: true, + } + }; + let mut out = Vec::new(); + if let Some(size) = self.advertise { + out.push(entry(CATALOG_NAME, size)); + } + // The staged upload lists at the size of what was last put, as a + // server that assembled it correctly would report — unless a test + // says the assembly went wrong. + if self.puts.load(Ordering::SeqCst) > 0 { + let held = self + .staged_size + .unwrap_or(self.last_put.lock().unwrap().len() as u64); + out.push(entry(UPLOAD_NAME, held)); + } + Ok(out) } async fn dir_validator( &self, @@ -1036,7 +1342,7 @@ mod derived_guard_tests { } async fn get( &self, - _id: &RemoteId, + id: &RemoteId, _r: Option>, ) -> Result, RemoteError> { match &self.fail_with { @@ -1045,7 +1351,17 @@ mod derived_guard_tests { Err(RemoteError::NotMaterialised(s.clone())) } Some(_) => Err(RemoteError::PermissionDenied), - None => Ok(self.body.clone()), + None if self.bodies.is_empty() => Ok(self.body.clone()), + None => { + let name = match id { + RemoteId::Path(p) => p.name().to_string(), + _ => String::new(), + }; + self.bodies + .get(&name) + .cloned() + .ok_or(RemoteError::NotFound(name)) + } } } async fn put( @@ -1065,7 +1381,15 @@ mod derived_guard_tests { ) -> Result<(), RemoteError> { Ok(()) } - async fn move_to(&self, _f: &RemoteId, _t: &RemotePath) -> Result<(), RemoteError> { + async fn move_to(&self, f: &RemoteId, t: &RemotePath) -> Result<(), RemoteError> { + let from = match f { + RemoteId::Path(p) => p.name().to_string(), + _ => String::new(), + }; + self.moves + .lock() + .unwrap() + .push((from, t.name().to_string())); Ok(()) } async fn create_dir(&self, _p: &RemotePath) -> Result<(), RemoteError> { @@ -1135,6 +1459,204 @@ mod derived_guard_tests { assert!(report.catalog_uploaded); } + // --- a damaged copy on the server (FR-NC-9) -------------------------- + // + // The one exception to everything above. A file SQLite calls malformed is + // not a read that failed; it is a file no device will ever read again, and + // leaving it alone pins it there for every client at once. + + /// The bytes of a catalog holding one collection, as a peer would upload. + fn a_catalog_with(collection: &str, name: &str) -> Vec { + let dir = std::env::temp_dir().join(format!("dr-generation-{name}")); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("catalog.sqlite"); + let cat = dr_catalog::Catalog::open(&path).unwrap(); + cat.connection() + .execute( + "INSERT INTO collections(uuid, name, kind, created, revision, modified) + VALUES (?1, ?2, 0, 0, 1, 1)", + rusqlite::params![format!("uuid-{collection}"), collection], + ) + .unwrap(); + let snap = dir.join("snap.sqlite"); + cat.snapshot_for_upload(&snap).unwrap(); + let bytes = std::fs::read(&snap).unwrap(); + let _ = std::fs::remove_dir_all(&dir); + bytes + } + + /// A body that is definitely not a SQLite database as the current copy, + /// with a chosen advertised size and whatever generations the test wants. + async fn corrupt_remote( + body_len: usize, + advertised: Option, + generations: &[(usize, Vec)], + name: &str, + ) -> (usize, SyncReport, Vec<(String, String)>) { + let (catalog_path, scratch) = fixture(name); + let (mut backend, puts, _) = Fussy::serving(None, Vec::new()); + backend.advertise = advertised; + backend + .bodies + .insert(CATALOG_NAME.to_string(), vec![0xAB; body_len]); + for (n, bytes) in generations { + backend.bodies.insert(generation_name(*n), bytes.clone()); + } + let moves = backend.moves.clone(); + let mut report = SyncReport::default(); + sync_catalog( + &backend, + &RemotePath::new(".darkroom-derived"), + &catalog_path, + &scratch, + &mut report, + ) + .await + .unwrap(); + let moves = moves.lock().unwrap().clone(); + (puts.load(Ordering::SeqCst), report, moves) + } + + #[tokio::test] + async fn a_damaged_catalog_that_arrived_whole_is_replaced() { + // The deadlock this exists to break: every device downloads the same + // unreadable file, every device declines to overwrite it, and + // collections and people stop crossing between devices for ever. + let (puts, report, moves) = corrupt_remote(64, Some(64), &[], "corrupt-whole").await; + assert_eq!(puts, 1, "ours goes up, to the staging name"); + assert!(report.catalog_replaced, "and the report says what happened"); + assert!(report.catalog_uploaded); + assert!( + !report.catalog_merged, + "there was nothing readable to merge" + ); + // The damaged file is kept by the rotation, not thrown away. + assert!(moves.contains(&(CATALOG_NAME.into(), generation_name(1)))); + assert_eq!( + moves.last().unwrap(), + &(UPLOAD_NAME.to_string(), CATALOG_NAME.to_string()), + "and the upload is moved into place last" + ); + } + + #[tokio::test] + async fn a_damaged_catalog_falls_back_to_the_generation_before_it() { + // What generations are for. The device that pushed the damaged copy + // may have been the only one holding some collection; the generation + // before it still has everything every device had agreed on. + let older = a_catalog_with("Iceland", "gen1"); + let (puts, report, _) = + corrupt_remote(64, Some(64), &[(1, older)], "corrupt-with-gen").await; + assert!(report.catalog_merged, "merged from catalog.1.sqlite"); + assert_eq!(report.collections_gained, 1, "and gained what it held"); + assert!(report.catalog_replaced); + assert_eq!(puts, 1); + } + + #[tokio::test] + async fn a_damaged_generation_is_skipped_for_the_one_behind_it() { + // Two bad pushes in a row must not be worse than one. + let older = a_catalog_with("Faroe", "gen2"); + let (_, report, _) = corrupt_remote( + 64, + Some(64), + &[(1, vec![0xCD; 64]), (2, older)], + "corrupt-two-deep", + ) + .await; + assert!(report.catalog_merged); + assert_eq!(report.collections_gained, 1); + } + + #[tokio::test] + async fn a_short_download_is_a_truncated_transfer_not_a_damaged_file() { + // The failure that matters most to get right. A phone on a flaky link + // aborts bodies constantly, and a partial download will not open + // either — treating that as damage would let one bad connection + // destroy a catalog the server was holding perfectly well. + let (puts, report, moves) = corrupt_remote(64, Some(4096), &[], "corrupt-short").await; + assert_eq!(puts, 0, "nothing is written over a copy that arrived short"); + assert!(moves.is_empty(), "and nothing is rotated"); + assert!(!report.catalog_replaced); + assert!(!report.catalog_uploaded); + } + + #[tokio::test] + async fn a_size_the_server_will_not_confirm_leaves_the_copy_alone() { + // Not knowing is not the same as knowing it is whole. Without a size + // to compare against there is no way to tell damage from truncation, + // and the answer to "I cannot tell" has to stay "do not overwrite". + let (puts, report, _) = corrupt_remote(64, None, &[], "corrupt-unconfirmed").await; + assert_eq!(puts, 0); + assert!(!report.catalog_replaced); + } + + #[tokio::test] + async fn a_push_rotates_oldest_first_and_lands_last() { + // The order is the safety: every destination is empty when it is + // moved into, so a failure at any step leaves a gap and never a + // missing current copy. + let (puts, report, moves) = + run_with_moves(Some(RemoteError::NotFound("nope".into())), "rotation").await; + assert_eq!(puts, 1); + assert!(report.catalog_uploaded); + assert_eq!( + moves, + vec![ + (generation_name(2), generation_name(3)), + (generation_name(1), generation_name(2)), + (CATALOG_NAME.to_string(), generation_name(1)), + (UPLOAD_NAME.to_string(), CATALOG_NAME.to_string()), + ] + ); + } + + #[tokio::test] + async fn an_upload_the_server_holds_at_the_wrong_size_is_not_rotated_in() { + // The assembly went wrong on the server. Rotating that into place would + // hand every other device the damaged file the generations exist to + // survive; discarding it costs one retry. + let (catalog_path, scratch) = fixture("wrong-size"); + let (mut backend, puts) = Fussy::reading(Some(RemoteError::NotFound("nope".into()))); + backend.staged_size = Some(7); + let moves = backend.moves.clone(); + let mut report = SyncReport::default(); + sync_catalog( + &backend, + &RemotePath::new(".darkroom-derived"), + &catalog_path, + &scratch, + &mut report, + ) + .await + .unwrap(); + assert_eq!(puts.load(Ordering::SeqCst), 1, "it was uploaded"); + assert!(!report.catalog_uploaded, "but not accepted"); + assert!(moves.lock().unwrap().is_empty(), "and nothing was rotated"); + } + + async fn run_with_moves( + fail_with: Option, + name: &str, + ) -> (usize, SyncReport, Vec<(String, String)>) { + let (catalog_path, scratch) = fixture(name); + let (backend, puts) = Fussy::reading(fail_with); + let moves = backend.moves.clone(); + let mut report = SyncReport::default(); + sync_catalog( + &backend, + &RemotePath::new(".darkroom-derived"), + &catalog_path, + &scratch, + &mut report, + ) + .await + .unwrap(); + let moves = moves.lock().unwrap().clone(); + (puts.load(Ordering::SeqCst), report, moves) + } + // --- the place (FR-UI-8) --------------------------------------------- // // The same "do not write over what you could not read" rule as above, for a diff --git a/ui/dr-ui/src/library_ui.rs b/ui/dr-ui/src/library_ui.rs index a369c83..63f2ee1 100644 --- a/ui/dr-ui/src/library_ui.rs +++ b/ui/dr-ui/src/library_ui.rs @@ -4155,6 +4155,34 @@ fn apply_zoom(window: &AppWindow, ctl: &Rc, delta: i32) { refresh_timeline(window, catalog, ctl); } +/// TRACES: NFR-R2 +/// Take the day's catalog backup if one is due, off the UI thread. +/// +/// Decided cheaply first — [`dr_catalog::recovery::backup_due`] reads a +/// directory listing, not the catalog — so the ordinary case of "backed up +/// this morning already" costs no thread and no connection. +fn spawn_scheduled_backup(ctl: &Rc) { + let Some((conn, _)) = ctl.session.borrow().clone() else { + return; + }; + let catalog_path = library::catalog_path(&conn.account); + if !dr_catalog::recovery::backup_due(&catalog_path) { + return; + } + std::thread::spawn(move || { + let catalog = match dr_catalog::Catalog::open(&catalog_path) { + Ok(c) => c, + Err(e) => { + log::warn!("scheduled backup: opening the catalog: {e}"); + return; + } + }; + if let Err(e) = dr_catalog::recovery::backup_if_due(catalog.connection(), &catalog_path) { + log::warn!("scheduled backup: {e}"); + } + }); +} + /// Push shards and the catalog to the server, and take what it has. /// /// Fired after the sweep completes, when there is a finished index worth @@ -4286,10 +4314,14 @@ fn start_derived_sync(window: &AppWindow, ctl: &Rc) { report.shards_uploaded, report.shards_downloaded, report.thumbnails_adopted, - if report.catalog_uploaded { - "pushed" - } else { - "not pushed" + // Replacing a damaged copy is said apart from an + // ordinary push: it is the one push that discarded + // something, and a log line that called it "pushed" + // would hide the only moment worth going back to. + match (report.catalog_uploaded, report.catalog_replaced) { + (true, true) => "pushed over a damaged copy", + (true, false) => "pushed", + _ => "not pushed", }, if report.collections_gained > 0 { format!(", {} collection(s) gained", report.collections_gained) @@ -4441,6 +4473,14 @@ fn start_sweep(window: &AppWindow, ctl: &Rc) { refresh_timeline(&w, catalog, &ctl_cb); } } + // TRACES: NFR-R2 + // The scheduled backup, at the moment the catalog is + // quietest and a day's edits have just been folded in. + // On its own thread, with its own connection: a copy + // of a 130 MB file is a second or two the Slint loop + // must not spend, and it runs beside the sync below, + // which is only a reader of the same file. + spawn_scheduled_backup(&ctl_cb); // Now that indexing is complete, hand the result to the // server so a second device inherits it rather than // repeating hours of range fetches.