// engine/spec-registry/validator.ts — AC-4: Pre-publish Validation
// Validates a spec directory before publishing to the registry.
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
/** Maximum total size of all spec files combined (500 KB). */
const MAX_TOTAL_SIZE = 500 * 1024;
/** Semver regex (simplified: major.minor.patch with optional pre-release). */
const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
/** Required files in a publishable spec directory. */
const REQUIRED_FILES = ['spec.md'];
/** Required fields in the planu-registry.json manifest. */
const REQUIRED_MANIFEST_FIELDS = [
    'name',
    'org',
    'version',
    'description',
    'category',
    'stack',
    'difficulty',
    'estimatedHours',
    'tags',
];
/** Required frontmatter fields in spec.md. */
const REQUIRED_FRONTMATTER_FIELDS = ['id', 'title', 'type', 'status'];
/** Simple frontmatter regex: extracts YAML block between --- delimiters. */
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
/**
 * Validate a spec directory for publishing to the registry.
 * Checks file existence, frontmatter fields, manifest structure, semver format, and size limits.
 * @param specDir - Absolute path to the spec directory to validate.
 * @returns Validation result with detailed error messages for each failure.
 */
export async function validateForPublish(specDir) {
    const errors = [];
    // Check required files exist
    for (const file of REQUIRED_FILES) {
        const exists = await fileExists(path.join(specDir, file));
        if (!exists) {
            errors.push(`Missing required file: ${file}`);
        }
    }
    // Validate spec.md frontmatter
    await validateFrontmatter(specDir, errors);
    // Validate planu-registry.json manifest
    await validateManifest(specDir, errors);
    // Check total size
    await validateTotalSize(specDir, errors);
    return { valid: errors.length === 0, errors };
}
/** Validate that spec.md has required frontmatter fields. */
async function validateFrontmatter(specDir, errors) {
    const specPath = path.join(specDir, 'spec.md');
    let content;
    try {
        content = await fs.readFile(specPath, 'utf-8');
    }
    catch {
        // Already reported as missing file
        return;
    }
    const match = FRONTMATTER_RE.exec(content);
    if (!match?.[1]) {
        errors.push('spec.md is missing YAML frontmatter (expected --- delimiters)');
        return;
    }
    const frontmatter = match[1];
    for (const field of REQUIRED_FRONTMATTER_FIELDS) {
        const fieldRegex = new RegExp(`^${field}:\\s*.+`, 'm');
        if (!fieldRegex.test(frontmatter)) {
            errors.push(`spec.md frontmatter missing required field: ${field}`);
        }
    }
}
/** Validate planu-registry.json manifest existence and required fields. */
async function validateManifest(specDir, errors) {
    const manifestPath = path.join(specDir, 'planu-registry.json');
    let raw;
    try {
        raw = await fs.readFile(manifestPath, 'utf-8');
    }
    catch {
        errors.push('Missing required file: planu-registry.json');
        return;
    }
    let manifest;
    try {
        manifest = JSON.parse(raw);
    }
    catch {
        errors.push('planu-registry.json contains invalid JSON');
        return;
    }
    for (const field of REQUIRED_MANIFEST_FIELDS) {
        if (manifest[field] === undefined || manifest[field] === null || manifest[field] === '') {
            errors.push(`planu-registry.json missing required field: ${field}`);
        }
    }
    // Validate semver format
    if (typeof manifest.version === 'string' && !SEMVER_RE.test(manifest.version)) {
        errors.push(`Invalid version format in planu-registry.json: "${manifest.version}". Expected semver (e.g., 1.0.0)`);
    }
    // Validate stack is an array
    if (manifest.stack !== undefined && !Array.isArray(manifest.stack)) {
        errors.push('planu-registry.json "stack" must be an array of strings');
    }
    // Validate tags is an array
    if (manifest.tags !== undefined && !Array.isArray(manifest.tags)) {
        errors.push('planu-registry.json "tags" must be an array of strings');
    }
    // Validate difficulty range
    if (typeof manifest.difficulty === 'number') {
        if (manifest.difficulty < 1 || manifest.difficulty > 5) {
            errors.push('planu-registry.json "difficulty" must be between 1 and 5');
        }
    }
}
/** Check that total size of spec files does not exceed limit. */
async function validateTotalSize(specDir, errors) {
    const allFiles = [...REQUIRED_FILES, 'planu-registry.json'];
    let totalSize = 0;
    for (const file of allFiles) {
        try {
            const stat = await fs.stat(path.join(specDir, file));
            totalSize += stat.size;
        }
        catch {
            // File doesn't exist — already reported
        }
    }
    if (totalSize > MAX_TOTAL_SIZE) {
        errors.push(`Total spec size ${totalSize} bytes exceeds limit of ${MAX_TOTAL_SIZE} bytes (500 KB)`);
    }
}
/** Check if a file exists at the given path. */
async function fileExists(filePath) {
    try {
        await fs.access(filePath);
        return true;
    }
    catch {
        return false;
    }
}
//# sourceMappingURL=validator.js.map