forked from yvv/decision
L'ordre des articles de chaque section (cran 2) et le titre du document (cran 4) deviennent des modules : des unités à part, dont les versions sont des propositions votées comme les articles (dépliées, panier, chaîne). - moteur : ld-engine/src/order.rs (qui est dans l'ordre, ordre proposé, complété à la fin quand un article entre ou sort, places redistribuées, nouvel article en fin de section) ; pending : un ordre de moins de deux articles n'est jamais à voter ; - données : migration 0007 (units.kind, module_amendments) ; le seed déclare ses modules ; module_amend.rs les ajoute une fois, gardé et journalisé, aux documents nés sans eux, sans toucher articles ni votes ; - serveur : modules.rs, order.rs, doc_title.rs (portes pour proposer, entrée en vigueur : les articles changent de place, le document change de titre, en et es passent « à revoir ») ; publication des modules ; - reste mineur : la médiane compte les statuts open, in_force, applied, et eux seuls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
241 lines
8.0 KiB
Rust
241 lines
8.0 KiB
Rust
//! 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<Self> {
|
|
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<InertiaLevel>,
|
|
pub order: Option<InertiaLevel>,
|
|
}
|
|
|
|
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<String> {
|
|
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<String> {
|
|
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<Vec<String>> {
|
|
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(()) }
|
|
}
|