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
JavaScript
"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) {