// engine/readiness-checker.ts — Spec readiness evaluation for implementation gate (SPEC-039)
import { readFile } from 'node:fs/promises';
import { readSpecTechnicalSection } from './spec-format/read-technical-section.js';
import { stripFrontmatter } from './frontmatter-parser.js';
import { loadReadinessConfig } from './readiness-config-loader.js';
import { runEarsGate } from './ears-gate.js';
import { findScenariosWithoutTests, parseFrontmatterScenarios, } from './validator/spec-compliance-runner.js';
import { evaluateImplementationContract } from './implementation-contract/index.js';
// ── SPEC-784: Technical section quality constants ─────────────────────────────
const TECHNICAL_MIN_CHARS = 500;
// Detects "See technical.md", "See spec.md", or "See `<file>` technical.md" patterns
const TECHNICAL_PLACEHOLDER_REGEX = /\bSee\s+(?:`?[^`\n]+`?\s+)?(?:technical|spec)\.md/i;
// ── Helpers ──────────────────────────────────────────────────────────────────
function scoreHuCompleteness(spec) {
    const blockers = [];
    const warnings = [];
    let points = 0;
    // title is required but may be empty
    if (spec.title.trim().length > 0) {
        points += 5;
    }
    else {
        blockers.push('Spec is missing a title');
    }
    // scope, target, type are non-optional on Spec — always award points
    points += 5; // scope
    points += 5; // target
    points += 5; // type
    // Only add a metadata warning if something major is obviously default/empty
    if (spec.scope === 'feature' && spec.target === 'backend' && spec.type === 'feature') {
        warnings.push('Spec metadata uses all default values — verify scope, target and type are intentional');
    }
    return { points, blockers, warnings };
}
async function readHuRaw(spec) {
    if (!spec.specPath) {
        return '';
    }
    try {
        return await readFile(spec.specPath, 'utf-8');
    }
    catch {
        return '';
    }
}
/**
 * Count BDD scenarios stored in the YAML frontmatter of a spec.md file.
 * Scenarios use a multi-line YAML structure that the simple KV frontmatter
 * parser cannot handle, so we scan the raw frontmatter block directly.
 *
 * Each scenario emitted by renderBddScenariosYaml starts with "  - title:"
 * so counting those lines gives the exact scenario count.
 */
function countFrontmatterScenarios(raw) {
    const FRONTMATTER_BLOCK_RE = /^---\n([\s\S]*?)\n---\n/;
    const match = FRONTMATTER_BLOCK_RE.exec(raw);
    if (!match?.[1]) {
        return 0;
    }
    const block = match[1];
    // Each scenario entry starts with "  - title:" (2-space indent)
    return (block.match(/^\s+-\s+title:/gm) ?? []).length;
}
async function readFichaContent(spec) {
    return readSpecTechnicalSection(spec);
}
/**
 * Extract acceptance-criterion lines from the spec body.
 * Accepts four valid shapes (any can appear anywhere in the body):
 *  1. Checkbox form:          `- [ ] criterion text` / `- [x] criterion text`
 *  2. GIVEN/WHEN/THEN bullet: `- GIVEN ... WHEN ... THEN ...` (case-insensitive)
 *  3. Plain bullets inside a `## Acceptance Criteria` (or synonyms) section.
 *  4. Full multi-line body of `## Acceptance criteria` — includes BDD paragraphs
 *     with FILES:/FUNCTIONS:/TEST: markers that live between bullet lines.
 *
 * Historical parser (v1.82 and earlier) matched only form #1, producing false
 * negatives on specs authored with plain GIVEN/WHEN/THEN lists — those specs
 * reported `criteria: 0` at score 70 even when fully specified. Tracked in
 * memory/feedback_check_readiness_false_negative.md.
 *
 * SPEC-777 fix: form #4 ensures that FILES:/FUNCTIONS: markers embedded in BDD
 * paragraphs inside the AC section are visible to checkSpecificityGate.
 */
