// engine/spec-decomposer/analyzer.ts — Analyze spec content into work units (SPEC-157)
// Reads spec.md acceptance criteria and technical.md file lists, then groups
// criteria into independent work units suitable for parallel agent execution.
import { readFile } from 'node:fs/promises';
import { extractAcceptanceCriteria } from '../shared/ac-extractor.js';
// ---------------------------------------------------------------------------
// Criterion classification
// ---------------------------------------------------------------------------
/** Keywords that suggest architecture/planning work → opus. */
const OPUS_KEYWORDS = [
    'architect',
    'design',
    'schema',
    'interface',
    'type',
    'pattern',
    'structure',
    'strategy',
    'dependency',
    'integration',
];
/** Keywords that suggest read-only or search work → haiku. */
const HAIKU_KEYWORDS = [
    'list',
    'read',
    'search',
    'find',
    'query',
    'display',
    'show',
    'report',
    'summary',
    'status',
];
function classifyModel(text) {
    const lower = text.toLowerCase();
    if (OPUS_KEYWORDS.some((kw) => lower.includes(kw))) {
        return 'opus';
    }
    if (HAIKU_KEYWORDS.some((kw) => lower.includes(kw))) {
        return 'haiku';
    }
    return 'sonnet';
}
// ---------------------------------------------------------------------------
// File path extraction helpers
// ---------------------------------------------------------------------------
const FILE_PATTERN = /`((?:src|tests)\/[^`]+\.(?:ts|js|tsx|jsx))`/g;
const MD_FILE_PATTERN = /^\s*[-*]\s+`?((?:src|tests)\/[\w./-]+\.(?:ts|js|tsx|jsx))`?/gm;
const SECTION_PATTERN = /^#{2,4}\s+(.+)$/gm;
function extractFilesFromText(text) {
    const files = new Set();
    let match;
    const pattern1 = new RegExp(FILE_PATTERN.source, 'g');
    while ((match = pattern1.exec(text)) !== null) {
        if (match[1]) {
            files.add(match[1]);
        }
    }
    const pattern2 = new RegExp(MD_FILE_PATTERN.source, 'gm');
    while ((match = pattern2.exec(text)) !== null) {
        if (match[1]) {
            files.add(match[1]);
        }
    }
    return [...files];
}
// ---------------------------------------------------------------------------
// Spec content parsers
// ---------------------------------------------------------------------------
/** Extract acceptance criteria lines from spec.md content. */
export function extractCriteriaFromContent(content) {
    return extractAcceptanceCriteria(content);
}
/** Extract all file paths mentioned in technical.md content. */
export function extractFilesFromTechnical(content) {
    return extractFilesFromText(content);
}
// ---------------------------------------------------------------------------
// Work unit grouping heuristics
// ---------------------------------------------------------------------------
/**
 * Categories used to cluster criteria into work units.
 * Each entry has a label and keywords that identify it.
 */
const CATEGORY_RULES = [
    {
        id: 'types',
        title: 'Types and interfaces',
        keywords: ['type', 'interface', 'schema', 'model', 'struct'],
    },
    {
        id: 'engine',
        title: 'Engine / business logic',
        keywords: ['engine', 'logic', 'algorithm', 'process', 'compute', 'analyze', 'detect'],
    },
    {
        id: 'storage',
        title: 'Storage / persistence',
        keywords: ['storage', 'store', 'persist', 'save', 'load', 'database', 'file', 'read', 'write'],
    },
    {
        id: 'tool',
        title: 'MCP tool / handler',
        keywords: ['tool', 'handler', 'endpoint', 'api', 'register', 'input', 'output'],
    },
    {
        id: 'tests',
        title: 'Tests',
        keywords: ['test', 'spec', 'unit', 'coverage', 'mock', 'fixture', 'assert'],
    },
];
function categorize(criterion) {
    const lower = criterion.toLowerCase();
    for (const rule of CATEGORY_RULES) {
        if (rule.keywords.some((kw) => lower.includes(kw))) {
            return rule.id;
        }
    }
    return 'general';
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/** Read spec.md file and return its content, or empty string on error. */
async function safeReadFile(filePath) {
    try {
        return await readFile(filePath, 'utf-8');
        /* v8 ignore next 3 */
    }
    catch {
        return '';
    }
}
/**
 * Analyze spec content from disk and produce raw work units.
 * Groups acceptance criteria by category heuristics.
 */
export async function analyzeSpec(specPath, technicalPath, maxTasks) {
    const [specContent, techContent] = await Promise.all([
        safeReadFile(specPath),
        safeReadFile(technicalPath),
    ]);
    const criteria = extractCriteriaFromContent(specContent);
    const allFiles = extractFilesFromTechnical(techContent);
    return groupCriteriaIntoUnits(criteria, allFiles, maxTasks);
}
/**
 * Group criteria lines into work units, respecting maxTasks limit.
 * Exported for unit-testing without touching the filesystem.
 */
export function groupCriteriaIntoUnits(criteria, allFiles, maxTasks) {
    if (criteria.length === 0) {
        // Generate a single fallback unit so orchestration always has something to run
        return [
            {
                id: 'unit-general',
                title: 'General implementation',
                criteria: [],
                suggestedFiles: allFiles,
                recommendedModel: 'sonnet',
                estimatedHours: 4,
            },
        ];
    }
    // Bucket criteria by category
    const buckets = new Map();
    for (const criterion of criteria) {
        const cat = categorize(criterion);
        const existing = buckets.get(cat) ?? [];
        existing.push(criterion);
        buckets.set(cat, existing);
    }
    // Assign files by rough path matching per category
    const filesByCategory = buildFileCategoryMap(allFiles);
    const units = [];
    for (const entry of buckets.entries()) {
        const catId = entry[0];
        const catCriteria = entry[1];
        const categoryInfo = CATEGORY_RULES.find((r) => r.id === catId) ?? {
            id: catId,
            title: catId,
        };
        const suggestedFiles = filesByCategory.get(catId) ?? [];
        const combinedText = catCriteria.join(' ');
        const model = classifyModel(combinedText);
        const hours = Math.max(1, Math.ceil(catCriteria.length * 1.5));
        units.push({
            id: `unit-${catId}`,
            title: categoryInfo.title,
            criteria: catCriteria,
            suggestedFiles,
            recommendedModel: model,
            estimatedHours: hours,
        });
    }
    // If fewer than 2 units were produced, add uncategorized files to general
    if (units.length === 0) {
        units.push({
            id: 'unit-general',
            title: 'General implementation',
            criteria,
            suggestedFiles: allFiles,
            recommendedModel: 'sonnet',
            estimatedHours: Math.max(2, criteria.length * 1.5),
        });
    }
    // Enforce maxTasks by merging excess units into the last one
    return mergeExcessUnits(units, maxTasks);
}
/** Map file paths to category IDs based on path segments. */
function buildFileCategoryMap(files) {
    const map = new Map();
    for (const file of files) {
        const lower = file.toLowerCase();
        let assigned = false;
        for (const rule of CATEGORY_RULES) {
            if (rule.keywords.some((kw) => lower.includes(kw))) {
                const existing = map.get(rule.id) ?? [];
                existing.push(file);
                map.set(rule.id, existing);
                assigned = true;
                break;
            }
        }
        if (!assigned) {
            const existing = map.get('general') ?? [];
            existing.push(file);
            map.set('general', existing);
        }
    }
    return map;
}
/** Merge units beyond maxTasks into the last retained unit. */
function mergeExcessUnits(units, maxTasks) {
    if (units.length <= maxTasks) {
        return units;
    }
    const retained = units.slice(0, maxTasks - 1);
    const overflow = units.slice(maxTasks - 1);
    const merged = {
        id: 'unit-general',
        title: 'Combined implementation',
        criteria: overflow.flatMap((u) => u.criteria),
        suggestedFiles: [...new Set(overflow.flatMap((u) => u.suggestedFiles))],
        recommendedModel: 'sonnet',
        estimatedHours: overflow.reduce((acc, u) => acc + u.estimatedHours, 0),
    };
    return [...retained, merged];
}
/** Extract section headers from markdown content (used by file-mapper). */
export function extractSectionHeaders(content) {
    const headers = [];
    let match;
    const pattern = new RegExp(SECTION_PATTERN.source, 'gm');
    while ((match = pattern.exec(content)) !== null) {
        if (match[1]) {
            headers.push(match[1].trim());
        }
    }
    return headers;
}
//# sourceMappingURL=analyzer.js.map