briefcase-node 2.4.1

Node.js bindings for Briefcase AI
Documentation
//! Node.js bindings for replay functionality

use napi::{Result, bindgen_prelude::*, Env};
use briefcase_core::{ReplayEngine, ReplayMode, ReplayStatus, ReplayPolicy, ReplayResult, ReplayStats};
use crate::storage::{JsSqliteBackend, JsLakeFSBackend};

/// Node.js wrapper for ReplayEngine
#[napi]
pub struct JsReplayEngine {
    inner: ReplayEngine,
}

#[napi]
impl JsReplayEngine {
    #[napi(constructor)]
    pub fn new_with_sqlite(storage: &JsSqliteBackend) -> Self {
        Self {
            inner: ReplayEngine::new(storage.inner.clone()),
        }
    }

    #[napi(factory)]
    pub fn new_with_lakefs(storage: &JsLakeFSBackend) -> Self {
        Self {
            inner: ReplayEngine::new(storage.inner.clone()),
        }
    }

    #[napi]
    pub fn set_default_mode(&mut self, mode: String) -> Result<()> {
        let replay_mode = match mode.as_str() {
            "strict" => ReplayMode::Strict,
            "tolerant" => ReplayMode::Tolerant,
            "validation_only" => ReplayMode::ValidationOnly,
            _ => return Err(napi::Error::from_reason(
                format!("Invalid replay mode: {}. Use 'strict', 'tolerant', or 'validation_only'", mode)
            )),
        };
        self.inner.set_default_mode(replay_mode);
        Ok(())
    }

    #[napi]
    pub fn replay(&self, env: Env, snapshot_id: String, mode: Option<String>) -> Result<napi::JsObject> {
        let replay_mode = if let Some(mode_str) = mode {
            Some(match mode_str.as_str() {
                "strict" => ReplayMode::Strict,
                "tolerant" => ReplayMode::Tolerant,
                "validation_only" => ReplayMode::ValidationOnly,
                _ => return Err(napi::Error::from_reason(
                    format!("Invalid replay mode: {}", mode_str)
                )),
            })
        } else {
            None
        };

        let engine = self.inner.clone();
        let task = ReplayTask {
            engine,
            snapshot_id,
            replay_mode,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }

    #[napi]
    pub fn replay_with_policy(&self, env: Env, snapshot_id: String, policy: &JsReplayPolicy, mode: Option<String>) -> Result<napi::JsObject> {
        let replay_mode = if let Some(mode_str) = mode {
            Some(match mode_str.as_str() {
                "strict" => ReplayMode::Strict,
                "tolerant" => ReplayMode::Tolerant,
                "validation_only" => ReplayMode::ValidationOnly,
                _ => return Err(napi::Error::from_reason(
                    format!("Invalid replay mode: {}", mode_str)
                )),
            })
        } else {
            None
        };

        let engine = self.inner.clone();
        let policy_inner = policy.inner.clone();
        let task = ReplayWithPolicyTask {
            engine,
            snapshot_id,
            policy: policy_inner,
            replay_mode,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }
}

/// Node.js wrapper for ReplayPolicy
#[napi]
pub struct JsReplayPolicy {
    pub inner: ReplayPolicy,
}

#[napi]
impl JsReplayPolicy {
    #[napi(constructor)]
    pub fn new(name: String) -> Self {
        Self {
            inner: ReplayPolicy::new(name),
        }
    }

    #[napi]
    pub fn with_exact_match(&mut self, field: String) -> &Self {
        self.inner = self.inner.clone().with_exact_match(field);
        self
    }

    #[napi]
    pub fn with_similarity_threshold(&mut self, field: String, threshold: f64) -> &Self {
        self.inner = self.inner.clone().with_similarity_threshold(field, threshold);
        self
    }

    #[napi(getter)]
    pub fn name(&self) -> String {
        self.inner.name.clone()
    }

    #[napi(getter)]
    pub fn rule_count(&self) -> u32 {
        self.inner.rules.len() as u32
    }
}

/// Node.js wrapper for ReplayResult
#[napi]
pub struct JsReplayResult {
    inner: ReplayResult,
}

#[napi]
impl JsReplayResult {
    #[napi(getter)]
    pub fn status(&self) -> String {
        match self.inner.status {
            ReplayStatus::Success => "success".to_string(),
            ReplayStatus::Failed => "failed".to_string(),
            ReplayStatus::Partial => "partial".to_string(),
            ReplayStatus::Pending => "pending".to_string(),
            ReplayStatus::Running => "running".to_string(),
        }
    }

    #[napi(getter)]
    pub fn outputs_match(&self) -> bool {
        self.inner.outputs_match
    }

    #[napi(getter)]
    pub fn execution_time_ms(&self) -> f64 {
        self.inner.execution_time_ms
    }

    #[napi(getter)]
    pub fn replay_output(&self) -> Option<serde_json::Value> {
        self.inner.replay_output.clone()
    }

    #[napi]
    pub fn to_object(&self) -> Result<serde_json::Value> {
        Ok(serde_json::json!({
            "status": self.status(),
            "outputs_match": self.inner.outputs_match,
            "execution_time_ms": self.inner.execution_time_ms,
            "replay_output": self.inner.replay_output,
            "policy_violations": self.inner.policy_violations,
        }))
    }
}

// Task implementations for async operations

struct ReplayTask {
    engine: ReplayEngine,
    snapshot_id: String,
    replay_mode: Option<ReplayMode>,
}

#[napi]
impl napi::Task for ReplayTask {
    type Output = ReplayResult;
    type JsValue = JsReplayResult;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.engine.replay(&self.snapshot_id, self.replay_mode.clone(), None)
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(JsReplayResult { inner: output })
    }
}

struct ReplayWithPolicyTask {
    engine: ReplayEngine,
    snapshot_id: String,
    policy: ReplayPolicy,
    replay_mode: Option<ReplayMode>,
}

#[napi]
impl napi::Task for ReplayWithPolicyTask {
    type Output = ReplayResult;
    type JsValue = JsReplayResult;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.engine.replay_with_policy(&self.snapshot_id, &self.policy, self.replay_mode.clone())
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(JsReplayResult { inner: output })
    }
}