Fusion du lot U3 (demande d'Yvv du 03/11) : « Info ouverture doc de réf. » avant le texte, hors vote (le paragraphe d'Yvv, les crans d'ouverture et le quorum de chaque cran lus dans l'API) ; une séparation avant le texte ; plus les titres « Introduction » et « Engagements fondamentaux » ; les informations de formule, inertie et ordonnancement là où elles servent (inventaire U3-informations.md) ; « Proposition d'amorce » ; le mot d'Yvv pour qui n'est pas invité ; réordonner sur plusieurs lignes corrigé ; API : seedFeatured

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 22:40:04 +01:00
co-authored by Claude Opus 5.5
28 changed files with 1225 additions and 94 deletions
+4 -3
View File
@@ -93,11 +93,12 @@ web/src/
# ChainSign (signature, Ğecko), VersionDetails, ProposeView, UnitBlock, DeepList,
# OrderLine / Reorder (l'ordre d'une section : une ligne en fin de section, les poignées
# sur ses articles, sur place), QuorumSetting (le quorum de l'auteur), Crans (l'inertie dessinée),
# TitleBar / TitlePropose (le titre du document), Seal…
# TitleBar / TitlePropose (le titre du document), OpeningInfo (« Info ouverture doc de réf. », lot U3), Seal…
lib/wallet/ # injected.ts (portefeuille injectedWeb3, polkadot-api chargé à la signature) · gecko.ts
lib/basket.ts # PUR : sections, tout pour / tout contre, versions en concurrence, versions retenues, modules
lib/objections.ts # où l'auteur répond à une objection · lib/diff.ts : le diff resserré d'une coquille
lib/order.ts # PUR : placer, monter / descendre, ce qui a bougé
lib/opening.ts # PUR : le chapitre d'ouverture, lu dans l'API (cran d'ouverture, Propositions d'amorce, quorums)
routes/ # / (accueil + choix du groupe) · connexion · invitation · equipe · g/[group] (piliers, en cours) ·
# g/[group]/docs · g/[group]/docs/[doc] (lecteur,
# vote déplié ?vote=, panneau sans onglets ?onglet=panier|proposer|details|reglages ;
@@ -105,7 +106,7 @@ web/src/
# · g/[group]/decisions · g/[group]/mandats ·
# g/[group]/protocoles (à venir) · acces (trustinWallet) · messages (la boîte) ;
# /groupes, /decisions… redirigent
web/e2e/ # Playwright C1–C38 + M1, M2 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 et
web/e2e/ # Playwright C1–C43 + M1, M2 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 et
# chain-mock.mjs (nœud et indexeur Ğ1 simulés) ; faux portefeuille dans helpers.ts
docker/ # app.Dockerfile · app-compose.yml (sonic)
.woodpecker/ # app.yml · maquette.yml
@@ -127,7 +128,7 @@ maquette/ # v2 intacte (frontend Nuxt, backend FastAPI, docker, docs-v
```bash
export PATH=$HOME/.cargo/bin:$PATH
cargo fmt --all --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace
cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 40 contrôles (42 tests, C25 et C35 en deux)
cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 45 contrôles (47 tests, C25 et C35 en deux)
LD_DEV_LOGIN=1 cargo run -p ld-server # :8002, sert web/build
cd web && npm run dev # :3002 à chaud
+3
View File
@@ -815,6 +815,9 @@ fn proposal_json(
"revision": p.revision, "createdAt": p.created_at, "updatedAt": p.updated_at,
"submittedAt": p.submitted_at, "enteredAt": p.entered_at, "confirmedAt": p.confirmed_at,
"seedMinimal": p.seed_minimal(), "seedProposal": p.seed_proposal(),
// Put forward by the seed (ADR 0011), in force since or not: the chapter « Info ouverture
// doc de réf. » (lot U3) says the level the Propositions d'amorce opened at.
"seedFeatured": p.seed_featured,
"tally": st.shown_tally(&p.id),
"tallyPending": st.pending_tally(&p.id),
"tallySimulated": st.simulated_tally(&p.id),
+45
View File
@@ -727,3 +727,48 @@ async fn a_seed_proposal_enters_by_the_vote_in_the_test_group() {
.expect("adoption");
assert_eq!(electorate, 16);
}
/// Lot U3 (Yvv, 2026-11-03: « un petit chapitre qui explicite les réglages initiaux de l'auteur,
/// notamment Quorum et Inertie »): the chapter « Info ouverture doc de réf. » says at which level
/// the Propositions d'amorce opened, read in the data of the document. Once one has entered into
/// force it is no longer a seed proposal (`seedProposal`), but the view still says the seed put
/// it forward (`seedFeatured`): its level at the opening stays readable, and it is never counted
/// among the other articles.
#[tokio::test]
async fn the_view_keeps_which_versions_the_seed_put_forward_once_they_enter_into_force() {
let state = fresh().await;
let mut a = Client::of(&state);
a.login("Yvv", false).await;
for group in GROUPS {
let doc = a.doc(group).await;
let featured: Vec<&str> = doc["units"]
.as_array()
.expect("units")
.iter()
.filter(|u| {
std::iter::once(&u["inForce"])
.chain(u["proposals"].as_array().expect("p").iter())
.any(|v| v["seedFeatured"].as_bool() == Some(true))
})
.filter_map(|u| u["code"].as_str())
.collect();
assert_eq!(featured, ["I1", "E1", "E12", "E2"], "{group}");
// Every version says it, true or false; the modules were never put forward.
for v in versions(&doc) {
let seeded = v["seedFeatured"].as_bool().unwrap_or_else(|| panic!("{group}: {v}"));
assert_eq!(seeded, v["seedProposal"].as_bool() == Some(true), "{group}: {}", v["title"]);
}
}
// Into force in the test group, by one voice (quorum 1): no longer a seed proposal, still
// put forward by the seed, at the level it opened at.
let doc = a.doc("essai").await;
let i1 = unit(&doc, "I1")["proposals"][0]["id"].as_str().expect("i1").to_owned();
assert_eq!(a.vote(&i1, "for").await["entered"][0]["code"], "I1");
let doc = a.doc("essai").await;
let p = &unit(&doc, "I1")["inForce"];
assert_eq!(p["id"], i1.as_str());
assert_eq!((p["seedProposal"].as_bool(), p["seedFeatured"].as_bool()), (Some(false), Some(true)));
assert_eq!(unit(&doc, "I1")["inertia"], LOW);
assert!(unit(&doc, "I1")["inertiaChanges"].as_array().expect("changes").is_empty());
}
@@ -4,7 +4,7 @@ Chaque contrôle existe en deux versions : **automatique** (Playwright, `web/e2e
`npm run e2e`) et **humaine** (Chiara, sur la production ou en local). Une remarque de Chiara
sur un contrôle vaut échec du contrôle tant qu'elle n'est pas traitée.
Où tester : les contrôles qui écrivent (C5, C6, C7, C21, C22, C25, C26, C28–C31, C35, C37, C38, M2) se font dans le **groupe
Où tester : les contrôles qui écrivent (C5, C6, C7, C21, C22, C25, C26, C28–C31, C35, C37, C38, C43, M2) se font dans le **groupe
d'essai** (caché, quorum 1 à chaque cran — une voix seule y passe, ADR 0013 —, jamais publié) — jamais sur le texte de la communauté. Au départ, rien
n'y est en vigueur (ADR 0011) : les contrôles qui lisent un texte en vigueur (C4 à C6, C20, C21, C24,
C28 à C31) y font d'abord entrer Raison d'être, Unicité du compte et Réciprocité par le vote (une voix
@@ -15,9 +15,9 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
| # | Contrôle | Geste humain | Attendu |
|---|---|---|---|
| C1 | L'accueil, sans session | ouvrir `/` sans être connectée | la clé de voûte en grand, jamais inclinée ; sous l'arche, centrés et dans cet ordre : « boîte à outils », « contexte et cadrage », « processus et modalités » ; puis le titre centré sur deux lignes, « Rendre possible » / « la décision collective » ; « Se connecter avec sa clé Ğ1 » et sa note ; pas de piliers ni de groupes ; le menu à gauche avec les quatre outils, trois éteints |
| C1 | L'accueil, sans session | ouvrir `/` sans être connectée | la clé de voûte en grand, jamais inclinée ; sous l'arche, centrés et dans cet ordre : « boîte à outils », « contexte et cadrage », « processus et modalités » ; puis le titre centré sur deux lignes, « Rendre possible » / « la décision collective » ; tant que la connexion Ğ1 n'est pas active (comme en production), le mot d'Yvv pour qui n'est pas encore invité à la place de « Se connecter avec sa clé Ğ1 » (C41) ; pas de piliers ni de groupes ; le menu à gauche avec les quatre outils, trois éteints |
| C2 | L'accueil, avec session | se connecter : on arrive sur `/` | le titre, le leitmotiv, puis directement le choix du groupe : Monnaie libre en grand, Axiom, Librodrome, Groupe local Vallée Drôme Crestois ; **pas** l'Atelier du canal ; `/groupes` et « Mes groupes » (menu) ramènent ici |
| C3 | Le texte au départ : les Propositions seed | Monnaie libre → Docs de référence → Acte d'engagement Certification | aucun article en vigueur (ADR 0011) : « Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition. » ; puis, **en tête du texte**, en entier, Raison d'être (sous « Introduction »), Unicité du compte, Responsabilité civique et Réciprocité (sous « Engagements fondamentaux »), chacune marquée « Proposition seed » (au survol : « Texte de départ mis en avant pour lancer le vote… »), sur un lavis d'eau, avec « 0 pour · 0 contre · 9 oui sur 12 » et « basse » (au lecteur d'écran : « inertie 2 / 5 · basse ») ; chaque section lue finit par une ligne d'ordre (C25) ; en dessous, « Propositions en cours » **repliées par section** (5 lignes : le nom, « 11 articles », un petit niveau d'eau par article ; formule, inertie et ordonnancement sont sortis du document, ADR 0014) ; « Tout déplier » : 23 lignes, une par article (les quatre Propositions seed ne s'y répètent pas) |
| C3 | Le texte au départ : les Propositions d'amorce | Monnaie libre → Docs de référence → Acte d'engagement Certification | aucun article en vigueur (ADR 0011) : le chapitre « Info ouverture doc de réf. » le dit, avec les mots d'Yvv (« Aucun article n'est encore en vigueur. Les Propositions d'amorce y entrent par le vote… », C39) ; puis, **en tête du texte**, en entier, Raison d'être, Unicité du compte, Responsabilité civique et Réciprocité (sans les titres « Introduction » et « Engagements fondamentaux », lot U3), chacune marquée « Proposition d'amorce » (au survol : « Texte de départ mis en avant pour lancer le vote… »), sur un lavis d'eau, avec « 0 pour · 0 contre · 9 oui sur 12 » et « basse » (au lecteur d'écran : « inertie 2 / 5 · basse ») ; chaque section lue finit par une ligne d'ordre (C25) ; en dessous, « Propositions en cours » **repliées par section** (5 lignes : le nom, « 11 articles », un petit niveau d'eau par article ; formule, inertie et ordonnancement sont sortis du document, ADR 0014) ; « Tout déplier » : 23 lignes, une par article (les quatre Propositions d'amorce ne s'y répètent pas) |
| C4 | Gestes contextuels | groupe d'essai, où la Raison d'être est entrée en vigueur par le vote : la survoler → « Le vote » | deux gestes sous l'article, « Le vote » et « Proposer une modification » (ses autres versions se lisent dans le vote) ; le vote se **déplie sous l'article**, sans fenêtre : « N oui sur Q votants pour passer » et les voix qui manquent ; « La formule du seuil » se déplie : « M — majorité à pleine participation : 50 % — inertie 2 / 5 · basse », et le lien « Réglages de la formule » ; l'URL garde le vote déplié (`?vote=`) ; « Le vote » de nouveau le replie |
| C5 | Entrée en vigueur | groupe d'essai (Raison d'être, Unicité du compte et Réciprocité entrées en vigueur par le vote) : déplier le vote de « Les deux garanties réciproques », « Pour » ; une deuxième personne si cette voix ne suffit pas | à la voix qui suffit, « … est entré en vigueur » ; le texte en vigueur compte quatre articles, le vote reste déplié sous le nouveau texte (autant de « pour » que de voix, 0 contre) |
| C6 | Coquille sans vote | groupe d'essai, **en administratrice** (un texte du seed n'a pas d'auteur) : Unicité du compte → « Le vote » → « Coquilles, traductions, objections, historique » (le panneau « Détails », sans onglets) → « Corriger une coquille », remplacer les apostrophes droites par des apostrophes typographiques (« Je m’engage sur l’honneur ») | « Coquille corrigée, sans toucher aux votes. » ; un changement de sens est refusé : « Ce changement touche au sens : propose-le comme modification. » (sans être administratrice, la correction attend : C31) |
@@ -55,6 +55,11 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
| C36 | Combien de oui pour passer, combien de non pour bloquer (ADR 0013 ; Yvv, 02/11) | Monnaie libre : la Raison d'être (en tête) → « Le vote » ; « Tout déplier » ; puis, l'automate donnant à une proposition du groupe d'essai les totaux 9 pour · 4 contre (cran 2, quorum 12), puis 3 pour · 0 contre | avant tout vote, sous l'article : « 9 oui sur 12 votants pour passer », au survol « Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ». » ; la ligne de la Proposition seed, juste au-dessus, ne redit plus les totaux ni les oui (lot U2) ; dans la liste, en bref : « 39 oui sur 50 » (cran 3) ; à 9 pour · 4 contre : « 11 oui pour passer · 4 non pour bloquer » sous l'article, « passe à 11 oui · bloquée à 4 non » dans la liste, « Il manque 2 voix pour. » ; à 3 pour · 0 contre : « 9 oui pour passer · le quorum la bloque », « passe à 9 oui » |
| C37 | Le quorum d'un article, par son autrice, avant le premier vote (ADR 0013) | groupe d'essai : proposer un article, puis son vote → « Quorum » : 3 → « Fixer ce quorum » ; « Revenir au défaut (1) » ; 100000 → « Fixer ce quorum » ; une autre personne ouvre le même vote, puis vote « contre » ; l'autrice tente 2 | le réglage n'apparaît qu'à l'autrice (`canSetQuorum`) : « Quorum · 1 votant », « Par défaut, celui de son cran : 1 votant. », le champ « Quorum de l'article » ; « Quorum fixé : 3 votants. », la ligne dit « … oui sur 3 votants pour passer » ; « Quorum revenu au défaut : 1 votant. » ; hors bornes (400) : « Ce quorum sort des bornes : au moins 2 votants (1 dans le groupe d'essai), au plus … » ; l'autre personne ne voit pas le réglage ; après son vote (409) : « Quelqu'un a déjà voté sur cet article : son quorum ne change pas. » (lot T2), puis le réglage disparaît |
| C38 | Ce que compte l'en-tête ; un changement d'inertie d'un ordre ou du titre (lot U2) | Monnaie libre : lire la ligne de faits sous le résumé ; « Tout déplier », compter les versions de la liste ; puis, dans le groupe d'essai, proposer un changement d'inertie de l'ordre de « Conclusion » et du titre (l'automate passe par `POST /api/units/{id}/inertia`, puis les retire) | « 0 article en vigueur · 4 Propositions seed · N propositions en cours », N le nombre de versions que montre la liste : les Propositions seed lues en tête sont dites à part, les ordres et titres proposés ne sont pas comptés (ils se votent à la ligne de leur module) ; sous le titre, la pastille « 1 changement d'inertie proposé », qui déplie son vote sous le titre ; à la fin de « Conclusion », la même pastille sur la ligne d'ordre, qui déplie son vote là ; aucun « inertie vers … » dans « Propositions en cours », et l'en-tête compte toujours ce que la liste montre |
| C39 | L'ouverture du document : « Info ouverture doc de réf. », une séparation, puis le texte (lot U3 ; Yvv, 03/11) | Monnaie libre → Acte d'engagement Certification : lire le haut de la page, dans les quatre ambiances ; puis le groupe d'essai, où la Raison d'être, Unicité du compte et Réciprocité sont en vigueur | avant le texte, hors vote (aucun bouton), le chapitre « Info ouverture doc de réf. » : le paragraphe d'Yvv mot pour mot (« Aucun article n'est encore en vigueur. Les Propositions d'amorce y entrent par le vote, comme les propositions qui suivent… ») ; « Inertie à l'ouverture » : Propositions d'amorce 2 / 5 · basse, Titre du document 4 / 5 · haute, Ordre de chaque section 2 / 5 · basse, Autres articles 3 / 5 · moyenne ; « Quorum » : « 12 · 12 · 50 · 50 · 50 votants, du cran 1 au cran 5. », sous le quorum d'un article les votants qui manquent comptent « non », l'auteur de la première version peut en fixer un autre tant que l'article ne garde la trace d'aucun vote ; puis un filet doux de la couleur de l'ambiance (jamais une bordure), puis le premier article ; aucun titre « Introduction » ni « Engagements fondamentaux » dans le texte, la ligne « Ordre de la section » des engagements fondamentaux toujours là ; lu dans les données (l'automate change les quorums et le cran du titre que donne l'API : le chapitre suit) ; dans le groupe d'essai, la première phrase n'est plus dite (« Les Propositions d'amorce entrent en vigueur par le vote… »), elles restent au cran 2, « 1 votant à chaque cran. » |
| C40 | « Proposition d'amorce » (lot U3) | Monnaie libre, rien en vigueur : les quatre articles en tête, la ligne de faits ; puis EN, puis ES | « Proposition d'amorce » sur les quatre, « 4 Propositions d'amorce » dans la ligne de faits, jamais « Proposition seed » ; « Starter proposal », « 4 starter proposals » ; « Propuesta de arranque », « 4 propuestas de arranque » |
| C41 | Qui n'est pas encore invité (lot U3 ; Yvv, 03/11) | sans session : `/`, puis « Se connecter » ; puis en anglais et en espagnol | à l'accueil, à la place du bouton de connexion Ğ1 (pas encore active) : « 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. », puis « Tu as reçu un lien d'invitation ? Ouvre-le simplement : il te connecte. » ; sur « Se connecter », le même mot une seule fois, et la ligne Ğ1 ne dit plus « En attendant, on entre avec un lien d'invitation » ; en, es : le même mot traduit |
| C42 | Une poignée tirée sur plusieurs lignes (lot U3, défaut vu par T4) | groupe d'essai : « Réordonner » les engagements fondamentaux ; tirer la poignée du dernier article jusqu'en haut, lentement ; puis flèche bas ; « Annuler » | l'article arrive en place 1, sans revenir en arrière en route ; « « … » en place 1 sur 4. » est dit, le focus est sur lui ; la flèche le descend en place 2, dit aussi |
| C43 | Les informations de fonctionnement là où elles servent (lot U3, ADR 0014) | Monnaie libre : le vote de la Raison d'être, « La formule du seuil », « Réglages de la formule » ; groupe d'essai : « Réordonner » les engagements fondamentaux, puis « N ordre(s) proposé(s) » et « Le vote » de l'ordre en vigueur → « Autres versions » | sous les boutons : « Le vote reste ouvert, sans échéance : tu peux changer ou retirer ton vote à tout moment, le dernier fait foi. » ; la formule : T′ (« votants retenus : 12, T ou Q s'il en manque »), G « déduit de la participation habituelle du groupe », « arrondi à l'entier au-dessus », « Sous le quorum Q de l'article… autant de « pour » que si Q personnes avaient voté » ; les réglages : « Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle. », « Elle ne dépend pas du quorum d'un article. », la phrase de G, jamais « d'habitude » ; en réordonnant : « S'il entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change. » ; le vote d'un ordre proposé à côté de l'ordre en vigueur : la marge à dépasser, et le texte remplacé qui redevient une proposition ; « Autres versions » : « Si plusieurs versions atteignent leur seuil, celle qui a la marge la plus large entre en vigueur. » (l'inventaire : `U3-informations.md`) |
## Au-delà de la liste
@@ -100,3 +105,4 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa
| 2026-11-03 | automate | local, lot T3b sur `582d6ba` (après la relecture de `2ce2028` : la phrase de G de N1.1, « autant de « pour » » ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests) au second passage ; le premier, 40 / 42 : C8 et C32 tombés sur `net::ERR_NETWORK_CHANGED` (modules de l'interface non chargés, vu dans leurs traces) |
| 2026-11-03 | automate | local, lot T3c sur `42c803e` (après la relecture de `e7ab82f` : « à la participation habituelle » à la place de « comme d'habitude » dans F1.2 et N1.1, « le quorum de l'article », « la part exigée », « repose sur moins de 5 propositions », « ramenée » ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests), du premier coup (19 h 51 - 19 h 55, sous le verrou `/tmp/ld-e2e.lock`) |
| 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` |
@@ -0,0 +1,179 @@
# Les informations de fonctionnement dans l'interface (lot U3)
> Préparé le 2026-11-03 (lot U3, branche `lot/u3-interface`), d'après la demande d'Yvv du 03/11
> vers 20 h 38 : « ordonnancement, inertie, et formule de vote sont des infos de fonctionnement. Les
> votes se font à leur place, et les infos doivent trouver leur place dans l'ux. Vérifie que toutes
> les infos utiles sont au bon endroit au moment où on en a besoin, tu as la place dans les panneaux
> qui s'ouvrent à droite. » Le lot T4 a sorti F1, F1.1, F1.2, F1.3, N1, N1.1 et O1 du document
> (ADR 0014). Leurs textes de référence sont ceux de M8 (`de1b7eb`, `seeds/acte-engagement-certification.json`,
> gardés dans `crates/ld-server/tests/fixtures/seed-m8/`) ; ce que chaque phrase de F1.2 et N1.1
> affirme, et le test qui le prouve : `t3c-liste-fermee.md` (numéros repris ici).
## Comment lire
Une ligne par information : la phrase source (abrégée), le **moment** où elle sert, **l'endroit** où
l'interface la dit, la **clé** i18n (`web/src/lib/i18n/fr.ts`, puis en, es), la **preuve**. « Déjà » :
l'interface la disait avant U3, à cet endroit ; « nouveau » ou « changé » : ce lot. Critère des lots T3
à T3c, gardé : chaque phrase dite reste vraie pour tout W et tout quorum ; les termes définis
(« participation habituelle », « quorum de l'article », « part exigée ») ; aucun chiffre daté ; les
nombres viennent de l'API.
Les endroits : **le chapitre** « Info ouverture doc de réf. » (avant le texte, hors vote,
`OpeningInfo.svelte`) ; **le vote déplié** sous une version (`VoteFold`), avec « Inertie »
(`InertiaPicker`), « Quorum » (`QuorumSetting`) et « La formule du seuil » (`Formula`) ; **le panneau
de droite** « Réglages de la formule » (`?onglet=reglages`, `FormulaSettings`) ; **la ligne d'ordre**
d'une section et « Réordonner » (`OrderLine`, `Reorder`) ; **la ligne du titre** (`TitleBar`) ;
**le panneau « Proposer »** (`ProposeView`).
Les preuves : les tests nommés sont sous `crates/` (Rust) ; « grille » renvoie à
`crates/ld-server/tests/seed_quorum_texts.rs` sur la grille de T3c. Chaque phrase de règle que l'écran
dit est épinglée, lettre pour lettre, par `web/tests/unit/rules.test.ts`, qui nomme à chaque ligne le
test qui la prouve ; les nombres lus dans l'API, par les contrôles e2e (C20, C39, C43).
## F1 « Formule de vote »
| # | Phrase source | Moment | Endroit | Clé | Preuve |
|---|---|---|---|---|---|
| F1.a | « La formule qui régit l'adoption ou le rejet de chaque modification » | au vote | vote déplié, « La formule du seuil » (déjà) | `formula.title`, `formula.intro` | une définition |
| F1.b | « Référence : g1vote (https://…) » | — | **non porté** (plus bas) | — | — |
## F1.1 « Formule du seuil WoT »
| # | Phrase source | Moment | Endroit | Clé | Preuve |
|---|---|---|---|---|---|
| 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.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_…` |
| F1.1.7 | « Leurs valeurs sont dans « Paramètres par défaut » et « Niveaux d'inertie ». » | au vote | le lien « Réglages de la formule » sous la formule (déjà) | `settings.title` | — |
## F1.2 « Paramètres par défaut » (numéros de `t3c-liste-fermee.md`)
| # | Phrase source | Moment | Endroit | Clé | Preuve |
|---|---|---|---|---|---|
| 1, 2, 9, 19 | l'introduction et les intertitres | — | rien à porter | — | — |
| 3, 4 | « Chaque version se vote à tout moment, sans échéance. Chacun peut changer ou retirer son vote : le dernier fait foi. » | au vote | vote déplié, sous les boutons, **nouveau** : « Le vote reste ouvert, sans échéance : tu peux changer ou retirer ton vote à tout moment, le dernier fait foi. » — pas sur une version retenue par une objection, où « Une objection retient cette version… » dit ce qui reste (déjà) | `vote.permanent` (`vote.held`) | `ld-engine` `ballot::the_last_one_stands_in_the_order_of_the_chain` ; `tests/ballots.rs` `the_last_one_stands_in_the_order_of_the_chain` |
| 5 | « Une proposition entre en vigueur dès qu'elle atteint son seuil. » | en lisant les propositions | « Propositions en cours » : « Chacune remonte dès qu'elle atteint son seuil » (déjà) ; « Seuil atteint. » dans le vote (déjà) | `deep.note`, `tally.reached` | une règle sans nombre |
| 6, 7 | « S'il y a déjà un texte en vigueur, il lui faut en plus une marge … plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir. » | au vote d'une version concurrente | vote déplié d'une version à côté d'un texte en vigueur (texte, titre, ordre), **nouveau** : « 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. » | `vote.rival` | `ld-engine` `compose::a_rival_must_beat_the_version_in_force` ; `tests/api.rs` `a_rival_replaces_the_seed_and_the_seed_returns_among_proposals` ; `tests/modules.rs` `an_order_is_proposed_with_the_articles_of_its_section_then_voted_into_force` (l'ordre d'avant revient parmi les propositions) |
| 8 | « Si plusieurs propositions y parviennent, celle qui a la marge la plus large entre en vigueur. » | au vote, en voyant les autres versions | vote déplié, dans « Autres versions », **nouveau** | `vote.others.rule` | `ld-engine` `compose::rivals_on_one_unit_yield_one_winner` |
| 10 | « W : le nombre de membres de la toile de confiance Ğ1, relu sur la chaîne toutes les 6 heures. » | au vote, aux réglages | « W — électorat : 5 745 (membres de la toile de confiance Ğ1) », formule et réglages (déjà) ; « toutes les 6 heures » **non porté** (plus bas) | `formula.W`, `formula.W.wot` | — |
| 11 | « T : les votes exprimés sur la version, pour et contre. » | au vote | « T — voix exprimées » (déjà) | `formula.T` | une définition |
| 12 | « Q, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». » | à l'ouverture ; au vote ; en fixant le quorum | le chapitre, **nouveau** : « Sous le quorum d'un article, les votants qui manquent comptent comme « non ». » ; l'infobulle du vote, « Quorum », les réglages (déjà) | `opening.quorum.rule`, `vote.need.title`, `quorum.explain`, `settings.quorum.note` | grille `under_the_quorum_…` |
| 13 | « Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au cran 5. » | à l'ouverture ; aux réglages ; en fixant le quorum | le chapitre, **nouveau** : « 12 · 12 · 50 · 50 · 50 votants, du cran 1 au cran 5. » (lu dans `doc.quorums` ; « 1 votant à chaque cran. » dans le groupe d'essai) ; la colonne « Quorum » des réglages, « Par défaut, celui de son cran : 12 votants. » (déjà) | `opening.quorum.levels`, `opening.quorum.same.*`, `settings.col.quorum`, `quorum.default.*` | `tests/quorum.rs` `each_level_has_its_quorum_and_every_evaluation_says_it` ; lu dans l'API : C39 (l'API changée, le chapitre suit), C20 |
| 14 | « 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. » | à l'ouverture ; en fixant le quorum | le chapitre, **nouveau**, la phrase de F1.2 mot pour mot ; « Quorum » du vote déplié, **changé** : « Tu fixes le quorum de cet article tant qu'il ne garde la trace d'aucun vote. » (avant : « tant que personne n'a voté », faux après un vote retiré, point 8 du lot T2) | `opening.quorum.author`, `quorum.explain` | `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` |
| 15 | « Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un texte. » | en fixant le quorum | le refus du serveur, dit à la personne : « Ce quorum sort des bornes : au moins 2 votants (1 dans le groupe d'essai)… » (déjà) | `quorum.error.quorum` | `tests/quorum.rs` `in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pending` ; `ld-engine` `threshold::one_vote_never_passes_with_a_quorum_of_two_or_more` |
| 16 | « B = 0,1 : dans la toile de confiance, B^W est si petit qu'il ne compte pas. » | au vote | « B — base : 0,1 » (déjà), **nouveau** `formula.B.wot` quand W est la toile (F1.1.6) | `formula.B`, `formula.B.wot` | grille `b_to_the_power_w_…` |
| 17 | « M et G : donnés par le cran d'inertie de l'article » | au vote ; aux réglages | « M — majorité à pleine participation : 50 % — inertie 2 / 5 · basse » (déjà) ; colonnes M et G des réglages, la ligne de l'article marquée (déjà) | `formula.M`, `settings.col.*`, `settings.current` | grille `the_formulas_…` |
| 18 | « G n'est pas fixé à la main : il se déduit de la participation habituelle du groupe. » | au vote ; aux réglages | « G — gradient : 0,103, déduit de la participation habituelle du groupe », **changé** ; aux réglages, **nouveau** : « G se calcule pour que la part exigée, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle. » | `formula.G`, `settings.gradient` | grille `g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum` |
| 20 | « 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 » à sa participation habituelle. » | aux réglages ; à l'ouverture | le tableau des réglages, ligne 3 (déjà) ; sous le tableau, **changé** : « Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle. » (avant : « à 100 votants comme d'habitude », faux quand la médiane sort de ses bornes, T3c) ; le chapitre : « Autres articles 3 / 5 · moyenne » | `settings.share.note`, `opening.inertia.others` | grille `the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum` ; C20 |
| 21 | « Tout nouvel article commence à ce cran. » | en proposant un nouvel article | **non dit dans le formulaire** (plus bas) ; dit aussitôt après, dans le vote qui se déplie : « Inertie · 3 / 5 · moyenne » | — | `tests/quorum.rs` `the_author_of_an_article_sets_its_quorum_before_the_first_vote` (cran 3) |
| 22 | « Au départ, aucun article n'est en vigueur. » | à l'ouverture | le chapitre, la phrase d'Yvv, tant que c'est vrai : « Aucun article n'est encore en vigueur. » | `opening.none` | C39 (dite à Monnaie libre, plus dite dans le groupe d'essai où trois articles sont en vigueur) |
| 23 | « Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). » | à l'ouverture ; en lisant le texte | le chapitre : le paragraphe d'Yvv, et « Propositions d'amorce 2 / 5 · basse » (le cran d'ouverture : avant tout changement voté, lu dans l'API, `lib/opening.ts`) ; la marque « Proposition d'amorce » sur chacune, son infobulle (déjà, mot changé) | `opening.amorce`, `opening.inertia.amorce`, `doc.seedProposal`, `doc.seedProposal.long` | `tests/seed_proposals.rs` `the_view_keeps_which_versions_the_seed_put_forward_once_they_enter_into_force` ; `web/tests/unit/opening.test.ts` ; C39, C40 |
| 24 | « 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. » | à l'ouverture | le chapitre : « Titre du document 4 / 5 · haute », « Ordre de chaque section 2 / 5 · basse », « Autres articles 3 / 5 · moyenne » ; les crans sur la ligne du titre et chaque ligne d'ordre (déjà) | `opening.inertia.title`, `opening.inertia.orders`, `opening.inertia.others` | `web/tests/unit/opening.test.ts` ; C39 (le cran du titre changé dans l'API, le chapitre suit) |
## N1 « Réglage de l'inertie »
| # | Phrase source | Moment | Endroit | Clé | Preuve |
|---|---|---|---|---|---|
| N1.1 | « Chaque article porte son inertie : un cran de 1 à 5 » | partout | les crans dessinés sous chaque article, ligne d'ordre, titre ; « Inertie · 2 / 5 · basse » au vote (déjà) | `inertia.level`, `inertia.badge` | — |
| N1.2 | « Plus le cran est haut, plus il faut de voix pour changer le texte quand peu de gens votent. » | au vote | « Inertie » du vote déplié (déjà) : « Plus l'inertie est haute, plus il faut de voix pour changer ce texte. » ; sa seconde phrase **changée** : « Sous le quorum de l'article, … ; au-delà, la part exigée baisse quand la participation monte. » | `inertia.explain` | seconde phrase : grille `under_the_quorum_…`, `the_share_asked_falls_…` ; la première **n'est pas prouvée** (plus bas) |
| N1.3 | « Le titre du document et l'ordre des articles de chaque section ont aussi la leur, qui se change de la même façon. » | au vote d'un module | les crans de la ligne du titre et des lignes d'ordre, « Inertie » dans leur vote (déjà) | — | — |
| N1.4 | « Tout membre du groupe peut proposer un autre cran. » | au vote | « Proposer un changement d'inertie » (déjà) | `inertia.change` | — |
| N1.5 | « Cette proposition se vote au cran actuel de l'article … l'inertie de l'inertie hérite du cran de l'article. » | en changeant l'inertie | sous le choix du cran (déjà) : « Un changement d'inertie se vote lui-même à l'inertie de l'article : 2 / 5 · basse. » | `inertia.change.note` | ADR 0007 ; `ld-engine` `compose::voting_level` |
## N1.1 « Niveaux d'inertie » (numéros de `t3c-liste-fermee.md`)
| # | Phrase source | Moment | Endroit | Clé | Preuve |
|---|---|---|---|---|---|
| 1 | « À chaque cran, le groupe demande une part de « pour » parmi les votants » | aux réglages | l'introduction des réglages (déjà) | `settings.intro` | — |
| 2 | « Chaque cran a aussi son quorum : sous le quorum de l'article, … il faut autant de « pour » que si ce quorum avait voté. » | au vote ; aux réglages | « La formule du seuil » (**changé**, F1.1.2) ; aux réglages, **changé** : « 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é. » | `formula.reading`, `settings.reading` | grille `under_the_quorum_…` |
| 3 | « La part exigée dépend du cran et du nombre de votants … au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout le monde vote. » | aux réglages ; au vote | aux réglages, **changé** : « 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. » (avant : « quand il vote comme d'habitude » et « Sous le quorum du cran », T3c) ; la formule (déjà) | `settings.reading`, `formula.reading` | grille `the_share_asked_falls_…` |
| 4 | « `Part exigée = M + (1 − M) × (1 − (T'/W)^G)` » | au vote | dans R, en MathML (déjà) | — | grille `the_formulas_…` |
| 5 à 10 | les cinq crans : quorum, part exigée au-delà du quorum de l'article à la participation habituelle, M quand tout le monde vote, ce qui est à ce cran au départ | aux réglages ; à l'ouverture | le tableau des réglages : M, G, part exigée, quorum, voix pour, lus dans l'API (déjà) ; sous le tableau `settings.share.note` (**changé**, F1.2.20) et « Voix pour : à 100 votants, la participation habituelle arrondie, ou au quorum s'il est plus haut. » (**changé**, avant « comme d'habitude ») ; ce qui est à chaque cran au départ : le chapitre | `settings.col.*`, `settings.share.note`, `settings.required.usual.*`, `settings.required.version.*`, `opening.inertia.*` | grille `the_share_of_a_level_…` ; `every_number_of_the_quorum_texts_is_the_engine_s` ; C20 |
| 11 | « La participation habituelle est la médiane du nombre de votants des propositions du groupe votées depuis un an. » | aux réglages | « Participation habituelle » (déjà) | `settings.usual.median.*` | `tests/inertia.rs` `the_usual_participation_is_the_median_of_five_voted_proposals` |
| 12 | « Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de référence du document » | aux réglages | **changé** : « … la participation de référence, tant qu'elle repose sur moins de 5 propositions votées depuis un an (0 pour l'instant). » (avant : « tant que le groupe n'a pas voté sur 5 propositions », faux quand des votes ont plus d'un an, T3c) | `settings.usual.reference.*` | grille `the_participation_habituelle_is_the_median_brought_back_within_its_bounds` ; `tests/inertia.rs` (ci-dessus) |
| 13 | « Elle ne dépend pas du quorum d'un article. » | aux réglages | « Participation habituelle », **nouveau** | `settings.usual.quorum` | `tests/quorum.rs` `in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pending` |
| 14 | « Elle est ramenée entre 1 % et 50 % de l'électorat, et se mesure une fois par jour. » | aux réglages | « Ramenée entre 1 % et 50 % de l'électorat … » quand elle l'est (bornes lues dans l'API), « Mesurée une fois par jour… » (déjà) | `settings.usual.bounded.*`, `settings.usual.when`, `settings.usual.never` | grille `the_participation_habituelle_…` ; `participation::a_measure_is_taken_again_after_a_day` ; C27 |
| 15 | « G se calcule pour que la part exigée, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle : `G = ln(…)/ln(p̃)` » | aux réglages | **nouveau**, la phrase sans la formule ; la valeur de G de chaque cran dans le tableau (déjà) | `settings.gradient` | grille `g_gives_…` |
## O1 « Ordonnancement du document »
| # | Phrase source | Moment | Endroit | Clé | Preuve |
|---|---|---|---|---|---|
| O1.1 | « L'ordre des articles de chaque section est lui-même soumis au vote. » | en lisant | la ligne « Ordre de la section » au bout de chaque section, « Le vote » (déjà) | `order.line` | — |
| O1.2 | « En fin de section, le module « Ordre des articles » se vote comme un article, au cran 2 (basse) au départ » | en réordonnant ; à l'ouverture | « Un ordre se vote à l'inertie 2 / 5 · basse, comme un article… » (déjà) ; le chapitre : « Ordre de chaque section 2 / 5 · basse » | `order.edit.note`, `opening.inertia.orders` | — |
| O1.3 | « chaque proposition est la liste des articles de la section, dans un autre ordre » | au vote d'un ordre | la liste dans le vote d'un ordre, les déplacements marqués (déjà) | `module.order.proposal` | — |
| O1.4 | « Quand un ordre entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change. » | en réordonnant | sous la liste à réordonner, **nouveau** : « S'il entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change. » | `order.edit.effect` | `tests/modules.rs` `an_order_is_proposed_with_the_articles_of_its_section_then_voted_into_force` |
| O1.5 | « Un nouvel article se place à la fin de sa section ; sa place se vote ensuite par l'ordre de la section. » | en proposant ; en réordonnant | « Proposer » (nouvel article) et sous la liste à réordonner (déjà) | `propose.section.note`, `module.order.rule` | — |
| O1.6 | « Un ordre proposé garde ses votes quand la section change … » | en réordonnant ; au vote d'un ordre | `module.order.rule` (déjà) ; « 1 article entré depuis, placé à la fin », « 1 article sorti depuis » au vote (déjà) | `module.order.rule`, `module.order.added.*`, `module.order.dropped.*` | `tests/modules.rs` `articles_that_enter_or_leave_a_section_complete_an_order_under_vote` |
| O1.7 | « Une section d'un seul article n'a pas d'ordre à voter. » | en lisant | par l'absence : pas de ligne d'ordre (déjà) | — | `tests/modules.rs` `an_order_of_one_article_has_nothing_to_decide` |
| O1.8 | « Le titre du document est lui aussi un module, voté au cran 4 (haute) au départ. » | en lisant ; à l'ouverture | la ligne du titre (« Titre du document », ses crans), « Le titre du document se vote à l'inertie 4 / 5 · haute… » (déjà) ; le chapitre | `module.title.name`, `title.propose.note`, `opening.inertia.title` | — |
| O1.9 | « Le titre de chaque article, lui, se vote avec son texte. » | en proposant | le formulaire « Proposer » : le titre et le texte partent ensemble (déjà, par ses champs) | `propose.title`, `propose.body` | — |
## Ce qui n'est pas porté, et pourquoi
- **F1.3 « Processus de dépôt officiel » en entier** : il décrit un calcul de hash IPFS, un ancrage
« on-chain via `system.remark` » et une mise à jour automatique des applications (Cesium, Gecko) ;
rien de cela n'existe aujourd'hui. Ce qui est vrai — la publication des textes en vigueur sur un
dépôt git, suspendue jusqu'au premier texte en vigueur (ADR 0011, 0012) — est déjà dit :
« Publié sur le dépôt des textes », « Publication en attente » (`doc.published`,
`doc.publish.pending`), et dans `/acces` → « Publication des textes ».
- **F1 « Référence : g1vote »** : la formule d'aujourd'hui n'est plus celle de g1vote (le quorum
`T′ = max(T, Q)` a remplacé le plancher C, ADR 0013) ; la dire « référence » serait inexact.
Un « d'après g1vote » serait vrai : à trancher par Yvv.
- **F1.2.10 « relu sur la chaîne toutes les 6 heures »** : l'API ne donne pas cette période
(`wot::REFRESH`) ; l'écrire en dur serait un chiffre de plus à tenir à jour. La source de W
(« membres de la toile de confiance Ğ1 ») est dite.
- **F1.2.21 « Tout nouvel article commence à ce cran »**, au moment d'écrire un nouvel article :
l'API ne donne pas le cran d'un nouvel article (`InertiaLevel::DEFAULT`) ; l'ordre de U3 ne
permet de toucher `crates/` que pour le chapitre. Le cran est dit dès que l'article est proposé
(son vote se déplie, « Inertie · 3 / 5 · moyenne »). Un champ de l'API le permettrait : à trancher.
- **F1.1.6, la phrase de G** « plus il est grand, plus la part demandée monte quand peu de gens
votent » : sans effet quand le quorum de l'article est W (relevé par T3c) ; le sens de G est dit par
`formula.G` et `settings.gradient`.
- **N1.1.15, la formule de G** (`G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`) : la phrase est portée, pas
la formule — un chapitre d'information, pas un manuel. À ajouter en MathML si Yvv la veut.
- **N1.5, l'exemple** « Ainsi, un article au cran 4 ne descend qu'avec les voix que demande le
cran 4 » : la règle est dite avec le cran de l'article (`inertia.change.note`).
## Ce qui reste à trancher ou n'est pas prouvé
- **`inertia.explain`, première phrase** (déjà en ligne, gardée) : « Plus l'inertie est haute, plus
il faut de voix pour changer ce texte. » C'est une tendance, pas une règle prouvée pour tout W et
tout quorum : quand tout le monde vote, les crans 1 et 2 demandent la même part (M = 50 % aux
deux) ; un quorum d'article fixé à W donne M à tous les crans. Aucun test ne la vérifie.
- **`formula.B.wot`** n'est dit que si W est celui de la toile (`electorateSource` = `wot`) ; il est
prouvé pour W de 100 à 10 000 000 (`wot::PLAUSIBLE`). Un `LD_WOT_FALLBACK` sous 100 le rendrait
faux (la production ne le règle pas).
- **« Le vote reste ouvert … à tout moment »** (`vote.permanent`) : vrai hors objection ; sur une
version retenue, la phrase ne s'affiche pas et `vote.held` dit ce qui reste (contre, ou retrait).
## Ce que l'API donne en plus (le seul changement sous `crates/`)
`seedFeatured` sur chaque version (`docs.rs`, `proposal_json`) : la colonne `proposals.seed_featured`,
qui reste vraie quand la Proposition d'amorce entre en vigueur, alors que `seedProposal` devient faux.
Sans elle, le chapitre ne saurait plus, une fois Raison d'être en vigueur, à quel cran les Propositions
d'amorce ont ouvert, et la compterait parmi les « autres articles ». Testé :
`tests/seed_proposals.rs` `the_view_keeps_which_versions_the_seed_put_forward_once_they_enter_into_force`
(vu rouge avant le champ).
## Le chapitre « Info ouverture doc de réf. »
Ce qu'il lit (`web/src/lib/opening.ts`, testé par `web/tests/unit/opening.test.ts`) :
- **le cran d'ouverture** d'un module : celui d'avant le premier changement **voté**
(`inertiaChanges`, cause `vote`) ; un changement par amendement du seed (« réglage de départ du
texte ») est un réglage de l'auteur, il compte dans l'ouverture ;
- **les Propositions d'amorce** : les articles dont une version porte `seedFeatured` ; **les autres
articles** : ceux du seed (une version d'origine `seed`), pas ceux qu'un membre a proposés depuis ;
- **le titre** et **l'ordre de chaque section** : les modules ; plusieurs crans dans une même
catégorie seraient tous dits ;
- **les quorums** : `doc.quorums`, du cran 1 au cran 5, dits une fois s'ils sont tous égaux.
Le paragraphe d'Yvv ne s'affiche que si le document a des Propositions d'amorce. **Sa première phrase,
« Aucun article n'est encore en vigueur. », ne s'affiche plus dès qu'un article est en vigueur** ; la
suite commençait par « Les Propositions d'amorce **y** entrent par le vote », où « y » n'a plus rien à
reprendre : elle dit alors « Les Propositions d'amorce **entrent en vigueur** par le vote, … » (clé
`opening.amorce.after`), le reste mot pour mot. À confirmer par Yvv.
@@ -193,3 +193,29 @@ Les mineurs de la relecture, tranchés ainsi (captures `/tmp/ld-u2-shots/`, avan
l'ordre en vigueur : déplace au moins un article. », lu aussi comme description du bouton.
- **Sur téléphone** : les trois lectures passent à la ligne plutôt que de cacher « Si tout
passe » ; tous les gestes du vote déplié, « Tout déplier » compris, font au moins 44 px.
## Les informations de fonctionnement (lot U3, 03/11)
Yvv, 03/11 : formule, inertie et ordonnancement ne se votent plus (ADR 0014), « les infos doivent
trouver leur place dans l'ux ». Tranché ainsi (inventaire : `U3-informations.md` ; captures :
`/tmp/ld-u3-shots/`) :
- **Avant le texte, hors vote, « Info ouverture doc de réf. »** (le titre d'Yvv) : son paragraphe,
« Aucun article n'est encore en vigueur. Les Propositions d'amorce y entrent par le vote… » (la
première phrase tant que c'est vrai) ; « Inertie à l'ouverture » (Propositions d'amorce, titre du
document, ordre de chaque section, autres articles, avec leurs crans) et « Quorum » (celui de chaque
cran, la règle de l'auteur), lus dans l'API. La voix de l'interface (Nunito) sur un lavis doux, pas
l'eau d'un vote. Puis **une séparation** : un filet de la couleur de la clé de voûte de l'ambiance,
qui s'efface à ses bouts, jamais une bordure ; puis le premier article.
- **Sans les titres « Introduction » et « Engagements fondamentaux »** dans le texte : leurs articles
portent déjà le titre. Ailleurs (la ligne d'ordre, le panier, « Propositions en cours »), la section
garde son nom.
- **Chaque règle au moment où elle sert** : le vote permanent sous les boutons du vote ; la marge à
dépasser dans le vote d'une version à côté d'un texte en vigueur ; qui l'emporte entre plusieurs
versions dans « Autres versions » ; T′, l'arrondi, G et B dans « La formule du seuil » ; la part
exigée, la participation habituelle et G dans « Réglages de la formule », avec les termes définis
des lots T3 à T3c ; ce qu'un ordre change, en réordonnant.
- **« Proposition d'amorce »** à la place de « Proposition seed ».
- **Réordonner au glisser** : la ligne sous le pointeur se lit à sa place dans la mise en page, pas là
où l'animation la montre : tirée sur plusieurs lignes, la poignée ne revient plus en arrière, et
la nouvelle place est dite.
+1 -1
View File
@@ -200,7 +200,7 @@ test('C27 — la participation ramenée dans ses bornes : la mesure, puis ce que
await expect(panel.getByText(/^3 votants sur 4 \(62,5\s%\) : la médiane des 12 propositions votées depuis un an\.$/)).toBeVisible();
// Then what the group asks, the group as subject.
await expect(panel.getByText(/^Ramenée entre 5\s% et 50\s% de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient \(50\s%\)\.$/)).toBeVisible();
await expect(panel.getByText("Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude.")).toBeVisible();
await expect(panel.getByText("Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle.")).toBeVisible();
await shot(page, 'c27-participation-bornee');
});
+2 -2
View File
@@ -57,7 +57,7 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
await shot(page, 'c35-2-poignees');
// By dragging its handle: Réciprocité onto the place of the article above it, Responsabilité
// civique (lot T4; a drag over two rows is not announced, see the report of lot T4 for U3).
// civique (lot T4). A drag over several rows is said and followed by the focus too (lot U3, C42).
await list.locator('li', { hasText: E2 }).locator('.grip').dragTo(list.locator('li', { hasText: EC }));
await expect(names).toHaveText([EF, E1, E2, EC]);
await expect(page.getByText(`« ${E2} » en place 3 sur 4.`)).toBeAttached();
@@ -261,7 +261,7 @@ test('C38 — l’en-tête compte ce que montre la liste ; un changement d’ine
await page.goto(DOC);
await unfold(page);
const facts = page.locator('.facts');
await expect.soft(facts).toContainText('0 article en vigueur · 4 Propositions seed · ');
await expect.soft(facts).toContainText("0 article en vigueur · 4 Propositions d'amorce · ");
await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`));
// A change of inertia on the order of a section and on the title (test group): each is found at
+257
View File
@@ -0,0 +1,257 @@
import { expect, test, type Page } from '@playwright/test';
import { DOC, seedsInForce, shot, signIn, TEST_DOC } from './helpers';
// Lot U3 (Yvv, 2026-11-03, vers 20 h 38 et avant 20 h 44): the rules of operation leave the votes
// (ADR 0014) and find their place in the screen. Before the text, a short chapter out of the vote,
// « Info ouverture doc de réf. », says the settings of the author at the opening, read in the data
// of the document; a separation; then the first article, with no title of section where the
// articles carry theirs. « Proposition d'amorce » for « Proposition seed ». A word for whoever is
// not invited yet. A handle dragged over several rows says where the article went.
test.beforeAll(async ({ browser }) => seedsInForce(browser));
const CSRF = { 'x-requested-with': 'libredecision' };
const TITLE = 'Info ouverture doc de réf.';
/** Yvv's paragraph, word for word (03/11, vers 20 h 38): it replaces « Commence par les Propositions seed ». */
const NONE = "Aucun article n'est encore en vigueur.";
const AMORCE =
"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.";
/** Once an article is in force, the first sentence goes; « y » has nothing to stand for, it says « en vigueur ». */
const AMORCE_AFTER =
"Les Propositions d'amorce entrent en vigueur 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.";
const 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.";
/** Yvv, 03/11 avant 20 h 44 (« @ chiara » lu « à Chiara »). */
const UNINVITED =
"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.";
/** The top of the chapter, the separation and the first article of the text, in this order on the page. */
async function topOf(page: Page) {
return page.evaluate(() => {
const box = (el: Element | null) => el?.getBoundingClientRect() ?? null;
const chapter = box(document.querySelector('section.opening'));
const rule = box(document.querySelector('hr.opening-rule'));
const first = box(document.querySelector('section.text article.unit'));
return { chapter: chapter && { top: chapter.top, bottom: chapter.bottom }, rule: rule && { top: rule.top, bottom: rule.bottom }, first: first && { top: first.top } };
});
}
test('C39 — l’ouverture du document : « Info ouverture doc de réf. », hors vote, lu dans les données ; une séparation ; puis le texte, sans les titres Introduction et Engagements fondamentaux', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
// The chapter, named as Yvv named it, before the text; out of the vote: no gesture of a vote.
const chapter = page.getByRole('region', { name: TITLE });
await expect(chapter).toBeVisible();
await expect(chapter.getByRole('heading', { name: TITLE })).toBeVisible();
await expect(chapter.getByRole('button')).toHaveCount(0);
// Nothing in force: Yvv's paragraph, word for word, in place of the old line.
await expect(chapter.getByText(`${NONE} ${AMORCE}`)).toBeVisible();
await expect(page.getByText(/Commence par les Propositions/)).toHaveCount(0);
// The settings of the author at the opening: the inertia, then the quorum of each level.
await expect(chapter.getByRole('heading', { name: "Inertie à l'ouverture" })).toBeVisible();
await expect(chapter.locator('li')).toHaveText([
/^Propositions d'amorce\s*2 \/ 5 · basse$/,
/^Titre du document\s*4 \/ 5 · haute$/,
/^Ordre de chaque section\s*2 \/ 5 · basse$/,
/^Autres articles\s*3 \/ 5 · moyenne$/,
]);
await expect(chapter.getByRole('heading', { name: 'Quorum' })).toBeVisible();
await expect(chapter).toContainText('12 · 12 · 50 · 50 · 50 votants, du cran 1 au cran 5.');
await expect(chapter).toContainText("Sous le quorum d'un article, les votants qui manquent comptent comme « non ».");
await expect(chapter).toContainText(AUTHOR);
// Then a separation, drawn without a border, then the first article: in this order.
const rule = page.locator('hr.opening-rule');
await expect(rule).toHaveCount(1);
const at = await topOf(page);
expect(at.chapter && at.rule && at.first).toBeTruthy();
expect(at.rule!.top).toBeGreaterThanOrEqual(at.chapter!.bottom);
expect(at.first!.top).toBeGreaterThanOrEqual(at.rule!.bottom);
// The four moods: the rule is a soft colour of the mood, never a border.
for (const mood of ['pierre', 'calcaire', 'nuit', 'ardoise']) {
await page.evaluate((m) => (document.documentElement.dataset.mood = m), mood);
const look = await rule.evaluate((el) => {
const cs = getComputedStyle(el);
return { image: cs.backgroundImage, borders: [cs.borderTopWidth, cs.borderBottomWidth, cs.borderLeftWidth, cs.borderRightWidth], height: el.getBoundingClientRect().height };
});
expect(look.image, mood).toContain('gradient');
expect(look.borders, mood).toEqual(['0px', '0px', '0px', '0px']);
expect(look.height, mood).toBeGreaterThanOrEqual(1);
}
await page.evaluate(() => (document.documentElement.dataset.mood = 'pierre'));
// The titles « Introduction » and « Engagements fondamentaux » are not shown: the articles carry
// theirs (Yvv: « Ça embrouille. »). The order of the section stays votable, at its end.
const text = page.locator('section.text');
await expect(text.locator('.section-name')).toHaveCount(0);
await expect(text.getByText('Introduction', { exact: true })).toHaveCount(0);
await expect(text.getByText('Engagements fondamentaux', { exact: true })).toHaveCount(0);
const order = text.getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' });
await expect(order).toBeVisible();
await expect(order.getByRole('button', { name: 'Réordonner' })).toBeAttached();
await shot(page, 'c39-1-ouverture');
// Read in the data of the document, never written in: other quorums, another level of the
// title, and the chapter follows.
await page.route(/\/api\/docs\/[^/?]+$/, async (route) => {
const res = await route.fetch();
const body = await res.json();
body.doc.quorums = [3, 4, 5, 6, 7];
body.modules.title.inertia = 5;
await route.fulfill({ response: res, json: body });
});
await page.reload();
await expect(chapter).toContainText('3 · 4 · 5 · 6 · 7 votants, du cran 1 au cran 5.');
await expect(chapter.locator('li').nth(1)).toHaveText(/^Titre du document\s*5 \/ 5 · très haute$/);
await page.unroute(/\/api\/docs\/[^/?]+$/);
// The test group, its seed proposals voted into force (beforeAll): the first sentence goes,
// they keep the level they opened at, and the quorum is one voter at each level.
await page.goto(TEST_DOC);
await expect(chapter).toBeVisible();
await expect(chapter.getByText(NONE)).toHaveCount(0);
await expect(chapter.getByText(AMORCE_AFTER)).toBeVisible();
await expect(chapter.locator('li').first()).toHaveText(/^Propositions d'amorce\s*2 \/ 5 · basse$/);
await expect(chapter.locator('li').last()).toHaveText(/^Autres articles\s*3 \/ 5 · moyenne$/);
await expect(chapter).toContainText('1 votant à chaque cran.');
for (const name of await text.locator('.section-name').allTextContents()) {
expect(['Introduction', 'Engagements fondamentaux']).not.toContain(name.trim());
}
await expect(text.getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' })).toBeVisible();
});
test('C40 — « Proposition d’amorce » à la place de « Proposition seed », en français, en anglais, en espagnol', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
const seeds = page.locator('section.text article.unit');
await expect(seeds.locator('.tag', { hasText: "Proposition d'amorce" })).toHaveCount(4);
await expect(page.locator('.facts')).toContainText("0 article en vigueur · 4 Propositions d'amorce · ");
await expect(page.locator('body')).not.toContainText(/propositions? seed/i);
await shot(page, 'c40-amorce');
const langs = page.getByRole('radiogroup', { name: 'Langue du texte' });
await langs.getByRole('radio', { name: 'EN' }).click();
await expect(seeds.locator('.tag', { hasText: 'Starter proposal' })).toHaveCount(4);
await expect(page.locator('.facts')).toContainText('4 starter proposals');
await expect(page.locator('body')).not.toContainText(/seed proposals?/i);
await page.getByRole('radiogroup', { name: 'Text language' }).getByRole('radio', { name: 'ES' }).click();
await expect(seeds.locator('.tag', { hasText: 'Propuesta de arranque' })).toHaveCount(4);
await expect(page.locator('.facts')).toContainText('4 propuestas de arranque');
await expect(page.locator('body')).not.toContainText(/propuestas? (seed|semilla)/i);
});
test('C41 — qui n’est pas encore invité : le mot d’Yvv à l’accueil et sur « Se connecter », sans redite', async ({ page }) => {
// The welcome without a session, the Ğ1 sign-in not active (the e2e server, as production):
// Yvv's word in place of a button that would lead nowhere.
await page.goto('/');
await expect(page.getByText(UNINVITED)).toBeVisible();
await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toHaveCount(0);
await expect(page.getByText("Tu as reçu un lien d'invitation ? Ouvre-le simplement : il te connecte.")).toBeVisible();
await shot(page, 'c41-1-accueil');
// « Se connecter »: said once, and the Ğ1 line no longer says how to get in meanwhile.
await page.goto('/connexion');
await expect(page.getByText(UNINVITED)).toHaveCount(1);
await expect(page.getByText(UNINVITED)).toBeVisible();
await expect(page.getByText("La connexion Ğ1 arrive : l'inscription de libreDecision auprès de connect.monnaie-libre.fr est en cours.", { exact: true })).toBeVisible();
await expect(page.getByText(/En attendant, on entre avec un lien d'invitation/)).toHaveCount(0);
await expect(page.getByText("Tu as reçu un lien d'invitation ? Ouvre-le simplement : il te connecte.")).toBeVisible();
await shot(page, 'c41-2-connexion');
// In English and in Spanish, the same word.
for (const [lang, words] of [
['en', 'To join a decision group, for now ask Chiara or Yvv for an invitation link. This prototype is not yet wired to the chain.'],
['es', '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.'],
] as const) {
await page.evaluate((l) => localStorage.setItem('ld:lang', l), lang);
await page.goto('/connexion');
await expect(page.getByText(words)).toBeVisible();
}
await page.evaluate(() => localStorage.setItem('ld:lang', 'fr'));
});
test('C42 — une poignée tirée sur plusieurs lignes : la nouvelle place est dite, le focus suit l’article', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(TEST_DOC);
// The fundamental commitments, four articles, in the text of the test group (beforeAll).
const line = page.locator('section.text').getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' });
await line.hover();
await line.getByRole('button', { name: 'Réordonner' }).click();
const list = page.getByRole('list', { name: 'Nouvel ordre des articles' });
const names = list.locator('button.item .name');
await expect(names).toHaveCount(4);
const before = await names.allTextContents();
const last = before[3]!;
// The last handle taken, and carried over the three rows above it, step by step, as a hand does.
const grip = await list.locator('li').nth(3).locator('.grip').boundingBox();
const top = await list.locator('li').nth(0).boundingBox();
expect(grip && top).toBeTruthy();
await page.mouse.move(grip!.x + grip!.width / 2, grip!.y + grip!.height / 2);
await page.mouse.down();
await page.mouse.move(grip!.x + grip!.width / 2, top!.y + top!.height / 2, { steps: 24 });
await page.mouse.up();
await expect(names).toHaveText([last, ...before.slice(0, 3)]);
// Said, and the focus on the article moved: the arrows go on from there.
await expect(page.getByText(`« ${last} » en place 1 sur 4.`)).toBeAttached();
await expect(list.locator('button.item').nth(0)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(names).toHaveText([before[0]!, last, before[1]!, before[2]!]);
await expect(page.getByText(`« ${last} » en place 2 sur 4.`)).toBeAttached();
await page.getByRole('button', { name: 'Annuler' }).click();
await expect(list).toHaveCount(0);
});
test('C43 — les informations de fonctionnement là où elles servent : le vote déplié, la formule, les réglages, l’ordre, le quorum', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
// The vote of a Proposition d'amorce: a permanent vote, the last gesture counts.
const i1 = page.locator('section.text article.unit', { hasText: "Raison d'être" }).first();
await i1.hover();
await i1.getByRole('button', { name: 'Le vote' }).click();
const fold = page.getByRole('region', { name: "Le vote de « Raison d'être »" });
await expect(fold).toContainText('Le vote reste ouvert, sans échéance : tu peux changer ou retirer ton vote à tout moment, le dernier fait foi.');
// The formula: T′, R rounded up, G from the usual turnout, the quorum of the article.
await fold.getByText('La formule du seuil').click();
await expect(fold).toContainText("T′ — votants retenus : 12, T ou Q s'il en manque");
await expect(fold).toContainText('G — gradient');
await expect(fold).toContainText('déduit de la participation habituelle du groupe');
await expect(fold).toContainText("Une version atteint son seuil quand ses voix pour atteignent R, arrondi à l'entier au-dessus.");
await expect(fold).toContainText("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é.");
await shot(page, 'c43-1-vote-formule');
// The settings: the defined terms, never « comme d'habitude ».
await fold.getByRole('button', { name: 'Réglages de la formule' }).click();
const panel = page.locator('aside.panel');
await expect(panel).toContainText("Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle.");
await expect(panel).toContainText("Elle ne dépend pas du quorum d'un article.");
await expect(panel).toContainText("G se calcule pour que la part exigée, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle.");
await expect(panel).not.toContainText("d'habitude");
await shot(page, 'c43-2-reglages');
await page.keyboard.press('Escape');
// The test group: the order of a section, and the order in force with an order proposed beside it.
await page.goto(TEST_DOC);
const line = page.locator('section.text').getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' });
await line.hover();
await line.getByRole('button', { name: 'Réordonner' }).click();
await expect(line).toContainText("S'il entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change.");
await page.getByRole('button', { name: 'Annuler' }).click();
// An order proposed beside the one in force (C35 leaves the old one back among the proposals; if
// not, one is proposed here): its vote says the margin it must beat.
const g = await (await page.request.get('/api/groups/essai')).json();
const d = await (await page.request.get(`/api/docs/${g.docs[0].id}`)).json();
const fond = d.modules.orders.find((o: { section: string }) => o.section === 'fondamental');
if (!fond.proposals.some((p: { status: string; kind: string }) => p.status === 'open' && p.kind === 'order')) {
const ids: string[] = fond.members;
const r = await page.request.post(`/api/units/${fond.id}/order`, { data: { order: [...ids.slice(1), ids[0]] }, headers: CSRF });
expect(r.ok()).toBe(true);
await page.reload();
}
await line.getByRole('button', { name: /ordres? proposés?$/ }).click();
const rival = line.locator('section.fold');
await expect(rival).toContainText(
'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.',
);
// The order in force: its other versions, and which one enters when several reach their threshold.
await line.hover();
await line.getByRole('button', { name: 'Le vote' }).click();
const inForce = line.locator('section.fold');
await inForce.getByRole('button', { name: /^Autres versions/ }).click();
await expect(inForce).toContainText('Si plusieurs versions atteignent leur seuil, celle qui a la marge la plus large entre en vigueur.');
await shot(page, 'c43-3-ordre-rival');
});
+13 -8
View File
@@ -13,7 +13,10 @@ test('C1 — l’accueil sans session : la voûte abrite les trois points, le ti
await expect(nav.locator('.tool', { hasText: item })).toBeVisible();
}
await expect(nav.locator('.tool.off')).toHaveCount(3);
await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toBeVisible();
// The Ğ1 sign-in not active (as in production): Yvv's word for whoever is not invited yet,
// in place of the button (lot U3, C41).
await expect(page.getByText("Pour rentrer dans un groupe de décisions, il faut pour l'instant demander un lien d'invitation à Chiara ou à Yvv.", { exact: false })).toBeVisible();
await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toHaveCount(0);
// The four pillars live on the page of a group, no longer here.
await expect(page.locator('.pillars')).toHaveCount(0);
await expect(page.getByText('Monnaie libre', { exact: true })).toHaveCount(0);
@@ -37,17 +40,18 @@ test('C2 — l’accueil avec session : le choix du groupe, ceux de la même cl
await expect(page).toHaveURL((u) => u.pathname === '/');
});
test('C3 — au départ, aucun texte en vigueur : la raison d’être et les trois engagements, Propositions seed, ouvrent le texte', async ({ page }) => {
test('C3 — au départ, aucun texte en vigueur : la raison d’être et les trois engagements, Propositions d’amorce, ouvrent le texte', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification");
// Nothing in force (Yvv, 2026-11-02): the page says where to begin, once — the seed proposals,
// read whole at the head of the text (lot U), each marked: never the bare page of a text in force.
await expect(page.getByText("Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.")).toBeVisible();
// Lot U3 (Yvv, 2026-11-03): said by « Info ouverture doc de réf. », in her words (C39).
await expect(page.getByRole('region', { name: 'Info ouverture doc de réf.' }).getByText("Aucun article n'est encore en vigueur. Les Propositions d'amorce y entrent par le vote")).toBeVisible();
const seeds = page.locator('section.text article.unit');
// Lot T4 (Yvv, 2026-11-03): « Responsabilité civique » between the two engagements.
await expect(seeds.locator('h2')).toHaveText(["Raison d'être", 'Unicité du compte', 'Responsabilité civique', 'Réciprocité']);
await expect(seeds.locator('.tag', { hasText: 'Proposition seed' })).toHaveCount(4);
await expect(seeds.locator('.tag', { hasText: "Proposition d'amorce" })).toHaveCount(4);
await expect(seeds.getByTitle("Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.")).toHaveCount(4);
// At the low level they start at, how far each is from passing.
for (const seed of await seeds.all()) await expect(seed).toContainText('0 pour · 0 contre · 9 oui sur 12');
@@ -61,7 +65,7 @@ test('C3 — au départ, aucun texte en vigueur : la raison d’être et les tro
await unfold(page);
// One line per article: the 27 less the 4 at the head, E3 and E7 holding their two variants inside.
await expect(page.locator('ul.deep button.row')).toHaveCount(23);
await expect(page.locator('ul.deep .tag', { hasText: 'Proposition seed' })).toHaveCount(0);
await expect(page.locator('ul.deep .tag', { hasText: "Proposition d'amorce" })).toHaveCount(0);
});
test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => {
@@ -331,8 +335,9 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
// At 100 voters, above every quorum: each level's share of the 100.
const required = panel.locator('table tbody td.required');
await expect(required).toHaveText(['60', '67', '75', '83', '90']);
await expect(panel.getByText("Voix pour : à 100 votants, comme d'habitude.")).toBeVisible();
await expect(panel.getByText("Part exigée : la part de « pour » demandée, à 100 votants comme d'habitude.")).toBeVisible();
// Lot U3: the defined terms of lot T3c, never « comme d'habitude ».
await expect(panel.getByText("Voix pour : à 100 votants, la participation habituelle arrondie, ou au quorum s'il est plus haut.")).toBeVisible();
await expect(panel.getByText("Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle.")).toBeVisible();
await expect(panel.getByText(/^Quorum : en dessous, les votants qui manquent comptent comme « non »\./)).toBeVisible();
await page.keyboard.press('Escape');
@@ -349,7 +354,7 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
// No vote: each level on its quorum, the voters missing counted as « non ».
await expect(quorums).toHaveText(['12', '12', '50', '50', '50']);
await expect(required).toHaveText(['8', '9', '39', '43', '46']);
await expect(panel.getByText('Voix pour : à 0 votant, la participation de cette version.')).toBeVisible();
await expect(panel.getByText("Voix pour : à 0 votant, la participation de cette version, ou au quorum s'il est plus haut.")).toBeVisible();
await page.keyboard.press('Escape');
// The five levels to choose from, in the test group (Réciprocité voted into force there,
+9 -2
View File
@@ -28,6 +28,9 @@
} = $props();
const p = $derived(levels.find((x) => x.level === inertia));
/** Q — the quorum of the article it is judged on; T′ = max(T, Q) (ADR 0013). */
const quorum = $derived(evaluation?.quorum ?? p?.quorum ?? floor);
const cast = $derived(tally.for + tally.against);
</script>
<details class="formula">
@@ -49,15 +52,19 @@
</math>
<ul class="values">
<li>{t('formula.W', { n: i18n.number(electorate), source: t(source === 'wot' ? 'formula.W.wot' : 'formula.W.group') })}</li>
<li>{t('formula.T', { n: tally.for + tally.against })}</li>
<li>{t('formula.T', { n: cast })}</li>
<li>{t('formula.Tprime', { n: i18n.number(Math.max(cast, quorum)) })}</li>
{#if p}
<li>{t('formula.M', { n: i18n.number(p.majorityPct) })} — {inertiaBadge(inertia)}</li>
<li>{t('formula.G', { n: i18n.number(Math.round(p.gradient * 1000) / 1000) })}</li>
{/if}
<li>{t('formula.B', { n: i18n.number(base) })}</li>
<li>{t('formula.Q', { n: i18n.number(evaluation?.quorum ?? p?.quorum ?? floor) })}</li>
<li>{t('formula.Q', { n: i18n.number(quorum) })}</li>
{#if evaluation}<li class="r">{t('formula.R', { n: evaluation.required })}</li>{/if}
</ul>
<p class="faint">{t('formula.round')}</p>
<!-- Checked for the web of trust only (W from 100, wot::PLAUSIBLE): in a small group, B^W may count. -->
{#if source === 'wot'}<p class="faint">{t('formula.B.wot')}</p>{/if}
<p class="faint">{t('formula.reading')}</p>
</details>
{#if onsettings}
@@ -64,7 +64,7 @@
{/if}
</p>
{#if p.bounded}<p class="faint small">{i18n.plural('settings.usual.bounded', p.voters, { lo: pct(p.bounds[0]), hi: pct(p.bounds[1]), pct: pct(p.ratio) })}</p>{/if}
<p class="faint small">{p.measuredAt ? t('settings.usual.when', { date: i18n.date(p.measuredAt) }) : t('settings.usual.never')}</p>
<p class="faint small">{p.measuredAt ? t('settings.usual.when', { date: i18n.date(p.measuredAt) }) : t('settings.usual.never')} {t('settings.usual.quorum')}</p>
</div>
<h4>{t('settings.levels')}</h4>
@@ -100,10 +100,11 @@
</tbody>
</table>
</div>
<p class="faint small">{i18n.plural('settings.share.note', p.voters)}</p>
<p class="faint small">{t('settings.share.note')}</p>
<p class="faint small">{t('settings.quorum.note')}</p>
<p class="faint small">{i18n.plural(version ? 'settings.required.version' : 'settings.required.usual', cast)}</p>
<p class="faint small">{t('settings.reading')}</p>
<p class="faint small">{t('settings.gradient')}</p>
</section>
{/if}
+129
View File
@@ -0,0 +1,129 @@
<script lang="ts">
// « Info ouverture doc de réf. » (Yvv, 2026-11-03: « un petit chapitre qui explicite les réglages
// initiaux de l'auteur, notamment Quorum et Inertie »), before the text, out of the vote: Yvv's
// paragraph — its first sentence while no article is in force —, the level each kind of module
// opened at and the quorum of each level, read in the data of the document (`lib/opening.ts`),
// never written in. Short: a chapter of information, not a manual.
import { i18n, t, type Key } from '$lib/i18n/index.svelte';
import { isAmorce, openingLevels, quorumsOf } from '$lib/opening';
import type { DocView } from '$lib/types';
import Crans from './Crans.svelte';
let { view, inForce }: { view: DocView; inForce: number } = $props();
const levels = $derived(openingLevels(view));
const rows = $derived(
(
[
['opening.inertia.amorce', levels.amorce],
['opening.inertia.title', levels.title],
['opening.inertia.orders', levels.orders],
['opening.inertia.others', levels.others],
] as [Key, typeof levels.amorce][]
).filter(([, ks]) => ks.length),
);
const amorce = $derived(view.units.some(isAmorce));
const quorums = $derived(quorumsOf(view.doc.quorums));
</script>
<section class="opening" aria-labelledby="opening-title">
<h2 id="opening-title">{t('opening.title')}</h2>
{#if amorce}
<!-- Once an article is in force, the first sentence goes; « y » then says « en vigueur ». -->
<p class="amorce">{#if inForce}{t('opening.amorce.after')}{:else}{t('opening.none')} {t('opening.amorce')}{/if}</p>
{/if}
<div class="settings">
{#if rows.length}
<div class="block">
<h3>{t('opening.inertia')}</h3>
<ul>
{#each rows as [key, ks] (key)}
<li><span class="what">{t(key)}</span> <span class="levels">{#each ks as k, i (k)}{#if i}<span aria-hidden="true">, </span>{/if}<Crans level={k} bare />{/each}</span></li>
{/each}
</ul>
</div>
{/if}
{#if quorums}
<div class="block">
<h3>{t('opening.quorum')}</h3>
<p class="quorums">
{quorums.same !== null
? i18n.plural('opening.quorum.same', quorums.same)
: t('opening.quorum.levels', { list: quorums.list.map((q) => i18n.number(q)).join(' · ') })}
</p>
<p class="faint small">{t('opening.quorum.rule')} {t('opening.quorum.author')}</p>
</div>
{/if}
</div>
</section>
<style>
/* Information, not a text to vote: the voice of the interface (Nunito), on a soft wash of the
mood, without a border; the water stays for what is voted. */
.opening {
display: grid;
gap: 0.75rem;
margin: 2rem -1.3rem 0;
padding: 1.1rem 1.3rem 1.2rem;
border-radius: var(--r-card);
background: color-mix(in srgb, var(--surface-2) 60%, transparent);
font-size: 0.92rem;
}
h2 {
font-family: var(--font-ui);
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink-soft);
}
.amorce {
font-family: var(--font-text);
line-height: 1.6;
color: var(--ink);
}
/* One column: the reading measure is too narrow for two without breaking a line of notches. */
.settings {
display: grid;
gap: 0.75rem;
}
.block {
display: grid;
align-content: start;
gap: 0.35rem;
min-width: 0;
}
h3 {
font-size: 0.86rem;
font-weight: 800;
}
ul {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.2rem;
}
li {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 0 0.8rem;
}
.what {
font-weight: 700;
}
.levels {
color: var(--ink-soft);
font-weight: 650;
font-size: 0.86rem;
}
.quorums {
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.small {
font-size: 0.84rem;
}
</style>
+21 -7
View File
@@ -111,11 +111,13 @@
}
}
// Dragging by the handle: the article takes the place of the row under the pointer.
// Dragging by the handle: the article takes the place of the row under the pointer. The pointer
// is captured by the list, which stays put, rather than by the handle of a row the reorder moves
// (a guard: what lot U3 found is the measure of the rows, in `drag`).
function grab(e: PointerEvent, id: string) {
if (busy || e.button !== 0) return;
e.preventDefault();
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
root?.setPointerCapture(e.pointerId);
dragging = id;
chosen = null;
error = '';
@@ -124,9 +126,12 @@
function drag(e: PointerEvent) {
const id = dragging;
if (!id || !root) return;
// The rows where the layout puts them, not where their animation shows them (`animate:flip`
// moves them by a transform): a row sliding under the pointer would send the article back.
const y = e.clientY - root.getBoundingClientRect().top;
for (const row of root.querySelectorAll<HTMLElement>('li[data-id]')) {
const box = row.getBoundingClientRect();
if (e.clientY < box.top || e.clientY >= box.bottom) continue;
const top = row.offsetTop - root.offsetTop;
if (y < top || y >= top + row.offsetHeight) continue;
const target = row.dataset.id;
if (target && target !== id) list = place(list, id, target);
return;
@@ -164,11 +169,20 @@
<div class="reorder">
<p class="faint small" id="order-help-{unit.id}">{t('order.edit.help')}</p>
<ol class="list" aria-label={t('order.edit.label')} aria-describedby="order-help-{unit.id}" bind:this={root}>
<ol
class="list"
aria-label={t('order.edit.label')}
aria-describedby="order-help-{unit.id}"
bind:this={root}
onpointermove={drag}
onpointerup={drop}
onpointercancel={drop}
onlostpointercapture={drop}
>
{#each list as id, i (id)}
{@const name = nameOf(id)}
<li data-id={id} class:chosen={chosen === id} class:moved={changed.has(id)} class:dragging={dragging === id} animate:flip={{ duration: reduce ? 0 : 140 }}>
<span class="grip" aria-hidden="true" onpointerdown={(e) => grab(e, id)} onpointermove={drag} onpointerup={drop} onpointercancel={drop}>
<span class="grip" aria-hidden="true" onpointerdown={(e) => grab(e, id)}>
<Icon name="grip" size={18} />
</span>
<button type="button" class="item" data-id={id} aria-pressed={chosen === id} disabled={busy} onclick={() => pick(id)} onkeydown={(e) => key(e, id)}>
@@ -186,7 +200,7 @@
{/each}
</ol>
<p class="visually-hidden" aria-live="polite">{said}</p>
<p class="faint small">{t('order.edit.note', { level: inertiaLevel(unit.inertia) })} {t('module.order.rule')}</p>
<p class="faint small">{t('order.edit.note', { level: inertiaLevel(unit.inertia) })} {t('order.edit.effect')} {t('module.order.rule')}</p>
{#if existing && !unchanged}<p class="note" role="status">{t('order.edit.exists')}</p>{/if}
{#if error}<p class="warn" role="alert">{error}</p>{/if}
<div class="actions">
+12
View File
@@ -93,6 +93,14 @@
(x): x is Proposal => !!x && x.id !== p.id && !besides.includes(x.id),
),
);
/**
* A version beside a text in force (lot U3, from F1.2): it must also beat the margin of that text,
* which comes back among the proposals if replaced (ADR 0009 for a module). Not a change of
* inertia, voted apart; not a removal, which has its own note.
*/
const rival = $derived(
!!unit.inForce && unit.inForce.id !== p.id && p.status === 'open' && (p.kind === 'text' || p.kind === 'title' || p.kind === 'order'),
);
/** The inertia is said by « Inertie », below; a change of inertia has none: its level is said here. */
const ownInertia = $derived(p.kind === 'inertia');
@@ -177,6 +185,7 @@
{#if ownInertia}<p class="faint small"><Crans level={p.votedAtInertia} /></p>{/if}
</div>
</div>
{#if rival}<p class="faint small">{t('vote.rival')}</p>{/if}
{#if p.blocked}
<div class="held" role="status">
<p class="lead">{t('vote.held')}</p>
@@ -254,6 +263,8 @@
{:else if access.simulated}
<p class="faint small">{t('vote.simulated.note')}</p>
{/if}
<!-- A permanent vote (F1.2, lot U3); held back by an objection, `vote.held` says what is left. -->
{#if !p.blocked}<p class="faint small">{t('vote.permanent')}</p>{/if}
{/if}
<p class="faint small">{t('vote.private')}</p>
{#if error}<p class="warn" role="alert">{error}</p>{/if}
@@ -284,6 +295,7 @@
</li>
{/each}
</ul>
<p class="faint small">{t('vote.others.rule')}</p>
</section>
{/if}
{#if !ownInertia}<InertiaPicker {doc} {unit} {onopen} />{/if}
+41 -18
View File
@@ -42,8 +42,9 @@ export const en: Messages = {
'signin.g1': 'Sign in with Ğ1',
'signin.g1.how': "You'll be sent to connect.monnaie-libre.fr, the sign-in service of the Monnaie libre forum. There you approve the sign-in in Ğecko (link or QR code), then come back here. No password and no key is ever passed to us.",
'signin.g1.members': 'For members of the Ğ1 web of trust only.',
'signin.g1.pending': "Ğ1 sign-in is on its way: libreDecision's registration with connect.monnaie-libre.fr is in progress. Meanwhile, you get in with an invitation link.",
'signin.g1.pending': "Ğ1 sign-in is on its way: libreDecision's registration with connect.monnaie-libre.fr is in progress.",
'signin.invite': 'Got an invitation link? Just open it: it signs you in.',
'signin.uninvited': 'To join a decision group, for now ask Chiara or Yvv for an invitation link. This prototype is not yet wired to the chain.',
'signin.dev': 'Development sign-in',
'signin.dev.name': 'Name',
'signin.dev.admin': 'access administrator',
@@ -103,8 +104,8 @@ export const en: Messages = {
'docs.inForce.other': '{n} articles in force',
'docs.open.one': '{n} open proposal',
'docs.open.other': '{n} open proposals',
'docs.seeds.one': '{n} seed proposal',
'docs.seeds.other': '{n} seed proposals',
'docs.seeds.one': '{n} starter proposal',
'docs.seeds.other': '{n} starter proposals',
'doc.view.label': 'Read',
'doc.view.inforce': 'In force',
@@ -125,7 +126,7 @@ export const en: Messages = {
'doc.seed.long': 'Came into force without a vote, so as not to start from a blank page. It remains open for voting, like any text.',
'doc.seed.votes.one': 'seed a minima · {n} vote',
'doc.seed.votes.other': 'seed a minima · {n} votes',
'doc.seedProposal': 'Seed proposal',
'doc.seedProposal': 'Starter proposal',
'doc.seedProposal.long': 'A starting text put forward to start the vote. It is not in force: it comes into force through the vote, like any proposal.',
'doc.confirmed': 'confirmed by vote',
'doc.adopted': 'adopted on {date}',
@@ -134,7 +135,21 @@ export const en: Messages = {
'doc.basket': 'Vote in bulk',
'doc.basket.count': 'Vote in bulk · {n}',
'doc.empty': 'No article is in force. The proposals below move up as soon as they reach their threshold.',
'doc.empty.seeds': 'No article is in force yet. Start with the seed proposals: they come into force by the vote, like any proposal.',
'opening.title': 'Opening info, reference doc',
'opening.none': 'No article is in force yet.',
'opening.amorce': 'Starter proposals come into force through the vote, like the proposals that follow, with less inertia so that the document comes to life more easily. It is recommended to raise the inertia afterwards, through the vote, to make changes less frequent.',
'opening.amorce.after': 'Starter proposals come into force through the vote, like the proposals that follow, with less inertia so that the document comes to life more easily. It is recommended to raise the inertia afterwards, through the vote, to make changes less frequent.',
'opening.inertia': 'Inertia at the opening',
'opening.inertia.amorce': 'Starter proposals',
'opening.inertia.title': 'Title of the document',
'opening.inertia.orders': 'Order of each section',
'opening.inertia.others': 'Other articles',
'opening.quorum': 'Quorum',
'opening.quorum.levels': '{list} voters, from level 1 to level 5.',
'opening.quorum.same.one': '{n} voter at each level.',
'opening.quorum.same.other': '{n} voters at each level.',
'opening.quorum.rule': "Below an article's quorum, the missing voters count as against.",
'opening.quorum.author': '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.',
'doc.others.one': '{n} other version open',
'doc.others.other': '{n} other versions open',
'doc.published': 'Published to the text repository',
@@ -202,6 +217,7 @@ export const en: Messages = {
'order.proposed.one': '{n} order proposed',
'order.proposed.other': '{n} orders proposed',
'order.edit.note': 'An order is voted at inertia {level}, like an article: unfolded, in the basket, on the Ğ1 chain in Monnaie libre.',
'order.edit.effect': 'If it comes into force, only the articles of this section change place; no text changes.',
'order.submitted': 'Order submitted to vote.',
'title.propose': 'Propose another title',
'title.propose.label': 'New title',
@@ -246,6 +262,7 @@ export const en: Messages = {
'vote.simulated.mark': 'simulated vote',
'vote.others': 'Other versions',
'vote.others.count': 'Other versions ({n})',
'vote.others.rule': 'If several versions reach their threshold, the one with the widest margin comes into force.',
'vote.need': '{yes} for to pass · {no} against to block',
'vote.need.quorum': '{yes} for to pass · the quorum blocks it',
'vote.need.first.one': '{yes} for out of {quorum} voter to pass',
@@ -259,6 +276,8 @@ export const en: Messages = {
'vote.stale': 'The text has changed in substance since you voted: your vote no longer counts until you reconfirm it.',
'vote.reconfirm': 'Reconfirm: {choice}',
'vote.private': 'Nobody sees your vote: only the totals are visible.',
'vote.permanent': 'The vote stays open, with no deadline: you can change or withdraw your vote at any time; the last one is the one that counts.',
'vote.rival': 'A text is already in force: to replace it, this version must also reach 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.',
'vote.held': 'An objection is holding this version back: until it is lifted, nobody can vote for it any more. You can vote against it or take your vote back; once lifted, it enters into force only with the votes still there.',
'vote.held.redit': 'It repeats “{other}”.',
'vote.held.contredit': 'It contradicts “{other}”.',
@@ -299,7 +318,7 @@ export const en: Messages = {
'quorum.heading.other': 'Quorum · {n} voters',
'quorum.default.one': 'By default, that of its level: {n} voter.',
'quorum.default.other': 'By default, that of its level: {n} voters.',
'quorum.explain': 'You set the quorum of this article as long as nobody has voted. Below it, the missing voters count as against.',
'quorum.explain': 'You set the quorum of this article as long as it keeps no trace of any vote. Below it, the missing voters count as against.',
'quorum.field': 'Quorum of the article',
'quorum.set': 'Set this quorum',
'quorum.reset': 'Back to the default ({n})',
@@ -404,7 +423,7 @@ export const en: Messages = {
'ballots.verify.unreadable': 'This is not a readable vote receipt.',
'inertia.title': 'Inertia',
'inertia.explain': 'The higher the inertia, the more votes it takes to change this text. Below the quorum, the missing voters count as against; beyond it, the share of votes in favour asked for falls as turnout rises.',
'inertia.explain': "The higher the inertia, the more votes it takes to change this text. Below the article's quorum, the missing voters count as against; beyond it, the share asked falls as turnout rises.",
'inertia.change': 'Propose an inertia change',
'inertia.change.to': 'New inertia',
'inertia.change.note': "A change of inertia is itself voted at the article's inertia: {level}.",
@@ -420,38 +439,42 @@ export const en: Messages = {
'formula.W.wot': 'members of the Ğ1 web of trust',
'formula.W.group': 'group members',
'formula.T': 'T — votes cast: {n}',
'formula.Tprime': 'T′ — voters counted: {n}, T or Q if some are missing',
'formula.M': 'M — majority at full turnout: {n}%',
'formula.G': 'G — gradient: {n}',
'formula.G': "G — gradient: {n}, derived from the group's usual turnout",
'formula.B': 'B — base: {n}',
'formula.B.wot': 'Raised to the power W, B is so small that it does not count in the web of trust.',
'formula.Q': 'Q — quorum: {n}',
'formula.R': 'R — current threshold: {n} votes in favour',
'formula.reading': 'Below the quorum Q, the missing voters count as against. 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.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.",
'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.',
'settings.usual': 'Usual turnout',
'settings.usual.median.one': '{n} voter out of {electorate} ({pct}): the median of the {sample} proposals voted on over the past year.',
'settings.usual.median.other': '{n} voters out of {electorate} ({pct}): the median of the {sample} proposals voted on over the past year.',
'settings.usual.reference.one': '{n} voter out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, until the group has voted on {min} proposals ({sample} so far).',
'settings.usual.reference.other': '{n} voters out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, until the group has voted on {min} proposals ({sample} so far).',
'settings.usual.reference.one': '{n} voter out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, while it rests on fewer than {min} proposals voted on over the past year ({sample} so far).',
'settings.usual.reference.other': '{n} voters out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, while it rests on fewer than {min} proposals voted on over the past year ({sample} so far).',
'settings.usual.bounded.one': 'Kept between {lo} and {hi} of the electorate: the group asks for its votes in favour as if {n} person voted ({pct}).',
'settings.usual.bounded.other': 'Kept between {lo} and {hi} of the electorate: the group asks for its votes in favour as if {n} people voted ({pct}).',
'settings.usual.when': 'Measured once a day, last on {date}.',
'settings.usual.never': 'Measured once a day.',
'settings.usual.quorum': 'It does not depend on the quorum of an article.',
'settings.levels': 'The five levels',
'settings.col.level': 'Level',
'settings.col.share': 'Share asked',
'settings.col.quorum': 'Quorum',
'settings.col.required': 'Votes for',
'settings.current': 'this article',
'settings.share.note.one': 'Share asked: the share of votes in favour asked, at {n} voter as usual.',
'settings.share.note.other': 'Share asked: the share of votes in favour asked, at {n} voters as usual.',
'settings.share.note': "Share asked: the one the level asks for beyond the article's quorum, at the usual turnout.",
'settings.quorum.note': "Quorum: below it, the missing voters count as against. It is the level's; the author of an article may set another one before the first vote.",
'settings.required.version.one': "Votes for: at {n} voter, this version's turnout.",
'settings.required.version.other': "Votes for: at {n} voters, this version's turnout.",
'settings.required.usual.one': 'Votes for: at {n} voter, as usual.',
'settings.required.usual.other': 'Votes for: at {n} voters, as usual.',
'settings.reading': 'At each level, the group asks for its share of votes in favour when it votes as usual; beyond the quorum, the more voters, the closer it comes to M. Below the quorum of the level, the missing voters count as against.',
'settings.required.version.one': "Votes for: at {n} voter, this version's turnout, or at the quorum if it is higher.",
'settings.required.version.other': "Votes for: at {n} voters, this version's turnout, or at the quorum if it is higher.",
'settings.required.usual.one': 'Votes for: at {n} voter, the usual turnout rounded, or at the quorum if it is higher.',
'settings.required.usual.other': 'Votes for: at {n} voters, the usual turnout rounded, or at the quorum if it is higher.',
'settings.reading': "Beyond the article's quorum, the more voters, the lower the share asked gets, down to the majority M when everyone votes. Below the article's quorum, the missing voters count as against, and it takes as many votes in favour as if that quorum had voted.",
'settings.gradient': "G is computed so that the share asked, beyond the article's quorum, is exactly that of the level at the usual turnout.",
'history.title': 'Corrections',
'history.initial': 'initial text',
+41 -18
View File
@@ -42,8 +42,9 @@ export const es: Messages = {
'signin.g1': 'Iniciar sesión con Ğ1',
'signin.g1.how': 'Se te redirige a connect.monnaie-libre.fr, el servicio de conexión del foro Monnaie libre. Allí apruebas la conexión en Ğecko (enlace o código QR) y luego vuelves aquí. No se nos transmite ninguna contraseña ni ninguna clave.',
'signin.g1.members': 'Reservado a los miembros de la red de confianza Ğ1.',
'signin.g1.pending': 'La conexión Ğ1 llega pronto: el registro de libreDecision en connect.monnaie-libre.fr está en curso. Mientras tanto, se entra con un enlace de invitación.',
'signin.g1.pending': 'La conexión Ğ1 llega pronto: el registro de libreDecision en connect.monnaie-libre.fr está en curso.',
'signin.invite': '¿Has recibido un enlace de invitación? Ábrelo sin más: te conecta.',
'signin.uninvited': '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.',
'signin.dev': 'Conexión de desarrollo',
'signin.dev.name': 'Nombre',
'signin.dev.admin': 'administrador de accesos',
@@ -103,8 +104,8 @@ export const es: Messages = {
'docs.inForce.other': '{n} artículos en vigor',
'docs.open.one': '{n} propuesta en curso',
'docs.open.other': '{n} propuestas en curso',
'docs.seeds.one': '{n} propuesta semilla',
'docs.seeds.other': '{n} propuestas semilla',
'docs.seeds.one': '{n} propuesta de arranque',
'docs.seeds.other': '{n} propuestas de arranque',
'doc.view.label': 'Leer',
'doc.view.inforce': 'En vigor',
@@ -125,7 +126,7 @@ export const es: Messages = {
'doc.seed.long': 'Entró en vigor sin votación, para no partir de una página en blanco. Sigue en votación, como todo texto.',
'doc.seed.votes.one': 'seed a minima · {n} voto',
'doc.seed.votes.other': 'seed a minima · {n} votos',
'doc.seedProposal': 'Propuesta seed',
'doc.seedProposal': 'Propuesta de arranque',
'doc.seedProposal.long': 'Texto de partida destacado para lanzar la votación. No está en vigor: entra en vigor por la votación, como toda propuesta.',
'doc.confirmed': 'confirmado por la votación',
'doc.adopted': 'adoptado el {date}',
@@ -134,7 +135,21 @@ export const es: Messages = {
'doc.basket': 'Votar en bloque',
'doc.basket.count': 'Votar en bloque · {n}',
'doc.empty': 'Ningún artículo está en vigor. Las propuestas de abajo suben en cuanto alcanzan su umbral.',
'doc.empty.seeds': 'Ningún artículo está aún en vigor. Empieza por las propuestas semilla: entran en vigor por el voto, como cualquier propuesta.',
'opening.title': 'Info de apertura, doc. de referencia',
'opening.none': 'Ningún artículo está aún en vigor.',
'opening.amorce': 'Las propuestas de arranque entran en vigor por el voto, como las propuestas que siguen, con menos inercia para que el documento cobre vida más fácilmente. Se recomienda aumentar después la inercia, por el voto, para reducir la frecuencia de los cambios.',
'opening.amorce.after': 'Las propuestas de arranque entran en vigor por el voto, como las propuestas que siguen, con menos inercia para que el documento cobre vida más fácilmente. Se recomienda aumentar después la inercia, por el voto, para reducir la frecuencia de los cambios.',
'opening.inertia': 'Inercia en la apertura',
'opening.inertia.amorce': 'Propuestas de arranque',
'opening.inertia.title': 'Título del documento',
'opening.inertia.orders': 'Orden de cada sección',
'opening.inertia.others': 'Demás artículos',
'opening.quorum': 'Quórum',
'opening.quorum.levels': '{list} votantes, del nivel 1 al nivel 5.',
'opening.quorum.same.one': '{n} votante en cada nivel.',
'opening.quorum.same.other': '{n} votantes en cada nivel.',
'opening.quorum.rule': 'Por debajo del quórum de un artículo, los votantes que faltan cuentan como «no».',
'opening.quorum.author': '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.',
'doc.others.one': '{n} otra versión en curso',
'doc.others.other': '{n} otras versiones en curso',
'doc.published': 'Publicado en el repositorio de textos',
@@ -202,6 +217,7 @@ export const es: Messages = {
'order.proposed.one': '{n} orden propuesto',
'order.proposed.other': '{n} órdenes propuestos',
'order.edit.note': 'Un orden se vota con la inercia {level}, como un artículo: desplegado, en la cesta, por la cadena Ğ1 en Monnaie libre.',
'order.edit.effect': 'Si entra en vigor, solo los artículos de esta sección cambian de lugar; ningún texto cambia.',
'order.submitted': 'Orden sometido a votación.',
'title.propose': 'Proponer otro título',
'title.propose.label': 'Nuevo título',
@@ -246,6 +262,7 @@ export const es: Messages = {
'vote.simulated.mark': 'voto simulado',
'vote.others': 'Otras versiones',
'vote.others.count': 'Otras versiones ({n})',
'vote.others.rule': 'Si varias versiones alcanzan su umbral, entra en vigor la que tiene el margen más amplio.',
'vote.need': '{yes} sí para aprobar · {no} no para bloquear',
'vote.need.quorum': '{yes} sí para aprobar · el quórum la bloquea',
'vote.need.first.one': '{yes} sí de {quorum} votante para aprobar',
@@ -259,6 +276,8 @@ export const es: Messages = {
'vote.stale': 'El texto ha cambiado de fondo desde tu voto: tu voto deja de contar hasta que lo reconfirmes.',
'vote.reconfirm': 'Reconfirmar: {choice}',
'vote.private': 'Tu voto no se muestra a nadie: solo se ven los totales.',
'vote.permanent': 'La votación sigue abierta, sin plazo: puedes cambiar o retirar tu voto en cualquier momento; el último es el que cuenta.',
'vote.rival': 'Ya hay un texto en vigor: para sustituirlo, esta versión 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.',
'vote.held': 'Una objeción retiene esta versión: hasta que se levante, nadie puede votar a favor. Puedes votar en contra o retirar tu voto; al levantarse, solo entra en vigor con los votos que sigan ahí.',
'vote.held.redit': 'Repite «{other}».',
'vote.held.contredit': 'Contradice «{other}».',
@@ -299,7 +318,7 @@ export const es: Messages = {
'quorum.heading.other': 'Quórum · {n} votantes',
'quorum.default.one': 'Por defecto, el de su nivel: {n} votante.',
'quorum.default.other': 'Por defecto, el de su nivel: {n} votantes.',
'quorum.explain': 'Fijas el quórum de este artículo mientras nadie haya votado. Por debajo, los votantes que faltan cuentan como «no».',
'quorum.explain': 'Fijas el quórum de este artículo mientras no guarde rastro de ningún voto. Por debajo, los votantes que faltan cuentan como «no».',
'quorum.field': 'Quórum del artículo',
'quorum.set': 'Fijar este quórum',
'quorum.reset': 'Volver al valor por defecto ({n})',
@@ -404,7 +423,7 @@ export const es: Messages = {
'ballots.verify.unreadable': 'No es un recibo de voto legible.',
'inertia.title': 'Inercia',
'inertia.explain': 'Cuanto más alta es la inercia, más votos hacen falta para cambiar este texto. Por debajo del quórum, los votantes que faltan cuentan como «no»; más allá, la proporción de «sí» pedida baja cuando sube la participación.',
'inertia.explain': 'Cuanto más alta es la inercia, más votos hacen falta para cambiar este texto. Por debajo del quórum del artículo, los votantes que faltan cuentan como «no»; más allá, la proporción exigida baja cuando sube la participación.',
'inertia.change': 'Proponer un cambio de inercia',
'inertia.change.to': 'Nueva inercia',
'inertia.change.note': 'Un cambio de inercia se vota a su vez con la inercia del artículo: {level}.',
@@ -420,38 +439,42 @@ export const es: Messages = {
'formula.W.wot': 'miembros de la red de confianza Ğ1',
'formula.W.group': 'miembros del grupo',
'formula.T': 'T — votos emitidos: {n}',
'formula.Tprime': 'T′ — votantes contados: {n}, T o Q si faltan',
'formula.M': 'M — mayoría con participación plena: {n} %',
'formula.G': 'G — gradiente: {n}',
'formula.G': 'G — gradiente: {n}, deducido de la participación habitual del grupo',
'formula.B': 'B — base: {n}',
'formula.B.wot': 'Elevada a la potencia W, B es tan pequeña que no cuenta en la red de confianza.',
'formula.Q': 'Q — quórum: {n}',
'formula.R': 'R — umbral actual: {n} votos a favor',
'formula.reading': 'Por debajo del quórum Q, los votantes que faltan cuentan como «no». 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.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.',
'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.',
'settings.usual': 'Participación habitual',
'settings.usual.median.one': '{n} votante de {electorate} ({pct}): la mediana de las {sample} propuestas votadas en el último año.',
'settings.usual.median.other': '{n} votantes de {electorate} ({pct}): la mediana de las {sample} propuestas votadas en el último año.',
'settings.usual.reference.one': '{n} votante de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).',
'settings.usual.reference.other': '{n} votantes de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).',
'settings.usual.reference.one': '{n} votante de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, mientras se base en menos de {min} propuestas votadas en el último año ({sample} por ahora).',
'settings.usual.reference.other': '{n} votantes de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, mientras se base en menos de {min} propuestas votadas en el último año ({sample} por ahora).',
'settings.usual.bounded.one': 'Mantenida entre el {lo} y el {hi} del electorado: el grupo pide sus votos a favor como si votara {n} persona ({pct}).',
'settings.usual.bounded.other': 'Mantenida entre el {lo} y el {hi} del electorado: el grupo pide sus votos a favor como si votaran {n} personas ({pct}).',
'settings.usual.when': 'Medida una vez al día, la última el {date}.',
'settings.usual.never': 'Medida una vez al día.',
'settings.usual.quorum': 'No depende del quórum de un artículo.',
'settings.levels': 'Los cinco niveles',
'settings.col.level': 'Nivel',
'settings.col.share': 'Proporción exigida',
'settings.col.quorum': 'Quórum',
'settings.col.required': 'Votos a favor',
'settings.current': 'este artículo',
'settings.share.note.one': 'Proporción exigida: la parte de votos a favor pedida, con {n} votante como de costumbre.',
'settings.share.note.other': 'Proporción exigida: la parte de votos a favor pedida, con {n} votantes como de costumbre.',
'settings.share.note': 'Proporción exigida: la que pide el nivel más allá del quórum del artículo, con la participación habitual.',
'settings.quorum.note': 'Quórum: por debajo, los votantes que faltan cuentan como «no». Es el del nivel; el autor de un artículo puede fijar otro antes del primer voto.',
'settings.required.version.one': 'Votos a favor: con {n} votante, la participación de esta versión.',
'settings.required.version.other': 'Votos a favor: con {n} votantes, la participación de esta versión.',
'settings.required.usual.one': 'Votos a favor: con {n} votante, como de costumbre.',
'settings.required.usual.other': 'Votos a favor: con {n} votantes, como de costumbre.',
'settings.reading': 'En cada nivel, el grupo pide su parte de votos a favor cuando vota como de costumbre; más allá del quórum, cuantos más votantes, más se acerca a M. Por debajo del quórum del nivel, los votantes que faltan cuentan como «no».',
'settings.required.version.one': 'Votos a favor: con {n} votante, la participación de esta versión, o con el quórum si es más alto.',
'settings.required.version.other': 'Votos a favor: con {n} votantes, la participación de esta versión, o con el quórum si es más alto.',
'settings.required.usual.one': 'Votos a favor: con {n} votante, la participación habitual redondeada, o con el quórum si es más alto.',
'settings.required.usual.other': 'Votos a favor: con {n} votantes, la participación habitual redondeada, o con el quórum si es más alto.',
'settings.reading': 'Más allá del quórum del artículo, cuantos más votantes, más baja la proporción exigida, hasta la mayoría M cuando vota todo el mundo. Por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum.',
'settings.gradient': 'G se calcula para que la proporción exigida, más allá del quórum del artículo, sea exactamente la del nivel con la participación habitual.',
'history.title': 'Correcciones',
'history.initial': 'texto inicial',
+41 -18
View File
@@ -46,8 +46,9 @@ export const fr = {
'signin.g1': 'Se connecter avec Ğ1',
'signin.g1.how': "Tu es envoyé sur connect.monnaie-libre.fr, le service de connexion du forum Monnaie libre. Tu y approuves la connexion dans Ğecko (lien ou QR code), puis tu reviens ici. Aucun mot de passe, aucune clé ne nous est transmise.",
'signin.g1.members': 'Réservé aux membres de la toile de confiance Ğ1.',
'signin.g1.pending': "La connexion Ğ1 arrive : l'inscription de libreDecision auprès de connect.monnaie-libre.fr est en cours. En attendant, on entre avec un lien d'invitation.",
'signin.g1.pending': "La connexion Ğ1 arrive : l'inscription de libreDecision auprès de connect.monnaie-libre.fr est en cours.",
'signin.invite': "Tu as reçu un lien d'invitation ? Ouvre-le simplement : il te connecte.",
'signin.uninvited': "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.",
'signin.dev': 'Connexion de développement',
'signin.dev.name': 'Nom',
'signin.dev.admin': 'administrateur des accès',
@@ -107,8 +108,8 @@ export const fr = {
'docs.inForce.other': '{n} articles en vigueur',
'docs.open.one': '{n} proposition en cours',
'docs.open.other': '{n} propositions en cours',
'docs.seeds.one': '{n} Proposition seed',
'docs.seeds.other': '{n} Propositions seed',
'docs.seeds.one': "{n} Proposition d'amorce",
'docs.seeds.other': "{n} Propositions d'amorce",
'doc.view.label': 'Lire',
'doc.view.inforce': 'En vigueur',
@@ -129,7 +130,7 @@ export const fr = {
'doc.seed.long': "Entré en vigueur sans vote, pour ne pas partir d'une page blanche. Il reste sous vote, comme tout texte.",
'doc.seed.votes.one': 'seed a minima · {n} vote',
'doc.seed.votes.other': 'seed a minima · {n} votes',
'doc.seedProposal': 'Proposition seed',
'doc.seedProposal': "Proposition d'amorce",
'doc.seedProposal.long': "Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.",
'doc.confirmed': 'confirmé par le vote',
'doc.adopted': 'adopté le {date}',
@@ -138,7 +139,21 @@ export const fr = {
'doc.basket': 'Voter en lot',
'doc.basket.count': 'Voter en lot · {n}',
'doc.empty': "Aucun article n'est en vigueur. Les propositions ci-dessous remontent dès qu'elles atteignent leur seuil.",
'doc.empty.seeds': "Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.",
'opening.title': 'Info ouverture doc de réf.',
'opening.none': "Aucun article n'est encore en vigueur.",
'opening.amorce': "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.",
'opening.amorce.after': "Les Propositions d'amorce entrent en vigueur 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.",
'opening.inertia': "Inertie à l'ouverture",
'opening.inertia.amorce': "Propositions d'amorce",
'opening.inertia.title': 'Titre du document',
'opening.inertia.orders': 'Ordre de chaque section',
'opening.inertia.others': 'Autres articles',
'opening.quorum': 'Quorum',
'opening.quorum.levels': '{list} votants, du cran 1 au cran 5.',
'opening.quorum.same.one': '{n} votant à chaque cran.',
'opening.quorum.same.other': '{n} votants à chaque cran.',
'opening.quorum.rule': "Sous le quorum d'un article, les votants qui manquent comptent comme « non ».",
'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.",
'doc.others.one': '{n} autre version en cours',
'doc.others.other': '{n} autres versions en cours',
'doc.published': 'Publié sur le dépôt des textes',
@@ -206,6 +221,7 @@ export const fr = {
'order.proposed.one': '{n} ordre proposé',
'order.proposed.other': '{n} ordres proposés',
'order.edit.note': "Un ordre se vote à l'inertie {level}, comme un article : déplié, dans le panier, par la chaîne Ğ1 dans Monnaie libre.",
'order.edit.effect': "S'il entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change.",
'order.submitted': 'Ordre proposé au vote.',
'title.propose': 'Proposer un autre titre',
'title.propose.label': 'Nouveau titre',
@@ -250,6 +266,7 @@ export const fr = {
'vote.simulated.mark': 'vote simulé',
'vote.others': 'Autres versions',
'vote.others.count': 'Autres versions ({n})',
'vote.others.rule': 'Si plusieurs versions atteignent leur seuil, celle qui a la marge la plus large entre en vigueur.',
'vote.need': '{yes} oui pour passer · {no} non pour bloquer',
'vote.need.quorum': '{yes} oui pour passer · le quorum la bloque',
'vote.need.first.one': '{yes} oui sur {quorum} votant pour passer',
@@ -263,6 +280,8 @@ export const fr = {
'vote.stale': "Le texte a changé de fond depuis ton vote : ton vote ne compte plus tant que tu ne l'as pas reconfirmé.",
'vote.reconfirm': 'Reconfirmer : {choice}',
'vote.private': 'Ton vote ne se montre à personne : seuls les totaux sont visibles.',
'vote.permanent': 'Le vote reste ouvert, sans échéance : tu peux changer ou retirer ton vote à tout moment, le dernier fait foi.',
'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.',
'vote.held': "Une objection retient cette version : jusqu'à sa levée, personne ne peut plus voter pour. Tu peux voter contre ou retirer ton vote ; à la levée, elle n'entre en vigueur qu'avec les voix encore là.",
'vote.held.redit': 'Elle redit « {other} ».',
'vote.held.contredit': 'Elle contredit « {other} ».',
@@ -303,7 +322,7 @@ export const fr = {
'quorum.heading.other': 'Quorum · {n} votants',
'quorum.default.one': 'Par défaut, celui de son cran : {n} votant.',
'quorum.default.other': 'Par défaut, celui de son cran : {n} votants.',
'quorum.explain': "Tu fixes le quorum de cet article tant que personne n'a voté. En dessous, les votants qui manquent comptent comme « non ».",
'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.field': "Quorum de l'article",
'quorum.set': 'Fixer ce quorum',
'quorum.reset': 'Revenir au défaut ({n})',
@@ -408,7 +427,7 @@ export const fr = {
'ballots.verify.unreadable': "Ce n'est pas un reçu de vote lisible.",
'inertia.title': 'Inertie',
'inertia.explain': "Plus l'inertie est haute, plus il faut de voix pour changer ce texte. Sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part de « pour » demandée baisse quand la participation monte.",
'inertia.explain': "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.",
'inertia.change': "Proposer un changement d'inertie",
'inertia.change.to': 'Nouvelle inertie',
'inertia.change.note': "Un changement d'inertie se vote lui-même à l'inertie de l'article : {level}.",
@@ -424,38 +443,42 @@ export const fr = {
'formula.W.wot': 'membres de la toile de confiance Ğ1',
'formula.W.group': 'membres du groupe',
'formula.T': 'T — voix exprimées : {n}',
'formula.Tprime': "T′ — votants retenus : {n}, T ou Q s'il en manque",
'formula.M': 'M — majorité à pleine participation : {n} %',
'formula.G': 'G — gradient : {n}',
'formula.G': 'G — gradient : {n}, déduit de la participation habituelle du groupe',
'formula.B': 'B — base : {n}',
'formula.B.wot': "À la puissance W, B est si petit qu'il ne compte pas dans la toile de confiance.",
'formula.Q': 'Q — quorum : {n}',
'formula.R': 'R — seuil actuel : {n} voix pour',
'formula.reading': "Sous le quorum Q, les votants qui manquent comptent comme « non ». 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.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.",
'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.",
'settings.usual': 'Participation habituelle',
'settings.usual.median.one': '{n} votant sur {electorate} ({pct}) : la médiane des {sample} propositions votées depuis un an.',
'settings.usual.median.other': '{n} votants sur {electorate} ({pct}) : la médiane des {sample} propositions votées depuis un an.',
'settings.usual.reference.one': "{n} votant sur {electorate} ({pct}), deux fois {q}, le quorum de référence : la participation de référence, tant que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).",
'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 que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).",
'settings.usual.reference.one': "{n} votant 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).",
'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).",
'settings.usual.bounded.one': "Ramenée entre {lo} et {hi} de l'électorat : le groupe demande ses voix pour comme si {n} personne votait ({pct}).",
'settings.usual.bounded.other': "Ramenée entre {lo} et {hi} de l'électorat : le groupe demande ses voix pour comme si {n} personnes votaient ({pct}).",
'settings.usual.when': 'Mesurée une fois par jour, la dernière fois le {date}.',
'settings.usual.never': 'Mesurée une fois par jour.',
'settings.usual.quorum': "Elle ne dépend pas du quorum d'un article.",
'settings.levels': 'Les cinq crans',
'settings.col.level': 'Cran',
'settings.col.share': 'Part exigée',
'settings.col.quorum': 'Quorum',
'settings.col.required': 'Voix pour',
'settings.current': 'cet article',
'settings.share.note.one': "Part exigée : la part de « pour » demandée, à {n} votant comme d'habitude.",
'settings.share.note.other': "Part exigée : la part de « pour » demandée, à {n} votants comme d'habitude.",
'settings.share.note': "Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle.",
'settings.quorum.note': "Quorum : en dessous, les votants qui manquent comptent comme « non ». C'est celui du cran ; l'auteur d'un article peut en fixer un autre avant le premier vote.",
'settings.required.version.one': 'Voix pour : à {n} votant, la participation de cette version.',
'settings.required.version.other': 'Voix pour : à {n} votants, la participation de cette version.',
'settings.required.usual.one': "Voix pour : à {n} votant, comme d'habitude.",
'settings.required.usual.other': "Voix pour : à {n} votants, comme d'habitude.",
'settings.reading': "À chaque cran, le groupe demande sa part de « pour » quand il vote comme d'habitude ; au-delà du quorum, plus il y a de votants, plus elle se rapproche de M. Sous le quorum du cran, les votants qui manquent comptent comme « non ».",
'settings.required.version.one': "Voix pour : à {n} votant, la participation de cette version, ou au quorum s'il est plus haut.",
'settings.required.version.other': "Voix pour : à {n} votants, la participation de cette version, ou au quorum s'il est plus haut.",
'settings.required.usual.one': "Voix pour : à {n} votant, la participation habituelle arrondie, ou au quorum s'il est plus haut.",
'settings.required.usual.other': "Voix pour : à {n} votants, la participation habituelle arrondie, ou au quorum s'il est plus haut.",
'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é.",
'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.",
'history.title': 'Corrections',
'history.initial': 'texte initial',
+13
View File
@@ -18,6 +18,19 @@ export function sectionLabel(s: string | null | undefined): string | null {
return s && SECTIONS.includes(s) ? t(`section.${s}` as Key) : null;
}
/**
* The sections whose articles carry the title already (Yvv, 2026-11-03: « Enlève les titres
* Introduction et Engagements fondamentaux, les titres sont déjà dans les contenus à voter. Ça
* embrouille. »): not named again where the text is read. They keep their name elsewhere — the
* order of the section, the basket, the open proposals.
*/
const TITLED_BY_THEIR_ARTICLES = ['introduction', 'fondamental'];
/** The name of a section where the text is read, or null. */
export function readerSectionLabel(s: string | null | undefined): string | null {
return s && TITLED_BY_THEIR_ARTICLES.includes(s) ? null : sectionLabel(s);
}
/** The name of a module (ADR 0009): « Titre du document », « Ordre des articles · Conclusion ». */
export function moduleName(unit: Unit): string {
if (unit.kind === 'title') return t('module.title.name');
+52
View File
@@ -0,0 +1,52 @@
// « Info ouverture doc de réf. » (lot U3; Yvv, 2026-11-03: « un petit chapitre qui explicite les
// réglages initiaux de l'auteur, notamment Quorum et Inertie »): what the chapter says is read in
// the data of the document, never written in. Pure: the component only says it.
import type { DocView, Inertia, Unit } from './types';
/**
* The level a module opened at: the one before the first change voted. A change of the seed's own
* settings (an amendment, « réglage de départ du texte ») is the author's, part of the opening.
*/
export function openedAt(u: Unit): Inertia {
const voted = u.inertiaChanges.find((c) => c.cause === 'vote');
return voted?.from ?? u.inertia;
}
/** A Proposition d'amorce (ADR 0011): put forward by the seed, open or in force since. */
export function isAmorce(u: Unit): boolean {
return [u.inForce, ...u.proposals].some((p) => p?.seedFeatured === true);
}
/** An article the seed wrote (the author's): not one a member proposed since. */
function fromSeed(u: Unit): boolean {
return [u.inForce, ...u.proposals].some((p) => p?.origin === 'seed');
}
const levels = (units: (Unit | null | undefined)[]): Inertia[] =>
[...new Set(units.filter((u): u is Unit => !!u).map(openedAt))].sort((a, b) => a - b);
export interface OpeningLevels {
amorce: Inertia[];
title: Inertia[];
orders: Inertia[];
others: Inertia[];
}
/** The levels at the opening, by kind of module, each said once, lowest first. */
export function openingLevels(view: Pick<DocView, 'units' | 'modules'>): OpeningLevels {
const seeded = view.units.filter(fromSeed);
return {
amorce: levels(seeded.filter(isAmorce)),
title: levels([view.modules.title]),
orders: levels(view.modules.orders),
others: levels(seeded.filter((u) => !isAmorce(u))),
};
}
/** The quorum of each level, 1 to 5 (ADR 0013): said once when they are all the same. */
export function quorumsOf(quorums: number[] | undefined): { same: number | null; list: number[] } | null {
if (!quorums?.length) return null;
const first = quorums[0] ?? 0;
return { same: quorums.every((q) => q === first) ? first : null, list: quorums };
}
+2
View File
@@ -117,6 +117,8 @@ export interface Proposal {
seedMinimal: boolean;
/** A seed proposal (ADR 0011): open, put forward by the seed to start the vote, not in force. */
seedProposal?: boolean;
/** Put forward by the seed (ADR 0011), in force since or not: a Proposition d'amorce stays one (lot U3). */
seedFeatured?: boolean;
/** The totals shown: recorded, pending and simulated votes, each intention in place of
* the vote its session sees as its own (ADR 0012). */
tally: Tally;
+15 -1
View File
@@ -27,11 +27,18 @@
{#if session.me}
<GroupChoice />
{:else}
{:else if session.ready && session.g1}
<div class="enter">
<a class="btn primary" href="/connexion">{t('home.signin')}</a>
<p class="faint">{t('home.signin.note')}</p>
</div>
{:else if session.ready}
<!-- The Ğ1 sign-in not active yet: whoever is not invited reads how to get in (Yvv,
2026-11-03), in place of a button that would lead to a door still shut. -->
<div class="enter">
<p class="uninvited">{t('signin.uninvited')}</p>
<p class="faint">{t('signin.invite')}</p>
</div>
{/if}
</div>
@@ -138,6 +145,13 @@
font-size: 0.9rem;
max-width: 34ch;
}
.enter .uninvited {
max-width: 40ch;
font-family: var(--font-text);
font-size: 1.02rem;
line-height: 1.6;
color: var(--ink);
}
/* The keystone sets the arch, then the three points come under it. */
@media (prefers-reduced-motion: no-preference) {
+2
View File
@@ -42,6 +42,8 @@
{:else}
<button class="btn primary big" type="button" disabled>{t('signin.g1')}</button>
<p>{t('signin.g1.pending')}</p>
<!-- Yvv, 2026-11-03: how whoever is not invited gets in, said once. -->
<p>{t('signin.uninvited')}</p>
{/if}
</section>
@@ -12,6 +12,7 @@
import DocPanel from '$lib/components/DocPanel.svelte';
import Guard from '$lib/components/Guard.svelte';
import Icon from '$lib/components/Icon.svelte';
import OpeningInfo from '$lib/components/OpeningInfo.svelte';
import OrderLine from '$lib/components/OrderLine.svelte';
import Reorder from '$lib/components/Reorder.svelte';
import TitleBar from '$lib/components/TitleBar.svelte';
@@ -21,7 +22,7 @@
import { sections, votableIds } from '$lib/basket';
import { DocState, panelSearch, shown, TABS, type Entry, type PanelState, type View } from '$lib/doc.svelte';
import { i18n, LANGS, t } from '$lib/i18n/index.svelte';
import { sectionLabel } from '$lib/labels';
import { readerSectionLabel } from '$lib/labels';
import { answerPanel } from '$lib/objections';
import { BasketState } from '$lib/picks.svelte';
import { session } from '$lib/session.svelte';
@@ -259,12 +260,18 @@
{#if doc.mode !== 'inforce'}<p class="note preview">{t(`doc.view.${doc.mode}.note`)}</p>{/if}
</header>
<!-- Before the text, out of the vote: the settings of the author at the opening (lot U3); then a
separation, a soft line of the mood, never a border; then the first article. -->
<OpeningInfo {view} inForce={doc.inForce.length} />
<hr class="opening-rule" />
<section class="text" aria-label={t('doc.view.inforce')}>
{#if reading}
<!-- Nothing in force (ADR 0011): the seed proposals at the head, said once. -->
{#if !doc.inForce.length}<p class="lead">{t(textSections.length ? 'doc.empty.seeds' : 'doc.empty')}</p>{/if}
<!-- Nothing in force and nothing to start from: said once. With the Propositions d'amorce
at the head (ADR 0011), the chapter says it, in Yvv's words. -->
{#if !doc.inForce.length && !textSections.length}<p class="lead">{t('doc.empty')}</p>{/if}
{#each textSections as s, i (`${i}:${s.section}`)}
{@const label = sectionLabel(s.section)}
{@const label = readerSectionLabel(s.section)}
<div class="text-section">
{#if label}<p class="section-name">{label}</p>{/if}
{#if !s.order || reordering !== s.order.id}
@@ -469,7 +476,22 @@
text-underline-offset: 3px;
}
.text {
margin-top: 2.2rem;
margin-top: 0;
}
/* The separation before the text (Yvv, 2026-11-03): a soft line in the keystone's colour of the
mood, fading at its ends — drawn by its background, never a border. */
.opening-rule {
height: 2px;
margin: 2.2rem 0 1.6rem;
border: 0;
border-radius: 2px;
background: linear-gradient(
to right,
transparent,
color-mix(in srgb, var(--cle) 45%, transparent) 18%,
color-mix(in srgb, var(--cle) 45%, transparent) 82%,
transparent
);
}
.lead {
margin-bottom: 1.2rem;
+3 -3
View File
@@ -16,7 +16,7 @@ describe('five levels of inertia', () => {
});
it('the threshold is told with the quorum, no longer with C (ADR 0013)', () => {
const told = [
'deep.note', 'formula.reading', 'settings.reading', 'settings.share.note.one', 'settings.share.note.other',
'deep.note', 'formula.reading', 'settings.reading', 'settings.share.note',
'settings.usual.reference.one', 'settings.usual.reference.other',
] as const;
for (const cat of [fr, en, es]) {
@@ -36,10 +36,10 @@ describe('five levels of inertia', () => {
expect(cat['quorum.error.voted']).not.toMatch(/ne change plus|no longer changes|ya no cambia/);
}
expect(fr['inertia.explain']).toBe(
"Plus l'inertie est haute, plus il faut de voix pour changer ce texte. Sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part de « pour » demandée baisse quand la participation monte.",
"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.",
);
expect(fr['formula.reading']).toBe(
"Sous le quorum Q, les votants qui manquent comptent comme « non ». 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 majorité M suffit.",
);
expect(fr['quorum.error.voted']).toBe("Quelqu'un a déjà voté sur cet article : son quorum ne change pas.");
});
+4 -3
View File
@@ -60,9 +60,10 @@ describe('anti-lexicon', () => {
describe('the reading of the formula', () => {
it('the settings say, like N1.1 and the formula, that the share falls beyond the quorum', () => {
expect(fr['formula.reading']).toContain('Au-delà');
expect(fr['settings.reading']).toContain('au-delà du quorum');
expect(en['settings.reading']).toContain('beyond the quorum');
expect(es['settings.reading']).toContain('más allá del quórum');
// Lot U3: the quorum of the article, the defined term of lot T3c.
expect(fr['settings.reading']).toContain("Au-delà du quorum de l'article");
expect(en['settings.reading']).toContain("Beyond the article's quorum");
expect(es['settings.reading']).toContain('Más allá del quórum del artículo');
});
});
+130
View File
@@ -0,0 +1,130 @@
// Lot U3 — « Info ouverture doc de réf. » (Yvv, 2026-11-03: « un petit chapitre qui explicite les
// réglages initiaux de l'auteur, notamment Quorum et Inertie »): what the chapter says is read in
// the data of the document, never written in. The level a module opened at is the one before any
// change voted (a change of the seed's settings, an amendment, is the author's); a Proposition
// d'amorce stays one once it has entered into force (`seedFeatured`).
import { describe, expect, it } from 'vitest';
import { isAmorce, openedAt, openingLevels, quorumsOf } from '../../src/lib/opening';
import type { DocView, Inertia, InertiaChange, Proposal, Unit } from '../../src/lib/types';
function version(over: Partial<Proposal> = {}): Proposal {
return {
id: 'p',
unitId: 'u',
kind: 'text',
inertiaTarget: null,
title: 'Un article',
body: '',
status: 'open',
origin: 'seed',
label: null,
author: null,
mine: false,
revision: 1,
createdAt: '2026-11-03T00:00:00Z',
updatedAt: '2026-11-03T00:00:00Z',
submittedAt: null,
enteredAt: null,
confirmedAt: null,
seedMinimal: false,
seedProposal: false,
seedFeatured: false,
tally: { for: 0, against: 0 },
tallyPending: { for: 0, against: 0 },
tallySimulated: { for: 0, against: 0 },
evaluation: null,
votedAtInertia: 3,
blocked: false,
votable: true,
objections: 0,
myVote: null,
myVoteState: null,
myVoteStale: false,
myVoteChain: false,
myBallot: null,
translations: {},
...over,
};
}
let n = 0;
function unit(inertia: Inertia, over: Partial<Unit> = {}, versions: Partial<Proposal>[] = [{}]): Unit {
n += 1;
return {
id: `u${n}`,
code: `X${n}`,
section: 'technique',
position: n,
inertia,
inertiaChanges: [],
inForce: null,
proposals: versions.map((v) => version({ id: `p${n}`, ...v })),
...over,
};
}
const change = (from: Inertia, to: Inertia, cause: InertiaChange['cause']): InertiaChange => ({ from, to, cause, at: '2026-11-03T00:00:00Z' });
describe('the level a module opened at', () => {
it('is its level when nothing changed it', () => {
expect(openedAt(unit(3))).toBe(3);
});
it('is the author’s: a change of the seed’s settings (an amendment) is part of the opening', () => {
expect(openedAt(unit(2, { inertiaChanges: [change(3, 2, 'amendment')] }))).toBe(2);
});
it('is the one before the first change voted, whatever came after', () => {
expect(openedAt(unit(4, { inertiaChanges: [change(2, 4, 'vote')] }))).toBe(2);
expect(openedAt(unit(5, { inertiaChanges: [change(3, 2, 'amendment'), change(2, 4, 'vote'), change(4, 5, 'vote')] }))).toBe(2);
});
});
describe('a Proposition d’amorce', () => {
it('is the article the seed put forward, open or in force since', () => {
expect(isAmorce(unit(2, {}, [{ seedProposal: true, seedFeatured: true }]))).toBe(true);
expect(isAmorce(unit(2, { inForce: version({ status: 'in_force', seedFeatured: true, enteredAt: '2026-11-03T00:00:00Z' }) }, []))).toBe(true);
expect(isAmorce(unit(3))).toBe(false);
});
it('a server that does not say it yet makes no article an amorce', () => {
const old = version();
delete (old as Partial<Proposal>).seedFeatured;
expect(isAmorce(unit(3, { proposals: [old] }))).toBe(false);
});
});
describe('the levels at the opening, by kind of module', () => {
function view(units: Unit[], title: Unit | null, orders: Unit[]): Pick<DocView, 'units' | 'modules'> {
return { units, modules: { title, orders } };
}
it('the amorces, the title, the order of each section, the other articles of the seed', () => {
const v = view(
[
unit(2, {}, [{ seedProposal: true, seedFeatured: true }]),
unit(3, { inertiaChanges: [change(2, 3, 'vote')], inForce: version({ status: 'in_force', seedFeatured: true }) }, []),
unit(3),
unit(4, { inertiaChanges: [change(3, 4, 'vote')] }),
// A member's article is not a setting of the author.
unit(5, {}, [{ origin: 'member' }]),
],
unit(4, { kind: 'title' }),
[unit(2, { kind: 'order', section: 'introduction' }), unit(3, { kind: 'order', section: 'technique', inertiaChanges: [change(2, 3, 'vote')] })],
);
expect(openingLevels(v)).toEqual({ amorce: [2], title: [4], orders: [2], others: [3] });
});
it('several levels in one kind are all said, lowest first; a kind that is absent says none', () => {
const v = view([unit(4), unit(3), unit(4)], null, []);
expect(openingLevels(v)).toEqual({ amorce: [], title: [], orders: [], others: [3, 4] });
});
});
describe('the quorum of each level', () => {
it('said once when every level has the same', () => {
expect(quorumsOf([1, 1, 1, 1, 1])).toEqual({ same: 1, list: [1, 1, 1, 1, 1] });
});
it('listed from level 1 to level 5 otherwise', () => {
expect(quorumsOf([12, 12, 50, 50, 50])).toEqual({ same: null, list: [12, 12, 50, 50, 50] });
});
it('nothing when the server does not say them', () => {
expect(quorumsOf(undefined)).toBeNull();
expect(quorumsOf([])).toBeNull();
});
});
+141
View File
@@ -0,0 +1,141 @@
// 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 { 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).
[
'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_…',
],
// 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/);
}
});
});