UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

522 lines (521 loc) 19.8 kB
/** * Integration Schema Validator * * Task: P2-3.2 - JSON Schema Validation Enforcement * Enforces JSON Schema validation at all 47 integration points * * Features: * - Automatic validation at data boundaries * - Schema registry with versioning * - Migration support between schema versions * - Performance: <50ms validation, <100ms schema loading * - Comprehensive error reporting with StandardError * * @module integration-schema-validator */ import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import { StandardError, ErrorCode } from './errors.js'; import { getGlobalLogger } from './logging.js'; import fs from 'fs/promises'; import path from 'path'; const logger = getGlobalLogger(); // ============================================================================ // Integration Categories (6 categories, 47 points total) // ============================================================================ const INTEGRATION_CATEGORIES = [ 'database-handoffs', 'file-operations', 'cfn-loop-communication', 'phase4-workflow', 'api-layer', 'data-format-transformations' ]; // ============================================================================ // IntegrationSchemaValidator Class // ============================================================================ export class IntegrationSchemaValidator { config; ajv; schemaCache; migrations; initialized = false; constructor(config){ this.config = { schemaPath: config.schemaPath, enableCache: config.enableCache ?? true, strictMode: config.strictMode ?? true, maxCacheSize: config.maxCacheSize ?? 1000, schemaExtension: config.schemaExtension ?? '.schema.json' }; // Initialize Ajv with formats support this.ajv = new Ajv({ allErrors: true, strict: this.config.strictMode, validateFormats: true, verbose: true }); addFormats(this.ajv); // Add format validators (date-time, email, uri, etc.) this.schemaCache = new Map(); this.migrations = {}; logger.debug('IntegrationSchemaValidator initialized', { schemaPath: this.config.schemaPath, enableCache: this.config.enableCache, strictMode: this.config.strictMode }); } /** * Initialize validator by loading all schemas */ async initialize() { if (this.initialized) { return; } try { // Verify schema directory exists await fs.access(this.config.schemaPath); // Load all schemas from directory const categories = await this.loadSchemaDirectory(); logger.info('Schema validator initialized', { categories: categories.length, schemasLoaded: this.schemaCache.size }); this.initialized = true; } catch (error) { logger.error('Failed to initialize schema validator', error, { schemaPath: this.config.schemaPath }); throw new StandardError(ErrorCode.CONFIGURATION_ERROR, `Failed to initialize schema validator: ${error.message}`, { schemaPath: this.config.schemaPath }, error); } } /** * Shutdown validator and clear caches */ async shutdown() { this.schemaCache.clear(); this.migrations = {}; this.initialized = false; logger.debug('Schema validator shutdown complete'); } /** * Validate data against a schema * * @param data - Data to validate * @param schemaId - Schema identifier (e.g., "database-handoffs/pattern-deployment") * @param version - Schema version (defaults to latest) * @throws StandardError with VALIDATION_FAILED code if validation fails */ async validate(data, schemaId, version) { this.ensureInitialized(); const startTime = Date.now(); try { // Get schema validator const schema = await this.getSchema(schemaId, version); // Perform validation const valid = schema.validator(data); if (!valid) { const errors = this.formatErrors(schema.validator.errors || []); logger.warn('Schema validation failed', { schemaId, version: schema.version, errorCount: errors.length, duration: Date.now() - startTime }); throw new StandardError(ErrorCode.VALIDATION_FAILED, `Schema validation failed for ${schemaId}@${schema.version}`, { schemaId, version: schema.version, errors, suggestions: this.generateSuggestions(errors, data) }); } logger.debug('Schema validation succeeded', { schemaId, version: schema.version, duration: Date.now() - startTime }); } catch (error) { if (error instanceof StandardError) { throw error; } throw new StandardError(ErrorCode.VALIDATION_FAILED, `Validation error: ${error.message}`, { schemaId, version }, error); } } /** * Validate batch of records */ async validateBatch(records, schemaId, version, options = {}) { this.ensureInitialized(); const result = { valid: true, totalRecords: records.length, validRecords: 0, invalidRecords: 0, errors: [] }; for(let i = 0; i < records.length; i++){ try { await this.validate(records[i], schemaId, version); result.validRecords++; } catch (error) { result.invalidRecords++; result.valid = false; if (error instanceof StandardError) { result.errors.push({ index: i, errors: error.context?.errors || [] }); } if (options.failFast) { break; } } } logger.info('Batch validation completed', { schemaId, version, total: result.totalRecords, valid: result.validRecords, invalid: result.invalidRecords }); return result; } /** * Migrate data from one schema version to another */ async migrate(data, schemaId, fromVersion, toVersion) { this.ensureInitialized(); const transition = `${fromVersion}->${toVersion}`; const migrationFn = this.migrations[schemaId]?.[transition]; if (!migrationFn) { // Check if migration is needed (same version) if (fromVersion === toVersion) { return data; } // No migration function registered throw new StandardError(ErrorCode.CONFIGURATION_ERROR, `No migration available for ${schemaId} from ${fromVersion} to ${toVersion}`, { schemaId, fromVersion, toVersion, transition }); } try { const migrated = await migrationFn(data, fromVersion, toVersion); logger.info('Schema migration completed', { schemaId, fromVersion, toVersion }); return migrated; } catch (error) { throw new StandardError(ErrorCode.VALIDATION_FAILED, `Migration failed: ${error.message}`, { schemaId, fromVersion, toVersion }, error); } } /** * Register a migration function */ registerMigration(schemaId, fromVersion, toVersion, migrationFn) { const transition = `${fromVersion}->${toVersion}`; if (!this.migrations[schemaId]) { this.migrations[schemaId] = {}; } this.migrations[schemaId][transition] = migrationFn; logger.debug('Migration registered', { schemaId, transition }); } /** * Get schema metadata */ async getSchema(schemaId, version) { const cacheKey = `${schemaId}@${version || 'latest'}`; // Check cache if (this.config.enableCache && this.schemaCache.has(cacheKey)) { return this.schemaCache.get(cacheKey); } // Load schema from file const schema = await this.loadSchema(schemaId, version); // Cache schema if (this.config.enableCache) { this.schemaCache.set(cacheKey, schema); // Enforce cache size limit if (this.schemaCache.size > this.config.maxCacheSize) { const firstKey = this.schemaCache.keys().next().value; this.schemaCache.delete(firstKey); } } return schema; } /** * Check if schema exists */ async hasSchema(schemaId, version) { try { await this.getSchema(schemaId, version); return true; } catch { return false; } } /** * List all available schemas */ async listSchemas(category) { this.ensureInitialized(); const schemas = Array.from(this.schemaCache.keys()).map((key)=>key.split('@')[0]) // Remove version .filter((id, index, self)=>self.indexOf(id) === index); // Unique if (category) { return schemas.filter((id)=>id.startsWith(`${category}/`)); } return schemas; } /** * Get all available versions for a schema */ async getVersions(schemaId) { const versions = Array.from(this.schemaCache.keys()).filter((key)=>key.startsWith(`${schemaId}@`)).map((key)=>key.split('@')[1]).filter((v)=>v !== 'latest'); // Also check filesystem for versions not yet cached const schemaDir = path.join(this.config.schemaPath, schemaId); try { const files = await fs.readdir(schemaDir); const fileVersions = files.filter((f)=>f.endsWith(this.config.schemaExtension)).map((f)=>{ const match = f.match(/v([\d.]+)/); return match ? match[1] : null; }).filter((v)=>v !== null); // Merge and deduplicate const allVersions = [ ...new Set([ ...versions, ...fileVersions ]) ]; return allVersions.sort(); } catch { return versions; } } /** * Get integration categories */ async getCategories() { return [ ...INTEGRATION_CATEGORIES ]; } /** * Get validator configuration */ getConfig() { return { ...this.config }; } // ============================================================================ // Private Methods // ============================================================================ ensureInitialized() { if (!this.initialized) { throw new StandardError(ErrorCode.CONFIGURATION_ERROR, 'Schema validator not initialized. Call initialize() first.', { initialized: this.initialized }); } } async loadSchemaDirectory() { const loadedCategories = []; for (const category of INTEGRATION_CATEGORIES){ const categoryPath = path.join(this.config.schemaPath, category); try { await fs.access(categoryPath); await this.loadCategorySchemas(category); loadedCategories.push(category); } catch (error) { logger.warn(`Category directory not found: ${category}`, { categoryPath }); } } return loadedCategories; } async loadCategorySchemas(category) { const categoryPath = path.join(this.config.schemaPath, category); try { const entries = await fs.readdir(categoryPath, { withFileTypes: true }); for (const entry of entries){ if (entry.isDirectory()) { // Schema subdirectory (e.g., pattern-deployment/) const schemaName = entry.name; const schemaId = `${category}/${schemaName}`; await this.loadSchemaVersions(schemaId, path.join(categoryPath, schemaName)); } else if (entry.isFile() && entry.name.endsWith(this.config.schemaExtension)) { // Direct schema file const schemaName = entry.name.replace(this.config.schemaExtension, ''); const schemaId = `${category}/${schemaName}`; await this.loadSchemaFile(schemaId, path.join(categoryPath, entry.name), '1.0.0'); } } } catch (error) { logger.error(`Failed to load schemas for category: ${category}`, error, { categoryPath }); } } async loadSchemaVersions(schemaId, schemaDir) { try { const files = await fs.readdir(schemaDir); for (const file of files){ if (file.endsWith(this.config.schemaExtension)) { const versionMatch = file.match(/v([\d.]+)/); const version = versionMatch ? versionMatch[1] : '1.0.0'; const filePath = path.join(schemaDir, file); await this.loadSchemaFile(schemaId, filePath, version); } } } catch (error) { logger.error(`Failed to load schema versions: ${schemaId}`, error, { schemaDir }); } } async loadSchemaFile(schemaId, filePath, version) { try { // Check if already loaded in cache const cacheKey = `${schemaId}@${version}`; if (this.schemaCache.has(cacheKey)) { logger.debug('Schema already cached', { schemaId, version }); return; } const content = await fs.readFile(filePath, 'utf-8'); const schema = JSON.parse(content); // Check if schema with this $id already exists in Ajv const schemaKey = schema.$id || `${schemaId}/${version}`; if (this.ajv.getSchema(schemaKey)) { logger.debug('Schema already compiled in Ajv', { schemaId, version, schemaKey }); // Use existing compiled schema const validator = this.ajv.getSchema(schemaKey); const category = schemaId.split('/')[0]; const metadata = { id: schemaId, version, category, description: schema.description, validator }; this.schemaCache.set(cacheKey, metadata); return; } // Compile schema with Ajv const validator = this.ajv.compile(schema); // Extract category from schemaId const category = schemaId.split('/')[0]; const metadata = { id: schemaId, version, category, description: schema.description, validator }; // Cache with version this.schemaCache.set(cacheKey, metadata); logger.debug('Schema loaded', { schemaId, version, filePath }); } catch (error) { logger.error(`Failed to load schema file: ${filePath}`, error, { schemaId, version }); throw error; } } async loadSchema(schemaId, version) { const category = schemaId.split('/')[0]; const schemaName = schemaId.split('/').slice(1).join('/'); // Determine file path const categoryPath = path.join(this.config.schemaPath, category); const schemaPath = path.join(categoryPath, schemaName); // Try versioned schema directory try { const versionedFile = version ? `v${version}${this.config.schemaExtension}` : `v1.0.0${this.config.schemaExtension}`; const filePath = path.join(schemaPath, versionedFile); await fs.access(filePath); const actualVersion = version || '1.0.0'; await this.loadSchemaFile(schemaId, filePath, actualVersion); return this.schemaCache.get(`${schemaId}@${actualVersion}`); } catch { // Try direct schema file const directFile = path.join(categoryPath, `${schemaName}${this.config.schemaExtension}`); try { await fs.access(directFile); const actualVersion = version || '1.0.0'; await this.loadSchemaFile(schemaId, directFile, actualVersion); return this.schemaCache.get(`${schemaId}@${actualVersion}`); } catch (error) { throw new StandardError(ErrorCode.FILE_NOT_FOUND, `Schema not found: ${schemaId}${version ? `@${version}` : ''}`, { schemaId, version, searchPaths: [ schemaPath, directFile ] }, error); } } } formatErrors(errors) { return errors.map((error)=>({ path: error.instancePath || error.schemaPath, message: error.message || 'Validation failed', keyword: error.keyword, params: error.params })); } generateSuggestions(errors, data) { const suggestions = []; for (const error of errors){ if (error.keyword === 'required' && error.params?.missingProperty) { const missing = error.params.missingProperty; suggestions.push(missing); // Check for typos in data keys const dataKeys = Object.keys(data); const similar = dataKeys.filter((key)=>this.levenshteinDistance(key, missing) <= 2); if (similar.length > 0) { suggestions.push(`Did you mean: ${similar.join(', ')}?`); } } } return [ ...new Set(suggestions) ]; // Deduplicate } levenshteinDistance(a, b) { const matrix = []; for(let i = 0; i <= b.length; i++){ matrix[i] = [ i ]; } for(let j = 0; j <= a.length; j++){ matrix[0][j] = j; } for(let i = 1; i <= b.length; i++){ for(let j = 1; j <= a.length; j++){ if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 // deletion ); } } } return matrix[b.length][a.length]; } } //# sourceMappingURL=integration-schema-validator.js.map