UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

555 lines (478 loc) 16.5 kB
const fs = require('fs').promises; const path = require('path'); const yaml = require('js-yaml'); const { validateYAML } = require('./yaml-validator'); const DependencyAnalyzer = require('./dependency-analyzer'); const SecurityChecker = require('./security-checker'); /** * Validates component modifications before applying them */ class ModificationValidator { constructor() { this.dependencyAnalyzer = new DependencyAnalyzer(); this.securityChecker = new SecurityChecker(); this.validationRules = { agent: this.validateAgentModification.bind(this), task: this.validateTaskModification.bind(this), workflow: this.validateWorkflowModification.bind(this), template: this.validateTemplateModification.bind(this) }; } /** * Validate a component modification * @param {string} componentType - Type of component (agent, task, workflow, etc.) * @param {string} originalContent - Original component content * @param {string} modifiedContent - Modified component content * @param {Object} options - Validation options * @returns {Object} Validation result with errors and warnings */ async validateModification(_componentType, originalContent, modifiedContent, options = {}) { const result = { valid: true, errors: [], warnings: [], suggestions: [], breakingChanges: [] }; // Basic validation if (!originalContent || !modifiedContent) { result.valid = false; result.errors.push('Original or modified content is empty'); return result; } // Run type-specific validation if (this.validationRules[componentType]) { const typeResult = await this.validationRules[componentType]( originalContent, modifiedContent, options ); this.mergeResults(result, typeResult); } else { result.warnings.push(`No specific validation rules for component type: ${componentType}`); } // Run security validation const securityResult = await this.validateSecurity(modifiedContent, componentType); this.mergeResults(result, securityResult); // Check for breaking changes const breakingChanges = await this.detectBreakingChanges( _componentType, originalContent, modifiedContent ); result.breakingChanges = breakingChanges; result.valid = result.errors.length === 0; return result; } /** * Validate agent modifications * @private */ async validateAgentModification(originalContent, modifiedContent, options) { const result = { valid: true, errors: [], warnings: [], suggestions: [] }; try { // Parse agent content const originalParts = this.parseAgentContent(originalContent); const modifiedParts = this.parseAgentContent(modifiedContent); // Validate YAML structure const yamlValidation = validateYAML(modifiedParts.yaml); if (!yamlValidation.valid) { result.valid = false; result.errors.push(`YAML validation failed: ${yamlValidation.error}`); return result; } const originalMeta = yaml.load(originalParts.yaml); const modifiedMeta = yaml.load(modifiedParts.yaml); // Check required fields const requiredFields = ['name', 'id', 'title', 'icon', 'whenToUse']; for (const field of requiredFields) { if (!modifiedMeta[field]) { result.errors.push(`Required field missing: ${field}`); } } // Validate dependencies if (modifiedMeta.dependencies) { const depValidation = await this.validateDependencies(modifiedMeta.dependencies); this.mergeResults(result, depValidation); } // Check command structure if (modifiedMeta.commands) { for (const [cmd, desc] of Object.entries(modifiedMeta.commands)) { if (!desc || typeof desc !== 'string') { result.errors.push(`Invalid command description for: ${cmd}`); } } } // Check for removed commands (breaking change) if (originalMeta.commands && modifiedMeta.commands) { const removedCommands = Object.keys(originalMeta.commands) .filter(cmd => !modifiedMeta.commands[cmd]); if (removedCommands.length > 0) { result.warnings.push(`Commands removed: ${removedCommands.join(', ')}`); } } // Validate markdown content const markdownValidation = this.validateMarkdown(modifiedParts.markdown); this.mergeResults(result, markdownValidation); } catch (_error) { result.valid = false; result.errors.push(`Failed to parse agent content: ${error.message}`); } return result; } /** * Validate task modifications * @private */ async validateTaskModification(originalContent, modifiedContent, options) { const result = { valid: true, errors: [], warnings: [], suggestions: [] }; // Check for required sections const requiredSections = ['## Purpose', '## Task Execution']; for (const section of requiredSections) { if (!modifiedContent.includes(section)) { result.errors.push(`Required section missing: ${section}`); } } // Validate elicitation blocks const elicitationBlocks = modifiedContent.match(/\[\[LLM:([\s\S]*?)\]\]/g) || []; for (const block of elicitationBlocks) { if (!block.includes(']]')) { result.errors.push('Unclosed elicitation block found'); } } // Check for task flow consistency const taskSteps = modifiedContent.match(/###\s+\d+\.\s+/g) || []; const expectedSteps = taskSteps.length; for (let i = 1; i <= expectedSteps; i++) { if (!modifiedContent.includes(`### ${i}.`)) { result.warnings.push(`Task step ${i} appears to be missing or misnumbered`); } } // Validate output format if specified const outputMatch = modifiedContent.match(/## Output Format[\s\S]*?```([\s\S]*?)```/); if (outputMatch) { const outputFormat = outputMatch[1].trim(); if (outputFormat.startsWith('json')) { try { // Extract JSON and validate const jsonContent = outputFormat.replace(/^json\s*/, ''); JSON.parse(jsonContent); } catch (_error) { result.warnings.push('Output format contains invalid JSON example'); } } } return result; } /** * Validate workflow modifications * @private */ async validateWorkflowModification(originalContent, modifiedContent, options) { const result = { valid: true, errors: [], warnings: [], suggestions: [] }; try { const _originalWorkflow = yaml.load(originalContent); const modifiedWorkflow = yaml.load(modifiedContent); // Validate YAML structure const yamlValidation = validateYAML(modifiedContent); if (!yamlValidation.valid) { result.valid = false; result.errors.push(`YAML validation failed: ${yamlValidation.error}`); return result; } // Check required fields if (!modifiedWorkflow.name) { result.errors.push('Workflow name is required'); } if (!modifiedWorkflow.phases || Object.keys(modifiedWorkflow.phases).length === 0) { result.errors.push('Workflow must have at least one phase'); } // Validate phase structure const phaseSequences = []; for (const [phaseName, phase] of Object.entries(modifiedWorkflow.phases || {})) { if (!phase.sequence) { result.errors.push(`Phase '${phaseName}' missing sequence number`); } else { phaseSequences.push(phase.sequence); } if (!phase.agents || phase.agents.length === 0) { result.errors.push(`Phase '${phaseName}' must have at least one agent`); } // Validate agent references for (const agent of phase.agents || []) { const agentExists = await this.checkAgentExists(agent); if (!agentExists) { result.warnings.push(`Agent '${agent}' referenced in phase '${phaseName}' not found`); } } } // Check for sequence gaps phaseSequences.sort((a, b) => a - b); for (let i = 1; i < phaseSequences.length; i++) { if (phaseSequences[i] - phaseSequences[i-1] > 2) { result.warnings.push('Large gap in phase sequences detected'); } } // Validate entry/exit criteria references for (const [phaseName, phase] of Object.entries(modifiedWorkflow.phases || {})) { if (phase.entry_criteria) { for (const criteria of phase.entry_criteria) { // Check if criteria references valid artifacts or phases const referencesValid = this.validateCriteriaReferences( criteria, modifiedWorkflow ); if (!referencesValid) { result.warnings.push( `Entry criteria '${criteria}' in phase '${phaseName}' may reference non-existent artifact` ); } } } } } catch (_error) { result.valid = false; result.errors.push(`Failed to parse workflow: ${error.message}`); } return result; } /** * Validate template modifications * @private */ async validateTemplateModification(originalContent, modifiedContent, options) { const result = { valid: true, errors: [], warnings: [], suggestions: [] }; // Check for placeholder consistency const placeholders = modifiedContent.match(/\{\{[^}]+\}\}/g) || []; const uniquePlaceholders = [...new Set(placeholders)]; // Warn about unreplaced placeholders if (uniquePlaceholders.length > 0) { result.suggestions.push( `Template contains ${uniquePlaceholders.length} placeholders: ${uniquePlaceholders.join(', ')}` ); } // Validate LLM instruction blocks const llmBlocks = modifiedContent.match(/\[\[LLM:([\s\S]*?)\]\]/g) || []; for (const block of llmBlocks) { if (block.length > 1000) { result.warnings.push('LLM instruction block exceeds recommended length (1000 chars)'); } } return result; } /** * Validate dependencies exist * @private */ async validateDependencies(dependencies) { const result = { valid: true, errors: [], warnings: [] }; const baseDir = path.join(process.cwd(), 'aios-core'); for (const [type, files] of Object.entries(dependencies)) { if (!Array.isArray(files)) continue; const typeDir = path.join(baseDir, type); for (const file of files) { const filePath = path.join(typeDir, file); try { await fs.access(filePath); } catch (_error) { result.warnings.push(`Dependency not found: ${type}/${file}`); } } } return result; } /** * Validate markdown content * @private */ validateMarkdown(content) { const result = { valid: true, errors: [], warnings: [] }; // Check for broken internal links const internalLinks = content.match(/\[([^\]]+)\]\(#[^)]+\)/g) || []; for (const link of internalLinks) { const anchor = link.match(/#([^)]+)/)[1]; const headingRegex = new RegExp(`^#+.*${anchor}`, 'mi'); if (!headingRegex.test(content)) { result.warnings.push(`Broken internal link: ${link}`); } } // Check for code block closure const codeBlocks = content.split('```'); if (codeBlocks.length % 2 === 0) { result.errors.push('Unclosed code block detected'); } return result; } /** * Validate security concerns * @private */ async validateSecurity(content, componentType) { const result = { valid: true, errors: [], warnings: [] }; const securityIssues = await this.securityChecker.checkContent(content); if (securityIssues.length > 0) { for (const issue of securityIssues) { if (issue.severity === 'high') { result.errors.push(`Security issue: ${issue.message}`); } else { result.warnings.push(`Security concern: ${issue.message}`); } } } return result; } /** * Detect breaking changes * @private */ async detectBreakingChanges(_componentType, originalContent, modifiedContent) { const breakingChanges = []; switch (_componentType) { case 'agent': // Check for removed commands try { const originalParts = this.parseAgentContent(originalContent); const modifiedParts = this.parseAgentContent(modifiedContent); const originalMeta = yaml.load(originalParts.yaml); const modifiedMeta = yaml.load(modifiedParts.yaml); if (originalMeta.commands && modifiedMeta.commands) { const removedCommands = Object.keys(originalMeta.commands) .filter(cmd => !modifiedMeta.commands[cmd]); if (removedCommands.length > 0) { breakingChanges.push({ type: 'removed_commands', items: removedCommands, impact: 'Users relying on these commands will need to update their workflows' }); } } } catch (_error) { // Ignore parsing errors here } break; case 'task': // Check for changed output format const originalOutput = originalContent.match(/## Output Format[\s\S]*?```[\s\S]*?```/); const modifiedOutput = modifiedContent.match(/## Output Format[\s\S]*?```[\s\S]*?```/); if (originalOutput && modifiedOutput && originalOutput[0] !== modifiedOutput[0]) { breakingChanges.push({ type: 'output_format_changed', impact: 'Components consuming this task output may need updates' }); } break; case 'workflow': // Check for removed phases try { const _originalWorkflow = yaml.load(originalContent); const modifiedWorkflow = yaml.load(modifiedContent); const originalPhases = Object.keys(originalWorkflow.phases || {}); const modifiedPhases = Object.keys(modifiedWorkflow.phases || {}); const removedPhases = originalPhases.filter(p => !modifiedPhases.includes(p)); if (removedPhases.length > 0) { breakingChanges.push({ type: 'removed_phases', items: removedPhases, impact: 'Projects using this workflow may fail at removed phases' }); } } catch (_error) { // Ignore parsing errors here } break; } return breakingChanges; } /** * Parse agent content into YAML and markdown sections * @private */ parseAgentContent(content) { const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!match) { throw new Error('Invalid agent content format'); } return { yaml: match[1], markdown: match[2] }; } /** * Check if an agent exists * @private */ async checkAgentExists(agentName) { const agentPath = path.join(process.cwd(), 'aios-core', 'agents', `${agentName}.md`); try { await fs.access(agentPath); return true; } catch { return false; } } /** * Validate criteria references * @private */ validateCriteriaReferences(criteria, workflow) { // Simple check - could be enhanced const artifacts = new Set(); for (const phase of Object.values(workflow.phases || {})) { if (phase.artifacts) { phase.artifacts.forEach(a => artifacts.add(a)); } } // Check if criteria mentions any known artifact for (const artifact of artifacts) { if (criteria.toLowerCase().includes(artifact.toLowerCase())) { return true; } } return false; } /** * Merge validation results * @private */ mergeResults(target, source) { target.errors.push(...(source.errors || [])); target.warnings.push(...(source.warnings || [])); target.suggestions.push(...(source.suggestions || [])); if (source.valid === false) { target.valid = false; } } } module.exports = ModificationValidator;