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