Skip to main content

gradatum_core/
status.rs

1//! Cycle de vie d'une note Gradatum — `NoteStatus` + state machine.
2//!
3//! ## Flux standard
4//!
5//! ```text
6//! Draft ──┬─→ PendingReview ──┬─→ Live          (curator admit)
7//!         │                    ├─→ Garbage        (curator reject)
8//!         │                    └─→ Staging ──┬─→ Live  (humain review optional)
9//!         │                                  └─→ Garbage
10//!         └─→ Garbage          (CLI direct trash)
11//! Live ──┬─→ Deprecated         (replace par autre NoteId)
12//!        └─→ Garbage             (delete explicite)
13//! Deprecated ──→ Live            (restore)
14//! Garbage    ──→ Live            (restore avant cleanup async)
15//! ```
16
17use serde::{Deserialize, Serialize};
18
19use crate::config::EmbedConfig;
20
21/// Statut du cycle de vie d'une note.
22///
23/// Décision Q6 brainstorming the maintainer 2026-05-03 : β workflow-aware (Live+PendingReview+Staging
24/// embeddables par défaut) + α configurable runtime via `[embed] embeddable_status` TOML.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
26#[serde(rename_all = "kebab-case")]
27pub enum NoteStatus {
28    /// Brouillon local, pas encore soumis au pipeline curator.
29    Draft,
30    /// Soumis, en attente de review humain (workflow optionnel).
31    Staging,
32    /// En attente de jugement curator (heuristique ou LLM).
33    ///
34    /// State par défaut attribué par `gradatum-chat::Heuristic` (invariant #3, B4).
35    PendingReview,
36    /// Admis, indexé, searchable, embeddable.
37    Live,
38    /// Remplacé par un autre NoteId. Successeur référencé dans `extra`.
39    Deprecated,
40    /// Rejeté → nettoyage async par le worker.
41    Garbage,
42}
43
44impl NoteStatus {
45    /// Vérifie si la transition vers `target` est valide selon la state machine.
46    ///
47    /// Utilisé par `gradatum-vault::update_status` pour enforcer les invariants de lifecycle.
48    pub fn can_transition_to(&self, target: NoteStatus) -> bool {
49        use NoteStatus::*;
50        matches!(
51            (self, target),
52            (Draft, PendingReview)
53                | (Draft, Garbage)
54                | (PendingReview, Live)
55                | (PendingReview, Garbage)
56                | (PendingReview, Staging)
57                | (Staging, Live)
58                | (Staging, Garbage)
59                | (Live, Deprecated)
60                | (Live, Garbage)
61                | (Deprecated, Live)  // restore
62                | (Garbage, Live) // restore avant cleanup async
63        )
64    }
65
66    /// Est-ce que ce statut est visible dans l'API de lecture par défaut ?
67    pub fn is_visible_default(&self) -> bool {
68        matches!(self, NoteStatus::Live)
69    }
70
71    /// Est-ce que ce statut doit être embeddé par défaut ?
72    ///
73    /// **Workflow-aware β** (décision Q6 2026-05-03) :
74    /// `[Live, PendingReview, Staging]` — l'embedding est précomputé pour les statuts
75    /// "review-or-better" afin que :
76    /// - Le curator puisse comparer sémantiquement une candidate à des notes équivalentes
77    ///   en attente d'admission.
78    /// - Le coût d'embed ne soit pas re-payé au passage `PendingReview → Live`.
79    ///
80    /// Exclus par défaut : `Draft` (pas d'engagement), `Deprecated`/`Garbage` (sortants).
81    pub fn is_embeddable_default(&self) -> bool {
82        matches!(
83            self,
84            NoteStatus::Live | NoteStatus::PendingReview | NoteStatus::Staging
85        )
86    }
87
88    /// Résout l'embeddabilité en tenant compte de la config runtime.
89    ///
90    /// Utilisé par `gradatum-worker` dans le pipeline d'embedding.
91    /// Si `embed.embeddable_status` est `None` → délègue à `is_embeddable_default()`.
92    ///
93    /// ## Note d'implémentation
94    ///
95    /// `EmbedConfig.embeddable_status` est `Option<Vec<String>>` (kebab-case string) —
96    /// maintenu délibérément en `String` pour que `config.rs` reste libre de tout type
97    /// métier (zéro cycle de dépendances). La comparaison s'effectue via `serde_kebab_repr()`.
98    pub fn is_embeddable(&self, cfg: &EmbedConfig) -> bool {
99        match cfg.embeddable_status.as_ref() {
100            Some(allowed) => allowed.iter().any(|s| s == self.serde_kebab_repr()),
101            None => self.is_embeddable_default(),
102        }
103    }
104
105    /// Représentation kebab-case de ce statut (identique à la sérialisation serde).
106    ///
107    /// Utilisé pour la comparaison avec `EmbedConfig.embeddable_status: Vec<String>`.
108    fn serde_kebab_repr(&self) -> &'static str {
109        match self {
110            NoteStatus::Draft => "draft",
111            NoteStatus::Staging => "staging",
112            NoteStatus::PendingReview => "pending-review",
113            NoteStatus::Live => "live",
114            NoteStatus::Deprecated => "deprecated",
115            NoteStatus::Garbage => "garbage",
116        }
117    }
118}
119
120impl std::fmt::Display for NoteStatus {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        f.write_str(self.serde_kebab_repr())
123    }
124}