v3 : modules d'ordre et de titre — moteur, données, serveur (L11)

L'ordre des articles de chaque section (cran 2) et le titre du document
(cran 4) deviennent des modules : des unités à part, dont les versions
sont des propositions votées comme les articles (dépliées, panier, chaîne).

- moteur : ld-engine/src/order.rs (qui est dans l'ordre, ordre proposé,
  complété à la fin quand un article entre ou sort, places redistribuées,
  nouvel article en fin de section) ; pending : un ordre de moins de deux
  articles n'est jamais à voter ;
- données : migration 0007 (units.kind, module_amendments) ; le seed
  déclare ses modules ; module_amend.rs les ajoute une fois, gardé et
  journalisé, aux documents nés sans eux, sans toucher articles ni votes ;
- serveur : modules.rs, order.rs, doc_title.rs (portes pour proposer,
  entrée en vigueur : les articles changent de place, le document change
  de titre, en et es passent « à revoir ») ; publication des modules ;
- reste mineur : la médiane compte les statuts open, in_force, applied,
  et eux seuls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-31 02:08:44 +01:00
co-authored by Claude Opus 5.5
parent 1a35d0f521
commit 3831ddb1bf
21 changed files with 2249 additions and 115 deletions
+57
View File
@@ -310,10 +310,66 @@ async fn a_withdrawn_proposal_leaves_the_usual_participation() {
assert_eq!((p["source"].as_str(), p["sample"].as_u64()), (Some("reference"), Some(4)));
}
#[tokio::test]
async fn the_usual_participation_counts_the_statuses_it_names_and_only_those() {
// Relecture of 71819f9: the list is explicit — open, in force, applied. A decision
// taken (a change of inertia applied) keeps its voters; a status not named stays out.
let (mut first, state) = Client::new().await;
let mut people = Vec::new();
for name in ["S1", "S2"] {
let mut c = first.other();
c.login(name).await;
people.push(c);
}
first.login("S1").await;
let doc = first.doc("essai").await;
let open: Vec<String> = ["I2", "T1", "E4", "E5", "E6", "E8"]
.iter()
.map(|code| unit(&doc, code)["proposals"][0]["id"].as_str().expect("open").to_owned())
.collect();
for p in &open {
for c in &mut people {
c.vote(p, "against").await;
}
}
let sample = |state: AppState| async move {
ld_server::participation::refresh(&state, true).await.expect("refresh");
ld_server::participation::stored(&state, "essai").await.expect("stored").expect("measure").sample
};
assert_eq!(sample(state.clone()).await, 6);
for (id, status) in [(&open[0], "applied"), (&open[1], "in_force")] {
sqlx::query("UPDATE proposals SET status = ? WHERE id = ?")
.bind(status)
.bind(id)
.execute(&state.db)
.await
.expect("status");
}
assert_eq!(sample(state.clone()).await, 6, "applied and in force stay counted");
sqlx::query("UPDATE proposals SET status = 'superseded' WHERE id = ?")
.bind(&open[2])
.execute(&state.db)
.await
.expect("status");
assert_eq!(sample(state.clone()).await, 5, "a status not named is out");
}
// ---------------------------------------------------------------------------
// The base as production holds it on 2026-10-30, then the new version
// ---------------------------------------------------------------------------
/// The seed as it was then: no modules yet (ADR 0009 came after).
fn seed_of_the_time(tag: &str) -> std::path::PathBuf {
let today = Config::for_tests().seed_dir.join("acte-engagement-certification.json");
let mut seed: Value = serde_json::from_str(&std::fs::read_to_string(today).expect("seed")).expect("json");
seed.as_object_mut().expect("object").remove("modules");
let dir = std::env::temp_dir().join(format!("ld-levels-seed-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("dir");
std::fs::write(dir.join("acte-engagement-certification.json"), seed.to_string()).expect("write");
dir
}
/// Migrations up to 0004: the schema production runs before this version.
async fn base_before_levels(tag: &str) -> (SqlitePool, AppState) {
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
@@ -325,6 +381,7 @@ async fn base_before_levels(tag: &str) -> (SqlitePool, AppState) {
before.run(&pool).await.expect("migrations 1 to 4");
let mut cfg = Config::for_tests();
cfg.data_dir = std::env::temp_dir().join(format!("ld-levels-{tag}-{}", std::process::id()));
cfg.seed_dir = seed_of_the_time(tag);
let state = AppState::new(cfg, pool.clone()).await.expect("state");
seed::documents(&state).await.expect("documents");
// The words of the time: every article `standard`.