Lot T3c : les tests d'abord, rouges sur le texte de e7ab82f (8 tests) — « comme d'habitude » contre le terme défini « participation habituelle » ; une grille commune (grid, 85 100 cas : W de 100 à 10 000 000, médianes sous 1 % et au-dessus de 50 % de W ramenées par Participation::of, chaque quorum d'auteur de 2 à W jusqu'à 1 000, les cinq crans) à la place d'as_usual_grid, qui sautait les médianes hors des bornes ; l'écart entre la valeur bornée et la médiane (W 7 224, médiane 40, cran 2 : 28 « pour » sur 40, pas 27) ; la part exigée qui baisse au-delà du quorum de l'article ; « une seule voix » sur la grille ; quorum.rs : la participation et G inchangés par le quorum d'un article, le cran 3 d'un nouvel article

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 19:48:08 +01:00
co-authored by Claude Opus 5.5
parent aaacfaa332
commit 3383f28739
2 changed files with 600 additions and 143 deletions
+12
View File
@@ -94,6 +94,11 @@ fn numbers(levels: &Value, key: &str) -> Vec<u64> {
levels.as_array().expect("levels").iter().map(|l| l[key].as_u64().expect("number")).collect()
}
/// G of each level, as served.
fn gradients(doc: &Value) -> Vec<Value> {
doc["levels"].as_array().expect("levels").iter().map(|l| l["gradient"].clone()).collect()
}
/// (quorum, quorumDefault, canSetQuorum) of a unit.
fn quorum_of(u: &Value) -> (Option<u64>, Option<u64>, Option<bool>) {
(u["quorum"].as_u64(), u["quorumDefault"].as_u64(), u["canSetQuorum"].as_bool())
@@ -174,6 +179,8 @@ async fn the_author_of_an_article_sets_its_quorum_before_the_first_vote() {
.await;
let (u, text) =
(created["unitId"].as_str().expect("unit").to_owned(), created["id"].as_str().expect("id").to_owned());
// « Tout nouvel article commence à ce cran » (F1.2, « Par défaut »): the default level, 3.
assert_eq!(unit_by_id(&chiara.doc("essai").await, &u)["inertia"], 3);
// Its author may, another member may not; the test group: its level's quorum, 1.
assert_eq!(quorum_of(unit_by_id(&chiara.doc("essai").await, &u)), (None, Some(1), Some(true)));
assert_eq!(quorum_of(unit_by_id(&luna.doc("essai").await, &u)), (None, Some(1), Some(false)));
@@ -270,6 +277,7 @@ async fn in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pen
chiara.login("Chiara", false).await;
let doc = admin.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let usual = (doc["doc"]["participation"].clone(), gradients(&doc));
let i1 = unit(&doc, "I1");
let (u, p) = (i1["id"].as_str().expect("u").to_owned(), i1["proposals"][0]["id"].as_str().expect("p").to_owned());
// A text of the seed has no author: an administrator answers for it.
@@ -287,6 +295,10 @@ async fn in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pen
}
let doc = admin.doc("monnaie-libre").await;
assert_eq!(unit(&doc, "I1")["proposals"][0]["evaluation"]["quorum"], 20);
// « Elle ne dépend pas du quorum d'un article » (N1.1, lot T3c): the participation habituelle,
// and G of each level, stay as they were (the reference rests on the document's, 2 × 50 / W).
assert_eq!((doc["doc"]["participation"].clone(), gradients(&doc)), usual);
assert!((doc["doc"]["participation"]["ratio"].as_f64().expect("p̃") - 100.0 / W as f64).abs() < 1e-12);
// A refused ballot, an expired one, are no vote.
ballot(&state, &doc_id, &p, "refused", 1).await;
ballot(&state, &doc_id, &p, "expired", 2).await;
+588 -143
View File
@@ -27,6 +27,18 @@
//! group votes as usual — « à la participation habituelle » was false under the quorum; « autant de
//! « pour » » in French; and « 100 votants » is checked as the parameter it is, not as a turnout
//! that would hold on every W.
//!
//! After the review of e7ab82f (lot T3c, before it went online: the same amendments, their
//! fingerprints `to` changed again): « comme d'habitude » reads first as the real median, and is
//! false where the median leaves its bounds (W 7 224, a median of 40, level 2, quorum 12: 28 « pour »
//! of 40, not 27). F1.2 and N1.1 write the defined term, « à la participation habituelle »: the
//! median brought back within 1 % and 50 % of W, the one G is computed on. « Sous le quorum de
//! l'article » where a bare « quorum » could be the level's; « la part exigée » where « la part »
//! could be the rounded share; « Tant qu'elle repose sur moins de 5 propositions » (the year counts).
//! Every sentence of F1.2 and N1.1 that says a number, a share or a direction is checked against the
//! engine on one grid (`grid`): W from 100, medians under 1 % and above 50 % of W, every quorum an
//! author may set, the five levels — the closed list given to the reviewer is
//! `docs/chantiers/acte-certification/t3c-liste-fermee.md`.
use ld_engine::inertia::{InertiaLevel, Participation, Scale, gradient, quorum_bounds};
use ld_engine::threshold::{FormulaParams, Tally, judged_voters, passes, required_ratio, wot_threshold, yes_needed};
@@ -302,7 +314,7 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
),
format!("**B = {}**", dec(lang, f.base)),
format!(
"Le cran par défaut est le {} ({}) : M = {}, un quorum de {qd} dans Monnaie libre, et, au-delà du quorum, le groupe demande {} de « pour » quand il vote comme d'habitude",
"Le cran par défaut est le {} ({}) : M = {}, un quorum de {qd} dans Monnaie libre, et, au-delà du quorum, le groupe demande {} de « pour » à sa participation habituelle",
f.default_level,
name(f.default_level),
pct(lang, d.majority_pct()),
@@ -330,7 +342,7 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
format!("In Monnaie libre, never less than {}: a single vote never passes a text", f.lowest_quorum),
format!("**B = {}**", dec(lang, f.base)),
format!(
"The default level is {} ({}): M = {}, a quorum of {qd} in Monnaie libre, and, beyond the quorum, the group asks for {} of votes in favour when it votes as usual",
"The default level is {} ({}): M = {}, a quorum of {qd} in Monnaie libre, and, beyond the quorum, the group asks for {} of votes in favour at its usual turnout",
f.default_level,
name(f.default_level),
pct(lang, d.majority_pct()),
@@ -358,7 +370,7 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
format!("En Monnaie libre, nunca menos de {}: un solo voto nunca hace pasar un texto", f.lowest_quorum),
format!("**B = {}**", dec(lang, f.base)),
format!(
"El nivel por defecto es el {} ({}): M = {}, un quórum de {qd} en Monnaie libre, y, más allá del quórum, el grupo pide un {} de votos a favor cuando vota como de costumbre",
"El nivel por defecto es el {} ({}): M = {}, un quórum de {qd} en Monnaie libre, y, más allá del quórum, el grupo pide un {} de votos a favor con su participación habitual",
f.default_level,
name(f.default_level),
pct(lang, d.majority_pct()),
@@ -390,17 +402,17 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
);
match lang {
"fr" => format!(
"- **Cran {} · {n}** : quorum {} ; {s} comme d'habitude, {mk} quand tout le monde vote. {what}",
"- **Cran {} · {n}** : quorum {} ; {s} à la participation habituelle, {mk} quand tout le monde vote. {what}",
k.get(),
q[i]
),
"en" => format!(
"- **Level {} · {n}**: quorum {}; {s} as usual, {mk} when everyone votes. {what}",
"- **Level {} · {n}**: quorum {}; {s} at the usual turnout, {mk} when everyone votes. {what}",
k.get(),
q[i]
),
_ => format!(
"- **Nivel {} · {n}**: quórum {}; {s} como de costumbre, {mk} cuando vota todo el mundo. {what}",
"- **Nivel {} · {n}**: quórum {}; {s} con la participación habitual, {mk} cuando vota todo el mundo. {what}",
k.get(),
q[i]
),
@@ -416,20 +428,20 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
out.extend(match lang {
"fr" => [
"propositions du groupe votées depuis un an".to_owned(),
format!("Tant que le groupe n'a pas voté sur {ms} propositions, elle vaut deux fois le quorum de référence du document : {r} votants dans Monnaie libre"),
format!("Elle reste entre {b0} et {b1} de l'électorat, et se mesure une fois par jour"),
format!("Tant qu'elle repose sur moins de {ms} propositions, elle vaut deux fois le quorum de référence du document : {r} votants dans Monnaie libre"),
format!("Elle est ramenée entre {b0} et {b1} de l'électorat, et se mesure une fois par jour"),
format!("où s est cette part du cran ({head} ou {last})"),
],
"en" => [
"the group's proposals voted on over the past year".to_owned(),
format!("Until the group has voted on {ms} proposals, it is twice the document's reference quorum: {r} voters in Monnaie libre"),
format!("It stays between {b0} and {b1} of the electorate, and is measured once a day"),
format!("While it rests on fewer than {ms} proposals, it is twice the document's reference quorum: {r} voters in Monnaie libre"),
format!("It is kept between {b0} and {b1} of the electorate, and is measured once a day"),
format!("where s is that share of the level ({head} or {last})"),
],
_ => [
"propuestas del grupo votadas en el último año".to_owned(),
format!("Hasta que el grupo haya votado {ms} propuestas, vale dos veces el quórum de referencia del documento: {r} votantes en Monnaie libre"),
format!("Se mantiene entre el {b0} y el {b1} del electorado, y se mide una vez al día"),
format!("Mientras se base en menos de {ms} propuestas, vale dos veces el quórum de referencia del documento: {r} votantes en Monnaie libre"),
format!("Se acota entre el {b0} y el {b1} del electorado, y se mide una vez al día"),
format!("donde s es esa proporción del nivel ({head} o {last})"),
],
});
@@ -619,63 +631,129 @@ fn the_three_phrases_of_the_review_of_m6b_are_written_true() {
// Lot T3: no dated figure, every sentence true for every W and every quorum
// ---------------------------------------------------------------------------
/// (article, language, the words of T2, the words of today): the only change of lot T3, with its
/// review (2ce2028: the sentence of G; « autant de « pour » » in French). F1.1 has none: it holds
/// no dated figure and every sentence of it is true (`b_to_the_power_w_…`).
const PHRASES_T3: [(&str, &str, &str, &str); 15] = [
/// (article, language, the words of T2, the words of today): the only change of the amendments of
/// lot T3, with its reviews — 2ce2028 (lot T3b: the sentence of G; « autant de « pour » » in French)
/// and e7ab82f (lot T3c: the defined term « participation habituelle » where T3 wrote « comme
/// d'habitude »; « le quorum de l'article » where a bare « quorum » could be the level's; « la part
/// exigée »; « repose sur moins de 5 propositions »; « ramenée entre 1 % et 50 % »). The five lines
/// of the levels change by `LEVEL_LINES_T3`. F1.1 has none: it holds no dated figure and every
/// sentence of it is true (`b_to_the_power_w_…`).
const PHRASES_T3: [(&str, &str, &str, &str); 27] = [
(
"F1.2",
"fr",
"un quorum de 50 dans Monnaie libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude.",
"un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » quand il vote comme d'habitude.",
"un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » à sa participation habituelle.",
),
(
"F1.2",
"en",
"a quorum of 50 in Monnaie libre, and the group asks for 75% of votes in favour when it votes as usual.",
"a quorum of 50 in Monnaie libre, and, beyond the quorum, the group asks for 75% of votes in favour when it votes as usual.",
"a quorum of 50 in Monnaie libre, and, beyond the quorum, the group asks for 75% of votes in favour at its usual turnout.",
),
(
"F1.2",
"es",
"un quórum de 50 en Monnaie libre, y el grupo pide un 75 % de votos a favor cuando vota como de costumbre.",
"un quórum de 50 en Monnaie libre, y, más allá del quórum, el grupo pide un 75 % de votos a favor cuando vota como de costumbre.",
"un quórum de 50 en Monnaie libre, y, más allá del quórum, el grupo pide un 75 % de votos a favor con su participación habitual.",
),
(
"N1.1",
"fr",
"Chaque cran a aussi son quorum, qui n'est pas un nombre minimal de votants : sous le quorum, les votants qui manquent comptent comme « non ».",
"Chaque cran a aussi son quorum : sous le quorum, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si le quorum avait voté.",
"Chaque cran a aussi son quorum : sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté.",
),
(
"N1.1",
"en",
"Each level also has its quorum, which is not a minimum number of voters: below the quorum, the voters who are missing count as votes against.",
"Each level also has its quorum: below the quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if the quorum had voted.",
"Each level also has its quorum: below the article's quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if that quorum had voted.",
),
(
"N1.1",
"es",
"Cada nivel tiene además su quórum, que no es un número mínimo de votantes: por debajo del quórum, los votantes que faltan cuentan como «no».",
"Cada nivel tiene además su quórum: por debajo del quórum, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado el quórum.",
"Cada nivel tiene además su quórum: por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum.",
),
(
"N1.1",
"fr",
"La part dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà du quorum, plus il y a de votants,",
"La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà de ce quorum, plus il y a de votants,",
),
(
"N1.1",
"en",
"The share depends on the level and on the number of voters, compared with the group's usual turnout: beyond the quorum, the more voters,",
"The share asked depends on the level and on the number of voters, compared with the group's usual turnout: beyond that quorum, the more voters,",
),
(
"N1.1",
"es",
"La proporción depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá del quórum, cuantos más votantes,",
"La proporción exigida depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá de ese quórum, cuantos más votantes,",
),
(
"N1.1",
"fr",
"où T' est le nombre de votants, ou le quorum s'il en manque.",
"où T' est le nombre de votants, ou le quorum de l'article s'il en manque.",
),
(
"N1.1",
"en",
"where T' is the number of voters, or the quorum if some are missing.",
"where T' is the number of voters, or the article's quorum if some are missing.",
),
(
"N1.1",
"es",
"donde T' es el número de votantes, o el quórum si faltan.",
"donde T' es el número de votantes, o el quórum del artículo si faltan.",
),
(
"N1.1",
"fr",
"**Les cinq crans** : le quorum dans Monnaie libre, la part exigée quand le groupe vote comme d'habitude, puis quand tout le monde vote,",
"**Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum, quand le groupe vote comme d'habitude, puis quand tout le monde vote,",
"**Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum de l'article, à la participation habituelle, puis quand tout le monde vote,",
),
(
"N1.1",
"en",
"**The five levels**: the quorum in Monnaie libre, the share asked when the group votes as usual, then when everyone votes,",
"**The five levels**: the quorum in Monnaie libre, the share asked beyond the quorum, when the group votes as usual, then when everyone votes,",
"**The five levels**: the quorum in Monnaie libre, the share asked beyond the article's quorum, at the usual turnout, then when everyone votes,",
),
(
"N1.1",
"es",
"**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida cuando el grupo vota como de costumbre, luego cuando vota todo el mundo,",
"**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida más allá del quórum, cuando el grupo vota como de costumbre, luego cuando vota todo el mundo,",
"**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida más allá del quórum del artículo, con la participación habitual, luego cuando vota todo el mundo,",
),
(
"N1.1",
"fr",
"Tant que le groupe n'a pas voté sur 5 propositions, elle vaut",
"Tant qu'elle repose sur moins de 5 propositions, elle vaut",
),
(
"N1.1",
"en",
"Until the group has voted on 5 proposals, it is",
"While it rests on fewer than 5 proposals, it is",
),
(
"N1.1",
"es",
"Hasta que el grupo haya votado 5 propuestas, vale",
"Mientras se base en menos de 5 propuestas, vale",
),
("N1.1", "fr", "Elle reste entre 1 % et 50 % de l'électorat", "Elle est ramenée entre 1 % et 50 % de l'électorat"),
("N1.1", "en", "It stays between 1% and 50% of the electorate", "It is kept between 1% and 50% of the electorate"),
(
"N1.1",
"es",
"Se mantiene entre el 1 % y el 50 % del electorado",
"Se acota entre el 1 % y el 50 % del electorado",
),
(
"N1.1",
@@ -699,22 +777,31 @@ const PHRASES_T3: [(&str, &str, &str, &str); 15] = [
"N1.1",
"fr",
"**G** se calcule pour que la part exigée, à la participation habituelle, soit exactement celle du cran : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est la part exigée comme d'habitude et p̃ la participation habituelle divisée par W.",
"**G** se calcule pour que la part exigée, au-delà du quorum, soit exactement celle du cran quand le groupe vote comme d'habitude : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W.",
"**G** se calcule pour que la part exigée, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W.",
),
(
"N1.1",
"en",
"**G** is computed so that the share asked, at the usual turnout, is exactly that of the level: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is the share asked as usual and p̃ the usual turnout divided by W.",
"**G** is computed so that the share asked, beyond the quorum, is exactly that of the level when the group votes as usual: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is that share of the level (60, 67, 75, 83 or 90%) and p̃ the usual turnout divided by W.",
"**G** is computed so that the share asked, beyond the article's quorum, is exactly that of the level at the usual turnout: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is that share of the level (60, 67, 75, 83 or 90%) and p̃ the usual turnout divided by W.",
),
(
"N1.1",
"es",
"**G** se calcula para que la proporción exigida, con la participación habitual, sea exactamente la del nivel: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es la proporción exigida como de costumbre y p̃ la participación habitual dividida por W.",
"**G** se calcula para que la proporción exigida, más allá del quórum, sea exactamente la del nivel cuando el grupo vota como de costumbre: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es esa proporción del nivel (60, 67, 75, 83 o 90 %) y p̃ la participación habitual dividida por W.",
"**G** se calcula para que la proporción exigida, más allá del quórum del artículo, sea exactamente la del nivel con la participación habitual: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es esa proporción del nivel (60, 67, 75, 83 o 90 %) y p̃ la participación habitual dividida por W.",
),
];
/// The five lines of the levels of N1.1 (lot T3c): « 60 % comme d'habitude » becomes « 60 % à la
/// participation habituelle », and so on, in each line and nothing else (language, words of T2,
/// words of today).
const LEVEL_LINES_T3: [(&str, &str, &str); 3] = [
("fr", " comme d'habitude, ", " à la participation habituelle, "),
("en", " as usual, ", " at the usual turnout, "),
("es", " como de costumbre, ", " con la participación habitual, "),
];
/// The texts of T2 with the phrases of lot T3 written in: what the seed gives today.
fn t3_from_t2(code: &str, lang: &str, t2: &str) -> String {
let mut b = t2.to_owned();
@@ -724,6 +811,11 @@ fn t3_from_t2(code: &str, lang: &str, t2: &str) -> String {
b = b.replacen(old, new, 1);
}
}
if code == "N1.1" {
let (_, old, new) = LEVEL_LINES_T3.iter().find(|(l, ..)| *l == lang).expect("a language");
assert_eq!(b.matches(old).count(), 5, "N1.1 {lang}: the five lines of the levels");
b = b.replace(old, new);
}
b
}
@@ -742,33 +834,192 @@ fn the_texts_of_lot_t3_change_these_phrases_only() {
assert!(!body.contains(old), "{code} {lang}: still the words of T2\n{body}");
assert!(body.contains(new), "{code} {lang}: missing « {new} »\n{body}");
}
for (lang, old, new) in LEVEL_LINES_T3 {
let (_, body) = text(&today, "N1.1", lang);
assert!(!body.contains(old), "N1.1 {lang}: « {old} » is left\n{body}");
assert_eq!(body.matches(new).count(), 6, "N1.1 {lang}: the five lines, and the list's introduction\n{body}");
}
// The example of N1.1 is gone whole, and nothing takes its place.
for (lang, word) in [("fr", "Exemple"), ("en", "Example"), ("es", "Ejemplo")] {
assert!(!text(&today, "N1.1", lang).1.contains(word), "N1.1 {lang}: an example is left");
}
}
/// The participations of the grid, as shares of W: the bounds, the reference of the document
/// (`2 × voteFloor / W`, brought back within the bounds) and one between.
fn participations(w: u32, vote_floor: f64) -> [f64; 4] {
[Participation::LOWEST, Participation::of(w, vote_floor, None, 0).ratio, 0.1, Participation::HIGHEST]
// ---------------------------------------------------------------------------
// Lot T3c: one grid for every sentence that says a number, a share or a direction
// ---------------------------------------------------------------------------
/// The medians of the grid on a web of `w` members (lot T3c), as measured, before the bounds:
/// under 1 % of W, at it, within, at 50 % and above. The participation habituelle is the median
/// brought back within its bounds, so a median out of them is not it.
fn medians(w: u32) -> Vec<u32> {
let one = w / 100;
let mut m: Vec<u32> = [1, 2, one.saturating_sub(1), one, one + 1, 12, 40, 50, 100, 120, 1000, w / 2, w / 2 + 1, w]
.into_iter()
.filter(|m| (1..=w).contains(m))
.collect();
m.sort_unstable();
m.dedup();
m
}
/// N1.1, the phrase of lot T3: « sous le quorum, les votants qui manquent comptent comme « non »,
/// et il faut autant de « pour » que si le quorum avait voté » (after the review of 2ce2028: « de
/// voix pour que » read first as « pour que », a conjunction). True for every quorum an author
/// may set (2 to W in Monnaie libre, 1 to W in the test group), every level, every W, every
/// participation within the bounds: the « pour » that pass with nobody against, fewer voters
/// than the quorum, are the threshold of the quorum; and under it, a tally passes exactly when
/// its « pour » reach that threshold. The phrase of T2, « qui n'est pas un nombre minimal de
/// votants », is false: at a quorum of 2, two « pour » are asked, at every level, on every W.
/// A usual turnout of the grid: the median measured (`None`: fewer than 5 proposals, the reference
/// of the document stands in) and the participation habituelle the engine takes from it
/// (`Participation::of`: the median, or the reference, brought back within 1 % and 50 % of W).
#[derive(Clone, Copy, Debug)]
struct Usual {
median: Option<u32>,
part: Participation,
}
impl Usual {
/// The participation habituelle, in voters: p̃ × W — not a whole number when it is a bound
/// (1 % of 7 224 is 72,24 voters). To a millionth of a voter: the division by W and the product
/// by W give 57,999 999 999 999 99 for a median of 58 on 5 707.
fn voters(&self, w: u32) -> f64 {
(self.part.voters(w) * 1e6).round() / 1e6
}
}
/// The usual turnouts of the grid on a web of `w`: the reference, then each median of `medians`.
fn usuals(w: u32, vote_floor: f64) -> Vec<Usual> {
std::iter::once(None)
.chain(medians(w).into_iter().map(Some))
.map(|median| {
let sample = if median.is_some() { 9 } else { 0 };
Usual { median, part: Participation::of(w, vote_floor, median.map(f64::from), sample) }
})
.collect()
}
/// The participations of the grid, as shares of W, each once: those of `usuals`. Both bounds are
/// among them on every W of Monnaie libre, where a median under 1 % or above 50 % of W is brought
/// back to one.
fn participations(w: u32, vote_floor: f64) -> Vec<f64> {
let mut p: Vec<f64> = usuals(w, vote_floor).iter().map(|u| u.part.ratio).collect();
p.sort_by(f64::total_cmp);
p.dedup();
p
}
/// The quorums an author may set on a web of `w` (ADR 0013), from the lowest bound to W: every one
/// up to W 1 000; beyond, `quorum_sample`.
fn author_quorums(w: u32, lowest: u32, around: &[f64]) -> Vec<u32> {
if w <= 1000 { (lowest..=w).collect() } else { quorum_sample(w, lowest, around) }
}
/// Quorums from the lowest bound to W, where every one would be too many: the bounds and their
/// neighbours, the quorums of the levels and theirs, 100, 1 000, W/2, and around each number of
/// voters of `around` (the participation habituelle, the median).
fn quorum_sample(w: u32, lowest: u32, around: &[f64]) -> Vec<u32> {
let mut q = vec![lowest, 2, 3, 11, 12, 13, 49, 50, 51, 100, 1000, w / 2, w - 1, w];
for a in around {
let (down, up) = (a.floor() as u32, a.ceil() as u32);
q.extend([down, up, up + 1]);
}
q.retain(|q| (lowest..=w).contains(q));
q.sort_unstable();
q.dedup();
q
}
/// One case of the grid: a web, a usual turnout of its group, a level, the quorum of the article.
struct Case {
w: u32,
usual: Usual,
k: InertiaLevel,
q: u32,
}
impl Case {
fn at(&self) -> String {
format!("W {} median {:?} usual {} k {} Q {}", self.w, self.usual.median, self.voters(), self.k.get(), self.q)
}
/// The participation habituelle, in voters.
fn voters(&self) -> f64 {
self.usual.voters(self.w)
}
/// The letters of the formula (the quorums of the scale do not enter: the quorum is the case's).
fn params(&self) -> FormulaParams {
let scale = Scale { electorate: self.w, participation: self.usual.part.ratio, quorums: [12, 12, 50, 50, 50] };
scale.params(self.k, self.q)
}
/// s_k, the share of the level.
fn s(&self) -> f64 {
self.k.share_pct() / 100.0
}
/// M_k.
fn m(&self) -> f64 {
self.k.majority_pct() / 100.0
}
}
/// The grid of the closed list (lot T3c, `docs/chantiers/acte-certification/t3c-liste-fermee.md`),
/// on which every sentence of F1.2 and N1.1 that says a number, a share or a direction is checked
/// against the engine: every W of the grid, from 100, the lowest the server takes as true
/// (`wot::PLAUSIBLE`), to 10 000 000; every usual turnout of `usuals` — the reference, medians under
/// 1 % of W, within the bounds, above 50 %, each brought back within its bounds as the engine does;
/// the five levels; the quorums an author may set, from 2 to W (`author_quorums`).
fn grid() -> Vec<Case> {
let mut grid = Vec::new();
for w in ELECTORATES {
let (lowest, _) = quorum_bounds(w, false);
for usual in usuals(w, 50.0) {
let around: Vec<f64> = std::iter::once(usual.voters(w)).chain(usual.median.map(f64::from)).collect();
for k in InertiaLevel::ALL {
for q in author_quorums(w, lowest, &around) {
grid.push(Case { w, usual, k, q });
}
}
}
}
grid
}
/// The share the engine asks of `voters` voters on a web of `w` — `voters` the participation
/// habituelle, not a whole number when it is a bound (1 % of 7 224 is 72,24 voters). The engine
/// reads T'/W only (`required_ratio`): on a web a hundred times larger, the same share is asked of a
/// hundred times more voters, a whole number for every usual turnout of the grid. Checked against
/// the engine on whole numbers (`the_share_of_a_level_is_asked_at_the_usual_turnout_…`).
fn share_at(w: u32, voters: f64, params: &FormulaParams) -> f64 {
let scaled = voters * 100.0;
assert!((scaled - scaled.round()).abs() < 1e-6, "{voters} voters on {w}");
required_ratio(w * 100, scaled.round() as u32, params)
}
/// N1.1, « Chaque cran a aussi son quorum : sous le quorum de l'article, les votants qui manquent
/// comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté » (lot T3; « de
/// voix pour que » read first as « pour que » until 2ce2028; a bare « sous le quorum », after « Chaque
/// cran a aussi son quorum », read first as the level's until e7ab82f, an article holding the one its
/// author set). F1.2, « Q, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme
/// « non » » and « Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un
/// texte ». True for every quorum an author may set (2 to W in Monnaie libre, 1 to W in the test
/// group), every level, every W, every participation of the grid (both bounds, where medians under
/// 1 % and above 50 % of W are brought back): the « pour » that pass with nobody against, fewer
/// voters than the quorum, are the threshold of the quorum; under it, a tally passes exactly when its
/// « pour » reach that threshold; in Monnaie libre, one « pour » never passes, whatever the
/// « contre ». The phrase of T2, « qui n'est pas un nombre minimal de votants », is false: at a
/// quorum of 2, two « pour » are asked, at every level, on every W.
#[test]
fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
let today = today_seed();
let said = [
("fr", "et il faut autant de « pour » que si le quorum avait voté."),
("en", "and it takes as many votes in favour as if the quorum had voted."),
("es", "y hacen falta tantos votos a favor como si hubiera votado el quórum."),
(
"fr",
"sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté.",
),
(
"en",
"below the article's quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if that quorum had voted.",
),
(
"es",
"por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum.",
),
];
for (lang, phrase) in said {
assert!(text(&today, "N1.1", lang).1.contains(phrase), "N1.1 {lang}: « {phrase} »");
@@ -789,7 +1040,11 @@ fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
for &w in electorates {
let (lowest, highest) = quorum_bounds(w, test_group);
assert_eq!(highest, w.max(lowest));
for p in participations(w, vote_floor) {
let ps = participations(w, vote_floor);
if !test_group {
assert!(ps.contains(&Participation::LOWEST) && ps.contains(&Participation::HIGHEST), "W {w}: {ps:?}");
}
for p in ps {
let scale = Scale { electorate: w, participation: p, quorums: [lowest; 5] };
for k in InertiaLevel::ALL {
for q in quorums(w, lowest) {
@@ -810,6 +1065,13 @@ fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
}
}
}
// F1.2: in Monnaie libre, one « pour » never passes, whatever the « contre ».
if !test_group {
for n in [0, 1, q, w - 1] {
let lone = Tally { for_votes: 1, against: n };
assert!(!passes(lone, w, &params).expect("valid"), "{at}: 1 pour, {n} contre");
}
}
if at_quorum == q {
minimum.push((test_group, w, k.get(), q));
}
@@ -830,75 +1092,136 @@ fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
assert!(minimum.iter().any(|m| m.0 && m.3 == 1));
}
/// One case of the grid of « comme d'habitude »: a web, the usual turnout of its group (in voters,
/// and as p̃), a level and a quorum.
struct AsUsual {
w: u32,
usual: u32,
p: f64,
k: InertiaLevel,
q: u32,
}
impl AsUsual {
fn at(&self) -> String {
format!("W {} usual {} k {} Q {}", self.w, self.usual, self.k.get(), self.q)
/// N1.1, « La part exigée dépend du cran et du nombre de votants, comparé à la participation
/// habituelle du groupe : au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la
/// majorité M quand tout le monde vote » (lot T3c: « la part exigée », the one the formula below
/// names; « ce quorum », the article's). On the grid: under the article's quorum, the share asked
/// stays that of the quorum; from it, it falls with every voter more, strictly, down to M when
/// everyone votes; and it depends on the participation habituelle (at it, s_k:
/// `the_share_of_a_level_is_asked_at_the_usual_turnout_…`). « La part » alone could be read as the
/// share of « pour » the voters must bring in whole votes: that one rises at times beyond the quorum
/// — 9 « pour » of 12 voters, 75 %, then 10 of 13, 76,9 % (W 5 707, the reference turnout, level 2,
/// quorum 12), while the share asked falls from 73,5 % to 73,2 %.
#[test]
fn the_share_asked_falls_beyond_the_article_s_quorum_down_to_m() {
let today = today_seed();
for (lang, phrase) in [
(
"fr",
"La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.",
),
(
"en",
"The share asked depends on the level and on the number of voters, compared with the group's usual turnout: beyond that quorum, the more voters, the lower it gets, down to the majority M when everyone votes.",
),
(
"es",
"La proporción exigida depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá de ese quórum, cuantos más votantes, más baja, hasta la mayoría M cuando vota todo el mundo.",
),
] {
assert!(text(&today, "N1.1", lang).1.contains(phrase), "N1.1 {lang}: « {phrase} »");
}
}
/// The grid of « comme d'habitude » (lot T3 and its review): every W of the grid, usual turnouts
/// within the bounds (a median out of them is brought back, and is then not the group's usual
/// turnout), every level, quorums from 2 to W, among them the usual turnout and one more.
fn as_usual_grid() -> Vec<AsUsual> {
let mut grid = Vec::new();
for w in ELECTORATES {
for usual in [w / 100 + 1, 12, 40, 50, 100, 120, 1000, w / 2] {
let part = Participation::of(w, 50.0, Some(f64::from(usual)), 9);
if part.bounded {
continue;
}
for k in InertiaLevel::ALL {
for q in [2, 12, 50, 100, 1000, usual, usual + 1, w].into_iter().filter(|q| (2..=w).contains(q)) {
grid.push(AsUsual { w, usual, p: part.ratio, k, q });
}
let (mut under, mut beyond) = (0u64, 0u64);
for c in grid() {
let p = c.params();
let share = |t: u32| required_ratio(c.w, judged_voters(t, c.q), &p);
let at = c.at();
// Every number of voters on the smallest webs; beyond, around the quorum, the participation
// habituelle and the median, W/2, W.
let mut ts: Vec<u32> = if c.w <= 101 {
(0..=c.w).collect()
} else {
let usual = c.voters();
let mut ts = vec![0, 1, c.q - 1, c.q, c.q + 1, c.q + 2, 2 * c.q, usual.floor() as u32, usual.ceil() as u32];
ts.extend(c.usual.median);
ts.extend([c.w / 2, c.w - 1, c.w]);
ts
};
ts.retain(|t| *t <= c.w);
ts.sort_unstable();
ts.dedup();
for pair in ts.windows(2) {
let (a, b) = (pair[0], pair[1]);
if a >= c.q {
assert!(share(b) < share(a), "{at}: from {a} to {b} voters, {} then {}", share(a), share(b));
beyond += 1;
} else if b <= c.q {
assert_eq!(share(a), share(b), "{at}: under the quorum, {a} and {b} voters");
under += 1;
}
}
assert!((share(c.w) - c.m()).abs() < 1e-12, "{at}: M when everyone votes");
}
grid
assert!(under > 0 && beyond > 0, "under {under}, beyond {beyond}");
// The share of « pour » in whole votes is not the share asked: it rises from 12 to 13 voters.
let w = 5707;
let scale = Scale { electorate: w, participation: Participation::of(w, 50.0, None, 0).ratio, quorums: [12; 5] };
let p = scale.params(level(2), 12);
let (at12, at13) = (wot_threshold(w, 12, &p).expect("valid"), wot_threshold(w, 13, &p).expect("valid"));
assert_eq!((at12, at13), (9, 10));
assert!(f64::from(at13) / 13.0 > f64::from(at12) / 12.0);
assert!(required_ratio(w, 13, &p) < required_ratio(w, 12, &p));
}
/// The letters of the formula for a case of the grid (the quorums of the scale do not enter: the
/// quorum is the case's).
fn as_usual_params(c: &AsUsual) -> FormulaParams {
Scale { electorate: c.w, participation: c.p, quorums: [12, 12, 50, 50, 50] }.params(c.k, c.q)
}
/// N1.1 and F1.2, lot T3: the share of a level « comme d'habitude » is asked beyond the quorum
/// only. When the usual turnout reaches the quorum, the group voting as usual is asked s_k; when
/// it does not (a web of fewer than 5 000 members and a median under 50 at a quorum of 50, an
/// author's quorum above the usual turnout), it is asked the threshold of the quorum, which is
/// not s_k of those who came. Every W, every median within the bounds, every level, quorums.
/// N1.1, « Les cinq crans : le quorum dans Monnaie libre, la part exigée au-delà du quorum de
/// l'article, à la participation habituelle, puis quand tout le monde vote » and its five lines
/// (« 60 % à la participation habituelle, 50 % quand tout le monde vote »…); F1.2, « au-delà du
/// quorum, le groupe demande 75 % de « pour » à sa participation habituelle ». Lot T3 wrote « comme
/// d'habitude », which reads first as the real median: false where the median leaves its bounds
/// (`the_participation_habituelle_is_the_median_brought_back_within_its_bounds`); the defined term
/// since the review of e7ab82f. On the grid: when the participation habituelle reaches the article's
/// quorum, the share asked there is s_k exactly; when it does not (a quorum above it: the author's,
/// or the level's on a small web), the voters are judged as the quorum, at a share lower than s_k,
/// asked of more voters than came; when everyone votes, M_k, at every quorum.
#[test]
fn the_share_of_a_level_as_usual_is_asked_beyond_the_quorum() {
let mut under = 0;
for c in as_usual_grid() {
let (s, m) = (c.k.share_pct() / 100.0, c.k.majority_pct() / 100.0);
let params = as_usual_params(&c);
fn the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum() {
let today = today_seed();
for (lang, intro) in [
(
"fr",
"la part exigée au-delà du quorum de l'article, à la participation habituelle, puis quand tout le monde vote",
),
("en", "the share asked beyond the article's quorum, at the usual turnout, then when everyone votes"),
(
"es",
"la proporción exigida más allá del quórum del artículo, con la participación habitual, luego cuando vota todo el mundo",
),
] {
assert!(text(&today, "N1.1", lang).1.contains(intro), "N1.1 {lang}: « {intro} »");
}
// The five lines and F1.2 carry their numbers: `every_number_of_the_quorum_texts_is_the_engine_s`.
let (mut beyond, mut under, mut low, mut high) = (0u64, 0u64, 0u64, 0u64);
for c in grid() {
let (s, m, p) = (c.s(), c.m(), c.params());
let at = c.at();
let asked = required_ratio(c.w, judged_voters(c.usual, c.q), &params);
if c.usual >= c.q {
let usual = c.voters();
if (usual - usual.round()).abs() < 1e-9 {
let whole = required_ratio(c.w, usual.round() as u32, &p);
assert!(close(share_at(c.w, usual, &p), whole), "{at}: the share on a web a hundred times larger");
}
let asked = share_at(c.w, usual.max(f64::from(c.q)), &p);
if usual >= f64::from(c.q) {
assert!((asked - s).abs() < 1e-9, "{at}: {asked}, not s");
beyond += 1;
} else {
assert_eq!(asked, required_ratio(c.w, c.q, &p), "{at}: judged as the quorum");
assert!(asked < s - 1e-9, "{at}: {asked}");
under += 1;
}
if c.usual.part.bounded && usual >= f64::from(c.q) {
if c.usual.part.ratio == Participation::LOWEST {
low += 1;
} else {
high += 1;
}
}
// Everyone votes: M, at every quorum.
assert!((required_ratio(c.w, judged_voters(c.w, c.q), &params) - m).abs() < 1e-12, "{at}: M");
assert!((required_ratio(c.w, judged_voters(c.w, c.q), &p) - m).abs() < 1e-12, "{at}: M");
}
assert!(under > 0);
assert!(beyond > 0 && under > 0 && low > 0 && high > 0, "beyond {beyond}, under {under}, low {low}, high {high}");
// The case of F1.2, its default level and quorum: W 3 000, a usual turnout of 40 voters
// (within the bounds: 1 % is 30). Voting as usual, the group is asked 38 « pour » of its 40
// voters: 95 %, not 75 %.
// (within the bounds: 1 % is 30). At its participation habituelle, the group is asked 38 « pour »
// of its 40 voters: 95 %, not 75 % — under the quorum, which the sentence leaves out.
let (w, usual) = (3000, 40);
let part = Participation::of(w, 50.0, Some(f64::from(usual)), 9);
assert!(!part.bounded);
@@ -912,19 +1235,20 @@ fn the_share_of_a_level_as_usual_is_asked_beyond_the_quorum() {
assert_eq!(wot_threshold(w, usual, &p), Ok(38), "but 40 voters are judged as 50");
}
/// N1.1, the sentence of G, after the review of 2ce2028. Lot T3 left « G se calcule pour que la
/// part exigée, à la participation habituelle, soit exactement celle du cran […], où s est la part
/// exigée comme d'habitude »: false where the usual turnout is under the quorum, the group voting
/// as usual being judged on the quorum (T' = Q), lower on the curve than s, and asked of more
/// voters than came. Written true for every W and every quorum: G is computed so that the share
/// asked, beyond the quorum, is exactly that of the level when the group votes as usual; s is that
/// share of the level, the one of the list (60, 67, 75, 83 or 90 %) — never « la part exigée »,
/// which the formula above it names and which moves with T'. On the grid of « comme d'habitude »,
/// with G as the text writes it.
/// N1.1, the sentence of G: « G se calcule pour que la part exigée, au-delà du quorum de l'article,
/// soit exactement celle du cran à la participation habituelle : `G = …`, où s est cette part du
/// cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W » — after the
/// reviews of 2ce2028 (« au-delà du quorum »: under it, the voters are judged as the quorum) and of
/// e7ab82f (the defined term where T3b wrote « quand le groupe vote comme d'habitude »; « du quorum
/// de l'article »). s is the share of the level, the one of the list — never « la part exigée »,
/// which the formula above names and which moves with T'. On the grid, with G as the text writes
/// it and p̃ the participation habituelle over W, the median brought back within its bounds: G is
/// the engine's, at every quorum; on the curve, the share at the participation habituelle is s; the
/// share asked there is s beyond the article's quorum, and lower, judged as the quorum, under it.
#[test]
fn g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usual() {
fn g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum() {
let today = today_seed();
// The sentence of G closes N1.1, in each language; nothing of the sentence of T3 is left.
// The sentence of G closes N1.1, in each language; nothing of the sentence of T2 is left.
for lang in LANGS {
let (_, body) = text(&today, "N1.1", lang);
let (_, _, before, now) = PHRASES_T3
@@ -933,44 +1257,50 @@ fn g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usua
.expect("the sentence of G");
assert!(body.ends_with(now), "N1.1 {lang}: the sentence of G\n{body}");
let (said_before, _) = before.split_once(" = ").expect("the formula");
assert!(!body.contains(said_before), "N1.1 {lang}: the sentence of T3 is left\n{body}");
let beyond = named(lang, "au-delà du quorum", "beyond the quorum", "más allá del quórum");
assert!(!body.contains(said_before), "N1.1 {lang}: the sentence of T2 is left\n{body}");
let (said_now, _) = now.split_once(" = ").expect("the formula");
assert!(said_now.contains(beyond), "N1.1 {lang}: « {beyond} »");
for words in [
named(
lang,
"au-delà du quorum de l'article",
"beyond the article's quorum",
"más allá del quórum del artículo",
),
named(lang, "à la participation habituelle", "at the usual turnout", "con la participación habitual"),
] {
assert!(said_now.contains(words), "N1.1 {lang}: « {words} »");
}
}
// By the engine, with G as the text writes it (s, the share of the level; M; p̃). On the curve,
// the share at the usual turnout is s, whatever the quorum: that is what G is computed for.
// The share asked of the group voting as usual is s beyond the quorum; under it, the share of
// the quorum, lower — the sentence of T3 said s there.
// By the engine, with G as the text writes it (s, the share of the level; M; p̃).
let grad = code_spans(text(&today, "N1.1", "fr").1)[1];
let (mut beyond, mut under) = (0, 0);
for c in as_usual_grid() {
let (s, m) = (c.k.share_pct() / 100.0, c.k.majority_pct() / 100.0);
let env: HashMap<String, f64> = HashMap::from([("s".into(), s), ("M".into(), m), ("p̃".into(), c.p)]);
let engine = as_usual_params(&c);
let (mut beyond, mut under) = (0u64, 0u64);
for c in grid() {
let (s, m) = (c.s(), c.m());
let env: HashMap<String, f64> =
HashMap::from([("s".into(), s), ("M".into(), m), ("p̃".into(), c.usual.part.ratio)]);
let engine = c.params();
let params = FormulaParams { gradient: eval(grad, "G", &env), ..engine };
let at = c.at();
assert!(close(params.gradient, engine.gradient), "{at}: G");
assert!((required_ratio(c.w, c.usual, &params) - s).abs() < 1e-9, "{at}: s on the curve");
let judged = judged_voters(c.usual, c.q);
let asked = required_ratio(c.w, judged, &params);
if c.usual >= c.q {
assert_eq!(judged, c.usual, "{at}");
let usual = c.voters();
assert!((share_at(c.w, usual, &params) - s).abs() < 1e-9, "{at}: s on the curve");
let asked = share_at(c.w, usual.max(f64::from(c.q)), &params);
if usual >= f64::from(c.q) {
assert!((asked - s).abs() < 1e-9, "{at}: {asked}, not s");
beyond += 1;
} else {
assert_eq!(judged, c.q, "{at}");
assert!(asked < s - 1e-9, "{at}: {asked}");
under += 1;
}
}
assert!(beyond > 0 && under > 0, "beyond {beyond}, under {under}");
// The case of the review: W 3 000, a usual turnout of 40 voters, level 3, quorum 50. On the
// curve, 75 % at 40 voters; the share asked is that of 50 voters, 74,23 %; 38 « pour » of the
// 40 who came, 95 %.
// The case of the review of 2ce2028: W 3 000, a usual turnout of 40 voters, level 3, quorum 50.
// On the curve, 75 % at 40 voters; the share asked is that of 50 voters, 74,23 %; 38 « pour »
// of the 40 who came, 95 %.
let (w, usual) = (3000, 40);
let c = AsUsual { w, usual, p: Participation::of(w, 50.0, Some(f64::from(usual)), 9).ratio, k: level(3), q: 50 };
let p = as_usual_params(&c);
let part = Participation::of(w, 50.0, Some(f64::from(usual)), 9);
let c = Case { w, usual: Usual { median: Some(usual), part }, k: level(3), q: 50 };
let p = c.params();
assert!((required_ratio(w, usual, &p) - 0.75).abs() < 1e-12);
let asked = required_ratio(w, judged_voters(usual, c.q), &p);
assert_eq!((asked * 10_000.0).round() / 100.0, 74.23);
@@ -978,8 +1308,113 @@ fn g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usua
assert_eq!((needed, f64::from(needed) / f64::from(usual)), (38, 0.95));
}
/// N1.1, « Tant que le groupe n'a pas voté sur 5 propositions, elle vaut deux fois le quorum de
/// référence du document : 100 votants dans Monnaie libre. […] Elle reste entre 1 % et 50 % de
/// N1.1, « La participation habituelle est la médiane du nombre de votants des propositions du
/// groupe votées depuis un an. Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois
/// le quorum de référence du document […]. Elle est ramenée entre 1 % et 50 % de l'électorat » —
/// and why F1.2 and N1.1 write this defined term where lot T3 wrote « comme d'habitude » (review of
/// e7ab82f). « Quand le groupe vote comme d'habitude » reads first as « when as many vote as the
/// median »: the engine computes G on the median brought back within its bounds, and where the
/// median is out of them, the share asked of as many voters as the median is not s_k — higher under
/// 1 % of W, lower above 50 %. The case of the review: W 7 224, a median of 40 voters (0,55 %),
/// level 2, quorum 12 — at 40 voters the share asked is 68,7 %, 28 « pour » of 40, not 27 (67 %);
/// at the participation habituelle, 72,24 voters (1 % of W), 67 % exactly. « Tant que le groupe n'a
/// pas voté sur 5 propositions » (T2) was false once the group had: the median rests on the
/// proposals voted on within the year, and with fewer than 5 there the reference stands in again
/// (`tests/inertia.rs`, `the_usual_participation_is_the_median_of_five_voted_proposals`); « Elle
/// reste entre » read as a fact, not as the rule that brings the median back.
#[test]
fn the_participation_habituelle_is_the_median_brought_back_within_its_bounds() {
let today = today_seed();
// The texts write the defined term, never the paraphrase: in N1.1, the list, its five lines and
// G (and, in French and Spanish, « comparé à la participation habituelle du groupe », already
// there); in F1.2, « Par défaut ».
for (lang, paraphrase, term, times, its, rests, bounds) in [
(
"fr",
"comme d'habitude",
"à la participation habituelle",
8,
"à sa participation habituelle",
"Tant qu'elle repose sur moins de",
"Elle est ramenée entre",
),
(
"en",
"as usual",
"at the usual turnout",
7,
"at its usual turnout",
"While it rests on fewer than",
"It is kept between",
),
(
"es",
"como de costumbre",
"con la participación habitual",
8,
"con su participación habitual",
"Mientras se base en menos de",
"Se acota entre",
),
] {
let (n11, f12) = (text(&today, "N1.1", lang).1, text(&today, "F1.2", lang).1);
for (code, body) in [("N1.1", n11), ("F1.2", f12)] {
assert!(!body.contains(paraphrase), "{code} {lang}: « {paraphrase} »\n{body}");
}
assert_eq!(n11.matches(term).count(), times, "N1.1 {lang}: the list, its five lines, G\n{n11}");
assert_eq!(f12.matches(its).count(), 1, "F1.2 {lang}: by default");
assert!(n11.contains(rests) && n11.contains(bounds), "N1.1 {lang}: the definition");
}
// The engine: the median — or, on fewer than 5 proposals, the reference — brought back within
// 1 % and 50 % of W.
for w in ELECTORATES {
for u in usuals(w, 50.0) {
let raw = match u.median {
Some(m) => f64::from(m) / f64::from(w),
None => Participation::reference_ratio(w, 50.0),
};
let at = format!("W {w} {u:?}");
assert_eq!(u.part.ratio, raw.clamp(Participation::LOWEST, Participation::HIGHEST), "{at}");
assert_eq!(u.part.bounded, u.part.ratio != raw, "{at}");
if let Some(m) = u.median {
let few = Participation::of(w, 50.0, Some(f64::from(m)), Participation::MIN_SAMPLE - 1);
assert_eq!(few.ratio, Participation::of(w, 50.0, None, 0).ratio, "{at}: the reference");
}
}
}
// The case of the review.
let (w, median) = (7224, 40);
let part = Participation::of(w, 50.0, Some(f64::from(median)), 9);
assert_eq!((part.bounded, part.ratio), (true, Participation::LOWEST));
let c = Case { w, usual: Usual { median: Some(median), part }, k: level(2), q: 12 };
let p = c.params();
assert_eq!((required_ratio(w, median, &p) * 1000.0).round() / 10.0, 68.7);
assert_eq!(wot_threshold(w, median, &p), Ok(28));
assert_eq!((c.s() * f64::from(median)).ceil(), 27.0, "67 % of 40, rounded up");
assert!(!passes(Tally { for_votes: 27, against: 13 }, w, &p).expect("valid"));
assert!(passes(Tally { for_votes: 28, against: 12 }, w, &p).expect("valid"));
assert!((c.voters() - 72.24).abs() < 1e-9);
assert!((share_at(w, c.voters(), &p) - c.s()).abs() < 1e-12, "67 % at the participation habituelle");
// On the grid: every median out of its bounds that reaches the quorum — as many voters as the
// median are not asked s_k.
let (mut under_one, mut above_half) = (0u64, 0u64);
for c in grid() {
let Some(median) = c.usual.median.filter(|m| c.usual.part.bounded && *m >= c.q) else { continue };
let at = c.at();
let at_median = required_ratio(c.w, median, &c.params());
if c.usual.part.ratio == Participation::LOWEST {
assert!(at_median > c.s() + 1e-9, "{at}: {at_median}");
under_one += 1;
} else {
assert!(at_median < c.s() - 1e-9, "{at}: {at_median}");
above_half += 1;
}
}
assert!(under_one > 0 && above_half > 0, "under 1 % {under_one}, above 50 % {above_half}");
}
/// N1.1, « Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de
/// référence du document : 100 votants dans Monnaie libre. […] Elle est ramenée entre 1 % et 50 % de
/// l'électorat » (review of 2ce2028). 100 is a parameter, 2 × voteFloor, which the number test
/// checks as such (`figures`); it is not a turnout that holds on every W: the engine's p0 is 100
/// voters on a web of 200 to 10 000 members only (1 % and 50 % of W); beyond, the bounds the
@@ -1002,9 +1437,9 @@ fn the_reference_turnout_is_a_parameter_the_bounds_bring_back() {
}
// The text says the parameter first, then the bounds, in each language.
for (lang, reference, bounds) in [
("fr", format!("{} votants dans Monnaie libre.", f.reference), "Elle reste entre"),
("en", format!("{} voters in Monnaie libre.", f.reference), "It stays between"),
("es", format!("{} votantes en Monnaie libre.", f.reference), "Se mantiene entre"),
("fr", format!("{} votants dans Monnaie libre.", f.reference), "Elle est ramenée entre"),
("en", format!("{} voters in Monnaie libre.", f.reference), "It is kept between"),
("es", format!("{} votantes en Monnaie libre.", f.reference), "Se acota entre"),
] {
let (_, body) = text(&seed, "N1.1", lang);
let at = |s: &str| body.find(s).unwrap_or_else(|| panic!("N1.1 {lang}: « {s} »"));
@@ -1017,7 +1452,9 @@ fn the_reference_turnout_is_a_parameter_the_bounds_bring_back() {
/// web of trust (`wot::PLAUSIBLE`, 100 to 10 000 000) — the engine's threshold is the one
/// without B, even where the share lands on a whole number (`threshold::settle`, `clamp_ceil`):
/// M × W when everyone votes, s × the usual turnout. Where W is a handful of members (the test
/// group, whose W is not the web of trust these texts speak of), B^W does count.
/// group, whose W is not the web of trust these texts speak of), B^W does count. Lot T3c: on the
/// participations of the grid (both bounds, where medians under 1 % and above 50 % of W are brought
/// back) and quorums from 2 to W (`quorum_sample`).
#[test]
fn b_to_the_power_w_never_counts_in_the_web_of_trust() {
assert_eq!((*PLAUSIBLE.start(), *PLAUSIBLE.end()), (ELECTORATES[0], ELECTORATES[ELECTORATES.len() - 1]));
@@ -1027,7 +1464,7 @@ fn b_to_the_power_w_never_counts_in_the_web_of_trust() {
let usual = (p * f64::from(w)).round() as u32;
let scale = Scale { electorate: w, participation: p, quorums: [12, 12, 50, 50, 50] };
for k in InertiaLevel::ALL {
for q in [2, 12, 50, 100, w].into_iter().filter(|q| *q <= w) {
for q in quorum_sample(w, 2, &[p * f64::from(w)]) {
let with_b = scale.params(k, q);
assert_eq!(with_b.base, 0.1);
let without = FormulaParams { base: 0.0, ..with_b };
@@ -1199,14 +1636,22 @@ fn the_formulas_of_the_texts_are_those_the_engine_computes() {
let [voters, threshold] = <[&str; 2]>::try_from(code_spans(f11)).expect("two formulas in F1.1");
let [share, grad] = <[&str; 2]>::try_from(code_spans(n11)).expect("two formulas in N1.1");
let mut checked = 0;
for w in [2u32, 40, 5707, 7224] {
for median in [None, Some(1.0), Some(9.0), Some(120.0), Some(360.0)] {
let p = Participation::of(w, 50.0, median, 9).ratio;
// Lot T3c: every W of the grid, and two of a handful of members; every participation of the
// grid (both bounds among them, where medians under 1 % and above 50 % of W are brought
// back: p̃ is the participation habituelle over W); quorums from 1 to W.
for w in [2u32, 40].into_iter().chain(ELECTORATES) {
for p in participations(w, 50.0) {
let scale = Scale { electorate: w, participation: p, quorums: [12, 12, 50, 50, 50] };
let usual = p * f64::from(w);
let (down, up) = (usual.floor() as u32, usual.ceil() as u32);
let mut quorums = vec![1u32, 2, 12, 50, up, w / 2, w];
quorums.retain(|q| (1..=w).contains(q));
quorums.sort_unstable();
quorums.dedup();
for k in InertiaLevel::ALL {
for q in [1u32, 2, 12, 50].into_iter().filter(|q| *q <= w) {
for &q in &quorums {
let params = scale.params(k, q);
for t in [0u32, 1, 9, 12, 13, 50, 51, 120, 500, w].into_iter().filter(|t| *t <= w) {
for t in [0u32, 1, 9, 12, 13, 50, 51, 120, 500, down, up, w].into_iter().filter(|t| *t <= w) {
let mut env: HashMap<String, f64> = HashMap::from([
("T".into(), f64::from(t)),
("Q".into(), f64::from(q)),