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