use std::collections::HashMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use super::confidence::{EdgeEvidence, Evidence, Provenance};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum EntityType {
Person,
Project,
Tool,
Service,
Preference,
Decision,
Event,
Concept,
Case,
Pattern,
Thread,
Thought,
Question,
Observation,
Policy,
Measurement,
Outcome,
}
impl EntityType {
#[must_use]
pub fn is_mutable(&self) -> bool {
!matches!(
self,
Self::Decision
| Self::Event
| Self::Case
| Self::Observation
| Self::Measurement
| Self::Outcome
)
}
}
impl fmt::Display for EntityType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = serde_json::to_value(self)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_else(|| format!("{self:?}"));
write!(f, "{s}")
}
}
impl std::str::FromStr for EntityType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_value(serde_json::Value::String(s.to_string()))
.map_err(|_| format!("unknown entity type: {s}"))
}
}
#[derive(Debug, Clone)]
pub struct NewEntity {
pub name: String,
pub entity_type: EntityType,
pub abstract_text: String,
pub overview: Option<String>,
pub content: Option<String>,
pub attributes: Option<serde_json::Value>,
pub source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entity {
pub id: serde_json::Value,
pub name: String,
pub entity_type: EntityType,
#[serde(rename = "abstract")]
pub abstract_text: String,
pub overview: String,
pub content: Option<String>,
pub attributes: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embedding: Option<Vec<f32>>,
#[serde(default = "default_true")]
pub mutable: bool,
#[serde(default)]
pub access_count: i64,
#[serde(default = "default_utility_score")]
pub utility_score: f64,
#[serde(default)]
pub utility_updates: i64,
pub created_at: serde_json::Value,
pub updated_at: serde_json::Value,
pub source: Option<String>,
}
impl Entity {
#[must_use]
pub fn id_string(&self) -> String {
match &self.id {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
#[must_use]
pub fn updated_at_string(&self) -> String {
match &self.updated_at {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
}
fn default_true() -> bool {
true
}
fn default_utility_score() -> f64 {
0.5
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct EntityUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub abstract_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub overview: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct NewRelationship {
pub from_entity: String,
pub to_entity: String,
pub rel_type: String,
pub description: Option<String>,
pub confidence: Option<f32>,
pub source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Relationship {
pub id: serde_json::Value,
#[serde(rename = "in")]
pub from_id: serde_json::Value,
#[serde(rename = "out")]
pub to_id: serde_json::Value,
pub rel_type: String,
pub description: Option<String>,
pub valid_from: serde_json::Value,
pub valid_until: Option<serde_json::Value>,
pub confidence: f64,
#[serde(default)]
pub alpha: Option<f64>,
#[serde(default)]
pub beta: Option<f64>,
#[serde(default)]
pub self_reinforcements: Option<i64>,
#[serde(default)]
pub last_reinforced: Option<serde_json::Value>,
pub source: Option<String>,
}
impl Relationship {
#[must_use]
pub fn id_string(&self) -> String {
match &self.id {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
#[must_use]
pub fn evidence(&self) -> Evidence {
Evidence::from_stored(self.alpha, self.beta, self.confidence)
}
#[must_use]
pub fn edge_evidence(&self) -> EdgeEvidence {
EdgeEvidence::new(self.evidence(), self.self_reinforcements.unwrap_or(0))
}
}
#[derive(Debug, Clone, Copy)]
pub enum Direction {
Outgoing,
Incoming,
Both,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntitySummary {
pub id: serde_json::Value,
pub name: String,
pub entity_type: EntityType,
#[serde(rename = "abstract")]
pub abstract_text: String,
}
impl EntitySummary {
#[must_use]
pub fn id_string(&self) -> String {
match &self.id {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityDetail {
pub id: serde_json::Value,
pub name: String,
pub entity_type: EntityType,
#[serde(rename = "abstract")]
pub abstract_text: String,
pub overview: String,
pub attributes: Option<serde_json::Value>,
#[serde(default)]
pub access_count: i64,
#[serde(default = "default_utility_score")]
pub utility_score: f64,
pub updated_at: serde_json::Value,
pub source: Option<String>,
}
impl EntityDetail {
#[must_use]
pub fn id_string(&self) -> String {
match &self.id {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
#[must_use]
pub fn updated_at_string(&self) -> String {
match &self.updated_at {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SearchOptions {
pub limit: usize,
pub entity_type: Option<String>,
pub keyword: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchSource {
Semantic,
Graph { parent: String, rel_type: String },
Keyword,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoredEntity {
pub entity: EntityDetail,
pub score: f64,
pub source: MatchSource,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpisodeSearchResult {
pub episode: Episode,
pub score: f64,
pub distance: f64,
}
#[derive(Debug, Clone)]
pub struct QueryOptions {
pub limit: usize,
pub entity_type: Option<String>,
pub keyword: Option<String>,
pub graph_depth: u32,
pub include_episodes: bool,
}
impl Default for QueryOptions {
fn default() -> Self {
Self {
limit: 10,
entity_type: None,
keyword: None,
graph_depth: 1,
include_episodes: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResult {
pub entities: Vec<ScoredEntity>,
pub episodes: Vec<EpisodeSearchResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub entity: Entity,
pub score: f64,
pub distance: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraversalNode {
pub entity: EntitySummary,
pub edges: Vec<TraversalEdge>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraversalEdge {
pub rel_type: String,
pub direction: String,
pub target: TraversalNode,
pub valid_from: serde_json::Value,
pub valid_until: Option<serde_json::Value>,
pub confidence: f64,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct EdgeRow {
pub rel_type: String,
pub valid_from: serde_json::Value,
pub valid_until: Option<serde_json::Value>,
pub target_id: serde_json::Value,
#[serde(default = "default_confidence")]
pub confidence: f64,
#[serde(default)]
pub last_reinforced: Option<serde_json::Value>,
}
fn default_confidence() -> f64 {
1.0
}
impl EdgeRow {
#[must_use]
pub fn target_id_string(&self) -> String {
match &self.target_id {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphStats {
pub entity_count: u64,
pub relationship_count: u64,
pub episode_count: u64,
pub entity_type_counts: HashMap<String, u64>,
}
#[derive(Debug, Clone)]
pub struct NewEpisode {
pub session_id: String,
pub abstract_text: String,
pub overview: Option<String>,
pub content: Option<String>,
pub log_number: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Episode {
pub id: serde_json::Value,
pub session_id: String,
pub timestamp: serde_json::Value,
#[serde(rename = "abstract")]
pub abstract_text: String,
pub overview: Option<String>,
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embedding: Option<Vec<f32>>,
pub log_number: Option<i64>,
#[serde(default)]
pub provenance: Option<String>,
#[serde(default, deserialize_with = "super::util::count_or_zero")]
pub access_count: i64,
}
impl Episode {
#[must_use]
pub fn id_string(&self) -> String {
match &self.id {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
#[must_use]
pub fn provenance(&self) -> Provenance {
Provenance::from_stored(self.provenance.as_deref())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedEntity {
pub name: String,
#[serde(rename = "type")]
pub entity_type: EntityType,
#[serde(rename = "abstract")]
pub abstract_text: String,
pub overview: Option<String>,
pub content: Option<String>,
pub attributes: Option<serde_json::Value>,
}
impl ExtractedEntity {
#[must_use]
pub fn to_new_entity(&self, session_id: &str) -> NewEntity {
NewEntity {
name: self.name.clone(),
entity_type: self.entity_type.clone(),
abstract_text: self.abstract_text.clone(),
overview: self.overview.clone(),
content: self.content.clone(),
attributes: self.attributes.clone(),
source: Some(session_id.to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedRelationship {
pub source: String,
pub target: String,
pub rel_type: String,
pub description: Option<String>,
#[serde(default)]
pub confidence: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedCase {
pub problem: String,
pub solution: String,
pub context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedPattern {
pub name: String,
pub process: String,
pub conditions: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedPreference {
pub facet: String,
pub value: String,
pub context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExtractionResult {
#[serde(default)]
pub entities: Vec<ExtractedEntity>,
#[serde(default)]
pub relationships: Vec<ExtractedRelationship>,
#[serde(default)]
pub cases: Vec<ExtractedCase>,
#[serde(default)]
pub patterns: Vec<ExtractedPattern>,
#[serde(default)]
pub preferences: Vec<ExtractedPreference>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DedupDecision {
Skip,
Create,
Merge { target: String },
}
pub mod pipeline_rels {
pub const EVOLVED_FROM: &str = "EVOLVED_FROM";
pub const CRYSTALLIZED_FROM: &str = "CRYSTALLIZED_FROM";
pub const INFORMED_BY: &str = "INFORMED_BY";
pub const EXPLORES: &str = "EXPLORES";
pub const GRADUATED_TO: &str = "GRADUATED_TO";
pub const ARCHIVED_FROM: &str = "ARCHIVED_FROM";
pub const CONNECTED_TO: &str = "CONNECTED_TO";
pub const PROMPTED_BY: &str = "PROMPTED_BY";
pub const ANSWERED_BY: &str = "ANSWERED_BY";
}
pub mod vigil_rels {
pub const MEASURED_DURING: &str = "MEASURED_DURING";
pub const RESULTED_IN: &str = "RESULTED_IN";
pub const TRIGGERED_BY: &str = "TRIGGERED_BY";
}
#[derive(Debug, Clone, Default)]
pub struct VigilSyncReport {
pub measurements_created: u32,
pub outcomes_created: u32,
pub events_created: u32,
pub relationships_created: u32,
pub skipped: u32,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PipelineDocuments {
pub learning: String,
pub thoughts: String,
pub curiosity: String,
pub reflections: String,
pub praxis: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PipelineSyncReport {
pub entities_created: u32,
pub entities_updated: u32,
pub entities_archived: u32,
pub relationships_created: u32,
pub relationships_skipped: u32,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct PipelineGraphStats {
pub by_stage: HashMap<String, HashMap<String, u64>>,
pub stale_thoughts: Vec<EntityDetail>,
pub stale_questions: Vec<EntityDetail>,
pub orphan_count: u64,
pub total_entities: u64,
pub last_movement: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PipelineEntry {
pub title: String,
pub body: String,
pub status: String,
pub stage: String,
pub entity_type: EntityType,
pub date: Option<String>,
pub source_ref: Option<String>,
pub destination: Option<String>,
pub connected_to: Vec<String>,
pub sub_type: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IngestionReport {
pub episodes_created: u32,
pub entities_created: u32,
pub entities_merged: u32,
pub entities_skipped: u32,
pub relationships_created: u32,
pub relationships_skipped: u32,
pub errors: Vec<String>,
pub estimated_tokens: u64,
#[serde(default)]
pub entity_ids: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn episode_with(embedding: Option<Vec<f32>>) -> Episode {
Episode {
id: serde_json::json!("episode:one"),
session_id: "s1".into(),
timestamp: serde_json::json!("2026-01-01T00:00:00Z"),
abstract_text: "a".into(),
overview: None,
content: None,
embedding,
log_number: Some(1),
provenance: None,
access_count: 0,
}
}
#[test]
fn an_episode_without_an_embedding_carries_no_embedding_field() {
let json = serde_json::to_value(episode_with(None)).unwrap();
assert!(json.get("embedding").is_none(), "{json}");
let json = serde_json::to_value(episode_with(Some(vec![0.5; 384]))).unwrap();
assert!(json.get("embedding").is_some(), "{json}");
}
#[test]
fn an_episode_round_trips_without_its_embedding() {
let line = serde_json::to_string(&episode_with(None)).unwrap();
let parsed: Episode = serde_json::from_str(&line).unwrap();
assert!(parsed.embedding.is_none());
assert_eq!(parsed.session_id, "s1");
}
#[test]
fn pipeline_documents_survive_the_daemon_wire_format() {
let docs = PipelineDocuments {
learning: "# learning".into(),
thoughts: "# thoughts".into(),
curiosity: String::new(),
reflections: "# reflections".into(),
praxis: String::new(),
};
let line = serde_json::to_string(&docs).unwrap();
assert_eq!(
serde_json::from_str::<PipelineDocuments>(&line).unwrap(),
docs
);
}
}