use std::collections::{BTreeMap, BTreeSet};
use crate::models::StoredMemoryAnchor;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum AnchorProjectionRelation {
MentionsSurface,
AnchorProximity,
}
impl AnchorProjectionRelation {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::MentionsSurface => "mentions_surface",
Self::AnchorProximity => "anchor_proximity",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct AnchorProjectionEdge {
pub source: String,
pub target: String,
pub relation: AnchorProjectionRelation,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AnchorProjection {
pub memory_nodes: Vec<String>,
pub anchor_nodes: Vec<String>,
pub edges: Vec<AnchorProjectionEdge>,
}
#[must_use]
pub fn anchor_surface_key(anchor: &StoredMemoryAnchor) -> String {
format!(
"{}:{}",
anchor.anchor_kind.as_str(),
anchor.anchor_value_hash
)
}
#[must_use]
pub fn project_memory_anchor_graph(anchors: &[StoredMemoryAnchor]) -> AnchorProjection {
let mut memory_nodes: BTreeSet<String> = BTreeSet::new();
let mut anchor_nodes: BTreeSet<String> = BTreeSet::new();
let mut edges: BTreeSet<AnchorProjectionEdge> = BTreeSet::new();
let mut surfaces_by_memory: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for anchor in anchors {
let memory = anchor.memory_id.clone();
let surface = anchor_surface_key(anchor);
memory_nodes.insert(memory.clone());
anchor_nodes.insert(surface.clone());
edges.insert(AnchorProjectionEdge {
source: memory.clone(),
target: surface.clone(),
relation: AnchorProjectionRelation::MentionsSurface,
});
surfaces_by_memory
.entry(memory)
.or_default()
.insert(surface);
}
for surfaces in surfaces_by_memory.values() {
let ordered: Vec<&String> = surfaces.iter().collect();
for (index, left) in ordered.iter().enumerate() {
for right in ordered.iter().skip(index + 1) {
edges.insert(AnchorProjectionEdge {
source: (*left).clone(),
target: (*right).clone(),
relation: AnchorProjectionRelation::AnchorProximity,
});
}
}
}
AnchorProjection {
memory_nodes: memory_nodes.into_iter().collect(),
anchor_nodes: anchor_nodes.into_iter().collect(),
edges: edges.into_iter().collect(),
}
}
#[cfg(test)]
mod tests {
use super::{AnchorProjectionRelation, project_memory_anchor_graph};
use crate::models::{
MemoryAnchorFreshnessState, MemoryAnchorKind, MemoryAnchorSource, StoredMemoryAnchor,
};
fn anchor(memory: &str, kind: MemoryAnchorKind, hash: &str) -> StoredMemoryAnchor {
StoredMemoryAnchor {
memory_id: memory.to_string(),
anchor_kind: kind,
anchor_value_hash: hash.to_string(),
redacted_anchor_value: "redacted".to_string(),
confidence: 1.0,
source: MemoryAnchorSource::Explicit,
provenance: "test".to_string(),
captured_span_hash: "blake3:span".to_string(),
freshness_state: MemoryAnchorFreshnessState::Current,
generation: 0,
created_at: "2026-06-07T00:00:00Z".to_string(),
updated_at: "2026-06-07T00:00:00Z".to_string(),
}
}
#[test]
fn projection_links_memories_to_shared_surface() {
let anchors = vec![
anchor("mem_a", MemoryAnchorKind::Path, "blake3:fileX"),
anchor("mem_b", MemoryAnchorKind::Path, "blake3:fileX"),
];
let projection = project_memory_anchor_graph(&anchors);
assert_eq!(
projection.memory_nodes,
vec!["mem_a".to_string(), "mem_b".to_string()]
);
assert_eq!(
projection.anchor_nodes,
vec!["path:blake3:fileX".to_string()]
);
assert_eq!(projection.edges.len(), 2);
assert!(
projection
.edges
.iter()
.all(|edge| edge.relation == AnchorProjectionRelation::MentionsSurface)
);
}
#[test]
fn projection_emits_proximity_for_co_mentioned_surfaces() {
let anchors = vec![
anchor("mem_a", MemoryAnchorKind::Path, "blake3:fileX"),
anchor("mem_a", MemoryAnchorKind::Symbol, "blake3:funcY"),
];
let projection = project_memory_anchor_graph(&anchors);
let proximity: Vec<_> = projection
.edges
.iter()
.filter(|edge| edge.relation == AnchorProjectionRelation::AnchorProximity)
.collect();
assert_eq!(proximity.len(), 1);
assert_eq!(proximity[0].source, "path:blake3:fileX");
assert_eq!(proximity[0].target, "symbol:blake3:funcY");
}
#[test]
fn projection_is_deterministic_and_deduped() {
let forward = vec![
anchor("mem_a", MemoryAnchorKind::Path, "blake3:f1"),
anchor("mem_a", MemoryAnchorKind::Symbol, "blake3:s1"),
anchor("mem_a", MemoryAnchorKind::Path, "blake3:f1"),
];
let mut reversed = forward.clone();
reversed.reverse();
let first = project_memory_anchor_graph(&forward);
let second = project_memory_anchor_graph(&reversed);
assert_eq!(first, second, "projection is independent of input order");
assert_eq!(first.anchor_nodes.len(), 2);
let mentions = first
.edges
.iter()
.filter(|edge| edge.relation == AnchorProjectionRelation::MentionsSurface)
.count();
assert_eq!(mentions, 2);
}
}