v3 : socle Rust (moteur pur, accès trustinWallet, serveur) ; maquette Nuxt rangée dans maquette/
- crates/ld-engine : formule inertielle (parité avec la maquette, garde C), coquille / changement de fond, assistant d'édition à règles, composition d'un document sous vote permanent (adoptions, aperçus, rivales). - crates/trustin-access : qui voit, qui ouvre, qui gère les accès. - crates/ld-server : API axum + SQLite, connexion Ğ1 (client OAuth2 de connect.monnaie-libre.fr), invitations, entrée en vigueur automatique, signalements de contradiction, traductions éditoriales, publication git, taille de la toile lue sur la chaîne. - seeds/acte-engagement-certification.json : 33 articles, fr + en + es ; en vigueur : préambule, E1, E2 (seed a minima). - maquette/ : frontend, backend et docker v2 déplacés tels quels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
//! Seeds — so as not to start from a blank page.
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
use crate::AppState;
|
||||
use crate::util::{self, LangMap};
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeedDoc {
|
||||
pub slug: String,
|
||||
pub title: LangMap,
|
||||
pub summary: LangMap,
|
||||
pub source_lang: String,
|
||||
pub instances: Vec<SeedInstance>,
|
||||
pub units: Vec<SeedUnit>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeedInstance {
|
||||
pub group: String,
|
||||
pub vote_floor: f64,
|
||||
pub publish_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SeedUnit {
|
||||
pub code: String,
|
||||
pub position: f64,
|
||||
pub inertia: String,
|
||||
pub versions: Vec<SeedVersion>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SeedVersion {
|
||||
pub label: Option<String>,
|
||||
/// `in_force` (seed a minima) or `open` (proposal at zero votes).
|
||||
pub status: String,
|
||||
pub title: LangMap,
|
||||
pub body: LangMap,
|
||||
}
|
||||
|
||||
pub async fn ensure_groups(state: &AppState) -> anyhow::Result<()> {
|
||||
for (position, g) in trustin_access::default_groups().into_iter().enumerate() {
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO groups (slug, name, parent, policy, hidden, position) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&g.slug)
|
||||
.bind(&g.name)
|
||||
.bind(&g.parent)
|
||||
.bind(g.policy.as_str())
|
||||
.bind(g.hidden)
|
||||
.bind(i64::try_from(position).unwrap_or(0))
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
|
||||
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(());
|
||||
}
|
||||
};
|
||||
let mut files = Vec::new();
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|x| x == "json") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
for path in files {
|
||||
load_file(state, &path).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_file(state: &AppState, path: &Path) -> anyhow::Result<()> {
|
||||
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 {
|
||||
let group_exists: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM groups WHERE slug = ?")
|
||||
.bind(&inst.group)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
if group_exists == 0 {
|
||||
tracing::warn!(group = %inst.group, slug = %seed.slug, "seed names an unknown group");
|
||||
continue;
|
||||
}
|
||||
let present: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM documents WHERE group_slug = ? AND slug = ?")
|
||||
.bind(&inst.group)
|
||||
.bind(&seed.slug)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
if present > 0 {
|
||||
continue;
|
||||
}
|
||||
instantiate(state, &seed, inst).await?;
|
||||
tracing::info!(group = %inst.group, slug = %seed.slug, "seed instantiated");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn instantiate(state: &AppState, seed: &SeedDoc, inst: &SeedInstance) -> anyhow::Result<()> {
|
||||
let src = seed.source_lang.as_str();
|
||||
let pick = |m: &LangMap| m.get(src).cloned().unwrap_or_default();
|
||||
let now = util::now();
|
||||
let doc_id = util::id();
|
||||
let mut tx = state.db.begin().await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO documents (id, group_slug, slug, title, summary, source_lang, vote_floor, publish_path, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&doc_id)
|
||||
.bind(&inst.group)
|
||||
.bind(&seed.slug)
|
||||
.bind(util::lang_json(&seed.title))
|
||||
.bind(util::lang_json(&seed.summary))
|
||||
.bind(src)
|
||||
.bind(inst.vote_floor)
|
||||
.bind(&inst.publish_path)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
for unit in &seed.units {
|
||||
let unit_id = util::id();
|
||||
sqlx::query("INSERT INTO units (id, doc_id, code, position, inertia, in_force_id, created_at) VALUES (?, ?, ?, ?, ?, NULL, ?)")
|
||||
.bind(&unit_id)
|
||||
.bind(&doc_id)
|
||||
.bind(&unit.code)
|
||||
.bind(unit.position)
|
||||
.bind(&unit.inertia)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
for v in &unit.versions {
|
||||
let pid = util::id();
|
||||
let (title, body) = (pick(&v.title), pick(&v.body));
|
||||
let status = if v.status == "in_force" { "in_force" } else { "open" };
|
||||
sqlx::query(
|
||||
"INSERT INTO proposals (id, unit_id, kind, title, body, status, origin, label, author_id, revision,
|
||||
created_at, updated_at, submitted_at)
|
||||
VALUES (?, ?, 'text', ?, ?, ?, 'seed', ?, NULL, 1, ?, ?, ?)",
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(&unit_id)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(status)
|
||||
.bind(&v.label)
|
||||
.bind(&now)
|
||||
.bind(&now)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO revisions (proposal_id, revision, kind, title, body, author_id, created_at) VALUES (?, 1, 'initial', ?, ?, NULL, ?)",
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if status == "in_force" {
|
||||
sqlx::query("UPDATE units SET in_force_id = ? WHERE id = ?")
|
||||
.bind(&pid)
|
||||
.bind(&unit_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
let hash = util::source_hash(&title, &body);
|
||||
for lang in util::LANGS.iter().filter(|l| **l != src) {
|
||||
let (Some(t), Some(b)) = (v.title.get(*lang), v.body.get(*lang)) else { continue };
|
||||
sqlx::query(
|
||||
"INSERT INTO translations (proposal_id, lang, title, body, source_hash, origin, updated_by, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'seed', NULL, ?)",
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(lang)
|
||||
.bind(t)
|
||||
.bind(b)
|
||||
.bind(&hash)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user