Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,506 @@
//! WASM bindings for EXO-AI 2025 Cognitive Substrate
//!
//! This module provides browser bindings for the EXO substrate, enabling:
//! - Pattern storage and retrieval
//! - Similarity search with various distance metrics
//! - Temporal memory coordination
//! - Causal queries
//! - Browser-based cognitive operations
use js_sys::{Array, Float32Array, Object, Promise, Reflect};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::{from_value, to_value};
use std::collections::HashMap;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
use web_sys::console;
mod types;
mod utils;
pub use types::*;
pub use utils::*;
/// Initialize panic hook and tracing for better error messages
#[wasm_bindgen(start)]
pub fn init() {
utils::set_panic_hook();
tracing_wasm::set_as_global_default();
}
/// WASM-specific error type that can cross the JS boundary
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExoError {
pub message: String,
pub kind: String,
}
impl ExoError {
pub fn new(message: impl Into<String>, kind: impl Into<String>) -> Self {
Self {
message: message.into(),
kind: kind.into(),
}
}
}
impl From<ExoError> for JsValue {
fn from(err: ExoError) -> Self {
let obj = Object::new();
Reflect::set(&obj, &"message".into(), &err.message.into()).unwrap();
Reflect::set(&obj, &"kind".into(), &err.kind.into()).unwrap();
obj.into()
}
}
impl From<String> for ExoError {
fn from(s: String) -> Self {
ExoError::new(s, "Error")
}
}
#[allow(dead_code)]
type ExoResult<T> = Result<T, ExoError>;
/// Configuration for EXO substrate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubstrateConfig {
/// Vector dimensions
pub dimensions: usize,
/// Distance metric (euclidean, cosine, dotproduct, manhattan)
#[serde(default = "default_metric")]
pub distance_metric: String,
/// Enable HNSW index for faster search
#[serde(default = "default_true")]
pub use_hnsw: bool,
/// Enable temporal memory coordination
#[serde(default = "default_true")]
pub enable_temporal: bool,
/// Enable causal tracking
#[serde(default = "default_true")]
pub enable_causal: bool,
}
fn default_metric() -> String {
"cosine".to_string()
}
fn default_true() -> bool {
true
}
/// Pattern representation in the cognitive substrate
#[wasm_bindgen]
#[derive(Clone)]
pub struct Pattern {
inner: PatternInner,
}
#[derive(Clone, Serialize, Deserialize)]
struct PatternInner {
/// Vector embedding
embedding: Vec<f32>,
/// Metadata (stored as HashMap to match ruvector-core)
metadata: Option<HashMap<String, serde_json::Value>>,
/// Temporal timestamp (milliseconds since epoch)
timestamp: f64,
/// Pattern ID
id: Option<String>,
/// Causal antecedents (IDs of patterns that influenced this one)
antecedents: Vec<String>,
}
#[wasm_bindgen]
impl Pattern {
#[wasm_bindgen(constructor)]
pub fn new(
embedding: Float32Array,
metadata: Option<JsValue>,
antecedents: Option<Vec<String>>,
) -> Result<Pattern, JsValue> {
let embedding_vec = embedding.to_vec();
if embedding_vec.is_empty() {
return Err(JsValue::from_str("Embedding cannot be empty"));
}
let metadata = if let Some(meta) = metadata {
let json_val: serde_json::Value = from_value(meta)
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
// Convert to HashMap if it's an object, otherwise wrap it
match json_val {
serde_json::Value::Object(map) => Some(map.into_iter().collect()),
other => {
let mut map = HashMap::new();
map.insert("value".to_string(), other);
Some(map)
}
}
} else {
None
};
Ok(Pattern {
inner: PatternInner {
embedding: embedding_vec,
metadata,
timestamp: js_sys::Date::now(),
id: None,
antecedents: antecedents.unwrap_or_default(),
},
})
}
#[wasm_bindgen(getter)]
pub fn id(&self) -> Option<String> {
self.inner.id.clone()
}
#[wasm_bindgen(getter)]
pub fn embedding(&self) -> Float32Array {
Float32Array::from(&self.inner.embedding[..])
}
#[wasm_bindgen(getter)]
pub fn metadata(&self) -> Option<JsValue> {
self.inner.metadata.as_ref().map(|m| {
let json_val = serde_json::Value::Object(m.clone().into_iter().collect());
to_value(&json_val).unwrap()
})
}
#[wasm_bindgen(getter)]
pub fn timestamp(&self) -> f64 {
self.inner.timestamp
}
#[wasm_bindgen(getter)]
pub fn antecedents(&self) -> Vec<String> {
self.inner.antecedents.clone()
}
}
/// Search result from substrate query
#[wasm_bindgen]
pub struct SearchResult {
inner: SearchResultInner,
}
#[derive(Clone, Serialize, Deserialize)]
struct SearchResultInner {
id: String,
score: f32,
pattern: Option<PatternInner>,
}
#[wasm_bindgen]
impl SearchResult {
#[wasm_bindgen(getter)]
pub fn id(&self) -> String {
self.inner.id.clone()
}
#[wasm_bindgen(getter)]
pub fn score(&self) -> f32 {
self.inner.score
}
#[wasm_bindgen(getter)]
pub fn pattern(&self) -> Option<Pattern> {
self.inner.pattern.clone().map(|p| Pattern { inner: p })
}
}
/// Main EXO substrate interface for browser deployment
#[wasm_bindgen]
pub struct ExoSubstrate {
// Using ruvector-core as placeholder until exo-core is implemented
db: Arc<Mutex<ruvector_core::vector_db::VectorDB>>,
config: SubstrateConfig,
dimensions: usize,
}
#[wasm_bindgen]
impl ExoSubstrate {
/// Create a new EXO substrate instance
///
/// # Arguments
/// * `config` - Configuration object with dimensions, distance_metric, etc.
///
/// # Example
/// ```javascript
/// const substrate = new ExoSubstrate({
/// dimensions: 384,
/// distance_metric: "cosine",
/// use_hnsw: true,
/// enable_temporal: true,
/// enable_causal: true
/// });
/// ```
#[wasm_bindgen(constructor)]
pub fn new(config: JsValue) -> Result<ExoSubstrate, JsValue> {
let config: SubstrateConfig =
from_value(config).map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
// Validate configuration
if config.dimensions == 0 {
return Err(JsValue::from_str("Dimensions must be greater than 0"));
}
// Create underlying vector database
let distance_metric = match config.distance_metric.as_str() {
"euclidean" => ruvector_core::types::DistanceMetric::Euclidean,
"cosine" => ruvector_core::types::DistanceMetric::Cosine,
"dotproduct" => ruvector_core::types::DistanceMetric::DotProduct,
"manhattan" => ruvector_core::types::DistanceMetric::Manhattan,
_ => {
return Err(JsValue::from_str(&format!(
"Unknown distance metric: {}",
config.distance_metric
)))
}
};
let hnsw_config = if config.use_hnsw {
Some(ruvector_core::types::HnswConfig::default())
} else {
None
};
let db_options = ruvector_core::types::DbOptions {
dimensions: config.dimensions,
distance_metric,
storage_path: ":memory:".to_string(), // WASM uses in-memory storage
hnsw_config,
quantization: None,
};
let db = ruvector_core::vector_db::VectorDB::new(db_options)
.map_err(|e| JsValue::from_str(&format!("Failed to create substrate: {}", e)))?;
console::log_1(
&format!(
"EXO substrate initialized with {} dimensions",
config.dimensions
)
.into(),
);
Ok(ExoSubstrate {
db: Arc::new(Mutex::new(db)),
dimensions: config.dimensions,
config,
})
}
/// Store a pattern in the substrate
///
/// # Arguments
/// * `pattern` - Pattern object with embedding, metadata, and optional antecedents
///
/// # Returns
/// Pattern ID as a string
#[wasm_bindgen]
pub fn store(&self, pattern: &Pattern) -> Result<String, JsValue> {
if pattern.inner.embedding.len() != self.dimensions {
return Err(JsValue::from_str(&format!(
"Pattern embedding dimension mismatch: expected {}, got {}",
self.dimensions,
pattern.inner.embedding.len()
)));
}
let entry = ruvector_core::types::VectorEntry {
id: pattern.inner.id.clone(),
vector: pattern.inner.embedding.clone(),
metadata: pattern.inner.metadata.clone(),
};
let db = self.db.lock();
let id = db
.insert(entry)
.map_err(|e| JsValue::from_str(&format!("Failed to store pattern: {}", e)))?;
console::log_1(&format!("Pattern stored with ID: {}", id).into());
Ok(id)
}
/// Query the substrate for similar patterns
///
/// # Arguments
/// * `embedding` - Query embedding as Float32Array
/// * `k` - Number of results to return
///
/// # Returns
/// Promise that resolves to an array of SearchResult objects
#[wasm_bindgen]
pub fn query(&self, embedding: Float32Array, k: u32) -> Result<Promise, JsValue> {
let query_vec = embedding.to_vec();
if query_vec.len() != self.dimensions {
return Err(JsValue::from_str(&format!(
"Query embedding dimension mismatch: expected {}, got {}",
self.dimensions,
query_vec.len()
)));
}
let db = self.db.clone();
let promise = future_to_promise(async move {
let search_query = ruvector_core::types::SearchQuery {
vector: query_vec,
k: k as usize,
filter: None,
ef_search: None,
};
let db_guard = db.lock();
let results = db_guard
.search(search_query)
.map_err(|e| JsValue::from_str(&format!("Search failed: {}", e)))?;
drop(db_guard);
let js_results: Vec<JsValue> = results
.into_iter()
.map(|r| {
let result = SearchResult {
inner: SearchResultInner {
id: r.id,
score: r.score,
pattern: None, // Can be populated if needed
},
};
to_value(&result.inner).unwrap()
})
.collect();
Ok(Array::from_iter(js_results).into())
});
Ok(promise)
}
/// Get substrate statistics
///
/// # Returns
/// Object with substrate statistics
#[wasm_bindgen]
pub fn stats(&self) -> Result<JsValue, JsValue> {
let db = self.db.lock();
let count = db
.len()
.map_err(|e| JsValue::from_str(&format!("Failed to get stats: {}", e)))?;
let stats = serde_json::json!({
"dimensions": self.dimensions,
"pattern_count": count,
"distance_metric": self.config.distance_metric,
"temporal_enabled": self.config.enable_temporal,
"causal_enabled": self.config.enable_causal,
});
to_value(&stats)
.map_err(|e| JsValue::from_str(&format!("Failed to serialize stats: {}", e)))
}
/// Get a pattern by ID
///
/// # Arguments
/// * `id` - Pattern ID
///
/// # Returns
/// Pattern object or null if not found
#[wasm_bindgen]
pub fn get(&self, id: &str) -> Result<Option<Pattern>, JsValue> {
let db = self.db.lock();
let entry = db
.get(id)
.map_err(|e| JsValue::from_str(&format!("Failed to get pattern: {}", e)))?;
Ok(entry.map(|e| Pattern {
inner: PatternInner {
embedding: e.vector,
metadata: e.metadata,
timestamp: js_sys::Date::now(),
id: e.id,
antecedents: vec![],
},
}))
}
/// Delete a pattern by ID
///
/// # Arguments
/// * `id` - Pattern ID to delete
///
/// # Returns
/// True if deleted, false if not found
#[wasm_bindgen]
pub fn delete(&self, id: &str) -> Result<bool, JsValue> {
let db = self.db.lock();
db.delete(id)
.map_err(|e| JsValue::from_str(&format!("Failed to delete pattern: {}", e)))
}
/// Get the number of patterns in the substrate
#[wasm_bindgen]
pub fn len(&self) -> Result<usize, JsValue> {
let db = self.db.lock();
db.len()
.map_err(|e| JsValue::from_str(&format!("Failed to get length: {}", e)))
}
/// Check if the substrate is empty
#[wasm_bindgen(js_name = isEmpty)]
pub fn is_empty(&self) -> Result<bool, JsValue> {
let db = self.db.lock();
db.is_empty()
.map_err(|e| JsValue::from_str(&format!("Failed to check if empty: {}", e)))
}
/// Get substrate dimensions
#[wasm_bindgen(getter)]
pub fn dimensions(&self) -> usize {
self.dimensions
}
}
/// Get version information
#[wasm_bindgen]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
/// Detect SIMD support in the current environment
#[wasm_bindgen(js_name = detectSIMD)]
pub fn detect_simd() -> bool {
#[cfg(target_feature = "simd128")]
{
true
}
#[cfg(not(target_feature = "simd128"))]
{
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_version() {
assert!(!version().is_empty());
}
#[wasm_bindgen_test]
fn test_detect_simd() {
let _ = detect_simd();
}
}

View File

@@ -0,0 +1,177 @@
//! Type conversions for JavaScript interoperability
//!
//! This module provides type conversions between Rust and JavaScript types
//! for seamless WASM integration.
use js_sys::{Array, Float32Array, Object, Reflect};
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
/// JavaScript-compatible query configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryConfig {
/// Query vector (will be converted from Float32Array)
pub embedding: Vec<f32>,
/// Number of results to return
pub k: usize,
/// Optional metadata filter
pub filter: Option<serde_json::Value>,
/// Optional ef_search parameter for HNSW
pub ef_search: Option<usize>,
}
/// Causal cone type for temporal queries
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CausalConeType {
/// Past light cone (all events that could have influenced this point)
Past,
/// Future light cone (all events this point could influence)
Future,
/// Custom light cone with specified velocity
LightCone,
}
/// Causal query configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CausalQueryConfig {
/// Base query configuration
pub query: QueryConfig,
/// Reference timestamp (milliseconds since epoch)
pub reference_time: f64,
/// Cone type
pub cone_type: CausalConeType,
/// Optional velocity parameter for light cone queries (in ms^-1)
pub velocity: Option<f32>,
}
/// Topological query types for advanced substrate operations
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TopologicalQuery {
/// Find persistent homology features
PersistentHomology {
dimension: usize,
epsilon_min: f32,
epsilon_max: f32,
},
/// Compute Betti numbers (topological invariants)
BettiNumbers { max_dimension: usize },
/// Check sheaf consistency
SheafConsistency { section_ids: Vec<String> },
}
/// Result from causal query
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CausalResult {
/// Pattern ID
pub id: String,
/// Similarity score
pub score: f32,
/// Causal distance (number of hops in causal graph)
pub causal_distance: Option<usize>,
/// Temporal distance (milliseconds)
pub temporal_distance: f64,
/// Optional pattern data
pub pattern: Option<serde_json::Value>,
}
/// Convert JavaScript array to Rust Vec<f32>
pub fn js_array_to_vec_f32(arr: &Array) -> Result<Vec<f32>, JsValue> {
let mut vec = Vec::with_capacity(arr.length() as usize);
for i in 0..arr.length() {
let val = arr.get(i);
if let Some(num) = val.as_f64() {
vec.push(num as f32);
} else {
return Err(JsValue::from_str(&format!(
"Array element at index {} is not a number",
i
)));
}
}
Ok(vec)
}
/// Convert Rust Vec<f32> to JavaScript Float32Array
pub fn vec_f32_to_js_array(vec: &[f32]) -> Float32Array {
Float32Array::from(vec)
}
/// Convert JavaScript object to JSON value
pub fn js_object_to_json(obj: &JsValue) -> Result<serde_json::Value, JsValue> {
serde_wasm_bindgen::from_value(obj.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to convert to JSON: {}", e)))
}
/// Convert JSON value to JavaScript object
pub fn json_to_js_object(value: &serde_json::Value) -> Result<JsValue, JsValue> {
serde_wasm_bindgen::to_value(value)
.map_err(|e| JsValue::from_str(&format!("Failed to convert from JSON: {}", e)))
}
/// Helper to create JavaScript error objects
pub fn create_js_error(message: &str, kind: &str) -> JsValue {
let obj = Object::new();
Reflect::set(&obj, &"message".into(), &message.into()).unwrap();
Reflect::set(&obj, &"kind".into(), &kind.into()).unwrap();
Reflect::set(&obj, &"name".into(), &"ExoError".into()).unwrap();
obj.into()
}
/// Helper to validate vector dimensions
pub fn validate_dimensions(vec: &[f32], expected: usize) -> Result<(), JsValue> {
if vec.len() != expected {
return Err(create_js_error(
&format!(
"Dimension mismatch: expected {}, got {}",
expected,
vec.len()
),
"DimensionError",
));
}
Ok(())
}
/// Helper to validate vector is not empty
pub fn validate_not_empty(vec: &[f32]) -> Result<(), JsValue> {
if vec.is_empty() {
return Err(create_js_error("Vector cannot be empty", "ValidationError"));
}
Ok(())
}
/// Helper to validate k parameter
pub fn validate_k(k: usize) -> Result<(), JsValue> {
if k == 0 {
return Err(create_js_error(
"k must be greater than 0",
"ValidationError",
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_causal_cone_type_serialization() {
let cone = CausalConeType::Past;
let json = serde_json::to_string(&cone).unwrap();
assert_eq!(json, "\"past\"");
}
#[test]
fn test_topological_query_serialization() {
let query = TopologicalQuery::PersistentHomology {
dimension: 2,
epsilon_min: 0.1,
epsilon_max: 1.0,
};
let json = serde_json::to_value(&query).unwrap();
assert_eq!(json["type"], "persistent_homology");
}
}

View File

@@ -0,0 +1,180 @@
//! Utility functions for WASM runtime
//!
//! This module provides utility functions for panic handling, logging,
//! and browser environment detection.
use wasm_bindgen::prelude::*;
use web_sys::console;
/// Set up panic hook for better error messages in browser console
pub fn set_panic_hook() {
console_error_panic_hook::set_once();
}
/// Log a message to the browser console
pub fn log(message: &str) {
console::log_1(&JsValue::from_str(message));
}
/// Log a warning to the browser console
pub fn warn(message: &str) {
console::warn_1(&JsValue::from_str(message));
}
/// Log an error to the browser console
pub fn error(message: &str) {
console::error_1(&JsValue::from_str(message));
}
/// Log debug information (includes timing)
pub fn debug(message: &str) {
console::debug_1(&JsValue::from_str(message));
}
/// Measure execution time of a function
pub fn measure_time<F, R>(name: &str, f: F) -> R
where
F: FnOnce() -> R,
{
let start = js_sys::Date::now();
let result = f();
let elapsed = js_sys::Date::now() - start;
log(&format!("{} took {:.2}ms", name, elapsed));
result
}
/// Check if running in a Web Worker context
#[wasm_bindgen]
pub fn is_web_worker() -> bool {
js_sys::eval("typeof WorkerGlobalScope !== 'undefined'")
.map(|v| v.is_truthy())
.unwrap_or(false)
}
/// Check if running in a browser with WebAssembly support
#[wasm_bindgen]
pub fn is_wasm_supported() -> bool {
js_sys::eval("typeof WebAssembly !== 'undefined'")
.map(|v| v.is_truthy())
.unwrap_or(false)
}
/// Get browser performance metrics
#[wasm_bindgen]
pub fn get_performance_metrics() -> Result<JsValue, JsValue> {
let window = web_sys::window().ok_or_else(|| JsValue::from_str("No window object"))?;
let performance = window
.performance()
.ok_or_else(|| JsValue::from_str("No performance object"))?;
let timing = performance.timing();
let metrics = serde_json::json!({
"navigation_start": timing.navigation_start(),
"dom_complete": timing.dom_complete(),
"load_event_end": timing.load_event_end(),
});
serde_wasm_bindgen::to_value(&metrics)
.map_err(|e| JsValue::from_str(&format!("Failed to serialize metrics: {}", e)))
}
/// Get available memory (if supported by browser)
#[wasm_bindgen]
pub fn get_memory_info() -> Result<JsValue, JsValue> {
// Try to access performance.memory (Chrome only)
let window = web_sys::window().ok_or_else(|| JsValue::from_str("No window object"))?;
let performance = window
.performance()
.ok_or_else(|| JsValue::from_str("No performance object"))?;
// This is non-standard and may not be available
let result = js_sys::Reflect::get(&performance, &JsValue::from_str("memory"));
if let Ok(memory) = result {
if !memory.is_undefined() {
return Ok(memory);
}
}
// Fallback: return empty object
Ok(js_sys::Object::new().into())
}
/// Format bytes to human-readable string
pub fn format_bytes(bytes: f64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = bytes;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
format!("{:.2} {}", size, UNITS[unit_index])
}
/// Generate a random UUID v4
#[wasm_bindgen]
pub fn generate_uuid() -> String {
// Use crypto.randomUUID if available, otherwise fallback
let result = js_sys::eval(
"typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : null"
);
if let Ok(uuid) = result {
if let Some(uuid_str) = uuid.as_string() {
return uuid_str;
}
}
// Fallback: simple UUID generation
use getrandom::getrandom;
let mut bytes = [0u8; 16];
if getrandom(&mut bytes).is_ok() {
// Set version (4) and variant bits
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5],
bytes[6], bytes[7],
bytes[8], bytes[9],
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
)
} else {
// Ultimate fallback: timestamp-based ID
format!("{}-{}", js_sys::Date::now(), js_sys::Math::random())
}
}
/// Check if localStorage is available
#[wasm_bindgen]
pub fn is_local_storage_available() -> bool {
js_sys::eval("typeof localStorage !== 'undefined'")
.map(|v| v.is_truthy())
.unwrap_or(false)
}
/// Check if IndexedDB is available
#[wasm_bindgen]
pub fn is_indexed_db_available() -> bool {
js_sys::eval("typeof indexedDB !== 'undefined'")
.map(|v| v.is_truthy())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(100.0), "100.00 B");
assert_eq!(format_bytes(1024.0), "1.00 KB");
assert_eq!(format_bytes(1024.0 * 1024.0), "1.00 MB");
}
}