v3 : socle Rust (moteur pur, accès trustinWallet, serveur) ; maquette Nuxt rangée dans maquette/
- crates/ld-engine : formule inertielle (parité avec la maquette, garde C), coquille / changement de fond, assistant d'édition à règles, composition d'un document sous vote permanent (adoptions, aperçus, rivales). - crates/trustin-access : qui voit, qui ouvre, qui gère les accès. - crates/ld-server : API axum + SQLite, connexion Ğ1 (client OAuth2 de connect.monnaie-libre.fr), invitations, entrée en vigueur automatique, signalements de contradiction, traductions éditoriales, publication git, taille de la toile lue sur la chaîne. - seeds/acte-engagement-certification.json : 33 articles, fr + en + es ; en vigueur : préambule, E1, E2 (seed a minima). - maquette/ : frontend, backend et docker v2 déplacés tels quels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user