From e0e193efb4dd18302393b40d7e34ed1cdd4744c2 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 19 Sep 2026 23:12:42 +0200 Subject: [PATCH] Do not recount face coverage on every confirm, and count it without listing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every click on the Identity screen's face grid — confirm, reject, split, rename, merge — redrew the whole screen, and the redraw recomputed the coverage line. That line lists every repair's outstanding images to count them: six scans of the images table with a correlated EXISTS over the 8 KB face rows, an ORDER BY the job's visiting order, a Target with its path per row, and a thumbnail-index query per image with faces. On the reference library (24k images, 19k faces) that was ~200 ms of the ~540 ms each click cost, spent computing a figure a confirm cannot change. `refresh` now takes what changed: `Changed::Identities` re-reads the rail and the grid and leaves the coverage line alone; `Changed::Library` — an open, a sweep ending or stopped, the face data deleted — re-reads it too. For the times it does run, `repairs::counts` counts instead of building and dropping the lists, and the thumbnail store is read once (`ThumbStore::held`) rather than probed once per image in the audit, the outstanding list and the proxy repair. `identity_bench` is the measurement: the reads a click performs and the batch writes, timed against a copy of a real catalog. --- core/dr-thumbs/src/lib.rs | 17 ++++ docs/traceability.md | 18 ++-- ui/dr-ui/examples/identity_bench.rs | 147 ++++++++++++++++++++++++++++ ui/dr-ui/src/faces.rs | 19 +++- ui/dr-ui/src/identity_ui.rs | 56 ++++++++--- ui/dr-ui/src/repairs.rs | 62 +++++++++++- 6 files changed, 291 insertions(+), 28 deletions(-) create mode 100644 ui/dr-ui/examples/identity_bench.rs diff --git a/core/dr-thumbs/src/lib.rs b/core/dr-thumbs/src/lib.rs index 6b77960..0a36e0e 100644 --- a/core/dr-thumbs/src/lib.rs +++ b/core/dr-thumbs/src/lib.rs @@ -240,6 +240,23 @@ impl ThumbStore { .collect() } + /// Every file id stored at one size, read from the index in one query. + /// + /// For a pass that asks about thousands of images at once — the face + /// audit, the repair lists. Each [`contains`](Self::contains) is a + /// prepared statement and a b-tree probe; asked ten thousand times over a + /// scan it costs more than the scan does, where one walk of the index is + /// a few milliseconds and answers every row. + pub fn held(&self, size: ThumbSize) -> Result, ThumbError> { + let mut stmt = self + .index + .prepare("SELECT file_id FROM entries WHERE size = ?1")?; + let ids = stmt + .query_map([size as i64], |r| r.get::<_, i64>(0))? + .collect::, _>>()?; + Ok(ids.into_iter().map(|id| id as u64).collect()) + } + /// Store a thumbnail, opening a new shard if the active one is full. /// /// Re-storing an existing id overwrites in place rather than migrating it diff --git a/docs/traceability.md b/docs/traceability.md index d9362d3..297b91f 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,7 +9,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 390 | +| Source files scanned | 391 | | TRACES tags found | 1567 | | Requirements defined | 184 | | Requirements deferred (post-v1) | 24 | @@ -39,7 +39,7 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc | 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:1559`](../ui/dr-ui/src/lib.rs#L1559), [`ui/dr-ui/src/library.rs:183`](../ui/dr-ui/src/library.rs#L183), [`ui/dr-ui/src/library.rs:3396`](../ui/dr-ui/src/library.rs#L3396), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137) | | FR-CAT-13 | [`core/dr-catalog/src/rating.rs:278`](../core/dr-catalog/src/rating.rs#L278), [`core/dr-catalog/src/schema.rs:740`](../core/dr-catalog/src/schema.rs#L740), [`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:1564`](../ui/dr-ui/src/library.rs#L1564), [`ui/dr-ui/src/library.rs:1627`](../ui/dr-ui/src/library.rs#L1627), [`ui/dr-ui/src/library.rs:1643`](../ui/dr-ui/src/library.rs#L1643), [`ui/dr-ui/src/library.rs:1750`](../ui/dr-ui/src/library.rs#L1750), [`ui/dr-ui/src/library_ui.rs:1322`](../ui/dr-ui/src/library_ui.rs#L1322), [`ui/dr-ui/src/library_ui.rs:301`](../ui/dr-ui/src/library_ui.rs#L301), [`ui/dr-ui/src/library_ui.rs:3427`](../ui/dr-ui/src/library_ui.rs#L3427), [`ui/dr-ui/src/library_ui.rs:3483`](../ui/dr-ui/src/library_ui.rs#L3483), [`ui/dr-ui/src/library_ui.rs:3581`](../ui/dr-ui/src/library_ui.rs#L3581), [`ui/dr-ui/src/library_ui.rs:392`](../ui/dr-ui/src/library_ui.rs#L392), [`ui/dr-ui/src/library_ui.rs:537`](../ui/dr-ui/src/library_ui.rs#L537), [`ui/dr-ui/src/library_ui.rs:5766`](../ui/dr-ui/src/library_ui.rs#L5766), [`ui/dr-ui/src/settings_ui.rs:540`](../ui/dr-ui/src/settings_ui.rs#L540), [`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:900`](../ui/dr-ui/ui/app.slint#L900), [`ui/dr-ui/ui/settings.slint:1093`](../ui/dr-ui/ui/settings.slint#L1093), [`ui/dr-ui/ui/settings.slint:199`](../ui/dr-ui/ui/settings.slint#L199) | -| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:1076`](../core/dr-catalog/src/schema.rs#L1076), [`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:4592`](../ui/dr-ui/src/library.rs#L4592), [`ui/dr-ui/src/library.rs:4626`](../ui/dr-ui/src/library.rs#L4626), [`ui/dr-ui/src/library_ui.rs:238`](../ui/dr-ui/src/library_ui.rs#L238), [`ui/dr-ui/src/library_ui.rs:914`](../ui/dr-ui/src/library_ui.rs#L914), [`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-15 | [`core/dr-catalog/src/schema.rs:1076`](../core/dr-catalog/src/schema.rs#L1076), [`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:393`](../core/dr-thumbs/src/lib.rs#L393), [`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:4592`](../ui/dr-ui/src/library.rs#L4592), [`ui/dr-ui/src/library.rs:4626`](../ui/dr-ui/src/library.rs#L4626), [`ui/dr-ui/src/library_ui.rs:238`](../ui/dr-ui/src/library_ui.rs#L238), [`ui/dr-ui/src/library_ui.rs:914`](../ui/dr-ui/src/library_ui.rs#L914), [`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:3940`](../ui/dr-ui/src/library.rs#L3940), [`ui/dr-ui/src/library_ui.rs:177`](../ui/dr-ui/src/library_ui.rs#L177), [`ui/dr-ui/src/library_ui.rs:4515`](../ui/dr-ui/src/library_ui.rs#L4515), [`ui/dr-ui/src/library_ui.rs:6027`](../ui/dr-ui/src/library_ui.rs#L6027), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1), [`ui/dr-ui/src/repairs.rs:670`](../ui/dr-ui/src/repairs.rs#L670), [`ui/dr-ui/ui/app.slint:387`](../ui/dr-ui/ui/app.slint#L387), [`ui/dr-ui/ui/settings.slint:435`](../ui/dr-ui/ui/settings.slint#L435), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | @@ -50,7 +50,7 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc | FR-CAT-8 | [`core/dr-catalog/src/schema.rs:644`](../core/dr-catalog/src/schema.rs#L644), [`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:1400`](../core/dr-pipeline/src/sidecar.rs#L1400), [`core/dr-pipeline/src/sidecar.rs:2232`](../core/dr-pipeline/src/sidecar.rs#L2232), [`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:5430`](../ui/dr-ui/src/develop.rs#L5430), [`ui/dr-ui/src/develop.rs:5459`](../ui/dr-ui/src/develop.rs#L5459), [`ui/dr-ui/src/develop.rs:6833`](../ui/dr-ui/src/develop.rs#L6833), [`ui/dr-ui/src/develop.rs:6887`](../ui/dr-ui/src/develop.rs#L6887), [`ui/dr-ui/src/develop.rs:6989`](../ui/dr-ui/src/develop.rs#L6989), [`ui/dr-ui/src/export.rs:812`](../ui/dr-ui/src/export.rs#L812), [`ui/dr-ui/src/lib.rs:1330`](../ui/dr-ui/src/lib.rs#L1330), [`ui/dr-ui/src/lib.rs:1888`](../ui/dr-ui/src/lib.rs#L1888), [`ui/dr-ui/src/lib.rs:2457`](../ui/dr-ui/src/lib.rs#L2457), [`ui/dr-ui/src/lib.rs:2604`](../ui/dr-ui/src/lib.rs#L2604), [`ui/dr-ui/src/lib.rs:757`](../ui/dr-ui/src/lib.rs#L757), [`ui/dr-ui/src/library.rs:1455`](../ui/dr-ui/src/library.rs#L1455), [`ui/dr-ui/src/library.rs:1490`](../ui/dr-ui/src/library.rs#L1490), [`ui/dr-ui/src/library.rs:2491`](../ui/dr-ui/src/library.rs#L2491), [`ui/dr-ui/src/library.rs:564`](../ui/dr-ui/src/library.rs#L564), [`ui/dr-ui/src/library.rs:616`](../ui/dr-ui/src/library.rs#L616), [`ui/dr-ui/src/library.rs:653`](../ui/dr-ui/src/library.rs#L653), [`ui/dr-ui/src/library.rs:7386`](../ui/dr-ui/src/library.rs#L7386), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:926`](../ui/dr-ui/src/library.rs#L926), [`ui/dr-ui/src/library_ui.rs:1339`](../ui/dr-ui/src/library_ui.rs#L1339), [`ui/dr-ui/src/library_ui.rs:6350`](../ui/dr-ui/src/library_ui.rs#L6350), [`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:1048`](../core/dr-catalog/src/schema.rs#L1048), [`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:4719`](../ui/dr-ui/src/develop.rs#L4719), [`ui/dr-ui/src/develop.rs:4762`](../ui/dr-ui/src/develop.rs#L4762), [`ui/dr-ui/src/library.rs:1355`](../ui/dr-ui/src/library.rs#L1355), [`ui/dr-ui/src/library.rs:1408`](../ui/dr-ui/src/library.rs#L1408), [`ui/dr-ui/src/library.rs:1439`](../ui/dr-ui/src/library.rs#L1439), [`ui/dr-ui/src/library.rs:1490`](../ui/dr-ui/src/library.rs#L1490), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:2019`](../ui/dr-ui/src/library.rs#L2019), [`ui/dr-ui/src/library.rs:2471`](../ui/dr-ui/src/library.rs#L2471), [`ui/dr-ui/src/library.rs:2527`](../ui/dr-ui/src/library.rs#L2527), [`ui/dr-ui/src/library.rs:274`](../ui/dr-ui/src/library.rs#L274), [`ui/dr-ui/src/library.rs:5224`](../ui/dr-ui/src/library.rs#L5224), [`ui/dr-ui/src/library.rs:653`](../ui/dr-ui/src/library.rs#L653), [`ui/dr-ui/src/library.rs:910`](../ui/dr-ui/src/library.rs#L910), [`ui/dr-ui/src/library.rs:926`](../ui/dr-ui/src/library.rs#L926), [`ui/dr-ui/src/library.rs:980`](../ui/dr-ui/src/library.rs#L980), [`ui/dr-ui/src/library_ui.rs:1406`](../ui/dr-ui/src/library_ui.rs#L1406), [`ui/dr-ui/src/library_ui.rs:1997`](../ui/dr-ui/src/library_ui.rs#L1997), [`ui/dr-ui/src/library_ui.rs:2041`](../ui/dr-ui/src/library_ui.rs#L2041), [`ui/dr-ui/src/library_ui.rs:2057`](../ui/dr-ui/src/library_ui.rs#L2057), [`ui/dr-ui/src/library_ui.rs:2151`](../ui/dr-ui/src/library_ui.rs#L2151), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:2993`](../ui/dr-ui/src/library_ui.rs#L2993), [`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:3616`](../ui/dr-ui/src/library_ui.rs#L3616), [`ui/dr-ui/src/library_ui.rs:3838`](../ui/dr-ui/src/library_ui.rs#L3838), [`ui/dr-ui/src/library_ui.rs:4116`](../ui/dr-ui/src/library_ui.rs#L4116), [`ui/dr-ui/src/library_ui.rs:4232`](../ui/dr-ui/src/library_ui.rs#L4232), [`ui/dr-ui/src/library_ui.rs:4420`](../ui/dr-ui/src/library_ui.rs#L4420), [`ui/dr-ui/src/library_ui.rs:4542`](../ui/dr-ui/src/library_ui.rs#L4542), [`ui/dr-ui/src/library_ui.rs:573`](../ui/dr-ui/src/library_ui.rs#L573), [`ui/dr-ui/src/library_ui.rs:662`](../ui/dr-ui/src/library_ui.rs#L662), [`ui/dr-ui/src/library_ui.rs:6864`](../ui/dr-ui/src/library_ui.rs#L6864), [`ui/dr-ui/src/library_ui.rs:6946`](../ui/dr-ui/src/library_ui.rs#L6946), [`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:507`](../core/dr-catalog/src/schema.rs#L507), [`core/dr-catalog/src/schema.rs:685`](../core/dr-catalog/src/schema.rs#L685), [`core/dr-catalog/src/schema.rs:877`](../core/dr-catalog/src/schema.rs#L877), [`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:1287`](../ui/dr-ui/src/identity_ui.rs#L1287), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2576`](../ui/dr-ui/src/lib.rs#L2576), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1), [`ui/dr-ui/ui/settings.slint:529`](../ui/dr-ui/ui/settings.slint#L529), [`ui/dr-ui/ui/settings.slint:94`](../ui/dr-ui/ui/settings.slint#L94) | +| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:507`](../core/dr-catalog/src/schema.rs#L507), [`core/dr-catalog/src/schema.rs:685`](../core/dr-catalog/src/schema.rs#L685), [`core/dr-catalog/src/schema.rs:877`](../core/dr-catalog/src/schema.rs#L877), [`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:1315`](../ui/dr-ui/src/identity_ui.rs#L1315), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2576`](../ui/dr-ui/src/lib.rs#L2576), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1), [`ui/dr-ui/ui/settings.slint:529`](../ui/dr-ui/ui/settings.slint#L529), [`ui/dr-ui/ui/settings.slint:94`](../ui/dr-ui/ui/settings.slint#L94) | | FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:877`](../core/dr-catalog/src/schema.rs#L877), [`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:345`](../ui/dr-ui/src/library.rs#L345), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:507`](../core/dr-catalog/src/schema.rs#L507), [`core/dr-catalog/src/schema.rs:877`](../core/dr-catalog/src/schema.rs#L877), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-13 | [`core/dr-catalog/src/schema.rs:783`](../core/dr-catalog/src/schema.rs#L783), [`core/dr-types/src/place.rs:129`](../core/dr-types/src/place.rs#L129), [`ui/dr-ui/src/identity.rs:104`](../ui/dr-ui/src/identity.rs#L104), [`ui/dr-ui/src/library.rs:321`](../ui/dr-ui/src/library.rs#L321), [`ui/dr-ui/src/library.rs:7219`](../ui/dr-ui/src/library.rs#L7219), [`ui/dr-ui/src/library.rs:7262`](../ui/dr-ui/src/library.rs#L7262), [`ui/dr-ui/src/library.rs:7300`](../ui/dr-ui/src/library.rs#L7300), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707), [`ui/dr-ui/ui/identity.slint:63`](../ui/dr-ui/ui/identity.slint#L63), [`ui/dr-ui/ui/library.slint:1512`](../ui/dr-ui/ui/library.slint#L1512) | @@ -58,9 +58,9 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc | 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:4146`](../ui/dr-ui/src/develop.rs#L4146), [`ui/dr-ui/src/develop.rs:4159`](../ui/dr-ui/src/develop.rs#L4159), [`ui/dr-ui/src/develop.rs:4204`](../ui/dr-ui/src/develop.rs#L4204), [`ui/dr-ui/src/develop.rs:4215`](../ui/dr-ui/src/develop.rs#L4215), [`ui/dr-ui/src/develop.rs:4221`](../ui/dr-ui/src/develop.rs#L4221), [`ui/dr-ui/src/develop.rs:4238`](../ui/dr-ui/src/develop.rs#L4238), [`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:8768`](../ui/dr-ui/src/develop.rs#L8768), [`ui/dr-ui/src/develop.rs:8832`](../ui/dr-ui/src/develop.rs#L8832), [`ui/dr-ui/src/develop.rs:8855`](../ui/dr-ui/src/develop.rs#L8855), [`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:1932`](../ui/dr-ui/src/lib.rs#L1932), [`ui/dr-ui/src/lib.rs:2040`](../ui/dr-ui/src/lib.rs#L2040), [`ui/dr-ui/src/lib.rs:2153`](../ui/dr-ui/src/lib.rs#L2153), [`ui/dr-ui/src/lib.rs:2194`](../ui/dr-ui/src/lib.rs#L2194), [`ui/dr-ui/src/lib.rs:3687`](../ui/dr-ui/src/lib.rs#L3687), [`ui/dr-ui/src/lib.rs:442`](../ui/dr-ui/src/lib.rs#L442), [`ui/dr-ui/src/peaking.rs:1`](../ui/dr-ui/src/peaking.rs#L1), [`ui/dr-ui/ui/app.slint:2028`](../ui/dr-ui/ui/app.slint#L2028), [`ui/dr-ui/ui/app.slint:3035`](../ui/dr-ui/ui/app.slint#L3035), [`ui/dr-ui/ui/app.slint:90`](../ui/dr-ui/ui/app.slint#L90), [`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:171`](../core/dr-pipeline/src/sidecar.rs#L171), [`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:564`](../ui/dr-ui/src/library.rs#L564) | | FR-CULL-5 | [`core/dr-catalog/src/bursts.rs:1`](../core/dr-catalog/src/bursts.rs#L1), [`core/dr-catalog/src/schema.rs:550`](../core/dr-catalog/src/schema.rs#L550), [`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:6039`](../ui/dr-ui/src/library.rs#L6039) | -| 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:618`](../core/dr-catalog/src/schema.rs#L618), [`core/dr-catalog/src/schema.rs:836`](../core/dr-catalog/src/schema.rs#L836), [`core/dr-catalog/src/schema.rs:877`](../core/dr-catalog/src/schema.rs#L877), [`core/dr-face/src/align.rs:296`](../core/dr-face/src/align.rs#L296), [`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:431`](../ui/dr-ui/src/faces.rs#L431), [`ui/dr-ui/src/faces.rs:447`](../ui/dr-ui/src/faces.rs#L447), [`ui/dr-ui/src/faces.rs:569`](../ui/dr-ui/src/faces.rs#L569), [`ui/dr-ui/src/identity_ui.rs:1287`](../ui/dr-ui/src/identity_ui.rs#L1287), [`ui/dr-ui/src/identity_ui.rs:570`](../ui/dr-ui/src/identity_ui.rs#L570), [`ui/dr-ui/src/lib.rs:1511`](../ui/dr-ui/src/lib.rs#L1511), [`ui/dr-ui/src/lib.rs:3942`](../ui/dr-ui/src/lib.rs#L3942), [`ui/dr-ui/src/library.rs:3624`](../ui/dr-ui/src/library.rs#L3624), [`ui/dr-ui/src/library.rs:3889`](../ui/dr-ui/src/library.rs#L3889), [`ui/dr-ui/src/library_ui.rs:406`](../ui/dr-ui/src/library_ui.rs#L406), [`ui/dr-ui/src/library_ui.rs:493`](../ui/dr-ui/src/library_ui.rs#L493), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1), [`ui/dr-ui/src/settings_ui.rs:210`](../ui/dr-ui/src/settings_ui.rs#L210), [`ui/dr-ui/src/settings_ui.rs:552`](../ui/dr-ui/src/settings_ui.rs#L552), [`ui/dr-ui/ui/app.slint:946`](../ui/dr-ui/ui/app.slint#L946), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100), [`ui/dr-ui/ui/settings.slint:467`](../ui/dr-ui/ui/settings.slint#L467), [`ui/dr-ui/ui/settings.slint:529`](../ui/dr-ui/ui/settings.slint#L529), [`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-8a | [`core/dr-catalog/src/faces.rs:124`](../core/dr-catalog/src/faces.rs#L124), [`core/dr-catalog/src/schema.rs:783`](../core/dr-catalog/src/schema.rs#L783), [`core/dr-face/src/classify.rs:1`](../core/dr-face/src/classify.rs#L1), [`core/dr-face/src/eyes.rs:1`](../core/dr-face/src/eyes.rs#L1), [`core/dr-face/src/landmarks.rs:1`](../core/dr-face/src/landmarks.rs#L1), [`core/dr-types/src/place.rs:129`](../core/dr-types/src/place.rs#L129), [`ui/dr-ui/src/faces.rs:160`](../ui/dr-ui/src/faces.rs#L160), [`ui/dr-ui/src/faces.rs:1733`](../ui/dr-ui/src/faces.rs#L1733), [`ui/dr-ui/src/faces.rs:293`](../ui/dr-ui/src/faces.rs#L293), [`ui/dr-ui/src/identity.rs:104`](../ui/dr-ui/src/identity.rs#L104), [`ui/dr-ui/src/library.rs:321`](../ui/dr-ui/src/library.rs#L321), [`ui/dr-ui/src/library.rs:7219`](../ui/dr-ui/src/library.rs#L7219), [`ui/dr-ui/src/library.rs:7262`](../ui/dr-ui/src/library.rs#L7262), [`ui/dr-ui/src/library.rs:7300`](../ui/dr-ui/src/library.rs#L7300), [`ui/dr-ui/src/repairs.rs:554`](../ui/dr-ui/src/repairs.rs#L554), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707), [`ui/dr-ui/ui/identity.slint:273`](../ui/dr-ui/ui/identity.slint#L273), [`ui/dr-ui/ui/identity.slint:63`](../ui/dr-ui/ui/identity.slint#L63), [`ui/dr-ui/ui/library.slint:1512`](../ui/dr-ui/ui/library.slint#L1512) | -| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:685`](../core/dr-catalog/src/schema.rs#L685), [`core/dr-catalog/src/schema.rs:877`](../core/dr-catalog/src/schema.rs#L877), [`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:569`](../ui/dr-ui/src/faces.rs#L569), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/repairs.rs:500`](../ui/dr-ui/src/repairs.rs#L500), [`ui/dr-ui/ui/identity.slint:347`](../ui/dr-ui/ui/identity.slint#L347) | +| 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:618`](../core/dr-catalog/src/schema.rs#L618), [`core/dr-catalog/src/schema.rs:836`](../core/dr-catalog/src/schema.rs#L836), [`core/dr-catalog/src/schema.rs:877`](../core/dr-catalog/src/schema.rs#L877), [`core/dr-face/src/align.rs:296`](../core/dr-face/src/align.rs#L296), [`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:446`](../ui/dr-ui/src/faces.rs#L446), [`ui/dr-ui/src/faces.rs:462`](../ui/dr-ui/src/faces.rs#L462), [`ui/dr-ui/src/faces.rs:584`](../ui/dr-ui/src/faces.rs#L584), [`ui/dr-ui/src/identity_ui.rs:1315`](../ui/dr-ui/src/identity_ui.rs#L1315), [`ui/dr-ui/src/identity_ui.rs:593`](../ui/dr-ui/src/identity_ui.rs#L593), [`ui/dr-ui/src/lib.rs:1511`](../ui/dr-ui/src/lib.rs#L1511), [`ui/dr-ui/src/lib.rs:3942`](../ui/dr-ui/src/lib.rs#L3942), [`ui/dr-ui/src/library.rs:3624`](../ui/dr-ui/src/library.rs#L3624), [`ui/dr-ui/src/library.rs:3889`](../ui/dr-ui/src/library.rs#L3889), [`ui/dr-ui/src/library_ui.rs:406`](../ui/dr-ui/src/library_ui.rs#L406), [`ui/dr-ui/src/library_ui.rs:493`](../ui/dr-ui/src/library_ui.rs#L493), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1), [`ui/dr-ui/src/settings_ui.rs:210`](../ui/dr-ui/src/settings_ui.rs#L210), [`ui/dr-ui/src/settings_ui.rs:552`](../ui/dr-ui/src/settings_ui.rs#L552), [`ui/dr-ui/ui/app.slint:946`](../ui/dr-ui/ui/app.slint#L946), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100), [`ui/dr-ui/ui/settings.slint:467`](../ui/dr-ui/ui/settings.slint#L467), [`ui/dr-ui/ui/settings.slint:529`](../ui/dr-ui/ui/settings.slint#L529), [`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-8a | [`core/dr-catalog/src/faces.rs:124`](../core/dr-catalog/src/faces.rs#L124), [`core/dr-catalog/src/schema.rs:783`](../core/dr-catalog/src/schema.rs#L783), [`core/dr-face/src/classify.rs:1`](../core/dr-face/src/classify.rs#L1), [`core/dr-face/src/eyes.rs:1`](../core/dr-face/src/eyes.rs#L1), [`core/dr-face/src/landmarks.rs:1`](../core/dr-face/src/landmarks.rs#L1), [`core/dr-types/src/place.rs:129`](../core/dr-types/src/place.rs#L129), [`ui/dr-ui/src/faces.rs:166`](../ui/dr-ui/src/faces.rs#L166), [`ui/dr-ui/src/faces.rs:1748`](../ui/dr-ui/src/faces.rs#L1748), [`ui/dr-ui/src/faces.rs:308`](../ui/dr-ui/src/faces.rs#L308), [`ui/dr-ui/src/identity.rs:104`](../ui/dr-ui/src/identity.rs#L104), [`ui/dr-ui/src/library.rs:321`](../ui/dr-ui/src/library.rs#L321), [`ui/dr-ui/src/library.rs:7219`](../ui/dr-ui/src/library.rs#L7219), [`ui/dr-ui/src/library.rs:7262`](../ui/dr-ui/src/library.rs#L7262), [`ui/dr-ui/src/library.rs:7300`](../ui/dr-ui/src/library.rs#L7300), [`ui/dr-ui/src/repairs.rs:554`](../ui/dr-ui/src/repairs.rs#L554), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707), [`ui/dr-ui/ui/identity.slint:273`](../ui/dr-ui/ui/identity.slint#L273), [`ui/dr-ui/ui/identity.slint:63`](../ui/dr-ui/ui/identity.slint#L63), [`ui/dr-ui/ui/library.slint:1512`](../ui/dr-ui/ui/library.slint#L1512) | +| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:685`](../core/dr-catalog/src/schema.rs#L685), [`core/dr-catalog/src/schema.rs:877`](../core/dr-catalog/src/schema.rs#L877), [`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:584`](../ui/dr-ui/src/faces.rs#L584), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/repairs.rs:500`](../ui/dr-ui/src/repairs.rs#L500), [`ui/dr-ui/ui/identity.slint:347`](../ui/dr-ui/ui/identity.slint#L347) | | 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:2845`](../core/dr-pipeline/src/mask.rs#L2845), [`core/dr-pipeline/src/mask.rs:2858`](../core/dr-pipeline/src/mask.rs#L2858), [`core/dr-pipeline/src/mask.rs:2877`](../core/dr-pipeline/src/mask.rs#L2877), [`core/dr-pipeline/src/mask.rs:2891`](../core/dr-pipeline/src/mask.rs#L2891), [`core/dr-pipeline/src/mask.rs:2901`](../core/dr-pipeline/src/mask.rs#L2901), [`core/dr-pipeline/src/mask.rs:710`](../core/dr-pipeline/src/mask.rs#L710), [`core/dr-pipeline/src/mask.rs:749`](../core/dr-pipeline/src/mask.rs#L749), [`core/dr-pipeline/src/mask.rs:809`](../core/dr-pipeline/src/mask.rs#L809), [`core/dr-pipeline/src/mask.rs:828`](../core/dr-pipeline/src/mask.rs#L828), [`core/dr-pipeline/src/mask.rs:852`](../core/dr-pipeline/src/mask.rs#L852), [`core/dr-pipeline/src/mask.rs:864`](../core/dr-pipeline/src/mask.rs#L864), [`core/dr-pipeline/src/mask.rs:876`](../core/dr-pipeline/src/mask.rs#L876), [`core/dr-pipeline/src/sidecar.rs:1336`](../core/dr-pipeline/src/sidecar.rs#L1336), [`core/dr-pipeline/src/sidecar.rs:1515`](../core/dr-pipeline/src/sidecar.rs#L1515), [`core/dr-pipeline/src/sidecar.rs:1604`](../core/dr-pipeline/src/sidecar.rs#L1604), [`core/dr-pipeline/src/sidecar.rs:1708`](../core/dr-pipeline/src/sidecar.rs#L1708), [`core/dr-pipeline/src/sidecar.rs:1916`](../core/dr-pipeline/src/sidecar.rs#L1916), [`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:3666`](../ui/dr-ui/src/develop.rs#L3666), [`ui/dr-ui/src/develop.rs:3693`](../ui/dr-ui/src/develop.rs#L3693), [`ui/dr-ui/src/develop.rs:3725`](../ui/dr-ui/src/develop.rs#L3725), [`ui/dr-ui/src/develop.rs:3749`](../ui/dr-ui/src/develop.rs#L3749), [`ui/dr-ui/src/develop.rs:3755`](../ui/dr-ui/src/develop.rs#L3755), [`ui/dr-ui/src/develop.rs:3762`](../ui/dr-ui/src/develop.rs#L3762), [`ui/dr-ui/src/develop.rs:3782`](../ui/dr-ui/src/develop.rs#L3782), [`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) | | FR-DEV-12 | [`core/dr-pipeline/ops/colour_grading.yaml:1`](../core/dr-pipeline/ops/colour_grading.yaml#L1) | @@ -121,7 +121,7 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc | 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:1048`](../core/dr-catalog/src/schema.rs#L1048), [`core/dr-catalog/src/schema.rs:1503`](../core/dr-catalog/src/schema.rs#L1503), [`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:1350`](../ui/dr-ui/src/lib.rs#L1350), [`ui/dr-ui/src/lib.rs:2487`](../ui/dr-ui/src/lib.rs#L2487), [`ui/dr-ui/src/lib.rs:2558`](../ui/dr-ui/src/lib.rs#L2558), [`ui/dr-ui/src/lib.rs:3854`](../ui/dr-ui/src/lib.rs#L3854), [`ui/dr-ui/src/lib.rs:3855`](../ui/dr-ui/src/lib.rs#L3855), [`ui/dr-ui/src/lib.rs:3894`](../ui/dr-ui/src/lib.rs#L3894), [`ui/dr-ui/src/library.rs:2173`](../ui/dr-ui/src/library.rs#L2173), [`ui/dr-ui/src/library.rs:2196`](../ui/dr-ui/src/library.rs#L2196), [`ui/dr-ui/src/library.rs:2471`](../ui/dr-ui/src/library.rs#L2471), [`ui/dr-ui/src/library.rs:2652`](../ui/dr-ui/src/library.rs#L2652), [`ui/dr-ui/src/library.rs:2789`](../ui/dr-ui/src/library.rs#L2789), [`ui/dr-ui/src/library.rs:2804`](../ui/dr-ui/src/library.rs#L2804), [`ui/dr-ui/src/library_ui.rs:1482`](../ui/dr-ui/src/library_ui.rs#L1482), [`ui/dr-ui/src/library_ui.rs:1555`](../ui/dr-ui/src/library_ui.rs#L1555), [`ui/dr-ui/src/library_ui.rs:1656`](../ui/dr-ui/src/library_ui.rs#L1656), [`ui/dr-ui/src/library_ui.rs:1711`](../ui/dr-ui/src/library_ui.rs#L1711), [`ui/dr-ui/src/library_ui.rs:1846`](../ui/dr-ui/src/library_ui.rs#L1846), [`ui/dr-ui/src/library_ui.rs:1964`](../ui/dr-ui/src/library_ui.rs#L1964), [`ui/dr-ui/src/library_ui.rs:2731`](../ui/dr-ui/src/library_ui.rs#L2731), [`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:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:517`](../ui/dr-ui/src/library_ui.rs#L517), [`ui/dr-ui/src/library_ui.rs:527`](../ui/dr-ui/src/library_ui.rs#L527), [`ui/dr-ui/src/library_ui.rs:542`](../ui/dr-ui/src/library_ui.rs#L542), [`ui/dr-ui/src/library_ui.rs:589`](../ui/dr-ui/src/library_ui.rs#L589), [`ui/dr-ui/src/library_ui.rs:604`](../ui/dr-ui/src/library_ui.rs#L604), [`ui/dr-ui/src/library_ui.rs:678`](../ui/dr-ui/src/library_ui.rs#L678), [`ui/dr-ui/src/library_ui.rs:6881`](../ui/dr-ui/src/library_ui.rs#L6881), [`ui/dr-ui/src/library_ui.rs:6899`](../ui/dr-ui/src/library_ui.rs#L6899), [`ui/dr-ui/src/library_ui.rs:709`](../ui/dr-ui/src/library_ui.rs#L709), [`ui/dr-ui/src/library_ui.rs:721`](../ui/dr-ui/src/library_ui.rs#L721), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:157`](../ui/dr-ui/src/settings_ui.rs#L157), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:524`](../ui/dr-ui/src/settings_ui.rs#L524), [`ui/dr-ui/ui/app.slint:1610`](../ui/dr-ui/ui/app.slint#L1610), [`ui/dr-ui/ui/app.slint:3163`](../ui/dr-ui/ui/app.slint#L3163), [`ui/dr-ui/ui/app.slint:541`](../ui/dr-ui/ui/app.slint#L541), [`ui/dr-ui/ui/app.slint:549`](../ui/dr-ui/ui/app.slint#L549), [`ui/dr-ui/ui/app.slint:555`](../ui/dr-ui/ui/app.slint#L555), [`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), [`ui/dr-ui/ui/settings.slint:114`](../ui/dr-ui/ui/settings.slint#L114), [`ui/dr-ui/ui/settings.slint:656`](../ui/dr-ui/ui/settings.slint#L656) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1711`](../ui/dr-ui/src/library_ui.rs#L1711), [`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:1042`](../ui/dr-ui/src/derived_sync.rs#L1042), [`ui/dr-ui/src/derived_sync.rs:642`](../ui/dr-ui/src/derived_sync.rs#L642), [`ui/dr-ui/src/library.rs:1084`](../ui/dr-ui/src/library.rs#L1084), [`ui/dr-ui/src/library.rs:2293`](../ui/dr-ui/src/library.rs#L2293), [`ui/dr-ui/src/library.rs:2383`](../ui/dr-ui/src/library.rs#L2383), [`ui/dr-ui/src/library.rs:4060`](../ui/dr-ui/src/library.rs#L4060), [`ui/dr-ui/src/library_ui.rs:1482`](../ui/dr-ui/src/library_ui.rs#L1482), [`ui/dr-ui/src/library_ui.rs:1555`](../ui/dr-ui/src/library_ui.rs#L1555), [`ui/dr-ui/src/library_ui.rs:1754`](../ui/dr-ui/src/library_ui.rs#L1754), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/src/repairs.rs:1049`](../ui/dr-ui/src/repairs.rs#L1049), [`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:1042`](../ui/dr-ui/src/derived_sync.rs#L1042), [`ui/dr-ui/src/derived_sync.rs:642`](../ui/dr-ui/src/derived_sync.rs#L642), [`ui/dr-ui/src/library.rs:1084`](../ui/dr-ui/src/library.rs#L1084), [`ui/dr-ui/src/library.rs:2293`](../ui/dr-ui/src/library.rs#L2293), [`ui/dr-ui/src/library.rs:2383`](../ui/dr-ui/src/library.rs#L2383), [`ui/dr-ui/src/library.rs:4060`](../ui/dr-ui/src/library.rs#L4060), [`ui/dr-ui/src/library_ui.rs:1482`](../ui/dr-ui/src/library_ui.rs#L1482), [`ui/dr-ui/src/library_ui.rs:1555`](../ui/dr-ui/src/library_ui.rs#L1555), [`ui/dr-ui/src/library_ui.rs:1754`](../ui/dr-ui/src/library_ui.rs#L1754), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/src/repairs.rs:1105`](../ui/dr-ui/src/repairs.rs#L1105), [`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:3940`](../ui/dr-ui/src/library.rs#L3940), [`ui/dr-ui/src/library_ui.rs:4515`](../ui/dr-ui/src/library_ui.rs#L4515), [`ui/dr-ui/ui/settings.slint:435`](../ui/dr-ui/ui/settings.slint#L435) | | 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:377`](../core/dr-types/src/settings.rs#L377), [`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:1559`](../ui/dr-ui/src/lib.rs#L1559), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | @@ -177,8 +177,8 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc | NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | | NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | | NFR-RES-1 | [`core/dr-gpu/src/adjust.rs:1057`](../core/dr-gpu/src/adjust.rs#L1057), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/develop.rs:4521`](../ui/dr-ui/src/develop.rs#L4521), [`ui/dr-ui/src/identity_ui.rs:125`](../ui/dr-ui/src/identity_ui.rs#L125), [`ui/dr-ui/src/lib.rs:100`](../ui/dr-ui/src/lib.rs#L100), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | -| NFR-RES-2 | [`ui/dr-ui/src/faces.rs:447`](../ui/dr-ui/src/faces.rs#L447), [`ui/dr-ui/src/library.rs:3624`](../ui/dr-ui/src/library.rs#L3624), [`ui/dr-ui/src/repairs.rs:1049`](../ui/dr-ui/src/repairs.rs#L1049), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1) | -| NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:1048`](../core/dr-catalog/src/schema.rs#L1048), [`core/dr-gpu/src/lib.rs:95`](../core/dr-gpu/src/lib.rs#L95), [`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), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376) | +| NFR-RES-2 | [`ui/dr-ui/src/faces.rs:462`](../ui/dr-ui/src/faces.rs#L462), [`ui/dr-ui/src/library.rs:3624`](../ui/dr-ui/src/library.rs#L3624), [`ui/dr-ui/src/repairs.rs:1105`](../ui/dr-ui/src/repairs.rs#L1105), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1) | +| NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:1048`](../core/dr-catalog/src/schema.rs#L1048), [`core/dr-gpu/src/lib.rs:95`](../core/dr-gpu/src/lib.rs#L95), [`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), [`core/dr-thumbs/src/lib.rs:393`](../core/dr-thumbs/src/lib.rs#L393) | | NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`core/dr-decode/src/error.rs:30`](../core/dr-decode/src/error.rs#L30) | | NFR-SEC-2 | [`core/dr-sync/src/account.rs:318`](../core/dr-sync/src/account.rs#L318), [`platform/dr-plat/src/crash.rs:1`](../platform/dr-plat/src/crash.rs#L1), [`platform/dr-plat/src/diagnostics.rs:1`](../platform/dr-plat/src/diagnostics.rs#L1), [`platform/dr-plat/src/diagnostics/bundle.rs:1`](../platform/dr-plat/src/diagnostics/bundle.rs#L1), [`platform/dr-plat/src/diagnostics/redact.rs:1`](../platform/dr-plat/src/diagnostics/redact.rs#L1), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) | | NFR-SEC-3 | [`core/dr-sync-nextcloud/src/lib.rs:1`](../core/dr-sync-nextcloud/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1) | diff --git a/ui/dr-ui/examples/identity_bench.rs b/ui/dr-ui/examples/identity_bench.rs new file mode 100644 index 0000000..3f6b877 --- /dev/null +++ b/ui/dr-ui/examples/identity_bench.rs @@ -0,0 +1,147 @@ +//! What one click on the Identity screen costs, off the GUI. +//! +//! cargo run --release -p dr-ui --example identity_bench -- CATALOG.sqlite THUMBS_DIR +//! +//! Times each read the screen performs after a confirm, a reject or a +//! split — the people rail, the selected person's faces with their crops, +//! the coverage line — and the two batch writes, against a *copy* of a real +//! catalog. It writes to the catalog it is given (the batch operations are +//! the point), so never hand it the library's own file. +//! +//! The figures are wall-clock on this machine and this library, for reading +//! side by side before and after a change; they are not a gate. + +use std::path::PathBuf; +use std::time::{Duration, Instant}; + +use dr_catalog::faces::{self, PersonId}; +use dr_catalog::Catalog; +use dr_thumbs::ThumbStore; +use dr_ui::identity; +use dr_ui::repairs::{self, Capabilities, Scope}; + +const MODEL_ID: &str = "scrfd_10g+w600k_mbf"; + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + if args.len() < 2 { + eprintln!("usage: identity_bench CATALOG.sqlite THUMBS_DIR"); + std::process::exit(2); + } + let catalog = Catalog::open(&PathBuf::from(&args[0])).expect("catalog"); + let store = ThumbStore::open(&PathBuf::from(&args[1])).expect("thumbs"); + let conn = catalog.connection(); + + // The person with the most faces: the worst case for the face grid, and + // the one a user is likeliest to be confirming through. + let (biggest, n_faces): (i64, i64) = conn + .query_row( + "SELECT person_id, COUNT(*) c FROM face_person GROUP BY 1 ORDER BY c DESC LIMIT 1", + [], + |r| Ok((r.get(0)?, r.get(1)?)), + ) + .expect("a person"); + let biggest = PersonId(biggest as u64); + println!("largest person {biggest:?} holds {n_faces} faces\n"); + + // ── the reads a click triggers ──────────────────────────────────── + let detector = dr_types::FaceDetector::for_model_id(MODEL_ID).unwrap_or_default(); + let registry = repairs::registry( + Scope::Outstanding, + MODEL_ID, + detector, + Capabilities { + gpu: true, + face_models: true, + eye_models: true, + }, + ); + + time("load_people", 5, || { + identity::load_people(&catalog, MODEL_ID).unwrap(); + }); + time("load_faces (largest person)", 5, || { + identity::load_faces(&catalog, &store, biggest).unwrap(); + }); + time("audit (coverage line)", 5, || { + dr_ui::faces::audit(&catalog, &store, MODEL_ID, ®istry).unwrap(); + }); + + // ── the batch writes ────────────────────────────────────────────── + // Every face of the largest person is demoted to a suggestion, then + // confirmed back in one call, so the measurement covers the whole group. + let ids: Vec = { + let mut q = conn + .prepare("SELECT face_id FROM face_person WHERE person_id = ?1") + .unwrap(); + q.query_map([biggest.0 as i64], |r| r.get(0)) + .unwrap() + .collect::>() + .unwrap() + }; + let demote = |conn: &rusqlite::Connection| { + conn.execute( + "UPDATE face_person SET confirmed = 0, probability = 0.5 WHERE person_id = ?1", + [biggest.0 as i64], + ) + .unwrap(); + }; + demote(conn); + time("confirm_all (largest person)", 3, || { + demote(conn); + identity::confirm_all(&catalog, biggest).unwrap(); + }); + + // Split the group onto a new person and fold it straight back, so the + // catalog ends where it started apart from the redirect rows. + let members: Vec = ids.iter().map(|&i| faces::FaceId(i as u64)).collect(); + time("split_off (largest person, all faces)", 3, || { + let new = identity::split_off(&catalog, biggest, &members, "").unwrap(); + faces::merge_people(conn, biggest, new).unwrap(); + }); + // The split rejects every face from `biggest`; a merge back does not + // clear that, so clear it here or a later run measures a different table. + conn.execute( + "DELETE FROM face_person_rejected WHERE person_id = ?1", + [biggest.0 as i64], + ) + .unwrap(); +} + +/// Run `f` a few times and print the best wall-clock, the median, and the +/// best CPU time. The best is what the code costs, the median is what the +/// user waits — and the CPU figure is the one to compare across runs, since +/// this path is single-threaded and a build running on the same machine +/// doubles the wall clock without touching it. +fn time(label: &str, runs: usize, mut f: impl FnMut()) { + let mut wall: Vec = Vec::with_capacity(runs); + let mut cpu: Vec = Vec::with_capacity(runs); + for _ in 0..runs { + let c = cpu_now(); + let t = Instant::now(); + f(); + wall.push(t.elapsed()); + cpu.push(cpu_now().saturating_sub(c)); + } + wall.sort(); + cpu.sort(); + println!( + "{label:42} best {:8.1} ms median {:8.1} ms cpu {:8.1} ms", + wall[0].as_secs_f64() * 1e3, + wall[runs / 2].as_secs_f64() * 1e3, + cpu[0].as_secs_f64() * 1e3 + ); +} + +/// This thread's time on a CPU so far, from the scheduler's own account. +/// +/// `/proc/self/schedstat` is the main thread's; the bench runs everything on +/// it. Zero where the file is missing, which only makes the CPU column +/// useless rather than the run. +fn cpu_now() -> Duration { + std::fs::read_to_string("/proc/self/schedstat") + .ok() + .and_then(|s| s.split_whitespace().next()?.parse::().ok()) + .map(Duration::from_nanos) + .unwrap_or_default() +} diff --git a/ui/dr-ui/src/faces.rs b/ui/dr-ui/src/faces.rs index ed37df6..a475fad 100644 --- a/ui/dr-ui/src/faces.rs +++ b/ui/dr-ui/src/faces.rs @@ -116,6 +116,12 @@ pub fn faces_outstanding( ORDER BY i.id", faces::embedder_sql("fi.model_id") ))?; + // The store's index in one read rather than a probe per image; see + // `audit`, which splits the same list the same way. + let held = store.held(FACE_TIER).unwrap_or_else(|e| { + log::warn!("faces: reading the thumbnail index: {e}"); + Default::default() + }); let rows = stmt .query_map([faces::embedder_of(model_id)], |r| { Ok(FaceRequest { @@ -134,7 +140,7 @@ pub fn faces_outstanding( // a whole-library button that could only reach photographs the user had // personally zoomed into. `repairs::spawn` is that // requirement implemented; this one is the local-only variant. - .filter(|req| store.contains(req.file_id, FACE_TIER)) + .filter(|req| held.contains(&req.file_id)) .collect(); Ok(rows) } @@ -270,12 +276,21 @@ pub fn audit( )", faces::embedder_sql("fi.model_id") ))?; + // One read of the store's index, not one probe per outstanding image: + // `contains` answers the same question, and asked four thousand times + // it cost more than every query above put together. A store whose index + // cannot be read is treated as holding nothing, which is what `contains` + // reports for it too. + let held = store.held(FACE_TIER).unwrap_or_else(|e| { + log::warn!("faces: reading the thumbnail index: {e}"); + Default::default() + }); let (mut ready, mut awaiting) = (0u64, 0u64); for file_id in stmt .query_map([faces::embedder_of(model_id)], |r| r.get::<_, i64>(0))? .filter_map(Result::ok) { - if store.contains(file_id as u64, FACE_TIER) { + if held.contains(&(file_id as u64)) { ready += 1; } else { awaiting += 1; diff --git a/ui/dr-ui/src/identity_ui.rs b/ui/dr-ui/src/identity_ui.rs index 0ec61ec..6a8a35a 100644 --- a/ui/dr-ui/src/identity_ui.rs +++ b/ui/dr-ui/src/identity_ui.rs @@ -138,6 +138,26 @@ impl IdentityController { } } +/// What a reload has to re-read, named by what just changed. +/// +/// The coverage line is the expensive half of a redraw: it lists every +/// repair's outstanding images to count them, which is several scans of the +/// whole `images` table (`crate::repairs::counts`). A confirm, a reject, a +/// rename or a merge moves faces between people and cannot change how many +/// images have been indexed, so a redraw for one of those must not pay for +/// it — that was 200 ms of the half-second every click on the face grid used +/// to cost on the reference library. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Changed { + /// Who the faces belong to. The rail and the grid are re-read; the + /// coverage line is left as it was. + Identities, + /// Which faces exist: a sweep finished or was stopped, the face data was + /// deleted, the screen was opened onto a catalog another device may have + /// indexed. Everything is re-read, the coverage line included. + Library, +} + /// Push the people rail and the face grid into the window. /// /// **Draws with the portraits it already has and cuts the rest afterwards.** @@ -154,6 +174,7 @@ pub fn refresh( store: Option>, model_id: &str, eyes: bool, + changed: Changed, ) { let borrow = catalog.borrow(); let Some(cat) = borrow.as_ref() else { @@ -274,7 +295,9 @@ pub fn refresh( window.set_identity_picked(ctl.picked.borrow().len() as i32); drop(borrow); - refresh_coverage(window, catalog, store.as_deref(), model_id, eyes); + if changed == Changed::Library { + refresh_coverage(window, catalog, store.as_deref(), model_id, eyes); + } // Last, so a portrait cannot delay anything above it. if let Some(store) = store { @@ -628,8 +651,10 @@ pub fn wire( // The availability closure is an argument rather than named in the // body: macro hygiene would bind the name to this scope's `Rc`, which the // first `move` closure would then take with it. + // The last argument is a `Changed` variant, named bare so the call + // stays on one line. macro_rules! reload { - ($w:expr, $ctl:expr, $catalog:expr, $store:expr, $settings:expr, $eyes:expr) => { + ($w:expr, $ctl:expr, $catalog:expr, $store:expr, $settings:expr, $eyes:expr, $changed:ident) => { refresh( &$w, &$ctl, @@ -637,6 +662,7 @@ pub fn wire( $store(), &model_id(&$settings), $eyes(), + Changed::$changed, ) }; } @@ -662,7 +688,7 @@ pub fn wire( // A fact about the filesystem, so it is re-checked on every open // rather than cached: the user may have just put the models there. w.set_identity_model_missing(models_present().is_none()); - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Library); }); } @@ -702,7 +728,7 @@ pub fn wire( // The offer was about the person being navigated away from. Left // up, its "Merge" would fold whoever is selected *now*. clear_merge_offer(&w, &ctl); - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Identities); reset_name_field(&w); }); } @@ -748,7 +774,7 @@ pub fn wire( Err(e) => log::warn!("identity: looking for a namesake: {e}"), } } - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Identities); // `rename` trims; the field should show what was actually stored // rather than the spacing the user happened to type. reset_name_field(&w); @@ -786,7 +812,7 @@ pub fn wire( } } clear_merge_offer(&w, &ctl); - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Identities); reset_name_field(&w); }); } @@ -832,7 +858,7 @@ pub fn wire( log::warn!("identity: confirm: {e}"); } } - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Identities); }); } @@ -853,7 +879,7 @@ pub fn wire( log::warn!("identity: reject: {e}"); } } - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Identities); }); } @@ -897,7 +923,7 @@ pub fn wire( Err(e) => log::warn!("identity: confirm all: {e}"), } } - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Identities); }); } @@ -931,7 +957,7 @@ pub fn wire( } } ctl.clear_picks(); - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Identities); }); } @@ -1091,6 +1117,7 @@ pub fn wire( store_tick(), &model_id(&settings_tick), eyes_tick(), + Changed::Identities, ); } }, @@ -1268,6 +1295,7 @@ pub fn wire( store_tick(), &model_id(&settings_tick), eyes_tick(), + Changed::Library, ); } }, @@ -1306,7 +1334,7 @@ pub fn wire( a.finish("stopped"); } w.set_identity_indexing(false); - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Library); }); } @@ -1351,7 +1379,7 @@ pub fn wire( ctl.selected.set(None); ctl.clear_picks(); } - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Identities); }); } @@ -1365,7 +1393,7 @@ pub fn wire( window.on_identity_toggle_show_ignored(move || { let Some(w) = weak.upgrade() else { return }; ctl.show_ignored.set(!ctl.show_ignored.get()); - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Identities); }); } @@ -1387,7 +1415,7 @@ pub fn wire( ctl.selected.set(None); ctl.clear_picks(); ctl.covers.borrow_mut().clear(); - reload!(w, ctl, catalog, store, settings, eyes_available); + reload!(w, ctl, catalog, store, settings, eyes_available, Library); }); } } diff --git a/ui/dr-ui/src/repairs.rs b/ui/dr-ui/src/repairs.rs index 1c88138..51522f6 100644 --- a/ui/dr-ui/src/repairs.rs +++ b/ui/dr-ui/src/repairs.rs @@ -732,12 +732,19 @@ fn faces_without_proxy( WHERE r.file_id IS NOT NULL AND {VISIBLE} AND {} = ?1", faces::embedder_sql("f.model_id"), ))?; + // The index once, not a probe per image with faces — see + // `ThumbStore::held`. An unreadable index reads as empty, as `contains` + // would have reported it. + let held = store.held(dr_thumbs::ThumbSize::Large).unwrap_or_else(|e| { + log::warn!("repairs: reading the thumbnail index: {e}"); + Default::default() + }); let rows = stmt .query_map([embedder], |r| { Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)) })? .filter_map(Result::ok) - .filter(|(_, file_id)| !store.contains(*file_id as u64, dr_thumbs::ThumbSize::Large)) + .filter(|(_, file_id)| !held.contains(&(*file_id as u64))) .map(|(id, _)| id) .collect(); Ok(rows) @@ -802,7 +809,14 @@ fn still_owed( } } -/// How many images each repair still lists, for the settings line. +/// How many images each repair still lists, for the settings line and the +/// coverage line. +/// +/// Counted, not listed. [`listed`] builds a `Target` per image — its path, +/// its size — and sorts the faces-first order the job visits them in, none of +/// which a count reads; asked for six repairs on a 24,000-image library that +/// was 350 ms of `source_ref` strings built to be dropped. A `COUNT(*)` over +/// the same predicate is the same number in a tenth of the time. pub fn counts( catalog: &Catalog, store: &ThumbStore, @@ -810,10 +824,52 @@ pub fn counts( ) -> Result, dr_catalog::CatalogError> { repairs .iter() - .map(|r| Ok((r.label, listed(catalog, store, r)?.len() as u64))) + .map(|r| Ok((r.label, count(catalog, store, r)?))) .collect() } +/// How many images one repair lists — the size of [`listed`]'s answer, +/// without building it. +fn count( + catalog: &Catalog, + store: &ThumbStore, + repair: &Repair, +) -> Result { + match &repair.needs { + Needs::Sql(sql) => { + let n: i64 = catalog.connection().query_row( + &format!( + "SELECT COUNT(*) + FROM images i + JOIN remote r ON r.image_id = i.id + WHERE r.file_id IS NOT NULL AND {VISIBLE} AND ({sql})" + ), + [], + |r| r.get(0), + )?; + Ok(n as u64) + } + // The set is built from its own query and may name images `listed` + // would not visit, so it is intersected with the same base rather + // than trusted for its size. + Needs::Set(f) => { + let set = f(catalog, store)?; + let mut stmt = catalog.connection().prepare(&format!( + "SELECT i.id + FROM images i + JOIN remote r ON r.image_id = i.id + WHERE r.file_id IS NOT NULL AND {VISIBLE}" + ))?; + let n = stmt + .query_map([], |r| r.get::<_, i64>(0))? + .filter_map(Result::ok) + .filter(|id| set.contains(id)) + .count(); + Ok(n as u64) + } + } +} + /// One image on the work list, with the most any repair claiming it asks /// for. struct Planned {