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