pub mod content;
pub mod migrate_inputs;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::memory::{MemoryId, MemoryRef};
use crate::partition::PartitionPath;
use crate::summarizer::SummaryStyle;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
pub struct SummaryId(Ulid);
impl SummaryId {
#[must_use]
pub fn generate() -> Self {
if std::env::var_os("KIROMI_AI_TEST_DETERMINISTIC_ULID").is_some() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1_000_000);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
return SummaryId(Ulid::from_parts(n, u128::from(n)));
}
SummaryId(Ulid::new())
}
#[must_use]
pub const fn from_ulid(u: Ulid) -> Self {
SummaryId(u)
}
#[must_use]
pub const fn as_ulid(&self) -> Ulid {
self.0
}
}
impl fmt::Display for SummaryId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl FromStr for SummaryId {
type Err = ulid::DecodeError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
s.parse::<Ulid>().map(SummaryId)
}
}
impl From<SummaryId> for String {
fn from(id: SummaryId) -> String {
id.0.to_string()
}
}
impl TryFrom<String> for SummaryId {
type Error = ulid::DecodeError;
fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
s.parse()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum SummarySubject {
Memory(MemoryRef),
Partition(PartitionPath),
Tenant,
}
impl SummarySubject {
#[must_use]
pub fn kind_str(&self) -> &'static str {
match self {
SummarySubject::Memory(_) => "memory",
SummarySubject::Partition(_) => "partition",
SummarySubject::Tenant => "tenant",
}
}
#[must_use]
pub fn partition_path(&self) -> Option<&PartitionPath> {
match self {
SummarySubject::Memory(r) => Some(&r.partition),
SummarySubject::Partition(p) => Some(p),
SummarySubject::Tenant => None,
}
}
#[must_use]
pub fn memory_id(&self) -> Option<MemoryId> {
match self {
SummarySubject::Memory(r) => Some(r.id),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SummaryRef {
pub id: SummaryId,
pub subject: SummarySubject,
pub style: SummaryStyle,
pub version: u32,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SummaryRecord {
pub r#ref: SummaryRef,
pub content: crate::summary::content::SummaryContent,
pub summarizer_id: String,
pub inputs: Vec<SummarySubject>,
pub superseded_by: Option<SummaryId>,
pub created_at_ms: i64,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaleKind {
Summary,
ChildIndex,
Both,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum Scope {
All,
Tenant,
Partition(PartitionPath),
Memory(MemoryRef),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_roundtrips_through_string() {
let id = SummaryId::generate();
let s = id.to_string();
let back: SummaryId = s.parse().unwrap();
assert_eq!(id, back);
}
#[test]
fn subject_kind_strings_are_stable() {
let m = SummarySubject::Memory(MemoryRef {
id: MemoryId::generate(),
partition: "user=alex".parse().unwrap(),
});
let p = SummarySubject::Partition("user=alex".parse().unwrap());
let t = SummarySubject::Tenant;
assert_eq!(m.kind_str(), "memory");
assert_eq!(p.kind_str(), "partition");
assert_eq!(t.kind_str(), "tenant");
}
#[test]
fn subject_round_trips_through_json() {
let p = SummarySubject::Partition("user=alex/year=2026".parse().unwrap());
let j = serde_json::to_string(&p).unwrap();
let back: SummarySubject = serde_json::from_str(&j).unwrap();
assert_eq!(p, back);
}
#[test]
fn subject_partition_path_helpers() {
let mref = MemoryRef {
id: MemoryId::generate(),
partition: "user=alex".parse().unwrap(),
};
let m = SummarySubject::Memory(mref.clone());
assert_eq!(
m.partition_path().map(|p| p.as_str().to_string()),
Some("user=alex".to_string())
);
assert_eq!(m.memory_id(), Some(mref.id));
let p = SummarySubject::Partition("a=b".parse().unwrap());
assert!(p.partition_path().is_some());
assert!(p.memory_id().is_none());
let t = SummarySubject::Tenant;
assert!(t.partition_path().is_none());
assert!(t.memory_id().is_none());
}
#[test]
fn summary_id_serde_round_trip() {
let id = SummaryId::generate();
let j = serde_json::to_string(&id).unwrap();
let back: SummaryId = serde_json::from_str(&j).unwrap();
assert_eq!(id, back);
}
#[test]
fn summary_id_from_ulid_and_as_ulid_round_trip() {
let raw = ulid::Ulid::new();
let id = SummaryId::from_ulid(raw);
assert_eq!(id.as_ulid(), raw);
}
}