Files
YvvandClaude Opus 5.5 67ddee28b2 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>
2026-10-21 02:04:42 +02:00

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,
}
}