Skip to main content

ruvector_graph_transformer_node/
lib.rs

1//! Node.js bindings for RuVector Graph Transformer via NAPI-RS
2//!
3//! Exposes proof-gated operations, sublinear attention, physics-informed
4//! layers, biological-inspired learning, verified training, manifold
5//! distance, temporal causal attention, and economic game-theoretic
6//! attention to Node.js applications.
7//!
8//! This crate embeds a self-contained graph transformer implementation
9//! to avoid coupling with the evolving `ruvector-graph-transformer` crate.
10
11#![deny(clippy::all)]
12
13mod transformer;
14
15use napi::bindgen_prelude::*;
16use napi_derive::napi;
17use transformer::{CoreGraphTransformer, Edge as CoreEdge, PipelineStage as CorePipelineStage};
18
19/// Graph Transformer with proof-gated operations for Node.js.
20///
21/// Provides sublinear attention over graph structures, physics-informed
22/// layers (Hamiltonian dynamics), biologically-inspired learning (spiking
23/// networks, Hebbian plasticity), and verified training with proof receipts.
24///
25/// # Example
26/// ```javascript
27/// const { GraphTransformer } = require('ruvector-graph-transformer-node');
28/// const gt = new GraphTransformer();
29/// console.log(gt.version());
30/// ```
31#[napi]
32pub struct GraphTransformer {
33    inner: CoreGraphTransformer,
34}
35
36#[napi]
37impl GraphTransformer {
38    /// Create a new Graph Transformer instance.
39    ///
40    /// # Arguments
41    /// * `config` - Optional JSON configuration (reserved for future use)
42    ///
43    /// # Example
44    /// ```javascript
45    /// const gt = new GraphTransformer();
46    /// const gt2 = new GraphTransformer({ maxFuel: 10000 });
47    /// ```
48    #[napi(constructor)]
49    pub fn new(_config: Option<serde_json::Value>) -> Self {
50        Self {
51            inner: CoreGraphTransformer::new(),
52        }
53    }
54
55    /// Get the library version string.
56    ///
57    /// # Example
58    /// ```javascript
59    /// console.log(gt.version()); // "2.0.4"
60    /// ```
61    #[napi]
62    pub fn version(&self) -> String {
63        self.inner.version()
64    }
65
66    // ===================================================================
67    // Proof-Gated Operations
68    // ===================================================================
69
70    /// Create a proof gate for a given dimension.
71    ///
72    /// Returns a JSON object describing the gate (id, dimension, verified).
73    ///
74    /// # Arguments
75    /// * `dim` - The dimension to gate on
76    ///
77    /// # Example
78    /// ```javascript
79    /// const gate = gt.createProofGate(128);
80    /// console.log(gate.dimension); // 128
81    /// ```
82    #[napi]
83    pub fn create_proof_gate(&mut self, dim: u32) -> Result<serde_json::Value> {
84        let gate = self.inner.create_proof_gate(dim);
85        serde_json::to_value(&gate).map_err(|e| {
86            Error::new(
87                Status::GenericFailure,
88                format!("Serialization error: {}", e),
89            )
90        })
91    }
92
93    /// Prove that two dimensions are equal.
94    ///
95    /// Returns a proof result with proof_id, expected, actual, and verified fields.
96    ///
97    /// # Arguments
98    /// * `expected` - The expected dimension
99    /// * `actual` - The actual dimension
100    ///
101    /// # Example
102    /// ```javascript
103    /// const proof = gt.proveDimension(128, 128);
104    /// console.log(proof.verified); // true
105    /// ```
106    #[napi]
107    pub fn prove_dimension(&mut self, expected: u32, actual: u32) -> Result<serde_json::Value> {
108        let result = self
109            .inner
110            .prove_dimension(expected, actual)
111            .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))?;
112        serde_json::to_value(&result).map_err(|e| {
113            Error::new(
114                Status::GenericFailure,
115                format!("Serialization error: {}", e),
116            )
117        })
118    }
119
120    /// Create a proof attestation (serializable receipt) for a given proof ID.
121    ///
122    /// Returns the attestation as a byte buffer (82 bytes) that can be
123    /// embedded in RVF WITNESS_SEG entries.
124    ///
125    /// # Arguments
126    /// * `proof_id` - The proof term ID to create an attestation for
127    ///
128    /// # Example
129    /// ```javascript
130    /// const proof = gt.proveDimension(64, 64);
131    /// const attestation = gt.createAttestation(proof.proof_id);
132    /// console.log(attestation.length); // 82
133    /// ```
134    #[napi]
135    pub fn create_attestation(&self, proof_id: u32) -> Result<Vec<u8>> {
136        let att = self.inner.create_attestation(proof_id);
137        Ok(att.to_bytes())
138    }
139
140    /// Compose a chain of pipeline stages, verifying type compatibility.
141    ///
142    /// Each stage must have `name`, `input_type_id`, and `output_type_id`.
143    /// Returns a composed proof with the overall input/output types and
144    /// the number of stages verified.
145    ///
146    /// # Arguments
147    /// * `stages` - Array of stage descriptors as JSON objects
148    ///
149    /// # Example
150    /// ```javascript
151    /// const composed = gt.composeProofs([
152    ///   { name: 'embed', input_type_id: 1, output_type_id: 2 },
153    ///   { name: 'align', input_type_id: 2, output_type_id: 3 },
154    /// ]);
155    /// console.log(composed.chain_name); // "embed >> align"
156    /// ```
157    #[napi]
158    pub fn compose_proofs(&mut self, stages: Vec<serde_json::Value>) -> Result<serde_json::Value> {
159        let rust_stages: Vec<CorePipelineStage> = stages
160            .into_iter()
161            .map(|v| {
162                serde_json::from_value(v).map_err(|e| {
163                    Error::new(
164                        Status::InvalidArg,
165                        format!("Invalid stage descriptor: {}", e),
166                    )
167                })
168            })
169            .collect::<Result<Vec<_>>>()?;
170
171        let result = self
172            .inner
173            .compose_proofs(&rust_stages)
174            .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))?;
175
176        serde_json::to_value(&result).map_err(|e| {
177            Error::new(
178                Status::GenericFailure,
179                format!("Serialization error: {}", e),
180            )
181        })
182    }
183
184    /// Verify an attestation from its byte representation.
185    ///
186    /// Returns `true` if the attestation is structurally valid.
187    ///
188    /// # Arguments
189    /// * `bytes` - The attestation bytes (82 bytes minimum)
190    ///
191    /// # Example
192    /// ```javascript
193    /// const valid = gt.verifyAttestation(attestationBytes);
194    /// ```
195    #[napi]
196    pub fn verify_attestation(&self, bytes: Vec<u8>) -> bool {
197        self.inner.verify_attestation(&bytes)
198    }
199
200    // ===================================================================
201    // Sublinear Attention
202    // ===================================================================
203
204    /// Sublinear graph attention using personalized PageRank sparsification.
205    ///
206    /// Instead of attending to all N nodes (O(N*d)), uses PPR to select
207    /// the top-k most relevant nodes, achieving O(k*d) complexity.
208    ///
209    /// # Arguments
210    /// * `query` - Query vector (length must equal `dim`)
211    /// * `edges` - Adjacency list: edges[i] is the list of neighbor indices for node i
212    /// * `dim` - Dimension of the query vector
213    /// * `k` - Number of top nodes to attend to
214    ///
215    /// # Returns
216    /// JSON object with `scores`, `top_k_indices`, and `sparsity_ratio`
217    ///
218    /// # Example
219    /// ```javascript
220    /// const result = gt.sublinearAttention([1.0, 0.5], [[1, 2], [0, 2], [0, 1]], 2, 2);
221    /// console.log(result.top_k_indices);
222    /// ```
223    #[napi]
224    pub fn sublinear_attention(
225        &mut self,
226        query: Vec<f64>,
227        edges: Vec<Vec<u32>>,
228        dim: u32,
229        k: u32,
230    ) -> Result<serde_json::Value> {
231        let result = self
232            .inner
233            .sublinear_attention(&query, &edges, dim, k)
234            .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))?;
235
236        serde_json::to_value(&result).map_err(|e| {
237            Error::new(
238                Status::GenericFailure,
239                format!("Serialization error: {}", e),
240            )
241        })
242    }
243
244    /// Compute personalized PageRank scores from a source node.
245    ///
246    /// # Arguments
247    /// * `source` - Source node index
248    /// * `adjacency` - Adjacency list for the graph
249    /// * `alpha` - Teleport probability (typically 0.15)
250    ///
251    /// # Returns
252    /// Array of PPR scores, one per node
253    ///
254    /// # Example
255    /// ```javascript
256    /// const scores = gt.pprScores(0, [[1], [0, 2], [1]], 0.15);
257    /// ```
258    #[napi]
259    pub fn ppr_scores(
260        &mut self,
261        source: u32,
262        adjacency: Vec<Vec<u32>>,
263        alpha: f64,
264    ) -> Result<Vec<f64>> {
265        Ok(self.inner.ppr_scores(source, &adjacency, alpha))
266    }
267
268    // ===================================================================
269    // Physics-Informed Layers
270    // ===================================================================
271
272    /// Symplectic integrator step (leapfrog / Stormer-Verlet).
273    ///
274    /// Integrates Hamiltonian dynamics with a harmonic potential V(q) = 0.5*|q|^2,
275    /// preserving the symplectic structure (energy-conserving).
276    ///
277    /// # Arguments
278    /// * `positions` - Position coordinates
279    /// * `momenta` - Momentum coordinates (same length as positions)
280    /// * `dt` - Time step
281    ///
282    /// # Returns
283    /// JSON object with `positions`, `momenta`, and `energy`
284    ///
285    /// # Example
286    /// ```javascript
287    /// const state = gt.hamiltonianStep([1.0, 0.0], [0.0, 1.0], 0.01);
288    /// console.log(state.energy);
289    /// ```
290    #[napi]
291    pub fn hamiltonian_step(
292        &mut self,
293        positions: Vec<f64>,
294        momenta: Vec<f64>,
295        dt: f64,
296    ) -> Result<serde_json::Value> {
297        let result = self
298            .inner
299            .hamiltonian_step(&positions, &momenta, dt)
300            .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))?;
301
302        serde_json::to_value(&result).map_err(|e| {
303            Error::new(
304                Status::GenericFailure,
305                format!("Serialization error: {}", e),
306            )
307        })
308    }
309
310    /// Hamiltonian step with graph edge interactions.
311    ///
312    /// `positions` and `momenta` are arrays of coordinates. `edges` is an
313    /// array of `{ src, tgt }` objects defining graph interactions.
314    ///
315    /// # Returns
316    /// JSON object with `positions`, `momenta`, `energy`, and `energy_conserved`
317    ///
318    /// # Example
319    /// ```javascript
320    /// const state = gt.hamiltonianStepGraph(
321    ///   [1.0, 0.0], [0.0, 1.0],
322    ///   [{ src: 0, tgt: 1 }], 0.01
323    /// );
324    /// ```
325    #[napi]
326    pub fn hamiltonian_step_graph(
327        &mut self,
328        positions: Vec<f64>,
329        momenta: Vec<f64>,
330        edges: Vec<serde_json::Value>,
331        dt: f64,
332    ) -> Result<serde_json::Value> {
333        let rust_edges: Vec<CoreEdge> = edges
334            .into_iter()
335            .map(|v| {
336                serde_json::from_value(v)
337                    .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid edge: {}", e)))
338            })
339            .collect::<Result<Vec<_>>>()?;
340
341        let result = self
342            .inner
343            .hamiltonian_step_graph(&positions, &momenta, &rust_edges, dt)
344            .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))?;
345
346        serde_json::to_value(&result).map_err(|e| {
347            Error::new(
348                Status::GenericFailure,
349                format!("Serialization error: {}", e),
350            )
351        })
352    }
353
354    // ===================================================================
355    // Biological-Inspired
356    // ===================================================================
357
358    /// Spiking neural attention: event-driven sparse attention.
359    ///
360    /// Nodes emit attention only when their membrane potential exceeds
361    /// a threshold, producing sparse activation patterns.
362    ///
363    /// # Arguments
364    /// * `spikes` - Membrane potentials for each node
365    /// * `edges` - Adjacency list for the graph
366    /// * `threshold` - Firing threshold
367    ///
368    /// # Returns
369    /// Output activation vector (one value per node)
370    ///
371    /// # Example
372    /// ```javascript
373    /// const output = gt.spikingAttention([0.5, 1.5, 0.3], [[1], [0, 2], [1]], 1.0);
374    /// ```
375    #[napi]
376    pub fn spiking_attention(
377        &mut self,
378        spikes: Vec<f64>,
379        edges: Vec<Vec<u32>>,
380        threshold: f64,
381    ) -> Result<Vec<f64>> {
382        Ok(self.inner.spiking_attention(&spikes, &edges, threshold))
383    }
384
385    /// Hebbian learning rule update.
386    ///
387    /// Applies the outer-product Hebbian rule: w_ij += lr * pre_i * post_j.
388    /// The weight vector is a flattened (pre.len * post.len) matrix.
389    ///
390    /// # Arguments
391    /// * `pre` - Pre-synaptic activations
392    /// * `post` - Post-synaptic activations
393    /// * `weights` - Current weight vector (flattened matrix)
394    /// * `lr` - Learning rate
395    ///
396    /// # Returns
397    /// Updated weight vector
398    ///
399    /// # Example
400    /// ```javascript
401    /// const updated = gt.hebbianUpdate([1.0, 0.0], [0.0, 1.0], [0, 0, 0, 0], 0.1);
402    /// ```
403    #[napi]
404    pub fn hebbian_update(
405        &mut self,
406        pre: Vec<f64>,
407        post: Vec<f64>,
408        weights: Vec<f64>,
409        lr: f64,
410    ) -> Result<Vec<f64>> {
411        Ok(self.inner.hebbian_update(&pre, &post, &weights, lr))
412    }
413
414    /// Spiking step over 2D node features with adjacency matrix.
415    ///
416    /// `features` is an array of arrays (n x dim). `adjacency` is a flat
417    /// row-major array (n x n). Returns `{ features, spikes, weights }`.
418    ///
419    /// # Example
420    /// ```javascript
421    /// const result = gt.spikingStep(
422    ///   [[0.8, 0.6], [0.1, 0.2]],
423    ///   [0, 0.5, 0.3, 0]
424    /// );
425    /// ```
426    #[napi]
427    pub fn spiking_step(
428        &mut self,
429        features: Vec<Vec<f64>>,
430        adjacency: Vec<f64>,
431    ) -> Result<serde_json::Value> {
432        let result = self.inner.spiking_step(&features, &adjacency, 1.0);
433        serde_json::to_value(&result).map_err(|e| {
434            Error::new(
435                Status::GenericFailure,
436                format!("Serialization error: {}", e),
437            )
438        })
439    }
440
441    // ===================================================================
442    // Verified Training
443    // ===================================================================
444
445    /// A single verified SGD step with proof of gradient application.
446    ///
447    /// Applies w' = w - lr * grad and returns the new weights along with
448    /// a proof receipt, loss before/after, and gradient norm.
449    ///
450    /// # Arguments
451    /// * `weights` - Current weight vector
452    /// * `gradients` - Gradient vector (same length as weights)
453    /// * `lr` - Learning rate
454    ///
455    /// # Returns
456    /// JSON object with `weights`, `proof_id`, `loss_before`, `loss_after`, `gradient_norm`
457    ///
458    /// # Example
459    /// ```javascript
460    /// const result = gt.verifiedStep([1.0, 2.0], [0.1, 0.2], 0.01);
461    /// console.log(result.loss_after < result.loss_before); // true
462    /// ```
463    #[napi]
464    pub fn verified_step(
465        &mut self,
466        weights: Vec<f64>,
467        gradients: Vec<f64>,
468        lr: f64,
469    ) -> Result<serde_json::Value> {
470        let result = self
471            .inner
472            .verified_step(&weights, &gradients, lr)
473            .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))?;
474
475        serde_json::to_value(&result).map_err(|e| {
476            Error::new(
477                Status::GenericFailure,
478                format!("Serialization error: {}", e),
479            )
480        })
481    }
482
483    /// Verified training step with features, targets, and weights.
484    ///
485    /// Computes MSE loss, applies SGD, and produces a training certificate.
486    ///
487    /// # Arguments
488    /// * `features` - Input feature vector
489    /// * `targets` - Target values
490    /// * `weights` - Current weight vector
491    ///
492    /// # Returns
493    /// JSON object with `weights`, `certificate_id`, `loss`,
494    /// `loss_monotonic`, `lipschitz_satisfied`
495    ///
496    /// # Example
497    /// ```javascript
498    /// const result = gt.verifiedTrainingStep([1.0, 2.0], [0.5, 1.0], [0.5, 0.5]);
499    /// ```
500    #[napi]
501    pub fn verified_training_step(
502        &mut self,
503        features: Vec<f64>,
504        targets: Vec<f64>,
505        weights: Vec<f64>,
506    ) -> Result<serde_json::Value> {
507        let result = self
508            .inner
509            .verified_training_step(&features, &targets, &weights, 0.001)
510            .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))?;
511
512        serde_json::to_value(&result).map_err(|e| {
513            Error::new(
514                Status::GenericFailure,
515                format!("Serialization error: {}", e),
516            )
517        })
518    }
519
520    // ===================================================================
521    // Manifold
522    // ===================================================================
523
524    /// Product manifold distance (mixed curvature spaces).
525    ///
526    /// Splits vectors into sub-spaces according to the curvatures array:
527    /// - curvature > 0: spherical distance
528    /// - curvature < 0: hyperbolic distance
529    /// - curvature == 0: Euclidean distance
530    ///
531    /// # Arguments
532    /// * `a` - First point
533    /// * `b` - Second point (same length as `a`)
534    /// * `curvatures` - Curvature for each sub-space
535    ///
536    /// # Returns
537    /// The product manifold distance as a number
538    ///
539    /// # Example
540    /// ```javascript
541    /// const d = gt.productManifoldDistance([1, 0, 0, 1], [0, 1, 1, 0], [0.0, -1.0]);
542    /// ```
543    #[napi]
544    pub fn product_manifold_distance(&self, a: Vec<f64>, b: Vec<f64>, curvatures: Vec<f64>) -> f64 {
545        self.inner.product_manifold_distance(&a, &b, &curvatures)
546    }
547
548    /// Product manifold attention with mixed curvatures.
549    ///
550    /// Computes attention in a product of spherical, hyperbolic, and
551    /// Euclidean subspaces, combining the results.
552    ///
553    /// # Arguments
554    /// * `features` - Input feature vector
555    /// * `edges` - Array of `{ src, tgt }` objects
556    ///
557    /// # Returns
558    /// JSON object with `output`, `curvatures`, `distances`
559    ///
560    /// # Example
561    /// ```javascript
562    /// const result = gt.productManifoldAttention(
563    ///   [1.0, 0.5, -0.3, 0.8],
564    ///   [{ src: 0, tgt: 1 }]
565    /// );
566    /// ```
567    #[napi]
568    pub fn product_manifold_attention(
569        &mut self,
570        features: Vec<f64>,
571        edges: Vec<serde_json::Value>,
572    ) -> Result<serde_json::Value> {
573        let rust_edges: Vec<CoreEdge> = edges
574            .into_iter()
575            .map(|v| {
576                serde_json::from_value(v)
577                    .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid edge: {}", e)))
578            })
579            .collect::<Result<Vec<_>>>()?;
580
581        let curvatures = vec![0.0, -1.0]; // default mixed curvatures
582        let result = self
583            .inner
584            .product_manifold_attention(&features, &rust_edges, &curvatures);
585
586        serde_json::to_value(&result).map_err(|e| {
587            Error::new(
588                Status::GenericFailure,
589                format!("Serialization error: {}", e),
590            )
591        })
592    }
593
594    // ===================================================================
595    // Temporal
596    // ===================================================================
597
598    /// Causal attention with temporal ordering.
599    ///
600    /// Attention scores are masked so that a key at time t_j can only
601    /// attend to queries at time t_i <= t_j (no information leakage
602    /// from the future).
603    ///
604    /// # Arguments
605    /// * `query` - Query vector
606    /// * `keys` - Array of key vectors
607    /// * `timestamps` - Timestamp for each key (same length as keys)
608    ///
609    /// # Returns
610    /// Softmax attention weights (one per key, sums to 1.0)
611    ///
612    /// # Example
613    /// ```javascript
614    /// const scores = gt.causalAttention(
615    ///   [1.0, 0.0],
616    ///   [[1.0, 0.0], [0.0, 1.0], [0.5, 0.5]],
617    ///   [1.0, 2.0, 3.0]
618    /// );
619    /// ```
620    #[napi]
621    pub fn causal_attention(
622        &mut self,
623        query: Vec<f64>,
624        keys: Vec<Vec<f64>>,
625        timestamps: Vec<f64>,
626    ) -> Result<Vec<f64>> {
627        Ok(self.inner.causal_attention(&query, &keys, &timestamps))
628    }
629
630    /// Causal attention over features, timestamps, and graph edges.
631    ///
632    /// Returns attention-weighted output features where each node can
633    /// only attend to neighbors with earlier or equal timestamps.
634    ///
635    /// # Arguments
636    /// * `features` - Feature value for each node
637    /// * `timestamps` - Timestamp for each node
638    /// * `edges` - Array of `{ src, tgt }` objects
639    ///
640    /// # Returns
641    /// Array of attention-weighted output values
642    ///
643    /// # Example
644    /// ```javascript
645    /// const output = gt.causalAttentionGraph(
646    ///   [1.0, 0.5, 0.8],
647    ///   [1.0, 2.0, 3.0],
648    ///   [{ src: 0, tgt: 1 }, { src: 1, tgt: 2 }]
649    /// );
650    /// ```
651    #[napi]
652    pub fn causal_attention_graph(
653        &mut self,
654        features: Vec<f64>,
655        timestamps: Vec<f64>,
656        edges: Vec<serde_json::Value>,
657    ) -> Result<Vec<f64>> {
658        let rust_edges: Vec<CoreEdge> = edges
659            .into_iter()
660            .map(|v| {
661                serde_json::from_value(v)
662                    .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid edge: {}", e)))
663            })
664            .collect::<Result<Vec<_>>>()?;
665
666        Ok(self
667            .inner
668            .causal_attention_graph(&features, &timestamps, &rust_edges))
669    }
670
671    /// Extract Granger causality DAG from attention history.
672    ///
673    /// Tests pairwise Granger causality between all nodes and returns
674    /// edges where the F-statistic exceeds the significance threshold.
675    ///
676    /// # Arguments
677    /// * `attention_history` - Flat array (T x N, row-major)
678    /// * `num_nodes` - Number of nodes N
679    /// * `num_steps` - Number of time steps T
680    ///
681    /// # Returns
682    /// JSON object with `edges` and `num_nodes`
683    ///
684    /// # Example
685    /// ```javascript
686    /// const dag = gt.grangerExtract(flatHistory, 3, 20);
687    /// console.log(dag.edges); // [{ source, target, f_statistic, is_causal }]
688    /// ```
689    #[napi]
690    pub fn granger_extract(
691        &mut self,
692        attention_history: Vec<f64>,
693        num_nodes: u32,
694        num_steps: u32,
695    ) -> Result<serde_json::Value> {
696        let dag = self
697            .inner
698            .granger_extract(&attention_history, num_nodes, num_steps);
699
700        serde_json::to_value(&dag).map_err(|e| {
701            Error::new(
702                Status::GenericFailure,
703                format!("Serialization error: {}", e),
704            )
705        })
706    }
707
708    // ===================================================================
709    // Economic / Game-Theoretic
710    // ===================================================================
711
712    /// Game-theoretic attention: computes Nash equilibrium allocations.
713    ///
714    /// Each node is a player with features as utility parameters. Edges
715    /// define strategic interactions. Uses best-response iteration to
716    /// converge to Nash equilibrium.
717    ///
718    /// # Arguments
719    /// * `features` - Feature/utility value for each node
720    /// * `edges` - Array of `{ src, tgt }` objects
721    ///
722    /// # Returns
723    /// JSON object with `allocations`, `utilities`, `nash_gap`, `converged`
724    ///
725    /// # Example
726    /// ```javascript
727    /// const result = gt.gameTheoreticAttention(
728    ///   [1.0, 0.5, 0.8],
729    ///   [{ src: 0, tgt: 1 }, { src: 1, tgt: 2 }]
730    /// );
731    /// console.log(result.converged); // true
732    /// ```
733    #[napi]
734    pub fn game_theoretic_attention(
735        &mut self,
736        features: Vec<f64>,
737        edges: Vec<serde_json::Value>,
738    ) -> Result<serde_json::Value> {
739        let rust_edges: Vec<CoreEdge> = edges
740            .into_iter()
741            .map(|v| {
742                serde_json::from_value(v)
743                    .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid edge: {}", e)))
744            })
745            .collect::<Result<Vec<_>>>()?;
746
747        let result = self.inner.game_theoretic_attention(&features, &rust_edges);
748
749        serde_json::to_value(&result).map_err(|e| {
750            Error::new(
751                Status::GenericFailure,
752                format!("Serialization error: {}", e),
753            )
754        })
755    }
756
757    // ===================================================================
758    // Stats
759    // ===================================================================
760
761    /// Get aggregate statistics as a JSON object.
762    ///
763    /// # Example
764    /// ```javascript
765    /// const stats = gt.stats();
766    /// console.log(stats.proofs_verified);
767    /// ```
768    #[napi]
769    pub fn stats(&self) -> serde_json::Value {
770        serde_json::to_value(self.inner.stats()).unwrap_or(serde_json::Value::Null)
771    }
772
773    /// Reset all internal state (caches, counters, gates).
774    ///
775    /// # Example
776    /// ```javascript
777    /// gt.reset();
778    /// ```
779    #[napi]
780    pub fn reset(&mut self) {
781        self.inner.reset();
782    }
783}
784
785/// Get the library version.
786#[napi]
787pub fn version() -> String {
788    env!("CARGO_PKG_VERSION").to_string()
789}
790
791/// Module initialization message.
792#[napi]
793pub fn init() -> String {
794    "RuVector Graph Transformer Node.js bindings initialized".to_string()
795}