1use std::collections::HashMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5
6use super::confidence::{EdgeEvidence, Evidence, Provenance};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11#[serde(rename_all = "snake_case")]
12pub enum EntityType {
13 Person,
14 Project,
15 Tool,
16 Service,
17 Preference,
18 Decision,
19 Event,
20 Concept,
21 Case,
22 Pattern,
23 Thread,
24 Thought,
25 Question,
26 Observation,
27 Policy,
28 Measurement,
29 Outcome,
30}
31
32impl EntityType {
33 #[must_use]
34 pub fn is_mutable(&self) -> bool {
35 !matches!(
36 self,
37 Self::Decision
38 | Self::Event
39 | Self::Case
40 | Self::Observation
41 | Self::Measurement
42 | Self::Outcome
43 )
44 }
45}
46
47impl fmt::Display for EntityType {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 let s = serde_json::to_value(self)
50 .ok()
51 .and_then(|v| v.as_str().map(String::from))
52 .unwrap_or_else(|| format!("{self:?}"));
53 write!(f, "{s}")
54 }
55}
56
57impl std::str::FromStr for EntityType {
58 type Err = String;
59
60 fn from_str(s: &str) -> Result<Self, Self::Err> {
61 serde_json::from_value(serde_json::Value::String(s.to_string()))
62 .map_err(|_| format!("unknown entity type: {s}"))
63 }
64}
65
66#[derive(Debug, Clone)]
68pub struct NewEntity {
69 pub name: String,
70 pub entity_type: EntityType,
71 pub abstract_text: String,
72 pub overview: Option<String>,
73 pub content: Option<String>,
74 pub attributes: Option<serde_json::Value>,
75 pub source: Option<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct Entity {
81 pub id: serde_json::Value,
82 pub name: String,
83 pub entity_type: EntityType,
84 #[serde(rename = "abstract")]
85 pub abstract_text: String,
86 pub overview: String,
87 pub content: Option<String>,
88 pub attributes: Option<serde_json::Value>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub embedding: Option<Vec<f32>>,
93 #[serde(default = "default_true")]
94 pub mutable: bool,
95 #[serde(default)]
96 pub access_count: i64,
97 #[serde(default = "default_utility_score")]
99 pub utility_score: f64,
100 #[serde(default)]
102 pub utility_updates: i64,
103 pub created_at: serde_json::Value,
104 pub updated_at: serde_json::Value,
105 pub source: Option<String>,
106}
107
108impl Entity {
109 #[must_use]
111 pub fn id_string(&self) -> String {
112 match &self.id {
113 serde_json::Value::String(s) => s.clone(),
114 other => other.to_string(),
115 }
116 }
117
118 #[must_use]
120 pub fn updated_at_string(&self) -> String {
121 match &self.updated_at {
122 serde_json::Value::String(s) => s.clone(),
123 other => other.to_string(),
124 }
125 }
126}
127
128fn default_true() -> bool {
129 true
130}
131
132fn default_utility_score() -> f64 {
133 0.5
134}
135
136#[derive(Debug, Clone, Default, Serialize)]
138pub struct EntityUpdate {
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub abstract_text: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub overview: Option<String>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub content: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub attributes: Option<serde_json::Value>,
147}
148
149#[derive(Debug, Clone)]
151pub struct NewRelationship {
152 pub from_entity: String,
153 pub to_entity: String,
154 pub rel_type: String,
155 pub description: Option<String>,
156 pub confidence: Option<f32>,
157 pub source: Option<String>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct Relationship {
163 pub id: serde_json::Value,
164 #[serde(rename = "in")]
165 pub from_id: serde_json::Value,
166 #[serde(rename = "out")]
167 pub to_id: serde_json::Value,
168 pub rel_type: String,
169 pub description: Option<String>,
170 pub valid_from: serde_json::Value,
171 pub valid_until: Option<serde_json::Value>,
172 pub confidence: f64,
175 #[serde(default)]
178 pub alpha: Option<f64>,
179 #[serde(default)]
181 pub beta: Option<f64>,
182 #[serde(default)]
185 pub self_reinforcements: Option<i64>,
186 #[serde(default)]
189 pub last_reinforced: Option<serde_json::Value>,
190 pub source: Option<String>,
191}
192
193impl Relationship {
194 #[must_use]
196 pub fn id_string(&self) -> String {
197 match &self.id {
198 serde_json::Value::String(s) => s.clone(),
199 other => other.to_string(),
200 }
201 }
202
203 #[must_use]
206 pub fn evidence(&self) -> Evidence {
207 Evidence::from_stored(self.alpha, self.beta, self.confidence)
208 }
209
210 #[must_use]
213 pub fn edge_evidence(&self) -> EdgeEvidence {
214 EdgeEvidence::new(self.evidence(), self.self_reinforcements.unwrap_or(0))
215 }
216}
217
218#[derive(Debug, Clone, Copy)]
220pub enum Direction {
221 Outgoing,
222 Incoming,
223 Both,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct EntitySummary {
231 pub id: serde_json::Value,
232 pub name: String,
233 pub entity_type: EntityType,
234 #[serde(rename = "abstract")]
235 pub abstract_text: String,
236}
237
238impl EntitySummary {
239 #[must_use]
240 pub fn id_string(&self) -> String {
241 match &self.id {
242 serde_json::Value::String(s) => s.clone(),
243 other => other.to_string(),
244 }
245 }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct EntityDetail {
251 pub id: serde_json::Value,
252 pub name: String,
253 pub entity_type: EntityType,
254 #[serde(rename = "abstract")]
255 pub abstract_text: String,
256 pub overview: String,
257 pub attributes: Option<serde_json::Value>,
258 #[serde(default)]
259 pub access_count: i64,
260 #[serde(default = "default_utility_score")]
261 pub utility_score: f64,
262 pub updated_at: serde_json::Value,
263 pub source: Option<String>,
264}
265
266impl EntityDetail {
267 #[must_use]
268 pub fn id_string(&self) -> String {
269 match &self.id {
270 serde_json::Value::String(s) => s.clone(),
271 other => other.to_string(),
272 }
273 }
274
275 #[must_use]
276 pub fn updated_at_string(&self) -> String {
277 match &self.updated_at {
278 serde_json::Value::String(s) => s.clone(),
279 other => other.to_string(),
280 }
281 }
282}
283
284#[derive(Debug, Clone, Default)]
288pub struct SearchOptions {
289 pub limit: usize,
290 pub entity_type: Option<String>,
291 pub keyword: Option<String>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(rename_all = "snake_case")]
297pub enum MatchSource {
298 Semantic,
300 Graph { parent: String, rel_type: String },
302 Keyword,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct ScoredEntity {
309 pub entity: EntityDetail,
310 pub score: f64,
311 pub source: MatchSource,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct EpisodeSearchResult {
317 pub episode: Episode,
318 pub score: f64,
319 pub distance: f64,
320}
321
322#[derive(Debug, Clone)]
324pub struct QueryOptions {
325 pub limit: usize,
326 pub entity_type: Option<String>,
327 pub keyword: Option<String>,
328 pub graph_depth: u32,
329 pub include_episodes: bool,
330}
331
332impl Default for QueryOptions {
333 fn default() -> Self {
334 Self {
335 limit: 10,
336 entity_type: None,
337 keyword: None,
338 graph_depth: 1,
339 include_episodes: false,
340 }
341 }
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct QueryResult {
347 pub entities: Vec<ScoredEntity>,
348 pub episodes: Vec<EpisodeSearchResult>,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct SearchResult {
354 pub entity: Entity,
355 pub score: f64,
356 pub distance: f64,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct TraversalNode {
362 pub entity: EntitySummary,
363 pub edges: Vec<TraversalEdge>,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct TraversalEdge {
369 pub rel_type: String,
370 pub direction: String,
371 pub target: TraversalNode,
372 pub valid_from: serde_json::Value,
373 pub valid_until: Option<serde_json::Value>,
374 pub confidence: f64,
375}
376
377#[derive(Debug, Clone, serde::Deserialize)]
379pub struct EdgeRow {
380 pub rel_type: String,
381 pub valid_from: serde_json::Value,
382 pub valid_until: Option<serde_json::Value>,
383 pub target_id: serde_json::Value,
384 #[serde(default = "default_confidence")]
385 pub confidence: f64,
386 #[serde(default)]
387 pub last_reinforced: Option<serde_json::Value>,
388}
389
390fn default_confidence() -> f64 {
391 1.0
392}
393
394impl EdgeRow {
395 #[must_use]
396 pub fn target_id_string(&self) -> String {
397 match &self.target_id {
398 serde_json::Value::String(s) => s.clone(),
399 other => other.to_string(),
400 }
401 }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct GraphStats {
407 pub entity_count: u64,
408 pub relationship_count: u64,
409 pub episode_count: u64,
410 pub entity_type_counts: HashMap<String, u64>,
411}
412
413#[derive(Debug, Clone)]
417pub struct NewEpisode {
418 pub session_id: String,
419 pub abstract_text: String,
420 pub overview: Option<String>,
421 pub content: Option<String>,
422 pub log_number: Option<u32>,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct Episode {
428 pub id: serde_json::Value,
429 pub session_id: String,
430 pub timestamp: serde_json::Value,
431 #[serde(rename = "abstract")]
432 pub abstract_text: String,
433 pub overview: Option<String>,
434 pub content: Option<String>,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub embedding: Option<Vec<f32>>,
438 pub log_number: Option<i64>,
439 #[serde(default)]
443 pub provenance: Option<String>,
444 #[serde(default, deserialize_with = "super::util::count_or_zero")]
447 pub access_count: i64,
448}
449
450impl Episode {
451 #[must_use]
452 pub fn id_string(&self) -> String {
453 match &self.id {
454 serde_json::Value::String(s) => s.clone(),
455 other => other.to_string(),
456 }
457 }
458
459 #[must_use]
463 pub fn provenance(&self) -> Provenance {
464 Provenance::from_stored(self.provenance.as_deref())
465 }
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct ExtractedEntity {
471 pub name: String,
472 #[serde(rename = "type")]
473 pub entity_type: EntityType,
474 #[serde(rename = "abstract")]
475 pub abstract_text: String,
476 pub overview: Option<String>,
477 pub content: Option<String>,
478 pub attributes: Option<serde_json::Value>,
479}
480
481impl ExtractedEntity {
482 #[must_use]
484 pub fn to_new_entity(&self, session_id: &str) -> NewEntity {
485 NewEntity {
486 name: self.name.clone(),
487 entity_type: self.entity_type.clone(),
488 abstract_text: self.abstract_text.clone(),
489 overview: self.overview.clone(),
490 content: self.content.clone(),
491 attributes: self.attributes.clone(),
492 source: Some(session_id.to_string()),
493 }
494 }
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct ExtractedRelationship {
500 pub source: String,
501 pub target: String,
502 pub rel_type: String,
503 pub description: Option<String>,
504 #[serde(default)]
505 pub confidence: Option<String>,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct ExtractedCase {
511 pub problem: String,
512 pub solution: String,
513 pub context: Option<String>,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct ExtractedPattern {
519 pub name: String,
520 pub process: String,
521 pub conditions: Option<String>,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct ExtractedPreference {
527 pub facet: String,
528 pub value: String,
529 pub context: Option<String>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, Default)]
534pub struct ExtractionResult {
535 #[serde(default)]
536 pub entities: Vec<ExtractedEntity>,
537 #[serde(default)]
538 pub relationships: Vec<ExtractedRelationship>,
539 #[serde(default)]
540 pub cases: Vec<ExtractedCase>,
541 #[serde(default)]
542 pub patterns: Vec<ExtractedPattern>,
543 #[serde(default)]
544 pub preferences: Vec<ExtractedPreference>,
545}
546
547#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
549#[serde(rename_all = "snake_case")]
550pub enum DedupDecision {
551 Skip,
552 Create,
553 Merge { target: String },
554}
555
556pub mod pipeline_rels {
560 pub const EVOLVED_FROM: &str = "EVOLVED_FROM";
561 pub const CRYSTALLIZED_FROM: &str = "CRYSTALLIZED_FROM";
562 pub const INFORMED_BY: &str = "INFORMED_BY";
563 pub const EXPLORES: &str = "EXPLORES";
564 pub const GRADUATED_TO: &str = "GRADUATED_TO";
565 pub const ARCHIVED_FROM: &str = "ARCHIVED_FROM";
566 pub const CONNECTED_TO: &str = "CONNECTED_TO";
567 pub const PROMPTED_BY: &str = "PROMPTED_BY";
568 pub const ANSWERED_BY: &str = "ANSWERED_BY";
569}
570
571pub mod vigil_rels {
573 pub const MEASURED_DURING: &str = "MEASURED_DURING";
574 pub const RESULTED_IN: &str = "RESULTED_IN";
575 pub const TRIGGERED_BY: &str = "TRIGGERED_BY";
576}
577
578#[derive(Debug, Clone, Default)]
580pub struct VigilSyncReport {
581 pub measurements_created: u32,
582 pub outcomes_created: u32,
583 pub events_created: u32,
584 pub relationships_created: u32,
585 pub skipped: u32,
586 pub errors: Vec<String>,
587}
588
589#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
591pub struct PipelineDocuments {
592 pub learning: String,
593 pub thoughts: String,
594 pub curiosity: String,
595 pub reflections: String,
596 pub praxis: String,
597}
598
599#[derive(Debug, Clone, Default, Serialize, Deserialize)]
601pub struct PipelineSyncReport {
602 pub entities_created: u32,
603 pub entities_updated: u32,
604 pub entities_archived: u32,
605 pub relationships_created: u32,
606 pub relationships_skipped: u32,
607 pub errors: Vec<String>,
608}
609
610#[derive(Debug, Clone, Default)]
612pub struct PipelineGraphStats {
613 pub by_stage: HashMap<String, HashMap<String, u64>>,
614 pub stale_thoughts: Vec<EntityDetail>,
615 pub stale_questions: Vec<EntityDetail>,
616 pub orphan_count: u64,
617 pub total_entities: u64,
618 pub last_movement: Option<String>,
619}
620
621#[derive(Debug, Clone)]
623pub struct PipelineEntry {
624 pub title: String,
626 pub body: String,
628 pub status: String,
630 pub stage: String,
632 pub entity_type: EntityType,
634 pub date: Option<String>,
636 pub source_ref: Option<String>,
638 pub destination: Option<String>,
640 pub connected_to: Vec<String>,
642 pub sub_type: Option<String>,
644}
645
646#[derive(Debug, Clone, Default, Serialize, Deserialize)]
648pub struct IngestionReport {
649 pub episodes_created: u32,
650 pub entities_created: u32,
651 pub entities_merged: u32,
652 pub entities_skipped: u32,
653 pub relationships_created: u32,
654 pub relationships_skipped: u32,
655 pub errors: Vec<String>,
656 pub estimated_tokens: u64,
657 #[serde(default)]
661 pub entity_ids: Vec<String>,
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 fn episode_with(embedding: Option<Vec<f32>>) -> Episode {
669 Episode {
670 id: serde_json::json!("episode:one"),
671 session_id: "s1".into(),
672 timestamp: serde_json::json!("2026-01-01T00:00:00Z"),
673 abstract_text: "a".into(),
674 overview: None,
675 content: None,
676 embedding,
677 log_number: Some(1),
678 provenance: None,
679 access_count: 0,
680 }
681 }
682
683 #[test]
686 fn an_episode_without_an_embedding_carries_no_embedding_field() {
687 let json = serde_json::to_value(episode_with(None)).unwrap();
688 assert!(json.get("embedding").is_none(), "{json}");
689
690 let json = serde_json::to_value(episode_with(Some(vec![0.5; 384]))).unwrap();
691 assert!(json.get("embedding").is_some(), "{json}");
692 }
693
694 #[test]
695 fn an_episode_round_trips_without_its_embedding() {
696 let line = serde_json::to_string(&episode_with(None)).unwrap();
697 let parsed: Episode = serde_json::from_str(&line).unwrap();
698 assert!(parsed.embedding.is_none());
699 assert_eq!(parsed.session_id, "s1");
700 }
701
702 #[test]
703 fn pipeline_documents_survive_the_daemon_wire_format() {
704 let docs = PipelineDocuments {
705 learning: "# learning".into(),
706 thoughts: "# thoughts".into(),
707 curiosity: String::new(),
708 reflections: "# reflections".into(),
709 praxis: String::new(),
710 };
711 let line = serde_json::to_string(&docs).unwrap();
712 assert_eq!(
713 serde_json::from_str::<PipelineDocuments>(&line).unwrap(),
714 docs
715 );
716 }
717}