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
JavaScript
/**
* 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