// Planu — Spec Splitter Engine
// Detects oversized specs and suggests atomic divisions
import { calculateEstimation, inferDifficulty } from './estimator.js';
// === Thresholds ===
const CRITERIA_THRESHOLD = 12;
const MODULE_THRESHOLD = 4;
const HOURS_THRESHOLD = 16;
// === Public API ===
/**
 * Analyze a spec and determine if it should be split into smaller specs.
 * Returns null if the spec is appropriately sized.
 */
export function analyzeSplit(input, specId) {
    // Trivial specs are atomic by definition
    if (input.scope === 'trivial') {
        return null;
    }
    const shouldSplit = detectOversizedSpec(input.criteriaCount, input.moduleCount, input.estimatedHours);
    if (!shouldSplit) {
        return null;
    }
    const reason = buildSplitReason(input.criteriaCount, input.moduleCount, input.estimatedHours);
    const subSpecs = generateSubSpecs(input);
    const dependencyOrder = subSpecs.map((_, i) => `${specId}-sub-${String(i + 1)}`);
    const totalEstimate = calculateTotalEstimate(subSpecs);
    return {
        originalSpecId: specId,
        reason,
        suggestedSpecs: subSpecs,
        dependencyOrder,
        totalEstimate,
    };
}
/**
 * Detect if a spec exceeds size thresholds.
 */
export function detectOversizedSpec(criteriaCount, moduleCount, estimatedHours) {
    return (criteriaCount > CRITERIA_THRESHOLD ||
        moduleCount > MODULE_THRESHOLD ||
        estimatedHours > HOURS_THRESHOLD);
}
/**
 * Format split suggestion for display based on experience level.
 */
