Fusion de chantier/acte-certification (lots H et S) dans le lot V : le tampon et ses états, avec le quorum et les Propositions seed

- conflits résolus des deux côtés : kept_params garde le quorum (S) et la part simulée, la version déplacée, le cran d'avant (V) ; proposals.rs garde seedMinimal / seedProposal (S) et tallyPending / tallySimulated / myVoteState (V) ;
- simulation::reexamine juge sur le quorum gardé par l'adoption (params.quorum, à défaut celui de l'article) ;
- l'acte répété n'examine plus une adoption qui tient (params.reexamined) ;
- tests V repris sans texte en vigueur au départ (I1, E1 : la Proposition seed) et sur un quorum 2 (small_web) ; le test overview d'une personne invitée dans Monnaie libre suit la simulation (elle vote, chaîne fermée).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 05:32:51 +01:00
co-authored by Claude Opus 5.5
66 changed files with 5134 additions and 701 deletions
+30 -6
View File
@@ -16,7 +16,12 @@
//! its threshold, it **returns among the proposals** and the version it had
//! displaced comes back into force (a removed text comes back, a level of
//! inertia goes back, the order and the title of before apply again); its
//! adoption is void. At or above, it stays.
//! adoption is void. At or above, it stays, and its adoption says so
//! (`params.reexamined`): the act run again examines it no more.
//!
//! Told only (no `--apply`), the re-examination sets the simulated votes apart
//! without removing them: an entry that would return only once a more recent one
//! returned before it is told as no longer in effect.
use crate::AppState;
use crate::adoption;
@@ -141,7 +146,8 @@ pub async fn discard(state: &AppState, apply: bool) -> AppResult<Discarded> {
for a in adoptions {
let params: Value = serde_json::from_str(&a.params).unwrap_or(Value::Null);
let simulated = tally_of(&params["simulated"]);
if simulated.cast() == 0 {
// Without simulated votes, or examined by an act already: nothing to examine.
if simulated.cast() == 0 || !params["reexamined"].is_null() {
continue;
}
if let Some(r) = reexamine(state, &a, &params, simulated, apply).await? {
@@ -164,14 +170,19 @@ async fn reexamine(
let in_effect = match a.kind.as_str() {
"remove" => p.status == "applied" && unit.in_force_id.is_none(),
"inertia" => {
p.status == "applied" && p.inertia_target.as_deref().and_then(InertiaLevel::parse) == Some(docs::inertia_of(unit))
p.status == "applied"
&& p.inertia_target.as_deref().and_then(InertiaLevel::parse) == Some(docs::inertia_of(unit))
}
_ => unit.in_force_id.as_deref() == Some(p.id.as_str()),
};
// Applied: the simulated votes are gone. Told only: they still count, set them apart.
let proven = if apply { st.tally(&p.id) } else { minus(st.tally(&p.id), st.simulated_counted(&p.id)) };
let level = serde_json::from_value::<InertiaLevel>(params["level"].clone()).unwrap_or_else(|_| st.voting_level(unit, &p));
let eval = st.scale().evaluate(proven, level).ok();
// At the level and on the quorum it was voted at (ADR 0013; an adoption of before
// holds the C of before, `constant`, which is no quorum: the article's quorum now).
let level =
serde_json::from_value::<InertiaLevel>(params["level"].clone()).unwrap_or_else(|_| st.voting_level(unit, &p));
let quorum = params["quorum"].as_u64().and_then(|q| u32::try_from(q).ok()).unwrap_or_else(|| st.quorum_of(unit));
let eval = st.scale().evaluate(proven, level, quorum).ok();
let holds = eval.is_some_and(|e| e.reached);
let displaced = params["displaced"].as_str().map(str::to_owned);
let mut r = Reexamined {
@@ -196,6 +207,15 @@ async fn reexamine(
if apply && in_effect && !holds {
give_back(state, &st, unit, &p, a, params, displaced.as_deref()).await?;
r.returned = true;
} else if apply {
// It stays (or something else took its place since): examined once, never again.
let mut kept = params.clone();
kept["reexamined"] = serde_json::json!({ "at": util::now(), "proven": r.proven, "required": r.required });
sqlx::query("UPDATE adoptions SET params = ? WHERE id = ?")
.bind(kept.to_string())
.bind(a.id)
.execute(&state.db)
.await?;
}
Ok(Some(r))
}
@@ -225,7 +245,11 @@ async fn give_back(
.await?;
if a.kind == "inertia" {
let from = serde_json::from_value::<InertiaLevel>(params["from_inertia"].clone()).unwrap_or_default();
sqlx::query("UPDATE units SET inertia = ? WHERE id = ?").bind(from.as_db()).bind(&unit.id).execute(&mut *tx).await?;
sqlx::query("UPDATE units SET inertia = ? WHERE id = ?")
.bind(from.as_db())
.bind(&unit.id)
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM inertia_changes WHERE proposal_id = ? AND cause = 'vote'")
.bind(&p.id)
.execute(&mut *tx)