feat(decompiler): WASM Louvain pipeline — npx now produces 589+ modules

Compiled ruvector-decompiler to WASM via wasm-pack:
- crates/ruvector-decompiler-wasm/ — wasm-bindgen wrapper (cdylib)
- rayon gated behind optional `parallel` feature (sequential in WASM)
- DecompileConfig now Deserializable for JSON config passing
- 1.5MB WASM binary at npm/packages/ruvector/wasm/

npx ruvector decompile now tries: WASM Louvain → Rust binary → keyword split
Result: 589 modules from Claude Code (was 5 with keyword splitter)

59 Rust tests pass, WASM verified from Node.js.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
rUv 2026-04-03 15:25:23 +00:00
parent 39740007ef
commit a3029eaecb
13 changed files with 442 additions and 7 deletions

View file

@ -149,6 +149,7 @@ members = [
"examples/climate-consciousness",
# JS bundle decompiler (ADR-135)
"crates/ruvector-decompiler",
"crates/ruvector-decompiler-wasm",
]
resolver = "2"

View file

@ -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

View file

@ -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()
}

View file

@ -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"]

View file

@ -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];

View file

@ -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<usize>,

View file

@ -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"
],

View file

@ -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,
};

View file

@ -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"
]
}

View file

@ -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;

View file

@ -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();

View file

@ -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;