//! Inertial threshold for binary votes — the Ğ1 "g1vote" formula, judged on a quorum. //! //! ```text //! T' = max(T, Q) //! R = B^W + (M + (1 − M)(1 − (T'/W)^G)) · T' //! ``` //! //! R is the minimum number of *for* votes; a version passes when `for ≥ ⌈R⌉`. //! W is the size of the electorate, T the votes cast (for + against). At low //! participation the formula asks for near-unanimity, at full participation the //! simple majority M suffices. Q is the **quorum** (ADR 0013, Yvv, 2026-11-02: //! « ce n'est pas un nombre de vote pour a priori, c'est plutôt un quorum, à partir //! de ce nombre, alors il faut la bonne proportion attendue par l'indice ») — and //! below it the voters missing count as against (`judged_voters`). It replaces the //! floor C of *for* votes added in front of the formula (ADR 0002, 0007). //! //! Parity with `maquette/frontend/app/engine/threshold.ts` at Q = 0 (the C = 0 of //! the maquette), with two deliberate guards — the participation ratio is capped //! at 1, so a stale W can never lower the bar below M; and a count that lands on a //! whole number stays on it (`settle`), so a rounding error never adds a vote. //! Which M, G and Q a text is judged by: `inertia` (five levels, their quorums). use serde::{Deserialize, Serialize}; /// The letters of the formula, named. #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct FormulaParams { /// M, in percent (0–100). pub majority_pct: f64, /// G. pub gradient: f64, /// B. pub base: f64, /// Q — the quorum: the voters (for + against) from which the share of the inertia /// applies; below it, the voters missing count as against (`judged_voters`). pub quorum: u32, } impl FormulaParams { pub const DEFAULT_BASE: f64 = 0.1; } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ThresholdError { EmptyElectorate, MajorityOutOfRange, } /// T' — the voters a version is judged on. **The rule of the quorum, here alone.** /// /// Yvv, 2026-11-03, to « Sous le quorum, les votants qui manquent comptent-ils comme des /// « non » ? » : « Ils comptent comme non ». Below the quorum Q, the voters who did not /// come count as against: the share of the inertia is asked of max(T, Q) voters. At level /// 2, quorum 12, 9 « pour » pass with 3 « contre » at most; Yvv's own example, 60 % and /// Q = 10, takes 6 « pour ». So an « against » never helps a version pass — completing /// the quorum with a « non » adds nothing the missing voters did not already count. pub fn judged_voters(cast: u32, quorum: u32) -> u32 { cast.max(quorum) } /// The share of *for* votes asked of `voters` voters (T' when judging a version). pub fn required_ratio(electorate: u32, voters: u32, params: &FormulaParams) -> f64 { let m = params.majority_pct / 100.0; let participation = (f64::from(voters) / f64::from(electorate.max(1))).min(1.0); m + (1.0 - m) * (1.0 - participation.powf(params.gradient)) } /// ⌈R⌉ — the minimum number of *for* votes for adoption, at `cast` votes cast. pub fn wot_threshold(electorate: u32, cast: u32, params: &FormulaParams) -> Result { if electorate == 0 { return Err(ThresholdError::EmptyElectorate); } if !(0.0..=100.0).contains(¶ms.majority_pct) { return Err(ThresholdError::MajorityOutOfRange); } let voters = judged_voters(cast, params.quorum); let offset = params.base.powf(f64::from(electorate)); let raw = if voters == 0 { offset } else { offset + settle(required_ratio(electorate, voters, params) * f64::from(voters)) }; // `raw` is finite and non-negative for any valid input; saturate rather than wrap. Ok(clamp_ceil(raw)) } /// Does a version with this tally pass? At least one vote cast, and its *for* reach /// ⌈R⌉ on the voters it is judged on (`judged_voters`). pub fn passes(tally: Tally, electorate: u32, params: &FormulaParams) -> Result { let cast = tally.cast(); Ok(cast > 0 && tally.for_votes >= wot_threshold(electorate, cast, params)?) } /// A count of votes that lands on a whole number stays on it: a rounding error /// of the logarithms (a billionth of a vote) must never raise a threshold by /// one. At the usual participation, 90 % of 70 votes is exactly 63. fn settle(votes: f64) -> f64 { let whole = votes.round(); if (votes - whole).abs() < 1e-9 { whole } else { votes } } fn clamp_ceil(raw: f64) -> u32 { let ceiled = raw.ceil(); if ceiled <= 0.0 { 0 } else if ceiled >= f64::from(u32::MAX) { u32::MAX } else { // Exact: `ceiled` is an integer in (0, u32::MAX). ceiled as u32 } } /// Votes counted on one proposal. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Tally { #[serde(rename = "for")] pub for_votes: u32, pub against: u32, } impl Tally { pub fn cast(&self) -> u32 { self.for_votes.saturating_add(self.against) } } /// Where a proposal stands, in words a screen can say. #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Evaluation { pub cast: u32, /// Q, the quorum it is judged on. pub quorum: u32, /// ⌈R⌉ at the current participation (on T' = max(T, Q) voters). pub required: u32, /// `passes`: `for ≥ ⌈R⌉` with at least one vote cast. pub reached: bool, /// *for* votes still missing with the *against* of now: `yes_needed − for` (0 once reached). pub missing: u32, /// The share of *for* the inertia asks of T' voters. pub required_ratio: f64, /// « Bien cotée » : it holds the share its inertia asks of the T voters who came, /// it only lacks voters (the quorum ignored). Used by the "if the well-rated pass" preview. pub well_rated: bool, /// The total of *for* that passes with the *against* cast now. pub yes_needed: u32, /// The total of *against* that holds the *for* cast now in check: 0 when the quorum /// already holds them (they do not pass even without any *against*). pub no_needed: u32, } /// The number of votes searched for never goes past this: the five levels always find /// theirs well before (`yes_needed`, `no_needed`). const SEARCH_LIMIT: u32 = u32::MAX / 4; /// The smallest `n` in `0..` for which `holds(n)`, `holds` being monotone (false, then /// true); `u32::MAX` if it never holds up to `SEARCH_LIMIT`. fn first(mut holds: impl FnMut(u32) -> Result) -> Result { if holds(0)? { return Ok(0); } let (mut lo, mut hi) = (0u32, 1u32); while !holds(hi)? { if hi >= SEARCH_LIMIT { return Ok(u32::MAX); } lo = hi; hi = hi.saturating_mul(2).min(SEARCH_LIMIT); } // holds(lo) is false, holds(hi) true. while hi - lo > 1 { let mid = lo + (hi - lo) / 2; if holds(mid)? { hi = mid } else { lo = mid } } Ok(hi) } /// The total of *for* that passes with `against` votes against. A *for* more never /// hurts (`passes` is monotone): every total from this one on passes. pub fn yes_needed(against: u32, electorate: u32, params: &FormulaParams) -> Result { first(|y| passes(Tally { for_votes: y, against }, electorate, params)) } /// The total of *against* that holds `for_votes` in check; 0 when they do not pass even /// without any *against* (the quorum holds them). An *against* more never helps: every /// total from this one on holds them. pub fn no_needed(for_votes: u32, electorate: u32, params: &FormulaParams) -> Result { first(|n| passes(Tally { for_votes, against: n }, electorate, params).map(|p| !p)) } pub fn evaluate(tally: Tally, electorate: u32, params: &FormulaParams) -> Result { let cast = tally.cast(); let required = wot_threshold(electorate, cast, params)?; let reached = passes(tally, electorate, params)?; let without_quorum = FormulaParams { quorum: 0, ..*params }; let required_without_quorum = wot_threshold(electorate, cast, &without_quorum)?; let yes = yes_needed(tally.against, electorate, params)?; Ok(Evaluation { cast, quorum: params.quorum, required, reached, missing: if reached { 0 } else { yes.saturating_sub(tally.for_votes) }, required_ratio: required_ratio(electorate, judged_voters(cast, params.quorum), params), well_rated: cast > 0 && tally.for_votes >= required_without_quorum, yes_needed: yes, no_needed: no_needed(tally.for_votes, electorate, params)?, }) } #[cfg(test)] mod tests { use super::*; fn g1vote(m: f64, g: f64) -> FormulaParams { FormulaParams { majority_pct: m, gradient: g, base: 0.1, quorum: 0 } } /// The letters of the « standard » inertia before the five levels: M 50 %, G 0.2. fn standard(quorum: u32) -> FormulaParams { FormulaParams { quorum, ..g1vote(50.0, 0.2) } } #[test] fn reference_case_of_the_smith_vote() { // Engagement forgeron v2.0.0: W 7224, 97 for, 23 against, M50 B.1 G.2 → 94. // The formula's own reference, letters given, without a quorum (Q = 0, the C = 0 // of the maquette): the five levels choose M, G and Q (ADR 0007, 0013). let p = g1vote(50.0, 0.2); assert_eq!(wot_threshold(7224, 120, &p), Ok(94)); let e = evaluate(Tally { for_votes: 97, against: 23 }, 7224, &p).expect("valid"); assert!(e.reached); assert_eq!(e.missing, 0); } #[test] fn parity_with_the_typescript_reference() { // Values computed by maquette/frontend/app/engine/threshold.ts (C = 0). let p = g1vote(50.0, 0.2); assert_eq!(wot_threshold(7224, 0, &p), Ok(0)); assert_eq!(wot_threshold(7224, 1, &p), Ok(1)); assert_eq!(wot_threshold(100, 100, &p), Ok(50)); assert_eq!(wot_threshold(5, 5, &p), Ok(3)); let high = g1vote(60.0, 0.4); assert_eq!(wot_threshold(7224, 120, &high), Ok(111)); } #[test] fn yvv_s_example_sixty_percent_and_a_quorum_of_ten() { // Yvv, 2026-11-02: « Si c'est 60% et c=10, alors il faut 6 voix pour et ça passe. » // A share of 60 % whatever the participation (G = 0), a large web (B^W vanishes). let p = FormulaParams { majority_pct: 60.0, gradient: 0.0, base: 0.1, quorum: 10 }; // G = 0: (T/W)^0 = 1, the share is M, 60 %. assert!((required_ratio(7224, 10, &p) - 0.6).abs() < 1e-12); for against in 0..=4 { let t = Tally { for_votes: 6, against }; assert_eq!(passes(t, 7224, &p), Ok(true), "6 pour, {against} contre"); } assert_eq!(passes(Tally { for_votes: 5, against: 0 }, 7224, &p), Ok(false), "5 of the 10 counted"); assert_eq!(wot_threshold(7224, 6, &p), Ok(6)); // Beyond the quorum, the share of all who came: 6 pour and 5 contre are 11 voters. assert_eq!(passes(Tally { for_votes: 6, against: 5 }, 7224, &p), Ok(false)); assert_eq!(passes(Tally { for_votes: 7, against: 4 }, 7224, &p), Ok(true)); } #[test] fn under_the_quorum_the_voters_missing_count_as_against() { // Yvv, 2026-11-03: « Ils comptent comme non ». T' = max(T, Q). assert_eq!( (judged_voters(0, 12), judged_voters(9, 12), judged_voters(12, 12), judged_voters(30, 12)), (12, 12, 12, 30) ); let p = standard(50); // One « pour » alone is judged on 50 voters; so are 1 « pour » and 49 « contre ». assert_eq!(wot_threshold(5745, 1, &p), wot_threshold(5745, 50, &p)); assert!(!passes(Tally { for_votes: 1, against: 0 }, 5745, &p).expect("valid")); } #[test] fn an_against_never_helps_a_version_pass() { // Without the rule, an « against » that completes the quorum could make a text pass. for (m, g, q, w) in [(50.0, 0.1, 12, 5707), (55.0, 0.15, 50, 5707), (66.0, 0.3, 50, 5745), (50.0, 0.6, 1, 2)] { let p = FormulaParams { majority_pct: m, gradient: g, base: 0.1, quorum: q }; for y in 0..80 { let mut passed = passes(Tally { for_votes: y, against: 0 }, w, &p).expect("valid"); for n in 1..80 { let now = passes(Tally { for_votes: y, against: n }, w, &p).expect("valid"); assert!(passed || !now, "M {m} Q {q} W {w}: {y} pour, {n} contre passes, {} contre did not", n - 1); passed = now; } } } } #[test] fn one_vote_never_passes_with_a_quorum_of_two_or_more() { // The invariant of Monnaie libre (quorum 2 at least): one voice alone never passes. for q in [2, 12, 50] { for g in [0.05, 0.1, 0.3, 1.77] { let p = FormulaParams { majority_pct: 50.0, gradient: g, base: 0.1, quorum: q }; let e = evaluate(Tally { for_votes: 1, against: 0 }, 5745, &p).expect("valid"); assert!(!e.reached, "Q {q}, G {g}"); assert!(e.well_rated, "unanimous, it only lacks voters"); } } // Without a quorum, the trap of a permanent vote: a lone « pour » meets the inertial bar. assert!(evaluate(Tally { for_votes: 1, against: 0 }, 5745, &standard(0)).expect("valid").reached); } #[test] fn yes_needed_and_no_needed_say_where_the_line_is() { // At level 2 on the scale of Monnaie libre (G 0.10274, W 5707), quorum 12. let p = FormulaParams { majority_pct: 50.0, gradient: 0.102_742, base: 0.1, quorum: 12 }; let e = evaluate(Tally::default(), 5707, &p).expect("valid"); assert_eq!((e.quorum, e.required, e.yes_needed, e.missing, e.no_needed), (12, 9, 9, 9, 0)); // 9 « pour »: they pass with 3 « contre » at most; the 4th holds them. let e = evaluate(Tally { for_votes: 9, against: 0 }, 5707, &p).expect("valid"); assert!(e.reached); assert_eq!((e.yes_needed, e.no_needed, e.missing), (9, 4, 0)); // With a 4th « contre », 14 voters at least: 11 « pour » are asked (10,2 rounded up). let e = evaluate(Tally { for_votes: 9, against: 4 }, 5707, &p).expect("valid"); assert!(!e.reached); assert_eq!(e.yes_needed, 11); assert_eq!(e.missing, 2); // 8 « pour »: the quorum already holds them, no « contre » is needed. let e = evaluate(Tally { for_votes: 8, against: 0 }, 5707, &p).expect("valid"); assert_eq!((e.reached, e.no_needed, e.missing), (false, 0, 1)); } #[test] fn passes_means_yes_needed_reached_means_no_needed_not_reached() { // On a grid: levels (M, G as at the reference participation and beyond), quorums, // electorates, « pour », « contre ». passes ⇔ for ≥ yesNeeded(against) ⇔ against < noNeeded(for). let levels = [(50.0, 0.055), (50.0, 0.103), (55.0, 0.145), (60.0, 0.212), (66.0, 0.303), (66.0, 1.77)]; for (m, g) in levels { for q in [0, 1, 2, 12, 50] { for w in [1, 2, 16, 40, 500, 5707] { let p = FormulaParams { majority_pct: m, gradient: g, base: 0.1, quorum: q }; let yes: Vec = (0..70).map(|n| yes_needed(n, w, &p).expect("valid")).collect(); for y in 0..70 { let no = no_needed(y, w, &p).expect("valid"); for n in 0..70 { let t = Tally { for_votes: y, against: n }; let ok = passes(t, w, &p).expect("valid"); assert_eq!(ok, y >= yes[n as usize], "M {m} G {g} Q {q} W {w}: {y} pour {n} contre"); assert_eq!(ok, n < no, "M {m} G {g} Q {q} W {w}: {y} pour {n} contre, noNeeded {no}"); } } } } } } #[test] fn well_rated_holds_the_share_on_those_who_came() { let p = standard(50); // 30 « pour », 2 « contre »: the share holds on 32 voters, the quorum is not there. let e = evaluate(Tally { for_votes: 30, against: 2 }, 5745, &p).expect("valid"); assert!(!e.reached && e.well_rated); // 10 « pour », 10 « contre »: the share does not hold. assert!(!evaluate(Tally { for_votes: 10, against: 10 }, 5745, &p).expect("valid").well_rated); } #[test] fn no_vote_is_never_reached() { for q in [0, 1, 12] { let p = FormulaParams { quorum: q, ..g1vote(50.0, 0.1) }; let e = evaluate(Tally::default(), 3, &p).expect("valid"); assert!(!e.reached); assert!(!e.well_rated); } } #[test] fn the_test_group_passes_with_one_voice_at_a_quorum_of_one() { // Two people, quorum 1 (Yvv, 2026-11-02: the test group stays at 1 everywhere): one // « pour » of one voter holds any share; a « contre » beside it does not. let p = standard(1); assert!(evaluate(Tally { for_votes: 1, against: 0 }, 2, &p).expect("valid").reached); assert!(!evaluate(Tally { for_votes: 1, against: 1 }, 2, &p).expect("valid").reached); assert!(evaluate(Tally { for_votes: 2, against: 0 }, 2, &p).expect("valid").reached); } #[test] fn stale_electorate_cannot_lower_the_bar_below_majority() { let p = g1vote(50.0, 0.2); // 30 votes cast for an electorate believed to be 10: the reference // would ask 12 (ratio below M), the cap keeps the simple majority: // ⌈B^W + 0.5 × 30⌉ = 16, the tiny B^W offset rounding up. let r = wot_threshold(10, 30, &p).expect("valid"); assert_eq!(r, 16); } #[test] fn a_whole_number_of_votes_never_gains_one_by_rounding() { // 100 members, everyone votes, M 55 %: 55 % of 100 = 55. In floating point 0.55 × 100 // is 55.000000000000007, which would ask for 56 (B^W = 10⁻¹⁰⁰ adds nothing). let p = FormulaParams { majority_pct: 55.0, gradient: 0.3, base: 0.1, quorum: 1 }; assert_eq!(wot_threshold(100, 100, &p), Ok(55)); // A genuine fraction still rounds up. assert_eq!(wot_threshold(101, 99, &FormulaParams { quorum: 0, ..p }), Ok(55)); } #[test] fn errors() { let p = g1vote(50.0, 0.2); assert_eq!(wot_threshold(0, 1, &p), Err(ThresholdError::EmptyElectorate)); let bad = g1vote(120.0, 0.2); assert_eq!(wot_threshold(10, 1, &bad), Err(ThresholdError::MajorityOutOfRange)); assert_eq!(yes_needed(0, 0, &p), Err(ThresholdError::EmptyElectorate)); } }