v3 : F1.2, N1, N1.1 et O1 réécrits selon les règles appliquées (Yvv, 02/11)
ci/woodpecker/push/app Pipeline was successful
ci/woodpecker/push/maquette Pipeline was successful

- Le seed porte les nouveaux textes (fr, en, es), ceux du brouillon relu
  (textes-regles-seed.md), pour les bases neuves.
- L'amendement de seed vise aussi une proposition ouverte du seed, par son
  étiquette (Version::Open) ; mêmes gardes : rien d'une personne, 0 vote,
  révision 1, empreinte attendue. Quatre amendements, de l'empreinte du seed du
  21/10 (inchangée depuis 67ddee2) à celle d'aujourd'hui.
- Tests vus rouges : remplacement une fois, un vote garde le texte, republication
  de Monnaie libre seule, base neuve « present ».

Boucle : 237 tests Rust, svelte-check 0, 55 vitest, e2e 35 / 35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-02 01:56:34 +01:00
co-authored by Claude Opus 5.5
parent 0919b467c9
commit 3f10a0e860
8 changed files with 405 additions and 42 deletions
+117 -21
View File
@@ -1,5 +1,10 @@
//! Seed amendments — a starting text replaced by a newer text of the seed.
//!
//! The text replaced is an article's version in force (a seed a minima), or, since
//! 2026-11-02, one of its open proposals from the seed, named by its label: the
//! rule articles F1.2, N1, N1.1 and O1 rewritten after the rules applied (Yvv).
//! The guards are the same.
//!
//! A seeded document is never instantiated again: once in the database, it is
//! runtime data. When the seed's text of an article changes (Yvv, 2026-10-30:
//! the Préambule becomes « Raison d'être »), new bases get it from the seed;
@@ -25,6 +30,24 @@ use crate::seed::{SeedDoc, SeedVersion};
use crate::util;
use sqlx::SqliteConnection;
/// Which version of the article an amendment replaces.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Version {
/// Its text in force, a seed a minima.
InForce,
/// An open proposal of the seed, at zero votes, by its label.
Open(&'static str),
}
impl Version {
fn status(self) -> &'static str {
match self {
Version::InForce => "in_force",
Version::Open(_) => "open",
}
}
}
/// One starting text to replace, in every document instantiated from a seed.
pub struct Amendment {
/// Stable identifier: each document's decision is kept under it.
@@ -33,6 +56,8 @@ pub struct Amendment {
pub doc: &'static str,
/// Code of the article.
pub unit: &'static str,
/// The version replaced.
pub version: Version,
/// Fingerprint of the starting text expected in force (source language).
pub from: &'static str,
/// Fingerprint of the new text: the version in force of the seed today, which
@@ -42,14 +67,54 @@ pub struct Amendment {
pub reason: &'static str,
}
pub const AMENDMENTS: &[Amendment] = &[Amendment {
id: "2026-10-30-i1-raison-d-etre",
doc: "acte-engagement-certification",
unit: "I1",
from: "ac17693c61b678ae48f5386f3d528a78385a62f3e2bb12a802a21f45c762b375",
to: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
reason: "texte de départ remplacé : I1",
}];
pub const AMENDMENTS: &[Amendment] = &[
Amendment {
id: "2026-10-30-i1-raison-d-etre",
doc: "acte-engagement-certification",
unit: "I1",
version: Version::InForce,
from: "ac17693c61b678ae48f5386f3d528a78385a62f3e2bb12a802a21f45c762b375",
to: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
reason: "texte de départ remplacé : I1",
},
// The rule articles, open proposals of the seed, after the rules applied (Yvv, 2026-11-02).
Amendment {
id: "2026-11-02-f1-2-regles",
doc: "acte-engagement-certification",
unit: "F1.2",
version: Version::Open("1.0.0"),
from: "044c65d6b97e4278184cb1acfe6fa2227ddfe91465c717d8a81f52092314fae6",
to: "b24ff4d1c95cfbaae0231885009ee3d600957a3ce498bede80b29bceca651a60",
reason: "proposition de départ réécrite : F1.2",
},
Amendment {
id: "2026-11-02-n1-regles",
doc: "acte-engagement-certification",
unit: "N1",
version: Version::Open("1.0.0"),
from: "eed13441afae8d33eba37545163037ccf89b0801d84072ef55290575a1410142",
to: "c1facdfd0c0f3aaa94b74192e146557558fe88f66304dc6d21279714d7fb727e",
reason: "proposition de départ réécrite : N1",
},
Amendment {
id: "2026-11-02-n1-1-regles",
doc: "acte-engagement-certification",
unit: "N1.1",
version: Version::Open("1.0.0"),
from: "d5d06c5ae5f3a1c1b08e3c3721b2b09c3dcfd528c11d16d3368d6e14d8942bee",
to: "865ed24d5eb18745a8c35dc727247219c449e078d39b6a18ec2fdcfd4f8289a0",
reason: "proposition de départ réécrite : N1.1",
},
Amendment {
id: "2026-11-02-o1-regles",
doc: "acte-engagement-certification",
unit: "O1",
version: Version::Open("1.0.0"),
from: "40ab4c06a9e374bded6023b9dffbee5736fc4000d422c5a819f35cdb00b02642",
to: "4173738055af10fceb74bb6e954bdfefd7ab364480d49afbc92ea5f68825c537",
reason: "proposition de départ réécrite : O1",
},
];
/// The fingerprint of a text: the hash translations are pinned to.
pub fn fingerprint(title: &str, body: &str) -> String {
@@ -88,7 +153,12 @@ pub enum Verdict {
/// The rule, apart from the database: any trace of a person keeps the text.
pub fn verdict(facts: Option<&Facts>, a: &Amendment) -> Verdict {
let Some(f) = facts else { return Verdict::Refuse("no_text_in_force") };
let Some(f) = facts else {
return Verdict::Refuse(match a.version {
Version::InForce => "no_text_in_force",
Version::Open(_) => "no_open_version",
});
};
if f.fingerprint == a.to {
return Verdict::Present;
}
@@ -160,8 +230,12 @@ fn target_of<'a>(seeds: &'a [SeedDoc], a: &Amendment) -> Option<(&'a SeedDoc, &'
tracing::error!(amendment = a.id, doc = a.doc, "seed amendment: no such seed document, nothing decided");
return None;
};
let version =
seed.units.iter().find(|u| u.code == a.unit).and_then(|u| u.versions.iter().find(|v| v.status == "in_force"));
let version = seed.units.iter().find(|u| u.code == a.unit).and_then(|u| {
u.versions.iter().find(|v| match a.version {
Version::InForce => v.status == "in_force",
Version::Open(label) => v.status == "open" && v.label.as_deref() == Some(label),
})
});
let src = seed.source_lang.as_str();
let text = version.and_then(|v| Some((v.title.get(src)?, v.body.get(src)?)));
match (version, text) {
@@ -178,7 +252,7 @@ fn target_of<'a>(seeds: &'a [SeedDoc], a: &Amendment) -> Option<(&'a SeedDoc, &'
}
}
async fn facts(conn: &mut SqliteConnection, proposal_id: &str) -> anyhow::Result<Option<Facts>> {
async fn facts(conn: &mut SqliteConnection, proposal_id: &str, status: &str) -> anyhow::Result<Option<Facts>> {
#[derive(sqlx::FromRow)]
struct Row {
origin: String,
@@ -189,9 +263,10 @@ async fn facts(conn: &mut SqliteConnection, proposal_id: &str) -> anyhow::Result
body: String,
}
let Some(p) = sqlx::query_as::<_, Row>(
"SELECT origin, entered_at, confirmed_at, revision, title, body FROM proposals WHERE id = ? AND status = 'in_force'",
"SELECT origin, entered_at, confirmed_at, revision, title, body FROM proposals WHERE id = ? AND status = ?",
)
.bind(proposal_id)
.bind(status)
.fetch_optional(&mut *conn)
.await?
else {
@@ -252,33 +327,49 @@ async fn decide(
if decided > 0 {
return Ok(None);
}
let in_force: Option<String> =
sqlx::query_scalar::<_, Option<String>>("SELECT in_force_id FROM units WHERE doc_id = ? AND code = ?")
let target_id: Option<String> = match a.version {
Version::InForce => {
sqlx::query_scalar::<_, Option<String>>("SELECT in_force_id FROM units WHERE doc_id = ? AND code = ?")
.bind(doc_id)
.bind(a.unit)
.fetch_optional(&mut *tx)
.await?
.flatten()
}
Version::Open(label) => {
sqlx::query_scalar(
"SELECT p.id FROM proposals p JOIN units u ON u.id = p.unit_id
WHERE u.doc_id = ? AND u.code = ? AND p.status = 'open' AND p.origin = 'seed' AND p.label = ?
ORDER BY p.rowid LIMIT 1",
)
.bind(doc_id)
.bind(a.unit)
.bind(label)
.fetch_optional(&mut *tx)
.await?
.flatten();
let found = match &in_force {
Some(id) => facts(&mut tx, id).await?,
}
};
let found = match &target_id {
Some(id) => facts(&mut tx, id, a.version.status()).await?,
None => None,
};
let mut v = verdict(found.as_ref(), a);
let now = util::now();
if let (Verdict::Apply, Some(pid)) = (v, &in_force) {
if let (Verdict::Apply, Some(pid)) = (v, &target_id) {
let src = seed.source_lang.as_str();
let pick = |m: &util::LangMap| m.get(src).cloned().unwrap_or_default();
let (title, body) = (pick(&target.title), pick(&target.body));
// Written only over the text that was judged, still untouched.
let done = sqlx::query(
"UPDATE proposals SET title = ?, body = ?, revision = revision + 1, updated_at = ?
WHERE id = ? AND status = 'in_force' AND origin = 'seed' AND entered_at IS NULL
WHERE id = ? AND status = ? AND origin = 'seed' AND entered_at IS NULL
AND confirmed_at IS NULL AND revision = 1",
)
.bind(&title)
.bind(&body)
.bind(&now)
.bind(pid)
.bind(a.version.status())
.execute(&mut *tx)
.await?;
if done.rows_affected() == 1 {
@@ -335,7 +426,7 @@ async fn decide(
.bind(a.id)
.bind(doc_id)
.bind(a.unit)
.bind(&in_force)
.bind(&target_id)
.bind(outcome)
.bind(reason)
.bind(&now)
@@ -353,6 +444,7 @@ mod tests {
id: "test",
doc: "doc",
unit: "I1",
version: Version::InForce,
from: "old",
to: "new",
reason: "texte de départ remplacé : I1",
@@ -398,5 +490,9 @@ mod tests {
for (facts, reason) in cases {
assert_eq!(verdict(facts.as_ref(), &A), Verdict::Refuse(reason), "{facts:?}");
}
// An open proposal of the seed that is no longer there (withdrawn, never seeded).
let open = Amendment { version: Version::Open("1.0.0"), ..A };
assert_eq!(verdict(None, &open), Verdict::Refuse("no_open_version"));
assert_eq!(verdict(Some(&intact()), &open), Verdict::Apply);
}
}
+2 -2
View File
@@ -936,11 +936,11 @@ async fn restart(before: &AppState, cfg: Config) -> (Client, AppState) {
(Client { router: app(state.clone()), cookie: None, set_cookie: None }, state)
}
/// Each document's decision on the amendment: (group, outcome, reason, at).
/// Each document's decision on the amendment of I1: (group, outcome, reason, at).
async fn decisions(state: &AppState) -> Vec<(String, String, Option<String>, String)> {
sqlx::query_as(
"SELECT d.group_slug, s.outcome, s.reason, s.at FROM seed_amendments s JOIN documents d ON d.id = s.doc_id
ORDER BY d.group_slug",
WHERE s.amendment_id = '2026-10-30-i1-raison-d-etre' ORDER BY d.group_slug",
)
.fetch_all(&state.db)
.await
+57
View File
@@ -0,0 +1,57 @@
{
"_source": "seeds/acte-engagement-certification.json au commit 70aa9ba : les quatre articles des règles tels que la production les tient (seed du 21/10, inchangé)",
"units": {
"F1.2": {
"label": "1.0.0",
"title": {
"fr": "Paramètres par défaut",
"en": "Default parameters",
"es": "Parámetros por defecto"
},
"body": {
"fr": "**Seuil = M + (1-M) × (1 - (T/W)^G)**\n\n- **T** = votes exprimés (pour + contre)\n- **W** = taille de la toile de confiance\n- **M** = majorité cible (50% par défaut)\n- **G** = gradient d'inertie (0.2 par défaut)\n\n**Valeurs par défaut (inertie standard) :**\n- Durée : 30 jours (vote permanent)\n- Majorité cible M = 50%\n- Gradient G = 0.2\n- Base B = 0.1, Constante C = 0\n\n**Lecture du curseur d'inertie :**\nLe curseur affiché sur chaque section représente le niveau G. Plus le curseur est à droite, plus le remplacement est difficile. La courbe associée montre le seuil requis en fonction de la participation.\n\nMode compact : **D30M50B.1G.2**",
"en": "**Seuil = M + (1-M) × (1 - (T/W)^G)**\n\n- **T** = votes cast (for + against)\n- **W** = size of the web of trust\n- **M** = target majority (50% by default)\n- **G** = inertia gradient (0.2 by default)\n\n**Default values (standard inertia):**\n- Duration: 30 days (permanent vote)\n- Target majority M = 50%\n- Gradient G = 0.2\n- Base B = 0.1, Constant C = 0\n\n**Reading the inertia slider:**\nThe slider shown on each section represents the level G. The further to the right the slider, the harder the replacement. The associated curve shows the required threshold as a function of participation.\n\nCompact mode: **D30M50B.1G.2**",
"es": "**Seuil = M + (1-M) × (1 - (T/W)^G)**\n\n- **T** = votos emitidos (a favor + en contra)\n- **W** = tamaño de la red de confianza\n- **M** = mayoría objetivo (50% por defecto)\n- **G** = gradiente de inercia (0.2 por defecto)\n\n**Valores por defecto (inercia estándar):**\n- Duración: 30 días (votación permanente)\n- Mayoría objetivo M = 50%\n- Gradiente G = 0.2\n- Base B = 0.1, Constante C = 0\n\n**Lectura del cursor de inercia:**\nEl cursor que se muestra en cada sección representa el nivel G. Cuanto más a la derecha está el cursor, más difícil es la sustitución. La curva asociada muestra el umbral requerido en función de la participación.\n\nModo compacto: **D30M50B.1G.2**"
}
},
"N1": {
"label": "1.0.0",
"title": {
"fr": "Réglage de l'inertie",
"en": "Inertia setting",
"es": "Ajuste de la inercia"
},
"body": {
"fr": "Le réglage de l'inertie définit la difficulté de remplacement de chaque section du document. Ce réglage est lui-même soumis à l'inertie la plus élevée, pour empêcher la modification des règles de modification.",
"en": "The inertia setting defines how difficult it is to replace each section of the document. This setting is itself subject to the highest inertia, to prevent the rules of modification from being modified.",
"es": "El ajuste de la inercia define la dificultad de sustitución de cada sección del documento. Este ajuste está a su vez sometido a la inercia más elevada, para impedir la modificación de las reglas de modificación."
}
},
"N1.1": {
"label": "1.0.0",
"title": {
"fr": "Niveaux d'inertie",
"en": "Inertia levels",
"es": "Niveles de inercia"
},
"body": {
"fr": "**Formule simplifiée :** `Seuil = M + (1-M) × (1 - (T/W)^G)`\n\nLe **gradient G** contrôle la courbe d'inertie : plus G est élevé, plus le seuil reste haut même avec une participation croissante.\n\n**Basse** — G=0.1, M=50%\n- La courbe descend très vite vers 50%\n- Dès 10% de participation, le seuil est proche de la majorité simple\n- *Usage : annexes, recommandations, conseils*\n\n**Standard** — G=0.2, M=50%\n- Descente progressive, équilibre entre stabilité et réactivité\n- À 25% de participation, le seuil avoisine 65%\n- *Usage : engagements fondamentaux et techniques*\n\n**Haute** — G=0.4, M=60%\n- Courbe résistante, le seuil reste élevé longtemps\n- Super-majorité de 60% même à pleine participation\n- *Usage : formule de vote elle-même*\n\n**Très haute** — G=0.6, M=66%\n- Quasi-unanimité requise à tout niveau de participation\n- Seul un consensus massif (66%+) peut modifier ce réglage\n- *Usage : réglage de l'inertie (méta-protection)*\n\nLe curseur d'inertie associé à chaque section du document reflète visuellement ces niveaux.",
"en": "**Simplified formula:** `Seuil = M + (1-M) × (1 - (T/W)^G)`\n\nThe **gradient G** controls the inertia curve: the higher G is, the higher the threshold remains, even as participation increases.\n\n**Low** — G=0.1, M=50%\n- The curve drops very quickly towards 50%\n- From 10% participation, the threshold is close to a simple majority\n- *Use: annexes, recommendations, advice*\n\n**Standard** — G=0.2, M=50%\n- Gradual descent, balance between stability and responsiveness\n- At 25% participation, the threshold is around 65%\n- *Use: fundamental and technical commitments*\n\n**High** — G=0.4, M=60%\n- Resistant curve, the threshold stays high for a long time\n- Supermajority of 60% even at full participation\n- *Use: the voting formula itself*\n\n**Very high** — G=0.6, M=66%\n- Near-unanimity required at every level of participation\n- Only a massive consensus (66%+) can modify this setting\n- *Use: the inertia setting (meta-protection)*\n\nThe inertia slider associated with each section of the document visually reflects these levels.",
"es": "**Fórmula simplificada:** `Seuil = M + (1-M) × (1 - (T/W)^G)`\n\nEl **gradiente G** controla la curva de inercia: cuanto más alto es G, más alto se mantiene el umbral, incluso con una participación creciente.\n\n**Baja** — G=0.1, M=50%\n- La curva desciende muy rápido hacia el 50%\n- A partir del 10% de participación, el umbral se acerca a la mayoría simple\n- *Uso: anexos, recomendaciones, consejos*\n\n**Estándar** — G=0.2, M=50%\n- Descenso progresivo, equilibrio entre estabilidad y reactividad\n- Con un 25% de participación, el umbral ronda el 65%\n- *Uso: compromisos fundamentales y técnicos*\n\n**Alta** — G=0.4, M=60%\n- Curva resistente, el umbral se mantiene elevado durante mucho tiempo\n- Supermayoría del 60% incluso con participación plena\n- *Uso: la propia fórmula de votación*\n\n**Muy alta** — G=0.6, M=66%\n- Se requiere casi unanimidad con cualquier nivel de participación\n- Solo un consenso masivo (66%+) puede modificar este ajuste\n- *Uso: el ajuste de la inercia (metaprotección)*\n\nEl cursor de inercia asociado a cada sección del documento refleja visualmente estos niveles."
}
},
"O1": {
"label": "1.0.0",
"title": {
"fr": "Ordonnancement du document",
"en": "Ordering of the document",
"es": "Ordenación del documento"
},
"body": {
"fr": "L'ordre de présentation des items dans le document est lui-même soumis au vote. Toute proposition de réorganisation doit atteindre le seuil d'adoption avec l'inertie standard.",
"en": "The order in which items are presented in the document is itself subject to vote. Any reorganisation proposal must reach the adoption threshold with standard inertia.",
"es": "El orden de presentación de los elementos en el documento está a su vez sometido a votación. Toda propuesta de reorganización debe alcanzar el umbral de adopción con la inercia estándar."
}
}
}
}
+207
View File
@@ -0,0 +1,207 @@
//! The four rule articles of the seed rewritten after the rules applied (Yvv,
//! 2026-11-02): F1.2, N1, N1.1 and O1 are open proposals at zero votes, not texts
//! in force; a guarded seed amendment replaces them where nobody touched them.
use ld_server::seed_amend::{self, AMENDMENTS, Version};
use ld_server::{AppState, Config, open_db, seed};
use serde_json::Value;
const CODES: [&str; 4] = ["F1.2", "N1", "N1.1", "O1"];
fn read(path: std::path::PathBuf) -> Value {
serde_json::from_str(&std::fs::read_to_string(path).expect("read")).expect("json")
}
/// The four articles as production holds them (the seed of 21/10, unchanged until 02/11).
fn before() -> Value {
read(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-regles-avant.json"))
}
fn today_seed() -> Value {
read(Config::for_tests().seed_dir.join("acte-engagement-certification.json"))
}
fn version<'a>(seed: &'a Value, code: &str) -> &'a Value {
let u = seed["units"].as_array().expect("units").iter().find(|u| u["code"] == code).expect("unit");
&u["versions"][0]
}
fn fr(v: &Value) -> (&str, &str) {
(v["title"]["fr"].as_str().expect("title"), v["body"]["fr"].as_str().expect("body"))
}
/// Today's seed with the four rule articles of before.
fn seed_dir_before(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("ld-seed-rules-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("dir");
let mut seed = today_seed();
let old = before();
for u in seed["units"].as_array_mut().expect("units") {
let code = u["code"].as_str().expect("code").to_owned();
if let Some(o) = old["units"].get(&code) {
u["versions"][0]["title"] = o["title"].clone();
u["versions"][0]["body"] = o["body"].clone();
}
}
std::fs::write(dir.join("acte-engagement-certification.json"), seed.to_string()).expect("write");
dir
}
/// A base seeded with the rule articles of before, as production was, by the code of
/// the time (no amendment).
async fn base_before(tag: &str) -> AppState {
let mut cfg = Config::for_tests();
cfg.seed_dir = seed_dir_before(tag);
let db = open_db("sqlite::memory:").await.expect("db");
let state = AppState::new(cfg, db).await.expect("state");
seed::documents(&state).await.expect("seed of before");
state
}
/// The same base, started again with today's seed and its amendments.
async fn restart(before: &AppState) -> AppState {
let state = AppState::new(Config::for_tests(), before.db.clone()).await.expect("state");
seed::ensure(&state).await.expect("seed");
state
}
async fn proposal(state: &AppState, group: &str, code: &str) -> (String, String, String, i64) {
sqlx::query_as(
"SELECT p.id, p.title, p.body, p.revision FROM proposals p JOIN units u ON u.id = p.unit_id
JOIN documents d ON d.id = u.doc_id
WHERE d.group_slug = ? AND u.code = ? AND p.status = 'open' AND p.origin = 'seed' AND p.label = '1.0.0'",
)
.bind(group)
.bind(code)
.fetch_one(&state.db)
.await
.expect("seed proposal")
}
/// Each decision on the four rule amendments: (group, unit, outcome, reason).
async fn decisions(state: &AppState) -> Vec<(String, String, String, Option<String>)> {
sqlx::query_as(
"SELECT d.group_slug, s.unit_code, s.outcome, s.reason FROM seed_amendments s
JOIN documents d ON d.id = s.doc_id
WHERE s.amendment_id LIKE '2026-11-02-%' ORDER BY d.group_slug, s.unit_code",
)
.fetch_all(&state.db)
.await
.expect("decisions")
}
#[test]
fn the_shipped_amendments_go_from_the_texts_of_before_to_the_seed_of_today() {
let (old, today) = (before(), today_seed());
for code in CODES {
let a = AMENDMENTS.iter().find(|a| a.unit == code).unwrap_or_else(|| panic!("no amendment for {code}"));
assert_eq!(a.doc, "acte-engagement-certification");
assert_eq!(a.version, Version::Open("1.0.0"), "{code}: the open proposal of the seed");
let (t, b) = fr(&old["units"][code]);
assert_eq!(seed_amend::fingerprint(t, b), a.from, "{code}: the text production holds");
let v = version(&today, code);
assert_eq!((v["status"].as_str(), v["label"].as_str()), (Some("open"), Some("1.0.0")), "{code}");
let (t, b) = fr(v);
assert_eq!(seed_amend::fingerprint(t, b), a.to, "{code}: the text of the seed today");
assert_ne!(a.from, a.to, "{code}");
}
}
#[tokio::test]
async fn untouched_rule_articles_are_replaced_once_and_a_vote_keeps_its_text() {
let base = base_before("replace").await;
// In the test group someone voted on N1: there it keeps its text.
let (n1, ..) = proposal(&base, "essai", "N1").await;
sqlx::query("INSERT INTO identities (id, kind, display_name, created_at) VALUES ('dev:vera', 'dev', 'Vera', '2026-11-01T00:00:00Z')")
.execute(&base.db)
.await
.expect("identity");
sqlx::query("INSERT INTO votes (proposal_id, identity_id, choice, revision, cast_at) VALUES (?, 'dev:vera', 'against', 1, '2026-11-01T00:00:00Z')")
.bind(&n1)
.execute(&base.db)
.await
.expect("vote");
let state = restart(&base).await;
let (old, today) = (before(), today_seed());
for group in ["essai", "monnaie-libre"] {
for code in CODES {
let (id, title, body, revision) = proposal(&state, group, code).await;
let kept = group == "essai" && code == "N1";
let expected = if kept { fr(&old["units"][code]) } else { fr(version(&today, code)) };
assert_eq!((title.as_str(), body.as_str()), expected, "{group} {code}");
assert_eq!(revision, if kept { 1 } else { 2 }, "{group} {code}");
if kept {
continue;
}
let kinds: Vec<String> = sqlx::query_scalar("SELECT kind FROM revisions WHERE proposal_id = ? ORDER BY id")
.bind(&id)
.fetch_all(&state.db)
.await
.expect("revisions");
assert_eq!(kinds, ["initial", "amendment"], "{group} {code}");
for lang in ["en", "es"] {
let (t, b, hash): (String, String, String) = sqlx::query_as(
"SELECT title, body, source_hash FROM translations WHERE proposal_id = ? AND lang = ?",
)
.bind(&id)
.bind(lang)
.fetch_one(&state.db)
.await
.expect("translation");
let v = version(&today, code);
assert_eq!(
(t.as_str(), b.as_str()),
(v["title"][lang].as_str().expect("t"), v["body"][lang].as_str().expect("b"))
);
assert_eq!(hash, seed_amend::fingerprint(&title, &body), "{group} {code} {lang}: up to date");
}
}
}
let expected: Vec<(String, String, String, Option<String>)> = ["essai", "monnaie-libre"]
.iter()
.flat_map(|g| {
CODES.iter().map(move |c| {
let kept = *g == "essai" && *c == "N1";
(
(*g).to_owned(),
(*c).to_owned(),
if kept { "refused" } else { "applied" }.to_owned(),
kept.then(|| "votes".to_owned()),
)
})
})
.collect();
assert_eq!(decisions(&state).await, expected);
// Monnaie libre republishes, once per article; the test group never.
let queued: Vec<(String, String)> = sqlx::query_as(
"SELECT d.group_slug, o.reason FROM outbox o JOIN documents d ON d.id = o.doc_id
WHERE o.reason LIKE '%F1.2%' OR o.reason LIKE '%N1%' OR o.reason LIKE '%O1%' ORDER BY o.id",
)
.fetch_all(&state.db)
.await
.expect("outbox");
assert_eq!(queued.len(), 4, "{queued:?}");
assert!(queued.iter().all(|(g, _)| g == "monnaie-libre"), "{queued:?}");
// Decided once: a second start changes nothing.
let again = restart(&state).await;
assert_eq!(decisions(&again).await, expected);
let (_, _, _, revision) = proposal(&again, "monnaie-libre", "N1.1").await;
assert_eq!(revision, 2);
}
#[tokio::test]
async fn a_new_base_is_born_with_the_rules_of_today() {
let db = open_db("sqlite::memory:").await.expect("db");
let state = AppState::new(Config::for_tests(), db).await.expect("state");
seed::ensure(&state).await.expect("seed");
let today = today_seed();
for group in ["essai", "monnaie-libre"] {
for code in CODES {
let (_, title, body, revision) = proposal(&state, group, code).await;
assert_eq!(((title.as_str(), body.as_str()), revision), (fr(version(&today, code)), 1), "{group} {code}");
}
}
assert!(decisions(&state).await.iter().all(|(_, _, o, _)| o == "present"));
assert_eq!(decisions(&state).await.len(), 8);
}
+4 -2
View File
@@ -147,5 +147,7 @@ les lettres d'aucun cran.
fois par jour, un texte peut passer ou ne plus passer sans nouveau vote sur lui (comme quand W
change). Chaque entrée en vigueur garde la médiane qui l'a jugée.
- Le document publié (`document.json`) porte l'inertie en nombre (1 à 5), et non plus en mot.
- Le texte N1.1 du seed (« Niveaux d'inertie », quatre niveaux) est une proposition de la
communauté : il n'est pas modifié et décrit désormais autre chose que la règle en place.
- Le texte N1.1 du seed (« Niveaux d'inertie », quatre niveaux) décrivait autre chose que la règle
en place. Le 2026-11-02, Yvv a fait réécrire F1.2, N1, N1.1 et O1 selon les règles appliquées
(`docs/chantiers/acte-certification/textes-regles-seed.md`) ; un amendement de seed gardé remplace
ces propositions ouvertes là où personne n'y a touché (`seed_amend`, `Version::Open`).
@@ -369,10 +369,10 @@ Les raisons longues sont dans `docs/adr/`.
## Questions ouvertes (à Yvv)
- **Licence** du code — non choisie.
- **Texte « Paramètres par défaut » du seed** (F1.2) : il annonce C = 0 et une durée de 30 jours,
alors que le vote est permanent avec C = 50. À reformuler par proposition (c'est leur rôle),
ou à corriger dans le seed avant la première publication si c'est une erreur de transcription.
- ~~Licence du code~~ — AGPL-3.0 (Yvv, 2026-11-02).
- ~~Texte « Paramètres par défaut » du seed (F1.2)~~ — réécrit avec N1, N1.1 et O1 selon les règles
appliquées (Yvv, 2026-11-02) ; un amendement de seed gardé remplace ces propositions ouvertes à
0 vote, et Yvv relit les textes en ligne.
- Anglais : l'interface est en anglais américain, le seed en anglais britannique.
## Ce qui reste après la livraison
@@ -82,3 +82,4 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa
| 2026-10-31 | automate | local, fusion de la tête du chantier (`924a40d` : B2, F) dans le lot D ; C25 et C26 supportent un groupe d'essai où une voix suffit | C1–C33, M1 | 34 / 34 verts (35 tests) |
| 2026-10-31 | automate | local, `033b331` et la relecture de `cd7e683` (coquille sur un texte qui vient d'entrer, objection face à un texte retiré, objection en double, retrait qui relance l'adoption) | C1–C33, M1 | 34 / 34 verts (35 tests) |
| 2026-11-02 | automate | local, lot G sur `5d1f865` (décision d'Yvv du 01/11 : sur une version retenue, « contre » et retrait, jamais « pour » ; C28 ajusté : « Contre » et le retrait sous l'article, le panier) | C1–C33, M1 | 34 / 34 verts (35 tests) |
| 2026-11-02 | automate | local, lot G fusionné (`0919b46`) et les articles des règles réécrits (amendement de seed sur une proposition ouverte) | C1–C33, M1 | 34 / 34 verts (35 tests) |
+13 -13
View File
@@ -1,5 +1,5 @@
{
"_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). En vigueur au départ : I1, E1, E2 (seed a minima, aucun vote). Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21). Inertie 3 / 5 partout, 4 pour E1 et E2 (revue du 2026-10-30). I1 « Raison d'être » remplace le Préambule (texte d'Yvv, revue du 2026-10-30). Modules : le titre du document à 4, l'ordre des articles de chaque section à 2 (revue du 2026-10-30, ADR 0009).",
"_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). En vigueur au départ : I1, E1, E2 (seed a minima, aucun vote). Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21). Inertie 3 / 5 partout, 4 pour E1 et E2 (revue du 2026-10-30). I1 « Raison d'être » remplace le Préambule (texte d'Yvv, revue du 2026-10-30). Modules : le titre du document à 4, l'ordre des articles de chaque section à 2 (revue du 2026-10-30, ADR 0009). F1.2, N1, N1.1, O1 réécrits selon les règles appliquées (Yvv, 2026-11-02).",
"slug": "acte-engagement-certification",
"title": {
"fr": "Acte d'engagement Certification",
@@ -692,9 +692,9 @@
"es": "Parámetros por defecto"
},
"body": {
"fr": "**Seuil = M + (1-M) × (1 - (T/W)^G)**\n\n- **T** = votes exprimés (pour + contre)\n- **W** = taille de la toile de confiance\n- **M** = majorité cible (50% par défaut)\n- **G** = gradient d'inertie (0.2 par défaut)\n\n**Valeurs par défaut (inertie standard) :**\n- Durée : 30 jours (vote permanent)\n- Majorité cible M = 50%\n- Gradient G = 0.2\n- Base B = 0.1, Constante C = 0\n\n**Lecture du curseur d'inertie :**\nLe curseur affiché sur chaque section représente le niveau G. Plus le curseur est à droite, plus le remplacement est difficile. La courbe associée montre le seuil requis en fonction de la participation.\n\nMode compact : **D30M50B.1G.2**",
"en": "**Seuil = M + (1-M) × (1 - (T/W)^G)**\n\n- **T** = votes cast (for + against)\n- **W** = size of the web of trust\n- **M** = target majority (50% by default)\n- **G** = inertia gradient (0.2 by default)\n\n**Default values (standard inertia):**\n- Duration: 30 days (permanent vote)\n- Target majority M = 50%\n- Gradient G = 0.2\n- Base B = 0.1, Constant C = 0\n\n**Reading the inertia slider:**\nThe slider shown on each section represents the level G. The further to the right the slider, the harder the replacement. The associated curve shows the required threshold as a function of participation.\n\nCompact mode: **D30M50B.1G.2**",
"es": "**Seuil = M + (1-M) × (1 - (T/W)^G)**\n\n- **T** = votos emitidos (a favor + en contra)\n- **W** = tamaño de la red de confianza\n- **M** = mayoría objetivo (50% por defecto)\n- **G** = gradiente de inercia (0.2 por defecto)\n\n**Valores por defecto (inercia estándar):**\n- Duración: 30 días (votación permanente)\n- Mayoría objetivo M = 50%\n- Gradiente G = 0.2\n- Base B = 0.1, Constante C = 0\n\n**Lectura del cursor de inercia:**\nEl cursor que se muestra en cada sección representa el nivel G. Cuanto más a la derecha está el cursor, más difícil es la sustitución. La curva asociada muestra el umbral requerido en función de la participación.\n\nModo compacto: **D30M50B.1G.2**"
"fr": "Les valeurs des lettres de la formule (voir « Formule du seuil WoT ») et la façon dont le vote se déroule.\n\n**Un vote permanent, sans durée**\n\n- Chaque version se vote à tout moment, sans échéance. Chacun peut changer ou retirer son vote : le dernier fait foi.\n- Une proposition entre en vigueur dès qu'elle atteint son seuil. S'il y a déjà un texte en vigueur, il lui faut en plus une marge (voix pour moins voix exigées) plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir.\n- Si plusieurs propositions y parviennent, celle qui a la marge la plus large entre en vigueur.\n\n**Les lettres**\n\n- **W** : le nombre de membres de la toile de confiance Ğ1, relu sur la chaîne toutes les 6 heures.\n- **T** : les votes exprimés sur la version, pour et contre.\n- **C = 50** : le nombre minimal de votes « pour » dans Monnaie libre. Sous 50 voix pour, rien ne passe : une seule voix ne fait jamais passer un texte.\n- **B = 0,1** : dans la toile de confiance, `B^W` est si petit qu'il ne compte pas.\n- **M** et **G** : donnés par le cran d'inertie de l'article (voir « Niveaux d'inertie »). G n'est pas fixé à la main : il se déduit de la participation habituelle du groupe.\n\n**Par défaut**\n\n- Le cran par défaut est le 3 (moyenne) : M = 55 %, et le groupe demande 75 % de « pour » au-delà de C quand il vote comme d'habitude. Tout nouvel article commence à ce cran.\n- Au départ, Unicité du compte, Réciprocité et le titre du document sont au cran 4 (haute), l'ordre des articles de chaque section au cran 2 (basse), tous les autres articles au cran 3.",
"en": "The values of the letters of the formula (see “WoT threshold formula”) and how the vote runs.\n\n**A permanent vote, with no duration**\n\n- Every version can be voted on at any time, with no deadline. Anyone can change or withdraw their vote: the last one is the one that counts.\n- A proposal comes into force as soon as it reaches its threshold. If a text is already in force, it also needs a wider margin (votes in favour minus votes required) than that text has. The replaced text becomes a proposal again, with its votes: it can come back.\n- If several proposals get there, the one with the widest margin comes into force.\n\n**The letters**\n\n- **W**: the number of members of the Ğ1 web of trust, read again from the chain every 6 hours.\n- **T**: the votes cast on the version, for and against.\n- **C = 50**: the minimum number of votes in favour in Monnaie libre. Below 50 votes in favour, nothing passes: a single vote never passes a text.\n- **B = 0.1**: in the web of trust, `B^W` is so small that it does not count.\n- **M** and **G**: given by the article's level of inertia (see “Inertia levels”). G is not set by hand: it follows from the group's usual turnout.\n\n**By default**\n\n- The default level is 3 (medium): M = 55%, and the group asks for 75% of votes in favour beyond C when it votes as usual. Every new article starts at this level.\n- At the start, Uniqueness of the account, Reciprocity and the title of the document are at level 4 (high), the order of the articles of each section at level 2 (low), all other articles at level 3.",
"es": "Los valores de las letras de la fórmula (ver «Fórmula del umbral WoT») y cómo se desarrolla la votación.\n\n**Una votación permanente, sin duración**\n\n- Cada versión se vota en cualquier momento, sin plazo. Cada cual puede cambiar o retirar su voto: el último es el que cuenta.\n- Una propuesta entra en vigor en cuanto alcanza su umbral. Si ya hay un texto en vigor, necesita además un margen (votos a favor menos votos exigidos) más amplio que el de ese texto. El texto sustituido vuelve a ser una propuesta, con sus votos: puede volver.\n- Si varias propuestas lo consiguen, entra en vigor la que tiene el margen más amplio.\n\n**Las letras**\n\n- **W**: el número de miembros de la red de confianza Ğ1, leído de nuevo en la cadena cada 6 horas.\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **C = 50**: el número mínimo de votos a favor en Monnaie libre. Con menos de 50 votos a favor, nada pasa: un solo voto nunca hace pasar un texto.\n- **B = 0,1**: en la red de confianza, `B^W` es tan pequeño que no cuenta.\n- **M** y **G**: los da el nivel de inercia del artículo (ver «Niveles de inercia»). G no se fija a mano: se deduce de la participación habitual del grupo.\n\n**Por defecto**\n\n- El nivel por defecto es el 3 (media): M = 55 %, y el grupo pide un 75 % de votos a favor más allá de C cuando vota como de costumbre. Todo artículo nuevo empieza en este nivel.\n- Al inicio, Unicidad de la cuenta, Reciprocidad y el título del documento están en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2 (baja), todos los demás artículos en el nivel 3."
}
}
]
@@ -736,9 +736,9 @@
"es": "Ajuste de la inercia"
},
"body": {
"fr": "Le réglage de l'inertie définit la difficulté de remplacement de chaque section du document. Ce réglage est lui-même soumis à l'inertie la plus élevée, pour empêcher la modification des règles de modification.",
"en": "The inertia setting defines how difficult it is to replace each section of the document. This setting is itself subject to the highest inertia, to prevent the rules of modification from being modified.",
"es": "El ajuste de la inercia define la dificultad de sustitución de cada sección del documento. Este ajuste está a su vez sometido a la inercia más elevada, para impedir la modificación de las reglas de modificación."
"fr": "Chaque article porte son inertie : un cran de 1 à 5 (voir « Niveaux d'inertie »). Plus le cran est haut, plus il faut de voix pour changer le texte quand peu de gens votent. Le titre du document et l'ordre des articles de chaque section ont aussi la leur, qui se change de la même façon.\n\nTout membre du groupe peut proposer un autre cran. Cette proposition se vote au cran actuel de l'article, comme son texte : l'inertie de l'inertie hérite du cran de l'article. Ainsi, un article au cran 4 ne descend qu'avec les voix que demande le cran 4.",
"en": "Every article carries its inertia: a level from 1 to 5 (see “Inertia levels”). The higher the level, the more votes it takes to change the text when few people vote. The title of the document and the order of the articles of each section have theirs too, changed in the same way.\n\nAny member of the group can propose another level. That proposal is voted at the article's current level, like its text: the inertia of the inertia inherits the article's level. So an article at level 4 only comes down with the votes that level 4 asks for.",
"es": "Cada artículo lleva su inercia: un nivel del 1 al 5 (ver «Niveles de inercia»). Cuanto más alto es el nivel, más votos hacen falta para cambiar el texto cuando vota poca gente. El título del documento y el orden de los artículos de cada sección también tienen la suya, que se cambia del mismo modo.\n\nCualquier miembro del grupo puede proponer otro nivel. Esa propuesta se vota con el nivel actual del artículo, como su texto: la inercia de la inercia hereda el nivel del artículo. Así, un artículo en el nivel 4 solo baja con los votos que pide el nivel 4."
}
}
]
@@ -758,9 +758,9 @@
"es": "Niveles de inercia"
},
"body": {
"fr": "**Formule simplifiée :** `Seuil = M + (1-M) × (1 - (T/W)^G)`\n\nLe **gradient G** contrôle la courbe d'inertie : plus G est élevé, plus le seuil reste haut même avec une participation croissante.\n\n**Basse** — G=0.1, M=50%\n- La courbe descend très vite vers 50%\n- Dès 10% de participation, le seuil est proche de la majorité simple\n- *Usage : annexes, recommandations, conseils*\n\n**Standard** — G=0.2, M=50%\n- Descente progressive, équilibre entre stabilité et réactivité\n- À 25% de participation, le seuil avoisine 65%\n- *Usage : engagements fondamentaux et techniques*\n\n**Haute** — G=0.4, M=60%\n- Courbe résistante, le seuil reste élevé longtemps\n- Super-majorité de 60% même à pleine participation\n- *Usage : formule de vote elle-même*\n\n**Très haute** — G=0.6, M=66%\n- Quasi-unanimité requise à tout niveau de participation\n- Seul un consensus massif (66%+) peut modifier ce réglage\n- *Usage : réglage de l'inertie (méta-protection)*\n\nLe curseur d'inertie associé à chaque section du document reflète visuellement ces niveaux.",
"en": "**Simplified formula:** `Seuil = M + (1-M) × (1 - (T/W)^G)`\n\nThe **gradient G** controls the inertia curve: the higher G is, the higher the threshold remains, even as participation increases.\n\n**Low** — G=0.1, M=50%\n- The curve drops very quickly towards 50%\n- From 10% participation, the threshold is close to a simple majority\n- *Use: annexes, recommendations, advice*\n\n**Standard** — G=0.2, M=50%\n- Gradual descent, balance between stability and responsiveness\n- At 25% participation, the threshold is around 65%\n- *Use: fundamental and technical commitments*\n\n**High** — G=0.4, M=60%\n- Resistant curve, the threshold stays high for a long time\n- Supermajority of 60% even at full participation\n- *Use: the voting formula itself*\n\n**Very high** — G=0.6, M=66%\n- Near-unanimity required at every level of participation\n- Only a massive consensus (66%+) can modify this setting\n- *Use: the inertia setting (meta-protection)*\n\nThe inertia slider associated with each section of the document visually reflects these levels.",
"es": "**Fórmula simplificada:** `Seuil = M + (1-M) × (1 - (T/W)^G)`\n\nEl **gradiente G** controla la curva de inercia: cuanto más alto es G, más alto se mantiene el umbral, incluso con una participación creciente.\n\n**Baja** — G=0.1, M=50%\n- La curva desciende muy rápido hacia el 50%\n- A partir del 10% de participación, el umbral se acerca a la mayoría simple\n- *Uso: anexos, recomendaciones, consejos*\n\n**Estándar** — G=0.2, M=50%\n- Descenso progresivo, equilibrio entre estabilidad y reactividad\n- Con un 25% de participación, el umbral ronda el 65%\n- *Uso: compromisos fundamentales y técnicos*\n\n**Alta** — G=0.4, M=60%\n- Curva resistente, el umbral se mantiene elevado durante mucho tiempo\n- Supermayoría del 60% incluso con participación plena\n- *Uso: la propia fórmula de votación*\n\n**Muy alta** — G=0.6, M=66%\n- Se requiere casi unanimidad con cualquier nivel de participación\n- Solo un consenso masivo (66%+) puede modificar este ajuste\n- *Uso: el ajuste de la inercia (metaprotección)*\n\nEl cursor de inercia asociado a cada sección del documento refleja visualmente estos niveles."
"fr": "À chaque cran, le groupe demande d'abord C voix pour, puis une part de « pour » parmi les votes au-delà de C. Cette part dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.\n\n`Part exigée = M + (1 − M) × (1 − (T/W)^G)`\n\n**Les cinq crans** : la part exigée quand le groupe vote comme d'habitude, puis quand tout le monde vote, et ce qui est à ce cran au départ.\n\n- **Cran 1 · très basse** : 60 % comme d'habitude, 50 % quand tout le monde vote. Aucun article au départ.\n- **Cran 2 · basse** : 67 % comme d'habitude, 50 % quand tout le monde vote. L'ordre des articles de chaque section.\n- **Cran 3 · moyenne** : 75 % comme d'habitude, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.\n- **Cran 4 · haute** : 83 % comme d'habitude, 60 % quand tout le monde vote. Unicité du compte, Réciprocité, le titre du document.\n- **Cran 5 · très haute** : 90 % comme d'habitude, 66 % quand tout le monde vote. Aucun article au départ.\n\n**La participation habituelle** est la médiane du nombre de votants des propositions du groupe votées depuis un an. Tant que le groupe n'a pas voté sur 5 propositions, elle vaut deux fois C : 100 votants dans Monnaie libre. Elle reste entre 1 % et 50 % de l'électorat, et se mesure une fois par jour.\n\n**G** se calcule pour que la part exigée, à la participation habituelle p̃, soit exactement celle du cran : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est la part exigée comme d'habitude.\n\nExemple : avec 7 224 membres et une participation habituelle de 120 votants, quand 120 personnes votent, il faut 92, 97, 103, 109 ou 113 voix pour, du cran 1 au cran 5. Au cran 3 : 50 voix pour, plus 75 % des 70 autres votes, soit 102,5, arrondi à 103.",
"en": "At each level, the group first asks for C votes in favour, then for a share of votes in favour among the votes beyond C. That share depends on the level and on the number of voters, compared with the group's usual turnout: the more voters, the lower it gets, down to the majority M when everyone votes.\n\n`Share asked = M + (1 − M) × (1 − (T/W)^G)`\n\n**The five levels**: the share asked when the group votes as usual, then when everyone votes, and what is at that level at the start.\n\n- **Level 1 · very low**: 60% as usual, 50% when everyone votes. No article at the start.\n- **Level 2 · low**: 67% as usual, 50% when everyone votes. The order of the articles of each section.\n- **Level 3 · medium**: 75% as usual, 55% when everyone votes. The default level: all other articles, and every new article.\n- **Level 4 · high**: 83% as usual, 60% when everyone votes. Uniqueness of the account, Reciprocity, the title of the document.\n- **Level 5 · very high**: 90% as usual, 66% when everyone votes. No article at the start.\n\n**The usual turnout** is the median number of voters on the group's proposals voted on over the past year. Until the group has voted on 5 proposals, it is twice C: 100 voters in Monnaie libre. It stays between 1% and 50% of the electorate, and is measured once a day.\n\n**G** is computed so that the share asked, at the usual turnout p̃, is exactly that of the level: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is the share asked as usual.\n\nExample: with 7,224 members and a usual turnout of 120 voters, when 120 people vote, it takes 92, 97, 103, 109 or 113 votes in favour, from level 1 to level 5. At level 3: 50 votes in favour, plus 75% of the 70 other votes, i.e. 102.5, rounded up to 103.",
"es": "En cada nivel, el grupo pide primero C votos a favor, y luego una proporción de votos a favor entre los votos más allá de C. Esa proporción depende del nivel y del número de votantes, comparado con la participación habitual del grupo: cuantos más votantes, más baja, hasta la mayoría M cuando vota todo el mundo.\n\n`Proporción exigida = M + (1 − M) × (1 − (T/W)^G)`\n\n**Los cinco niveles**: la proporción exigida cuando el grupo vota como de costumbre, luego cuando vota todo el mundo, y lo que está en ese nivel al inicio.\n\n- **Nivel 1 · muy baja**: 60 % como de costumbre, 50 % cuando vota todo el mundo. Ningún artículo al inicio.\n- **Nivel 2 · baja**: 67 % como de costumbre, 50 % cuando vota todo el mundo. El orden de los artículos de cada sección.\n- **Nivel 3 · media**: 75 % como de costumbre, 55 % cuando vota todo el mundo. El nivel por defecto: todos los demás artículos, y todo artículo nuevo.\n- **Nivel 4 · alta**: 83 % como de costumbre, 60 % cuando vota todo el mundo. Unicidad de la cuenta, Reciprocidad, el título del documento.\n- **Nivel 5 · muy alta**: 90 % como de costumbre, 66 % cuando vota todo el mundo. Ningún artículo al inicio.\n\n**La participación habitual** es la mediana del número de votantes de las propuestas del grupo votadas en el último año. Hasta que el grupo haya votado 5 propuestas, vale dos veces C: 100 votantes en Monnaie libre. Se mantiene entre el 1 % y el 50 % del electorado, y se mide una vez al día.\n\n**G** se calcula para que la proporción exigida, con la participación habitual p̃, sea exactamente la del nivel: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es la proporción exigida como de costumbre.\n\nEjemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 92, 97, 103, 109 o 113 votos a favor, del nivel 1 al nivel 5. En el nivel 3: 50 votos a favor, más el 75 % de los otros 70 votos, es decir 102,5, redondeado a 103."
}
}
]
@@ -780,9 +780,9 @@
"es": "Ordenación del documento"
},
"body": {
"fr": "L'ordre de présentation des items dans le document est lui-même soumis au vote. Toute proposition de réorganisation doit atteindre le seuil d'adoption avec l'inertie standard.",
"en": "The order in which items are presented in the document is itself subject to vote. Any reorganisation proposal must reach the adoption threshold with standard inertia.",
"es": "El orden de presentación de los elementos en el documento está a su vez sometido a votación. Toda propuesta de reorganización debe alcanzar el umbral de adopción con la inercia estándar."
"fr": "L'ordre des articles de chaque section est lui-même soumis au vote. En fin de section, le module « Ordre des articles » se vote comme un article, au cran 2 (basse) au départ : chaque proposition est la liste des articles de la section, dans un autre ordre. Quand un ordre entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change.\n\nUn nouvel article se place à la fin de sa section ; sa place se vote ensuite par l'ordre de la section. Un ordre proposé garde ses votes quand la section change : un article arrivé depuis s'y ajoute à la fin, un article sorti en disparaît. Une section d'un seul article n'a pas d'ordre à voter.\n\nLe titre du document est lui aussi un module, voté au cran 4 (haute) au départ. Le titre de chaque article, lui, se vote avec son texte.",
"en": "The order of the articles of each section is itself put to the vote. At the end of each section, the “Order of the articles” module is voted on like an article, at level 2 (low) at the start: each proposal is the list of the section's articles, in another order. When an order comes into force, only the articles of that section change place; no text changes.\n\nA new article goes at the end of its section; its place is then voted on through the order of the section. A proposed order keeps its votes when the section changes: an article that has arrived since is added at the end, an article that has left drops out of it. A section with a single article has no order to vote on.\n\nThe title of the document is also a module, voted on at level 4 (high) at the start. The title of each article is voted on with its text.",
"es": "El orden de los artículos de cada sección está a su vez sometido a votación. Al final de cada sección, el módulo «Orden de los artículos» se vota como un artículo, con el nivel 2 (baja) al inicio: cada propuesta es la lista de los artículos de la sección, en otro orden. Cuando un orden entra en vigor, solo los artículos de esa sección cambian de lugar; ningún texto cambia.\n\nUn artículo nuevo se coloca al final de su sección; su lugar se vota después mediante el orden de la sección. Un orden propuesto conserva sus votos cuando la sección cambia: un artículo llegado después se añade al final, un artículo que ha salido desaparece de él. Una sección de un solo artículo no tiene orden que votar.\n\nEl título del documento es también un módulo, votado con el nivel 4 (alta) al inicio. El título de cada artículo, en cambio, se vota con su texto."
}
}
]