linreg_core/
lib.rs

1//! # linreg-core
2//!
3//! A lightweight, self-contained linear regression library in pure Rust.
4//!
5//! **No external math dependencies.** All linear algebra (matrices, QR decomposition)
6//! and statistical functions (distributions, hypothesis tests) are implemented from
7//! scratch. Compiles to WebAssembly for browser use or runs as a native Rust crate.
8//!
9//! ## What This Does
10//!
11//! - **OLS Regression** — Ordinary Least Squares with numerically stable QR decomposition
12//! - **Regularized Regression** — Ridge, Lasso, and Elastic Net via coordinate descent
13//! - **Diagnostic Tests** — 8+ statistical tests for validating regression assumptions
14//! - **WASM Support** — Same API works in browsers via WebAssembly
15//!
16//! ## Quick Start
17//!
18//! ### Native Rust
19//!
20//! Add to `Cargo.toml` (no WASM overhead):
21//!
22//! ```toml
23//! [dependencies]
24//! linreg-core = { version = "0.2", default-features = false }
25//! ```
26//!
27//! ```rust
28//! use linreg_core::core::ols_regression;
29//!
30//! let y = vec![2.5, 3.7, 4.2, 5.1, 6.3];
31//! let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
32//! let x2 = vec![2.0, 4.0, 5.0, 4.0, 3.0];
33//! let names = vec!["Intercept".into(), "Temp".into(), "Pressure".into()];
34//!
35//! let result = ols_regression(&y, &[x1, x2], &names)?;
36//! println!("R²: {}", result.r_squared);
37//! println!("F-statistic: {}", result.f_statistic);
38//! # Ok::<(), linreg_core::Error>(())
39//! ```
40//!
41//! ### WebAssembly (JavaScript)
42//!
43//! ```toml
44//! [dependencies]
45//! linreg-core = "0.2"
46//! ```
47//!
48//! Build with `wasm-pack build --target web`, then use in JavaScript:
49//!
50//! ```text
51//! import init, { ols_regression } from './linreg_core.js';
52//! await init();
53//!
54//! const result = JSON.parse(ols_regression(
55//!     JSON.stringify([2.5, 3.7, 4.2, 5.1, 6.3]),
56//!     JSON.stringify([[1,2,3,4,5], [2,4,5,4,3]]),
57//!     JSON.stringify(["Intercept", "X1", "X2"])
58//! ));
59//! console.log("R²:", result.r_squared);
60//! ```
61//!
62//! ## Regularized Regression
63//!
64//! ```no_run
65//! use linreg_core::regularized::{ridge, lasso};
66//! use linreg_core::linalg::Matrix;
67//!
68//! let x = Matrix::new(100, 3, vec![0.0; 300]);
69//! let y = vec![0.0; 100];
70//!
71//! // Ridge regression (L2 penalty - shrinks coefficients, handles multicollinearity)
72//! let ridge_result = ridge::ridge_fit(&x, &y, &ridge::RidgeFitOptions {
73//!     lambda: 1.0,
74//!     intercept: true,
75//!     standardize: true,
76//! })?;
77//!
78//! // Lasso regression (L1 penalty - does variable selection by zeroing coefficients)
79//! let lasso_result = lasso::lasso_fit(&x, &y, &lasso::LassoFitOptions {
80//!     lambda: 0.1,
81//!     intercept: true,
82//!     standardize: true,
83//!     ..Default::default()
84//! })?;
85//! # Ok::<(), linreg_core::Error>(())
86//! ```
87//!
88//! ## Diagnostic Tests
89//!
90//! After fitting a model, validate its assumptions:
91//!
92//! | Test | Tests For | Use When |
93//! |------|-----------|----------|
94//! | [`rainbow_test`] | Linearity | Checking if relationships are linear |
95//! | [`harvey_collier_test`] | Functional form | Suspecting model misspecification |
96//! | [`breusch_pagan_test`] | Heteroscedasticity | Variance changes with predictors |
97//! | [`white_test`] | Heteroscedasticity | More general than Breusch-Pagan |
98//! | [`shapiro_wilk_test`] | Normality | Small to moderate samples (n ≤ 5000) |
99//! | [`jarque_bera_test`] | Normality | Large samples, skewness/kurtosis |
100//! | [`anderson_darling_test`] | Normality | Tail-sensitive, any sample size |
101//! | [`durbin_watson_test`] | Autocorrelation | Time series or ordered data |
102//! | [`cooks_distance_test`] | Influential points | Identifying high-impact observations |
103//!
104//! ```rust
105//! use linreg_core::diagnostics::{rainbow_test, breusch_pagan_test, RainbowMethod};
106//!
107//! # let y = vec![2.5, 3.7, 4.2, 5.1, 6.3];
108//! # let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
109//! # let x2 = vec![2.0, 4.0, 5.0, 4.0, 3.0];
110//! // Rainbow test for linearity
111//! let rainbow = rainbow_test(&y, &[x1.clone(), x2.clone()], 0.5, RainbowMethod::R)?;
112//! if rainbow.r_result.as_ref().map_or(false, |r| r.p_value < 0.05) {
113//!     println!("Warning: relationship may be non-linear");
114//! }
115//!
116//! // Breusch-Pagan test for heteroscedasticity
117//! let bp = breusch_pagan_test(&y, &[x1, x2])?;
118//! if bp.p_value < 0.05 {
119//!     println!("Warning: residuals have non-constant variance");
120//! }
121//! # Ok::<(), linreg_core::Error>(())
122//! ```
123//!
124//! ## Feature Flags
125//!
126//! | Flag | Default | Description |
127//! |------|---------|-------------|
128//! | `wasm` | Yes | Enables WASM bindings and browser support |
129//! | `validation` | No | Includes test data for validation tests |
130//!
131//! For native-only builds (smaller binary, no WASM deps):
132//!
133//! ```toml
134//! linreg-core = { version = "0.2", default-features = false }
135//! ```
136//!
137//! ## Why This Library?
138//!
139//! - **Zero dependencies** — No `nalgebra`, no `statrs`, no `ndarray`. Pure Rust.
140//! - **Validated** — Outputs match R's `lm()` and Python's `statsmodels`
141//! - **WASM-ready** — Same code runs natively and in browsers
142//! - **Small** — Core is ~2000 lines, compiles quickly
143//! - **Permissive license** — MIT OR Apache-2.0
144//!
145//! ## Module Structure
146//!
147//! - [`core`] — OLS regression, coefficients, residuals, VIF
148//! - [`regularized`] — Ridge, Lasso, Elastic Net, regularization paths
149//! - [`diagnostics`] — All diagnostic tests (linearity, heteroscedasticity, normality, autocorrelation)
150//! - [`distributions`] — Statistical distributions (t, F, χ², normal, beta, gamma)
151//! - [`linalg`] — Matrix operations, QR decomposition, linear system solver
152//! - [`error`] — Error types and Result alias
153//!
154//! ## Links
155//!
156//! - [Repository](https://github.com/jesse-anderson/linreg-core)
157//! - [Documentation](https://docs.rs/linreg-core)
158//! - [Examples](https://github.com/jesse-anderson/linreg-core/tree/main/examples)
159//!
160//! ## Disclaimer
161//!
162//! This library is under active development and has not reached 1.0 stability.
163//! While outputs are validated against R and Python implementations, **do not
164//! use this library for critical applications** (medical, financial, safety-critical
165//! systems) without independent verification. See the LICENSE for full terms.
166//! The software is provided "as is" without warranty of any kind.
167
168// Import core modules (always available)
169pub mod core;
170pub mod diagnostics;
171pub mod distributions;
172pub mod linalg;
173pub mod regularized;
174pub mod error;
175
176// Unit tests are now in tests/unit/ directory
177// - error_tests.rs -> tests/unit/error_tests.rs
178// - core_tests.rs -> tests/unit/core_tests.rs
179// - linalg_tests.rs -> tests/unit/linalg_tests.rs
180// - validation_tests.rs -> tests/validation/main.rs
181// - diagnostics_tests.rs: disabled (references unimplemented functions)
182
183// Re-export public API (always available)
184pub use core::{RegressionOutput, VifResult};
185pub use diagnostics::{
186    DiagnosticTestResult,
187    RainbowTestOutput, RainbowSingleResult, RainbowMethod,
188    WhiteTestOutput, WhiteSingleResult, WhiteMethod,
189    CooksDistanceResult,
190};
191
192// Re-export core test functions with different names to avoid WASM conflicts
193pub use diagnostics::rainbow_test as rainbow_test_core;
194pub use diagnostics::white_test as white_test_core;
195
196pub use error::{Error, Result, error_json, error_to_json};
197
198// ============================================================================
199// WASM-specific code (only compiled when "wasm" feature is enabled)
200// ============================================================================
201
202#[cfg(feature = "wasm")]
203use wasm_bindgen::prelude::*;
204
205#[cfg(feature = "wasm")]
206use std::collections::HashSet;
207
208#[cfg(feature = "wasm")]
209use serde::Serialize;
210
211#[cfg(feature = "wasm")]
212use crate::distributions::{student_t_cdf, normal_inverse_cdf};
213
214// ============================================================================
215// CSV Parsing (WASM-only)
216// ============================================================================
217
218#[cfg(feature = "wasm")]
219#[derive(Serialize)]
220struct ParsedCsv {
221    headers: Vec<String>,
222    data: Vec<serde_json::Map<String, serde_json::Value>>,
223    numeric_columns: Vec<String>,
224}
225
226#[cfg(feature = "wasm")]
227#[wasm_bindgen]
228/// Parses CSV data and returns it as a JSON string.
229///
230/// Parses the CSV content and identifies numeric columns. Returns a JSON object
231/// with headers, data rows, and a list of numeric column names.
232///
233/// # Arguments
234///
235/// * `content` - CSV content as a string
236///
237/// # Returns
238///
239/// JSON string with structure:
240/// ```json
241/// {
242///   "headers": ["col1", "col2", ...],
243///   "data": [{"col1": 1.0, "col2": "text"}, ...],
244///   "numeric_columns": ["col1", ...]
245/// }
246/// ```
247///
248/// # Errors
249///
250/// Returns a JSON error object if parsing fails or domain check fails.
251pub fn parse_csv(content: &str) -> String {
252    if let Err(e) = check_domain() {
253        return error_to_json(&e);
254    }
255
256    let mut reader = csv::ReaderBuilder::new()
257        .has_headers(true)
258        .flexible(true)
259        .from_reader(content.as_bytes());
260
261    // Get headers
262    let headers: Vec<String> = match reader.headers() {
263        Ok(h) => h.iter().map(|s| s.to_string()).collect(),
264        Err(e) => return error_json(&format!("Failed to read headers: {}", e)),
265    };
266
267    let mut data = Vec::new();
268    let mut numeric_col_set = HashSet::new();
269
270    for result in reader.records() {
271        let record = match result {
272            Ok(r) => r,
273            Err(e) => return error_json(&format!("Failed to parse CSV record: {}", e)),
274        };
275
276        if record.len() != headers.len() {
277            continue;
278        }
279
280        let mut row_map = serde_json::Map::new();
281
282        for (i, field) in record.iter().enumerate() {
283            if i >= headers.len() {
284                continue;
285            }
286
287            let header = &headers[i];
288            let val_trimmed = field.trim();
289
290            // Try to parse as f64
291            if let Ok(num) = val_trimmed.parse::<f64>() {
292                if num.is_finite() {
293                    row_map.insert(
294                        header.clone(),
295                        serde_json::Value::Number(serde_json::Number::from_f64(num).unwrap()),
296                    );
297                    numeric_col_set.insert(header.clone());
298                    continue;
299                }
300            }
301
302            // Fallback to string
303            row_map.insert(header.clone(), serde_json::Value::String(val_trimmed.to_string()));
304        }
305        data.push(row_map);
306    }
307
308    let mut numeric_columns: Vec<String> = numeric_col_set.into_iter().collect();
309    numeric_columns.sort();
310
311    let output = ParsedCsv {
312        headers,
313        data,
314        numeric_columns,
315    };
316
317    serde_json::to_string(&output).unwrap_or_else(|_| error_json("Failed to serialize CSV output"))
318}
319
320// ============================================================================
321// OLS Regression WASM Wrapper
322// ============================================================================
323
324#[cfg(feature = "wasm")]
325#[wasm_bindgen]
326/// Performs OLS regression via WASM.
327///
328/// All parameters and return values are JSON-encoded strings for JavaScript
329/// interoperability. Returns regression output including coefficients,
330/// standard errors, diagnostic statistics, and VIF analysis.
331///
332/// # Arguments
333///
334/// * `y_json` - JSON array of response variable values: `[1.0, 2.0, 3.0]`
335/// * `x_vars_json` - JSON array of predictor arrays: `[[1.0, 2.0], [0.5, 1.0]]`
336/// * `variable_names` - JSON array of variable names: `["Intercept", "X1", "X2"]`
337///
338/// # Returns
339///
340/// JSON string containing the complete regression output with coefficients,
341/// standard errors, t-statistics, p-values, R², F-statistic, residuals, leverage, VIF, etc.
342///
343/// # Errors
344///
345/// Returns a JSON error object if:
346/// - JSON parsing fails
347/// - Insufficient data (n ≤ k + 1)
348/// - Matrix is singular
349/// - Domain check fails
350pub fn ols_regression(
351    y_json: &str,
352    x_vars_json: &str,
353    variable_names: &str,
354) -> String {
355    if let Err(e) = check_domain() {
356        return error_to_json(&e);
357    }
358
359    // Parse JSON input
360    let y: Vec<f64> = match serde_json::from_str(y_json) {
361        Ok(v) => v,
362        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
363    };
364
365    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
366        Ok(v) => v,
367        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
368    };
369
370    let names: Vec<String> = match serde_json::from_str(variable_names) {
371        Ok(v) => v,
372        Err(_) => vec!["Intercept".to_string()],
373    };
374
375    // Call core function
376    match core::ols_regression(&y, &x_vars, &names) {
377        Ok(output) => serde_json::to_string(&output)
378            .unwrap_or_else(|_| error_json("Failed to serialize output")),
379        Err(e) => error_json(&e.to_string()),
380    }
381}
382
383// ============================================================================
384// Diagnostic Tests WASM Wrappers
385// ============================================================================
386
387/// Performs the Rainbow test for linearity via WASM.
388///
389/// The Rainbow test checks whether the relationship between predictors and response
390/// is linear. A significant p-value suggests non-linearity.
391///
392/// # Arguments
393///
394/// * `y_json` - JSON array of response variable values
395/// * `x_vars_json` - JSON array of predictor arrays
396/// * `fraction` - Fraction of data to use in the central subset (0.0 to 1.0, typically 0.5)
397/// * `method` - Method to use: "r", "python", or "both" (case-insensitive, defaults to "r")
398///
399/// # Returns
400///
401/// JSON string containing test statistic, p-value, and interpretation.
402///
403/// # Errors
404///
405/// Returns a JSON error object if parsing fails or domain check fails.
406#[cfg(feature = "wasm")]
407#[wasm_bindgen]
408pub fn rainbow_test(
409    y_json: &str,
410    x_vars_json: &str,
411    fraction: f64,
412    method: &str,
413) -> String {
414    if let Err(e) = check_domain() {
415        return error_to_json(&e);
416    }
417
418    let y: Vec<f64> = match serde_json::from_str(y_json) {
419        Ok(v) => v,
420        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
421    };
422
423    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
424        Ok(v) => v,
425        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
426    };
427
428    // Parse method parameter (default to "r" for R)
429    let method = match method.to_lowercase().as_str() {
430        "python" => diagnostics::RainbowMethod::Python,
431        "both" => diagnostics::RainbowMethod::Both,
432        _ => diagnostics::RainbowMethod::R,  // Default to R
433    };
434
435    match diagnostics::rainbow_test(&y, &x_vars, fraction, method) {
436        Ok(output) => serde_json::to_string(&output)
437            .unwrap_or_else(|_| error_json("Failed to serialize Rainbow test result")),
438        Err(e) => error_json(&e.to_string()),
439    }
440}
441
442/// Performs the Harvey-Collier test for linearity via WASM.
443///
444/// The Harvey-Collier test checks whether the residuals exhibit a linear trend,
445/// which would indicate that the model's functional form is misspecified.
446/// A significant p-value suggests non-linearity.
447///
448/// # Arguments
449///
450/// * `y_json` - JSON array of response variable values
451/// * `x_vars_json` - JSON array of predictor arrays
452///
453/// # Returns
454///
455/// JSON string containing test statistic, p-value, and interpretation.
456///
457/// # Errors
458///
459/// Returns a JSON error object if parsing fails or domain check fails.
460#[cfg(feature = "wasm")]
461#[wasm_bindgen]
462pub fn harvey_collier_test(
463    y_json: &str,
464    x_vars_json: &str,
465) -> String {
466    if let Err(e) = check_domain() {
467        return error_to_json(&e);
468    }
469
470    let y: Vec<f64> = match serde_json::from_str(y_json) {
471        Ok(v) => v,
472        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
473    };
474
475    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
476        Ok(v) => v,
477        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
478    };
479
480    match diagnostics::harvey_collier_test(&y, &x_vars) {
481        Ok(output) => serde_json::to_string(&output)
482            .unwrap_or_else(|_| error_json("Failed to serialize Harvey-Collier test result")),
483        Err(e) => error_json(&e.to_string()),
484    }
485}
486
487/// Performs the Breusch-Pagan test for heteroscedasticity via WASM.
488///
489/// The Breusch-Pagan test checks whether the variance of residuals is constant
490/// across the range of predicted values (homoscedasticity assumption).
491/// A significant p-value suggests heteroscedasticity.
492///
493/// # Arguments
494///
495/// * `y_json` - JSON array of response variable values
496/// * `x_vars_json` - JSON array of predictor arrays
497///
498/// # Returns
499///
500/// JSON string containing test statistic, p-value, and interpretation.
501///
502/// # Errors
503///
504/// Returns a JSON error object if parsing fails or domain check fails.
505#[cfg(feature = "wasm")]
506#[wasm_bindgen]
507pub fn breusch_pagan_test(
508    y_json: &str,
509    x_vars_json: &str,
510) -> String {
511    if let Err(e) = check_domain() {
512        return error_to_json(&e);
513    }
514
515    let y: Vec<f64> = match serde_json::from_str(y_json) {
516        Ok(v) => v,
517        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
518    };
519
520    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
521        Ok(v) => v,
522        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
523    };
524
525    match diagnostics::breusch_pagan_test(&y, &x_vars) {
526        Ok(output) => serde_json::to_string(&output)
527            .unwrap_or_else(|_| error_json("Failed to serialize Breusch-Pagan test result")),
528        Err(e) => error_json(&e.to_string()),
529    }
530}
531
532/// Performs the White test for heteroscedasticity via WASM.
533///
534/// The White test is a more general test for heteroscedasticity that does not
535/// assume a specific form of heteroscedasticity. A significant p-value suggests
536/// that the error variance is not constant.
537///
538/// # Arguments
539///
540/// * `y_json` - JSON array of response variable values
541/// * `x_vars_json` - JSON array of predictor arrays
542/// * `method` - Method to use: "r", "python", or "both" (case-insensitive, defaults to "r")
543///
544/// # Returns
545///
546/// JSON string containing test statistic, p-value, and interpretation.
547///
548/// # Errors
549///
550/// Returns a JSON error object if parsing fails or domain check fails.
551#[cfg(feature = "wasm")]
552#[wasm_bindgen]
553pub fn white_test(
554    y_json: &str,
555    x_vars_json: &str,
556    method: &str,
557) -> String {
558    if let Err(e) = check_domain() {
559        return error_to_json(&e);
560    }
561
562    let y: Vec<f64> = match serde_json::from_str(y_json) {
563        Ok(v) => v,
564        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
565    };
566
567    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
568        Ok(v) => v,
569        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
570    };
571
572    // Parse method parameter (default to "r" for R)
573    let method = match method.to_lowercase().as_str() {
574        "python" => diagnostics::WhiteMethod::Python,
575        "both" => diagnostics::WhiteMethod::Both,
576        _ => diagnostics::WhiteMethod::R,  // Default to R
577    };
578
579    match diagnostics::white_test(&y, &x_vars, method) {
580        Ok(output) => serde_json::to_string(&output)
581            .unwrap_or_else(|_| error_json("Failed to serialize White test result")),
582        Err(e) => error_json(&e.to_string()),
583    }
584}
585
586/// Performs the R method White test for heteroscedasticity via WASM.
587///
588/// This implementation matches R's `skedastic::white()` function behavior.
589/// Uses the standard QR decomposition and the R-specific auxiliary matrix
590/// structure (intercept, X, X² only - no cross-products).
591///
592/// # Arguments
593///
594/// * `y_json` - JSON array of response variable values
595/// * `x_vars_json` - JSON array of predictor arrays (each array is a column)
596///
597/// # Returns
598///
599/// JSON string containing test statistic, p-value, and interpretation.
600///
601/// # Errors
602///
603/// Returns a JSON error object if parsing fails or domain check fails.
604#[cfg(feature = "wasm")]
605#[wasm_bindgen]
606pub fn r_white_test(
607    y_json: &str,
608    x_vars_json: &str,
609) -> String {
610    if let Err(e) = check_domain() {
611        return error_to_json(&e);
612    }
613
614    let y: Vec<f64> = match serde_json::from_str(y_json) {
615        Ok(v) => v,
616        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
617    };
618
619    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
620        Ok(v) => v,
621        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
622    };
623
624    match diagnostics::r_white_method(&y, &x_vars) {
625        Ok(output) => serde_json::to_string(&output)
626            .unwrap_or_else(|_| error_json("Failed to serialize R White test result")),
627        Err(e) => error_json(&e.to_string()),
628    }
629}
630
631/// Performs the Python method White test for heteroscedasticity via WASM.
632///
633/// This implementation matches Python's `statsmodels.stats.diagnostic.het_white()` function.
634/// Uses the LINPACK QR decomposition with column pivoting and the Python-specific
635/// auxiliary matrix structure (intercept, X, X², and cross-products).
636///
637/// # Arguments
638///
639/// * `y_json` - JSON array of response variable values
640/// * `x_vars_json` - JSON array of predictor arrays (each array is a column)
641///
642/// # Returns
643///
644/// JSON string containing test statistic, p-value, and interpretation.
645///
646/// # Errors
647///
648/// Returns a JSON error object if parsing fails or domain check fails.
649#[cfg(feature = "wasm")]
650#[wasm_bindgen]
651pub fn python_white_test(
652    y_json: &str,
653    x_vars_json: &str,
654) -> String {
655    if let Err(e) = check_domain() {
656        return error_to_json(&e);
657    }
658
659    let y: Vec<f64> = match serde_json::from_str(y_json) {
660        Ok(v) => v,
661        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
662    };
663
664    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
665        Ok(v) => v,
666        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
667    };
668
669    match diagnostics::python_white_method(&y, &x_vars) {
670        Ok(output) => serde_json::to_string(&output)
671            .unwrap_or_else(|_| error_json("Failed to serialize Python White test result")),
672        Err(e) => error_json(&e.to_string()),
673    }
674}
675
676/// Performs the Jarque-Bera test for normality via WASM.
677///
678/// The Jarque-Bera test checks whether the residuals are normally distributed
679/// by examining skewness and kurtosis. A significant p-value suggests that
680/// the residuals deviate from normality.
681///
682/// # Arguments
683///
684/// * `y_json` - JSON array of response variable values
685/// * `x_vars_json` - JSON array of predictor arrays
686///
687/// # Returns
688///
689/// JSON string containing test statistic, p-value, and interpretation.
690///
691/// # Errors
692///
693/// Returns a JSON error object if parsing fails or domain check fails.
694#[cfg(feature = "wasm")]
695#[wasm_bindgen]
696pub fn jarque_bera_test(
697    y_json: &str,
698    x_vars_json: &str,
699) -> String {
700    if let Err(e) = check_domain() {
701        return error_to_json(&e);
702    }
703
704    let y: Vec<f64> = match serde_json::from_str(y_json) {
705        Ok(v) => v,
706        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
707    };
708
709    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
710        Ok(v) => v,
711        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
712    };
713
714    match diagnostics::jarque_bera_test(&y, &x_vars) {
715        Ok(output) => serde_json::to_string(&output)
716            .unwrap_or_else(|_| error_json("Failed to serialize Jarque-Bera test result")),
717        Err(e) => error_json(&e.to_string()),
718    }
719}
720
721// ============================================================================
722// Durbin-Watson Test (WASM wrapper)
723// ============================================================================
724
725/// Performs the Durbin-Watson test for autocorrelation via WASM.
726///
727/// The Durbin-Watson test checks for autocorrelation in the residuals.
728/// Values near 2 indicate no autocorrelation, values near 0 suggest positive
729/// autocorrelation, and values near 4 suggest negative autocorrelation.
730///
731/// # Arguments
732///
733/// * `y_json` - JSON array of response variable values
734/// * `x_vars_json` - JSON array of predictor arrays
735///
736/// # Returns
737///
738/// JSON string containing the DW statistic and interpretation.
739///
740/// # Errors
741///
742/// Returns a JSON error object if parsing fails or domain check fails.
743#[cfg(feature = "wasm")]
744#[wasm_bindgen]
745pub fn durbin_watson_test(
746    y_json: &str,
747    x_vars_json: &str,
748) -> String {
749    if let Err(e) = check_domain() {
750        return error_to_json(&e);
751    }
752
753    let y: Vec<f64> = match serde_json::from_str(y_json) {
754        Ok(v) => v,
755        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
756    };
757
758    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
759        Ok(v) => v,
760        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
761    };
762
763    match diagnostics::durbin_watson_test(&y, &x_vars) {
764        Ok(output) => serde_json::to_string(&output)
765            .unwrap_or_else(|_| error_json("Failed to serialize Durbin-Watson test result")),
766        Err(e) => error_json(&e.to_string()),
767    }
768}
769
770// ============================================================================
771// Shapiro-Wilk Test (WASM wrapper)
772// ============================================================================
773
774/// Performs the Shapiro-Wilk test for normality via WASM.
775///
776/// The Shapiro-Wilk test is a powerful test for normality,
777/// especially for small to moderate sample sizes (3 ≤ n ≤ 5000). It tests
778/// the null hypothesis that the residuals are normally distributed.
779///
780/// # Arguments
781///
782/// * `y_json` - JSON array of response variable values
783/// * `x_vars_json` - JSON array of predictor arrays
784///
785/// # Returns
786///
787/// JSON string containing the W statistic (ranges from 0 to 1), p-value,
788/// and interpretation.
789///
790/// # Errors
791///
792/// Returns a JSON error object if parsing fails or domain check fails.
793#[cfg(feature = "wasm")]
794#[wasm_bindgen]
795pub fn shapiro_wilk_test(
796    y_json: &str,
797    x_vars_json: &str,
798) -> String {
799    if let Err(e) = check_domain() {
800        return error_to_json(&e);
801    }
802
803    let y: Vec<f64> = match serde_json::from_str(y_json) {
804        Ok(v) => v,
805        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
806    };
807
808    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
809        Ok(v) => v,
810        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
811    };
812
813    match diagnostics::shapiro_wilk_test(&y, &x_vars) {
814        Ok(output) => serde_json::to_string(&output)
815            .unwrap_or_else(|_| error_json("Failed to serialize Shapiro-Wilk test result")),
816        Err(e) => error_json(&e.to_string()),
817    }
818}
819
820/// Performs the Anderson-Darling test for normality via WASM.
821///
822/// The Anderson-Darling test checks whether the residuals are normally distributed
823/// by comparing the empirical distribution to the expected normal distribution.
824/// This test is particularly sensitive to deviations in the tails of the distribution.
825/// A significant p-value suggests that the residuals deviate from normality.
826///
827/// # Arguments
828///
829/// * `y_json` - JSON array of response variable values
830/// * `x_vars_json` - JSON array of predictor arrays
831///
832/// # Returns
833///
834/// JSON string containing the A² statistic, p-value, and interpretation.
835///
836/// # Errors
837///
838/// Returns a JSON error object if parsing fails or domain check fails.
839#[cfg(feature = "wasm")]
840#[wasm_bindgen]
841pub fn anderson_darling_test(
842    y_json: &str,
843    x_vars_json: &str,
844) -> String {
845    if let Err(e) = check_domain() {
846        return error_to_json(&e);
847    }
848
849    let y: Vec<f64> = match serde_json::from_str(y_json) {
850        Ok(v) => v,
851        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
852    };
853
854    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
855        Ok(v) => v,
856        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
857    };
858
859    match diagnostics::anderson_darling_test(&y, &x_vars) {
860        Ok(output) => serde_json::to_string(&output)
861            .unwrap_or_else(|_| error_json("Failed to serialize Anderson-Darling test result")),
862        Err(e) => error_json(&e.to_string()),
863    }
864}
865
866// ============================================================================
867// Cook's Distance (WASM wrapper)
868// ============================================================================
869
870/// Computes Cook's distance for identifying influential observations via WASM.
871///
872/// Cook's distance measures how much each observation influences the regression
873/// model by comparing coefficient estimates with and without that observation.
874/// Unlike hypothesis tests, this is an influence measure - not a test with p-values.
875///
876/// # Arguments
877///
878/// * `y_json` - JSON array of response variable values
879/// * `x_vars_json` - JSON array of predictor arrays
880///
881/// # Returns
882///
883/// JSON string containing:
884/// - Vector of Cook's distances (one per observation)
885/// - Thresholds for identifying influential observations
886/// - Indices of potentially influential observations
887/// - Interpretation and guidance
888///
889/// # Errors
890///
891/// Returns a JSON error object if parsing fails or domain check fails.
892#[cfg(feature = "wasm")]
893#[wasm_bindgen]
894pub fn cooks_distance_test(
895    y_json: &str,
896    x_vars_json: &str,
897) -> String {
898    if let Err(e) = check_domain() {
899        return error_to_json(&e);
900    }
901
902    let y: Vec<f64> = match serde_json::from_str(y_json) {
903        Ok(v) => v,
904        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
905    };
906
907    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
908        Ok(v) => v,
909        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
910    };
911
912    match diagnostics::cooks_distance_test(&y, &x_vars) {
913        Ok(output) => serde_json::to_string(&output)
914            .unwrap_or_else(|_| error_json("Failed to serialize Cook's distance result")),
915        Err(e) => error_json(&e.to_string()),
916    }
917}
918
919// ============================================================================
920// Regularized Regression WASM Wrappers
921// ============================================================================
922
923#[cfg(feature = "wasm")]
924#[wasm_bindgen]
925/// Performs Ridge regression via WASM.
926///
927/// Ridge regression adds an L2 penalty to the coefficients, which helps with
928/// multicollinearity and overfitting. The intercept is never penalized.
929///
930/// # Arguments
931///
932/// * `y_json` - JSON array of response variable values
933/// * `x_vars_json` - JSON array of predictor arrays
934/// * `variable_names` - JSON array of variable names
935/// * `lambda` - Regularization strength (>= 0, typical range 0.01 to 100)
936/// * `standardize` - Whether to standardize predictors (recommended: true)
937///
938/// # Returns
939///
940/// JSON string containing:
941/// - `lambda` - Lambda value used
942/// - `intercept` - Intercept coefficient
943/// - `coefficients` - Slope coefficients
944/// - `fitted_values` - Predictions on training data
945/// - `residuals` - Residuals (y - fitted_values)
946/// - `df` - Effective degrees of freedom
947///
948/// # Errors
949///
950/// Returns a JSON error object if parsing fails, lambda is negative,
951/// or domain check fails.
952pub fn ridge_regression(
953    y_json: &str,
954    x_vars_json: &str,
955    _variable_names: &str,
956    lambda: f64,
957    standardize: bool,
958) -> String {
959    if let Err(e) = check_domain() {
960        return error_to_json(&e);
961    }
962
963    // Parse JSON input
964    let y: Vec<f64> = match serde_json::from_str(y_json) {
965        Ok(v) => v,
966        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
967    };
968
969    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
970        Ok(v) => v,
971        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
972    };
973
974    // Build design matrix with intercept column
975    let n = y.len();
976    let p = x_vars.len();
977
978    if n <= p + 1 {
979        return error_json(&format!(
980            "Insufficient data: need at least {} observations for {} predictors",
981            p + 2,
982            p
983        ));
984    }
985
986    let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
987    for (j, x_var) in x_vars.iter().enumerate() {
988        if x_var.len() != n {
989            return error_json(&format!(
990                "x_vars[{}] has {} elements, expected {}",
991                j,
992                x_var.len(),
993                n
994            ));
995        }
996        for (i, &val) in x_var.iter().enumerate() {
997            x_data[i * (p + 1) + j + 1] = val;
998        }
999    }
1000
1001    let x = linalg::Matrix::new(n, p + 1, x_data);
1002
1003    // Configure ridge options
1004    let options = regularized::ridge::RidgeFitOptions {
1005        lambda,
1006        intercept: true,
1007        standardize,
1008    };
1009
1010    match regularized::ridge::ridge_fit(&x, &y, &options) {
1011        Ok(output) => serde_json::to_string(&output)
1012            .unwrap_or_else(|_| error_json("Failed to serialize ridge regression result")),
1013        Err(e) => error_json(&e.to_string()),
1014    }
1015}
1016
1017#[cfg(feature = "wasm")]
1018#[wasm_bindgen]
1019/// Performs Lasso regression via WASM.
1020///
1021/// Lasso regression adds an L1 penalty to the coefficients, which performs
1022/// automatic variable selection by shrinking some coefficients to exactly zero.
1023/// The intercept is never penalized.
1024///
1025/// # Arguments
1026///
1027/// * `y_json` - JSON array of response variable values
1028/// * `x_vars_json` - JSON array of predictor arrays
1029/// * `variable_names` - JSON array of variable names
1030/// * `lambda` - Regularization strength (>= 0, typical range 0.01 to 10)
1031/// * `standardize` - Whether to standardize predictors (recommended: true)
1032///
1033/// # Returns
1034///
1035/// JSON string containing:
1036/// - `lambda` - Lambda value used
1037/// - `intercept` - Intercept coefficient
1038/// - `coefficients` - Slope coefficients (some may be exactly zero)
1039/// - `fitted_values` - Predictions on training data
1040/// - `residuals` - Residuals (y - fitted_values)
1041/// - `n_nonzero` - Number of non-zero coefficients (excluding intercept)
1042/// - `iterations` - Number of coordinate descent iterations
1043/// - `converged` - Whether the algorithm converged
1044///
1045/// # Errors
1046///
1047/// Returns a JSON error object if parsing fails, lambda is negative,
1048/// or domain check fails.
1049pub fn lasso_regression(
1050    y_json: &str,
1051    x_vars_json: &str,
1052    _variable_names: &str,
1053    lambda: f64,
1054    standardize: bool,
1055) -> String {
1056    if let Err(e) = check_domain() {
1057        return error_to_json(&e);
1058    }
1059
1060    // Parse JSON input
1061    let y: Vec<f64> = match serde_json::from_str(y_json) {
1062        Ok(v) => v,
1063        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
1064    };
1065
1066    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
1067        Ok(v) => v,
1068        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
1069    };
1070
1071    // Build design matrix with intercept column
1072    let n = y.len();
1073    let p = x_vars.len();
1074
1075    if n <= p + 1 {
1076        return error_json(&format!(
1077            "Insufficient data: need at least {} observations for {} predictors",
1078            p + 2,
1079            p
1080        ));
1081    }
1082
1083    let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
1084    for (j, x_var) in x_vars.iter().enumerate() {
1085        if x_var.len() != n {
1086            return error_json(&format!(
1087                "x_vars[{}] has {} elements, expected {}",
1088                j,
1089                x_var.len(),
1090                n
1091            ));
1092        }
1093        for (i, &val) in x_var.iter().enumerate() {
1094            x_data[i * (p + 1) + j + 1] = val;
1095        }
1096    }
1097
1098    let x = linalg::Matrix::new(n, p + 1, x_data);
1099
1100    // Configure lasso options
1101    let options = regularized::lasso::LassoFitOptions {
1102        lambda,
1103        intercept: true,
1104        standardize,
1105        ..Default::default()
1106    };
1107
1108    match regularized::lasso::lasso_fit(&x, &y, &options) {
1109        Ok(output) => serde_json::to_string(&output)
1110            .unwrap_or_else(|_| error_json("Failed to serialize lasso regression result")),
1111        Err(e) => error_json(&e.to_string()),
1112    }
1113}
1114
1115#[cfg(feature = "wasm")]
1116#[wasm_bindgen]
1117/// Generates a lambda path for regularized regression via WASM.
1118///
1119/// Creates a logarithmically-spaced sequence of lambda values from lambda_max
1120/// (where all penalized coefficients are zero) down to lambda_min. This is
1121/// useful for fitting regularization paths and selecting optimal lambda via
1122/// cross-validation.
1123///
1124/// # Arguments
1125///
1126/// * `y_json` - JSON array of response variable values
1127/// * `x_vars_json` - JSON array of predictor arrays
1128/// * `n_lambda` - Number of lambda values to generate (default: 100)
1129/// * `lambda_min_ratio` - Ratio for smallest lambda (default: 0.0001 if n >= p, else 0.01)
1130///
1131/// # Returns
1132///
1133/// JSON string containing:
1134/// - `lambda_path` - Array of lambda values in decreasing order
1135/// - `lambda_max` - Maximum lambda value
1136/// - `lambda_min` - Minimum lambda value
1137/// - `n_lambda` - Number of lambda values
1138///
1139/// # Errors
1140///
1141/// Returns a JSON error object if parsing fails or domain check fails.
1142pub fn make_lambda_path(
1143    y_json: &str,
1144    x_vars_json: &str,
1145    n_lambda: usize,
1146    lambda_min_ratio: f64,
1147) -> String {
1148    if let Err(e) = check_domain() {
1149        return error_to_json(&e);
1150    }
1151
1152    // Parse JSON input
1153    let y: Vec<f64> = match serde_json::from_str(y_json) {
1154        Ok(v) => v,
1155        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
1156    };
1157
1158    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
1159        Ok(v) => v,
1160        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
1161    };
1162
1163    // Build design matrix with intercept column
1164    let n = y.len();
1165    let p = x_vars.len();
1166
1167    let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
1168    for (j, x_var) in x_vars.iter().enumerate() {
1169        if x_var.len() != n {
1170            return error_json(&format!(
1171                "x_vars[{}] has {} elements, expected {}",
1172                j,
1173                x_var.len(),
1174                n
1175            ));
1176        }
1177        for (i, &val) in x_var.iter().enumerate() {
1178            x_data[i * (p + 1) + j + 1] = val;
1179        }
1180    }
1181
1182    let x = linalg::Matrix::new(n, p + 1, x_data);
1183
1184    // Standardize X for lambda path computation
1185    let x_mean: Vec<f64> = (0..x.cols)
1186        .map(|j| {
1187            if j == 0 {
1188                1.0 // Intercept column
1189            } else {
1190                (0..n).map(|i| x.get(i, j)).sum::<f64>() / n as f64
1191            }
1192        })
1193        .collect();
1194
1195    let x_std: Vec<f64> = (0..x.cols)
1196        .map(|j| {
1197            if j == 0 {
1198                0.0 // Intercept column - no centering
1199            } else {
1200                let mean = x_mean[j];
1201                let variance = (0..n).map(|i| (x.get(i, j) - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
1202                variance.sqrt()
1203            }
1204        })
1205        .collect();
1206
1207    // Build standardized X matrix
1208    let mut x_std_data = vec![1.0; n * (p + 1)];
1209    for j in 0..x.cols {
1210        for i in 0..n {
1211            if j == 0 {
1212                x_std_data[i * (p + 1)] = 1.0; // Intercept
1213            } else {
1214                let std = x_std[j];
1215                if std > 1e-10 {
1216                    x_std_data[i * (p + 1) + j] = (x.get(i, j) - x_mean[j]) / std;
1217                } else {
1218                    x_std_data[i * (p + 1) + j] = 0.0;
1219                }
1220            }
1221        }
1222    }
1223    let x_std = linalg::Matrix::new(n, p + 1, x_std_data);
1224
1225    // Center y
1226    let y_mean: f64 = y.iter().sum::<f64>() / n as f64;
1227    let y_centered: Vec<f64> = y.iter().map(|&yi| yi - y_mean).collect();
1228
1229    // Configure lambda path options
1230    let options = regularized::path::LambdaPathOptions {
1231        nlambda: n_lambda.max(1),
1232        lambda_min_ratio: if lambda_min_ratio > 0.0 { Some(lambda_min_ratio) } else { None },
1233        alpha: 1.0, // Lasso
1234        ..Default::default()
1235    };
1236
1237    let lambda_path = regularized::path::make_lambda_path(&x_std, &y_centered, &options, None, Some(0));
1238
1239    let lambda_max = lambda_path.first().copied().unwrap_or(0.0);
1240    let lambda_min = lambda_path.last().copied().unwrap_or(0.0);
1241
1242    // Return as JSON
1243    let result = serde_json::json!({
1244        "lambda_path": lambda_path,
1245        "lambda_max": lambda_max,
1246        "lambda_min": lambda_min,
1247        "n_lambda": lambda_path.len()
1248    });
1249
1250    result.to_string()
1251}
1252
1253// ============================================================================
1254// Statistical Utility Functions (WASM wrappers)
1255// ============================================================================
1256
1257#[cfg(feature = "wasm")]
1258#[wasm_bindgen]
1259/// Computes the Student's t-distribution cumulative distribution function.
1260///
1261/// Returns P(T ≤ t) for a t-distribution with the given degrees of freedom.
1262///
1263/// # Arguments
1264///
1265/// * `t` - t-statistic value
1266/// * `df` - Degrees of freedom
1267///
1268/// # Returns
1269///
1270/// The CDF value, or `NaN` if domain check fails.
1271pub fn get_t_cdf(t: f64, df: f64) -> f64 {
1272    if check_domain().is_err() {
1273        return f64::NAN;
1274    }
1275
1276    student_t_cdf(t, df)
1277}
1278
1279#[cfg(feature = "wasm")]
1280#[wasm_bindgen]
1281/// Computes the critical t-value for a given significance level.
1282///
1283/// Returns the t-value such that the area under the t-distribution curve
1284/// to the right equals alpha/2 (two-tailed test).
1285///
1286/// # Arguments
1287///
1288/// * `alpha` - Significance level (typically 0.05 for 95% confidence)
1289/// * `df` - Degrees of freedom
1290///
1291/// # Returns
1292///
1293/// The critical t-value, or `NaN` if domain check fails.
1294pub fn get_t_critical(alpha: f64, df: f64) -> f64 {
1295    if check_domain().is_err() {
1296        return f64::NAN;
1297    }
1298
1299    core::t_critical_quantile(df, alpha)
1300}
1301
1302#[cfg(feature = "wasm")]
1303#[wasm_bindgen]
1304/// Computes the inverse of the standard normal CDF (probit function).
1305///
1306/// Returns the z-score such that P(Z ≤ z) = p for a standard normal distribution.
1307///
1308/// # Arguments
1309///
1310/// * `p` - Probability (0 < p < 1)
1311///
1312/// # Returns
1313///
1314/// The z-score, or `NaN` if domain check fails.
1315pub fn get_normal_inverse(p: f64) -> f64 {
1316    if check_domain().is_err() {
1317        return f64::NAN;
1318    }
1319
1320    normal_inverse_cdf(p)
1321}
1322
1323// ============================================================================
1324// Domain Check (WASM-only)
1325// ============================================================================
1326//
1327// By default, all domains are allowed. To enable domain restriction, set the
1328// LINREG_DOMAIN_RESTRICT environment variable at build time:
1329//
1330//   LINREG_DOMAIN_RESTRICT=example.com,yoursite.com wasm-pack build
1331//
1332// Example for jesse-anderson.net:
1333//   LINREG_DOMAIN_RESTRICT=jesse-anderson.net,tools.jesse-anderson.net,localhost,127.0.0.1 wasm-pack build
1334//
1335// This allows downstream users to use the library without modification while
1336// still providing domain restriction as an opt-in security feature.
1337
1338#[cfg(feature = "wasm")]
1339fn check_domain() -> Result<()> {
1340    // Read allowed domains from build-time environment variable
1341    let allowed_domains = option_env!("LINREG_DOMAIN_RESTRICT");
1342
1343    match allowed_domains {
1344        Some(domains) if !domains.is_empty() => {
1345            // Domain restriction is enabled
1346            let window = web_sys::window().ok_or(Error::DomainCheck("No window found".to_string()))?;
1347            let location = window.location();
1348            let hostname = location.hostname().map_err(|_| Error::DomainCheck("No hostname found".to_string()))?;
1349
1350            let domain_list: Vec<&str> = domains.split(',').map(|s| s.trim()).collect();
1351
1352            if domain_list.contains(&hostname.as_str()) {
1353                Ok(())
1354            } else {
1355                Err(Error::DomainCheck(format!(
1356                    "Unauthorized domain: {}. Allowed: {}",
1357                    hostname, domains
1358                )))
1359            }
1360        }
1361        _ => {
1362            // No restriction - allow all domains
1363            Ok(())
1364        }
1365    }
1366}
1367
1368// ============================================================================
1369// Test Functions (WASM-only)
1370// ============================================================================
1371
1372#[cfg(test)]
1373mod tests {
1374    use super::*;
1375
1376    #[test]
1377    fn verify_housing_regression_integrity() {
1378        let result = test_housing_regression_native();
1379        if let Err(e) = result {
1380            panic!("Regression test failed: {}", e);
1381        }
1382    }
1383}
1384
1385#[cfg(feature = "wasm")]
1386#[wasm_bindgen]
1387/// Simple test function to verify WASM is working.
1388///
1389/// Returns a success message confirming the WASM module loaded correctly.
1390///
1391/// # Errors
1392///
1393/// Returns a JSON error object if domain check fails.
1394pub fn test() -> String {
1395    if let Err(e) = check_domain() {
1396        return error_to_json(&e);
1397    }
1398    "Rust WASM is working!".to_string()
1399}
1400
1401#[cfg(feature = "wasm")]
1402#[wasm_bindgen]
1403/// Returns the current version of the library.
1404///
1405/// Returns the Cargo package version as a string (e.g., "0.1.0").
1406///
1407/// # Errors
1408///
1409/// Returns a JSON error object if domain check fails.
1410pub fn get_version() -> String {
1411    if let Err(e) = check_domain() {
1412        return error_to_json(&e);
1413    }
1414    env!("CARGO_PKG_VERSION").to_string()
1415}
1416
1417#[cfg(feature = "wasm")]
1418#[wasm_bindgen]
1419/// Test function for t-critical value computation.
1420///
1421/// Returns JSON with the computed t-critical value for the given parameters.
1422///
1423/// # Errors
1424///
1425/// Returns a JSON error object if domain check fails.
1426pub fn test_t_critical(df: f64, alpha: f64) -> String {
1427    if let Err(e) = check_domain() {
1428        return error_to_json(&e);
1429    }
1430    let t_crit = core::t_critical_quantile(df, alpha);
1431    format!(r#"{{"df": {}, "alpha": {}, "t_critical": {}}}"#, df, alpha, t_crit)
1432}
1433
1434#[cfg(feature = "wasm")]
1435#[wasm_bindgen]
1436/// Test function for confidence interval computation.
1437///
1438/// Returns JSON with the computed confidence interval for a coefficient.
1439///
1440/// # Errors
1441///
1442/// Returns a JSON error object if domain check fails.
1443pub fn test_ci(coef: f64, se: f64, df: f64, alpha: f64) -> String {
1444    if let Err(e) = check_domain() {
1445        return error_to_json(&e);
1446    }
1447    let t_crit = core::t_critical_quantile(df, alpha);
1448    format!(r#"{{"lower": {}, "upper": {}}}"#, coef - t_crit * se, coef + t_crit * se)
1449}
1450
1451#[cfg(feature = "wasm")]
1452#[wasm_bindgen]
1453/// Test function for R accuracy validation.
1454///
1455/// Returns JSON comparing our statistical functions against R reference values.
1456///
1457/// # Errors
1458///
1459/// Returns a JSON error object if domain check fails.
1460pub fn test_r_accuracy() -> String {
1461    if let Err(e) = check_domain() {
1462        return error_to_json(&e);
1463    }
1464    format!(
1465        r#"{{"two_tail_p": {}, "qt_975": {}}}"#,
1466        core::two_tailed_p_value(1.6717, 21.0),
1467        core::t_critical_quantile(21.0, 0.05)
1468    )
1469}
1470
1471#[cfg(feature = "wasm")]
1472#[wasm_bindgen]
1473/// Test function for regression validation against R reference values.
1474///
1475/// Runs a regression on a housing dataset and compares results against R's lm() output.
1476/// Returns JSON with status "PASS" or "FAIL" with details.
1477///
1478/// # Errors
1479///
1480/// Returns a JSON error object if domain check fails.
1481pub fn test_housing_regression() -> String {
1482    if let Err(e) = check_domain() {
1483        return error_to_json(&e);
1484    }
1485
1486    match test_housing_regression_native() {
1487        Ok(result) => result,
1488        Err(e) => serde_json::json!({ "status": "ERROR", "error": e.to_string() }).to_string()
1489    }
1490}
1491
1492// Native Rust test function (works without WASM feature)
1493fn test_housing_regression_native() -> Result<String> {
1494    let y = vec![
1495        245.5, 312.8, 198.4, 425.6, 278.9, 356.2, 189.5, 512.3, 234.7, 298.1,
1496        445.8, 167.9, 367.4, 289.6, 198.2, 478.5, 256.3, 334.7, 178.5, 398.9,
1497        223.4, 312.5, 156.8, 423.7, 267.9
1498    ];
1499
1500    let square_feet = vec![
1501        1200.0, 1800.0, 950.0, 2400.0, 1450.0, 2000.0, 1100.0, 2800.0, 1350.0, 1650.0,
1502        2200.0, 900.0, 1950.0, 1500.0, 1050.0, 2600.0, 1300.0, 1850.0, 1000.0, 2100.0,
1503        1250.0, 1700.0, 850.0, 2350.0, 1400.0
1504    ];
1505    let bedrooms = vec![
1506        3.0, 4.0, 2.0, 4.0, 3.0, 4.0, 2.0, 5.0, 3.0, 3.0,
1507        4.0, 2.0, 4.0, 3.0, 2.0, 5.0, 3.0, 4.0, 2.0, 4.0,
1508        3.0, 3.0, 2.0, 4.0, 3.0
1509    ];
1510    let age = vec![
1511        15.0, 10.0, 25.0, 5.0, 8.0, 12.0, 20.0, 2.0, 18.0, 7.0,
1512        3.0, 30.0, 6.0, 14.0, 22.0, 1.0, 16.0, 9.0, 28.0, 4.0,
1513        19.0, 11.0, 35.0, 3.0, 13.0
1514    ];
1515
1516    let x_vars = vec![square_feet, bedrooms, age];
1517    let names = vec!["Intercept".to_string(), "Square_Feet".to_string(), "Bedrooms".to_string(), "Age".to_string()];
1518
1519    let result = core::ols_regression(&y, &x_vars, &names)?;
1520
1521    // Check against R results
1522    let expected_coeffs = [52.1271333, 0.1613877, 0.9545492, -1.1811815];
1523    let expected_std_errs = [31.18201809, 0.01875072, 10.44400198, 0.73219949];
1524
1525    let tolerance = 1e-4;
1526    let mut mismatches = vec![];
1527
1528    for i in 0..4 {
1529        if (result.coefficients[i] - expected_coeffs[i]).abs() > tolerance {
1530            mismatches.push(format!("coeff[{}] differs: got {}, expected {}", i, result.coefficients[i], expected_coeffs[i]));
1531        }
1532        if (result.std_errors[i] - expected_std_errs[i]).abs() > tolerance {
1533            mismatches.push(format!("std_err[{}] differs: got {}, expected {}", i, result.std_errors[i], expected_std_errs[i]));
1534        }
1535    }
1536
1537    if mismatches.is_empty() {
1538        Ok(serde_json::json!({ "status": "PASS" }).to_string())
1539    } else {
1540        Ok(serde_json::json!({ "status": "FAIL", "mismatches": mismatches }).to_string())
1541    }
1542}