1use crate::invariant::{
2 duplicate_active_ids_for, duplicate_active_ids_for_incoming, log_auto_deprecation,
3};
4use crate::lifecycle::validate_transition;
5use crate::traits::{StageStore, StoreError, StoreStats};
6use noether_core::stage::{Stage, StageId, StageLifecycle};
7use std::collections::{BTreeMap, HashMap};
8
9#[derive(Debug, Default)]
11pub struct MemoryStore {
12 stages: HashMap<String, Stage>,
13}
14
15impl MemoryStore {
16 pub fn new() -> Self {
17 Self::default()
18 }
19
20 pub fn len(&self) -> usize {
21 self.stages.len()
22 }
23
24 pub fn is_empty(&self) -> bool {
25 self.stages.is_empty()
26 }
27
28 #[doc(hidden)]
34 pub fn inject_raw_for_testing(&mut self, stage: Stage) {
35 self.stages.insert(stage.id.0.clone(), stage);
36 }
37}
38
39impl StageStore for MemoryStore {
40 fn put(&mut self, stage: Stage) -> Result<StageId, StoreError> {
41 let id = stage.id.clone();
42 if self.stages.contains_key(&id.0) {
43 return Err(StoreError::AlreadyExists(id));
44 }
45
46 let duplicates = duplicate_active_ids_for_incoming(&self.stages, &stage);
54 let signature_id = stage.signature_id.clone();
55
56 self.stages.insert(id.0.clone(), stage);
57
58 for old_id in &duplicates {
63 if let Some(existing) = self.stages.get_mut(&old_id.0) {
64 existing.lifecycle = StageLifecycle::Deprecated {
65 successor_id: id.clone(),
66 };
67 }
68 }
69 log_auto_deprecation(&duplicates, &id, signature_id.as_ref());
70
71 Ok(id)
72 }
73
74 fn upsert(&mut self, stage: Stage) -> Result<StageId, StoreError> {
75 let id = stage.id.clone();
76 let duplicates = duplicate_active_ids_for_incoming(&self.stages, &stage);
77 let signature_id = stage.signature_id.clone();
78 self.stages.insert(id.0.clone(), stage);
79 let actually_deprecated: Vec<StageId> = duplicates
80 .into_iter()
81 .filter(|old_id| *old_id != id)
82 .collect();
83 for old_id in &actually_deprecated {
84 if let Some(existing) = self.stages.get_mut(&old_id.0) {
85 existing.lifecycle = StageLifecycle::Deprecated {
86 successor_id: id.clone(),
87 };
88 }
89 }
90 log_auto_deprecation(&actually_deprecated, &id, signature_id.as_ref());
91 Ok(id)
92 }
93
94 fn remove(&mut self, id: &StageId) -> Result<(), StoreError> {
95 self.stages.remove(&id.0);
96 Ok(())
97 }
98
99 fn get(&self, id: &StageId) -> Result<Option<&Stage>, StoreError> {
100 Ok(self.stages.get(&id.0))
101 }
102
103 fn contains(&self, id: &StageId) -> bool {
104 self.stages.contains_key(&id.0)
105 }
106
107 fn list(&self, lifecycle: Option<&StageLifecycle>) -> Vec<&Stage> {
108 self.stages
109 .values()
110 .filter(|s| lifecycle.is_none() || lifecycle == Some(&s.lifecycle))
111 .collect()
112 }
113
114 fn update_lifecycle(
115 &mut self,
116 id: &StageId,
117 lifecycle: StageLifecycle,
118 ) -> Result<(), StoreError> {
119 let current = self
121 .stages
122 .get(&id.0)
123 .ok_or_else(|| StoreError::NotFound(id.clone()))?;
124
125 validate_transition(¤t.lifecycle, &lifecycle)
126 .map_err(|reason| StoreError::InvalidTransition { reason })?;
127
128 if let StageLifecycle::Deprecated { ref successor_id } = lifecycle {
129 if !self.stages.contains_key(&successor_id.0) {
130 return Err(StoreError::InvalidSuccessor {
131 reason: format!("successor {successor_id:?} not found in store"),
132 });
133 }
134 }
135
136 let (duplicates, signature_id) = if matches!(lifecycle, StageLifecycle::Active) {
140 let sig = current.signature_id.clone();
141 (
142 duplicate_active_ids_for(&self.stages, id, sig.as_ref()),
143 sig,
144 )
145 } else {
146 (Vec::new(), None)
147 };
148
149 self.stages.get_mut(&id.0).unwrap().lifecycle = lifecycle;
151 for old_id in &duplicates {
152 if let Some(existing) = self.stages.get_mut(&old_id.0) {
153 existing.lifecycle = StageLifecycle::Deprecated {
154 successor_id: id.clone(),
155 };
156 }
157 }
158 log_auto_deprecation(&duplicates, id, signature_id.as_ref());
159 Ok(())
160 }
161
162 fn stats(&self) -> StoreStats {
163 let mut by_lifecycle: BTreeMap<String, usize> = BTreeMap::new();
164 let mut by_effect: BTreeMap<String, usize> = BTreeMap::new();
165
166 for stage in self.stages.values() {
167 let lc_name = match &stage.lifecycle {
168 StageLifecycle::Draft => "draft",
169 StageLifecycle::Active => "active",
170 StageLifecycle::Deprecated { .. } => "deprecated",
171 StageLifecycle::Tombstone => "tombstone",
172 };
173 *by_lifecycle.entry(lc_name.into()).or_default() += 1;
174
175 for effect in stage.signature.effects.iter() {
176 let effect_name = format!("{effect:?}");
177 *by_effect.entry(effect_name).or_default() += 1;
178 }
179 }
180
181 StoreStats {
182 total: self.stages.len(),
183 by_lifecycle,
184 by_effect,
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use noether_core::effects::EffectSet;
193 use noether_core::stage::{CostEstimate, StageSignature};
194 use noether_core::types::NType;
195 use std::collections::BTreeSet;
196
197 fn make_stage(id: &str) -> Stage {
198 Stage {
199 id: StageId(id.into()),
200 signature_id: None,
201 signature: StageSignature {
202 input: NType::Text,
203 output: NType::Number,
204 effects: EffectSet::pure(),
205 implementation_hash: format!("impl_{id}"),
206 },
207 capabilities: BTreeSet::new(),
208 cost: CostEstimate {
209 time_ms_p50: None,
210 tokens_est: None,
211 memory_mb: None,
212 },
213 description: "test stage".into(),
214 examples: vec![],
215 lifecycle: StageLifecycle::Active,
216 ed25519_signature: None,
217 signer_public_key: None,
218 implementation_code: None,
219 implementation_language: None,
220 ui_style: None,
221 tags: vec![],
222 aliases: vec![],
223 name: None,
224 properties: Vec::new(),
225 }
226 }
227
228 #[test]
229 fn put_and_get() {
230 let mut store = MemoryStore::new();
231 let stage = make_stage("abc123");
232 store.put(stage.clone()).unwrap();
233 let retrieved = store.get(&StageId("abc123".into())).unwrap().unwrap();
234 assert_eq!(retrieved.id, stage.id);
235 }
236
237 #[test]
238 fn duplicate_put_fails() {
239 let mut store = MemoryStore::new();
240 store.put(make_stage("abc123")).unwrap();
241 assert!(store.put(make_stage("abc123")).is_err());
242 }
243
244 #[test]
245 fn valid_lifecycle_transition() {
246 let mut store = MemoryStore::new();
247 let mut draft = make_stage("abc123");
248 draft.lifecycle = StageLifecycle::Draft;
249 store.put(draft).unwrap();
250 store
251 .update_lifecycle(&StageId("abc123".into()), StageLifecycle::Active)
252 .unwrap();
253 let stage = store.get(&StageId("abc123".into())).unwrap().unwrap();
254 assert_eq!(stage.lifecycle, StageLifecycle::Active);
255 }
256
257 #[test]
258 fn invalid_lifecycle_transition_fails() {
259 let mut store = MemoryStore::new();
260 let mut draft = make_stage("abc123");
261 draft.lifecycle = StageLifecycle::Draft;
262 store.put(draft).unwrap();
263 let result = store.update_lifecycle(&StageId("abc123".into()), StageLifecycle::Tombstone);
265 assert!(result.is_err());
266 }
267
268 #[test]
269 fn deprecation_requires_valid_successor() {
270 let mut store = MemoryStore::new();
271 store.put(make_stage("old")).unwrap();
272 let result = store.update_lifecycle(
274 &StageId("old".into()),
275 StageLifecycle::Deprecated {
276 successor_id: StageId("nonexistent".into()),
277 },
278 );
279 assert!(result.is_err());
280
281 store.put(make_stage("new")).unwrap();
283 store
284 .update_lifecycle(
285 &StageId("old".into()),
286 StageLifecycle::Deprecated {
287 successor_id: StageId("new".into()),
288 },
289 )
290 .unwrap();
291 }
292
293 #[test]
294 fn get_by_signature_returns_active_impl() {
295 use noether_core::stage::SignatureId;
296 let mut store = MemoryStore::new();
297 let mut stage = make_stage("impl_a");
298 stage.signature_id = Some(SignatureId("sig_one".into()));
299 store.put(stage).unwrap();
300
301 let found = store.get_by_signature(&SignatureId("sig_one".into()));
302 assert!(found.is_some(), "stage pinned by signature should resolve");
303 assert_eq!(found.unwrap().id, StageId("impl_a".into()));
304
305 assert!(store
306 .get_by_signature(&SignatureId("sig_missing".into()))
307 .is_none());
308 }
309
310 #[test]
311 fn get_by_signature_skips_deprecated() {
312 use noether_core::stage::SignatureId;
313 let mut store = MemoryStore::new();
314 let mut old = make_stage("impl_old");
318 old.signature_id = Some(SignatureId("sig".into()));
319 store.put(old).unwrap();
320 let mut new = make_stage("impl_new");
321 new.signature_id = Some(SignatureId("sig".into()));
322 store.put(new).unwrap();
323
324 assert!(matches!(
327 store
328 .get(&StageId("impl_old".into()))
329 .unwrap()
330 .unwrap()
331 .lifecycle,
332 StageLifecycle::Deprecated { .. }
333 ));
334
335 let found = store.get_by_signature(&SignatureId("sig".into())).unwrap();
336 assert_eq!(found.id, StageId("impl_new".into()));
337 }
338
339 #[test]
340 fn put_enforces_one_active_per_signature() {
341 use noether_core::stage::SignatureId;
342 let mut store = MemoryStore::new();
343 let mut a = make_stage("impl_a");
344 a.signature_id = Some(SignatureId("sig".into()));
345 let mut b = make_stage("impl_b");
346 b.signature_id = Some(SignatureId("sig".into()));
347
348 store.put(a).unwrap();
349 store.put(b).unwrap();
350
351 let stored_a = store.get(&StageId("impl_a".into())).unwrap().unwrap();
353 match &stored_a.lifecycle {
354 StageLifecycle::Deprecated { successor_id } => {
355 assert_eq!(successor_id.0, "impl_b");
356 }
357 other => panic!("expected Deprecated, got {other:?}"),
358 }
359
360 let stored_b = store.get(&StageId("impl_b".into())).unwrap().unwrap();
362 assert!(matches!(stored_b.lifecycle, StageLifecycle::Active));
363 }
364
365 #[test]
366 fn put_draft_does_not_trigger_deprecation() {
367 use noether_core::stage::SignatureId;
370 let mut store = MemoryStore::new();
371 let mut active = make_stage("impl_active");
372 active.signature_id = Some(SignatureId("sig".into()));
373 let mut draft = make_stage("impl_draft");
374 draft.signature_id = Some(SignatureId("sig".into()));
375 draft.lifecycle = StageLifecycle::Draft;
376
377 store.put(active).unwrap();
378 store.put(draft).unwrap();
379
380 let stored = store.get(&StageId("impl_active".into())).unwrap().unwrap();
381 assert!(
382 matches!(stored.lifecycle, StageLifecycle::Active),
383 "draft put must not deprecate existing Active"
384 );
385 }
386
387 #[test]
388 fn update_lifecycle_to_active_deprecates_existing() {
389 use noether_core::stage::SignatureId;
392 let mut store = MemoryStore::new();
393 let mut existing = make_stage("impl_existing");
394 existing.signature_id = Some(SignatureId("sig".into()));
395 let mut draft = make_stage("impl_new");
396 draft.signature_id = Some(SignatureId("sig".into()));
397 draft.lifecycle = StageLifecycle::Draft;
398
399 store.put(existing).unwrap();
400 store.put(draft).unwrap();
401
402 store
404 .update_lifecycle(&StageId("impl_new".into()), StageLifecycle::Active)
405 .unwrap();
406
407 let stored_existing = store
408 .get(&StageId("impl_existing".into()))
409 .unwrap()
410 .unwrap();
411 assert!(
412 matches!(stored_existing.lifecycle, StageLifecycle::Deprecated { .. }),
413 "existing Active must be auto-deprecated when another stage \
414 with the same signature is promoted to Active"
415 );
416 }
417
418 #[test]
419 fn upsert_enforces_one_active_per_signature() {
420 use noether_core::stage::SignatureId;
424 let mut store = MemoryStore::new();
425 let mut a = make_stage("impl_a");
426 a.signature_id = Some(SignatureId("sig".into()));
427 let mut b = make_stage("impl_b");
428 b.signature_id = Some(SignatureId("sig".into()));
429
430 store.upsert(a).unwrap();
431 store.upsert(b).unwrap();
432
433 let stored_a = store.get(&StageId("impl_a".into())).unwrap().unwrap();
434 match &stored_a.lifecycle {
435 StageLifecycle::Deprecated { successor_id } => {
436 assert_eq!(successor_id.0, "impl_b");
437 }
438 other => panic!("expected Deprecated, got {other:?}"),
439 }
440
441 let stored_b = store.get(&StageId("impl_b".into())).unwrap().unwrap();
442 assert!(matches!(stored_b.lifecycle, StageLifecycle::Active));
443 }
444
445 #[test]
446 fn upsert_replacing_self_does_not_deprecate_self() {
447 use noether_core::stage::SignatureId;
451 let mut store = MemoryStore::new();
452 let mut stage = make_stage("impl");
453 stage.signature_id = Some(SignatureId("sig".into()));
454 store.upsert(stage.clone()).unwrap();
455 store.upsert(stage).unwrap();
456
457 let stored = store.get(&StageId("impl".into())).unwrap().unwrap();
458 assert!(
459 matches!(stored.lifecycle, StageLifecycle::Active),
460 "self-upsert must not auto-deprecate"
461 );
462 }
463
464 #[test]
465 fn list_filters_by_lifecycle() {
466 let mut store = MemoryStore::new();
467 store.put(make_stage("a")).unwrap();
468 let mut draft = make_stage("b");
469 draft.lifecycle = StageLifecycle::Draft;
470 store.put(draft).unwrap();
471
472 let active = store.list(Some(&StageLifecycle::Active));
473 assert_eq!(active.len(), 1);
474 let all = store.list(None);
475 assert_eq!(all.len(), 2);
476 }
477
478 #[test]
479 fn stats_returns_counts() {
480 let mut store = MemoryStore::new();
481 store.put(make_stage("a")).unwrap();
482 store.put(make_stage("b")).unwrap();
483 let mut draft = make_stage("c");
484 draft.lifecycle = StageLifecycle::Draft;
485 store.put(draft).unwrap();
486
487 let stats = store.stats();
488 assert_eq!(stats.total, 3);
489 assert_eq!(stats.by_lifecycle.get("active"), Some(&2));
490 assert_eq!(stats.by_lifecycle.get("draft"), Some(&1));
491 }
492}