/**
 * Key Result Types for OKR Closed-Loop Execution
 *
 * Defines structured, measurable Key Results that attach to Missions.
 * Each KR tracks a quantifiable target with baseline/current/target values,
 * measurement history, and linked WorkItems.
 *
 * @module types/v2/key-result
 */
import { randomUUID } from 'crypto';
/** All valid KR metric types. */
export const KR_METRIC_TYPES = [
    'number',
    'percentage',
    'boolean',
    'currency',
];
/** All valid KR statuses. */
export const KR_STATUSES = [
    'not_started',
    'on_track',
    'at_risk',
    'off_track',
    'achieved',
];
/** All valid measurement sources. */
export const KR_MEASUREMENT_SOURCES = [
    'manual',
    'task_completion',
    'skill_output',
];
// ---------------------------------------------------------------------------
// Validators
// ---------------------------------------------------------------------------
/**
 * Check if a value is a valid KRMetricType.
 */
export function isValidKRMetricType(value) {
    return typeof value === 'string' && KR_METRIC_TYPES.includes(value);
}
/**
 * Check if a value is a valid KRStatus.
 */
export function isValidKRStatus(value) {
    return typeof value === 'string' && KR_STATUSES.includes(value);
}
/**
 * Check if a value is a valid KRMeasurementSource.
 */
export function isValidKRMeasurementSource(value) {
    return typeof value === 'string' && KR_MEASUREMENT_SOURCES.includes(value);
}
/**
 * Validate a CreateKeyResultInput.
 *
 * @returns Array of validation error messages (empty if valid)
 */
export function validateCreateKeyResultInput(input) {
    const errors = [];
    if (!input || typeof input !== 'object')
        return ['Input must be an object'];
    const obj = input;
    if (!obj.missionId || typeof obj.missionId !== 'string')
        errors.push('missionId is required');
    if (!obj.title || typeof obj.title !== 'string')
        errors.push('title is required');
    if (!isValidKRMetricType(obj.metricType))
        errors.push(`metricType must be one of: ${KR_METRIC_TYPES.join(', ')}`);
    if (typeof obj.baseline !== 'number')
        errors.push('baseline must be a number');
    if (typeof obj.target !== 'number')
        errors.push('target must be a number');
    if (!obj.unit || typeof obj.unit !== 'string')
        errors.push('unit is required');
    if (obj.baseline === obj.target)
        errors.push('baseline and target must be different');
    if (obj.measurementSource !== undefined && !isValidKRMeasurementSource(obj.measurementSource)) {
        errors.push(`measurementSource must be one of: ${KR_MEASUREMENT_SOURCES.join(', ')}`);
    }
    return errors;
}
/**
 * Type guard for KeyResult.
 */
export function isKeyResult(value) {
    if (!value || typeof value !== 'object')
        return false;
    const obj = value;
    return (typeof obj.id === 'string' &&
        typeof obj.missionId === 'string' &&
        typeof obj.title === 'string' &&
        isValidKRMetricType(obj.metricType) &&
        typeof obj.baseline === 'number' &&
        typeof obj.target === 'number' &&
        typeof obj.current === 'number' &&
        typeof obj.unit === 'string' &&
        isValidKRStatus(obj.status) &&
        Array.isArray(obj.linkedWorkItemIds) &&
        Array.isArray(obj.measurements));
}
// ---------------------------------------------------------------------------
// Factory & Computation
// ---------------------------------------------------------------------------
/** Maximum measurement history entries to retain per KR. */
export const MAX_MEASUREMENT_HISTORY = 50;
/**
 * Compute the progress percentage for a Key Result.
 *
 * Works for both "higher is better" (target > baseline) and
 * "lower is better" (target < baseline) metrics.
 *
 * @returns Progress as 0-100 (clamped)
 */
export function computeKRProgress(kr) {
    const range = kr.target - kr.baseline;
    if (range === 0)
        return 100;
    const progress = ((kr.current - kr.baseline) / range) * 100;
    return Math.max(0, Math.min(100, progress));
}
/**
 * Progress thresholds (0-100) that delimit the KR status bands. A progress at or
 * above a threshold maps to the associated status (checked high → low).
 */
export const KR_STATUS_THRESHOLDS = {
    /** At/above this progress the KR is fully achieved. */
    ACHIEVED: 100,
    /** At/above this progress the KR is healthy / on track. */
    ON_TRACK: 50,
    /** At/above this progress the KR is at risk (but not off track). */
    AT_RISK: 25,
};
/**
 * Derive KR status from progress percentage.
 *
 * - achieved: progress >= 100%
 * - on_track: progress >= 50%
 * - at_risk: progress >= 25%
 * - off_track: progress > 0%
 * - not_started: progress === 0% and current === baseline
 */
