1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// 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);
}
}