export function formatSplitSuggestion(suggestion, level) {
    if (level === 'beginner') {
        return formatForBeginner(suggestion);
    }
    return formatForExpert(suggestion);
}
// === Internal ===
function buildSplitReason(criteria, modules, hours) {
    const reasons = [];
    if (criteria > CRITERIA_THRESHOLD) {
        reasons.push(`${String(criteria)} criteria (threshold: ${String(CRITERIA_THRESHOLD)})`);
    }
    if (modules > MODULE_THRESHOLD) {
        reasons.push(`${String(modules)} modules (threshold: ${String(MODULE_THRESHOLD)})`);
    }
    if (hours > HOURS_THRESHOLD) {
        reasons.push(`${String(hours)}h estimated (threshold: ${String(HOURS_THRESHOLD)}h)`);
    }
    return `Spec exceeds size thresholds: ${reasons.join(', ')}. Consider splitting into smaller, atomic specs.`;
}
function generateSubSpecs(input) {
    const subSpecs = [];
    // Strategy: split by target layer
    const targets = inferTargets(input.target, input.scope);
    for (const target of targets) {
        const criteriaShare = Math.ceil(input.criteriaCount / targets.length);
        const hoursShare = Math.round((input.estimatedHours / targets.length) * 10) / 10;
        const subScope = inferSubScope(input.scope);
        const dependsOn = [];
        // Shared specs should be done first
        if (target.target !== 'shared') {
            const sharedIdx = targets.findIndex((t) => t.target === 'shared');
            if (sharedIdx >= 0) {
                dependsOn.push(String(sharedIdx));
            }
        }
        // Database specs should be done before backend
        if (target.target === 'backend') {
            const dbIdx = targets.findIndex((t) => t.target === 'database');
            if (dbIdx >= 0) {
                dependsOn.push(String(dbIdx));
            }
        }
        subSpecs.push({
            suggestedTitle: `${target.label} — ${input.description.slice(0, 40)}`,
            type: input.type,
            target: target.target,
            scope: subScope,
            description: `${target.label} portion of the original spec.`,
            criteriaCount: criteriaShare,
            dependsOn,
            estimatedHours: hoursShare,
        });
    }
    return subSpecs;
}
function inferTargets(target, scope) {
    if (target === 'fullstack' && (scope === 'cross-module' || scope === 'architectural')) {
        return [
            { target: 'shared', label: 'Shared types and interfaces' },
            { target: 'database', label: 'Database schema and migrations' },
            { target: 'backend', label: 'Backend API and services' },
            { target: 'frontend', label: 'Frontend UI and integration' },
        ];
    }
    if (target === 'fullstack') {
        return [
            { target: 'backend', label: 'Backend implementation' },
            { target: 'frontend', label: 'Frontend implementation' },
        ];
    }
    // For non-fullstack, split by functionality
    return [
        { target, label: 'Core implementation' },
        { target, label: 'Tests and validation' },
    ];
}
function inferSubScope(scope) {
    const subScopeMap = {
        trivial: 'trivial',
        feature: 'feature',
        'cross-module': 'feature',
        architectural: 'cross-module',
    };
    return subScopeMap[scope];
}
function calculateTotalEstimate(subSpecs) {
    // Use first sub-spec's type/scope for the total estimate
    const firstSub = subSpecs[0];
    const type = firstSub?.type ?? 'feature';
    const scope = firstSub?.scope ?? 'feature';
    const difficulty = inferDifficulty(type, scope);
    return calculateEstimation(type, scope, difficulty);
}
function formatForBeginner(suggestion) {
    const lines = [];
    lines.push('## Why split this spec?');
    lines.push('');
    lines.push(suggestion.reason);
    lines.push('');
    lines.push('Splitting a large spec into smaller ones helps because:');
    lines.push('- Each sub-spec is easier to review and understand');
    lines.push('- You can implement and test each piece independently');
    lines.push('- If one part has issues, it does not block the others');
    lines.push('- Estimation is more accurate for smaller specs');
    lines.push('');
    lines.push('## Suggested sub-specs');
    lines.push('');
    for (const [i, sub] of suggestion.suggestedSpecs.entries()) {
        lines.push(`### ${String(i + 1)}. ${sub.suggestedTitle}`);
        lines.push('');
        lines.push(`- **Target:** ${sub.target}`);
        lines.push(`- **Scope:** ${sub.scope}`);
        lines.push(`- **Estimated hours:** ${String(sub.estimatedHours)}h`);
        lines.push(`- **Criteria:** ~${String(sub.criteriaCount)}`);
        if (sub.dependsOn.length > 0) {
            lines.push(`- **Depends on:** sub-spec(s) ${sub.dependsOn.join(', ')}`);
        }
        lines.push('');
    }
    lines.push('## What to do next');
    lines.push('');
    lines.push('1. Review the suggested sub-specs above');
    lines.push('2. Decide if you want to split or keep as one spec');
    lines.push('3. If splitting, create each sub-spec with `create_spec`');
    lines.push('4. Implement in dependency order (shared first, then dependent parts)');
    return lines.join('\n');
}
function formatForExpert(suggestion) {
    const lines = [];
    lines.push(`> ${suggestion.reason}`);
    lines.push('');
    lines.push('| # | Sub-spec | Target | Scope | Hours | Criteria | Depends On |');
    lines.push('|---|----------|--------|-------|-------|----------|------------|');
    for (const [i, sub] of suggestion.suggestedSpecs.entries()) {
        const deps = sub.dependsOn.length > 0 ? sub.dependsOn.join(', ') : '—';
        lines.push(`| ${String(i + 1)} | ${sub.suggestedTitle} | ${sub.target} | ${sub.scope} | ${String(sub.estimatedHours)}h | ${String(sub.criteriaCount)} | ${deps} |`);
    }
    lines.push('');
    lines.push('```mermaid');
    lines.push('graph LR');
    for (const [i, sub] of suggestion.suggestedSpecs.entries()) {
        const nodeId = `S${String(i + 1)}`;
        lines.push(`  ${nodeId}["${String(i + 1)}. ${sub.target}"]`);
    }
    for (const [i, sub] of suggestion.suggestedSpecs.entries()) {
        for (const dep of sub.dependsOn) {
            const depIdx = parseInt(dep, 10);
            if (!isNaN(depIdx)) {
                lines.push(`  S${String(depIdx + 1)} --> S${String(i + 1)}`);
            }
        }
    }
    lines.push('```');
    return lines.join('\n');
}
// === SPEC-289: analyze_spec_size and executeSplit ===
/** Words that signal subsystem boundaries in a description */
const SUBSYSTEM_SIGNAL_WORDS = [
    'and',
    'also',
    'additionally',
    'furthermore',
    'as well as',
    'plus',
    'moreover',
];
/** Vague words in ACs that indicate ambiguous criteria */
const VAGUE_WORDS = ['should handle', 'properly', 'efficiently', 'correctly', 'appropriately'];
/** Threshold: >15 ACs triggers split suggestion */
const AC_SPLIT_THRESHOLD = 15;
/** Threshold: >40h estimation triggers split suggestion */
const HOURS_SPLIT_THRESHOLD = 40;
/** Threshold: >3 subsystem signals triggers split suggestion */
const SUBSYSTEM_SPLIT_THRESHOLD = 3;
/**
 * Analyzes a spec to determine if it should be split.
 * Returns a SpecSizeAnalysis with reasons and suggested splits.
 */
