// engine/observatory-engine.ts — SPEC-572: Planu Observatory local insights engine
//
// Derives aggregate patterns from spec actuals.
// PRIVACY: never includes file paths, spec titles, or user data — only aggregate numbers.
// The tool layer (observatory-insights.ts) is responsible for loading actuals from storage.
const MIN_SAMPLE_VARIANCE = 3;
const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Compute local insights from pre-loaded actuals data.
 * Accepts an array of ActualWithScope objects (PII-free aggregates).
 */
export function computeLocalInsights(actuals) {
    if (actuals.length === 0) {
        return [];
    }
    const insights = [];
    const estimationInsights = computeEstimationInsights(actuals);
    insights.push(...estimationInsights);
    return insights;
}
/**
 * Compute velocity insight when multiple projects are involved.
 * Returns null when insufficient data is available.
 */
export function computeVelocityInsight(actuals, projectCount) {
    if (projectCount < 2 || actuals.length < MIN_SAMPLE_VARIANCE) {
        return null;
    }
    const avgHours = actuals.reduce((s, a) => s + a.actualHours, 0) / actuals.length;
    const rounded = Math.round(avgHours * 10) / 10;
    return {
        pattern: `Average spec takes ${rounded}h to complete across ${projectCount} projects`,
        confidence: actuals.length >= 10 ? 'high' : 'medium',
        sampleSize: actuals.length,
        category: 'velocity',
    };
}
/**
 * Returns true if the given ISO timestamp is less than 24 hours old.
 */
export function isCacheValid(fetchedAt) {
    const fetched = new Date(fetchedAt).getTime();
    const now = Date.now();
    return now - fetched < CACHE_MAX_AGE_MS;
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
function computeEstimationInsights(actuals) {
    const insights = [];
    // Overall bias
    if (actuals.length >= MIN_SAMPLE_VARIANCE) {
        const biases = actuals.map((a) => (a.actualHours - a.estimatedHours) / a.estimatedHours);
        const avgBias = biases.reduce((s, b) => s + b, 0) / biases.length;
        if (Math.abs(avgBias) >= 0.1) {
            const direction = avgBias > 0 ? 'underestimated' : 'overestimated';
            const pct = Math.round(Math.abs(avgBias) * 100);
            const confidence = biases.length >= 10 ? 'high' : biases.length >= 5 ? 'medium' : 'low';
            insights.push({
                pattern: `Specs are ${direction} by ${pct}% on average`,
                confidence,
                sampleSize: biases.length,
                category: 'estimation',
            });
        }
    }
    // Per-scope insights
    const scopeMap = groupByScope(actuals);
    for (const [scope, scopeActuals] of scopeMap.entries()) {
        if (scopeActuals.length < MIN_SAMPLE_VARIANCE) {
            continue;
        }
        const biases = scopeActuals.map((a) => (a.actualHours - a.estimatedHours) / a.estimatedHours);
        const avgBias = biases.reduce((s, b) => s + b, 0) / biases.length;
        if (Math.abs(avgBias) >= 0.15) {
            const direction = avgBias > 0 ? 'underestimated' : 'overestimated';
            const pct = Math.round(Math.abs(avgBias) * 100);
            insights.push({
                pattern: `${scope} scope specs are ${direction} by ${pct}%`,
                confidence: scopeActuals.length >= 5 ? 'medium' : 'low',
                sampleSize: scopeActuals.length,
                category: 'estimation',
            });
        }
    }
    return insights;
}
function groupByScope(actuals) {
    const map = new Map();
    for (const actual of actuals) {
        const key = actual.scope;
        const existing = map.get(key);
        if (existing !== undefined) {
            existing.push(actual);
        }
        else {
            map.set(key, [actual]);
        }
    }
    return map;
}
//# sourceMappingURL=observatory-engine.js.map