Skip to main content

jar_genesis/
snapshot.rs

1//! Snapshot lookup: find ranking/variances for a given epoch from the cache.
2//!
3//! Scores (v3 BT output) are the single source of truth for v3. From scores
4//! we derive both the ranking (sorted by mu descending) and variances
5//! (commitId × sigma2 pairs). For v2, we fall back to ranking.json.
6
7/// A snapshot contains the ranking and optionally variances (for v3).
8#[derive(Debug)]
9pub struct Snapshot {
10    pub ranking: serde_json::Value,
11    pub variances: Option<serde_json::Value>,
12}
13
14/// Find the last index with epoch < target_epoch and return its commit hash.
15fn find_prior_commit_hash(indices: &[serde_json::Value], epoch: u64) -> Option<String> {
16    let last = indices
17        .iter()
18        .rfind(|idx| idx["epoch"].as_u64().map(|e| e < epoch).unwrap_or(false))?;
19    last["commitHash"].as_str().map(|s| s.to_string())
20}
21
22/// Derive ranking and variances from a scores array (v3 BT output).
23/// Scores format: [{"commit": "hash", "mu": N, "sigma2": N}, ...]
24/// Returns (ranking sorted by mu desc, variances as [["hash", sigma2], ...]).
25fn derive_from_scores(scores: &serde_json::Value) -> Option<Snapshot> {
26    let arr = scores.as_array()?;
27    // Sort by mu descending to derive ranking
28    let mut entries: Vec<(&serde_json::Value, i64)> = arr
29        .iter()
30        .filter_map(|s| {
31            let mu = s["mu"].as_i64()?;
32            Some((s, mu))
33        })
34        .collect();
35    entries.sort_by_key(|entry| std::cmp::Reverse(entry.1));
36
37    let ranking: Vec<serde_json::Value> = entries
38        .iter()
39        .filter_map(|(s, _)| s.get("commit").cloned())
40        .collect();
41
42    // Extract variances as [["hash", sigma2], ...]
43    let variances: Vec<serde_json::Value> = arr
44        .iter()
45        .filter_map(|s| {
46            let commit = s.get("commit")?;
47            let sigma2 = s.get("sigma2")?;
48            Some(serde_json::json!([commit, sigma2]))
49        })
50        .collect();
51
52    Some(Snapshot {
53        ranking: serde_json::json!(ranking),
54        variances: Some(serde_json::json!(variances)),
55    })
56}
57
58/// Find the snapshot for a given epoch, checking scores.json first (v3),
59/// then falling back to ranking.json (v2).
60///
61/// Returns:
62/// - `Ok(None)` if no prior index exists (first commit, no ranking needed)
63/// - `Ok(Some(snapshot))` if found in either source
64/// - `Err(...)` if a prior index exists but its commit hash is missing from
65///   both sources (stale cache)
66pub fn find(
67    indices: &[serde_json::Value],
68    ranking_map: &serde_json::Value,
69    scores_map: &serde_json::Value,
70    epoch: u64,
71) -> Result<Option<Snapshot>, Box<dyn std::error::Error>> {
72    let commit_hash = match find_prior_commit_hash(indices, epoch) {
73        Some(h) => h,
74        None => return Ok(None),
75    };
76
77    // Try scores.json first (v3)
78    if let Some(scores) = scores_map.get(&commit_hash)
79        && let Some(snapshot) = derive_from_scores(scores)
80    {
81        return Ok(Some(snapshot));
82    }
83
84    // Fall back to ranking.json (v2). Pass empty variances so v3's
85    // select-targets gets a valid array (defaults to BT_SCALE per commit).
86    if let Some(ranking) = ranking_map.get(&commit_hash) {
87        return Ok(Some(Snapshot {
88            ranking: ranking.clone(),
89            variances: Some(serde_json::json!([])),
90        }));
91    }
92
93    Err(format!(
94        "cache stale: commit {} not found in ranking.json or scores.json",
95        &commit_hash[..8.min(commit_hash.len())]
96    )
97    .into())
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_find_no_prior_index() {
106        let indices: Vec<serde_json::Value> = vec![];
107        let ranking = serde_json::json!({});
108        let scores = serde_json::json!({});
109        assert!(find(&indices, &ranking, &scores, 1000).unwrap().is_none());
110    }
111
112    #[test]
113    fn test_find_all_future() {
114        let indices = vec![serde_json::json!({"commitHash": "abc", "epoch": 2000})];
115        let ranking = serde_json::json!({"abc": ["abc"]});
116        let scores = serde_json::json!({});
117        assert!(find(&indices, &ranking, &scores, 1000).unwrap().is_none());
118    }
119
120    #[test]
121    fn test_find_ranking_fallback() {
122        let indices = vec![
123            serde_json::json!({"commitHash": "aaa", "epoch": 100}),
124            serde_json::json!({"commitHash": "bbb", "epoch": 200}),
125        ];
126        let ranking = serde_json::json!({
127            "aaa": ["aaa"],
128            "bbb": ["bbb", "aaa"],
129        });
130        let scores = serde_json::json!({});
131        let snap = find(&indices, &ranking, &scores, 250).unwrap().unwrap();
132        assert_eq!(snap.ranking, serde_json::json!(["bbb", "aaa"]));
133        assert_eq!(snap.variances, Some(serde_json::json!([])));
134    }
135
136    #[test]
137    fn test_find_scores_preferred() {
138        let indices = vec![serde_json::json!({"commitHash": "aaa", "epoch": 100})];
139        let ranking = serde_json::json!({"aaa": ["aaa"]});
140        let scores = serde_json::json!({
141            "aaa": [
142                {"commit": "aaa", "mu": 500, "sigma2": 23000000}
143            ]
144        });
145        let snap = find(&indices, &ranking, &scores, 200).unwrap().unwrap();
146        assert_eq!(snap.ranking, serde_json::json!(["aaa"]));
147        assert!(snap.variances.is_some());
148    }
149
150    #[test]
151    fn test_find_scores_sorted_by_mu() {
152        let indices = vec![serde_json::json!({"commitHash": "aaa", "epoch": 100})];
153        let ranking = serde_json::json!({});
154        let scores = serde_json::json!({
155            "aaa": [
156                {"commit": "bbb", "mu": -100, "sigma2": 20000000},
157                {"commit": "aaa", "mu": 500, "sigma2": 23000000}
158            ]
159        });
160        let snap = find(&indices, &ranking, &scores, 200).unwrap().unwrap();
161        // aaa has higher mu, should come first
162        assert_eq!(snap.ranking, serde_json::json!(["aaa", "bbb"]));
163    }
164
165    #[test]
166    fn test_find_stale_cache_errors() {
167        let indices = vec![serde_json::json!({"commitHash": "abc12345", "epoch": 100})];
168        let ranking = serde_json::json!({});
169        let scores = serde_json::json!({});
170        let err = find(&indices, &ranking, &scores, 200).unwrap_err();
171        assert!(err.to_string().contains("cache stale"));
172        assert!(err.to_string().contains("abc12345"));
173    }
174}