forked from yvv/decision
v3 : F1.2, N1, N1.1 et O1 réécrits selon les règles appliquées (Yvv, 02/11)
- Le seed porte les nouveaux textes (fr, en, es), ceux du brouillon relu
(textes-regles-seed.md), pour les bases neuves.
- L'amendement de seed vise aussi une proposition ouverte du seed, par son
étiquette (Version::Open) ; mêmes gardes : rien d'une personne, 0 vote,
révision 1, empreinte attendue. Quatre amendements, de l'empreinte du seed du
21/10 (inchangée depuis 67ddee2) à celle d'aujourd'hui.
- Tests vus rouges : remplacement une fois, un vote garde le texte, republication
de Monnaie libre seule, base neuve « present ».
Boucle : 237 tests Rust, svelte-check 0, 55 vitest, e2e 35 / 35.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
//! Seed amendments — a starting text replaced by a newer text of the seed.
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! 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:
|
||||
//! the Préambule becomes « Raison d'être »), new bases get it from the seed;
|
||||
@@ -25,6 +30,24 @@ use crate::seed::{SeedDoc, SeedVersion};
|
||||
use crate::util;
|
||||
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.
|
||||
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.
|
||||
@@ -33,6 +56,8 @@ pub struct Amendment {
|
||||
pub doc: &'static str,
|
||||
/// Code of the article.
|
||||
pub unit: &'static str,
|
||||
/// The version replaced.
|
||||
pub version: Version,
|
||||
/// Fingerprint of the starting text expected in force (source language).
|
||||
pub from: &'static str,
|
||||
/// Fingerprint of the new text: the version in force of the seed today, which
|
||||
@@ -42,14 +67,54 @@ pub struct Amendment {
|
||||
pub reason: &'static str,
|
||||
}
|
||||
|
||||
pub const AMENDMENTS: &[Amendment] = &[Amendment {
|
||||
id: "2026-10-30-i1-raison-d-etre",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "I1",
|
||||
from: "ac17693c61b678ae48f5386f3d528a78385a62f3e2bb12a802a21f45c762b375",
|
||||
to: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
|
||||
reason: "texte de départ remplacé : I1",
|
||||
}];
|
||||
pub const AMENDMENTS: &[Amendment] = &[
|
||||
Amendment {
|
||||
id: "2026-10-30-i1-raison-d-etre",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "I1",
|
||||
version: Version::InForce,
|
||||
from: "ac17693c61b678ae48f5386f3d528a78385a62f3e2bb12a802a21f45c762b375",
|
||||
to: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
|
||||
reason: "texte de départ remplacé : I1",
|
||||
},
|
||||
// The rule articles, open proposals of the seed, after the rules applied (Yvv, 2026-11-02).
|
||||
Amendment {
|
||||
id: "2026-11-02-f1-2-regles",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "F1.2",
|
||||
version: Version::Open("1.0.0"),
|
||||
from: "044c65d6b97e4278184cb1acfe6fa2227ddfe91465c717d8a81f52092314fae6",
|
||||
to: "b24ff4d1c95cfbaae0231885009ee3d600957a3ce498bede80b29bceca651a60",
|
||||
reason: "proposition de départ réécrite : F1.2",
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-02-n1-regles",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "N1",
|
||||
version: Version::Open("1.0.0"),
|
||||
from: "eed13441afae8d33eba37545163037ccf89b0801d84072ef55290575a1410142",
|
||||
to: "c1facdfd0c0f3aaa94b74192e146557558fe88f66304dc6d21279714d7fb727e",
|
||||
reason: "proposition de départ réécrite : N1",
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-02-n1-1-regles",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "N1.1",
|
||||
version: Version::Open("1.0.0"),
|
||||
from: "d5d06c5ae5f3a1c1b08e3c3721b2b09c3dcfd528c11d16d3368d6e14d8942bee",
|
||||
to: "865ed24d5eb18745a8c35dc727247219c449e078d39b6a18ec2fdcfd4f8289a0",
|
||||
reason: "proposition de départ réécrite : N1.1",
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-02-o1-regles",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "O1",
|
||||
version: Version::Open("1.0.0"),
|
||||
from: "40ab4c06a9e374bded6023b9dffbee5736fc4000d422c5a819f35cdb00b02642",
|
||||
to: "4173738055af10fceb74bb6e954bdfefd7ab364480d49afbc92ea5f68825c537",
|
||||
reason: "proposition de départ réécrite : O1",
|
||||
},
|
||||
];
|
||||
|
||||
/// The fingerprint of a text: the hash translations are pinned to.
|
||||
pub fn fingerprint(title: &str, body: &str) -> String {
|
||||
@@ -88,7 +153,12 @@ pub enum Verdict {
|
||||
|
||||
/// The rule, apart from the database: any trace of a person keeps the text.
|
||||
pub fn verdict(facts: Option<&Facts>, a: &Amendment) -> Verdict {
|
||||
let Some(f) = facts else { return Verdict::Refuse("no_text_in_force") };
|
||||
let Some(f) = facts else {
|
||||
return Verdict::Refuse(match a.version {
|
||||
Version::InForce => "no_text_in_force",
|
||||
Version::Open(_) => "no_open_version",
|
||||
});
|
||||
};
|
||||
if f.fingerprint == a.to {
|
||||
return Verdict::Present;
|
||||
}
|
||||
@@ -160,8 +230,12 @@ fn target_of<'a>(seeds: &'a [SeedDoc], a: &Amendment) -> Option<(&'a SeedDoc, &'
|
||||
tracing::error!(amendment = a.id, doc = a.doc, "seed amendment: no such seed document, nothing decided");
|
||||
return None;
|
||||
};
|
||||
let version =
|
||||
seed.units.iter().find(|u| u.code == a.unit).and_then(|u| u.versions.iter().find(|v| v.status == "in_force"));
|
||||
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",
|
||||
Version::Open(label) => v.status == "open" && v.label.as_deref() == Some(label),
|
||||
})
|
||||
});
|
||||
let src = seed.source_lang.as_str();
|
||||
let text = version.and_then(|v| Some((v.title.get(src)?, v.body.get(src)?)));
|
||||
match (version, text) {
|
||||
@@ -178,7 +252,7 @@ fn target_of<'a>(seeds: &'a [SeedDoc], a: &Amendment) -> Option<(&'a SeedDoc, &'
|
||||
}
|
||||
}
|
||||
|
||||
async fn facts(conn: &mut SqliteConnection, proposal_id: &str) -> anyhow::Result<Option<Facts>> {
|
||||
async fn facts(conn: &mut SqliteConnection, proposal_id: &str, status: &str) -> anyhow::Result<Option<Facts>> {
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct Row {
|
||||
origin: String,
|
||||
@@ -189,9 +263,10 @@ async fn facts(conn: &mut SqliteConnection, proposal_id: &str) -> anyhow::Result
|
||||
body: String,
|
||||
}
|
||||
let Some(p) = sqlx::query_as::<_, Row>(
|
||||
"SELECT origin, entered_at, confirmed_at, revision, title, body FROM proposals WHERE id = ? AND status = 'in_force'",
|
||||
"SELECT origin, entered_at, confirmed_at, revision, title, body FROM proposals WHERE id = ? AND status = ?",
|
||||
)
|
||||
.bind(proposal_id)
|
||||
.bind(status)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?
|
||||
else {
|
||||
@@ -252,33 +327,49 @@ async fn decide(
|
||||
if decided > 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let in_force: Option<String> =
|
||||
sqlx::query_scalar::<_, Option<String>>("SELECT in_force_id FROM units WHERE doc_id = ? AND code = ?")
|
||||
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",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.bind(a.unit)
|
||||
.bind(label)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.flatten();
|
||||
let found = match &in_force {
|
||||
Some(id) => facts(&mut tx, id).await?,
|
||||
}
|
||||
};
|
||||
let found = match &target_id {
|
||||
Some(id) => facts(&mut tx, id, a.version.status()).await?,
|
||||
None => None,
|
||||
};
|
||||
let mut v = verdict(found.as_ref(), a);
|
||||
let now = util::now();
|
||||
if let (Verdict::Apply, Some(pid)) = (v, &in_force) {
|
||||
if let (Verdict::Apply, Some(pid)) = (v, &target_id) {
|
||||
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));
|
||||
// Written only over the text that was judged, still untouched.
|
||||
let done = sqlx::query(
|
||||
"UPDATE proposals SET title = ?, body = ?, revision = revision + 1, updated_at = ?
|
||||
WHERE id = ? AND status = 'in_force' AND origin = 'seed' AND entered_at IS NULL
|
||||
WHERE id = ? AND status = ? AND origin = 'seed' AND entered_at IS NULL
|
||||
AND confirmed_at IS NULL AND revision = 1",
|
||||
)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(&now)
|
||||
.bind(pid)
|
||||
.bind(a.version.status())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if done.rows_affected() == 1 {
|
||||
@@ -335,7 +426,7 @@ async fn decide(
|
||||
.bind(a.id)
|
||||
.bind(doc_id)
|
||||
.bind(a.unit)
|
||||
.bind(&in_force)
|
||||
.bind(&target_id)
|
||||
.bind(outcome)
|
||||
.bind(reason)
|
||||
.bind(&now)
|
||||
@@ -353,6 +444,7 @@ mod tests {
|
||||
id: "test",
|
||||
doc: "doc",
|
||||
unit: "I1",
|
||||
version: Version::InForce,
|
||||
from: "old",
|
||||
to: "new",
|
||||
reason: "texte de départ remplacé : I1",
|
||||
@@ -398,5 +490,9 @@ mod tests {
|
||||
for (facts, reason) in cases {
|
||||
assert_eq!(verdict(facts.as_ref(), &A), Verdict::Refuse(reason), "{facts:?}");
|
||||
}
|
||||
// An open proposal of the seed that is no longer there (withdrawn, never seeded).
|
||||
let open = Amendment { version: Version::Open("1.0.0"), ..A };
|
||||
assert_eq!(verdict(None, &open), Verdict::Refuse("no_open_version"));
|
||||
assert_eq!(verdict(Some(&intact()), &open), Verdict::Apply);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user