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