export function analyzeSpecSize(spec) {
    const criteria = extractCriteria(spec);
    const criteriaCount = criteria.length;
    const estimatedHours = spec.estimation.devHours;
    const reasons = [];
    const ambiguousCriteria = [];
    // Check AC count threshold
    if (criteriaCount > AC_SPLIT_THRESHOLD) {
        reasons.push(`${String(criteriaCount)} acceptance criteria exceed the threshold of ${String(AC_SPLIT_THRESHOLD)}`);
    }
    // Check estimation threshold
    if (estimatedHours > HOURS_SPLIT_THRESHOLD) {
        reasons.push(`${String(estimatedHours)}h estimated exceeds the threshold of ${String(HOURS_SPLIT_THRESHOLD)}h`);
    }
    // Check subsystem count using description
    const subsystemCount = countSubsystemSignals(spec.title + ' ' + spec.specPath);
    if (subsystemCount > SUBSYSTEM_SPLIT_THRESHOLD) {
        reasons.push(`${String(subsystemCount)} subsystem signals detected (e.g. "and", "also", "additionally")`);
    }
    // Detect ambiguous criteria
    for (const criterion of criteria) {
        const lower = criterion.toLowerCase();
        for (const vague of VAGUE_WORDS) {
            if (lower.includes(vague)) {
                ambiguousCriteria.push(criterion);
                break;
            }
        }
    }
    const shouldSplit = reasons.length > 0;
    const suggestedSplits = shouldSplit ? buildSplitProposals(criteria, spec.target, spec.scope) : [];
    return {
        specId: spec.id,
        specTitle: spec.title,
        shouldSplit,
        reasons,
        suggestedSplits,
        criteriaCount,
        estimatedHours,
        ambiguousCriteria,
    };
}
/**
 * Executes the split of a spec into child specs.
 * Returns the list of child spec objects to be persisted by the caller.
 */
export function buildChildSpecs(original, proposals, nextIdFn) {
    const now = new Date().toISOString();
    const criteria = extractCriteria(original);
    return proposals.map((proposal, i) => {
        const childId = nextIdFn(i);
        const childSlug = slugifyTitle(proposal.title);
        const difficulty = inferDifficulty(original.type, original.scope);
        const estimation = calculateEstimation(original.type, original.scope, difficulty);
        const childSpec = {
            id: childId,
            title: proposal.title,
            slug: childSlug,
            feature: original.feature,
            type: original.type,
            scope: 'feature',
            status: 'draft',
            difficulty,
            risk: original.risk,
            projectId: original.projectId,
            createdAt: now,
            updatedAt: now,
            specPath: original.specPath.replace(/\/[^/]+\/spec\.md$/, `/${childId}-${childSlug}/spec.md`),
            technicalPath: original.specPath.replace(/\/[^/]+\/spec\.md$/, `/${childId}-${childSlug}/spec.md`),
            progressPath: undefined,
            estimation,
            actuals: null,
            target: original.target,
            tags: [...original.tags],
            dependencies: proposal.dependsOn.map((idx) => nextIdFn(idx)),
            blockedBy: [],
            gitBranch: `feat/${childId.toLowerCase()}-${childSlug}`,
            impactAnalysis: null,
            reviewNotes: [`Split from ${original.id}: ${original.title}`],
        };
        // Attach the relevant criteria as review notes for context
        const assignedCriteria = proposal.criteriaIndices
            .map((idx) => criteria[idx])
            .filter((c) => c !== undefined);
        if (assignedCriteria.length > 0) {
            childSpec.reviewNotes = [
                ...(childSpec.reviewNotes ?? []),
                `Assigned criteria: ${assignedCriteria.join('; ')}`,
            ];
        }
        return childSpec;
    });
}
// === Internal helpers ===
/** Extract acceptance criteria lines from spec path or title (simple heuristic) */
function extractCriteria(spec) {
    // We cannot read the file in a pure engine function — use reviewNotes or title
    // as proxy for criteria count. The handler reads the actual file content.
    // This overload is used when the caller passes pre-extracted criteria.
    return spec.reviewNotes ?? [];
}
/** Count subsystem signal word occurrences in a text */
function countSubsystemSignals(text) {
    const lower = text.toLowerCase();
    let count = 0;
    for (const word of SUBSYSTEM_SIGNAL_WORDS) {
        const regex = new RegExp(`\\b${word}\\b`, 'g');
        const matches = lower.match(regex);
        if (matches) {
            count += matches.length;
        }
    }
    return count;
}
/** Build split proposals from criteria list */
function buildSplitProposals(criteria, target, scope) {
    if (criteria.length === 0) {
        return buildDefaultProposals(target, scope);
    }
    const half = Math.ceil(criteria.length / 2);
    const firstIndices = criteria.slice(0, half).map((_, i) => i);
    const secondIndices = criteria.slice(half).map((_, i) => half + i);
    return [
        {
            title: `Core Implementation`,
            description: 'Core features and primary acceptance criteria',
            criteriaIndices: firstIndices,
            dependsOn: [],
        },
        {
            title: `Extended Features and Validation`,
            description: 'Extended features, edge cases and validation criteria',
            criteriaIndices: secondIndices,
            dependsOn: [0],
        },
    ];
}
/** Fallback proposals when no criteria are available */
function buildDefaultProposals(target, scope) {
    const targets = inferTargets(target, scope);
    return targets.map((t, i) => ({
        title: t.label,
        description: `${t.label} portion of the original spec`,
        criteriaIndices: [],
        dependsOn: i > 0 ? [i - 1] : [],
    }));
}
/** Simple slugify for child spec titles */
function slugifyTitle(title) {
    return title
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-|-$/g, '')
        .slice(0, 60);
}
//# sourceMappingURL=spec-splitter.js.map