Skip to main content

semantic_memory/
authority.rs

1//! Atomic, capability-gated authority mutations over the SQLite fact graph.
2//!
3//! This is deliberately a narrow write surface. Facts remain append-only; the
4//! authority tables record lineage heads while graph edges preserve the
5//! supersession or redaction event that caused a head transition.
6
7use crate::authority_contracts::{
8    AuthorityAdmission, AuthorityFaultStage, AuthorityOperationKind, AuthorityPermit,
9    AuthorityReceiptV1, AuthoritySnapshotId, AuthorityStateV1, RetrievalEpoch,
10};
11use crate::db::with_transaction;
12use crate::error::MemoryError;
13use crate::origin_authority::{
14    decide, label_digest, GovernedAccessRequestV1, GovernedFactAccessV1,
15    GovernedFactListResponseV1, GovernedGraphResponseV1, GovernedReplayResponseV1,
16    GovernedSearchResponseV1, GovernedStateResolutionResponseV1, OriginAuthorityLabelV1,
17    OriginAuthorityRecordV1, OriginDerivationKindV1,
18};
19use crate::quantize::{self, Quantizer};
20use crate::transition_contracts::{
21    MemoryTransitionCandidateV1, MemoryTransitionOutcomeV1, MemoryTransitionRecordV1,
22    MemoryTransitionVerificationV1, TransitionDisposition, TransitionOperation,
23    MEMORY_TRANSITION_RECORD_V1,
24};
25use crate::transition_verifier::{digest as transition_digest, verify_candidate};
26use crate::MemoryStore;
27use chrono::Utc;
28use rusqlite::{params, Connection, OptionalExtension, Transaction};
29use serde::Serialize;
30use std::sync::{Arc, Mutex};
31
32const RECEIPT_SCHEMA: &str = "authority_receipt_v1";
33const REDACTED_CONTENT: &str = "[REDACTED]";
34
35#[derive(Clone)]
36pub struct MemoryAuthority {
37    store: MemoryStore,
38}
39
40impl MemoryAuthority {
41    pub(crate) fn new(store: MemoryStore) -> Self {
42        Self { store }
43    }
44
45    /// Append a new fact and start a new authority lineage.
46    pub async fn append(
47        &self,
48        permit: AuthorityPermit,
49        caller_idempotency_key: String,
50        namespace: String,
51        content: String,
52        source: Option<String>,
53    ) -> Result<AuthorityReceiptV1, MemoryError> {
54        self.mutate(
55            permit,
56            caller_idempotency_key,
57            Mutation::Append {
58                namespace,
59                content,
60                source,
61            },
62        )
63        .await
64    }
65
66    /// Append a replacement fact and supersede the current lineage head.
67    pub async fn supersede(
68        &self,
69        permit: AuthorityPermit,
70        caller_idempotency_key: String,
71        target_fact_id: String,
72        content: String,
73        source: Option<String>,
74    ) -> Result<AuthorityReceiptV1, MemoryError> {
75        self.mutate(
76            permit,
77            caller_idempotency_key,
78            Mutation::Supersede {
79                target_fact_id,
80                content,
81                source,
82            },
83        )
84        .await
85    }
86
87    /// Append a redaction tombstone and make it the current lineage head.
88    pub async fn redact(
89        &self,
90        permit: AuthorityPermit,
91        caller_idempotency_key: String,
92        target_fact_id: String,
93        reason: String,
94    ) -> Result<AuthorityReceiptV1, MemoryError> {
95        self.mutate(
96            permit,
97            caller_idempotency_key,
98            Mutation::Redact {
99                target_fact_id,
100                reason,
101            },
102        )
103        .await
104    }
105
106    /// Forget a canonical fact and every accessible derived artifact in one transaction.
107    pub async fn forget(
108        &self,
109        permit: AuthorityPermit,
110        caller_idempotency_key: String,
111        request: crate::ForgettingClosureRequestV1,
112    ) -> Result<crate::ForgettingClosureReceiptV1, MemoryError> {
113        crate::forgetting::forget(&self.store, permit, caller_idempotency_key, request).await
114    }
115
116    /// Governed forgetting validates every root against the same policy evaluator before the
117    /// closure planner can inspect or invalidate it. Administrative authority is an explicit,
118    /// time-bounded elevation rather than an implicit consequence of a write permit.
119    pub async fn forget_governed(
120        &self,
121        permit: AuthorityPermit,
122        caller_idempotency_key: String,
123        request: crate::ForgettingClosureRequestV1,
124        access: GovernedAccessRequestV1,
125    ) -> Result<crate::ForgettingClosureReceiptV1, MemoryError> {
126        let access = access.with_purpose(crate::GovernedAccessPurposeV1::Admin);
127        for fact_id in &request.root_fact_ids {
128            let governed = self.get_fact_governed(fact_id, access.clone()).await?;
129            if !governed.decision.allowed {
130                return Err(MemoryError::OriginAuthorityRejected {
131                    principal: access.caller.0.clone(),
132                    reason: format!(
133                        "governed forgetting denied for '{}': {}",
134                        fact_id,
135                        governed.decision.reasons.join(",")
136                    ),
137                });
138            }
139        }
140        self.forget(permit, caller_idempotency_key, request).await
141    }
142
143    /// Deterministically verify a source-grounded candidate immediately before authority commit.
144    ///
145    /// Failed verification is durably quarantined and never invokes canonical mutation. A passing
146    /// verification, its immutable evidence, and the existing authority mutation commit in one
147    /// SQLite transaction.
148    pub async fn verify_and_commit(
149        &self,
150        permit: AuthorityPermit,
151        caller_idempotency_key: String,
152        candidate: MemoryTransitionCandidateV1,
153    ) -> Result<MemoryTransitionOutcomeV1, MemoryError> {
154        let kind = candidate_operation_kind(&candidate);
155        validate_authority_request(&permit, &caller_idempotency_key, kind)?;
156        let prepared = match candidate_content_for_embedding(&candidate) {
157            Some(content) => {
158                let (embedding, sparse, sparse_representation) =
159                    self.store.embed_text_with_sparse_internal(content).await?;
160                self.store.validate_embedding_dimensions(&embedding)?;
161                let embedding_bytes = crate::db::embedding_to_bytes(&embedding);
162                let q8_bytes = Quantizer::new(self.store.inner.config.embedding.dimensions)
163                    .quantize(&embedding)
164                    .map(|qv| quantize::pack_quantized(&qv))
165                    .ok();
166                Some(FactEmbedding {
167                    embedding: embedding_bytes,
168                    q8: q8_bytes,
169                    sparse,
170                    sparse_representation,
171                })
172            }
173            None => None,
174        };
175        let fault = self.store.inner.authority_fault.clone();
176        let outcome = self
177            .store
178            .with_write_conn(move |conn| {
179                execute_compiled_transition(
180                    conn,
181                    &permit,
182                    &caller_idempotency_key,
183                    candidate,
184                    &fault,
185                    prepared,
186                )
187            })
188            .await?;
189        if matches!(outcome, MemoryTransitionOutcomeV1::Committed { .. }) {
190            self.store.clear_search_cache();
191        }
192        Ok(outcome)
193    }
194
195    /// Inspect a committed or quarantined transition by caller idempotency key.
196    pub async fn get_transition_by_idempotency_key(
197        &self,
198        caller_idempotency_key: &str,
199    ) -> Result<Option<MemoryTransitionRecordV1>, MemoryError> {
200        let key = caller_idempotency_key.to_string();
201        self.store
202            .with_read_conn(move |conn| get_transition_record(conn, &key))
203            .await
204    }
205
206    /// Load an immutable selective-forgetting receipt by caller idempotency key.
207    pub async fn get_forgetting_receipt_by_idempotency_key(
208        &self,
209        caller_idempotency_key: &str,
210    ) -> Result<Option<crate::ForgettingClosureReceiptV1>, MemoryError> {
211        crate::forgetting::get_receipt(&self.store, caller_idempotency_key).await
212    }
213
214    /// Find a committed authority receipt by operation identity.
215    pub async fn get_receipt_by_operation_id(
216        &self,
217        operation_id: &str,
218    ) -> Result<Option<AuthorityReceiptV1>, MemoryError> {
219        let operation_id = operation_id.to_string();
220        self.store
221            .with_read_conn(move |conn| get_receipt(conn, "operation_id", &operation_id))
222            .await
223    }
224
225    /// Find a committed authority receipt by the caller's idempotency key.
226    pub async fn get_receipt_by_idempotency_key(
227        &self,
228        caller_idempotency_key: &str,
229    ) -> Result<Option<AuthorityReceiptV1>, MemoryError> {
230        let key = caller_idempotency_key.to_string();
231        self.store
232            .with_read_conn(move |conn| get_receipt(conn, "caller_idempotency_key", &key))
233            .await
234    }
235
236    /// Load the immutable write-time origin label for a canonical fact.
237    pub async fn get_origin_authority(
238        &self,
239        fact_id: &str,
240    ) -> Result<Option<OriginAuthorityRecordV1>, MemoryError> {
241        let fact_id = fact_id.to_string();
242        self.store
243            .with_read_conn(move |conn| load_origin_record(conn, &fact_id))
244            .await
245    }
246
247    /// Direct-ID governed recall. Content is never returned when the decision denies access.
248    pub async fn get_fact_governed(
249        &self,
250        fact_id: &str,
251        request: GovernedAccessRequestV1,
252    ) -> Result<GovernedFactAccessV1, MemoryError> {
253        let fact_id = fact_id.to_string();
254        self.store
255            .with_read_conn(move |conn| governed_fact_access(conn, &fact_id, &request))
256            .await
257    }
258
259    /// Governed export uses the same decision path as direct recall.
260    pub async fn export_fact_governed(
261        &self,
262        fact_id: &str,
263        request: GovernedAccessRequestV1,
264    ) -> Result<GovernedFactAccessV1, MemoryError> {
265        self.get_fact_governed(
266            fact_id,
267            request.with_purpose(crate::GovernedAccessPurposeV1::Export),
268        )
269        .await
270    }
271
272    /// Governed hybrid search filters every result after cache retrieval, preventing cache bypass.
273    pub async fn search_governed(
274        &self,
275        query: &str,
276        top_k: Option<usize>,
277        request: GovernedAccessRequestV1,
278    ) -> Result<GovernedSearchResponseV1, MemoryError> {
279        self.search_governed_with_view(query, top_k, request, crate::StateView::Current)
280            .await
281    }
282
283    /// Read the current authority snapshot and retrieval epoch for cache validation.
284    pub async fn current_state(&self) -> Result<AuthorityStateV1, MemoryError> {
285        self.store
286            .with_read_conn(|conn| {
287                with_transaction(conn, |tx| {
288                    let epoch = current_epoch(tx)?;
289                    let snapshot_id = snapshot_id(tx, epoch)?;
290                    Ok(AuthorityStateV1 {
291                        snapshot_id,
292                        retrieval_epoch: RetrievalEpoch(epoch),
293                    })
294                })
295            })
296            .await
297    }
298
299    /// Read-only compatibility shim for callers that only need the retrieval epoch.
300    pub(crate) async fn current_retrieval_epoch(&self) -> Result<RetrievalEpoch, MemoryError> {
301        Ok(self.current_state().await?.retrieval_epoch)
302    }
303
304    /// Governed current or historical search. The candidate source is intentionally irrelevant:
305    /// every returned row is passed through the same canonical evaluator used by direct access.
306    pub async fn search_governed_with_view(
307        &self,
308        query: &str,
309        top_k: Option<usize>,
310        request: GovernedAccessRequestV1,
311        view: crate::StateView,
312    ) -> Result<GovernedSearchResponseV1, MemoryError> {
313        let namespace = request.scope.namespace.clone();
314        let raw = self
315            .store
316            .search_with_view(query, top_k, Some(&[namespace.as_str()]), None, view)
317            .await?;
318        let mut results = Vec::new();
319        let mut decisions = Vec::new();
320        for result in raw {
321            let fact_id = match &result.source {
322                crate::SearchSource::Fact { fact_id, .. } => fact_id.clone(),
323                crate::SearchSource::Chunk { chunk_id, .. } => format!("chunk:{chunk_id}"),
324                crate::SearchSource::Message { message_id, .. } => format!("message:{message_id}"),
325                crate::SearchSource::Episode { episode_id, .. } => format!("episode:{episode_id}"),
326                crate::SearchSource::Projection { projection_id, .. } => {
327                    format!("projection:{projection_id}")
328                }
329            };
330            let decision = if matches!(result.source, crate::SearchSource::Fact { .. }) {
331                self.get_fact_governed(&fact_id, request.clone())
332                    .await?
333                    .decision
334            } else {
335                decide(&fact_id, None, None, None, &request)
336            };
337            if decision.allowed {
338                results.push(result);
339            }
340            decisions.push(decision);
341        }
342        Ok(GovernedSearchResponseV1 { results, decisions })
343    }
344
345    /// Governed enumeration applies the same direct-ID policy check to every listed fact.
346    pub async fn list_facts_governed(
347        &self,
348        request: GovernedAccessRequestV1,
349        limit: usize,
350        offset: usize,
351        view: crate::StateView,
352    ) -> Result<GovernedFactListResponseV1, MemoryError> {
353        let facts = self
354            .store
355            .list_facts_with_view(&request.scope.namespace, limit, offset, view)
356            .await?;
357        let mut admitted = Vec::new();
358        let mut decisions = Vec::new();
359        for fact in facts {
360            let response = self.get_fact_governed(&fact.id, request.clone()).await?;
361            if let Some(fact) = response.fact {
362                admitted.push(fact);
363            }
364            decisions.push(response.decision);
365        }
366        Ok(GovernedFactListResponseV1 {
367            facts: admitted,
368            decisions,
369        })
370    }
371
372    /// State resolution is candidate generation only; this wrapper removes denied assertions
373    /// before an answer can leave the crate, including historical state views and cache-backed
374    /// searches. The resolution receipt remains an audit record, not a content channel.
375    pub async fn resolve_memory_governed(
376        &self,
377        query: &str,
378        top_k: Option<usize>,
379        request: GovernedAccessRequestV1,
380        mode: crate::StateResolutionMode,
381        budget: usize,
382    ) -> Result<GovernedStateResolutionResponseV1, MemoryError> {
383        let namespaces = [request.scope.namespace.as_str()];
384        let mut response = self
385            .store
386            .resolve_memory(query, top_k, Some(&namespaces), mode, budget)
387            .await?;
388        let mut decisions = Vec::new();
389        response.assertions.retain(|assertion| {
390            let Some(fact_id) = assertion.memory_id.strip_prefix("fact:") else {
391                return false;
392            };
393            // This async call cannot occur in `retain`; decisions are collected below.
394            !fact_id.is_empty()
395        });
396        let mut admitted = Vec::new();
397        for assertion in response.assertions.drain(..) {
398            let fact_id = assertion
399                .memory_id
400                .strip_prefix("fact:")
401                .unwrap_or_default();
402            let decision = self
403                .get_fact_governed(fact_id, request.clone())
404                .await?
405                .decision;
406            if decision.allowed {
407                admitted.push(assertion);
408            }
409            decisions.push(decision);
410        }
411        response.assertions = admitted;
412        response.alternatives.retain(|alternative| {
413            response
414                .assertions
415                .iter()
416                .any(|assertion| assertion.memory_id == alternative.assertion.memory_id)
417        });
418        response.answer = response
419            .assertions
420            .first()
421            .map(|assertion| assertion.content.clone());
422        Ok(GovernedStateResolutionResponseV1 {
423            response,
424            decisions,
425        })
426    }
427
428    /// Governed graph traversal authorizes every fact endpoint before returning an edge.
429    pub async fn list_graph_edges_for_node_governed(
430        &self,
431        node_id: &str,
432        request: GovernedAccessRequestV1,
433    ) -> Result<GovernedGraphResponseV1, MemoryError> {
434        let edges = self.store.list_graph_edges_for_node(node_id).await?;
435        let mut admitted = Vec::new();
436        let mut decisions = Vec::new();
437        for edge in edges {
438            let mut edge_allowed = true;
439            for endpoint in [&edge.source, &edge.target] {
440                if let Some(fact_id) = endpoint.strip_prefix("fact:") {
441                    let decision = self
442                        .get_fact_governed(fact_id, request.clone())
443                        .await?
444                        .decision;
445                    edge_allowed &= decision.allowed;
446                    decisions.push(decision);
447                }
448            }
449            if edge_allowed {
450                admitted.push(edge);
451            }
452        }
453        Ok(GovernedGraphResponseV1 {
454            edges: admitted,
455            decisions,
456        })
457    }
458
459    /// Replay a durable receipt, then authorize every replay result for the current caller.
460    pub async fn replay_search_receipt_governed(
461        &self,
462        receipt_id: &str,
463        query: &str,
464        top_k: Option<usize>,
465        request: GovernedAccessRequestV1,
466    ) -> Result<GovernedReplayResponseV1, MemoryError> {
467        let request = request.with_purpose(crate::GovernedAccessPurposeV1::Replay);
468        let namespace = request.scope.namespace.clone();
469        let replay = self
470            .store
471            .replay_search_receipt(receipt_id, query, top_k, Some(&[namespace.as_str()]), None)
472            .await?;
473        let mut allowed_result_ids = Vec::new();
474        let mut decisions = Vec::new();
475        for result_id in &replay.replay_receipt.result_ids {
476            let decision = if let Some(fact_id) = result_id.strip_prefix("fact:") {
477                self.get_fact_governed(fact_id, request.clone())
478                    .await?
479                    .decision
480            } else {
481                decide(result_id, None, None, None, &request)
482            };
483            if decision.allowed {
484                allowed_result_ids.push(result_id.clone());
485            }
486            decisions.push(decision);
487        }
488        Ok(GovernedReplayResponseV1 {
489            replay,
490            allowed_result_ids,
491            decisions,
492        })
493    }
494
495    /// Append an origin revocation without mutating the immutable write-time label.
496    pub async fn revoke_origin(
497        &self,
498        permit: AuthorityPermit,
499        caller_idempotency_key: String,
500        fact_id: &str,
501        revocation_reference: String,
502    ) -> Result<crate::OriginAuthorityDecisionV1, MemoryError> {
503        if permit.capability != AuthorityPermit::REVOKE_ORIGIN_CAPABILITY
504            || permit.principal.trim().is_empty()
505            || caller_idempotency_key.trim().is_empty()
506            || revocation_reference.trim().is_empty()
507            || permit.origin_authority.is_none()
508        {
509            return Err(MemoryError::OriginAuthorityRejected {
510                principal: permit.principal,
511                reason: "revocation requires an origin-bound revoke capability and reference"
512                    .into(),
513            });
514        }
515        let fact_id = fact_id.to_string();
516        let principal = permit.principal.clone();
517        let request = GovernedAccessRequestV1::new(
518            &principal,
519            &principal,
520            crate::GovernedAccessPurposeV1::Recall,
521            "general",
522        );
523        self.store
524            .with_write_conn(move |conn| {
525                with_transaction(conn, |tx| {
526                    let existing: Option<(String, String)> = tx
527                        .query_row(
528                            "SELECT fact_id, revocation_reference FROM origin_authority_revocations
529                             WHERE caller_idempotency_key = ?1",
530                            params![caller_idempotency_key],
531                            |row| Ok((row.get(0)?, row.get(1)?)),
532                        )
533                        .optional()?;
534                    if let Some((existing_fact, existing_reference)) = existing {
535                        if existing_fact != fact_id || existing_reference != revocation_reference {
536                            return Err(MemoryError::AuthorityIdempotencyConflict {
537                                key: caller_idempotency_key,
538                            });
539                        }
540                    } else {
541                        let exists: bool = tx.query_row(
542                            "SELECT EXISTS(SELECT 1 FROM origin_authority_labels WHERE fact_id = ?1)",
543                            params![fact_id],
544                            |row| row.get(0),
545                        )?;
546                        if !exists {
547                            return Err(MemoryError::OriginAuthorityRejected {
548                                principal: principal.clone(),
549                                reason: "cannot revoke a fact without a canonical origin label".into(),
550                            });
551                        }
552                        tx.execute(
553                            "INSERT INTO origin_authority_revocations
554                             (revocation_id, fact_id, caller_idempotency_key, principal,
555                              revocation_reference, revoked_at)
556                             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
557                            params![
558                                uuid::Uuid::new_v4().to_string(),
559                                fact_id,
560                                caller_idempotency_key,
561                                principal,
562                                revocation_reference,
563                                Utc::now().format("%Y-%m-%d %H:%M:%S%.6f").to_string(),
564                            ],
565                        )?;
566                    }
567                    let fact = crate::knowledge::get_fact(tx, &fact_id)?;
568                    let origin = load_origin_record(tx, &fact_id)?;
569                    Ok(decide(
570                        &fact_id,
571                        fact.as_ref().map(|fact| fact.namespace.as_str()),
572                        origin.as_ref(),
573                        Some(&revocation_reference),
574                        &request,
575                    ))
576                })
577            })
578            .await
579    }
580
581    /// Install a one-shot fault for the next matching authority stage.
582    #[cfg(any(test, feature = "testing"))]
583    pub fn set_fault(&self, stage: Option<AuthorityFaultStage>) {
584        let mut guard = self
585            .store
586            .inner
587            .authority_fault
588            .lock()
589            .unwrap_or_else(|poisoned| poisoned.into_inner());
590        *guard = stage;
591    }
592
593    async fn mutate(
594        &self,
595        permit: AuthorityPermit,
596        caller_idempotency_key: String,
597        mutation: Mutation,
598    ) -> Result<AuthorityReceiptV1, MemoryError> {
599        validate_authority_request(&permit, &caller_idempotency_key, mutation.kind())?;
600
601        let prepared = match mutation.content_for_embedding() {
602            Some(content) => {
603                let (embedding, sparse, sparse_representation) =
604                    self.store.embed_text_with_sparse_internal(content).await?;
605                self.store.validate_embedding_dimensions(&embedding)?;
606                let embedding_bytes = crate::db::embedding_to_bytes(&embedding);
607                let q8_bytes = Quantizer::new(self.store.inner.config.embedding.dimensions)
608                    .quantize(&embedding)
609                    .map(|qv| quantize::pack_quantized(&qv))
610                    .ok();
611
612                Some(FactEmbedding {
613                    embedding: embedding_bytes,
614                    q8: q8_bytes,
615                    sparse,
616                    sparse_representation,
617                })
618            }
619            None => None,
620        };
621
622        let fault = self.store.inner.authority_fault.clone();
623        let result = self
624            .store
625            .with_write_conn(move |conn| {
626                execute_mutation(
627                    conn,
628                    &permit,
629                    &caller_idempotency_key,
630                    mutation,
631                    &fault,
632                    prepared,
633                )
634            })
635            .await?;
636        self.store.clear_search_cache();
637        Ok(result)
638    }
639}
640
641fn validate_authority_request(
642    permit: &AuthorityPermit,
643    caller_idempotency_key: &str,
644    kind: AuthorityOperationKind,
645) -> Result<(), MemoryError> {
646    if permit.principal.trim().is_empty()
647        || permit.caller_id.trim().is_empty()
648        || permit.capability != kind.capability()
649        || caller_idempotency_key.trim().is_empty()
650    {
651        return Err(MemoryError::AuthorityUnauthorized {
652            operation: kind.as_str().to_string(),
653            principal: permit.principal.clone(),
654        });
655    }
656    let append_admitted = match &permit.admission {
657        AuthorityAdmission::OperatorSystem => true,
658        AuthorityAdmission::Evidence { evidence_refs } => !evidence_refs.is_empty(),
659        AuthorityAdmission::Unspecified => false,
660    };
661    if kind == AuthorityOperationKind::Append && !append_admitted {
662        return Err(MemoryError::AuthorityAdmissionRejected {
663            principal: permit.principal.clone(),
664            reason: "authoritative append requires evidence or an operator/system permit".into(),
665        });
666    }
667    let origin =
668        permit
669            .origin_authority
670            .as_ref()
671            .ok_or_else(|| MemoryError::OriginAuthorityRejected {
672                principal: permit.principal.clone(),
673                reason: "governed canonical writes require an immutable origin label".into(),
674            })?;
675    if origin.origin_principal != permit.principal
676        || origin.schema_version != crate::origin_authority::ORIGIN_AUTHORITY_LABEL_V1
677        || origin.revocation_status != crate::RevocationStatusV1::Active
678        || label_digest(origin).is_err()
679    {
680        return Err(MemoryError::OriginAuthorityRejected {
681            principal: permit.principal.clone(),
682            reason: "origin label is inconsistent, inactive, or bound to another principal".into(),
683        });
684    }
685    Ok(())
686}
687
688#[derive(Debug)]
689enum Mutation {
690    Append {
691        namespace: String,
692        content: String,
693        source: Option<String>,
694    },
695    Supersede {
696        target_fact_id: String,
697        content: String,
698        source: Option<String>,
699    },
700    Redact {
701        target_fact_id: String,
702        reason: String,
703    },
704}
705
706impl Mutation {
707    fn content_for_embedding(&self) -> Option<&str> {
708        match self {
709            Self::Append { content, .. } => Some(content),
710            Self::Supersede { content, .. } => Some(content),
711            Self::Redact { .. } => None,
712        }
713    }
714
715    fn kind(&self) -> AuthorityOperationKind {
716        match self {
717            Self::Append { .. } => AuthorityOperationKind::Append,
718            Self::Supersede { .. } => AuthorityOperationKind::Supersede,
719            Self::Redact { .. } => AuthorityOperationKind::Redact,
720        }
721    }
722}
723
724impl AuthorityOperationKind {
725    fn as_str(self) -> &'static str {
726        match self {
727            Self::Append => "append",
728            Self::Supersede => "supersede",
729            Self::Redact => "redact",
730        }
731    }
732
733    fn capability(self) -> &'static str {
734        match self {
735            Self::Append => AuthorityPermit::APPEND_CAPABILITY,
736            Self::Supersede => AuthorityPermit::SUPERSEDE_CAPABILITY,
737            Self::Redact => AuthorityPermit::REDACT_CAPABILITY,
738        }
739    }
740}
741
742#[derive(Debug)]
743struct LineageTarget {
744    fact_id: String,
745    lineage_id: String,
746    namespace: String,
747    version: i64,
748}
749
750#[derive(Serialize)]
751struct Payload<'a> {
752    operation_kind: AuthorityOperationKind,
753    mutation: &'a MutationForDigest,
754    origin_label_digest: &'a str,
755}
756
757#[derive(Serialize)]
758#[serde(tag = "kind", rename_all = "snake_case")]
759enum MutationForDigest {
760    Append {
761        namespace: String,
762        content: String,
763        source: Option<String>,
764    },
765    Supersede {
766        target_fact_id: String,
767        content: String,
768        source: Option<String>,
769    },
770    Redact {
771        target_fact_id: String,
772        reason: String,
773    },
774}
775
776fn candidate_operation_kind(candidate: &MemoryTransitionCandidateV1) -> AuthorityOperationKind {
777    match candidate.operation {
778        TransitionOperation::Append { .. } => AuthorityOperationKind::Append,
779        TransitionOperation::Supersede { .. } => AuthorityOperationKind::Supersede,
780        TransitionOperation::Retract { .. } => AuthorityOperationKind::Redact,
781    }
782}
783
784fn candidate_content_for_embedding(candidate: &MemoryTransitionCandidateV1) -> Option<&str> {
785    let assertion_id = match &candidate.operation {
786        TransitionOperation::Append { assertion_id } => assertion_id,
787        TransitionOperation::Supersede { draft } => &draft.replacement_assertion_id,
788        TransitionOperation::Retract { .. } => return None,
789    };
790    candidate
791        .assertions
792        .iter()
793        .find(|assertion| &assertion.assertion_id == assertion_id)
794        .map(|assertion| assertion.content.as_str())
795}
796
797fn mutation_from_candidate(
798    candidate: &MemoryTransitionCandidateV1,
799    candidate_digest: &str,
800) -> Result<Mutation, MemoryError> {
801    let source = |spans: &[crate::SourceSpanRefV1]| {
802        serde_json::to_string(&serde_json::json!({
803            "memory_transition_candidate_id": candidate.candidate_id,
804            "candidate_digest": candidate_digest,
805            "source_spans": spans,
806        }))
807        .map(Some)
808        .map_err(|error| MemoryError::Other(format!("serialize transition source: {error}")))
809    };
810    match &candidate.operation {
811        TransitionOperation::Append { assertion_id } => {
812            let assertion = candidate
813                .assertions
814                .iter()
815                .find(|assertion| &assertion.assertion_id == assertion_id)
816                .ok_or_else(|| MemoryError::InvalidConfig {
817                    field: "transition.operation.assertion_id",
818                    reason: "referenced assertion draft is missing".into(),
819                })?;
820            Ok(Mutation::Append {
821                namespace: assertion.namespace.clone(),
822                content: assertion.content.clone(),
823                source: source(&assertion.source_spans)?,
824            })
825        }
826        TransitionOperation::Supersede { draft } => {
827            let assertion = candidate
828                .assertions
829                .iter()
830                .find(|assertion| assertion.assertion_id == draft.replacement_assertion_id)
831                .ok_or_else(|| MemoryError::InvalidConfig {
832                    field: "transition.operation.replacement_assertion_id",
833                    reason: "referenced replacement assertion draft is missing".into(),
834                })?;
835            Ok(Mutation::Supersede {
836                target_fact_id: draft.target_fact_id.clone(),
837                content: assertion.content.clone(),
838                source: source(&assertion.source_spans)?,
839            })
840        }
841        TransitionOperation::Retract {
842            target_fact_id,
843            reason,
844            ..
845        } => Ok(Mutation::Redact {
846            target_fact_id: target_fact_id.clone(),
847            reason: reason.clone(),
848        }),
849    }
850}
851
852fn execute_compiled_transition(
853    conn: &Connection,
854    permit: &AuthorityPermit,
855    key: &str,
856    candidate: MemoryTransitionCandidateV1,
857    fault: &Arc<Mutex<Option<AuthorityFaultStage>>>,
858    prepared: Option<FactEmbedding>,
859) -> Result<MemoryTransitionOutcomeV1, MemoryError> {
860    let candidate_digest = transition_digest(&candidate)?;
861    // Safety: verification, quarantine/evidence persistence, and the existing canonical authority
862    // mutation share this transaction, so no verified partial transition can become observable.
863    with_transaction(conn, |tx| {
864        if let Some(record) = get_transition_record(tx, key)? {
865            if record.candidate_digest != candidate_digest
866                || record.principal != permit.principal
867                || record.caller_id != permit.caller_id
868            {
869                return Err(MemoryError::AuthorityIdempotencyConflict {
870                    key: key.to_string(),
871                });
872            }
873            return outcome_from_record(tx, record);
874        }
875
876        let verification = verify_candidate(tx, &candidate)?;
877        if verification.candidate_digest != candidate_digest {
878            return Err(MemoryError::DigestError(
879                "transition verifier candidate digest drift".into(),
880            ));
881        }
882        if verification.disposition == TransitionDisposition::Quarantine {
883            let record = build_transition_record(key, permit, candidate, verification, None);
884            insert_transition_record(tx, &record)?;
885            return Ok(MemoryTransitionOutcomeV1::Quarantined { record });
886        }
887
888        let mutation = mutation_from_candidate(&candidate, &candidate_digest)?;
889        let authority_receipt = execute_mutation_tx(tx, permit, key, mutation, fault, prepared)?;
890        let record = build_transition_record(
891            key,
892            permit,
893            candidate,
894            verification.clone(),
895            Some(authority_receipt.receipt_id.clone()),
896        );
897        insert_transition_record(tx, &record)?;
898        Ok(MemoryTransitionOutcomeV1::Committed {
899            record,
900            verification,
901            authority_receipt,
902        })
903    })
904}
905
906fn build_transition_record(
907    key: &str,
908    permit: &AuthorityPermit,
909    candidate: MemoryTransitionCandidateV1,
910    verification: MemoryTransitionVerificationV1,
911    authority_receipt_id: Option<String>,
912) -> MemoryTransitionRecordV1 {
913    MemoryTransitionRecordV1 {
914        schema_version: MEMORY_TRANSITION_RECORD_V1.into(),
915        record_id: uuid::Uuid::new_v4().to_string(),
916        caller_idempotency_key: key.to_string(),
917        principal: permit.principal.clone(),
918        caller_id: permit.caller_id.clone(),
919        candidate_digest: verification.candidate_digest.clone(),
920        disposition: verification.disposition,
921        candidate,
922        verification,
923        authority_receipt_id,
924        created_at: Utc::now().format("%Y-%m-%d %H:%M:%S%.6f").to_string(),
925    }
926}
927
928fn insert_transition_record(
929    tx: &Transaction<'_>,
930    record: &MemoryTransitionRecordV1,
931) -> Result<(), MemoryError> {
932    let candidate_json = serde_json::to_string(&record.candidate)
933        .map_err(|error| MemoryError::Other(format!("serialize transition candidate: {error}")))?;
934    let verification_json = serde_json::to_string(&record.verification).map_err(|error| {
935        MemoryError::Other(format!("serialize transition verification: {error}"))
936    })?;
937    tx.execute(
938        "INSERT INTO memory_transition_records
939         (record_id, caller_idempotency_key, principal, caller_id, candidate_digest,
940          candidate_json, verification_json, disposition, authority_receipt_id, created_at)
941         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
942        params![
943            record.record_id,
944            record.caller_idempotency_key,
945            record.principal,
946            record.caller_id,
947            record.candidate_digest,
948            candidate_json,
949            verification_json,
950            match record.disposition {
951                TransitionDisposition::Commit => "commit",
952                TransitionDisposition::Quarantine => "quarantine",
953            },
954            record.authority_receipt_id,
955            record.created_at,
956        ],
957    )?;
958    Ok(())
959}
960
961fn outcome_from_record(
962    conn: &Connection,
963    record: MemoryTransitionRecordV1,
964) -> Result<MemoryTransitionOutcomeV1, MemoryError> {
965    match record.disposition {
966        TransitionDisposition::Quarantine => Ok(MemoryTransitionOutcomeV1::Quarantined { record }),
967        TransitionDisposition::Commit => {
968            let receipt_id =
969                record
970                    .authority_receipt_id
971                    .as_deref()
972                    .ok_or_else(|| MemoryError::CorruptData {
973                        table: "memory_transition_records",
974                        row_id: record.record_id.clone(),
975                        detail: "committed transition is missing authority receipt ID".into(),
976                    })?;
977            let receipt_json: String = conn.query_row(
978                "SELECT receipt_json FROM authority_receipts WHERE receipt_id = ?1",
979                params![receipt_id],
980                |row| row.get(0),
981            )?;
982            let authority_receipt =
983                serde_json::from_str(&receipt_json).map_err(|error| MemoryError::CorruptData {
984                    table: "authority_receipts",
985                    row_id: receipt_id.to_string(),
986                    detail: format!("invalid stored receipt: {error}"),
987                })?;
988            Ok(MemoryTransitionOutcomeV1::Committed {
989                verification: record.verification.clone(),
990                record,
991                authority_receipt,
992            })
993        }
994    }
995}
996
997fn execute_mutation(
998    conn: &Connection,
999    permit: &AuthorityPermit,
1000    key: &str,
1001    mutation: Mutation,
1002    fault: &Arc<Mutex<Option<AuthorityFaultStage>>>,
1003    prepared: Option<FactEmbedding>,
1004) -> Result<AuthorityReceiptV1, MemoryError> {
1005    // Safety: this closure owns every canonical authority write and only commits after the
1006    // mutation journal, epoch, lineage, and receipt are complete.
1007    with_transaction(conn, |tx| {
1008        execute_mutation_tx(tx, permit, key, mutation, fault, prepared)
1009    })
1010}
1011
1012fn execute_mutation_tx(
1013    tx: &Transaction<'_>,
1014    permit: &AuthorityPermit,
1015    key: &str,
1016    mutation: Mutation,
1017    fault: &Arc<Mutex<Option<AuthorityFaultStage>>>,
1018    mut prepared: Option<FactEmbedding>,
1019) -> Result<AuthorityReceiptV1, MemoryError> {
1020    let kind = mutation.kind();
1021    let origin_label = effective_origin_label(tx, permit, &mutation)?;
1022    let origin_label_digest = label_digest(&origin_label).map_err(MemoryError::DigestError)?;
1023    let payload_digest = payload_digest(&mutation, &origin_label_digest)?;
1024    let operation = kind.as_str().to_string();
1025
1026    if let Some(existing) = existing_operation(tx, key)? {
1027        if existing.payload_digest != payload_digest
1028            || existing.principal != permit.principal
1029            || existing.caller_id != permit.caller_id
1030            || existing.operation_kind != operation
1031        {
1032            return Err(MemoryError::AuthorityIdempotencyConflict {
1033                key: key.to_string(),
1034            });
1035        }
1036        let receipt: AuthorityReceiptV1 =
1037            serde_json::from_str(&existing.receipt_json).map_err(|e| MemoryError::CorruptData {
1038                table: "authority_receipts",
1039                row_id: key.to_string(),
1040                detail: format!("invalid stored receipt: {e}"),
1041            })?;
1042        return Ok(receipt);
1043    }
1044
1045    verify_all_lineages(tx)?;
1046    let before_epoch = current_epoch(tx)?;
1047    let before_snapshot_id = snapshot_id(tx, before_epoch)?;
1048    let operation_id = uuid::Uuid::new_v4().to_string();
1049    let content_digest = mutation_content_digest(&mutation)?;
1050
1051    fault_gate(fault, AuthorityFaultStage::BeforeAppend)?;
1052    let requires_embedding = matches!(
1053        mutation.kind(),
1054        AuthorityOperationKind::Append | AuthorityOperationKind::Supersede
1055    );
1056    let prepared = if requires_embedding {
1057        Some(prepared.take().ok_or_else(|| {
1058            MemoryError::Other("governed write is missing precomputed embedding".to_string())
1059        })?)
1060    } else {
1061        None
1062    };
1063    let (fact_id, lineage_id, target) =
1064        append_fact(tx, &mutation, &operation_id, &content_digest, prepared)?;
1065    persist_origin_label(tx, &fact_id, &origin_label, &origin_label_digest)?;
1066    fault_gate(fault, AuthorityFaultStage::AfterAppend)?;
1067
1068    fault_gate(fault, AuthorityFaultStage::BeforeLineage)?;
1069    apply_lineage_transition(
1070        tx,
1071        &mutation,
1072        &operation_id,
1073        &fact_id,
1074        &lineage_id,
1075        target.as_ref(),
1076        &content_digest,
1077        before_epoch,
1078    )?;
1079    fault_gate(fault, AuthorityFaultStage::AfterLineage)?;
1080    verify_all_lineages(tx)?;
1081
1082    let after_epoch = before_epoch
1083        .checked_add(1)
1084        .ok_or_else(|| MemoryError::Other("authority retrieval epoch overflow".to_string()))?;
1085    let affected_ids = affected_ids(&fact_id, &lineage_id, target.as_ref());
1086    let affected_json = serde_json::to_string(&affected_ids)
1087        .map_err(|e| MemoryError::Other(format!("serialize affected IDs: {e}")))?;
1088    let committed_at = Utc::now().format("%Y-%m-%d %H:%M:%S%.6f").to_string();
1089
1090    fault_gate(fault, AuthorityFaultStage::BeforeJournal)?;
1091    tx.execute(
1092        "INSERT INTO operation_journal
1093             (operation_id, caller_idempotency_key, operation_kind, payload_digest,
1094              principal, caller_id, before_epoch, after_epoch, affected_ids_json,
1095              content_digest, committed_at)
1096             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
1097        params![
1098            operation_id,
1099            key,
1100            operation,
1101            payload_digest,
1102            permit.principal,
1103            permit.caller_id,
1104            before_epoch as i64,
1105            after_epoch as i64,
1106            affected_json,
1107            content_digest,
1108            committed_at,
1109        ],
1110    )?;
1111    fault_gate(fault, AuthorityFaultStage::AfterJournal)?;
1112
1113    fault_gate(fault, AuthorityFaultStage::BeforeEpoch)?;
1114    let changed = tx.execute(
1115        "UPDATE authority_state SET retrieval_epoch = ?1 WHERE id = 1 AND retrieval_epoch = ?2",
1116        params![after_epoch as i64, before_epoch as i64],
1117    )?;
1118    if changed != 1 {
1119        return Err(MemoryError::Other(
1120            "authority retrieval epoch changed concurrently".to_string(),
1121        ));
1122    }
1123    tx.execute(
1124        "UPDATE authority_lineages SET updated_epoch = ?1 WHERE lineage_id = ?2",
1125        params![after_epoch as i64, lineage_id],
1126    )?;
1127    fault_gate(fault, AuthorityFaultStage::AfterEpoch)?;
1128
1129    let after_snapshot_id = snapshot_id(tx, after_epoch)?;
1130    let receipt_id = uuid::Uuid::new_v4().to_string();
1131    let mut receipt = AuthorityReceiptV1 {
1132        schema_version: RECEIPT_SCHEMA.to_string(),
1133        receipt_id,
1134        operation_id,
1135        caller_idempotency_key: key.to_string(),
1136        principal: permit.principal.clone(),
1137        caller_id: permit.caller_id.clone(),
1138        operation_kind: kind,
1139        before_snapshot_id,
1140        after_snapshot_id,
1141        before_epoch: RetrievalEpoch(before_epoch),
1142        after_epoch: RetrievalEpoch(after_epoch),
1143        affected_ids,
1144        content_digest,
1145        origin_label_digest: Some(origin_label_digest),
1146        receipt_digest: String::new(),
1147        committed_at,
1148    };
1149    receipt.receipt_digest = digest_serialized(&receipt_without_digest(&receipt)?)?;
1150    let receipt_json = serde_json::to_string(&receipt)
1151        .map_err(|e| MemoryError::Other(format!("serialize authority receipt: {e}")))?;
1152
1153    fault_gate(fault, AuthorityFaultStage::BeforeReceipt)?;
1154    tx.execute(
1155            "INSERT INTO authority_receipts
1156             (receipt_id, operation_id, caller_idempotency_key, receipt_json, receipt_digest, created_at)
1157             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
1158            params![
1159                receipt.receipt_id,
1160                receipt.operation_id,
1161                receipt.caller_idempotency_key,
1162                receipt_json,
1163                receipt.receipt_digest,
1164                receipt.committed_at,
1165            ],
1166        )?;
1167    fault_gate(fault, AuthorityFaultStage::AfterReceipt)?;
1168    Ok(receipt)
1169}
1170
1171fn append_fact(
1172    tx: &Transaction<'_>,
1173    mutation: &Mutation,
1174    operation_id: &str,
1175    content_digest: &str,
1176    prepared: Option<FactEmbedding>,
1177) -> Result<(String, String, Option<LineageTarget>), MemoryError> {
1178    let (namespace, content, source, target) = match mutation {
1179        Mutation::Append {
1180            namespace,
1181            content,
1182            source,
1183        } => (namespace.clone(), content.clone(), source.clone(), None),
1184        Mutation::Supersede {
1185            target_fact_id,
1186            content,
1187            source,
1188        } => {
1189            let target = load_active_target(tx, target_fact_id)?;
1190            (
1191                target.namespace.clone(),
1192                content.clone(),
1193                source.clone(),
1194                Some(target),
1195            )
1196        }
1197        Mutation::Redact {
1198            target_fact_id,
1199            reason: _,
1200        } => {
1201            let target = load_active_target(tx, target_fact_id)?;
1202            (
1203                target.namespace.clone(),
1204                REDACTED_CONTENT.to_string(),
1205                None,
1206                Some(target),
1207            )
1208        }
1209    };
1210    let source = source.as_deref();
1211    if namespace.trim().is_empty() || content.is_empty() {
1212        return Err(MemoryError::InvalidConfig {
1213            field: "authority.fact",
1214            reason: "namespace and content must not be empty".to_string(),
1215        });
1216    }
1217
1218    let fact_id = uuid::Uuid::new_v4().to_string();
1219    let lineage_id = target
1220        .as_ref()
1221        .map(|value| value.lineage_id.clone())
1222        .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
1223
1224    match mutation.kind() {
1225        AuthorityOperationKind::Append | AuthorityOperationKind::Supersede => {
1226            let prepared = prepared.ok_or_else(|| {
1227                MemoryError::Other("governed write is missing precomputed embedding".to_string())
1228            })?;
1229            if prepared.embedding.is_empty() {
1230                return Err(MemoryError::Other(
1231                    "governed write produced empty embedding bytes".to_string(),
1232                ));
1233            }
1234            let metadata = serde_json::json!({
1235                "authority": {
1236                    "operation_id": operation_id,
1237                    "lineage_id": lineage_id,
1238                    "content_digest": content_digest,
1239                }
1240            });
1241            crate::knowledge::insert_fact_in_tx(
1242                tx,
1243                &fact_id,
1244                &namespace,
1245                &content,
1246                &prepared.embedding,
1247                prepared.q8.as_deref(),
1248                source,
1249                Some(&metadata),
1250            )?;
1251            if let Some((weights, representation)) = prepared
1252                .sparse
1253                .as_ref()
1254                .zip(prepared.sparse_representation.as_deref())
1255            {
1256                crate::db::store_sparse_vector(
1257                    tx,
1258                    &format!("fact:{fact_id}"),
1259                    weights,
1260                    representation,
1261                )?;
1262            }
1263        }
1264        AuthorityOperationKind::Redact => {
1265            let metadata = serde_json::json!({
1266                "authority": {
1267                    "operation_id": operation_id,
1268                    "lineage_id": lineage_id,
1269                    "content_digest": content_digest,
1270                }
1271            })
1272            .to_string();
1273            tx.execute(
1274                "INSERT INTO facts (id, namespace, content, source, embedding, metadata)
1275                 VALUES (?1, ?2, ?3, ?4, NULL, ?5)",
1276                params![fact_id, namespace, content, source, metadata],
1277            )?;
1278            tx.execute(
1279                "INSERT INTO facts_rowid_map (fact_id) VALUES (?1)",
1280                params![fact_id],
1281            )?;
1282            let fts_rowid = tx.last_insert_rowid();
1283            tx.execute(
1284                "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
1285                params![fts_rowid, content],
1286            )?;
1287        }
1288    }
1289    Ok((fact_id, lineage_id, target))
1290}
1291
1292#[derive(Debug)]
1293struct FactEmbedding {
1294    embedding: Vec<u8>,
1295    q8: Option<Vec<u8>>,
1296    sparse: Option<crate::SparseWeights>,
1297    sparse_representation: Option<String>,
1298}
1299
1300fn apply_lineage_transition(
1301    tx: &Transaction<'_>,
1302    mutation: &Mutation,
1303    operation_id: &str,
1304    fact_id: &str,
1305    lineage_id: &str,
1306    target: Option<&LineageTarget>,
1307    content_digest: &str,
1308    before_epoch: u64,
1309) -> Result<(), MemoryError> {
1310    let kind = mutation.kind().as_str();
1311    if let Some(target) = target {
1312        let relation = if matches!(mutation, Mutation::Redact { .. }) {
1313            "redacts"
1314        } else {
1315            "supersedes"
1316        };
1317        let recorded_at = Utc::now().format("%Y-%m-%d %H:%M:%S%.6f").to_string();
1318        let edge_type = serde_json::json!({"type": "entity", "relation": relation}).to_string();
1319        let edge_metadata = serde_json::json!({
1320            "operation_id": operation_id,
1321            "lineage_id": lineage_id,
1322        })
1323        .to_string();
1324        let edge_digest = digest_serialized(&(
1325            format!("fact:{fact_id}"),
1326            format!("fact:{}", target_fact_id(target)),
1327            edge_type.clone(),
1328            edge_metadata.clone(),
1329        ))?;
1330        tx.execute(
1331            "INSERT INTO graph_edges
1332             (id, source, target, edge_type, weight, metadata, content_digest,
1333              recorded_at, valid_time, recorded_time)
1334             VALUES (?1, ?2, ?3, ?4, 1.0, ?5, ?6, ?7, ?7, ?7)",
1335            params![
1336                uuid::Uuid::new_v4().to_string(),
1337                format!("fact:{fact_id}"),
1338                format!("fact:{}", target_fact_id(target)),
1339                edge_type,
1340                edge_metadata,
1341                edge_digest,
1342                recorded_at,
1343            ],
1344        )?;
1345        tx.execute(
1346            "UPDATE authority_versions SET is_active = 0 WHERE fact_id = ?1 AND is_active = 1",
1347            params![target_fact_id(target)],
1348        )?;
1349        crate::db::delete_sparse_vector(tx, &format!("fact:{}", target_fact_id(target)))?;
1350        tx.execute(
1351            "INSERT INTO authority_versions
1352             (fact_id, lineage_id, version, operation_kind, is_active, is_redacted, content_digest)
1353             VALUES (?1, ?2, ?3, ?4, 1, ?5, ?6)",
1354            params![
1355                fact_id,
1356                lineage_id,
1357                target.version + 1,
1358                kind,
1359                if matches!(mutation, Mutation::Redact { .. }) {
1360                    1
1361                } else {
1362                    0
1363                },
1364                content_digest,
1365            ],
1366        )?;
1367        tx.execute(
1368            "UPDATE authority_lineages SET active_head_id = ?1 WHERE lineage_id = ?2",
1369            params![fact_id, lineage_id],
1370        )?;
1371    } else {
1372        tx.execute(
1373            "INSERT INTO authority_lineages (lineage_id, active_head_id, updated_epoch)
1374             VALUES (?1, ?2, ?3)",
1375            params![lineage_id, fact_id, before_epoch as i64],
1376        )?;
1377        tx.execute(
1378            "INSERT INTO authority_versions
1379             (fact_id, lineage_id, version, operation_kind, is_active, is_redacted, content_digest)
1380             VALUES (?1, ?2, 1, ?3, 1, 0, ?4)",
1381            params![fact_id, lineage_id, kind, content_digest],
1382        )?;
1383    }
1384    verify_lineage(tx, lineage_id)
1385}
1386
1387fn target_fact_id(target: &LineageTarget) -> &str {
1388    &target.fact_id
1389}
1390
1391fn load_active_target(tx: &Transaction<'_>, fact_id: &str) -> Result<LineageTarget, MemoryError> {
1392    let target: Option<(String, String, i64, i64, String)> = tx
1393        .query_row(
1394            "SELECT av.lineage_id, f.namespace, av.version, av.is_active, al.active_head_id
1395             FROM authority_versions av
1396             JOIN facts f ON f.id = av.fact_id
1397             JOIN authority_lineages al ON al.lineage_id = av.lineage_id
1398             WHERE av.fact_id = ?1",
1399            params![fact_id],
1400            |row| {
1401                Ok((
1402                    row.get(0)?,
1403                    row.get(1)?,
1404                    row.get(2)?,
1405                    row.get(3)?,
1406                    row.get(4)?,
1407                ))
1408            },
1409        )
1410        .optional()?;
1411    let Some((lineage_id, namespace, version, is_active, active_head_id)) = target else {
1412        return Err(MemoryError::FactNotFound(fact_id.to_string()));
1413    };
1414    if is_active != 1 || active_head_id != fact_id {
1415        return Err(MemoryError::AuthorityLineageInconsistent {
1416            lineage_id,
1417            detail: "target is not the active head".to_string(),
1418        });
1419    }
1420    verify_lineage(tx, &lineage_id)?;
1421    Ok(LineageTarget {
1422        fact_id: fact_id.to_string(),
1423        lineage_id,
1424        namespace,
1425        version,
1426    })
1427}
1428
1429fn verify_lineage(tx: &Transaction<'_>, lineage_id: &str) -> Result<(), MemoryError> {
1430    let (active_count, active_head, stored_head): (i64, Option<String>, Option<String>) = tx
1431        .query_row(
1432            "SELECT
1433                 (SELECT COUNT(*) FROM authority_versions WHERE lineage_id = ?1 AND is_active = 1),
1434                 (SELECT fact_id FROM authority_versions WHERE lineage_id = ?1 AND is_active = 1),
1435                 (SELECT active_head_id FROM authority_lineages WHERE lineage_id = ?1)",
1436            params![lineage_id],
1437            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1438        )?;
1439    if active_count != 1 || active_head.is_none() || active_head != stored_head {
1440        return Err(MemoryError::AuthorityLineageInconsistent {
1441            lineage_id: lineage_id.to_string(),
1442            detail: format!(
1443                "expected one active version matching stored head; count={active_count}, active={active_head:?}, stored={stored_head:?}"
1444            ),
1445        });
1446    }
1447    Ok(())
1448}
1449
1450fn verify_all_lineages(tx: &Transaction<'_>) -> Result<(), MemoryError> {
1451    let mut stmt = tx.prepare("SELECT lineage_id FROM authority_lineages ORDER BY lineage_id")?;
1452    let lineage_ids: Vec<String> = stmt
1453        .query_map([], |row| row.get(0))?
1454        .collect::<Result<Vec<_>, _>>()?;
1455    for lineage_id in lineage_ids {
1456        verify_lineage(tx, &lineage_id)?;
1457    }
1458    Ok(())
1459}
1460
1461fn current_epoch(tx: &Transaction<'_>) -> Result<u64, MemoryError> {
1462    let value: i64 = tx.query_row(
1463        "SELECT retrieval_epoch FROM authority_state WHERE id = 1",
1464        [],
1465        |row| row.get(0),
1466    )?;
1467    u64::try_from(value).map_err(|_| MemoryError::Other("negative authority epoch".to_string()))
1468}
1469
1470fn snapshot_id(tx: &Transaction<'_>, epoch: u64) -> Result<AuthoritySnapshotId, MemoryError> {
1471    let mut stmt = tx
1472        .prepare("SELECT lineage_id, active_head_id FROM authority_lineages ORDER BY lineage_id")?;
1473    let heads: Vec<(String, String)> = stmt
1474        .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
1475        .collect::<Result<Vec<_>, _>>()?;
1476    let digest = digest_serialized(&(epoch, heads))?;
1477    Ok(AuthoritySnapshotId(format!("epoch:{epoch}:{digest}")))
1478}
1479
1480fn affected_ids(fact_id: &str, lineage_id: &str, target: Option<&LineageTarget>) -> Vec<String> {
1481    let mut ids = vec![fact_id.to_string(), lineage_id.to_string()];
1482    if let Some(target) = target {
1483        ids.push(target_fact_id(target).to_string());
1484    }
1485    ids
1486}
1487
1488fn payload_digest(mutation: &Mutation, origin_label_digest: &str) -> Result<String, MemoryError> {
1489    let digest_mutation = match mutation {
1490        Mutation::Append {
1491            namespace,
1492            content,
1493            source,
1494        } => MutationForDigest::Append {
1495            namespace: namespace.clone(),
1496            content: content.clone(),
1497            source: source.clone(),
1498        },
1499        Mutation::Supersede {
1500            target_fact_id,
1501            content,
1502            source,
1503        } => MutationForDigest::Supersede {
1504            target_fact_id: target_fact_id.clone(),
1505            content: content.clone(),
1506            source: source.clone(),
1507        },
1508        Mutation::Redact {
1509            target_fact_id,
1510            reason,
1511        } => MutationForDigest::Redact {
1512            target_fact_id: target_fact_id.clone(),
1513            reason: reason.clone(),
1514        },
1515    };
1516    digest_serialized(&Payload {
1517        operation_kind: mutation.kind(),
1518        mutation: &digest_mutation,
1519        origin_label_digest,
1520    })
1521}
1522
1523fn effective_origin_label(
1524    tx: &Transaction<'_>,
1525    permit: &AuthorityPermit,
1526    mutation: &Mutation,
1527) -> Result<OriginAuthorityLabelV1, MemoryError> {
1528    let proposed =
1529        permit
1530            .origin_authority
1531            .clone()
1532            .ok_or_else(|| MemoryError::OriginAuthorityRejected {
1533                principal: permit.principal.clone(),
1534                reason: "canonical write has no origin label".into(),
1535            })?;
1536    let target_id = match mutation {
1537        Mutation::Append { namespace, .. } => {
1538            return proposed
1539                .bind_resource_scope(crate::NamespaceScopeV1::exact(namespace))
1540                .map_err(|reason| MemoryError::OriginAuthorityRejected {
1541                    principal: permit.principal.clone(),
1542                    reason,
1543                })
1544        }
1545        Mutation::Supersede { target_fact_id, .. } | Mutation::Redact { target_fact_id, .. } => {
1546            target_fact_id
1547        }
1548    };
1549    let target =
1550        load_origin_record(tx, target_id)?.ok_or_else(|| MemoryError::OriginAuthorityRejected {
1551            principal: permit.principal.clone(),
1552            reason: format!("target '{target_id}' has no canonical origin label"),
1553        })?;
1554    // A replacement inherits the target's immutable resource scope. A caller cannot use a
1555    // freshly supplied, broader label to widen a lineage during supersession or redaction.
1556    let proposed = proposed
1557        .bind_resource_scope(target.label.resource_scope.clone())
1558        .map_err(|reason| MemoryError::OriginAuthorityRejected {
1559            principal: permit.principal.clone(),
1560            reason,
1561        })?;
1562    let content_digest = mutation_content_digest(mutation)?;
1563    OriginAuthorityLabelV1::derive(
1564        &[target.label, proposed],
1565        OriginDerivationKindV1::Other,
1566        content_digest,
1567    )
1568    .map_err(|reason| MemoryError::OriginAuthorityRejected {
1569        principal: permit.principal.clone(),
1570        reason,
1571    })
1572}
1573
1574fn persist_origin_label(
1575    tx: &Transaction<'_>,
1576    fact_id: &str,
1577    label: &OriginAuthorityLabelV1,
1578    label_digest: &str,
1579) -> Result<(), MemoryError> {
1580    let label_json = serde_json::to_string(label)
1581        .map_err(|error| MemoryError::Other(format!("serialize origin label: {error}")))?;
1582    tx.execute(
1583        "INSERT INTO origin_authority_labels (fact_id, label_json, label_digest, recorded_at)
1584         VALUES (?1, ?2, ?3, ?4)",
1585        params![
1586            fact_id,
1587            label_json,
1588            label_digest,
1589            Utc::now().format("%Y-%m-%d %H:%M:%S%.6f").to_string(),
1590        ],
1591    )?;
1592    Ok(())
1593}
1594
1595fn load_origin_record(
1596    conn: &Connection,
1597    fact_id: &str,
1598) -> Result<Option<OriginAuthorityRecordV1>, MemoryError> {
1599    let row: Option<(String, String, String)> = conn
1600        .query_row(
1601            "SELECT label_json, label_digest, recorded_at FROM origin_authority_labels
1602             WHERE fact_id = ?1",
1603            params![fact_id],
1604            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1605        )
1606        .optional()?;
1607    row.map(|(json, label_digest, recorded_at)| {
1608        let label = serde_json::from_str(&json).map_err(|error| MemoryError::CorruptData {
1609            table: "origin_authority_labels",
1610            row_id: fact_id.into(),
1611            detail: format!("invalid origin label: {error}"),
1612        })?;
1613        Ok(OriginAuthorityRecordV1 {
1614            fact_id: fact_id.into(),
1615            label,
1616            label_digest,
1617            recorded_at,
1618        })
1619    })
1620    .transpose()
1621}
1622
1623fn governed_fact_access(
1624    conn: &Connection,
1625    fact_id: &str,
1626    request: &GovernedAccessRequestV1,
1627) -> Result<GovernedFactAccessV1, MemoryError> {
1628    let fact = crate::knowledge::get_fact(conn, fact_id)?;
1629    let origin = load_origin_record(conn, fact_id)?;
1630    let revocation_reference: Option<String> = conn
1631        .query_row(
1632            "SELECT revocation_reference FROM origin_authority_revocations
1633             WHERE fact_id = ?1 ORDER BY revoked_at DESC LIMIT 1",
1634            params![fact_id],
1635            |row| row.get(0),
1636        )
1637        .optional()?;
1638    let decision = decide(
1639        fact_id,
1640        fact.as_ref().map(|fact| fact.namespace.as_str()),
1641        origin.as_ref(),
1642        revocation_reference.as_deref(),
1643        request,
1644    );
1645    Ok(GovernedFactAccessV1 {
1646        fact: decision.allowed.then_some(fact).flatten(),
1647        decision,
1648        origin,
1649    })
1650}
1651
1652fn mutation_content_digest(mutation: &Mutation) -> Result<String, MemoryError> {
1653    match mutation {
1654        Mutation::Append {
1655            namespace,
1656            content,
1657            source,
1658        } => digest_serialized(&(namespace, content, source)),
1659        Mutation::Supersede {
1660            target_fact_id,
1661            content,
1662            source,
1663        } => digest_serialized(&(target_fact_id, content, source)),
1664        Mutation::Redact {
1665            target_fact_id,
1666            reason,
1667        } => digest_serialized(&(REDACTED_CONTENT, target_fact_id, reason)),
1668    }
1669}
1670
1671fn digest_serialized<T: Serialize>(value: &T) -> Result<String, MemoryError> {
1672    let bytes = serde_json::to_vec(value)
1673        .map_err(|e| MemoryError::DigestError(format!("serialize digest input: {e}")))?;
1674    Ok(blake3::hash(&bytes).to_hex().to_string())
1675}
1676
1677fn receipt_without_digest(receipt: &AuthorityReceiptV1) -> Result<AuthorityReceiptV1, MemoryError> {
1678    let mut value = receipt.clone();
1679    value.receipt_digest.clear();
1680    Ok(value)
1681}
1682
1683#[derive(Debug)]
1684struct ExistingOperation {
1685    payload_digest: String,
1686    principal: String,
1687    caller_id: String,
1688    operation_kind: String,
1689    receipt_json: String,
1690}
1691
1692fn existing_operation(
1693    tx: &Transaction<'_>,
1694    key: &str,
1695) -> Result<Option<ExistingOperation>, MemoryError> {
1696    tx.query_row(
1697        "SELECT oj.payload_digest, oj.principal, oj.caller_id, oj.operation_kind,
1698                ar.receipt_json
1699         FROM operation_journal oj
1700         JOIN authority_receipts ar ON ar.operation_id = oj.operation_id
1701         WHERE oj.caller_idempotency_key = ?1",
1702        params![key],
1703        |row| {
1704            Ok(ExistingOperation {
1705                payload_digest: row.get(0)?,
1706                principal: row.get(1)?,
1707                caller_id: row.get(2)?,
1708                operation_kind: row.get(3)?,
1709                receipt_json: row.get(4)?,
1710            })
1711        },
1712    )
1713    .optional()
1714    .map_err(MemoryError::Database)
1715}
1716
1717fn get_receipt(
1718    conn: &Connection,
1719    field: &str,
1720    value: &str,
1721) -> Result<Option<AuthorityReceiptV1>, MemoryError> {
1722    let sql = format!("SELECT receipt_json FROM authority_receipts WHERE {field} = ?1");
1723    let json: Option<String> = conn
1724        .query_row(&sql, params![value], |row| row.get(0))
1725        .optional()?;
1726    json.map(|raw| {
1727        serde_json::from_str(&raw).map_err(|e| MemoryError::CorruptData {
1728            table: "authority_receipts",
1729            row_id: value.to_string(),
1730            detail: format!("invalid stored receipt: {e}"),
1731        })
1732    })
1733    .transpose()
1734}
1735
1736fn get_transition_record(
1737    conn: &Connection,
1738    key: &str,
1739) -> Result<Option<MemoryTransitionRecordV1>, MemoryError> {
1740    type TransitionRow = (
1741        String,
1742        String,
1743        String,
1744        String,
1745        String,
1746        String,
1747        String,
1748        Option<String>,
1749        String,
1750    );
1751    let row: Option<TransitionRow> = conn
1752        .query_row(
1753            "SELECT record_id, principal, caller_id, candidate_digest, candidate_json,
1754                    verification_json, disposition, authority_receipt_id, created_at
1755             FROM memory_transition_records WHERE caller_idempotency_key = ?1",
1756            params![key],
1757            |row| {
1758                Ok((
1759                    row.get(0)?,
1760                    row.get(1)?,
1761                    row.get(2)?,
1762                    row.get(3)?,
1763                    row.get(4)?,
1764                    row.get(5)?,
1765                    row.get(6)?,
1766                    row.get(7)?,
1767                    row.get(8)?,
1768                ))
1769            },
1770        )
1771        .optional()?;
1772    let Some((
1773        record_id,
1774        principal,
1775        caller_id,
1776        candidate_digest,
1777        candidate_json,
1778        verification_json,
1779        disposition,
1780        authority_receipt_id,
1781        created_at,
1782    )) = row
1783    else {
1784        return Ok(None);
1785    };
1786    let candidate =
1787        serde_json::from_str(&candidate_json).map_err(|error| MemoryError::CorruptData {
1788            table: "memory_transition_records",
1789            row_id: record_id.clone(),
1790            detail: format!("invalid candidate JSON: {error}"),
1791        })?;
1792    let verification =
1793        serde_json::from_str(&verification_json).map_err(|error| MemoryError::CorruptData {
1794            table: "memory_transition_records",
1795            row_id: record_id.clone(),
1796            detail: format!("invalid verification JSON: {error}"),
1797        })?;
1798    let disposition = match disposition.as_str() {
1799        "commit" => TransitionDisposition::Commit,
1800        "quarantine" => TransitionDisposition::Quarantine,
1801        other => {
1802            return Err(MemoryError::CorruptData {
1803                table: "memory_transition_records",
1804                row_id: record_id,
1805                detail: format!("invalid transition disposition '{other}'"),
1806            })
1807        }
1808    };
1809    Ok(Some(MemoryTransitionRecordV1 {
1810        schema_version: MEMORY_TRANSITION_RECORD_V1.into(),
1811        record_id,
1812        caller_idempotency_key: key.to_string(),
1813        principal,
1814        caller_id,
1815        candidate_digest,
1816        candidate,
1817        verification,
1818        disposition,
1819        authority_receipt_id,
1820        created_at,
1821    }))
1822}
1823
1824fn fault_gate(
1825    fault: &Arc<Mutex<Option<AuthorityFaultStage>>>,
1826    stage: AuthorityFaultStage,
1827) -> Result<(), MemoryError> {
1828    let mut guard = fault
1829        .lock()
1830        .unwrap_or_else(|poisoned| poisoned.into_inner());
1831    if *guard == Some(stage) {
1832        *guard = None;
1833        return Err(MemoryError::AuthorityFaultInjected { stage });
1834    }
1835    Ok(())
1836}