Skip to main content

nsr_nodejs/
lib.rs

1//! NSR Node.js Bindings
2//!
3//! Native Node.js bindings for the NSR (Neuro-Symbolic Recursive) AI framework.
4
5#![deny(clippy::all)]
6
7mod runtime;
8
9use napi::bindgen_prelude::*;
10use napi_derive::napi;
11use std::sync::{Arc, Mutex};
12
13use stateset_nsr::nsr::{
14    GroundedInput, NSRConfig, NSRMachine, NSRMachineBuilder,
15    Program, Primitive, SemanticValue, TrainingExample,
16};
17use stateset_nsr::nsr::machine::presets;
18
19use crate::runtime::get_runtime;
20
21// ============================================================================
22// Grounded Input
23// ============================================================================
24
25/// Raw input to the NSR system (text, number, image, embedding, etc.)
26#[napi(js_name = "GroundedInput")]
27pub struct JsGroundedInput {
28    inner: GroundedInput,
29}
30
31#[napi]
32impl JsGroundedInput {
33    /// Create a text input
34    #[napi(factory)]
35    pub fn text(s: String) -> Self {
36        Self {
37            inner: GroundedInput::Text(s),
38        }
39    }
40
41    /// Create a numeric input
42    #[napi(factory)]
43    pub fn number(n: f64) -> Self {
44        Self {
45            inner: GroundedInput::Number(n),
46        }
47    }
48
49    /// Create an image input with dimensions
50    #[napi(factory)]
51    pub fn image(data: Vec<f64>, width: u32, height: u32, channels: Option<u32>) -> Self {
52        let data_f32: Vec<f32> = data.into_iter().map(|x| x as f32).collect();
53        Self {
54            inner: GroundedInput::ImageWithDims {
55                data: data_f32,
56                width: width as usize,
57                height: height as usize,
58                channels: channels.unwrap_or(3) as usize,
59            },
60        }
61    }
62
63    /// Create an embedding input (pre-computed vector)
64    #[napi(factory)]
65    pub fn embedding(data: Vec<f64>) -> Self {
66        let data_f32: Vec<f32> = data.into_iter().map(|x| x as f32).collect();
67        Self {
68            inner: GroundedInput::Embedding(data_f32),
69        }
70    }
71
72    /// Create a nil/empty input
73    #[napi(factory)]
74    pub fn nil() -> Self {
75        Self {
76            inner: GroundedInput::Nil,
77        }
78    }
79
80    /// Check if this is a text input
81    #[napi]
82    pub fn is_text(&self) -> bool {
83        matches!(self.inner, GroundedInput::Text(_))
84    }
85
86    /// Check if this is a number input
87    #[napi]
88    pub fn is_number(&self) -> bool {
89        matches!(self.inner, GroundedInput::Number(_))
90    }
91
92    /// Check if this is a nil input
93    #[napi]
94    pub fn is_nil(&self) -> bool {
95        matches!(self.inner, GroundedInput::Nil)
96    }
97
98    /// Get text content if this is a text input
99    #[napi]
100    pub fn as_text(&self) -> Option<String> {
101        if let GroundedInput::Text(s) = &self.inner {
102            Some(s.clone())
103        } else {
104            None
105        }
106    }
107
108    /// Get numeric value if this is a number input
109    #[napi]
110    pub fn as_number(&self) -> Option<f64> {
111        if let GroundedInput::Number(n) = &self.inner {
112            Some(*n)
113        } else {
114            None
115        }
116    }
117
118    #[napi]
119    pub fn to_string(&self) -> String {
120        format!("{}", self.inner)
121    }
122}
123
124// ============================================================================
125// Semantic Value
126// ============================================================================
127
128/// Computed semantic value (output of NSR inference)
129#[napi(js_name = "SemanticValue")]
130pub struct JsSemanticValue {
131    inner: SemanticValue,
132}
133
134#[napi]
135impl JsSemanticValue {
136    /// Create an integer value
137    #[napi(factory)]
138    pub fn integer(n: i64) -> Self {
139        Self {
140            inner: SemanticValue::Integer(n),
141        }
142    }
143
144    /// Create a float value
145    #[napi(factory)]
146    pub fn float(n: f64) -> Self {
147        Self {
148            inner: SemanticValue::Float(n),
149        }
150    }
151
152    /// Create a boolean value
153    #[napi(factory)]
154    pub fn boolean(b: bool) -> Self {
155        Self {
156            inner: SemanticValue::Boolean(b),
157        }
158    }
159
160    /// Create a string value
161    #[napi(factory)]
162    pub fn string(s: String) -> Self {
163        Self {
164            inner: SemanticValue::String(s),
165        }
166    }
167
168    /// Create a symbol value
169    #[napi(factory)]
170    pub fn symbol(s: String) -> Self {
171        Self {
172            inner: SemanticValue::Symbol(s),
173        }
174    }
175
176    /// Create an action sequence (for SCAN-like tasks)
177    #[napi(factory)]
178    pub fn actions(actions: Vec<String>) -> Self {
179        Self {
180            inner: SemanticValue::ActionSequence(actions),
181        }
182    }
183
184    /// Create a list of values
185    #[napi(factory)]
186    pub fn list(items: Vec<&JsSemanticValue>) -> Self {
187        Self {
188            inner: SemanticValue::List(items.into_iter().map(|v| v.inner.clone()).collect()),
189        }
190    }
191
192    /// Create a null value
193    #[napi(factory)]
194    pub fn null() -> Self {
195        Self {
196            inner: SemanticValue::Null,
197        }
198    }
199
200    /// Get integer value if this is an integer
201    #[napi]
202    pub fn as_integer(&self) -> Option<i64> {
203        self.inner.as_integer()
204    }
205
206    /// Get float value if this is a float
207    #[napi]
208    pub fn as_float(&self) -> Option<f64> {
209        self.inner.as_float()
210    }
211
212    /// Get string value if this is a string or symbol
213    #[napi]
214    pub fn as_string(&self) -> Option<String> {
215        self.inner.as_string().map(|s| s.to_string())
216    }
217
218    /// Get action sequence if this is an action sequence
219    #[napi]
220    pub fn as_actions(&self) -> Option<Vec<String>> {
221        self.inner.as_actions().map(|a| a.to_vec())
222    }
223
224    /// Check if this is an error
225    #[napi]
226    pub fn is_error(&self) -> bool {
227        self.inner.is_error()
228    }
229
230    /// Check if this is null
231    #[napi]
232    pub fn is_null(&self) -> bool {
233        matches!(self.inner, SemanticValue::Null)
234    }
235
236    #[napi]
237    pub fn to_string(&self) -> String {
238        format!("{}", self.inner)
239    }
240}
241
242// ============================================================================
243// Primitive Operations
244// ============================================================================
245
246/// Built-in primitive operations
247#[napi(js_name = "Primitive")]
248pub struct JsPrimitive {
249    inner: Primitive,
250}
251
252#[napi]
253impl JsPrimitive {
254    #[napi(factory)]
255    pub fn add() -> Self {
256        Self { inner: Primitive::Add }
257    }
258
259    #[napi(factory)]
260    pub fn sub() -> Self {
261        Self { inner: Primitive::Sub }
262    }
263
264    #[napi(factory)]
265    pub fn mul() -> Self {
266        Self { inner: Primitive::Mul }
267    }
268
269    #[napi(factory)]
270    pub fn div() -> Self {
271        Self { inner: Primitive::Div }
272    }
273
274    #[napi(factory)]
275    pub fn eq() -> Self {
276        Self { inner: Primitive::Eq }
277    }
278
279    #[napi(factory)]
280    pub fn lt() -> Self {
281        Self { inner: Primitive::Lt }
282    }
283
284    #[napi(factory)]
285    pub fn gt() -> Self {
286        Self { inner: Primitive::Gt }
287    }
288
289    #[napi(factory)]
290    pub fn and() -> Self {
291        Self { inner: Primitive::And }
292    }
293
294    #[napi(factory)]
295    pub fn or() -> Self {
296        Self { inner: Primitive::Or }
297    }
298
299    #[napi(factory)]
300    pub fn not() -> Self {
301        Self { inner: Primitive::Not }
302    }
303
304    #[napi(factory)]
305    pub fn cons() -> Self {
306        Self { inner: Primitive::Cons }
307    }
308
309    #[napi(factory)]
310    pub fn car() -> Self {
311        Self { inner: Primitive::Car }
312    }
313
314    #[napi(factory)]
315    pub fn cdr() -> Self {
316        Self { inner: Primitive::Cdr }
317    }
318
319    #[napi(factory)]
320    pub fn identity() -> Self {
321        Self { inner: Primitive::Identity }
322    }
323
324    /// Get the arity of this primitive
325    #[napi]
326    pub fn arity(&self) -> u32 {
327        self.inner.arity() as u32
328    }
329
330    /// Get the name of this primitive
331    #[napi]
332    pub fn name(&self) -> String {
333        format!("{:?}", self.inner)
334    }
335}
336
337// ============================================================================
338// Program
339// ============================================================================
340
341/// Functional program for computing semantics
342#[napi(js_name = "Program")]
343pub struct JsProgram {
344    inner: Program,
345}
346
347#[napi]
348impl JsProgram {
349    /// Create a constant program
350    #[napi(factory)]
351    pub fn constant(value: &JsSemanticValue) -> Self {
352        Self {
353            inner: Program::constant(value.inner.clone()),
354        }
355    }
356
357    /// Create a variable reference program
358    #[napi(factory)]
359    pub fn var(index: u32) -> Self {
360        Self {
361            inner: Program::var(index as usize),
362        }
363    }
364
365    /// Create a child reference program
366    #[napi(factory)]
367    pub fn child(index: u32) -> Self {
368        Self {
369            inner: Program::child(index as usize),
370        }
371    }
372
373    /// Create a primitive operation program
374    #[napi(factory)]
375    pub fn primitive(prim: &JsPrimitive, args: Vec<&JsProgram>) -> Self {
376        Self {
377            inner: Program::primitive(
378                prim.inner.clone(),
379                args.into_iter().map(|p| p.inner.clone()).collect(),
380            ),
381        }
382    }
383
384    /// Create a lambda program
385    #[napi(factory)]
386    pub fn lambda(arity: u32, body: &JsProgram) -> Self {
387        Self {
388            inner: Program::lambda(arity as usize, body.inner.clone()),
389        }
390    }
391
392    /// Create an application program
393    #[napi(factory)]
394    pub fn apply(func: &JsProgram, args: Vec<&JsProgram>) -> Self {
395        Self {
396            inner: Program::apply(
397                func.inner.clone(),
398                args.into_iter().map(|p| p.inner.clone()).collect(),
399            ),
400        }
401    }
402
403    /// Create a conditional program
404    #[napi(factory)]
405    pub fn if_then_else(cond: &JsProgram, then_branch: &JsProgram, else_branch: &JsProgram) -> Self {
406        Self {
407            inner: Program::if_then_else(
408                cond.inner.clone(),
409                then_branch.inner.clone(),
410                else_branch.inner.clone(),
411            ),
412        }
413    }
414
415    /// Get the depth of this program
416    #[napi]
417    pub fn depth(&self) -> u32 {
418        self.inner.depth() as u32
419    }
420
421    /// Get the size of this program
422    #[napi]
423    pub fn size(&self) -> u32 {
424        self.inner.size() as u32
425    }
426
427    /// Check if this is a constant program
428    #[napi]
429    pub fn is_constant(&self) -> bool {
430        self.inner.is_constant()
431    }
432}
433
434// ============================================================================
435// Configuration
436// ============================================================================
437
438/// Configuration for the NSR machine
439#[napi(object)]
440#[derive(Clone)]
441pub struct JsNSRConfig {
442    #[napi(js_name = "embeddingDim")]
443    pub embedding_dim: u32,
444    #[napi(js_name = "hiddenSize")]
445    pub hidden_size: u32,
446    #[napi(js_name = "maxSeqLen")]
447    pub max_seq_len: u32,
448    #[napi(js_name = "beamWidth")]
449    pub beam_width: u32,
450    #[napi(js_name = "enableSynthesis")]
451    pub enable_synthesis: bool,
452    #[napi(js_name = "maxProgramDepth")]
453    pub max_program_depth: u32,
454}
455
456impl Default for JsNSRConfig {
457    fn default() -> Self {
458        let config = NSRConfig::default();
459        Self {
460            embedding_dim: config.embedding_dim as u32,
461            hidden_size: config.hidden_size as u32,
462            max_seq_len: config.max_seq_len as u32,
463            beam_width: config.beam_width as u32,
464            enable_synthesis: config.enable_synthesis,
465            max_program_depth: config.max_program_depth as u32,
466        }
467    }
468}
469
470impl From<JsNSRConfig> for NSRConfig {
471    fn from(js: JsNSRConfig) -> Self {
472        NSRConfig {
473            embedding_dim: js.embedding_dim as usize,
474            hidden_size: js.hidden_size as usize,
475            max_seq_len: js.max_seq_len as usize,
476            beam_width: js.beam_width as usize,
477            enable_synthesis: js.enable_synthesis,
478            max_program_depth: js.max_program_depth as usize,
479            ..Default::default()
480        }
481    }
482}
483
484// ============================================================================
485// Training Example
486// ============================================================================
487
488/// A training example for the NSR machine
489#[napi(js_name = "TrainingExample")]
490pub struct JsTrainingExample {
491    inner: TrainingExample,
492}
493
494#[napi]
495impl JsTrainingExample {
496    /// Create a new training example
497    #[napi(constructor)]
498    pub fn new(
499        inputs: Vec<&JsGroundedInput>,
500        output: &JsSemanticValue,
501        difficulty: Option<f64>,
502    ) -> Self {
503        Self {
504            inner: TrainingExample::new(
505                inputs.into_iter().map(|i| i.inner.clone()).collect(),
506                output.inner.clone(),
507            )
508            .with_difficulty(difficulty.unwrap_or(0.0) as f32),
509        }
510    }
511
512    /// Create a training example from a single text input
513    #[napi(factory)]
514    pub fn from_text(text: String, output: &JsSemanticValue) -> Self {
515        Self {
516            inner: TrainingExample::new(
517                vec![GroundedInput::Text(text)],
518                output.inner.clone(),
519            ),
520        }
521    }
522
523    /// Create a training example from multiple text tokens
524    #[napi(factory)]
525    pub fn from_tokens(tokens: Vec<String>, output: &JsSemanticValue) -> Self {
526        Self {
527            inner: TrainingExample::new(
528                tokens.into_iter().map(GroundedInput::Text).collect(),
529                output.inner.clone(),
530            ),
531        }
532    }
533
534    /// Get the difficulty
535    #[napi(getter)]
536    pub fn difficulty(&self) -> f64 {
537        self.inner.difficulty as f64
538    }
539
540    /// Get the number of inputs
541    #[napi(getter)]
542    pub fn input_count(&self) -> u32 {
543        self.inner.inputs.len() as u32
544    }
545
546    #[napi]
547    pub fn to_string(&self) -> String {
548        format!(
549            "TrainingExample(inputs={}, difficulty={:.2})",
550            self.inner.inputs.len(),
551            self.inner.difficulty
552        )
553    }
554}
555
556// ============================================================================
557// Training Stats
558// ============================================================================
559
560/// Statistics from training
561#[napi(object)]
562pub struct JsTrainingStats {
563    #[napi(js_name = "totalExamples")]
564    pub total_examples: u32,
565    #[napi(js_name = "successfulAbductions")]
566    pub successful_abductions: u32,
567    #[napi(js_name = "trainingTimeMs")]
568    pub training_time_ms: u32,
569}
570
571// ============================================================================
572// Inference Result
573// ============================================================================
574
575/// Result of running inference
576#[napi(js_name = "InferenceResult")]
577pub struct JsInferenceResult {
578    output_str: Option<String>,
579    confidence_val: f64,
580    symbols_vec: Vec<u32>,
581    node_count_val: u32,
582    log_prob_val: f64,
583}
584
585#[napi]
586impl JsInferenceResult {
587    /// Get the output value
588    #[napi]
589    pub fn output(&self) -> Option<String> {
590        self.output_str.clone()
591    }
592
593    /// Get the confidence score
594    #[napi]
595    pub fn confidence(&self) -> f64 {
596        self.confidence_val
597    }
598
599    /// Get the symbol sequence
600    #[napi]
601    pub fn symbols(&self) -> Vec<u32> {
602        self.symbols_vec.clone()
603    }
604
605    /// Get the number of GSS nodes
606    #[napi]
607    pub fn node_count(&self) -> u32 {
608        self.node_count_val
609    }
610
611    /// Get the log probability
612    #[napi]
613    pub fn log_probability(&self) -> f64 {
614        self.log_prob_val
615    }
616
617    #[napi]
618    pub fn to_string(&self) -> String {
619        format!(
620            "InferenceResult(output={:?}, confidence={:.4}, symbols={})",
621            self.output_str,
622            self.confidence_val,
623            self.symbols_vec.len()
624        )
625    }
626}
627
628// ============================================================================
629// Evaluation Result
630// ============================================================================
631
632/// Result of evaluation
633#[napi(object)]
634pub struct JsEvaluationResult {
635    pub accuracy: f64,
636    pub correct: u32,
637    pub total: u32,
638}
639
640// ============================================================================
641// NSR Statistics
642// ============================================================================
643
644/// Machine statistics
645#[napi(object)]
646pub struct JsNSRStats {
647    #[napi(js_name = "trainingExamples")]
648    pub training_examples: u32,
649    #[napi(js_name = "successfulInferences")]
650    pub successful_inferences: u32,
651    #[napi(js_name = "programsLearned")]
652    pub programs_learned: u32,
653    #[napi(js_name = "vocabularySize")]
654    pub vocabulary_size: u32,
655}
656
657// ============================================================================
658// NSR Machine Builder
659// ============================================================================
660
661/// Builder for creating configured NSRMachine instances
662#[napi(js_name = "NSRMachineBuilder")]
663pub struct JsNSRMachineBuilder {
664    inner: Option<NSRMachineBuilder>,
665}
666
667#[napi]
668impl JsNSRMachineBuilder {
669    #[napi(constructor)]
670    pub fn new() -> Self {
671        Self {
672            inner: Some(NSRMachineBuilder::new()),
673        }
674    }
675
676    /// Set the embedding dimension
677    #[napi]
678    pub fn embedding_dim(&mut self, dim: u32) -> &Self {
679        if let Some(builder) = self.inner.take() {
680            self.inner = Some(builder.embedding_dim(dim as usize));
681        }
682        self
683    }
684
685    /// Set the hidden layer size
686    #[napi]
687    pub fn hidden_size(&mut self, size: u32) -> &Self {
688        if let Some(builder) = self.inner.take() {
689            self.inner = Some(builder.hidden_size(size as usize));
690        }
691        self
692    }
693
694    /// Set the maximum sequence length
695    #[napi]
696    pub fn max_seq_len(&mut self, len: u32) -> &Self {
697        if let Some(builder) = self.inner.take() {
698            self.inner = Some(builder.max_seq_len(len as usize));
699        }
700        self
701    }
702
703    /// Set the beam width for search
704    #[napi]
705    pub fn beam_width(&mut self, width: u32) -> &Self {
706        if let Some(builder) = self.inner.take() {
707            self.inner = Some(builder.beam_width(width as usize));
708        }
709        self
710    }
711
712    /// Add a symbol to the vocabulary
713    #[napi]
714    pub fn add_symbol(&mut self, name: String) -> &Self {
715        if let Some(builder) = self.inner.take() {
716            self.inner = Some(builder.add_symbol(name));
717        }
718        self
719    }
720
721    /// Enable or disable program synthesis
722    #[napi]
723    pub fn enable_synthesis(&mut self, enable: bool) -> &Self {
724        if let Some(builder) = self.inner.take() {
725            self.inner = Some(builder.enable_synthesis(enable));
726        }
727        self
728    }
729
730    /// Enable explainability features
731    #[napi]
732    pub fn with_explainability(&mut self) -> &Self {
733        if let Some(builder) = self.inner.take() {
734            self.inner = Some(builder.with_explainability());
735        }
736        self
737    }
738
739    /// Build the NSR machine
740    #[napi]
741    pub fn build(&mut self) -> Result<JsNSRMachine> {
742        let builder = self.inner.take().ok_or_else(|| {
743            Error::from_reason("Builder already consumed")
744        })?;
745        Ok(JsNSRMachine {
746            inner: Arc::new(Mutex::new(builder.build())),
747        })
748    }
749}
750
751// ============================================================================
752// NSR Machine
753// ============================================================================
754
755/// The Neural-Symbolic Recursive Machine - main Node.js interface
756#[napi(js_name = "NSRMachine")]
757pub struct JsNSRMachine {
758    inner: Arc<Mutex<NSRMachine>>,
759}
760
761#[napi]
762impl JsNSRMachine {
763    /// Create a new default NSR machine
764    #[napi(constructor)]
765    pub fn new() -> Self {
766        Self {
767            inner: Arc::new(Mutex::new(NSRMachine::default())),
768        }
769    }
770
771    /// Create with configuration
772    #[napi(factory)]
773    pub fn with_config(config: JsNSRConfig) -> Self {
774        Self {
775            inner: Arc::new(Mutex::new(NSRMachine::with_config(config.into()))),
776        }
777    }
778
779    /// Perform inference on inputs
780    #[napi]
781    pub fn infer(&self, inputs: Vec<&JsGroundedInput>) -> Result<JsInferenceResult> {
782        let rust_inputs: Vec<GroundedInput> = inputs.into_iter().map(|i| i.inner.clone()).collect();
783        let machine = self.inner.clone();
784
785        let runtime = get_runtime();
786        runtime.block_on(async {
787            let mut m = machine.lock().map_err(|e| Error::from_reason(e.to_string()))?;
788            let result = m.infer(&rust_inputs)
789                .await
790                .map_err(|e| Error::from_reason(e.to_string()))?;
791
792            Ok(JsInferenceResult {
793                output_str: result.output().map(|v| format!("{}", v)),
794                confidence_val: result.confidence(),
795                symbols_vec: result.symbols().into_iter().map(|s| s as u32).collect(),
796                node_count_val: result.gss.nodes.len() as u32,
797                log_prob_val: result.gss.log_probability(),
798            })
799        })
800    }
801
802    /// Train the machine on examples
803    #[napi]
804    pub fn train(&self, examples: Vec<&JsTrainingExample>) -> Result<JsTrainingStats> {
805        let rust_examples: Vec<TrainingExample> =
806            examples.into_iter().map(|e| e.inner.clone()).collect();
807        let machine = self.inner.clone();
808        let total = rust_examples.len() as u32;
809
810        let runtime = get_runtime();
811        let start = std::time::Instant::now();
812
813        runtime.block_on(async {
814            let mut m = machine.lock().map_err(|e| Error::from_reason(e.to_string()))?;
815            let stats = m.train(&rust_examples)
816                .await
817                .map_err(|e| Error::from_reason(e.to_string()))?;
818
819            Ok(JsTrainingStats {
820                total_examples: total,
821                successful_abductions: stats.successful_abductions as u32,
822                training_time_ms: start.elapsed().as_millis() as u32,
823            })
824        })
825    }
826
827    /// Evaluate accuracy on test examples
828    #[napi]
829    pub fn evaluate(&self, examples: Vec<&JsTrainingExample>) -> Result<JsEvaluationResult> {
830        let rust_examples: Vec<TrainingExample> =
831            examples.into_iter().map(|e| e.inner.clone()).collect();
832        let machine = self.inner.clone();
833        let total = rust_examples.len() as u32;
834
835        let runtime = get_runtime();
836        runtime.block_on(async {
837            let mut m = machine.lock().map_err(|e| Error::from_reason(e.to_string()))?;
838            let mut correct = 0u32;
839
840            for example in &rust_examples {
841                if let Ok(result) = m.infer(&example.inputs).await {
842                    if let Some(output) = result.output() {
843                        if output == &example.output {
844                            correct += 1;
845                        }
846                    }
847                }
848            }
849
850            Ok(JsEvaluationResult {
851                accuracy: if total > 0 { correct as f64 / total as f64 } else { 0.0 },
852                correct,
853                total,
854            })
855        })
856    }
857
858    /// Add a symbol to the vocabulary
859    #[napi]
860    pub fn add_symbol(&self, name: String) -> Result<u32> {
861        let mut m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
862        Ok(m.add_symbol(&name) as u32)
863    }
864
865    /// Add multiple symbols at once
866    #[napi]
867    pub fn add_symbols(&self, names: Vec<String>) -> Result<Vec<u32>> {
868        let mut m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
869        Ok(names.iter().map(|n| m.add_symbol(n) as u32).collect())
870    }
871
872    /// Get a symbol's name by ID
873    #[napi]
874    pub fn get_symbol_name(&self, symbol_id: u32) -> Result<Option<String>> {
875        let m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
876        Ok(m.vocabulary().get_name(symbol_id as usize).map(|s| s.to_string()))
877    }
878
879    /// Get a symbol's ID by name
880    #[napi]
881    pub fn get_symbol_id(&self, name: String) -> Result<Option<u32>> {
882        let m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
883        Ok(m.vocabulary().get_by_name(&name).map(|id| id as u32))
884    }
885
886    /// Get all symbol names
887    #[napi]
888    pub fn get_all_symbols(&self) -> Result<Vec<String>> {
889        let m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
890        let vocab = m.vocabulary();
891        let mut names = Vec::new();
892        for i in 0..vocab.len() {
893            if let Some(name) = vocab.get_name(i) {
894                names.push(name.to_string());
895            }
896        }
897        Ok(names)
898    }
899
900    /// Get the vocabulary size
901    #[napi(getter)]
902    pub fn vocabulary_size(&self) -> Result<u32> {
903        let m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
904        Ok(m.vocabulary().len() as u32)
905    }
906
907    /// Get machine statistics
908    #[napi(getter)]
909    pub fn statistics(&self) -> Result<JsNSRStats> {
910        let m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
911        let stats = m.statistics();
912        Ok(JsNSRStats {
913            training_examples: stats.training_examples as u32,
914            successful_inferences: stats.successful_inferences as u32,
915            programs_learned: stats.programs_learned as u32,
916            vocabulary_size: stats.vocabulary_size as u32,
917        })
918    }
919
920    /// Get configuration
921    #[napi(getter)]
922    pub fn config(&self) -> Result<JsNSRConfig> {
923        let m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
924        let cfg = m.config();
925        Ok(JsNSRConfig {
926            embedding_dim: cfg.embedding_dim as u32,
927            hidden_size: cfg.hidden_size as u32,
928            max_seq_len: cfg.max_seq_len as u32,
929            beam_width: cfg.beam_width as u32,
930            enable_synthesis: cfg.enable_synthesis,
931            max_program_depth: cfg.max_program_depth as u32,
932        })
933    }
934
935    /// Set the program for a symbol
936    /// This is essential for the deduction-abduction loop to work correctly
937    #[napi]
938    pub fn set_program(&self, symbol_id: u32, program: &JsProgram) -> Result<()> {
939        let mut m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
940        m.set_symbol_program(symbol_id as usize, program.inner.clone());
941        Ok(())
942    }
943
944    /// Set a constant program for a symbol (convenience method)
945    /// Creates a program that always returns the specified value
946    #[napi]
947    pub fn set_constant_program(&self, symbol_id: u32, value: &JsSemanticValue) -> Result<()> {
948        let mut m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
949        m.set_symbol_program(symbol_id as usize, Program::constant(value.inner.clone()));
950        Ok(())
951    }
952
953    /// Automatically set up constant programs for all symbols
954    /// Each symbol gets a program that returns SemanticValue::Symbol(symbol_name)
955    /// This is useful for classification tasks
956    #[napi]
957    pub fn setup_classification_programs(&self) -> Result<()> {
958        let mut m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
959        let vocab_len = m.vocabulary().len();
960        for symbol_id in 0..vocab_len {
961            if let Some(name) = m.vocabulary().get_name(symbol_id) {
962                let program = Program::constant(SemanticValue::Symbol(name.to_string()));
963                m.set_symbol_program(symbol_id, program);
964            }
965        }
966        Ok(())
967    }
968
969    /// Get the program for a symbol (if any)
970    #[napi]
971    pub fn get_program(&self, symbol_id: u32) -> Result<Option<JsProgram>> {
972        let m = self.inner.lock().map_err(|e| Error::from_reason(e.to_string()))?;
973        Ok(m.get_symbol_program(symbol_id as usize).map(|p| JsProgram {
974            inner: p.clone(),
975        }))
976    }
977}
978
979// ============================================================================
980// Preset Machine Functions
981// ============================================================================
982
983/// Create a SCAN-configured NSR machine
984#[napi]
985pub fn scan_machine() -> JsNSRMachine {
986    JsNSRMachine {
987        inner: Arc::new(Mutex::new(presets::scan_machine())),
988    }
989}
990
991/// Create a PCFG-configured NSR machine
992#[napi]
993pub fn pcfg_machine() -> JsNSRMachine {
994    JsNSRMachine {
995        inner: Arc::new(Mutex::new(presets::pcfg_machine())),
996    }
997}
998
999/// Create a HINT-configured NSR machine
1000#[napi]
1001pub fn hint_machine() -> JsNSRMachine {
1002    JsNSRMachine {
1003        inner: Arc::new(Mutex::new(presets::hint_machine())),
1004    }
1005}
1006
1007/// Create a COGS-configured NSR machine
1008#[napi]
1009pub fn cogs_machine() -> JsNSRMachine {
1010    JsNSRMachine {
1011        inner: Arc::new(Mutex::new(presets::cogs_machine())),
1012    }
1013}
1014
1015/// Get the library version
1016#[napi]
1017pub fn version() -> String {
1018    env!("CARGO_PKG_VERSION").to_string()
1019}