// engine/code-health-signals.ts — SPEC-640: Code health signals for model routing
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
const PUBLIC_API_MARKERS = [
    'src/tools/',
    'src/core/',
    'src/index.ts',
    '/handler.ts',
    '/schema.ts',
    'api.',
];
function isPublicApiFile(filePath) {
    return PUBLIC_API_MARKERS.some((m) => filePath.includes(m));
}
/** Approximate cyclomatic complexity by counting decision points in source. */
function approximateCyclomaticComplexity(source) {
    // Count: if, else if, for, while, switch case, ??, &&, ||, ternary ?
    const decisions = source.match(/\b(if|else if|for|while|case|catch)\b|\?\?|&&|\|\||(?<!\?)\?(?!\?)/g);
    return 1 + (decisions?.length ?? 0);
}
/** Approximate caller count by looking for the file's exported function names in the source tree. */
async function estimateCallerCount(projectPath, _filePath, _source) {
    // Without full AST analysis, return a conservative 0 — real implementation
    // would need a codebase-wide grep. This is a stub that returns 0 for unknown.
    try {
        const { execSync } = await import('node:child_process');
        const result = execSync(`git -C "${projectPath}" diff --name-only HEAD~1 HEAD 2>/dev/null || echo ""`, {
            encoding: 'utf-8',
            timeout: 3000,
        });
        // Approximate: more changed files in last commit = higher impact
        return result.trim().split('\n').filter(Boolean).length;
    }
    catch {
        return 0;
    }
}
/**
 * SPEC-640: Compute code health signals for a file.
 * Returns default signals when file cannot be read.
 */
export async function computeHealthSignals(projectPath, filePath, coveragePercent = null) {
    let source = '';
    try {
        source = await readFile(join(projectPath, filePath), 'utf-8');
    }
    catch {
        // file doesn't exist yet (new file) — low complexity
    }
    const cyclomaticComplexity = source ? approximateCyclomaticComplexity(source) : 1;
    const callerCount = await estimateCallerCount(projectPath, filePath, source);
    const isPublicApi = isPublicApiFile(filePath);
    return { cyclomaticComplexity, coveragePercent, callerCount, isPublicApi };
}
/** SPEC-640: Upgrade model one tier based on health signals. */
function bumpTier(model) {
    if (model === 'haiku') {
        return 'sonnet';
    }
    if (model === 'sonnet') {
        return 'opus';
    }
    return 'opus';
}
/**
 * SPEC-640: Adjust a model route based on code health signals.
 * Returns the adjusted model and a rationale explaining why.
 */
export function adjustModelForHealth(original, signals) {
    const reasons = [];
    let adjusted = original;
    // Rule 1: High cyclomatic complexity → never haiku
    if (signals.cyclomaticComplexity > 15 && adjusted === 'haiku') {
        adjusted = 'sonnet';
        reasons.push(`complexity ${signals.cyclomaticComplexity} > 15 → sonnet min`);
    }
    // Rule 2: Low coverage → bump one tier
    if (signals.coveragePercent !== null && signals.coveragePercent < 60) {
        const bumped = bumpTier(adjusted);
        if (bumped !== adjusted) {
            reasons.push(`coverage ${signals.coveragePercent}% < 60% → bumped ${adjusted} → ${bumped}`);
            adjusted = bumped;
        }
    }
    // Rule 3: Many callers → always opus
    if (signals.callerCount > 10 && adjusted !== 'opus') {
        reasons.push(`${signals.callerCount} callers > 10 → opus`);
        adjusted = 'opus';
    }
    // Rule 4: Public API surface → minimum sonnet
    if (signals.isPublicApi && adjusted === 'haiku') {
        adjusted = 'sonnet';
        reasons.push('public API surface → sonnet min');
    }
    return {
        originalModel: original,
        adjustedModel: adjusted,
        rationale: reasons.length > 0 ? reasons.join('; ') : 'no health adjustment needed',
    };
}
//# sourceMappingURL=code-health-signals.js.map