forked from yvv/decision
- Blueprint définitif (docs/dev/BLUEPRINT-V2.md + JSON) : panel 4 visions + 3 juges, corpus doctrinal (Mon nom est personne, Une économie du don, SejeteralO), 4 critiques d'intégration, 3 vérificateurs, réparations - Corpus d'intention + lectures brutes archivés - Moteur porté Python→TS pur : threshold (inertie WoT, Smith, ComTech), nuancé 6 niveaux, DSL modeParams — 45 tests vitest, parité croisée vérifiée (Forgeron W=7224 T=120 ⇒ 94) - Contrat de domaine v2 (types/domain.ts) + lexique UI (lexicon.ts) - vitest + idb-keyval en dépendances Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
110 lines
3.0 KiB
TypeScript
110 lines
3.0 KiB
TypeScript
/**
|
|
* 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<number, string> = {
|
|
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<number, number>
|
|
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<number, number> = {}
|
|
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,
|
|
}
|
|
}
|