Fusion du lot T4 (demande d'Yvv du 03/11) : I1, E1, E2 prennent les textes d'Yvv ; E12 « Responsabilité civique » entre E1 et E2 ; les sections formule, inertie, ordonnancement sortent du document sans rien effacer (migration 0012 additive, ADR 0014) ; amendements gardés pour la production ; fichier de relecture textes-amorce.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 21:53:00 +01:00
co-authored by Claude Opus 5.5
35 changed files with 3825 additions and 447 deletions
+16 -6
View File
@@ -64,7 +64,10 @@ crates/
# · modules.rs / order.rs / doc_title.rs (les modules, ADR 0009 : l'ordre d'une # · modules.rs / order.rs / doc_title.rs (les modules, ADR 0009 : l'ordre d'une
# section, le titre du document) · module_amend.rs (ajout gardé) · quorum.rs (le quorum # section, le titre du document) · module_amend.rs (ajout gardé) · quorum.rs (le quorum
# d'un article, PUT /units/{id}/quorum ; « un vote existe » : voted_units, ADR 0013) · # d'un article, PUT /units/{id}/quorum ; « un vote existe » : voted_units, ADR 0013) ·
# quorum_amend.rs (les quorums par cran, amendement gardé) · web.rs (sert web/build) # quorum_amend.rs (les quorums par cran, amendement gardé) · article_amend.rs (un
# article du seed ajouté à un document existant, gardé : E12, lot T4) · retire.rs
# (formule, inertie, ordonnancement sortis du document sans rien effacer :
# units.retired_at, ADR 0014) · web.rs (sert web/build)
tests/ # api.rs (scénarios bout en bout) · ballots.rs (vote par la chaîne, indexeur simulé) · tests/ # api.rs (scénarios bout en bout) · ballots.rs (vote par la chaîne, indexeur simulé) ·
# objections.rs · typos.rs (lot F, horloge avancée, migration 0008) · modules.rs (ordre, # objections.rs · typos.rs (lot F, horloge avancée, migration 0008) · modules.rs (ordre,
# titre, ajout gardé) · seed_proposals.rs (Propositions seed, publication suspendue) · # titre, ajout gardé) · seed_proposals.rs (Propositions seed, publication suspendue) ·
@@ -74,10 +77,14 @@ crates/
# seed_quorum_texts.rs (lots T, T2 et T3 : F1.1, F1.2, N1.1 ; aucun chiffre daté, chaque # seed_quorum_texts.rs (lots T, T2 et T3 : F1.1, F1.2, N1.1 ; aucun chiffre daté, chaque
# phrase vraie pour tout W et tout quorum ; fixtures/seed-quorum-m6b.json et # phrase vraie pour tout W et tout quorum ; fixtures/seed-quorum-m6b.json et
# seed-quorum-t2.json, leurs textes depuis M6b et M6c, pour rejouer T2 et T3 sur une # seed-quorum-t2.json, leurs textes depuis M6b et M6c, pour rejouer T2 et T3 sur une
# base comme la production) # base comme la production ; depuis T4, sur le seed de M8) · amorce.rs (lot T4 : les
seeds/ # acte-engagement-certification.json (trilingue, 33 articles, aucun en vigueur ; # textes d'Yvv, E12, le retrait ; après M6c et après M8, chaque ligne de chaque table
# I1/E1/E2 Propositions seed au cran 2 (seed::SEED_PROPOSAL_LEVEL), inertie 3 ailleurs ; # gardée, le retour arrière) · fixtures/seed-m8/ (le seed de M8 : les derniers textes
# modules en vigueur sans vote : le titre à 4, l'ordre de chaque section à 2 ; # de formule, inertie, ordonnancement, référence du lot U3)
seeds/ # acte-engagement-certification.json (trilingue, 27 articles, 5 sections, aucun en
# vigueur ; I1/E1/E12/E2 Propositions seed au cran 2 (seed::SEED_PROPOSAL_LEVEL),
# inertie 3 ailleurs ; modules en vigueur sans vote : le titre à 4, l'ordre de chaque
# section à 2 ;
# quorums par cran : 12 · 12 · 50 · 50 · 50 Monnaie libre, 1 partout essai) # quorums par cran : 12 · 12 · 50 · 50 · 50 Monnaie libre, 1 partout essai)
# upcoming/<groupe>.json : les « À venir » des outils d'un groupe (fr, en, es), lus au démarrage # upcoming/<groupe>.json : les « À venir » des outils d'un groupe (fr, en, es), lus au démarrage
web/src/ web/src/
@@ -134,7 +141,10 @@ docker build -f docker/app.Dockerfile --target production . # rejoue toute la
est la personne ou le groupe, jamais le système. Le français du texte voté fait foi. est la personne ou le groupe, jamais le système. Le français du texte voté fait foi.
- **Moteur pur** : une règle de vote / édition / composition s'écrit et se teste dans - **Moteur pur** : une règle de vote / édition / composition s'écrit et se teste dans
`ld-engine` avant d'être câblée. Le serveur ne décide jamais l'accès seul : `trustin-access`. `ld-engine` avant d'être câblée. Le serveur ne décide jamais l'accès seul : `trustin-access`.
- **Invariants** : au départ, aucun article n'est en vigueur : I1, E1 et E2 sont des Propositions - **Invariants** : les informations de fonctionnement (formule, inertie, ordonnancement) ne se
votent pas : hors du document, une unité retirée (`units.retired_at`) n'est ni lue, ni votée, ni
comptée, ni publiée, et rien n'est effacé (ADR 0014) ; au départ, aucun article n'est en
vigueur : I1, E1, E12 et E2 sont des Propositions
seed (ouvertes, mises en avant, au cran 2 ; seuls les modules sont des *seeds a minima*), et la seed (ouvertes, mises en avant, au cran 2 ; seuls les modules sont des *seeds a minima*), et la
publication d'un document attend son premier texte en vigueur (ADR 0011) ; un quorum par cran publication d'un document attend son premier texte en vigueur (ADR 0011) ; un quorum par cran
(12 · 12 · 50 · 50 · 50 dans Monnaie libre), que l'auteur d'un article change avant le premier (12 · 12 · 50 · 50 · 50 dans Monnaie libre), que l'auteur d'un article change avant le premier
@@ -0,0 +1,49 @@
-- The information on how the vote works leaves the document (2026-11-03, lot T4,
-- ADR 0014). Additive only: no row is dropped, no column an older image reads is
-- rewritten.
--
-- Yvv, 2026-11-03: « les 3 derniers modules ne sont pas de votes : ordonnancement,
-- inertie, et formule de vote sont des infos de fonctionnement. Les votes se font à
-- leur place, et les infos doivent trouver leur place dans l'ux. » The articles of the
-- sections `formule`, `inertie` and `ordonnancement`, and the order of each, are no
-- texts to vote. A new base is born without them (the seed no longer holds them); an
-- existing base keeps every row of them — units, versions, revisions, translations —
-- and marks the units retired: the document no longer lists them, counts them,
-- votes on them or publishes them. An older image (rollback) ignores the column and
-- shows them again exactly as they were.
-- When the unit left the document (NULL: it is in it), and the amendment that took it out.
ALTER TABLE units ADD COLUMN retired_at TEXT;
ALTER TABLE units ADD COLUMN retired_reason TEXT;
-- The retirement of an existing document's units (src/retire.rs): decided once per
-- document, only where no person left a trace on any of them. Each decision is kept
-- here: applied (and the codes retired), refused (why, and on which unit), or present
-- (the document was born without them).
CREATE TABLE retirement_amendments (
amendment_id TEXT NOT NULL,
doc_id TEXT NOT NULL REFERENCES documents(id),
outcome TEXT NOT NULL, -- applied | refused | present
reason TEXT, -- why it was refused
unit_code TEXT, -- the unit the trace was found on
retired TEXT, -- JSON array of the codes retired
at TEXT NOT NULL,
PRIMARY KEY (amendment_id, doc_id)
);
-- An article the seed now holds, added to an existing document (src/article_amend.rs):
-- decided once per document. Each decision is kept here: applied (the unit created,
-- where it was placed and why), refused (why), or present (born with it).
CREATE TABLE article_amendments (
amendment_id TEXT NOT NULL,
doc_id TEXT NOT NULL REFERENCES documents(id),
unit_code TEXT NOT NULL,
unit_id TEXT, -- the unit created, or found
outcome TEXT NOT NULL, -- applied | refused | present
reason TEXT, -- why it was refused
placement TEXT, -- between (where the seed puts it) | end (of its section)
placement_reason TEXT, -- why at the end
position REAL,
at TEXT NOT NULL,
PRIMARY KEY (amendment_id, doc_id)
);
+378
View File
@@ -0,0 +1,378 @@
//! Article amendments — an article the seed now holds, added to a document that was
//! born without it.
//!
//! Yvv, 2026-11-03 (lot T4): « AJOUT d'un texte : RESPONSABILITE CIVIQUE → Je déclare sur
//! l'honneur avoir la capacité légale et physique de signer un contrat et d'en assumer la
//! responsabilité civique, pour certifier à mon tour. » A new base gets it from the seed:
//! E12 « Responsabilité civique », a seed proposal (ADR 0011) at `seed::SEED_PROPOSAL_LEVEL`,
//! between E1 and E2 in the section `fondamental`. An existing base — production holds the
//! certification act twice, Monnaie libre and the test group — gets it through an amendment
//! listed here, decided once per document, on the model of `module_amend`: the article of
//! the seed is written as a new base writes it (`seed::write_unit`: its unit, its version
//! open and put forward, its first revision, its translations), and nothing else is
//! written — no text changes, no vote is touched, no other article moves.
//!
//! **Where it goes.** The order of a section is a module (ADR 0009). In force at the start
//! without a vote (a seed a minima), it reads the positions of the articles. While no vote
//! exists on that order (`quorum::voted_units`: a vote, a line of a ballot, an adoption), the
//! article takes the place the seed gives it: between `after` and `before`, adjacent in the
//! order of the section now, half-way between their positions — so the order in force says
//! it at once. Once someone voted on the order, or when the two are no longer adjacent, or
//! one of them is gone from the section, its place is that of any new article: the end of
//! its section (`ld_engine::order::place_at_end`); the order of the section is then voted
//! as always (an open order completes it at its end, ADR 0009).
//!
//! The document holds a unit of that code already: present when it is the seed's (born
//! with it), refused otherwise (`code_taken`); no such section in the document: refused
//! (`no_section`). Each decision is kept in `article_amendments` (where the article went,
//! and why at the end) and in the logs. Applied where the document publishes, the
//! publication queue gets a row, which waits for the first text (ADR 0011).
use crate::AppState;
use crate::quorum;
use crate::seed::{self, SeedDoc, SeedUnit};
use crate::seed_amend::fingerprint;
use crate::util;
use sqlx::SqliteConnection;
/// One article to add to every document instantiated from a seed.
pub struct ArticleAmendment {
/// Stable identifier: each document's decision is kept under it.
pub id: &'static str,
/// Slug of the seed document.
pub doc: &'static str,
/// Code of the article: one no document holds yet.
pub unit: &'static str,
/// Fingerprint of its text in the seed (source language), which must hash to it: a seed
/// edited later never reaches a base by this way.
pub text: &'static str,
/// The two articles of its section it goes between, in this order.
pub after: &'static str,
pub before: &'static str,
/// Reason given to the publication queue, and so to the commit of the texts.
pub reason: &'static str,
}
pub const AMENDMENTS: &[ArticleAmendment] = &[ArticleAmendment {
id: "2026-11-03-e12-responsabilite-civique",
doc: "acte-engagement-certification",
unit: "E12",
text: "3fa5a8e5df24e9ebb6b2882968bd0b6989c820a3f60d7c67bdb7fccf17ae1296",
after: "E1",
before: "E2",
reason: "Proposition seed ajoutée : E12, Responsabilité civique (texte d'Yvv du 03/11)",
}];
/// What the database says of the document.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Facts {
/// A unit of that code: `Some(true)` the seed's, `Some(false)` another one.
pub existing: Option<bool>,
/// The section is in the document (an article of it, not retired).
pub section: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Apply,
/// Born with it: nothing to do.
Present,
Refuse(&'static str),
}
/// Whether the article is added, apart from the database.
pub fn verdict(f: &Facts) -> Verdict {
match f.existing {
Some(true) => Verdict::Present,
Some(false) => Verdict::Refuse("code_taken"),
None if !f.section => Verdict::Refuse("no_section"),
None => Verdict::Apply,
}
}
/// Where the article goes.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Placement {
/// Where the seed puts it, at this position: the order in force says it at once.
Between(f64),
/// At the end of its section, as any new article (ADR 0009), and why.
End(&'static str),
}
/// Where it goes, apart from the database: `voted`, whether a vote exists on the order of
/// its section; `members`, the section in its order now (code, position); `positions`,
/// every article of the document.
pub fn placement(voted: bool, members: &[(String, f64)], after: &str, before: &str, positions: &[f64]) -> Placement {
if voted {
return Placement::End("order_voted");
}
let at = |code: &str| members.iter().position(|(c, _)| c == code);
let (Some(i), Some(j)) = (at(after), at(before)) else { return Placement::End("not_in_section") };
let (a, b) = (members[i].1, members[j].1);
// Adjacent in the order, and nothing of the document between their places.
if j != i + 1 || a >= b || positions.iter().any(|p| *p > a && *p < b) {
return Placement::End("not_adjacent");
}
Placement::Between(f64::midpoint(a, b))
}
/// A decision taken at this start.
#[derive(Debug, Clone, PartialEq)]
pub struct Outcome {
pub amendment: &'static str,
pub group: String,
pub verdict: Verdict,
pub placement: Option<Placement>,
}
/// Decides every amendment not yet decided, document by document.
pub async fn apply(state: &AppState, seeds: &[SeedDoc], list: &[ArticleAmendment]) -> anyhow::Result<Vec<Outcome>> {
let mut out = Vec::new();
for a in list {
let Some((seed, unit)) = seed_says(seeds, a) else { continue };
let docs: Vec<(String, String, Option<String>)> =
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((v, placement)) = decide(state, a, seed, unit, &doc_id, publish_path.is_some()).await? else {
continue;
};
match (v, placement) {
(Verdict::Apply, Some(p)) => {
tracing::info!(amendment = a.id, %group, unit = a.unit, placement = ?p, "article added")
}
(Verdict::Refuse(why), _) => {
tracing::warn!(amendment = a.id, %group, unit = a.unit, reason = why, "article amendment refused: the document stays as it is")
}
_ => tracing::info!(amendment = a.id, %group, unit = a.unit, "article amendment: already there"),
}
if v == Verdict::Apply && publish_path.is_some() {
state.publish_wake.notify_one();
}
out.push(Outcome { amendment: a.id, group, verdict: v, placement });
}
}
Ok(out)
}
/// The seed must hold the article as a seed proposal with the text the amendment names,
/// between the two it names, in their section.
fn seed_says<'a>(seeds: &'a [SeedDoc], a: &ArticleAmendment) -> Option<(&'a SeedDoc, &'a SeedUnit)> {
let found = seeds.iter().find(|s| s.slug == a.doc).and_then(|s| {
let src = s.source_lang.as_str();
let of = |code: &str| s.units.iter().find(|u| u.code == code);
let (u, after, before) = (of(a.unit)?, of(a.after)?, of(a.before)?);
let v = u.versions.iter().find(|v| v.seed_proposal)?;
let print = fingerprint(v.title.get(src)?, v.body.get(src)?);
let placed = u.section.is_some()
&& after.section == u.section
&& before.section == u.section
&& after.position < u.position
&& u.position < before.position;
(print == a.text && u.inertia.is_none() && placed).then_some((s, u))
});
if found.is_none() {
tracing::error!(
amendment = a.id,
doc = a.doc,
unit = a.unit,
"article amendment: the seed does not hold this article where it names it, nothing decided"
);
}
found
}
async fn facts(conn: &mut SqliteConnection, doc_id: &str, a: &ArticleAmendment, section: &str) -> sqlx::Result<Facts> {
let existing: Option<(String, i64)> = sqlx::query_as(
"SELECT u.id, (SELECT COUNT(*) FROM proposals p WHERE p.unit_id = u.id AND p.origin = 'seed')
FROM units u WHERE u.doc_id = ? AND u.code = ?",
)
.bind(doc_id)
.bind(a.unit)
.fetch_optional(&mut *conn)
.await?;
let in_section: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM units WHERE doc_id = ? AND kind = 'article' AND section = ? AND retired_at IS NULL",
)
.bind(doc_id)
.bind(section)
.fetch_one(&mut *conn)
.await?;
Ok(Facts { existing: existing.map(|(_, seed)| seed > 0), section: in_section > 0 })
}
/// The placement, and the end of the section (the place of any new article there).
async fn where_to(
conn: &mut SqliteConnection,
doc_id: &str,
a: &ArticleAmendment,
section: &str,
) -> sqlx::Result<(Placement, f64)> {
let articles: Vec<(String, String, Option<String>, f64)> = sqlx::query_as(
"SELECT id, code, section, position FROM units WHERE doc_id = ? AND kind = 'article' AND retired_at IS NULL",
)
.bind(doc_id)
.fetch_all(&mut *conn)
.await?;
let order: Option<String> = sqlx::query_scalar(
"SELECT id FROM units WHERE doc_id = ? AND kind = 'order' AND section = ? AND retired_at IS NULL",
)
.bind(doc_id)
.bind(section)
.fetch_optional(&mut *conn)
.await?;
let voted = match &order {
Some(id) => quorum::voted_units(conn, doc_id).await?.contains(id),
None => false,
};
let members: Vec<(String, f64)> = crate::order::sections_now(conn, doc_id)
.await?
.into_iter()
.find(|(s, _)| s == section)
.map(|(_, ids)| {
ids.iter().filter_map(|id| articles.iter().find(|x| &x.0 == id).map(|x| (x.1.clone(), x.3))).collect()
})
.unwrap_or_default();
let positions: Vec<f64> = articles.iter().map(|x| x.3).collect();
let places: Vec<(Option<&str>, f64)> = articles.iter().map(|x| (x.2.as_deref(), x.3)).collect();
let end = ld_engine::order::place_at_end(&places, Some(section));
Ok((placement(voted, &members, a.after, a.before, &positions), end))
}
/// One document, in one transaction: `None` when it was decided at an earlier start.
async fn decide(
state: &AppState,
a: &ArticleAmendment,
seed: &SeedDoc,
unit: &SeedUnit,
doc_id: &str,
publishes: bool,
) -> anyhow::Result<Option<(Verdict, Option<Placement>)>> {
let mut tx = state.db.begin().await?;
let decided: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM article_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 section = unit.section.clone().unwrap_or_default();
let found = facts(&mut tx, doc_id, a, &section).await?;
let v = verdict(&found);
let now = util::now();
let (mut placement, mut position, mut unit_id) = (None, None, None);
if v == Verdict::Apply {
let (p, end) = where_to(&mut tx, doc_id, a, &section).await?;
let at = match p {
Placement::Between(at) => at,
Placement::End(_) => end,
};
unit_id = Some(seed::write_unit(&mut tx, doc_id, &seed.source_lang, unit, at, &now).await?);
(placement, position) = (Some(p), Some(at));
if publishes {
let where_ = match p {
Placement::Between(_) => format!("entre {} et {}", a.after, a.before),
Placement::End(_) => "en fin de section".to_owned(),
};
sqlx::query("INSERT INTO outbox (doc_id, reason, created_at, next_at) VALUES (?, ?, ?, ?)")
.bind(doc_id)
.bind(format!("{}, {where_}", a.reason))
.bind(&now)
.bind(&now)
.execute(&mut *tx)
.await?;
}
} else if v == Verdict::Present {
unit_id = sqlx::query_scalar("SELECT id FROM units WHERE doc_id = ? AND code = ?")
.bind(doc_id)
.bind(a.unit)
.fetch_optional(&mut *tx)
.await?;
}
let (outcome, reason) = match v {
Verdict::Apply => ("applied", None),
Verdict::Present => ("present", None),
Verdict::Refuse(why) => ("refused", Some(why)),
};
let (kind, why_end) = match placement {
Some(Placement::Between(_)) => (Some("between"), None),
Some(Placement::End(why)) => (Some("end"), Some(why)),
None => (None, None),
};
sqlx::query(
"INSERT INTO article_amendments (amendment_id, doc_id, unit_code, unit_id, outcome, reason, placement,
placement_reason, position, at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(a.id)
.bind(doc_id)
.bind(a.unit)
.bind(&unit_id)
.bind(outcome)
.bind(reason)
.bind(kind)
.bind(why_end)
.bind(position)
.bind(&now)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(Some((v, placement)))
}
#[cfg(test)]
mod tests {
use super::*;
fn section(codes: &[(&str, f64)]) -> Vec<(String, f64)> {
codes.iter().map(|(c, p)| ((*c).to_owned(), *p)).collect()
}
#[test]
fn a_document_without_the_article_receives_it() {
assert_eq!(verdict(&Facts { existing: None, section: true }), Verdict::Apply);
}
#[test]
fn born_with_it_or_its_code_taken_it_is_not_written_again() {
assert_eq!(verdict(&Facts { existing: Some(true), section: true }), Verdict::Present);
assert_eq!(verdict(&Facts { existing: Some(false), section: true }), Verdict::Refuse("code_taken"));
assert_eq!(verdict(&Facts { existing: None, section: false }), Verdict::Refuse("no_section"));
}
#[test]
fn it_goes_between_its_two_while_the_order_was_never_voted() {
let fondamental = section(&[("T1", 3.0), ("E1", 4.0), ("E2", 5.0)]);
let all = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
assert_eq!(placement(false, &fondamental, "E1", "E2", &all), Placement::Between(4.5));
// A member's article at the end of the section changes nothing between them.
let longer = section(&[("T1", 3.0), ("E1", 4.0), ("E2", 5.0), ("N3", 5.5)]);
assert_eq!(placement(false, &longer, "E1", "E2", &[3.0, 4.0, 5.0, 5.5, 6.0]), Placement::Between(4.5));
}
#[test]
fn otherwise_at_the_end_of_its_section_and_why() {
let fondamental = section(&[("T1", 3.0), ("E1", 4.0), ("E2", 5.0)]);
let all = [3.0, 4.0, 5.0];
assert_eq!(placement(true, &fondamental, "E1", "E2", &all), Placement::End("order_voted"));
// An order voted into force that put E2 first.
let moved = section(&[("T1", 3.0), ("E2", 4.0), ("E1", 5.0)]);
assert_eq!(placement(false, &moved, "E1", "E2", &all), Placement::End("not_adjacent"));
let apart = section(&[("E1", 4.0), ("T1", 4.5), ("E2", 5.0)]);
assert_eq!(placement(false, &apart, "E1", "E2", &all), Placement::End("not_adjacent"));
// Something of another section between their places.
assert_eq!(placement(false, &fondamental, "E1", "E2", &[4.0, 4.2, 5.0]), Placement::End("not_adjacent"));
let gone = section(&[("T1", 3.0), ("E1", 4.0)]);
assert_eq!(placement(false, &gone, "E1", "E2", &all), Placement::End("not_in_section"));
}
#[test]
fn the_shipped_amendment_adds_e12_between_e1_and_e2() {
let a = &AMENDMENTS[0];
assert_eq!((a.unit, a.after, a.before), ("E12", "E1", "E2"));
}
}
+12 -7
View File
@@ -156,10 +156,12 @@ pub async fn group(
sqlx::query_as::<_, DocListRow>( sqlx::query_as::<_, DocListRow>(
"SELECT d.id, d.slug, d.title, d.summary, "SELECT d.id, d.slug, d.title, d.summary,
(SELECT COUNT(*) FROM units u (SELECT COUNT(*) FROM units u
WHERE u.doc_id = d.id AND u.kind = 'article' AND u.in_force_id IS NOT NULL) AS in_force, WHERE u.doc_id = d.id AND u.kind = 'article' AND u.in_force_id IS NOT NULL
-- Articles in force; open proposals of articles and modules alike (ADR 0009). AND u.retired_at IS NULL) AS in_force,
-- Articles in force; open proposals of articles and modules alike (ADR 0009);
-- nothing of a unit out of the document (ADR 0014).
(SELECT COUNT(*) FROM proposals p JOIN units u ON u.id = p.unit_id (SELECT COUNT(*) FROM proposals p JOIN units u ON u.id = p.unit_id
WHERE u.doc_id = d.id AND p.status = 'open') AS open WHERE u.doc_id = d.id AND p.status = 'open' AND u.retired_at IS NULL) AS open
FROM documents d WHERE d.group_slug = ? ORDER BY d.created_at", FROM documents d WHERE d.group_slug = ? ORDER BY d.created_at",
) )
.bind(&slug) .bind(&slug)
@@ -298,7 +300,7 @@ pub fn inertia_of(unit: &UnitRow) -> InertiaLevel {
pub struct DocState { pub struct DocState {
pub doc: DocRow, pub doc: DocRow,
pub group: GroupRow, pub group: GroupRow,
/// Every unit, articles and modules, by position. /// Every unit in the document, articles and modules, by position: none retired (ADR 0014).
pub units: Vec<UnitRow>, pub units: Vec<UnitRow>,
/// The units that are modules (ADR 0009), by unit id; any other is an article. /// The units that are modules (ADR 0009), by unit id; any other is an article.
pub module_kinds: HashMap<String, ModuleKind>, pub module_kinds: HashMap<String, ModuleKind>,
@@ -359,14 +361,17 @@ pub async fn electorate(state: &AppState, group: &GroupRow) -> AppResult<(u32, &
pub async fn load_state(state: &AppState, doc_id: &str) -> AppResult<DocState> { pub async fn load_state(state: &AppState, doc_id: &str) -> AppResult<DocState> {
let doc = load_doc(state, doc_id).await?; let doc = load_doc(state, doc_id).await?;
let group = load_group(state, &doc.group_slug).await?; let group = load_group(state, &doc.group_slug).await?;
// The units in the document: a unit retired (ADR 0014) is not read, nor its versions —
// neither listed, nor counted, nor voted on, nor published (`retire`).
let units = sqlx::query_as::<_, UnitRow>( let units = sqlx::query_as::<_, UnitRow>(
"SELECT id, code, section, position, inertia, in_force_id, quorum FROM units WHERE doc_id = ? ORDER BY position, id", "SELECT id, code, section, position, inertia, in_force_id, quorum FROM units
WHERE doc_id = ? AND retired_at IS NULL ORDER BY position, id",
) )
.bind(doc_id) .bind(doc_id)
.fetch_all(&state.db) .fetch_all(&state.db)
.await?; .await?;
let kinds: Vec<(String, String)> = let kinds: Vec<(String, String)> =
sqlx::query_as("SELECT id, kind FROM units WHERE doc_id = ? AND kind <> 'article'") sqlx::query_as("SELECT id, kind FROM units WHERE doc_id = ? AND kind <> 'article' AND retired_at IS NULL")
.bind(doc_id) .bind(doc_id)
.fetch_all(&state.db) .fetch_all(&state.db)
.await?; .await?;
@@ -375,7 +380,7 @@ pub async fn load_state(state: &AppState, doc_id: &str) -> AppResult<DocState> {
// Same second (the variants of a seed): the order they were written in, not their // Same second (the variants of a seed): the order they were written in, not their
// random identifiers. The engine ranks rivals on its own; this order is what people see. // random identifiers. The engine ranks rivals on its own; this order is what people see.
let proposals = sqlx::query_as::<_, ProposalRow>(&format!( let proposals = sqlx::query_as::<_, ProposalRow>(&format!(
"{PROPOSAL_SELECT} WHERE p.unit_id IN (SELECT id FROM units WHERE doc_id = ?) "{PROPOSAL_SELECT} WHERE p.unit_id IN (SELECT id FROM units WHERE doc_id = ? AND retired_at IS NULL)
AND p.status IN ('open', 'in_force', 'draft') ORDER BY p.submitted_at, p.created_at, p.rowid" AND p.status IN ('open', 'in_force', 'draft') ORDER BY p.submitted_at, p.created_at, p.rowid"
)) ))
.bind(doc_id) .bind(doc_id)
+2
View File
@@ -10,6 +10,7 @@
pub mod access; pub mod access;
pub mod adoption; pub mod adoption;
pub mod article_amend;
pub mod auth; pub mod auth;
pub mod ballots; pub mod ballots;
pub mod clock; pub mod clock;
@@ -31,6 +32,7 @@ pub mod proposals;
pub mod publish; pub mod publish;
pub mod quorum; pub mod quorum;
pub mod quorum_amend; pub mod quorum_amend;
pub mod retire;
pub mod seed; pub mod seed;
pub mod seed_amend; pub mod seed_amend;
pub mod seed_proposal_amend; pub mod seed_proposal_amend;
+3 -1
View File
@@ -37,7 +37,9 @@ pub async fn sections_now(conn: &mut SqliteConnection, doc_id: &str) -> sqlx::Re
position: f64, position: f64,
visible: bool, visible: bool,
} }
// Seen: a text in force, or an open version (`DocState::members` says the same). // Seen: a text in force, or an open version (`DocState::members` says the same). It reads
// only the columns of before 0012: a document born now, or given its modules by an
// amendment, holds no retired unit (ADR 0014); `article_amend` keeps the live ones.
let rows = sqlx::query_as::<_, Row>( let rows = sqlx::query_as::<_, Row>(
"SELECT u.id, u.section, u.position, "SELECT u.id, u.section, u.position,
(u.in_force_id IS NOT NULL OR EXISTS (SELECT 1 FROM proposals p (u.in_force_id IS NOT NULL OR EXISTS (SELECT 1 FROM proposals p
+4 -2
View File
@@ -137,7 +137,8 @@ pub async fn decisions(
/// order of each section (ADR 0009): the version in force (adopted by a vote, /// order of each section (ADR 0009): the version in force (adopted by a vote,
/// or a seed a minima), the open versions, and whether `me` holds a valid vote /// or a seed a minima), the open versions, and whether `me` holds a valid vote
/// on one of them. A change of inertia is not a version of the text; the order /// on one of them. A change of inertia is not a version of the text; the order
/// of a section of fewer than two articles has nothing to decide; a version held /// of a section of fewer than two articles has nothing to decide; a unit out of the
/// document (ADR 0014) is no module to vote; a version held
/// by an objection is not counted as one to vote while it is held: it takes no /// by an objection is not counted as one to vote while it is held: it takes no
/// new *for*, only against or a withdrawal (ADR 0010). /// new *for*, only against or a withdrawal (ADR 0010).
async fn modules(state: &AppState, doc_id: &str, me: &str) -> AppResult<Vec<Module>> { async fn modules(state: &AppState, doc_id: &str, me: &str) -> AppResult<Vec<Module>> {
@@ -169,7 +170,8 @@ async fn modules(state: &AppState, doc_id: &str, me: &str) -> AppResult<Vec<Modu
WHERE b.requested_by = ?1 AND b.status = 'pending' AND b.mode = 'chain' WHERE b.requested_by = ?1 AND b.status = 'pending' AND b.mode = 'chain'
AND l.proposal_id = p.id AND l.revision = p.revision AND l.choice <> 'none')) AS voted AND l.proposal_id = p.id AND l.revision = p.revision AND l.choice <> 'none')) AS voted
FROM units u JOIN proposals p ON p.unit_id = u.id FROM units u JOIN proposals p ON p.unit_id = u.id
WHERE u.doc_id = ?2 AND p.kind <> 'inertia' AND (p.id = u.in_force_id OR p.status = 'open') WHERE u.doc_id = ?2 AND u.retired_at IS NULL AND p.kind <> 'inertia'
AND (p.id = u.in_force_id OR p.status = 'open')
ORDER BY u.position, u.id", ORDER BY u.position, u.id",
) )
.bind(me) .bind(me)
+5
View File
@@ -60,6 +60,11 @@ pub(crate) async fn guarded(
if p.status == "draft" && p.author_id.as_deref() != Some(user.id.as_str()) { if p.status == "draft" && p.author_id.as_deref() != Some(user.id.as_str()) {
return Err(AppError::NotFound); return Err(AppError::NotFound);
} }
// A version of a unit out of the document (ADR 0014) is no longer in it: nothing is
// read, voted, corrected, translated or objected on it.
if !st.units.iter().any(|u| u.id == p.unit_id) {
return Err(AppError::NotFound);
}
Ok((p, st)) Ok((p, st))
} }
+6 -3
View File
@@ -195,7 +195,8 @@ pub async fn waits_for_a_text(state: &AppState, doc_id: &str) -> anyhow::Result<
return Ok(false); return Ok(false);
} }
let in_force: i64 = sqlx::query_scalar( let in_force: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM units WHERE doc_id = ? AND kind = 'article' AND in_force_id IS NOT NULL", "SELECT COUNT(*) FROM units WHERE doc_id = ? AND kind = 'article' AND in_force_id IS NOT NULL
AND retired_at IS NULL",
) )
.bind(doc_id) .bind(doc_id)
.fetch_one(&state.db) .fetch_one(&state.db)
@@ -317,8 +318,10 @@ async fn last_change(state: &AppState, doc_id: &str) -> anyhow::Result<String> {
let at: Option<String> = sqlx::query_scalar( let at: Option<String> = sqlx::query_scalar(
"SELECT MAX(d) FROM ( "SELECT MAX(d) FROM (
SELECT MAX(at) AS d FROM adoptions WHERE doc_id = ?1 SELECT MAX(at) AS d FROM adoptions WHERE doc_id = ?1
UNION ALL SELECT MAX(p.updated_at) FROM proposals p JOIN units u ON u.in_force_id = p.id WHERE u.doc_id = ?1 UNION ALL SELECT MAX(p.updated_at) FROM proposals p JOIN units u ON u.in_force_id = p.id
UNION ALL SELECT MAX(t.updated_at) FROM translations t JOIN units u ON u.in_force_id = t.proposal_id WHERE u.doc_id = ?1 WHERE u.doc_id = ?1 AND u.retired_at IS NULL
UNION ALL SELECT MAX(t.updated_at) FROM translations t JOIN units u ON u.in_force_id = t.proposal_id
WHERE u.doc_id = ?1 AND u.retired_at IS NULL
UNION ALL SELECT created_at FROM documents WHERE id = ?1)", UNION ALL SELECT created_at FROM documents WHERE id = ?1)",
) )
.bind(doc_id) .bind(doc_id)
+373
View File
@@ -0,0 +1,373 @@
//! Retirements — the units a seed no longer holds, taken out of an existing document
//! without erasing anything (ADR 0014).
//!
//! Yvv, 2026-11-03 (lot T4): « Attention les 3 derniers modules ne sont pas de votes :
//! ordonnancement, inertie, et formule de vote sont des infos de fonctionnement. Les votes
//! se font à leur place, et les infos doivent trouver leur place dans l'ux. […] Vire les 3
//! du bas de la liste. » The articles of the sections `formule` (F1, F1.1, F1.2, F1.3),
//! `inertie` (N1, N1.1) and `ordonnancement` (O1), and the order of each section, say how
//! the vote works: they are no texts to vote. A new base is born without them (the seed no
//! longer holds them). An existing base keeps every row of them — units, versions,
//! revisions, translations, the publication queue — and marks the units retired
//! (`units.retired_at`, `retired_reason`): nothing else is written on them.
//!
//! **Out of the document.** A unit retired is not read with its document (`docs::load_state`
//! reads the units with `retired_at IS NULL`, and their versions only), so it is neither
//! listed, nor voted on (a vote, a grouped vote, a ballot), nor proposed, corrected,
//! translated or objected on (`proposals::guarded`: 404), nor given a change of inertia, a
//! quorum or an order (the unit is not found: 404), nor counted (`docs::group`,
//! `overview::modules`), nor published (`publish`: `document.json` and the Markdown read the
//! document's state; `waits_for_a_text` counts the articles in it). The order of a section
//! reads its articles in it (`DocState::members`). An older image
//! (rollback) ignores the column and shows them again, exactly as they were.
//!
//! **The guard**, decided once per document, all of them or none: they leave only if no
//! person left a trace on any of them, in any of its versions, whatever became of it —
//! a vote (simulated included), a line of a ballot (whatever its fate), an adoption, a
//! version written by a member (a text, a removal, a change of inertia, an order), a
//! revision written by a person (a typo fix, a substantive change), a typo fix waiting or
//! refused, a translation written by a member, an objection naming it, a quorum set. Their
//! texts may be at any revision the seed amendments wrote (F1.2 and N1.1 at 4 after M6c, 5
//! after M8): an amendment is no trace of a person. Refused, nothing moves; the reason and
//! the unit it was found on are kept (`retirement_amendments`) and in the logs. Applied
//! where the document publishes, the queue gets a row, which waits for the first text
//! (ADR 0011); the rows queued before for those texts stay, and go out with it.
use crate::AppState;
use crate::seed::SeedDoc;
use crate::util;
use sqlx::SqliteConnection;
/// The retirement of lot T4: the information on how the vote works (ADR 0014).
pub const RETIREMENT: &str = "2026-11-03-fonctionnement-hors-vote";
/// Units to take out of every document instantiated from a seed, by section.
pub struct Retirement {
/// Stable identifier: each document's decision is kept under it.
pub id: &'static str,
/// Slug of the seed document.
pub doc: &'static str,
/// The sections that leave: their articles, and the order of each.
pub sections: &'static [&'static str],
/// Reason given to the publication queue, and so to the commit of the texts.
pub reason: &'static str,
}
pub const AMENDMENTS: &[Retirement] = &[Retirement {
id: RETIREMENT,
doc: "acte-engagement-certification",
sections: &["formule", "inertie", "ordonnancement"],
reason: "informations de fonctionnement sorties du document, qui ne se votent pas : formule de vote (F1, F1.1, F1.2, F1.3), inertie (N1, N1.1), ordonnancement (O1), et l'ordre de chaque section (ADR 0014)",
}];
/// What the database says of one unit that would leave, all its versions together.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UnitFacts {
pub code: String,
/// Votes that count, of the server, proven or simulated.
pub votes: i64,
/// Lines of ballots, whatever their fate.
pub ballots: i64,
/// Adoptions, versions voted into force or confirmed, changes of inertia by a vote.
pub adopted: i64,
/// Versions written by a member: a text, a removal, a change of inertia, an order.
pub member_versions: i64,
/// Revisions written by a person, or of a kind no seed amendment writes; typo fixes.
pub corrected: i64,
/// Translations written or corrected by a member.
pub member_translations: i64,
/// Objections naming one of its versions, standing or lifted.
pub signals: i64,
/// Its own quorum set, or a change of it journaled.
pub quorum_set: i64,
}
impl UnitFacts {
/// The first trace of a person found on it, if any.
pub fn trace(&self) -> Option<&'static str> {
[
(self.votes, "votes"),
(self.ballots, "ballots"),
(self.adopted, "adopted"),
(self.member_versions, "member_proposal"),
(self.corrected, "corrected"),
(self.member_translations, "translation_edited"),
(self.signals, "signalled"),
(self.quorum_set, "quorum_set"),
]
.into_iter()
.find(|(n, _)| *n > 0)
.map(|(_, why)| why)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
Apply,
/// Born without them, or already out: nothing to do.
Present,
/// A trace of a person, and the unit it was found on.
Refuse(&'static str, String),
}
/// The rule, apart from the database: all of them leave, or none.
pub fn verdict(units: &[UnitFacts]) -> Verdict {
if units.is_empty() {
return Verdict::Present;
}
for u in units {
if let Some(why) = u.trace() {
return Verdict::Refuse(why, u.code.clone());
}
}
Verdict::Apply
}
/// A decision taken at this start.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outcome {
pub amendment: &'static str,
pub group: String,
pub verdict: Verdict,
pub retired: Vec<String>,
}
/// Decides every retirement not yet decided, document by document.
pub async fn apply(state: &AppState, seeds: &[SeedDoc], list: &[Retirement]) -> anyhow::Result<Vec<Outcome>> {
let mut out = Vec::new();
for a in list {
if !seed_says(seeds, a) {
continue;
}
let docs: Vec<(String, String, Option<String>)> =
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((v, retired)) = decide(state, a, &doc_id, publish_path.is_some()).await? else { continue };
match &v {
Verdict::Apply => {
tracing::info!(amendment = a.id, %group, retired = ?retired, "units out of the document")
}
Verdict::Present => {
tracing::info!(amendment = a.id, %group, "retirement: none of them in the document")
}
Verdict::Refuse(why, unit) => tracing::warn!(
amendment = a.id, %group, reason = why, %unit,
"retirement refused: a person left a trace, they stay in the document"
),
}
if v == Verdict::Apply && publish_path.is_some() {
state.publish_wake.notify_one();
}
out.push(Outcome { amendment: a.id, group, verdict: v, retired });
}
}
Ok(out)
}
/// The seed must no longer hold any unit of those sections: a seed that still gives them
/// never takes them out.
fn seed_says(seeds: &[SeedDoc], a: &Retirement) -> bool {
let says = seeds
.iter()
.find(|s| s.slug == a.doc)
.is_some_and(|s| s.units.iter().all(|u| u.section.as_deref().is_none_or(|x| !a.sections.contains(&x))));
if !says {
tracing::error!(
amendment = a.id,
doc = a.doc,
"retirement: the seed still holds these sections, or no such document, nothing decided"
);
}
says
}
async fn unit_facts(conn: &mut SqliteConnection, unit_id: &str, code: &str) -> sqlx::Result<UnitFacts> {
let n = |sql: &'static str| sqlx::query_scalar::<_, i64>(sql).bind(unit_id);
Ok(UnitFacts {
code: code.to_owned(),
votes: n("SELECT COUNT(*) FROM votes WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)")
.fetch_one(&mut *conn)
.await?,
ballots: n(
"SELECT COUNT(*) FROM ballot_lines WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)",
)
.fetch_one(&mut *conn)
.await?,
adopted: n("SELECT (SELECT COUNT(*) FROM adoptions WHERE unit_id = ?1)
+ (SELECT COUNT(*) FROM proposals WHERE unit_id = ?1
AND (entered_at IS NOT NULL OR confirmed_at IS NOT NULL))
+ (SELECT COUNT(*) FROM inertia_changes WHERE unit_id = ?1 AND cause = 'vote')")
.fetch_one(&mut *conn)
.await?,
member_versions: n(
"SELECT COUNT(*) FROM proposals WHERE unit_id = ?1 AND (origin <> 'seed' OR author_id IS NOT NULL)",
)
.fetch_one(&mut *conn)
.await?,
corrected: n(
"SELECT (SELECT COUNT(*) FROM typos
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1))
+ (SELECT COUNT(*) FROM revisions
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)
AND (author_id IS NOT NULL OR kind NOT IN ('initial', 'amendment', 'seed_proposal')))",
)
.fetch_one(&mut *conn)
.await?,
member_translations: n("SELECT COUNT(*) FROM translations
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)
AND (origin <> 'seed' OR updated_by IS NOT NULL)")
.fetch_one(&mut *conn)
.await?,
signals: n("SELECT COUNT(*) FROM signals
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)
OR target_proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)")
.fetch_one(&mut *conn)
.await?,
quorum_set: n("SELECT (SELECT COUNT(*) FROM units WHERE id = ?1 AND quorum IS NOT NULL)
+ (SELECT COUNT(*) FROM quorum_changes WHERE unit_id = ?1)")
.fetch_one(&mut *conn)
.await?,
})
}
/// One document, in one transaction: `None` when it was decided at an earlier start.
async fn decide(
state: &AppState,
a: &Retirement,
doc_id: &str,
publishes: bool,
) -> anyhow::Result<Option<(Verdict, Vec<String>)>> {
let mut tx = state.db.begin().await?;
let decided: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM retirement_amendments WHERE amendment_id = ? AND doc_id = ?")
.bind(a.id)
.bind(doc_id)
.fetch_one(&mut *tx)
.await?;
if decided > 0 {
return Ok(None);
}
// The units of those sections still in the document: the articles in their order, then
// the order of each section.
let marks = vec!["?"; a.sections.len()].join(", ");
let sql = format!(
"SELECT id, code FROM units WHERE doc_id = ? AND retired_at IS NULL AND section IN ({marks})
ORDER BY kind <> 'article', position, code"
);
let mut q = sqlx::query_as::<_, (String, String)>(&sql).bind(doc_id);
for s in a.sections {
q = q.bind(*s);
}
let units = q.fetch_all(&mut *tx).await?;
let mut facts = Vec::with_capacity(units.len());
for (id, code) in &units {
facts.push(unit_facts(&mut tx, id, code).await?);
}
let mut v = verdict(&facts);
let now = util::now();
let mut retired = Vec::new();
if v == Verdict::Apply {
for (id, code) in &units {
// Only the two columns of the retirement; nothing else of the unit, nothing of its
// versions, revisions or translations.
let done =
sqlx::query("UPDATE units SET retired_at = ?, retired_reason = ? WHERE id = ? AND retired_at IS NULL")
.bind(&now)
.bind(a.id)
.bind(id)
.execute(&mut *tx)
.await?;
if done.rows_affected() == 1 {
retired.push(code.clone());
}
}
if retired.len() != units.len() {
// What was examined changed under us: nothing is written.
tx.rollback().await?;
tx = state.db.begin().await?;
v = Verdict::Refuse("changed", String::new());
retired.clear();
} else 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, unit) = match &v {
Verdict::Apply => ("applied", None, None),
Verdict::Present => ("present", None, None),
Verdict::Refuse(why, unit) => ("refused", Some(*why), Some(unit.clone()).filter(|u| !u.is_empty())),
};
sqlx::query(
"INSERT INTO retirement_amendments (amendment_id, doc_id, outcome, reason, unit_code, retired, at)
VALUES (?, ?, ?, ?, ?, ?, ?)",
)
.bind(a.id)
.bind(doc_id)
.bind(outcome)
.bind(reason)
.bind(unit)
.bind((v == Verdict::Apply).then(|| serde_json::to_string(&retired).unwrap_or_else(|_| "[]".into())))
.bind(&now)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(Some((v, retired)))
}
#[cfg(test)]
mod tests {
use super::*;
fn untouched(code: &str) -> UnitFacts {
UnitFacts { code: code.into(), ..UnitFacts::default() }
}
#[test]
fn untouched_they_all_leave() {
assert_eq!(verdict(&[untouched("F1"), untouched("F1.2"), untouched("ordre.formule")]), Verdict::Apply);
}
#[test]
fn born_without_them_nothing_to_do() {
assert_eq!(verdict(&[]), Verdict::Present);
}
#[test]
fn any_trace_of_a_person_keeps_them_all_and_says_where() {
let cases = [
(UnitFacts { votes: 1, ..untouched("N1") }, "votes"),
(UnitFacts { ballots: 1, ..untouched("N1") }, "ballots"),
(UnitFacts { adopted: 1, ..untouched("N1") }, "adopted"),
(UnitFacts { member_versions: 1, ..untouched("N1") }, "member_proposal"),
(UnitFacts { corrected: 1, ..untouched("N1") }, "corrected"),
(UnitFacts { member_translations: 1, ..untouched("N1") }, "translation_edited"),
(UnitFacts { signals: 1, ..untouched("N1") }, "signalled"),
(UnitFacts { quorum_set: 1, ..untouched("N1") }, "quorum_set"),
];
for (facts, why) in cases {
let all = [untouched("F1"), facts.clone(), untouched("O1")];
assert_eq!(verdict(&all), Verdict::Refuse(why, "N1".into()), "{facts:?}");
}
// The first trace, in the order of the units, then of the kinds.
let two = [
untouched("F1"),
UnitFacts { signals: 1, votes: 2, ..untouched("F1.1") },
UnitFacts { quorum_set: 1, ..untouched("N1") },
];
assert_eq!(verdict(&two), Verdict::Refuse("votes", "F1.1".into()));
}
#[test]
fn the_shipped_retirement_takes_out_the_three_sections_of_operation() {
assert_eq!(AMENDMENTS[0].id, RETIREMENT);
assert_eq!(AMENDMENTS[0].sections, ["formule", "inertie", "ordonnancement"]);
}
}
+102 -75
View File
@@ -12,18 +12,26 @@
//! document holds; a starting text in force the seed now puts forward as a seed //! 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 //! proposal (ADR 0011), a seed proposal amendment (`seed_proposal_amend`), on an
//! article nobody has touched; the quorums by level the seed now gives a document //! 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. //! (ADR 0013), a quorum amendment (`quorum_amend`), where the document holds none; an
//! article the seed now holds, an article amendment (`article_amend`), placed where the
//! seed puts it while the order of its section was never voted on; the units the seed no
//! longer holds — the information on how the vote works (ADR 0014) —, a retirement
//! (`retire`), where no person left a trace on them: they stay in the database, out of
//! the document.
use crate::AppState; use crate::AppState;
use crate::article_amend;
use crate::inertia_amend; use crate::inertia_amend;
use crate::module_amend; use crate::module_amend;
use crate::modules; use crate::modules;
use crate::quorum_amend; use crate::quorum_amend;
use crate::retire;
use crate::seed_amend; use crate::seed_amend;
use crate::seed_proposal_amend; use crate::seed_proposal_amend;
use crate::util::{self, LangMap}; use crate::util::{self, LangMap};
use ld_engine::inertia::{InertiaLevel, quorum_bounds}; use ld_engine::inertia::{InertiaLevel, quorum_bounds};
use serde::Deserialize; use serde::Deserialize;
use sqlx::SqliteConnection;
use std::path::Path; use std::path::Path;
/// The level of inertia a seed proposal starts at (ADR 0011). Yvv, 2026-11-02: « on /// The level of inertia a seed proposal starts at (ADR 0011). Yvv, 2026-11-02: « on
@@ -132,8 +140,9 @@ pub async fn ensure_groups(state: &AppState) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
/// On start: the groups, the documents missing from them, then the seed, /// On start: the groups, the documents missing from them, then the seed, inertia,
/// inertia and module amendments (each decided once per document). /// module, seed proposal, quorum, article amendments and the retirements (each decided
/// once per document).
pub async fn ensure(state: &AppState) -> anyhow::Result<()> { pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
let seeds = documents(state).await?; let seeds = documents(state).await?;
seed_amend::apply(state, &seeds, seed_amend::AMENDMENTS).await?; seed_amend::apply(state, &seeds, seed_amend::AMENDMENTS).await?;
@@ -142,6 +151,8 @@ pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
// After the text amendments: a starting text replaced first, then put forward. // After the text amendments: a starting text replaced first, then put forward.
seed_proposal_amend::apply(state, &seeds, seed_proposal_amend::AMENDMENTS).await?; seed_proposal_amend::apply(state, &seeds, seed_proposal_amend::AMENDMENTS).await?;
quorum_amend::apply(state, &seeds, quorum_amend::AMENDMENTS).await?; quorum_amend::apply(state, &seeds, quorum_amend::AMENDMENTS).await?;
article_amend::apply(state, &seeds, article_amend::AMENDMENTS).await?;
retire::apply(state, &seeds, retire::AMENDMENTS).await?;
Ok(()) Ok(())
} }
@@ -225,7 +236,6 @@ fn check(seed: &SeedDoc) -> anyhow::Result<()> {
async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> anyhow::Result<()> { async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> anyhow::Result<()> {
let src = seed.source_lang.as_str(); let src = seed.source_lang.as_str();
let pick = |m: &LangMap| m.get(src).cloned().unwrap_or_default();
let now = util::now(); let now = util::now();
let doc_id = util::id(); let doc_id = util::id();
let mut tx = state.db.begin().await?; let mut tx = state.db.begin().await?;
@@ -253,77 +263,7 @@ async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> a
.await?; .await?;
} }
for unit in &seed.units { for unit in &seed.units {
let unit_id = util::id(); write_unit(&mut tx, &doc_id, src, unit, unit.position, &now).await?;
sqlx::query(
"INSERT INTO units (id, doc_id, code, section, position, inertia, in_force_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, NULL, ?)",
)
.bind(&unit_id)
.bind(&doc_id)
.bind(&unit.code)
.bind(&unit.section)
.bind(unit.position)
.bind(unit.level().as_db())
.bind(&now)
.execute(&mut *tx)
.await?;
for v in &unit.versions {
let pid = util::id();
let (title, body) = (pick(&v.title), pick(&v.body));
let status = if v.status == "in_force" { "in_force" } else { "open" };
sqlx::query(
"INSERT INTO proposals (id, unit_id, kind, title, body, status, origin, label, author_id, revision,
created_at, updated_at, submitted_at)
VALUES (?, ?, 'text', ?, ?, ?, 'seed', ?, NULL, 1, ?, ?, ?)",
)
.bind(&pid)
.bind(&unit_id)
.bind(&title)
.bind(&body)
.bind(status)
.bind(&v.label)
.bind(&now)
.bind(&now)
.bind(&now)
.execute(&mut *tx)
.await?;
sqlx::query(
"INSERT INTO revisions (proposal_id, revision, kind, title, body, author_id, created_at) VALUES (?, 1, 'initial', ?, ?, NULL, ?)",
)
.bind(&pid)
.bind(&title)
.bind(&body)
.bind(&now)
.execute(&mut *tx)
.await?;
// The mark, written apart: only where the seed puts the version forward.
if v.seed_proposal && status == "open" {
sqlx::query("UPDATE proposals SET seed_featured = 1 WHERE id = ?").bind(&pid).execute(&mut *tx).await?;
}
if status == "in_force" {
sqlx::query("UPDATE units SET in_force_id = ? WHERE id = ?")
.bind(&pid)
.bind(&unit_id)
.execute(&mut *tx)
.await?;
}
let hash = util::source_hash(&title, &body);
for lang in util::LANGS.iter().filter(|l| **l != src) {
let (Some(t), Some(b)) = (v.title.get(*lang), v.body.get(*lang)) else { continue };
sqlx::query(
"INSERT INTO translations (proposal_id, lang, title, body, source_hash, origin, updated_by, updated_at)
VALUES (?, ?, ?, ?, ?, 'seed', NULL, ?)",
)
.bind(&pid)
.bind(lang)
.bind(t)
.bind(b)
.bind(&hash)
.bind(&now)
.execute(&mut *tx)
.await?;
}
}
} }
// The modules, from the document just written: its title, the order of each section. // The modules, from the document just written: its title, the order of each section.
if let Some(m) = &seed.modules { if let Some(m) = &seed.modules {
@@ -343,6 +283,93 @@ async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> a
Ok(()) Ok(())
} }
/// Writes one article of the seed into a document, at `position`: its unit (at its level),
/// its versions — in force, open, put forward —, their first revision and their
/// translations. A new base (`instantiate`), or an article a seed adds later
/// (`article_amend`). Returns the unit's id.
pub(crate) async fn write_unit(
conn: &mut SqliteConnection,
doc_id: &str,
src: &str,
unit: &SeedUnit,
position: f64,
now: &str,
) -> sqlx::Result<String> {
let pick = |m: &LangMap| m.get(src).cloned().unwrap_or_default();
let unit_id = util::id();
sqlx::query(
"INSERT INTO units (id, doc_id, code, section, position, inertia, in_force_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, NULL, ?)",
)
.bind(&unit_id)
.bind(doc_id)
.bind(&unit.code)
.bind(&unit.section)
.bind(position)
.bind(unit.level().as_db())
.bind(now)
.execute(&mut *conn)
.await?;
for v in &unit.versions {
let pid = util::id();
let (title, body) = (pick(&v.title), pick(&v.body));
let status = if v.status == "in_force" { "in_force" } else { "open" };
sqlx::query(
"INSERT INTO proposals (id, unit_id, kind, title, body, status, origin, label, author_id, revision,
created_at, updated_at, submitted_at)
VALUES (?, ?, 'text', ?, ?, ?, 'seed', ?, NULL, 1, ?, ?, ?)",
)
.bind(&pid)
.bind(&unit_id)
.bind(&title)
.bind(&body)
.bind(status)
.bind(&v.label)
.bind(now)
.bind(now)
.bind(now)
.execute(&mut *conn)
.await?;
sqlx::query(
"INSERT INTO revisions (proposal_id, revision, kind, title, body, author_id, created_at) VALUES (?, 1, 'initial', ?, ?, NULL, ?)",
)
.bind(&pid)
.bind(&title)
.bind(&body)
.bind(now)
.execute(&mut *conn)
.await?;
// The mark, written apart: only where the seed puts the version forward.
if v.seed_proposal && status == "open" {
sqlx::query("UPDATE proposals SET seed_featured = 1 WHERE id = ?").bind(&pid).execute(&mut *conn).await?;
}
if status == "in_force" {
sqlx::query("UPDATE units SET in_force_id = ? WHERE id = ?")
.bind(&pid)
.bind(&unit_id)
.execute(&mut *conn)
.await?;
}
let hash = util::source_hash(&title, &body);
for lang in util::LANGS.iter().filter(|l| **l != src) {
let (Some(t), Some(b)) = (v.title.get(*lang), v.body.get(*lang)) else { continue };
sqlx::query(
"INSERT INTO translations (proposal_id, lang, title, body, source_hash, origin, updated_by, updated_at)
VALUES (?, ?, ?, ?, ?, 'seed', NULL, ?)",
)
.bind(&pid)
.bind(lang)
.bind(t)
.bind(b)
.bind(&hash)
.bind(now)
.execute(&mut *conn)
.await?;
}
}
Ok(unit_id)
}
/// The quorums by level as `documents.quorums` holds them: `[12,12,50,50,50]`. /// The quorums by level as `documents.quorums` holds them: `[12,12,50,50,50]`.
pub fn quorums_json(quorums: &[u32; 5]) -> String { pub fn quorums_json(quorums: &[u32; 5]) -> String {
serde_json::to_string(quorums).unwrap_or_else(|_| "null".into()) serde_json::to_string(quorums).unwrap_or_else(|_| "null".into())
+133 -8
View File
@@ -25,6 +25,15 @@
//! The earlier one is then `superseded_by` it: the seed no longer gives its text, //! The earlier one is then `superseded_by` it: the seed no longer gives its text,
//! so it decides nothing more, without an error (the decisions already taken stay). //! so it decides nothing more, without an error (the decisions already taken stay).
//! //!
//! Lot T4 (Yvv, 2026-11-03, « Modif des textes Acte engagement »): the three seed proposals
//! I1, E1 and E2 take new texts. Such an amendment examines the seed proposal only
//! (`Version::SeedProposal`): open, put forward, never in force. Its history holds, besides
//! its text, the line that made it a seed proposal (kind `seed_proposal`, ADR 0011), which
//! changes no text: it is admitted when a seed proposal amendment applied on that very
//! version wrote it. The same day, the articles of operation leave the seed (F1, F1.1, F1.2,
//! F1.3, N1, N1.1, O1; ADR 0014): the last amendments of their texts are superseded by their
//! retirement (`retire`), and decide nothing more either.
//!
//! A seeded document is never instantiated again: once in the database, it is //! 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: //! 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; //! the Préambule becomes « Raison d'être »), new bases get it from the seed;
@@ -64,6 +73,8 @@ pub enum Version {
InForce, InForce,
/// An open proposal of the seed, at zero votes, by its label. /// An open proposal of the seed, at zero votes, by its label.
Open(&'static str), Open(&'static str),
/// Its seed proposal (ADR 0011), and nothing else: never a text in force.
SeedProposal,
} }
/// One starting text to replace, in every document instantiated from a seed. /// One starting text to replace, in every document instantiated from a seed.
@@ -86,8 +97,9 @@ pub struct Amendment {
/// The earlier amendment of the same text whose text this one replaces, if any: /// The earlier amendment of the same text whose text this one replaces, if any:
/// its revision 2 is then admitted (see the module). /// its revision 2 is then admitted (see the module).
pub follows: Option<&'static str>, pub follows: Option<&'static str>,
/// The later amendment whose text the seed gives instead, if any: this one then /// The later amendment whose text the seed gives instead, if any — or the retirement
/// decides nothing more, without an error. /// that takes the article out of the seed (`retire`): this one then decides nothing
/// more, without an error.
pub superseded_by: Option<&'static str>, pub superseded_by: Option<&'static str>,
} }
@@ -101,7 +113,7 @@ pub const AMENDMENTS: &[Amendment] = &[
to: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a", to: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
reason: "texte de départ remplacé : I1", reason: "texte de départ remplacé : I1",
follows: None, follows: None,
superseded_by: None, superseded_by: Some("2026-11-03-i1-amorce"),
}, },
// The rule articles, open proposals of the seed, after the rules applied (Yvv, 2026-11-02). // The rule articles, open proposals of the seed, after the rules applied (Yvv, 2026-11-02).
Amendment { Amendment {
@@ -124,7 +136,7 @@ pub const AMENDMENTS: &[Amendment] = &[
to: "c1facdfd0c0f3aaa94b74192e146557558fe88f66304dc6d21279714d7fb727e", to: "c1facdfd0c0f3aaa94b74192e146557558fe88f66304dc6d21279714d7fb727e",
reason: "proposition de départ réécrite : N1", reason: "proposition de départ réécrite : N1",
follows: None, follows: None,
superseded_by: None, superseded_by: Some(crate::retire::RETIREMENT),
}, },
Amendment { Amendment {
id: "2026-11-02-n1-1-regles", id: "2026-11-02-n1-1-regles",
@@ -146,7 +158,7 @@ pub const AMENDMENTS: &[Amendment] = &[
to: "4173738055af10fceb74bb6e954bdfefd7ab364480d49afbc92ea5f68825c537", to: "4173738055af10fceb74bb6e954bdfefd7ab364480d49afbc92ea5f68825c537",
reason: "proposition de départ réécrite : O1", reason: "proposition de départ réécrite : O1",
follows: None, follows: None,
superseded_by: None, superseded_by: Some(crate::retire::RETIREMENT),
}, },
// The articles that still described the floor C, after the quorum by level (ADR 0013; // The articles that still described the floor C, after the quorum by level (ADR 0013;
// Yvv, 2026-11-02 and 03; lot T). F1.1 was never amended; F1.2 and N1.1 replace the // Yvv, 2026-11-02 and 03; lot T). F1.1 was never amended; F1.2 and N1.1 replace the
@@ -197,7 +209,7 @@ pub const AMENDMENTS: &[Amendment] = &[
to: "8375b78af825e5ce5f978442519c078010ac9ad87f5cbc9358b407aa3668c5e5", to: "8375b78af825e5ce5f978442519c078010ac9ad87f5cbc9358b407aa3668c5e5",
reason: "proposition de départ réécrite : F1.1, la part du cran au-delà du quorum", reason: "proposition de départ réécrite : F1.1, la part du cran au-delà du quorum",
follows: Some("2026-11-03-f1-1-quorum"), follows: Some("2026-11-03-f1-1-quorum"),
superseded_by: None, superseded_by: Some(crate::retire::RETIREMENT),
}, },
Amendment { Amendment {
id: "2026-11-03-f1-2-phrases", id: "2026-11-03-f1-2-phrases",
@@ -242,7 +254,7 @@ pub const AMENDMENTS: &[Amendment] = &[
to: "0c3b800b0c72b1edd4b884c03c6c5479c17609dcf82a95b593d408821e28bb4e", to: "0c3b800b0c72b1edd4b884c03c6c5479c17609dcf82a95b593d408821e28bb4e",
reason: "proposition de départ réécrite : F1.2, la part du cran par défaut au-delà du quorum, à la participation habituelle", reason: "proposition de départ réécrite : F1.2, la part du cran par défaut au-delà du quorum, à la participation habituelle",
follows: Some("2026-11-03-f1-2-phrases"), follows: Some("2026-11-03-f1-2-phrases"),
superseded_by: None, superseded_by: Some(crate::retire::RETIREMENT),
}, },
Amendment { Amendment {
id: "2026-11-03-n1-1-chiffres", id: "2026-11-03-n1-1-chiffres",
@@ -253,6 +265,45 @@ pub const AMENDMENTS: &[Amendment] = &[
to: "8107c5fc746121f18c8a440e86e23310c5a59ae8e92f5a07f6b098419980263c", to: "8107c5fc746121f18c8a440e86e23310c5a59ae8e92f5a07f6b098419980263c",
reason: "proposition de départ réécrite : N1.1, sans chiffre daté ; sous le quorum de l'article, autant de « pour » que si ce quorum avait voté ; au-delà, la part du cran à la participation habituelle", reason: "proposition de départ réécrite : N1.1, sans chiffre daté ; sous le quorum de l'article, autant de « pour » que si ce quorum avait voté ; au-delà, la part du cran à la participation habituelle",
follows: Some("2026-11-03-n1-1-phrases"), follows: Some("2026-11-03-n1-1-phrases"),
superseded_by: Some(crate::retire::RETIREMENT),
},
// The three seed proposals, after Yvv's texts of 03/11 (lot T4): « Derrière une clé il y a
// un et un seul être humain vivant… » (I1, its second and third paragraphs rewritten too);
// « …qu'un seul et unique compte certifiant » (E1); « …qui s'engagent elles-mêmes, à
// scrupuleusement respecter ces trois principes fondamentaux » (E2). Each replaces the
// text of the seed proposal only, never a text in force: I1 the Raison d'être of 30/10
// (at its revision 2 in production), E1 and E2 their text of the seed (revision 1).
Amendment {
id: "2026-11-03-i1-amorce",
doc: "acte-engagement-certification",
unit: "I1",
version: Version::SeedProposal,
from: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
to: "32eb36b4c678c97d4597f0e4cc494a92c9956792839d6eb398a81c1dcc46320f",
reason: "Proposition seed réécrite : I1, la Raison d'être (texte d'Yvv du 03/11)",
follows: Some("2026-10-30-i1-raison-d-etre"),
superseded_by: None,
},
Amendment {
id: "2026-11-03-e1-amorce",
doc: "acte-engagement-certification",
unit: "E1",
version: Version::SeedProposal,
from: "bdca2713fdb1186bc59ac130f13a205a35376986f14f7cfba7223b37ed9a41bd",
to: "df31d0ec5f7f0f8f88d930347ce370634881ca62aed52beb3ffe780fe992b9c9",
reason: "Proposition seed réécrite : E1, l'unicité du compte certifiant (texte d'Yvv du 03/11)",
follows: None,
superseded_by: None,
},
Amendment {
id: "2026-11-03-e2-amorce",
doc: "acte-engagement-certification",
unit: "E2",
version: Version::SeedProposal,
from: "bde76a30edaf8b623f3de0ff7eb94ec57eb5134b3c8aeae1590798fb6b9956f0",
to: "a7dd469004c0e495524a9c38305177c352e36cd240f59a3b43e28d9222a6ae24",
reason: "Proposition seed réécrite : E2, la réciprocité des trois principes (texte d'Yvv du 03/11)",
follows: None,
superseded_by: None, superseded_by: None,
}, },
]; ];
@@ -285,6 +336,9 @@ pub struct Facts {
pub signals: i64, pub signals: i64,
/// Typo fixes someone else wrote on it, whatever became of them (2026-10-31). /// Typo fixes someone else wrote on it, whatever became of them (2026-10-31).
pub typos: i64, pub typos: i64,
/// The seed proposal amendments applied on it (ADR 0011): each wrote a line of its
/// history (kind `seed_proposal`) that changes no text.
pub put_forward: i64,
pub fingerprint: String, pub fingerprint: String,
} }
@@ -318,11 +372,17 @@ pub fn verdict(facts: Option<&Facts>, a: &Amendment, earlier: &[&str]) -> Verdic
return Verdict::Refuse(match a.version { return Verdict::Refuse(match a.version {
Version::InForce => "no_text_in_force", Version::InForce => "no_text_in_force",
Version::Open(_) => "no_open_version", Version::Open(_) => "no_open_version",
Version::SeedProposal => "no_seed_proposal",
}); });
}; };
if f.fingerprint == a.to { if f.fingerprint == a.to {
return Verdict::Present; return Verdict::Present;
} }
// A line that made the version a seed proposal changes no text: set aside, as long as a
// seed proposal amendment applied on it wrote each one (ADR 0011).
let status_lines = f.revisions.iter().filter(|k| *k == "seed_proposal").count();
let texts: Vec<&str> = f.revisions.iter().map(String::as_str).filter(|k| *k != "seed_proposal").collect();
let status_written = i64::try_from(status_lines).unwrap_or(i64::MAX) <= f.put_forward;
// The text the amendments it follows wrote: by them alone, in their order, the last // The text the amendments it follows wrote: by them alone, in their order, the last
// being the one it follows (a base born later holds the last ones only), one revision // being the one it follows (a base born later holds the last ones only), one revision
// each over the seed's. // each over the seed's.
@@ -345,7 +405,7 @@ pub fn verdict(facts: Option<&Facts>, a: &Amendment, earlier: &[&str]) -> Verdic
"votes" "votes"
} else if f.ballots > 0 { } else if f.ballots > 0 {
"ballots" "ballots"
} else if f.revisions != history || f.typos > 0 { } else if texts != history || !status_written || f.typos > 0 {
"corrected" "corrected"
} else if f.member_translations > 0 { } else if f.member_translations > 0 {
"translation_edited" "translation_edited"
@@ -411,6 +471,7 @@ fn target_of<'a>(seeds: &'a [SeedDoc], a: &Amendment) -> Option<(&'a SeedDoc, &'
// The starting text: in force, or since 2026-11-02 put forward (ADR 0011). // The starting text: in force, or since 2026-11-02 put forward (ADR 0011).
Version::InForce => v.status == "in_force" || v.seed_proposal, Version::InForce => v.status == "in_force" || v.seed_proposal,
Version::Open(label) => v.status == "open" && v.label.as_deref() == Some(label), Version::Open(label) => v.status == "open" && v.label.as_deref() == Some(label),
Version::SeedProposal => v.seed_proposal,
}) })
}); });
let src = seed.source_lang.as_str(); let src = seed.source_lang.as_str();
@@ -497,6 +558,12 @@ pub(crate) async fn facts(
.bind(proposal_id) .bind(proposal_id)
.fetch_all(&mut *conn) .fetch_all(&mut *conn)
.await?; .await?;
let put_forward: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM seed_proposal_amendments WHERE proposal_id = ? AND outcome = 'applied'",
)
.bind(proposal_id)
.fetch_one(&mut *conn)
.await?;
Ok(Some(Facts { Ok(Some(Facts {
origin: p.origin, origin: p.origin,
entered: p.entered_at.is_some(), entered: p.entered_at.is_some(),
@@ -509,6 +576,7 @@ pub(crate) async fn facts(
member_translations, member_translations,
signals, signals,
typos, typos,
put_forward,
fingerprint: fingerprint(&p.title, &p.body), fingerprint: fingerprint(&p.title, &p.body),
})) }))
} }
@@ -556,6 +624,8 @@ async fn decide(
None => open(None).fetch_optional(&mut *tx).await?.map(|id| (id, "open")), None => open(None).fetch_optional(&mut *tx).await?.map(|id| (id, "open")),
}, },
Version::Open(label) => open(Some(label)).fetch_optional(&mut *tx).await?.map(|id| (id, "open")), Version::Open(label) => open(Some(label)).fetch_optional(&mut *tx).await?.map(|id| (id, "open")),
// Its seed proposal, open and put forward; a text in force is never examined.
Version::SeedProposal => open(None).fetch_optional(&mut *tx).await?.map(|id| (id, "open")),
}; };
let target_id = examined.as_ref().map(|(id, _)| id.clone()); let target_id = examined.as_ref().map(|(id, _)| id.clone());
let found = match &examined { let found = match &examined {
@@ -874,6 +944,57 @@ mod tests {
assert_eq!(verdict(Some(&written_by(&["m5", "t", "t2"])), &third, &["m5", "t"]), Verdict::Refuse("revised")); assert_eq!(verdict(Some(&written_by(&["m5", "t", "t2"])), &third, &["m5", "t"]), Verdict::Refuse("revised"));
} }
/// A version the seed proposal amendment put forward (ADR 0011), as production holds I1,
/// E1 and E2 since M6: over the texts `applied` wrote, one line `seed_proposal` that
/// changes no text, at the same revision.
fn put_forward(applied: &[&str]) -> Facts {
let mut f = written_by(applied);
f.revisions.push("seed_proposal".into());
f.put_forward = 1;
f
}
#[test]
fn a_seed_proposal_is_rewritten_over_the_line_that_put_it_forward_and_nothing_else() {
// Lot T4 (2026-11-03): I1 follows the amendment of 30/10, E1 and E2 follow none.
let i1 = Amendment { version: Version::SeedProposal, follows: Some("r"), ..A };
let e1 = Amendment { version: Version::SeedProposal, ..A };
// Production since M6: the text of 30/10 (I1) or the seed's (E1), then put forward.
assert_eq!(verdict(Some(&put_forward(&["r"])), &i1, &["r"]), Verdict::Apply);
assert_eq!(verdict(Some(&put_forward(&[])), &e1, &[]), Verdict::Apply);
// A base born with the seed proposals: nothing put them forward, revision 1.
assert_eq!(verdict(Some(&intact()), &i1, &["r"]), Verdict::Apply);
assert_eq!(verdict(Some(&intact()), &e1, &[]), Verdict::Apply);
// The new text already there.
let present = Facts { fingerprint: "new".into(), ..put_forward(&[]) };
assert_eq!(verdict(Some(&present), &e1, &[]), Verdict::Present);
// No seed proposal: a base older than M6, or a version voted into force or withdrawn.
assert_eq!(verdict(None, &e1, &[]), Verdict::Refuse("no_seed_proposal"));
// A line `seed_proposal` that no amendment wrote, or one more than they wrote.
let unwritten = Facts { put_forward: 0, ..put_forward(&[]) };
assert_eq!(verdict(Some(&unwritten), &e1, &[]), Verdict::Refuse("corrected"));
let mut twice = put_forward(&[]);
twice.revisions.push("seed_proposal".into());
assert_eq!(verdict(Some(&twice), &e1, &[]), Verdict::Refuse("corrected"));
// A typo once put forward: a revision no amendment wrote.
let mut typo = put_forward(&["r"]);
typo.revision = 3;
typo.revisions.push("typo".into());
assert_eq!(verdict(Some(&typo), &i1, &["r"]), Verdict::Refuse("revised"));
// Any trace of a person keeps the text, with its reason.
for (f, why) in [
(Facts { votes: 1, ..put_forward(&[]) }, "votes"),
(Facts { ballots: 1, ..put_forward(&[]) }, "ballots"),
(Facts { typos: 1, ..put_forward(&[]) }, "corrected"),
(Facts { member_translations: 1, ..put_forward(&[]) }, "translation_edited"),
(Facts { signals: 1, ..put_forward(&[]) }, "signalled"),
(Facts { fingerprint: "other".into(), ..put_forward(&[]) }, "text_differs"),
(Facts { origin: "member".into(), ..put_forward(&[]) }, "not_a_seed"),
] {
assert_eq!(verdict(Some(&f), &e1, &[]), Verdict::Refuse(why), "{f:?}");
}
}
#[test] #[test]
fn the_amendments_followed_are_read_through_the_list_oldest_first() { fn the_amendments_followed_are_read_through_the_list_oldest_first() {
let by_id = |id: &str| AMENDMENTS.iter().find(|a| a.id == id).expect("amendment"); let by_id = |id: &str| AMENDMENTS.iter().find(|a| a.id == id).expect("amendment");
@@ -895,6 +1016,10 @@ mod tests {
["2026-11-02-n1-1-regles", "2026-11-03-n1-1-quorum", "2026-11-03-n1-1-phrases"] ["2026-11-02-n1-1-regles", "2026-11-03-n1-1-quorum", "2026-11-03-n1-1-phrases"]
); );
assert!(earlier(AMENDMENTS, by_id("2026-10-30-i1-raison-d-etre")).is_empty()); assert!(earlier(AMENDMENTS, by_id("2026-10-30-i1-raison-d-etre")).is_empty());
// Lot T4: I1 follows the text of 30/10; E1 and E2, the seed's.
assert_eq!(earlier(AMENDMENTS, by_id("2026-11-03-i1-amorce")), ["2026-10-30-i1-raison-d-etre"]);
assert!(earlier(AMENDMENTS, by_id("2026-11-03-e1-amorce")).is_empty());
assert!(earlier(AMENDMENTS, by_id("2026-11-03-e2-amorce")).is_empty());
// Each one followed wrote the text the next replaces, and is superseded by it. // Each one followed wrote the text the next replaces, and is superseded by it.
for a in AMENDMENTS.iter().filter(|a| a.follows.is_some()) { for a in AMENDMENTS.iter().filter(|a| a.follows.is_some()) {
let e = by_id(a.follows.expect("follows")); let e = by_id(a.follows.expect("follows"));
@@ -59,6 +59,10 @@ pub struct SeedProposalAmendment {
pub levels: &'static [u8], pub levels: &'static [u8],
/// Reason given to the publication queue, and so to the texts repository. /// Reason given to the publication queue, and so to the texts repository.
pub reason: &'static str, pub reason: &'static str,
/// The seed amendment that gives the seed proposal another text since (lot T4): the seed
/// no longer puts this one forward, and this amendment decides nothing more, without
/// an error (the decisions already taken stay).
pub superseded_by: Option<&'static str>,
} }
pub const AMENDMENTS: &[SeedProposalAmendment] = &[ pub const AMENDMENTS: &[SeedProposalAmendment] = &[
@@ -69,6 +73,7 @@ pub const AMENDMENTS: &[SeedProposalAmendment] = &[
text: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a", text: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
levels: &[3], levels: &[3],
reason: "Proposition seed : I1 sort de la vigueur", reason: "Proposition seed : I1 sort de la vigueur",
superseded_by: Some("2026-11-03-i1-amorce"),
}, },
SeedProposalAmendment { SeedProposalAmendment {
id: "2026-11-02-e1-proposition-seed", id: "2026-11-02-e1-proposition-seed",
@@ -77,6 +82,7 @@ pub const AMENDMENTS: &[SeedProposalAmendment] = &[
text: "bdca2713fdb1186bc59ac130f13a205a35376986f14f7cfba7223b37ed9a41bd", text: "bdca2713fdb1186bc59ac130f13a205a35376986f14f7cfba7223b37ed9a41bd",
levels: &[3, 4], levels: &[3, 4],
reason: "Proposition seed : E1 sort de la vigueur", reason: "Proposition seed : E1 sort de la vigueur",
superseded_by: Some("2026-11-03-e1-amorce"),
}, },
SeedProposalAmendment { SeedProposalAmendment {
id: "2026-11-02-e2-proposition-seed", id: "2026-11-02-e2-proposition-seed",
@@ -85,6 +91,7 @@ pub const AMENDMENTS: &[SeedProposalAmendment] = &[
text: "bde76a30edaf8b623f3de0ff7eb94ec57eb5134b3c8aeae1590798fb6b9956f0", text: "bde76a30edaf8b623f3de0ff7eb94ec57eb5134b3c8aeae1590798fb6b9956f0",
levels: &[3, 4], levels: &[3, 4],
reason: "Proposition seed : E2 sort de la vigueur", reason: "Proposition seed : E2 sort de la vigueur",
superseded_by: Some("2026-11-03-e2-amorce"),
}, },
]; ];
@@ -257,6 +264,15 @@ fn seed_says(seeds: &[SeedDoc], a: &SeedProposalAmendment) -> bool {
}); });
match unit { match unit {
Some((u, print)) if print == a.text && u.inertia.is_none() => true, Some((u, print)) if print == a.text && u.inertia.is_none() => true,
_ if a.superseded_by.is_some() => {
tracing::debug!(
amendment = a.id,
unit = a.unit,
superseded_by = a.superseded_by,
"seed proposal amendment superseded: nothing decided"
);
false
}
_ => { _ => {
tracing::error!( tracing::error!(
amendment = a.id, amendment = a.id,
@@ -459,6 +475,7 @@ mod tests {
text: "seed", text: "seed",
levels: &[3, 4], levels: &[3, 4],
reason: "Proposition seed : E1 sort de la vigueur", reason: "Proposition seed : E1 sort de la vigueur",
superseded_by: None,
}; };
fn intact() -> Facts { fn intact() -> Facts {
File diff suppressed because it is too large Load Diff
+38 -10
View File
@@ -442,7 +442,8 @@ async fn previews_and_assistant() {
a.login("Yvv", false).await; a.login("Yvv", false).await;
let id = doc_id(&mut a, "monnaie-libre").await; let id = doc_id(&mut a, "monnaie-libre").await;
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=all"), None).await; let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=all"), None).await;
assert_eq!(v["units"].as_array().expect("u").len(), 33); // 27 articles since lot T4 (the articles of operation out of the document, ADR 0014; E12 in).
assert_eq!(v["units"].as_array().expect("u").len(), 27);
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=wellrated"), None).await; let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=wellrated"), None).await;
assert_eq!(v["units"].as_array().expect("u").len(), 0, "nothing in force, nothing well rated at zero votes"); assert_eq!(v["units"].as_array().expect("u").len(), 0, "nothing in force, nothing well rated at zero votes");
let (_, v) = a let (_, v) = a
@@ -951,11 +952,21 @@ const PREAMBULE: [(&str, &str, &str); 3] = [
const RAISON_D_ETRE: &str = "*«\u{a0}Derrière une clé il n'y a qu'un seul être humain vivant\u{a0}; derrière un être humain, il n'y a qu'une seule clé certifiante.\u{a0}»*\n\nC'est le seul et unique propos de la certification"; const RAISON_D_ETRE: &str = "*«\u{a0}Derrière une clé il n'y a qu'un seul être humain vivant\u{a0}; derrière un être humain, il n'y a qu'une seule clé certifiante.\u{a0}»*\n\nC'est le seul et unique propos de la certification";
/// The Raison d'être since lot T4 (Yvv, 03/11): its beginning.
const RAISON_D_ETRE_T4: &str = "*«\u{a0}Derrière une clé il y a un et un seul être humain vivant\u{a0}; derrière un être humain, il y a une et une seule clé certifiante.\u{a0}»*\n\nC'est l'unique enjeu de la certification";
fn today_seed() -> Value { fn today_seed() -> Value {
let path = Config::for_tests().seed_dir.join("acte-engagement-certification.json"); let path = Config::for_tests().seed_dir.join("acte-engagement-certification.json");
serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json") serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json")
} }
/// The seed of M8 (de1b7eb): the last that gave I1 the Raison d'être of 30/10.
fn m8_seed() -> Value {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/seed-m8/acte-engagement-certification.json");
serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json")
}
/// The seed production runs, I1 in force: the amendment of 30/10 is replayed on it. /// The seed production runs, I1 in force: the amendment of 30/10 is replayed on it.
fn production_seed() -> Value { fn production_seed() -> Value {
let path = production_seed_dir().join("acte-engagement-certification.json"); let path = production_seed_dir().join("acte-engagement-certification.json");
@@ -1026,15 +1037,24 @@ async fn a_new_base_starts_with_the_raison_d_etre() {
for group in ["monnaie-libre", "essai"] { for group in ["monnaie-libre", "essai"] {
let i1 = i1_of(&mut c, group).await; let i1 = i1_of(&mut c, group).await;
assert_eq!(i1["title"], "Raison d'être"); assert_eq!(i1["title"], "Raison d'être");
assert!(i1["body"].as_str().expect("body").starts_with(RAISON_D_ETRE), "{i1}"); assert!(i1["body"].as_str().expect("body").starts_with(RAISON_D_ETRE_T4), "{i1}");
assert_eq!((i1["revision"].as_i64(), i1["seedProposal"].as_bool()), (Some(1), Some(true))); assert_eq!((i1["revision"].as_i64(), i1["seedProposal"].as_bool()), (Some(1), Some(true)));
for (lang, title) in [("en", "Purpose"), ("es", "Razón de ser")] { for (lang, title) in [("en", "Purpose"), ("es", "Razón de ser")] {
let tr = &i1["translations"][lang]; let tr = &i1["translations"][lang];
assert_eq!((tr["title"].as_str(), tr["stale"].as_bool()), (Some(title), Some(false)), "{lang}"); assert_eq!((tr["title"].as_str(), tr["stale"].as_bool()), (Some(title), Some(false)), "{lang}");
} }
} }
// Born with the text, put forward: nothing to replace, and it is said once. // Born with the text of lot T4, put forward: nothing to replace, and it is said once; the
assert_eq!(outcomes(&decisions(&state).await), [("essai", "present", None), ("monnaie-libre", "present", None)]); // amendment of 30/10, superseded, decides nothing on a new base.
assert!(decisions(&state).await.is_empty());
let t4: Vec<(String, String)> = sqlx::query_as(
"SELECT d.group_slug, s.outcome FROM seed_amendments s JOIN documents d ON d.id = s.doc_id
WHERE s.amendment_id = '2026-11-03-i1-amorce' ORDER BY d.group_slug",
)
.fetch_all(&state.db)
.await
.expect("decisions");
assert_eq!(t4, [("essai".to_owned(), "present".to_owned()), ("monnaie-libre".to_owned(), "present".to_owned())]);
} }
#[tokio::test] #[tokio::test]
@@ -1043,13 +1063,21 @@ async fn the_shipped_amendment_goes_from_the_preambule_to_the_seed_text() {
assert_eq!(a.doc, "acte-engagement-certification"); assert_eq!(a.doc, "acte-engagement-certification");
let (_, title, body) = PREAMBULE[0]; let (_, title, body) = PREAMBULE[0];
assert_eq!(ld_server::seed_amend::fingerprint(title, body), a.from, "the Préambule bases hold"); assert_eq!(ld_server::seed_amend::fingerprint(title, body), a.from, "the Préambule bases hold");
let seed = today_seed(); let i1_of = |seed: &Value| -> (String, String, Value) {
let i1 = seed["units"].as_array().expect("units").iter().find(|u| u["code"] == "I1").expect("I1"); let i1 = seed["units"].as_array().expect("units").iter().find(|u| u["code"] == "I1").expect("I1");
let v = &i1["versions"][0]; let v = &i1["versions"][0];
// Today the starting text is a seed proposal (ADR 0011); the text is the same. let (title, body) = (v["title"]["fr"].as_str().expect("title"), v["body"]["fr"].as_str().expect("body"));
(title.to_owned(), body.to_owned(), v.clone())
};
// Until M8 the starting text is a seed proposal (ADR 0011); the text is the same.
let (title, body, v) = i1_of(&m8_seed());
assert_eq!((v["status"].as_str(), v["seedProposal"].as_bool()), (Some("open"), Some(true))); assert_eq!((v["status"].as_str(), v["seedProposal"].as_bool()), (Some("open"), Some(true)));
let (title, body) = (v["title"]["fr"].as_str().expect("title"), v["body"]["fr"].as_str().expect("body")); assert_eq!(ld_server::seed_amend::fingerprint(&title, &body), a.to, "the text of the seed of M8");
assert_eq!(ld_server::seed_amend::fingerprint(title, body), a.to, "the text of the seed today"); // Lot T4 replaces it, after it only (Yvv's text of 03/11): this one decides nothing more.
let t4 = ld_server::seed_amend::AMENDMENTS.iter().find(|x| x.id == "2026-11-03-i1-amorce").expect("lot T4");
assert_eq!((t4.from, t4.follows, a.superseded_by), (a.to, Some(a.id), Some(t4.id)));
let (title, body, _) = i1_of(&today_seed());
assert_eq!(ld_server::seed_amend::fingerprint(&title, &body), t4.to, "the text of the seed today");
} }
#[tokio::test] #[tokio::test]
+4 -4
View File
@@ -794,10 +794,10 @@ async fn a_session_sees_its_own_ballots_only() {
assert_eq!((find(&mine, &i1)["myVote"].as_str(), find(&mine, &e1)["myVote"].as_str()), (Some("for"), None)); assert_eq!((find(&mine, &i1)["myVote"].as_str(), find(&mine, &e1)["myVote"].as_str()), (Some("for"), None));
let theirs = other.doc("monnaie-libre").await; let theirs = other.doc("monnaie-libre").await;
assert_eq!((find(&theirs, &i1)["myVote"].as_str(), find(&theirs, &e1)["myVote"].as_str()), (None, Some("against"))); assert_eq!((find(&theirs, &i1)["myVote"].as_str(), find(&theirs, &e1)["myVote"].as_str()), (None, Some("against")));
// « Décisions » counts the same way (33 articles, the title, 7 orders with something to decide). // « Décisions » counts the same way (27 articles, the title, 5 orders; lot T4, ADR 0014).
let d = chiara.ok("GET", "/api/groups/monnaie-libre/decisions", None).await; let d = chiara.ok("GET", "/api/groups/monnaie-libre/decisions", None).await;
assert_eq!(d["canVote"], true); assert_eq!(d["canVote"], true);
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(41), Some(40))); assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(33), Some(32)));
} }
#[tokio::test] #[tokio::test]
@@ -2271,12 +2271,12 @@ async fn on_a_base_like_production_the_three_migrations_pass_in_order_and_0010_r
let state = AppState::new(Config::for_tests(), pool.clone()).await.expect("state"); let state = AppState::new(Config::for_tests(), pool.clone()).await.expect("state");
let mut all = sqlx::migrate!("./migrations"); let mut all = sqlx::migrate!("./migrations");
all.set_ignore_missing(true); all.set_ignore_missing(true);
all.run(&pool).await.expect("0009 to 0011"); all.run(&pool).await.expect("0009 to 0012");
let order: Vec<i64> = sqlx::query_scalar("SELECT version FROM _sqlx_migrations ORDER BY rowid") let order: Vec<i64> = sqlx::query_scalar("SELECT version FROM _sqlx_migrations ORDER BY rowid")
.fetch_all(&pool) .fetch_all(&pool)
.await .await
.expect("order"); .expect("order");
assert_eq!(order, (1..=11).collect::<Vec<i64>>(), "0009, then 0010, then 0011"); assert_eq!(order, (1..=12).collect::<Vec<i64>>(), "0009, then 0010, then 0011 (and 0012 since lot T4)");
assert_eq!(tables(pool.clone()).await, written, "no row rewritten"); assert_eq!(tables(pool.clone()).await, written, "no row rewritten");
let added: Vec<(String, String, Option<String>)> = sqlx::query_as( let added: Vec<(String, String, Option<String>)> = sqlx::query_as(
"SELECT b.id, b.mode, l.outcome FROM ballots b JOIN ballot_lines l ON l.ballot_id = b.id ORDER BY b.id", "SELECT b.id, b.mode, l.outcome FROM ballots b JOIN ballot_lines l ON l.ballot_id = b.id ORDER BY b.id",
@@ -0,0 +1,793 @@
{
"_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). Aucun article en vigueur au départ. I1, E1, E2 : Propositions seed (« seedProposal »), ouvertes, mises en avant pour amorcer le vote, sans niveau d'inertie ici : elles démarrent au cran SEED_PROPOSAL_LEVEL (crates/ld-server/src/seed.rs : 2 « basse », choix d'Yvv du 2026-11-02, qui remontera leur inertie par un vote une fois les textes passés ; ADR 0011). Tant qu'aucun article n'est en vigueur, la publication attend le premier texte. Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21), inertie 3 / 5. 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, en vigueur sans vote (seed a minima ; 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). Quorums par cran (« quorums », ADR 0013, Yvv, 2026-11-02 et 03) : 12 · 12 · 50 · 50 · 50 dans Monnaie libre, 1 partout dans le groupe d'essai ; sous le quorum, les votants qui manquent comptent comme « non » ; voteFloor reste le quorum de référence de la participation p0 = 2 × voteFloor / W. Une base existante reçoit ces changements par des amendements gardés (seed_amend, seed_proposal_amend, quorum_amend), jamais en réinstanciant le document. F1.1, F1.2 et N1.1 réécrits pour le quorum par cran (ADR 0013 ; Yvv, 2026-11-02 et 03 ; lot T, textes relus dans docs/chantiers/acte-certification/textes-quorum-seed.md) : sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part du cran. Trois phrases reprises après la relecture de M6b (lot T2) : la part du cran, la plus haute au quorum, baisse quand la participation monte (F1.1) ; l'auteur fixe le quorum tant que l'article ne garde la trace d'aucun vote (F1.2) ; le quorum n'est pas un nombre minimal de votants (N1.1). Lot T3 (Yvv, 03/11 : « pas de chiffre datés dans un texte voté ») : l'exemple chiffré de N1.1 retiré (aucun nombre des textes ne dépend de W ni d'une participation) ; N1.1, sous le quorum, il faut autant de « pour » que si le quorum avait voté (vrai pour tout quorum) ; N1.1 et F1.2, la part du cran comme d'habitude au-delà du quorum. Après la relecture de 2ce2028 (lot T3b, même amendement) : N1.1, G se calcule pour que la part exigée, au-delà du quorum, soit celle du cran quand le groupe vote comme d'habitude (vrai pour tout W et tout quorum). Après la relecture de e7ab82f (lot T3c, mêmes amendements) : F1.2 et N1.1 écrivent le terme défini, « à la participation habituelle » (la médiane ramenée entre 1 % et 50 % de W, celle sur laquelle G se calcule), à la place de « comme d'habitude », faux quand la médiane sort de ses bornes ; N1.1, « sous le quorum de l'article », « la part exigée », « Tant qu'elle repose sur moins de 5 propositions », « Elle est ramenée entre 1 % et 50 % » (liste fermée des phrases : docs/chantiers/acte-certification/t3c-liste-fermee.md).",
"slug": "acte-engagement-certification",
"title": {
"fr": "Acte d'engagement Certification",
"en": "Certification Commitment Act",
"es": "Acta de compromiso de Certificación"
},
"summary": {
"fr": "Ce que chaque membre de la toile de confiance Ğ1 s'engage à respecter quand il certifie. Texte modulaire sous vote permanent : chaque article entre en vigueur dès qu'il atteint son seuil.",
"en": "What every member of the Ğ1 web of trust commits to uphold when certifying. A modular text under permanent vote: each article comes into force as soon as it reaches its threshold.",
"es": "Lo que todo miembro de la red de confianza Ğ1 se compromete a respetar cuando certifica. Texto modular en votación permanente: cada artículo entra en vigor en cuanto alcanza su umbral."
},
"sourceLang": "fr",
"instances": [
{
"group": "monnaie-libre",
"voteFloor": 50,
"quorums": [12, 12, 50, 50, 50],
"publishPath": "monnaie-libre/acte-engagement-certification"
},
{
"group": "essai",
"voteFloor": 1,
"quorums": [1, 1, 1, 1, 1],
"publishPath": null
}
],
"modules": {
"title": {
"inertia": 4
},
"order": {
"inertia": 2
}
},
"units": [
{
"code": "I1",
"section": "introduction",
"position": 1,
"versions": [
{
"label": "1.0.0",
"status": "open",
"seedProposal": true,
"title": {
"fr": "Raison d'être",
"en": "Purpose",
"es": "Razón de ser"
},
"body": {
"fr": "*« Derrière une clé il n'y a qu'un seul être humain vivant ; derrière un être humain, il n'y a qu'une seule clé certifiante. »*\n\nC'est le seul et unique propos de la certification, qui donne à notre toile fiduciaire sa valeur. Nous pouvons nous fier à l'unicité et l'intégrité de tout geste signé par cette clé. C'est l'enjeu de la certification.\n\nCet acte d'engagement est par ailleurs le seul acte de nature contractuelle qui fonde l'usage de la première monnaie libre.",
"en": "*“Behind a key there is only one living human being; behind a human being, there is only one certifying key.”*\n\nThis is the one and only purpose of certification, which gives our fiduciary web its value. We can rely on the uniqueness and integrity of every action signed with this key. That is what is at stake in certification.\n\nThis commitment act is, moreover, the only act of a contractual nature on which the use of the first libre currency is founded.",
"es": "*«Detrás de una clave no hay más que un solo ser humano vivo; detrás de un ser humano, no hay más que una sola clave certificadora.»*\n\nEste es el propósito único y exclusivo de la certificación, que da su valor a nuestra red fiduciaria. Podemos fiarnos de la unicidad y la integridad de toda acción firmada con esta clave. Eso es lo que está en juego en la certificación.\n\nEsta acta de compromiso es, además, el único acto de naturaleza contractual en el que se funda el uso de la primera moneda libre."
}
}
]
},
{
"code": "I2",
"section": "introduction",
"position": 2,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Les deux garanties réciproques",
"en": "The two reciprocal guarantees",
"es": "Las dos garantías recíprocas"
},
"body": {
"fr": "La Certification repose sur deux garanties réciproques :\n\n**1.** Derrière une clé publique créatrice de monnaie se trouve un **être humain vivant**.\n\n**2.** Derrière cet être humain se trouve **une seule et unique** clé publique créatrice de monnaie.\n\nLa Certification est un acte technique et fiduciaire, pas un acte d'adhésion morale ou de sympathie.",
"en": "Certification rests on two reciprocal guarantees:\n\n**1.** Behind a money-creating public key there is a **living human being**.\n\n**2.** Behind this human being there is **one single and unique** money-creating public key.\n\nCertification is a technical and fiduciary act, not an act of moral endorsement or of sympathy.",
"es": "La Certificación se basa en dos garantías recíprocas:\n\n**1.** Detrás de una clave pública creadora de moneda hay un **ser humano vivo**.\n\n**2.** Detrás de este ser humano hay **una sola y única** clave pública creadora de moneda.\n\nLa Certificación es un acto técnico y fiduciario, no un acto de adhesión moral ni de simpatía."
}
}
]
},
{
"code": "T1",
"section": "fondamental",
"position": 3,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Engagements fondamentaux",
"en": "Fundamental commitments",
"es": "Compromisos fundamentales"
},
"body": {
"fr": "Les engagements fondamentaux ci-dessous constituent le socle irréductible de l'acte d'engagement. Ils sont pris sur l'honneur par tout membre de la toile de confiance.",
"en": "The fundamental commitments below form the irreducible foundation of the commitment act. They are made on their honour by every member of the web of trust.",
"es": "Los compromisos fundamentales que siguen constituyen la base irreductible del acta de compromiso. Todo miembro de la red de confianza los asume por su honor."
}
}
]
},
{
"code": "E1",
"section": "fondamental",
"position": 4,
"versions": [
{
"label": "1.0.0",
"status": "open",
"seedProposal": true,
"title": {
"fr": "Unicité du compte",
"en": "Uniqueness of the account",
"es": "Unicidad de la cuenta"
},
"body": {
"fr": "Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte cocréateur de monnaie G1.",
"en": "I commit on my honour to have, now and ever, no more than one single and unique account co-creating G1 money.",
"es": "Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta cocreadora de moneda G1."
}
}
]
},
{
"code": "E2",
"section": "fondamental",
"position": 5,
"versions": [
{
"label": "1.0.0",
"status": "open",
"seedProposal": true,
"title": {
"fr": "Réciprocité",
"en": "Reciprocity",
"es": "Reciprocidad"
},
"body": {
"fr": "Je m'engage sur l'honneur à ne certifier que des personnes physiques qui respectent scrupuleusement ces deux présents engagements fondamentaux.",
"en": "I commit on my honour to certify only natural persons who scrupulously respect these two fundamental commitments.",
"es": "Me comprometo por mi honor a certificar únicamente a personas físicas que respeten escrupulosamente estos dos compromisos fundamentales."
}
}
]
},
{
"code": "T2",
"section": "technique",
"position": 6,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Engagements techniques",
"en": "Technical commitments",
"es": "Compromisos técnicos"
},
"body": {
"fr": "Les engagements techniques définissent les obligations pratiques et vérifiables du certificateur pour garantir la qualité de la toile de confiance.",
"en": "The technical commitments define the practical and verifiable obligations of the certifier to guarantee the quality of the web of trust.",
"es": "Los compromisos técnicos definen las obligaciones prácticas y verificables del certificador para garantizar la calidad de la red de confianza."
}
}
]
},
{
"code": "E3",
"section": "technique",
"position": 7,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Connaissance suffisante",
"en": "Sufficient knowledge",
"es": "Conocimiento suficiente"
},
"body": {
"fr": "Je me suis assuré de connaître suffisamment la personne qui gère cette clé publique. Connaître suffisamment ne signifie pas « avoir vu » ; c'est assurer à la communauté G1 que je pourrai la contacter facilement et être en mesure de repérer un double-compte ou tout autre problème.",
"en": "I have made sure that I know the person who manages this public key well enough. Knowing well enough does not mean “having seen”; it means assuring the G1 community that I will be able to contact them easily and to detect a duplicate account or any other problem.",
"es": "Me he asegurado de conocer suficientemente a la persona que gestiona esta clave pública. Conocer suficientemente no significa «haber visto»; significa asegurar a la comunidad G1 que podré contactarla fácilmente y estar en condiciones de detectar una cuenta duplicada o cualquier otro problema."
}
},
{
"label": "0.4.0-draft",
"status": "open",
"title": {
"fr": "Connaissance suffisante",
"en": "Sufficient knowledge",
"es": "Conocimiento suficiente"
},
"body": {
"fr": "Je me suis assuré de connaître suffisamment la personne qui gère cette clé publique : je sais la recontacter par au moins deux canaux indépendants, je suis en mesure de repérer un double-compte, et je m'engage à signaler tout doute aux experts de la communauté. « Avoir vu » quelqu'un ne suffit pas.",
"en": "I have made sure that I know the person who manages this public key well enough: I can get back in touch with them through at least two independent channels, I am able to detect a duplicate account, and I commit to reporting any doubt to the community experts. “Having seen” someone is not enough.",
"es": "Me he asegurado de conocer suficientemente a la persona que gestiona esta clave pública: puedo volver a contactarla por al menos dos canales independientes, estoy en condiciones de detectar una cuenta duplicada y me comprometo a comunicar cualquier duda a los expertos de la comunidad. «Haber visto» a alguien no basta."
}
}
]
},
{
"code": "E4",
"section": "technique",
"position": 8,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Vérification personnelle de la clé",
"en": "Personal verification of the key",
"es": "Verificación personal de la clave"
},
"body": {
"fr": "J'ai personnellement vérifié que c'est bien cette clé publique que je m'apprête à certifier, en la comparant avec la personne concernée et non par un intermédiaire.",
"en": "I have personally verified that this is indeed the public key I am about to certify, by comparing it with the person concerned and not through an intermediary.",
"es": "He verificado personalmente que esta es efectivamente la clave pública que me dispongo a certificar, comparándola con la persona interesada y no a través de un intermediario."
}
}
]
},
{
"code": "E5",
"section": "technique",
"position": 9,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Joignabilité réciproque",
"en": "Mutual reachability",
"es": "Contacto recíproco"
},
"body": {
"fr": "Je suis joignable rapidement et facilement par mes certifieurs, et je peux joindre rapidement et facilement les personnes que je certifie, par plusieurs moyens de communication différents et indépendants (physique, téléphone, email, messagerie, etc.).",
"en": "I can be reached quickly and easily by my certifiers, and I can quickly and easily reach the people I certify, through several different and independent means of communication (in person, phone, email, messaging, etc.).",
"es": "Soy localizable de forma rápida y sencilla por mis certificadores, y puedo localizar de forma rápida y sencilla a las personas que certifico, por varios medios de comunicación diferentes e independientes (en persona, teléfono, correo electrónico, mensajería, etc.)."
}
}
]
},
{
"code": "E6",
"section": "technique",
"position": 10,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Document de révocation",
"en": "Revocation document",
"es": "Documento de revocación"
},
"body": {
"fr": "J'ai vérifié avec la personne certifiée qu'elle a bien généré son document de révocation de compte et qu'elle le conserve en lieu sûr.",
"en": "I have checked with the certified person that they have indeed generated their account revocation document and that they keep it in a safe place.",
"es": "He verificado con la persona certificada que ha generado efectivamente su documento de revocación de cuenta y que lo conserva en un lugar seguro."
}
}
]
},
{
"code": "E7",
"section": "technique",
"position": 11,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Rencontre physique ou vérification multi-canaux",
"en": "In-person meeting or multi-channel verification",
"es": "Encuentro en persona o verificación multicanal"
},
"body": {
"fr": "J'ai rencontré la personne physiquement (préférable), **OU** j'ai vérifié à distance le lien personne / clé publique par plusieurs moyens de communication différents et indépendants : courrier + réseau social + forum + email + visio + téléphone.",
"en": "I have met the person face to face (preferable), **OR** I have remotely verified the person / public key link through several different and independent means of communication: postal mail + social network + forum + email + video call + phone.",
"es": "Me he encontrado físicamente con la persona (preferible), **O** he verificado a distancia el vínculo persona / clave pública por varios medios de comunicación diferentes e independientes: correo postal + red social + foro + correo electrónico + videollamada + teléfono."
}
},
{
"label": "0.4.0-draft",
"status": "open",
"title": {
"fr": "Rencontre physique ou vérification multi-canaux",
"en": "In-person meeting or multi-channel verification",
"es": "Encuentro en persona o verificación multicanal"
},
"body": {
"fr": "J'ai rencontré la personne physiquement (préférable), **OU** j'ai vérifié à distance le lien personne / clé publique par au moins trois moyens de communication différents et indépendants, dont un échange en visio en direct.",
"en": "I have met the person face to face (preferable), **OR** I have remotely verified the person / public key link through at least three different and independent means of communication, including a live video call.",
"es": "Me he encontrado físicamente con la persona (preferible), **O** he verificado a distancia el vínculo persona / clave pública por al menos tres medios de comunicación diferentes e independientes, entre ellos una videollamada en directo."
}
}
]
},
{
"code": "T3",
"section": "technique",
"position": 12,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Conseils et bonnes pratiques",
"en": "Advice and good practices",
"es": "Consejos y buenas prácticas"
},
"body": {
"fr": "Les pratiques suivantes sont fortement recommandées pour garantir la qualité et la sécurité de la toile de confiance.",
"en": "The following practices are strongly recommended to guarantee the quality and security of the web of trust.",
"es": "Las siguientes prácticas se recomiendan encarecidamente para garantizar la calidad y la seguridad de la red de confianza."
}
}
]
},
{
"code": "E8",
"section": "technique",
"position": 13,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Ne jamais certifier seul",
"en": "Never certify alone",
"es": "No certificar nunca en solitario"
},
"body": {
"fr": "Ne certifiez jamais seul, mais accompagné d'au moins un autre membre de la toile de confiance G1, pour garantir un double contrôle.",
"en": "Never certify alone, but accompanied by at least one other member of the G1 web of trust, to guarantee a double check.",
"es": "No certifique nunca en solitario, sino junto con al menos otro miembro de la red de confianza G1, para garantizar un doble control."
}
}
]
},
{
"code": "E9",
"section": "technique",
"position": 14,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Vérification des certifications existantes",
"en": "Verification of existing certifications",
"es": "Verificación de las certificaciones existentes"
},
"body": {
"fr": "Avant toute Certification, vérifiez si le compte a déjà reçu des certifications et de qui elles proviennent. Contactez les certifieurs existants en cas de doute. Si un certifieurs existant ne connaît pas la personne, alertez immédiatement les experts de la communauté.",
"en": "Before any Certification, check whether the account has already received certifications and who they come from. Contact the existing certifiers if in doubt. If an existing certifier does not know the person, alert the community experts immediately.",
"es": "Antes de cualquier Certificación, verifique si la cuenta ya ha recibido certificaciones y de quién proceden. En caso de duda, contacte con los certificadores existentes. Si un certificador existente no conoce a la persona, alerte de inmediato a los expertos de la comunidad."
}
}
]
},
{
"code": "E10",
"section": "technique",
"position": 15,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Vérification de maîtrise du compte",
"en": "Verification of account control",
"es": "Verificación del control de la cuenta"
},
"body": {
"fr": "Vérifiez la maîtrise du compte par un transfert-test : envoyez quelques G1 et demandez le renvoi, afin de vous assurer que la personne contrôle bien sa clé privée.",
"en": "Verify control of the account with a test transfer: send a few G1 and ask for them to be sent back, to make sure that the person really controls their private key.",
"es": "Verifique el control de la cuenta mediante una transferencia de prueba: envíe unos pocos G1 y pida que se los devuelvan, para asegurarse de que la persona controla realmente su clave privada."
}
}
]
},
{
"code": "E11",
"section": "technique",
"position": 16,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Transmission et compréhension du document",
"en": "Transmission and understanding of the document",
"es": "Transmisión y comprensión del documento"
},
"body": {
"fr": "Vérifiez que vos contacts ont bien étudié et compris le présent acte d'engagement dans sa version à jour avant de procéder à la Certification.",
"en": "Check that your contacts have indeed studied and understood the current version of this commitment act before proceeding with the Certification.",
"es": "Verifique que sus contactos han estudiado y comprendido efectivamente la versión vigente de la presente acta de compromiso antes de proceder a la Certificación."
}
}
]
},
{
"code": "K1",
"section": "conclusion",
"position": 17,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Règles abrégées de la toile de confiance",
"en": "Abridged rules of the web of trust",
"es": "Reglas abreviadas de la red de confianza"
},
"body": {
"fr": "**Paramètres protocolaires en vigueur :**\n\n- Stock de **100 certifications** possibles\n- 1 certification émissible tous les **5 jours**\n- Nouveau compte valide si **>= 5 certifications** reçues en **2 mois**\n- Condition de distance : **<= 5 pas** de **80% des sentinelles**\n- Sentinelle : membre ayant reçu et émis >= Y[N] certifs (Y = ceil(N^(1/5)))\n- Certifications actives valables **2 ans**\n- Renouvellement de l'accord tous les **12 mois**\n\n*Note : le vote porte sur l'inclusion de ces règles dans le document, pas sur les valeurs des variables protocolaires elles-mêmes, qui sont fixées par le protocole Duniter.*",
"en": "**Protocol parameters in force:**\n\n- Stock of **100 certifications** available\n- 1 certification can be issued every **5 days**\n- New account valid if **>= 5 certifications** received within **2 months**\n- Distance rule: **<= 5 steps** from **80% of the sentries**\n- Sentry: member who has received and issued >= Y[N] certifications (Y = ceil(N^(1/5)))\n- Active certifications valid for **2 years**\n- Renewal of the agreement every **12 months**\n\n*Note: the vote concerns the inclusion of these rules in the document, not the values of the protocol variables themselves, which are set by the Duniter protocol.*",
"es": "**Parámetros de protocolo vigentes:**\n\n- Reserva de **100 certificaciones** posibles\n- 1 certificación emitible cada **5 días**\n- Cuenta nueva válida si recibe **>= 5 certificaciones** en **2 meses**\n- Regla de distancia: **<= 5 pasos** del **80% de los centinelas**\n- Centinela: miembro que ha recibido y emitido >= Y[N] certificaciones (Y = ceil(N^(1/5)))\n- Certificaciones activas válidas durante **2 años**\n- Renovación del acuerdo cada **12 meses**\n\n*Nota: la votación se refiere a la inclusión de estas reglas en el documento, no a los valores de las variables del protocolo en sí, que fija el protocolo Duniter.*"
}
}
]
},
{
"code": "K2",
"section": "conclusion",
"position": 18,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Monnaie G1",
"en": "G1 currency",
"es": "Moneda G1"
},
"body": {
"fr": "**Paramètres monétaires :**\n\n- 1 Dividende Universel (DU) par personne par jour\n- Réévaluation à chaque équinoxe : `DU(n+1) = DU(n) + c² × (M/N) / 182.625` avec c = 4.88%\n- DU(0) = 10.00 G1\n\n*Note : le vote porte sur l'inclusion de ces paramètres dans le document, pas sur les valeurs monétaires elles-mêmes, qui découlent de la TRM et du bloc 0.*",
"en": "**Monetary parameters:**\n\n- 1 Universal Dividend (UD) per person per day\n- Revaluation at each equinox: `DU(n+1) = DU(n) + c² × (M/N) / 182.625` with c = 4.88%\n- DU(0) = 10.00 G1\n\n*Note: the vote concerns the inclusion of these parameters in the document, not the monetary values themselves, which derive from the RTM and block 0.*",
"es": "**Parámetros monetarios:**\n\n- 1 Dividendo Universal (DU) por persona y día\n- Revalorización en cada equinoccio: `DU(n+1) = DU(n) + c² × (M/N) / 182.625` con c = 4.88%\n- DU(0) = 10.00 G1\n\n*Nota: la votación se refiere a la inclusión de estos parámetros en el documento, no a los valores monetarios en sí, que se derivan de la TRM y del bloque 0.*"
}
}
]
},
{
"code": "X1",
"section": "annexe",
"position": 19,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Annexe 1 : Intégration logicielle",
"en": "Annex 1: Software integration",
"es": "Anexo 1: Integración en el software"
},
"body": {
"fr": "Les obligations pour les logiciels implémentant la Certification G1.",
"en": "The obligations for software implementing G1 Certification.",
"es": "Las obligaciones del software que implementa la Certificación G1."
}
}
]
},
{
"code": "X1.1",
"section": "annexe",
"position": 20,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Dépôt de référence",
"en": "Reference repository",
"es": "Repositorio de referencia"
},
"body": {
"fr": "Le texte de référence de l'acte d'engagement Certification est hébergé dans le dépôt git officiel : https://git.duniter.org/documents/g1_monetary_license\n\nLes applications Cesium, Gecko et toute application de Certification doivent pointer vers ce dépôt pour afficher la version en vigueur.",
"en": "The reference text of the Certification commitment act is hosted in the official git repository: https://git.duniter.org/documents/g1_monetary_license\n\nThe Cesium and Gecko applications, and any Certification application, must point to this repository to display the version in force.",
"es": "El texto de referencia del acta de compromiso de Certificación está alojado en el repositorio git oficial: https://git.duniter.org/documents/g1_monetary_license\n\nLas aplicaciones Cesium, Gecko y cualquier aplicación de Certificación deben apuntar a este repositorio para mostrar la versión vigente."
}
}
]
},
{
"code": "X1.2",
"section": "annexe",
"position": 21,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Obligation de transmission",
"en": "Transmission obligation",
"es": "Obligación de transmisión"
},
"body": {
"fr": "Tout logiciel G1 permettant la Certification doit transmettre le présent acte d'engagement au certifié et en afficher les paramètres du bloc 0 de la blockchain Duniter.",
"en": "Any G1 software that enables Certification must transmit this commitment act to the certified person and display the block 0 parameters of the Duniter blockchain.",
"es": "Todo software G1 que permita la Certificación debe transmitir la presente acta de compromiso a la persona certificada y mostrar los parámetros del bloque 0 de la blockchain Duniter."
}
}
]
},
{
"code": "X1.3",
"section": "annexe",
"position": 22,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Logiciel auditable",
"en": "Auditable software",
"es": "Software auditable"
},
"body": {
"fr": "Tout logiciel utilisé pour la Certification ou la création monétaire doit être publié sous licence libre, afin de permettre son audit par la communauté.",
"en": "Any software used for Certification or money creation must be published under a free licence, so that it can be audited by the community.",
"es": "Todo software utilizado para la Certificación o la creación monetaria debe publicarse bajo licencia libre, para permitir su auditoría por parte de la comunidad."
}
}
]
},
{
"code": "X2",
"section": "annexe",
"position": 23,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Annexe 2 : Questions à la Certification",
"en": "Annex 2: Questions for Certification",
"es": "Anexo 2: Preguntas para la Certificación"
},
"body": {
"fr": "Liste de questions à présenter dans les logiciels de Certification.Inspirée de la checklist de la Charte 1.0 et des discussions forum (topic 32412).",
"en": "List of questions to be presented in Certification software. Inspired by the checklist of Charter 1.0 and by forum discussions (topic 32412).",
"es": "Lista de preguntas que deben presentarse en el software de Certificación. Inspirada en la lista de verificación de la Carta 1.0 y en los debates del foro (tema 32412)."
}
}
]
},
{
"code": "X2.1",
"section": "annexe",
"position": 24,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Questions piège (réponse attendue : NON)",
"en": "Trap questions (expected answer: NO)",
"es": "Preguntas trampa (respuesta esperada: NO)"
},
"body": {
"fr": "**Si OUI à l'une de ces questions, la Certification doit être refusée.**\n\n- La personne m'a contacté uniquement pour obtenir ma Certification\n- Je certifie sous la pression ou pour faire plaisir\n- Je n'ai aucun moyen de vérifier l'identité de la personne\n- La personne refuse de me fournir plusieurs moyens de contact\n- Je soupçonne que la personne possède déjà un compte membre\n- Je ne connais aucun de ses autres certifieurs\n- La personne ne comprend pas ce qu'est la monnaie libre\n- La personne n'a pas lu le présent acte d'engagement",
"en": "**If YES to any of these questions, the Certification must be refused.**\n\n- The person contacted me only to obtain my Certification\n- I am certifying under pressure or to please someone\n- I have no way of verifying the person's identity\n- The person refuses to give me several means of contact\n- I suspect that the person already has a member account\n- I do not know any of their other certifiers\n- The person does not understand what libre currency is\n- The person has not read this commitment act",
"es": "**Si la respuesta es SÍ a alguna de estas preguntas, la Certificación debe rechazarse.**\n\n- La persona me ha contactado únicamente para obtener mi Certificación\n- Certifico bajo presión o por complacer\n- No tengo ningún medio para verificar la identidad de la persona\n- La persona se niega a proporcionarme varios medios de contacto\n- Sospecho que la persona ya posee una cuenta de miembro\n- No conozco a ninguno de sus otros certificadores\n- La persona no entiende qué es la moneda libre\n- La persona no ha leído la presente acta de compromiso"
}
}
]
},
{
"code": "X2.2",
"section": "annexe",
"position": 25,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Questions identité (réponse attendue : OUI)",
"en": "Identity questions (expected answer: YES)",
"es": "Preguntas de identidad (respuesta esperada: SÍ)"
},
"body": {
"fr": "**Si NON à l'une de ces questions, la Certification doit être refusée.**\n\n- Je connais cette personne suffisamment pour la recontacter\n- J'ai vérifié personnellement le lien personne / clé publique\n- La personne est une personne physique vivante (pas une entité morale)\n- Je pourrais reconnaître cette personne si je la croisais\n- J'ai au moins 2 moyens de contact différents pour cette personne",
"en": "**If NO to any of these questions, the Certification must be refused.**\n\n- I know this person well enough to get back in touch with them\n- I have personally verified the person / public key link\n- The person is a living natural person (not a legal entity)\n- I would recognise this person if I came across them\n- I have at least 2 different means of contact for this person",
"es": "**Si la respuesta es NO a alguna de estas preguntas, la Certificación debe rechazarse.**\n\n- Conozco a esta persona suficientemente para volver a contactarla\n- He verificado personalmente el vínculo persona / clave pública\n- La persona es una persona física viva (no una persona jurídica)\n- Reconocería a esta persona si me la cruzara\n- Tengo al menos 2 medios de contacto diferentes para esta persona"
}
}
]
},
{
"code": "X2.3",
"section": "annexe",
"position": 26,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Questions sécurité (réponse attendue : OUI)",
"en": "Security questions (expected answer: YES)",
"es": "Preguntas de seguridad (respuesta esperada: SÍ)"
},
"body": {
"fr": "**Si NON, la Certification est déconseillée.**\n\n- La personne a généré son document de révocation\n- La personne maîtrise effectivement son compte (test de transfert effectué)\n- La personne sait où retrouver le présent acte d'engagement dans sa version à jour",
"en": "**If NO, the Certification is not recommended.**\n\n- The person has generated their revocation document\n- The person actually controls their account (test transfer carried out)\n- The person knows where to find the current version of this commitment act",
"es": "**Si la respuesta es NO, se desaconseja la Certificación.**\n\n- La persona ha generado su documento de revocación\n- La persona controla efectivamente su cuenta (transferencia de prueba realizada)\n- La persona sabe dónde encontrar la versión vigente de la presente acta de compromiso"
}
}
]
},
{
"code": "F1",
"section": "formule",
"position": 27,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Formule de vote",
"en": "Voting formula",
"es": "Fórmula de votación"
},
"body": {
"fr": "La formule qui régit l'adoption ou le rejet de chaque modification du présent document. Référence : g1vote (https://g1vote-view-237903.pages.duniter.org/).",
"en": "The formula that governs the adoption or rejection of each amendment to this document. Reference: g1vote (https://g1vote-view-237903.pages.duniter.org/).",
"es": "La fórmula que rige la adopción o el rechazo de cada modificación del presente documento. Referencia: g1vote (https://g1vote-view-237903.pages.duniter.org/)."
}
}
]
},
{
"code": "F1.1",
"section": "formule",
"position": 28,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Formule du seuil WoT",
"en": "WoT threshold formula",
"es": "Fórmula del umbral WoT"
},
"body": {
"fr": "Le seuil est le nombre de voix pour qu'il faut à une version pour entrer en vigueur. Sous le quorum Q, les votants qui manquent comptent comme « non » : le seuil se calcule comme si Q personnes avaient voté. Au-delà, il faut la part de « pour » que demande le cran d'inertie de l'article, parmi tous les votants : cette part est la plus haute au quorum et baisse quand la participation monte, jusqu'à la majorité M quand tout le monde vote.\n\n`T' = max(T, Q)`\n`Seuil = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUne version passe quand ses voix pour atteignent le seuil, arrondi à l'entier au-dessus.\n\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q** : le quorum de l'article ; **T'** : les votants retenus, T, ou Q s'il en manque.\n- **W** : le nombre de membres de la toile de confiance Ğ1.\n- **B** : la base ; à la puissance W, elle est si petite qu'elle ne compte pas.\n- **M** : la majorité demandée quand tout le monde vote.\n- **G** : le gradient : plus il est grand, plus la part demandée monte quand peu de gens votent.\n\nLeurs valeurs sont dans « Paramètres par défaut » et « Niveaux d'inertie ».",
"en": "The threshold is the number of votes in favour a version needs to come into force. Below the quorum Q, the voters who are missing count as votes against: the threshold is worked out as if Q people had voted. Beyond it, it takes the share of votes in favour that the article's level of inertia asks for, among all the voters: that share is highest at the quorum and falls as turnout rises, down to the majority M when everyone votes.\n\n`T' = max(T, Q)`\n`Threshold = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nA version passes when its votes in favour reach the threshold, rounded up to a whole number.\n\n- **T**: the votes cast on the version, for and against.\n- **Q**: the article's quorum; **T'**: the voters counted, T, or Q if some are missing.\n- **W**: the number of members of the Ğ1 web of trust.\n- **B**: the base; raised to the power W, it is so small that it does not count.\n- **M**: the majority asked for when everyone votes.\n- **G**: the gradient: the larger it is, the higher the share asked for when few people vote.\n\nTheir values are in “Default parameters” and “Inertia levels”.",
"es": "El umbral es el número de votos a favor que necesita una versión para entrar en vigor. Por debajo del quórum Q, los votantes que faltan cuentan como «no»: el umbral se calcula como si hubieran votado Q personas. Más allá, hace falta la proporción de votos a favor que pide el nivel de inercia del artículo, entre todos los votantes: esa proporción es la más alta en el quórum y baja cuando sube la participación, hasta la mayoría M cuando vota todo el mundo.\n\n`T' = max(T, Q)`\n`Umbral = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUna versión pasa cuando sus votos a favor alcanzan el umbral, redondeado al entero superior.\n\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**: el quórum del artículo; **T'**: los votantes contados, T, o Q si faltan.\n- **W**: el número de miembros de la red de confianza Ğ1.\n- **B**: la base; elevada a la potencia W, es tan pequeña que no cuenta.\n- **M**: la mayoría pedida cuando vota todo el mundo.\n- **G**: el gradiente: cuanto mayor es, más alta es la proporción pedida cuando vota poca gente.\n\nSus valores están en «Parámetros por defecto» y «Niveles de inercia»."
}
}
]
},
{
"code": "F1.2",
"section": "formule",
"position": 29,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Paramètres par défaut",
"en": "Default parameters",
"es": "Parámetros por defecto"
},
"body": {
"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- **Q**, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au cran 5. L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote. Dans Monnaie libre, jamais moins de 2 : 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 %, un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » à sa participation habituelle. Tout nouvel article commence à ce cran.\n- Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, 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- **Q**, the quorum: below this number of voters, those who are missing count as votes against. Each article takes the one of its level: 12, 12, 50, 50 and 50 in Monnaie libre, from level 1 to level 5. The author of the first version of an article, an administrator for the starting texts, can set another one as long as the article keeps no trace of any vote. In Monnaie libre, never less than 2: 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%, a quorum of 50 in Monnaie libre, and, beyond the quorum, the group asks for 75% of votes in favour at its usual turnout. Every new article starts at this level.\n- At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, 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- **Q**, el quórum: por debajo de este número de votantes, los que faltan cuentan como «no». Cada artículo toma el de su nivel: 12, 12, 50, 50 y 50 en Monnaie libre, del nivel 1 al nivel 5. El autor de la primera versión de un artículo, un administrador para los textos de partida, puede fijar otro mientras el artículo no guarde rastro de ningún voto. En Monnaie libre, nunca menos de 2: 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 %, un quórum de 50 en Monnaie libre, y, más allá del quórum, el grupo pide un 75 % de votos a favor con su participación habitual. Todo artículo nuevo empieza en este nivel.\n- Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3."
}
}
]
},
{
"code": "F1.3",
"section": "formule",
"position": 30,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Processus de dépôt officiel",
"en": "Official filing process",
"es": "Proceso de depósito oficial"
},
"body": {
"fr": "Lorsqu'une proposition alternative atteint le seuil d'adoption :\n\n1. Le texte de remplacement est intégré au document officiel\n2. Le hash IPFS du document mis à jour est calculé\n3. Le hash est ancré on-chain via `system.remark`\n4. Le dépôt git officiel est synchronisé\n5. Les applications (Cesium, Gecko) mettent à jour automatiquement",
"en": "When an alternative proposal reaches the adoption threshold:\n\n1. The replacement text is incorporated into the official document\n2. The IPFS hash of the updated document is computed\n3. The hash is anchored on-chain via `system.remark`\n4. The official git repository is synchronised\n5. The applications (Cesium, Gecko) update automatically",
"es": "Cuando una propuesta alternativa alcanza el umbral de adopción:\n\n1. El texto de sustitución se integra en el documento oficial\n2. Se calcula el hash IPFS del documento actualizado\n3. El hash se ancla on-chain mediante `system.remark`\n4. Se sincroniza el repositorio git oficial\n5. Las aplicaciones (Cesium, Gecko) se actualizan automáticamente"
}
}
]
},
{
"code": "N1",
"section": "inertie",
"position": 31,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Réglage de l'inertie",
"en": "Inertia setting",
"es": "Ajuste de la inercia"
},
"body": {
"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."
}
}
]
},
{
"code": "N1.1",
"section": "inertie",
"position": 32,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Niveaux d'inertie",
"en": "Inertia levels",
"es": "Niveles de inercia"
},
"body": {
"fr": "À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). Chaque cran a aussi son quorum : sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté. La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà de ce quorum, 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)`, où T' est le nombre de votants, ou le quorum de l'article s'il en manque.\n\n**Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum de l'article, à la participation habituelle, puis quand tout le monde vote, et ce qui est à ce cran au départ.\n\n- **Cran 1 · très basse** : quorum 12 ; 60 % à la participation habituelle, 50 % quand tout le monde vote. Aucun article au départ.\n- **Cran 2 · basse** : quorum 12 ; 67 % à la participation habituelle, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.\n- **Cran 3 · moyenne** : quorum 50 ; 75 % à la participation habituelle, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.\n- **Cran 4 · haute** : quorum 50 ; 83 % à la participation habituelle, 60 % quand tout le monde vote. Le titre du document.\n- **Cran 5 · très haute** : quorum 50 ; 90 % à la participation habituelle, 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 qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de référence du document : 100 votants dans Monnaie libre. Elle ne dépend pas du quorum d'un article. Elle est ramenée 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, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W.",
"en": "At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). Each level also has its quorum: below the article's quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if that quorum had voted. The share asked depends on the level and on the number of voters, compared with the group's usual turnout: beyond that quorum, 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)`, where T' is the number of voters, or the article's quorum if some are missing.\n\n**The five levels**: the quorum in Monnaie libre, the share asked beyond the article's quorum, at the usual turnout, then when everyone votes, and what is at that level at the start.\n\n- **Level 1 · very low**: quorum 12; 60% at the usual turnout, 50% when everyone votes. No article at the start.\n- **Level 2 · low**: quorum 12; 67% at the usual turnout, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.\n- **Level 3 · medium**: quorum 50; 75% at the usual turnout, 55% when everyone votes. The default level: all other articles, and every new article.\n- **Level 4 · high**: quorum 50; 83% at the usual turnout, 60% when everyone votes. The title of the document.\n- **Level 5 · very high**: quorum 50; 90% at the usual turnout, 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. While it rests on fewer than 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It is kept between 1% and 50% of the electorate, and is measured once a day.\n\n**G** is computed so that the share asked, beyond the article's quorum, is exactly that of the level at the usual turnout: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is that share of the level (60, 67, 75, 83 or 90%) and p̃ the usual turnout divided by W.",
"es": "En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). Cada nivel tiene además su quórum: por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum. La proporción exigida depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá de ese quórum, 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)`, donde T' es el número de votantes, o el quórum del artículo si faltan.\n\n**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida más allá del quórum del artículo, con la participación habitual, luego cuando vota todo el mundo, y lo que está en ese nivel al inicio.\n\n- **Nivel 1 · muy baja**: quórum 12; 60 % con la participación habitual, 50 % cuando vota todo el mundo. Ningún artículo al inicio.\n- **Nivel 2 · baja**: quórum 12; 67 % con la participación habitual, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.\n- **Nivel 3 · media**: quórum 50; 75 % con la participación habitual, 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**: quórum 50; 83 % con la participación habitual, 60 % cuando vota todo el mundo. El título del documento.\n- **Nivel 5 · muy alta**: quórum 50; 90 % con la participación habitual, 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. Mientras se base en menos de 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se acota 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, más allá del quórum del artículo, sea exactamente la del nivel con la participación habitual: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es esa proporción del nivel (60, 67, 75, 83 o 90 %) y p̃ la participación habitual dividida por W."
}
}
]
},
{
"code": "O1",
"section": "ordonnancement",
"position": 33,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Ordonnancement du document",
"en": "Ordering of the document",
"es": "Ordenación del documento"
},
"body": {
"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."
}
}
]
}
]
}
+3 -3
View File
@@ -112,7 +112,7 @@ async fn the_five_levels_and_the_scale_are_served() {
let (mut c, _) = Client::new().await; let (mut c, _) = Client::new().await;
c.login("Chiara").await; c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await; let doc = c.doc("monnaie-libre").await;
// 3 everywhere, low for the three seed proposals (ADR 0011). // 3 everywhere, low for the four seed proposals (ADR 0011; E12 since lot T4).
let other: Vec<&str> = doc["units"] let other: Vec<&str> = doc["units"]
.as_array() .as_array()
.expect("units") .expect("units")
@@ -120,8 +120,8 @@ async fn the_five_levels_and_the_scale_are_served() {
.filter(|u| u["inertia"] != 3) .filter(|u| u["inertia"] != 3)
.map(|u| u["code"].as_str().expect("code")) .map(|u| u["code"].as_str().expect("code"))
.collect(); .collect();
assert_eq!(other, ["I1", "E1", "E2"]); assert_eq!(other, ["I1", "E1", "E12", "E2"]);
assert!(["I1", "E1", "E2"].iter().all(|c| unit(&doc, c)["inertia"] == LOW)); assert!(["I1", "E1", "E12", "E2"].iter().all(|c| unit(&doc, c)["inertia"] == LOW));
// No vote yet: the reference, 2C/W = 100 voters out of 5745. // No vote yet: the reference, 2C/W = 100 voters out of 5745.
let p = &doc["doc"]["participation"]; let p = &doc["doc"]["participation"];
+58 -43
View File
@@ -146,8 +146,9 @@ async fn a_new_base_has_its_title_and_the_order_of_each_section() {
c.login("Chiara").await; c.login("Chiara").await;
for group in ["monnaie-libre", "essai"] { for group in ["monnaie-libre", "essai"] {
let doc = c.doc(group).await; let doc = c.doc(group).await;
// The articles stay the articles: 33 of them, none of the modules. // The articles stay the articles: 27 of them, none of the modules (since lot T4, the
assert_eq!(doc["units"].as_array().expect("units").len(), 33, "{group}"); // rules of operation are out of the document, ADR 0014).
assert_eq!(doc["units"].as_array().expect("units").len(), 27, "{group}");
let title = &doc["modules"]["title"]; let title = &doc["modules"]["title"];
assert_eq!( assert_eq!(
(title["kind"].as_str(), title["code"].as_str(), title["inertia"].as_u64()), (title["kind"].as_str(), title["code"].as_str(), title["inertia"].as_u64()),
@@ -163,27 +164,15 @@ async fn a_new_base_has_its_title_and_the_order_of_each_section() {
// One order per section, in the order of the document, each at level 2. // One order per section, in the order of the document, each at level 2.
let orders = doc["modules"]["orders"].as_array().expect("orders"); let orders = doc["modules"]["orders"].as_array().expect("orders");
let sections: Vec<&str> = orders.iter().filter_map(|o| o["section"].as_str()).collect(); let sections: Vec<&str> = orders.iter().filter_map(|o| o["section"].as_str()).collect();
assert_eq!( assert_eq!(sections, ["introduction", "fondamental", "technique", "conclusion", "annexe"]);
sections,
[
"introduction",
"fondamental",
"technique",
"conclusion",
"annexe",
"formule",
"inertie",
"ordonnancement"
]
);
assert!(orders.iter().all(|o| o["inertia"] == 2 && o["inForce"]["seedMinimal"] == true)); assert!(orders.iter().all(|o| o["inertia"] == 2 && o["inForce"]["seedMinimal"] == true));
// Its version in force is the order the section reads in now. // Its version in force is the order the section reads in now.
let fondamental = order_of(&doc, "fondamental"); let fondamental = order_of(&doc, "fondamental");
assert_eq!(codes(&doc, &fondamental["members"]), ["T1", "E1", "E2"]); assert_eq!(codes(&doc, &fondamental["members"]), ["T1", "E1", "E12", "E2"]);
assert_eq!(fondamental["inForce"]["order"]["ids"], fondamental["members"]); assert_eq!(fondamental["inForce"]["order"]["ids"], fondamental["members"]);
assert_eq!(fondamental["moot"], false); // Every section holds two articles or more: each order has something to decide
// « Ordonnancement » holds one article: nothing to decide. // (one of a single article: `an_order_of_one_article_has_nothing_to_decide`).
assert_eq!(order_of(&doc, "ordonnancement")["moot"], true); assert!(orders.iter().all(|o| o["moot"] == false), "{group}");
} }
// Born with them: the addition says so, once per document. // Born with them: the addition says so, once per document.
let rows: Vec<(String, String)> = sqlx::query_as( let rows: Vec<(String, String)> = sqlx::query_as(
@@ -197,11 +186,35 @@ async fn a_new_base_has_its_title_and_the_order_of_each_section() {
let doc = c.doc("monnaie-libre").await; let doc = c.doc("monnaie-libre").await;
let id = doc["doc"]["id"].as_str().expect("id"); let id = doc["doc"]["id"].as_str().expect("id");
let all = c.ok("GET", &format!("/api/docs/{id}/preview?mode=all"), None).await; let all = c.ok("GET", &format!("/api/docs/{id}/preview?mode=all"), None).await;
assert_eq!(all["units"].as_array().expect("units").len(), 33); assert_eq!(all["units"].as_array().expect("units").len(), 27);
// The list of the group: the articles in force — none, I1, E1 and E2 are seed proposals // The list of the group: the articles in force — none, I1, E1, E12 and E2 are seed
// (ADR 0011), three more open versions —; no module has an open proposal yet. // proposals (ADR 0011) —, one open version per article and two more (E3, E7); no module
// has an open proposal yet.
let g = c.ok("GET", "/api/groups/monnaie-libre", None).await; let g = c.ok("GET", "/api/groups/monnaie-libre", None).await;
assert_eq!((g["docs"][0]["inForce"].as_u64(), g["docs"][0]["open"].as_u64()), (Some(0), Some(35))); assert_eq!((g["docs"][0]["inForce"].as_u64(), g["docs"][0]["open"].as_u64()), (Some(0), Some(29)));
}
/// The seed of M8 (tests/fixtures/seed-m8): « Ordonnancement » held one article, O1. Lot T4
/// takes that section out of the document (ADR 0014); a base born on that seed still has it.
fn m8_config() -> Config {
let mut cfg = Config::for_tests();
cfg.seed_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-m8");
cfg
}
#[tokio::test]
async fn an_order_of_one_article_has_nothing_to_decide() {
let db = open_db("sqlite::memory:").await.expect("db");
let state = AppState::new(m8_config(), db).await.expect("state");
seed::ensure(&state).await.expect("seed");
let mut c = Client::of(&state);
c.login("Chiara").await;
for group in ["monnaie-libre", "essai"] {
let doc = c.doc(group).await;
assert_eq!(codes(&doc, &order_of(&doc, "ordonnancement")["members"]), ["O1"], "{group}");
assert_eq!(order_of(&doc, "ordonnancement")["moot"], true, "{group}");
assert_eq!(order_of(&doc, "conclusion")["moot"], false, "{group}");
}
} }
#[tokio::test] #[tokio::test]
@@ -251,7 +264,7 @@ async fn an_order_is_proposed_with_the_articles_of_its_section_then_voted_into_f
assert_eq!((back["status"].as_str(), back["origin"].as_str()), (Some("open"), Some("seed"))); assert_eq!((back["status"].as_str(), back["origin"].as_str()), (Some("open"), Some("seed")));
assert_eq!(codes(&doc, &back["order"]["ids"]), ["K1", "K2"]); assert_eq!(codes(&doc, &back["order"]["ids"]), ["K1", "K2"]);
// Nothing else moved: the other sections read as before, the texts are the same. // Nothing else moved: the other sections read as before, the texts are the same.
assert_eq!(read_order(&doc, "fondamental"), ["T1", "E1", "E2"]); assert_eq!(read_order(&doc, "fondamental"), ["T1", "E1", "E12", "E2"]);
assert_eq!(read_order(&doc, "technique")[0], "T2"); assert_eq!(read_order(&doc, "technique")[0], "T2");
} }
@@ -259,12 +272,12 @@ async fn an_order_is_proposed_with_the_articles_of_its_section_then_voted_into_f
async fn articles_that_enter_or_leave_a_section_complete_an_order_under_vote() { async fn articles_that_enter_or_leave_a_section_complete_an_order_under_vote() {
let (mut yvv, mut chiara, state) = two().await; let (mut yvv, mut chiara, state) = two().await;
// The order stays under vote with one voice. // The order stays under vote with one voice.
two_voices(&state, "ordre.inertie").await; two_voices(&state, "ordre.conclusion").await;
let doc = yvv.doc("essai").await; let doc = yvv.doc("essai").await;
let module = order_of(&doc, "inertie")["id"].as_str().expect("id").to_owned(); let module = order_of(&doc, "conclusion")["id"].as_str().expect("id").to_owned();
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned(); let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let (n1, n11) = (id_of(&doc, "N1"), id_of(&doc, "N1.1")); let (k1, k2) = (id_of(&doc, "K1"), id_of(&doc, "K2"));
let p = yvv.ok("POST", &format!("/api/units/{module}/order"), Some(json!({ "order": [n11, n1] }))).await["id"] let p = yvv.ok("POST", &format!("/api/units/{module}/order"), Some(json!({ "order": [k2, k1] }))).await["id"]
.as_str() .as_str()
.expect("id") .expect("id")
.to_owned(); .to_owned();
@@ -286,20 +299,22 @@ async fn articles_that_enter_or_leave_a_section_complete_an_order_under_vote() {
.ok( .ok(
"POST", "POST",
&format!("/api/docs/{doc_id}/proposals"), &format!("/api/docs/{doc_id}/proposals"),
Some(json!({ "section": "inertie", "title": "Inertie des modules", "body": "Le titre du document est à 4.", "submit": true })), Some(
json!({ "section": "conclusion", "title": "Rappel", "body": "Un rappel pour finir.", "submit": true }),
),
) )
.await; .await;
let doc = yvv.doc("essai").await; let doc = yvv.doc("essai").await;
let fresh = unit(&doc, "N1")["code"] let fresh = unit(&doc, "K1")["code"]
.as_str() .as_str()
.map(|_| ()) .map(|_| ())
.and_then(|()| doc["units"].as_array().expect("u").iter().find(|u| u["id"] == created["unitId"]).cloned()); .and_then(|()| doc["units"].as_array().expect("u").iter().find(|u| u["id"] == created["unitId"]).cloned());
let fresh = fresh.expect("the new article"); let fresh = fresh.expect("the new article");
let new_code = fresh["code"].as_str().expect("code").to_owned(); let new_code = fresh["code"].as_str().expect("code").to_owned();
assert_eq!(read_order(&doc, "inertie"), ["N1", "N1.1", new_code.as_str()], "a new article goes at the end"); assert_eq!(read_order(&doc, "conclusion"), ["K1", "K2", new_code.as_str()], "a new article goes at the end");
let module_now = order_of(&doc, "inertie"); let module_now = order_of(&doc, "conclusion");
let open = module_now["proposals"].as_array().expect("p").iter().find(|x| x["id"] == p.as_str()).expect("p"); let open = module_now["proposals"].as_array().expect("p").iter().find(|x| x["id"] == p.as_str()).expect("p");
assert_eq!(codes(&doc, &open["order"]["ids"]), ["N1.1", "N1", new_code.as_str()]); assert_eq!(codes(&doc, &open["order"]["ids"]), ["K2", "K1", new_code.as_str()]);
assert_eq!(codes(&doc, &open["order"]["added"]), [new_code.as_str()]); assert_eq!(codes(&doc, &open["order"]["added"]), [new_code.as_str()]);
// Its vote stays: completing an order never asks the voters again. // Its vote stays: completing an order never asks the voters again.
assert_eq!((open["tally"]["for"].as_u64(), open["myVote"].as_str()), (Some(1), Some("for"))); assert_eq!((open["tally"]["for"].as_u64(), open["myVote"].as_str()), (Some(1), Some("for")));
@@ -307,47 +322,47 @@ async fn articles_that_enter_or_leave_a_section_complete_an_order_under_vote() {
let added = created["id"].as_str().expect("id"); let added = created["id"].as_str().expect("id");
chiara.ok("POST", &format!("/api/proposals/{added}/withdraw"), None).await; chiara.ok("POST", &format!("/api/proposals/{added}/withdraw"), None).await;
let doc = yvv.doc("essai").await; let doc = yvv.doc("essai").await;
let open = order_of(&doc, "inertie")["proposals"] let open = order_of(&doc, "conclusion")["proposals"]
.as_array() .as_array()
.expect("p") .expect("p")
.iter() .iter()
.find(|x| x["id"] == p.as_str()) .find(|x| x["id"] == p.as_str())
.cloned() .cloned()
.expect("p"); .expect("p");
assert_eq!(codes(&doc, &open["order"]["ids"]), ["N1.1", "N1"]); assert_eq!(codes(&doc, &open["order"]["ids"]), ["K2", "K1"]);
assert!(open["order"]["added"].as_array().expect("a").is_empty()); assert!(open["order"]["added"].as_array().expect("a").is_empty());
// An order proposed with three articles, one of which then leaves: dropped from it. // An order proposed with three articles, one of which then leaves: dropped from it.
let created = chiara let created = chiara
.ok( .ok(
"POST", "POST",
&format!("/api/docs/{doc_id}/proposals"), &format!("/api/docs/{doc_id}/proposals"),
Some(json!({ "section": "inertie", "title": "Autre", "body": "Un article de passage dans la section.", "submit": true })), Some(json!({ "section": "conclusion", "title": "Autre", "body": "Un article de passage dans la section.", "submit": true })),
) )
.await; .await;
let other = created["unitId"].as_str().expect("unit").to_owned(); let other = created["unitId"].as_str().expect("unit").to_owned();
let three = yvv.ok("POST", &format!("/api/units/{module}/order"), Some(json!({ "order": [other, n11, n1] }))).await let three = yvv.ok("POST", &format!("/api/units/{module}/order"), Some(json!({ "order": [other, k2, k1] }))).await
["id"] ["id"]
.as_str() .as_str()
.expect("id") .expect("id")
.to_owned(); .to_owned();
chiara.ok("POST", &format!("/api/proposals/{}/withdraw", created["id"].as_str().expect("id")), None).await; chiara.ok("POST", &format!("/api/proposals/{}/withdraw", created["id"].as_str().expect("id")), None).await;
let doc = yvv.doc("essai").await; let doc = yvv.doc("essai").await;
let o = order_of(&doc, "inertie")["proposals"] let o = order_of(&doc, "conclusion")["proposals"]
.as_array() .as_array()
.expect("p") .expect("p")
.iter() .iter()
.find(|x| x["id"] == three.as_str()) .find(|x| x["id"] == three.as_str())
.cloned() .cloned()
.expect("o"); .expect("o");
assert_eq!(codes(&doc, &o["order"]["ids"]), ["N1.1", "N1"]); assert_eq!(codes(&doc, &o["order"]["ids"]), ["K2", "K1"]);
assert_eq!(o["order"]["dropped"], json!([other])); assert_eq!(o["order"]["dropped"], json!([other]));
// Completed, it reads like the first one: the second voice brings that order in. // Completed, it reads like the first one: the second voice brings that order in.
let v = chiara.vote(&p, "for").await; let v = chiara.vote(&p, "for").await;
assert_eq!(v["entered"][0]["kind"], "order"); assert_eq!(v["entered"][0]["kind"], "order");
let doc = yvv.doc("essai").await; let doc = yvv.doc("essai").await;
assert_eq!(read_order(&doc, "inertie"), ["N1.1", "N1"]); assert_eq!(read_order(&doc, "conclusion"), ["K2", "K1"]);
// Now the order in force: the other one changes nothing more and cannot enter. // Now the order in force: the other one changes nothing more and cannot enter.
let o = order_of(&doc, "inertie")["proposals"] let o = order_of(&doc, "conclusion")["proposals"]
.as_array() .as_array()
.expect("p") .expect("p")
.iter() .iter()
@@ -503,10 +518,10 @@ async fn a_new_article_goes_at_the_end_of_the_section_asked_for() {
.to_owned() .to_owned()
}; };
let (ca, cb) = (code(&a), code(&b)); let (ca, cb) = (code(&a), code(&b));
assert_eq!(read_order(&doc, "fondamental"), ["T1", "E1", "E2", ca.as_str()]); assert_eq!(read_order(&doc, "fondamental"), ["T1", "E1", "E12", "E2", ca.as_str()]);
assert_eq!(read_order(&doc, "introduction"), ["I1", "I2", cb.as_str()]); assert_eq!(read_order(&doc, "introduction"), ["I1", "I2", cb.as_str()]);
// It joins the order of its section, at its end. // It joins the order of its section, at its end.
assert_eq!(codes(&doc, &order_of(&doc, "fondamental")["members"]), ["T1", "E1", "E2", ca.as_str()]); assert_eq!(codes(&doc, &order_of(&doc, "fondamental")["members"]), ["T1", "E1", "E12", "E2", ca.as_str()]);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+15 -15
View File
@@ -169,11 +169,11 @@ async fn decisions_one_line_per_doc_with_the_modules_left_to_vote() {
assert_eq!(docs.len(), 1); assert_eq!(docs.len(), 1);
assert_eq!(docs[0]["slug"], "acte-engagement-certification"); assert_eq!(docs[0]["slug"], "acte-engagement-certification");
assert_eq!(docs[0]["title"]["fr"], "Acte d'engagement Certification"); assert_eq!(docs[0]["title"]["fr"], "Acte d'engagement Certification");
// 3 seed proposals (ADR 0011) + 30 open articles, the two variants counting once; the title // 4 seed proposals (ADR 0011) + 23 open articles, the two variants counting once; the title
// of the document and the order of 7 sections (« ordonnancement » holds one article: nothing // of the document and the order of its 5 sections (since lot T4, the rules of operation are
// to decide). // out of the document, ADR 0014).
assert_eq!(docs[0]["toVote"], 41); assert_eq!(docs[0]["toVote"], 33);
assert_eq!(docs[0]["withoutMyVote"], 41); assert_eq!(docs[0]["withoutMyVote"], 33);
let doc = the_doc(&mut c, "essai").await; let doc = the_doc(&mut c, "essai").await;
// Votes that adopt nothing: the quorum 2 (ADR 0013), one voice of two. // Votes that adopt nothing: the quorum 2 (ADR 0013), one voice of two.
@@ -183,9 +183,9 @@ async fn decisions_one_line_per_doc_with_the_modules_left_to_vote() {
c.vote(&version(&doc, "E3", "1"), "against").await; c.vote(&version(&doc, "E3", "1"), "against").await;
c.vote(&version(&doc, "I1", "0"), "for").await; c.vote(&version(&doc, "I1", "0"), "for").await;
let d = decisions(&mut c, "essai").await; let d = decisions(&mut c, "essai").await;
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(41), Some(39))); assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(33), Some(31)));
// Someone else has not voted yet. // Someone else has not voted yet.
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 41); assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 33);
} }
#[tokio::test] #[tokio::test]
@@ -201,7 +201,7 @@ async fn an_adopted_module_leaves_the_count_and_a_vote_to_reconfirm_does_not_cou
let v = yvv.vote(&e3, "for").await; let v = yvv.vote(&e3, "for").await;
assert_eq!(v["entered"][0]["code"], "E3"); assert_eq!(v["entered"][0]["code"], "E3");
let d = decisions(&mut yvv, "essai").await; let d = decisions(&mut yvv, "essai").await;
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(40), Some(40))); assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(32), Some(32)));
// A new article by Chiara: one more module to vote. // A new article by Chiara: one more module to vote.
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned(); let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
@@ -216,9 +216,9 @@ async fn an_adopted_module_leaves_the_count_and_a_vote_to_reconfirm_does_not_cou
.await; .await;
assert_eq!(s, StatusCode::OK, "{v}"); assert_eq!(s, StatusCode::OK, "{v}");
let added = v["id"].as_str().expect("id").to_owned(); let added = v["id"].as_str().expect("id").to_owned();
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["toVote"], 41); assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["toVote"], 33);
yvv.vote(&added, "against").await; yvv.vote(&added, "against").await;
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 40); assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 32);
// Chiara changes its meaning: Yvv's vote is to reconfirm, it no longer counts as his vote. // Chiara changes its meaning: Yvv's vote is to reconfirm, it no longer counts as his vote.
let (s, v) = chiara let (s, v) = chiara
.call( .call(
@@ -231,7 +231,7 @@ async fn an_adopted_module_leaves_the_count_and_a_vote_to_reconfirm_does_not_cou
.await; .await;
assert_eq!(s, StatusCode::OK, "{v}"); assert_eq!(s, StatusCode::OK, "{v}");
assert_eq!(v["edit"], "substantive"); assert_eq!(v["edit"], "substantive");
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 41); assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 33);
} }
#[tokio::test] #[tokio::test]
@@ -251,7 +251,7 @@ async fn a_version_held_by_an_objection_is_not_to_vote_while_it_is_held() {
assert_eq!(s, StatusCode::OK, "{v}"); assert_eq!(s, StatusCode::OK, "{v}");
// Nobody can vote on it while it is held: one module fewer to vote, for everyone. // Nobody can vote on it while it is held: one module fewer to vote, for everyone.
let d = decisions(&mut yvv, "essai").await; let d = decisions(&mut yvv, "essai").await;
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(40), Some(40))); assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(32), Some(32)));
// The list of the group still counts it among the proposals in progress: it is open. // The list of the group still counts it among the proposals in progress: it is open.
let g = yvv.get("/api/groups/essai").await; let g = yvv.get("/api/groups/essai").await;
let open = doc["units"] let open = doc["units"]
@@ -271,10 +271,10 @@ async fn while_the_chain_is_closed_a_guest_votes_in_simulation_and_has_a_count_o
// No vote account: Monnaie libre votes in simulation, for whoever opens it (ADR 0012). // No vote account: Monnaie libre votes in simulation, for whoever opens it (ADR 0012).
let d = decisions(&mut guest, "monnaie-libre").await; let d = decisions(&mut guest, "monnaie-libre").await;
assert_eq!(d["canVote"], true); assert_eq!(d["canVote"], true);
assert_eq!(d["docs"][0]["toVote"], 41); assert_eq!(d["docs"][0]["toVote"], 33);
assert_eq!(d["docs"][0]["withoutMyVote"], 41); assert_eq!(d["docs"][0]["withoutMyVote"], 33);
// In the test group, the same person votes. // In the test group, the same person votes.
let d = decisions(&mut guest, "essai").await; let d = decisions(&mut guest, "essai").await;
assert_eq!(d["canVote"], true); assert_eq!(d["canVote"], true);
assert_eq!(d["docs"][0]["withoutMyVote"], 41); assert_eq!(d["docs"][0]["withoutMyVote"], 33);
} }
+10 -2
View File
@@ -329,6 +329,14 @@ fn production_config() -> Config {
cfg cfg
} }
/// The seed of M6 to M8 (de1b7eb), which wrote the quorums and put I1 forward: since lot T4 the
/// seed of today gives I1 another text, and a base of 02/11 keeps it in force under it.
fn m8_config() -> Config {
let mut cfg = Config::for_tests();
cfg.seed_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-m8");
cfg
}
async fn restart(before: &AppState, cfg: Config) -> AppState { async fn restart(before: &AppState, cfg: Config) -> AppState {
let state = AppState::new(cfg, before.db.clone()).await.expect("state"); let state = AppState::new(cfg, before.db.clone()).await.expect("state");
seed::ensure(&state).await.expect("seed"); seed::ensure(&state).await.expect("seed");
@@ -374,8 +382,8 @@ async fn the_quorum_amendment_writes_the_quorums_by_level_once() {
assert_eq!(c.doc("monnaie-libre").await["doc"]["quorums"], json!([50, 50, 50, 50, 50])); assert_eq!(c.doc("monnaie-libre").await["doc"]["quorums"], json!([50, 50, 50, 50, 50]));
assert_eq!(c.doc("essai").await["doc"]["quorums"], json!([1, 1, 1, 1, 1])); assert_eq!(c.doc("essai").await["doc"]["quorums"], json!([1, 1, 1, 1, 1]));
// Started with today's seed: written, where none was. // Started with the seed of M6 (and of M8): written, where none was.
let state = restart(&born, Config::for_tests()).await; let state = restart(&born, m8_config()).await;
let first = decisions(&state).await; let first = decisions(&state).await;
assert_eq!( assert_eq!(
first.iter().map(|(g, f, t, o, r)| row(g, f.as_deref(), t, o, r.as_deref())).collect::<Vec<_>>(), first.iter().map(|(g, f, t, o, r)| row(g, f.as_deref(), t, o, r.as_deref())).collect::<Vec<_>>(),
+32 -17
View File
@@ -4,6 +4,10 @@
//! base is born with them open; an existing base gets them through a guarded //! base is born with them open; an existing base gets them through a guarded
//! amendment, only where nobody has touched the text. Monnaie libre publishes no //! amendment, only where nobody has touched the text. Monnaie libre publishes no
//! document without any article in force: the queue waits for the first text. //! document without any article in force: the queue waits for the first text.
//!
//! Lot T4 (2026-11-03): a fourth, E12 « Responsabilité civique », and Yvv's texts for the three
//! (`tests/amorce.rs`). A new base is born with the four; the bases made as production are
//! started here with the seed of M8, the last that gave the three their texts of before.
use axum::body::Body; use axum::body::Body;
use axum::http::{Request, StatusCode, header}; use axum::http::{Request, StatusCode, header};
@@ -107,7 +111,7 @@ async fn fresh_with(cfg: Config) -> AppState {
} }
#[tokio::test] #[tokio::test]
async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force() { async fn a_new_base_is_born_with_four_seed_proposals_and_no_article_in_force() {
let state = fresh().await; let state = fresh().await;
let mut c = Client::of(&state); let mut c = Client::of(&state);
c.login("Chiara", false).await; c.login("Chiara", false).await;
@@ -121,7 +125,8 @@ async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force()
.filter_map(|u| u["code"].as_str()) .filter_map(|u| u["code"].as_str())
.collect(); .collect();
assert!(in_force.is_empty(), "{group}: no article in force, {in_force:?}"); assert!(in_force.is_empty(), "{group}: no article in force, {in_force:?}");
for code in CODES { for code in CODES.iter().chain(&["E12"]) {
let code = *code;
let u = unit(&doc, code); let u = unit(&doc, code);
let p = &u["proposals"][0]; let p = &u["proposals"][0];
assert_eq!( assert_eq!(
@@ -135,19 +140,20 @@ async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force()
assert!(p["translations"]["en"]["title"].is_string(), "{group} {code}: translated"); assert!(p["translations"]["en"]["title"].is_string(), "{group} {code}: translated");
} }
assert_eq!(unit(&doc, "I1")["proposals"][0]["title"], "Raison d'être"); assert_eq!(unit(&doc, "I1")["proposals"][0]["title"], "Raison d'être");
// Only those three; the title and the orders stay seeds a minima, in force. // Only those four; the title and the orders stay seeds a minima, in force.
let marked: Vec<&Value> = versions(&doc).into_iter().filter(|v| v["seedProposal"] == true).collect(); let marked: Vec<&Value> = versions(&doc).into_iter().filter(|v| v["seedProposal"] == true).collect();
assert_eq!(marked.len(), 3, "{group}"); assert_eq!(marked.len(), 4, "{group}");
let title = &doc["modules"]["title"]["inForce"]; let title = &doc["modules"]["title"]["inForce"];
assert_eq!((title["seedMinimal"].as_bool(), title["seedProposal"].as_bool()), (Some(true), Some(false))); assert_eq!((title["seedMinimal"].as_bool(), title["seedProposal"].as_bool()), (Some(true), Some(false)));
assert!( assert!(
doc["modules"]["orders"].as_array().expect("orders").iter().all(|o| o["inForce"]["seedMinimal"] == true) doc["modules"]["orders"].as_array().expect("orders").iter().all(|o| o["inForce"]["seedMinimal"] == true)
); );
// The counts: no article in force, three more open versions; still 41 modules to vote. // The counts: no article in force, 29 open versions (27 articles, E3 and E7 two each);
// 33 modules to vote (27 articles, the title, the order of 5 sections; lot T4).
let g = c.get(&format!("/api/groups/{group}")).await; let g = c.get(&format!("/api/groups/{group}")).await;
assert_eq!((g["docs"][0]["inForce"].as_u64(), g["docs"][0]["open"].as_u64()), (Some(0), Some(35)), "{group}"); assert_eq!((g["docs"][0]["inForce"].as_u64(), g["docs"][0]["open"].as_u64()), (Some(0), Some(29)), "{group}");
let d = c.get(&format!("/api/groups/{group}/decisions")).await; let d = c.get(&format!("/api/groups/{group}/decisions")).await;
assert_eq!(d["docs"][0]["toVote"], 41, "{group}"); assert_eq!(d["docs"][0]["toVote"], 33, "{group}");
// The preview of the text in force is empty; « if everything passes » shows them. // The preview of the text in force is empty; « if everything passes » shows them.
let id = doc["doc"]["id"].as_str().expect("id"); let id = doc["doc"]["id"].as_str().expect("id");
let p = c.get(&format!("/api/docs/{id}/preview")).await; let p = c.get(&format!("/api/docs/{id}/preview")).await;
@@ -155,7 +161,8 @@ async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force()
let all = c.get(&format!("/api/docs/{id}/preview?mode=all")).await; let all = c.get(&format!("/api/docs/{id}/preview?mode=all")).await;
assert!(all["units"].as_array().expect("units").iter().any(|u| u["title"] == "Raison d'être")); assert!(all["units"].as_array().expect("units").iter().any(|u| u["title"] == "Raison d'être"));
} }
// Born with them: nothing to amend, and it is said once per article and group. // Born with them: the seed proposal amendments, superseded since lot T4 (the seed puts
// forward Yvv's texts, not theirs), decide nothing; the texts of lot T4 are said present.
let decided: Vec<(String, String, String)> = sqlx::query_as( let decided: Vec<(String, String, String)> = sqlx::query_as(
"SELECT d.group_slug, s.unit_code, s.outcome FROM seed_proposal_amendments s "SELECT d.group_slug, s.unit_code, s.outcome FROM seed_proposal_amendments s
JOIN documents d ON d.id = s.doc_id ORDER BY d.group_slug, s.unit_code", JOIN documents d ON d.id = s.doc_id ORDER BY d.group_slug, s.unit_code",
@@ -163,14 +170,13 @@ async fn a_new_base_is_born_with_three_seed_proposals_and_no_article_in_force()
.fetch_all(&state.db) .fetch_all(&state.db)
.await .await
.expect("decisions"); .expect("decisions");
assert_eq!(decided.len(), 6, "{decided:?}"); assert!(decided.is_empty(), "{decided:?}");
assert!(decided.iter().all(|(_, _, o)| o == "present"), "{decided:?}");
let changes: i64 = let changes: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM inertia_changes").fetch_one(&state.db).await.expect("changes"); sqlx::query_scalar("SELECT COUNT(*) FROM inertia_changes").fetch_one(&state.db).await.expect("changes");
assert_eq!(changes, 0, "born at the low level: nothing changed"); assert_eq!(changes, 0, "born at the low level: nothing changed");
// The Raison d'être of a new base is still the text its own amendment names. // The Raison d'être of a new base is the text its own amendment of lot T4 names.
let i1: Vec<String> = sqlx::query_scalar( let i1: Vec<String> = sqlx::query_scalar(
"SELECT outcome FROM seed_amendments WHERE amendment_id = '2026-10-30-i1-raison-d-etre' ORDER BY doc_id", "SELECT outcome FROM seed_amendments WHERE amendment_id = '2026-11-03-i1-amorce' ORDER BY doc_id",
) )
.fetch_all(&state.db) .fetch_all(&state.db)
.await .await
@@ -242,7 +248,16 @@ async fn production(tag: &str, cfg: Config) -> AppState {
state state
} }
/// The same base, started again with today's seed (and its amendments). /// The seed of M6 for these three texts, unchanged until M8 (de1b7eb): since lot T4 (03/11) the
/// seed of today gives them Yvv's texts, and their seed proposal amendments are superseded
/// (`tests/amorce.rs`).
fn m8() -> Config {
let mut cfg = Config::for_tests();
cfg.seed_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-m8");
cfg
}
/// The same base, started again with a later seed (and its amendments).
async fn restart(before: &AppState, cfg: Config) -> AppState { async fn restart(before: &AppState, cfg: Config) -> AppState {
let state = AppState::new(cfg, before.db.clone()).await.expect("state"); let state = AppState::new(cfg, before.db.clone()).await.expect("state");
seed::ensure(&state).await.expect("seed"); seed::ensure(&state).await.expect("seed");
@@ -367,7 +382,7 @@ async fn the_production_base_turns_its_untouched_texts_into_seed_proposals_once(
}; };
let translated = translations(before.clone()).await; let translated = translations(before.clone()).await;
let state = restart(&before, Config::for_tests()).await; let state = restart(&before, m8()).await;
let first = decisions(&state).await; let first = decisions(&state).await;
// Every text becomes a seed proposal; the level goes low where nobody touched it: // Every text becomes a seed proposal; the level goes low where nobody touched it:
// an open change of inertia by a member keeps it (I1 and E1 in Monnaie libre). // an open change of inertia by a member keeps it (I1 and E1 in Monnaie libre).
@@ -480,7 +495,7 @@ async fn the_production_base_turns_its_untouched_texts_into_seed_proposals_once(
); );
// Decided once: a second start changes nothing. // Decided once: a second start changes nothing.
let again = restart(&state, Config::for_tests()).await; let again = restart(&state, m8()).await;
assert_eq!(decisions(&again).await, first); assert_eq!(decisions(&again).await, first);
for group in GROUPS { for group in GROUPS {
for code in CODES { for code in CODES {
@@ -508,7 +523,7 @@ async fn a_vote_keeps_the_text_in_force_and_the_refusal_is_kept() {
let v = c.vote(&e1.id, "against").await; let v = c.vote(&e1.id, "against").await;
assert!(v["entered"].as_array().expect("entered").is_empty()); assert!(v["entered"].as_array().expect("entered").is_empty());
let state = restart(&before, Config::for_tests()).await; let state = restart(&before, m8()).await;
let first = decisions(&state).await; let first = decisions(&state).await;
assert_eq!( assert_eq!(
outcomes(&first), outcomes(&first),
@@ -533,7 +548,7 @@ async fn a_vote_keeps_the_text_in_force_and_the_refusal_is_kept() {
// Decided once: the vote withdrawn, a later start does not look again. // Decided once: the vote withdrawn, a later start does not look again.
let (s, v) = c.call("POST", &format!("/api/proposals/{}/vote", kept.id), Some(json!({ "choice": null }))).await; let (s, v) = c.call("POST", &format!("/api/proposals/{}/vote", kept.id), Some(json!({ "choice": null }))).await;
assert_eq!(s, StatusCode::OK, "{v}"); assert_eq!(s, StatusCode::OK, "{v}");
let again = restart(&state, Config::for_tests()).await; let again = restart(&state, m8()).await;
assert_eq!(decisions(&again).await, first); assert_eq!(decisions(&again).await, first);
assert!(version(&again, "essai", "E1").await.in_force); assert!(version(&again, "essai", "E1").await.in_force);
} }
+51 -35
View File
@@ -39,9 +39,17 @@
//! engine on one grid (`grid`): W from 100, medians under 1 % and above 50 % of W, every quorum an //! engine on one grid (`grid`): W from 100, medians under 1 % and above 50 % of W, every quorum an
//! author may set, the five levels — the closed list given to the reviewer is //! author may set, the five levels — the closed list given to the reviewer is
//! `docs/chantiers/acte-certification/t3c-liste-fermee.md`. //! `docs/chantiers/acte-certification/t3c-liste-fermee.md`.
//!
//! Lot T4 (2026-11-03, Yvv: « ordonnancement, inertie, et formule de vote sont des infos de
//! fonctionnement »): F1.1, F1.2 and N1.1 leave the document (ADR 0014); they are no texts to
//! vote any more. Their last texts — the seed of M8 (de1b7eb), kept in
//! `tests/fixtures/seed-m8/` — stay the reference of what the interface will say of the rules
//! (lot U3), and every truth this file checks of them against the engine still holds there. The
//! amendments that wrote them are replayed on that seed, as production played them.
use ld_engine::inertia::{InertiaLevel, Participation, Scale, gradient, quorum_bounds}; use ld_engine::inertia::{InertiaLevel, Participation, Scale, gradient, quorum_bounds};
use ld_engine::threshold::{FormulaParams, Tally, judged_voters, passes, required_ratio, wot_threshold, yes_needed}; use ld_engine::threshold::{FormulaParams, Tally, judged_voters, passes, required_ratio, wot_threshold, yes_needed};
use ld_server::retire::RETIREMENT;
use ld_server::seed_amend::{self, AMENDMENTS, Version}; use ld_server::seed_amend::{self, AMENDMENTS, Version};
use ld_server::wot::PLAUSIBLE; use ld_server::wot::PLAUSIBLE;
use ld_server::{AppState, Config, open_db, seed}; use ld_server::{AppState, Config, open_db, seed};
@@ -64,8 +72,15 @@ fn read(path: PathBuf) -> Value {
serde_json::from_str(&std::fs::read_to_string(path).expect("read")).expect("json") serde_json::from_str(&std::fs::read_to_string(path).expect("read")).expect("json")
} }
fn today_seed() -> Value { /// The seed production runs since M8 (de1b7eb): the last that holds F1.1, F1.2 and N1.1, and
read(Config::for_tests().seed_dir.join("acte-engagement-certification.json")) /// the reference of their texts since lot T4 took them out of the document.
fn m8_seed() -> Value {
read(fixtures().join("seed-m8/acte-engagement-certification.json"))
}
/// A start with the seed of M8: lot T3 decides.
fn m8_config(tag: &str) -> Config {
config(seed_dir(&format!("{tag}-m8"), &m8_seed()))
} }
fn fixtures() -> PathBuf { fn fixtures() -> PathBuf {
@@ -89,7 +104,7 @@ fn m6b_texts() -> Value {
/// The seed production runs since M6b: today's, with the texts of lot T. /// The seed production runs since M6b: today's, with the texts of lot T.
fn m6b_seed() -> Value { fn m6b_seed() -> Value {
with_texts(&today_seed(), &m6b_texts(), &CODES) with_texts(&m8_seed(), &m6b_texts(), &CODES)
} }
/// F1.1, F1.2 and N1.1 as lot T2 wrote them: production's since M6c (84f015a), unchanged by M7b. /// F1.1, F1.2 and N1.1 as lot T2 wrote them: production's since M6c (84f015a), unchanged by M7b.
@@ -99,7 +114,7 @@ fn t2_texts() -> Value {
/// The seed production runs since M6c: today's, with the texts of lot T2. /// The seed production runs since M6c: today's, with the texts of lot T2.
fn t2_seed() -> Value { fn t2_seed() -> Value {
with_texts(&today_seed(), &t2_texts(), &CODES) with_texts(&m8_seed(), &t2_texts(), &CODES)
} }
fn unit<'a>(seed: &'a Value, code: &str) -> &'a Value { fn unit<'a>(seed: &'a Value, code: &str) -> &'a Value {
@@ -481,7 +496,7 @@ fn formulas(code: &str, lang: &str) -> Vec<String> {
/// article and language listed at once. /// article and language listed at once.
#[test] #[test]
fn every_number_of_the_quorum_texts_is_the_engine_s() { fn every_number_of_the_quorum_texts_is_the_engine_s() {
let seed = today_seed(); let seed = m8_seed();
let f = figures(&seed); let f = figures(&seed);
let mut wrong = Vec::new(); let mut wrong = Vec::new();
for code in CODES { for code in CODES {
@@ -821,7 +836,7 @@ fn t3_from_t2(code: &str, lang: &str, t2: &str) -> String {
#[test] #[test]
fn the_texts_of_lot_t3_change_these_phrases_only() { fn the_texts_of_lot_t3_change_these_phrases_only() {
let (t2, today) = (t2_seed(), today_seed()); let (t2, today) = (t2_seed(), m8_seed());
for code in CODES { for code in CODES {
for lang in LANGS { for lang in LANGS {
let (before, now) = (text(&t2, code, lang), text(&today, code, lang)); let (before, now) = (text(&t2, code, lang), text(&today, code, lang));
@@ -1006,7 +1021,7 @@ fn share_at(w: u32, voters: f64, params: &FormulaParams) -> f64 {
/// quorum of 2, two « pour » are asked, at every level, on every W. /// quorum of 2, two « pour » are asked, at every level, on every W.
#[test] #[test]
fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() { fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
let today = today_seed(); let today = m8_seed();
let said = [ let said = [
( (
"fr", "fr",
@@ -1104,7 +1119,7 @@ fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
/// quorum 12), while the share asked falls from 73,5 % to 73,2 %. /// quorum 12), while the share asked falls from 73,5 % to 73,2 %.
#[test] #[test]
fn the_share_asked_falls_beyond_the_article_s_quorum_down_to_m() { fn the_share_asked_falls_beyond_the_article_s_quorum_down_to_m() {
let today = today_seed(); let today = m8_seed();
for (lang, phrase) in [ for (lang, phrase) in [
( (
"fr", "fr",
@@ -1175,7 +1190,7 @@ fn the_share_asked_falls_beyond_the_article_s_quorum_down_to_m() {
/// asked of more voters than came; when everyone votes, M_k, at every quorum. /// asked of more voters than came; when everyone votes, M_k, at every quorum.
#[test] #[test]
fn the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum() { fn the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum() {
let today = today_seed(); let today = m8_seed();
for (lang, intro) in [ for (lang, intro) in [
( (
"fr", "fr",
@@ -1247,7 +1262,7 @@ fn the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quoru
/// share asked there is s beyond the article's quorum, and lower, judged as the quorum, under it. /// share asked there is s beyond the article's quorum, and lower, judged as the quorum, under it.
#[test] #[test]
fn g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum() { fn g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum() {
let today = today_seed(); let today = m8_seed();
// The sentence of G closes N1.1, in each language; nothing of the sentence of T2 is left. // The sentence of G closes N1.1, in each language; nothing of the sentence of T2 is left.
for lang in LANGS { for lang in LANGS {
let (_, body) = text(&today, "N1.1", lang); let (_, body) = text(&today, "N1.1", lang);
@@ -1324,7 +1339,7 @@ fn g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quor
/// reste entre » read as a fact, not as the rule that brings the median back. /// reste entre » read as a fact, not as the rule that brings the median back.
#[test] #[test]
fn the_participation_habituelle_is_the_median_brought_back_within_its_bounds() { fn the_participation_habituelle_is_the_median_brought_back_within_its_bounds() {
let today = today_seed(); let today = m8_seed();
// The texts write the defined term, never the paraphrase: in N1.1, the list, its five lines and // The texts write the defined term, never the paraphrase: in N1.1, the list, its five lines and
// G (and, in French and Spanish, « comparé à la participation habituelle du groupe », already // G (and, in French and Spanish, « comparé à la participation habituelle du groupe », already
// there); in F1.2, « Par défaut ». // there); in F1.2, « Par défaut ».
@@ -1421,7 +1436,7 @@ fn the_participation_habituelle_is_the_median_brought_back_within_its_bounds() {
/// paragraph says after it bring it back. The voted text is not changed for this. /// paragraph says after it bring it back. The voted text is not changed for this.
#[test] #[test]
fn the_reference_turnout_is_a_parameter_the_bounds_bring_back() { fn the_reference_turnout_is_a_parameter_the_bounds_bring_back() {
let seed = today_seed(); let seed = m8_seed();
let f = figures(&seed); let f = figures(&seed);
let vote_floor = f64::from(f.reference) / 2.0; let vote_floor = f64::from(f.reference) / 2.0;
for w in ELECTORATES.into_iter().chain([199, 200, 10_000, 10_001]) { for w in ELECTORATES.into_iter().chain([199, 200, 10_000, 10_001]) {
@@ -1629,7 +1644,7 @@ fn close(a: f64, b: f64) -> bool {
#[test] #[test]
fn the_formulas_of_the_texts_are_those_the_engine_computes() { fn the_formulas_of_the_texts_are_those_the_engine_computes() {
let seed = today_seed(); let seed = m8_seed();
for lang in LANGS { for lang in LANGS {
let (_, f11) = text(&seed, "F1.1", lang); let (_, f11) = text(&seed, "F1.1", lang);
let (_, n11) = text(&seed, "N1.1", lang); let (_, n11) = text(&seed, "N1.1", lang);
@@ -1693,7 +1708,7 @@ fn the_formulas_of_the_texts_are_those_the_engine_computes() {
#[test] #[test]
fn the_quorum_texts_follow_the_rules_of_form() { fn the_quorum_texts_follow_the_rules_of_form() {
let seed = today_seed(); let seed = m8_seed();
let titles: HashMap<&str, Vec<&str>> = LANGS let titles: HashMap<&str, Vec<&str>> = LANGS
.iter() .iter()
.map(|l| { .map(|l| {
@@ -1812,8 +1827,8 @@ fn the_amendments_of_lot_t2_went_from_the_texts_of_m6b_to_those_of_t2() {
assert_eq!(seed_amend::fingerprint(title, body), a.to, "{code}: the text production holds since M6c"); assert_eq!(seed_amend::fingerprint(title, body), a.to, "{code}: the text production holds since M6c");
assert_ne!(a.from, a.to, "{code}"); assert_ne!(a.from, a.to, "{code}");
// It follows lot T's, which wrote the text it replaces; lot T3's replaces it in turn, // It follows lot T's, which wrote the text it replaces; lot T3's replaces it in turn,
// where it changes the text (F1.2, N1.1). // where it changes the text (F1.2, N1.1); F1.1 leaves the seed with lot T4 (ADR 0014).
let later = AMENDMENTS.iter().find(|l| l.follows == Some(a.id)).map(|l| l.id); let later = AMENDMENTS.iter().find(|l| l.follows == Some(a.id)).map(|l| l.id).or(Some(RETIREMENT));
assert_eq!( assert_eq!(
(a.follows, t.to, t.superseded_by, a.superseded_by), (a.follows, t.to, t.superseded_by, a.superseded_by),
(Some(t.id), a.from, Some(a.id), later), (Some(t.id), a.from, Some(a.id), later),
@@ -1873,7 +1888,7 @@ async fn production_after_m6(tag: &str) -> AppState {
let first = AppState::new(config(seed_dir(&format!("{tag}-born"), &born)), db.clone()).await.expect("state"); let first = AppState::new(config(seed_dir(&format!("{tag}-born"), &born)), db.clone()).await.expect("state");
seed::documents(&first).await.expect("born"); seed::documents(&first).await.expect("born");
start(&db, config(seed_dir(&format!("{tag}-m5"), &prod))).await; start(&db, config(seed_dir(&format!("{tag}-m5"), &prod))).await;
let m6 = with_texts(&today_seed(), &prod, &CODES); let m6 = with_texts(&m8_seed(), &prod, &CODES);
start(&db, config(seed_dir(&format!("{tag}-m6"), &m6))).await start(&db, config(seed_dir(&format!("{tag}-m6"), &m6))).await
} }
@@ -2128,8 +2143,8 @@ async fn a_vote_a_ballot_or_a_member_s_translation_keeps_the_text_of_before() {
#[tokio::test] #[tokio::test]
async fn a_new_base_is_born_with_the_quorum_texts() { async fn a_new_base_is_born_with_the_quorum_texts() {
let db = open_db("sqlite::memory:").await.expect("db"); let db = open_db("sqlite::memory:").await.expect("db");
let state = start(&db, Config::for_tests()).await; let state = start(&db, m8_config("new")).await;
let today = today_seed(); let today = m8_seed();
for group in GROUPS { for group in GROUPS {
for code in CODES { for code in CODES {
let (_, title, body, revision) = proposal(&state, group, code).await; let (_, title, body, revision) = proposal(&state, group, code).await;
@@ -2430,7 +2445,7 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_three_phrases() {
let first = AppState::new(config(seed_dir("t2-born-m5", &prod)), db.clone()).await.expect("state"); let first = AppState::new(config(seed_dir("t2-born-m5", &prod)), db.clone()).await.expect("state");
seed::documents(&first).await.expect("born"); seed::documents(&first).await.expect("born");
start(&db, config(seed_dir("t2-born-m5-m5", &prod))).await; start(&db, config(seed_dir("t2-born-m5-m5", &prod))).await;
start(&db, config(seed_dir("t2-born-m5-m6", &with_texts(&today_seed(), &prod, &CODES)))).await; start(&db, config(seed_dir("t2-born-m5-m6", &with_texts(&m8_seed(), &prod, &CODES)))).await;
let base = start(&db, m6b_config("t2-born-m5")).await; let base = start(&db, m6b_config("t2-born-m5")).await;
for group in GROUPS { for group in GROUPS {
for code in CODES { for code in CODES {
@@ -2461,7 +2476,7 @@ const T3_CODES: [&str; 2] = ["F1.2", "N1.1"];
#[test] #[test]
fn the_amendments_of_lot_t3_go_from_the_texts_of_t2_to_the_seed_of_today() { fn the_amendments_of_lot_t3_go_from_the_texts_of_t2_to_the_seed_of_today() {
let (t2, today) = (t2_seed(), today_seed()); let (t2, today) = (t2_seed(), m8_seed());
for code in CODES { for code in CODES {
let t2a = amendment(code, "-phrases"); let t2a = amendment(code, "-phrases");
let found = AMENDMENTS.iter().find(|a| a.unit == code && a.id.ends_with("-chiffres")); let found = AMENDMENTS.iter().find(|a| a.unit == code && a.id.ends_with("-chiffres"));
@@ -2470,7 +2485,7 @@ fn the_amendments_of_lot_t3_go_from_the_texts_of_t2_to_the_seed_of_today() {
for lang in LANGS { for lang in LANGS {
assert_eq!(text(&t2, code, lang), text(&today, code, lang), "{code} {lang}"); assert_eq!(text(&t2, code, lang), text(&today, code, lang), "{code} {lang}");
} }
assert_eq!(t2a.superseded_by, None, "{code}"); assert_eq!(t2a.superseded_by, Some(RETIREMENT), "{code}: out of the seed since lot T4");
continue; continue;
} }
let a = found.unwrap_or_else(|| panic!("{code}: the amendment of lot T3")); let a = found.unwrap_or_else(|| panic!("{code}: the amendment of lot T3"));
@@ -2481,10 +2496,11 @@ fn the_amendments_of_lot_t3_go_from_the_texts_of_t2_to_the_seed_of_today() {
let (title, body) = text(&today, code, "fr"); let (title, body) = text(&today, code, "fr");
assert_eq!(seed_amend::fingerprint(title, body), a.to, "{code}: the text of the seed today"); assert_eq!(seed_amend::fingerprint(title, body), a.to, "{code}: the text of the seed today");
assert_ne!(a.from, a.to, "{code}"); assert_ne!(a.from, a.to, "{code}");
// It follows lot T2's, which wrote the text it replaces; nothing replaces it yet. // It follows lot T2's, which wrote the text it replaces; lot T4 takes the article out of
// the seed (ADR 0014), and with it this amendment.
assert_eq!( assert_eq!(
(a.follows, t2a.to, t2a.superseded_by, a.superseded_by), (a.follows, t2a.to, t2a.superseded_by, a.superseded_by),
(Some(t2a.id), a.from, Some(a.id), None), (Some(t2a.id), a.from, Some(a.id), Some(RETIREMENT)),
"{code}" "{code}"
); );
let t = amendment(code, "-quorum"); let t = amendment(code, "-quorum");
@@ -2513,7 +2529,7 @@ fn history(revision: i64) -> Vec<&'static str> {
#[tokio::test] #[tokio::test]
async fn production_after_m6c_gets_the_texts_without_dated_figures_once() { async fn production_after_m6c_gets_the_texts_without_dated_figures_once() {
let base = production_after_m6c("t3-apply").await; let base = production_after_m6c("t3-apply").await;
let (t2, today) = (t2_seed(), today_seed()); let (t2, today) = (t2_seed(), m8_seed());
// The base is as production after M6c: the texts of lot T2, F1.1 at revision 3, F1.2 and // The base is as production after M6c: the texts of lot T2, F1.1 at revision 3, F1.2 and
// N1.1 at revision 4. // N1.1 at revision 4.
for group in GROUPS { for group in GROUPS {
@@ -2533,7 +2549,7 @@ async fn production_after_m6c_gets_the_texts_without_dated_figures_once() {
let revisions = count(&base, "SELECT COUNT(*) FROM revisions").await; let revisions = count(&base, "SELECT COUNT(*) FROM revisions").await;
// Started with today's seed. // Started with today's seed.
let state = start(&base.db, Config::for_tests()).await; let state = start(&base.db, m8_config("t3-apply")).await;
let mut touched = Vec::new(); let mut touched = Vec::new();
for group in GROUPS { for group in GROUPS {
for code in T3_CODES { for code in T3_CODES {
@@ -2597,7 +2613,7 @@ async fn production_after_m6c_gets_the_texts_without_dated_figures_once() {
assert!(rows.iter().all(|(g, _)| g == "monnaie-libre"), "{rows:?}"); assert!(rows.iter().all(|(g, _)| g == "monnaie-libre"), "{rows:?}");
assert_eq!(count(&state, "SELECT COUNT(*) FROM outbox").await, outbox + 2); assert_eq!(count(&state, "SELECT COUNT(*) FROM outbox").await, outbox + 2);
// Decided once: a second start replays nothing. // Decided once: a second start replays nothing.
let again = start(&state.db, Config::for_tests()).await; let again = start(&state.db, m8_config("t3-apply-again")).await;
assert_eq!(decisions(&again, "-chiffres").await, expected); assert_eq!(decisions(&again, "-chiffres").await, expected);
assert_eq!(proposals(&again).await, proposals(&state).await); assert_eq!(proposals(&again).await, proposals(&state).await);
assert_eq!(translations(&again).await, translations(&state).await); assert_eq!(translations(&again).await, translations(&state).await);
@@ -2649,7 +2665,7 @@ async fn an_article_voted_on_keeps_the_text_of_t2_and_says_why() {
luna.vote(&f12_essai, Some("against")).await; luna.vote(&f12_essai, Some("against")).await;
luna.vote(&f12_essai, None).await; luna.vote(&f12_essai, None).await;
let state = start(&base.db, Config::for_tests()).await; let state = start(&base.db, m8_config("t3-refuse")).await;
let refused = let refused =
|g: &str, c: &str, why: &str| (g.to_owned(), c.to_owned(), "refused".to_owned(), Some(why.to_owned())); |g: &str, c: &str, why: &str| (g.to_owned(), c.to_owned(), "refused".to_owned(), Some(why.to_owned()));
let applied = |g: &str, c: &str| (g.to_owned(), c.to_owned(), "applied".to_owned(), None); let applied = |g: &str, c: &str| (g.to_owned(), c.to_owned(), "applied".to_owned(), None);
@@ -2662,7 +2678,7 @@ async fn an_article_voted_on_keeps_the_text_of_t2_and_says_why() {
refused("monnaie-libre", "N1.1", "votes"), refused("monnaie-libre", "N1.1", "votes"),
] ]
); );
let (t2, today) = (t2_seed(), today_seed()); let (t2, today) = (t2_seed(), m8_seed());
for group in GROUPS { for group in GROUPS {
for code in T3_CODES { for code in T3_CODES {
let kept = (group, code) != ("essai", "F1.2"); let kept = (group, code) != ("essai", "F1.2");
@@ -2676,7 +2692,7 @@ async fn an_article_voted_on_keeps_the_text_of_t2_and_says_why() {
assert!(queued(&state, "-chiffres").await.is_empty()); assert!(queued(&state, "-chiffres").await.is_empty());
// The refusals stand: decided once, never tried again, even once the vote is gone. // The refusals stand: decided once, never tried again, even once the vote is gone.
sqlx::query("DELETE FROM votes WHERE proposal_id = ?").bind(&n11).execute(&state.db).await.expect("delete"); sqlx::query("DELETE FROM votes WHERE proposal_id = ?").bind(&n11).execute(&state.db).await.expect("delete");
let again = start(&state.db, Config::for_tests()).await; let again = start(&state.db, m8_config("t3-refuse-again")).await;
assert_eq!(decisions(&again, "-chiffres").await, decisions(&state, "-chiffres").await); assert_eq!(decisions(&again, "-chiffres").await, decisions(&state, "-chiffres").await);
let (_, title, body, _) = proposal(&again, "monnaie-libre", "N1.1").await; let (_, title, body, _) = proposal(&again, "monnaie-libre", "N1.1").await;
assert_eq!((title.as_str(), body.as_str()), text(&t2, "N1.1", "fr")); assert_eq!((title.as_str(), body.as_str()), text(&t2, "N1.1", "fr"));
@@ -2691,7 +2707,7 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_texts_of_t3() {
let first = AppState::new(config(seed_dir("t3-born-m5", &prod)), db.clone()).await.expect("state"); let first = AppState::new(config(seed_dir("t3-born-m5", &prod)), db.clone()).await.expect("state");
seed::documents(&first).await.expect("born"); seed::documents(&first).await.expect("born");
start(&db, config(seed_dir("t3-born-m5-m5", &prod))).await; start(&db, config(seed_dir("t3-born-m5-m5", &prod))).await;
start(&db, config(seed_dir("t3-born-m5-m6", &with_texts(&today_seed(), &prod, &CODES)))).await; start(&db, config(seed_dir("t3-born-m5-m6", &with_texts(&m8_seed(), &prod, &CODES)))).await;
start(&db, m6b_config("t3-born-m5")).await; start(&db, m6b_config("t3-born-m5")).await;
let base = start(&db, t2_config("t3-born-m5")).await; let base = start(&db, t2_config("t3-born-m5")).await;
for group in GROUPS { for group in GROUPS {
@@ -2700,8 +2716,8 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_texts_of_t3() {
assert_eq!(kinds(&base, &id).await, history(3), "{group} {code}: lots T and T2"); assert_eq!(kinds(&base, &id).await, history(3), "{group} {code}: lots T and T2");
} }
} }
let state = start(&db, Config::for_tests()).await; let state = start(&db, m8_config("t3-born-m5")).await;
let today = today_seed(); let today = m8_seed();
for group in GROUPS { for group in GROUPS {
for code in T3_CODES { for code in T3_CODES {
let (id, title, body, revision) = proposal(&state, group, code).await; let (id, title, body, revision) = proposal(&state, group, code).await;
@@ -2720,8 +2736,8 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_texts_of_t3() {
async fn a_base_born_with_the_texts_of_t2_gets_the_texts_of_t3() { async fn a_base_born_with_the_texts_of_t2_gets_the_texts_of_t3() {
let db = open_db("sqlite::memory:").await.expect("db"); let db = open_db("sqlite::memory:").await.expect("db");
start(&db, t2_config("t3-born-t2")).await; start(&db, t2_config("t3-born-t2")).await;
let state = start(&db, Config::for_tests()).await; let state = start(&db, m8_config("t3-born-t2")).await;
let today = today_seed(); let today = m8_seed();
for group in GROUPS { for group in GROUPS {
for code in CODES { for code in CODES {
let (id, title, body, revision) = proposal(&state, group, code).await; let (id, title, body, revision) = proposal(&state, group, code).await;
+17 -5
View File
@@ -4,7 +4,10 @@
//! //!
//! Played with the seed M5 shipped (3f10a0e, the fixture of production): since lot T //! Played with the seed M5 shipped (3f10a0e, the fixture of production): since lot T
//! (2026-11-03), today's seed gives F1.2 and N1.1 the texts of the quorum, and their //! (2026-11-03), today's seed gives F1.2 and N1.1 the texts of the quorum, and their
//! amendments of 02/11 are superseded (`tests/seed_quorum_texts.rs`). //! amendments of 02/11 are superseded (`tests/seed_quorum_texts.rs`). Since lot T4 (the same
//! day, ADR 0014), the four leave the seed: the seed of M8 (`tests/fixtures/seed-m8/`), the last
//! that holds them, stands for « today » here, and their last amendments are superseded by
//! their retirement (`retire`).
use ld_server::seed_amend::{self, AMENDMENTS, Version}; use ld_server::seed_amend::{self, AMENDMENTS, Version};
use ld_server::{AppState, Config, open_db, seed}; use ld_server::{AppState, Config, open_db, seed};
@@ -21,8 +24,13 @@ fn before() -> Value {
read(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-regles-avant.json")) read(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-regles-avant.json"))
} }
/// The seed of M8 (de1b7eb): the last that holds the rule articles.
fn m8_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-m8")
}
fn today_seed() -> Value { fn today_seed() -> Value {
read(Config::for_tests().seed_dir.join("acte-engagement-certification.json")) read(m8_dir().join("acte-engagement-certification.json"))
} }
/// The seed M5 shipped: production's since 2026-11-02. /// The seed M5 shipped: production's since 2026-11-02.
@@ -125,12 +133,14 @@ fn the_shipped_amendments_go_from_the_texts_of_before_to_the_seed_of_m5() {
let (t, b) = fr(version(&today, code)); let (t, b) = fr(version(&today, code));
let now = seed_amend::fingerprint(t, b); let now = seed_amend::fingerprint(t, b);
let mut last = a; let mut last = a;
while let Some(later) = last.superseded_by { while let Some(later) = last.superseded_by.filter(|l| *l != ld_server::retire::RETIREMENT) {
let l = AMENDMENTS.iter().find(|l| l.id == later).expect("the later amendment"); let l = AMENDMENTS.iter().find(|l| l.id == later).expect("the later amendment");
assert_eq!((l.unit, l.follows, l.from), (code, Some(last.id), last.to), "{code}: {}", l.id); assert_eq!((l.unit, l.follows, l.from), (code, Some(last.id), last.to), "{code}: {}", l.id);
last = l; last = l;
} }
assert_eq!(now, last.to, "{code}: the text of today"); assert_eq!(now, last.to, "{code}: the text of today");
// Lot T4 takes the article out of the seed: the last amendment of its text decides nothing more.
assert_eq!(last.superseded_by, Some(ld_server::retire::RETIREMENT), "{code}");
} }
} }
@@ -217,9 +227,11 @@ async fn untouched_rule_articles_are_replaced_once_and_a_vote_keeps_its_text() {
} }
#[tokio::test] #[tokio::test]
async fn a_new_base_is_born_with_the_rules_of_today() { async fn a_base_born_on_the_seed_of_m8_holds_its_rules() {
let db = open_db("sqlite::memory:").await.expect("db"); let db = open_db("sqlite::memory:").await.expect("db");
let state = AppState::new(Config::for_tests(), db).await.expect("state"); let mut cfg = Config::for_tests();
cfg.seed_dir = m8_dir();
let state = AppState::new(cfg, db).await.expect("state");
seed::ensure(&state).await.expect("seed"); seed::ensure(&state).await.expect("seed");
let today = today_seed(); let today = today_seed();
for group in ["essai", "monnaie-libre"] { for group in ["essai", "monnaie-libre"] {
@@ -0,0 +1,143 @@
# 0014 — Les informations de fonctionnement ne se votent pas
> Décision d'Yvv du 2026-11-03 (lot T4). Complète [0009](0009-modules-ordre-et-titre.md) (les
> modules) et [0011](0011-propositions-seed.md) (les Propositions seed). La migration est la
> `0012`. Les textes, avant et après, sont relus dans
> `docs/chantiers/acte-certification/textes-amorce.md`.
**Contexte.** Le seed de l'Acte d'engagement portait, depuis la maquette, trois sections au bas du
document : `formule` (F1 « Formule de vote », F1.1 « Formule du seuil WoT », F1.2 « Paramètres par
défaut », F1.3 « Processus de dépôt officiel »), `inertie` (N1 « Réglage de l'inertie », N1.1
« Niveaux d'inertie ») et `ordonnancement` (O1 « Ordonnancement du document »), chacune avec son
module « ordre des articles » (ADR 0009). Les lots T à T3c ont réécrit F1.1, F1.2 et N1.1 pour
qu'ils disent les règles du moteur. Yvv, 2026-11-03 : « Attention les 3 derniers modules ne sont pas
de votes : ordonnancement, inertie, et formule de vote sont des infos de fonctionnement. Les votes
se font à leur place, et les infos doivent trouver leur place dans l'ux. […] Vire les 3 du bas de
la liste. »
## Décision
**Ce qui sort du document** : les sept articles F1, F1.1, F1.2, F1.3, N1, N1.1, O1, et les trois
modules `ordre.formule`, `ordre.inertie`, `ordre.ordonnancement`. Le document garde cinq
sections (introduction, fondamental, technique, conclusion, annexe) et 27 articles, dont quatre
Propositions seed : I1, E1, E12 « Responsabilité civique » (nouveau, entre E1 et E2) et E2.
**Hors du document, une unité n'est rien pour le vote.** Elle n'est ni lue avec son document
(`docs::load_state` ne lit que les unités sans `retired_at`, et leurs versions), donc ni listée,
ni comptée (`docs::group` : « propositions en cours » ; `overview::modules` : « modules à
voter »), ni mise dans l'ordre d'une section (`DocState::members`), ni publiée (`document.json` et
le Markdown lisent l'état du document ; `publish::waits_for_a_text` et `last_change` l'ignorent).
**Aucun vote futur** : l'API répond 404 à un vote, à la lecture d'une de ses versions, à une
correction, une traduction, une objection faite depuis elle, une proposition sur elle, un
changement d'inertie, un quorum, un ordre (`proposals::guarded` : la version doit être celle d'une
unité du document) ; 400 à un vote groupé qui la nomme (`lines`), à une objection qui la vise
(`other`) et à un nouvel article dans une section sortie (`section`). Rien n'est écrit par ces
refus.
**Une base neuve** naît sans eux : le seed ne les contient plus.
**Une base existante** (la production : Monnaie libre et le groupe d'essai) **n'efface rien**. La
migration `0012` est additive : `units.retired_at` (quand l'unité est sortie) et
`units.retired_reason` (l'amendement qui l'a sortie), les journaux `retirement_amendments` et
`article_amendments`. Le retrait `2026-11-03-fonctionnement-hors-vote`
(`crates/ld-server/src/retire.rs`) est décidé **une fois par document**, pour toutes les unités ou
aucune, et n'écrit que ces deux colonnes : ni leurs versions, ni leurs révisions, ni leurs
traductions, ni la file de publication ne changent. Il n'a lieu que si **aucune personne n'a
laissé de trace** sur l'une d'elles, dans l'une de ses versions, quoi qu'il en soit advenu, et il
dit la première trouvée (raison et code de l'unité) :
| Trace | Raison du refus |
|---|---|
| un vote du serveur, prouvé ou simulé | `votes` |
| une ligne de bulletin, quel que soit son sort | `ballots` |
| une adoption, une version entrée en vigueur ou confirmée, une inertie changée par un vote | `adopted` |
| une version écrite par un membre (texte, retrait, inertie, ordre) | `member_proposal` |
| une révision écrite par une personne, ou d'un genre qu'aucun amendement n'écrit ; une coquille, en attente ou refusée | `corrected` |
| une traduction écrite ou corrigée par un membre | `translation_edited` |
| une objection qui la nomme, levée ou non | `signalled` |
| un quorum fixé (`units.quorum`, `quorum_changes`) | `quorum_set` |
Un amendement du seed n'est pas une trace de personne : F1.1 à sa révision 3, F1.2 et N1.1 à leur
révision 4 (après M6c) ou 5 (après M8) sortent comme les autres. Refusé, rien ne bouge ; la
décision est consignée (`retirement_amendments` : l'issue, la raison, l'unité, les codes sortis)
et dans les journaux. Appliqué là où le document publie (Monnaie libre), une ligne s'ajoute à la
file ; comme toutes, elle attend le premier texte en vigueur (ADR 0011).
Les amendements qui réécrivaient ces textes (`2026-11-02-n1-regles`, `-o1-regles`,
`2026-11-03-f1-1-phrases`, `-f1-2-chiffres`, `-n1-1-chiffres`) sont **dépassés par le retrait**
(`superseded_by`) : le seed ne donne plus leur texte, ils ne se décident plus. Une production
restée à M6c sort donc F1.2 et N1.1 avec le texte de M6c.
## Les textes d'amorce
I1, E1 et E2 prennent les textes d'Yvv du 03/11 par trois amendements gardés de
`seed_amend.rs` sur la Proposition seed elle-même (`Version::SeedProposal`), jamais sur un texte
en vigueur : `2026-11-03-i1-amorce` (suit `2026-10-30-i1-raison-d-etre`), `2026-11-03-e1-amorce`,
`2026-11-03-e2-amorce`. Même garde que tout amendement du seed (une trace de personne refuse,
avec sa raison) ; la ligne d'historique `seed_proposal`, qui ne change aucun texte, est admise
quand l'amendement de Proposition seed l'a écrite, et seulement elle. Les amendements de
Proposition seed (`seed_proposal_amend.rs`) sont dépassés par eux.
E12 entre par un ajout gardé (`crates/ld-server/src/article_amend.rs`, sur le modèle de
`module_amend`), une fois par document : écrit comme une base neuve l'écrit
(`seed::write_unit`), **entre E1 et E2** tant qu'aucun vote n'existe sur l'ordre de la section
(`quorum::voted_units`) et qu'ils s'y suivent — l'ordre en vigueur, qui lit les positions, le dit
aussitôt —, sinon **en fin de section**, comme tout nouvel article. Refus : `code_taken` (une
autre unité porte déjà ce code), `no_section`. Le code E12 suit la famille des engagements (E3 à
E11 existent) ; la place vient de l'ordre de la section, pas du numéro.
## Ce que dira l'interface (lot U3)
Les informations de fonctionnement se montrent **hors du vote** : la formule du seuil, les crans
d'inertie, le quorum, l'ordonnancement, à leur place dans l'écran (Yvv : « les infos doivent
trouver leur place dans l'ux »), d'après les paramètres du moteur et du document, pas d'après
un texte voté. Les vérités que F1.1, F1.2 et N1.1 portaient, chacune vérifiée par un test
(lots T à T3c, `t3c-liste-fermee.md`), en sont la référence ; leurs derniers textes restent dans
`crates/ld-server/tests/fixtures/seed-m8/` et, en production, dans leurs lignes. T4 ne touche pas
`web/`.
## La file de publication
Rien n'y est effacé. Les lignes écrites pour ces textes avant T4 (les amendements de M5 à M8,
« F1.2 », « N1.1 »…) restent telles quelles ; comme toute la file, elles attendent le premier
texte en vigueur (ADR 0011) et partent avec lui : leurs raisons sont des lignes du message de
commit, alors que le contenu publié (`document.json`, le Markdown) ne contient plus ces
articles ; la ligne du retrait, ajoutée à la même file, dit qu'ils sont sortis.
## Le retour arrière
L'image d'avant (`de1b7eb`) ne connaît pas `retired_at` : elle lit les unités sans cette colonne
et les montre de nouveau, **telles qu'elles étaient** (versions, révisions, traductions, ordres
intacts : le retrait n'a écrit que ses deux colonnes). Elle démarre sur la base migrée : les
migrations inconnues sont tolérées (`open_db`). Ce que le retour ne défait pas : E12 reste
(une Proposition seed, que l'image d'avant lit comme les autres), I1, E1 et E2 gardent leurs
textes de T4 (une révision de plus).
**Prouvé** par le test `an_image_that_ignores_the_column_shows_the_rules_of_operation_as_they_were`
(`tests/amorce.rs` : la colonne remise à vide, la vue est celle d'avant), et le 2026-11-03 avec
l'image de `de1b7eb` elle-même, hors du dépôt : une base née du seed de production du 02/11 puis
démarrée par `de1b7eb` (un vote, un article de membre) ; l'image de T4 démarre dessus ; puis
`de1b7eb` de nouveau. Les sept articles et les trois ordres se lisent à l'identique dans les deux
groupes ; aucune ligne d'aucune table n'a disparu ; T4 n'a modifié que `retired_at` et
`retired_reason` (20 unités), et les trois Propositions seed amendées (6 versions, 12
traductions) ; `de1b7eb` relancée n'a rien écrit (une session de lecture). **Sur une copie de la
production**, la même procédure : relever chaque ligne de chaque table (rowid compris) ; démarrer
l'image de T4 sur la copie, sans clé de publication ; relever de nouveau (aucune ligne absente,
aucune colonne changée hors de celles-là) ; démarrer `de1b7eb` sur la copie et comparer
`GET /api/docs/{id}` d'avant et d'après pour F1 à O1 et `ordre.formule`, `ordre.inertie`,
`ordre.ordonnancement`.
Limite : le retrait n'est décidé qu'une fois ; si, après un
retour arrière, quelqu'un agit sur ces articles puis T4 revient, `retired_at` les sort de
nouveau sans relire la garde. Les remettre serait alors une décision (`retired_at` à NULL : une
écriture, pas un effacement).
## Limites connues
- **Une base d'avant M6** (I1, E1, E2 en vigueur, pas Propositions seed) : l'amendement de
Proposition seed étant dépassé, ils restent en vigueur avec leur texte, et les trois
amendements d'amorce refusent (`no_seed_proposal`). Aucune base réelle n'est dans ce cas.
- Un article de membre prend le code `N<n>` (`proposals::new_unit`) d'après le nombre de codes
en `N` du document, unités sorties comprises : en production, N1 et N1.1 restent dans la base
et aucun code n'est repris ; dans une base neuve, le premier article de membre est N1, sans
rapport avec l'ancien « Réglage de l'inertie ».
+1
View File
@@ -18,3 +18,4 @@ pas une décision : on en écrit une nouvelle qui la remplace, et on le note dan
| [0011](0011-propositions-seed.md) | Les Propositions seed : rien en vigueur au départ, trois textes mis en avant au cran 2, la publication attend le premier texte (complète 0002) | 2026-11-02 | | [0011](0011-propositions-seed.md) | Les Propositions seed : rien en vigueur au départ, trois textes mis en avant au cran 2, la publication attend le premier texte (complète 0002) | 2026-11-02 |
| [0012](0012-tampon-des-votes.md) | Le tampon des votes : l'intention visible tout de suite, la preuve de la chaîne, la réconciliation a posteriori ; la simulation tant que la chaîne est fermée (complète 0008, révise 0010) | 2026-11-02 | | [0012](0012-tampon-des-votes.md) | Le tampon des votes : l'intention visible tout de suite, la preuve de la chaîne, la réconciliation a posteriori ; la simulation tant que la chaîne est fermée (complète 0008, révise 0010) | 2026-11-02 |
| [0013](0013-quorum.md) | Le quorum par cran (12 · 12 · 50 · 50 · 50), fixé par l'auteur avant le premier vote ; sous le quorum, les votants qui manquent comptent comme « non » (remplace le C de 0002 et 0007) | 2026-11-03 | | [0013](0013-quorum.md) | Le quorum par cran (12 · 12 · 50 · 50 · 50), fixé par l'auteur avant le premier vote ; sous le quorum, les votants qui manquent comptent comme « non » (remplace le C de 0002 et 0007) | 2026-11-03 |
| [0014](0014-informations-de-fonctionnement.md) | Les informations de fonctionnement ne se votent pas : formule, inertie, ordonnancement sortent du document sans rien effacer (`units.retired_at`) ; E12 « Responsabilité civique » ajoutée (complète 0009 et 0011) | 2026-11-03 |
@@ -17,13 +17,13 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
|---|---|---|---| |---|---|---|---|
| C1 | L'accueil, sans session | ouvrir `/` sans être connectée | la clé de voûte en grand, jamais inclinée ; sous l'arche, centrés et dans cet ordre : « boîte à outils », « contexte et cadrage », « processus et modalités » ; puis le titre centré sur deux lignes, « Rendre possible » / « la décision collective » ; « Se connecter avec sa clé Ğ1 » et sa note ; pas de piliers ni de groupes ; le menu à gauche avec les quatre outils, trois éteints | | C1 | L'accueil, sans session | ouvrir `/` sans être connectée | la clé de voûte en grand, jamais inclinée ; sous l'arche, centrés et dans cet ordre : « boîte à outils », « contexte et cadrage », « processus et modalités » ; puis le titre centré sur deux lignes, « Rendre possible » / « la décision collective » ; « Se connecter avec sa clé Ğ1 » et sa note ; pas de piliers ni de groupes ; le menu à gauche avec les quatre outils, trois éteints |
| C2 | L'accueil, avec session | se connecter : on arrive sur `/` | le titre, le leitmotiv, puis directement le choix du groupe : Monnaie libre en grand, Axiom, Librodrome, Groupe local Vallée Drôme Crestois ; **pas** l'Atelier du canal ; `/groupes` et « Mes groupes » (menu) ramènent ici | | C2 | L'accueil, avec session | se connecter : on arrive sur `/` | le titre, le leitmotiv, puis directement le choix du groupe : Monnaie libre en grand, Axiom, Librodrome, Groupe local Vallée Drôme Crestois ; **pas** l'Atelier du canal ; `/groupes` et « Mes groupes » (menu) ramènent ici |
| C3 | Le texte au départ : les Propositions seed | Monnaie libre → Docs de référence → Acte d'engagement Certification | aucun article en vigueur (ADR 0011) : « Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition. » ; puis, **en tête du texte**, en entier, Raison d'être (sous « Introduction »), Unicité du compte et Réciprocité (sous « Engagements fondamentaux »), chacune marquée « Proposition seed » (au survol : « Texte de départ mis en avant pour lancer le vote… »), sur un lavis d'eau, avec « 0 pour · 0 contre · 9 oui sur 12 » et « basse » (au lecteur d'écran : « inertie 2 / 5 · basse ») ; chaque section lue finit par une ligne d'ordre (C25) ; en dessous, « Propositions en cours » **repliées par section** (8 lignes : le nom, « 11 articles », un petit niveau d'eau par article) ; « Tout déplier » : 30 lignes, une par article (les trois Propositions seed ne s'y répètent pas) | | C3 | Le texte au départ : les Propositions seed | Monnaie libre → Docs de référence → Acte d'engagement Certification | aucun article en vigueur (ADR 0011) : « Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition. » ; puis, **en tête du texte**, en entier, Raison d'être (sous « Introduction »), Unicité du compte, Responsabilité civique et Réciprocité (sous « Engagements fondamentaux »), chacune marquée « Proposition seed » (au survol : « Texte de départ mis en avant pour lancer le vote… »), sur un lavis d'eau, avec « 0 pour · 0 contre · 9 oui sur 12 » et « basse » (au lecteur d'écran : « inertie 2 / 5 · basse ») ; chaque section lue finit par une ligne d'ordre (C25) ; en dessous, « Propositions en cours » **repliées par section** (5 lignes : le nom, « 11 articles », un petit niveau d'eau par article ; formule, inertie et ordonnancement sont sortis du document, ADR 0014) ; « Tout déplier » : 23 lignes, une par article (les quatre Propositions seed ne s'y répètent pas) |
| C4 | Gestes contextuels | groupe d'essai, où la Raison d'être est entrée en vigueur par le vote : la survoler → « Le vote » | deux gestes sous l'article, « Le vote » et « Proposer une modification » (ses autres versions se lisent dans le vote) ; le vote se **déplie sous l'article**, sans fenêtre : « N oui sur Q votants pour passer » et les voix qui manquent ; « La formule du seuil » se déplie : « M — majorité à pleine participation : 50 % — inertie 2 / 5 · basse », et le lien « Réglages de la formule » ; l'URL garde le vote déplié (`?vote=`) ; « Le vote » de nouveau le replie | | C4 | Gestes contextuels | groupe d'essai, où la Raison d'être est entrée en vigueur par le vote : la survoler → « Le vote » | deux gestes sous l'article, « Le vote » et « Proposer une modification » (ses autres versions se lisent dans le vote) ; le vote se **déplie sous l'article**, sans fenêtre : « N oui sur Q votants pour passer » et les voix qui manquent ; « La formule du seuil » se déplie : « M — majorité à pleine participation : 50 % — inertie 2 / 5 · basse », et le lien « Réglages de la formule » ; l'URL garde le vote déplié (`?vote=`) ; « Le vote » de nouveau le replie |
| C5 | Entrée en vigueur | groupe d'essai (Raison d'être, Unicité du compte et Réciprocité entrées en vigueur par le vote) : déplier le vote de « Les deux garanties réciproques », « Pour » ; une deuxième personne si cette voix ne suffit pas | à la voix qui suffit, « … est entré en vigueur » ; le texte en vigueur compte quatre articles, le vote reste déplié sous le nouveau texte (autant de « pour » que de voix, 0 contre) | | C5 | Entrée en vigueur | groupe d'essai (Raison d'être, Unicité du compte et Réciprocité entrées en vigueur par le vote) : déplier le vote de « Les deux garanties réciproques », « Pour » ; une deuxième personne si cette voix ne suffit pas | à la voix qui suffit, « … est entré en vigueur » ; le texte en vigueur compte quatre articles, le vote reste déplié sous le nouveau texte (autant de « pour » que de voix, 0 contre) |
| C6 | Coquille sans vote | groupe d'essai, **en administratrice** (un texte du seed n'a pas d'auteur) : Unicité du compte → « Le vote » → « Coquilles, traductions, objections, historique » (le panneau « Détails », sans onglets) → « Corriger une coquille », remplacer les apostrophes droites par des apostrophes typographiques (« Je m’engage sur l’honneur ») | « Coquille corrigée, sans toucher aux votes. » ; un changement de sens est refusé : « Ce changement touche au sens : propose-le comme modification. » (sans être administratrice, la correction attend : C31) | | C6 | Coquille sans vote | groupe d'essai, **en administratrice** (un texte du seed n'a pas d'auteur) : Unicité du compte → « Le vote » → « Coquilles, traductions, objections, historique » (le panneau « Détails », sans onglets) → « Corriger une coquille », remplacer les apostrophes droites par des apostrophes typographiques (« Je m’engage sur l’honneur ») | « Coquille corrigée, sans toucher aux votes. » ; un changement de sens est refusé : « Ce changement touche au sens : propose-le comme modification. » (sans être administratrice, la correction attend : C31) |
| C7 | Une décision par proposition | groupe d'essai : « Proposer un nouvel article », deux paragraphes « Je m'engage à… » | l'assistant : « Ce texte semble porter 2 décisions » ; « Découper en 2 brouillons » ; « Tes brouillons » apparaît | | C7 | Une décision par proposition | groupe d'essai : « Proposer un nouvel article », deux paragraphes « Je m'engage à… » | l'assistant : « Ce texte semble porter 2 décisions » ; « Découper en 2 brouillons » ; « Tes brouillons » apparaît |
| C8 | Trois langues | choisir EN puis ES pour le texte | « Certification Commitment Act », « Acta de compromiso de Certificación » ; l'interface suit | | C8 | Trois langues | choisir EN puis ES pour le texte | « Certification Commitment Act », « Acta de compromiso de Certificación » ; l'interface suit |
| C9 | Les aperçus | « Si tout passe », puis « Si les bien cotées passent » | 33 articles avec la mention d'aperçu, puis aucun (rien en vigueur dans Monnaie libre, rien de bien coté sans « pour ») | | C9 | Les aperçus | « Si tout passe », puis « Si les bien cotées passent » | 27 articles avec la mention d'aperçu, puis aucun (rien en vigueur dans Monnaie libre, rien de bien coté sans « pour ») |
| C10 | trustinWallet | ouvrir `/acces` sans être administratrice | « La gestion des accès est réservée aux administrateurs de trustinWallet. » (côté admin : créer un lien d'invitation) | | C10 | trustinWallet | ouvrir `/acces` sans être administratrice | « La gestion des accès est réservée aux administrateurs de trustinWallet. » (côté admin : créer un lien d'invitation) |
| C11 | Une personne, une voix | avec un lien d'invitation et un portefeuille dont le compte n'est pas membre de la toile (Bob, chaîne simulée), voter « Pour » sur un article de Monnaie libre | le vote dit que la voix est celle de l'identité Ğ1 du portefeuille qui signe ; après signature : « Ton vote n'a pas été compté : le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1. », et sous l'article « Ton dernier vote n'a pas été compté… », 0 pour ; dans le groupe d'essai, la même personne vote directement, sans portefeuille | | C11 | Une personne, une voix | avec un lien d'invitation et un portefeuille dont le compte n'est pas membre de la toile (Bob, chaîne simulée), voter « Pour » sur un article de Monnaie libre | le vote dit que la voix est celle de l'identité Ğ1 du portefeuille qui signe ; après signature : « Ton vote n'a pas été compté : le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1. », et sous l'article « Ton dernier vote n'a pas été compté… », 0 pour ; dans le groupe d'essai, la même personne vote directement, sans portefeuille |
| C12 | Lien d'équipe | administrateur : `/acces` → « Liens d'équipe », cocher le groupe d'essai, créer ; l'ouvrir dans deux navigateurs, sous deux noms | chacun arrive sur ses groupes sous son nom ; dans `/acces`, « 2 / 30 personnes » et deux identités « venue par » le lien ; un nom déjà pris est refusé, même autrement écrit (« luna » pour « Luna ») ; sur un navigateur où quelqu'un est connecté, « Ce n'est pas moi » le déconnecte ; « Révoquer » ferme le lien | | C12 | Lien d'équipe | administrateur : `/acces` → « Liens d'équipe », cocher le groupe d'essai, créer ; l'ouvrir dans deux navigateurs, sous deux noms | chacun arrive sur ses groupes sous son nom ; dans `/acces`, « 2 / 30 personnes » et deux identités « venue par » le lien ; un nom déjà pris est refusé, même autrement écrit (« luna » pour « Luna ») ; sur un navigateur où quelqu'un est connecté, « Ce n'est pas moi » le déconnecte ; « Révoquer » ferme le lien |
@@ -33,7 +33,7 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
| C14 | La page d'un groupe | Monnaie libre | le nom ; le pavé des quatre piliers (Docs de référence « ouvert », les trois autres « à venir ») ; « En cours » : l'Acte d'engagement Certification, qui ouvre son lecteur | | C14 | La page d'un groupe | Monnaie libre | le nom ; le pavé des quatre piliers (Docs de référence « ouvert », les trois autres « à venir ») ; « En cours » : l'Acte d'engagement Certification, qui ouvre son lecteur |
| C15 | Le menu | survoler les outils ; ouvrir Docs, puis Mandats ; revenir à l'accueil | « 1 » à côté de « Docs de référence » ; aucun outil surligné sur la page du groupe, seul celui où l'on est ensuite ; au survol, seul Docs réagit ; Mandats s'ouvre quand même (ses « À venir ») ; hors d'un groupe, les outils mènent au dernier groupe ouvert (ou à l'accueil s'il n'y en a pas) | | C15 | Le menu | survoler les outils ; ouvrir Docs, puis Mandats ; revenir à l'accueil | « 1 » à côté de « Docs de référence » ; aucun outil surligné sur la page du groupe, seul celui où l'on est ensuite ; au survol, seul Docs réagit ; Mandats s'ouvre quand même (ses « À venir ») ; hors d'un groupe, les outils mènent au dernier groupe ouvert (ou à l'accueil s'il n'y en a pas) |
| C16 | En cours, à venir | Monnaie libre → Docs, Mandats, Protocoles ; les anciennes adresses `/decisions`, `/mandats`, `/protocoles` | Docs : « En cours » (Acte d'engagement Certification) et « À venir » (Acte d'engagement forgerons) ; Mandats : Administration forum Duniter, Administration forum Monnaie libre ; Protocoles : Runtime upgrade — décision on-chain ; les « À venir » sont éteints, sans lien ; ils suivent la langue ; les anciennes adresses mènent aux outils du groupe courant | | C16 | En cours, à venir | Monnaie libre → Docs, Mandats, Protocoles ; les anciennes adresses `/decisions`, `/mandats`, `/protocoles` | Docs : « En cours » (Acte d'engagement Certification) et « À venir » (Acte d'engagement forgerons) ; Mandats : Administration forum Duniter, Administration forum Monnaie libre ; Protocoles : Runtime upgrade — décision on-chain ; les « À venir » sont éteints, sans lien ; ils suivent la langue ; les anciennes adresses mènent aux outils du groupe courant |
| C17 | Décisions | Monnaie libre → Décisions ; voter (par la chaîne : bulletin, transaction, confirmation) sur une version d'un article, recharger | une ligne par doc : « Acte d'engagement Certification · 41 modules à voter · dont 41 sans ton vote » (33 articles, le titre, l'ordre de 7 sections), puis « dont 40 » ; la ligne ouvre le lecteur | | C17 | Décisions | Monnaie libre → Décisions ; voter (par la chaîne : bulletin, transaction, confirmation) sur une version d'un article, recharger | une ligne par doc : « Acte d'engagement Certification · 33 modules à voter · dont 33 sans ton vote » (27 articles, le titre, l'ordre de 5 sections), puis « dont 32 » ; la ligne ouvre le lecteur |
| C18 | La Raison d'être | Monnaie libre → Acte d'engagement Certification → « Si tout passe », puis EN, puis ES | en tête du texte, « Raison d'être » : la première phrase en italique, puis deux paragraphes ; « Purpose », « Razón de ser » | | C18 | La Raison d'être | Monnaie libre → Acte d'engagement Certification → « Si tout passe », puis EN, puis ES | en tête du texte, « Raison d'être » : la première phrase en italique, puis deux paragraphes ; « Purpose », « Razón de ser » |
| C19 | Les versions d'un article | Monnaie libre → « Propositions en cours » → « Tout déplier » → « Connaissance suffisante » (au clavier : Tab jusqu'à la ligne, Entrée) | une seule ligne, « 2 versions » ; dépliée : « variante 1.0.0 » et « variante 0.4.0-draft », chacune avec son niveau et ce qui la distingue ; un clic sur une version déplie son vote sous elle (cran 3, quorum 50 : « 39 oui sur 50 votants pour passer »), sans « Autres versions » : l'autre version est juste là, dans la liste (lot U2) ; un nouveau clic sur la ligne la replie | | C19 | Les versions d'un article | Monnaie libre → « Propositions en cours » → « Tout déplier » → « Connaissance suffisante » (au clavier : Tab jusqu'à la ligne, Entrée) | une seule ligne, « 2 versions » ; dépliée : « variante 1.0.0 » et « variante 0.4.0-draft », chacune avec son niveau et ce qui la distingue ; un clic sur une version déplie son vote sous elle (cran 3, quorum 50 : « 39 oui sur 50 votants pour passer »), sans « Autres versions » : l'autre version est juste là, dans la liste (lot U2) ; un nouveau clic sur la ligne la replie |
| C20 | Réglages de la formule et cinq crans | Monnaie libre → Acte d'engagement : « Réglages de la formule » (en-tête) ; puis, en tête du texte, Unicité du compte (Proposition seed) → « Le vote » → « Réglages de la formule » ; dans le groupe d'essai, Réciprocité (entrée en vigueur par le vote) → « Le vote » → « Proposer un changement d'inertie » → 5 → « Soumettre au vote » | W 5 745 (toile de confiance Ğ1), B 0,1 ; « 100 votants sur 5 745 (1,74 %), deux fois 50, le quorum de référence : la participation de référence… (0 pour l'instant) » ; le tableau des cinq crans : très basse, basse, moyenne, haute, très haute ; M 50, 50, 55, 60, 66 % ; part exigée 60, 67, 75, 83, 90 % ; quorum 12, 12, 50, 50, 50 (ADR 0013) ; voix pour à 100 votants 60, 67, 75, 83, 90, « Voix pour : à 100 votants, comme d'habitude. », « Part exigée : la part de « pour » demandée, à 100 votants comme d'habitude. », « Quorum : en dessous, les votants qui manquent comptent comme « non ». … » ; depuis le vote déplié : « Inertie · 2 / 5 · basse », la ligne du cran 2 marquée « cet article », à 0 votant 8, 9, 39, 43, 46 voix, « Voix pour : à 0 votant, la participation de cette version. » ; le sélecteur montre les cinq crans, 2 « actuelle », atteignable au clavier mais jamais choisi ; un seul arrêt de tabulation ; les flèches passent d'un cran à l'autre et le choisissent, en boucle ; le vote de la proposition se déplie : « inertie vers 5 / 5 · très haute », votée à « inertie 2 / 5 · basse » | | C20 | Réglages de la formule et cinq crans | Monnaie libre → Acte d'engagement : « Réglages de la formule » (en-tête) ; puis, en tête du texte, Unicité du compte (Proposition seed) → « Le vote » → « Réglages de la formule » ; dans le groupe d'essai, Réciprocité (entrée en vigueur par le vote) → « Le vote » → « Proposer un changement d'inertie » → 5 → « Soumettre au vote » | W 5 745 (toile de confiance Ğ1), B 0,1 ; « 100 votants sur 5 745 (1,74 %), deux fois 50, le quorum de référence : la participation de référence… (0 pour l'instant) » ; le tableau des cinq crans : très basse, basse, moyenne, haute, très haute ; M 50, 50, 55, 60, 66 % ; part exigée 60, 67, 75, 83, 90 % ; quorum 12, 12, 50, 50, 50 (ADR 0013) ; voix pour à 100 votants 60, 67, 75, 83, 90, « Voix pour : à 100 votants, comme d'habitude. », « Part exigée : la part de « pour » demandée, à 100 votants comme d'habitude. », « Quorum : en dessous, les votants qui manquent comptent comme « non ». … » ; depuis le vote déplié : « Inertie · 2 / 5 · basse », la ligne du cran 2 marquée « cet article », à 0 votant 8, 9, 39, 43, 46 voix, « Voix pour : à 0 votant, la participation de cette version. » ; le sélecteur montre les cinq crans, 2 « actuelle », atteignable au clavier mais jamais choisi ; un seul arrêt de tabulation ; les flèches passent d'un cran à l'autre et le choisissent, en boucle ; le vote de la proposition se déplie : « inertie vers 5 / 5 · très haute », votée à « inertie 2 / 5 · basse » |
@@ -41,20 +41,20 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
| C22 | Le vote groupé | groupe d'essai : « Propositions en cours » → « Engagements techniques » → « Voter cette section » ; « Tout pour » ; dans « Connaissance suffisante », « Pour » sur la variante 1.0.0 ; recharger ; « Voter (n) » | le panier s'ouvre sur la section ; « Tout pour » remplit les articles à une version, pas « Connaissance suffisante » ni « Rencontre physique… » (« Plusieurs versions : choisis la tienne. », et le compte des articles laissés) ; le panier reste après rechargement (« Voter en lot · n ») ; un seul envoi : « n votes enregistrés. », le panier vide, la variante choisie à 1 pour | | C22 | Le vote groupé | groupe d'essai : « Propositions en cours » → « Engagements techniques » → « Voter cette section » ; « Tout pour » ; dans « Connaissance suffisante », « Pour » sur la variante 1.0.0 ; recharger ; « Voter (n) » | le panier s'ouvre sur la section ; « Tout pour » remplit les articles à une version, pas « Connaissance suffisante » ni « Rencontre physique… » (« Plusieurs versions : choisis la tienne. », et le compte des articles laissés) ; le panier reste après rechargement (« Voter en lot · n ») ; un seul envoi : « n votes enregistrés. », le panier vide, la variante choisie à 1 pour |
| C23 | Le vote par la chaîne Ğ1 | Monnaie libre, un portefeuille dans le navigateur : « Voter en lot », « Pour » sur Unicité du compte et Réciprocité (Propositions seed), « Signer et envoyer (2) » ; puis, en tête du texte, sous Réciprocité, « Contre » (l'indexeur en retard) puis « Retirer mon vote » ; puis sans portefeuille : la Raison d'être → « Pour » → « Passer par Ğecko » | une seule signature pour les deux : 0,1 Ğ1 vers le compte des votes et une remarque `ld1:` + 64 chiffres, rien du vote en clair ; « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « Compté pour l'identité Ğ1 « Alice ». » ; sous la proposition « Ton vote : pour · inscrit sur la chaîne Ğ1 », 1 pour ; « En attente de la chaîne Ğ1 : contre. » ; le retrait, plus récent sur la chaîne, fait foi même quand le « contre » est confirmé après lui (0 pour · 0 contre) ; Ğecko : un QR code et le lien `g1://<compte>?amount=0.1&comment=ld1:…`, puis la confirmation dès que la chaîne la montre | | C23 | Le vote par la chaîne Ğ1 | Monnaie libre, un portefeuille dans le navigateur : « Voter en lot », « Pour » sur Unicité du compte et Réciprocité (Propositions seed), « Signer et envoyer (2) » ; puis, en tête du texte, sous Réciprocité, « Contre » (l'indexeur en retard) puis « Retirer mon vote » ; puis sans portefeuille : la Raison d'être → « Pour » → « Passer par Ğecko » | une seule signature pour les deux : 0,1 Ğ1 vers le compte des votes et une remarque `ld1:` + 64 chiffres, rien du vote en clair ; « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « Compté pour l'identité Ğ1 « Alice ». » ; sous la proposition « Ton vote : pour · inscrit sur la chaîne Ğ1 », 1 pour ; « En attente de la chaîne Ğ1 : contre. » ; le retrait, plus récent sur la chaîne, fait foi même quand le « contre » est confirmé après lui (0 pour · 0 contre) ; Ğecko : un QR code et le lien `g1://<compte>?amount=0.1&comment=ld1:…`, puis la confirmation dès que la chaîne la montre |
| C24 | Sans compte des votes | Monnaie libre sur un serveur sans `LD_VOTE_ACCOUNT`, puis le groupe d'essai | pas de « Voter en lot » ni « Voter cette section » ; sous la première Proposition seed : « Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer. Tu peux lire, proposer et faire une objection. », pas de bouton, les totaux et « 9 oui sur 12 votants pour passer » restent ; le panier ouvert par son adresse (`?onglet=panier`) dit la même chose, son bouton éteint, et **jamais** « Tes votes comptent dès l'envoi » ; dans le groupe d'essai, le panier et les boutons sont là | | C24 | Sans compte des votes | Monnaie libre sur un serveur sans `LD_VOTE_ACCOUNT`, puis le groupe d'essai | pas de « Voter en lot » ni « Voter cette section » ; sous la première Proposition seed : « Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer. Tu peux lire, proposer et faire une objection. », pas de bouton, les totaux et « 9 oui sur 12 votants pour passer » restent ; le panier ouvert par son adresse (`?onglet=panier`) dit la même chose, son bouton éteint, et **jamais** « Tes votes comptent dès l'envoi » ; dans le groupe d'essai, le panier et les boutons sont là |
| C25 | L'ordre des articles | groupe d'essai : « Tout déplier » ; en fin de « Conclusion », la ligne « Ordre de la section » → « Réordonner » ; cliquer « Monnaie G1 » puis la place 1 ; au clavier, flèche bas puis haut ; « Proposer cet ordre » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas (quorum 1 dans le groupe d'essai : ADR 0013) ; puis, dans Monnaie libre, sur un téléphone, avec un portefeuille : « Voter en lot », « Pour » sur « Ordre des articles · Engagements fondamentaux », « Signer et envoyer (1) » | chaque section finit par **une ligne** « Ordre de la section » (7, aucune pour l'Ordonnancement, un seul article) : la poignée, les crans « basse », « seed a minima · 0 vote », **jamais la liste de ses articles** ; à la fin du texte quand la section s'y lit, sinon à la fin de sa section dans « Propositions en cours » ; le vote d'un ordre en vigueur déplié sous sa ligne montre la liste entière, et son inertie une fois, « Inertie · 2 / 5 · basse » (lot U2) ; « Réordonner » : les articles de la section, sur place, sans panneau ; l'article choisi marqué, puis à sa place, annoncé (« « Monnaie G1 » en place 1 sur 2. »), « Proposer cet ordre » éteint tant que c'est l'ordre en vigueur, et il le dit à côté (« C'est l'ordre en vigueur : déplace au moins un article. ») ; le vote de l'ordre proposé se déplie sous la ligne, l'ordre proposé en clair, « Ordre en vigueur » parmi ses « Autres versions », « 1 ordre proposé » sur la ligne ; aucune ligne d'ordre parmi les propositions ; il est dans le panier, à la fin de sa section ; à la voix qui suffit : « Le nouvel ordre de « Conclusion » est entré en vigueur. », la section se lit dans le nouvel ordre ; par la chaîne : « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « seed a minima · 1 vote » ; sur un téléphone, les gestes du titre sans survol (réordonner sur un téléphone : C35) | | C25 | L'ordre des articles | groupe d'essai : « Tout déplier » ; en fin de « Conclusion », la ligne « Ordre de la section » → « Réordonner » ; cliquer « Monnaie G1 » puis la place 1 ; au clavier, flèche bas puis haut ; « Proposer cet ordre » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas (quorum 1 dans le groupe d'essai : ADR 0013) ; puis, dans Monnaie libre, sur un téléphone, avec un portefeuille : « Voter en lot », « Pour » sur « Ordre des articles · Engagements fondamentaux », « Signer et envoyer (1) » | chaque section finit par **une ligne** « Ordre de la section » (5 ; aucune pour la formule, l'inertie, l'ordonnancement, sortis du document : ADR 0014) : la poignée, les crans « basse », « seed a minima · 0 vote », **jamais la liste de ses articles** ; à la fin du texte quand la section s'y lit, sinon à la fin de sa section dans « Propositions en cours » ; le vote d'un ordre en vigueur déplié sous sa ligne montre la liste entière, et son inertie une fois, « Inertie · 2 / 5 · basse » (lot U2) ; « Réordonner » : les articles de la section, sur place, sans panneau ; l'article choisi marqué, puis à sa place, annoncé (« « Monnaie G1 » en place 1 sur 2. »), « Proposer cet ordre » éteint tant que c'est l'ordre en vigueur, et il le dit à côté (« C'est l'ordre en vigueur : déplace au moins un article. ») ; le vote de l'ordre proposé se déplie sous la ligne, l'ordre proposé en clair, « Ordre en vigueur » parmi ses « Autres versions », « 1 ordre proposé » sur la ligne ; aucune ligne d'ordre parmi les propositions ; il est dans le panier, à la fin de sa section ; à la voix qui suffit : « Le nouvel ordre de « Conclusion » est entré en vigueur. », la section se lit dans le nouvel ordre ; par la chaîne : « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « seed a minima · 1 vote » ; sur un téléphone, les gestes du titre sans survol (réordonner sur un téléphone : C35) |
| C26 | Le titre du document | groupe d'essai : sous le titre, « Proposer un autre titre » ; « Acte d'engagement de la certification » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas ; lire en anglais | sous le titre, une ligne : « Titre du document · seed a minima · 0 vote », les crans « inertie 4 / 5 · haute », deux gestes ; le panneau nommé « Proposer un autre titre » ; le titre proposé se vote **sous le titre** (« 1 titre proposé »), jamais parmi les propositions ; à la voix qui suffit : « Le titre « … » est entré en vigueur. », le document change de titre, son vote reste déplié sous le titre, son inertie dite une fois (« Inertie · 4 / 5 · haute ») ; en anglais, l'ancien titre traduit, marqué « translation needs review » | | C26 | Le titre du document | groupe d'essai : sous le titre, « Proposer un autre titre » ; « Acte d'engagement de la certification » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas ; lire en anglais | sous le titre, une ligne : « Titre du document · seed a minima · 0 vote », les crans « inertie 4 / 5 · haute », deux gestes ; le panneau nommé « Proposer un autre titre » ; le titre proposé se vote **sous le titre** (« 1 titre proposé »), jamais parmi les propositions ; à la voix qui suffit : « Le titre « … » est entré en vigueur. », le document change de titre, son vote reste déplié sous le titre, son inertie dite une fois (« Inertie · 4 / 5 · haute ») ; en anglais, l'ancien titre traduit, marqué « translation needs review » |
| C27 | La participation ramenée dans ses bornes | un groupe de 4 dont la médiane est 2,5 votants, ramenée à 50 % (l'automate fait répondre ces chiffres au serveur) : « Réglages de la formule » | « 3 votants sur 4 (62,5 %) : la médiane des 12 propositions votées depuis un an. » — la part de la mesure elle-même, jamais 3 sur 4 arrondi d'abord ; puis « Ramenée entre 5 % et 50 % de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient (50 %). » ; « Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude. » | | C27 | La participation ramenée dans ses bornes | un groupe de 4 dont la médiane est 2,5 votants, ramenée à 50 % (l'automate fait répondre ces chiffres au serveur) : « Réglages de la formule » | « 3 votants sur 4 (62,5 %) : la médiane des 12 propositions votées depuis un an. » — la part de la mesure elle-même, jamais 3 sur 4 arrondi d'abord ; puis « Ramenée entre 5 % et 50 % de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient (50 %). » ; « Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude. » |
| C28 | Objection « redit » ; sur la version retenue, « contre » et retrait, jamais « pour » | groupe d'essai, à trois (Aude, Oscar, Vera) : Aude propose un nouvel article qui répète « Unicité du compte » ; Oscar, dans « Détails » de cette proposition → « Faire une objection » → « Redit », « En vigueur — Unicité du compte », un motif → « Envoyer l'objection » ; Vera déplie son vote → « Contre », puis « Retirer mon vote » ; Vera ouvre « Voter en lot » → « Tout le document » : « Tout pour », puis « Tout contre », puis « Vider » ; Aude ouvre « Messages » → « Modifier l'article visé », complète le texte et soumet ; Oscar ouvre « Messages » → « Lever l'objection » ; Vera déplie de nouveau son vote | « Objection enregistrée : jusqu'à sa levée, personne ne peut plus voter pour la dernière arrivée des deux versions. » ; chez Oscar, « « … » redit « Unicité du compte » », « personne ne peut plus voter pour cette version », « Lever l'objection » ; chez Vera, « Une objection retient cette version : jusqu'à sa levée, personne ne peut plus voter pour. Tu peux voter contre ou retirer ton vote ; à la levée, elle n'entre en vigueur qu'avec les voix encore là. », « Elle redit « Unicité du compte ». », pas de « Pour », mais « Contre », la ligne marquée « retenue par une objection » ; après « Contre » : « Vote enregistré. », « Ton vote : contre », « 0 pour · 1 contre », et « Retirer mon vote » ; après le retrait : « Vote retiré. », « 0 pour · 0 contre », toujours pas de « Pour » ; dans le panier, la version retenue montre « Contre » sans « Pour », avec « une objection la retient : contre ou retrait, pas de « pour » » ; « Tout pour » la laisse de côté et le dit (« Tu as tout mis pour, sauf 1 version qu'une objection retient : sur elle, tu peux voter contre ou retirer ton vote. », le nombre suivant les versions retenues du document) ; « Tout contre » la prend (« Contre » enfoncé) ; chez Aude, « Messages, 1 non lu », le message « Oscar fait une objection : ta version « … » redit « Unicité du compte »… Pour répondre, modifie l'article visé. », puis le panneau « Proposer » sur Unicité du compte, partant de son texte, avec « Cette proposition répond à une objection… » ; chez Oscar, « Ton objection sur « … » a reçu une réponse. », une échéance, « Lever l'objection » et « Réitérer l'objection » ; levée : « Objection levée. », le message dit « objection levée » sans plus aucun geste ; Vera peut de nouveau voter, « Pour » compris | | C28 | Objection « redit » ; sur la version retenue, « contre » et retrait, jamais « pour » | groupe d'essai, à trois (Aude, Oscar, Vera) : Aude propose un nouvel article qui répète « Unicité du compte » ; Oscar, dans « Détails » de cette proposition → « Faire une objection » → « Redit », « En vigueur — Unicité du compte », un motif → « Envoyer l'objection » ; Vera déplie son vote → « Contre », puis « Retirer mon vote » ; Vera ouvre « Voter en lot » → « Tout le document » : « Tout pour », puis « Tout contre », puis « Vider » ; Aude ouvre « Messages » → « Modifier l'article visé », complète le texte et soumet ; Oscar ouvre « Messages » → « Lever l'objection » ; Vera déplie de nouveau son vote | « Objection enregistrée : jusqu'à sa levée, personne ne peut plus voter pour la dernière arrivée des deux versions. » ; chez Oscar, « « … » redit « Unicité du compte » », « personne ne peut plus voter pour cette version », « Lever l'objection » ; chez Vera, « Une objection retient cette version : jusqu'à sa levée, personne ne peut plus voter pour. Tu peux voter contre ou retirer ton vote ; à la levée, elle n'entre en vigueur qu'avec les voix encore là. », « Elle redit « Unicité du compte ». », pas de « Pour », mais « Contre », la ligne marquée « retenue par une objection » ; après « Contre » : « Vote enregistré. », « Ton vote : contre », « 0 pour · 1 contre », et « Retirer mon vote » ; après le retrait : « Vote retiré. », « 0 pour · 0 contre », toujours pas de « Pour » ; dans le panier, la version retenue montre « Contre » sans « Pour », avec « une objection la retient : contre ou retrait, pas de « pour » » ; « Tout pour » la laisse de côté et le dit (« Tu as tout mis pour, sauf 1 version qu'une objection retient : sur elle, tu peux voter contre ou retirer ton vote. », le nombre suivant les versions retenues du document) ; « Tout contre » la prend (« Contre » enfoncé) ; chez Aude, « Messages, 1 non lu », le message « Oscar fait une objection : ta version « … » redit « Unicité du compte »… Pour répondre, modifie l'article visé. », puis le panneau « Proposer » sur Unicité du compte, partant de son texte, avec « Cette proposition répond à une objection… » ; chez Oscar, « Ton objection sur « … » a reçu une réponse. », une échéance, « Lever l'objection » et « Réitérer l'objection » ; levée : « Objection levée. », le message dit « objection levée » sans plus aucun geste ; Vera peut de nouveau voter, « Pour » compris |
| C29 | Objection « contredit », levée à l'échéance | groupe d'essai : Aude propose un article qui contredit « Réciprocité » ; Oscar fait une objection « Contredit » ; Aude déplie le vote de sa proposition → « Reformuler », change le fond, « Enregistrer la correction » ; Oscar ne fait rien ; trois jours passent (l'automate avance l'horloge du serveur : `POST /api/dev/clock`, connexion de développement seulement) | chez Aude : « Elle contredit « Réciprocité ». », « Pour qu'elle se vote à nouveau, reformule-la… », « Reformuler » ouvre « Détails » avec le formulaire de modification ; « Nouvelle version enregistrée : les votants devront reconfirmer. », puis « Une réponse est arrivée : Oscar lève ou réitère l'objection d'ici le … » ; trois jours après : l'objection dit « levée : 3 jours sans suite », la proposition se vote de nouveau, le message d'Oscar dit « objection levée : 3 jours sans suite », sans geste | | C29 | Objection « contredit », levée à l'échéance | groupe d'essai : Aude propose un article qui contredit « Réciprocité » ; Oscar fait une objection « Contredit » ; Aude déplie le vote de sa proposition → « Reformuler », change le fond, « Enregistrer la correction » ; Oscar ne fait rien ; trois jours passent (l'automate avance l'horloge du serveur : `POST /api/dev/clock`, connexion de développement seulement) | chez Aude : « Elle contredit « Réciprocité ». », « Pour qu'elle se vote à nouveau, reformule-la… », « Reformuler » ouvre « Détails » avec le formulaire de modification ; « Nouvelle version enregistrée : les votants devront reconfirmer. », puis « Une réponse est arrivée : Oscar lève ou réitère l'objection d'ici le … » ; trois jours après : l'objection dit « levée : 3 jours sans suite », la proposition se vote de nouveau, le message d'Oscar dit « objection levée : 3 jours sans suite », sans geste |
| C30 | La coquille d'un autre, validée par l'auteur | groupe d'essai : Aude propose « Clé unique et vivante » ; Vera, dans « Détails » → « Corriger une coquille », apostrophes typographiques → « Enregistrer la correction » ; Aude ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend l'auteur du texte : elle s'applique quand il la valide, ou d'elle-même le … » ; le texte ne change pas ; « Coquilles proposées » montre le diff, « proposée par Vera », « en attente : s'applique d'elle-même le … » ; chez Aude : « Vera propose une correction de coquille dans ta version « Clé unique et vivante ». », le diff, une échéance, « Valider » / « Refuser » ; « Correction appliquée. », le message dit « correction appliquée » ; le texte porte la correction, l'historique « coquille corrigée · par Vera · validée par Aude » | | C30 | La coquille d'un autre, validée par l'auteur | groupe d'essai : Aude propose « Clé unique et vivante » ; Vera, dans « Détails » → « Corriger une coquille », apostrophes typographiques → « Enregistrer la correction » ; Aude ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend l'auteur du texte : elle s'applique quand il la valide, ou d'elle-même le … » ; le texte ne change pas ; « Coquilles proposées » montre le diff, « proposée par Vera », « en attente : s'applique d'elle-même le … » ; chez Aude : « Vera propose une correction de coquille dans ta version « Clé unique et vivante ». », le diff, une échéance, « Valider » / « Refuser » ; « Correction appliquée. », le message dit « correction appliquée » ; le texte porte la correction, l'historique « coquille corrigée · par Vera · validée par Aude » |
| C31 | Coquille sur un texte en vigueur | groupe d'essai : une administratrice (Ada) ouvre le groupe ; Vera, sur « Raison d'être » → « Le vote » → « Coquilles, traductions, objections, historique » → « Corriger une coquille », « C'est le seul » → « C’est le seul » ; Ada ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend un administrateur, ce texte venant du seed… » ; le texte en vigueur ne change pas (« C'est le seul ») ; la correction en attente se lit dans « Détails » ; chez Ada : « Vera propose une correction de coquille dans « Raison d'être », un texte du seed : un administrateur la valide. » ; après « Valider », le texte en vigueur porte « C’est le seul » et l'historique « validée par Ada » | | C31 | Coquille sur un texte en vigueur | groupe d'essai : une administratrice (Ada) ouvre le groupe ; Vera, sur « Raison d'être » → « Le vote » → « Coquilles, traductions, objections, historique » → « Corriger une coquille », « C'est l'unique » → « C’est l’unique » ; Ada ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend un administrateur, ce texte venant du seed… » ; le texte en vigueur ne change pas (« C'est l'unique enjeu ») ; la correction en attente se lit dans « Détails » ; chez Ada : « Vera propose une correction de coquille dans « Raison d'être », un texte du seed : un administrateur la valide. » ; après « Valider », le texte en vigueur porte « C’est l’unique enjeu » et l'historique « validée par Ada » |
| C32 | Lâcher ou garder un bulletin | Monnaie libre, un portefeuille dans le navigateur (Eve, chaîne simulée), « Propositions en cours » → Document de révocation : « Contre » le nœud éteint ; puis « Contre », fermer la fenêtre pendant que le portefeuille demande, refuser dans le portefeuille ; puis « Contre », fermer, signer dans le portefeuille ; une autre personne, sans portefeuille : « Contre » → « Passer par Ğecko », fermer, payer depuis un téléphone | nœud éteint : « La chaîne Ğ1 ne répond pas. Réessaie dans un moment. », le bulletin lâché aussitôt, rien « en attente » sous l'article ; fenêtre fermée puis refus : lâché ; fenêtre fermée puis signature : gardé, et la chaîne le confirme sans la page (« Ton vote : contre · inscrit sur la chaîne Ğ1 ») ; Ğecko, fenêtre fermée : « En attente de la chaîne Ğ1 : contre. », puis confirmé sans la page | | C32 | Lâcher ou garder un bulletin | Monnaie libre, un portefeuille dans le navigateur (Eve, chaîne simulée), « Propositions en cours » → Document de révocation : « Contre » le nœud éteint ; puis « Contre », fermer la fenêtre pendant que le portefeuille demande, refuser dans le portefeuille ; puis « Contre », fermer, signer dans le portefeuille ; une autre personne, sans portefeuille : « Contre » → « Passer par Ğecko », fermer, payer depuis un téléphone | nœud éteint : « La chaîne Ğ1 ne répond pas. Réessaie dans un moment. », le bulletin lâché aussitôt, rien « en attente » sous l'article ; fenêtre fermée puis refus : lâché ; fenêtre fermée puis signature : gardé, et la chaîne le confirme sans la page (« Ton vote : contre · inscrit sur la chaîne Ğ1 ») ; Ğecko, fenêtre fermée : « En attente de la chaîne Ğ1 : contre. », puis confirmé sans la page |
| C33 | Le reçu de ton vote | Monnaie libre, un portefeuille : « Contre » sur Ne jamais certifier seul (« Propositions en cours »), « Télécharger ton reçu », « Tes bulletins » ; sur « Tes bulletins », « Fichier du reçu » : le reçu ; puis coller le reçu avec un choix changé, « Vérifier » ; une autre personne ouvre « Tes bulletins » | le reçu `recu-vote-<date>-<id>.json` : les votes, le sel, la remarque `ld1:…`, « confirmed », l'identité « Alice » ; « Tes bulletins de vote » : « Compté pour l'identité Ğ1 « Alice » » et la remarque ; « Le reçu tient : ses votes et son sel donnent bien ce commentaire. », « Ne jamais certifier seul : contre » ; changé : « Le reçu ne tient pas » ; l'autre personne : « Tu n'as encore envoyé aucun bulletin par la chaîne Ğ1. », et le reçu lui est refusé | | C33 | Le reçu de ton vote | Monnaie libre, un portefeuille : « Contre » sur Ne jamais certifier seul (« Propositions en cours »), « Télécharger ton reçu », « Tes bulletins » ; sur « Tes bulletins », « Fichier du reçu » : le reçu ; puis coller le reçu avec un choix changé, « Vérifier » ; une autre personne ouvre « Tes bulletins » | le reçu `recu-vote-<date>-<id>.json` : les votes, le sel, la remarque `ld1:…`, « confirmed », l'identité « Alice » ; « Tes bulletins de vote » : « Compté pour l'identité Ğ1 « Alice » » et la remarque ; « Le reçu tient : ses votes et son sel donnent bien ce commentaire. », « Ne jamais certifier seul : contre » ; changé : « Le reçu ne tient pas » ; l'autre personne : « Tu n'as encore envoyé aucun bulletin par la chaîne Ğ1. », et le reçu lui est refusé |
| C34 | Le vote simulé, la chaîne fermée (ADR 0012) | Monnaie libre sur un serveur sans compte des votes (l'automate ferme la chaîne du serveur jetable, puis la rouvre) : « Propositions en cours » → Logiciel auditable → « Contre », sans portefeuille ; puis « Voter en lot », « Contre » encore, « Voter (1) » ; puis « Tes bulletins » | « Voter en lot » est là, marqué « vote simulé » ; aucune fenêtre de portefeuille ; « Vote simulé. », **jamais** « Vote enregistré. » ; « Ton vote : contre · simulé » aussitôt, et un « contre » de plus dans les totaux ; dans l'API, `myVoteState` « simulated » et `tallySimulated` ; le panier : « 1 vote simulé. », jamais « enregistré » ; dans « Tes bulletins » : « Simulé : compté pour « Sacha » tant que les votes simulés ne sont pas effacés », sans bloc (l'ouverture de la chaîne n'efface rien : c'est un acte humain, sans date promise) | | C34 | Le vote simulé, la chaîne fermée (ADR 0012) | Monnaie libre sur un serveur sans compte des votes (l'automate ferme la chaîne du serveur jetable, puis la rouvre) : « Propositions en cours » → Logiciel auditable → « Contre », sans portefeuille ; puis « Voter en lot », « Contre » encore, « Voter (1) » ; puis « Tes bulletins » | « Voter en lot » est là, marqué « vote simulé » ; aucune fenêtre de portefeuille ; « Vote simulé. », **jamais** « Vote enregistré. » ; « Ton vote : contre · simulé » aussitôt, et un « contre » de plus dans les totaux ; dans l'API, `myVoteState` « simulated » et `tallySimulated` ; le panier : « 1 vote simulé. », jamais « enregistré » ; dans « Tes bulletins » : « Simulé : compté pour « Sacha » tant que les votes simulés ne sont pas effacés », sans bloc (l'ouverture de la chaîne n'efface rien : c'est un acte humain, sans date promise) |
| C35 | Réordonner une section, par poignées, puis voter (lot U) | groupe d'essai : à la fin des « Engagements fondamentaux », la ligne « Ordre de la section » → « Réordonner » ; glisser la poignée de Réciprocité sur Unicité du compte ; au clavier, flèche haut puis bas ; « Descendre « Réciprocité » » ; cliquer Réciprocité puis la place 2 ; « Annuler », puis « Réordonner » et « Monter « Réciprocité » » ; « Proposer cet ordre » ; « Pour » (une deuxième voix si la sienne ne suffit pas) ; puis, sur un téléphone, dans Monnaie libre : « Tout déplier », « Ordre des articles · Engagements techniques » → « Réordonner » | la ligne ne répète aucun article ; « Réordonner » : les articles de la section, en vigueur ou seulement proposés (« Engagements fondamentaux » marqué « proposé »), sur place (pas de panneau), une poignée devant chacun, les articles en vigueur de la section repliés dans la liste (pas deux fois) ; chaque geste annonce la place (« « Réciprocité » en place 2 sur 3. ») ; « Proposer cet ordre » éteint sur l'ordre en vigueur, et il le dit à côté, lu aussi comme sa description (« C'est l'ordre en vigueur : déplace au moins un article. ») : la phrase s'en va dès qu'un article bouge et revient quand l'ordre redevient celui en vigueur (lot U2) ; « Annuler » rend la section telle qu'elle se lisait ; « Ordre proposé au vote. », son vote déplié aussitôt sous la ligne, l'ordre proposé en clair, « 1 ordre proposé » ; « Vote enregistré. » ; en vigueur, la section se lit dans le nouvel ordre, « adopté le … » ; sur un téléphone : 11 articles, rien ne déborde de la page, l'article, sa poignée et ses flèches font au moins 44 px | | C35 | Réordonner une section, par poignées, puis voter (lot U) | groupe d'essai : à la fin des « Engagements fondamentaux », la ligne « Ordre de la section » → « Réordonner » ; glisser la poignée de Réciprocité sur Responsabilité civique ; au clavier, flèche haut puis bas ; « Descendre « Réciprocité » » ; cliquer Réciprocité puis la place 2 ; « Annuler », puis « Réordonner » et deux fois « Monter « Réciprocité » » ; « Proposer cet ordre » ; « Pour » (une deuxième voix si la sienne ne suffit pas) ; puis, sur un téléphone, dans Monnaie libre : « Tout déplier », « Ordre des articles · Engagements techniques » → « Réordonner » | la ligne ne répète aucun article ; « Réordonner » : les articles de la section, en vigueur ou seulement proposés (« Engagements fondamentaux » marqué « proposé »), sur place (pas de panneau), une poignée devant chacun, les articles en vigueur de la section repliés dans la liste (pas deux fois) ; chaque geste annonce la place (« « Réciprocité » en place 3 sur 4. ») ; « Proposer cet ordre » éteint sur l'ordre en vigueur, et il le dit à côté, lu aussi comme sa description (« C'est l'ordre en vigueur : déplace au moins un article. ») : la phrase s'en va dès qu'un article bouge et revient quand l'ordre redevient celui en vigueur (lot U2) ; « Annuler » rend la section telle qu'elle se lisait ; « Ordre proposé au vote. », son vote déplié aussitôt sous la ligne, l'ordre proposé en clair, « 1 ordre proposé » ; « Vote enregistré. » ; en vigueur, la section se lit dans le nouvel ordre, « adopté le … » ; sur un téléphone : 11 articles, rien ne déborde de la page, l'article, sa poignée et ses flèches font au moins 44 px |
| C36 | Combien de oui pour passer, combien de non pour bloquer (ADR 0013 ; Yvv, 02/11) | Monnaie libre : la Raison d'être (en tête) → « Le vote » ; « Tout déplier » ; puis, l'automate donnant à une proposition du groupe d'essai les totaux 9 pour · 4 contre (cran 2, quorum 12), puis 3 pour · 0 contre | avant tout vote, sous l'article : « 9 oui sur 12 votants pour passer », au survol « Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ». » ; la ligne de la Proposition seed, juste au-dessus, ne redit plus les totaux ni les oui (lot U2) ; dans la liste, en bref : « 39 oui sur 50 » (cran 3) ; à 9 pour · 4 contre : « 11 oui pour passer · 4 non pour bloquer » sous l'article, « passe à 11 oui · bloquée à 4 non » dans la liste, « Il manque 2 voix pour. » ; à 3 pour · 0 contre : « 9 oui pour passer · le quorum la bloque », « passe à 9 oui » | | C36 | Combien de oui pour passer, combien de non pour bloquer (ADR 0013 ; Yvv, 02/11) | Monnaie libre : la Raison d'être (en tête) → « Le vote » ; « Tout déplier » ; puis, l'automate donnant à une proposition du groupe d'essai les totaux 9 pour · 4 contre (cran 2, quorum 12), puis 3 pour · 0 contre | avant tout vote, sous l'article : « 9 oui sur 12 votants pour passer », au survol « Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ». » ; la ligne de la Proposition seed, juste au-dessus, ne redit plus les totaux ni les oui (lot U2) ; dans la liste, en bref : « 39 oui sur 50 » (cran 3) ; à 9 pour · 4 contre : « 11 oui pour passer · 4 non pour bloquer » sous l'article, « passe à 11 oui · bloquée à 4 non » dans la liste, « Il manque 2 voix pour. » ; à 3 pour · 0 contre : « 9 oui pour passer · le quorum la bloque », « passe à 9 oui » |
| C37 | Le quorum d'un article, par son autrice, avant le premier vote (ADR 0013) | groupe d'essai : proposer un article, puis son vote → « Quorum » : 3 → « Fixer ce quorum » ; « Revenir au défaut (1) » ; 100000 → « Fixer ce quorum » ; une autre personne ouvre le même vote, puis vote « contre » ; l'autrice tente 2 | le réglage n'apparaît qu'à l'autrice (`canSetQuorum`) : « Quorum · 1 votant », « Par défaut, celui de son cran : 1 votant. », le champ « Quorum de l'article » ; « Quorum fixé : 3 votants. », la ligne dit « … oui sur 3 votants pour passer » ; « Quorum revenu au défaut : 1 votant. » ; hors bornes (400) : « Ce quorum sort des bornes : au moins 2 votants (1 dans le groupe d'essai), au plus … » ; l'autre personne ne voit pas le réglage ; après son vote (409) : « Quelqu'un a déjà voté sur cet article : son quorum ne change pas. » (lot T2), puis le réglage disparaît | | C37 | Le quorum d'un article, par son autrice, avant le premier vote (ADR 0013) | groupe d'essai : proposer un article, puis son vote → « Quorum » : 3 → « Fixer ce quorum » ; « Revenir au défaut (1) » ; 100000 → « Fixer ce quorum » ; une autre personne ouvre le même vote, puis vote « contre » ; l'autrice tente 2 | le réglage n'apparaît qu'à l'autrice (`canSetQuorum`) : « Quorum · 1 votant », « Par défaut, celui de son cran : 1 votant. », le champ « Quorum de l'article » ; « Quorum fixé : 3 votants. », la ligne dit « … oui sur 3 votants pour passer » ; « Quorum revenu au défaut : 1 votant. » ; hors bornes (400) : « Ce quorum sort des bornes : au moins 2 votants (1 dans le groupe d'essai), au plus … » ; l'autre personne ne voit pas le réglage ; après son vote (409) : « Quelqu'un a déjà voté sur cet article : son quorum ne change pas. » (lot T2), puis le réglage disparaît |
| C38 | Ce que compte l'en-tête ; un changement d'inertie d'un ordre ou du titre (lot U2) | Monnaie libre : lire la ligne de faits sous le résumé ; « Tout déplier », compter les versions de la liste ; puis, dans le groupe d'essai, proposer un changement d'inertie de l'ordre de « Conclusion » et du titre (l'automate passe par `POST /api/units/{id}/inertia`, puis les retire) | « 0 article en vigueur · 3 Propositions seed · N propositions en cours », N le nombre de versions que montre la liste : les Propositions seed lues en tête sont dites à part, les ordres et titres proposés ne sont pas comptés (ils se votent à la ligne de leur module) ; sous le titre, la pastille « 1 changement d'inertie proposé », qui déplie son vote sous le titre ; à la fin de « Conclusion », la même pastille sur la ligne d'ordre, qui déplie son vote là ; aucun « inertie vers … » dans « Propositions en cours », et l'en-tête compte toujours ce que la liste montre | | C38 | Ce que compte l'en-tête ; un changement d'inertie d'un ordre ou du titre (lot U2) | Monnaie libre : lire la ligne de faits sous le résumé ; « Tout déplier », compter les versions de la liste ; puis, dans le groupe d'essai, proposer un changement d'inertie de l'ordre de « Conclusion » et du titre (l'automate passe par `POST /api/units/{id}/inertia`, puis les retire) | « 0 article en vigueur · 4 Propositions seed · N propositions en cours », N le nombre de versions que montre la liste : les Propositions seed lues en tête sont dites à part, les ordres et titres proposés ne sont pas comptés (ils se votent à la ligne de leur module) ; sous le titre, la pastille « 1 changement d'inertie proposé », qui déplie son vote sous le titre ; à la fin de « Conclusion », la même pastille sur la ligne d'ordre, qui déplie son vote là ; aucun « inertie vers … » dans « Propositions en cours », et l'en-tête compte toujours ce que la liste montre |
## Au-delà de la liste ## Au-delà de la liste
@@ -99,3 +99,4 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa
| 2026-11-03 | automate | local, lot T3 sur `b2d653e` (N1.1 et F1.2 sans chiffre daté, chaque phrase vraie pour tout W et tout quorum ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests) au second passage ; le premier, 40 / 42 : C33 et C38 tombés sur `net::ERR_NETWORK_CHANGED` (le réseau du poste a changé pendant le passage, vu dans leurs traces) | | 2026-11-03 | automate | local, lot T3 sur `b2d653e` (N1.1 et F1.2 sans chiffre daté, chaque phrase vraie pour tout W et tout quorum ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests) au second passage ; le premier, 40 / 42 : C33 et C38 tombés sur `net::ERR_NETWORK_CHANGED` (le réseau du poste a changé pendant le passage, vu dans leurs traces) |
| 2026-11-03 | automate | local, lot T3b sur `582d6ba` (après la relecture de `2ce2028` : la phrase de G de N1.1, « autant de « pour » » ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests) au second passage ; le premier, 40 / 42 : C8 et C32 tombés sur `net::ERR_NETWORK_CHANGED` (modules de l'interface non chargés, vu dans leurs traces) | | 2026-11-03 | automate | local, lot T3b sur `582d6ba` (après la relecture de `2ce2028` : la phrase de G de N1.1, « autant de « pour » » ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests) au second passage ; le premier, 40 / 42 : C8 et C32 tombés sur `net::ERR_NETWORK_CHANGED` (modules de l'interface non chargés, vu dans leurs traces) |
| 2026-11-03 | automate | local, lot T3c sur `42c803e` (après la relecture de `e7ab82f` : « à la participation habituelle » à la place de « comme d'habitude » dans F1.2 et N1.1, « le quorum de l'article », « la part exigée », « repose sur moins de 5 propositions », « ramenée » ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests), du premier coup (19 h 51 - 19 h 55, sous le verrou `/tmp/ld-e2e.lock`) | | 2026-11-03 | automate | local, lot T3c sur `42c803e` (après la relecture de `e7ab82f` : « à la participation habituelle » à la place de « comme d'habitude » dans F1.2 et N1.1, « le quorum de l'article », « la part exigée », « repose sur moins de 5 propositions », « ramenée » ; aucun contrôle changé) | C1–C38, M1, M2 | 40 / 40 verts (42 tests), du premier coup (19 h 51 - 19 h 55, sous le verrou `/tmp/ld-e2e.lock`) |
| 2026-11-03 | automate | local, lot T4 sur `3996f6f` (les textes d'Yvv du 03/11 pour I1, E1, E2 ; E12 « Responsabilité civique » entre E1 et E2 ; formule, inertie et ordonnancement sortis du document, ADR 0014 : C3, C9, C17, C18, C25, C31, C35, C38 repris ; C35 glisse d'une place) | C1–C38, M1, M2 | 40 / 40 verts (42 tests) à la boucle finale (21 h 47 - 21 h 50, sous le verrou `/tmp/ld-e2e.lock`) ; avant, 39 / 42 (C18, C31, C35 lisaient l'ancien texte d'I1 ou trois articles), puis 41 / 42 (C35 : un glisser de deux places réordonne mais n'est pas annoncé, signalé pour U3) |
@@ -0,0 +1,296 @@
# Textes d'amorce et structure de l'Acte d'engagement (lot T4)
> Préparé le 2026-11-03 pour ta relecture, Yvv (lot T4, branche `lot/t4-amorce`), d'après ta
> demande du 03/11 vers 20 h 38 : « Modif des textes Acte engagement » (I1, E1, un article
> nouveau « Responsabilité civique », E2) et « les 3 derniers modules ne sont pas de votes :
> ordonnancement, inertie, et formule de vote sont des infos de fonctionnement […] Vire les 3 du
> bas de la liste. » Les textes sont dans `seeds/acte-engagement-certification.json` et dans des
> amendements gardés pour la production ; la décision durable est l'ADR 0014. **Rien n'est en
> ligne.** L'interface des informations de fonctionnement est le lot U3.
## À confirmer : les choix faits pour toi
Chacun est appliqué ; dis lequel tu changes.
1. **« il y a une et une seule clé certifiante ».** Ta phrase disait « derrière un être humain,
il n'y a une et une seule clé certifiante ». Lu comme une coquille, sur le parallèle avec
« il y a un et un seul être humain vivant ». L'autre lecture serait « il n'y a qu'une seule
clé » (le texte d'avant).
2. **I1 en trois paragraphes**, le premier en italique entre « », comme aujourd'hui, avec les
espaces insécables du texte d'avant (à l'intérieur des guillemets, devant le point-virgule).
3. **Tes virgules, gardées telles quelles** : « de nature contractuelle, qui fonde l'usage »
(I1) et « qui s'engagent elles-mêmes, à scrupuleusement respecter » (E2). La seconde sépare
le verbe de son complément ; sans elle : « qui s'engagent elles-mêmes à scrupuleusement
respecter ». En anglais et en espagnol, la première est rendue, la seconde non (elle n'y
aurait pas de sens).
4. **Le deuxième paragraphe d'I1** est le tien mot pour mot : « C'est l'unique enjeu de la
certification et de votre engagement, il donne à notre toile fiduciaire sa valeur : … ».
5. **« ces trois principes fondamentaux »** (E2) se lit, comme « ces deux présents engagements »
avant, sur les articles de la section : E1, E12 et E2.
6. **L'article nouveau** : code **E12**, Proposition seed (ouverte, mise en avant, 0 vote), au
**cran 2** comme I1, E1 et E2, section « fondamental », **entre E1 et E2**. Titres : « Civic
responsibility », « Responsabilidad cívica ». Le code : E3 à E11 existent, E12 est le premier
libre de la famille des engagements ; c'est un identifiant, la place vient de l'ordre de la
section (ADR 0009), pas du numéro. Aucune unité ne porte E12, en production non plus (les
articles de membre prennent des codes en N).
7. **Les titres restent** : « Raison d'être », « Unicité du compte », « Réciprocité ».
8. **Les traductions** : l'anglais britannique et les guillemets “ ” des textes d'aujourd'hui ;
l'espagnol avec «» serrés. « votre engagement » est rendu « your commitment » et « su
compromiso » (le vouvoiement, comme le français). « l'unique enjeu » : « the one thing at
stake », « lo único que está en juego ».
9. **En production, rien n'est effacé** (ADR 0014) : les sept articles et les trois ordres
sortent du document par une marque (`units.retired_at`), et seulement si personne n'y a
laissé de trace. L'ordre de « fondamental » prend E12 entre E1 et E2 tant que personne n'a
voté sur cet ordre ; sinon E12 va en fin de section, comme tout nouvel article.
## Le document après T4
Cinq sections, 27 articles : introduction (I1, I2) ; fondamental (T1, E1, **E12**, E2) ;
technique (T2, E3 à E7, T3, E8 à E11) ; conclusion (K1, K2) ; annexe (X1 à X2.3). Quatre Propositions
seed au cran 2 : I1, E1, E12, E2. Aucun article en vigueur au départ (ADR 0011). « Modules à
voter » : 33 (27 articles, le titre, l'ordre de 5 sections), contre 41 avant.
## Les textes, avant et après
Les blocs sont les textes exacts du seed (Markdown : `*…*` est l'italique), espaces insécables compris. « Avant » : le texte en ligne aujourd'hui, celui du seed de M8.
### I1 — Raison d'être
#### I1 · Français (fait foi)
Titre : **Raison d'être**
Avant :
```markdown
*« Derrière une clé il n'y a qu'un seul être humain vivant ; derrière un être humain, il n'y a qu'une seule clé certifiante. »*
C'est le seul et unique propos de la certification, qui donne à notre toile fiduciaire sa valeur. Nous pouvons nous fier à l'unicité et l'intégrité de tout geste signé par cette clé. C'est l'enjeu de la certification.
Cet acte d'engagement est par ailleurs le seul acte de nature contractuelle qui fonde l'usage de la première monnaie libre.
```
Après :
```markdown
*« Derrière une clé il y a un et un seul être humain vivant ; derrière un être humain, il y a une et une seule clé certifiante. »*
C'est l'unique enjeu de la certification et de votre engagement, il donne à notre toile fiduciaire sa valeur : pouvoir nous fier à l'unicité et l'intégrité de tout geste signé par cette clé.
Cet acte d'engagement est par ailleurs le seul acte de nature contractuelle, qui fonde l'usage de la première monnaie libre.
```
#### I1 · English
Titre : **Purpose**
Avant :
```markdown
*“Behind a key there is only one living human being; behind a human being, there is only one certifying key.”*
This is the one and only purpose of certification, which gives our fiduciary web its value. We can rely on the uniqueness and integrity of every action signed with this key. That is what is at stake in certification.
This commitment act is, moreover, the only act of a contractual nature on which the use of the first libre currency is founded.
```
Après :
```markdown
*“Behind a key there is one and only one living human being; behind a human being, there is one and only one certifying key.”*
This is the one thing at stake in certification and in your commitment; it gives our fiduciary web its value: being able to rely on the uniqueness and integrity of every action signed with this key.
This commitment act is, moreover, the only act of a contractual nature, which founds the use of the first libre currency.
```
#### I1 · Español
Titre : **Razón de ser**
Avant :
```markdown
*«Detrás de una clave no hay más que un solo ser humano vivo; detrás de un ser humano, no hay más que una sola clave certificadora.»*
Este es el propósito único y exclusivo de la certificación, que da su valor a nuestra red fiduciaria. Podemos fiarnos de la unicidad y la integridad de toda acción firmada con esta clave. Eso es lo que está en juego en la certificación.
Esta acta de compromiso es, además, el único acto de naturaleza contractual en el que se funda el uso de la primera moneda libre.
```
Après :
```markdown
*«Detrás de una clave hay un solo y único ser humano vivo; detrás de un ser humano, hay una sola y única clave certificadora.»*
Es lo único que está en juego en la certificación y en su compromiso; da su valor a nuestra red fiduciaria: poder fiarnos de la unicidad y la integridad de toda acción firmada con esta clave.
Esta acta de compromiso es, además, el único acto de naturaleza contractual, que funda el uso de la primera moneda libre.
```
### E1 — Unicité du compte
#### E1 · Français (fait foi)
Titre : **Unicité du compte**
Avant :
```markdown
Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte cocréateur de monnaie G1.
```
Après :
```markdown
Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte certifiant.
```
#### E1 · English
Titre : **Uniqueness of the account**
Avant :
```markdown
I commit on my honour to have, now and ever, no more than one single and unique account co-creating G1 money.
```
Après :
```markdown
I commit on my honour to have, now and ever, no more than one single and unique certifying account.
```
#### E1 · Español
Titre : **Unicidad de la cuenta**
Avant :
```markdown
Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta cocreadora de moneda G1.
```
Après :
```markdown
Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta certificadora.
```
### E12 — Responsabilité civique (nouveau)
#### E12 · Français (fait foi)
Titre : **Responsabilité civique**
```markdown
Je déclare sur l'honneur avoir la capacité légale et physique de signer un contrat et d'en assumer la responsabilité civique, pour certifier à mon tour.
```
#### E12 · English
Titre : **Civic responsibility**
```markdown
I declare on my honour that I have the legal and physical capacity to sign a contract and to assume its civic responsibility, in order to certify in my turn.
```
#### E12 · Español
Titre : **Responsabilidad cívica**
```markdown
Declaro por mi honor tener la capacidad legal y física de firmar un contrato y de asumir su responsabilidad cívica, para certificar a mi vez.
```
### E2 — Réciprocité
#### E2 · Français (fait foi)
Titre : **Réciprocité**
Avant :
```markdown
Je m'engage sur l'honneur à ne certifier que des personnes physiques qui respectent scrupuleusement ces deux présents engagements fondamentaux.
```
Après :
```markdown
Je m'engage sur l'honneur à ne certifier que des personnes physiques qui s'engagent elles-mêmes, à scrupuleusement respecter ces trois principes fondamentaux.
```
#### E2 · English
Titre : **Reciprocity**
Avant :
```markdown
I commit on my honour to certify only natural persons who scrupulously respect these two fundamental commitments.
```
Après :
```markdown
I commit on my honour to certify only natural persons who themselves commit to scrupulously respecting these three fundamental principles.
```
#### E2 · Español
Titre : **Reciprocidad**
Avant :
```markdown
Me comprometo por mi honor a certificar únicamente a personas físicas que respeten escrupulosamente estos dos compromisos fundamentales.
```
Après :
```markdown
Me comprometo por mi honor a certificar únicamente a personas físicas que se comprometan ellas mismas a respetar escrupulosamente estos tres principios fundamentales.
```
## Ce qui sort du document
| Section | Article | Titre |
|---|---|---|
| formule | F1 | Formule de vote |
| formule | F1.1 | Formule du seuil WoT |
| formule | F1.2 | Paramètres par défaut |
| formule | F1.3 | Processus de dépôt officiel |
| inertie | N1 | Réglage de l'inertie |
| inertie | N1.1 | Niveaux d'inertie |
| ordonnancement | O1 | Ordonnancement du document |
Et l'ordre des articles de chacune de ces trois sections (`ordre.formule`, `ordre.inertie`,
`ordre.ordonnancement`). Ils ne sont plus ni listés, ni votables (vote, proposition, coquille,
objection, inertie, quorum : refusés), ni comptés, ni publiés. Aucun autre article ne les
cite.
**Leurs textes ne se perdent pas.** Ce qu'ils disaient de vrai (la formule, les crans, le
quorum, chaque phrase vérifiée par un test des lots T à T3c, `t3c-liste-fermee.md`) est la
référence de l'écran du lot U3, qui les montrera hors du vote, à leur place dans l'interface.
Leurs derniers textes restent dans `crates/ld-server/tests/fixtures/seed-m8/` et, en
production, dans leurs lignes.
## En production
- **I1, E1, E2** : trois amendements gardés (`2026-11-03-i1-amorce`, `-e1-amorce`,
`-e2-amorce`) réécrivent la Proposition seed, une fois par document, seulement si le texte en
place est celui qu'on attend (son empreinte) et que personne n'y a touché : un vote, un
bulletin, une coquille, une traduction de membre, une objection suffisent à garder le texte,
avec sa raison. L'ancien texte reste dans l'historique de la version.
- **E12** : un ajout gardé (`2026-11-03-e12-responsabilite-civique`), une fois par document.
- **Formule, inertie, ordonnancement** : le retrait `2026-11-03-fonctionnement-hors-vote`,
une fois par document, tous ou aucun.
- **La file de publication** (suspendue jusqu'au premier texte en vigueur) : rien n'y est
effacé. Les lignes déjà écrites pour F1.2, N1.1… restent et partiront avec le premier texte,
comme lignes du message de commit ; les textes publiés ne contiennent plus ces articles. T4
ajoute ses propres lignes (les trois textes, E12, le retrait).
- **Un retour arrière** vers l'image d'avant remet les sept articles et les trois ordres tels
qu'ils étaient (vérifié avec l'image `de1b7eb`) ; E12 et les nouveaux textes d'I1, E1, E2
restent.
+32 -164
View File
@@ -1,5 +1,5 @@
{ {
"_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). Aucun article en vigueur au départ. I1, E1, E2 : Propositions seed (« seedProposal »), ouvertes, mises en avant pour amorcer le vote, sans niveau d'inertie ici : elles démarrent au cran SEED_PROPOSAL_LEVEL (crates/ld-server/src/seed.rs : 2 « basse », choix d'Yvv du 2026-11-02, qui remontera leur inertie par un vote une fois les textes passés ; ADR 0011). Tant qu'aucun article n'est en vigueur, la publication attend le premier texte. Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21), inertie 3 / 5. 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, en vigueur sans vote (seed a minima ; 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). Quorums par cran (« quorums », ADR 0013, Yvv, 2026-11-02 et 03) : 12 · 12 · 50 · 50 · 50 dans Monnaie libre, 1 partout dans le groupe d'essai ; sous le quorum, les votants qui manquent comptent comme « non » ; voteFloor reste le quorum de référence de la participation p0 = 2 × voteFloor / W. Une base existante reçoit ces changements par des amendements gardés (seed_amend, seed_proposal_amend, quorum_amend), jamais en réinstanciant le document. F1.1, F1.2 et N1.1 réécrits pour le quorum par cran (ADR 0013 ; Yvv, 2026-11-02 et 03 ; lot T, textes relus dans docs/chantiers/acte-certification/textes-quorum-seed.md) : sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part du cran. Trois phrases reprises après la relecture de M6b (lot T2) : la part du cran, la plus haute au quorum, baisse quand la participation monte (F1.1) ; l'auteur fixe le quorum tant que l'article ne garde la trace d'aucun vote (F1.2) ; le quorum n'est pas un nombre minimal de votants (N1.1). Lot T3 (Yvv, 03/11 : « pas de chiffre datés dans un texte voté ») : l'exemple chiffré de N1.1 retiré (aucun nombre des textes ne dépend de W ni d'une participation) ; N1.1, sous le quorum, il faut autant de « pour » que si le quorum avait voté (vrai pour tout quorum) ; N1.1 et F1.2, la part du cran comme d'habitude au-delà du quorum. Après la relecture de 2ce2028 (lot T3b, même amendement) : N1.1, G se calcule pour que la part exigée, au-delà du quorum, soit celle du cran quand le groupe vote comme d'habitude (vrai pour tout W et tout quorum). Après la relecture de e7ab82f (lot T3c, mêmes amendements) : F1.2 et N1.1 écrivent le terme défini, « à la participation habituelle » (la médiane ramenée entre 1 % et 50 % de W, celle sur laquelle G se calcule), à la place de « comme d'habitude », faux quand la médiane sort de ses bornes ; N1.1, « sous le quorum de l'article », « la part exigée », « Tant qu'elle repose sur moins de 5 propositions », « Elle est ramenée entre 1 % et 50 % » (liste fermée des phrases : docs/chantiers/acte-certification/t3c-liste-fermee.md).", "_source": "maquette/frontend/app/data/seeds/duniter-g1.bundle.json (doc engagement-certification, 33 clauses + 2 variantes). Aucun article en vigueur au départ. I1, E1, E12, E2 : Propositions seed (« seedProposal »), ouvertes, mises en avant pour amorcer le vote, sans niveau d'inertie ici : elles démarrent au cran SEED_PROPOSAL_LEVEL (crates/ld-server/src/seed.rs : 2 « basse », choix d'Yvv du 2026-11-02, qui remontera leur inertie par un vote une fois les textes passés ; ADR 0011). Tant qu'aucun article n'est en vigueur, la publication attend le premier texte. Tout le reste : propositions ouvertes à 0 vote (Yvv, 2026-10-21), inertie 3 / 5. 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, en vigueur sans vote (seed a minima ; revue du 2026-10-30, ADR 0009). Quorums par cran (« quorums », ADR 0013, Yvv, 2026-11-02 et 03) : 12 · 12 · 50 · 50 · 50 dans Monnaie libre, 1 partout dans le groupe d'essai ; sous le quorum, les votants qui manquent comptent comme « non » ; voteFloor reste le quorum de référence de la participation p0 = 2 × voteFloor / W. Une base existante reçoit ces changements par des amendements gardés (seed_amend, seed_proposal_amend, quorum_amend, article_amend, retire), jamais en réinstanciant le document. Lot T4 (Yvv, 2026-11-03 : « Modif des textes Acte engagement ») : I1, E1 et E2 réécrits ; E12 « Responsabilité civique », Proposition seed, entre E1 et E2 ; les sections formule (F1, F1.1, F1.2, F1.3), inertie (N1, N1.1) et ordonnancement (O1) sortent du document : ce sont des informations de fonctionnement, qui ne se votent pas (ADR 0014) ; leurs derniers textes restent dans crates/ld-server/tests/fixtures/seed-m8/ (textes relus dans docs/chantiers/acte-certification/textes-amorce.md).",
"slug": "acte-engagement-certification", "slug": "acte-engagement-certification",
"title": { "title": {
"fr": "Acte d'engagement Certification", "fr": "Acte d'engagement Certification",
@@ -50,9 +50,9 @@
"es": "Razón de ser" "es": "Razón de ser"
}, },
"body": { "body": {
"fr": "*« Derrière une clé il n'y a qu'un seul être humain vivant ; derrière un être humain, il n'y a qu'une seule clé certifiante. »*\n\nC'est le seul et unique propos de la certification, qui donne à notre toile fiduciaire sa valeur. Nous pouvons nous fier à l'unicité et l'intégrité de tout geste signé par cette clé. C'est l'enjeu de la certification.\n\nCet acte d'engagement est par ailleurs le seul acte de nature contractuelle qui fonde l'usage de la première monnaie libre.", "fr": "*« Derrière une clé il y a un et un seul être humain vivant ; derrière un être humain, il y a une et une seule clé certifiante. »*\n\nC'est l'unique enjeu de la certification et de votre engagement, il donne à notre toile fiduciaire sa valeur : pouvoir nous fier à l'unicité et l'intégrité de tout geste signé par cette clé.\n\nCet acte d'engagement est par ailleurs le seul acte de nature contractuelle, qui fonde l'usage de la première monnaie libre.",
"en": "*“Behind a key there is only one living human being; behind a human being, there is only one certifying key.”*\n\nThis is the one and only purpose of certification, which gives our fiduciary web its value. We can rely on the uniqueness and integrity of every action signed with this key. That is what is at stake in certification.\n\nThis commitment act is, moreover, the only act of a contractual nature on which the use of the first libre currency is founded.", "en": "*“Behind a key there is one and only one living human being; behind a human being, there is one and only one certifying key.”*\n\nThis is the one thing at stake in certification and in your commitment; it gives our fiduciary web its value: being able to rely on the uniqueness and integrity of every action signed with this key.\n\nThis commitment act is, moreover, the only act of a contractual nature, which founds the use of the first libre currency.",
"es": "*«Detrás de una clave no hay más que un solo ser humano vivo; detrás de un ser humano, no hay más que una sola clave certificadora.»*\n\nEste es el propósito único y exclusivo de la certificación, que da su valor a nuestra red fiduciaria. Podemos fiarnos de la unicidad y la integridad de toda acción firmada con esta clave. Eso es lo que está en juego en la certificación.\n\nEsta acta de compromiso es, además, el único acto de naturaleza contractual en el que se funda el uso de la primera moneda libre." "es": "*«Detrás de una clave hay un solo y único ser humano vivo; detrás de un ser humano, hay una sola y única clave certificadora.»*\n\nEs lo único que está en juego en la certificación y en su compromiso; da su valor a nuestra red fiduciaria: poder fiarnos de la unicidad y la integridad de toda acción firmada con esta clave.\n\nEsta acta de compromiso es, además, el único acto de naturaleza contractual, que funda el uso de la primera moneda libre."
} }
} }
] ]
@@ -116,9 +116,31 @@
"es": "Unicidad de la cuenta" "es": "Unicidad de la cuenta"
}, },
"body": { "body": {
"fr": "Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte cocréateur de monnaie G1.", "fr": "Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte certifiant.",
"en": "I commit on my honour to have, now and ever, no more than one single and unique account co-creating G1 money.", "en": "I commit on my honour to have, now and ever, no more than one single and unique certifying account.",
"es": "Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta cocreadora de moneda G1." "es": "Me comprometo por mi honor a no tener, ahora ni nunca, más que una sola y única cuenta certificadora."
}
}
]
},
{
"code": "E12",
"section": "fondamental",
"position": 4.5,
"versions": [
{
"label": "1.0.0",
"status": "open",
"seedProposal": true,
"title": {
"fr": "Responsabilité civique",
"en": "Civic responsibility",
"es": "Responsabilidad cívica"
},
"body": {
"fr": "Je déclare sur l'honneur avoir la capacité légale et physique de signer un contrat et d'en assumer la responsabilité civique, pour certifier à mon tour.",
"en": "I declare on my honour that I have the legal and physical capacity to sign a contract and to assume its civic responsibility, in order to certify in my turn.",
"es": "Declaro por mi honor tener la capacidad legal y física de firmar un contrato y de asumir su responsabilidad cívica, para certificar a mi vez."
} }
} }
] ]
@@ -138,9 +160,9 @@
"es": "Reciprocidad" "es": "Reciprocidad"
}, },
"body": { "body": {
"fr": "Je m'engage sur l'honneur à ne certifier que des personnes physiques qui respectent scrupuleusement ces deux présents engagements fondamentaux.", "fr": "Je m'engage sur l'honneur à ne certifier que des personnes physiques qui s'engagent elles-mêmes, à scrupuleusement respecter ces trois principes fondamentaux.",
"en": "I commit on my honour to certify only natural persons who scrupulously respect these two fundamental commitments.", "en": "I commit on my honour to certify only natural persons who themselves commit to scrupulously respecting these three fundamental principles.",
"es": "Me comprometo por mi honor a certificar únicamente a personas físicas que respeten escrupulosamente estos dos compromisos fundamentales." "es": "Me comprometo por mi honor a certificar únicamente a personas físicas que se comprometan ellas mismas a respetar escrupulosamente estos tres principios fundamentales."
} }
} }
] ]
@@ -634,160 +656,6 @@
} }
} }
] ]
},
{
"code": "F1",
"section": "formule",
"position": 27,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Formule de vote",
"en": "Voting formula",
"es": "Fórmula de votación"
},
"body": {
"fr": "La formule qui régit l'adoption ou le rejet de chaque modification du présent document. Référence : g1vote (https://g1vote-view-237903.pages.duniter.org/).",
"en": "The formula that governs the adoption or rejection of each amendment to this document. Reference: g1vote (https://g1vote-view-237903.pages.duniter.org/).",
"es": "La fórmula que rige la adopción o el rechazo de cada modificación del presente documento. Referencia: g1vote (https://g1vote-view-237903.pages.duniter.org/)."
}
}
]
},
{
"code": "F1.1",
"section": "formule",
"position": 28,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Formule du seuil WoT",
"en": "WoT threshold formula",
"es": "Fórmula del umbral WoT"
},
"body": {
"fr": "Le seuil est le nombre de voix pour qu'il faut à une version pour entrer en vigueur. Sous le quorum Q, les votants qui manquent comptent comme « non » : le seuil se calcule comme si Q personnes avaient voté. Au-delà, il faut la part de « pour » que demande le cran d'inertie de l'article, parmi tous les votants : cette part est la plus haute au quorum et baisse quand la participation monte, jusqu'à la majorité M quand tout le monde vote.\n\n`T' = max(T, Q)`\n`Seuil = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUne version passe quand ses voix pour atteignent le seuil, arrondi à l'entier au-dessus.\n\n- **T** : les votes exprimés sur la version, pour et contre.\n- **Q** : le quorum de l'article ; **T'** : les votants retenus, T, ou Q s'il en manque.\n- **W** : le nombre de membres de la toile de confiance Ğ1.\n- **B** : la base ; à la puissance W, elle est si petite qu'elle ne compte pas.\n- **M** : la majorité demandée quand tout le monde vote.\n- **G** : le gradient : plus il est grand, plus la part demandée monte quand peu de gens votent.\n\nLeurs valeurs sont dans « Paramètres par défaut » et « Niveaux d'inertie ».",
"en": "The threshold is the number of votes in favour a version needs to come into force. Below the quorum Q, the voters who are missing count as votes against: the threshold is worked out as if Q people had voted. Beyond it, it takes the share of votes in favour that the article's level of inertia asks for, among all the voters: that share is highest at the quorum and falls as turnout rises, down to the majority M when everyone votes.\n\n`T' = max(T, Q)`\n`Threshold = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nA version passes when its votes in favour reach the threshold, rounded up to a whole number.\n\n- **T**: the votes cast on the version, for and against.\n- **Q**: the article's quorum; **T'**: the voters counted, T, or Q if some are missing.\n- **W**: the number of members of the Ğ1 web of trust.\n- **B**: the base; raised to the power W, it is so small that it does not count.\n- **M**: the majority asked for when everyone votes.\n- **G**: the gradient: the larger it is, the higher the share asked for when few people vote.\n\nTheir values are in “Default parameters” and “Inertia levels”.",
"es": "El umbral es el número de votos a favor que necesita una versión para entrar en vigor. Por debajo del quórum Q, los votantes que faltan cuentan como «no»: el umbral se calcula como si hubieran votado Q personas. Más allá, hace falta la proporción de votos a favor que pide el nivel de inercia del artículo, entre todos los votantes: esa proporción es la más alta en el quórum y baja cuando sube la participación, hasta la mayoría M cuando vota todo el mundo.\n\n`T' = max(T, Q)`\n`Umbral = B^W + (M + (1 − M) × (1 − (T'/W)^G)) × T'`\n\nUna versión pasa cuando sus votos a favor alcanzan el umbral, redondeado al entero superior.\n\n- **T**: los votos emitidos sobre la versión, a favor y en contra.\n- **Q**: el quórum del artículo; **T'**: los votantes contados, T, o Q si faltan.\n- **W**: el número de miembros de la red de confianza Ğ1.\n- **B**: la base; elevada a la potencia W, es tan pequeña que no cuenta.\n- **M**: la mayoría pedida cuando vota todo el mundo.\n- **G**: el gradiente: cuanto mayor es, más alta es la proporción pedida cuando vota poca gente.\n\nSus valores están en «Parámetros por defecto» y «Niveles de inercia»."
}
}
]
},
{
"code": "F1.2",
"section": "formule",
"position": 29,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Paramètres par défaut",
"en": "Default parameters",
"es": "Parámetros por defecto"
},
"body": {
"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- **Q**, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme « non ». Chaque article prend celui de son cran : 12, 12, 50, 50 et 50 dans Monnaie libre, du cran 1 au cran 5. L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote. Dans Monnaie libre, jamais moins de 2 : 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 %, un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » à sa participation habituelle. Tout nouvel article commence à ce cran.\n- Au départ, aucun article n'est en vigueur. Raison d'être, Unicité du compte et Réciprocité sont des Propositions seed, mises en avant pour lancer le vote, au cran 2 (basse). Le titre du document est au cran 4 (haute), l'ordre des articles de chaque section au cran 2, 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- **Q**, the quorum: below this number of voters, those who are missing count as votes against. Each article takes the one of its level: 12, 12, 50, 50 and 50 in Monnaie libre, from level 1 to level 5. The author of the first version of an article, an administrator for the starting texts, can set another one as long as the article keeps no trace of any vote. In Monnaie libre, never less than 2: 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%, a quorum of 50 in Monnaie libre, and, beyond the quorum, the group asks for 75% of votes in favour at its usual turnout. Every new article starts at this level.\n- At the start, no article is in force. Purpose, Uniqueness of the account and Reciprocity are seed proposals, put forward to start the vote, at level 2 (low). The title of the document is at level 4 (high), the order of the articles of each section at level 2, 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- **Q**, el quórum: por debajo de este número de votantes, los que faltan cuentan como «no». Cada artículo toma el de su nivel: 12, 12, 50, 50 y 50 en Monnaie libre, del nivel 1 al nivel 5. El autor de la primera versión de un artículo, un administrador para los textos de partida, puede fijar otro mientras el artículo no guarde rastro de ningún voto. En Monnaie libre, nunca menos de 2: 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 %, un quórum de 50 en Monnaie libre, y, más allá del quórum, el grupo pide un 75 % de votos a favor con su participación habitual. Todo artículo nuevo empieza en este nivel.\n- Al inicio, ningún artículo está en vigor. Razón de ser, Unicidad de la cuenta y Reciprocidad son Propuestas seed, puestas en primer plano para lanzar la votación, en el nivel 2 (baja). El título del documento está en el nivel 4 (alta), el orden de los artículos de cada sección en el nivel 2, todos los demás artículos en el nivel 3."
}
}
]
},
{
"code": "F1.3",
"section": "formule",
"position": 30,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Processus de dépôt officiel",
"en": "Official filing process",
"es": "Proceso de depósito oficial"
},
"body": {
"fr": "Lorsqu'une proposition alternative atteint le seuil d'adoption :\n\n1. Le texte de remplacement est intégré au document officiel\n2. Le hash IPFS du document mis à jour est calculé\n3. Le hash est ancré on-chain via `system.remark`\n4. Le dépôt git officiel est synchronisé\n5. Les applications (Cesium, Gecko) mettent à jour automatiquement",
"en": "When an alternative proposal reaches the adoption threshold:\n\n1. The replacement text is incorporated into the official document\n2. The IPFS hash of the updated document is computed\n3. The hash is anchored on-chain via `system.remark`\n4. The official git repository is synchronised\n5. The applications (Cesium, Gecko) update automatically",
"es": "Cuando una propuesta alternativa alcanza el umbral de adopción:\n\n1. El texto de sustitución se integra en el documento oficial\n2. Se calcula el hash IPFS del documento actualizado\n3. El hash se ancla on-chain mediante `system.remark`\n4. Se sincroniza el repositorio git oficial\n5. Las aplicaciones (Cesium, Gecko) se actualizan automáticamente"
}
}
]
},
{
"code": "N1",
"section": "inertie",
"position": 31,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Réglage de l'inertie",
"en": "Inertia setting",
"es": "Ajuste de la inercia"
},
"body": {
"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."
}
}
]
},
{
"code": "N1.1",
"section": "inertie",
"position": 32,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Niveaux d'inertie",
"en": "Inertia levels",
"es": "Niveles de inercia"
},
"body": {
"fr": "À chaque cran, le groupe demande une part de « pour » parmi les votants (voir « Formule du seuil WoT »). Chaque cran a aussi son quorum : sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté. La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà de ce quorum, 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)`, où T' est le nombre de votants, ou le quorum de l'article s'il en manque.\n\n**Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum de l'article, à la participation habituelle, puis quand tout le monde vote, et ce qui est à ce cran au départ.\n\n- **Cran 1 · très basse** : quorum 12 ; 60 % à la participation habituelle, 50 % quand tout le monde vote. Aucun article au départ.\n- **Cran 2 · basse** : quorum 12 ; 67 % à la participation habituelle, 50 % quand tout le monde vote. Raison d'être, Unicité du compte, Réciprocité, et l'ordre des articles de chaque section.\n- **Cran 3 · moyenne** : quorum 50 ; 75 % à la participation habituelle, 55 % quand tout le monde vote. Le cran par défaut : tous les autres articles, et chaque nouvel article.\n- **Cran 4 · haute** : quorum 50 ; 83 % à la participation habituelle, 60 % quand tout le monde vote. Le titre du document.\n- **Cran 5 · très haute** : quorum 50 ; 90 % à la participation habituelle, 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 qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de référence du document : 100 votants dans Monnaie libre. Elle ne dépend pas du quorum d'un article. Elle est ramenée 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, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W.",
"en": "At each level, the group asks for a share of votes in favour among the voters (see “WoT threshold formula”). Each level also has its quorum: below the article's quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if that quorum had voted. The share asked depends on the level and on the number of voters, compared with the group's usual turnout: beyond that quorum, 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)`, where T' is the number of voters, or the article's quorum if some are missing.\n\n**The five levels**: the quorum in Monnaie libre, the share asked beyond the article's quorum, at the usual turnout, then when everyone votes, and what is at that level at the start.\n\n- **Level 1 · very low**: quorum 12; 60% at the usual turnout, 50% when everyone votes. No article at the start.\n- **Level 2 · low**: quorum 12; 67% at the usual turnout, 50% when everyone votes. Purpose, Uniqueness of the account, Reciprocity, and the order of the articles of each section.\n- **Level 3 · medium**: quorum 50; 75% at the usual turnout, 55% when everyone votes. The default level: all other articles, and every new article.\n- **Level 4 · high**: quorum 50; 83% at the usual turnout, 60% when everyone votes. The title of the document.\n- **Level 5 · very high**: quorum 50; 90% at the usual turnout, 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. While it rests on fewer than 5 proposals, it is twice the document's reference quorum: 100 voters in Monnaie libre. It does not depend on the quorum of an article. It is kept between 1% and 50% of the electorate, and is measured once a day.\n\n**G** is computed so that the share asked, beyond the article's quorum, is exactly that of the level at the usual turnout: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is that share of the level (60, 67, 75, 83 or 90%) and p̃ the usual turnout divided by W.",
"es": "En cada nivel, el grupo pide una proporción de votos a favor entre los votantes (ver «Fórmula del umbral WoT»). Cada nivel tiene además su quórum: por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum. La proporción exigida depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá de ese quórum, 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)`, donde T' es el número de votantes, o el quórum del artículo si faltan.\n\n**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida más allá del quórum del artículo, con la participación habitual, luego cuando vota todo el mundo, y lo que está en ese nivel al inicio.\n\n- **Nivel 1 · muy baja**: quórum 12; 60 % con la participación habitual, 50 % cuando vota todo el mundo. Ningún artículo al inicio.\n- **Nivel 2 · baja**: quórum 12; 67 % con la participación habitual, 50 % cuando vota todo el mundo. Razón de ser, Unicidad de la cuenta, Reciprocidad, y el orden de los artículos de cada sección.\n- **Nivel 3 · media**: quórum 50; 75 % con la participación habitual, 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**: quórum 50; 83 % con la participación habitual, 60 % cuando vota todo el mundo. El título del documento.\n- **Nivel 5 · muy alta**: quórum 50; 90 % con la participación habitual, 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. Mientras se base en menos de 5 propuestas, vale dos veces el quórum de referencia del documento: 100 votantes en Monnaie libre. No depende del quórum de un artículo. Se acota 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, más allá del quórum del artículo, sea exactamente la del nivel con la participación habitual: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es esa proporción del nivel (60, 67, 75, 83 o 90 %) y p̃ la participación habitual dividida por W."
}
}
]
},
{
"code": "O1",
"section": "ordonnancement",
"position": 33,
"inertia": 3,
"versions": [
{
"label": "1.0.0",
"status": "open",
"title": {
"fr": "Ordonnancement du document",
"en": "Ordering of the document",
"es": "Ordenación del documento"
},
"body": {
"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."
}
}
]
} }
] ]
} }
+6 -6
View File
@@ -142,10 +142,10 @@ test('C17 — Décisions : une ligne par doc, les modules à voter, dont sans to
const rows = page.locator('ul.lines > li'); const rows = page.locator('ul.lines > li');
await expect(rows).toHaveCount(1); await expect(rows).toHaveCount(1);
await expect(rows.first()).toContainText("Acte d'engagement Certification"); await expect(rows.first()).toContainText("Acte d'engagement Certification");
// 33 articles, the title of the document and the order of 7 sections (ADR 0009; the // 27 articles, the title of the document and the order of its 5 sections (ADR 0009; since
// section of a single article has no order to decide). // lot T4, the rules of operation are out of the document, ADR 0014).
await expect(rows.first()).toContainText('41 modules à voter'); await expect(rows.first()).toContainText('33 modules à voter');
await expect(rows.first()).toContainText('dont 41 sans ton vote'); await expect(rows.first()).toContainText('dont 33 sans ton vote');
// A vote on one version of E3 (it has a variant): one module fewer without her vote. « Contre » // A vote on one version of E3 (it has a variant): one module fewer without her vote. « Contre »
// counts as much, and leaves the previews of C9 unchanged (the server is shared). In Monnaie // counts as much, and leaves the previews of C9 unchanged (the server is shared). In Monnaie
// libre the vote goes through the Ğ1 chain: the ballot, then what her wallet (Charlie's // libre the vote goes through the Ğ1 chain: the ballot, then what her wallet (Charlie's
@@ -165,8 +165,8 @@ test('C17 — Décisions : une ligne par doc, les modules à voter, dont sans to
.poll(async () => (await (await page.request.get(`/api/ballots/${ballot.id}`)).json()).ballot.status, { timeout: 15_000 }) .poll(async () => (await (await page.request.get(`/api/ballots/${ballot.id}`)).json()).ballot.status, { timeout: 15_000 })
.toBe('confirmed'); .toBe('confirmed');
await page.reload(); await page.reload();
await expect(rows.first()).toContainText('41 modules à voter'); await expect(rows.first()).toContainText('33 modules à voter');
await expect(rows.first()).toContainText('dont 40 sans ton vote'); await expect(rows.first()).toContainText('dont 32 sans ton vote');
await rows.first().getByRole('link').click(); await rows.first().getByRole('link').click();
await expect(page).toHaveURL(path(DOC)); await expect(page).toHaveURL(path(DOC));
}); });
+2 -1
View File
@@ -119,7 +119,8 @@ export async function testDoc(page: Page): Promise<{
* force in the test group, as the vote does it: voices for until each enters, then those * force in the test group, as the vote does it: voices for until each enters, then those
* voices withdrawn. A version in force stays so (a permanent vote: only a rival displaces * voices withdrawn. A version in force stays so (a permanent vote: only a rival displaces
* it), at zero votes, as the controls written before the seed proposals found it. Never in * it), at zero votes, as the controls written before the seed proposals found it. Never in
* Monnaie libre, whose vote goes through the chain; nothing done twice. * Monnaie libre, whose vote goes through the chain; nothing done twice. E12 « Responsabilité
* civique », the fourth (lot T4), stays a seed proposal: open, as C35 reads it.
*/ */
export async function seedsInForce(browser: Browser) { export async function seedsInForce(browser: Browser) {
const voices = [await person(browser, 'Amorce A'), await person(browser, 'Amorce B')]; const voices = [await person(browser, 'Amorce A'), await person(browser, 'Amorce B')];
+4 -3
View File
@@ -42,9 +42,10 @@ test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée
await signIn(page, 'Chiara'); await signIn(page, 'Chiara');
await page.goto(TEST_DOC); await page.goto(TEST_DOC);
await unfold(page); await unfold(page);
// Each section ends with its order, in one line; a section of one article has nothing to decide. // Each section ends with its order, in one line: five sections (formule, inertie and
// ordonnancement are out of the document, ADR 0014).
const modules = page.getByRole('group', { name: /^Ordre des articles · / }); const modules = page.getByRole('group', { name: /^Ordre des articles · / });
await expect(modules).toHaveCount(7); await expect(modules).toHaveCount(5);
await expect(page.getByRole('group', { name: 'Ordre des articles · Ordonnancement' })).toHaveCount(0); await expect(page.getByRole('group', { name: 'Ordre des articles · Ordonnancement' })).toHaveCount(0);
const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' }); const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' });
await expect(order).toContainText('Ordre de la section'); await expect(order).toContainText('Ordre de la section');
@@ -62,7 +63,7 @@ test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée
const fondVote = page.getByRole('region', { name: 'Le vote de « Ordre des articles · Engagements fondamentaux »' }); const fondVote = page.getByRole('region', { name: 'Le vote de « Ordre des articles · Engagements fondamentaux »' });
// Its inertia, said once, in its own place (lot U2). // Its inertia, said once, in its own place (lot U2).
await expect(fondVote.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible(); await expect(fondVote.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible();
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Réciprocité']); await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Responsabilité civique', 'Réciprocité']);
await fond.getByRole('button', { name: 'Le vote' }).click(); await fond.getByRole('button', { name: 'Le vote' }).click();
// Another order: « Réordonner », the article, then its new place — in place, no panel. // Another order: « Réordonner », the article, then its new place — in place, no panel.
+3 -3
View File
@@ -243,11 +243,11 @@ test('C31 — une coquille sur un texte en vigueur attend sa validation, puis s
const panel = vera.locator('aside.panel'); const panel = vera.locator('aside.panel');
await panel.getByText('Corriger une coquille').click(); await panel.getByText('Corriger une coquille').click();
const body = panel.getByLabel('Texte', { exact: true }); const body = panel.getByLabel('Texte', { exact: true });
await body.fill((await body.inputValue()).replace("C'est le seul", 'C’est le seul')); await body.fill((await body.inputValue()).replace("C'est l'unique", 'C’est l’unique'));
await panel.getByRole('button', { name: 'Enregistrer la correction' }).click(); await panel.getByRole('button', { name: 'Enregistrer la correction' }).click();
await expect(vera.getByText(/^Ta correction attend un administrateur, ce texte venant du seed/)).toBeVisible(); await expect(vera.getByText(/^Ta correction attend un administrateur, ce texte venant du seed/)).toBeVisible();
// In force, only a validated fix changes the text. // In force, only a validated fix changes the text.
await expect(purpose).toContainText("C'est le seul et unique propos"); await expect(purpose).toContainText("C'est l'unique enjeu");
await expect(panel.locator('li.fix')).toContainText("en attente : s'applique d'elle-même le"); await expect(panel.locator('li.fix')).toContainText("en attente : s'applique d'elle-même le");
await ada.reload(); await ada.reload();
@@ -258,7 +258,7 @@ test('C31 — une coquille sur un texte en vigueur attend sa validation, puis s
await expect(ada.getByText('Correction appliquée.')).toBeVisible(); await expect(ada.getByText('Correction appliquée.')).toBeVisible();
await vera.reload(); await vera.reload();
await expect(purpose).toContainText('C’est le seul et unique propos'); await expect(purpose).toContainText('C’est l’unique enjeu');
const doc = await testDoc(vera); const doc = await testDoc(vera);
const unit = doc.units.find((u) => u.inForce?.title === "Raison d'être"); const unit = doc.units.find((u) => u.inForce?.title === "Raison d'être");
const after = await details(vera, unit?.inForce?.id ?? ''); const after = await details(vera, unit?.inForce?.id ?? '');
+21 -17
View File
@@ -10,6 +10,8 @@ test.beforeAll(async ({ browser }) => seedsInForce(browser));
const EF = 'Engagements fondamentaux'; const EF = 'Engagements fondamentaux';
const E1 = 'Unicité du compte'; const E1 = 'Unicité du compte';
// Lot T4 (Yvv, 2026-11-03): between the two, a seed proposal, left open by `seedsInForce`.
const EC = 'Responsabilité civique';
const E2 = 'Réciprocité'; const E2 = 'Réciprocité';
const ORDER = `Ordre des articles · ${EF}`; const ORDER = `Ordre des articles · ${EF}`;
const CSRF = { 'x-requested-with': 'libredecision' }; const CSRF = { 'x-requested-with': 'libredecision' };
@@ -41,11 +43,11 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
await expect(page.locator('aside.panel')).toHaveCount(0); await expect(page.locator('aside.panel')).toHaveCount(0);
const list = page.locator('section.text').getByRole('list', { name: 'Nouvel ordre des articles' }); const list = page.locator('section.text').getByRole('list', { name: 'Nouvel ordre des articles' });
const names = list.locator('button.item .name'); const names = list.locator('button.item .name');
await expect(names).toHaveText([EF, E1, E2]); await expect(names).toHaveText([EF, E1, EC, E2]);
// The articles in force of the section are in the list now, not twice; the one only proposed says so. // The articles in force of the section are in the list now, not twice; the one only proposed says so.
await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(0); await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(0);
await expect(list.locator('li', { hasText: EF })).toContainText('proposé'); await expect(list.locator('li', { hasText: EF })).toContainText('proposé');
await expect(list.locator('.grip')).toHaveCount(3); await expect(list.locator('.grip')).toHaveCount(4);
const submit = page.getByRole('button', { name: 'Proposer cet ordre' }); const submit = page.getByRole('button', { name: 'Proposer cet ordre' });
await expect(submit).toBeDisabled(); await expect(submit).toBeDisabled();
// Off, and it says why (UX-lecteur.md: « le bouton reste éteint et le dit » — lot U2). // Off, and it says why (UX-lecteur.md: « le bouton reste éteint et le dit » — lot U2).
@@ -54,43 +56,45 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
await expect(submit).toHaveAccessibleDescription(why); await expect(submit).toHaveAccessibleDescription(why);
await shot(page, 'c35-2-poignees'); await shot(page, 'c35-2-poignees');
// By dragging its handle: Réciprocité onto the place of Unicité du compte. // By dragging its handle: Réciprocité onto the place of the article above it, Responsabilité
await list.locator('li', { hasText: E2 }).locator('.grip').dragTo(list.locator('li', { hasText: E1 })); // civique (lot T4; a drag over two rows is not announced, see the report of lot T4 for U3).
await expect(names).toHaveText([EF, E2, E1]); await list.locator('li', { hasText: E2 }).locator('.grip').dragTo(list.locator('li', { hasText: EC }));
await expect(page.getByText(`« ${E2} » en place 2 sur 3.`)).toBeAttached(); await expect(names).toHaveText([EF, E1, E2, EC]);
await expect(page.getByText(`« ${E2} » en place 3 sur 4.`)).toBeAttached();
await expect(submit).toBeEnabled(); await expect(submit).toBeEnabled();
await expect(page.getByText(why)).toHaveCount(0); await expect(page.getByText(why)).toHaveCount(0);
// With the keyboard: the article keeps the focus, the arrows move it. // With the keyboard: the article keeps the focus, the arrows move it.
const items = list.locator('button.item'); const items = list.locator('button.item');
await items.nth(1).focus(); await items.nth(2).focus();
await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowUp');
await expect(names).toHaveText([E2, EF, E1]); await expect(names).toHaveText([EF, E2, E1, EC]);
await expect(items.nth(0)).toBeFocused(); await expect(items.nth(1)).toBeFocused();
await page.keyboard.press('ArrowDown'); await page.keyboard.press('ArrowDown');
await expect(names).toHaveText([EF, E2, E1]); await expect(names).toHaveText([EF, E1, E2, EC]);
// Without dragging: its arrows, or an article then its new place. // Without dragging: its arrows, or an article then its new place.
await list.getByRole('button', { name: `Descendre « ${E2} »` }).click(); await list.getByRole('button', { name: `Descendre « ${E2} »` }).click();
await expect(names).toHaveText([EF, E1, E2]); await expect(names).toHaveText([EF, E1, EC, E2]);
await expect(submit).toBeDisabled(); await expect(submit).toBeDisabled();
await expect(page.getByText(why)).toBeVisible(); await expect(page.getByText(why)).toBeVisible();
await items.nth(2).click(); await items.nth(3).click();
await expect(items.nth(2)).toHaveAttribute('aria-pressed', 'true'); await expect(items.nth(3)).toHaveAttribute('aria-pressed', 'true');
await items.nth(1).click(); await items.nth(1).click();
await expect(names).toHaveText([EF, E2, E1]); await expect(names).toHaveText([EF, E2, E1, EC]);
// « Annuler » gives the section back as it read. // « Annuler » gives the section back as it read.
await page.getByRole('button', { name: 'Annuler' }).click(); await page.getByRole('button', { name: 'Annuler' }).click();
await expect(list).toHaveCount(0); await expect(list).toHaveCount(0);
await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(1); await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(1);
await line.getByRole('button', { name: 'Réordonner' }).click(); await line.getByRole('button', { name: 'Réordonner' }).click();
await list.getByRole('button', { name: `Monter « ${E2} »` }).click(); await list.getByRole('button', { name: `Monter « ${E2} »` }).click();
await expect(names).toHaveText([EF, E2, E1]); await list.getByRole('button', { name: `Monter « ${E2} »` }).click();
await expect(names).toHaveText([EF, E2, E1, EC]);
// Then the vote: the order goes to the vote, and its vote unfolds at once, in the same place. // Then the vote: the order goes to the vote, and its vote unfolds at once, in the same place.
await submit.click(); await submit.click();
await expect(page.getByText('Ordre proposé au vote.')).toBeVisible(); await expect(page.getByText('Ordre proposé au vote.')).toBeVisible();
const fold = line.locator('section.fold'); const fold = line.locator('section.fold');
await expect(fold).toHaveAttribute('aria-label', `Le vote de « ${ORDER} »`); await expect(fold).toHaveAttribute('aria-label', `Le vote de « ${ORDER} »`);
await expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([EF, E2, E1]); await expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([EF, E2, E1, EC]);
await expect(line).toContainText('1 ordre proposé'); await expect(line).toContainText('1 ordre proposé');
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, ''); const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
await fold.getByRole('button', { name: 'Pour', exact: true }).click(); await fold.getByRole('button', { name: 'Pour', exact: true }).click();
@@ -257,7 +261,7 @@ test('C38 — l’en-tête compte ce que montre la liste ; un changement d’ine
await page.goto(DOC); await page.goto(DOC);
await unfold(page); await unfold(page);
const facts = page.locator('.facts'); const facts = page.locator('.facts');
await expect.soft(facts).toContainText('0 article en vigueur · 3 Propositions seed · '); await expect.soft(facts).toContainText('0 article en vigueur · 4 Propositions seed · ');
await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`)); await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`));
// A change of inertia on the order of a section and on the title (test group): each is found at // A change of inertia on the order of a section and on the title (test group): each is found at
+13 -10
View File
@@ -37,7 +37,7 @@ test('C2 — l’accueil avec session : le choix du groupe, ceux de la même cl
await expect(page).toHaveURL((u) => u.pathname === '/'); await expect(page).toHaveURL((u) => u.pathname === '/');
}); });
test('C3 — au départ, aucun texte en vigueur : la raison d’être et les deux engagements, Propositions seed, ouvrent le texte', async ({ page }) => { test('C3 — au départ, aucun texte en vigueur : la raison d’être et les trois engagements, Propositions seed, ouvrent le texte', async ({ page }) => {
await signIn(page, 'Chiara'); await signIn(page, 'Chiara');
await page.goto(DOC); await page.goto(DOC);
await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification"); await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification");
@@ -45,20 +45,22 @@ test('C3 — au départ, aucun texte en vigueur : la raison d’être et les deu
// read whole at the head of the text (lot U), each marked: never the bare page of a text in force. // read whole at the head of the text (lot U), each marked: never the bare page of a text in force.
await expect(page.getByText("Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.")).toBeVisible(); await expect(page.getByText("Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.")).toBeVisible();
const seeds = page.locator('section.text article.unit'); const seeds = page.locator('section.text article.unit');
await expect(seeds.locator('h2')).toHaveText(["Raison d'être", 'Unicité du compte', 'Réciprocité']); // Lot T4 (Yvv, 2026-11-03): « Responsabilité civique » between the two engagements.
await expect(seeds.locator('.tag', { hasText: 'Proposition seed' })).toHaveCount(3); await expect(seeds.locator('h2')).toHaveText(["Raison d'être", 'Unicité du compte', 'Responsabilité civique', 'Réciprocité']);
await expect(seeds.getByTitle("Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.")).toHaveCount(3); await expect(seeds.locator('.tag', { hasText: 'Proposition seed' })).toHaveCount(4);
await expect(seeds.getByTitle("Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.")).toHaveCount(4);
// At the low level they start at, how far each is from passing. // At the low level they start at, how far each is from passing.
for (const seed of await seeds.all()) await expect(seed).toContainText('0 pour · 0 contre · 9 oui sur 12'); for (const seed of await seeds.all()) await expect(seed).toContainText('0 pour · 0 contre · 9 oui sur 12');
for (const seed of await seeds.all()) await expect(seed).toContainText('inertie 2 / 5 · basse'); for (const seed of await seeds.all()) await expect(seed).toContainText('inertie 2 / 5 · basse');
// Below, the open proposals, folded by section; the seed proposals are not said twice. // Below, the open proposals, folded by section; the seed proposals are not said twice.
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible();
await expect(page.locator('ul.deep li.section')).toHaveCount(8); // Five sections: formule, inertie and ordonnancement are out of the document (ADR 0014).
await expect(page.locator('ul.deep li.section')).toHaveCount(5);
await expect(page.locator('ul.deep button.row')).toHaveCount(0); await expect(page.locator('ul.deep button.row')).toHaveCount(0);
await expect(page.locator('ul.deep li.section', { hasText: 'Engagements techniques' })).toContainText('11 articles'); await expect(page.locator('ul.deep li.section', { hasText: 'Engagements techniques' })).toContainText('11 articles');
await unfold(page); await unfold(page);
// One line per article: the 33 less the 3 at the head, E3 and E7 holding their two variants inside. // One line per article: the 27 less the 4 at the head, E3 and E7 holding their two variants inside.
await expect(page.locator('ul.deep button.row')).toHaveCount(30); await expect(page.locator('ul.deep button.row')).toHaveCount(23);
await expect(page.locator('ul.deep .tag', { hasText: 'Proposition seed' })).toHaveCount(0); await expect(page.locator('ul.deep .tag', { hasText: 'Proposition seed' })).toHaveCount(0);
}); });
@@ -172,7 +174,7 @@ test('C9 — les aperçus : bien cotées, et tout', async ({ page }) => {
await page.goto(DOC); await page.goto(DOC);
await page.getByRole('radio', { name: 'Si tout passe' }).click(); await page.getByRole('radio', { name: 'Si tout passe' }).click();
await expect(page.getByText("Aperçu : le texte tel qu'il serait si toutes les propositions", { exact: false })).toBeVisible(); await expect(page.getByText("Aperçu : le texte tel qu'il serait si toutes les propositions", { exact: false })).toBeVisible();
await expect(page.locator('section.text article.unit')).toHaveCount(33); await expect(page.locator('section.text article.unit')).toHaveCount(27);
// Nothing in force in Monnaie libre, and nothing well rated yet (no « pour » there so far). // Nothing in force in Monnaie libre, and nothing well rated yet (no « pour » there so far).
await page.getByRole('radio', { name: 'Si les bien cotées passent' }).click(); await page.getByRole('radio', { name: 'Si les bien cotées passent' }).click();
await expect(page.locator('section.text article.unit')).toHaveCount(0); await expect(page.locator('section.text article.unit')).toHaveCount(0);
@@ -267,8 +269,9 @@ test('C18 — la Raison d’être ouvre le texte, en trois langues', async ({ pa
// The first sentence in italics, then two paragraphs. // The first sentence in italics, then two paragraphs.
const paragraphs = first.locator('.prose p'); const paragraphs = first.locator('.prose p');
await expect(paragraphs).toHaveCount(3); await expect(paragraphs).toHaveCount(3);
await expect(paragraphs.nth(0).locator('em')).toContainText("Derrière une clé il n'y a qu'un seul être humain vivant"); // The text of Yvv of 2026-11-03 (lot T4).
await expect(paragraphs.nth(1)).toContainText("C'est le seul et unique propos de la certification"); await expect(paragraphs.nth(0).locator('em')).toContainText('Derrière une clé il y a un et un seul être humain vivant');
await expect(paragraphs.nth(1)).toContainText("C'est l'unique enjeu de la certification et de votre engagement");
await expect(paragraphs.nth(2)).toContainText('la première monnaie libre'); await expect(paragraphs.nth(2)).toContainText('la première monnaie libre');
await page.getByRole('radiogroup', { name: 'Langue du texte' }).getByRole('radio', { name: 'EN' }).click(); await page.getByRole('radiogroup', { name: 'Langue du texte' }).getByRole('radio', { name: 'EN' }).click();
await expect(first.locator('h2')).toHaveText('Purpose'); await expect(first.locator('h2')).toHaveText('Purpose');