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.
237 lines (236 loc) • 8.94 kB
JavaScript
/**
* Skill Frontmatter Parser
*
* Parses and validates YAML frontmatter in SKILL.md files
* Supports semantic versioning and comprehensive metadata tracking
*
* @module skill-frontmatter-parser
* @version 1.0.0
*/ import * as yaml from 'js-yaml';
import { StandardError } from './errors.js';
/**
* Frontmatter parser error
*/ export class FrontmatterParseError extends StandardError {
context;
constructor(message, context){
super('FRONTMATTER_PARSE_ERROR', message, context), this.context = context;
this.name = 'FrontmatterParseError';
}
}
/**
* Frontmatter validation error
*/ export class FrontmatterValidationError extends StandardError {
errors;
constructor(message, errors){
super('FRONTMATTER_VALIDATION_ERROR', message, {
errors
}), this.errors = errors;
this.name = 'FrontmatterValidationError';
}
}
/**
* Semantic version regex pattern
*/ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
/**
* Valid skill status values
*/ const VALID_STATUSES = [
'draft',
'approved',
'staging',
'deployed',
'deprecated'
];
/**
* Parse SKILL.md content and extract frontmatter
*
* @param content - Raw SKILL.md file content
* @returns Parsed document with frontmatter and content
* @throws FrontmatterParseError if parsing fails
*/ export function parseFrontmatter(content) {
// Extract frontmatter block
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
if (!frontmatterMatch) {
throw new FrontmatterParseError('No frontmatter block found. SKILL.md must start with YAML frontmatter enclosed in ---');
}
const [, frontmatterYaml, markdownContent] = frontmatterMatch;
// Parse YAML
let frontmatter;
try {
frontmatter = yaml.load(frontmatterYaml);
} catch (error) {
throw new FrontmatterParseError('Failed to parse YAML frontmatter', {
error: error instanceof Error ? error.message : String(error),
yaml: frontmatterYaml.substring(0, 200)
});
}
// Validate structure
if (!frontmatter || typeof frontmatter !== 'object') {
throw new FrontmatterParseError('Frontmatter must be a valid YAML object');
}
return {
frontmatter: frontmatter,
content: markdownContent.trim(),
raw: content
};
}
/**
* Validate frontmatter against schema
*
* @param frontmatter - Frontmatter object to validate
* @returns Validation result with errors and warnings
*/ export function validateFrontmatter(frontmatter) {
const errors = [];
const warnings = [];
// Required fields
if (!frontmatter.name || typeof frontmatter.name !== 'string') {
errors.push('Field "name" is required and must be a string');
}
if (!frontmatter.version || typeof frontmatter.version !== 'string') {
errors.push('Field "version" is required and must be a string');
} else if (!SEMVER_PATTERN.test(frontmatter.version)) {
errors.push(`Field "version" must be valid semantic version (e.g., 1.0.0), got: ${frontmatter.version}`);
}
if (!Array.isArray(frontmatter.tags)) {
errors.push('Field "tags" is required and must be an array');
} else if (frontmatter.tags.length === 0) {
warnings.push('Field "tags" is empty, consider adding tags for categorization');
} else if (!frontmatter.tags.every((tag)=>typeof tag === 'string')) {
errors.push('All tags must be strings');
}
if (!frontmatter.status) {
errors.push('Field "status" is required');
} else if (!VALID_STATUSES.includes(frontmatter.status)) {
errors.push(`Field "status" must be one of: ${VALID_STATUSES.join(', ')}, got: ${frontmatter.status}`);
}
if (!frontmatter.author || typeof frontmatter.author !== 'string') {
errors.push('Field "author" is required and must be a string');
}
if (!frontmatter.description || typeof frontmatter.description !== 'string') {
errors.push('Field "description" is required and must be a string');
} else if (frontmatter.description.length < 10) {
warnings.push('Field "description" is very short, consider adding more detail');
}
// Optional fields validation
if (frontmatter.dependencies !== undefined) {
if (!Array.isArray(frontmatter.dependencies)) {
errors.push('Field "dependencies" must be an array if provided');
} else if (!frontmatter.dependencies.every((dep)=>typeof dep === 'string')) {
errors.push('All dependencies must be strings');
}
}
if (frontmatter.created !== undefined && typeof frontmatter.created !== 'string') {
errors.push('Field "created" must be a string (ISO date) if provided');
}
if (frontmatter.updated !== undefined && typeof frontmatter.updated !== 'string') {
errors.push('Field "updated" must be a string (ISO date) if provided');
}
// Date validation
if (frontmatter.created) {
const createdDate = new Date(frontmatter.created);
if (isNaN(createdDate.getTime())) {
errors.push(`Field "created" is not a valid date: ${frontmatter.created}`);
}
}
if (frontmatter.updated) {
const updatedDate = new Date(frontmatter.updated);
if (isNaN(updatedDate.getTime())) {
errors.push(`Field "updated" is not a valid date: ${frontmatter.updated}`);
}
// Check if updated is after created
if (frontmatter.created) {
const createdDate = new Date(frontmatter.created);
const updatedDate = new Date(frontmatter.updated);
if (updatedDate < createdDate) {
errors.push('Field "updated" cannot be before "created"');
}
}
}
return {
valid: errors.length === 0,
errors,
warnings
};
}
/**
* Parse and validate SKILL.md content
*
* @param content - Raw SKILL.md file content
* @returns Parsed and validated document
* @throws FrontmatterValidationError if validation fails
*/ export function parseAndValidate(content) {
const parsed = parseFrontmatter(content);
const validation = validateFrontmatter(parsed.frontmatter);
if (!validation.valid) {
throw new FrontmatterValidationError('Frontmatter validation failed', validation.errors);
}
return parsed;
}
/**
* Serialize frontmatter to YAML string
*
* @param frontmatter - Frontmatter object to serialize
* @returns YAML string representation
*/ export function serializeFrontmatter(frontmatter) {
return yaml.dump(frontmatter, {
indent: 2,
lineWidth: 100,
noRefs: true,
sortKeys: false
});
}
/**
* Update frontmatter in SKILL.md content
*
* @param content - Original SKILL.md content
* @param updates - Partial frontmatter updates
* @returns Updated SKILL.md content
*/ export function updateFrontmatter(content, updates) {
const parsed = parseFrontmatter(content);
const updated = {
...parsed.frontmatter,
...updates
};
// Update timestamp
updated.updated = new Date().toISOString().split('T')[0];
const yamlString = serializeFrontmatter(updated);
return `---\n${yamlString}---\n${parsed.content}`;
}
/**
* Create new SKILL.md content with frontmatter
*
* @param frontmatter - Frontmatter object
* @param content - Markdown content
* @returns Complete SKILL.md content
*/ export function createSkillDocument(frontmatter, content) {
// Validate before creating
const validation = validateFrontmatter(frontmatter);
if (!validation.valid) {
throw new FrontmatterValidationError('Cannot create skill document with invalid frontmatter', validation.errors);
}
const yamlString = serializeFrontmatter(frontmatter);
return `---\n${yamlString}---\n${content}`;
}
/**
* Extract frontmatter summary for display
*
* @param frontmatter - Frontmatter object
* @returns Human-readable summary
*/ export function getFrontmatterSummary(frontmatter) {
return `${frontmatter.name} v${frontmatter.version} [${frontmatter.status}]`;
}
/**
* Compare two versions using semantic versioning
*
* @param version1 - First version string
* @param version2 - Second version string
* @returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2
*/ export function compareVersions(version1, version2) {
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);
for(let i = 0; i < 3; i++){
if (v1Parts[i] > v2Parts[i]) return 1;
if (v1Parts[i] < v2Parts[i]) return -1;
}
return 0;
}
//# sourceMappingURL=skill-frontmatter-parser.js.map