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}