diff --git a/core/dr-catalog/examples/sync_probe.rs b/core/dr-catalog/examples/sync_probe.rs new file mode 100644 index 0000000..b79026c --- /dev/null +++ b/core/dr-catalog/examples/sync_probe.rs @@ -0,0 +1,111 @@ +//! What a second device ends up with after adopting this library. +//! +//! Stands up an empty catalog, gives it the images the real one has, adopts the +//! face shards into it exactly as a sync would, merges the real catalog in as a +//! remote — and then counts. The point is to answer "why does the tablet show +//! fewer faces for this person" without needing the tablet. +//! +//! cargo run -p dr-catalog --example sync_probe -- CATALOG.sqlite FACES_DIR + +use std::path::PathBuf; + +use dr_catalog::face_shard::{self, FaceShardStore}; +use dr_catalog::Catalog; + +const MODEL: &str = "w600k_mbf"; + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + if args.len() < 2 { + eprintln!("usage: sync_probe CATALOG.sqlite FACES_DIR"); + std::process::exit(2); + } + let source = PathBuf::from(&args[0]); + let faces_dir = PathBuf::from(&args[1]); + + let dir = std::env::temp_dir().join(format!("dr-sync-probe-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let dest = dir.join("catalog.sqlite"); + + let far = Catalog::open(&dest).expect("fresh catalog"); + let conn = far.connection(); + + // The images a scan would have found. Nothing else: no faces, no people. + conn.execute( + "ATTACH DATABASE ?1 AS src", + [source.to_string_lossy().as_ref()], + ) + .unwrap(); + // Foreign keys off for the copy: `images` carries self-references + // (`shadowed_by`) that are only consistent once every row is in, and this + // is a bulk clone rather than an edit. + conn.execute_batch( + "PRAGMA foreign_keys = OFF; + INSERT INTO roots SELECT * FROM src.roots; + INSERT INTO images SELECT * FROM src.images; + INSERT INTO remote SELECT * FROM src.remote; + PRAGMA foreign_keys = ON;", + ) + .unwrap(); + let images: i64 = conn + .query_row("SELECT COUNT(*) FROM images", [], |r| r.get(0)) + .unwrap(); + conn.execute_batch("DETACH DATABASE src").unwrap(); + println!("second device starts with {images} image(s), no faces"); + + // Adopt every shard, which is what a completed face sync leaves behind. + let store = FaceShardStore::open(&faces_dir).expect("shard store"); + let adopted = face_shard::import_from_shards(conn, &store, MODEL).expect("import"); + let faces: i64 = conn + .query_row("SELECT COUNT(*) FROM faces", [], |r| r.get(0)) + .unwrap(); + println!("adopted {adopted} image(s) from the shards -> {faces} face(s)"); + + // Then the catalog merge, which is where people and their judgements come. + let report = dr_catalog::sync::merge_remote(conn, &source).expect("merge"); + println!( + "merge: {} people in, {} updated, {} kept local, {} face(s) assigned, \ + {} kept local, {} rejection(s)", + report.people_inserted, + report.people_updated, + report.people_kept_local, + report.faces_assigned, + report.faces_kept_local, + report.faces_rejected, + ); + + // Per person, against what the source holds. + conn.execute( + "ATTACH DATABASE ?1 AS src", + [source.to_string_lossy().as_ref()], + ) + .unwrap(); + let mut q = conn + .prepare( + "SELECT p.name, + (SELECT COUNT(*) FROM src.face_person sfp + JOIN src.people sp ON sp.id = sfp.person_id + WHERE sp.uuid = p.uuid) AS there, + (SELECT COUNT(*) FROM face_person fp WHERE fp.person_id = p.id) AS here + FROM people p + WHERE p.name != '' + ORDER BY there DESC LIMIT 12", + ) + .unwrap(); + println!("\n{:<24} {:>8} {:>8}", "person", "source", "here"); + let rows = q + .query_map([], |r| { + Ok(( + r.get::<_, String>(0)?, + r.get::<_, i64>(1)?, + r.get::<_, i64>(2)?, + )) + }) + .unwrap(); + for row in rows.flatten() { + println!("{:<24} {:>8} {:>8}", row.0, row.1, row.2); + } + + println!("\nprobe catalog left at {}", dest.display()); +} diff --git a/docs/traceability.md b/docs/traceability.md index e1c2e53..ca44ba4 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,7 +9,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 277 | +| Source files scanned | 278 | | TRACES tags found | 811 | | Requirements defined | 177 | | Requirements covered | 106 |