From e8e9633b75997c34fc3a6bd4ea3b19498508a7ca Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 05:46:09 +0100 Subject: [PATCH] =?UTF-8?q?ADR=200012=20r=C3=A9=C3=A9crite=20sur=20les=20d?= =?UTF-8?q?eux=20d=C3=A9cisions=20d'Yvv=20:=20=C2=AB=20Il=20compte,=20puis?= =?UTF-8?q?=20s'efface=20=C2=BB=20(02/11,=2006=20h=2026=20:=20compte=20jus?= =?UTF-8?q?qu'=C3=A0=20l'acte=20=C2=AB=20supprime=20=C2=BB,=20r=C3=A9exame?= =?UTF-8?q?n=20sur=20les=20seuls=20votes=20prouv=C3=A9s,=20la=20commande?= =?UTF-8?q?=20et=20ce=20qu'elle=20=C3=A9crit)=20et=20=C2=AB=20Pas=20avant?= =?UTF-8?q?=20des=20votes=20prouv=C3=A9s=20=C2=BB=20(03/11,=2005=20h=2045?= =?UTF-8?q?=20:=20la=20publication)=20;=20le=20quorum=20sur=20les=20totaux?= =?UTF-8?q?=20montr=C3=A9s,=20=C2=AB=20un=20vote=20existe=20=C2=BB,=20l'AP?= =?UTF-8?q?I,=20les=20donn=C3=A9es=20;=20commentaires=20de=20la=20bascule?= =?UTF-8?q?=20et=20de=20casts=5Fsimulated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- crates/ld-server/src/config.rs | 5 +- crates/trustin-access/src/lib.rs | 3 +- docs/adr/0012-tampon-des-votes.md | 162 ++++++++++++++++++++++-------- 3 files changed, 124 insertions(+), 46 deletions(-) diff --git a/crates/ld-server/src/config.rs b/crates/ld-server/src/config.rs index 41fd27a..e0a47e7 100644 --- a/crates/ld-server/src/config.rs +++ b/crates/ld-server/src/config.rs @@ -84,8 +84,9 @@ pub struct Config { pub build: String, /// The vote on the chain; `None` = off (no `LD_VOTE_ACCOUNT`). pub vote: Option, - /// Whether a simulated vote counts for adoption while the chain is closed - /// (ADR 0012): `ld_engine::buffer::SIMULATED_ADOPTS`, the default of lot V. + /// Whether a simulated vote counts for adoption (ADR 0012): + /// `ld_engine::buffer::SIMULATED_ADOPTS` — Yvv, 2026-11-02: « Il compte, puis + /// s'efface »: it counts until the act « supprime » sets it aside. pub simulated_adopts: bool, } diff --git a/crates/trustin-access/src/lib.rs b/crates/trustin-access/src/lib.rs index cb544cf..154098b 100644 --- a/crates/trustin-access/src/lib.rs +++ b/crates/trustin-access/src/lib.rs @@ -163,7 +163,8 @@ pub fn chain_vote_counts(member: bool) -> bool { /// ADR 0012): whoever may ask for a ballot casts a simulated vote, written by the /// server, marked as such, for the identity of the session — there is no wallet. /// One person signed in two ways may then hold two voices: accepted while the -/// simulation lasts, every simulated vote is set aside when the chain opens. +/// simulation lasts — Yvv, 2026-11-02: « Il compte, puis s'efface »: every +/// simulated vote counts until an act « supprime » sets them all aside. pub fn casts_simulated(identity: &Identity, group: &Group, granted: bool, chain_open: bool) -> bool { !chain_open && asks_ballot(identity, group, granted) } diff --git a/docs/adr/0012-tampon-des-votes.md b/docs/adr/0012-tampon-des-votes.md index c55c2f9..40f88c9 100644 --- a/docs/adr/0012-tampon-des-votes.md +++ b/docs/adr/0012-tampon-des-votes.md @@ -2,9 +2,12 @@ > Complète [0008](0008-vote-par-transaction.md) (vote par la chaîne Ğ1) et révise une phrase de > [0010](0010-objections-et-coquilles-validees.md) (un bulletin scellé avant une objection et -> confirmé après). Demande d'Yvv du 2026-11-02 ; la façon de compter une intention, ce qu'on dit -> quand elle est écartée et le poids d'un vote simulé sont les choix par défaut du lot V, isolés -> dans `ld_engine::buffer` pour être changés seuls. Lot V, 2026-11-02. +> confirmé après). Demande d'Yvv du 2026-11-02. **Deux décisions d'Yvv font foi** : le poids +> d'un vote simulé, « Il compte, puis s'efface » (2026-11-02, 06 h 26), et la publication d'un +> texte entré par des votes simulés, « Pas avant des votes prouvés » (2026-11-03, 05 h 45). La +> façon de compter une intention et ce qu'on dit quand elle est écartée restent les choix du lot +> V, isolés dans `ld_engine::buffer` pour être changés seuls. Se lit avec +> [0013](0013-quorum.md) (le quorum, jugé sur les totaux montrés). Lot V, 2026-11-02 et 03. **Contexte.** Yvv, 2026-11-02 : « Simule le vote à tout endroit. […] du point de vue de l'architecture, il faut prévoir le "tampon" entre la base des votes et la mise à jour par les @@ -76,23 +79,30 @@ vote est écrit d'un coup et il est enregistré (`recorded`) dès l'écriture, c |---|---|---|---|---| | enregistré | prouvé par la chaîne, ou vote du serveur d'un groupe géré | `recorded` | oui | oui | | en attente | une intention sans preuve, dans sa fenêtre | `pending` | oui, **à la place** du vote que la personne voit comme le sien ; jamais un « pour » sur une version retenue | **jamais** | -| simulé | un vote de la simulation, chaîne fermée | `simulated` | oui, tant que la chaîne est fermée | oui, tant que la chaîne est fermée (**défaut**, bascule ci-dessous) | +| simulé | un vote de la simulation, chaîne fermée | `simulated` | oui, **jusqu'à l'acte « supprime »** | oui, **jusqu'à l'acte « supprime »** (décision d'Yvv, « Il compte, puis s'efface ») | | écarté | expiré, refusé, dépassé, « pour » sur une version retenue | — | non | non | - **L'adoption ne compte jamais une intention non confirmée.** Une entrée en vigueur ne repose - que sur des votes prouvés, sur les votes du serveur dans un groupe géré, et, par défaut, - sur les votes simulés tant que la chaîne est fermée. Le balayage d'adoption et la mesure de - la participation p̃ lisent les mêmes poids (`ld_engine::buffer::Rules`). + que sur des votes prouvés, sur les votes du serveur dans un groupe géré, et sur les votes + simulés jusqu'à l'acte qui les écarte (plus bas). Le balayage d'adoption et la mesure de la + participation p̃ lisent les mêmes poids (`ld_engine::buffer::Rules`). +- **Le quorum se juge sur les totaux montrés** (ADR 0013) : l'évaluation qu'on montre + (`evaluation` : `quorum`, `yesNeeded`, `noNeeded`, `missing`) et les cinq crans du détail se + calculent sur `shown_tally` — votes enregistrés, en attente et simulés, l'intention à la place + du vote vu de la personne (`DocState::shown_evaluation`) ; l'adoption, sur `tally` + (`DocState::evaluation`). - **Une intention compte pour la session qui l'a demandée** : elle remplace, dans les totaux montrés, le vote que cette session voit comme le sien (`my_votes`), et la dernière intention de la session sur une version remplace les précédentes. Une intention sur une révision dépassée ne compte pas (elle serait « à reconfirmer ») mais retire bien le vote d'avant ; un « pour » en attente sur une version retenue ne pèse rien tant qu'elle est retenue (il serait - écarté) : `ld_engine::buffer::pending_shift`. + écarté) : `ld_engine::buffer::weigh_intent`. - `tallyPending` dit la part des totaux montrés qui attend encore sa preuve ; `tallySimulated` la part qui vient de la simulation. Les textes publiés (`publish.rs`) ne publient jamais une intention : ils lisent `recorded_tally` (ce qui est enregistré, simulation comprise tant - qu'elle compte), ou `tally` pour les articles en vigueur, comme avant. + qu'elle compte), ou `tally` pour les articles en vigueur, comme avant ; et rien ne part tant + qu'une entrée due à des votes simulés ne tient pas sur les seuls votes prouvés (« La + publication », plus bas). ### Ce que la personne voit, à chaque étape @@ -123,36 +133,79 @@ attente) : la voix est celle de **l'identité de la session** (il n'y a pas de p position est « bloc 0 », au rang de son arrivée ; puis la même réconciliation, le même balayage d'adoption, les mêmes messages. L'architecture est ainsi éprouvée de bout en bout, en production, sans la chaîne. Qui peut voter en simulation : la règle de trustinWallet, -`trustin_access::casts_simulated` (qui ouvre le groupe, la chaîne fermée). +`trustin_access::casts_simulated` (qui ouvre le groupe, la chaîne fermée) — une personne +invitée comprise. -- **Défaut, en attendant la réponse d'Yvv** : un vote simulé compte dans les totaux et dans - l'adoption **comme un vote prouvé** ; il est marqué « simulé » (`myVoteState`, - `tallySimulated`, `mode` du bulletin, « Tes bulletins »). Avec C = 50 et cinq personnes au - courant, rien n'entrera en vigueur dans Monnaie libre. -- **La bascule** vers « simulé = affiché, jamais compté pour l'adoption » tient en une ligne : - `ld_engine::buffer::SIMULATED_ADOPTS = false` (repris par `Config::simulated_adopts`). Les - totaux montrés n'en changent pas ; l'adoption et p̃ les ignorent alors. Les deux branches sont - testées. -- **L'ouverture de la chaîne les écarte tous.** Dès que `LD_VOTE_ACCOUNT` est posé (un acte - « déploie »), les votes simulés ne pèsent plus rien, nulle part : ni totaux, ni adoption, ni - p̃, ni « ton vote », ni « Décisions » (`Rules::chain_open`) ; un nouveau vote simulé est - refusé (`409 chain_only`, on vote par la chaîne). Une identité qui vote ensuite par la chaîne - passe devant ses votes simulés (bloc 0, avant tout bloc réel). Les lignes restent en base - jusqu'à leur **effacement, un acte « supprime » d'Yvv**, préparé ici, jamais exécuté par le - code : - ```sql - DELETE FROM votes WHERE source = 'simulated'; - DELETE FROM messages WHERE topic = 'ballot' - AND subject_id IN (SELECT id FROM ballots WHERE mode = 'simulated'); - DELETE FROM ballot_lines WHERE ballot_id IN (SELECT id FROM ballots WHERE mode = 'simulated'); - DELETE FROM ballots WHERE mode = 'simulated'; +**La décision d'Yvv, 2026-11-02 à 06 h 26 : « Il compte, puis s'efface ».** + +- **Il compte.** Un vote simulé compte dans les totaux et **pour l'entrée en vigueur**, comme un + vote prouvé, avec la marque « simulé » (`myVoteState`, `tallySimulated`, `mode` du bulletin, + « Tes bulletins »). Avec les quorums de l'ADR 0013, neuf « pour » simulés suffisent à faire + entrer I1, E1 ou E2 au cran 2 (quorum 12) : le texte est alors en vigueur sur le site, et ne + se publie pas (« La publication », plus bas). La règle : `ld_engine::buffer::SIMULATED_ADOPTS + = true`, reprise par `Config::simulated_adopts` ; à `false`, « simulé = affiché, jamais compté + pour l'adoption » (les totaux montrés ne changent pas, l'adoption et p̃ les ignorent). Les deux + branches sont testées. +- **L'ouverture de la chaîne n'efface rien d'elle-même.** Dès que `LD_VOTE_ACCOUNT` est posé + (un acte « déploie »), on vote par la chaîne : un nouveau vote simulé est refusé (`409 + chain_only`). Les votes simulés déjà là **continuent de compter**, marqués, jusqu'à l'acte qui + suit. Une identité Ğ1 (une session ouverte par la clé Ğ1, `idty:`) qui vote ensuite + par la chaîne passe devant son vote simulé (bloc 0, avant tout bloc réel). +- **Puis il s'efface, par un acte « supprime » d'Yvv**, jamais déclenché par le code : ni le + serveur, ni l'ouverture de la chaîne, ni un balayage ne l'appellent. Il est préparé et testé + ici : `simulation::discard`, en ligne de commande dans l'image : + ```sh + ld-server discard-simulation # à blanc : dit ce qu'il ferait, n'écrit rien + ld-server discard-simulation --apply # l'acte ``` - Ce qui serait entré en vigueur par des votes simulés y reste (une adoption ne se défait pas) ; - avec la règle par défaut et C = 50, rien n'y sera entré dans Monnaie libre. + Il lit la base du volume (`LD_DATA_DIR`, `libredecision.db`) et imprime un JSON : les + bulletins, lignes, votes et messages simulés, et chaque entrée réexaminée (article, sorte, + votes d'entrée et leur part simulée, votes prouvés, « pour » exigés, tient ou non, rendue ou + non, la version déplacée). Avec `--apply` : + 1. **le tri** : il efface les messages des bulletins simulés, les votes simulés + (`votes.source = 'simulated'`), leurs lignes et les bulletins simulés, puis réconcilie de + nouveau chaque identité qui avait voté en simulation (son vote prouvé, s'il y en a un, + tient) ; + 2. **le réexamen** : chaque entrée en vigueur qui a compté des votes simulés + (`adoptions.params.simulated`), la plus récente d'abord, encore en vigueur, est jugée de + nouveau sur les seuls votes prouvés, au cran et sur le quorum où elle a été votée + (`params.level`, `params.quorum`). **Sous son seuil, elle retourne parmi les + propositions** : la version qu'elle avait déplacée revient en vigueur (un texte retiré + revient, un cran d'inertie redescend, l'ordre et le titre d'avant s'appliquent de + nouveau), son adoption est effacée, une publication est mise en file. **À son seuil ou + au-dessus, elle reste**, et son adoption le dit (`params.reexamined`) ; + 3. les envois en attente de la publication repartent. + + Répété, l'acte ne trouve plus rien (aucun bulletin simulé, aucune entrée à réexaminer). + **À blanc**, le réexamen met les votes simulés de côté sans les effacer : une entrée qui ne + reviendrait qu'après le retour d'une plus récente est dite « plus en vigueur ». - Pour les tests seulement, et seulement avec la connexion de développement, `POST /api/dev/vote-chain {"open": false}` ferme la chaîne d'un serveur qui en a une (le contrôle e2e de la simulation tourne sur le serveur jetable des e2e, qui a sa chaîne simulée). +### La publication : « Pas avant des votes prouvés » + +**La décision d'Yvv, 2026-11-03 à 05 h 45 : « Pas avant des votes prouvés ».** Un texte qui +entre en vigueur grâce à des votes simulés est **en vigueur sur le site**, mais **ne déclenche +pas la publication** vers le dépôt des textes, que lisent les applis clientes. La publication +d'un document attend que tout ce qui y est en vigueur et y est entré par des votes simulés — +texte, retrait, cran d'inertie, ordre, titre — **tienne sur les seuls votes prouvés**, selon le +réexamen même de l'acte « supprime », fait à blanc (`simulation::unproven`). La chaîne étant +fermée, **rien ne se publie dans Monnaie libre avant son ouverture** ; après elle, la file part +dès que l'acte a rendu ce qui ne tient pas, ou dès que des votes prouvés portent ce qui est +entré. + +- Un seul endroit : `publish::waits_for_proven_votes`, après `publish::waits_for_a_text` (ADR + 0011), tous deux lus par `publish::waits`. Comme pour l'attente du premier texte, ce n'est pas + un échec : aucune tentative comptée, la file attend et se relit toutes les dix minutes ; l'API + dit `publication.state = "suspended"` et `publication.waitsFor = "proven"` (`"text"` pour + l'attente du premier texte). +- La bascule, pour publier quand même si Yvv revenait sur ce choix : `LD_PUBLISH_SIMULATED=1` + (`Publish::simulated`). Testé dans les deux sens. +- Plus strict qu'« un texte prouvé suffit » : tant qu'une entrée simulée ne tient pas, même un + texte prouvé du même document attend, pour que le dépôt ne porte jamais une entrée que les + votes prouvés ne portent pas. + ### Un « pour » prouvé pendant une objection (révise 0010) 0010 disait : « un bulletin scellé avant l'objection et confirmé après compte tel qu'il est @@ -165,6 +218,18 @@ qui l'a demandé reçoit un message. « Contre » et retrait, prouvés pendant l preuve : la chaîne ne donne que des blocs, et l'objection une heure. Une objection levée avant la preuve laisse le « pour » compter. +### « Un vote existe » (ADR 0013) + +Le quorum d'un article se fixe avant le premier vote (`quorum::voted_units`, la seule +requête). Le tampon gardant l'historique des bulletins, elle compte : un vote qui compte +(serveur, chaîne, **simulé**) ; toute ligne d'un bulletin **en attente** ou **prouvé**, de la +chaîne ou de la simulation, **quel que soit son sort** (la voix qui compte, un vote dépassé, un +« pour » écarté par une objection, **un retrait**) ; une adoption. Un vote retiré sur la chaîne +ou en simulation laisse donc sa trace. Un bulletin lâché sans signature, jamais vu à temps ou +refusé par la chaîne n'est pas un vote. Deux limites : dans un groupe géré, sans tampon, un vote +retiré avant toute adoption ne laisse rien (ADR 0013) ; l'acte « supprime » emporte la trace des +votes simulés avec eux. + ### L'API, contrat partagé avec les lots U et S Dans le JSON d'une proposition (`GET /api/docs/{id}`, `GET /api/proposals/{id}`) : @@ -172,6 +237,7 @@ Dans le JSON d'une proposition (`GET /api/docs/{id}`, `GET /api/proposals/{id}`) - `tally: {for, against}` — les totaux montrés, intentions comprises ; - `tallyPending: {for, against}` — la part encore sans preuve ; - `tallySimulated: {for, against}` — la part simulée (ajouté) ; +- `evaluation` — sur les totaux montrés (ADR 0013 : `quorum`, `yesNeeded`, `noNeeded`) ; - `myVote: 'for' | 'against' | null` — ce que la personne voit comme son vote maintenant : son dernier geste, une intention en attente comprise (un retrait en attente : `null`) ; - `myVoteState: 'recorded' | 'pending' | 'simulated' | null` ; @@ -179,15 +245,19 @@ Dans le JSON d'une proposition (`GET /api/docs/{id}`, `GET /api/proposals/{id}`) `reason: 'objection'` pour un « pour » écarté). Dans l'accès (`vote` du JSON du document) : `{ way, open, simulated, reason, chain }` ; -pendant la simulation, `way: "server"`, `open: true`, `simulated: true`, `chain: null`. +pendant la simulation, `way: "server"`, `open: true`, `simulated: true`, `chain: null`. Un +vote écrit par le serveur répond `{ entered, simulated }` (et `count` pour un vote groupé). Les bulletins (`GET /api/ballots/{id}`, `GET /api/ballots`, le reçu) gagnent `mode: 'chain' | -'simulated'`. Les messages gagnent le sujet `ballot`, de sortes `ballot_expired`, -`ballot_refused`, `ballot_objection`, avec `ballot: {id, status, reason, mode, createdAt, -settledAt, lines: [{proposalId, unitId, code, title, choice, outcome}]}`. +'simulated'` (le bloc d'un bulletin simulé n'est jamais montré). Les messages gagnent le sujet +`ballot`, de sortes `ballot_expired`, `ballot_refused`, `ballot_objection`, avec `ballot: {id, +status, reason, mode, createdAt, settledAt, lines: [{proposalId, unitId, code, title, choice, +outcome}]}`. La publication d'un document (`doc.publication`) gagne `waitsFor: 'text' | +'proven' | null`. ## Les données -Migration `0010_vote_buffer.sql`, additive : +Migration `0010_vote_buffer.sql`, additive (S a pris `0009` et `0011` ; les trois passent dans +l'ordre sur une base neuve comme sur une base arrêtée à `0008`, la production) : - `ballots.mode TEXT NOT NULL DEFAULT 'chain'` (`chain` | `simulated`) : tous les bulletins d'avant restent des bulletins de la chaîne ; @@ -196,10 +266,13 @@ Migration `0010_vote_buffer.sql`, additive : écartée par une objection » : la prochaine réconciliation de leur identité les qualifie) ; - un index `ballots(doc_id, status)` pour lire les intentions d'un document ; - `votes.source` prend une valeur de plus, `simulated` (colonne texte, rien à changer) ; - `messages.topic` aussi, `ballot`. + `messages.topic` aussi, `ballot` ; `adoptions.params` (JSON) garde de plus, pour l'acte, la + part simulée des votes d'entrée (`simulated`), la version déplacée (`displaced`), le cran + d'avant (`from_inertia`), et après l'acte `reexamined`. -Aucune ligne existante n'est réécrite. La purge (0008) efface avec un bulletin lâché les -messages qui en parlent. +Aucune ligne existante n'est réécrite, et le premier démarrage n'écrit rien du tampon (la +chaîne fermée en production : aucun bulletin). La purge (0008) efface avec un bulletin lâché +les messages qui en parlent. ## Coût @@ -215,6 +288,9 @@ messages qui en parlent. - Un « pour » payé juste avant une objection mais lu juste après (l'indexeur suit à quelques secondes) est écarté : la personne revote à la levée. - **La simulation rend possible deux voix pour une personne** (une session par invitation, une - session Ğ1), ce que la chaîne empêche : accepté tant qu'elle dure, et effacé à l'ouverture. - Chaque geste simulé laisse un bulletin en base jusqu'à l'acte « supprime ». + session Ğ1), ce que la chaîne empêche : accepté tant qu'elle dure, et effacé par l'acte + « supprime ». Chaque geste simulé laisse un bulletin en base jusqu'à l'acte. +- **Un texte entré par des votes simulés se lit en vigueur sur le site sans être publié** ; le + dépôt des textes garde la dernière publication jusqu'aux votes prouvés. L'acte peut faire + retourner parmi les propositions un texte que des personnes ont lu en vigueur. - Un message sur un bulletin lâché disparaît avec lui à la purge (une semaine).