use serde::{Deserialize, Serialize};
use crate::error::{check_non_negative_finite, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EnergyComponent {
Syn,
Str,
Log,
Boot,
Sheaf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResidualClass {
Syntax,
Type,
Build,
TestFailure,
Lint,
Format,
Runtime,
Dependency,
Manifest,
ImportGraph,
SymbolMismatch,
InterfaceMismatch,
OwnershipViolation,
ContextDrift,
Regression,
Policy,
SensorUnavailable,
ToolFailure,
SheafInconsistency,
CapabilityDenied,
BudgetExhausted,
}
impl ResidualClass {
pub fn default_component(self) -> EnergyComponent {
use EnergyComponent::*;
use ResidualClass::*;
match self {
Syntax | Type | Build => Syn,
Lint | Format | ImportGraph | SymbolMismatch | InterfaceMismatch
| OwnershipViolation | Manifest | Dependency => Str,
TestFailure | Runtime | Regression => Log,
SensorUnavailable | ToolFailure => Boot,
SheafInconsistency | ContextDrift => Sheaf,
Policy | CapabilityDenied | BudgetExhausted => Str,
}
}
pub fn is_admissibility_outcome(self) -> bool {
matches!(
self,
ResidualClass::CapabilityDenied | ResidualClass::BudgetExhausted
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResidualSeverity {
Hint,
Warning,
Error,
Blocking,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IndependenceRoute {
DeterministicTool,
Compiler,
Lsp,
TestOracle,
FormalSolver,
RepoScript,
ExternalApi,
SeparateModel,
SameModelCritique,
}
impl IndependenceRoute {
pub fn is_full_weight_eligible(self) -> bool {
!matches!(self, IndependenceRoute::SameModelCritique)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SensorRef {
pub id: String,
pub route: IndependenceRoute,
}
impl SensorRef {
pub fn new(id: impl Into<String>, route: IndependenceRoute) -> Self {
Self {
id: id.into(),
route,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SymbolRef {
pub name: String,
pub container: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EvidencePayload {
pub summary: String,
pub raw: Option<String>,
pub structured: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CorrectionDirection {
pub direction_id: String,
pub addresses: ResidualClass,
pub instruction: String,
pub target_paths: Vec<String>,
pub target_symbols: Vec<SymbolRef>,
pub rationale: String,
}
impl CorrectionDirection {
pub fn new(addresses: ResidualClass, instruction: impl Into<String>) -> Self {
Self {
direction_id: uuid::Uuid::new_v4().to_string(),
addresses,
instruction: instruction.into(),
target_paths: Vec::new(),
target_symbols: Vec::new(),
rationale: String::new(),
}
}
pub fn with_rationale(mut self, rationale: impl Into<String>) -> Self {
self.rationale = rationale.into();
self
}
pub fn with_paths(mut self, paths: Vec<String>) -> Self {
self.target_paths = paths;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResidualEvent {
pub residual_id: String,
pub node_id: String,
pub generation: u32,
pub component: EnergyComponent,
pub class: ResidualClass,
pub severity: ResidualSeverity,
pub score: f64,
pub sensor: SensorRef,
pub evidence: EvidencePayload,
pub affected_paths: Vec<String>,
pub affected_symbols: Vec<SymbolRef>,
pub correction_directions: Vec<CorrectionDirection>,
}
impl ResidualEvent {
pub fn new(
node_id: impl Into<String>,
generation: u32,
class: ResidualClass,
severity: ResidualSeverity,
score: f64,
sensor: SensorRef,
) -> Result<Self> {
check_non_negative_finite(score, "residual score")?;
Ok(Self {
residual_id: uuid::Uuid::new_v4().to_string(),
node_id: node_id.into(),
generation,
component: class.default_component(),
class,
severity,
score,
sensor,
evidence: EvidencePayload::default(),
affected_paths: Vec::new(),
affected_symbols: Vec::new(),
correction_directions: Vec::new(),
})
}
pub fn with_evidence(mut self, evidence: EvidencePayload) -> Self {
self.evidence = evidence;
self
}
pub fn with_component(mut self, component: EnergyComponent) -> Self {
self.component = component;
self
}
pub fn with_paths(mut self, paths: Vec<String>) -> Self {
self.affected_paths = paths;
self
}
pub fn with_correction(mut self, direction: CorrectionDirection) -> Self {
self.correction_directions.push(direction);
self
}
pub fn is_admissibility_outcome(&self) -> bool {
self.class.is_admissibility_outcome()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResidualEventRef {
pub residual_id: String,
pub class: ResidualClass,
pub component: EnergyComponent,
pub weighted_energy: f64,
}
#[cfg(test)]
mod tests {
use super::*;
fn sensor() -> SensorRef {
SensorRef::new("compiler", IndependenceRoute::Compiler)
}
#[test]
fn rejects_negative_score() {
let err = ResidualEvent::new(
"n1",
0,
ResidualClass::Type,
ResidualSeverity::Error,
-1.0,
sensor(),
);
assert!(err.is_err());
}
#[test]
fn rejects_nan_and_inf_score() {
assert!(ResidualEvent::new(
"n1",
0,
ResidualClass::Type,
ResidualSeverity::Error,
f64::NAN,
sensor()
)
.is_err());
assert!(ResidualEvent::new(
"n1",
0,
ResidualClass::Type,
ResidualSeverity::Error,
f64::INFINITY,
sensor()
)
.is_err());
}
#[test]
fn class_maps_to_default_component() {
assert_eq!(
ResidualClass::Type.default_component(),
EnergyComponent::Syn
);
assert_eq!(
ResidualClass::TestFailure.default_component(),
EnergyComponent::Log
);
assert_eq!(
ResidualClass::ImportGraph.default_component(),
EnergyComponent::Str
);
assert_eq!(
ResidualClass::ToolFailure.default_component(),
EnergyComponent::Boot
);
assert_eq!(
ResidualClass::SheafInconsistency.default_component(),
EnergyComponent::Sheaf
);
}
#[test]
fn admissibility_outcomes_flagged() {
assert!(ResidualClass::CapabilityDenied.is_admissibility_outcome());
assert!(ResidualClass::BudgetExhausted.is_admissibility_outcome());
assert!(!ResidualClass::Type.is_admissibility_outcome());
}
#[test]
fn same_model_critique_not_full_weight() {
assert!(!IndependenceRoute::SameModelCritique.is_full_weight_eligible());
assert!(IndependenceRoute::Compiler.is_full_weight_eligible());
}
}