Lot T4 : les tests d'avant suivent le seed de T4 sans rien perdre de ce qu'ils prouvent

Les tests qui lisaient F, N ou O rejouent leurs transitions sur le seed de M8
(tests/fixtures/seed-m8) : seed_quorum_texts, seed_rules, seed_proposals, api,
quorum ; ils disent en plus que ces amendements sont dépassés par le retrait
(superseded_by). Les comptes suivent le document de T4 : 27 articles, quatre
Propositions seed (I1, E1, E12, E2), 33 modules à voter (27 articles, le titre,
l'ordre de 5 sections). modules.rs : l'ordre sous vote se rejoue sur
« conclusion » (K1, K2) ; « un ordre d'un seul article n'a rien à décider » sur
une base née du seed de M8 (O1). seed_amend : tests unitaires de
Version::SeedProposal et de la ligne seed_proposal (put_forward). order.rs ne lit
que les colonnes d'avant 0012 : sections_now ne sert qu'à la naissance des
modules, et article_amend n'en garde que les articles vivants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 21:21:44 +01:00
co-authored by Claude Opus 5.5
parent dda1ca85dd
commit 093d1ca471
14 changed files with 384 additions and 189 deletions
+1 -3
View File
@@ -232,9 +232,7 @@ async fn where_to(
.into_iter()
.find(|(s, _)| s == section)
.map(|(_, ids)| {
ids.iter()
.filter_map(|id| articles.iter().find(|x| &x.0 == id).map(|x| (x.1.clone(), x.3)))
.collect()
ids.iter().filter_map(|id| articles.iter().find(|x| &x.0 == id).map(|x| (x.1.clone(), x.3))).collect()
})
.unwrap_or_default();
let positions: Vec<f64> = articles.iter().map(|x| x.3).collect();
+4 -4
View File
@@ -37,14 +37,14 @@ pub async fn sections_now(conn: &mut SqliteConnection, doc_id: &str) -> sqlx::Re
position: f64,
visible: bool,
}
// Seen: a text in force, or an open version (`DocState::members` says the same); never a
// unit out of the document (ADR 0014).
// Seen: a text in force, or an open version (`DocState::members` says the same). It reads
// only the columns of before 0012: a document born now, or given its modules by an
// amendment, holds no retired unit (ADR 0014); `article_amend` keeps the live ones.
let rows = sqlx::query_as::<_, Row>(
"SELECT u.id, u.section, u.position,
(u.in_force_id IS NOT NULL OR EXISTS (SELECT 1 FROM proposals p
WHERE p.unit_id = u.id AND p.status = 'open' AND p.kind IN ('text', 'remove'))) AS visible
FROM units u WHERE u.doc_id = ? AND u.kind = 'article' AND u.retired_at IS NULL
ORDER BY u.position, u.id",
FROM units u WHERE u.doc_id = ? AND u.kind = 'article' ORDER BY u.position, u.id",
)
.bind(doc_id)
.fetch_all(&mut *conn)
+23 -15
View File
@@ -18,7 +18,7 @@
//! quorum or an order (the unit is not found: 404), nor counted (`docs::group`,
//! `overview::modules`), nor published (`publish`: `document.json` and the Markdown read the
//! document's state; `waits_for_a_text` counts the articles in it). The order of a section
//! reads its articles in it (`order::sections_now`, `DocState::members`). An older image
//! reads its articles in it (`DocState::members`). An older image
//! (rollback) ignores the column and shows them again, exactly as they were.
//!
//! **The guard**, decided once per document, all of them or none: they leave only if no
@@ -147,8 +147,12 @@ pub async fn apply(state: &AppState, seeds: &[SeedDoc], list: &[Retirement]) ->
for (doc_id, group, publish_path) in docs {
let Some((v, retired)) = decide(state, a, &doc_id, publish_path.is_some()).await? else { continue };
match &v {
Verdict::Apply => tracing::info!(amendment = a.id, %group, retired = ?retired, "units out of the document"),
Verdict::Present => tracing::info!(amendment = a.id, %group, "retirement: none of them in the document"),
Verdict::Apply => {
tracing::info!(amendment = a.id, %group, retired = ?retired, "units out of the document")
}
Verdict::Present => {
tracing::info!(amendment = a.id, %group, "retirement: none of them in the document")
}
Verdict::Refuse(why, unit) => tracing::warn!(
amendment = a.id, %group, reason = why, %unit,
"retirement refused: a person left a trace, they stay in the document"
@@ -203,11 +207,13 @@ async fn unit_facts(conn: &mut SqliteConnection, unit_id: &str, code: &str) -> s
)
.fetch_one(&mut *conn)
.await?,
corrected: n("SELECT (SELECT COUNT(*) FROM typos
corrected: n(
"SELECT (SELECT COUNT(*) FROM typos
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1))
+ (SELECT COUNT(*) FROM revisions
WHERE proposal_id IN (SELECT id FROM proposals WHERE unit_id = ?1)
AND (author_id IS NOT NULL OR kind NOT IN ('initial', 'amendment', 'seed_proposal')))")
AND (author_id IS NOT NULL OR kind NOT IN ('initial', 'amendment', 'seed_proposal')))",
)
.fetch_one(&mut *conn)
.await?,
member_translations: n("SELECT COUNT(*) FROM translations
@@ -267,12 +273,13 @@ async fn decide(
for (id, code) in &units {
// Only the two columns of the retirement; nothing else of the unit, nothing of its
// versions, revisions or translations.
let done = sqlx::query("UPDATE units SET retired_at = ?, retired_reason = ? WHERE id = ? AND retired_at IS NULL")
.bind(&now)
.bind(a.id)
.bind(id)
.execute(&mut *tx)
.await?;
let done =
sqlx::query("UPDATE units SET retired_at = ?, retired_reason = ? WHERE id = ? AND retired_at IS NULL")
.bind(&now)
.bind(a.id)
.bind(id)
.execute(&mut *tx)
.await?;
if done.rows_affected() == 1 {
retired.push(code.clone());
}
@@ -350,10 +357,11 @@ mod tests {
assert_eq!(verdict(&all), Verdict::Refuse(why, "N1".into()), "{facts:?}");
}
// The first trace, in the order of the units, then of the kinds.
let two = [untouched("F1"), UnitFacts { signals: 1, votes: 2, ..untouched("F1.1") }, UnitFacts {
quorum_set: 1,
..untouched("N1")
}];
let two = [
untouched("F1"),
UnitFacts { signals: 1, votes: 2, ..untouched("F1.1") },
UnitFacts { quorum_set: 1, ..untouched("N1") },
];
assert_eq!(verdict(&two), Verdict::Refuse("votes", "F1.1".into()));
}
+55
View File
@@ -944,6 +944,57 @@ mod tests {
assert_eq!(verdict(Some(&written_by(&["m5", "t", "t2"])), &third, &["m5", "t"]), Verdict::Refuse("revised"));
}
/// A version the seed proposal amendment put forward (ADR 0011), as production holds I1,
/// E1 and E2 since M6: over the texts `applied` wrote, one line `seed_proposal` that
/// changes no text, at the same revision.
fn put_forward(applied: &[&str]) -> Facts {
let mut f = written_by(applied);
f.revisions.push("seed_proposal".into());
f.put_forward = 1;
f
}
#[test]
fn a_seed_proposal_is_rewritten_over_the_line_that_put_it_forward_and_nothing_else() {
// Lot T4 (2026-11-03): I1 follows the amendment of 30/10, E1 and E2 follow none.
let i1 = Amendment { version: Version::SeedProposal, follows: Some("r"), ..A };
let e1 = Amendment { version: Version::SeedProposal, ..A };
// Production since M6: the text of 30/10 (I1) or the seed's (E1), then put forward.
assert_eq!(verdict(Some(&put_forward(&["r"])), &i1, &["r"]), Verdict::Apply);
assert_eq!(verdict(Some(&put_forward(&[])), &e1, &[]), Verdict::Apply);
// A base born with the seed proposals: nothing put them forward, revision 1.
assert_eq!(verdict(Some(&intact()), &i1, &["r"]), Verdict::Apply);
assert_eq!(verdict(Some(&intact()), &e1, &[]), Verdict::Apply);
// The new text already there.
let present = Facts { fingerprint: "new".into(), ..put_forward(&[]) };
assert_eq!(verdict(Some(&present), &e1, &[]), Verdict::Present);
// No seed proposal: a base older than M6, or a version voted into force or withdrawn.
assert_eq!(verdict(None, &e1, &[]), Verdict::Refuse("no_seed_proposal"));
// A line `seed_proposal` that no amendment wrote, or one more than they wrote.
let unwritten = Facts { put_forward: 0, ..put_forward(&[]) };
assert_eq!(verdict(Some(&unwritten), &e1, &[]), Verdict::Refuse("corrected"));
let mut twice = put_forward(&[]);
twice.revisions.push("seed_proposal".into());
assert_eq!(verdict(Some(&twice), &e1, &[]), Verdict::Refuse("corrected"));
// A typo once put forward: a revision no amendment wrote.
let mut typo = put_forward(&["r"]);
typo.revision = 3;
typo.revisions.push("typo".into());
assert_eq!(verdict(Some(&typo), &i1, &["r"]), Verdict::Refuse("revised"));
// Any trace of a person keeps the text, with its reason.
for (f, why) in [
(Facts { votes: 1, ..put_forward(&[]) }, "votes"),
(Facts { ballots: 1, ..put_forward(&[]) }, "ballots"),
(Facts { typos: 1, ..put_forward(&[]) }, "corrected"),
(Facts { member_translations: 1, ..put_forward(&[]) }, "translation_edited"),
(Facts { signals: 1, ..put_forward(&[]) }, "signalled"),
(Facts { fingerprint: "other".into(), ..put_forward(&[]) }, "text_differs"),
(Facts { origin: "member".into(), ..put_forward(&[]) }, "not_a_seed"),
] {
assert_eq!(verdict(Some(&f), &e1, &[]), Verdict::Refuse(why), "{f:?}");
}
}
#[test]
fn the_amendments_followed_are_read_through_the_list_oldest_first() {
let by_id = |id: &str| AMENDMENTS.iter().find(|a| a.id == id).expect("amendment");
@@ -965,6 +1016,10 @@ mod tests {
["2026-11-02-n1-1-regles", "2026-11-03-n1-1-quorum", "2026-11-03-n1-1-phrases"]
);
assert!(earlier(AMENDMENTS, by_id("2026-10-30-i1-raison-d-etre")).is_empty());
// Lot T4: I1 follows the text of 30/10; E1 and E2, the seed's.
assert_eq!(earlier(AMENDMENTS, by_id("2026-11-03-i1-amorce")), ["2026-10-30-i1-raison-d-etre"]);
assert!(earlier(AMENDMENTS, by_id("2026-11-03-e1-amorce")).is_empty());
assert!(earlier(AMENDMENTS, by_id("2026-11-03-e2-amorce")).is_empty());
// Each one followed wrote the text the next replaces, and is superseded by it.
for a in AMENDMENTS.iter().filter(|a| a.follows.is_some()) {
let e = by_id(a.follows.expect("follows"));