Sprint 1 : scaffolding complet de Glibredecision
Plateforme de decisions collectives pour Duniter/G1. Backend FastAPI async + PostgreSQL (14 tables, 8 routers, 6 services, moteur de vote avec formule d'inertie WoT/Smith/TechComm). Frontend Nuxt 4 + Nuxt UI v3 + Pinia (9 pages, 5 stores). Infrastructure Docker + Woodpecker CI + Traefik. Documentation technique et utilisateur (15 fichiers). Seed : Licence G1, Engagement Forgeron v2.0.0, 4 protocoles de vote. 30 tests unitaires (formules, mode params, vote nuance) -- tous verts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
95
backend/app/engine/nuanced_vote.py
Normal file
95
backend/app/engine/nuanced_vote.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Six-level nuanced vote evaluation.
|
||||
|
||||
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 >= threshold_pct% of total votes.
|
||||
A minimum number of participants is also required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
LEVEL_LABELS: dict[int, str] = {
|
||||
0: "CONTRE",
|
||||
1: "PAS DU TOUT",
|
||||
2: "PAS D'ACCORD",
|
||||
3: "NEUTRE",
|
||||
4: "D'ACCORD",
|
||||
5: "TOUT A FAIT",
|
||||
}
|
||||
|
||||
NUM_LEVELS = 6
|
||||
|
||||
|
||||
def evaluate_nuanced(
|
||||
votes: list[int],
|
||||
threshold_pct: int = 80,
|
||||
min_participants: int = 59,
|
||||
) -> dict:
|
||||
"""Evaluate a nuanced vote from a list of individual vote levels.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
votes:
|
||||
List of vote levels (each 0-5). One entry per voter.
|
||||
threshold_pct:
|
||||
Minimum percentage of positive votes (levels 3-5) out of total
|
||||
for adoption.
|
||||
min_participants:
|
||||
Minimum number of participants required for validity.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
{
|
||||
"total": int,
|
||||
"per_level_counts": {0: int, 1: int, ..., 5: int},
|
||||
"positive_count": int, # levels 3 + 4 + 5
|
||||
"positive_pct": float, # 0.0 - 100.0
|
||||
"threshold_met": bool,
|
||||
"min_participants_met": bool,
|
||||
"adopted": bool,
|
||||
}
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If any vote value is outside the 0-5 range.
|
||||
"""
|
||||
# Validate vote levels
|
||||
for v in votes:
|
||||
if v < 0 or v > 5:
|
||||
raise ValueError(
|
||||
f"Niveau de vote invalide : {v}. Les niveaux valides sont 0-5."
|
||||
)
|
||||
|
||||
total = len(votes)
|
||||
|
||||
per_level_counts: dict[int, int] = {level: 0 for level in range(NUM_LEVELS)}
|
||||
for v in votes:
|
||||
per_level_counts[v] += 1
|
||||
|
||||
# Positive = levels 3 (NEUTRE), 4 (D'ACCORD), 5 (TOUT A FAIT)
|
||||
positive_count = per_level_counts[3] + per_level_counts[4] + per_level_counts[5]
|
||||
|
||||
positive_pct = (positive_count / total * 100.0) if total > 0 else 0.0
|
||||
|
||||
threshold_met = positive_pct >= threshold_pct
|
||||
min_participants_met = total >= min_participants
|
||||
adopted = threshold_met and min_participants_met
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"per_level_counts": per_level_counts,
|
||||
"positive_count": positive_count,
|
||||
"positive_pct": round(positive_pct, 2),
|
||||
"threshold_met": threshold_met,
|
||||
"min_participants_met": min_participants_met,
|
||||
"adopted": adopted,
|
||||
}
|
||||
Reference in New Issue
Block a user