use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::bitemporal::BiTemporal;
use crate::hlc::{Hlc, HlcClock};
use crate::scope::Scope;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Episode {
pub id: Ulid,
pub scope: Scope,
pub source: String,
pub content: String,
pub t_ref: Option<DateTime<Utc>>,
pub bt: BiTemporal,
#[serde(default)]
pub metadata: serde_json::Map<String, serde_json::Value>,
}
impl Episode {
pub fn ground_valid_axis(&mut self) {
if let Some(t) = self.t_ref {
self.bt.valid.0 = Hlc::from_utc(t);
}
}
pub fn new(
scope: Scope,
source: impl Into<String>,
content: impl Into<String>,
clock: &HlcClock,
) -> Self {
Self {
id: Ulid::new(),
scope,
source: source.into(),
content: content.into(),
t_ref: None,
bt: BiTemporal::now(clock),
metadata: serde_json::Map::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Chunk {
pub id: Ulid,
pub scope: Scope,
pub episode_id: Ulid,
pub text: String,
pub tokens: u32,
pub offset: u32,
#[serde(default)]
pub heading_path: Vec<String>,
#[serde(default)]
pub overlap_tail: String,
#[serde(default, skip_serializing)]
pub embedding: Option<Vec<f32>>,
#[serde(default)]
pub parent_id: Option<Ulid>,
pub bt: BiTemporal,
}
impl Chunk {
pub fn new(
scope: Scope,
episode_id: Ulid,
text: impl Into<String>,
tokens: u32,
offset: u32,
heading_path: Vec<String>,
clock: &HlcClock,
) -> Self {
Self {
id: Ulid::new(),
scope,
episode_id,
text: text.into(),
tokens,
offset,
heading_path,
overlap_tail: String::new(),
embedding: None,
parent_id: None,
bt: BiTemporal::now(clock),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Entity {
pub id: Ulid,
pub scope: Scope,
pub name: String,
#[serde(default)]
pub aliases: Vec<String>,
pub entity_type: String,
#[serde(default, skip_serializing)]
pub embedding: Option<Vec<f32>>,
pub bt: BiTemporal,
pub confidence: f32,
}
impl Entity {
pub fn new(
scope: Scope,
name: impl Into<String>,
entity_type: impl Into<String>,
confidence: f32,
clock: &HlcClock,
) -> Self {
Self {
id: Ulid::new(),
scope,
name: name.into(),
aliases: Vec::new(),
entity_type: entity_type.into(),
embedding: None,
bt: BiTemporal::now(clock),
confidence,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Relation {
pub id: Ulid,
pub scope: Scope,
pub src: Ulid,
pub dst: Ulid,
pub rel_type: String,
pub bt: BiTemporal,
pub confidence: f32,
#[serde(default)]
pub provenance: Vec<Ulid>,
}
impl Relation {
pub fn new(
scope: Scope,
src: Ulid,
dst: Ulid,
rel_type: impl Into<String>,
confidence: f32,
clock: &HlcClock,
) -> Self {
Self {
id: Ulid::new(),
scope,
src,
dst,
rel_type: rel_type.into(),
bt: BiTemporal::now(clock),
confidence,
provenance: Vec::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Fact {
pub id: Ulid,
pub scope: Scope,
pub subject: Ulid,
pub predicate: String,
pub object: Ulid,
pub fact_text: String,
#[serde(default, skip_serializing)]
pub embedding: Option<Vec<f32>>,
pub bt: BiTemporal,
pub confidence: f32,
#[serde(default)]
pub provenance: Vec<Ulid>,
pub activation: f32,
}
impl Fact {
pub fn new(
scope: Scope,
subject: Ulid,
predicate: impl Into<String>,
object: Ulid,
fact_text: impl Into<String>,
confidence: f32,
clock: &HlcClock,
) -> Self {
Self {
id: Ulid::new(),
scope,
subject,
predicate: predicate.into(),
object,
fact_text: fact_text.into(),
embedding: None,
bt: BiTemporal::now(clock),
confidence,
provenance: Vec::new(),
activation: 0.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Community {
pub id: Ulid,
pub scope: Scope,
pub level: u8,
pub parent: Option<Ulid>,
#[serde(default)]
pub members: Vec<Ulid>,
pub summary: String,
#[serde(default, skip_serializing)]
pub summary_embedding: Option<Vec<f32>>,
pub bt: BiTemporal,
}
impl Community {
pub fn new(scope: Scope, level: u8, summary: impl Into<String>, clock: &HlcClock) -> Self {
Self {
id: Ulid::new(),
scope,
level,
parent: None,
members: Vec::new(),
summary: summary.into(),
summary_embedding: None,
bt: BiTemporal::now(clock),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hlc::HlcClock;
use crate::scope::Scope;
fn dev_scope() -> Scope {
Scope::dev()
}
fn test_clock() -> std::sync::Arc<HlcClock> {
HlcClock::new(0)
}
#[test]
fn chunk_deserializes_without_parent_id() {
let clock = test_clock();
let chunk = Chunk::new(dev_scope(), ulid::Ulid::new(), "hello world", 2, 0, vec![], &clock);
let mut map: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(&serde_json::to_string(&chunk).unwrap()).unwrap();
map.remove("parent_id");
let stripped = serde_json::to_string(&map).unwrap();
let back: Chunk = serde_json::from_str(&stripped)
.expect("back-compat: must deserialize without parent_id");
assert!(chunk.parent_id.is_none(), "parent_id must be None for pre-existing rows");
assert_eq!(back.text, chunk.text);
}
#[test]
fn chunk_deserializes_with_parent_id() {
let clock = test_clock();
let mut chunk =
Chunk::new(dev_scope(), ulid::Ulid::new(), "hello world", 2, 0, vec![], &clock);
let parent = ulid::Ulid::new();
chunk.parent_id = Some(parent);
let json = serde_json::to_string(&chunk).unwrap();
let back: Chunk = serde_json::from_str(&json).expect("must deserialize with parent_id");
assert_eq!(back.parent_id, Some(parent), "parent_id must be Some when present in JSON");
}
#[test]
fn chunk_new_has_none_parent_id() {
let clock = test_clock();
let chunk = Chunk::new(dev_scope(), ulid::Ulid::new(), "test text", 3, 0, vec![], &clock);
assert!(chunk.parent_id.is_none(), "Chunk::new must produce parent_id = None");
}
#[test]
fn chunk_with_parent_id_roundtrips_serde() {
let clock = test_clock();
let mut chunk = Chunk::new(
dev_scope(),
ulid::Ulid::new(),
"test text",
3,
0,
vec!["Section 1".to_string()],
&clock,
);
let parent = ulid::Ulid::new();
chunk.parent_id = Some(parent);
let json = serde_json::to_string(&chunk).expect("serialize");
let back: Chunk = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.parent_id, Some(parent));
assert_eq!(back.text, chunk.text);
}
}