use crate::algorithms::discover_alpha_plus_plus;
use crate::error::{codes, wasm_err};
use crate::models::OCEL;
use crate::oc_petri_net::flatten_ocel_to_eventlog_for_type;
use crate::state::{get_or_init_state, StoredObject};
use crate::utilities::to_js;
use serde_json::json;
use std::collections::HashSet;
use wasm_bindgen::prelude::*;
#[cfg(feature = "ocel")]
pub fn oc_conformance_check_inner(ocel: &OCEL) -> Result<serde_json::Value, String> {
let mut per_type = serde_json::Map::new();
let mut total_traces = 0usize;
let mut total_fitting = 0usize;
for obj_type in &ocel.object_types {
let log = flatten_ocel_to_eventlog_for_type(ocel, obj_type)
.map_err(|e| format!("flatten failed: {:?}", e))?;
let trace_count = log.traces.len();
let temp_handle = get_or_init_state()
.store_object(StoredObject::EventLog(log.clone()))
.map_err(|_e| "Failed to store flattened EventLog".to_string())?;
let net_js = discover_alpha_plus_plus(&temp_handle, "concept:name", 0.5)
.map_err(|e| format!("alpha++ failed: {:?}", e))?;
let net_json: serde_json::Value = serde_wasm_bindgen::from_value(net_js)
.map_err(|e| format!("Failed to parse Petri Net: {}", e))?;
let transition_labels: HashSet<String> =
match net_json.get("transitions").and_then(|t| t.as_array()) {
Some(arr) => arr
.iter()
.filter_map(|t| t.get("label").and_then(|l| l.as_str()).map(String::from))
.collect(),
None => return Err("Net JSON missing or invalid 'transitions' array".to_string()),
};
let mut fitting = 0usize;
let mut deviations: Vec<serde_json::Value> = Vec::new();
for trace in &log.traces {
let activities: Vec<String> = trace
.events
.iter()
.filter_map(|e| {
e.attributes.get("concept:name").and_then(|v| match v {
crate::models::AttributeValue::String(s) => Some(s.clone()),
_ => None,
})
})
.collect();
let missing: Vec<String> = activities
.iter()
.filter(|a| !transition_labels.contains(a.as_str()))
.cloned()
.collect();
if missing.is_empty() {
fitting += 1;
} else {
deviations.push(json!({
"trace_activities": activities,
"missing_transitions": missing,
}));
}
}
let fitness = if trace_count > 0 {
fitting as f64 / trace_count as f64
} else {
1.0
};
total_traces += trace_count;
total_fitting += fitting;
per_type.insert(
obj_type.clone(),
json!({
"fitness": fitness,
"traces": trace_count,
"fitting_traces": fitting,
"deviations": deviations.len(),
"sample_deviations": &deviations[..deviations.len().min(5)],
}),
);
}
let overall_fitness = if total_traces > 0 {
total_fitting as f64 / total_traces as f64
} else {
1.0
};
let mut result = serde_json::Map::new();
result.extend(per_type);
result.insert(
"overall".into(),
json!({
"fitness": overall_fitness,
"total_traces": total_traces,
"fitting_traces": total_fitting,
}),
);
Ok(serde_json::Value::Object(result))
}
#[cfg(feature = "ocel")]
#[wasm_bindgen]
pub fn oc_conformance_check(ocel_handle: &str) -> Result<JsValue, JsValue> {
let ocel = get_ocel(ocel_handle)?;
let result =
oc_conformance_check_inner(&ocel).map_err(|e| wasm_err(codes::INTERNAL_ERROR, &e))?;
to_js(&result)
}
#[cfg(feature = "ocel")]
#[wasm_bindgen]
pub fn oc_conformance_info() -> JsValue {
let info = json!({
"module": "oc_conformance",
"description": "Object-Centric conformance checking against OC Petri Nets",
"functions": [
{
"name": "oc_conformance_check",
"description": "Check conformance of OCEL traces against discovered nets",
"params": ["ocel_handle"],
"returns": "JSON {object_type: {fitness, traces, …}, overall: {fitness, …}}"
},
{
"name": "oc_conformance_info",
"description": "Get information about this module",
"params": [],
"returns": "JSON info"
}
]
});
to_js(&info).unwrap_or(JsValue::NULL)
}
fn get_ocel(handle: &str) -> Result<OCEL, JsValue> {
get_or_init_state().with_object(handle, |obj| match obj {
Some(StoredObject::OCEL(ocel)) => Ok(ocel.clone()),
Some(_) => Err(wasm_err(codes::INVALID_INPUT, "Object is not an OCEL")),
None => Err(wasm_err(
codes::INVALID_HANDLE,
format!("OCEL '{}' not found", handle),
)),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{OCELEvent, OCELObject, OCEL};
fn create_test_ocel() -> OCEL {
OCEL {
event_types: vec!["A".to_string(), "B".to_string()],
object_types: vec!["Order".to_string()],
events: vec![
OCELEvent {
id: "e1".to_string(),
event_type: "A".to_string(),
timestamp: "2024-01-01T10:00:00Z".to_string(),
attributes: std::collections::HashMap::new(),
object_ids: vec!["order1".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e2".to_string(),
event_type: "B".to_string(),
timestamp: "2024-01-01T11:00:00Z".to_string(),
attributes: std::collections::HashMap::new(),
object_ids: vec!["order1".to_string()],
object_refs: vec![],
},
],
objects: vec![OCELObject {
id: "order1".to_string(),
object_type: "Order".to_string(),
attributes: std::collections::HashMap::new(),
changes: vec![],
embedded_relations: vec![],
}],
object_relations: vec![],
}
}
#[test]
fn test_oc_conformance_basic() {
let ocel = OCEL {
event_types: vec![],
object_types: vec![], events: vec![],
objects: vec![],
object_relations: vec![],
};
let result = oc_conformance_check_inner(&ocel);
assert!(result.is_ok(), "Empty OCEL conformance should succeed");
let json = result.unwrap();
assert!(json.get("overall").is_some());
assert_eq!(json["overall"]["fitness"].as_f64().unwrap(), 1.0);
}
#[test]
fn test_oc_conformance_empty_ocel() {
let ocel = OCEL {
event_types: vec![],
object_types: vec![],
events: vec![],
objects: vec![],
object_relations: vec![],
};
let result = oc_conformance_check_inner(&ocel);
assert!(result.is_ok(), "Empty OCEL should succeed");
let json = result.unwrap();
let overall_fitness = json["overall"]["fitness"].as_f64().unwrap();
assert_eq!(overall_fitness, 1.0, "Empty log fitness should be 1.0");
}
#[test]
fn test_oc_conformance_invalid_handle() {
let found = get_or_init_state().with_object("no_such_handle", |obj| {
Ok::<bool, wasm_bindgen::JsValue>(obj.is_some())
});
assert!(found.is_ok());
assert!(!found.unwrap(), "Unknown handle must return None");
}
#[test]
fn test_oc_conformance_returns_json_structure() {
let ocel = OCEL {
event_types: vec![],
object_types: vec![],
events: vec![],
objects: vec![],
object_relations: vec![],
};
let result = oc_conformance_check_inner(&ocel).expect("Conformance check failed");
assert!(result.is_object());
let overall = &result["overall"];
assert!(overall["fitness"].is_f64());
assert!(overall["total_traces"].is_number());
assert!(overall["fitting_traces"].is_number());
}
}