Skip to main content

tuitbot_core/storage/watchtower/
mod.rs

1//! CRUD operations for Watchtower ingestion tables.
2//!
3//! Manages source contexts, content nodes, content chunks, draft seeds,
4//! and remote sync connections for the Cold-Start Watchtower RAG pipeline.
5
6pub mod chunks;
7pub mod connections;
8mod nodes;
9mod seeds;
10mod sources;
11
12#[cfg(test)]
13mod tests;
14#[cfg(test)]
15mod tests_storage;
16
17pub use chunks::*;
18pub use connections::*;
19pub use nodes::*;
20pub use seeds::*;
21pub use sources::*;
22
23// ============================================================================
24// Row types (tuple aliases for sqlx::query_as)
25// ============================================================================
26
27/// Row type for source_contexts queries.
28type SourceContextRow = (
29    i64,
30    String,
31    String,
32    String,
33    Option<String>,
34    String,
35    Option<String>,
36    String,
37    String,
38);
39
40/// Row type for content_nodes queries.
41type ContentNodeRow = (
42    i64,
43    String,
44    i64,
45    String,
46    String,
47    Option<String>,
48    String,
49    Option<String>,
50    Option<String>,
51    String,
52    String,
53    String,
54);
55
56/// Row type for draft_seeds queries (includes chunk_id).
57type DraftSeedRow = (
58    i64,
59    String,
60    i64,
61    String,
62    Option<String>,
63    f64,
64    String,
65    String,
66    Option<String>,
67    Option<i64>,
68);
69
70/// Row type for content_chunks queries.
71type ContentChunkRow = (
72    i64,    // id
73    String, // account_id
74    i64,    // node_id
75    String, // heading_path
76    String, // chunk_text
77    String, // chunk_hash
78    i64,    // chunk_index
79    f64,    // retrieval_boost
80    String, // status
81    String, // created_at
82    String, // updated_at
83);
84
85// ============================================================================
86// Row structs
87// ============================================================================
88
89/// A registered content source.
90#[derive(Debug, Clone, serde::Serialize)]
91pub struct SourceContext {
92    pub id: i64,
93    pub account_id: String,
94    pub source_type: String,
95    pub config_json: String,
96    pub sync_cursor: Option<String>,
97    pub status: String,
98    pub error_message: Option<String>,
99    pub created_at: String,
100    pub updated_at: String,
101}
102
103impl SourceContext {
104    fn from_row(r: SourceContextRow) -> Self {
105        Self {
106            id: r.0,
107            account_id: r.1,
108            source_type: r.2,
109            config_json: r.3,
110            sync_cursor: r.4,
111            status: r.5,
112            error_message: r.6,
113            created_at: r.7,
114            updated_at: r.8,
115        }
116    }
117}
118
119/// An ingested content node from a source.
120#[derive(Debug, Clone, serde::Serialize)]
121pub struct ContentNode {
122    pub id: i64,
123    pub account_id: String,
124    pub source_id: i64,
125    pub relative_path: String,
126    pub content_hash: String,
127    pub title: Option<String>,
128    pub body_text: String,
129    pub front_matter_json: Option<String>,
130    pub tags: Option<String>,
131    pub status: String,
132    pub ingested_at: String,
133    pub updated_at: String,
134}
135
136impl ContentNode {
137    fn from_row(r: ContentNodeRow) -> Self {
138        Self {
139            id: r.0,
140            account_id: r.1,
141            source_id: r.2,
142            relative_path: r.3,
143            content_hash: r.4,
144            title: r.5,
145            body_text: r.6,
146            front_matter_json: r.7,
147            tags: r.8,
148            status: r.9,
149            ingested_at: r.10,
150            updated_at: r.11,
151        }
152    }
153}
154
155/// A pre-computed draft seed derived from a content node.
156#[derive(Debug, Clone, serde::Serialize)]
157pub struct DraftSeed {
158    pub id: i64,
159    pub account_id: String,
160    pub node_id: i64,
161    pub seed_text: String,
162    pub archetype_suggestion: Option<String>,
163    pub engagement_weight: f64,
164    pub status: String,
165    pub created_at: String,
166    pub used_at: Option<String>,
167    pub chunk_id: Option<i64>,
168}
169
170impl DraftSeed {
171    fn from_row(r: DraftSeedRow) -> Self {
172        Self {
173            id: r.0,
174            account_id: r.1,
175            node_id: r.2,
176            seed_text: r.3,
177            archetype_suggestion: r.4,
178            engagement_weight: r.5,
179            status: r.6,
180            created_at: r.7,
181            used_at: r.8,
182            chunk_id: r.9,
183        }
184    }
185}
186
187/// A heading-delimited fragment of a content node.
188#[derive(Debug, Clone, serde::Serialize)]
189pub struct ContentChunk {
190    pub id: i64,
191    pub account_id: String,
192    pub node_id: i64,
193    pub heading_path: String,
194    pub chunk_text: String,
195    pub chunk_hash: String,
196    pub chunk_index: i64,
197    pub retrieval_boost: f64,
198    pub status: String,
199    pub created_at: String,
200    pub updated_at: String,
201}
202
203impl ContentChunk {
204    fn from_row(r: ContentChunkRow) -> Self {
205        Self {
206            id: r.0,
207            account_id: r.1,
208            node_id: r.2,
209            heading_path: r.3,
210            chunk_text: r.4,
211            chunk_hash: r.5,
212            chunk_index: r.6,
213            retrieval_boost: r.7,
214            status: r.8,
215            created_at: r.9,
216            updated_at: r.10,
217        }
218    }
219}
220
221/// Result of an upsert operation on a content node.
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub enum UpsertResult {
224    /// A new node was inserted.
225    Inserted,
226    /// An existing node was updated (content hash changed).
227    Updated,
228    /// The node was skipped (content hash unchanged).
229    Skipped,
230}
231
232/// Row type for seeds with their parent node context.
233#[derive(Debug, Clone, serde::Serialize)]
234pub struct SeedWithContext {
235    /// The seed hook text.
236    pub seed_text: String,
237    /// Title from the parent content node.
238    pub source_title: Option<String>,
239    /// Suggested archetype for the seed.
240    pub archetype_suggestion: Option<String>,
241    /// Engagement weight for retrieval ranking.
242    pub engagement_weight: f64,
243}
244
245/// A content chunk joined with its parent node metadata for retrieval display.
246#[derive(Debug, Clone, serde::Serialize)]
247pub struct ChunkWithNodeContext {
248    /// The underlying chunk data.
249    pub chunk: ContentChunk,
250    /// Relative path of the parent content node.
251    pub relative_path: String,
252    /// Title of the parent content node (may be None for untitled notes).
253    pub source_title: Option<String>,
254}