forked from yvv/decision
WIP lot V : le tampon câblé dans le serveur (migration 0010, simulation, discard-simulation), tests en cours de reprise — repris tel quel après la coupure du 02/11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+103
-18
@@ -13,6 +13,7 @@ use crate::util::{self, LangMap};
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use ld_engine::ballot::Choice;
|
||||
use ld_engine::buffer::{self, Rules, Stage};
|
||||
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,21 @@ pub struct DocState {
|
||||
/// The units that are modules (ADR 0009), by unit id; any other is an article.
|
||||
pub module_kinds: HashMap<String, ModuleKind>,
|
||||
pub proposals: Vec<ProposalRow>,
|
||||
/// What adoption counts (ADR 0012): recorded votes, and simulated ones by
|
||||
/// default — never an intention without proof.
|
||||
pub tallies: HashMap<String, Tally>,
|
||||
/// What is recorded and shown: recorded votes and simulated ones, without the
|
||||
/// intentions (what a published text may say).
|
||||
pub recorded: HashMap<String, Tally>,
|
||||
/// The totals shown: `recorded`, each intention in place of the vote its
|
||||
/// session sees as its own (`ld_engine::buffer::weigh_intent`).
|
||||
pub shown: HashMap<String, Tally>,
|
||||
/// The part of `shown` still waiting for its proof.
|
||||
pub pending: HashMap<String, Tally>,
|
||||
/// The part of `shown` that comes from the simulation.
|
||||
pub simulated: HashMap<String, Tally>,
|
||||
/// What each stage of a vote weighs.
|
||||
pub rules: Rules,
|
||||
/// 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>,
|
||||
@@ -333,30 +348,70 @@ pub async fn load_state(state: &AppState, doc_id: &str) -> AppResult<DocState> {
|
||||
struct TallyRow {
|
||||
proposal_id: String,
|
||||
choice: String,
|
||||
source: String,
|
||||
n: i64,
|
||||
}
|
||||
let rows = sqlx::query_as::<_, TallyRow>(
|
||||
"SELECT v.proposal_id, v.choice, COUNT(*) AS n FROM votes v JOIN proposals p ON p.id = v.proposal_id
|
||||
"SELECT v.proposal_id, v.choice, v.source, COUNT(*) AS n FROM votes v JOIN proposals p ON p.id = v.proposal_id
|
||||
JOIN units u ON u.id = p.unit_id
|
||||
WHERE u.doc_id = ? AND v.revision = p.revision GROUP BY v.proposal_id, v.choice",
|
||||
WHERE u.doc_id = ? AND v.revision = p.revision GROUP BY v.proposal_id, v.choice, v.source",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
let mut tallies: HashMap<String, Tally> = HashMap::new();
|
||||
for r in rows {
|
||||
let t = tallies.entry(r.proposal_id).or_default();
|
||||
let n = u32::try_from(r.n).unwrap_or(0);
|
||||
match r.choice.as_str() {
|
||||
"for" => t.for_votes = n,
|
||||
"against" => t.against = n,
|
||||
let rules = state.vote_rules();
|
||||
let add = |map: &mut HashMap<String, Tally>, id: &str, choice: &str, n: u32| {
|
||||
let t = map.entry(id.to_owned()).or_default();
|
||||
match choice {
|
||||
"for" => t.for_votes = t.for_votes.saturating_add(n),
|
||||
"against" => t.against = t.against.saturating_add(n),
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
let (mut tallies, mut recorded, mut simulated) = (HashMap::new(), HashMap::new(), HashMap::new());
|
||||
for r in rows {
|
||||
let n = u32::try_from(r.n).unwrap_or(0);
|
||||
let stage = Stage::of_source(&r.source);
|
||||
if rules.adopts(stage) {
|
||||
add(&mut tallies, &r.proposal_id, &r.choice, n);
|
||||
}
|
||||
if rules.shown(stage) {
|
||||
add(&mut recorded, &r.proposal_id, &r.choice, n);
|
||||
if stage == Stage::Simulated {
|
||||
add(&mut simulated, &r.proposal_id, &r.choice, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A standing objection holds back the latest arrival of its pair (never a version
|
||||
// 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?;
|
||||
|
||||
// The intentions (ADR 0012): seen at once in the totals shown, in place of the
|
||||
// vote their session sees as its own; never counted for adoption.
|
||||
let mut shown = recorded.clone();
|
||||
let mut pending: HashMap<String, Tally> = HashMap::new();
|
||||
let mut seen_by: HashMap<String, HashMap<String, MyVote>> = HashMap::new();
|
||||
for i in ballots::intents(state, doc_id).await? {
|
||||
let Some(p) = proposals.iter().find(|p| p.id == i.proposal_id) else { continue };
|
||||
if !matches!(p.status.as_str(), "open" | "in_force") {
|
||||
continue;
|
||||
}
|
||||
if !seen_by.contains_key(&i.requester) {
|
||||
seen_by.insert(i.requester.clone(), ballots::my_votes(state, &i.requester, doc_id).await?);
|
||||
}
|
||||
let seen = seen_by
|
||||
.get(&i.requester)
|
||||
.and_then(|m| m.get(&p.id))
|
||||
.filter(|v| v.revision == p.revision)
|
||||
.and_then(|v| Choice::parse(&v.choice));
|
||||
let w = buffer::weigh_intent(seen, i.choice, i.revision == p.revision, blocked.contains(&p.id));
|
||||
let t = shown.entry(p.id.clone()).or_default();
|
||||
*t = w.shift.apply(*t);
|
||||
let q = pending.entry(p.id.clone()).or_default();
|
||||
q.for_votes = q.for_votes.saturating_add(w.pending.for_votes);
|
||||
q.against = q.against.saturating_add(w.pending.against);
|
||||
}
|
||||
let (electorate, electorate_source) = electorate(state, &group).await?;
|
||||
let measure = participation::stored(state, &group.slug).await?;
|
||||
let participation = Participation::of(
|
||||
@@ -373,6 +428,11 @@ pub async fn load_state(state: &AppState, doc_id: &str) -> AppResult<DocState> {
|
||||
module_kinds,
|
||||
proposals,
|
||||
tallies,
|
||||
recorded,
|
||||
shown,
|
||||
pending,
|
||||
simulated,
|
||||
rules,
|
||||
blocked,
|
||||
signal_counts,
|
||||
electorate,
|
||||
@@ -397,13 +457,34 @@ impl DocState {
|
||||
self.tallies.get(id).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The totals shown to people: today the live count, as `tally`. The one
|
||||
/// place where a publication by steps would plug in — on the chain, who
|
||||
/// voted and when is public, and live totals tell what (the secret of the
|
||||
/// vote, review of 74f96be, a decision that waits for Yvv). Adoption reads
|
||||
/// `tally`; the published texts (`publish.rs`) would follow this one.
|
||||
/// The totals shown to people: the live count, intentions included (ADR
|
||||
/// 0012: « je vois ce que j'ai fait sans attendre »). The one place where a
|
||||
/// publication by steps would plug in — on the chain, who voted and when is
|
||||
/// public, and live totals tell what (the secret of the vote, review of
|
||||
/// 74f96be). Adoption reads `tally`, never an intention.
|
||||
pub fn shown_tally(&self, id: &str) -> Tally {
|
||||
self.tally(id)
|
||||
self.shown.get(id).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The part of `shown_tally` still waiting for its proof.
|
||||
pub fn pending_tally(&self, id: &str) -> Tally {
|
||||
self.pending.get(id).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The part of `shown_tally` that comes from the simulation.
|
||||
pub fn simulated_tally(&self, id: &str) -> Tally {
|
||||
self.simulated.get(id).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// What is recorded and shown, without the intentions: what a published text says.
|
||||
pub fn recorded_tally(&self, id: &str) -> Tally {
|
||||
self.recorded.get(id).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The simulated share of what adoption counts for a version (zero when the
|
||||
/// simulation counts for display only).
|
||||
pub fn simulated_counted(&self, id: &str) -> Tally {
|
||||
if self.rules.adopts(Stage::Simulated) { self.simulated_tally(id) } else { Tally::default() }
|
||||
}
|
||||
|
||||
/// How far the threshold is, as shown: on `shown_tally`.
|
||||
@@ -641,6 +722,7 @@ fn proposal_json(
|
||||
me: &str,
|
||||
) -> serde_json::Value {
|
||||
let (mine, ballot) = mine;
|
||||
let seen = ballots::seen(p.revision, mine, ballot);
|
||||
let eval = st.shown_evaluation(unit, p);
|
||||
let (kind, target) = match p.kind() {
|
||||
// A module's version says what it holds (ADR 0009).
|
||||
@@ -661,14 +743,17 @@ fn proposal_json(
|
||||
"submittedAt": p.submitted_at, "enteredAt": p.entered_at, "confirmedAt": p.confirmed_at,
|
||||
"seedMinimal": p.status == "in_force" && p.entered_at.is_none() && p.confirmed_at.is_none() && p.origin == "seed",
|
||||
"tally": st.shown_tally(&p.id),
|
||||
"tallyPending": st.pending_tally(&p.id),
|
||||
"tallySimulated": st.simulated_tally(&p.id),
|
||||
"evaluation": eval,
|
||||
"votedAtInertia": voted_at,
|
||||
"blocked": st.blocked.contains(&p.id),
|
||||
"votable": st.votable(p),
|
||||
"objections": st.signal_counts.get(&p.id).copied().unwrap_or(0),
|
||||
"myVote": mine.map(|v| v.choice.as_str()),
|
||||
"myVoteStale": mine.is_some_and(|v| v.revision != p.revision),
|
||||
"myVoteChain": mine.is_some_and(|v| v.source == "chain"),
|
||||
"myVote": seen.choice,
|
||||
"myVoteState": seen.state,
|
||||
"myVoteStale": seen.stale,
|
||||
"myVoteChain": seen.chain,
|
||||
"myBallot": ballot,
|
||||
"translations": translations.cloned().unwrap_or_default(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user