frankensearch-core 0.2.3

Core traits, types, and error types for frankensearch
use std::fs;
use std::path::PathBuf;

use frankensearch_core::{
    ExplainedSource, ExplanationPhase, HitExplanation, RankMovement, ScoreComponent,
};

fn assert_golden_json<T: serde::Serialize>(name: &str, value: &T) {
    let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/golden");
    let golden_path = dir.join(format!("{name}.golden.json"));
    let actual = serde_json::to_string_pretty(value).expect("serialize golden json");

    if std::env::var("UPDATE_GOLDENS").is_ok() {
        fs::create_dir_all(&dir).expect("create golden directory");
        fs::write(&golden_path, actual.as_bytes()).expect("write golden file");
        return;
    }

    let expected = fs::read_to_string(&golden_path).unwrap_or_else(|_| {
        panic!(
            "Golden file not found: {}\nSet UPDATE_GOLDENS=1 to create it.",
            golden_path.display()
        );
    });

    let actual_trimmed = actual.trim_end_matches(['\n', '\r']);
    let expected_trimmed = expected.trim_end_matches(['\n', '\r']);

    if actual_trimmed != expected_trimmed {
        let actual_path = golden_path.with_extension("actual.json");
        fs::write(&actual_path, actual_trimmed.as_bytes()).expect("write actual file");
        panic!(
            "GOLDEN MISMATCH: {name}\nexpected: {}\nactual: {}",
            golden_path.display(),
            actual_path.display()
        );
    }
}

#[test]
fn test_hit_explanation_conformance() {
    let explanation = HitExplanation {
        final_score: 0.812,
        phase: ExplanationPhase::Refined,
        rank_movement: Some(RankMovement {
            initial_rank: 5,
            refined_rank: 2,
            delta: -3,
            reason: "quality model promoted result".to_owned(),
        }),
        components: vec![ScoreComponent {
            source: ExplainedSource::SemanticQuality {
                embedder: "all-MiniLM-L6-v2".to_owned(),
                cosine_sim: 0.92,
            },
            raw_score: 0.92,
            normalized_score: 0.87,
            rrf_contribution: 0.014,
            weight: 0.7,
        }],
    };

    assert_golden_json("hit_explanation_v1", &explanation);
}