export function extractCriteriaLines(huContent) {
    const lines = huContent.split('\n');
    const criteria = [];
    let inCriteriaSection = false;
    for (const raw of lines) {
        const line = raw.trim();
        // Detect entering / leaving an Acceptance Criteria section
        if (/^#{1,6}\s+/.test(line)) {
            inCriteriaSection =
                /^#{1,6}\s+(acceptance\s+criteria|criteria|criterios\s+de\s+aceptaci[oó]n|criterios)\b/i.test(line);
            continue;
        }
        // Form 1 — checkbox (always counts, regardless of section)
        if (/^- \[[ x]\]/i.test(line)) {
            criteria.push(line);
            continue;
        }
        // Form 2 — GIVEN/WHEN/THEN bullet (always counts, case-insensitive)
        if (/^- .*\bgiven\b.*\bwhen\b.*\bthen\b/i.test(line)) {
            criteria.push(line);
            continue;
        }
        // Forms 3+4 — inside an Acceptance Criteria section: capture ALL non-empty
        // lines (bullets, BDD paragraphs, FILES:/FUNCTIONS:/TEST: annotations, etc.)
        if (inCriteriaSection && line.length > 0) {
            criteria.push(line);
        }
    }
    return criteria;
}
function frontmatterScenarioCriteria(raw) {
    return parseFrontmatterScenarios(raw).map((scenario) => {
        const tests = scenario.tests
            ?.map((test) => `${test.path}${test.line !== undefined ? ` line ${String(test.line)}` : ''}`)
            .join(' ') ?? '';
        return [scenario.title, tests].filter((part) => part.trim().length > 0).join(' ');
    });
}
function scoreCriteria(criteriaLines, vagueWords) {
    const blockers = [];
    const warnings = [];
    const count = criteriaLines.length;
    let points;
    if (count >= 5) {
        points = 30;
    }
    else if (count >= 3) {
        points = 20;
    }
    else if (count >= 1) {
        points = 10;
    }
    else {
        blockers.push('No acceptance criteria found — add at least 3 testable criteria');
        return { points: 0, blockers, warnings };
    }
    // Vague criteria detection
    for (const line of criteriaLines) {
        const lower = line.toLowerCase();
        const vagueFound = vagueWords.filter((word) => {
            const pattern = new RegExp(`\\b${word}\\b`);
            return pattern.test(lower);
        });
        if (vagueFound.length > 0) {
            warnings.push(`Vague criterion detected (words: ${vagueFound.join(', ')}): "${line.slice(0, 80)}"`);
        }
    }
    return { points, blockers, warnings };
}
function scoreFilesIdentified(spec, fichaContent) {
    const blockers = [];
    const warnings = [];
    const hasFicha = fichaContent.trim().length > 0;
    const hasTagsWithFiles = spec.tags.some((tag) => tag.includes('src/') || tag.includes('.ts') || tag.includes('.js'));
    let points;
    if (hasFicha) {
        points = 20;
    }
    else if (hasTagsWithFiles) {
        points = 10;
    }
    else {
        points = 0;
        warnings.push('No inline ## Technical section found in spec.md — files to create/modify are not identified.');
    }
    return { points, blockers, warnings };
}
function scoreDependencies(spec) {
    const blockers = [];
    const warnings = [];
    if (spec.dependencies.length === 0) {
        return { points: 30, blockers, warnings };
    }
    warnings.push(`Spec has ${spec.dependencies.length} dependencies (${spec.dependencies.join(', ')}). Verify they are implemented before starting.`);
    return { points: 20, blockers, warnings };
}
function buildRecommendations(score, huPoints, criteriaPoints, filesPoints, depsPoints) {
    const recs = [];
    if (score >= 70) {
        return recs;
    }
    if (huPoints < 20) {
        recs.push('Complete the spec metadata: title, scope, target, type');
    }
    if (criteriaPoints < 30) {
        recs.push('Add more acceptance criteria (aim for 5+ testable criteria)');
    }
    if (filesPoints < 20) {
        recs.push('Add an inline ## Technical section to spec.md with key files');
    }
    if (depsPoints < 30) {
        recs.push('Verify all spec dependencies are in "done" status');
    }
    if (criteriaPoints > 0 && criteriaPoints < 30) {
        recs.push('Review criteria for vague language — each criterion should be independently testable');
    }
    return recs;
}
// ── SPEC-784: Technical section quality helpers ───────────────────────────────
/**
 * Extract the content of the `## Technical` section from a spec body.
 * Returns an empty string when the section is absent.
 */
