ADR 0012 réécrite sur les deux décisions d'Yvv : « Il compte, puis s'efface » (02/11, 06 h 26 : compte jusqu'à l'acte « supprime », réexamen sur les seuls votes prouvés, la commande et ce qu'elle écrit) et « Pas avant des votes prouvés » (03/11, 05 h 45 : la publication) ; le quorum sur les totaux montrés, « un vote existe », l'API, les données ; commentaires de la bascule et de casts_simulated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 05:46:09 +01:00
co-authored by Claude Opus 5.5
parent 4c9e796137
commit e8e9633b75
3 changed files with 124 additions and 46 deletions
+3 -2
View File
@@ -84,8 +84,9 @@ pub struct Config {
pub build: String,
/// The vote on the chain; `None` = off (no `LD_VOTE_ACCOUNT`).
pub vote: Option<VoteChain>,
/// 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,
}
+2 -1
View File
@@ -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)
}
+119 -43
View File
@@ -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:<index>`) 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).