Lot S, phase 2 : le quorum dans le serveur (ADR 0013), vu rouge puis vert

- migration 0011 (additive ; 0010 reste au lot V) : documents.quorums (par cran,
  NULL = vote_floor à chaque cran, le C d'avant), units.quorum (NULL = celui du
  cran), journaux quorum_changes et quorum_amendments ;
- quorum effectif = units.quorum ?? quorums[cran] ; les propositions d'inertie et
  les modules en héritent ; p0 = 2 × vote_floor / W, jamais le quorum d'un article ;
- route PUT /units/{id}/quorum { "quorum": n | null } : trustin_access::sets_quorum
  (l'auteur de la première version, un administrateur pour le seed), refusée dès
  qu'un vote existe (quorum::voted_units : vote du serveur, bulletin de la chaîne
  en attente ou confirmé, adoption) ; bornes 2..W, 1..W dans le groupe d'essai ;
- amendement gardé 2026-11-03-quorums-par-cran : écrit les quorums du seed là où
  il n'y en a pas, une fois par document, consigné, file de publication là où il
  publie ;
- JSON : evaluation.quorum / yesNeeded / noNeeded ; l'unité : quorum,
  quorumDefault, canSetQuorum ; doc.quorums ; levels[].quorum ; document.json
  publié : quorums ; adoptions.params : quorum (plus de constant) ;
- seed : quorums 12 · 12 · 50 · 50 · 50 (Monnaie libre), 1 partout (essai).
- tests : tests/quorum.rs (5) et les règles pures ; les scénarios du groupe
  d'essai qui demandaient deux voix posent le quorum 2 de l'article, comme son
  auteur le ferait ; les chiffres de Monnaie libre suivent (9 au cran 2, 39 au
  cran 3, 43 au cran 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 05:00:41 +01:00
co-authored by Claude Opus 5.5
parent 37147d1f59
commit 01b7841472
20 changed files with 1205 additions and 95 deletions
+31 -3
View File
@@ -11,16 +11,18 @@
//! section, ADR 0009), a module amendment (`module_amend`), built from what the
//! document holds; a starting text in force the seed now puts forward as a seed
//! proposal (ADR 0011), a seed proposal amendment (`seed_proposal_amend`), on an
//! article nobody has touched.
//! article nobody has touched; the quorums by level the seed now gives a document
//! (ADR 0013), a quorum amendment (`quorum_amend`), where the document holds none.
use crate::AppState;
use crate::inertia_amend;
use crate::module_amend;
use crate::modules;
use crate::quorum_amend;
use crate::seed_amend;
use crate::seed_proposal_amend;
use crate::util::{self, LangMap};
use ld_engine::inertia::InertiaLevel;
use ld_engine::inertia::{InertiaLevel, quorum_bounds};
use serde::Deserialize;
use std::path::Path;
@@ -63,8 +65,13 @@ pub struct SeedModule {
#[serde(rename_all = "camelCase")]
pub struct SeedInstance {
pub group: String,
/// The reference quorum Q₀ (the C of before): p0 = 2Q₀/W while too few proposals are
/// voted on; every level's quorum where `quorums` is missing.
pub vote_floor: f64,
pub publish_path: Option<String>,
/// The quorum of each level, 1 to 5 (ADR 0013): the default of an article at that level.
#[serde(default)]
pub quorums: Option<[u32; 5]>,
}
#[derive(Deserialize)]
@@ -134,6 +141,7 @@ pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
module_amend::apply(state, &seeds, module_amend::AMENDMENTS).await?;
// After the text amendments: a starting text replaced first, then put forward.
seed_proposal_amend::apply(state, &seeds, seed_proposal_amend::AMENDMENTS).await?;
quorum_amend::apply(state, &seeds, quorum_amend::AMENDMENTS).await?;
Ok(())
}
@@ -191,8 +199,15 @@ async fn load_file(state: &AppState, path: &Path) -> anyhow::Result<SeedDoc> {
}
/// A seed proposal is open, and its article takes its level from `SEED_PROPOSAL_LEVEL`
/// alone; every other article says its level.
/// alone; every other article says its level. A quorum is never under the lowest an
/// author may set (`quorum_bounds`): 2, or 1 in the test group.
fn check(seed: &SeedDoc) -> anyhow::Result<()> {
for inst in &seed.instances {
let (lowest, _) = quorum_bounds(1, inst.group == trustin_access::TEST_GROUP);
if inst.quorums.is_some_and(|q| q.iter().any(|q| *q < lowest)) {
anyhow::bail!("{}: a quorum under {lowest}", inst.group);
}
}
for u in &seed.units {
if u.versions.iter().any(|v| v.seed_proposal && v.status != "open") {
anyhow::bail!("{}: a seed proposal is open, never in force", u.code);
@@ -229,6 +244,14 @@ async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> a
.bind(&now)
.execute(&mut *tx)
.await?;
// The quorums by level, written apart: only where the seed gives them (ADR 0013).
if let Some(q) = &inst.quorums {
sqlx::query("UPDATE documents SET quorums = ? WHERE id = ?")
.bind(quorums_json(q))
.bind(&doc_id)
.execute(&mut *tx)
.await?;
}
for unit in &seed.units {
let unit_id = util::id();
sqlx::query(
@@ -319,3 +342,8 @@ async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> a
tx.commit().await?;
Ok(())
}
/// The quorums by level as `documents.quorums` holds them: `[12,12,50,50,50]`.
pub fn quorums_json(quorums: &[u32; 5]) -> String {
serde_json::to_string(quorums).unwrap_or_else(|_| "null".into())
}