Lot S, phase 2 : le quorum dans le moteur (ADR 0013), vu rouge puis vert

Le quorum Q (votants = pour + contre) remplace le C additif : T' = max(T, Q),
la version passe si pour ≥ ⌈B^W + ratio(T')·T'⌉, ratio la courbe des cinq crans
(M, G calé à la participation de référence, inchangé). La règle d'Yvv du 03/11
(« Ils comptent comme non ») est seule dans threshold::judged_voters. Pour
l'écran : yes_needed (le total de pour qui passe avec les contre d'aujourd'hui)
et no_needed (le total de contre qui tient les pour d'aujourd'hui, 0 si le quorum
suffit) ; passe ⇔ pour ≥ yesNeeded ⇔ contre < noNeeded, éprouvé sur une grille.
well_rated : la part tient sur ceux qui sont venus. Scale porte les quorums par
cran, UnitState le quorum propre d'un article ; p0 reste 2·vote_floor/W.

Vu rouge : sans la règle (T' = T), 9 tests échouent ; avec l'autre lecture d'un
quorum (un minimum de votants, les absents non comptés), 8 échouent, dont « un
contre n'aide jamais ». Chiffres de contrôle (W 5 707, p0 = 100/W) tenus : 9, 39,
46 au quorum ; 7, 8, 9, 9, 10 à dix votants.

Le serveur suit au commit suivant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 04:36:43 +01:00
co-authored by Claude Opus 5.5
parent e49f7fb98f
commit 37147d1f59
4 changed files with 419 additions and 158 deletions
+28 -9
View File
@@ -19,7 +19,8 @@
//! - what the document would read if the well-rated, or all, proposals passed
//! (`preview`);
//! - which formula a proposal is judged by (`params_for`): the level of
//! inertia of its unit, on the scale of its group (`inertia::Scale`).
//! inertia of its unit and its quorum, on the scale of its document
//! (`inertia::Scale`).
use crate::inertia::{InertiaLevel, Scale};
use crate::threshold::{FormulaParams, Tally, ThresholdError, evaluate};
@@ -40,6 +41,10 @@ pub struct UnitState {
pub id: String,
pub position: f64,
pub inertia: InertiaLevel,
/// Its own quorum, set by the author of its first version before the first vote
/// (ADR 0013); `None`: the quorum of its level.
#[serde(default)]
pub quorum: Option<u32>,
pub in_force: Option<VersionText>,
}
@@ -84,8 +89,11 @@ pub fn voting_level(unit: &UnitState, kind: ProposalKind) -> InertiaLevel {
}
}
/// The formula a proposal is judged by: the level it is voted at, and the quorum of its
/// unit — its own, or that level's (a change of inertia inherits both).
pub fn params_for(unit: &UnitState, kind: ProposalKind, scale: &Scale) -> FormulaParams {
scale.params(voting_level(unit, kind))
let level = voting_level(unit, kind);
scale.params(level, scale.quorum(level, unit.quorum))
}
/// A proposal that changes nothing cannot enter into force.
@@ -264,6 +272,7 @@ mod tests {
id: id.into(),
position,
inertia: InertiaLevel::DEFAULT,
quorum: None,
in_force: in_force.map(|b| VersionText {
id: format!("{id}-v"),
title: id.into(),
@@ -286,8 +295,9 @@ mod tests {
}
}
/// Monnaie libre before any median: W 5745, C 50, the reference p0 = 2C/W.
const SCALE: Scale = Scale { electorate: 5745, floor: 50.0, participation: 100.0 / 5745.0 };
/// Monnaie libre before any median: W 5745, the reference p0 = 2 × 50 / W, the quorums
/// 12 · 12 · 50 · 50 · 50 (ADR 0013).
const SCALE: Scale = Scale { electorate: 5745, participation: 100.0 / 5745.0, quorums: [12, 12, 50, 50, 50] };
fn level(k: u8) -> InertiaLevel {
InertiaLevel::new(k).expect("1 to 5")
@@ -301,12 +311,18 @@ mod tests {
}
#[test]
fn the_floor_holds_then_releases() {
fn the_quorum_holds_then_releases() {
// Level 3, quorum 50: under it the voters missing count as against, 39 « pour » pass.
let units = [unit("E3", 7.0, None)];
let few = [prop("p1", "E3", ProposalKind::Text, "connaître", 49, 0)];
let few = [prop("p1", "E3", ProposalKind::Text, "connaître", 38, 0)];
assert!(adoptions(&units, &few, &SCALE).expect("valid").is_empty());
let enough = [prop("p1", "E3", ProposalKind::Text, "connaître", 50, 0)];
let enough = [prop("p1", "E3", ProposalKind::Text, "connaître", 39, 0)];
assert_eq!(adoptions(&units, &enough, &SCALE).expect("valid"), vec!["p1".to_string()]);
// Its own quorum, lower, set by its author: the same curve on fewer voters.
let mut own = unit("E3", 7.0, None);
own.quorum = Some(12);
let lower = [prop("p1", "E3", ProposalKind::Text, "connaître", 11, 0)];
assert_eq!(adoptions(&[own], &lower, &SCALE).expect("valid"), vec!["p1".to_string()]);
}
#[test]
@@ -339,7 +355,10 @@ mod tests {
assert_eq!(voting_level(&u, kind), level(4), "inherits the level of the article");
assert_eq!(params_for(&u, kind, &SCALE), params_for(&u, ProposalKind::Text, &SCALE));
}
assert_eq!(params_for(&u, ProposalKind::Text, &SCALE).majority_pct, 60.0);
let p = params_for(&u, ProposalKind::Text, &SCALE);
assert_eq!((p.majority_pct, p.quorum), (60.0, 50), "the level, and so its quorum");
u.quorum = Some(20);
assert_eq!(params_for(&u, ProposalKind::Inertia(level(1)), &SCALE).quorum, 20, "and the article's own");
}
#[test]
@@ -358,7 +377,7 @@ mod tests {
// The seed a minima has no vote: the first rival that reaches its threshold wins.
let rival = [prop("r", "E1", ProposalKind::Text, "unicité reformulée", 50, 0)];
assert_eq!(adoptions(&units, &rival, &SCALE).expect("valid"), vec!["r".to_string()]);
// A version in force by vote, margin 60 − 58 = 2: a rival at 55 (margin 55 − 54 = 1) waits.
// A version in force by vote, margin 60 − 47 = 13: a rival at 55 (margin 55 − 43 = 12) waits.
if let Some(v) = units[0].in_force.as_mut() {
v.tally = Tally { for_votes: 60, against: 0 };
}