use crate::state::{get_or_init_state, StoredObject};
use crate::utilities::to_js_str;
use serde_json::{json, Value};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn measure_trace_determinism(
handle: &str,
activity_key: &str,
algorithm: &str,
) -> Result<JsValue, JsValue> {
get_or_init_state().with_event_log(handle, |log| {
let log_size: usize = log.traces.iter().map(|t| t.events.len()).sum();
let mut hashes: Vec<String> = Vec::new();
for _run in 0..3 {
let output = run_discovery_algorithm(handle, activity_key, algorithm)?;
let output_str = match output {
Value::String(s) => s,
_ => serde_json::to_string(&output).unwrap_or_default(),
};
let hash = blake3_hash(&output_str);
hashes.push(hash);
}
let stable = hashes.iter().all(|h| h == &hashes[0]);
let result = json!({
"algorithm": algorithm,
"log_size": log_size,
"run_count": 3,
"hashes": hashes,
"stable": stable,
"all_identical": stable
});
to_js_str(&result)
})
}
#[wasm_bindgen]
pub fn measure_algorithm_quality_baseline(
handle: &str,
activity_key: &str,
algorithm: &str,
) -> Result<JsValue, JsValue> {
get_or_init_state().with_event_log(handle, |log| {
let log_size: usize = log.traces.iter().map(|t| t.events.len()).sum();
let output = run_discovery_algorithm(handle, activity_key, algorithm)?;
let fitness = estimate_fitness(&output);
let precision = estimate_precision(&output);
let quality_score = (fitness + precision) / 2.0;
let model_size = extract_model_size(&output);
let result = json!({
"algorithm": algorithm,
"log_size": log_size,
"fitness": fitness,
"precision": precision,
"quality_score": quality_score,
"model_size": model_size
});
to_js_str(&result)
})
}
#[wasm_bindgen]
pub fn benchmark_algorithm(
handle: &str,
activity_key: &str,
algorithm: &str,
iterations: u32,
) -> Result<JsValue, JsValue> {
get_or_init_state().with_event_log(handle, |log| {
let log_size: usize = log.traces.iter().map(|t| t.events.len()).sum();
let mut latencies: Vec<f64> = Vec::new();
for _ in 0..iterations {
#[cfg(target_arch = "wasm32")]
let start = js_sys::Date::now();
#[cfg(not(target_arch = "wasm32"))]
let start = std::time::Instant::now();
let _ = run_discovery_algorithm(handle, activity_key, algorithm);
#[cfg(target_arch = "wasm32")]
let elapsed_ms = js_sys::Date::now() - start;
#[cfg(not(target_arch = "wasm32"))]
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
latencies.push(elapsed_ms);
}
latencies.sort_by(f64::total_cmp);
let mean = latencies.iter().sum::<f64>() / latencies.len() as f64;
let min = latencies.first().copied().unwrap_or(0.0);
let max = latencies.last().copied().unwrap_or(0.0);
let p50_idx = latencies.len() / 2;
let p95_idx = (latencies.len() as f64 * 0.95) as usize;
let p99_idx = (latencies.len() as f64 * 0.99) as usize;
let result = json!({
"algorithm": algorithm,
"iterations": iterations,
"log_size": log_size,
"p50_ms": latencies.get(p50_idx).copied().unwrap_or(0.0),
"p95_ms": latencies.get(p95_idx).copied().unwrap_or(0.0),
"p99_ms": latencies.get(p99_idx).copied().unwrap_or(0.0),
"mean_ms": mean,
"min_ms": min,
"max_ms": max
});
to_js_str(&result)
})
}
#[wasm_bindgen]
pub fn validate_output_format(output_json: &str, algorithm: &str) -> Result<JsValue, JsValue> {
let parsed: Result<Value, _> = serde_json::from_str(output_json);
let output = match parsed {
Ok(v) => v,
Err(e) => {
let result = json!({
"algorithm": algorithm,
"valid": false,
"missing_fields": [],
"extra_fields": [],
"schema_errors": [format!("Invalid JSON: {}", e)]
});
return to_js_str(&result);
}
};
let (required_fields, optional_fields) = get_algorithm_schema_fields(algorithm);
let obj = match output.as_object() {
Some(o) => o,
None => {
let result = json!({
"algorithm": algorithm,
"valid": false,
"missing_fields": required_fields,
"extra_fields": [],
"schema_errors": ["Output is not a JSON object".to_string()]
});
return to_js_str(&result);
}
};
let mut missing = Vec::new();
for field in &required_fields {
if !obj.contains_key(field) {
missing.push(field.clone());
}
}
let mut extra = Vec::new();
let all_allowed: Vec<&String> = required_fields
.iter()
.chain(optional_fields.iter())
.collect();
for key in obj.keys() {
if !all_allowed.contains(&key) {
extra.push(key.clone());
}
}
let valid = missing.is_empty();
let result = json!({
"algorithm": algorithm,
"valid": valid,
"missing_fields": missing,
"extra_fields": extra,
"schema_errors": []
});
to_js_str(&result)
}
#[wasm_bindgen]
pub fn get_algorithm_metadata(algorithm: &str) -> Result<JsValue, JsValue> {
let metadata = match algorithm {
"dfg" => json!({
"name": "dfg",
"display_name": "Directly-Follows Graph",
"category": "discovery",
"time_complexity": "O(n log n)",
"space_complexity": "O(m)",
"speed_score": 5,
"quality_score": 30,
"supports_ocel": false,
"supports_streaming": false,
"required_inputs": ["log_handle", "activity_key"],
"output_type": "dfg"
}),
"heuristic_miner" => json!({
"name": "heuristic_miner",
"display_name": "Heuristic Miner",
"category": "discovery",
"time_complexity": "O(n)",
"space_complexity": "O(m)",
"speed_score": 25,
"quality_score": 50,
"supports_ocel": false,
"supports_streaming": false,
"required_inputs": ["log_handle", "activity_key", "threshold"],
"output_type": "dfg"
}),
"genetic_algorithm" => json!({
"name": "genetic_algorithm",
"display_name": "Genetic Algorithm",
"category": "discovery",
"time_complexity": "O(n * population * generations)",
"space_complexity": "O(m * population)",
"speed_score": 75,
"quality_score": 80,
"supports_ocel": false,
"supports_streaming": false,
"required_inputs": ["log_handle", "activity_key"],
"output_type": "petrinet"
}),
"ilp" => json!({
"name": "ilp",
"display_name": "Integer Linear Programming",
"category": "discovery",
"time_complexity": "O(n^3) [solver-dependent]",
"space_complexity": "O(m^2)",
"speed_score": 80,
"quality_score": 90,
"supports_ocel": false,
"supports_streaming": false,
"required_inputs": ["log_handle", "activity_key"],
"output_type": "petrinet"
}),
_ => json!({
"name": algorithm,
"display_name": algorithm,
"category": "unknown",
"time_complexity": "unknown",
"space_complexity": "unknown",
"speed_score": 0,
"quality_score": 0,
"supports_ocel": false,
"supports_streaming": false,
"required_inputs": ["log_handle", "activity_key"],
"output_type": "unknown"
}),
};
to_js_str(&metadata)
}
fn run_discovery_algorithm(
handle: &str,
activity_key: &str,
algorithm: &str,
) -> Result<Value, JsValue> {
match algorithm {
"dfg" => {
let js_val = crate::discovery::discover_dfg(handle, activity_key)?;
serde_wasm_bindgen::from_value(js_val).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[cfg(feature = "discovery_advanced")]
"im" | "inductive" => {
let js_val = crate::more_discovery::discover_inductive_miner(handle, activity_key)?;
serde_wasm_bindgen::from_value(js_val).map_err(|e| JsValue::from_str(&e.to_string()))
}
_ => Err(JsValue::from_str(&format!(
"Unsupported algorithm in testing utility: {}",
algorithm
))),
}
}
fn blake3_hash(data: &str) -> String {
blake3::hash(data.as_bytes()).to_hex().to_string()
}
fn estimate_fitness(output: &Value) -> f64 {
if let Some(obj) = output.as_object() {
if obj.contains_key("fitness") {
if let Some(f) = obj["fitness"].as_f64() {
return f.clamp(0.0, 1.0);
}
}
if obj.contains_key("nodes") && obj.contains_key("edges") {
return 0.8;
}
}
0.5 }
fn estimate_precision(output: &Value) -> f64 {
if let Some(obj) = output.as_object() {
if obj.contains_key("precision") {
if let Some(p) = obj["precision"].as_f64() {
return p.clamp(0.0, 1.0);
}
}
if obj.contains_key("nodes") && obj.contains_key("edges") {
return 0.85;
}
}
0.5 }
fn extract_model_size(output: &Value) -> Value {
if let Some(obj) = output.as_object() {
let nodes = obj
.get("nodes")
.and_then(|v| v.as_array())
.map_or(0, |v| v.len());
let edges = obj
.get("edges")
.and_then(|v| v.as_array())
.map_or(0, |v| v.len());
let places = obj.get("places").and_then(|v| v.as_u64()).unwrap_or(0);
let transitions = obj.get("transitions").and_then(|v| v.as_u64()).unwrap_or(0);
return json!({
"nodes": nodes,
"edges": edges,
"places": places,
"transitions": transitions
});
}
json!({"nodes": 0, "edges": 0, "places": 0, "transitions": 0})
}
fn get_algorithm_schema_fields(algorithm: &str) -> (Vec<String>, Vec<String>) {
match algorithm {
"dfg" => (
vec!["nodes".to_string(), "edges".to_string()],
vec!["total_events".to_string()],
),
"petrinet" | "genetic_algorithm" | "ilp" | "alpha_plus_plus" => (
vec!["places".to_string(), "transitions".to_string()],
vec!["arcs".to_string()],
),
"process_tree" | "inductive_miner" => {
(vec!["tree".to_string()], vec!["children".to_string()])
}
_ => (vec![], vec![]),
}
}