forked from yvv/decision
v3 : F1.2, N1, N1.1 et O1 réécrits selon les règles appliquées (Yvv, 02/11)
- 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:
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user