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