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