diff --git a/Cargo.toml b/Cargo.toml index a43d3942..df22071d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,6 +149,7 @@ members = [ "examples/climate-consciousness", # JS bundle decompiler (ADR-135) "crates/ruvector-decompiler", + "crates/ruvector-decompiler-wasm", ] resolver = "2" diff --git a/crates/ruvector-decompiler-wasm/Cargo.toml b/crates/ruvector-decompiler-wasm/Cargo.toml new file mode 100644 index 00000000..1246df8a --- /dev/null +++ b/crates/ruvector-decompiler-wasm/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ruvector-decompiler-wasm" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +description = "WASM bindings for the RuVector JavaScript bundle decompiler (Louvain pipeline)" +keywords = ["decompiler", "javascript", "wasm", "mincut", "louvain"] +categories = ["wasm", "development-tools"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +ruvector-decompiler = { path = "../ruvector-decompiler", default-features = false, features = ["wasm"] } +wasm-bindgen = { workspace = true } +serde-wasm-bindgen = "0.6" +serde_json = { workspace = true } +console_error_panic_hook = "0.1" +getrandom = { version = "0.2", features = ["js"] } + +[profile.release] +opt-level = "s" +lto = true + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false diff --git a/crates/ruvector-decompiler-wasm/src/lib.rs b/crates/ruvector-decompiler-wasm/src/lib.rs new file mode 100644 index 00000000..810df866 --- /dev/null +++ b/crates/ruvector-decompiler-wasm/src/lib.rs @@ -0,0 +1,49 @@ +//! WASM bindings for the RuVector JavaScript bundle decompiler. +//! +//! Exposes the full Louvain graph-partitioning decompiler pipeline +//! (parse -> graph -> partition -> infer -> witness) to Node.js / browser. +//! +//! ## Usage from Node.js +//! +//! ```javascript +//! const wasm = require('./ruvector_decompiler_wasm'); +//! const result = JSON.parse(wasm.decompile(source, '{}')); +//! console.log(result.modules.length); +//! ``` + +use wasm_bindgen::prelude::*; + +/// Initialize the WASM module (sets up panic hook for better error messages). +#[wasm_bindgen(start)] +pub fn init() { + console_error_panic_hook::set_once(); +} + +/// Decompile a minified JavaScript bundle using the full Louvain pipeline. +/// +/// # Arguments +/// +/// * `source` - The minified JavaScript source code. +/// * `config_json` - JSON string of `DecompileConfig` fields. Pass `"{}"` for defaults. +/// +/// # Returns +/// +/// A JSON string containing the `DecompileResult` (modules, witness, inferred names, etc.) +/// or a JSON object with an `"error"` field on failure. +#[wasm_bindgen] +pub fn decompile(source: &str, config_json: &str) -> String { + let config: ruvector_decompiler::DecompileConfig = + serde_json::from_str(config_json).unwrap_or_default(); + match ruvector_decompiler::decompile(source, &config) { + Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| { + serde_json::json!({"error": format!("serialization failed: {}", e)}).to_string() + }), + Err(e) => serde_json::json!({"error": e.to_string()}).to_string(), + } +} + +/// Return the version of the decompiler WASM module. +#[wasm_bindgen] +pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_string() +} diff --git a/crates/ruvector-decompiler/Cargo.toml b/crates/ruvector-decompiler/Cargo.toml index 273ceb23..8b345653 100644 --- a/crates/ruvector-decompiler/Cargo.toml +++ b/crates/ruvector-decompiler/Cargo.toml @@ -18,13 +18,17 @@ serde_json = { workspace = true } regex = "1" thiserror = { workspace = true } once_cell = "1" -rayon = { workspace = true } +rayon = { workspace = true, optional = true } memchr = "2" ort = { version = "=2.0.0-rc.10", optional = true, default-features = false, features = ["ndarray", "std"] } ndarray = { version = "0.16", optional = true } [features] -default = [] +default = ["parallel"] +# Parallel Louvain via rayon (not available in WASM) +parallel = ["rayon"] +# WASM compatibility: pass through to ruvector-mincut wasm feature +wasm = ["ruvector-mincut/wasm"] # Enable neural name inference using ONNX Runtime (via `ort` crate) # or a GGUF/RVF model file. Adds model loading + inference capability. neural = ["ort", "ndarray"] diff --git a/crates/ruvector-decompiler/src/partitioner.rs b/crates/ruvector-decompiler/src/partitioner.rs index c5268075..d8dfb2b1 100644 --- a/crates/ruvector-decompiler/src/partitioner.rs +++ b/crates/ruvector-decompiler/src/partitioner.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; +#[cfg(feature = "parallel")] use rayon::prelude::*; use crate::error::{DecompilerError, Result}; @@ -164,8 +165,12 @@ fn louvain_partition( // Parallel phase: compute best community for each node. // Each node reads community[] and sigma_totals[] (snapshot). // No writes during this phase. - let gains: Vec<(usize, usize, f64)> = (0..n) - .into_par_iter() + #[cfg(feature = "parallel")] + let iter = (0..n).into_par_iter(); + #[cfg(not(feature = "parallel"))] + let iter = (0..n).into_iter(); + + let gains: Vec<(usize, usize, f64)> = iter .filter_map(|i| { let current_comm = community[i]; let ki = node_weights[i]; diff --git a/crates/ruvector-decompiler/src/types.rs b/crates/ruvector-decompiler/src/types.rs index 02148ed2..2af4cdbc 100644 --- a/crates/ruvector-decompiler/src/types.rs +++ b/crates/ruvector-decompiler/src/types.rs @@ -158,7 +158,7 @@ pub struct ModuleWitnessData { } /// Configuration for the decompiler pipeline. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct DecompileConfig { /// Target number of modules to reconstruct. If `None`, auto-detect. pub target_modules: Option, diff --git a/npm/packages/ruvector/package.json b/npm/packages/ruvector/package.json index b98be8a4..742e0640 100644 --- a/npm/packages/ruvector/package.json +++ b/npm/packages/ruvector/package.json @@ -1,6 +1,6 @@ { "name": "ruvector", - "version": "0.2.19", + "version": "0.2.20", "description": "Self-learning vector database for Node.js — hybrid search, Graph RAG, FlashAttention-3, DiskANN, 50+ attention mechanisms", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -90,6 +90,7 @@ "bin/", "dist/", "src/decompiler/", + "wasm/", "README.md", "LICENSE" ], diff --git a/npm/packages/ruvector/src/decompiler/index.js b/npm/packages/ruvector/src/decompiler/index.js index 1acc818c..404d38dd 100644 --- a/npm/packages/ruvector/src/decompiler/index.js +++ b/npm/packages/ruvector/src/decompiler/index.js @@ -27,6 +27,55 @@ const { computeMetrics, computeModuleMetrics } = require('./metrics'); const { reconstructCode, reconstructRunnable } = require('./reconstructor'); const { validateReconstruction } = require('./validator'); +/** + * Try the WASM Louvain decompiler (full graph-partitioning pipeline). + * Returns null if WASM module is not available or fails. + * + * @param {string} source - raw JavaScript source + * @param {object} [options] + * @returns {{modules: object[], metrics: object, witness: object|null}|null} + */ +function tryWasmDecompiler(source, options = {}) { + try { + const wasm = require('../../wasm/ruvector_decompiler_wasm'); + const configJson = JSON.stringify({ + target_modules: null, + min_confidence: options.minConfidence || 0.3, + generate_source_maps: false, + generate_witness: options.witness !== false, + output_filename: 'bundle.js', + model_path: null, + hierarchical_output: true, + max_depth: 3, + min_folder_size: 3, + }); + const resultJson = wasm.decompile(source, configJson); + const result = JSON.parse(resultJson); + if (result.error) return null; + + // Convert Rust DecompileResult to Node.js format + return { + modules: (result.modules || []).map((m) => ({ + name: m.name, + content: m.source || '', + declarations: (m.declarations && m.declarations.length) || 0, + fragments: (m.declarations && m.declarations.length) || 0, + confidence: 0.8, + })), + metrics: { + source: { sizeBytes: source.length }, + modules: (result.modules || []).length, + engine: 'wasm-louvain', + }, + witness: result.witness || null, + moduleTree: result.module_tree || null, + beautifiedSource: source, + }; + } catch { + return null; // WASM not available, fall back + } +} + /** * Try to beautify source code using js-beautify (optional dep). * Falls back to returning the source unchanged if not installed. @@ -118,7 +167,13 @@ function decompileSource(source, options = {}) { filePath, } = options; - // Try Rust Louvain pipeline first (878+ modules, 100% parse rate) + // Priority 1: WASM Louvain (full pipeline, works everywhere, no binary needed) + if (useRust !== false && source.length > 1000) { + const wasmResult = tryWasmDecompiler(source, options); + if (wasmResult) return wasmResult; + } + + // Priority 2: Rust binary (full pipeline, requires cargo build) if (useRust && filePath && source.length > 100000) { const tmpDir = path.join(require('os').tmpdir(), 'ruvector-decompile-' + Date.now()); const rustResult = tryRustDecompiler(filePath, tmpDir); @@ -404,4 +459,5 @@ module.exports = { reconstructCode, reconstructRunnable, validateReconstruction, + tryWasmDecompiler, }; diff --git a/npm/packages/ruvector/wasm/package.json b/npm/packages/ruvector/wasm/package.json new file mode 100644 index 00000000..669273aa --- /dev/null +++ b/npm/packages/ruvector/wasm/package.json @@ -0,0 +1,27 @@ +{ + "name": "ruvector-decompiler-wasm", + "collaborators": [ + "Ruvector Team" + ], + "description": "WASM bindings for the RuVector JavaScript bundle decompiler (Louvain pipeline)", + "version": "2.1.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/ruvnet/ruvector" + }, + "files": [ + "ruvector_decompiler_wasm_bg.wasm", + "ruvector_decompiler_wasm.js", + "ruvector_decompiler_wasm.d.ts" + ], + "main": "ruvector_decompiler_wasm.js", + "types": "ruvector_decompiler_wasm.d.ts", + "keywords": [ + "decompiler", + "javascript", + "wasm", + "mincut", + "louvain" + ] +} \ No newline at end of file diff --git a/npm/packages/ruvector/wasm/ruvector_decompiler_wasm.d.ts b/npm/packages/ruvector/wasm/ruvector_decompiler_wasm.d.ts new file mode 100644 index 00000000..5ac28ce9 --- /dev/null +++ b/npm/packages/ruvector/wasm/ruvector_decompiler_wasm.d.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Decompile a minified JavaScript bundle using the full Louvain pipeline. + * + * # Arguments + * + * * `source` - The minified JavaScript source code. + * * `config_json` - JSON string of `DecompileConfig` fields. Pass `"{}"` for defaults. + * + * # Returns + * + * A JSON string containing the `DecompileResult` (modules, witness, inferred names, etc.) + * or a JSON object with an `"error"` field on failure. + */ +export function decompile(source: string, config_json: string): string; + +/** + * Initialize the WASM module (sets up panic hook for better error messages). + */ +export function init(): void; + +/** + * Return the version of the decompiler WASM module. + */ +export function version(): string; diff --git a/npm/packages/ruvector/wasm/ruvector_decompiler_wasm.js b/npm/packages/ruvector/wasm/ruvector_decompiler_wasm.js new file mode 100644 index 00000000..c44ff53c --- /dev/null +++ b/npm/packages/ruvector/wasm/ruvector_decompiler_wasm.js @@ -0,0 +1,220 @@ +/* @ts-self-types="./ruvector_decompiler_wasm.d.ts" */ + +/** + * Decompile a minified JavaScript bundle using the full Louvain pipeline. + * + * # Arguments + * + * * `source` - The minified JavaScript source code. + * * `config_json` - JSON string of `DecompileConfig` fields. Pass `"{}"` for defaults. + * + * # Returns + * + * A JSON string containing the `DecompileResult` (modules, witness, inferred names, etc.) + * or a JSON object with an `"error"` field on failure. + * @param {string} source + * @param {string} config_json + * @returns {string} + */ +function decompile(source, config_json) { + let deferred3_0; + let deferred3_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(source, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(config_json, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const len1 = WASM_VECTOR_LEN; + wasm.decompile(retptr, ptr0, len0, ptr1, len1); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred3_0 = r0; + deferred3_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export(deferred3_0, deferred3_1, 1); + } +} +exports.decompile = decompile; + +/** + * Initialize the WASM module (sets up panic hook for better error messages). + */ +function init() { + wasm.init(); +} +exports.init = init; + +/** + * Return the version of the decompiler WASM module. + * @returns {string} + */ +function version() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.version(retptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export(deferred1_0, deferred1_1, 1); + } +} +exports.version = version; + +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg___wbindgen_throw_39bc967c0e5a9b58: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_export(deferred0_0, deferred0_1, 1); + } + }, + __wbg_new_227d7c05414eb861: function() { + const ret = new Error(); + return addHeapObject(ret); + }, + __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) { + const ret = getObject(arg1).stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbindgen_object_drop_ref: function(arg0) { + takeObject(arg0); + }, + }; + return { + __proto__: null, + "./ruvector_decompiler_wasm_bg.js": import0, + }; +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function dropObject(idx) { + if (idx < 1028) return; + heap[idx] = heap_next; + heap_next = idx; +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getObject(idx) { return heap[idx]; } + +let heap = new Array(1024).fill(undefined); +heap.push(undefined, null, true, false); + +let heap_next = heap.length; + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +function decodeText(ptr, len) { + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +const wasmPath = `${__dirname}/ruvector_decompiler_wasm_bg.wasm`; +const wasmBytes = require('fs').readFileSync(wasmPath); +const wasmModule = new WebAssembly.Module(wasmBytes); +let wasm = new WebAssembly.Instance(wasmModule, __wbg_get_imports()).exports; +wasm.__wbindgen_start(); diff --git a/npm/packages/ruvector/wasm/ruvector_decompiler_wasm_bg.wasm b/npm/packages/ruvector/wasm/ruvector_decompiler_wasm_bg.wasm new file mode 100644 index 00000000..295dbbd8 Binary files /dev/null and b/npm/packages/ruvector/wasm/ruvector_decompiler_wasm_bg.wasm differ diff --git a/npm/packages/ruvector/wasm/ruvector_decompiler_wasm_bg.wasm.d.ts b/npm/packages/ruvector/wasm/ruvector_decompiler_wasm_bg.wasm.d.ts new file mode 100644 index 00000000..bbb65c2e --- /dev/null +++ b/npm/packages/ruvector/wasm/ruvector_decompiler_wasm_bg.wasm.d.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const decompile: (a: number, b: number, c: number, d: number, e: number) => void; +export const version: (a: number) => void; +export const init: () => void; +export const mincut_add_result: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; +export const mincut_get_coordinator: () => number; +export const mincut_get_result: () => number; +export const mincut_init: (a: number, b: number, c: number) => void; +export const mincut_is_complete: () => number; +export const __wbindgen_export: (a: number, b: number, c: number) => void; +export const __wbindgen_export2: (a: number, b: number) => number; +export const __wbindgen_export3: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_add_to_stack_pointer: (a: number) => number; +export const __wbindgen_start: () => void;