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