function extractTechnicalSection(body) {
    // Match ## Technical (case-insensitive, optional trailing whitespace)
    const sectionRe = /^#{1,6}\s+technical\b[^\n]*/im;
    const match = sectionRe.exec(body);
    if (!match) {
        return '';
    }
    const afterHeader = body.slice(match.index + match[0].length);
    // Content ends at the next same-or-higher heading or end of string
    const nextHeadingRe = /^#{1,6}\s+/m;
    const nextMatch = nextHeadingRe.exec(afterHeader);
    return nextMatch ? afterHeader.slice(0, nextMatch.index) : afterHeader;
}
/**
 * SPEC-784: Returns true when the ## Technical body contains a pointer
 * like "See technical.md" or "See `some-file` spec.md" — a placeholder
 * that defers content to another file without providing it inline.
 */
export function detectTechnicalPlaceholder(body) {
    const section = extractTechnicalSection(body);
    if (section.trim().length === 0) {
        return false;
    }
    return TECHNICAL_PLACEHOLDER_REGEX.test(section);
}
/**
 * SPEC-784: Returns true when the ## Technical body is too shallow:
 * fewer than TECHNICAL_MIN_CHARS characters AND contains zero file
 * paths under src/ or tests/.
 */
export function detectTechnicalTooShallow(body) {
    const section = extractTechnicalSection(body);
    if (section.trim().length === 0) {
        // No section at all — not "shallow", handled by other gates
        return false;
    }
    if (section.length >= TECHNICAL_MIN_CHARS) {
        return false;
    }
    // Has at least one file path under src/ or tests/ → not too shallow
    const hasFilePaths = /\b(?:src|tests)\/[a-zA-Z0-9/_.-]+\.[a-z]+\b/.test(section);
    return !hasFilePaths;
}
// ── SPEC-629 / SPEC-777: Specificity gate helpers ────────────────────────────
/**
 * Extract distinct file paths from arbitrary text.
 * Recognises paths in multiple formats:
 *  - bare:          src/engine/foo.ts
 *  - backtick:      `src/engine/foo.ts`
 *  - after FILES::  FILES: `a.ts`, `b.ts`
 *  - comma-sep:     src/a.ts, src/b.ts
 *  - in BDD paras:  GIVEN ... FILES: `src/...`
 */
export function extractFilePaths(text) {
    const found = new Set();
    // Match paths that look like <dir>/<rest>.<ext> where dir is a valid identifier
    // Works inside backticks, after commas/spaces, after FILES: markers
    const RE = /(?:[`\s,]|^|FILES:\s*)([a-zA-Z][a-zA-Z0-9_-]*(?:\/[a-zA-Z0-9_.-]+)+\.[a-zA-Z]{1,6})(?=[`\s,\n)]|$)/g;
    let m;
    while ((m = RE.exec(text)) !== null) {
        const p = m[1];
        if (p !== undefined) {
            found.add(p);
        }
    }
    return found;
}
/**
 * Extract function/method names from text.
 * Recognises:
 *  - After FUNCTIONS:: FUNCTIONS: `detectStuckSpecs(...)`, `buildMergedBranchSet(...)`
 *  - Code spans: `fnName(...)` or `fnName()`
 *  - Inline: createFoo() detectBar(arg)
 */
