1use std::collections::HashMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum EntityType {
11 Person,
12 Project,
13 Tool,
14 Service,
15 Preference,
16 Decision,
17 Event,
18 Concept,
19 Case,
20 Pattern,
21 Thread,
22 Thought,
23 Question,
24 Observation,
25 Policy,
26 Measurement,
27 Outcome,
28}
29
30impl EntityType {
31 #[must_use]
32 pub fn is_mutable(&self) -> bool {
33 !matches!(
34 self,
35 Self::Decision
36 | Self::Event
37 | Self::Case
38 | Self::Observation
39 | Self::Measurement
40 | Self::Outcome
41 )
42 }
43}
44
45impl fmt::Display for EntityType {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 let s = serde_json::to_value(self)
48 .ok()
49 .and_then(|v| v.as_str().map(String::from))
50 .unwrap_or_else(|| format!("{self:?}"));
51 write!(f, "{s}")
52 }
53}
54
55impl std::str::FromStr for EntityType {
56 type Err = String;
57
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 serde_json::from_value(serde_json::Value::String(s.to_string()))
60 .map_err(|_| format!("unknown entity type: {s}"))
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct NewEntity {
67 pub name: String,
68 pub entity_type: EntityType,
69 pub abstract_text: String,
70 pub overview: Option<String>,
71 pub content: Option<String>,
72 pub attributes: Option<serde_json::Value>,
73 pub source: Option<String>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Entity {
79 pub id: serde_json::Value,
80 pub name: String,
81 pub entity_type: EntityType,
82 #[serde(rename = "abstract")]
83 pub abstract_text: String,
84 pub overview: String,
85 pub content: Option<String>,
86 pub attributes: Option<serde_json::Value>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub embedding: Option<Vec<f32>>,
91 #[serde(default = "default_true")]
92 pub mutable: bool,
93 #[serde(default)]
94 pub access_count: i64,
95 #[serde(default = "default_utility_score")]
97 pub utility_score: f64,
98 #[serde(default)]
100 pub utility_updates: i64,
101 pub created_at: serde_json::Value,
102 pub updated_at: serde_json::Value,
103 pub source: Option<String>,
104}
105
106impl Entity {
107 #[must_use]
109 pub fn id_string(&self) -> String {
110 match &self.id {
111 serde_json::Value::String(s) => s.clone(),
112 other => other.to_string(),
113 }
114 }
115
116 #[must_use]
118 pub fn updated_at_string(&self) -> String {
119 match &self.updated_at {
120 serde_json::Value::String(s) => s.clone(),
121 other => other.to_string(),
122 }
123 }
124}
125
126fn default_true() -> bool {
127 true
128}
129
130fn default_utility_score() -> f64 {
131 0.5
132}
133
134#[derive(Debug, Clone, Default, Serialize)]
136pub struct EntityUpdate {
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub abstract_text: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub overview: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub content: Option<String>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub attributes: Option<serde_json::Value>,
145}
146
147#[derive(Debug, Clone)]
149pub struct NewRelationship {
150 pub from_entity: String,
151 pub to_entity: String,
152 pub rel_type: String,
153 pub description: Option<String>,
154 pub confidence: Option<f32>,
155 pub source: Option<String>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct Relationship {
161 pub id: serde_json::Value,
162 #[serde(rename = "in")]
163 pub from_id: serde_json::Value,
164 #[serde(rename = "out")]
165 pub to_id: serde_json::Value,
166 pub rel_type: String,
167 pub description: Option<String>,
168 pub valid_from: serde_json::Value,
169 pub valid_until: Option<serde_json::Value>,
170 pub confidence: f64,
171 #[serde(default)]
174 pub last_reinforced: Option<serde_json::Value>,
175 pub source: Option<String>,
176}
177
178impl Relationship {
179 #[must_use]
181 pub fn id_string(&self) -> String {
182 match &self.id {
183 serde_json::Value::String(s) => s.clone(),
184 other => other.to_string(),
185 }
186 }
187}
188
189#[derive(Debug, Clone, Copy)]
191pub enum Direction {
192 Outgoing,
193 Incoming,
194 Both,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct EntitySummary {
202 pub id: serde_json::Value,
203 pub name: String,
204 pub entity_type: EntityType,
205 #[serde(rename = "abstract")]
206 pub abstract_text: String,
207}
208
209impl EntitySummary {
210 #[must_use]
211 pub fn id_string(&self) -> String {
212 match &self.id {
213 serde_json::Value::String(s) => s.clone(),
214 other => other.to_string(),
215 }
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct EntityDetail {
222 pub id: serde_json::Value,
223 pub name: String,
224 pub entity_type: EntityType,
225 #[serde(rename = "abstract")]
226 pub abstract_text: String,
227 pub overview: String,
228 pub attributes: Option<serde_json::Value>,
229 #[serde(default)]
230 pub access_count: i64,
231 #[serde(default = "default_utility_score")]
232 pub utility_score: f64,
233 pub updated_at: serde_json::Value,
234 pub source: Option<String>,
235}
236
237impl EntityDetail {
238 #[must_use]
239 pub fn id_string(&self) -> String {
240 match &self.id {
241 serde_json::Value::String(s) => s.clone(),
242 other => other.to_string(),
243 }
244 }
245
246 #[must_use]
247 pub fn updated_at_string(&self) -> String {
248 match &self.updated_at {
249 serde_json::Value::String(s) => s.clone(),
250 other => other.to_string(),
251 }
252 }
253}
254
255#[derive(Debug, Clone, Default)]
259pub struct SearchOptions {
260 pub limit: usize,
261 pub entity_type: Option<String>,
262 pub keyword: Option<String>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(rename_all = "snake_case")]
268pub enum MatchSource {
269 Semantic,
271 Graph { parent: String, rel_type: String },
273 Keyword,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct ScoredEntity {
280 pub entity: EntityDetail,
281 pub score: f64,
282 pub source: MatchSource,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct EpisodeSearchResult {
288 pub episode: Episode,
289 pub score: f64,
290 pub distance: f64,
291}
292
293#[derive(Debug, Clone)]
295pub struct QueryOptions {
296 pub limit: usize,
297 pub entity_type: Option<String>,
298 pub keyword: Option<String>,
299 pub graph_depth: u32,
300 pub include_episodes: bool,
301}
302
303impl Default for QueryOptions {
304 fn default() -> Self {
305 Self {
306 limit: 10,
307 entity_type: None,
308 keyword: None,
309 graph_depth: 1,
310 include_episodes: false,
311 }
312 }
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct QueryResult {
318 pub entities: Vec<ScoredEntity>,
319 pub episodes: Vec<EpisodeSearchResult>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct SearchResult {
325 pub entity: Entity,
326 pub score: f64,
327 pub distance: f64,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct TraversalNode {
333 pub entity: EntitySummary,
334 pub edges: Vec<TraversalEdge>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct TraversalEdge {
340 pub rel_type: String,
341 pub direction: String,
342 pub target: TraversalNode,
343 pub valid_from: serde_json::Value,
344 pub valid_until: Option<serde_json::Value>,
345 pub confidence: f64,
346}
347
348#[derive(Debug, Clone, serde::Deserialize)]
350pub struct EdgeRow {
351 pub rel_type: String,
352 pub valid_from: serde_json::Value,
353 pub valid_until: Option<serde_json::Value>,
354 pub target_id: serde_json::Value,
355 #[serde(default = "default_confidence")]
356 pub confidence: f64,
357 #[serde(default)]
358 pub last_reinforced: Option<serde_json::Value>,
359}
360
361fn default_confidence() -> f64 {
362 1.0
363}
364
365impl EdgeRow {
366 #[must_use]
367 pub fn target_id_string(&self) -> String {
368 match &self.target_id {
369 serde_json::Value::String(s) => s.clone(),
370 other => other.to_string(),
371 }
372 }
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct GraphStats {
378 pub entity_count: u64,
379 pub relationship_count: u64,
380 pub episode_count: u64,
381 pub entity_type_counts: HashMap<String, u64>,
382}
383
384#[derive(Debug, Clone)]
388pub struct NewEpisode {
389 pub session_id: String,
390 pub abstract_text: String,
391 pub overview: Option<String>,
392 pub content: Option<String>,
393 pub log_number: Option<u32>,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct Episode {
399 pub id: serde_json::Value,
400 pub session_id: String,
401 pub timestamp: serde_json::Value,
402 #[serde(rename = "abstract")]
403 pub abstract_text: String,
404 pub overview: Option<String>,
405 pub content: Option<String>,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub embedding: Option<Vec<f32>>,
409 pub log_number: Option<i64>,
410}
411
412impl Episode {
413 #[must_use]
414 pub fn id_string(&self) -> String {
415 match &self.id {
416 serde_json::Value::String(s) => s.clone(),
417 other => other.to_string(),
418 }
419 }
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct ExtractedEntity {
425 pub name: String,
426 #[serde(rename = "type")]
427 pub entity_type: EntityType,
428 #[serde(rename = "abstract")]
429 pub abstract_text: String,
430 pub overview: Option<String>,
431 pub content: Option<String>,
432 pub attributes: Option<serde_json::Value>,
433}
434
435impl ExtractedEntity {
436 #[must_use]
438 pub fn to_new_entity(&self, session_id: &str) -> NewEntity {
439 NewEntity {
440 name: self.name.clone(),
441 entity_type: self.entity_type.clone(),
442 abstract_text: self.abstract_text.clone(),
443 overview: self.overview.clone(),
444 content: self.content.clone(),
445 attributes: self.attributes.clone(),
446 source: Some(session_id.to_string()),
447 }
448 }
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct ExtractedRelationship {
454 pub source: String,
455 pub target: String,
456 pub rel_type: String,
457 pub description: Option<String>,
458 #[serde(default)]
459 pub confidence: Option<String>,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct ExtractedCase {
465 pub problem: String,
466 pub solution: String,
467 pub context: Option<String>,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct ExtractedPattern {
473 pub name: String,
474 pub process: String,
475 pub conditions: Option<String>,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct ExtractedPreference {
481 pub facet: String,
482 pub value: String,
483 pub context: Option<String>,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize, Default)]
488pub struct ExtractionResult {
489 #[serde(default)]
490 pub entities: Vec<ExtractedEntity>,
491 #[serde(default)]
492 pub relationships: Vec<ExtractedRelationship>,
493 #[serde(default)]
494 pub cases: Vec<ExtractedCase>,
495 #[serde(default)]
496 pub patterns: Vec<ExtractedPattern>,
497 #[serde(default)]
498 pub preferences: Vec<ExtractedPreference>,
499}
500
501#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
503#[serde(rename_all = "snake_case")]
504pub enum DedupDecision {
505 Skip,
506 Create,
507 Merge { target: String },
508}
509
510pub mod pipeline_rels {
514 pub const EVOLVED_FROM: &str = "EVOLVED_FROM";
515 pub const CRYSTALLIZED_FROM: &str = "CRYSTALLIZED_FROM";
516 pub const INFORMED_BY: &str = "INFORMED_BY";
517 pub const EXPLORES: &str = "EXPLORES";
518 pub const GRADUATED_TO: &str = "GRADUATED_TO";
519 pub const ARCHIVED_FROM: &str = "ARCHIVED_FROM";
520 pub const CONNECTED_TO: &str = "CONNECTED_TO";
521 pub const PROMPTED_BY: &str = "PROMPTED_BY";
522 pub const ANSWERED_BY: &str = "ANSWERED_BY";
523}
524
525pub mod vigil_rels {
527 pub const MEASURED_DURING: &str = "MEASURED_DURING";
528 pub const RESULTED_IN: &str = "RESULTED_IN";
529 pub const TRIGGERED_BY: &str = "TRIGGERED_BY";
530}
531
532#[derive(Debug, Clone, Default)]
534pub struct VigilSyncReport {
535 pub measurements_created: u32,
536 pub outcomes_created: u32,
537 pub events_created: u32,
538 pub relationships_created: u32,
539 pub skipped: u32,
540 pub errors: Vec<String>,
541}
542
543#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
545pub struct PipelineDocuments {
546 pub learning: String,
547 pub thoughts: String,
548 pub curiosity: String,
549 pub reflections: String,
550 pub praxis: String,
551}
552
553#[derive(Debug, Clone, Default, Serialize, Deserialize)]
555pub struct PipelineSyncReport {
556 pub entities_created: u32,
557 pub entities_updated: u32,
558 pub entities_archived: u32,
559 pub relationships_created: u32,
560 pub relationships_skipped: u32,
561 pub errors: Vec<String>,
562}
563
564#[derive(Debug, Clone, Default)]
566pub struct PipelineGraphStats {
567 pub by_stage: HashMap<String, HashMap<String, u64>>,
568 pub stale_thoughts: Vec<EntityDetail>,
569 pub stale_questions: Vec<EntityDetail>,
570 pub orphan_count: u64,
571 pub total_entities: u64,
572 pub last_movement: Option<String>,
573}
574
575#[derive(Debug, Clone)]
577pub struct PipelineEntry {
578 pub title: String,
580 pub body: String,
582 pub status: String,
584 pub stage: String,
586 pub entity_type: EntityType,
588 pub date: Option<String>,
590 pub source_ref: Option<String>,
592 pub destination: Option<String>,
594 pub connected_to: Vec<String>,
596 pub sub_type: Option<String>,
598}
599
600#[derive(Debug, Clone, Default, Serialize, Deserialize)]
602pub struct IngestionReport {
603 pub episodes_created: u32,
604 pub entities_created: u32,
605 pub entities_merged: u32,
606 pub entities_skipped: u32,
607 pub relationships_created: u32,
608 pub relationships_skipped: u32,
609 pub errors: Vec<String>,
610 pub estimated_tokens: u64,
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 fn episode_with(embedding: Option<Vec<f32>>) -> Episode {
618 Episode {
619 id: serde_json::json!("episode:one"),
620 session_id: "s1".into(),
621 timestamp: serde_json::json!("2026-01-01T00:00:00Z"),
622 abstract_text: "a".into(),
623 overview: None,
624 content: None,
625 embedding,
626 log_number: Some(1),
627 }
628 }
629
630 #[test]
633 fn an_episode_without_an_embedding_carries_no_embedding_field() {
634 let json = serde_json::to_value(episode_with(None)).unwrap();
635 assert!(json.get("embedding").is_none(), "{json}");
636
637 let json = serde_json::to_value(episode_with(Some(vec![0.5; 384]))).unwrap();
638 assert!(json.get("embedding").is_some(), "{json}");
639 }
640
641 #[test]
642 fn an_episode_round_trips_without_its_embedding() {
643 let line = serde_json::to_string(&episode_with(None)).unwrap();
644 let parsed: Episode = serde_json::from_str(&line).unwrap();
645 assert!(parsed.embedding.is_none());
646 assert_eq!(parsed.session_id, "s1");
647 }
648
649 #[test]
650 fn pipeline_documents_survive_the_daemon_wire_format() {
651 let docs = PipelineDocuments {
652 learning: "# learning".into(),
653 thoughts: "# thoughts".into(),
654 curiosity: String::new(),
655 reflections: "# reflections".into(),
656 praxis: String::new(),
657 };
658 let line = serde_json::to_string(&docs).unwrap();
659 assert_eq!(
660 serde_json::from_str::<PipelineDocuments>(&line).unwrap(),
661 docs
662 );
663 }
664}