forked from yvv/decision
Moteur : le tampon des votes (ld_engine::buffer) — réconciliation, poids des états, intention à la place du vote vu ; trustinWallet : casts_simulated (lot V, ADR 0012)
Testés d'abord (vus rouges) : une voix par identité, la dernière dans l'ordre de la chaîne quel que soit l'ordre de lecture, un « pour » prouvé sur une version retenue écarté (le vote d'avant reste), le vote simulé au bloc 0, ce que pèse chaque état chaîne fermée ou ouverte, la bascule SIMULATED_ADOPTS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,384 @@
|
|||||||
|
//! The buffer between the votes and the chain (ADR 0012).
|
||||||
|
//!
|
||||||
|
//! Yvv, 2026-11-02: « 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). »
|
||||||
|
//!
|
||||||
|
//! A vote goes through three times:
|
||||||
|
//! - **the intention**, the gesture itself (a sealed ballot, `ballot`): seen at
|
||||||
|
//! once by the person and in the totals shown, never counted for adoption;
|
||||||
|
//! - **the proof**, the chain read by the indexer (or, while the chain is
|
||||||
|
//! closed, the simulated confirmer);
|
||||||
|
//! - **the reconciliation**, after each proof: one voice per identity and per
|
||||||
|
//! version, the last in the order of the chain, whatever the order the proofs
|
||||||
|
//! were read in (`reconcile`). What the chain never confirms in time, refuses,
|
||||||
|
//! or what a more recent vote of the same identity overtook, is set aside — and
|
||||||
|
//! a *for* proven while an objection holds its version back.
|
||||||
|
//!
|
||||||
|
//! What each stage of a vote weighs, in the totals shown and in adoption, is
|
||||||
|
//! `Rules`. Pure: the facts come from the caller.
|
||||||
|
|
||||||
|
use crate::ballot::{ChainPosition, Choice};
|
||||||
|
use crate::objection;
|
||||||
|
use crate::threshold::Tally;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// Whether a simulated vote counts for adoption, while the chain is closed.
|
||||||
|
/// The default of lot V, until Yvv answers: like a proven vote (with C = 50 and
|
||||||
|
/// five people, nothing enters into force in Monnaie libre). `false` turns it to
|
||||||
|
/// « shown, never counted for adoption »; the totals shown do not change.
|
||||||
|
pub const SIMULATED_ADOPTS: bool = true;
|
||||||
|
|
||||||
|
/// Where a vote stands.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Stage {
|
||||||
|
/// Proven by the chain, or written by the server in a managed group.
|
||||||
|
Recorded,
|
||||||
|
/// An intention still waiting for its proof, within its window.
|
||||||
|
Pending,
|
||||||
|
/// A vote of the simulation, while the chain is closed.
|
||||||
|
Simulated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stage {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Recorded => "recorded",
|
||||||
|
Self::Pending => "pending",
|
||||||
|
Self::Simulated => "simulated",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stage of a vote that counts (`votes.source`): `simulated`, or recorded
|
||||||
|
/// (`server`, `chain`). An intention is never in `votes`.
|
||||||
|
pub fn of_source(source: &str) -> Self {
|
||||||
|
if source == "simulated" { Self::Simulated } else { Self::Recorded }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the stages weigh, given whether the chain is open.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Rules {
|
||||||
|
/// The vote account is set: votes go through the chain, the simulation is over.
|
||||||
|
pub chain_open: bool,
|
||||||
|
/// See `SIMULATED_ADOPTS`.
|
||||||
|
pub simulated_adopts: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rules {
|
||||||
|
/// Does a vote at this stage weigh in the totals shown?
|
||||||
|
pub fn shown(&self, stage: Stage) -> bool {
|
||||||
|
match stage {
|
||||||
|
Stage::Recorded | Stage::Pending => true,
|
||||||
|
Stage::Simulated => !self.chain_open,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does a vote at this stage weigh in adoption (and in the usual participation)?
|
||||||
|
/// Never an intention without proof.
|
||||||
|
pub fn adopts(&self, stage: Stage) -> bool {
|
||||||
|
match stage {
|
||||||
|
Stage::Recorded => true,
|
||||||
|
Stage::Pending => false,
|
||||||
|
Stage::Simulated => !self.chain_open && self.simulated_adopts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A proven line of one identity on one version: where its proof sits on the
|
||||||
|
/// chain, its choice, and whether an objection held the version back when the
|
||||||
|
/// proof arrived.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Proven {
|
||||||
|
pub position: ChainPosition,
|
||||||
|
pub choice: Choice,
|
||||||
|
pub held: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What became of a proven line.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Outcome {
|
||||||
|
/// The voice of the identity on this version (a withdrawal too: then no voice).
|
||||||
|
Counted,
|
||||||
|
/// A more recent proven line of the same identity decided.
|
||||||
|
Superseded,
|
||||||
|
/// A *for* proven while an objection held the version back: never counts.
|
||||||
|
Objection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Outcome {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Counted => "counted",
|
||||||
|
Self::Superseded => "superseded",
|
||||||
|
Self::Objection => "objection",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(raw: &str) -> Option<Self> {
|
||||||
|
[Self::Counted, Self::Superseded, Self::Objection].into_iter().find(|o| o.as_str() == raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A *for* whose proof arrives while an objection holds its version back is set
|
||||||
|
/// aside (ADR 0012, revising 0010): against and a withdrawal still count (Yvv,
|
||||||
|
/// 2026-11-01: « Retirer ou "contre" »).
|
||||||
|
pub fn discarded_by_objection(choice: Choice, held: bool) -> bool {
|
||||||
|
!objection::may_cast(held, choice)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reconciliation of one identity on one version.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Reconciled {
|
||||||
|
/// One per line, in the order given.
|
||||||
|
pub outcomes: Vec<Outcome>,
|
||||||
|
/// The line that stands, if any: `None` when every line was set aside (the
|
||||||
|
/// vote of before, if any, is left as it is).
|
||||||
|
pub standing: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One voice per identity and per version, the last in the order of the chain:
|
||||||
|
/// the block, then the rank of the event — whatever the order the lines come in.
|
||||||
|
/// A *for* proven while held is set aside first and never takes a place in the
|
||||||
|
/// order. Two lines at the same place (never on the chain): the later given.
|
||||||
|
pub fn reconcile(lines: &[Proven]) -> Reconciled {
|
||||||
|
let mut outcomes = vec![Outcome::Superseded; lines.len()];
|
||||||
|
let mut standing: Option<usize> = None;
|
||||||
|
for (i, l) in lines.iter().enumerate() {
|
||||||
|
if discarded_by_objection(l.choice, l.held) {
|
||||||
|
outcomes[i] = Outcome::Objection;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if standing.is_none_or(|j| lines[j].position <= l.position) {
|
||||||
|
standing = Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(i) = standing {
|
||||||
|
outcomes[i] = Outcome::Counted;
|
||||||
|
}
|
||||||
|
Reconciled { outcomes, standing }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A change of the totals shown, for and against.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct Shift {
|
||||||
|
pub for_votes: i64,
|
||||||
|
pub against: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shift {
|
||||||
|
/// The totals, shifted; never below zero.
|
||||||
|
pub fn apply(self, t: Tally) -> Tally {
|
||||||
|
let add = |n: u32, d: i64| u32::try_from((i64::from(n) + d).clamp(0, i64::from(u32::MAX))).unwrap_or(n);
|
||||||
|
Tally { for_votes: add(t.for_votes, self.for_votes), against: add(t.against, self.against) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What an intention weighs in the totals shown.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct Weighed {
|
||||||
|
/// What it changes in the totals shown.
|
||||||
|
pub shift: Shift,
|
||||||
|
/// The part of the totals shown it adds that still waits for its proof.
|
||||||
|
pub pending: Tally,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The latest intention of a session on a version, still without proof, takes
|
||||||
|
/// the place — in the totals shown only — of the vote this session sees as its
|
||||||
|
/// own:
|
||||||
|
/// - `seen`: that vote, if it counts in the totals shown now (*for* or *against*);
|
||||||
|
/// - `intent`: the choice of the intention (`None`: a withdrawal);
|
||||||
|
/// - `current`: it votes the revision in place (otherwise it would be « to
|
||||||
|
/// reconfirm »: it adds nothing, but still takes the vote of before away);
|
||||||
|
/// - `held`: an objection holds the version back — a *for* then weighs nothing
|
||||||
|
/// at all (it would be set aside), the vote of before stays.
|
||||||
|
pub fn weigh_intent(seen: Option<Choice>, intent: Choice, current: bool, held: bool) -> Weighed {
|
||||||
|
if discarded_by_objection(intent, held) {
|
||||||
|
return Weighed::default();
|
||||||
|
}
|
||||||
|
let one = |c: Option<Choice>| match c {
|
||||||
|
Some(Choice::For) => Tally { for_votes: 1, against: 0 },
|
||||||
|
Some(Choice::Against) => Tally { for_votes: 0, against: 1 },
|
||||||
|
Some(Choice::None) | None => Tally::default(),
|
||||||
|
};
|
||||||
|
let new = one(current.then_some(intent));
|
||||||
|
let old = one(seen);
|
||||||
|
Weighed {
|
||||||
|
shift: Shift {
|
||||||
|
for_votes: i64::from(new.for_votes) - i64::from(old.for_votes),
|
||||||
|
against: i64::from(new.against) - i64::from(old.against),
|
||||||
|
},
|
||||||
|
pending: new,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const FOR: Choice = Choice::For;
|
||||||
|
const AGAINST: Choice = Choice::Against;
|
||||||
|
const NONE: Choice = Choice::None;
|
||||||
|
|
||||||
|
fn at(block: u32, event: u32) -> ChainPosition {
|
||||||
|
ChainPosition { block, event }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line(block: u32, event: u32, choice: Choice) -> Proven {
|
||||||
|
Proven { position: at(block, event), choice, held: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The outcome of each line, keyed by its position: comparable across orders.
|
||||||
|
fn by_position(lines: &[Proven]) -> (Vec<(ChainPosition, Outcome)>, Option<ChainPosition>) {
|
||||||
|
let r = reconcile(lines);
|
||||||
|
let mut out: Vec<(ChainPosition, Outcome)> =
|
||||||
|
lines.iter().zip(&r.outcomes).map(|(l, o)| (l.position, *o)).collect();
|
||||||
|
out.sort_by_key(|(p, _)| *p);
|
||||||
|
(out, r.standing.map(|i| lines[i].position))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_last_in_the_order_of_the_chain_stands_whatever_the_order_of_reading() {
|
||||||
|
let a = line(200, 2, FOR);
|
||||||
|
let b = line(200, 4, AGAINST);
|
||||||
|
let c = line(300, 1, NONE);
|
||||||
|
let expected = (
|
||||||
|
vec![(a.position, Outcome::Superseded), (b.position, Outcome::Superseded), (c.position, Outcome::Counted)],
|
||||||
|
Some(c.position),
|
||||||
|
);
|
||||||
|
// Every order the indexer may read them in gives the same voice.
|
||||||
|
for order in [[a, b, c], [a, c, b], [b, a, c], [b, c, a], [c, a, b], [c, b, a]] {
|
||||||
|
assert_eq!(by_position(&order), expected, "{order:?}");
|
||||||
|
}
|
||||||
|
// The withdrawal stands: no voice.
|
||||||
|
let r = reconcile(&[a, b, c]);
|
||||||
|
assert_eq!(r.standing.map(|i| [a, b, c][i].choice), Some(NONE));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_voice_per_identity_never_two() {
|
||||||
|
for lines in [
|
||||||
|
vec![],
|
||||||
|
vec![line(5, 1, FOR)],
|
||||||
|
vec![line(5, 1, FOR), line(5, 2, FOR), line(4, 9, AGAINST)],
|
||||||
|
vec![Proven { held: true, ..line(9, 1, FOR) }, line(8, 1, AGAINST), line(7, 1, FOR)],
|
||||||
|
] {
|
||||||
|
let r = reconcile(&lines);
|
||||||
|
assert_eq!(r.outcomes.len(), lines.len());
|
||||||
|
let counted = r.outcomes.iter().filter(|o| **o == Outcome::Counted).count();
|
||||||
|
assert_eq!(counted, usize::from(r.standing.is_some()), "{lines:?}");
|
||||||
|
assert!(counted <= 1);
|
||||||
|
}
|
||||||
|
assert_eq!(reconcile(&[]).standing, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_for_proven_while_held_is_set_aside_and_the_vote_of_before_stays() {
|
||||||
|
let before = line(100, 1, AGAINST);
|
||||||
|
let held_for = Proven { held: true, ..line(200, 1, FOR) };
|
||||||
|
for lines in [[before, held_for], [held_for, before]] {
|
||||||
|
let (outcomes, standing) = by_position(&lines);
|
||||||
|
assert_eq!(outcomes, vec![(before.position, Outcome::Counted), (held_for.position, Outcome::Objection)]);
|
||||||
|
assert_eq!(standing, Some(before.position), "the *for* takes no place in the order");
|
||||||
|
}
|
||||||
|
// Alone, it leaves nothing standing: the vote of before, if any, is left as it is.
|
||||||
|
let r = reconcile(&[held_for]);
|
||||||
|
assert_eq!((r.outcomes, r.standing), (vec![Outcome::Objection], None));
|
||||||
|
// Against and a withdrawal, proven while held, count.
|
||||||
|
for choice in [AGAINST, NONE] {
|
||||||
|
let held = Proven { held: true, ..line(300, 1, choice) };
|
||||||
|
let r = reconcile(&[before, held_for, held]);
|
||||||
|
assert_eq!(r.standing, Some(2), "{choice:?}");
|
||||||
|
assert_eq!(r.outcomes, vec![Outcome::Superseded, Outcome::Objection, Outcome::Counted]);
|
||||||
|
}
|
||||||
|
assert!(discarded_by_objection(FOR, true));
|
||||||
|
assert!(!discarded_by_objection(FOR, false));
|
||||||
|
assert!(!discarded_by_objection(AGAINST, true) && !discarded_by_objection(NONE, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_lines_at_the_same_place_the_later_given_stands() {
|
||||||
|
let r = reconcile(&[line(0, 3, FOR), line(0, 3, AGAINST)]);
|
||||||
|
assert_eq!(r.standing, Some(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_simulated_vote_at_block_zero_comes_before_any_vote_of_the_chain() {
|
||||||
|
// The simulation proves at block 0, in the order votes arrive (ADR 0012).
|
||||||
|
let simulated = line(0, 41, FOR);
|
||||||
|
let chain = line(1, 0, AGAINST);
|
||||||
|
assert_eq!(by_position(&[chain, simulated]).1, Some(chain.position));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn what_each_stage_weighs() {
|
||||||
|
let closed = Rules { chain_open: false, simulated_adopts: SIMULATED_ADOPTS };
|
||||||
|
for stage in [Stage::Recorded, Stage::Pending, Stage::Simulated] {
|
||||||
|
assert!(closed.shown(stage), "{stage:?}: everything is seen while the chain is closed");
|
||||||
|
}
|
||||||
|
assert!(closed.adopts(Stage::Recorded));
|
||||||
|
assert!(!closed.adopts(Stage::Pending), "never an intention without proof");
|
||||||
|
assert!(closed.adopts(Stage::Simulated), "the default: a simulated vote counts like a proven one");
|
||||||
|
// The switch: shown, never counted for adoption.
|
||||||
|
let shown_only = Rules { simulated_adopts: false, ..closed };
|
||||||
|
assert!(shown_only.shown(Stage::Simulated) && !shown_only.adopts(Stage::Simulated));
|
||||||
|
assert!(shown_only.adopts(Stage::Recorded) && !shown_only.adopts(Stage::Pending));
|
||||||
|
// The chain opens: the simulated votes weigh nothing any more, anywhere.
|
||||||
|
for adopts in [true, false] {
|
||||||
|
let open = Rules { chain_open: true, simulated_adopts: adopts };
|
||||||
|
assert!(!open.shown(Stage::Simulated) && !open.adopts(Stage::Simulated));
|
||||||
|
assert!(open.shown(Stage::Pending) && !open.adopts(Stage::Pending));
|
||||||
|
assert!(open.shown(Stage::Recorded) && open.adopts(Stage::Recorded));
|
||||||
|
}
|
||||||
|
assert_eq!(Stage::of_source("simulated"), Stage::Simulated);
|
||||||
|
assert_eq!(Stage::of_source("chain"), Stage::Recorded);
|
||||||
|
assert_eq!(Stage::of_source("server"), Stage::Recorded);
|
||||||
|
for o in [Outcome::Counted, Outcome::Superseded, Outcome::Objection] {
|
||||||
|
assert_eq!(Outcome::parse(o.as_str()), Some(o));
|
||||||
|
}
|
||||||
|
assert_eq!(Outcome::parse("maybe"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shift(for_votes: i64, against: i64) -> Shift {
|
||||||
|
Shift { for_votes, against }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tally(for_votes: u32, against: u32) -> Tally {
|
||||||
|
Tally { for_votes, against }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_intention_takes_the_place_of_the_vote_one_sees_as_one_s_own() {
|
||||||
|
let w = |seen, intent, current, held| weigh_intent(seen, intent, current, held);
|
||||||
|
// A first vote.
|
||||||
|
assert_eq!(w(None, FOR, true, false), Weighed { shift: shift(1, 0), pending: tally(1, 0) });
|
||||||
|
// From *for* to *against*: one *for* less, one *against* more, that one waiting.
|
||||||
|
assert_eq!(w(Some(FOR), AGAINST, true, false), Weighed { shift: shift(-1, 1), pending: tally(0, 1) });
|
||||||
|
// The same choice again: nothing moves, the voice now waits for its proof.
|
||||||
|
assert_eq!(w(Some(FOR), FOR, true, false), Weighed { shift: shift(0, 0), pending: tally(1, 0) });
|
||||||
|
// A withdrawal takes the vote away at once.
|
||||||
|
assert_eq!(w(Some(AGAINST), NONE, true, false), Weighed { shift: shift(0, -1), pending: tally(0, 0) });
|
||||||
|
assert_eq!(w(None, NONE, true, false), Weighed::default());
|
||||||
|
// On a revision gone by: it adds nothing, it still takes the vote of before away.
|
||||||
|
assert_eq!(w(Some(FOR), AGAINST, false, false), Weighed { shift: shift(-1, 0), pending: tally(0, 0) });
|
||||||
|
// Held back by an objection: a *for* weighs nothing (it would be set aside)...
|
||||||
|
assert_eq!(w(Some(AGAINST), FOR, true, true), Weighed::default());
|
||||||
|
assert_eq!(w(None, FOR, true, true), Weighed::default());
|
||||||
|
// ...against and a withdrawal do.
|
||||||
|
assert_eq!(w(Some(FOR), AGAINST, true, true), Weighed { shift: shift(-1, 1), pending: tally(0, 1) });
|
||||||
|
assert_eq!(w(Some(FOR), NONE, true, true), Weighed { shift: shift(-1, 0), pending: tally(0, 0) });
|
||||||
|
// A « seen » withdrawal is no vote.
|
||||||
|
assert_eq!(w(Some(NONE), FOR, true, false), w(None, FOR, true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_shift_never_takes_the_totals_below_zero() {
|
||||||
|
assert_eq!(shift(-1, 2).apply(tally(3, 0)), tally(2, 2));
|
||||||
|
assert_eq!(shift(-1, -1).apply(tally(0, 0)), tally(0, 0));
|
||||||
|
assert_eq!(shift(1, 0).apply(tally(u32::MAX, 0)), tally(u32::MAX, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
pub mod address;
|
pub mod address;
|
||||||
pub mod assist;
|
pub mod assist;
|
||||||
pub mod ballot;
|
pub mod ballot;
|
||||||
|
pub mod buffer;
|
||||||
pub mod compose;
|
pub mod compose;
|
||||||
pub mod correction;
|
pub mod correction;
|
||||||
pub mod edit;
|
pub mod edit;
|
||||||
|
|||||||
@@ -23,7 +23,10 @@
|
|||||||
//! administrator; it is reiterated by the person who raised it only —
|
//! administrator; it is reiterated by the person who raised it only —
|
||||||
//! `lifts_objection` / `reiterates_objection`;
|
//! `lifts_objection` / `reiterates_objection`;
|
||||||
//! - each person reads their own messages only — `reads_message`
|
//! - each person reads their own messages only — `reads_message`
|
||||||
//! (2026-10-31, lot F, ADR 0010).
|
//! (2026-10-31, lot F, ADR 0010);
|
||||||
|
//! - while the chain is closed, the vote is simulated: whoever may ask for a
|
||||||
|
//! ballot votes on the server, marked as simulated — `casts_simulated`
|
||||||
|
//! (2026-11-02, lot V, ADR 0012).
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -152,6 +155,16 @@ pub fn chain_vote_counts(member: bool) -> bool {
|
|||||||
member
|
member
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// While the chain is closed (no vote account yet), a group that votes on the
|
||||||
|
/// chain votes in simulation (Yvv, 2026-11-02: « Simule le vote à tout endroit »,
|
||||||
|
/// 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.
|
||||||
|
pub fn casts_simulated(identity: &Identity, group: &Group, granted: bool, chain_open: bool) -> bool {
|
||||||
|
!chain_open && asks_ballot(identity, group, granted)
|
||||||
|
}
|
||||||
|
|
||||||
/// Access management stays with trustinWallet's administrators.
|
/// Access management stays with trustinWallet's administrators.
|
||||||
pub fn manages_access(identity: &Identity) -> bool {
|
pub fn manages_access(identity: &Identity) -> bool {
|
||||||
identity.admin
|
identity.admin
|
||||||
@@ -276,6 +289,20 @@ mod tests {
|
|||||||
assert!(!chain_vote_counts(false), "a former member, or an account without identity, does not vote");
|
assert!(!chain_vote_counts(false), "a former member, or an account without identity, does not vote");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn while_the_chain_is_closed_whoever_may_ask_a_ballot_votes_in_simulation() {
|
||||||
|
let ml = group("monnaie-libre");
|
||||||
|
let invited = who(IdentityKind::Invite, false, false);
|
||||||
|
let member = who(IdentityKind::G1, true, false);
|
||||||
|
assert!(casts_simulated(&invited, &ml, true, false));
|
||||||
|
assert!(casts_simulated(&member, &ml, false, false));
|
||||||
|
assert!(!casts_simulated(&invited, &ml, false, false), "not without opening the group");
|
||||||
|
// The chain open: a ballot to sign, never a simulated vote.
|
||||||
|
assert!(!casts_simulated(&member, &ml, false, true));
|
||||||
|
// A managed group votes on the server, never in simulation.
|
||||||
|
assert!(!casts_simulated(&invited, &group("essai"), true, false));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn in_a_managed_group_every_member_votes_on_the_server() {
|
fn in_a_managed_group_every_member_votes_on_the_server() {
|
||||||
let essai = group("essai");
|
let essai = group("essai");
|
||||||
|
|||||||
Reference in New Issue
Block a user