assay_core/
fingerprint.rs

1use sha2::{Digest, Sha256};
2
3#[derive(Debug, Clone)]
4pub struct Fingerprint {
5    pub hex: String,
6    pub components: Vec<String>,
7}
8
9pub fn sha256_hex(s: &str) -> String {
10    let mut h = Sha256::new();
11    h.update(s.as_bytes());
12    hex::encode(h.finalize())
13}
14
15/// Computes a deterministic fingerprint for a test case execution context.
16///
17/// Inputs are canonicalized (sorted map keys via serde_json where applicable)
18/// to ensure stable hashing.
19pub struct Context<'a> {
20    pub suite: &'a str,
21    pub model: &'a str,
22    pub test_id: &'a str,
23    pub prompt: &'a str,
24    pub context: Option<&'a [String]>,
25    pub expected_canonical: &'a str,
26    pub policy_hash: Option<&'a str>,
27    pub metric_versions: &'a [(&'a str, &'a str)],
28}
29
30/// Computes a deterministic fingerprint for a test case execution context.
31///
32/// Inputs are canonicalized (sorted map keys via serde_json where applicable)
33/// to ensure stable hashing.
34pub fn compute(ctx: Context<'_>) -> Fingerprint {
35    let mut parts = Vec::new();
36
37    // Core Identity
38    parts.push(format!("suite={}", ctx.suite));
39    parts.push(format!("model={}", ctx.model));
40    parts.push(format!("test_id={}", ctx.test_id));
41
42    // Input (Exact text match required)
43    parts.push(format!("prompt={}", ctx.prompt));
44    if let Some(c) = ctx.context {
45        parts.push(format!("context={}", c.join("\n")));
46    } else {
47        parts.push("context=".to_string());
48    }
49
50    // Expected (Outcome logic)
51    parts.push(format!("expected={}", ctx.expected_canonical));
52    if let Some(ph) = ctx.policy_hash {
53        parts.push(format!("policy_hash={}", ph));
54    }
55
56    // Metric Logic Versions (Code change invalidation)
57    let mut mv = ctx.metric_versions.to_vec();
58    mv.sort_by_key(|(name, _)| *name);
59    let mv_str = mv
60        .into_iter()
61        .map(|(n, v)| format!("{n}:{v}"))
62        .collect::<Vec<_>>()
63        .join(",");
64    parts.push(format!("metrics={}", mv_str));
65
66    // Assay Version (Invalidate all on update)
67    // Optional: We can include this or rely on metric_versions for granular invalidation.
68    // Putting it here ensures safety on logic changes in runner itself.
69    parts.push(format!("assay_version={}", env!("CARGO_PKG_VERSION")));
70
71    let raw = parts.join("\n");
72    let hex = sha256_hex(&raw);
73
74    Fingerprint {
75        hex,
76        components: parts,
77    }
78}