lawkit-js
Version:
A Node.js wrapper for the lawkit CLI tool - statistical law analysis toolkit for fraud detection, data quality assessment, and audit compliance.
517 lines (460 loc) • 17.7 kB
JavaScript
/**
* Node.js API wrapper for lawkit CLI tool
*
* This module provides a JavaScript API for the lawkit CLI tool,
* allowing you to analyze statistical laws programmatically.
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const { writeFileSync, mkdtempSync, rmSync } = require('fs');
const { tmpdir } = require('os');
/**
* @typedef {'benf'|'pareto'|'zipf'|'normal'|'poisson'|'analyze'|'validate'|'diagnose'|'generate'|'list'} Command
* @typedef {'text'|'json'|'csv'|'yaml'|'toml'|'xml'} OutputFormat
*/
/**
* Options for lawkit operations
* @typedef {Object} LawkitOptions
* @property {OutputFormat} [output='text'] - Output format (maps to --format option)
* @property {boolean} [quiet=false] - Suppress output
* @property {boolean} [verbose=false] - Verbose output
* @property {string} [filter] - Filter numbers by range
* @property {number} [minCount] - Minimum count threshold
*
* // Integration options
* @property {string} [laws] - Laws to analyze (comma-separated)
* @property {string} [focus] - Analysis focus area
* @property {number} [threshold] - Analysis threshold for anomaly detection
* @property {boolean} [recommend=false] - Enable recommendation mode
* @property {string} [report] - Analysis report type
* @property {boolean} [consistencyCheck=false] - Enable consistency check
* @property {boolean} [crossValidation=false] - Enable cross-validation
* @property {number} [confidenceLevel] - Confidence level
* @property {string} [purpose] - Analysis purpose
*
* // Benford-specific options
* @property {string} [thresholdLevel] - Anomaly detection threshold level
* @property {number} [confidence] - Statistical confidence level
* @property {number} [sampleSize] - Maximum sample size for large datasets
* @property {number} [minValue] - Minimum value to include in analysis
*
* // Pareto-specific options
* @property {number} [concentration] - Concentration threshold
* @property {boolean} [giniCoefficient=false] - Calculate Gini coefficient
* @property {string} [percentiles] - Custom percentiles to calculate
* @property {boolean} [businessAnalysis=false] - Enable business analysis
*
* // Zipf-specific options
* @property {boolean} [text=false] - Enable text analysis mode
* @property {number} [words] - Maximum number of words to analyze
* @property {number} [vocabularySize] - Vocabulary size for text generation
* @property {number} [exponent] - Zipf exponent
*
* // Normal distribution options
* @property {string} [test] - Normality test method
* @property {boolean} [outliers=false] - Enable outlier detection
* @property {string} [outlierMethod] - Outlier detection method
* @property {boolean} [qualityControl=false] - Enable quality control analysis
* @property {string} [specLimits] - Specification limits for quality control
* @property {boolean} [enableTimeseries=false] - Enable time series analysis
* @property {number} [timeseriesWindow] - Time series analysis window size
* @property {number} [mean] - Mean of normal distribution
* @property {number} [stddev] - Standard deviation of normal distribution
*
* // Poisson distribution options
* @property {boolean} [predict=false] - Enable probability prediction
* @property {number} [maxEvents] - Maximum number of events for analysis
* @property {boolean} [rareEvents=false] - Focus on rare event analysis
* @property {number} [lambda] - Lambda parameter for Poisson distribution
* @property {boolean} [timeSeries=false] - Generate time-series event data
*
* // Generation options
* @property {number} [samples] - Number of samples to generate
* @property {number} [seed] - Random seed for reproducible generation
* @property {string} [outputFile] - Output file path
* @property {number} [fraudRate] - Fraud injection rate for testing
* @property {string} [range] - Number range for generation
* @property {number} [scale] - Scale parameter for distributions
*/
/**
* Result of a lawkit analysis
* @typedef {Object} AnalysisResult
* @property {string} law - Statistical law analyzed
* @property {number} pValue - P-value of the test
* @property {number} statistic - Test statistic
* @property {boolean} significant - Whether result is statistically significant
* @property {string} conclusion - Conclusion of the analysis
*/
/**
* Error thrown when lawkit command fails
*/
class LawkitError extends Error {
constructor(message, exitCode, stderr) {
super(message);
this.name = 'LawkitError';
this.exitCode = exitCode;
this.stderr = stderr;
}
}
/**
* Determine the platform-specific binary name and directory
* @returns {Object} Platform info with subdir and binaryName
*/
function getPlatformInfo() {
const platform = process.platform;
const arch = process.arch;
if (platform === 'win32') {
return { subdir: 'win32-x64', binaryName: 'lawkit.exe' };
} else if (platform === 'darwin') {
if (arch === 'arm64') {
return { subdir: 'darwin-arm64', binaryName: 'lawkit' };
} else {
return { subdir: 'darwin-x64', binaryName: 'lawkit' };
}
} else if (platform === 'linux') {
return { subdir: 'linux-x64', binaryName: 'lawkit' };
} else {
throw new Error(`Unsupported platform: ${platform}-${arch}`);
}
}
/**
* Get the path to the lawkit binary
* @returns {string} Path to lawkit binary
*/
function getLawkitBinaryPath() {
try {
// Get platform-specific binary path
const platformInfo = getPlatformInfo();
const platformBinaryPath = path.join(__dirname, 'bin', platformInfo.subdir, platformInfo.binaryName);
if (fs.existsSync(platformBinaryPath)) {
return platformBinaryPath;
}
// Check if old-style flat binary exists (for backwards compatibility)
const binaryName = process.platform === 'win32' ? 'lawkit.exe' : 'lawkit';
const legacyBinaryPath = path.join(__dirname, 'bin', binaryName);
if (fs.existsSync(legacyBinaryPath)) {
return legacyBinaryPath;
}
// Fall back to system PATH
return 'lawkit';
} catch (error) {
// If platform detection fails, fall back to system PATH
return 'lawkit';
}
}
/**
* Execute lawkit command
* @param {string[]} args - Command arguments
* @returns {Promise<{stdout: string, stderr: string}>} Command output
*/
function executeLawkit(args) {
return new Promise((resolve, reject) => {
const lawkitPath = getLawkitBinaryPath();
const child = spawn(lawkitPath, args, {
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
resolve({ stdout, stderr });
} else {
reject(new LawkitError(
`lawkit exited with code ${code}`,
code,
stderr
));
}
});
child.on('error', (err) => {
if (err.code === 'ENOENT') {
reject(new LawkitError(
'lawkit command not found. Please install lawkit CLI tool.',
-1,
''
));
} else {
reject(new LawkitError(err.message, -1, ''));
}
});
});
}
/**
* Analyze data using Benford's Law
*
* @param {string|number[]} data - Data file path or array of numbers
* @param {LawkitOptions} [options={}] - Analysis options
* @returns {Promise<string|AnalysisResult>} Analysis result
*
* @example
* // Analyze file
* const result = await benford('data.csv');
* console.log(result);
*
* @example
* // Analyze array with JSON output
* const numbers = [1, 10, 100, 1000, 2000];
* const result = await benford(numbers, { output: 'json' });
* console.log(result);
*/
async function benford(data, options = {}) {
return executeAnalysis('benf', data, options);
}
/**
* Analyze data using Pareto Principle
*
* @param {string|number[]} data - Data file path or array of numbers
* @param {LawkitOptions} [options={}] - Analysis options
* @returns {Promise<string|AnalysisResult>} Analysis result
*/
async function pareto(data, options = {}) {
return executeAnalysis('pareto', data, options);
}
/**
* Analyze data using Zipf's Law
*
* @param {string|number[]} data - Data file path or array of numbers
* @param {LawkitOptions} [options={}] - Analysis options
* @returns {Promise<string|AnalysisResult>} Analysis result
*/
async function zipf(data, options = {}) {
return executeAnalysis('zipf', data, options);
}
/**
* Analyze data using Normal Distribution
*
* @param {string|number[]} data - Data file path or array of numbers
* @param {LawkitOptions} [options={}] - Analysis options
* @returns {Promise<string|AnalysisResult>} Analysis result
*/
async function normal(data, options = {}) {
return executeAnalysis('normal', data, options);
}
/**
* Analyze data using Poisson Distribution
*
* @param {string|number[]} data - Data file path or array of numbers
* @param {LawkitOptions} [options={}] - Analysis options
* @returns {Promise<string|AnalysisResult>} Analysis result
*/
async function poisson(data, options = {}) {
return executeAnalysis('poisson', data, options);
}
/**
* Common function to execute analysis
* @private
*/
async function executeAnalysis(command, data, options = {}) {
let inputPath = data;
let tempFile = null;
// If data is an array, write to temporary file
if (Array.isArray(data)) {
const tmpDir = mkdtempSync(path.join(tmpdir(), 'lawkit-'));
tempFile = path.join(tmpDir, 'data.txt');
writeFileSync(tempFile, data.join('\n'), 'utf8');
inputPath = tempFile;
}
try {
const args = [command];
// Common options
if (options.output) args.push('--format', options.output);
if (options.quiet) args.push('--quiet');
if (options.verbose) args.push('--verbose');
if (options.filter) args.push('--filter', options.filter);
if (options.minCount !== undefined) args.push('--min-count', options.minCount.toString());
// Integration options
if (options.laws) args.push('--laws', options.laws);
if (options.focus) args.push('--focus', options.focus);
if (options.threshold !== undefined) args.push('--threshold', options.threshold.toString());
if (options.recommend) args.push('--recommend');
if (options.report) args.push('--report', options.report);
if (options.consistencyCheck) args.push('--consistency-check');
if (options.crossValidation) args.push('--cross-validation');
if (options.confidenceLevel !== undefined) args.push('--confidence-level', options.confidenceLevel.toString());
if (options.purpose) args.push('--purpose', options.purpose);
// Benford-specific options
if (options.thresholdLevel) args.push('--threshold', options.thresholdLevel);
if (options.confidence !== undefined) args.push('--confidence', options.confidence.toString());
if (options.sampleSize !== undefined) args.push('--sample-size', options.sampleSize.toString());
if (options.minValue !== undefined) args.push('--min-value', options.minValue.toString());
// Pareto-specific options
if (options.concentration !== undefined) args.push('--concentration', options.concentration.toString());
if (options.giniCoefficient) args.push('--gini-coefficient');
if (options.percentiles) args.push('--percentiles', options.percentiles);
if (options.businessAnalysis) args.push('--business-analysis');
// Zipf-specific options
if (options.text) args.push('--text');
if (options.words !== undefined) args.push('--words', options.words.toString());
if (options.vocabularySize !== undefined) args.push('--vocabulary-size', options.vocabularySize.toString());
if (options.exponent !== undefined) args.push('--exponent', options.exponent.toString());
// Normal distribution options
if (options.test) args.push('--test', options.test);
if (options.outliers) args.push('--outliers');
if (options.outlierMethod) args.push('--outlier-method', options.outlierMethod);
if (options.qualityControl) args.push('--quality-control');
if (options.specLimits) args.push('--spec-limits', options.specLimits);
if (options.enableTimeseries) args.push('--enable-timeseries');
if (options.timeseriesWindow !== undefined) args.push('--timeseries-window', options.timeseriesWindow.toString());
if (options.mean !== undefined) args.push('--mean', options.mean.toString());
if (options.stddev !== undefined) args.push('--stddev', options.stddev.toString());
// Poisson distribution options
if (options.predict) args.push('--predict');
if (options.maxEvents !== undefined) args.push('--max-events', options.maxEvents.toString());
if (options.rareEvents) args.push('--rare-events');
if (options.lambda !== undefined) args.push('--lambda', options.lambda.toString());
if (options.timeSeries) args.push('--time-series');
// Generation options
if (options.samples !== undefined) args.push('--samples', options.samples.toString());
if (options.seed !== undefined) args.push('--seed', options.seed.toString());
if (options.outputFile) args.push('--output-file', options.outputFile);
if (options.fraudRate !== undefined) args.push('--fraud-rate', options.fraudRate.toString());
if (options.range) args.push('--range', options.range);
if (options.scale !== undefined) args.push('--scale', options.scale.toString());
// Add input file
args.push(inputPath);
const { stdout, stderr } = await executeLawkit(args);
// If output format is JSON, parse the result
if (options.output === 'json') {
try {
return JSON.parse(stdout);
} catch (e) {
throw new LawkitError(`Failed to parse JSON output: ${e.message}`, -1, '');
}
}
// Return raw output for other formats
return stdout;
} finally {
// Clean up temporary file if created
if (tempFile) {
rmSync(path.dirname(tempFile), { recursive: true, force: true });
}
}
}
/**
* Check if lawkit command is available in the system
*
* @returns {Promise<boolean>} True if lawkit is available, false otherwise
*
* @example
* if (!(await isLawkitAvailable())) {
* console.error('Please install lawkit CLI tool');
* process.exit(1);
* }
*/
async function isLawkitAvailable() {
try {
await executeLawkit(['--version']);
return true;
} catch (err) {
return false;
}
}
/**
* Perform comprehensive analysis using multiple statistical laws
*
* @param {string|number[]} data - Data file path or array of numbers
* @param {LawkitOptions} [options={}] - Analysis options
* @returns {Promise<string|AnalysisResult>} Analysis result
*/
async function analyze(data, options = {}) {
return executeAnalysis('analyze', data, options);
}
/**
* Validate data quality using statistical tests
*
* @param {string|number[]} data - Data file path or array of numbers
* @param {LawkitOptions} [options={}] - Analysis options
* @returns {Promise<string|AnalysisResult>} Validation result
*/
async function validate(data, options = {}) {
return executeAnalysis('validate', data, options);
}
/**
* Diagnose data anomalies and provide recommendations
*
* @param {string|number[]} data - Data file path or array of numbers
* @param {LawkitOptions} [options={}] - Analysis options
* @returns {Promise<string|AnalysisResult>} Diagnostic result
*/
async function diagnose(data, options = {}) {
return executeAnalysis('diagnose', data, options);
}
/**
* Generate sample data for testing statistical laws
*
* @param {string} law - Statistical law to generate data for ('benf', 'pareto', 'zipf', 'normal', 'poisson')
* @param {Object} [options={}] - Generation options
* @param {number} [options.count=1000] - Number of samples to generate
* @param {string} [options.outputFile] - Output file path
* @returns {Promise<string>} Generated data or file path
*/
async function generate(law, options = {}) {
const args = ['generate', law];
// Add generation options
if (options.count !== undefined) {
args.push('--count', options.count.toString());
}
if (options.outputFile) {
args.push('--output-file', options.outputFile);
}
// Add other common options
if (options.output) {
args.push('--format', options.output);
}
if (options.quiet) {
args.push('--quiet');
}
const { stdout, stderr } = await executeLawkit(args);
// If output format is JSON, parse the result
if (options.output === 'json') {
try {
return JSON.parse(stdout);
} catch (e) {
throw new LawkitError(`Failed to parse JSON output: ${e.message}`, -1, '');
}
}
return stdout;
}
/**
* List available statistical laws and commands
*
* @param {Object} [options={}] - List options
* @returns {Promise<string>} List of available commands
*/
async function list(options = {}) {
const args = ['list'];
if (options.output) {
args.push('--format', options.output);
}
const { stdout, stderr } = await executeLawkit(args);
// If output format is JSON, parse the result
if (options.output === 'json') {
try {
return JSON.parse(stdout);
} catch (e) {
throw new LawkitError(`Failed to parse JSON output: ${e.message}`, -1, '');
}
}
return stdout;
}
module.exports = {
benford,
pareto,
zipf,
normal,
poisson,
analyze,
validate,
diagnose,
generate,
list,
isLawkitAvailable,
LawkitError
};