1use crate::authority_contracts::{
9 AuthoritySnapshotId, RetrievalEpoch, RetrievalWitnessV1, StageOutcomeV1,
10};
11use crate::knowledge::StateView;
12use crate::types::{GraphEdgeType, SearchResult, SearchSource};
13use crate::{MemoryError, MemoryStore};
14use chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16use std::collections::{BTreeMap, BTreeSet};
17
18pub const STATE_RESOLUTION_RECEIPT_V1: &str = "state_resolution_receipt_v1";
19pub const STATE_RESOLVED_RETRIEVAL_V1: &str = "state_resolved_retrieval_v1";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum StateResolutionMode {
25 Current,
26 HistoricalAt(String),
27 Transition { from: String, to: String },
28 Trajectory { points: Vec<String> },
29}
30
31impl StateResolutionMode {
32 pub fn historical_at(as_of: impl Into<String>) -> Self {
33 Self::HistoricalAt(as_of.into())
34 }
35
36 pub fn transition(from: impl Into<String>, to: impl Into<String>) -> Self {
37 Self::Transition {
38 from: from.into(),
39 to: to.into(),
40 }
41 }
42
43 pub fn trajectory(points: Vec<String>) -> Self {
44 Self::Trajectory { points }
45 }
46
47 pub fn state_view(&self) -> StateView {
49 match self {
50 Self::Current => StateView::Current,
51 Self::HistoricalAt(as_of) => StateView::HistoricalAt(as_of.clone()),
52 Self::Transition { .. } | Self::Trajectory { .. } => StateView::IncludeSuperseded,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum PremiseStatus {
61 Supported,
62 Stale,
63 Contradicted,
64 Unsupported,
65 Ambiguous,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71pub enum AnswerDisposition {
72 Answer,
73 CorrectPremise,
74 DiscloseConflict,
75 Abstain,
76 RequestEvidence,
77}
78
79pub type AnswerPolicy = AnswerDisposition;
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum DependencyState {
85 Valid,
86 Invalid,
87 Uncertain,
88 Pending,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case", tag = "kind")]
94pub enum StateDependencyEdgeV1 {
95 Invalidates {
96 source_fact_id: String,
97 target_fact_id: String,
98 },
99 Weakens {
100 source_fact_id: String,
101 target_fact_id: String,
102 },
103 RequiresReevaluation {
104 source_fact_id: String,
105 target_fact_id: String,
106 },
107 ScopeChanges {
108 source_fact_id: String,
109 target_fact_id: String,
110 from_scope: String,
111 to_scope: String,
112 },
113 DerivedFromState {
114 source_fact_id: String,
115 target_fact_id: String,
116 },
117}
118
119impl StateDependencyEdgeV1 {
120 pub fn invalidates(source: impl Into<String>, target: impl Into<String>) -> Self {
121 Self::Invalidates {
122 source_fact_id: source.into(),
123 target_fact_id: target.into(),
124 }
125 }
126
127 pub fn weakens(source: impl Into<String>, target: impl Into<String>) -> Self {
128 Self::Weakens {
129 source_fact_id: source.into(),
130 target_fact_id: target.into(),
131 }
132 }
133
134 pub fn requires_reevaluation(source: impl Into<String>, target: impl Into<String>) -> Self {
135 Self::RequiresReevaluation {
136 source_fact_id: source.into(),
137 target_fact_id: target.into(),
138 }
139 }
140
141 pub fn scope_changes(
142 source: impl Into<String>,
143 target: impl Into<String>,
144 from_scope: impl Into<String>,
145 to_scope: impl Into<String>,
146 ) -> Self {
147 Self::ScopeChanges {
148 source_fact_id: source.into(),
149 target_fact_id: target.into(),
150 from_scope: from_scope.into(),
151 to_scope: to_scope.into(),
152 }
153 }
154
155 pub fn derived_from_state(source: impl Into<String>, target: impl Into<String>) -> Self {
156 Self::DerivedFromState {
157 source_fact_id: source.into(),
158 target_fact_id: target.into(),
159 }
160 }
161
162 pub fn source(&self) -> &str {
163 match self {
164 Self::Invalidates { source_fact_id, .. }
165 | Self::Weakens { source_fact_id, .. }
166 | Self::RequiresReevaluation { source_fact_id, .. }
167 | Self::ScopeChanges { source_fact_id, .. }
168 | Self::DerivedFromState { source_fact_id, .. } => source_fact_id,
169 }
170 }
171
172 pub fn target(&self) -> &str {
173 match self {
174 Self::Invalidates { target_fact_id, .. }
175 | Self::Weakens { target_fact_id, .. }
176 | Self::RequiresReevaluation { target_fact_id, .. }
177 | Self::ScopeChanges { target_fact_id, .. }
178 | Self::DerivedFromState { target_fact_id, .. } => target_fact_id,
179 }
180 }
181
182 pub fn relation(&self) -> &'static str {
183 match self {
184 Self::Invalidates { .. } => "invalidates",
185 Self::Weakens { .. } => "weakens",
186 Self::RequiresReevaluation { .. } => "requires_reevaluation",
187 Self::ScopeChanges { .. } => "scope_changes",
188 Self::DerivedFromState { .. } => "derived_from_state",
189 }
190 }
191
192 pub fn digest(&self) -> String {
193 blake3::hash(&serde_json::to_vec(self).expect("state edge is serializable"))
194 .to_hex()
195 .to_string()
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct DependencyResolutionV1 {
201 pub states: BTreeMap<String, DependencyState>,
202 pub premise_status: PremiseStatus,
203 pub invalid_lineage: bool,
204 pub unresolved_conflict: bool,
205 pub budget_exhausted: bool,
206 pub visited_nodes: Vec<String>,
207}
208
209impl DependencyResolutionV1 {
210 pub fn status(&self, id: &str) -> Option<DependencyState> {
211 self.states.get(id).copied()
212 }
213}
214
215fn set_state(states: &mut BTreeMap<String, DependencyState>, id: &str, next: DependencyState) {
216 let rank = |state: DependencyState| match state {
217 DependencyState::Valid => 0,
218 DependencyState::Pending => 1,
219 DependencyState::Uncertain => 2,
220 DependencyState::Invalid => 3,
221 };
222 let replace = states
223 .get(id)
224 .map_or(true, |current| rank(next) > rank(*current));
225 if replace {
226 states.insert(id.to_string(), next);
227 }
228}
229
230fn has_cycle(edges: &[StateDependencyEdgeV1]) -> bool {
231 let mut adjacency: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
232 for edge in edges {
233 adjacency
234 .entry(edge.source())
235 .or_default()
236 .push(edge.target());
237 }
238 fn visit<'a>(
239 node: &'a str,
240 adjacency: &BTreeMap<&'a str, Vec<&'a str>>,
241 visiting: &mut BTreeSet<&'a str>,
242 visited: &mut BTreeSet<&'a str>,
243 ) -> bool {
244 if visiting.contains(node) {
245 return true;
246 }
247 if visited.contains(node) {
248 return false;
249 }
250 visiting.insert(node);
251 if let Some(next) = adjacency.get(node) {
252 if next
253 .iter()
254 .any(|child| visit(child, adjacency, visiting, visited))
255 {
256 return true;
257 }
258 }
259 visiting.remove(node);
260 visited.insert(node);
261 false
262 }
263 let mut visiting = BTreeSet::new();
264 let mut visited = BTreeSet::new();
265 adjacency
266 .keys()
267 .any(|node| visit(node, &adjacency, &mut visiting, &mut visited))
268}
269
270pub fn resolve_dependency_states(
272 edges: &[StateDependencyEdgeV1],
273 roots: &[String],
274 budget: usize,
275) -> DependencyResolutionV1 {
276 let mut states = BTreeMap::new();
277 let mut visited = BTreeSet::new();
278 for root in roots {
279 if !root.trim().is_empty() {
280 states.insert(root.clone(), DependencyState::Valid);
281 }
282 }
283 let mut invalid_lineage = has_cycle(edges);
284 let mut target_invalidators: BTreeMap<&str, usize> = BTreeMap::new();
285 for edge in edges {
286 if edge.source().trim().is_empty() || edge.target().trim().is_empty() {
287 invalid_lineage = true;
288 }
289 if matches!(edge, StateDependencyEdgeV1::Invalidates { .. }) {
290 *target_invalidators.entry(edge.target()).or_default() += 1;
291 }
292 }
293 let unresolved_conflict = target_invalidators.values().any(|count| *count > 1);
294 invalid_lineage |= unresolved_conflict;
295
296 if budget == 0 && !states.is_empty() {
297 return DependencyResolutionV1 {
298 states: states
299 .into_keys()
300 .map(|id| (id, DependencyState::Pending))
301 .collect(),
302 premise_status: PremiseStatus::Unsupported,
303 invalid_lineage,
304 unresolved_conflict,
305 budget_exhausted: true,
306 visited_nodes: Vec::new(),
307 };
308 }
309
310 let mut changed = true;
311 while changed {
312 changed = false;
313 for edge in edges {
314 if visited.len() >= budget && budget > 0 {
315 break;
316 }
317 let source_state = states.get(edge.source()).copied();
318 let target_state = states.get(edge.target()).copied();
319 if source_state.is_some() || target_state.is_some() {
320 visited.insert(edge.source().to_string());
321 visited.insert(edge.target().to_string());
322 }
323 let before = states.clone();
324 match edge {
325 StateDependencyEdgeV1::Invalidates { .. } => {
326 if source_state.is_some() || target_state.is_some() {
330 set_state(&mut states, edge.target(), DependencyState::Invalid);
331 }
332 }
333 StateDependencyEdgeV1::Weakens { .. }
334 | StateDependencyEdgeV1::RequiresReevaluation { .. }
335 | StateDependencyEdgeV1::ScopeChanges { .. } => {
336 if source_state.is_some() || target_state.is_some() {
337 set_state(&mut states, edge.target(), DependencyState::Uncertain);
338 }
339 }
340 StateDependencyEdgeV1::DerivedFromState { .. } => {
341 if let Some(state) = target_state {
342 set_state(&mut states, edge.source(), state);
343 }
344 }
345 }
346 changed |= before != states;
347 }
348 if budget > 0 && visited.len() >= budget {
349 break;
350 }
351 }
352 let budget_exhausted = budget > 0 && (visited.len() >= budget && edges.len() > visited.len());
353 let aggregate = if invalid_lineage || unresolved_conflict {
354 PremiseStatus::Ambiguous
355 } else if states.values().any(|s| *s == DependencyState::Invalid) {
356 PremiseStatus::Stale
359 } else if states.values().any(|s| *s == DependencyState::Uncertain) {
360 PremiseStatus::Ambiguous
361 } else if states.values().any(|s| *s == DependencyState::Pending) || budget_exhausted {
362 PremiseStatus::Unsupported
363 } else if states.is_empty() {
364 PremiseStatus::Unsupported
365 } else {
366 PremiseStatus::Supported
367 };
368 DependencyResolutionV1 {
369 states,
370 premise_status: aggregate,
371 invalid_lineage,
372 unresolved_conflict,
373 budget_exhausted,
374 visited_nodes: visited.into_iter().collect(),
375 }
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
379pub struct AnswerPolicyDecision {
380 pub disposition: AnswerDisposition,
381 pub confident_negative: bool,
382}
383
384pub fn answer_policy_for(
386 premise_status: PremiseStatus,
387 evidence_sufficient: bool,
388 unresolved_conflict: bool,
389 invalid_lineage: bool,
390 budget_exhausted: bool,
391) -> AnswerPolicyDecision {
392 let disposition =
393 if invalid_lineage || unresolved_conflict || premise_status == PremiseStatus::Ambiguous {
394 AnswerDisposition::DiscloseConflict
395 } else if !evidence_sufficient
396 || premise_status == PremiseStatus::Unsupported
397 || budget_exhausted
398 {
399 AnswerDisposition::RequestEvidence
400 } else {
401 match premise_status {
402 PremiseStatus::Supported => AnswerDisposition::Answer,
403 PremiseStatus::Stale => AnswerDisposition::CorrectPremise,
404 PremiseStatus::Contradicted => AnswerDisposition::DiscloseConflict,
405 PremiseStatus::Unsupported | PremiseStatus::Ambiguous => {
406 AnswerDisposition::RequestEvidence
407 }
408 }
409 };
410 AnswerPolicyDecision {
411 disposition,
412 confident_negative: false,
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
420pub struct ResolvedAssertionV1 {
421 pub schema_version: String,
422 pub memory_id: String,
423 pub content: String,
424 pub premise_status: PremiseStatus,
425 pub state_view: StateView,
426 pub source_kind: String,
427}
428
429#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
430pub struct BeliefAlternativeV1 {
431 pub assertion: ResolvedAssertionV1,
432 pub reason: String,
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct StateResolutionReceiptV1 {
437 pub schema_version: String,
438 pub receipt_id: String,
439 pub request_id: String,
440 pub state_view: StateView,
441 pub resolution_mode: StateResolutionMode,
442 pub premise_status: PremiseStatus,
443 pub answer_disposition: AnswerDisposition,
444 pub resolved_memory_ids: Vec<String>,
445 pub alternative_memory_ids: Vec<String>,
446 pub dependency_edge_digests: Vec<String>,
447 pub evidence_sufficient: bool,
448 pub unresolved_conflict: bool,
449 pub invalid_lineage: bool,
450 pub budget_exhausted: bool,
451 pub evaluated_at: String,
452 pub receipt_digest: String,
453 pub stage_outcomes: Vec<(String, StageOutcomeV1)>,
454 pub degradations: Vec<String>,
455}
456
457#[derive(Serialize)]
458struct ReceiptDigestInput<'a> {
459 schema_version: &'a str,
460 request_id: &'a str,
461 state_view: &'a StateView,
462 resolution_mode: &'a StateResolutionMode,
463 premise_status: PremiseStatus,
464 answer_disposition: AnswerDisposition,
465 resolved_memory_ids: &'a [String],
466 alternative_memory_ids: &'a [String],
467 dependency_edge_digests: &'a [String],
468 evidence_sufficient: bool,
469 unresolved_conflict: bool,
470 invalid_lineage: bool,
471 budget_exhausted: bool,
472}
473
474impl StateResolutionReceiptV1 {
475 fn digest_for(&self) -> Result<String, MemoryError> {
476 let input = ReceiptDigestInput {
477 schema_version: &self.schema_version,
478 request_id: &self.request_id,
479 state_view: &self.state_view,
480 resolution_mode: &self.resolution_mode,
481 premise_status: self.premise_status,
482 answer_disposition: self.answer_disposition,
483 resolved_memory_ids: &self.resolved_memory_ids,
484 alternative_memory_ids: &self.alternative_memory_ids,
485 dependency_edge_digests: &self.dependency_edge_digests,
486 evidence_sufficient: self.evidence_sufficient,
487 unresolved_conflict: self.unresolved_conflict,
488 invalid_lineage: self.invalid_lineage,
489 budget_exhausted: self.budget_exhausted,
490 };
491 Ok(blake3::hash(
492 &serde_json::to_vec(&input)
493 .map_err(|e| MemoryError::Other(format!("receipt serialization failed: {e}")))?,
494 )
495 .to_hex()
496 .to_string())
497 }
498
499 pub fn with_dependency_edge_digests(
500 mut self,
501 dependency_edge_digests: Vec<String>,
502 ) -> Result<Self, MemoryError> {
503 self.dependency_edge_digests = dependency_edge_digests;
504 self.receipt_digest = self.digest_for()?;
505 self.receipt_id = format!("state-resolution:{}", self.receipt_digest);
506 Ok(self)
507 }
508
509 #[allow(clippy::too_many_arguments)]
510 pub fn new_at(
511 request_id: impl Into<String>,
512 state_view: StateView,
513 resolution_mode: StateResolutionMode,
514 premise_status: PremiseStatus,
515 answer_disposition: AnswerDisposition,
516 resolved_memory_ids: Vec<String>,
517 alternative_memory_ids: Vec<String>,
518 evidence_sufficient: bool,
519 unresolved_conflict: bool,
520 invalid_lineage: bool,
521 budget_exhausted: bool,
522 evaluated_at: impl Into<String>,
523 ) -> Result<Self, MemoryError> {
524 let request_id = request_id.into();
525 let evaluated_at = evaluated_at.into();
526 if request_id.trim().is_empty() {
527 return Err(MemoryError::Other(
528 "state resolution request ID must not be empty".into(),
529 ));
530 }
531 DateTime::parse_from_rfc3339(&evaluated_at).map_err(|e| {
532 MemoryError::Other(format!(
533 "invalid state resolution timestamp '{evaluated_at}': {e}"
534 ))
535 })?;
536 validate_state_view(&state_view)?;
537 let schema_version = STATE_RESOLUTION_RECEIPT_V1.to_string();
538 let input = ReceiptDigestInput {
539 schema_version: &schema_version,
540 request_id: &request_id,
541 state_view: &state_view,
542 resolution_mode: &resolution_mode,
543 premise_status,
544 answer_disposition,
545 resolved_memory_ids: &resolved_memory_ids,
546 alternative_memory_ids: &alternative_memory_ids,
547 dependency_edge_digests: &[],
548 evidence_sufficient,
549 unresolved_conflict,
550 invalid_lineage,
551 budget_exhausted,
552 };
553 let receipt_digest = blake3::hash(
554 &serde_json::to_vec(&input)
555 .map_err(|e| MemoryError::Other(format!("receipt serialization failed: {e}")))?,
556 )
557 .to_hex()
558 .to_string();
559 let receipt_id = format!("state-resolution:{receipt_digest}");
560 Ok(Self {
561 schema_version,
562 receipt_id,
563 request_id,
564 state_view,
565 resolution_mode,
566 premise_status,
567 answer_disposition,
568 resolved_memory_ids,
569 alternative_memory_ids,
570 dependency_edge_digests: Vec::new(),
571 evidence_sufficient,
572 unresolved_conflict,
573 invalid_lineage,
574 budget_exhausted,
575 evaluated_at,
576 receipt_digest,
577 stage_outcomes: vec![("state_resolution".into(), StageOutcomeV1::Applied)],
578 degradations: Vec::new(),
579 })
580 }
581
582 pub fn new(
583 request_id: impl Into<String>,
584 state_view: StateView,
585 resolution_mode: StateResolutionMode,
586 premise_status: PremiseStatus,
587 answer_disposition: AnswerDisposition,
588 resolved_memory_ids: Vec<String>,
589 alternative_memory_ids: Vec<String>,
590 evidence_sufficient: bool,
591 unresolved_conflict: bool,
592 invalid_lineage: bool,
593 budget_exhausted: bool,
594 ) -> Result<Self, MemoryError> {
595 Self::new_at(
596 request_id,
597 state_view,
598 resolution_mode,
599 premise_status,
600 answer_disposition,
601 resolved_memory_ids,
602 alternative_memory_ids,
603 evidence_sufficient,
604 unresolved_conflict,
605 invalid_lineage,
606 budget_exhausted,
607 Utc::now().to_rfc3339(),
608 )
609 }
610}
611
612fn validate_state_view(view: &StateView) -> Result<(), MemoryError> {
613 if let StateView::HistoricalAt(value) | StateView::RecordedAsOf(value) = view {
614 DateTime::parse_from_rfc3339(value).map_err(|e| {
615 MemoryError::Other(format!("invalid StateView timestamp '{value}': {e}"))
616 })?;
617 }
618 Ok(())
619}
620
621#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
622pub struct ResolvedMemoryAnswerV1 {
623 pub schema_version: String,
624 pub query: String,
625 pub answer: Option<String>,
626 pub premise_status: PremiseStatus,
627 pub answer_disposition: AnswerDisposition,
628 pub state_view: StateView,
629 pub assertions: Vec<ResolvedAssertionV1>,
630 pub alternatives: Vec<BeliefAlternativeV1>,
631 pub receipt: StateResolutionReceiptV1,
632 pub retrieval_witness: RetrievalWitnessV1,
633}
634
635#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
636pub struct StateResolvedRetrievalResponseV1<T> {
637 pub schema_version: String,
638 pub state_view: StateView,
639 pub results: Vec<T>,
640 pub retrieval_witness: RetrievalWitnessV1,
641 pub resolution_receipt: StateResolutionReceiptV1,
642}
643
644fn edge_from_stored(edge: &crate::StoredGraphEdge) -> Option<StateDependencyEdgeV1> {
645 let graph_type = edge
646 .edge_type_parsed
647 .clone()
648 .or_else(|| serde_json::from_str(&edge.edge_type).ok())?;
649 let GraphEdgeType::Entity { relation } = graph_type else {
650 return None;
651 };
652 match relation.as_str() {
653 "invalidates" => Some(StateDependencyEdgeV1::invalidates(
654 &edge.source,
655 &edge.target,
656 )),
657 "weakens" => Some(StateDependencyEdgeV1::weakens(&edge.source, &edge.target)),
658 "requires_reevaluation" => Some(StateDependencyEdgeV1::requires_reevaluation(
659 &edge.source,
660 &edge.target,
661 )),
662 "scope_changes" => {
663 let metadata = edge
664 .metadata
665 .as_deref()
666 .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok());
667 Some(StateDependencyEdgeV1::scope_changes(
668 &edge.source,
669 &edge.target,
670 metadata
671 .as_ref()
672 .and_then(|v| v.get("from_scope"))
673 .and_then(|v| v.as_str())
674 .unwrap_or("unknown"),
675 metadata
676 .as_ref()
677 .and_then(|v| v.get("to_scope"))
678 .and_then(|v| v.as_str())
679 .unwrap_or("unknown"),
680 ))
681 }
682 "derived_from_state" => Some(StateDependencyEdgeV1::derived_from_state(
683 &edge.source,
684 &edge.target,
685 )),
686 _ => None,
687 }
688}
689
690fn result_id(result: &SearchResult) -> String {
691 result.source.result_id()
692}
693
694fn result_status(result: &SearchResult, resolution: &DependencyResolutionV1) -> PremiseStatus {
695 let id = match &result.source {
696 SearchSource::Fact { fact_id, .. } => format!("fact:{fact_id}"),
697 _ => result_id(result),
698 };
699 match resolution.status(&id) {
700 Some(DependencyState::Invalid) => PremiseStatus::Stale,
701 Some(DependencyState::Uncertain) => PremiseStatus::Ambiguous,
702 Some(DependencyState::Pending) => PremiseStatus::Unsupported,
703 Some(DependencyState::Valid) => PremiseStatus::Supported,
704 None => PremiseStatus::Supported,
705 }
706}
707
708pub(crate) async fn witnessed_retrieval(
709 store: &MemoryStore,
710 request_id: String,
711 query: &str,
712 results: &[SearchResult],
713) -> Result<RetrievalWitnessV1, MemoryError> {
714 let query_digest = blake3::hash(query.as_bytes()).to_hex().to_string();
715 let result_ids = results.iter().map(result_id).collect::<Vec<_>>();
716 let result_digests = results
717 .iter()
718 .map(|result| blake3::hash(result.content.as_bytes()).to_hex().to_string())
719 .collect::<Vec<_>>();
720 let (epoch, heads) = store
721 .with_read_conn(move |conn| {
722 let epoch: i64 = conn.query_row(
723 "SELECT retrieval_epoch FROM authority_state WHERE id = 1",
724 [],
725 |row| row.get(0),
726 )?;
727 let heads: Vec<(String, String)> = conn
728 .prepare(
729 "SELECT lineage_id, active_head_id FROM authority_lineages ORDER BY lineage_id",
730 )?
731 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
732 .collect::<Result<Vec<_>, _>>()?;
733 let epoch = u64::try_from(epoch)
734 .map_err(|_| MemoryError::Other("negative authority epoch".into()))?;
735 Ok((epoch, heads))
736 })
737 .await?;
738 let snapshot_digest = blake3::hash(
739 &serde_json::to_vec(&(epoch, &heads))
740 .map_err(|e| MemoryError::Other(format!("snapshot serialization failed: {e}")))?,
741 )
742 .to_hex()
743 .to_string();
744 Ok(RetrievalWitnessV1 {
745 schema_version: "retrieval_witness_v1".into(),
746 request_id,
747 evaluated_at: Utc::now().to_rfc3339(),
748 authority_snapshot_id: AuthoritySnapshotId(format!("epoch:{epoch}:{snapshot_digest}")),
749 retrieval_epoch: RetrievalEpoch(epoch),
750 query_digest,
751 config_digest: blake3::hash(b"state-resolution-search-v1")
752 .to_hex()
753 .to_string(),
754 ordered_result_ids: result_ids,
755 ordered_result_digests: result_digests,
756 stage_outcomes: vec![
757 ("retrieval".into(), StageOutcomeV1::Applied),
758 ("state_resolution".into(), StageOutcomeV1::Applied),
759 ],
760 degradations: Vec::new(),
761 cached_witness_parent: None,
762 })
763}
764
765impl MemoryStore {
766 pub async fn resolve_memory(
769 &self,
770 query: &str,
771 top_k: Option<usize>,
772 namespaces: Option<&[&str]>,
773 mode: StateResolutionMode,
774 budget: usize,
775 ) -> Result<ResolvedMemoryAnswerV1, MemoryError> {
776 let state_view = mode.state_view();
777 let results = self
778 .search_with_view(query, top_k, namespaces, None, state_view.clone())
779 .await?;
780 let result_ids: Vec<String> = results.iter().map(result_id).collect();
781 let fact_ids: Vec<String> = results
782 .iter()
783 .filter_map(|result| match &result.source {
784 SearchSource::Fact { fact_id, .. } => Some(format!("fact:{fact_id}")),
785 _ => None,
786 })
787 .collect();
788 let stored_edges = if fact_ids.is_empty() {
789 Vec::new()
790 } else {
791 self.list_graph_edges_for_neighborhood(fact_ids.clone(), 2, budget.max(1))
792 .await?
793 };
794 let dependency_edges: Vec<StateDependencyEdgeV1> =
795 stored_edges.iter().filter_map(edge_from_stored).collect();
796 let mut resolution = resolve_dependency_states(&dependency_edges, &fact_ids, budget);
797 let budget_exhausted =
798 resolution.budget_exhausted || (budget > 0 && results.len() > budget);
799 if budget_exhausted {
800 resolution.budget_exhausted = true;
801 if resolution.premise_status == PremiseStatus::Supported {
802 resolution.premise_status = PremiseStatus::Unsupported;
803 }
804 }
805 let evidence_sufficient = !results.is_empty();
806 let decision = answer_policy_for(
807 resolution.premise_status,
808 evidence_sufficient,
809 resolution.unresolved_conflict,
810 resolution.invalid_lineage,
811 resolution.budget_exhausted,
812 );
813 let request_id = blake3::hash(query.as_bytes()).to_hex().to_string();
814 let retrieval_witness = witnessed_retrieval(self, request_id, query, &results).await?;
815 let assertions: Vec<ResolvedAssertionV1> = results
816 .iter()
817 .map(|result| ResolvedAssertionV1 {
818 schema_version: "resolved_assertion_v1".into(),
819 memory_id: result_id(result),
820 content: result.content.clone(),
821 premise_status: result_status(result, &resolution),
822 state_view: state_view.clone(),
823 source_kind: result.source.source_kind().into(),
824 })
825 .collect();
826 let resolved_memory_ids = assertions
827 .iter()
828 .map(|a| a.memory_id.clone())
829 .collect::<Vec<_>>();
830 let alternatives = assertions
831 .iter()
832 .skip(1)
833 .cloned()
834 .map(|assertion| BeliefAlternativeV1 {
835 assertion,
836 reason: "additional witnessed retrieval candidate".into(),
837 })
838 .collect::<Vec<_>>();
839 let alternative_memory_ids = alternatives
840 .iter()
841 .map(|a| a.assertion.memory_id.clone())
842 .collect::<Vec<_>>();
843 let mut receipt = StateResolutionReceiptV1::new(
844 blake3::hash(query.as_bytes()).to_hex().to_string(),
845 state_view.clone(),
846 mode,
847 resolution.premise_status,
848 decision.disposition,
849 resolved_memory_ids,
850 alternative_memory_ids,
851 evidence_sufficient,
852 resolution.unresolved_conflict,
853 resolution.invalid_lineage,
854 resolution.budget_exhausted,
855 )?;
856 receipt = receipt.with_dependency_edge_digests(
857 dependency_edges
858 .iter()
859 .map(StateDependencyEdgeV1::digest)
860 .collect(),
861 )?;
862 let answer = assertions
863 .first()
864 .map(|assertion| assertion.content.clone());
865 Ok(ResolvedMemoryAnswerV1 {
866 schema_version: STATE_RESOLVED_RETRIEVAL_V1.into(),
867 query: query.into(),
868 answer,
869 premise_status: resolution.premise_status,
870 answer_disposition: decision.disposition,
871 state_view,
872 assertions,
873 alternatives,
874 receipt,
875 retrieval_witness,
876 })
877 }
878
879 pub async fn add_state_dependency_edge(
881 &self,
882 edge: StateDependencyEdgeV1,
883 weight: f64,
884 ) -> Result<crate::StoredGraphEdge, MemoryError> {
885 let metadata = match &edge {
886 StateDependencyEdgeV1::ScopeChanges {
887 from_scope,
888 to_scope,
889 ..
890 } => Some(serde_json::json!({"from_scope": from_scope, "to_scope": to_scope})),
891 _ => None,
892 };
893 self.add_graph_edge(
894 edge.source(),
895 edge.target(),
896 GraphEdgeType::Entity {
897 relation: edge.relation().into(),
898 },
899 weight,
900 metadata,
901 )
902 .await
903 }
904}