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
+62 -4
View File
@@ -289,13 +289,15 @@ pub async fn status(state: &AppState) -> anyhow::Result<serde_json::Value> {
}))
}
/// Markdown per language + JSON. Only the units in force, in order.
/// Markdown per language + JSON. Only the articles in force, in order; the
/// title of the document is its title in force (ADR 0009).
pub async fn render(state: &AppState, st: &DocState) -> anyhow::Result<Vec<(String, String)>> {
let titles = util::parse_lang(&st.doc.title);
let src = st.doc.source_lang.clone();
let modules = modules_json(state, st).await?;
let stale_title = |lang: &str| modules["title"]["translations"][lang]["stale"].as_bool().unwrap_or(false);
let in_force: Vec<_> = st
.units
.iter()
.articles()
.filter_map(|u| {
let fid = u.in_force_id.as_ref()?;
st.proposals.iter().find(|p| &p.id == fid).map(|p| (u, p))
@@ -313,6 +315,9 @@ pub async fn render(state: &AppState, st: &DocState) -> anyhow::Result<Vec<(Stri
st.doc.slug,
if lang == src { "fait_foi: oui\n".to_owned() } else { "traduction: editoriale\n".to_owned() },
);
if lang != src && stale_title(lang) {
md.push_str(&format!("\n<!-- {lang} : titre à revoir, le titre source a changé -->\n"));
}
for (_, p) in &in_force {
let t = tr.get(&p.id).and_then(|m| m.get(lang));
let (ut, ub, fallback) = match t {
@@ -356,8 +361,61 @@ pub async fn render(state: &AppState, st: &DocState) -> anyhow::Result<Vec<(Stri
let json = json!({
"document": st.doc.slug, "group": st.group.slug, "title": titles, "sourceLang": src,
"source": source_url, "updatedAt": updated, "electorate": st.electorate,
"voteFloor": st.doc.vote_floor, "units": units,
"voteFloor": st.doc.vote_floor, "units": units, "modules": modules,
});
files.push(("document.json".into(), format!("{}\n", serde_json::to_string_pretty(&json)?)));
Ok(files)
}
/// The modules of the document as published: the title and the order of each
/// section, each with its level, its state and its votes (ADR 0009).
async fn modules_json(state: &AppState, st: &DocState) -> anyhow::Result<serde_json::Value> {
use crate::modules::ModuleKind;
let status = |p: &docs::ProposalRow| {
if p.entered_at.is_some() {
"adopted"
} else if p.confirmed_at.is_some() {
"confirmed"
} else {
"seed-minimal"
}
};
let version = |u: &docs::UnitRow| u.in_force_id.as_ref().and_then(|id| st.proposals.iter().find(|p| &p.id == id));
let title = match st.modules(ModuleKind::Title).first().and_then(|u| Some((*u, version(u)?))) {
Some((u, p)) => {
let tr = docs::translations_for(state, &[p]).await.map_err(|e| anyhow::anyhow!("{e}"))?;
let translations: serde_json::Map<String, serde_json::Value> = tr
.get(&p.id)
.map(|m| {
m.iter().map(|(l, t)| (l.clone(), json!({ "title": t["title"], "stale": t["stale"] }))).collect()
})
.unwrap_or_default();
let tally = st.tally(&p.id);
json!({
"inertia": crate::docs::inertia_of(u), "status": status(p), "enteredAt": p.entered_at,
"votes": { "for": tally.for_votes, "against": tally.against }, "translations": translations,
})
}
None => serde_json::Value::Null,
};
let orders: Vec<_> = st
.modules(ModuleKind::Order)
.into_iter()
.filter_map(|u| {
let p = version(u)?;
let section = u.section.clone().unwrap_or_default();
let codes: Vec<&str> = st
.members(&section)
.iter()
.filter_map(|id| st.units.iter().find(|x| &x.id == id).map(|x| x.code.as_str()))
.collect();
let tally = st.tally(&p.id);
Some(json!({
"section": section, "inertia": crate::docs::inertia_of(u), "status": status(p),
"enteredAt": p.entered_at, "votes": { "for": tally.for_votes, "against": tally.against },
"articles": codes,
}))
})
.collect();
Ok(json!({ "title": title, "orders": orders }))
}