Lot T4 : les textes et la structure de l'Acte d'engagement (Yvv, 03/11) — I1, E1, E2 prennent les textes d'Yvv (fr, en, es ; « il y a une et une seule clé » lu comme la coquille de « il n'y a une et une seule ») ; E12 « Responsabilité civique », Proposition seed au cran 2, entre E1 et E2 ; les sections formule, inertie, ordonnancement sortent du document (ADR 0014). Migration 0012 additive (units.retired_at et retired_reason, retirement_amendments, article_amendments) ; une unité retirée n'est ni lue avec son document, ni listée, ni votée, ni proposée, corrigée, traduite ou objectée (404), ni comptée, ni publiée ; rien d'effacé. Amendements gardés, une fois par document : 2026-11-03-i1-amorce, -e1-amorce, -e2-amorce (Version::SeedProposal ; la ligne seed_proposal admise quand l'amendement de Proposition seed l'a écrite), 2026-11-03-e12-responsabilite-civique (article_amend : entre E1 et E2 tant que l'ordre de la section n'a pas de vote, sinon en fin de section), 2026-11-03-fonctionnement-hors-vote (retire : tous ou aucun, refus à la moindre trace d'une personne, avec sa raison et l'unité) ; les amendements dont le seed ne donne plus le texte, dépassés (superseded_by) ; seed::write_unit partagé

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 21:09:00 +01:00
co-authored by Claude Opus 5.5
parent 4365ed607a
commit dda1ca85dd
14 changed files with 1067 additions and 265 deletions
+102 -75
View File
@@ -12,18 +12,26 @@
//! document holds; a starting text in force the seed now puts forward as a seed
//! proposal (ADR 0011), a seed proposal amendment (`seed_proposal_amend`), on an
//! article nobody has touched; 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::article_amend;
use crate::inertia_amend;
use crate::module_amend;
use crate::modules;
use crate::quorum_amend;
use crate::retire;
use crate::seed_amend;
use crate::seed_proposal_amend;
use crate::util::{self, LangMap};
use ld_engine::inertia::{InertiaLevel, quorum_bounds};
use serde::Deserialize;
use sqlx::SqliteConnection;
use std::path::Path;
/// 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(())
}
/// On start: the groups, the documents missing from them, then the seed,
/// inertia and module amendments (each decided once per document).
/// On start: the groups, the documents missing from them, then the seed, inertia,
/// module, seed proposal, quorum, article amendments and the retirements (each decided
/// once per document).
pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
let seeds = documents(state).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.
seed_proposal_amend::apply(state, &seeds, seed_proposal_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(())
}
@@ -225,7 +236,6 @@ fn check(seed: &SeedDoc) -> anyhow::Result<()> {
async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> anyhow::Result<()> {
let src = seed.source_lang.as_str();
let pick = |m: &LangMap| m.get(src).cloned().unwrap_or_default();
let now = util::now();
let doc_id = util::id();
let mut tx = state.db.begin().await?;
@@ -253,77 +263,7 @@ async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> a
.await?;
}
for unit in &seed.units {
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(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?;
}
}
write_unit(&mut tx, &doc_id, src, unit, unit.position, &now).await?;
}
// The modules, from the document just written: its title, the order of each section.
if let Some(m) = &seed.modules {
@@ -343,6 +283,93 @@ async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> a
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]`.
pub fn quorums_json(quorums: &[u32; 5]) -> String {
serde_json::to_string(quorums).unwrap_or_else(|_| "null".into())