use serde::{Deserialize, Serialize};
pub(crate) const REL_CONTRADICTS: &str = "contradicts";
pub(crate) const REL_REFLECTS_ON: &str = "reflects_on";
pub(crate) const REL_DERIVES_FROM: &str = "derives_from";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AttestLevel {
Unsigned,
SelfSigned,
PeerAttested,
SignedByPeer,
DaemonSigned,
}
impl AttestLevel {
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
match s {
"unsigned" => Some(Self::Unsigned),
"self_signed" => Some(Self::SelfSigned),
"peer_attested" => Some(Self::PeerAttested),
"signed_by_peer" => Some(Self::SignedByPeer),
"daemon_signed" => Some(Self::DaemonSigned),
_ => None,
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Unsigned => "unsigned",
Self::SelfSigned => "self_signed",
Self::PeerAttested => "peer_attested",
Self::SignedByPeer => "signed_by_peer",
Self::DaemonSigned => "daemon_signed",
}
}
}
impl std::fmt::Display for AttestLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemoryLinkRelation {
RelatedTo,
Supersedes,
Contradicts,
DerivedFrom,
ReflectsOn,
DerivesFrom,
}
impl MemoryLinkRelation {
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
match s {
"related_to" => Some(Self::RelatedTo),
"supersedes" => Some(Self::Supersedes),
REL_CONTRADICTS => Some(Self::Contradicts),
"derived_from" => Some(Self::DerivedFrom),
REL_REFLECTS_ON => Some(Self::ReflectsOn),
REL_DERIVES_FROM => Some(Self::DerivesFrom),
_ => None,
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::RelatedTo => "related_to",
Self::Supersedes => "supersedes",
Self::Contradicts => REL_CONTRADICTS,
Self::DerivedFrom => "derived_from",
Self::ReflectsOn => REL_REFLECTS_ON,
Self::DerivesFrom => REL_DERIVES_FROM,
}
}
#[must_use]
pub const fn default_relation() -> Self {
Self::RelatedTo
}
pub const COUNT: usize = 6;
#[must_use]
pub const fn all() -> &'static [Self; Self::COUNT] {
&[
Self::RelatedTo,
Self::Supersedes,
Self::Contradicts,
Self::DerivedFrom,
Self::ReflectsOn,
Self::DerivesFrom,
]
}
}
impl std::fmt::Display for MemoryLinkRelation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Default for MemoryLinkRelation {
fn default() -> Self {
Self::default_relation()
}
}
impl std::str::FromStr for MemoryLinkRelation {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_str(s).ok_or_else(|| {
format!(
"invalid memory_link relation '{s}' (expected one of: related_to, \
supersedes, contradicts, derived_from, reflects_on, derives_from)"
)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryLink {
pub source_id: String,
pub target_id: String,
pub relation: MemoryLinkRelation,
pub created_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub valid_from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub valid_until: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attest_level: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct LinkBody {
#[serde(default)]
pub source_id: Option<String>,
#[serde(default)]
pub from: Option<String>,
#[serde(default)]
pub target_id: Option<String>,
#[serde(default)]
pub to: Option<String>,
#[serde(default)]
pub relation: Option<String>,
#[serde(default)]
pub rel_type: Option<String>,
}
impl LinkBody {
#[must_use]
pub fn resolved(&self) -> (String, String, String) {
let s = self
.source_id
.clone()
.or_else(|| self.from.clone())
.unwrap_or_default();
let t = self
.target_id
.clone()
.or_else(|| self.to.clone())
.unwrap_or_default();
let r = self
.relation
.clone()
.or_else(|| self.rel_type.clone())
.unwrap_or_else(default_relation);
(s, t, r)
}
}
fn default_relation() -> String {
MemoryLinkRelation::RelatedTo.as_str().to_string()
}
pub const ENTITY_TAG: &str = "entity";
pub const ENTITY_KIND: &str = "entity";
#[derive(Debug, Clone, Serialize)]
pub struct EntityRecord {
pub entity_id: String,
pub canonical_name: String,
pub namespace: String,
pub aliases: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct EntityRegistration {
pub entity_id: String,
pub canonical_name: String,
pub namespace: String,
pub aliases: Vec<String>,
pub created: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct KgTimelineEvent {
pub target_id: String,
pub relation: String,
pub valid_from: String,
pub valid_until: Option<String>,
pub observed_by: Option<String>,
pub title: String,
pub target_namespace: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct KgQueryNode {
pub target_id: String,
pub relation: String,
pub valid_from: Option<String>,
pub valid_until: Option<String>,
pub observed_by: Option<String>,
pub title: String,
pub target_namespace: String,
pub depth: usize,
pub path: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DuplicateMatch {
pub id: String,
pub title: String,
pub namespace: String,
pub similarity: f32,
}
#[derive(Debug, Clone, Serialize)]
pub struct DuplicateCheck {
pub is_duplicate: bool,
pub threshold: f32,
pub nearest: Option<DuplicateMatch>,
pub candidates_scanned: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct TaxonomyNode {
pub namespace: String,
pub name: String,
pub count: usize,
pub subtree_count: usize,
pub children: Vec<TaxonomyNode>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Taxonomy {
pub tree: TaxonomyNode,
pub total_count: usize,
pub truncated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct VectorClock {
#[serde(default)]
pub entries: std::collections::BTreeMap<String, String>,
}
impl VectorClock {
#[allow(dead_code)] pub fn observe(&mut self, peer_id: &str, at: &str) {
self.entries
.entry(peer_id.to_string())
.and_modify(|existing| {
if at > existing.as_str() {
*existing = at.to_string();
}
})
.or_insert_with(|| at.to_string());
}
#[must_use]
#[allow(dead_code)] pub fn latest_from(&self, peer_id: &str) -> Option<&str> {
self.entries.get(peer_id).map(String::as_str)
}
}
#[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncStateEntry {
pub agent_id: String,
pub peer_id: String,
pub last_seen_at: String,
pub last_pulled_at: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_link_body(json: serde_json::Value) -> LinkBody {
serde_json::from_value(json).expect("LinkBody deserialises")
}
#[test]
fn link_body_resolved_uses_canonical_fields_when_present() {
let b = parse_link_body(serde_json::json!({
"source_id": "src",
"target_id": "tgt",
"relation": "supersedes",
}));
let (s, t, r) = b.resolved();
assert_eq!(s, "src");
assert_eq!(t, "tgt");
assert_eq!(r, "supersedes");
}
#[test]
fn link_body_resolved_falls_back_to_from_alias() {
let b = parse_link_body(serde_json::json!({
"from": "from-id",
"to": "to-id",
"rel_type": "contradicts",
}));
let (s, t, r) = b.resolved();
assert_eq!(s, "from-id");
assert_eq!(t, "to-id");
assert_eq!(r, "contradicts");
}
#[test]
fn link_body_resolved_defaults_relation_to_related_to() {
let b = parse_link_body(serde_json::json!({
"source_id": "a",
"target_id": "b",
}));
let (_s, _t, r) = b.resolved();
assert_eq!(r, "related_to");
}
#[test]
fn link_body_resolved_empty_payload_returns_empty_strings_and_default() {
let b = parse_link_body(serde_json::json!({}));
let (s, t, r) = b.resolved();
assert_eq!(s, "");
assert_eq!(t, "");
assert_eq!(r, "related_to");
}
#[test]
fn link_body_resolved_canonical_wins_over_alias() {
let b = parse_link_body(serde_json::json!({
"source_id": "canonical-src",
"from": "alias-src",
"target_id": "canonical-tgt",
"to": "alias-tgt",
"relation": "canonical-rel",
"rel_type": "alias-rel",
}));
let (s, t, r) = b.resolved();
assert_eq!(s, "canonical-src");
assert_eq!(t, "canonical-tgt");
assert_eq!(r, "canonical-rel");
}
#[test]
fn attest_level_round_trips_strings() {
for (s, v) in [
("unsigned", AttestLevel::Unsigned),
("self_signed", AttestLevel::SelfSigned),
("peer_attested", AttestLevel::PeerAttested),
] {
assert_eq!(AttestLevel::from_str(s), Some(v));
assert_eq!(v.as_str(), s);
assert_eq!(format!("{v}"), s);
}
}
#[test]
fn attest_level_from_str_returns_none_for_unknown() {
assert_eq!(AttestLevel::from_str("unknown"), None);
assert_eq!(AttestLevel::from_str(""), None);
}
#[test]
fn vector_clock_observe_advances_monotonically() {
let mut c = VectorClock::default();
c.observe("peer-a", "2026-01-01T00:00:00Z");
assert_eq!(c.latest_from("peer-a"), Some("2026-01-01T00:00:00Z"));
c.observe("peer-a", "2026-02-01T00:00:00Z");
assert_eq!(c.latest_from("peer-a"), Some("2026-02-01T00:00:00Z"));
c.observe("peer-a", "2025-12-01T00:00:00Z");
assert_eq!(c.latest_from("peer-a"), Some("2026-02-01T00:00:00Z"));
}
#[test]
fn vector_clock_latest_from_unknown_peer_is_none() {
let c = VectorClock::default();
assert_eq!(c.latest_from("never-seen"), None);
}
#[test]
fn vector_clock_serializes_as_object_with_entries() {
let mut c = VectorClock::default();
c.observe("peer-a", "2026-01-01T00:00:00Z");
let json = serde_json::to_value(&c).unwrap();
assert!(json.get("entries").is_some());
assert_eq!(
json["entries"]["peer-a"],
serde_json::Value::String("2026-01-01T00:00:00Z".to_string())
);
}
#[test]
fn memory_link_relation_from_str_returns_none_for_unknown() {
assert_eq!(MemoryLinkRelation::from_str("bogus"), None);
assert_eq!(MemoryLinkRelation::from_str(""), None);
assert_eq!(MemoryLinkRelation::from_str("RELATED_TO"), None);
}
#[test]
fn memory_link_relation_default_relation_is_related_to() {
let d = MemoryLinkRelation::default_relation();
assert_eq!(d, MemoryLinkRelation::RelatedTo);
assert_eq!(d.as_str(), "related_to");
}
#[test]
fn memory_link_relation_default_trait_uses_related_to() {
let d: MemoryLinkRelation = Default::default();
assert_eq!(d, MemoryLinkRelation::RelatedTo);
}
#[test]
fn memory_link_relation_from_str_trait_round_trips_canonical_strings() {
for (s, v) in [
("related_to", MemoryLinkRelation::RelatedTo),
("supersedes", MemoryLinkRelation::Supersedes),
("contradicts", MemoryLinkRelation::Contradicts),
("derived_from", MemoryLinkRelation::DerivedFrom),
("reflects_on", MemoryLinkRelation::ReflectsOn),
("derives_from", MemoryLinkRelation::DerivesFrom),
] {
let parsed: MemoryLinkRelation =
<MemoryLinkRelation as std::str::FromStr>::from_str(s).unwrap();
assert_eq!(parsed, v);
assert_eq!(format!("{v}"), s);
}
}
#[test]
fn memory_link_relation_from_str_trait_returns_helpful_error_for_unknown() {
let err = <MemoryLinkRelation as std::str::FromStr>::from_str("nope").unwrap_err();
assert!(err.contains("nope"));
assert!(err.contains("related_to"));
assert!(err.contains("reflects_on"));
}
}