// engine/spec-quality-scorer.ts — Spec quality scoring logic (SPEC-314)
import { readFile } from 'node:fs/promises';
import { readSpecTechnicalSection } from './spec-format/read-technical-section.js';
import { stripFrontmatter } from './frontmatter-parser.js';
// ── Constants ────────────────────────────────────────────────────────────────
const MAX_DIMENSION_SCORE = 25;
const BDD_PATTERN = /\b(given|when|then)\b/gi;
const MEASURABILITY_UNIT_PATTERN = /\b\d+(%|ms|s|px|kb|mb|gb|rpm|req)\b|\b<\s*\d+|\b>\s*\d+|\b[≥≤]\s*\d+/i;
const MEASURABILITY_VERB_PATTERN = /\b(returns|redirects|displays|shows|saves|throws|creates|deletes|updates|sends)\b/i;
const VAGUE_WORDS = [
    'works',
    'fast',
    'easy',
    'good',
    'simple',
    'nice',
    'better',
    'quickly',
    'efficiently',
    'properly',
    'correctly',
    'seamlessly',
    'robust',
    'scalable',
    'intuitive',
];
const CONTRADICTION_PAIRS = [
    ['always', 'never'],
    ['mandatory', 'optional'],
    ['required', 'not required'],
    ['must', 'must not'],
    ['synchronous', 'asynchronous'],
    ['blocking', 'non-blocking'],
];
// ── Internal helpers ─────────────────────────────────────────────────────────
function gradeFromTotal(total) {
    if (total >= 90) {
        return 'A';
    }
    if (total >= 75) {
        return 'B';
    }
    if (total >= 60) {
        return 'C';
    }
    if (total >= 45) {
        return 'D';
    }
    return 'F';
}
async function readSpecContent(spec) {
    if (!spec.specPath) {
        return '';
    }
    try {
        const raw = await readFile(spec.specPath, 'utf-8');
        return stripFrontmatter(raw);
    }
    catch (err) {
        const code = err.code;
        if (code !== 'ENOENT') {
            throw err;
        }
        return '';
    }
}
async function readTechnicalContent(spec) {
    return readSpecTechnicalSection(spec);
}
function extractCriteriaLines(content) {
    return content
        .split('\n')
        .filter((line) => /^- \[[ x]\]/.test(line.trim()))
        .map((line) => line.trim());
}
// ── Dimension scorers ────────────────────────────────────────────────────────
function scoreCompleteness(spec, content) {
    const issues = [];
    const recommendations = [];
    let score = 0;
    // Title present (5 pts)
    if (spec.title.trim().length > 5) {
        score += 5;
    }
    else {
        issues.push('Title is missing or too short');
        recommendations.push('Add a descriptive title (>5 chars)');
    }
    // Description / body present (5 pts)
    const bodyLength = content.trim().length;
    if (bodyLength > 100) {
        score += 5;
    }
    else {
        issues.push('Spec body is missing or too brief');
        recommendations.push('Add a description with context and motivation');
    }
    // Acceptance criteria present (5 pts)
    const criteriaLines = extractCriteriaLines(content);
    if (criteriaLines.length >= 3) {
        score += 5;
    }
    else if (criteriaLines.length >= 1) {
        score += 2;
        issues.push('Less than 3 acceptance criteria found');
        recommendations.push('Add at least 3 acceptance criteria');
    }
    else {
        issues.push('No acceptance criteria found');
        recommendations.push('Add acceptance criteria in checkbox format (- [ ] ...)');
    }
    // Scope set to non-default (5 pts) — cast to handle partial specs at runtime
    const scope = spec.scope;
    if (scope?.trim().length) {
        score += 5;
    }
    else {
        issues.push('Scope not defined');
        recommendations.push('Set scope: trivial | feature | cross-module | architectural');
    }
    // Estimation present (5 pts) — check devHours is non-zero as a proxy
    const estimation = spec.estimation;
    const hasEstimation = (estimation?.devHours ?? 0) > 0;
    if (hasEstimation) {
        score += 5;
    }
    else {
        issues.push('No estimation provided');
        recommendations.push('Add a time estimation using the estimate tool');
    }
    return { score, maxScore: MAX_DIMENSION_SCORE, issues, recommendations };
}
function detectBddKeywords(line) {
    const found = new Set();
    const lower = line.toLowerCase();
    const matches = lower.match(BDD_PATTERN);
    if (matches) {
        for (const m of matches) {
            found.add(m.toLowerCase());
        }
    }
    return found;
}
function isMeasurable(line) {
    return MEASURABILITY_UNIT_PATTERN.test(line) || MEASURABILITY_VERB_PATTERN.test(line);
}
function scoreTestability(criteriaLines) {
    const issues = [];
    const recommendations = [];
    let score = MAX_DIMENSION_SCORE;
    if (criteriaLines.length === 0) {
        return {
            score: 0,
            maxScore: MAX_DIMENSION_SCORE,
            issues: ['No acceptance criteria to evaluate testability'],
            recommendations: ['Add verifiable acceptance criteria'],
            bddCoverage: 0,
        };
    }
    // Penalize vague words (up to -15 pts)
    let vagueCount = 0;
    for (const line of criteriaLines) {
        const lower = line.toLowerCase();
        const found = VAGUE_WORDS.filter((w) => {
            const pattern = new RegExp(`\\b${w}\\b`);
            return pattern.test(lower);
        });
        if (found.length > 0) {
            vagueCount++;
            issues.push(`Vague criterion (${found.join(', ')}): "${line.slice(0, 70)}"`);
        }
    }
    const vagueRatio = vagueCount / criteriaLines.length;
    if (vagueRatio >= 0.5) {
        score -= 15;
        recommendations.push('Rewrite criteria using specific, measurable outcomes');
    }
    else if (vagueRatio > 0) {
        score -= Math.round(vagueRatio * 10);
        recommendations.push('Replace vague words with specific, verifiable conditions');
    }
    // Reward for many criteria (bonus up to +5 absorbed in initial 25)
    if (criteriaLines.length < 3) {
        score -= 5;
        recommendations.push('Add more acceptance criteria (aim for 5+)');
    }
    // BDD structure detection: +5 per full BDD (GIVEN+WHEN+THEN), +2 per partial (2 of 3)
    let bddAcCount = 0;
    let bddBonus = 0;
    for (const line of criteriaLines) {
        const keywords = detectBddKeywords(line);
        const hasGiven = keywords.has('given');
        const hasWhen = keywords.has('when');
        const hasThen = keywords.has('then');
        const matchCount = (hasGiven ? 1 : 0) + (hasWhen ? 1 : 0) + (hasThen ? 1 : 0);
        if (matchCount === 3) {
            bddBonus += 5;
            bddAcCount++;
        }
        else if (matchCount === 2) {
            bddBonus += 2;
            bddAcCount++;
        }
    }
    if (bddBonus > 0) {
        score += bddBonus;
    }
    else if (criteriaLines.length >= 3) {
        recommendations.push('Use GIVEN/WHEN/THEN structure in acceptance criteria for better testability');
    }
    // Measurability detection: +3 per AC with measurable threshold or action verb
    let measurableBonus = 0;
    for (const line of criteriaLines) {
        if (isMeasurable(line)) {
            measurableBonus += 3;
        }
    }
    if (measurableBonus > 0) {
        score += measurableBonus;
    }
    else if (criteriaLines.length >= 3) {
        recommendations.push('Add measurable thresholds (e.g., <200ms, HTTP 200) or action verbs to criteria');
    }
    const bddCoverage = criteriaLines.length > 0 ? Math.round((bddAcCount / criteriaLines.length) * 100) : 0;
    return {
        score: Math.max(0, Math.min(MAX_DIMENSION_SCORE, score)),
        maxScore: MAX_DIMENSION_SCORE,
        issues,
        recommendations,
        bddCoverage,
    };
}
function scoreAmbiguity(content, criteriaLines) {
    const issues = [];
    const recommendations = [];
    let score = MAX_DIMENSION_SCORE;
    if (content.trim().length === 0) {
        return {
            score: 0,
            maxScore: MAX_DIMENSION_SCORE,
            issues: ['No spec content to check for ambiguity'],
            recommendations: ['Add spec content with clear requirements'],
        };
    }
    const lower = content.toLowerCase();
    // Check for contradiction pairs (-3 pts each, max -9)
    let contradictions = 0;
    for (const [termA, termB] of CONTRADICTION_PAIRS) {
        if (lower.includes(termA) && lower.includes(termB)) {
            contradictions++;
            if (contradictions <= 3) {
                issues.push(`Potential contradiction: "${termA}" and "${termB}" both appear in spec`);
            }
        }
    }
    if (contradictions > 0) {
        score -= Math.min(9, contradictions * 3);
        recommendations.push('Review and resolve contradictory requirements');
    }
    // Check for undefined terms (TBD, TODO, FIXME) (-3 pts each, max -9)
    const undefinedTerms = ['tbd', 'todo', 'fixme', 'placeholder', 'to be defined', 'to be decided'];
    let undefinedCount = 0;
    for (const term of undefinedTerms) {
        if (lower.includes(term)) {
            undefinedCount++;
            issues.push(`Undefined placeholder found: "${term}"`);
        }
    }
    if (undefinedCount > 0) {
        score -= Math.min(9, undefinedCount * 3);
        recommendations.push('Replace all TBD/TODO/FIXME markers with concrete requirements');
    }
    // Check for edge case coverage: criteria with error/failure/invalid/empty (-4 if missing)
    const edgeCaseKeywords = ['error', 'fail', 'invalid', 'empty', 'null', 'missing', 'timeout'];
    const hasEdgeCases = criteriaLines.length > 0 &&
        criteriaLines.some((line) => {
            const l = line.toLowerCase();
            return edgeCaseKeywords.some((k) => l.includes(k));
        });
    if (criteriaLines.length >= 3 && !hasEdgeCases) {
        score -= 4;
        issues.push('No edge case criteria found (error, failure, invalid inputs)');
        recommendations.push('Add criteria covering error conditions and edge cases');
    }
    return {
        score: Math.max(0, Math.min(MAX_DIMENSION_SCORE, score)),
        maxScore: MAX_DIMENSION_SCORE,
        issues,
        recommendations,
    };
}
function scoreRisk(spec, technicalContent) {
    const issues = [];
    const recommendations = [];
    let score = MAX_DIMENSION_SCORE;
    // Cross-module dependencies increase risk (-5 if many deps)
    if (spec.dependencies.length >= 3) {
        score -= 5;
        issues.push(`High dependency count (${spec.dependencies.length} deps): ${spec.dependencies.join(', ')}`);
        recommendations.push('Document dependency risks and verify all deps are done before starting');
    }
    else if (spec.dependencies.length > 0) {
        score -= 2;
        issues.push(`Has ${spec.dependencies.length} dependencies — verify they are implemented`);
    }
    // Architectural scope multiplies risk (-5 if architectural)
    if (spec.scope === 'architectural') {
        score -= 5;
        issues.push('Architectural scope — higher risk of cross-cutting impact');
        recommendations.push('Ensure ADRs and architecture docs are updated');
    }
    else if (spec.scope === 'cross-module') {
        score -= 2;
        issues.push('Cross-module scope — coordinate with other teams/specs');
    }
    // Missing NFRs in technical content (-5 if technical file exists but no NFRs)
    if (technicalContent.trim().length > 50) {
        const lowerTech = technicalContent.toLowerCase();
        const nfrKeywords = [
            'performance',
            'security',
            'scalability',
            'reliability',
            'availability',
            'latency',
            'throughput',
            'sla',
            'nfr',
        ];
        const hasNfrs = nfrKeywords.some((k) => lowerTech.includes(k));
        if (!hasNfrs) {
            score -= 5;
            issues.push('No non-functional requirements found in technical spec');
            recommendations.push('Document NFRs: performance, security, scalability constraints');
        }
    }
    else if (technicalContent.trim().length === 0) {
        score -= 5;
        issues.push('No technical spec file found');
        recommendations.push('Run create_spec_tech to generate a technical specification');
    }
    // Tags with security/perf keywords indicate risk coverage is good (bonus up to +2 absorbed)
    const riskTags = ['security', 'performance', 'breaking-change', 'migration', 'data-loss'];
    const hasRiskTags = spec.tags.some((tag) => riskTags.some((rt) => tag.toLowerCase().includes(rt)));
    if (hasRiskTags && score < MAX_DIMENSION_SCORE) {
        // Risk tags show awareness — partial recovery
        score = Math.min(MAX_DIMENSION_SCORE, score + 2);
    }
    return {
        score: Math.max(0, Math.min(MAX_DIMENSION_SCORE, score)),
        maxScore: MAX_DIMENSION_SCORE,
        issues,
        recommendations,
    };
}
// ── Public API ───────────────────────────────────────────────────────────────
export async function scoreSpecQuality(spec) {
    const [specContent, technicalContent] = await Promise.all([
        readSpecContent(spec),
        readTechnicalContent(spec),
    ]);
    const criteriaLines = extractCriteriaLines(specContent);
    const completenessDetail = scoreCompleteness(spec, specContent);
    const testabilityDetail = scoreTestability(criteriaLines);
    const ambiguityDetail = scoreAmbiguity(specContent, criteriaLines);
    const riskDetail = scoreRisk(spec, technicalContent);
    const total = completenessDetail.score + testabilityDetail.score + ambiguityDetail.score + riskDetail.score;
    const qualityScore = {
        total,
        completeness: completenessDetail.score,
        testability: testabilityDetail.score,
        ambiguity: ambiguityDetail.score,
        risk: riskDetail.score,
        grade: gradeFromTotal(total),
    };
    return {
        specId: spec.id,
        score: qualityScore,
        dimensions: {
            completeness: completenessDetail,
            testability: testabilityDetail,
            ambiguity: ambiguityDetail,
            risk: riskDetail,
        },
    };
}
//# sourceMappingURL=spec-quality-scorer.js.map