use briefcase_core::{ConsensusConfidence, DriftCalculator, DriftMetrics, DriftStatus};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct WasmDriftCalculator {
inner: DriftCalculator,
}
impl Default for WasmDriftCalculator {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmDriftCalculator {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmDriftCalculator {
WasmDriftCalculator {
inner: DriftCalculator::new(),
}
}
#[wasm_bindgen(js_name = withThreshold)]
pub fn with_threshold(threshold: f64) -> WasmDriftCalculator {
WasmDriftCalculator {
inner: DriftCalculator::with_threshold(threshold),
}
}
#[wasm_bindgen(js_name = calculateDrift)]
pub fn calculate_drift(&self, outputs: &JsValue) -> Result<WasmDriftMetrics, JsValue> {
let outputs_array: js_sys::Array = outputs
.clone()
.dyn_into()
.map_err(|_| JsValue::from_str("Expected an array of strings"))?;
let mut output_strings = Vec::new();
for i in 0..outputs_array.length() {
let item = outputs_array.get(i);
let string_value = item
.as_string()
.ok_or_else(|| JsValue::from_str("All items must be strings"))?;
output_strings.push(string_value);
}
let metrics = self.inner.calculate_drift(&output_strings);
Ok(WasmDriftMetrics { inner: metrics })
}
#[wasm_bindgen(js_name = calculateDriftFromOutputs)]
pub fn calculate_drift_from_outputs(
&self,
outputs: &JsValue,
) -> Result<WasmDriftMetrics, JsValue> {
let outputs_array: js_sys::Array = outputs
.clone()
.dyn_into()
.map_err(|_| JsValue::from_str("Expected an array of output objects"))?;
let mut core_outputs = Vec::new();
for i in 0..outputs_array.length() {
let item = outputs_array.get(i);
let output_value: serde_json::Value = serde_wasm_bindgen::from_value(item)
.map_err(|e| JsValue::from_str(&format!("Failed to parse output: {}", e)))?;
if let Some(obj) = output_value.as_object() {
let name = obj
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("output")
.to_string();
let value = obj.get("value").cloned().unwrap_or(serde_json::Value::Null);
let data_type = obj
.get("data_type")
.and_then(|v| v.as_str())
.unwrap_or("string")
.to_string();
let output = briefcase_core::Output::new(name, value, data_type);
core_outputs.push(output);
}
}
let metrics = self.inner.calculate_drift_from_outputs(&core_outputs);
Ok(WasmDriftMetrics { inner: metrics })
}
#[wasm_bindgen(js_name = getStatus)]
pub fn get_status(&self, metrics: &WasmDriftMetrics) -> String {
match self.inner.get_status(&metrics.inner) {
DriftStatus::Stable => "stable".to_string(),
DriftStatus::Drifting => "drifting".to_string(),
DriftStatus::Critical => "critical".to_string(),
}
}
}
#[wasm_bindgen]
pub struct WasmDriftMetrics {
inner: DriftMetrics,
}
#[wasm_bindgen]
impl WasmDriftMetrics {
#[wasm_bindgen(getter)]
pub fn consistency_score(&self) -> f64 {
self.inner.consistency_score
}
#[wasm_bindgen(getter)]
pub fn agreement_rate(&self) -> f64 {
self.inner.agreement_rate
}
#[wasm_bindgen(getter)]
pub fn drift_score(&self) -> f64 {
self.inner.drift_score
}
#[wasm_bindgen(getter)]
pub fn consensus_output(&self) -> Option<String> {
self.inner.consensus_output.clone()
}
#[wasm_bindgen(getter)]
pub fn consensus_confidence(&self) -> String {
match self.inner.consensus_confidence {
ConsensusConfidence::High => "high".to_string(),
ConsensusConfidence::Medium => "medium".to_string(),
ConsensusConfidence::Low => "low".to_string(),
ConsensusConfidence::None => "none".to_string(),
}
}
#[wasm_bindgen(getter)]
pub fn outliers(&self) -> Result<JsValue, JsValue> {
let outliers: Vec<u32> = self.inner.outliers.iter().map(|&i| i as u32).collect();
serde_wasm_bindgen::to_value(&outliers)
.map_err(|e| JsValue::from_str(&format!("Conversion error: {}", e)))
}
#[wasm_bindgen(js_name = toObject)]
pub fn to_object(&self) -> Result<JsValue, JsValue> {
let obj = serde_json::json!({
"consistency_score": self.inner.consistency_score,
"agreement_rate": self.inner.agreement_rate,
"drift_score": self.inner.drift_score,
"consensus_output": self.inner.consensus_output,
"consensus_confidence": self.consensus_confidence(),
"outliers": self.inner.outliers,
});
serde_wasm_bindgen::to_value(&obj)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
}
#[cfg(test)]
mod tests {
use briefcase_core::{ConsensusConfidence, DriftCalculator, DriftStatus, Output};
#[test]
fn test_drift_all_identical_outputs() {
let calc = DriftCalculator::new();
let outputs = vec!["positive".to_string(); 10];
let metrics = calc.calculate_drift(&outputs);
assert!((metrics.consistency_score - 1.0).abs() < 0.01);
assert!((metrics.agreement_rate - 1.0).abs() < 0.01);
assert!(metrics.drift_score < 0.1);
assert_eq!(metrics.consensus_output, Some("positive".to_string()));
assert!(matches!(
metrics.consensus_confidence,
ConsensusConfidence::High
));
assert!(metrics.outliers.is_empty());
}
#[test]
fn test_drift_all_different_outputs() {
let calc = DriftCalculator::new();
let outputs: Vec<String> = (0..10).map(|i| format!("output_{}", i)).collect();
let metrics = calc.calculate_drift(&outputs);
assert!(metrics.consistency_score < 1.0);
assert!(metrics.drift_score > 0.0);
}
#[test]
fn test_drift_majority_agreement() {
let calc = DriftCalculator::new();
let mut outputs = vec!["positive".to_string(); 8];
outputs.push("negative".to_string());
outputs.push("neutral".to_string());
let metrics = calc.calculate_drift(&outputs);
assert!(metrics.agreement_rate > 0.5);
assert_eq!(metrics.consensus_output, Some("positive".to_string()));
}
#[test]
fn test_drift_custom_threshold() {
let calc = DriftCalculator::with_threshold(0.3);
let outputs = vec!["a".to_string(), "a".to_string(), "b".to_string()];
let metrics = calc.calculate_drift(&outputs);
let status = calc.get_status(&metrics);
assert!(matches!(
status,
DriftStatus::Stable | DriftStatus::Drifting | DriftStatus::Critical
));
}
#[test]
fn test_drift_single_output() {
let calc = DriftCalculator::new();
let outputs = vec!["only_one".to_string()];
let metrics = calc.calculate_drift(&outputs);
assert!((metrics.consistency_score - 1.0).abs() < 0.01);
assert_eq!(metrics.consensus_output, Some("only_one".to_string()));
}
#[test]
fn test_drift_empty_outputs() {
let calc = DriftCalculator::new();
let outputs: Vec<String> = vec![];
let metrics = calc.calculate_drift(&outputs);
assert!(metrics.consistency_score >= 0.0);
}
#[test]
fn test_drift_status_stable() {
let calc = DriftCalculator::new();
let outputs = vec!["same".to_string(); 20];
let metrics = calc.calculate_drift(&outputs);
let status = calc.get_status(&metrics);
assert!(matches!(status, DriftStatus::Stable));
}
#[test]
fn test_drift_from_outputs() {
let calc = DriftCalculator::new();
let outputs: Vec<Output> = (0..5)
.map(|_| Output::new("sentiment", serde_json::json!("positive"), "string"))
.collect();
let metrics = calc.calculate_drift_from_outputs(&outputs);
assert!(metrics.consistency_score > 0.5);
}
#[test]
fn test_drift_from_mixed_outputs() {
let calc = DriftCalculator::new();
let outputs = vec![
Output::new("sentiment", serde_json::json!("positive"), "string"),
Output::new("sentiment", serde_json::json!("negative"), "string"),
Output::new("sentiment", serde_json::json!("positive"), "string"),
Output::new("sentiment", serde_json::json!("neutral"), "string"),
Output::new("sentiment", serde_json::json!("positive"), "string"),
];
let metrics = calc.calculate_drift_from_outputs(&outputs);
assert!(metrics.drift_score > 0.0);
assert!(!metrics.outliers.is_empty() || metrics.agreement_rate < 1.0);
}
#[test]
fn test_drift_outlier_detection() {
let calc = DriftCalculator::new();
let mut outputs = vec!["consensus".to_string(); 9];
outputs.push("outlier".to_string());
let metrics = calc.calculate_drift(&outputs);
assert!(!metrics.outliers.is_empty() || metrics.agreement_rate < 1.0);
}
}