1use anyhow::{Context, Result};
3use rusqlite::{params_from_iter, Connection, OptionalExtension, Row as SqlRow};
4use serde::Serialize;
5use std::collections::{BTreeSet, HashMap};
6pub const CURRENT_CONFIDENCE_FLOOR: f64 = 0.80;
7const VISIBILITY_BATCH_CHUNK_SIZE: usize = 900;
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "snake_case")]
12pub enum MemoryVisibilityClass {
13 Current,
14 LegacyUnverified,
15 Quarantined,
16 Expired,
17 Superseded,
18 NotYetValid,
19 Inactive,
20}
21impl MemoryVisibilityClass {
22 pub const fn as_str(self) -> &'static str {
23 match self {
24 Self::Current => "current",
25 Self::LegacyUnverified => "legacy_unverified",
26 Self::Quarantined => "quarantined",
27 Self::Expired => "expired",
28 Self::Superseded => "superseded",
29 Self::NotYetValid => "not_yet_valid",
30 Self::Inactive => "inactive",
31 }
32 }
33}
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
35#[serde(rename_all = "snake_case")]
36pub enum MemoryVisibilityReason {
37 CurrentEligible,
38 StatusQuarantined,
39 ValidityExpired,
40 StatusSuperseded,
41 ValidityNotYetStarted,
42 StatusInactive,
43 ProvenanceMissing,
44 ProvenanceMalformed,
45 ConfidenceMissing,
46 ConfidenceBelowFloor,
47 ValidityStartMissing,
48 MutableStateIdentityMissing,
49 RowMissing,
50}
51impl MemoryVisibilityReason {
52 pub const fn as_str(self) -> &'static str {
53 match self {
54 Self::CurrentEligible => "current_eligible",
55 Self::StatusQuarantined => "status_quarantined",
56 Self::ValidityExpired => "validity_expired",
57 Self::StatusSuperseded => "status_superseded",
58 Self::ValidityNotYetStarted => "validity_not_yet_started",
59 Self::StatusInactive => "status_inactive",
60 Self::ProvenanceMissing => "legacy_unverified_provenance_missing",
61 Self::ProvenanceMalformed => "legacy_unverified_provenance_malformed",
62 Self::ConfidenceMissing => "legacy_unverified_confidence_missing",
63 Self::ConfidenceBelowFloor => "legacy_unverified_confidence_below_floor",
64 Self::ValidityStartMissing => "legacy_unverified_validity_start_missing",
65 Self::MutableStateIdentityMissing => "legacy_unverified_mutable_state_identity_missing",
66 Self::RowMissing => "legacy_unverified_row_missing",
67 }
68 }
69}
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
71pub struct MemoryVisibility {
72 pub classification: MemoryVisibilityClass,
73 pub reason: MemoryVisibilityReason,
74 pub current_context_eligible: bool,
75}
76impl MemoryVisibility {
77 fn excluded(classification: MemoryVisibilityClass, reason: MemoryVisibilityReason) -> Self {
78 Self {
79 classification,
80 reason,
81 current_context_eligible: false,
82 }
83 }
84
85 pub fn admitted_by_shadow_mode(&self) -> bool {
89 self.current_context_eligible && self.classification != MemoryVisibilityClass::Current
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum CurrentContextGateMode {
101 Enforce,
103 Shadow,
110}
111
112pub const CURRENT_CONTEXT_GATE_ENV: &str = "REMEM_CURRENT_CONTEXT_GATE";
113
114pub fn current_context_gate_mode() -> CurrentContextGateMode {
118 match std::env::var(CURRENT_CONTEXT_GATE_ENV)
119 .ok()
120 .as_deref()
121 .map(str::trim)
122 {
123 Some("shadow") => CurrentContextGateMode::Shadow,
124 _ => CurrentContextGateMode::Enforce,
125 }
126}
127
128fn apply_gate_mode(visibility: MemoryVisibility) -> MemoryVisibility {
129 if visibility.current_context_eligible
130 || visibility.classification != MemoryVisibilityClass::LegacyUnverified
131 || current_context_gate_mode() != CurrentContextGateMode::Shadow
132 {
133 return visibility;
134 }
135 MemoryVisibility {
136 current_context_eligible: true,
137 ..visibility
138 }
139}
140#[derive(Clone)]
141struct Row {
142 status: String,
143 memory_type: String,
144 topic_key: Option<String>,
145 source_candidate_id: Option<i64>,
146 evidence_event_ids: Option<String>,
147 source_trust_class: Option<String>,
148 confidence: Option<f64>,
149 valid_from_epoch: Option<i64>,
150 valid_to_epoch: Option<i64>,
151 expires_at_epoch: Option<i64>,
152 state_key_id: Option<i64>,
153 lesson_has_proof: bool,
154 candidate_has_proof: bool,
155 direct_evidence_resolves: bool,
156 state_key_resolves: bool,
157}
158const VISIBILITY_PROJECTION_SQL: &str =
159 "SELECT id, status, memory_type, topic_key, source_candidate_id, evidence_event_ids,
160 source_trust_class,
161 COALESCE(confidence, (
162 SELECT candidate.confidence FROM memory_candidates candidate
163 WHERE candidate.id = memories.source_candidate_id
164 AND candidate.review_status IN ('accepted', 'approved', 'auto_promoted')
165 )),
166 COALESCE(valid_from_epoch, (
167 SELECT candidate.created_at_epoch FROM memory_candidates candidate
168 WHERE candidate.id = memories.source_candidate_id
169 AND candidate.review_status IN ('accepted', 'approved', 'auto_promoted')
170 )), valid_to_epoch,
171 expires_at_epoch, state_key_id,
172 EXISTS(
173 SELECT 1 FROM memory_lessons lesson
174 WHERE lesson.memory_id = memories.id
175 AND lesson.confidence >= 0.80
176 AND trim(COALESCE(lesson.source_evidence, '')) <> ''
177 ),
178 EXISTS(
179 SELECT 1 FROM memory_candidates candidate
180 WHERE candidate.id = memories.source_candidate_id
181 AND candidate.review_status IN ('accepted', 'approved', 'auto_promoted')
182 AND CASE WHEN json_valid(candidate.evidence_event_ids) THEN
183 json_array_length(candidate.evidence_event_ids) > 0
184 AND NOT EXISTS (
185 SELECT 1 FROM json_each(candidate.evidence_event_ids) evidence
186 LEFT JOIN captured_events event ON event.id = evidence.value
187 WHERE evidence.type <> 'integer' OR event.id IS NULL
188 )
189 ELSE 0 END
190 ),
191 CASE
192 WHEN evidence_event_ids IS NULL THEN 1
193 WHEN json_valid(evidence_event_ids) THEN
194 json_array_length(evidence_event_ids) > 0
195 AND NOT EXISTS (
196 SELECT 1 FROM json_each(evidence_event_ids) evidence
197 LEFT JOIN captured_events event ON event.id = evidence.value
198 WHERE evidence.type <> 'integer' OR event.id IS NULL
199 )
200 ELSE 0
201 END,
202 EXISTS(SELECT 1 FROM memory_state_keys state_key
203 WHERE state_key.id = memories.state_key_id
204 AND (memories.owner_scope IS NULL
205 OR state_key.owner_scope = memories.owner_scope)
206 AND state_key.owner_key = COALESCE(memories.owner_key, memories.project)
207 AND state_key.memory_type = memories.memory_type)
208 FROM memories WHERE id IN";
209
210fn read_visibility_row(row: &SqlRow<'_>) -> rusqlite::Result<Row> {
211 Ok(Row {
212 status: row.get(1)?,
213 memory_type: row.get(2)?,
214 topic_key: row.get(3)?,
215 source_candidate_id: row.get(4)?,
216 evidence_event_ids: row.get(5)?,
217 source_trust_class: row.get(6)?,
218 confidence: row.get(7)?,
219 valid_from_epoch: row.get(8)?,
220 valid_to_epoch: row.get(9)?,
221 expires_at_epoch: row.get(10)?,
222 state_key_id: row.get(11)?,
223 lesson_has_proof: row.get(12)?,
224 candidate_has_proof: row.get(13)?,
225 direct_evidence_resolves: row.get(14)?,
226 state_key_resolves: row.get(15)?,
227 })
228}
229
230fn missing_row_visibility() -> MemoryVisibility {
231 MemoryVisibility::excluded(
232 MemoryVisibilityClass::LegacyUnverified,
233 MemoryVisibilityReason::RowMissing,
234 )
235}
236
237pub fn classify_memories(
243 conn: &Connection,
244 memory_ids: &[i64],
245 as_of_epoch: i64,
246) -> Result<HashMap<i64, MemoryVisibility>> {
247 let ids = memory_ids.iter().copied().collect::<BTreeSet<_>>();
248 let mut classifications = ids
249 .iter()
250 .map(|id| (*id, missing_row_visibility()))
251 .collect::<HashMap<_, _>>();
252 for chunk in ids
253 .iter()
254 .copied()
255 .collect::<Vec<_>>()
256 .chunks(VISIBILITY_BATCH_CHUNK_SIZE)
257 {
258 let placeholders = std::iter::repeat_n("?", chunk.len())
259 .collect::<Vec<_>>()
260 .join(",");
261 let sql = format!("{VISIBILITY_PROJECTION_SQL} ({placeholders})");
262 let mut statement = conn
263 .prepare(&sql)
264 .context("prepare batch memory trust and visibility classification")?;
265 let rows = statement
266 .query_map(params_from_iter(chunk.iter()), |row| {
267 Ok((row.get::<_, i64>(0)?, read_visibility_row(row)?))
268 })
269 .context("query batch memory trust and visibility classification")?;
270 for row in rows {
271 let (id, visibility_row) =
272 row.context("read batch memory trust and visibility classification row")?;
273 classifications.insert(id, classify_row(&visibility_row, as_of_epoch));
274 }
275 }
276 Ok(classifications)
277}
278
279pub fn classify_memory(
280 conn: &Connection,
281 memory_id: i64,
282 as_of_epoch: i64,
283) -> Result<MemoryVisibility> {
284 Ok(classify_memories(conn, &[memory_id], as_of_epoch)?
285 .remove(&memory_id)
286 .unwrap_or_else(missing_row_visibility))
287}
288
289pub fn admit_for_current_context(
296 conn: &Connection,
297 memory_id: i64,
298 as_of_epoch: i64,
299) -> Result<MemoryVisibility> {
300 Ok(apply_gate_mode(classify_memory(
301 conn,
302 memory_id,
303 as_of_epoch,
304 )?))
305}
306
307pub fn admit_many_for_current_context(
311 conn: &Connection,
312 memory_ids: &[i64],
313 as_of_epoch: i64,
314) -> Result<HashMap<i64, MemoryVisibility>> {
315 Ok(classify_memories(conn, memory_ids, as_of_epoch)?
316 .into_iter()
317 .map(|(id, visibility)| (id, apply_gate_mode(visibility)))
318 .collect())
319}
320
321pub fn admit_for_historical_context(
325 conn: &Connection,
326 memory_id: i64,
327 as_of_epoch: i64,
328) -> Result<MemoryVisibility> {
329 let sql = format!("{VISIBILITY_PROJECTION_SQL} (?1)");
330 let row = conn
331 .query_row(&sql, [memory_id], read_visibility_row)
332 .optional()?
333 .ok_or_else(|| anyhow::anyhow!("memory visibility row missing for id={memory_id}"))?;
334 let mut historical = row.clone();
335 historical.status = "active".to_string();
336 historical.valid_to_epoch = None;
337 historical.expires_at_epoch = None;
338 Ok(apply_gate_mode(classify_row(&historical, as_of_epoch)))
339}
340
341fn classify_row(row: &Row, as_of: i64) -> MemoryVisibility {
342 if row.status == "quarantined" {
343 return MemoryVisibility::excluded(
344 MemoryVisibilityClass::Quarantined,
345 MemoryVisibilityReason::StatusQuarantined,
346 );
347 }
348 if row.valid_to_epoch.is_some_and(|v| v <= as_of)
349 || row.expires_at_epoch.is_some_and(|v| v <= as_of)
350 {
351 return MemoryVisibility::excluded(
352 MemoryVisibilityClass::Expired,
353 MemoryVisibilityReason::ValidityExpired,
354 );
355 }
356 if row.status == "superseded" {
357 return MemoryVisibility::excluded(
358 MemoryVisibilityClass::Superseded,
359 MemoryVisibilityReason::StatusSuperseded,
360 );
361 }
362 if row.valid_from_epoch.is_some_and(|v| v > as_of) {
363 return MemoryVisibility::excluded(
364 MemoryVisibilityClass::NotYetValid,
365 MemoryVisibilityReason::ValidityNotYetStarted,
366 );
367 }
368 if row.status != "active" {
369 return MemoryVisibility::excluded(
370 MemoryVisibilityClass::Inactive,
371 MemoryVisibilityReason::StatusInactive,
372 );
373 }
374 let direct_user = row.source_trust_class.as_deref() == Some("user_prompt");
375 let explicit_writer_proof = direct_user || row.lesson_has_proof;
376 if !explicit_writer_proof {
377 if row.source_candidate_id.is_none() && row.evidence_event_ids.is_none() {
378 return MemoryVisibility::excluded(
379 MemoryVisibilityClass::LegacyUnverified,
380 MemoryVisibilityReason::ProvenanceMissing,
381 );
382 }
383 let valid_evidence = row.evidence_event_ids.as_deref().is_none_or(|raw| {
384 serde_json::from_str::<Vec<i64>>(raw)
385 .ok()
386 .is_some_and(|ids| !ids.is_empty() && ids.into_iter().all(|id| id > 0))
387 });
388 if !valid_evidence {
389 return MemoryVisibility::excluded(
390 MemoryVisibilityClass::LegacyUnverified,
391 MemoryVisibilityReason::ProvenanceMalformed,
392 );
393 }
394 if row.source_candidate_id.is_some() && !row.candidate_has_proof {
395 return MemoryVisibility::excluded(
396 MemoryVisibilityClass::LegacyUnverified,
397 MemoryVisibilityReason::ProvenanceMalformed,
398 );
399 }
400 if row.evidence_event_ids.is_some() && !row.direct_evidence_resolves {
401 return MemoryVisibility::excluded(
402 MemoryVisibilityClass::LegacyUnverified,
403 MemoryVisibilityReason::ProvenanceMalformed,
404 );
405 }
406 let Some(confidence) = row.confidence else {
407 return MemoryVisibility::excluded(
408 MemoryVisibilityClass::LegacyUnverified,
409 MemoryVisibilityReason::ConfidenceMissing,
410 );
411 };
412 if !confidence.is_finite() || confidence < CURRENT_CONFIDENCE_FLOOR {
413 return MemoryVisibility::excluded(
414 MemoryVisibilityClass::LegacyUnverified,
415 MemoryVisibilityReason::ConfidenceBelowFloor,
416 );
417 }
418 if row.valid_from_epoch.is_none() {
419 return MemoryVisibility::excluded(
420 MemoryVisibilityClass::LegacyUnverified,
421 MemoryVisibilityReason::ValidityStartMissing,
422 );
423 }
424 }
425 let mutable = matches!(
426 row.memory_type.as_str(),
427 "decision" | "architecture" | "preference"
428 ) && row
429 .topic_key
430 .as_deref()
431 .is_some_and(|key| !key.trim().is_empty());
432 if !direct_user && mutable && (row.state_key_id.is_none() || !row.state_key_resolves) {
433 return MemoryVisibility::excluded(
434 MemoryVisibilityClass::LegacyUnverified,
435 MemoryVisibilityReason::MutableStateIdentityMissing,
436 );
437 }
438 MemoryVisibility {
439 classification: MemoryVisibilityClass::Current,
440 reason: MemoryVisibilityReason::CurrentEligible,
441 current_context_eligible: true,
442 }
443}
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 struct GateModeEnv {
451 _lock: crate::runtime_config::TestEnvGuard,
452 previous: Option<String>,
453 }
454
455 impl GateModeEnv {
456 fn set(value: Option<&str>) -> Self {
457 let lock = crate::runtime_config::ENV_LOCK
458 .lock()
459 .expect("env lock should acquire");
460 let previous = std::env::var(CURRENT_CONTEXT_GATE_ENV).ok();
461 apply(value);
462 Self {
463 _lock: lock,
464 previous,
465 }
466 }
467 }
468
469 impl Drop for GateModeEnv {
470 fn drop(&mut self) {
471 apply(self.previous.take().as_deref());
472 }
473 }
474
475 fn apply(value: Option<&str>) {
476 match value {
477 Some(value) => unsafe { std::env::set_var(CURRENT_CONTEXT_GATE_ENV, value) },
478 None => unsafe { std::env::remove_var(CURRENT_CONTEXT_GATE_ENV) },
479 }
480 }
481
482 fn row() -> Row {
483 Row {
484 status: "active".into(),
485 memory_type: "bugfix".into(),
486 topic_key: None,
487 source_candidate_id: Some(1),
488 evidence_event_ids: Some("[2]".into()),
489 source_trust_class: Some("local_tool_output".into()),
490 confidence: Some(0.9),
491 valid_from_epoch: Some(10),
492 valid_to_epoch: None,
493 expires_at_epoch: None,
494 state_key_id: None,
495 lesson_has_proof: false,
496 candidate_has_proof: true,
497 direct_evidence_resolves: true,
498 state_key_resolves: true,
499 }
500 }
501 #[test]
502 fn unknown_proof_fails_closed() {
503 let mut value = row();
504 value.confidence = None;
505 assert_eq!(
506 classify_row(&value, 20).reason.as_str(),
507 "legacy_unverified_confidence_missing"
508 );
509 }
510 #[test]
511 fn lifecycle_precedes_proof_and_current_rows_survive() {
512 assert!(classify_row(&row(), 20).current_context_eligible);
513 let mut value = row();
514 value.status = "quarantined".into();
515 value.source_candidate_id = None;
516 assert_eq!(
517 classify_row(&value, 20).classification,
518 MemoryVisibilityClass::Quarantined
519 );
520 }
521
522 #[test]
523 fn lifecycle_collision_order_and_future_validity_are_stable() {
524 let mut value = row();
525 value.status = "superseded".into();
526 value.expires_at_epoch = Some(20);
527 assert_eq!(
528 classify_row(&value, 20).classification,
529 MemoryVisibilityClass::Expired
530 );
531 value.expires_at_epoch = None;
532 value.valid_from_epoch = Some(21);
533 assert_eq!(
534 classify_row(&value, 20).reason,
535 MemoryVisibilityReason::StatusSuperseded
536 );
537 value.status = "active".into();
538 assert_eq!(
539 classify_row(&value, 20).reason,
540 MemoryVisibilityReason::ValidityNotYetStarted
541 );
542 }
543 #[test]
544 fn direct_user_proof_preserves_compatibility() {
545 let mut value = row();
546 value.source_trust_class = Some("user_prompt".into());
547 value.source_candidate_id = None;
548 value.evidence_event_ids = None;
549 value.confidence = None;
550 value.valid_from_epoch = None;
551 assert!(classify_row(&value, 20).current_context_eligible);
552 }
553
554 #[test]
555 fn direct_user_preference_with_writer_state_identity_is_current() {
556 let mut value = row();
557 value.memory_type = "preference".into();
558 value.topic_key = Some("manual-formatting".into());
559 value.state_key_id = None;
560 value.source_trust_class = Some("user_prompt".into());
561 value.source_candidate_id = None;
562 value.evidence_event_ids = None;
563 value.confidence = None;
564 value.valid_from_epoch = None;
565 assert!(classify_row(&value, 20).current_context_eligible);
566 }
567
568 #[test]
569 fn shadow_mode_admits_legacy_unverified_but_never_relaxes_lifecycle() -> Result<()> {
570 let conn = Connection::open_in_memory()?;
571 crate::migrate::run_migrations(&conn)?;
572 conn.execute(
574 "INSERT INTO memories
575 (id, project, title, content, memory_type, created_at_epoch,
576 updated_at_epoch, status, source_trust_class)
577 VALUES (1, '/repo', 'legacy', 'body', 'bugfix',
578 1, 1, 'active', 'local_tool_output')",
579 [],
580 )?;
581 conn.execute(
582 "INSERT INTO memories
583 (id, project, title, content, memory_type, created_at_epoch,
584 updated_at_epoch, status, source_trust_class)
585 VALUES (2, '/repo', 'poisoned', 'body', 'bugfix',
586 1, 1, 'quarantined', 'user_prompt')",
587 [],
588 )?;
589
590 let _guard = GateModeEnv::set(Some("shadow"));
591
592 let legacy = admit_for_current_context(&conn, 1, 2)?;
593 assert!(
594 legacy.current_context_eligible,
595 "shadow mode must admit legacy-unverified rows so injection can be measured"
596 );
597 assert_eq!(
598 legacy.classification,
599 MemoryVisibilityClass::LegacyUnverified,
600 "shadow mode must preserve the real classification for reporting"
601 );
602 assert!(legacy.admitted_by_shadow_mode());
603
604 let quarantined = admit_for_current_context(&conn, 2, 2)?;
605 assert!(
606 !quarantined.current_context_eligible,
607 "shadow mode must not relax the quarantine security boundary"
608 );
609 assert!(!quarantined.admitted_by_shadow_mode());
610
611 assert!(!classify_memory(&conn, 1, 2)?.current_context_eligible);
613
614 let batched = admit_many_for_current_context(&conn, &[1, 2], 2)?;
615 assert!(batched[&1].current_context_eligible);
616 assert!(!batched[&2].current_context_eligible);
617 Ok(())
618 }
619
620 #[test]
621 fn gate_mode_defaults_to_enforce_for_unset_and_unknown_values() {
622 for value in [None, Some("yes-please-disable"), Some("off"), Some("")] {
623 let _guard = GateModeEnv::set(value);
624 assert_eq!(
625 current_context_gate_mode(),
626 CurrentContextGateMode::Enforce,
627 "only the documented 'shadow' value may relax the gate, got {value:?}"
628 );
629 }
630 let _shadow = GateModeEnv::set(Some(" shadow "));
631 assert_eq!(
632 current_context_gate_mode(),
633 CurrentContextGateMode::Shadow,
634 "surrounding whitespace should not defeat the documented value"
635 );
636 }
637
638 #[test]
639 fn batch_classification_matches_single_rows_and_marks_missing() -> Result<()> {
640 let conn = Connection::open_in_memory()?;
641 crate::migrate::run_migrations(&conn)?;
642 conn.execute(
643 "INSERT INTO memories
644 (id, project, title, content, memory_type, created_at_epoch,
645 updated_at_epoch, status, source_trust_class)
646 VALUES (1, '/repo', 'direct user', 'body', 'bugfix',
647 1, 1, 'active', 'user_prompt')",
648 [],
649 )?;
650 conn.execute(
651 "INSERT INTO memories
652 (id, project, title, content, memory_type, created_at_epoch,
653 updated_at_epoch, status, source_trust_class)
654 VALUES (2, '/repo', 'legacy', 'body', 'bugfix',
655 1, 1, 'active', 'local_tool_output')",
656 [],
657 )?;
658
659 let classifications = classify_memories(&conn, &[1, 2, 99, 1], 2)?;
660
661 assert_eq!(classifications.len(), 3);
662 assert_eq!(classifications[&1], classify_memory(&conn, 1, 2)?);
663 assert!(classifications[&1].current_context_eligible);
664 assert_eq!(
665 classifications[&2],
666 classify_memory(&conn, 2, 2)?,
667 "batch and single-row paths must agree on excluded rows"
668 );
669 assert_eq!(
670 classifications[&2].reason,
671 MemoryVisibilityReason::ProvenanceMissing
672 );
673 assert_eq!(
674 classifications[&99].reason,
675 MemoryVisibilityReason::RowMissing
676 );
677 assert!(!classifications[&99].current_context_eligible);
678 Ok(())
679 }
680
681 #[test]
682 fn auto_promoted_candidate_is_valid_writer_proof() -> Result<()> {
683 let conn = Connection::open_in_memory()?;
684 crate::migrate::run_migrations(&conn)?;
685 conn.execute(
686 "INSERT INTO memories
687 (id, project, title, content, memory_type, created_at_epoch,
688 updated_at_epoch, status, source_trust_class)
689 VALUES (1, '/repo', 'automatic capture', 'body', 'bugfix',
690 1, 1, 'active', 'local_tool_output')",
691 [],
692 )?;
693 crate::truth::test_support::seed_current_memory_proof(&conn, 1)?;
694 conn.execute(
695 "UPDATE memory_candidates SET review_status = 'auto_promoted'
696 WHERE id = (SELECT source_candidate_id FROM memories WHERE id = 1)",
697 [],
698 )?;
699 assert!(classify_memory(&conn, 1, 2)?.current_context_eligible);
700 Ok(())
701 }
702
703 #[test]
704 fn dangling_candidate_and_event_references_fail_closed() -> Result<()> {
705 let conn = Connection::open_in_memory()?;
706 crate::migrate::run_migrations(&conn)?;
707 conn.execute(
708 "INSERT INTO memories
709 (id, project, title, content, memory_type, created_at_epoch,
710 updated_at_epoch, status, source_candidate_id, confidence, valid_from_epoch)
711 VALUES (1, '/repo', 'dangling candidate', 'body', 'bugfix',
712 1, 1, 'active', 999, 0.9, 1)",
713 [],
714 )?;
715 conn.execute(
716 "INSERT INTO memories
717 (id, project, title, content, memory_type, created_at_epoch,
718 updated_at_epoch, status, evidence_event_ids, confidence, valid_from_epoch)
719 VALUES (2, '/repo', 'dangling event', 'body', 'bugfix',
720 1, 1, 'active', '[999]', 0.9, 1)",
721 [],
722 )?;
723
724 for id in [1, 2] {
725 let visibility = classify_memory(&conn, id, 2)?;
726 assert_eq!(
727 visibility.reason,
728 MemoryVisibilityReason::ProvenanceMalformed
729 );
730 assert!(!visibility.current_context_eligible);
731 }
732 Ok(())
733 }
734
735 #[test]
736 fn syntactically_malformed_candidate_and_direct_evidence_fail_closed() -> Result<()> {
737 let conn = Connection::open_in_memory()?;
738 crate::migrate::run_migrations(&conn)?;
739 conn.execute(
740 "INSERT INTO memory_candidates
741 (id, scope, memory_type, topic_key, text, evidence_event_ids, confidence,
742 risk_class, review_status, created_at_epoch, updated_at_epoch)
743 VALUES (1, 'project', 'bugfix', 'malformed-proof', 'candidate', 'not-json', 0.9,
744 'low', 'accepted', 1, 1)",
745 [],
746 )?;
747 conn.execute(
748 "INSERT INTO memories
749 (id, project, title, content, memory_type, created_at_epoch,
750 updated_at_epoch, status, source_candidate_id)
751 VALUES (1, '/repo', 'bad candidate evidence', 'body', 'bugfix',
752 1, 1, 'active', 1)",
753 [],
754 )?;
755 conn.execute(
756 "INSERT INTO memories
757 (id, project, title, content, memory_type, created_at_epoch,
758 updated_at_epoch, status, evidence_event_ids, confidence, valid_from_epoch)
759 VALUES (2, '/repo', 'bad direct evidence', 'body', 'bugfix',
760 1, 1, 'active', 'not-json', 0.9, 1)",
761 [],
762 )?;
763
764 for id in [1, 2] {
765 let visibility = classify_memory(&conn, id, 2)?;
766 assert_eq!(
767 visibility.reason,
768 MemoryVisibilityReason::ProvenanceMalformed
769 );
770 assert!(!visibility.current_context_eligible);
771 }
772 Ok(())
773 }
774}