UNPKG

al-development-collection

Version:

AI Native AL Development toolkit for Microsoft Dynamics 365 Business Central with GitHub Copilot integration

429 lines (369 loc) 12.5 kB
/** * Validation script for AL Development Collection * Validates collection manifest, file existence, and frontmatter compliance * * Usage: node validate-al-collection.js */ const fs = require('fs'); const path = require('path'); const yaml = require('js-yaml'); // ANSI color codes for terminal output const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' }; // Validation results let errors = []; let warnings = []; let successes = []; /** * Log message with color */ function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } /** * Add error */ function addError(message) { errors.push(message); log(`❌ ERROR: ${message}`, 'red'); } /** * Add warning */ function addWarning(message) { warnings.push(message); log(`⚠️ WARNING: ${message}`, 'yellow'); } /** * Add success */ function addSuccess(message) { successes.push(message); log(`✅ ${message}`, 'green'); } /** * Check if file exists */ function fileExists(filePath) { return fs.existsSync(filePath); } /** * Read YAML file */ function readYaml(filePath) { try { const fileContents = fs.readFileSync(filePath, 'utf8'); return yaml.load(fileContents); } catch (e) { addError(`Failed to parse YAML file ${filePath}: ${e.message}`); return null; } } /** * Read markdown frontmatter */ function readFrontmatter(filePath) { try { const content = fs.readFileSync(filePath, 'utf8'); // Support both Unix (\n) and Windows (\r\n) line endings const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!match) { return null; } return yaml.load(match[1]); } catch (e) { addWarning(`Failed to parse frontmatter in ${filePath}: ${e.message}`); return null; } } /** * Validate collection manifest structure */ function validateCollectionManifest(manifest, manifestPath) { log('\n📋 Validating Collection Manifest...', 'cyan'); // Required fields const requiredFields = ['id', 'name', 'description', 'items']; requiredFields.forEach(field => { if (!manifest[field]) { addError(`Collection manifest missing required field: ${field}`); } }); // Validate ID format (lowercase with hyphens) if (manifest.id && !/^[a-z0-9-]+$/.test(manifest.id)) { addError(`Collection ID must be lowercase with hyphens only: ${manifest.id}`); } else if (manifest.id) { addSuccess(`Collection ID is valid: ${manifest.id}`); } // Validate items array if (!Array.isArray(manifest.items)) { addError('Collection items must be an array'); } else { addSuccess(`Collection has ${manifest.items.length} items`); } // Validate tags (if present) if (manifest.tags && !Array.isArray(manifest.tags)) { addError('Collection tags must be an array'); } else if (manifest.tags) { addSuccess(`Collection has ${manifest.tags.length} tags`); } // Validate display settings if (manifest.display) { if (manifest.display.ordering && !['alpha', 'manual'].includes(manifest.display.ordering)) { addError(`Invalid display ordering: ${manifest.display.ordering} (must be 'alpha' or 'manual')`); } if (typeof manifest.display.show_badge !== 'undefined' && typeof manifest.display.show_badge !== 'boolean') { addError('Display show_badge must be a boolean'); } } } /** * Validate collection items (file existence and frontmatter) */ function validateCollectionItems(manifest) { log('\n📁 Validating Collection Items...', 'cyan'); const validKinds = ['instruction', 'prompt', 'agent']; const itemsByKind = { 'instruction': 0, 'prompt': 0, 'agent': 0 }; manifest.items.forEach((item, index) => { const itemNum = index + 1; // Check required fields if (!item.path) { addError(`Item ${itemNum}: Missing required field 'path'`); return; } if (!item.kind) { addError(`Item ${itemNum}: Missing required field 'kind'`); return; } // Validate kind if (!validKinds.includes(item.kind)) { addError(`Item ${itemNum}: Invalid kind '${item.kind}' (must be one of: ${validKinds.join(', ')})`); } else { itemsByKind[item.kind]++; } // Check file existence if (!fileExists(item.path)) { addError(`Item ${itemNum}: File not found: ${item.path}`); return; } // Check file extension matches kind const ext = path.extname(item.path); const basename = path.basename(item.path, ext); switch (item.kind) { case 'instruction': if (!item.path.endsWith('.instructions.md')) { addWarning(`Item ${itemNum}: Instruction file should end with .instructions.md: ${item.path}`); } break; case 'prompt': if (!item.path.endsWith('.prompt.md')) { addWarning(`Item ${itemNum}: Prompt file should end with .prompt.md: ${item.path}`); } break; case 'agent': if (!item.path.endsWith('.agent.md')) { addWarning(`Item ${itemNum}: Agent file should end with .agent.md: ${item.path}`); } break; } // Validate frontmatter const frontmatter = readFrontmatter(item.path); if (!frontmatter) { addWarning(`Item ${itemNum}: No frontmatter found in ${item.path}`); } else { // Check for description if (!frontmatter.description) { addWarning(`Item ${itemNum}: Missing description in frontmatter: ${item.path}`); } // Validate specific frontmatter by kind if (item.kind === 'instruction') { if (!frontmatter.applyTo && !frontmatter.globs) { addWarning(`Item ${itemNum}: Instruction missing 'applyTo' or 'globs' in frontmatter: ${item.path}`); } } if (item.kind === 'prompt') { // Support both 'mode' (legacy) and 'agent' (new GitHub Copilot convention) if (!frontmatter.mode && !frontmatter.agent) { addWarning(`Item ${itemNum}: Prompt missing 'mode' or 'agent' in frontmatter: ${item.path}`); } if (!frontmatter.tools) { addWarning(`Item ${itemNum}: Prompt missing 'tools' in frontmatter: ${item.path}`); } if (!frontmatter.model) { addWarning(`Item ${itemNum}: Prompt missing 'model' in frontmatter: ${item.path}`); } } if (item.kind === 'agent') { if (!frontmatter.tools) { addWarning(`Item ${itemNum}: Agent missing 'tools' in frontmatter: ${item.path}`); } if (!frontmatter.model) { addWarning(`Item ${itemNum}: Agent missing 'model' in frontmatter: ${item.path}`); } } addSuccess(`Item ${itemNum}: ${path.basename(item.path)} validated`); } // Validate agent usage field if (item.kind === 'agent') { if (item.usage && !['recommended', 'optional'].includes(item.usage)) { addWarning(`Item ${itemNum}: Agent usage should be 'recommended' or 'optional': ${item.usage}`); } } }); // Summary by kind log('\n📊 Items by Kind:', 'cyan'); Object.entries(itemsByKind).forEach(([kind, count]) => { log(` ${kind}: ${count}`, 'blue'); }); } /** * Validate file naming conventions */ function validateFileNaming(manifest) { log('\n🏷️ Validating File Naming Conventions...', 'cyan'); manifest.items.forEach((item, index) => { const filename = path.basename(item.path); // Check for lowercase with hyphens const nameWithoutExt = filename.replace(/\.(instructions|prompt|agent)\.md$/, ''); if (!/^[a-z0-9-]+$/.test(nameWithoutExt)) { addWarning(`Item ${index + 1}: Filename should be lowercase with hyphens: ${filename}`); } // Check for proper prefix (al- for this collection) if (!nameWithoutExt.startsWith('al-')) { addWarning(`Item ${index + 1}: Filename should start with 'al-' prefix: ${filename}`); } }); addSuccess('File naming conventions validated'); } /** * Validate collection documentation exists */ function validateDocumentation(manifest) { log('\n📖 Validating Documentation...', 'cyan'); const docFile = `${manifest.id}.md`; if (!fileExists(docFile)) { addError(`Collection documentation not found: ${docFile}`); } else { addSuccess(`Collection documentation exists: ${docFile}`); // Validate documentation content const docContent = fs.readFileSync(docFile, 'utf8'); // Check for required sections const requiredSections = [ '# ', // Title 'What\'s Included', 'Quick Start', 'Requirements' ]; requiredSections.forEach(section => { if (!docContent.includes(section)) { addWarning(`Documentation missing recommended section: ${section}`); } }); } } /** * Check for duplicate IDs or paths */ function validateUniqueness(manifest) { log('\n🔍 Validating Uniqueness...', 'cyan'); const paths = new Set(); let duplicates = 0; manifest.items.forEach((item, index) => { if (paths.has(item.path)) { addError(`Duplicate path found: ${item.path}`); duplicates++; } else { paths.add(item.path); } }); if (duplicates === 0) { addSuccess('No duplicate paths found'); } } /** * Validate directory structure */ function validateDirectoryStructure() { log('\n📂 Validating Directory Structure...', 'cyan'); const requiredDirs = [ 'instructions', 'prompts', 'agents', 'collections' ]; requiredDirs.forEach(dir => { if (!fs.existsSync(dir)) { addWarning(`Recommended directory not found: ${dir}`); } else { addSuccess(`Directory exists: ${dir}`); } }); } /** * Main validation function */ function validateCollection() { log('\n╔════════════════════════════════════════════════╗', 'cyan'); log('║ AL Development Collection Validator v1.0 ║', 'cyan'); log('╚════════════════════════════════════════════════╝', 'cyan'); log(`\n🕐 Started: ${new Date().toISOString()}\n`, 'blue'); const manifestPath = 'collections/al-development.collection.yml'; // Check if manifest exists if (!fileExists(manifestPath)) { addError(`Collection manifest not found: ${manifestPath}`); return printSummary(); } addSuccess(`Collection manifest found: ${manifestPath}`); // Load manifest const manifest = readYaml(manifestPath); if (!manifest) { return printSummary(); } // Run validations validateCollectionManifest(manifest, manifestPath); validateCollectionItems(manifest); validateFileNaming(manifest); validateDocumentation(manifest); validateUniqueness(manifest); validateDirectoryStructure(); // Print summary printSummary(); } /** * Print validation summary */ function printSummary() { log('\n' + '═'.repeat(50), 'cyan'); log('📊 VALIDATION SUMMARY', 'cyan'); log('═'.repeat(50), 'cyan'); log(`\n✅ Successes: ${successes.length}`, 'green'); log(`⚠️ Warnings: ${warnings.length}`, 'yellow'); log(`❌ Errors: ${errors.length}`, 'red'); if (errors.length === 0 && warnings.length === 0) { log('\n🎉 Collection is fully compliant and ready for contribution!', 'green'); log('═'.repeat(50) + '\n', 'cyan'); process.exit(0); } else if (errors.length === 0) { log('\n✅ Collection is valid but has some warnings to address.', 'yellow'); log('═'.repeat(50) + '\n', 'cyan'); process.exit(0); } else { log('\n❌ Collection has errors that must be fixed before contribution.', 'red'); log('═'.repeat(50) + '\n', 'cyan'); process.exit(1); } } // Run validation if executed directly if (require.main === module) { validateCollection(); } module.exports = { validateCollection };