// Estimation Reconciler (SPEC-182)
// Closes the loop: estimate_ai_cost prediction vs actual token usage from ledger
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const ACCURATE_THRESHOLD_PCT = 15;
const GRADE_EXCELLENT = 90;
const GRADE_GOOD = 75;
const GRADE_FAIR = 50;
const CALIBRATION_RECENT_WEIGHT = 2;
// ---------------------------------------------------------------------------
// Core functions
// ---------------------------------------------------------------------------
/**
 * Aggregates ledger entries into actual token/cost totals.
 */
function aggregateEntries(entries) {
    if (entries.length === 0) {
        return {
            inputTokens: 0,
            outputTokens: 0,
            totalTokens: 0,
            costUsd: 0,
            models: [],
            callCount: 0,
        };
    }
    let inputTokens = 0;
    let outputTokens = 0;
    let costUsd = 0;
    const modelSet = new Set();
    for (const entry of entries) {
        inputTokens += entry.inputTokens;
        outputTokens += entry.outputTokens;
        costUsd += entry.costUsd;
        modelSet.add(entry.model);
    }
    return {
        inputTokens,
        outputTokens,
        totalTokens: inputTokens + outputTokens,
        costUsd,
        models: [...modelSet],
        callCount: entries.length,
    };
}
/**
 * Computes deviation percent: (estimated - actual) / actual * 100.
 * Returns 0 when actual is 0 to avoid division by zero.
 */
function computeDeviationPercent(estimated, actual) {
    if (actual === 0) {
        return 0;
    }
    return ((estimated - actual) / actual) * 100;
}
/**
 * Converts deviation percent to accuracy: max(0, 100 - abs(deviation)).
 */
function deviationToAccuracy(deviationPct) {
    return Math.max(0, 100 - Math.abs(deviationPct));
}
/**
 * Maps accuracy value to a grade label.
 */
function gradeAccuracy(accuracy) {
    if (accuracy >= GRADE_EXCELLENT) {
        return 'excellent';
    }
    if (accuracy >= GRADE_GOOD) {
        return 'good';
    }
    if (accuracy >= GRADE_FAIR) {
        return 'fair';
    }
    return 'poor';
}
/**
 * Maps deviation percent to a direction label.
 */
function computeDirection(deviationPct) {
    if (Math.abs(deviationPct) <= ACCURATE_THRESHOLD_PCT) {
        return 'accurate';
    }
    return deviationPct > 0 ? 'over' : 'under';
}
/**
 * Generates a human-readable calibration suggestion from deviation info.
 */
function buildCalibrationSuggestion(direction, tokenDeviationPct) {
    if (direction === 'accurate') {
        return undefined;
    }
    const absPct = Math.abs(tokenDeviationPct).toFixed(1);
    if (direction === 'over') {
        return (`Estimates are running ${absPct}% high. ` +
            `Consider reducing future token estimates by ~${absPct}% for specs of similar size.`);
    }
    return (`Estimates are running ${absPct}% low. ` +
        `Consider increasing future token estimates by ~${absPct}% for specs of similar size.`);
}
// ---------------------------------------------------------------------------
// Exported functions
// ---------------------------------------------------------------------------
/**
 * Pure reconciliation: compares a pre-implementation estimate against actual
 * ledger entries to produce deviation, accuracy, and calibration data.
 */
export function reconcile(input) {
    const estimatedTotal = input.estimatedInputTokens + input.estimatedOutputTokens;
    const actual = aggregateEntries(input.actualEntries);
    const tokenDeviationPct = computeDeviationPercent(estimatedTotal, actual.totalTokens);
    const costDeviationPct = computeDeviationPercent(input.estimatedCostUsd, actual.costUsd);
    const tokenAccuracy = deviationToAccuracy(tokenDeviationPct);
    const costAccuracy = deviationToAccuracy(costDeviationPct);
    const direction = computeDirection(tokenDeviationPct);
    return {
        specId: input.specId,
        estimated: {
            inputTokens: input.estimatedInputTokens,
            outputTokens: input.estimatedOutputTokens,
            totalTokens: estimatedTotal,
            costUsd: input.estimatedCostUsd,
            model: input.estimatedModel,
        },
        actual,
        deviation: {
            tokenPercent: tokenDeviationPct,
            costPercent: costDeviationPct,
            absoluteTokenDiff: estimatedTotal - actual.totalTokens,
            absoluteCostDiff: input.estimatedCostUsd - actual.costUsd,
            direction,
        },
        accuracy: {
            tokenAccuracy,
            costAccuracy,
            grade: gradeAccuracy(tokenAccuracy),
        },
        calibrationSuggestion: buildCalibrationSuggestion(direction, tokenDeviationPct),
    };
}
/**
 * Formats a ReconciliationResult as a markdown report with visual indicators.
 */
