assay_core/
fingerprint.rs1use 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
15pub 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
30pub fn compute(ctx: Context<'_>) -> Fingerprint {
35 let mut parts = Vec::new();
36
37 parts.push(format!("suite={}", ctx.suite));
39 parts.push(format!("model={}", ctx.model));
40 parts.push(format!("test_id={}", ctx.test_id));
41
42 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 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 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 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}