// engine/code-impact-analyzer.ts — Code Change → Spec Impact Analyzer (SPEC-366)
import { execSync } from 'node:child_process';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function specIdFromPath(specId) {
    return specId.toLowerCase().replace(/-/g, '');
}
function fileMatchesSpec(filePath, spec) {
    const lower = filePath.toLowerCase();
    const specLower = specIdFromPath(spec.id);
    // Direct binding: file path contains specId
    if (lower.includes(specLower)) {
        return 'direct-binding';
    }
    // Filename match: spec title words appear in the file path
    const titleWords = spec.title
        .toLowerCase()
        .split(/\W+/)
        .filter((w) => w.length > 3);
    const matchCount = titleWords.filter((w) => lower.includes(w)).length;
    if (titleWords.length > 0 && matchCount >= Math.ceil(titleWords.length * 0.5)) {
        return 'filename-match';
    }
    // Semantic match: spec tags appear in the diff/path
    const specTags = spec.tags.map((t) => t.toLowerCase());
    if (specTags.some((t) => lower.includes(t))) {
        return 'semantic-match';
    }
    return null;
}
function diffMatchesSpec(diffText, spec) {
    const lower = diffText.toLowerCase();
    const specLower = specIdFromPath(spec.id);
    if (lower.includes(specLower)) {
        return 'direct-binding';
    }
    const titleWords = spec.title
        .toLowerCase()
        .split(/\W+/)
        .filter((w) => w.length > 3);
    const matchCount = titleWords.filter((w) => lower.includes(w)).length;
    if (titleWords.length > 0 && matchCount >= Math.ceil(titleWords.length * 0.5)) {
        return 'semantic-match';
    }
    const specTags = spec.tags.map((t) => t.toLowerCase());
    if (specTags.some((t) => lower.includes(t))) {
        return 'semantic-match';
    }
    return null;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function analyzeCodeImpact(specs, filePath, diffText) {
    const affectedSpecs = [];
    for (const spec of specs) {
        let relation = null;
        if (filePath !== undefined) {
            relation = fileMatchesSpec(filePath, spec);
        }
        if (relation === null && diffText !== undefined) {
            relation = diffMatchesSpec(diffText, spec);
        }
        if (relation !== null) {
            affectedSpecs.push({
                specId: spec.id,
                title: spec.title,
                relation,
                status: spec.status,
            });
        }
    }
    // Sort by relation priority
    const order = {
        'direct-binding': 0,
        'filename-match': 1,
        'semantic-match': 2,
    };
    affectedSpecs.sort((a, b) => order[a.relation] - order[b.relation]);
    return {
        ...(filePath !== undefined ? { filePath } : {}),
        affectedSpecs,
        totalSpecs: specs.length,
    };
}
export function checkCodeChangeCompliance(specs, filePath, commitDiff) {
    const impact = analyzeCodeImpact(specs, filePath, commitDiff);
    const approvedStatuses = new Set(['approved', 'implementing', 'done', 'in-progress']);
    const justifyingSpecs = impact.affectedSpecs.filter((s) => approvedStatuses.has(s.status.toLowerCase()));
    const unjustifiedChanges = [];
    if (impact.affectedSpecs.length === 0) {
        unjustifiedChanges.push({
            filePath: filePath ?? 'unknown',
            reason: 'No related specs found for this change.',
        });
    }
    else if (justifyingSpecs.length === 0) {
        unjustifiedChanges.push({
            filePath: filePath ?? 'unknown',
            reason: `Related specs found (${impact.affectedSpecs.map((s) => s.specId).join(', ')}) but none are approved/implementing.`,
        });
    }
    const total = impact.affectedSpecs.length === 0 ? 0 : justifyingSpecs.length;
    const denom = Math.max(impact.affectedSpecs.length, 1);
    const complianceScore = Math.round((total / denom) * 100);
    const summary = complianceScore >= 100
        ? `Change is fully justified by ${String(justifyingSpecs.length)} spec(s).`
        : complianceScore > 0
            ? `Partial compliance (${String(complianceScore)}%). Some specs are not yet approved.`
            : 'Change is not justified by any approved spec.';
    return { complianceScore, justifyingSpecs, unjustifiedChanges, summary };
}
export function suggestSpecUpdates(specs, filePath) {
    const suggestions = [];
    const impact = analyzeCodeImpact(specs, filePath);
    if (impact.affectedSpecs.length === 0) {
        suggestions.push({
            type: 'create',
            reason: `No specs found for "${filePath}". Consider creating a new spec to document this functionality.`,
        });
    }
    else {
        for (const affected of impact.affectedSpecs) {
            const spec = specs.find((s) => s.id === affected.specId);
            if (spec !== undefined && spec.status !== 'done') {
                suggestions.push({
                    type: 'update',
                    specId: affected.specId,
                    title: spec.title,
                    reason: `File "${filePath}" matches this spec (${affected.relation}). Update acceptance criteria if behavior changed.`,
                });
            }
        }
    }
    return { filePath, suggestions };
}
export function runGitDiffImpact(specs, projectPath) {
    let diffText = '';
    try {
        diffText = execSync('git diff HEAD', {
            cwd: projectPath,
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
    }
    catch {
        // ignore git errors — proceed with empty diff
    }
    return analyzeCodeImpact(specs, undefined, diffText);
}
//# sourceMappingURL=code-impact-analyzer.js.map