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