use super::powl_model::DeclaredPowlModel;
use serde::{Deserialize, Serialize};
use wasm4pm_compat::conformance::TokenReplayResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PowlConformanceOutcome {
pub model_name: String,
pub fitness: f64,
pub precision: Option<f64>,
pub generalization: Option<f64>,
pub simplicity: Option<f64>,
pub admitted: bool,
pub rationale: String,
pub log_moves: Vec<String>,
pub model_moves: Vec<String>,
}
impl PowlConformanceOutcome {
pub fn refused(model_name: impl Into<String>, rationale: impl Into<String>) -> Self {
PowlConformanceOutcome {
model_name: model_name.into(),
fitness: 0.0,
precision: None,
generalization: None,
simplicity: None,
admitted: false,
rationale: rationale.into(),
log_moves: Vec::new(),
model_moves: Vec::new(),
}
}
pub fn meets_threshold(&self, model: &DeclaredPowlModel) -> bool {
self.fitness >= model.fitness_threshold
}
}
pub fn check_conformance(
model: &DeclaredPowlModel,
ocel_events: &[String],
) -> PowlConformanceOutcome {
if model.model.is_none() {
return PowlConformanceOutcome::refused(
&model.name,
"No POWL model declared — conformance UNKNOWN, not Refused",
);
}
if ocel_events.is_empty() {
return PowlConformanceOutcome::refused(
&model.name,
"Empty event log — no execution evidence to replay",
);
}
let expected_activities = derive_expected_activities(model);
let produced: usize = ocel_events.len();
let matched: usize = ocel_events
.iter()
.filter(|e| expected_activities.contains(&e.as_str()))
.count();
let log_move_events: Vec<String> = ocel_events
.iter()
.filter(|e| !expected_activities.contains(&e.as_str()))
.cloned()
.collect();
let model_move_activities: Vec<String> = expected_activities
.iter()
.filter(|a| !ocel_events.iter().any(|e| e == *a))
.map(|a| a.to_string())
.collect();
let log_moves_count = log_move_events.len();
let model_moves_count = model_move_activities.len();
let fitness = TokenReplayResult::calculate_fitness(
produced,
matched, log_moves_count, model_moves_count, );
let precision = if expected_activities.is_empty() {
None
} else {
Some((matched as f64 / expected_activities.len() as f64).clamp(0.0, 1.0))
};
PowlConformanceOutcome {
model_name: model.name.clone(),
fitness,
precision,
generalization: None, simplicity: None, admitted: fitness >= model.fitness_threshold,
rationale: format!(
"Structural token-replay: fitness={:.3} (threshold={:.3}), \
log_moves={} (forbidden actions), model_moves={} (skipped steps). \
generalization/simplicity=UNKNOWN (full alignment deferred).",
fitness, model.fitness_threshold, log_moves_count, model_moves_count
),
log_moves: log_move_events,
model_moves: model_move_activities,
}
}
fn derive_expected_activities(model: &DeclaredPowlModel) -> Vec<&'static str> {
match model.name.as_str() {
n if n.contains("admission") => vec!["Raw", "Candidate", "Admitted", "Refused"],
n if n.contains("receipt") => {
vec!["ReceiptCreated", "ReceiptValidated", "ReceiptAdmitted"]
}
n if n.contains("diagnostic") => {
vec![
"ObservationCreated",
"DiagnosticEmitted",
"DiagnosticPublished",
]
}
_ => vec![], }
}