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