use serde::{Deserialize, Serialize};
use chio_core::underwriting::{
UnderwritingComplianceEvidence, UNDERWRITING_COMPLIANCE_EVIDENCE_SCHEMA,
};
use crate::operator_report::ComplianceReport;
pub const COMPLIANCE_SCORE_MAX: u32 = 1000;
pub const WEIGHT_DENY_RATE: u32 = 300;
pub const WEIGHT_REVOCATION: u32 = 300;
pub const WEIGHT_VELOCITY_ANOMALY: u32 = 150;
pub const WEIGHT_POLICY_COVERAGE: u32 = 150;
pub const WEIGHT_ATTESTATION_FRESHNESS: u32 = 100;
pub const DEFAULT_ATTESTATION_STALENESS_SECS: u64 = 7_776_000;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceScoreInputs {
pub total_receipts: u64,
pub deny_receipts: u64,
pub observed_capabilities: u64,
pub revoked_capabilities: u64,
pub any_revoked: bool,
pub velocity_windows: u64,
pub anomalous_velocity_windows: u64,
pub attestation_age_secs: Option<u64>,
}
impl ComplianceScoreInputs {
#[must_use]
pub fn new(
total_receipts: u64,
deny_receipts: u64,
observed_capabilities: u64,
revoked_capabilities: u64,
velocity_windows: u64,
anomalous_velocity_windows: u64,
attestation_age_secs: Option<u64>,
) -> Self {
let any_revoked = revoked_capabilities > 0;
Self {
total_receipts,
deny_receipts,
observed_capabilities,
revoked_capabilities,
any_revoked,
velocity_windows,
anomalous_velocity_windows,
attestation_age_secs,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceFactor {
pub name: String,
pub weight: u32,
pub deduction: u32,
pub points: u32,
pub rate: f64,
}
impl ComplianceFactor {
fn from_rate(name: &str, weight: u32, rate: f64) -> Self {
let clamped = rate.clamp(0.0, 1.0);
let raw = (clamped * f64::from(weight)).round();
let deduction = raw.clamp(0.0, f64::from(weight)) as u32;
let points = weight.saturating_sub(deduction);
Self {
name: name.to_string(),
weight,
deduction,
points,
rate: clamped,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceFactorBreakdown {
pub deny_rate: ComplianceFactor,
pub revocation: ComplianceFactor,
pub velocity_anomaly: ComplianceFactor,
pub policy_coverage: ComplianceFactor,
pub attestation_freshness: ComplianceFactor,
}
impl ComplianceFactorBreakdown {
#[must_use]
pub fn total_deductions(&self) -> u32 {
self.deny_rate.deduction
+ self.revocation.deduction
+ self.velocity_anomaly.deduction
+ self.policy_coverage.deduction
+ self.attestation_freshness.deduction
}
#[must_use]
pub fn total_points(&self) -> u32 {
self.deny_rate.points
+ self.revocation.points
+ self.velocity_anomaly.points
+ self.policy_coverage.points
+ self.attestation_freshness.points
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceScore {
pub agent_id: String,
pub score: u32,
pub factor_breakdown: ComplianceFactorBreakdown,
pub generated_at: u64,
pub inputs: ComplianceScoreInputs,
}
impl ComplianceScore {
#[must_use]
pub fn as_underwriting_evidence(&self) -> UnderwritingComplianceEvidence {
UnderwritingComplianceEvidence {
schema: UNDERWRITING_COMPLIANCE_EVIDENCE_SCHEMA.to_string(),
agent_id: self.agent_id.clone(),
score: self.score,
generated_at: self.generated_at,
total_receipts: self.inputs.total_receipts,
deny_receipts: self.inputs.deny_receipts,
observed_capabilities: self.inputs.observed_capabilities,
revoked_capabilities: self.inputs.revoked_capabilities,
attestation_age_secs: self.inputs.attestation_age_secs,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceScoreConfig {
pub attestation_staleness_secs: u64,
pub treat_any_revocation_as_full: bool,
pub revocation_ceiling: u32,
}
impl Default for ComplianceScoreConfig {
fn default() -> Self {
Self {
attestation_staleness_secs: DEFAULT_ATTESTATION_STALENESS_SECS,
treat_any_revocation_as_full: true,
revocation_ceiling: 499,
}
}
}
#[must_use]
pub fn compliance_score(
report: &ComplianceReport,
inputs: &ComplianceScoreInputs,
config: &ComplianceScoreConfig,
agent_id: &str,
now: u64,
) -> ComplianceScore {
let breakdown = compliance_factor_breakdown(report, inputs, config);
let raw_score = COMPLIANCE_SCORE_MAX.saturating_sub(breakdown.total_deductions());
let score = if inputs.any_revoked || inputs.revoked_capabilities > 0 {
raw_score.min(config.revocation_ceiling)
} else {
raw_score
};
ComplianceScore {
agent_id: agent_id.to_string(),
score,
factor_breakdown: breakdown,
generated_at: now,
inputs: inputs.clone(),
}
}
#[must_use]
pub fn compliance_factor_breakdown(
report: &ComplianceReport,
inputs: &ComplianceScoreInputs,
config: &ComplianceScoreConfig,
) -> ComplianceFactorBreakdown {
let deny_rate = if inputs.total_receipts == 0 {
0.0
} else {
inputs.deny_receipts as f64 / inputs.total_receipts as f64
};
let revocation_rate = if inputs.observed_capabilities == 0 {
if config.treat_any_revocation_as_full && inputs.any_revoked {
1.0
} else {
0.0
}
} else {
let raw = inputs.revoked_capabilities as f64 / inputs.observed_capabilities as f64;
if config.treat_any_revocation_as_full && inputs.any_revoked {
raw.max(1.0)
} else {
raw
}
};
let velocity_rate = if inputs.velocity_windows == 0 {
0.0
} else {
inputs.anomalous_velocity_windows as f64 / inputs.velocity_windows as f64
};
let policy_coverage_gap = if report.matching_receipts == 0 {
0.0
} else {
let checkpoint_coverage = report.checkpoint_coverage_rate.unwrap_or_else(|| {
if report.matching_receipts == 0 {
1.0
} else {
report.evidence_ready_receipts as f64 / report.matching_receipts as f64
}
});
let lineage_coverage = report.lineage_coverage_rate.unwrap_or_else(|| {
if report.matching_receipts == 0 {
1.0
} else {
report.lineage_covered_receipts as f64 / report.matching_receipts as f64
}
});
let avg_coverage = ((checkpoint_coverage + lineage_coverage) / 2.0).clamp(0.0, 1.0);
1.0 - avg_coverage
};
let freshness_rate = match inputs.attestation_age_secs {
None => 1.0,
Some(age) => {
if config.attestation_staleness_secs == 0 {
0.0
} else {
(age as f64 / config.attestation_staleness_secs as f64).clamp(0.0, 1.0)
}
}
};
ComplianceFactorBreakdown {
deny_rate: ComplianceFactor::from_rate("deny_rate", WEIGHT_DENY_RATE, deny_rate),
revocation: ComplianceFactor::from_rate("revocation", WEIGHT_REVOCATION, revocation_rate),
velocity_anomaly: ComplianceFactor::from_rate(
"velocity_anomaly",
WEIGHT_VELOCITY_ANOMALY,
velocity_rate,
),
policy_coverage: ComplianceFactor::from_rate(
"policy_coverage",
WEIGHT_POLICY_COVERAGE,
policy_coverage_gap,
),
attestation_freshness: ComplianceFactor::from_rate(
"attestation_freshness",
WEIGHT_ATTESTATION_FRESHNESS,
freshness_rate,
),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::evidence_export::{EvidenceChildReceiptScope, EvidenceExportQuery};
fn perfect_report() -> ComplianceReport {
ComplianceReport {
matching_receipts: 1000,
evidence_ready_receipts: 1000,
uncheckpointed_receipts: 0,
checkpoint_coverage_rate: Some(1.0),
lineage_covered_receipts: 1000,
lineage_gap_receipts: 0,
lineage_coverage_rate: Some(1.0),
pending_settlement_receipts: 0,
failed_settlement_receipts: 0,
direct_evidence_export_supported: true,
child_receipt_scope: EvidenceChildReceiptScope::FullQueryWindow,
proofs_complete: true,
export_query: EvidenceExportQuery::default(),
export_scope_note: None,
}
}
#[test]
fn clean_agent_scores_above_900() {
let inputs = ComplianceScoreInputs::new(1000, 0, 1, 0, 0, 0, Some(0));
let score = compliance_score(
&perfect_report(),
&inputs,
&ComplianceScoreConfig::default(),
"agent-1",
0,
);
assert!(
score.score > 900,
"clean agent should score >900, got {}",
score.score
);
}
#[test]
fn revocation_flag_drives_score_below_500() {
let mut inputs = ComplianceScoreInputs::new(1000, 0, 1, 1, 0, 0, Some(0));
inputs.any_revoked = true;
let score = compliance_score(
&perfect_report(),
&inputs,
&ComplianceScoreConfig::default(),
"agent-2",
0,
);
assert!(
score.score < 500,
"revoked agent should score <500, got {}",
score.score
);
}
#[test]
fn empty_report_scores_perfectly_on_coverage() {
let report = ComplianceReport {
matching_receipts: 0,
evidence_ready_receipts: 0,
uncheckpointed_receipts: 0,
checkpoint_coverage_rate: None,
lineage_covered_receipts: 0,
lineage_gap_receipts: 0,
lineage_coverage_rate: None,
pending_settlement_receipts: 0,
failed_settlement_receipts: 0,
direct_evidence_export_supported: true,
child_receipt_scope: EvidenceChildReceiptScope::FullQueryWindow,
proofs_complete: true,
export_query: EvidenceExportQuery::default(),
export_scope_note: None,
};
let inputs = ComplianceScoreInputs::new(0, 0, 0, 0, 0, 0, Some(0));
let breakdown =
compliance_factor_breakdown(&report, &inputs, &ComplianceScoreConfig::default());
assert_eq!(breakdown.policy_coverage.deduction, 0);
assert_eq!(breakdown.deny_rate.deduction, 0);
}
#[test]
fn stale_attestation_deducts_freshness_factor() {
let inputs = ComplianceScoreInputs::new(
100,
0,
1,
0,
0,
0,
Some(DEFAULT_ATTESTATION_STALENESS_SECS),
);
let breakdown = compliance_factor_breakdown(
&perfect_report(),
&inputs,
&ComplianceScoreConfig::default(),
);
assert_eq!(
breakdown.attestation_freshness.deduction, WEIGHT_ATTESTATION_FRESHNESS,
"fully stale attestation should deduct the full weight"
);
}
#[test]
fn weights_sum_to_maximum() {
assert_eq!(
WEIGHT_DENY_RATE
+ WEIGHT_REVOCATION
+ WEIGHT_VELOCITY_ANOMALY
+ WEIGHT_POLICY_COVERAGE
+ WEIGHT_ATTESTATION_FRESHNESS,
COMPLIANCE_SCORE_MAX
);
}
}