1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! Gradatum note lifecycle — `NoteStatus` and state machine.
//!
//! ## Standard flow
//!
//! ```text
//! Draft ──┬─→ PendingReview ──┬─→ Live (curator admit)
//! │ ├─→ Garbage (curator reject)
//! │ └─→ Staging ──┬─→ Live (optional human review)
//! │ └─→ Garbage
//! └─→ Garbage (CLI direct trash)
//! Live ──┬─→ Deprecated (replaced by another NoteId)
//! └─→ Garbage (explicit delete)
//! Deprecated ──→ Live (restore)
//! Garbage ──→ Live (restore before async cleanup)
//! ```
use serde::{Deserialize, Serialize};
use crate::config::EmbedConfig;
/// Note lifecycle status.
///
/// Workflow-aware defaults: `Live`, `PendingReview`, and `Staging` are embeddable by default;
/// this set is configurable at runtime via `[embed] embeddable_status` in TOML.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NoteStatus {
/// Local draft, not yet submitted to the curator pipeline.
Draft,
/// Submitted, awaiting optional human review.
Staging,
/// Awaiting curator judgement (heuristic or LLM).
///
/// Default state assigned by `gradatum-chat::Heuristic`.
PendingReview,
/// Admitted, indexed, searchable, embeddable.
Live,
/// Replaced by another `NoteId`. Successor referenced in `extra`.
Deprecated,
/// Rejected → async cleanup by the worker.
Garbage,
}
impl NoteStatus {
/// Checks whether the transition to `target` is valid per the state machine.
///
/// Used by `gradatum-vault::update_status` to enforce lifecycle invariants.
pub fn can_transition_to(&self, target: NoteStatus) -> bool {
use NoteStatus::*;
matches!(
(self, target),
(Draft, PendingReview)
| (Draft, Garbage)
| (PendingReview, Live)
| (PendingReview, Garbage)
| (PendingReview, Staging)
| (Staging, Live)
| (Staging, Garbage)
| (Live, Deprecated)
| (Live, Garbage)
| (Deprecated, Live) // restore
| (Garbage, Live) // restore avant cleanup async
)
}
/// Returns `true` if this status is visible in the read API by default.
pub fn is_visible_default(&self) -> bool {
matches!(self, NoteStatus::Live)
}
/// Returns `true` if this status is embeddable by default.
///
/// **Workflow-aware defaults**: `[Live, PendingReview, Staging]` — embeddings are
/// pre-computed for "review-or-better" statuses so that:
/// - The curator can semantically compare a candidate against equivalent notes
/// awaiting admission.
/// - The embedding cost is not re-paid at the `PendingReview → Live` transition.
///
/// Excluded by default: `Draft` (no commitment), `Deprecated`/`Garbage` (outgoing).
pub fn is_embeddable_default(&self) -> bool {
matches!(
self,
NoteStatus::Live | NoteStatus::PendingReview | NoteStatus::Staging
)
}
/// Resolves embeddability taking runtime configuration into account.
///
/// Used by `gradatum-worker` in the embedding pipeline.
/// If `embed.embeddable_status` is `None` → delegates to `is_embeddable_default()`.
///
/// ## Implementation note
///
/// `EmbedConfig.embeddable_status` is `Option<Vec<String>>` (kebab-case strings) —
/// deliberately kept as `String` so that `config.rs` remains free of domain types
/// (zero dependency cycle). Comparison is performed via `serde_kebab_repr()`.
pub fn is_embeddable(&self, cfg: &EmbedConfig) -> bool {
match cfg.embeddable_status.as_ref() {
Some(allowed) => allowed.iter().any(|s| s == self.serde_kebab_repr()),
None => self.is_embeddable_default(),
}
}
/// Kebab-case representation of this status (identical to the serde serialisation).
///
/// Used for comparison with `EmbedConfig.embeddable_status: Vec<String>`.
fn serde_kebab_repr(&self) -> &'static str {
match self {
NoteStatus::Draft => "draft",
NoteStatus::Staging => "staging",
NoteStatus::PendingReview => "pending-review",
NoteStatus::Live => "live",
NoteStatus::Deprecated => "deprecated",
NoteStatus::Garbage => "garbage",
}
}
}
impl std::fmt::Display for NoteStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.serde_kebab_repr())
}
}