forked from yvv/decision
L'ordre des articles de chaque section (cran 2) et le titre du document (cran 4) deviennent des modules : des unités à part, dont les versions sont des propositions votées comme les articles (dépliées, panier, chaîne). - moteur : ld-engine/src/order.rs (qui est dans l'ordre, ordre proposé, complété à la fin quand un article entre ou sort, places redistribuées, nouvel article en fin de section) ; pending : un ordre de moins de deux articles n'est jamais à voter ; - données : migration 0007 (units.kind, module_amendments) ; le seed déclare ses modules ; module_amend.rs les ajoute une fois, gardé et journalisé, aux documents nés sans eux, sans toucher articles ni votes ; - serveur : modules.rs, order.rs, doc_title.rs (portes pour proposer, entrée en vigueur : les articles changent de place, le document change de titre, en et es passent « à revoir ») ; publication des modules ; - reste mineur : la médiane compte les statuts open, in_force, applied, et eux seuls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
4.5 KiB
Rust
123 lines
4.5 KiB
Rust
//! What is left to vote in a document — the line of « Décisions ».
|
|
//!
|
|
//! A module is an article, the title of the document, or the order of a
|
|
//! section (ADR 0009) — a unit. It stays **to vote** as long as no version
|
|
//! of it was adopted by a vote: its version in force is a seed a minima not yet
|
|
//! confirmed, or it has no version in force and at least one open version.
|
|
//! Variants of an article count once. The order of a section of fewer than two
|
|
//! articles has nothing to decide: never to vote. For a person who votes, a module is
|
|
//! **without their vote** when they hold no valid vote (one not « to
|
|
//! reconfirm ») on any of its versions.
|
|
|
|
/// One article, as one person sees it.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct Module {
|
|
/// The version in force, if any: `Some(true)` when a vote adopted or
|
|
/// confirmed it, `Some(false)` for a seed a minima.
|
|
pub in_force: Option<bool>,
|
|
/// Open versions of the article (proposals to replace, add or remove it).
|
|
pub open: u32,
|
|
/// The person holds a valid vote on one of its versions, in force or open.
|
|
pub voted: bool,
|
|
/// Nothing to decide: the order of a section of fewer than two articles
|
|
/// (`order::is_moot`). Never to vote, and not shown.
|
|
pub moot: bool,
|
|
}
|
|
|
|
impl Module {
|
|
pub fn to_vote(&self) -> bool {
|
|
if self.moot {
|
|
return false;
|
|
}
|
|
match self.in_force {
|
|
Some(adopted) => !adopted,
|
|
None => self.open > 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct Pending {
|
|
/// Modules not yet adopted by a vote.
|
|
pub to_vote: u32,
|
|
/// Among them, those where the person has no valid vote.
|
|
pub without_my_vote: u32,
|
|
}
|
|
|
|
pub fn pending(modules: &[Module]) -> Pending {
|
|
modules.iter().filter(|m| m.to_vote()).fold(Pending::default(), |acc, m| Pending {
|
|
to_vote: acc.to_vote + 1,
|
|
without_my_vote: acc.without_my_vote + u32::from(!m.voted),
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const SEED: Option<bool> = Some(false);
|
|
const ADOPTED: Option<bool> = Some(true);
|
|
|
|
fn m(in_force: Option<bool>, open: u32, voted: bool) -> Module {
|
|
Module { in_force, open, voted, moot: false }
|
|
}
|
|
|
|
#[test]
|
|
fn a_seed_a_minima_stays_to_vote_until_a_vote_confirms_it() {
|
|
assert!(m(SEED, 0, false).to_vote());
|
|
assert!(m(SEED, 2, true).to_vote());
|
|
assert!(!m(ADOPTED, 0, false).to_vote());
|
|
}
|
|
|
|
#[test]
|
|
fn an_adopted_article_is_no_longer_to_vote_even_with_rivals() {
|
|
assert!(!m(ADOPTED, 3, false).to_vote());
|
|
}
|
|
|
|
#[test]
|
|
fn an_empty_slot_is_to_vote_only_with_an_open_version() {
|
|
assert!(m(None, 1, false).to_vote());
|
|
assert!(!m(None, 0, false).to_vote());
|
|
}
|
|
|
|
#[test]
|
|
fn variants_count_once_and_my_vote_on_any_version_counts() {
|
|
// Seed of the certification act: 3 seeds a minima, 30 open articles (two with a variant).
|
|
let mut modules = vec![m(SEED, 0, false); 3];
|
|
modules.extend(vec![m(None, 1, false); 28]);
|
|
modules.extend(vec![m(None, 2, false); 2]);
|
|
assert_eq!(pending(&modules), Pending { to_vote: 33, without_my_vote: 33 });
|
|
modules[0].voted = true;
|
|
modules[31].voted = true;
|
|
assert_eq!(pending(&modules), Pending { to_vote: 33, without_my_vote: 31 });
|
|
// Once adopted, an article leaves both counts, voted or not.
|
|
modules[1].in_force = ADOPTED;
|
|
modules[31].in_force = ADOPTED;
|
|
assert_eq!(pending(&modules), Pending { to_vote: 31, without_my_vote: 30 });
|
|
}
|
|
|
|
#[test]
|
|
fn an_order_with_nothing_to_decide_is_never_to_vote() {
|
|
let one_article = Module { moot: true, ..m(SEED, 1, false) };
|
|
assert!(!one_article.to_vote());
|
|
assert_eq!(pending(&[one_article]), Pending::default());
|
|
}
|
|
|
|
#[test]
|
|
fn the_modules_of_the_certification_act() {
|
|
// 33 articles; the title of the document; the order of each of the 8 sections,
|
|
// all seeds a minima — but « ordonnancement » holds one article: 41 to vote.
|
|
let mut modules = vec![m(SEED, 0, false); 3];
|
|
modules.extend(vec![m(None, 1, false); 30]);
|
|
modules.push(m(SEED, 0, false));
|
|
modules.extend(vec![m(SEED, 0, false); 7]);
|
|
modules.push(Module { moot: true, ..m(SEED, 0, false) });
|
|
assert_eq!(pending(&modules), Pending { to_vote: 41, without_my_vote: 41 });
|
|
}
|
|
|
|
#[test]
|
|
fn nothing_to_vote_in_an_empty_document() {
|
|
assert_eq!(pending(&[]), Pending::default());
|
|
}
|
|
}
|