export function formatReconciliation(result) {
    const gradeIcon = result.accuracy.grade === 'excellent'
        ? '✅'
        : result.accuracy.grade === 'good'
            ? '✅'
            : result.accuracy.grade === 'fair'
                ? '⚠️'
                : '❌';
    const directionLabel = result.deviation.direction === 'accurate'
        ? '✅ Within 15% (accurate)'
        : result.deviation.direction === 'over'
            ? `⚠️ Over-estimated by ${result.deviation.tokenPercent.toFixed(1)}%`
            : `❌ Under-estimated by ${Math.abs(result.deviation.tokenPercent).toFixed(1)}%`;
    const lines = [
        `## Estimation Reconciliation — ${result.specId}`,
        '',
        '### Token Summary',
        `| Metric | Estimated | Actual | Deviation |`,
        `|--------|-----------|--------|-----------|`,
        `| Input tokens | ${String(result.estimated.inputTokens)} | ${String(result.actual.inputTokens)} | — |`,
        `| Output tokens | ${String(result.estimated.outputTokens)} | ${String(result.actual.outputTokens)} | — |`,
        `| Total tokens | ${String(result.estimated.totalTokens)} | ${String(result.actual.totalTokens)} | ${result.deviation.tokenPercent.toFixed(1)}% |`,
        `| Cost (USD) | $${result.estimated.costUsd.toFixed(6)} | $${result.actual.costUsd.toFixed(6)} | ${result.deviation.costPercent.toFixed(1)}% |`,
        '',
        '### Accuracy',
        `${gradeIcon} Grade: **${result.accuracy.grade.toUpperCase()}**`,
        `- Token accuracy: ${result.accuracy.tokenAccuracy.toFixed(1)}%`,
        `- Cost accuracy: ${result.accuracy.costAccuracy.toFixed(1)}%`,
        `- Direction: ${directionLabel}`,
        '',
        '### Actual Usage Details',
        `- API calls: ${String(result.actual.callCount)}`,
        `- Models used: ${result.actual.models.join(', ') || '—'}`,
    ];
    if (result.calibrationSuggestion) {
        lines.push('', '### Calibration Suggestion', result.calibrationSuggestion);
    }
    return lines.join('\n');
}
/**
 * Extracts calibration data points from multiple reconciliation results.
 */
export function buildCalibrationData(results) {
    return results.map((r) => ({
        specId: r.specId,
        timestamp: new Date().toISOString(),
        estimatedTokens: r.estimated.totalTokens,
        actualTokens: r.actual.totalTokens,
        ratio: r.estimated.totalTokens > 0 ? r.actual.totalTokens / r.estimated.totalTokens : 1,
    }));
}
/**
 * Computes a weighted average calibration factor from historical data.
 * Recent entries (second half of the array) are weighted 2x.
 * Returns the factor to multiply future estimates by (>1 = need to increase estimates).
 */
export function suggestCalibrationFactor(calibrations) {
    if (calibrations.length === 0) {
        return 1;
    }
    const midpoint = Math.floor(calibrations.length / 2);
    let weightedSum = 0;
    let totalWeight = 0;
    for (let i = 0; i < calibrations.length; i++) {
        const weight = i >= midpoint ? CALIBRATION_RECENT_WEIGHT : 1;
        weightedSum += (calibrations[i]?.ratio ?? 1) * weight;
        totalWeight += weight;
    }
    return weightedSum / totalWeight;
}
//# sourceMappingURL=reconciler.js.map