1use std::collections::HashMap;
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10use super::confidence::{EdgeEvidence, Evidence, Provenance};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15#[serde(rename_all = "snake_case")]
16pub enum EntityType {
17 Person,
18 Project,
19 Tool,
20 Service,
21 Preference,
22 Decision,
23 Event,
24 Concept,
25 Case,
26 Pattern,
27 Thread,
28 Thought,
29 Question,
30 Observation,
31 Policy,
32 Measurement,
33 Outcome,
34}
35
36impl EntityType {
37 #[must_use]
38 pub fn is_mutable(&self) -> bool {
39 !matches!(
40 self,
41 Self::Decision
42 | Self::Event
43 | Self::Case
44 | Self::Observation
45 | Self::Measurement
46 | Self::Outcome
47 )
48 }
49}
50
51impl fmt::Display for EntityType {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 let s = serde_json::to_value(self)
54 .ok()
55 .and_then(|v| v.as_str().map(String::from))
56 .unwrap_or_else(|| format!("{self:?}"));
57 write!(f, "{s}")
58 }
59}
60
61impl std::str::FromStr for EntityType {
62 type Err = String;
63
64 fn from_str(s: &str) -> Result<Self, Self::Err> {
65 serde_json::from_value(serde_json::Value::String(s.to_string()))
66 .map_err(|_| format!("unknown entity type: {s}"))
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct NewEntity {
73 pub name: String,
74 pub entity_type: EntityType,
75 pub abstract_text: String,
76 pub overview: Option<String>,
77 pub content: Option<String>,
78 pub attributes: Option<serde_json::Value>,
79 pub source: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Entity {
85 pub id: serde_json::Value,
86 pub name: String,
87 pub entity_type: EntityType,
88 #[serde(rename = "abstract")]
89 pub abstract_text: String,
90 pub overview: String,
91 pub content: Option<String>,
92 pub attributes: Option<serde_json::Value>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub embedding: Option<Vec<f32>>,
97 #[serde(default = "default_true")]
98 pub mutable: bool,
99 #[serde(default)]
100 pub access_count: i64,
101 #[serde(default = "default_utility_score")]
103 pub utility_score: f64,
104 #[serde(default)]
106 pub utility_updates: i64,
107 pub created_at: serde_json::Value,
108 pub updated_at: serde_json::Value,
109 pub source: Option<String>,
110}
111
112impl Entity {
113 #[must_use]
115 pub fn id_string(&self) -> String {
116 match &self.id {
117 serde_json::Value::String(s) => s.clone(),
118 other => other.to_string(),
119 }
120 }
121
122 #[must_use]
124 pub fn updated_at_string(&self) -> String {
125 match &self.updated_at {
126 serde_json::Value::String(s) => s.clone(),
127 other => other.to_string(),
128 }
129 }
130}
131
132fn default_true() -> bool {
133 true
134}
135
136fn default_utility_score() -> f64 {
137 0.5
138}
139
140#[derive(Debug, Clone, Default, Serialize)]
142pub struct EntityUpdate {
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub abstract_text: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub overview: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub content: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub attributes: Option<serde_json::Value>,
151}
152
153#[derive(Debug, Clone)]
155pub struct NewRelationship {
156 pub from_entity: String,
157 pub to_entity: String,
158 pub rel_type: String,
159 pub description: Option<String>,
160 pub confidence: Option<f32>,
161 pub source: Option<String>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct Relationship {
167 pub id: serde_json::Value,
168 #[serde(rename = "in")]
169 pub from_id: serde_json::Value,
170 #[serde(rename = "out")]
171 pub to_id: serde_json::Value,
172 pub rel_type: String,
173 pub description: Option<String>,
174 pub valid_from: serde_json::Value,
175 pub valid_until: Option<serde_json::Value>,
176 pub confidence: f64,
179 #[serde(default)]
182 pub alpha: Option<f64>,
183 #[serde(default)]
185 pub beta: Option<f64>,
186 #[serde(default)]
189 pub self_reinforcements: Option<i64>,
190 #[serde(default)]
193 pub last_reinforced: Option<serde_json::Value>,
194 pub source: Option<String>,
195}
196
197impl Relationship {
198 #[must_use]
200 pub fn id_string(&self) -> String {
201 match &self.id {
202 serde_json::Value::String(s) => s.clone(),
203 other => other.to_string(),
204 }
205 }
206
207 #[must_use]
210 pub fn evidence(&self) -> Evidence {
211 Evidence::from_stored(self.alpha, self.beta, self.confidence)
212 }
213
214 #[must_use]
217 pub fn edge_evidence(&self) -> EdgeEvidence {
218 EdgeEvidence::new(self.evidence(), self.self_reinforcements.unwrap_or(0))
219 }
220}
221
222#[derive(Debug, Clone, Copy)]
224pub enum Direction {
225 Outgoing,
226 Incoming,
227 Both,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct EntitySummary {
235 pub id: serde_json::Value,
236 pub name: String,
237 pub entity_type: EntityType,
238 #[serde(rename = "abstract")]
239 pub abstract_text: String,
240}
241
242impl EntitySummary {
243 #[must_use]
244 pub fn id_string(&self) -> String {
245 match &self.id {
246 serde_json::Value::String(s) => s.clone(),
247 other => other.to_string(),
248 }
249 }
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct EntityDetail {
255 pub id: serde_json::Value,
256 pub name: String,
257 pub entity_type: EntityType,
258 #[serde(rename = "abstract")]
259 pub abstract_text: String,
260 pub overview: String,
261 pub attributes: Option<serde_json::Value>,
262 #[serde(default)]
263 pub access_count: i64,
264 #[serde(default = "default_utility_score")]
265 pub utility_score: f64,
266 pub updated_at: serde_json::Value,
267 pub source: Option<String>,
268}
269
270impl EntityDetail {
271 #[must_use]
272 pub fn id_string(&self) -> String {
273 match &self.id {
274 serde_json::Value::String(s) => s.clone(),
275 other => other.to_string(),
276 }
277 }
278
279 #[must_use]
280 pub fn updated_at_string(&self) -> String {
281 match &self.updated_at {
282 serde_json::Value::String(s) => s.clone(),
283 other => other.to_string(),
284 }
285 }
286}
287
288#[derive(Debug, Clone, Default)]
292pub struct SearchOptions {
293 pub limit: usize,
294 pub entity_type: Option<String>,
295 pub keyword: Option<String>,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300#[serde(rename_all = "snake_case")]
301pub enum MatchSource {
302 Semantic,
304 Graph { parent: String, rel_type: String },
306 Keyword,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct ScoredEntity {
313 pub entity: EntityDetail,
314 #[serde(default)]
323 pub similarity: f64,
324 pub score: f64,
325 pub source: MatchSource,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct EpisodeSearchResult {
331 pub episode: Episode,
332 pub score: f64,
333 pub distance: f64,
334}
335
336#[derive(Debug, Clone)]
338pub struct QueryOptions {
339 pub limit: usize,
340 pub entity_type: Option<String>,
341 pub keyword: Option<String>,
342 pub graph_depth: u32,
343 pub include_episodes: bool,
344}
345
346impl Default for QueryOptions {
347 fn default() -> Self {
348 Self {
349 limit: 10,
350 entity_type: None,
351 keyword: None,
352 graph_depth: 1,
353 include_episodes: false,
354 }
355 }
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct QueryResult {
361 pub entities: Vec<ScoredEntity>,
362 pub episodes: Vec<EpisodeSearchResult>,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct SearchResult {
368 pub entity: Entity,
369 pub score: f64,
372 pub distance: f64,
374}
375
376impl SearchResult {
377 #[must_use]
383 pub fn similarity(&self) -> f64 {
384 1.0 - self.distance
385 }
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct TraversalNode {
391 pub entity: EntitySummary,
392 pub edges: Vec<TraversalEdge>,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct TraversalEdge {
398 pub rel_type: String,
399 pub direction: String,
400 pub target: TraversalNode,
401 pub valid_from: serde_json::Value,
402 pub valid_until: Option<serde_json::Value>,
403 pub confidence: f64,
404}
405
406#[derive(Debug, Clone, serde::Deserialize)]
408pub struct EdgeRow {
409 pub rel_type: String,
410 pub valid_from: serde_json::Value,
411 pub valid_until: Option<serde_json::Value>,
412 pub target_id: serde_json::Value,
413 #[serde(default = "default_confidence")]
414 pub confidence: f64,
415 #[serde(default)]
416 pub last_reinforced: Option<serde_json::Value>,
417}
418
419fn default_confidence() -> f64 {
420 1.0
421}
422
423impl EdgeRow {
424 #[must_use]
425 pub fn target_id_string(&self) -> String {
426 match &self.target_id {
427 serde_json::Value::String(s) => s.clone(),
428 other => other.to_string(),
429 }
430 }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct GraphStats {
436 pub entity_count: u64,
437 pub relationship_count: u64,
438 pub episode_count: u64,
439 pub entity_type_counts: HashMap<String, u64>,
440}
441
442#[derive(Debug, Clone)]
446pub struct NewEpisode {
447 pub session_id: String,
448 pub abstract_text: String,
449 pub overview: Option<String>,
450 pub content: Option<String>,
451 pub log_number: Option<u32>,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct Episode {
457 pub id: serde_json::Value,
458 pub session_id: String,
459 pub timestamp: serde_json::Value,
460 #[serde(rename = "abstract")]
461 pub abstract_text: String,
462 pub overview: Option<String>,
463 pub content: Option<String>,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
466 pub embedding: Option<Vec<f32>>,
467 pub log_number: Option<i64>,
468 #[serde(default)]
472 pub provenance: Option<String>,
473 #[serde(default, deserialize_with = "super::util::count_or_zero")]
476 pub access_count: i64,
477}
478
479impl Episode {
480 #[must_use]
481 pub fn id_string(&self) -> String {
482 match &self.id {
483 serde_json::Value::String(s) => s.clone(),
484 other => other.to_string(),
485 }
486 }
487
488 #[must_use]
492 pub fn provenance(&self) -> Provenance {
493 Provenance::from_stored(self.provenance.as_deref())
494 }
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct ExtractedEntity {
500 pub name: String,
501 #[serde(rename = "type")]
502 pub entity_type: EntityType,
503 #[serde(rename = "abstract")]
504 pub abstract_text: String,
505 pub overview: Option<String>,
506 pub content: Option<String>,
507 pub attributes: Option<serde_json::Value>,
508}
509
510impl ExtractedEntity {
511 #[must_use]
513 pub fn to_new_entity(&self, session_id: &str) -> NewEntity {
514 NewEntity {
515 name: self.name.clone(),
516 entity_type: self.entity_type.clone(),
517 abstract_text: self.abstract_text.clone(),
518 overview: self.overview.clone(),
519 content: self.content.clone(),
520 attributes: self.attributes.clone(),
521 source: Some(session_id.to_string()),
522 }
523 }
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct ExtractedRelationship {
529 pub source: String,
530 pub target: String,
531 pub rel_type: String,
532 pub description: Option<String>,
533 #[serde(default)]
534 pub confidence: Option<String>,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct ExtractedCase {
540 pub problem: String,
541 pub solution: String,
542 pub context: Option<String>,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct ExtractedPattern {
548 pub name: String,
549 pub process: String,
550 pub conditions: Option<String>,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct ExtractedPreference {
556 pub facet: String,
557 pub value: String,
558 pub context: Option<String>,
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize, Default)]
563pub struct ExtractionResult {
564 #[serde(default)]
565 pub entities: Vec<ExtractedEntity>,
566 #[serde(default)]
567 pub relationships: Vec<ExtractedRelationship>,
568 #[serde(default)]
569 pub cases: Vec<ExtractedCase>,
570 #[serde(default)]
571 pub patterns: Vec<ExtractedPattern>,
572 #[serde(default)]
573 pub preferences: Vec<ExtractedPreference>,
574}
575
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
578#[serde(rename_all = "snake_case")]
579pub enum DedupDecision {
580 Skip,
581 Create,
582 Merge { target: String },
583}
584
585pub mod pipeline_rels {
589 pub const EVOLVED_FROM: &str = "EVOLVED_FROM";
590 pub const CRYSTALLIZED_FROM: &str = "CRYSTALLIZED_FROM";
591 pub const INFORMED_BY: &str = "INFORMED_BY";
592 pub const EXPLORES: &str = "EXPLORES";
593 pub const GRADUATED_TO: &str = "GRADUATED_TO";
594 pub const ARCHIVED_FROM: &str = "ARCHIVED_FROM";
595 pub const CONNECTED_TO: &str = "CONNECTED_TO";
596 pub const PROMPTED_BY: &str = "PROMPTED_BY";
597 pub const ANSWERED_BY: &str = "ANSWERED_BY";
598}
599
600pub mod vigil_rels {
602 pub const MEASURED_DURING: &str = "MEASURED_DURING";
603 pub const RESULTED_IN: &str = "RESULTED_IN";
604 pub const TRIGGERED_BY: &str = "TRIGGERED_BY";
605}
606
607#[derive(Debug, Clone, Default)]
609pub struct VigilSyncReport {
610 pub measurements_created: u32,
611 pub outcomes_created: u32,
612 pub events_created: u32,
613 pub relationships_created: u32,
614 pub skipped: u32,
615 pub errors: Vec<String>,
616}
617
618#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
620pub struct PipelineDocuments {
621 pub learning: String,
622 pub thoughts: String,
623 pub curiosity: String,
624 pub reflections: String,
625 pub praxis: String,
626}
627
628#[derive(Debug, Clone, Default, Serialize, Deserialize)]
630pub struct PipelineSyncReport {
631 pub entities_created: u32,
632 pub entities_updated: u32,
633 pub entities_archived: u32,
634 pub relationships_created: u32,
635 pub relationships_skipped: u32,
636 pub errors: Vec<String>,
637}
638
639#[derive(Debug, Clone, Default)]
641pub struct PipelineGraphStats {
642 pub by_stage: HashMap<String, HashMap<String, u64>>,
643 pub stale_thoughts: Vec<EntityDetail>,
644 pub stale_questions: Vec<EntityDetail>,
645 pub orphan_count: u64,
646 pub total_entities: u64,
647 pub last_movement: Option<String>,
648}
649
650#[derive(Debug, Clone)]
652pub struct PipelineEntry {
653 pub title: String,
655 pub body: String,
657 pub status: String,
659 pub stage: String,
661 pub entity_type: EntityType,
663 pub date: Option<String>,
665 pub source_ref: Option<String>,
667 pub destination: Option<String>,
669 pub connected_to: Vec<String>,
671 pub sub_type: Option<String>,
673}
674
675#[derive(Debug, Clone, Default, Serialize, Deserialize)]
677pub struct IngestionReport {
678 pub episodes_created: u32,
679 pub entities_created: u32,
680 pub entities_merged: u32,
681 pub entities_skipped: u32,
682 pub relationships_created: u32,
683 pub relationships_skipped: u32,
684 pub errors: Vec<String>,
685 pub estimated_tokens: u64,
686 #[serde(default)]
690 pub entity_ids: Vec<String>,
691 #[serde(default)]
694 pub dedup_llm_calls: u32,
695 #[serde(default)]
698 pub dedup_fast_path: u32,
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 fn episode_with(embedding: Option<Vec<f32>>) -> Episode {
706 Episode {
707 id: serde_json::json!("episode:one"),
708 session_id: "s1".into(),
709 timestamp: serde_json::json!("2026-01-01T00:00:00Z"),
710 abstract_text: "a".into(),
711 overview: None,
712 content: None,
713 embedding,
714 log_number: Some(1),
715 provenance: None,
716 access_count: 0,
717 }
718 }
719
720 #[test]
723 fn an_episode_without_an_embedding_carries_no_embedding_field() {
724 let json = serde_json::to_value(episode_with(None)).unwrap();
725 assert!(json.get("embedding").is_none(), "{json}");
726
727 let json = serde_json::to_value(episode_with(Some(vec![0.5; 384]))).unwrap();
728 assert!(json.get("embedding").is_some(), "{json}");
729 }
730
731 #[test]
732 fn an_episode_round_trips_without_its_embedding() {
733 let line = serde_json::to_string(&episode_with(None)).unwrap();
734 let parsed: Episode = serde_json::from_str(&line).unwrap();
735 assert!(parsed.embedding.is_none());
736 assert_eq!(parsed.session_id, "s1");
737 }
738
739 #[test]
740 fn pipeline_documents_survive_the_daemon_wire_format() {
741 let docs = PipelineDocuments {
742 learning: "# learning".into(),
743 thoughts: "# thoughts".into(),
744 curiosity: String::new(),
745 reflections: "# reflections".into(),
746 praxis: String::new(),
747 };
748 let line = serde_json::to_string(&docs).unwrap();
749 assert_eq!(
750 serde_json::from_str::<PipelineDocuments>(&line).unwrap(),
751 docs
752 );
753 }
754}