WIP lot S : Propositions seed (I1, E1, E2 ouvertes, cran 2), amendement gardé, publication suspendue sans article en vigueur

Travail en cours, sauvegardé à la demande d'orch : le moteur, le serveur et
tests/seed_proposals.rs (verts) ; les autres tests et les e2e ne sont pas
encore repris.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-02 07:50:31 +01:00
co-authored by Claude Opus 5.5
parent 5cf30e0bfa
commit 6228d3913a
15 changed files with 2513 additions and 55 deletions
+35 -36
View File
@@ -3,7 +3,9 @@
//! The text replaced is an article's version in force (a seed a minima), or, since
//! 2026-11-02, one of its open proposals from the seed, named by its label: the
//! rule articles F1.2, N1, N1.1 and O1 rewritten after the rules applied (Yvv).
//! The guards are the same.
//! The guards are the same. Since the same day, a starting text may be a seed
//! proposal instead of a text in force (ADR 0011): where an article has no text
//! in force, `Version::InForce` examines its seed proposal (a new base: present).
//!
//! 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:
@@ -33,21 +35,12 @@ use sqlx::SqliteConnection;
/// Which version of the article an amendment replaces.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Version {
/// Its text in force, a seed a minima.
/// Its starting text: in force (a seed a minima), or its seed proposal (ADR 0011).
InForce,
/// An open proposal of the seed, at zero votes, by its label.
Open(&'static str),
}
impl Version {
fn status(self) -> &'static str {
match self {
Version::InForce => "in_force",
Version::Open(_) => "open",
}
}
}
/// One starting text to replace, in every document instantiated from a seed.
pub struct Amendment {
/// Stable identifier: each document's decision is kept under it.
@@ -232,7 +225,8 @@ fn target_of<'a>(seeds: &'a [SeedDoc], a: &Amendment) -> Option<(&'a SeedDoc, &'
};
let version = seed.units.iter().find(|u| u.code == a.unit).and_then(|u| {
u.versions.iter().find(|v| match a.version {
Version::InForce => v.status == "in_force",
// The starting text: in force, or since 2026-11-02 put forward (ADR 0011).
Version::InForce => v.status == "in_force" || v.seed_proposal,
Version::Open(label) => v.status == "open" && v.label.as_deref() == Some(label),
})
});
@@ -252,7 +246,8 @@ fn target_of<'a>(seeds: &'a [SeedDoc], a: &Amendment) -> Option<(&'a SeedDoc, &'
}
}
async fn facts(conn: &mut SqliteConnection, proposal_id: &str, status: &str) -> anyhow::Result<Option<Facts>> {
/// What the database says of a version holding `status` (`None`: it does not).
pub(crate) async fn facts(conn: &mut SqliteConnection, proposal_id: &str, status: &str) -> anyhow::Result<Option<Facts>> {
#[derive(sqlx::FromRow)]
struct Row {
origin: String,
@@ -327,35 +322,39 @@ async fn decide(
if decided > 0 {
return Ok(None);
}
let target_id: Option<String> = match a.version {
Version::InForce => {
sqlx::query_scalar::<_, Option<String>>("SELECT in_force_id FROM units WHERE doc_id = ? AND code = ?")
.bind(doc_id)
.bind(a.unit)
.fetch_optional(&mut *tx)
.await?
.flatten()
}
Version::Open(label) => {
sqlx::query_scalar(
"SELECT p.id FROM proposals p JOIN units u ON u.id = p.unit_id
WHERE u.doc_id = ? AND u.code = ? AND p.status = 'open' AND p.origin = 'seed' AND p.label = ?
ORDER BY p.rowid LIMIT 1",
)
let in_force = || {
sqlx::query_scalar::<_, Option<String>>("SELECT in_force_id FROM units WHERE doc_id = ? AND code = ?")
.bind(doc_id)
.bind(a.unit)
.bind(label)
.fetch_optional(&mut *tx)
.await?
}
};
let found = match &target_id {
Some(id) => facts(&mut tx, id, a.version.status()).await?,
let open = |label: Option<&'static str>| {
sqlx::query_scalar::<_, String>(
"SELECT p.id FROM proposals p JOIN units u ON u.id = p.unit_id
WHERE u.doc_id = ? AND u.code = ? AND p.status = 'open' AND p.origin = 'seed'
AND (p.label = ?3 OR (?3 IS NULL AND p.seed_featured = 1))
ORDER BY p.rowid LIMIT 1",
)
.bind(doc_id)
.bind(a.unit)
.bind(label)
};
// The version examined, and the status it holds.
let examined: Option<(String, &'static str)> = match a.version {
Version::InForce => match in_force().fetch_optional(&mut *tx).await?.flatten() {
Some(id) => Some((id, "in_force")),
// No text in force: the starting text may be a seed proposal (ADR 0011).
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")),
};
let target_id = examined.as_ref().map(|(id, _)| id.clone());
let found = match &examined {
Some((id, status)) => facts(&mut tx, id, status).await?,
None => None,
};
let mut v = verdict(found.as_ref(), a);
let now = util::now();
if let (Verdict::Apply, Some(pid)) = (v, &target_id) {
if let (Verdict::Apply, Some((pid, status))) = (v, &examined) {
let src = seed.source_lang.as_str();
let pick = |m: &util::LangMap| m.get(src).cloned().unwrap_or_default();
let (title, body) = (pick(&target.title), pick(&target.body));
@@ -369,7 +368,7 @@ async fn decide(
.bind(&body)
.bind(&now)
.bind(pid)
.bind(a.version.status())
.bind(*status)
.execute(&mut *tx)
.await?;
if done.rows_affected() == 1 {