v3 : sur une version retenue, « contre » et retrait oui, « pour » non (moteur, serveur, chaîne)

Décision d'Yvv du 01/11/2026 : « Retirer ou "contre" ». Le moteur dit la règle
(objection::may_cast) ; le vote direct, le vote groupé et le bulletin de la chaîne
l'appliquent par DocState::admits : « pour » 409 objection, « contre » et retrait
acceptés, une ligne refusée refuse tout le vote. Un bulletin scellé avant
l'objection compte tel qu'il est signé ; une version retenue n'entre jamais.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-02 00:03:45 +01:00
co-authored by Claude Opus 5.5
parent 61547c11c0
commit 238c7db93f
8 changed files with 222 additions and 19 deletions
+12 -2
View File
@@ -12,6 +12,7 @@ use crate::participation;
use crate::util::{self, LangMap};
use axum::Json;
use axum::extract::{Path, Query, State};
use ld_engine::ballot::Choice;
use ld_engine::compose::{self, PreviewMode, ProposalKind, ProposalState, UnitState, VersionText};
use ld_engine::inertia::{InertiaLevel, Participation, Scale};
use ld_engine::threshold::{Evaluation, Tally};
@@ -265,7 +266,8 @@ pub struct DocState {
pub module_kinds: HashMap<String, ModuleKind>,
pub proposals: Vec<ProposalRow>,
pub tallies: HashMap<String, Tally>,
/// Versions held back by a standing objection: not votable, never entering into force.
/// Versions held back by a standing objection: against or a withdrawal only, never
/// a new *for* (`admits`), and never entering into force.
pub blocked: HashSet<String>,
/// Standing objections touching each version, either side.
pub signal_counts: HashMap<String, i64>,
@@ -353,7 +355,7 @@ pub async fn load_state(state: &AppState, doc_id: &str) -> AppResult<DocState> {
}
// A standing objection holds back the latest arrival of its pair (never a version
// in force): not votable, and it cannot enter into force (ADR 0010).
// in force): no new *for*, and it cannot enter into force (ADR 0010).
let (blocked, signal_counts) = crate::objections::held(state, doc_id, &units).await?;
let (electorate, electorate_source) = electorate(state, &group).await?;
let measure = participation::stored(state, &group.slug).await?;
@@ -414,6 +416,14 @@ impl DocState {
matches!(p.status.as_str(), "open" | "in_force") && !self.blocked.contains(&p.id)
}
/// May `choice` be cast on `p` now — by one vote, a grouped one or a ballot of the
/// chain? Open or in force; held back by an objection, against or a withdrawal
/// only, never a *for* (`ld_engine::objection::may_cast`, Yvv 2026-11-01).
pub fn admits(&self, p: &ProposalRow, choice: Choice) -> bool {
matches!(p.status.as_str(), "open" | "in_force")
&& ld_engine::objection::may_cast(self.blocked.contains(&p.id), choice)
}
/// The module a unit is, if it is one.
pub fn module_of(&self, unit_id: &str) -> Option<ModuleKind> {
self.module_kinds.get(unit_id).copied()