/** * Six-level nuanced vote evaluation. * * Exact TypeScript port of backend/app/engine/nuanced_vote.py * (Python is the reference implementation). * * Levels: * 0 - CONTRE * 1 - PAS DU TOUT * 2 - PAS D'ACCORD * 3 - NEUTRE * 4 - D'ACCORD * 5 - TOUT A FAIT * * Adoption rule: * The sum of votes at levels 3 + 4 + 5 must be >= thresholdPct% of total votes. * A minimum number of participants is also required. */ export const LEVEL_LABELS: Record = { 0: 'CONTRE', 1: 'PAS DU TOUT', 2: "PAS D'ACCORD", 3: 'NEUTRE', 4: "D'ACCORD", 5: 'TOUT A FAIT', } export const NUM_LEVELS = 6 /** Result shape mirrors the Python dict returned by evaluate_nuanced(). */ export interface NuancedResult { total: number per_level_counts: Record positive_count: number positive_pct: number threshold_met: boolean min_participants_met: boolean adopted: boolean } /** * Round to 2 decimals with ties-to-even, mirroring Python's round(x, 2). */ function round2(value: number): number { const scaled = value * 100 const floor = Math.floor(scaled) const diff = scaled - floor let rounded: number if (diff > 0.5) { rounded = floor + 1 } else if (diff < 0.5) { rounded = floor } else { rounded = floor % 2 === 0 ? floor : floor + 1 } return rounded / 100 } /** * Evaluate a nuanced vote from a list of individual vote levels. * * @param votes - List of vote levels (each 0-5), one entry per voter * @param thresholdPct - Minimum percentage of positive votes (levels 3-5) for adoption * @param minParticipants - Minimum number of participants required for validity * @returns Aggregated result (counts, percentage, adoption flags) * @throws Error if any vote value is outside the 0-5 integer range */ export function nuancedResult( votes: number[], thresholdPct: number = 80, minParticipants: number = 59, ): NuancedResult { // Validate vote levels (integer check added: JS has no KeyError safety net) for (const v of votes) { if (!Number.isInteger(v) || v < 0 || v > 5) { throw new Error(`Niveau de vote invalide : ${v}. Les niveaux valides sont 0-5.`) } } const total = votes.length const perLevelCounts: Record = {} for (let level = 0; level < NUM_LEVELS; level++) { perLevelCounts[level] = 0 } for (const v of votes) { perLevelCounts[v] = (perLevelCounts[v] ?? 0) + 1 } // Positive = levels 3 (NEUTRE), 4 (D'ACCORD), 5 (TOUT A FAIT) const positiveCount = (perLevelCounts[3] ?? 0) + (perLevelCounts[4] ?? 0) + (perLevelCounts[5] ?? 0) const positivePct = total > 0 ? (positiveCount / total) * 100.0 : 0.0 const thresholdMet = positivePct >= thresholdPct const minParticipantsMet = total >= minParticipants const adopted = thresholdMet && minParticipantsMet return { total, per_level_counts: perLevelCounts, positive_count: positiveCount, positive_pct: round2(positivePct), threshold_met: thresholdMet, min_participants_met: minParticipantsMet, adopted, } }