use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const PPR_AMPLIFICATION_FLOOR: f32 = 0.75;
pub const AUTHOR_FINGERPRINT_BYTES: usize = 8;
pub const AUTHOR_RATE_LIMIT_WINDOW_SECS: u64 = 60;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct TrustBoundary {
pub confidence_floor: f32,
pub consumer_opt_in: bool,
}
impl TrustBoundary {
#[must_use]
pub fn new(confidence_floor: f32, consumer_opt_in: bool) -> Option<Self> {
if !confidence_floor.is_finite() || !(0.0..=1.0).contains(&confidence_floor) {
return None;
}
Some(Self {
confidence_floor,
consumer_opt_in,
})
}
#[must_use]
pub fn ppr_default(consumer_opt_in: bool) -> Self {
Self {
confidence_floor: PPR_AMPLIFICATION_FLOOR,
consumer_opt_in,
}
}
#[must_use]
pub fn admit(&self, candidate: &Candidate) -> bool {
if !self.consumer_opt_in {
return false;
}
if !candidate.opt_in {
return false;
}
if !candidate.confidence.is_finite() {
return false;
}
candidate.confidence >= self.confidence_floor
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Candidate {
pub confidence: f32,
pub opt_in: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AuthorFingerprint([u8; AUTHOR_FINGERPRINT_BYTES]);
impl AuthorFingerprint {
#[must_use]
pub fn from_author_id(author_id: &str) -> Self {
let mut hasher = Sha256::new();
hasher.update(author_id.as_bytes());
let digest = hasher.finalize();
let mut out = [0u8; AUTHOR_FINGERPRINT_BYTES];
out.copy_from_slice(&digest[..AUTHOR_FINGERPRINT_BYTES]);
Self(out)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; AUTHOR_FINGERPRINT_BYTES] {
&self.0
}
#[must_use]
pub fn as_hex(&self) -> String {
let mut s = String::with_capacity(AUTHOR_FINGERPRINT_BYTES * 2);
for b in &self.0 {
use std::fmt::Write as _;
let _ = write!(s, "{b:02x}");
}
s
}
}
#[derive(Debug, Clone)]
pub struct AuthorRateLimiter {
per_commit_cap: u32,
window_start_secs: u64,
buckets: std::collections::HashMap<AuthorFingerprint, u32>,
}
impl AuthorRateLimiter {
#[must_use]
pub fn new(per_commit_cap: u32, now_secs: u64) -> Self {
Self {
per_commit_cap,
window_start_secs: now_secs,
buckets: std::collections::HashMap::new(),
}
}
pub fn tick(&mut self, now_secs: u64) {
if now_secs >= self.window_start_secs
&& now_secs - self.window_start_secs >= AUTHOR_RATE_LIMIT_WINDOW_SECS
{
self.buckets.clear();
self.window_start_secs = now_secs;
}
}
pub fn admit(&mut self, author: &AuthorFingerprint) -> bool {
let counter = self.buckets.entry(*author).or_insert(0);
if *counter >= self.per_commit_cap {
return false;
}
*counter += 1;
true
}
#[must_use]
pub fn count(&self, author: &AuthorFingerprint) -> u32 {
self.buckets.get(author).copied().unwrap_or(0)
}
#[must_use]
pub fn window_start_secs(&self) -> u64 {
self.window_start_secs
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn admit_rejects_when_consumer_not_opted_in() {
let tb = TrustBoundary::new(0.5, false).unwrap();
let c = Candidate {
confidence: 0.99,
opt_in: true,
};
assert!(!tb.admit(&c));
}
#[test]
fn admit_rejects_when_producer_did_not_opt_in() {
let tb = TrustBoundary::new(0.5, true).unwrap();
let c = Candidate {
confidence: 0.99,
opt_in: false,
};
assert!(!tb.admit(&c));
}
#[test]
fn admit_rejects_below_confidence_floor() {
let tb = TrustBoundary::new(0.75, true).unwrap();
let c = Candidate {
confidence: 0.7499,
opt_in: true,
};
assert!(!tb.admit(&c));
}
#[test]
fn admit_accepts_at_and_above_floor() {
let tb = TrustBoundary::new(0.75, true).unwrap();
assert!(tb.admit(&Candidate {
confidence: 0.75,
opt_in: true
}));
assert!(tb.admit(&Candidate {
confidence: 0.99,
opt_in: true
}));
}
#[test]
fn admit_rejects_nan_confidence() {
let tb = TrustBoundary::new(0.5, true).unwrap();
assert!(!tb.admit(&Candidate {
confidence: f32::NAN,
opt_in: true
}));
}
#[test]
fn new_rejects_out_of_range_floor() {
assert!(TrustBoundary::new(-0.1, true).is_none());
assert!(TrustBoundary::new(1.1, true).is_none());
assert!(TrustBoundary::new(f32::NAN, true).is_none());
}
#[test]
fn ppr_default_uses_spec_pinned_floor() {
let tb = TrustBoundary::ppr_default(true);
assert!((tb.confidence_floor - PPR_AMPLIFICATION_FLOOR).abs() < f32::EPSILON);
}
#[test]
fn fingerprint_is_deterministic_and_truncated() {
let a = AuthorFingerprint::from_author_id("alice@example.com");
let b = AuthorFingerprint::from_author_id("alice@example.com");
assert_eq!(a, b);
assert_eq!(a.as_bytes().len(), AUTHOR_FINGERPRINT_BYTES);
assert_eq!(a.as_hex().len(), AUTHOR_FINGERPRINT_BYTES * 2);
}
#[test]
fn fingerprint_distinguishes_distinct_authors() {
let a = AuthorFingerprint::from_author_id("alice");
let b = AuthorFingerprint::from_author_id("bob");
assert_ne!(a, b);
}
#[test]
fn rate_limiter_admits_up_to_cap_then_rejects() {
let author = AuthorFingerprint::from_author_id("author-x");
let mut rl = AuthorRateLimiter::new(3, 0);
assert!(rl.admit(&author));
assert!(rl.admit(&author));
assert!(rl.admit(&author));
assert!(!rl.admit(&author));
assert_eq!(rl.count(&author), 3);
}
#[test]
fn rate_limiter_resets_after_window_elapses() {
let author = AuthorFingerprint::from_author_id("author-x");
let mut rl = AuthorRateLimiter::new(2, 0);
assert!(rl.admit(&author));
assert!(rl.admit(&author));
assert!(!rl.admit(&author));
rl.tick(AUTHOR_RATE_LIMIT_WINDOW_SECS);
assert!(rl.admit(&author));
assert_eq!(rl.window_start_secs(), AUTHOR_RATE_LIMIT_WINDOW_SECS);
}
#[test]
fn rate_limiter_does_not_reset_within_window() {
let author = AuthorFingerprint::from_author_id("author-x");
let mut rl = AuthorRateLimiter::new(2, 0);
assert!(rl.admit(&author));
assert!(rl.admit(&author));
rl.tick(AUTHOR_RATE_LIMIT_WINDOW_SECS - 1);
assert!(!rl.admit(&author));
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn admit_rejects_below_confidence_floor(
floor in 0.0f32..=1.0,
delta in 0.0001f32..0.5,
opt_in in any::<bool>(),
) {
let tb = TrustBoundary::new(floor, true).unwrap();
let conf = (floor - delta).max(0.0);
if conf < floor {
let c = Candidate { confidence: conf, opt_in };
prop_assert!(!tb.admit(&c));
}
}
#[test]
fn admit_accepts_above_floor_with_opt_in(
floor in 0.0f32..=1.0,
above in 0.0f32..=0.5,
) {
let tb = TrustBoundary::new(floor, true).unwrap();
let conf = (floor + above).min(1.0);
let c = Candidate { confidence: conf, opt_in: true };
prop_assert!(tb.admit(&c));
}
#[test]
fn fingerprint_stable_across_calls(s in "[a-zA-Z0-9@._-]{1,64}") {
let a = AuthorFingerprint::from_author_id(&s);
let b = AuthorFingerprint::from_author_id(&s);
prop_assert_eq!(a, b);
}
}
}