// JSON-based Plugin Support for Custom Calculations // Extensible calculation engine with user-defined formulas and plugins interface PluginMetadata { id: string; name: string; version: string; author: string; description: string; category: string; tags: string[]; compatibility: string[]; lastUpdated: Date; } interface PluginInput { name: string; type: 'number' | 'string' | 'boolean' | 'select' | 'file'; label: string; description?: string; required: boolean; defaultValue?: any; validation?: { min?: number; max?: number; pattern?: string; options?: string[]; }; unit?: string; } interface PluginOutput { name: string; type: 'number' | 'string' | 'object' | 'array'; label: string; description?: string; unit?: string; format?: string; } interface PluginCalculation { formula: string; conditions?: Array<{ condition: string; formula: string; }>; constants?: Record; functions?: Record; } interface Plugin { metadata: PluginMetadata; inputs: PluginInput[]; outputs: PluginOutput[]; calculations: PluginCalculation[]; documentation?: { overview: string; examples: Array<{ title: string; description: string; inputs: Record; expectedOutput: Record; }>; references: string[]; }; } interface PluginExecutionResult { success: boolean; outputs: Record; errors?: string[]; warnings?: string[]; executionTime: number; pluginId: string; } class PluginSystem { private plugins: Map = new Map(); private readonly PLUGIN_STORAGE_KEY = 'prithvi_plugins'; private readonly EXECUTION_TIMEOUT = 10000; // 10 seconds constructor() { this.loadPlugins(); this.initializeBuiltinPlugins(); } // Plugin Management loadPlugin(pluginData: string | Plugin): boolean { try { let plugin: Plugin; if (typeof pluginData === 'string') { plugin = JSON.parse(pluginData); } else { plugin = pluginData; } // Validate plugin structure this.validatePlugin(plugin); // Check for conflicts if (this.plugins.has(plugin.metadata.id)) { const existingPlugin = this.plugins.get(plugin.metadata.id)!; if (existingPlugin.metadata.version >= plugin.metadata.version) { throw new Error(`Plugin ${plugin.metadata.id} version ${plugin.metadata.version} is not newer than existing version ${existingPlugin.metadata.version}`); } } // Install plugin plugin.metadata.lastUpdated = new Date(); this.plugins.set(plugin.metadata.id, plugin); this.savePlugins(); return true; } catch (error) { throw new Error(`Failed to load plugin: ${error instanceof Error ? error.message : 'Unknown error'}`); } } unloadPlugin(pluginId: string): boolean { try { if (!this.plugins.has(pluginId)) { throw new Error(`Plugin ${pluginId} not found`); } this.plugins.delete(pluginId); this.savePlugins(); return true; } catch (error) { throw new Error(`Failed to unload plugin: ${error instanceof Error ? error.message : 'Unknown error'}`); } } getPlugin(pluginId: string): Plugin | undefined { return this.plugins.get(pluginId); } getAllPlugins(): Plugin[] { return Array.from(this.plugins.values()).sort((a, b) => a.metadata.name.localeCompare(b.metadata.name) ); } getPluginsByCategory(category: string): Plugin[] { return this.getAllPlugins().filter(plugin => plugin.metadata.category.toLowerCase() === category.toLowerCase() ); } searchPlugins(query: string): Plugin[] { if (!query || query.trim().length === 0) { return this.getAllPlugins(); } const searchTerm = query.toLowerCase(); return this.getAllPlugins().filter(plugin => plugin.metadata.name.toLowerCase().includes(searchTerm) || plugin.metadata.description.toLowerCase().includes(searchTerm) || plugin.metadata.tags.some(tag => tag.toLowerCase().includes(searchTerm)) ); } // Plugin Execution executePlugin(pluginId: string, inputs: Record): Promise { return new Promise((resolve, reject) => { const startTime = Date.now(); try { const plugin = this.plugins.get(pluginId); if (!plugin) { reject(new Error(`Plugin ${pluginId} not found`)); return; } // Set execution timeout const timeoutId = setTimeout(() => { reject(new Error(`Plugin execution timed out after ${this.EXECUTION_TIMEOUT}ms`)); }, this.EXECUTION_TIMEOUT); // Validate inputs const validationResult = this.validateInputs(plugin, inputs); if (!validationResult.valid) { clearTimeout(timeoutId); resolve({ success: false, outputs: {}, errors: validationResult.errors, executionTime: Date.now() - startTime, pluginId }); return; } // Execute calculations const executionResult = this.executeCalculations(plugin, inputs); clearTimeout(timeoutId); resolve({ success: executionResult.success, outputs: executionResult.outputs, errors: executionResult.errors, warnings: executionResult.warnings, executionTime: Date.now() - startTime, pluginId }); } catch (error) { reject(error); } }); } // Plugin Creation Helper createPlugin(metadata: PluginMetadata, config: { inputs: PluginInput[]; outputs: PluginOutput[]; calculations: PluginCalculation[]; documentation?: Plugin['documentation']; }): Plugin { try { const plugin: Plugin = { metadata: { ...metadata, lastUpdated: new Date() }, inputs: config.inputs, outputs: config.outputs, calculations: config.calculations, documentation: config.documentation }; this.validatePlugin(plugin); return plugin; } catch (error) { throw new Error(`Failed to create plugin: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Export/Import exportPlugin(pluginId: string): string { try { const plugin = this.plugins.get(pluginId); if (!plugin) { throw new Error(`Plugin ${pluginId} not found`); } return JSON.stringify(plugin, null, 2); } catch (error) { throw new Error(`Failed to export plugin: ${error instanceof Error ? error.message : 'Unknown error'}`); } } exportAllPlugins(): string { try { const allPlugins = Array.from(this.plugins.values()); return JSON.stringify({ plugins: allPlugins, exportDate: new Date().toISOString(), version: '1.0' }, null, 2); } catch (error) { throw new Error(`Failed to export plugins: ${error instanceof Error ? error.message : 'Unknown error'}`); } } importPlugins(importData: string): { successful: number; failed: number; errors: string[] } { try { const data = JSON.parse(importData); const plugins = data.plugins || [data]; // Handle single plugin or multiple let successful = 0; let failed = 0; const errors: string[] = []; plugins.forEach((plugin: Plugin, index: number) => { try { this.loadPlugin(plugin); successful++; } catch (error) { failed++; errors.push(`Plugin ${index + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`); } }); return { successful, failed, errors }; } catch (error) { throw new Error(`Failed to import plugins: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Private Methods private validatePlugin(plugin: Plugin): void { // Validate metadata if (!plugin.metadata || !plugin.metadata.id || !plugin.metadata.name) { throw new Error('Plugin metadata is incomplete'); } if (!plugin.metadata.version || !this.isValidVersion(plugin.metadata.version)) { throw new Error('Plugin version is invalid'); } // Validate inputs if (!Array.isArray(plugin.inputs)) { throw new Error('Plugin inputs must be an array'); } plugin.inputs.forEach((input, index) => { if (!input.name || !input.type || !input.label) { throw new Error(`Input ${index} is incomplete`); } if (!['number', 'string', 'boolean', 'select', 'file'].includes(input.type)) { throw new Error(`Input ${index} has invalid type: ${input.type}`); } }); // Validate outputs if (!Array.isArray(plugin.outputs) || plugin.outputs.length === 0) { throw new Error('Plugin must have at least one output'); } plugin.outputs.forEach((output, index) => { if (!output.name || !output.type || !output.label) { throw new Error(`Output ${index} is incomplete`); } }); // Validate calculations if (!Array.isArray(plugin.calculations) || plugin.calculations.length === 0) { throw new Error('Plugin must have at least one calculation'); } plugin.calculations.forEach((calc, index) => { if (!calc.formula || typeof calc.formula !== 'string') { throw new Error(`Calculation ${index} must have a valid formula`); } }); } private validateInputs(plugin: Plugin, inputs: Record): { valid: boolean; errors: string[]; } { const errors: string[] = []; plugin.inputs.forEach(inputDef => { const value = inputs[inputDef.name]; // Check required fields if (inputDef.required && (value === undefined || value === null || value === '')) { errors.push(`${inputDef.label} is required`); return; } if (value === undefined || value === null) { return; // Skip validation for optional empty fields } // Type validation switch (inputDef.type) { case 'number': if (typeof value !== 'number' || isNaN(value)) { errors.push(`${inputDef.label} must be a valid number`); } else { if (inputDef.validation?.min !== undefined && value < inputDef.validation.min) { errors.push(`${inputDef.label} must be at least ${inputDef.validation.min}`); } if (inputDef.validation?.max !== undefined && value > inputDef.validation.max) { errors.push(`${inputDef.label} must be at most ${inputDef.validation.max}`); } } break; case 'string': if (typeof value !== 'string') { errors.push(`${inputDef.label} must be a string`); } else { if (inputDef.validation?.pattern) { const regex = new RegExp(inputDef.validation.pattern); if (!regex.test(value)) { errors.push(`${inputDef.label} format is invalid`); } } } break; case 'select': if (inputDef.validation?.options && !inputDef.validation.options.includes(value)) { errors.push(`${inputDef.label} must be one of: ${inputDef.validation.options.join(', ')}`); } break; case 'boolean': if (typeof value !== 'boolean') { errors.push(`${inputDef.label} must be true or false`); } break; } }); return { valid: errors.length === 0, errors }; } private executeCalculations(plugin: Plugin, inputs: Record): { success: boolean; outputs: Record; errors?: string[]; warnings?: string[]; } { try { const outputs: Record = {}; const warnings: string[] = []; for (const calculation of plugin.calculations) { try { // Create calculation context const context = { ...inputs, ...(calculation.constants || {}), Math: Math, abs: Math.abs, sqrt: Math.sqrt, pow: Math.pow, exp: Math.exp, log: Math.log, sin: Math.sin, cos: Math.cos, tan: Math.tan, max: Math.max, min: Math.min, round: Math.round, floor: Math.floor, ceil: Math.ceil }; // Execute formula let formula = calculation.formula; // Check conditions if (calculation.conditions) { for (const condition of calculation.conditions) { if (this.evaluateCondition(condition.condition, context)) { formula = condition.formula; break; } } } // Replace variables in formula const result = this.evaluateFormula(formula, context); // Map results to outputs plugin.outputs.forEach(outputDef => { if (outputDef.name in result) { outputs[outputDef.name] = result[outputDef.name]; } else if (typeof result === 'number' && plugin.outputs.length === 1) { outputs[outputDef.name] = result; } }); } catch (error) { warnings.push(`Calculation error: ${error instanceof Error ? error.message : 'Unknown error'}`); } } return { success: Object.keys(outputs).length > 0, outputs, warnings: warnings.length > 0 ? warnings : undefined }; } catch (error) { return { success: false, outputs: {}, errors: [`Execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`] }; } } private evaluateFormula(formula: string, context: Record): any { try { // Basic safety checks if (formula.includes('eval') || formula.includes('Function') || formula.includes('require')) { throw new Error('Unsafe formula detected'); } // Create a safe evaluation context const safeContext = { ...context }; delete (safeContext as any).constructor; delete (safeContext as any).__proto__; // Simple formula evaluation using Function constructor (safer than eval) const func = new Function(...Object.keys(safeContext), `return ${formula}`); return func(...Object.values(safeContext)); } catch (error) { throw new Error(`Formula evaluation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } private evaluateCondition(condition: string, context: Record): boolean { try { const result = this.evaluateFormula(condition, context); return Boolean(result); } catch (error) { return false; } } private isValidVersion(version: string): boolean { return /^\d+\.\d+\.\d+$/.test(version); } private loadPlugins(): void { try { const data = localStorage.getItem(this.PLUGIN_STORAGE_KEY); if (data) { const plugins = JSON.parse(data); plugins.forEach((plugin: Plugin) => { plugin.metadata.lastUpdated = new Date(plugin.metadata.lastUpdated); this.plugins.set(plugin.metadata.id, plugin); }); } } catch (error) { console.error('Failed to load plugins from storage:', error); } } private savePlugins(): void { try { const plugins = Array.from(this.plugins.values()); localStorage.setItem(this.PLUGIN_STORAGE_KEY, JSON.stringify(plugins)); } catch (error) { throw new Error(`Failed to save plugins: ${error instanceof Error ? error.message : 'Storage quota exceeded'}`); } } private initializeBuiltinPlugins(): void { // Water Quality Index Calculator Plugin const wqiPlugin: Plugin = { metadata: { id: 'builtin_wqi_calculator', name: 'Water Quality Index Calculator', version: '1.0.0', author: 'VBharat AI', description: 'Comprehensive Water Quality Index calculation based on multiple parameters', category: 'Water Quality', tags: ['water', 'quality', 'index', 'pollution'], compatibility: ['v1.0'], lastUpdated: new Date() }, inputs: [ { name: 'ph', type: 'number', label: 'pH', description: 'pH value of water sample', required: true, validation: { min: 0, max: 14 }, unit: 'pH units' }, { name: 'do', type: 'number', label: 'Dissolved Oxygen', description: 'Dissolved oxygen concentration', required: true, validation: { min: 0, max: 20 }, unit: 'mg/L' }, { name: 'bod', type: 'number', label: 'BOD', description: 'Biochemical Oxygen Demand', required: true, validation: { min: 0 }, unit: 'mg/L' }, { name: 'nitrate', type: 'number', label: 'Nitrate', description: 'Nitrate concentration', required: true, validation: { min: 0 }, unit: 'mg/L' }, { name: 'phosphate', type: 'number', label: 'Phosphate', description: 'Phosphate concentration', required: true, validation: { min: 0 }, unit: 'mg/L' }, { name: 'temperature', type: 'number', label: 'Temperature Deviation', description: 'Temperature deviation from normal', required: true, validation: { min: -10, max: 10 }, unit: '°C' } ], outputs: [ { name: 'wqi', type: 'number', label: 'Water Quality Index', description: 'Overall water quality index value', unit: 'WQI' }, { name: 'category', type: 'string', label: 'Quality Category', description: 'Water quality classification' }, { name: 'recommendation', type: 'string', label: 'Recommendation', description: 'Treatment or action recommendation' } ], calculations: [ { formula: ` // Individual quality indices var qi_ph = ph >= 6.5 && ph <= 8.5 ? 100 : max(0, 100 - abs(7 - ph) * 15); var qi_do = min(100, (do / 14.6) * 100); var qi_bod = max(0, 100 - (bod * 5)); var qi_nitrate = max(0, 100 - (nitrate * 2)); var qi_phosphate = max(0, 100 - (phosphate * 10)); var qi_temp = max(0, 100 - abs(temperature) * 5); // Weights (sum = 1.0) var w_ph = 0.15; var w_do = 0.25; var w_bod = 0.25; var w_nitrate = 0.15; var w_phosphate = 0.10; var w_temp = 0.10; // Weighted WQI calculation var wqi_value = (qi_ph * w_ph + qi_do * w_do + qi_bod * w_bod + qi_nitrate * w_nitrate + qi_phosphate * w_phosphate + qi_temp * w_temp); // Determine category and recommendation var category = wqi_value > 90 ? 'Excellent' : wqi_value > 70 ? 'Good' : wqi_value > 50 ? 'Medium' : wqi_value > 25 ? 'Bad' : 'Very Bad'; var recommendation = wqi_value > 70 ? 'Water is suitable for drinking with standard treatment' : wqi_value > 50 ? 'Requires advanced treatment before use' : 'Extensive treatment required, monitor pollution sources'; ({ wqi: round(wqi_value * 100) / 100, category: category, recommendation: recommendation }) `, constants: { IDEAL_DO: 14.6, NEUTRAL_PH: 7.0 } } ], documentation: { overview: 'Calculates comprehensive Water Quality Index based on six key parameters with appropriate weightings according to Indian standards.', examples: [ { title: 'Clean River Water', description: 'Typical values for unpolluted river water', inputs: { ph: 7.2, do: 8.5, bod: 2.0, nitrate: 5.0, phosphate: 0.1, temperature: 1.0 }, expectedOutput: { wqi: 85.5, category: 'Good', recommendation: 'Water is suitable for drinking with standard treatment' } } ], references: [ 'CPCB Water Quality Criteria', 'IS 10500:2012 Drinking Water Standards', 'WHO Water Quality Guidelines' ] } }; this.plugins.set(wqiPlugin.metadata.id, wqiPlugin); // Structural Load Calculator Plugin const loadPlugin: Plugin = { metadata: { id: 'builtin_structural_load', name: 'Structural Load Calculator', version: '1.0.0', author: 'VBharat AI', description: 'Calculate design loads for structures as per IS 875', category: 'Structural Engineering', tags: ['structural', 'load', 'design', 'IS875'], compatibility: ['v1.0'], lastUpdated: new Date() }, inputs: [ { name: 'dead_load', type: 'number', label: 'Dead Load', description: 'Permanent structural load', required: true, validation: { min: 0 }, unit: 'kN/m²' }, { name: 'live_load', type: 'number', label: 'Live Load', description: 'Variable/imposed load', required: true, validation: { min: 0 }, unit: 'kN/m²' }, { name: 'wind_load', type: 'number', label: 'Wind Load', description: 'Wind pressure load', required: false, defaultValue: 0, validation: { min: 0 }, unit: 'kN/m²' }, { name: 'seismic_load', type: 'number', label: 'Seismic Load', description: 'Earthquake load', required: false, defaultValue: 0, validation: { min: 0 }, unit: 'kN/m²' }, { name: 'load_combination', type: 'select', label: 'Load Combination', description: 'Design load combination type', required: true, validation: { options: ['basic', 'wind', 'seismic', 'all'] } } ], outputs: [ { name: 'design_load', type: 'number', label: 'Design Load', description: 'Factored design load', unit: 'kN/m²' }, { name: 'safety_factor', type: 'number', label: 'Safety Factor', description: 'Overall safety factor achieved' }, { name: 'governing_combination', type: 'string', label: 'Governing Combination', description: 'Critical load combination' } ], calculations: [ { formula: ` // Load combinations as per IS 875 var basic_combo = 1.5 * (dead_load + live_load); var wind_combo = 1.2 * (dead_load + live_load + wind_load); var seismic_combo = 1.2 * (dead_load + live_load + seismic_load); var wind_uplift = 0.9 * dead_load + 1.5 * wind_load; var combinations = { 'basic': basic_combo, 'wind': max(basic_combo, wind_combo, wind_uplift), 'seismic': max(basic_combo, seismic_combo), 'all': max(basic_combo, wind_combo, seismic_combo, wind_uplift) }; var design_load_value = combinations[load_combination]; var safety_factor_value = design_load_value / (dead_load + live_load); var governing = design_load_value === basic_combo ? 'Basic (1.5DL+1.5LL)' : design_load_value === wind_combo ? 'Wind (1.2DL+1.2LL+1.2WL)' : design_load_value === seismic_combo ? 'Seismic (1.2DL+1.2LL+1.2EL)' : 'Wind Uplift (0.9DL+1.5WL)'; ({ design_load: round(design_load_value * 100) / 100, safety_factor: round(safety_factor_value * 100) / 100, governing_combination: governing }) ` } ], documentation: { overview: 'Calculates structural design loads according to IS 875 load combinations for different loading scenarios.', examples: [ { title: 'Residential Building', description: 'Typical residential structure loads', inputs: { dead_load: 4.0, live_load: 2.0, wind_load: 1.0, seismic_load: 0.8, load_combination: 'all' }, expectedOutput: { design_load: 9.6, safety_factor: 1.6, governing_combination: 'Wind (1.2DL+1.2LL+1.2WL)' } } ], references: [ 'IS 875-1987 Design Loads for Buildings', 'IS 1893-2016 Earthquake Resistant Design', 'IS 456-2000 Plain and Reinforced Concrete' ] } }; this.plugins.set(loadPlugin.metadata.id, loadPlugin); } } export const pluginSystem = new PluginSystem(); export { Plugin, PluginInput, PluginOutput, PluginExecutionResult, PluginMetadata };