kiromi-ai-memory 0.2.2

Local-first multi-tenant memory store engine: Markdown/text content on object storage, metadata in SQLite, plugin-shaped embedder/storage/metadata, hybrid text+vector search.
Documentation
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Explicit / inferred links between memories.
//!
//! Plan 15 broadened [`LinkKind`] from a single `Explicit` variant to a
//! richer set: `Supersedes`, `Contradicts`, `Derived`, `PartOf`,
//! `Related`. The enum stays `#[non_exhaustive]` so future kinds drop in
//! without a SemVer break.

use serde::{Deserialize, Serialize};

use crate::graph::NodeRef;
use crate::memory::MemoryId;

/// Link kind. Slice 1 produced only `Explicit`; Plan 15 added the
/// remaining variants. The persisted SQL string is `as_persisted_str`.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum LinkKind {
    /// User- or app-asserted link. Bidirectional: writing `A→B` records `B→A`.
    Explicit,
    /// "This memory replaces an older one." Non-destructive update — the
    /// older memory stays addressable; the link records the supersession.
    Supersedes,
    /// "This memory disagrees with another." The engine just records the
    /// edge; callers implement belief propagation on top.
    Contradicts,
    /// "This memory was generated from another." E.g. a summary's source
    /// memory.
    Derived,
    /// "This memory is a fragment of a larger one." For chunked
    /// transcripts.
    PartOf,
    /// "Agent thinks these are related." Distinct from inferred-via-similarity.
    Related,
}

impl LinkKind {
    /// String tag persisted in `link.kind`.
    #[must_use]
    pub const fn as_persisted_str(self) -> &'static str {
        match self {
            LinkKind::Explicit => "explicit",
            LinkKind::Supersedes => "supersedes",
            LinkKind::Contradicts => "contradicts",
            LinkKind::Derived => "derived",
            LinkKind::PartOf => "part_of",
            LinkKind::Related => "related",
        }
    }

    /// Parse from the persisted tag. Unknown tags fall back to
    /// [`LinkKind::Explicit`] so legacy rows always rehydrate (the
    /// pre-Plan-15 schema only persisted the `'explicit'` literal).
    #[must_use]
    pub fn from_persisted(s: &str) -> Self {
        match s {
            "explicit" => LinkKind::Explicit,
            "supersedes" => LinkKind::Supersedes,
            "contradicts" => LinkKind::Contradicts,
            "derived" => LinkKind::Derived,
            "part_of" => LinkKind::PartOf,
            "related" => LinkKind::Related,
            _ => LinkKind::Explicit,
        }
    }
}

/// A directed link.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Link {
    /// Source memory.
    pub src: MemoryId,
    /// Destination memory.
    pub dst: MemoryId,
    /// Kind.
    pub kind: LinkKind,
    /// Created-at unix millis.
    pub created_at_ms: i64,
}

/// Plan 16: a directed edge between two arbitrary nodes (memory,
/// summary, or partition). Returned by the generalized link API
/// ([`crate::Memory::edges_from`] / [`crate::Memory::edges_to`]).
///
/// `Edge` is the `node_link`-shaped sibling of [`Link`]. The legacy
/// memory↔memory [`Link`] type stays for backwards compatibility on the
/// `links_of` read path.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Edge {
    /// Source node.
    pub src: NodeRef,
    /// Destination node.
    pub dst: NodeRef,
    /// Kind.
    pub kind: LinkKind,
    /// Created-at unix millis.
    pub created_at_ms: i64,
    /// Optional caller-supplied note (free-form).
    pub note: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn link_serde_roundtrips() {
        let l = Link {
            src: MemoryId::generate(),
            dst: MemoryId::generate(),
            kind: LinkKind::Explicit,
            created_at_ms: 1_700_000_000_000,
        };
        let j = serde_json::to_string(&l).unwrap();
        let back: Link = serde_json::from_str(&j).unwrap();
        assert_eq!(l, back);
    }

    #[test]
    fn link_kind_persists_round_trip() {
        for k in [
            LinkKind::Explicit,
            LinkKind::Supersedes,
            LinkKind::Contradicts,
            LinkKind::Derived,
            LinkKind::PartOf,
            LinkKind::Related,
        ] {
            let s = k.as_persisted_str();
            assert_eq!(LinkKind::from_persisted(s), k);
        }
        // Unknown rows fall back to Explicit (pre-Plan-15 sentinel).
        assert_eq!(LinkKind::from_persisted("unknown"), LinkKind::Explicit);
    }
}