import { existsSync, readFileSync, writeFileSync } from 'fs';
import { ProcessMiningClient } from '../src/client.js';
const defaultConfig = {
activityKey: 'concept:name',
timestampKey: 'time:timestamp',
format: 'json',
verbose: false,
};
class WASM4PMCLI {
constructor() {
this.client = new ProcessMiningClient();
this.config = { ...defaultConfig };
}
async init() {
try {
await this.client.init();
console.log('✅ wasm4pm initialized');
} catch (error) {
console.error('❌ Failed to initialize:', error);
process.exit(1);
}
}
async load(filePath) {
if (!existsSync(filePath)) {
console.error(`❌ File not found: ${filePath}`);
return null;
}
try {
const content = readFileSync(filePath, 'utf-8');
const ext = filePath.split('.').pop()?.toLowerCase();
let log;
if (ext === 'xes') {
log = this.client.loadEventLogFromXES(content);
} else if (ext === 'json') {
log = this.client.loadEventLogFromJSON(content);
} else {
console.error(`❌ Unsupported format: ${ext}`);
return null;
}
console.log(`✅ Loaded ${filePath}`);
return log;
} catch (error) {
console.error(`❌ Failed to load file:`, error);
return null;
}
}
async discover(filePath, algorithm) {
const log = await this.load(filePath);
if (!log) return;
console.log(`\n🔬 Running ${algorithm} discovery...`);
try {
const startTime = Date.now();
let result;
switch (algorithm.toLowerCase()) {
case 'dfg':
result = log.discoverDFG({ minFrequency: 1 });
break;
case 'alpha':
case 'alpha++':
result = log.discoverAlphaPlusPlus();
break;
case 'declare':
result = log.discoverDECLARE();
break;
case 'ilp':
result = log.discoverILPPetriNet();
break;
case 'optimized':
result = log.discoverOptimizedDFG({ fitnessWeight: 0.7, simplicityWeight: 0.3 });
break;
case 'genetic':
console.log('🧬 Running genetic algorithm discovery...');
result = log.discoverGeneticAlgorithm({ populationSize: 50, generations: 20 });
break;
case 'pso':
console.log('🐝 Running particle swarm optimization...');
result = log.discoverPSOAlgorithm({ swarmSize: 30, iterations: 50 });
break;
case 'heuristic':
console.log('⚠️ Heuristic miner (use advanced_algorithms module)');
return;
default:
console.error(`❌ Unknown algorithm: ${algorithm}`);
return;
}
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`✅ Discovery completed in ${duration}s`);
const json = result.toJSON();
this.printResult(json, algorithm);
if (this.config.outputFile) {
this.saveResult(json);
}
} catch (error) {
console.error(`❌ Discovery failed:`, error);
}
}
async analyze(filePath) {
const log = await this.load(filePath);
if (!log) return;
console.log(`\n📊 Analyzing event log...`);
try {
const stats = log.getStats();
const traceLengthStats = log.getTraceLengthStats();
const activityFreqs = log.getActivityFrequencies();
console.log('\n📈 Statistics:');
console.log(` Cases: ${stats.total_cases.toLocaleString()}`);
console.log(` Events: ${stats.total_events.toLocaleString()}`);
console.log(` Avg Events/Case: ${stats.avg_events_per_case.toFixed(2)}`);
console.log('\n📏 Case Length Distribution:');
console.log(` Min: ${traceLengthStats.min}`);
console.log(` Max: ${traceLengthStats.max}`);
console.log(` Avg: ${traceLengthStats.average.toFixed(2)}`);
console.log(` Median: ${traceLengthStats.median}`);
console.log('\n🎯 Top 10 Activities:');
activityFreqs.slice(0, 10).forEach(([activity, freq], idx) => {
const pct = ((freq / stats.total_events) * 100).toFixed(1);
console.log(` ${idx + 1}. ${activity}: ${freq} (${pct}%)`);
});
if (this.config.outputFile) {
const result = {
statistics: stats,
caseLengthStats: traceLengthStats,
activityFrequencies: activityFreqs,
};
this.saveResult(result);
}
} catch (error) {
console.error(`❌ Analysis failed:`, error);
}
}
async export(filePath, format) {
const log = await this.load(filePath);
if (!log) return;
console.log(`\n💾 Exporting to ${format.toUpperCase()}...`);
try {
let exported;
switch (format.toLowerCase()) {
case 'json':
exported = log.toJSON();
break;
case 'xes':
exported = log.toXES();
break;
default:
console.error(`❌ Unsupported export format: ${format}`);
return;
}
const outputFile =
this.config.outputFile || filePath.replace(/\.[^.]+$/, `.exported.${format}`);
writeFileSync(outputFile, exported);
console.log(`✅ Exported to ${outputFile}`);
} catch (error) {
console.error(`❌ Export failed:`, error);
}
}
printResult(result, label) {
console.log(`\n🔍 ${label.toUpperCase()} Result:`);
if (typeof result === 'object') {
if ('nodes' in result && 'edges' in result) {
console.log(` Nodes: ${result.nodes}`);
console.log(` Edges: ${result.edges}`);
} else if ('places' in result && 'transitions' in result) {
console.log(` Places: ${result.places}`);
console.log(` Transitions: ${result.transitions}`);
console.log(` Arcs: ${result.arcs}`);
} else if ('constraints' in result) {
console.log(` Constraints: ${result.constraints.length}`);
} else {
console.log(JSON.stringify(result, null, 2).split('\n').slice(0, 10).join('\n'));
}
}
}
saveResult(result) {
if (!this.config.outputFile) return;
const content = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
writeFileSync(this.config.outputFile, content);
console.log(`✅ Results saved to ${this.config.outputFile}`);
}
async run(args) {
const [command, ...cmdArgs] = args.slice(2);
for (let i = 0; i < cmdArgs.length; i++) {
if (cmdArgs[i] === '-o' || cmdArgs[i] === '--output') {
this.config.outputFile = cmdArgs[++i];
} else if (cmdArgs[i] === '-v' || cmdArgs[i] === '--verbose') {
this.config.verbose = true;
} else if (cmdArgs[i] === '--activity-key') {
this.config.activityKey = cmdArgs[++i];
} else if (cmdArgs[i] === '--timestamp-key') {
this.config.timestampKey = cmdArgs[++i];
}
}
switch (command?.toLowerCase()) {
case 'load':
await this.load(cmdArgs[0]);
break;
case 'discover':
await this.discover(cmdArgs[0], cmdArgs[1] || 'dfg');
break;
case 'analyze':
await this.analyze(cmdArgs[0]);
break;
case 'export':
await this.export(cmdArgs[0], cmdArgs[1] || 'json');
break;
case 'help':
case '-h':
case '--help':
this.printHelp();
break;
case 'version':
case '-v':
case '--version':
console.log('wasm4pm v0.1.0');
break;
default:
console.error(`❌ Unknown command: ${command}`);
this.printHelp();
process.exit(1);
}
}
printHelp() {
console.log(`
╔════════════════════════════════════════════════════════════════╗
║ wasm4pm v0.1.0 ║
║ Process Mining in WebAssembly ║
╚════════════════════════════════════════════════════════════════╝
USAGE:
wasm4pm <command> [options]
COMMANDS:
load <file> Load and validate an event log
discover <file> <algo> Discover process model
Algorithms: dfg, alpha, declare, ilp, optimized, genetic, pso
analyze <file> Analyze event log statistics
export <file> <format> Export results (json, xes)
help Show this help message
version Show version
OPTIONS:
-o, --output <file> Save results to file
-v, --verbose Enable verbose output
--activity-key <key> Activity attribute key (default: concept:name)
--timestamp-key <key> Timestamp attribute key (default: time:timestamp)
EXAMPLES:
wasm4pm load data/log.xes
wasm4pm discover data/log.xes dfg -o result.json
wasm4pm discover data/log.xes alpha
wasm4pm discover data/log.xes ilp -o petri_net.json
wasm4pm discover data/log.xes genetic -o evolved_model.json
wasm4pm discover data/log.xes pso -o swarm_model.json
wasm4pm analyze data/log.xes --verbose
wasm4pm export data/log.json xes -o log.xes
DOCUMENTATION:
https://github.com/seanchatmangpt/wasm4pm
`);
}
}
const cli = new WASM4PMCLI();
cli.init().then(() => cli.run(process.argv));