//! Quorum amendments — the quorums by level a seed now gives a document, written into //! a document born without them (ADR 0013). //! //! Yvv, 2026-11-02: « 12 · 12 · 50 · 50 · 50 » in Monnaie libre, the test group at 1 //! everywhere. A new base gets them from the seed (`quorums` of each instance). An //! existing base — production holds two instances of the certification act — reads //! `vote_floor` at every level until then (the C of before: 50 and 1), and gets them //! through an amendment listed here: written **only where the document holds none**; //! nothing else is touched (no article's own quorum, no vote, no text). //! //! Like the other amendments of the seed, each document is decided once, at the first //! start that knows the amendment, and the decision is kept in `quorum_amendments` //! (applied, refused and why, or present) and in the logs. Applied where the document //! publishes, the publication queue gets a row (`document.json` carries the quorums). use crate::AppState; use crate::seed::{SeedDoc, quorums_json}; use crate::util; /// The quorums by level to write into every document instantiated from a seed. pub struct QuorumAmendment { /// Stable identifier: each document's decision is kept under it. pub id: &'static str, /// Slug of the seed document. pub doc: &'static str, /// The quorums of each group's instance: the seed's today, which must say so. pub quorums: &'static [(&'static str, [u32; 5])], /// Reason given to the publication queue, and so to the commit of the texts. pub reason: &'static str, } pub const AMENDMENTS: &[QuorumAmendment] = &[QuorumAmendment { id: "2026-11-03-quorums-par-cran", doc: "acte-engagement-certification", quorums: &[("monnaie-libre", [12, 12, 50, 50, 50]), ("essai", [1, 1, 1, 1, 1])], reason: "quorums par cran : 12 · 12 · 50 · 50 · 50 (ADR 0013)", }]; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Verdict { Apply, /// Born with these quorums: nothing to do. Present, Refuse(&'static str), } /// The rule, apart from the database: `found` is what the document holds. pub fn verdict(found: Option<&str>, wanted: &[u32; 5]) -> Verdict { match found { None => Verdict::Apply, Some(raw) if serde_json::from_str::<[u32; 5]>(raw).ok().as_ref() == Some(wanted) => Verdict::Present, // Written otherwise: kept as it is. Some(_) => Verdict::Refuse("quorums_set"), } } /// A decision taken at this start. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Outcome { pub amendment: &'static str, pub group: String, pub verdict: Verdict, } /// Decides every amendment not yet decided, document by document. pub async fn apply(state: &AppState, seeds: &[SeedDoc], list: &[QuorumAmendment]) -> anyhow::Result> { let mut out = Vec::new(); for a in list { let docs: Vec<(String, String, Option)> = sqlx::query_as("SELECT id, group_slug, publish_path FROM documents WHERE slug = ? ORDER BY group_slug") .bind(a.doc) .fetch_all(&state.db) .await?; for (doc_id, group, publish_path) in docs { let Some(wanted) = seed_says(seeds, a, &group) else { continue }; let Some(v) = decide(state, a, &wanted, &doc_id, publish_path.is_some()).await? else { continue }; match v { Verdict::Apply => tracing::info!(amendment = a.id, %group, quorums = ?wanted, "quorums written"), Verdict::Present => tracing::info!(amendment = a.id, %group, "quorums: already there"), Verdict::Refuse(why) => { tracing::warn!(amendment = a.id, %group, reason = why, "quorums refused: the document keeps its own") } } if v == Verdict::Apply && publish_path.is_some() { state.publish_wake.notify_one(); } out.push(Outcome { amendment: a.id, group, verdict: v }); } } Ok(out) } /// The seed must give the group's instance the quorums the amendment writes: a seed /// edited later never reaches a base by this way, and a seed without them decides nothing. fn seed_says(seeds: &[SeedDoc], a: &QuorumAmendment, group: &str) -> Option<[u32; 5]> { let wanted = a.quorums.iter().find(|(g, _)| *g == group).map(|(_, q)| *q)?; let declared = seeds .iter() .find(|s| s.slug == a.doc) .and_then(|s| s.instances.iter().find(|i| i.group == group)) .and_then(|i| i.quorums); if declared == Some(wanted) { Some(wanted) } else { tracing::warn!(amendment = a.id, doc = a.doc, %group, "quorum amendment: the seed does not give these quorums, nothing decided"); None } } /// One document, in one transaction: `None` when it was decided at an earlier start. async fn decide( state: &AppState, a: &QuorumAmendment, wanted: &[u32; 5], doc_id: &str, publishes: bool, ) -> anyhow::Result> { let mut tx = state.db.begin().await?; let decided: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM quorum_amendments WHERE amendment_id = ? AND doc_id = ?") .bind(a.id) .bind(doc_id) .fetch_one(&mut *tx) .await?; if decided > 0 { return Ok(None); } let found: Option = sqlx::query_scalar("SELECT quorums FROM documents WHERE id = ?").bind(doc_id).fetch_one(&mut *tx).await?; let v = verdict(found.as_deref(), wanted); let now = util::now(); let to = quorums_json(wanted); if v == Verdict::Apply { sqlx::query("UPDATE documents SET quorums = ? WHERE id = ? AND quorums IS NULL") .bind(&to) .bind(doc_id) .execute(&mut *tx) .await?; if publishes { sqlx::query("INSERT INTO outbox (doc_id, reason, created_at, next_at) VALUES (?, ?, ?, ?)") .bind(doc_id) .bind(a.reason) .bind(&now) .bind(&now) .execute(&mut *tx) .await?; } } let (outcome, reason) = match v { Verdict::Apply => ("applied", None), Verdict::Present => ("present", None), Verdict::Refuse(why) => ("refused", Some(why)), }; sqlx::query( "INSERT INTO quorum_amendments (amendment_id, doc_id, from_quorums, to_quorums, outcome, reason, at) VALUES (?, ?, ?, ?, ?, ?, ?)", ) .bind(a.id) .bind(doc_id) .bind(&found) .bind(&to) .bind(outcome) .bind(reason) .bind(&now) .execute(&mut *tx) .await?; tx.commit().await?; Ok(Some(v)) } #[cfg(test)] mod tests { use super::*; const ML: [u32; 5] = [12, 12, 50, 50, 50]; #[test] fn written_where_none_kept_where_other() { assert_eq!(verdict(None, &ML), Verdict::Apply); assert_eq!(verdict(Some("[12,12,50,50,50]"), &ML), Verdict::Present); assert_eq!(verdict(Some("[12, 12, 50, 50, 50]"), &ML), Verdict::Present, "the same numbers"); assert_eq!(verdict(Some("[3,3,3,3,3]"), &ML), Verdict::Refuse("quorums_set")); assert_eq!(verdict(Some("not json"), &ML), Verdict::Refuse("quorums_set")); } #[test] fn the_amendment_says_what_the_seed_says() { let seed: serde_json::Value = serde_json::from_str(include_str!("../../../seeds/acte-engagement-certification.json")).expect("seed"); for (group, quorums) in AMENDMENTS[0].quorums { let inst = seed["instances"].as_array().expect("instances").iter().find(|i| i["group"] == *group); assert_eq!(inst.map(|i| i["quorums"].clone()), Some(serde_json::json!(quorums)), "{group}"); } } }