diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3795440 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [22.x, 24.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test diff --git a/README.md b/README.md index 19451ed..d00c47c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,432 @@ -apparatus +Apparatus ========= -A collection of low-level machine learning algorithms for node.js. +Apparatus is a collection of low-level machine learning algorithms for node.js. -This project is quite new and documentation will be on the way shortly. -In the meantime you can check out the spec folder for examples of how -to use the algorithms. - -Note that within "apparatus" the interface to the algorithms in +Note that within Apparatus the interface to the algorithms in primarily arrays of numbers and vectors. If you're looking for feature extraction from text or natural language check out the "natural" -[https://github.com/NaturalNode/natural](https://github.com/NaturalNode/natural) node package. "natural" uses +[https://github.com/NaturalNode/natural](https://github.com/NaturalNode/natural) node package. Natural uses many of these algorithms but adds a layer of natural language/text feature extraction. + +# Apparatus Machine Learning Algorithms Documentation + +Documentation of the low-level machine learning algorithms implemented in Apparatus. + +## Table of Contents + +1. [Classifiers](#classifiers) + - [Naïve Bayes](#naïve-bayes-classifier) + - [Logistic Regression](#logistic-regression-classifier) + - [Random Forest](#random-forest-classifier) +2. [Clusterers](#clusterers) + - [K-Means](#k-means-clustering) + +--- + +## Classifiers + +Classification algorithms predict discrete categories/labels for input data. + +### Naïve Bayes Classifier + +**Type:** Probabilistic classifier +**Use Cases:** Text classification, spam detection, sentiment analysis, category prediction +**Time Complexity:** O(n) training, O(1) prediction +**Space Complexity:** O(features × classes) + +#### Overview + +Naïve Bayes is a probabilistic classifier based on Bayes' theorem with the assumption that features are conditionally independent given the class label. + +**Formula:** + +$$P(C|X) = \frac{P(X|C) \times P(C)}{P(X)}$$ + +Where: +- **P(C|X)** = Posterior probability of class C given features X +- **P(X|C)** = Likelihood of features X given class C +- **P(C)** = Prior probability of class C +- **P(X)** = Probability of observing features X + +#### How It Works + +1. **Training Phase:** + - Count feature occurrences for each class + - Calculate feature probabilities per class + - Store class priors (how often each class appears) + +2. **Prediction Phase:** + - For input features, compute probability of each class + - Multiply: P(class) × P(feature1|class) × P(feature2|class) × ... + - Return class with highest probability + +#### Example + +```javascript +const BayesClassifier = require('./lib/apparatus/classifier/bayes_classifier'); + +const classifier = new BayesClassifier(); + +// Training: [feature_vector, label] +classifier.addExample([1, 0, 1, 0], 'spam'); +classifier.addExample([0, 1, 0, 1], 'ham'); +classifier.addExample([1, 1, 1, 0], 'spam'); +classifier.train(); + +// Prediction +const result = classifier.classify([1, 0, 1, 0]); // Returns 'spam' +``` + +#### Parameters + +- **Smoothing:** Laplace smoothing factor (default: 1.0) + - Prevents zero probabilities for unseen features + - Higher values = more smoothing + +--- + +### Logistic Regression Classifier + +**Type:** Linear classifier (multiclass support) +**Use Cases:** Multi-class classification, probability estimation, linear decision boundaries +**Time Complexity:** O(n × iterations × classes) training, O(features × classes) prediction +**Space Complexity:** O(features × classes) + +#### Overview + +Logistic Regression is a linear classification algorithm that uses the sigmoid function to map input to probabilities. It supports multiclass classification by training separate binary classifiers for each class (one-vs-all). + +**Formula:** + +$$P(y=1|x) = \frac{1}{1 + e^{-(\theta^T x)}}$$ + +Where: +- **θ** = Learned weights (one set per class) +- **x** = Feature vector +- Sigmoid function maps linear combination to [0, 1] + +#### How It Works + +1. **Initialization:** + - Start with random weights (θ) + - Define learning data and labels + +2. **Training via Gradient Descent:** + - Compute predictions using current weights + - Calculate cost (loss) using cross-entropy + - Update weights in direction of steepest descent + - Repeat for max iterations + +3. **Prediction:** + - Compute linear combination: z = θ^T × x + - Apply sigmoid: P = 1 / (1 + e^-z) + - If P > 0.5 → class 1, else → class 0 + +#### Example + +```javascript +const LogisticRegression = require('./lib/apparatus/classifier/logistic_regression_classifier'); + +const classifier = new LogisticRegression(); + +// Training: array of [features] and array of [labels] +const examples = [ + [2.1, 1.0], + [2.0, 0.9], + [8.0, 8.0], + [8.1, 7.9] +]; +const labels = [0, 0, 1, 1]; + +classifier.addExample(examples[0], labels[0]); +classifier.addExample(examples[1], labels[1]); +classifier.addExample(examples[2], labels[2]); +classifier.addExample(examples[3], labels[3]); +classifier.train(); + +// Prediction (returns probability or class) +const prob = classifier.classify([2.0, 1.0]); // Close to 0 +const prob2 = classifier.classify([8.0, 8.0]); // Close to 1 +``` + +#### Parameters + +- **Learning Rate:** Controls step size in gradient descent +- **Max Iterations:** Maximum training iterations +- **Regularization:** Optional L2 regularization to prevent overfitting + +--- + +### Random Forest Classifier + +**Type:** Ensemble classifier (decision trees) - **Binary classification only** +**Use Cases:** Binary classification, feature importance, non-linear boundaries +**Time Complexity:** O(n × k × log n) training, O(k × depth) prediction +**Space Complexity:** O(k × tree_nodes) +**Constraint:** Only supports binary classification with classes **1** and **-1** + +#### Overview + +Random Forest is an ensemble method that builds multiple decision trees and combines their predictions. Each tree is trained on a random subset of data and features. + +**Important:** This implementation only supports binary classification where labels must be **1** or **-1**. + +#### How It Works + +1. **Forest Creation:** + - Build k decision trees (configurable) + - For each tree: bootstrap sample of training data + - At each node: randomly select subset of features + - Split on feature that maximizes information gain + +2. **Prediction:** + - Each tree predicts independently + - Return majority vote (classification) or average (regression) + +3. **Feature Importance:** + - Track how often features are used for splits + - Normalize by depth/impact + +#### Example + +```javascript +const RandomForest = require('./lib/apparatus/classifier/randomforest_classifier'); + +const classifier = new RandomForest(); + +// Add training examples - MUST use labels 1 or -1 +classifier.addExample([2.0, 1.0], 1); +classifier.addExample([2.1, 0.9], 1); +classifier.addExample([2.0, 1.1], 1); +classifier.addExample([8.0, 8.0], -1); +classifier.addExample([8.1, 7.9], -1); +classifier.addExample([8.0, 8.1], -1); + +// Train with configuration +classifier.train({ numTrees: 10, maxDepth: 4 }); + +// Predict - returns probability and classifications +const result = classifier.classify([2.0, 1.0]); // Class 1 +const result2 = classifier.classify([8.0, 8.0]); // Class -1 +``` + +#### Parameters + +- **numTrees:** Number of trees to build (default: 100) + - Higher = better accuracy, slower training + - Typical: 10-100 + +- **maxDepth:** Maximum tree depth (default: 4) + - Controls tree complexity + - Prevents overfitting + +- **numTries:** Random hypotheses at each node (default: 10) + - Controls randomness in feature selection + +--- + +## Clusterers + +Clustering algorithms partition data into groups without labeled targets. + +### K-Means Clustering + +**Type:** Unsupervised clustering (centroid-based) +**Use Cases:** Customer segmentation, image compression, document clustering, anomaly detection +**Time Complexity:** O(n × k × iterations × d) +**Space Complexity:** O(n × d) + +#### Overview + +K-Means is an iterative algorithm that partitions data into k clusters by minimizing within-cluster variance. The goal is to find cluster centers (centroids) that minimize the sum of squared distances to all points. + +**Objective Function:** + +$$J = \sum_{i=1}^{k} \sum_{x \in C_i} ||x - \mu_i||^2$$ + +Where: +- **k** = Number of clusters +- **C_i** = Set of points in cluster i +- **μ_i** = Centroid of cluster i +- **||x - μ_i||²** = Squared Euclidean distance + +#### Algorithm Steps + +1. **Initialization:** + - Select k initial centroids + - Options: random, k-means++, or user-provided + +2. **Assignment Step:** + - For each point: assign to nearest centroid + - Calculate distances to all k centroids + +3. **Update Step:** + - Recompute each centroid as mean of assigned points + - Move centroids toward cluster center + +4. **Convergence Check:** + - If centroids changed → repeat from Assignment + - Else → done! + +#### Variants in Apparatus + +**Legacy Implementation (kmeans-legacy.js):** +- Uses Sylvester matrix library +- Implemented circa 2011 +- Slower but foundational + +**Modernized Implementation (kmeans-modernized.js):** +- Pure JavaScript arrays (no external dependencies) +- K-means++ initialization (better starting points) +- Multiple restart support (finds better solutions) +- **7.6x faster** than legacy (see benchmark) +- Thread-safe and scalable + +#### K-Means++ Initialization + +Instead of random centroids, k-means++ selects initial centers probabilistically: + +1. Choose first centroid randomly from data +2. For i = 2 to k: + - Probability of selecting point x is proportional to D(x)² + - D(x) = distance to nearest existing centroid +3. Result: initial centers spread out, converges faster + +#### Example + +```javascript +// Modernized version (recommended) +const KMeans = require('./lib/apparatus/clusterer/kmeans'); + +const data = [ + [1, 1], + [2, 2], + [10, 10], + [11, 11] +]; + +// Legacy API (backwards compatible) +const kmeans = new KMeans(data); +const result = kmeans.cluster(2); + +// Access results via Sylvester-compatible API +console.log(result.elements.length); // 4 assignments +console.log(result.e(1)); // Cluster of point 1 (1-indexed) +console.log(result.e(2)); // Cluster of point 2 +// etc. +``` + +#### Advanced Example with Modernized API + +```javascript +const KMeansModernized = require('./lib/apparatus/clusterer/kmeans-modernized'); + +const data = [[1,1], [2,2], [10,10], [11,11]]; + +// More control with modernized API +const kmeans = new KMeansModernized(2, { + maxIterations: 100, // Max iterations + tolerance: 0.0001, // Convergence threshold + initialization: 'kmeans++', // 'random' or 'kmeans++' + seed: 42 // For reproducible results +}); + +kmeans.fit(data); + +// Get results +const assignments = kmeans.getAssignments(); // [0, 0, 1, 1] +const clusters = kmeans.getClusters(); // [[0, 1], [2, 3]] +const centroids = kmeans.getCentroids(); // [[1.5, 1.5], [10.5, 10.5]] +``` + +#### Performance Comparison + +See `benchmark/kmeans_benchmark.js` for full results: + +| Dataset | Points | Modernized | Legacy | Speedup | +|---------|--------|------------|--------|---------| +| Random | 100 | 0.17 ms | 0.81 ms | **4.8x** | +| Small | 30 | 0.01 ms | 0.10 ms | **6.9x** | +| Medium | 250 | 0.18 ms | 1.16 ms | **6.3x** | +| Large | 1,000 | 0.88 ms | 9.14 ms | **10.4x** | +| Very Large | 5,000 | 4.07 ms | 44.71 ms | **11.0x** | + +**Average speedup: 7.6x faster, 6.3% more memory** + +#### Parameters + +**KMeansModernized Constructor:** + +```javascript +new KMeansModernized(k, options) +``` + +- **k** (required): Number of clusters +- **options.maxIterations**: Max iterations (default: 100) +- **options.tolerance**: Convergence tolerance (default: 0.0001) +- **options.initialization**: + - `'random'` - Random centroid selection + - `'kmeans++'` - K-means++ initialization (recommended) +- **options.seed**: Random seed for reproducibility +- **options.restarts**: Multiple runs to find better solution +- **options.distanceFunction**: Custom distance metric (default: Euclidean) + + +## Comparison Table + +| Algorithm | Type | Classes | Training | Prediction | Complexity | Best Use Case | +|-----------|------|---------|----------|-----------|-----------|---------------| +| Naïve Bayes | Classification | Multi | O(n) | O(1) | Low | Text, fast baseline | +| Logistic Regression | Classification | Multi | O(n×iter×c) | O(d×c) | Low-Mid | Linear boundaries, multiclass | +| Random Forest | Classification | **Binary only (±1)** | O(n×k×log n) | O(k×depth) | High | Non-linear binary problems | +| K-Means | Clustering | N/A | O(n×k×iter×d) | O(k×d) | Mid | Segmentation, exploration | + +**Note:** d = dimensions, n = samples, k = clusters/trees, c = classes, iter = iterations + +--- + +## Installation & Usage + +### Install Apparatus + +```bash +npm install apparatus +``` + +### Basic Example + +```javascript +const BayesClassifier = require('apparatus/lib/apparatus/classifier/bayes_classifier'); +const KMeans = require('apparatus/lib/apparatus/clusterer/kmeans'); + +// Classification +const classifier = new BayesClassifier(); +classifier.addExample([1, 0], 'class_a'); +classifier.addExample([0, 1], 'class_b'); +classifier.train(); +console.log(classifier.classify([1, 0])); // 'class_a' + +// Clustering +const kmeans = new KMeans([[1,1], [2,2], [10,10]]); +const result = kmeans.cluster(2); +console.log(result.elements); // [1, 1, 2] +``` + +--- + +## References & Further Reading + +- **Naïve Bayes:** [Wikipedia](https://en.wikipedia.org/wiki/Naive_Bayes_classifier) +- **Logistic Regression:** [Wikipedia](https://en.wikipedia.org/wiki/Logistic_regression) +- **Random Forest:** [Breiman, 2001](https://en.wikipedia.org/wiki/Random_forest) +- **K-Means:** [MacQueen, 1967](https://en.wikipedia.org/wiki/K-means_clustering) +- **K-Means++:** [Arthur & Vassilvitskii, 2007](https://en.wikipedia.org/wiki/K-means%2B%2B) + +--- + +**Last Updated:** February 24, 2026 +**Apparatus Version:** 0.0.11 + diff --git a/benchmark/kmeans_benchmark.js b/benchmark/kmeans_benchmark.js new file mode 100644 index 0000000..871f68c --- /dev/null +++ b/benchmark/kmeans_benchmark.js @@ -0,0 +1,225 @@ +/* +K-Means Performance Benchmark +Comparing legacy (Sylvester-based) vs modernized implementations +*/ + +const KMeansLegacy = require('../lib/apparatus/clusterer/kmeans-legacy') +const KMeansModernized = require('../lib/apparatus/clusterer/kmeans-modernized') + +// Generate random dataset +function generateDataset (numPoints, dimensions) { + const data = [] + for (let i = 0; i < numPoints; i++) { + const point = [] + for (let j = 0; j < dimensions; j++) { + point.push(Math.random() * 100) + } + data.push(point) + } + return data +} + +// Generate clustered dataset (more realistic) +function generateClusteredDataset (numClusters, pointsPerCluster, dimensions) { + const data = [] + + for (let c = 0; c < numClusters; c++) { + // Random cluster center + const center = [] + for (let d = 0; d < dimensions; d++) { + center.push(Math.random() * 100) + } + + // Generate points around center + for (let p = 0; p < pointsPerCluster; p++) { + const point = [] + for (let d = 0; d < dimensions; d++) { + point.push(center[d] + (Math.random() - 0.5) * 10) + } + data.push(point) + } + } + + return data +} + +// Benchmark function +function benchmark (name, fn, iterations = 1) { + const start = process.hrtime.bigint() + const startMem = process.memoryUsage().heapUsed + + let result + for (let i = 0; i < iterations; i++) { + result = fn() + } + + const end = process.hrtime.bigint() + const endMem = process.memoryUsage().heapUsed + + const durationMs = Number(end - start) / 1000000 / iterations + const memoryDelta = (endMem - startMem) / 1024 / 1024 + + return { + name, + duration: durationMs, + memory: memoryDelta, + result + } +} + +// Run benchmark suite +function runBenchmark (datasetName, data, k, iterations = 5) { + console.log(`\n${'='.repeat(60)}`) + console.log(`Benchmark: ${datasetName}`) + console.log(`Dataset: ${data.length} points, ${data[0].length} dimensions, k=${k}`) + console.log(`Iterations: ${iterations}`) + console.log('='.repeat(60)) + + // Warm up + try { + new KMeansLegacy(data).cluster(k) + } catch (e) { + console.log('Legacy warmup failed (expected if Sylvester not installed)') + } + + const kmeansModern = new KMeansModernized(k) + kmeansModern.fit(data) + + // Benchmark Legacy + let legacyResult + try { + legacyResult = benchmark('KMeans Legacy', () => { + const kmeans = new KMeansLegacy(data) + return kmeans.cluster(k) + }, iterations) + } catch (error) { + legacyResult = { + name: 'KMeans Legacy', + duration: null, + memory: null, + error: error.message + } + } + + // Benchmark Modernized + const modernizedResult = benchmark('KMeans Modernized', () => { + const kmeans = new KMeansModernized(k, { maxIterations: 100 }) + kmeans.fit(data) + return kmeans.getAssignments() + }, iterations) + + // Results + console.log('\nResults:') + console.log('-'.repeat(60)) + + if (legacyResult.error) { + console.log(`Legacy: ERROR - ${legacyResult.error}`) + } else { + console.log(`Legacy: ${legacyResult.duration.toFixed(2)} ms, Memory: ${legacyResult.memory.toFixed(2)} MB`) + } + + console.log(`Modernized: ${modernizedResult.duration.toFixed(2)} ms, Memory: ${modernizedResult.memory.toFixed(2)} MB`) + + if (!legacyResult.error) { + const speedup = (legacyResult.duration / modernizedResult.duration).toFixed(2) + const faster = speedup > 1 ? 'Modernized' : 'Legacy' + const ratio = speedup > 1 ? speedup : (1 / speedup).toFixed(2) + + const memDiff = modernizedResult.memory - legacyResult.memory + const memComparison = memDiff < 0 + ? `${Math.abs(memDiff).toFixed(2)} MB less memory` + : `${memDiff.toFixed(2)} MB more memory` + + console.log(`\nComparison: ${faster} is ${ratio}x faster, ${memComparison}`) + } + + return { legacy: legacyResult, modernized: modernizedResult } +} + +// Main benchmark suite +console.log('\n' + '='.repeat(60)) +console.log('K-MEANS PERFORMANCE BENCHMARK') +console.log('='.repeat(60)) + +const results = [] + +// Test 1: Random dataset (worst case - no natural clusters) +const random = generateDataset(100, 2) +results.push(runBenchmark('Random Dataset (100 points, 2D)', random, 5, 5)) + +// Test 2: Small dataset +const small = generateClusteredDataset(3, 10, 2) +results.push(runBenchmark('Small Clustered Dataset (30 points, 2D)', small, 3, 10)) + +// Test 3: Medium dataset +const medium = generateClusteredDataset(5, 50, 2) +results.push(runBenchmark('Medium Clustered Dataset (250 points, 2D)', medium, 5, 5)) + +// Test 4: Large dataset +const large = generateClusteredDataset(10, 100, 2) +results.push(runBenchmark('Large Clustered Dataset (1000 points, 2D)', large, 10, 3)) + +// Test 5: High dimensional +const highDim = generateClusteredDataset(5, 50, 10) +results.push(runBenchmark('High Dimensional Clustered (250 points, 10D)', highDim, 5, 5)) + +// Test 6: Very large dataset +const veryLarge = generateClusteredDataset(10, 500, 2) +results.push(runBenchmark('Very Large Clustered Dataset (5000 points, 2D)', veryLarge, 10, 1)) + +// Summary +console.log('\n' + '='.repeat(60)) +console.log('SUMMARY') +console.log('='.repeat(60)) + +let modernizedWins = 0 +let totalSpeedup = 0 +let totalMemoryLegacy = 0 +let totalMemoryModernized = 0 +let validComparisons = 0 + +results.forEach((r, i) => { + if (!r.legacy.error && r.modernized.duration > 0) { + const speedup = r.legacy.duration / r.modernized.duration + if (speedup > 1) { + modernizedWins++ + } + totalSpeedup += speedup + totalMemoryLegacy += Math.abs(r.legacy.memory) + totalMemoryModernized += Math.abs(r.modernized.memory) + validComparisons++ + } +}) + +if (validComparisons > 0) { + const avgSpeedup = (totalSpeedup / validComparisons).toFixed(2) + const avgMemLegacy = (totalMemoryLegacy / validComparisons).toFixed(2) + const avgMemModernized = (totalMemoryModernized / validComparisons).toFixed(2) + + console.log('\nPerformance:') + console.log(` Modernized won ${modernizedWins}/${validComparisons} benchmarks`) + console.log(` Average speedup: ${avgSpeedup}x faster`) + + console.log('\nMemory Usage (average absolute delta):') + console.log(` Legacy: ${avgMemLegacy} MB`) + console.log(` Modernized: ${avgMemModernized} MB`) + + if (totalMemoryModernized < totalMemoryLegacy) { + const memSavings = ((1 - totalMemoryModernized / totalMemoryLegacy) * 100).toFixed(1) + console.log(` Modernized uses ${memSavings}% less memory on average`) + } else { + const memIncrease = ((totalMemoryModernized / totalMemoryLegacy - 1) * 100).toFixed(1) + console.log(` Modernized uses ${memIncrease}% more memory on average`) + } + + console.log(`\nOverall: Modernized is ${avgSpeedup}x faster`) +} else { + console.log('Legacy implementation requires Sylvester library to be installed.') + console.log('Install with: npm install sylvester') + console.log('\nModernized implementation:') + results.forEach((r, i) => { + console.log(` Test ${i + 1}: ${r.modernized.duration.toFixed(2)} ms (${r.modernized.memory.toFixed(2)} MB)`) + }) +} + +console.log('\n' + '='.repeat(60)) diff --git a/benchmark/logistic_regression_benchmark.js b/benchmark/logistic_regression_benchmark.js new file mode 100644 index 0000000..628a6bc --- /dev/null +++ b/benchmark/logistic_regression_benchmark.js @@ -0,0 +1,190 @@ +/* +Benchmark comparing Logistic Regression Classifier implementations +Original (Sylvester-based) vs Modernized (direct array manipulation) +*/ + +'use strict' + +const LogisticRegressionLegacy = require('../lib/apparatus/classifier/logistic-regression-legacy') +const LogisticRegressionModernized = require('../lib/apparatus/classifier/logistic-regression-modernized') + +/** + * Generate random training data + */ +function generateTrainingData (numSamples, numFeatures, numClasses) { + const data = [] + + for (let classIdx = 0; classIdx < numClasses; classIdx++) { + const samplesPerClass = numSamples / numClasses + for (let i = 0; i < samplesPerClass; i++) { + const example = new Array(numFeatures) + for (let j = 0; j < numFeatures; j++) { + // Generate features biased by class + example[j] = Math.random() + (classIdx * 0.5) + } + data.push({ + features: example, + label: `class_${classIdx}` + }) + } + } + + return data +} + +/** + * Measure memory usage in MB + */ +function getMemoryUsage () { + if (global.gc) { + global.gc() + } + const used = process.memoryUsage() + return { + heapUsed: Math.round(used.heapUsed / 1024 / 1024 * 100) / 100, + heapTotal: Math.round(used.heapTotal / 1024 / 1024 * 100) / 100, + external: Math.round(used.external / 1024 / 1024 * 100) / 100 + } +} + +/** + * Benchmark training phase + */ +function benchmarkTraining (ClassifierClass, data, label) { + const classifier = new ClassifierClass() + + // Measure memory before training + const memBefore = getMemoryUsage() + const startTime = process.hrtime.bigint() + + // Add examples + for (let i = 0; i < data.length; i++) { + classifier.addExample(data[i].features, data[i].label) + } + + // Train + classifier.train() + + const endTime = process.hrtime.bigint() + const memAfter = getMemoryUsage() + + const timeMs = Number(endTime - startTime) / 1000000 // Convert to milliseconds + const heapUsedDiff = memAfter.heapUsed - memBefore.heapUsed + + return { + label, + trainingTime: Math.round(timeMs * 100) / 100, + memoryUsed: Math.round(heapUsedDiff * 100) / 100, + classifier + } +} + +/** + * Benchmark classification phase + */ +function benchmarkClassification (classifier, testData, label) { + const memBefore = getMemoryUsage() + const startTime = process.hrtime.bigint() + + // Classify all test samples + let correctCount = 0 + for (let i = 0; i < testData.length; i++) { + const result = classifier.getClassifications(testData[i].features) + // Check if top prediction matches actual label + if (result[0].label === testData[i].label) { + correctCount++ + } + } + + const endTime = process.hrtime.bigint() + const memAfter = getMemoryUsage() + + const timeMs = Number(endTime - startTime) / 1000000 + const heapUsedDiff = memAfter.heapUsed - memBefore.heapUsed + const accuracy = Math.round((correctCount / testData.length) * 10000) / 100 + + return { + label, + classificationTime: Math.round(timeMs * 100) / 100, + memoryUsed: Math.round(heapUsedDiff * 100) / 100, + accuracy, + sampleCount: testData.length + } +} + +/** + * Main benchmark + */ +function runBenchmark () { + console.log('='.repeat(80)) + console.log('Logistic Regression Classifier - Performance Benchmark') + console.log('='.repeat(80)) + console.log() + + const testConfigs = [ + { samples: 100, features: 10, classes: 2, name: 'Small (100 samples)' }, + { samples: 500, features: 20, classes: 3, name: 'Medium (500 samples)' }, + { samples: 2000, features: 50, classes: 5, name: 'Large (2000 samples)' } + ] + + for (const config of testConfigs) { + console.log(`\n${'='.repeat(80)}`) + console.log(`Dataset: ${config.name}`) + console.log(`Features: ${config.features}, Classes: ${config.classes}`) + console.log('-'.repeat(80)) + + // Generate data split + const allData = generateTrainingData(config.samples, config.features, config.classes) + const trainSize = Math.floor(allData.length * 0.8) + const trainData = allData.slice(0, trainSize) + const testData = allData.slice(trainSize) + + console.log(`Training samples: ${trainData.length}, Test samples: ${testData.length}`) + console.log() + + // Benchmark legacy (Sylvester) + console.log('Legacy Classifier (Sylvester):') + const legacyTrain = benchmarkTraining(LogisticRegressionLegacy, trainData, 'Legacy') + console.log(` Training time: ${legacyTrain.trainingTime} ms`) + console.log(` Memory used: ${legacyTrain.memoryUsed} MB`) + + const legacyClass = benchmarkClassification(legacyTrain.classifier, testData, 'Legacy') + console.log(` Classification time: ${legacyClass.classificationTime} ms (${testData.length} samples)`) + console.log(` Memory used: ${legacyClass.memoryUsed} MB`) + console.log(` Accuracy: ${legacyClass.accuracy}%`) + console.log() + + // Benchmark modernized + console.log('Modernized Classifier (Direct arrays):') + const modernTrain = benchmarkTraining(LogisticRegressionModernized, trainData, 'Modernized') + console.log(` Training time: ${modernTrain.trainingTime} ms`) + console.log(` Memory used: ${modernTrain.memoryUsed} MB`) + + const modernClass = benchmarkClassification(modernTrain.classifier, testData, 'Modernized') + console.log(` Classification time: ${modernClass.classificationTime} ms (${testData.length} samples)`) + console.log(` Memory used: ${modernClass.memoryUsed} MB`) + console.log(` Accuracy: ${modernClass.accuracy}%`) + console.log() + + // Calculate improvements + const speedupTrain = Math.round((legacyTrain.trainingTime / modernTrain.trainingTime) * 100) / 100 + const speedupClass = Math.round((legacyClass.classificationTime / modernClass.classificationTime) * 100) / 100 + const memSavingsTrain = Math.round((legacyTrain.memoryUsed - modernTrain.memoryUsed) * 100) / 100 + const memSavingsClass = Math.round((legacyClass.memoryUsed - modernClass.memoryUsed) * 100) / 100 + + console.log('IMPROVEMENTS (Modernized vs Legacy):') + console.log(` Training speedup: ${speedupTrain}x faster`) + console.log(` Classification speedup: ${speedupClass}x faster`) + console.log(` Training memory saving: ${memSavingsTrain} MB (${Math.round((memSavingsTrain / Math.abs(legacyTrain.memoryUsed)) * 100)}%)`) + console.log(` Classification memory saving: ${memSavingsClass} MB (${Math.round((memSavingsClass / Math.abs(legacyClass.memoryUsed)) * 100)}%)`) + } + + console.log() + console.log('='.repeat(80)) + console.log('Benchmark Complete') + console.log('='.repeat(80)) +} + +// Run benchmark +console.log('\nNote: For accurate memory measurements, run with: node --expose-gc benchmark/logistic_regression_benchmark.js\n') +runBenchmark() diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..e60cf03 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,7 @@ +export { BayesClassifier } from './lib/apparatus/classifier/bayes_classifier' +export { LogisticRegressionModernized as LogisticRegressionClassifier } from './lib/apparatus/classifier/logistic_regression_classifier' +export { RandomForestClassifier } from './lib/apparatus/classifier/randomforest_classifier' +export { KMeans } from './lib/apparatus/clusterer/kmeans' + +export type { Classification } from './lib/apparatus/classifier/classifier' +export type { RandomForestOptions } from './lib/apparatus/classifier/randomforest_classifier' diff --git a/index.js b/index.js index a4de1e2..48e3295 100644 --- a/index.js +++ b/index.js @@ -1,2 +1 @@ - -module.exports = require('./lib/apparatus/'); +module.exports = require('./lib/apparatus/') diff --git a/lib/apparatus/classifier/bayes_classifier.d.ts b/lib/apparatus/classifier/bayes_classifier.d.ts new file mode 100644 index 0000000..b493bac --- /dev/null +++ b/lib/apparatus/classifier/bayes_classifier.d.ts @@ -0,0 +1,72 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +import { Classifier, Classification } from './classifier' + +/** + * Bayes Classifier + * + * Naive Bayes classifier implementation + */ +export class BayesClassifier extends Classifier { + /** + * Initialize with optional smoothing parameter + * @param smoothing - Smoothing constant (default: 1.0) + */ + constructor (smoothing?: number) + + /** + * Add an example to the training set + * @param observation - Feature vector (array or sparse object) + * @param label - Class label + */ + addExample (observation: number[] | { [key: string]: any }, label: string): void + + /** + * Train the classifier + */ + train (): void + + /** + * Get probability of a class given an observation + * @param observation - Feature vector (array or sparse object) + * @param label - Class label + * @returns Log probability + */ + probabilityOfClass (observation: number[] | { [key: string]: any }, label: string): number + + /** + * Get classifications for an observation + * @param observation - Feature vector (array or sparse object) + * @returns Array of {label, value} pairs sorted by probability + */ + getClassifications (observation: number[] | { [key: string]: any }): Classification[] + + /** + * Restore classifier from JSON + * @param classifier - JSON representation + * @returns Restored classifier + */ + static restore (classifier: any): BayesClassifier +} + +export default BayesClassifier diff --git a/lib/apparatus/classifier/bayes_classifier.js b/lib/apparatus/classifier/bayes_classifier.js index f88b588..f45b380 100644 --- a/lib/apparatus/classifier/bayes_classifier.js +++ b/lib/apparatus/classifier/bayes_classifier.js @@ -20,113 +20,107 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var util = require('util'), -Classifier = require('./classifier'); - -var BayesClassifier = function(smoothing) { - Classifier.call(this); - this.classFeatures = {}; - this.classTotals = {}; - this.totalExamples = 1; // start at one to smooth - this.smoothing = smoothing === undefined ? 1.0 : smoothing; -}; - -util.inherits(BayesClassifier, Classifier); - -function addExample(observation, label) { - if(!this.classFeatures[label]) { - this.classFeatures[label] = {}; - this.classTotals[label] = 1; // give an extra for smoothing +const Classifier = require('./classifier') + +class BayesClassifier extends Classifier { + constructor (smoothing) { + super() + this.classFeatures = {} + this.classTotals = {} + this.totalExamples = 1 // start at one to smooth + this.smoothing = smoothing === undefined ? 1.0 : smoothing + } + + addExample (observation, label) { + if (!this.classFeatures[label]) { + this.classFeatures[label] = {} + this.classTotals[label] = 1 // give an extra for smoothing } - if(observation instanceof Array) { - var i = observation.length; - this.totalExamples++; - this.classTotals[label]++; - - while(i--) { - if(observation[i]) { - if(this.classFeatures[label][i]) { - this.classFeatures[label][i]++; - } else { - // give an extra for smoothing - this.classFeatures[label][i] = 1 + this.smoothing; - } - } + if (observation instanceof Array) { + let i = observation.length + this.totalExamples++ + this.classTotals[label]++ + + while (i--) { + if (observation[i]) { + if (this.classFeatures[label][i]) { + this.classFeatures[label][i]++ + } else { + // give an extra for smoothing + this.classFeatures[label][i] = 1 + this.smoothing + } } + } } else { - // sparse observation - for(var key in observation){ - value = observation[key]; - - if(this.classFeatures[label][value]) { - this.classFeatures[label][value]++; - } else { - // give an extra for smoothing - this.classFeatures[label][value] = 1 + this.smoothing; - } + // sparse observation + for (const key in observation) { + const value = observation[key] + + if (this.classFeatures[label][value]) { + this.classFeatures[label][value]++ + } else { + // give an extra for smoothing + this.classFeatures[label][value] = 1 + this.smoothing } + } } -} + } -function train() { + train () { -} + } -function probabilityOfClass(observation, label) { - var prob = 0; + probabilityOfClass (observation, label) { + let prob = 0 - if(observation instanceof Array){ - var i = observation.length; + if (observation instanceof Array) { + let i = observation.length - while(i--) { - if(observation[i]) { - var count = this.classFeatures[label][i] || this.smoothing; - // numbers are tiny, add logs rather than take product - prob += Math.log(count / this.classTotals[label]); - } + while (i--) { + if (observation[i]) { + const count = this.classFeatures[label][i] || this.smoothing + // numbers are tiny, add logs rather than take product + prob += Math.log(count / this.classTotals[label]) } + } } else { - // sparse observation - for(var key in observation){ - var count = this.classFeatures[label][observation[key]] || this.smoothing; - // numbers are tiny, add logs rather than take product - prob += Math.log(count / this.classTotals[label]); - } + // sparse observation + for (const key in observation) { + const count = this.classFeatures[label][observation[key]] || this.smoothing + // numbers are tiny, add logs rather than take product + prob += Math.log(count / this.classTotals[label]) + } } // p(C) * unlogging the above calculation P(X|C) - prob = (this.classTotals[label] / this.totalExamples) * Math.exp(prob); + prob = (this.classTotals[label] / this.totalExamples) * Math.exp(prob) - return prob; -} + return prob + } -function getClassifications(observation) { - var classifier = this; - var labels = []; + getClassifications (observation) { + const classifier = this + const labels = [] - for(var className in this.classFeatures) { - labels.push({label: className, - value: classifier.probabilityOfClass(observation, className)}); + for (const className in this.classFeatures) { + labels.push({ + label: className, + value: classifier.probabilityOfClass(observation, className) + }) } - return labels.sort(function(x, y) { - return y.value - x.value; - }); -} + return labels.sort(function (x, y) { + return y.value - x.value + }) + } -function restore(classifier) { - classifier = Classifier.restore(classifier); - classifier.__proto__ = BayesClassifier.prototype; + static restore (classifier) { + classifier = Classifier.restore(classifier) + Object.setPrototypeOf(classifier, BayesClassifier.prototype) - return classifier; + return classifier + } } -BayesClassifier.prototype.addExample = addExample; -BayesClassifier.prototype.train = train; -BayesClassifier.prototype.getClassifications = getClassifications; -BayesClassifier.prototype.probabilityOfClass = probabilityOfClass; - -BayesClassifier.restore = restore; - -module.exports = BayesClassifier; \ No newline at end of file +module.exports = BayesClassifier diff --git a/lib/apparatus/classifier/classifier.d.ts b/lib/apparatus/classifier/classifier.d.ts new file mode 100644 index 0000000..8313fad --- /dev/null +++ b/lib/apparatus/classifier/classifier.d.ts @@ -0,0 +1,72 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/** + * Classification result + */ +export interface Classification { + label: string | number + value: number +} + +/** + * Base Classifier class + * + * This is the base class for all classifiers in Apparatus. + * Subclasses must implement addExample() and train() methods. + */ +export class Classifier { + /** + * Add an example to the training set + * @param observation - The feature vector (array or object) + * @param classification - The label/class for this example + */ + addExample (observation: any, classification: string | number): void + + /** + * Train the classifier on the added examples + */ + train (): void + + /** + * Classify an observation + * @param observation - The feature vector to classify + * @returns The predicted class label + */ + classify (observation: any): string | number + + /** + * Get classifications for an observation with confidence scores + * @param observation - The feature vector to classify + * @returns Array of {label, value} pairs sorted by confidence + */ + getClassifications (observation: any): Classification[] + + /** + * Restore a classifier from JSON + * @param classifier - JSON representation of classifier or JSON string + * @returns Restored classifier instance + */ + static restore (classifier: any): Classifier +} + +export default Classifier diff --git a/lib/apparatus/classifier/classifier.js b/lib/apparatus/classifier/classifier.js index 1725add..cefbd29 100644 --- a/lib/apparatus/classifier/classifier.js +++ b/lib/apparatus/classifier/classifier.js @@ -20,35 +20,28 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -function Classifier() { +class Classifier { + addExample (observation, classification) { + throw new Error('Not implemented') + } + + classify (observation) { + const classifications = this.getClassifications(observation) + if (!classifications || classifications.length === 0) { + throw new Error('Not Trained') + } + return classifications[0].label + } + + train () { + throw new Error('Not implemented') + } + + static restore (classifier) { + classifier = typeof classifier === 'string' ? JSON.parse(classifier) : classifier + + return classifier + } } -function restore(classifier) { - classifier = typeof classifier == 'string' ? JSON.parse(classifier) : classifier; - - return classifier; -} - -function addExample(observation, classification) { - throw 'Not implemented'; -} - -function classify(observation) { - var classifications = this.getClassifications(observation); - if(!classifications || classifications.length === 0) { - throw "Not Trained"; - } - return classifications[0].label; -} - -function train() { - throw 'Not implemented'; -} - -Classifier.prototype.addExample = addExample; -Classifier.prototype.train = train; -Classifier.prototype.classify = classify; - -Classifier.restore = restore; - -module.exports = Classifier; +module.exports = Classifier diff --git a/lib/apparatus/classifier/logistic-regression-legacy.d.ts b/lib/apparatus/classifier/logistic-regression-legacy.d.ts new file mode 100644 index 0000000..52e195d --- /dev/null +++ b/lib/apparatus/classifier/logistic-regression-legacy.d.ts @@ -0,0 +1,64 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +import { Classifier, Classification } from './classifier' + +/** + * Logistic Regression Classifier - Legacy Version + * + * Legacy implementation of logistic regression classifier. + * For new code, use LogisticRegressionModernized instead. + */ +export class LogisticRegressionLegacy extends Classifier { + /** + * Initialize the classifier + */ + constructor () + + /** + * Add an example to the training set + * @param data - Feature vector + * @param classification - Class label + */ + addExample (data: number[], classification: string): void + + /** + * Train the classifier on added examples + */ + train (): void + + /** + * Get classifications for an observation with confidence scores + * @param observation - Feature vector to classify + * @returns Array of {label, value} pairs sorted by confidence + */ + getClassifications (observation: number[]): Classification[] + + /** + * Restore classifier from JSON + * @param classifier - JSON representation + * @returns Restored classifier + */ + static restore (classifier: any): LogisticRegressionLegacy +} + +export default LogisticRegressionLegacy diff --git a/lib/apparatus/classifier/logistic-regression-legacy.js b/lib/apparatus/classifier/logistic-regression-legacy.js new file mode 100644 index 0000000..4e9df25 --- /dev/null +++ b/lib/apparatus/classifier/logistic-regression-legacy.js @@ -0,0 +1,178 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +const Classifier = require('./classifier') + +const sylvester = require('sylvester') +const Matrix = sylvester.Matrix +const Vector = sylvester.Vector + +function sigmoid (z) { + return 1 / (1 + Math.exp(0 - z)) +} + +function hypothesis (theta, Observations) { + return Observations.x(theta).map(sigmoid) +} + +function cost (theta, Examples, classifications) { + const hypothesisResult = hypothesis(theta, Examples) + + const ones = Vector.One(Examples.rows()) + const cost1 = Vector.Zero(Examples.rows()).subtract(classifications).elementMultiply(hypothesisResult.log()) + const cost0 = ones.subtract(classifications).elementMultiply(ones.subtract(hypothesisResult).log()) + + return (1 / Examples.rows()) * cost1.subtract(cost0).sum() +} + +function descendGradient (theta, Examples, classifications) { + const maxIt = 500 * Examples.rows() + let last + let current + let learningRate = 3 + let learningRateFound = false + + Examples = Matrix.One(Examples.rows(), 1).augment(Examples) + theta = theta.augment([0]) + + while (!learningRateFound && learningRate !== 0) { + let i = 0 + last = null + + while (true) { + const hypothesisResult = hypothesis(theta, Examples) + theta = theta.subtract(Examples.transpose().x( + hypothesisResult.subtract(classifications)).x(1 / Examples.rows()).x(learningRate)) + current = cost(theta, Examples, classifications) + + i++ + + if (last) { + if (current < last) { learningRateFound = true } else { break } + + if (last - current < 0.0001) { break } + } + + if (i >= maxIt) { + throw new Error('unable to find minimum') + } + + last = current + } + + learningRate /= 3 + } + + return theta.chomp(1) +} + +class LogisticRegressionClassifier extends Classifier { + constructor () { + super() + this.examples = {} + this.features = [] + this.featurePositions = {} + this.maxFeaturePosition = 0 + this.classifications = [] + this.exampleCount = 0 + } + + createClassifications () { + const classifications = [] + + for (let i = 0; i < this.exampleCount; i++) { + const classification = [] + + Object.keys(this.examples).forEach(() => { + classification.push(0) + }) + + classifications.push(classification) + } + + return classifications + } + + computeThetas (Examples, Classifications) { + this.theta = [] + + // each class will have it's own theta. + const zero = function () { return 0 } + for (let i = 1; i <= this.classifications.length; i++) { + const theta = Examples.row(1).map(zero) + this.theta.push(descendGradient(theta, Examples, Classifications.column(i))) + } + } + + train () { + const examples = [] + const classifications = this.createClassifications() + let d = 0; let c = 0 + + for (const classification in this.examples) { + for (let i = 0; i < this.examples[classification].length; i++) { + const doc = this.examples[classification][i] + const example = doc + + examples.push(example) + classifications[d][c] = 1 + d++ + } + + c++ + } + + this.computeThetas(Matrix.create(examples), Matrix.create(classifications)) + } + + addExample (data, classification) { + if (!this.examples[classification]) { + this.examples[classification] = [] + this.classifications.push(classification) + } + + this.examples[classification].push(data) + this.exampleCount++ + } + + getClassifications (observation) { + observation = Vector.create(observation) + const classifications = [] + + for (let i = 0; i < this.theta.length; i++) { + classifications.push({ label: this.classifications[i], value: sigmoid(observation.dot(this.theta[i])) }) + } + + return classifications.sort(function (x, y) { + return y.value - x.value + }) + } + + static restore (classifier) { + classifier = Classifier.restore(classifier) + Object.setPrototypeOf(classifier, LogisticRegressionClassifier.prototype) + + return classifier + } +} + +module.exports = LogisticRegressionClassifier diff --git a/lib/apparatus/classifier/logistic-regression-modernized.d.ts b/lib/apparatus/classifier/logistic-regression-modernized.d.ts new file mode 100644 index 0000000..705e0e8 --- /dev/null +++ b/lib/apparatus/classifier/logistic-regression-modernized.d.ts @@ -0,0 +1,63 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +import { Classifier, Classification } from './classifier' + +/** + * Logistic Regression Classifier - Modernized Version + * + * Supports multi-class logistic regression using gradient descent optimization + */ +export class LogisticRegressionModernized extends Classifier { + /** + * Initialize the classifier + */ + constructor () + + /** + * Add an example to the training set + * @param data - Feature vector + * @param classification - Class label + */ + addExample (data: number[], classification: string): void + + /** + * Train the classifier on added examples + */ + train (): void + + /** + * Get classifications for an observation with confidence scores + * @param observation - Feature vector to classify + * @returns Array of {label, value} pairs sorted by confidence (sigmoid scores) + */ + getClassifications (observation: number[]): Classification[] + + /** + * Restore classifier from JSON + * @param classifier - JSON representation + * @returns Restored classifier + */ + static restore (classifier: any): LogisticRegressionModernized +} + +export default LogisticRegressionModernized diff --git a/lib/apparatus/classifier/logistic-regression-modernized.js b/lib/apparatus/classifier/logistic-regression-modernized.js new file mode 100644 index 0000000..9eb9bb4 --- /dev/null +++ b/lib/apparatus/classifier/logistic-regression-modernized.js @@ -0,0 +1,362 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +const Classifier = require('./classifier') + +/** + * Sigmoid function + */ +function sigmoid (z) { + return 1 / (1 + Math.exp(-z)) +} + +/** + * Dot product of two vectors (arrays) + */ +function dotProduct (a, b) { + let sum = 0 + for (let i = 0; i < a.length; i++) { + sum += a[i] * b[i] + } + return sum +} + +/** + * Matrix-vector multiplication + * matrix: array of arrays (rows x cols) + * vector: array (cols) + * returns: array (rows) + */ +function matrixVectorMultiply (matrix, vector) { + const result = new Array(matrix.length) + for (let i = 0; i < matrix.length; i++) { + result[i] = dotProduct(matrix[i], vector) + } + return result +} + +/** + * Matrix transpose + * matrix: array of arrays (rows x cols) + * returns: array of arrays (cols x rows) + */ +function matrixTranspose (matrix) { + if (matrix.length === 0) return [] + + const rows = matrix.length + const cols = matrix[0].length + const result = Array(cols) + + for (let j = 0; j < cols; j++) { + result[j] = new Array(rows) + for (let i = 0; i < rows; i++) { + result[j][i] = matrix[i][j] + } + } + return result +} + +/** + * Element-wise operations + */ +function elementWiseSubtract (a, b) { + const result = new Array(a.length) + for (let i = 0; i < a.length; i++) { + result[i] = a[i] - b[i] + } + return result +} + +function elementWiseMultiply (a, b) { + const result = new Array(a.length) + for (let i = 0; i < a.length; i++) { + result[i] = a[i] * b[i] + } + return result +} + +function elementWiseLog (a) { + const result = new Array(a.length) + for (let i = 0; i < a.length; i++) { + result[i] = Math.log(a[i]) + } + return result +} + +function elementWiseApply (a, fn) { + const result = new Array(a.length) + for (let i = 0; i < a.length; i++) { + result[i] = fn(a[i]) + } + return result +} + +/** + * Sum of array elements + */ +function sum (arr) { + let total = 0 + for (let i = 0; i < arr.length; i++) { + total += arr[i] + } + return total +} + +/** + * Scalar multiply + */ +function scalarMultiply (arr, scalar) { + const result = new Array(arr.length) + for (let i = 0; i < arr.length; i++) { + result[i] = arr[i] * scalar + } + return result +} + +/** + * Augment matrix with ones column + */ +function augmentWithOnes (matrix) { + const result = new Array(matrix.length) + for (let i = 0; i < matrix.length; i++) { + result[i] = [1, ...matrix[i]] + } + return result +} + +/** + * Cost function + */ +function cost (theta, Examples, classifications) { + const hypothesisResult = matrixVectorMultiply(Examples, theta) + const sigmoidResult = elementWiseApply(hypothesisResult, sigmoid) + + const numExamples = Examples.length + const ones = Array(numExamples).fill(1) + + // cost1 = (-classifications) .* log(sigmoidResult) + const negClassifications = elementWiseMultiply(classifications, Array(numExamples).fill(-1)) + const cost1 = elementWiseMultiply( + negClassifications, + elementWiseLog(sigmoidResult) + ) + + // cost0 = (1 - classifications) .* log(1 - sigmoidResult) + const oneMinusHypothesis = elementWiseSubtract(ones, sigmoidResult) + const oneMinusClassifications = elementWiseSubtract(ones, classifications) + const cost0 = elementWiseMultiply( + oneMinusClassifications, + elementWiseLog(oneMinusHypothesis) + ) + + const totalCost = sum(elementWiseSubtract(cost1, cost0)) + return totalCost / numExamples +} + +/** + * Descending gradient function - trains the model + * Optimized for speed with pre-computed matrix transpose + */ +function descendGradient (theta, Examples, classifications) { + const maxIterPerRate = 100 + let learningRate = 1.0 + let currentTheta = theta.slice() + let bestTheta = theta.slice() + let bestCost = cost(currentTheta, Examples, classifications) + let learningRateFound = false + const m = Examples.length + + // Pre-compute transpose once - this is the key optimization! + const ExamplesTransposed = matrixTranspose(Examples) + + while (!learningRateFound && learningRate > 0.0001) { + let iterationCount = 0 + let lastCost = bestCost + let improvementCount = 0 + + while (iterationCount < maxIterPerRate) { + // Compute hypothesis and error + const hypothesisResult = matrixVectorMultiply(Examples, currentTheta) + const sigmoidResult = elementWiseApply(hypothesisResult, sigmoid) + + // Gradient = X^T * (h(X) - y) / m + const error = elementWiseSubtract(sigmoidResult, classifications) + const gradient = matrixVectorMultiply(ExamplesTransposed, error) + const scaledGradient = scalarMultiply(gradient, learningRate / m) + + // Update theta + currentTheta = elementWiseSubtract(currentTheta, scaledGradient) + + // Evaluate cost + const currentCost = cost(currentTheta, Examples, classifications) + + if (currentCost < bestCost) { + bestCost = currentCost + bestTheta = currentTheta.slice() + improvementCount++ + } + + // Check for convergence with improved logic + if (lastCost - currentCost < 0.00001) { + learningRateFound = true + break + } + + lastCost = currentCost + iterationCount++ + } + + // If we made progress, accept this learning rate + if (improvementCount > maxIterPerRate * 0.1) { + learningRateFound = true + } else { + // Try smaller learning rate + learningRate *= 0.5 + currentTheta = bestTheta.slice() // Reset to best known theta + } + } + + return bestTheta.slice(1) // Remove augmented 0 at the beginning +} + +/** + * Logistic Regression Classifier - Modernized version + */ +class LogisticRegressionModernized extends Classifier { + constructor () { + super() + this.examples = {} + this.features = [] + this.featurePositions = {} + this.maxFeaturePosition = 0 + this.classifications = [] + this.exampleCount = 0 + this.theta = [] + } + + /** + * Create classifications matrix + */ + createClassifications () { + const classifications = [] + + for (let i = 0; i < this.exampleCount; i++) { + const classification = [] + + Object.keys(this.examples).forEach(() => { + classification.push(0) + }) + + classifications.push(classification) + } + + return classifications + } + + /** + * Compute theta parameters for each class + */ + computeThetas (Examples, Classifications) { + this.theta = [] + + // each class will have its own theta + const zeroVector = new Array(Examples[0].length).fill(0) + + for (let i = 0; i < this.classifications.length; i++) { + // Extract column i from Classifications + const classColumn = new Array(Classifications.length) + for (let j = 0; j < Classifications.length; j++) { + classColumn[j] = Classifications[j][i] + } + + this.theta.push(descendGradient(zeroVector, Examples, classColumn)) + } + } + + /** + * Train the classifier + */ + train () { + const examples = [] + const classifications = this.createClassifications() + let d = 0 + let c = 0 + + for (const classification in this.examples) { + for (let i = 0; i < this.examples[classification].length; i++) { + const doc = this.examples[classification][i] + examples.push(doc) + classifications[d][c] = 1 + d++ + } + + c++ + } + + const augmentedExamples = augmentWithOnes(examples) + this.computeThetas(augmentedExamples, classifications) + } + + /** + * Add example to training set + */ + addExample (data, classification) { + if (!this.examples[classification]) { + this.examples[classification] = [] + this.classifications.push(classification) + } + + this.examples[classification].push(data) + this.exampleCount++ + } + + /** + * Get classifications for an observation + */ + getClassifications (observation) { + const classifications = [] + + for (let i = 0; i < this.theta.length; i++) { + const score = dotProduct(observation, this.theta[i]) + classifications.push({ + label: this.classifications[i], + value: sigmoid(score) + }) + } + + return classifications.sort(function (x, y) { + return y.value - x.value + }) + } + + /** + * Restore classifier from JSON + */ + static restore (classifier) { + classifier = Classifier.restore(classifier) + Object.setPrototypeOf(classifier, LogisticRegressionModernized.prototype) + + return classifier + } +} + +module.exports = LogisticRegressionModernized diff --git a/lib/apparatus/classifier/logistic_regression_classifier.d.ts b/lib/apparatus/classifier/logistic_regression_classifier.d.ts new file mode 100644 index 0000000..379e669 --- /dev/null +++ b/lib/apparatus/classifier/logistic_regression_classifier.d.ts @@ -0,0 +1,31 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/** + * Logistic Regression Classifier + * + * This module exports the modernized Logistic Regression Classifier. + * For the legacy implementation, use logistic-regression-legacy. + */ +export { LogisticRegressionModernized as default } from './logistic-regression-modernized' +export { LogisticRegressionModernized } from './logistic-regression-modernized' +export type { Classification } from './classifier' diff --git a/lib/apparatus/classifier/logistic_regression_classifier.js b/lib/apparatus/classifier/logistic_regression_classifier.js index 043e898..4669225 100644 --- a/lib/apparatus/classifier/logistic_regression_classifier.js +++ b/lib/apparatus/classifier/logistic_regression_classifier.js @@ -20,173 +20,5 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var util = require('util'), - Classifier = require('./classifier'); - -var sylvester = require('sylvester'), -Matrix = sylvester.Matrix, -Vector = sylvester.Vector; - -function sigmoid(z) { - return 1 / (1 + Math.exp(0 - z)); -} - -function hypothesis(theta, Observations) { - return Observations.x(theta).map(sigmoid); -} - -function cost(theta, Examples, classifications) { - var hypothesisResult = hypothesis(theta, Examples); - - var ones = Vector.One(Examples.rows()); - var cost_1 = Vector.Zero(Examples.rows()).subtract(classifications).elementMultiply(hypothesisResult.log()); - var cost_0 = ones.subtract(classifications).elementMultiply(ones.subtract(hypothesisResult).log()); - - return (1 / Examples.rows()) * cost_1.subtract(cost_0).sum(); -} - -function descendGradient(theta, Examples, classifications) { - var maxIt = 500 * Examples.rows(); - var last; - var current; - var learningRate = 3; - var learningRateFound = false; - - Examples = Matrix.One(Examples.rows(), 1).augment(Examples); - theta = theta.augment([0]); - - while(!learningRateFound && learningRate !== 0) { - var i = 0; - last = null; - - while(true) { - var hypothesisResult = hypothesis(theta, Examples); - theta = theta.subtract(Examples.transpose().x( - hypothesisResult.subtract(classifications)).x(1 / Examples.rows()).x(learningRate)); - current = cost(theta, Examples, classifications); - - i++; - - if(last) { - if(current < last) - learningRateFound = true; - else - break; - - if(last - current < 0.0001) - break; - } - - if(i >= maxIt) { - throw 'unable to find minimum'; - } - - last = current; - } - - learningRate /= 3; - } - - return theta.chomp(1); -} - -var LogisticRegressionClassifier = function() { - Classifier.call(this); - this.examples = {}; - this.features = []; - this.featurePositions = {}; - this.maxFeaturePosition = 0; - this.classifications = []; - this.exampleCount = 0; -}; - -util.inherits(LogisticRegressionClassifier, Classifier); - -function createClassifications() { - var classifications = []; - - for(var i = 0; i < this.exampleCount; i++) { - var classification = []; - - for(var _ in this.examples) { - classification.push(0); - } - - classifications.push(classification); - } - - return classifications; -} - -function computeThetas(Examples, Classifications) { - this.theta = []; - - // each class will have it's own theta. - var zero = function() { return 0; }; - for(var i = 1; i <= this.classifications.length; i++) { - var theta = Examples.row(1).map(zero); - this.theta.push(descendGradient(theta, Examples, Classifications.column(i))); - } -} - -function train() { - var examples = []; - var classifications = this.createClassifications(); - var d = 0, c = 0; - - for(var classification in this.examples) { - for(var i = 0; i < this.examples[classification].length; i++) { - var doc = this.examples[classification][i]; - var example = doc; - - examples.push(example); - classifications[d][c] = 1; - d++; - } - - c++; - } - - this.computeThetas($M(examples), $M(classifications)); -} - -function addExample(data, classification) { - if(!this.examples[classification]) { - this.examples[classification] = []; - this.classifications.push(classification); - } - - this.examples[classification].push(data); - this.exampleCount++; -} - -function getClassifications(observation) { - observation = $V(observation); - var classifications = []; - - for(var i = 0; i < this.theta.length; i++) { - classifications.push({label: this.classifications[i], value: sigmoid(observation.dot(this.theta[i])) }); - } - - return classifications.sort(function(x, y) { - return y.value - x.value; - }); -} - -function restore(classifier) { - classifier = Classifier.restore(classifier); - classifier.__proto__ = LogisticRegressionClassifier.prototype; - - return classifier; -} - -LogisticRegressionClassifier.prototype.addExample = addExample; -LogisticRegressionClassifier.prototype.restore = restore; -LogisticRegressionClassifier.prototype.train = train; -LogisticRegressionClassifier.prototype.createClassifications = createClassifications; -LogisticRegressionClassifier.prototype.computeThetas = computeThetas; -LogisticRegressionClassifier.prototype.getClassifications = getClassifications; - -LogisticRegressionClassifier.restore = restore; - -module.exports = LogisticRegressionClassifier; +// Use the modernized implementation directly +module.exports = require('./logistic-regression-modernized') diff --git a/lib/apparatus/classifier/randomforest_classifier.d.ts b/lib/apparatus/classifier/randomforest_classifier.d.ts new file mode 100644 index 0000000..bbc2010 --- /dev/null +++ b/lib/apparatus/classifier/randomforest_classifier.d.ts @@ -0,0 +1,107 @@ +/* +Copyright (c) 2012 Andrej Karpathy + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import { Classifier } from './classifier' + +/** + * Random Forest Classifier Options + */ +export interface RandomForestOptions { + /** + * Number of trees to train (default: 100) + */ + numTrees?: number + + /** + * Maximum depth of each tree in the forest (default: 4) + */ + maxDepth?: number + + /** + * Number of random hypotheses generated at each node during training (default: 10) + */ + numTries?: number + + /** + * Weak learner training function + */ + trainFun?: (data: number[][], labels: number[], ix: number[], options: RandomForestOptions) => any + + /** + * Weak learner test function + */ + testFun?: (inst: number[], model: any) => number + + /** + * Type of weak learner (0: decisionStump, 1: decision2DStump) + */ + type?: number +} + +/** + * Random Forest Classifier + * + * Ensemble classifier using multiple decision trees + */ +export class RandomForestClassifier extends Classifier { + /** + * Initialize with optional options + * @param options - Configuration options for the forest + */ + constructor (options?: RandomForestOptions) + + /** + * Add an example to the training set + * @param data - Feature vector + * @param label - Classification label (typically 1 or -1 for binary classification) + */ + addExample (data: number[], label: number): void + + /** + * Train the classifier on added examples + */ + train (): void + + /** + * Classify an observation (returns the predicted label) + * @param inst - Feature vector to classify + * @returns Predicted label (1 or -1) + */ + classify (inst: number[]): number + + /** + * Get probability prediction for a single instance + * @param inst - Feature vector to predict + * @returns Probability/prediction value in range [0, 1] + */ + predictOne (inst: number[]): number + + /** + * Get probability predictions for multiple instances + * @param data - Array of feature vectors + * @returns Array of probability values + */ + predict (data: number[][]): number[] +} + +export default RandomForestClassifier diff --git a/lib/apparatus/classifier/randomforest_classifier.js b/lib/apparatus/classifier/randomforest_classifier.js index c3c0a2d..7a67a76 100644 --- a/lib/apparatus/classifier/randomforest_classifier.js +++ b/lib/apparatus/classifier/randomforest_classifier.js @@ -21,411 +21,393 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var util = require('util'), - Classifier = require('./classifier'); - -var RandomForestClassifier = function(options) { - Classifier.call(this); - this.options = options; - this.examples = []; - this.example_size = 0; - this.labels = []; -}; - -/* - data is 2D array of size N x D of examples - labels is a 1D array of labels (only -1 or 1 for now). In future will support multiclass or maybe even regression - options.numTrees can be used to customize number of trees to train (default = 100) - options.maxDepth is the maximum depth of each tree in the forest (default = 4) - options.numTries is the number of random hypotheses generated at each node during training (default = 10) - options.trainFun is a function with signature "function myWeakTrain(data, labels, ix, options)". Here, ix is a list of - indexes into data of the instances that should be payed attention to. Everything not in the list - should be ignored. This is done for efficiency. The function should return a model where you store - variables. (i.e. model = {}; model.myvar = 5;) This will be passed to testFun. - options.testFun is a function with signature "funtion myWeakTest(inst, model)" where inst is 1D array specifying an example, - and model will be the same model that you return in options.trainFun. For example, model.myvar will be 5. - see decisionStumpTrain() and decisionStumpTest() below for example. -*/ -function trainRandomForest(data, labels, options) { - options = options || {}; - this.numTrees = options.numTrees || 100; - +const Classifier = require('./classifier') + +class RandomForestClassifier extends Classifier { + constructor (options) { + super() + this.options = options + this.examples = [] + this.example_size = 0 + this.labels = [] + } + + /* + data is 2D array of size N x D of examples + labels is a 1D array of labels (only -1 or 1 for now). In future will support multiclass or maybe even regression + options.numTrees can be used to customize number of trees to train (default = 100) + options.maxDepth is the maximum depth of each tree in the forest (default = 4) + options.numTries is the number of random hypotheses generated at each node during training (default = 10) + options.trainFun is a function with signature "function myWeakTrain(data, labels, ix, options)". Here, ix is a list of + indexes into data of the instances that should be payed attention to. Everything not in the list + should be ignored. This is done for efficiency. The function should return a model where you store + variables. (i.e. model = {}; model.myvar = 5;) This will be passed to testFun. + options.testFun is a function with signature "funtion myWeakTest(inst, model)" where inst is 1D array specifying an example, + and model will be the same model that you return in options.trainFun. For example, model.myvar will be 5. + see decisionStumpTrain() and decisionStumpTest() below for example. + */ + trainRandomForest (data, labels, options) { + options = options || {} + this.numTrees = options.numTrees || 100 + // initialize many trees and train them all independently - this.trees= new Array(this.numTrees); - for(var i=0;i this.example_size) { + const newSize = observation.length + this.example_size = newSize + for (let i = 0; i < this.examples.length; i++) { + const e = this.examples[i] + for (let j = e.length; j < newSize; j++) { + e.push(0.0) + } } - dec /= this.numTrees; - return dec; -} - -/* - convenience function. Here, data is NxD array. - returns probabilities of being 1 for all data in an array. -*/ -function predict(data) { - var probabilities= new Array(data.length); - for(var i=0;i 0.5) { + return [{ + value: prob, + label: 1 + }, + { + value: (1 - prob), + label: -1 + }] + } else { + return [{ + value: (1 - prob), + label: -1 + }, + { + value: prob, + label: 1 + }] + } + } + + static restore (classifier) { + classifier = Classifier.restore(classifier) + Object.setPrototypeOf(classifier, RandomForestClassifier.prototype) + + // change prototypes recursively for the trees? + + return classifier + } } - + // represents a single decision tree -var DecisionTree = function(options) { -}; - -function trainDT(data, labels, options) { - options = options || {}; - var maxDepth = options.maxDepth || 4; - var weakType = options.type || 0; - - var trainFun = decision2DStumpTrain; - var testFun = decision2DStumpTest; - - if(options.trainFun) trainFun = options.trainFun; - if(options.testFun) testFun = options.testFun; - - if(weakType == 0) { - trainFun= decisionStumpTrain; - testFun= decisionStumpTest; +class DecisionTree { + constructor (options) { + this.options = options + } + + train (data, labels, options) { + options = options || {} + const maxDepth = options.maxDepth || 4 + const weakType = options.type || 0 + + let trainFun = decision2DStumpTrain + let testFun = decision2DStumpTest + + if (options.trainFun) trainFun = options.trainFun + if (options.testFun) testFun = options.testFun + + if (weakType === 0) { + trainFun = decisionStumpTrain + testFun = decisionStumpTest } - if(weakType == 1) { - trainFun= decision2DStumpTrain; - testFun= decision2DStumpTest; + if (weakType === 1) { + trainFun = decision2DStumpTrain + testFun = decision2DStumpTest } - + // initialize various helper variables - var numInternals= Math.pow(2, maxDepth)-1; - var numNodes= Math.pow(2, maxDepth + 1)-1; - var ixs= new Array(numNodes); - for(var i=1;i %f, %f. Gain %f", thr, H, LH, RH, informationGain); - if(informationGain > bestGain || i === 0) { - bestGain= informationGain; - bestThr= thr; - } - } - - model= {}; - model.thr= bestThr; - model.ri= ri; - return model; -} - -// returns a decision for a single data instance -function decisionStumpTest(inst, model) { - if(!model) { - // this is a leaf that never received any data... - return 1; - } - return inst[model.ri] < model.thr ? 1 : -1; -} - -// returns model. Code duplication with decisionStumpTrain :( -function decision2DStumpTrain(data, labels, ix, options) { - - options = options || {}; - var numtries = options.numTries || 10; - - // choose a dimension at random and pick a best split - var N = ix.length; - - var ri1= 0; - var ri2= 1; - if(data[0].length > 2) { - // more than 2D data. Pick 2 random dimensions - ri1= randi(0, data[0].length); - ri2= randi(0, data[0].length); - while(ri2 == ri1) ri2= randi(0, data[0].length); // must be distinct! - } - - // evaluate class entropy of incoming data - var H= entropy(labels, ix); - var bestGain=0; - var bestw1, bestw2, bestthr; - var dots= new Array(ix.length); - for(var i=0;i %f, %f. Gain %f", thr, H, LH, RH, informationGain); - if(informationGain > bestGain || i === 0) { - bestGain= informationGain; - bestw1= w1; - bestw2= w2; - bestthr= dotthr; - } - } - - model= {}; - model.w1= bestw1; - model.w2= bestw2; - model.dotthr= bestthr; - return model; -} - -// returns label for a single data instance -function decision2DStumpTest(inst, model) { - if(!model) { - // this is a leaf that never received any data... - return 1; +function decisionStumpTrain (data, labels, ix, options) { + options = options || {} + const numtries = options.numTries || 10 + + // choose a dimension at random and pick a best split + const ri = randi(0, data[0].length) + const N = ix.length + + // evaluate class entropy of incoming data + const H = entropy(labels, ix) + let bestGain = 0 + let bestThr = 0 + for (let i = 0; i < numtries; i++) { + // pick a random splitting threshold + const ix1 = ix[randi(0, N)] + let ix2 = ix[randi(0, N)] + while (ix2 === ix1) ix2 = ix[randi(0, N)] // enforce distinctness of ix2 + + const a = Math.random() + const thr = data[ix1][ri] * a + data[ix2][ri] * (1 - a) + + // measure information gain we'd get from split with thr + let l1 = 1; let r1 = 1; let lm1 = 1; let rm1 = 1 // counts for Left and label 1, right and label 1, left and minus 1, right and minus 1 + for (let j = 0; j < ix.length; j++) { + if (data[ix[j]][ri] < thr) { + if (labels[ix[j]] === 1) l1++ + else lm1++ + } else { + if (labels[ix[j]] === 1) r1++ + else rm1++ + } } - return inst[0]*model.w1 + inst[1]*model.w2 < model.dotthr ? 1 : -1; -} - -// Misc utility functions -function entropy(labels, ix) { - var N= ix.length; - var p=0.0; - for(var i=0;i %f, %f. Gain %f", thr, H, LH, RH, informationGain); + if (informationGain > bestGain || i === 0) { + bestGain = informationGain + bestThr = thr } - p=(1+p)/(N+2); // let's be bayesian about this - q=(1+N-p)/(N+2); - return (-p*Math.log(p) -q*Math.log(q)); -} - -// generate random floating point number between a and b -function randf(a, b) { - return Math.random()*(b-a)+a; + } + + const model = {} + model.thr = bestThr + model.ri = ri + return model } -// generate random integer between a and b (b excluded) -function randi(a, b) { - return Math.floor(Math.random()*(b-a)+a); +// returns a decision for a single data instance +function decisionStumpTest (inst, model) { + if (!model) { + // this is a leaf that never received any data... + return 1 + } + return inst[model.ri] < model.thr ? 1 : -1 } -// apparatus adapter +// returns model. Code duplication with decisionStumpTrain :( +function decision2DStumpTrain (data, labels, ix, options) { + options = options || {} + const numtries = options.numTries || 10 -util.inherits(RandomForestClassifier, Classifier); + // choose a dimension at random and pick a best split + const N = ix.length -function restore(classifier) { - classifier = Classifier.restore(classifier); - classifier.__proto__ = RandomForestClassifier.prototype; + let ri1 = 0 + let ri2 = 1 + if (data[0].length > 2) { + // more than 2D data. Pick 2 random dimensions + ri1 = randi(0, data[0].length) + ri2 = randi(0, data[0].length) + while (ri2 === ri1) ri2 = randi(0, data[0].length) // must be distinct! + } - // change prototypes recursively for the trees? + // evaluate class entropy of incoming data + const H = entropy(labels, ix) + let bestGain = 0 + let bestw1, bestw2, bestthr + const dots = new Array(ix.length) + for (let i = 0; i < numtries; i++) { + // pick random line parameters + const alpha = randf(0, 2 * Math.PI) + const w1 = Math.cos(alpha) + const w2 = Math.sin(alpha) - return classifier; -} + // project data on this line and get the dot products + for (let j = 0; j < ix.length; j++) { + dots[j] = w1 * data[ix[j]][ri1] + w2 * data[ix[j]][ri2] + } -function addExample(observation, classification) { - if(classification != -1 && classification != 1){ - throw 'Only classes 1 and -1 are currently supported'; + // we are in a tricky situation because data dot product distribution + // can be skewed. So we don't want to select just randomly between + // min and max. But we also don't want to sort as that is too expensive + // let's pick two random points and make the threshold be somewhere between them. + // for skewed datasets, the selected points will with relatively high likelihood + // be in the high-desnity regions, so the thresholds will make sense + const ix1 = ix[randi(0, N)] + let ix2 = ix[randi(0, N)] + while (ix2 === ix1) ix2 = ix[randi(0, N)] // enforce distinctness of ix2 + const a = Math.random() + const dotthr = dots[ix1] * a + dots[ix2] * (1 - a) + + // measure information gain we'd get from split with thr + let l1 = 1; let r1 = 1; let lm1 = 1; let rm1 = 1 // counts for Left and label 1, right and label 1, left and minus 1, right and minus 1 + for (let j = 0; j < ix.length; j++) { + if (dots[j] < dotthr) { + if (labels[ix[j]] === 1) l1++ + else lm1++ + } else { + if (labels[ix[j]] === 1) r1++ + else rm1++ + } } + let t = l1 + lm1 + l1 = l1 / t + lm1 = lm1 / t + t = r1 + rm1 + r1 = r1 / t + rm1 = rm1 / t + + const LH = -l1 * Math.log(l1) - lm1 * Math.log(lm1) // left and right entropy + const RH = -r1 * Math.log(r1) - rm1 * Math.log(rm1) - if(observation.length > this.example_size) { - var new_size = observation.length; - this.example_size = new_size; - for(var i = 0; i < this.examples.length; i++){ - var e = this.examples[i]; - for(var j=e.length; e %f, %f. Gain %f", thr, H, LH, RH, informationGain); + if (informationGain > bestGain || i === 0) { + bestGain = informationGain + bestw1 = w1 + bestw2 = w2 + bestthr = dotthr } - this.examples.push(observation); - this.labels.push(classification); + } + + const model = {} + model.w1 = bestw1 + model.w2 = bestw2 + model.dotthr = bestthr + return model } -function train() { - this.trainRandomForest(this.examples, this.labels, this.options); +// returns label for a single data instance +function decision2DStumpTest (inst, model) { + if (!model) { + // this is a leaf that never received any data... + return 1 + } + return inst[0] * model.w1 + inst[1] * model.w2 < model.dotthr ? 1 : -1 } -function getClassifications(observation) { - if(observation.length != this.example_size) { - throw 'Observation should be of length ' + this.example_size; - } - var prob = this.predictOne(observation); - if(prob > 0.5) - return [ { value: prob, - label: 1 }, - { value: (1-prob), - label: -1 } ]; - else - return [ { value: (1-prob), - label: -1 }, - { value: prob, - label: 1 } ]; +// Misc utility functions +function entropy (labels, ix) { + const N = ix.length + let p = 0.0 + for (let i = 0; i < N; i++) { + if (labels[ix[i]] === 1) p += 1 + } + p = (1 + p) / (N + 2) // let's be bayesian about this + const q = (1 + N - p) / (N + 2) + return (-p * Math.log(p) - q * Math.log(q)) } -RandomForestClassifier.prototype.train = train; -RandomForestClassifier.prototype.trainRandomForest = trainRandomForest; -RandomForestClassifier.prototype.predictOne = predictOne; -RandomForestClassifier.prototype.restore = restore; -RandomForestClassifier.prototype.addExample = addExample; -RandomForestClassifier.prototype.getClassifications = getClassifications; +// generate random floating point number between a and b +function randf (a, b) { + return Math.random() * (b - a) + a +} -DecisionTree.prototype.train = trainDT; -DecisionTree.prototype.predictOne = predictOneDT; +// generate random integer between a and b (b excluded) +function randi (a, b) { + return Math.floor(Math.random() * (b - a) + a) +} -RandomForestClassifier.DecisionTree = DecisionTree; -RandomForestClassifier.decisionStumpTrain = decisionStumpTrain; -RandomForestClassifier.decisionStumpTest = decisionStumpTest; -RandomForestClassifier.decision2DStumpTrain = decision2DStumpTrain; -RandomForestClassifier.decision2DStumpTest = decision2DStumpTest; +// Static properties for compatibility +RandomForestClassifier.DecisionTree = DecisionTree +RandomForestClassifier.decisionStumpTrain = decisionStumpTrain +RandomForestClassifier.decisionStumpTest = decisionStumpTest +RandomForestClassifier.decision2DStumpTrain = decision2DStumpTrain +RandomForestClassifier.decision2DStumpTest = decision2DStumpTest -module.exports = RandomForestClassifier; +module.exports = RandomForestClassifier diff --git a/lib/apparatus/clusterer/kmeans-legacy.js b/lib/apparatus/clusterer/kmeans-legacy.js new file mode 100644 index 0000000..3f40ca9 --- /dev/null +++ b/lib/apparatus/clusterer/kmeans-legacy.js @@ -0,0 +1,116 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +const Sylvester = require('sylvester') +const Matrix = Sylvester.Matrix + +function KMeans (Observations) { + if (!Observations.elements) { Observations = Matrix.create(Observations) } + + this.Observations = Observations +} + +// create an initial centroid matrix with initial values between +// 0 and the max of feature data X. +function createCentroids (k) { + const Centroid = [] + const maxes = this.Observations.maxColumns() + // console.log(maxes); + + for (let i = 1; i <= k; i++) { + const centroid = [] + for (let j = 1; j <= this.Observations.cols(); j++) { + centroid.push(Math.random() * maxes.e(j)) + } + + Centroid.push(centroid) + } + + // console.log(centroid) + + return Matrix.create(Centroid) +} + +// get the euclidian distance between the feature data X and +// a given centroid matrix C. +function distanceFrom (Centroids) { + const distances = [] + + for (let i = 1; i <= this.Observations.rows(); i++) { + const distance = [] + + for (let j = 1; j <= Centroids.rows(); j++) { + distance.push(this.Observations.row(i).distanceFrom(Centroids.row(j))) + } + + distances.push(distance) + } + + return Matrix.create(distances) +} + +// categorize the feature data X into k clusters. return a vector +// containing the results. +function cluster (k) { + let Centroids = this.createCentroids(k) + let LastDistances = Matrix.Zero(this.Observations.rows(), this.Observations.cols()) + let Distances = this.distanceFrom(Centroids) + let Groups + + while (!(LastDistances.eql(Distances))) { + Groups = Distances.minColumnIndexes() + LastDistances = Distances + + const newCentroids = [] + + for (let i = 1; i <= Centroids.rows(); i++) { + const centroid = [] + + for (let j = 1; j <= Centroids.cols(); j++) { + let sum = 0 + let count = 0 + + for (let l = 1; l <= this.Observations.rows(); l++) { + if (Groups.e(l) === i) { + count++ + sum += this.Observations.e(l, j) + } + } + + centroid.push(sum / count) + } + + newCentroids.push(centroid) + } + + Centroids = Matrix.create(newCentroids) + Distances = this.distanceFrom(Centroids) + } + + return Groups +} + +KMeans.prototype.createCentroids = createCentroids +KMeans.prototype.distanceFrom = distanceFrom +KMeans.prototype.cluster = cluster + +module.exports = KMeans diff --git a/lib/apparatus/clusterer/kmeans-modernized.d.ts b/lib/apparatus/clusterer/kmeans-modernized.d.ts new file mode 100644 index 0000000..9ecdd7c --- /dev/null +++ b/lib/apparatus/clusterer/kmeans-modernized.d.ts @@ -0,0 +1,179 @@ +/* +Copyright (c) 2026 Hugo W.L. ter Doest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/** + * Options for KMeansModernized clustering + */ +export interface KMeansOptions { + /** Maximum number of iterations (default: 100) */ + maxIterations?: number + /** Convergence tolerance (default: 0.0001) */ + tolerance?: number + /** Initialization method: 'random' or 'kmeans++' (default: 'kmeans++') */ + initialization?: 'random' | 'kmeans++' + /** Random seed for deterministic results (default: undefined) */ + seed?: number + /** Number of restarts to pick the best run (default: 1) */ + restarts?: number +} + +/** + * KMeansModernized - Modern K-Means clustering implementation + * + * Advanced features: + * - K-means++ initialization (better than random) + * - Multiple restarts with best result selection + * - Deterministic seeding for reproducibility + * - Tolerance-based convergence + * + * Usage: + * const kmeans = new KMeansModernized(3, { restarts: 10 }); + * kmeans.fit(data); + * const assignments = kmeans.getAssignments(); + */ +export class KMeansModernized { + /** Number of clusters */ + k: number + /** Maximum number of iterations */ + maxIterations: number + /** Convergence tolerance */ + tolerance: number + /** Initialization method */ + initialization: string + /** Random seed */ + seed: number | undefined + /** Number of restarts */ + restarts: number + /** Cluster centroids */ + centroids: number[][] | null + /** Cluster assignments (indices of points in each cluster) */ + clusters: number[][] | null + /** Cluster assignment for each data point */ + assignments: number[] | null + /** Number of iterations in the final fit */ + iterations: number + + /** + * Initialize KMeansModernized + * @param k - Number of clusters + * @param options - Configuration options + */ + constructor (k: number, options?: KMeansOptions) + + /** + * Initialize centroids using random selection + * @param data - Data points + * @param rng - Random number generator function + * @returns Initial centroid positions + */ + initializeRandomCentroids (data: number[][], rng: () => number): number[][] + + /** + * Initialize centroids using k-means++ algorithm + * @param data - Data points + * @param rng - Random number generator function + * @returns Initial centroid positions + */ + initializeKMeansPlusPlusCentroids (data: number[][], rng: () => number): number[][] + + /** + * Assign each data point to the nearest centroid + * @param data - Data points + * @param centroids - Centroid positions + */ + assignPointsToClusters ( + data: number[][], + centroids?: number[][] + ): { clusters: number[][]; assignments: number[] } + + /** + * Update centroids based on cluster means + * @param data - Data points + * @param clusters - Point indices for each cluster + * @param currentCentroids - Current centroid positions + * @returns Updated centroid positions + */ + updateCentroids ( + data: number[][], + clusters: number[][], + currentCentroids: number[][] + ): number[][] + + /** + * Check if centroids have converged + * @param oldCentroids - Previous centroid positions + * @param newCentroids - New centroid positions + * @returns True if converged within tolerance + */ + hasConverged (oldCentroids: number[][], newCentroids: number[][]): boolean + + /** + * Calculate total inertia (sum of squared distances to centroids) + * @param data - Data points + * @param centroids - Centroid positions + * @param assignments - Cluster assignments + * @returns Total inertia + */ + calculateInertia (data: number[][], centroids: number[][], assignments: number[]): number + + /** + * Run a single K-Means fit with a given RNG + * @param data - Data points + * @param rng - Random number generator function + */ + runSingleFit ( + data: number[][], + rng: () => number + ): { centroids: number[][]; clusters: number[][]; assignments: number[]; iterations: number; inertia: number } + + /** + * Fit the model to the data + * @param data - Array of data points (vectors) + * @returns Returns this for chaining + */ + fit (data: number[][]): KMeansModernized + + /** + * Predict cluster assignments for new data points + * @param data - Data points to predict + * @returns Array of cluster indices + */ + predict (data: number[][] | number[]): number[] + + /** + * Get the centroids + * @returns Cluster centroids + */ + getCentroids (): number[][] + + /** + * Get cluster assignments for the training data + * @returns Array of cluster indices + */ + getAssignments (): number[] + + /** + * Get clusters (indices of points in each cluster) + * @returns Array of clusters + */ + getClusters (): number[][] +} diff --git a/lib/apparatus/clusterer/kmeans-modernized.js b/lib/apparatus/clusterer/kmeans-modernized.js new file mode 100644 index 0000000..f5e5f7a --- /dev/null +++ b/lib/apparatus/clusterer/kmeans-modernized.js @@ -0,0 +1,390 @@ +/* +Copyright (c) 2026, Hugo W.L. ter Doest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +'use strict' + +/** + * Calculate Euclidean distance between two vectors + */ +function euclideanDistance (a, b) { + let sum = 0 + for (let i = 0; i < a.length; i++) { + const diff = a[i] - b[i] + sum += diff * diff + } + return Math.sqrt(sum) +} + +/** + * Calculate squared Euclidean distance between two vectors + */ +function euclideanDistanceSquared (a, b) { + let sum = 0 + for (let i = 0; i < a.length; i++) { + const diff = a[i] - b[i] + sum += diff * diff + } + return sum +} + +/** + * Create a deterministic RNG when a seed is provided + */ +function createRng (seed) { + if (typeof seed !== 'number' || !isFinite(seed)) { + return Math.random + } + + let state = seed >>> 0 + return function () { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 4294967296 + } +} + +/** + * Calculate the mean of vectors + */ +function calculateMean (vectors) { + if (vectors.length === 0) return null + + const dimensions = vectors[0].length + const mean = new Array(dimensions).fill(0) + + for (let i = 0; i < vectors.length; i++) { + for (let j = 0; j < dimensions; j++) { + mean[j] += vectors[i][j] + } + } + + for (let j = 0; j < dimensions; j++) { + mean[j] /= vectors.length + } + + return mean +} + +/** + * K-Means clustering algorithm (Modernized version) + * + * This is the advanced implementation with k-means++, multiple restarts, + * and configurable options. For the original Apparatus API, use KMeans + * from kmeans.js instead. + */ +class KMeansModernized { + /** + * @param {number} k - Number of clusters + * @param {Object} options - Configuration options + * @param {number} options.maxIterations - Maximum number of iterations (default: 100) + * @param {number} options.tolerance - Convergence tolerance (default: 0.0001) + * @param {string} options.initialization - Initialization method: 'random' or 'kmeans++' (default: 'kmeans++') + * @param {number} options.seed - Random seed for deterministic results (default: undefined) + * @param {number} options.restarts - Number of restarts to pick the best run (default: 1) + */ + constructor (k, options = {}) { + this.k = k + this.maxIterations = options.maxIterations || 100 + this.tolerance = options.tolerance || 0.0001 + this.initialization = options.initialization || 'kmeans++' + this.seed = options.seed + this.restarts = options.restarts || 1 + this.centroids = null + this.clusters = null + this.iterations = 0 + } + + /** + * Initialize centroids using random selection + */ + initializeRandomCentroids (data, rng) { + const centroids = [] + const indices = new Set() + + while (indices.size < this.k) { + const randomIndex = Math.floor(rng() * data.length) + if (!indices.has(randomIndex)) { + indices.add(randomIndex) + centroids.push([...data[randomIndex]]) + } + } + + return centroids + } + + /** + * Initialize centroids using k-means++ algorithm + * This tends to find better initial centroids than random selection + */ + initializeKMeansPlusPlusCentroids (data, rng) { + const centroids = [] + + // Choose first centroid randomly + const firstIndex = Math.floor(rng() * data.length) + centroids.push([...data[firstIndex]]) + + // Choose remaining centroids + for (let i = 1; i < this.k; i++) { + const distances = data.map(point => { + // Find minimum distance to existing centroids + let minDist = Infinity + for (const centroid of centroids) { + const dist = euclideanDistance(point, centroid) + if (dist < minDist) { + minDist = dist + } + } + return minDist * minDist // Square the distance for probability weighting + }) + + // Calculate total distance + const totalDist = distances.reduce((sum, d) => sum + d, 0) + + // Choose next centroid with probability proportional to distance squared + let random = rng() * totalDist + let selectedIndex = 0 + + for (let j = 0; j < distances.length; j++) { + random -= distances[j] + if (random <= 0) { + selectedIndex = j + break + } + } + + centroids.push([...data[selectedIndex]]) + } + + return centroids + } + + /** + * Assign each data point to the nearest centroid + */ + assignPointsToClusters (data, centroids = this.centroids) { + const clusters = Array.from({ length: this.k }, () => []) + const assignments = new Array(data.length) + + for (let i = 0; i < data.length; i++) { + let minDistance = Infinity + let clusterIndex = 0 + + for (let j = 0; j < this.k; j++) { + const distance = euclideanDistanceSquared(data[i], centroids[j]) + if (distance < minDistance) { + minDistance = distance + clusterIndex = j + } + } + + clusters[clusterIndex].push(i) + assignments[i] = clusterIndex + } + + return { clusters, assignments } + } + + /** + * Update centroids based on the mean of points in each cluster + */ + updateCentroids (data, clusters, currentCentroids) { + const newCentroids = [] + + for (let i = 0; i < this.k; i++) { + if (clusters[i].length === 0) { + // Keep old centroid if cluster is empty + newCentroids.push([...currentCentroids[i]]) + } else { + const clusterPoints = clusters[i].map(idx => data[idx]) + newCentroids.push(calculateMean(clusterPoints)) + } + } + + return newCentroids + } + + /** + * Check if centroids have converged + */ + hasConverged (oldCentroids, newCentroids) { + for (let i = 0; i < this.k; i++) { + const distance = euclideanDistance(oldCentroids[i], newCentroids[i]) + if (distance > this.tolerance) { + return false + } + } + return true + } + + /** + * Compute total inertia (sum of squared distances to centroids) + */ + calculateInertia (data, centroids, assignments) { + let total = 0 + for (let i = 0; i < data.length; i++) { + const centroid = centroids[assignments[i]] + total += euclideanDistanceSquared(data[i], centroid) + } + return total + } + + /** + * Run a single K-Means fit with a given RNG + */ + runSingleFit (data, rng) { + let centroids + + if (this.initialization === 'kmeans++') { + centroids = this.initializeKMeansPlusPlusCentroids(data, rng) + } else { + centroids = this.initializeRandomCentroids(data, rng) + } + + let clusters = null + let assignments = null + let iterations = 0 + + // Iterate until convergence or max iterations + for (let iter = 0; iter < this.maxIterations; iter++) { + iterations = iter + 1 + + // Assign points to clusters + const result = this.assignPointsToClusters(data, centroids) + clusters = result.clusters + assignments = result.assignments + + // Update centroids + const newCentroids = this.updateCentroids(data, clusters, centroids) + + // Check for convergence + if (this.hasConverged(centroids, newCentroids)) { + centroids = newCentroids + break + } + + centroids = newCentroids + } + + const inertia = this.calculateInertia(data, centroids, assignments) + return { centroids, clusters, assignments, iterations, inertia } + } + + /** + * Fit the model to the data + * @param {Array>} data - Array of data points (vectors) + * @returns {KMeansModernized} - Returns this for chaining + */ + fit (data) { + if (!data || data.length === 0) { + throw new Error('Data cannot be empty') + } + + if (data.length < this.k) { + throw new Error(`Number of data points (${data.length}) must be >= k (${this.k})`) + } + + const restarts = this.restarts && this.restarts > 0 ? this.restarts : 1 + let best = null + + for (let r = 0; r < restarts; r++) { + const seed = typeof this.seed === 'number' && isFinite(this.seed) ? this.seed + r : undefined + const rng = createRng(seed) + const result = this.runSingleFit(data, rng) + + if (!best || result.inertia < best.inertia) { + best = result + } + } + + this.centroids = best.centroids + this.clusters = best.clusters + this.assignments = best.assignments + this.iterations = best.iterations + + return this + } + + /** + * Predict cluster assignments for new data points + * @param {Array>} data - Array of data points + * @returns {Array} - Array of cluster indices + */ + predict (data) { + if (!this.centroids) { + throw new Error('Model must be fitted before prediction') + } + + if (!Array.isArray(data[0])) { + // Single data point + data = [data] + } + + const predictions = [] + for (const point of data) { + let minDistance = Infinity + let clusterIndex = 0 + + for (let j = 0; j < this.k; j++) { + const distance = euclideanDistance(point, this.centroids[j]) + if (distance < minDistance) { + minDistance = distance + clusterIndex = j + } + } + + predictions.push(clusterIndex) + } + + return predictions + } + + /** + * Get the centroids + * @returns {Array>} - Cluster centroids + */ + getCentroids () { + return this.centroids + } + + /** + * Get cluster assignments for the training data + * @returns {Array} - Array of cluster indices + */ + getAssignments () { + return this.assignments + } + + /** + * Get clusters (indices of points in each cluster) + * @returns {Array>} - Array of clusters + */ + getClusters () { + return this.clusters + } +} + +// Export utility functions for use by other modules +KMeansModernized.euclideanDistance = euclideanDistance +KMeansModernized.euclideanDistanceSquared = euclideanDistanceSquared +KMeansModernized.createRng = createRng +KMeansModernized.calculateMean = calculateMean + +module.exports = KMeansModernized diff --git a/lib/apparatus/clusterer/kmeans.d.ts b/lib/apparatus/clusterer/kmeans.d.ts new file mode 100644 index 0000000..c42e291 --- /dev/null +++ b/lib/apparatus/clusterer/kmeans.d.ts @@ -0,0 +1,73 @@ +/* +Copyright (c) 2026, Hugo W.L. ter Doest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* +Backwards compatibility wrapper for the original Apparatus KMeans API + +This module provides the original Apparatus API for K-Means clustering. +The implementation uses the modernized KMeans internally. + +For the modern API with additional features, see kmeans-modernized.d.ts +*/ + +import { VectorLike } from './vector-like' + +/** + * KMeans - Original Apparatus API + * + * Supports the old Apparatus API: + * new KMeans(observations) + * kmeans.cluster(k) + * kmeans.createCentroids(k) + * kmeans.distanceFrom(centroids) + */ +export class KMeans { + Observations: number[][] + + /** + * Initialize with observations (old Apparatus API) + * @param observations - Data points + */ + constructor (observations: number[][]) + + /** + * Cluster the observations into k clusters (old Apparatus API) + * @param k - Number of clusters + * @returns Cluster assignments for each observation (Sylvester-compatible vector) + */ + cluster (k: number): VectorLike + + /** + * Create initial centroids (old Apparatus API) + * @param k - Number of clusters + * @returns Initial centroid positions + */ + createCentroids (k: number): number[][] + + /** + * Calculate distances from observations to centroids (old Apparatus API) + * @param centroids - Centroid positions + * @returns Distance matrix + */ + distanceFrom (centroids: number[][]): number[][] +} + diff --git a/lib/apparatus/clusterer/kmeans.js b/lib/apparatus/clusterer/kmeans.js index d862b2d..e571691 100644 --- a/lib/apparatus/clusterer/kmeans.js +++ b/lib/apparatus/clusterer/kmeans.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2011, Chris Umbel +Copyright (c) 2026, Hugo W.L. ter Doest Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -20,99 +20,123 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var Sylvester = require('sylvester'), -Matrix = Sylvester.Matrix, -Vector = Sylvester.Vector; - -function KMeans(Observations) { - if(!Observations.elements) - Observations = $M(Observations); - - this.Observations = Observations; -} +/* +Backwards compatibility wrapper for the original Apparatus KMeans API -// create an initial centroid matrix with initial values between -// 0 and the max of feature data X. -function createCentroids(k) { - var Centroid = []; - var maxes = this.Observations.maxColumns(); - //console.log(maxes); +This module provides the original Apparatus API for K-Means clustering. +It extends KMeansModernized with a legacy-compatible interface. - for(var i = 1; i <= k; i++) { - var centroid = []; - for(var j = 1; j <= this.Observations.cols(); j++) { - centroid.push(Math.random() * maxes.e(j)); - } +Old API usage: + const kmeans = new KMeans([[1,2], [3,4], [5,6]]); + const assignments = kmeans.cluster(3); +*/ - Centroid.push(centroid); +'use strict' + +const KMeansModernized = require('./kmeans-modernized') +const VectorLike = require('./vector-like') + +// Use utility functions from KMeansModernized +const { euclideanDistance, createRng } = KMeansModernized + +/** + * KMeans - Original Apparatus API wrapper + * + * Extends KMeansModernized to provide backward-compatible API: + * new KMeans(observations) + * kmeans.cluster(k) + * kmeans.createCentroids(k) + * kmeans.distanceFrom(centroids) + */ +class KMeans extends KMeansModernized { + /** + * Initialize with observations (old Apparatus API) + * @param {Array>} observations - Data points + */ + constructor (observations) { + // Call parent with placeholder k (will be set in cluster()) + super(1) + + if (!Array.isArray(observations)) { + throw new Error('KMeans expects an array of observations') } - //console.log(centroid) - - return $M(Centroid); -} - -// get the euclidian distance between the feature data X and -// a given centroid matrix C. -function distanceFrom(Centroids) { - var distances = []; - - for(var i = 1; i <= this.Observations.rows(); i++) { - var distance = []; - - for(var j = 1; j <= Centroids.rows(); j++) { - distance.push(this.Observations.row(i).distanceFrom(Centroids.row(j))); - } - - distances.push(distance); + if (observations.length === 0) { + throw new Error('Observations cannot be empty') } - return $M(distances); -} + if (!Array.isArray(observations[0])) { + throw new Error('Observations must be an array of arrays') + } -// categorize the feature data X into k clusters. return a vector -// containing the results. -function cluster(k) { - var Centroids = this.createCentroids(k); - var LastDistances = Matrix.Zero(this.Observations.rows(), this.Observations.cols()); - var Distances = this.distanceFrom(Centroids); - var Groups; + this.Observations = observations + } + + /** + * Cluster the observations into k clusters (old Apparatus API) + * @param {number} k - Number of clusters + * @returns {VectorLike} - Cluster assignments wrapped in Sylvester-compatible object + */ + cluster (k) { + if (!isFinite(k) || k < 1) { + throw new Error('k must be a positive integer') + } - while(!(LastDistances.eql(Distances))) { - Groups = Distances.minColumnIndexes(); - LastDistances = Distances; + if (k > this.Observations.length) { + throw new Error(`k (${k}) cannot be greater than number of observations (${this.Observations.length})`) + } - var newCentroids = []; + // Set k and fit using parent's fit method + this.k = k + this.fit(this.Observations) + + // Return assignments wrapped in Sylvester-like Vector for backwards compatibility + // VectorLike handles 0->1 index conversion lazily + return new VectorLike(this.getAssignments()) + } + + /** + * Create initial centroids (old Apparatus API) + * @param {number} k - Number of clusters + * @returns {Array>} - Initial centroid positions + */ + createCentroids (k) { + if (!isFinite(k) || k < 1) { + throw new Error('k must be a positive integer') + } - for(var i = 1; i <= Centroids.rows(); i++) { - var centroid = []; + const rng = createRng() + return this.initializeRandomCentroids(this.Observations, rng) + } + + /** + * Calculate distances from observations to centroids (old Apparatus API) + * @param {Array>} centroids - Centroid positions + * @returns {Array>} - Distance matrix + */ + distanceFrom (centroids) { + if (!Array.isArray(centroids)) { + throw new Error('centroids must be an array') + } - for(var j = 1; j <= Centroids.cols(); j++) { - var sum = 0; - var count = 0; + if (centroids.length === 0) { + throw new Error('centroids cannot be empty') + } - for(var l = 1; l <= this.Observations.rows(); l++) { - if(Groups.e(l) == i) { - count++; - sum += this.Observations.e(l, j); - } - } + const distances = [] - centroid.push(sum / count); - } + for (let i = 0; i < this.Observations.length; i++) { + const distRow = [] - newCentroids.push(centroid); - } + for (let j = 0; j < centroids.length; j++) { + distRow.push(euclideanDistance(this.Observations[i], centroids[j])) + } - Centroids = $M(newCentroids); - Distances = this.distanceFrom(Centroids); + distances.push(distRow) } - return Groups; + return distances + } } -KMeans.prototype.createCentroids = createCentroids; -KMeans.prototype.distanceFrom = distanceFrom; -KMeans.prototype.cluster = cluster; - -module.exports = KMeans; +module.exports = KMeans diff --git a/lib/apparatus/clusterer/vector-like.js b/lib/apparatus/clusterer/vector-like.js new file mode 100644 index 0000000..e648c4f --- /dev/null +++ b/lib/apparatus/clusterer/vector-like.js @@ -0,0 +1,59 @@ +/* +Copyright (c) 2026, Hugo W.L. ter Doest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +'use strict' + +/** + * VectorLike - Sylvester Vector API compatible wrapper + * Makes arrays compatible with Sylvester Vector API without needing Sylvester + * The legacy API used Sylvester Vectors which have .elements and .e(i) + */ +class VectorLike { + /** + * Create a VectorLike object + * @param {Array} zeroIndexedArray - 0-indexed array of cluster assignments + */ + constructor (zeroIndexedArray) { + // Store 0-indexed array internally for efficiency + this._array = zeroIndexedArray + } + + /** + * Get element at 1-based index (Sylvester compatibility) + * Converts from 0-indexed to 1-indexed on the fly + * @param {number} i - 1-based index + * @returns {number} - Element value (1-indexed cluster assignment) + */ + e (i) { + return this._array[i - 1] + 1 + } + + /** + * Getter for elements array + * Converts from 0-indexed to 1-indexed for Sylvester compatibility + */ + get elements () { + return this._array.map(a => a + 1) + } +} + +module.exports = VectorLike diff --git a/lib/apparatus/index.js b/lib/apparatus/index.js index 922b686..51f122b 100644 --- a/lib/apparatus/index.js +++ b/lib/apparatus/index.js @@ -1,4 +1,4 @@ - -exports.BayesClassifier = require('./classifier/bayes_classifier'); -exports.LogisticRegressionClassifier = require('./classifier/logistic_regression_classifier'); -exports.KMeans = require('./clusterer/kmeans'); +exports.BayesClassifier = require('./classifier/bayes_classifier') +exports.LogisticRegressionClassifier = require('./classifier/logistic_regression_classifier') +exports.RandomForestClassifier = require('./classifier/randomforest_classifier') +exports.KMeans = require('./clusterer/kmeans') diff --git a/package-lock.json b/package-lock.json index e12144b..8111fad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,162 @@ "sylvester": ">= 0.0.8" }, "devDependencies": { - "jasmine": "^6.0.0" + "jasmine": "^6.0.0", + "standard": "^17.1.0" }, "engines": { "node": ">=0.2.6" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -43,6 +193,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -54,6 +242,67 @@ "node": ">=14" } }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -80,173 +329,2527 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "MIT" + "license": "Python-2.0" }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/jasmine": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-6.0.0.tgz", - "integrity": "sha512-eSPL6LPWT39WwvHSEEbRXuSvioXMTheNhIPaeUT1OPmSprDZwj4S29884DkTx6/tyiOWTWB1N+LdW2ZSg74aEA==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { - "@jasminejs/reporters": "^1.0.0", - "glob": "^10.2.2 || ^11.0.3 || ^12.0.0 || ^13.0.0", - "jasmine-core": "~6.0.0" + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, - "bin": { - "jasmine": "bin/jasmine.js" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jasmine-core": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-6.0.1.tgz", - "integrity": "sha512-gUtzV5ASR0MLBwDNqri4kBsgKNCcRQd9qOlNw/w/deavD0cl3JmWXXfH8JhKM4LTg6LPTt2IOQ4px3YYfgh2Xg==", + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, - "license": "ISC" - }, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/builtins/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-config-standard-jsx": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-11.0.0.tgz", + "integrity": "sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peerDependencies": { + "eslint": "^8.8.0", + "eslint-plugin-react": "^7.28.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", + "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-6.0.0.tgz", + "integrity": "sha512-eSPL6LPWT39WwvHSEEbRXuSvioXMTheNhIPaeUT1OPmSprDZwj4S29884DkTx6/tyiOWTWB1N+LdW2ZSg74aEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jasminejs/reporters": "^1.0.0", + "glob": "^10.2.2 || ^11.0.3 || ^12.0.0 || ^13.0.0", + "jasmine-core": "~6.0.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-6.0.1.tgz", + "integrity": "sha512-gUtzV5ASR0MLBwDNqri4kBsgKNCcRQd9qOlNw/w/deavD0cl3JmWXXfH8JhKM4LTg6LPTt2IOQ4px3YYfgh2Xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/load-json-file/node_modules/type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -254,80 +2857,924 @@ "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", + "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0", + "load-json-file": "^5.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-conf/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "*" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, - "license": "BlueOak-1.0.0" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/signal-exit": { @@ -343,6 +3790,88 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/standard": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/standard/-/standard-17.1.2.tgz", + "integrity": "sha512-WLm12WoXveKkvnPnPnaFUUHuOB2cUdAsJ4AiGHL2G0UNMrcRAWY2WriQaV8IQ3oRmYr0AWUbLNr94ekYFAHOrA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "eslint": "^8.41.0", + "eslint-config-standard": "17.1.0", + "eslint-config-standard-jsx": "^11.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.36.1", + "standard-engine": "^15.1.0", + "version-guard": "^1.1.1" + }, + "bin": { + "standard": "bin/cmd.cjs" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/standard-engine": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-15.1.0.tgz", + "integrity": "sha512-VHysfoyxFu/ukT+9v49d4BRXIokFRZuH3z1VRxzFArZdjSCFpro6rEIU3ji7e4AoAtuSfKBkiOmsrDqKW5ZSRw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "get-stdin": "^8.0.0", + "minimist": "^1.2.6", + "pkg-conf": "^3.1.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -407,6 +3936,104 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -447,6 +4074,55 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/sylvester": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/sylvester/-/sylvester-0.0.21.tgz", @@ -455,6 +4131,169 @@ "node": ">=0.2.6" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/version-guard": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/version-guard/-/version-guard-1.1.3.tgz", + "integrity": "sha512-JwPr6erhX53EWH/HCSzfy1tTFrtPXUe927wdM1jqBBeYp1OM+qPHjWbsvv6pIBduqdgxxS+ScfG7S28pzyr2DQ==", + "dev": true, + "license": "0BSD", + "engines": { + "node": ">=0.10.48" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -471,6 +4310,105 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -568,6 +4506,36 @@ "engines": { "node": ">=8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 344a1d4..b783f27 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,14 @@ "sylvester": ">= 0.0.8" }, "devDependencies": { - "jasmine": "^6.0.0" + "jasmine": "^6.0.0", + "standard": "^17.1.0" }, "scripts": { - "test": "jasmine" + "test": "jasmine", + "benchmark": "node benchmark/kmeans_benchmark.js && node benchmark/logistic_regression_benchmark.js", + "lint": "standard", + "lint:fix": "standard --fix" }, "author": "Chris Umbel ", "keywords": [ @@ -32,6 +36,7 @@ "regression" ], "main": "./lib/apparatus/index.js", + "types": "./index.d.ts", "maintainers": [ { "name": "Chris Umbel", diff --git a/spec/bayes_classifier_spec.js b/spec/bayes_classifier_spec.js index 215c74e..bf1c5cd 100644 --- a/spec/bayes_classifier_spec.js +++ b/spec/bayes_classifier_spec.js @@ -20,102 +20,103 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var BayesClassifier = new require('../lib/apparatus/classifier/bayes_classifier'); - -describe('bayes', function() { - it('should throw if not trained', function() { - var bayes = new BayesClassifier(); - expect(function() { bayes.classify([0,0,0,0,1,1]) }).toThrow(); - }); - - it('should perform binary classifcation', function() { - var bayes = new BayesClassifier(); - bayes.addExample([1,1,1,0,0,0], 'one'); - bayes.addExample([1,0,1,0,0,0], 'one'); - bayes.addExample([1,1,1,0,0,0], 'one'); - bayes.addExample([0,0,0,1,1,1], 'two'); - bayes.addExample([0,0,0,1,0,1], 'two'); - bayes.addExample([0,0,0,1,1,0], 'two'); - - bayes.train(); - - expect(bayes.classify([1,1,0,0,0,0])).toBe('one'); - expect(bayes.classify([0,0,0,0,1,1])).toBe('two'); - }); - - it('should classify', function() { - var bayes = new BayesClassifier(); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([1,0,1,0,0,0,0,0,0], 'one'); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,0,1,1,1,0,0,0], 'two'); - bayes.addExample([0,0,0,1,0,1,0,0,0], 'two'); - bayes.addExample([0,0,0,1,1,0,0,0,0], 'two'); - bayes.addExample([0,0,0,0,0,0,1,1,1], 'three'); - bayes.addExample([0,0,0,0,0,0,1,0,1], 'three'); - bayes.addExample([0,0,0,0,0,0,1,1,0], 'three'); - - bayes.train(); - - expect(bayes.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(bayes.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(bayes.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - - }); - - it('should classify with deserialized classifier', function() { - var bayes = new BayesClassifier(); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([1,0,1,0,0,0,0,0,0], 'one'); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,0,1,1,1,0,0,0], 'two'); - bayes.addExample([0,0,0,1,0,1,0,0,0], 'two'); - bayes.addExample([0,0,0,1,1,0,0,0,0], 'two'); - bayes.addExample([0,0,0,0,0,0,1,1,1], 'three'); - bayes.addExample([0,0,0,0,0,0,1,0,1], 'three'); - bayes.addExample([0,0,0,0,0,0,1,1,0], 'three'); - - bayes.train(); - - var obj = JSON.stringify(bayes); - var newBayes = BayesClassifier.restore(JSON.parse(obj)); - - expect(newBayes.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(newBayes.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(newBayes.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - }); - - it('should classify with smoothing', function() { - var bayes = new BayesClassifier(0.3); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,0,1,1,1,0,0,0], 'two'); - bayes.addExample([0,0,0,0,0,1,0,0,0], 'two'); - bayes.addExample([0,0,0,0,1,0,0,0,0], 'two'); - bayes.addExample([0,0,0,0,0,0,1,1,1], 'three'); - bayes.addExample([0,0,0,0,0,0,0,0,1], 'three'); - bayes.addExample([0,0,0,0,0,0,0,1,0], 'three'); - - bayes.train(); - - expect(bayes.classify([1,0,0,0,0,0,1,0,0])).toBe('one'); - expect(bayes.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(bayes.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - }); - - it('should classify with sparse observations', function() { - var bayes = new BayesClassifier(); - bayes.addExample({'a': 1, 'b': 'a', 'c': false}, 'one'); - bayes.addExample({'a': 1, 'b': 'b', 'c': false}, 'one'); - bayes.addExample({'a': 4, 'b': 'c', 'c': true}, 'one'); - bayes.addExample({'a': 2, 'b': 'c', 'c': false}, 'two'); - bayes.addExample({'a': 2, 'b': 'd', 'c': false}, 'two'); - bayes.addExample({'a': 2, 'b': 'e'}, 'two'); - - bayes.train(); - - expect(bayes.classify({'a': 1, 'f': 'e', 'c': true})).toBe('one'); - expect(bayes.classify({'a': 2, 'f': 'r'})).toBe('two'); - }); -}); +/* global describe, it, expect */ + +const BayesClassifier = require('../lib/apparatus/classifier/bayes_classifier') + +describe('bayes', function () { + it('should throw if not trained', function () { + const bayes = new BayesClassifier() + expect(function () { bayes.classify([0, 0, 0, 0, 1, 1]) }).toThrow() + }) + + it('should perform binary classifcation', function () { + const bayes = new BayesClassifier() + bayes.addExample([1, 1, 1, 0, 0, 0], 'one') + bayes.addExample([1, 0, 1, 0, 0, 0], 'one') + bayes.addExample([1, 1, 1, 0, 0, 0], 'one') + bayes.addExample([0, 0, 0, 1, 1, 1], 'two') + bayes.addExample([0, 0, 0, 1, 0, 1], 'two') + bayes.addExample([0, 0, 0, 1, 1, 0], 'two') + + bayes.train() + + expect(bayes.classify([1, 1, 0, 0, 0, 0])).toBe('one') + expect(bayes.classify([0, 0, 0, 0, 1, 1])).toBe('two') + }) + + it('should classify', function () { + const bayes = new BayesClassifier() + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + + bayes.train() + + expect(bayes.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(bayes.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(bayes.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should classify with deserialized classifier', function () { + const bayes = new BayesClassifier() + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + + bayes.train() + + const obj = JSON.stringify(bayes) + const newBayes = BayesClassifier.restore(JSON.parse(obj)) + + expect(newBayes.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(newBayes.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(newBayes.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should classify with smoothing', function () { + const bayes = new BayesClassifier(0.3) + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 0, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 1, 0, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 0, 0, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 0, 1, 0], 'three') + + bayes.train() + + expect(bayes.classify([1, 0, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(bayes.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(bayes.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should classify with sparse observations', function () { + const bayes = new BayesClassifier() + bayes.addExample({ a: 1, b: 'a', c: false }, 'one') + bayes.addExample({ a: 1, b: 'b', c: false }, 'one') + bayes.addExample({ a: 4, b: 'c', c: true }, 'one') + bayes.addExample({ a: 2, b: 'c', c: false }, 'two') + bayes.addExample({ a: 2, b: 'd', c: false }, 'two') + bayes.addExample({ a: 2, b: 'e' }, 'two') + + bayes.train() + + expect(bayes.classify({ a: 1, f: 'e', c: true })).toBe('one') + expect(bayes.classify({ a: 2, f: 'r' })).toBe('two') + }) +}) diff --git a/spec/kmeans_comparison_spec.js b/spec/kmeans_comparison_spec.js new file mode 100644 index 0000000..de51e18 --- /dev/null +++ b/spec/kmeans_comparison_spec.js @@ -0,0 +1,140 @@ +/* +Comparison tests between KMeans (modernized) and KMeans-Legacy (Sylvester) +Verifies both implementations produce equivalent results +*/ + +/* global describe, it, expect */ + +'use strict' + +const KMeans = require('../lib/apparatus/clusterer/kmeans') +const KMeansLegacy = require('../lib/apparatus/clusterer/kmeans-legacy') + +describe('KMeans vs KMeans-Legacy Comparison', () => { + const testData = [ + [1, 2], + [1.5, 1.8], + [5, 8], + [8, 8], + [1, 0.6], + [9, 11], + [8, 2], + [10, 2], + [9, 3] + ] + + it('should produce same cluster assignments with k=2', () => { + const kmeans = new KMeans(testData) + const modernResult = kmeans.cluster(2) + + const kmeansLegacy = new KMeansLegacy(testData) + const legacyResult = kmeansLegacy.cluster(2) + + // Both should return cluster assignments for all points + expect(modernResult.elements.length).toBe(testData.length) + expect(legacyResult.elements.length).toBe(testData.length) + + // All assignments should be valid cluster numbers (1 or 2) + modernResult.elements.forEach(assignment => { + expect([1, 2]).toContain(assignment) + }) + + legacyResult.elements.forEach(assignment => { + expect([1, 2]).toContain(assignment) + }) + }) + + it('should produce same cluster assignments with k=3', () => { + const kmeans = new KMeans(testData) + const modernResult = kmeans.cluster(3) + + const kmeansLegacy = new KMeansLegacy(testData) + const legacyResult = kmeansLegacy.cluster(3) + + // Both should return cluster assignments for all points + expect(modernResult.elements.length).toBe(testData.length) + expect(legacyResult.elements.length).toBe(testData.length) + + // All assignments should be valid cluster numbers (1, 2, or 3) + modernResult.elements.forEach(assignment => { + expect([1, 2, 3]).toContain(assignment) + }) + + legacyResult.elements.forEach(assignment => { + expect([1, 2, 3]).toContain(assignment) + }) + }) + + it('should have consistent clustering - same data produces same results in multiple runs', () => { + // Run modernized version twice + const kmeans1 = new KMeans(testData) + const result1 = kmeans1.cluster(2) + + const kmeans2 = new KMeans(testData) + const result2 = kmeans2.cluster(2) + + // Results should be identical (or inverse/permuted, which is expected for k-means) + // At minimum, we check the structure is the same + expect(result1.elements.length).toBe(result2.elements.length) + }) + + it('should correctly assign nearby points to same cluster', () => { + // Points [1, 2] and [1.5, 1.8] are very close + // Points [5, 8] and [8, 8] are close + const kmeans = new KMeans(testData) + const result = kmeans.cluster(2) + + const assignments = result.elements + + // Point 0: [1, 2] + const cluster0 = assignments[0] + const cluster1 = assignments[1] // [1.5, 1.8] + + // These two close points should be in the same cluster + expect(cluster0).toBe(cluster1) + }) + + it('should match legacy implementation clustering logic', () => { + const smallData = [ + [0, 0], + [1, 1], + [10, 10], + [11, 11] + ] + + const kmeans = new KMeans(smallData) + const modernResult = kmeans.cluster(2) + + const kmeansLegacy = new KMeansLegacy(smallData) + const legacyResult = kmeansLegacy.cluster(2) + + // Both should assign [0,0] and [1,1] to same cluster + const modernAssignments = modernResult.elements + const legacyAssignments = legacyResult.elements + + expect(modernAssignments[0]).toBe(modernAssignments[1]) + expect(legacyAssignments[0]).toBe(legacyAssignments[1]) + + // Both should assign [10,10] and [11,11] to same cluster + expect(modernAssignments[2]).toBe(modernAssignments[3]) + expect(legacyAssignments[2]).toBe(legacyAssignments[3]) + }) + + it('VectorLike should provide same interface as Sylvester Vector', () => { + const kmeans = new KMeans(testData) + const result = kmeans.cluster(2) + + // Check Sylvester-compatible interface + expect(typeof result.e).toBe('function') + expect(typeof result.elements).toBe('object') + expect(Array.isArray(result.elements)).toBe(true) + + // Check 1-based indexing (Sylvester style) + const firstElement = result.e(1) + expect(typeof firstElement).toBe('number') + expect([1, 2]).toContain(firstElement) + + // elements should be 1-indexed + expect(result.elements[0]).toBeGreaterThanOrEqual(1) + }) +}) diff --git a/spec/kmeans_spec.js b/spec/kmeans_spec.js index e1a3aa8..7cc9ab1 100644 --- a/spec/kmeans_spec.js +++ b/spec/kmeans_spec.js @@ -19,71 +19,71 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/* global describe, it, expect */ +const KMeans = require('.././lib/apparatus/clusterer/kmeans') -var KMeans = require('.././lib/apparatus/clusterer/kmeans'); +describe('kmeans', function () { + it('should return cluster assignments for all observations', function () { + const observations = [ + [1, 1], + [2, 2], + [10, 10], + [11, 11] + ] -describe('kmeans', function() { - it('should return cluster assignments for all observations', function() { - var observations = [ - [1, 1], - [2, 2], - [10, 10], - [11, 11] - ]; + const kmeans = new KMeans(observations) + const clusters = kmeans.cluster(2) - var kmeans = new KMeans(observations); - var clusters = kmeans.cluster(2); + // Verify we got cluster assignments for all points + expect(clusters.elements.length).toBe(4) - // Verify we got cluster assignments for all points - expect(clusters.elements.length).toBe(4); - - // All cluster assignments should be valid numbers (1 or 2) - for (var i = 1; i <= 4; i++) { - expect(clusters.e(i)).toBeGreaterThan(0); - expect(clusters.e(i)).toBeLessThanOrEqual(2); - } - }); + // All cluster assignments should be valid numbers (1 or 2) + for (let i = 1; i <= 4; i++) { + expect(clusters.e(i)).toBeGreaterThan(0) + expect(clusters.e(i)).toBeLessThanOrEqual(2) + } + }) - it('should handle single cluster', function() { - var observations = [ - [1, 1], - [1.5, 1.5], - [2, 2] - ]; + it('should handle single cluster', function () { + const observations = [ + [1, 1], + [1.5, 1.5], + [2, 2] + ] - var kmeans = new KMeans(observations); - var clusters = kmeans.cluster(1); + const kmeans = new KMeans(observations) + const clusters = kmeans.cluster(1) - // All points should be in the same cluster - expect(clusters.e(1)).toBe(1); - expect(clusters.e(2)).toBe(1); - expect(clusters.e(3)).toBe(1); - }); + // All points should be in the same cluster + expect(clusters.e(1)).toBe(1) + expect(clusters.e(2)).toBe(1) + expect(clusters.e(3)).toBe(1) + }) - it('should cluster nearby points together', function() { - var observations = [ - [0, 0], - [0.1, 0.1], - [0.2, 0.2], - [100, 100], - [100.1, 100.1], - [100.2, 100.2] - ]; + it('should cluster nearby points together', function () { + const observations = [ + [0, 0], + [0.1, 0.1], + [0.2, 0.2], + [100, 100], + [100.1, 100.1], + [100.2, 100.2] + ] - var kmeans = new KMeans(observations); - var clusters = kmeans.cluster(2); + const kmeans = new KMeans(observations) + const clusters = kmeans.cluster(2) - // Points 1-3 should be in the same cluster (they're very close) - var cluster1 = clusters.e(1); - expect(clusters.e(2)).toBe(cluster1); - expect(clusters.e(3)).toBe(cluster1); - - // Points 4-6 should be in the same cluster (they're very close) - var cluster2 = clusters.e(4); - expect(clusters.e(5)).toBe(cluster2); - expect(clusters.e(6)).toBe(cluster2); - - // The two clusters should be different - expect(cluster1).not.toBe(cluster2); - }); -}); + // Points 1-3 should be in the same cluster (they're very close) + const cluster1 = clusters.e(1) + expect(clusters.e(2)).toBe(cluster1) + expect(clusters.e(3)).toBe(cluster1) + + // Points 4-6 should be in the same cluster (they're very close) + const cluster2 = clusters.e(4) + expect(clusters.e(5)).toBe(cluster2) + expect(clusters.e(6)).toBe(cluster2) + + // The two clusters should be different + expect(cluster1).not.toBe(cluster2) + }) +}) diff --git a/spec/logistic_regression_classifier_spec.js b/spec/logistic_regression_classifier_spec.js index 50ed6b0..0952884 100644 --- a/spec/logistic_regression_classifier_spec.js +++ b/spec/logistic_regression_classifier_spec.js @@ -20,99 +20,100 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var LogisticRegressionClassifier = new require('../lib/apparatus/classifier/logistic_regression_classifier'); - -describe('logistic regression', function() { - it('should classify with examples added in groups', function() { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1,1,1,0,0,0], 'one'); - logistic.addExample([1,0,1,0,0,0], 'one'); - logistic.addExample([1,1,1,0,0,0], 'one'); - logistic.addExample([0,0,0,1,1,1], 'two'); - logistic.addExample([0,0,0,1,0,1], 'two'); - logistic.addExample([0,0,0,1,1,0], 'two'); - - logistic.train(); - - expect(logistic.classify([0,1,1,0,0,0])).toBe('one'); - expect(logistic.classify([0,0,0,0,1,1])).toBe('two'); - }); - - it('should classify - part 1', function() { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,0,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([0,0,0,1,1,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,0,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,1,0,0,0,0], 'two'); - logistic.addExample([0,0,0,0,0,0,1,1,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,0,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,1,0], 'three'); - - logistic.train(); - - expect(logistic.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(logistic.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(logistic.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - - }); - - it('should allow retraining', function() { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,0,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([0,0,0,1,1,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,0,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,1,0,0,0,0], 'two'); - logistic.train(); - logistic.addExample([0,0,0,0,0,0,1,1,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,0,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,1,0], 'three'); - logistic.train(); - - expect(logistic.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(logistic.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(logistic.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - }); - - it('should classify part - part 2', function() { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,0,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([0,0,0,1,1,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,0,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,1,0,0,0,0], 'two'); - logistic.addExample([0,0,0,0,0,0,1,1,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,0,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,1,0], 'three'); - - logistic.train(); - - var obj = JSON.stringify(logistic); - var newLogistic = LogisticRegressionClassifier.restore(JSON.parse(obj)); - - expect(newLogistic.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(newLogistic.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(newLogistic.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - }); - - it('should not run into an infinite loop (1)', function () { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one'); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'two'); - - logistic.train(); - }); - - it('should not run into an infinite loop (2)', function () { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one'); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'two'); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'three'); - - logistic.train(); - }); -}); +/* global describe, it, expect */ + +const LogisticRegressionClassifier = require('../lib/apparatus/classifier/logistic-regression-modernized') + +describe('logistic regression', function () { + it('should classify with examples added in groups', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0], 'one') + logistic.addExample([1, 0, 1, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0], 'one') + logistic.addExample([0, 0, 0, 1, 1, 1], 'two') + logistic.addExample([0, 0, 0, 1, 0, 1], 'two') + logistic.addExample([0, 0, 0, 1, 1, 0], 'two') + + logistic.train() + + expect(logistic.classify([0, 1, 1, 0, 0, 0])).toBe('one') + expect(logistic.classify([0, 0, 0, 0, 1, 1])).toBe('two') + }) + + it('should classify - part 1', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + + logistic.train() + + expect(logistic.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(logistic.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(logistic.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should allow retraining', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + logistic.train() + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + logistic.train() + + expect(logistic.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(logistic.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(logistic.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should classify part - part 2', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + + logistic.train() + + const obj = JSON.stringify(logistic) + const newLogistic = LogisticRegressionClassifier.restore(JSON.parse(obj)) + + expect(newLogistic.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(newLogistic.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(newLogistic.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should not run into an infinite loop (1)', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'two') + + logistic.train() + }) + + it('should not run into an infinite loop (2)', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'two') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'three') + + logistic.train() + }) +}) diff --git a/spec/logistic_regression_comparison_spec.js b/spec/logistic_regression_comparison_spec.js new file mode 100644 index 0000000..7cc53cc --- /dev/null +++ b/spec/logistic_regression_comparison_spec.js @@ -0,0 +1,222 @@ +/* +Comparison tests between LogisticRegressionClassifier (modernized) and LogisticRegressionClassifier-Legacy (Sylvester) +Verifies both implementations produce equivalent results +*/ + +/* global describe, it, expect */ + +'use strict' + +const LogisticRegressionClassifier = require('../lib/apparatus/classifier/logistic_regression_classifier') +const LogisticRegressionClassifierLegacy = require('../lib/apparatus/classifier/logistic-regression-legacy') + +describe('LogisticRegressionClassifier vs Legacy Comparison', () => { + const trainingData = [ + { text: [1, 0, 1, 0], label: 'positive' }, + { text: [1, 1, 0, 0], label: 'positive' }, + { text: [0, 1, 1, 0], label: 'positive' }, + { text: [0, 0, 1, 1], label: 'negative' }, + { text: [0, 0, 0, 1], label: 'negative' }, + { text: [1, 0, 0, 1], label: 'negative' } + ] + + it('should both train successfully on same data', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + // Both should have learned theta values + expect(modernClassifier.theta).toBeDefined() + expect(modernClassifier.theta.length).toBeGreaterThan(0) + + expect(legacyClassifier.theta).toBeDefined() + expect(legacyClassifier.theta.length).toBeGreaterThan(0) + }) + + it('should both classify observations with consistent class labels', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + // Test observation + const testObservation = [1, 0, 1, 0] + + const modernResult = modernClassifier.getClassifications(testObservation) + const legacyResult = legacyClassifier.getClassifications(testObservation) + + // Both should return array of classifications + expect(Array.isArray(modernResult)).toBe(true) + expect(Array.isArray(legacyResult)).toBe(true) + + // Both should return results for same classes + expect(modernResult.length).toBe(legacyResult.length) + expect(modernResult.length).toBe(2) // positive and negative + + // Both should have label and value properties + modernResult.forEach(result => { + expect(result.label).toBeDefined() + expect(result.value).toBeDefined() + expect(typeof result.value).toBe('number') + expect(result.value).toBeGreaterThanOrEqual(0) + expect(result.value).toBeLessThanOrEqual(1) + }) + + legacyResult.forEach(result => { + expect(result.label).toBeDefined() + expect(result.value).toBeDefined() + expect(typeof result.value).toBe('number') + }) + }) + + it('should return sorted results by confidence', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + const testObservation = [1, 0, 1, 0] + + const modernResult = modernClassifier.getClassifications(testObservation) + const legacyResult = legacyClassifier.getClassifications(testObservation) + + // Both should return results sorted by value (descending) + for (let i = 0; i < modernResult.length - 1; i++) { + expect(modernResult[i].value).toBeGreaterThanOrEqual(modernResult[i + 1].value) + } + + for (let i = 0; i < legacyResult.length - 1; i++) { + expect(legacyResult[i].value).toBeGreaterThanOrEqual(legacyResult[i + 1].value) + } + }) + + it('should have consistent class labels in both implementations', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + const testObservation = [1, 0, 1, 0] + + const modernResult = modernClassifier.getClassifications(testObservation) + const legacyResult = legacyClassifier.getClassifications(testObservation) + + // Extract labels and sort for comparison + const modernLabels = modernResult.map(r => r.label).sort() + const legacyLabels = legacyResult.map(r => r.label).sort() + + expect(modernLabels).toEqual(legacyLabels) + }) + + it('should both classify training examples with reasonable confidence', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + // Test on a training example + const positiveExample = trainingData[0].text + + const modernResult = modernClassifier.getClassifications(positiveExample) + const legacyResult = legacyClassifier.getClassifications(positiveExample) + + // Top prediction should be the correct class + expect(modernResult[0].label).toBe('positive') + expect(legacyResult[0].label).toBe('positive') + + // Confidence should be reasonably high + expect(modernResult[0].value).toBeGreaterThan(0.3) + expect(legacyResult[0].value).toBeGreaterThan(0.3) + }) + + it('should produce results with value between 0 and 1 (sigmoid output)', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const testObservation = [1, 1, 0, 0] + const modernResult = modernClassifier.getClassifications(testObservation) + + modernResult.forEach(result => { + expect(result.value).toBeGreaterThanOrEqual(0) + expect(result.value).toBeLessThanOrEqual(1) + }) + }) + + it('should both handle multiple classes correctly', () => { + const multiClassData = [ + { text: [1, 0, 0], label: 'class_a' }, + { text: [1, 1, 0], label: 'class_a' }, + { text: [0, 1, 0], label: 'class_b' }, + { text: [0, 1, 1], label: 'class_b' }, + { text: [0, 0, 1], label: 'class_c' }, + { text: [1, 0, 1], label: 'class_c' } + ] + + const modernClassifier = new LogisticRegressionClassifier() + multiClassData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + multiClassData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + const testObservation = [1, 0, 0] + + const modernResult = modernClassifier.getClassifications(testObservation) + const legacyResult = legacyClassifier.getClassifications(testObservation) + + // Both should return 3 class predictions + expect(modernResult.length).toBe(3) + expect(legacyResult.length).toBe(3) + + // Both should contain all three classes + const modernLabels = modernResult.map(r => r.label).sort() + const legacyLabels = legacyResult.map(r => r.label).sort() + + expect(modernLabels).toEqual(['class_a', 'class_b', 'class_c']) + expect(legacyLabels).toEqual(['class_a', 'class_b', 'class_c']) + }) +}) diff --git a/spec/randomforest_classifier_spec.js b/spec/randomforest_classifier_spec.js index 9ca7125..6854d3d 100644 --- a/spec/randomforest_classifier_spec.js +++ b/spec/randomforest_classifier_spec.js @@ -21,35 +21,37 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var RandomForestClassifier = new require('../lib/apparatus/classifier/randomforest_classifier'); - -describe('randomforest', function() { - it('should perform binary classifcation', function() { - var randomforest = new RandomForestClassifier(); - - randomforest.addExample([-0.4326, 1.1909 ], 1); - randomforest.addExample([1.5 , 3.0 ], 1); - randomforest.addExample([0.1253 , -0.0376 ], 1); - randomforest.addExample([0.2877 , 0.3273 ], 1); - randomforest.addExample([-1.1465, 0.1746 ], 1); - randomforest.addExample([1.8133 , 2.1139 ], -1); - randomforest.addExample([2.7258 , 3.0668 ], -1); - randomforest.addExample([1.4117 , 2.0593 ], -1); - randomforest.addExample([4.1832 , 1.9044 ], -1); - randomforest.addExample([1.8636 , 1.1677 ], -1); - - randomforest.train(); - - expect(randomforest.classify([-0.5 , -0.5 ])).toBe(1); - - // random forest are not deterministic, check on average it works - var count = 0; - for(var tests=0; tests<200; tests++){ - randomforest.train(); - if(randomforest.classify([1.0, 2.0]) == 1) { - count++; - } - } - expect(count).toBeGreaterThan(50); - }); - }); +/* global describe, it, expect */ + +const RandomForestClassifier = require('../lib/apparatus/classifier/randomforest_classifier') + +describe('randomforest', function () { + it('should perform binary classifcation', function () { + const randomforest = new RandomForestClassifier() + + randomforest.addExample([-0.4326, 1.1909], 1) + randomforest.addExample([1.5, 3.0], 1) + randomforest.addExample([0.1253, -0.0376], 1) + randomforest.addExample([0.2877, 0.3273], 1) + randomforest.addExample([-1.1465, 0.1746], 1) + randomforest.addExample([1.8133, 2.1139], -1) + randomforest.addExample([2.7258, 3.0668], -1) + randomforest.addExample([1.4117, 2.0593], -1) + randomforest.addExample([4.1832, 1.9044], -1) + randomforest.addExample([1.8636, 1.1677], -1) + + randomforest.train() + + expect(randomforest.classify([-0.5, -0.5])).toBe(1) + + // random forest are not deterministic, check on average it works + let count = 0 + for (let tests = 0; tests < 200; tests++) { + randomforest.train() + if (randomforest.classify([1.0, 2.0]) === 1) { + count++ + } + } + expect(count).toBeGreaterThan(50) + }) +})