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.
394 lines (393 loc) • 13.7 kB
JavaScript
/**
* Promotion Validator
*
* Validates skills in staging before promotion to production.
* Part of Task 1.2: Staging → Production Promotion Workflow
*
* Validation checks:
* - Content integrity (all required files exist)
* - Schema compliance (frontmatter valid)
* - Test results (test.sh passes - if exists)
* - No conflicts with existing production skills
*
* @example
* ```typescript
* const validation = await validateStagedSkill('.claude/skills/staging/auth-v2');
* if (!validation.success) {
* console.error('Validation failed:', validation.errors);
* }
* ```
*/ import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';
import { exec } from 'child_process';
import { createLogger } from '../lib/logging.js';
import { fileExists } from '../lib/file-operations.js';
import { parseFrontmatter } from './skill-validator.js';
const logger = createLogger('promotion-validator');
const fsReadFile = promisify(fs.readFile);
const fsStat = promisify(fs.stat);
const fsAccess = promisify(fs.access);
const execPromise = promisify(exec);
/**
* Validate a staged skill before promotion
*/ export async function validateStagedSkill(skillPath) {
const errors = [];
const warnings = [];
const checks = {
contentIntegrity: false,
schemaCompliance: false,
testsPassed: false,
noConflicts: false
};
try {
logger.info('Starting staged skill validation', {
skillPath
});
// Normalize path
const normalizedPath = path.resolve(skillPath);
// 1. Check if skill directory exists
if (!await fileExists(normalizedPath)) {
errors.push(`Skill directory does not exist: ${normalizedPath}`);
return {
success: false,
errors,
warnings,
checks,
validatedAt: new Date()
};
}
const stats = await fsStat(normalizedPath);
if (!stats.isDirectory()) {
errors.push(`Path is not a directory: ${normalizedPath}`);
return {
success: false,
errors,
warnings,
checks,
validatedAt: new Date()
};
}
// 2. Content integrity check
logger.debug('Checking content integrity', {
skillPath
});
const contentCheck = await checkContentIntegrity(normalizedPath);
checks.contentIntegrity = contentCheck.success;
if (!contentCheck.success) {
errors.push(...contentCheck.errors || []);
}
if (contentCheck.warnings) {
warnings.push(...contentCheck.warnings);
}
// 3. Schema compliance check
logger.debug('Checking schema compliance', {
skillPath
});
const schemaCheck = await checkSchemaCompliance(normalizedPath);
checks.schemaCompliance = schemaCheck.success;
if (!schemaCheck.success) {
errors.push(...schemaCheck.errors || []);
}
if (schemaCheck.warnings) {
warnings.push(...schemaCheck.warnings);
}
// 4. Test execution check (if test.sh exists)
logger.debug('Checking tests', {
skillPath
});
const testCheck = await checkTests(normalizedPath);
checks.testsPassed = testCheck.success;
if (!testCheck.success && testCheck.errors) {
// Test failures are warnings, not hard errors (unless critical)
if (testCheck.critical) {
errors.push(...testCheck.errors);
} else {
warnings.push(...testCheck.errors);
}
}
if (testCheck.warnings) {
warnings.push(...testCheck.warnings);
}
// 5. Conflict check (production skill doesn't exist or is compatible)
logger.debug('Checking for conflicts', {
skillPath
});
const conflictCheck = await checkConflicts(normalizedPath);
checks.noConflicts = conflictCheck.success;
if (!conflictCheck.success) {
errors.push(...conflictCheck.errors || []);
}
if (conflictCheck.warnings) {
warnings.push(...conflictCheck.warnings);
}
// Determine overall success
const success = errors.length === 0;
if (success) {
logger.info('Validation passed', {
skillPath,
warnings: warnings.length
});
} else {
logger.error('Validation failed', {
skillPath,
errors: errors.length,
warnings: warnings.length
});
}
return {
success,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined,
checks,
validatedAt: new Date()
};
} catch (error) {
logger.error('Validation error', {
error,
skillPath
});
return {
success: false,
errors: [
`Validation error: ${error instanceof Error ? error.message : String(error)}`
],
warnings,
checks,
validatedAt: new Date()
};
}
}
/**
* Check content integrity (all required files exist)
*/ async function checkContentIntegrity(skillPath) {
const errors = [];
const warnings = [];
try {
// Required files
const requiredFiles = [
'SKILL.md',
'execute.sh'
];
for (const file of requiredFiles){
const filePath = path.join(skillPath, file);
if (!await fileExists(filePath)) {
errors.push(`Missing required file: ${file}`);
} else {
// Check file is readable
try {
await fsAccess(filePath, fs.constants.R_OK);
} catch {
errors.push(`File not readable: ${file}`);
}
}
}
// Optional files (warn if missing)
const optionalFiles = [
'test.sh',
'README.md'
];
for (const file of optionalFiles){
const filePath = path.join(skillPath, file);
if (!await fileExists(filePath)) {
warnings.push(`Optional file missing: ${file}`);
}
}
// Check execute.sh is executable
const executeScript = path.join(skillPath, 'execute.sh');
if (await fileExists(executeScript)) {
try {
const stats = await fsStat(executeScript);
const isExecutable = (stats.mode & fs.constants.S_IXUSR) !== 0;
if (!isExecutable) {
errors.push('execute.sh is not executable (chmod +x required)');
}
} catch (error) {
warnings.push(`Could not check execute.sh permissions: ${error}`);
}
}
return {
success: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined
};
} catch (error) {
return {
success: false,
errors: [
`Content integrity check failed: ${error instanceof Error ? error.message : String(error)}`
]
};
}
}
/**
* Check schema compliance (frontmatter valid)
*/ async function checkSchemaCompliance(skillPath) {
const errors = [];
const warnings = [];
try {
const skillMdPath = path.join(skillPath, 'SKILL.md');
if (!await fileExists(skillMdPath)) {
errors.push('SKILL.md does not exist');
return {
success: false,
errors
};
}
// Parse frontmatter
const frontmatter = parseFrontmatter(skillPath);
// Required fields
const requiredFields = [
'name',
'description',
'version'
];
for (const field of requiredFields){
if (!frontmatter[field]) {
errors.push(`Missing required frontmatter field: ${field}`);
}
}
// Validate version format (semantic versioning)
if (frontmatter.version) {
const versionRegex = /^\d+\.\d+\.\d+$/;
if (!versionRegex.test(frontmatter.version)) {
errors.push(`Invalid version format: ${frontmatter.version} (expected: X.Y.Z)`);
}
}
// Optional fields (warn if missing)
const optionalFields = [
'author',
'tags',
'dependencies'
];
for (const field of optionalFields){
if (!frontmatter[field]) {
warnings.push(`Optional frontmatter field missing: ${field}`);
}
}
// Validate tags (should be array)
if (frontmatter.tags && !Array.isArray(frontmatter.tags)) {
warnings.push('Frontmatter tags should be an array');
}
return {
success: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined
};
} catch (error) {
return {
success: false,
errors: [
`Schema compliance check failed: ${error instanceof Error ? error.message : String(error)}`
]
};
}
}
/**
* Check test execution (run test.sh if exists)
*/ async function checkTests(skillPath) {
const errors = [];
const warnings = [];
try {
const testScript = path.join(skillPath, 'test.sh');
// If test.sh doesn't exist, pass (tests are optional)
if (!await fileExists(testScript)) {
warnings.push('No test.sh found (tests are optional)');
return {
success: true,
warnings
};
}
// Check if test.sh is executable
const stats = await fsStat(testScript);
const isExecutable = (stats.mode & fs.constants.S_IXUSR) !== 0;
if (!isExecutable) {
errors.push('test.sh exists but is not executable');
return {
success: false,
errors,
critical: true
};
}
// Run tests with 30-second timeout
logger.debug('Running tests', {
testScript
});
try {
const { stdout, stderr } = await execPromise(`cd ${skillPath} && ./test.sh`, {
timeout: 30000,
maxBuffer: 1024 * 1024
});
logger.debug('Tests passed', {
stdout,
stderr
});
return {
success: true
};
} catch (execError) {
const error = execError;
// Test failed
const errorMsg = `Tests failed (exit code ${error.code || 'unknown'}): ${error.stderr || error.stdout || 'No output'}`;
errors.push(errorMsg);
logger.warn('Tests failed', {
error: errorMsg
});
// Test failures are non-critical warnings (allow promotion with --force)
return {
success: false,
errors,
critical: false
};
}
} catch (error) {
return {
success: false,
errors: [
`Test check failed: ${error instanceof Error ? error.message : String(error)}`
],
critical: false
};
}
}
/**
* Check for conflicts with existing production skills
*/ async function checkConflicts(skillPath) {
const errors = [];
const warnings = [];
try {
const skillName = path.basename(skillPath);
const productionPath = path.join('.claude/skills', skillName);
// Check if production skill already exists
if (await fileExists(productionPath)) {
warnings.push(`Production skill already exists: ${skillName} (use --overwrite to replace)`);
// Check if production skill has a different version
try {
const stagingFrontmatter = parseFrontmatter(skillPath);
const productionFrontmatter = parseFrontmatter(productionPath);
const stagingVersion = stagingFrontmatter.version;
const productionVersion = productionFrontmatter.version;
if (stagingVersion === productionVersion) {
warnings.push(`Both staging and production have version ${stagingVersion}`);
} else {
warnings.push(`Version mismatch: staging=${stagingVersion}, production=${productionVersion}`);
}
} catch (error) {
warnings.push('Could not compare versions (frontmatter parse error)');
}
}
return {
success: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined
};
} catch (error) {
return {
success: false,
errors: [
`Conflict check failed: ${error instanceof Error ? error.message : String(error)}`
]
};
}
}
//# sourceMappingURL=promotion-validator.js.map