export function deriveKRStatus(progress, current, baseline) {
    if (progress >= KR_STATUS_THRESHOLDS.ACHIEVED)
        return 'achieved';
    if (current === baseline)
        return 'not_started';
    if (progress >= KR_STATUS_THRESHOLDS.ON_TRACK)
        return 'on_track';
    if (progress >= KR_STATUS_THRESHOLDS.AT_RISK)
        return 'at_risk';
    return 'off_track';
}
/**
 * Map a cascade child's rolled-up progress to a single synthetic KR status used
 * to fold the child into its parent's roll-up recommendation.
 *
 * Unlike {@link deriveKRStatus}, this mapper deliberately has NO
 * `current === baseline` short-circuit: a child stalled at `0` progress is a
 * genuine "in trouble" signal (`off_track`), NOT `not_started`. Treating an
 * all-children-at-zero parent as `not_started` would suppress the
 * escalate/replan recommendation the cascade is meant to surface (spec §4.2).
 *
 * Banding mirrors {@link deriveKRStatus}:
 * - achieved: progress >= 100%
 * - on_track: progress >= 50%
 * - at_risk: progress >= 25%
 * - off_track: progress < 25% (including exactly 0%)
 *
 * @param rolledUpProgress - The child's rolled-up progress (0-100)
 * @returns The synthetic {@link KRStatus} for the child
 *
 * @example
 * ```ts
 * deriveCascadeChildStatus(0);  // 'off_track' (a stalled child escalates)
 * deriveCascadeChildStatus(75); // 'on_track'
 * ```
 */
export function deriveCascadeChildStatus(rolledUpProgress) {
    if (rolledUpProgress >= KR_STATUS_THRESHOLDS.ACHIEVED)
        return 'achieved';
    if (rolledUpProgress >= KR_STATUS_THRESHOLDS.ON_TRACK)
        return 'on_track';
    if (rolledUpProgress >= KR_STATUS_THRESHOLDS.AT_RISK)
        return 'at_risk';
    return 'off_track';
}
/**
 * Compute an equal-weight rolled-up progress for a cascade node.
 *
 * v1 weighting is intentionally equal-weight: the node's own progress counts
 * the same as each approved child's rolled-up progress. Future ownership /
 * complexity weighting is a documented hook (spec §4.2) — pass per-input
 * weights here when that lands; do NOT implement weighting now.
 *
 * @param ownProgress - This mission's own `overallProgress` (0-100)
 * @param childProgresses - Each approved child's `rolledUpProgress` (0-100)
 * @returns Equal-weight average rounded to an integer 0-100; for a leaf
 *   (no children) this equals `ownProgress` (rounded)
 *
 * @example
 * ```ts
 * computeRolledUpProgress(40, [80, 60]); // round((40 + 80 + 60) / 3) = 60
 * computeRolledUpProgress(40, []);       // 40 (leaf)
 * ```
 */
export function computeRolledUpProgress(ownProgress, childProgresses) {
    const values = [ownProgress, ...childProgresses];
    const total = values.reduce((sum, v) => sum + v, 0);
    return Math.round(total / values.length);
}
/**
 * Create a new KeyResult from validated input.
 */
export function createKeyResult(input) {
    const now = new Date().toISOString();
    return {
        id: randomUUID(),
        missionId: input.missionId,
        title: input.title,
        metricType: input.metricType,
        baseline: input.baseline,
        target: input.target,
        current: input.baseline,
        unit: input.unit,
        status: 'not_started',
        measurementSource: input.measurementSource ?? 'manual',
        measurementConfig: input.measurementConfig,
        linkedWorkItemIds: [],
        measurements: [],
        createdAt: now,
        updatedAt: now,
    };
}
/**
 * Derive the OKR recommendation from KR status distribution.
 *
 * @param statuses - Array of KR statuses for a mission
 * @param staleCycles - Number of consecutive review cycles with no progress
 * @returns Recommendation for next action
 */
export function deriveOKRRecommendation(statuses, staleCycles = 0) {
    if (statuses.length === 0)
        return 'continue';
    if (staleCycles >= 2)
        return 'escalate';
    const counts = {
        not_started: 0,
        on_track: 0,
        at_risk: 0,
        off_track: 0,
        achieved: 0,
    };
    for (const s of statuses)
        counts[s]++;
    const total = statuses.length;
    const healthyRatio = (counts.on_track + counts.achieved) / total;
    const offTrackRatio = counts.off_track / total;
    if (healthyRatio >= 0.6)
        return 'continue';
    if (offTrackRatio > 0.4)
        return 'replan';
    return 'adjust_strategy';
}
//# sourceMappingURL=key-result.types.js.map