Merge: make face sync actually reach the other device
Build and test / Desktop (Linux) (push) Successful in 31m3s
Build and test / Layer separation (push) Successful in 36s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
Traceability / Requirement traces (push) Successful in 30s
Build and test / Android (aarch64) (push) Failing after 50m38s
Build and test / Desktop (Linux) (push) Successful in 31m3s
Build and test / Layer separation (push) Successful in 36s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
Traceability / Requirement traces (push) Successful in 30s
Build and test / Android (aarch64) (push) Failing after 50m38s
Two independent faults, both of which had to be fixed before a tablet could show what a laptop had indexed. An image was exported to the face shards exactly once, so a re-index updated the catalog and nothing else — this library went from 1,807 faces to 15,194 and kept syncing the original 1,807. And people never crossed a device boundary at all: the catalog merge handled collections and keywords only, so the far end received every face and no groups, and drew an empty People screen over a full catalog. Names, confirmations, rejections and set-aside groups now merge by uuid, with faces matched across devices by photograph and box overlap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,34 @@ impl FaceShardStore {
|
|||||||
&self.client
|
&self.client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When this store last recorded an export of `file_id`.
|
||||||
|
///
|
||||||
|
/// `None` for an image it does not hold, or one held from before the column
|
||||||
|
/// existed. Both mean "cannot vouch for it", and the export treats them the
|
||||||
|
/// same: send it again.
|
||||||
|
pub fn indexed_at(&self, file_id: u64, model_id: &str) -> Option<i64> {
|
||||||
|
let shard: i64 = self
|
||||||
|
.index
|
||||||
|
.query_row(
|
||||||
|
"SELECT shard FROM entries WHERE file_id = ?1 AND model_id = ?2",
|
||||||
|
rusqlite::params![file_id as i64, model_id],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.ok()
|
||||||
|
.flatten()?;
|
||||||
|
let conn = self.open_shard(shard as u32, false).ok()?;
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT indexed_at FROM indexed WHERE file_id = ?1 AND model_id = ?2",
|
||||||
|
rusqlite::params![file_id as i64, model_id],
|
||||||
|
|r| r.get::<_, Option<i64>>(0),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn contains(&self, file_id: u64, model_id: &str) -> bool {
|
pub fn contains(&self, file_id: u64, model_id: &str) -> bool {
|
||||||
self.index
|
self.index
|
||||||
.query_row(
|
.query_row(
|
||||||
@@ -151,6 +179,22 @@ impl FaceShardStore {
|
|||||||
model_id: &str,
|
model_id: &str,
|
||||||
source_edge: u32,
|
source_edge: u32,
|
||||||
faces: &[SharedFace],
|
faces: &[SharedFace],
|
||||||
|
) -> Result<u32, CatalogError> {
|
||||||
|
self.put_image_at(file_id, model_id, source_edge, faces, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::put_image`], carrying the catalog's own index time.
|
||||||
|
///
|
||||||
|
/// Separate entry point rather than a sixth positional argument on the
|
||||||
|
/// common one: every caller that has a timestamp is the export path, and
|
||||||
|
/// every caller that does not is a test or an adoption.
|
||||||
|
pub fn put_image_at(
|
||||||
|
&mut self,
|
||||||
|
file_id: u64,
|
||||||
|
model_id: &str,
|
||||||
|
source_edge: u32,
|
||||||
|
faces: &[SharedFace],
|
||||||
|
indexed_at: Option<i64>,
|
||||||
) -> Result<u32, CatalogError> {
|
) -> Result<u32, CatalogError> {
|
||||||
let incoming = faces
|
let incoming = faces
|
||||||
.iter()
|
.iter()
|
||||||
@@ -189,16 +233,18 @@ impl FaceShardStore {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT INTO indexed (file_id, model_id, faces_found, source_edge)
|
"INSERT INTO indexed (file_id, model_id, faces_found, source_edge, indexed_at)
|
||||||
VALUES (?1, ?2, ?3, ?4)
|
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||||
ON CONFLICT(file_id, model_id) DO UPDATE SET
|
ON CONFLICT(file_id, model_id) DO UPDATE SET
|
||||||
faces_found = excluded.faces_found,
|
faces_found = excluded.faces_found,
|
||||||
source_edge = excluded.source_edge",
|
source_edge = excluded.source_edge,
|
||||||
|
indexed_at = excluded.indexed_at",
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
file_id as i64,
|
file_id as i64,
|
||||||
model_id,
|
model_id,
|
||||||
faces.len() as i64,
|
faces.len() as i64,
|
||||||
source_edge as i64
|
source_edge as i64,
|
||||||
|
indexed_at
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
@@ -404,19 +450,33 @@ pub fn export_to_shards(
|
|||||||
model_id: &str,
|
model_id: &str,
|
||||||
) -> Result<usize, CatalogError> {
|
) -> Result<usize, CatalogError> {
|
||||||
let mut q = conn.prepare(
|
let mut q = conn.prepare(
|
||||||
"SELECT r.file_id, fi.image_id, fi.source_edge
|
"SELECT r.file_id, fi.image_id, fi.source_edge, fi.indexed_at
|
||||||
FROM face_index fi
|
FROM face_index fi
|
||||||
JOIN remote r ON r.image_id = fi.image_id
|
JOIN remote r ON r.image_id = fi.image_id
|
||||||
WHERE fi.model_id = ?1
|
WHERE fi.model_id = ?1
|
||||||
ORDER BY fi.image_id",
|
ORDER BY fi.image_id",
|
||||||
)?;
|
)?;
|
||||||
let rows: Vec<(i64, i64, i64)> = q
|
let rows: Vec<(i64, i64, i64, i64)> = q
|
||||||
.query_map([model_id], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))?
|
.query_map([model_id], |r| {
|
||||||
|
Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?))
|
||||||
|
})?
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
let mut exported = 0;
|
let mut exported = 0;
|
||||||
for (file_id, image_id, edge) in rows {
|
for (file_id, image_id, edge, indexed_at) in rows {
|
||||||
if store.contains(file_id as u64, model_id) {
|
// **Not `contains`.** Asking only whether the shard has heard of this
|
||||||
|
// image means it is exported once and never again — so re-indexing it
|
||||||
|
// updates the catalog and nothing else, and every other device keeps
|
||||||
|
// the first answer for ever. That is exactly what happened when the
|
||||||
|
// detection floors changed and crops were added: a library re-indexed
|
||||||
|
// from 1,800 faces to 15,000 went on syncing the original 1,800.
|
||||||
|
//
|
||||||
|
// The comparison is against when the *catalog* indexed it, so a
|
||||||
|
// re-index is visible and an unchanged image still costs nothing.
|
||||||
|
if store
|
||||||
|
.indexed_at(file_id as u64, model_id)
|
||||||
|
.is_some_and(|was| was >= indexed_at)
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut fq = conn.prepare(
|
let mut fq = conn.prepare(
|
||||||
@@ -441,7 +501,13 @@ pub fn export_to_shards(
|
|||||||
})?
|
})?
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
store.put_image(file_id as u64, model_id, edge as u32, &faces)?;
|
store.put_image_at(
|
||||||
|
file_id as u64,
|
||||||
|
model_id,
|
||||||
|
edge as u32,
|
||||||
|
&faces,
|
||||||
|
Some(indexed_at),
|
||||||
|
)?;
|
||||||
exported += 1;
|
exported += 1;
|
||||||
}
|
}
|
||||||
Ok(exported)
|
Ok(exported)
|
||||||
@@ -628,8 +694,17 @@ CREATE TABLE IF NOT EXISTS indexed (
|
|||||||
model_id TEXT NOT NULL,
|
model_id TEXT NOT NULL,
|
||||||
faces_found INTEGER NOT NULL,
|
faces_found INTEGER NOT NULL,
|
||||||
source_edge INTEGER NOT NULL,
|
source_edge INTEGER NOT NULL,
|
||||||
PRIMARY KEY(file_id, model_id)
|
-- When the *catalog* indexed this image, copied across rather than the
|
||||||
|
-- time the shard was written. It is what lets a later pass notice that an
|
||||||
|
-- image has been re-indexed since it was exported; a shard-local write
|
||||||
|
-- time could not, because it advances even when nothing changed.
|
||||||
|
--
|
||||||
|
-- Nullable so a shard written before this column can still be read: an
|
||||||
|
-- absent value reads as "unknown", which forces one re-export and then
|
||||||
|
-- settles.
|
||||||
|
indexed_at INTEGER
|
||||||
);
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS indexed_key ON indexed(file_id, model_id);
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
//! A unique index on the name would instead abort the merge transaction at
|
//! A unique index on the name would instead abort the merge transaction at
|
||||||
//! that moment, which is the ordinary case rather than a corner one.
|
//! that moment, which is the ordinary case rather than a corner one.
|
||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::{Connection, OptionalExtension};
|
||||||
|
|
||||||
use crate::error::CatalogError;
|
use crate::error::CatalogError;
|
||||||
|
|
||||||
@@ -101,6 +101,19 @@ pub struct MergeReport {
|
|||||||
pub keywords_deleted: usize,
|
pub keywords_deleted: usize,
|
||||||
/// Keywords where this device's revision was at least as high.
|
/// Keywords where this device's revision was at least as high.
|
||||||
pub keywords_kept_local: usize,
|
pub keywords_kept_local: usize,
|
||||||
|
/// People the remote had and this device did not.
|
||||||
|
pub people_inserted: usize,
|
||||||
|
/// People the remote had renamed, set aside, or brought back.
|
||||||
|
pub people_updated: usize,
|
||||||
|
/// People where this device's revision was at least as high.
|
||||||
|
pub people_kept_local: usize,
|
||||||
|
/// Faces this device now agrees belong to somebody.
|
||||||
|
pub faces_assigned: usize,
|
||||||
|
/// Faces whose local confirmation outranked the remote's.
|
||||||
|
pub faces_kept_local: usize,
|
||||||
|
/// "Not this person" judgements taken from the remote.
|
||||||
|
pub faces_rejected: usize,
|
||||||
|
|
||||||
/// Redundant identities for one word, retired by
|
/// Redundant identities for one word, retired by
|
||||||
/// [`crate::keywords::fuse_duplicates`].
|
/// [`crate::keywords::fuse_duplicates`].
|
||||||
pub keywords_fused: usize,
|
pub keywords_fused: usize,
|
||||||
@@ -179,6 +192,19 @@ pub fn merge_all(conn: &Connection) -> Result<MergeReport, CatalogError> {
|
|||||||
let mut report = MergeReport::default();
|
let mut report = MergeReport::default();
|
||||||
merge_collections_within(&tx, &mut report)?;
|
merge_collections_within(&tx, &mut report)?;
|
||||||
merge_keywords_within(&tx, &mut report)?;
|
merge_keywords_within(&tx, &mut report)?;
|
||||||
|
merge_people_within(&tx, &mut report)?;
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge people and identity judgements from an attached catalog.
|
||||||
|
///
|
||||||
|
/// The people half of [`merge_all`], on its own, for the same reason the other
|
||||||
|
/// two have one: the rules are independent and worth exercising alone.
|
||||||
|
pub fn merge_people(conn: &Connection) -> Result<MergeReport, CatalogError> {
|
||||||
|
let tx = conn.unchecked_transaction()?;
|
||||||
|
let mut report = MergeReport::default();
|
||||||
|
merge_people_within(&tx, &mut report)?;
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
@@ -685,6 +711,381 @@ fn attached_has_table(conn: &Connection, schema: &str, table: &str) -> Result<bo
|
|||||||
Ok(n > 0)
|
Ok(n > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── people, and who the user said they are ────────────────────────────────
|
||||||
|
|
||||||
|
/// Merge people and the user's identity judgements from an attached catalog.
|
||||||
|
///
|
||||||
|
/// # Why this is here at all
|
||||||
|
///
|
||||||
|
/// Face *data* syncs as sealed shards ([`crate::face_shard`]) — boxes,
|
||||||
|
/// landmarks, embeddings, the run marker. What the shards deliberately do not
|
||||||
|
/// carry is who anybody **is**: the person rows, their names, and the
|
||||||
|
/// assignments joining the two. Those were supposed to travel in the catalog
|
||||||
|
/// snapshot, which is a whole-file copy and therefore does contain them — but
|
||||||
|
/// the snapshot is *merged*, not adopted, and this merge only ever looked at
|
||||||
|
/// collections and keywords. So a second device received every face and no
|
||||||
|
/// people at all, and drew an empty People screen over a full catalog.
|
||||||
|
///
|
||||||
|
/// # What travels, and what is recomputed
|
||||||
|
///
|
||||||
|
/// The rule this module already follows for the rest of the catalog: user
|
||||||
|
/// judgements travel, inference is rebuilt. Concretely (docs/faces.md, and the
|
||||||
|
/// asymmetry `crate::faces` opens with):
|
||||||
|
///
|
||||||
|
/// - **People** — uuid, name, and whether the user set them aside. Merged by
|
||||||
|
/// uuid on `revision`, exactly as a collection is.
|
||||||
|
/// - **Confirmations** — the user said this face is this person.
|
||||||
|
/// - **Rejections** — the user said it is *not*, which is equally a fact and
|
||||||
|
/// is why re-clustering does not put it back.
|
||||||
|
/// - **The assignments inside an ignored group** — carried even though they are
|
||||||
|
/// only suggestions, because they are what anchors the ignore. Without them
|
||||||
|
/// a group set aside on one device reappears on the other, which is the same
|
||||||
|
/// fault that made "Not interested" not stick locally.
|
||||||
|
///
|
||||||
|
/// Ordinary suggestions are *not* carried. They are this pass's own output,
|
||||||
|
/// clustering is deterministic, and both devices hold the same embeddings — so
|
||||||
|
/// each recomputes them and arrives at the same answer. Shipping them would
|
||||||
|
/// double the merge for no new information.
|
||||||
|
///
|
||||||
|
/// # Faces have no cross-device identity, so one is derived
|
||||||
|
///
|
||||||
|
/// `faces.id` is a local row id and means nothing in another catalog; there is
|
||||||
|
/// no uuid to fall back on. What both devices *do* agree on is `oc:fileid` and
|
||||||
|
/// the box, so a remote face is matched to the local face on the same
|
||||||
|
/// photograph whose box overlaps it most, above a floor of 0.5 IoU.
|
||||||
|
///
|
||||||
|
/// That is not a new rule: it is the one
|
||||||
|
/// [`crate::faces::record_detections`] already uses to carry a confirmation
|
||||||
|
/// across a re-index, and it is loose on purpose — the question is "is this the
|
||||||
|
/// same face in the frame", not "is this the same rectangle", and a device
|
||||||
|
/// running a newer detector is entitled to have moved the box a little.
|
||||||
|
fn merge_people_within(tx: &Connection, report: &mut MergeReport) -> Result<(), CatalogError> {
|
||||||
|
// A remote written before faces existed has none of these tables, and one
|
||||||
|
// written before V10 has no `ignored`. Both are ordinary — `remote_is_
|
||||||
|
// mergeable` admits any catalog at or below this schema version — so they
|
||||||
|
// are probed for rather than assumed, and an absent one skips this half
|
||||||
|
// instead of aborting a merge that would otherwise have succeeded.
|
||||||
|
if !remote_has(tx, "people")? || !remote_has(tx, "faces")? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let ignored_col = remote_has_column(tx, "people", "ignored")?;
|
||||||
|
// Spelled into the SQL rather than branched around it: the two queries
|
||||||
|
// below would otherwise each need a second copy.
|
||||||
|
let ignored_sel = if ignored_col { "r.ignored" } else { "0" };
|
||||||
|
|
||||||
|
// ---- the people themselves -------------------------------------------
|
||||||
|
struct Incoming {
|
||||||
|
uuid: String,
|
||||||
|
name: String,
|
||||||
|
ignored: bool,
|
||||||
|
created: i64,
|
||||||
|
revision: i64,
|
||||||
|
modified: i64,
|
||||||
|
/// Resolved after every person exists, since a merge target can arrive
|
||||||
|
/// after the person redirecting to it.
|
||||||
|
merged_into_uuid: Option<String>,
|
||||||
|
verdict: MergeVerdict,
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows: Vec<Incoming> = {
|
||||||
|
let mut stmt = tx.prepare(&format!(
|
||||||
|
"SELECT r.uuid, r.name, {ignored_sel}, r.created, r.revision, r.modified,
|
||||||
|
rm.uuid, l.revision, l.modified
|
||||||
|
FROM remote_cat.people r
|
||||||
|
LEFT JOIN main.people l ON l.uuid = r.uuid
|
||||||
|
LEFT JOIN remote_cat.people rm ON rm.id = r.merged_into"
|
||||||
|
))?;
|
||||||
|
let mapped = stmt.query_map([], |r| {
|
||||||
|
let revision: i64 = r.get(4)?;
|
||||||
|
let modified: i64 = r.get(5)?;
|
||||||
|
let local_rev: Option<i64> = r.get(7)?;
|
||||||
|
let local_mod: Option<i64> = r.get(8)?;
|
||||||
|
Ok(Incoming {
|
||||||
|
uuid: r.get(0)?,
|
||||||
|
name: r.get(1)?,
|
||||||
|
ignored: r.get(2)?,
|
||||||
|
created: r.get(3)?,
|
||||||
|
revision,
|
||||||
|
modified,
|
||||||
|
merged_into_uuid: r.get(6)?,
|
||||||
|
// People have no tombstone: a person is merged away rather
|
||||||
|
// than deleted, and `merged_into` is that redirect.
|
||||||
|
verdict: verdict(local_rev.zip(local_mod), (revision, modified), false),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
mapped.collect::<Result<_, _>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
for p in &rows {
|
||||||
|
match p.verdict {
|
||||||
|
MergeVerdict::InsertedFromRemote => {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO people (uuid, name, ignored, created, revision, modified)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
rusqlite::params![p.uuid, p.name, p.ignored, p.created, p.revision, p.modified],
|
||||||
|
)?;
|
||||||
|
report.people_inserted += 1;
|
||||||
|
}
|
||||||
|
MergeVerdict::UpdatedFromRemote | MergeVerdict::DeletedByRemote => {
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE people
|
||||||
|
SET name = ?2, ignored = ?3, revision = ?4, modified = ?5
|
||||||
|
WHERE uuid = ?1",
|
||||||
|
rusqlite::params![p.uuid, p.name, p.ignored, p.revision, p.modified],
|
||||||
|
)?;
|
||||||
|
report.people_updated += 1;
|
||||||
|
}
|
||||||
|
MergeVerdict::KeptLocal => report.people_kept_local += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirects, once every person on both sides exists locally.
|
||||||
|
for p in rows.iter().filter(|p| p.merged_into_uuid.is_some()) {
|
||||||
|
if p.verdict == MergeVerdict::KeptLocal {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE people
|
||||||
|
SET merged_into = (SELECT id FROM people WHERE uuid = ?2)
|
||||||
|
WHERE uuid = ?1",
|
||||||
|
rusqlite::params![p.uuid, p.merged_into_uuid],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- match the remote's faces onto this device's ----------------------
|
||||||
|
let face_map = match_faces(tx)?;
|
||||||
|
if face_map.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- confirmations, and the anchors under an ignored group -----------
|
||||||
|
{
|
||||||
|
let mut stmt = tx.prepare(&format!(
|
||||||
|
"SELECT fp.face_id, p.uuid, fp.probability, fp.confirmed
|
||||||
|
FROM remote_cat.face_person fp
|
||||||
|
JOIN remote_cat.people p ON p.id = fp.person_id
|
||||||
|
WHERE fp.confirmed = 1 OR {} = 1",
|
||||||
|
if ignored_col { "p.ignored" } else { "0" }
|
||||||
|
))?;
|
||||||
|
let incoming: Vec<(i64, String, f64, bool)> = stmt
|
||||||
|
.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)))?
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
for (remote_face, uuid, probability, confirmed) in incoming {
|
||||||
|
let Some(&local_face) = face_map.get(&remote_face) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let person: Option<i64> = tx
|
||||||
|
.query_row("SELECT id FROM people WHERE uuid = ?1", [&uuid], |r| {
|
||||||
|
r.get(0)
|
||||||
|
})
|
||||||
|
.optional()?;
|
||||||
|
let Some(person) = person else { continue };
|
||||||
|
|
||||||
|
// A local confirmation is never overwritten, in either direction.
|
||||||
|
// Two devices confirming the same face as different people is a
|
||||||
|
// genuine disagreement and there is no revision on an assignment to
|
||||||
|
// settle it with; silently taking the remote's answer would let a
|
||||||
|
// sync undo something the user did here. It stays as it is, and the
|
||||||
|
// user can change it on the device they are looking at.
|
||||||
|
let locally_confirmed: bool = tx.query_row(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM face_person
|
||||||
|
WHERE face_id = ?1 AND confirmed = 1)",
|
||||||
|
[local_face],
|
||||||
|
|r| r.get(0),
|
||||||
|
)?;
|
||||||
|
if locally_confirmed {
|
||||||
|
report.faces_kept_local += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A rejection here outranks an assignment from elsewhere: it is
|
||||||
|
// this user's judgement about this pair, and re-suggesting what
|
||||||
|
// they pushed away is the behaviour that makes the feature feel
|
||||||
|
// broken.
|
||||||
|
let rejected: bool = tx.query_row(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM face_person_rejected
|
||||||
|
WHERE face_id = ?1 AND person_id = ?2)",
|
||||||
|
[local_face, person],
|
||||||
|
|r| r.get(0),
|
||||||
|
)?;
|
||||||
|
if rejected {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO face_person (face_id, person_id, probability, confirmed)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)
|
||||||
|
ON CONFLICT(face_id) DO UPDATE SET
|
||||||
|
person_id = excluded.person_id,
|
||||||
|
probability = excluded.probability,
|
||||||
|
confirmed = excluded.confirmed",
|
||||||
|
rusqlite::params![local_face, person, probability, confirmed],
|
||||||
|
)?;
|
||||||
|
report.faces_assigned += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- rejections, as a set union --------------------------------------
|
||||||
|
{
|
||||||
|
let mut stmt = tx.prepare(
|
||||||
|
"SELECT fr.face_id, p.uuid
|
||||||
|
FROM remote_cat.face_person_rejected fr
|
||||||
|
JOIN remote_cat.people p ON p.id = fr.person_id",
|
||||||
|
)?;
|
||||||
|
let incoming: Vec<(i64, String)> = stmt
|
||||||
|
.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
for (remote_face, uuid) in incoming {
|
||||||
|
let Some(&local_face) = face_map.get(&remote_face) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let person: Option<i64> = tx
|
||||||
|
.query_row("SELECT id FROM people WHERE uuid = ?1", [&uuid], |r| {
|
||||||
|
r.get(0)
|
||||||
|
})
|
||||||
|
.optional()?;
|
||||||
|
let Some(person) = person else { continue };
|
||||||
|
|
||||||
|
let n = tx.execute(
|
||||||
|
"INSERT OR IGNORE INTO face_person_rejected (face_id, person_id)
|
||||||
|
VALUES (?1, ?2)",
|
||||||
|
[local_face, person],
|
||||||
|
)?;
|
||||||
|
report.faces_rejected += n;
|
||||||
|
|
||||||
|
// A rejection that lands on a face currently *suggested* to be
|
||||||
|
// that person has to take the suggestion with it, or the screen
|
||||||
|
// keeps offering exactly what the other device just refused.
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM face_person
|
||||||
|
WHERE face_id = ?1 AND person_id = ?2 AND confirmed = 0",
|
||||||
|
[local_face, person],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the attached remote holds a table.
|
||||||
|
fn remote_has(tx: &Connection, table: &str) -> Result<bool, CatalogError> {
|
||||||
|
Ok(tx.query_row(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM remote_cat.sqlite_master
|
||||||
|
WHERE type = 'table' AND name = ?1)",
|
||||||
|
[table],
|
||||||
|
|r| r.get::<_, bool>(0),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a table in the attached remote holds a column.
|
||||||
|
fn remote_has_column(tx: &Connection, table: &str, column: &str) -> Result<bool, CatalogError> {
|
||||||
|
// `pragma_table_info` takes the schema as a second argument, which is the
|
||||||
|
// only way to ask about an attached database rather than the main one.
|
||||||
|
let mut stmt =
|
||||||
|
tx.prepare("SELECT 1 FROM pragma_table_info(?1, 'remote_cat') WHERE name = ?2")?;
|
||||||
|
Ok(stmt.exists(rusqlite::params![table, column])?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remote face row id to local face row id, by photograph and box overlap.
|
||||||
|
///
|
||||||
|
/// See [`merge_people_within`] for why a face has no shared identity and this
|
||||||
|
/// has to be derived. Only faces from the same model are compared: boxes from
|
||||||
|
/// two different detectors are not the same measurement, and matching across
|
||||||
|
/// them would attach a judgement to a face nobody looked at.
|
||||||
|
fn match_faces(tx: &Connection) -> Result<std::collections::HashMap<i64, i64>, CatalogError> {
|
||||||
|
/// Loose on purpose — "the same face in the frame", not "the same
|
||||||
|
/// rectangle". The figure `record_detections` uses for the same job.
|
||||||
|
const MIN_IOU: f32 = 0.5;
|
||||||
|
|
||||||
|
type Boxed = (i64, f32, f32, f32, f32);
|
||||||
|
|
||||||
|
// Local faces, grouped by the photograph's cross-device id.
|
||||||
|
let mut local: std::collections::HashMap<(i64, String), Vec<Boxed>> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
{
|
||||||
|
let mut stmt = tx.prepare(
|
||||||
|
"SELECT f.id, r.file_id, f.model_id, f.x, f.y, f.w, f.h
|
||||||
|
FROM main.faces f
|
||||||
|
JOIN main.remote r ON r.image_id = f.image_id
|
||||||
|
WHERE r.file_id IS NOT NULL",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map([], |r| {
|
||||||
|
Ok((
|
||||||
|
r.get::<_, i64>(1)?,
|
||||||
|
r.get::<_, String>(2)?,
|
||||||
|
(
|
||||||
|
r.get::<_, i64>(0)?,
|
||||||
|
r.get::<_, f64>(3)? as f32,
|
||||||
|
r.get::<_, f64>(4)? as f32,
|
||||||
|
r.get::<_, f64>(5)? as f32,
|
||||||
|
r.get::<_, f64>(6)? as f32,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
for row in rows {
|
||||||
|
let (file_id, model, boxed) = row?;
|
||||||
|
local.entry((file_id, model)).or_default().push(boxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if local.is_empty() {
|
||||||
|
return Ok(Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut map = std::collections::HashMap::new();
|
||||||
|
let mut stmt = tx.prepare(
|
||||||
|
"SELECT f.id, r.file_id, f.model_id, f.x, f.y, f.w, f.h
|
||||||
|
FROM remote_cat.faces f
|
||||||
|
JOIN remote_cat.remote r ON r.image_id = f.image_id
|
||||||
|
WHERE r.file_id IS NOT NULL",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map([], |r| {
|
||||||
|
Ok((
|
||||||
|
r.get::<_, i64>(0)?,
|
||||||
|
r.get::<_, i64>(1)?,
|
||||||
|
r.get::<_, String>(2)?,
|
||||||
|
(
|
||||||
|
r.get::<_, f64>(3)? as f32,
|
||||||
|
r.get::<_, f64>(4)? as f32,
|
||||||
|
r.get::<_, f64>(5)? as f32,
|
||||||
|
r.get::<_, f64>(6)? as f32,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let (remote_id, file_id, model, rbox) = row?;
|
||||||
|
let Some(candidates) = local.get(&(file_id, model)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let best = candidates
|
||||||
|
.iter()
|
||||||
|
.map(|&(id, x, y, w, h)| (id, iou(rbox, (x, y, w, h))))
|
||||||
|
.filter(|&(_, score)| score >= MIN_IOU)
|
||||||
|
.max_by(|a, b| a.1.total_cmp(&b.1));
|
||||||
|
if let Some((local_id, _)) = best {
|
||||||
|
map.insert(remote_id, local_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Intersection over union of two `(x, y, w, h)` boxes.
|
||||||
|
fn iou(a: (f32, f32, f32, f32), b: (f32, f32, f32, f32)) -> f32 {
|
||||||
|
let x0 = a.0.max(b.0);
|
||||||
|
let y0 = a.1.max(b.1);
|
||||||
|
let x1 = (a.0 + a.2).min(b.0 + b.2);
|
||||||
|
let y1 = (a.1 + a.3).min(b.1 + b.3);
|
||||||
|
let inter = (x1 - x0).max(0.0) * (y1 - y0).max(0.0);
|
||||||
|
let union = a.2 * a.3 + b.2 * b.3 - inter;
|
||||||
|
if union <= 0.0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
inter / union
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1490,4 +1891,286 @@ mod tests {
|
|||||||
assert_eq!(report.kept_local, 1);
|
assert_eq!(report.kept_local, 1);
|
||||||
assert!(report.should_upload());
|
assert!(report.should_upload());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── people, and who the user said they are ────────────────────────────
|
||||||
|
|
||||||
|
/// An image present in `db` and carrying the cross-device file id both
|
||||||
|
/// catalogs agree on.
|
||||||
|
fn add_synced_image(c: &Connection, db: &str, id: i64, file_id: i64) {
|
||||||
|
add_image_without_hash(c, db, id);
|
||||||
|
c.execute(
|
||||||
|
&format!("INSERT INTO {db}.remote(image_id, file_id) VALUES (?1, ?2)"),
|
||||||
|
rusqlite::params![id, file_id],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A face on `image`, at a box the caller can nudge to test the matching.
|
||||||
|
fn add_face(c: &Connection, db: &str, id: i64, image: i64, x: f64) -> i64 {
|
||||||
|
c.execute(
|
||||||
|
&format!(
|
||||||
|
"INSERT INTO {db}.faces
|
||||||
|
(id, image_id, x, y, w, h, landmarks, detector_confidence,
|
||||||
|
embedding, crop_px, model_id, detected_at)
|
||||||
|
VALUES (?1, ?2, ?3, 0.2, 0.2, 0.2, X'00', 0.9, X'00', 150.0,
|
||||||
|
'w600k_mbf', 0)"
|
||||||
|
),
|
||||||
|
rusqlite::params![id, image, x],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_person(c: &Connection, db: &str, id: i64, uuid: &str, name: &str, ignored: bool) {
|
||||||
|
c.execute(
|
||||||
|
&format!(
|
||||||
|
"INSERT INTO {db}.people(id, uuid, name, ignored, created, revision, modified)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, 0, 1, 1)"
|
||||||
|
),
|
||||||
|
rusqlite::params![id, uuid, name, ignored],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assign(c: &Connection, db: &str, face: i64, person: i64, confirmed: bool) {
|
||||||
|
c.execute(
|
||||||
|
&format!(
|
||||||
|
"INSERT INTO {db}.face_person(face_id, person_id, probability, confirmed)
|
||||||
|
VALUES (?1, ?2, 0.9, ?3)"
|
||||||
|
),
|
||||||
|
rusqlite::params![face, person, confirmed],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn person_of(c: &Connection, face: i64) -> Option<(String, bool)> {
|
||||||
|
c.query_row(
|
||||||
|
"SELECT p.name, fp.confirmed
|
||||||
|
FROM main.face_person fp JOIN main.people p ON p.id = fp.person_id
|
||||||
|
WHERE fp.face_id = ?1",
|
||||||
|
[face],
|
||||||
|
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bug: a second device received every face through the shards and no
|
||||||
|
/// people at all, because this merge only ever looked at collections and
|
||||||
|
/// keywords. It drew an empty People screen over a full catalog.
|
||||||
|
#[test]
|
||||||
|
fn a_named_person_and_their_confirmed_face_cross_over() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
for db in ["main", "remote_cat"] {
|
||||||
|
add_synced_image(&c, db, 1, 5000);
|
||||||
|
}
|
||||||
|
// The same face, found independently on each device, so the row ids
|
||||||
|
// differ — which is the whole difficulty.
|
||||||
|
let local = add_face(&c, "main", 7, 1, 0.30);
|
||||||
|
let remote = add_face(&c, "remote_cat", 42, 1, 0.31);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-anna", "Anna", false);
|
||||||
|
assign(&c, "remote_cat", remote, 3, true);
|
||||||
|
|
||||||
|
let report = merge_all(&c).unwrap();
|
||||||
|
assert_eq!(report.people_inserted, 1);
|
||||||
|
assert_eq!(report.faces_assigned, 1);
|
||||||
|
assert_eq!(person_of(&c, local), Some(("Anna".to_string(), true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boxes from two devices are close but not identical. Matching has to be
|
||||||
|
/// by overlap, not equality, or nothing ever lines up.
|
||||||
|
#[test]
|
||||||
|
fn a_face_in_a_different_photograph_is_not_matched() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
for db in ["main", "remote_cat"] {
|
||||||
|
add_synced_image(&c, db, 1, 5000);
|
||||||
|
add_synced_image(&c, db, 2, 6000);
|
||||||
|
}
|
||||||
|
let elsewhere = add_face(&c, "main", 7, 2, 0.30);
|
||||||
|
let remote = add_face(&c, "remote_cat", 42, 1, 0.30);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-anna", "Anna", false);
|
||||||
|
assign(&c, "remote_cat", remote, 3, true);
|
||||||
|
|
||||||
|
merge_all(&c).unwrap();
|
||||||
|
assert_eq!(person_of(&c, elsewhere), None, "matched across photographs");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two faces in one frame, and the judgement must land on the right one.
|
||||||
|
#[test]
|
||||||
|
fn the_overlapping_face_is_the_one_that_gets_the_name() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
for db in ["main", "remote_cat"] {
|
||||||
|
add_synced_image(&c, db, 1, 5000);
|
||||||
|
}
|
||||||
|
let left = add_face(&c, "main", 7, 1, 0.10);
|
||||||
|
let right = add_face(&c, "main", 8, 1, 0.70);
|
||||||
|
let remote = add_face(&c, "remote_cat", 42, 1, 0.71);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-bob", "Bob", false);
|
||||||
|
assign(&c, "remote_cat", remote, 3, true);
|
||||||
|
|
||||||
|
merge_all(&c).unwrap();
|
||||||
|
assert_eq!(person_of(&c, right), Some(("Bob".to_string(), true)));
|
||||||
|
assert_eq!(person_of(&c, left), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A group set aside on one device stays set aside on the other — which
|
||||||
|
/// needs its *suggestions* to travel, since that is what anchors it.
|
||||||
|
#[test]
|
||||||
|
fn a_group_set_aside_stays_set_aside_on_the_other_device() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
for db in ["main", "remote_cat"] {
|
||||||
|
add_synced_image(&c, db, 1, 5000);
|
||||||
|
}
|
||||||
|
let local = add_face(&c, "main", 7, 1, 0.30);
|
||||||
|
let remote = add_face(&c, "remote_cat", 42, 1, 0.30);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-stranger", "", true);
|
||||||
|
// Only ever a suggestion, which is exactly why it needs carrying.
|
||||||
|
assign(&c, "remote_cat", remote, 3, false);
|
||||||
|
|
||||||
|
merge_all(&c).unwrap();
|
||||||
|
let ignored: bool = c
|
||||||
|
.query_row(
|
||||||
|
"SELECT ignored FROM main.people WHERE uuid = 'u-stranger'",
|
||||||
|
[],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(ignored, "the set-aside flag did not travel");
|
||||||
|
assert_eq!(person_of(&c, local), Some((String::new(), false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ordinary suggestion is this pass's own output. Both devices hold the
|
||||||
|
/// same embeddings and clustering is deterministic, so each recomputes it —
|
||||||
|
/// shipping it would double the merge for no new information.
|
||||||
|
#[test]
|
||||||
|
fn an_ordinary_suggestion_does_not_travel() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
for db in ["main", "remote_cat"] {
|
||||||
|
add_synced_image(&c, db, 1, 5000);
|
||||||
|
}
|
||||||
|
let local = add_face(&c, "main", 7, 1, 0.30);
|
||||||
|
let remote = add_face(&c, "remote_cat", 42, 1, 0.30);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-guess", "", false);
|
||||||
|
assign(&c, "remote_cat", remote, 3, false);
|
||||||
|
|
||||||
|
merge_all(&c).unwrap();
|
||||||
|
assert_eq!(person_of(&c, local), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sync must not undo what the user did on the device they are holding.
|
||||||
|
#[test]
|
||||||
|
fn a_local_confirmation_outranks_the_remotes() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
for db in ["main", "remote_cat"] {
|
||||||
|
add_synced_image(&c, db, 1, 5000);
|
||||||
|
}
|
||||||
|
let local = add_face(&c, "main", 7, 1, 0.30);
|
||||||
|
let remote = add_face(&c, "remote_cat", 42, 1, 0.30);
|
||||||
|
add_person(&c, "main", 1, "u-anna", "Anna", false);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-bob", "Bob", false);
|
||||||
|
assign(&c, "main", local, 1, true);
|
||||||
|
assign(&c, "remote_cat", remote, 3, true);
|
||||||
|
|
||||||
|
let report = merge_all(&c).unwrap();
|
||||||
|
assert_eq!(report.faces_kept_local, 1);
|
||||||
|
assert_eq!(person_of(&c, local), Some(("Anna".to_string(), true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Not this person" is a judgement too, and it has to outrank an
|
||||||
|
/// assignment arriving from elsewhere.
|
||||||
|
#[test]
|
||||||
|
fn a_rejection_travels_and_removes_the_suggestion_it_contradicts() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
for db in ["main", "remote_cat"] {
|
||||||
|
add_synced_image(&c, db, 1, 5000);
|
||||||
|
}
|
||||||
|
let local = add_face(&c, "main", 7, 1, 0.30);
|
||||||
|
let remote = add_face(&c, "remote_cat", 42, 1, 0.30);
|
||||||
|
add_person(&c, "main", 1, "u-anna", "Anna", false);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-anna", "Anna", false);
|
||||||
|
// Locally suggested; the other device says it is not her.
|
||||||
|
assign(&c, "main", local, 1, false);
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO remote_cat.face_person_rejected(face_id, person_id)
|
||||||
|
VALUES (?1, 3)",
|
||||||
|
[remote],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
merge_all(&c).unwrap();
|
||||||
|
assert_eq!(person_of(&c, local), None, "the refused suggestion stayed");
|
||||||
|
let rejected: bool = c
|
||||||
|
.query_row(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM main.face_person_rejected WHERE face_id = ?1)",
|
||||||
|
[local],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(rejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rename on the other device wins on revision, like everything else.
|
||||||
|
#[test]
|
||||||
|
fn a_higher_revision_renames_a_person() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
add_person(&c, "main", 1, "u-anna", "Ana", false);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-anna", "Anna", false);
|
||||||
|
c.execute(
|
||||||
|
"UPDATE remote_cat.people SET revision = 5, modified = 5 WHERE uuid = 'u-anna'",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let report = merge_all(&c).unwrap();
|
||||||
|
assert_eq!(report.people_updated, 1);
|
||||||
|
let name: String = c
|
||||||
|
.query_row(
|
||||||
|
"SELECT name FROM main.people WHERE uuid = 'u-anna'",
|
||||||
|
[],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(name, "Anna");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_lower_revision_does_not_rename_a_person() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
add_person(&c, "main", 1, "u-anna", "Anna", false);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-anna", "Ana", false);
|
||||||
|
c.execute("UPDATE main.people SET revision = 9, modified = 9", [])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let report = merge_all(&c).unwrap();
|
||||||
|
assert_eq!(report.people_kept_local, 1);
|
||||||
|
let name: String = c
|
||||||
|
.query_row(
|
||||||
|
"SELECT name FROM main.people WHERE uuid = 'u-anna'",
|
||||||
|
[],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(name, "Anna");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merging twice must not double anything — a sync runs on every pass.
|
||||||
|
#[test]
|
||||||
|
fn merging_people_twice_changes_nothing_the_second_time() {
|
||||||
|
let c = two_catalogs();
|
||||||
|
for db in ["main", "remote_cat"] {
|
||||||
|
add_synced_image(&c, db, 1, 5000);
|
||||||
|
}
|
||||||
|
add_face(&c, "main", 7, 1, 0.30);
|
||||||
|
let remote = add_face(&c, "remote_cat", 42, 1, 0.30);
|
||||||
|
add_person(&c, "remote_cat", 3, "u-anna", "Anna", false);
|
||||||
|
assign(&c, "remote_cat", remote, 3, true);
|
||||||
|
|
||||||
|
merge_all(&c).unwrap();
|
||||||
|
let second = merge_all(&c).unwrap();
|
||||||
|
assert_eq!(second.people_inserted, 0);
|
||||||
|
let people: i64 = c
|
||||||
|
.query_row("SELECT COUNT(*) FROM main.people", [], |r| r.get(0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(people, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,10 +206,17 @@ pub fn v1_for_attached(schema_name: &str) -> String {
|
|||||||
/// lost by its absence — it exists to make the *grid* page quickly, and the
|
/// lost by its absence — it exists to make the *grid* page quickly, and the
|
||||||
/// grid never reads across an attachment.
|
/// grid never reads across an attachment.
|
||||||
pub fn for_attached(schema_name: &str) -> String {
|
pub fn for_attached(schema_name: &str) -> String {
|
||||||
|
// V10 is `ALTER TABLE`, which the textual rewrite cannot qualify, so its
|
||||||
|
// columns are spelled out. A remote genuinely older than V10 is a real
|
||||||
|
// case and `merge::merge_people_within` probes for them; this is the
|
||||||
|
// *current* shape, which is what the tests want.
|
||||||
format!(
|
format!(
|
||||||
"{}\n{}",
|
"{}\n{}\n{}\n\
|
||||||
|
ALTER TABLE {schema_name}.people ADD COLUMN ignored INTEGER NOT NULL DEFAULT 0;\n\
|
||||||
|
ALTER TABLE {schema_name}.faces ADD COLUMN crop BLOB;",
|
||||||
rewrite_for_attached(V1, schema_name),
|
rewrite_for_attached(V1, schema_name),
|
||||||
rewrite_for_attached(V6, schema_name)
|
rewrite_for_attached(V6, schema_name),
|
||||||
|
rewrite_for_attached(V8, schema_name),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -38,24 +38,24 @@ _None._
|
|||||||
| 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:1126`](../ui/dr-ui/src/lib.rs#L1126), [`ui/dr-ui/src/library.rs:163`](../ui/dr-ui/src/library.rs#L163), [`ui/dr-ui/src/library.rs:2247`](../ui/dr-ui/src/library.rs#L2247), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
| 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:1126`](../ui/dr-ui/src/lib.rs#L1126), [`ui/dr-ui/src/library.rs:163`](../ui/dr-ui/src/library.rs#L163), [`ui/dr-ui/src/library.rs:2247`](../ui/dr-ui/src/library.rs#L2247), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
||||||
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118) |
|
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118) |
|
||||||
| FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) |
|
| FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) |
|
||||||
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:634`](../core/dr-catalog/src/schema.rs#L634), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1225`](../ui/dr-ui/src/collections_ui.rs#L1225), [`ui/dr-ui/src/collections_ui.rs:1995`](../ui/dr-ui/src/collections_ui.rs#L1995), [`ui/dr-ui/src/library.rs:163`](../ui/dr-ui/src/library.rs#L163), [`ui/dr-ui/src/library.rs:180`](../ui/dr-ui/src/library.rs#L180), [`ui/dr-ui/src/library.rs:209`](../ui/dr-ui/src/library.rs#L209), [`ui/dr-ui/src/library.rs:3661`](../ui/dr-ui/src/library.rs#L3661), [`ui/dr-ui/src/library.rs:3695`](../ui/dr-ui/src/library.rs#L3695), [`ui/dr-ui/src/library_ui.rs:185`](../ui/dr-ui/src/library_ui.rs#L185), [`ui/dr-ui/src/library_ui.rs:758`](../ui/dr-ui/src/library_ui.rs#L758), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) |
|
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:641`](../core/dr-catalog/src/schema.rs#L641), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1225`](../ui/dr-ui/src/collections_ui.rs#L1225), [`ui/dr-ui/src/collections_ui.rs:1995`](../ui/dr-ui/src/collections_ui.rs#L1995), [`ui/dr-ui/src/library.rs:163`](../ui/dr-ui/src/library.rs#L163), [`ui/dr-ui/src/library.rs:180`](../ui/dr-ui/src/library.rs#L180), [`ui/dr-ui/src/library.rs:209`](../ui/dr-ui/src/library.rs#L209), [`ui/dr-ui/src/library.rs:3661`](../ui/dr-ui/src/library.rs#L3661), [`ui/dr-ui/src/library.rs:3695`](../ui/dr-ui/src/library.rs#L3695), [`ui/dr-ui/src/library_ui.rs:185`](../ui/dr-ui/src/library_ui.rs#L185), [`ui/dr-ui/src/library_ui.rs:758`](../ui/dr-ui/src/library_ui.rs#L758), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) |
|
||||||
| FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`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-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
| 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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
||||||
| 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:69`](../core/dr-sync/src/scan.rs#L69), [`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:464`](../ui/dr-ui/src/import.rs#L464), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489), [`ui/dr-ui/src/library.rs:2724`](../ui/dr-ui/src/library.rs#L2724), [`ui/dr-ui/src/library.rs:3195`](../ui/dr-ui/src/library.rs#L3195), [`ui/dr-ui/src/library_ui.rs:172`](../ui/dr-ui/src/library_ui.rs#L172), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/src/library_ui.rs:4593`](../ui/dr-ui/src/library_ui.rs#L4593), [`ui/dr-ui/ui/app.slint:316`](../ui/dr-ui/ui/app.slint#L316), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
| 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:69`](../core/dr-sync/src/scan.rs#L69), [`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:464`](../ui/dr-ui/src/import.rs#L464), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489), [`ui/dr-ui/src/library.rs:2724`](../ui/dr-ui/src/library.rs#L2724), [`ui/dr-ui/src/library.rs:3195`](../ui/dr-ui/src/library.rs#L3195), [`ui/dr-ui/src/library_ui.rs:172`](../ui/dr-ui/src/library_ui.rs#L172), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/src/library_ui.rs:4593`](../ui/dr-ui/src/library_ui.rs#L4593), [`ui/dr-ui/ui/app.slint:316`](../ui/dr-ui/ui/app.slint#L316), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
||||||
| 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:311`](../core/dr-catalog/src/schema.rs#L311), [`ui/dr-ui/src/library.rs:190`](../ui/dr-ui/src/library.rs#L190), [`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) |
|
| 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:318`](../core/dr-catalog/src/schema.rs#L318), [`ui/dr-ui/src/library.rs:190`](../ui/dr-ui/src/library.rs#L190), [`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) |
|
||||||
| 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:1052`](../core/dr-catalog/src/schema.rs#L1052), [`core/dr-catalog/src/schema.rs:549`](../core/dr-catalog/src/schema.rs#L549), [`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:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3709`](../ui/dr-ui/src/library.rs#L3709), [`ui/dr-ui/src/library_ui.rs:629`](../ui/dr-ui/src/library_ui.rs#L629), [`ui/dr-ui/src/library_ui.rs:6390`](../ui/dr-ui/src/library_ui.rs#L6390), [`ui/dr-ui/src/library_ui.rs:6401`](../ui/dr-ui/src/library_ui.rs#L6401), [`ui/dr-ui/src/library_ui.rs:6414`](../ui/dr-ui/src/library_ui.rs#L6414), [`ui/dr-ui/src/library_ui.rs:6429`](../ui/dr-ui/src/library_ui.rs#L6429), [`ui/dr-ui/src/library_ui.rs:6438`](../ui/dr-ui/src/library_ui.rs#L6438), [`ui/dr-ui/ui/app.slint:261`](../ui/dr-ui/ui/app.slint#L261), [`ui/dr-ui/ui/app.slint:442`](../ui/dr-ui/ui/app.slint#L442), [`ui/dr-ui/ui/library.slint:1225`](../ui/dr-ui/ui/library.slint#L1225), [`ui/dr-ui/ui/library.slint:1228`](../ui/dr-ui/ui/library.slint#L1228), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:856`](../ui/dr-ui/ui/library.slint#L856), [`ui/dr-ui/ui/library.slint:908`](../ui/dr-ui/ui/library.slint#L908) |
|
| 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:1059`](../core/dr-catalog/src/schema.rs#L1059), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`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:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3709`](../ui/dr-ui/src/library.rs#L3709), [`ui/dr-ui/src/library_ui.rs:629`](../ui/dr-ui/src/library_ui.rs#L629), [`ui/dr-ui/src/library_ui.rs:6390`](../ui/dr-ui/src/library_ui.rs#L6390), [`ui/dr-ui/src/library_ui.rs:6401`](../ui/dr-ui/src/library_ui.rs#L6401), [`ui/dr-ui/src/library_ui.rs:6414`](../ui/dr-ui/src/library_ui.rs#L6414), [`ui/dr-ui/src/library_ui.rs:6429`](../ui/dr-ui/src/library_ui.rs#L6429), [`ui/dr-ui/src/library_ui.rs:6438`](../ui/dr-ui/src/library_ui.rs#L6438), [`ui/dr-ui/ui/app.slint:261`](../ui/dr-ui/ui/app.slint#L261), [`ui/dr-ui/ui/app.slint:442`](../ui/dr-ui/ui/app.slint#L442), [`ui/dr-ui/ui/library.slint:1225`](../ui/dr-ui/ui/library.slint#L1225), [`ui/dr-ui/ui/library.slint:1228`](../ui/dr-ui/ui/library.slint#L1228), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:856`](../ui/dr-ui/ui/library.slint#L856), [`ui/dr-ui/ui/library.slint:908`](../ui/dr-ui/ui/library.slint#L908) |
|
||||||
| 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:549`](../core/dr-catalog/src/schema.rs#L549), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`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:214`](../ui/dr-ui/src/library.rs#L214), [`ui/dr-ui/src/library.rs:3955`](../ui/dr-ui/src/library.rs#L3955), [`ui/dr-ui/src/library_ui.rs:316`](../ui/dr-ui/src/library_ui.rs#L316), [`ui/dr-ui/src/library_ui.rs:388`](../ui/dr-ui/src/library_ui.rs#L388), [`ui/dr-ui/src/library_ui.rs:5204`](../ui/dr-ui/src/library_ui.rs#L5204), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:6530`](../ui/dr-ui/src/library_ui.rs#L6530), [`ui/dr-ui/ui/app.slint:264`](../ui/dr-ui/ui/app.slint#L264), [`ui/dr-ui/ui/app.slint:442`](../ui/dr-ui/ui/app.slint#L442), [`ui/dr-ui/ui/app.slint:687`](../ui/dr-ui/ui/app.slint#L687), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1301`](../ui/dr-ui/ui/library.slint#L1301), [`ui/dr-ui/ui/library.slint:908`](../ui/dr-ui/ui/library.slint#L908), [`ui/dr-ui/ui/settings.slint:147`](../ui/dr-ui/ui/settings.slint#L147) |
|
| 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:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`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:214`](../ui/dr-ui/src/library.rs#L214), [`ui/dr-ui/src/library.rs:3955`](../ui/dr-ui/src/library.rs#L3955), [`ui/dr-ui/src/library_ui.rs:316`](../ui/dr-ui/src/library_ui.rs#L316), [`ui/dr-ui/src/library_ui.rs:388`](../ui/dr-ui/src/library_ui.rs#L388), [`ui/dr-ui/src/library_ui.rs:5204`](../ui/dr-ui/src/library_ui.rs#L5204), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:6530`](../ui/dr-ui/src/library_ui.rs#L6530), [`ui/dr-ui/ui/app.slint:264`](../ui/dr-ui/ui/app.slint#L264), [`ui/dr-ui/ui/app.slint:442`](../ui/dr-ui/ui/app.slint#L442), [`ui/dr-ui/ui/app.slint:687`](../ui/dr-ui/ui/app.slint#L687), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1301`](../ui/dr-ui/ui/library.slint#L1301), [`ui/dr-ui/ui/library.slint:908`](../ui/dr-ui/ui/library.slint#L908), [`ui/dr-ui/ui/settings.slint:147`](../ui/dr-ui/ui/settings.slint#L147) |
|
||||||
| 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:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3485`](../ui/dr-ui/src/library_ui.rs#L3485), [`ui/dr-ui/src/library_ui.rs:4183`](../ui/dr-ui/src/library_ui.rs#L4183), [`ui/dr-ui/ui/app.slint:437`](../ui/dr-ui/ui/app.slint#L437), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2564`](../ui/dr-ui/ui/library.slint#L2564), [`ui/dr-ui/ui/library.slint:879`](../ui/dr-ui/ui/library.slint#L879), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898) |
|
| 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:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3485`](../ui/dr-ui/src/library_ui.rs#L3485), [`ui/dr-ui/src/library_ui.rs:4183`](../ui/dr-ui/src/library_ui.rs#L4183), [`ui/dr-ui/ui/app.slint:437`](../ui/dr-ui/ui/app.slint#L437), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2564`](../ui/dr-ui/ui/library.slint#L2564), [`ui/dr-ui/ui/library.slint:879`](../ui/dr-ui/ui/library.slint#L879), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898) |
|
||||||
| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`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:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`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), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:3374`](../ui/dr-ui/src/develop.rs#L3374), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1387`](../ui/dr-ui/src/lib.rs#L1387), [`ui/dr-ui/src/lib.rs:1788`](../ui/dr-ui/src/lib.rs#L1788), [`ui/dr-ui/src/lib.rs:1924`](../ui/dr-ui/src/lib.rs#L1924), [`ui/dr-ui/src/lib.rs:498`](../ui/dr-ui/src/lib.rs#L498), [`ui/dr-ui/src/lib.rs:923`](../ui/dr-ui/src/lib.rs#L923), [`ui/dr-ui/src/library.rs:1656`](../ui/dr-ui/src/library.rs#L1656), [`ui/dr-ui/src/library.rs:460`](../ui/dr-ui/src/library.rs#L460), [`ui/dr-ui/src/library.rs:507`](../ui/dr-ui/src/library.rs#L507), [`ui/dr-ui/src/library.rs:544`](../ui/dr-ui/src/library.rs#L544), [`ui/dr-ui/src/library.rs:792`](../ui/dr-ui/src/library.rs#L792), [`ui/dr-ui/src/library_ui.rs:4885`](../ui/dr-ui/src/library_ui.rs#L4885), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`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:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`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), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:3374`](../ui/dr-ui/src/develop.rs#L3374), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1387`](../ui/dr-ui/src/lib.rs#L1387), [`ui/dr-ui/src/lib.rs:1788`](../ui/dr-ui/src/lib.rs#L1788), [`ui/dr-ui/src/lib.rs:1924`](../ui/dr-ui/src/lib.rs#L1924), [`ui/dr-ui/src/lib.rs:498`](../ui/dr-ui/src/lib.rs#L498), [`ui/dr-ui/src/lib.rs:923`](../ui/dr-ui/src/lib.rs#L923), [`ui/dr-ui/src/library.rs:1656`](../ui/dr-ui/src/library.rs#L1656), [`ui/dr-ui/src/library.rs:460`](../ui/dr-ui/src/library.rs#L460), [`ui/dr-ui/src/library.rs:507`](../ui/dr-ui/src/library.rs#L507), [`ui/dr-ui/src/library.rs:544`](../ui/dr-ui/src/library.rs#L544), [`ui/dr-ui/src/library.rs:792`](../ui/dr-ui/src/library.rs#L792), [`ui/dr-ui/src/library_ui.rs:4885`](../ui/dr-ui/src/library_ui.rs#L4885), [`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:606`](../core/dr-catalog/src/schema.rs#L606), [`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:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2870`](../ui/dr-ui/src/develop.rs#L2870), [`ui/dr-ui/src/library.rs:149`](../ui/dr-ui/src/library.rs#L149), [`ui/dr-ui/src/library.rs:1616`](../ui/dr-ui/src/library.rs#L1616), [`ui/dr-ui/src/library.rs:1693`](../ui/dr-ui/src/library.rs#L1693), [`ui/dr-ui/src/library.rs:234`](../ui/dr-ui/src/library.rs#L234), [`ui/dr-ui/src/library.rs:4062`](../ui/dr-ui/src/library.rs#L4062), [`ui/dr-ui/src/library.rs:544`](../ui/dr-ui/src/library.rs#L544), [`ui/dr-ui/src/library.rs:776`](../ui/dr-ui/src/library.rs#L776), [`ui/dr-ui/src/library.rs:792`](../ui/dr-ui/src/library.rs#L792), [`ui/dr-ui/src/library.rs:846`](../ui/dr-ui/src/library.rs#L846), [`ui/dr-ui/src/library_ui.rs:1565`](../ui/dr-ui/src/library_ui.rs#L1565), [`ui/dr-ui/src/library_ui.rs:1591`](../ui/dr-ui/src/library_ui.rs#L1591), [`ui/dr-ui/src/library_ui.rs:1607`](../ui/dr-ui/src/library_ui.rs#L1607), [`ui/dr-ui/src/library_ui.rs:1701`](../ui/dr-ui/src/library_ui.rs#L1701), [`ui/dr-ui/src/library_ui.rs:227`](../ui/dr-ui/src/library_ui.rs#L227), [`ui/dr-ui/src/library_ui.rs:2317`](../ui/dr-ui/src/library_ui.rs#L2317), [`ui/dr-ui/src/library_ui.rs:260`](../ui/dr-ui/src/library_ui.rs#L260), [`ui/dr-ui/src/library_ui.rs:2755`](../ui/dr-ui/src/library_ui.rs#L2755), [`ui/dr-ui/src/library_ui.rs:2979`](../ui/dr-ui/src/library_ui.rs#L2979), [`ui/dr-ui/src/library_ui.rs:3260`](../ui/dr-ui/src/library_ui.rs#L3260), [`ui/dr-ui/src/library_ui.rs:3348`](../ui/dr-ui/src/library_ui.rs#L3348), [`ui/dr-ui/src/library_ui.rs:3536`](../ui/dr-ui/src/library_ui.rs#L3536), [`ui/dr-ui/src/library_ui.rs:3654`](../ui/dr-ui/src/library_ui.rs#L3654), [`ui/dr-ui/src/library_ui.rs:441`](../ui/dr-ui/src/library_ui.rs#L441), [`ui/dr-ui/src/library_ui.rs:499`](../ui/dr-ui/src/library_ui.rs#L499), [`ui/dr-ui/src/library_ui.rs:5302`](../ui/dr-ui/src/library_ui.rs#L5302), [`ui/dr-ui/src/library_ui.rs:5417`](../ui/dr-ui/src/library_ui.rs#L5417), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`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:613`](../core/dr-catalog/src/schema.rs#L613), [`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:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2870`](../ui/dr-ui/src/develop.rs#L2870), [`ui/dr-ui/src/library.rs:149`](../ui/dr-ui/src/library.rs#L149), [`ui/dr-ui/src/library.rs:1616`](../ui/dr-ui/src/library.rs#L1616), [`ui/dr-ui/src/library.rs:1693`](../ui/dr-ui/src/library.rs#L1693), [`ui/dr-ui/src/library.rs:234`](../ui/dr-ui/src/library.rs#L234), [`ui/dr-ui/src/library.rs:4062`](../ui/dr-ui/src/library.rs#L4062), [`ui/dr-ui/src/library.rs:544`](../ui/dr-ui/src/library.rs#L544), [`ui/dr-ui/src/library.rs:776`](../ui/dr-ui/src/library.rs#L776), [`ui/dr-ui/src/library.rs:792`](../ui/dr-ui/src/library.rs#L792), [`ui/dr-ui/src/library.rs:846`](../ui/dr-ui/src/library.rs#L846), [`ui/dr-ui/src/library_ui.rs:1565`](../ui/dr-ui/src/library_ui.rs#L1565), [`ui/dr-ui/src/library_ui.rs:1591`](../ui/dr-ui/src/library_ui.rs#L1591), [`ui/dr-ui/src/library_ui.rs:1607`](../ui/dr-ui/src/library_ui.rs#L1607), [`ui/dr-ui/src/library_ui.rs:1701`](../ui/dr-ui/src/library_ui.rs#L1701), [`ui/dr-ui/src/library_ui.rs:227`](../ui/dr-ui/src/library_ui.rs#L227), [`ui/dr-ui/src/library_ui.rs:2317`](../ui/dr-ui/src/library_ui.rs#L2317), [`ui/dr-ui/src/library_ui.rs:260`](../ui/dr-ui/src/library_ui.rs#L260), [`ui/dr-ui/src/library_ui.rs:2755`](../ui/dr-ui/src/library_ui.rs#L2755), [`ui/dr-ui/src/library_ui.rs:2979`](../ui/dr-ui/src/library_ui.rs#L2979), [`ui/dr-ui/src/library_ui.rs:3260`](../ui/dr-ui/src/library_ui.rs#L3260), [`ui/dr-ui/src/library_ui.rs:3348`](../ui/dr-ui/src/library_ui.rs#L3348), [`ui/dr-ui/src/library_ui.rs:3536`](../ui/dr-ui/src/library_ui.rs#L3536), [`ui/dr-ui/src/library_ui.rs:3654`](../ui/dr-ui/src/library_ui.rs#L3654), [`ui/dr-ui/src/library_ui.rs:441`](../ui/dr-ui/src/library_ui.rs#L441), [`ui/dr-ui/src/library_ui.rs:499`](../ui/dr-ui/src/library_ui.rs#L499), [`ui/dr-ui/src/library_ui.rs:5302`](../ui/dr-ui/src/library_ui.rs#L5302), [`ui/dr-ui/src/library_ui.rs:5417`](../ui/dr-ui/src/library_ui.rs#L5417), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||||
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
| 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:350`](../core/dr-catalog/src/schema.rs#L350), [`core/dr-catalog/src/schema.rs:435`](../core/dr-catalog/src/schema.rs#L435), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:589`](../ui/dr-ui/src/develop.rs#L589), [`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:1896`](../ui/dr-ui/src/lib.rs#L1896), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:357`](../core/dr-catalog/src/schema.rs#L357), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:589`](../ui/dr-ui/src/develop.rs#L589), [`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:1896`](../ui/dr-ui/src/lib.rs#L1896), [`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:435`](../core/dr-catalog/src/schema.rs#L435), [`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:255`](../ui/dr-ui/src/library.rs#L255), [`ui/dr-ui/src/library.rs:285`](../ui/dr-ui/src/library.rs#L285), [`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:442`](../core/dr-catalog/src/schema.rs#L442), [`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:255`](../ui/dr-ui/src/library.rs#L255), [`ui/dr-ui/src/library.rs:285`](../ui/dr-ui/src/library.rs#L285), [`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:350`](../core/dr-catalog/src/schema.rs#L350), [`core/dr-catalog/src/schema.rs:435`](../core/dr-catalog/src/schema.rs#L435), [`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-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:357`](../core/dr-catalog/src/schema.rs#L357), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`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-2 | [`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), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) |
|
| FR-CULL-2 | [`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), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) |
|
||||||
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/library.rs:214`](../ui/dr-ui/src/library.rs#L214), [`ui/dr-ui/src/library.rs:460`](../ui/dr-ui/src/library.rs#L460) |
|
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/library.rs:214`](../ui/dr-ui/src/library.rs#L214), [`ui/dr-ui/src/library.rs:460`](../ui/dr-ui/src/library.rs#L460) |
|
||||||
| 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:394`](../core/dr-catalog/src/schema.rs#L394), [`core/dr-catalog/src/schema.rs:435`](../core/dr-catalog/src/schema.rs#L435), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:2725`](../ui/dr-ui/src/library.rs#L2725), [`ui/dr-ui/src/library.rs:2829`](../ui/dr-ui/src/library.rs#L2829), [`ui/dr-ui/ui/settings.slint:404`](../ui/dr-ui/ui/settings.slint#L404), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) |
|
| 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:401`](../core/dr-catalog/src/schema.rs#L401), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:2725`](../ui/dr-ui/src/library.rs#L2725), [`ui/dr-ui/src/library.rs:2829`](../ui/dr-ui/src/library.rs#L2829), [`ui/dr-ui/ui/settings.slint:404`](../ui/dr-ui/ui/settings.slint#L404), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) |
|
||||||
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:435`](../core/dr-catalog/src/schema.rs#L435), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
|
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
|
||||||
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389) |
|
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389) |
|
||||||
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:620`](../core/dr-pipeline/src/framing.rs#L620), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1297`](../ui/dr-ui/src/develop.rs#L1297), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1775`](../ui/dr-ui/src/develop.rs#L1775), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:1807`](../ui/dr-ui/src/develop.rs#L1807), [`ui/dr-ui/src/develop.rs:1829`](../ui/dr-ui/src/develop.rs#L1829), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:4007`](../ui/dr-ui/src/develop.rs#L4007), [`ui/dr-ui/src/develop.rs:4057`](../ui/dr-ui/src/develop.rs#L4057), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1472`](../ui/dr-ui/src/lib.rs#L1472), [`ui/dr-ui/src/lib.rs:2174`](../ui/dr-ui/src/lib.rs#L2174), [`ui/dr-ui/src/lib.rs:319`](../ui/dr-ui/src/lib.rs#L319), [`ui/dr-ui/src/library.rs:507`](../ui/dr-ui/src/library.rs#L507), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:1761`](../ui/dr-ui/ui/app.slint#L1761), [`ui/dr-ui/ui/app.slint:790`](../ui/dr-ui/ui/app.slint#L790), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) |
|
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:620`](../core/dr-pipeline/src/framing.rs#L620), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1297`](../ui/dr-ui/src/develop.rs#L1297), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1775`](../ui/dr-ui/src/develop.rs#L1775), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:1807`](../ui/dr-ui/src/develop.rs#L1807), [`ui/dr-ui/src/develop.rs:1829`](../ui/dr-ui/src/develop.rs#L1829), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:4007`](../ui/dr-ui/src/develop.rs#L4007), [`ui/dr-ui/src/develop.rs:4057`](../ui/dr-ui/src/develop.rs#L4057), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1472`](../ui/dr-ui/src/lib.rs#L1472), [`ui/dr-ui/src/lib.rs:2174`](../ui/dr-ui/src/lib.rs#L2174), [`ui/dr-ui/src/lib.rs:319`](../ui/dr-ui/src/lib.rs#L319), [`ui/dr-ui/src/library.rs:507`](../ui/dr-ui/src/library.rs#L507), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:1761`](../ui/dr-ui/ui/app.slint#L1761), [`ui/dr-ui/ui/app.slint:790`](../ui/dr-ui/ui/app.slint#L790), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) |
|
||||||
| FR-DEV-3a | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop.rs:1194`](../ui/dr-ui/src/develop.rs#L1194), [`ui/dr-ui/src/lib.rs:613`](../ui/dr-ui/src/lib.rs#L613), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) |
|
| FR-DEV-3a | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop.rs:1194`](../ui/dr-ui/src/develop.rs#L1194), [`ui/dr-ui/src/lib.rs:613`](../ui/dr-ui/src/lib.rs#L613), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) |
|
||||||
@@ -93,14 +93,14 @@ _None._
|
|||||||
| FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`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:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49) |
|
| FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`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:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49) |
|
||||||
| 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:489`](../ui/dr-ui/src/import.rs#L489) |
|
| 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:489`](../ui/dr-ui/src/import.rs#L489) |
|
||||||
| FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) |
|
| 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/schema.rs:1007`](../core/dr-catalog/src/schema.rs#L1007), [`core/dr-catalog/src/schema.rs:606`](../core/dr-catalog/src/schema.rs#L606), [`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:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1819`](../ui/dr-ui/src/lib.rs#L1819), [`ui/dr-ui/src/lib.rs:2710`](../ui/dr-ui/src/lib.rs#L2710), [`ui/dr-ui/src/library.rs:1435`](../ui/dr-ui/src/library.rs#L1435), [`ui/dr-ui/src/library.rs:1458`](../ui/dr-ui/src/library.rs#L1458), [`ui/dr-ui/src/library.rs:1616`](../ui/dr-ui/src/library.rs#L1616), [`ui/dr-ui/src/library_ui.rs:1066`](../ui/dr-ui/src/library_ui.rs#L1066), [`ui/dr-ui/src/library_ui.rs:1139`](../ui/dr-ui/src/library_ui.rs#L1139), [`ui/dr-ui/src/library_ui.rs:1240`](../ui/dr-ui/src/library_ui.rs#L1240), [`ui/dr-ui/src/library_ui.rs:1295`](../ui/dr-ui/src/library_ui.rs#L1295), [`ui/dr-ui/src/library_ui.rs:1413`](../ui/dr-ui/src/library_ui.rs#L1413), [`ui/dr-ui/src/library_ui.rs:1532`](../ui/dr-ui/src/library_ui.rs#L1532), [`ui/dr-ui/src/library_ui.rs:2059`](../ui/dr-ui/src/library_ui.rs#L2059), [`ui/dr-ui/src/library_ui.rs:268`](../ui/dr-ui/src/library_ui.rs#L268), [`ui/dr-ui/src/library_ui.rs:279`](../ui/dr-ui/src/library_ui.rs#L279), [`ui/dr-ui/src/library_ui.rs:287`](../ui/dr-ui/src/library_ui.rs#L287), [`ui/dr-ui/src/library_ui.rs:299`](../ui/dr-ui/src/library_ui.rs#L299), [`ui/dr-ui/src/library_ui.rs:308`](../ui/dr-ui/src/library_ui.rs#L308), [`ui/dr-ui/src/library_ui.rs:400`](../ui/dr-ui/src/library_ui.rs#L400), [`ui/dr-ui/src/library_ui.rs:410`](../ui/dr-ui/src/library_ui.rs#L410), [`ui/dr-ui/src/library_ui.rs:452`](../ui/dr-ui/src/library_ui.rs#L452), [`ui/dr-ui/src/library_ui.rs:515`](../ui/dr-ui/src/library_ui.rs#L515), [`ui/dr-ui/src/library_ui.rs:5319`](../ui/dr-ui/src/library_ui.rs#L5319), [`ui/dr-ui/src/library_ui.rs:5337`](../ui/dr-ui/src/library_ui.rs#L5337), [`ui/dr-ui/src/library_ui.rs:5349`](../ui/dr-ui/src/library_ui.rs#L5349), [`ui/dr-ui/src/library_ui.rs:546`](../ui/dr-ui/src/library_ui.rs#L546), [`ui/dr-ui/src/library_ui.rs:558`](../ui/dr-ui/src/library_ui.rs#L558), [`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:2330`](../ui/dr-ui/ui/app.slint#L2330), [`ui/dr-ui/ui/app.slint:431`](../ui/dr-ui/ui/app.slint#L431), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:980`](../ui/dr-ui/ui/library.slint#L980) |
|
| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:1014`](../core/dr-catalog/src/schema.rs#L1014), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`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:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1819`](../ui/dr-ui/src/lib.rs#L1819), [`ui/dr-ui/src/lib.rs:2710`](../ui/dr-ui/src/lib.rs#L2710), [`ui/dr-ui/src/library.rs:1435`](../ui/dr-ui/src/library.rs#L1435), [`ui/dr-ui/src/library.rs:1458`](../ui/dr-ui/src/library.rs#L1458), [`ui/dr-ui/src/library.rs:1616`](../ui/dr-ui/src/library.rs#L1616), [`ui/dr-ui/src/library_ui.rs:1066`](../ui/dr-ui/src/library_ui.rs#L1066), [`ui/dr-ui/src/library_ui.rs:1139`](../ui/dr-ui/src/library_ui.rs#L1139), [`ui/dr-ui/src/library_ui.rs:1240`](../ui/dr-ui/src/library_ui.rs#L1240), [`ui/dr-ui/src/library_ui.rs:1295`](../ui/dr-ui/src/library_ui.rs#L1295), [`ui/dr-ui/src/library_ui.rs:1413`](../ui/dr-ui/src/library_ui.rs#L1413), [`ui/dr-ui/src/library_ui.rs:1532`](../ui/dr-ui/src/library_ui.rs#L1532), [`ui/dr-ui/src/library_ui.rs:2059`](../ui/dr-ui/src/library_ui.rs#L2059), [`ui/dr-ui/src/library_ui.rs:268`](../ui/dr-ui/src/library_ui.rs#L268), [`ui/dr-ui/src/library_ui.rs:279`](../ui/dr-ui/src/library_ui.rs#L279), [`ui/dr-ui/src/library_ui.rs:287`](../ui/dr-ui/src/library_ui.rs#L287), [`ui/dr-ui/src/library_ui.rs:299`](../ui/dr-ui/src/library_ui.rs#L299), [`ui/dr-ui/src/library_ui.rs:308`](../ui/dr-ui/src/library_ui.rs#L308), [`ui/dr-ui/src/library_ui.rs:400`](../ui/dr-ui/src/library_ui.rs#L400), [`ui/dr-ui/src/library_ui.rs:410`](../ui/dr-ui/src/library_ui.rs#L410), [`ui/dr-ui/src/library_ui.rs:452`](../ui/dr-ui/src/library_ui.rs#L452), [`ui/dr-ui/src/library_ui.rs:515`](../ui/dr-ui/src/library_ui.rs#L515), [`ui/dr-ui/src/library_ui.rs:5319`](../ui/dr-ui/src/library_ui.rs#L5319), [`ui/dr-ui/src/library_ui.rs:5337`](../ui/dr-ui/src/library_ui.rs#L5337), [`ui/dr-ui/src/library_ui.rs:5349`](../ui/dr-ui/src/library_ui.rs#L5349), [`ui/dr-ui/src/library_ui.rs:546`](../ui/dr-ui/src/library_ui.rs#L546), [`ui/dr-ui/src/library_ui.rs:558`](../ui/dr-ui/src/library_ui.rs#L558), [`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:2330`](../ui/dr-ui/ui/app.slint#L2330), [`ui/dr-ui/ui/app.slint:431`](../ui/dr-ui/ui/app.slint#L431), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:980`](../ui/dr-ui/ui/library.slint#L980) |
|
||||||
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1295`](../ui/dr-ui/src/library_ui.rs#L1295) |
|
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1295`](../ui/dr-ui/src/library_ui.rs#L1295) |
|
||||||
| FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/library_ui.rs:1066`](../ui/dr-ui/src/library_ui.rs#L1066), [`ui/dr-ui/src/library_ui.rs:1139`](../ui/dr-ui/src/library_ui.rs#L1139), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) |
|
| FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/library_ui.rs:1066`](../ui/dr-ui/src/library_ui.rs#L1066), [`ui/dr-ui/src/library_ui.rs:1139`](../ui/dr-ui/src/library_ui.rs#L1139), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) |
|
||||||
| 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:95`](../core/dr-sync-nextcloud/src/lib.rs#L95), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3195`](../ui/dr-ui/src/library.rs#L3195), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372) |
|
| 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:95`](../core/dr-sync-nextcloud/src/lib.rs#L95), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3195`](../ui/dr-ui/src/library.rs#L3195), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372) |
|
||||||
| 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:116`](../core/dr-types/src/settings.rs#L116), [`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:1126`](../ui/dr-ui/src/lib.rs#L1126), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
| 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:116`](../core/dr-types/src/settings.rs#L116), [`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:1126`](../ui/dr-ui/src/lib.rs#L1126), [`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:123`](../ui/dr-ui/src/import.rs#L123), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import.rs:585`](../ui/dr-ui/src/import.rs#L585), [`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:1126`](../ui/dr-ui/src/lib.rs#L1126) |
|
| 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:123`](../ui/dr-ui/src/import.rs#L123), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import.rs:585`](../ui/dr-ui/src/import.rs#L585), [`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:1126`](../ui/dr-ui/src/lib.rs#L1126) |
|
||||||
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1788`](../ui/dr-ui/src/lib.rs#L1788), [`ui/dr-ui/src/library.rs:460`](../ui/dr-ui/src/library.rs#L460), [`ui/dr-ui/src/library_ui.rs:467`](../ui/dr-ui/src/library_ui.rs#L467) |
|
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1788`](../ui/dr-ui/src/lib.rs#L1788), [`ui/dr-ui/src/library.rs:460`](../ui/dr-ui/src/library.rs#L460), [`ui/dr-ui/src/library_ui.rs:467`](../ui/dr-ui/src/library_ui.rs#L467) |
|
||||||
| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:549`](../core/dr-catalog/src/schema.rs#L549), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:846`](../ui/dr-ui/src/library.rs#L846), [`ui/dr-ui/src/library.rs:976`](../ui/dr-ui/src/library.rs#L976) |
|
| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:846`](../ui/dr-ui/src/library.rs#L846), [`ui/dr-ui/src/library.rs:976`](../ui/dr-ui/src/library.rs#L976) |
|
||||||
| FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
|
| FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
|
||||||
| FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
|
| FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
|
||||||
| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:863`](../ui/dr-ui/src/lib.rs#L863), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:863`](../ui/dr-ui/src/lib.rs#L863), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
||||||
@@ -123,7 +123,7 @@ _None._
|
|||||||
| NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) |
|
| NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) |
|
||||||
| NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
| NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
||||||
| NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
| NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
||||||
| NFR-P5 | [`core/dr-catalog/src/schema.rs:311`](../core/dr-catalog/src/schema.rs#L311), [`ui/dr-ui/src/library.rs:3795`](../ui/dr-ui/src/library.rs#L3795), [`ui/dr-ui/src/library.rs:4568`](../ui/dr-ui/src/library.rs#L4568), [`ui/dr-ui/src/library_ui.rs:4073`](../ui/dr-ui/src/library_ui.rs#L4073), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) |
|
| NFR-P5 | [`core/dr-catalog/src/schema.rs:318`](../core/dr-catalog/src/schema.rs#L318), [`ui/dr-ui/src/library.rs:3795`](../ui/dr-ui/src/library.rs#L3795), [`ui/dr-ui/src/library.rs:4568`](../ui/dr-ui/src/library.rs#L4568), [`ui/dr-ui/src/library_ui.rs:4073`](../ui/dr-ui/src/library_ui.rs#L4073), [`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:944`](../ui/dr-ui/src/export.rs#L944), [`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-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:944`](../ui/dr-ui/src/export.rs#L944), [`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:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`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-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`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-3 | [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1) |
|
| NFR-PORT-3 | [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1) |
|
||||||
@@ -133,10 +133,10 @@ _None._
|
|||||||
| NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
| 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-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
||||||
| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/lib.rs:72`](../ui/dr-ui/src/lib.rs#L72) |
|
| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/lib.rs:72`](../ui/dr-ui/src/lib.rs#L72) |
|
||||||
| 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:606`](../core/dr-catalog/src/schema.rs#L606), [`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), [`ui/dr-ui/src/library.rs:2724`](../ui/dr-ui/src/library.rs#L2724) |
|
| 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:613`](../core/dr-catalog/src/schema.rs#L613), [`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), [`ui/dr-ui/src/library.rs:2724`](../ui/dr-ui/src/library.rs#L2724) |
|
||||||
| NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) |
|
| NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) |
|
||||||
| NFR-SEC-2 | [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) |
|
| NFR-SEC-2 | [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) |
|
||||||
| NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:435`](../core/dr-catalog/src/schema.rs#L435), [`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/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`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/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
| R1 | [`tools/traceability/src/lib.rs:495`](../tools/traceability/src/lib.rs#L495), [`tools/traceability/src/lib.rs:499`](../tools/traceability/src/lib.rs#L499) |
|
| R1 | [`tools/traceability/src/lib.rs:495`](../tools/traceability/src/lib.rs#L495), [`tools/traceability/src/lib.rs:499`](../tools/traceability/src/lib.rs#L499) |
|
||||||
| R4 | [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
|
| R4 | [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user