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