forked from yvv/decision
ADR 0012 : le tampon des votes — l'intention tout de suite, la preuve de la chaîne, la réconciliation ; la simulation tant que la chaîne est fermée (lot V)
Demande d'Yvv du 02/11 : « je vois ce que j'ai fait sans attendre », contrôle a posteriori (bien unique, bien le dernier qui vaut), et le vote simulé partout dans Monnaie libre tant que LD_VOTE_ACCOUNT n'est pas posé. Révise une phrase de 0010 : un « pour » prouvé pendant une objection est écarté. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,10 @@
|
||||
> changés sans toucher au reste. Corrigé le 2026-10-31 après la relecture indépendante de
|
||||
> `74f96be` (lot B2) : finalité, recherche filtrée et paginée, tâche de fond, plafond, bulletin
|
||||
> Ğecko, bulletins lâchés, reçu.
|
||||
> **Complétée par [0012](0012-tampon-des-votes.md)** (2026-11-02, lot V) : le bulletin en attente
|
||||
> est une intention, visible aussitôt pour la personne et dans les totaux montrés (jamais dans
|
||||
> l'adoption) ; la réconciliation remplace « le dernier fait foi » ligne à ligne ; sans
|
||||
> `LD_VOTE_ACCOUNT`, le vote est simulé au lieu d'être fermé.
|
||||
|
||||
**Contexte.** Jusqu'ici, un vote était une ligne écrite par le serveur pour la session connectée.
|
||||
Dans Monnaie libre, l'électorat W est la toile de confiance, mais la connexion Ğ1
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
> coquille d'un texte du seed validée par un administrateur sont les choix par défaut d'orch.
|
||||
> Lot F, 2026-10-31. **Révisée le 2026-11-01** (décision d'Yvv, lot G) : sur une version
|
||||
> retenue, on peut encore retirer son vote ou voter « contre », jamais ajouter un « pour ».
|
||||
> **Révisée par [0012](0012-tampon-des-votes.md)** (2026-11-02, lot V) : un « pour » dont la
|
||||
> preuve arrive sur la chaîne pendant qu'une objection retient la version est écarté ; il ne
|
||||
> compte plus « tel qu'il est signé ».
|
||||
|
||||
**Contexte.** Le signalement de contradiction (0004) bloquait l'entrée en vigueur sans dire à
|
||||
qui revenait le geste suivant, et une coquille corrigée par n'importe qui changeait le texte
|
||||
@@ -37,7 +40,8 @@ d'un autre sans lui demander. Yvv, 2026-10-30 :
|
||||
direct, vote groupé, bulletin de la chaîne : `409 objection`, et une ligne refusée refuse tout le
|
||||
vote). La version ne peut que perdre des voix tant qu'elle est retenue ; à la levée, elle
|
||||
n'entre qu'avec les voix encore là. Les votes déjà faits restent ; un bulletin scellé avant
|
||||
l'objection et confirmé après compte tel qu'il est signé, sans la faire entrer. Règle :
|
||||
l'objection et confirmé après compte tel qu'il est signé pour « contre » et le retrait ; son
|
||||
« pour » est écarté (0012). Règle :
|
||||
`ld_engine::objection::may_cast`, appliquée par `DocState::admits`.
|
||||
- Une personne n'a qu'une objection debout sur une même paire (`objection_exists`).
|
||||
- **L'auteur de la version retenue répond** : pour une redite, il **modifie l'article visé**
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
# 0012 — Le tampon des votes : l'intention tout de suite, la preuve de la chaîne, la réconciliation
|
||||
|
||||
> 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.
|
||||
|
||||
**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
|
||||
indexeurs de la chaîne. Il faut prévoir le bon process pour une ux moderne "je vois ce que j'ai
|
||||
fait sans attendre", avec un contrôle a posteriori de l'intégrité du vote (bien unique, bien le
|
||||
dernier qui vaut). » Et ses décisions déjà prises : une personne, une voix, autant de fois
|
||||
qu'elle veut, le dernier vote fait foi ; dans Monnaie libre l'intégrité vient de la chaîne Ğ1
|
||||
(0,1 Ğ1 par vote, un seul geste, un bulletin scellé) ; les totaux se voient en direct et
|
||||
**chacun voit son propre vote en direct** (« tout le monde a maintenant cette habitude,
|
||||
difficile de régresser ») ; rien n'est dit à l'écran du recoupement entre la chaîne et les
|
||||
totaux ; sur une version retenue par une objection, « contre » et retrait, jamais « pour ».
|
||||
|
||||
Jusqu'ici, dans Monnaie libre, un bulletin restait « en attente » sans rien compter jusqu'à ce
|
||||
que l'indexeur le confirme (6 à 30 secondes, une journée au plus par Ğecko), et sans
|
||||
`LD_VOTE_ACCOUNT` le vote était éteint (« pas encore ouvert »). Un bulletin scellé avant une
|
||||
objection et payé après comptait « pour » (relevé par une relecture).
|
||||
|
||||
## Décision
|
||||
|
||||
### Le cheminement d'un vote : trois temps
|
||||
|
||||
```
|
||||
geste ──► INTENTION ──────────► PREUVE ─────────────► RÉCONCILIATION ──► votes (ce qui compte)
|
||||
ballots (pending) la chaîne, lue par ld_engine::buffer
|
||||
visible aussitôt l'indexeur ; ou le ::reconcile, par
|
||||
(ton vote, totaux) confirmateur simulé identité et version
|
||||
│
|
||||
└── jamais prouvée dans sa fenêtre, refusée par la chaîne,
|
||||
dépassée, « pour » prouvé sur une version retenue : ÉCARTÉE
|
||||
```
|
||||
|
||||
1. **L'intention** est le geste lui-même : un bulletin scellé (`ballots`, `ballot_lines`,
|
||||
0008), un seul pour tout un panier. Elle est écrite **avant** que le portefeuille signe et se
|
||||
voit **aussitôt** : pour la personne (« ton vote », état `pending`) et dans les totaux
|
||||
montrés (`DocState::shown_tally`). Rien n'est parti (refus du portefeuille, nœud muet,
|
||||
fenêtre fermée avant la signature) : la page la lâche aussitôt (`DELETE`, 0008) et elle
|
||||
disparaît des totaux. Un code Ğecko montré est gardé, comme avant.
|
||||
2. **La preuve** vient de la chaîne, lue par l'indexeur, sans rien changer à 0008 : la
|
||||
remarque exacte, le virement du signataire vers le compte des votes, une identité Ğ1 membre
|
||||
au moment de la confirmation, un bloc définitif. Pendant la simulation (plus bas), un
|
||||
**confirmateur simulé** tient la place de l'indexeur.
|
||||
3. **La réconciliation** a lieu à chaque preuve, pour chaque version du bulletin, et pour
|
||||
l'identité qui a signé : elle relit **toutes** les lignes prouvées de cette identité sur
|
||||
cette version et en tire la voix qui tient (`ld_engine::buffer::reconcile`, pur, testé) :
|
||||
- **une seule voix par identité Ğ1** et par version ;
|
||||
- **la dernière dans l'ordre de la chaîne** (le bloc, puis le rang de l'évènement), quel que
|
||||
soit l'ordre dans lequel l'indexeur les a lues : le résultat ne dépend pas de l'ordre des
|
||||
confirmations ; « retirer mon vote » (`none`) qui tient efface la voix ;
|
||||
- elle **écarte** : ce que la chaîne ne confirme jamais dans la fenêtre de validité (24
|
||||
heures, plus une heure de grâce si l'indexeur se tait ou si le bloc n'est pas encore
|
||||
définitif : `expired`) ; ce que la chaîne refuse (`refused` : pas d'identité, pas membre,
|
||||
virement manquant) ; ce qu'un vote plus récent de la même identité a dépassé
|
||||
(`superseded`) ; et **un « pour » dont la preuve arrive pendant qu'une objection retient la
|
||||
version** (`objection`, voir plus bas).
|
||||
|
||||
Chaque ligne d'un bulletin prouvé porte son issue (`ballot_lines.outcome` : `counted`,
|
||||
`superseded`, `objection`), recalculée à chaque réconciliation sauf `objection`, fixée au
|
||||
moment de la preuve. La table `votes` reste ce qui compte : une ligne par identité et par
|
||||
version, écrite par la réconciliation (sources `chain`, `simulated`) ou par le serveur dans
|
||||
un groupe géré (`server`). Chaque vote compté se vérifie toujours contre la chaîne en
|
||||
recalculant H depuis les lignes et le sel (0008).
|
||||
|
||||
**Dans un groupe géré** (essai, Axiom…), il n'y a pas de tampon : le serveur est l'autorité, le
|
||||
vote est écrit d'un coup et il est enregistré (`recorded`) dès l'écriture, comme avant.
|
||||
|
||||
### Les états d'un vote, et ce que chacun pèse
|
||||
|
||||
| État | Ce que c'est | `myVoteState` | Totaux montrés (`shown_tally`) | Adoption (`tally`) |
|
||||
|---|---|---|---|---|
|
||||
| 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) |
|
||||
| é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`).
|
||||
- **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`.
|
||||
- `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.
|
||||
|
||||
### Ce que la personne voit, à chaque étape
|
||||
|
||||
| Étape | Sous l'article (`myVote`, `myVoteState`, `myBallot`) | Totaux | Boîte « Messages » |
|
||||
|---|---|---|---|
|
||||
| elle vote (groupe géré) | son choix, `recorded` | aussitôt | — |
|
||||
| elle vote, la chaîne ouverte | son choix, `pending` ; « En attente de la chaîne Ğ1 : … » | aussitôt, avec son choix | — |
|
||||
| la chaîne le confirme | son choix, `recorded` ; « inscrit sur la chaîne Ğ1 » | inchangés (l'intention devient preuve) | — |
|
||||
| rien n'est parti (portefeuille, nœud, fenêtre fermée) | son vote d'avant | son intention retirée | — |
|
||||
| jamais vu sur la chaîne en 24 h | son vote d'avant ; « Ton dernier vote n'a pas été vu sur la chaîne Ğ1 à temps » | son intention retirée | « Ton vote … n'a pas été vu sur la chaîne Ğ1 en 24 heures : il est écarté. Tu peux voter à nouveau. » |
|
||||
| refusé par la chaîne | son vote d'avant ; « Ton dernier vote n'a pas été compté : … » | son intention retirée | « Ton vote … n'a pas été compté : … » |
|
||||
| un « pour » prouvé sur une version retenue | son vote d'avant ; « Ton dernier vote n'a pas été compté : une objection retenait cette version… » | inchangés | « Ton « pour » sur … est écarté : une objection retenait cette version quand la chaîne a confirmé ton vote. Ton vote d'avant reste. » |
|
||||
| dépassé par un vote plus récent de la même identité | le plus récent | le plus récent | — (c'est son propre geste) |
|
||||
| elle vote, la chaîne fermée (simulation) | son choix, `simulated` | aussitôt | — |
|
||||
|
||||
Rien n'est dit à l'écran sur le recoupement entre la chaîne et les totaux (décision d'Yvv).
|
||||
Un message n'offre aucun geste : il dit ce qui s'est passé et mène au texte, où l'on revote.
|
||||
|
||||
### La simulation, tant que la chaîne est fermée
|
||||
|
||||
Sans `LD_VOTE_ACCOUNT`, on vote **quand même, partout, dans Monnaie libre**, sans portefeuille :
|
||||
le bouton de vote s'affiche (l'accès dit `open: true`, `simulated: true`, `way: "server"`), le
|
||||
vote part par l'écriture du serveur (`POST /api/proposals/{id}/vote`, `POST
|
||||
/api/docs/{id}/votes`) et passe **par le même tampon** : un bulletin scellé (`ballots.mode =
|
||||
'simulated'`, son sel et son empreinte, sans compte ni montant), puis le **confirmateur simulé**
|
||||
le prouve aussitôt, dans la même requête (et le balayage reprend un bulletin simulé resté en
|
||||
attente) : la voix est celle de **l'identité de la session** (il n'y a pas de portefeuille), sa
|
||||
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).
|
||||
|
||||
- **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';
|
||||
```
|
||||
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.
|
||||
- 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).
|
||||
|
||||
### 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
|
||||
signé, sans la faire entrer ». Une relecture a relevé qu'un « pour » entrait ainsi sur une
|
||||
version qui ne doit plus en prendre. Désormais, **un « pour » dont la preuve arrive pendant
|
||||
qu'une objection retient sa version est écarté** (`objection`) : il ne compte pas, comme s'il
|
||||
n'avait pas été émis ; le vote d'avant de la même identité, s'il y en a un, reste ; la personne
|
||||
qui l'a demandé reçoit un message. « Contre » et retrait, prouvés pendant l'objection, comptent
|
||||
(Yvv, 01/11 : « Retirer ou "contre" »). « Arrive » se lit à l'heure du serveur qui lit la
|
||||
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.
|
||||
|
||||
### L'API, contrat partagé avec les lots U et S
|
||||
|
||||
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é) ;
|
||||
- `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` ;
|
||||
- inchangés : `myVoteStale`, `myVoteChain`, `myBallot` (qui peut dire `status: 'refused'`,
|
||||
`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`.
|
||||
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}]}`.
|
||||
|
||||
## Les données
|
||||
|
||||
Migration `0010_vote_buffer.sql`, additive :
|
||||
|
||||
- `ballots.mode TEXT NOT NULL DEFAULT 'chain'` (`chain` | `simulated`) : tous les bulletins
|
||||
d'avant restent des bulletins de la chaîne ;
|
||||
- `ballot_lines.outcome TEXT` (`counted` | `superseded` | `objection`, `NULL` tant que le
|
||||
bulletin n'est pas prouvé ; les lignes d'avant restent à `NULL` et se lisent comme « pas
|
||||
é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`.
|
||||
|
||||
Aucune ligne existante n'est réécrite. La purge (0008) efface avec un bulletin lâché les
|
||||
messages qui en parlent.
|
||||
|
||||
## Coût
|
||||
|
||||
- **Les totaux montrés sont provisoires** le temps de la preuve : une intention compte pour la
|
||||
session qui l'a demandée. Si une même identité vote depuis deux sessions à la fois, ou si une
|
||||
session signe avec un autre portefeuille que celui de son vote précédent, les totaux montrés
|
||||
peuvent s'écarter d'une voix jusqu'à la preuve ; la réconciliation remet le compte juste. Une
|
||||
personne qui ouvre Monnaie libre peut faire paraître une voix dans les totaux montrés pendant
|
||||
24 heures sans payer (une par session et par version, 20 bulletins au plus) : elle ne compte
|
||||
jamais pour l'adoption.
|
||||
- L'intention compte dès que le bulletin est scellé, quelques secondes avant la signature ; si
|
||||
rien ne part, la page la lâche aussitôt.
|
||||
- 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 ».
|
||||
- Un message sur un bulletin lâché disparaît avec lui à la purge (une semaine).
|
||||
@@ -15,3 +15,4 @@ pas une décision : on en écrit une nouvelle qui la remplace, et on le note dan
|
||||
| [0008](0008-vote-par-transaction.md) | Le vote par transaction sur la chaîne Ğ1 : bulletin scellé, confirmation par l'indexeur, le dernier fait foi | 2026-10-30 |
|
||||
| [0009](0009-modules-ordre-et-titre.md) | Les modules : l'ordre des articles de chaque section (inertie 2), le titre du document (inertie 4) | 2026-10-31 |
|
||||
| [0010](0010-objections-et-coquilles-validees.md) | Objections « redit » / « contredit », coquilles validées, messagerie (remplace 0004, complète 0003) | 2026-10-31 |
|
||||
| [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 |
|
||||
|
||||
Reference in New Issue
Block a user