export function extractFunctionNames(text) {
    const found = new Set();
    // camelCase identifiers followed by ( in code spans or inline
    const RE = /`?([a-z][a-zA-Z0-9]+)\s*\([^)]*\)`?/g;
    let m;
    while ((m = RE.exec(text)) !== null) {
        if (m[1] !== undefined) {
            found.add(m[1]);
        }
    }
    return found;
}
// ── SPEC-629: Specificity gate (difficulty >= 3) ────────────────────────────
/**
 * For specs with difficulty >= 3, enforces that criteria contain:
 * - 3+ concrete file paths (src/ or tests/)
 * - 1+ criterion with exact function/type name
 * - 1+ anticipated test break (file + line number)
 * Returns blocker strings; empty array = gate passed.
 */
function checkSpecificityGate(spec, criteriaLines, fichaContent) {
    if (spec.difficulty < 3) {
        return [];
    }
    const allText = [...criteriaLines, fichaContent].join('\n');
    const blockers = [];
    // Count distinct file paths in src/ or tests/ — use enhanced extractor (SPEC-777)
    const uniquePaths = extractFilePaths(allText);
    // Also keep the legacy word-boundary regex for backward compat with bare paths not in backticks
    const legacyMatches = allText.match(/\b(?:src|tests)\/[a-zA-Z0-9/_.-]+\.[a-z]+\b/g) ?? [];
    for (const p of legacyMatches) {
        uniquePaths.add(p);
    }
    if (uniquePaths.size < 3) {
        blockers.push(`Difficulty ${String(spec.difficulty)} spec requires 3+ concrete file paths in criteria (src/ or tests/) — found ${String(uniquePaths.size)}. ` +
            `Add exact paths like 'src/engine/foo/bar.ts'.`);
    }
    // Detect function calls via enhanced extractor + legacy regex (SPEC-777)
    const functionNames = extractFunctionNames(allText);
    const hasFunctionName = functionNames.size > 0 || /\b[a-z][a-zA-Z0-9]+\(\)/.test(allText);
    const hasTypeName = /\b[A-Z][a-zA-Z]{2,}\b/.test(allText);
    if (!hasFunctionName && !hasTypeName) {
        blockers.push(`Difficulty ${String(spec.difficulty)} spec requires at least 1 criterion with an exact function name (e.g. createFoo()) or type name (e.g. FooBar).`);
    }
    // Detect anticipated test breaks: a test file path + "line N" (with optional ~) or "change ... from ... to"
    const hasTestBreak = /\btests?\/[a-zA-Z0-9/_.-]+\.test\.[a-z]+.*\bline\s+~?\d+/i.test(allText) ||
        /\bchange\b.*\bfrom\b.*\bto\b/i.test(allText);
    if (!hasTestBreak) {
        blockers.push(`Difficulty ${String(spec.difficulty)} spec requires at least 1 anticipated test break ` +
            `(e.g. 'tests/api/foo.test.ts line 47: change count from 477 to 478').`);
    }
    return blockers;
}
// ── Public API ───────────────────────────────────────────────────────────────
/**
 * SPEC-769: Pure in-memory readiness check — no file I/O.
 * Accepts the raw spec.md content (including frontmatter) and returns a quick
 * score + warnings without loading technical.md or any additional file.
 *
 * Scoring (max 100):
 *  - Base (title/metadata present): 20
 *  - Criteria: 0 | 20 | 35 | 50  (0 / 1-2 / 3-4 / 5+)
 *  - Technical files (src/*.ts paths found in body): 30
 */
