UNPKG

credl-parser-evaluator

Version:

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

1,075 lines 106 kB
"use strict"; /** * CREDL Schema Validator * * Validates CREDL file structure, types, and cross-references according to the * CREDL v0.2 specification. Implements strict validation with fail-fast behavior. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SchemaValidator = void 0; const CREDLTypes_1 = require("../types/CREDLTypes"); const validation_messages_1 = require("./validation-messages"); class SchemaValidator { /** * Validate a complete CREDL file */ static validate(credlFile, options) { const opts = { ...this.DEFAULT_OPTIONS, ...options }; const errors = []; const warnings = []; try { // Validate required core blocks this.validateMetadata(credlFile.metadata, errors, warnings, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } this.validateAssets(credlFile.assets, errors, warnings, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } this.validateSpaces(credlFile.spaces, errors, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } this.validateAssumptions(credlFile.assumptions, errors, warnings, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } this.validateModels(credlFile.models, errors, warnings, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } this.validateSimulation(credlFile.simulation, errors, warnings, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } this.validateOutputs(credlFile.outputs, errors, warnings, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } // Validate optional first-class blocks (per Behavioral Specification 8.1) if (credlFile.scenarios) { this.validateScenarios(credlFile.scenarios, errors, warnings, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } } if (credlFile.waterfall) { this.validateWaterfall(credlFile.waterfall, errors, warnings, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } } // Validate legacy extension block if present (backward compatibility) if (credlFile.extensions) { this.validateExtensions(credlFile.extensions, errors, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } } if (credlFile.presets) { this.validatePresets(credlFile.presets, errors, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } } if (credlFile.templates) { this.validateTemplates(credlFile.templates, errors, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } } // Validate cross-references if (opts.checkCrossReferences) { this.validateCrossReferences(credlFile, errors, warnings, opts); if (opts.failFast && errors.length > 0) { return { isValid: false, errors, warnings }; } } return { isValid: errors.length === 0, errors, warnings }; } catch (error) { // Enhanced validation error handling with categorized messages let errorMessage = validation_messages_1.VALIDATION_MESSAGES.GENERAL.VALIDATION_FAILED; let helpText = validation_messages_1.VALIDATION_HELP.GENERAL.VALIDATION_ERRORS; if (error instanceof Error) { // Categorize common validation errors if (error.message.includes('circular') || error.message.includes('reference')) { errorMessage = 'Circular reference detected during validation'; helpText = 'Remove circular references between blocks (e.g., space → assumption → space).'; } else if (error.message.includes('memory') || error.message.includes('heap')) { errorMessage = 'File too complex for validation'; helpText = 'Consider breaking large files into smaller components or reducing cross-references.'; } else if (error.message.includes('stack') || error.message.includes('recursion')) { errorMessage = 'Maximum validation depth exceeded'; helpText = 'Reduce nesting levels or circular references in your CREDL structure.'; } else { errorMessage = validation_messages_1.VALIDATION_MESSAGES.GENERAL.UNEXPECTED_ERROR(error.message); helpText = validation_messages_1.VALIDATION_HELP.GENERAL.VALIDATION_ERRORS; } } errors.push({ field: 'validation', message: errorMessage, severity: 'error', help: helpText }); return { isValid: false, errors, warnings }; } } /** * Validate metadata block */ static validateMetadata(metadata, errors, warnings, _opts) { if (!metadata) { errors.push({ field: 'metadata', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.MISSING_BLOCK, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.VERSION_FORMAT }); return; } // Version validation per behavioral specification // Parser version 1.0 supports CREDL specification versions 0.1 and 0.2 const SUPPORTED_CREDL_VERSIONS = { min: 0.1, max: 0.2 }; if (typeof metadata.version !== 'string' || metadata.version.trim().length === 0) { errors.push({ field: 'metadata.version', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.MISSING_VERSION, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.VERSION_FORMAT }); } else { // Parse CREDL specification version string and validate compatibility const credlSpecVersion = parseFloat(metadata.version); if (isNaN(credlSpecVersion)) { errors.push({ field: 'metadata.version', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.INVALID_VERSION, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.VERSION_FORMAT }); } else if (credlSpecVersion < SUPPORTED_CREDL_VERSIONS.min) { errors.push({ field: 'metadata.version', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.UNSUPPORTED_VERSION(metadata.version, SUPPORTED_CREDL_VERSIONS.min.toString()), severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.VERSION_FORMAT }); } else if (credlSpecVersion > SUPPORTED_CREDL_VERSIONS.max) { // Future CREDL specification versions generate warnings but don't stop processing warnings.push({ field: 'metadata.version', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.NEWER_VERSION(metadata.version, SUPPORTED_CREDL_VERSIONS.max.toString()), severity: 'warning', help: validation_messages_1.VALIDATION_HELP.METADATA.VERSION_FORMAT }); } // Versions 0.1 and 0.2 are fully supported - no warnings } // Name validation if (typeof metadata.name !== 'string' || metadata.name.trim().length === 0) { errors.push({ field: 'metadata.name', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.MISSING_NAME, severity: 'error' }); } if (typeof metadata.description !== 'string' || metadata.description.trim().length === 0) { errors.push({ field: 'metadata.description', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.MISSING_DESCRIPTION, severity: 'error' }); } // New required field per behavioral specification if (typeof metadata.analysis_start_date !== 'string' || metadata.analysis_start_date.trim().length === 0) { errors.push({ field: 'metadata.analysis_start_date', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.MISSING_ANALYSIS_START, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.ANALYSIS_START }); } else { // Validate ISO 8601 date format (YYYY-MM-DD) const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(metadata.analysis_start_date)) { errors.push({ field: 'metadata.analysis_start_date', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.INVALID_DATE_FORMAT, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.DATE_FORMAT }); } else { // Validate it's a real date const date = new Date(metadata.analysis_start_date); if (isNaN(date.getTime())) { errors.push({ field: 'metadata.analysis_start_date', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.INVALID_DATE, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.DATE_FORMAT }); } } } if (typeof metadata.created_date !== 'string' || metadata.created_date.trim().length === 0) { errors.push({ field: 'metadata.created_date', message: 'created_date is required and must be a non-empty string', severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.CREATED_DATE }); } else { // Validate ISO 8601 date format (YYYY-MM-DD) const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(metadata.created_date)) { errors.push({ field: 'metadata.created_date', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.INVALID_DATE_FORMAT, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.DATE_FORMAT }); } else { // Validate it's a real date const date = new Date(metadata.created_date); if (isNaN(date.getTime())) { errors.push({ field: 'metadata.created_date', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.INVALID_DATE, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.DATE_FORMAT }); } } } // Optional fields validation if (metadata.modified_date !== undefined) { if (typeof metadata.modified_date !== 'string') { errors.push({ field: 'metadata.modified_date', message: 'modified_date must be a string if provided', severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.DATE_FORMAT }); } else { const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(metadata.modified_date)) { errors.push({ field: 'metadata.modified_date', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.INVALID_DATE_FORMAT, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.DATE_FORMAT }); } else { // Validate it's a real date const date = new Date(metadata.modified_date); if (isNaN(date.getTime())) { errors.push({ field: 'metadata.modified_date', message: validation_messages_1.VALIDATION_MESSAGES.METADATA.INVALID_DATE, severity: 'error', help: validation_messages_1.VALIDATION_HELP.METADATA.DATE_FORMAT }); } } } } if (metadata.author && typeof metadata.author !== 'string') { errors.push({ field: 'metadata.author', message: 'author must be a string if provided', severity: 'error' }); } } /** * Validate assets block with enhanced error handling */ static validateAssets(assets, errors, warnings, opts) { if (!Array.isArray(assets)) { errors.push({ field: 'assets', message: validation_messages_1.VALIDATION_MESSAGES.ASSETS.MISSING_ARRAY, severity: 'error', help: validation_messages_1.VALIDATION_HELP.ASSETS.REQUIRED_FIELDS }); return; } if (assets.length === 0) { errors.push({ field: 'assets', message: validation_messages_1.VALIDATION_MESSAGES.ASSETS.EMPTY_ARRAY, severity: 'error', help: validation_messages_1.VALIDATION_HELP.ASSETS.REQUIRED_FIELDS }); return; } const assetIds = new Set(); for (let i = 0; i < assets.length; i++) { const asset = assets[i]; const fieldPrefix = `assets[${i}]`; if (!asset || typeof asset !== 'object') { errors.push({ field: fieldPrefix, message: 'asset must be an object', severity: 'error' }); continue; } // Enhanced field validation with specific error messages and help text if (typeof asset.id !== 'string' || asset.id.trim().length === 0) { errors.push({ field: `${fieldPrefix}.id`, message: validation_messages_1.VALIDATION_MESSAGES.ASSETS.MISSING_ID, severity: 'error', help: validation_messages_1.VALIDATION_HELP.ASSETS.REQUIRED_FIELDS }); } else { // Check for duplicate IDs if (assetIds.has(asset.id)) { errors.push({ field: `${fieldPrefix}.id`, message: validation_messages_1.VALIDATION_MESSAGES.ASSETS.DUPLICATE_ID(asset.id), severity: 'error', help: 'Each asset must have a unique identifier. Change the id field to a unique value.' }); } else { assetIds.add(asset.id); } } if (typeof asset.name !== 'string' || asset.name.trim().length === 0) { errors.push({ field: `${fieldPrefix}.name`, message: validation_messages_1.VALIDATION_MESSAGES.ASSETS.MISSING_NAME, severity: 'error', help: validation_messages_1.VALIDATION_HELP.ASSETS.REQUIRED_FIELDS }); } // Enhanced property_type validation with actionable guidance if (!Object.values(CREDLTypes_1.PropertyType).includes(asset.property_type)) { errors.push({ field: `${fieldPrefix}.property_type`, message: validation_messages_1.VALIDATION_MESSAGES.ASSETS.INVALID_PROPERTY_TYPE(Object.values(CREDLTypes_1.PropertyType)), severity: 'error', help: validation_messages_1.VALIDATION_HELP.ASSETS.PROPERTY_TYPES }); } if (typeof asset.location !== 'string' || asset.location.trim().length === 0) { errors.push({ field: `${fieldPrefix}.location`, message: validation_messages_1.VALIDATION_MESSAGES.ASSETS.MISSING_LOCATION, severity: 'error', help: validation_messages_1.VALIDATION_HELP.ASSETS.REQUIRED_FIELDS }); } if (typeof asset.total_area_sf !== 'number' || asset.total_area_sf <= 0) { errors.push({ field: `${fieldPrefix}.total_area_sf`, message: validation_messages_1.VALIDATION_MESSAGES.ASSETS.MISSING_AREA, severity: 'error', help: validation_messages_1.VALIDATION_HELP.ASSETS.REQUIRED_FIELDS }); } // Validate buildings array if (!Array.isArray(asset.buildings)) { errors.push({ field: `${fieldPrefix}.buildings`, message: 'buildings must be an array', severity: 'error' }); } else if (asset.buildings.length === 0) { errors.push({ field: `${fieldPrefix}.buildings`, message: 'at least one building is required', severity: 'error' }); } else { this.validateBuildings(asset.buildings, `${fieldPrefix}.buildings`, errors, opts); // Asset area validation per behavioral specification (10% tolerance for common areas) if (typeof asset.total_area_sf === 'number' && asset.total_area_sf > 0) { const totalBuildingArea = asset.buildings .filter((b) => b && typeof b.total_area_sf === 'number') .reduce((sum, b) => sum + (b.total_area_sf || 0), 0); if (totalBuildingArea > 0) { const ratio = totalBuildingArea / asset.total_area_sf; if (ratio > 1.1) { // >10% over asset area warnings.push({ field: `${fieldPrefix}.total_area_sf`, message: `Building areas (${totalBuildingArea.toLocaleString()} SF) exceed asset area by ${((ratio - 1) * 100).toFixed(1)}%. Consider accounting for common areas.`, severity: 'warning' }); } else if (ratio < 0.5) { // <50% of asset area - unusually low warnings.push({ field: `${fieldPrefix}.total_area_sf`, message: `Building areas (${totalBuildingArea.toLocaleString()} SF) are only ${(ratio * 100).toFixed(1)}% of asset area (${asset.total_area_sf.toLocaleString()} SF). This seems unusually low.`, severity: 'warning' }); } } } } } } /** * Validate buildings within an asset */ static validateBuildings(buildings, fieldPrefix, errors, _opts) { const buildingIds = new Set(); for (let i = 0; i < buildings.length; i++) { const building = buildings[i]; const buildingFieldPrefix = `${fieldPrefix}[${i}]`; if (!building || typeof building !== 'object') { errors.push({ field: buildingFieldPrefix, message: 'building must be an object', severity: 'error' }); continue; } // Validate required fields if (typeof building.id !== 'string' || building.id.trim().length === 0) { errors.push({ field: `${buildingFieldPrefix}.id`, message: 'id is required and must be a non-empty string', severity: 'error' }); } else { // Check for duplicate building IDs if (buildingIds.has(building.id)) { errors.push({ field: `${buildingFieldPrefix}.id`, message: `duplicate building id: ${building.id}`, severity: 'error' }); } else { buildingIds.add(building.id); } } if (typeof building.name !== 'string' || building.name.trim().length === 0) { errors.push({ field: `${buildingFieldPrefix}.name`, message: 'name is required and must be a non-empty string', severity: 'error' }); } // Validate optional numeric fields if (building.floors !== undefined && (typeof building.floors !== 'number' || building.floors <= 0)) { errors.push({ field: `${buildingFieldPrefix}.floors`, message: 'floors must be a positive number if provided', severity: 'error' }); } if (building.total_area_sf !== undefined && (typeof building.total_area_sf !== 'number' || building.total_area_sf <= 0)) { errors.push({ field: `${buildingFieldPrefix}.total_area_sf`, message: 'total_area_sf must be a positive number if provided', severity: 'error' }); } if (building.year_built !== undefined && (typeof building.year_built !== 'number' || building.year_built < 1800 || building.year_built > new Date().getFullYear() + 10)) { errors.push({ field: `${buildingFieldPrefix}.year_built`, message: 'year_built must be a reasonable year if provided', severity: 'error' }); } } } /** * Validate spaces with field exclusivity and conditional requirements */ static validateSpaces(spaces, errors, opts) { if (!Array.isArray(spaces)) { errors.push({ field: 'spaces', message: validation_messages_1.VALIDATION_MESSAGES.SPACES.MISSING_ARRAY, severity: 'error', help: validation_messages_1.VALIDATION_HELP.SPACES.REQUIRED_FIELDS }); return; } const spaceIds = new Set(); for (let i = 0; i < spaces.length; i++) { const space = spaces[i]; const fieldPrefix = `spaces[${i}]`; if (!space || typeof space !== 'object') { errors.push({ field: fieldPrefix, message: validation_messages_1.VALIDATION_MESSAGES.SPACES.INVALID_SPACE, severity: 'error', help: validation_messages_1.VALIDATION_HELP.SPACES.REQUIRED_FIELDS }); continue; } // Validate required fields if (typeof space.id !== 'string' || space.id.trim().length === 0) { errors.push({ field: `${fieldPrefix}.id`, message: validation_messages_1.VALIDATION_MESSAGES.SPACES.MISSING_ID, severity: 'error', help: validation_messages_1.VALIDATION_HELP.SPACES.REQUIRED_FIELDS }); } else { // Check for duplicate space IDs if (spaceIds.has(space.id)) { errors.push({ field: `${fieldPrefix}.id`, message: validation_messages_1.VALIDATION_MESSAGES.SPACES.DUPLICATE_ID(space.id), severity: 'error', help: 'Each space must have a unique identifier across your entire CREDL file' }); } spaceIds.add(space.id); } if (typeof space.parent_building !== 'string' || space.parent_building.trim().length === 0) { errors.push({ field: `${fieldPrefix}.parent_building`, message: validation_messages_1.VALIDATION_MESSAGES.SPACES.MISSING_PARENT_BUILDING, severity: 'error', help: validation_messages_1.VALIDATION_HELP.SPACES.PARENT_BUILDING }); } if (!Object.values(CREDLTypes_1.SpaceType).includes(space.type)) { errors.push({ field: `${fieldPrefix}.type`, message: `type must be one of: ${Object.values(CREDLTypes_1.SpaceType).join(', ')}`, severity: 'error' }); } if (typeof space.area_sf !== 'number' || space.area_sf <= 0) { errors.push({ field: `${fieldPrefix}.area_sf`, message: validation_messages_1.VALIDATION_MESSAGES.SPACES.MISSING_AREA, severity: 'error', help: validation_messages_1.VALIDATION_HELP.SPACES.REQUIRED_FIELDS }); } // Validate auto-populated source_template field cannot be manually specified if (space.source_template !== undefined) { errors.push({ field: `${fieldPrefix}.source_template`, message: validation_messages_1.VALIDATION_MESSAGES.SPACES.AUTO_POPULATED_FIELD, severity: 'error', help: validation_messages_1.VALIDATION_HELP.SPACES.SOURCE_TEMPLATE }); } // Validate field exclusivity: prevent conflicts between preset references and direct field specifications // This ensures spaces use either preset values OR direct values, not both which would create ambiguity if (space.preset_lease && space.lease) { errors.push({ field: `${fieldPrefix}`, message: validation_messages_1.VALIDATION_MESSAGES.SPACES.PRESET_LEASE_CONFLICT, severity: 'error', help: validation_messages_1.VALIDATION_HELP.SPACES.PRESET_CONFLICTS }); } if (space.preset_expense && space.expense) { errors.push({ field: `${fieldPrefix}`, message: validation_messages_1.VALIDATION_MESSAGES.SPACES.PRESET_EXPENSE_CONFLICT, severity: 'error', help: validation_messages_1.VALIDATION_HELP.SPACES.PRESET_CONFLICTS }); } // Validate lease information (behavioral specification 4.1 & 4.2) // Spaces must have either direct blocks OR use preset references const hasDirectLease = !!space.lease; const hasDirectLeaseAssumptions = !!space.lease_assumptions; const hasPresetReferences = !!(space.preset_lease || space.preset_expenses); // Check if space has neither direct fields nor preset references if (!hasDirectLease && !hasDirectLeaseAssumptions && !hasPresetReferences) { errors.push({ field: fieldPrefix, message: validation_messages_1.VALIDATION_MESSAGES.SPACES.MISSING_LEASE_INFO, severity: 'error', help: validation_messages_1.VALIDATION_HELP.SPACES.LEASE_OPTIONS }); } // Validate direct lease block if present if (hasDirectLease) { this.validateLease(space.lease, `${fieldPrefix}.lease`, errors, opts); } // Validate direct lease_assumptions block if present if (hasDirectLeaseAssumptions) { this.validateLeaseAssumptions(space.lease_assumptions, `${fieldPrefix}.lease_assumptions`, errors, opts); } } } /** * Validate lease information */ static validateLease(lease, fieldPrefix, errors, _opts) { if (!lease || typeof lease !== 'object') { errors.push({ field: fieldPrefix, message: 'lease must be an object', severity: 'error' }); return; } // Validate required fields if (!Object.values(CREDLTypes_1.LeaseStatus).includes(lease.status)) { errors.push({ field: `${fieldPrefix}.status`, message: `status must be one of: ${Object.values(CREDLTypes_1.LeaseStatus).join(', ')}`, severity: 'error' }); } if (typeof lease.rent_psf !== 'number' || lease.rent_psf < 0) { errors.push({ field: `${fieldPrefix}.rent_psf`, message: 'rent_psf is required and must be a non-negative number', severity: 'error' }); } if (!Object.values(CREDLTypes_1.LeaseType).includes(lease.lease_type)) { errors.push({ field: `${fieldPrefix}.lease_type`, message: `lease_type must be one of: ${Object.values(CREDLTypes_1.LeaseType).join(', ')}`, severity: 'error' }); } // Conditional field requirements based on lease status // Leased spaces require tenant information, vacant spaces should not have tenant specified if (lease.status === CREDLTypes_1.LeaseStatus.LEASED) { // Required fields for leased spaces if (typeof lease.tenant !== 'string' || lease.tenant.trim().length === 0) { errors.push({ field: `${fieldPrefix}.tenant`, message: 'tenant is required for leased spaces and must be a non-empty string', severity: 'error' }); } } else if (lease.status === CREDLTypes_1.LeaseStatus.VACANT) { // For vacant spaces, tenant should not be specified if (lease.tenant !== undefined) { errors.push({ field: `${fieldPrefix}.tenant`, message: 'tenant field is not allowed for vacant spaces', severity: 'error' }); } } // Validate optional escalation_rate if (lease.escalation_rate !== undefined) { if (typeof lease.escalation_rate !== 'number') { errors.push({ field: `${fieldPrefix}.escalation_rate`, message: 'escalation_rate must be a number if provided', severity: 'error' }); } } // Validate renewal_probability (behavioral specification 4.3) if (lease.renewal_probability !== undefined) { if (typeof lease.renewal_probability !== 'number' || lease.renewal_probability < 0 || lease.renewal_probability > 1) { errors.push({ field: `${fieldPrefix}.renewal_probability`, message: 'renewal_probability must be a number between 0 and 1 if provided', severity: 'error' }); } } // Validate optional financial fields if (lease.security_deposit !== undefined) { if (typeof lease.security_deposit !== 'number' || lease.security_deposit < 0) { errors.push({ field: `${fieldPrefix}.security_deposit`, message: 'security_deposit must be a non-negative number if provided', severity: 'error' }); } } if (lease.tenant_improvements !== undefined) { if (typeof lease.tenant_improvements !== 'number' || lease.tenant_improvements < 0) { errors.push({ field: `${fieldPrefix}.tenant_improvements`, message: 'tenant_improvements must be a non-negative number if provided', severity: 'error' }); } } if (lease.leasing_commissions !== undefined) { if (typeof lease.leasing_commissions !== 'number' || lease.leasing_commissions < 0) { errors.push({ field: `${fieldPrefix}.leasing_commissions`, message: 'leasing_commissions must be a non-negative number if provided', severity: 'error' }); } } // start_date and end_date are only required for leased spaces (behavioral specification 4.1) if (lease.status === CREDLTypes_1.LeaseStatus.LEASED) { if (typeof lease.start_date !== 'string' || lease.start_date.trim().length === 0) { errors.push({ field: `${fieldPrefix}.start_date`, message: 'start_date is required for leased spaces and must be a non-empty string', severity: 'error' }); } if (typeof lease.end_date !== 'string' || lease.end_date.trim().length === 0) { errors.push({ field: `${fieldPrefix}.end_date`, message: 'end_date is required for leased spaces and must be a non-empty string', severity: 'error' }); } } else { // For vacant/proposed spaces, dates are optional market assumptions (behavioral specification 4.2) if (lease.start_date !== undefined && (typeof lease.start_date !== 'string' || lease.start_date.trim().length === 0)) { errors.push({ field: `${fieldPrefix}.start_date`, message: 'start_date must be a non-empty string if provided', severity: 'error' }); } if (lease.end_date !== undefined && (typeof lease.end_date !== 'string' || lease.end_date.trim().length === 0)) { errors.push({ field: `${fieldPrefix}.end_date`, message: 'end_date must be a non-empty string if provided', severity: 'error' }); } } } /** * Validate lease assumptions */ static validateLeaseAssumptions(assumptions, fieldPrefix, errors, _opts) { if (!assumptions || typeof assumptions !== 'object') { errors.push({ field: fieldPrefix, message: 'lease_assumptions must be an object', severity: 'error' }); return; } if (typeof assumptions.rent_psf !== 'number' || assumptions.rent_psf < 0) { errors.push({ field: `${fieldPrefix}.rent_psf`, message: 'rent_psf is required and must be a non-negative number', severity: 'error' }); } if (!Object.values(CREDLTypes_1.LeaseType).includes(assumptions.lease_type)) { errors.push({ field: `${fieldPrefix}.lease_type`, message: `lease_type must be one of: ${Object.values(CREDLTypes_1.LeaseType).join(', ')}`, severity: 'error' }); } // Validate optional leasing timeline assumptions (behavioral specification 4.2) if (assumptions.expected_turnover_months !== undefined) { if (typeof assumptions.expected_turnover_months !== 'number' || assumptions.expected_turnover_months < 0) { errors.push({ field: `${fieldPrefix}.expected_turnover_months`, message: 'expected_turnover_months must be a non-negative number if provided', severity: 'error' }); } } if (assumptions.absorption_sf_per_month !== undefined) { if (typeof assumptions.absorption_sf_per_month !== 'number' || assumptions.absorption_sf_per_month < 0) { errors.push({ field: `${fieldPrefix}.absorption_sf_per_month`, message: 'absorption_sf_per_month must be a non-negative number if provided', severity: 'error' }); } } // Validate optional renewal probability for future leases (behavioral specification 4.3) if (assumptions.renewal_probability !== undefined) { if (typeof assumptions.renewal_probability !== 'number' || assumptions.renewal_probability < 0 || assumptions.renewal_probability > 1) { errors.push({ field: `${fieldPrefix}.renewal_probability`, message: 'renewal_probability must be a number between 0 and 1 if provided', severity: 'error' }); } } } /** * Validate assumptions block with scope resolution and conflict detection */ static validateAssumptions(assumptions, errors, warnings, opts) { if (!Array.isArray(assumptions)) { errors.push({ field: 'assumptions', message: 'assumptions must be an array', severity: 'error' }); return; } const scopeMap = new Map(); // scope -> assumption names for (let i = 0; i < assumptions.length; i++) { const assumption = assumptions[i]; const fieldPrefix = `assumptions[${i}]`; if (!assumption || typeof assumption !== 'object') { errors.push({ field: fieldPrefix, message: 'assumption must be an object', severity: 'error' }); continue; } // Validate required fields if (typeof assumption.name !== 'string' || assumption.name.trim().length === 0) { errors.push({ field: `${fieldPrefix}.name`, message: 'name is required and must be a non-empty string', severity: 'error' }); } // Validate assumption type if (!Object.values(CREDLTypes_1.AssumptionType).includes(assumption.type)) { errors.push({ field: `${fieldPrefix}.type`, message: `type must be one of: ${Object.values(CREDLTypes_1.AssumptionType).join(', ')}`, severity: 'error' }); continue; } // Validate scope format and apply defaults this.validateAssumptionScope(assumption, fieldPrefix, errors, warnings); // Validate tag taxonomy when tags are provided this.validateAssumptionTags(assumption, fieldPrefix, errors, warnings); // Track name+scope combinations for conflict detection const normalizedScope = this.normalizeAssumptionScope(assumption.scope); const scopeKey = `${assumption.name}@${normalizedScope}`; if (scopeMap.has(scopeKey)) { // Duplicate name+scope combination found errors.push({ field: `${fieldPrefix}`, message: `Scope conflict detected: Assumption '${assumption.name}' is defined multiple times for scope '${normalizedScope}'`, severity: 'error' }); } else { scopeMap.set(scopeKey, [assumption.name]); } // Type-specific validation this.validateAssumptionByType(assumption, fieldPrefix, errors, opts); } // Check for assumptions targeting the same scope // this.validateAssumptionScopeConflicts(scopeMap, errors); // Now handled inline above } /** * Validate assumption based on its type */ static validateAssumptionByType(assumption, fieldPrefix, errors, opts) { if ((0, CREDLTypes_1.isFixedAssumption)(assumption)) { this.validateFixedAssumption(assumption, fieldPrefix, errors, opts); } else if ((0, CREDLTypes_1.isDistributionAssumption)(assumption)) { this.validateDistributionAssumption(assumption, fieldPrefix, errors, opts); } else if ((0, CREDLTypes_1.isExpressionAssumption)(assumption)) { this.validateExpressionAssumption(assumption, fieldPrefix, errors, opts); } else if ((0, CREDLTypes_1.isTableAssumption)(assumption)) { this.validateTableAssumption(assumption, fieldPrefix, errors, opts); } } /** * Validate fixed assumption */ static validateFixedAssumption(assumption, fieldPrefix, errors, _opts) { if (typeof assumption.value !== 'number') { errors.push({ field: `${fieldPrefix}.value`, message: 'value is required and must be a number for fixed assumptions', severity: 'error' }); } } /** * Validate distribution assumption */ static validateDistributionAssumption(assumption, fieldPrefix, errors, _opts) { if (!Object.values(CREDLTypes_1.DistributionType).includes(assumption.distribution)) { errors.push({ field: `${fieldPrefix}.distribution`, message: `distribution must be one of: ${Object.values(CREDLTypes_1.DistributionType).join(', ')}`, severity: 'error' }); } if (!assumption.parameters || typeof assumption.parameters !== 'object') { errors.push({ field: `${fieldPrefix}.parameters`, message: 'parameters is required and must be an object for distribution assumptions', severity: 'error' }); return; } // Validate parameters based on distribution type switch (assumption.distribution) { case CREDLTypes_1.DistributionType.NORMAL: if (typeof assumption.parameters.mean !== 'number') { errors.push({ field: `${fieldPrefix}.parameters.mean`, message: 'mean is required for normal distribution', severity: 'error' }); } if (typeof assumption.parameters.stddev !== 'number' || assumption.parameters.stddev <= 0) { errors.push({ field: `${fieldPrefix}.parameters.stddev`, message: 'stddev is required and must be positive for normal distribution', severity: 'error' }); } break; case CREDLTypes_1.DistributionType.UNIFORM: if (typeof assumption.parameters.min !== 'number') { errors.push({ field: `${fieldPrefix}.parameters.min`, message: 'min is required for uniform distribution', severity: 'error' }); } if (typeof assumption.parameters.max !== 'number') { errors.push({ field: `${fieldPrefix}.parameters.max`, message: 'max is required for uniform distribution', severity: 'error' }); } if (typeof assumption.parameters.min === 'number' && typeof assumption.parameters.max === 'number' && assumption.parameters.min >= assumption.parameters.max) { errors.push({ field: `${fieldPrefix}.parameters`, message: 'min must be less than max for uniform distribution', severity: 'error' }); } break; case CREDLTypes_1.DistributionType.TRIANGULAR: if (typeof assumption.parameters.min !== 'number') { errors.push({ field: `${fieldPrefix}.parameters.min`, message: 'min is required for triangular distribution', severity: 'error' }); } if (typeof assumption.parameters.max !== 'number') { errors.push({ field: `${fieldPrefix}.parameters.max`, message: 'max is required for triangular distribution', severity: 'error' }); } if (typeof assumption.parameters.mode !== 'number') { errors.push({ field: `${fieldPrefix}.parameters.mode`, message: 'mode is required for triangular distribution', severity: 'error' }); } break; } } /** * Validate expression assumption */ static validateExpressionAssumption(assumption, fieldPrefix, errors, _opts) { if (typeof assumption.formula !== 'string' || assumption.formula.trim().length === 0) { errors.push({ field: `${fieldPrefix}.formula`, message: 'formula is required and must be a non-empty string for expression assumptions', severity: 'error' }); } } /** * Validate table assumption */ static validateTableAssumption(assumption, fieldPrefix, errors, _opts) { if (!Array.isArray(assumption.values)) { errors.push({ field: `${fieldPrefix}.values`, message: 'values is required and must be an array for table assumptions', severity: 'error' }); return; } if (assumption.values.length === 0) { errors.push({ field: `${fieldPrefix}.values`, message: 'values array cannot be empty for table assumptions', severity: 'error' }); return; } for (let i = 0; i < assumption.values.length; i++) { const value = assumption.values[i]; const valueFieldPrefix = `${fieldPrefix}.values[${i}]`; if (!value || typeof value !== 'object') { errors.push({ field: valueFieldPrefix, message: 'table value must be an object', severity: 'error' }); continue; } if (typeof value.name !== 'string' || value.name.trim().length === 0) { errors.push({ field: `${valueFieldPrefix}.name`, message: 'name is required and must be a non-empty string for table values', severity: 'error' }); } } } /** * Validate models block per CREDL Behavioral Specification */ static validateModels(models, errors, warnings, _opts) { if (!Array.isArray(models)) { errors.push({ field: 'models', message: 'models must be an array', severity: 'error' }); return; } if (models.length === 0) { errors.push({ field: 'models', message: 'at least one model is required', severity: 'error' }); return; } // Behavioral Specification 6.1: Exactly one model per CREDL file if (models.length > 1) { errors.push({ field: 'models'