Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e27dd999f | ||
|
|
b28f1573c9 | ||
|
|
8ac9d835e3 | ||
|
|
a43a0989c1 | ||
|
|
521ff27b05 | ||
|
|
fadfff5564 | ||
|
|
7819187b0b | ||
|
|
266a443dc6 | ||
|
|
a03473a01e | ||
|
|
7d8be18c3b | ||
|
|
8b92837327 | ||
|
|
842a546796 | ||
|
|
0e266241d0 | ||
|
|
d66823b2f6 | ||
|
|
1938496cea | ||
|
|
db6041d32f | ||
|
|
a6f5e22bea | ||
|
|
848968b154 | ||
|
|
7425766640 | ||
|
|
233cc45ff8 | ||
|
|
0d087d2b81 | ||
|
|
c626f1aded | ||
|
|
5730d782fe | ||
|
|
a47aed0f72 | ||
|
|
a6000f92ae | ||
|
|
d15d46e4fc | ||
|
|
8528a269fc | ||
|
|
9d478d668a | ||
|
|
7734e2dbc7 | ||
|
|
2bbd017087 | ||
|
|
d6cf8fdfed | ||
|
|
590ccd7267 | ||
|
|
87730d670d | ||
|
|
3996f6f99d | ||
|
|
093d1ca471 | ||
|
|
dda1ca85dd | ||
|
|
4365ed607a | ||
|
|
b26c00fbd6 | ||
|
|
27995b9eee | ||
|
|
8d6c35da56 | ||
|
|
d9a583c223 | ||
|
|
4f070e7b77 | ||
|
|
9d366c9afe | ||
|
|
7be6396186 | ||
|
|
ef36861953 | ||
|
|
cc8ba88d02 | ||
|
|
c976f33fa5 | ||
|
|
416364475f | ||
|
|
591ed37837 | ||
|
|
de1b7eb5d7 | ||
|
|
3b38280169 | ||
|
|
42c803e544 | ||
|
|
077c87c9db | ||
|
|
3383f28739 | ||
|
|
ca0c729481 | ||
|
|
aaacfaa332 | ||
|
|
77bc79d9ed | ||
|
|
d17a5f6d92 | ||
|
|
e7ab82f48f | ||
|
|
a8939b20c5 | ||
|
|
582d6babd0 | ||
|
|
581f7c3c9e | ||
|
|
1fe31f567b | ||
|
|
a4d4b15bc1 | ||
|
|
c05319dc4a | ||
|
|
cc24e6f487 | ||
|
|
953637ffc4 | ||
|
|
2ce2028eb2 | ||
|
|
0b4923f12a | ||
|
|
7147cb4735 | ||
|
|
b2d653ea4f | ||
|
|
6db4f900a7 | ||
|
|
aba86200c6 | ||
|
|
03a7e19da0 | ||
|
|
58c2c5d2ab | ||
|
|
8c1e7af12f | ||
|
|
a83f1af5f9 | ||
|
|
fe6d0c99cf | ||
|
|
ba6a051fd1 | ||
|
|
16ffae514e | ||
|
|
23d666195c | ||
|
|
8e3aaa313b | ||
|
|
d56b978af4 | ||
|
|
6f6197cc23 | ||
|
|
484f263ec8 | ||
|
|
bb4e7b796f | ||
|
|
1fadc1ecb6 | ||
|
|
b0e849b9cb | ||
|
|
b39f7678b0 | ||
|
|
cbf477c562 | ||
|
|
77fe70d978 |
@@ -64,18 +64,27 @@ crates/
|
||||
# · modules.rs / order.rs / doc_title.rs (les modules, ADR 0009 : l'ordre d'une
|
||||
# section, le titre du document) · module_amend.rs (ajout gardé) · quorum.rs (le quorum
|
||||
# d'un article, PUT /units/{id}/quorum ; « un vote existe » : voted_units, ADR 0013) ·
|
||||
# quorum_amend.rs (les quorums par cran, amendement gardé) · web.rs (sert web/build)
|
||||
# quorum_amend.rs (les quorums par cran, amendement gardé) · article_amend.rs (un
|
||||
# article du seed ajouté à un document existant, gardé : E12, lot T4) · retire.rs
|
||||
# (formule, inertie, ordonnancement sortis du document sans rien effacer :
|
||||
# units.retired_at, ADR 0014) · web.rs (sert web/build)
|
||||
tests/ # api.rs (scénarios bout en bout) · ballots.rs (vote par la chaîne, indexeur simulé) ·
|
||||
# objections.rs · typos.rs (lot F, horloge avancée, migration 0008) · modules.rs (ordre,
|
||||
# titre, ajout gardé) · seed_proposals.rs (Propositions seed, publication suspendue) ·
|
||||
# quorum.rs (quorums par cran, la route, « un vote existe », l'amendement) ·
|
||||
# fixtures/seed-production-2026-11-02/ (le seed que la production fait tourner :
|
||||
# I1/E1/E2 en vigueur, pour rejouer les amendements sur une base comme elle) ·
|
||||
# seed_quorum_texts.rs (lots T et T2 : F1.1, F1.2, N1.1 ; fixtures/seed-quorum-m6b.json,
|
||||
# leurs textes depuis M6b, pour rejouer T2 sur une base comme la production)
|
||||
seeds/ # acte-engagement-certification.json (trilingue, 33 articles, aucun en vigueur ;
|
||||
# I1/E1/E2 Propositions seed au cran 2 (seed::SEED_PROPOSAL_LEVEL), inertie 3 ailleurs ;
|
||||
# modules en vigueur sans vote : le titre à 4, l'ordre de chaque section à 2 ;
|
||||
# seed_quorum_texts.rs (lots T, T2 et T3 : F1.1, F1.2, N1.1 ; aucun chiffre daté, chaque
|
||||
# phrase vraie pour tout W et tout quorum ; fixtures/seed-quorum-m6b.json et
|
||||
# seed-quorum-t2.json, leurs textes depuis M6b et M6c, pour rejouer T2 et T3 sur une
|
||||
# base comme la production ; depuis T4, sur le seed de M8) · amorce.rs (lot T4 : les
|
||||
# textes d'Yvv, E12, le retrait ; après M6c et après M8, chaque ligne de chaque table
|
||||
# gardée, le retour arrière) · fixtures/seed-m8/ (le seed de M8 : les derniers textes
|
||||
# de formule, inertie, ordonnancement, référence du lot U3)
|
||||
seeds/ # acte-engagement-certification.json (trilingue, 27 articles, 5 sections, aucun en
|
||||
# vigueur ; I1/E1/E12/E2 Propositions seed au cran 2 (seed::SEED_PROPOSAL_LEVEL),
|
||||
# inertie 3 ailleurs ; modules en vigueur sans vote : le titre à 4, l'ordre de chaque
|
||||
# section à 2 ;
|
||||
# quorums par cran : 12 · 12 · 50 · 50 · 50 Monnaie libre, 1 partout essai)
|
||||
# upcoming/<groupe>.json : les « À venir » des outils d'un groupe (fr, en, es), lus au démarrage
|
||||
web/src/
|
||||
@@ -84,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 ;
|
||||
@@ -96,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–C37 + M1 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 et
|
||||
web/e2e/ # Playwright C1–C44 + 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
|
||||
@@ -118,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 : 38 contrôles (40 tests, C25 et C35 en deux)
|
||||
cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 46 contrôles (48 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
|
||||
@@ -132,7 +142,10 @@ docker build -f docker/app.Dockerfile --target production . # rejoue toute la
|
||||
est la personne ou le groupe, jamais le système. Le français du texte voté fait foi.
|
||||
- **Moteur pur** : une règle de vote / édition / composition s'écrit et se teste dans
|
||||
`ld-engine` avant d'être câblée. Le serveur ne décide jamais l'accès seul : `trustin-access`.
|
||||
- **Invariants** : au départ, aucun article n'est en vigueur : I1, E1 et E2 sont des Propositions
|
||||
- **Invariants** : les informations de fonctionnement (formule, inertie, ordonnancement) ne se
|
||||
votent pas : hors du document, une unité retirée (`units.retired_at`) n'est ni lue, ni votée, ni
|
||||
comptée, ni publiée, et rien n'est effacé (ADR 0014) ; au départ, aucun article n'est en
|
||||
vigueur : I1, E1, E12 et E2 sont des Propositions
|
||||
seed (ouvertes, mises en avant, au cran 2 ; seuls les modules sont des *seeds a minima*), et la
|
||||
publication d'un document attend son premier texte en vigueur (ADR 0011) ; un quorum par cran
|
||||
(12 · 12 · 50 · 50 · 50 dans Monnaie libre), que l'auteur d'un article change avant le premier
|
||||
|
||||
+29
-3
@@ -1,6 +1,6 @@
|
||||
# HANDOFF — libreDecision
|
||||
|
||||
> Dernière mise à jour : 2026-11-03 à 09 h 46, intégrateur libredecision-d4 (orch : dev-63, session
|
||||
> Dernière mise à jour : 2026-11-04 à 00 h 42, intégrateur libredecision-d4 (orch : dev-63, session
|
||||
> racine /DEV, mandat « déploie » et « pousse » du 03/11 au 08/11). État vivant : le tableau
|
||||
> `docs/chantiers/acte-certification/tableau.md`.
|
||||
|
||||
@@ -27,8 +27,34 @@
|
||||
poignées et vote sur place, Propositions seed en tête, « 9 oui sur 12 votants pour passer », votes
|
||||
simulés dits simulés. **À dire à Yvv** : « Fixer ce quorum » est visible pour lui (administrateur) sur
|
||||
les 33 articles du seed tant que personne n'y a voté, et écrit en base.
|
||||
- **En cours** : lot T2 (les trois phrases, M6c, lancé à 09 h 45), puis lot U2 (les mineurs de la
|
||||
relecture de M7). Listes d'attente : au tableau.
|
||||
- **M6c en ligne** depuis le 03/11 à 11 h 05 (`84f015a`, pipeline 40 ;
|
||||
`actes/2026-11-03-m6c-phrases-quorum.md`) : les trois phrases de F1.1, F1.2 et N1.1 justes (lot T2),
|
||||
l'interface sans « presque l'unanimité ». Pour Yvv (`textes-quorum-seed.md`, section « Lot T2 ») :
|
||||
F1.2 « tant que l'article ne garde la trace d'aucun vote » (point 8) ; N1.1 « pas un nombre minimal de
|
||||
votants », faux pour un quorum d'auteur bas.
|
||||
- **M7b en ligne** depuis le 03/11 à 12 h 23 (`6f6197c`, pipeline 41 ;
|
||||
`actes/2026-11-03-m7b-lecteur-mineurs.md`) : les mineurs du lecteur (lot U2) : cibles de 44 px sur
|
||||
téléphone, les trois lectures entières, « Proposer cet ordre » dit pourquoi il est éteint, une
|
||||
information à un seul endroit dans le vote déplié, l'en-tête « 3 Propositions seed · 32 propositions en
|
||||
cours », la pastille d'un changement d'inertie de module.
|
||||
- **M8 en ligne** depuis le 03/11 à 20 h 31 (`de1b7eb`, pipeline 42 ; `actes/2026-11-03-m8-chiffres-dates.md`) :
|
||||
lots T3, T3b et T3c, sur la réponse d'Yvv du 03/11 (« pas de chiffre datés dans un texte voté »). L'exemple chiffré de
|
||||
N1.1 sort ; F1.2 et N1.1 écrivent le terme défini (« à la participation habituelle », « le quorum de
|
||||
l'article ») et chaque phrase est vraie pour tout W et tout quorum ; F1.1 inchangé. Trois relectures :
|
||||
« 2ce2028 : à reprendre », « e7ab82f : à reprendre », « de1b7eb : ok ». En tête du dossier, pour Yvv,
|
||||
chaque phrase avant / après. Copies retirées (orch à 20 h 43, l'intégrateur à 20 h 45).
|
||||
- **Lots T4, U3, U3b et U3c en ligne** (M9, 04/11 ; `590ccd7`, `a47aed0`) sur la demande d'Yvv de 20 h 38 : I1, E1, E2 réécrits, E12
|
||||
« Responsabilité civique » ajouté entre E1 et E2, les sections formule, inertie et ordonnancement sortent
|
||||
du document (rien effacé, ADR 0014). Relecture d'Yvv : `docs/chantiers/acte-certification/textes-amorce.md`.
|
||||
**U3** (fusionné à 22 h 40) : titres, séparation, « Info ouverture doc de réf. », informations
|
||||
dans les panneaux, « Proposition d'amorce », texte pour qui n'est pas invité ; U3b fusionné (23 h 00, `d66823b`, le tableau des crans lisible sur bureau). U3c fusionné (23 h 49, `fadfff5`, la phrase de la majorité M vraie pour tout W). **M9 en ligne** depuis le 04/11 à 00 h 34 (`fadfff5`, pipeline 43 ; acte exécuté par orch de 00 h 16 à 00 h 39, `actes/2026-11-03-m9-amorce-hors-vote.md`) : rien effacé, retour sur `de1b7eb` possible (image `previous`). Reste à Yvv : confirmer les choix (Q15, Q16 au tableau ; `textes-amorce.md`)
|
||||
(T4 et U3 ensemble). Défauts et questions à Yvv : au tableau.
|
||||
- H, S, V, T, U, T2, U2, T3, T3b et T3c sont en ligne. Listes d'attente (relectures de M6, M6c, M7, M7b, M8) : au tableau, section Actes. Le plus
|
||||
visible : le vote d'un changement d'inertie de module ne dit pas le cran visé.
|
||||
- **Purge faite** le 03/11 à 12 h 30-12 h 31 (ordre d'orch PURGE-LD) : les sept copies de travail des lots
|
||||
fusionnés et leurs 13 branches (`-d`, rien poussé) ; les bases de test et captures des sous-agents sous
|
||||
`/tmp` (synthétiques, aucune de la production) ; 688 restes des tests cargo et scripts e2e. Restent
|
||||
`/tmp/ld-e2e`, son verrou et `/tmp/ld-h-shots` (captures du lot H, hors de l'ordre).
|
||||
|
||||
## Avant le 03/11
|
||||
|
||||
|
||||
@@ -194,3 +194,6 @@ Acte exécuté le 03/11/2026, 10h48-11h06, par orch (session dev-63, e66db8ca),
|
||||
- Copies retirées à 11h06:08 : `/tmp/ld-m6c-copie` (10h23:10, fournie à l'intégrateur),
|
||||
`/tmp/ld-m6c-avant`, `/tmp/ld-m6c-apres`. Celles de l'intégrateur sont à retirer par lui.
|
||||
- Image de retour : `libredecision-app:previous` (`b2e597b`), rien à couper.
|
||||
- Copies de l'intégrateur retirées le 03/11 à 11 h 07 (scratchpad de libredecision-d4) : `m6c/base` et
|
||||
`m6c/rep` (copie de la copie de production de 10 h 23, avec son dépôt nu local) et les deux `.dump`.
|
||||
Plus aucune base dans ce scratchpad.
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Acte — Mise en ligne M7b (les mineurs du lecteur, lot U2)
|
||||
Classe : déploie
|
||||
Projet : libreDecision Date : 2026-11-03
|
||||
**État : EXÉCUTÉ** le 03/11 de 12 h 07 à 12 h 26 — **FIGÉ** sur `6f6197c` (`6f6197cc231837df55d6b3e1986f61be5d708a1f`) le 03/11 à 12 h 07 — préparé
|
||||
par l'intégrateur libredecision-d4 ; exécuté par orch (dev-63) sous son mandat (« déploie » et « pousse »,
|
||||
du 03/11 au 08/11). Relecture indépendante : « 6f6197c : ok » (prémisse 3).
|
||||
|
||||
## Intention
|
||||
|
||||
Mettre en ligne le lot U2 : les mineurs d'interface des relectures de M7 (`b2e597b`) et de M6c
|
||||
(`84f015a`), défaut retenu par orch le 03/11 à 09 h 45 (U2 après T2).
|
||||
|
||||
1. Cibles d'au moins 44 px sur téléphone : « Tout déplier », « Autres versions », « Fixer ce quorum », et
|
||||
tous les gestes du vote déplié et du réordonnancement.
|
||||
2. Les trois lectures (« En vigueur », « Si les bien cotées passent », « Si tout passe ») passent à la
|
||||
ligne sur téléphone au lieu de cacher « Si tout passe ».
|
||||
3. « Proposer cet ordre » éteint dit pourquoi : « C'est l'ordre en vigueur : déplace au moins un
|
||||
article. »
|
||||
4. Une information à un seul endroit dans le vote déplié : l'inertie dite une fois (ses crans dans
|
||||
« Inertie ») ; la ligne d'une Proposition seed s'efface sous son vote déplié ; l'autre version d'un
|
||||
article n'est plus redite dans « Autres versions ».
|
||||
5. L'en-tête compte ce que montre la liste : « 0 article en vigueur · 3 Propositions seed · 32
|
||||
propositions en cours ».
|
||||
6. Un changement d'inertie proposé sur un ordre ou sur le titre a sa pastille sur la ligne du module.
|
||||
7. `settings.reading` dit « au-delà du quorum » (fr, en, es) ; l'anglais en un seul vocabulaire (« for »,
|
||||
« against », « favour ») ; le commentaire de `crates/ld-engine/src/threshold.rs` écrit juste, et un test.
|
||||
|
||||
## Ce qui part
|
||||
|
||||
Version `6f6197c`, qui contient la version en ligne `84f015a`. **L'interface**, plus un commentaire et un
|
||||
test dans `crates/ld-engine` ; le reste est documentation (`CONTROLES.md`, `UX-lecteur.md`, `CLAUDE.md`,
|
||||
actes, tableau, HANDOFF). `git diff 84f015a 6f6197c -- crates/ld-server crates/trustin-access seeds docker
|
||||
.woodpecker .env data` → **vide** (vu par l'intégrateur à 11 h 39 et par orch à 11 h 40) : le serveur est
|
||||
celui qui est en ligne.
|
||||
|
||||
## Ce que l'acte écrit dans la base de production (donnée runtime)
|
||||
|
||||
**Rien.** Aucune migration, aucun amendement, aucun code serveur nouveau. Au démarrage, comme à chaque
|
||||
démarrage depuis M6, la file de publication suspendue reporte ses lignes en attente (`next_at`).
|
||||
|
||||
## Répétition
|
||||
|
||||
**Faite le 03/11 de 11 h 43 à 11 h 44**, avec le serveur de `6f6197c` (binaire de la boucle, copié en
|
||||
`m7b/ld-server-6f6197c`), sur une copie de la copie de production prise par orch à 11 h 40 min 27 (santé
|
||||
`84f015ad`), publication vers un dépôt nu local (`m7b/repetition.sh` ; journal `m7b/repetition-m7b.log`,
|
||||
scratchpad de l'intégrateur). Deux démarrages.
|
||||
|
||||
- `m7b/counts.sh` avant et après le second démarrage : **identiques**.
|
||||
- **La base entière comparée** (`.dump`) : seules changent les 10 lignes de la file en attente, par leur
|
||||
`next_at`.
|
||||
- Journal : « publication suspended », aucun amendement, aucune ligne ERROR ni WARN ; 0 commit vers le
|
||||
dépôt des textes.
|
||||
|
||||
## Recette écran
|
||||
|
||||
**Faite le 03/11 de 11 h 45 à 11 h 50**, sur d'autres copies de la copie de 11 h 40, interface de
|
||||
`6f6197c`, connexion de développement (identités `dev:` dans ces copies seulement), bureau 1360 × 900 et
|
||||
téléphone (Pixel 7), `fr-FR`, vue membre et vue administrateur. Scripts `m7b/recette.mjs` et
|
||||
`m7b/probe.mjs` ; rapport `m7b/shots/rapport.json` ; captures dans
|
||||
`/tmp/claude-1001/-home-yvv-Documents-PROD-DEV-libreDecision/d0a7108b-7f2f-484a-bcb0-de339238199d/scratchpad/m7b/shots/`,
|
||||
regardées. `recette.mjs` : 12 étapes, **4 en échec** par délai dépassé (« 2 les trois lectures » et
|
||||
« 1-4b » sur bureau et téléphone) : mes sélecteurs, pas l'interface (les lectures sont des `radio`, pas
|
||||
des `button` ; une ligne à deux versions s'ouvre d'abord sur ses variantes). Ces mesures ont été refaites
|
||||
par `probe.mjs` (sortie dans le transcript de l'intégrateur, non conservée en fichier) : téléphone, rangée
|
||||
des lectures 370 / 370, « Tout déplier » 44 px, vote de la variante 1.0.0 sans « Autres versions » ni
|
||||
0.4.0, gestes du vote 44 px ; bureau, rangée 412 / 412, « Tout déplier » 36 px. Le relecteur les a
|
||||
refaites de son côté, vraies (prémisse 3).
|
||||
|
||||
| Point | Mesuré sur les données de production | Captures |
|
||||
|---|---|---|
|
||||
| 1 | Téléphone : « Tout déplier » 44 px ; dans le vote déplié, Pour, Contre, « Réglages de la formule », « Coquilles… » 44 px ; « Proposer cet ordre » 44 px ; administrateur : « Fixer ce quorum » 44 px, le champ 44 px. (Bureau : 36 à 39 px, la règle vaut pour le téléphone.) | `telephone-1c-admin-quorum`, `telephone-4b-deux-versions`, `telephone-3-ordre-inchange` |
|
||||
| 2 | Téléphone : la rangée des lectures tient (370 px de contenu pour 370 visibles ; avant M7b, 412 pour 370 selon la relecture de M7, non remesuré ici), « Si tout passe » entier sur sa ligne ; la page ne déborde pas. | `telephone-5-entete` |
|
||||
| 3 | « Proposer cet ordre » éteint, et la phrase « C'est l'ordre en vigueur : déplace au moins un article. » visible (bureau et téléphone). | `*-3-ordre-inchange` |
|
||||
| 4 | Vote déplié de Raison d'être : « Inertie · ▮▮▯▯▯ 2 / 5 · basse » une fois (l'autre « 2 / 5 » est la ligne d'historique) ; la ligne « 0 pour · 0 contre · 9 oui sur 12 » n'est plus redite au-dessus. Rencontre physique (deux variantes) : le vote de la variante 1.0.0 ne redit pas l'autre (0 « Autres versions », 0 mention de 0.4.0). | `*-4a-seed-deplie`, `*-4b-deux-versions` |
|
||||
| 5 | « 0 article en vigueur · 3 Propositions seed · 32 propositions en cours » dans la lecture « En vigueur » (avant : « 35 propositions en cours » ; la production n'a aucun ordre proposé ouvert, l'écart de 35 à 32 est celui des 3 Propositions seed, comptées à part). | `*-5-entete` |
|
||||
| 6 | Aucun changement d'inertie de module ouvert en production : prouvé par C38 sur une base de test, pas vu sur ces données. | — |
|
||||
|
||||
Vu en passant, **pas du lot U2, déjà en ligne, en liste d'attente** :
|
||||
- sur bureau, le bas du menu latéral (langues, ambiances, « Se déconnecter ») déborde au bord droit
|
||||
quand le nom affiché est long (« Recette U2 bur… » : la quatrième pastille et « Se déconnecter »
|
||||
coupés ; « Sonde bur… » : tout tient) ;
|
||||
- sur téléphone, le sceau fixe chevauche « Réordonner » en bas à droite.
|
||||
Aussi : la carte du document sur la page du groupe compte toujours le total du serveur (35, Propositions
|
||||
seed comprises) ; changer ce compte toucherait le serveur (rapport de U2).
|
||||
|
||||
## Effet attendu
|
||||
|
||||
- Le lecteur change pour tout le monde sur ces sept points ; les données, les votes, les textes et la
|
||||
publication ne changent pas.
|
||||
- Le dépôt des textes reste à `9e0f45a`.
|
||||
|
||||
Irréversibilité : **aucune** côté données ; le code se remet en arrière.
|
||||
|
||||
## Avant — prémisses vivantes
|
||||
|
||||
1. La production sert `84f015a` : santé `"build":"84f015ad"` ; `prod/main` → `84f015a`.
|
||||
2. `git diff 84f015a 6f6197c -- crates/ld-server crates/trustin-access seeds docker .woodpecker .env data` →
|
||||
vide.
|
||||
3. La relecture indépendante de `6f6197c`, inscrite « ok » dans `~/harness/etat/relectures.json`.
|
||||
**Faite** : sous-agent neuf, sans le contexte de l'intégrateur, sur `84f015a..6f6197c`, lancée à
|
||||
11 h 51, rendue à 12 h 05, inscrite à 12 h 06 (transcript
|
||||
`~/.claude/projects/-home-yvv-Documents-PROD-DEV-libreDecision/d0a7108b-7f2f-484a-bcb0-de339238199d/subagents/agent-afbf95d4047008567.jsonl`,
|
||||
dernière ligne « 6f6197c : ok »). Aucun bloquant, aucun « à reprendre ». Ce qu'elle a exécuté : le diff
|
||||
entier ; e2e 42 / 42 du premier coup (11 h 58 à 12 h 01, binaire identique octet pour octet) ; vitest
|
||||
78 / 78 ; tests du moteur ; sondes Playwright sur deux copies de la copie de 11 h 40 (bureau, Pixel 7,
|
||||
360 et 320 px ; membre et administrateur ; fr, en, es ; écritures dans le groupe d'essai pour voir la
|
||||
pastille d'un changement d'inertie de module) : les neuf points tenus, rien de cassé (vote simulé,
|
||||
objections, panier) ; sa propre répétition (seules les `next_at` de la file) ; le retour.
|
||||
Mineurs, **liste d'attente** :
|
||||
- le vote d'un changement d'inertie de module (ouvert par la nouvelle pastille) ne dit pas le cran
|
||||
visé : seulement « inertie 2 / 5 · basse », le cran où il se vote ; « vers 3 / 5 » n'est que dans
|
||||
l'étiquette pour lecteur d'écran (`VoteFold.svelte:167`, `:177` ; `OrderLine.svelte:58-61`,
|
||||
`TitleBar.svelte:53-56`). Défaut déjà là dans `84f015a`, plus accessible depuis U2 ; aucun
|
||||
changement de ce type ouvert en production ;
|
||||
- l'en-tête ne suit la liste qu'en lecture « En vigueur » (`+page.svelte:159-160`) : nuancer
|
||||
`UX-lecteur.md` ou suivre la lecture ;
|
||||
- le commentaire du test moteur (`threshold.rs`, « 85 % of the quorum ») parle des paramètres de
|
||||
référence, pas du cran 2 ;
|
||||
- trois inexactitudes de ce dossier (4 étapes de recette en échec non dites, « 35, ordres compris »,
|
||||
la phrase de la vérification après) : **corrigées** ;
|
||||
- les copies de production de l'intégrateur : à retirer après l'acte (prévu).
|
||||
Non vérifié par elle : sonic (image, témoin `deploying`, santé), la mesure « avant » sur `84f015a`,
|
||||
les ambiances sombres, un vrai lecteur d'écran. Ses copies de la base de production sont supprimées.
|
||||
4. Boucle verte sur `6f6197c`, le 03/11 de 11 h 39 à 11 h 43, du premier coup (journal `m7b/loop.log`) :
|
||||
fmt, clippy `-D warnings`, 302 tests Rust, svelte-check 0 / 0, 78 vitest, build, e2e 42 / 42 (11 h 40 à
|
||||
11 h 43). Image de production construite de 11 h 43 à 11 h 45, sortie 0 (302 tests Rust « ok »,
|
||||
svelte-check 0 / 0, 78 vitest, build) ; image de contrôle retirée à 11 h 50.
|
||||
5. Sur sonic, aucun témoin `/opt/libredecision-app/deploying`.
|
||||
|
||||
## Commande exacte
|
||||
|
||||
```sh
|
||||
cd /home/yvv/Documents/PROD/DEV/libreDecision
|
||||
git push prod 6f6197cc231837df55d6b3e1986f61be5d708a1f:main
|
||||
```
|
||||
|
||||
## Vérifications après
|
||||
|
||||
- [x] Santé vue de l'extérieur → `"build":"6f6197cc"`.
|
||||
- [x] Journaux : « publication suspended … waiting=10 », aucune ligne ERROR ni WARN, aucun amendement.
|
||||
- [x] Le paquet JS servi, vu de l'extérieur, contient « changement d'inertie proposé » et « au-delà du
|
||||
quorum » (nouveaux dans `6f6197c` ; « déplace au moins un article » existait déjà dans `84f015a`).
|
||||
- [x] Sur une copie fraîche : comptes de `counts.sh` identiques à la copie d'avant ; `integrity_check` ok.
|
||||
- [x] Le dépôt des textes reste à `9e0f45a`.
|
||||
|
||||
## Retour
|
||||
|
||||
`docker tag libredecision-app:previous libredecision-app:latest`, puis
|
||||
`cd /opt/libredecision-app && docker compose up -d --force-recreate`. `previous` sera `84f015a`, le même
|
||||
serveur, qui attend aussi le premier texte pour publier : rien à couper, aucune donnée à reprendre.
|
||||
|
||||
## Exécution
|
||||
|
||||
Acte exécuté le 03/11/2026, 12h07-12h26, par orch (session dev-63, e66db8ca), sous mandat « déploie »
|
||||
(03/11 → 08/11, `~/harness/etat/mandats.json`).
|
||||
|
||||
- **Prémisses** (12 h 07 min 36 s) : santé `"build":"84f015ad"` ; `prod/main` → `84f015a` ; diff
|
||||
`84f015a..6f6197c` sur `crates/ld-server crates/trustin-access seeds docker .woodpecker .env data` vide ;
|
||||
« 6f6197c : ok » inscrit (12 h 06) ; aucun témoin `deploying` sur sonic.
|
||||
- **Copie d'avant** (12 h 07 min 45 s) : `integrity_check` ok, `foreign_key_check` vide ; identities 1,
|
||||
votes 0, revisions 119, translations 144, inertia_changes 10, file `pending=10 done=13`.
|
||||
- **Poussée** (12 h 08) : la commande du dossier, telle quelle → `84f015a..6f6197c 6f6197cc… -> main`.
|
||||
- **Pipeline 41** : créé à 12 h 08 min 13 s ; santé `"build":"6f6197cc"` dès 12 h 12 min 39 s ; `success`
|
||||
à 12 h 23 min 26 s (app puis maquette ; image `latest` 2637acd8a06e construite à 12 h 12 min 04 s).
|
||||
- **Redémarrage à froid** (12 h 23 min 46 s) : « libreDecision listening bind=0.0.0.0:8002
|
||||
build=6f6197cc », puis « publication suspended: no article in force … waiting=10 » ; aucune ligne
|
||||
ERROR, WARN ni amendement. Santé ensuite `{"build":"6f6197cc","ok":true}`.
|
||||
- **Copie d'après** (12 h 24 min 03 s) : `integrity_check` ok, `foreign_key_check` vide ; sortie de
|
||||
`counts.sh` **identique à la ligne près** à celle d'avant (migrations 1 à 11, quorums
|
||||
`[12,12,50,50,50]` / `[1,1,1,1,1]`, 22 amendements du seed, file `pending=10 done=13`).
|
||||
- **Paquet servi** (12 h 24 min 18 s, `curl -4` depuis le poste, 19 nœuds et 40 morceaux) :
|
||||
« changement d'inertie proposé » et « au-delà du quorum » présents, chacun dans un fichier.
|
||||
- **Dépôt des textes** : `main` → `9e0f45a9e8d4af356a0c42daa5f4fb9eb9e6dfb3`, inchangé.
|
||||
- **Retour** : `previous` = 7cc0a31053dc (construite à 10 h 53 min 40 s, `84f015a`) ; le conteneur tourne
|
||||
sur `latest` 2637acd8a06e.
|
||||
- **Copies retirées** à 12 h 26 min 19 s : `/tmp/ld-m7b-copie` (11 h 40, celle de la répétition),
|
||||
`/tmp/ld-m7b-avant`, `/tmp/ld-m7b-apres`.
|
||||
- **Branche locale** : `main` avancée `84f015a..6f6197c` à 12 h 26 min 19 s.
|
||||
- Écart au déroulé : aucun.
|
||||
- Copies de l'intégrateur retirées le 03/11 à 12 h 28 (scratchpad de libredecision-d4) : `m7b/base`,
|
||||
`m7b/rep` (répétition), `m7b/recette-apres`, `m7b/recette-sonde` (recette écran) et les deux `.dump`.
|
||||
Plus aucune base dans ce scratchpad.
|
||||
@@ -0,0 +1,341 @@
|
||||
# Acte — Mise en ligne M8 (aucun chiffre daté dans les textes votés, lots T3, T3b et T3c)
|
||||
Classe : déploie
|
||||
Projet : libreDecision Date : 2026-11-03
|
||||
**État : EXÉCUTÉ** le 03/11 de 20 h 26 à 20 h 44 (voir « Exécution ») ; figé sur `de1b7eb` (`de1b7eb5d7e982aee7c70a41c9f5799fb171493e`) le 03/11 à 20 h 23 — préparé par
|
||||
l'intégrateur libredecision-d4 ; exécuté par orch (dev-63) sous son mandat (« déploie » et « pousse », du
|
||||
03/11 au 08/11). Relecture indépendante : « de1b7eb : ok » (prémisse 3).
|
||||
|
||||
## Pour Yvv : ce que tu liras de changé
|
||||
|
||||
Deux textes votés de départ changent, dans les deux groupes : N1.1 « Niveaux d'inertie » et F1.2
|
||||
« Paramètres par défaut ». F1.1 « Formule du seuil WoT » n'est pas touché. Chaque phrase finale, avant (le
|
||||
texte en ligne aujourd'hui) et après, en français, qui fait foi.
|
||||
|
||||
**N1.1 « Niveaux d'inertie »**
|
||||
|
||||
1. Le quorum :
|
||||
- avant : « Chaque cran a aussi son quorum, qui n'est pas un nombre minimal de votants : sous le quorum,
|
||||
les votants qui manquent comptent comme « non ». »
|
||||
- après : « Chaque cran a aussi son quorum : sous le quorum de l'article, les votants qui manquent
|
||||
comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté. »
|
||||
2. La part :
|
||||
- avant : « La part dépend du cran et du nombre de votants, comparé à la participation habituelle du
|
||||
groupe : au-delà du quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout
|
||||
le monde vote. »
|
||||
- après : « La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle
|
||||
du groupe : au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand
|
||||
tout le monde vote. »
|
||||
3. La formule :
|
||||
- avant : « `Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum
|
||||
s'il en manque. »
|
||||
- après : « `Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum de
|
||||
l'article s'il en manque. »
|
||||
4. L'introduction des cinq crans :
|
||||
- avant : « **Les cinq crans** : le quorum dans Monnaie libre, la part exigée quand le groupe vote comme
|
||||
d'habitude, puis quand tout le monde vote, et ce qui est à ce cran au départ. »
|
||||
- après : « **Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum de
|
||||
l'article, à la participation habituelle, puis quand tout le monde vote, et ce qui est à ce cran au
|
||||
départ. »
|
||||
5. Les cinq lignes des crans, la même retouche dans chacune :
|
||||
- avant : « … quorum 12 ; 60 % comme d'habitude, 50 % quand tout le monde vote. … » (de même 67, 75,
|
||||
83 et 90 %) ;
|
||||
- après : « … quorum 12 ; 60 % à la participation habituelle, 50 % quand tout le monde vote. … ». Le
|
||||
reste de chaque ligne ne change pas.
|
||||
6. La référence :
|
||||
- avant : « Tant que le groupe n'a pas voté sur 5 propositions, elle vaut deux fois le quorum de
|
||||
référence du document : 100 votants dans Monnaie libre. »
|
||||
- après : « Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de référence
|
||||
du document : 100 votants dans Monnaie libre. »
|
||||
7. Les bornes :
|
||||
- avant : « Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour. »
|
||||
- après : « Elle est ramenée entre 1 % et 50 % de l'électorat, et se mesure une fois par jour. »
|
||||
8. G :
|
||||
- avant : « **G** se calcule pour que la part exigée, à la participation habituelle, soit exactement
|
||||
celle du cran : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est la part exigée comme d'habitude et p̃
|
||||
la participation habituelle divisée par W. »
|
||||
- après : « **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(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette
|
||||
part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W. »
|
||||
9. L'exemple, retiré (rien ne le remplace) : « Exemple : avec 7 224 membres et une participation
|
||||
habituelle de 120 votants, quand 120 personnes votent, il faut 72, 81, 90, 100 ou 108 voix pour, du
|
||||
cran 1 au cran 5 ; au cran 3, 75 % des 120 votants, soit 90. Sous le quorum, les absents comptent comme
|
||||
« non » : au cran 2 (quorum 12), 9 voix pour passent avec 3 « contre » au plus ; au cran 3 (quorum 50),
|
||||
il faut 39 voix pour, avec 11 « contre » au plus. »
|
||||
|
||||
**F1.2 « Paramètres par défaut »**
|
||||
|
||||
10. Le cran par défaut :
|
||||
- avant : « Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et le
|
||||
groupe demande 75 % de « pour » quand il vote comme d'habitude. Tout nouvel article commence à ce
|
||||
cran. »
|
||||
- après : « Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et,
|
||||
au-delà du quorum, le groupe demande 75 % de « pour » à sa participation habituelle. Tout nouvel
|
||||
article commence à ce cran. »
|
||||
|
||||
**Pourquoi.**
|
||||
- **L'exemple (9)** : ta décision du 03/11, aucun chiffre daté dans un texte voté. 7 224 membres et
|
||||
120 votants sont un état de la toile et une participation d'une date ; ce qui en était calculé sort avec
|
||||
eux.
|
||||
- **Le quorum (1)** : « pas un nombre minimal de votants » est faux pour un quorum bas fixé par l'auteur.
|
||||
Au quorum 2 (le plus bas dans Monnaie libre), il faut 2 « pour », à tout cran et sur toute toile : le
|
||||
quorum y est bien un minimum. La phrase nouvelle est vraie pour tout quorum : sous le quorum de
|
||||
l'article, le moteur demande exactement le seuil de ce quorum. « Autant de « pour » » plutôt que
|
||||
« autant de voix pour », qui se lisait d'abord « pour que » ; « de l'article », parce qu'un article peut
|
||||
avoir le quorum que son auteur a fixé, pas celui du cran.
|
||||
- **« À la participation habituelle » (4, 5, 8, 10)** : les textes disaient la part du cran « comme
|
||||
d'habitude », ce qui se lit comme le nombre de votants que le groupe a vraiment d'habitude. Le moteur,
|
||||
lui, prend la participation habituelle telle que N1.1 la définit : la médiane, **ramenée entre 1 % et
|
||||
50 % de l'électorat**. Dès que la médiane sort de ces bornes, ou reste sous le quorum de l'article, la
|
||||
part n'est plus celle du cran. Deux exemples, calculés par le moteur :
|
||||
- 7 224 membres, médiane de 40 votants (sous 1 %, ramenée à 72,24), cran 2, quorum 12 : à 40 votants,
|
||||
il faut 28 « pour » (68,7 %), pas 27 (67 %) ;
|
||||
- 3 000 membres, médiane de 40 (dans les bornes), cran 3, quorum 50 : la part se calcule sur le quorum,
|
||||
il faut 38 « pour » de ces 40 votants (95 %), pas 75 %.
|
||||
Le terme défini, « à la participation habituelle », et « au-delà du quorum de l'article » rendent ces
|
||||
phrases vraies dans tous les cas.
|
||||
- **La part exigée, le quorum de l'article (2, 3)** : le terme défini, au lieu de « la part » et « le
|
||||
quorum » nus. La part exigée baisse au-delà du quorum ; le nombre de « pour », arrondi à l'entier, peut
|
||||
remonter d'une voix (9 sur 12, puis 10 sur 13).
|
||||
- **La référence (6)** : la médiane ne compte que l'année écoulée. Avec moins de 5 propositions dans
|
||||
l'année, la référence de 100 votants revient, même si le groupe en a voté plus avant ; « tant que le
|
||||
groupe n'a pas voté sur 5 propositions » était faux.
|
||||
- **Les bornes (7)** : « est ramenée » dit ce que fait le moteur (une médiane hors des bornes est remplacée
|
||||
par la borne) ; « reste » se lisait comme un constat.
|
||||
|
||||
Trois tours de relecture indépendante y ont mené : T3 (le retrait et les phrases 1, 4, 10), puis T3b (G, et
|
||||
« autant de « pour » ») après « 2ce2028 : à reprendre », puis T3c (le terme défini partout, 2, 3, 5, 6, 7)
|
||||
après « e7ab82f : à reprendre ». Orch a donné son accord sur chaque tour : à 17 h 46, après 18 h 13 et
|
||||
après 19 h 18, avec ces mots : « c'est une correction de vérité, qui reste dans ce qu'il a validé ». Le
|
||||
détail et les textes entiers en fr, en, es : `docs/chantiers/acte-certification/textes-quorum-seed.md`
|
||||
(section « Lot T3 »). Chaque phrase de F1.2 et de N1.1, avec le test qui la vérifie :
|
||||
`docs/chantiers/acte-certification/t3c-liste-fermee.md`.
|
||||
|
||||
## Intention
|
||||
|
||||
Mettre en ligne les lots T3, T3b et T3c. Yvv, le 03/11 avant 16 h 56, mot pour mot : « Je valide
|
||||
textes-quorum-seed (dont le point 3, pas de chiffre datés dans un texte voté). Tu peux lancer la livraison
|
||||
de decision. » (registre `~/harness/etat/reponses-yvv.md`, commit `9b701da`). Ordre T3 d'orch (dev-63, 03/11
|
||||
vers 16 h 57) : retirer des textes votés F1.1, F1.2, N1.1 (fr, en, es ; deux groupes) tout chiffre qui
|
||||
dépend de l'état de la toile ou d'une participation à une date ; le texte reste vrai pour tout W ; la phrase
|
||||
de N1.1 « pas un nombre minimal de votants », fausse pour un quorum d'auteur bas, écrite vraie. Points 1, 2,
|
||||
4 et 8 de `textes-quorum-seed.md` : le défaut reste.
|
||||
|
||||
**Les preuves** (`crates/ld-server/tests/seed_quorum_texts.rs`, `crates/ld-server/tests/quorum.rs`) : la
|
||||
liste fermée (`t3c-liste-fermee.md`) nomme, pour chaque phrase qui affirme un nombre, une part ou un sens de
|
||||
variation, le test qui la vérifie contre le moteur sur une grille commune de 85 100 cas. Cette grille
|
||||
couvre W de 100 à 10 000 000, des médianes sous 1 % et au-dessus de 50 % de W ramenées par le moteur,
|
||||
chaque quorum d'auteur de 2 à W jusqu'à W = 1 000, et les cinq crans. Huit tests sont vus rouges sur le
|
||||
texte de `e7ab82f`, et quatre mutations du code sont vues rouges. Aucun chiffre daté : le test des nombres
|
||||
n'admet que des paramètres.
|
||||
|
||||
## Ce qui part
|
||||
|
||||
Version `de1b7eb` (lots T3, T3b et T3c), qui contient la version en ligne `6f6197c`. Code touché : `crates/ld-server/src/seed_amend.rs`
|
||||
(les deux amendements), `crates/ld-server/src/wot.rs` (une constante rendue publique pour les tests, valeur
|
||||
inchangée), `seeds/acte-engagement-certification.json` (bases neuves), des tests (`tests/seed_quorum_texts.rs`, `tests/quorum.rs`) et une fixture
|
||||
(`tests/fixtures/seed-quorum-t2.json`, les textes de T2) ; le reste est documentation. `web/` et
|
||||
`crates/ld-engine` : rien. Configuration (`docker/`, `.woodpecker/`, `.env`, `data/`) et migrations : rien
|
||||
(prémisse 4).
|
||||
|
||||
## Ce que l'acte écrit dans la base de production (donnée runtime)
|
||||
|
||||
- **Aucune migration.**
|
||||
- **Amendement de seed gardé** `2026-11-03-f1-2-chiffres` et `2026-11-03-n1-1-chiffres`, qui suivent
|
||||
(`follows`) ceux de T2 (`2026-11-03-f1-2-phrases`, `-n1-1-phrases`), décidé une fois par document, dans une
|
||||
transaction, seulement si personne n'a touché l'article : 0 vote (simulé compris), 0 ligne de bulletin,
|
||||
aucune coquille, traduction de membre ni objection, texte d'avant identique à l'empreinte posée par T2
|
||||
(F1.2 `96caf353…`, N1.1 `7fbedcb8…`) ; textes d'après : F1.2 `0c3b800b…`, N1.1 `8107c5fc…` ; F1.2 et N1.1 admis à la révision 4 seulement si ce sont les
|
||||
amendements suivis (M5, M6b, M6c), et eux seuls, qui l'ont écrite.
|
||||
- `proposals` : texte fr, révision + 1 (F1.2 et N1.1 → 5) ;
|
||||
- `revisions` : une ligne `amendment` ;
|
||||
- `translations` : en et es remplacés ;
|
||||
- `seed_amendments` : la décision ;
|
||||
- `outbox` : une ligne par article dans Monnaie libre, qui attend (publication suspendue) ; rien pour
|
||||
l'essai.
|
||||
Refus possibles, le texte d'avant reste : `not_a_seed`, `no_open_version`, `voted_into_force`,
|
||||
`confirmed`, `votes`, `ballots`, `corrected`, `translation_edited`, `signalled`, `revised`,
|
||||
`text_differs`, `changed`.
|
||||
- Les amendements de T2 sur F1.2 et N1.1 sont marqués dépassés : ils ne décident plus rien.
|
||||
- **F1.1 n'est pas touché. Aucun vote, aucune autre proposition, aucun autre article n'est touché ; la
|
||||
publication reste suspendue.**
|
||||
|
||||
## Répétition
|
||||
|
||||
**Faite le 03/11 de 20 h 03 min 37 à 20 h 04 min 20, réseau coupé** (`unshare -rn`, boucle locale seule :
|
||||
`m8/repetition-isolee.sh`), avec le serveur de `de1b7eb` (binaire de la boucle, construit à 20 h 03, copié en
|
||||
`m8/ld-server-de1b7eb` ; il porte l'empreinte `8107c5fc…`), sur une copie (18 h 47 min 52, empreintes
|
||||
inchangées depuis) de la copie de production prise par orch de 17 h 46 min 22 à 17 h 46 min 30 (production
|
||||
sur `6f6197cc`), publication vers un dépôt nu local (journal `m8/repetition-m8c.log`, scratchpad de
|
||||
l'intégrateur). Deux démarrages. Orch comparera les comptes de la production à ceux de cette copie avant de
|
||||
pousser.
|
||||
|
||||
| | Avant | Après |
|
||||
|---|---|---|
|
||||
| F1.1 (deux groupes) | ouverte, seed, révision 3, 0 vote, 0 bulletin | inchangée |
|
||||
| F1.2, N1.1 (deux groupes) | ouvertes, seed, révision 4, 0 vote, 0 bulletin | révision 5 |
|
||||
| Révisions | 119 | 123 |
|
||||
| Traductions | 144 | 144 (en, es des deux articles remplacés) |
|
||||
| `seed_amendments` | 22 | 26 |
|
||||
| File de publication (Monnaie libre) | 10 en attente, 13 faites | 12 en attente, 13 faites |
|
||||
| Commits vers le dépôt des textes (local) | — | 0 (« publication suspended … waiting=12 ») |
|
||||
| Tout le reste (`m8/counts.sh` : migrations 1–11, propositions, unités en vigueur, I1/E1/E2, `inertia_changes`, votes, bulletins, signalements, identités, quorums) | | identique |
|
||||
|
||||
**La base entière comparée** (`.dump`, et colonne par colonne) : changent exactement les 4 propositions de
|
||||
F1.2 et N1.1 (`body`, `revision`, `updated_at` : `ff53cbdc`, `13ee215f` dans Monnaie libre, `e7ae6bcb`,
|
||||
`d75f7e90` dans l'essai), leurs 8 traductions en et es (`body`, `source_hash`, `updated_at`), 4 révisions et
|
||||
4 décisions ajoutées, 2 lignes de file ajoutées et les 10 lignes en attente reportées (`next_at`), les deux
|
||||
compteurs (`sqlite_sequence` : révisions 123, file 25). Rien d'autre (`settings` inchangé : W n'a pas pu être
|
||||
relu, réseau coupé). `integrity_check` ok, `foreign_key_check` vide. Les textes écrits sont ceux du seed de
|
||||
`de1b7eb`, en fr, en et es, dans les deux groupes (comparés). Journal : quatre « seed amendment applied »
|
||||
(`2026-11-03-f1-2-chiffres`, `-n1-1-chiffres` × essai, Monnaie libre), « publication suspended …
|
||||
waiting=12 », aucune ligne ERROR ; un seul WARN, attendu réseau coupé : « web of trust size: no node nor
|
||||
indexer answered, keeping the last value ». **Un second démarrage ne rejoue rien** (comptes identiques,
|
||||
aucun « applied »).
|
||||
|
||||
Deux répétitions précédentes, sur `2ce2028` (17 h 54, réseau non coupé : W relu sur `rpc.duniter.org`,
|
||||
`settings.wot_size` 5 709 → 5 711) et sur `e7ab82f` (18 h 54, réseau coupé), avaient donné les mêmes comptes ;
|
||||
seuls les textes écrits diffèrent.
|
||||
|
||||
## Effet attendu
|
||||
|
||||
- Dans les deux groupes, N1.1 et F1.2 montrent les phrases de « Pour Yvv » (1 à 10), en fr, en et es :
|
||||
N1.1 n'a plus d'exemple chiffré, dit le quorum de l'article et la part du cran « au-delà du quorum de
|
||||
l'article, à la participation habituelle » ; F1.2 de même. F1.1 inchangé.
|
||||
- Rien d'autre ne change ; le dépôt des textes reste à `9e0f45a`.
|
||||
|
||||
Irréversibilité : **partielle**. Le code se remet en arrière ; les nouveaux textes restent (l'ancien est
|
||||
dans l'historique de chaque proposition).
|
||||
|
||||
## Avant — prémisses vivantes
|
||||
|
||||
1. La production sert `6f6197c` : santé `"build":"6f6197cc"` ; `prod/main` → `6f6197c`.
|
||||
2. Sur une copie fraîche, `m8/premisse.sh <db>` (scratchpad de l'intégrateur, lecture seule) : 6 lignes,
|
||||
toutes `open`, origine `seed`, révision 3 pour F1.1 et 4 pour F1.2 et N1.1, votes, bulletins, coquilles
|
||||
et objections à 0 ; migration 11. Un article touché est refusé par l'amendement avec sa raison, et l'acte
|
||||
reste valable pour l'autre. **Lu par l'intégrateur le 03/11 à 17 h 47, puis à 18 h 54**, sur ses copies de
|
||||
la copie d'orch de 17 h 46 : l'attendu exact ; 0 vote et 0 ligne de bulletin en tout ; une identité. À
|
||||
relire sur la copie fraîche juste avant la poussée, et comparer `counts.sh` à la colonne « Avant » de la
|
||||
répétition.
|
||||
3. La relecture indépendante de `de1b7eb`, inscrite « ok » dans `~/harness/etat/relectures.json`.
|
||||
- Première relecture, **`2ce2028` : à reprendre** (sous-agent neuf, sans le contexte de l'intégrateur,
|
||||
lancé à 17 h 55, inscrit à 18 h 12 ; transcript
|
||||
`~/.claude/projects/-home-yvv-Documents-PROD-DEV-libreDecision/d0a7108b-7f2f-484a-bcb0-de339238199d/subagents/agent-a3341da520d4c5530.jsonl`) :
|
||||
la phrase de G de N1.1 fausse sous le quorum ; mineurs : « autant de voix pour que si » ambigu, le test
|
||||
de « 100 votants » qui ne pouvait pas échouer, la répétition qui avait lu W sur la chaîne. Tout repris
|
||||
par le lot T3b (`e7ab82f`).
|
||||
- Deuxième relecture, **`e7ab82f` : à reprendre** (sous-agent neuf, sans le contexte de l'intégrateur,
|
||||
lancé à 18 h 56, inscrit à 19 h 16 ; transcript
|
||||
`~/.claude/projects/-home-yvv-Documents-PROD-DEV-libreDecision/d0a7108b-7f2f-484a-bcb0-de339238199d/subagents/agent-a92ddff6f85c88f90.jsonl`) :
|
||||
« quand le groupe vote comme d'habitude » se lit comme la médiane réelle, alors que le moteur cale G sur
|
||||
la participation habituelle bornée (W 7 224, médiane 40, cran 2 : 28 « pour » sur 40, pas 27) ; la
|
||||
grille des tests sautait ces cas. Repris par le lot T3c (lancé à 19 h 18, fusionné à 19 h 57 :
|
||||
`de1b7eb`), avec la liste fermée des phrases demandée par orch (`t3c-liste-fermee.md`).
|
||||
- **Relecture de `de1b7eb` : ok.** Sous-agent neuf, sans le contexte de l'intégrateur, lancé le 03/11 à
|
||||
20 h 05 sur la liste fermée (consigne `m8/relis-m8c.md` du scratchpad de l'intégrateur), rendu vers
|
||||
20 h 22, inscrit à 20 h 23 (transcript
|
||||
`~/.claude/projects/-home-yvv-Documents-PROD-DEV-libreDecision/d0a7108b-7f2f-484a-bcb0-de339238199d/subagents/agent-a95bc78d7d3d1e898.jsonl`,
|
||||
dernière ligne « de1b7eb : ok »). Aucun bloquant, aucun « à reprendre ». Ce qu'elle a exécuté :
|
||||
- diff mot à mot des neuf textes entre `6f6197c` (identique à la base de production) et `de1b7eb` :
|
||||
F1.1 inchangé, F1.2 et N1.1 comme annoncé, il ne reste que des paramètres ; en et es fidèles ;
|
||||
- la liste fermée, complète (24 entrées de F1.2, 15 de N1.1), relue une à une contre le moteur ; le
|
||||
terme défini partout ;
|
||||
- une sonde à elle sur `ld-engine`, 0 faux : sous le quorum (absents « non », autant de « pour » que si
|
||||
ce quorum avait voté, jamais une voix seule) sur 808 436 970 cas ; la part qui baisse au-delà du
|
||||
quorum et vaut M à W, 142 988 400 cas ; ⌈s × P⌉ « pour » à la participation habituelle P,
|
||||
947 070 cas ; B^W qui ne compte pas, 175 465 570 cas ;
|
||||
- fmt, clippy, 316 tests ; empreintes recalculées ; la mutation « participation non bornée » rejouée
|
||||
(6 tests rouges) ;
|
||||
- l'amendement sur des copies de `base-b`, réseau coupé : appliqué une fois par groupe, rien d'autre que
|
||||
le report de la file ; second démarrage sans rejeu ; retour sur `6f6197c` : 200 partout, nouveaux
|
||||
textes servis, JSON identique à celui de `de1b7eb` ; gardes `votes`, `ballots`, `corrected`,
|
||||
`translation_edited`, `signalled`, `text_differs`, le texte intact à chaque refus.
|
||||
Mineurs, **liste d'attente** (aucun ne rend une phrase fausse ; un changement après la mise en ligne
|
||||
demande un nouvel amendement gardé, possible tant qu'aucun vote n'existe sur l'article) :
|
||||
- F1.2, « au-delà du quorum » sans « de l'article » (vrai au défaut ; à aligner sur N1.1) ;
|
||||
- un quorum fixé à W : deux phrases sans objet (F1.1, G ; N1.1, « dépend du nombre de votants ») ;
|
||||
- en, « It is kept between » : « is brought back within » serait plus fidèle ;
|
||||
- une proposition rivale d'un membre ne bloque pas la garde (conception depuis le lot T ; aucune en
|
||||
production).
|
||||
Non vérifié par elle : npm, e2e, image (aucun changement sous `web/` ; faits par l'intégrateur,
|
||||
prémisse 5), la production elle-même (prémisses 1, 2, 6).
|
||||
4. `git diff 6f6197c de1b7eb -- docker/ .woodpecker/ .env data/ crates/ld-server/migrations/ web/ crates/ld-engine/`
|
||||
→ vide (vu par l'intégrateur le 03/11 à 20 h 04).
|
||||
5. Boucle verte sur `de1b7eb`, le 03/11 de 19 h 57 à 20 h 03 (journal `m8/loop-c.log`), du premier coup :
|
||||
fmt, clippy `-D warnings`, 316 tests Rust, svelte-check 0 / 0 (648 fichiers), 78 vitest, build, e2e
|
||||
42 / 42 (19 h 59 à 20 h 03). Image de production construite le 03/11 de 20 h 05 à 20 h 10 (copie
|
||||
principale sur `591ed37`, même code que `de1b7eb` : le dossier seul en plus), sortie 0 : étage serveur
|
||||
rejoué (fmt, clippy, 316 tests Rust « ok », 0 échec, build release) ; étage web repris du cache (`web/`
|
||||
identique depuis `6f6197c`) ; image de contrôle retirée à 20 h 10 (journal `m8/docker-m8c.log`).
|
||||
6. Sur sonic, aucun témoin `/opt/libredecision-app/deploying`.
|
||||
|
||||
## Commande exacte
|
||||
|
||||
```sh
|
||||
cd /home/yvv/Documents/PROD/DEV/libreDecision
|
||||
git push prod de1b7eb5d7e982aee7c70a41c9f5799fb171493e:main
|
||||
```
|
||||
|
||||
## Vérifications après
|
||||
|
||||
- [ ] Santé vue de l'extérieur → `"build":"de1b7eb5"`.
|
||||
- [ ] Journaux : quatre « seed amendment applied » (`2026-11-03-f1-2-chiffres`, `-n1-1-chiffres` × essai,
|
||||
Monnaie libre), « publication suspended … waiting=12 », aucune ligne ERROR ni WARN.
|
||||
- [ ] Redémarrage à froid : aucun amendement rejoué.
|
||||
- [ ] Sur une copie fraîche : F1.2 et N1.1 en révision 5, F1.1 en révision 3, dans les deux groupes ; le
|
||||
reste identique à la ligne près à la copie d'avant (comptes de `counts.sh`) ; `integrity_check` ok.
|
||||
- [ ] Le dépôt des textes reste à `9e0f45a`.
|
||||
|
||||
## Retour
|
||||
|
||||
- **Le code** : `docker tag libredecision-app:previous libredecision-app:latest`, puis
|
||||
`cd /opt/libredecision-app && docker compose up -d --force-recreate`. `previous` sera `6f6197c`, qui
|
||||
attend aussi le premier texte pour publier : pas de coupure de la publication à faire. `6f6197c` sert les
|
||||
nouveaux textes tels quels (ce sont des données ; vérifié par la relecture de `de1b7eb` : 200 partout,
|
||||
JSON identique à celui de `de1b7eb`).
|
||||
- **Les textes** : pas de retour automatique ; l'ancien texte reste dans l'historique de chaque
|
||||
proposition, un nouvel amendement gardé pourrait le remettre.
|
||||
|
||||
## Exécution
|
||||
|
||||
Acte exécuté le 03/11/2026, 20h26-20h44, par orch (session dev-63, e66db8ca), sous mandat « déploie »
|
||||
(03/11 → 08/11, `~/harness/etat/mandats.json`).
|
||||
|
||||
- **Prémisses** (20 h 26 min 26 s) : santé `{"build":"6f6197cc","ok":true}` ; `prod/main` → `6f6197c`,
|
||||
ancêtre de `de1b7eb` ; diff `6f6197c..de1b7eb` sur `docker/ .woodpecker/ .env data/
|
||||
crates/ld-server/migrations/ web/ crates/ld-engine/` : 0 fichier ; « de1b7eb : ok » inscrit (20 h 23) ;
|
||||
aucune autre relecture de ce code en cours (intégrateur) ; aucun témoin `deploying` sur sonic ; dépôt des
|
||||
textes `main` → `9e0f45a9e8d4af356a0c42daa5f4fb9eb9e6dfb3` ; CI : 41 `success` `6f6197cc` en tête.
|
||||
- **Copie d'avant** (20 h 26 min 35 s, `/tmp/ld-m8-avant`) : sortie de `m8/counts.sh` **identique à la ligne
|
||||
près** à celle de la copie de 17 h 46, base de la répétition (migrations 1 à 11, integrity ok, fk vide,
|
||||
identities 1, votes 0, revisions 119, translations 144, file `pending=10 done=13`, `seed_amendments` 22) ;
|
||||
`m8/premisse.sh` : les 6 lignes attendues (F1.1 révision 3, F1.2 et N1.1 révision 4, `open`, `seed`,
|
||||
votes, bulletins, coquilles et objections à 0), 0 vote et 0 bulletin en tout, migration 11.
|
||||
- **Poussée** (20 h 27) : la commande du dossier, telle quelle → `6f6197c..de1b7eb de1b7eb5… -> main`.
|
||||
- **Pipeline 42** : créé à 20 h 27 min 06 s ; image `latest` 78ca8a1d10e4 construite à 20 h 31 min 25 s ;
|
||||
santé `"build":"de1b7eb5"` dès 20 h 31 min 57 s ; `success` à 20 h 42 min 11 s.
|
||||
- **Journal du premier démarrage** (19:31:53Z) : quatre « seed amendment applied »
|
||||
(`2026-11-03-f1-2-chiffres` essai et monnaie-libre, `2026-11-03-n1-1-chiffres` essai et monnaie-libre),
|
||||
« libreDecision listening … build=de1b7eb5 », « publication suspended … waiting=12 » ; aucune ligne ERROR
|
||||
ni WARN.
|
||||
- **Redémarrage à froid** (20 h 42 min 50 s) : « libreDecision listening … build=de1b7eb5 », puis
|
||||
« publication suspended … waiting=12 » ; aucun amendement rejoué, aucune ligne ERROR ni WARN. Santé ensuite
|
||||
`{"build":"de1b7eb5","ok":true}`.
|
||||
- **Copie d'après** (20 h 43 min 08 s, `/tmp/ld-m8-apres`) : `counts.sh` diffère de la copie d'avant
|
||||
exactement comme la répétition : revisions 119 → 123, file `pending=10` → `pending=12` (`done=13`),
|
||||
`seed_amendments` 22 → 26 ; tout le reste identique à la ligne près (integrity ok, fk vide, translations 144,
|
||||
votes 0, identities 1). `premisse.sh` : F1.2 et N1.1 en révision 5, F1.1 en révision 3, dans les deux
|
||||
groupes, toujours 0 vote, bulletin, coquille et objection. Les traductions en et es de F1.2 sont épinglées
|
||||
à l'empreinte `0c3b800b…`, celles de N1.1 à `8107c5fc…` (les `to` des amendements) ; les quatre
|
||||
décisions `2026-11-03-f1-2-chiffres` et `-n1-1-chiffres` inscrites ; aucun « 7 224 » dans les textes fr,
|
||||
en et es des quatre propositions ; « participation habituelle » présent, « comme d'habitude » absent.
|
||||
- **Dépôt des textes** : `main` → `9e0f45a9e8d4af356a0c42daa5f4fb9eb9e6dfb3`, inchangé.
|
||||
- **Retour** : `previous` = 2637acd8a06e (construite le 03/11 à 12 h 12 min 04 s, `6f6197c`) ; le conteneur
|
||||
tourne sur `latest` 78ca8a1d10e4.
|
||||
- **Copies retirées** à 20 h 43 min 58 s : `/tmp/ld-m8-copie` (17 h 46, base des répétitions),
|
||||
`/tmp/ld-m8-avant`, `/tmp/ld-m8-apres`. Les copies de l'intégrateur (`m8/base-b`, `m8/lecture`, `m8/rep`,
|
||||
`m8/dump`) sont à retirer par lui, sur ce message.
|
||||
- **Branche locale** : `main` avancée `6f6197c..de1b7eb` à 20 h 43 min 58 s.
|
||||
- Écart au déroulé : aucun.
|
||||
- Copies de l'intégrateur retirées le 03/11 à 20 h 45 (scratchpad de libredecision-d4) : `m8/base-b` et
|
||||
`m8/lecture` (copies de la copie de 17 h 46), `m8/rep` (la base de la répétition, avec son dépôt nu local)
|
||||
et `m8/dump` (les deux `.dump` et leur diff). Plus aucune base dans ce scratchpad.
|
||||
@@ -0,0 +1,361 @@
|
||||
# Acte — Mise en ligne M9 (l'Acte d'engagement d'Yvv et les informations de fonctionnement hors vote, lots T4, U3, U3b et U3c)
|
||||
Classe : déploie
|
||||
Projet : libreDecision Date : 2026-11-03
|
||||
|
||||
**État : EXÉCUTÉ** le 04/11 de 00 h 16 à 00 h 39 (voir « Exécution ») ; figé sur `fadfff5`
|
||||
(`fadfff55649140a8e240644c3a958ed256a291fd`, lot U3c fusionné le 03/11 à
|
||||
23 h 49) le 04/11 à 00 h 15 — préparé par l'intégrateur libredecision-d4 ; exécuté par orch (dev-63) sous son
|
||||
mandat (« déploie » et « pousse », du 03/11 au 08/11). Relecture indépendante : « fadfff5 : ok »
|
||||
(prémisse 3). Mot : **« déploie »** (rien n'est effacé ; voir
|
||||
« Ce que l'acte écrit »).
|
||||
|
||||
## Pour Yvv : ce que tu liras de changé
|
||||
|
||||
Dans les deux groupes (Monnaie libre et l'essai), en français ici ; en anglais et en espagnol dans
|
||||
`docs/chantiers/acte-certification/textes-amorce.md`. Tous restent des **Propositions d'amorce** ouvertes,
|
||||
0 vote, au cran 2 ; aucun article n'est en vigueur.
|
||||
|
||||
**I1 « Raison d'être »**
|
||||
- Avant : *« Derrière une clé il n'y a qu'un seul être humain vivant ; derrière un être humain, il n'y a
|
||||
qu'une seule clé certifiante. »* / C'est le seul et unique propos de la certification, qui donne à notre
|
||||
toile fiduciaire sa valeur. Nous pouvons nous fier à l'unicité et l'intégrité de tout geste signé par cette
|
||||
clé. C'est l'enjeu de la certification. / Cet acte d'engagement est par ailleurs le seul acte de nature
|
||||
contractuelle qui fonde l'usage de la première monnaie libre.
|
||||
- Après : *« Derrière une clé il y a un et un seul être humain vivant ; derrière un être humain, il y a une
|
||||
et une seule clé certifiante. »* / C'est l'unique enjeu de la certification et de votre engagement, il
|
||||
donne à notre toile fiduciaire sa valeur : pouvoir nous fier à l'unicité et l'intégrité de tout geste signé
|
||||
par cette clé. / Cet acte d'engagement est par ailleurs le seul acte de nature contractuelle, qui fonde
|
||||
l'usage de la première monnaie libre.
|
||||
|
||||
**E1 « Unicité du compte »**
|
||||
- Avant : Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte cocréateur de
|
||||
monnaie G1.
|
||||
- Après : Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte certifiant.
|
||||
|
||||
**E12 « Responsabilité civique »** (nouveau, entre E1 et E2)
|
||||
- Je déclare sur l'honneur avoir la capacité légale et physique de signer un contrat et d'en assumer la
|
||||
responsabilité civique, pour certifier à mon tour.
|
||||
|
||||
**E2 « Réciprocité »**
|
||||
- Avant : Je m'engage sur l'honneur à ne certifier que des personnes physiques qui respectent
|
||||
scrupuleusement ces deux présents engagements fondamentaux.
|
||||
- Après : Je m'engage sur l'honneur à ne certifier que des personnes physiques qui s'engagent elles-mêmes, à
|
||||
scrupuleusement respecter ces trois principes fondamentaux.
|
||||
|
||||
**Ce qui sort du document** (rien n'est effacé) : les sections « Formule de vote » (F1, F1.1, F1.2, F1.3),
|
||||
« Inertie » (N1, N1.1) et « Ordonnancement » (O1), avec l'ordre de chacune. Elles ne se lisent plus, ne se
|
||||
votent plus, ne se publient plus. Leurs informations sont maintenant dans l'interface, là où elles servent :
|
||||
au vote déplié, dans le panneau « Réglages de la formule », sur la ligne d'ordre et au réglage du quorum.
|
||||
|
||||
**Ce que tu verras à l'écran**
|
||||
- En haut du document, avant le texte, l'encadré **« Info ouverture doc de réf. »**, hors vote : ton
|
||||
paragraphe (« Aucun article n'est encore en vigueur. Les Propositions d'amorce y entrent par le vote… »),
|
||||
l'inertie à l'ouverture (Propositions d'amorce 2 / 5, titre 4 / 5, ordre de chaque section 2 / 5, autres
|
||||
articles 3 / 5) et le quorum (12 · 12 · 50 · 50 · 50 votants, du cran 1 au cran 5), lus dans le document.
|
||||
- Un filet entre cet encadré et le texte ; plus les titres « Introduction » ni « Engagements fondamentaux ».
|
||||
- « Proposition d'amorce » à la place de « Proposition seed ».
|
||||
- Sans être connecté, l'accueil dit : « 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. »
|
||||
|
||||
**Les choix faits pour toi** (appliqués ; dis lequel tu changes, il se change par un nouvel amendement tant
|
||||
qu'aucun vote n'existe) : « il y a une et une seule clé » (« n'y a » lu comme une coquille, la seule
|
||||
correction de tes mots) ; I1 en trois paragraphes, le premier en italique ; tes virgules gardées ; « ces
|
||||
trois principes » = E1, E12, E2 ; E12 au cran 2 ; le chapitre titré comme tu l'as écrit ; « @ chiara ou Yvv »
|
||||
lu « à Chiara ou à Yvv » ; une fois un article en vigueur, la première phrase de ton paragraphe disparaît et « y entrent
|
||||
par le vote » devient « entrent en vigueur par le vote ».
|
||||
|
||||
Tes deux propositions d'inertie retirées le 03/11 (sur I1 et E1, Q12) ne bloquent pas la réécriture de ces
|
||||
textes : elles portent sur l'inertie, pas sur le texte, et restent dans l'historique.
|
||||
|
||||
## Intention
|
||||
|
||||
Mettre en ligne la demande d'Yvv du 03/11, faite dans la session de l'intégrateur (vers 20 h 38, puis avant
|
||||
20 h 44), mot pour mot dans `docs/chantiers/acte-certification/tableau.md` et
|
||||
`textes-amorce.md` : les textes I1, E1, E2, l'article nouveau « Responsabilité civique » ; « ordonnancement,
|
||||
inertie, et formule de vote sont des infos de fonctionnement […] Vire les 3 du bas de la liste » ; le
|
||||
chapitre « Info ouverture doc de réf. » ; la séparation avant le texte ; les titres « Introduction » et
|
||||
« Engagements fondamentaux » retirés ; le texte pour qui n'est pas invité. Un seul acte pour T4 et U3 (accord
|
||||
d'orch le 03/11 à 21 h 58) : T4 seul retirerait les informations du document avant que l'interface ne les
|
||||
dise.
|
||||
|
||||
## Ce qui part
|
||||
|
||||
Version `fadfff5` (lots T4, U3, U3b, U3c), qui contient la version en ligne `de1b7eb`.
|
||||
- **Serveur** : migration `0012_retired.sql` (additive) ; `retire.rs` (le retrait) ; `article_amend.rs`
|
||||
(E12) ; `seed_amend.rs` (trois amendements, I1, E1, E2) ; les lectures, votes, propositions, objections,
|
||||
coquilles, traductions, comptes et la publication ignorent une unité retirée ; `docs.rs` : `seedFeatured`.
|
||||
- **Seed** (`seeds/acte-engagement-certification.json`, bases neuves) : les textes d'Yvv, E12, sans les
|
||||
sections formule, inertie, ordonnancement.
|
||||
- **Interface** (`web/`) : le chapitre, la séparation, les titres, les informations dans le vote et les
|
||||
panneaux, « Proposition d'amorce », le texte pour qui n'est pas invité, le réordonnancement sur plusieurs
|
||||
lignes, le tableau des crans lisible sur bureau ; « La formule du seuil » dit « quand tout le monde vote,
|
||||
la part exigée est la majorité M » (U3c, vrai pour tout W).
|
||||
- Configuration (`docker/`, `.woodpecker/`, `.env`, `data/`) : rien (prémisse 4).
|
||||
|
||||
## Ce que l'acte écrit dans la base de production (donnée runtime)
|
||||
|
||||
Au premier démarrage, dans une transaction par amendement et par document, une fois :
|
||||
- **Migration 0012, additive** : deux colonnes vides `units.retired_at`, `units.retired_reason` ; deux tables
|
||||
de journal `retirement_amendments`, `article_amendments`. Aucune colonne retirée, aucune ligne effacée.
|
||||
- **`2026-11-03-i1-amorce`, `-e1-amorce`, `-e2-amorce`** (amendements de seed gardés, `Version::SeedProposal`) :
|
||||
texte fr, révision + 1 (I1 → 3, E1 et E2 → 2), une ligne `revisions` `amendment`, en et es remplacés, la
|
||||
décision dans `seed_amendments`, une ligne de file (Monnaie libre, raison « Proposition d'amorce
|
||||
réécrite : … »). Empreintes : I1 `a7095d70…` →
|
||||
`32eb36b4…`, E1 `bdca2713…` → `df31d0ec…`, E2 `bde76a30…` → `a7dd4690…`. Refusé à la moindre trace d'une
|
||||
personne (vote, simulé compris ; bulletin ; coquille ; traduction de membre ; objection ; texte différent de
|
||||
l'empreinte) : le texte d'avant reste.
|
||||
- **`2026-11-03-e12-responsabilite-civique`** (`article_amend.rs`) : une unité E12 (section
|
||||
`fondamental`, position 4,5, entre E1 et E2 tant que l'ordre de la section n'a pas de vote, sinon en fin de
|
||||
section), sa Proposition d'amorce ouverte (cran 2, `seed_featured`), sa révision initiale, en et es
|
||||
(empreinte `3fa5a8e5…`), la décision dans `article_amendments`, une ligne de file (Monnaie libre, « Proposition d'amorce
|
||||
ajoutée : E12 … »).
|
||||
- **`2026-11-03-fonctionnement-hors-vote`** (`retire.rs`) : `UPDATE units SET retired_at, retired_reason`
|
||||
sur dix unités par document (F1, F1.1, F1.2, F1.3, N1, N1.1, O1, `ordre.formule`, `ordre.inertie`,
|
||||
`ordre.ordonnancement`), **tous ou aucun**, seulement si aucune trace d'une personne n'existe sur elles
|
||||
(raisons de refus : `votes`, `ballots`, `adopted`, `member_proposal`, `corrected`,
|
||||
`translation_edited`, `signalled`, `quorum_set`, `changed`) ; la décision dans `retirement_amendments` ;
|
||||
une ligne de file (Monnaie libre). Leurs propositions, révisions, traductions, changements d'inertie et
|
||||
lignes de file restent.
|
||||
- La file de publication : les lignes en attente sont reportées (`next_at`) ; la publication reste
|
||||
suspendue (aucun texte en vigueur). Le groupe d'essai ne publie jamais.
|
||||
- **Rien d'autre** : aucun vote, aucune identité, aucun autre article. (Les cinq réglages de participation
|
||||
sont remesurés par la tâche quotidienne si la dernière mesure a plus de 24 h, avec ou sans M9 : mêmes
|
||||
valeurs tant qu'aucune proposition n'a été votée.)
|
||||
|
||||
## Répétition
|
||||
|
||||
**Faite sur `fadfff5` le 03/11 de 23 h 50 min 24 à 23 h 52 min 32, réseau coupé** (`unshare -rn`, boucle
|
||||
locale seule : `m9/repetition-isolee.sh`, scratchpad de l'intégrateur), avec le serveur construit sur
|
||||
`fadfff5` (`m9/ld-server-fadfff5`), sur une copie de la copie de production prise par orch de 22 h 41 min 52
|
||||
à 22 h 42 min 10 (production sur `de1b7eb5` ; empreintes vérifiées : `05058b76…`, `fd4c9fda…`, WAL vide),
|
||||
publication vers un dépôt nu local. Journal `m9/repetition-fadfff5.log` ; comparaison ligne à ligne de
|
||||
chaque table par `m9/compare.py` (lecture seule, par `rowid`). Une première répétition sur `d66823b`
|
||||
(22 h 45 min 39 à 22 h 47 min 46, `m9/repetition-d66823b.log`, états gardés dans `m9/snap-d66823b/`) a donné
|
||||
les mêmes comptes ; la relecture de `fadfff5` a comparé les deux, heures et identifiants neutralisés : seules
|
||||
diffèrent les raisons des quatre lignes de file (« Proposition d'amorce »).
|
||||
|
||||
| Table | Avant | Après | Ce qui change |
|
||||
|---|---|---|---|
|
||||
| `_sqlx_migrations` | 11 | 12 | + la migration 12 |
|
||||
| `units` | 84 | 86 | + E12 × 2 ; 20 unités marquées (`retired_at`, `retired_reason`) ; les 64 autres : deux colonnes nouvelles vides |
|
||||
| `proposals` | 91 | 93 | + E12 × 2 (ouvertes, seed, `seed_featured`) ; I1, E1, E2 × 2 : `body`, `revision`, `updated_at` |
|
||||
| `revisions` | 123 | 131 | + 6 `amendment` (I1, E1, E2 × 2), + 2 `initial` (E12 × 2) |
|
||||
| `translations` | 144 | 148 | + 4 (E12 en, es × 2) ; 12 remplacées (I1, E1, E2 en, es × 2 : `body`, `source_hash`, `updated_at`) |
|
||||
| `seed_amendments` | 26 | 32 | + 6 décisions `applied` |
|
||||
| `article_amendments` | — | 2 | table nouvelle, 2 décisions `applied` (`between`, 4,5) |
|
||||
| `retirement_amendments` | — | 2 | table nouvelle, 2 décisions `applied` |
|
||||
| `outbox` | 25 (12 en attente) | 30 (17 en attente) | + 5 (Monnaie libre) ; les 12 en attente reportées (`next_at`) |
|
||||
| `settings` | 7 | 7 | les 5 mesures de participation refaites (dernière le 02/11 à 22 h 09 UTC, plus de 24 h) : mêmes valeurs (médiane nulle, 0 proposition), seule l'heure change ; ce n'est pas M9 (absent de la répétition de `d66823b`, faite moins de 24 h après) |
|
||||
| Les 21 autres tables (votes 0, bulletins 0, identités 1, quorums, inertie, invitations, groupes, sessions…) | | | identiques |
|
||||
|
||||
**Aucune ligne supprimée, dans aucune table.** `integrity_check` ok, `foreign_key_check` vide. Dans chaque
|
||||
groupe, l'ordre de « fondamental » lu : T1, E1, E12, E2. Journal : six « seed amendment applied », deux
|
||||
« article added … placement=Between(4.5) », deux « units out of the document … retired=[F1, F1.1, F1.2,
|
||||
F1.3, N1, N1.1, O1, ordre.formule, ordre.inertie, ordre.ordonnancement] », « publication suspended …
|
||||
waiting=17 », aucune ligne ERROR ; un seul WARN, attendu réseau coupé (« web of trust size: no node nor
|
||||
indexer answered, keeping the last value »). Dépôt nu : 0 commit.
|
||||
|
||||
**Ce qu'on lit ensuite** (connexion de développement sur une copie à part de l'état d'après) : dans chaque
|
||||
groupe, 27 unités, cinq ordres de section (introduction, fondamental, technique, conclusion, annexe) ; I1,
|
||||
E1, E12, E2 au cran 2, Propositions d'amorce (`seedProposal`, `seedFeatured`) ; ni F, ni N, ni O.
|
||||
|
||||
**Un second démarrage ne rejoue rien** : toutes les tables identiques, seul le report de la file (`next_at`).
|
||||
|
||||
**Le retour, rejoué** : le serveur de `de1b7eb` (`m8/ld-server-de1b7eb`, celui de la répétition de M8) sur
|
||||
une copie de l'état d'après : démarre, santé ok ; il n'écrit rien d'autre que le report de la file ; il
|
||||
montre, dans chaque groupe, **34 unités** : les sept articles F1 à O1 et les huit ordres de section
|
||||
reviennent, avec les nouveaux textes d'I1, E1, E2 et E12 entre E1 et E2 (ce sont des données). **Un nouveau
|
||||
passage** au serveur de `fadfff5` ensuite ne rejoue rien (seul le report de la file) : les dix unités restent
|
||||
retirées, puisque leur marque est restée.
|
||||
|
||||
## Effet attendu
|
||||
|
||||
- Dans les deux groupes : I1, E1, E2 aux textes d'Yvv, E12 entre E1 et E2, en fr, en, es ; ni formule, ni
|
||||
inertie, ni ordonnancement dans le document ; l'encadré « Info ouverture doc de réf. » en tête.
|
||||
- L'accueil sans session dit le texte pour qui n'est pas invité.
|
||||
- La publication reste suspendue ; le dépôt des textes reste à `9e0f45a`.
|
||||
|
||||
Irréversibilité : **partielle**. Le code se remet en arrière (`previous` = `de1b7eb`) et les sept articles
|
||||
réapparaissent ; les nouveaux textes et E12 restent (l'ancien texte est dans l'historique de chaque
|
||||
proposition) ; la migration 0012 reste (ignorée par `de1b7eb`, `set_ignore_missing`).
|
||||
|
||||
## Avant — prémisses vivantes
|
||||
|
||||
1. La production sert `de1b7eb` : santé `"build":"de1b7eb5"` ; `prod/main` → `de1b7eb`.
|
||||
2. Sur une copie fraîche, `m8/counts.sh <db>` identique à la ligne près à la colonne « Avant » de la
|
||||
répétition (migrations 1 à 11, integrity ok, identities 1, votes 0, revisions 123, translations 144, file
|
||||
`pending=12 done=13`, `seed_amendments` 26) ; et `m9/traces.sh <db>` (scratchpad de l'intégrateur,
|
||||
lecture seule) : pour I1, E1, E2 et les dix unités, dans les deux groupes, votes, bulletins, coquilles,
|
||||
objections, traductions de membre et quorums fixés à 0 ; propositions de membre : seulement
|
||||
`inertia/withdrawn` sur I1 et E1 dans Monnaie libre (les deux propositions d'inertie d'Yvv du 29/10,
|
||||
retirées par lui le 03/11, Q12). Elles ne touchent pas le retrait : I1 et E1 ne sont pas retirés, et la
|
||||
garde du retrait n'a aucune exception (une proposition de membre, de toute sorte et de tout statut, sur
|
||||
une des dix unités, refuse le retrait : `retire.rs`, `member_versions`). Elles ne touchent pas non plus
|
||||
la réécriture d'I1 et d'E1 : sa garde lit les traces sur la Proposition d'amorce elle-même (votes,
|
||||
bulletins, coquilles, traductions de membre, objections, révision, empreinte), pas les autres
|
||||
propositions de l'article (`seed_amend.rs`, `facts`) ; elles portent sur l'inertie, pas sur le texte, et
|
||||
restent dans l'historique de l'article. En vigueur : seuls les trois ordres (modules en vigueur sans
|
||||
vote) ; en tout 0 vote, 0 bulletin, aucune E12, migration 11. **Lu par l'intégrateur le
|
||||
03/11 à 22 h 50** sur sa copie de la copie d'orch de 22 h 41 : l'attendu exact. Une trace fait refuser
|
||||
l'amendement concerné avec sa raison, et rien ne bouge pour lui.
|
||||
3. La relecture indépendante, inscrite « ok » dans `~/harness/etat/relectures.json`.
|
||||
- **`fadfff5` : ok.** Sous-agent neuf, sans le contexte de l'intégrateur, sur le delta `d66823b..fadfff5`
|
||||
(accord d'orch, en réponse à la question de l'intégrateur du 03/11 vers 23 h 27), lancé le 03/11 à 23 h 50 sur la consigne `m9/relis-m9b.md`,
|
||||
inscrit le 04/11 à 00 h 14 (transcript
|
||||
`~/.claude/projects/-home-yvv-Documents-PROD-DEV-libreDecision/d0a7108b-7f2f-484a-bcb0-de339238199d/subagents/agent-ad733bc6ca86cb5de.jsonl`,
|
||||
dernière ligne « fadfff5 : ok »). Aucun bloquant, aucun « à reprendre ». Ce qu'elle a vérifié :
|
||||
- le delta ne fait que `formula.reading` (fr, en, es) et les quatre raisons ; aucune empreinte, garde,
|
||||
seed ni migration ;
|
||||
- la nouvelle phrase vraie sur 201 484 750 cas (tous les W de 1 à 20 000, 179 692 tirés jusqu'à
|
||||
10 000 000, les cinq crans, les quorums de la borne basse à W) ; l'ancienne fausse sur 17 couples
|
||||
(W, cran), tous à W ≤ 15 ; en et es disent la même chose ; mutations vues rouges ;
|
||||
- la répétition rejouée par elle, réseau coupé, sur une copie de la copie d'orch, comparée table par
|
||||
table à celle de `d66823b` : seules les quatre raisons diffèrent (et la mesure quotidienne de
|
||||
participation, contrôlée en relançant `d66823b` sur la même copie) ;
|
||||
- boucle : 338 tests Rust, 116 vitest, e2e 48 / 48.
|
||||
Mineurs, liste d'attente : hors de la toile, l'écran ne dit pas que B^W peut demander une voix de plus
|
||||
dans un petit groupe ; « la part de « pour » demandée » puis « la part exigée » dans la même phrase.
|
||||
- **`d66823b` : à reprendre** (sous-agent neuf, sans le contexte de l'intégrateur, lancé le 03/11 à
|
||||
23 h 00 sur la consigne `m9/relis-m9.md` du scratchpad de l'intégrateur, inscrit à 23 h 25 ; transcript
|
||||
`~/.claude/projects/-home-yvv-Documents-PROD-DEV-libreDecision/d0a7108b-7f2f-484a-bcb0-de339238199d/subagents/agent-a81f5c0481c04ce9b.jsonl`).
|
||||
Aucun bloquant. **À reprendre** : `formula.reading` (« La formule du seuil » du vote déplié) finit par
|
||||
« quand tout le monde vote, la majorité M suffit », faux pour W ≤ 15 quand M·W est entier (B^W ajoute
|
||||
une voix ; aucun groupe actuel touché : l'essai a un membre, Monnaie libre 5 745) → lot U3c, lancé à
|
||||
23 h 26, avec les raisons de la file (« Proposition d'amorce » au lieu de « Proposition seed », elles
|
||||
iront dans le message de commit public). **Tient**, prouvé par elle :
|
||||
- le retrait : chaque route sondée sur les 10 unités des deux groupes répond 404, 400 ou 409 ; aucune
|
||||
vue ne contient une unité retirée ; une trace de membre posée à la main (quatre essais : inertie
|
||||
retirée sur F1.3, ordre retiré, brouillon sur N1.1, texte remplacé sur F1) refuse le retrait de son
|
||||
groupe, `member_proposal`, l'autre groupe appliqué ;
|
||||
- la migration 0012 additive ; « AUCUNE LIGNE SUPPRIMÉE » à chaque étape ;
|
||||
- le retour sur `de1b7eb` (binaire reconstruit par elle) : 34 articles, 8 ordres, JSON des 10 unités
|
||||
identique à avant M9 ; repassage sans écriture ; les deux propositions d'inertie retirées d'Yvv,
|
||||
JSON identique avant M9, après M9 et après le retour ;
|
||||
- les textes d'Yvv octet par octet : seule différence « n'y a » → « y a » ; empreintes recalculées ;
|
||||
- boucle : 336 tests Rust, 113 vitest, e2e 48 / 48.
|
||||
Mineurs, **liste d'attente** (aucun ne touche la production d'aujourd'hui) :
|
||||
- la garde de réécriture d'I1, E1, E2 ne lit que la Proposition d'amorce : un quorum fixé sur
|
||||
l'article, un texte rival d'un membre ou un vote sur une proposition d'inertie retirée la laissent
|
||||
passer (la garde du retrait compte le quorum fixé) ; la prémisse 2 lit ces traces avant l'acte ;
|
||||
- `inertia.explain`, première phrase : une tendance, pas une règle ;
|
||||
- `vote.others.rule` ne dit pas qu'une version retenue par une objection n'entre pas en vigueur ;
|
||||
- après un retour, une trace laissée sur F1 à O1 n'empêche pas le repassage de les tenir retirées
|
||||
(limite connue de l'ADR 0014).
|
||||
4. `git diff de1b7eb fadfff5 -- docker/ .woodpecker/ .env data/` → vide ; sous
|
||||
`crates/ld-server/migrations/`, le seul ajout `0012_retired.sql` ; `git diff d66823b fadfff5 -- crates/
|
||||
seeds/` → les quatre raisons et des tests seulement. **Vu par l'intégrateur le 04/11 à 00 h 13.**
|
||||
5. Boucle verte sur `fadfff5`, le 03/11 de 23 h 52 à 23 h 58 (copie principale sur `a43a098`, même code
|
||||
que `fadfff5` : le tableau seul en plus ; journal `m9/loop.log`), du premier coup : fmt, clippy
|
||||
`-D warnings`, 338 tests Rust, svelte-check 0 / 0 (652 fichiers), 116 vitest, build, e2e 48 / 48 (46
|
||||
contrôles C1 à C44, M1, M2 ; 23 h 54 à 23 h 58). Image de production construite du 03/11 à 23 h 58 au
|
||||
04/11 à 00 h 12 (copie principale sur `a43a098`), sortie 0 : étage serveur rejoué (fmt, clippy, 338 tests
|
||||
Rust, 0 échec, build release) ; étage web rejoué (svelte-check, vitest, build) ; image de contrôle retirée
|
||||
à 00 h 12 (journal `m9/docker.log`). Sur `d66823b` avant elle : boucle 23 h 00 - 23 h 07, image 23 h 07 -
|
||||
23 h 11, toutes deux vertes.
|
||||
**Recette écran sur la copie de production**, faite sur `d66823b` (U3c ne change à l'écran que la fin de
|
||||
`formula.reading`, épinglée par `rules.test.ts`) (l'état d'après M9 de la répétition, serveur de `d66823b`,
|
||||
chaîne coupée, connexion de développement sur cette copie seulement ; 32 captures,
|
||||
`m9/recette/shots/`, bureau 1360 et Pixel 7, pierre et nuit) : l'accueil sans session dit le texte pour
|
||||
qui n'est pas invité ; en tête du document, « Info ouverture doc de réf. » avec le paragraphe d'Yvv, les
|
||||
crans 2 · 4 · 2 · 3 et le quorum 12 · 12 · 50 · 50 · 50 lus dans la base ; le filet ; puis « Raison
|
||||
d'être » sans titre de section, « Unicité du compte », « Responsabilité civique », « Réciprocité »,
|
||||
chacune « Proposition d'amorce », cran 2, « 9 oui sur 12 » ; ni formule, ni inertie, ni ordonnancement
|
||||
dans la page ; le vote déplié d'E12 dit le vote permanent, l'inertie et le quorum de l'article ; le
|
||||
panneau des réglages se lit en entier (W 5 709, la valeur de la copie).
|
||||
6. Sur sonic, aucun témoin `/opt/libredecision-app/deploying`.
|
||||
|
||||
## Commande exacte
|
||||
|
||||
```sh
|
||||
cd /home/yvv/Documents/PROD/DEV/libreDecision
|
||||
git push prod fadfff55649140a8e240644c3a958ed256a291fd:main
|
||||
```
|
||||
|
||||
Puis la branche du chantier (orch, même mandat) : `git push origin chantier/acte-certification`.
|
||||
|
||||
## Vérifications après
|
||||
|
||||
- [ ] Santé vue de l'extérieur → `"build":"fadfff55"`.
|
||||
- [ ] Journaux : six « seed amendment applied » (`2026-11-03-i1-amorce`, `-e1-amorce`, `-e2-amorce` × essai,
|
||||
Monnaie libre), deux « article added … Between(4.5) », deux « units out of the document »,
|
||||
« publication suspended … waiting=17 », aucune ligne ERROR ni WARN.
|
||||
- [ ] Redémarrage à froid : aucun amendement rejoué.
|
||||
- [ ] Sur une copie fraîche : `m9/compare.py <copie d'avant> <copie d'après>` donne exactement la table de
|
||||
la répétition (à la mesure quotidienne de `settings` près), « AUCUNE LIGNE SUPPRIMÉE » ;
|
||||
`integrity_check` ok ; les cinq nouvelles lignes de file disent « Proposition d'amorce … » et
|
||||
« informations de fonctionnement sorties du document ».
|
||||
- [ ] Vue de l'extérieur, `https://decision.librodrome.org/` sans session : le texte pour qui n'est pas invité.
|
||||
- [ ] Le dépôt des textes reste à `9e0f45a`.
|
||||
|
||||
## Retour
|
||||
|
||||
- **Le code** : `docker tag libredecision-app:previous libredecision-app:latest`, puis
|
||||
`cd /opt/libredecision-app && docker compose up -d --force-recreate`. `previous` sera `de1b7eb`, qui ignore
|
||||
la migration 0012 et la colonne : les sept articles et les trois ordres réapparaissent, rien d'effacé
|
||||
(rejoué en répétition). Un nouveau passage à `fadfff5` ne rejoue rien.
|
||||
- **Les textes** : pas de retour automatique. L'ancien texte français reste dans l'historique de chaque
|
||||
proposition (`revisions`) ; les traductions en et es d'I1, E1, E2 sont remplacées sur place : les
|
||||
anciennes ne restent que dans git (le seed de `de1b7eb`, identiques 12 sur 12, vérifié par la relecture).
|
||||
Un nouvel amendement gardé pourrait les remettre. E12 reste (donnée).
|
||||
|
||||
## Exécution
|
||||
|
||||
Acte exécuté le 04/11/2026, 00h16-00h39, par orch (session dev-63, e66db8ca), sous mandat « déploie »
|
||||
(03/11 → 08/11, `~/harness/etat/mandats.json`).
|
||||
|
||||
- **Prémisses** (00 h 16 min 33 s) : santé `{"build":"de1b7eb5","ok":true}` ; `prod/main` → `de1b7eb`,
|
||||
ancêtre de `fadfff5` ; diff `de1b7eb..fadfff5` sur `docker/ .woodpecker/ .env data/` : 0 fichier ; sous
|
||||
`crates/ld-server/migrations/`, le seul ajout `0012_retired.sql` ; « fadfff5 : ok » inscrit (00 h 14), la
|
||||
seule relecture de ce code lancée par l'intégrateur, close ; aucun témoin `deploying` sur sonic.
|
||||
- **Copie d'avant** (00 h 16 min 42 s, `/tmp/ld-m9-avant`) : sortie de `m8/counts.sh` **identique à la ligne
|
||||
près** à celle de la copie de 22 h 41, base de la répétition ; `m9/traces.sh` : l'attendu de la prémisse 2
|
||||
(pour I1, E1, E2 et les dix unités, dans les deux groupes : 0 vote, bulletin, coquille, objection,
|
||||
traduction de membre et quorum fixé ; propositions de membre : seulement `inertia/withdrawn` sur I1 et E1
|
||||
dans Monnaie libre ; en vigueur : les trois ordres).
|
||||
- **Poussée** (00 h 17) : la commande du dossier, telle quelle → `de1b7eb..fadfff5 fadfff55… -> main`.
|
||||
- **Pipeline 43** : créé à 00 h 17 min 07 s ; image `latest` df3787b28ee9 construite à 00 h 23 min 19 s ;
|
||||
santé `"build":"fadfff55"` dès 00 h 24 min 02 s ; `success` à 00 h 34 min 49 s. `sbom-publish` en échec,
|
||||
comme au pipeline 42 (étape non bloquante, déjà en échec avant M9).
|
||||
- **Journal du premier démarrage** (23:23:46Z) : six « seed amendment applied » (`2026-11-03-i1-amorce`,
|
||||
`-e1-amorce`, `-e2-amorce`, essai et monnaie-libre), deux « article added
|
||||
amendment="2026-11-03-e12-responsabilite-civique" … placement=Between(4.5) », deux « units out of the
|
||||
document amendment="2026-11-03-fonctionnement-hors-vote" … retired=["F1", "F1.1", "F1.2", "F1.3", "N1",
|
||||
"N1.1", "O1", "ordre.formule", "ordre.inertie", "ordre.ordonnancement"] », « libreDecision listening …
|
||||
build=fadfff55 », « publication suspended … waiting=17 » ; aucune ligne ERROR ni WARN.
|
||||
- **Redémarrage à froid** (00 h 36 min 24 s) : « libreDecision listening … build=fadfff55 », puis
|
||||
« publication suspended … waiting=17 » ; aucun amendement rejoué, aucune ligne ERROR ni WARN. Santé ensuite
|
||||
`{"build":"fadfff55","ok":true}`.
|
||||
- **Copie d'après** (00 h 36 min 42 s, `/tmp/ld-m9-apres`) : `m9/compare.py <avant> <après>` donne la table
|
||||
de la répétition, ligne pour ligne : `_sqlx_migrations` 11 → 12 (+ 12 `retired`) ; `units` 84 → 86
|
||||
(+ E12 × 2, `fondamental`, position 4,5, cran 2 ; 20 unités marquées
|
||||
`2026-11-03-fonctionnement-hors-vote`, les 64 autres aux deux colonnes nouvelles vides) ; `proposals`
|
||||
91 → 93 (+ E12 × 2, `open`, `seed`, `seed_featured` ; I1, E1, E2 × 2 : `body`, `revision`, `updated_at`) ;
|
||||
`revisions` 123 → 131 (+ 6 `amendment`, + 2 `initial`) ; `translations` 144 → 148 (+ 4, 12 remplacées :
|
||||
`body`, `source_hash`, `updated_at`) ; `seed_amendments` 26 → 32 ; `article_amendments` et
|
||||
`retirement_amendments` nouvelles, 2 décisions `applied` chacune ; `outbox` 25 → 30 (`pending=17
|
||||
done=13`), les 12 en attente reportées (`next_at`) ; les 21 autres tables identiques.
|
||||
**« AUCUNE LIGNE SUPPRIMÉE »** ; `integrity_check` ok ; `foreign_key_check` vide. Les cinq nouvelles
|
||||
lignes de file : « Proposition d'amorce réécrite : I1, … », « … : E1, … », « … : E2, … », « Proposition
|
||||
d'amorce ajoutée : E12, … », « informations de fonctionnement sorties du document … ».
|
||||
- **Écart à la répétition, `settings`** : 2 lignes changées, l'heure seule — `wot_size` (5709 → 5709) et
|
||||
`wot_source` (`chain:https://rpc.duniter.org`, inchangé), relus au démarrage, le réseau répondant (la
|
||||
répétition, réseau coupé, gardait la dernière valeur : son seul WARN). Les 5 mesures de participation ne
|
||||
sont pas refaites : la production les a refaites le 03/11 à 22:12:52Z, moins de 24 h avant (la
|
||||
répétition, sur la copie de 22 h 41, les avait refaites). Rien d'autre.
|
||||
- **Vu de l'extérieur** (00 h 37 min 59 s, Chromium, locale `fr-FR`, sans session) :
|
||||
`https://decision.librodrome.org/` dit « 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. » ;
|
||||
aucune requête hors de `decision.librodrome.org`.
|
||||
- **Dépôt des textes** : `main` → `9e0f45a9e8d4af356a0c42daa5f4fb9eb9e6dfb3`, inchangé.
|
||||
- **Retour** : `previous` = 78ca8a1d10e4 (construite le 03/11 à 20 h 31 min 25 s, `de1b7eb`) ; le conteneur
|
||||
tourne sur `latest` df3787b28ee9.
|
||||
- **Copies retirées** à 00 h 38 min 25 s : `/tmp/ld-m9-copie` (22 h 41, base des répétitions),
|
||||
`/tmp/ld-m9-avant`, `/tmp/ld-m9-apres`. Les copies de l'intégrateur (`m9/base`, `m9/rep`, `m9/recette`,
|
||||
`m9/snap-d66823b`) sont à retirer par lui, sur ce message.
|
||||
- **Branche locale** : `main` avancée `de1b7eb..fadfff5` à 00 h 38.
|
||||
- Écart au déroulé : aucun, hors l'écart de `settings` ci-dessus, qui n'est pas de M9.
|
||||
- Copies de l'intégrateur retirées le 04/11 à 00 h 41 (scratchpad de libredecision-d4) : `m9/base` (la
|
||||
copie de la copie d'orch de 22 h 41, donnée aux relecteurs), `m9/rep` (la base de la répétition de
|
||||
`fadfff5`, avec son dépôt nu local et ses états), `m9/recette` (la base et les captures de la recette sur
|
||||
l'état d'après M9) et `m9/snap-d66823b` (les états de la répétition de `d66823b`). Plus aucune base dans ce
|
||||
scratchpad.
|
||||
@@ -430,6 +430,104 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lot U3c (review of d66823b), `formula.reading`: « quand tout le monde vote, la part exigée est
|
||||
/// la majorité M ». On every W from 1 to 10 000 000 — the groups the server counts by their
|
||||
/// members (the test group: 2) and the web of trust it takes as true (`wot::PLAUSIBLE`, 100 to
|
||||
/// 10 000 000) — at the five levels, on both bounds of the quorum an author may set (in Monnaie
|
||||
/// libre and in the test group: 1, 2, W) and at both bounds of the usual participation (G at its
|
||||
/// lowest and at its highest): when the W vote, the voters judged are T′ = max(W, Q), never fewer
|
||||
/// than W, and the share asked of them is M_k, exactly.
|
||||
///
|
||||
/// The phrase of before, « quand tout le monde vote, la majorité M suffit », read as « ⌈M·W⌉
|
||||
/// « pour » pass », was false: R = B^W + M·W, and B^W adds one « pour » where M·W is a whole
|
||||
/// number, on 17 (W, level) pairs, every W up to 15 (W 4, level 2: 3 « pour » of 4, not 2; W 5,
|
||||
/// level 4: 4, not 3; W 10, level 4: 7, not 6) — none from W = 16 to 200, where it is checked.
|
||||
/// The new phrase says nothing of B^W: `formula.B.wot` does, in the web of trust only.
|
||||
#[test]
|
||||
fn when_everyone_votes_the_share_asked_is_m_on_every_electorate() {
|
||||
const LARGEST: u32 = 10_000_000;
|
||||
let params: Vec<(InertiaLevel, f64, FormulaParams)> = InertiaLevel::ALL
|
||||
.iter()
|
||||
.flat_map(|k| {
|
||||
[Participation::LOWEST, Participation::HIGHEST].map(|p| {
|
||||
let scale = Scale { electorate: 1, participation: p, quorums: [1; 5] };
|
||||
(*k, k.majority_pct() / 100.0, scale.params(*k, 1))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut cases = 0u64;
|
||||
for w in 1..=LARGEST {
|
||||
let ((lowest, highest), (lowest_essai, _)) = (quorum_bounds(w, false), quorum_bounds(w, true));
|
||||
// The share reads the voters judged only: W for every quorum up to W, the quorum above
|
||||
// it (2 on a web of 1, outside the test group).
|
||||
let judged = [lowest_essai, lowest, highest].map(|q| judged_voters(w, q));
|
||||
assert!(judged.iter().all(|v| *v >= w), "W {w}: {judged:?}");
|
||||
for (i, voters) in judged.iter().enumerate() {
|
||||
if judged[..i].contains(voters) {
|
||||
continue;
|
||||
}
|
||||
for (k, m, p) in ¶ms {
|
||||
let share = required_ratio(w, *voters, p);
|
||||
assert_eq!(share, *m, "W {w}, level {}, G {}, {voters} voters judged", k.get(), p.gradient);
|
||||
cases += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Every W, ten (level, G) each, and on a web of 1 the quorum of 2 besides.
|
||||
assert_eq!(cases, u64::from(LARGEST) * 10 + 10);
|
||||
// The share the evaluation gives (`Evaluation::required_ratio`), on every split of the W
|
||||
// voters, on the smallest electorates and on the web of trust.
|
||||
for w in (1..=40).chain([100, 101, 5707, 7224, LARGEST]) {
|
||||
let scale =
|
||||
Scale { electorate: w, participation: Participation::of(w, 50.0, None, 0).ratio, quorums: [2; 5] };
|
||||
for k in InertiaLevel::ALL {
|
||||
for q in [quorum_bounds(w, false).0, w] {
|
||||
for against in [0, w / 3, w / 2, w] {
|
||||
let tally = Tally { for_votes: w - against, against };
|
||||
let e = scale.evaluate(tally, k, q).expect("valid");
|
||||
assert_eq!(e.required_ratio, k.majority_pct() / 100.0, "W {w}, level {}, Q {q}", k.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// « La majorité M suffit » was false: the « pour » asked when the W vote, against ⌈M·W⌉.
|
||||
let mut more = Vec::new();
|
||||
for w in 1..=200u32 {
|
||||
for k in InertiaLevel::ALL {
|
||||
let p = Scale { electorate: w, participation: 0.1, quorums: [1; 5] }.params(k, 1);
|
||||
let asked = crate::threshold::wot_threshold(w, w, &p).expect("valid");
|
||||
let majority = (k.majority_pct() as u32 * w).div_ceil(100);
|
||||
if asked != majority {
|
||||
assert_eq!(asked, majority + 1, "W {w}, level {}: one « pour » more, B^W", k.get());
|
||||
assert_eq!(k.majority_pct() as u32 * w % 100, 0, "W {w}: M·W is a whole number");
|
||||
more.push((w, k.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
more,
|
||||
[
|
||||
(2, 1),
|
||||
(2, 2),
|
||||
(4, 1),
|
||||
(4, 2),
|
||||
(5, 4),
|
||||
(6, 1),
|
||||
(6, 2),
|
||||
(8, 1),
|
||||
(8, 2),
|
||||
(10, 1),
|
||||
(10, 2),
|
||||
(10, 4),
|
||||
(12, 1),
|
||||
(12, 2),
|
||||
(14, 1),
|
||||
(14, 2),
|
||||
(15, 4)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_author_sets_a_quorum_from_two_to_w_from_one_in_the_test_group() {
|
||||
assert_eq!(quorum_bounds(5745, false), (2, 5745));
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
//! ```
|
||||
//!
|
||||
//! R is the minimum number of *for* votes; a version passes when `for ≥ ⌈R⌉`.
|
||||
//! W is the size of the electorate, T the votes cast (for + against). At low
|
||||
//! participation the formula asks for near-unanimity, at full participation the
|
||||
//! simple majority M suffices. Q is the **quorum** (ADR 0013, Yvv, 2026-11-02:
|
||||
//! W is the size of the electorate, T the votes cast (for + against). The share of
|
||||
//! *for* asked is judged on T' voters, never fewer than the quorum: it is at its
|
||||
//! highest at the quorum (the share of the level at T' = Q), then falls as turnout
|
||||
//! rises above it, down to the simple majority M when the whole electorate votes.
|
||||
//! Below the quorum it does not rise any further: the voters missing count as
|
||||
//! against. Q is the **quorum** (ADR 0013, Yvv, 2026-11-02:
|
||||
//! « ce n'est pas un nombre de vote pour a priori, c'est plutôt un quorum, à partir
|
||||
//! de ce nombre, alors il faut la bonne proportion attendue par l'indice ») — and
|
||||
//! below it the voters missing count as against (`judged_voters`). It replaces the
|
||||
@@ -234,6 +237,28 @@ mod tests {
|
||||
assert_eq!(e.missing, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_share_is_highest_at_the_quorum_then_falls_to_the_majority() {
|
||||
// What the head of this module says (lot U2, review of M6c): judged on T' voters, the
|
||||
// share of *for* is at its highest at the quorum, falls as turnout rises, and is M when
|
||||
// the whole electorate votes; below the quorum it stays that of the quorum.
|
||||
let (w, q) = (5745, 12);
|
||||
let p = standard(q);
|
||||
let share = |cast: u32| required_ratio(w, judged_voters(cast, q), &p);
|
||||
for cast in 0..q {
|
||||
assert!((share(cast) - share(q)).abs() < 1e-12, "below the quorum, at {cast}");
|
||||
}
|
||||
let mut last = share(q);
|
||||
for cast in (q + 1..w).step_by(7).chain([w]) {
|
||||
let now = share(cast);
|
||||
assert!(now < last, "falls at {cast}: {now} after {last}");
|
||||
last = now;
|
||||
}
|
||||
assert!((share(w) - 0.5).abs() < 1e-12, "M at full turnout");
|
||||
// Never « near-unanimity »: at this level, 85 % of the quorum.
|
||||
assert!(share(q) > 0.8 && share(q) < 0.9, "at the quorum: {}", share(q));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_with_the_typescript_reference() {
|
||||
// Values computed by maquette/frontend/app/engine/threshold.ts (C = 0).
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
-- The information on how the vote works leaves the document (2026-11-03, lot T4,
|
||||
-- ADR 0014). Additive only: no row is dropped, no column an older image reads is
|
||||
-- rewritten.
|
||||
--
|
||||
-- Yvv, 2026-11-03: « les 3 derniers modules ne sont pas de votes : 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. » The articles of the
|
||||
-- sections `formule`, `inertie` and `ordonnancement`, and the order of each, are no
|
||||
-- texts to vote. A new base is born without them (the seed no longer holds them); an
|
||||
-- existing base keeps every row of them — units, versions, revisions, translations —
|
||||
-- and marks the units retired: the document no longer lists them, counts them,
|
||||
-- votes on them or publishes them. An older image (rollback) ignores the column and
|
||||
-- shows them again exactly as they were.
|
||||
|
||||
-- When the unit left the document (NULL: it is in it), and the amendment that took it out.
|
||||
ALTER TABLE units ADD COLUMN retired_at TEXT;
|
||||
ALTER TABLE units ADD COLUMN retired_reason TEXT;
|
||||
|
||||
-- The retirement of an existing document's units (src/retire.rs): decided once per
|
||||
-- document, only where no person left a trace on any of them. Each decision is kept
|
||||
-- here: applied (and the codes retired), refused (why, and on which unit), or present
|
||||
-- (the document was born without them).
|
||||
CREATE TABLE retirement_amendments (
|
||||
amendment_id TEXT NOT NULL,
|
||||
doc_id TEXT NOT NULL REFERENCES documents(id),
|
||||
outcome TEXT NOT NULL, -- applied | refused | present
|
||||
reason TEXT, -- why it was refused
|
||||
unit_code TEXT, -- the unit the trace was found on
|
||||
retired TEXT, -- JSON array of the codes retired
|
||||
at TEXT NOT NULL,
|
||||
PRIMARY KEY (amendment_id, doc_id)
|
||||
);
|
||||
|
||||
-- An article the seed now holds, added to an existing document (src/article_amend.rs):
|
||||
-- decided once per document. Each decision is kept here: applied (the unit created,
|
||||
-- where it was placed and why), refused (why), or present (born with it).
|
||||
CREATE TABLE article_amendments (
|
||||
amendment_id TEXT NOT NULL,
|
||||
doc_id TEXT NOT NULL REFERENCES documents(id),
|
||||
unit_code TEXT NOT NULL,
|
||||
unit_id TEXT, -- the unit created, or found
|
||||
outcome TEXT NOT NULL, -- applied | refused | present
|
||||
reason TEXT, -- why it was refused
|
||||
placement TEXT, -- between (where the seed puts it) | end (of its section)
|
||||
placement_reason TEXT, -- why at the end
|
||||
position REAL,
|
||||
at TEXT NOT NULL,
|
||||
PRIMARY KEY (amendment_id, doc_id)
|
||||
);
|
||||
@@ -0,0 +1,378 @@
|
||||
//! Article amendments — an article the seed now holds, added to a document that was
|
||||
//! born without it.
|
||||
//!
|
||||
//! Yvv, 2026-11-03 (lot T4): « AJOUT d'un texte : RESPONSABILITE CIVIQUE → Je déclare sur
|
||||
//! l'honneur avoir la capacité légale et physique de signer un contrat et d'en assumer la
|
||||
//! responsabilité civique, pour certifier à mon tour. » A new base gets it from the seed:
|
||||
//! E12 « Responsabilité civique », a seed proposal (ADR 0011) at `seed::SEED_PROPOSAL_LEVEL`,
|
||||
//! between E1 and E2 in the section `fondamental`. An existing base — production holds the
|
||||
//! certification act twice, Monnaie libre and the test group — gets it through an amendment
|
||||
//! listed here, decided once per document, on the model of `module_amend`: the article of
|
||||
//! the seed is written as a new base writes it (`seed::write_unit`: its unit, its version
|
||||
//! open and put forward, its first revision, its translations), and nothing else is
|
||||
//! written — no text changes, no vote is touched, no other article moves.
|
||||
//!
|
||||
//! **Where it goes.** The order of a section is a module (ADR 0009). In force at the start
|
||||
//! without a vote (a seed a minima), it reads the positions of the articles. While no vote
|
||||
//! exists on that order (`quorum::voted_units`: a vote, a line of a ballot, an adoption), the
|
||||
//! article takes the place the seed gives it: between `after` and `before`, adjacent in the
|
||||
//! order of the section now, half-way between their positions — so the order in force says
|
||||
//! it at once. Once someone voted on the order, or when the two are no longer adjacent, or
|
||||
//! one of them is gone from the section, its place is that of any new article: the end of
|
||||
//! its section (`ld_engine::order::place_at_end`); the order of the section is then voted
|
||||
//! as always (an open order completes it at its end, ADR 0009).
|
||||
//!
|
||||
//! The document holds a unit of that code already: present when it is the seed's (born
|
||||
//! with it), refused otherwise (`code_taken`); no such section in the document: refused
|
||||
//! (`no_section`). Each decision is kept in `article_amendments` (where the article went,
|
||||
//! and why at the end) and in the logs. Applied where the document publishes, the
|
||||
//! publication queue gets a row, which waits for the first text (ADR 0011).
|
||||
|
||||
use crate::AppState;
|
||||
use crate::quorum;
|
||||
use crate::seed::{self, SeedDoc, SeedUnit};
|
||||
use crate::seed_amend::fingerprint;
|
||||
use crate::util;
|
||||
use sqlx::SqliteConnection;
|
||||
|
||||
/// One article to add to every document instantiated from a seed.
|
||||
pub struct ArticleAmendment {
|
||||
/// Stable identifier: each document's decision is kept under it.
|
||||
pub id: &'static str,
|
||||
/// Slug of the seed document.
|
||||
pub doc: &'static str,
|
||||
/// Code of the article: one no document holds yet.
|
||||
pub unit: &'static str,
|
||||
/// Fingerprint of its text in the seed (source language), which must hash to it: a seed
|
||||
/// edited later never reaches a base by this way.
|
||||
pub text: &'static str,
|
||||
/// The two articles of its section it goes between, in this order.
|
||||
pub after: &'static str,
|
||||
pub before: &'static str,
|
||||
/// Reason given to the publication queue, and so to the commit of the texts.
|
||||
pub reason: &'static str,
|
||||
}
|
||||
|
||||
pub const AMENDMENTS: &[ArticleAmendment] = &[ArticleAmendment {
|
||||
id: "2026-11-03-e12-responsabilite-civique",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "E12",
|
||||
text: "3fa5a8e5df24e9ebb6b2882968bd0b6989c820a3f60d7c67bdb7fccf17ae1296",
|
||||
after: "E1",
|
||||
before: "E2",
|
||||
reason: "Proposition d'amorce ajoutée : E12, Responsabilité civique (texte d'Yvv du 03/11)",
|
||||
}];
|
||||
|
||||
/// What the database says of the document.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct Facts {
|
||||
/// A unit of that code: `Some(true)` the seed's, `Some(false)` another one.
|
||||
pub existing: Option<bool>,
|
||||
/// The section is in the document (an article of it, not retired).
|
||||
pub section: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Verdict {
|
||||
Apply,
|
||||
/// Born with it: nothing to do.
|
||||
Present,
|
||||
Refuse(&'static str),
|
||||
}
|
||||
|
||||
/// Whether the article is added, apart from the database.
|
||||
pub fn verdict(f: &Facts) -> Verdict {
|
||||
match f.existing {
|
||||
Some(true) => Verdict::Present,
|
||||
Some(false) => Verdict::Refuse("code_taken"),
|
||||
None if !f.section => Verdict::Refuse("no_section"),
|
||||
None => Verdict::Apply,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the article goes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Placement {
|
||||
/// Where the seed puts it, at this position: the order in force says it at once.
|
||||
Between(f64),
|
||||
/// At the end of its section, as any new article (ADR 0009), and why.
|
||||
End(&'static str),
|
||||
}
|
||||
|
||||
/// Where it goes, apart from the database: `voted`, whether a vote exists on the order of
|
||||
/// its section; `members`, the section in its order now (code, position); `positions`,
|
||||
/// every article of the document.
|
||||
pub fn placement(voted: bool, members: &[(String, f64)], after: &str, before: &str, positions: &[f64]) -> Placement {
|
||||
if voted {
|
||||
return Placement::End("order_voted");
|
||||
}
|
||||
let at = |code: &str| members.iter().position(|(c, _)| c == code);
|
||||
let (Some(i), Some(j)) = (at(after), at(before)) else { return Placement::End("not_in_section") };
|
||||
let (a, b) = (members[i].1, members[j].1);
|
||||
// Adjacent in the order, and nothing of the document between their places.
|
||||
if j != i + 1 || a >= b || positions.iter().any(|p| *p > a && *p < b) {
|
||||
return Placement::End("not_adjacent");
|
||||
}
|
||||
Placement::Between(f64::midpoint(a, b))
|
||||
}
|
||||
|
||||
/// A decision taken at this start.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Outcome {
|
||||
pub amendment: &'static str,
|
||||
pub group: String,
|
||||
pub verdict: Verdict,
|
||||
pub placement: Option<Placement>,
|
||||
}
|
||||
|
||||
/// Decides every amendment not yet decided, document by document.
|
||||
pub async fn apply(state: &AppState, seeds: &[SeedDoc], list: &[ArticleAmendment]) -> anyhow::Result<Vec<Outcome>> {
|
||||
let mut out = Vec::new();
|
||||
for a in list {
|
||||
let Some((seed, unit)) = seed_says(seeds, a) else { continue };
|
||||
let docs: Vec<(String, String, Option<String>)> =
|
||||
sqlx::query_as("SELECT id, group_slug, publish_path FROM documents WHERE slug = ? ORDER BY group_slug")
|
||||
.bind(a.doc)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
for (doc_id, group, publish_path) in docs {
|
||||
let Some((v, placement)) = decide(state, a, seed, unit, &doc_id, publish_path.is_some()).await? else {
|
||||
continue;
|
||||
};
|
||||
match (v, placement) {
|
||||
(Verdict::Apply, Some(p)) => {
|
||||
tracing::info!(amendment = a.id, %group, unit = a.unit, placement = ?p, "article added")
|
||||
}
|
||||
(Verdict::Refuse(why), _) => {
|
||||
tracing::warn!(amendment = a.id, %group, unit = a.unit, reason = why, "article amendment refused: the document stays as it is")
|
||||
}
|
||||
_ => tracing::info!(amendment = a.id, %group, unit = a.unit, "article amendment: already there"),
|
||||
}
|
||||
if v == Verdict::Apply && publish_path.is_some() {
|
||||
state.publish_wake.notify_one();
|
||||
}
|
||||
out.push(Outcome { amendment: a.id, group, verdict: v, placement });
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The seed must hold the article as a seed proposal with the text the amendment names,
|
||||
/// between the two it names, in their section.
|
||||
fn seed_says<'a>(seeds: &'a [SeedDoc], a: &ArticleAmendment) -> Option<(&'a SeedDoc, &'a SeedUnit)> {
|
||||
let found = seeds.iter().find(|s| s.slug == a.doc).and_then(|s| {
|
||||
let src = s.source_lang.as_str();
|
||||
let of = |code: &str| s.units.iter().find(|u| u.code == code);
|
||||
let (u, after, before) = (of(a.unit)?, of(a.after)?, of(a.before)?);
|
||||
let v = u.versions.iter().find(|v| v.seed_proposal)?;
|
||||
let print = fingerprint(v.title.get(src)?, v.body.get(src)?);
|
||||
let placed = u.section.is_some()
|
||||
&& after.section == u.section
|
||||
&& before.section == u.section
|
||||
&& after.position < u.position
|
||||
&& u.position < before.position;
|
||||
(print == a.text && u.inertia.is_none() && placed).then_some((s, u))
|
||||
});
|
||||
if found.is_none() {
|
||||
tracing::error!(
|
||||
amendment = a.id,
|
||||
doc = a.doc,
|
||||
unit = a.unit,
|
||||
"article amendment: the seed does not hold this article where it names it, nothing decided"
|
||||
);
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
async fn facts(conn: &mut SqliteConnection, doc_id: &str, a: &ArticleAmendment, section: &str) -> sqlx::Result<Facts> {
|
||||
let existing: Option<(String, i64)> = sqlx::query_as(
|
||||
"SELECT u.id, (SELECT COUNT(*) FROM proposals p WHERE p.unit_id = u.id AND p.origin = 'seed')
|
||||
FROM units u WHERE u.doc_id = ? AND u.code = ?",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.bind(a.unit)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?;
|
||||
let in_section: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM units WHERE doc_id = ? AND kind = 'article' AND section = ? AND retired_at IS NULL",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.bind(section)
|
||||
.fetch_one(&mut *conn)
|
||||
.await?;
|
||||
Ok(Facts { existing: existing.map(|(_, seed)| seed > 0), section: in_section > 0 })
|
||||
}
|
||||
|
||||
/// The placement, and the end of the section (the place of any new article there).
|
||||
async fn where_to(
|
||||
conn: &mut SqliteConnection,
|
||||
doc_id: &str,
|
||||
a: &ArticleAmendment,
|
||||
section: &str,
|
||||
) -> sqlx::Result<(Placement, f64)> {
|
||||
let articles: Vec<(String, String, Option<String>, f64)> = sqlx::query_as(
|
||||
"SELECT id, code, section, position FROM units WHERE doc_id = ? AND kind = 'article' AND retired_at IS NULL",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.fetch_all(&mut *conn)
|
||||
.await?;
|
||||
let order: Option<String> = sqlx::query_scalar(
|
||||
"SELECT id FROM units WHERE doc_id = ? AND kind = 'order' AND section = ? AND retired_at IS NULL",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.bind(section)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?;
|
||||
let voted = match &order {
|
||||
Some(id) => quorum::voted_units(conn, doc_id).await?.contains(id),
|
||||
None => false,
|
||||
};
|
||||
let members: Vec<(String, f64)> = crate::order::sections_now(conn, doc_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|(s, _)| s == section)
|
||||
.map(|(_, ids)| {
|
||||
ids.iter().filter_map(|id| articles.iter().find(|x| &x.0 == id).map(|x| (x.1.clone(), x.3))).collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let positions: Vec<f64> = articles.iter().map(|x| x.3).collect();
|
||||
let places: Vec<(Option<&str>, f64)> = articles.iter().map(|x| (x.2.as_deref(), x.3)).collect();
|
||||
let end = ld_engine::order::place_at_end(&places, Some(section));
|
||||
Ok((placement(voted, &members, a.after, a.before, &positions), end))
|
||||
}
|
||||
|
||||
/// One document, in one transaction: `None` when it was decided at an earlier start.
|
||||
async fn decide(
|
||||
state: &AppState,
|
||||
a: &ArticleAmendment,
|
||||
seed: &SeedDoc,
|
||||
unit: &SeedUnit,
|
||||
doc_id: &str,
|
||||
publishes: bool,
|
||||
) -> anyhow::Result<Option<(Verdict, Option<Placement>)>> {
|
||||
let mut tx = state.db.begin().await?;
|
||||
let decided: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM article_amendments WHERE amendment_id = ? AND doc_id = ?")
|
||||
.bind(a.id)
|
||||
.bind(doc_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
if decided > 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let section = unit.section.clone().unwrap_or_default();
|
||||
let found = facts(&mut tx, doc_id, a, §ion).await?;
|
||||
let v = verdict(&found);
|
||||
let now = util::now();
|
||||
let (mut placement, mut position, mut unit_id) = (None, None, None);
|
||||
if v == Verdict::Apply {
|
||||
let (p, end) = where_to(&mut tx, doc_id, a, §ion).await?;
|
||||
let at = match p {
|
||||
Placement::Between(at) => at,
|
||||
Placement::End(_) => end,
|
||||
};
|
||||
unit_id = Some(seed::write_unit(&mut tx, doc_id, &seed.source_lang, unit, at, &now).await?);
|
||||
(placement, position) = (Some(p), Some(at));
|
||||
if publishes {
|
||||
let where_ = match p {
|
||||
Placement::Between(_) => format!("entre {} et {}", a.after, a.before),
|
||||
Placement::End(_) => "en fin de section".to_owned(),
|
||||
};
|
||||
sqlx::query("INSERT INTO outbox (doc_id, reason, created_at, next_at) VALUES (?, ?, ?, ?)")
|
||||
.bind(doc_id)
|
||||
.bind(format!("{}, {where_}", a.reason))
|
||||
.bind(&now)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
} else if v == Verdict::Present {
|
||||
unit_id = sqlx::query_scalar("SELECT id FROM units WHERE doc_id = ? AND code = ?")
|
||||
.bind(doc_id)
|
||||
.bind(a.unit)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
let (outcome, reason) = match v {
|
||||
Verdict::Apply => ("applied", None),
|
||||
Verdict::Present => ("present", None),
|
||||
Verdict::Refuse(why) => ("refused", Some(why)),
|
||||
};
|
||||
let (kind, why_end) = match placement {
|
||||
Some(Placement::Between(_)) => (Some("between"), None),
|
||||
Some(Placement::End(why)) => (Some("end"), Some(why)),
|
||||
None => (None, None),
|
||||
};
|
||||
sqlx::query(
|
||||
"INSERT INTO article_amendments (amendment_id, doc_id, unit_code, unit_id, outcome, reason, placement,
|
||||
placement_reason, position, at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(a.id)
|
||||
.bind(doc_id)
|
||||
.bind(a.unit)
|
||||
.bind(&unit_id)
|
||||
.bind(outcome)
|
||||
.bind(reason)
|
||||
.bind(kind)
|
||||
.bind(why_end)
|
||||
.bind(position)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Some((v, placement)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn section(codes: &[(&str, f64)]) -> Vec<(String, f64)> {
|
||||
codes.iter().map(|(c, p)| ((*c).to_owned(), *p)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_without_the_article_receives_it() {
|
||||
assert_eq!(verdict(&Facts { existing: None, section: true }), Verdict::Apply);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn born_with_it_or_its_code_taken_it_is_not_written_again() {
|
||||
assert_eq!(verdict(&Facts { existing: Some(true), section: true }), Verdict::Present);
|
||||
assert_eq!(verdict(&Facts { existing: Some(false), section: true }), Verdict::Refuse("code_taken"));
|
||||
assert_eq!(verdict(&Facts { existing: None, section: false }), Verdict::Refuse("no_section"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_goes_between_its_two_while_the_order_was_never_voted() {
|
||||
let fondamental = section(&[("T1", 3.0), ("E1", 4.0), ("E2", 5.0)]);
|
||||
let all = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
|
||||
assert_eq!(placement(false, &fondamental, "E1", "E2", &all), Placement::Between(4.5));
|
||||
// A member's article at the end of the section changes nothing between them.
|
||||
let longer = section(&[("T1", 3.0), ("E1", 4.0), ("E2", 5.0), ("N3", 5.5)]);
|
||||
assert_eq!(placement(false, &longer, "E1", "E2", &[3.0, 4.0, 5.0, 5.5, 6.0]), Placement::Between(4.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn otherwise_at_the_end_of_its_section_and_why() {
|
||||
let fondamental = section(&[("T1", 3.0), ("E1", 4.0), ("E2", 5.0)]);
|
||||
let all = [3.0, 4.0, 5.0];
|
||||
assert_eq!(placement(true, &fondamental, "E1", "E2", &all), Placement::End("order_voted"));
|
||||
// An order voted into force that put E2 first.
|
||||
let moved = section(&[("T1", 3.0), ("E2", 4.0), ("E1", 5.0)]);
|
||||
assert_eq!(placement(false, &moved, "E1", "E2", &all), Placement::End("not_adjacent"));
|
||||
let apart = section(&[("E1", 4.0), ("T1", 4.5), ("E2", 5.0)]);
|
||||
assert_eq!(placement(false, &apart, "E1", "E2", &all), Placement::End("not_adjacent"));
|
||||
// Something of another section between their places.
|
||||
assert_eq!(placement(false, &fondamental, "E1", "E2", &[4.0, 4.2, 5.0]), Placement::End("not_adjacent"));
|
||||
let gone = section(&[("T1", 3.0), ("E1", 4.0)]);
|
||||
assert_eq!(placement(false, &gone, "E1", "E2", &all), Placement::End("not_in_section"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shipped_amendment_adds_e12_between_e1_and_e2() {
|
||||
let a = &AMENDMENTS[0];
|
||||
assert_eq!((a.unit, a.after, a.before), ("E12", "E1", "E2"));
|
||||
}
|
||||
}
|
||||
@@ -156,10 +156,12 @@ pub async fn group(
|
||||
sqlx::query_as::<_, DocListRow>(
|
||||
"SELECT d.id, d.slug, d.title, d.summary,
|
||||
(SELECT COUNT(*) FROM units u
|
||||
WHERE u.doc_id = d.id AND u.kind = 'article' AND u.in_force_id IS NOT NULL) AS in_force,
|
||||
-- Articles in force; open proposals of articles and modules alike (ADR 0009).
|
||||
WHERE u.doc_id = d.id AND u.kind = 'article' AND u.in_force_id IS NOT NULL
|
||||
AND u.retired_at IS NULL) AS in_force,
|
||||
-- Articles in force; open proposals of articles and modules alike (ADR 0009);
|
||||
-- nothing of a unit out of the document (ADR 0014).
|
||||
(SELECT COUNT(*) FROM proposals p JOIN units u ON u.id = p.unit_id
|
||||
WHERE u.doc_id = d.id AND p.status = 'open') AS open
|
||||
WHERE u.doc_id = d.id AND p.status = 'open' AND u.retired_at IS NULL) AS open
|
||||
FROM documents d WHERE d.group_slug = ? ORDER BY d.created_at",
|
||||
)
|
||||
.bind(&slug)
|
||||
@@ -298,7 +300,7 @@ pub fn inertia_of(unit: &UnitRow) -> InertiaLevel {
|
||||
pub struct DocState {
|
||||
pub doc: DocRow,
|
||||
pub group: GroupRow,
|
||||
/// Every unit, articles and modules, by position.
|
||||
/// Every unit in the document, articles and modules, by position: none retired (ADR 0014).
|
||||
pub units: Vec<UnitRow>,
|
||||
/// The units that are modules (ADR 0009), by unit id; any other is an article.
|
||||
pub module_kinds: HashMap<String, ModuleKind>,
|
||||
@@ -359,14 +361,17 @@ pub async fn electorate(state: &AppState, group: &GroupRow) -> AppResult<(u32, &
|
||||
pub async fn load_state(state: &AppState, doc_id: &str) -> AppResult<DocState> {
|
||||
let doc = load_doc(state, doc_id).await?;
|
||||
let group = load_group(state, &doc.group_slug).await?;
|
||||
// The units in the document: a unit retired (ADR 0014) is not read, nor its versions —
|
||||
// neither listed, nor counted, nor voted on, nor published (`retire`).
|
||||
let units = sqlx::query_as::<_, UnitRow>(
|
||||
"SELECT id, code, section, position, inertia, in_force_id, quorum FROM units WHERE doc_id = ? ORDER BY position, id",
|
||||
"SELECT id, code, section, position, inertia, in_force_id, quorum FROM units
|
||||
WHERE doc_id = ? AND retired_at IS NULL ORDER BY position, id",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
let kinds: Vec<(String, String)> =
|
||||
sqlx::query_as("SELECT id, kind FROM units WHERE doc_id = ? AND kind <> 'article'")
|
||||
sqlx::query_as("SELECT id, kind FROM units WHERE doc_id = ? AND kind <> 'article' AND retired_at IS NULL")
|
||||
.bind(doc_id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
@@ -375,7 +380,7 @@ pub async fn load_state(state: &AppState, doc_id: &str) -> AppResult<DocState> {
|
||||
// Same second (the variants of a seed): the order they were written in, not their
|
||||
// random identifiers. The engine ranks rivals on its own; this order is what people see.
|
||||
let proposals = sqlx::query_as::<_, ProposalRow>(&format!(
|
||||
"{PROPOSAL_SELECT} WHERE p.unit_id IN (SELECT id FROM units WHERE doc_id = ?)
|
||||
"{PROPOSAL_SELECT} WHERE p.unit_id IN (SELECT id FROM units WHERE doc_id = ? AND retired_at IS NULL)
|
||||
AND p.status IN ('open', 'in_force', 'draft') ORDER BY p.submitted_at, p.created_at, p.rowid"
|
||||
))
|
||||
.bind(doc_id)
|
||||
@@ -810,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),
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
pub mod access;
|
||||
pub mod adoption;
|
||||
pub mod article_amend;
|
||||
pub mod auth;
|
||||
pub mod ballots;
|
||||
pub mod clock;
|
||||
@@ -31,6 +32,7 @@ pub mod proposals;
|
||||
pub mod publish;
|
||||
pub mod quorum;
|
||||
pub mod quorum_amend;
|
||||
pub mod retire;
|
||||
pub mod seed;
|
||||
pub mod seed_amend;
|
||||
pub mod seed_proposal_amend;
|
||||
|
||||
@@ -37,7 +37,9 @@ pub async fn sections_now(conn: &mut SqliteConnection, doc_id: &str) -> sqlx::Re
|
||||
position: f64,
|
||||
visible: bool,
|
||||
}
|
||||
// Seen: a text in force, or an open version (`DocState::members` says the same).
|
||||
// Seen: a text in force, or an open version (`DocState::members` says the same). It reads
|
||||
// only the columns of before 0012: a document born now, or given its modules by an
|
||||
// amendment, holds no retired unit (ADR 0014); `article_amend` keeps the live ones.
|
||||
let rows = sqlx::query_as::<_, Row>(
|
||||
"SELECT u.id, u.section, u.position,
|
||||
(u.in_force_id IS NOT NULL OR EXISTS (SELECT 1 FROM proposals p
|
||||
|
||||
@@ -137,7 +137,8 @@ pub async fn decisions(
|
||||
/// order of each section (ADR 0009): the version in force (adopted by a vote,
|
||||
/// or a seed a minima), the open versions, and whether `me` holds a valid vote
|
||||
/// on one of them. A change of inertia is not a version of the text; the order
|
||||
/// of a section of fewer than two articles has nothing to decide; a version held
|
||||
/// of a section of fewer than two articles has nothing to decide; a unit out of the
|
||||
/// document (ADR 0014) is no module to vote; a version held
|
||||
/// by an objection is not counted as one to vote while it is held: it takes no
|
||||
/// new *for*, only against or a withdrawal (ADR 0010).
|
||||
async fn modules(state: &AppState, doc_id: &str, me: &str) -> AppResult<Vec<Module>> {
|
||||
@@ -169,7 +170,8 @@ async fn modules(state: &AppState, doc_id: &str, me: &str) -> AppResult<Vec<Modu
|
||||
WHERE b.requested_by = ?1 AND b.status = 'pending' AND b.mode = 'chain'
|
||||
AND l.proposal_id = p.id AND l.revision = p.revision AND l.choice <> 'none')) AS voted
|
||||
FROM units u JOIN proposals p ON p.unit_id = u.id
|
||||
WHERE u.doc_id = ?2 AND p.kind <> 'inertia' AND (p.id = u.in_force_id OR p.status = 'open')
|
||||
WHERE u.doc_id = ?2 AND u.retired_at IS NULL AND p.kind <> 'inertia'
|
||||
AND (p.id = u.in_force_id OR p.status = 'open')
|
||||
ORDER BY u.position, u.id",
|
||||
)
|
||||
.bind(me)
|
||||
|
||||
@@ -60,6 +60,11 @@ pub(crate) async fn guarded(
|
||||
if p.status == "draft" && p.author_id.as_deref() != Some(user.id.as_str()) {
|
||||
return Err(AppError::NotFound);
|
||||
}
|
||||
// A version of a unit out of the document (ADR 0014) is no longer in it: nothing is
|
||||
// read, voted, corrected, translated or objected on it.
|
||||
if !st.units.iter().any(|u| u.id == p.unit_id) {
|
||||
return Err(AppError::NotFound);
|
||||
}
|
||||
Ok((p, st))
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +195,8 @@ pub async fn waits_for_a_text(state: &AppState, doc_id: &str) -> anyhow::Result<
|
||||
return Ok(false);
|
||||
}
|
||||
let in_force: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM units WHERE doc_id = ? AND kind = 'article' AND in_force_id IS NOT NULL",
|
||||
"SELECT COUNT(*) FROM units WHERE doc_id = ? AND kind = 'article' AND in_force_id IS NOT NULL
|
||||
AND retired_at IS NULL",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.fetch_one(&state.db)
|
||||
@@ -317,8 +318,10 @@ async fn last_change(state: &AppState, doc_id: &str) -> anyhow::Result<String> {
|
||||
let at: Option<String> = sqlx::query_scalar(
|
||||
"SELECT MAX(d) FROM (
|
||||
SELECT MAX(at) AS d FROM adoptions WHERE doc_id = ?1
|
||||
UNION ALL SELECT MAX(p.updated_at) FROM proposals p JOIN units u ON u.in_force_id = p.id WHERE u.doc_id = ?1
|
||||
UNION ALL SELECT MAX(t.updated_at) FROM translations t JOIN units u ON u.in_force_id = t.proposal_id WHERE u.doc_id = ?1
|
||||
UNION ALL SELECT MAX(p.updated_at) FROM proposals p JOIN units u ON u.in_force_id = p.id
|
||||
WHERE u.doc_id = ?1 AND u.retired_at IS NULL
|
||||
UNION ALL SELECT MAX(t.updated_at) FROM translations t JOIN units u ON u.in_force_id = t.proposal_id
|
||||
WHERE u.doc_id = ?1 AND u.retired_at IS NULL
|
||||
UNION ALL SELECT created_at FROM documents WHERE id = ?1)",
|
||||
)
|
||||
.bind(doc_id)
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
//! Retirements — the units a seed no longer holds, taken out of an existing document
|
||||
//! without erasing anything (ADR 0014).
|
||||
//!
|
||||
//! Yvv, 2026-11-03 (lot T4): « Attention les 3 derniers modules ne sont pas de votes :
|
||||
//! 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. […] Vire les 3
|
||||
//! du bas de la liste. » The articles of the sections `formule` (F1, F1.1, F1.2, F1.3),
|
||||
//! `inertie` (N1, N1.1) and `ordonnancement` (O1), and the order of each section, say how
|
||||
//! the vote works: they are no texts to vote. A new base is born without them (the seed no
|
||||
//! longer holds them). An existing base keeps every row of them — units, versions,
|
||||
//! revisions, translations, the publication queue — and marks the units retired
|
||||
//! (`units.retired_at`, `retired_reason`): nothing else is written on them.
|
||||
//!
|
||||
//! **Out of the document.** A unit retired is not read with its document (`docs::load_state`
|
||||
//! reads the units with `retired_at IS NULL`, and their versions only), so it is neither
|
||||
//! listed, nor voted on (a vote, a grouped vote, a ballot), nor proposed, corrected,
|
||||
//! translated or objected on (`proposals::guarded`: 404), nor given a change of inertia, a
|
||||
//! quorum or an order (the unit is not found: 404), nor counted (`docs::group`,
|
||||
//! `overview::modules`), nor published (`publish`: `document.json` and the Markdown read the
|
||||
//! document's state; `waits_for_a_text` counts the articles in it). The order of a section
|
||||
//! reads its articles in it (`DocState::members`). An older image
|
||||
//! (rollback) ignores the column and shows them again, exactly as they were.
|
||||
//!
|
||||
//! **The guard**, decided once per document, all of them or none: they leave only if no
|
||||
//! person left a trace on any of them, in any of its versions, whatever became of it —
|
||||
//! a vote (simulated included), a line of a ballot (whatever its fate), an adoption, a
|
||||
//! version written by a member (a text, a removal, a change of inertia, an order), a
|
||||
//! revision written by a person (a typo fix, a substantive change), a typo fix waiting or
|
||||
//! refused, a translation written by a member, an objection naming it, a quorum set. Their
|
||||
//! texts may be at any revision the seed amendments wrote (F1.2 and N1.1 at 4 after M6c, 5
|
||||
//! after M8): an amendment is no trace of a person. Refused, nothing moves; the reason and
|
||||
//! the unit it was found on are kept (`retirement_amendments`) and in the logs. Applied
|
||||
//! where the document publishes, the queue gets a row, which waits for the first text
|
||||
//! (ADR 0011); the rows queued before for those texts stay, and go out with it.
|
||||
|
||||
use crate::AppState;
|
||||
use crate::seed::SeedDoc;
|
||||
use crate::util;
|
||||
use sqlx::SqliteConnection;
|
||||
|
||||
/// The retirement of lot T4: the information on how the vote works (ADR 0014).
|
||||
pub const RETIREMENT: &str = "2026-11-03-fonctionnement-hors-vote";
|
||||
|
||||
/// Units to take out of every document instantiated from a seed, by section.
|
||||
pub struct Retirement {
|
||||
/// Stable identifier: each document's decision is kept under it.
|
||||
pub id: &'static str,
|
||||
/// Slug of the seed document.
|
||||
pub doc: &'static str,
|
||||
/// The sections that leave: their articles, and the order of each.
|
||||
pub sections: &'static [&'static str],
|
||||
/// Reason given to the publication queue, and so to the commit of the texts.
|
||||
pub reason: &'static str,
|
||||
}
|
||||
|
||||
pub const AMENDMENTS: &[Retirement] = &[Retirement {
|
||||
id: RETIREMENT,
|
||||
doc: "acte-engagement-certification",
|
||||
sections: &["formule", "inertie", "ordonnancement"],
|
||||
reason: "informations de fonctionnement sorties du document, qui ne se votent pas : formule de vote (F1, F1.1, F1.2, F1.3), inertie (N1, N1.1), ordonnancement (O1), et l'ordre de chaque section (ADR 0014)",
|
||||
}];
|
||||
|
||||
/// What the database says of one unit that would leave, all its versions together.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct UnitFacts {
|
||||
pub code: String,
|
||||
/// Votes that count, of the server, proven or simulated.
|
||||
pub votes: i64,
|
||||
/// Lines of ballots, whatever their fate.
|
||||
pub ballots: i64,
|
||||
/// Adoptions, versions voted into force or confirmed, changes of inertia by a vote.
|
||||
pub adopted: i64,
|
||||
/// Versions written by a member: a text, a removal, a change of inertia, an order.
|
||||
pub member_versions: i64,
|
||||
/// Revisions written by a person, or of a kind no seed amendment writes; typo fixes.
|
||||
pub corrected: i64,
|
||||
/// Translations written or corrected by a member.
|
||||
pub member_translations: i64,
|
||||
/// Objections naming one of its versions, standing or lifted.
|
||||
pub signals: i64,
|
||||
/// Its own quorum set, or a change of it journaled.
|
||||
pub quorum_set: i64,
|
||||
}
|
||||
|
||||
impl UnitFacts {
|
||||
/// The first trace of a person found on it, if any.
|
||||
pub fn trace(&self) -> Option<&'static str> {
|
||||
[
|
||||
(self.votes, "votes"),
|
||||
(self.ballots, "ballots"),
|
||||
(self.adopted, "adopted"),
|
||||
(self.member_versions, "member_proposal"),
|
||||
(self.corrected, "corrected"),
|
||||
(self.member_translations, "translation_edited"),
|
||||
(self.signals, "signalled"),
|
||||
(self.quorum_set, "quorum_set"),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|(n, _)| *n > 0)
|
||||
.map(|(_, why)| why)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Verdict {
|
||||
Apply,
|
||||
/// Born without them, or already out: nothing to do.
|
||||
Present,
|
||||
/// A trace of a person, and the unit it was found on.
|
||||
Refuse(&'static str, String),
|
||||
}
|
||||
|
||||
/// The rule, apart from the database: all of them leave, or none.
|
||||
pub fn verdict(units: &[UnitFacts]) -> Verdict {
|
||||
if units.is_empty() {
|
||||
return Verdict::Present;
|
||||
}
|
||||
for u in units {
|
||||
if let Some(why) = u.trace() {
|
||||
return Verdict::Refuse(why, u.code.clone());
|
||||
}
|
||||
}
|
||||
Verdict::Apply
|
||||
}
|
||||
|
||||
/// A decision taken at this start.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Outcome {
|
||||
pub amendment: &'static str,
|
||||
pub group: String,
|
||||
pub verdict: Verdict,
|
||||
pub retired: Vec<String>,
|
||||
}
|
||||
|
||||
/// Decides every retirement not yet decided, document by document.
|
||||
pub async fn apply(state: &AppState, seeds: &[SeedDoc], list: &[Retirement]) -> anyhow::Result<Vec<Outcome>> {
|
||||
let mut out = Vec::new();
|
||||
for a in list {
|
||||
if !seed_says(seeds, a) {
|
||||
continue;
|
||||
}
|
||||
let docs: Vec<(String, String, Option<String>)> =
|
||||
sqlx::query_as("SELECT id, group_slug, publish_path FROM documents WHERE slug = ? ORDER BY group_slug")
|
||||
.bind(a.doc)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
for (doc_id, group, publish_path) in docs {
|
||||
let Some((v, retired)) = decide(state, a, &doc_id, publish_path.is_some()).await? else { continue };
|
||||
match &v {
|
||||
Verdict::Apply => {
|
||||
tracing::info!(amendment = a.id, %group, retired = ?retired, "units out of the document")
|
||||
}
|
||||
Verdict::Present => {
|
||||
tracing::info!(amendment = a.id, %group, "retirement: none of them in the document")
|
||||
}
|
||||
Verdict::Refuse(why, unit) => tracing::warn!(
|
||||
amendment = a.id, %group, reason = why, %unit,
|
||||
"retirement refused: a person left a trace, they stay in the document"
|
||||
),
|
||||
}
|
||||
if v == Verdict::Apply && publish_path.is_some() {
|
||||
state.publish_wake.notify_one();
|
||||
}
|
||||
out.push(Outcome { amendment: a.id, group, verdict: v, retired });
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The seed must no longer hold any unit of those sections: a seed that still gives them
|
||||
/// never takes them out.
|
||||
fn seed_says(seeds: &[SeedDoc], a: &Retirement) -> bool {
|
||||
let says = seeds
|
||||
.iter()
|
||||
.find(|s| s.slug == a.doc)
|
||||
.is_some_and(|s| s.units.iter().all(|u| u.section.as_deref().is_none_or(|x| !a.sections.contains(&x))));
|
||||
if !says {
|
||||
tracing::error!(
|
||||
amendment = a.id,
|
||||
doc = a.doc,
|
||||
"retirement: the seed still holds these sections, or no such document, nothing decided"
|
||||
);
|
||||
}
|
||||
says
|
||||
}
|
||||
|
||||
async fn unit_facts(conn: &mut SqliteConnection, unit_id: &str, code: &str) -> sqlx::Result<UnitFacts> {
|
||||
let n = |sql: &'static str| sqlx::query_scalar::<_, i64>(sql).bind(unit_id);
|
||||
Ok(UnitFacts {
|
||||
code: code.to_owned(),
|
||||
votes: n("SELECT COUNT(*) FROM votes WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)")
|
||||
.fetch_one(&mut *conn)
|
||||
.await?,
|
||||
ballots: n(
|
||||
"SELECT COUNT(*) FROM ballot_lines WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)",
|
||||
)
|
||||
.fetch_one(&mut *conn)
|
||||
.await?,
|
||||
adopted: n("SELECT (SELECT COUNT(*) FROM adoptions WHERE unit_id = ?1)
|
||||
+ (SELECT COUNT(*) FROM proposals WHERE unit_id = ?1
|
||||
AND (entered_at IS NOT NULL OR confirmed_at IS NOT NULL))
|
||||
+ (SELECT COUNT(*) FROM inertia_changes WHERE unit_id = ?1 AND cause = 'vote')")
|
||||
.fetch_one(&mut *conn)
|
||||
.await?,
|
||||
member_versions: n(
|
||||
"SELECT COUNT(*) FROM proposals WHERE unit_id = ?1 AND (origin <> 'seed' OR author_id IS NOT NULL)",
|
||||
)
|
||||
.fetch_one(&mut *conn)
|
||||
.await?,
|
||||
corrected: n(
|
||||
"SELECT (SELECT COUNT(*) FROM typos
|
||||
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1))
|
||||
+ (SELECT COUNT(*) FROM revisions
|
||||
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)
|
||||
AND (author_id IS NOT NULL OR kind NOT IN ('initial', 'amendment', 'seed_proposal')))",
|
||||
)
|
||||
.fetch_one(&mut *conn)
|
||||
.await?,
|
||||
member_translations: n("SELECT COUNT(*) FROM translations
|
||||
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)
|
||||
AND (origin <> 'seed' OR updated_by IS NOT NULL)")
|
||||
.fetch_one(&mut *conn)
|
||||
.await?,
|
||||
signals: n("SELECT COUNT(*) FROM signals
|
||||
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)
|
||||
OR target_proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)")
|
||||
.fetch_one(&mut *conn)
|
||||
.await?,
|
||||
quorum_set: n("SELECT (SELECT COUNT(*) FROM units WHERE id = ?1 AND quorum IS NOT NULL)
|
||||
+ (SELECT COUNT(*) FROM quorum_changes WHERE unit_id = ?1)")
|
||||
.fetch_one(&mut *conn)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
||||
/// One document, in one transaction: `None` when it was decided at an earlier start.
|
||||
async fn decide(
|
||||
state: &AppState,
|
||||
a: &Retirement,
|
||||
doc_id: &str,
|
||||
publishes: bool,
|
||||
) -> anyhow::Result<Option<(Verdict, Vec<String>)>> {
|
||||
let mut tx = state.db.begin().await?;
|
||||
let decided: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM retirement_amendments WHERE amendment_id = ? AND doc_id = ?")
|
||||
.bind(a.id)
|
||||
.bind(doc_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
if decided > 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
// The units of those sections still in the document: the articles in their order, then
|
||||
// the order of each section.
|
||||
let marks = vec!["?"; a.sections.len()].join(", ");
|
||||
let sql = format!(
|
||||
"SELECT id, code FROM units WHERE doc_id = ? AND retired_at IS NULL AND section IN ({marks})
|
||||
ORDER BY kind <> 'article', position, code"
|
||||
);
|
||||
let mut q = sqlx::query_as::<_, (String, String)>(&sql).bind(doc_id);
|
||||
for s in a.sections {
|
||||
q = q.bind(*s);
|
||||
}
|
||||
let units = q.fetch_all(&mut *tx).await?;
|
||||
let mut facts = Vec::with_capacity(units.len());
|
||||
for (id, code) in &units {
|
||||
facts.push(unit_facts(&mut tx, id, code).await?);
|
||||
}
|
||||
let mut v = verdict(&facts);
|
||||
let now = util::now();
|
||||
let mut retired = Vec::new();
|
||||
if v == Verdict::Apply {
|
||||
for (id, code) in &units {
|
||||
// Only the two columns of the retirement; nothing else of the unit, nothing of its
|
||||
// versions, revisions or translations.
|
||||
let done =
|
||||
sqlx::query("UPDATE units SET retired_at = ?, retired_reason = ? WHERE id = ? AND retired_at IS NULL")
|
||||
.bind(&now)
|
||||
.bind(a.id)
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if done.rows_affected() == 1 {
|
||||
retired.push(code.clone());
|
||||
}
|
||||
}
|
||||
if retired.len() != units.len() {
|
||||
// What was examined changed under us: nothing is written.
|
||||
tx.rollback().await?;
|
||||
tx = state.db.begin().await?;
|
||||
v = Verdict::Refuse("changed", String::new());
|
||||
retired.clear();
|
||||
} else if publishes {
|
||||
sqlx::query("INSERT INTO outbox (doc_id, reason, created_at, next_at) VALUES (?, ?, ?, ?)")
|
||||
.bind(doc_id)
|
||||
.bind(a.reason)
|
||||
.bind(&now)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
let (outcome, reason, unit) = match &v {
|
||||
Verdict::Apply => ("applied", None, None),
|
||||
Verdict::Present => ("present", None, None),
|
||||
Verdict::Refuse(why, unit) => ("refused", Some(*why), Some(unit.clone()).filter(|u| !u.is_empty())),
|
||||
};
|
||||
sqlx::query(
|
||||
"INSERT INTO retirement_amendments (amendment_id, doc_id, outcome, reason, unit_code, retired, at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(a.id)
|
||||
.bind(doc_id)
|
||||
.bind(outcome)
|
||||
.bind(reason)
|
||||
.bind(unit)
|
||||
.bind((v == Verdict::Apply).then(|| serde_json::to_string(&retired).unwrap_or_else(|_| "[]".into())))
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Some((v, retired)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn untouched(code: &str) -> UnitFacts {
|
||||
UnitFacts { code: code.into(), ..UnitFacts::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn untouched_they_all_leave() {
|
||||
assert_eq!(verdict(&[untouched("F1"), untouched("F1.2"), untouched("ordre.formule")]), Verdict::Apply);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn born_without_them_nothing_to_do() {
|
||||
assert_eq!(verdict(&[]), Verdict::Present);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_trace_of_a_person_keeps_them_all_and_says_where() {
|
||||
let cases = [
|
||||
(UnitFacts { votes: 1, ..untouched("N1") }, "votes"),
|
||||
(UnitFacts { ballots: 1, ..untouched("N1") }, "ballots"),
|
||||
(UnitFacts { adopted: 1, ..untouched("N1") }, "adopted"),
|
||||
(UnitFacts { member_versions: 1, ..untouched("N1") }, "member_proposal"),
|
||||
(UnitFacts { corrected: 1, ..untouched("N1") }, "corrected"),
|
||||
(UnitFacts { member_translations: 1, ..untouched("N1") }, "translation_edited"),
|
||||
(UnitFacts { signals: 1, ..untouched("N1") }, "signalled"),
|
||||
(UnitFacts { quorum_set: 1, ..untouched("N1") }, "quorum_set"),
|
||||
];
|
||||
for (facts, why) in cases {
|
||||
let all = [untouched("F1"), facts.clone(), untouched("O1")];
|
||||
assert_eq!(verdict(&all), Verdict::Refuse(why, "N1".into()), "{facts:?}");
|
||||
}
|
||||
// The first trace, in the order of the units, then of the kinds.
|
||||
let two = [
|
||||
untouched("F1"),
|
||||
UnitFacts { signals: 1, votes: 2, ..untouched("F1.1") },
|
||||
UnitFacts { quorum_set: 1, ..untouched("N1") },
|
||||
];
|
||||
assert_eq!(verdict(&two), Verdict::Refuse("votes", "F1.1".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shipped_retirement_takes_out_the_three_sections_of_operation() {
|
||||
assert_eq!(AMENDMENTS[0].id, RETIREMENT);
|
||||
assert_eq!(AMENDMENTS[0].sections, ["formule", "inertie", "ordonnancement"]);
|
||||
}
|
||||
}
|
||||
+102
-75
@@ -12,18 +12,26 @@
|
||||
//! document holds; a starting text in force the seed now puts forward as a seed
|
||||
//! proposal (ADR 0011), a seed proposal amendment (`seed_proposal_amend`), on an
|
||||
//! article nobody has touched; the quorums by level the seed now gives a document
|
||||
//! (ADR 0013), a quorum amendment (`quorum_amend`), where the document holds none.
|
||||
//! (ADR 0013), a quorum amendment (`quorum_amend`), where the document holds none; an
|
||||
//! article the seed now holds, an article amendment (`article_amend`), placed where the
|
||||
//! seed puts it while the order of its section was never voted on; the units the seed no
|
||||
//! longer holds — the information on how the vote works (ADR 0014) —, a retirement
|
||||
//! (`retire`), where no person left a trace on them: they stay in the database, out of
|
||||
//! the document.
|
||||
|
||||
use crate::AppState;
|
||||
use crate::article_amend;
|
||||
use crate::inertia_amend;
|
||||
use crate::module_amend;
|
||||
use crate::modules;
|
||||
use crate::quorum_amend;
|
||||
use crate::retire;
|
||||
use crate::seed_amend;
|
||||
use crate::seed_proposal_amend;
|
||||
use crate::util::{self, LangMap};
|
||||
use ld_engine::inertia::{InertiaLevel, quorum_bounds};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqliteConnection;
|
||||
use std::path::Path;
|
||||
|
||||
/// The level of inertia a seed proposal starts at (ADR 0011). Yvv, 2026-11-02: « on
|
||||
@@ -132,8 +140,9 @@ pub async fn ensure_groups(state: &AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// On start: the groups, the documents missing from them, then the seed,
|
||||
/// inertia and module amendments (each decided once per document).
|
||||
/// On start: the groups, the documents missing from them, then the seed, inertia,
|
||||
/// module, seed proposal, quorum, article amendments and the retirements (each decided
|
||||
/// once per document).
|
||||
pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
|
||||
let seeds = documents(state).await?;
|
||||
seed_amend::apply(state, &seeds, seed_amend::AMENDMENTS).await?;
|
||||
@@ -142,6 +151,8 @@ pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
|
||||
// After the text amendments: a starting text replaced first, then put forward.
|
||||
seed_proposal_amend::apply(state, &seeds, seed_proposal_amend::AMENDMENTS).await?;
|
||||
quorum_amend::apply(state, &seeds, quorum_amend::AMENDMENTS).await?;
|
||||
article_amend::apply(state, &seeds, article_amend::AMENDMENTS).await?;
|
||||
retire::apply(state, &seeds, retire::AMENDMENTS).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -225,7 +236,6 @@ fn check(seed: &SeedDoc) -> anyhow::Result<()> {
|
||||
|
||||
async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> anyhow::Result<()> {
|
||||
let src = seed.source_lang.as_str();
|
||||
let pick = |m: &LangMap| m.get(src).cloned().unwrap_or_default();
|
||||
let now = util::now();
|
||||
let doc_id = util::id();
|
||||
let mut tx = state.db.begin().await?;
|
||||
@@ -253,77 +263,7 @@ async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> a
|
||||
.await?;
|
||||
}
|
||||
for unit in &seed.units {
|
||||
let unit_id = util::id();
|
||||
sqlx::query(
|
||||
"INSERT INTO units (id, doc_id, code, section, position, inertia, in_force_id, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NULL, ?)",
|
||||
)
|
||||
.bind(&unit_id)
|
||||
.bind(&doc_id)
|
||||
.bind(&unit.code)
|
||||
.bind(&unit.section)
|
||||
.bind(unit.position)
|
||||
.bind(unit.level().as_db())
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
for v in &unit.versions {
|
||||
let pid = util::id();
|
||||
let (title, body) = (pick(&v.title), pick(&v.body));
|
||||
let status = if v.status == "in_force" { "in_force" } else { "open" };
|
||||
sqlx::query(
|
||||
"INSERT INTO proposals (id, unit_id, kind, title, body, status, origin, label, author_id, revision,
|
||||
created_at, updated_at, submitted_at)
|
||||
VALUES (?, ?, 'text', ?, ?, ?, 'seed', ?, NULL, 1, ?, ?, ?)",
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(&unit_id)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(status)
|
||||
.bind(&v.label)
|
||||
.bind(&now)
|
||||
.bind(&now)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO revisions (proposal_id, revision, kind, title, body, author_id, created_at) VALUES (?, 1, 'initial', ?, ?, NULL, ?)",
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
// The mark, written apart: only where the seed puts the version forward.
|
||||
if v.seed_proposal && status == "open" {
|
||||
sqlx::query("UPDATE proposals SET seed_featured = 1 WHERE id = ?").bind(&pid).execute(&mut *tx).await?;
|
||||
}
|
||||
if status == "in_force" {
|
||||
sqlx::query("UPDATE units SET in_force_id = ? WHERE id = ?")
|
||||
.bind(&pid)
|
||||
.bind(&unit_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
let hash = util::source_hash(&title, &body);
|
||||
for lang in util::LANGS.iter().filter(|l| **l != src) {
|
||||
let (Some(t), Some(b)) = (v.title.get(*lang), v.body.get(*lang)) else { continue };
|
||||
sqlx::query(
|
||||
"INSERT INTO translations (proposal_id, lang, title, body, source_hash, origin, updated_by, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'seed', NULL, ?)",
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(lang)
|
||||
.bind(t)
|
||||
.bind(b)
|
||||
.bind(&hash)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
write_unit(&mut tx, &doc_id, src, unit, unit.position, &now).await?;
|
||||
}
|
||||
// The modules, from the document just written: its title, the order of each section.
|
||||
if let Some(m) = &seed.modules {
|
||||
@@ -343,6 +283,93 @@ async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> a
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes one article of the seed into a document, at `position`: its unit (at its level),
|
||||
/// its versions — in force, open, put forward —, their first revision and their
|
||||
/// translations. A new base (`instantiate`), or an article a seed adds later
|
||||
/// (`article_amend`). Returns the unit's id.
|
||||
pub(crate) async fn write_unit(
|
||||
conn: &mut SqliteConnection,
|
||||
doc_id: &str,
|
||||
src: &str,
|
||||
unit: &SeedUnit,
|
||||
position: f64,
|
||||
now: &str,
|
||||
) -> sqlx::Result<String> {
|
||||
let pick = |m: &LangMap| m.get(src).cloned().unwrap_or_default();
|
||||
let unit_id = util::id();
|
||||
sqlx::query(
|
||||
"INSERT INTO units (id, doc_id, code, section, position, inertia, in_force_id, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NULL, ?)",
|
||||
)
|
||||
.bind(&unit_id)
|
||||
.bind(doc_id)
|
||||
.bind(&unit.code)
|
||||
.bind(&unit.section)
|
||||
.bind(position)
|
||||
.bind(unit.level().as_db())
|
||||
.bind(now)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
for v in &unit.versions {
|
||||
let pid = util::id();
|
||||
let (title, body) = (pick(&v.title), pick(&v.body));
|
||||
let status = if v.status == "in_force" { "in_force" } else { "open" };
|
||||
sqlx::query(
|
||||
"INSERT INTO proposals (id, unit_id, kind, title, body, status, origin, label, author_id, revision,
|
||||
created_at, updated_at, submitted_at)
|
||||
VALUES (?, ?, 'text', ?, ?, ?, 'seed', ?, NULL, 1, ?, ?, ?)",
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(&unit_id)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(status)
|
||||
.bind(&v.label)
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO revisions (proposal_id, revision, kind, title, body, author_id, created_at) VALUES (?, 1, 'initial', ?, ?, NULL, ?)",
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(now)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
// The mark, written apart: only where the seed puts the version forward.
|
||||
if v.seed_proposal && status == "open" {
|
||||
sqlx::query("UPDATE proposals SET seed_featured = 1 WHERE id = ?").bind(&pid).execute(&mut *conn).await?;
|
||||
}
|
||||
if status == "in_force" {
|
||||
sqlx::query("UPDATE units SET in_force_id = ? WHERE id = ?")
|
||||
.bind(&pid)
|
||||
.bind(&unit_id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
let hash = util::source_hash(&title, &body);
|
||||
for lang in util::LANGS.iter().filter(|l| **l != src) {
|
||||
let (Some(t), Some(b)) = (v.title.get(*lang), v.body.get(*lang)) else { continue };
|
||||
sqlx::query(
|
||||
"INSERT INTO translations (proposal_id, lang, title, body, source_hash, origin, updated_by, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'seed', NULL, ?)",
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(lang)
|
||||
.bind(t)
|
||||
.bind(b)
|
||||
.bind(&hash)
|
||||
.bind(now)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(unit_id)
|
||||
}
|
||||
|
||||
/// The quorums by level as `documents.quorums` holds them: `[12,12,50,50,50]`.
|
||||
pub fn quorums_json(quorums: &[u32; 5]) -> String {
|
||||
serde_json::to_string(quorums).unwrap_or_else(|_| "null".into())
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
//! earlier one replaces the text that one wrote. The same day (lot T2, after the
|
||||
//! review of M6b), three phrases of those texts are written true by amendments that
|
||||
//! follow lot T's: F1.1 is then at its revision 2, F1.2 and N1.1 at their revision 3.
|
||||
//! Still the same day (lot T3, Yvv: no dated figure in a voted text), F1.2 and N1.1 are
|
||||
//! written without a figure that depends on the web of trust, and true for every quorum,
|
||||
//! by amendments that follow lot T2's: they are then at their revision 4; F1.1 is left.
|
||||
//! So an amendment admits, besides the seed's own revision 1, the revision written
|
||||
//! by the amendments it follows (`earlier`: the one it follows, the one that one
|
||||
//! follows…), applied in that order, the last being the one it follows, and nothing
|
||||
@@ -22,6 +25,15 @@
|
||||
//! The earlier one is then `superseded_by` it: the seed no longer gives its text,
|
||||
//! so it decides nothing more, without an error (the decisions already taken stay).
|
||||
//!
|
||||
//! Lot T4 (Yvv, 2026-11-03, « Modif des textes Acte engagement »): the three seed proposals
|
||||
//! I1, E1 and E2 take new texts. Such an amendment examines the seed proposal only
|
||||
//! (`Version::SeedProposal`): open, put forward, never in force. Its history holds, besides
|
||||
//! its text, the line that made it a seed proposal (kind `seed_proposal`, ADR 0011), which
|
||||
//! changes no text: it is admitted when a seed proposal amendment applied on that very
|
||||
//! version wrote it. The same day, the articles of operation leave the seed (F1, F1.1, F1.2,
|
||||
//! F1.3, N1, N1.1, O1; ADR 0014): the last amendments of their texts are superseded by their
|
||||
//! retirement (`retire`), and decide nothing more either.
|
||||
//!
|
||||
//! A seeded document is never instantiated again: once in the database, it is
|
||||
//! runtime data. When the seed's text of an article changes (Yvv, 2026-10-30:
|
||||
//! the Préambule becomes « Raison d'être »), new bases get it from the seed;
|
||||
@@ -61,6 +73,8 @@ pub enum Version {
|
||||
InForce,
|
||||
/// An open proposal of the seed, at zero votes, by its label.
|
||||
Open(&'static str),
|
||||
/// Its seed proposal (ADR 0011), and nothing else: never a text in force.
|
||||
SeedProposal,
|
||||
}
|
||||
|
||||
/// One starting text to replace, in every document instantiated from a seed.
|
||||
@@ -83,8 +97,9 @@ pub struct Amendment {
|
||||
/// The earlier amendment of the same text whose text this one replaces, if any:
|
||||
/// its revision 2 is then admitted (see the module).
|
||||
pub follows: Option<&'static str>,
|
||||
/// The later amendment whose text the seed gives instead, if any: this one then
|
||||
/// decides nothing more, without an error.
|
||||
/// The later amendment whose text the seed gives instead, if any — or the retirement
|
||||
/// that takes the article out of the seed (`retire`): this one then decides nothing
|
||||
/// more, without an error.
|
||||
pub superseded_by: Option<&'static str>,
|
||||
}
|
||||
|
||||
@@ -98,7 +113,7 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
to: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
|
||||
reason: "texte de départ remplacé : I1",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
superseded_by: Some("2026-11-03-i1-amorce"),
|
||||
},
|
||||
// The rule articles, open proposals of the seed, after the rules applied (Yvv, 2026-11-02).
|
||||
Amendment {
|
||||
@@ -121,7 +136,7 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
to: "c1facdfd0c0f3aaa94b74192e146557558fe88f66304dc6d21279714d7fb727e",
|
||||
reason: "proposition de départ réécrite : N1",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
superseded_by: Some(crate::retire::RETIREMENT),
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-02-n1-1-regles",
|
||||
@@ -143,7 +158,7 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
to: "4173738055af10fceb74bb6e954bdfefd7ab364480d49afbc92ea5f68825c537",
|
||||
reason: "proposition de départ réécrite : O1",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
superseded_by: Some(crate::retire::RETIREMENT),
|
||||
},
|
||||
// The articles that still described the floor C, after the quorum by level (ADR 0013;
|
||||
// Yvv, 2026-11-02 and 03; lot T). F1.1 was never amended; F1.2 and N1.1 replace the
|
||||
@@ -194,7 +209,7 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
to: "8375b78af825e5ce5f978442519c078010ac9ad87f5cbc9358b407aa3668c5e5",
|
||||
reason: "proposition de départ réécrite : F1.1, la part du cran au-delà du quorum",
|
||||
follows: Some("2026-11-03-f1-1-quorum"),
|
||||
superseded_by: None,
|
||||
superseded_by: Some(crate::retire::RETIREMENT),
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-03-f1-2-phrases",
|
||||
@@ -205,7 +220,7 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
to: "96caf353b33805f2894593d72c5b70fce2e751039cd7f4651d8842c986729a14",
|
||||
reason: "proposition de départ réécrite : F1.2, le quorum fixé tant qu'aucun vote n'a laissé de trace",
|
||||
follows: Some("2026-11-03-f1-2-quorum"),
|
||||
superseded_by: None,
|
||||
superseded_by: Some("2026-11-03-f1-2-chiffres"),
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-03-n1-1-phrases",
|
||||
@@ -216,6 +231,79 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
to: "7fbedcb8750c208f8790c4f87dc21d8bd1096a27a1a0044af6b9b8f2057f2aa8",
|
||||
reason: "proposition de départ réécrite : N1.1, le quorum n'est pas un minimum de votants",
|
||||
follows: Some("2026-11-03-n1-1-quorum"),
|
||||
superseded_by: Some("2026-11-03-n1-1-chiffres"),
|
||||
},
|
||||
// No dated figure in a voted text (lot T3, Yvv, 2026-11-03: « pas de chiffre datés dans un
|
||||
// texte voté »), every sentence true for every W and every quorum: N1.1 loses its example
|
||||
// (7 224 members, 120 voters) and says that under the quorum it takes as many « pour » as if
|
||||
// the quorum had voted; N1.1 and F1.2 ask the share of a level « comme d'habitude » beyond the
|
||||
// quorum. Each replaces the text lot T2 wrote, at its revision 4. F1.1 is not touched. After
|
||||
// the review of 2ce2028 (lot T3b, before it went online, so the same amendment): N1.1 says G
|
||||
// gives the share of the level beyond the quorum, and « autant de « pour » » in French. After
|
||||
// the review of e7ab82f (lot T3c, still before it went online, still the same amendments): the
|
||||
// defined term « à la participation habituelle » where « comme d'habitude » read first as the
|
||||
// real median, false once it leaves its bounds (F1.2, N1.1); in N1.1, « le quorum de
|
||||
// l'article » where a bare « quorum » could be the level's, « la part exigée », « Tant qu'elle
|
||||
// repose sur moins de 5 propositions » (the year counts), « Elle est ramenée entre 1 % et 50 % ».
|
||||
Amendment {
|
||||
id: "2026-11-03-f1-2-chiffres",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "F1.2",
|
||||
version: Version::Open("1.0.0"),
|
||||
from: "96caf353b33805f2894593d72c5b70fce2e751039cd7f4651d8842c986729a14",
|
||||
to: "0c3b800b0c72b1edd4b884c03c6c5479c17609dcf82a95b593d408821e28bb4e",
|
||||
reason: "proposition de départ réécrite : F1.2, la part du cran par défaut au-delà du quorum, à la participation habituelle",
|
||||
follows: Some("2026-11-03-f1-2-phrases"),
|
||||
superseded_by: Some(crate::retire::RETIREMENT),
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-03-n1-1-chiffres",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "N1.1",
|
||||
version: Version::Open("1.0.0"),
|
||||
from: "7fbedcb8750c208f8790c4f87dc21d8bd1096a27a1a0044af6b9b8f2057f2aa8",
|
||||
to: "8107c5fc746121f18c8a440e86e23310c5a59ae8e92f5a07f6b098419980263c",
|
||||
reason: "proposition de départ réécrite : N1.1, sans chiffre daté ; sous le quorum de l'article, autant de « pour » que si ce quorum avait voté ; au-delà, la part du cran à la participation habituelle",
|
||||
follows: Some("2026-11-03-n1-1-phrases"),
|
||||
superseded_by: Some(crate::retire::RETIREMENT),
|
||||
},
|
||||
// The three seed proposals, after Yvv's texts of 03/11 (lot T4): « Derrière une clé il y a
|
||||
// un et un seul être humain vivant… » (I1, its second and third paragraphs rewritten too);
|
||||
// « …qu'un seul et unique compte certifiant » (E1); « …qui s'engagent elles-mêmes, à
|
||||
// scrupuleusement respecter ces trois principes fondamentaux » (E2). Each replaces the
|
||||
// text of the seed proposal only, never a text in force: I1 the Raison d'être of 30/10
|
||||
// (at its revision 2 in production), E1 and E2 their text of the seed (revision 1).
|
||||
Amendment {
|
||||
id: "2026-11-03-i1-amorce",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "I1",
|
||||
version: Version::SeedProposal,
|
||||
from: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
|
||||
to: "32eb36b4c678c97d4597f0e4cc494a92c9956792839d6eb398a81c1dcc46320f",
|
||||
reason: "Proposition d'amorce réécrite : I1, la Raison d'être (texte d'Yvv du 03/11)",
|
||||
follows: Some("2026-10-30-i1-raison-d-etre"),
|
||||
superseded_by: None,
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-03-e1-amorce",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "E1",
|
||||
version: Version::SeedProposal,
|
||||
from: "bdca2713fdb1186bc59ac130f13a205a35376986f14f7cfba7223b37ed9a41bd",
|
||||
to: "df31d0ec5f7f0f8f88d930347ce370634881ca62aed52beb3ffe780fe992b9c9",
|
||||
reason: "Proposition d'amorce réécrite : E1, l'unicité du compte certifiant (texte d'Yvv du 03/11)",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-03-e2-amorce",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "E2",
|
||||
version: Version::SeedProposal,
|
||||
from: "bde76a30edaf8b623f3de0ff7eb94ec57eb5134b3c8aeae1590798fb6b9956f0",
|
||||
to: "a7dd469004c0e495524a9c38305177c352e36cd240f59a3b43e28d9222a6ae24",
|
||||
reason: "Proposition d'amorce réécrite : E2, la réciprocité des trois principes (texte d'Yvv du 03/11)",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
},
|
||||
];
|
||||
@@ -248,6 +336,9 @@ pub struct Facts {
|
||||
pub signals: i64,
|
||||
/// Typo fixes someone else wrote on it, whatever became of them (2026-10-31).
|
||||
pub typos: i64,
|
||||
/// The seed proposal amendments applied on it (ADR 0011): each wrote a line of its
|
||||
/// history (kind `seed_proposal`) that changes no text.
|
||||
pub put_forward: i64,
|
||||
pub fingerprint: String,
|
||||
}
|
||||
|
||||
@@ -281,11 +372,17 @@ pub fn verdict(facts: Option<&Facts>, a: &Amendment, earlier: &[&str]) -> Verdic
|
||||
return Verdict::Refuse(match a.version {
|
||||
Version::InForce => "no_text_in_force",
|
||||
Version::Open(_) => "no_open_version",
|
||||
Version::SeedProposal => "no_seed_proposal",
|
||||
});
|
||||
};
|
||||
if f.fingerprint == a.to {
|
||||
return Verdict::Present;
|
||||
}
|
||||
// A line that made the version a seed proposal changes no text: set aside, as long as a
|
||||
// seed proposal amendment applied on it wrote each one (ADR 0011).
|
||||
let status_lines = f.revisions.iter().filter(|k| *k == "seed_proposal").count();
|
||||
let texts: Vec<&str> = f.revisions.iter().map(String::as_str).filter(|k| *k != "seed_proposal").collect();
|
||||
let status_written = i64::try_from(status_lines).unwrap_or(i64::MAX) <= f.put_forward;
|
||||
// The text the amendments it follows wrote: by them alone, in their order, the last
|
||||
// being the one it follows (a base born later holds the last ones only), one revision
|
||||
// each over the seed's.
|
||||
@@ -308,7 +405,7 @@ pub fn verdict(facts: Option<&Facts>, a: &Amendment, earlier: &[&str]) -> Verdic
|
||||
"votes"
|
||||
} else if f.ballots > 0 {
|
||||
"ballots"
|
||||
} else if f.revisions != history || f.typos > 0 {
|
||||
} else if texts != history || !status_written || f.typos > 0 {
|
||||
"corrected"
|
||||
} else if f.member_translations > 0 {
|
||||
"translation_edited"
|
||||
@@ -374,6 +471,7 @@ fn target_of<'a>(seeds: &'a [SeedDoc], a: &Amendment) -> Option<(&'a SeedDoc, &'
|
||||
// The starting text: in force, or since 2026-11-02 put forward (ADR 0011).
|
||||
Version::InForce => v.status == "in_force" || v.seed_proposal,
|
||||
Version::Open(label) => v.status == "open" && v.label.as_deref() == Some(label),
|
||||
Version::SeedProposal => v.seed_proposal,
|
||||
})
|
||||
});
|
||||
let src = seed.source_lang.as_str();
|
||||
@@ -460,6 +558,12 @@ pub(crate) async fn facts(
|
||||
.bind(proposal_id)
|
||||
.fetch_all(&mut *conn)
|
||||
.await?;
|
||||
let put_forward: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM seed_proposal_amendments WHERE proposal_id = ? AND outcome = 'applied'",
|
||||
)
|
||||
.bind(proposal_id)
|
||||
.fetch_one(&mut *conn)
|
||||
.await?;
|
||||
Ok(Some(Facts {
|
||||
origin: p.origin,
|
||||
entered: p.entered_at.is_some(),
|
||||
@@ -472,6 +576,7 @@ pub(crate) async fn facts(
|
||||
member_translations,
|
||||
signals,
|
||||
typos,
|
||||
put_forward,
|
||||
fingerprint: fingerprint(&p.title, &p.body),
|
||||
}))
|
||||
}
|
||||
@@ -519,6 +624,8 @@ async fn decide(
|
||||
None => open(None).fetch_optional(&mut *tx).await?.map(|id| (id, "open")),
|
||||
},
|
||||
Version::Open(label) => open(Some(label)).fetch_optional(&mut *tx).await?.map(|id| (id, "open")),
|
||||
// Its seed proposal, open and put forward; a text in force is never examined.
|
||||
Version::SeedProposal => open(None).fetch_optional(&mut *tx).await?.map(|id| (id, "open")),
|
||||
};
|
||||
let target_id = examined.as_ref().map(|(id, _)| id.clone());
|
||||
let found = match &examined {
|
||||
@@ -782,6 +889,112 @@ mod tests {
|
||||
assert_eq!(verdict(Some(&written_by(&["m5", "t"])), &second, &["m5"]), Verdict::Refuse("revised"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fourth_amendment_admits_the_texts_the_three_before_wrote_and_nothing_else() {
|
||||
// Lot T3 (2026-11-03): F1.2 and N1.1 were amended by M5, M6b and M6c (revision 4 in
|
||||
// production); the texts without dated figures replace the text of M6c, after it only.
|
||||
let fourth = Amendment { version: Version::Open("1.0.0"), follows: Some("t2"), ..A };
|
||||
let before: &[&str] = &["m5", "t", "t2"];
|
||||
// Production: M5, M6b, M6c, each its revision.
|
||||
assert_eq!(verdict(Some(&written_by(&["m5", "t", "t2"])), &fourth, before), Verdict::Apply);
|
||||
// A base born with the text of M5 (02/11): M6b and M6c wrote its revisions 2 and 3.
|
||||
assert_eq!(verdict(Some(&written_by(&["t", "t2"])), &fourth, before), Verdict::Apply);
|
||||
// A base born with the text of M6b, then of M6c: its revision 2, its revision 1.
|
||||
assert_eq!(verdict(Some(&written_by(&["t2"])), &fourth, before), Verdict::Apply);
|
||||
assert_eq!(verdict(Some(&intact()), &fourth, before), Verdict::Apply);
|
||||
// The last revision is not M6c's, or not by them alone, or out of order: refused.
|
||||
for applied in [
|
||||
&["m5", "t"][..],
|
||||
&["t"],
|
||||
&["m5"],
|
||||
&["t2", "t"],
|
||||
&["m5", "t2", "t"],
|
||||
&["other", "t", "t2"],
|
||||
&["m5", "t", "t2", "t2"],
|
||||
] {
|
||||
assert_eq!(verdict(Some(&written_by(applied)), &fourth, before), Verdict::Refuse("revised"), "{applied:?}");
|
||||
}
|
||||
// A typo after M6c: a revision no amendment wrote.
|
||||
let typo = Facts {
|
||||
revision: 5,
|
||||
revisions: vec![
|
||||
"initial".into(),
|
||||
"amendment".into(),
|
||||
"amendment".into(),
|
||||
"amendment".into(),
|
||||
"typo".into(),
|
||||
],
|
||||
..written_by(&["m5", "t", "t2"])
|
||||
};
|
||||
assert_eq!(verdict(Some(&typo), &fourth, before), Verdict::Refuse("revised"));
|
||||
// Any trace of a person keeps the text, with its reason.
|
||||
for (f, why) in [
|
||||
(Facts { votes: 1, ..written_by(&["m5", "t", "t2"]) }, "votes"),
|
||||
(Facts { ballots: 1, ..written_by(&["m5", "t", "t2"]) }, "ballots"),
|
||||
(Facts { typos: 1, ..written_by(&["m5", "t", "t2"]) }, "corrected"),
|
||||
(Facts { member_translations: 1, ..written_by(&["m5", "t", "t2"]) }, "translation_edited"),
|
||||
(Facts { signals: 1, ..written_by(&["m5", "t", "t2"]) }, "signalled"),
|
||||
(Facts { fingerprint: "other".into(), ..written_by(&["m5", "t", "t2"]) }, "text_differs"),
|
||||
(Facts { confirmed: true, ..written_by(&["m5", "t", "t2"]) }, "confirmed"),
|
||||
] {
|
||||
assert_eq!(verdict(Some(&f), &fourth, before), Verdict::Refuse(why), "{f:?}");
|
||||
}
|
||||
// Lot T2's own amendment never admits a revision 4.
|
||||
let third = Amendment { version: Version::Open("1.0.0"), follows: Some("t"), ..A };
|
||||
assert_eq!(verdict(Some(&written_by(&["m5", "t", "t2"])), &third, &["m5", "t"]), Verdict::Refuse("revised"));
|
||||
}
|
||||
|
||||
/// A version the seed proposal amendment put forward (ADR 0011), as production holds I1,
|
||||
/// E1 and E2 since M6: over the texts `applied` wrote, one line `seed_proposal` that
|
||||
/// changes no text, at the same revision.
|
||||
fn put_forward(applied: &[&str]) -> Facts {
|
||||
let mut f = written_by(applied);
|
||||
f.revisions.push("seed_proposal".into());
|
||||
f.put_forward = 1;
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_seed_proposal_is_rewritten_over_the_line_that_put_it_forward_and_nothing_else() {
|
||||
// Lot T4 (2026-11-03): I1 follows the amendment of 30/10, E1 and E2 follow none.
|
||||
let i1 = Amendment { version: Version::SeedProposal, follows: Some("r"), ..A };
|
||||
let e1 = Amendment { version: Version::SeedProposal, ..A };
|
||||
// Production since M6: the text of 30/10 (I1) or the seed's (E1), then put forward.
|
||||
assert_eq!(verdict(Some(&put_forward(&["r"])), &i1, &["r"]), Verdict::Apply);
|
||||
assert_eq!(verdict(Some(&put_forward(&[])), &e1, &[]), Verdict::Apply);
|
||||
// A base born with the seed proposals: nothing put them forward, revision 1.
|
||||
assert_eq!(verdict(Some(&intact()), &i1, &["r"]), Verdict::Apply);
|
||||
assert_eq!(verdict(Some(&intact()), &e1, &[]), Verdict::Apply);
|
||||
// The new text already there.
|
||||
let present = Facts { fingerprint: "new".into(), ..put_forward(&[]) };
|
||||
assert_eq!(verdict(Some(&present), &e1, &[]), Verdict::Present);
|
||||
// No seed proposal: a base older than M6, or a version voted into force or withdrawn.
|
||||
assert_eq!(verdict(None, &e1, &[]), Verdict::Refuse("no_seed_proposal"));
|
||||
// A line `seed_proposal` that no amendment wrote, or one more than they wrote.
|
||||
let unwritten = Facts { put_forward: 0, ..put_forward(&[]) };
|
||||
assert_eq!(verdict(Some(&unwritten), &e1, &[]), Verdict::Refuse("corrected"));
|
||||
let mut twice = put_forward(&[]);
|
||||
twice.revisions.push("seed_proposal".into());
|
||||
assert_eq!(verdict(Some(&twice), &e1, &[]), Verdict::Refuse("corrected"));
|
||||
// A typo once put forward: a revision no amendment wrote.
|
||||
let mut typo = put_forward(&["r"]);
|
||||
typo.revision = 3;
|
||||
typo.revisions.push("typo".into());
|
||||
assert_eq!(verdict(Some(&typo), &i1, &["r"]), Verdict::Refuse("revised"));
|
||||
// Any trace of a person keeps the text, with its reason.
|
||||
for (f, why) in [
|
||||
(Facts { votes: 1, ..put_forward(&[]) }, "votes"),
|
||||
(Facts { ballots: 1, ..put_forward(&[]) }, "ballots"),
|
||||
(Facts { typos: 1, ..put_forward(&[]) }, "corrected"),
|
||||
(Facts { member_translations: 1, ..put_forward(&[]) }, "translation_edited"),
|
||||
(Facts { signals: 1, ..put_forward(&[]) }, "signalled"),
|
||||
(Facts { fingerprint: "other".into(), ..put_forward(&[]) }, "text_differs"),
|
||||
(Facts { origin: "member".into(), ..put_forward(&[]) }, "not_a_seed"),
|
||||
] {
|
||||
assert_eq!(verdict(Some(&f), &e1, &[]), Verdict::Refuse(why), "{f:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_amendments_followed_are_read_through_the_list_oldest_first() {
|
||||
let by_id = |id: &str| AMENDMENTS.iter().find(|a| a.id == id).expect("amendment");
|
||||
@@ -794,7 +1007,19 @@ mod tests {
|
||||
["2026-11-02-n1-1-regles", "2026-11-03-n1-1-quorum"]
|
||||
);
|
||||
assert_eq!(earlier(AMENDMENTS, by_id("2026-11-03-f1-1-phrases")), ["2026-11-03-f1-1-quorum"]);
|
||||
assert_eq!(
|
||||
earlier(AMENDMENTS, by_id("2026-11-03-f1-2-chiffres")),
|
||||
["2026-11-02-f1-2-regles", "2026-11-03-f1-2-quorum", "2026-11-03-f1-2-phrases"]
|
||||
);
|
||||
assert_eq!(
|
||||
earlier(AMENDMENTS, by_id("2026-11-03-n1-1-chiffres")),
|
||||
["2026-11-02-n1-1-regles", "2026-11-03-n1-1-quorum", "2026-11-03-n1-1-phrases"]
|
||||
);
|
||||
assert!(earlier(AMENDMENTS, by_id("2026-10-30-i1-raison-d-etre")).is_empty());
|
||||
// Lot T4: I1 follows the text of 30/10; E1 and E2, the seed's.
|
||||
assert_eq!(earlier(AMENDMENTS, by_id("2026-11-03-i1-amorce")), ["2026-10-30-i1-raison-d-etre"]);
|
||||
assert!(earlier(AMENDMENTS, by_id("2026-11-03-e1-amorce")).is_empty());
|
||||
assert!(earlier(AMENDMENTS, by_id("2026-11-03-e2-amorce")).is_empty());
|
||||
// Each one followed wrote the text the next replaces, and is superseded by it.
|
||||
for a in AMENDMENTS.iter().filter(|a| a.follows.is_some()) {
|
||||
let e = by_id(a.follows.expect("follows"));
|
||||
|
||||
@@ -59,6 +59,10 @@ pub struct SeedProposalAmendment {
|
||||
pub levels: &'static [u8],
|
||||
/// Reason given to the publication queue, and so to the texts repository.
|
||||
pub reason: &'static str,
|
||||
/// The seed amendment that gives the seed proposal another text since (lot T4): the seed
|
||||
/// no longer puts this one forward, and this amendment decides nothing more, without
|
||||
/// an error (the decisions already taken stay).
|
||||
pub superseded_by: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub const AMENDMENTS: &[SeedProposalAmendment] = &[
|
||||
@@ -69,6 +73,7 @@ pub const AMENDMENTS: &[SeedProposalAmendment] = &[
|
||||
text: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
|
||||
levels: &[3],
|
||||
reason: "Proposition seed : I1 sort de la vigueur",
|
||||
superseded_by: Some("2026-11-03-i1-amorce"),
|
||||
},
|
||||
SeedProposalAmendment {
|
||||
id: "2026-11-02-e1-proposition-seed",
|
||||
@@ -77,6 +82,7 @@ pub const AMENDMENTS: &[SeedProposalAmendment] = &[
|
||||
text: "bdca2713fdb1186bc59ac130f13a205a35376986f14f7cfba7223b37ed9a41bd",
|
||||
levels: &[3, 4],
|
||||
reason: "Proposition seed : E1 sort de la vigueur",
|
||||
superseded_by: Some("2026-11-03-e1-amorce"),
|
||||
},
|
||||
SeedProposalAmendment {
|
||||
id: "2026-11-02-e2-proposition-seed",
|
||||
@@ -85,6 +91,7 @@ pub const AMENDMENTS: &[SeedProposalAmendment] = &[
|
||||
text: "bde76a30edaf8b623f3de0ff7eb94ec57eb5134b3c8aeae1590798fb6b9956f0",
|
||||
levels: &[3, 4],
|
||||
reason: "Proposition seed : E2 sort de la vigueur",
|
||||
superseded_by: Some("2026-11-03-e2-amorce"),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -257,6 +264,15 @@ fn seed_says(seeds: &[SeedDoc], a: &SeedProposalAmendment) -> bool {
|
||||
});
|
||||
match unit {
|
||||
Some((u, print)) if print == a.text && u.inertia.is_none() => true,
|
||||
_ if a.superseded_by.is_some() => {
|
||||
tracing::debug!(
|
||||
amendment = a.id,
|
||||
unit = a.unit,
|
||||
superseded_by = a.superseded_by,
|
||||
"seed proposal amendment superseded: nothing decided"
|
||||
);
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
tracing::error!(
|
||||
amendment = a.id,
|
||||
@@ -459,6 +475,7 @@ mod tests {
|
||||
text: "seed",
|
||||
levels: &[3, 4],
|
||||
reason: "Proposition seed : E1 sort de la vigueur",
|
||||
superseded_by: None,
|
||||
};
|
||||
|
||||
fn intact() -> Facts {
|
||||
|
||||
@@ -12,7 +12,9 @@ use std::time::Duration;
|
||||
|
||||
/// W is read again this often.
|
||||
pub const REFRESH: Duration = Duration::from_secs(6 * 3600);
|
||||
const PLAUSIBLE: std::ops::RangeInclusive<u32> = 100..=10_000_000;
|
||||
/// The sizes of the web of trust taken as true; anything else read is ignored. The texts of
|
||||
/// the seed are written true for every W in it (lot T3).
|
||||
pub const PLAUSIBLE: std::ops::RangeInclusive<u32> = 100..=10_000_000;
|
||||
|
||||
fn twox_128(data: &[u8]) -> [u8; 16] {
|
||||
let mut out = [0u8; 16];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -442,7 +442,8 @@ async fn previews_and_assistant() {
|
||||
a.login("Yvv", false).await;
|
||||
let id = doc_id(&mut a, "monnaie-libre").await;
|
||||
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=all"), None).await;
|
||||
assert_eq!(v["units"].as_array().expect("u").len(), 33);
|
||||
// 27 articles since lot T4 (the articles of operation out of the document, ADR 0014; E12 in).
|
||||
assert_eq!(v["units"].as_array().expect("u").len(), 27);
|
||||
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=wellrated"), None).await;
|
||||
assert_eq!(v["units"].as_array().expect("u").len(), 0, "nothing in force, nothing well rated at zero votes");
|
||||
let (_, v) = a
|
||||
@@ -951,11 +952,21 @@ const PREAMBULE: [(&str, &str, &str); 3] = [
|
||||
|
||||
const RAISON_D_ETRE: &str = "*«\u{a0}Derrière une clé il n'y a qu'un seul être humain vivant\u{a0}; derrière un être humain, il n'y a qu'une seule clé certifiante.\u{a0}»*\n\nC'est le seul et unique propos de la certification";
|
||||
|
||||
/// The Raison d'être since lot T4 (Yvv, 03/11): its beginning.
|
||||
const RAISON_D_ETRE_T4: &str = "*«\u{a0}Derrière une clé il y a un et un seul être humain vivant\u{a0}; derrière un être humain, il y a une et une seule clé certifiante.\u{a0}»*\n\nC'est l'unique enjeu de la certification";
|
||||
|
||||
fn today_seed() -> Value {
|
||||
let path = Config::for_tests().seed_dir.join("acte-engagement-certification.json");
|
||||
serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json")
|
||||
}
|
||||
|
||||
/// The seed of M8 (de1b7eb): the last that gave I1 the Raison d'être of 30/10.
|
||||
fn m8_seed() -> Value {
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/seed-m8/acte-engagement-certification.json");
|
||||
serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json")
|
||||
}
|
||||
|
||||
/// The seed production runs, I1 in force: the amendment of 30/10 is replayed on it.
|
||||
fn production_seed() -> Value {
|
||||
let path = production_seed_dir().join("acte-engagement-certification.json");
|
||||
@@ -1026,15 +1037,24 @@ async fn a_new_base_starts_with_the_raison_d_etre() {
|
||||
for group in ["monnaie-libre", "essai"] {
|
||||
let i1 = i1_of(&mut c, group).await;
|
||||
assert_eq!(i1["title"], "Raison d'être");
|
||||
assert!(i1["body"].as_str().expect("body").starts_with(RAISON_D_ETRE), "{i1}");
|
||||
assert!(i1["body"].as_str().expect("body").starts_with(RAISON_D_ETRE_T4), "{i1}");
|
||||
assert_eq!((i1["revision"].as_i64(), i1["seedProposal"].as_bool()), (Some(1), Some(true)));
|
||||
for (lang, title) in [("en", "Purpose"), ("es", "Razón de ser")] {
|
||||
let tr = &i1["translations"][lang];
|
||||
assert_eq!((tr["title"].as_str(), tr["stale"].as_bool()), (Some(title), Some(false)), "{lang}");
|
||||
}
|
||||
}
|
||||
// Born with the text, put forward: nothing to replace, and it is said once.
|
||||
assert_eq!(outcomes(&decisions(&state).await), [("essai", "present", None), ("monnaie-libre", "present", None)]);
|
||||
// Born with the text of lot T4, put forward: nothing to replace, and it is said once; the
|
||||
// amendment of 30/10, superseded, decides nothing on a new base.
|
||||
assert!(decisions(&state).await.is_empty());
|
||||
let t4: Vec<(String, String)> = sqlx::query_as(
|
||||
"SELECT d.group_slug, s.outcome FROM seed_amendments s JOIN documents d ON d.id = s.doc_id
|
||||
WHERE s.amendment_id = '2026-11-03-i1-amorce' ORDER BY d.group_slug",
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.expect("decisions");
|
||||
assert_eq!(t4, [("essai".to_owned(), "present".to_owned()), ("monnaie-libre".to_owned(), "present".to_owned())]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1043,13 +1063,21 @@ async fn the_shipped_amendment_goes_from_the_preambule_to_the_seed_text() {
|
||||
assert_eq!(a.doc, "acte-engagement-certification");
|
||||
let (_, title, body) = PREAMBULE[0];
|
||||
assert_eq!(ld_server::seed_amend::fingerprint(title, body), a.from, "the Préambule bases hold");
|
||||
let seed = today_seed();
|
||||
let i1 = seed["units"].as_array().expect("units").iter().find(|u| u["code"] == "I1").expect("I1");
|
||||
let v = &i1["versions"][0];
|
||||
// Today the starting text is a seed proposal (ADR 0011); the text is the same.
|
||||
let i1_of = |seed: &Value| -> (String, String, Value) {
|
||||
let i1 = seed["units"].as_array().expect("units").iter().find(|u| u["code"] == "I1").expect("I1");
|
||||
let v = &i1["versions"][0];
|
||||
let (title, body) = (v["title"]["fr"].as_str().expect("title"), v["body"]["fr"].as_str().expect("body"));
|
||||
(title.to_owned(), body.to_owned(), v.clone())
|
||||
};
|
||||
// Until M8 the starting text is a seed proposal (ADR 0011); the text is the same.
|
||||
let (title, body, v) = i1_of(&m8_seed());
|
||||
assert_eq!((v["status"].as_str(), v["seedProposal"].as_bool()), (Some("open"), Some(true)));
|
||||
let (title, body) = (v["title"]["fr"].as_str().expect("title"), v["body"]["fr"].as_str().expect("body"));
|
||||
assert_eq!(ld_server::seed_amend::fingerprint(title, body), a.to, "the text of the seed today");
|
||||
assert_eq!(ld_server::seed_amend::fingerprint(&title, &body), a.to, "the text of the seed of M8");
|
||||
// Lot T4 replaces it, after it only (Yvv's text of 03/11): this one decides nothing more.
|
||||
let t4 = ld_server::seed_amend::AMENDMENTS.iter().find(|x| x.id == "2026-11-03-i1-amorce").expect("lot T4");
|
||||
assert_eq!((t4.from, t4.follows, a.superseded_by), (a.to, Some(a.id), Some(t4.id)));
|
||||
let (title, body, _) = i1_of(&today_seed());
|
||||
assert_eq!(ld_server::seed_amend::fingerprint(&title, &body), t4.to, "the text of the seed today");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -794,10 +794,10 @@ async fn a_session_sees_its_own_ballots_only() {
|
||||
assert_eq!((find(&mine, &i1)["myVote"].as_str(), find(&mine, &e1)["myVote"].as_str()), (Some("for"), None));
|
||||
let theirs = other.doc("monnaie-libre").await;
|
||||
assert_eq!((find(&theirs, &i1)["myVote"].as_str(), find(&theirs, &e1)["myVote"].as_str()), (None, Some("against")));
|
||||
// « Décisions » counts the same way (33 articles, the title, 7 orders with something to decide).
|
||||
// « Décisions » counts the same way (27 articles, the title, 5 orders; lot T4, ADR 0014).
|
||||
let d = chiara.ok("GET", "/api/groups/monnaie-libre/decisions", None).await;
|
||||
assert_eq!(d["canVote"], true);
|
||||
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(41), Some(40)));
|
||||
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(33), Some(32)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2271,12 +2271,12 @@ async fn on_a_base_like_production_the_three_migrations_pass_in_order_and_0010_r
|
||||
let state = AppState::new(Config::for_tests(), pool.clone()).await.expect("state");
|
||||
let mut all = sqlx::migrate!("./migrations");
|
||||
all.set_ignore_missing(true);
|
||||
all.run(&pool).await.expect("0009 to 0011");
|
||||
all.run(&pool).await.expect("0009 to 0012");
|
||||
let order: Vec<i64> = sqlx::query_scalar("SELECT version FROM _sqlx_migrations ORDER BY rowid")
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.expect("order");
|
||||
assert_eq!(order, (1..=11).collect::<Vec<i64>>(), "0009, then 0010, then 0011");
|
||||
assert_eq!(order, (1..=12).collect::<Vec<i64>>(), "0009, then 0010, then 0011 (and 0012 since lot T4)");
|
||||
assert_eq!(tables(pool.clone()).await, written, "no row rewritten");
|
||||
let added: Vec<(String, String, Option<String>)> = sqlx::query_as(
|
||||
"SELECT b.id, b.mode, l.outcome FROM ballots b JOIN ballot_lines l ON l.ballot_id = b.id ORDER BY b.id",
|
||||
|
||||
@@ -0,0 +1,793 @@
|
||||
{
|
||||
"_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). Aucun article en vigueur au départ. I1, E1, E2 : Propositions seed (« seedProposal »), ouvertes, mises en avant pour amorcer le vote, sans niveau d'inertie ici : elles démarrent au cran SEED_PROPOSAL_LEVEL (crates/ld-server/src/seed.rs : 2 « basse », choix d'Yvv du 2026-11-02, qui remontera leur inertie par un vote une fois les textes passés ; ADR 0011). Tant qu'aucun article n'est en vigueur, la publication attend le premier texte. Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21), inertie 3 / 5. I1 « Raison d'être » remplace le Préambule (texte d'Yvv, revue du 2026-10-30). Modules : le titre du document à 4, l'ordre des articles de chaque section à 2, en vigueur sans vote (seed a minima ; revue du 2026-10-30, ADR 0009). F1.2, N1, N1.1, O1 réécrits selon les règles appliquées (Yvv, 2026-11-02). Quorums par cran (« quorums », ADR 0013, Yvv, 2026-11-02 et 03) : 12 · 12 · 50 · 50 · 50 dans Monnaie libre, 1 partout dans le groupe d'essai ; sous le quorum, les votants qui manquent comptent comme « non » ; voteFloor reste le quorum de référence de la participation p0 = 2 × voteFloor / W. Une base existante reçoit ces changements par des amendements gardés (seed_amend, seed_proposal_amend, quorum_amend), jamais en réinstanciant le document. F1.1, F1.2 et N1.1 réécrits pour le quorum par cran (ADR 0013 ; Yvv, 2026-11-02 et 03 ; lot T, textes relus dans docs/chantiers/acte-certification/textes-quorum-seed.md) : sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part du cran. Trois phrases reprises après la relecture de M6b (lot T2) : la part du cran, la plus haute au quorum, baisse quand la participation monte (F1.1) ; l'auteur fixe le quorum tant que l'article ne garde la trace d'aucun vote (F1.2) ; le quorum n'est pas un nombre minimal de votants (N1.1). Lot T3 (Yvv, 03/11 : « pas de chiffre datés dans un texte voté ») : l'exemple chiffré de N1.1 retiré (aucun nombre des textes ne dépend de W ni d'une participation) ; N1.1, sous le quorum, il faut autant de « pour » que si le quorum avait voté (vrai pour tout quorum) ; N1.1 et F1.2, la part du cran comme d'habitude au-delà du quorum. Après la relecture de 2ce2028 (lot T3b, même amendement) : N1.1, G se calcule pour que la part exigée, au-delà du quorum, soit celle du cran quand le groupe vote comme d'habitude (vrai pour tout W et tout quorum). Après la relecture de e7ab82f (lot T3c, mêmes amendements) : F1.2 et N1.1 écrivent le terme défini, « à la participation habituelle » (la médiane ramenée entre 1 % et 50 % de W, celle sur laquelle G se calcule), à la place de « comme d'habitude », faux quand la médiane sort de ses bornes ; N1.1, « sous le quorum de l'article », « la part exigée », « Tant qu'elle repose sur moins de 5 propositions », « Elle est ramenée entre 1 % et 50 % » (liste fermée des phrases : docs/chantiers/acte-certification/t3c-liste-fermee.md).",
|
||||
"slug": "acte-engagement-certification",
|
||||
"title": {
|
||||
"fr": "Acte d'engagement Certification",
|
||||
"en": "Certification Commitment Act",
|
||||
"es": "Acta de compromiso de Certificación"
|
||||
},
|
||||
"summary": {
|
||||
"fr": "Ce que chaque membre de la toile de confiance Ğ1 s'engage à respecter quand il certifie. Texte modulaire sous vote permanent : chaque article entre en vigueur dès qu'il atteint son seuil.",
|
||||
"en": "What every member of the Ğ1 web of trust commits to uphold when certifying. A modular text under permanent vote: each article comes into force as soon as it reaches its threshold.",
|
||||
"es": "Lo que todo miembro de la red de confianza Ğ1 se compromete a respetar cuando certifica. Texto modular en votación permanente: cada artículo entra en vigor en cuanto alcanza su umbral."
|
||||
},
|
||||
"sourceLang": "fr",
|
||||
"instances": [
|
||||
{
|
||||
"group": "monnaie-libre",
|
||||
"voteFloor": 50,
|
||||
"quorums": [12, 12, 50, 50, 50],
|
||||
"publishPath": "monnaie-libre/acte-engagement-certification"
|
||||
},
|
||||
{
|
||||
"group": "essai",
|
||||
"voteFloor": 1,
|
||||
"quorums": [1, 1, 1, 1, 1],
|
||||
"publishPath": null
|
||||
}
|
||||
],
|
||||
"modules": {
|
||||
"title": {
|
||||
"inertia": 4
|
||||
},
|
||||
"order": {
|
||||
"inertia": 2
|
||||
}
|
||||
},
|
||||
"units": [
|
||||
{
|
||||
"code": "I1",
|
||||
"section": "introduction",
|
||||
"position": 1,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"seedProposal": true,
|
||||
"title": {
|
||||
"fr": "Raison d'être",
|
||||
"en": "Purpose",
|
||||
"es": "Razón de ser"
|
||||
},
|
||||
"body": {
|
||||
"fr": "*« Derrière une clé il n'y a qu'un seul être humain vivant ; derrière un être humain, il n'y a qu'une seule clé certifiante. »*\n\nC'est le seul et unique propos de la certification, qui donne à notre toile fiduciaire sa valeur. Nous pouvons nous fier à l'unicité et l'intégrité de tout geste signé par cette clé. C'est l'enjeu de la certification.\n\nCet acte d'engagement est par ailleurs le seul acte de nature contractuelle qui fonde l'usage de la première monnaie libre.",
|
||||
"en": "*“Behind a key there is only one living human being; behind a human being, there is only one certifying key.”*\n\nThis is the one and only purpose of certification, which gives our fiduciary web its value. We can rely on the uniqueness and integrity of every action signed with this key. That is what is at stake in certification.\n\nThis commitment act is, moreover, the only act of a contractual nature on which the use of the first libre currency is founded.",
|
||||
"es": "*«Detrás de una clave no hay más que un solo ser humano vivo; detrás de un ser humano, no hay más que una sola clave certificadora.»*\n\nEste es el propósito único y exclusivo de la certificación, que da su valor a nuestra red fiduciaria. Podemos fiarnos de la unicidad y la integridad de toda acción firmada con esta clave. Eso es lo que está en juego en la certificación.\n\nEsta acta de compromiso es, además, el único acto de naturaleza contractual en el que se funda el uso de la primera moneda libre."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "I2",
|
||||
"section": "introduction",
|
||||
"position": 2,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Les deux garanties réciproques",
|
||||
"en": "The two reciprocal guarantees",
|
||||
"es": "Las dos garantías recíprocas"
|
||||
},
|
||||
"body": {
|
||||
"fr": "La Certification repose sur deux garanties réciproques :\n\n**1.** Derrière une clé publique créatrice de monnaie se trouve un **être humain vivant**.\n\n**2.** Derrière cet être humain se trouve **une seule et unique** clé publique créatrice de monnaie.\n\nLa Certification est un acte technique et fiduciaire, pas un acte d'adhésion morale ou de sympathie.",
|
||||
"en": "Certification rests on two reciprocal guarantees:\n\n**1.** Behind a money-creating public key there is a **living human being**.\n\n**2.** Behind this human being there is **one single and unique** money-creating public key.\n\nCertification is a technical and fiduciary act, not an act of moral endorsement or of sympathy.",
|
||||
"es": "La Certificación se basa en dos garantías recíprocas:\n\n**1.** Detrás de una clave pública creadora de moneda hay un **ser humano vivo**.\n\n**2.** Detrás de este ser humano hay **una sola y única** clave pública creadora de moneda.\n\nLa Certificación es un acto técnico y fiduciario, no un acto de adhesión moral ni de simpatía."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "T1",
|
||||
"section": "fondamental",
|
||||
"position": 3,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Engagements fondamentaux",
|
||||
"en": "Fundamental commitments",
|
||||
"es": "Compromisos fundamentales"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Les engagements fondamentaux ci-dessous constituent le socle irréductible de l'acte d'engagement. Ils sont pris sur l'honneur par tout membre de la toile de confiance.",
|
||||
"en": "The fundamental commitments below form the irreducible foundation of the commitment act. They are made on their honour by every member of the web of trust.",
|
||||
"es": "Los compromisos fundamentales que siguen constituyen la base irreductible del acta de compromiso. Todo miembro de la red de confianza los asume por su honor."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E1",
|
||||
"section": "fondamental",
|
||||
"position": 4,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"seedProposal": true,
|
||||
"title": {
|
||||
"fr": "Unicité du compte",
|
||||
"en": "Uniqueness of the account",
|
||||
"es": "Unicidad de la cuenta"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte cocréateur de monnaie G1.",
|
||||
"en": "I commit on my honour to have, now and ever, no more than one single and unique account co-creating G1 money.",
|
||||
"es": "Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta cocreadora de moneda G1."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E2",
|
||||
"section": "fondamental",
|
||||
"position": 5,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"seedProposal": true,
|
||||
"title": {
|
||||
"fr": "Réciprocité",
|
||||
"en": "Reciprocity",
|
||||
"es": "Reciprocidad"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Je m'engage sur l'honneur à ne certifier que des personnes physiques qui respectent scrupuleusement ces deux présents engagements fondamentaux.",
|
||||
"en": "I commit on my honour to certify only natural persons who scrupulously respect these two fundamental commitments.",
|
||||
"es": "Me comprometo por mi honor a certificar únicamente a personas físicas que respeten escrupulosamente estos dos compromisos fundamentales."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "T2",
|
||||
"section": "technique",
|
||||
"position": 6,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Engagements techniques",
|
||||
"en": "Technical commitments",
|
||||
"es": "Compromisos técnicos"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Les engagements techniques définissent les obligations pratiques et vérifiables du certificateur pour garantir la qualité de la toile de confiance.",
|
||||
"en": "The technical commitments define the practical and verifiable obligations of the certifier to guarantee the quality of the web of trust.",
|
||||
"es": "Los compromisos técnicos definen las obligaciones prácticas y verificables del certificador para garantizar la calidad de la red de confianza."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E3",
|
||||
"section": "technique",
|
||||
"position": 7,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Connaissance suffisante",
|
||||
"en": "Sufficient knowledge",
|
||||
"es": "Conocimiento suficiente"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Je me suis assuré de connaître suffisamment la personne qui gère cette clé publique. Connaître suffisamment ne signifie pas « avoir vu » ; c'est assurer à la communauté G1 que je pourrai la contacter facilement et être en mesure de repérer un double-compte ou tout autre problème.",
|
||||
"en": "I have made sure that I know the person who manages this public key well enough. Knowing well enough does not mean “having seen”; it means assuring the G1 community that I will be able to contact them easily and to detect a duplicate account or any other problem.",
|
||||
"es": "Me he asegurado de conocer suficientemente a la persona que gestiona esta clave pública. Conocer suficientemente no significa «haber visto»; significa asegurar a la comunidad G1 que podré contactarla fácilmente y estar en condiciones de detectar una cuenta duplicada o cualquier otro problema."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "0.4.0-draft",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Connaissance suffisante",
|
||||
"en": "Sufficient knowledge",
|
||||
"es": "Conocimiento suficiente"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Je me suis assuré de connaître suffisamment la personne qui gère cette clé publique : je sais la recontacter par au moins deux canaux indépendants, je suis en mesure de repérer un double-compte, et je m'engage à signaler tout doute aux experts de la communauté. « Avoir vu » quelqu'un ne suffit pas.",
|
||||
"en": "I have made sure that I know the person who manages this public key well enough: I can get back in touch with them through at least two independent channels, I am able to detect a duplicate account, and I commit to reporting any doubt to the community experts. “Having seen” someone is not enough.",
|
||||
"es": "Me he asegurado de conocer suficientemente a la persona que gestiona esta clave pública: puedo volver a contactarla por al menos dos canales independientes, estoy en condiciones de detectar una cuenta duplicada y me comprometo a comunicar cualquier duda a los expertos de la comunidad. «Haber visto» a alguien no basta."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E4",
|
||||
"section": "technique",
|
||||
"position": 8,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Vérification personnelle de la clé",
|
||||
"en": "Personal verification of the key",
|
||||
"es": "Verificación personal de la clave"
|
||||
},
|
||||
"body": {
|
||||
"fr": "J'ai personnellement vérifié que c'est bien cette clé publique que je m'apprête à certifier, en la comparant avec la personne concernée et non par un intermédiaire.",
|
||||
"en": "I have personally verified that this is indeed the public key I am about to certify, by comparing it with the person concerned and not through an intermediary.",
|
||||
"es": "He verificado personalmente que esta es efectivamente la clave pública que me dispongo a certificar, comparándola con la persona interesada y no a través de un intermediario."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E5",
|
||||
"section": "technique",
|
||||
"position": 9,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Joignabilité réciproque",
|
||||
"en": "Mutual reachability",
|
||||
"es": "Contacto recíproco"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Je suis joignable rapidement et facilement par mes certifieurs, et je peux joindre rapidement et facilement les personnes que je certifie, par plusieurs moyens de communication différents et indépendants (physique, téléphone, email, messagerie, etc.).",
|
||||
"en": "I can be reached quickly and easily by my certifiers, and I can quickly and easily reach the people I certify, through several different and independent means of communication (in person, phone, email, messaging, etc.).",
|
||||
"es": "Soy localizable de forma rápida y sencilla por mis certificadores, y puedo localizar de forma rápida y sencilla a las personas que certifico, por varios medios de comunicación diferentes e independientes (en persona, teléfono, correo electrónico, mensajería, etc.)."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E6",
|
||||
"section": "technique",
|
||||
"position": 10,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Document de révocation",
|
||||
"en": "Revocation document",
|
||||
"es": "Documento de revocación"
|
||||
},
|
||||
"body": {
|
||||
"fr": "J'ai vérifié avec la personne certifiée qu'elle a bien généré son document de révocation de compte et qu'elle le conserve en lieu sûr.",
|
||||
"en": "I have checked with the certified person that they have indeed generated their account revocation document and that they keep it in a safe place.",
|
||||
"es": "He verificado con la persona certificada que ha generado efectivamente su documento de revocación de cuenta y que lo conserva en un lugar seguro."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E7",
|
||||
"section": "technique",
|
||||
"position": 11,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Rencontre physique ou vérification multi-canaux",
|
||||
"en": "In-person meeting or multi-channel verification",
|
||||
"es": "Encuentro en persona o verificación multicanal"
|
||||
},
|
||||
"body": {
|
||||
"fr": "J'ai rencontré la personne physiquement (préférable), **OU** j'ai vérifié à distance le lien personne / clé publique par plusieurs moyens de communication différents et indépendants : courrier + réseau social + forum + email + visio + téléphone.",
|
||||
"en": "I have met the person face to face (preferable), **OR** I have remotely verified the person / public key link through several different and independent means of communication: postal mail + social network + forum + email + video call + phone.",
|
||||
"es": "Me he encontrado físicamente con la persona (preferible), **O** he verificado a distancia el vínculo persona / clave pública por varios medios de comunicación diferentes e independientes: correo postal + red social + foro + correo electrónico + videollamada + teléfono."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "0.4.0-draft",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Rencontre physique ou vérification multi-canaux",
|
||||
"en": "In-person meeting or multi-channel verification",
|
||||
"es": "Encuentro en persona o verificación multicanal"
|
||||
},
|
||||
"body": {
|
||||
"fr": "J'ai rencontré la personne physiquement (préférable), **OU** j'ai vérifié à distance le lien personne / clé publique par au moins trois moyens de communication différents et indépendants, dont un échange en visio en direct.",
|
||||
"en": "I have met the person face to face (preferable), **OR** I have remotely verified the person / public key link through at least three different and independent means of communication, including a live video call.",
|
||||
"es": "Me he encontrado físicamente con la persona (preferible), **O** he verificado a distancia el vínculo persona / clave pública por al menos tres medios de comunicación diferentes e independientes, entre ellos una videollamada en directo."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "T3",
|
||||
"section": "technique",
|
||||
"position": 12,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Conseils et bonnes pratiques",
|
||||
"en": "Advice and good practices",
|
||||
"es": "Consejos y buenas prácticas"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Les pratiques suivantes sont fortement recommandées pour garantir la qualité et la sécurité de la toile de confiance.",
|
||||
"en": "The following practices are strongly recommended to guarantee the quality and security of the web of trust.",
|
||||
"es": "Las siguientes prácticas se recomiendan encarecidamente para garantizar la calidad y la seguridad de la red de confianza."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E8",
|
||||
"section": "technique",
|
||||
"position": 13,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Ne jamais certifier seul",
|
||||
"en": "Never certify alone",
|
||||
"es": "No certificar nunca en solitario"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Ne certifiez jamais seul, mais accompagné d'au moins un autre membre de la toile de confiance G1, pour garantir un double contrôle.",
|
||||
"en": "Never certify alone, but accompanied by at least one other member of the G1 web of trust, to guarantee a double check.",
|
||||
"es": "No certifique nunca en solitario, sino junto con al menos otro miembro de la red de confianza G1, para garantizar un doble control."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E9",
|
||||
"section": "technique",
|
||||
"position": 14,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Vérification des certifications existantes",
|
||||
"en": "Verification of existing certifications",
|
||||
"es": "Verificación de las certificaciones existentes"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Avant toute Certification, vérifiez si le compte a déjà reçu des certifications et de qui elles proviennent. Contactez les certifieurs existants en cas de doute. Si un certifieurs existant ne connaît pas la personne, alertez immédiatement les experts de la communauté.",
|
||||
"en": "Before any Certification, check whether the account has already received certifications and who they come from. Contact the existing certifiers if in doubt. If an existing certifier does not know the person, alert the community experts immediately.",
|
||||
"es": "Antes de cualquier Certificación, verifique si la cuenta ya ha recibido certificaciones y de quién proceden. En caso de duda, contacte con los certificadores existentes. Si un certificador existente no conoce a la persona, alerte de inmediato a los expertos de la comunidad."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E10",
|
||||
"section": "technique",
|
||||
"position": 15,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Vérification de maîtrise du compte",
|
||||
"en": "Verification of account control",
|
||||
"es": "Verificación del control de la cuenta"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Vérifiez la maîtrise du compte par un transfert-test : envoyez quelques G1 et demandez le renvoi, afin de vous assurer que la personne contrôle bien sa clé privée.",
|
||||
"en": "Verify control of the account with a test transfer: send a few G1 and ask for them to be sent back, to make sure that the person really controls their private key.",
|
||||
"es": "Verifique el control de la cuenta mediante una transferencia de prueba: envíe unos pocos G1 y pida que se los devuelvan, para asegurarse de que la persona controla realmente su clave privada."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E11",
|
||||
"section": "technique",
|
||||
"position": 16,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Transmission et compréhension du document",
|
||||
"en": "Transmission and understanding of the document",
|
||||
"es": "Transmisión y comprensión del documento"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Vérifiez que vos contacts ont bien étudié et compris le présent acte d'engagement dans sa version à jour avant de procéder à la Certification.",
|
||||
"en": "Check that your contacts have indeed studied and understood the current version of this commitment act before proceeding with the Certification.",
|
||||
"es": "Verifique que sus contactos han estudiado y comprendido efectivamente la versión vigente de la presente acta de compromiso antes de proceder a la Certificación."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "K1",
|
||||
"section": "conclusion",
|
||||
"position": 17,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Règles abrégées de la toile de confiance",
|
||||
"en": "Abridged rules of the web of trust",
|
||||
"es": "Reglas abreviadas de la red de confianza"
|
||||
},
|
||||
"body": {
|
||||
"fr": "**Paramètres protocolaires en vigueur :**\n\n- Stock de **100 certifications** possibles\n- 1 certification émissible tous les **5 jours**\n- Nouveau compte valide si **>= 5 certifications** reçues en **2 mois**\n- Condition de distance : **<= 5 pas** de **80% des sentinelles**\n- Sentinelle : membre ayant reçu et émis >= Y[N] certifs (Y = ceil(N^(1/5)))\n- Certifications actives valables **2 ans**\n- Renouvellement de l'accord tous les **12 mois**\n\n*Note : le vote porte sur l'inclusion de ces règles dans le document, pas sur les valeurs des variables protocolaires elles-mêmes, qui sont fixées par le protocole Duniter.*",
|
||||
"en": "**Protocol parameters in force:**\n\n- Stock of **100 certifications** available\n- 1 certification can be issued every **5 days**\n- New account valid if **>= 5 certifications** received within **2 months**\n- Distance rule: **<= 5 steps** from **80% of the sentries**\n- Sentry: member who has received and issued >= Y[N] certifications (Y = ceil(N^(1/5)))\n- Active certifications valid for **2 years**\n- Renewal of the agreement every **12 months**\n\n*Note: the vote concerns the inclusion of these rules in the document, not the values of the protocol variables themselves, which are set by the Duniter protocol.*",
|
||||
"es": "**Parámetros de protocolo vigentes:**\n\n- Reserva de **100 certificaciones** posibles\n- 1 certificación emitible cada **5 días**\n- Cuenta nueva válida si recibe **>= 5 certificaciones** en **2 meses**\n- Regla de distancia: **<= 5 pasos** del **80% de los centinelas**\n- Centinela: miembro que ha recibido y emitido >= Y[N] certificaciones (Y = ceil(N^(1/5)))\n- Certificaciones activas válidas durante **2 años**\n- Renovación del acuerdo cada **12 meses**\n\n*Nota: la votación se refiere a la inclusión de estas reglas en el documento, no a los valores de las variables del protocolo en sí, que fija el protocolo Duniter.*"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "K2",
|
||||
"section": "conclusion",
|
||||
"position": 18,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Monnaie G1",
|
||||
"en": "G1 currency",
|
||||
"es": "Moneda G1"
|
||||
},
|
||||
"body": {
|
||||
"fr": "**Paramètres monétaires :**\n\n- 1 Dividende Universel (DU) par personne par jour\n- Réévaluation à chaque équinoxe : `DU(n+1) = DU(n) + c² × (M/N) / 182.625` avec c = 4.88%\n- DU(0) = 10.00 G1\n\n*Note : le vote porte sur l'inclusion de ces paramètres dans le document, pas sur les valeurs monétaires elles-mêmes, qui découlent de la TRM et du bloc 0.*",
|
||||
"en": "**Monetary parameters:**\n\n- 1 Universal Dividend (UD) per person per day\n- Revaluation at each equinox: `DU(n+1) = DU(n) + c² × (M/N) / 182.625` with c = 4.88%\n- DU(0) = 10.00 G1\n\n*Note: the vote concerns the inclusion of these parameters in the document, not the monetary values themselves, which derive from the RTM and block 0.*",
|
||||
"es": "**Parámetros monetarios:**\n\n- 1 Dividendo Universal (DU) por persona y día\n- Revalorización en cada equinoccio: `DU(n+1) = DU(n) + c² × (M/N) / 182.625` con c = 4.88%\n- DU(0) = 10.00 G1\n\n*Nota: la votación se refiere a la inclusión de estos parámetros en el documento, no a los valores monetarios en sí, que se derivan de la TRM y del bloque 0.*"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "X1",
|
||||
"section": "annexe",
|
||||
"position": 19,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Annexe 1 : Intégration logicielle",
|
||||
"en": "Annex 1: Software integration",
|
||||
"es": "Anexo 1: Integración en el software"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Les obligations pour les logiciels implémentant la Certification G1.",
|
||||
"en": "The obligations for software implementing G1 Certification.",
|
||||
"es": "Las obligaciones del software que implementa la Certificación G1."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "X1.1",
|
||||
"section": "annexe",
|
||||
"position": 20,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Dépôt de référence",
|
||||
"en": "Reference repository",
|
||||
"es": "Repositorio de referencia"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Le texte de référence de l'acte d'engagement Certification est hébergé dans le dépôt git officiel : https://git.duniter.org/documents/g1_monetary_license\n\nLes applications Cesium, Gecko et toute application de Certification doivent pointer vers ce dépôt pour afficher la version en vigueur.",
|
||||
"en": "The reference text of the Certification commitment act is hosted in the official git repository: https://git.duniter.org/documents/g1_monetary_license\n\nThe Cesium and Gecko applications, and any Certification application, must point to this repository to display the version in force.",
|
||||
"es": "El texto de referencia del acta de compromiso de Certificación está alojado en el repositorio git oficial: https://git.duniter.org/documents/g1_monetary_license\n\nLas aplicaciones Cesium, Gecko y cualquier aplicación de Certificación deben apuntar a este repositorio para mostrar la versión vigente."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "X1.2",
|
||||
"section": "annexe",
|
||||
"position": 21,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Obligation de transmission",
|
||||
"en": "Transmission obligation",
|
||||
"es": "Obligación de transmisión"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Tout logiciel G1 permettant la Certification doit transmettre le présent acte d'engagement au certifié et en afficher les paramètres du bloc 0 de la blockchain Duniter.",
|
||||
"en": "Any G1 software that enables Certification must transmit this commitment act to the certified person and display the block 0 parameters of the Duniter blockchain.",
|
||||
"es": "Todo software G1 que permita la Certificación debe transmitir la presente acta de compromiso a la persona certificada y mostrar los parámetros del bloque 0 de la blockchain Duniter."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "X1.3",
|
||||
"section": "annexe",
|
||||
"position": 22,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Logiciel auditable",
|
||||
"en": "Auditable software",
|
||||
"es": "Software auditable"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Tout logiciel utilisé pour la Certification ou la création monétaire doit être publié sous licence libre, afin de permettre son audit par la communauté.",
|
||||
"en": "Any software used for Certification or money creation must be published under a free licence, so that it can be audited by the community.",
|
||||
"es": "Todo software utilizado para la Certificación o la creación monetaria debe publicarse bajo licencia libre, para permitir su auditoría por parte de la comunidad."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "X2",
|
||||
"section": "annexe",
|
||||
"position": 23,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Annexe 2 : Questions à la Certification",
|
||||
"en": "Annex 2: Questions for Certification",
|
||||
"es": "Anexo 2: Preguntas para la Certificación"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Liste de questions à présenter dans les logiciels de Certification.Inspirée de la checklist de la Charte 1.0 et des discussions forum (topic 32412).",
|
||||
"en": "List of questions to be presented in Certification software. Inspired by the checklist of Charter 1.0 and by forum discussions (topic 32412).",
|
||||
"es": "Lista de preguntas que deben presentarse en el software de Certificación. Inspirada en la lista de verificación de la Carta 1.0 y en los debates del foro (tema 32412)."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "X2.1",
|
||||
"section": "annexe",
|
||||
"position": 24,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Questions piège (réponse attendue : NON)",
|
||||
"en": "Trap questions (expected answer: NO)",
|
||||
"es": "Preguntas trampa (respuesta esperada: NO)"
|
||||
},
|
||||
"body": {
|
||||
"fr": "**Si OUI à l'une de ces questions, la Certification doit être refusée.**\n\n- La personne m'a contacté uniquement pour obtenir ma Certification\n- Je certifie sous la pression ou pour faire plaisir\n- Je n'ai aucun moyen de vérifier l'identité de la personne\n- La personne refuse de me fournir plusieurs moyens de contact\n- Je soupçonne que la personne possède déjà un compte membre\n- Je ne connais aucun de ses autres certifieurs\n- La personne ne comprend pas ce qu'est la monnaie libre\n- La personne n'a pas lu le présent acte d'engagement",
|
||||
"en": "**If YES to any of these questions, the Certification must be refused.**\n\n- The person contacted me only to obtain my Certification\n- I am certifying under pressure or to please someone\n- I have no way of verifying the person's identity\n- The person refuses to give me several means of contact\n- I suspect that the person already has a member account\n- I do not know any of their other certifiers\n- The person does not understand what libre currency is\n- The person has not read this commitment act",
|
||||
"es": "**Si la respuesta es SÍ a alguna de estas preguntas, la Certificación debe rechazarse.**\n\n- La persona me ha contactado únicamente para obtener mi Certificación\n- Certifico bajo presión o por complacer\n- No tengo ningún medio para verificar la identidad de la persona\n- La persona se niega a proporcionarme varios medios de contacto\n- Sospecho que la persona ya posee una cuenta de miembro\n- No conozco a ninguno de sus otros certificadores\n- La persona no entiende qué es la moneda libre\n- La persona no ha leído la presente acta de compromiso"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "X2.2",
|
||||
"section": "annexe",
|
||||
"position": 25,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Questions identité (réponse attendue : OUI)",
|
||||
"en": "Identity questions (expected answer: YES)",
|
||||
"es": "Preguntas de identidad (respuesta esperada: SÍ)"
|
||||
},
|
||||
"body": {
|
||||
"fr": "**Si NON à l'une de ces questions, la Certification doit être refusée.**\n\n- Je connais cette personne suffisamment pour la recontacter\n- J'ai vérifié personnellement le lien personne / clé publique\n- La personne est une personne physique vivante (pas une entité morale)\n- Je pourrais reconnaître cette personne si je la croisais\n- J'ai au moins 2 moyens de contact différents pour cette personne",
|
||||
"en": "**If NO to any of these questions, the Certification must be refused.**\n\n- I know this person well enough to get back in touch with them\n- I have personally verified the person / public key link\n- The person is a living natural person (not a legal entity)\n- I would recognise this person if I came across them\n- I have at least 2 different means of contact for this person",
|
||||
"es": "**Si la respuesta es NO a alguna de estas preguntas, la Certificación debe rechazarse.**\n\n- Conozco a esta persona suficientemente para volver a contactarla\n- He verificado personalmente el vínculo persona / clave pública\n- La persona es una persona física viva (no una persona jurídica)\n- Reconocería a esta persona si me la cruzara\n- Tengo al menos 2 medios de contacto diferentes para esta persona"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "X2.3",
|
||||
"section": "annexe",
|
||||
"position": 26,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Questions sécurité (réponse attendue : OUI)",
|
||||
"en": "Security questions (expected answer: YES)",
|
||||
"es": "Preguntas de seguridad (respuesta esperada: SÍ)"
|
||||
},
|
||||
"body": {
|
||||
"fr": "**Si NON, la Certification est déconseillée.**\n\n- La personne a généré son document de révocation\n- La personne maîtrise effectivement son compte (test de transfert effectué)\n- La personne sait où retrouver le présent acte d'engagement dans sa version à jour",
|
||||
"en": "**If NO, the Certification is not recommended.**\n\n- The person has generated their revocation document\n- The person actually controls their account (test transfer carried out)\n- The person knows where to find the current version of this commitment act",
|
||||
"es": "**Si la respuesta es NO, se desaconseja la Certificación.**\n\n- La persona ha generado su documento de revocación\n- La persona controla efectivamente su cuenta (transferencia de prueba realizada)\n- La persona sabe dónde encontrar la versión vigente de la presente acta de compromiso"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "F1",
|
||||
"section": "formule",
|
||||
"position": 27,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Formule de vote",
|
||||
"en": "Voting formula",
|
||||
"es": "Fórmula de votación"
|
||||
},
|
||||
"body": {
|
||||
"fr": "La formule qui régit l'adoption ou le rejet de chaque modification du présent document. Référence : g1vote (https://g1vote-view-237903.pages.duniter.org/).",
|
||||
"en": "The formula that governs the adoption or rejection of each amendment to this document. Reference: g1vote (https://g1vote-view-237903.pages.duniter.org/).",
|
||||
"es": "La fórmula que rige la adopción o el rechazo de cada modificación del presente documento. Referencia: g1vote (https://g1vote-view-237903.pages.duniter.org/)."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "F1.1",
|
||||
"section": "formule",
|
||||
"position": 28,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Formule du seuil WoT",
|
||||
"en": "WoT threshold formula",
|
||||
"es": "Fórmula del umbral WoT"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Le seuil est le nombre de voix pour qu'il faut à une version pour entrer en vigueur. Sous le quorum Q, les votants qui manquent comptent comme « non » : le seuil se calcule comme si Q personnes avaient voté. Au-delà, il faut la part de « pour » que demande le cran d'inertie de l'article, parmi tous les votants : cette part est la plus haute au quorum et baisse quand la participation monte, jusqu'à la majorité M quand tout le monde vote.\n\n`T' = max(T, Q)`\n`Seuil = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUne version passe quand ses voix pour atteignent le seuil, arrondi à l'entier au-dessus.\n\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q** : le quorum de l'article ; **T'** : les votants retenus, T, ou Q s'il en manque.\n- **W** : le nombre de membres de la toile de confiance Ğ1.\n- **B** : la base ; à la puissance W, elle est si petite qu'elle ne compte pas.\n- **M** : la majorité demandée quand tout le monde vote.\n- **G** : le gradient : plus il est grand, plus la part demandée monte quand peu de gens votent.\n\nLeurs valeurs sont dans « Paramètres par défaut » et « Niveaux d'inertie ».",
|
||||
"en": "The threshold is the number of votes in favour a version needs to come into force. Below the quorum Q, the voters who are missing count as votes against: the threshold is worked out as if Q people had voted. Beyond it, it takes the share of votes in favour that the article's level of inertia asks for, among all the voters: that share is highest at the quorum and falls as turnout rises, down to the majority M when everyone votes.\n\n`T' = max(T, Q)`\n`Threshold = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nA version passes when its votes in favour reach the threshold, rounded up to a whole number.\n\n- **T**: the votes cast on the version, for and against.\n- **Q**: the article's quorum; **T'**: the voters counted, T, or Q if some are missing.\n- **W**: the number of members of the Ğ1 web of trust.\n- **B**: the base; raised to the power W, it is so small that it does not count.\n- **M**: the majority asked for when everyone votes.\n- **G**: the gradient: the larger it is, the higher the share asked for when few people vote.\n\nTheir values are in “Default parameters” and “Inertia levels”.",
|
||||
"es": "El umbral es el número de votos a favor que necesita una versión para entrar en vigor. Por debajo del quórum Q, los votantes que faltan cuentan como «no»: el umbral se calcula como si hubieran votado Q personas. Más allá, hace falta la proporción de votos a favor que pide el nivel de inercia del artículo, entre todos los votantes: esa proporción es la más alta en el quórum y baja cuando sube la participación, hasta la mayoría M cuando vota todo el mundo.\n\n`T' = max(T, Q)`\n`Umbral = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUna versión pasa cuando sus votos a favor alcanzan el umbral, redondeado al entero superior.\n\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**: el quórum del artículo; **T'**: los votantes contados, T, o Q si faltan.\n- **W**: el número de miembros de la red de confianza Ğ1.\n- **B**: la base; elevada a la potencia W, es tan pequeña que no cuenta.\n- **M**: la mayoría pedida cuando vota todo el mundo.\n- **G**: el gradiente: cuanto mayor es, más alta es la proporción pedida cuando vota poca gente.\n\nSus valores están en «Parámetros por defecto» y «Niveles de inercia»."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "F1.2",
|
||||
"section": "formule",
|
||||
"position": 29,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Paramètres par défaut",
|
||||
"en": "Default parameters",
|
||||
"es": "Parámetros por defecto"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la façon dont le vote se déroule.\n\n**Un vote permanent, sans durée**\n\n- Chaque version se vote à tout moment, sans échéance. Chacun peut changer ou retirer son vote : le dernier fait foi.\n- Une proposition entre en vigueur dès qu'elle atteint son seuil. S'il y a déjà un texte en vigueur, il lui faut en plus une marge (voix pour moins voix exigées) plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir.\n- Si plusieurs propositions y parviennent, celle qui a la marge la plus large entre en vigueur.\n\n**Les lettres**\n\n- **W** : le nombre de membres de la toile de confiance Ğ1, relu sur la chaîne toutes les 6 heures.\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q**, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au cran 5. L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote. Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un texte.\n- **B = 0,1** : dans la toile de confiance, `B^W` est si petit qu'il ne compte pas.\n- **M** et **G** : donnés par le cran d'inertie de l'article (voir « Niveaux d'inertie »). G n'est pas fixé à la main : il se déduit de la participation habituelle du groupe.\n\n**Par défaut**\n\n- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » à sa participation habituelle. Tout nouvel article commence à ce cran.\n- Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, tous les autres articles au cran 3.",
|
||||
"en": "The values of the letters of the formula (see “WoT threshold formula”) and how the vote runs.\n\n**A permanent vote, with no duration**\n\n- Every version can be voted on at any time, with no deadline. Anyone can change or withdraw their vote: the last one is the one that counts.\n- A proposal comes into force as soon as it reaches its threshold. If a text is already in force, it also needs a wider margin (votes in favour minus votes required) than that text has. The replaced text becomes a proposal again, with its votes: it can come back.\n- If several proposals get there, the one with the widest margin comes into force.\n\n**The letters**\n\n- **W**: the number of members of the Ğ1 web of trust, read again from the chain every 6 hours.\n- **T**: the votes cast on the version, for and against.\n- **Q**, the quorum: below this number of voters, those who are missing count as votes against. Each article takes the one of its level: 12, 12, 50, 50 and 50 in Monnaie libre, from level 1 to level 5. The author of the first version of an article, an administrator for the starting texts, can set another one as long as the article keeps no trace of any vote. In Monnaie libre, never less than 2: a single vote never passes a text.\n- **B = 0.1**: in the web of trust, `B^W` is so small that it does not count.\n- **M** and **G**: given by the article's level of inertia (see “Inertia levels”). G is not set by hand: it follows from the group's usual turnout.\n\n**By default**\n\n- The default level is 3 (medium): M = 55%, a quorum of 50 in Monnaie libre, and, beyond the quorum, the group asks for 75% of votes in favour at its usual turnout. Every new article starts at this level.\n- At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, all other articles at level 3.",
|
||||
"es": "Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y cómo se desarrolla la votación.\n\n**Una votación permanente, sin duración**\n\n- Cada versión se vota en cualquier momento, sin plazo. Cada cual puede cambiar o retirar su voto: el último es el que cuenta.\n- Una propuesta entra en vigor en cuanto alcanza su umbral. Si ya hay un texto en vigor, necesita además un margen (votos a favor menos votos exigidos) más amplio que el de ese texto. El texto sustituido vuelve a ser una propuesta, con sus votos: puede volver.\n- Si varias propuestas lo consiguen, entra en vigor la que tiene el margen más amplio.\n\n**Las letras**\n\n- **W**: el número de miembros de la red de confianza Ğ1, leído de nuevo en la cadena cada 6 horas.\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**, el quórum: por debajo de este número de votantes, los que faltan cuentan como «no». Cada artículo toma el de su nivel: 12, 12, 50, 50 y 50 en Monnaie libre, del nivel 1 al nivel 5. El autor de la primera versión de un artículo, un administrador para los textos de partida, puede fijar otro mientras el artículo no guarde rastro de ningún voto. En Monnaie libre, nunca menos de 2: un solo voto nunca hace pasar un texto.\n- **B = 0,1**: en la red de confianza, `B^W` es tan pequeño que no cuenta.\n- **M** y **G**: los da el nivel de inercia del artículo (ver «Niveles de inercia»). G no se fija a mano: se deduce de la participación habitual del grupo.\n\n**Por defecto**\n\n- El nivel por defecto es el 3 (media): M = 55 %, un quórum de 50 en Monnaie libre, y, más allá del quórum, el grupo pide un 75 % de votos a favor con su participación habitual. Todo artículo nuevo empieza en este nivel.\n- Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "F1.3",
|
||||
"section": "formule",
|
||||
"position": 30,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Processus de dépôt officiel",
|
||||
"en": "Official filing process",
|
||||
"es": "Proceso de depósito oficial"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Lorsqu'une proposition alternative atteint le seuil d'adoption :\n\n1. Le texte de remplacement est intégré au document officiel\n2. Le hash IPFS du document mis à jour est calculé\n3. Le hash est ancré on-chain via `system.remark`\n4. Le dépôt git officiel est synchronisé\n5. Les applications (Cesium, Gecko) mettent à jour automatiquement",
|
||||
"en": "When an alternative proposal reaches the adoption threshold:\n\n1. The replacement text is incorporated into the official document\n2. The IPFS hash of the updated document is computed\n3. The hash is anchored on-chain via `system.remark`\n4. The official git repository is synchronised\n5. The applications (Cesium, Gecko) update automatically",
|
||||
"es": "Cuando una propuesta alternativa alcanza el umbral de adopción:\n\n1. El texto de sustitución se integra en el documento oficial\n2. Se calcula el hash IPFS del documento actualizado\n3. El hash se ancla on-chain mediante `system.remark`\n4. Se sincroniza el repositorio git oficial\n5. Las aplicaciones (Cesium, Gecko) se actualizan automáticamente"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "N1",
|
||||
"section": "inertie",
|
||||
"position": 31,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Réglage de l'inertie",
|
||||
"en": "Inertia setting",
|
||||
"es": "Ajuste de la inercia"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Chaque article porte son inertie : un cran de 1 à 5 (voir « Niveaux d'inertie »). Plus le cran est haut, plus il faut de voix pour changer le texte quand peu de gens votent. 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.\n\nTout membre du groupe peut proposer un autre cran. Cette proposition se vote au cran actuel de l'article, comme son texte : l'inertie de l'inertie hérite du cran de l'article. Ainsi, un article au cran 4 ne descend qu'avec les voix que demande le cran 4.",
|
||||
"en": "Every article carries its inertia: a level from 1 to 5 (see “Inertia levels”). The higher the level, the more votes it takes to change the text when few people vote. The title of the document and the order of the articles of each section have theirs too, changed in the same way.\n\nAny member of the group can propose another level. That proposal is voted at the article's current level, like its text: the inertia of the inertia inherits the article's level. So an article at level 4 only comes down with the votes that level 4 asks for.",
|
||||
"es": "Cada artículo lleva su inercia: un nivel del 1 al 5 (ver «Niveles de inercia»). Cuanto más alto es el nivel, más votos hacen falta para cambiar el texto cuando vota poca gente. El título del documento y el orden de los artículos de cada sección también tienen la suya, que se cambia del mismo modo.\n\nCualquier miembro del grupo puede proponer otro nivel. Esa propuesta se vota con el nivel actual del artículo, como su texto: la inercia de la inercia hereda el nivel del artículo. Así, un artículo en el nivel 4 solo baja con los votos que pide el nivel 4."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "N1.1",
|
||||
"section": "inertie",
|
||||
"position": 32,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Niveaux d'inertie",
|
||||
"en": "Inertia levels",
|
||||
"es": "Niveles de inercia"
|
||||
},
|
||||
"body": {
|
||||
"fr": "À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). Chaque cran a aussi son quorum : 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é. La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.\n\n`Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum de l'article s'il en manque.\n\n**Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum de l'article, à la participation habituelle, puis quand tout le monde vote, et ce qui est à ce cran au départ.\n\n- **Cran 1 · très basse** : quorum 12 ; 60 % à la participation habituelle, 50 % quand tout le monde vote. Aucun article au départ.\n- **Cran 2 · basse** : quorum 12 ; 67 % à la participation habituelle, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.\n- **Cran 3 · moyenne** : quorum 50 ; 75 % à la participation habituelle, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.\n- **Cran 4 · haute** : quorum 50 ; 83 % à la participation habituelle, 60 % quand tout le monde vote. Le titre du document.\n- **Cran 5 · très haute** : quorum 50 ; 90 % à la participation habituelle, 66 % quand tout le monde vote. Aucun article au départ.\n\n**La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de référence du document : 100 votants dans Monnaie libre. Elle ne dépend pas du quorum d'un article. Elle est ramenée entre 1 % et 50 % de l'électorat, et se mesure une fois par jour.\n\n**G** se calcule pour que la part exigée, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W.",
|
||||
"en": "At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). Each level also has its quorum: below the article's quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if that quorum had voted. The share asked depends on the level and on the number of voters, compared with the group's usual turnout: beyond that quorum, the more voters, the lower it gets, down to the majority M when everyone votes.\n\n`Share asked = M + (1 − M) × (1 − (T'/W)^G)`, where T' is the number of voters, or the article's quorum if some are missing.\n\n**The five levels**: the quorum in Monnaie libre, the share asked beyond the article's quorum, at the usual turnout, then when everyone votes, and what is at that level at the start.\n\n- **Level 1 · very low**: quorum 12; 60% at the usual turnout, 50% when everyone votes. No article at the start.\n- **Level 2 · low**: quorum 12; 67% at the usual turnout, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.\n- **Level 3 · medium**: quorum 50; 75% at the usual turnout, 55% when everyone votes. The default level: all other articles, and every new article.\n- **Level 4 · high**: quorum 50; 83% at the usual turnout, 60% when everyone votes. The title of the document.\n- **Level 5 · very high**: quorum 50; 90% at the usual turnout, 66% when everyone votes. No article at the start.\n\n**The usual turnout** is the median number of voters on the group's proposals voted on over the past year. While it rests on fewer than 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It is kept between 1% and 50% of the electorate, and is measured once a day.\n\n**G** is computed so that the share asked, beyond the article's quorum, is exactly that of the level at the usual turnout: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is that share of the level (60, 67, 75, 83 or 90%) and p̃ the usual turnout divided by W.",
|
||||
"es": "En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). Cada nivel tiene además su quórum: por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum. La proporción exigida depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá de ese quórum, cuantos más votantes, más baja, hasta la mayoría M cuando vota todo el mundo.\n\n`Proporción exigida = M + (1 − M) × (1 − (T'/W)^G)`, donde T' es el número de votantes, o el quórum del artículo si faltan.\n\n**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida más allá del quórum del artículo, con la participación habitual, luego cuando vota todo el mundo, y lo que está en ese nivel al inicio.\n\n- **Nivel 1 · muy baja**: quórum 12; 60 % con la participación habitual, 50 % cuando vota todo el mundo. Ningún artículo al inicio.\n- **Nivel 2 · baja**: quórum 12; 67 % con la participación habitual, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.\n- **Nivel 3 · media**: quórum 50; 75 % con la participación habitual, 55 % cuando vota todo el mundo. El nivel por defecto: todos los demás artículos, y todo artículo nuevo.\n- **Nivel 4 · alta**: quórum 50; 83 % con la participación habitual, 60 % cuando vota todo el mundo. El título del documento.\n- **Nivel 5 · muy alta**: quórum 50; 90 % con la participación habitual, 66 % cuando vota todo el mundo. Ningún artículo al inicio.\n\n**La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Mientras se base en menos de 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se acota entre el 1 % y el 50 % del electorado, y se mide una vez al día.\n\n**G** se calcula para que la proporción exigida, más allá del quórum del artículo, sea exactamente la del nivel con la participación habitual: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es esa proporción del nivel (60, 67, 75, 83 o 90 %) y p̃ la participación habitual dividida por W."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "O1",
|
||||
"section": "ordonnancement",
|
||||
"position": 33,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Ordonnancement du document",
|
||||
"en": "Ordering of the document",
|
||||
"es": "Ordenación del documento"
|
||||
},
|
||||
"body": {
|
||||
"fr": "L'ordre des articles de chaque section est lui-même soumis au vote. En fin de section, le module « Ordre des articles » se vote comme un article, au cran 2 (basse) au départ : chaque proposition est la liste des articles de la section, dans un autre ordre. Quand un ordre entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change.\n\nUn nouvel article se place à la fin de sa section ; sa place se vote ensuite par l'ordre de la section. Un ordre proposé garde ses votes quand la section change : un article arrivé depuis s'y ajoute à la fin, un article sorti en disparaît. Une section d'un seul article n'a pas d'ordre à voter.\n\nLe titre du document est lui aussi un module, voté au cran 4 (haute) au départ. Le titre de chaque article, lui, se vote avec son texte.",
|
||||
"en": "The order of the articles of each section is itself put to the vote. At the end of each section, the “Order of the articles” module is voted on like an article, at level 2 (low) at the start: each proposal is the list of the section's articles, in another order. When an order comes into force, only the articles of that section change place; no text changes.\n\nA new article goes at the end of its section; its place is then voted on through the order of the section. A proposed order keeps its votes when the section changes: an article that has arrived since is added at the end, an article that has left drops out of it. A section with a single article has no order to vote on.\n\nThe title of the document is also a module, voted on at level 4 (high) at the start. The title of each article is voted on with its text.",
|
||||
"es": "El orden de los artículos de cada sección está a su vez sometido a votación. Al final de cada sección, el módulo «Orden de los artículos» se vota como un artículo, con el nivel 2 (baja) al inicio: cada propuesta es la lista de los artículos de la sección, en otro orden. Cuando un orden entra en vigor, solo los artículos de esa sección cambian de lugar; ningún texto cambia.\n\nUn artículo nuevo se coloca al final de su sección; su lugar se vota después mediante el orden de la sección. Un orden propuesto conserva sus votos cuando la sección cambia: un artículo llegado después se añade al final, un artículo que ha salido desaparece de él. Una sección de un solo artículo no tiene orden que votar.\n\nEl título del documento es también un módulo, votado con el nivel 4 (alta) al inicio. El título de cada artículo, en cambio, se vota con su texto."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"_source": "seeds/acte-engagement-certification.json au commit 8c1e7af : F1.1, F1.2 et N1.1 tels que la production les tient depuis M6c (84f015a, le 03/11 à 11 h 05 ; lot T2, les trois phrases), inchangés par M7b (6f6197c)",
|
||||
"units": {
|
||||
"F1.1": {
|
||||
"label": "1.0.0",
|
||||
"title": {
|
||||
"fr": "Formule du seuil WoT",
|
||||
"en": "WoT threshold formula",
|
||||
"es": "Fórmula del umbral WoT"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Le seuil est le nombre de voix pour qu'il faut à une version pour entrer en vigueur. Sous le quorum Q, les votants qui manquent comptent comme « non » : le seuil se calcule comme si Q personnes avaient voté. Au-delà, il faut la part de « pour » que demande le cran d'inertie de l'article, parmi tous les votants : cette part est la plus haute au quorum et baisse quand la participation monte, jusqu'à la majorité M quand tout le monde vote.\n\n`T' = max(T, Q)`\n`Seuil = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUne version passe quand ses voix pour atteignent le seuil, arrondi à l'entier au-dessus.\n\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q** : le quorum de l'article ; **T'** : les votants retenus, T, ou Q s'il en manque.\n- **W** : le nombre de membres de la toile de confiance Ğ1.\n- **B** : la base ; à la puissance W, elle est si petite qu'elle ne compte pas.\n- **M** : la majorité demandée quand tout le monde vote.\n- **G** : le gradient : plus il est grand, plus la part demandée monte quand peu de gens votent.\n\nLeurs valeurs sont dans « Paramètres par défaut » et « Niveaux d'inertie ».",
|
||||
"en": "The threshold is the number of votes in favour a version needs to come into force. Below the quorum Q, the voters who are missing count as votes against: the threshold is worked out as if Q people had voted. Beyond it, it takes the share of votes in favour that the article's level of inertia asks for, among all the voters: that share is highest at the quorum and falls as turnout rises, down to the majority M when everyone votes.\n\n`T' = max(T, Q)`\n`Threshold = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nA version passes when its votes in favour reach the threshold, rounded up to a whole number.\n\n- **T**: the votes cast on the version, for and against.\n- **Q**: the article's quorum; **T'**: the voters counted, T, or Q if some are missing.\n- **W**: the number of members of the Ğ1 web of trust.\n- **B**: the base; raised to the power W, it is so small that it does not count.\n- **M**: the majority asked for when everyone votes.\n- **G**: the gradient: the larger it is, the higher the share asked for when few people vote.\n\nTheir values are in “Default parameters” and “Inertia levels”.",
|
||||
"es": "El umbral es el número de votos a favor que necesita una versión para entrar en vigor. Por debajo del quórum Q, los votantes que faltan cuentan como «no»: el umbral se calcula como si hubieran votado Q personas. Más allá, hace falta la proporción de votos a favor que pide el nivel de inercia del artículo, entre todos los votantes: esa proporción es la más alta en el quórum y baja cuando sube la participación, hasta la mayoría M cuando vota todo el mundo.\n\n`T' = max(T, Q)`\n`Umbral = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUna versión pasa cuando sus votos a favor alcanzan el umbral, redondeado al entero superior.\n\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**: el quórum del artículo; **T'**: los votantes contados, T, o Q si faltan.\n- **W**: el número de miembros de la red de confianza Ğ1.\n- **B**: la base; elevada a la potencia W, es tan pequeña que no cuenta.\n- **M**: la mayoría pedida cuando vota todo el mundo.\n- **G**: el gradiente: cuanto mayor es, más alta es la proporción pedida cuando vota poca gente.\n\nSus valores están en «Parámetros por defecto» y «Niveles de inercia»."
|
||||
}
|
||||
},
|
||||
"F1.2": {
|
||||
"label": "1.0.0",
|
||||
"title": {
|
||||
"fr": "Paramètres par défaut",
|
||||
"en": "Default parameters",
|
||||
"es": "Parámetros por defecto"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la façon dont le vote se déroule.\n\n**Un vote permanent, sans durée**\n\n- Chaque version se vote à tout moment, sans échéance. Chacun peut changer ou retirer son vote : le dernier fait foi.\n- Une proposition entre en vigueur dès qu'elle atteint son seuil. S'il y a déjà un texte en vigueur, il lui faut en plus une marge (voix pour moins voix exigées) plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir.\n- Si plusieurs propositions y parviennent, celle qui a la marge la plus large entre en vigueur.\n\n**Les lettres**\n\n- **W** : le nombre de membres de la toile de confiance Ğ1, relu sur la chaîne toutes les 6 heures.\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q**, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au cran 5. L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote. Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un texte.\n- **B = 0,1** : dans la toile de confiance, `B^W` est si petit qu'il ne compte pas.\n- **M** et **G** : donnés par le cran d'inertie de l'article (voir « Niveaux d'inertie »). G n'est pas fixé à la main : il se déduit de la participation habituelle du groupe.\n\n**Par défaut**\n\n- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude. Tout nouvel article commence à ce cran.\n- Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, tous les autres articles au cran 3.",
|
||||
"en": "The values of the letters of the formula (see “WoT threshold formula”) and how the vote runs.\n\n**A permanent vote, with no duration**\n\n- Every version can be voted on at any time, with no deadline. Anyone can change or withdraw their vote: the last one is the one that counts.\n- A proposal comes into force as soon as it reaches its threshold. If a text is already in force, it also needs a wider margin (votes in favour minus votes required) than that text has. The replaced text becomes a proposal again, with its votes: it can come back.\n- If several proposals get there, the one with the widest margin comes into force.\n\n**The letters**\n\n- **W**: the number of members of the Ğ1 web of trust, read again from the chain every 6 hours.\n- **T**: the votes cast on the version, for and against.\n- **Q**, the quorum: below this number of voters, those who are missing count as votes against. Each article takes the one of its level: 12, 12, 50, 50 and 50 in Monnaie libre, from level 1 to level 5. The author of the first version of an article, an administrator for the starting texts, can set another one as long as the article keeps no trace of any vote. In Monnaie libre, never less than 2: a single vote never passes a text.\n- **B = 0.1**: in the web of trust, `B^W` is so small that it does not count.\n- **M** and **G**: given by the article's level of inertia (see “Inertia levels”). G is not set by hand: it follows from the group's usual turnout.\n\n**By default**\n\n- The default level is 3 (medium): M = 55%, a quorum of 50 in Monnaie libre, and the group asks for 75% of votes in favour when it votes as usual. Every new article starts at this level.\n- At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, all other articles at level 3.",
|
||||
"es": "Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y cómo se desarrolla la votación.\n\n**Una votación permanente, sin duración**\n\n- Cada versión se vota en cualquier momento, sin plazo. Cada cual puede cambiar o retirar su voto: el último es el que cuenta.\n- Una propuesta entra en vigor en cuanto alcanza su umbral. Si ya hay un texto en vigor, necesita además un margen (votos a favor menos votos exigidos) más amplio que el de ese texto. El texto sustituido vuelve a ser una propuesta, con sus votos: puede volver.\n- Si varias propuestas lo consiguen, entra en vigor la que tiene el margen más amplio.\n\n**Las letras**\n\n- **W**: el número de miembros de la red de confianza Ğ1, leído de nuevo en la cadena cada 6 horas.\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**, el quórum: por debajo de este número de votantes, los que faltan cuentan como «no». Cada artículo toma el de su nivel: 12, 12, 50, 50 y 50 en Monnaie libre, del nivel 1 al nivel 5. El autor de la primera versión de un artículo, un administrador para los textos de partida, puede fijar otro mientras el artículo no guarde rastro de ningún voto. En Monnaie libre, nunca menos de 2: un solo voto nunca hace pasar un texto.\n- **B = 0,1**: en la red de confianza, `B^W` es tan pequeño que no cuenta.\n- **M** y **G**: los da el nivel de inercia del artículo (ver «Niveles de inercia»). G no se fija a mano: se deduce de la participación habitual del grupo.\n\n**Por defecto**\n\n- El nivel por defecto es el 3 (media): M = 55 %, un quórum de 50 en Monnaie libre, y el grupo pide un 75 % de votos a favor cuando vota como de costumbre. Todo artículo nuevo empieza en este nivel.\n- Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3."
|
||||
}
|
||||
},
|
||||
"N1.1": {
|
||||
"label": "1.0.0",
|
||||
"title": {
|
||||
"fr": "Niveaux d'inertie",
|
||||
"en": "Inertia levels",
|
||||
"es": "Niveles de inercia"
|
||||
},
|
||||
"body": {
|
||||
"fr": "À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). Chaque cran a aussi son quorum, qui n'est pas un nombre minimal de votants : sous le quorum, les votants qui manquent comptent comme « non ». La part dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà du quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.\n\n`Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum s'il en manque.\n\n**Les cinq crans** : le quorum dans Monnaie libre, la part exigée quand le groupe vote comme d'habitude, puis quand tout le monde vote, et ce qui est à ce cran au départ.\n\n- **Cran 1 · très basse** : quorum 12 ; 60 % comme d'habitude, 50 % quand tout le monde vote. Aucun article au départ.\n- **Cran 2 · basse** : quorum 12 ; 67 % comme d'habitude, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.\n- **Cran 3 · moyenne** : quorum 50 ; 75 % comme d'habitude, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.\n- **Cran 4 · haute** : quorum 50 ; 83 % comme d'habitude, 60 % quand tout le monde vote. Le titre du document.\n- **Cran 5 · très haute** : quorum 50 ; 90 % comme d'habitude, 66 % quand tout le monde vote. Aucun article au départ.\n\n**La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. Tant que le groupe n'a pas voté sur 5 propositions, elle vaut deux fois le quorum de référence du document : 100 votants dans Monnaie libre. Elle ne dépend pas du quorum d'un article. Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour.\n\n**G** se calcule pour que la part exigée, à la participation habituelle, soit exactement celle du cran : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est la part exigée comme d'habitude et p̃ la participation habituelle divisée par W.\n\nExemple : avec 7 224 membres et une participation habituelle de 120 votants, quand 120 personnes votent, il faut 72, 81, 90, 100 ou 108 voix pour, du cran 1 au cran 5 ; au cran 3, 75 % des 120 votants, soit 90. Sous le quorum, les absents comptent comme « non » : au cran 2 (quorum 12), 9 voix pour passent avec 3 « contre » au plus ; au cran 3 (quorum 50), il faut 39 voix pour, avec 11 « contre » au plus.",
|
||||
"en": "At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). Each level also has its quorum, which is not a minimum number of voters: below the quorum, the voters who are missing count as votes against. The share depends on the level and on the number of voters, compared with the group's usual turnout: beyond the quorum, the more voters, the lower it gets, down to the majority M when everyone votes.\n\n`Share asked = M + (1 − M) × (1 − (T'/W)^G)`, where T' is the number of voters, or the quorum if some are missing.\n\n**The five levels**: the quorum in Monnaie libre, the share asked when the group votes as usual, then when everyone votes, and what is at that level at the start.\n\n- **Level 1 · very low**: quorum 12; 60% as usual, 50% when everyone votes. No article at the start.\n- **Level 2 · low**: quorum 12; 67% as usual, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.\n- **Level 3 · medium**: quorum 50; 75% as usual, 55% when everyone votes. The default level: all other articles, and every new article.\n- **Level 4 · high**: quorum 50; 83% as usual, 60% when everyone votes. The title of the document.\n- **Level 5 · very high**: quorum 50; 90% as usual, 66% when everyone votes. No article at the start.\n\n**The usual turnout** is the median number of voters on the group's proposals voted on over the past year. Until the group has voted on 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It stays between 1% and 50% of the electorate, and is measured once a day.\n\n**G** is computed so that the share asked, at the usual turnout, is exactly that of the level: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is the share asked as usual and p̃ the usual turnout divided by W.\n\nExample: with 7,224 members and a usual turnout of 120 voters, when 120 people vote, it takes 72, 81, 90, 100 or 108 votes in favour, from level 1 to level 5; at level 3, 75% of the 120 voters, i.e. 90. Below the quorum, those missing count as votes against: at level 2 (quorum 12), 9 votes in favour pass with 3 votes against at most; at level 3 (quorum 50), it takes 39 votes in favour, with 11 votes against at most.",
|
||||
"es": "En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). Cada nivel tiene además su quórum, que no es un número mínimo de votantes: por debajo del quórum, los votantes que faltan cuentan como «no». La proporción depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá del quórum, cuantos más votantes, más baja, hasta la mayoría M cuando vota todo el mundo.\n\n`Proporción exigida = M + (1 − M) × (1 − (T'/W)^G)`, donde T' es el número de votantes, o el quórum si faltan.\n\n**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida cuando el grupo vota como de costumbre, luego cuando vota todo el mundo, y lo que está en ese nivel al inicio.\n\n- **Nivel 1 · muy baja**: quórum 12; 60 % como de costumbre, 50 % cuando vota todo el mundo. Ningún artículo al inicio.\n- **Nivel 2 · baja**: quórum 12; 67 % como de costumbre, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.\n- **Nivel 3 · media**: quórum 50; 75 % como de costumbre, 55 % cuando vota todo el mundo. El nivel por defecto: todos los demás artículos, y todo artículo nuevo.\n- **Nivel 4 · alta**: quórum 50; 83 % como de costumbre, 60 % cuando vota todo el mundo. El título del documento.\n- **Nivel 5 · muy alta**: quórum 50; 90 % como de costumbre, 66 % cuando vota todo el mundo. Ningún artículo al inicio.\n\n**La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Hasta que el grupo haya votado 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se mantiene entre el 1 % y el 50 % del electorado, y se mide una vez al día.\n\n**G** se calcula para que la proporción exigida, con la participación habitual, sea exactamente la del nivel: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es la proporción exigida como de costumbre y p̃ la participación habitual dividida por W.\n\nEjemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 72, 81, 90, 100 o 108 votos a favor, del nivel 1 al nivel 5; en el nivel 3, el 75 % de los 120 votantes, es decir 90. Por debajo del quórum, los que faltan cuentan como «no»: en el nivel 2 (quórum 12), 9 votos a favor pasan con 3 en contra como mucho; en el nivel 3 (quórum 50), hacen falta 39 votos a favor, con 11 en contra como mucho."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ async fn the_five_levels_and_the_scale_are_served() {
|
||||
let (mut c, _) = Client::new().await;
|
||||
c.login("Chiara").await;
|
||||
let doc = c.doc("monnaie-libre").await;
|
||||
// 3 everywhere, low for the three seed proposals (ADR 0011).
|
||||
// 3 everywhere, low for the four seed proposals (ADR 0011; E12 since lot T4).
|
||||
let other: Vec<&str> = doc["units"]
|
||||
.as_array()
|
||||
.expect("units")
|
||||
@@ -120,8 +120,8 @@ async fn the_five_levels_and_the_scale_are_served() {
|
||||
.filter(|u| u["inertia"] != 3)
|
||||
.map(|u| u["code"].as_str().expect("code"))
|
||||
.collect();
|
||||
assert_eq!(other, ["I1", "E1", "E2"]);
|
||||
assert!(["I1", "E1", "E2"].iter().all(|c| unit(&doc, c)["inertia"] == LOW));
|
||||
assert_eq!(other, ["I1", "E1", "E12", "E2"]);
|
||||
assert!(["I1", "E1", "E12", "E2"].iter().all(|c| unit(&doc, c)["inertia"] == LOW));
|
||||
|
||||
// No vote yet: the reference, 2C/W = 100 voters out of 5745.
|
||||
let p = &doc["doc"]["participation"];
|
||||
|
||||
@@ -146,8 +146,9 @@ async fn a_new_base_has_its_title_and_the_order_of_each_section() {
|
||||
c.login("Chiara").await;
|
||||
for group in ["monnaie-libre", "essai"] {
|
||||
let doc = c.doc(group).await;
|
||||
// The articles stay the articles: 33 of them, none of the modules.
|
||||
assert_eq!(doc["units"].as_array().expect("units").len(), 33, "{group}");
|
||||
// The articles stay the articles: 27 of them, none of the modules (since lot T4, the
|
||||
// rules of operation are out of the document, ADR 0014).
|
||||
assert_eq!(doc["units"].as_array().expect("units").len(), 27, "{group}");
|
||||
let title = &doc["modules"]["title"];
|
||||
assert_eq!(
|
||||
(title["kind"].as_str(), title["code"].as_str(), title["inertia"].as_u64()),
|
||||
@@ -163,27 +164,15 @@ async fn a_new_base_has_its_title_and_the_order_of_each_section() {
|
||||
// One order per section, in the order of the document, each at level 2.
|
||||
let orders = doc["modules"]["orders"].as_array().expect("orders");
|
||||
let sections: Vec<&str> = orders.iter().filter_map(|o| o["section"].as_str()).collect();
|
||||
assert_eq!(
|
||||
sections,
|
||||
[
|
||||
"introduction",
|
||||
"fondamental",
|
||||
"technique",
|
||||
"conclusion",
|
||||
"annexe",
|
||||
"formule",
|
||||
"inertie",
|
||||
"ordonnancement"
|
||||
]
|
||||
);
|
||||
assert_eq!(sections, ["introduction", "fondamental", "technique", "conclusion", "annexe"]);
|
||||
assert!(orders.iter().all(|o| o["inertia"] == 2 && o["inForce"]["seedMinimal"] == true));
|
||||
// Its version in force is the order the section reads in now.
|
||||
let fondamental = order_of(&doc, "fondamental");
|
||||
assert_eq!(codes(&doc, &fondamental["members"]), ["T1", "E1", "E2"]);
|
||||
assert_eq!(codes(&doc, &fondamental["members"]), ["T1", "E1", "E12", "E2"]);
|
||||
assert_eq!(fondamental["inForce"]["order"]["ids"], fondamental["members"]);
|
||||
assert_eq!(fondamental["moot"], false);
|
||||
// « Ordonnancement » holds one article: nothing to decide.
|
||||
assert_eq!(order_of(&doc, "ordonnancement")["moot"], true);
|
||||
// Every section holds two articles or more: each order has something to decide
|
||||
// (one of a single article: `an_order_of_one_article_has_nothing_to_decide`).
|
||||
assert!(orders.iter().all(|o| o["moot"] == false), "{group}");
|
||||
}
|
||||
// Born with them: the addition says so, once per document.
|
||||
let rows: Vec<(String, String)> = sqlx::query_as(
|
||||
@@ -197,11 +186,35 @@ async fn a_new_base_has_its_title_and_the_order_of_each_section() {
|
||||
let doc = c.doc("monnaie-libre").await;
|
||||
let id = doc["doc"]["id"].as_str().expect("id");
|
||||
let all = c.ok("GET", &format!("/api/docs/{id}/preview?mode=all"), None).await;
|
||||
assert_eq!(all["units"].as_array().expect("units").len(), 33);
|
||||
// The list of the group: the articles in force — none, I1, E1 and E2 are seed proposals
|
||||
// (ADR 0011), three more open versions —; no module has an open proposal yet.
|
||||
assert_eq!(all["units"].as_array().expect("units").len(), 27);
|
||||
// The list of the group: the articles in force — none, I1, E1, E12 and E2 are seed
|
||||
// proposals (ADR 0011) —, one open version per article and two more (E3, E7); no module
|
||||
// has an open proposal yet.
|
||||
let g = c.ok("GET", "/api/groups/monnaie-libre", None).await;
|
||||
assert_eq!((g["docs"][0]["inForce"].as_u64(), g["docs"][0]["open"].as_u64()), (Some(0), Some(35)));
|
||||
assert_eq!((g["docs"][0]["inForce"].as_u64(), g["docs"][0]["open"].as_u64()), (Some(0), Some(29)));
|
||||
}
|
||||
|
||||
/// The seed of M8 (tests/fixtures/seed-m8): « Ordonnancement » held one article, O1. Lot T4
|
||||
/// takes that section out of the document (ADR 0014); a base born on that seed still has it.
|
||||
fn m8_config() -> Config {
|
||||
let mut cfg = Config::for_tests();
|
||||
cfg.seed_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-m8");
|
||||
cfg
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_order_of_one_article_has_nothing_to_decide() {
|
||||
let db = open_db("sqlite::memory:").await.expect("db");
|
||||
let state = AppState::new(m8_config(), db).await.expect("state");
|
||||
seed::ensure(&state).await.expect("seed");
|
||||
let mut c = Client::of(&state);
|
||||
c.login("Chiara").await;
|
||||
for group in ["monnaie-libre", "essai"] {
|
||||
let doc = c.doc(group).await;
|
||||
assert_eq!(codes(&doc, &order_of(&doc, "ordonnancement")["members"]), ["O1"], "{group}");
|
||||
assert_eq!(order_of(&doc, "ordonnancement")["moot"], true, "{group}");
|
||||
assert_eq!(order_of(&doc, "conclusion")["moot"], false, "{group}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -251,7 +264,7 @@ async fn an_order_is_proposed_with_the_articles_of_its_section_then_voted_into_f
|
||||
assert_eq!((back["status"].as_str(), back["origin"].as_str()), (Some("open"), Some("seed")));
|
||||
assert_eq!(codes(&doc, &back["order"]["ids"]), ["K1", "K2"]);
|
||||
// Nothing else moved: the other sections read as before, the texts are the same.
|
||||
assert_eq!(read_order(&doc, "fondamental"), ["T1", "E1", "E2"]);
|
||||
assert_eq!(read_order(&doc, "fondamental"), ["T1", "E1", "E12", "E2"]);
|
||||
assert_eq!(read_order(&doc, "technique")[0], "T2");
|
||||
}
|
||||
|
||||
@@ -259,12 +272,12 @@ async fn an_order_is_proposed_with_the_articles_of_its_section_then_voted_into_f
|
||||
async fn articles_that_enter_or_leave_a_section_complete_an_order_under_vote() {
|
||||
let (mut yvv, mut chiara, state) = two().await;
|
||||
// The order stays under vote with one voice.
|
||||
two_voices(&state, "ordre.inertie").await;
|
||||
two_voices(&state, "ordre.conclusion").await;
|
||||
let doc = yvv.doc("essai").await;
|
||||
let module = order_of(&doc, "inertie")["id"].as_str().expect("id").to_owned();
|
||||
let module = order_of(&doc, "conclusion")["id"].as_str().expect("id").to_owned();
|
||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
||||
let (n1, n11) = (id_of(&doc, "N1"), id_of(&doc, "N1.1"));
|
||||
let p = yvv.ok("POST", &format!("/api/units/{module}/order"), Some(json!({ "order": [n11, n1] }))).await["id"]
|
||||
let (k1, k2) = (id_of(&doc, "K1"), id_of(&doc, "K2"));
|
||||
let p = yvv.ok("POST", &format!("/api/units/{module}/order"), Some(json!({ "order": [k2, k1] }))).await["id"]
|
||||
.as_str()
|
||||
.expect("id")
|
||||
.to_owned();
|
||||
@@ -286,20 +299,22 @@ async fn articles_that_enter_or_leave_a_section_complete_an_order_under_vote() {
|
||||
.ok(
|
||||
"POST",
|
||||
&format!("/api/docs/{doc_id}/proposals"),
|
||||
Some(json!({ "section": "inertie", "title": "Inertie des modules", "body": "Le titre du document est à 4.", "submit": true })),
|
||||
Some(
|
||||
json!({ "section": "conclusion", "title": "Rappel", "body": "Un rappel pour finir.", "submit": true }),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let doc = yvv.doc("essai").await;
|
||||
let fresh = unit(&doc, "N1")["code"]
|
||||
let fresh = unit(&doc, "K1")["code"]
|
||||
.as_str()
|
||||
.map(|_| ())
|
||||
.and_then(|()| doc["units"].as_array().expect("u").iter().find(|u| u["id"] == created["unitId"]).cloned());
|
||||
let fresh = fresh.expect("the new article");
|
||||
let new_code = fresh["code"].as_str().expect("code").to_owned();
|
||||
assert_eq!(read_order(&doc, "inertie"), ["N1", "N1.1", new_code.as_str()], "a new article goes at the end");
|
||||
let module_now = order_of(&doc, "inertie");
|
||||
assert_eq!(read_order(&doc, "conclusion"), ["K1", "K2", new_code.as_str()], "a new article goes at the end");
|
||||
let module_now = order_of(&doc, "conclusion");
|
||||
let open = module_now["proposals"].as_array().expect("p").iter().find(|x| x["id"] == p.as_str()).expect("p");
|
||||
assert_eq!(codes(&doc, &open["order"]["ids"]), ["N1.1", "N1", new_code.as_str()]);
|
||||
assert_eq!(codes(&doc, &open["order"]["ids"]), ["K2", "K1", new_code.as_str()]);
|
||||
assert_eq!(codes(&doc, &open["order"]["added"]), [new_code.as_str()]);
|
||||
// Its vote stays: completing an order never asks the voters again.
|
||||
assert_eq!((open["tally"]["for"].as_u64(), open["myVote"].as_str()), (Some(1), Some("for")));
|
||||
@@ -307,47 +322,47 @@ async fn articles_that_enter_or_leave_a_section_complete_an_order_under_vote() {
|
||||
let added = created["id"].as_str().expect("id");
|
||||
chiara.ok("POST", &format!("/api/proposals/{added}/withdraw"), None).await;
|
||||
let doc = yvv.doc("essai").await;
|
||||
let open = order_of(&doc, "inertie")["proposals"]
|
||||
let open = order_of(&doc, "conclusion")["proposals"]
|
||||
.as_array()
|
||||
.expect("p")
|
||||
.iter()
|
||||
.find(|x| x["id"] == p.as_str())
|
||||
.cloned()
|
||||
.expect("p");
|
||||
assert_eq!(codes(&doc, &open["order"]["ids"]), ["N1.1", "N1"]);
|
||||
assert_eq!(codes(&doc, &open["order"]["ids"]), ["K2", "K1"]);
|
||||
assert!(open["order"]["added"].as_array().expect("a").is_empty());
|
||||
// An order proposed with three articles, one of which then leaves: dropped from it.
|
||||
let created = chiara
|
||||
.ok(
|
||||
"POST",
|
||||
&format!("/api/docs/{doc_id}/proposals"),
|
||||
Some(json!({ "section": "inertie", "title": "Autre", "body": "Un article de passage dans la section.", "submit": true })),
|
||||
Some(json!({ "section": "conclusion", "title": "Autre", "body": "Un article de passage dans la section.", "submit": true })),
|
||||
)
|
||||
.await;
|
||||
let other = created["unitId"].as_str().expect("unit").to_owned();
|
||||
let three = yvv.ok("POST", &format!("/api/units/{module}/order"), Some(json!({ "order": [other, n11, n1] }))).await
|
||||
let three = yvv.ok("POST", &format!("/api/units/{module}/order"), Some(json!({ "order": [other, k2, k1] }))).await
|
||||
["id"]
|
||||
.as_str()
|
||||
.expect("id")
|
||||
.to_owned();
|
||||
chiara.ok("POST", &format!("/api/proposals/{}/withdraw", created["id"].as_str().expect("id")), None).await;
|
||||
let doc = yvv.doc("essai").await;
|
||||
let o = order_of(&doc, "inertie")["proposals"]
|
||||
let o = order_of(&doc, "conclusion")["proposals"]
|
||||
.as_array()
|
||||
.expect("p")
|
||||
.iter()
|
||||
.find(|x| x["id"] == three.as_str())
|
||||
.cloned()
|
||||
.expect("o");
|
||||
assert_eq!(codes(&doc, &o["order"]["ids"]), ["N1.1", "N1"]);
|
||||
assert_eq!(codes(&doc, &o["order"]["ids"]), ["K2", "K1"]);
|
||||
assert_eq!(o["order"]["dropped"], json!([other]));
|
||||
// Completed, it reads like the first one: the second voice brings that order in.
|
||||
let v = chiara.vote(&p, "for").await;
|
||||
assert_eq!(v["entered"][0]["kind"], "order");
|
||||
let doc = yvv.doc("essai").await;
|
||||
assert_eq!(read_order(&doc, "inertie"), ["N1.1", "N1"]);
|
||||
assert_eq!(read_order(&doc, "conclusion"), ["K2", "K1"]);
|
||||
// Now the order in force: the other one changes nothing more and cannot enter.
|
||||
let o = order_of(&doc, "inertie")["proposals"]
|
||||
let o = order_of(&doc, "conclusion")["proposals"]
|
||||
.as_array()
|
||||
.expect("p")
|
||||
.iter()
|
||||
@@ -503,10 +518,10 @@ async fn a_new_article_goes_at_the_end_of_the_section_asked_for() {
|
||||
.to_owned()
|
||||
};
|
||||
let (ca, cb) = (code(&a), code(&b));
|
||||
assert_eq!(read_order(&doc, "fondamental"), ["T1", "E1", "E2", ca.as_str()]);
|
||||
assert_eq!(read_order(&doc, "fondamental"), ["T1", "E1", "E12", "E2", ca.as_str()]);
|
||||
assert_eq!(read_order(&doc, "introduction"), ["I1", "I2", cb.as_str()]);
|
||||
// It joins the order of its section, at its end.
|
||||
assert_eq!(codes(&doc, &order_of(&doc, "fondamental")["members"]), ["T1", "E1", "E2", ca.as_str()]);
|
||||
assert_eq!(codes(&doc, &order_of(&doc, "fondamental")["members"]), ["T1", "E1", "E12", "E2", ca.as_str()]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -169,11 +169,11 @@ async fn decisions_one_line_per_doc_with_the_modules_left_to_vote() {
|
||||
assert_eq!(docs.len(), 1);
|
||||
assert_eq!(docs[0]["slug"], "acte-engagement-certification");
|
||||
assert_eq!(docs[0]["title"]["fr"], "Acte d'engagement Certification");
|
||||
// 3 seed proposals (ADR 0011) + 30 open articles, the two variants counting once; the title
|
||||
// of the document and the order of 7 sections (« ordonnancement » holds one article: nothing
|
||||
// to decide).
|
||||
assert_eq!(docs[0]["toVote"], 41);
|
||||
assert_eq!(docs[0]["withoutMyVote"], 41);
|
||||
// 4 seed proposals (ADR 0011) + 23 open articles, the two variants counting once; the title
|
||||
// of the document and the order of its 5 sections (since lot T4, the rules of operation are
|
||||
// out of the document, ADR 0014).
|
||||
assert_eq!(docs[0]["toVote"], 33);
|
||||
assert_eq!(docs[0]["withoutMyVote"], 33);
|
||||
|
||||
let doc = the_doc(&mut c, "essai").await;
|
||||
// Votes that adopt nothing: the quorum 2 (ADR 0013), one voice of two.
|
||||
@@ -183,9 +183,9 @@ async fn decisions_one_line_per_doc_with_the_modules_left_to_vote() {
|
||||
c.vote(&version(&doc, "E3", "1"), "against").await;
|
||||
c.vote(&version(&doc, "I1", "0"), "for").await;
|
||||
let d = decisions(&mut c, "essai").await;
|
||||
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(41), Some(39)));
|
||||
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(33), Some(31)));
|
||||
// Someone else has not voted yet.
|
||||
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 41);
|
||||
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 33);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -201,7 +201,7 @@ async fn an_adopted_module_leaves_the_count_and_a_vote_to_reconfirm_does_not_cou
|
||||
let v = yvv.vote(&e3, "for").await;
|
||||
assert_eq!(v["entered"][0]["code"], "E3");
|
||||
let d = decisions(&mut yvv, "essai").await;
|
||||
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(40), Some(40)));
|
||||
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(32), Some(32)));
|
||||
|
||||
// A new article by Chiara: one more module to vote.
|
||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
||||
@@ -216,9 +216,9 @@ async fn an_adopted_module_leaves_the_count_and_a_vote_to_reconfirm_does_not_cou
|
||||
.await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
let added = v["id"].as_str().expect("id").to_owned();
|
||||
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["toVote"], 41);
|
||||
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["toVote"], 33);
|
||||
yvv.vote(&added, "against").await;
|
||||
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 40);
|
||||
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 32);
|
||||
// Chiara changes its meaning: Yvv's vote is to reconfirm, it no longer counts as his vote.
|
||||
let (s, v) = chiara
|
||||
.call(
|
||||
@@ -231,7 +231,7 @@ async fn an_adopted_module_leaves_the_count_and_a_vote_to_reconfirm_does_not_cou
|
||||
.await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
assert_eq!(v["edit"], "substantive");
|
||||
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 41);
|
||||
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 33);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -251,7 +251,7 @@ async fn a_version_held_by_an_objection_is_not_to_vote_while_it_is_held() {
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
// Nobody can vote on it while it is held: one module fewer to vote, for everyone.
|
||||
let d = decisions(&mut yvv, "essai").await;
|
||||
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(40), Some(40)));
|
||||
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(32), Some(32)));
|
||||
// The list of the group still counts it among the proposals in progress: it is open.
|
||||
let g = yvv.get("/api/groups/essai").await;
|
||||
let open = doc["units"]
|
||||
@@ -271,10 +271,10 @@ async fn while_the_chain_is_closed_a_guest_votes_in_simulation_and_has_a_count_o
|
||||
// No vote account: Monnaie libre votes in simulation, for whoever opens it (ADR 0012).
|
||||
let d = decisions(&mut guest, "monnaie-libre").await;
|
||||
assert_eq!(d["canVote"], true);
|
||||
assert_eq!(d["docs"][0]["toVote"], 41);
|
||||
assert_eq!(d["docs"][0]["withoutMyVote"], 41);
|
||||
assert_eq!(d["docs"][0]["toVote"], 33);
|
||||
assert_eq!(d["docs"][0]["withoutMyVote"], 33);
|
||||
// In the test group, the same person votes.
|
||||
let d = decisions(&mut guest, "essai").await;
|
||||
assert_eq!(d["canVote"], true);
|
||||
assert_eq!(d["docs"][0]["withoutMyVote"], 41);
|
||||
assert_eq!(d["docs"][0]["withoutMyVote"], 33);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,11 @@ fn numbers(levels: &Value, key: &str) -> Vec<u64> {
|
||||
levels.as_array().expect("levels").iter().map(|l| l[key].as_u64().expect("number")).collect()
|
||||
}
|
||||
|
||||
/// G of each level, as served.
|
||||
fn gradients(doc: &Value) -> Vec<Value> {
|
||||
doc["levels"].as_array().expect("levels").iter().map(|l| l["gradient"].clone()).collect()
|
||||
}
|
||||
|
||||
/// (quorum, quorumDefault, canSetQuorum) of a unit.
|
||||
fn quorum_of(u: &Value) -> (Option<u64>, Option<u64>, Option<bool>) {
|
||||
(u["quorum"].as_u64(), u["quorumDefault"].as_u64(), u["canSetQuorum"].as_bool())
|
||||
@@ -174,6 +179,8 @@ async fn the_author_of_an_article_sets_its_quorum_before_the_first_vote() {
|
||||
.await;
|
||||
let (u, text) =
|
||||
(created["unitId"].as_str().expect("unit").to_owned(), created["id"].as_str().expect("id").to_owned());
|
||||
// « Tout nouvel article commence à ce cran » (F1.2, « Par défaut »): the default level, 3.
|
||||
assert_eq!(unit_by_id(&chiara.doc("essai").await, &u)["inertia"], 3);
|
||||
// Its author may, another member may not; the test group: its level's quorum, 1.
|
||||
assert_eq!(quorum_of(unit_by_id(&chiara.doc("essai").await, &u)), (None, Some(1), Some(true)));
|
||||
assert_eq!(quorum_of(unit_by_id(&luna.doc("essai").await, &u)), (None, Some(1), Some(false)));
|
||||
@@ -270,6 +277,7 @@ async fn in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pen
|
||||
chiara.login("Chiara", false).await;
|
||||
let doc = admin.doc("monnaie-libre").await;
|
||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
||||
let usual = (doc["doc"]["participation"].clone(), gradients(&doc));
|
||||
let i1 = unit(&doc, "I1");
|
||||
let (u, p) = (i1["id"].as_str().expect("u").to_owned(), i1["proposals"][0]["id"].as_str().expect("p").to_owned());
|
||||
// A text of the seed has no author: an administrator answers for it.
|
||||
@@ -287,6 +295,10 @@ async fn in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pen
|
||||
}
|
||||
let doc = admin.doc("monnaie-libre").await;
|
||||
assert_eq!(unit(&doc, "I1")["proposals"][0]["evaluation"]["quorum"], 20);
|
||||
// « Elle ne dépend pas du quorum d'un article » (N1.1, lot T3c): the participation habituelle,
|
||||
// and G of each level, stay as they were (the reference rests on the document's, 2 × 50 / W).
|
||||
assert_eq!((doc["doc"]["participation"].clone(), gradients(&doc)), usual);
|
||||
assert!((doc["doc"]["participation"]["ratio"].as_f64().expect("p̃") - 100.0 / W as f64).abs() < 1e-12);
|
||||
// A refused ballot, an expired one, are no vote.
|
||||
ballot(&state, &doc_id, &p, "refused", 1).await;
|
||||
ballot(&state, &doc_id, &p, "expired", 2).await;
|
||||
@@ -317,6 +329,14 @@ fn production_config() -> Config {
|
||||
cfg
|
||||
}
|
||||
|
||||
/// The seed of M6 to M8 (de1b7eb), which wrote the quorums and put I1 forward: since lot T4 the
|
||||
/// seed of today gives I1 another text, and a base of 02/11 keeps it in force under it.
|
||||
fn m8_config() -> Config {
|
||||
let mut cfg = Config::for_tests();
|
||||
cfg.seed_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-m8");
|
||||
cfg
|
||||
}
|
||||
|
||||
async fn restart(before: &AppState, cfg: Config) -> AppState {
|
||||
let state = AppState::new(cfg, before.db.clone()).await.expect("state");
|
||||
seed::ensure(&state).await.expect("seed");
|
||||
@@ -362,8 +382,8 @@ async fn the_quorum_amendment_writes_the_quorums_by_level_once() {
|
||||
assert_eq!(c.doc("monnaie-libre").await["doc"]["quorums"], json!([50, 50, 50, 50, 50]));
|
||||
assert_eq!(c.doc("essai").await["doc"]["quorums"], json!([1, 1, 1, 1, 1]));
|
||||
|
||||
// Started with today's seed: written, where none was.
|
||||
let state = restart(&born, Config::for_tests()).await;
|
||||
// Started with the seed of M6 (and of M8): written, where none was.
|
||||
let state = restart(&born, m8_config()).await;
|
||||
let first = decisions(&state).await;
|
||||
assert_eq!(
|
||||
first.iter().map(|(g, f, t, o, r)| row(g, f.as_deref(), t, o, r.as_deref())).collect::<Vec<_>>(),
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
//! base is born with them open; an existing base gets them through a guarded
|
||||
//! amendment, only where nobody has touched the text. Monnaie libre publishes no
|
||||
//! document without any article in force: the queue waits for the first text.
|
||||
//!
|
||||
//! Lot T4 (2026-11-03): a fourth, E12 « Responsabilité civique », and Yvv's texts for the three
|
||||
//! (`tests/amorce.rs`). A new base is born with the four; the bases made as production are
|
||||
//! started here with the seed of M8, the last that gave the three their texts of before.
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
@@ -107,7 +111,7 @@ async fn fresh_with(cfg: Config) -> AppState {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force() {
|
||||
async fn a_new_base_is_born_with_four_seed_proposals_and_no_article_in_force() {
|
||||
let state = fresh().await;
|
||||
let mut c = Client::of(&state);
|
||||
c.login("Chiara", false).await;
|
||||
@@ -121,7 +125,8 @@ async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force()
|
||||
.filter_map(|u| u["code"].as_str())
|
||||
.collect();
|
||||
assert!(in_force.is_empty(), "{group}: no article in force, {in_force:?}");
|
||||
for code in CODES {
|
||||
for code in CODES.iter().chain(&["E12"]) {
|
||||
let code = *code;
|
||||
let u = unit(&doc, code);
|
||||
let p = &u["proposals"][0];
|
||||
assert_eq!(
|
||||
@@ -135,19 +140,20 @@ async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force()
|
||||
assert!(p["translations"]["en"]["title"].is_string(), "{group} {code}: translated");
|
||||
}
|
||||
assert_eq!(unit(&doc, "I1")["proposals"][0]["title"], "Raison d'être");
|
||||
// Only those three; the title and the orders stay seeds a minima, in force.
|
||||
// Only those four; the title and the orders stay seeds a minima, in force.
|
||||
let marked: Vec<&Value> = versions(&doc).into_iter().filter(|v| v["seedProposal"] == true).collect();
|
||||
assert_eq!(marked.len(), 3, "{group}");
|
||||
assert_eq!(marked.len(), 4, "{group}");
|
||||
let title = &doc["modules"]["title"]["inForce"];
|
||||
assert_eq!((title["seedMinimal"].as_bool(), title["seedProposal"].as_bool()), (Some(true), Some(false)));
|
||||
assert!(
|
||||
doc["modules"]["orders"].as_array().expect("orders").iter().all(|o| o["inForce"]["seedMinimal"] == true)
|
||||
);
|
||||
// The counts: no article in force, three more open versions; still 41 modules to vote.
|
||||
// The counts: no article in force, 29 open versions (27 articles, E3 and E7 two each);
|
||||
// 33 modules to vote (27 articles, the title, the order of 5 sections; lot T4).
|
||||
let g = c.get(&format!("/api/groups/{group}")).await;
|
||||
assert_eq!((g["docs"][0]["inForce"].as_u64(), g["docs"][0]["open"].as_u64()), (Some(0), Some(35)), "{group}");
|
||||
assert_eq!((g["docs"][0]["inForce"].as_u64(), g["docs"][0]["open"].as_u64()), (Some(0), Some(29)), "{group}");
|
||||
let d = c.get(&format!("/api/groups/{group}/decisions")).await;
|
||||
assert_eq!(d["docs"][0]["toVote"], 41, "{group}");
|
||||
assert_eq!(d["docs"][0]["toVote"], 33, "{group}");
|
||||
// The preview of the text in force is empty; « if everything passes » shows them.
|
||||
let id = doc["doc"]["id"].as_str().expect("id");
|
||||
let p = c.get(&format!("/api/docs/{id}/preview")).await;
|
||||
@@ -155,7 +161,8 @@ async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force()
|
||||
let all = c.get(&format!("/api/docs/{id}/preview?mode=all")).await;
|
||||
assert!(all["units"].as_array().expect("units").iter().any(|u| u["title"] == "Raison d'être"));
|
||||
}
|
||||
// Born with them: nothing to amend, and it is said once per article and group.
|
||||
// Born with them: the seed proposal amendments, superseded since lot T4 (the seed puts
|
||||
// forward Yvv's texts, not theirs), decide nothing; the texts of lot T4 are said present.
|
||||
let decided: Vec<(String, String, String)> = sqlx::query_as(
|
||||
"SELECT d.group_slug, s.unit_code, s.outcome FROM seed_proposal_amendments s
|
||||
JOIN documents d ON d.id = s.doc_id ORDER BY d.group_slug, s.unit_code",
|
||||
@@ -163,14 +170,13 @@ async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force()
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.expect("decisions");
|
||||
assert_eq!(decided.len(), 6, "{decided:?}");
|
||||
assert!(decided.iter().all(|(_, _, o)| o == "present"), "{decided:?}");
|
||||
assert!(decided.is_empty(), "{decided:?}");
|
||||
let changes: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM inertia_changes").fetch_one(&state.db).await.expect("changes");
|
||||
assert_eq!(changes, 0, "born at the low level: nothing changed");
|
||||
// The Raison d'être of a new base is still the text its own amendment names.
|
||||
// The Raison d'être of a new base is the text its own amendment of lot T4 names.
|
||||
let i1: Vec<String> = sqlx::query_scalar(
|
||||
"SELECT outcome FROM seed_amendments WHERE amendment_id = '2026-10-30-i1-raison-d-etre' ORDER BY doc_id",
|
||||
"SELECT outcome FROM seed_amendments WHERE amendment_id = '2026-11-03-i1-amorce' ORDER BY doc_id",
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
@@ -242,7 +248,16 @@ async fn production(tag: &str, cfg: Config) -> AppState {
|
||||
state
|
||||
}
|
||||
|
||||
/// The same base, started again with today's seed (and its amendments).
|
||||
/// The seed of M6 for these three texts, unchanged until M8 (de1b7eb): since lot T4 (03/11) the
|
||||
/// seed of today gives them Yvv's texts, and their seed proposal amendments are superseded
|
||||
/// (`tests/amorce.rs`).
|
||||
fn m8() -> Config {
|
||||
let mut cfg = Config::for_tests();
|
||||
cfg.seed_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-m8");
|
||||
cfg
|
||||
}
|
||||
|
||||
/// The same base, started again with a later seed (and its amendments).
|
||||
async fn restart(before: &AppState, cfg: Config) -> AppState {
|
||||
let state = AppState::new(cfg, before.db.clone()).await.expect("state");
|
||||
seed::ensure(&state).await.expect("seed");
|
||||
@@ -367,7 +382,7 @@ async fn the_production_base_turns_its_untouched_texts_into_seed_proposals_once(
|
||||
};
|
||||
let translated = translations(before.clone()).await;
|
||||
|
||||
let state = restart(&before, Config::for_tests()).await;
|
||||
let state = restart(&before, m8()).await;
|
||||
let first = decisions(&state).await;
|
||||
// Every text becomes a seed proposal; the level goes low where nobody touched it:
|
||||
// an open change of inertia by a member keeps it (I1 and E1 in Monnaie libre).
|
||||
@@ -480,7 +495,7 @@ async fn the_production_base_turns_its_untouched_texts_into_seed_proposals_once(
|
||||
);
|
||||
|
||||
// Decided once: a second start changes nothing.
|
||||
let again = restart(&state, Config::for_tests()).await;
|
||||
let again = restart(&state, m8()).await;
|
||||
assert_eq!(decisions(&again).await, first);
|
||||
for group in GROUPS {
|
||||
for code in CODES {
|
||||
@@ -508,7 +523,7 @@ async fn a_vote_keeps_the_text_in_force_and_the_refusal_is_kept() {
|
||||
let v = c.vote(&e1.id, "against").await;
|
||||
assert!(v["entered"].as_array().expect("entered").is_empty());
|
||||
|
||||
let state = restart(&before, Config::for_tests()).await;
|
||||
let state = restart(&before, m8()).await;
|
||||
let first = decisions(&state).await;
|
||||
assert_eq!(
|
||||
outcomes(&first),
|
||||
@@ -533,7 +548,7 @@ async fn a_vote_keeps_the_text_in_force_and_the_refusal_is_kept() {
|
||||
// Decided once: the vote withdrawn, a later start does not look again.
|
||||
let (s, v) = c.call("POST", &format!("/api/proposals/{}/vote", kept.id), Some(json!({ "choice": null }))).await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
let again = restart(&state, Config::for_tests()).await;
|
||||
let again = restart(&state, m8()).await;
|
||||
assert_eq!(decisions(&again).await, first);
|
||||
assert!(version(&again, "essai", "E1").await.in_force);
|
||||
}
|
||||
@@ -712,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());
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,10 @@
|
||||
//!
|
||||
//! Played with the seed M5 shipped (3f10a0e, the fixture of production): since lot T
|
||||
//! (2026-11-03), today's seed gives F1.2 and N1.1 the texts of the quorum, and their
|
||||
//! amendments of 02/11 are superseded (`tests/seed_quorum_texts.rs`).
|
||||
//! amendments of 02/11 are superseded (`tests/seed_quorum_texts.rs`). Since lot T4 (the same
|
||||
//! day, ADR 0014), the four leave the seed: the seed of M8 (`tests/fixtures/seed-m8/`), the last
|
||||
//! that holds them, stands for « today » here, and their last amendments are superseded by
|
||||
//! their retirement (`retire`).
|
||||
|
||||
use ld_server::seed_amend::{self, AMENDMENTS, Version};
|
||||
use ld_server::{AppState, Config, open_db, seed};
|
||||
@@ -21,8 +24,13 @@ fn before() -> Value {
|
||||
read(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-regles-avant.json"))
|
||||
}
|
||||
|
||||
/// The seed of M8 (de1b7eb): the last that holds the rule articles.
|
||||
fn m8_dir() -> std::path::PathBuf {
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-m8")
|
||||
}
|
||||
|
||||
fn today_seed() -> Value {
|
||||
read(Config::for_tests().seed_dir.join("acte-engagement-certification.json"))
|
||||
read(m8_dir().join("acte-engagement-certification.json"))
|
||||
}
|
||||
|
||||
/// The seed M5 shipped: production's since 2026-11-02.
|
||||
@@ -125,12 +133,14 @@ fn the_shipped_amendments_go_from_the_texts_of_before_to_the_seed_of_m5() {
|
||||
let (t, b) = fr(version(&today, code));
|
||||
let now = seed_amend::fingerprint(t, b);
|
||||
let mut last = a;
|
||||
while let Some(later) = last.superseded_by {
|
||||
while let Some(later) = last.superseded_by.filter(|l| *l != ld_server::retire::RETIREMENT) {
|
||||
let l = AMENDMENTS.iter().find(|l| l.id == later).expect("the later amendment");
|
||||
assert_eq!((l.unit, l.follows, l.from), (code, Some(last.id), last.to), "{code}: {}", l.id);
|
||||
last = l;
|
||||
}
|
||||
assert_eq!(now, last.to, "{code}: the text of today");
|
||||
// Lot T4 takes the article out of the seed: the last amendment of its text decides nothing more.
|
||||
assert_eq!(last.superseded_by, Some(ld_server::retire::RETIREMENT), "{code}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,9 +227,11 @@ async fn untouched_rule_articles_are_replaced_once_and_a_vote_keeps_its_text() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_new_base_is_born_with_the_rules_of_today() {
|
||||
async fn a_base_born_on_the_seed_of_m8_holds_its_rules() {
|
||||
let db = open_db("sqlite::memory:").await.expect("db");
|
||||
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
||||
let mut cfg = Config::for_tests();
|
||||
cfg.seed_dir = m8_dir();
|
||||
let state = AppState::new(cfg, db).await.expect("state");
|
||||
seed::ensure(&state).await.expect("seed");
|
||||
let today = today_seed();
|
||||
for group in ["essai", "monnaie-libre"] {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# 0014 — Les informations de fonctionnement ne se votent pas
|
||||
|
||||
> Décision d'Yvv du 2026-11-03 (lot T4). Complète [0009](0009-modules-ordre-et-titre.md) (les
|
||||
> modules) et [0011](0011-propositions-seed.md) (les Propositions seed). La migration est la
|
||||
> `0012`. Les textes, avant et après, sont relus dans
|
||||
> `docs/chantiers/acte-certification/textes-amorce.md`.
|
||||
|
||||
**Contexte.** Le seed de l'Acte d'engagement portait, depuis la maquette, trois sections au bas du
|
||||
document : `formule` (F1 « Formule de vote », F1.1 « Formule du seuil WoT », F1.2 « Paramètres par
|
||||
défaut », F1.3 « Processus de dépôt officiel »), `inertie` (N1 « Réglage de l'inertie », N1.1
|
||||
« Niveaux d'inertie ») et `ordonnancement` (O1 « Ordonnancement du document »), chacune avec son
|
||||
module « ordre des articles » (ADR 0009). Les lots T à T3c ont réécrit F1.1, F1.2 et N1.1 pour
|
||||
qu'ils disent les règles du moteur. Yvv, 2026-11-03 : « Attention les 3 derniers modules ne sont pas
|
||||
de votes : 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. […] Vire les 3 du bas de
|
||||
la liste. »
|
||||
|
||||
## Décision
|
||||
|
||||
**Ce qui sort du document** : les sept articles F1, F1.1, F1.2, F1.3, N1, N1.1, O1, et les trois
|
||||
modules `ordre.formule`, `ordre.inertie`, `ordre.ordonnancement`. Le document garde cinq
|
||||
sections (introduction, fondamental, technique, conclusion, annexe) et 27 articles, dont quatre
|
||||
Propositions seed : I1, E1, E12 « Responsabilité civique » (nouveau, entre E1 et E2) et E2.
|
||||
|
||||
**Hors du document, une unité n'est rien pour le vote.** Elle n'est ni lue avec son document
|
||||
(`docs::load_state` ne lit que les unités sans `retired_at`, et leurs versions), donc ni listée,
|
||||
ni comptée (`docs::group` : « propositions en cours » ; `overview::modules` : « modules à
|
||||
voter »), ni mise dans l'ordre d'une section (`DocState::members`), ni publiée (`document.json` et
|
||||
le Markdown lisent l'état du document ; `publish::waits_for_a_text` et `last_change` l'ignorent).
|
||||
**Aucun vote futur** : l'API répond 404 à un vote, à la lecture d'une de ses versions, à une
|
||||
correction, une traduction, une objection faite depuis elle, une proposition sur elle, un
|
||||
changement d'inertie, un quorum, un ordre (`proposals::guarded` : la version doit être celle d'une
|
||||
unité du document) ; 400 à un vote groupé qui la nomme (`lines`), à une objection qui la vise
|
||||
(`other`) et à un nouvel article dans une section sortie (`section`). Rien n'est écrit par ces
|
||||
refus.
|
||||
|
||||
**Une base neuve** naît sans eux : le seed ne les contient plus.
|
||||
|
||||
**Une base existante** (la production : Monnaie libre et le groupe d'essai) **n'efface rien**. La
|
||||
migration `0012` est additive : `units.retired_at` (quand l'unité est sortie) et
|
||||
`units.retired_reason` (l'amendement qui l'a sortie), les journaux `retirement_amendments` et
|
||||
`article_amendments`. Le retrait `2026-11-03-fonctionnement-hors-vote`
|
||||
(`crates/ld-server/src/retire.rs`) est décidé **une fois par document**, pour toutes les unités ou
|
||||
aucune, et n'écrit que ces deux colonnes : ni leurs versions, ni leurs révisions, ni leurs
|
||||
traductions, ni la file de publication ne changent. Il n'a lieu que si **aucune personne n'a
|
||||
laissé de trace** sur l'une d'elles, dans l'une de ses versions, quoi qu'il en soit advenu, et il
|
||||
dit la première trouvée (raison et code de l'unité) :
|
||||
|
||||
| Trace | Raison du refus |
|
||||
|---|---|
|
||||
| un vote du serveur, prouvé ou simulé | `votes` |
|
||||
| une ligne de bulletin, quel que soit son sort | `ballots` |
|
||||
| une adoption, une version entrée en vigueur ou confirmée, une inertie changée par un vote | `adopted` |
|
||||
| une version écrite par un membre (texte, retrait, inertie, ordre) | `member_proposal` |
|
||||
| une révision écrite par une personne, ou d'un genre qu'aucun amendement n'écrit ; une coquille, en attente ou refusée | `corrected` |
|
||||
| une traduction écrite ou corrigée par un membre | `translation_edited` |
|
||||
| une objection qui la nomme, levée ou non | `signalled` |
|
||||
| un quorum fixé (`units.quorum`, `quorum_changes`) | `quorum_set` |
|
||||
|
||||
Un amendement du seed n'est pas une trace de personne : F1.1 à sa révision 3, F1.2 et N1.1 à leur
|
||||
révision 4 (après M6c) ou 5 (après M8) sortent comme les autres. Refusé, rien ne bouge ; la
|
||||
décision est consignée (`retirement_amendments` : l'issue, la raison, l'unité, les codes sortis)
|
||||
et dans les journaux. Appliqué là où le document publie (Monnaie libre), une ligne s'ajoute à la
|
||||
file ; comme toutes, elle attend le premier texte en vigueur (ADR 0011).
|
||||
|
||||
Les amendements qui réécrivaient ces textes (`2026-11-02-n1-regles`, `-o1-regles`,
|
||||
`2026-11-03-f1-1-phrases`, `-f1-2-chiffres`, `-n1-1-chiffres`) sont **dépassés par le retrait**
|
||||
(`superseded_by`) : le seed ne donne plus leur texte, ils ne se décident plus. Une production
|
||||
restée à M6c sort donc F1.2 et N1.1 avec le texte de M6c.
|
||||
|
||||
## Les textes d'amorce
|
||||
|
||||
I1, E1 et E2 prennent les textes d'Yvv du 03/11 par trois amendements gardés de
|
||||
`seed_amend.rs` sur la Proposition seed elle-même (`Version::SeedProposal`), jamais sur un texte
|
||||
en vigueur : `2026-11-03-i1-amorce` (suit `2026-10-30-i1-raison-d-etre`), `2026-11-03-e1-amorce`,
|
||||
`2026-11-03-e2-amorce`. Même garde que tout amendement du seed (une trace de personne refuse,
|
||||
avec sa raison) ; la ligne d'historique `seed_proposal`, qui ne change aucun texte, est admise
|
||||
quand l'amendement de Proposition seed l'a écrite, et seulement elle. Les amendements de
|
||||
Proposition seed (`seed_proposal_amend.rs`) sont dépassés par eux.
|
||||
|
||||
E12 entre par un ajout gardé (`crates/ld-server/src/article_amend.rs`, sur le modèle de
|
||||
`module_amend`), une fois par document : écrit comme une base neuve l'écrit
|
||||
(`seed::write_unit`), **entre E1 et E2** tant qu'aucun vote n'existe sur l'ordre de la section
|
||||
(`quorum::voted_units`) et qu'ils s'y suivent — l'ordre en vigueur, qui lit les positions, le dit
|
||||
aussitôt —, sinon **en fin de section**, comme tout nouvel article. Refus : `code_taken` (une
|
||||
autre unité porte déjà ce code), `no_section`. Le code E12 suit la famille des engagements (E3 à
|
||||
E11 existent) ; la place vient de l'ordre de la section, pas du numéro.
|
||||
|
||||
## Ce que dira l'interface (lot U3)
|
||||
|
||||
Les informations de fonctionnement se montrent **hors du vote** : la formule du seuil, les crans
|
||||
d'inertie, le quorum, l'ordonnancement, à leur place dans l'écran (Yvv : « les infos doivent
|
||||
trouver leur place dans l'ux »), d'après les paramètres du moteur et du document, pas d'après
|
||||
un texte voté. Les vérités que F1.1, F1.2 et N1.1 portaient, chacune vérifiée par un test
|
||||
(lots T à T3c, `t3c-liste-fermee.md`), en sont la référence ; leurs derniers textes restent dans
|
||||
`crates/ld-server/tests/fixtures/seed-m8/` et, en production, dans leurs lignes. T4 ne touche pas
|
||||
`web/`.
|
||||
|
||||
## La file de publication
|
||||
|
||||
Rien n'y est effacé. Les lignes écrites pour ces textes avant T4 (les amendements de M5 à M8,
|
||||
« F1.2 », « N1.1 »…) restent telles quelles ; comme toute la file, elles attendent le premier
|
||||
texte en vigueur (ADR 0011) et partent avec lui : leurs raisons sont des lignes du message de
|
||||
commit, alors que le contenu publié (`document.json`, le Markdown) ne contient plus ces
|
||||
articles ; la ligne du retrait, ajoutée à la même file, dit qu'ils sont sortis.
|
||||
|
||||
## Le retour arrière
|
||||
|
||||
L'image d'avant (`de1b7eb`) ne connaît pas `retired_at` : elle lit les unités sans cette colonne
|
||||
et les montre de nouveau, **telles qu'elles étaient** (versions, révisions, traductions, ordres
|
||||
intacts : le retrait n'a écrit que ses deux colonnes). Elle démarre sur la base migrée : les
|
||||
migrations inconnues sont tolérées (`open_db`). Ce que le retour ne défait pas : E12 reste
|
||||
(une Proposition seed, que l'image d'avant lit comme les autres), I1, E1 et E2 gardent leurs
|
||||
textes de T4 (une révision de plus).
|
||||
|
||||
**Prouvé** par le test `an_image_that_ignores_the_column_shows_the_rules_of_operation_as_they_were`
|
||||
(`tests/amorce.rs` : la colonne remise à vide, la vue est celle d'avant), et le 2026-11-03 avec
|
||||
l'image de `de1b7eb` elle-même, hors du dépôt : une base née du seed de production du 02/11 puis
|
||||
démarrée par `de1b7eb` (un vote, un article de membre) ; l'image de T4 démarre dessus ; puis
|
||||
`de1b7eb` de nouveau. Les sept articles et les trois ordres se lisent à l'identique dans les deux
|
||||
groupes ; aucune ligne d'aucune table n'a disparu ; T4 n'a modifié que `retired_at` et
|
||||
`retired_reason` (20 unités), et les trois Propositions seed amendées (6 versions, 12
|
||||
traductions) ; `de1b7eb` relancée n'a rien écrit (une session de lecture). **Sur une copie de la
|
||||
production**, la même procédure : relever chaque ligne de chaque table (rowid compris) ; démarrer
|
||||
l'image de T4 sur la copie, sans clé de publication ; relever de nouveau (aucune ligne absente,
|
||||
aucune colonne changée hors de celles-là) ; démarrer `de1b7eb` sur la copie et comparer
|
||||
`GET /api/docs/{id}` d'avant et d'après pour F1 à O1 et `ordre.formule`, `ordre.inertie`,
|
||||
`ordre.ordonnancement`.
|
||||
|
||||
Limite : le retrait n'est décidé qu'une fois ; si, après un
|
||||
retour arrière, quelqu'un agit sur ces articles puis T4 revient, `retired_at` les sort de
|
||||
nouveau sans relire la garde. Les remettre serait alors une décision (`retired_at` à NULL : une
|
||||
écriture, pas un effacement).
|
||||
|
||||
## Limites connues
|
||||
|
||||
- **Une base d'avant M6** (I1, E1, E2 en vigueur, pas Propositions seed) : l'amendement de
|
||||
Proposition seed étant dépassé, ils restent en vigueur avec leur texte, et les trois
|
||||
amendements d'amorce refusent (`no_seed_proposal`). Aucune base réelle n'est dans ce cas.
|
||||
- Un article de membre prend le code `N<n>` (`proposals::new_unit`) d'après le nombre de codes
|
||||
en `N` du document, unités sorties comprises : en production, N1 et N1.1 restent dans la base
|
||||
et aucun code n'est repris ; dans une base neuve, le premier article de membre est N1, sans
|
||||
rapport avec l'ancien « Réglage de l'inertie ».
|
||||
@@ -18,3 +18,4 @@ pas une décision : on en écrit une nouvelle qui la remplace, et on le note dan
|
||||
| [0011](0011-propositions-seed.md) | Les Propositions seed : rien en vigueur au départ, trois textes mis en avant au cran 2, la publication attend le premier texte (complète 0002) | 2026-11-02 |
|
||||
| [0012](0012-tampon-des-votes.md) | Le tampon des votes : l'intention visible tout de suite, la preuve de la chaîne, la réconciliation a posteriori ; la simulation tant que la chaîne est fermée (complète 0008, révise 0010) | 2026-11-02 |
|
||||
| [0013](0013-quorum.md) | Le quorum par cran (12 · 12 · 50 · 50 · 50), fixé par l'auteur avant le premier vote ; sous le quorum, les votants qui manquent comptent comme « non » (remplace le C de 0002 et 0007) | 2026-11-03 |
|
||||
| [0014](0014-informations-de-fonctionnement.md) | Les informations de fonctionnement ne se votent pas : formule, inertie, ordonnancement sortent du document sans rien effacer (`units.retired_at`) ; E12 « Responsabilité civique » ajoutée (complète 0009 et 0011) | 2026-11-03 |
|
||||
|
||||
@@ -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) 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,44 +15,52 @@ 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 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** (8 lignes : le nom, « 11 articles », un petit niveau d'eau par article) ; « Tout déplier » : 30 lignes, une par article (les trois 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) |
|
||||
| C7 | Une décision par proposition | groupe d'essai : « Proposer un nouvel article », deux paragraphes « Je m'engage à… » | l'assistant : « Ce texte semble porter 2 décisions » ; « Découper en 2 brouillons » ; « Tes brouillons » apparaît |
|
||||
| C8 | Trois langues | choisir EN puis ES pour le texte | « Certification Commitment Act », « Acta de compromiso de Certificación » ; l'interface suit |
|
||||
| C9 | Les aperçus | « Si tout passe », puis « Si les bien cotées passent » | 33 articles avec la mention d'aperçu, puis aucun (rien en vigueur dans Monnaie libre, rien de bien coté sans « pour ») |
|
||||
| C9 | Les aperçus | « Si tout passe », puis « Si les bien cotées passent » | 27 articles avec la mention d'aperçu, puis aucun (rien en vigueur dans Monnaie libre, rien de bien coté sans « pour ») |
|
||||
| C10 | trustinWallet | ouvrir `/acces` sans être administratrice | « La gestion des accès est réservée aux administrateurs de trustinWallet. » (côté admin : créer un lien d'invitation) |
|
||||
| C11 | Une personne, une voix | avec un lien d'invitation et un portefeuille dont le compte n'est pas membre de la toile (Bob, chaîne simulée), voter « Pour » sur un article de Monnaie libre | le vote dit que la voix est celle de l'identité Ğ1 du portefeuille qui signe ; après signature : « Ton vote n'a pas été compté : le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1. », et sous l'article « Ton dernier vote n'a pas été compté… », 0 pour ; dans le groupe d'essai, la même personne vote directement, sans portefeuille |
|
||||
| C12 | Lien d'équipe | administrateur : `/acces` → « Liens d'équipe », cocher le groupe d'essai, créer ; l'ouvrir dans deux navigateurs, sous deux noms | chacun arrive sur ses groupes sous son nom ; dans `/acces`, « 2 / 30 personnes » et deux identités « venue par » le lien ; un nom déjà pris est refusé, même autrement écrit (« luna » pour « Luna ») ; sur un navigateur où quelqu'un est connecté, « Ce n'est pas moi » le déconnecte ; « Révoquer » ferme le lien |
|
||||
| M1 | Mobile | téléphone (ou fenêtre étroite) | le logo dans la barre du haut ; l'accueil sous la voûte tient à 412 et à 360 de large (les trois points dans l'ouverture, rien ne déborde) et montre les groupes ; menu en tiroir (le nombre à côté de « Docs de référence », les quatre pastilles d'ambiance) ; **au clavier, les pastilles** : un seul arrêt de Tab, les flèches gauche / droite passent d'une ambiance à l'autre et la choisissent ; l'anneau de focus, en pointillé, se lit sur la page (contraste 3:1 au moins, dans les quatre ambiances) et ne se confond pas avec l'anneau plein de l'ambiance choisie ; les gestes visibles sans survol, « Le vote » d'au moins 44 px ; le vote de la Raison d'être (en tête) se déplie sous elle : « 9 oui sur 12 votants pour passer » ; « Réglages de la formule » depuis le vote : le panneau en feuille, nommé « Réglages de la formule » en tête, le tableau des cinq crans tient dans la feuille ; « Voter en lot » : le panier tient dans la feuille, son bouton « Signer et envoyer » reste en vue |
|
||||
| M2 | Téléphone : les gestes du lecteur, les trois lectures (lot U2) | téléphone (Pixel 7), groupe d'essai : une version concurrente de la Raison d'être et un article à soi (l'automate les propose) ; lire la rangée « Lire » ; « Tout déplier » ; la Raison d'être → « Le vote » → « Autres versions (1) », « La formule du seuil » ; puis le vote de son article → « Proposer un changement d'inertie » | « En vigueur », « Si les bien cotées passent », « Si tout passe » se lisent en entier : la rangée passe à la ligne, rien ne défile ni ne se coupe ; « Tout déplier », « Autres versions (1) », « Fixer ce quorum » et **tous** les gestes en vue du vote déplié (Pour, Contre, les dépliants, « Réglages de la formule », « Coquilles… », « Soumettre au vote ») font au moins 44 px de haut |
|
||||
| C13 | Logo et ambiances | ouvrir `/` (appareil en clair, puis en sombre) ; choisir chaque pastille d'ambiance ; recharger | le logo « Clé de voûte » (signe, *libre*, **Decision**) droit, sans tampon ; le sceau 井 en bas à droite ; première visite : pierre (clair) ou nuit (sombre) ; les quatre pastilles (Pierre, Calcaire, Nuit, Ardoise) changent l'ambiance, qui reste après rechargement ; favicon, icône Apple et manifeste servis |
|
||||
| C14 | La page d'un groupe | Monnaie libre | le nom ; le pavé des quatre piliers (Docs de référence « ouvert », les trois autres « à venir ») ; « En cours » : l'Acte d'engagement Certification, qui ouvre son lecteur |
|
||||
| C15 | Le menu | survoler les outils ; ouvrir Docs, puis Mandats ; revenir à l'accueil | « 1 » à côté de « Docs de référence » ; aucun outil surligné sur la page du groupe, seul celui où l'on est ensuite ; au survol, seul Docs réagit ; Mandats s'ouvre quand même (ses « À venir ») ; hors d'un groupe, les outils mènent au dernier groupe ouvert (ou à l'accueil s'il n'y en a pas) |
|
||||
| C16 | En cours, à venir | Monnaie libre → Docs, Mandats, Protocoles ; les anciennes adresses `/decisions`, `/mandats`, `/protocoles` | Docs : « En cours » (Acte d'engagement Certification) et « À venir » (Acte d'engagement forgerons) ; Mandats : Administration forum Duniter, Administration forum Monnaie libre ; Protocoles : Runtime upgrade — décision on-chain ; les « À venir » sont éteints, sans lien ; ils suivent la langue ; les anciennes adresses mènent aux outils du groupe courant |
|
||||
| C17 | Décisions | Monnaie libre → Décisions ; voter (par la chaîne : bulletin, transaction, confirmation) sur une version d'un article, recharger | une ligne par doc : « Acte d'engagement Certification · 41 modules à voter · dont 41 sans ton vote » (33 articles, le titre, l'ordre de 7 sections), puis « dont 40 » ; la ligne ouvre le lecteur |
|
||||
| C17 | Décisions | Monnaie libre → Décisions ; voter (par la chaîne : bulletin, transaction, confirmation) sur une version d'un article, recharger | une ligne par doc : « Acte d'engagement Certification · 33 modules à voter · dont 33 sans ton vote » (27 articles, le titre, l'ordre de 5 sections), puis « dont 32 » ; la ligne ouvre le lecteur |
|
||||
| C18 | La Raison d'être | Monnaie libre → Acte d'engagement Certification → « Si tout passe », puis EN, puis ES | en tête du texte, « Raison d'être » : la première phrase en italique, puis deux paragraphes ; « Purpose », « Razón de ser » |
|
||||
| C19 | Les versions d'un article | Monnaie libre → « Propositions en cours » → « Tout déplier » → « Connaissance suffisante » (au clavier : Tab jusqu'à la ligne, Entrée) | une seule ligne, « 2 versions » ; dépliée : « variante 1.0.0 » et « variante 0.4.0-draft », chacune avec son niveau et ce qui la distingue ; un clic sur une version déplie son vote sous elle (cran 3, quorum 50 : « 39 oui sur 50 votants pour passer ») ; un nouveau clic sur la ligne la replie |
|
||||
| C19 | Les versions d'un article | Monnaie libre → « Propositions en cours » → « Tout déplier » → « Connaissance suffisante » (au clavier : Tab jusqu'à la ligne, Entrée) | une seule ligne, « 2 versions » ; dépliée : « variante 1.0.0 » et « variante 0.4.0-draft », chacune avec son niveau et ce qui la distingue ; un clic sur une version déplie son vote sous elle (cran 3, quorum 50 : « 39 oui sur 50 votants pour passer »), sans « Autres versions » : l'autre version est juste là, dans la liste (lot U2) ; un nouveau clic sur la ligne la replie |
|
||||
| C20 | Réglages de la formule et cinq crans | Monnaie libre → Acte d'engagement : « Réglages de la formule » (en-tête) ; puis, en tête du texte, Unicité du compte (Proposition seed) → « Le vote » → « Réglages de la formule » ; dans le groupe d'essai, Réciprocité (entrée en vigueur par le vote) → « Le vote » → « Proposer un changement d'inertie » → 5 → « Soumettre au vote » | W 5 745 (toile de confiance Ğ1), B 0,1 ; « 100 votants sur 5 745 (1,74 %), deux fois 50, le quorum de référence : la participation de référence… (0 pour l'instant) » ; le tableau des cinq crans : très basse, basse, moyenne, haute, très haute ; M 50, 50, 55, 60, 66 % ; part exigée 60, 67, 75, 83, 90 % ; quorum 12, 12, 50, 50, 50 (ADR 0013) ; voix pour à 100 votants 60, 67, 75, 83, 90, « Voix pour : à 100 votants, comme d'habitude. », « Part exigée : la part de « pour » demandée, à 100 votants comme d'habitude. », « Quorum : en dessous, les votants qui manquent comptent comme « non ». … » ; depuis le vote déplié : « Inertie · 2 / 5 · basse », la ligne du cran 2 marquée « cet article », à 0 votant 8, 9, 39, 43, 46 voix, « Voix pour : à 0 votant, la participation de cette version. » ; le sélecteur montre les cinq crans, 2 « actuelle », atteignable au clavier mais jamais choisi ; un seul arrêt de tabulation ; les flèches passent d'un cran à l'autre et le choisissent, en boucle ; le vote de la proposition se déplie : « inertie vers 5 / 5 · très haute », votée à « inertie 2 / 5 · basse » |
|
||||
| C21 | Le vote déplié | groupe d'essai : Unicité du compte → « Le vote », puis « Pour » ; une autre personne ouvre le même vote | sous l'article : le niveau d'eau, « 0 pour · 0 contre », « N oui sur Q votants pour passer » (le quorum en infobulle), « Il manque … », les crans de l'article, « Tu n'as pas voté sur cette version. » ; après « Pour » : « Ton vote : pour · enregistré », « 1 pour · 0 contre », le bouton enfoncé, « Ton vote ne se montre à personne : seuls les totaux sont visibles. » ; l'autre personne voit 1 pour, jamais qui ; « Le vote » replie ; « Retirer mon vote » remet 0 |
|
||||
| C22 | Le vote groupé | groupe d'essai : « Propositions en cours » → « Engagements techniques » → « Voter cette section » ; « Tout pour » ; dans « Connaissance suffisante », « Pour » sur la variante 1.0.0 ; recharger ; « Voter (n) » | le panier s'ouvre sur la section ; « Tout pour » remplit les articles à une version, pas « Connaissance suffisante » ni « Rencontre physique… » (« Plusieurs versions : choisis la tienne. », et le compte des articles laissés) ; le panier reste après rechargement (« Voter en lot · n ») ; un seul envoi : « n votes enregistrés. », le panier vide, la variante choisie à 1 pour |
|
||||
| C23 | Le vote par la chaîne Ğ1 | Monnaie libre, un portefeuille dans le navigateur : « Voter en lot », « Pour » sur Unicité du compte et Réciprocité (Propositions seed), « Signer et envoyer (2) » ; puis, en tête du texte, sous Réciprocité, « Contre » (l'indexeur en retard) puis « Retirer mon vote » ; puis sans portefeuille : la Raison d'être → « Pour » → « Passer par Ğecko » | une seule signature pour les deux : 0,1 Ğ1 vers le compte des votes et une remarque `ld1:` + 64 chiffres, rien du vote en clair ; « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « Compté pour l'identité Ğ1 « Alice ». » ; sous la proposition « Ton vote : pour · inscrit sur la chaîne Ğ1 », 1 pour ; « En attente de la chaîne Ğ1 : contre. » ; le retrait, plus récent sur la chaîne, fait foi même quand le « contre » est confirmé après lui (0 pour · 0 contre) ; Ğecko : un QR code et le lien `g1://<compte>?amount=0.1&comment=ld1:…`, puis la confirmation dès que la chaîne la montre |
|
||||
| C24 | Sans compte des votes | Monnaie libre sur un serveur sans `LD_VOTE_ACCOUNT`, puis le groupe d'essai | pas de « Voter en lot » ni « Voter cette section » ; sous la première Proposition seed : « Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer. Tu peux lire, proposer et faire une objection. », pas de bouton, les totaux et « 9 oui sur 12 votants pour passer » restent ; le panier ouvert par son adresse (`?onglet=panier`) dit la même chose, son bouton éteint, et **jamais** « Tes votes comptent dès l'envoi » ; dans le groupe d'essai, le panier et les boutons sont là |
|
||||
| C25 | L'ordre des articles | groupe d'essai : « Tout déplier » ; en fin de « Conclusion », la ligne « Ordre de la section » → « Réordonner » ; cliquer « Monnaie G1 » puis la place 1 ; au clavier, flèche bas puis haut ; « Proposer cet ordre » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas (quorum 1 dans le groupe d'essai : ADR 0013) ; puis, dans Monnaie libre, sur un téléphone, avec un portefeuille : « Voter en lot », « Pour » sur « Ordre des articles · Engagements fondamentaux », « Signer et envoyer (1) » | chaque section finit par **une ligne** « Ordre de la section » (7, aucune pour l'Ordonnancement, un seul article) : la poignée, les crans « basse », « seed a minima · 0 vote », **jamais la liste de ses articles** ; à la fin du texte quand la section s'y lit, sinon à la fin de sa section dans « Propositions en cours » ; le vote d'un ordre en vigueur déplié sous sa ligne montre la liste entière ; « Réordonner » : les articles de la section, sur place, sans panneau ; l'article choisi marqué, puis à sa place, annoncé (« « Monnaie G1 » en place 1 sur 2. »), « Proposer cet ordre » éteint tant que c'est l'ordre en vigueur ; le vote de l'ordre proposé se déplie sous la ligne, l'ordre proposé en clair, « Ordre en vigueur » parmi ses « Autres versions », « 1 ordre proposé » sur la ligne ; aucune ligne d'ordre parmi les propositions ; il est dans le panier, à la fin de sa section ; à la voix qui suffit : « Le nouvel ordre de « Conclusion » est entré en vigueur. », la section se lit dans le nouvel ordre ; par la chaîne : « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « seed a minima · 1 vote » ; sur un téléphone, les gestes du titre sans survol (réordonner sur un téléphone : C35) |
|
||||
| C26 | Le titre du document | groupe d'essai : sous le titre, « Proposer un autre titre » ; « Acte d'engagement de la certification » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas ; lire en anglais | sous le titre, une ligne : « Titre du document · seed a minima · 0 vote », les crans « inertie 4 / 5 · haute », deux gestes ; le panneau nommé « Proposer un autre titre » ; le titre proposé se vote **sous le titre** (« 1 titre proposé »), jamais parmi les propositions ; à la voix qui suffit : « Le titre « … » est entré en vigueur. », le document change de titre, son vote reste déplié sous le titre ; en anglais, l'ancien titre traduit, marqué « translation needs review » |
|
||||
| C25 | L'ordre des articles | groupe d'essai : « Tout déplier » ; en fin de « Conclusion », la ligne « Ordre de la section » → « Réordonner » ; cliquer « Monnaie G1 » puis la place 1 ; au clavier, flèche bas puis haut ; « Proposer cet ordre » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas (quorum 1 dans le groupe d'essai : ADR 0013) ; puis, dans Monnaie libre, sur un téléphone, avec un portefeuille : « Voter en lot », « Pour » sur « Ordre des articles · Engagements fondamentaux », « Signer et envoyer (1) » | chaque section finit par **une ligne** « Ordre de la section » (5 ; aucune pour la formule, l'inertie, l'ordonnancement, sortis du document : ADR 0014) : la poignée, les crans « basse », « seed a minima · 0 vote », **jamais la liste de ses articles** ; à la fin du texte quand la section s'y lit, sinon à la fin de sa section dans « Propositions en cours » ; le vote d'un ordre en vigueur déplié sous sa ligne montre la liste entière, et son inertie une fois, « Inertie · 2 / 5 · basse » (lot U2) ; « Réordonner » : les articles de la section, sur place, sans panneau ; l'article choisi marqué, puis à sa place, annoncé (« « Monnaie G1 » en place 1 sur 2. »), « Proposer cet ordre » éteint tant que c'est l'ordre en vigueur, et il le dit à côté (« C'est l'ordre en vigueur : déplace au moins un article. ») ; le vote de l'ordre proposé se déplie sous la ligne, l'ordre proposé en clair, « Ordre en vigueur » parmi ses « Autres versions », « 1 ordre proposé » sur la ligne ; aucune ligne d'ordre parmi les propositions ; il est dans le panier, à la fin de sa section ; à la voix qui suffit : « Le nouvel ordre de « Conclusion » est entré en vigueur. », la section se lit dans le nouvel ordre ; par la chaîne : « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « seed a minima · 1 vote » ; sur un téléphone, les gestes du titre sans survol (réordonner sur un téléphone : C35) |
|
||||
| C26 | Le titre du document | groupe d'essai : sous le titre, « Proposer un autre titre » ; « Acte d'engagement de la certification » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas ; lire en anglais | sous le titre, une ligne : « Titre du document · seed a minima · 0 vote », les crans « inertie 4 / 5 · haute », deux gestes ; le panneau nommé « Proposer un autre titre » ; le titre proposé se vote **sous le titre** (« 1 titre proposé »), jamais parmi les propositions ; à la voix qui suffit : « Le titre « … » est entré en vigueur. », le document change de titre, son vote reste déplié sous le titre, son inertie dite une fois (« Inertie · 4 / 5 · haute ») ; en anglais, l'ancien titre traduit, marqué « translation needs review » |
|
||||
| C27 | La participation ramenée dans ses bornes | un groupe de 4 dont la médiane est 2,5 votants, ramenée à 50 % (l'automate fait répondre ces chiffres au serveur) : « Réglages de la formule » | « 3 votants sur 4 (62,5 %) : la médiane des 12 propositions votées depuis un an. » — la part de la mesure elle-même, jamais 3 sur 4 arrondi d'abord ; puis « Ramenée entre 5 % et 50 % de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient (50 %). » ; « Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude. » |
|
||||
| C28 | Objection « redit » ; sur la version retenue, « contre » et retrait, jamais « pour » | groupe d'essai, à trois (Aude, Oscar, Vera) : Aude propose un nouvel article qui répète « Unicité du compte » ; Oscar, dans « Détails » de cette proposition → « Faire une objection » → « Redit », « En vigueur — Unicité du compte », un motif → « Envoyer l'objection » ; Vera déplie son vote → « Contre », puis « Retirer mon vote » ; Vera ouvre « Voter en lot » → « Tout le document » : « Tout pour », puis « Tout contre », puis « Vider » ; Aude ouvre « Messages » → « Modifier l'article visé », complète le texte et soumet ; Oscar ouvre « Messages » → « Lever l'objection » ; Vera déplie de nouveau son vote | « Objection enregistrée : jusqu'à sa levée, personne ne peut plus voter pour la dernière arrivée des deux versions. » ; chez Oscar, « « … » redit « Unicité du compte » », « personne ne peut plus voter pour cette version », « Lever l'objection » ; chez Vera, « 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à. », « Elle redit « Unicité du compte ». », pas de « Pour », mais « Contre », la ligne marquée « retenue par une objection » ; après « Contre » : « Vote enregistré. », « Ton vote : contre », « 0 pour · 1 contre », et « Retirer mon vote » ; après le retrait : « Vote retiré. », « 0 pour · 0 contre », toujours pas de « Pour » ; dans le panier, la version retenue montre « Contre » sans « Pour », avec « une objection la retient : contre ou retrait, pas de « pour » » ; « Tout pour » la laisse de côté et le dit (« Tu as tout mis pour, sauf 1 version qu'une objection retient : sur elle, tu peux voter contre ou retirer ton vote. », le nombre suivant les versions retenues du document) ; « Tout contre » la prend (« Contre » enfoncé) ; chez Aude, « Messages, 1 non lu », le message « Oscar fait une objection : ta version « … » redit « Unicité du compte »… Pour répondre, modifie l'article visé. », puis le panneau « Proposer » sur Unicité du compte, partant de son texte, avec « Cette proposition répond à une objection… » ; chez Oscar, « Ton objection sur « … » a reçu une réponse. », une échéance, « Lever l'objection » et « Réitérer l'objection » ; levée : « Objection levée. », le message dit « objection levée » sans plus aucun geste ; Vera peut de nouveau voter, « Pour » compris |
|
||||
| C29 | Objection « contredit », levée à l'échéance | groupe d'essai : Aude propose un article qui contredit « Réciprocité » ; Oscar fait une objection « Contredit » ; Aude déplie le vote de sa proposition → « Reformuler », change le fond, « Enregistrer la correction » ; Oscar ne fait rien ; trois jours passent (l'automate avance l'horloge du serveur : `POST /api/dev/clock`, connexion de développement seulement) | chez Aude : « Elle contredit « Réciprocité ». », « Pour qu'elle se vote à nouveau, reformule-la… », « Reformuler » ouvre « Détails » avec le formulaire de modification ; « Nouvelle version enregistrée : les votants devront reconfirmer. », puis « Une réponse est arrivée : Oscar lève ou réitère l'objection d'ici le … » ; trois jours après : l'objection dit « levée : 3 jours sans suite », la proposition se vote de nouveau, le message d'Oscar dit « objection levée : 3 jours sans suite », sans geste |
|
||||
| C30 | La coquille d'un autre, validée par l'auteur | groupe d'essai : Aude propose « Clé unique et vivante » ; Vera, dans « Détails » → « Corriger une coquille », apostrophes typographiques → « Enregistrer la correction » ; Aude ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend l'auteur du texte : elle s'applique quand il la valide, ou d'elle-même le … » ; le texte ne change pas ; « Coquilles proposées » montre le diff, « proposée par Vera », « en attente : s'applique d'elle-même le … » ; chez Aude : « Vera propose une correction de coquille dans ta version « Clé unique et vivante ». », le diff, une échéance, « Valider » / « Refuser » ; « Correction appliquée. », le message dit « correction appliquée » ; le texte porte la correction, l'historique « coquille corrigée · par Vera · validée par Aude » |
|
||||
| C31 | Coquille sur un texte en vigueur | groupe d'essai : une administratrice (Ada) ouvre le groupe ; Vera, sur « Raison d'être » → « Le vote » → « Coquilles, traductions, objections, historique » → « Corriger une coquille », « C'est le seul » → « C’est le seul » ; Ada ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend un administrateur, ce texte venant du seed… » ; le texte en vigueur ne change pas (« C'est le seul ») ; la correction en attente se lit dans « Détails » ; chez Ada : « Vera propose une correction de coquille dans « Raison d'être », un texte du seed : un administrateur la valide. » ; après « Valider », le texte en vigueur porte « C’est le seul » et l'historique « validée par Ada » |
|
||||
| C31 | Coquille sur un texte en vigueur | groupe d'essai : une administratrice (Ada) ouvre le groupe ; Vera, sur « Raison d'être » → « Le vote » → « Coquilles, traductions, objections, historique » → « Corriger une coquille », « C'est l'unique » → « C’est l’unique » ; Ada ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend un administrateur, ce texte venant du seed… » ; le texte en vigueur ne change pas (« C'est l'unique enjeu ») ; la correction en attente se lit dans « Détails » ; chez Ada : « Vera propose une correction de coquille dans « Raison d'être », un texte du seed : un administrateur la valide. » ; après « Valider », le texte en vigueur porte « C’est l’unique enjeu » et l'historique « validée par Ada » |
|
||||
| C32 | Lâcher ou garder un bulletin | Monnaie libre, un portefeuille dans le navigateur (Eve, chaîne simulée), « Propositions en cours » → Document de révocation : « Contre » le nœud éteint ; puis « Contre », fermer la fenêtre pendant que le portefeuille demande, refuser dans le portefeuille ; puis « Contre », fermer, signer dans le portefeuille ; une autre personne, sans portefeuille : « Contre » → « Passer par Ğecko », fermer, payer depuis un téléphone | nœud éteint : « La chaîne Ğ1 ne répond pas. Réessaie dans un moment. », le bulletin lâché aussitôt, rien « en attente » sous l'article ; fenêtre fermée puis refus : lâché ; fenêtre fermée puis signature : gardé, et la chaîne le confirme sans la page (« Ton vote : contre · inscrit sur la chaîne Ğ1 ») ; Ğecko, fenêtre fermée : « En attente de la chaîne Ğ1 : contre. », puis confirmé sans la page |
|
||||
| C33 | Le reçu de ton vote | Monnaie libre, un portefeuille : « Contre » sur Ne jamais certifier seul (« Propositions en cours »), « Télécharger ton reçu », « Tes bulletins » ; sur « Tes bulletins », « Fichier du reçu » : le reçu ; puis coller le reçu avec un choix changé, « Vérifier » ; une autre personne ouvre « Tes bulletins » | le reçu `recu-vote-<date>-<id>.json` : les votes, le sel, la remarque `ld1:…`, « confirmed », l'identité « Alice » ; « Tes bulletins de vote » : « Compté pour l'identité Ğ1 « Alice » » et la remarque ; « Le reçu tient : ses votes et son sel donnent bien ce commentaire. », « Ne jamais certifier seul : contre » ; changé : « Le reçu ne tient pas » ; l'autre personne : « Tu n'as encore envoyé aucun bulletin par la chaîne Ğ1. », et le reçu lui est refusé |
|
||||
| C34 | Le vote simulé, la chaîne fermée (ADR 0012) | Monnaie libre sur un serveur sans compte des votes (l'automate ferme la chaîne du serveur jetable, puis la rouvre) : « Propositions en cours » → Logiciel auditable → « Contre », sans portefeuille ; puis « Voter en lot », « Contre » encore, « Voter (1) » ; puis « Tes bulletins » | « Voter en lot » est là, marqué « vote simulé » ; aucune fenêtre de portefeuille ; « Vote simulé. », **jamais** « Vote enregistré. » ; « Ton vote : contre · simulé » aussitôt, et un « contre » de plus dans les totaux ; dans l'API, `myVoteState` « simulated » et `tallySimulated` ; le panier : « 1 vote simulé. », jamais « enregistré » ; dans « Tes bulletins » : « Simulé : compté pour « Sacha » tant que les votes simulés ne sont pas effacés », sans bloc (l'ouverture de la chaîne n'efface rien : c'est un acte humain, sans date promise) |
|
||||
| C35 | Réordonner une section, par poignées, puis voter (lot U) | groupe d'essai : à la fin des « Engagements fondamentaux », la ligne « Ordre de la section » → « Réordonner » ; glisser la poignée de Réciprocité sur Unicité du compte ; au clavier, flèche haut puis bas ; « Descendre « Réciprocité » » ; cliquer Réciprocité puis la place 2 ; « Annuler », puis « Réordonner » et « Monter « Réciprocité » » ; « Proposer cet ordre » ; « Pour » (une deuxième voix si la sienne ne suffit pas) ; puis, sur un téléphone, dans Monnaie libre : « Tout déplier », « Ordre des articles · Engagements techniques » → « Réordonner » | la ligne ne répète aucun article ; « Réordonner » : les articles de la section, en vigueur ou seulement proposés (« Engagements fondamentaux » marqué « proposé »), sur place (pas de panneau), une poignée devant chacun, les articles en vigueur de la section repliés dans la liste (pas deux fois) ; chaque geste annonce la place (« « Réciprocité » en place 2 sur 3. ») ; « Proposer cet ordre » éteint sur l'ordre en vigueur ; « Annuler » rend la section telle qu'elle se lisait ; « Ordre proposé au vote. », son vote déplié aussitôt sous la ligne, l'ordre proposé en clair, « 1 ordre proposé » ; « Vote enregistré. » ; en vigueur, la section se lit dans le nouvel ordre, « adopté le … » ; sur un téléphone : 11 articles, rien ne déborde de la page, l'article, sa poignée et ses flèches font au moins 44 px |
|
||||
| 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 ». » ; 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 plus. », puis le réglage disparaît |
|
||||
| C35 | Réordonner une section, par poignées, puis voter (lot U) | groupe d'essai : à la fin des « Engagements fondamentaux », la ligne « Ordre de la section » → « Réordonner » ; glisser la poignée de Réciprocité sur Responsabilité civique ; au clavier, flèche haut puis bas ; « Descendre « Réciprocité » » ; cliquer Réciprocité puis la place 2 ; « Annuler », puis « Réordonner » et deux fois « Monter « Réciprocité » » ; « Proposer cet ordre » ; « Pour » (une deuxième voix si la sienne ne suffit pas) ; puis, sur un téléphone, dans Monnaie libre : « Tout déplier », « Ordre des articles · Engagements techniques » → « Réordonner » | la ligne ne répète aucun article ; « Réordonner » : les articles de la section, en vigueur ou seulement proposés (« Engagements fondamentaux » marqué « proposé »), sur place (pas de panneau), une poignée devant chacun, les articles en vigueur de la section repliés dans la liste (pas deux fois) ; chaque geste annonce la place (« « Réciprocité » en place 3 sur 4. ») ; « Proposer cet ordre » éteint sur l'ordre en vigueur, et il le dit à côté, lu aussi comme sa description (« C'est l'ordre en vigueur : déplace au moins un article. ») : la phrase s'en va dès qu'un article bouge et revient quand l'ordre redevient celui en vigueur (lot U2) ; « Annuler » rend la section telle qu'elle se lisait ; « Ordre proposé au vote. », son vote déplié aussitôt sous la ligne, l'ordre proposé en clair, « 1 ordre proposé » ; « Vote enregistré. » ; en vigueur, la section se lit dans le nouvel ordre, « adopté le … » ; sur un téléphone : 11 articles, rien ne déborde de la page, l'article, sa poignée et ses flèches font au moins 44 px |
|
||||
| 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`) |
|
||||
| C44 | Les réglages de la formule se lisent en entier (lot U3b) | Monnaie libre : « Réglages de la formule » en tête du document, puis depuis le vote d'Unicité du compte ; sur un bureau (1360 × 900, puis 1024 × 768) et sur un téléphone ; en FR, EN, ES | le tableau « Les cinq crans » tient dans le panneau : ses six colonnes, « Voix pour » et ses cinq chiffres compris, sans que le tableau, le panneau ou la page défilent de côté ; les en-têtes peuvent passer sur deux lignes (« Part / exigée », « Voix / pour »), le nom du cran aussi, un chiffre jamais ; depuis le vote, « cet article » sur la ligne du cran 2 ; dans les quatre ambiances |
|
||||
|
||||
## Au-delà de la liste
|
||||
|
||||
@@ -93,3 +101,11 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa
|
||||
| 2026-11-03 | automate | local, lot S (Propositions seed, ADR 0011 ; le quorum par cran, ADR 0013 : C3, C4, C5, C9, C18 à C20, C23, C24, C27, M1 repris) | C1–C33, M1 | 34 / 34 verts (35 tests) |
|
||||
| 2026-11-03 | automate | local, lot U sur `6fb8238` (le lecteur refait : l'ordre en une ligne au bout de sa section, poignées sur place et vote ; oui pour passer · non pour bloquer ; quorum de l'auteur ; panneau sans onglets ; C3, C4, C6, C19 à C21, C23 à C26, C34, M1 repris) | C1–C37, M1 | 38 / 38 verts (40 tests) |
|
||||
| 2026-11-03 | automate | local, lot T2 sur `f27ee08` (les trois phrases de F1.1, F1.2 et N1.1 ; l'espace perdu du vote déplié ; C34 lit la ligne entière, C37 « ne change pas ») | C1–C37, M1 | 38 / 38 verts (40 tests), du premier coup |
|
||||
| 2026-11-03 | automate | local, lot U2 sur `bb4e7b7` (les mineurs de la relecture de M7 : cibles de 44 px et trois lectures entières sur téléphone, M2 ; « Proposer cet ordre » dit pourquoi, C35 ; une information à un seul endroit, C19, C36 ; l'en-tête et les changements d'inertie d'un module, C38 ; C25 et C26 lisent l'inertie dans « Inertie ») | C1–C38, M1, M2 | 40 / 40 verts (42 tests), du premier coup |
|
||||
| 2026-11-03 | automate | local, lot T3 sur `b2d653e` (N1.1 et F1.2 sans chiffre daté, chaque phrase vraie pour tout W et tout quorum ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests) au second passage ; le premier, 40 / 42 : C33 et C38 tombés sur `net::ERR_NETWORK_CHANGED` (le réseau du poste a changé pendant le passage, vu dans leurs traces) |
|
||||
| 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` |
|
||||
| 2026-11-03 | automate | local, lot U3b sur `a6f5e22` (le tableau des cinq crans se lit en entier dans le panneau des réglages, sur bureau comme sur téléphone, en fr, en, es, C44) | C1–C44, M1, M2 | 46 / 46 verts (48 tests), du premier coup à la boucle finale (22 h 55 - 22 h 59, sous le verrou `/tmp/ld-e2e.lock`) ; avant le code, C44 vu rouge sur l'interface de `a47aed0` : 12 cas sur 18 (bureau 1360 et 1024, en tête et depuis une version, trois langues : le tableau défile, 406 à 501 px pour 394), le téléphone vert |
|
||||
| 2026-11-03 | automate | local, lot U3c sur `266a443` (après la relecture de `d66823b` : `formula.reading` dit « quand tout le monde vote, la part exigée est la majorité M » à la place de « la majorité M suffit », faux pour W ≤ 15, en fr, en, es ; les quatre raisons de la file de T4 disent « Proposition d'amorce » ; aucun contrôle changé) | C1–C44, M1, M2 | 46 / 46 verts (48 tests), du premier coup (23 h 42 - 23 h 46, sous le verrou `/tmp/ld-e2e.lock`) ; boucle : fmt, clippy, 338 tests Rust, npm ci, check 0 / 0, 116 tests unitaires, build |
|
||||
|
||||
@@ -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é), **changé** (lot U3c) : « Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la part exigée est la majorité M. » (avant : « … la majorité M suffit », faux pour W ≤ 15 : quand les W votent, R = B^W + M·W, et B^W ajoute une voix quand M·W est entier, sur 17 couples (W, cran), par exemple W = 4, cran 2 : 3 « pour » sur 4, pas 2 ; relecture de `d66823b`) | `formula.reading` | grille `the_share_asked_falls_beyond_the_article_s_quorum_down_to_m` ; « la part exigée est la majorité M » : `ld-engine` `inertia::when_everyone_votes_the_share_asked_is_m_on_every_electorate` (tout W de 1 à 10 000 000, les cinq crans, G aux deux bornes de la participation habituelle, les quorums 1, 2 et W : la part exigée des T′ = max(W, Q) votants vaut M exactement ; la phrase d'avant fausse sur les 17 couples, tous W ≤ 15) ; `web/tests/unit/rules.test.ts` : la phrase en fr, en, es, aucune chaîne ne dit que la majorité suffit |
|
||||
| F1.1.4 | `T' = max(T, Q)` et `Seuil = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'` | au vote | « La formule du seuil », en MathML (déjà) | — | grille `the_formulas_of_the_texts_are_those_the_engine_computes` |
|
||||
| F1.1.5 | « Une version passe quand ses voix pour atteignent le seuil, arrondi à l'entier au-dessus. » | au vote | « La formule du seuil », **nouveau** : « Une version atteint son seuil quand ses voix pour atteignent R, arrondi à l'entier au-dessus. » | `formula.round` | `ld-engine` `threshold::a_whole_number_of_votes_never_gains_one_by_rounding` |
|
||||
| F1.1.6 | les lettres T, Q, T′, W, B, M, G | au vote | « La formule du seuil » : chaque lettre avec sa valeur lue dans l'API (déjà), **nouveau** T′ (« votants retenus : 12, T ou Q s'il en manque »), **changé** G (« déduit de la participation habituelle du groupe »), **nouveau** sous la liste, dans la toile seulement : « À la puissance W, B est si petit qu'il ne compte pas dans la toile de confiance. » | `formula.T`, `formula.Tprime`, `formula.Q`, `formula.W`, `formula.B`, `formula.B.wot`, `formula.M`, `formula.G` | `formula.Tprime` : `the_formulas_…` ; `formula.B.wot` : grille `b_to_the_power_w_never_counts_in_the_web_of_trust` (W de 100 à 10 000 000) ; G : grille `g_gives_…` |
|
||||
| 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.
|
||||
@@ -169,3 +169,58 @@ refus du serveur sont dits dans les mots de la personne : hors bornes (400), un
|
||||
- La ligne d'ordre, ses gestes discrets comme ceux d'un article (au survol sur bureau, toujours
|
||||
visibles sur téléphone) : elle ne crie plus, mais « Réordonner » se trouve au survol.
|
||||
- Le mot court dans la liste : « passe à 11 oui · bloquée à 4 non ».
|
||||
|
||||
## Après la relecture de M7 (lot U2, 03/11)
|
||||
|
||||
Les mineurs de la relecture, tranchés ainsi (captures `/tmp/ld-u2-shots/`, avant et après) :
|
||||
|
||||
- **Une information à un seul endroit** dans le vote déplié : l'inertie se dit une fois, dans
|
||||
« Inertie », ses crans avec elle (« Inertie · ▮▮▯▯▯ 2 / 5 · basse ») ; seul le vote d'un
|
||||
changement d'inertie dit sous ses totaux le cran où il se vote. La ligne d'état d'une
|
||||
Proposition seed (totaux, oui, crans, autres versions) s'efface tant que son vote est déplié
|
||||
juste dessous ; les gestes gardent leur place (à droite sur bureau, à gauche sur téléphone).
|
||||
« Autres versions » ne nomme pas les versions déjà listées à côté (un article à deux versions,
|
||||
dans la liste) ; il nomme encore le texte en vigueur, lu plus haut.
|
||||
- **La ligne de faits compte ce que la page montre** : « N articles en vigueur · N Propositions
|
||||
seed · N propositions en cours ». Les Propositions seed n'y sont dites que lorsqu'elles sont lues
|
||||
en tête (rien en vigueur) ; « propositions en cours » compte les versions que montre la liste
|
||||
du bas, jamais les ordres, titres ni changements d'inertie d'un module, qui se votent à la ligne
|
||||
de leur module. La carte du document, sur la page du groupe, garde le total du serveur.
|
||||
- **Un changement d'inertie d'un ordre ou du titre** a sa pastille sur la ligne de son module,
|
||||
à côté de « N ordres proposés » ou « N titres proposés » : « 1 changement d'inertie proposé »,
|
||||
qui déplie son vote là.
|
||||
- **« Proposer cet ordre » éteint le dit**, à sa gauche (sur téléphone, au-dessus) : « C'est
|
||||
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.
|
||||
- **Le panneau des réglages se lit en entier** (lot U3b) : le tableau des cinq crans vit dans le
|
||||
panneau de droite, 27rem sur tout bureau, à peine plus large que la feuille d'un téléphone ; il en
|
||||
prend donc les règles partout (cellules resserrées, en-têtes et nom du cran sur deux lignes au
|
||||
besoin, un chiffre jamais coupé) : « Voix pour » ne se cache plus derrière un défilement de côté,
|
||||
en français, en anglais, en espagnol (C44 ; captures : `/tmp/ld-u3b-shots/`).
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
# Lot T3c : la liste fermée des phrases de F1.2 et N1.1
|
||||
|
||||
> Préparé le 2026-11-03 (lot T3c, branche `lot/t3c-habituelle`), après la relecture indépendante de
|
||||
> `e7ab82f` et le complément d'orch du même soir : « que T3c soit le dernier tour ». Remise telle
|
||||
> quelle au relecteur. Elle donne **toutes** les phrases de F1.2 « Paramètres par défaut » et de N1.1
|
||||
> « Niveaux d'inertie », dans leur texte final (le français fait foi ; en et es disent la même chose,
|
||||
> mot pour mot dans `seeds/acte-engagement-certification.json`). Pour chacune : ce qu'elle affirme, et
|
||||
> le test qui la vérifie contre le moteur avec sa grille, ou « rien à vérifier » (une définition, une
|
||||
> règle sans nombre). Ce qui a changé depuis `e7ab82f`, avant et après, avec ses raisons :
|
||||
> `textes-quorum-seed.md`, « Après la relecture de e7ab82f ». F1.1 n'est pas touché (ordre de T3c).
|
||||
|
||||
## La grille
|
||||
|
||||
Les tests nommés sont dans `crates/ld-server/tests/seed_quorum_texts.rs`, sauf mention. **La grille
|
||||
T3c** (`grid()`, 85 100 cas) :
|
||||
|
||||
- **W** : 100, 101, 1 000, 3 000, 5 707, 7 224, 10 000, 1 000 000, 10 000 000 (`ELECTORATES` ; 100 et
|
||||
10 000 000 sont les bornes de `wot::PLAUSIBLE`, la toile que le serveur tient pour vraie).
|
||||
- **La participation habituelle** : la référence (moins de 5 propositions : 2 × 50 votants), puis les
|
||||
médianes 1, 2, W/100 − 1, W/100, W/100 + 1, 12, 40, 50, 100, 120, 1 000, W/2, W/2 + 1 et W (celles de
|
||||
1 à W). Chacune passe par `Participation::of`, comme en production : **une médiane sous 1 % de W ou
|
||||
au-dessus de 50 % de W est ramenée à la borne**, et c'est la valeur bornée qui est la participation
|
||||
habituelle (W 7 224, médiane 40 : 72,24 votants). Cette valeur n'est pas toujours un nombre entier de
|
||||
votants : la part qu'y demande le moteur se lit sur une toile cent fois plus grande (`share_at`, le
|
||||
moteur ne lit que T'/W ; vérifié contre le moteur sur les nombres entiers).
|
||||
- **Le quorum de l'article** : chaque quorum qu'un auteur peut fixer, de 2 à W, jusqu'à W = 1 000 ; au-delà,
|
||||
2, 3, 11, 12, 13, 49, 50, 51, 100, 1 000, W/2, W − 1, W, et autour de la participation habituelle et de
|
||||
la médiane (l'entier en dessous, au-dessus, et le suivant).
|
||||
- **Les cinq crans.**
|
||||
|
||||
Certains tests ont leur propre grille, dite à leur ligne. « Vu rouge » : le test échoue sur le texte de
|
||||
`e7ab82f` (8 tests, sortie gardée), ou sous une mutation du code, défaite aussitôt (quatre mutations,
|
||||
en bas).
|
||||
|
||||
## F1.2 « Paramètres par défaut »
|
||||
|
||||
1. « Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la façon dont le vote se
|
||||
déroule. »
|
||||
- Affirme : ce que contient l'article. **Rien à vérifier.**
|
||||
2. Intertitre « **Un vote permanent, sans durée** ». **Rien à vérifier.**
|
||||
3. « Chaque version se vote à tout moment, sans échéance. »
|
||||
- Affirme : une règle sans nombre. **Rien à vérifier** sur la grille.
|
||||
4. « Chacun peut changer ou retirer son vote : le dernier fait foi. »
|
||||
- Affirme : une règle sans nombre. **Rien à vérifier** sur la grille. Tenue ailleurs, sans être reprise
|
||||
par T3c : `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`. *À savoir* : sur une version
|
||||
retenue par une objection, on ne peut que retirer son vote ou passer à « contre » (ADR 0010) ; la
|
||||
phrase ne le dit pas. Hors du champ de T3c (ni nombre, ni part, ni terme défini) : laissée, à
|
||||
trancher.
|
||||
5. « Une proposition entre en vigueur dès qu'elle atteint son seuil. »
|
||||
- Affirme : une règle sans nombre (le seuil est F1.1). **Rien à vérifier** sur la grille. *À savoir* :
|
||||
une version retenue par une objection n'entre pas en vigueur (ADR 0010), même au seuil ; même
|
||||
remarque que la phrase 4.
|
||||
6. « S'il y a déjà un texte en vigueur, il lui faut en plus une marge (voix pour moins voix exigées) plus
|
||||
large que celle de ce texte. »
|
||||
- Affirme : une règle sans nombre. **Rien à vérifier** sur la grille ; tenue par `ld-engine`
|
||||
`compose::a_rival_must_beat_the_version_in_force`.
|
||||
7. « Le texte remplacé redevient une proposition, avec ses votes : il peut revenir. »
|
||||
- Affirme : une règle sans nombre. **Rien à vérifier** ; tenue par `tests/api.rs`
|
||||
`a_rival_replaces_the_seed_and_the_seed_returns_among_proposals`.
|
||||
8. « Si plusieurs propositions y parviennent, celle qui a la marge la plus large entre en vigueur. »
|
||||
- Affirme : une règle sans nombre. **Rien à vérifier** ; tenue par `ld-engine`
|
||||
`compose::rivals_on_one_unit_yield_one_winner`.
|
||||
9. Intertitre « **Les lettres** ». **Rien à vérifier.**
|
||||
10. « **W** : le nombre de membres de la toile de confiance Ğ1, relu sur la chaîne toutes les 6 heures. »
|
||||
- Affirme : une définition, et le nombre 6.
|
||||
- Test : `every_number_of_the_quorum_texts_is_the_engine_s` (6 = `wot::REFRESH` ; tout chiffre que
|
||||
rien ne justifie fait échouer), le même sur chaque W de la grille.
|
||||
11. « **T** : les votes exprimés sur la version, pour et contre. » — une définition. **Rien à vérifier.**
|
||||
12. « **Q**, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». »
|
||||
- Affirme : sous Q votants, la part se demande à Q votants, les absents comptés « non ».
|
||||
- Test : `under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted` — W de la grille, chaque
|
||||
quorum de 2 à W jusqu'à W = 10 000 (au-delà : 2, 3, 12, 50, 100, 1 000, W/2, W − 1, W), chaque
|
||||
participation de la grille (les deux bornes y sont), les cinq crans ; le groupe d'essai de 1 à
|
||||
40 membres, quorum 1 à W ; 833 105 cas. Vu rouge sous la mutation 2.
|
||||
13. « Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au
|
||||
cran 5. »
|
||||
- Affirme : les quorums par cran, et qu'un article sans quorum à lui prend celui de son cran.
|
||||
- Tests : `every_number_of_the_quorum_texts_is_the_engine_s` (les cinq nombres, lus dans le seed) ;
|
||||
`tests/quorum.rs` `each_level_has_its_quorum_and_every_evaluation_says_it` (chaque évaluation
|
||||
sur le quorum de son cran).
|
||||
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. »
|
||||
- Affirme : une règle sans nombre (qui, jusqu'à quand). **Rien à vérifier** sur la grille ; tenue par
|
||||
`tests/quorum.rs` `the_author_of_an_article_sets_its_quorum_before_the_first_vote`,
|
||||
`in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pending`, et
|
||||
`tests/ballots.rs` `a_vote_exists_for_every_state_of_the_buffer` (lot T2).
|
||||
15. « Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un texte. »
|
||||
- Affirme : le quorum le plus bas est 2 ; un seul « pour » ne passe jamais, quels que soient les
|
||||
« contre ».
|
||||
- Tests : `every_number_of_the_quorum_texts_is_the_engine_s` (2 = `quorum_bounds(W, false)`, le même
|
||||
sur chaque W) ; `under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted` (sa grille, plus
|
||||
haut : à chaque cas de Monnaie libre, 1 « pour » avec 0, 1, Q ou W − 1 « contre » ne passe pas ; au
|
||||
quorum 2, deux « pour » sur chaque W et à chaque cran) ; `tests/quorum.rs`
|
||||
`in_monnaie_libre_a_quorum_holds_two_voices_…` (un quorum de 1 refusé).
|
||||
16. « **B = 0,1** : dans la toile de confiance, `B^W` est si petit qu'il ne compte pas. »
|
||||
- Affirme : 0,1, et que B^W ne change aucun seuil dans la toile.
|
||||
- Tests : `every_number_of_the_quorum_texts_is_the_engine_s` (0,1 = `FormulaParams::DEFAULT_BASE`) ;
|
||||
`b_to_the_power_w_never_counts_in_the_web_of_trust` — W de la grille et 102, 5 000, 5 708, chaque
|
||||
participation de la grille, quorums de 2 à W (échantillon), neuf nombres de votants par cas,
|
||||
41 505 cas : le seuil avec B est celui sans B. Vu rouge sous la mutation 2.
|
||||
17. « **M** et **G** : donnés par le cran d'inertie de l'article (voir « Niveaux d'inertie »). »
|
||||
- Affirme : M et G sont ceux du cran (G aussi de la participation habituelle, phrase suivante).
|
||||
- Test : `the_formulas_of_the_texts_are_those_the_engine_computes` — W 2, 40 et ceux de la grille,
|
||||
chaque participation de la grille, quorums 1, 2, 12, 50, ⌈p̃ × W⌉, W/2, W, douze nombres de
|
||||
votants, les cinq crans, 19 950 cas par langue : M du cran, G écrit comme N1.1 l'écrit égal à
|
||||
celui du moteur. Vu rouge sous les mutations 1 et 2.
|
||||
18. « G n'est pas fixé à la main : il se déduit de la participation habituelle du groupe. »
|
||||
- Affirme : G vient de la participation habituelle — la valeur bornée, pas la médiane brute.
|
||||
- Tests : `g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum` (la
|
||||
grille T3c : G écrit avec p̃ la participation habituelle divisée par W est celui du moteur, à
|
||||
chaque quorum) ; `the_formulas_…` (ci-dessus). Vus rouges sous la mutation 1.
|
||||
19. Intertitre « **Par défaut** ». **Rien à vérifier.**
|
||||
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. »
|
||||
- Affirme : le cran 3, son nom, M = 55 %, le quorum 50, et, quand la participation habituelle
|
||||
atteint le quorum, une part exigée de 75 % à cette participation.
|
||||
- Tests : `every_number_of_the_quorum_texts_is_the_engine_s` (3, moyenne, 55, 50, 75 : le cran par
|
||||
défaut du moteur, son M, son s, son quorum dans le seed) ;
|
||||
`the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum` (la grille T3c,
|
||||
donc le cran 3 à chaque quorum, dont 50 : à la participation habituelle, s exactement quand elle
|
||||
atteint le quorum — 16 985 cas, dont 2 375 où la médiane était sous 1 % de W et 7 365 au-dessus de
|
||||
50 % ; jugée sur le quorum, plus bas que s, sinon — 68 115 cas). Vu rouge sur le texte de
|
||||
`e7ab82f` (« quand il vote comme d'habitude ») et sous la mutation 1.
|
||||
21. « Tout nouvel article commence à ce cran. »
|
||||
- Affirme : un article créé est au cran 3.
|
||||
- Test : `tests/quorum.rs` `the_author_of_an_article_sets_its_quorum_before_the_first_vote` (ajouté
|
||||
par T3c : l'article « Nouveau » est au cran 3). Vu rouge sous la mutation 3.
|
||||
22. « Au départ, aucun article n'est en vigueur. »
|
||||
- Affirme : une règle sans nombre. **Rien à vérifier** sur la grille ; tenue par
|
||||
`tests/seed_proposals.rs` `a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force`.
|
||||
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). »
|
||||
- Affirme : les trois titres, le cran 2 et son nom.
|
||||
- Tests : `every_number_of_the_quorum_texts_is_the_engine_s` (les titres des versions `seedProposal`
|
||||
du seed, `seed::SEED_PROPOSAL_LEVEL`) ; `tests/seed_proposals.rs` (ci-dessus).
|
||||
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. »
|
||||
- Affirme : trois crans lus dans le seed.
|
||||
- Test : `every_number_of_the_quorum_texts_is_the_engine_s` (`modules.title.inertia`,
|
||||
`modules.order.inertia`, chaque `inertia` du seed, toutes égales).
|
||||
|
||||
## N1.1 « Niveaux d'inertie »
|
||||
|
||||
1. « À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil
|
||||
WoT »). »
|
||||
- Affirme : qu'il y a une part, définie plus bas (phrase 4). **Rien à vérifier** à elle seule.
|
||||
2. « Chaque cran a aussi son quorum : 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é. »
|
||||
- Affirme : sous le quorum de l'article (celui de son cran, ou celui que son auteur a fixé), les
|
||||
« pour » qui passent sont le seuil du quorum, et un décompte passe dès que ses « pour » l'atteignent.
|
||||
- Test : `under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted` (sa grille : F1.2,
|
||||
phrase 12). Vu rouge sur le texte de `e7ab82f` (« sous le quorum », « si le quorum ») et sous la
|
||||
mutation 2.
|
||||
3. « La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle du
|
||||
groupe : au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand
|
||||
tout le monde vote. »
|
||||
- Affirme : la part exigée dépend de k, de T' et de la participation habituelle ; au-delà du quorum de
|
||||
l'article, elle baisse strictement à chaque votant de plus ; à W votants, M.
|
||||
- Tests : `the_share_asked_falls_beyond_the_article_s_quorum_down_to_m` (la grille T3c ; chaque
|
||||
nombre de votants de 0 à W sur W 100 et 101, ailleurs autour du quorum, de la participation
|
||||
habituelle et de la médiane, W/2, W − 1, W : 808 390 paires au-delà du quorum, strictement
|
||||
décroissantes, 767 455 en dessous, égales ; M à W ; et la part en voix entières, ⌈part × T⌉ / T,
|
||||
qui remonte de 9 sur 12 à 10 sur 13 à W 5 707) ; la dépendance à la participation habituelle :
|
||||
`the_share_of_a_level_is_asked_at_the_usual_turnout_…` (phrase 5). Vu rouge sur le texte de
|
||||
`e7ab82f` (« La part », « au-delà du quorum ») et sous la mutation 2.
|
||||
4. « `Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum de
|
||||
l'article s'il en manque. »
|
||||
- Affirme : la formule, et T' = max(T, Q) avec Q le quorum de l'article.
|
||||
- Test : `the_formulas_of_the_texts_are_those_the_engine_computes` (sa grille : F1.2, phrase 17 ;
|
||||
T' et la part, évalués tels qu'écrits, égaux au moteur). Vu rouge sous les mutations 1 et 2.
|
||||
5. « **Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum de l'article, à la
|
||||
participation habituelle, puis quand tout le monde vote, et ce qui est à ce cran au départ. »
|
||||
- Affirme : ce que disent les colonnes des cinq lignes ; la deuxième vaut au-delà du quorum de
|
||||
l'article, à la participation habituelle.
|
||||
- Test : `the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum` (la
|
||||
grille T3c, F1.2 phrase 20). Vu rouge sur le texte de `e7ab82f`.
|
||||
6. à 10. Les cinq lignes, par exemple « **Cran 1 · très basse** : quorum 12 ; 60 % à la participation
|
||||
habituelle, 50 % quand tout le monde vote. Aucun article au départ. » ; puis 2 · basse : 12 ; 67 %,
|
||||
50 % ; Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section ;
|
||||
3 · moyenne : 50 ; 75 %, 55 % ; le cran par défaut : tous les autres articles, et chaque nouvel
|
||||
article ; 4 · haute : 50 ; 83 %, 60 % ; le titre du document ; 5 · très haute : 50 ; 90 %, 66 % ;
|
||||
aucun article au départ.
|
||||
- Affirment : le quorum du cran, s_k à la participation habituelle (au-delà du quorum de l'article),
|
||||
M_k quand tout le monde vote, ce qui est au cran au départ.
|
||||
- Tests : `every_number_of_the_quorum_texts_is_the_engine_s` (chaque nombre, chaque nom de cran, ce
|
||||
qui est au départ, construits depuis le moteur et le seed) ;
|
||||
`the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum` (s_k et M_k sur la
|
||||
grille T3c) ; `the_participation_habituelle_is_the_median_brought_back_within_its_bounds` (pourquoi
|
||||
le terme défini). Vus rouges sur le texte de `e7ab82f` (« comme d'habitude ») ; le second sous la
|
||||
mutation 1.
|
||||
11. « **La participation habituelle** est la médiane du nombre de votants des propositions du groupe
|
||||
votées depuis un an. »
|
||||
- Affirme : une définition (et « un an »).
|
||||
- Tests : `every_number_of_the_quorum_texts_is_the_engine_s` (`participation::WINDOW_DAYS` = 365) ;
|
||||
`tests/inertia.rs` `the_usual_participation_is_the_median_of_five_voted_proposals`,
|
||||
`the_usual_participation_counts_the_statuses_it_names_and_only_those`,
|
||||
`a_withdrawn_proposal_leaves_the_usual_participation`.
|
||||
12. « Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de référence du
|
||||
document : 100 votants dans Monnaie libre. »
|
||||
- Affirme : moins de 5 propositions votées dans l'année, la référence 2 × 50 = 100 votants, avant les
|
||||
bornes (phrase 14).
|
||||
- Tests : `every_number_of_the_quorum_texts_is_the_engine_s` (5 = `Participation::MIN_SAMPLE`,
|
||||
100 = 2 × `voteFloor`) ; `the_participation_habituelle_is_the_median_brought_back_within_its_bounds`
|
||||
(sur chaque W et chaque médiane de la grille, 4 propositions donnent la référence) ;
|
||||
`tests/inertia.rs` `the_usual_participation_is_the_median_of_five_voted_proposals` (cinq
|
||||
propositions votées, dont une il y a plus d'un an : la référence revient) ;
|
||||
`the_reference_turnout_is_a_parameter_the_bounds_bring_back` (100 votants de W 200 à 10 000, la
|
||||
borne ailleurs). Vus rouges sur le texte de `e7ab82f` (« Tant que le groupe n'a pas voté sur 5
|
||||
propositions ») ; le dernier sous la mutation 1.
|
||||
13. « Elle ne dépend pas du quorum d'un article. »
|
||||
- Affirme : fixer le quorum d'un article ne change ni la participation habituelle ni G.
|
||||
- Tests : `tests/quorum.rs` `in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pending`
|
||||
(ajouté par T3c : I1 au quorum 20, la participation servie et G de chaque cran restent, p̃ =
|
||||
100 / W) ; `g_gives_…` (G le même à chaque quorum de la grille). Vu rouge sous la mutation 4.
|
||||
14. « Elle est ramenée entre 1 % et 50 % de l'électorat, et se mesure une fois par jour. »
|
||||
- Affirme : la valeur bornée est la participation habituelle ; 1, 50 ; une fois par jour.
|
||||
- Tests : `every_number_of_the_quorum_texts_is_the_engine_s` (`Participation::LOWEST`, `HIGHEST`,
|
||||
`participation::EVERY_HOURS` = 24) ;
|
||||
`the_participation_habituelle_is_the_median_brought_back_within_its_bounds` (sur chaque W et
|
||||
chaque médiane : la valeur est la médiane, ou la référence, ramenée ; le cas de la relecture,
|
||||
W 7 224, médiane 40 : 72,24 votants ; sur la grille, 1 175 cas où la médiane, sous 1 %, atteint le
|
||||
quorum et où la part à la médiane dépasse s, 10 270 au-dessus de 50 % où elle est sous s) ;
|
||||
`ld-server` `participation::a_measure_is_taken_again_after_a_day`. Vus rouges sur le texte de
|
||||
`e7ab82f` (« Elle reste entre ») et sous la mutation 1.
|
||||
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(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette part du
|
||||
cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W. »
|
||||
- Affirme : la formule de G ; à la participation habituelle, la courbe donne s ; la part exigée y est
|
||||
s quand elle atteint le quorum de l'article ; s est la part de la liste ; p̃ est la valeur bornée
|
||||
sur W.
|
||||
- Tests : `g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum` (la
|
||||
grille T3c : 16 985 cas au-delà, s exactement ; 68 115 en dessous, plus bas, jugés sur le quorum) ;
|
||||
`the_formulas_of_the_texts_are_those_the_engine_computes` ; `every_number_of_the_quorum_texts_is_the_engine_s`
|
||||
(60, 67, 75, 83, 90). Vu rouge sur le texte de `e7ab82f` (« quand le groupe vote comme
|
||||
d'habitude ») et sous les mutations 1 et 2.
|
||||
|
||||
## Les mutations, défaites aussitôt
|
||||
|
||||
| | Mutation | Rouges |
|
||||
|---|---|---|
|
||||
| 1 | `Participation::of` ne ramène plus la médiane dans ses bornes (`ratio = raw`) | 6 : `g_gives_…`, `the_participation_habituelle_…`, `the_formulas_…`, `the_reference_turnout_…`, `the_share_of_a_level_…`, `under_the_quorum_…` |
|
||||
| 2 | `judged_voters` sans quorum (`cast`) | 7 : `b_to_the_power_w_…`, `the_formulas_…`, `the_share_asked_falls_…`, `the_share_of_a_level_…`, `the_three_phrases_of_the_review_of_m6b_…`, `under_the_quorum_…`, `g_gives_…` |
|
||||
| 3 | un nouvel article au cran 2 (`proposals.rs`) | `tests/quorum.rs` `the_author_of_an_article_sets_its_quorum_before_the_first_vote` |
|
||||
| 4 | la participation calée sur le quorum d'un article (`docs.rs`) | `tests/quorum.rs` `in_monnaie_libre_a_quorum_holds_two_voices_…` |
|
||||
|
||||
Sur le texte de `e7ab82f`, avant le changement : 8 tests rouges, `every_number_…`, `g_gives_…`,
|
||||
`the_participation_habituelle_…`, `the_reference_turnout_…`, `the_share_asked_falls_…`,
|
||||
`the_share_of_a_level_…`, `the_texts_of_lot_t3_change_these_phrases_only`, `under_the_quorum_…`.
|
||||
|
||||
## Hors de la liste, pour information
|
||||
|
||||
- **F1.1**, non touché : « G : le gradient : plus il est grand, plus la part demandée monte quand peu de
|
||||
gens votent » n'a pas d'effet sur un article dont l'auteur a fixé le quorum à W (la part y est M à
|
||||
tout G). Un cas limite, laissé : F1.1 n'a pas d'amendement.
|
||||
- **F1.2, phrases 4 et 5** : la version retenue par une objection (ADR 0010) n'y est pas dite. Règles
|
||||
sans nombre, hors du champ de T3c ; à trancher si on veut les écrire.
|
||||
- **L'interface** (`settings.reading`, `settings.share.note`, `settings.required.usual`) dit encore
|
||||
« comme d'habitude » ; non touchée (ordre de T3c).
|
||||
@@ -101,8 +101,14 @@ Lots en cours, en copies de travail (`.claude/worktrees/agent-<id>`) :
|
||||
d'Yvv), interface, onze espaces perdus. **M6c en préparation** (`actes/2026-11-03-m6c-phrases-quorum.md`) :
|
||||
boucle verte du premier coup (301 Rust, 69 vitest, e2e 40 / 40), image à 10 h 29, répétition sur une
|
||||
copie de la production de 10 h 23 (écritures exactement annoncées) ; relecture « 84f015a : ok » inscrite
|
||||
à 10 h 47 ; **PRÊT M6c envoyé à orch à 10 h 48**.
|
||||
- **U2** (après T2, défaut retenu par orch le 03/11 à 09 h 45) : les mineurs de la relecture de M7.
|
||||
à 10 h 47 ; PRÊT M6c envoyé à orch à 10 h 48 ; **en ligne** (pipeline 40, `success` à 11 h 05).
|
||||
- **U2** (après T2, défaut retenu par orch le 03/11 à 09 h 45) : les mineurs d'interface des relectures
|
||||
de M7 et de M6c. **Lancé le 03/11 à 11 h 08** ; **fini et fusionné à 11 h 39** (`6f6197c`) : neuf
|
||||
points, C38 et M2 vus rouges, boucle verte du premier coup (302 Rust, 78 vitest, e2e 42 / 42). **M7b en
|
||||
préparation** (`actes/2026-11-03-m7b-lecteur-mineurs.md`) : image à 11 h 45 ; répétition sur une copie
|
||||
de la production de 11 h 40 (0 écriture) ; recette écran des points visibles ; relecture neuve lancée
|
||||
à 11 h 51, « 6f6197c : ok » inscrite à 12 h 06 ; PRÊT M7b envoyé à orch à 12 h 07 ; **en ligne** (pipeline 41,
|
||||
`success` à 12 h 23).
|
||||
|
||||
**M6 en ligne** depuis le 03/11 à 07 h 03 (`94ddb17`, pipeline 37) : H, S et V. Production `94ddb175` ;
|
||||
`main` et `chantier/acte-certification` poussés vers origin par orch. Le dépôt des textes reste à `9e0f45a`
|
||||
@@ -114,8 +120,75 @@ par orch. Copies retirées (orch 08 h 13, intégrateur 08 h 14).
|
||||
**M7 en ligne** depuis le 03/11 à 09 h 42 (`b2e597b`, pipeline 39) : lot U, le lecteur refait.
|
||||
Production `b2e597b6` ; `main` et `chantier/acte-certification` poussés vers origin par orch. Copies
|
||||
retirées (orch, puis l'intégrateur à 09 h 45).
|
||||
Suite : **T2** (en cours depuis 09 h 45, M6c), puis **U2** (les mineurs de M7), un sous-agent à la fois ;
|
||||
PRÊT et actes par orch.
|
||||
**M6c en ligne** depuis le 03/11 à 11 h 05 (`84f015a`, pipeline 40) : lot T2, les trois phrases de
|
||||
F1.1, F1.2 et N1.1 justes. Production `84f015ad` ; `main` et le chantier poussés vers origin par orch.
|
||||
Copies retirées (orch, puis l'intégrateur à 11 h 07).
|
||||
**M7b en ligne** depuis le 03/11 à 12 h 23 (`6f6197c`, pipeline 41) : lot U2, les mineurs du lecteur.
|
||||
Production `6f6197cc` ; `main` et le chantier poussés vers origin par orch. Copies de production retirées
|
||||
(orch ; l'intégrateur à 12 h 28).
|
||||
**Les lots H, S, V, T, U, T2, U2, T3, T3b et T3c sont en ligne.** **Lot T3 en cours** depuis le 03/11 à 17 h 02
|
||||
(ordre T3 d'orch, sur la réponse d'Yvv avant 16 h 56 : « Je valide textes-quorum-seed (dont le point 3,
|
||||
pas de chiffre datés dans un texte voté). Tu peux lancer la livraison de decision. ») : l'exemple chiffré
|
||||
de N1.1 sort des textes votés, la phrase « pas un nombre minimal de votants » réécrite vraie, « comme
|
||||
d'habitude » au-delà du quorum (N1.1, F1.2), F1.1 laissé ; amendements gardés qui suivent T2.
|
||||
**Fusionné** le 03/11 à 17 h 44 (`0b4923f`, `2ce2028`). Relecture de `2ce2028` : à reprendre (18 h 12, la
|
||||
phrase de G de N1.1) → **lot T3b**, fusionné à 18 h 47 (`e7ab82f`) ; boucle verte (18 h 47 - 18 h 54), répétition
|
||||
réseau coupé (18 h 54), image (18 h 56 - 19 h 01). Relecture de `e7ab82f` : **à reprendre** (19 h 16 : « comme
|
||||
d'habitude » se lit comme la médiane réelle, le moteur prend la participation bornée) → **lot T3c**, fusionné à 19 h 57
|
||||
(`de1b7eb`) : le terme défini « à la participation habituelle » partout, « sous le quorum de l'article », la
|
||||
grille des tests étendue, la liste fermée des phrases (`t3c-liste-fermee.md`, demandée par orch) ; boucle verte
|
||||
(19 h 57 - 20 h 03), répétition réseau coupé (20 h 03). Image (20 h 05 - 20 h 10). **Relecture de `de1b7eb` : ok** (inscrite à
|
||||
20 h 23). **M8 en ligne** depuis le 03/11 à 20 h 31 (`de1b7eb`, pipeline 42 `success` à 20 h 42 ; acte exécuté par
|
||||
orch de 20 h 26 à 20 h 44, `actes/2026-11-03-m8-chiffres-dates.md`) : F1.2 et N1.1 en révision 5 dans les deux
|
||||
groupes, le reste identique ; copies retirées (orch à 20 h 43, l'intégrateur à 20 h 45). **Demande d'Yvv du 03/11 vers 20 h 38** (dans la session de l'intégrateur, relayée à orch) : I1, E1, E2
|
||||
réécrits ; un article nouveau « Responsabilité civique » entre E1 et E2 ; les sections formule, inertie et
|
||||
ordonnancement sortent du vote (« infos de fonctionnement ») et vont dans l'interface ; un petit chapitre
|
||||
« Info ouverture doc de réf. » (réglages de l'auteur : quorum, inertie) ; « Propositions d'amorce » ; les
|
||||
titres « Introduction » et « Engagements fondamentaux » retirés ; une séparation avant le texte. **Lot T4**
|
||||
(textes et structure) **fusionné** (`590ccd7`, boucle du lot verte sur `3996f6f` ; boucle de l'intégrateur
|
||||
verte sur `590ccd7`, 21 h 56 - 22 h 02 : fmt, clippy, 335 tests, check 0 / 0, 78 vitest, build, e2e 42 / 42) :
|
||||
I1, E1, E2 aux textes d'Yvv ; E12 « Responsabilité civique » entre E1 et E2 ; formule, inertie,
|
||||
ordonnancement retirés sans rien effacer (migration 0012 additive, `units.retired_at`, ADR 0014) ; le retour
|
||||
sur l'image de `de1b7eb` rend les sept articles. Fichier de relecture pour Yvv :
|
||||
`docs/chantiers/acte-certification/textes-amorce.md` (huit choix à confirmer en tête). **Lot U3**
|
||||
(interface) **fusionné** à 22 h 40 (`a47aed0` ; boucle du lot verte sur `d15d46e` : 336 tests, check 0 / 0,
|
||||
113 vitest, build, e2e 47 / 47, 45 contrôles C1 à C43, M1, M2) : « Info ouverture doc de réf. » avant le
|
||||
texte, hors vote ; une séparation ; plus les titres « Introduction » et « Engagements fondamentaux » ; les
|
||||
informations de formule, inertie, ordonnancement dans le vote déplié et le panneau des réglages (inventaire
|
||||
`U3-informations.md`) ; « Proposition d'amorce » ; le mot d'Yvv pour qui n'est pas invité ; réordonner sur
|
||||
plusieurs lignes corrigé (la cause : la mesure des lignes pendant `animate:flip`) ; API : `seedFeatured`.
|
||||
Captures : `/tmp/ld-u3-shots/`. **Lot U3b fusionné** à 23 h 00 (`d66823b`, boucle du lot verte sur `a6f5e22` : 336 tests,
|
||||
113 vitest, e2e 48 / 48, C44 vu rouge avant) : le tableau des cinq crans se lit en entier dans le panneau des
|
||||
réglages, sur bureau comme sur téléphone, en fr, en, es. **M9 sur `d66823b`** : répétition faite sur la
|
||||
copie de production d'orch de 22 h 41 (22 h 45 - 22 h 47, réseau coupé ; aucune ligne supprimée ; retour
|
||||
sur `de1b7eb` rejoué) ; boucle verte (23 h 00 - 23 h 07), image (23 h 07 - 23 h 11), recette écran sur la copie de production après
|
||||
M9 ; **relecture de `d66823b` : à reprendre** (inscrite à 23 h 25 ; aucun bloquant ; une phrase de
|
||||
`formula.reading`, « quand tout le monde vote, la majorité M suffit », fausse pour W ≤ 15) → **lot U3c fusionné**
|
||||
à 23 h 49 (`fadfff5` ; boucle du lot verte sur `266a443` : 338 tests, 116 vitest, e2e 48 / 48 ; la phrase
|
||||
« quand tout le monde vote, la part exigée est la majorité M », vraie pour tout W, grille `ld-engine` de W = 1
|
||||
à 10 000 000 ; les raisons de la file en « Proposition d'amorce ») ; **M9 sur `fadfff5`** : répétition
|
||||
réseau coupé (23 h 50 - 23 h 52 ; aucune ligne supprimée ; retour sur `de1b7eb` rejoué), boucle verte (23 h 52
|
||||
- 23 h 58 : 338 tests, 116 vitest, e2e 48 / 48), image (23 h 58 - 00 h 12) ; **relecture du delta
|
||||
`d66823b..fadfff5` : ok** (inscrite le 04/11 à 00 h 14) ; **dossier figé le 04/11 à 00 h 15** ; **M9 en ligne** depuis le
|
||||
04/11 (acte exécuté par orch de 00 h 16 à 00 h 39, pipeline 43 `success` à 00 h 34 ; santé `fadfff55`) ; dossier
|
||||
`actes/2026-11-03-m9-amorce-hors-vote.md` (préparé, non figé). Mise en ligne prévue :
|
||||
un seul acte **M9** pour T4 et U3 (T4 seul retirerait les informations avant que l'interface ne les dise) —
|
||||
accord d'orch à 21 h 58, à trois conditions pour figer : une relecture neuve de tout `27995b9..<tête>` (le
|
||||
retrait « tous ou aucun » et sa garde, la migration 0012 additive, le retour sur `de1b7eb` qui rend les sept
|
||||
articles, les textes d'Yvv mot pour mot sauf « n'y a », la recette écran de U3) ; le dossier, avec en tête
|
||||
chaque texte avant / après et la preuve chiffrée du retrait table par table sur la copie fraîche, retour
|
||||
rejoué ; la copie fraîche demandée à orch une fois U3 fusionné. Orch pousse le chantier en une fois avec M9. Défauts appliqués, questions à Yvv : « il y a une et une seule clé » ; I1 en trois paragraphes ;
|
||||
« Responsabilité civique » Proposition d'amorce au cran 2 ; « starter proposal » / « propuesta de arranque » ;
|
||||
retrait sans rien effacer (« déploie ») ; le chapitre hors vote, titré comme Yvv l'écrit. Ajout d'Yvv avant
|
||||
20 h 44, pour U3 : un texte pour qui n'est pas encore invité (« 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. ») ; défaut « à Chiara ou à Yvv », sur l'accueil sans session et la page « Se connecter ». Listes d'attente : ci-dessous (Actes). Bases de test restées sous `/tmp`, toutes synthétiques,
|
||||
aucune de la production : **purgées** sur ordre d'orch (PURGE-LD, 03/11 à 12 h 31), le 03/11 à 12 h 30-12 h 31 :
|
||||
les neuf dossiers des sous-agents (`ld-relis-m6`, `-m7`, `-m6c`, `-m7b`, `ld-u2-data`, `ld-u-data`,
|
||||
`ld-h-data`, `ld-u-shots`, `ld-u2-shots`), 688 restes des tests cargo et scripts e2e des lots ; les sept
|
||||
copies de travail des lots fusionnés (sans `--force`, aucun refus) et leurs 13 branches (`git branch -d`,
|
||||
aucune poussée de suppression). Restent `/tmp/ld-e2e`, son verrou et `/tmp/ld-h-shots` (captures du lot
|
||||
H, hors de l'ordre).
|
||||
|
||||
Yvv relit les quatre textes en ligne. Au prochain tour, avec ses retours :
|
||||
- N1.1 : G se calcule sur p̃ rapporté à W ;
|
||||
@@ -187,6 +260,8 @@ chaîne (question stratégique, `docs/recherche/vote-g1-chaine.md`) ; l'affichag
|
||||
| Q12 | En production, Yvv a ouvert le 29/10 deux propositions d'inertie, I1 → 5 et E1 → 5 (0 vote). Ouvertes au démarrage de M6, elles retiennent I1 et E1 aux crans 3 et 4 pour de bon (amendement décidé une fois). Les retirer ? | **tranché** (03/11, formulaire d'orch, reçu à 05 h 51) : « Oui, retire-les » → avant le démarrage de M6, par le geste d'Yvv à l'écran (« Les miennes » › la ligne d'inertie › « Coquilles, traductions, objections, historique » › « Retirer ma proposition »), vérifié sur `3f10a0e` en local ; fait par Yvv à l'écran (annoncé à 06 h 07), dossier `actes/2026-11-03-m6-propositions-seed-quorum-tampon.md` |
|
||||
| Q13 | Les textes du quorum (F1.1, F1.2, N1.1 ; `textes-quorum-seed.md`) : annoncer l'intention de remonter l'inertie ? « Propositions seed » dans un texte voté ? garder l'exemple 7 224 / 120 ? nommer le groupe d'essai ? | **défauts appliqués** : non ; « Propositions seed » ; garder ; ne pas le nommer. Posé à orch le 03/11 à 07 h 34, pour Yvv à son retour |
|
||||
| Q14 | La relecture de `bb4bdf5` (M6b) trouve trois phrases inexactes : F1.2 « ensuite, il ne change plus » (faux après l'acte « supprime ») ; F1.1 « presque l'unanimité quand peu de gens votent » (faux aux crans 1 à 3 avec le quorum : 9 « pour » sur 12 suffisent au cran 2) ; N1.1, première phrase (« un quorum de votants » lu comme un minimum). Les corriger par un lot T2 (amendement gardé qui suit celui de M6b, M6c), possible tant que personne n'a voté sur l'article ? | **tranché par orch** (03/11, reçu à 08 h 07) : T2 après U, un sous-agent à la fois (risque faible : une seule identité en production) ; les phrases inexactes vont dans la liste d'Yvv avec la proposition. Posé à orch le 03/11 à 07 h 56 |
|
||||
| Q15 | Les textes de l'Acte d'engagement (demande d'Yvv du 03/11 vers 20 h 38) : les choix faits en les reprenant | **défauts appliqués** (T4, U3) : « il y a une et une seule clé certifiante » (« n'y a » lu comme une coquille) ; I1 en trois paragraphes ; tes virgules gardées ; « ces trois principes » = E1, E12, E2 ; E12 Proposition d'amorce au cran 2 entre E1 et E2 ; titres gardés ; traductions au vouvoiement ; rien d'effacé (« déploie ») ; le chapitre hors vote, titré « Info ouverture doc de réf. » ; « Proposition d'amorce », « Starter proposal », « Propuesta de arranque » ; « @ chiara » lu « à Chiara ». Relecture : `textes-amorce.md`, ouvert chez Yvv le 03/11 vers 21 h 55 |
|
||||
| Q16 | Le paragraphe d'Yvv du chapitre, une fois un article en vigueur : sa première phrase (« Aucun article n'est encore en vigueur. ») disparaît, et « y entrent par le vote » n'a plus rien à reprendre | **défaut appliqué** (U3) : « Les Propositions d'amorce entrent en vigueur par le vote, comme les propositions qui suivent, … » (clé `opening.amorce.after`) ; dans Monnaie libre, rien n'est en vigueur : la phrase d'Yvv s'affiche telle quelle |
|
||||
|
||||
Défauts posés par orch, sans question (Yvv peut les corriger) :
|
||||
- Décisions : « modules restant à voter » = les modules du doc pas encore adoptés par un vote
|
||||
@@ -211,10 +286,62 @@ Défauts posés par orch, sans question (Yvv peut les corriger) :
|
||||
|
||||
- M7 (lot U, le lecteur) : **exécuté** le 03/11/2026 par orch sous mandat, en ligne (pipeline 39 `success` à 09 h 42) ; santé `b2e597b6` ; redémarrage à froid sans rejeu ; base identique à la ligne près ; dépôt des textes à `9e0f45a` ; image de retour `previous` = `bb4bdf5`. Copies retirées : orch, puis l'intégrateur à 09 h 45. Figé le 03/11 à 09 h 28 sur `b2e597b` (`actes/2026-11-03-m7-lecteur.md`) ; interface seule, aucune écriture en base ; répétition et recette écran sur une copie de la production de 08 h 49 ; relecture « à reprendre » (le dossier) puis « b2e597b : ok » ; PRÊT M7 envoyé à orch à 09 h 29. À dire à Yvv : « Fixer ce quorum » visible pour l'administrateur sur les 33 articles du seed.
|
||||
|
||||
- M6c (lot T2, trois phrases) : **figé** le 03/11 à 10 h 48 sur `84f015a` (`actes/2026-11-03-m6c-phrases-quorum.md`) ; répétition sur une copie de la production de 10 h 23 ; boucle verte, image à 10 h 29 ; relecture « 84f015a : ok » inscrite à 10 h 47 ; PRÊT M6c envoyé à orch à 10 h 48.
|
||||
- M6c (lot T2, trois phrases) : **exécuté** le 03/11/2026 par orch sous mandat (pipeline 40 `success` à 11 h 05) ; santé `84f015ad` ; six amendements appliqués, redémarrage à froid sans rejeu ; F1.1 en révision 3, F1.2 et N1.1 en révision 4, le reste identique ; dépôt des textes à `9e0f45a` ; image de retour `previous` = `b2e597b`. Copies retirées : orch, puis l'intégrateur à 11 h 07. Figé le 03/11 à 10 h 48 sur `84f015a` (`actes/2026-11-03-m6c-phrases-quorum.md`) ; répétition sur une copie de la production de 10 h 23 ; boucle verte, image à 10 h 29 ; relecture « 84f015a : ok » inscrite à 10 h 47 ; PRÊT M6c envoyé à orch à 10 h 48.
|
||||
|
||||
Restes mineurs de `71819f9` et du lot B : faits dans le lot D.
|
||||
|
||||
- M7b (lot U2, les mineurs du lecteur) : **exécuté** le 03/11/2026 par orch sous mandat, 12 h 07 – 12 h 26 (pipeline 41 `success` à 12 h 23) ; santé `6f6197cc` ; redémarrage à froid propre ; comptes identiques ; dépôt des textes à `9e0f45a` ; image de retour `previous` = `84f015a`. Copies retirées : orch, puis l'intégrateur à 12 h 28. Figé le 03/11 à 12 h 07 sur `6f6197c` (`actes/2026-11-03-m7b-lecteur-mineurs.md`) ; interface seule, aucune écriture en base ; répétition et recette sur une copie de la production de 11 h 40 ; relecture « 6f6197c : ok » inscrite à 12 h 06 ; PRÊT M7b envoyé à orch à 12 h 07.
|
||||
|
||||
- M8 (lots T3, T3b, T3c : aucun chiffre daté dans les textes votés) : **exécuté** le 03/11/2026 par orch sous mandat, 20 h 26 – 20 h 44 (pipeline 42 `success` à 20 h 42) ; santé `de1b7eb5` ; quatre amendements appliqués, redémarrage à froid sans rejeu ; dépôt des textes à `9e0f45a` ; image de retour `previous` = `6f6197c`. Copies retirées : orch à 20 h 43, l'intégrateur à 20 h 45. Figé le 03/11 à 20 h 23 sur `de1b7eb` (`actes/2026-11-03-m8-chiffres-dates.md`) ; F1.2 et N1.1 réécrits par les amendements gardés `2026-11-03-f1-2-chiffres` et `-n1-1-chiffres` (révision 4 → 5, deux groupes), F1.1 inchangé ; répétition réseau coupé sur une copie de la production de 17 h 46 (20 h 03) ; boucle verte, image à 20 h 10 ; relectures « 2ce2028 : à reprendre » (18 h 12), « e7ab82f : à reprendre » (19 h 16), « de1b7eb : ok » (20 h 23) ; ACTE M8 envoyé à orch à 20 h 25.
|
||||
|
||||
Liste d'attente (relectures de M8 ; aucun ne rend une phrase fausse ; après la mise en ligne, un changement de texte demande un nouvel amendement gardé, possible tant qu'aucun vote n'existe sur l'article) :
|
||||
- F1.2, « au-delà du quorum » sans « de l'article » (vrai au défaut ; à aligner sur N1.1) ;
|
||||
- F1.1, « le nombre de voix pour qu'il faut » se lit d'abord « pour qu' » ; « quorum de référence » défini nulle part ;
|
||||
- un quorum fixé à W : deux phrases sans objet (F1.1, G ; N1.1, « dépend du nombre de votants ») ;
|
||||
- en, « It is kept between » : « is brought back within » serait plus fidèle ;
|
||||
- F1.2 ne dit pas la version retenue par une objection (ADR 0010) ; F1.1, B^W compte dans le groupe d'essai (points 4 et 10) ;
|
||||
- une proposition rivale d'un membre ne bloque pas la garde des amendements (conception depuis le lot T ; aucune en production) ;
|
||||
- l'interface (`settings.*`) dit encore « comme d'habitude ».
|
||||
|
||||
- M9 (lots T4, U3, U3b, U3c : l'Acte d'engagement d'Yvv, les informations de fonctionnement hors vote) :
|
||||
**figé** le 04/11 à 00 h 15 sur `fadfff5` (`actes/2026-11-03-m9-amorce-hors-vote.md`) ; relectures
|
||||
« d66823b : à reprendre » (03/11, 23 h 25) puis « fadfff5 : ok » (04/11, 00 h 14) ; répétition réseau coupé
|
||||
sur la copie de production d'orch de 22 h 41 ; **exécuté** le 04/11 par orch sous mandat, 00 h 16 – 00 h 39
|
||||
(pipeline 43 `success` à 00 h 34) ; santé `fadfff55` ; six amendements, E12 ajouté, dix unités retirées par
|
||||
groupe, redémarrage à froid sans rejeu ; la table de la répétition retrouvée ligne pour ligne, aucune ligne
|
||||
supprimée (seul écart : `settings`, W relu au démarrage, même valeur) ; dépôt des textes à `9e0f45a` ; image
|
||||
de retour `previous` = `de1b7eb`. Copies retirées : orch à 00 h 38, l'intégrateur à 00 h 41.
|
||||
- Mineurs de la relecture de `fadfff5` (liste d'attente) : hors de la toile, l'écran ne dit pas que B^W peut
|
||||
demander une voix de plus dans un petit groupe ; « la part de « pour » demandée » puis « la part exigée »
|
||||
dans la même phrase de `formula.reading`.
|
||||
|
||||
Liste d'attente (relecture de `d66823b`, M9 ; aucun ne touche la production d'aujourd'hui) :
|
||||
- la garde de réécriture d'I1, E1, E2 ne lit que la Proposition d'amorce (un quorum fixé, un texte rival, un
|
||||
vote sur une proposition d'inertie retirée la laissent passer ; la garde du retrait compte le quorum fixé) ;
|
||||
- `inertia.explain`, première phrase (« plus il faut de voix ») : une tendance, pas une règle ;
|
||||
- `vote.others.rule` ne dit pas qu'une version retenue par une objection n'entre pas en vigueur ;
|
||||
- après un retour, une trace laissée sur F1 à O1 n'empêche pas le repassage de les tenir retirées (ADR 0014) ;
|
||||
- I2 « Les deux garanties réciproques » redit la première phrase d'I1 (à montrer à Yvv, rien à changer sans lui) ;
|
||||
- `deep.note` (« Propositions en cours », en ligne depuis M7) : le seuil « la part de voix pour que demande son
|
||||
inertie, comptée sur au moins son quorum », inexact d'une voix pour W ≤ 15 (B^W) ; vu par le lot U3c ;
|
||||
- les trois lignes de file de M6 en production disent « Proposition seed » : elles iront dans le premier
|
||||
commit public avec celles de M9 (donnée, pas touchée).
|
||||
|
||||
Liste d'attente (orch, 03/11 à 20 h 47 : les restes des tests sous `/tmp`) :
|
||||
- les tests Rust laissent sous `/tmp` des centaines de dossiers `ld-*` (568 le 03/11 à 20 h 47 : 563 copies du
|
||||
seed `acte-engagement-certification.json` des tests de `seed_quorum_texts.rs`, `objections.rs`, `api.rs`, un
|
||||
WAL orphelin `ld-lot-f-twice-*.db-wal`/`-shm`), **tous synthétiques** (identités `dev:` seulement, aucune de
|
||||
la production, vérifié) ; correctif : chaque test écrit sous un dossier temporaire à lui (`tempfile`) et le
|
||||
retire, même en échec ; puis retrait des restes sur ordre d'orch.
|
||||
|
||||
Liste d'attente (relecture de `6f6197c`, M7b ; pas de lot sans mot d'orch) :
|
||||
- **le vote d'un changement d'inertie de module ne dit pas le cran visé** (seulement le cran où il se
|
||||
vote ; « vers N / 5 » dans l'étiquette pour lecteur d'écran seulement : `VoteFold.svelte:167`, `:177`) ;
|
||||
la pastille de U2 y mène directement ; aucun changement de ce type ouvert en production ;
|
||||
- l'en-tête ne suit la liste qu'en lecture « En vigueur » (`+page.svelte:159-160`) ;
|
||||
- commentaire du test moteur (`threshold.rs`, « 85 % of the quorum ») : les paramètres de référence, pas
|
||||
le cran 2.
|
||||
|
||||
Liste d'attente (relecture de `84f015a`, M6c) :
|
||||
- **pour Yvv** : N1.1 « qui n'est pas un nombre minimal de votants » est vrai pour les quorums par
|
||||
cran, faux pour un quorum d'auteur bas (2 ou 3 à tout cran, 6 au cran 3…) où le moteur demande Q
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
# Textes d'amorce et structure de l'Acte d'engagement (lot T4)
|
||||
|
||||
> Préparé le 2026-11-03 pour ta relecture, Yvv (lot T4, branche `lot/t4-amorce`), d'après ta
|
||||
> demande du 03/11 vers 20 h 38 : « Modif des textes Acte engagement » (I1, E1, un article
|
||||
> nouveau « Responsabilité civique », E2) et « les 3 derniers modules ne sont pas de votes :
|
||||
> ordonnancement, inertie, et formule de vote sont des infos de fonctionnement […] Vire les 3 du
|
||||
> bas de la liste. » Les textes sont dans `seeds/acte-engagement-certification.json` et dans des
|
||||
> amendements gardés pour la production ; la décision durable est l'ADR 0014. **Rien n'est en
|
||||
> ligne.** L'interface des informations de fonctionnement est le lot U3.
|
||||
|
||||
## À confirmer : les choix faits pour toi
|
||||
|
||||
Chacun est appliqué ; dis lequel tu changes.
|
||||
|
||||
1. **« il y a une et une seule clé certifiante ».** Ta phrase disait « derrière un être humain,
|
||||
il n'y a une et une seule clé certifiante ». Lu comme une coquille, sur le parallèle avec
|
||||
« il y a un et un seul être humain vivant ». L'autre lecture serait « il n'y a qu'une seule
|
||||
clé » (le texte d'avant).
|
||||
2. **I1 en trois paragraphes**, le premier en italique entre « », comme aujourd'hui, avec les
|
||||
espaces insécables du texte d'avant (à l'intérieur des guillemets, devant le point-virgule).
|
||||
3. **Tes virgules, gardées telles quelles** : « de nature contractuelle, qui fonde l'usage »
|
||||
(I1) et « qui s'engagent elles-mêmes, à scrupuleusement respecter » (E2). La seconde sépare
|
||||
le verbe de son complément ; sans elle : « qui s'engagent elles-mêmes à scrupuleusement
|
||||
respecter ». En anglais et en espagnol, la première est rendue, la seconde non (elle n'y
|
||||
aurait pas de sens).
|
||||
4. **Le deuxième paragraphe d'I1** est le tien mot pour mot : « C'est l'unique enjeu de la
|
||||
certification et de votre engagement, il donne à notre toile fiduciaire sa valeur : … ».
|
||||
5. **« ces trois principes fondamentaux »** (E2) se lit, comme « ces deux présents engagements »
|
||||
avant, sur les articles de la section : E1, E12 et E2.
|
||||
6. **L'article nouveau** : code **E12**, Proposition seed (ouverte, mise en avant, 0 vote), au
|
||||
**cran 2** comme I1, E1 et E2, section « fondamental », **entre E1 et E2**. Titres : « Civic
|
||||
responsibility », « Responsabilidad cívica ». Le code : E3 à E11 existent, E12 est le premier
|
||||
libre de la famille des engagements ; c'est un identifiant, la place vient de l'ordre de la
|
||||
section (ADR 0009), pas du numéro. Aucune unité ne porte E12, en production non plus (les
|
||||
articles de membre prennent des codes en N).
|
||||
7. **Les titres restent** : « Raison d'être », « Unicité du compte », « Réciprocité ».
|
||||
8. **Les traductions** : l'anglais britannique et les guillemets “ ” des textes d'aujourd'hui ;
|
||||
l'espagnol avec «» serrés. « votre engagement » est rendu « your commitment » et « su
|
||||
compromiso » (le vouvoiement, comme le français). « l'unique enjeu » : « the one thing at
|
||||
stake », « lo único que está en juego ».
|
||||
9. **En production, rien n'est effacé** (ADR 0014) : les sept articles et les trois ordres
|
||||
sortent du document par une marque (`units.retired_at`), et seulement si personne n'y a
|
||||
laissé de trace. L'ordre de « fondamental » prend E12 entre E1 et E2 tant que personne n'a
|
||||
voté sur cet ordre ; sinon E12 va en fin de section, comme tout nouvel article.
|
||||
|
||||
## Le document après T4
|
||||
|
||||
Cinq sections, 27 articles : introduction (I1, I2) ; fondamental (T1, E1, **E12**, E2) ;
|
||||
technique (T2, E3 à E7, T3, E8 à E11) ; conclusion (K1, K2) ; annexe (X1 à X2.3). Quatre Propositions
|
||||
seed au cran 2 : I1, E1, E12, E2. Aucun article en vigueur au départ (ADR 0011). « Modules à
|
||||
voter » : 33 (27 articles, le titre, l'ordre de 5 sections), contre 41 avant.
|
||||
|
||||
## Les textes, avant et après
|
||||
|
||||
Les blocs sont les textes exacts du seed (Markdown : `*…*` est l'italique), espaces insécables compris. « Avant » : le texte en ligne aujourd'hui, celui du seed de M8.
|
||||
|
||||
### I1 — Raison d'être
|
||||
|
||||
#### I1 · Français (fait foi)
|
||||
|
||||
Titre : **Raison d'être**
|
||||
|
||||
Avant :
|
||||
|
||||
```markdown
|
||||
*« Derrière une clé il n'y a qu'un seul être humain vivant ; derrière un être humain, il n'y a qu'une seule clé certifiante. »*
|
||||
|
||||
C'est le seul et unique propos de la certification, qui donne à notre toile fiduciaire sa valeur. Nous pouvons nous fier à l'unicité et l'intégrité de tout geste signé par cette clé. C'est l'enjeu de la certification.
|
||||
|
||||
Cet acte d'engagement est par ailleurs le seul acte de nature contractuelle qui fonde l'usage de la première monnaie libre.
|
||||
```
|
||||
|
||||
Après :
|
||||
|
||||
```markdown
|
||||
*« Derrière une clé il y a un et un seul être humain vivant ; derrière un être humain, il y a une et une seule clé certifiante. »*
|
||||
|
||||
C'est l'unique enjeu de la certification et de votre engagement, il donne à notre toile fiduciaire sa valeur : pouvoir nous fier à l'unicité et l'intégrité de tout geste signé par cette clé.
|
||||
|
||||
Cet acte d'engagement est par ailleurs le seul acte de nature contractuelle, qui fonde l'usage de la première monnaie libre.
|
||||
```
|
||||
|
||||
#### I1 · English
|
||||
|
||||
Titre : **Purpose**
|
||||
|
||||
Avant :
|
||||
|
||||
```markdown
|
||||
*“Behind a key there is only one living human being; behind a human being, there is only one certifying key.”*
|
||||
|
||||
This is the one and only purpose of certification, which gives our fiduciary web its value. We can rely on the uniqueness and integrity of every action signed with this key. That is what is at stake in certification.
|
||||
|
||||
This commitment act is, moreover, the only act of a contractual nature on which the use of the first libre currency is founded.
|
||||
```
|
||||
|
||||
Après :
|
||||
|
||||
```markdown
|
||||
*“Behind a key there is one and only one living human being; behind a human being, there is one and only one certifying key.”*
|
||||
|
||||
This is the one thing at stake in certification and in your commitment; it gives our fiduciary web its value: being able to rely on the uniqueness and integrity of every action signed with this key.
|
||||
|
||||
This commitment act is, moreover, the only act of a contractual nature, which founds the use of the first libre currency.
|
||||
```
|
||||
|
||||
#### I1 · Español
|
||||
|
||||
Titre : **Razón de ser**
|
||||
|
||||
Avant :
|
||||
|
||||
```markdown
|
||||
*«Detrás de una clave no hay más que un solo ser humano vivo; detrás de un ser humano, no hay más que una sola clave certificadora.»*
|
||||
|
||||
Este es el propósito único y exclusivo de la certificación, que da su valor a nuestra red fiduciaria. Podemos fiarnos de la unicidad y la integridad de toda acción firmada con esta clave. Eso es lo que está en juego en la certificación.
|
||||
|
||||
Esta acta de compromiso es, además, el único acto de naturaleza contractual en el que se funda el uso de la primera moneda libre.
|
||||
```
|
||||
|
||||
Après :
|
||||
|
||||
```markdown
|
||||
*«Detrás de una clave hay un solo y único ser humano vivo; detrás de un ser humano, hay una sola y única clave certificadora.»*
|
||||
|
||||
Es lo único que está en juego en la certificación y en su compromiso; da su valor a nuestra red fiduciaria: poder fiarnos de la unicidad y la integridad de toda acción firmada con esta clave.
|
||||
|
||||
Esta acta de compromiso es, además, el único acto de naturaleza contractual, que funda el uso de la primera moneda libre.
|
||||
```
|
||||
|
||||
### E1 — Unicité du compte
|
||||
|
||||
#### E1 · Français (fait foi)
|
||||
|
||||
Titre : **Unicité du compte**
|
||||
|
||||
Avant :
|
||||
|
||||
```markdown
|
||||
Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte cocréateur de monnaie G1.
|
||||
```
|
||||
|
||||
Après :
|
||||
|
||||
```markdown
|
||||
Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte certifiant.
|
||||
```
|
||||
|
||||
#### E1 · English
|
||||
|
||||
Titre : **Uniqueness of the account**
|
||||
|
||||
Avant :
|
||||
|
||||
```markdown
|
||||
I commit on my honour to have, now and ever, no more than one single and unique account co-creating G1 money.
|
||||
```
|
||||
|
||||
Après :
|
||||
|
||||
```markdown
|
||||
I commit on my honour to have, now and ever, no more than one single and unique certifying account.
|
||||
```
|
||||
|
||||
#### E1 · Español
|
||||
|
||||
Titre : **Unicidad de la cuenta**
|
||||
|
||||
Avant :
|
||||
|
||||
```markdown
|
||||
Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta cocreadora de moneda G1.
|
||||
```
|
||||
|
||||
Après :
|
||||
|
||||
```markdown
|
||||
Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta certificadora.
|
||||
```
|
||||
|
||||
### E12 — Responsabilité civique (nouveau)
|
||||
|
||||
#### E12 · Français (fait foi)
|
||||
|
||||
Titre : **Responsabilité civique**
|
||||
|
||||
```markdown
|
||||
Je déclare sur l'honneur avoir la capacité légale et physique de signer un contrat et d'en assumer la responsabilité civique, pour certifier à mon tour.
|
||||
```
|
||||
|
||||
#### E12 · English
|
||||
|
||||
Titre : **Civic responsibility**
|
||||
|
||||
```markdown
|
||||
I declare on my honour that I have the legal and physical capacity to sign a contract and to assume its civic responsibility, in order to certify in my turn.
|
||||
```
|
||||
|
||||
#### E12 · Español
|
||||
|
||||
Titre : **Responsabilidad cívica**
|
||||
|
||||
```markdown
|
||||
Declaro por mi honor tener la capacidad legal y física de firmar un contrato y de asumir su responsabilidad cívica, para certificar a mi vez.
|
||||
```
|
||||
|
||||
### E2 — Réciprocité
|
||||
|
||||
#### E2 · Français (fait foi)
|
||||
|
||||
Titre : **Réciprocité**
|
||||
|
||||
Avant :
|
||||
|
||||
```markdown
|
||||
Je m'engage sur l'honneur à ne certifier que des personnes physiques qui respectent scrupuleusement ces deux présents engagements fondamentaux.
|
||||
```
|
||||
|
||||
Après :
|
||||
|
||||
```markdown
|
||||
Je m'engage sur l'honneur à ne certifier que des personnes physiques qui s'engagent elles-mêmes, à scrupuleusement respecter ces trois principes fondamentaux.
|
||||
```
|
||||
|
||||
#### E2 · English
|
||||
|
||||
Titre : **Reciprocity**
|
||||
|
||||
Avant :
|
||||
|
||||
```markdown
|
||||
I commit on my honour to certify only natural persons who scrupulously respect these two fundamental commitments.
|
||||
```
|
||||
|
||||
Après :
|
||||
|
||||
```markdown
|
||||
I commit on my honour to certify only natural persons who themselves commit to scrupulously respecting these three fundamental principles.
|
||||
```
|
||||
|
||||
#### E2 · Español
|
||||
|
||||
Titre : **Reciprocidad**
|
||||
|
||||
Avant :
|
||||
|
||||
```markdown
|
||||
Me comprometo por mi honor a certificar únicamente a personas físicas que respeten escrupulosamente estos dos compromisos fundamentales.
|
||||
```
|
||||
|
||||
Après :
|
||||
|
||||
```markdown
|
||||
Me comprometo por mi honor a certificar únicamente a personas físicas que se comprometan ellas mismas a respetar escrupulosamente estos tres principios fundamentales.
|
||||
```
|
||||
|
||||
## Ce qui sort du document
|
||||
|
||||
| Section | Article | Titre |
|
||||
|---|---|---|
|
||||
| formule | F1 | Formule de vote |
|
||||
| formule | F1.1 | Formule du seuil WoT |
|
||||
| formule | F1.2 | Paramètres par défaut |
|
||||
| formule | F1.3 | Processus de dépôt officiel |
|
||||
| inertie | N1 | Réglage de l'inertie |
|
||||
| inertie | N1.1 | Niveaux d'inertie |
|
||||
| ordonnancement | O1 | Ordonnancement du document |
|
||||
|
||||
Et l'ordre des articles de chacune de ces trois sections (`ordre.formule`, `ordre.inertie`,
|
||||
`ordre.ordonnancement`). Ils ne sont plus ni listés, ni votables (vote, proposition, coquille,
|
||||
objection, inertie, quorum : refusés), ni comptés, ni publiés. Aucun autre article ne les
|
||||
cite.
|
||||
|
||||
**Leurs textes ne se perdent pas.** Ce qu'ils disaient de vrai (la formule, les crans, le
|
||||
quorum, chaque phrase vérifiée par un test des lots T à T3c, `t3c-liste-fermee.md`) est la
|
||||
référence de l'écran du lot U3, qui les montrera hors du vote, à leur place dans l'interface.
|
||||
Leurs derniers textes restent dans `crates/ld-server/tests/fixtures/seed-m8/` et, en
|
||||
production, dans leurs lignes.
|
||||
|
||||
## En production
|
||||
|
||||
- **I1, E1, E2** : trois amendements gardés (`2026-11-03-i1-amorce`, `-e1-amorce`,
|
||||
`-e2-amorce`) réécrivent la Proposition seed, une fois par document, seulement si le texte en
|
||||
place est celui qu'on attend (son empreinte) et que personne n'y a touché : un vote, un
|
||||
bulletin, une coquille, une traduction de membre, une objection suffisent à garder le texte,
|
||||
avec sa raison. L'ancien texte reste dans l'historique de la version.
|
||||
- **E12** : un ajout gardé (`2026-11-03-e12-responsabilite-civique`), une fois par document.
|
||||
- **Formule, inertie, ordonnancement** : le retrait `2026-11-03-fonctionnement-hors-vote`,
|
||||
une fois par document, tous ou aucun.
|
||||
- **La file de publication** (suspendue jusqu'au premier texte en vigueur) : rien n'y est
|
||||
effacé. Les lignes déjà écrites pour F1.2, N1.1… restent et partiront avec le premier texte,
|
||||
comme lignes du message de commit ; les textes publiés ne contiennent plus ces articles. T4
|
||||
ajoute ses propres lignes (les trois textes, E12, le retrait).
|
||||
- **Un retour arrière** vers l'image d'avant remet les sept articles et les trois ordres tels
|
||||
qu'ils étaient (vérifié avec l'image `de1b7eb`) ; E12 et les nouveaux textes d'I1, E1, E2
|
||||
restent.
|
||||
@@ -11,8 +11,423 @@
|
||||
> **Mis à jour le 2026-11-03 par le lot T2** (branche `lot/t2-phrases`), après la relecture
|
||||
> indépendante de M6b : ces textes sont en ligne depuis M6b (03/11, 08 h 02), et trois de leurs
|
||||
> phrases disaient plus que les règles. Elles sont reprises ci-dessous, avant et après
|
||||
> (« Lot T2 »). Les blocs de texte de ce fichier sont ceux du seed d'aujourd'hui, phrases
|
||||
> reprises comprises. Rien du lot T2 n'est en ligne.
|
||||
> (« Lot T2 »). *(En ligne depuis M6c, le 03/11 à 11 h 05.)*
|
||||
>
|
||||
> **Mis à jour le 2026-11-03 par le lot T3** (branche `lot/t3-chiffres`), après ta validation du
|
||||
> jour : N1.1 perd son exemple chiffré, et trois phrases, deux de N1.1 et une de F1.2, sont écrites
|
||||
> vraies pour tout W et tout quorum (« Lot T3 », ci-dessous). Les blocs de texte de ce fichier sont
|
||||
> ceux du seed d'aujourd'hui. Rien du lot T3 n'est en ligne.
|
||||
>
|
||||
> **Repris le 2026-11-03 par le lot T3b** (branche `lot/t3b-g`), après la relecture indépendante de
|
||||
> `2ce2028`, avant la mise en ligne : la phrase de G (N1.1) écrite vraie sous le quorum, « autant de
|
||||
> « pour » » en français, le test des nombres honnête sur « 100 votants » (« Après la relecture de
|
||||
> 2ce2028 », ci-dessous). Le même amendement, avec sa nouvelle empreinte ; rien n'est en ligne.
|
||||
>
|
||||
> **Repris le 2026-11-03 par le lot T3c** (branche `lot/t3c-habituelle`), après la relecture
|
||||
> indépendante de `e7ab82f`, avant la mise en ligne : le terme défini « à la participation
|
||||
> habituelle » à la place de « comme d'habitude », qui se lisait comme la médiane réelle et devenait
|
||||
> faux quand elle sort de ses bornes ; puis un passage complet sur F1.2 et N1.1, phrase par phrase
|
||||
> (« Après la relecture de e7ab82f », ci-dessous). La liste fermée de leurs phrases, chacune avec le
|
||||
> test qui la vérifie et sa grille, est dans `t3c-liste-fermee.md`. Les mêmes amendements, avec leurs
|
||||
> nouvelles empreintes ; rien n'est en ligne.
|
||||
|
||||
## Lot T3 : aucun chiffre daté, chaque phrase vraie pour tout W
|
||||
|
||||
Ta réponse du 03/11 (avant 16 h 56), mot pour mot : « Je valide textes-quorum-seed (dont le point
|
||||
3, pas de chiffre datés dans un texte voté). Tu peux lancer la livraison de decision. »
|
||||
|
||||
**Les points tranchés le 03/11** (« Points à trancher », plus bas) :
|
||||
|
||||
- **point 3 : retiré.** L'exemple de N1.1 (7 224 membres, 120 votants, et ce qui en était calculé)
|
||||
sort des trois langues. Rien ne le remplace : un exemple qui ne porterait que des paramètres, vrai
|
||||
pour tout W, ne dirait rien de plus que la liste des crans ;
|
||||
- **points 1, 2, 4 et 8 : le défaut reste.** 1 : ton intention de remonter l'inertie n'est pas
|
||||
écrite ; 2 : « Propositions seed » reste le mot du texte voté ; 4 : le groupe d'essai n'est pas
|
||||
nommé, les quorums sont ceux « dans Monnaie libre » ; 8 : F1.2 garde « tant que l'article ne garde
|
||||
la trace d'aucun vote » (le texte en ligne), et l'interface ta formule « avant le premier vote ».
|
||||
|
||||
**Est daté** tout nombre qui dépend de l'état de la toile (W) ou d'une participation à une date.
|
||||
Restent les paramètres et les règles : les quorums 12 · 12 · 50 · 50 · 50, M et s de chaque cran,
|
||||
B = 0,1, les bornes 1 % et 50 %, « jamais moins de 2 », 6 heures, un an, une fois par jour,
|
||||
5 propositions, la référence « deux fois le quorum de référence : 100 votants » (2 × 50). F1.1 et
|
||||
F1.2 n'avaient aucun chiffre daté. Le test des nombres (`every_number_of_the_quorum_texts_is_the_engine_s`)
|
||||
n'admet plus que des paramètres : chacun vient du seed ou d'une constante du moteur ; aucune toile ni
|
||||
participation n'y entre (« 100 votants » est un paramètre, 2 × 50, pas une participation vraie sur
|
||||
toute toile : voir « Après la relecture de 2ce2028 »). Vu rouge sur le texte de T2 : N1.1 portait
|
||||
7 224 · 120 · 72 · 81 · 90 · 100 · 108 · 75 % · 90 · 9 · 3 · 39 · 11, que rien ne justifie.
|
||||
|
||||
Ensuite, **chaque phrase des trois textes relue** pour tout W et tout quorum dans les bornes
|
||||
(2 à W dans Monnaie libre, 1 à W dans le groupe d'essai), tout cran, toute participation entre 1 %
|
||||
et 50 %. Trois phrases étaient fausses quelque part, chacune prouvée par un test ; elles sont
|
||||
corrigées au plus court. Rien d'autre ne change ; les titres restent. *(La relecture de `2ce2028`
|
||||
en a trouvé une quatrième, la phrase de G : reprise ci-dessous.)*
|
||||
|
||||
### Après la relecture de e7ab82f
|
||||
|
||||
La relecture indépendante de `e7ab82f` rend « à reprendre » pour un point, « comme d'habitude » ; orch
|
||||
demande ensuite un passage complet sur F1.2 et N1.1, phrase par phrase (« que T3c soit le dernier
|
||||
tour »). T3 n'étant pas en ligne, les amendements `2026-11-03-f1-2-chiffres` et
|
||||
`2026-11-03-n1-1-chiffres` sont repris eux-mêmes (leurs nouvelles empreintes : « La garde, en
|
||||
production »), sans en ajouter. F1.1 ne change pas. **La liste fermée** des phrases de F1.2 et N1.1,
|
||||
chacune avec ce qu'elle affirme et le test qui la vérifie sur sa grille, est dans
|
||||
`t3c-liste-fermee.md`, remise telle quelle au relecteur.
|
||||
|
||||
**Le terme défini.** N1.1 définit la participation habituelle : la médiane du nombre de votants des
|
||||
propositions votées depuis un an, ou deux fois le quorum de référence quand elle repose sur moins de
|
||||
5 propositions, **ramenée entre 1 % et 50 % de l'électorat** ; le moteur calcule G sur cette valeur
|
||||
bornée (`Participation::of`). « Quand le groupe vote comme d'habitude » se lit d'abord comme la médiane
|
||||
réelle, et sous cette lecture, trois phrases et les cinq lignes des crans deviennent fausses dès que la
|
||||
médiane sort de ses bornes. **L'exemple de la relecture** : une toile de 7 224 membres (celle de 2024),
|
||||
une médiane de 40 votants (0,55 % de W, ramenée à 1 % : 72,24 votants), cran 2, quorum 12. À 40
|
||||
votants, au-delà du quorum, la part exigée est de 68,7 % : il faut 28 « pour » sur 40, pas 27 (67 % de
|
||||
40, arrondi au-dessus) ; 27 « pour » et 13 « contre » ne passent pas. À la participation habituelle,
|
||||
72,24 votants, la part est de 67 % exactement. Le cas est réaliste dans Monnaie libre aujourd'hui. Le
|
||||
test de T3 sautait ces cas (`if part.bounded { continue; }`) : la grille de T3c les prend tous, la
|
||||
participation habituelle étant la valeur bornée (prouvé par
|
||||
`the_participation_habituelle_is_the_median_brought_back_within_its_bounds`, qui calcule aussi
|
||||
l'exemple).
|
||||
|
||||
Chaque phrase changée, avant (le texte de `e7ab82f`) et après :
|
||||
|
||||
- **F1.2, « Par défaut »**.
|
||||
- **Avant** : « …un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de
|
||||
« pour » quand il vote comme d'habitude. »
|
||||
- **Après** : « …un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de
|
||||
« pour » à sa participation habituelle. »
|
||||
- en : « …asks for 75% of votes in favour when it votes as usual. » → « …at its usual turnout. » ;
|
||||
es : « …un 75 % de votos a favor cuando vota como de costumbre. » → « …con su participación
|
||||
habitual. »
|
||||
- **Pourquoi** : le terme défini (l'exemple ci-dessus). « Au-delà du quorum » reste : sous « Par
|
||||
défaut », juste après « un quorum de 50 », il se lit comme ce quorum-là, celui d'un article au cran
|
||||
par défaut ; lu comme le quorum de l'article, il est vrai aussi.
|
||||
- **N1.1, la phrase du quorum** (point 3 de l'ordre, défaut appliqué mot pour mot).
|
||||
- **Avant** : « Chaque cran a aussi son quorum : sous le quorum, les votants qui manquent comptent
|
||||
comme « non », et il faut autant de « pour » que si le quorum avait voté. »
|
||||
- **Après** : « Chaque cran a aussi son quorum : 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é. »
|
||||
- en : « below the quorum, […] as if the quorum had voted » → « below the article's quorum, […] as if
|
||||
that quorum had voted » ; es : « por debajo del quórum, […] como si hubiera votado el quórum » →
|
||||
« por debajo del quórum del artículo, […] como si hubiera votado ese quórum ».
|
||||
- **Pourquoi** : juste après « son quorum » (celui du cran), « sous le quorum » se lisait d'abord comme
|
||||
le quorum du cran, alors qu'un article peut avoir celui que son auteur a fixé. Le test de la phrase
|
||||
reste vrai pour tout quorum dans les bornes (`under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted`).
|
||||
- **N1.1, la phrase de la part** (premier paragraphe, la dernière).
|
||||
- **Avant** : « La part dépend du cran et du nombre de votants, comparé à la participation habituelle
|
||||
du groupe : au-delà du quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand
|
||||
tout le monde vote. »
|
||||
- **Après** : « La part exigée dépend du cran et du nombre de votants, comparé à la participation
|
||||
habituelle du groupe : au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la
|
||||
majorité M quand tout le monde vote. »
|
||||
- en : « The share depends […] beyond the quorum » → « The share asked depends […] beyond that
|
||||
quorum » ; es : « La proporción depende […] más allá del quórum » → « La proporción exigida depende
|
||||
[…] más allá de ese quórum ».
|
||||
- **Pourquoi** : « la part » seule, après « une part de « pour » parmi les votants », peut se lire
|
||||
comme la part de « pour » à apporter en voix entières, qui remonte parfois au-delà du quorum (toile
|
||||
de 5 707 membres, participation de référence, cran 2, quorum 12 : 9 « pour » sur 12, 75 %, puis 10
|
||||
sur 13, 76,9 %), alors que la part exigée, celle de la formule qui suit, baisse (73,5 %, puis
|
||||
73,2 %). « Ce quorum » : la phrase d'avant en nomme deux, celui du cran et celui de l'article ; entre
|
||||
celui du cran et un quorum d'auteur plus haut, la part ne baisse pas. Prouvé par
|
||||
`the_share_asked_falls_beyond_the_article_s_quorum_down_to_m`.
|
||||
- **N1.1, la formule de la part** (T').
|
||||
- **Avant** : « …, où T' est le nombre de votants, ou le quorum s'il en manque. »
|
||||
- **Après** : « …, où T' est le nombre de votants, ou le quorum de l'article s'il en manque. »
|
||||
- en : « or the quorum if some are missing » → « or the article's quorum if some are missing » ; es :
|
||||
« o el quórum si faltan » → « o el quórum del artículo si faltan ».
|
||||
- **Pourquoi** : T' se calcule sur le quorum de l'article (F1.1 : « Q : le quorum de l'article ») ; la
|
||||
raison de la phrase du quorum.
|
||||
- **N1.1, l'introduction des crans**.
|
||||
- **Avant** : « **Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum,
|
||||
quand le groupe vote comme d'habitude, puis quand tout le monde vote, et ce qui est à ce cran au
|
||||
départ. »
|
||||
- **Après** : « **Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum de
|
||||
l'article, à la participation habituelle, puis quand tout le monde vote, et ce qui est à ce cran au
|
||||
départ. »
|
||||
- en : « the share asked beyond the quorum, when the group votes as usual, » → « the share asked
|
||||
beyond the article's quorum, at the usual turnout, » ; es : « la proporción exigida más allá del
|
||||
quórum, cuando el grupo vota como de costumbre, » → « la proporción exigida más allá del quórum del
|
||||
artículo, con la participación habitual, ».
|
||||
- **Pourquoi** : le terme défini ; et « de l'article » : juste après « le quorum dans Monnaie libre »
|
||||
(celui du cran), « au-delà du quorum » se lisait comme ce quorum-là. Pour un article dont l'auteur a
|
||||
fixé un quorum au-dessus de la participation habituelle, elle-même au-dessus du quorum du cran, la
|
||||
part du cran n'est pas demandée à la participation habituelle : les votants y sont jugés comme le
|
||||
quorum de l'article, à une part plus basse que celle du cran, demandée à plus de votants qu'il n'en
|
||||
vient (sur la grille, 68 115 cas où la participation habituelle n'atteint pas le quorum de
|
||||
l'article).
|
||||
- **N1.1, les cinq lignes des crans**.
|
||||
- **Avant** : « 60 % comme d'habitude, 50 % quand tout le monde vote » (de même 67, 75, 83, 90 %).
|
||||
- **Après** : « 60 % à la participation habituelle, 50 % quand tout le monde vote ».
|
||||
- en : « 60% as usual » → « 60% at the usual turnout » ; es : « 60 % como de costumbre » → « 60 % con
|
||||
la participación habitual ».
|
||||
- **Pourquoi** : le terme défini.
|
||||
- **N1.1, la participation habituelle de référence**.
|
||||
- **Avant** : « Tant que le groupe n'a pas voté sur 5 propositions, elle vaut deux fois le quorum de
|
||||
référence du document : 100 votants dans Monnaie libre. »
|
||||
- **Après** : « Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de
|
||||
référence du document : 100 votants dans Monnaie libre. »
|
||||
- en : « Until the group has voted on 5 proposals, it is » → « While it rests on fewer than 5
|
||||
proposals, it is » ; es : « Hasta que el grupo haya votado 5 propuestas, vale » → « Mientras se
|
||||
base en menos de 5 propuestas, vale ».
|
||||
- **Pourquoi** : fausse une fois que le groupe a voté sur 5 propositions. La médiane ne compte que les
|
||||
propositions votées depuis un an (la phrase d'avant) ; avec moins de 5 dans l'année, la référence
|
||||
revient (`tests/inertia.rs`, `the_usual_participation_is_the_median_of_five_voted_proposals` : une
|
||||
des cinq votée il y a plus d'un an, la référence revient). « Elle repose sur » renvoie aux
|
||||
propositions de la phrase d'avant.
|
||||
- **N1.1, les bornes**.
|
||||
- **Avant** : « Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour. »
|
||||
- **Après** : « Elle est ramenée entre 1 % et 50 % de l'électorat, et se mesure une fois par jour. »
|
||||
- en : « It stays between » → « It is kept between » ; es : « Se mantiene entre » → « Se acota
|
||||
entre ».
|
||||
- **Pourquoi** : c'est la définition sur laquelle tout le lot s'appuie : la participation habituelle
|
||||
est la valeur ramenée. « Reste » se lit comme un constat (la médiane resterait d'elle-même entre les
|
||||
bornes), et la première phrase, « est la médiane », serait fausse pour une médiane de 40 sur 7 224 ;
|
||||
« est ramenée » dit la règle. En espagnol, « se mantiene » a les deux lectures ; « se acota » (« est
|
||||
bornée ») n'en a qu'une.
|
||||
- **N1.1, la phrase de G**.
|
||||
- **Avant** : « **G** se calcule pour que la part exigée, au-delà du quorum, soit exactement celle du
|
||||
cran quand le groupe vote comme d'habitude : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette
|
||||
part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W. »
|
||||
- **Après** : « **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(1 − (s − M)/(1 − M)) / ln(p̃)`, où
|
||||
s est cette part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W. »
|
||||
- en : « beyond the quorum, is exactly that of the level when the group votes as usual: » → « beyond
|
||||
the article's quorum, is exactly that of the level at the usual turnout: » ; es : « más allá del
|
||||
quórum, sea exactamente la del nivel cuando el grupo vota como de costumbre: » → « más allá del quórum
|
||||
del artículo, sea exactamente la del nivel con la participación habitual: ».
|
||||
- **Pourquoi** : G se calcule sur la valeur bornée ; à la médiane réelle hors des bornes, la part n'est
|
||||
pas s (l'exemple) ; « de l'article », comme l'introduction des crans.
|
||||
|
||||
**Ce qui est relu et laissé**, phrase par phrase, avec son test : `t3c-liste-fermee.md` (24 entrées pour
|
||||
F1.2, intertitres compris, 15 pour N1.1). Trois points, hors du champ de T3c, sont dans « Points à trancher » (11, 12, 13) :
|
||||
l'interface dit encore « comme d'habitude » ; F1.2 ne dit pas la version retenue par une objection ;
|
||||
F1.1, G sans effet au quorum W.
|
||||
|
||||
**Les tests** (`crates/ld-server/tests/seed_quorum_texts.rs`, sauf mention), vus rouges sur le texte
|
||||
de `e7ab82f` (8 tests), puis verts :
|
||||
|
||||
- une grille commune, `grid()` (85 100 cas) : W de 100 (la borne basse de `wot::PLAUSIBLE`) à
|
||||
10 000 000 ; la référence et des médianes sous 1 % de W, dans les bornes et au-dessus de 50 %, chacune
|
||||
ramenée par `Participation::of`, la participation habituelle étant la valeur bornée ; chaque quorum
|
||||
d'auteur de 2 à W jusqu'à W = 1 000, un échantillon au-delà ; les cinq crans. Elle remplace
|
||||
`as_usual_grid`, qui sautait les médianes hors des bornes ;
|
||||
- renommés : `the_share_of_a_level_as_usual_is_asked_beyond_the_quorum` →
|
||||
`the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum` ;
|
||||
`g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usual` →
|
||||
`g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum` ;
|
||||
- nouveaux : `the_participation_habituelle_is_the_median_brought_back_within_its_bounds` (l'écart entre
|
||||
la valeur bornée et la médiane, l'exemple de la relecture, et pourquoi le terme défini) ;
|
||||
`the_share_asked_falls_beyond_the_article_s_quorum_down_to_m` ; dans `tests/quorum.rs`, la
|
||||
participation et G inchangés quand un administrateur fixe le quorum d'un article de Monnaie libre, et
|
||||
le cran 3 d'un nouvel article ;
|
||||
- étendus à la grille : `under_the_quorum_…` (plus « une seule voix ne fait jamais passer un texte »),
|
||||
`b_to_the_power_w_…`, `the_formulas_…` ; le test des nombres vérifie toujours tout ;
|
||||
- quatre mutations du code, défaites aussitôt, chacune vue rouge (`t3c-liste-fermee.md`).
|
||||
|
||||
### Après la relecture de 2ce2028
|
||||
|
||||
La relecture indépendante de `2ce2028` rend « à reprendre » pour une phrase, et relève deux mineurs.
|
||||
T3 n'étant pas en ligne, l'amendement `2026-11-03-n1-1-chiffres` est repris lui-même (sa nouvelle
|
||||
empreinte, dans « La garde, en production »), sans en ajouter un. F1.2 et F1.1 ne changent pas.
|
||||
|
||||
- **La phrase de G** (N1.1, dernier paragraphe, que T3 avait laissée).
|
||||
- **Avant** : « **G** se calcule pour que la part exigée, à la participation habituelle, soit
|
||||
exactement celle du cran : `G = …`, où s est la part exigée comme d'habitude et p̃ la
|
||||
participation habituelle divisée par W. »
|
||||
- **Après** : « **G** se calcule pour que la part exigée, au-delà du quorum, soit exactement celle
|
||||
du cran quand le groupe vote comme d'habitude : `G = …`, où s est cette part du cran (60, 67, 75,
|
||||
83 ou 90 %) et p̃ la participation habituelle divisée par W. » *(Repris après la relecture de
|
||||
e7ab82f, ci-dessus : « au-delà du quorum de l'article », « à la participation habituelle ».)*
|
||||
- **Pourquoi** : fausse quand la participation habituelle est sous le quorum, où la part exigée
|
||||
se calcule sur T' = Q. Sur une toile de 3 000 membres dont la médiane est de 40 votants (dans les
|
||||
bornes), au cran 3, quorum 50 : la courbe donne 75 % à 40 votants, mais la part exigée est
|
||||
74,23 % de 50, soit 38 « pour » des 40 votants, 95 %. G ne dépend pas du quorum : la courbe passe
|
||||
toujours par s à la participation habituelle, c'est le quorum qui la coupe en dessous. La phrase
|
||||
nouvelle ne dit s qu'au-delà du quorum, comme l'introduction de la liste des crans, et s y est la
|
||||
part du cran, celle de la liste, plus « la part exigée comme d'habitude » (qui, sous le quorum,
|
||||
n'est pas s). C'est le défaut proposé par l'intégrateur, mot pour mot. Les cinq nombres entre
|
||||
parenthèses sont gardés : sans eux, « cette part » se lirait aussi comme la « Part exigée » de la
|
||||
formule au-dessus, qui change avec T' ; ce sont des paramètres (s de chaque cran), que le test des
|
||||
nombres vérifie. Prouvé par
|
||||
`g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usual`, sur la grille de
|
||||
`the_share_of_a_level_as_usual_is_asked_beyond_the_quorum` (la même fonction, `as_usual_grid` ;
|
||||
renommés par T3c : `g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum`,
|
||||
`the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum`, `grid`) :
|
||||
G tel que le texte l'écrit est celui du moteur ; la courbe passe par s à la participation
|
||||
habituelle, à tout quorum ; la part exigée du groupe qui vote comme d'habitude est s au-delà du
|
||||
quorum (970 cas), et plus basse que s, jugée sur le quorum, en dessous (700 cas), là où la phrase
|
||||
de T3 disait s. Vu rouge sur le texte de T3.
|
||||
- en, avant : « **G** is computed so that the share asked, at the usual turnout, is exactly that of
|
||||
the level: `G = …`, where s is the share asked as usual and p̃ the usual turnout divided by W. » ;
|
||||
après : « **G** is computed so that the share asked, beyond the quorum, is exactly that of the
|
||||
level when the group votes as usual: `G = …`, where s is that share of the level (60, 67, 75, 83 or
|
||||
90%) and p̃ the usual turnout divided by W. »
|
||||
- es, avant : « **G** se calcula para que la proporción exigida, con la participación habitual, sea
|
||||
exactamente la del nivel: `G = …`, donde s es la proporción exigida como de costumbre y p̃ la
|
||||
participación habitual dividida por W. » ; après : « **G** se calcula para que la proporción
|
||||
exigida, más allá del quórum, sea exactamente la del nivel cuando el grupo vota como de costumbre:
|
||||
`G = …`, donde s es esa proporción del nivel (60, 67, 75, 83 o 90 %) y p̃ la participación
|
||||
habitual dividida por W. »
|
||||
- **La phrase du quorum, en français** (N1.1, premier paragraphe).
|
||||
- **Avant** : « …, et il faut autant de voix pour que si le quorum avait voté. »
|
||||
- **Après** : « …, et il faut autant de « pour » que si le quorum avait voté. »
|
||||
- **Pourquoi** : « de voix pour que » se lit d'abord « pour que », la conjonction. Le sens ne change
|
||||
pas, et le même test le tient (`under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted`).
|
||||
en et es restent : « as many votes in favour as », « tantos votos a favor como » sont sans
|
||||
ambiguïté. La raison de l'amendement, qui part dans le commit du dépôt des textes, dit de même
|
||||
« autant de « pour » », et nomme G.
|
||||
- **Le test des nombres et « 100 votants »** : le texte voté ne change pas. Le test calculait la
|
||||
référence comme p0 × W avant les bornes, c'est-à-dire 2 × 50 quel que soit W : sa vérification
|
||||
« les mêmes chiffres sur toute toile » ne pouvait pas échouer pour 100. 100 y est maintenant ce
|
||||
qu'il est, un paramètre (deux fois le quorum de référence), sans prétendre qu'il vaut sur toute
|
||||
toile ; un test à part dit où il vaut (`the_reference_turnout_is_a_parameter_the_bounds_bring_back`) :
|
||||
la participation de référence est de 100 votants sur une toile de 200 à 10 000 membres, et là
|
||||
seulement ; en deçà, les bornes la ramènent à 50 % de W, au-delà à 1 %, comme N1.1 le dit dans le
|
||||
même paragraphe, après. Ce test tombe quand on lui fait dire « de 100 à 10 000 » (W 100).
|
||||
|
||||
**Chaque phrase relue, encore**, en fr, en et es, en cherchant « comme d'habitude », « à la
|
||||
participation habituelle » et « la part du cran » sans « au-delà du quorum » :
|
||||
|
||||
- **F1.1** : « Le seuil est le nombre de voix pour […] » (une définition) ; « Sous le quorum Q […]
|
||||
comme si Q personnes avaient voté » ; « Au-delà, il faut la part de « pour » que demande le cran
|
||||
[…] la plus haute au quorum […] jusqu'à la majorité M quand tout le monde vote » (la part du cran,
|
||||
sous « Au-delà ») ; les deux formules ; « Une version passe […] arrondi à l'entier au-dessus » ; T,
|
||||
Q et T', W, B, M ; G, « plus la part demandée monte quand peu de gens votent » (vrai à tout T' sous
|
||||
W, donc au quorum aussi) ; « Leurs valeurs sont dans […] ». Aucune n'est de la famille cherchée.
|
||||
- **F1.2** : l'introduction ; les trois points du vote permanent ; W, T, Q, B ; « G n'est pas fixé à
|
||||
la main : il se déduit de la participation habituelle du groupe » (dit d'où vient G, rien de la
|
||||
part) ; « au-delà du quorum, le groupe demande 75 % de « pour » quand il vote comme d'habitude »
|
||||
(« au-delà du quorum », T3) ; « Tout nouvel article commence à ce cran » ; « Au départ […] ». Juste.
|
||||
- **N1.1** : « le groupe demande une part de « pour » parmi les votants » (une part, pas celle du
|
||||
cran ; les votants qui manquent y comptent, comme la phrase suivante le dit) ; la phrase du quorum
|
||||
(ci-dessus) ; « La part dépend du cran et du nombre de votants, comparé à la participation
|
||||
habituelle […] : au-delà du quorum, plus il y a de votants, plus elle baisse » (la courbe dépend de
|
||||
p̃ à tout quorum ; le nombre de votants est T', que la formule définit juste après) ; la formule et
|
||||
T' ; l'introduction de la liste, « la part exigée au-delà du quorum, quand le groupe vote comme
|
||||
d'habitude, puis quand tout le monde vote » (T3) ; les cinq crans, « 60 % comme d'habitude » […]
|
||||
(les colonnes que l'introduction définit, au-delà du quorum ; non répété à chaque ligne) ; la
|
||||
participation habituelle, ses 5 propositions et ses 100 votants (un paramètre, ramené par les
|
||||
bornes que la phrase d'après donne) ; « Elle ne dépend pas du quorum d'un article » ; les bornes et
|
||||
la mesure ; **la phrase de G : la seule de la famille qui restait fausse**, reprise.
|
||||
- Hors des trois textes, vu en cherchant et laissé (hors de ce lot) : la phrase de F1.1 « le nombre
|
||||
de voix pour qu'il faut » a la même lecture « pour qu' » que celle du point 3, mais F1.1 n'a pas
|
||||
d'amendement de T3 et ce lot n'en ajoute pas ; dans l'interface, `settings.reading` dit encore « À
|
||||
chaque cran, le groupe demande sa part de « pour » quand il vote comme d'habitude ; au-delà du
|
||||
quorum, […] » (fr, en, es), la même imprécision que la phrase de G, pour sa première moitié.
|
||||
|
||||
### N1.1 — Niveaux d'inertie
|
||||
|
||||
- **Le quorum** (premier paragraphe).
|
||||
- **Avant** : « Chaque cran a aussi son quorum, qui n'est pas un nombre minimal de votants : sous
|
||||
le quorum, les votants qui manquent comptent comme « non ». »
|
||||
- **Après** : « Chaque cran a aussi son quorum : sous le quorum, les votants qui manquent comptent
|
||||
comme « non », et il faut autant de « pour » que si le quorum avait voté. » *(T3 écrivait
|
||||
« autant de voix pour que » ; repris après la relecture de 2ce2028, ci-dessus. Puis, après celle de
|
||||
e7ab82f : « sous le quorum de l'article », « que si ce quorum avait voté ».)*
|
||||
- **Pourquoi** : « pas un nombre minimal de votants » est faux pour un quorum d'auteur bas. Au
|
||||
quorum 2, la part du quorum dépasse 50 % : il faut 2 « pour », une seule voix ne passe jamais, et
|
||||
le quorum est alors un minimum de votants, à tout cran et sur toute toile (de même au quorum 1 du
|
||||
groupe d'essai). La phrase nouvelle est vraie pour tout quorum : avec moins de votants que le
|
||||
quorum, les « pour » qui passent sans « contre » sont exactement le seuil du quorum
|
||||
(`yes_needed(0)` égale le seuil à T = Q), et un décompte passe dès que ses « pour » l'atteignent.
|
||||
Prouvé sur une grille par `under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted` :
|
||||
W de 100 à 10 000 000, chaque quorum de 2 à W jusqu'à 10 000 membres (des quorums choisis
|
||||
au-delà), les cinq crans, quatre participations dans les bornes ; le groupe d'essai de 1 à
|
||||
40 membres, chaque quorum de 1 à W ; plus de 500 000 cas. C'est la phrase proposée par l'intégrateur dans l'ordre du lot, mot pour mot.
|
||||
- en : « Each level also has its quorum: below the quorum, the voters who are missing count as
|
||||
votes against, and it takes as many votes in favour as if the quorum had voted. » ; es : « Cada
|
||||
nivel tiene además su quórum: por debajo del quórum, los votantes que faltan cuentan como «no»,
|
||||
y hacen falta tantos votos a favor como si hubiera votado el quórum. »
|
||||
- **La liste des crans** (son introduction).
|
||||
- **Avant** : « **Les cinq crans** : le quorum dans Monnaie libre, la part exigée quand le groupe
|
||||
vote comme d'habitude, puis quand tout le monde vote, et ce qui est à ce cran au départ. »
|
||||
- **Après** : « **Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du
|
||||
quorum, quand le groupe vote comme d'habitude, puis quand tout le monde vote, et ce qui est à ce
|
||||
cran au départ. » *(Repris après la relecture de e7ab82f, ci-dessus : « au-delà du quorum de
|
||||
l'article, à la participation habituelle ».)*
|
||||
- **Pourquoi** : « 75 % comme d'habitude » n'est vrai que si la participation habituelle atteint
|
||||
le quorum. En dessous, le groupe qui vote comme d'habitude est jugé sur le quorum : sur une toile
|
||||
de 3 000 membres dont la médiane est de 40 votants (dans les bornes : 1 %, c'est 30), au cran 3,
|
||||
quorum 50, il faut 38 « pour » de ces 40 votants, 95 %, pas 75 %. Cela arrive dès que la toile
|
||||
passe sous 5 000 membres (1 % sous 50) avec une médiane sous 50, ou quand l'auteur d'un article
|
||||
fixe un quorum au-dessus de la participation habituelle. Prouvé par
|
||||
`the_share_of_a_level_as_usual_is_asked_beyond_the_quorum` (renommé par T3c) : la part du cran exactement quand la
|
||||
participation habituelle atteint le quorum, une part plus basse, sur plus de votants, sinon ; M
|
||||
quand tout le monde vote, à tout quorum.
|
||||
- en : « the share asked beyond the quorum, when the group votes as usual, then when everyone
|
||||
votes » ; es : « la proporción exigida más allá del quórum, cuando el grupo vota como de
|
||||
costumbre, luego cuando vota todo el mundo ».
|
||||
- **L'exemple** (dernier paragraphe) : retiré entier, dans les trois langues (point 3).
|
||||
|
||||
### F1.2 — Paramètres par défaut
|
||||
|
||||
- **Avant** : « Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie
|
||||
libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude. »
|
||||
- **Après** : « Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie
|
||||
libre, et, au-delà du quorum, le groupe demande 75 % de « pour » quand il vote comme d'habitude. »
|
||||
*(Repris après la relecture de e7ab82f, ci-dessus : « à sa participation habituelle ».)*
|
||||
- **Pourquoi** : la raison de la liste des crans de N1.1 ; le cas de 3 000 membres est celui-ci
|
||||
(cran 3, quorum 50). en : « and, beyond the quorum, the group asks for 75% of votes in favour
|
||||
when it votes as usual » ; es : « y, más allá del quórum, el grupo pide un 75 % de votos a favor
|
||||
cuando vota como de costumbre ».
|
||||
|
||||
### F1.1 — relu, laissé
|
||||
|
||||
Aucun chiffre daté, aucune phrase fausse : pas d'amendement. « B : la base ; à la puissance W, elle
|
||||
est si petite qu'elle ne compte pas » est vrai sur toute toile de 100 à 10 000 000 membres : le
|
||||
seuil du moteur y est celui sans B, même quand la part tombe juste sur un entier (`threshold::settle`,
|
||||
`clamp_ceil` : M × W quand tout le monde vote, s × la participation habituelle, 60 · 67 · 75 · 83 ·
|
||||
90 « pour » à 100 votants), prouvé par `b_to_the_power_w_never_counts_in_the_web_of_trust`. Ce
|
||||
n'est pas vrai d'un très petit groupe : dans le groupe d'essai, où W est le nombre de ses membres,
|
||||
B^W compte (deux membres qui votent : deux « pour », jamais un seul). F1.1 dit que W est la toile
|
||||
de confiance, et les textes ne décrivent pas le groupe d'essai (point 4) : laissé (point 10).
|
||||
|
||||
### La garde, en production
|
||||
|
||||
Dans les deux groupes, F1.2 et N1.1 sont à leur révision 4 (M5, puis M6b, puis M6c), F1.1 à sa
|
||||
révision 3. Les amendements `2026-11-03-f1-2-chiffres` et `2026-11-03-n1-1-chiffres` suivent ceux de
|
||||
T2 (`2026-11-03-f1-2-phrases`, `-n1-1-phrases`), qui sont **dépassés** : ils ne décident plus rien,
|
||||
leurs décisions du 03/11 restent. La garde de révision admet la révision écrite par les amendements
|
||||
suivis, dans leur ordre, le dernier étant celui de T2, et eux seuls : en production, M5, M6b et M6c
|
||||
(révision 4) ; sur une base née le 02/11 avec les textes de M5, M6b et M6c (révision 3) ; née avec
|
||||
ceux de M6b, M6c seul (révision 2) ; née avec ceux de M6c, la révision 1 ; le texte étant celui que
|
||||
M6c a posé (empreinte). Le reste de la garde ne change pas : origine du seed, jamais entré en
|
||||
vigueur ni confirmé ; aucune trace de vote (vote du serveur, prouvé ou simulé ; ligne de bulletin en
|
||||
attente ou prouvée, quel qu'en soit le sort) ; aucune coquille, traduction de membre ni objection.
|
||||
|
||||
| Article | Empreinte d'avant : le texte de T2, en ligne | Empreinte d'après : le texte de T3 |
|
||||
|---|---|---|
|
||||
| F1.2 | `96caf353b33805f2894593d72c5b70fce2e751039cd7f4651d8842c986729a14` | `0c3b800b0c72b1edd4b884c03c6c5479c17609dcf82a95b593d408821e28bb4e` |
|
||||
| N1.1 | `7fbedcb8750c208f8790c4f87dc21d8bd1096a27a1a0044af6b9b8f2057f2aa8` | `8107c5fc746121f18c8a440e86e23310c5a59ae8e92f5a07f6b098419980263c` |
|
||||
|
||||
Les empreintes d'après sont celles des textes repris après la relecture de `e7ab82f` (lot T3c). Les
|
||||
précédentes n'ont jamais été en ligne et ne servent plus : F1.2,
|
||||
`ddce5aa5fcd13dfc3e374c37509a1bb8cfafddefb6e6add5a06b130b43216d62` (T3 et T3b) ; N1.1,
|
||||
`7f62fd42617c6e63b1783d5a4d472ed733aa074222240faf834e6b31f1c9c166` (T3b) et
|
||||
`4289e3579ee747f6023162339ed60601b810d33462617b42a9f8b5b3735c3238` (`2ce2028`).
|
||||
|
||||
Appliqué, dans une transaction, par document : le texte (fr) remplacé, **révision 4 → 5** pour F1.2
|
||||
et N1.1 ; une ligne « texte de départ remplacé » dans l'historique, sans auteur (la révision 4, le
|
||||
texte de T2, reste lisible, avec son diff) ; en et es remplacés ; la décision dans
|
||||
`seed_amendments`. **La file de publication de Monnaie libre** reçoit deux lignes de plus
|
||||
(« proposition de départ réécrite : F1.2, la part du cran par défaut au-delà du quorum, à la
|
||||
participation habituelle » et « … : N1.1, sans chiffre daté ; sous le quorum de l'article, autant de
|
||||
« pour » que si ce quorum avait voté ; au-delà, la part du cran à la participation habituelle ») ; elles
|
||||
attendent, comme les autres, le premier texte en vigueur (ADR 0011) puis des votes prouvés (ADR
|
||||
0012) : rien n'est publié tant qu'aucun article n'est en vigueur. Le groupe d'essai ne publie
|
||||
jamais. Refusé, le texte de T2 reste, avec sa raison (`votes`, `ballots`, `corrected`,
|
||||
`translation_edited`, `signalled`, `revised`, `text_differs`…), consignée une fois pour toutes.
|
||||
|
||||
Testé (`crates/ld-server/tests/seed_quorum_texts.rs`) sur une base faite comme la production après
|
||||
M6c — née avec les textes du 21/10, démarrée par le seed de M5, puis de M6, de M6b et de M6c (les
|
||||
textes de T2 gardés dans `tests/fixtures/seed-quorum-t2.json`) : appliqué une fois dans les deux
|
||||
groupes, quatre révisions et deux lignes de file de plus, F1.1 et tout le reste intacts ; au second
|
||||
démarrage, rien n'est rejoué. Refusé avec sa raison là où un vote simulé est compté (`votes`), là où
|
||||
un bulletin attend (`ballots`), là où un vote direct tient dans le groupe d'essai (`votes`) ; le
|
||||
refus tient même une fois le vote parti. Appliqué aussi sur une base née le 02/11 avec les textes de
|
||||
M5, et sur une base née avec ceux de T2. Les deux limites de T2 restent (le commentaire de
|
||||
`seed_amend.rs`) : un vote direct retiré dans le groupe d'essai, ou un vote simulé que l'acte
|
||||
« supprime » aurait effacé avant la mise en ligne, ne laissent pas de trace.
|
||||
|
||||
En production d'après l'ordre T3 : 0 vote, 0 bulletin (à revoir à la répétition).
|
||||
|
||||
## Lot T2 : trois phrases reprises (après la relecture de M6b)
|
||||
|
||||
@@ -79,6 +494,8 @@ anciens mots ont disparu des trois langues (`the_three_phrases_of_the_review_of_
|
||||
12, 39 sur 50, 43 sur 50, 46 sur 50**, du cran 1 au cran 5 (le moteur, dans
|
||||
`the_three_phrases_of_the_review_of_m6b_are_written_true`). « Au-delà du quorum » est ajouté à
|
||||
la troisième phrase : sous le quorum, la part ne baisse pas, elle reste celle du quorum.
|
||||
- *Reprise par le lot T3* : « qui n'est pas un nombre minimal de votants » est vrai aux quorums des
|
||||
crans, faux pour un quorum d'auteur bas (au quorum 2, il faut 2 « pour »).
|
||||
|
||||
### L'interface, dans le même sens
|
||||
|
||||
@@ -147,10 +564,10 @@ En production aujourd'hui : 0 vote, 0 bulletin, une seule identité (dit par orc
|
||||
le départ d'après l'ADR 0011 : aucun article en vigueur, Raison d'être, Unicité du compte et
|
||||
Réciprocité en Propositions seed au cran 2, le titre du document au cran 4.
|
||||
- **Les titres** sont gardés tous les trois : ils restent vrais.
|
||||
- **Les nombres.** Chaque nombre de ces textes, dans les trois langues, est calculé par le moteur ou
|
||||
lu dans le seed par un test (`crates/ld-server/tests/seed_quorum_texts.rs`,
|
||||
- **Les nombres.** Chaque nombre de ces textes, dans les trois langues, est un paramètre lu dans le
|
||||
seed ou une constante du moteur, vérifiés par un test (`crates/ld-server/tests/seed_quorum_texts.rs`,
|
||||
`every_number_of_the_quorum_texts_is_the_engine_s`) : un chiffre que rien ne justifie fait échouer
|
||||
le test. Les trois formules écrites dans les textes sont évaluées telles quelles et comparées au
|
||||
le test ; depuis le lot T3, aucun ne dépend de W ni d'une participation. Les trois formules écrites dans les textes sont évaluées telles quelles et comparées au
|
||||
moteur sur une grille de cas (`the_formulas_of_the_texts_are_those_the_engine_computes`).
|
||||
- **La forme**, vérifiée par le test `the_quorum_texts_follow_the_rules_of_form` et par `render()`
|
||||
(`web/src/lib/markdown.ts`) : sous-ensemble de Markdown, ni tableau ni intertitre ; une ligne vide
|
||||
@@ -158,7 +575,7 @@ En production aujourd'hui : 0 vote, 0 bulletin, une seule identité (dit par orc
|
||||
WoT », « Paramètres par défaut », « Niveaux d'inertie »), jamais par le code ; apostrophes droites ;
|
||||
« » espacés en français, “ ” en anglais britannique (*favour*), «» serrés en espagnol ; formules en
|
||||
code, avec × et −, jamais d'astérisque.
|
||||
- **Longueur** (signes, après le lot T2 ; avant : fr 1081 · 2055 · 2372, en 1058 · 1977 · 2246, es 1083 · 2035 · 2454) : fr : F1.1 1122 · F1.2 2035 · N1.1 2449 ; en : F1.1 1093 · F1.2 1950 · N1.1 2315 ; es : F1.1 1126 · F1.2 2020 · N1.1 2526. Limite d'une proposition : 4 000 (`crates/ld-server/src/proposals.rs:32`) ;
|
||||
- **Longueur** (signes, après la relecture de `e7ab82f` ; après celle de `2ce2028` : fr F1.2 2055 · N1.1 2112, en 1970 · 1987, es 2042 · 2163 ; après le lot T3 : N1.1 fr 2075, en 1937, es 2125 ; après le lot T2 : fr 1122 · 2035 · 2449, en 1093 · 1950 · 2315, es 1126 · 2020 · 2526 ; avant : fr 1081 · 2055 · 2372, en 1058 · 1977 · 2246, es 1083 · 2035 · 2454) : fr : F1.1 1122 · F1.2 2054 · N1.1 2226 ; en : F1.1 1093 · F1.2 1968 · N1.1 2079 ; es : F1.1 1126 · F1.2 2042 · N1.1 2264. Limite d'une proposition : 4 000 (`crates/ld-server/src/proposals.rs:32`) ;
|
||||
au-delà de 700, l'assistant dit « texte long » (`crates/ld-engine/src/assist.rs:25`), comme pour les
|
||||
textes actuels de F1.2 et N1.1.
|
||||
- **Le ton** est celui de F1.2, N1 et O1 tels que M5 les a posés : le groupe, la personne, jamais le
|
||||
@@ -308,7 +725,7 @@ Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la fa
|
||||
|
||||
**Par défaut**
|
||||
|
||||
- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude. Tout nouvel article commence à ce cran.
|
||||
- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » à sa participation habituelle. Tout nouvel article commence à ce cran.
|
||||
- Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, tous les autres articles au cran 3.
|
||||
```
|
||||
|
||||
@@ -335,7 +752,7 @@ The values of the letters of the formula (see “WoT threshold formula”) and h
|
||||
|
||||
**By default**
|
||||
|
||||
- The default level is 3 (medium): M = 55%, a quorum of 50 in Monnaie libre, and the group asks for 75% of votes in favour when it votes as usual. Every new article starts at this level.
|
||||
- The default level is 3 (medium): M = 55%, a quorum of 50 in Monnaie libre, and, beyond the quorum, the group asks for 75% of votes in favour at its usual turnout. Every new article starts at this level.
|
||||
- At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, all other articles at level 3.
|
||||
```
|
||||
|
||||
@@ -362,7 +779,7 @@ Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y có
|
||||
|
||||
**Por defecto**
|
||||
|
||||
- El nivel por defecto es el 3 (media): M = 55 %, un quórum de 50 en Monnaie libre, y el grupo pide un 75 % de votos a favor cuando vota como de costumbre. Todo artículo nuevo empieza en este nivel.
|
||||
- El nivel por defecto es el 3 (media): M = 55 %, un quórum de 50 en Monnaie libre, y, más allá del quórum, el grupo pide un 75 % de votos a favor con su participación habitual. Todo artículo nuevo empieza en este nivel.
|
||||
- Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3.
|
||||
```
|
||||
|
||||
@@ -372,30 +789,28 @@ Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y có
|
||||
|
||||
**Ce qui est faux** : le quorum remplace C ; la part se calcule sur T', les votants ou le quorum s'il en manque ; au cran 2 sont les trois Propositions seed et l'ordre de chaque section, au cran 4 le titre seul ; la référence est deux fois le quorum de référence du document (50 dans Monnaie libre), jamais le quorum d'un article ; avec le quorum, l'exemple donne 72, 81, 90, 100 ou 108 voix pour ; et dans la formule de G, p̃ est une part de l'électorat, pas un nombre de votants.
|
||||
|
||||
**Titre** gardé : il reste vrai. Chaque cran dit maintenant son quorum ; l'exemple dit aussi deux cas sous le quorum.
|
||||
**Titre** gardé : il reste vrai. Chaque cran dit maintenant son quorum ; l'exemple disait aussi deux cas sous le quorum. *(Retiré par le lot T3 : aucun chiffre daté dans un texte voté.)*
|
||||
|
||||
### N1.1 · Français (fait foi)
|
||||
|
||||
Titre : **Niveaux d'inertie**
|
||||
|
||||
```markdown
|
||||
À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). Chaque cran a aussi son quorum, qui n'est pas un nombre minimal de votants : sous le quorum, les votants qui manquent comptent comme « non ». La part dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà du quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.
|
||||
À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). Chaque cran a aussi son quorum : sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté. La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.
|
||||
|
||||
`Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum s'il en manque.
|
||||
`Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum de l'article s'il en manque.
|
||||
|
||||
**Les cinq crans** : le quorum dans Monnaie libre, la part exigée quand le groupe vote comme d'habitude, puis quand tout le monde vote, et ce qui est à ce cran au départ.
|
||||
**Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum de l'article, à la participation habituelle, puis quand tout le monde vote, et ce qui est à ce cran au départ.
|
||||
|
||||
- **Cran 1 · très basse** : quorum 12 ; 60 % comme d'habitude, 50 % quand tout le monde vote. Aucun article au départ.
|
||||
- **Cran 2 · basse** : quorum 12 ; 67 % comme d'habitude, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.
|
||||
- **Cran 3 · moyenne** : quorum 50 ; 75 % comme d'habitude, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.
|
||||
- **Cran 4 · haute** : quorum 50 ; 83 % comme d'habitude, 60 % quand tout le monde vote. Le titre du document.
|
||||
- **Cran 5 · très haute** : quorum 50 ; 90 % comme d'habitude, 66 % quand tout le monde vote. Aucun article au départ.
|
||||
- **Cran 1 · très basse** : quorum 12 ; 60 % à la participation habituelle, 50 % quand tout le monde vote. Aucun article au départ.
|
||||
- **Cran 2 · basse** : quorum 12 ; 67 % à la participation habituelle, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.
|
||||
- **Cran 3 · moyenne** : quorum 50 ; 75 % à la participation habituelle, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.
|
||||
- **Cran 4 · haute** : quorum 50 ; 83 % à la participation habituelle, 60 % quand tout le monde vote. Le titre du document.
|
||||
- **Cran 5 · très haute** : quorum 50 ; 90 % à la participation habituelle, 66 % quand tout le monde vote. Aucun article au départ.
|
||||
|
||||
**La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. Tant que le groupe n'a pas voté sur 5 propositions, elle vaut deux fois le quorum de référence du document : 100 votants dans Monnaie libre. Elle ne dépend pas du quorum d'un article. Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour.
|
||||
**La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de référence du document : 100 votants dans Monnaie libre. Elle ne dépend pas du quorum d'un article. Elle est ramenée entre 1 % et 50 % de l'électorat, et se mesure une fois par jour.
|
||||
|
||||
**G** se calcule pour que la part exigée, à la participation habituelle, soit exactement celle du cran : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est la part exigée comme d'habitude et p̃ la participation habituelle divisée par W.
|
||||
|
||||
Exemple : avec 7 224 membres et une participation habituelle de 120 votants, quand 120 personnes votent, il faut 72, 81, 90, 100 ou 108 voix pour, du cran 1 au cran 5 ; au cran 3, 75 % des 120 votants, soit 90. Sous le quorum, les absents comptent comme « non » : au cran 2 (quorum 12), 9 voix pour passent avec 3 « contre » au plus ; au cran 3 (quorum 50), il faut 39 voix pour, avec 11 « contre » au plus.
|
||||
**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(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W.
|
||||
```
|
||||
|
||||
### N1.1 · English
|
||||
@@ -403,23 +818,21 @@ Exemple : avec 7 224 membres et une participation habituelle de 120 votants, qua
|
||||
Titre : **Inertia levels**
|
||||
|
||||
```markdown
|
||||
At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). Each level also has its quorum, which is not a minimum number of voters: below the quorum, the voters who are missing count as votes against. The share depends on the level and on the number of voters, compared with the group's usual turnout: beyond the quorum, the more voters, the lower it gets, down to the majority M when everyone votes.
|
||||
At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). Each level also has its quorum: below the article's quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if that quorum had voted. The share asked depends on the level and on the number of voters, compared with the group's usual turnout: beyond that quorum, the more voters, the lower it gets, down to the majority M when everyone votes.
|
||||
|
||||
`Share asked = M + (1 − M) × (1 − (T'/W)^G)`, where T' is the number of voters, or the quorum if some are missing.
|
||||
`Share asked = M + (1 − M) × (1 − (T'/W)^G)`, where T' is the number of voters, or the article's quorum if some are missing.
|
||||
|
||||
**The five levels**: the quorum in Monnaie libre, the share asked when the group votes as usual, then when everyone votes, and what is at that level at the start.
|
||||
**The five levels**: the quorum in Monnaie libre, the share asked beyond the article's quorum, at the usual turnout, then when everyone votes, and what is at that level at the start.
|
||||
|
||||
- **Level 1 · very low**: quorum 12; 60% as usual, 50% when everyone votes. No article at the start.
|
||||
- **Level 2 · low**: quorum 12; 67% as usual, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.
|
||||
- **Level 3 · medium**: quorum 50; 75% as usual, 55% when everyone votes. The default level: all other articles, and every new article.
|
||||
- **Level 4 · high**: quorum 50; 83% as usual, 60% when everyone votes. The title of the document.
|
||||
- **Level 5 · very high**: quorum 50; 90% as usual, 66% when everyone votes. No article at the start.
|
||||
- **Level 1 · very low**: quorum 12; 60% at the usual turnout, 50% when everyone votes. No article at the start.
|
||||
- **Level 2 · low**: quorum 12; 67% at the usual turnout, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.
|
||||
- **Level 3 · medium**: quorum 50; 75% at the usual turnout, 55% when everyone votes. The default level: all other articles, and every new article.
|
||||
- **Level 4 · high**: quorum 50; 83% at the usual turnout, 60% when everyone votes. The title of the document.
|
||||
- **Level 5 · very high**: quorum 50; 90% at the usual turnout, 66% when everyone votes. No article at the start.
|
||||
|
||||
**The usual turnout** is the median number of voters on the group's proposals voted on over the past year. Until the group has voted on 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It stays between 1% and 50% of the electorate, and is measured once a day.
|
||||
**The usual turnout** is the median number of voters on the group's proposals voted on over the past year. While it rests on fewer than 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It is kept between 1% and 50% of the electorate, and is measured once a day.
|
||||
|
||||
**G** is computed so that the share asked, at the usual turnout, is exactly that of the level: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is the share asked as usual and p̃ the usual turnout divided by W.
|
||||
|
||||
Example: with 7,224 members and a usual turnout of 120 voters, when 120 people vote, it takes 72, 81, 90, 100 or 108 votes in favour, from level 1 to level 5; at level 3, 75% of the 120 voters, i.e. 90. Below the quorum, those missing count as votes against: at level 2 (quorum 12), 9 votes in favour pass with 3 votes against at most; at level 3 (quorum 50), it takes 39 votes in favour, with 11 votes against at most.
|
||||
**G** is computed so that the share asked, beyond the article's quorum, is exactly that of the level at the usual turnout: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is that share of the level (60, 67, 75, 83 or 90%) and p̃ the usual turnout divided by W.
|
||||
```
|
||||
|
||||
### N1.1 · Español
|
||||
@@ -427,23 +840,21 @@ Example: with 7,224 members and a usual turnout of 120 voters, when 120 people v
|
||||
Titre : **Niveles de inercia**
|
||||
|
||||
```markdown
|
||||
En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). Cada nivel tiene además su quórum, que no es un número mínimo de votantes: por debajo del quórum, los votantes que faltan cuentan como «no». La proporción depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá del quórum, cuantos más votantes, más baja, hasta la mayoría M cuando vota todo el mundo.
|
||||
En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). Cada nivel tiene además su quórum: por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum. La proporción exigida depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá de ese quórum, cuantos más votantes, más baja, hasta la mayoría M cuando vota todo el mundo.
|
||||
|
||||
`Proporción exigida = M + (1 − M) × (1 − (T'/W)^G)`, donde T' es el número de votantes, o el quórum si faltan.
|
||||
`Proporción exigida = M + (1 − M) × (1 − (T'/W)^G)`, donde T' es el número de votantes, o el quórum del artículo si faltan.
|
||||
|
||||
**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida cuando el grupo vota como de costumbre, luego cuando vota todo el mundo, y lo que está en ese nivel al inicio.
|
||||
**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida más allá del quórum del artículo, con la participación habitual, luego cuando vota todo el mundo, y lo que está en ese nivel al inicio.
|
||||
|
||||
- **Nivel 1 · muy baja**: quórum 12; 60 % como de costumbre, 50 % cuando vota todo el mundo. Ningún artículo al inicio.
|
||||
- **Nivel 2 · baja**: quórum 12; 67 % como de costumbre, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.
|
||||
- **Nivel 3 · media**: quórum 50; 75 % como de costumbre, 55 % cuando vota todo el mundo. El nivel por defecto: todos los demás artículos, y todo artículo nuevo.
|
||||
- **Nivel 4 · alta**: quórum 50; 83 % como de costumbre, 60 % cuando vota todo el mundo. El título del documento.
|
||||
- **Nivel 5 · muy alta**: quórum 50; 90 % como de costumbre, 66 % cuando vota todo el mundo. Ningún artículo al inicio.
|
||||
- **Nivel 1 · muy baja**: quórum 12; 60 % con la participación habitual, 50 % cuando vota todo el mundo. Ningún artículo al inicio.
|
||||
- **Nivel 2 · baja**: quórum 12; 67 % con la participación habitual, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.
|
||||
- **Nivel 3 · media**: quórum 50; 75 % con la participación habitual, 55 % cuando vota todo el mundo. El nivel por defecto: todos los demás artículos, y todo artículo nuevo.
|
||||
- **Nivel 4 · alta**: quórum 50; 83 % con la participación habitual, 60 % cuando vota todo el mundo. El título del documento.
|
||||
- **Nivel 5 · muy alta**: quórum 50; 90 % con la participación habitual, 66 % cuando vota todo el mundo. Ningún artículo al inicio.
|
||||
|
||||
**La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Hasta que el grupo haya votado 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se mantiene entre el 1 % y el 50 % del electorado, y se mide una vez al día.
|
||||
**La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Mientras se base en menos de 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se acota entre el 1 % y el 50 % del electorado, y se mide una vez al día.
|
||||
|
||||
**G** se calcula para que la proporción exigida, con la participación habitual, sea exactamente la del nivel: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es la proporción exigida como de costumbre y p̃ la participación habitual dividida por W.
|
||||
|
||||
Ejemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 72, 81, 90, 100 o 108 votos a favor, del nivel 1 al nivel 5; en el nivel 3, el 75 % de los 120 votantes, es decir 90. Por debajo del quórum, los que faltan cuentan como «no»: en el nivel 2 (quórum 12), 9 votos a favor pasan con 3 en contra como mucho; en el nivel 3 (quórum 50), hacen falta 39 votos a favor, con 11 en contra como mucho.
|
||||
**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: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es esa proporción del nivel (60, 67, 75, 83 o 90 %) y p̃ la participación habitual dividida por W.
|
||||
```
|
||||
|
||||
## Les autres articles, cherchés et laissés
|
||||
@@ -466,15 +877,18 @@ inertie, le cran de départ de Raison d'être, Unicité du compte et Réciprocit
|
||||
1. **Ton intention de remonter l'inertie** une fois les textes passés : elle n'est pas écrite (ce n'est
|
||||
pas une règle). Si tu veux que F1.2 l'annonce sans la promettre, une phrase vraie suffit : « Leur
|
||||
cran se change ensuite par un vote, comme celui de tout article (voir « Réglage de l'inertie »). »
|
||||
*(Tranché le 03/11 : le défaut reste, rien n'est écrit.)*
|
||||
2. **« Propositions seed »** dans un texte voté : c'est le mot de l'interface (« Proposition seed »,
|
||||
« Seed proposal », « Propuesta seed »). Autre choix : « propositions mises en avant ».
|
||||
*(Tranché le 03/11 : le défaut reste, « Propositions seed ».)*
|
||||
3. **L'exemple de N1.1** (7 224 membres, 120 votants) reste celui de M5 et des tests (le vote
|
||||
forgeron de 2024) : une illustration, pas la toile d'aujourd'hui (5 707 membres en production le
|
||||
02/11). À ce W et à la participation de référence, les deux exemples sous le quorum donnent les
|
||||
mêmes nombres de « pour » : 9 au cran 2, 39 au cran 3 (`crates/ld-engine/src/inertia.rs`,
|
||||
`the_control_figures_of_the_quorum`). À retirer si tu ne veux pas de chiffre daté dans un texte voté.
|
||||
**Tranché le 03/11 : retiré** (« pas de chiffre datés dans un texte voté » ; lot T3).
|
||||
4. **Le groupe d'essai** (quorum 1 partout) n'est pas nommé : les textes donnent les quorums « dans
|
||||
Monnaie libre », comme F1.2 donnait C = 50.
|
||||
Monnaie libre », comme F1.2 donnait C = 50. *(Tranché le 03/11 : le défaut reste.)*
|
||||
5. **F1.1 a changé de plan** : le sens d'abord, la formule ensuite, les lettres en liste (la liste
|
||||
d'aujourd'hui ne s'affiche pas comme liste, faute de ligne vide). B y est « la base » (W l'exposant :
|
||||
« exposant de base » était faux), M n'y est plus `majority_pct / 100`, et « Seuil » est traduit en
|
||||
@@ -491,9 +905,28 @@ inertie, le cran de départ de Raison d'être, Unicité du compte et Réciprocit
|
||||
n'est compté sur l'article » (défaut d'orch) se lit mieux mais se trompe pour un vote retiré
|
||||
sur la chaîne (plus compté, le quorum reste figé). Les trois phrases de l'interface sur le
|
||||
quorum de l'auteur (« avant le premier vote », « tant que personne n'a voté ») gardent ta
|
||||
formule : à aligner si tu le veux.
|
||||
formule : à aligner si tu le veux. *(Tranché le 03/11 : le défaut reste, « garder la trace »
|
||||
dans F1.2, ta formule dans l'interface.)*
|
||||
9. **Lot T2, F1.1 et N1.1** : sans chiffre, pour ne pas dater le texte voté ; les chiffres de la
|
||||
production (64,4 à 91,9 % ; 9 « pour » sur 12) sont ici, calculés par le moteur.
|
||||
10. **Lot T3, F1.1 et le groupe d'essai** (pour information) : « B […] ne compte pas » est vrai dans
|
||||
la toile, faux dans un groupe de quelques membres, où B^W demande une voix de plus quand la part
|
||||
tombe juste sur un entier (deux membres qui votent : deux « pour »). Les textes disent que W est
|
||||
la toile de confiance et ne décrivent pas le groupe d'essai (point 4) : laissé. Si tu veux qu'ils
|
||||
le décrivent aussi, F1.1 dirait « dans la toile de confiance », comme F1.2.
|
||||
11. **Lot T3, l'interface** (hors de ce lot) : `settings.reading` dit encore « le groupe demande sa
|
||||
part de « pour » quand il vote comme d'habitude », sans « au-delà du quorum » pour cette part ;
|
||||
même imprécision que la phrase de F1.2 et N1.1 reprise ici (fr, en, es). *(Après T3c : l'interface
|
||||
dit aussi « comme d'habitude » dans `settings.share.note` et `settings.required.usual` ; les textes
|
||||
votés disent maintenant « à la participation habituelle ». Non touchée, ordre de T3c.)*
|
||||
12. **Lot T3c, F1.2, « Un vote permanent »** (pour information, hors du champ de T3c) : « Chacun peut
|
||||
changer ou retirer son vote » et « Une proposition entre en vigueur dès qu'elle atteint son seuil »
|
||||
ne disent pas la version retenue par une objection (ADR 0010 : on n'y ajoute jamais un « pour », et
|
||||
elle n'entre pas en vigueur tant que l'objection tient). Règles sans nombre, laissées : à écrire si
|
||||
tu le veux, par un autre lot.
|
||||
13. **Lot T3c, F1.1** (pour information) : « G : plus il est grand, plus la part demandée monte quand peu
|
||||
de gens votent » n'a pas d'effet sur un article dont l'auteur a fixé le quorum à W (la part y est M
|
||||
à tout G). Cas limite ; F1.1 n'a pas d'amendement, laissé.
|
||||
|
||||
## Les chiffres et leur source
|
||||
|
||||
@@ -522,17 +955,23 @@ Chaque nombre est vérifié par `every_number_of_the_quorum_texts_is_the_engine_
|
||||
| W relu toutes les 6 heures | F1.2 | `wot::REFRESH` (`crates/ld-server/src/wot.rs:13-14`) |
|
||||
| B = 0,1 | F1.2 | `FormulaParams::DEFAULT_BASE` |
|
||||
| Cran 3 par défaut : M = 55 %, quorum 50, 75 % | F1.2, N1.1 | `InertiaLevel::DEFAULT`, `majority_pct`, `share_pct` ; le quorum du cran |
|
||||
| M = 50, 50, 55, 60, 66 % ; s = 60, 67, 75, 83, 90 % | N1.1 | `InertiaLevel::majority_pct`, `share_pct` |
|
||||
| M = 50, 50, 55, 60, 66 % ; s = 60, 67, 75, 83, 90 % (la liste des crans ; s aussi dans la phrase de G) | N1.1 | `InertiaLevel::majority_pct`, `share_pct` |
|
||||
| Propositions seed au cran 2 | F1.2, N1.1 | `seed::SEED_PROPOSAL_LEVEL` ; les versions `seedProposal` du seed, leurs titres dans chaque langue |
|
||||
| Le titre au cran 4, l'ordre de chaque section au cran 2 | F1.2, N1.1 | le seed, `modules.title.inertia`, `modules.order.inertia` |
|
||||
| Tous les autres articles au cran 3 ; rien aux crans 1 et 5 | F1.2, N1.1 | le seed : chaque `inertia` (toutes égales, vérifié) |
|
||||
| 5 propositions ; 100 votants (deux fois le quorum de référence) | N1.1 | `Participation::MIN_SAMPLE` ; `Participation::of(W, voteFloor, None, 0).voters(W)`, `voteFloor` = 50 |
|
||||
| Entre 1 % et 50 % | N1.1 | `Participation::LOWEST`, `HIGHEST` |
|
||||
| 5 propositions ; 100 votants (deux fois le quorum de référence) | N1.1 | `Participation::MIN_SAMPLE` ; 2 × `voteFloor` (50), un paramètre, écrit avant les bornes : la participation de référence ne vaut 100 votants que sur une toile de 200 à 10 000 membres, les bornes la ramènent ailleurs (`the_reference_turnout_is_a_parameter_the_bounds_bring_back`) ; moins de 5 propositions votées dans l'année, la référence (lot T3c : `the_participation_habituelle_is_the_median_brought_back_within_its_bounds`, `tests/inertia.rs` `the_usual_participation_is_the_median_of_five_voted_proposals`) |
|
||||
| Ramenée entre 1 % et 50 % | N1.1 | `Participation::LOWEST`, `HIGHEST` ; la valeur bornée est la participation habituelle (lot T3c : `the_participation_habituelle_is_the_median_brought_back_within_its_bounds`) |
|
||||
| Depuis un an ; une fois par jour | N1.1 | `participation::WINDOW_DAYS` = 365, `EVERY_HOURS` = 24 |
|
||||
| 72, 81, 90, 100, 108 voix pour à 120 votants | N1.1 | `Scale::required(120, k, Q_k)`, W 7 224, médiane 120 ; déjà `inertia.rs`, `the_values_shown_to_yvv` |
|
||||
| 75 % des 120 votants, soit 90 | N1.1 | la part du cran 3 × 120, égale au seuil du moteur (vérifié) |
|
||||
| Cran 2 (quorum 12) : 9 « pour », 3 « contre » au plus | N1.1 | `threshold::yes_needed(0)`, puis `no_needed(9) − 1`, W 7 224, médiane 120 |
|
||||
| Cran 3 (quorum 50) : 39 « pour », 11 « contre » au plus | N1.1 | idem ; les deux exemples restent sous le quorum (vérifié) |
|
||||
|
||||
**Lot T3** : les quatre lignes de l'exemple de N1.1 sont ôtées de ce tableau (72, 81, 90, 100 et
|
||||
108 voix pour à 120 votants ; 75 % des 120 votants, soit 90 ; au cran 2, 9 « pour » et 3 « contre »
|
||||
au plus ; au cran 3, 39 et 11) : l'exemple est sorti du texte (point 3), parce que ses chiffres
|
||||
dépendaient de la toile et d'une participation (7 224 membres, 120 votants). Ils restent des
|
||||
chiffres de contrôle du moteur (`inertia.rs`, `the_values_shown_to_yvv`,
|
||||
`the_control_figures_of_the_quorum`), pas des textes. Tous les nombres qui restent sont des
|
||||
paramètres, qu'aucune toile ni participation ne change ; seul le quorum le plus bas est calculé sur W
|
||||
par le test, le même sur toute toile de 100 à 10 000 000 membres. « 100 votants » est un paramètre,
|
||||
pas une participation vraie sur toute toile (lot T3b, après la relecture de `2ce2028`).
|
||||
|
||||
Les règles sans chiffre :
|
||||
|
||||
@@ -543,6 +982,14 @@ Les règles sans chiffre :
|
||||
| Une version passe quand ses « pour » atteignent le seuil arrondi au-dessus | F1.1 | `threshold::passes`, `clamp_ceil` |
|
||||
| La participation habituelle ne dépend pas du quorum d'un article | N1.1 | `inertia::Participation::reference_ratio` (le quorum de référence du document) ; ADR 0013 |
|
||||
| Au départ, aucun article en vigueur | F1.2 | ADR 0011 ; le seed (aucune version `in_force` hors modules) |
|
||||
| Sous le quorum de l'article, autant de « pour » que si ce quorum avait voté (lot T3, puis T3b et T3c) | N1.1 | `threshold::judged_voters` ; `yes_needed(0)` égale `wot_threshold(W, Q)` ; `under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted` |
|
||||
| Dans Monnaie libre, un seul « pour » ne passe jamais (lot T3c, sur la grille) | F1.2 | `threshold::passes` ; `under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted` |
|
||||
| Au-delà du quorum de l'article, la part exigée baisse à chaque votant de plus, jusqu'à M (lot T3c) | N1.1 | `threshold::required_ratio`, `judged_voters` ; `the_share_asked_falls_beyond_the_article_s_quorum_down_to_m` |
|
||||
| La part du cran à la participation habituelle, au-delà du quorum de l'article (lot T3, puis T3c) | F1.2, N1.1 | `inertia::gradient`, `Participation::of` ; `the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum` |
|
||||
| G donne la part du cran au-delà du quorum de l'article, à la participation habituelle (lot T3b, puis T3c) | N1.1 | `inertia::gradient`, `threshold::judged_voters` ; `g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum` |
|
||||
| La participation habituelle ne dépend pas du quorum d'un article (lot T3c, servie) | N1.1 | `docs.rs` (`Participation::of` sur `vote_floor`) ; `tests/quorum.rs` `in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pending` |
|
||||
| Tout nouvel article commence au cran 3 (lot T3c) | F1.2 | `proposals.rs` (`InertiaLevel::DEFAULT`) ; `tests/quorum.rs` `the_author_of_an_article_sets_its_quorum_before_the_first_vote` |
|
||||
| B^W ne compte pas dans la toile (W de 100 à 10 000 000) | F1.1, F1.2 | `wot::PLAUSIBLE` ; `b_to_the_power_w_never_counts_in_the_web_of_trust` |
|
||||
|
||||
## Les textes dans le seed
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). Aucun article en vigueur au départ. I1, E1, E2 : Propositions seed (« seedProposal »), ouvertes, mises en avant pour amorcer le vote, sans niveau d'inertie ici : elles démarrent au cran SEED_PROPOSAL_LEVEL (crates/ld-server/src/seed.rs : 2 « basse », choix d'Yvv du 2026-11-02, qui remontera leur inertie par un vote une fois les textes passés ; ADR 0011). Tant qu'aucun article n'est en vigueur, la publication attend le premier texte. Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21), inertie 3 / 5. I1 « Raison d'être » remplace le Préambule (texte d'Yvv, revue du 2026-10-30). Modules : le titre du document à 4, l'ordre des articles de chaque section à 2, en vigueur sans vote (seed a minima ; revue du 2026-10-30, ADR 0009). F1.2, N1, N1.1, O1 réécrits selon les règles appliquées (Yvv, 2026-11-02). Quorums par cran (« quorums », ADR 0013, Yvv, 2026-11-02 et 03) : 12 · 12 · 50 · 50 · 50 dans Monnaie libre, 1 partout dans le groupe d'essai ; sous le quorum, les votants qui manquent comptent comme « non » ; voteFloor reste le quorum de référence de la participation p0 = 2 × voteFloor / W. Une base existante reçoit ces changements par des amendements gardés (seed_amend, seed_proposal_amend, quorum_amend), jamais en réinstanciant le document. F1.1, F1.2 et N1.1 réécrits pour le quorum par cran (ADR 0013 ; Yvv, 2026-11-02 et 03 ; lot T, textes relus dans docs/chantiers/acte-certification/textes-quorum-seed.md) : sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part du cran. Trois phrases reprises après la relecture de M6b (lot T2) : la part du cran, la plus haute au quorum, baisse quand la participation monte (F1.1) ; l'auteur fixe le quorum tant que l'article ne garde la trace d'aucun vote (F1.2) ; le quorum n'est pas un nombre minimal de votants (N1.1).",
|
||||
"_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). Aucun article en vigueur au départ. I1, E1, E12, E2 : Propositions seed (« seedProposal »), ouvertes, mises en avant pour amorcer le vote, sans niveau d'inertie ici : elles démarrent au cran SEED_PROPOSAL_LEVEL (crates/ld-server/src/seed.rs : 2 « basse », choix d'Yvv du 2026-11-02, qui remontera leur inertie par un vote une fois les textes passés ; ADR 0011). Tant qu'aucun article n'est en vigueur, la publication attend le premier texte. Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21), inertie 3 / 5. I1 « Raison d'être » remplace le Préambule (texte d'Yvv, revue du 2026-10-30). Modules : le titre du document à 4, l'ordre des articles de chaque section à 2, en vigueur sans vote (seed a minima ; revue du 2026-10-30, ADR 0009). Quorums par cran (« quorums », ADR 0013, Yvv, 2026-11-02 et 03) : 12 · 12 · 50 · 50 · 50 dans Monnaie libre, 1 partout dans le groupe d'essai ; sous le quorum, les votants qui manquent comptent comme « non » ; voteFloor reste le quorum de référence de la participation p0 = 2 × voteFloor / W. Une base existante reçoit ces changements par des amendements gardés (seed_amend, seed_proposal_amend, quorum_amend, article_amend, retire), jamais en réinstanciant le document. Lot T4 (Yvv, 2026-11-03 : « Modif des textes Acte engagement ») : I1, E1 et E2 réécrits ; E12 « Responsabilité civique », Proposition seed, entre E1 et E2 ; les sections formule (F1, F1.1, F1.2, F1.3), inertie (N1, N1.1) et ordonnancement (O1) sortent du document : ce sont des informations de fonctionnement, qui ne se votent pas (ADR 0014) ; leurs derniers textes restent dans crates/ld-server/tests/fixtures/seed-m8/ (textes relus dans docs/chantiers/acte-certification/textes-amorce.md).",
|
||||
"slug": "acte-engagement-certification",
|
||||
"title": {
|
||||
"fr": "Acte d'engagement Certification",
|
||||
@@ -50,9 +50,9 @@
|
||||
"es": "Razón de ser"
|
||||
},
|
||||
"body": {
|
||||
"fr": "*« Derrière une clé il n'y a qu'un seul être humain vivant ; derrière un être humain, il n'y a qu'une seule clé certifiante. »*\n\nC'est le seul et unique propos de la certification, qui donne à notre toile fiduciaire sa valeur. Nous pouvons nous fier à l'unicité et l'intégrité de tout geste signé par cette clé. C'est l'enjeu de la certification.\n\nCet acte d'engagement est par ailleurs le seul acte de nature contractuelle qui fonde l'usage de la première monnaie libre.",
|
||||
"en": "*“Behind a key there is only one living human being; behind a human being, there is only one certifying key.”*\n\nThis is the one and only purpose of certification, which gives our fiduciary web its value. We can rely on the uniqueness and integrity of every action signed with this key. That is what is at stake in certification.\n\nThis commitment act is, moreover, the only act of a contractual nature on which the use of the first libre currency is founded.",
|
||||
"es": "*«Detrás de una clave no hay más que un solo ser humano vivo; detrás de un ser humano, no hay más que una sola clave certificadora.»*\n\nEste es el propósito único y exclusivo de la certificación, que da su valor a nuestra red fiduciaria. Podemos fiarnos de la unicidad y la integridad de toda acción firmada con esta clave. Eso es lo que está en juego en la certificación.\n\nEsta acta de compromiso es, además, el único acto de naturaleza contractual en el que se funda el uso de la primera moneda libre."
|
||||
"fr": "*« Derrière une clé il y a un et un seul être humain vivant ; derrière un être humain, il y a une et une seule clé certifiante. »*\n\nC'est l'unique enjeu de la certification et de votre engagement, il donne à notre toile fiduciaire sa valeur : pouvoir nous fier à l'unicité et l'intégrité de tout geste signé par cette clé.\n\nCet acte d'engagement est par ailleurs le seul acte de nature contractuelle, qui fonde l'usage de la première monnaie libre.",
|
||||
"en": "*“Behind a key there is one and only one living human being; behind a human being, there is one and only one certifying key.”*\n\nThis is the one thing at stake in certification and in your commitment; it gives our fiduciary web its value: being able to rely on the uniqueness and integrity of every action signed with this key.\n\nThis commitment act is, moreover, the only act of a contractual nature, which founds the use of the first libre currency.",
|
||||
"es": "*«Detrás de una clave hay un solo y único ser humano vivo; detrás de un ser humano, hay una sola y única clave certificadora.»*\n\nEs lo único que está en juego en la certificación y en su compromiso; da su valor a nuestra red fiduciaria: poder fiarnos de la unicidad y la integridad de toda acción firmada con esta clave.\n\nEsta acta de compromiso es, además, el único acto de naturaleza contractual, que funda el uso de la primera moneda libre."
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -116,9 +116,31 @@
|
||||
"es": "Unicidad de la cuenta"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte cocréateur de monnaie G1.",
|
||||
"en": "I commit on my honour to have, now and ever, no more than one single and unique account co-creating G1 money.",
|
||||
"es": "Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta cocreadora de moneda G1."
|
||||
"fr": "Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte certifiant.",
|
||||
"en": "I commit on my honour to have, now and ever, no more than one single and unique certifying account.",
|
||||
"es": "Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta certificadora."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "E12",
|
||||
"section": "fondamental",
|
||||
"position": 4.5,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"seedProposal": true,
|
||||
"title": {
|
||||
"fr": "Responsabilité civique",
|
||||
"en": "Civic responsibility",
|
||||
"es": "Responsabilidad cívica"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Je déclare sur l'honneur avoir la capacité légale et physique de signer un contrat et d'en assumer la responsabilité civique, pour certifier à mon tour.",
|
||||
"en": "I declare on my honour that I have the legal and physical capacity to sign a contract and to assume its civic responsibility, in order to certify in my turn.",
|
||||
"es": "Declaro por mi honor tener la capacidad legal y física de firmar un contrato y de asumir su responsabilidad cívica, para certificar a mi vez."
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -138,9 +160,9 @@
|
||||
"es": "Reciprocidad"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Je m'engage sur l'honneur à ne certifier que des personnes physiques qui respectent scrupuleusement ces deux présents engagements fondamentaux.",
|
||||
"en": "I commit on my honour to certify only natural persons who scrupulously respect these two fundamental commitments.",
|
||||
"es": "Me comprometo por mi honor a certificar únicamente a personas físicas que respeten escrupulosamente estos dos compromisos fundamentales."
|
||||
"fr": "Je m'engage sur l'honneur à ne certifier que des personnes physiques qui s'engagent elles-mêmes, à scrupuleusement respecter ces trois principes fondamentaux.",
|
||||
"en": "I commit on my honour to certify only natural persons who themselves commit to scrupulously respecting these three fundamental principles.",
|
||||
"es": "Me comprometo por mi honor a certificar únicamente a personas físicas que se comprometan ellas mismas a respetar escrupulosamente estos tres principios fundamentales."
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -634,160 +656,6 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "F1",
|
||||
"section": "formule",
|
||||
"position": 27,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Formule de vote",
|
||||
"en": "Voting formula",
|
||||
"es": "Fórmula de votación"
|
||||
},
|
||||
"body": {
|
||||
"fr": "La formule qui régit l'adoption ou le rejet de chaque modification du présent document. Référence : g1vote (https://g1vote-view-237903.pages.duniter.org/).",
|
||||
"en": "The formula that governs the adoption or rejection of each amendment to this document. Reference: g1vote (https://g1vote-view-237903.pages.duniter.org/).",
|
||||
"es": "La fórmula que rige la adopción o el rechazo de cada modificación del presente documento. Referencia: g1vote (https://g1vote-view-237903.pages.duniter.org/)."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "F1.1",
|
||||
"section": "formule",
|
||||
"position": 28,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Formule du seuil WoT",
|
||||
"en": "WoT threshold formula",
|
||||
"es": "Fórmula del umbral WoT"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Le seuil est le nombre de voix pour qu'il faut à une version pour entrer en vigueur. Sous le quorum Q, les votants qui manquent comptent comme « non » : le seuil se calcule comme si Q personnes avaient voté. Au-delà, il faut la part de « pour » que demande le cran d'inertie de l'article, parmi tous les votants : cette part est la plus haute au quorum et baisse quand la participation monte, jusqu'à la majorité M quand tout le monde vote.\n\n`T' = max(T, Q)`\n`Seuil = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUne version passe quand ses voix pour atteignent le seuil, arrondi à l'entier au-dessus.\n\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q** : le quorum de l'article ; **T'** : les votants retenus, T, ou Q s'il en manque.\n- **W** : le nombre de membres de la toile de confiance Ğ1.\n- **B** : la base ; à la puissance W, elle est si petite qu'elle ne compte pas.\n- **M** : la majorité demandée quand tout le monde vote.\n- **G** : le gradient : plus il est grand, plus la part demandée monte quand peu de gens votent.\n\nLeurs valeurs sont dans « Paramètres par défaut » et « Niveaux d'inertie ».",
|
||||
"en": "The threshold is the number of votes in favour a version needs to come into force. Below the quorum Q, the voters who are missing count as votes against: the threshold is worked out as if Q people had voted. Beyond it, it takes the share of votes in favour that the article's level of inertia asks for, among all the voters: that share is highest at the quorum and falls as turnout rises, down to the majority M when everyone votes.\n\n`T' = max(T, Q)`\n`Threshold = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nA version passes when its votes in favour reach the threshold, rounded up to a whole number.\n\n- **T**: the votes cast on the version, for and against.\n- **Q**: the article's quorum; **T'**: the voters counted, T, or Q if some are missing.\n- **W**: the number of members of the Ğ1 web of trust.\n- **B**: the base; raised to the power W, it is so small that it does not count.\n- **M**: the majority asked for when everyone votes.\n- **G**: the gradient: the larger it is, the higher the share asked for when few people vote.\n\nTheir values are in “Default parameters” and “Inertia levels”.",
|
||||
"es": "El umbral es el número de votos a favor que necesita una versión para entrar en vigor. Por debajo del quórum Q, los votantes que faltan cuentan como «no»: el umbral se calcula como si hubieran votado Q personas. Más allá, hace falta la proporción de votos a favor que pide el nivel de inercia del artículo, entre todos los votantes: esa proporción es la más alta en el quórum y baja cuando sube la participación, hasta la mayoría M cuando vota todo el mundo.\n\n`T' = max(T, Q)`\n`Umbral = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUna versión pasa cuando sus votos a favor alcanzan el umbral, redondeado al entero superior.\n\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**: el quórum del artículo; **T'**: los votantes contados, T, o Q si faltan.\n- **W**: el número de miembros de la red de confianza Ğ1.\n- **B**: la base; elevada a la potencia W, es tan pequeña que no cuenta.\n- **M**: la mayoría pedida cuando vota todo el mundo.\n- **G**: el gradiente: cuanto mayor es, más alta es la proporción pedida cuando vota poca gente.\n\nSus valores están en «Parámetros por defecto» y «Niveles de inercia»."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "F1.2",
|
||||
"section": "formule",
|
||||
"position": 29,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Paramètres par défaut",
|
||||
"en": "Default parameters",
|
||||
"es": "Parámetros por defecto"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la façon dont le vote se déroule.\n\n**Un vote permanent, sans durée**\n\n- Chaque version se vote à tout moment, sans échéance. Chacun peut changer ou retirer son vote : le dernier fait foi.\n- Une proposition entre en vigueur dès qu'elle atteint son seuil. S'il y a déjà un texte en vigueur, il lui faut en plus une marge (voix pour moins voix exigées) plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir.\n- Si plusieurs propositions y parviennent, celle qui a la marge la plus large entre en vigueur.\n\n**Les lettres**\n\n- **W** : le nombre de membres de la toile de confiance Ğ1, relu sur la chaîne toutes les 6 heures.\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q**, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au cran 5. L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote. Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un texte.\n- **B = 0,1** : dans la toile de confiance, `B^W` est si petit qu'il ne compte pas.\n- **M** et **G** : donnés par le cran d'inertie de l'article (voir « Niveaux d'inertie »). G n'est pas fixé à la main : il se déduit de la participation habituelle du groupe.\n\n**Par défaut**\n\n- Le cran par défaut est le 3 (moyenne) : M = 55 %, un quorum de 50 dans Monnaie libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude. Tout nouvel article commence à ce cran.\n- Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, tous les autres articles au cran 3.",
|
||||
"en": "The values of the letters of the formula (see “WoT threshold formula”) and how the vote runs.\n\n**A permanent vote, with no duration**\n\n- Every version can be voted on at any time, with no deadline. Anyone can change or withdraw their vote: the last one is the one that counts.\n- A proposal comes into force as soon as it reaches its threshold. If a text is already in force, it also needs a wider margin (votes in favour minus votes required) than that text has. The replaced text becomes a proposal again, with its votes: it can come back.\n- If several proposals get there, the one with the widest margin comes into force.\n\n**The letters**\n\n- **W**: the number of members of the Ğ1 web of trust, read again from the chain every 6 hours.\n- **T**: the votes cast on the version, for and against.\n- **Q**, the quorum: below this number of voters, those who are missing count as votes against. Each article takes the one of its level: 12, 12, 50, 50 and 50 in Monnaie libre, from level 1 to level 5. The author of the first version of an article, an administrator for the starting texts, can set another one as long as the article keeps no trace of any vote. In Monnaie libre, never less than 2: a single vote never passes a text.\n- **B = 0.1**: in the web of trust, `B^W` is so small that it does not count.\n- **M** and **G**: given by the article's level of inertia (see “Inertia levels”). G is not set by hand: it follows from the group's usual turnout.\n\n**By default**\n\n- The default level is 3 (medium): M = 55%, a quorum of 50 in Monnaie libre, and the group asks for 75% of votes in favour when it votes as usual. Every new article starts at this level.\n- At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, all other articles at level 3.",
|
||||
"es": "Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y cómo se desarrolla la votación.\n\n**Una votación permanente, sin duración**\n\n- Cada versión se vota en cualquier momento, sin plazo. Cada cual puede cambiar o retirar su voto: el último es el que cuenta.\n- Una propuesta entra en vigor en cuanto alcanza su umbral. Si ya hay un texto en vigor, necesita además un margen (votos a favor menos votos exigidos) más amplio que el de ese texto. El texto sustituido vuelve a ser una propuesta, con sus votos: puede volver.\n- Si varias propuestas lo consiguen, entra en vigor la que tiene el margen más amplio.\n\n**Las letras**\n\n- **W**: el número de miembros de la red de confianza Ğ1, leído de nuevo en la cadena cada 6 horas.\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**, el quórum: por debajo de este número de votantes, los que faltan cuentan como «no». Cada artículo toma el de su nivel: 12, 12, 50, 50 y 50 en Monnaie libre, del nivel 1 al nivel 5. El autor de la primera versión de un artículo, un administrador para los textos de partida, puede fijar otro mientras el artículo no guarde rastro de ningún voto. En Monnaie libre, nunca menos de 2: un solo voto nunca hace pasar un texto.\n- **B = 0,1**: en la red de confianza, `B^W` es tan pequeño que no cuenta.\n- **M** y **G**: los da el nivel de inercia del artículo (ver «Niveles de inercia»). G no se fija a mano: se deduce de la participación habitual del grupo.\n\n**Por defecto**\n\n- El nivel por defecto es el 3 (media): M = 55 %, un quórum de 50 en Monnaie libre, y el grupo pide un 75 % de votos a favor cuando vota como de costumbre. Todo artículo nuevo empieza en este nivel.\n- Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "F1.3",
|
||||
"section": "formule",
|
||||
"position": 30,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Processus de dépôt officiel",
|
||||
"en": "Official filing process",
|
||||
"es": "Proceso de depósito oficial"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Lorsqu'une proposition alternative atteint le seuil d'adoption :\n\n1. Le texte de remplacement est intégré au document officiel\n2. Le hash IPFS du document mis à jour est calculé\n3. Le hash est ancré on-chain via `system.remark`\n4. Le dépôt git officiel est synchronisé\n5. Les applications (Cesium, Gecko) mettent à jour automatiquement",
|
||||
"en": "When an alternative proposal reaches the adoption threshold:\n\n1. The replacement text is incorporated into the official document\n2. The IPFS hash of the updated document is computed\n3. The hash is anchored on-chain via `system.remark`\n4. The official git repository is synchronised\n5. The applications (Cesium, Gecko) update automatically",
|
||||
"es": "Cuando una propuesta alternativa alcanza el umbral de adopción:\n\n1. El texto de sustitución se integra en el documento oficial\n2. Se calcula el hash IPFS del documento actualizado\n3. El hash se ancla on-chain mediante `system.remark`\n4. Se sincroniza el repositorio git oficial\n5. Las aplicaciones (Cesium, Gecko) se actualizan automáticamente"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "N1",
|
||||
"section": "inertie",
|
||||
"position": 31,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Réglage de l'inertie",
|
||||
"en": "Inertia setting",
|
||||
"es": "Ajuste de la inercia"
|
||||
},
|
||||
"body": {
|
||||
"fr": "Chaque article porte son inertie : un cran de 1 à 5 (voir « Niveaux d'inertie »). Plus le cran est haut, plus il faut de voix pour changer le texte quand peu de gens votent. 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.\n\nTout membre du groupe peut proposer un autre cran. Cette proposition se vote au cran actuel de l'article, comme son texte : l'inertie de l'inertie hérite du cran de l'article. Ainsi, un article au cran 4 ne descend qu'avec les voix que demande le cran 4.",
|
||||
"en": "Every article carries its inertia: a level from 1 to 5 (see “Inertia levels”). The higher the level, the more votes it takes to change the text when few people vote. The title of the document and the order of the articles of each section have theirs too, changed in the same way.\n\nAny member of the group can propose another level. That proposal is voted at the article's current level, like its text: the inertia of the inertia inherits the article's level. So an article at level 4 only comes down with the votes that level 4 asks for.",
|
||||
"es": "Cada artículo lleva su inercia: un nivel del 1 al 5 (ver «Niveles de inercia»). Cuanto más alto es el nivel, más votos hacen falta para cambiar el texto cuando vota poca gente. El título del documento y el orden de los artículos de cada sección también tienen la suya, que se cambia del mismo modo.\n\nCualquier miembro del grupo puede proponer otro nivel. Esa propuesta se vota con el nivel actual del artículo, como su texto: la inercia de la inercia hereda el nivel del artículo. Así, un artículo en el nivel 4 solo baja con los votos que pide el nivel 4."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "N1.1",
|
||||
"section": "inertie",
|
||||
"position": 32,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Niveaux d'inertie",
|
||||
"en": "Inertia levels",
|
||||
"es": "Niveles de inercia"
|
||||
},
|
||||
"body": {
|
||||
"fr": "À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). Chaque cran a aussi son quorum, qui n'est pas un nombre minimal de votants : sous le quorum, les votants qui manquent comptent comme « non ». La part dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà du quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.\n\n`Part exigée = M + (1 − M) × (1 − (T'/W)^G)`, où T' est le nombre de votants, ou le quorum s'il en manque.\n\n**Les cinq crans** : le quorum dans Monnaie libre, la part exigée quand le groupe vote comme d'habitude, puis quand tout le monde vote, et ce qui est à ce cran au départ.\n\n- **Cran 1 · très basse** : quorum 12 ; 60 % comme d'habitude, 50 % quand tout le monde vote. Aucun article au départ.\n- **Cran 2 · basse** : quorum 12 ; 67 % comme d'habitude, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.\n- **Cran 3 · moyenne** : quorum 50 ; 75 % comme d'habitude, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.\n- **Cran 4 · haute** : quorum 50 ; 83 % comme d'habitude, 60 % quand tout le monde vote. Le titre du document.\n- **Cran 5 · très haute** : quorum 50 ; 90 % comme d'habitude, 66 % quand tout le monde vote. Aucun article au départ.\n\n**La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. Tant que le groupe n'a pas voté sur 5 propositions, elle vaut deux fois le quorum de référence du document : 100 votants dans Monnaie libre. Elle ne dépend pas du quorum d'un article. Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour.\n\n**G** se calcule pour que la part exigée, à la participation habituelle, soit exactement celle du cran : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est la part exigée comme d'habitude et p̃ la participation habituelle divisée par W.\n\nExemple : avec 7 224 membres et une participation habituelle de 120 votants, quand 120 personnes votent, il faut 72, 81, 90, 100 ou 108 voix pour, du cran 1 au cran 5 ; au cran 3, 75 % des 120 votants, soit 90. Sous le quorum, les absents comptent comme « non » : au cran 2 (quorum 12), 9 voix pour passent avec 3 « contre » au plus ; au cran 3 (quorum 50), il faut 39 voix pour, avec 11 « contre » au plus.",
|
||||
"en": "At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). Each level also has its quorum, which is not a minimum number of voters: below the quorum, the voters who are missing count as votes against. The share depends on the level and on the number of voters, compared with the group's usual turnout: beyond the quorum, the more voters, the lower it gets, down to the majority M when everyone votes.\n\n`Share asked = M + (1 − M) × (1 − (T'/W)^G)`, where T' is the number of voters, or the quorum if some are missing.\n\n**The five levels**: the quorum in Monnaie libre, the share asked when the group votes as usual, then when everyone votes, and what is at that level at the start.\n\n- **Level 1 · very low**: quorum 12; 60% as usual, 50% when everyone votes. No article at the start.\n- **Level 2 · low**: quorum 12; 67% as usual, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.\n- **Level 3 · medium**: quorum 50; 75% as usual, 55% when everyone votes. The default level: all other articles, and every new article.\n- **Level 4 · high**: quorum 50; 83% as usual, 60% when everyone votes. The title of the document.\n- **Level 5 · very high**: quorum 50; 90% as usual, 66% when everyone votes. No article at the start.\n\n**The usual turnout** is the median number of voters on the group's proposals voted on over the past year. Until the group has voted on 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It stays between 1% and 50% of the electorate, and is measured once a day.\n\n**G** is computed so that the share asked, at the usual turnout, is exactly that of the level: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is the share asked as usual and p̃ the usual turnout divided by W.\n\nExample: with 7,224 members and a usual turnout of 120 voters, when 120 people vote, it takes 72, 81, 90, 100 or 108 votes in favour, from level 1 to level 5; at level 3, 75% of the 120 voters, i.e. 90. Below the quorum, those missing count as votes against: at level 2 (quorum 12), 9 votes in favour pass with 3 votes against at most; at level 3 (quorum 50), it takes 39 votes in favour, with 11 votes against at most.",
|
||||
"es": "En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). Cada nivel tiene además su quórum, que no es un número mínimo de votantes: por debajo del quórum, los votantes que faltan cuentan como «no». La proporción depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá del quórum, cuantos más votantes, más baja, hasta la mayoría M cuando vota todo el mundo.\n\n`Proporción exigida = M + (1 − M) × (1 − (T'/W)^G)`, donde T' es el número de votantes, o el quórum si faltan.\n\n**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida cuando el grupo vota como de costumbre, luego cuando vota todo el mundo, y lo que está en ese nivel al inicio.\n\n- **Nivel 1 · muy baja**: quórum 12; 60 % como de costumbre, 50 % cuando vota todo el mundo. Ningún artículo al inicio.\n- **Nivel 2 · baja**: quórum 12; 67 % como de costumbre, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.\n- **Nivel 3 · media**: quórum 50; 75 % como de costumbre, 55 % cuando vota todo el mundo. El nivel por defecto: todos los demás artículos, y todo artículo nuevo.\n- **Nivel 4 · alta**: quórum 50; 83 % como de costumbre, 60 % cuando vota todo el mundo. El título del documento.\n- **Nivel 5 · muy alta**: quórum 50; 90 % como de costumbre, 66 % cuando vota todo el mundo. Ningún artículo al inicio.\n\n**La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Hasta que el grupo haya votado 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se mantiene entre el 1 % y el 50 % del electorado, y se mide una vez al día.\n\n**G** se calcula para que la proporción exigida, con la participación habitual, sea exactamente la del nivel: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es la proporción exigida como de costumbre y p̃ la participación habitual dividida por W.\n\nEjemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 72, 81, 90, 100 o 108 votos a favor, del nivel 1 al nivel 5; en el nivel 3, el 75 % de los 120 votantes, es decir 90. Por debajo del quórum, los que faltan cuentan como «no»: en el nivel 2 (quórum 12), 9 votos a favor pasan con 3 en contra como mucho; en el nivel 3 (quórum 50), hacen falta 39 votos a favor, con 11 en contra como mucho."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "O1",
|
||||
"section": "ordonnancement",
|
||||
"position": 33,
|
||||
"inertia": 3,
|
||||
"versions": [
|
||||
{
|
||||
"label": "1.0.0",
|
||||
"status": "open",
|
||||
"title": {
|
||||
"fr": "Ordonnancement du document",
|
||||
"en": "Ordering of the document",
|
||||
"es": "Ordenación del documento"
|
||||
},
|
||||
"body": {
|
||||
"fr": "L'ordre des articles de chaque section est lui-même soumis au vote. En fin de section, le module « Ordre des articles » se vote comme un article, au cran 2 (basse) au départ : chaque proposition est la liste des articles de la section, dans un autre ordre. Quand un ordre entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change.\n\nUn nouvel article se place à la fin de sa section ; sa place se vote ensuite par l'ordre de la section. Un ordre proposé garde ses votes quand la section change : un article arrivé depuis s'y ajoute à la fin, un article sorti en disparaît. Une section d'un seul article n'a pas d'ordre à voter.\n\nLe titre du document est lui aussi un module, voté au cran 4 (haute) au départ. Le titre de chaque article, lui, se vote avec son texte.",
|
||||
"en": "The order of the articles of each section is itself put to the vote. At the end of each section, the “Order of the articles” module is voted on like an article, at level 2 (low) at the start: each proposal is the list of the section's articles, in another order. When an order comes into force, only the articles of that section change place; no text changes.\n\nA new article goes at the end of its section; its place is then voted on through the order of the section. A proposed order keeps its votes when the section changes: an article that has arrived since is added at the end, an article that has left drops out of it. A section with a single article has no order to vote on.\n\nThe title of the document is also a module, voted on at level 4 (high) at the start. The title of each article is voted on with its text.",
|
||||
"es": "El orden de los artículos de cada sección está a su vez sometido a votación. Al final de cada sección, el módulo «Orden de los artículos» se vota como un artículo, con el nivel 2 (baja) al inicio: cada propuesta es la lista de los artículos de la sección, en otro orden. Cuando un orden entra en vigor, solo los artículos de esa sección cambian de lugar; ningún texto cambia.\n\nUn artículo nuevo se coloca al final de su sección; su lugar se vota después mediante el orden de la sección. Un orden propuesto conserva sus votos cuando la sección cambia: un artículo llegado después se añade al final, un artículo que ha salido desaparece de él. Una sección de un solo artículo no tiene orden que votar.\n\nEl título del documento es también un módulo, votado con el nivel 4 (alta) al inicio. El título de cada artículo, en cambio, se vota con su texto."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -142,10 +142,10 @@ test('C17 — Décisions : une ligne par doc, les modules à voter, dont sans to
|
||||
const rows = page.locator('ul.lines > li');
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(rows.first()).toContainText("Acte d'engagement Certification");
|
||||
// 33 articles, the title of the document and the order of 7 sections (ADR 0009; the
|
||||
// section of a single article has no order to decide).
|
||||
await expect(rows.first()).toContainText('41 modules à voter');
|
||||
await expect(rows.first()).toContainText('dont 41 sans ton vote');
|
||||
// 27 articles, the title of the document and the order of its 5 sections (ADR 0009; since
|
||||
// lot T4, the rules of operation are out of the document, ADR 0014).
|
||||
await expect(rows.first()).toContainText('33 modules à voter');
|
||||
await expect(rows.first()).toContainText('dont 33 sans ton vote');
|
||||
// A vote on one version of E3 (it has a variant): one module fewer without her vote. « Contre »
|
||||
// counts as much, and leaves the previews of C9 unchanged (the server is shared). In Monnaie
|
||||
// libre the vote goes through the Ğ1 chain: the ballot, then what her wallet (Charlie's
|
||||
@@ -165,8 +165,8 @@ test('C17 — Décisions : une ligne par doc, les modules à voter, dont sans to
|
||||
.poll(async () => (await (await page.request.get(`/api/ballots/${ballot.id}`)).json()).ballot.status, { timeout: 15_000 })
|
||||
.toBe('confirmed');
|
||||
await page.reload();
|
||||
await expect(rows.first()).toContainText('41 modules à voter');
|
||||
await expect(rows.first()).toContainText('dont 40 sans ton vote');
|
||||
await expect(rows.first()).toContainText('33 modules à voter');
|
||||
await expect(rows.first()).toContainText('dont 32 sans ton vote');
|
||||
await rows.first().getByRole('link').click();
|
||||
await expect(page).toHaveURL(path(DOC));
|
||||
});
|
||||
|
||||
+2
-1
@@ -119,7 +119,8 @@ export async function testDoc(page: Page): Promise<{
|
||||
* force in the test group, as the vote does it: voices for until each enters, then those
|
||||
* voices withdrawn. A version in force stays so (a permanent vote: only a rival displaces
|
||||
* it), at zero votes, as the controls written before the seed proposals found it. Never in
|
||||
* Monnaie libre, whose vote goes through the chain; nothing done twice.
|
||||
* Monnaie libre, whose vote goes through the chain; nothing done twice. E12 « Responsabilité
|
||||
* civique », the fourth (lot T4), stays a seed proposal: open, as C35 reads it.
|
||||
*/
|
||||
export async function seedsInForce(browser: Browser) {
|
||||
const voices = [await person(browser, 'Amorce A'), await person(browser, 'Amorce B')];
|
||||
|
||||
+66
-1
@@ -1,5 +1,5 @@
|
||||
import { expect, test, type Locator } from '@playwright/test';
|
||||
import { DOC, expectWelcome, shot, signIn } from './helpers';
|
||||
import { DOC, expectWelcome, propose, seedsInForce, shot, signIn, TEST_DOC, testDoc } from './helpers';
|
||||
|
||||
/** WCAG contrast ratio of two CSS colours `rgb(r, g, b)`. */
|
||||
function contrast(a: string, b: string): number {
|
||||
@@ -112,3 +112,68 @@ test('M1 — mobile : logo dans la barre, l’accueil sous la voûte, menu en ti
|
||||
await panel.getByRole('group', { name: 'Tout le document' }).getByRole('button', { name: 'Vider' }).click();
|
||||
await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible();
|
||||
});
|
||||
|
||||
/** The height of a target, in CSS pixels. */
|
||||
async function height(target: Locator): Promise<number> {
|
||||
return (await target.boundingBox())?.height ?? 0;
|
||||
}
|
||||
|
||||
/** Every gesture in sight under `root` — buttons, links, summaries — under 44 px high. */
|
||||
async function small(root: Locator): Promise<string[]> {
|
||||
return root.evaluate((el) =>
|
||||
[...el.querySelectorAll('button, a[href], summary, [role="radio"]')]
|
||||
.filter((b) => b.checkVisibility({ visibilityProperty: true }) && b.getClientRects().length > 0)
|
||||
.map((b) => ({ what: (b.textContent || b.getAttribute('aria-label') || '').replace(/\s+/g, ' ').trim(), h: b.getBoundingClientRect().height }))
|
||||
.filter((x) => x.h < 44)
|
||||
.map((x) => `${x.what} (${Math.round(x.h)} px)`),
|
||||
);
|
||||
}
|
||||
|
||||
test('M2 — téléphone : les gestes du lecteur font au moins 44 px, les trois lectures se lisent en entier', async ({ page, browser }) => {
|
||||
await seedsInForce(browser);
|
||||
await signIn(page, 'Chiara');
|
||||
// In the test group: a rival of the Raison d'être (in force), so that its vote names its
|
||||
// « Autres versions », and an article of hers, whose quorum she sets before any vote.
|
||||
const doc = await testDoc(page);
|
||||
const i1 = doc.units.find((u) => u.code === 'I1');
|
||||
const r = await page.request.post(`/api/docs/${doc.id}/proposals`, {
|
||||
data: { kind: 'text', unitId: i1?.id, title: "Raison d'être", body: 'Une clé, une personne vivante : la certification ne garantit rien d’autre.', submit: true },
|
||||
headers: { 'x-requested-with': 'libredecision' },
|
||||
});
|
||||
expect(r.ok()).toBe(true);
|
||||
const mine = await propose(page, 'Cibles au doigt', 'Je propose cet article pour régler son quorum au doigt.');
|
||||
await page.goto(TEST_DOC);
|
||||
|
||||
// The three readings, each whole on the screen: « Si tout passe » is never cut (lot U2).
|
||||
const views = page.getByRole('radiogroup', { name: 'Lire' });
|
||||
const frame = await views.boundingBox();
|
||||
expect.soft(await views.evaluate((el) => el.scrollWidth <= el.clientWidth + 1)).toBe(true);
|
||||
for (const reading of await views.getByRole('radio').all()) {
|
||||
const b = await reading.boundingBox();
|
||||
expect.soft(b && frame && b.x >= frame.x - 1 && b.x + b.width <= frame.x + frame.width + 1, await reading.textContent()).toBe(true);
|
||||
}
|
||||
await expect.soft(views.getByRole('radio', { name: 'Si tout passe' })).toBeInViewport({ ratio: 1 });
|
||||
await shot(page, 'm2-1-lectures');
|
||||
|
||||
// « Tout déplier », then the vote of the Raison d'être: every gesture a finger's target.
|
||||
expect.soft(await height(page.getByRole('button', { name: 'Tout déplier' }))).toBeGreaterThanOrEqual(44);
|
||||
const first = page.locator('section.text article.unit').first();
|
||||
await first.getByRole('button', { name: 'Le vote' }).click();
|
||||
const fold = first.locator('section.fold');
|
||||
const others = fold.getByRole('button', { name: 'Autres versions (1)' });
|
||||
expect.soft(await height(others)).toBeGreaterThanOrEqual(44);
|
||||
await others.click();
|
||||
await fold.getByText('La formule du seuil').click();
|
||||
expect.soft(await small(fold)).toEqual([]);
|
||||
await shot(page, 'm2-2-vote-deplie');
|
||||
|
||||
// Her own article: « Fixer ce quorum » and the rest of its vote.
|
||||
await page.goto(`${TEST_DOC}?vote=${mine}`);
|
||||
const hers = page.getByRole('region', { name: 'Le vote de « Cibles au doigt »' });
|
||||
const set = hers.getByRole('button', { name: 'Fixer ce quorum' });
|
||||
expect.soft(await height(set)).toBeGreaterThanOrEqual(44);
|
||||
await hers.getByText("Proposer un changement d'inertie").click();
|
||||
expect.soft(await small(hers)).toEqual([]);
|
||||
await set.scrollIntoViewIfNeeded();
|
||||
await shot(page, 'm2-3-quorum');
|
||||
});
|
||||
|
||||
@@ -42,9 +42,10 @@ test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(TEST_DOC);
|
||||
await unfold(page);
|
||||
// Each section ends with its order, in one line; a section of one article has nothing to decide.
|
||||
// Each section ends with its order, in one line: five sections (formule, inertie and
|
||||
// ordonnancement are out of the document, ADR 0014).
|
||||
const modules = page.getByRole('group', { name: /^Ordre des articles · / });
|
||||
await expect(modules).toHaveCount(7);
|
||||
await expect(modules).toHaveCount(5);
|
||||
await expect(page.getByRole('group', { name: 'Ordre des articles · Ordonnancement' })).toHaveCount(0);
|
||||
const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' });
|
||||
await expect(order).toContainText('Ordre de la section');
|
||||
@@ -60,8 +61,9 @@ test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée
|
||||
await fond.hover();
|
||||
await fond.getByRole('button', { name: 'Le vote' }).click();
|
||||
const fondVote = page.getByRole('region', { name: 'Le vote de « Ordre des articles · Engagements fondamentaux »' });
|
||||
await expect(fondVote.getByText('inertie 2 / 5 · basse', { exact: true })).toBeVisible();
|
||||
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Réciprocité']);
|
||||
// Its inertia, said once, in its own place (lot U2).
|
||||
await expect(fondVote.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible();
|
||||
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Responsabilité civique', 'Réciprocité']);
|
||||
await fond.getByRole('button', { name: 'Le vote' }).click();
|
||||
|
||||
// Another order: « Réordonner », the article, then its new place — in place, no panel.
|
||||
@@ -171,7 +173,7 @@ test('C26 — le titre du document : proposé et voté en tête, ses traductions
|
||||
// In force now: its vote stays unfolded, under the title.
|
||||
const vote = bar.locator('section.fold');
|
||||
await expect(bar.getByRole('button', { name: 'Le vote' })).toHaveAttribute('aria-expanded', 'true');
|
||||
await expect(vote.getByText('inertie 4 / 5 · haute', { exact: true })).toBeVisible();
|
||||
await expect(vote.getByRole('heading', { name: 'Inertie · 4 / 5 · haute' })).toBeVisible();
|
||||
await expect(vote.getByText(`${voices} pour · 0 contre`)).toBeVisible();
|
||||
await shot(page, 'c26-2-titre-vote');
|
||||
await bar.getByRole('button', { name: 'Le vote' }).click();
|
||||
@@ -198,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');
|
||||
});
|
||||
|
||||
|
||||
@@ -243,11 +243,11 @@ test('C31 — une coquille sur un texte en vigueur attend sa validation, puis s
|
||||
const panel = vera.locator('aside.panel');
|
||||
await panel.getByText('Corriger une coquille').click();
|
||||
const body = panel.getByLabel('Texte', { exact: true });
|
||||
await body.fill((await body.inputValue()).replace("C'est le seul", 'C’est le seul'));
|
||||
await body.fill((await body.inputValue()).replace("C'est l'unique", 'C’est l’unique'));
|
||||
await panel.getByRole('button', { name: 'Enregistrer la correction' }).click();
|
||||
await expect(vera.getByText(/^Ta correction attend un administrateur, ce texte venant du seed/)).toBeVisible();
|
||||
// In force, only a validated fix changes the text.
|
||||
await expect(purpose).toContainText("C'est le seul et unique propos");
|
||||
await expect(purpose).toContainText("C'est l'unique enjeu");
|
||||
await expect(panel.locator('li.fix')).toContainText("en attente : s'applique d'elle-même le");
|
||||
|
||||
await ada.reload();
|
||||
@@ -258,7 +258,7 @@ test('C31 — une coquille sur un texte en vigueur attend sa validation, puis s
|
||||
await expect(ada.getByText('Correction appliquée.')).toBeVisible();
|
||||
|
||||
await vera.reload();
|
||||
await expect(purpose).toContainText('C’est le seul et unique propos');
|
||||
await expect(purpose).toContainText('C’est l’unique enjeu');
|
||||
const doc = await testDoc(vera);
|
||||
const unit = doc.units.find((u) => u.inForce?.title === "Raison d'être");
|
||||
const after = await details(vera, unit?.inForce?.id ?? '');
|
||||
|
||||
@@ -10,6 +10,8 @@ test.beforeAll(async ({ browser }) => seedsInForce(browser));
|
||||
|
||||
const EF = 'Engagements fondamentaux';
|
||||
const E1 = 'Unicité du compte';
|
||||
// Lot T4 (Yvv, 2026-11-03): between the two, a seed proposal, left open by `seedsInForce`.
|
||||
const EC = 'Responsabilité civique';
|
||||
const E2 = 'Réciprocité';
|
||||
const ORDER = `Ordre des articles · ${EF}`;
|
||||
const CSRF = { 'x-requested-with': 'libredecision' };
|
||||
@@ -41,50 +43,58 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
|
||||
await expect(page.locator('aside.panel')).toHaveCount(0);
|
||||
const list = page.locator('section.text').getByRole('list', { name: 'Nouvel ordre des articles' });
|
||||
const names = list.locator('button.item .name');
|
||||
await expect(names).toHaveText([EF, E1, E2]);
|
||||
await expect(names).toHaveText([EF, E1, EC, E2]);
|
||||
// The articles in force of the section are in the list now, not twice; the one only proposed says so.
|
||||
await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(0);
|
||||
await expect(list.locator('li', { hasText: EF })).toContainText('proposé');
|
||||
await expect(list.locator('.grip')).toHaveCount(3);
|
||||
await expect(list.locator('.grip')).toHaveCount(4);
|
||||
const submit = page.getByRole('button', { name: 'Proposer cet ordre' });
|
||||
await expect(submit).toBeDisabled();
|
||||
// Off, and it says why (UX-lecteur.md: « le bouton reste éteint et le dit » — lot U2).
|
||||
const why = "C'est l'ordre en vigueur : déplace au moins un article.";
|
||||
await expect(page.getByText(why)).toBeVisible();
|
||||
await expect(submit).toHaveAccessibleDescription(why);
|
||||
await shot(page, 'c35-2-poignees');
|
||||
|
||||
// By dragging its handle: Réciprocité onto the place of Unicité du compte.
|
||||
await list.locator('li', { hasText: E2 }).locator('.grip').dragTo(list.locator('li', { hasText: E1 }));
|
||||
await expect(names).toHaveText([EF, E2, E1]);
|
||||
await expect(page.getByText(`« ${E2} » en place 2 sur 3.`)).toBeAttached();
|
||||
// By dragging its handle: Réciprocité onto the place of the article above it, Responsabilité
|
||||
// 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();
|
||||
await expect(submit).toBeEnabled();
|
||||
await expect(page.getByText(why)).toHaveCount(0);
|
||||
// With the keyboard: the article keeps the focus, the arrows move it.
|
||||
const items = list.locator('button.item');
|
||||
await items.nth(1).focus();
|
||||
await items.nth(2).focus();
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await expect(names).toHaveText([E2, EF, E1]);
|
||||
await expect(items.nth(0)).toBeFocused();
|
||||
await expect(names).toHaveText([EF, E2, E1, EC]);
|
||||
await expect(items.nth(1)).toBeFocused();
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await expect(names).toHaveText([EF, E2, E1]);
|
||||
await expect(names).toHaveText([EF, E1, E2, EC]);
|
||||
// Without dragging: its arrows, or an article then its new place.
|
||||
await list.getByRole('button', { name: `Descendre « ${E2} »` }).click();
|
||||
await expect(names).toHaveText([EF, E1, E2]);
|
||||
await expect(names).toHaveText([EF, E1, EC, E2]);
|
||||
await expect(submit).toBeDisabled();
|
||||
await items.nth(2).click();
|
||||
await expect(items.nth(2)).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(page.getByText(why)).toBeVisible();
|
||||
await items.nth(3).click();
|
||||
await expect(items.nth(3)).toHaveAttribute('aria-pressed', 'true');
|
||||
await items.nth(1).click();
|
||||
await expect(names).toHaveText([EF, E2, E1]);
|
||||
await expect(names).toHaveText([EF, E2, E1, EC]);
|
||||
// « Annuler » gives the section back as it read.
|
||||
await page.getByRole('button', { name: 'Annuler' }).click();
|
||||
await expect(list).toHaveCount(0);
|
||||
await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(1);
|
||||
await line.getByRole('button', { name: 'Réordonner' }).click();
|
||||
await list.getByRole('button', { name: `Monter « ${E2} »` }).click();
|
||||
await expect(names).toHaveText([EF, E2, E1]);
|
||||
await list.getByRole('button', { name: `Monter « ${E2} »` }).click();
|
||||
await expect(names).toHaveText([EF, E2, E1, EC]);
|
||||
|
||||
// Then the vote: the order goes to the vote, and its vote unfolds at once, in the same place.
|
||||
await submit.click();
|
||||
await expect(page.getByText('Ordre proposé au vote.')).toBeVisible();
|
||||
const fold = line.locator('section.fold');
|
||||
await expect(fold).toHaveAttribute('aria-label', `Le vote de « ${ORDER} »`);
|
||||
await expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([EF, E2, E1]);
|
||||
await expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([EF, E2, E1, EC]);
|
||||
await expect(line).toContainText('1 ordre proposé');
|
||||
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
|
||||
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
@@ -145,6 +155,9 @@ test('C36 — combien de oui pour passer, combien de non pour bloquer : sous l
|
||||
await expect(need).toBeVisible();
|
||||
// The quorum in a tooltip, never as a title.
|
||||
await expect(need).toHaveAttribute('title', 'Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ».');
|
||||
// Said once: the line of the seed proposal, right above its vote, does not say it again (lot U2).
|
||||
await expect(i1.locator('footer')).not.toContainText(/oui (sur|pour)/);
|
||||
await expect(i1.locator('footer')).not.toContainText(/\d+ pour · \d+ contre/);
|
||||
// In the list, short: at level 3, quorum 50.
|
||||
await unfold(page);
|
||||
await expect(deepRow(page, 'Vérification personnelle de la clé')).toContainText('39 oui sur 50');
|
||||
@@ -237,3 +250,48 @@ test('C37 — le quorum d’un article : son auteur le fixe avant le premier vot
|
||||
await expect(fold.getByRole('region', { name: /^Quorum/ })).toHaveCount(0);
|
||||
await yvv.context().close();
|
||||
});
|
||||
|
||||
test('C38 — l’en-tête compte ce que montre la liste ; un changement d’inertie d’un ordre ou du titre se trouve à sa ligne', async ({ page }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
/** The versions « Propositions en cours » shows: one per line, or those inside a line of several. */
|
||||
const listed = () =>
|
||||
page.locator('ul.deep li.article').evaluateAll((lis) => lis.reduce((n, li) => n + Math.max(1, li.querySelectorAll('ul.versions > li').length), 0));
|
||||
// Monnaie libre, nothing in force: the seed proposals at the head, said apart; the count of the
|
||||
// proposals in progress is what the list below shows (lot U2: the orders are not counted there).
|
||||
await page.goto(DOC);
|
||||
await unfold(page);
|
||||
const facts = page.locator('.facts');
|
||||
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
|
||||
// the line of its module, where its vote unfolds — never a line of « Propositions en cours ».
|
||||
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 modules: { id: string; inertia: number }[] = [d.modules.orders.find((o: { section: string }) => o.section === 'conclusion'), d.modules.title];
|
||||
const made: string[] = [];
|
||||
for (const unit of modules) {
|
||||
const r = await page.request.post(`/api/units/${unit.id}/inertia`, { data: { target: unit.inertia === 5 ? 4 : unit.inertia + 1 }, headers: CSRF });
|
||||
expect(r.ok()).toBe(true);
|
||||
made.push((await r.json()).id);
|
||||
}
|
||||
await page.goto(TEST_DOC);
|
||||
const title = page.getByRole('group', { name: 'Titre du document' });
|
||||
const titleChange = title.getByRole('button', { name: "1 changement d'inertie proposé" });
|
||||
await expect(titleChange).toBeVisible();
|
||||
await titleChange.click();
|
||||
await expect(title.getByRole('region', { name: /^Le vote de « inertie vers \d \/ 5 · / })).toBeVisible();
|
||||
await shot(page, 'c38-1-titre-inertie');
|
||||
await unfold(page);
|
||||
await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`));
|
||||
await expect(page.locator('ul.deep').getByText(/inertie vers/)).toHaveCount(0);
|
||||
const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' });
|
||||
const orderChange = order.getByRole('button', { name: "1 changement d'inertie proposé" });
|
||||
await expect(orderChange).toBeVisible();
|
||||
await orderChange.click();
|
||||
await expect(order.getByRole('region', { name: /^Le vote de « inertie vers \d \/ 5 · / })).toBeVisible();
|
||||
await order.scrollIntoViewIfNeeded();
|
||||
await shot(page, 'c38-2-ordre-inertie');
|
||||
// Withdrawn: the group is left as found.
|
||||
for (const id of made) expect((await page.request.post(`/api/proposals/${id}/withdraw`, { headers: CSRF })).ok()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import { devices, 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');
|
||||
});
|
||||
|
||||
/**
|
||||
* What of the table of the five levels is not seen whole in the settings panel: a header or a
|
||||
* cell that leaves the table or the panel, a text that leaves its cell; the table, the panel or
|
||||
* the page scrolled sideways to reach it. Empty when every column reads in full.
|
||||
*/
|
||||
async function hiddenInSettings(page: Page): Promise<string[]> {
|
||||
return page.evaluate(() => {
|
||||
const panel = document.querySelector('aside.panel');
|
||||
const scroll = panel?.querySelector('.settings .scroll');
|
||||
const table = scroll?.querySelector('table');
|
||||
const body = panel?.querySelector('.body');
|
||||
if (!panel || !scroll || !table || !body) return ['pas de tableau dans le panneau'];
|
||||
const faults: string[] = [];
|
||||
for (const [el, what] of [
|
||||
[scroll, 'le tableau'],
|
||||
[body, 'le panneau'],
|
||||
[document.documentElement, 'la page'],
|
||||
] as const) {
|
||||
if (el.scrollWidth > el.clientWidth + 1) faults.push(`${what} défile de côté (${el.scrollWidth} > ${el.clientWidth})`);
|
||||
}
|
||||
const p = panel.getBoundingClientRect();
|
||||
const s = scroll.getBoundingClientRect();
|
||||
const left = Math.max(p.left, s.left, 0);
|
||||
const right = Math.min(p.right, s.right, document.documentElement.clientWidth);
|
||||
const cols = table.querySelectorAll('thead th').length;
|
||||
if (cols !== 6) faults.push(`${cols} colonnes au lieu de 6`);
|
||||
for (const cell of table.querySelectorAll('th, td')) {
|
||||
const name = (cell.textContent ?? '').replace(/\s+/g, ' ').trim();
|
||||
const c = cell.getBoundingClientRect();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(cell);
|
||||
const r = range.getBoundingClientRect();
|
||||
if (c.left < left - 0.5 || c.right > right + 0.5)
|
||||
faults.push(`« ${name} » : sa cellule sort du panneau (${Math.round(c.left)}–${Math.round(c.right)} hors de ${Math.round(left)}–${Math.round(right)})`);
|
||||
if (r.left < c.left - 0.5 || r.right > c.right + 0.5) faults.push(`« ${name} » : son texte sort de sa cellule`);
|
||||
}
|
||||
return faults;
|
||||
});
|
||||
}
|
||||
|
||||
test('C44 — les réglages de la formule se lisent en entier : chaque colonne des cinq crans et chaque en-tête dans le panneau, sur bureau et sur téléphone', async ({ page, browser }) => {
|
||||
// Lot U3b (03/11): on a desktop, the panel cut the table on the right, « Voix pour » out of sight.
|
||||
await signIn(page, 'Chiara');
|
||||
const g = await (await page.request.get('/api/groups/monnaie-libre')).json();
|
||||
const d = await (await page.request.get(`/api/docs/${g.docs[0].id}`)).json();
|
||||
const e1 = d.units.find((u: { code: string }) => u.code === 'E1');
|
||||
const version: string = e1.proposals.find((p: { seedProposal?: boolean }) => p.seedProposal).id;
|
||||
const phone = await browser.newContext({ ...devices['Pixel 7'], locale: 'fr-FR' });
|
||||
const screens: { name: string; tag: string; page: Page; size?: { width: number; height: number } }[] = [
|
||||
{ name: 'bureau 1360 × 900', tag: 'bureau-1360', page, size: { width: 1360, height: 900 } },
|
||||
{ name: 'bureau 1024 × 768', tag: 'bureau-1024', page, size: { width: 1024, height: 768 } },
|
||||
{ name: 'téléphone (Pixel 7)', tag: 'telephone', page: await phone.newPage() },
|
||||
];
|
||||
for (const screen of screens) {
|
||||
const p = screen.page;
|
||||
if (screen.size) await p.setViewportSize(screen.size);
|
||||
if (p !== page) await signIn(p, 'Chiara');
|
||||
// From the head of the document (the usual turnout), then from a version (« cet article » in its
|
||||
// row); in each language, whose headers are longer in Spanish (« Proporción exigida », « Votos a favor »).
|
||||
for (const [lang, last, current] of [
|
||||
['fr', 'Voix pour', 'cet article'],
|
||||
['en', 'Votes for', 'this article'],
|
||||
['es', 'Votos a favor', 'este artículo'],
|
||||
]) {
|
||||
for (const from of ['tete', 'version']) {
|
||||
await p.goto(from === 'tete' ? `${DOC}?onglet=reglages` : `${DOC}?onglet=reglages&version=${version}`);
|
||||
if ((await p.evaluate(() => localStorage.getItem('ld:lang'))) !== lang) {
|
||||
await p.evaluate((l) => localStorage.setItem('ld:lang', l), lang);
|
||||
await p.reload();
|
||||
}
|
||||
const panel = p.locator('aside.panel');
|
||||
await expect(panel.locator('table tbody tr')).toHaveCount(5);
|
||||
if (from === 'version') await expect(panel.locator('tr.current th')).toContainText(current);
|
||||
await expect(panel.locator('thead th').last()).toHaveText(last);
|
||||
await panel.locator('table').scrollIntoViewIfNeeded();
|
||||
// Soft: every screen is looked at, and the report names each one where the table is cut.
|
||||
expect.soft(await hiddenInSettings(p), `${screen.name}, ${lang}, ${from === 'tete' ? 'en tête du document' : 'depuis une version'}`).toEqual([]);
|
||||
if (lang === 'fr') await shot(p, `c44-${screen.tag}-${from}`);
|
||||
}
|
||||
}
|
||||
await p.evaluate(() => localStorage.setItem('ld:lang', 'fr'));
|
||||
}
|
||||
await phone.close();
|
||||
});
|
||||
+26
-16
@@ -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,29 +40,32 @@ 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 deux 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');
|
||||
await expect(seeds.locator('h2')).toHaveText(["Raison d'être", 'Unicité du compte', 'Réciprocité']);
|
||||
await expect(seeds.locator('.tag', { hasText: 'Proposition seed' })).toHaveCount(3);
|
||||
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(3);
|
||||
// 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 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');
|
||||
for (const seed of await seeds.all()) await expect(seed).toContainText('inertie 2 / 5 · basse');
|
||||
// Below, the open proposals, folded by section; the seed proposals are not said twice.
|
||||
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible();
|
||||
await expect(page.locator('ul.deep li.section')).toHaveCount(8);
|
||||
// Five sections: formule, inertie and ordonnancement are out of the document (ADR 0014).
|
||||
await expect(page.locator('ul.deep li.section')).toHaveCount(5);
|
||||
await expect(page.locator('ul.deep button.row')).toHaveCount(0);
|
||||
await expect(page.locator('ul.deep li.section', { hasText: 'Engagements techniques' })).toContainText('11 articles');
|
||||
await unfold(page);
|
||||
// One line per article: the 33 less the 3 at the head, E3 and E7 holding their two variants inside.
|
||||
await expect(page.locator('ul.deep button.row')).toHaveCount(30);
|
||||
await expect(page.locator('ul.deep .tag', { hasText: 'Proposition seed' })).toHaveCount(0);
|
||||
// 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 d'amorce" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => {
|
||||
@@ -172,7 +178,7 @@ test('C9 — les aperçus : bien cotées, et tout', async ({ page }) => {
|
||||
await page.goto(DOC);
|
||||
await page.getByRole('radio', { name: 'Si tout passe' }).click();
|
||||
await expect(page.getByText("Aperçu : le texte tel qu'il serait si toutes les propositions", { exact: false })).toBeVisible();
|
||||
await expect(page.locator('section.text article.unit')).toHaveCount(33);
|
||||
await expect(page.locator('section.text article.unit')).toHaveCount(27);
|
||||
// Nothing in force in Monnaie libre, and nothing well rated yet (no « pour » there so far).
|
||||
await page.getByRole('radio', { name: 'Si les bien cotées passent' }).click();
|
||||
await expect(page.locator('section.text article.unit')).toHaveCount(0);
|
||||
@@ -267,8 +273,9 @@ test('C18 — la Raison d’être ouvre le texte, en trois langues', async ({ pa
|
||||
// The first sentence in italics, then two paragraphs.
|
||||
const paragraphs = first.locator('.prose p');
|
||||
await expect(paragraphs).toHaveCount(3);
|
||||
await expect(paragraphs.nth(0).locator('em')).toContainText("Derrière une clé il n'y a qu'un seul être humain vivant");
|
||||
await expect(paragraphs.nth(1)).toContainText("C'est le seul et unique propos de la certification");
|
||||
// The text of Yvv of 2026-11-03 (lot T4).
|
||||
await expect(paragraphs.nth(0).locator('em')).toContainText('Derrière une clé il y a un et un seul être humain vivant');
|
||||
await expect(paragraphs.nth(1)).toContainText("C'est l'unique enjeu de la certification et de votre engagement");
|
||||
await expect(paragraphs.nth(2)).toContainText('la première monnaie libre');
|
||||
await page.getByRole('radiogroup', { name: 'Langue du texte' }).getByRole('radio', { name: 'EN' }).click();
|
||||
await expect(first.locator('h2')).toHaveText('Purpose');
|
||||
@@ -298,6 +305,8 @@ test('C19 — un article, une ligne : ses versions se déplient', async ({ page
|
||||
// Level 3, the quorum 50 (ADR 0013): 39 « oui » pass it, the voters missing counted as « non »
|
||||
// (« sur 50 votants » before any vote; a « contre » of another control may be there already).
|
||||
await expect(page.locator('section.fold .need')).toHaveText(/^39 oui (sur 50 votants pour passer|pour passer · le quorum la bloque)$/);
|
||||
// Its other version is right there in the list: the vote does not list it again (lot U2).
|
||||
await expect(page.locator('section.fold').getByRole('button', { name: /^Autres versions/ })).toHaveCount(0);
|
||||
await expect(page).toHaveURL(/vote=/);
|
||||
await e3.click();
|
||||
await expect(e3).toHaveAttribute('aria-expanded', 'false');
|
||||
@@ -326,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');
|
||||
|
||||
@@ -344,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,
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
// (ADR 0007, « cinq crans »). The water level of a vote stands upright; the
|
||||
// weight of a text lies flat — two shapes one does not confuse. The words stay
|
||||
// beside it: « inertie 3 / 5 · moyenne », or only « moyenne » when `short`
|
||||
// (a screen reader still hears the whole).
|
||||
// (a screen reader still hears the whole), or « 3 / 5 · moyenne » when `bare`,
|
||||
// after a heading that already says « Inertie ».
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import { inertiaBadge } from '$lib/labels';
|
||||
import { inertiaBadge, inertiaLevel } from '$lib/labels';
|
||||
import type { Inertia } from '$lib/types';
|
||||
|
||||
let { level, short = false }: { level: Inertia; short?: boolean } = $props();
|
||||
let { level, short = false, bare = false }: { level: Inertia; short?: boolean; bare?: boolean } = $props();
|
||||
const full = $derived(inertiaBadge(level));
|
||||
</script>
|
||||
|
||||
@@ -16,7 +17,9 @@
|
||||
<span class="notches" aria-hidden="true">
|
||||
{#each [1, 2, 3, 4, 5] as k (k)}<span class="notch" class:on={k <= level}></span>{/each}
|
||||
</span>
|
||||
{#if short}<span class="visually-hidden">{full}</span><span aria-hidden="true">{t(`inertia.name.${level}`)}</span>{:else}<span>{full}</span>{/if}
|
||||
{#if short}<span class="visually-hidden">{full}</span><span aria-hidden="true">{t(`inertia.name.${level}`)}</span>
|
||||
{:else if bare}<span>{inertiaLevel(level)}</span>
|
||||
{:else}<span>{full}</span>{/if}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
/** Every section open (a filter is on). */
|
||||
all?: boolean;
|
||||
ontoggle: (unit: Unit, p: Proposal) => void;
|
||||
fold: Snippet<[Unit, Proposal]>;
|
||||
/** The vote of a version, and the versions already listed beside it (not named again inside). */
|
||||
fold: Snippet<[Unit, Proposal, string[]]>;
|
||||
/** The order line of a section, at its end. */
|
||||
order: Snippet<[Unit]>;
|
||||
/** Opens the basket on a section; absent when this session does not vote here. */
|
||||
@@ -153,7 +154,7 @@
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{#if vote === best.id}<div class="fold">{@render fold(unit, best)}</div>{/if}
|
||||
{#if vote === best.id}<div class="fold">{@render fold(unit, best, [])}</div>{/if}
|
||||
</li>
|
||||
{:else if best}
|
||||
{@const vopen = isVersionsOpen(a)}
|
||||
@@ -194,7 +195,7 @@
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{#if vote === p.id}{@render fold(unit, p)}{/if}
|
||||
{#if vote === p.id}{@render fold(unit, p, a.versions.map((v) => v.id))}{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -431,7 +432,8 @@
|
||||
}
|
||||
}
|
||||
@media (max-width: 899px) {
|
||||
.vote-section {
|
||||
.vote-section,
|
||||
.linkish {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
@@ -70,15 +77,17 @@
|
||||
min-width: 0;
|
||||
border-radius: var(--r-input);
|
||||
background: var(--surface-2);
|
||||
padding: 0.7rem 0.9rem;
|
||||
padding: 0 0.9rem;
|
||||
}
|
||||
/* The whole height of the closed box is the summary: a target a finger finds (lot U2). */
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-weight: 750;
|
||||
color: var(--ink-soft);
|
||||
padding-block: 0.7rem;
|
||||
}
|
||||
.formula[open] summary {
|
||||
margin-bottom: 0.7rem;
|
||||
.formula[open] {
|
||||
padding-bottom: 0.7rem;
|
||||
}
|
||||
.formula > :not(summary) + :not(summary) {
|
||||
margin-top: 0.6rem;
|
||||
@@ -112,4 +121,9 @@
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
@media (max-width: 899px) {
|
||||
.settings-link {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -147,12 +148,20 @@
|
||||
font-size: 0.88rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* The table lives in the side panel, 27rem at most whatever the screen (DocPanel), about as
|
||||
narrow as a phone's sheet: on a desktop as on a phone, the cells sit close and the headers
|
||||
and the level's name wrap, so that the six columns hold, in each language; a figure never
|
||||
wraps (lot U3b). */
|
||||
th,
|
||||
td {
|
||||
padding: 0.45rem 0.55rem;
|
||||
padding: 0.4rem 0.3rem;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
thead th,
|
||||
th[scope='row'] {
|
||||
white-space: normal;
|
||||
}
|
||||
thead th {
|
||||
font-size: 0.74rem;
|
||||
font-weight: 800;
|
||||
@@ -192,15 +201,4 @@
|
||||
.tag {
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
/* On a phone, six columns hold in the sheet: the headers and the level's name wrap. */
|
||||
@media (max-width: 480px) {
|
||||
th,
|
||||
td {
|
||||
padding: 0.4rem 0.3rem;
|
||||
}
|
||||
thead th,
|
||||
th[scope='row'] {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
// The inertia of an article: its level out of five, its past, the changes
|
||||
// under vote, and the choice of a new level. A change of inertia is voted at
|
||||
// the level of the article itself (ADR 0007).
|
||||
// The inertia of an article: its level out of five — drawn, said once in the vote (lot
|
||||
// U2) —, its past, the changes under vote, and the choice of a new level. A change of
|
||||
// inertia is voted at the level of the article itself (ADR 0007).
|
||||
import { api, ApiError } from '$lib/api';
|
||||
import type { DocState } from '$lib/doc.svelte';
|
||||
import { i18n, t, type Key } from '$lib/i18n/index.svelte';
|
||||
import { inertiaLevel } from '$lib/labels';
|
||||
import { INERTIA_LEVELS, type Inertia, type Unit } from '$lib/types';
|
||||
import Crans from './Crans.svelte';
|
||||
|
||||
let { doc, unit, onopen }: { doc: DocState; unit: Unit; onopen: (proposalId: string) => void } = $props();
|
||||
|
||||
@@ -59,7 +60,7 @@
|
||||
</script>
|
||||
|
||||
<section class="inertia" aria-labelledby="inertia-{unit.id}">
|
||||
<h4 id="inertia-{unit.id}">{t('inertia.title')} · <span class="muted">{inertiaLevel(unit.inertia)}</span></h4>
|
||||
<h4 id="inertia-{unit.id}">{t('inertia.title')} · <span class="muted"><Crans level={unit.inertia} bare /></span></h4>
|
||||
<p class="faint small">{t('inertia.explain')}</p>
|
||||
{#each unit.inertiaChanges as c (c.at + c.to)}
|
||||
<p class="faint small">{t(`inertia.history.${c.cause}`, { from: level(c.from), to: level(c.to), date: i18n.date(c.at) })}</p>
|
||||
@@ -195,6 +196,19 @@
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
/* On a phone: every gesture big enough for a finger (lot U2). */
|
||||
@media (max-width: 899px) {
|
||||
details > summary {
|
||||
padding-block: 0.75rem;
|
||||
}
|
||||
details[open] > summary {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.btn,
|
||||
.link {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.levels button {
|
||||
transition: none;
|
||||
|
||||
@@ -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>
|
||||
@@ -2,7 +2,8 @@
|
||||
// The order of a section, at its end (ADR 0009; Yvv, 2026-11-02: « la répétition de
|
||||
// "ordonnancement" est nulle. Il faudrait que ce soit en fin de section »): one line, never
|
||||
// the list of its articles again — the handle that marks an order, « Ordre de la section »,
|
||||
// its weight in notches, its state, the orders proposed. « Le vote » unfolds here the vote
|
||||
// its weight in notches, its state, the orders proposed and the changes of its inertia
|
||||
// proposed (lot U2: found here, where their vote unfolds). « Le vote » unfolds here the vote
|
||||
// of any of its versions (the list whole); « Réordonner » puts handles on the articles of
|
||||
// the section, in place.
|
||||
import type { Snippet } from 'svelte';
|
||||
@@ -37,6 +38,7 @@
|
||||
|
||||
const p = $derived(unit.inForce);
|
||||
const proposed = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'order'));
|
||||
const inertia = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'inertia'));
|
||||
// Any version of the module unfolds here: its order in force, an order proposed, a change of its inertia.
|
||||
const shown = $derived([p, ...unit.proposals.filter((x) => x.status === 'open')].find((x) => x?.id === vote) ?? null);
|
||||
const votes = $derived((p?.tally.for ?? 0) + (p?.tally.against ?? 0));
|
||||
@@ -53,6 +55,10 @@
|
||||
{@const first = proposed[0]}
|
||||
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('order.proposed', proposed.length)}</button>
|
||||
{/if}
|
||||
{#if inertia.length && !reordering}
|
||||
{@const first = inertia[0]}
|
||||
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('module.inertia.proposed', inertia.length)}</button>
|
||||
{/if}
|
||||
{#if !reordering}
|
||||
<span class="links">
|
||||
{#if p}<button type="button" class:on={shown?.id === p.id} aria-expanded={shown?.id === p.id} aria-controls="vote-{p.id}" onclick={() => onvote(p.id)}>{t('doc.action.vote')}</button>{/if}
|
||||
|
||||
@@ -110,4 +110,10 @@
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
/* On a phone: « Fixer ce quorum » and « Revenir au défaut » as tall as the field (lot U2). */
|
||||
@media (max-width: 899px) {
|
||||
.btn {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
// or uses its arrows; with the keyboard, the up and down arrows move the article that has
|
||||
// the focus, and the new place is said. Then « Proposer cet ordre »: the order goes to the
|
||||
// vote (ADR 0009, `POST /units/{id}/order`) and its vote unfolds at once, here. An order
|
||||
// already proposed is voted, never proposed twice.
|
||||
// already proposed is voted, never proposed twice. While the order is the one in force,
|
||||
// the button stays off and says why, beside it (lot U2, UX-lecteur.md).
|
||||
import { tick, untrack } from 'svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { api } from '$lib/api';
|
||||
@@ -110,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 = '';
|
||||
@@ -123,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;
|
||||
@@ -141,10 +147,7 @@
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (unchanged) {
|
||||
error = t('order.edit.unchanged');
|
||||
return;
|
||||
}
|
||||
if (unchanged) return;
|
||||
if (existing) {
|
||||
ondone(existing.id);
|
||||
return;
|
||||
@@ -166,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)}>
|
||||
@@ -188,12 +200,19 @@
|
||||
{/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">
|
||||
{#if unchanged}<p class="why" id="order-why-{unit.id}">{t('order.edit.unchanged')}</p>{/if}
|
||||
<button class="btn quiet small" type="button" disabled={busy} onclick={oncancel}>{t('common.cancel')}</button>
|
||||
<button class="btn primary small" type="button" disabled={busy || unchanged} onclick={submit}>{t(existing ? 'order.edit.vote' : 'order.edit.submit')}</button>
|
||||
<button
|
||||
class="btn primary small"
|
||||
type="button"
|
||||
disabled={busy || unchanged}
|
||||
aria-describedby={unchanged ? `order-why-${unit.id}` : undefined}
|
||||
onclick={submit}>{t(existing ? 'order.edit.vote' : 'order.edit.submit')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -338,15 +357,31 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
/* Why the button is off, on its left: read before one reaches for it. */
|
||||
.why {
|
||||
flex: 1 1 14rem;
|
||||
margin: 0;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 650;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
/* On a phone: every target is big enough for a finger. */
|
||||
@media (max-width: 899px) {
|
||||
.arrow {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
.actions .btn {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
/* Its own line, above the two buttons kept together. */
|
||||
.why {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes unfold {
|
||||
from {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
// The title of the document is a module (ADR 0009, inertia 4): one line under the title,
|
||||
// of the same voice as the order of a section — its weight in notches, its state, the
|
||||
// titles proposed. « Le vote » unfolds here the vote of any of its versions; another title
|
||||
// is written in the panel.
|
||||
// titles proposed and the changes of its inertia proposed (lot U2). « Le vote » unfolds here
|
||||
// the vote of any of its versions; another title is written in the panel.
|
||||
import type { Snippet } from 'svelte';
|
||||
import { i18n, t } from '$lib/i18n/index.svelte';
|
||||
import { moduleName } from '$lib/labels';
|
||||
@@ -33,6 +33,7 @@
|
||||
const p = $derived(unit.inForce);
|
||||
const votes = $derived((p?.tally.for ?? 0) + (p?.tally.against ?? 0));
|
||||
const proposed = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'title'));
|
||||
const inertia = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'inertia'));
|
||||
const shown = $derived([p, ...unit.proposals.filter((x) => x.status === 'open')].find((x) => x?.id === vote) ?? null);
|
||||
</script>
|
||||
|
||||
@@ -49,6 +50,10 @@
|
||||
{@const first = proposed[0]}
|
||||
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('title.proposed', proposed.length)}</button>
|
||||
{/if}
|
||||
{#if inertia.length}
|
||||
{@const first = inertia[0]}
|
||||
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('module.inertia.proposed', inertia.length)}</button>
|
||||
{/if}
|
||||
<span class="links">
|
||||
{#if p}<button type="button" class:on={shown?.id === p.id} aria-expanded={shown?.id === p.id} aria-controls="vote-{p.id}" onclick={() => onvote(p.id)}>{t('doc.action.vote')}</button>{/if}
|
||||
<button type="button" onclick={onpropose}>{t('title.propose')}</button>
|
||||
|
||||
@@ -67,15 +67,19 @@
|
||||
<Prose text={text.body} />
|
||||
{#if actions}
|
||||
<footer class="actions">
|
||||
<span class="status faint">
|
||||
{#if seed}
|
||||
{@const need = needOf(seed)}
|
||||
{t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need}{' · '}<span title={need.title}>{need.short}</span>{/if}
|
||||
<span class="crans"><Crans level={seed.votedAtInertia} short /></span>
|
||||
{:else if seedMinimal}<span title={t('doc.seed.long')}>{i18n.plural('doc.seed.votes', votes)}</span>
|
||||
{:else if enteredAt}{t('doc.adopted', { date: i18n.date(enteredAt) })}{/if}
|
||||
{#if rivals > 0} · {i18n.plural('doc.others', rivals)}{/if}
|
||||
</span>
|
||||
<!-- Its vote unfolded says the totals, the « oui » to pass, the inertia and the other
|
||||
versions: the line does not say them again (lot U2). -->
|
||||
{#if !(seed && voteOpen)}
|
||||
<span class="status faint">
|
||||
{#if seed}
|
||||
{@const need = needOf(seed)}
|
||||
{t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need}{' · '}<span title={need.title}>{need.short}</span>{/if}
|
||||
<span class="crans"><Crans level={seed.votedAtInertia} short /></span>
|
||||
{:else if seedMinimal}<span title={t('doc.seed.long')}>{i18n.plural('doc.seed.votes', votes)}</span>
|
||||
{:else if enteredAt}{t('doc.adopted', { date: i18n.date(enteredAt) })}{/if}
|
||||
{#if rivals > 0 && !voteOpen}{' · '}{i18n.plural('doc.others', rivals)}{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="links">
|
||||
<button type="button" class:on={voteOpen} aria-expanded={voteOpen} aria-controls={voteFor ? `vote-${voteFor}` : undefined} onclick={() => onaction('vote')}>{t('doc.action.vote')}</button>
|
||||
<button type="button" onclick={() => onaction('propose')}>{proposeLabel ?? t('doc.action.propose')}</button>
|
||||
@@ -129,11 +133,13 @@
|
||||
.crans {
|
||||
margin-left: 0.6rem;
|
||||
}
|
||||
/* The gestures keep their place whether the line of state is there or not (it steps aside
|
||||
while the vote is unfolded, lot U2): on the right on a desktop, on the left on a phone. */
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 0.4rem 1rem;
|
||||
margin-top: 0.7rem;
|
||||
min-height: 2.25rem;
|
||||
@@ -170,6 +176,11 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@media (min-width: 900px) {
|
||||
.links {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
@media (max-width: 899px) {
|
||||
.links button {
|
||||
min-height: 2.75rem;
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
// many « oui » pass it, how many « non » block it (ADR 0013), its weight in notches,
|
||||
// its other versions — each opens its own vote where it reads —, and the quorum its
|
||||
// author may set before the first vote. The rest of a version (typos, translations,
|
||||
// objections, history) lives in the panel, « Détails ».
|
||||
// objections, history) lives in the panel, « Détails ». Lot U2: each thing said once —
|
||||
// the inertia in its own place, the other versions only those not already in sight.
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import type { DocState } from '$lib/doc.svelte';
|
||||
@@ -31,6 +32,7 @@
|
||||
doc,
|
||||
proposal: p,
|
||||
unit,
|
||||
besides = [],
|
||||
onopen,
|
||||
onsettings,
|
||||
ondetails,
|
||||
@@ -40,6 +42,8 @@
|
||||
doc: DocState;
|
||||
proposal: Proposal;
|
||||
unit: Unit;
|
||||
/** The versions already in sight beside this vote (the versions of its article, listed): not named again. */
|
||||
besides?: string[];
|
||||
/** Unfolds the vote of another version, where it reads (another text, a change of inertia). */
|
||||
onopen: (proposalId: string) => void;
|
||||
onsettings: (proposalId: string) => void;
|
||||
@@ -80,12 +84,25 @@
|
||||
const nameOf = (id: string) => doc.articleTitle(id, doc.original ? source : i18n.lang, source);
|
||||
/** « 9 oui pour passer · 4 non pour bloquer » — the quorum in its tooltip. */
|
||||
const need = $derived(needOf(p));
|
||||
/** The other live versions of the module (its changes of inertia live with its inertia). */
|
||||
/**
|
||||
* The other live versions of the module (its changes of inertia live with its inertia),
|
||||
* less those already in sight beside the vote.
|
||||
*/
|
||||
const others = $derived(
|
||||
[unit.inForce, ...unit.proposals.filter((x) => x.status === 'open' && x.kind !== 'inertia')].filter(
|
||||
(x): x is Proposal => !!x && x.id !== p.id,
|
||||
(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');
|
||||
|
||||
/** What tells another version apart, in a few words. */
|
||||
function otherLabel(v: Proposal): string {
|
||||
@@ -165,9 +182,10 @@
|
||||
{#if need}<p class="need" title={need.title}>{need.long}</p>{/if}
|
||||
<p class="state">{p.evaluation.reached ? t('tally.reached') : i18n.plural('tally.missing', p.evaluation.missing)}</p>
|
||||
{/if}
|
||||
<p class="faint small"><Crans level={p.votedAtInertia} /></p>
|
||||
{#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>
|
||||
@@ -245,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}
|
||||
@@ -275,9 +295,10 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p class="faint small">{t('vote.others.rule')}</p>
|
||||
</section>
|
||||
{/if}
|
||||
{#if p.kind !== 'inertia'}<InertiaPicker {doc} {unit} {onopen} />{/if}
|
||||
{#if !ownInertia}<InertiaPicker {doc} {unit} {onopen} />{/if}
|
||||
{#if !unit.kind && unit.canSetQuorum}<QuorumSetting {doc} {unit} />{/if}
|
||||
{#if detail && view}
|
||||
<Formula
|
||||
@@ -504,6 +525,14 @@
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
/* On a phone: every gesture of the vote big enough for a finger (lot U2). */
|
||||
@media (max-width: 899px) {
|
||||
.choice .btn,
|
||||
.disclose,
|
||||
.linkish {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
}
|
||||
@keyframes unfold {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
+56
-29
@@ -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,6 +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} starter proposal',
|
||||
'docs.seeds.other': '{n} starter proposals',
|
||||
|
||||
'doc.view.label': 'Read',
|
||||
'doc.view.inforce': 'In force',
|
||||
@@ -123,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}',
|
||||
@@ -132,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',
|
||||
@@ -200,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',
|
||||
@@ -207,10 +225,12 @@ export const en: Messages = {
|
||||
'title.submitted': 'Title submitted to vote.',
|
||||
'title.proposed.one': '{n} title proposed',
|
||||
'title.proposed.other': '{n} titles proposed',
|
||||
'module.inertia.proposed.one': '{n} inertia change proposed',
|
||||
'module.inertia.proposed.other': '{n} inertia changes proposed',
|
||||
|
||||
'tally.short': '{for} for · {against} against',
|
||||
'tally.missing.one': '{n} more vote in favor needed.',
|
||||
'tally.missing.other': '{n} more votes in favor needed.',
|
||||
'tally.missing.one': '{n} more vote in favour needed.',
|
||||
'tally.missing.other': '{n} more votes in favour needed.',
|
||||
'tally.reached': 'Threshold reached.',
|
||||
'tally.level': 'Level: {pct}% of the threshold',
|
||||
'inertia.name.1': 'very low',
|
||||
@@ -242,19 +262,22 @@ export const en: Messages = {
|
||||
'vote.simulated.mark': 'simulated vote',
|
||||
'vote.others': 'Other versions',
|
||||
'vote.others.count': 'Other versions ({n})',
|
||||
'vote.need': '{yes} yes to pass · {no} no to block',
|
||||
'vote.need.quorum': '{yes} yes to pass · the quorum blocks it',
|
||||
'vote.need.first.one': '{yes} yes out of {quorum} voter to pass',
|
||||
'vote.need.first.other': '{yes} yes out of {quorum} voters to pass',
|
||||
'vote.need.short': 'passes at {yes} yes · blocked at {no} no',
|
||||
'vote.need.short.quorum': 'passes at {yes} yes',
|
||||
'vote.need.short.first': '{yes} yes out of {quorum}',
|
||||
'vote.need.title.one': 'Quorum: {n} voter. Below it, the missing voters count as “no”.',
|
||||
'vote.need.title.other': 'Quorum: {n} voters. Below it, the missing voters count as “no”.',
|
||||
'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',
|
||||
'vote.need.first.other': '{yes} for out of {quorum} voters to pass',
|
||||
'vote.need.short': 'passes at {yes} for · blocked at {no} against',
|
||||
'vote.need.short.quorum': 'passes at {yes} for',
|
||||
'vote.need.short.first': '{yes} for out of {quorum}',
|
||||
'vote.need.title.one': 'Quorum: {n} voter. Below it, the missing voters count as against.',
|
||||
'vote.need.title.other': 'Quorum: {n} voters. Below it, the missing voters count as against.',
|
||||
'vote.retracted': 'Vote withdrawn.',
|
||||
'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}”.',
|
||||
@@ -295,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 “no”.',
|
||||
'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})',
|
||||
@@ -400,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 “no”; beyond it, the share of “yes” 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}.",
|
||||
@@ -411,43 +434,47 @@ export const en: Messages = {
|
||||
'inertia.history.amendment': 'Went from {from} to {to} on {date}: starting setting of the text.',
|
||||
|
||||
'formula.title': 'The threshold formula',
|
||||
'formula.intro': 'The number of votes in favor required, R, depends on turnout:',
|
||||
'formula.intro': 'The number of votes in favour required, R, depends on turnout:',
|
||||
'formula.W': 'W — electorate: {n} ({source})',
|
||||
'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 favor',
|
||||
'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.R': 'R — current threshold: {n} votes in favour',
|
||||
'formula.round': 'A version reaches its threshold when its votes in favour reach R, rounded up to a whole number.',
|
||||
'formula.reading': "Below the article's quorum Q, the missing voters count as against: it takes as many votes in favour as if Q people had voted. Beyond it, the share of votes in favour asked for, highest at the quorum, falls as turnout rises; when everyone votes, the share asked is the majority M.",
|
||||
'common.pct': '{n}%',
|
||||
'settings.title': 'Formula settings',
|
||||
'settings.intro': 'To change a text, the group asks for a number of votes in favour that depends on its electorate, its usual turnout and the inertia of the article.',
|
||||
'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; 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',
|
||||
|
||||
+43
-16
@@ -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,6 +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 de arranque',
|
||||
'docs.seeds.other': '{n} propuestas de arranque',
|
||||
|
||||
'doc.view.label': 'Leer',
|
||||
'doc.view.inforce': 'En vigor',
|
||||
@@ -123,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}',
|
||||
@@ -132,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',
|
||||
@@ -200,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',
|
||||
@@ -207,6 +225,8 @@ export const es: Messages = {
|
||||
'title.submitted': 'Título sometido a votación.',
|
||||
'title.proposed.one': '{n} título propuesto',
|
||||
'title.proposed.other': '{n} títulos propuestos',
|
||||
'module.inertia.proposed.one': '{n} cambio de inercia propuesto',
|
||||
'module.inertia.proposed.other': '{n} cambios de inercia propuestos',
|
||||
|
||||
'tally.short': '{for} a favor · {against} en contra',
|
||||
'tally.missing.one': 'Falta {n} voto a favor.',
|
||||
@@ -242,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',
|
||||
@@ -255,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}».',
|
||||
@@ -295,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})',
|
||||
@@ -400,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}.',
|
||||
@@ -416,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, la proporción exigida es la mayoría M.',
|
||||
'common.pct': '{n} %',
|
||||
'settings.title': 'Ajustes de la fórmula',
|
||||
'settings.intro': 'Para cambiar un texto, el grupo pide un número de votos a favor que depende de su electorado, de su participación habitual y de la inercia del artículo.',
|
||||
'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; 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',
|
||||
|
||||
+43
-16
@@ -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,6 +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 d'amorce",
|
||||
'docs.seeds.other': "{n} Propositions d'amorce",
|
||||
|
||||
'doc.view.label': 'Lire',
|
||||
'doc.view.inforce': 'En vigueur',
|
||||
@@ -127,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}',
|
||||
@@ -136,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',
|
||||
@@ -204,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',
|
||||
@@ -211,6 +229,8 @@ export const fr = {
|
||||
'title.submitted': 'Titre proposé au vote.',
|
||||
'title.proposed.one': '{n} titre proposé',
|
||||
'title.proposed.other': '{n} titres proposés',
|
||||
'module.inertia.proposed.one': "{n} changement d'inertie proposé",
|
||||
'module.inertia.proposed.other': "{n} changements d'inertie proposés",
|
||||
|
||||
'tally.short': '{for} pour · {against} contre',
|
||||
'tally.missing.one': 'Il manque {n} voix pour.',
|
||||
@@ -246,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',
|
||||
@@ -259,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} ».',
|
||||
@@ -299,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})',
|
||||
@@ -404,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}.",
|
||||
@@ -420,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 part exigée est la majorité M.",
|
||||
'common.pct': '{n} %',
|
||||
'settings.title': 'Réglages de la formule',
|
||||
'settings.intro': "Pour changer un texte, le groupe demande un nombre de voix pour qui dépend de son électorat, de sa participation habituelle et de l'inertie de l'article.",
|
||||
'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 ; 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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
@@ -153,6 +154,11 @@
|
||||
const sectionKeys = $derived([...new Set((view?.units ?? []).map((u) => u.section))]);
|
||||
const deepSecs = $derived(deepSections(deep, deepOrders, sectionKeys));
|
||||
const canVote = $derived(!!view?.vote.open);
|
||||
// The facts of the head say what the page shows (lot U2): the seed proposals read at the head
|
||||
// while nothing is in force, apart; then the proposals of « Propositions en cours » — the
|
||||
// articles' versions, never the orders and titles proposed, voted at their module's line.
|
||||
const headSeeds = $derived(doc.seedHead.length);
|
||||
const openArticles = $derived(doc.open.filter((x) => !x.unit.kind).length - headSeeds);
|
||||
|
||||
/** The other versions of an article open to the vote, besides the one read. */
|
||||
const othersOf = (e: Entry) => e.unit.proposals.filter((p) => p.status === 'open' && p.kind !== 'inertia' && p.id !== e.proposal.id).length;
|
||||
@@ -164,11 +170,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet foldOf(unit: Unit, p: Proposal)}
|
||||
{#snippet foldOf(unit: Unit, p: Proposal, besides: string[] = [])}
|
||||
<VoteFold
|
||||
{doc}
|
||||
proposal={p}
|
||||
{unit}
|
||||
{besides}
|
||||
onopen={(id) => go(panel, id)}
|
||||
onsettings={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'settings' })}
|
||||
ondetails={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'details' })}
|
||||
@@ -223,7 +230,7 @@
|
||||
{/if}
|
||||
<p class="summary">{summary}</p>
|
||||
<p class="facts faint">
|
||||
{i18n.plural('docs.inForce', doc.inForce.length)} · {i18n.plural('docs.open', doc.open.length)}
|
||||
{i18n.plural('docs.inForce', doc.inForce.length)} · {#if headSeeds}{i18n.plural('docs.seeds', headSeeds)}{' · '}{/if}{i18n.plural('docs.open', openArticles)}
|
||||
{#if view.doc.publication.state === 'done'} · {t('doc.published')}{:else if view.doc.publication.state === 'pending'} · {t('doc.publish.pending')}{/if}
|
||||
· <button class="linkish" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'settings' })}>{t('settings.title')}</button>
|
||||
</p>
|
||||
@@ -253,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}
|
||||
@@ -463,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;
|
||||
@@ -517,13 +545,15 @@
|
||||
.new {
|
||||
margin-top: 1.4rem;
|
||||
}
|
||||
/* On a phone: the three readings on one line that scrolls, the targets big enough for a finger. */
|
||||
/* On a phone: the three readings go to the line rather than hide « Si tout passe » off the
|
||||
edge (lot U2); each row fills the width, the targets big enough for a finger. */
|
||||
@media (max-width: 899px) {
|
||||
.seg.views {
|
||||
flex-wrap: nowrap;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
.seg.views button {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.seg button {
|
||||
min-height: 2.75rem;
|
||||
|
||||
@@ -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,11 @@ 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.",
|
||||
);
|
||||
// Lot U3c: « la part exigée est la majorité M », no longer « la majorité M suffit » (rules.test.ts).
|
||||
expect(fr['formula.reading']).toBe(
|
||||
"Sous le quorum Q, 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 part exigée est la majorité M.",
|
||||
);
|
||||
expect(fr['quorum.error.voted']).toBe("Quelqu'un a déjà voté sur cet article : son quorum ne change pas.");
|
||||
});
|
||||
|
||||
@@ -55,3 +55,28 @@ describe('anti-lexicon', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Lot U2 (reviews of M6c and M7): the same words for the same things.
|
||||
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à');
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('English: one vocabulary', () => {
|
||||
it('« Pour » and « Contre » are for and against, never yes and no', () => {
|
||||
for (const [k, v] of Object.entries(en)) {
|
||||
// What is read, the placeholders ({yes}, {no}: the numbers) left out.
|
||||
const shown = v.replace(/\{\w+\}/g, '#');
|
||||
expect(shown, k).not.toMatch(/\byes\b/i);
|
||||
expect(shown, k).not.toMatch(/“no”|# no\b/);
|
||||
}
|
||||
});
|
||||
it('one spelling, the one that dominates: favour', () => {
|
||||
for (const [k, v] of Object.entries(en)) expect(v, k).not.toMatch(/\bfavor\b/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// Lot U2 (review of M7): in the vote unfolded, one piece of information in one place. Rendered as
|
||||
// the server would (svelte/server), and read as a person reads it: the tags left out, and what a
|
||||
// closed <details> holds left out too (only its summary shows).
|
||||
import { render } from 'svelte/server';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import UnitBlock from '../../src/lib/components/UnitBlock.svelte';
|
||||
import VoteFold from '../../src/lib/components/VoteFold.svelte';
|
||||
import type { DocState } from '../../src/lib/doc.svelte';
|
||||
import type { Inertia, Proposal, Unit } from '../../src/lib/types';
|
||||
|
||||
function version(id: string, over: Partial<Proposal> = {}): Proposal {
|
||||
return {
|
||||
id,
|
||||
unitId: 'u1',
|
||||
kind: 'text',
|
||||
inertiaTarget: null,
|
||||
title: 'Connaissance suffisante',
|
||||
body: 'Je connais suffisamment la personne.',
|
||||
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,
|
||||
tally: { for: 0, against: 0 },
|
||||
tallyPending: { for: 0, against: 0 },
|
||||
tallySimulated: { for: 0, against: 0 },
|
||||
evaluation: { cast: 0, required: 9, reached: false, missing: 9, requiredRatio: 0.75, wellRated: false, quorum: 12, yesNeeded: 9, noNeeded: 0 },
|
||||
votedAtInertia: 2,
|
||||
blocked: false,
|
||||
votable: true,
|
||||
objections: 0,
|
||||
myVote: null,
|
||||
myVoteState: null,
|
||||
myVoteStale: false,
|
||||
myVoteChain: false,
|
||||
myBallot: null,
|
||||
translations: {},
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
function unitOf(proposals: Proposal[], inertia: Inertia = 2): Unit {
|
||||
return { id: 'u1', code: 'E3', section: 'technique', position: 1, inertia, inertiaChanges: [], inForce: null, proposals };
|
||||
}
|
||||
|
||||
/** The text a person reads: no tags, no server markers, nothing inside a closed <details> but its summary. */
|
||||
function read(html: string): string {
|
||||
return html
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/(<details(?![^>]*\sopen)[^>]*>[\s\S]*?<\/summary>)[\s\S]*?<\/details>/g, '$1')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
const doc = { view: null, original: false, articleTitle: () => '', unit: () => undefined } as unknown as DocState;
|
||||
|
||||
function fold(p: Proposal, unit: Unit, besides?: string[]): string {
|
||||
const { body } = render(VoteFold, {
|
||||
props: { doc, proposal: p, unit, besides, onopen: noop, onsettings: noop, ondetails: noop, onanswer: noop, onsign: noop },
|
||||
});
|
||||
return read(body);
|
||||
}
|
||||
|
||||
const count = (text: string, part: string) => text.split(part).length - 1;
|
||||
|
||||
describe('the vote unfolded says each thing once', () => {
|
||||
it('the inertia of the article, once (its notches with it)', () => {
|
||||
const p = version('p1');
|
||||
const text = fold(p, unitOf([p]));
|
||||
expect(count(text, '2 / 5 · basse'), text).toBe(1);
|
||||
});
|
||||
it('the inertia a change of inertia is voted at, once', () => {
|
||||
const p = version('p2', { kind: 'inertia', inertiaTarget: 4, title: 'inertia:4' });
|
||||
const text = fold(p, unitOf([p]));
|
||||
expect(count(text, '2 / 5 · basse'), text).toBe(1);
|
||||
});
|
||||
it('the other versions already listed beside the vote are not listed again', () => {
|
||||
const a = version('p1', { label: '1.0.0' });
|
||||
const b = version('p2', { label: '0.4.0-draft' });
|
||||
const unit = unitOf([a, b]);
|
||||
// Alone (in the text, say), it names its other version …
|
||||
expect(fold(a, unit)).toContain('Autres versions (1)');
|
||||
// … but in the list of the versions of its article, the other one is right there.
|
||||
expect(fold(a, unit, ['p1', 'p2'])).not.toContain('Autres versions');
|
||||
});
|
||||
it('an other version not in sight is still named (the text in force, above)', () => {
|
||||
const inForce = version('p0', { status: 'in_force', title: 'Connaissance suffisante' });
|
||||
const a = version('p1', { label: '1.0.0' });
|
||||
const b = version('p2', { label: '0.4.0-draft' });
|
||||
const unit = { ...unitOf([a, b]), inForce };
|
||||
expect(fold(a, unit, ['p1', 'p2'])).toContain('Autres versions (1)');
|
||||
});
|
||||
});
|
||||
|
||||
function block(seed: Proposal, voteOpen: boolean, rivals = 0): string {
|
||||
const { body } = render(UnitBlock, {
|
||||
props: {
|
||||
text: { title: seed.title, body: seed.body, translated: false, stale: false, missing: false },
|
||||
seed,
|
||||
rivals,
|
||||
voteFor: seed.id,
|
||||
voteOpen,
|
||||
onaction: noop,
|
||||
},
|
||||
});
|
||||
return read(body);
|
||||
}
|
||||
|
||||
describe('a seed proposal at the head, its vote unfolded', () => {
|
||||
const seed = version('p1', { seedProposal: true, title: "Raison d'être" });
|
||||
it('folded: its line says how far it is from passing', () => {
|
||||
const text = block(seed, false, 1);
|
||||
expect(text).toContain('0 pour · 0 contre · 9 oui sur 12');
|
||||
expect(text).toContain('1 autre version');
|
||||
});
|
||||
it('unfolded: the vote below says it, the line does not say it again', () => {
|
||||
const text = block(seed, true, 1);
|
||||
expect(text).not.toContain('0 pour · 0 contre');
|
||||
expect(text).not.toContain('9 oui sur 12');
|
||||
expect(text).not.toContain('basse');
|
||||
expect(text).not.toContain('autre version');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
// Lot U3 (Yvv, 2026-11-03): the rules of operation leave the votes (ADR 0014) and the screen says
|
||||
// them where they serve. Each phrase the screen says of a rule is pinned here, letter for letter,
|
||||
// with the test of the engine that proves it — the closed list of lot T3c
|
||||
// (docs/chantiers/acte-certification/t3c-liste-fermee.md) on the texts of M8, kept in
|
||||
// crates/ld-server/tests/fixtures/seed-m8/. The inventory of where each lives:
|
||||
// docs/chantiers/acte-certification/U3-informations.md.
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { en } from '../../src/lib/i18n/en';
|
||||
import { es } from '../../src/lib/i18n/es';
|
||||
import { fr } from '../../src/lib/i18n/fr';
|
||||
|
||||
const CATALOGS = { fr, en, es };
|
||||
|
||||
describe('Yvv’s words, word for word', () => {
|
||||
it('the chapter is « Info ouverture doc de réf. », and its paragraph replaces « Commence par les Propositions seed »', () => {
|
||||
expect(fr['opening.title']).toBe('Info ouverture doc de réf.');
|
||||
expect(`${fr['opening.none']} ${fr['opening.amorce']}`).toBe(
|
||||
"Aucun article n'est encore en vigueur. Les Propositions d'amorce y entrent par le vote, comme les propositions qui suivent, avec moins d'inertie pour que le document prenne vie plus facilement. Il est recommandé d'augmenter ensuite l'inertie, par le vote, pour diminuer la fréquence des changements.",
|
||||
);
|
||||
expect(Object.keys(fr)).not.toContain('doc.empty.seeds');
|
||||
});
|
||||
it('once an article is in force, the first sentence goes, and « y » says what it stood for', () => {
|
||||
expect(fr['opening.amorce.after']).toBe(fr['opening.amorce'].replace("y entrent par le vote", 'entrent en vigueur par le vote'));
|
||||
});
|
||||
it('the word for whoever is not invited yet (« @ chiara » read « à Chiara »), and its translations', () => {
|
||||
expect(fr['signin.uninvited']).toBe(
|
||||
"Pour rentrer dans un groupe de décisions, il faut pour l'instant demander un lien d'invitation à Chiara ou à Yvv. Ce prototype n'est pas encore câblé sur la chaîne.",
|
||||
);
|
||||
expect(en['signin.uninvited']).toBe(
|
||||
'To join a decision group, for now ask Chiara or Yvv for an invitation link. This prototype is not yet wired to the chain.',
|
||||
);
|
||||
expect(es['signin.uninvited']).toBe(
|
||||
'Para entrar en un grupo de decisiones, por ahora hay que pedir un enlace de invitación a Chiara o a Yvv. Este prototipo aún no está conectado a la cadena.',
|
||||
);
|
||||
});
|
||||
it('said once: the Ğ1 line no longer says how to get in meanwhile', () => {
|
||||
expect(fr['signin.g1.pending']).toBe("La connexion Ğ1 arrive : l'inscription de libreDecision auprès de connect.monnaie-libre.fr est en cours.");
|
||||
for (const cat of Object.values(CATALOGS)) expect(cat['signin.g1.pending']).not.toMatch(/invitation|invitación/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('« Proposition d’amorce » (fr), « Starter proposal » (en), « Propuesta de arranque » (es)', () => {
|
||||
it('names the seed proposals everywhere', () => {
|
||||
expect([fr['doc.seedProposal'], fr['docs.seeds.one'], fr['docs.seeds.other']]).toEqual(["Proposition d'amorce", "{n} Proposition d'amorce", "{n} Propositions d'amorce"]);
|
||||
expect([en['doc.seedProposal'], en['docs.seeds.one'], en['docs.seeds.other']]).toEqual(['Starter proposal', '{n} starter proposal', '{n} starter proposals']);
|
||||
expect([es['doc.seedProposal'], es['docs.seeds.one'], es['docs.seeds.other']]).toEqual(['Propuesta de arranque', '{n} propuesta de arranque', '{n} propuestas de arranque']);
|
||||
});
|
||||
it('never « seed » next to « proposition », « proposal », « propuesta »', () => {
|
||||
for (const [lang, cat] of Object.entries(CATALOGS)) {
|
||||
for (const [k, v] of Object.entries(cat)) {
|
||||
expect(v, `${lang} ${k}`).not.toMatch(/propositions? seed|seed proposals?|propuestas? (seed|semilla)/i);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('the rules the screen says, true for every W and every quorum (lots T3 to T3c)', () => {
|
||||
it('the defined term « participation habituelle », never « comme d’habitude »', () => {
|
||||
const asUsual = { fr: /comme d'habitude/, en: /as usual/, es: /como de costumbre/ };
|
||||
for (const [lang, cat] of Object.entries(CATALOGS)) {
|
||||
for (const [k, v] of Object.entries(cat)) expect(v, `${lang} ${k}`).not.toMatch(asUsual[lang as keyof typeof asUsual]);
|
||||
}
|
||||
});
|
||||
|
||||
// Each phrase, and the test that proves it (crates/ld-server/tests/…).
|
||||
const PROVEN: [keyof typeof fr, string, string][] = [
|
||||
// seed_quorum_texts.rs under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted (N1.1 2, F1.2 12);
|
||||
// the_share_asked_falls_beyond_the_article_s_quorum_down_to_m (N1.1 3); ld-engine
|
||||
// inertia::when_everyone_votes_the_share_asked_is_m_on_every_electorate (« la part exigée est la
|
||||
// majorité M », W from 1 to 10 000 000, lot U3c).
|
||||
[
|
||||
'formula.reading',
|
||||
"Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la part exigée est la majorité M.",
|
||||
'under_the_quorum_… · the_share_asked_falls_… · ld-engine inertia::when_everyone_votes_…',
|
||||
],
|
||||
// the_formulas_of_the_texts_are_those_the_engine_computes (T' = max(T, Q), F1.1, N1.1 4).
|
||||
['formula.Tprime', "T′ — votants retenus : {n}, T ou Q s'il en manque", 'the_formulas_…'],
|
||||
// ld-engine threshold::a_whole_number_of_votes_never_gains_one_by_rounding (« A genuine fraction still
|
||||
// rounds up »: F1.1 « arrondi à l'entier au-dessus »).
|
||||
['formula.round', "Une version atteint son seuil quand ses voix pour atteignent R, arrondi à l'entier au-dessus.", 'ld-engine threshold'],
|
||||
// g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum (F1.2 18).
|
||||
['formula.G', 'G — gradient : {n}, déduit de la participation habituelle du groupe', 'g_gives_…'],
|
||||
// b_to_the_power_w_never_counts_in_the_web_of_trust (F1.2 16), said only when W is the web of trust.
|
||||
['formula.B.wot', "À la puissance W, B est si petit qu'il ne compte pas dans la toile de confiance.", 'b_to_the_power_w_…'],
|
||||
// the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum (F1.2 20, N1.1 5).
|
||||
['settings.share.note', "Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle.", 'the_share_of_a_level_…'],
|
||||
// the_share_asked_falls_… (N1.1 3), under_the_quorum_… (N1.1 2).
|
||||
[
|
||||
'settings.reading',
|
||||
"Au-delà du quorum de l'article, plus il y a de votants, plus la part exigée baisse, jusqu'à la majorité M quand tout le monde vote. Sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté.",
|
||||
'the_share_asked_falls_… · under_the_quorum_…',
|
||||
],
|
||||
// the_participation_habituelle_is_the_median_brought_back_within_its_bounds; tests/inertia.rs
|
||||
// the_usual_participation_is_the_median_of_five_voted_proposals (N1.1 12: the year counts).
|
||||
[
|
||||
'settings.usual.reference.other',
|
||||
"{n} votants sur {electorate} ({pct}), deux fois {q}, le quorum de référence : la participation de référence, tant qu'elle repose sur moins de {min} propositions votées depuis un an ({sample} pour l'instant).",
|
||||
'the_participation_habituelle_… · inertia.rs',
|
||||
],
|
||||
// tests/quorum.rs in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pending (N1.1 13).
|
||||
['settings.usual.quorum', "Elle ne dépend pas du quorum d'un article.", 'quorum.rs in_monnaie_libre_…'],
|
||||
// g_gives_… (N1.1 15).
|
||||
['settings.gradient', "G se calcule pour que la part exigée, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle.", 'g_gives_…'],
|
||||
// tests/quorum.rs the_author_of_an_article_sets_its_quorum_before_the_first_vote; tests/ballots.rs
|
||||
// a_vote_exists_for_every_state_of_the_buffer (F1.2 14, lot T2: « la trace d'aucun vote »).
|
||||
['quorum.explain', "Tu fixes le quorum de cet article tant qu'il ne garde la trace d'aucun vote. En dessous, les votants qui manquent comptent comme « non ».", 'quorum.rs · ballots.rs'],
|
||||
[
|
||||
'opening.quorum.author',
|
||||
"L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote.",
|
||||
'quorum.rs · ballots.rs',
|
||||
],
|
||||
// under_the_quorum_… (F1.2 12), each_level_has_its_quorum_and_every_evaluation_says_it (F1.2 13).
|
||||
['opening.quorum.rule', "Sous le quorum d'un article, les votants qui manquent comptent comme « non ».", 'under_the_quorum_… · quorum.rs'],
|
||||
// ld-engine compose::a_rival_must_beat_the_version_in_force (F1.2 6); tests/api.rs
|
||||
// a_rival_replaces_the_seed_and_the_seed_returns_among_proposals (F1.2 7).
|
||||
[
|
||||
'vote.rival',
|
||||
'Un texte est déjà en vigueur : pour le remplacer, cette version doit en plus atteindre une marge (voix pour moins voix exigées) plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir.',
|
||||
'compose · api.rs',
|
||||
],
|
||||
// ld-engine compose::rivals_on_one_unit_yield_one_winner (F1.2 8).
|
||||
['vote.others.rule', 'Si plusieurs versions atteignent leur seuil, celle qui a la marge la plus large entre en vigueur.', 'compose'],
|
||||
// ld-engine ballot::the_last_one_stands_in_the_order_of_the_chain; tests/ballots.rs the same (F1.2 3, 4).
|
||||
['vote.permanent', 'Le vote reste ouvert, sans échéance : tu peux changer ou retirer ton vote à tout moment, le dernier fait foi.', 'ballot · ballots.rs'],
|
||||
// tests/modules.rs an_order_is_proposed_with_the_articles_of_its_section_then_voted_into_force (O1 4:
|
||||
// « Nothing else moved: the other sections read as before »).
|
||||
['order.edit.effect', "S'il entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change.", 'modules.rs'],
|
||||
];
|
||||
it.each(PROVEN)('%s', (key, text) => {
|
||||
expect(fr[key]).toBe(text);
|
||||
for (const cat of [en, es]) expect(cat[key], key).toBeTruthy();
|
||||
});
|
||||
it('the defined terms are carried in English and in Spanish too', () => {
|
||||
for (const k of ['settings.share.note', 'settings.gradient', 'formula.G'] as const) {
|
||||
expect(en[k], k).toMatch(/usual turnout/);
|
||||
expect(es[k], k).toMatch(/participación habitual/);
|
||||
}
|
||||
for (const k of ['settings.share.note', 'settings.reading', 'settings.gradient', 'formula.reading', 'inertia.explain'] as const) {
|
||||
expect(en[k], k).toMatch(/article's quorum/);
|
||||
expect(es[k], k).toMatch(/quórum (Q )?del artículo/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Lot U3c (review of d66823b): « quand tout le monde vote, la majorité M suffit » was false on a small
|
||||
// electorate. When the W vote, R = B^W + M·W; when M·W is a whole number, B^W adds one « pour » on
|
||||
// every W up to 15 (W 4, level 2: 3 « pour » of 4, not 2; W 5, level 4: 4 of 5, not 3). What holds for
|
||||
// every W and every quorum is the share asked: M at T′ = W — ld-engine
|
||||
// inertia::when_everyone_votes_the_share_asked_is_m_on_every_electorate, W from 1 to 10 000 000.
|
||||
// B^W is said apart, in the web of trust only (`formula.B.wot`).
|
||||
describe('when everyone votes, the share asked is M — never « M suffices » (lot U3c)', () => {
|
||||
it('the reading of the formula says it, in the three languages', () => {
|
||||
expect(fr['formula.reading']).toBe(
|
||||
"Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la part exigée est la majorité M.",
|
||||
);
|
||||
expect(en['formula.reading']).toBe(
|
||||
"Below the article's quorum Q, the missing voters count as against: it takes as many votes in favour as if Q people had voted. Beyond it, the share of votes in favour asked for, highest at the quorum, falls as turnout rises; when everyone votes, the share asked is the majority M.",
|
||||
);
|
||||
expect(es['formula.reading']).toBe(
|
||||
'Por debajo del quórum Q del artículo, los votantes que faltan cuentan como «no»: hacen falta tantos votos a favor como si hubieran votado Q personas. Más allá, la proporción de votos a favor pedida, la más alta en el quórum, baja cuando sube la participación; cuando vota todo el mundo, la proporción exigida es la mayoría M.',
|
||||
);
|
||||
});
|
||||
|
||||
// « M suffit », « la majorité suffit », « majority M is enough », « basta la mayoría M »…: false for
|
||||
// W up to 15, so never said, in a catalogue or in the text of a component.
|
||||
const SUFFICES: Record<keyof typeof CATALOGS, RegExp> = {
|
||||
fr: /\bM suffi|majorit[ée][^.;:]*suffi|suffi[^.;:]*majorit[ée]/i,
|
||||
en: /majority[^.;:]*(enough|suffice|sufficient)|(enough|suffice|sufficient)[^.;:]*majority/i,
|
||||
es: /mayor[íi]a[^.;:]*(basta|suficiente|alcanza)|(basta|suficiente|alcanza con)[^.;:]*mayor[íi]a/i,
|
||||
};
|
||||
it('no string of the interface says the majority suffices', () => {
|
||||
for (const [lang, cat] of Object.entries(CATALOGS)) {
|
||||
for (const [k, v] of Object.entries(cat)) expect(v, `${lang} ${k}`).not.toMatch(SUFFICES[lang as keyof typeof CATALOGS]);
|
||||
}
|
||||
const svelte = (dir: string): string[] =>
|
||||
readdirSync(dir).flatMap((name) => {
|
||||
const p = join(dir, name);
|
||||
return statSync(p).isDirectory() ? svelte(p) : p.endsWith('.svelte') ? [p] : [];
|
||||
});
|
||||
for (const file of svelte(join(__dirname, '../../src'))) {
|
||||
const shown = readFileSync(file, 'utf8')
|
||||
.replace(/<script[\s\S]*?<\/script>/g, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/g, '')
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/<[^>]+>/g, ' ');
|
||||
for (const re of Object.values(SUFFICES)) expect(shown, file).not.toMatch(re);
|
||||
}
|
||||
});
|
||||
|
||||
// The closed list of what the screen says of full turnout, each judged against the engine: the
|
||||
// share asked, M — true for every W (the grid above). A new one fails here until it is judged.
|
||||
it('what the screen says of full turnout is the share asked, M', () => {
|
||||
const FULL: Record<keyof typeof CATALOGS, RegExp> = {
|
||||
fr: /tout le monde vote|pleine participation/,
|
||||
en: /everyone votes|full turnout/,
|
||||
es: /vota todo el mundo|participación plena/,
|
||||
};
|
||||
for (const [lang, cat] of Object.entries(CATALOGS)) {
|
||||
const told = Object.entries(cat)
|
||||
.filter(([, v]) => FULL[lang as keyof typeof CATALOGS].test(v))
|
||||
.map(([k]) => k);
|
||||
// formula.M names the letter, « M — majorité à pleine participation » (the share asked when the W
|
||||
// vote); formula.reading and settings.reading: « la part exigée … la majorité M ».
|
||||
expect(told, lang).toEqual(['formula.M', 'formula.reading', 'settings.reading']);
|
||||
}
|
||||
const SHARE = [
|
||||
[
|
||||
'formula.reading',
|
||||
{ fr: 'la part exigée est la majorité M', en: 'the share asked is the majority M', es: 'la proporción exigida es la mayoría M' },
|
||||
],
|
||||
[
|
||||
'settings.reading',
|
||||
{
|
||||
fr: "plus la part exigée baisse, jusqu'à la majorité M quand tout le monde vote",
|
||||
en: 'the lower the share asked gets, down to the majority M when everyone votes',
|
||||
es: 'más baja la proporción exigida, hasta la mayoría M cuando vota todo el mundo',
|
||||
},
|
||||
],
|
||||
] as const;
|
||||
for (const [k, share] of SHARE) {
|
||||
for (const [lang, cat] of Object.entries(CATALOGS)) expect(cat[k], `${lang} ${k}`).toContain(share[lang as keyof typeof CATALOGS]);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user