use sha2::{Digest, Sha256};
use ulid::Ulid;
pub fn normalize_text(text: &str) -> String {
let trimmed = text.trim();
let chars: Vec<char> = trimmed.chars().collect();
if chars.is_empty() {
return String::new();
}
let mut result = Vec::with_capacity(chars.len());
for c in chars {
if result.is_empty()
|| !result
.last()
.is_some_and(|last: &char| last.is_whitespace() && c.is_whitespace())
{
result.push(c);
}
}
let leading = result.iter().take_while(|c| c.is_whitespace()).count();
let trailing = result
.iter()
.rev()
.take_while(|c| c.is_whitespace())
.count();
let end = result.len() - trailing;
result[leading..end].iter().collect()
}
pub fn stable_id(prefix: &str, parts: &[&str], length: usize) -> String {
let input = parts.join("\n");
let hash = Sha256::digest(input.as_bytes());
let hex = hex::encode(hash);
let truncated = &hex[..length.min(hex.len())];
format!("{}_{}", prefix, truncated)
}
pub fn ulid() -> String {
Ulid::new().to_string()
}
pub fn sha256_text(text: &str) -> String {
let hash = Sha256::digest(text.as_bytes());
hex::encode(hash)
}
pub fn sha256_bytes(data: &[u8]) -> String {
let hash = Sha256::digest(data);
hex::encode(hash)
}
pub fn claim_id(source_id: &str, span_id: &str, text: &str) -> String {
stable_id("clm", &[source_id, span_id, text], 20)
}
pub fn evidence_bundle_id(claim_id: &str) -> String {
stable_id("evb", &[claim_id], 20)
}
pub fn support_judgment_id(claim_id: &str, support_state: &str) -> String {
stable_id("sup", &[claim_id, support_state], 20)
}
pub fn contradiction_id(left_claim_id: &str, right_claim_id: &str, pattern: &str) -> String {
let (a, b) = if left_claim_id < right_claim_id {
(left_claim_id, right_claim_id)
} else {
(right_claim_id, left_claim_id)
};
stable_id("ctr", &[a, b, pattern], 20)
}
pub fn export_receipt_id(operation: &str, input_refs: &[&str]) -> String {
stable_id(
"xpt",
[operation]
.iter()
.chain(input_refs.iter())
.copied()
.collect::<Vec<_>>()
.as_slice(),
20,
)
}
pub fn support_admission_receipt_id(claim_id: &str, prev_ref: &str, new_ref: &str) -> String {
stable_id("sar", &[claim_id, prev_ref, new_ref], 20)
}
pub fn ledger_append_receipt_id(ledger_path: &str, sequence: u64, entry_digest: &str) -> String {
stable_id(
"lap",
&[ledger_path, &sequence.to_string(), entry_digest],
20,
)
}
pub fn supersession_receipt_id(superseded_ref: &str, superseding_ref: &str) -> String {
stable_id("ssr", &[superseded_ref, superseding_ref], 20)
}
pub fn contradiction_resolution_receipt_id(
contradiction_id: &str,
prev_status: &str,
resolution: &str,
) -> String {
stable_id("crr", &[contradiction_id, prev_status, resolution], 20)
}
pub fn proof_debt_budget_id(scope: &str) -> String {
stable_id("pdb", &[scope], 20)
}
pub fn proof_debt_debit_id(budget_id: &str, source: &str, amount_micros: u64) -> String {
stable_id(
"pdd",
&[budget_id, source, &amount_micros.to_string()],
20,
)
}
pub fn proof_debt_credit_id(budget_id: &str, source: &str, amount_micros: u64) -> String {
stable_id(
"pdc",
&[budget_id, source, &amount_micros.to_string()],
20,
)
}
pub fn proof_debt_waiver_id(budget_id: &str, operator_ref: &str, amount_micros: u64) -> String {
stable_id(
"pdw",
&[budget_id, operator_ref, &amount_micros.to_string()],
20,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stable_id_is_deterministic() {
let id1 = stable_id("test", &["a", "b", "c"], 16);
let id2 = stable_id("test", &["a", "b", "c"], 16);
assert_eq!(id1, id2);
}
#[test]
fn stable_id_differs_for_different_inputs() {
let id1 = stable_id("test", &["a", "b"], 16);
let id2 = stable_id("test", &["a", "c"], 16);
assert_ne!(id1, id2);
}
#[test]
fn stable_id_differs_for_different_prefixes() {
let id1 = stable_id("aaa", &["x"], 16);
let id2 = stable_id("bbb", &["x"], 16);
assert_ne!(id1, id2);
}
#[test]
fn ulid_is_unique() {
let ids: Vec<_> = (0..100).map(|_| ulid()).collect();
let unique: std::collections::HashSet<_> = ids.iter().collect();
assert_eq!(unique.len(), 100);
}
#[test]
fn sha256_text_is_deterministic() {
let h1 = sha256_text("hello world");
let h2 = sha256_text("hello world");
assert_eq!(h1, h2);
}
#[test]
fn sha256_text_differs_for_different_inputs() {
let h1 = sha256_text("hello");
let h2 = sha256_text("world");
assert_ne!(h1, h2);
}
#[test]
fn claim_id_begins_with_clm() {
let id = claim_id("src1", "sp1", "some claim text");
assert!(id.starts_with("clm_"));
}
#[test]
fn evidence_bundle_id_begins_with_evb() {
let id = evidence_bundle_id("clm_abc123");
assert!(id.starts_with("evb_"));
}
#[test]
fn support_judgment_id_begins_with_sup() {
let id = support_judgment_id("clm_abc123", "supported");
assert!(id.starts_with("sup_"));
}
#[test]
fn contradiction_id_is_ordered_and_deterministic() {
let id1 = contradiction_id("clm_a", "clm_b", "exact");
let id2 = contradiction_id("clm_b", "clm_a", "exact");
assert_eq!(id1, id2);
assert!(id1.starts_with("ctr_"));
}
#[test]
fn normalize_text_collapse_whitespace() {
let input = " hello world \n\n foo ";
let normalized = normalize_text(input);
assert_eq!(normalized, "hello world foo");
}
}