From 03a7e19da0e5ca4b8670a16eb64b0726702b6c2d Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 17:22:37 +0100 Subject: [PATCH 1/5] =?UTF-8?q?Lot=20T3=20:=20les=20tests=20d'abord,=20rou?= =?UTF-8?q?ges=20sur=20les=20textes=20de=20T2=20(aucun=20chiffre=20dat?= =?UTF-8?q?=C3=A9=20:=20l'exemple=20de=20N1.1=20fait=20=C3=A9chouer=20le?= =?UTF-8?q?=20test=20des=20nombres=20;=20la=20phrase=20du=20quorum=20de=20?= =?UTF-8?q?N1.1=20vraie=20pour=20tout=20quorum,=20sur=20une=20grille=20de?= =?UTF-8?q?=20W,=20de=20quorums,=20de=20crans=20et=20de=20participations?= =?UTF-8?q?=20;=20=C2=AB=20comme=20d'habitude=20=C2=BB=20au-del=C3=A0=20du?= =?UTF-8?q?=20quorum=20seulement=20;=20B^W=20ne=20compte=20jamais=20dans?= =?UTF-8?q?=20la=20toile=20;=20l'amendement=20gard=C3=A9=20qui=20suit=20ce?= =?UTF-8?q?lui=20de=20T2)=20;=20textes=20de=20T2=20gard=C3=A9s=20dans=20te?= =?UTF-8?q?sts/fixtures/seed-quorum-t2.json=20;=20wot::PLAUSIBLE=20public?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- crates/ld-server/src/seed_amend.rs | 63 ++ crates/ld-server/src/wot.rs | 4 +- .../tests/fixtures/seed-quorum-t2.json | 44 + crates/ld-server/tests/seed_quorum_texts.rs | 797 +++++++++++++++--- 4 files changed, 806 insertions(+), 102 deletions(-) create mode 100644 crates/ld-server/tests/fixtures/seed-quorum-t2.json diff --git a/crates/ld-server/src/seed_amend.rs b/crates/ld-server/src/seed_amend.rs index 238601e..fa91ab9 100644 --- a/crates/ld-server/src/seed_amend.rs +++ b/crates/ld-server/src/seed_amend.rs @@ -782,6 +782,61 @@ mod tests { assert_eq!(verdict(Some(&written_by(&["m5", "t"])), &second, &["m5"]), Verdict::Refuse("revised")); } + #[test] + fn a_fourth_amendment_admits_the_texts_the_three_before_wrote_and_nothing_else() { + // Lot T3 (2026-11-03): F1.2 and N1.1 were amended by M5, M6b and M6c (revision 4 in + // production); the texts without dated figures replace the text of M6c, after it only. + let fourth = Amendment { version: Version::Open("1.0.0"), follows: Some("t2"), ..A }; + let before: &[&str] = &["m5", "t", "t2"]; + // Production: M5, M6b, M6c, each its revision. + assert_eq!(verdict(Some(&written_by(&["m5", "t", "t2"])), &fourth, before), Verdict::Apply); + // A base born with the text of M5 (02/11): M6b and M6c wrote its revisions 2 and 3. + assert_eq!(verdict(Some(&written_by(&["t", "t2"])), &fourth, before), Verdict::Apply); + // A base born with the text of M6b, then of M6c: its revision 2, its revision 1. + assert_eq!(verdict(Some(&written_by(&["t2"])), &fourth, before), Verdict::Apply); + assert_eq!(verdict(Some(&intact()), &fourth, before), Verdict::Apply); + // The last revision is not M6c's, or not by them alone, or out of order: refused. + for applied in [ + &["m5", "t"][..], + &["t"], + &["m5"], + &["t2", "t"], + &["m5", "t2", "t"], + &["other", "t", "t2"], + &["m5", "t", "t2", "t2"], + ] { + assert_eq!(verdict(Some(&written_by(applied)), &fourth, before), Verdict::Refuse("revised"), "{applied:?}"); + } + // A typo after M6c: a revision no amendment wrote. + let typo = Facts { + revision: 5, + revisions: vec![ + "initial".into(), + "amendment".into(), + "amendment".into(), + "amendment".into(), + "typo".into(), + ], + ..written_by(&["m5", "t", "t2"]) + }; + assert_eq!(verdict(Some(&typo), &fourth, before), Verdict::Refuse("revised")); + // Any trace of a person keeps the text, with its reason. + for (f, why) in [ + (Facts { votes: 1, ..written_by(&["m5", "t", "t2"]) }, "votes"), + (Facts { ballots: 1, ..written_by(&["m5", "t", "t2"]) }, "ballots"), + (Facts { typos: 1, ..written_by(&["m5", "t", "t2"]) }, "corrected"), + (Facts { member_translations: 1, ..written_by(&["m5", "t", "t2"]) }, "translation_edited"), + (Facts { signals: 1, ..written_by(&["m5", "t", "t2"]) }, "signalled"), + (Facts { fingerprint: "other".into(), ..written_by(&["m5", "t", "t2"]) }, "text_differs"), + (Facts { confirmed: true, ..written_by(&["m5", "t", "t2"]) }, "confirmed"), + ] { + assert_eq!(verdict(Some(&f), &fourth, before), Verdict::Refuse(why), "{f:?}"); + } + // Lot T2's own amendment never admits a revision 4. + let third = Amendment { version: Version::Open("1.0.0"), follows: Some("t"), ..A }; + assert_eq!(verdict(Some(&written_by(&["m5", "t", "t2"])), &third, &["m5", "t"]), Verdict::Refuse("revised")); + } + #[test] fn the_amendments_followed_are_read_through_the_list_oldest_first() { let by_id = |id: &str| AMENDMENTS.iter().find(|a| a.id == id).expect("amendment"); @@ -794,6 +849,14 @@ mod tests { ["2026-11-02-n1-1-regles", "2026-11-03-n1-1-quorum"] ); assert_eq!(earlier(AMENDMENTS, by_id("2026-11-03-f1-1-phrases")), ["2026-11-03-f1-1-quorum"]); + assert_eq!( + earlier(AMENDMENTS, by_id("2026-11-03-f1-2-chiffres")), + ["2026-11-02-f1-2-regles", "2026-11-03-f1-2-quorum", "2026-11-03-f1-2-phrases"] + ); + assert_eq!( + earlier(AMENDMENTS, by_id("2026-11-03-n1-1-chiffres")), + ["2026-11-02-n1-1-regles", "2026-11-03-n1-1-quorum", "2026-11-03-n1-1-phrases"] + ); assert!(earlier(AMENDMENTS, by_id("2026-10-30-i1-raison-d-etre")).is_empty()); // Each one followed wrote the text the next replaces, and is superseded by it. for a in AMENDMENTS.iter().filter(|a| a.follows.is_some()) { diff --git a/crates/ld-server/src/wot.rs b/crates/ld-server/src/wot.rs index 3596034..bf5a51e 100644 --- a/crates/ld-server/src/wot.rs +++ b/crates/ld-server/src/wot.rs @@ -12,7 +12,9 @@ use std::time::Duration; /// W is read again this often. pub const REFRESH: Duration = Duration::from_secs(6 * 3600); -const PLAUSIBLE: std::ops::RangeInclusive = 100..=10_000_000; +/// The sizes of the web of trust taken as true; anything else read is ignored. The texts of +/// the seed are written true for every W in it (lot T3). +pub const PLAUSIBLE: std::ops::RangeInclusive = 100..=10_000_000; fn twox_128(data: &[u8]) -> [u8; 16] { let mut out = [0u8; 16]; diff --git a/crates/ld-server/tests/fixtures/seed-quorum-t2.json b/crates/ld-server/tests/fixtures/seed-quorum-t2.json new file mode 100644 index 0000000..447fc06 --- /dev/null +++ b/crates/ld-server/tests/fixtures/seed-quorum-t2.json @@ -0,0 +1,44 @@ +{ + "_source": "seeds/acte-engagement-certification.json au commit 8c1e7af : F1.1, F1.2 et N1.1 tels que la production les tient depuis M6c (84f015a, le 03/11 à 11 h 05 ; lot T2, les trois phrases), inchangés par M7b (6f6197c)", + "units": { + "F1.1": { + "label": "1.0.0", + "title": { + "fr": "Formule du seuil WoT", + "en": "WoT threshold formula", + "es": "Fórmula del umbral WoT" + }, + "body": { + "fr": "Le seuil est le nombre de voix pour qu'il faut à une version pour entrer en vigueur. Sous le quorum Q, les votants qui manquent comptent comme « non » : le seuil se calcule comme si Q personnes avaient voté. Au-delà, il faut la part de « pour » que demande le cran d'inertie de l'article, parmi tous les votants : cette part est la plus haute au quorum et baisse quand la participation monte, jusqu'à la majorité M quand tout le monde vote.\n\n`T' = max(T, Q)`\n`Seuil = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUne version passe quand ses voix pour atteignent le seuil, arrondi à l'entier au-dessus.\n\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q** : le quorum de l'article ; **T'** : les votants retenus, T, ou Q s'il en manque.\n- **W** : le nombre de membres de la toile de confiance Ğ1.\n- **B** : la base ; à la puissance W, elle est si petite qu'elle ne compte pas.\n- **M** : la majorité demandée quand tout le monde vote.\n- **G** : le gradient : plus il est grand, plus la part demandée monte quand peu de gens votent.\n\nLeurs valeurs sont dans « Paramètres par défaut » et « Niveaux d'inertie ».", + "en": "The threshold is the number of votes in favour a version needs to come into force. Below the quorum Q, the voters who are missing count as votes against: the threshold is worked out as if Q people had voted. Beyond it, it takes the share of votes in favour that the article's level of inertia asks for, among all the voters: that share is highest at the quorum and falls as turnout rises, down to the majority M when everyone votes.\n\n`T' = max(T, Q)`\n`Threshold = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nA version passes when its votes in favour reach the threshold, rounded up to a whole number.\n\n- **T**: the votes cast on the version, for and against.\n- **Q**: the article's quorum; **T'**: the voters counted, T, or Q if some are missing.\n- **W**: the number of members of the Ğ1 web of trust.\n- **B**: the base; raised to the power W, it is so small that it does not count.\n- **M**: the majority asked for when everyone votes.\n- **G**: the gradient: the larger it is, the higher the share asked for when few people vote.\n\nTheir values are in “Default parameters” and “Inertia levels”.", + "es": "El umbral es el número de votos a favor que necesita una versión para entrar en vigor. Por debajo del quórum Q, los votantes que faltan cuentan como «no»: el umbral se calcula como si hubieran votado Q personas. Más allá, hace falta la proporción de votos a favor que pide el nivel de inercia del artículo, entre todos los votantes: esa proporción es la más alta en el quórum y baja cuando sube la participación, hasta la mayoría M cuando vota todo el mundo.\n\n`T' = max(T, Q)`\n`Umbral = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUna versión pasa cuando sus votos a favor alcanzan el umbral, redondeado al entero superior.\n\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**: el quórum del artículo; **T'**: los votantes contados, T, o Q si faltan.\n- **W**: el número de miembros de la red de confianza Ğ1.\n- **B**: la base; elevada a la potencia W, es tan pequeña que no cuenta.\n- **M**: la mayoría pedida cuando vota todo el mundo.\n- **G**: el gradiente: cuanto mayor es, más alta es la proporción pedida cuando vota poca gente.\n\nSus valores están en «Parámetros por defecto» y «Niveles de inercia»." + } + }, + "F1.2": { + "label": "1.0.0", + "title": { + "fr": "Paramètres par défaut", + "en": "Default parameters", + "es": "Parámetros por defecto" + }, + "body": { + "fr": "Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la façon dont le vote se déroule.\n\n**Un vote permanent, sans durée**\n\n- Chaque version se vote à tout moment, sans échéance. Chacun peut changer ou retirer son vote : le dernier fait foi.\n- Une proposition entre en vigueur dès qu'elle atteint son seuil. S'il y a déjà un texte en vigueur, il lui faut en plus une marge (voix pour moins voix exigées) plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir.\n- Si plusieurs propositions y parviennent, celle qui a la marge la plus large entre en vigueur.\n\n**Les lettres**\n\n- **W** : le nombre de membres de la toile de confiance Ğ1, relu sur la chaîne toutes les 6 heures.\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q**, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au cran 5. L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote. Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un texte.\n- **B = 0,1** : dans la toile de confiance, `B^W` est si petit qu'il ne compte pas.\n- **M** et **G** : donnés par le cran d'inertie de l'article (voir « Niveaux d'inertie »). G n'est pas fixé à la main : il se déduit de la participation habituelle du groupe.\n\n**Par défaut**\n\n- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude. Tout nouvel article commence à ce cran.\n- Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, tous les autres articles au cran 3.", + "en": "The values of the letters of the formula (see “WoT threshold formula”) and how the vote runs.\n\n**A permanent vote, with no duration**\n\n- Every version can be voted on at any time, with no deadline. Anyone can change or withdraw their vote: the last one is the one that counts.\n- A proposal comes into force as soon as it reaches its threshold. If a text is already in force, it also needs a wider margin (votes in favour minus votes required) than that text has. The replaced text becomes a proposal again, with its votes: it can come back.\n- If several proposals get there, the one with the widest margin comes into force.\n\n**The letters**\n\n- **W**: the number of members of the Ğ1 web of trust, read again from the chain every 6 hours.\n- **T**: the votes cast on the version, for and against.\n- **Q**, the quorum: below this number of voters, those who are missing count as votes against. Each article takes the one of its level: 12, 12, 50, 50 and 50 in Monnaie libre, from level 1 to level 5. The author of the first version of an article, an administrator for the starting texts, can set another one as long as the article keeps no trace of any vote. In Monnaie libre, never less than 2: a single vote never passes a text.\n- **B = 0.1**: in the web of trust, `B^W` is so small that it does not count.\n- **M** and **G**: given by the article's level of inertia (see “Inertia levels”). G is not set by hand: it follows from the group's usual turnout.\n\n**By default**\n\n- The default level is 3 (medium): M = 55%, a quorum of 50 in Monnaie libre, and the group asks for 75% of votes in favour when it votes as usual. Every new article starts at this level.\n- At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, all other articles at level 3.", + "es": "Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y cómo se desarrolla la votación.\n\n**Una votación permanente, sin duración**\n\n- Cada versión se vota en cualquier momento, sin plazo. Cada cual puede cambiar o retirar su voto: el último es el que cuenta.\n- Una propuesta entra en vigor en cuanto alcanza su umbral. Si ya hay un texto en vigor, necesita además un margen (votos a favor menos votos exigidos) más amplio que el de ese texto. El texto sustituido vuelve a ser una propuesta, con sus votos: puede volver.\n- Si varias propuestas lo consiguen, entra en vigor la que tiene el margen más amplio.\n\n**Las letras**\n\n- **W**: el número de miembros de la red de confianza Ğ1, leído de nuevo en la cadena cada 6 horas.\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**, el quórum: por debajo de este número de votantes, los que faltan cuentan como «no». Cada artículo toma el de su nivel: 12, 12, 50, 50 y 50 en Monnaie libre, del nivel 1 al nivel 5. El autor de la primera versión de un artículo, un administrador para los textos de partida, puede fijar otro mientras el artículo no guarde rastro de ningún voto. En Monnaie libre, nunca menos de 2: un solo voto nunca hace pasar un texto.\n- **B = 0,1**: en la red de confianza, `B^W` es tan pequeño que no cuenta.\n- **M** y **G**: los da el nivel de inercia del artículo (ver «Niveles de inercia»). G no se fija a mano: se deduce de la participación habitual del grupo.\n\n**Por defecto**\n\n- El nivel por defecto es el 3 (media): M = 55 %, un quórum de 50 en Monnaie libre, y el grupo pide un 75 % de votos a favor cuando vota como de costumbre. Todo artículo nuevo empieza en este nivel.\n- Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3." + } + }, + "N1.1": { + "label": "1.0.0", + "title": { + "fr": "Niveaux d'inertie", + "en": "Inertia levels", + "es": "Niveles de inercia" + }, + "body": { + "fr": "À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). 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 ». 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, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.\n\n`Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum s'il en manque.\n\n**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, et ce qui est à ce cran au départ.\n\n- **Cran 1 · très basse** : quorum 12 ; 60 % comme d'habitude, 50 % quand tout le monde vote. Aucun article au départ.\n- **Cran 2 · basse** : quorum 12 ; 67 % comme d'habitude, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.\n- **Cran 3 · moyenne** : quorum 50 ; 75 % comme d'habitude, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.\n- **Cran 4 · haute** : quorum 50 ; 83 % comme d'habitude, 60 % quand tout le monde vote. Le titre du document.\n- **Cran 5 · très haute** : quorum 50 ; 90 % comme d'habitude, 66 % quand tout le monde vote. Aucun article au départ.\n\n**La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. 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 ne dépend pas du quorum d'un article. Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour.\n\n**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.\n\nExemple : avec 7 224 membres et une participation habituelle de 120 votants, quand 120 personnes votent, il faut 72, 81, 90, 100 ou 108 voix pour, du cran 1 au cran 5 ; au cran 3, 75 % des 120 votants, soit 90. Sous le quorum, les absents comptent comme « non » : au cran 2 (quorum 12), 9 voix pour passent avec 3 « contre » au plus ; au cran 3 (quorum 50), il faut 39 voix pour, avec 11 « contre » au plus.", + "en": "At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). 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. 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 lower it gets, down to the majority M when everyone votes.\n\n`Share asked = M + (1 − M) × (1 − (T'/W)^G)`, where T' is the number of voters, or the quorum if some are missing.\n\n**The five levels**: the quorum in Monnaie libre, the share asked when the group votes as usual, then when everyone votes, and what is at that level at the start.\n\n- **Level 1 · very low**: quorum 12; 60% as usual, 50% when everyone votes. No article at the start.\n- **Level 2 · low**: quorum 12; 67% as usual, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.\n- **Level 3 · medium**: quorum 50; 75% as usual, 55% when everyone votes. The default level: all other articles, and every new article.\n- **Level 4 · high**: quorum 50; 83% as usual, 60% when everyone votes. The title of the document.\n- **Level 5 · very high**: quorum 50; 90% as usual, 66% when everyone votes. No article at the start.\n\n**The usual turnout** is the median number of voters on the group's proposals voted on over the past year. Until the group has voted on 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It stays between 1% and 50% of the electorate, and is measured once a day.\n\n**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.\n\nExample: with 7,224 members and a usual turnout of 120 voters, when 120 people vote, it takes 72, 81, 90, 100 or 108 votes in favour, from level 1 to level 5; at level 3, 75% of the 120 voters, i.e. 90. Below the quorum, those missing count as votes against: at level 2 (quorum 12), 9 votes in favour pass with 3 votes against at most; at level 3 (quorum 50), it takes 39 votes in favour, with 11 votes against at most.", + "es": "En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). 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». 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, más baja, hasta la mayoría M cuando vota todo el mundo.\n\n`Proporción exigida = M + (1 − M) × (1 − (T'/W)^G)`, donde T' es el número de votantes, o el quórum si faltan.\n\n**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, y lo que está en ese nivel al inicio.\n\n- **Nivel 1 · muy baja**: quórum 12; 60 % como de costumbre, 50 % cuando vota todo el mundo. Ningún artículo al inicio.\n- **Nivel 2 · baja**: quórum 12; 67 % como de costumbre, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.\n- **Nivel 3 · media**: quórum 50; 75 % como de costumbre, 55 % cuando vota todo el mundo. El nivel por defecto: todos los demás artículos, y todo artículo nuevo.\n- **Nivel 4 · alta**: quórum 50; 83 % como de costumbre, 60 % cuando vota todo el mundo. El título del documento.\n- **Nivel 5 · muy alta**: quórum 50; 90 % como de costumbre, 66 % cuando vota todo el mundo. Ningún artículo al inicio.\n\n**La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Hasta que el grupo haya votado 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se mantiene entre el 1 % y el 50 % del electorado, y se mide una vez al día.\n\n**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.\n\nEjemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 72, 81, 90, 100 o 108 votos a favor, del nivel 1 al nivel 5; en el nivel 3, el 75 % de los 120 votantes, es decir 90. Por debajo del quórum, los que faltan cuentan como «no»: en el nivel 2 (quórum 12), 9 votos a favor pasan con 3 en contra como mucho; en el nivel 3 (quórum 50), hacen falta 39 votos a favor, con 11 en contra como mucho." + } + } + } +} diff --git a/crates/ld-server/tests/seed_quorum_texts.rs b/crates/ld-server/tests/seed_quorum_texts.rs index ca9751a..89800cd 100644 --- a/crates/ld-server/tests/seed_quorum_texts.rs +++ b/crates/ld-server/tests/seed_quorum_texts.rs @@ -13,10 +13,19 @@ //! ne change plus », N1.1 « le groupe demande un quorum de votants ». A second guarded //! amendment, which follows lot T's, writes them true on a base made as production is //! after M6b; the texts of M6b are kept in `tests/fixtures/seed-quorum-m6b.json`. +//! +//! Lot T3 (2026-11-03, Yvv: « pas de chiffre datés dans un texte voté »): no figure of these +//! texts depends on the web of trust or on a turnout — the example of N1.1 goes; every sentence +//! is true for every W and every quorum in the bounds — N1.1 « qui n'est pas un nombre minimal +//! de votants » was false for a low quorum, and « comme d'habitude » holds beyond the quorum +//! only (N1.1, F1.2). A third guarded amendment, which follows lot T2's, writes them on a base +//! made as production is after M6c; the texts of T2 are kept in +//! `tests/fixtures/seed-quorum-t2.json`. F1.1 needs none: it is not touched. use ld_engine::inertia::{InertiaLevel, Participation, Scale, gradient, quorum_bounds}; -use ld_engine::threshold::{FormulaParams, Tally, judged_voters, no_needed, passes, required_ratio, yes_needed}; +use ld_engine::threshold::{FormulaParams, Tally, judged_voters, passes, required_ratio, wot_threshold, yes_needed}; use ld_server::seed_amend::{self, AMENDMENTS, Version}; +use ld_server::wot::PLAUSIBLE; use ld_server::{AppState, Config, open_db, seed}; use serde_json::Value; use std::collections::HashMap; @@ -25,10 +34,9 @@ use std::path::{Path, PathBuf}; const CODES: [&str; 3] = ["F1.1", "F1.2", "N1.1"]; const LANGS: [&str; 3] = ["fr", "en", "es"]; const GROUPS: [&str; 2] = ["essai", "monnaie-libre"]; -/// The illustration of N1.1: the web of trust and the usual turnout of the smith vote -/// of 2024 (ADR 0007, 0013), not the size of the web today. -const EXAMPLE_W: u32 = 7224; -const EXAMPLE_MEDIAN: u32 = 120; +/// Sizes of the web of trust, W in Monnaie libre: the bounds the server takes as true +/// (`wot::PLAUSIBLE`), the web of 2024 (7 224) and of 02/11 (5 707), and some between. +const ELECTORATES: [u32; 9] = [100, 101, 1000, 3000, 5707, 7224, 10_000, 1_000_000, 10_000_000]; // --------------------------------------------------------------------------- // The seeds @@ -66,6 +74,16 @@ fn m6b_seed() -> Value { with_texts(&today_seed(), &m6b_texts(), &CODES) } +/// F1.1, F1.2 and N1.1 as lot T2 wrote them: production's since M6c (84f015a), unchanged by M7b. +fn t2_texts() -> Value { + read(fixtures().join("seed-quorum-t2.json")) +} + +/// The seed production runs since M6c: today's, with the texts of lot T2. +fn t2_seed() -> Value { + with_texts(&today_seed(), &t2_texts(), &CODES) +} + fn unit<'a>(seed: &'a Value, code: &str) -> &'a Value { seed["units"].as_array().expect("units").iter().find(|u| u["code"] == code).expect("unit") } @@ -102,10 +120,16 @@ fn seed_dir(tag: &str, seed: &Value) -> PathBuf { // The figures: from the engine and the seed, never by hand // --------------------------------------------------------------------------- +/// Every figure the texts may hold: parameters of the seed and constants of the engine and +/// the server. Since lot T3 (Yvv, 03/11: « pas de chiffre datés dans un texte voté »), none +/// depends on the web of trust W or on a turnout: no participation goes in, and the same +/// figures come out on every W of Monnaie libre (`figures`). +#[derive(Debug, PartialEq)] struct Figures { /// Q_k of Monnaie libre (the seed). quorums: [u32; 5], - /// Twice the reference quorum of the document, in voters (the engine's p0 · W). + /// Twice the reference quorum of the document, in voters (the engine's p0 · W, before + /// the bounds). reference: u32, /// The lowest quorum an author may set in Monnaie libre. lowest_quorum: u32, @@ -120,36 +144,20 @@ struct Figures { min_sample: u32, /// The bounds of the usual turnout, in percent. bounds: (f64, f64), - /// « pour » asked at the example's 120 voters, level 1 to 5. - example: [u32; 5], - /// Under the quorum: (level, quorum, « pour » that pass, « contre » at most). - under: [(u8, u32, u32, u32); 2], } fn level(k: u8) -> InertiaLevel { InertiaLevel::new(k).expect("1 to 5") } -fn figures(seed: &Value) -> Figures { +/// The figures as the engine gives them on an electorate of `w`. +fn figures_at(seed: &Value, w: u32) -> Figures { let ml = seed["instances"].as_array().expect("instances").iter().find(|i| i["group"] == "monnaie-libre").expect("ml"); let quorums: [u32; 5] = serde_json::from_value(ml["quorums"].clone()).expect("quorums"); let vote_floor = ml["voteFloor"].as_f64().expect("voteFloor"); - let p0 = Participation::of(EXAMPLE_W, vote_floor, None, 0); - assert!(!p0.bounded, "the reference turnout is not brought back within the bounds"); - let reference = p0.voters(EXAMPLE_W); - assert!((reference - reference.round()).abs() < 1e-9, "a whole number of voters: {reference}"); - let scale = - Scale { electorate: EXAMPLE_W, participation: f64::from(EXAMPLE_MEDIAN) / f64::from(EXAMPLE_W), quorums }; - let example = InertiaLevel::ALL.map(|k| scale.required(EXAMPLE_MEDIAN, k, scale.quorum(k, None)).expect("valid")); - let under = [2u8, 3].map(|k| { - let q = scale.quorum(level(k), None); - let p = scale.params(level(k), q); - let yes = yes_needed(0, EXAMPLE_W, &p).expect("valid"); - let most = no_needed(yes, EXAMPLE_W, &p).expect("valid") - 1; - assert!(yes + most <= q, "level {k}: the example stays under the quorum"); - (k, q, yes, most) - }); + let reference = Participation::reference_ratio(w, vote_floor) * f64::from(w); + assert!((reference - reference.round()).abs() < 1e-6, "a whole number of voters: {reference}"); let levels: Vec = seed["units"] .as_array() .expect("units") @@ -164,7 +172,7 @@ fn figures(seed: &Value) -> Figures { Figures { quorums, reference: reference.round() as u32, - lowest_quorum: quorum_bounds(EXAMPLE_W, false).0, + lowest_quorum: quorum_bounds(w, false).0, base: FormulaParams::DEFAULT_BASE, refresh_hours: ld_server::wot::REFRESH.as_secs() / 3600, default_level: InertiaLevel::DEFAULT.get(), @@ -174,20 +182,17 @@ fn figures(seed: &Value) -> Figures { others_level: levels[0], min_sample: Participation::MIN_SAMPLE, bounds: (Participation::LOWEST * 100.0, Participation::HIGHEST * 100.0), - example, - under, } } -/// A whole number as the language writes it: 7 224, 7,224, 7224. -fn int(lang: &str, n: u32) -> String { - let digits = n.to_string(); - if n < 1000 || lang == "es" { - return digits; +/// The figures of the texts: the same on every W Monnaie libre may have — none is dated. +fn figures(seed: &Value) -> Figures { + let mut all = ELECTORATES.iter().map(|w| (*w, figures_at(seed, *w))); + let (_, first) = all.next().expect("an electorate"); + for (w, f) in all { + assert_eq!(f, first, "W {w}: a figure of the texts depends on the web of trust"); } - let sep = if lang == "fr" { " " } else { "," }; - let (head, tail) = digits.split_at(digits.len() - 3); - format!("{head}{sep}{tail}") + first } /// A decimal: 0,1 or 0.1. @@ -271,10 +276,6 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec { let qd = q[usize::from(f.default_level - 1)]; let name = |k: u8| level_name(lang, k); let sp = and_list(lang, &seed_proposal_titles(seed, lang)); - let (m, w) = (EXAMPLE_MEDIAN, int(lang, EXAMPLE_W)); - let e = f.example; - let ed = e[usize::from(f.default_level - 1)]; - let [(k2, q2, y2, n2), (k3, q3, y3, n3)] = f.under; match (code, lang) { ("F1.1", _) => vec![], ("F1.2", "fr") => vec![ @@ -289,7 +290,7 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec { ), format!("**B = {}**", dec(lang, f.base)), format!( - "Le cran par défaut est le {} ({}) : M = {}, un quorum de {qd} dans Monnaie libre, et 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 » quand il vote comme d'habitude", f.default_level, name(f.default_level), pct(lang, d.majority_pct()), @@ -317,7 +318,7 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec { 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 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 when it votes as usual", f.default_level, name(f.default_level), pct(lang, d.majority_pct()), @@ -345,7 +346,7 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec { 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 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 cuando vota como de costumbre", f.default_level, name(f.default_level), pct(lang, d.majority_pct()), @@ -394,9 +395,6 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec { } }) .collect(); - let sd = pct(lang, d.share_pct()); - // « 75 % des 120 votants, soit 90 »: the share of the default level, exactly. - assert_eq!(f64::from(ed), (d.share_pct() * f64::from(m) / 100.0).ceil()); let (b0, b1) = (pct(lang, f.bounds.0), pct(lang, f.bounds.1)); let (ms, r) = (f.min_sample, f.reference); out.extend(match lang { @@ -404,31 +402,16 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec { "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!( - "avec {w} membres et une participation habituelle de {m} votants, quand {m} personnes votent, il faut {}, {}, {}, {} ou {} voix pour, du cran {lo} au cran {hi} ; au cran {}, {sd} des {m} votants, soit {ed}", - e[0], e[1], e[2], e[3], e[4], f.default_level - ), - format!("au cran {k2} (quorum {q2}), {y2} voix pour passent avec {n2} « contre » au plus ; au cran {k3} (quorum {q3}), il faut {y3} voix pour, avec {n3} « contre » au plus"), ], "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!( - "with {w} members and a usual turnout of {m} voters, when {m} people vote, it takes {}, {}, {}, {} or {} votes in favour, from level {lo} to level {hi}; at level {}, {sd} of the {m} voters, i.e. {ed}", - e[0], e[1], e[2], e[3], e[4], f.default_level - ), - format!("at level {k2} (quorum {q2}), {y2} votes in favour pass with {n2} votes against at most; at level {k3} (quorum {q3}), it takes {y3} votes in favour, with {n3} votes against at most"), ], _ => [ "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!( - "con {w} miembros y una participación habitual de {m} votantes, cuando votan {m} personas, hacen falta {}, {}, {}, {} o {} votos a favor, del nivel {lo} al nivel {hi}; en el nivel {}, el {sd} de los {m} votantes, es decir {ed}", - e[0], e[1], e[2], e[3], e[4], f.default_level - ), - format!("en el nivel {k2} (quórum {q2}), {y2} votos a favor pasan con {n2} en contra como mucho; en el nivel {k3} (quórum {q3}), hacen falta {y3} votos a favor, con {n3} en contra como mucho"), ], }); out @@ -462,16 +445,22 @@ fn formulas(code: &str, lang: &str) -> Vec { } } +/// Every number of the texts is a parameter of the seed or a constant of the engine (lot T3: +/// none depends on W or on a turnout, `figures`); a digit nothing stands for fails, every +/// article and language listed at once. #[test] fn every_number_of_the_quorum_texts_is_the_engine_s() { let seed = today_seed(); let f = figures(&seed); + let mut wrong = Vec::new(); for code in CODES { for lang in LANGS { let (_, body) = text(&seed, code, lang); let mut rest = body.to_owned(); for claim in claims(code, lang, &f, &seed) { - assert!(rest.contains(&claim), "{code} {lang}: missing « {claim} »\n{body}"); + if !rest.contains(&claim) { + wrong.push(format!("{code} {lang}: missing « {claim} »")); + } rest = rest.replacen(&claim, "", 1); } let spans = code_spans(body); @@ -480,17 +469,20 @@ fn every_number_of_the_quorum_texts_is_the_engine_s() { rest = rest.replacen(&format!("`{span}`"), "", 1); } let rest = rest.replace("Ğ1", ""); - let stray: Vec = rest.chars().filter(char::is_ascii_digit).collect(); - assert!(stray.is_empty(), "{code} {lang}: a number no engine figure stands for: {stray:?}\n{rest}"); + let stray: String = rest.chars().filter(char::is_ascii_digit).collect(); + if !stray.is_empty() { + wrong.push(format!("{code} {lang}: numbers no parameter stands for: {stray}\n{rest}")); + } } } + assert!(wrong.is_empty(), "{}", wrong.join("\n\n")); } // --------------------------------------------------------------------------- // Lot T2: the three phrases the review of M6b left inexact // --------------------------------------------------------------------------- -/// (article, language, the words of M6b, the words of today): the only change of lot T2. +/// (article, language, the words of M6b, the words of T2): the only change of lot T2. const PHRASES: [(&str, &str, &str, &str); 9] = [ ( "F1.1", @@ -550,10 +542,10 @@ const PHRASES: [(&str, &str, &str, &str); 9] = [ #[test] fn the_three_phrases_of_the_review_of_m6b_are_written_true() { - let (m6b, today) = (m6b_seed(), today_seed()); + let (m6b, t2) = (m6b_seed(), t2_seed()); for (code, lang, old, new) in PHRASES { assert!(text(&m6b, code, lang).1.contains(old), "{code} {lang}: the words of M6b"); - let (_, body) = text(&today, code, lang); + let (_, body) = text(&t2, code, lang); assert!(!body.contains(old), "{code} {lang}: still the words of M6b\n{body}"); assert!(body.contains(new), "{code} {lang}: missing « {new} »\n{body}"); } @@ -582,11 +574,13 @@ fn the_three_phrases_of_the_review_of_m6b_are_written_true() { } } } - // N1.1: the quorum is no minimum of voters. In Monnaie libre (W 5 707 on 02/11, the - // reference turnout), at every level, fewer voters than its quorum pass when they all - // vote « pour »: 8 of 12, 9 of 12, 39 of 50, 43 of 50, 46 of 50. + // N1.1 of T2: the quorum is no minimum of voters. True at the quorums of the levels in + // Monnaie libre (W 5 707 on 02/11, the reference turnout): fewer voters than the quorum pass + // when they all vote « pour », 8 of 12, 9 of 12, 39 of 50, 43 of 50, 46 of 50. False at a low + // quorum an author may set, where Q « pour » are asked: lot T3 writes it true for every quorum + // (`under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted`). let w = 5707; - let f = figures(&today); + let f = figures(&t2); let scale = Scale { electorate: w, participation: Participation::of(w, 50.0, None, 0).ratio, quorums: f.quorums }; let fewer: Vec<(u32, u32)> = InertiaLevel::ALL .iter() @@ -602,6 +596,301 @@ fn the_three_phrases_of_the_review_of_m6b_are_written_true() { assert!(fewer.iter().all(|(yes, q)| yes < q)); } +// --------------------------------------------------------------------------- +// 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. 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); 12] = [ + ( + "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.", + ), + ( + "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.", + ), + ( + "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.", + ), + ( + "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 voix pour que si le 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.", + ), + ( + "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.", + ), + ( + "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,", + ), + ( + "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,", + ), + ( + "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,", + ), + ( + "N1.1", + "fr", + "\n\nExemple : avec 7 224 membres et une participation habituelle de 120 votants, quand 120 personnes votent, il faut 72, 81, 90, 100 ou 108 voix pour, du cran 1 au cran 5 ; au cran 3, 75 % des 120 votants, soit 90. Sous le quorum, les absents comptent comme « non » : au cran 2 (quorum 12), 9 voix pour passent avec 3 « contre » au plus ; au cran 3 (quorum 50), il faut 39 voix pour, avec 11 « contre » au plus.", + "", + ), + ( + "N1.1", + "en", + "\n\nExample: with 7,224 members and a usual turnout of 120 voters, when 120 people vote, it takes 72, 81, 90, 100 or 108 votes in favour, from level 1 to level 5; at level 3, 75% of the 120 voters, i.e. 90. Below the quorum, those missing count as votes against: at level 2 (quorum 12), 9 votes in favour pass with 3 votes against at most; at level 3 (quorum 50), it takes 39 votes in favour, with 11 votes against at most.", + "", + ), + ( + "N1.1", + "es", + "\n\nEjemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 72, 81, 90, 100 o 108 votos a favor, del nivel 1 al nivel 5; en el nivel 3, el 75 % de los 120 votantes, es decir 90. Por debajo del quórum, los que faltan cuentan como «no»: en el nivel 2 (quórum 12), 9 votos a favor pasan con 3 en contra como mucho; en el nivel 3 (quórum 50), hacen falta 39 votos a favor, con 11 en contra como mucho.", + "", + ), +]; + +/// 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(); + for (c, l, old, new) in PHRASES_T3 { + if (c, l) == (code, lang) { + assert_eq!(b.matches(old).count(), 1, "{code} {lang}: « {old} » once in the text of T2"); + b = b.replacen(old, new, 1); + } + } + b +} + +#[test] +fn the_texts_of_lot_t3_change_these_phrases_only() { + let (t2, today) = (t2_seed(), today_seed()); + for code in CODES { + for lang in LANGS { + let (before, now) = (text(&t2, code, lang), text(&today, code, lang)); + assert_eq!(before.0, now.0, "{code} {lang}: the title is kept"); + assert_eq!(t3_from_t2(code, lang, before.1), now.1, "{code} {lang}: the phrases of T3, and nothing else"); + } + } + for (code, lang, old, new) in PHRASES_T3 { + let (_, body) = text(&today, code, lang); + assert!(!body.contains(old), "{code} {lang}: still the words of T2\n{body}"); + assert!(body.contains(new), "{code} {lang}: missing « {new} »\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] +} + +/// N1.1, the phrase of lot T3: « sous le quorum, les votants qui manquent comptent comme « non », +/// et il faut autant de voix pour que si le quorum avait voté ». 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. +#[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 voix 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."), + ]; + for (lang, phrase) in said { + assert!(text(&today, "N1.1", lang).1.contains(phrase), "N1.1 {lang}: « {phrase} »"); + } + let quorums = |w: u32, lowest: u32| -> Vec { + if w <= 10_000 { + (lowest..=w).collect() + } else { + [lowest, 2, 3, 12, 50, 100, 1000, w / 2, w - 1, w].into_iter().filter(|q| *q >= lowest).collect() + } + }; + let mut cases = 0u64; + let mut minimum = Vec::new(); + // Monnaie libre on every W of the grid (its reference quorum 50); the test group on a few + // members (its reference quorum 1). + let grid: [(bool, f64, &[u32]); 2] = [(false, 50.0, &ELECTORATES), (true, 1.0, &[1, 2, 3, 4, 10, 40])]; + for (test_group, vote_floor, electorates) in grid { + 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 scale = Scale { electorate: w, participation: p, quorums: [lowest; 5] }; + for k in InertiaLevel::ALL { + for q in quorums(w, lowest) { + let params = scale.params(k, q); + let at = format!("essai {test_group} W {w} p̃ {p} k {} Q {q}", k.get()); + // The « pour » asked had the quorum voted: never more than the quorum. + let at_quorum = wot_threshold(w, q, ¶ms).expect("valid"); + assert!((1..=q).contains(&at_quorum), "{at}: {at_quorum}"); + assert_eq!(yes_needed(0, w, ¶ms), Ok(at_quorum), "{at}"); + for t in [0, 1, q / 2, q - 1].into_iter().filter(|t| *t < q) { + assert_eq!(wot_threshold(w, t, ¶ms), Ok(at_quorum), "{at}, {t} voters"); + } + if q <= 30 { + for y in 0..q { + for n in 0..q - y { + let ok = passes(Tally { for_votes: y, against: n }, w, ¶ms).expect("valid"); + assert_eq!(ok, y >= at_quorum, "{at}: {y} pour, {n} contre"); + } + } + } + if at_quorum == q { + minimum.push((test_group, w, k.get(), q)); + } + cases += 1; + } + } + } + } + } + assert!(cases > 500_000, "{cases}"); + // « Pas un nombre minimal de votants » (T2) is false: the quorum is a minimum at Q = 2 in + // Monnaie libre, at every level and on every W; at Q = 1 in the test group. + for w in ELECTORATES { + for k in 1..=5 { + assert!(minimum.contains(&(false, w, k, 2)), "W {w} k {k}: two « pour » at a quorum of 2"); + } + } + assert!(minimum.iter().any(|m| m.0 && m.3 == 1)); +} + +/// 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. +#[test] +fn the_share_of_a_level_as_usual_is_asked_beyond_the_quorum() { + let mut under = 0; + for w in ELECTORATES { + let medians = [w / 100 + 1, 12, 40, 50, 100, 120, 1000, w / 2]; + for usual in medians { + let part = Participation::of(w, 50.0, Some(f64::from(usual)), 9); + if part.bounded { + continue; + } + let scale = Scale { electorate: w, participation: part.ratio, quorums: [12, 12, 50, 50, 50] }; + for k in InertiaLevel::ALL { + let (s, m) = (k.share_pct() / 100.0, k.majority_pct() / 100.0); + for q in [2, 12, 50, 100, 1000, usual, usual + 1, w].into_iter().filter(|q| (2..=w).contains(q)) { + let params = scale.params(k, q); + let at = format!("W {w} usual {usual} k {} Q {q}", k.get()); + let asked = required_ratio(w, judged_voters(usual, q), ¶ms); + if usual >= q { + assert!((asked - s).abs() < 1e-9, "{at}: {asked}, not s"); + } else { + assert!(asked < s - 1e-9, "{at}: {asked}"); + under += 1; + } + // Everyone votes: M, at every quorum. + assert!((required_ratio(w, judged_voters(w, q), ¶ms) - m).abs() < 1e-12, "{at}: M"); + } + } + } + } + assert!(under > 0); + // 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 %. + let (w, usual) = (3000, 40); + let part = Participation::of(w, 50.0, Some(f64::from(usual)), 9); + assert!(!part.bounded); + let scale = Scale { electorate: w, participation: part.ratio, quorums: [12, 12, 50, 50, 50] }; + let d = InertiaLevel::DEFAULT; + let q = scale.quorum(d, None); + assert_eq!((d.get(), q, d.share_pct()), (3, 50, 75.0)); + let p = scale.params(d, q); + assert_eq!(yes_needed(0, w, &p), Ok(38)); + assert!((required_ratio(w, usual, &p) - 0.75).abs() < 1e-12, "75 % is the share of 40 voters on the curve"); + assert_eq!(wot_threshold(w, usual, &p), Ok(38), "but 40 voters are judged as 50"); +} + +/// F1.1, « B : la base ; à la puissance W, elle est si petite qu'elle ne compte pas », and F1.2, +/// « dans la toile de confiance, `B^W` est si petit qu'il ne compte pas »: true on every W of the +/// 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. +#[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])); + let mut cases = 0; + for w in ELECTORATES.into_iter().chain([102, 5000, 5708]) { + for p in participations(w, 50.0) { + 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) { + let with_b = scale.params(k, q); + assert_eq!(with_b.base, 0.1); + let without = FormulaParams { base: 0.0, ..with_b }; + for t in [0, 1, q - 1, q, q + 1, usual, w / 2, w - 1, w].into_iter().filter(|t| *t <= w) { + assert_eq!( + wot_threshold(w, t, &with_b), + wot_threshold(w, t, &without), + "W {w} p̃ {p} k {} Q {q} T {t}", + k.get() + ); + cases += 1; + } + } + } + } + } + assert!(cases > 5000, "{cases}"); + // On a whole number: everyone votes on an even W at level 1, half exactly; the reference + // turnout of 100 voters, s_k of them exactly. + let level1 = Scale { electorate: 5708, participation: 0.1, quorums: [12; 5] }.params(level(1), 12); + assert_eq!(wot_threshold(5708, 5708, &level1), Ok(2854)); + let w = 5707; + let scale = Scale { electorate: w, participation: Participation::of(w, 50.0, None, 0).ratio, quorums: [12; 5] }; + let at_100: Vec = InertiaLevel::ALL.iter().map(|k| scale.required(100, *k, 12).expect("valid")).collect(); + assert_eq!(at_100, [60, 67, 75, 83, 90]); + // The test group: two members, both vote. B^W = 0,01 asks for two « pour », not one — a tie + // never passes there. + let essai = Scale { electorate: 2, participation: Participation::of(2, 1.0, None, 0).ratio, quorums: [1; 5] }; + let p = essai.params(level(1), 1); + assert_eq!(wot_threshold(2, 2, &p), Ok(2)); + assert_eq!(wot_threshold(2, 2, &FormulaParams { base: 0.0, ..p }), Ok(1)); +} + // --------------------------------------------------------------------------- // The formulas: evaluated as written, against the engine // --------------------------------------------------------------------------- @@ -896,26 +1185,28 @@ fn the_amendments_of_lot_t_went_from_m5_to_the_texts_of_m6b() { } #[test] -fn the_amendments_of_lot_t2_go_from_the_texts_of_m6b_to_the_seed_of_today() { - let (m6b, today) = (m6b_seed(), today_seed()); +fn the_amendments_of_lot_t2_went_from_the_texts_of_m6b_to_those_of_t2() { + let (m6b, t2) = (m6b_seed(), t2_seed()); for code in CODES { let a = amendment(code, "-phrases"); let t = amendment(code, "-quorum"); assert_eq!((a.doc, a.version), ("acte-engagement-certification", Version::Open("1.0.0")), "{code}"); let (title, body) = text(&m6b, code, "fr"); - assert_eq!(seed_amend::fingerprint(title, body), a.from, "{code}: the text production holds"); - let (title, body) = text(&today, code, "fr"); - assert_eq!(seed_amend::fingerprint(title, body), a.to, "{code}: the text of the seed today"); + assert_eq!(seed_amend::fingerprint(title, body), a.from, "{code}: the text production held before M6c"); + let (title, body) = text(&t2, code, "fr"); + assert_eq!(seed_amend::fingerprint(title, body), a.to, "{code}: the text production holds since M6c"); assert_ne!(a.from, a.to, "{code}"); - // It follows lot T's, which wrote the text it replaces; nothing replaces it yet. + // It follows lot T's, which wrote the text it replaces; lot T3's replaces it in turn, + // where it changes the text (F1.2, N1.1). + let later = AMENDMENTS.iter().find(|l| l.follows == Some(a.id)).map(|l| l.id); assert_eq!( (a.follows, t.to, t.superseded_by, a.superseded_by), - (Some(t.id), a.from, Some(a.id), None), + (Some(t.id), a.from, Some(a.id), later), "{code}" ); // The titles stay, and only the phrases of the review change (`PHRASES`). for lang in LANGS { - let (before, now) = (text(&m6b, code, lang), text(&today, code, lang)); + let (before, now) = (text(&m6b, code, lang), text(&t2, code, lang)); assert_eq!(before.0, now.0, "{code} {lang}: the title is kept"); let mut b = before.1.to_owned(); for (c, l, old, new) in PHRASES { @@ -1008,7 +1299,12 @@ async fn decisions(state: &AppState, lot: &str) -> Vec<(String, String, String, /// The rows the amendments of a lot added to the publication queue: (group, reason). async fn queued(state: &AppState, lot: &str) -> Vec<(String, String)> { - let reasons: Vec<&str> = CODES.iter().map(|c| amendment(c, lot).reason).collect(); + let reasons: Vec<&str> = AMENDMENTS + .iter() + .filter(|a| a.id.starts_with("2026-11-03-") && a.id.ends_with(lot)) + .map(|a| a.reason) + .collect(); + assert!(!reasons.is_empty(), "no amendment « {lot} »"); let rows: Vec<(String, String)> = sqlx::query_as("SELECT d.group_slug, o.reason FROM outbox o JOIN documents d ON d.id = o.doc_id ORDER BY o.id") .fetch_all(&state.db) @@ -1022,6 +1318,11 @@ fn m6b_config(tag: &str) -> Config { config(seed_dir(&format!("{tag}-m6b"), &m6b_seed())) } +/// A start with the seed of M6c (84f015a, and M7b since): lot T2 decides, lot T3 does not yet. +fn t2_config(tag: &str) -> Config { + config(seed_dir(&format!("{tag}-t2"), &t2_seed())) +} + /// Lot T, as M6b played it on production (03/11, 08 h 02): started with the seed of M6b. #[tokio::test] async fn production_after_m6_gets_the_quorum_texts_once() { @@ -1220,15 +1521,25 @@ async fn a_new_base_is_born_with_the_quorum_texts() { assert_eq!(((title.as_str(), body.as_str()), revision), (text(&today, code, "fr"), 1), "{group} {code}"); } } - // Lot T2's amendments find their text there: present. - let got = decisions(&state, "-phrases").await; - assert_eq!(got.len(), 6); - assert!(got.iter().all(|(_, _, o, _)| o == "present"), "{got:?}"); - // The amendments of M5 and of lot T on these articles, superseded, decide nothing on a - // new base. + // Lot T3's amendments find their text there: present. + let present = |g: &str, c: &str| (g.to_owned(), c.to_owned(), "present".to_owned(), None); + assert_eq!( + decisions(&state, "-chiffres").await, + [ + present("essai", "F1.2"), + present("essai", "N1.1"), + present("monnaie-libre", "F1.2"), + present("monnaie-libre", "N1.1") + ] + ); + // Lot T2's: present on F1.1, which lot T3 leaves as it is. + assert_eq!(decisions(&state, "-phrases").await, [present("essai", "F1.1"), present("monnaie-libre", "F1.1")]); + // The amendments of M5, of lot T and of lot T2 on these articles, superseded, decide nothing + // on a new base. let m5: Vec = sqlx::query_scalar( "SELECT amendment_id FROM seed_amendments - WHERE amendment_id IN ('2026-11-02-f1-2-regles', '2026-11-02-n1-1-regles') OR amendment_id LIKE '2026-11-03-%-quorum'", + WHERE amendment_id IN ('2026-11-02-f1-2-regles', '2026-11-02-n1-1-regles', '2026-11-03-f1-2-phrases', + '2026-11-03-n1-1-phrases') OR amendment_id LIKE '2026-11-03-%-quorum'", ) .fetch_all(&state.db) .await @@ -1265,7 +1576,7 @@ async fn last_revision(state: &AppState, id: &str) -> (i64, String, String, Stri #[tokio::test] async fn production_after_m6b_gets_the_three_phrases_once() { let base = production_after_m6b("t2-apply").await; - let (m6b, today) = (m6b_seed(), today_seed()); + let (m6b, t2) = (m6b_seed(), t2_seed()); // The base is as production after M6b: the texts of lot T, at revision 2 and 3. for group in GROUPS { for code in CODES { @@ -1285,13 +1596,13 @@ async fn production_after_m6b_gets_the_three_phrases_once() { let outbox = count(&base, "SELECT COUNT(*) FROM outbox").await; let revisions = count(&base, "SELECT COUNT(*) FROM revisions").await; - // Started with today's seed. - let state = start(&base.db, Config::for_tests()).await; + // Started with the seed of M6c. + let state = start(&base.db, t2_config("t2-apply")).await; let mut touched = Vec::new(); for group in GROUPS { for code in CODES { let (id, title, body, revision) = proposal(&state, group, code).await; - assert_eq!((title.as_str(), body.as_str()), text(&today, code, "fr"), "{group} {code}"); + assert_eq!((title.as_str(), body.as_str()), text(&t2, code, "fr"), "{group} {code}"); let m5 = code != "F1.1"; assert_eq!(revision, if m5 { 4 } else { 3 }, "{group} {code}"); let mut expected = vec!["initial", "amendment", "amendment"]; @@ -1327,7 +1638,7 @@ async fn production_after_m6b_gets_the_three_phrases_once() { .fetch_one(&state.db) .await .expect("translation"); - assert_eq!((t.as_str(), b.as_str()), text(&today, code, lang), "{group} {code} {lang}"); + assert_eq!((t.as_str(), b.as_str()), text(&t2, code, lang), "{group} {code} {lang}"); assert_eq!(hash, seed_amend::fingerprint(&title, &body), "{group} {code} {lang}: up to date"); assert_eq!((origin.as_str(), by), ("seed", None)); } @@ -1356,7 +1667,7 @@ async fn production_after_m6b_gets_the_three_phrases_once() { assert!(rows.iter().all(|(g, _)| g == "monnaie-libre"), "{rows:?}"); assert_eq!(count(&state, "SELECT COUNT(*) FROM outbox").await, outbox + 3); // Decided once: a second start replays nothing. - let again = start(&state.db, Config::for_tests()).await; + let again = start(&state.db, t2_config("t2-apply-again")).await; assert_eq!(decisions(&again, "-phrases").await, expected); assert_eq!(proposals(&again).await, proposals(&state).await); assert_eq!(translations(&again).await, translations(&state).await); @@ -1455,7 +1766,7 @@ async fn an_article_voted_on_keeps_the_text_of_m6b_and_says_why() { }; assert_eq!((traces(n11.clone()).await, traces(f12_essai.clone()).await), (1, 0)); - let state = start(&base.db, Config::for_tests()).await; + let state = start(&base.db, t2_config("t2-refuse")).await; let refused = |g: &str, c: &str, why: &str| (g.to_owned(), c.to_owned(), "refused".to_owned(), Some(why.to_owned())); let applied = |g: &str, c: &str| (g.to_owned(), c.to_owned(), "applied".to_owned(), None); @@ -1470,12 +1781,12 @@ async fn an_article_voted_on_keeps_the_text_of_m6b_and_says_why() { applied("monnaie-libre", "N1.1"), ] ); - let (m6b, today) = (m6b_seed(), today_seed()); + let (m6b, t2) = (m6b_seed(), t2_seed()); for group in GROUPS { for code in CODES { let kept = matches!((group, code), ("essai", "N1.1") | ("monnaie-libre", "F1.1" | "F1.2")); let (_, title, body, revision) = proposal(&state, group, code).await; - let expected = if kept { text(&m6b, code, "fr") } else { text(&today, code, "fr") }; + let expected = if kept { text(&m6b, code, "fr") } else { text(&t2, code, "fr") }; assert_eq!((title.as_str(), body.as_str()), expected, "{group} {code}"); let lot_t = if code == "F1.1" { 2 } else { 3 }; assert_eq!(revision, lot_t + i64::from(!kept), "{group} {code}"); @@ -1489,7 +1800,7 @@ async fn an_article_voted_on_keeps_the_text_of_m6b_and_says_why() { ); // The refusals stand: decided once, never tried again, even once the vote is gone. sqlx::query("DELETE FROM votes WHERE proposal_id = ?").bind(&f11).execute(&state.db).await.expect("delete"); - let again = start(&state.db, Config::for_tests()).await; + let again = start(&state.db, t2_config("t2-refuse-again")).await; assert_eq!(decisions(&again, "-phrases").await, decisions(&state, "-phrases").await); let (_, title, body, _) = proposal(&again, "monnaie-libre", "F1.1").await; assert_eq!((title.as_str(), body.as_str()), text(&m6b, "F1.1", "fr")); @@ -1512,12 +1823,12 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_three_phrases() { assert_eq!(kinds(&base, &id).await, ["initial", "amendment"], "{group} {code}: lot T, alone"); } } - let state = start(&db, Config::for_tests()).await; - let today = today_seed(); + let state = start(&db, t2_config("t2-born-m5")).await; + let t2 = t2_seed(); for group in GROUPS { for code in CODES { let (id, title, body, revision) = proposal(&state, group, code).await; - assert_eq!(((title.as_str(), body.as_str()), revision), (text(&today, code, "fr"), 3), "{group} {code}"); + assert_eq!(((title.as_str(), body.as_str()), revision), (text(&t2, code, "fr"), 3), "{group} {code}"); assert_eq!(kinds(&state, &id).await, ["initial", "amendment", "amendment"], "{group} {code}"); } } @@ -1525,3 +1836,287 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_three_phrases() { assert_eq!(got.len(), 6); assert!(got.iter().all(|(_, _, o, _)| o == "applied"), "{got:?}"); } + +// --------------------------------------------------------------------------- +// Lot T3 in production: on a base made as production is after M6c +// --------------------------------------------------------------------------- + +/// The articles lot T3 rewrites: F1.1 is left as it is, without an amendment. +const T3_CODES: [&str; 2] = ["F1.2", "N1.1"]; + +#[test] +fn the_amendments_of_lot_t3_go_from_the_texts_of_t2_to_the_seed_of_today() { + let (t2, today) = (t2_seed(), today_seed()); + for code in CODES { + let t2a = amendment(code, "-phrases"); + let found = AMENDMENTS.iter().find(|a| a.unit == code && a.id.ends_with("-chiffres")); + if !T3_CODES.contains(&code) { + assert!(found.is_none(), "{code}: no amendment for a text lot T3 does not change"); + for lang in LANGS { + assert_eq!(text(&t2, code, lang), text(&today, code, lang), "{code} {lang}"); + } + assert_eq!(t2a.superseded_by, None, "{code}"); + continue; + } + let a = found.unwrap_or_else(|| panic!("{code}: the amendment of lot T3")); + assert_eq!((a.doc, a.version), ("acte-engagement-certification", Version::Open("1.0.0")), "{code}"); + assert!(a.id.starts_with("2026-11-03-"), "{}", a.id); + let (title, body) = text(&t2, code, "fr"); + assert_eq!(seed_amend::fingerprint(title, body), a.from, "{code}: the text production holds since M6c"); + let (title, body) = text(&today, code, "fr"); + assert_eq!(seed_amend::fingerprint(title, body), a.to, "{code}: the text of the seed today"); + assert_ne!(a.from, a.to, "{code}"); + // It follows lot T2's, which wrote the text it replaces; nothing replaces it yet. + assert_eq!( + (a.follows, t2a.to, t2a.superseded_by, a.superseded_by), + (Some(t2a.id), a.from, Some(a.id), None), + "{code}" + ); + let t = amendment(code, "-quorum"); + assert_eq!( + seed_amend::earlier(AMENDMENTS, a), + [t.follows.expect("the amendment of M5"), t.id, t2a.id], + "{code}: M5, lot T, lot T2" + ); + } +} + +/// Production's history, for these articles, after M6c (03/11, 11 h 05): F1.1 at its +/// revision 3, F1.2 and N1.1 at their revision 4, each written by an amendment of the seed. +async fn production_after_m6c(tag: &str) -> AppState { + let base = production_after_m6b(tag).await; + start(&base.db, t2_config(tag)).await +} + +/// The history of a starting text at `revision`: « initial », then one « amendment » for each +/// text of the seed written over it. +fn history(revision: i64) -> Vec<&'static str> { + let amended = usize::try_from(revision - 1).expect("a revision"); + std::iter::once("initial").chain(std::iter::repeat_n("amendment", amended)).collect() +} + +#[tokio::test] +async fn production_after_m6c_gets_the_texts_without_dated_figures_once() { + let base = production_after_m6c("t3-apply").await; + let (t2, today) = (t2_seed(), today_seed()); + // The base is as production after M6c: the texts of lot T2, F1.1 at revision 3, F1.2 and + // N1.1 at revision 4. + for group in GROUPS { + for code in CODES { + let (id, title, body, revision) = proposal(&base, group, code).await; + assert_eq!((title.as_str(), body.as_str()), text(&t2, code, "fr"), "{group} {code}"); + assert_eq!(revision, if code == "F1.1" { 3 } else { 4 }, "{group} {code}"); + assert_eq!(kinds(&base, &id).await, history(revision), "{group} {code}"); + } + } + let lot_t2 = decisions(&base, "-phrases").await; + assert_eq!(lot_t2.len(), 6); + assert!(lot_t2.iter().all(|(_, _, o, _)| o == "applied"), "{lot_t2:?}"); + assert!(decisions(&base, "-chiffres").await.is_empty(), "the seed of M6c decides nothing of lot T3"); + let (before, before_tr) = (proposals(&base).await, translations(&base).await); + let outbox = count(&base, "SELECT COUNT(*) FROM outbox").await; + let revisions = count(&base, "SELECT COUNT(*) FROM revisions").await; + + // Started with today's seed. + let state = start(&base.db, Config::for_tests()).await; + let mut touched = Vec::new(); + for group in GROUPS { + for code in T3_CODES { + let (id, title, body, revision) = proposal(&state, group, code).await; + assert_eq!((title.as_str(), body.as_str()), text(&today, code, "fr"), "{group} {code}"); + assert_eq!(revision, 5, "{group} {code}"); + assert_eq!(kinds(&state, &id).await, history(5), "{group} {code}"); + // The history keeps the text of lot T2, and says the new one, without author. + assert_eq!( + last_revision(&state, &id).await, + (5, "amendment".to_owned(), title.clone(), body.clone(), None), + "{group} {code}" + ); + let (kind, t, b): (String, String, String) = + sqlx::query_as("SELECT kind, title, body FROM revisions WHERE proposal_id = ? AND revision = 4") + .bind(&id) + .fetch_one(&state.db) + .await + .expect("revision of lot T2"); + assert_eq!( + (kind.as_str(), (t.as_str(), b.as_str())), + ("amendment", text(&t2, code, "fr")), + "{group} {code}" + ); + for lang in ["en", "es"] { + let (t, b, hash, origin, by): (String, String, String, String, Option) = sqlx::query_as( + "SELECT title, body, source_hash, origin, updated_by FROM translations WHERE proposal_id = ? AND lang = ?", + ) + .bind(&id) + .bind(lang) + .fetch_one(&state.db) + .await + .expect("translation"); + assert_eq!((t.as_str(), b.as_str()), text(&today, code, lang), "{group} {code} {lang}"); + assert_eq!(hash, seed_amend::fingerprint(&title, &body), "{group} {code} {lang}: up to date"); + assert_eq!((origin.as_str(), by), ("seed", None)); + } + touched.push(id); + } + } + let expected: Vec<(String, String, String, Option)> = GROUPS + .iter() + .flat_map(|g| T3_CODES.iter().map(move |c| ((*g).to_owned(), (*c).to_owned(), "applied".to_owned(), None))) + .collect(); + assert_eq!(decisions(&state, "-chiffres").await, expected); + // Lot T2's decisions stay as they were taken; superseded, it decides nothing more. + assert_eq!(decisions(&state, "-phrases").await, lot_t2); + // Nothing else is touched: F1.1, no other proposal, no other translation, no vote, four + // revisions. + let keep = |rows: Vec| rows.into_iter().filter(|r| !touched.contains(&r.0)).collect::>(); + assert_eq!(keep(proposals(&state).await), keep(before)); + let others = |rows: Vec<(String, String, String, String, String)>| { + rows.into_iter().filter(|r| !touched.contains(&r.0)).collect::>() + }; + assert_eq!(others(translations(&state).await), others(before_tr)); + assert_eq!(count(&state, "SELECT COUNT(*) FROM votes").await, 0); + assert_eq!(count(&state, "SELECT COUNT(*) FROM revisions").await, revisions + 4); + // Monnaie libre republishes, once per article; the test group never. + let rows = queued(&state, "-chiffres").await; + assert_eq!(rows.len(), 2, "{rows:?}"); + assert!(rows.iter().all(|(g, _)| g == "monnaie-libre"), "{rows:?}"); + assert_eq!(count(&state, "SELECT COUNT(*) FROM outbox").await, outbox + 2); + // Decided once: a second start replays nothing. + let again = start(&state.db, Config::for_tests()).await; + assert_eq!(decisions(&again, "-chiffres").await, expected); + assert_eq!(proposals(&again).await, proposals(&state).await); + assert_eq!(translations(&again).await, translations(&state).await); + assert_eq!(count(&again, "SELECT COUNT(*) FROM revisions").await, revisions + 4); + assert_eq!(count(&again, "SELECT COUNT(*) FROM outbox").await, outbox + 2); +} + +#[tokio::test] +async fn an_article_voted_on_keeps_the_text_of_t2_and_says_why() { + let base = production_after_m6c("t3-refuse").await; + let doc: String = sqlx::query_scalar("SELECT id FROM documents WHERE group_slug = 'monnaie-libre'") + .fetch_one(&base.db) + .await + .expect("doc"); + identity(&base, "dev:vera").await; + // In Monnaie libre, a simulated vote counted on N1.1, at its revision 4… + let (n11, ..) = proposal(&base, "monnaie-libre", "N1.1").await; + sqlx::query( + "INSERT INTO votes (proposal_id, identity_id, choice, revision, cast_at, source) + VALUES (?, 'dev:vera', 'for', 4, '2026-11-03T12:00:00Z', 'simulated')", + ) + .bind(&n11) + .execute(&base.db) + .await + .expect("vote"); + // …and a ballot still pending on F1.2, at its revision 4: cast, not proven yet. + let (f12, ..) = proposal(&base, "monnaie-libre", "F1.2").await; + sqlx::query( + "INSERT INTO ballots (id, doc_id, requested_by, salt, digest, remark_hash, account, amount, status, mode, + created_at, expires_at) + VALUES ('b3', ?, 'dev:vera', 'salt', 'digest-b3', 'remark-b3', '', 0, 'pending', 'simulated', + '2026-11-03T12:00:00Z', '2026-11-04T12:00:00Z')", + ) + .bind(&doc) + .execute(&base.db) + .await + .expect("ballot"); + sqlx::query("INSERT INTO ballot_lines (ballot_id, proposal_id, revision, choice) VALUES ('b3', ?, 4, 'against')") + .bind(&f12) + .execute(&base.db) + .await + .expect("line"); + // In the test group, Luna votes « contre » N1.1 and keeps her vote; she votes on F1.2, then + // withdraws: a direct vote withdrawn leaves no trace (the guard's known limit, `seed_amend`). + let luna = Voter::login(&base, "Luna").await; + let (n11_essai, ..) = proposal(&base, "essai", "N1.1").await; + luna.vote(&n11_essai, Some("against")).await; + let (f12_essai, ..) = proposal(&base, "essai", "F1.2").await; + luna.vote(&f12_essai, Some("against")).await; + luna.vote(&f12_essai, None).await; + + let state = start(&base.db, Config::for_tests()).await; + let refused = + |g: &str, c: &str, why: &str| (g.to_owned(), c.to_owned(), "refused".to_owned(), Some(why.to_owned())); + let applied = |g: &str, c: &str| (g.to_owned(), c.to_owned(), "applied".to_owned(), None); + assert_eq!( + decisions(&state, "-chiffres").await, + [ + applied("essai", "F1.2"), + refused("essai", "N1.1", "votes"), + refused("monnaie-libre", "F1.2", "ballots"), + refused("monnaie-libre", "N1.1", "votes"), + ] + ); + let (t2, today) = (t2_seed(), today_seed()); + for group in GROUPS { + for code in T3_CODES { + let kept = (group, code) != ("essai", "F1.2"); + let (_, title, body, revision) = proposal(&state, group, code).await; + let expected = if kept { text(&t2, code, "fr") } else { text(&today, code, "fr") }; + assert_eq!((title.as_str(), body.as_str()), expected, "{group} {code}"); + assert_eq!(revision, 4 + i64::from(!kept), "{group} {code}"); + } + } + // Nothing applied in Monnaie libre: nothing more to publish. + assert!(queued(&state, "-chiffres").await.is_empty()); + // The refusals stand: decided once, never tried again, even once the vote is gone. + sqlx::query("DELETE FROM votes WHERE proposal_id = ?").bind(&n11).execute(&state.db).await.expect("delete"); + let again = start(&state.db, Config::for_tests()).await; + assert_eq!(decisions(&again, "-chiffres").await, decisions(&state, "-chiffres").await); + let (_, title, body, _) = proposal(&again, "monnaie-libre", "N1.1").await; + assert_eq!((title.as_str(), body.as_str()), text(&t2, "N1.1", "fr")); +} + +/// A base born on 02/11 with the seed of M5 (F1.2 and N1.1 at their revision 1, in M5's +/// words): lot T wrote their revision 2, lot T2 their revision 3, and lot T3 follows there too. +#[tokio::test] +async fn a_base_born_with_the_texts_of_m5_gets_the_texts_of_t3() { + let prod = production_seed(); + let db = open_db("sqlite::memory:").await.expect("db"); + let first = AppState::new(config(seed_dir("t3-born-m5", &prod)), db.clone()).await.expect("state"); + seed::documents(&first).await.expect("born"); + start(&db, config(seed_dir("t3-born-m5-m5", &prod))).await; + start(&db, config(seed_dir("t3-born-m5-m6", &with_texts(&today_seed(), &prod, &CODES)))).await; + start(&db, m6b_config("t3-born-m5")).await; + let base = start(&db, t2_config("t3-born-m5")).await; + for group in GROUPS { + for code in T3_CODES { + let (id, ..) = proposal(&base, group, code).await; + assert_eq!(kinds(&base, &id).await, history(3), "{group} {code}: lots T and T2"); + } + } + let state = start(&db, Config::for_tests()).await; + let today = today_seed(); + for group in GROUPS { + for code in T3_CODES { + let (id, title, body, revision) = proposal(&state, group, code).await; + assert_eq!(((title.as_str(), body.as_str()), revision), (text(&today, code, "fr"), 4), "{group} {code}"); + assert_eq!(kinds(&state, &id).await, history(4), "{group} {code}"); + } + } + let got = decisions(&state, "-chiffres").await; + assert_eq!(got.len(), 4); + assert!(got.iter().all(|(_, _, o, _)| o == "applied"), "{got:?}"); +} + +/// A base born with the texts of T2 (a first start on the seed of M6c): their revision 1, +/// replaced as it is. +#[tokio::test] +async fn a_base_born_with_the_texts_of_t2_gets_the_texts_of_t3() { + let db = open_db("sqlite::memory:").await.expect("db"); + start(&db, t2_config("t3-born-t2")).await; + let state = start(&db, Config::for_tests()).await; + let today = today_seed(); + for group in GROUPS { + for code in CODES { + let (id, title, body, revision) = proposal(&state, group, code).await; + let changed = T3_CODES.contains(&code); + assert_eq!((title.as_str(), body.as_str()), text(&today, code, "fr"), "{group} {code}"); + assert_eq!(revision, 1 + i64::from(changed), "{group} {code}"); + assert_eq!(kinds(&state, &id).await, history(revision), "{group} {code}"); + } + } + let got = decisions(&state, "-chiffres").await; + assert_eq!(got.len(), 4); + assert!(got.iter().all(|(_, _, o, _)| o == "applied"), "{got:?}"); +} From aba86200c60e5d88d9c1f458bf8124aa9e75a4bd Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 17:27:30 +0100 Subject: [PATCH 2/5] =?UTF-8?q?Lot=20T3=20:=20N1.1=20et=20F1.2=20sans=20ch?= =?UTF-8?q?iffre=20dat=C3=A9,=20chaque=20phrase=20vraie=20pour=20tout=20W?= =?UTF-8?q?=20et=20tout=20quorum=20(fr,=20en,=20es)=20=E2=80=94=20l'exempl?= =?UTF-8?q?e=20de=20N1.1=20retir=C3=A9=20;=20=C2=AB=20sous=20le=20quorum,?= =?UTF-8?q?=20les=20votants=20qui=20manquent=20comptent=20comme=20=C2=AB?= =?UTF-8?q?=20non=20=C2=BB,=20et=20il=20faut=20autant=20de=20voix=20pour?= =?UTF-8?q?=20que=20si=20le=20quorum=20avait=20vot=C3=A9=20=C2=BB=20;=20la?= =?UTF-8?q?=20part=20du=20cran=20=C2=AB=20comme=20d'habitude=20=C2=BB=20au?= =?UTF-8?q?-del=C3=A0=20du=20quorum=20(N1.1,=20F1.2)=20;=20F1.1=20laiss?= =?UTF-8?q?=C3=A9=20;=20amendements=20gard=C3=A9s=202026-11-03-f1-2-chiffr?= =?UTF-8?q?es=20et=20-n1-1-chiffres=20qui=20suivent=20ceux=20de=20T2=20(d?= =?UTF-8?q?=C3=A9pass=C3=A9s)=20;=20le=20=5Fsource=20du=20seed=20le=20dit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- crates/ld-server/src/seed_amend.rs | 32 +++++++++++++++++++++++- seeds/acte-engagement-certification.json | 14 +++++------ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/crates/ld-server/src/seed_amend.rs b/crates/ld-server/src/seed_amend.rs index fa91ab9..b34e760 100644 --- a/crates/ld-server/src/seed_amend.rs +++ b/crates/ld-server/src/seed_amend.rs @@ -13,6 +13,9 @@ //! earlier one replaces the text that one wrote. The same day (lot T2, after the //! review of M6b), three phrases of those texts are written true by amendments that //! follow lot T's: F1.1 is then at its revision 2, F1.2 and N1.1 at their revision 3. +//! Still the same day (lot T3, Yvv: no dated figure in a voted text), F1.2 and N1.1 are +//! written without a figure that depends on the web of trust, and true for every quorum, +//! by amendments that follow lot T2's: they are then at their revision 4; F1.1 is left. //! So an amendment admits, besides the seed's own revision 1, the revision written //! by the amendments it follows (`earlier`: the one it follows, the one that one //! follows…), applied in that order, the last being the one it follows, and nothing @@ -205,7 +208,7 @@ pub const AMENDMENTS: &[Amendment] = &[ to: "96caf353b33805f2894593d72c5b70fce2e751039cd7f4651d8842c986729a14", reason: "proposition de départ réécrite : F1.2, le quorum fixé tant qu'aucun vote n'a laissé de trace", follows: Some("2026-11-03-f1-2-quorum"), - superseded_by: None, + superseded_by: Some("2026-11-03-f1-2-chiffres"), }, Amendment { id: "2026-11-03-n1-1-phrases", @@ -216,6 +219,33 @@ pub const AMENDMENTS: &[Amendment] = &[ to: "7fbedcb8750c208f8790c4f87dc21d8bd1096a27a1a0044af6b9b8f2057f2aa8", reason: "proposition de départ réécrite : N1.1, le quorum n'est pas un minimum de votants", follows: Some("2026-11-03-n1-1-quorum"), + superseded_by: Some("2026-11-03-n1-1-chiffres"), + }, + // No dated figure in a voted text (lot T3, Yvv, 2026-11-03: « pas de chiffre datés dans un + // texte voté »), every sentence true for every W and every quorum: N1.1 loses its example + // (7 224 members, 120 voters) and says that under the quorum it takes as many « pour » as if + // the quorum had voted; N1.1 and F1.2 ask the share of a level « comme d'habitude » beyond the + // quorum. Each replaces the text lot T2 wrote, at its revision 4. F1.1 is not touched. + Amendment { + id: "2026-11-03-f1-2-chiffres", + doc: "acte-engagement-certification", + unit: "F1.2", + version: Version::Open("1.0.0"), + from: "96caf353b33805f2894593d72c5b70fce2e751039cd7f4651d8842c986729a14", + to: "ddce5aa5fcd13dfc3e374c37509a1bb8cfafddefb6e6add5a06b130b43216d62", + reason: "proposition de départ réécrite : F1.2, la part du cran par défaut au-delà du quorum", + follows: Some("2026-11-03-f1-2-phrases"), + superseded_by: None, + }, + Amendment { + id: "2026-11-03-n1-1-chiffres", + doc: "acte-engagement-certification", + unit: "N1.1", + version: Version::Open("1.0.0"), + from: "7fbedcb8750c208f8790c4f87dc21d8bd1096a27a1a0044af6b9b8f2057f2aa8", + to: "4289e3579ee747f6023162339ed60601b810d33462617b42a9f8b5b3735c3238", + reason: "proposition de départ réécrite : N1.1, sans chiffre daté ; sous le quorum, autant de voix pour que si le quorum avait voté", + follows: Some("2026-11-03-n1-1-phrases"), superseded_by: None, }, ]; diff --git a/seeds/acte-engagement-certification.json b/seeds/acte-engagement-certification.json index 126e16a..c7063a8 100644 --- a/seeds/acte-engagement-certification.json +++ b/seeds/acte-engagement-certification.json @@ -1,5 +1,5 @@ { - "_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). Aucun article en vigueur au départ. I1, E1, E2 : Propositions seed (« seedProposal »), ouvertes, mises en avant pour amorcer le vote, sans niveau d'inertie ici : elles démarrent au cran SEED_PROPOSAL_LEVEL (crates/ld-server/src/seed.rs : 2 « basse », choix d'Yvv du 2026-11-02, qui remontera leur inertie par un vote une fois les textes passés ; ADR 0011). Tant qu'aucun article n'est en vigueur, la publication attend le premier texte. Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21), inertie 3 / 5. I1 « Raison d'être » remplace le Préambule (texte d'Yvv, revue du 2026-10-30). Modules : le titre du document à 4, l'ordre des articles de chaque section à 2, en vigueur sans vote (seed a minima ; revue du 2026-10-30, ADR 0009). F1.2, N1, N1.1, O1 réécrits selon les règles appliquées (Yvv, 2026-11-02). Quorums par cran (« quorums », ADR 0013, Yvv, 2026-11-02 et 03) : 12 · 12 · 50 · 50 · 50 dans Monnaie libre, 1 partout dans le groupe d'essai ; sous le quorum, les votants qui manquent comptent comme « non » ; voteFloor reste le quorum de référence de la participation p0 = 2 × voteFloor / W. Une base existante reçoit ces changements par des amendements gardés (seed_amend, seed_proposal_amend, quorum_amend), jamais en réinstanciant le document. F1.1, F1.2 et N1.1 réécrits pour le quorum par cran (ADR 0013 ; Yvv, 2026-11-02 et 03 ; lot T, textes relus dans docs/chantiers/acte-certification/textes-quorum-seed.md) : sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part du cran. Trois phrases reprises après la relecture de M6b (lot T2) : la part du cran, la plus haute au quorum, baisse quand la participation monte (F1.1) ; l'auteur fixe le quorum tant que l'article ne garde la trace d'aucun vote (F1.2) ; le quorum n'est pas un nombre minimal de votants (N1.1).", + "_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). Aucun article en vigueur au départ. I1, E1, E2 : Propositions seed (« seedProposal »), ouvertes, mises en avant pour amorcer le vote, sans niveau d'inertie ici : elles démarrent au cran SEED_PROPOSAL_LEVEL (crates/ld-server/src/seed.rs : 2 « basse », choix d'Yvv du 2026-11-02, qui remontera leur inertie par un vote une fois les textes passés ; ADR 0011). Tant qu'aucun article n'est en vigueur, la publication attend le premier texte. Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21), inertie 3 / 5. I1 « Raison d'être » remplace le Préambule (texte d'Yvv, revue du 2026-10-30). Modules : le titre du document à 4, l'ordre des articles de chaque section à 2, en vigueur sans vote (seed a minima ; revue du 2026-10-30, ADR 0009). F1.2, N1, N1.1, O1 réécrits selon les règles appliquées (Yvv, 2026-11-02). Quorums par cran (« quorums », ADR 0013, Yvv, 2026-11-02 et 03) : 12 · 12 · 50 · 50 · 50 dans Monnaie libre, 1 partout dans le groupe d'essai ; sous le quorum, les votants qui manquent comptent comme « non » ; voteFloor reste le quorum de référence de la participation p0 = 2 × voteFloor / W. Une base existante reçoit ces changements par des amendements gardés (seed_amend, seed_proposal_amend, quorum_amend), jamais en réinstanciant le document. F1.1, F1.2 et N1.1 réécrits pour le quorum par cran (ADR 0013 ; Yvv, 2026-11-02 et 03 ; lot T, textes relus dans docs/chantiers/acte-certification/textes-quorum-seed.md) : sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part du cran. Trois phrases reprises après la relecture de M6b (lot T2) : la part du cran, la plus haute au quorum, baisse quand la participation monte (F1.1) ; l'auteur fixe le quorum tant que l'article ne garde la trace d'aucun vote (F1.2) ; le quorum n'est pas un nombre minimal de votants (N1.1). Lot T3 (Yvv, 03/11 : « pas de chiffre datés dans un texte voté ») : l'exemple chiffré de N1.1 retiré (aucun nombre des textes ne dépend de W ni d'une participation) ; N1.1, sous le quorum, il faut autant de voix pour que si le quorum avait voté (vrai pour tout quorum) ; N1.1 et F1.2, la part du cran comme d'habitude au-delà du quorum.", "slug": "acte-engagement-certification", "title": { "fr": "Acte d'engagement Certification", @@ -694,9 +694,9 @@ "es": "Parámetros por defecto" }, "body": { - "fr": "Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la façon dont le vote se déroule.\n\n**Un vote permanent, sans durée**\n\n- Chaque version se vote à tout moment, sans échéance. Chacun peut changer ou retirer son vote : le dernier fait foi.\n- Une proposition entre en vigueur dès qu'elle atteint son seuil. S'il y a déjà un texte en vigueur, il lui faut en plus une marge (voix pour moins voix exigées) plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir.\n- Si plusieurs propositions y parviennent, celle qui a la marge la plus large entre en vigueur.\n\n**Les lettres**\n\n- **W** : le nombre de membres de la toile de confiance Ğ1, relu sur la chaîne toutes les 6 heures.\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q**, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au cran 5. L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote. Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un texte.\n- **B = 0,1** : dans la toile de confiance, `B^W` est si petit qu'il ne compte pas.\n- **M** et **G** : donnés par le cran d'inertie de l'article (voir « Niveaux d'inertie »). G n'est pas fixé à la main : il se déduit de la participation habituelle du groupe.\n\n**Par défaut**\n\n- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude. Tout nouvel article commence à ce cran.\n- Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, tous les autres articles au cran 3.", - "en": "The values of the letters of the formula (see “WoT threshold formula”) and how the vote runs.\n\n**A permanent vote, with no duration**\n\n- Every version can be voted on at any time, with no deadline. Anyone can change or withdraw their vote: the last one is the one that counts.\n- A proposal comes into force as soon as it reaches its threshold. If a text is already in force, it also needs a wider margin (votes in favour minus votes required) than that text has. The replaced text becomes a proposal again, with its votes: it can come back.\n- If several proposals get there, the one with the widest margin comes into force.\n\n**The letters**\n\n- **W**: the number of members of the Ğ1 web of trust, read again from the chain every 6 hours.\n- **T**: the votes cast on the version, for and against.\n- **Q**, the quorum: below this number of voters, those who are missing count as votes against. Each article takes the one of its level: 12, 12, 50, 50 and 50 in Monnaie libre, from level 1 to level 5. The author of the first version of an article, an administrator for the starting texts, can set another one as long as the article keeps no trace of any vote. In Monnaie libre, never less than 2: a single vote never passes a text.\n- **B = 0.1**: in the web of trust, `B^W` is so small that it does not count.\n- **M** and **G**: given by the article's level of inertia (see “Inertia levels”). G is not set by hand: it follows from the group's usual turnout.\n\n**By default**\n\n- The default level is 3 (medium): M = 55%, a quorum of 50 in Monnaie libre, and the group asks for 75% of votes in favour when it votes as usual. Every new article starts at this level.\n- At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, all other articles at level 3.", - "es": "Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y cómo se desarrolla la votación.\n\n**Una votación permanente, sin duración**\n\n- Cada versión se vota en cualquier momento, sin plazo. Cada cual puede cambiar o retirar su voto: el último es el que cuenta.\n- Una propuesta entra en vigor en cuanto alcanza su umbral. Si ya hay un texto en vigor, necesita además un margen (votos a favor menos votos exigidos) más amplio que el de ese texto. El texto sustituido vuelve a ser una propuesta, con sus votos: puede volver.\n- Si varias propuestas lo consiguen, entra en vigor la que tiene el margen más amplio.\n\n**Las letras**\n\n- **W**: el número de miembros de la red de confianza Ğ1, leído de nuevo en la cadena cada 6 horas.\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**, el quórum: por debajo de este número de votantes, los que faltan cuentan como «no». Cada artículo toma el de su nivel: 12, 12, 50, 50 y 50 en Monnaie libre, del nivel 1 al nivel 5. El autor de la primera versión de un artículo, un administrador para los textos de partida, puede fijar otro mientras el artículo no guarde rastro de ningún voto. En Monnaie libre, nunca menos de 2: un solo voto nunca hace pasar un texto.\n- **B = 0,1**: en la red de confianza, `B^W` es tan pequeño que no cuenta.\n- **M** y **G**: los da el nivel de inercia del artículo (ver «Niveles de inercia»). G no se fija a mano: se deduce de la participación habitual del grupo.\n\n**Por defecto**\n\n- El nivel por defecto es el 3 (media): M = 55 %, un quórum de 50 en Monnaie libre, y el grupo pide un 75 % de votos a favor cuando vota como de costumbre. Todo artículo nuevo empieza en este nivel.\n- Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3." + "fr": "Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la façon dont le vote se déroule.\n\n**Un vote permanent, sans durée**\n\n- Chaque version se vote à tout moment, sans échéance. Chacun peut changer ou retirer son vote : le dernier fait foi.\n- Une proposition entre en vigueur dès qu'elle atteint son seuil. S'il y a déjà un texte en vigueur, il lui faut en plus une marge (voix pour moins voix exigées) plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir.\n- Si plusieurs propositions y parviennent, celle qui a la marge la plus large entre en vigueur.\n\n**Les lettres**\n\n- **W** : le nombre de membres de la toile de confiance Ğ1, relu sur la chaîne toutes les 6 heures.\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q**, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au cran 5. L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote. Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un texte.\n- **B = 0,1** : dans la toile de confiance, `B^W` est si petit qu'il ne compte pas.\n- **M** et **G** : donnés par le cran d'inertie de l'article (voir « Niveaux d'inertie »). G n'est pas fixé à la main : il se déduit de la participation habituelle du groupe.\n\n**Par défaut**\n\n- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » quand il vote comme d'habitude. Tout nouvel article commence à ce cran.\n- Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, tous les autres articles au cran 3.", + "en": "The values of the letters of the formula (see “WoT threshold formula”) and how the vote runs.\n\n**A permanent vote, with no duration**\n\n- Every version can be voted on at any time, with no deadline. Anyone can change or withdraw their vote: the last one is the one that counts.\n- A proposal comes into force as soon as it reaches its threshold. If a text is already in force, it also needs a wider margin (votes in favour minus votes required) than that text has. The replaced text becomes a proposal again, with its votes: it can come back.\n- If several proposals get there, the one with the widest margin comes into force.\n\n**The letters**\n\n- **W**: the number of members of the Ğ1 web of trust, read again from the chain every 6 hours.\n- **T**: the votes cast on the version, for and against.\n- **Q**, the quorum: below this number of voters, those who are missing count as votes against. Each article takes the one of its level: 12, 12, 50, 50 and 50 in Monnaie libre, from level 1 to level 5. The author of the first version of an article, an administrator for the starting texts, can set another one as long as the article keeps no trace of any vote. In Monnaie libre, never less than 2: a single vote never passes a text.\n- **B = 0.1**: in the web of trust, `B^W` is so small that it does not count.\n- **M** and **G**: given by the article's level of inertia (see “Inertia levels”). G is not set by hand: it follows from the group's usual turnout.\n\n**By default**\n\n- The default level is 3 (medium): M = 55%, 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. Every new article starts at this level.\n- At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, all other articles at level 3.", + "es": "Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y cómo se desarrolla la votación.\n\n**Una votación permanente, sin duración**\n\n- Cada versión se vota en cualquier momento, sin plazo. Cada cual puede cambiar o retirar su voto: el último es el que cuenta.\n- Una propuesta entra en vigor en cuanto alcanza su umbral. Si ya hay un texto en vigor, necesita además un margen (votos a favor menos votos exigidos) más amplio que el de ese texto. El texto sustituido vuelve a ser una propuesta, con sus votos: puede volver.\n- Si varias propuestas lo consiguen, entra en vigor la que tiene el margen más amplio.\n\n**Las letras**\n\n- **W**: el número de miembros de la red de confianza Ğ1, leído de nuevo en la cadena cada 6 horas.\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**, el quórum: por debajo de este número de votantes, los que faltan cuentan como «no». Cada artículo toma el de su nivel: 12, 12, 50, 50 y 50 en Monnaie libre, del nivel 1 al nivel 5. El autor de la primera versión de un artículo, un administrador para los textos de partida, puede fijar otro mientras el artículo no guarde rastro de ningún voto. En Monnaie libre, nunca menos de 2: un solo voto nunca hace pasar un texto.\n- **B = 0,1**: en la red de confianza, `B^W` es tan pequeño que no cuenta.\n- **M** y **G**: los da el nivel de inercia del artículo (ver «Niveles de inercia»). G no se fija a mano: se deduce de la participación habitual del grupo.\n\n**Por defecto**\n\n- El nivel por defecto es el 3 (media): M = 55 %, 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. Todo artículo nuevo empieza en este nivel.\n- Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3." } } ] @@ -760,9 +760,9 @@ "es": "Niveles de inercia" }, "body": { - "fr": "À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). 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 ». 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, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.\n\n`Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum s'il en manque.\n\n**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, et ce qui est à ce cran au départ.\n\n- **Cran 1 · très basse** : quorum 12 ; 60 % comme d'habitude, 50 % quand tout le monde vote. Aucun article au départ.\n- **Cran 2 · basse** : quorum 12 ; 67 % comme d'habitude, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.\n- **Cran 3 · moyenne** : quorum 50 ; 75 % comme d'habitude, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.\n- **Cran 4 · haute** : quorum 50 ; 83 % comme d'habitude, 60 % quand tout le monde vote. Le titre du document.\n- **Cran 5 · très haute** : quorum 50 ; 90 % comme d'habitude, 66 % quand tout le monde vote. Aucun article au départ.\n\n**La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. 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 ne dépend pas du quorum d'un article. Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour.\n\n**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.\n\nExemple : avec 7 224 membres et une participation habituelle de 120 votants, quand 120 personnes votent, il faut 72, 81, 90, 100 ou 108 voix pour, du cran 1 au cran 5 ; au cran 3, 75 % des 120 votants, soit 90. Sous le quorum, les absents comptent comme « non » : au cran 2 (quorum 12), 9 voix pour passent avec 3 « contre » au plus ; au cran 3 (quorum 50), il faut 39 voix pour, avec 11 « contre » au plus.", - "en": "At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). 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. 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 lower it gets, down to the majority M when everyone votes.\n\n`Share asked = M + (1 − M) × (1 − (T'/W)^G)`, where T' is the number of voters, or the quorum if some are missing.\n\n**The five levels**: the quorum in Monnaie libre, the share asked when the group votes as usual, then when everyone votes, and what is at that level at the start.\n\n- **Level 1 · very low**: quorum 12; 60% as usual, 50% when everyone votes. No article at the start.\n- **Level 2 · low**: quorum 12; 67% as usual, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.\n- **Level 3 · medium**: quorum 50; 75% as usual, 55% when everyone votes. The default level: all other articles, and every new article.\n- **Level 4 · high**: quorum 50; 83% as usual, 60% when everyone votes. The title of the document.\n- **Level 5 · very high**: quorum 50; 90% as usual, 66% when everyone votes. No article at the start.\n\n**The usual turnout** is the median number of voters on the group's proposals voted on over the past year. Until the group has voted on 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It stays between 1% and 50% of the electorate, and is measured once a day.\n\n**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.\n\nExample: with 7,224 members and a usual turnout of 120 voters, when 120 people vote, it takes 72, 81, 90, 100 or 108 votes in favour, from level 1 to level 5; at level 3, 75% of the 120 voters, i.e. 90. Below the quorum, those missing count as votes against: at level 2 (quorum 12), 9 votes in favour pass with 3 votes against at most; at level 3 (quorum 50), it takes 39 votes in favour, with 11 votes against at most.", - "es": "En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). 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». 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, más baja, hasta la mayoría M cuando vota todo el mundo.\n\n`Proporción exigida = M + (1 − M) × (1 − (T'/W)^G)`, donde T' es el número de votantes, o el quórum si faltan.\n\n**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, y lo que está en ese nivel al inicio.\n\n- **Nivel 1 · muy baja**: quórum 12; 60 % como de costumbre, 50 % cuando vota todo el mundo. Ningún artículo al inicio.\n- **Nivel 2 · baja**: quórum 12; 67 % como de costumbre, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.\n- **Nivel 3 · media**: quórum 50; 75 % como de costumbre, 55 % cuando vota todo el mundo. El nivel por defecto: todos los demás artículos, y todo artículo nuevo.\n- **Nivel 4 · alta**: quórum 50; 83 % como de costumbre, 60 % cuando vota todo el mundo. El título del documento.\n- **Nivel 5 · muy alta**: quórum 50; 90 % como de costumbre, 66 % cuando vota todo el mundo. Ningún artículo al inicio.\n\n**La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Hasta que el grupo haya votado 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se mantiene entre el 1 % y el 50 % del electorado, y se mide una vez al día.\n\n**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.\n\nEjemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 72, 81, 90, 100 o 108 votos a favor, del nivel 1 al nivel 5; en el nivel 3, el 75 % de los 120 votantes, es decir 90. Por debajo del quórum, los que faltan cuentan como «no»: en el nivel 2 (quórum 12), 9 votos a favor pasan con 3 en contra como mucho; en el nivel 3 (quórum 50), hacen falta 39 votos a favor, con 11 en contra como mucho." + "fr": "À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). Chaque cran a aussi son quorum : sous le quorum, les votants qui manquent comptent comme « non », et il faut autant de voix pour que si le quorum avait voté. 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, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.\n\n`Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum s'il en manque.\n\n**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, et ce qui est à ce cran au départ.\n\n- **Cran 1 · très basse** : quorum 12 ; 60 % comme d'habitude, 50 % quand tout le monde vote. Aucun article au départ.\n- **Cran 2 · basse** : quorum 12 ; 67 % comme d'habitude, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.\n- **Cran 3 · moyenne** : quorum 50 ; 75 % comme d'habitude, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.\n- **Cran 4 · haute** : quorum 50 ; 83 % comme d'habitude, 60 % quand tout le monde vote. Le titre du document.\n- **Cran 5 · très haute** : quorum 50 ; 90 % comme d'habitude, 66 % quand tout le monde vote. Aucun article au départ.\n\n**La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. 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 ne dépend pas du quorum d'un article. Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour.\n\n**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.", + "en": "At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). 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. 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 lower it gets, down to the majority M when everyone votes.\n\n`Share asked = M + (1 − M) × (1 − (T'/W)^G)`, where T' is the number of voters, or the quorum if some are missing.\n\n**The five levels**: the quorum in Monnaie libre, the share asked beyond the quorum, when the group votes as usual, then when everyone votes, and what is at that level at the start.\n\n- **Level 1 · very low**: quorum 12; 60% as usual, 50% when everyone votes. No article at the start.\n- **Level 2 · low**: quorum 12; 67% as usual, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.\n- **Level 3 · medium**: quorum 50; 75% as usual, 55% when everyone votes. The default level: all other articles, and every new article.\n- **Level 4 · high**: quorum 50; 83% as usual, 60% when everyone votes. The title of the document.\n- **Level 5 · very high**: quorum 50; 90% as usual, 66% when everyone votes. No article at the start.\n\n**The usual turnout** is the median number of voters on the group's proposals voted on over the past year. Until the group has voted on 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It stays between 1% and 50% of the electorate, and is measured once a day.\n\n**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.", + "es": "En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). 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. 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, más baja, hasta la mayoría M cuando vota todo el mundo.\n\n`Proporción exigida = M + (1 − M) × (1 − (T'/W)^G)`, donde T' es el número de votantes, o el quórum si faltan.\n\n**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, y lo que está en ese nivel al inicio.\n\n- **Nivel 1 · muy baja**: quórum 12; 60 % como de costumbre, 50 % cuando vota todo el mundo. Ningún artículo al inicio.\n- **Nivel 2 · baja**: quórum 12; 67 % como de costumbre, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.\n- **Nivel 3 · media**: quórum 50; 75 % como de costumbre, 55 % cuando vota todo el mundo. El nivel por defecto: todos los demás artículos, y todo artículo nuevo.\n- **Nivel 4 · alta**: quórum 50; 83 % como de costumbre, 60 % cuando vota todo el mundo. El título del documento.\n- **Nivel 5 · muy alta**: quórum 50; 90 % como de costumbre, 66 % cuando vota todo el mundo. Ningún artículo al inicio.\n\n**La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Hasta que el grupo haya votado 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se mantiene entre el 1 % y el 50 % del electorado, y se mide una vez al día.\n\n**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." } } ] From 6db4f900a7beec92d2ac3a5a986e178f06f7ab4b Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 17:27:30 +0100 Subject: [PATCH 3/5] =?UTF-8?q?Lot=20T3=20:=20textes-quorum-seed.md=20(sec?= =?UTF-8?q?tion=20=C2=AB=20Lot=20T3=20=C2=BB=20:=20la=20phrase=20d'Yvv=20d?= =?UTF-8?q?u=2003/11,=20les=20points=20tranch=C3=A9s,=20chaque=20phrase=20?= =?UTF-8?q?avant=20/=20apr=C3=A8s=20et=20pourquoi,=20F1.1=20relu=20et=20la?= =?UTF-8?q?iss=C3=A9,=20la=20garde=20en=20production=20et=20la=20file=20de?= =?UTF-8?q?=20Monnaie=20libre=20;=20les=20blocs=20F1.2=20et=20N1.1=20?= =?UTF-8?q?=C3=A0=20jour=20;=20point=203=20tranch=C3=A9,=20points=2010=20e?= =?UTF-8?q?t=2011=20;=20les=20lignes=20des=20chiffres=20retir=C3=A9s=20?= =?UTF-8?q?=C3=B4t=C3=A9es)=20;=20CLAUDE.md=20(les=20tests=20des=20textes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 6 +- .../acte-certification/textes-quorum-seed.md | 212 +++++++++++++++--- 2 files changed, 187 insertions(+), 31 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 14112e1..b7dbe54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,8 +71,10 @@ crates/ # quorum.rs (quorums par cran, la route, « un vote existe », l'amendement) · # fixtures/seed-production-2026-11-02/ (le seed que la production fait tourner : # I1/E1/E2 en vigueur, pour rejouer les amendements sur une base comme elle) · - # seed_quorum_texts.rs (lots T et T2 : F1.1, F1.2, N1.1 ; fixtures/seed-quorum-m6b.json, - # leurs textes depuis M6b, pour rejouer T2 sur une base comme la production) + # seed_quorum_texts.rs (lots T, T2 et T3 : F1.1, F1.2, N1.1 ; aucun chiffre daté, chaque + # phrase vraie pour tout W et tout quorum ; fixtures/seed-quorum-m6b.json et + # seed-quorum-t2.json, leurs textes depuis M6b et M6c, pour rejouer T2 et T3 sur une + # base comme la production) seeds/ # acte-engagement-certification.json (trilingue, 33 articles, aucun en vigueur ; # I1/E1/E2 Propositions seed au cran 2 (seed::SEED_PROPOSAL_LEVEL), inertie 3 ailleurs ; # modules en vigueur sans vote : le titre à 4, l'ordre de chaque section à 2 ; diff --git a/docs/chantiers/acte-certification/textes-quorum-seed.md b/docs/chantiers/acte-certification/textes-quorum-seed.md index f01657e..2ca39de 100644 --- a/docs/chantiers/acte-certification/textes-quorum-seed.md +++ b/docs/chantiers/acte-certification/textes-quorum-seed.md @@ -11,8 +11,147 @@ > **Mis à jour le 2026-11-03 par le lot T2** (branche `lot/t2-phrases`), après la relecture > indépendante de M6b : ces textes sont en ligne depuis M6b (03/11, 08 h 02), et trois de leurs > phrases disaient plus que les règles. Elles sont reprises ci-dessous, avant et après -> (« Lot T2 »). Les blocs de texte de ce fichier sont ceux du seed d'aujourd'hui, phrases -> reprises comprises. Rien du lot T2 n'est en ligne. +> (« Lot T2 »). *(En ligne depuis M6c, le 03/11 à 11 h 05.)* +> +> **Mis à jour le 2026-11-03 par le lot T3** (branche `lot/t3-chiffres`), après ta validation du +> jour : N1.1 perd son exemple chiffré, et quatre de ses phrases et de celles de F1.2 sont écrites +> vraies pour tout W et tout quorum (« Lot T3 », ci-dessous). Les blocs de texte de ce fichier sont +> ceux du seed d'aujourd'hui. Rien du lot T3 n'est en ligne. + +## Lot T3 : aucun chiffre daté, chaque phrase vraie pour tout W + +Ta réponse du 03/11 (avant 16 h 56), mot pour mot : « Je valide textes-quorum-seed (dont le point +3, pas de chiffre datés dans un texte voté). Tu peux lancer la livraison de decision. » + +**Les points tranchés le 03/11** (« Points à trancher », plus bas) : + +- **point 3 : retiré.** L'exemple de N1.1 (7 224 membres, 120 votants, et ce qui en était calculé) + sort des trois langues. Rien ne le remplace : un exemple qui ne porterait que des paramètres, vrai + pour tout W, ne dirait rien de plus que la liste des crans ; +- **points 1, 2, 4 et 8 : le défaut reste.** 1 : ton intention de remonter l'inertie n'est pas + écrite ; 2 : « Propositions seed » reste le mot du texte voté ; 4 : le groupe d'essai n'est pas + nommé, les quorums sont ceux « dans Monnaie libre » ; 8 : F1.2 garde « tant que l'article ne garde + la trace d'aucun vote » (le texte en ligne), et l'interface ta formule « avant le premier vote ». + +**Est daté** tout nombre qui dépend de l'état de la toile (W) ou d'une participation à une date. +Restent les paramètres et les règles : les quorums 12 · 12 · 50 · 50 · 50, M et s de chaque cran, +B = 0,1, les bornes 1 % et 50 %, « jamais moins de 2 », 6 heures, un an, une fois par jour, +5 propositions, la référence « deux fois le quorum de référence : 100 votants » (2 × 50). F1.1 et +F1.2 n'avaient aucun chiffre daté. Le test des nombres (`every_number_of_the_quorum_texts_is_the_engine_s`) +n'admet plus que des paramètres : chacun vient du seed ou d'une constante du moteur, et ils sont les +mêmes sur toute toile de 100 à 10 000 000 membres (`wot::PLAUSIBLE`, les tailles que le serveur tient +pour vraies) ; aucune participation n'y entre. Vu rouge sur le texte de T2 : N1.1 portait +7 224 · 120 · 72 · 81 · 90 · 100 · 108 · 75 % · 90 · 9 · 3 · 39 · 11, que rien ne justifie. + +Ensuite, **chaque phrase des trois textes relue** pour tout W et tout quorum dans les bornes +(2 à W dans Monnaie libre, 1 à W dans le groupe d'essai), tout cran, toute participation entre 1 % +et 50 %. Trois phrases étaient fausses quelque part, chacune prouvée par un test ; elles sont +corrigées au plus court. Rien d'autre ne change ; les titres restent. + +### N1.1 — Niveaux d'inertie + +- **Le quorum** (premier paragraphe). + - **Avant** : « 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 ». » + - **Après** : « Chaque cran a aussi son quorum : sous le quorum, les votants qui manquent comptent + comme « non », et il faut autant de voix pour que si le quorum avait voté. » + - **Pourquoi** : « pas un nombre minimal de votants » est faux pour un quorum d'auteur bas. Au + quorum 2, la part du quorum dépasse 50 % : il faut 2 « pour », une seule voix ne passe jamais, et + le quorum est alors un minimum de votants, à tout cran et sur toute toile (de même au quorum 1 du + groupe d'essai). La phrase nouvelle est vraie pour tout quorum : avec moins de votants que le + quorum, les « pour » qui passent sans « contre » sont exactement le seuil du quorum + (`yes_needed(0)` égale le seuil à T = Q), et un décompte passe dès que ses « pour » l'atteignent. + Prouvé sur une grille par `under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted` : + W de 100 à 10 000 000, chaque quorum de 2 à W jusqu'à 10 000 membres (des quorums choisis + au-delà), les cinq crans, quatre participations dans les bornes ; le groupe d'essai de 1 à + 40 membres, chaque quorum de 1 à W ; plus de 500 000 cas. C'est le défaut d'orch, mot pour mot. + - en : « 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. » ; es : « 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. » +- **La liste des crans** (son introduction). + - **Avant** : « **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, et ce qui est à ce cran au départ. » + - **Après** : « **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, et ce qui est à ce + cran au départ. » + - **Pourquoi** : « 75 % comme d'habitude » n'est vrai que si la participation habituelle atteint + le quorum. En dessous, le groupe qui vote comme d'habitude est jugé sur le quorum : sur une toile + de 3 000 membres dont la médiane est de 40 votants (dans les bornes : 1 %, c'est 30), au cran 3, + quorum 50, il faut 38 « pour » de ces 40 votants, 95 %, pas 75 %. Cela arrive dès que la toile + passe sous 5 000 membres (1 % sous 50) avec une médiane sous 50, ou quand l'auteur d'un article + fixe un quorum au-dessus de la participation habituelle. Prouvé par + `the_share_of_a_level_as_usual_is_asked_beyond_the_quorum` : la part du cran exactement quand la + participation habituelle atteint le quorum, une part plus basse, sur plus de votants, sinon ; M + quand tout le monde vote, à tout quorum. + - en : « the share asked beyond the quorum, when the group votes as usual, then when everyone + votes » ; es : « la proporción exigida más allá del quórum, cuando el grupo vota como de + costumbre, luego cuando vota todo el mundo ». +- **L'exemple** (dernier paragraphe) : retiré entier, dans les trois langues (point 3). + +### F1.2 — Paramètres par défaut + +- **Avant** : « Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie + libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude. » +- **Après** : « Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie + libre, et, au-delà du quorum, le groupe demande 75 % de « pour » quand il vote comme d'habitude. » +- **Pourquoi** : la raison de la liste des crans de N1.1 ; le cas de 3 000 membres est celui-ci + (cran 3, quorum 50). en : « and, beyond the quorum, the group asks for 75% of votes in favour + when it votes as usual » ; es : « y, más allá del quórum, el grupo pide un 75 % de votos a favor + cuando vota como de costumbre ». + +### F1.1 — relu, laissé + +Aucun chiffre daté, aucune phrase fausse : pas d'amendement. « B : la base ; à la puissance W, elle +est si petite qu'elle ne compte pas » est vrai sur toute toile de 100 à 10 000 000 membres : le +seuil du moteur y est celui sans B, même quand la part tombe juste sur un entier (`threshold::settle`, +`clamp_ceil` : M × W quand tout le monde vote, s × la participation habituelle, 60 · 67 · 75 · 83 · +90 « pour » à 100 votants), prouvé par `b_to_the_power_w_never_counts_in_the_web_of_trust`. Ce +n'est pas vrai d'un très petit groupe : dans le groupe d'essai, où W est le nombre de ses membres, +B^W compte (deux membres qui votent : deux « pour », jamais un seul). F1.1 dit que W est la toile +de confiance, et les textes ne décrivent pas le groupe d'essai (point 4) : laissé (point 10). + +### La garde, en production + +Dans les deux groupes, F1.2 et N1.1 sont à leur révision 4 (M5, puis M6b, puis M6c), F1.1 à sa +révision 3. Les amendements `2026-11-03-f1-2-chiffres` et `2026-11-03-n1-1-chiffres` suivent ceux de +T2 (`2026-11-03-f1-2-phrases`, `-n1-1-phrases`), qui sont **dépassés** : ils ne décident plus rien, +leurs décisions du 03/11 restent. La garde de révision admet la révision écrite par les amendements +suivis, dans leur ordre, le dernier étant celui de T2, et eux seuls : en production, M5, M6b et M6c +(révision 4) ; sur une base née le 02/11 avec les textes de M5, M6b et M6c (révision 3) ; née avec +ceux de M6b, M6c seul (révision 2) ; née avec ceux de M6c, la révision 1 ; le texte étant celui que +M6c a posé (empreinte). Le reste de la garde ne change pas : origine du seed, jamais entré en +vigueur ni confirmé ; aucune trace de vote (vote du serveur, prouvé ou simulé ; ligne de bulletin en +attente ou prouvée, quel qu'en soit le sort) ; aucune coquille, traduction de membre ni objection. + +| Article | Empreinte d'avant : le texte de T2, en ligne | Empreinte d'après : le texte de T3 | +|---|---|---| +| F1.2 | `96caf353b33805f2894593d72c5b70fce2e751039cd7f4651d8842c986729a14` | `ddce5aa5fcd13dfc3e374c37509a1bb8cfafddefb6e6add5a06b130b43216d62` | +| N1.1 | `7fbedcb8750c208f8790c4f87dc21d8bd1096a27a1a0044af6b9b8f2057f2aa8` | `4289e3579ee747f6023162339ed60601b810d33462617b42a9f8b5b3735c3238` | + +Appliqué, dans une transaction, par document : le texte (fr) remplacé, **révision 4 → 5** pour F1.2 +et N1.1 ; une ligne « texte de départ remplacé » dans l'historique, sans auteur (la révision 4, le +texte de T2, reste lisible, avec son diff) ; en et es remplacés ; la décision dans +`seed_amendments`. **La file de publication de Monnaie libre** reçoit deux lignes de plus +(« proposition de départ réécrite : F1.2, la part du cran par défaut au-delà du quorum » et « … : +N1.1, sans chiffre daté ; sous le quorum, autant de voix pour que si le quorum avait voté ») ; elles +attendent, comme les autres, le premier texte en vigueur (ADR 0011) puis des votes prouvés (ADR +0012) : rien n'est publié tant qu'aucun article n'est en vigueur. Le groupe d'essai ne publie +jamais. Refusé, le texte de T2 reste, avec sa raison (`votes`, `ballots`, `corrected`, +`translation_edited`, `signalled`, `revised`, `text_differs`…), consignée une fois pour toutes. + +Testé (`crates/ld-server/tests/seed_quorum_texts.rs`) sur une base faite comme la production après +M6c — née avec les textes du 21/10, démarrée par le seed de M5, puis de M6, de M6b et de M6c (les +textes de T2 gardés dans `tests/fixtures/seed-quorum-t2.json`) : appliqué une fois dans les deux +groupes, quatre révisions et deux lignes de file de plus, F1.1 et tout le reste intacts ; au second +démarrage, rien n'est rejoué. Refusé avec sa raison là où un vote simulé est compté (`votes`), là où +un bulletin attend (`ballots`), là où un vote direct tient dans le groupe d'essai (`votes`) ; le +refus tient même une fois le vote parti. Appliqué aussi sur une base née le 02/11 avec les textes de +M5, et sur une base née avec ceux de T2. Les deux limites de T2 restent (le commentaire de +`seed_amend.rs`) : un vote direct retiré dans le groupe d'essai, ou un vote simulé que l'acte +« supprime » aurait effacé avant la mise en ligne, ne laissent pas de trace. + +En production d'après l'ordre T3 : 0 vote, 0 bulletin (à revoir à la répétition). ## Lot T2 : trois phrases reprises (après la relecture de M6b) @@ -79,6 +218,8 @@ anciens mots ont disparu des trois langues (`the_three_phrases_of_the_review_of_ 12, 39 sur 50, 43 sur 50, 46 sur 50**, du cran 1 au cran 5 (le moteur, dans `the_three_phrases_of_the_review_of_m6b_are_written_true`). « Au-delà du quorum » est ajouté à la troisième phrase : sous le quorum, la part ne baisse pas, elle reste celle du quorum. +- *Reprise par le lot T3* : « qui n'est pas un nombre minimal de votants » est vrai aux quorums des + crans, faux pour un quorum d'auteur bas (au quorum 2, il faut 2 « pour »). ### L'interface, dans le même sens @@ -147,10 +288,10 @@ En production aujourd'hui : 0 vote, 0 bulletin, une seule identité (dit par orc le départ d'après l'ADR 0011 : aucun article en vigueur, Raison d'être, Unicité du compte et Réciprocité en Propositions seed au cran 2, le titre du document au cran 4. - **Les titres** sont gardés tous les trois : ils restent vrais. -- **Les nombres.** Chaque nombre de ces textes, dans les trois langues, est calculé par le moteur ou - lu dans le seed par un test (`crates/ld-server/tests/seed_quorum_texts.rs`, +- **Les nombres.** Chaque nombre de ces textes, dans les trois langues, est un paramètre lu dans le + seed ou une constante du moteur, vérifiés par un test (`crates/ld-server/tests/seed_quorum_texts.rs`, `every_number_of_the_quorum_texts_is_the_engine_s`) : un chiffre que rien ne justifie fait échouer - le test. Les trois formules écrites dans les textes sont évaluées telles quelles et comparées au + le test ; depuis le lot T3, aucun ne dépend de W ni d'une participation. Les trois formules écrites dans les textes sont évaluées telles quelles et comparées au moteur sur une grille de cas (`the_formulas_of_the_texts_are_those_the_engine_computes`). - **La forme**, vérifiée par le test `the_quorum_texts_follow_the_rules_of_form` et par `render()` (`web/src/lib/markdown.ts`) : sous-ensemble de Markdown, ni tableau ni intertitre ; une ligne vide @@ -158,7 +299,7 @@ En production aujourd'hui : 0 vote, 0 bulletin, une seule identité (dit par orc WoT », « Paramètres par défaut », « Niveaux d'inertie »), jamais par le code ; apostrophes droites ; « » espacés en français, “ ” en anglais britannique (*favour*), «» serrés en espagnol ; formules en code, avec × et −, jamais d'astérisque. -- **Longueur** (signes, après le lot T2 ; avant : fr 1081 · 2055 · 2372, en 1058 · 1977 · 2246, es 1083 · 2035 · 2454) : fr : F1.1 1122 · F1.2 2035 · N1.1 2449 ; en : F1.1 1093 · F1.2 1950 · N1.1 2315 ; es : F1.1 1126 · F1.2 2020 · N1.1 2526. Limite d'une proposition : 4 000 (`crates/ld-server/src/proposals.rs:32`) ; +- **Longueur** (signes, après le lot T3 ; après le lot T2 : fr 1122 · 2035 · 2449, en 1093 · 1950 · 2315, es 1126 · 2020 · 2526 ; avant : fr 1081 · 2055 · 2372, en 1058 · 1977 · 2246, es 1083 · 2035 · 2454) : fr : F1.1 1122 · F1.2 2055 · N1.1 2075 ; en : F1.1 1093 · F1.2 1970 · N1.1 1937 ; es : F1.1 1126 · F1.2 2042 · N1.1 2125. Limite d'une proposition : 4 000 (`crates/ld-server/src/proposals.rs:32`) ; au-delà de 700, l'assistant dit « texte long » (`crates/ld-engine/src/assist.rs:25`), comme pour les textes actuels de F1.2 et N1.1. - **Le ton** est celui de F1.2, N1 et O1 tels que M5 les a posés : le groupe, la personne, jamais le @@ -308,7 +449,7 @@ Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la fa **Par défaut** -- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude. Tout nouvel article commence à ce cran. +- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » quand il vote comme d'habitude. Tout nouvel article commence à ce cran. - Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, tous les autres articles au cran 3. ``` @@ -335,7 +476,7 @@ The values of the letters of the formula (see “WoT threshold formula”) and h **By default** -- The default level is 3 (medium): M = 55%, a quorum of 50 in Monnaie libre, and the group asks for 75% of votes in favour when it votes as usual. Every new article starts at this level. +- The default level is 3 (medium): M = 55%, 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. Every new article starts at this level. - At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, all other articles at level 3. ``` @@ -362,7 +503,7 @@ Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y có **Por defecto** -- El nivel por defecto es el 3 (media): M = 55 %, un quórum de 50 en Monnaie libre, y el grupo pide un 75 % de votos a favor cuando vota como de costumbre. Todo artículo nuevo empieza en este nivel. +- El nivel por defecto es el 3 (media): M = 55 %, 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. Todo artículo nuevo empieza en este nivel. - Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3. ``` @@ -372,18 +513,18 @@ Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y có **Ce qui est faux** : le quorum remplace C ; la part se calcule sur T', les votants ou le quorum s'il en manque ; au cran 2 sont les trois Propositions seed et l'ordre de chaque section, au cran 4 le titre seul ; la référence est deux fois le quorum de référence du document (50 dans Monnaie libre), jamais le quorum d'un article ; avec le quorum, l'exemple donne 72, 81, 90, 100 ou 108 voix pour ; et dans la formule de G, p̃ est une part de l'électorat, pas un nombre de votants. -**Titre** gardé : il reste vrai. Chaque cran dit maintenant son quorum ; l'exemple dit aussi deux cas sous le quorum. +**Titre** gardé : il reste vrai. Chaque cran dit maintenant son quorum ; l'exemple disait aussi deux cas sous le quorum. *(Retiré par le lot T3 : aucun chiffre daté dans un texte voté.)* ### N1.1 · Français (fait foi) Titre : **Niveaux d'inertie** ```markdown -À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). 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 ». 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, plus elle baisse, jusqu'à la majorité M quand tout le monde vote. +À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). Chaque cran a aussi son quorum : sous le quorum, les votants qui manquent comptent comme « non », et il faut autant de voix pour que si le quorum avait voté. 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, plus elle baisse, jusqu'à la majorité M quand tout le monde vote. `Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum s'il en manque. -**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, et ce qui est à ce cran au départ. +**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, et ce qui est à ce cran au départ. - **Cran 1 · très basse** : quorum 12 ; 60 % comme d'habitude, 50 % quand tout le monde vote. Aucun article au départ. - **Cran 2 · basse** : quorum 12 ; 67 % comme d'habitude, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section. @@ -394,8 +535,6 @@ Titre : **Niveaux d'inertie** **La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. 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 ne dépend pas du quorum d'un article. Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour. **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. - -Exemple : avec 7 224 membres et une participation habituelle de 120 votants, quand 120 personnes votent, il faut 72, 81, 90, 100 ou 108 voix pour, du cran 1 au cran 5 ; au cran 3, 75 % des 120 votants, soit 90. Sous le quorum, les absents comptent comme « non » : au cran 2 (quorum 12), 9 voix pour passent avec 3 « contre » au plus ; au cran 3 (quorum 50), il faut 39 voix pour, avec 11 « contre » au plus. ``` ### N1.1 · English @@ -403,11 +542,11 @@ Exemple : avec 7 224 membres et une participation habituelle de 120 votants, qua Titre : **Inertia levels** ```markdown -At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). 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. 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 lower it gets, down to the majority M when everyone votes. +At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). 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. 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 lower it gets, down to the majority M when everyone votes. `Share asked = M + (1 − M) × (1 − (T'/W)^G)`, where T' is the number of voters, or the quorum if some are missing. -**The five levels**: the quorum in Monnaie libre, the share asked when the group votes as usual, then when everyone votes, and what is at that level at the start. +**The five levels**: the quorum in Monnaie libre, the share asked beyond the quorum, when the group votes as usual, then when everyone votes, and what is at that level at the start. - **Level 1 · very low**: quorum 12; 60% as usual, 50% when everyone votes. No article at the start. - **Level 2 · low**: quorum 12; 67% as usual, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section. @@ -418,8 +557,6 @@ At each level, the group asks for a share of votes in favour among the voters (s **The usual turnout** is the median number of voters on the group's proposals voted on over the past year. Until the group has voted on 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It stays between 1% and 50% of the electorate, and is measured once a day. **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. - -Example: with 7,224 members and a usual turnout of 120 voters, when 120 people vote, it takes 72, 81, 90, 100 or 108 votes in favour, from level 1 to level 5; at level 3, 75% of the 120 voters, i.e. 90. Below the quorum, those missing count as votes against: at level 2 (quorum 12), 9 votes in favour pass with 3 votes against at most; at level 3 (quorum 50), it takes 39 votes in favour, with 11 votes against at most. ``` ### N1.1 · Español @@ -427,11 +564,11 @@ Example: with 7,224 members and a usual turnout of 120 voters, when 120 people v Titre : **Niveles de inercia** ```markdown -En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). 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». 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, más baja, hasta la mayoría M cuando vota todo el mundo. +En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). 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. 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, más baja, hasta la mayoría M cuando vota todo el mundo. `Proporción exigida = M + (1 − M) × (1 − (T'/W)^G)`, donde T' es el número de votantes, o el quórum si faltan. -**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, y lo que está en ese nivel al inicio. +**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, y lo que está en ese nivel al inicio. - **Nivel 1 · muy baja**: quórum 12; 60 % como de costumbre, 50 % cuando vota todo el mundo. Ningún artículo al inicio. - **Nivel 2 · baja**: quórum 12; 67 % como de costumbre, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección. @@ -442,8 +579,6 @@ En cada nivel, el grupo pide una proporción de votos a favor entre los votantes **La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Hasta que el grupo haya votado 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se mantiene entre el 1 % y el 50 % del electorado, y se mide una vez al día. **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. - -Ejemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 72, 81, 90, 100 o 108 votos a favor, del nivel 1 al nivel 5; en el nivel 3, el 75 % de los 120 votantes, es decir 90. Por debajo del quórum, los que faltan cuentan como «no»: en el nivel 2 (quórum 12), 9 votos a favor pasan con 3 en contra como mucho; en el nivel 3 (quórum 50), hacen falta 39 votos a favor, con 11 en contra como mucho. ``` ## Les autres articles, cherchés et laissés @@ -466,15 +601,18 @@ inertie, le cran de départ de Raison d'être, Unicité du compte et Réciprocit 1. **Ton intention de remonter l'inertie** une fois les textes passés : elle n'est pas écrite (ce n'est pas une règle). Si tu veux que F1.2 l'annonce sans la promettre, une phrase vraie suffit : « Leur cran se change ensuite par un vote, comme celui de tout article (voir « Réglage de l'inertie »). » + *(Tranché le 03/11 : le défaut reste, rien n'est écrit.)* 2. **« Propositions seed »** dans un texte voté : c'est le mot de l'interface (« Proposition seed », « Seed proposal », « Propuesta seed »). Autre choix : « propositions mises en avant ». + *(Tranché le 03/11 : le défaut reste, « Propositions seed ».)* 3. **L'exemple de N1.1** (7 224 membres, 120 votants) reste celui de M5 et des tests (le vote forgeron de 2024) : une illustration, pas la toile d'aujourd'hui (5 707 membres en production le 02/11). À ce W et à la participation de référence, les deux exemples sous le quorum donnent les mêmes nombres de « pour » : 9 au cran 2, 39 au cran 3 (`crates/ld-engine/src/inertia.rs`, `the_control_figures_of_the_quorum`). À retirer si tu ne veux pas de chiffre daté dans un texte voté. + **Tranché le 03/11 : retiré** (« pas de chiffre datés dans un texte voté » ; lot T3). 4. **Le groupe d'essai** (quorum 1 partout) n'est pas nommé : les textes donnent les quorums « dans - Monnaie libre », comme F1.2 donnait C = 50. + Monnaie libre », comme F1.2 donnait C = 50. *(Tranché le 03/11 : le défaut reste.)* 5. **F1.1 a changé de plan** : le sens d'abord, la formule ensuite, les lettres en liste (la liste d'aujourd'hui ne s'affiche pas comme liste, faute de ligne vide). B y est « la base » (W l'exposant : « exposant de base » était faux), M n'y est plus `majority_pct / 100`, et « Seuil » est traduit en @@ -491,9 +629,18 @@ inertie, le cran de départ de Raison d'être, Unicité du compte et Réciprocit n'est compté sur l'article » (défaut d'orch) se lit mieux mais se trompe pour un vote retiré sur la chaîne (plus compté, le quorum reste figé). Les trois phrases de l'interface sur le quorum de l'auteur (« avant le premier vote », « tant que personne n'a voté ») gardent ta - formule : à aligner si tu le veux. + formule : à aligner si tu le veux. *(Tranché le 03/11 : le défaut reste, « garder la trace » + dans F1.2, ta formule dans l'interface.)* 9. **Lot T2, F1.1 et N1.1** : sans chiffre, pour ne pas dater le texte voté ; les chiffres de la production (64,4 à 91,9 % ; 9 « pour » sur 12) sont ici, calculés par le moteur. +10. **Lot T3, F1.1 et le groupe d'essai** (pour information) : « B […] ne compte pas » est vrai dans + la toile, faux dans un groupe de quelques membres, où B^W demande une voix de plus quand la part + tombe juste sur un entier (deux membres qui votent : deux « pour »). Les textes disent que W est + la toile de confiance et ne décrivent pas le groupe d'essai (point 4) : laissé. Si tu veux qu'ils + le décrivent aussi, F1.1 dirait « dans la toile de confiance », comme F1.2. +11. **Lot T3, l'interface** (hors de ce lot) : `settings.reading` dit encore « le groupe demande sa + part de « pour » quand il vote comme d'habitude », sans « au-delà du quorum » pour cette part ; + même imprécision que la phrase de F1.2 et N1.1 reprise ici (fr, en, es). ## Les chiffres et leur source @@ -526,13 +673,17 @@ Chaque nombre est vérifié par `every_number_of_the_quorum_texts_is_the_engine_ | Propositions seed au cran 2 | F1.2, N1.1 | `seed::SEED_PROPOSAL_LEVEL` ; les versions `seedProposal` du seed, leurs titres dans chaque langue | | Le titre au cran 4, l'ordre de chaque section au cran 2 | F1.2, N1.1 | le seed, `modules.title.inertia`, `modules.order.inertia` | | Tous les autres articles au cran 3 ; rien aux crans 1 et 5 | F1.2, N1.1 | le seed : chaque `inertia` (toutes égales, vérifié) | -| 5 propositions ; 100 votants (deux fois le quorum de référence) | N1.1 | `Participation::MIN_SAMPLE` ; `Participation::of(W, voteFloor, None, 0).voters(W)`, `voteFloor` = 50 | +| 5 propositions ; 100 votants (deux fois le quorum de référence) | N1.1 | `Participation::MIN_SAMPLE` ; `Participation::reference_ratio(W, voteFloor) × W` avant les bornes, `voteFloor` = 50, le même sur toute toile | | Entre 1 % et 50 % | N1.1 | `Participation::LOWEST`, `HIGHEST` | | Depuis un an ; une fois par jour | N1.1 | `participation::WINDOW_DAYS` = 365, `EVERY_HOURS` = 24 | -| 72, 81, 90, 100, 108 voix pour à 120 votants | N1.1 | `Scale::required(120, k, Q_k)`, W 7 224, médiane 120 ; déjà `inertia.rs`, `the_values_shown_to_yvv` | -| 75 % des 120 votants, soit 90 | N1.1 | la part du cran 3 × 120, égale au seuil du moteur (vérifié) | -| Cran 2 (quorum 12) : 9 « pour », 3 « contre » au plus | N1.1 | `threshold::yes_needed(0)`, puis `no_needed(9) − 1`, W 7 224, médiane 120 | -| Cran 3 (quorum 50) : 39 « pour », 11 « contre » au plus | N1.1 | idem ; les deux exemples restent sous le quorum (vérifié) | + +**Lot T3** : les quatre lignes de l'exemple de N1.1 sont ôtées de ce tableau (72, 81, 90, 100 et +108 voix pour à 120 votants ; 75 % des 120 votants, soit 90 ; au cran 2, 9 « pour » et 3 « contre » +au plus ; au cran 3, 39 et 11) : l'exemple est sorti du texte (point 3), parce que ses chiffres +dépendaient de la toile et d'une participation (7 224 membres, 120 votants). Ils restent des +chiffres de contrôle du moteur (`inertia.rs`, `the_values_shown_to_yvv`, +`the_control_figures_of_the_quorum`), pas des textes. Tous les nombres qui restent sont des +paramètres : le test les retrouve identiques sur toute toile de 100 à 10 000 000 membres. Les règles sans chiffre : @@ -543,6 +694,9 @@ Les règles sans chiffre : | Une version passe quand ses « pour » atteignent le seuil arrondi au-dessus | F1.1 | `threshold::passes`, `clamp_ceil` | | La participation habituelle ne dépend pas du quorum d'un article | N1.1 | `inertia::Participation::reference_ratio` (le quorum de référence du document) ; ADR 0013 | | Au départ, aucun article en vigueur | F1.2 | ADR 0011 ; le seed (aucune version `in_force` hors modules) | +| Sous le quorum, autant de voix pour que si le quorum avait voté (lot T3) | N1.1 | `threshold::judged_voters` ; `yes_needed(0)` égale `wot_threshold(W, Q)` ; `under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted` | +| La part du cran comme d'habitude, au-delà du quorum (lot T3) | F1.2, N1.1 | `inertia::gradient` ; `the_share_of_a_level_as_usual_is_asked_beyond_the_quorum` | +| B^W ne compte pas dans la toile (W de 100 à 10 000 000) | F1.1, F1.2 | `wot::PLAUSIBLE` ; `b_to_the_power_w_never_counts_in_the_web_of_trust` | ## Les textes dans le seed From b2d653ea4f63466bbc49dd6c4ba2f45f354673fb Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 17:32:09 +0100 Subject: [PATCH 4/5] =?UTF-8?q?Lot=20T3=20:=20textes-quorum-seed.md,=20l'e?= =?UTF-8?q?n-t=C3=AAte=20compte=20juste=20(trois=20phrases,=20deux=20de=20?= =?UTF-8?q?N1.1=20et=20une=20de=20F1.2,=20et=20l'exemple)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/chantiers/acte-certification/textes-quorum-seed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/chantiers/acte-certification/textes-quorum-seed.md b/docs/chantiers/acte-certification/textes-quorum-seed.md index 2ca39de..5787161 100644 --- a/docs/chantiers/acte-certification/textes-quorum-seed.md +++ b/docs/chantiers/acte-certification/textes-quorum-seed.md @@ -14,7 +14,7 @@ > (« Lot T2 »). *(En ligne depuis M6c, le 03/11 à 11 h 05.)* > > **Mis à jour le 2026-11-03 par le lot T3** (branche `lot/t3-chiffres`), après ta validation du -> jour : N1.1 perd son exemple chiffré, et quatre de ses phrases et de celles de F1.2 sont écrites +> jour : N1.1 perd son exemple chiffré, et trois phrases, deux de N1.1 et une de F1.2, sont écrites > vraies pour tout W et tout quorum (« Lot T3 », ci-dessous). Les blocs de texte de ce fichier sont > ceux du seed d'aujourd'hui. Rien du lot T3 n'est en ligne. From 7147cb4735d34e7c44b00329ed9ad1d346598e01 Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 17:42:44 +0100 Subject: [PATCH 5/5] =?UTF-8?q?Lot=20T3=20:=20boucle=20compl=C3=A8te=20ver?= =?UTF-8?q?te=20sur=20b2d653e=20(fmt,=20clippy,=20312=20tests=20Rust,=20ch?= =?UTF-8?q?eck=200=20/=200,=2078=20tests=20unitaires,=20build,=2042=20e2e?= =?UTF-8?q?=20au=20second=20passage,=20le=20premier=20coup=C3=A9=20par=20u?= =?UTF-8?q?n=20changement=20de=20r=C3=A9seau=20du=20poste)=20;=20ligne=20a?= =?UTF-8?q?u=20journal=20de=20CONTROLES.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/chantiers/acte-certification/CONTROLES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/chantiers/acte-certification/CONTROLES.md b/docs/chantiers/acte-certification/CONTROLES.md index 555ca13..de30238 100644 --- a/docs/chantiers/acte-certification/CONTROLES.md +++ b/docs/chantiers/acte-certification/CONTROLES.md @@ -96,3 +96,4 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa | 2026-11-03 | automate | local, lot U sur `6fb8238` (le lecteur refait : l'ordre en une ligne au bout de sa section, poignées sur place et vote ; oui pour passer · non pour bloquer ; quorum de l'auteur ; panneau sans onglets ; C3, C4, C6, C19 à C21, C23 à C26, C34, M1 repris) | C1–C37, M1 | 38 / 38 verts (40 tests) | | 2026-11-03 | automate | local, lot T2 sur `f27ee08` (les trois phrases de F1.1, F1.2 et N1.1 ; l'espace perdu du vote déplié ; C34 lit la ligne entière, C37 « ne change pas ») | C1–C37, M1 | 38 / 38 verts (40 tests), du premier coup | | 2026-11-03 | automate | local, lot U2 sur `bb4e7b7` (les mineurs de la relecture de M7 : cibles de 44 px et trois lectures entières sur téléphone, M2 ; « Proposer cet ordre » dit pourquoi, C35 ; une information à un seul endroit, C19, C36 ; l'en-tête et les changements d'inertie d'un module, C38 ; C25 et C26 lisent l'inertie dans « Inertie ») | C1–C38, M1, M2 | 40 / 40 verts (42 tests), du premier coup | +| 2026-11-03 | automate | local, lot T3 sur `b2d653e` (N1.1 et F1.2 sans chiffre daté, chaque phrase vraie pour tout W et tout quorum ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests) au second passage ; le premier, 40 / 42 : C33 et C38 tombés sur `net::ERR_NETWORK_CHANGED` (le réseau du poste a changé pendant le passage, vu dans leurs traces) |