export function checkReadinessInternal(body) {
    const bodyContent = stripFrontmatter(body);
    const criteriaLines = extractCriteriaLines(bodyContent);
    const scenarioCount = criteriaLines.length === 0 ? countFrontmatterScenarios(body) : 0;
    const criteriaCount = criteriaLines.length > 0 ? criteriaLines.length : scenarioCount;
    const config = loadReadinessConfig();
    const vagueWords = config.vagueWords.map((v) => v.word);
    const warnings = [];
    // Score based on criteria count
    let criteriaScore;
    if (criteriaCount === 0) {
        criteriaScore = 0;
        warnings.push('No acceptance criteria found — add at least 3 testable criteria');
    }
    else if (criteriaCount >= 5) {
        criteriaScore = 50;
    }
    else if (criteriaCount >= 3) {
        criteriaScore = 35;
    }
    else {
        criteriaScore = 20;
        warnings.push('Add more acceptance criteria (need at least 3 for adequate readiness)');
    }
    // Vague criteria detection (re-uses existing logic)
    const criteriaResult = scoreCriteria(criteriaLines.length > 0 ? criteriaLines : [], vagueWords);
    warnings.push(...criteriaResult.warnings);
    // Technical files presence (proxy for inline ## Technical — check src/ paths in body)
    const hasTechnicalFiles = /\bsrc\/[^\s]+\.[jt]s\b/.test(bodyContent);
    const technicalScore = hasTechnicalFiles ? 30 : 0;
    if (!hasTechnicalFiles && criteriaCount > 0) {
        warnings.push('No source file paths (e.g. src/engine/foo.ts) found — add file entries in ## Technical section');
    }
    // SPEC-784: Technical section quality gates
    // Run both detectors against the raw body (includes ## Technical heading)
    if (detectTechnicalPlaceholder(bodyContent)) {
        warnings.push('TECHNICAL_PLACEHOLDER_DETECTED: ## Technical section contains a placeholder pointer ' +
            '(e.g. "See technical.md") — inline the content instead of deferring to another file');
    }
    if (detectTechnicalTooShallow(bodyContent)) {
        warnings.push('TECHNICAL_TOO_SHALLOW: ## Technical section is too short (< 500 chars) and has no ' +
            'src/ or tests/ file paths — add concrete file paths and implementation details');
    }
    // Base points (title present, valid structure)
    const baseScore = 20;
    // When any SPEC-784 technical blocker is present, cap the score at 0 so the
    // approval gate hard-blocks (score < 70 with no-forceApprove path).
    const technicalBlockerCount = warnings.filter((w) => w.startsWith('TECHNICAL_PLACEHOLDER_DETECTED') || w.startsWith('TECHNICAL_TOO_SHALLOW')).length;
    if (technicalBlockerCount > 0) {
        return { score: 0, warnings, criteriaCount };
    }
    const score = Math.min(100, baseScore + criteriaScore + technicalScore);
    return { score, warnings, criteriaCount };
}
export async function checkSpecReadiness(spec, mode, projectHash) {
    const config = loadReadinessConfig(projectHash);
    const vagueWords = config.vagueWords.map((v) => v.word);
    const huRaw = await readHuRaw(spec);
    const huContent = stripFrontmatter(huRaw);
    const fichaContent = await readFichaContent(spec);
    const criteriaLines = extractCriteriaLines(huContent);
    // When a spec uses BDD format, acceptance criteria are stored as `scenarios:` in
    // the YAML frontmatter (multi-line YAML that the simple KV parser cannot handle).
    // If no criteria were found in the body, fall back to counting frontmatter scenarios.
    const scenarioCriteria = criteriaLines.length === 0 ? frontmatterScenarioCriteria(huRaw) : [];
    const effectiveCriteriaLines = scenarioCriteria.length > 0 ? scenarioCriteria : criteriaLines;
    const hu = scoreHuCompleteness(spec);
    const criteria = scoreCriteria(effectiveCriteriaLines, vagueWords);
    const files = scoreFilesIdentified(spec, fichaContent);
    const deps = scoreDependencies(spec);
    const totalScore = hu.points + criteria.points + files.points + deps.points;
    // SPEC-631: EARS gate — hard-block for vague modal verbs ("should/may/could/might") in criteria
    const earsBlockers = runEarsGate(criteriaLines);
    const allBlockers = [
        ...hu.blockers,
        ...criteria.blockers,
        ...files.blockers,
        ...deps.blockers,
        ...earsBlockers,
    ];
    const allWarnings = [...hu.warnings, ...criteria.warnings, ...files.warnings, ...deps.warnings];
    if (mode === 'strict' && spec.scope !== 'trivial' && spec.difficulty >= 3) {
        for (const issue of evaluateImplementationContract(huContent, effectiveCriteriaLines)) {
            allBlockers.push(`${issue.code}: ${issue.message}`);
        }
    }
    // SPEC-732: Executable AC gate — block approved when any scenario lacks a `tests` array.
    // Only enforce when the spec uses frontmatter scenarios (BDD format).
    const bddScenarioCount = countFrontmatterScenarios(huRaw);
    if (bddScenarioCount > 0) {
        const scenariosWithoutTests = findScenariosWithoutTests(huRaw);
        for (const title of scenariosWithoutTests) {
            allBlockers.push(`scenarios_missing_tests: scenario "${title}" has no tests array — add tests entries (SPEC-732)`);
        }
    }
    // SPEC-784: Technical section quality gates — blocker-severity issues
    if (detectTechnicalPlaceholder(huContent)) {
        allBlockers.push('TECHNICAL_PLACEHOLDER_DETECTED: ## Technical section contains a placeholder pointer ' +
            '(e.g. "See technical.md") — inline the content instead of deferring to another file');
    }
    if (detectTechnicalTooShallow(huContent)) {
        allBlockers.push('TECHNICAL_TOO_SHALLOW: ## Technical section is too short (< 500 chars) and has no ' +
            'src/ or tests/ file paths — add concrete file paths and implementation details');
    }
    // SPEC-629: Specificity gate caps score at 60 for difficulty >= 3 specs that lack
    // concrete file paths, function names, or anticipated test breaks.
    const specificityBlockers = checkSpecificityGate(spec, effectiveCriteriaLines, fichaContent);
    allBlockers.push(...specificityBlockers);
    const effectiveScore = specificityBlockers.length > 0 && spec.difficulty >= 3 ? Math.min(totalScore, 60) : totalScore;
    const breakdown = {
        hu: hu.points,
        criteria: criteria.points,
        files: files.points,
        dependencies: deps.points,
    };
    const { thresholds } = config;
    const blockerThreshold = mode === 'strict' ? thresholds.strictBlocker : thresholds.lenientBlocker;
    const cautionThreshold = mode === 'strict' ? thresholds.strictCaution : thresholds.lenientCaution;
    if (effectiveScore < blockerThreshold) {
        allBlockers.push(`ReadinessScore ${String(effectiveScore)} is below ${String(blockerThreshold)} — spec is not ready for implementation`);
    }
    else if (effectiveScore < cautionThreshold) {
        allWarnings.push(`ReadinessScore ${String(effectiveScore)} is below ${String(cautionThreshold)} — proceed with caution`);
    }
    const readinessScore = {
        score: effectiveScore,
        breakdown,
        blockers: allBlockers,
        warnings: allWarnings,
    };
    const isReady = effectiveScore >= cautionThreshold && allBlockers.length === 0;
    const recommendations = buildRecommendations(effectiveScore, hu.points, criteria.points, files.points, deps.points);
    if (specificityBlockers.length > 0) {
        recommendations.push('Add exact file paths (src/ or tests/), function names (e.g. createFoo()), and anticipated test breaks to unlock implementation for this difficulty spec.');
    }
    return {
        specId: spec.id,
        score: effectiveScore,
        ready: isReady,
        issues: readinessScore,
        recommendations,
        mode,
    };
}
//# sourceMappingURL=readiness-checker.js.map