//! Modules — what a document decides besides its articles (ADR 0009). //! //! Yvv, 2026-10-30: « En fin de section, il y a un "module" qui permet de //! changer l'ordre des "articles". Lui-même à (2) » ; « Le titre du doc à (4) » //! — and, asked whether the title of an article is voted apart: « Non, titre et //! texte ensemble ». So two modules, each a unit of its own kind: //! //! - the **title of the document** (`title`, level 4): its version in force is //! the title in the source language (`doc_title`); //! - the **order of the articles of each section** (`order`, level 2): its //! version in force is the order the section reads in now (`order`). //! //! A module is voted like an article — unfolded, in the basket, on the Ğ1 //! chain — because its versions are proposals like any other: the same //! threshold, the same margin over the version in force, the same seed a //! minima at the start. What differs is only what entering into force does. //! //! A new document gets its modules from the seed (`modules` in the seed file); //! an existing one, once, from what it holds (`module_amend`). Both go through //! `create`, which reads the document as it is and touches no article and no //! vote. use crate::docs::{DocState, ProposalRow, UnitRow}; use crate::error::{AppError, AppResult}; use crate::seed::SeedModules; use crate::util; use crate::{doc_title, order}; use ld_engine::inertia::InertiaLevel; use sqlx::SqliteConnection; /// The code of the title module of a document. pub const TITLE_CODE: &str = "titre"; /// The code of the order module of a section. pub fn order_code(section: &str) -> String { format!("ordre.{section}") } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ModuleKind { Title, Order, } impl ModuleKind { /// `units.kind`; an article is no module. pub fn parse(raw: &str) -> Option { match raw { "title" => Some(Self::Title), "order" => Some(Self::Order), _ => None, } } pub fn as_str(self) -> &'static str { match self { Self::Title => "title", Self::Order => "order", } } } /// The levels a document's modules start at; `None`: no such module. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Levels { pub title: Option, pub order: Option, } impl From<&SeedModules> for Levels { fn from(m: &SeedModules) -> Self { Self { title: m.title.as_ref().map(|t| t.inertia), order: m.order.as_ref().map(|o| o.inertia) } } } /// A version of a module, written directly in force (a seed a minima) or open. pub(crate) struct NewVersion<'a> { pub unit_id: &'a str, pub kind: ModuleKind, pub title: &'a str, pub body: &'a str, pub author: Option<&'a str>, pub in_force: bool, pub now: &'a str, } pub(crate) async fn insert_version(conn: &mut SqliteConnection, v: NewVersion<'_>) -> sqlx::Result { let id = util::id(); let (status, origin) = if v.in_force { ("in_force", "seed") } else { ("open", "member") }; sqlx::query( "INSERT INTO proposals (id, unit_id, kind, title, body, status, origin, label, author_id, revision, created_at, updated_at, submitted_at) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, 1, ?, ?, ?)", ) .bind(&id) .bind(v.unit_id) .bind(v.kind.as_str()) .bind(v.title) .bind(v.body) .bind(status) .bind(origin) .bind(v.author) .bind(v.now) .bind(v.now) .bind(v.now) .execute(&mut *conn) .await?; sqlx::query( "INSERT INTO revisions (proposal_id, revision, kind, title, body, author_id, created_at) VALUES (?, 1, 'initial', ?, ?, ?, ?)", ) .bind(&id) .bind(v.title) .bind(v.body) .bind(v.author) .bind(v.now) .execute(&mut *conn) .await?; if v.in_force { sqlx::query("UPDATE units SET in_force_id = ? WHERE id = ?") .bind(&id) .bind(v.unit_id) .execute(&mut *conn) .await?; } Ok(id) } async fn insert_unit( conn: &mut SqliteConnection, doc_id: &str, kind: ModuleKind, code: &str, section: Option<&str>, level: InertiaLevel, now: &str, ) -> sqlx::Result { let id = util::id(); // Its place is not among the articles: the reader puts each module where it belongs. sqlx::query( "INSERT INTO units (id, doc_id, code, section, position, inertia, in_force_id, created_at, kind) VALUES (?, ?, ?, ?, 0, ?, NULL, ?, ?)", ) .bind(&id) .bind(doc_id) .bind(code) .bind(section) .bind(level.as_db()) .bind(now) .bind(kind.as_str()) .execute(&mut *conn) .await?; Ok(id) } /// Creates the modules of a document from what it holds now — its title, and /// the order each of its sections reads in — each in force as a seed a minima. /// No article, no vote is touched. Returns the codes created. pub async fn create(conn: &mut SqliteConnection, doc_id: &str, levels: Levels, now: &str) -> sqlx::Result> { let mut created = Vec::new(); if let Some(level) = levels.title { let (raw, src): (String, String) = sqlx::query_as("SELECT title, source_lang FROM documents WHERE id = ?") .bind(doc_id) .fetch_one(&mut *conn) .await?; let titles = util::parse_lang(&raw); let title = titles.get(&src).cloned().unwrap_or_default(); let unit = insert_unit(conn, doc_id, ModuleKind::Title, TITLE_CODE, None, level, now).await?; let pid = insert_version( conn, NewVersion { unit_id: &unit, kind: ModuleKind::Title, title: &title, body: "", author: None, in_force: true, now, }, ) .await?; doc_title::seed_translations(conn, &pid, &title, &titles, &src, now).await?; created.push(TITLE_CODE.to_owned()); } if let Some(level) = levels.order { for (section, members) in order::sections_now(conn, doc_id).await? { let code = order_code(§ion); let unit = insert_unit(conn, doc_id, ModuleKind::Order, &code, Some(§ion), level, now).await?; let body = ld_engine::order::canonical(&members); insert_version( conn, NewVersion { unit_id: &unit, kind: ModuleKind::Order, title: &code, body: &body, author: None, in_force: true, now, }, ) .await?; created.push(code); } } Ok(created) } /// What entering into force does, besides the version taking its place: the /// order of a section moves its articles, the title of a document becomes its /// title. Returns the kind kept in `adoptions` (`text` for an article). pub async fn on_entry( conn: &mut SqliteConnection, st: &DocState, unit: &UnitRow, p: &ProposalRow, ) -> AppResult<&'static str> { match st.module_of(&unit.id) { None => Ok("text"), Some(ModuleKind::Order) => { order::enter(conn, st, unit, p).await?; Ok("order") } Some(ModuleKind::Title) => { doc_title::enter(conn, &st.doc.id, unit, p).await?; Ok("title") } } } /// The generic writing of a version stops at a module: one proposes a module /// through its own door (`order::propose`, `doc_title::propose`). pub fn require_article(st: &DocState, unit_id: &str) -> AppResult<()> { if st.module_of(unit_id).is_some() { Err(AppError::Invalid("module")) } else { Ok(()) } } /// An order is never corrected nor translated: another order is another proposal. pub fn require_text(p: &ProposalRow) -> AppResult<()> { if p.kind == ModuleKind::Order.as_str() { Err(AppError::Conflict("order_fixed")) } else { Ok(()) } }