UNPKG

credl-parser-evaluator

Version:

TypeScript-based CREDL Parser and Evaluator that processes CREDL files and outputs complete Intermediate Representations

1,105 lines (1,104 loc) 198 kB
"use strict"; /** * IRBuilder - Builds Intermediate Representation from parsed and validated CREDL data * * This class takes validated CREDL data and transforms it into a complete IR structure * that includes all resolved references, flattened hierarchies, and computed relationships. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.IRBuilder = void 0; const CREDLTypes_1 = require("../types/CREDLTypes"); const PresetResolver_1 = require("./PresetResolver"); const TemplateResolver_1 = require("./TemplateResolver"); class IRBuilder { /** * Get building registry for reference validation */ static getBuildingRegistry() { return this.buildingRegistry; } /** * Get parent asset for a building ID */ static getAssetForBuilding(buildingId) { return this.buildingRegistry.get(buildingId)?.asset; } /** * Build complete IR from validated CREDL data */ static buildIR(credlFile, options) { const opts = { ...this.DEFAULT_OPTIONS, ...options }; const errors = []; const warnings = []; // Clear registry for fresh build this.buildingRegistry.clear(); // Initialize partial processing status const partialStatus = { completed_steps: [], failed_steps: [], recovery_actions: [], processing_summary: "" }; try { // Initialize IR structure const ir = { metadata: credlFile.metadata, assets: [], spaces: [], assumptions: [], models: credlFile.models, simulation: credlFile.simulation, outputs: credlFile.outputs, resolution_order: [], template_generated_spaces: [], preset_applications: {}, cross_reference_resolution: { dependency_graph: [], cross_references: { assumption_to_models: {}, model_to_assumptions: {}, model_to_outputs: {}, output_to_models: {}, simulation_to_outputs: [], available_outputs: [], scenario_to_assumptions: {}, assumption_to_scenarios: {} }, resolution_order: [], circular_dependencies: [], orphaned_elements: { assumptions: [], outputs: [], models: [] }, coverage_analysis: { assumption_coverage: 0, output_coverage: 0, model_utilization: 0 } }, validation: { isValid: true, errors: [], warnings: [] }, partial_processing_status: partialStatus, generated_at: new Date().toISOString(), generator_version: "1.0.0" }; // Step 1: Process assets with comprehensive error recovery this.executeStepWithRecovery('assets', () => { ir.assets = this.processAssets(credlFile.assets, errors, warnings, opts); ir.resolution_order.push('assets'); }, partialStatus, ir, credlFile, errors, warnings); // Step 1.5: Process presets with comprehensive error recovery if (opts.resolvePresets && credlFile.presets) { this.executeStepWithRecovery('presets', () => { this.presetResolver.clear(); // Clear any previous presets this.presetApplications = {}; // Clear preset applications this.presetResolver.parsePresets(credlFile, errors, warnings); ir.resolution_order.push('presets'); }, partialStatus, ir, credlFile, errors, warnings); } // Step 2: Process assumptions with comprehensive error recovery this.executeStepWithRecovery('assumptions', () => { ir.assumptions = this.processAssumptions(credlFile.assumptions, errors, warnings, opts); ir.resolution_order.push('assumptions'); }, partialStatus, ir, credlFile, errors, warnings); // Step 3: Process spaces with comprehensive error recovery this.executeStepWithRecovery('spaces', () => { ir.spaces = this.processSpaces(credlFile.spaces, ir.assets, errors, warnings, opts); ir.resolution_order.push('spaces'); }, partialStatus, ir, credlFile, errors, warnings); // Step 3.5: Process templates with comprehensive error recovery if (credlFile.templates && credlFile.use_templates) { this.executeStepWithRecovery('templates', () => { const templateSpaces = this.processTemplates(credlFile.templates, credlFile.use_templates, ir.assets, credlFile.presets, errors, warnings, opts); ir.spaces = [...ir.spaces, ...templateSpaces]; ir.template_generated_spaces = templateSpaces.map(s => s.id); ir.resolution_order.push('templates'); }, partialStatus, ir, credlFile, errors, warnings); } // Step 4: Process models with comprehensive error recovery this.executeStepWithRecovery('models', () => { ir.models = this.processModels(credlFile.models, ir.assumptions, errors, warnings, opts); ir.resolution_order.push('models'); }, partialStatus, ir, credlFile, errors, warnings); // Step 5: Process simulation with comprehensive error recovery this.executeStepWithRecovery('simulation', () => { ir.simulation = this.processSimulation(credlFile.simulation, errors, warnings, opts); ir.resolution_order.push('simulation'); }, partialStatus, ir, credlFile, errors, warnings); // Step 6: Process outputs with comprehensive error recovery this.executeStepWithRecovery('outputs', () => { ir.outputs = this.processOutputs(credlFile.outputs, ir.models, errors, warnings, opts); ir.resolution_order.push('outputs'); }, partialStatus, ir, credlFile, errors, warnings); // Step 7a: Process first-class scenarios block (per Behavioral Specification 8.1) if (credlFile.scenarios) { this.executeStepWithRecovery('scenarios', () => { ir.scenarios = this.processScenarios(credlFile.scenarios, ir.assumptions, errors, warnings, 'scenarios'); ir.resolution_order.push('scenarios'); }, partialStatus, ir, credlFile, errors, warnings); } // Step 7b: Process first-class waterfall block (per Behavioral Specification 8.1) if (credlFile.waterfall) { this.executeStepWithRecovery('waterfall', () => { ir.waterfall = this.processWaterfall(credlFile.waterfall, errors, warnings, 'waterfall'); ir.resolution_order.push('waterfall'); }, partialStatus, ir, credlFile, errors, warnings); } // Step 7c: Process legacy extensions (backward compatibility) this.executeStepWithRecovery('extensions', () => { this.processExtensions(credlFile.extensions, ir, errors, warnings, opts); ir.resolution_order.push('extensions'); }, partialStatus, ir, credlFile, errors, warnings); // Step 8: Resolve cross-references with comprehensive error recovery this.executeStepWithRecovery('cross_reference_resolution', () => { ir.cross_reference_resolution = this.resolveCrossReferences(ir, errors, warnings, opts); ir.resolution_order.push('cross_reference_resolution'); }, partialStatus, ir, credlFile, errors, warnings); // Step 9: Comprehensive validation with comprehensive error recovery this.executeStepWithRecovery('comprehensive_validation', () => { this.validateCompleteIR(ir, errors, warnings, opts); ir.resolution_order.push('comprehensive_validation'); }, partialStatus, ir, credlFile, errors, warnings); // Step 10: Validate relationships with comprehensive error recovery this.executeStepWithRecovery('relationships', () => { this.validateRelationships(ir.assets, ir.spaces, errors, warnings); ir.resolution_order.push('relationships'); }, partialStatus, ir, credlFile, errors, warnings); // Generate processing summary this.generateProcessingSummary(partialStatus, ir); // Copy preset applications to IR ir.preset_applications = { ...this.presetApplications }; // Include validation results ir.validation = { isValid: errors.length === 0, errors, warnings }; return ir; } catch (error) { // Handle unexpected errors during IR building errors.push({ field: 'ir_generation', message: `IR generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, severity: 'error' }); return { metadata: credlFile?.metadata || { version: "0.2", name: "Error", description: "Failed to process CREDL file", analysis_start_date: new Date().toISOString().split('T')[0], // YYYY-MM-DD format created_date: new Date().toISOString().split('T')[0] // YYYY-MM-DD format }, assets: [], spaces: [], assumptions: [], models: credlFile?.models || [], simulation: credlFile?.simulation || { type: 'Monte Carlo', iterations: 0, processes: {}, outputs: { summary_metrics: [] } }, outputs: credlFile?.outputs || { format: 'JSON', metrics: [] }, resolution_order: ['error'], template_generated_spaces: [], preset_applications: {}, cross_reference_resolution: { dependency_graph: [], cross_references: { assumption_to_models: {}, model_to_assumptions: {}, model_to_outputs: {}, output_to_models: {}, simulation_to_outputs: [], available_outputs: [], scenario_to_assumptions: {}, assumption_to_scenarios: {} }, resolution_order: [], circular_dependencies: [], orphaned_elements: { assumptions: [], outputs: [], models: [] }, coverage_analysis: { assumption_coverage: 0, output_coverage: 0, model_utilization: 0 } }, validation: { isValid: false, errors, warnings }, generated_at: new Date().toISOString(), generator_version: "1.0.0" }; } } /** * Process assets with property types, locations, and total areas * Validates asset structure and builds asset registry for reference resolution */ static processAssets(assets, errors, warnings, _opts) { const processedAssets = []; const assetIdMap = new Map(); const buildingIdMap = new Map(); for (let i = 0; i < assets.length; i++) { const asset = assets[i]; const fieldPrefix = `assets[${i}]`; if (!asset) { errors.push({ field: fieldPrefix, message: 'Asset is null or undefined', severity: 'error' }); continue; } try { // Process buildings with parent-child relationship tracking const processedBuildings = asset.buildings.map((building, buildingIndex) => { const buildingFieldPrefix = `${fieldPrefix}.buildings[${buildingIndex}]`; // Check for duplicate building IDs across all assets if (buildingIdMap.has(building.id)) { const existing = buildingIdMap.get(building.id); warnings.push({ field: buildingFieldPrefix, message: `Duplicate building ID "${building.id}" found in assets "${existing.asset.id}" and "${asset.id}"`, severity: 'warning' }); } const processedBuilding = { id: building.id, name: building.name, ...(building.floors !== undefined && { floors: building.floors }), ...(building.total_area_sf !== undefined && { total_area_sf: building.total_area_sf }), ...(building.year_built !== undefined && { year_built: building.year_built }) }; // Register building for reference validation later buildingIdMap.set(building.id, { asset, building: processedBuilding }); // Validate building structure this.validateBuildingStructure(processedBuilding, buildingFieldPrefix, warnings); return processedBuilding; }); // Create processed asset with validated structure const processedAsset = { id: asset.id, name: asset.name, property_type: asset.property_type, location: asset.location, total_area_sf: asset.total_area_sf, buildings: processedBuildings, ...(asset.acquisition_date !== undefined && { acquisition_date: asset.acquisition_date }), ...(asset.acquisition_price !== undefined && { acquisition_price: asset.acquisition_price }) }; // Validate property type consistency this.validateAssetPropertyType(processedAsset, fieldPrefix, warnings); // Validate location format this.validateAssetLocation(processedAsset, fieldPrefix, warnings); // Validate total area consistency this.validateAssetAreas(processedAsset, fieldPrefix, warnings); // Register asset for reference resolution assetIdMap.set(asset.id, processedAsset); processedAssets.push(processedAsset); } catch (error) { errors.push({ field: fieldPrefix, message: `Failed to process asset: ${error instanceof Error ? error.message : 'Unknown error'}`, severity: 'error' }); } } // Store building registry for later reference by spaces this.buildingRegistry = buildingIdMap; return processedAssets; } /** * Validate asset property type for consistency */ static validateAssetPropertyType(asset, fieldPrefix, warnings) { // Add property-type specific validations switch (asset.property_type) { case 'Office': if (asset.total_area_sf > 10000000) { // 10M sq ft seems large for office warnings.push({ field: `${fieldPrefix}.total_area_sf`, message: `Large office property area: ${asset.total_area_sf} sq ft`, severity: 'warning' }); } break; case 'Retail': if (asset.buildings.length > 20) { warnings.push({ field: `${fieldPrefix}.buildings`, message: `Many buildings for retail property: ${asset.buildings.length}`, severity: 'warning' }); } break; case 'Industrial': if (asset.total_area_sf < 10000) { warnings.push({ field: `${fieldPrefix}.total_area_sf`, message: `Small industrial property area: ${asset.total_area_sf} sq ft`, severity: 'warning' }); } break; } } /** * Validate asset location format and consistency */ static validateAssetLocation(asset, fieldPrefix, warnings) { // Check for common location format patterns if (!asset.location.includes(',')) { warnings.push({ field: `${fieldPrefix}.location`, message: `Location may be missing city/state format: "${asset.location}"`, severity: 'warning' }); } // Check for empty or very short locations if (asset.location.trim().length < 5) { warnings.push({ field: `${fieldPrefix}.location`, message: `Location appears incomplete: "${asset.location}"`, severity: 'warning' }); } } /** * Validate building structure and properties */ static validateBuildingStructure(building, fieldPrefix, warnings) { // Validate building name is meaningful if (building.name.trim().length < 2) { warnings.push({ field: `${fieldPrefix}.name`, message: `Building name is too short: "${building.name}"`, severity: 'warning' }); } // Validate floors if specified if (building.floors !== undefined) { if (building.floors < 1) { warnings.push({ field: `${fieldPrefix}.floors`, message: `Building floors must be at least 1, got: ${building.floors}`, severity: 'warning' }); } if (building.floors > 200) { warnings.push({ field: `${fieldPrefix}.floors`, message: `Very tall building: ${building.floors} floors`, severity: 'warning' }); } } // Validate area if specified if (building.total_area_sf !== undefined && building.total_area_sf <= 0) { warnings.push({ field: `${fieldPrefix}.total_area_sf`, message: `Building area must be positive, got: ${building.total_area_sf}`, severity: 'warning' }); } // Validate year built if specified if (building.year_built !== undefined) { const currentYear = new Date().getFullYear(); if (building.year_built < 1800) { warnings.push({ field: `${fieldPrefix}.year_built`, message: `Very old building year: ${building.year_built}`, severity: 'warning' }); } if (building.year_built > currentYear + 5) { warnings.push({ field: `${fieldPrefix}.year_built`, message: `Future building year: ${building.year_built}`, severity: 'warning' }); } } } /** * Validate asset total area consistency with buildings */ static validateAssetAreas(asset, fieldPrefix, warnings) { // Calculate total building areas if specified const buildingAreasSum = asset.buildings .filter(b => b.total_area_sf !== undefined) .reduce((sum, b) => sum + (b.total_area_sf || 0), 0); if (buildingAreasSum > 0) { // Check if building areas sum exceeds asset total area if (buildingAreasSum > asset.total_area_sf * 1.1) { // Allow 10% tolerance warnings.push({ field: `${fieldPrefix}.buildings`, message: `Building areas sum (${buildingAreasSum}) exceeds asset total area (${asset.total_area_sf})`, severity: 'warning' }); } // Check if building areas are much smaller than asset total if (buildingAreasSum < asset.total_area_sf * 0.5) { // Less than 50% warnings.push({ field: `${fieldPrefix}.buildings`, message: `Building areas sum (${buildingAreasSum}) is much smaller than asset total area (${asset.total_area_sf})`, severity: 'warning' }); } } // Validate individual building areas asset.buildings.forEach((building, i) => { if (building.total_area_sf && building.total_area_sf > asset.total_area_sf) { warnings.push({ field: `${fieldPrefix}.buildings[${i}].total_area_sf`, message: `Building area (${building.total_area_sf}) exceeds asset total area (${asset.total_area_sf})`, severity: 'warning' }); } }); } /** * Process assumptions with comprehensive validation and cross-reference checking */ static processAssumptions(assumptions, errors, warnings, _opts) { const processedAssumptions = []; const assumptionNameMap = new Map(); for (let i = 0; i < assumptions.length; i++) { const assumption = assumptions[i]; const fieldPrefix = `assumptions[${i}]`; if (!assumption) { errors.push({ field: fieldPrefix, message: 'Assumption is null or undefined', severity: 'error' }); continue; } try { // Validate required fields if (!assumption.name) { errors.push({ field: `${fieldPrefix}.name`, message: 'Assumption name is required', severity: 'error' }); continue; } if (!assumption.type) { errors.push({ field: `${fieldPrefix}.type`, message: 'Assumption type is required', severity: 'error' }); continue; } // Check for duplicate assumption names if (assumptionNameMap.has(assumption.name)) { warnings.push({ field: `${fieldPrefix}.name`, message: `Duplicate assumption name "${assumption.name}" found`, severity: 'warning' }); } // Validate assumption type const validTypes = ['fixed', 'distribution', 'expression', 'table']; if (!validTypes.includes(assumption.type)) { errors.push({ field: `${fieldPrefix}.type`, message: `Invalid assumption type "${assumption.type}". Valid types: ${validTypes.join(', ')}`, severity: 'error' }); continue; } // Process assumption based on type const processedAssumption = this.processAssumptionByType(assumption, fieldPrefix, errors, warnings); if (!processedAssumption) continue; // Validate cross-references to assets and spaces this.validateAssumptionCrossReferences(processedAssumption, fieldPrefix, warnings); // Validate scope and tags this.validateAssumptionMetadata(processedAssumption, fieldPrefix, warnings); // Register assumption assumptionNameMap.set(assumption.name, processedAssumption); processedAssumptions.push(processedAssumption); } catch (error) { errors.push({ field: fieldPrefix, message: `Failed to process assumption: ${error instanceof Error ? error.message : 'Unknown error'}`, severity: 'error' }); } } // Additional cross-assumption validations this.validateAssumptionDependencies(processedAssumptions, errors, warnings); return processedAssumptions; } /** * Process assumption based on its type with specific validation */ static processAssumptionByType(assumption, fieldPrefix, errors, warnings) { // Auto-populate resolved_scope and applicable_spaces per DSL-to-Engine mapping const resolvedScope = this.resolveScopeHierarchy(assumption.scope || 'global'); const applicableSpaces = this.calculateApplicableSpaces(assumption.scope || 'global'); const baseAssumption = { name: assumption.name, type: assumption.type, scope: assumption.scope || 'global', tags: assumption.tags, description: assumption.description, resolved_value: undefined, source_preset: undefined, // Auto-populated fields for engine execution resolved_scope: resolvedScope, applicable_spaces: applicableSpaces, dependencies: [] // For expression dependencies (enhanced later) }; switch (assumption.type) { case 'fixed': return this.processFixedAssumption(assumption, baseAssumption, fieldPrefix, errors, warnings); case 'distribution': return this.processDistributionAssumption(assumption, baseAssumption, fieldPrefix, errors, warnings); case 'expression': return this.processExpressionAssumption(assumption, baseAssumption, fieldPrefix, errors, warnings); case 'table': return this.processTableAssumption(assumption, baseAssumption, fieldPrefix, errors, warnings); default: errors.push({ field: `${fieldPrefix}.type`, message: `Unknown assumption type: ${assumption.type}`, severity: 'error' }); return null; } } /** * Process fixed assumption with validation */ static processFixedAssumption(assumption, baseAssumption, fieldPrefix, errors, warnings) { if (assumption.value === undefined || assumption.value === null) { errors.push({ field: `${fieldPrefix}.value`, message: 'Fixed assumption must have a value', severity: 'error' }); return null; } if (typeof assumption.value !== 'number') { errors.push({ field: `${fieldPrefix}.value`, message: 'Fixed assumption value must be a number', severity: 'error' }); return null; } if (!isFinite(assumption.value)) { errors.push({ field: `${fieldPrefix}.value`, message: 'Fixed assumption value must be finite', severity: 'error' }); return null; } // Validate for reasonable ranges based on common CRE assumptions if (Math.abs(assumption.value) > 1000000000) { // 1 billion warnings.push({ field: `${fieldPrefix}.value`, message: `Very large fixed assumption value: ${assumption.value}`, severity: 'warning' }); } return { ...baseAssumption, value: assumption.value, resolved_value: assumption.value }; } /** * Process distribution assumption with parameter validation */ static processDistributionAssumption(assumption, baseAssumption, fieldPrefix, errors, warnings) { if (!assumption.distribution) { errors.push({ field: `${fieldPrefix}.distribution`, message: 'Distribution assumption must specify distribution type', severity: 'error' }); return null; } const validDistributions = ['normal', 'uniform', 'triangular', 'lognormal']; if (!validDistributions.includes(assumption.distribution)) { errors.push({ field: `${fieldPrefix}.distribution`, message: `Invalid distribution type "${assumption.distribution}". Valid types: ${validDistributions.join(', ')}`, severity: 'error' }); return null; } if (!assumption.parameters || typeof assumption.parameters !== 'object') { errors.push({ field: `${fieldPrefix}.parameters`, message: 'Distribution assumption must have parameters object', severity: 'error' }); return null; } // Validate parameters based on distribution type const isValidParams = this.validateDistributionParameters(assumption.distribution, assumption.parameters, `${fieldPrefix}.parameters`, errors, warnings); if (!isValidParams) return null; return { ...baseAssumption, distribution: assumption.distribution, parameters: assumption.parameters, resolved_value: undefined // Will be resolved during simulation }; } /** * Process expression assumption with formula validation */ static processExpressionAssumption(assumption, baseAssumption, fieldPrefix, errors, _warnings) { if (!assumption.formula || typeof assumption.formula !== 'string') { errors.push({ field: `${fieldPrefix}.formula`, message: 'Expression assumption must have a formula string', severity: 'error' }); return null; } if (assumption.formula.trim().length === 0) { errors.push({ field: `${fieldPrefix}.formula`, message: 'Expression assumption formula cannot be empty', severity: 'error' }); return null; } // Basic formula validation (can be enhanced with proper expression parser) if (assumption.formula.length > 1000) { errors.push({ field: `${fieldPrefix}.formula`, message: 'Expression assumption formula is too long (max 1000 characters)', severity: 'error' }); return null; } return { ...baseAssumption, formula: assumption.formula, resolved_value: undefined // Will be resolved during evaluation }; } /** * Process table assumption with table validation */ static processTableAssumption(assumption, baseAssumption, fieldPrefix, errors, warnings) { if (!assumption.values || !Array.isArray(assumption.values)) { errors.push({ field: `${fieldPrefix}.values`, message: 'Table assumption must have a values array', severity: 'error' }); return null; } if (assumption.values.length === 0) { errors.push({ field: `${fieldPrefix}.values`, message: 'Table assumption must have at least one value', severity: 'error' }); return null; } // Validate each table value const validatedValues = []; for (let i = 0; i < assumption.values.length; i++) { const value = assumption.values[i]; const valuePrefix = `${fieldPrefix}.values[${i}]`; if (!value || typeof value !== 'object') { errors.push({ field: valuePrefix, message: 'Table value must be an object', severity: 'error' }); continue; } if (!value.name) { errors.push({ field: `${valuePrefix}.name`, message: 'Table value must have a name', severity: 'error' }); continue; } // Check for at least one value field const hasValueField = value.amount_per_sf !== undefined || value.amount !== undefined || value.percentage !== undefined; if (!hasValueField) { warnings.push({ field: valuePrefix, message: 'Table value should have at least one value field (amount_per_sf, amount, or percentage)', severity: 'warning' }); } // Validate numeric fields ['amount_per_sf', 'amount', 'percentage'].forEach(field => { if (value[field] !== undefined) { if (typeof value[field] !== 'number' || !isFinite(value[field])) { errors.push({ field: `${valuePrefix}.${field}`, message: `Table value ${field} must be a finite number`, severity: 'error' }); } } }); validatedValues.push(value); } if (validatedValues.length === 0) { errors.push({ field: `${fieldPrefix}.values`, message: 'Table assumption has no valid values', severity: 'error' }); return null; } return { ...baseAssumption, values: validatedValues, resolved_value: validatedValues // Tables resolve to their full structure }; } /** * Validate distribution parameters based on distribution type */ static validateDistributionParameters(distributionType, parameters, fieldPrefix, errors, warnings) { switch (distributionType) { case 'normal': if (parameters.mean === undefined || typeof parameters.mean !== 'number') { errors.push({ field: `${fieldPrefix}.mean`, message: 'Normal distribution requires numeric mean parameter', severity: 'error' }); return false; } if (parameters.stddev === undefined || typeof parameters.stddev !== 'number' || parameters.stddev <= 0) { errors.push({ field: `${fieldPrefix}.stddev`, message: 'Normal distribution requires positive numeric stddev parameter', severity: 'error' }); return false; } break; case 'uniform': if (parameters.min === undefined || typeof parameters.min !== 'number') { errors.push({ field: `${fieldPrefix}.min`, message: 'Uniform distribution requires numeric min parameter', severity: 'error' }); return false; } if (parameters.max === undefined || typeof parameters.max !== 'number') { errors.push({ field: `${fieldPrefix}.max`, message: 'Uniform distribution requires numeric max parameter', severity: 'error' }); return false; } if (parameters.max <= parameters.min) { errors.push({ field: `${fieldPrefix}.max`, message: 'Uniform distribution max must be greater than min', severity: 'error' }); return false; } break; case 'triangular': if (parameters.min === undefined || typeof parameters.min !== 'number') { errors.push({ field: `${fieldPrefix}.min`, message: 'Triangular distribution requires numeric min parameter', severity: 'error' }); return false; } if (parameters.max === undefined || typeof parameters.max !== 'number') { errors.push({ field: `${fieldPrefix}.max`, message: 'Triangular distribution requires numeric max parameter', severity: 'error' }); return false; } if (parameters.mode === undefined || typeof parameters.mode !== 'number') { errors.push({ field: `${fieldPrefix}.mode`, message: 'Triangular distribution requires numeric mode parameter', severity: 'error' }); return false; } if (parameters.max <= parameters.min) { errors.push({ field: `${fieldPrefix}.max`, message: 'Triangular distribution max must be greater than min', severity: 'error' }); return false; } if (parameters.mode < parameters.min || parameters.mode > parameters.max) { errors.push({ field: `${fieldPrefix}.mode`, message: 'Triangular distribution mode must be between min and max', severity: 'error' }); return false; } break; case 'lognormal': if (parameters.mean === undefined || typeof parameters.mean !== 'number') { errors.push({ field: `${fieldPrefix}.mean`, message: 'Lognormal distribution requires numeric mean parameter', severity: 'error' }); return false; } if (parameters.stddev === undefined || typeof parameters.stddev !== 'number' || parameters.stddev <= 0) { errors.push({ field: `${fieldPrefix}.stddev`, message: 'Lognormal distribution requires positive numeric stddev parameter', severity: 'error' }); return false; } if (parameters.stddev > 5) { warnings.push({ field: `${fieldPrefix}.stddev`, message: 'Very large standard deviation for lognormal distribution may cause numerical issues', severity: 'warning' }); } break; } return true; } /** * Validate cross-references to assets and spaces in assumption scope */ static validateAssumptionCrossReferences(assumption, fieldPrefix, warnings) { if (!assumption.scope) return; // Parse scope for asset and space references const scope = assumption.scope.toLowerCase(); // Check for asset references (format: "asset:asset_id" or "assets:asset_id1,asset_id2") const assetMatches = scope.match(/assets?:([a-zA-Z0-9_,-]+)/g); if (assetMatches) { assetMatches.forEach(match => { const parts = match.split(':'); if (parts.length > 1 && parts[1]) { const assetIds = parts[1].split(','); assetIds.forEach(assetId => { const trimmedId = assetId.trim(); const assetExists = Array.from(this.buildingRegistry.values()) .some(info => info.asset.id === trimmedId); if (!assetExists) { warnings.push({ field: `${fieldPrefix}.scope`, message: `Assumption "${assumption.name}" references non-existent asset "${trimmedId}"`, severity: 'warning' }); } }); } }); } // Check for space references (format: "space:space_id" or "spaces:space_id1,space_id2") const spaceMatches = scope.match(/spaces?:([a-zA-Z0-9_,-]+)/g); if (spaceMatches) { spaceMatches.forEach(match => { const parts = match.split(':'); if (parts.length > 1 && parts[1]) { const spaceIds = parts[1].split(','); spaceIds.forEach(spaceId => { // Note: We can't validate space references here as spaces might not be processed yet // This validation would be better done in a post-processing step spaceId.trim(); // Just to use the variable }); } }); } // Check for building references (format: "building:building_id") const buildingMatches = scope.match(/buildings?:([a-zA-Z0-9_,-]+)/g); if (buildingMatches) { buildingMatches.forEach(match => { const parts = match.split(':'); if (parts.length > 1 && parts[1]) { const buildingIds = parts[1].split(','); buildingIds.forEach(buildingId => { const trimmedId = buildingId.trim(); if (!this.buildingRegistry.has(trimmedId)) { warnings.push({ field: `${fieldPrefix}.scope`, message: `Assumption "${assumption.name}" references non-existent building "${trimmedId}"`, severity: 'warning' }); } }); } }); } } /** * Resolve scope hierarchy for assumptions per DSL-to-Engine mapping */ static resolveScopeHierarchy(scope) { const defaultScope = { type: 'global', target: 'all', hierarchy_level: 5 }; if (!scope || scope === 'global') { return defaultScope; } // Parse scope string format: "type:target" or just "type" const parts = scope.split(':'); if (parts.length === 1) { const scopeType = parts[0] || 'global'; return { type: scopeType, target: 'all', hierarchy_level: this.getScopeHierarchyLevel(scopeType) }; } if (parts.length === 2) { const [type, target] = parts; const scopeType = type || 'global'; const scopeTarget = target || 'all'; return { type: scopeType, target: scopeTarget, hierarchy_level: this.getScopeHierarchyLevel(scopeType) }; } return defaultScope; } /** * Get hierarchy level for scope types (lower number = more specific) */ static getScopeHierarchyLevel(scopeType) { const hierarchy = { 'space_id': 1, // Most specific 'space_type': 2, 'building_id': 3, 'asset_id': 4, 'global': 5 // Least specific }; return hierarchy[scopeType] || 5; // Default to global level } /** * Calculate applicable spaces based on scope per DSL-to-Engine mapping */ static calculateApplicableSpaces(scope) { if (!scope || scope === 'global') { return ['*']; // Global scope applies to all spaces } const parts = scope.split(':'); if (parts.length !== 2) { return ['*']; // Default to all spaces if scope format is invalid } const [scopeType, target] = parts; const scopeTarget = target || 'unknown'; switch (scopeType) { case 'space_id': return [scopeTarget]; // Single space case 'space_type': // Note: We can't calculate this accurately here without processed spaces // This would need to be updated in a post-processing step return [`@space_type:${scopeTarget}`]; // Placeholder for space type resolution case 'building_id': // Note: Similar limitation - needs post-processing with actual spaces return [`@building_id:${scopeTarget}`]; // Placeholder for building resolution case 'asset_id': // Note: Similar limitation - needs post-processing with actual spaces return [`@asset_id:${scopeTarget}`]; // Placeholder for asset resolution default: return ['*']; // Default to all spaces } } /** * Validate assumption metadata (tags, description, etc.) */ static validateAssumptionMetadata(assumption, fieldPrefix, warnings) { // Validate tags format if (assumption.tags) { if (!Array.isArray(assumption.tags)) { warnings.push({ field: `${fieldPrefix}.tags`, message: 'Assumption tags should be an array', severity: 'warning' }); } else { assumption.tags.forEach((tag, index) => { if (typeof tag !== 'string') { warnings.push({ field: `${fieldPrefix}.tags[${index}]`, message: 'Assumption tag should be a string', severity: 'warning' }); } else if (tag.trim().length === 0) { warnings.push({ field: `${fieldPrefix}.tags[${index}]`, message: 'Assumption tag cannot be empty', severity: 'warning' }); } }); } } // Validate description length if (assumption.description && assumption.description.length > 1000) { warnings.push({ field: `${fieldPrefix}.description`, message: 'Assumption description is very long (>1000 characters)', severity: 'warning' }); } // Validate scope format if (assumption.scope && assumption.scope.trim().length === 0) { warnings.push({ field: `${fieldPrefix}.scope`, message: 'Assumption scope cannot be empty if specified', severity: 'warning' }); } } /** * Validate dependencies between assumptions */ static validateAssumptionDependencies(assumptions, _errors, warnings) { // Check for potential circular dependencies in expression assumptions const expressionAssumptions = assumptions.filter(a => a.type === 'expression'); expressionAssumptions.forEach(assumption => { if ('formula' in assumption && assumption.formula) { // Basic check for self-reference const formula = assumption.formula.toLowerCase(); const assumptionName = assumption.name.toLowerCase(); if (formula.includes(assumptionName)) { warnings.push({ field: 'assumptions', message: `Expression assumption "${assumption.name}" may reference itself, check for circular dependencies`, severity: 'warning' }); } // Check for references to other assumptions that don't exist assumptions.forEach(otherAssumption => { if (otherAssumption.name !== assumption.name) {