1use 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub(crate) async fn current_retrieval_epoch(&self) -> Result<RetrievalEpoch, MemoryError> {
329 Ok(self.current_state().await?.retrieval_epoch)
330 }
331
332 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 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 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 !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 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 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 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 #[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 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 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 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 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}