Skip to main content

semantic_memory/
knowledge.rs

1//! Fact CRUD with FTS5 synchronization.
2//!
3//! Every fact operation that touches `facts_fts` is transactional.
4
5use crate::db;
6use crate::db::{bytes_to_embedding, parse_optional_json, with_transaction};
7#[cfg(feature = "hnsw")]
8use crate::db::{enqueue_pending_index_op, PendingIndexOpKind};
9#[cfg(feature = "hnsw")]
10use crate::episodes;
11use crate::error::MemoryError;
12use crate::quantize::{self, Quantizer};
13use crate::types::{Fact, NamespaceDeleteReport};
14use crate::{merge_trace_ctx, MemoryStore};
15use rusqlite::{params, Connection};
16use stack_ids::TraceCtx;
17
18/// Insert a fact and its FTS entry in a transaction.
19#[allow(dead_code)]
20pub fn insert_fact_with_fts(
21    conn: &Connection,
22    fact_id: &str,
23    namespace: &str,
24    content: &str,
25    embedding_bytes: &[u8],
26    source: Option<&str>,
27    metadata: Option<&serde_json::Value>,
28) -> Result<(), MemoryError> {
29    insert_fact_with_fts_q8(
30        conn,
31        fact_id,
32        namespace,
33        content,
34        embedding_bytes,
35        None,
36        source,
37        metadata,
38        None,
39    )
40}
41
42/// Insert a fact with both f32 and quantized embeddings.
43#[allow(clippy::too_many_arguments)]
44pub fn insert_fact_with_fts_q8(
45    conn: &Connection,
46    fact_id: &str,
47    namespace: &str,
48    content: &str,
49    embedding_bytes: &[u8],
50    q8_bytes: Option<&[u8]>,
51    source: Option<&str>,
52    metadata: Option<&serde_json::Value>,
53    sparse: Option<(&crate::SparseWeights, &str)>,
54) -> Result<(), MemoryError> {
55    let metadata_str = metadata.map(|m| m.to_string());
56    with_transaction(conn, |tx| {
57        tx.execute(
58            "INSERT INTO facts (id, namespace, content, source, embedding, embedding_q8, metadata)
59             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
60            params![
61                fact_id,
62                namespace,
63                content,
64                source,
65                embedding_bytes,
66                q8_bytes,
67                metadata_str
68            ],
69        )?;
70
71        tx.execute(
72            "INSERT INTO facts_rowid_map (fact_id) VALUES (?1)",
73            params![fact_id],
74        )?;
75        let fts_rowid = tx.last_insert_rowid();
76
77        tx.execute(
78            "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
79            params![fts_rowid, content],
80        )?;
81
82        #[cfg(feature = "hnsw")]
83        enqueue_pending_index_op(
84            tx,
85            &format!("fact:{}", fact_id),
86            "fact",
87            PendingIndexOpKind::Upsert,
88        )?;
89        db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
90        if let Some((weights, representation)) = sparse {
91            db::store_sparse_vector(tx, &format!("fact:{fact_id}"), weights, representation)?;
92        }
93
94        let changed = tx.execute(
95            "UPDATE authority_state SET retrieval_epoch = retrieval_epoch + 1 WHERE id = 1",
96            [],
97        )?;
98        if changed != 1 {
99            return Err(MemoryError::Other(
100                "failed to advance corpus authority epoch after fact insert".to_string(),
101            ));
102        }
103
104        Ok(())
105    })
106}
107
108/// Insert a fact within an existing transaction (no nested transaction).
109///
110/// Used by the import boundary where the outer transaction is already active.
111#[allow(clippy::too_many_arguments)]
112pub fn insert_fact_in_tx(
113    tx: &rusqlite::Transaction<'_>,
114    fact_id: &str,
115    namespace: &str,
116    content: &str,
117    embedding_bytes: &[u8],
118    q8_bytes: Option<&[u8]>,
119    source: Option<&str>,
120    metadata: Option<&serde_json::Value>,
121) -> Result<(), MemoryError> {
122    let metadata_str = metadata.map(|m| m.to_string());
123    tx.execute(
124        "INSERT INTO facts (id, namespace, content, source, embedding, embedding_q8, metadata)
125         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
126        params![
127            fact_id,
128            namespace,
129            content,
130            source,
131            embedding_bytes,
132            q8_bytes,
133            metadata_str
134        ],
135    )?;
136
137    tx.execute(
138        "INSERT INTO facts_rowid_map (fact_id) VALUES (?1)",
139        params![fact_id],
140    )?;
141    let fts_rowid = tx.last_insert_rowid();
142
143    tx.execute(
144        "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
145        params![fts_rowid, content],
146    )?;
147
148    #[cfg(feature = "hnsw")]
149    enqueue_pending_index_op(
150        tx,
151        &format!("fact:{}", fact_id),
152        "fact",
153        PendingIndexOpKind::Upsert,
154    )?;
155    db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
156
157    Ok(())
158}
159
160/// Delete a fact and its FTS entry in a transaction.
161#[allow(dead_code)] // public API — used by external consumers, not internally
162pub fn delete_fact_with_fts(conn: &Connection, fact_id: &str) -> Result<(), MemoryError> {
163    with_transaction(conn, |tx| {
164        let fts_rowid: i64 = tx
165            .query_row(
166                "SELECT rowid FROM facts_rowid_map WHERE fact_id = ?1",
167                params![fact_id],
168                |row| row.get(0),
169            )
170            .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
171
172        let content: String = tx
173            .query_row(
174                "SELECT content FROM facts WHERE id = ?1",
175                params![fact_id],
176                |row| row.get(0),
177            )
178            .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
179
180        tx.execute(
181            "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
182            params![fts_rowid, content],
183        )?;
184        tx.execute(
185            "DELETE FROM facts_rowid_map WHERE fact_id = ?1",
186            params![fact_id],
187        )?;
188        tx.execute(
189            "DELETE FROM episode_causes WHERE cause_node_id IN (?1, ?2)",
190            params![fact_id, format!("fact:{fact_id}")],
191        )?;
192        tx.execute(
193            "DELETE FROM derivation_edges
194             WHERE (source_kind = 'fact' AND source_id = ?1)
195                OR (target_kind = 'fact' AND target_id = ?1)",
196            params![fact_id],
197        )?;
198        tx.execute("DELETE FROM facts WHERE id = ?1", params![fact_id])?;
199
200        #[cfg(feature = "hnsw")]
201        enqueue_pending_index_op(
202            tx,
203            &format!("fact:{}", fact_id),
204            "fact",
205            PendingIndexOpKind::Delete,
206        )?;
207        db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
208
209        Ok(())
210    })
211}
212
213/// Update a fact's content and embeddings, with FTS synchronization.
214#[allow(dead_code)] // public API — used by external consumers, not internally
215pub fn update_fact_with_fts(
216    conn: &Connection,
217    fact_id: &str,
218    new_content: &str,
219    new_embedding_bytes: &[u8],
220    new_q8_bytes: Option<&[u8]>,
221) -> Result<(), MemoryError> {
222    with_transaction(conn, |tx| {
223        let (fts_rowid, old_content): (i64, String) = tx
224            .query_row(
225                "SELECT fm.rowid, f.content
226                 FROM facts f
227                 JOIN facts_rowid_map fm ON fm.fact_id = f.id
228                 WHERE f.id = ?1",
229                params![fact_id],
230                |row| Ok((row.get(0)?, row.get(1)?)),
231            )
232            .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
233
234        tx.execute(
235            "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
236            params![fts_rowid, old_content],
237        )?;
238
239        tx.execute(
240            "UPDATE facts
241             SET content = ?1,
242                 embedding = ?2,
243                 embedding_q8 = ?3,
244                 updated_at = datetime('now')
245             WHERE id = ?4",
246            params![new_content, new_embedding_bytes, new_q8_bytes, fact_id],
247        )?;
248
249        tx.execute(
250            "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
251            params![fts_rowid, new_content],
252        )?;
253        tx.execute(
254            "DELETE FROM derivation_edges
255             WHERE (source_kind = 'fact' AND source_id = ?1)
256                OR (target_kind = 'fact' AND target_id = ?1)",
257            params![fact_id],
258        )?;
259
260        #[cfg(feature = "hnsw")]
261        enqueue_pending_index_op(
262            tx,
263            &format!("fact:{}", fact_id),
264            "fact",
265            PendingIndexOpKind::Upsert,
266        )?;
267        db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
268
269        Ok(())
270    })
271}
272
273/// Delete all namespace-scoped memory atomically and report every affected surface.
274#[cfg(feature = "admin-ops")]
275pub fn delete_namespace(
276    conn: &Connection,
277    namespace: &str,
278) -> Result<NamespaceDeleteReport, MemoryError> {
279    with_transaction(conn, |tx| {
280        let mut report = NamespaceDeleteReport::default();
281        let delete_session = |session_id: &str| -> Result<(usize, usize), MemoryError> {
282            let message_data: Vec<(i64, String, i64, bool)> = {
283                let mut stmt = tx.prepare(
284                    "SELECT m.id, m.content, mm.rowid, m.embedding IS NOT NULL
285                     FROM messages m
286                     JOIN messages_rowid_map mm ON mm.message_id = m.id
287                     WHERE m.session_id = ?1",
288                )?;
289                let rows = stmt.query_map(params![session_id], |row| {
290                    Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
291                })?;
292                rows.collect::<Result<Vec<_>, _>>()?
293            };
294
295            for (message_id, content, fts_rowid, has_embedding) in &message_data {
296                #[cfg(not(feature = "hnsw"))]
297                let _ = (message_id, has_embedding);
298                tx.execute(
299                    "INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', ?1, ?2)",
300                    params![fts_rowid, content],
301                )?;
302                #[cfg(feature = "hnsw")]
303                if *has_embedding {
304                    enqueue_pending_index_op(
305                        tx,
306                        &format!("msg:{}", message_id),
307                        "message",
308                        PendingIndexOpKind::Delete,
309                    )?;
310                }
311            }
312
313            let affected = tx.execute("DELETE FROM sessions WHERE id = ?1", params![session_id])?;
314            if affected == 0 {
315                return Err(MemoryError::SessionNotFound(session_id.to_string()));
316            }
317            let hnsw_ops = message_data
318                .iter()
319                .filter(|(_, _, _, has_embedding)| *has_embedding)
320                .count();
321            Ok((message_data.len(), hnsw_ops))
322        };
323
324        let document_ids: Vec<String> = {
325            let mut stmt = tx.prepare("SELECT id FROM documents WHERE namespace = ?1")?;
326            let ids = stmt
327                .query_map(params![namespace], |row| row.get(0))?
328                .collect::<Result<Vec<_>, _>>()?;
329            ids
330        };
331
332        let session_ids: Vec<String> = {
333            let mut stmt = tx.prepare("SELECT id, metadata FROM sessions")?;
334            let rows = stmt.query_map([], |row| {
335                Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
336            })?;
337            let mut ids = Vec::new();
338            for row in rows {
339                let (session_id, metadata_raw) = row?;
340                let metadata = parse_optional_json(
341                    "sessions",
342                    &session_id,
343                    "metadata",
344                    metadata_raw.as_deref(),
345                )?;
346                let namespace_matches = metadata
347                    .as_ref()
348                    .and_then(|value| {
349                        value
350                            .get("namespace")
351                            .or_else(|| value.get("scope_namespace"))
352                    })
353                    .and_then(|value| value.as_str())
354                    == Some(namespace);
355                if namespace_matches {
356                    ids.push(session_id);
357                }
358            }
359            ids
360        };
361
362        for session_id in &session_ids {
363            let (messages, hnsw_ops) = delete_session(session_id)?;
364            report.messages += messages;
365            report.hnsw_ops += hnsw_ops;
366        }
367        report.sessions = session_ids.len();
368
369        let delete_derivation_edges_for_id = |kind: &str, id: &str| -> Result<(), MemoryError> {
370            tx.execute(
371                "DELETE FROM derivation_edges
372                 WHERE (source_kind = ?1 AND source_id = ?2)
373                    OR (target_kind = ?1 AND target_id = ?2)",
374                params![kind, id],
375            )?;
376            Ok(())
377        };
378
379        let delete_derivation_edges_for_ids =
380            |kind: &str, ids: &[String]| -> Result<(), MemoryError> {
381                for id in ids {
382                    delete_derivation_edges_for_id(kind, id)?;
383                }
384                Ok(())
385            };
386
387        let facts: Vec<(String, i64, String)> = {
388            let mut stmt = tx.prepare(
389                "SELECT f.id, fm.rowid, f.content
390                 FROM facts f
391                 JOIN facts_rowid_map fm ON fm.fact_id = f.id
392                 WHERE f.namespace = ?1",
393            )?;
394            let facts = stmt
395                .query_map(params![namespace], |row| {
396                    Ok((row.get(0)?, row.get(1)?, row.get(2)?))
397                })?
398                .collect::<Result<Vec<_>, _>>()?;
399            facts
400        };
401
402        for (fact_id, fts_rowid, content) in &facts {
403            tx.execute(
404                "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
405                params![fts_rowid, content],
406            )?;
407            tx.execute(
408                "DELETE FROM facts_rowid_map WHERE fact_id = ?1",
409                params![fact_id],
410            )?;
411
412            #[cfg(feature = "hnsw")]
413            enqueue_pending_index_op(
414                tx,
415                &format!("fact:{}", fact_id),
416                "fact",
417                PendingIndexOpKind::Delete,
418            )?;
419            #[cfg(feature = "hnsw")]
420            {
421                report.hnsw_ops += 1;
422            }
423        }
424        tx.execute("DELETE FROM facts WHERE namespace = ?1", params![namespace])?;
425        report.facts = facts.len();
426
427        for doc_id in &document_ids {
428            let mut stmt = tx.prepare(
429                "SELECT c.id, c.content, cm.rowid
430                 FROM chunks c
431                 JOIN chunks_rowid_map cm ON cm.chunk_id = c.id
432                 WHERE c.document_id = ?1",
433            )?;
434            let chunk_rows: Vec<(String, String, i64)> = stmt
435                .query_map(params![doc_id], |row| {
436                    Ok((row.get(0)?, row.get(1)?, row.get(2)?))
437                })?
438                .collect::<Result<Vec<_>, _>>()?;
439            report.chunks += chunk_rows.len();
440
441            for (chunk_id, content, fts_rowid) in &chunk_rows {
442                tx.execute(
443                    "INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', ?1, ?2)",
444                    params![fts_rowid, content],
445                )?;
446                tx.execute(
447                    "DELETE FROM chunks_rowid_map WHERE chunk_id = ?1",
448                    params![chunk_id],
449                )?;
450                #[cfg(feature = "hnsw")]
451                enqueue_pending_index_op(
452                    tx,
453                    &format!("chunk:{}", chunk_id),
454                    "chunk",
455                    PendingIndexOpKind::Delete,
456                )?;
457                #[cfg(feature = "hnsw")]
458                {
459                    report.hnsw_ops += 1;
460                }
461            }
462
463            tx.execute("DELETE FROM chunks WHERE document_id = ?1", params![doc_id])?;
464        }
465
466        for doc_id in &document_ids {
467            let mut stmt = tx.prepare(
468                "SELECT e.episode_id, e.search_text, erm.rowid
469                 FROM episodes e
470                 JOIN episodes_rowid_map erm ON erm.episode_id = e.episode_id
471                 WHERE e.document_id = ?1",
472            )?;
473            let episode_rows: Vec<(String, String, i64)> = stmt
474                .query_map(params![doc_id], |row| {
475                    Ok((row.get(0)?, row.get(1)?, row.get(2)?))
476                })?
477                .collect::<Result<Vec<_>, _>>()?;
478            report.episodes += episode_rows.len();
479
480            for (episode_id, search_text, fts_rowid) in &episode_rows {
481                tx.execute(
482                    "INSERT INTO episodes_fts(episodes_fts, rowid, content) VALUES ('delete', ?1, ?2)",
483                    params![fts_rowid, search_text],
484                )?;
485                tx.execute(
486                    "DELETE FROM episodes_rowid_map WHERE episode_id = ?1",
487                    params![episode_id],
488                )?;
489                tx.execute(
490                    "DELETE FROM episode_causes WHERE episode_id = ?1",
491                    params![episode_id],
492                )?;
493                #[cfg(feature = "hnsw")]
494                enqueue_pending_index_op(
495                    tx,
496                    &episodes::episode_item_key(episode_id),
497                    "episode",
498                    PendingIndexOpKind::Delete,
499                )?;
500                #[cfg(feature = "hnsw")]
501                {
502                    report.hnsw_ops += 1;
503                }
504            }
505
506            tx.execute(
507                "DELETE FROM episodes WHERE document_id = ?1",
508                params![doc_id],
509            )?;
510            tx.execute("DELETE FROM documents WHERE id = ?1", params![doc_id])?;
511        }
512        report.documents = document_ids.len();
513
514        let claim_ids: Vec<String> = {
515            let mut stmt =
516                tx.prepare("SELECT claim_id FROM claim_versions WHERE scope_namespace = ?1")?;
517            let ids = stmt
518                .query_map(params![namespace], |row| row.get(0))?
519                .collect::<Result<Vec<_>, _>>()?;
520            ids
521        };
522
523        let claim_version_ids: Vec<String> = {
524            let mut stmt = tx.prepare(
525                "SELECT claim_version_id FROM claim_versions WHERE scope_namespace = ?1",
526            )?;
527            let ids = stmt
528                .query_map(params![namespace], |row| row.get(0))?
529                .collect::<Result<Vec<_>, _>>()?;
530            ids
531        };
532
533        let relation_version_ids: Vec<String> = {
534            let mut stmt = tx.prepare(
535                "SELECT relation_version_id FROM relation_versions WHERE scope_namespace = ?1",
536            )?;
537            let ids = stmt
538                .query_map(params![namespace], |row| row.get(0))?
539                .collect::<Result<Vec<_>, _>>()?;
540            ids
541        };
542
543        let alias_entity_ids: Vec<String> = {
544            let mut stmt = tx.prepare(
545                "SELECT canonical_entity_id FROM entity_aliases WHERE scope_namespace = ?1",
546            )?;
547            let ids = stmt
548                .query_map(params![namespace], |row| row.get(0))?
549                .collect::<Result<Vec<_>, _>>()?;
550            ids
551        };
552
553        let evidence_handles: Vec<String> = {
554            let mut stmt = tx.prepare(
555                "SELECT er.fetch_handle FROM evidence_refs er
556                 JOIN projection_import_log pil ON er.source_envelope_id = pil.source_envelope_id
557                 WHERE pil.scope_namespace = ?1",
558            )?;
559            let handles = stmt
560                .query_map(params![namespace], |row| row.get(0))?
561                .collect::<Result<Vec<_>, _>>()?;
562            handles
563        };
564
565        let episode_ids: Vec<String> = {
566            let mut stmt = tx.prepare(
567                "SELECT episode_id FROM episode_links
568                 WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
569            )?;
570            let ids = stmt
571                .query_map(params![namespace], |row| row.get(0))?
572                .collect::<Result<Vec<_>, _>>()?;
573            ids
574        };
575
576        delete_derivation_edges_for_ids("claim", &claim_ids)?;
577        delete_derivation_edges_for_ids("claim_version", &claim_version_ids)?;
578        delete_derivation_edges_for_ids("relation_version", &relation_version_ids)?;
579        delete_derivation_edges_for_ids("entity", &alias_entity_ids)?;
580        delete_derivation_edges_for_ids("evidence_ref", &evidence_handles)?;
581        delete_derivation_edges_for_ids("episode", &episode_ids)?;
582
583        report.projection_rows += tx.execute(
584            "DELETE FROM claim_versions WHERE scope_namespace = ?1",
585            params![namespace],
586        )?;
587        report.projection_rows += tx.execute(
588            "DELETE FROM relation_versions WHERE scope_namespace = ?1",
589            params![namespace],
590        )?;
591        report.projection_rows += tx.execute(
592            "DELETE FROM entity_aliases WHERE scope_namespace = ?1",
593            params![namespace],
594        )?;
595        report.projection_rows += tx.execute(
596            "DELETE FROM evidence_refs
597             WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
598            params![namespace],
599        )?;
600        report.projection_rows += tx.execute(
601            "DELETE FROM episode_links
602             WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
603            params![namespace],
604        )?;
605        report.projection_rows += tx.execute(
606            "DELETE FROM projection_import_failures WHERE scope_namespace = ?1",
607            params![namespace],
608        )?;
609        report.projection_rows += tx.execute(
610            "DELETE FROM projection_import_log WHERE scope_namespace = ?1",
611            params![namespace],
612        )?;
613
614        Ok(report)
615    })
616}
617
618/// Get a fact by ID.
619pub fn get_fact(conn: &Connection, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
620    let result = conn.query_row(
621        "SELECT id, namespace, content, source, created_at, updated_at, metadata
622         FROM facts WHERE id = ?1",
623        params![fact_id],
624        |row| {
625            Ok((
626                row.get::<_, String>(0)?,
627                row.get::<_, String>(1)?,
628                row.get::<_, String>(2)?,
629                row.get::<_, Option<String>>(3)?,
630                row.get::<_, String>(4)?,
631                row.get::<_, String>(5)?,
632                row.get::<_, Option<String>>(6)?,
633            ))
634        },
635    );
636
637    match result {
638        Ok((id, namespace, content, source, created_at, updated_at, metadata_raw)) => {
639            Ok(Some(Fact {
640                metadata: parse_optional_json("facts", &id, "metadata", metadata_raw.as_deref())?,
641                id,
642                namespace,
643                content,
644                source,
645                created_at,
646                updated_at,
647            }))
648        }
649        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
650        Err(err) => Err(MemoryError::Database(err)),
651    }
652}
653
654/// Get a fact embedding vector.
655pub fn get_fact_embedding(
656    conn: &Connection,
657    fact_id: &str,
658) -> Result<Option<Vec<f32>>, MemoryError> {
659    let result: Result<Option<Vec<u8>>, _> = conn.query_row(
660        "SELECT embedding FROM facts WHERE id = ?1",
661        params![fact_id],
662        |row| row.get(0),
663    );
664
665    match result {
666        Ok(Some(bytes)) => Ok(Some(bytes_to_embedding(&bytes)?)),
667        Ok(None) => Ok(None),
668        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
669        Err(err) => Err(MemoryError::Database(err)),
670    }
671}
672
673/// List the distinct namespaces that currently contain facts.
674pub fn list_fact_namespaces(conn: &Connection) -> Result<Vec<String>, MemoryError> {
675    let mut stmt = conn.prepare("SELECT DISTINCT namespace FROM facts ORDER BY namespace")?;
676    let rows = stmt
677        .query_map([], |row| row.get::<_, String>(0))?
678        .collect::<Result<Vec<_>, _>>()?;
679    Ok(rows)
680}
681
682/// List facts within a namespace.
683#[allow(dead_code)] // retained as an internal compatibility seam for older callers
684pub fn list_facts(
685    conn: &Connection,
686    namespace: &str,
687    limit: usize,
688    offset: usize,
689) -> Result<Vec<Fact>, MemoryError> {
690    list_facts_with_view(conn, namespace, limit, offset, &StateView::Current)
691}
692
693/// Authority state selected by a fact retrieval.
694#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
695pub enum StateView {
696    Current,
697    HistoricalAt(String),
698    RecordedAsOf(String),
699    IncludeSuperseded,
700}
701
702pub(crate) fn fact_is_visible_with_view(
703    conn: &Connection,
704    fact_id: &str,
705    view: &StateView,
706) -> Result<bool, MemoryError> {
707    let forgotten: bool = conn.query_row(
708        "SELECT EXISTS(SELECT 1 FROM forgotten_facts WHERE fact_id = ?1)",
709        params![fact_id],
710        |row| row.get(0),
711    )?;
712    if forgotten {
713        return Ok(false);
714    }
715    let cutoff = match view {
716        StateView::HistoricalAt(value) | StateView::RecordedAsOf(value) => {
717            let parsed = chrono::DateTime::parse_from_rfc3339(value).map_err(|e| {
718                MemoryError::Other(format!("invalid StateView timestamp '{value}': {e}"))
719            })?;
720            Some(
721                parsed
722                    .with_timezone(&chrono::Utc)
723                    .format("%Y-%m-%d %H:%M:%S%.6f")
724                    .to_string(),
725            )
726        }
727        _ => None,
728    };
729    let include_superseded = matches!(view, StateView::IncludeSuperseded);
730    let visible: i64 = conn.query_row(
731        "SELECT EXISTS(
732             SELECT 1 FROM facts f
733             WHERE f.id = ?1
734               AND (?2 IS NULL OR f.created_at <= ?2)
735               AND (?3 = 1 OR NOT EXISTS (
736                   SELECT 1 FROM graph_edges ge
737                   WHERE ge.target = 'fact:' || f.id
738                     AND ge.is_invalidated = 0
739                     AND COALESCE(
740                         json_extract(ge.edge_type, '$.relation'),
741                         json_extract(ge.edge_type, '$.entity.relation')
742                     ) IN ('supersedes', 'redacts')
743                     AND (?2 IS NULL OR COALESCE(ge.recorded_time, ge.recorded_at) <= ?2)
744               ))
745         )",
746        params![fact_id, cutoff, include_superseded],
747        |row| row.get(0),
748    )?;
749    Ok(visible != 0)
750}
751
752/// List facts under an explicit authority-state view. Inconsistent lineage is rejected.
753pub fn list_facts_with_view(
754    conn: &Connection,
755    namespace: &str,
756    limit: usize,
757    offset: usize,
758    view: &StateView,
759) -> Result<Vec<Fact>, MemoryError> {
760    let cutoff = match view {
761        StateView::HistoricalAt(value) | StateView::RecordedAsOf(value) => {
762            let parsed = chrono::DateTime::parse_from_rfc3339(value).map_err(|e| {
763                MemoryError::Other(format!("invalid StateView timestamp '{value}': {e}"))
764            })?;
765            Some(
766                parsed
767                    .with_timezone(&chrono::Utc)
768                    .format("%Y-%m-%d %H:%M:%S%.6f")
769                    .to_string(),
770            )
771        }
772        _ => None,
773    };
774    let inconsistent: i64 = conn.query_row(
775        "SELECT COUNT(*) FROM (
776             SELECT target FROM graph_edges
777             WHERE is_invalidated = 0
778               AND COALESCE(
779                   json_extract(edge_type, '$.relation'),
780                   json_extract(edge_type, '$.entity.relation')
781               ) IN ('supersedes', 'redacts')
782               AND (?1 IS NULL OR COALESCE(recorded_time, recorded_at) <= ?1)
783             GROUP BY target HAVING COUNT(DISTINCT source) > 1
784         )",
785        params![cutoff.as_deref()],
786        |row| row.get(0),
787    )?;
788    if inconsistent != 0 {
789        return Err(MemoryError::Other(
790            "inconsistent fact lineage: multiple active heads".into(),
791        ));
792    }
793    let include_superseded = matches!(view, StateView::IncludeSuperseded);
794    let mut stmt = conn.prepare(
795        "SELECT id, namespace, content, source, created_at, updated_at, metadata
796         FROM facts
797         WHERE namespace = ?1
798           AND NOT EXISTS (
799               SELECT 1 FROM forgotten_facts ff WHERE ff.fact_id = facts.id
800           )
801           AND (?4 IS NULL OR created_at <= ?4)
802           AND (?5 = 1 OR NOT EXISTS (
803               SELECT 1 FROM graph_edges ge
804               WHERE ge.target = 'fact:' || facts.id
805                 AND ge.is_invalidated = 0
806                 AND COALESCE(
807                     json_extract(ge.edge_type, '$.relation'),
808                     json_extract(ge.edge_type, '$.entity.relation')
809                 ) IN ('supersedes', 'redacts')
810                 AND (?4 IS NULL OR COALESCE(ge.recorded_time, ge.recorded_at) <= ?4)
811           ))
812         ORDER BY updated_at DESC
813         LIMIT ?2 OFFSET ?3",
814    )?;
815
816    let facts = stmt
817        .query_map(
818            params![
819                namespace,
820                limit as i64,
821                offset as i64,
822                cutoff,
823                include_superseded
824            ],
825            |row| {
826                Ok((
827                    row.get::<_, String>(0)?,
828                    row.get::<_, String>(1)?,
829                    row.get::<_, String>(2)?,
830                    row.get::<_, Option<String>>(3)?,
831                    row.get::<_, String>(4)?,
832                    row.get::<_, String>(5)?,
833                    row.get::<_, Option<String>>(6)?,
834                ))
835            },
836        )?
837        .collect::<Result<Vec<_>, _>>()?
838        .into_iter()
839        .map(
840            |(id, namespace, content, source, created_at, updated_at, metadata_raw)| {
841                Ok(Fact {
842                    metadata: parse_optional_json(
843                        "facts",
844                        &id,
845                        "metadata",
846                        metadata_raw.as_deref(),
847                    )?,
848                    id,
849                    namespace,
850                    content,
851                    source,
852                    created_at,
853                    updated_at,
854                })
855            },
856        )
857        .collect::<Result<Vec<_>, MemoryError>>()?;
858
859    Ok(facts)
860}
861
862impl MemoryStore {
863    /// Explicitly ungoverned compatibility write.
864    ///
865    /// This preserves the pre-authority raw storage API for migrations and local tooling. It does
866    /// not create an origin label and its output is therefore denied by every governed path.
867    pub async fn add_fact_raw_compat(
868        &self,
869        namespace: &str,
870        content: &str,
871        source: Option<&str>,
872        metadata: Option<serde_json::Value>,
873        trace_ctx: Option<&TraceCtx>,
874    ) -> Result<Fact, MemoryError> {
875        let id = self
876            .add_fact_with_trace(namespace, content, source, metadata, trace_ctx)
877            .await?;
878        self.get_fact(&id)
879            .await?
880            .ok_or(MemoryError::FactNotFound(id))
881    }
882
883    /// Store a fact with automatic embedding. Returns the fact ID (UUID v4).
884    ///
885    /// This is a non-authoritative storage primitive. Governed mutations must
886    /// use [`MemoryStore::authority`] so admission and lineage are enforced.
887    pub async fn add_fact(
888        &self,
889        namespace: &str,
890        content: &str,
891        source: Option<&str>,
892        metadata: Option<serde_json::Value>,
893    ) -> Result<String, MemoryError> {
894        self.add_fact_with_trace(namespace, content, source, metadata, None)
895            .await
896    }
897
898    /// Store a fact with automatic embedding and optional trace metadata.
899    pub async fn add_fact_with_trace(
900        &self,
901        namespace: &str,
902        content: &str,
903        source: Option<&str>,
904        metadata: Option<serde_json::Value>,
905        trace_ctx: Option<&TraceCtx>,
906    ) -> Result<String, MemoryError> {
907        self.validate_content("fact.content", content)?;
908
909        // Dedup: check if a fact with the same content already exists.
910        // This prevents the 4-5% DB bloat from duplicate ingestion.
911        let ns_check = namespace.to_string();
912        let ct_check = content.to_string();
913        let existing_id = self
914            .with_read_conn(move |conn| {
915                let result: Option<String> = conn
916                    .query_row(
917                        "SELECT id FROM facts WHERE content = ?1 AND namespace = ?2 LIMIT 1",
918                        rusqlite::params![&ct_check, &ns_check],
919                        |row| row.get::<_, String>(0),
920                    )
921                    .ok();
922                Ok(result)
923            })
924            .await?;
925
926        if let Some(id) = existing_id {
927            return Ok(id);
928        }
929
930        let (embedding, sparse, sparse_representation) = self
931            .embed_text_with_sparse_internal(content, crate::EmbeddingPurpose::Document)
932            .await?;
933        self.validate_embedding_dimensions(&embedding)?;
934        let embedding_bytes = db::embedding_to_bytes(&embedding);
935        let fact_id = uuid::Uuid::new_v4().to_string();
936        let max_facts_per_namespace = self.inner.config.limits.max_facts_per_namespace;
937
938        let quantizer = Quantizer::new(self.inner.config.embedding.dimensions);
939        // INTENTIONAL: q8 quantization is an optional search optimization; missing q8 is non-fatal
940        let q8_bytes = quantizer
941            .quantize(&embedding)
942            .map(|qv| quantize::pack_quantized(&qv))
943            .ok();
944
945        let ns = namespace.to_string();
946        let ct = content.to_string();
947        let fid = fact_id.clone();
948        let src = source.map(|s| s.to_string());
949        let meta = merge_trace_ctx(metadata, trace_ctx);
950        self.with_write_conn(move |conn| {
951            let current_count: usize = conn.query_row(
952                "SELECT COUNT(*) FROM facts WHERE namespace = ?1",
953                rusqlite::params![&ns],
954                |row| row.get(0),
955            )?;
956            if current_count >= max_facts_per_namespace {
957                return Err(MemoryError::NamespaceFull {
958                    namespace: ns.clone(),
959                    count: current_count,
960                    limit: max_facts_per_namespace,
961                });
962            }
963            insert_fact_with_fts_q8(
964                conn,
965                &fid,
966                &ns,
967                &ct,
968                &embedding_bytes,
969                q8_bytes.as_deref(),
970                src.as_deref(),
971                meta.as_ref(),
972                sparse.as_ref().zip(sparse_representation.as_deref()),
973            )
974        })
975        .await?;
976
977        self.clear_search_cache();
978
979        #[cfg(feature = "hnsw")]
980        self.sync_pending_hnsw_ops_best_effort("add_fact").await;
981
982        Ok(fact_id)
983    }
984
985    /// Store a fact with a pre-computed embedding.
986    pub async fn add_fact_with_embedding(
987        &self,
988        namespace: &str,
989        content: &str,
990        embedding: &[f32],
991        source: Option<&str>,
992        metadata: Option<serde_json::Value>,
993    ) -> Result<String, MemoryError> {
994        self.add_fact_with_embedding_and_trace(
995            namespace, content, embedding, source, metadata, None,
996        )
997        .await
998    }
999
1000    /// Store a fact with a pre-computed embedding and optional trace metadata.
1001    pub async fn add_fact_with_embedding_and_trace(
1002        &self,
1003        namespace: &str,
1004        content: &str,
1005        embedding: &[f32],
1006        source: Option<&str>,
1007        metadata: Option<serde_json::Value>,
1008        trace_ctx: Option<&TraceCtx>,
1009    ) -> Result<String, MemoryError> {
1010        self.validate_content("fact.content", content)?;
1011        self.validate_embedding_dimensions(embedding)?;
1012        let embedding_bytes = db::embedding_to_bytes(embedding);
1013        let sparse = self.inner.config.search.derive_sparse_from_dense.then(|| {
1014            crate::SparseWeights::from_dense(
1015                embedding,
1016                self.inner.config.search.sparse_derive_top_k,
1017                self.inner.config.search.sparse_derive_min_weight,
1018            )
1019        });
1020        let fact_id = uuid::Uuid::new_v4().to_string();
1021        let max_facts_per_namespace = self.inner.config.limits.max_facts_per_namespace;
1022
1023        let quantizer = Quantizer::new(self.inner.config.embedding.dimensions);
1024        // INTENTIONAL: q8 quantization is an optional search optimization; missing q8 is non-fatal
1025        let q8_bytes = quantizer
1026            .quantize(embedding)
1027            .map(|qv| quantize::pack_quantized(&qv))
1028            .ok();
1029
1030        let ns = namespace.to_string();
1031        let ct = content.to_string();
1032        let fid = fact_id.clone();
1033        let src = source.map(|s| s.to_string());
1034        let meta = merge_trace_ctx(metadata, trace_ctx);
1035        self.with_write_conn(move |conn| {
1036            let current_count: usize = conn.query_row(
1037                "SELECT COUNT(*) FROM facts WHERE namespace = ?1",
1038                rusqlite::params![&ns],
1039                |row| row.get(0),
1040            )?;
1041            if current_count >= max_facts_per_namespace {
1042                return Err(MemoryError::NamespaceFull {
1043                    namespace: ns.clone(),
1044                    count: current_count,
1045                    limit: max_facts_per_namespace,
1046                });
1047            }
1048            insert_fact_with_fts_q8(
1049                conn,
1050                &fid,
1051                &ns,
1052                &ct,
1053                &embedding_bytes,
1054                q8_bytes.as_deref(),
1055                src.as_deref(),
1056                meta.as_ref(),
1057                sparse
1058                    .as_ref()
1059                    .map(|weights| (weights, "generic_dense_derived_sparse")),
1060            )
1061        })
1062        .await?;
1063
1064        self.clear_search_cache();
1065
1066        #[cfg(feature = "hnsw")]
1067        self.sync_pending_hnsw_ops_best_effort("add_fact_with_embedding")
1068            .await;
1069
1070        Ok(fact_id)
1071    }
1072
1073    /// **DANGER**: This physically mutates/deletes a truth-bearing row.
1074    /// This is admin-only and gated behind the `admin-ops` feature.
1075    /// Default agent-facing APIs should use supersession (add a new fact
1076    /// with a supersession link) instead of hard delete/update.
1077    #[cfg(feature = "admin-ops")]
1078    pub async fn update_fact(&self, fact_id: &str, content: &str) -> Result<(), MemoryError> {
1079        self.validate_content("fact.content", content)?;
1080        let (embedding, sparse, sparse_representation) = self
1081            .embed_text_with_sparse_internal(content, crate::EmbeddingPurpose::Document)
1082            .await?;
1083        self.validate_embedding_dimensions(&embedding)?;
1084        let embedding_bytes = db::embedding_to_bytes(&embedding);
1085        // INTENTIONAL: q8 quantization is an optional search optimization; missing q8 is non-fatal
1086        let q8_bytes = Quantizer::new(self.inner.config.embedding.dimensions)
1087            .quantize(&embedding)
1088            .map(|qv| quantize::pack_quantized(&qv))
1089            .ok();
1090
1091        let fid = fact_id.to_string();
1092        let ct = content.to_string();
1093        self.with_write_conn(move |conn| {
1094            update_fact_with_fts(conn, &fid, &ct, &embedding_bytes, q8_bytes.as_deref())?;
1095            let item_key = format!("fact:{fid}");
1096            if let Some((weights, representation)) =
1097                sparse.as_ref().zip(sparse_representation.as_deref())
1098            {
1099                db::store_sparse_vector(conn, &item_key, weights, representation)?;
1100            } else {
1101                db::delete_sparse_vector(conn, &item_key)?;
1102            }
1103            Ok(())
1104        })
1105        .await?;
1106
1107        #[cfg(feature = "hnsw")]
1108        self.sync_pending_hnsw_ops_best_effort("update_fact").await;
1109
1110        self.clear_search_cache();
1111
1112        Ok(())
1113    }
1114
1115    /// **DANGER**: This physically mutates/deletes a truth-bearing row.
1116    /// This is admin-only and gated behind the `admin-ops` feature.
1117    /// Default agent-facing APIs should use supersession (add a new fact
1118    /// with a supersession link) instead of hard delete/update.
1119    #[cfg(feature = "admin-ops")]
1120    pub async fn delete_fact(&self, fact_id: &str) -> Result<(), MemoryError> {
1121        let fid = fact_id.to_string();
1122        self.with_write_conn(move |conn| delete_fact_with_fts(conn, &fid))
1123            .await?;
1124
1125        #[cfg(feature = "hnsw")]
1126        self.sync_pending_hnsw_ops_best_effort("delete_fact").await;
1127
1128        self.clear_search_cache();
1129
1130        Ok(())
1131    }
1132
1133    #[cfg(not(feature = "admin-ops"))]
1134    #[doc = r#"
1135    ```compile_fail
1136    use semantic_memory::MemoryStore;
1137
1138    fn main() {
1139        let _ = MemoryStore::update_fact;
1140        let _ = MemoryStore::delete_fact;
1141        let _ = MemoryStore::delete_namespace;
1142        let _ = MemoryStore::reembed_all;
1143    }
1144    ```"#]
1145    pub(crate) const _MEM014_ADMIN_OPS_GUARD: () = ();
1146
1147    /// **DANGER**: physically deletes every truth-bearing row in a namespace.
1148    /// This is admin-only and gated behind the `admin-ops` feature. Ordinary
1149    /// callers must use governed supersession/forgetting flows instead.
1150    #[cfg(feature = "admin-ops")]
1151    pub async fn delete_namespace(
1152        &self,
1153        namespace: &str,
1154    ) -> Result<NamespaceDeleteReport, MemoryError> {
1155        let ns = namespace.to_string();
1156        let count = self
1157            .with_write_conn(move |conn| delete_namespace(conn, &ns))
1158            .await?;
1159
1160        #[cfg(feature = "hnsw")]
1161        self.sync_pending_hnsw_ops_best_effort("delete_namespace")
1162            .await;
1163
1164        self.clear_search_cache();
1165
1166        Ok(count)
1167    }
1168
1169    /// Get a fact by ID.
1170    pub async fn get_fact(&self, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
1171        let fid = fact_id.to_string();
1172        self.with_read_conn(move |conn| get_fact(conn, &fid)).await
1173    }
1174
1175    /// Explicitly ungoverned compatibility read. Prefer `authority().get_fact_governed`.
1176    pub async fn get_fact_raw_compat(&self, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
1177        self.get_fact(fact_id).await
1178    }
1179
1180    /// Get a fact's embedding vector.
1181    pub async fn get_fact_embedding(&self, fact_id: &str) -> Result<Option<Vec<f32>>, MemoryError> {
1182        let fid = fact_id.to_string();
1183        self.with_read_conn(move |conn| get_fact_embedding(conn, &fid))
1184            .await
1185    }
1186
1187    /// List all facts in a namespace using the default `Current` view.
1188    pub async fn list_facts(
1189        &self,
1190        namespace: &str,
1191        limit: usize,
1192        offset: usize,
1193    ) -> Result<Vec<Fact>, MemoryError> {
1194        self.list_facts_with_view(namespace, limit, offset, StateView::Current)
1195            .await
1196    }
1197
1198    /// List facts under an explicit bitemporal authority-state view.
1199    pub async fn list_facts_with_view(
1200        &self,
1201        namespace: &str,
1202        limit: usize,
1203        offset: usize,
1204        view: StateView,
1205    ) -> Result<Vec<Fact>, MemoryError> {
1206        let ns = namespace.to_string();
1207        self.with_read_conn(move |conn| list_facts_with_view(conn, &ns, limit, offset, &view))
1208            .await
1209    }
1210
1211    /// List the distinct namespaces that currently contain facts.
1212    pub async fn list_fact_namespaces(&self) -> Result<Vec<String>, MemoryError> {
1213        self.with_read_conn(move |conn| list_fact_namespaces(conn))
1214            .await
1215    }
1216}
1217
1218#[cfg(test)]
1219mod state_view_regression_tests {
1220    use super::*;
1221    use crate::db::run_migrations;
1222    use rusqlite::Connection;
1223
1224    fn seeded() -> Connection {
1225        let conn = Connection::open_in_memory().unwrap();
1226        run_migrations(&conn).unwrap();
1227        for (id, content, created) in [
1228            ("old", "same topic old", "2026-07-10 21:00:00"),
1229            ("new", "same topic new", "2026-07-10 21:12:01"),
1230        ] {
1231            conn.execute(
1232                "INSERT INTO facts(id, namespace, content, created_at, updated_at) VALUES (?1, 'n', ?2, ?3, ?3)",
1233                params![id, content, created],
1234            ).unwrap();
1235        }
1236        conn
1237    }
1238
1239    fn supersedes(conn: &Connection, source: &str, target: &str, recorded: &str) {
1240        conn.execute(
1241            "INSERT INTO graph_edges(id, source, target, edge_type, weight, content_digest, recorded_at, valid_time, recorded_time)
1242             VALUES (lower(hex(randomblob(16))), ?1, ?2, '{\"type\":\"entity\",\"relation\":\"supersedes\"}', 1, lower(hex(randomblob(16))), ?3, ?3, ?3)",
1243            params![format!("fact:{source}"), format!("fact:{target}"), recorded],
1244        ).unwrap();
1245    }
1246
1247    fn supersedes_canonical(conn: &Connection, source: &str, target: &str, recorded: &str) {
1248        conn.execute(
1249            "INSERT INTO graph_edges(id, source, target, edge_type, weight, content_digest, recorded_at, valid_time, recorded_time)
1250             VALUES (lower(hex(randomblob(16))), ?1, ?2, '{\"entity\":{\"relation\":\"supersedes\"}}', 1, lower(hex(randomblob(16))), ?3, ?3, ?3)",
1251            params![format!("fact:{source}"), format!("fact:{target}"), recorded],
1252        ).unwrap();
1253    }
1254
1255    #[test]
1256    fn historical_view_excludes_future_fact_and_reconstructs_pre_supersession_head() {
1257        let conn = seeded();
1258        supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1259        let rows = list_facts_with_view(
1260            &conn,
1261            "n",
1262            10,
1263            0,
1264            &StateView::HistoricalAt("2026-07-10T21:11:50Z".into()),
1265        )
1266        .unwrap();
1267        assert_eq!(
1268            rows.iter().map(|f| f.id.as_str()).collect::<Vec<_>>(),
1269            ["old"]
1270        );
1271    }
1272
1273    #[test]
1274    fn historical_view_preserves_pre_adjudication_conflict() {
1275        let conn = seeded();
1276        conn.execute(
1277            "UPDATE facts SET created_at = '2026-07-10 21:10:00', updated_at = '2026-07-10 21:10:00' WHERE id = 'new'",
1278            [],
1279        )
1280        .unwrap();
1281        supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1282
1283        let rows = list_facts_with_view(
1284            &conn,
1285            "n",
1286            10,
1287            0,
1288            &StateView::HistoricalAt("2026-07-10T21:11:50Z".into()),
1289        )
1290        .unwrap();
1291        let ids = rows.iter().map(|fact| fact.id.as_str()).collect::<Vec<_>>();
1292        assert!(
1293            ids.contains(&"old"),
1294            "prior observation must remain visible"
1295        );
1296        assert!(ids.contains(&"new"), "conflicting observation created before the cutoff must remain visible until adjudication");
1297    }
1298
1299    #[test]
1300    fn current_view_excludes_superseded_fact() {
1301        let conn = seeded();
1302        supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1303        let rows = list_facts_with_view(&conn, "n", 10, 0, &StateView::Current).unwrap();
1304        assert_eq!(
1305            rows.iter().map(|f| f.id.as_str()).collect::<Vec<_>>(),
1306            ["new"]
1307        );
1308    }
1309
1310    #[test]
1311    fn current_view_accepts_canonical_entity_edge_serialization() {
1312        let conn = seeded();
1313        supersedes_canonical(&conn, "new", "old", "2026-07-10 21:12:01");
1314        let rows = list_facts_with_view(&conn, "n", 10, 0, &StateView::Current).unwrap();
1315        assert_eq!(
1316            rows.iter().map(|fact| fact.id.as_str()).collect::<Vec<_>>(),
1317            vec!["new"]
1318        );
1319    }
1320
1321    #[test]
1322    fn multiple_active_heads_fail_closed() {
1323        let conn = seeded();
1324        conn.execute("INSERT INTO facts(id, namespace, content, created_at, updated_at) VALUES ('other', 'n', 'same topic conflicting', '2026-07-10 21:13:00', '2026-07-10 21:13:00')", []).unwrap();
1325        supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1326        supersedes(&conn, "other", "old", "2026-07-10 21:13:00");
1327        assert!(list_facts_with_view(&conn, "n", 10, 0, &StateView::Current).is_err());
1328    }
1329}