v3 : la Raison d'être remplace le Préambule (L7), amendement de seed gardé

- seed : I1 devient « Raison d'être », texte d'Yvv (fr), traduit en « Purpose » et
  « Razón de ser » ; apostrophes droites comme le reste du seed, insécables avant « ; » et
  dans les guillemets.
- seed_amend.rs : un amendement de seed remplace un texte de départ dans une base existante,
  décidé une fois par document et seulement si personne n'a touché l'article (seed a minima
  d'origine, révision 1, aucun vote, aucune correction, aucune traduction de membre, aucun
  signalement, empreinte attendue). Décision consignée (migration 0004, table
  seed_amendments), ligne d'historique « texte de départ remplacé » avec son diff,
  republication mise en file là où le document publie.
- Une coquille se mesure désormais aussi contre le texte posé par l'amendement.
- Tests : règle pure (3), scénarios d'API (base neuve, amendement appliqué et republié une
  seule fois, cinq refus), e2e C13 ; C3, C6 et l'API suivent le nouveau texte (C6 : les
  apostrophes typographiques d'Unicité du compte, « latransmission » ayant disparu).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-30 09:47:17 +01:00
co-authored by Claude Opus 5.5
parent d400bfc92e
commit a5b92faa96
14 changed files with 801 additions and 37 deletions
+20 -7
View File
@@ -2,10 +2,13 @@
//!
//! On start, missing groups are created and every `seeds/*.json` document is
//! instantiated in each group it names, **only if that group does not hold it
//! yet**. An existing document is never touched: from its first vote on, it
//! is runtime data.
//! yet**. An existing document is never instantiated again: from its first
//! vote on, it is runtime data. The one way a newer seed text reaches an
//! existing document is a seed amendment (`seed_amend`): listed, decided once,
//! and applied only to an article nobody has touched.
use crate::AppState;
use crate::seed_amend;
use crate::util::{self, LangMap};
use serde::Deserialize;
use std::path::Path;
@@ -64,14 +67,23 @@ pub async fn ensure_groups(state: &AppState) -> anyhow::Result<()> {
Ok(())
}
/// On start: the groups, the documents missing from them, then the seed
/// amendments (each decided once per document).
pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
let seeds = documents(state).await?;
seed_amend::apply(state, &seeds, seed_amend::AMENDMENTS).await?;
Ok(())
}
/// The groups and every seed document missing from its groups; returns the seeds read.
pub async fn documents(state: &AppState) -> anyhow::Result<Vec<SeedDoc>> {
ensure_groups(state).await?;
let dir = state.cfg.seed_dir.clone();
let mut entries = match tokio::fs::read_dir(&dir).await {
Ok(e) => e,
Err(e) => {
tracing::warn!(dir = %dir.display(), error = %e, "no seed directory");
return Ok(());
return Ok(Vec::new());
}
};
let mut files = Vec::new();
@@ -82,13 +94,14 @@ pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
}
}
files.sort();
let mut seeds = Vec::new();
for path in files {
load_file(state, &path).await?;
seeds.push(load_file(state, &path).await?);
}
Ok(())
Ok(seeds)
}
async fn load_file(state: &AppState, path: &Path) -> anyhow::Result<()> {
async fn load_file(state: &AppState, path: &Path) -> anyhow::Result<SeedDoc> {
let raw = tokio::fs::read_to_string(path).await?;
let seed: SeedDoc = serde_json::from_str(&raw).map_err(|e| anyhow::anyhow!("{}: {e}", path.display()))?;
for inst in &seed.instances {
@@ -111,7 +124,7 @@ async fn load_file(state: &AppState, path: &Path) -> anyhow::Result<()> {
instantiate(state, &seed, inst).await?;
tracing::info!(group = %inst.group, slug = %seed.slug, "seed instantiated");
}
Ok(())
Ok(seed)
}
async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> anyhow::Result<()> {