forked from yvv/decision
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:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user