forked from yvv/decision
Fusion du lot U3c (relecture de d66823b) : formula.reading dit « quand tout le monde vote, la part exigée est la majorité M » (fr, en, es), vrai pour tout W (grille ld-engine de W = 1 à 10 000 000) ; les raisons des lignes de file de M9 disent « Proposition d'amorce réécrite / ajoutée »
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -430,6 +430,104 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lot U3c (review of d66823b), `formula.reading`: « quand tout le monde vote, la part exigée est
|
||||
/// la majorité M ». On every W from 1 to 10 000 000 — the groups the server counts by their
|
||||
/// members (the test group: 2) and the web of trust it takes as true (`wot::PLAUSIBLE`, 100 to
|
||||
/// 10 000 000) — at the five levels, on both bounds of the quorum an author may set (in Monnaie
|
||||
/// libre and in the test group: 1, 2, W) and at both bounds of the usual participation (G at its
|
||||
/// lowest and at its highest): when the W vote, the voters judged are T′ = max(W, Q), never fewer
|
||||
/// than W, and the share asked of them is M_k, exactly.
|
||||
///
|
||||
/// The phrase of before, « quand tout le monde vote, la majorité M suffit », read as « ⌈M·W⌉
|
||||
/// « pour » pass », was false: R = B^W + M·W, and B^W adds one « pour » where M·W is a whole
|
||||
/// number, on 17 (W, level) pairs, every W up to 15 (W 4, level 2: 3 « pour » of 4, not 2; W 5,
|
||||
/// level 4: 4, not 3; W 10, level 4: 7, not 6) — none from W = 16 to 200, where it is checked.
|
||||
/// The new phrase says nothing of B^W: `formula.B.wot` does, in the web of trust only.
|
||||
#[test]
|
||||
fn when_everyone_votes_the_share_asked_is_m_on_every_electorate() {
|
||||
const LARGEST: u32 = 10_000_000;
|
||||
let params: Vec<(InertiaLevel, f64, FormulaParams)> = InertiaLevel::ALL
|
||||
.iter()
|
||||
.flat_map(|k| {
|
||||
[Participation::LOWEST, Participation::HIGHEST].map(|p| {
|
||||
let scale = Scale { electorate: 1, participation: p, quorums: [1; 5] };
|
||||
(*k, k.majority_pct() / 100.0, scale.params(*k, 1))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut cases = 0u64;
|
||||
for w in 1..=LARGEST {
|
||||
let ((lowest, highest), (lowest_essai, _)) = (quorum_bounds(w, false), quorum_bounds(w, true));
|
||||
// The share reads the voters judged only: W for every quorum up to W, the quorum above
|
||||
// it (2 on a web of 1, outside the test group).
|
||||
let judged = [lowest_essai, lowest, highest].map(|q| judged_voters(w, q));
|
||||
assert!(judged.iter().all(|v| *v >= w), "W {w}: {judged:?}");
|
||||
for (i, voters) in judged.iter().enumerate() {
|
||||
if judged[..i].contains(voters) {
|
||||
continue;
|
||||
}
|
||||
for (k, m, p) in ¶ms {
|
||||
let share = required_ratio(w, *voters, p);
|
||||
assert_eq!(share, *m, "W {w}, level {}, G {}, {voters} voters judged", k.get(), p.gradient);
|
||||
cases += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Every W, ten (level, G) each, and on a web of 1 the quorum of 2 besides.
|
||||
assert_eq!(cases, u64::from(LARGEST) * 10 + 10);
|
||||
// The share the evaluation gives (`Evaluation::required_ratio`), on every split of the W
|
||||
// voters, on the smallest electorates and on the web of trust.
|
||||
for w in (1..=40).chain([100, 101, 5707, 7224, LARGEST]) {
|
||||
let scale =
|
||||
Scale { electorate: w, participation: Participation::of(w, 50.0, None, 0).ratio, quorums: [2; 5] };
|
||||
for k in InertiaLevel::ALL {
|
||||
for q in [quorum_bounds(w, false).0, w] {
|
||||
for against in [0, w / 3, w / 2, w] {
|
||||
let tally = Tally { for_votes: w - against, against };
|
||||
let e = scale.evaluate(tally, k, q).expect("valid");
|
||||
assert_eq!(e.required_ratio, k.majority_pct() / 100.0, "W {w}, level {}, Q {q}", k.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// « La majorité M suffit » was false: the « pour » asked when the W vote, against ⌈M·W⌉.
|
||||
let mut more = Vec::new();
|
||||
for w in 1..=200u32 {
|
||||
for k in InertiaLevel::ALL {
|
||||
let p = Scale { electorate: w, participation: 0.1, quorums: [1; 5] }.params(k, 1);
|
||||
let asked = crate::threshold::wot_threshold(w, w, &p).expect("valid");
|
||||
let majority = (k.majority_pct() as u32 * w).div_ceil(100);
|
||||
if asked != majority {
|
||||
assert_eq!(asked, majority + 1, "W {w}, level {}: one « pour » more, B^W", k.get());
|
||||
assert_eq!(k.majority_pct() as u32 * w % 100, 0, "W {w}: M·W is a whole number");
|
||||
more.push((w, k.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
more,
|
||||
[
|
||||
(2, 1),
|
||||
(2, 2),
|
||||
(4, 1),
|
||||
(4, 2),
|
||||
(5, 4),
|
||||
(6, 1),
|
||||
(6, 2),
|
||||
(8, 1),
|
||||
(8, 2),
|
||||
(10, 1),
|
||||
(10, 2),
|
||||
(10, 4),
|
||||
(12, 1),
|
||||
(12, 2),
|
||||
(14, 1),
|
||||
(14, 2),
|
||||
(15, 4)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_author_sets_a_quorum_from_two_to_w_from_one_in_the_test_group() {
|
||||
assert_eq!(quorum_bounds(5745, false), (2, 5745));
|
||||
|
||||
@@ -60,7 +60,7 @@ pub const AMENDMENTS: &[ArticleAmendment] = &[ArticleAmendment {
|
||||
text: "3fa5a8e5df24e9ebb6b2882968bd0b6989c820a3f60d7c67bdb7fccf17ae1296",
|
||||
after: "E1",
|
||||
before: "E2",
|
||||
reason: "Proposition seed ajoutée : E12, Responsabilité civique (texte d'Yvv du 03/11)",
|
||||
reason: "Proposition d'amorce ajoutée : E12, Responsabilité civique (texte d'Yvv du 03/11)",
|
||||
}];
|
||||
|
||||
/// What the database says of the document.
|
||||
|
||||
@@ -280,7 +280,7 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
version: Version::SeedProposal,
|
||||
from: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
|
||||
to: "32eb36b4c678c97d4597f0e4cc494a92c9956792839d6eb398a81c1dcc46320f",
|
||||
reason: "Proposition seed réécrite : I1, la Raison d'être (texte d'Yvv du 03/11)",
|
||||
reason: "Proposition d'amorce réécrite : I1, la Raison d'être (texte d'Yvv du 03/11)",
|
||||
follows: Some("2026-10-30-i1-raison-d-etre"),
|
||||
superseded_by: None,
|
||||
},
|
||||
@@ -291,7 +291,7 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
version: Version::SeedProposal,
|
||||
from: "bdca2713fdb1186bc59ac130f13a205a35376986f14f7cfba7223b37ed9a41bd",
|
||||
to: "df31d0ec5f7f0f8f88d930347ce370634881ca62aed52beb3ffe780fe992b9c9",
|
||||
reason: "Proposition seed réécrite : E1, l'unicité du compte certifiant (texte d'Yvv du 03/11)",
|
||||
reason: "Proposition d'amorce réécrite : E1, l'unicité du compte certifiant (texte d'Yvv du 03/11)",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
},
|
||||
@@ -302,7 +302,7 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
version: Version::SeedProposal,
|
||||
from: "bde76a30edaf8b623f3de0ff7eb94ec57eb5134b3c8aeae1590798fb6b9956f0",
|
||||
to: "a7dd469004c0e495524a9c38305177c352e36cd240f59a3b43e28d9222a6ae24",
|
||||
reason: "Proposition seed réécrite : E2, la réciprocité des trois principes (texte d'Yvv du 03/11)",
|
||||
reason: "Proposition d'amorce réécrite : E2, la réciprocité des trois principes (texte d'Yvv du 03/11)",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
},
|
||||
|
||||
@@ -710,6 +710,62 @@ async fn production_after_m6c_gets_lot_t4_once_and_nothing_is_erased() {
|
||||
production_gets_lot_t4_once(production_after_m6c("m6c").await, 4).await;
|
||||
}
|
||||
|
||||
/// Lot U3c (review of d66823b): the reasons of the queue lines lot T4 writes become lines of the
|
||||
/// public commit of the texts when the first text enters into force. They name the seed proposals
|
||||
/// as the screen does, « Proposition d'amorce » (Yvv's word), never « Proposition seed ». The
|
||||
/// amendments of M6, applied in production (`seed_proposal_amend`), keep theirs: their lines are
|
||||
/// already in its queue.
|
||||
#[tokio::test]
|
||||
async fn the_queue_names_the_starter_proposals_as_the_screen_does() {
|
||||
const REWRITTEN: [(&str, &str); 3] = [
|
||||
("2026-11-03-i1-amorce", "Proposition d'amorce réécrite : I1, la Raison d'être (texte d'Yvv du 03/11)"),
|
||||
(
|
||||
"2026-11-03-e1-amorce",
|
||||
"Proposition d'amorce réécrite : E1, l'unicité du compte certifiant (texte d'Yvv du 03/11)",
|
||||
),
|
||||
(
|
||||
"2026-11-03-e2-amorce",
|
||||
"Proposition d'amorce réécrite : E2, la réciprocité des trois principes (texte d'Yvv du 03/11)",
|
||||
),
|
||||
];
|
||||
const ADDED: &str = "Proposition d'amorce ajoutée : E12, Responsabilité civique (texte d'Yvv du 03/11)";
|
||||
for (id, reason) in REWRITTEN {
|
||||
let a = ld_server::seed_amend::AMENDMENTS.iter().find(|a| a.id == id).expect(id);
|
||||
assert_eq!(a.reason, reason, "{id}");
|
||||
}
|
||||
let e12 = ld_server::article_amend::AMENDMENTS.iter().find(|a| a.id == ADD).expect(ADD);
|
||||
assert_eq!(e12.reason, ADDED);
|
||||
// The lines lot T4 writes on a base as production holds it after M8: Monnaie libre only.
|
||||
let base = production_after_m8("reasons").await;
|
||||
let last: i64 = count(&base, "SELECT IFNULL(MAX(id), 0) FROM outbox").await;
|
||||
let state = t4(&base).await;
|
||||
let written: Vec<(String, String)> = sqlx::query_as(
|
||||
"SELECT d.group_slug, o.reason FROM outbox o JOIN documents d ON d.id = o.doc_id WHERE o.id > ? ORDER BY o.id",
|
||||
)
|
||||
.bind(last)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.expect("outbox");
|
||||
assert!(written.iter().all(|(g, _)| g == "monnaie-libre"), "{written:?}");
|
||||
let reasons: Vec<&str> = written.iter().map(|(_, r)| r.as_str()).collect();
|
||||
let between = format!("{ADDED}, entre E1 et E2");
|
||||
for line in REWRITTEN.iter().map(|(_, r)| *r).chain([between.as_str()]) {
|
||||
assert_eq!(reasons.iter().filter(|r| **r == line).count(), 1, "« {line} » in {reasons:?}");
|
||||
}
|
||||
assert_eq!(reasons.len(), 5, "and the retirement of the rules of operation: {reasons:?}");
|
||||
assert!(reasons.iter().all(|r| !r.contains("seed")), "{reasons:?}");
|
||||
// M6's, unchanged.
|
||||
let m6: Vec<&str> = ld_server::seed_proposal_amend::AMENDMENTS.iter().map(|a| a.reason).collect();
|
||||
assert_eq!(
|
||||
m6,
|
||||
[
|
||||
"Proposition seed : I1 sort de la vigueur",
|
||||
"Proposition seed : E1 sort de la vigueur",
|
||||
"Proposition seed : E2 sort de la vigueur"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// After the retirement, the API refuses everything on them
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -108,3 +108,4 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa
|
||||
| 2026-11-03 | automate | local, lot T4 sur `3996f6f` (les textes d'Yvv du 03/11 pour I1, E1, E2 ; E12 « Responsabilité civique » entre E1 et E2 ; formule, inertie et ordonnancement sortis du document, ADR 0014 : C3, C9, C17, C18, C25, C31, C35, C38 repris ; C35 glisse d'une place) | C1–C38, M1, M2 | 40 / 40 verts (42 tests) à la boucle finale (21 h 47 - 21 h 50, sous le verrou `/tmp/ld-e2e.lock`) ; avant, 39 / 42 (C18, C31, C35 lisaient l'ancien texte d'I1 ou trois articles), puis 41 / 42 (C35 : un glisser de deux places réordonne mais n'est pas annoncé, signalé pour U3) |
|
||||
| 2026-11-03 | automate | local, lot U3 sur `d15d46e` (l'interface de l'Acte d'engagement : le chapitre « Info ouverture doc de réf. », la séparation, plus les titres « Introduction » et « Engagements fondamentaux », C39 ; « Proposition d'amorce » en fr, en, es, C40 ; le mot pour qui n'est pas invité, C41 ; une poignée tirée sur plusieurs lignes, annoncée et suivie par le focus, C42 ; les informations de fonctionnement là où elles servent, C43 ; C1, C3, C20, C27, C35, C38 suivent les mots) | C1–C43, M1, M2 | 45 / 45 verts (47 tests), du premier coup à la boucle finale (22 h 32 - 22 h 37, sous le verrou `/tmp/ld-e2e.lock`) ; avant le code, C39 à C43 vus rouges (5 / 5) sur `590ccd7` |
|
||||
| 2026-11-03 | automate | local, lot U3b sur `a6f5e22` (le tableau des cinq crans se lit en entier dans le panneau des réglages, sur bureau comme sur téléphone, en fr, en, es, C44) | C1–C44, M1, M2 | 46 / 46 verts (48 tests), du premier coup à la boucle finale (22 h 55 - 22 h 59, sous le verrou `/tmp/ld-e2e.lock`) ; avant le code, C44 vu rouge sur l'interface de `a47aed0` : 12 cas sur 18 (bureau 1360 et 1024, en tête et depuis une version, trois langues : le tableau défile, 406 à 501 px pour 394), le téléphone vert |
|
||||
| 2026-11-03 | automate | local, lot U3c sur `266a443` (après la relecture de `d66823b` : `formula.reading` dit « quand tout le monde vote, la part exigée est la majorité M » à la place de « la majorité M suffit », faux pour W ≤ 15, en fr, en, es ; les quatre raisons de la file de T4 disent « Proposition d'amorce » ; aucun contrôle changé) | C1–C44, M1, M2 | 46 / 46 verts (48 tests), du premier coup (23 h 42 - 23 h 46, sous le verrou `/tmp/ld-e2e.lock`) ; boucle : fmt, clippy, 338 tests Rust, npm ci, check 0 / 0, 116 tests unitaires, build |
|
||||
|
||||
@@ -43,7 +43,7 @@ test qui la prouve ; les nombres lus dans l'API, par les contrôles e2e (C20, C3
|
||||
|---|---|---|---|---|---|
|
||||
| F1.1.1 | « Le seuil est le nombre de voix pour qu'il faut à une version pour entrer en vigueur. » | au vote | « La formule du seuil » (déjà), R | `formula.intro`, `formula.R` | une définition |
|
||||
| F1.1.2 | « Sous le quorum Q, les votants qui manquent comptent comme « non » : le seuil se calcule comme si Q personnes avaient voté. » | au vote | « La formule du seuil », **changé** : « Sous le quorum Q de l'article, … : il faut autant de « pour » que si Q personnes avaient voté. » ; l'infobulle de « 9 oui sur 12 » (déjà) | `formula.reading`, `vote.need.title` | grille `under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted` |
|
||||
| F1.1.3 | « Au-delà, il faut la part de « pour » que demande le cran … la plus haute au quorum et baisse quand la participation monte, jusqu'à la majorité M » | au vote | « La formule du seuil » (déjà, suite de la même clé) | `formula.reading` | grille `the_share_asked_falls_beyond_the_article_s_quorum_down_to_m` |
|
||||
| F1.1.3 | « Au-delà, il faut la part de « pour » que demande le cran … la plus haute au quorum et baisse quand la participation monte, jusqu'à la majorité M » | au vote | « La formule du seuil » (déjà, suite de la même clé), **changé** (lot U3c) : « Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la part exigée est la majorité M. » (avant : « … la majorité M suffit », faux pour W ≤ 15 : quand les W votent, R = B^W + M·W, et B^W ajoute une voix quand M·W est entier, sur 17 couples (W, cran), par exemple W = 4, cran 2 : 3 « pour » sur 4, pas 2 ; relecture de `d66823b`) | `formula.reading` | grille `the_share_asked_falls_beyond_the_article_s_quorum_down_to_m` ; « la part exigée est la majorité M » : `ld-engine` `inertia::when_everyone_votes_the_share_asked_is_m_on_every_electorate` (tout W de 1 à 10 000 000, les cinq crans, G aux deux bornes de la participation habituelle, les quorums 1, 2 et W : la part exigée des T′ = max(W, Q) votants vaut M exactement ; la phrase d'avant fausse sur les 17 couples, tous W ≤ 15) ; `web/tests/unit/rules.test.ts` : la phrase en fr, en, es, aucune chaîne ne dit que la majorité suffit |
|
||||
| F1.1.4 | `T' = max(T, Q)` et `Seuil = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'` | au vote | « La formule du seuil », en MathML (déjà) | — | grille `the_formulas_of_the_texts_are_those_the_engine_computes` |
|
||||
| F1.1.5 | « Une version passe quand ses voix pour atteignent le seuil, arrondi à l'entier au-dessus. » | au vote | « La formule du seuil », **nouveau** : « Une version atteint son seuil quand ses voix pour atteignent R, arrondi à l'entier au-dessus. » | `formula.round` | `ld-engine` `threshold::a_whole_number_of_votes_never_gains_one_by_rounding` |
|
||||
| F1.1.6 | les lettres T, Q, T′, W, B, M, G | au vote | « La formule du seuil » : chaque lettre avec sa valeur lue dans l'API (déjà), **nouveau** T′ (« votants retenus : 12, T ou Q s'il en manque »), **changé** G (« déduit de la participation habituelle du groupe »), **nouveau** sous la liste, dans la toile seulement : « À la puissance W, B est si petit qu'il ne compte pas dans la toile de confiance. » | `formula.T`, `formula.Tprime`, `formula.Q`, `formula.W`, `formula.B`, `formula.B.wot`, `formula.M`, `formula.G` | `formula.Tprime` : `the_formulas_…` ; `formula.B.wot` : grille `b_to_the_power_w_never_counts_in_the_web_of_trust` (W de 100 à 10 000 000) ; G : grille `g_gives_…` |
|
||||
|
||||
@@ -447,7 +447,7 @@ export const en: Messages = {
|
||||
'formula.Q': 'Q — quorum: {n}',
|
||||
'formula.R': 'R — current threshold: {n} votes in favour',
|
||||
'formula.round': 'A version reaches its threshold when its votes in favour reach R, rounded up to a whole number.',
|
||||
'formula.reading': "Below the article's quorum Q, the missing voters count as against: it takes as many votes in favour as if Q people had voted. Beyond it, the share of votes in favour asked for, highest at the quorum, falls as turnout rises; when everyone votes, majority M is enough.",
|
||||
'formula.reading': "Below the article's quorum Q, the missing voters count as against: it takes as many votes in favour as if Q people had voted. Beyond it, the share of votes in favour asked for, highest at the quorum, falls as turnout rises; when everyone votes, the share asked is the majority M.",
|
||||
'common.pct': '{n}%',
|
||||
'settings.title': 'Formula settings',
|
||||
'settings.intro': 'To change a text, the group asks for a number of votes in favour that depends on its electorate, its usual turnout and the inertia of the article.',
|
||||
|
||||
@@ -447,7 +447,7 @@ export const es: Messages = {
|
||||
'formula.Q': 'Q — quórum: {n}',
|
||||
'formula.R': 'R — umbral actual: {n} votos a favor',
|
||||
'formula.round': 'Una versión alcanza su umbral cuando sus votos a favor alcanzan R, redondeado al entero superior.',
|
||||
'formula.reading': 'Por debajo del quórum Q del artículo, los votantes que faltan cuentan como «no»: hacen falta tantos votos a favor como si hubieran votado Q personas. Más allá, la proporción de votos a favor pedida, la más alta en el quórum, baja cuando sube la participación; cuando vota todo el mundo, basta la mayoría M.',
|
||||
'formula.reading': 'Por debajo del quórum Q del artículo, los votantes que faltan cuentan como «no»: hacen falta tantos votos a favor como si hubieran votado Q personas. Más allá, la proporción de votos a favor pedida, la más alta en el quórum, baja cuando sube la participación; cuando vota todo el mundo, la proporción exigida es la mayoría M.',
|
||||
'common.pct': '{n} %',
|
||||
'settings.title': 'Ajustes de la fórmula',
|
||||
'settings.intro': 'Para cambiar un texto, el grupo pide un número de votos a favor que depende de su electorado, de su participación habitual y de la inercia del artículo.',
|
||||
|
||||
@@ -451,7 +451,7 @@ export const fr = {
|
||||
'formula.Q': 'Q — quorum : {n}',
|
||||
'formula.R': 'R — seuil actuel : {n} voix pour',
|
||||
'formula.round': "Une version atteint son seuil quand ses voix pour atteignent R, arrondi à l'entier au-dessus.",
|
||||
'formula.reading': "Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la majorité M suffit.",
|
||||
'formula.reading': "Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la part exigée est la majorité M.",
|
||||
'common.pct': '{n} %',
|
||||
'settings.title': 'Réglages de la formule',
|
||||
'settings.intro': "Pour changer un texte, le groupe demande un nombre de voix pour qui dépend de son électorat, de sa participation habituelle et de l'inertie de l'article.",
|
||||
|
||||
@@ -38,8 +38,9 @@ describe('five levels of inertia', () => {
|
||||
expect(fr['inertia.explain']).toBe(
|
||||
"Plus l'inertie est haute, plus il faut de voix pour changer ce texte. Sous le quorum de l'article, les votants qui manquent comptent comme « non » ; au-delà, la part exigée baisse quand la participation monte.",
|
||||
);
|
||||
// Lot U3c: « la part exigée est la majorité M », no longer « la majorité M suffit » (rules.test.ts).
|
||||
expect(fr['formula.reading']).toBe(
|
||||
"Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la majorité M suffit.",
|
||||
"Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la part exigée est la majorité M.",
|
||||
);
|
||||
expect(fr['quorum.error.voted']).toBe("Quelqu'un a déjà voté sur cet article : son quorum ne change pas.");
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// (docs/chantiers/acte-certification/t3c-liste-fermee.md) on the texts of M8, kept in
|
||||
// crates/ld-server/tests/fixtures/seed-m8/. The inventory of where each lives:
|
||||
// docs/chantiers/acte-certification/U3-informations.md.
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { en } from '../../src/lib/i18n/en';
|
||||
import { es } from '../../src/lib/i18n/es';
|
||||
@@ -65,11 +67,13 @@ describe('the rules the screen says, true for every W and every quorum (lots T3
|
||||
// Each phrase, and the test that proves it (crates/ld-server/tests/…).
|
||||
const PROVEN: [keyof typeof fr, string, string][] = [
|
||||
// seed_quorum_texts.rs under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted (N1.1 2, F1.2 12);
|
||||
// the_share_asked_falls_beyond_the_article_s_quorum_down_to_m (N1.1 3).
|
||||
// the_share_asked_falls_beyond_the_article_s_quorum_down_to_m (N1.1 3); ld-engine
|
||||
// inertia::when_everyone_votes_the_share_asked_is_m_on_every_electorate (« la part exigée est la
|
||||
// majorité M », W from 1 to 10 000 000, lot U3c).
|
||||
[
|
||||
'formula.reading',
|
||||
"Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la majorité M suffit.",
|
||||
'under_the_quorum_… · the_share_asked_falls_…',
|
||||
"Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la part exigée est la majorité M.",
|
||||
'under_the_quorum_… · the_share_asked_falls_… · ld-engine inertia::when_everyone_votes_…',
|
||||
],
|
||||
// the_formulas_of_the_texts_are_those_the_engine_computes (T' = max(T, Q), F1.1, N1.1 4).
|
||||
['formula.Tprime', "T′ — votants retenus : {n}, T ou Q s'il en manque", 'the_formulas_…'],
|
||||
@@ -139,3 +143,84 @@ describe('the rules the screen says, true for every W and every quorum (lots T3
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Lot U3c (review of d66823b): « quand tout le monde vote, la majorité M suffit » was false on a small
|
||||
// electorate. When the W vote, R = B^W + M·W; when M·W is a whole number, B^W adds one « pour » on
|
||||
// every W up to 15 (W 4, level 2: 3 « pour » of 4, not 2; W 5, level 4: 4 of 5, not 3). What holds for
|
||||
// every W and every quorum is the share asked: M at T′ = W — ld-engine
|
||||
// inertia::when_everyone_votes_the_share_asked_is_m_on_every_electorate, W from 1 to 10 000 000.
|
||||
// B^W is said apart, in the web of trust only (`formula.B.wot`).
|
||||
describe('when everyone votes, the share asked is M — never « M suffices » (lot U3c)', () => {
|
||||
it('the reading of the formula says it, in the three languages', () => {
|
||||
expect(fr['formula.reading']).toBe(
|
||||
"Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la part exigée est la majorité M.",
|
||||
);
|
||||
expect(en['formula.reading']).toBe(
|
||||
"Below the article's quorum Q, the missing voters count as against: it takes as many votes in favour as if Q people had voted. Beyond it, the share of votes in favour asked for, highest at the quorum, falls as turnout rises; when everyone votes, the share asked is the majority M.",
|
||||
);
|
||||
expect(es['formula.reading']).toBe(
|
||||
'Por debajo del quórum Q del artículo, los votantes que faltan cuentan como «no»: hacen falta tantos votos a favor como si hubieran votado Q personas. Más allá, la proporción de votos a favor pedida, la más alta en el quórum, baja cuando sube la participación; cuando vota todo el mundo, la proporción exigida es la mayoría M.',
|
||||
);
|
||||
});
|
||||
|
||||
// « M suffit », « la majorité suffit », « majority M is enough », « basta la mayoría M »…: false for
|
||||
// W up to 15, so never said, in a catalogue or in the text of a component.
|
||||
const SUFFICES: Record<keyof typeof CATALOGS, RegExp> = {
|
||||
fr: /\bM suffi|majorit[ée][^.;:]*suffi|suffi[^.;:]*majorit[ée]/i,
|
||||
en: /majority[^.;:]*(enough|suffice|sufficient)|(enough|suffice|sufficient)[^.;:]*majority/i,
|
||||
es: /mayor[íi]a[^.;:]*(basta|suficiente|alcanza)|(basta|suficiente|alcanza con)[^.;:]*mayor[íi]a/i,
|
||||
};
|
||||
it('no string of the interface says the majority suffices', () => {
|
||||
for (const [lang, cat] of Object.entries(CATALOGS)) {
|
||||
for (const [k, v] of Object.entries(cat)) expect(v, `${lang} ${k}`).not.toMatch(SUFFICES[lang as keyof typeof CATALOGS]);
|
||||
}
|
||||
const svelte = (dir: string): string[] =>
|
||||
readdirSync(dir).flatMap((name) => {
|
||||
const p = join(dir, name);
|
||||
return statSync(p).isDirectory() ? svelte(p) : p.endsWith('.svelte') ? [p] : [];
|
||||
});
|
||||
for (const file of svelte(join(__dirname, '../../src'))) {
|
||||
const shown = readFileSync(file, 'utf8')
|
||||
.replace(/<script[\s\S]*?<\/script>/g, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/g, '')
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/<[^>]+>/g, ' ');
|
||||
for (const re of Object.values(SUFFICES)) expect(shown, file).not.toMatch(re);
|
||||
}
|
||||
});
|
||||
|
||||
// The closed list of what the screen says of full turnout, each judged against the engine: the
|
||||
// share asked, M — true for every W (the grid above). A new one fails here until it is judged.
|
||||
it('what the screen says of full turnout is the share asked, M', () => {
|
||||
const FULL: Record<keyof typeof CATALOGS, RegExp> = {
|
||||
fr: /tout le monde vote|pleine participation/,
|
||||
en: /everyone votes|full turnout/,
|
||||
es: /vota todo el mundo|participación plena/,
|
||||
};
|
||||
for (const [lang, cat] of Object.entries(CATALOGS)) {
|
||||
const told = Object.entries(cat)
|
||||
.filter(([, v]) => FULL[lang as keyof typeof CATALOGS].test(v))
|
||||
.map(([k]) => k);
|
||||
// formula.M names the letter, « M — majorité à pleine participation » (the share asked when the W
|
||||
// vote); formula.reading and settings.reading: « la part exigée … la majorité M ».
|
||||
expect(told, lang).toEqual(['formula.M', 'formula.reading', 'settings.reading']);
|
||||
}
|
||||
const SHARE = [
|
||||
[
|
||||
'formula.reading',
|
||||
{ fr: 'la part exigée est la majorité M', en: 'the share asked is the majority M', es: 'la proporción exigida es la mayoría M' },
|
||||
],
|
||||
[
|
||||
'settings.reading',
|
||||
{
|
||||
fr: "plus la part exigée baisse, jusqu'à la majorité M quand tout le monde vote",
|
||||
en: 'the lower the share asked gets, down to the majority M when everyone votes',
|
||||
es: 'más baja la proporción exigida, hasta la mayoría M cuando vota todo el mundo',
|
||||
},
|
||||
],
|
||||
] as const;
|
||||
for (const [k, share] of SHARE) {
|
||||
for (const [lang, cat] of Object.entries(CATALOGS)) expect(cat[k], `${lang} ${k}`).toContain(share[lang as keyof typeof CATALOGS]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user