diff --git a/core/crates/solstone-core-entity/src/fixture_tests.rs b/core/crates/solstone-core-entity/src/fixture_tests.rs index 26cbe3aa1..4e83c56fc 100644 --- a/core/crates/solstone-core-entity/src/fixture_tests.rs +++ b/core/crates/solstone-core-entity/src/fixture_tests.rs @@ -46,6 +46,13 @@ fn entity_slug_vectors_match_fixture() { fixture.entity_slug.max_length, crate::MAX_ENTITY_SLUG_LENGTH ); + // A loader that parses the file, misses the array and yields nothing would + // satisfy every assertion below perfectly. The declared count is what makes + // these tests rather than formalities. + assert_eq!( + fixture.entity_slug.vectors.len(), + fixture.entity_slug.vector_count + ); for vector in fixture.entity_slug.vectors { let native = entity_slug(&vector.name); if let Some(codepoint) = scalar_probe_codepoint(&vector.name) @@ -62,6 +69,13 @@ fn entity_slug_vectors_match_fixture() { #[test] fn normalize_resolution_query_vectors_match_fixture() { let fixture = entity_identity_fixture(); + // A loader that parses the file, misses the array and yields nothing would + // satisfy every assertion below perfectly. The declared count is what makes + // these tests rather than formalities. + assert_eq!( + fixture.normalize_resolution_query.vectors.len(), + fixture.normalize_resolution_query.vector_count + ); for vector in fixture.normalize_resolution_query.vectors { assert_eq!( normalize_resolution_query(&vector.query), @@ -75,6 +89,13 @@ fn normalize_resolution_query_vectors_match_fixture() { #[test] fn ambiguity_id_vectors_match_fixture() { let fixture = entity_identity_fixture(); + // A loader that parses the file, misses the array and yields nothing would + // satisfy every assertion below perfectly. The declared count is what makes + // these tests rather than formalities. + assert_eq!( + fixture.ambiguity_id.vectors.len(), + fixture.ambiguity_id.vector_count + ); for vector in fixture.ambiguity_id.vectors { let normalized_query = normalize_resolution_query(&vector.query); assert_eq!( @@ -104,6 +125,24 @@ fn ambiguity_id_vectors_match_fixture() { #[test] fn matching_vectors_match_fixture() { let fixture = entity_matching_fixture(); + // A loader that parses the file, misses the array and yields nothing would + // satisfy every assertion below perfectly. The declared count is what makes + // these tests rather than formalities. + assert_eq!(fixture.vectors.len(), fixture.vector_count); + // The refusal half is where the store declines to guess; without its own + // count a corpus that lost every refusal vector would still read as full. + assert_eq!( + fixture.vectors.iter().filter(|v| v.outcome.matched).count(), + fixture.matched_count + ); + assert_eq!( + fixture + .vectors + .iter() + .filter(|v| !v.outcome.matched) + .count(), + fixture.refusal_count + ); for vector in fixture.vectors { let candidates: Vec = vector .candidates diff --git a/core/crates/solstone-core-entity/src/test_support.rs b/core/crates/solstone-core-entity/src/test_support.rs index 2ff38bde1..160d8f64d 100644 --- a/core/crates/solstone-core-entity/src/test_support.rs +++ b/core/crates/solstone-core-entity/src/test_support.rs @@ -33,12 +33,14 @@ pub(crate) struct EntityIdentityFixture { #[derive(Debug, Deserialize)] pub(crate) struct SlugIdentityFixture { pub(crate) max_length: usize, + pub(crate) vector_count: usize, pub(crate) sweep: SweepFixture, pub(crate) vectors: Vec, } #[derive(Debug, Deserialize)] pub(crate) struct NormalizationIdentityFixture { + pub(crate) vector_count: usize, pub(crate) sweep: SweepFixture, pub(crate) vectors: Vec, } @@ -63,6 +65,7 @@ pub(crate) struct NormalizationVector { #[derive(Debug, Deserialize)] pub(crate) struct AmbiguityIdentityFixture { + pub(crate) vector_count: usize, pub(crate) vectors: Vec, } @@ -84,6 +87,9 @@ pub(crate) struct ScopeFixture { #[derive(Debug, Deserialize)] pub(crate) struct EntityMatchingFixture { pub(crate) fuzzy_threshold: f64, + pub(crate) vector_count: usize, + pub(crate) matched_count: usize, + pub(crate) refusal_count: usize, pub(crate) high_confidence_max_tier: u8, pub(crate) vectors: Vec, } diff --git a/core/fixtures/entity_identity.json b/core/fixtures/entity_identity.json index b96fd538c..93f1d9dda 100644 --- a/core/fixtures/entity_identity.json +++ b/core/fixtures/entity_identity.json @@ -1,6 +1,7 @@ { "ambiguity_id": { "description": "amb_ + sha256(scope_key|normalized_query)[:24]; the normalized query folds case, so the fold is inside an identity", + "vector_count": 10, "vectors": [ { "ambiguity_id": "amb_499fa5f27708c6120de29157", @@ -106,6 +107,7 @@ "scalar_values": 1112064, "sha256": "ccf541e524e22fa29b68da73d42a556f255381150d77cc30c583142976da5ce2" }, + "vector_count": 63, "vectors": [ { "name": "Alice Johnson", @@ -387,6 +389,7 @@ "scalar_values": 1112064, "sha256": "e79746779dcc65fc4ff9b128b1065d27b205eebe7a6235353f7859e46ae8d08f" }, + "vector_count": 63, "vectors": [ { "normalized": "alice johnson", diff --git a/core/fixtures/entity_matching.json b/core/fixtures/entity_matching.json index a76a20b3f..c68ccea25 100644 --- a/core/fixtures/entity_matching.json +++ b/core/fixtures/entity_matching.json @@ -2,7 +2,9 @@ "fuzzy_threshold": 90, "generated_by": "make core-fixtures", "high_confidence_max_tier": 4, + "matched_count": 43, "note": "Tiers 1-4 are high confidence and resolve silently. Tiers 5-8 are low confidence, and the caller surfaces them to the owner rather than guessing. A reimplementation that moves a case across the tier-4 boundary changes whether an owner is asked at all.", + "refusal_count": 22, "tiers": { "1": "exact name, id or aka", "2": "case-insensitive name, id or aka", @@ -13,6 +15,7 @@ "7": "prefix token, unambiguous only, 4-char minimum prefix", "8": "fuzzy, rapidfuzz token_sort_ratio at or above the threshold" }, + "vector_count": 65, "vectors": [ { "candidates": [ diff --git a/scripts/entity_corpus.py b/scripts/entity_corpus.py index 27c737249..6aab07b34 100644 --- a/scripts/entity_corpus.py +++ b/scripts/entity_corpus.py @@ -241,6 +241,10 @@ def build_entity_identity_fixture() -> dict[str, Any]: "versions": _versions(), "entity_slug": { "max_length": MAX_ENTITY_SLUG_LENGTH, + # Declared so a consumer can assert it before iterating. A loader + # that parses the file, misses the array and yields nothing would + # otherwise satisfy "every vector reproduces" perfectly. + "vector_count": len(slug_vectors), "sweep": { "description": ( "entity_slug('A' + chr(cp) + 'B') for every Unicode scalar " @@ -252,6 +256,7 @@ def build_entity_identity_fixture() -> dict[str, Any]: "vectors": slug_vectors, }, "normalize_resolution_query": { + "vector_count": len(normalize_vectors), "sweep": { "description": ( "normalize_resolution_query('A' + chr(cp) + 'B') for every " @@ -263,6 +268,7 @@ def build_entity_identity_fixture() -> dict[str, Any]: "vectors": normalize_vectors, }, "ambiguity_id": { + "vector_count": len(ambiguity_vectors), "description": ( "amb_ + sha256(scope_key|normalized_query)[:24]; the normalized " "query folds case, so the fold is inside an identity" @@ -462,6 +468,9 @@ def build_entity_matching_fixture() -> dict[str, Any]: "7": "prefix token, unambiguous only, 4-char minimum prefix", "8": "fuzzy, rapidfuzz token_sort_ratio at or above the threshold", }, + "vector_count": len(vectors), + "matched_count": sum(1 for v in vectors if v["outcome"]["matched"]), + "refusal_count": sum(1 for v in vectors if not v["outcome"]["matched"]), "high_confidence_max_tier": 4, "note": ( "Tiers 1-4 are high confidence and resolve silently. Tiers 5-8 are "