// engine/velocity-calculator.ts — Pure velocity metrics calculator (SPEC-288)
// Computes Lead Time, Throughput, Cycle Time, WIP, Flow Efficiency, and Burndown
// from Spec metadata. No storage writes — reads only.
// ---------------------------------------------------------------------------
// ISO Week helpers
// ---------------------------------------------------------------------------
/**
 * Returns the Monday of the ISO week containing the given date.
 */
function getWeekMonday(date) {
    const d = new Date(date);
    const day = d.getUTCDay();
    // getUTCDay: 0=Sunday ... 6=Saturday. ISO week starts Monday.
    const diff = day === 0 ? -6 : 1 - day;
    d.setUTCDate(d.getUTCDate() + diff);
    d.setUTCHours(0, 0, 0, 0);
    return d;
}
/**
 * Returns Sunday (end of week) for a given Monday.
 */
function getWeekSunday(monday) {
    const d = new Date(monday);
    d.setUTCDate(d.getUTCDate() + 6);
    d.setUTCHours(23, 59, 59, 999);
    return d;
}
/**
 * Returns an ISO week label "YYYY-WNN" for a given date.
 */
function isoWeekLabel(date) {
    const monday = getWeekMonday(date);
    // Compute ISO week number
    const jan4 = new Date(Date.UTC(monday.getUTCFullYear(), 0, 4));
    const jan4Monday = getWeekMonday(jan4);
    const weekNum = Math.round((monday.getTime() - jan4Monday.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1;
    const year = monday.getUTCFullYear();
    return `${String(year)}-W${String(weekNum).padStart(2, '0')}`;
}
/**
 * Build a sorted list of week start dates (Mondays) for the last N weeks.
 * The most recent week is the current (or reference) week.
 */
function buildWeekSlots(referenceDate, weeks) {
    const currentMonday = getWeekMonday(referenceDate);
    const slots = [];
    for (let i = weeks - 1; i >= 0; i--) {
        const monday = new Date(currentMonday);
        monday.setUTCDate(monday.getUTCDate() - i * 7);
        slots.push(monday);
    }
    return slots;
}
// ---------------------------------------------------------------------------
// Spec filtering
// ---------------------------------------------------------------------------
function applyFilters(specs, filters) {
    return specs.filter((s) => {
        if (filters.scope !== undefined && s.scope !== filters.scope) {
            return false;
        }
        if (filters.type !== undefined && s.type !== filters.type) {
            return false;
        }
        return true;
    });
}
// ---------------------------------------------------------------------------
// Throughput
// ---------------------------------------------------------------------------
/**
 * Computes weekly throughput (specs done per week) over the given weeks.
 */
export function computeThroughput(specs, filters, referenceDate) {
    const filtered = applyFilters(specs.filter((s) => s.status === 'done'), filters);
    const windowStart = new Date(referenceDate);
    windowStart.setUTCDate(windowStart.getUTCDate() - filters.weeks * 7);
    const weekSlots = buildWeekSlots(referenceDate, filters.weeks);
    return weekSlots.map((monday) => {
        const sunday = getWeekSunday(monday);
        const label = isoWeekLabel(monday);
        const completed = filtered.filter((s) => {
            const done = new Date(s.updatedAt);
            return done >= monday && done <= sunday;
        });
        return {
            weekLabel: label,
            weekStart: monday.toISOString(),
            weekEnd: sunday.toISOString(),
            specsCompleted: completed.length,
            specIds: completed.map((s) => s.id),
        };
    });
}
// ---------------------------------------------------------------------------
// Lead Time
// ---------------------------------------------------------------------------
/**
 * Computes lead time stats (done - created) in hours.
 */
export function computeLeadTime(specs, filters, referenceDate) {
    const windowStart = new Date(referenceDate);
    windowStart.setUTCDate(windowStart.getUTCDate() - filters.weeks * 7);
    const filtered = applyFilters(specs.filter((s) => {
        if (s.status !== 'done') {
            return false;
        }
        const done = new Date(s.updatedAt);
        return done >= windowStart;
    }), filters);
    if (filtered.length === 0) {
        return null;
    }
    const hours = filtered.map((s) => {
        const created = new Date(s.createdAt);
        const done = new Date(s.updatedAt);
        return Math.max(0, (done.getTime() - created.getTime()) / (1000 * 60 * 60));
    });
    const sorted = [...hours].sort((a, b) => a - b);
    const avg = hours.length > 0 ? hours.reduce((acc, h) => acc + h, 0) / hours.length : 0;
    const mid = Math.floor(sorted.length / 2);
    const median = sorted.length % 2 === 0
        ? ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2
        : (sorted[mid] ?? 0);
    // Group by scope
    const scopeGroups = {};
    filtered.forEach((s, i) => {
        const h = hours[i];
        if (h === undefined) {
            return;
        }
        const grp = scopeGroups[s.scope] ?? [];
        grp.push(h);
        scopeGroups[s.scope] = grp;
    });
    const ALL_SCOPES = ['trivial', 'feature', 'cross-module', 'architectural'];
    const byScope = {
        trivial: { avgHours: 0, sampleSize: 0 },
        feature: { avgHours: 0, sampleSize: 0 },
        'cross-module': { avgHours: 0, sampleSize: 0 },
        architectural: { avgHours: 0, sampleSize: 0 },
    };
    for (const scope of ALL_SCOPES) {
        const grp = scopeGroups[scope];
        if (grp && grp.length > 0) {
            byScope[scope] = {
                avgHours: grp.reduce((a, b) => a + b, 0) / grp.length,
                sampleSize: grp.length,
            };
        }
    }
    return {
        avgHours: avg,
        medianHours: median,
        minHours: sorted[0] ?? 0,
        maxHours: sorted[sorted.length - 1] ?? 0,
        sampleSize: filtered.length,
        byScope,
    };
}
// ---------------------------------------------------------------------------
// Cycle Time
// ---------------------------------------------------------------------------
/**
 * Resolve the implementing-start timestamp for cycle time calculation.
 * Priority: statusHistory entry > heuristic midpoint.
 */
function resolveImplementingStart(s, done) {
    const fromHistory = s.statusHistory?.find((h) => h.status === 'implementing')?.changedAt;
    if (fromHistory) {
        return new Date(fromHistory);
    }
    // Heuristic fallback for old specs without statusHistory.
    return new Date((new Date(s.createdAt).getTime() + done.getTime()) / 2);
}
/**
 * Resolve the done timestamp for cycle time calculation.
 * Priority: statusHistory entry > updatedAt.
 */
function resolveDoneAt(s) {
    const fromHistory = s.statusHistory?.find((h) => h.status === 'done')?.changedAt;
    if (fromHistory) {
        return new Date(fromHistory);
    }
    return new Date(s.updatedAt);
}
/**
 * Computes cycle time stats (done - implementing timestamp).
 * Uses real statusHistory timestamps when available (SPEC-529).
 * Falls back to updatedAt for done and heuristic midpoint for implementing start
 * on old specs that pre-date statusHistory tracking.
 */
export function computeCycleTime(specs, filters, referenceDate) {
    const windowStart = new Date(referenceDate);
    windowStart.setUTCDate(windowStart.getUTCDate() - filters.weeks * 7);
    const filtered = applyFilters(specs.filter((s) => {
        if (s.status !== 'done') {
            return false;
        }
        const done = resolveDoneAt(s);
        return done >= windowStart;
    }), filters);
    if (filtered.length === 0) {
        return null;
    }
    const hours = filtered.map((s) => {
        const done = resolveDoneAt(s);
        const implementingStart = resolveImplementingStart(s, done);
        return Math.max(0, (done.getTime() - implementingStart.getTime()) / (1000 * 60 * 60));
    });
    const sorted = [...hours].sort((a, b) => a - b);
    const avg = hours.length > 0 ? hours.reduce((acc, h) => acc + h, 0) / hours.length : 0;
    const mid = Math.floor(sorted.length / 2);
    const median = sorted.length % 2 === 0
        ? ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2
        : (sorted[mid] ?? 0);
    return { avgHours: avg, medianHours: median, sampleSize: filtered.length };
}
// ---------------------------------------------------------------------------
// WIP
// ---------------------------------------------------------------------------
/**
 * Computes current WIP (specs in 'implementing' status).
 */
export function computeWip(specs) {
    const implementing = specs.filter((s) => s.status === 'implementing');
    const now = Date.now();
    const ages = implementing.map((s) => {
        const updated = new Date(s.updatedAt).getTime();
        return Math.max(0, (now - updated) / (1000 * 60 * 60));
    });
    const avgAge = ages.length > 0 ? ages.reduce((a, b) => a + b, 0) / ages.length : 0;
    return {
        current: implementing.length,
        specIds: implementing.map((s) => s.id),
        avgAgeHours: avgAge,
    };
}
// ---------------------------------------------------------------------------
// Flow Efficiency
// ---------------------------------------------------------------------------
/**
 * Estimates flow efficiency as the ratio of cycle time to lead time.
 * Active states: implementing, review.
 * Waiting states: draft, approved.
 */
export function computeFlowEfficiency(specs, filters, referenceDate) {
    const lt = computeLeadTime(specs, filters, referenceDate);
    const ct = computeCycleTime(specs, filters, referenceDate);
    if (lt === null || ct === null || lt.avgHours === 0) {
        return null;
    }
    const efficiency = Math.min(1, ct.avgHours / lt.avgHours);
    return {
        efficiency,
        avgActiveHours: ct.avgHours,
        avgTotalHours: lt.avgHours,
        sampleSize: Math.min(lt.sampleSize, ct.sampleSize),
    };
}
// ---------------------------------------------------------------------------
// Burndown
// ---------------------------------------------------------------------------
/**
 * Computes burndown: specs created vs completed cumulatively per week.
 */
export function computeBurndown(specs, filters, referenceDate) {
    const filtered = applyFilters(specs, filters);
    const weekSlots = buildWeekSlots(referenceDate, filters.weeks);
    const periodStart = weekSlots[0];
    if (!periodStart) {
        return [];
    }
    let cumulativeCreated = 0;
    let cumulativeCompleted = 0;
    return weekSlots.map((monday) => {
        const sunday = getWeekSunday(monday);
        const label = isoWeekLabel(monday);
        const createdThisWeek = filtered.filter((s) => {
            const created = new Date(s.createdAt);
            return created >= monday && created <= sunday;
        }).length;
        const completedThisWeek = filtered.filter((s) => {
            if (s.status !== 'done') {
                return false;
            }
            const done = new Date(s.updatedAt);
            return done >= monday && done <= sunday;
        }).length;
        cumulativeCreated += createdThisWeek;
        cumulativeCompleted += completedThisWeek;
        return {
            weekLabel: label,
            weekStart: monday.toISOString(),
            weekEnd: sunday.toISOString(),
            completedCumulative: cumulativeCompleted,
            completedThisWeek,
            createdCumulative: cumulativeCreated,
            backlog: cumulativeCreated - cumulativeCompleted,
        };
    });
}
// ---------------------------------------------------------------------------
// Summary text
// ---------------------------------------------------------------------------
function buildSummaryText(avgThroughput, leadTime, wip, totalDone, weeks) {
    const parts = [];
    parts.push(`Velocity: ${avgThroughput.toFixed(1)} specs/week (${String(weeks)}w window)`);
    if (leadTime !== null) {
        const days = (leadTime.avgHours / 24).toFixed(1);
        parts.push(`Lead time: ${days} days avg`);
    }
    parts.push(`WIP: ${String(wip.current)}`);
    parts.push(`Total done: ${String(totalDone)}`);
    return parts.join(' | ');
}
// ---------------------------------------------------------------------------
// Main report builder
// ---------------------------------------------------------------------------
/**
 * Builds a full VelocityReport from a list of specs.
 * Pure function — no I/O.
 */
export function buildVelocityReport(specs, projectId, projectPath, filters, referenceDate = new Date()) {
    const windowStart = new Date(referenceDate);
    windowStart.setUTCDate(windowStart.getUTCDate() - filters.weeks * 7);
    const filteredDone = applyFilters(specs.filter((s) => s.status === 'done' && new Date(s.updatedAt) >= windowStart), filters);
    const throughput = computeThroughput(specs, filters, referenceDate);
    const totalDone = filteredDone.length;
    const nonZeroWeeks = throughput.filter((w) => w.specsCompleted > 0).length;
    const avgThroughput = throughput.length > 0
        ? throughput.reduce((acc, w) => acc + w.specsCompleted, 0) / throughput.length
        : 0;
    const leadTime = computeLeadTime(specs, filters, referenceDate);
    const cycleTime = computeCycleTime(specs, filters, referenceDate);
    const wip = computeWip(applyFilters(specs, filters));
    const flowEfficiency = computeFlowEfficiency(specs, filters, referenceDate);
    const burndown = computeBurndown(specs, filters, referenceDate);
    const summary = buildSummaryText(avgThroughput, leadTime, wip, totalDone, filters.weeks);
    void nonZeroWeeks; // computed but only used implicitly
    return {
        projectId,
        projectPath,
        generatedAt: referenceDate.toISOString(),
        weeks: filters.weeks,
        filters,
        avgThroughputPerWeek: avgThroughput,
        throughput,
        leadTime,
        cycleTime,
        wip,
        flowEfficiency,
        burndown,
        summary,
        totalDone,
        totalSpecs: specs.length,
    };
}
// ---------------------------------------------------------------------------
// Trend builder
// ---------------------------------------------------------------------------
function buildPeriodStats(specs, filters, periodStart, periodEnd, label) {
    const periodFilters = { ...filters, weeks: filters.weeks };
    const done = applyFilters(specs.filter((s) => {
        if (s.status !== 'done') {
            return false;
        }
        const doneDate = new Date(s.updatedAt);
        return doneDate >= periodStart && doneDate <= periodEnd;
    }), periodFilters);
    const weeksCount = Math.ceil((periodEnd.getTime() - periodStart.getTime()) / (7 * 24 * 60 * 60 * 1000));
    const avgThroughput = weeksCount > 0 ? done.length / weeksCount : 0;
    // Lead time for this period
    let avgLeadTimeHours = null;
    if (done.length > 0) {
        const hours = done.map((s) => {
            const created = new Date(s.createdAt).getTime();
            const doneTs = new Date(s.updatedAt).getTime();
            return Math.max(0, (doneTs - created) / (1000 * 60 * 60));
        });
        avgLeadTimeHours = hours.reduce((a, b) => a + b, 0) / hours.length;
    }
    return {
        label,
        fromDate: periodStart.toISOString(),
        toDate: periodEnd.toISOString(),
        avgThroughputPerWeek: avgThroughput,
        avgLeadTimeHours,
        totalDone: done.length,
    };
}
/**
 * Builds a trend comparison: last N weeks vs previous N weeks.
 */
export function buildVelocityTrend(specs, projectId, projectPath, filters, compareWeeks, referenceDate = new Date()) {
    const recentEnd = referenceDate;
    const recentStart = new Date(recentEnd);
    recentStart.setUTCDate(recentStart.getUTCDate() - compareWeeks * 7);
    const previousEnd = new Date(recentStart);
    previousEnd.setUTCMilliseconds(previousEnd.getUTCMilliseconds() - 1);
    const previousStart = new Date(previousEnd);
    previousStart.setUTCDate(previousStart.getUTCDate() - compareWeeks * 7);
    const recent = buildPeriodStats(specs, filters, recentStart, recentEnd, `Last ${String(compareWeeks)} weeks`);
    const previous = buildPeriodStats(specs, filters, previousStart, previousEnd, `Previous ${String(compareWeeks)} weeks`);
    const throughputDelta = recent.avgThroughputPerWeek - previous.avgThroughputPerWeek;
    const leadTimeDelta = recent.avgLeadTimeHours !== null && previous.avgLeadTimeHours !== null
        ? recent.avgLeadTimeHours - previous.avgLeadTimeHours
        : null;
    let trend;
    if (throughputDelta > 0.5) {
        trend = 'improving';
    }
    else if (throughputDelta < -0.5) {
        trend = 'declining';
    }
    else {
        trend = 'stable';
    }
    const trendEmoji = trend === 'improving' ? 'up' : trend === 'declining' ? 'down' : 'flat';
    const summary = [
        `Trend: ${trend} (${trendEmoji})`,
        `Recent: ${recent.avgThroughputPerWeek.toFixed(1)} specs/week`,
        `Previous: ${previous.avgThroughputPerWeek.toFixed(1)} specs/week`,
        leadTimeDelta !== null
            ? `Lead time delta: ${leadTimeDelta > 0 ? '+' : ''}${(leadTimeDelta / 24).toFixed(1)} days`
            : '',
    ]
        .filter(Boolean)
        .join(' | ');
    return {
        projectId,
        projectPath,
        generatedAt: referenceDate.toISOString(),
        compareWeeks,
        recent,
        previous,
        throughputDelta,
        leadTimeDelta,
        trend,
        summary,
    };
}
//# sourceMappingURL=velocity-calculator.js.map