// engine/coverage-gap-analyzer.ts — SPEC-323
// Maps spec acceptance criteria to existing test files and surfaces untested ACs.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { glob } from 'glob';
// ---------------------------------------------------------------------------
// Risk domain classification
// ---------------------------------------------------------------------------
/** Keywords that signal high/medium/low risk domains. */
const RISK_DOMAIN_MAP = [
    {
        keywords: ['auth', 'login', 'logout', 'password', 'token', 'session', 'permission', 'role'],
        domain: 'auth',
        level: 'high',
    },
    {
        keywords: ['payment', 'billing', 'charge', 'invoice', 'stripe', 'refund', 'subscription'],
        domain: 'payment',
        level: 'high',
    },
    {
        keywords: ['data', 'persist', 'database', 'integrity', 'transaction', 'migration', 'schema'],
        domain: 'data-integrity',
        level: 'high',
    },
    {
        keywords: ['security', 'encrypt', 'decrypt', 'hash', 'sanitize', 'injection', 'xss', 'csrf'],
        domain: 'security',
        level: 'high',
    },
    {
        keywords: ['api', 'endpoint', 'request', 'response', 'http', 'webhook', 'validation'],
        domain: 'api',
        level: 'medium',
    },
    {
        keywords: ['email', 'notification', 'alert', 'message', 'sms'],
        domain: 'notification',
        level: 'medium',
    },
    {
        keywords: ['ui', 'button', 'label', 'color', 'style', 'layout', 'cosmetic', 'visual'],
        domain: 'ui',
        level: 'low',
    },
    {
        keywords: ['log', 'monitor', 'metric', 'trace', 'analytics'],
        domain: 'observability',
        level: 'low',
    },
];
function classifyRisk(criterion) {
    const lower = criterion.toLowerCase();
    for (const entry of RISK_DOMAIN_MAP) {
        if (entry.keywords.some((kw) => lower.includes(kw))) {
            return { domain: entry.domain, level: entry.level };
        }
    }
    return { domain: 'general', level: 'medium' };
}
// ---------------------------------------------------------------------------
// AC extraction (includes both checked [x] and unchecked [ ] criteria)
// ---------------------------------------------------------------------------
function extractAcceptanceCriteria(specContent) {
    const lines = specContent.split('\n');
    const criteria = [];
    for (const line of lines) {
        const match = /^\s*-\s*\[\s*[xX ]?\]\s+(.+)$/.exec(line);
        if (match?.[1]) {
            criteria.push(match[1].trim());
        }
    }
    return criteria;
}
// ---------------------------------------------------------------------------
// Test file discovery
// ---------------------------------------------------------------------------
async function discoverTestFiles(projectPath) {
    const patterns = [
        'tests/**/*.test.ts',
        'tests/**/*.spec.ts',
        'test/**/*.test.ts',
        'test/**/*.spec.ts',
        '__tests__/**/*.ts',
        '**/*.test.py',
        '**/*Test.java',
        '**/*_test.go',
    ];
    const results = await Promise.all(patterns.map((p) => glob(p, { cwd: projectPath, absolute: true }).catch(() => [])));
    return [...new Set(results.flat())];
}
/** Read all discovered test files and concatenate content for substring matching. */
async function readTestCorpus(testFiles) {
    const contents = await Promise.all(testFiles.map((f) => readFile(f, 'utf8').catch(() => '')));
    return contents.join('\n').toLowerCase();
}
// ---------------------------------------------------------------------------
// Matching
// ---------------------------------------------------------------------------
/** Extract keyword tokens from a criterion for matching against test corpus. */
function extractKeywords(criterion) {
    return criterion
        .toLowerCase()
        .replace(/[^\w\s]/g, ' ')
        .split(/\s+/)
        .filter((w) => w.length >= 3)
        .slice(0, 5);
}
function isCovered(criterion, corpus) {
    const keywords = extractKeywords(criterion);
    if (keywords.length === 0) {
        return false;
    }
    const matchCount = keywords.filter((kw) => corpus.includes(kw)).length;
    return matchCount >= Math.ceil(keywords.length / 2);
}
// ---------------------------------------------------------------------------
// Recommendation builder
// ---------------------------------------------------------------------------
function buildRecommendation(criterion, domain, level) {
    const prefix = level === 'high'
        ? 'CRITICAL: Add test immediately.'
        : level === 'medium'
            ? 'Add test in next sprint.'
            : 'Low priority — consider adding a smoke test.';
    let hint;
    if (domain === 'auth') {
        hint = 'Cover both valid and invalid authentication scenarios.';
    }
    else if (domain === 'payment') {
        hint = 'Test success, failure, and refund flows with mocked payment gateway.';
    }
    else if (domain === 'data-integrity') {
        hint = 'Test transactional rollback and constraint violations.';
    }
    else if (domain === 'security') {
        hint = 'Include penetration test cases for injection and XSS vectors.';
    }
    else {
        hint = `Write a test for: "${criterion}"`;
    }
    return `${prefix} ${hint}`;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Analyze coverage gaps between spec acceptance criteria and existing test files.
 */
export async function analyzeCoverageGaps(specPath, projectPath) {
    const specContent = await readFile(join(specPath, 'spec.md'), 'utf8').catch(() => readFile(specPath, 'utf8'));
    const criteria = extractAcceptanceCriteria(specContent);
    const testFiles = await discoverTestFiles(projectPath);
    const corpus = await readTestCorpus(testFiles);
    const gaps = [];
    let coveredCount = 0;
    for (const criterion of criteria) {
        if (isCovered(criterion, corpus)) {
            coveredCount++;
        }
        else {
            const { domain, level } = classifyRisk(criterion);
            const recommendation = buildRecommendation(criterion, domain, level);
            gaps.push({ criterion, riskDomain: domain, riskLevel: level, recommendation });
        }
    }
    const total = criteria.length;
    const coverageScore = total === 0 ? 100 : Math.round((coveredCount / total) * 100);
    return {
        gaps,
        coverageScore,
        totalCriteria: total,
        coveredCriteria: coveredCount,
        testFilesScanned: testFiles.length,
    };
}
//# sourceMappingURL=coverage-gap-analyzer.js.map