// Lot U3 (Yvv, 2026-11-03): the rules of operation leave the votes (ADR 0014) and the screen says // them where they serve. Each phrase the screen says of a rule is pinned here, letter for letter, // with the test of the engine that proves it — the closed list of lot T3c // (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'; import { fr } from '../../src/lib/i18n/fr'; const CATALOGS = { fr, en, es }; describe('Yvv’s words, word for word', () => { it('the chapter is « Info ouverture doc de réf. », and its paragraph replaces « Commence par les Propositions seed »', () => { expect(fr['opening.title']).toBe('Info ouverture doc de réf.'); expect(`${fr['opening.none']} ${fr['opening.amorce']}`).toBe( "Aucun article n'est encore en vigueur. Les Propositions d'amorce y entrent par le vote, comme les propositions qui suivent, avec moins d'inertie pour que le document prenne vie plus facilement. Il est recommandé d'augmenter ensuite l'inertie, par le vote, pour diminuer la fréquence des changements.", ); expect(Object.keys(fr)).not.toContain('doc.empty.seeds'); }); it('once an article is in force, the first sentence goes, and « y » says what it stood for', () => { expect(fr['opening.amorce.after']).toBe(fr['opening.amorce'].replace("y entrent par le vote", 'entrent en vigueur par le vote')); }); it('the word for whoever is not invited yet (« @ chiara » read « à Chiara »), and its translations', () => { expect(fr['signin.uninvited']).toBe( "Pour rentrer dans un groupe de décisions, il faut pour l'instant demander un lien d'invitation à Chiara ou à Yvv. Ce prototype n'est pas encore câblé sur la chaîne.", ); expect(en['signin.uninvited']).toBe( 'To join a decision group, for now ask Chiara or Yvv for an invitation link. This prototype is not yet wired to the chain.', ); expect(es['signin.uninvited']).toBe( 'Para entrar en un grupo de decisiones, por ahora hay que pedir un enlace de invitación a Chiara o a Yvv. Este prototipo aún no está conectado a la cadena.', ); }); it('said once: the Ğ1 line no longer says how to get in meanwhile', () => { expect(fr['signin.g1.pending']).toBe("La connexion Ğ1 arrive : l'inscription de libreDecision auprès de connect.monnaie-libre.fr est en cours."); for (const cat of Object.values(CATALOGS)) expect(cat['signin.g1.pending']).not.toMatch(/invitation|invitación/); }); }); describe('« Proposition d’amorce » (fr), « Starter proposal » (en), « Propuesta de arranque » (es)', () => { it('names the seed proposals everywhere', () => { expect([fr['doc.seedProposal'], fr['docs.seeds.one'], fr['docs.seeds.other']]).toEqual(["Proposition d'amorce", "{n} Proposition d'amorce", "{n} Propositions d'amorce"]); expect([en['doc.seedProposal'], en['docs.seeds.one'], en['docs.seeds.other']]).toEqual(['Starter proposal', '{n} starter proposal', '{n} starter proposals']); expect([es['doc.seedProposal'], es['docs.seeds.one'], es['docs.seeds.other']]).toEqual(['Propuesta de arranque', '{n} propuesta de arranque', '{n} propuestas de arranque']); }); it('never « seed » next to « proposition », « proposal », « propuesta »', () => { for (const [lang, cat] of Object.entries(CATALOGS)) { for (const [k, v] of Object.entries(cat)) { expect(v, `${lang} ${k}`).not.toMatch(/propositions? seed|seed proposals?|propuestas? (seed|semilla)/i); } } }); }); describe('the rules the screen says, true for every W and every quorum (lots T3 to T3c)', () => { it('the defined term « participation habituelle », never « comme d’habitude »', () => { const asUsual = { fr: /comme d'habitude/, en: /as usual/, es: /como de costumbre/ }; for (const [lang, cat] of Object.entries(CATALOGS)) { for (const [k, v] of Object.entries(cat)) expect(v, `${lang} ${k}`).not.toMatch(asUsual[lang as keyof typeof asUsual]); } }); // 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); 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 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_…'], // ld-engine threshold::a_whole_number_of_votes_never_gains_one_by_rounding (« A genuine fraction still // rounds up »: F1.1 « arrondi à l'entier au-dessus »). ['formula.round', "Une version atteint son seuil quand ses voix pour atteignent R, arrondi à l'entier au-dessus.", 'ld-engine threshold'], // g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum (F1.2 18). ['formula.G', 'G — gradient : {n}, déduit de la participation habituelle du groupe', 'g_gives_…'], // b_to_the_power_w_never_counts_in_the_web_of_trust (F1.2 16), said only when W is the web of trust. ['formula.B.wot', "À la puissance W, B est si petit qu'il ne compte pas dans la toile de confiance.", 'b_to_the_power_w_…'], // the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum (F1.2 20, N1.1 5). ['settings.share.note', "Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle.", 'the_share_of_a_level_…'], // the_share_asked_falls_… (N1.1 3), under_the_quorum_… (N1.1 2). [ 'settings.reading', "Au-delà du quorum de l'article, plus il y a de votants, plus la part exigée baisse, jusqu'à la majorité M quand tout le monde vote. Sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté.", 'the_share_asked_falls_… · under_the_quorum_…', ], // the_participation_habituelle_is_the_median_brought_back_within_its_bounds; tests/inertia.rs // the_usual_participation_is_the_median_of_five_voted_proposals (N1.1 12: the year counts). [ 'settings.usual.reference.other', "{n} votants sur {electorate} ({pct}), deux fois {q}, le quorum de référence : la participation de référence, tant qu'elle repose sur moins de {min} propositions votées depuis un an ({sample} pour l'instant).", 'the_participation_habituelle_… · inertia.rs', ], // tests/quorum.rs in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pending (N1.1 13). ['settings.usual.quorum', "Elle ne dépend pas du quorum d'un article.", 'quorum.rs in_monnaie_libre_…'], // g_gives_… (N1.1 15). ['settings.gradient', "G se calcule pour que la part exigée, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle.", 'g_gives_…'], // tests/quorum.rs the_author_of_an_article_sets_its_quorum_before_the_first_vote; tests/ballots.rs // a_vote_exists_for_every_state_of_the_buffer (F1.2 14, lot T2: « la trace d'aucun vote »). ['quorum.explain', "Tu fixes le quorum de cet article tant qu'il ne garde la trace d'aucun vote. En dessous, les votants qui manquent comptent comme « non ».", 'quorum.rs · ballots.rs'], [ 'opening.quorum.author', "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.", 'quorum.rs · ballots.rs', ], // under_the_quorum_… (F1.2 12), each_level_has_its_quorum_and_every_evaluation_says_it (F1.2 13). ['opening.quorum.rule', "Sous le quorum d'un article, les votants qui manquent comptent comme « non ».", 'under_the_quorum_… · quorum.rs'], // ld-engine compose::a_rival_must_beat_the_version_in_force (F1.2 6); tests/api.rs // a_rival_replaces_the_seed_and_the_seed_returns_among_proposals (F1.2 7). [ 'vote.rival', 'Un texte est déjà en vigueur : pour le remplacer, cette version doit en plus atteindre 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.', 'compose · api.rs', ], // ld-engine compose::rivals_on_one_unit_yield_one_winner (F1.2 8). ['vote.others.rule', 'Si plusieurs versions atteignent leur seuil, celle qui a la marge la plus large entre en vigueur.', 'compose'], // ld-engine ballot::the_last_one_stands_in_the_order_of_the_chain; tests/ballots.rs the same (F1.2 3, 4). ['vote.permanent', 'Le vote reste ouvert, sans échéance : tu peux changer ou retirer ton vote à tout moment, le dernier fait foi.', 'ballot · ballots.rs'], // tests/modules.rs an_order_is_proposed_with_the_articles_of_its_section_then_voted_into_force (O1 4: // « Nothing else moved: the other sections read as before »). ['order.edit.effect', "S'il entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change.", 'modules.rs'], ]; it.each(PROVEN)('%s', (key, text) => { expect(fr[key]).toBe(text); for (const cat of [en, es]) expect(cat[key], key).toBeTruthy(); }); it('the defined terms are carried in English and in Spanish too', () => { for (const k of ['settings.share.note', 'settings.gradient', 'formula.G'] as const) { expect(en[k], k).toMatch(/usual turnout/); expect(es[k], k).toMatch(/participación habitual/); } for (const k of ['settings.share.note', 'settings.reading', 'settings.gradient', 'formula.reading', 'inertia.explain'] as const) { expect(en[k], k).toMatch(/article's quorum/); expect(es[k], k).toMatch(/quórum (Q )?del artículo/); } }); }); // 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]); } }); });