Skip to main content

khive_runtime/
embedding.rs

1//! Runtime-owned embedding registry records (ADR-071 ยง5).
2
3use khive_storage::{SqlRow, SqlValue};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// One persisted embedding model identity and its lifecycle history.
8///
9/// Registry reads preserve the stored UUID, canonical key and timestamp values;
10/// they do not reconstruct identity or fill in missing history.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct EmbeddingModelRecord {
13    pub id: Uuid,
14    pub engine_name: String,
15    pub model_id: String,
16    pub key_version: String,
17    pub dim: u32,
18    pub output_dim: Option<u32>,
19    pub status: EmbeddingModelStatus,
20    pub activated_at: Option<i64>,
21    pub superseded_at: Option<i64>,
22    pub superseded_by: Option<Uuid>,
23    pub canonical_key: Vec<u8>,
24    pub created_at: i64,
25}
26
27/// The lifecycle states admitted by the embedding registry.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum EmbeddingModelStatus {
31    Pending,
32    Active,
33    Superseded,
34    Archived,
35}
36
37impl EmbeddingModelStatus {
38    /// The stable registry and serialized spelling, also used by CLI projections.
39    pub fn as_str(&self) -> &'static str {
40        match self {
41            Self::Pending => "pending",
42            Self::Active => "active",
43            Self::Superseded => "superseded",
44            Self::Archived => "archived",
45        }
46    }
47}
48
49impl EmbeddingModelRecord {
50    /// Keep the established registry read policy: warn and skip malformed required
51    /// values. Newly projected non-NULL optional values must also be valid; they
52    /// never acquire a fabricated default. Legacy optional timestamps retain their
53    /// existing compatibility behavior: a non-integer value decodes as None.
54    pub(crate) fn from_sql_row(row: &SqlRow) -> Option<Self> {
55        let required_text = |column: &str| match row.get(column) {
56            Some(SqlValue::Text(value)) => Some(value.clone()),
57            other => {
58                tracing::warn!(target: "khive_runtime::runtime", column, value = ?other, "skipping registry row: unexpected type");
59                None
60            }
61        };
62        let required_blob = |column: &str| match row.get(column) {
63            Some(SqlValue::Blob(value)) => Some(value.as_slice()),
64            _ => {
65                tracing::warn!(target: "khive_runtime::runtime", column, "skipping registry row: expected blob");
66                None
67            }
68        };
69        let uuid = |column: &str, bytes: &[u8]| match Uuid::from_slice(bytes) {
70            Ok(value) => Some(value),
71            Err(_) => {
72                tracing::warn!(target: "khive_runtime::runtime", column, "skipping registry row: invalid UUID bytes");
73                None
74            }
75        };
76        let dimension = |column: &str, value: i64| match u32::try_from(value) {
77            Ok(value) => Some(value),
78            Err(_) => {
79                tracing::warn!(target: "khive_runtime::runtime", column, dim = value, "skipping registry row: dim out of u32 range");
80                None
81            }
82        };
83
84        let engine_name = required_text("engine_name")?;
85        let model_id = required_text("model_id")?;
86        let key_version = required_text("key_version")?;
87        let dim = match row.get("dim") {
88            Some(SqlValue::Integer(value)) => dimension("dim", *value)?,
89            other => {
90                tracing::warn!(target: "khive_runtime::runtime", column = "dim", value = ?other, "skipping registry row: unexpected type");
91                return None;
92            }
93        };
94        let status = match required_text("status")?.as_str() {
95            "pending" => EmbeddingModelStatus::Pending,
96            "active" => EmbeddingModelStatus::Active,
97            "superseded" => EmbeddingModelStatus::Superseded,
98            "archived" => EmbeddingModelStatus::Archived,
99            _ => {
100                tracing::warn!(target: "khive_runtime::runtime", "skipping registry row: unknown lifecycle status");
101                return None;
102            }
103        };
104        let id = uuid("id", required_blob("id")?)?;
105        let canonical_key = required_blob("canonical_key")?.to_vec();
106        let created_at = match row.get("created_at") {
107            Some(SqlValue::Integer(value)) => *value,
108            _ => {
109                tracing::warn!(target: "khive_runtime::runtime", column = "created_at", "skipping registry row: expected integer");
110                return None;
111            }
112        };
113        let output_dim = match row.get("output_dim") {
114            Some(SqlValue::Null) => None,
115            Some(SqlValue::Integer(value)) => Some(dimension("output_dim", *value)?),
116            _ => {
117                tracing::warn!(target: "khive_runtime::runtime", column = "output_dim", "skipping registry row: expected nullable dimension");
118                return None;
119            }
120        };
121        let superseded_by = match row.get("superseded_by") {
122            Some(SqlValue::Null) => None,
123            Some(SqlValue::Blob(value)) => Some(uuid("superseded_by", value)?),
124            _ => {
125                tracing::warn!(target: "khive_runtime::runtime", column = "superseded_by", "skipping registry row: expected nullable UUID bytes");
126                return None;
127            }
128        };
129        let optional_timestamp = |column: &str| match row.get(column) {
130            Some(SqlValue::Integer(value)) => Some(*value),
131            _ => None,
132        };
133
134        Some(Self {
135            id,
136            engine_name,
137            model_id,
138            key_version,
139            dim,
140            output_dim,
141            status,
142            activated_at: optional_timestamp("activated_at"),
143            superseded_at: optional_timestamp("superseded_at"),
144            superseded_by,
145            canonical_key,
146            created_at,
147        })
148    }
149}