1use crate::candidates::{discover_candidates, simhash_similarity};
4use crate::edges::{build_edges, EdgeBuildParams};
5use crate::keys::generate_keys;
6use hippmem_core::config::AlgoParams;
7use hippmem_core::ids::MemoryId;
8use hippmem_core::model::links::{ActivationState, AssociationLink, SemanticSignature};
9use hippmem_core::model::understanding::MemoryUnderstanding;
10use hippmem_core::model::unit::{
11 MemoryContent, MemoryLifecycle, MemoryStage, MemoryUnit, WriteContext,
12};
13use hippmem_core::score::UnitScore;
14
15pub struct StagedWriteInput {
16 pub id: MemoryId,
17 pub content: MemoryContent,
18 pub understanding: MemoryUnderstanding,
19 pub context: WriteContext,
20 pub semantic: SemanticSignature,
21}
22
23pub struct StagedWriteOutput {
24 pub unit: MemoryUnit,
25 pub created_links: Vec<AssociationLink>,
26}
27
28pub fn raw_to_indexed(
30 input: StagedWriteInput,
31 existing_units: &[MemoryUnit],
32 edge_params: &EdgeBuildParams,
33 algo_params: &AlgoParams,
34) -> Result<StagedWriteOutput, String> {
35 let now = input.context.local_time;
36 let keys = generate_keys(
37 &input.content,
38 &input.understanding,
39 &input.context,
40 &input.semantic,
41 )?;
42
43 let mut candidates: Vec<(&MemoryUnit, f32)> = existing_units
46 .iter()
47 .map(|unit| {
48 let sim = simhash_similarity(
50 &keys.lexical_signature.simhash,
51 &unit.association_keys.lexical_signature.simhash,
52 );
53 (unit, sim)
54 })
55 .collect();
56
57 candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
59 if edge_params.max_candidates > 0 {
60 candidates.truncate(edge_params.max_candidates);
61 }
62
63 let total_memories = (existing_units.len() + 1) as u32;
64 let mut all_links = Vec::new();
65 for (existing, _sim) in &candidates {
66 let mut cand = discover_candidates(&keys, &existing.association_keys);
67 cand.importance_value = existing.understanding.importance.value();
70 cand.co_context_score = context_shared_ratio(&input.context, &existing.context);
72 let result = build_edges(
73 input.id,
74 existing.id,
75 &cand,
76 cand.matched_dimensions.len(),
77 edge_params,
78 algo_params,
79 &existing.links,
80 now,
81 total_memories,
82 );
83 all_links.extend(result.created_links);
84 }
85
86 let unit = MemoryUnit {
87 schema_version: 1,
88 id: input.id,
89 created_at: now,
90 updated_at: now,
91 content: input.content,
92 context: input.context,
93 understanding: input.understanding,
94 association_keys: keys,
95 links: all_links.clone(),
96 activation: ActivationState {
97 last_retrieved_at: None,
98 retrieval_count: 0,
99 co_activations: vec![],
100 usage_score: UnitScore::new(0.5),
101 },
102 lifecycle: MemoryLifecycle::Active,
103 provenance: hippmem_core::model::unit::Provenance {
104 origin: hippmem_core::model::unit::SourceKind::Conversation,
105 generated_by: hippmem_core::model::unit::GeneratedBy::UserDirect,
106 reliability: UnitScore::new(0.5),
107 evidence_refs: vec![],
108 revision_history: vec![],
109 },
110 stage: MemoryStage::Indexed,
111 };
112
113 Ok(StagedWriteOutput {
114 unit,
115 created_links: all_links,
116 })
117}
118
119fn context_shared_ratio(a: &WriteContext, b: &WriteContext) -> f32 {
123 let fields = [
124 (a.conversation_id, b.conversation_id),
125 (a.session_id, b.session_id),
126 (a.project_id, b.project_id),
127 (a.task_id, b.task_id),
128 ];
129 let total: usize = fields
130 .iter()
131 .filter(|(fa, fb)| fa.is_some() || fb.is_some())
132 .count();
133 if total == 0 {
134 return 0.0;
135 }
136 let shared = fields
137 .iter()
138 .filter(|(fa, fb)| fa.is_some() && fa == fb)
139 .count();
140 shared as f32 / total as f32
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use hippmem_core::model::understanding::{EntityMention, EntityType};
147 use hippmem_core::model::unit::{ContentType, Language};
148 use hippmem_core::time::Timestamp;
149
150 fn make_input(id: u128, text: &str, entity: &str) -> StagedWriteInput {
151 StagedWriteInput {
152 id: MemoryId(id),
153 content: MemoryContent {
154 raw: text.into(),
155 summary: None,
156 normalized: None,
157 language: Language::Zh,
158 content_type: ContentType::UserStatement,
159 },
160 understanding: MemoryUnderstanding {
161 entities: vec![EntityMention {
162 text: entity.into(),
163 canonical: entity.to_lowercase(),
164 entity_type: EntityType::Other,
165 span: None,
166 confidence: UnitScore::new(0.8),
167 }],
168 events: vec![],
169 goals: vec![],
170 decisions: vec![],
171 preferences: vec![],
172 emotions: vec![],
173 causal_claims: vec![],
174 contradictions: vec![],
175 topics: vec![],
176 importance: UnitScore::new(0.5),
177 confidence: UnitScore::new(0.5),
178 },
179 context: WriteContext {
180 conversation_id: Some(1),
181 session_id: Some(1),
182 project_id: None,
183 task_id: None,
184 user_id: None,
185 local_time: Timestamp(1_700_000_000_000),
186 preceding_memory_ids: vec![],
187 source_refs: vec![],
188 },
189 semantic: SemanticSignature {
190 lexical_simhash: [1, 2, 3, 4],
191 dense_embedding_ref: None,
192 binary_code: [0, 0],
193 topic_minhash: [0u32; 16],
194 },
195 }
196 }
197
198 #[test]
199 fn raw_to_indexed_succeeds() {
200 let input = make_input(1, "Rust", "Rust");
201 let output = raw_to_indexed(
202 input,
203 &[],
204 &EdgeBuildParams::default(),
205 &AlgoParams::default(),
206 )
207 .unwrap();
208 assert_eq!(output.unit.stage, MemoryStage::Indexed);
209 }
210
211 #[test]
212 fn shared_entity_produces_links() {
213 let first = raw_to_indexed(
214 make_input(1, "Rust", "Rust"),
215 &[],
216 &EdgeBuildParams::default(),
217 &AlgoParams::default(),
218 )
219 .unwrap();
220 let second = raw_to_indexed(
221 make_input(2, "I also use Rust for systems programming", "Rust"),
222 &[first.unit],
223 &EdgeBuildParams::default(),
224 &AlgoParams::default(),
225 )
226 .unwrap();
227 assert!(!second.created_links.is_empty());
228 }
229}