Fusion de la tête du chantier (924a40d : lots B2 et F) dans le lot D

Les deux côtés gardés partout. Aux coutures :
- une objection (redit / contredit) porte sur deux articles, jamais sur un module ;
  un ordre ne se corrige pas, ni par son auteur ni par un autre ; une coquille du
  titre suit le chemin de F (l'auteur du titre, un administrateur pour le titre
  de départ, ou 3 jours), et documents.title suit à l'application ;
- votable() couvre les modules ; leurs totaux passent par shown_tally, à l'écran
  et dans document.json ;
- « Modules à voter » ne compte plus une version retenue par une objection (test
  vu rouge) ; la liste du groupe la compte toujours parmi les propositions en cours ;
- le panier : une fonction pour les articles et les modules, avec les versions
  retenues de F ; un nouvel article se propose dans une section, à sa fin, et peut
  répondre à une objection ;
- migrations 0007 (modules) puis 0008 (objections) ;
- C25 et C26 supportent un groupe d'essai où une voix suffit (dès 16 membres).

Boucle : fmt, clippy -D warnings, cargo test (227), svelte-check 0, vitest 55,
build, e2e 35 / 35 (C1–C33 et M1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-31 09:41:30 +01:00
co-authored by Claude Opus 5.5
82 changed files with 8285 additions and 677 deletions
+25 -2
View File
@@ -377,6 +377,19 @@ async fn the_generic_writing_stops_at_a_module_and_an_order_is_never_corrected()
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("not_an_order")));
let (s, v) = yvv.call("POST", &format!("/api/units/{module}/title"), Some(json!({ "title": "Autre" }))).await;
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("not_a_title")));
// An objection (« redit » / « contredit », ADR 0010) is between two articles: never a module.
let i2 = unit(&doc, "I2")["proposals"][0]["id"].as_str().expect("i2").to_owned();
let title_v = doc["modules"]["title"]["inForce"]["id"].as_str().expect("title").to_owned();
for (from, other) in [(seed_order, i2.as_str()), (i2.as_str(), seed_order), (i2.as_str(), title_v.as_str())] {
let (s, v) = yvv
.call(
"POST",
&format!("/api/proposals/{from}/objections"),
Some(json!({ "otherId": other, "kind": "contredit", "reason": "Les deux disent l'inverse." })),
)
.await;
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("other")), "{from} → {other}");
}
// A module's inertia is voted like an article's.
let i = yvv.ok("POST", &format!("/api/units/{module}/inertia"), Some(json!({ "target": 3 }))).await;
assert_eq!(i["existing"], false);
@@ -425,11 +438,21 @@ async fn another_title_enters_into_force_and_its_translations_are_to_review() {
let doc = yvv.doc("essai").await;
assert_eq!(doc["doc"]["title"]["en"], "Commitment Act of Certification");
assert_eq!(doc["modules"]["title"]["inForce"]["translations"]["en"]["stale"], false);
// A typo of the title in force is corrected without a vote, and the document follows.
// A typo of the title in force is corrected without a vote, by the path of any typo
// (ADR 0010): someone else's waits for the title's author, then the document follows.
let fixed = "Acte d’engagement de la certification";
let r = chiara.ok("PATCH", &format!("/api/proposals/{p}"), Some(json!({ "title": fixed, "body": "" }))).await;
assert_eq!(r["edit"], "typo");
assert_eq!((r["edit"].as_str(), r["typo"]["answerer"].as_str()), (Some("typo_pending"), Some("author")));
assert_eq!(yvv.doc("essai").await["doc"]["title"]["fr"], new_title, "nothing written yet");
let tid = r["typo"]["id"].as_str().expect("typo").to_owned();
assert_eq!(yvv.ok("POST", &format!("/api/typos/{tid}/validate"), None).await["status"], "applied");
assert_eq!(yvv.doc("essai").await["doc"]["title"]["fr"], fixed);
// An order is never corrected: neither by its author nor by anyone else.
let doc = yvv.doc("essai").await;
let order = order_of(&doc, "conclusion")["inForce"]["id"].as_str().expect("order").to_owned();
let (s, v) =
chiara.call("PATCH", &format!("/api/proposals/{order}"), Some(json!({ "title": "", "body": "x" }))).await;
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("order_fixed")));
// The test group never publishes; the title of the publication is covered by `render`.
let n: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM outbox o JOIN documents d ON d.id = o.doc_id WHERE d.group_slug = 'essai'",