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