v3 : inertie à cinq crans — moteur, données, serveur (L10)
Moteur (ld-engine/inertia.rs) : cinq crans, M_k = 50, 50, 55, 60, 66 % ; s_k = 60, 67, 75, 83, 90 % de « pour » au-delà de C à la participation habituelle p̃ ; G_k = ln(1 − (s_k − M_k)/(1 − M_k)) / ln(p̃). p̃ = médiane des votants des propositions du groupe (au moins 5, votées dans l'année), sinon la référence 2C/W ; bornée à [1 %, 50 %]. Le changement d'inertie hérite du cran de l'article (remplace « inertie + »). Un seuil qui tombe sur un entier n'y gagne plus une voix par arrondi. Données : migration 0005 (les quatre mots deviennent 2 à 5, gardés tels quels dans *_legacy ; adoptions intactes ; journaux inertia_changes et inertia_amendments). Seed : 3 partout, 4 pour E1 et E2. Amendement d'inertie gardé (E1, E2 : 3 → 4, seulement si aucun vote n'a changé leur inertie ; une proposition d'inertie ouverte reste ouverte, votée au nouveau cran), journalisé, une seule fois, republié. Serveur : participation mesurée une fois par jour (settings), échelle dans les seuils, le balayage et l'adoption ; crans, M, G, s_k, p̃ et sa source servis au client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use ld_engine::assist::{self, Hint};
|
||||
use ld_engine::edit::{self, EditKind};
|
||||
use ld_engine::threshold::InertiaPreset;
|
||||
use ld_engine::inertia::InertiaLevel;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -191,13 +191,14 @@ async fn new_unit(state: &AppState, st: &DocState, after: Option<&str>) -> AppRe
|
||||
let section = after.and_then(|a| st.units.iter().find(|u| u.id == a)).and_then(|u| u.section.clone());
|
||||
sqlx::query(
|
||||
"INSERT INTO units (id, doc_id, code, section, position, inertia, in_force_id, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'standard', NULL, ?)",
|
||||
VALUES (?, ?, ?, ?, ?, ?, NULL, ?)",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&st.doc.id)
|
||||
.bind(format!("N{}", n + 1))
|
||||
.bind(section)
|
||||
.bind(position_after(&st.units, after))
|
||||
.bind(InertiaLevel::DEFAULT.as_db())
|
||||
.bind(util::now())
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
@@ -626,28 +627,33 @@ pub async fn detail(
|
||||
_ => None,
|
||||
};
|
||||
let eval = st.evaluation(unit, &p);
|
||||
let voted_at = if p.kind == "inertia" { docs::inertia_of(unit).raised() } else { docs::inertia_of(unit) };
|
||||
let voted_at = st.voting_level(unit, &p);
|
||||
let target = p.inertia_target.as_deref().and_then(InertiaLevel::parse);
|
||||
let tr = docs::translations_for(&state, &[&p]).await?;
|
||||
Ok(Json(json!({
|
||||
"proposal": {
|
||||
"id": p.id, "kind": p.kind, "inertiaTarget": p.inertia_target, "title": p.title, "body": p.body,
|
||||
"id": p.id, "kind": p.kind, "inertiaTarget": target, "title": p.title, "body": p.body,
|
||||
"status": p.status, "origin": p.origin, "label": p.label, "revision": p.revision,
|
||||
"author": p.author_name, "authorInvited": p.author_invited.unwrap_or(false),
|
||||
"mine": p.author_id.as_deref() == Some(user.id.as_str()),
|
||||
"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.tally(&p.id), "evaluation": eval, "votedAtInertia": voted_at.as_str(),
|
||||
"tally": st.tally(&p.id), "evaluation": eval, "votedAtInertia": voted_at,
|
||||
"blocked": st.blocked.contains(&p.id),
|
||||
"myVote": my_vote.as_ref().map(|v| &v.0), "myVoteStale": my_vote.as_ref().is_some_and(|v| v.1 != p.revision),
|
||||
"translations": tr.get(&p.id).cloned().unwrap_or_default(),
|
||||
},
|
||||
"unit": { "id": unit.id, "code": unit.code, "inertia": unit.inertia },
|
||||
"unit": { "id": unit.id, "code": unit.code, "inertia": docs::inertia_of(unit) },
|
||||
"inForce": in_force.map(|v| json!({ "id": v.id, "title": v.title, "body": v.body })),
|
||||
"diff": diff,
|
||||
"history": history,
|
||||
"signals": signals,
|
||||
"electorate": st.electorate,
|
||||
"electorateSource": st.electorate_source,
|
||||
"voteFloor": st.doc.vote_floor,
|
||||
"participation": st.participation_json(),
|
||||
// The five levels, with the *for* votes each would ask at this version's participation.
|
||||
"levels": st.levels_json(st.tally(&p.id).cast()),
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -783,18 +789,20 @@ pub async fn lift_signal(
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct InertiaBody {
|
||||
target: String,
|
||||
/// A level, 1 to 5 (a word of before is read as its level).
|
||||
target: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Propose a change of inertia; it is voted one level above the unit. An open
|
||||
/// proposal for the same target is reused rather than duplicated.
|
||||
/// Propose a change of inertia; it is voted at the level of the unit (it
|
||||
/// inherits it, ADR 0007). An open proposal for the same target is reused
|
||||
/// rather than duplicated.
|
||||
pub async fn propose_inertia(
|
||||
State(state): State<AppState>,
|
||||
User(user): User,
|
||||
Path(unit_id): Path<String>,
|
||||
Json(b): Json<InertiaBody>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let target = InertiaPreset::parse(&b.target).ok_or(AppError::Invalid("inertia"))?;
|
||||
let target = serde_json::from_value::<InertiaLevel>(b.target).map_err(|_| AppError::Invalid("inertia"))?;
|
||||
let doc_id = doc_of_unit(&state, &unit_id).await?;
|
||||
let st = docs::load_state(&state, &doc_id).await?;
|
||||
docs::require_open(&state, &user, &st.group).await?;
|
||||
@@ -806,17 +814,18 @@ pub async fn propose_inertia(
|
||||
p.unit_id == unit_id
|
||||
&& p.kind == "inertia"
|
||||
&& p.status == "open"
|
||||
&& p.inertia_target.as_deref() == Some(target.as_str())
|
||||
&& p.inertia_target.as_deref().and_then(InertiaLevel::parse) == Some(target)
|
||||
}) {
|
||||
return Ok(Json(json!({ "id": existing.id, "existing": true })));
|
||||
}
|
||||
let title = format!("inertia:{}", target.as_str());
|
||||
let title = format!("inertia:{}", target.get());
|
||||
let level = target.as_db();
|
||||
let id = insert_proposal(
|
||||
&state,
|
||||
NewProposal {
|
||||
unit_id: &unit_id,
|
||||
kind: "inertia",
|
||||
inertia_target: Some(target.as_str()),
|
||||
inertia_target: Some(&level),
|
||||
title: &title,
|
||||
body: "",
|
||||
author: &user.id,
|
||||
@@ -869,14 +878,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn unit(id: &str, position: f64) -> UnitRow {
|
||||
UnitRow {
|
||||
id: id.into(),
|
||||
code: id.into(),
|
||||
section: None,
|
||||
position,
|
||||
inertia: "standard".into(),
|
||||
in_force_id: None,
|
||||
}
|
||||
UnitRow { id: id.into(), code: id.into(), section: None, position, inertia: "3".into(), in_force_id: None }
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user